package market import ( "fmt" "math" "sync" "time" "git.mleku.dev/mleku/dendrite/pkg/axiom" ) // OrderSide is buy or sell for trade execution. type OrderSide int const ( Buy OrderSide = iota Sell ) func (s OrderSide) String() string { if s == Buy { return "buy" } return "sell" } // TradeOrder is a request to execute a trade. type TradeOrder struct { ID string Asset string Side OrderSide Venue Venue Price float64 // limit price; 0 = market Quantity float64 CreatedAt time.Time } // Fill is the result of an executed order. type Fill struct { OrderID string Asset string Side OrderSide Venue Venue Price float64 Quantity float64 Fee float64 FilledAt time.Time } // Position tracks the organism's exposure in a single asset. type Position struct { Asset string Quantity float64 // positive = long, negative = short AvgEntry float64 // weighted average entry price Realized float64 // cumulative realized P&L FillCount int } // MarkToMarket returns unrealized P&L at the given market price. func (p *Position) MarkToMarket(marketPrice float64) float64 { if p.Quantity == 0 { return 0 } return p.Quantity * (marketPrice - p.AvgEntry) } // TotalPnL returns realized + unrealized P&L at the given market price. func (p *Position) TotalPnL(marketPrice float64) float64 { return p.Realized + p.MarkToMarket(marketPrice) } // ApplyFill updates the position based on a trade fill. func (p *Position) ApplyFill(f *Fill) { qty := f.Quantity if f.Side == Sell { qty = -qty } if p.Quantity == 0 { // Opening position. p.AvgEntry = f.Price p.Quantity = qty } else if (p.Quantity > 0 && qty > 0) || (p.Quantity < 0 && qty < 0) { // Adding to position — update weighted average. totalQty := p.Quantity + qty p.AvgEntry = (p.AvgEntry*p.Quantity + f.Price*qty) / totalQty p.Quantity = totalQty } else { // Reducing or closing — realize P&L. closingQty := qty if math.Abs(qty) > math.Abs(p.Quantity) { closingQty = -p.Quantity // close only existing position } p.Realized += -closingQty * (f.Price - p.AvgEntry) p.Quantity += qty // If position flipped, reset avg entry. if (p.Quantity > 0 && qty > 0) || (p.Quantity < 0 && qty < 0) { p.AvgEntry = f.Price } } p.Realized -= f.Fee p.FillCount++ } // Portfolio tracks all positions and aggregate economics. type Portfolio struct { mu sync.Mutex positions map[string]*Position // asset → position fills []Fill costs CostLedger } // CostLedger tracks operational costs: API fees, compute, bandwidth. type CostLedger struct { ExchangeFees float64 APIFees float64 Compute float64 Bandwidth float64 } // Total returns total operational cost. func (c *CostLedger) Total() float64 { return c.ExchangeFees + c.APIFees + c.Compute + c.Bandwidth } // NewPortfolio creates an empty portfolio. func NewPortfolio() *Portfolio { return &Portfolio{ positions: make(map[string]*Position), } } // RecordFill applies a fill to the appropriate position. func (pf *Portfolio) RecordFill(f Fill) { pf.mu.Lock() defer pf.mu.Unlock() pos, ok := pf.positions[f.Asset] if !ok { pos = &Position{Asset: f.Asset} pf.positions[f.Asset] = pos } pos.ApplyFill(&f) pf.fills = append(pf.fills, f) pf.costs.ExchangeFees += f.Fee } // AddCost records a non-trade cost. func (pf *Portfolio) AddCost(apiCost, computeCost, bandwidthCost float64) { pf.mu.Lock() defer pf.mu.Unlock() pf.costs.APIFees += apiCost pf.costs.Compute += computeCost pf.costs.Bandwidth += bandwidthCost } // Position returns the position for an asset, or nil. func (pf *Portfolio) Position(asset string) *Position { pf.mu.Lock() defer pf.mu.Unlock() return pf.positions[asset] } // TotalRealized returns aggregate realized P&L across all positions. func (pf *Portfolio) TotalRealized() float64 { pf.mu.Lock() defer pf.mu.Unlock() total := 0.0 for _, p := range pf.positions { total += p.Realized } return total } // TotalCosts returns aggregate operational costs. func (pf *Portfolio) TotalCosts() CostLedger { pf.mu.Lock() defer pf.mu.Unlock() return pf.costs } // FillCount returns total number of fills recorded. func (pf *Portfolio) FillCount() int { pf.mu.Lock() defer pf.mu.Unlock() return len(pf.fills) } // WinLossCounts returns the number of positions with positive and negative realized P&L. func (pf *Portfolio) WinLossCounts() (wins, losses int) { pf.mu.Lock() defer pf.mu.Unlock() for _, p := range pf.positions { if p.Realized > 0 { wins++ } else if p.Realized < 0 { losses++ } } return } // FlattenAll closes all open positions at the given market prices. // Returns the number of positions flattened and total P&L realized. // This is used at the end of each generation to start the next one clean. func (pf *Portfolio) FlattenAll(prices map[string]float64) (int, float64) { pf.mu.Lock() defer pf.mu.Unlock() flattened := 0 totalPnL := 0.0 for asset, pos := range pf.positions { if pos.Quantity == 0 { continue } price, ok := prices[asset] if !ok { continue } // Create a closing fill. side := Sell qty := pos.Quantity if qty < 0 { side = Buy qty = -qty } f := Fill{ Asset: asset, Side: side, Price: price, Quantity: qty, Fee: 0, // no fee for internal flattening FilledAt: time.Now(), } pnl := pos.MarkToMarket(price) pos.ApplyFill(&f) pf.fills = append(pf.fills, f) totalPnL += pnl flattened++ } return flattened, totalPnL } // AggregateExposure returns total absolute notional exposure across all positions. func (pf *Portfolio) AggregateExposure() float64 { pf.mu.Lock() defer pf.mu.Unlock() total := 0.0 for _, p := range pf.positions { total += math.Abs(p.Quantity * p.AvgEntry) } return total } // EconomicFitness returns revenue minus all costs for a generation. // Positive is coherent. Negative is incoherent. Zero is homeostasis. type EconomicFitness struct { Generation int Realized float64 // realized trading P&L Costs CostLedger // operational costs TradeCount int WinCount int LossCount int Time time.Time } // NetRevenue returns realized P&L minus total operational cost. func (ef *EconomicFitness) NetRevenue() float64 { return ef.Realized - ef.Costs.Total() } // WinRate returns the fraction of profitable trades. func (ef *EconomicFitness) WinRate() float64 { if ef.TradeCount == 0 { return 0 } return float64(ef.WinCount) / float64(ef.TradeCount) } // IsViable returns true if the organism covers its own costs. func (ef *EconomicFitness) IsViable() bool { return ef.NetRevenue() >= 0 } // FillToElements decomposes a trade fill into lattice elements. func FillToElements(f *Fill) []axiom.Element { return []axiom.Element{ element{"asset", f.Asset}, element{"trade-side", f.Side.String()}, element{"trade-price", f.Price}, element{"trade-quantity", f.Quantity}, element{"trade-fee", f.Fee}, element{"venue", string(f.Venue)}, element{"timestamp", fmt.Sprintf("%d", f.FilledAt.Unix())}, } } // EconomicFitnessToElements decomposes economic fitness into lattice elements. func EconomicFitnessToElements(ef *EconomicFitness) []axiom.Element { return []axiom.Element{ element{"generation", ef.Generation}, element{"realized-pnl", ef.Realized}, element{"net-revenue", ef.NetRevenue()}, element{"trade-count", ef.TradeCount}, element{"win-rate", ef.WinRate()}, element{"exchange-fees", ef.Costs.ExchangeFees}, element{"api-fees", ef.Costs.APIFees}, element{"economic-viable", ef.IsViable()}, element{"timestamp", fmt.Sprintf("%d", ef.Time.Unix())}, } }