Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions store/strategy.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,11 @@ type RiskControlConfig struct {
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
// Min AI confidence to open position (AI guided)
MinConfidence int `json:"min_confidence"`

// Profit drawdown close: close position when current profit > MinProfitPct and pullback from peak >= DrawdownClosePct (CODE ENFORCED)
EnableDrawdownClose bool `json:"enable_drawdown_close"` // default true; set false to disable
DrawdownCloseMinProfitPct float64 `json:"drawdown_close_min_profit_pct"` // min current profit % to consider (default 5)
DrawdownClosePct float64 `json:"drawdown_close_pct"` // close when pullback from peak >= this % (default 40)
}

// NewStrategyStore creates a new StrategyStore
Expand Down Expand Up @@ -315,8 +320,11 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
AltcoinMaxPositionValueRatio: 1.0, // Altcoin: max position = 1x equity (CODE ENFORCED)
MaxMarginUsage: 0.9, // Max 90% margin usage (CODE ENFORCED)
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
MinConfidence: 75, // Min 75% confidence (AI guided)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
MinConfidence: 75, // Min 75% confidence (AI guided)
EnableDrawdownClose: true, // Close when profit pulls back from peak
DrawdownCloseMinProfitPct: 5.0, // Only close if current profit > this %
DrawdownClosePct: 40.0, // Close when pullback from peak >= this %
},
}

Expand Down
39 changes: 33 additions & 6 deletions trader/auto_trader.go
Original file line number Diff line number Diff line change
Expand Up @@ -1812,6 +1812,19 @@ func (at *AutoTrader) startDrawdownMonitor() {
}()
}

// shouldTriggerDrawdownClose returns true when drawdown close is enabled, current profit is above min,
// and pullback from peak meets the threshold. Used by checkPositionDrawdown; exposed for tests.
func shouldTriggerDrawdownClose(enable bool, currentPnLPct, peakPnLPct, minProfitPct, pullbackPct float64) bool {
if !enable || currentPnLPct <= minProfitPct {
return false
}
if peakPnLPct <= 0 || currentPnLPct >= peakPnLPct {
return false
}
drawdownPct := ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100
return drawdownPct >= pullbackPct
}

