goportfolio

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2025 License: MIT-0 Imports: 11 Imported by: 1

README

goportfolio

goportfolio is a small, thread-safe helper for tracking balances, open positions, transactions, and mark-to-market values in trading tools or bots. It focuses on readability and a minimal surface area so it can be dropped into prototypes without ceremony.

Features

  • Gorilla-safe data structure: wraps every balance/position mutation with sync.RWMutex to keep concurrent readers/writers happy.
  • Basic position accounting: create, update, close, and inspect positions with realized/unrealized PnL fields for downstream consumers.
  • Transaction ledger: append-only slice that can be copied out for reporting or persistence layers.
  • Balance/position DTO snapshots: Snapshot, BalancesSnapshot, and PositionsSnapshot hand you serialization-ready copies without exposing internal maps.
  • Snapshot diffs: every PortfolioEvent carries a SnapshotDiff payload so streaming consumers can apply compact balance/position/transaction patches instead of reloading the full state.
  • Configurable pair parsing: pass WithPairParser to NewPortfolioWithOptions to support venues that do not use the default BASE/QUOTE slash format.
  • Fee schedules: describe fees per symbol/base/quote (or default) once and let the portfolio apply them for every transaction.
  • Persistence hooks: plug in JSON, BoltDB, or SQLite stores via WithSnapshotStore to automatically persist and rehydrate state between restarts.
  • Streaming events: Subscribe yields a channel that receives balance/position/transaction events for real-time UIs or alerting.
  • Portfolio valuation helper: converts every asset to a chosen quote asset using caller-provided price data and sums the result.

Getting Started

git clone https://github.com/evdnx/goportfolio.git
cd goportfolio
go test ./...

go.mod targets Go 1.25, but the code is free of 1.25-specific features and should work on any maintained Go release.

Usage

package main

import (
	"fmt"
	"log"
	"strings"
	"time"

	"github.com/evdnx/goportfolio"
)

func main() {
	customParser := func(symbol string) (string, string, bool) {
		parts := strings.Split(symbol, "-")
		if len(parts) != 2 {
			return "", "", false
		}
		return parts[0], parts[1], true
	}

	feeSchedule := &goportfolio.FeeSchedule{
		Symbols: map[string]float64{
			"CFX/USDT": 1, // flat fee applied automatically
		},
	}

	store := goportfolio.NewJSONFileStore("portfolio.json")

	p := goportfolio.NewPortfolioWithOptions(
		goportfolio.WithPairParser(customParser),
		goportfolio.WithFeeSchedule(feeSchedule),
		goportfolio.WithSnapshotStore(store),
	)
	if err := p.SetBalance("USDT", 1000); err != nil {
		log.Fatalf("seed balance: %v", err)
	}

	if err := p.AddTransaction(goportfolio.Transaction{
		ID:        "tx1",
		Symbol:    "CFX-USDT",
		Type:      "buy",
		Quantity:  10,
		Price:     2.5,
		Timestamp: time.Now(),
	}); err != nil {
		log.Fatalf("add transaction: %v", err)
	}

	priceBook := map[string]float64{
		"CFX/USDT": 2.7,
	}
	fmt.Printf("Total value in USDT: %.2f\n", p.GetTotalValue("USDT", priceBook))

	fmt.Printf("Balances DTOs: %+v\n", p.BalancesSnapshot())
	fmt.Printf("Positions DTOs: %+v\n", p.PositionsSnapshot())

	if err := p.LoadFromStore(); err != nil {
		log.Fatalf("rehydrate: %v", err)
	}

	events, cancel := p.Subscribe(10)
	defer cancel()

	if err := p.SetBalance("USDT", 750); err != nil {
		log.Fatalf("update balance: %v", err)
	}

	select {
	case evt := <-events:
		fmt.Printf("Event: %s %#v\n", evt.Type, evt.Balance)
		fmt.Printf("Diff: %#v\n", evt.Diff)
	case <-time.After(2 * time.Second):
		fmt.Println("no events received")
	}
}

Inspect the SnapshotDiff on every event to fan out only the balances, positions, or transaction slices that actually changed and ignore the rest of the snapshot payloads.

