package market import ( "fmt" "os" "sync" "time" ) // ExecutionMode determines whether the organism trades live or on paper. type ExecutionMode int const ( PaperTrading ExecutionMode = iota LiveTrading ) func (m ExecutionMode) String() string { if m == PaperTrading { return "paper" } return "live" } // Executor manages trade execution with safety constraints. // The organism proposes trades; the executor applies limits, circuit breakers, // and kill file checks before executing. type Executor struct { mu sync.Mutex Mode ExecutionMode Portfolio *Portfolio KillFilePath string // if this file exists, halt all trading MaxPerTrade float64 // maximum notional value per trade MaxAggregate float64 // maximum aggregate exposure DrawdownLimit float64 // halt if cumulative loss exceeds this fraction of capital Capital float64 // operator-allocated capital LogPath string // Callback for live execution. The organism provides this. // For paper trading, fills are simulated internally. Execute func(order TradeOrder) (*Fill, error) generation int halted bool haltReason string } // NewExecutor creates an executor in paper trading mode with sensible defaults. func NewExecutor(capital float64) *Executor { return &Executor{ Mode: PaperTrading, Portfolio: NewPortfolio(), KillFilePath: "_output/STOP", MaxPerTrade: capital * 0.02, // 2% per trade MaxAggregate: capital * 0.20, // 20% total exposure DrawdownLimit: 0.05, // 5% drawdown halts trading Capital: capital, } } // SetGeneration advances the generation counter. func (ex *Executor) SetGeneration(gen int) { ex.mu.Lock() defer ex.mu.Unlock() ex.generation = gen } // IsHalted returns whether trading is halted and why. func (ex *Executor) IsHalted() (bool, string) { ex.mu.Lock() defer ex.mu.Unlock() return ex.halted, ex.haltReason } // Submit proposes a trade. The executor applies all safety checks before // executing. Returns the fill if executed, nil if rejected. func (ex *Executor) Submit(order TradeOrder) (*Fill, error) { ex.mu.Lock() defer ex.mu.Unlock() // Kill file check. if ex.killFileExists() { ex.halted = true ex.haltReason = "kill file" return nil, fmt.Errorf("trading halted: kill file exists") } // Check halt state. if ex.halted { return nil, fmt.Errorf("trading halted: %s", ex.haltReason) } // Position size limit. notional := order.Price * order.Quantity if notional > ex.MaxPerTrade { return nil, fmt.Errorf("order notional %.2f exceeds per-trade limit %.2f", notional, ex.MaxPerTrade) } // Aggregate exposure limit. currentExposure := ex.Portfolio.AggregateExposure() if currentExposure+notional > ex.MaxAggregate { return nil, fmt.Errorf("aggregate exposure %.2f + order %.2f exceeds limit %.2f", currentExposure, notional, ex.MaxAggregate) } // Execute. var fill *Fill var err error if ex.Mode == PaperTrading { fill = ex.simulateFill(order) } else { if ex.Execute == nil { return nil, fmt.Errorf("live trading requires Execute callback") } fill, err = ex.Execute(order) if err != nil { return nil, fmt.Errorf("execution failed: %w", err) } } // Record the fill. ex.Portfolio.RecordFill(*fill) // Drawdown circuit breaker. realized := ex.Portfolio.TotalRealized() if ex.Capital > 0 && realized < 0 && (-realized/ex.Capital) > ex.DrawdownLimit { ex.halted = true ex.haltReason = fmt.Sprintf("drawdown %.2f%% exceeds limit %.2f%%", (-realized/ex.Capital)*100, ex.DrawdownLimit*100) } // Log the trade. ex.logTrade(order, fill) return fill, nil } // Resume clears the halt state. Requires operator action. func (ex *Executor) Resume() { ex.mu.Lock() defer ex.mu.Unlock() ex.halted = false ex.haltReason = "" } // EconomicFitness computes the fitness for the current generation. func (ex *Executor) EconomicFitness() *EconomicFitness { ex.mu.Lock() defer ex.mu.Unlock() ef := &EconomicFitness{ Generation: ex.generation, Realized: ex.Portfolio.TotalRealized(), Costs: ex.Portfolio.TotalCosts(), TradeCount: ex.Portfolio.FillCount(), Time: time.Now(), } // Count wins and losses via Portfolio method (thread-safe). ef.WinCount, ef.LossCount = ex.Portfolio.WinLossCounts() return ef } func (ex *Executor) killFileExists() bool { if ex.KillFilePath == "" { return false } _, err := os.Stat(ex.KillFilePath) return err == nil } func (ex *Executor) simulateFill(order TradeOrder) *Fill { return &Fill{ OrderID: order.ID, Asset: order.Asset, Side: order.Side, Venue: order.Venue, Price: order.Price, Quantity: order.Quantity, Fee: order.Price * order.Quantity * 0.001, // 0.1% simulated fee FilledAt: time.Now(), } } func (ex *Executor) logTrade(order TradeOrder, fill *Fill) { if ex.LogPath == "" { return } f, err := os.OpenFile(ex.LogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644) if err != nil { return } defer f.Close() fmt.Fprintf(f, "%s gen=%d mode=%s %s %s %.4f@%.2f venue=%s fee=%.4f\n", fill.FilledAt.Format(time.RFC3339), ex.generation, ex.Mode, fill.Side, fill.Asset, fill.Quantity, fill.Price, fill.Venue, fill.Fee, ) }