使用 Backtrader 复现国信证券这篇报告《CTA 系列专题之五:基于连续信号的商品期货交易策略》

好的,我们来使用 Backtrader 复现国信证券这篇报告《CTA 系列专题之五:基于连续信号的商品期货交易策略》。

请注意:

  1. 数据: 报告使用了特定标准(如过去半年日均成交额超过50亿)筛选的商品期货主力合约 15 分钟数据。此复现将使用 Backtrader 自带的示例数据或需要您自行准备符合格式的商品期货 15 分钟 K 线数据 (CSV 格式,至少包含 datetime, open, high, low, close, volume 列),并需要在添加数据时指定合约乘数 (mult) 和保证金 (margin) 以便进行准确的风险和收益计算。
  2. 简化:
    • 成交额筛选: 动态筛选合约逻辑复杂,此代码假定传入的数据 datas 已经是筛选好的合约。
    • VWAP 成交: Backtrader 默认使用下一 Bar 开盘价成交。实现 5 分钟 VWAP 需要更高频的数据或复杂的委托逻辑,此处简化为下一 Bar 开盘价。
    • 波动率目标调整: 报告中提到每月根据过去一年策略表现调整杠杆以达到 15% 的年化波动率目标。这需要滚动计算策略历史波动率并动态调整下单比例,实现较为复杂。代码中暂时省略了这一动态调整,使用固定的基准风险比例,但保留了最大杠杆限制的思路。您可以在此基础上进一步开发波动率目标模块。
    • TNR 指标周期: 报告未明确 TNR 计算周期 N 和 ΔTNR 计算周期 k,代码中暂定 N=20, k=3。
    • ATR 周期: 海龟交易法原文和报告未明确 ATR 周期,代码中暂定为 14。
  3. 参数: 报告中的参数(如均线周期、概率更新因子、U2P 阈值等)已尽量遵循,但某些细节可能需要根据实际数据和回测结果进行调优。

复现思路总结

本次 Backtrader 复现旨在将报告的核心思想转化为代码:

  1. 基础信号与过滤: 使用 EMA 均线交叉作为基础方向判断,但实际开仓需结合报告提出的两个过滤条件:
    • 低波动过滤 (Lev_ATR > 1):通过计算 ATR 与价格的比值,要求波动相对较小时才入场。
    • 噪音下降过滤 (ΔTNR > 0):通过计算趋势噪音比(TNR)的变化,要求市场噪音呈现下降趋势时才入场。
  2. 连续信号核心 (U2P):
    • 引入 UpProb (上涨概率) 和 DownProb (下跌概率),并在每个 Bar 根据基础信号(均线交叉状态)进行更新。
    • 计算 U2P = UpProb - DownProb 作为核心的连续信号强度指标。
  3. 交易决策:
    • 仅当满足开仓过滤条件 (Lev_ATR > 1ΔTNR > 0) |U2P| > 0.2 时才考虑开仓或调整仓位。
    • 信号方向由 U2P 的正负决定。
    • 目标仓位大小U2P绝对值成正比(体现信号强度),并结合基于 ATR 的风险控制(类似海龟,每单位 ATR 承担固定风险比例,如 0.5%)。
  4. 风险管理:
    • 使用基于 ATR 的资金管理方法确定基础头寸规模。
    • (简化)设定一个最大杠杆比例限制总风险暴露。
  5. 多品种: 策略设计为可同时运行在多个符合条件的商品期货合约上,资金按等风险权重分配(通过 ATR Sizer 实现)。

Backtrader 复现代码

import backtrader as bt
import backtrader.indicators as btind
import backtrader.feeds as btfeeds
import datetime
import math
import numpy as np

# --- 自定义指标 ---
class TNRIndicator(bt.Indicator):
    """趋势噪音比 Trend to Noise Ratio"""
    lines = ('tnr',)
    params = (('period', 20),) # N in the report

    def __init__(self):
        self.addminperiod(self.p.period + 1) # 需要 N+1 根 Bar

    def next(self):
        # 分子:|Close_t - Close_{t-N}|
        numerator = abs(self.data.close[0] - self.data.close[-self.p.period])

        # 分母:sum(|Close_i - Close_{i-1}|) for i from t-N+1 to t
        price_diffs = np.abs(np.diff(self.data.close.get(size=self.p.period + 1)))
        denominator = np.sum(price_diffs)

        if denominator == 0:
            self.lines.tnr[0] = 0.0 # Avoid division by zero
        else:
            self.lines.tnr[0] = numerator / denominator

class DeltaTNRIndicator(bt.Indicator):
    """TNR 变化率"""
    lines = ('delta_tnr',)
    params = (('period', 3),) # k in the report

    def __init__(self):
        self.tnr = TNRIndicator(self.data) # Depends on TNR
        self.tnr_mean = bt.indicators.SimpleMovingAverage(self.tnr(-1), period=self.p.period) # Mean of T-k to T-1
        self.addminperiod(self.tnr.minperiod + self.p.period)

    def next(self):
        # ΔTNR = TNR_t - mean(TNR_{t-k}..TNR_{t-1})
        # Note: self.tnr[0] is TNR_t
        #       self.tnr_mean[0] is the mean of TNR from t-k-1 to t-1 (using tnr(-1))
        self.lines.delta_tnr[0] = self.tnr[0] - self.tnr_mean[0]


class LevATRIndicator(bt.Indicator):
    """报告中的 Lev_ATR (波动调整杠杆率的中间计算,用于判断波动大小)"""
    lines = ('lev_atr',)
    params = (('period', 14), ('risk_pct', 0.005)) # ATR Period and 0.5% risk per ATR unit

    def __init__(self):
        self.atr = bt.indicators.AverageTrueRange(self.data, period=self.p.period)
        self.addminperiod(self.atr.minperiod)

    def next(self):
        if self.atr[0] == 0:
            self.lines.lev_atr[0] = float('inf') # Avoid division by zero, represent very low vol
        else:
            # Formula: (Risk % / ATR) * Close
            # Condition Lev_ATR > 1 means (Risk % / ATR) * Close > 1
            # Which simplifies to ATR / Close < Risk %
            # So Lev_ATR > 1 indicates low relative volatility
            self.lines.lev_atr[0] = (self.p.risk_pct / self.atr[0]) * self.data.close[0]

# --- 策略 ---
class ContinuousSignalStrategy(bt.Strategy):
    params = (
        ('ema_long_period', 100),
        ('ema_short_period', 10),
        ('tnr_n_period', 20),    # TNR lookback N
        ('tnr_k_period', 3),     # Delta TNR lookback k
        ('atr_period', 14),      # ATR period for LevATR and Sizer
        ('atr_risk_pct', 0.005), # Risk per ATR unit (0.5%)
        ('u2p_threshold', 0.2),  # |U2P| threshold for action
        ('prob_update_factor', 0.5), # Factor for updating probabilities
        ('max_leverage', 4.0),    # Maximum allowed leverage per instrument (approximate)
        ('printlog', True),
    )

    def log(self, txt, dt=None, doprint=False):
        ''' Logging function for this strategy'''
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            tm = self.datas[0].datetime.time(0)
            print('%s %s, %s' % (dt.isoformat(), tm.isoformat(), txt))

    def __init__(self):
        self.inds = {}
        self.up_prob = {}
        self.down_prob = {}
        self.u2p = {}
        self.entry_signal_long = {} # Tracks if basic EMA cross UP happened this bar
        self.entry_signal_short = {} # Tracks if basic EMA cross DOWN happened this bar
        self.order = {} # To keep track of pending orders per data feed

        for i, d in enumerate(self.datas):
            self.inds[d] = {}
            self.inds[d]['ema_long'] = bt.indicators.ExponentialMovingAverage(d.close, period=self.p.ema_long_period)
            self.inds[d]['ema_short'] = bt.indicators.ExponentialMovingAverage(d.close, period=self.p.ema_short_period)
            self.inds[d]['ema_cross'] = bt.indicators.CrossOver(self.inds[d]['ema_short'], self.inds[d]['ema_long'])

            self.inds[d]['lev_atr'] = LevATRIndicator(d, period=self.p.atr_period, risk_pct=self.p.atr_risk_pct)
            self.inds[d]['delta_tnr'] = DeltaTNRIndicator(d, period=self.p.tnr_k_period)
            self.inds[d]['atr'] = bt.indicators.AverageTrueRange(d, period=self.p.atr_period) # Needed for sizer

            # Initialize probabilities and U2P
            self.up_prob[d] = 0.5
            self.down_prob[d] = 0.5
            self.u2p[d] = 0.0
            self.entry_signal_long[d] = False
            self.entry_signal_short[d] = False
            self.order[d] = None

        # --- Sizer ---
        # We use a built-in sizer for simplicity, but scaling happens inside next()
        # The actual sizing logic should consider ATR risk per unit.
        # A custom sizer would be better but more complex.
        # We'll use order_target_percent, calculating the percent dynamically.
        # self.setsizer(bt.sizers.PercentSizer(percents=10)) # Placeholder sizer

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # Buy/Sell order submitted/accepted to/by broker - Nothing to do
             self.log(f'ORDER {order.ordtype.name} {order.Status.names[order.status]} - Data: {order.data._name}')
             self.order[order.data] = order # Track the order
             return

        # Check if an order has been completed
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(
                    f'BUY EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size} - Data: {order.data._name}')
            elif order.issell():
                self.log(
                    f'SELL EXECUTED, Price: {order.executed.price:.2f}, Cost: {order.executed.value:.2f}, Comm: {order.executed.comm:.2f}, Size: {order.executed.size} - Data: {order.data._name}')
            self.bar_executed = len(self) # Bar number when trade was executed

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log(f'ORDER {order.Status.names[order.status]} - Data: {order.data._name}')

        # Allow new orders for this data feed
        self.order[order.data] = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log(f'OPERATION PROFIT, GROSS {trade.pnl:.2f}, NET {trade.pnlcomm:.2f} - Data: {trade.data._name}')

    def next(self):
        for i, d in enumerate(self.datas):
            # Skip if there's a pending order for this data feed
            if self.order[d]:
                continue

            # --- 1. Update Probabilities based on Basic Signals ---
            # Check for NEW EMA Crossovers this bar
            current_cross = self.inds[d]['ema_cross'][0]
            long_signal_active = current_cross == 1.0
            short_signal_active = current_cross == -1.0

            # Get previous probabilities
            prev_up_prob = self.up_prob[d]
            prev_down_prob = self.down_prob[d]

            # Apply update rules from the report
            # UpProb_t = UpProb_{t-1} + factor * (DownProb_{t-1} * Long_t - UpProb_{t-1} * Short_t)
            up_prob_change = self.p.prob_update_factor * (prev_down_prob * long_signal_active - prev_up_prob * short_signal_active)
            self.up_prob[d] = max(0.0, min(1.0, prev_up_prob + up_prob_change)) # Clamp between 0 and 1

            # DownProb_t = DownProb_{t-1} + factor * (UpProb_{t-1} * Short_t - DownProb_{t-1} * Long_t)
            down_prob_change = self.p.prob_update_factor * (prev_up_prob * short_signal_active - prev_down_prob * long_signal_active)
            self.down_prob[d] = max(0.0, min(1.0, prev_down_prob + down_prob_change)) # Clamp between 0 and 1

            # Normalize probabilities (should ideally sum to 1, but updates might slightly deviate)
            prob_sum = self.up_prob[d] + self.down_prob[d]
            if prob_sum > 0:
                 self.up_prob[d] /= prob_sum
                 self.down_prob[d] /= prob_sum
            else: # Should not happen if initialized at 0.5/0.5
                 self.up_prob[d] = 0.5
                 self.down_prob[d] = 0.5

            # Calculate U2P
            self.u2p[d] = self.up_prob[d] - self.down_prob[d]

            # --- 2. Check Entry Filters ---
            lev_atr_val = self.inds[d]['lev_atr'][0]
            delta_tnr_val = self.inds[d]['delta_tnr'][0]

            # Entry allowed ONLY if Volatility is low AND Noise is decreasing
            entry_allowed = lev_atr_val > 1.0 and delta_tnr_val > 0.0

            # --- 3. Determine Target Position based on U2P and Filters ---
            target_percent = 0.0 # Default to flat

            if entry_allowed and abs(self.u2p[d]) > self.p.u2p_threshold:
                # Signal is active and strong enough
                signal_strength = self.u2p[d] # Use U2P directly for strength and direction

                # --- Sizing Logic ---
                # Calculate size based on ATR risk per unit, scaled by signal strength
                # Target value per position = SignalStrength * (ATR Risk % * Portfolio Value)
                # Target percent = Target value / Current Price / Portfolio Value * Price ? No.
                # Target percent = (SignalStrength * ATR Risk % * PortVal) / (Price * Multiplier * Qty) * Qty ???
                # Target percent = SignalStrength * SizePerATRRiskUnit * Price * Multiplier / PortVal
                # SizePerATRRiskUnit = (ATR Risk % * PortVal) / (ATR * Multiplier)
                # TargetPercent = SignalStrength * [(ATR Risk % * PortVal) / (ATR * Multiplier)] * Price * Multiplier / PortVal
                # TargetPercent = SignalStrength * ATR Risk % * Price / ATR

                atr_val = self.inds[d]['atr'][0]
                price = d.close[0]
                if atr_val > 0:
                    base_percent_per_signal_unit = self.p.atr_risk_pct * price / atr_val
                    target_percent = signal_strength * base_percent_per_signal_unit

                    # Apply Max Leverage Cap (approximate)
                    # Max percent = Max Leverage / (Price * Multiplier / Margin) # Incorrect
                    # Max percent = Max Leverage * Margin / (Price * Multiplier) # Incorrect
                    # Approximate: Max percent based on 1 / N assets, scaled by max leverage
                    # E.g., max total leverage = 4, 10 assets -> max leverage per asset ~ 0.4? No.
                    # Simpler cap: ensure |target_percent| * value / margin <= max_leverage
                    # Need margin per contract. Let's just cap the target_percent directly for simplicity.
                    # Max individual percent = MaxLeverage / NumberOfAssets (if equal allocation)
                    max_pos_percent = self.p.max_leverage / len(self.datas) # Rough approximation
                    target_percent = max(-max_pos_percent, min(max_pos_percent, target_percent))

            # --- 4. Place Order ---
            # Use order_target_percent to manage position based on continuous signal
            self.order_target_percent(data=d, target=target_percent)
            # Log the target calculation
            # self.log(f'{d._name}: U2P={self.u2p[d]:.4f}, LevATR={lev_atr_val:.2f}, dTN R={delta_tnr_val:.4f}, Allowed={entry_allowed}, TargetPct={target_percent:.4f}')


# --- Main Execution ---
if __name__ == '__main__':
    cerebro = bt.Cerebro()

    # --- Add Data ---
    # NOTE: You MUST replace this with your actual 15-minute data for commodity futures
    # Ensure CSV has: datetime, open, high, low, close, volume
    # Add 'mult' (contract multiplier) and 'margin' (per contract) parameters to data feed
    datapath_template = 'path_to_your_data/{}.csv' # Replace with your path and file naming convention
    # Example symbols (replace with yours!)
    symbols = ['RB000', 'HC000', 'I000', 'J000', 'JM000'] # Example rebar, hot coil, iron ore etc.

    start_date = datetime.datetime(2016, 1, 1) # Adjust start/end dates
    end_date = datetime.datetime(2023, 5, 31)

    for sym in symbols:
        try:
            # --- CRITICAL: Define multiplier and margin for each symbol ---
            # These are EXAMPLE values, replace with actuals!
            if sym == 'RB000': mult, margin = 10, 3000
            elif sym == 'HC000': mult, margin = 10, 3000
            elif sym == 'I000': mult, margin = 100, 8000
            elif sym == 'J000': mult, margin = 100, 9000
            elif sym == 'JM000': mult, margin = 60, 7000
            else: mult, margin = 1, 1000 # Default fallback

            data = btfeeds.GenericCSVData(
                dataname=datapath_template.format(sym),
                fromdate=start_date,
                todate=end_date,
                nullvalue=0.0,
                dtformat=('%Y-%m-%d %H:%M:%S'), # Adjust if your datetime format is different
                timeframe=bt.TimeFrame.Minutes,
                compression=15, # 15 Minutes
                datetime=0,
                open=1,
                high=2,
                low=3,
                close=4,
                volume=5,
                openinterest=-1, # No open interest in this example file
                name=sym # Assign name to data feed
            )
             # Add contract multiplier and margin info
            data.p.mult = mult
            data.p.margin = margin
            cerebro.adddata(data)
            print(f"Added data for {sym} with multiplier {mult}, margin {margin}")

        except Exception as e:
            print(f"Could not load data for {sym}: {e}")

    if not cerebro.datas:
         print("No data loaded. Exiting.")
         exit()

    # --- Add Strategy ---
    cerebro.addstrategy(ContinuousSignalStrategy, printlog=False) # Set printlog=True for detailed logs

    # --- Set Initial Capital ---
    start_cash = 10000000.0 # 10 Million
    cerebro.broker.setcash(start_cash)

    # --- Set Commission and Slippage ---
    # Commission: 0.3%% = 0.0003 (percentage)
    # Slippage: 1%% = 0.01 (percentage) - This is very high, adjust as needed
    cerebro.broker.setcommission(commission=0.0003, mult=1.0, margin=None) # Futures commission often per contract, use mult
    # Slippage can be set using cerebro.broker.set_slippage_perc(perc=0.01) or fixed points
    # Using Percent Slippage for this example
    cerebro.broker.set_slippage_perc(perc=0.01, slip_open=True, slip_limit=True, slip_match=True, slip_out=True)


    # --- Add Analyzers ---
    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='mysharpe', timeframe=bt.TimeFrame.Days)
    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='mydrawdown')
    cerebro.addanalyzer(bt.analyzers.Returns, _name='myreturns', timeframe=bt.TimeFrame.Days)
    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='mytradeanalysis')
    cerebro.addanalyzer(bt.analyzers.SQN, _name='mysqn')

    # --- Run Backtest ---
    print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
    results = cerebro.run()
    print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())

    # --- Print Analysis Results ---
    strat = results[0] # Get the first strategy instance
    analysis = strat.analyzers

    print("\n--- Strategy Analysis ---")
    print(f"Sharpe Ratio: {analysis.mysharpe.get_analysis().get('sharperatio', 'N/A')}")
    dd = analysis.mydrawdown.get_analysis()
    print(f"Max Drawdown: {dd.max.drawdown:.2f}%")
    print(f"Max Drawdown Period: {dd.max.len} bars")
    ret = analysis.myreturns.get_analysis()
    print(f"Total Return: {ret.get('rtot', 'N/A'):.4f}")
    print(f"Annualized Return: {ret.get('ravg', 'N/A'):.4f}") # Note: Backtrader's ravg might need scaling depending on timeframe
    sqn = analysis.mysqn.get_analysis()
    print(f"SQN: {sqn.get('sqn', 'N/A'):.2f}")
    print(f"SQN Avg Trade: {sqn.get('avg_trade', 'N/A'):.2f}")

    trade_analysis = analysis.mytradeanalysis.get_analysis()
    if trade_analysis:
        print("\n--- Trade Analysis ---")
        print(f"Total Trades: {trade_analysis.total.total}")
        print(f"Winning Trades: {trade_analysis.won.total}")
        print(f"Losing Trades: {trade_analysis.lost.total}")
        print(f"Win Rate: {trade_analysis.won.total / trade_analysis.total.total * 100:.2f}%" if trade_analysis.total.total else "N/A")
        print(f"Average Win $: {trade_analysis.won.pnl.average:.2f}")
        print(f"Average Loss $: {trade_analysis.lost.pnl.average:.2f}")
        print(f"Profit Factor: {abs(trade_analysis.won.pnl.total / trade_analysis.lost.pnl.total):.2f}" if trade_analysis.lost.pnl.total else "INF")

    # --- Plot Results ---
    # Ensure matplotlib is installed: pip install matplotlib
    try:
        print("\nPlotting results...")
        cerebro.plot(style='candlestick', barup='green', bardown='red', volup='green', voldown='red')
    except Exception as e:
        print(f"\nCould not plot results: {e}. Ensure matplotlib is installed and a GUI backend is available.")

如何运行:

  1. 安装 Backtrader: pip install backtrader
  2. 安装 Numpy: pip install numpy
  3. 安装 Matplotlib (可选,用于绘图): pip install matplotlib
  4. 准备数据:
    • 创建数据文件夹。
    • 将你的 15 分钟商品期货 CSV 数据放入该文件夹。
    • 确保 CSV 文件名与代码中 symbols 列表中的名称对应(例如 RB000.csv)。
    • 确保 CSV 格式正确,包含 datetime, open, high, low, close, volume 列,且日期时间格式与 dtformat 参数匹配。
  5. 修改代码:
    • 修改 datapath_template 指向你的数据文件夹。
    • 修改 symbols 列表为你实际拥有的数据文件名(不含 .csv)。
    • !! 关键 !! 修改 if __name__ == '__main__': 中的 mult (合约乘数) 和 margin (每手保证金) 为你所用合约的实际值。这对正确的风险计算至关重要。
    • 调整 start_date, end_date
    • 调整 start_cash
    • 根据需要调整佣金 (commission) 和滑点 (slippage_perc)。报告中 1‰ 的滑点按百分比算是 0.1% (0.001),代码中暂设为 1% (0.01) 是一个较大值,请务必调整。
    • (可选)调整策略参数 params
  6. 运行脚本: python your_script_name.py

这个复现版本提供了一个框架,抓住了报告的核心逻辑,但在某些高级特性(如动态波动率目标调整)上进行了简化。你可以基于此代码,根据实际需求和更深入的研究进行扩展和优化。

我们来对这个基于连续信号的商品期货 CTA 策略进行一次深度总结。

核心目标与创新

该策略的核心目标是克服传统 CTA 策略信号二元化(非多即空或空仓)的局限性,通过引入信号强度的概念,实现更动态和精细化的仓位管理。其主要创新在于:

  1. 信号不再是开关,而是刻度尺: 摒弃了简单的 0/1/-1 信号,尝试量化信号的“可信度”或“强度”。
  2. 双重过滤提升入场质量: 在基础信号(如均线交叉)之上,增加了市场状态的判断,旨在过滤掉不利于趋势跟踪的环境,只在“更有可能”成功的条件下入场。
  3. 仓位大小与信号强度挂钩: 这是策略的灵魂。通过一个动态变化的指标(U2P 概率差)来决定投入多少风险,而非简单的全仓或空仓。

策略运作机制详解

  1. 基础方向判定(骨架): 策略仍然需要一个基础的趋势判断机制,报告和代码中使用了经典的 EMA 均线交叉(短周期上穿长周期为多头,下穿为空头)。但这只是提供了初步的方向感。

  2. 入场质量控制(门禁): 在考虑执行基础信号前,设置了两道“门禁”:

    • 低相对波动率 (Lev_ATR > 1): 要求入场时的市场波动(ATR 相对于价格)处于相对较低的水平。逻辑在于:过高的波动率可能意味着趋势不稳定、噪音大或接近转折点,此时追涨杀跌风险较高。该策略倾向于在相对“平静”时捕捉趋势的启动或延续。
    • 噪音下降趋势 (ΔTNR > 0): 要求衡量趋势清晰度的趋势噪音比(TNR)呈现改善趋势(即噪音占比在减少)。逻辑在于:一个正在变得越来越“干净”的趋势,其持续的可能性被认为更高。在噪音减弱时入场,增加了信号的可靠性。
  3. 信号强度量化与持续跟踪(核心引擎 - U2P): 这是策略与传统 CTA 最本质的区别。

    • 概率动态更新: 策略并不直接使用均线交叉信号来开仓,而是用它来“驱动”两个内部状态变量:UpProb(上涨概率)和 DownProb(下跌概率)。当多头信号(短上穿长)出现时,UpProb 会增加,DownProb 会减少;空头信号反之。这个更新过程是持续的,反映了基础信号的持续状态而非仅仅是交叉瞬间。更新幅度由 prob_update_factor 控制。
    • U2P 概率差: 计算 U2P = UpProb - DownProb。这个值在 -1 到 +1 之间波动:
      • 接近 +1:表明策略内部模型对上涨趋势有很强信心。
      • 接近 -1:表明对下跌趋势有很强信心。
      • 接近 0:表明多空力量均衡或信号不明朗。
    • 行动阈值 (|U2P| > 0.2): 只有当 U2P 的绝对值超过一定阈值(如 0.2)时,才认为信号强度足够明确,值得采取行动(开仓或调整仓位),以过滤掉 U2P 在零附近徘徊时的噪音。
  4. 动态风险分配与仓位调整:

    • ATR 风险单位: 借鉴海龟交易法则,首先基于 ATR 计算一个标准风险单位对应的头寸(例如,设定每单位 ATR 承担账户 0.5% 的风险)。
    • U2P 强度缩放: 计算出的目标仓位大小,直接与 U2P 的值(信号强度)成正比。即 目标仓位 ≈ U2P * (基于 ATR 的标准风险头寸)。这意味着:
      • U2P 接近 +1 时,持有多头仓位,且接近 ATR 风险允许的最大头寸。
      • U2P 接近 -1 时,持有空头仓位,同样接近最大风险头寸。
      • U2P 绝对值较小(但大于阈值)时,持有对应方向的仓位,但头寸较小。
    • 最大杠杆限制: 设置一个总体的最大杠杆上限,防止即使在 U2P 很高的情况下,单一品种或整体风险暴露过大。
    • 持续调整: 由于 U2P 在每个 Bar 都会更新,策略会不断地重新评估目标仓位,并发出指令以趋近这个目标仓位。这导致仓位是动态变化的,而非简单的开仓/平仓。