Testing

The project ships with lightweight unit tests that cover transaction handling edge cases. Run them with:

go test ./...

Roadmap Ideas

These are intentionally out of scope for the current release, but would make natural extensions:

  • Built-in persistence migrations to evolve stored snapshots without forcing manual wipes.

License

MIT-0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type BalanceChange

type BalanceChange struct {
	Asset   string  `json:"asset"`
	Amount  float64 `json:"amount,omitempty"`
	Deleted bool    `json:"deleted,omitempty"`
}

BalanceChange flags an asset balance that was added/updated/removed.

type BalanceSnapshot

type BalanceSnapshot struct {
	Asset  string  `json:"asset"`
	Amount float64 `json:"amount"`
}

BalanceSnapshot is a DTO representing a copy of an asset balance.

type BoltDBStore

type BoltDBStore struct {
	// contains filtered or unexported fields
}

BoltDBStore persists snapshots inside a BoltDB bucket.

func NewBoltDBStore

func NewBoltDBStore(path string) (*BoltDBStore, error)

NewBoltDBStore opens a BoltDB database at the supplied path.

func (*BoltDBStore) Close

func (s *BoltDBStore) Close() error

Close releases the underlying BoltDB handle.

func (*BoltDBStore) Load

func (s *BoltDBStore) Load() (PortfolioSnapshot, error)

Load fetches the snapshot bytes from BoltDB.

func (*BoltDBStore) Save

func (s *BoltDBStore) Save(snapshot PortfolioSnapshot) error

Save writes the snapshot bytes into BoltDB.

type EventType

type EventType string

EventType identifies the kind of portfolio mutation.

const (
	EventBalanceChanged    EventType = "balance_changed"
	EventPositionChanged   EventType = "position_changed"
	EventTransactionAdded  EventType = "transaction_added"
	EventPortfolioRestored EventType = "portfolio_restored"
)

type FeeSchedule

type FeeSchedule struct {
	Symbols    map[string]float64
	Bases      map[string]float64
	Quotes     map[string]float64
	Default    float64
	HasDefault bool
}

FeeSchedule describes per-symbol/per-asset fee settings.

func (*FeeSchedule) Resolve

func (fs *FeeSchedule) Resolve(symbol, base, quote string) (float64, bool)

Resolve attempts to find a fee for the provided symbol/base/quote tuple.

type JSONFileStore

type JSONFileStore struct {
	Path string
}

JSONFileStore persists snapshots to a local JSON file.

func NewJSONFileStore

func NewJSONFileStore(path string) *JSONFileStore

NewJSONFileStore creates a JSON-backed snapshot store.

func (*JSONFileStore) Load

func (s *JSONFileStore) Load() (PortfolioSnapshot, error)

Load reads the snapshot from disk. Missing files return the zero snapshot.

func (*JSONFileStore) Save

func (s *JSONFileStore) Save(snapshot PortfolioSnapshot) error

Save writes the snapshot to disk using an atomic rename.

type Option

type Option func(*Portfolio)

Option configures a Portfolio instance.

func WithFeeSchedule

func WithFeeSchedule(schedule *FeeSchedule) Option

WithFeeSchedule configures the portfolio with a fee schedule.

func WithPairParser

func WithPairParser(parser PairParser) Option

WithPairParser overrides the default BASE/QUOTE parser used for balance updates.

func WithRiskPolicy

func WithRiskPolicy(policy *RiskPolicy) Option

WithRiskPolicy configures the portfolio with risk limits.

func WithSnapshotStore

func WithSnapshotStore(store SnapshotStore) Option

WithSnapshotStore wires a persistence layer that receives snapshots after every mutation.

type PairParser

type PairParser func(symbol string) (baseAsset, quoteAsset string, ok bool)

PairParser resolves a trading symbol (e.g. "BTC/USDT") into its base and quote assets. Implementations should return ok=false if they cannot derive a valid pair so callers can surface an error.

type Portfolio

type Portfolio struct {
	// contains filtered or unexported fields
}

Portfolio manages the trading portfolio

func NewPortfolio

func NewPortfolio() *Portfolio

NewPortfolio creates a new portfolio

