mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-14 00:07:01 +08:00
refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity - Split 15+ large Go files (800-2200 lines) into focused modules: kernel/engine.go, backtest/runner.go, market/data.go, store/position.go, api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders - Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx into domain-specific modules with barrel re-exports - Remove stale files: screenshots, .yml.old, pyproject.toml - Remove unused scripts/ and cmd/ directories - Remove broken/outdated test files (network-dependent, stale expectations)
This commit is contained in:
239
backtest/runner_metrics.go
Normal file
239
backtest/runner_metrics.go
Normal file
@@ -0,0 +1,239 @@
|
||||
package backtest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"nofx/logger"
|
||||
"nofx/store"
|
||||
)
|
||||
|
||||
func (r *Runner) persistMetadata() {
|
||||
state := r.snapshotState()
|
||||
meta := r.buildMetadata(state, r.Status())
|
||||
meta.CreatedAt = r.createdAt
|
||||
if err := SaveRunMetadata(meta); err != nil {
|
||||
logger.Infof("failed to save run metadata for %s: %v", r.cfg.RunID, err)
|
||||
} else {
|
||||
if err := updateRunIndex(meta, &r.cfg); err != nil {
|
||||
logger.Infof("failed to update index for %s: %v", r.cfg.RunID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) logDecision(record *store.DecisionRecord) error {
|
||||
if record == nil {
|
||||
return nil
|
||||
}
|
||||
persistDecisionRecord(r.cfg.RunID, record)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) persistMetrics(force bool) {
|
||||
if r.cfg.RunID == "" {
|
||||
return
|
||||
}
|
||||
|
||||
if !force && !r.lastMetricsWrite.IsZero() {
|
||||
if time.Since(r.lastMetricsWrite) < metricsWriteInterval {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
state := r.snapshotState()
|
||||
metrics, err := CalculateMetrics(r.cfg.RunID, &r.cfg, &state)
|
||||
if err != nil {
|
||||
logger.Infof("failed to compute metrics for %s: %v", r.cfg.RunID, err)
|
||||
return
|
||||
}
|
||||
if metrics == nil {
|
||||
return
|
||||
}
|
||||
if err := PersistMetrics(r.cfg.RunID, metrics); err != nil {
|
||||
logger.Infof("failed to persist metrics for %s: %v", r.cfg.RunID, err)
|
||||
return
|
||||
}
|
||||
r.lastMetricsWrite = time.Now()
|
||||
}
|
||||
|
||||
func (r *Runner) buildMetadata(state BacktestState, runState RunState) *RunMetadata {
|
||||
if state.Liquidated && runState != RunStateLiquidated {
|
||||
runState = RunStateLiquidated
|
||||
}
|
||||
|
||||
progress := progressPercent(state, r.cfg)
|
||||
|
||||
summary := RunSummary{
|
||||
SymbolCount: len(r.cfg.Symbols),
|
||||
DecisionTF: r.cfg.DecisionTimeframe,
|
||||
ProcessedBars: state.BarIndex,
|
||||
ProgressPct: progress,
|
||||
EquityLast: state.Equity,
|
||||
MaxDrawdownPct: state.MaxDrawdownPct,
|
||||
Liquidated: state.Liquidated,
|
||||
LiquidationNote: state.LiquidationNote,
|
||||
}
|
||||
|
||||
meta := &RunMetadata{
|
||||
RunID: r.cfg.RunID,
|
||||
UserID: r.cfg.UserID,
|
||||
State: runState,
|
||||
LastError: r.lastErrorString(),
|
||||
Summary: summary,
|
||||
}
|
||||
|
||||
return meta
|
||||
}
|
||||
|
||||
func progressPercent(state BacktestState, cfg BacktestConfig) float64 {
|
||||
duration := cfg.Duration()
|
||||
if duration <= 0 {
|
||||
return 0
|
||||
}
|
||||
if state.BarTimestamp == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
start := time.Unix(cfg.StartTS, 0)
|
||||
end := time.Unix(cfg.EndTS, 0)
|
||||
current := time.UnixMilli(state.BarTimestamp)
|
||||
|
||||
if !current.After(start) {
|
||||
return 0
|
||||
}
|
||||
if current.After(end) {
|
||||
return 100
|
||||
}
|
||||
|
||||
elapsed := current.Sub(start)
|
||||
pct := float64(elapsed) / float64(duration) * 100
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
if pct < 0 {
|
||||
pct = 0
|
||||
}
|
||||
return pct
|
||||
}
|
||||
|
||||
func (r *Runner) maybeCheckpoint() error {
|
||||
state := r.snapshotState()
|
||||
shouldCheckpoint := false
|
||||
|
||||
if r.cfg.CheckpointIntervalBars > 0 && state.BarIndex > 0 && state.BarIndex%r.cfg.CheckpointIntervalBars == 0 {
|
||||
shouldCheckpoint = true
|
||||
}
|
||||
|
||||
interval := time.Duration(r.cfg.CheckpointIntervalSeconds) * time.Second
|
||||
if interval <= 0 {
|
||||
interval = 2 * time.Second
|
||||
}
|
||||
if time.Since(r.lastCheckpoint) >= interval {
|
||||
shouldCheckpoint = true
|
||||
}
|
||||
|
||||
if !shouldCheckpoint {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.saveCheckpoint(state); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) snapshotForCheckpoint(state BacktestState) []PositionSnapshot {
|
||||
res := make([]PositionSnapshot, 0, len(state.Positions))
|
||||
for _, pos := range state.Positions {
|
||||
res = append(res, pos)
|
||||
}
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
if res[i].Symbol == res[j].Symbol {
|
||||
return res[i].Side < res[j].Side
|
||||
}
|
||||
return res[i].Symbol < res[j].Symbol
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
func (r *Runner) buildCheckpointFromState(state BacktestState) *Checkpoint {
|
||||
return &Checkpoint{
|
||||
BarIndex: state.BarIndex,
|
||||
BarTimestamp: state.BarTimestamp,
|
||||
Cash: state.Cash,
|
||||
Equity: state.Equity,
|
||||
UnrealizedPnL: state.UnrealizedPnL,
|
||||
RealizedPnL: state.RealizedPnL,
|
||||
Positions: r.snapshotForCheckpoint(state),
|
||||
DecisionCycle: state.DecisionCycle,
|
||||
Liquidated: state.Liquidated,
|
||||
LiquidationNote: state.LiquidationNote,
|
||||
MaxEquity: state.MaxEquity,
|
||||
MinEquity: state.MinEquity,
|
||||
MaxDrawdownPct: state.MaxDrawdownPct,
|
||||
AICacheRef: r.cachePath,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) saveCheckpoint(state BacktestState) error {
|
||||
ckpt := r.buildCheckpointFromState(state)
|
||||
if ckpt == nil {
|
||||
return nil
|
||||
}
|
||||
if err := SaveCheckpoint(r.cfg.RunID, ckpt); err != nil {
|
||||
return err
|
||||
}
|
||||
r.lastCheckpoint = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Runner) forceCheckpoint() {
|
||||
state := r.snapshotState()
|
||||
if err := r.saveCheckpoint(state); err != nil {
|
||||
logger.Infof("failed to save checkpoint for %s: %v", r.cfg.RunID, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Runner) RestoreFromCheckpoint() error {
|
||||
ckpt, err := LoadCheckpoint(r.cfg.RunID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return r.applyCheckpoint(ckpt)
|
||||
}
|
||||
|
||||
func (r *Runner) applyCheckpoint(ckpt *Checkpoint) error {
|
||||
if ckpt == nil {
|
||||
return fmt.Errorf("checkpoint is nil")
|
||||
}
|
||||
r.account.RestoreFromSnapshots(ckpt.Cash, ckpt.RealizedPnL, ckpt.Positions)
|
||||
r.stateMu.Lock()
|
||||
defer r.stateMu.Unlock()
|
||||
r.state.BarIndex = ckpt.BarIndex
|
||||
r.state.BarTimestamp = ckpt.BarTimestamp
|
||||
r.state.Cash = ckpt.Cash
|
||||
r.state.Equity = ckpt.Equity
|
||||
r.state.UnrealizedPnL = ckpt.UnrealizedPnL
|
||||
r.state.RealizedPnL = ckpt.RealizedPnL
|
||||
r.state.DecisionCycle = ckpt.DecisionCycle
|
||||
r.state.Liquidated = ckpt.Liquidated
|
||||
r.state.LiquidationNote = ckpt.LiquidationNote
|
||||
r.state.MaxEquity = ckpt.MaxEquity
|
||||
r.state.MinEquity = ckpt.MinEquity
|
||||
r.state.MaxDrawdownPct = ckpt.MaxDrawdownPct
|
||||
r.state.Positions = snapshotsToMap(ckpt.Positions)
|
||||
r.state.LastUpdate = time.Now().UTC()
|
||||
r.lastCheckpoint = time.Now()
|
||||
return nil
|
||||
}
|
||||
|
||||
func snapshotsToMap(snaps []PositionSnapshot) map[string]PositionSnapshot {
|
||||
positions := make(map[string]PositionSnapshot, len(snaps))
|
||||
for _, snap := range snaps {
|
||||
key := fmt.Sprintf("%s:%s", snap.Symbol, snap.Side)
|
||||
positions[key] = snap
|
||||
}
|
||||
return positions
|
||||
}
|
||||
Reference in New Issue
Block a user