Files
nofx/backtest/runner_metrics.go
T
tinkle-community cb31782be4 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)
2026-03-12 12:53:57 +08:00

240 lines
5.7 KiB
Go

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
}