func NewPortfolioWithOptions

func NewPortfolioWithOptions(options ...Option) *Portfolio

NewPortfolioWithOptions creates a portfolio configured by the supplied options.

func (*Portfolio) AddTransaction

func (p *Portfolio) AddTransaction(transaction Transaction) error

AddTransaction adds a transaction to the portfolio and updates balances/positions.

func (*Portfolio) BalancesSnapshot

func (p *Portfolio) BalancesSnapshot() []BalanceSnapshot

BalancesSnapshot returns a slice of balances safe for serialization.

func (*Portfolio) ClosePosition

func (p *Portfolio) ClosePosition(symbol string) error

ClosePosition closes a position

func (*Portfolio) GetAllPositions

func (p *Portfolio) GetAllPositions() map[string]Position

GetAllPositions returns all positions

func (*Portfolio) GetBalance

func (p *Portfolio) GetBalance(asset string) float64

GetBalance returns the balance of an asset

func (*Portfolio) GetPosition

func (p *Portfolio) GetPosition(symbol string) (Position, bool)

GetPosition returns a position by symbol

func (*Portfolio) GetTotalValue

func (p *Portfolio) GetTotalValue(quoteAsset string, prices map[string]float64) float64

GetTotalValue returns the total portfolio value in the specified quote asset

func (*Portfolio) GetTransactions

func (p *Portfolio) GetTransactions() []Transaction

GetTransactions returns all transactions

func (*Portfolio) LoadFromStore

func (p *Portfolio) LoadFromStore() error

LoadFromStore pulls the most recent snapshot from the configured store.

func (*Portfolio) PositionsSnapshot

func (p *Portfolio) PositionsSnapshot() []PositionSnapshot

PositionsSnapshot returns a slice of Position DTOs safe for serialization.

func (*Portfolio) Restore

func (p *Portfolio) Restore(snapshot PortfolioSnapshot)

Restore replaces the current in-memory state with the provided snapshot.

func (*Portfolio) SetBalance

func (p *Portfolio) SetBalance(asset string, amount float64) error

SetBalance sets the balance of an asset and persists/emits events if configured.

func (*Portfolio) SetDefaultFee

func (p *Portfolio) SetDefaultFee(fee float64)

SetDefaultFee updates the default fee applied when no symbol/base/quote match is found.

func (*Portfolio) SetFeeForBase

func (p *Portfolio) SetFeeForBase(base string, fee float64)

SetFeeForBase wires a fee that is applied to any symbol with the provided base asset.

func (*Portfolio) SetFeeForQuote

func (p *Portfolio) SetFeeForQuote(quote string, fee float64)

SetFeeForQuote wires a fee that is applied to any symbol with the provided quote asset.

func (*Portfolio) SetFeeForSymbol

func (p *Portfolio) SetFeeForSymbol(symbol string, fee float64)

SetFeeForSymbol wires a fixed fee for a specific trading symbol.

func (*Portfolio) SetFeeSchedule

func (p *Portfolio) SetFeeSchedule(schedule *FeeSchedule)

SetFeeSchedule replaces the existing fee schedule at runtime.

func (*Portfolio) SetRiskPolicy

func (p *Portfolio) SetRiskPolicy(policy *RiskPolicy)

SetRiskPolicy updates the risk controls enforced for subsequent transactions.

func (*Portfolio) Snapshot

func (p *Portfolio) Snapshot() PortfolioSnapshot

Snapshot returns a deep copy of the portfolio state.

func (*Portfolio) Subscribe

func (p *Portfolio) Subscribe(buffer int) (<-chan PortfolioEvent, func())

Subscribe returns a channel for streaming portfolio events and an unsubscribe function.

func (*Portfolio) UpdatePosition

func (p *Portfolio) UpdatePosition(position Position) error

UpdatePosition upserts a position and notifies subscribers.

type PortfolioEvent

type PortfolioEvent struct {
	Type        EventType          `json:"type"`
	Balance     *BalanceSnapshot   `json:"balance,omitempty"`
	Position    *PositionSnapshot  `json:"position,omitempty"`
	Transaction *Transaction       `json:"transaction,omitempty"`
	Snapshot    *PortfolioSnapshot `json:"snapshot,omitempty"`
	Diff        *SnapshotDiff      `json:"diff,omitempty"`
	Timestamp   time.Time          `json:"timestamp"`
	Message     string             `json:"message,omitempty"`
}

