# @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)