From 39eac5aca745266b6d4f2b75e8fc75fd5a71ed98 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Tue, 21 Jul 2026 15:02:23 +0900 Subject: [PATCH] =?UTF-8?q?config:=20stop=20the=20churn=20=E2=80=94=20hold?= =?UTF-8?q?=20for=20big=20moves,=20wide=20TP/SL,=20low=20leverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live decomposition of the losing streak: 23% win rate with avg win +$1.23 / avg loss -$1.04 on ~0.3-0.5% price moves, where the ~0.14% round-trip fee ate 30-50% of every tiny winner. Death by small-move grinding. The AI was closing positions on ±0.5% noise after the 60m min-hold, capping winners at ~0.86%. Redesign to 'few big positions, held for big moves': - Throttle: min hold 60m->4h, noise-close window 90m->8h, reentry 30m->3h, opens/hour 30->3, opens/cycle 6->2. Noise band widened -1%..+2% -> -4%..+6% so small moves can no longer trigger a close. - Exits: stop bypass -2.5% -> -5%, take-profit bypass +5% -> +12% (wide, asymmetric — let winners run, cut losers only on a real move). - Leverage 20x -> 5x: a -5% stop at 20x is instant liquidation; at 5x it is -25% of margin, survivable. 2 positions x 2.5x = 5x total (full margin, ~20% cushion) instead of 4x5x=20x. - Prompt now instructs the AI to set wide stops (~-5%) and distant targets (~+10-12%), hold multi-hour, and never scalp 0.5% moves. Live strategy updated (maxPos=2, lev=5, ratio=2.5). --- api/handler_user.go | 17 +++++------ api/handler_user_default_strategy_test.go | 14 ++++----- kernel/engine_prompt.go | 12 ++++---- kernel/engine_prompt_test.go | 4 +-- store/strategy.go | 10 +++---- trader/auto_trader_throttle.go | 36 ++++++++++++----------- trader/auto_trader_throttle_test.go | 19 +++++++----- web/src/pages/StrategyStudioPage.tsx | 15 +++++----- 8 files changed, 65 insertions(+), 62 deletions(-) diff --git a/api/handler_user.go b/api/handler_user.go index 14dbf621..0b882439 100644 --- a/api/handler_user.go +++ b/api/handler_user.go @@ -262,15 +262,14 @@ func (s *Server) createDefaultStrategies(userID string, lang string) error { c.CoinSource.VergexLimit = 10 c.CoinSource.VergexMarketType = "all" c.CoinSource.VergexChain = "hyperliquid" - c.RiskControl.MaxPositions = 4 - c.RiskControl.BTCETHMaxLeverage = 20 - c.RiskControl.AltcoinMaxLeverage = 20 - // 5× equity notional per position: 4 positions = 20x total account - // notional (full margin, ~5% liquidation cushion). Aggressive by - // operator choice — bigger single positions; the 0.4 short-signal - // floor keeps the book balanced so it is not a one-directional bet. - c.RiskControl.BTCETHMaxPositionValueRatio = 5.0 - c.RiskControl.AltcoinMaxPositionValueRatio = 5.0 + c.RiskControl.MaxPositions = 2 + c.RiskControl.BTCETHMaxLeverage = 5 + c.RiskControl.AltcoinMaxLeverage = 5 + // Few, concentrated positions held for big moves. 5x leverage so a + // wide (-5%) stop is survivable rather than an instant liquidation; + // 2 positions × 2.5x = 5x total notional (full margin, ~20% cushion). + c.RiskControl.BTCETHMaxPositionValueRatio = 2.5 + c.RiskControl.AltcoinMaxPositionValueRatio = 2.5 c.RiskControl.MaxMarginUsage = 1.0 c.RiskControl.MinConfidence = 78 c.RiskControl.MinRiskRewardRatio = 3.0 diff --git a/api/handler_user_default_strategy_test.go b/api/handler_user_default_strategy_test.go index 9182bac4..6c487bde 100644 --- a/api/handler_user_default_strategy_test.go +++ b/api/handler_user_default_strategy_test.go @@ -54,16 +54,16 @@ func TestCreateDefaultStrategiesUsesOneReadyToRunClaw402Preset(t *testing.T) { if trendCfg.CoinSource.SourceType != "vergex_signal" || trendCfg.CoinSource.VergexLimit != 10 || trendCfg.CoinSource.VergexMarketType != "all" { t.Fatalf("default strategy should use the Claw402/Vergex all-market signal ranking, got %+v", trendCfg.CoinSource) } - if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions != 4 { - t.Fatalf("default strategy should be Claw402/Vergex native with a 4-position balanced book, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl) + if trendCfg.CoinSource.UseAI500 || trendCfg.RiskControl.MaxPositions != 2 { + t.Fatalf("default strategy should be Claw402/Vergex native with a 2-position concentrated book, got coin=%+v risk=%+v", trendCfg.CoinSource, trendCfg.RiskControl) } - if trendCfg.RiskControl.BTCETHMaxLeverage != 20 || trendCfg.RiskControl.AltcoinMaxLeverage != 20 { - t.Fatalf("default strategy should use 20x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl) + if trendCfg.RiskControl.BTCETHMaxLeverage != 5 || trendCfg.RiskControl.AltcoinMaxLeverage != 5 { + t.Fatalf("default strategy should use 5x leverage for all Claw402 opens, got risk=%+v", trendCfg.RiskControl) } - if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 5 || - trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 5 || + if trendCfg.RiskControl.BTCETHMaxPositionValueRatio != 2.5 || + trendCfg.RiskControl.AltcoinMaxPositionValueRatio != 2.5 || trendCfg.RiskControl.MaxMarginUsage != 1.0 { - t.Fatalf("default strategy should size Claw402 opens at 5x equity notional (4 positions = 20x total at 20x), got risk=%+v", trendCfg.RiskControl) + t.Fatalf("default strategy should size Claw402 opens at 2.5x equity notional (2 positions = 5x total at 5x), got risk=%+v", trendCfg.RiskControl) } } diff --git a/kernel/engine_prompt.go b/kernel/engine_prompt.go index e9e6d73d..5f40c229 100644 --- a/kernel/engine_prompt.go +++ b/kernel/engine_prompt.go @@ -215,9 +215,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n") sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n") sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n") - sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 60 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n") - sb.WriteString("- Fees are the main edge killer: a round trip costs roughly 0.1%% of notional (about 1%% of margin at 10x). Only take setups whose expected move to target is at least 3x that cost; fewer, higher-conviction, longer-hold trades beat frequent scalps.\n") - sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n") + sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n") + sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n") + sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n") } else { sb.WriteString("# You are the NOFX Claw402 auto-trader\n\n") sb.WriteString("Trade only Hyperliquid instruments returned by this cycle's Claw402.ai/Vergex board. You may trade only the current candidate symbols and existing positions; never invent tickers or rotate outside the provided universe.\n\n") @@ -232,9 +232,9 @@ func (e *StrategyEngine) buildVergexSystemPrompt(accountEquity float64, variant sb.WriteString("- Ranking alone is not an entry reason; it only defines the candidate pool.\n") sb.WriteString("- Every symbol in Candidate Coins is part of the allowed trading universe; missing detail can lower confidence or trigger waiting, but does not make the symbol non-tradable.\n") sb.WriteString("- If Signal Lab or heatmap is absent from that symbol's Vergex Claw402 Signals, state it in reasoning; if it is present, never claim the symbol lacks that data.\n") - sb.WriteString("- Avoid churn: unless stopping out or taking a strong profit, hold new positions for at least 60 minutes; avoid flat/noise closes until roughly 90 minutes; after closing a symbol, wait 90 minutes before re-entry; open at most 1 new position per hour.\n") - sb.WriteString("- Fees are the main edge killer: a round trip costs roughly 0.1%% of notional (about 1%% of margin at 10x). Only take setups whose expected move to target is at least 3x that cost; fewer, higher-conviction, longer-hold trades beat frequent scalps.\n") - sb.WriteString("- Stops must sit beyond invalidation; targets should prefer heatmap resistance/liquidation zones or valid risk/reward levels.\n\n") + sb.WriteString("- Hold for BIG moves, do not churn: hold new positions for at least 4 hours; never close inside the -4%% to +6%% noise band before ~8 hours; after closing a symbol wait 3 hours before re-entry; open at most 1-2 new positions per hour. Small in-and-out trades bled this account to death on fees.\n") + sb.WriteString("- Fees are the main edge killer: a round trip costs ~0.1%% of notional. Only enter setups where the realistic target is a LARGE move: stop-loss around -5%% and take-profit around +10-12%% (roughly 2:1 or better). Do not aim for 0.5%% scalps — they cannot cover fees. Fewer, high-conviction, wide-target, multi-hour holds only.\n") + sb.WriteString("- Set WIDE stops and targets: place the stop well beyond short-term noise (around -5%%) and the target at a distant heatmap resistance/liquidation zone (around +10-12%%). Give the position room to develop; do not exit on small green or small red.\n\n") } writeModeVariant(&sb, variant, zh) diff --git a/kernel/engine_prompt_test.go b/kernel/engine_prompt_test.go index bc3b535d..f963c448 100644 --- a/kernel/engine_prompt_test.go +++ b/kernel/engine_prompt_test.go @@ -29,8 +29,8 @@ func TestBuildSystemPromptUsesVergexClaw402Prompt(t *testing.T) { if !strings.Contains(prompt, "Direction must be data-driven") { t.Fatalf("prompt should explain that direction is data-driven, not long-only:\n%s", prompt) } - if !strings.Contains(prompt, "every open position must use exactly 20x") { - t.Fatalf("prompt should force 20x leverage for Claw402 opens:\n%s", prompt) + if !strings.Contains(prompt, "every open position must use exactly 5x") { + t.Fatalf("prompt should force 5x leverage for Claw402 opens:\n%s", prompt) } if !strings.Contains(prompt, "use the full max notional per position") { t.Fatalf("prompt should force full-size Claw402 opens:\n%s", prompt) diff --git a/store/strategy.go b/store/strategy.go index fe790aed..54db31ab 100644 --- a/store/strategy.go +++ b/store/strategy.go @@ -1014,11 +1014,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig { PriceRankingLimit: 10, }, RiskControl: RiskControlConfig{ - MaxPositions: 4, // Room for ~2 long + 2 short (CODE ENFORCED) - BTCETHMaxLeverage: 20, // BTC/ETH exchange leverage (AI guided) - AltcoinMaxLeverage: 20, // TradeFi exchange leverage (AI guided) - BTCETHMaxPositionValueRatio: 5.0, // Per-position notional = equity × 5; 4 positions = 20x total (full margin, ~5% liquidation cushion — aggressive by operator choice) - AltcoinMaxPositionValueRatio: 5.0, // Per-position notional = equity × 5; 4 positions = 20x total (full margin, ~5% liquidation cushion — aggressive by operator choice) + MaxPositions: 2, // Few, concentrated positions held for big moves (CODE ENFORCED) + BTCETHMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation + AltcoinMaxLeverage: 5, // Low leverage so a big (-5%) stop is survivable, not an instant liquidation + BTCETHMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion) + AltcoinMaxPositionValueRatio: 2.5, // Per-position notional = equity × 2.5; 2 positions = 5x total (full margin at 5x, ~20% liquidation cushion) MaxMarginUsage: 1.0, // Claw402 Autopilot intentionally uses full margin when opening MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED) MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided) diff --git a/trader/auto_trader_throttle.go b/trader/auto_trader_throttle.go index 53a40f04..437cd702 100644 --- a/trader/auto_trader_throttle.go +++ b/trader/auto_trader_throttle.go @@ -10,23 +10,25 @@ import ( ) const ( - // 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 - // MaxPositions (concurrent) + the 45m min-hold + the 90m per-symbol reentry - // cooldown, so the per-hour cap only needs to be high enough not to block the - // directional pair from re-establishing after positions close. A tight value - // here (e.g. 2) starves the strategy: once a couple opens fire, every later - // cycle is blocked and the book drains to flat. Keep it generous. - autopilotMaxOpensPerHour = 30 - autopilotMaxOpensPerCycle = 6 - earlyCloseStopLossBypassPct = -2.5 - earlyCloseTakeProfitBypassPct = 5.0 - noiseCloseLossFloorPct = -1.0 - noiseCloseProfitCeilingPct = 2.0 + // "Hold for big moves, don't churn" regime. Live history showed the + // account bleeding to death by fees: 0.3-0.5% in/out moves where a ~0.14% + // round-trip fee ate 30-50% of every small winner. These values force + // positions to be held for hours and to develop meaningful moves before + // closing, and cut the trade frequency hard. + autopilotMinHoldDuration = 4 * time.Hour + autopilotNoiseCloseHoldDuration = 8 * time.Hour + autopilotReentryCooldown = 3 * time.Hour + // Drastically cut churn: at most a couple of new positions per hour/cycle. + autopilotMaxOpensPerHour = 3 + autopilotMaxOpensPerCycle = 2 + // Wide, asymmetric exits. Cut a loser only at a real -5% (at 5x leverage + // that is -25% of margin — survivable), let a winner run to +12% before + // any early take-profit. The noise band (-4%..+6%) blocks closing on the + // small moves that were grinding the account to nothing. + earlyCloseStopLossBypassPct = -5.0 + earlyCloseTakeProfitBypassPct = 12.0 + noiseCloseLossFloorPct = -4.0 + noiseCloseProfitCeilingPct = 6.0 ) func isOpenAction(action string) bool { diff --git a/trader/auto_trader_throttle_test.go b/trader/auto_trader_throttle_test.go index 28256ee5..d9ae1143 100644 --- a/trader/auto_trader_throttle_test.go +++ b/trader/auto_trader_throttle_test.go @@ -32,7 +32,8 @@ func TestTradeThrottleBlocksEarlyNoiseClose(t *testing.T) { func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) { at := &AutoTrader{} - ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -3.0) + // Only a real -5% stop bypasses the min hold now. + ctx := throttleContext("xyz:INTC", "long", 20*time.Minute, -6.0) reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0) if reason != "" { @@ -42,7 +43,8 @@ func TestTradeThrottleAllowsEarlyHardStop(t *testing.T) { func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) { at := &AutoTrader{} - ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, 0.4) + // Held past the 4h min hold but still inside the wide -4%..+6% noise band. + ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, 0.4) reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0) if !strings.Contains(reason, "noise band") { @@ -52,7 +54,8 @@ func TestTradeThrottleBlocksFlatCloseInsideNoiseWindow(t *testing.T) { func TestTradeThrottleAllowsConfirmedLossAfterMinimumHold(t *testing.T) { at := &AutoTrader{} - ctx := throttleContext("xyz:INTC", "long", 60*time.Minute, -1.2) + // Past the 4h min hold, loss beyond the -4% noise floor → close allowed. + ctx := throttleContext("xyz:INTC", "long", 5*time.Hour, -4.5) reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "close_long"}, ctx, 0) if reason != "" { @@ -76,13 +79,13 @@ func TestTradeThrottleBlocksOpensOverCycleCap(t *testing.T) { at := &AutoTrader{} ctx := &kernel.Context{} - // under the 6-per-cycle cap, a further open is allowed - if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 5); reason != "" { - t.Fatalf("expected open within the 6-per-cycle cap to be allowed, got %q", reason) + // under the 2-per-cycle cap, a further open is allowed + if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 1); reason != "" { + t.Fatalf("expected open within the 2-per-cycle cap to be allowed, got %q", reason) } // at the cap, the next open is blocked - if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 6); !strings.Contains(reason, "6 new position") { - t.Fatalf("expected open beyond the 6-per-cycle cap to be blocked, got %q", reason) + if reason := at.tradeThrottleReason(kernel.Decision{Symbol: "xyz:INTC", Action: "open_long"}, ctx, 2); !strings.Contains(reason, "2 new position") { + t.Fatalf("expected open beyond the 2-per-cycle cap to be blocked, got %q", reason) } } diff --git a/web/src/pages/StrategyStudioPage.tsx b/web/src/pages/StrategyStudioPage.tsx index b4760070..09a9f1d4 100644 --- a/web/src/pages/StrategyStudioPage.tsx +++ b/web/src/pages/StrategyStudioPage.tsx @@ -1388,14 +1388,13 @@ export function StrategyStudioPage() { }), risk_control: defaultRisk({ ...base.ai_config?.risk_control, - max_positions: 4, - btc_eth_max_leverage: 20, - altcoin_max_leverage: 20, - // 5× equity notional per position — 4 positions = 20x total account - // notional (full margin, ~5% liquidation cushion). Aggressive by - // operator choice; the 0.4 short-signal floor keeps the book balanced. - btc_eth_max_position_value_ratio: 5, - altcoin_max_position_value_ratio: 5, + max_positions: 2, + btc_eth_max_leverage: 5, + altcoin_max_leverage: 5, + // Few, concentrated positions held for big moves. 5x leverage so a + // wide (-5%) stop is survivable; 2 positions × 2.5x = 5x total. + btc_eth_max_position_value_ratio: 2.5, + altcoin_max_position_value_ratio: 2.5, max_margin_usage: 1.0, min_confidence: 78, min_risk_reward_ratio: 3,