// checkPositionDrawdown checks position drawdown situation
func (at *AutoTrader) checkPositionDrawdown() {
// Get current positions
Expand Down Expand Up @@ -1867,9 +1880,24 @@ func (at *AutoTrader) checkPositionDrawdown() {
drawdownPct = ((peakPnLPct - currentPnLPct) / peakPnLPct) * 100
}

// Check close position condition: profit > 5% and drawdown >= 40%
if currentPnLPct > 5.0 && drawdownPct >= 40.0 {
logger.Infof("🚨 Drawdown close position condition triggered: %s %s | Current profit: %.2f%% | Peak profit: %.2f%% | Drawdown: %.2f%%",
// Thresholds from strategy risk control (defaults: min profit 5%, pullback 40%)
minProfitPct := 5.0
pullbackPct := 40.0
enableDrawdownClose := true
if at.config.StrategyConfig != nil {
rc := at.config.StrategyConfig.RiskControl
enableDrawdownClose = rc.EnableDrawdownClose
if rc.DrawdownCloseMinProfitPct > 0 {
minProfitPct = rc.DrawdownCloseMinProfitPct
}
if rc.DrawdownClosePct > 0 {
pullbackPct = rc.DrawdownClosePct
}
}

// Close when current profit > minProfitPct and pullback from peak >= pullbackPct
if shouldTriggerDrawdownClose(enableDrawdownClose, currentPnLPct, peakPnLPct, minProfitPct, pullbackPct) {
logger.Infof("🚨 Drawdown close position condition triggered: %s %s | Current profit: %.2f%% | Peak profit: %.2f%% | Pullback: %.2f%%",
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)

// Execute close position
Expand All @@ -1880,9 +1908,8 @@ func (at *AutoTrader) checkPositionDrawdown() {
// Clear cache for this position after closing
at.ClearPeakPnLCache(symbol, side)
}
} else if currentPnLPct > 5.0 {
// Record situations close to close position condition (for debugging)
logger.Infof("📊 Drawdown monitoring: %s %s | Profit: %.2f%% | Peak: %.2f%% | Drawdown: %.2f%%",
} else if enableDrawdownClose && currentPnLPct > minProfitPct {
logger.Infof("📊 Drawdown monitoring: %s %s | Profit: %.2f%% | Peak: %.2f%% | Pullback: %.2f%%",
symbol, side, currentPnLPct, peakPnLPct, drawdownPct)
}
}
Expand Down
118 changes: 118 additions & 0 deletions trader/drawdown_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package trader

import (
"testing"
)

func TestShouldTriggerDrawdownClose(t *testing.T) {
tests := []struct {
name string
enable bool
currentPnLPct float64
peakPnLPct float64
minProfitPct float64
pullbackPct float64
expected bool
}{
{
name: "disabled returns false",
enable: false,
currentPnLPct: 10,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "current below min profit returns false",
enable: true,
currentPnLPct: 4,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "current equals min profit returns false",
enable: true,
currentPnLPct: 5,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "peak <= 0 returns false",
enable: true,
currentPnLPct: 10,
peakPnLPct: 0,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "current >= peak returns false",
enable: true,
currentPnLPct: 20,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "pullback below threshold returns false",
enable: true,
currentPnLPct: 15,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: false,
},
{
name: "pullback at threshold returns true",
enable: true,
currentPnLPct: 12,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: true,
},
{
name: "pullback above threshold returns true",
enable: true,
currentPnLPct: 6,
peakPnLPct: 20,
minProfitPct: 5,
pullbackPct: 40,
expected: true,
},
{
name: "default thresholds 5% profit and 40% pullback",
enable: true,
currentPnLPct: 6,
peakPnLPct: 10,
minProfitPct: 5,
pullbackPct: 40,
expected: true,
},
{
name: "custom thresholds 10% profit 30% pullback",
enable: true,
currentPnLPct: 14,
peakPnLPct: 20,
minProfitPct: 10,
pullbackPct: 30,
expected: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := shouldTriggerDrawdownClose(tt.enable, tt.currentPnLPct, tt.peakPnLPct, tt.minProfitPct, tt.pullbackPct)
if got != tt.expected {
t.Errorf("shouldTriggerDrawdownClose(%v, %.2f, %.2f, %.2f, %.2f) = %v, want %v",
tt.enable, tt.currentPnLPct, tt.peakPnLPct, tt.minProfitPct, tt.pullbackPct, got, tt.expected)
}
})
}
}
98 changes: 98 additions & 0 deletions web/src/components/strategy/RiskControlEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ export function RiskControlEditor({
minPositionSizeDesc: { zh: 'USDT 最小名义价值', en: 'Minimum notional value in USDT' },
minConfidence: { zh: '最小信心度', en: 'Min Confidence' },
minConfidenceDesc: { zh: 'AI 开仓信心度阈值', en: 'AI confidence threshold for entry' },
profitDrawdownClose: { zh: '盈利回撤平仓', en: 'Profit drawdown close' },
profitDrawdownCloseDesc: { zh: '当持仓盈利高于「最低盈利%」且从峰值回撤达到「回撤%」时自动平仓,锁定利润', en: 'Close position when profit is above min % and pullback from peak reaches threshold (lock in gains)' },
enableDrawdownClose: { zh: '启用盈利回撤平仓', en: 'Enable profit drawdown close' },
minProfitPct: { zh: '最低盈利 %', en: 'Min profit %' },
minProfitPctDesc: { zh: '仅当当前盈利高于此百分比时才考虑平仓', en: 'Only consider closing when current profit is above this %' },
pullbackFromPeakPct: { zh: '回撤 %(相对峰值)', en: 'Pullback from peak %' },
pullbackFromPeakPctDesc: { zh: '当盈利从峰值回撤达到此百分比时平仓', en: 'Close when profit has pulled back this much from peak' },
}
return translations[key]?.[language] || key
}
Expand Down Expand Up @@ -386,6 +393,97 @@ export function RiskControlEditor({
</div>
</div>
</div>

{/* Profit drawdown close */}
<div>
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('profitDrawdownClose')}
</h3>
</div>
<p className="text-xs mb-4" style={{ color: '#848E9C' }}>
{t('profitDrawdownCloseDesc')}
</p>
<div className="grid grid-cols-1 gap-4 mb-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
checked={config.enable_drawdown_close !== false}
onChange={(e) =>
updateField('enable_drawdown_close', e.target.checked)
}
disabled={disabled}
className="rounded accent-yellow-500"
/>
<span style={{ color: '#EAECEF' }}>{t('enableDrawdownClose')}</span>
</label>
</div>
<div className="grid grid-cols-2 gap-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minProfitPct')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minProfitPctDesc')}
</p>
<div className="flex items-center gap-2">
<input
type="number"
value={config.drawdown_close_min_profit_pct ?? 5}
onChange={(e) =>
updateField('drawdown_close_min_profit_pct', parseFloat(e.target.value) || 0)
}
disabled={disabled}
min={0}
max={100}
step={0.5}
className="w-20 px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<span style={{ color: '#848E9C' }}>%</span>
</div>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('pullbackFromPeakPct')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('pullbackFromPeakPctDesc')}
</p>
<div className="flex items-center gap-2">
<input
type="number"
value={config.drawdown_close_pct ?? 40}
onChange={(e) =>
updateField('drawdown_close_pct', parseFloat(e.target.value) || 0)
}
disabled={disabled}
min={0}
max={100}
step={5}
className="w-20 px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<span style={{ color: '#848E9C' }}>%</span>
</div>
</div>
</div>
</div>
</div>
)
}
5 changes: 5 additions & 0 deletions web/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,11 @@ 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)

// Profit drawdown close: close when current profit > min and pullback from peak >= % (CODE ENFORCED)
enable_drawdown_close?: boolean; // default true; false = disabled
drawdown_close_min_profit_pct?: number; // min current profit % to consider (default 5)
drawdown_close_pct?: number; // close when pullback from peak >= this % (default 40)
}

// Debate Arena Types
Expand Down