PortfolioEvent is sent to subscribers whenever the portfolio mutates.

type PortfolioSnapshot

type PortfolioSnapshot struct {
	Balances     []BalanceSnapshot  `json:"balances"`
	Positions    []PositionSnapshot `json:"positions"`
	Transactions []Transaction      `json:"transactions"`
}

PortfolioSnapshot represents a serializable copy of the portfolio.

type Position

type Position struct {
	Symbol        string
	EntryPrice    float64
	CurrentPrice  float64
	Quantity      float64
	OpenTime      time.Time
	LastUpdated   time.Time
	RealizedPnL   float64
	UnrealizedPnL float64
}

Position represents a trading position

type PositionChange

type PositionChange struct {
	Symbol   string            `json:"symbol"`
	Position *PositionSnapshot `json:"position,omitempty"`
	Deleted  bool              `json:"deleted,omitempty"`
}

PositionChange reports a position mutation. The snapshot is nil when deleted.

type PositionSnapshot

type PositionSnapshot struct {
	Symbol        string    `json:"symbol"`
	EntryPrice    float64   `json:"entryPrice"`
	CurrentPrice  float64   `json:"currentPrice"`
	Quantity      float64   `json:"quantity"`
	OpenTime      time.Time `json:"openTime"`
	LastUpdated   time.Time `json:"lastUpdated"`
	RealizedPnL   float64   `json:"realizedPnl"`
	UnrealizedPnL float64   `json:"unrealizedPnl"`
}

PositionSnapshot is a DTO mirroring Position but intended for serialization.

type RiskPolicy

type RiskPolicy struct {
	MaxExposure         float64
	MaxExposureBySymbol map[string]float64
	DailyLossLimit      float64
}

RiskPolicy defines the exposure and loss limits enforced before recording trades.

type SQLiteStore

type SQLiteStore struct {
	// contains filtered or unexported fields
}

SQLiteStore persists snapshots to an embedded SQLite database.

func NewSQLiteStore

func NewSQLiteStore(path string) (*SQLiteStore, error)

NewSQLiteStore opens/creates an on-disk SQLite database at path.

func (*SQLiteStore) Close

func (s *SQLiteStore) Close() error

Close releases the SQLite database handle.

func (*SQLiteStore) Load

func (s *SQLiteStore) Load() (PortfolioSnapshot, error)

Load fetches the snapshot from SQLite.

func (*SQLiteStore) Save

func (s *SQLiteStore) Save(snapshot PortfolioSnapshot) error

Save writes the snapshot into SQLite using an UPSERT.

type SnapshotDiff

type SnapshotDiff struct {
	Balances             []BalanceChange  `json:"balances,omitempty"`
	Positions            []PositionChange `json:"positions,omitempty"`
	TransactionsAppended []Transaction    `json:"transactionsAppended,omitempty"`
	TransactionsReset    bool             `json:"transactionsReset,omitempty"`
}

SnapshotDiff captures the mutations between two snapshots so downstream consumers can update their mirrors without reprocessing the entire state.

func DiffSnapshots

func DiffSnapshots(prev, next PortfolioSnapshot) SnapshotDiff

DiffSnapshots compares two snapshots and emits the minimal change-set needed to transform prev into next.

func (SnapshotDiff) IsEmpty

func (d SnapshotDiff) IsEmpty() bool

IsEmpty reports whether the diff contains any useful information.

type SnapshotStore

type SnapshotStore interface {
	Save(snapshot PortfolioSnapshot) error
	Load() (PortfolioSnapshot, error)
}

SnapshotStore persists and loads portfolio snapshots.

type Transaction

type Transaction struct {
	ID        string
	Symbol    string
	Type      string // "buy", "sell"
	Quantity  float64
	Price     float64
	Fee       float64 // optional; resolved via fee schedule when zero
	Timestamp time.Time
}

Transaction represents a portfolio transaction

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL