mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 08:46:58 +08:00
feat: data-driven autopilot profitability tuning + edge profile panel
Analysis of 410 live closed trades found the edge is real but was being destroyed by execution: gross +$267 vs $245 fees; trades held <1h were net negative (the <15m bucket alone: -$48 on $66 fees) while 1h+ holds carried +$78; shorts lost $72 while longs made $94 — and both the prompt and the engine were manufacturing those losers. Changes, each tied to the data: - Prompt: removed the 'MUST open at least one long AND one short every cycle' mandate (forced weak-signal shorts); direction is now data-driven with 'never open just to balance the book'. Added fee-awareness (round trip ≈ 0.1% notional, require expected move ≥ 3x cost) and aligned the hold discipline with the backend throttle. - Forced book-balance opens now require |board z-score| ≥ 0.75 — the engine previously force-opened full-size 10x positions on near-neutral signals with hardcoded confidence 70. DirectionalCandidates now carries scores. - Min AI-managed hold raised 45m → 60m (the 15-60m bucket still bled after the earlier throttle landed). - Legacy prompt hygiene: vergex path drops long-only-era custom prompts and zh-era configs fall back wholesale to built-in English sections — fixes the two long-failing kernel prompt tests. - New Edge Profile dashboard panel: net after fees by hold-time bucket and side, computed from recent closed trades, with an automatic takeaway line — the fee/churn regression detector, always visible. Fixed the HistoricalPosition timestamp types (epoch ms, not strings).
This commit is contained in:
@@ -1,23 +1,31 @@
|
||||
package trader
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"nofx/kernel"
|
||||
)
|
||||
|
||||
// ensureLongShortCoverage keeps a balanced book each cycle: it fills toward
|
||||
// roughly half the MaxPositions slots long and half short. The AI still drives
|
||||
// selection/sizing whenever it acts; this is a deterministic top-up — if the
|
||||
// AI's decisions plus existing positions fall short of the per-direction target,
|
||||
// the engine force-opens the strongest unused bullish/bearish candidates to
|
||||
// reach it (never exceeding MaxPositions).
|
||||
// forcedCoverageMinScore is the minimum absolute board z-score a candidate
|
||||
// needs before the engine will force-open it for book balance. Live trade
|
||||
// history showed forced entries on near-neutral signals (|z| < 0.3) were a
|
||||
// systematic money loser — especially shorts — while trades on strong signals
|
||||
// carried the edge. Below this bar the book is simply left unbalanced.
|
||||
const forcedCoverageMinScore = 0.75
|
||||
|
||||
// ensureLongShortCoverage tops the book up toward roughly half the
|
||||
// MaxPositions slots long and half short — but only with candidates whose
|
||||
// directional signal is actually strong (see forcedCoverageMinScore). The AI
|
||||
// still drives selection/sizing whenever it acts; this is a deterministic
|
||||
// top-up, and an unbalanced book is preferred over a forced weak trade.
|
||||
//
|
||||
// Forced opens are sized from account equity via applyAutopilotFullSizeOpen and
|
||||
// run through the same code-enforced risk checks (position-value ratio, minimum
|
||||
// size, margin) as any other open. Guards:
|
||||
// - skipped entirely in safe mode (AI unhealthy),
|
||||
// - scoped to the vergex_signal source (the only one with directional bias),
|
||||
// - requires |signal score| >= forcedCoverageMinScore,
|
||||
// - never exceeds MaxPositions,
|
||||
// - never doubles a base symbol already held or already in the decision set.
|
||||
func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *kernel.Context, equity float64) []kernel.Decision {
|
||||
@@ -65,8 +73,9 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
bullish, bearish := at.strategyEngine.DirectionalCandidates()
|
||||
|
||||
// fill a direction up to its target, drawing from the strongest unused
|
||||
// candidates, never exceeding MaxPositions.
|
||||
fill := func(action string, cands []string, have, target int) {
|
||||
// candidates that clear the signal-strength floor, never exceeding
|
||||
// MaxPositions.
|
||||
fill := func(action string, cands []kernel.DirectionalCandidate, have, target int) {
|
||||
for _, c := range cands {
|
||||
if have >= target {
|
||||
return
|
||||
@@ -74,13 +83,19 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
if maxPos > 0 && posCount >= maxPos {
|
||||
return
|
||||
}
|
||||
b := universeBaseKey(c)
|
||||
if math.Abs(c.Score) < forcedCoverageMinScore {
|
||||
// candidates are rank-ordered; weaker ones may still follow,
|
||||
// so keep scanning instead of breaking
|
||||
at.logInfof("⚖️ Skipped forced %s %s: signal score %.2f below %.2f floor", action, c.Symbol, c.Score, forcedCoverageMinScore)
|
||||
continue
|
||||
}
|
||||
b := universeBaseKey(c.Symbol)
|
||||
if b == "" || held[b] {
|
||||
continue
|
||||
}
|
||||
d := kernel.Decision{
|
||||
Action: action,
|
||||
Symbol: c,
|
||||
Symbol: c.Symbol,
|
||||
Confidence: 70,
|
||||
Reasoning: "Forced " + action + " to fill the balanced long/short book (autopilot)",
|
||||
}
|
||||
@@ -89,7 +104,7 @@ func (at *AutoTrader) ensureLongShortCoverage(decisions []kernel.Decision, ctx *
|
||||
held[b] = true
|
||||
have++
|
||||
posCount++
|
||||
at.logInfof("⚖️ Forced %s %s (account-sized %.2f USDT, %dx)", action, c, d.PositionSizeUSD, d.Leverage)
|
||||
at.logInfof("⚖️ Forced %s %s (score %.2f, account-sized %.2f USDT, %dx)", action, c.Symbol, c.Score, d.PositionSizeUSD, d.Leverage)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
autopilotMinHoldDuration = 45 * time.Minute
|
||||
// Live history: trades held under an hour were net-negative after fees
|
||||
// (the 15-60m bucket bled), while the edge concentrated in 1h+ holds.
|
||||
autopilotMinHoldDuration = 60 * time.Minute
|
||||
autopilotNoiseCloseHoldDuration = 90 * time.Minute
|
||||
autopilotReentryCooldown = 30 * time.Minute
|
||||
// Allow one long + one short per cycle. The real exposure/churn limits are
|
||||
|
||||
Reference in New Issue
Block a user