feat: make exit throttle gates strategy-configurable, soften defaults

The anti-churn exit gates (min hold, noise-close window, re-entry
cooldown, bypass and noise-band thresholds) were hardcoded constants in
auto_trader_throttle.go — every flat-ish position was forced to hold 8h+
and changing the pacing meant a code change and redeploy.

- RiskControlConfig gains 7 exit-gate fields (minutes / signed price-%),
  zero = built-in default; accessor methods centralize fallbacks and are
  hot-reloaded from the DB like the rest of the strategy config
- Throttle reads the gates from the strategy; prompt hold/exit guidance
  is now rendered from the same values so the AI is told exactly what
  the code will enforce
- Defaults softened: min hold 4h -> 1.5h, noise window 8h -> 3h,
  re-entry 3h -> 1.5h, bypasses -5/+12 -> -3/+8, noise band -4..+6 ->
  -2..+3 (price-basis). A +-2-3% move is 15-20x round-trip fees — no
  longer 'noise' worth locking
- Strategy Studio gets an 'exit pacing' row (min hold / flat-close
  window / re-entry cooldown); thresholds editable via strategy JSON
- Live strategy updated in data/data.db with the softened values
This commit is contained in:
tinkle-community
2026-07-25 19:26:04 +09:00
parent 05899d5afe
commit 434301cb09
6 changed files with 257 additions and 50 deletions

View File

@@ -2321,6 +2321,68 @@ export function StrategyStudioPage() {
</select>
</label>
</div>
<div className="mt-4 grid gap-4 sm:grid-cols-3">
<label className="space-y-2">
<span className="text-xs text-nofx-text-muted">
{text(language, '最小持仓时间', 'Min hold')}
</span>
<select
value={risk.min_hold_minutes ?? 90}
onChange={(event) =>
patchRisk({
min_hold_minutes: Number(event.target.value),
})
}
className="w-full rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg px-3 py-2 text-sm text-nofx-text"
>
{[30, 60, 90, 120, 180, 240].map((value) => (
<option key={value} value={value}>
{value >= 60 ? `${value / 60}h` : `${value}m`}
</option>
))}
</select>
</label>
<label className="space-y-2">
<span className="text-xs text-nofx-text-muted">
{text(language, '横盘仓锁定窗口', 'Flat-close window')}
</span>
<select
value={risk.noise_hold_minutes ?? 180}
onChange={(event) =>
patchRisk({
noise_hold_minutes: Number(event.target.value),
})
}
className="w-full rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg px-3 py-2 text-sm text-nofx-text"
>
{[60, 120, 180, 240, 360, 480].map((value) => (
<option key={value} value={value}>
{`${value / 60}h`}
</option>
))}
</select>
</label>
<label className="space-y-2">
<span className="text-xs text-nofx-text-muted">
{text(language, '同币再入冷却', 'Re-entry cooldown')}
</span>
<select
value={risk.reentry_cooldown_minutes ?? 90}
onChange={(event) =>
patchRisk({
reentry_cooldown_minutes: Number(event.target.value),
})
}
className="w-full rounded-lg border border-[rgba(26,24,19,0.14)] bg-nofx-bg px-3 py-2 text-sm text-nofx-text"
>
{[30, 60, 90, 120, 180].map((value) => (
<option key={value} value={value}>
{value >= 60 ? `${value / 60}h` : `${value}m`}
</option>
))}
</select>
</label>
</div>
</div>
</div>

View File

@@ -209,4 +209,14 @@ export interface RiskControlConfig {
min_position_size: number; // Min position size in USDT (CODE ENFORCED)
min_risk_reward_ratio: number; // Min take_profit / stop_loss ratio (AI guided)
min_confidence: number; // Min AI confidence to open position (AI guided)
// Exit throttle gates (CODE ENFORCED, 0/absent = backend defaults).
// PnL thresholds are PRICE-move percentages (leverage-independent).
min_hold_minutes?: number; // AI closes blocked before this unless a bypass fires
noise_hold_minutes?: number; // flat closes inside the noise band blocked until this
reentry_cooldown_minutes?: number; // same-symbol reopen cooldown after a close
early_stop_bypass_pct?: number; // price loss unlocking an early close (negative)
early_tp_bypass_pct?: number; // price profit unlocking an early close (positive)
noise_loss_floor_pct?: number; // noise band lower edge (negative)
noise_profit_ceiling_pct?: number; // noise band upper edge (positive)
}