多周期与多币种策略的工程结构

multi-timeframe-multi-symbol - 智策 SmartQuant
结论先行:跨周期跨币种跑策略需要干净工程:数据层分离、共享风险上限、按相关性控制仓位——别让一个币炸掉整个账户。工程结构决定策略能否长期稳定运行。具体方法与数据详见下文,实操前务必用历史数据回测验证,并结合自身资金与风险承受能力审慎决策。
策略编写 阅读 18 分钟 2026-08-12

多周期多币种策略的难点不在信号逻辑,而在工程:大周期K线必须只在收盘后才可用,每个币种必须持有各自独立的状态机。这两条做错,回测会凭空多出一段不存在的收益,而且你很难从曲线上看出来。

本文要点

  • 多周期策略最常见的错误是用当前尚未收盘的4小时K线去生成信号,这属于未来函数。
  • 正确做法是把大周期指标值向前平移一根,即只使用最后一根已收盘K线的结果。
  • 多币种不能共用一份持仓变量,每个symbol要有独立的状态对象,否则仓位会互相污染。
  • 多周期共振应写成显式的布尔组合,而不是嵌套的if,便于单独统计每个条件的贡献。
  • 工程结构建议分成数据层、指标层、信号层、执行层四层,各层只依赖下层。

为什么多周期策略特别容易写错

单周期策略的数据只有一张表,时间索引唯一,出错空间有限。一旦引入第二个周期,你手上就有了两条不同频率的时间轴,而这两条轴的对齐方式有一个极易踩错的细节:在15分钟这一格上,当前的4小时K线通常还没有走完

假设现在是14:15,4小时K线的区间是12:00到16:00。这根K线的收盘价、最高价、成交量此刻都还在变动。如果你直接取这根K线算MA或ATR,你用到的是未来一个多小时才会确定的信息。回测里它会安静地提高胜率,实盘里它根本拿不到。

多币种带来的是另一类错误:状态串台。很多人写循环时把持仓、开仓价、止损价定义成外层变量,循环到第二个币种时覆盖了第一个币种的状态。曲线看起来仍然平滑,但持仓记录已经完全不可信。

这两类错误都不会抛异常。它们的唯一症状是回测收益偏高、回撤偏小。看到夏普突然从 0.8 跳到 2.5,第一反应应该是查对齐和状态,而不是庆祝。

时间戳对齐:只用已收盘的大周期数据

对齐的目标是:对任意一个小周期时刻 t,取到的大周期指标必须来自 t 之前已经完全收盘的那根K线。实现上有两种等价写法,推荐第二种,因为它不依赖具体库的默认行为。

第一种是重采样后整体前移一格,也就是先按大周期聚合,再对指标序列做 shift(1),最后前向填充到小周期索引上。第二种是显式计算每个小周期时刻所属的大周期起点,再手动取上一个起点的值。第二种写法冗长,但每一步都能单独打印检查。

import pandas as pd

def align_higher_tf(df_low: pd.DataFrame,
                    rule: str = "4H") -> pd.DataFrame:
    """把大周期指标安全地对齐到小周期索引上。
    df_low: 索引为 UTC DatetimeIndex 的小周期 OHLCV
    返回: 与 df_low 同索引的大周期指标列
    """
    # 1) 聚合成大周期。label/closed 都用 left,
    #    保证 12:00 这根代表 [12:00, 16:00)
    agg = {"open": "first", "high": "max",
           "low": "min", "close": "last", "volume": "sum"}
    htf = df_low.resample(rule, label="left", closed="left").agg(agg)

    # 2) 在大周期上算指标
    htf["ma"] = htf["close"].rolling(50).mean()
    htf["trend_up"] = htf["close"] > htf["ma"]

    # 3) 关键一步:整体后移一根。
    #    12:00-16:00 这根的结果,最早只能在 16:00 之后使用
    htf_shift = htf[["ma", "trend_up"]].shift(1)

    # 4) 前向填充回小周期索引
    out = htf_shift.reindex(df_low.index, method="ffill")
    out.columns = ["htf_ma", "htf_trend_up"]
    return out


def assert_no_lookahead(df_low, out, rule="4H"):
    """自检:任一时刻的 htf 值,其来源K线必须已收盘。"""
    bucket = df_low.index.floor(rule)
    # 当前所属桶的数据绝不能出现在 out 里
    assert out["htf_ma"].groupby(bucket).nunique().max() <= 1
    return True
resample 的 label 与 closed 参数必须显式写出。不同 pandas 版本、不同 rule 的默认值不一致,依赖默认值是最隐蔽的对齐bug来源。
python

信号合并:把共振写成显式布尔表达式

多周期共振的常见需求是:大周期确认方向,小周期负责择时。写法上不要用嵌套 if 一层层套,而是把每个条件命名成一个布尔列,最后用一行逻辑运算合成。这样做有三个实际好处。

一是可统计。你能分别算出每个条件单独触发的次数、以及去掉某个条件后收益怎么变,这是判断条件是否真的有贡献的唯一办法。二是可调试。任何一根K线上你都能打印出四个布尔值,立刻知道信号为什么没触发。三是可扩展,加一个周期只是多一列。

另外要给共振设一个时效窗口。大周期在16:00确认转多,小周期在三天后才给出入场信号,这时大周期的确认早已过期。实践中给一个 N 根K线的有效期,超过就作废。

def build_signal(df, htf, valid_bars: int = 12):
    d = df.join(htf)

    # --- 每个条件一列,命名清楚 ---
    d["c_htf_trend"] = d["htf_trend_up"].fillna(False)
    d["c_ltf_cross"] = (d["close"] > d["ma20"]) & \
                       (d["close"].shift(1) <= d["ma20"].shift(1))
    d["c_vol"] = d["volume"] > d["volume"].rolling(96).mean()
    d["c_not_extreme"] = d["rsi14"] < 75

    # --- 大周期确认的时效性 ---
    #  确认后 valid_bars 根内有效
    fresh = d["c_htf_trend"].rolling(valid_bars).max().astype(bool)

    d["long_entry"] = (fresh
                       & d["c_ltf_cross"]
                       & d["c_vol"]
                       & d["c_not_extreme"])
    return d


def condition_contribution(d, cols, target="long_entry"):
    """逐个去掉条件,看信号数量变化,判断哪个条件在真正筛选"""
    base = int(d[target].sum())
    rep = {}
    for c in cols:
        others = [x for x in cols if x != c]
        n = int(d[others].all(axis=1).sum())
        rep[c] = {"base": base, "without": n}
    return rep
python

状态管理:每个币种一个独立状态机

多币种回测的正确抽象是:一个 Position 对象负责单个 symbol 的全部状态,一个 Portfolio 负责资金与总风险约束。信号计算是无状态的纯函数,状态只在执行层改变。这条边界划清了,串台问题基本不会发生。

单个币种需要维护的状态比新手预期的多:是否持仓、方向、开仓价、开仓时间、当前止损位(移动止损会变)、已加仓次数、本轮最高浮盈(用于回撤止盈)。这些都是 per-symbol 的,任何一个漏掉都会导致逻辑退化。

组合层要管的是跨币种约束:同时最多持仓几个、总名义敞口上限、相关性过高的币种不同时开。加密市场里绝大多数山寨币与 BTC 高度同向,所谓「分散到十个币」在下跌时往往是同一笔风险。

from dataclasses import dataclass, field
from typing import Dict, Optional

@dataclass
class Position:
    symbol: str
    side: int = 0            # 0 空仓, 1 多, -1 空
    qty: float = 0.0
    entry_px: float = 0.0
    entry_ts: Optional[object] = None
    stop_px: float = 0.0
    peak_pnl: float = 0.0    # 本轮最高浮盈, 用于回撤止盈
    adds: int = 0

    @property
    def is_open(self) -> bool:
        return self.side != 0

    def reset(self):
        self.side, self.qty, self.adds = 0, 0.0, 0
        self.entry_px = self.stop_px = self.peak_pnl = 0.0
        self.entry_ts = None


@dataclass
class Portfolio:
    cash: float
    max_positions: int = 5
    risk_per_trade: float = 0.01      # 单笔风险 1%
    positions: Dict[str, Position] = field(default_factory=dict)

    def get(self, symbol: str) -> Position:
        # 关键: 按 symbol 惰性创建, 绝不共用
        if symbol not in self.positions:
            self.positions[symbol] = Position(symbol)
        return self.positions[symbol]

    def open_count(self) -> int:
        return sum(p.is_open for p in self.positions.values())

    def can_open(self) -> bool:
        return self.open_count() < self.max_positions

    def size_by_risk(self, equity, entry, stop) -> float:
        risk_amt = equity * self.risk_per_trade
        per_unit = abs(entry - stop)
        return 0.0 if per_unit <= 0 else risk_amt / per_unit


# 主循环: 时间在外, 币种在内, 状态从 portfolio 取
def run(bars_by_symbol, pf: Portfolio, timeline):
    for ts in timeline:
        for sym, df in bars_by_symbol.items():
            if ts not in df.index:
                continue          # 该币此刻无数据, 跳过而非补零
            row = df.loc[ts]
            pos = pf.get(sym)     # 每个币独立状态
            step(pos, pf, row, ts)
python

工程结构:四层分工与目录组织

代码规模超过几百行之后,分层比技巧重要。推荐的划分是数据层、指标层、信号层、执行层,规则是上层可以调用下层,下层绝不反向依赖。这样任何一层都能被单独测试。

四层职责划分与禁止事项
职责输入 / 输出禁止做的事
数据层 data/拉取、缓存、清洗K线,统一为 UTC 索引,去重补缺交易所原始数据 / 标准 OHLCV DataFrame不做任何指标计算,不判断信号
指标层 features/计算 MA、ATR、RSI,做多周期对齐与 shiftOHLCV / 带指标列的 DataFrame不引用持仓状态,不下单
信号层 signals/把指标组合成布尔入场/出场列(纯函数)指标 DataFrame / 布尔信号列不知道账户有多少钱,不算仓位
执行层 engine/撮合、仓位、止损、组合约束、绩效统计信号 + 资金 / 成交记录与净值曲线不再修改指标,不偷看未来行

目录上对应四个包,再加 configs/ 存参数、tests/ 存单元测试、reports/ 存回测产出。参数一律走配置文件,不要散落在代码里的魔法数字——做参数扫描时你会感谢自己,具体方法见过拟合识别

撮合与绩效部分的具体实现,可以参考从零搭建最小回测引擎;那篇里的撮合循环可以直接套在本文的 Portfolio 之上。

多币种数据的三个坑

第一个坑是上线时间不同。ETH 有多年数据,某个新币只有三个月。如果你直接取时间交集,等于只用最短那个币的历史;如果直接取并集又会在早期出现大量 NaN。正确做法是各币种独立跑自己的可用区间,在组合层按实际在场币种数计算权重。

第二个坑是退市与改名。回测样本里只包含今天还在交易的币种,就是典型的幸存者偏差,结果会系统性偏乐观。至少要在报告里注明样本构成,最好保留已下架币种的历史数据。

第三个坑是不同币种的最小下单量和价格精度不同。回测里用浮点数随便下 0.0037 个 BTC 没问题,实盘会被拒单。建议在执行层加一次量化取整,并把因取整而无法下单的信号记录下来。

多币种数据的三个坑与处理方式
表现错误处理正确处理
上线时间不同新币只有几个月历史,老币有数年取时间交集,等于只用最短那段各币独立跑可用区间,组合层按在场币种数算权重
退市与改名样本里只剩今天仍在交易的币种默认忽略,结果系统性偏乐观保留下架币种历史,报告中注明样本构成
精度与最小下单量不同回测可下 0.0037 BTC,实盘被拒单浮点数直接成交,回测无法执行执行层量化取整,并统计被丢弃的信号数