策略的优势

  1. 更精细的风险管理: 通过将仓位与信号强度挂钩,避免了在信号刚出现或强度减弱时仍承担全额风险,有望降低波动和回撤。
  2. 潜在更高的风险调整后收益: 在信号明确且强的趋势中加大投入,在信号模糊或减弱时减少投入,理论上能更有效地捕捉趋势,提升夏普比率。
  3. 提高了入场信号质量: 双重过滤机制(低波+降噪)旨在避开部分容易失败的交易环境。
  4. 适应性潜力: U2P 机制作为一种信号强度量化和跟踪框架,理论上可以嫁接到其他类型的基础信号之上。
  5. 解释性相对较好: 相比纯黑箱模型,U2P 代表的概率差提供了一定的可解释性。

策略的劣势与挑战

  1. 复杂性增加: 引入了更多指标(TNR, ΔTNR, Lev_ATR, U2P)和参数,增加了理解、实现、调试和优化的难度。
  2. 参数敏感性高: EMA 周期、TNR 周期、ΔTNR 周期、ATR 周期、风险百分比、U2P 阈值、概率更新因子、最大杠杆等众多参数,使得策略对参数选择非常敏感,存在过拟合风险。需要严谨的回测和样本外验证。
  3. TNR/ΔTNR 的有效性: 这两个指标的有效性可能依赖于市场状态和参数设定,其作为过滤条件的普适性和稳健性需要大量数据检验。
  4. U2P 更新规则的启发性: 概率更新的具体数学形式 (factor * (DownProb * Long - UpProb * Short)) 具有一定的启发式成分,其能否精确反映未来概率值得商榷。更新因子的选择会影响策略的反应速度。
  5. 交易成本敏感性: 由于仓位是动态调整的,可能比传统 CTA 产生更多的交易次数,对交易成本(佣金和滑点)更敏感。虽然报告显示对成本不敏感,但实际运行中仍需关注。
  6. 实现细节: 精确的 VWAP 成交、动态的跨周期波动率目标调整(报告提到但代码简化了)等在实盘中会增加实现复杂度。

总结

该策略是传统趋势跟踪 CTA 的一次有意义的精细化升级。它试图通过量化信号强度优化入场时机来解决传统方法的痛点。核心的 U2P 概率差机制双重入场过滤 是其亮点,使得仓位管理更加动态和智能化

然而,这种精细化也带来了更高的复杂性参数依赖性。策略的长期稳健性高度依赖于参数的设定、对交易成本的控制以及其核心假设(如 TNR/ΔTNR 和 U2P 更新规则的有效性)在不同市场环境下的表现。

总体而言,这是一个理念先进、框架完整的量化策略,它代表了 CTA 策略从简单规则向更** nuanced(细致入微)** 的市场状态感知和风险管理演进的一个方向。其效果需要通过在更广泛数据和实际交易中的持续验证。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值