Add freqtrade quant strategies and deploy docs
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"candidate": "seed191_candidate_candidate_106",
|
||||
"train_score": 0.408524,
|
||||
"full_year_backtest": {
|
||||
"totalReturn": 32.81,
|
||||
"maxDrawdown": -17.06,
|
||||
"totalTrades": 86,
|
||||
"winRate": 27.91,
|
||||
"profitFactor": 1.77,
|
||||
"sharpeRatio": 1.08,
|
||||
"profitable_months": 8,
|
||||
"monthlyPnL": {
|
||||
"2025-01": 775.85,
|
||||
"2025-02": -450.57,
|
||||
"2025-03": -87.41,
|
||||
"2025-04": 796.12,
|
||||
"2025-05": 1235.77,
|
||||
"2025-06": 202.46,
|
||||
"2025-07": 1102.42,
|
||||
"2025-08": -228.33,
|
||||
"2025-09": 704.12,
|
||||
"2025-10": 1205.47,
|
||||
"2025-11": 93.56,
|
||||
"2025-12": -1902.76
|
||||
},
|
||||
"exit_reasons": {
|
||||
"trailing_stop_pct": 29.069767441860467,
|
||||
"protective_stop_pct": 0.0,
|
||||
"breakeven_stop_pct": 17.441860465116278,
|
||||
"time_stop_pct": 3.488372093023256
|
||||
}
|
||||
},
|
||||
"params": {
|
||||
"strategy_lever_rate": 1.5,
|
||||
"profit_line": 0.056,
|
||||
"lock_profit_rate": 0.26,
|
||||
"open_time_interval": 8,
|
||||
"up_line_span": 432,
|
||||
"up_line_offset": 0.96,
|
||||
"buy_stop_profit_span": 192,
|
||||
"buy_stop_profit_offset": 0.96,
|
||||
"ma_span_long": 2,
|
||||
"regime_slope_lookback": 48,
|
||||
"regime_slope_threshold": 0.005,
|
||||
"regime_displace_threshold": 0.16,
|
||||
"regime_vol_ema_span": 24,
|
||||
"regime_compression_threshold": 0.5,
|
||||
"regime_expansion_threshold": 1.0,
|
||||
"regime_hysteresis_bars": 5,
|
||||
"entry_up_line_span_short": 72,
|
||||
"entry_ma_span_short": 7,
|
||||
"entry_pullback_bars_min": 5,
|
||||
"exit_max_loss_pct": 0.03,
|
||||
"exit_breakeven_buffer": 0.005,
|
||||
"exit_max_hold_bars": 720
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"strategy_lever_rate": 1.5,
|
||||
"profit_line": 0.056,
|
||||
"lock_profit_rate": 0.26,
|
||||
"open_time_interval": 8,
|
||||
"up_line_span": 432,
|
||||
"up_line_offset": 0.96,
|
||||
"buy_stop_profit_span": 192,
|
||||
"buy_stop_profit_offset": 0.96,
|
||||
"ma_span_long": 2,
|
||||
"regime_slope_lookback": 48,
|
||||
"regime_slope_threshold": 0.005,
|
||||
"regime_displace_threshold": 0.16,
|
||||
"regime_vol_ema_span": 24,
|
||||
"regime_compression_threshold": 0.5,
|
||||
"regime_expansion_threshold": 1.0,
|
||||
"regime_hysteresis_bars": 5,
|
||||
"entry_up_line_span_short": 72,
|
||||
"entry_ma_span_short": 7,
|
||||
"entry_pullback_bars_min": 5,
|
||||
"exit_max_loss_pct": 0.03,
|
||||
"exit_breakeven_buffer": 0.005,
|
||||
"exit_max_hold_bars": 720
|
||||
}
|
||||
@@ -0,0 +1,541 @@
|
||||
# @param strategy_lever_rate float 策略风险杠杆系数
|
||||
# @param profit_line float 锁盈触发收益率
|
||||
# @param lock_profit_rate float 锁盈回撤保护比例
|
||||
# @param open_time_interval float 冷却窗口小时数
|
||||
# @param up_line_span int 开仓突破通道周期
|
||||
# @param up_line_offset float 突破通道偏移倍数
|
||||
# @param buy_stop_profit_span int 动态止盈底线周期
|
||||
# @param buy_stop_profit_offset float 动态止盈底线偏移倍数
|
||||
# @param ma_span_long int 长均线天数
|
||||
# @param regime_slope_lookback int 趋势检测回看K线数
|
||||
# @param regime_slope_threshold float 趋势斜率阈值
|
||||
# @param regime_displace_threshold float 价格偏离阈值
|
||||
# @param regime_vol_ema_span int 波动率EMA周期
|
||||
# @param regime_compression_threshold float 压缩状态波动阈值
|
||||
# @param regime_expansion_threshold float 扩张状态波动阈值
|
||||
# @param regime_hysteresis_bars int 状态切换确认K线数
|
||||
# @param entry_up_line_span_short int 压缩突破短通道周期
|
||||
# @param entry_ma_span_short int 回调短均线天数
|
||||
# @param entry_pullback_bars_min int 回调最低K线数
|
||||
# @param exit_max_loss_pct float 保护止损最大亏损比例
|
||||
# @param exit_breakeven_buffer float 保本止损触发缓冲
|
||||
# @param exit_max_hold_bars int 时间止损最大持仓K线数
|
||||
# @strategy tradeDirection long
|
||||
|
||||
SPREAD_SPAN = 6
|
||||
N_VALUE_SPAN = 10
|
||||
|
||||
|
||||
def _ema(values):
|
||||
value = None
|
||||
span = float(len(values))
|
||||
for number in values:
|
||||
number = float(number)
|
||||
if value is None:
|
||||
value = number
|
||||
else:
|
||||
value = 2 * number / (span + 1) + (span - 1) / (span + 1) * value
|
||||
return value
|
||||
|
||||
|
||||
def _history_bars(ctx, length, history=None):
|
||||
length = int(length)
|
||||
if length <= 0:
|
||||
return []
|
||||
if history is None:
|
||||
bars = ctx.bars(length + 1)
|
||||
if len(bars) <= 1:
|
||||
return []
|
||||
history = bars[:-1]
|
||||
if len(history) < length:
|
||||
return []
|
||||
return history[-length:]
|
||||
|
||||
|
||||
def _n_value(history):
|
||||
window = _history_bars(None, SPREAD_SPAN * N_VALUE_SPAN, history=history)
|
||||
if len(window) < SPREAD_SPAN * N_VALUE_SPAN:
|
||||
return None
|
||||
spreads = []
|
||||
for i in range(N_VALUE_SPAN):
|
||||
start = i * SPREAD_SPAN
|
||||
chunk = window[start:start + SPREAD_SPAN]
|
||||
high = max(bar.high for bar in chunk)
|
||||
low = min(bar.low for bar in chunk)
|
||||
spreads.append(high - low)
|
||||
return _ema(spreads)
|
||||
|
||||
|
||||
def _open_up_line(history, span):
|
||||
window = _history_bars(None, span, history=history)
|
||||
if len(window) < int(span):
|
||||
return None
|
||||
return max(bar.high for bar in window)
|
||||
|
||||
|
||||
def _stop_profit_bottom_line(history, span):
|
||||
window = _history_bars(None, span, history=history)
|
||||
if len(window) < int(span):
|
||||
return None
|
||||
return min(bar.low for bar in window)
|
||||
|
||||
|
||||
def _ma_long(history, span_days):
|
||||
length = int(span_days) * 24 * 6
|
||||
window = _history_bars(None, length, history=history)
|
||||
if length <= 0 or len(window) < length:
|
||||
return None
|
||||
return sum(bar.close for bar in window) / float(length)
|
||||
|
||||
|
||||
def _prepare_history(ctx, up_line_span, buy_stop_profit_span, ma_span_long,
|
||||
regime_slope_lookback=0, entry_up_line_span_short=0, entry_ma_span_short=0):
|
||||
ma_length = int(ma_span_long) * 24 * 6
|
||||
ma_short_length = int(entry_ma_span_short) * 24 * 6 if entry_ma_span_short else 0
|
||||
required = max(
|
||||
SPREAD_SPAN * N_VALUE_SPAN,
|
||||
int(up_line_span),
|
||||
int(buy_stop_profit_span),
|
||||
ma_length,
|
||||
int(regime_slope_lookback) + ma_length,
|
||||
int(entry_up_line_span_short),
|
||||
ma_short_length,
|
||||
)
|
||||
if required <= 0:
|
||||
return []
|
||||
return _history_bars(ctx, required)
|
||||
|
||||
|
||||
def _cached_indicators(ctx, params):
|
||||
if hasattr(ctx, 'indicator_value'):
|
||||
up_line_short = None
|
||||
ma_short = None
|
||||
if params.get('entry_up_line_span_short'):
|
||||
up_line_short = ctx.indicator_value('up_line_short')
|
||||
if params.get('entry_ma_span_short'):
|
||||
ma_short = ctx.indicator_value('ma_short')
|
||||
# Always compute history for regime signal MA slope calculation
|
||||
history = _prepare_history(
|
||||
ctx,
|
||||
params['up_line_span'],
|
||||
params['buy_stop_profit_span'],
|
||||
params['ma_span_long'],
|
||||
params.get('regime_slope_lookback', 0),
|
||||
params.get('entry_up_line_span_short', 0),
|
||||
params.get('entry_ma_span_short', 0),
|
||||
)
|
||||
return {
|
||||
'n_value': ctx.indicator_value('n_value'),
|
||||
'up_line': ctx.indicator_value('up_line'),
|
||||
'stop_profit_bottom': ctx.indicator_value('stop_profit_bottom'),
|
||||
'ma_long': ctx.indicator_value('ma_long'),
|
||||
'up_line_short': up_line_short,
|
||||
'ma_short': ma_short,
|
||||
'history': history,
|
||||
}
|
||||
history = _prepare_history(
|
||||
ctx,
|
||||
params['up_line_span'],
|
||||
params['buy_stop_profit_span'],
|
||||
params['ma_span_long'],
|
||||
params.get('regime_slope_lookback', 0),
|
||||
params.get('entry_up_line_span_short', 0),
|
||||
params.get('entry_ma_span_short', 0),
|
||||
)
|
||||
result = {
|
||||
'n_value': _n_value(history),
|
||||
'up_line': _open_up_line(history, params['up_line_span']),
|
||||
'stop_profit_bottom': _stop_profit_bottom_line(history, params['buy_stop_profit_span']),
|
||||
'ma_long': _ma_long(history, params['ma_span_long']),
|
||||
'history': history,
|
||||
}
|
||||
if params.get('entry_up_line_span_short'):
|
||||
result['up_line_short'] = _open_up_line(history, params['entry_up_line_span_short'])
|
||||
if params.get('entry_ma_span_short'):
|
||||
result['ma_short'] = _ma_long(history, params['entry_ma_span_short'])
|
||||
return result
|
||||
|
||||
|
||||
def _strategy_params(ctx):
|
||||
return {
|
||||
'strategy_lever_rate': float(ctx.param('strategy_lever_rate')),
|
||||
'profit_line': float(ctx.param('profit_line')),
|
||||
'lock_profit_rate': float(ctx.param('lock_profit_rate')),
|
||||
'open_time_interval': float(ctx.param('open_time_interval')),
|
||||
'up_line_span': int(ctx.param('up_line_span')),
|
||||
'up_line_offset': float(ctx.param('up_line_offset')),
|
||||
'buy_stop_profit_span': int(ctx.param('buy_stop_profit_span')),
|
||||
'buy_stop_profit_offset': float(ctx.param('buy_stop_profit_offset')),
|
||||
'ma_span_long': int(ctx.param('ma_span_long')),
|
||||
'regime_slope_lookback': int(ctx.param('regime_slope_lookback')),
|
||||
'regime_slope_threshold': float(ctx.param('regime_slope_threshold')),
|
||||
'regime_displace_threshold': float(ctx.param('regime_displace_threshold')),
|
||||
'regime_vol_ema_span': int(ctx.param('regime_vol_ema_span')),
|
||||
'regime_compression_threshold': float(ctx.param('regime_compression_threshold')),
|
||||
'regime_expansion_threshold': float(ctx.param('regime_expansion_threshold')),
|
||||
'regime_hysteresis_bars': int(ctx.param('regime_hysteresis_bars')),
|
||||
'entry_up_line_span_short': int(ctx.param('entry_up_line_span_short')),
|
||||
'entry_ma_span_short': int(ctx.param('entry_ma_span_short')),
|
||||
'entry_pullback_bars_min': int(ctx.param('entry_pullback_bars_min')),
|
||||
'exit_max_loss_pct': float(ctx.param('exit_max_loss_pct')),
|
||||
'exit_breakeven_buffer': float(ctx.param('exit_breakeven_buffer')),
|
||||
'exit_max_hold_bars': int(ctx.param('exit_max_hold_bars')),
|
||||
}
|
||||
|
||||
|
||||
def _ensure_indicator_cache(ctx, params):
|
||||
if hasattr(ctx, 'set_indicator_cache'):
|
||||
ctx.set_indicator_cache(params)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _position_size_pct(ctx, n_value, strategy_lever_rate):
|
||||
if n_value is None or n_value <= 0:
|
||||
return 0.0
|
||||
price = ctx.current_price()
|
||||
if price <= 0:
|
||||
return 0.0
|
||||
stop_loss_pct = n_value / price
|
||||
if stop_loss_pct <= 0:
|
||||
return 0.0
|
||||
pct = 0.01 * float(strategy_lever_rate) / stop_loss_pct
|
||||
return min(max(pct, 0.0), 1.0)
|
||||
|
||||
|
||||
def _time_diff_ms(current_time, last_close_time):
|
||||
if current_time is None or last_close_time is None:
|
||||
return None
|
||||
delta = current_time - last_close_time
|
||||
if hasattr(delta, 'total_seconds'):
|
||||
return delta.total_seconds() * 1000.0
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Market regime detection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _regime_state_init(ctx):
|
||||
defaults = {
|
||||
'effective_regime': 'range',
|
||||
'regime_candidate': 'range',
|
||||
'regime_candidate_bars': 0,
|
||||
'n_value_ema': None,
|
||||
'prev_vol_ratio': None,
|
||||
}
|
||||
for key, val in defaults.items():
|
||||
if ctx.get_state(key, None) is None:
|
||||
ctx.set_state(key, val)
|
||||
|
||||
|
||||
def _regime_signals(ctx, bar, params, indicators):
|
||||
ma_long = indicators['ma_long']
|
||||
n_value = indicators['n_value']
|
||||
history = indicators.get('history')
|
||||
if ma_long is None or n_value is None:
|
||||
return None
|
||||
|
||||
if bar.close == 0:
|
||||
return None
|
||||
price_displacement = (bar.close - ma_long) / ma_long
|
||||
|
||||
lookback = int(params['regime_slope_lookback'])
|
||||
ma_slope = None
|
||||
if history is not None and lookback > 0:
|
||||
ma_length = int(params['ma_span_long']) * 24 * 6
|
||||
past_history = history[:max(0, len(history) - lookback)]
|
||||
ma_past = _ma_long(past_history, params['ma_span_long'])
|
||||
if ma_past is not None and ma_past != 0:
|
||||
ma_slope = (ma_long - ma_past) / ma_past
|
||||
|
||||
n_ema = ctx.get_state('n_value_ema', None)
|
||||
ema_span = int(params['regime_vol_ema_span'])
|
||||
if n_ema is None:
|
||||
n_ema = n_value
|
||||
else:
|
||||
alpha = 2.0 / (ema_span + 1.0)
|
||||
n_ema = alpha * n_value + (1.0 - alpha) * n_ema
|
||||
ctx.set_state('n_value_ema', n_ema)
|
||||
|
||||
vol_ratio = n_value / n_ema if n_ema and n_ema > 0 else 1.0
|
||||
|
||||
prev_vol_ratio = ctx.get_state('prev_vol_ratio', None)
|
||||
ctx.set_state('prev_vol_ratio', vol_ratio)
|
||||
|
||||
return {
|
||||
'ma_slope': ma_slope,
|
||||
'price_displacement': price_displacement,
|
||||
'vol_ratio': vol_ratio,
|
||||
'vol_ratio_rising': prev_vol_ratio is not None and vol_ratio > prev_vol_ratio,
|
||||
}
|
||||
|
||||
|
||||
def _classify_regime(signals, params):
|
||||
if signals is None:
|
||||
return 'range'
|
||||
|
||||
vol_ratio = signals['vol_ratio']
|
||||
ma_slope = signals['ma_slope']
|
||||
price_displacement = signals['price_displacement']
|
||||
|
||||
if vol_ratio < float(params['regime_compression_threshold']):
|
||||
return 'compression'
|
||||
|
||||
if vol_ratio > float(params['regime_expansion_threshold']):
|
||||
return 'expansion'
|
||||
|
||||
slope_threshold = float(params['regime_slope_threshold'])
|
||||
displace_threshold = float(params['regime_displace_threshold'])
|
||||
if (ma_slope is not None
|
||||
and abs(ma_slope) > slope_threshold
|
||||
and abs(price_displacement) > displace_threshold):
|
||||
return 'trend'
|
||||
|
||||
return 'range'
|
||||
|
||||
|
||||
def _effective_regime(ctx, new_regime, params):
|
||||
prev_candidate = ctx.get_state('regime_candidate', 'range')
|
||||
if new_regime == prev_candidate:
|
||||
bars = ctx.get_state('regime_candidate_bars', 0) + 1
|
||||
ctx.set_state('regime_candidate_bars', bars)
|
||||
else:
|
||||
ctx.set_state('regime_candidate', new_regime)
|
||||
ctx.set_state('regime_candidate_bars', 1)
|
||||
return ctx.get_state('effective_regime', 'range')
|
||||
|
||||
hysteresis = int(params['regime_hysteresis_bars'])
|
||||
if ctx.get_state('regime_candidate_bars', 0) >= hysteresis:
|
||||
ctx.set_state('effective_regime', new_regime)
|
||||
return new_regime
|
||||
|
||||
return ctx.get_state('effective_regime', 'range')
|
||||
|
||||
|
||||
def _current_regime(ctx):
|
||||
return ctx.get_state('effective_regime', 'range')
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Entry modes
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _entry_cooldown_ok(ctx, params):
|
||||
last_close_time = ctx.get_state('last_close_time', None)
|
||||
time_diff_ms = _time_diff_ms(ctx.current_time, last_close_time)
|
||||
if time_diff_ms is None:
|
||||
return True
|
||||
time_range_ms = max(float(params['open_time_interval']), 0.0) * 60 * 60 * 1000.0
|
||||
return time_range_ms <= 0 or time_diff_ms >= time_range_ms
|
||||
|
||||
|
||||
def _entry_breakout_chase(ctx, bar, params, indicators):
|
||||
n_value = indicators['n_value']
|
||||
up_line = indicators['up_line']
|
||||
ma_long = indicators['ma_long']
|
||||
if n_value is None or up_line is None or ma_long is None:
|
||||
return False
|
||||
|
||||
if bar.close <= ma_long:
|
||||
return False
|
||||
|
||||
threshold = up_line + n_value * float(params['up_line_offset'])
|
||||
if bar.close > threshold:
|
||||
position_pct = _position_size_pct(ctx, n_value, float(params['strategy_lever_rate']))
|
||||
if position_pct > 0:
|
||||
ctx.buy(amount=position_pct)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _entry_compression_breakout(ctx, bar, params, indicators):
|
||||
n_value = indicators['n_value']
|
||||
up_line_short = indicators.get('up_line_short')
|
||||
if n_value is None or up_line_short is None:
|
||||
return False
|
||||
|
||||
n_ema = ctx.get_state('n_value_ema', None)
|
||||
if n_ema is None or n_ema <= 0:
|
||||
return False
|
||||
current_vol = n_value / n_ema
|
||||
|
||||
prev_vol = ctx.get_state('_prev_vol_saved', None)
|
||||
ctx.set_state('_prev_vol_saved', current_vol)
|
||||
if prev_vol is None:
|
||||
return False
|
||||
if current_vol <= prev_vol:
|
||||
return False
|
||||
|
||||
threshold = up_line_short + n_value * float(params['up_line_offset'])
|
||||
if bar.close > threshold:
|
||||
position_pct = _position_size_pct(ctx, n_value, float(params['strategy_lever_rate']))
|
||||
if position_pct > 0:
|
||||
ctx.buy(amount=position_pct)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _entry_pullback_reentry(ctx, bar, params, indicators):
|
||||
ma_short = indicators.get('ma_short')
|
||||
if ma_short is None:
|
||||
return False
|
||||
|
||||
bars_below = ctx.get_state('pullback_bars_below', 0)
|
||||
if bar.close < ma_short:
|
||||
ctx.set_state('pullback_bars_below', bars_below + 1)
|
||||
return False
|
||||
|
||||
min_bars = int(params['entry_pullback_bars_min'])
|
||||
if bars_below >= min_bars and bar.close > ma_short:
|
||||
ctx.set_state('pullback_bars_below', 0)
|
||||
n_value = indicators['n_value']
|
||||
position_pct = _position_size_pct(ctx, n_value, float(params['strategy_lever_rate']))
|
||||
if position_pct > 0:
|
||||
ctx.buy(amount=position_pct)
|
||||
return True
|
||||
|
||||
ctx.set_state('pullback_bars_below', 0)
|
||||
return False
|
||||
|
||||
|
||||
def _entry_router(ctx, bar, params, indicators, regime):
|
||||
if not _entry_cooldown_ok(ctx, params):
|
||||
return False
|
||||
|
||||
if regime == 'trend':
|
||||
return _entry_breakout_chase(ctx, bar, params, indicators)
|
||||
elif regime in ('compression', 'expansion'):
|
||||
return _entry_compression_breakout(ctx, bar, params, indicators)
|
||||
elif regime == 'range':
|
||||
return _entry_pullback_reentry(ctx, bar, params, indicators)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exit modules
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _exit_protective_stop(ctx, bar, params):
|
||||
loss_limit = (float(params['exit_max_loss_pct'])
|
||||
* max(ctx.balance, 0.0)
|
||||
* float(params['strategy_lever_rate']))
|
||||
unrealized_loss = max(ctx.entry_balance() - ctx.equity, 0.0)
|
||||
if unrealized_loss > loss_limit:
|
||||
ctx.close_position()
|
||||
ctx.set_state('exit_reason', 'protective_stop')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _exit_breakeven_stop(ctx, bar, params, indicators):
|
||||
profit = ctx.unrealized_profit_pct(bar.close)
|
||||
buffer_val = float(params['exit_breakeven_buffer'])
|
||||
|
||||
if profit > buffer_val:
|
||||
ctx.set_state('breakeven_armed', True)
|
||||
|
||||
if ctx.get_state('breakeven_armed', False):
|
||||
n_value = indicators.get('n_value')
|
||||
entry = ctx.entry_price()
|
||||
if n_value is not None and entry > 0:
|
||||
# Give 0.3 N-value breathing room below entry so noise doesn't trigger exit
|
||||
breakeven_level = entry - n_value * 0.3
|
||||
else:
|
||||
breakeven_level = entry * 0.999
|
||||
if bar.close < breakeven_level:
|
||||
ctx.close_position()
|
||||
ctx.set_state('exit_reason', 'breakeven_stop')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _exit_trailing_stop(ctx, bar, params, indicators):
|
||||
stop_profit_bottom = indicators['stop_profit_bottom']
|
||||
n_value = indicators['n_value']
|
||||
if stop_profit_bottom is None or n_value is None:
|
||||
return False
|
||||
|
||||
buy_stop_profit = stop_profit_bottom + n_value * float(params['buy_stop_profit_offset'])
|
||||
if bar.close < buy_stop_profit:
|
||||
ctx.close_position()
|
||||
ctx.set_state('exit_reason', 'trailing_stop')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _exit_time_stop(ctx, bar, params):
|
||||
bars_in_pos = ctx.get_state('bars_in_position', 0) + 1
|
||||
ctx.set_state('bars_in_position', bars_in_pos)
|
||||
if bars_in_pos >= int(params['exit_max_hold_bars']):
|
||||
ctx.close_position()
|
||||
ctx.set_state('exit_reason', 'time_stop')
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _exit_dispatcher(ctx, bar, params, indicators):
|
||||
# Priority: protective → trailing → breakeven → time
|
||||
# Trailing before breakeven: when trailing stop rises above entry, it captures
|
||||
# trend profits; breakeven only acts as safety net when trailing hasn't activated.
|
||||
if _exit_protective_stop(ctx, bar, params):
|
||||
return True
|
||||
if _exit_trailing_stop(ctx, bar, params, indicators):
|
||||
return True
|
||||
if _exit_breakeven_stop(ctx, bar, params, indicators):
|
||||
return True
|
||||
if _exit_time_stop(ctx, bar, params):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lifecycle
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def on_init(ctx):
|
||||
ctx.signal_timing = 'next_bar_open'
|
||||
ctx.max_profit = 0.0
|
||||
ctx.last_close_time = None
|
||||
ctx.last_close_index = None
|
||||
params = _strategy_params(ctx)
|
||||
_ensure_indicator_cache(ctx, params)
|
||||
ctx.set_state('strategy_params_cache', params)
|
||||
_regime_state_init(ctx)
|
||||
ctx.set_state('breakeven_armed', False)
|
||||
ctx.set_state('bars_in_position', 0)
|
||||
ctx.set_state('exit_reason', None)
|
||||
ctx.set_state('pullback_bars_below', 0)
|
||||
ctx.set_state('_prev_vol_saved', None)
|
||||
|
||||
|
||||
def on_bar(ctx, bar):
|
||||
params = ctx.get_state('strategy_params_cache') or _strategy_params(ctx)
|
||||
indicators = _cached_indicators(ctx, params)
|
||||
|
||||
n_value = indicators['n_value']
|
||||
up_line = indicators['up_line']
|
||||
stop_profit_bottom = indicators['stop_profit_bottom']
|
||||
ma_long = indicators['ma_long']
|
||||
if n_value is None or up_line is None or stop_profit_bottom is None or ma_long is None:
|
||||
return
|
||||
|
||||
# 1. Classify regime
|
||||
signals = _regime_signals(ctx, bar, params, indicators)
|
||||
new_regime = _classify_regime(signals, params)
|
||||
regime = _effective_regime(ctx, new_regime, params)
|
||||
|
||||
# 2. If in position, check exits
|
||||
if ctx.has_position() and ctx.is_long():
|
||||
# Track max profit for observability
|
||||
profit = ctx.unrealized_profit_pct(bar.close)
|
||||
if profit > ctx.max_profit:
|
||||
ctx.max_profit = profit
|
||||
|
||||
_exit_dispatcher(ctx, bar, params, indicators)
|
||||
return
|
||||
|
||||
# 3. If flat, reset exit state then check entry
|
||||
ctx.set_state('breakeven_armed', False)
|
||||
ctx.set_state('bars_in_position', 0)
|
||||
_entry_router(ctx, bar, params, indicators, regime)
|
||||
Reference in New Issue
Block a user