第三个坑对小资金账户尤其明显。若账户只有几千 USDT 又要分散到多个币种,单笔名义金额可能低于交易所的最小下单金额,信号会被静默丢弃。把这类信号计入统计后,你才知道这套配置实际能执行的信号只有原设计的多少比例。

把「因精度/最小金额被丢弃的信号数」当成一个正式指标输出。小资金账户上这个数可能占到信号总量的两成以上,直接决定策略是否可执行。

怎么验证你的实现是对的

工程正确性不能靠肉眼看曲线,要靠可重复的检查。下面五项是最低要求,每次改动核心逻辑后都应该跑一遍,成本很低但能拦住绝大多数隐蔽错误。

  1. 单币种退化测试:把多币种引擎只喂一个币,结果必须和单币版本逐笔一致。
  2. 时间倒序测试:故意把大周期 shift 去掉,收益应显著上升。如果没变化,说明大周期条件其实没起作用。
  3. 状态泄漏测试:跑两个完全独立的币,把其中一个的行情整体乘以 2,另一个的成交记录必须一字不变。
  4. 时间戳边界测试:单独打印 15:45、16:00、16:15 三个时刻取到的 htf 值,确认在 16:00 之后才切换。
  5. 成交明细抽查:随机抽 10 笔成交,手工核对开仓时刻的指标值是否满足入场条件。

这五项做完,剩下的分歧就是策略逻辑层面的了,可以进入Walk-Forward 验证阶段。在此之前的任何收益数字都不值得讨论,因为你还不确定它们是策略的功劳还是bug的功劳。

最后提醒一句关于工程成本的现实:多周期多币种的代码量通常是单周期单币的三到五倍,其中大半用在数据与状态管理上,而不是策略思路。这部分投入无法省略,也无法靠更聪明的信号绕过。

本文所有代码为教学用途的结构示例,未包含交易所限频、断线重连、部分成交等实盘必需处理。任何回测结论均为历史数据推演,不构成收益承诺,更不构成投资建议。

什么是常见问题?

多周期一定要用 shift(1) 吗?直接用当前大周期K线不行吗?

不行,除非你在实盘里也能接受同样的信息。当前大周期K线尚未收盘,它的收盘价要到该周期结束才确定,回测中使用它等于提前知道未来。唯一的例外是你只用该K线的开盘价——开盘价在周期开始时就已确定,可以安全使用。

多币种应该共用一套参数,还是每个币单独调参?

优先共用一套参数。每个币单独调参会把参数数量乘以币种数,过拟合风险急剧上升,而且你无法判断某个币的好表现是策略有效还是拟合了噪声。如果一套参数在多数币上都能工作,这本身就是策略稳健性的证据。

时间轴应该按币种取交集还是并集?

按并集建立总时间轴,但每个币种只在自己有数据的区间内交易,无数据时跳过而不是补零或前向填充。补零会造成虚假的横盘行情,前向填充会让指标误以为价格稳定,两者都会污染结果。

一次持仓几个币种比较合理?

这取决于风险预算而不是分散的直觉。若单笔风险设为账户 1%,同时持 5 个高相关币种在同向下跌时的实际风险接近 5%,而不是分散后的更低值。加密市场的山寨币与 BTC 相关性常在 0.8 以上,建议按「等效单一风险」而非币种数量来限制。

风险提示:量化交易同样存在亏损风险。本站所有策略、信号、收益数据均为历史回测数据,不构成收益承诺,也不构成投资建议。加密货币波动剧烈,请先用小资金验证,并遵守所在地法律法规。

📺 相关视频

TradingView Automated Trading with AI
TradingView Automated Trading with AI
AI自动化交易演示(OctoBot)
📚 参考来源
Google AI 优化指南(2026-06更新)

developers.google.com/search/docs/fundamentals/ai-optimization-guide — Google官方对生成式AI搜索优化的指引。

SE Ranking AI引用研究

seranking.com/blog/ai-search-research/ — AI答案引用来源研究(44%引用来自页面前30%)。

量化分析维基百科

en.wikipedia.org/wiki/Quantitative_analysis_(finance) — 量化分析的基础定义与学术脉络。