Sequoia-X 项目研究站 · 新增策略

Sequoia-X 新增策略:四步把私有逻辑接进流水线

这套系统的扩展点很明确:继承 BaseStrategy、实现 run() 返回股票代码列表、给类设一个 webhook_key、把它加进 main.py 的策略列表。做完这四步,它就会和其他七个策略一样,在每天补完增量之后自动执行,命中结果推到对应的飞书群。

扩展方式:新增策略类 + 注册不需要改动框架其他部分依据 strategy/base.py 与 main.py(2026-09 核验)
继承基类BaseStrategy,拿到 engine 与 settings
实现 run()返回股票代码列表
设置 webhook_key决定推送到哪个群
注册到 main.py策略列表顺序即执行顺序
策略契约示意(依据 strategy/base.py 与 main.py 整理,非官方流程图)。
依据来源官方 README · pyproject · .env.example
本页引用sequoia_x/strategy/base.py · main.py · pyproject.toml
核验方式源码静态核验 + 本站实测(2026-10-10 在 Windows / Python 3.11.9 真机跑通安装、回填、日常运行与推送)
核验基准commit 444c0db(2026-05-09)· Python ≥3.10 · 2026-10-12 质检复核(含 2026-10-10 本站实测)
Contract

Sequoia-X 的 BaseStrategy 契约是什么?

约束很少,但每一条都有明确用途。

契约项定义适用场景注意点
基类class BaseStrategy(ABC),用抽象方法强制子类实现所有具体策略的共同父类不实现 run() 的类无法实例化
抽象方法@abstractmethod def run(self) -> list[str]执行选股逻辑的入口返回空列表表示当天没有命中,不会触发推送
类属性webhook_key: str = "default"决定推送到哪个飞书群子类覆盖它(如 "rps"),再在 .env 里配同名键
构造参数__init__(self, engine: DataEngine, settings: Settings)拿到数据引擎与配置源码把它存成 self.engine / self.settings,新策略直接复用
异常边界现有策略在逐只循环内 try/except 并记 warning单只标的异常不应影响全市场扫描这是既有实现的写法约定,新策略建议保持一致
日志用 get_logger(__name__) 取命名 logger输出可追溯的执行记录同名 logger 幂等,不会重复叠加 handler
Steps

给 Sequoia-X 接自定义策略要走哪五步?

下面的骨架与现有策略结构一致,命名与注释风格照仓库保持。

  1. 新建策略文件

    在 sequoia_x/strategy/ 下新增 my_signal.py,导入 BaseStrategy、get_logger 与 pandas。文件名与类名风格参照现有策略(如 ma_volume.py 对应 MaVolumeStrategy)。

  2. 实现 run() 并返回代码列表

    用 self.engine.get_local_symbols() 拿全市场代码,逐只 self.engine.get_ohlcv(symbol) 取日 K,做向量化计算后把命中的代码 append 进列表返回。需要 60 日均线这类长窗口时,先判断 len(df) 是否够。

  3. 设置 webhook_key

    给类加一行 webhook_key = "my_signal",并在 .env 里补 STRATEGY_WEBHOOK_MY_SIGNAL(不配则回落到默认 Webhook,也能跑)。

  4. 注册到 main.py

    在导入区加一行 import,在 strategies 列表里追加实例 YourSignalStrategy(engine=engine, settings=settings)。列表顺序就是执行顺序。

  5. 跑一次验证

    先跑 python main.py 看日志里是否出现「YourSignalStrategy 选出 N 只股票」,再确认命中的推送落在了预期群里。空命中时只会有日志、不会有卡片。

策略骨架(结构与仓库现有策略一致)"""自定义信号策略:说明你判断什么形态。""" from sequoia_x.core.logger import get_logger from sequoia_x.strategy.base import BaseStrategy logger = get_logger(__name__) class YourSignalStrategy(BaseStrategy): """一句话说明选股条件。""" webhook_key: str = "my_signal" _MIN_BARS: int = 20 def run(self) -> list[str]: symbols = self.engine.get_local_symbols() selected: list[str] = [] for symbol in symbols: try: df = self.engine.get_ohlcv(symbol) if len(df) < self._MIN_BARS: continue # 向量化计算:rolling / shift / rank,不要用 iterrows df["ma20"] = df["close"].rolling(20).mean() last = df.iloc[-1] if last["close"] > last["ma20"]: selected.append(symbol) except Exception as exc: # noqa: BLE001 logger.warning(f"[{symbol}] YourSignalStrategy 计算失败:{exc}") continue logger.info(f"YourSignalStrategy 选出 {len(selected)} 只股票") return selected
先看现成的再动手:ma_volume.py 是最短的一份(约 60 行),适合当模板;rps_breakout.py 展示了「一次性读全表做横截面排名」的另一种取数方式。把这两个文件对着读一遍,比从零设计快得多。
Conventions

Sequoia-X 策略有哪四条写法约定?

这些约定来自现有七个策略的一致写法,不是硬性限制,但破坏它们会带来实际问题。

约定具体做法适用场景破坏后的后果
向量化计算用 rolling / shift / rank 等 pandas 方法,源码注释直接写「严禁 iterrows」全市场逐只计算逐行遍历会让日常运行从分钟级退化到不可接受
窗口隔离比较基准要排除当日,例如 high.shift(1).rolling(20).max()判断「突破前 N 日高点」这类条件不隔离会形成「当日包含自身」的自指比较,条件恒成立
最小样本用类常量声明(如 _MIN_BARS)并提前 continue需要 20 / 40 / 60 根窗口的策略窗口不足时得到 NaN,静默漏选或报错
单只容错逐只 try/except,异常写 warning 后 continue个别标的脏数据一个异常会让整个策略中断,后续股票全部漏算
可调参数外置用类属性(如 rps_period = 120)而不是函数内魔法数字需要试验不同阈值时参数埋在下标里时很难批量调整与比对
结果不排序或明确排序要排序就写明依据(如海龟按流通市值降序),不排就保持遍历顺序结果可解释性隐式顺序会让人误以为清单有优先级含义
Data access

Sequoia-X 的两条取数路径分别是什么?

现有策略里两种写法都有,选错了要么慢要么算不出。

对比项逐只读库 get_ohlcv一次性读全表 read_sql适用场景
调用方式self.engine.get_ohlcv(symbol),内部按 symbol 查询并排序sqlite3.connect(self.engine.db_path) + pd.read_sql前者适合形态类,后者适合横截面类
内存占用每次一只,峰值小整表进内存,随库增长机器内存有限时优先前者
能否做横向排名不能(看不到其他标的)可以,如 rank(pct=True)相对强度、行业内排名这类需求
现有例子海龟、均线放量、高窄旗形、涨停洗盘、上升跌停RPS 突破可以在仓库里各找一份对照
异常表现包在逐只 try 里,异常不影响其他标的读取失败时整体返回空列表整表读取的失败面更大
额外联网一般不联网(海龟查市值是例外)不联网定增公告策略则完全走外部接口,不走库
Tests

Sequoia-X 的测试与代码规范怎么落地?

仓库自带测试与静态检查配置,扩展时建议沿用。

项目配置适用场景注意点
测试框架pytest(testpaths = ["tests"])运行 pytest 即可执行全部测试新增策略建议在 tests/ 下补对应用例
属性测试hypothesis(dev 依赖 >=6.100)对边界条件做随机化验证阈值类策略尤其适合验证「窗口不足时的行为」
模拟对象pytest-mock(dev 依赖 >=3.12)替换数据引擎与网络请求现有测试文件名可按模块对照:config / data_engine / feishu / logger / main / strategy
代码检查ruff:line-length = 100、target-version = "py311"、select = ["E","F","I","UP"]保持风格一致import 顺序(I)与语法升级(UP)也在检查范围内
Python 版本requires-python >= 3.10,ruff 目标 py311类型标注与语法可用范围写 list[str] 这类内置泛型标注需要 3.9+
依赖锁定仓库含 uv.lock可复现的环境新增第三方依赖时应同步更新锁文件
关于测试的边界:仓库里的测试覆盖配置、数据引擎、通知、日志、主入口与策略几个模块,属于工程正确性层面的自测;项目本身不含回测或绩效评估测试,因此新增策略的「有效性」不在代码测试能回答的范围内。
Backtest

新策略写完怎么验证:一段可运行的后验脚本骨架

项目不含回测引擎,所以验证要自己写。下面这段骨架只做一件事:把历史信号取出来,按「信号日 → 次日开盘买入 → 持有 N 个交易日」算后验收益,并逐笔扣费。它最大的价值是强制你把成交口径写死。

import sqlite3, pandas as pd

HOLD_DAYS = [1, 5, 10, 20]                  # 持有周期(交易日)
FEE, TAX, SLIP = 0.00025, 0.0005, 0.001     # 佣金 / 卖出印花税 / 单边滑点(按你的券商改)

conn = sqlite3.connect("data/sequoia_v2.db")
bars = pd.read_sql(
    "SELECT symbol, date, open, high, low, close, volume, turnover "
    "FROM stock_daily ORDER BY symbol, date", conn)

def find_signals(bars):
#   把你的策略条件写在这里;硬约束:只能用当日及之前的数据
    out = []
    for symbol, g in bars.groupby("symbol"):
        if len(g) < 21:
            continue
        g = g.copy()
        g["high_20"] = g["high"].shift(1).rolling(20).max()   # shift(1) 防未来函数
        for i in range(20, len(g) - 1):                       # 留出次日,保证能成交
            row, prev = g.iloc[i], g.iloc[i - 1]
            if (row["close"] > row["high_20"] and row["turnover"] > 1e8
                    and row["close"] > row["open"] and row["close"] > prev["close"]):
                out.append((symbol, i))                        # (股票, 信号在序列中的位置)
    return out, bars

def forward_return(g, i, n):
    # 次日开盘买入 → 第 n 个交易日开盘卖出(若样本不够则丢弃)
    if i + n >= len(g):
        return None
    buy, sell = g["open"].iloc[i + 1], g["open"].iloc[i + n]
    gross = sell / buy - 1
    cost = FEE * 2 + TAX + SLIP * 2          # 双边佣金 + 卖出印花税 + 双边滑点
    return gross - cost

signals, bars = find_signals(bars)
rows = []
for symbol, i in signals:
    g = bars[bars["symbol"] == symbol]
    for n in HOLD_DAYS:
        r = forward_return(g, i, n)
        if r is not None:
            rows.append({"symbol": symbol, "signal_idx": i, "hold": n, "ret": r})

res = pd.DataFrame(rows)
print(res.groupby("hold")["ret"].agg(["count", "mean", "median",
      lambda s: (s > 0).mean()]).rename(columns={"<lambda_0>": "win_rate"}))
口径决策两种常见写法本站建议与理由
信号日定义收盘价算信号 / 盘中触及算信号用收盘价算——本项目所有策略都读最后一根 K 线,盘中触及无法用日线复现
成交价信号日收盘价 / 次日开盘价 / 次日均价次日开盘价最保守也最容易复现;用信号日收盘价买入等于偷了一个交易日
持有周期固定 N 个交易日 / 固定止盈止损先固定 N(1/5/10/20)分别统计,再考虑加止盈止损;项目本身不含止损逻辑
成本不加成本 / 只加佣金 / 佣金+印花税+滑点三项都加。形态类信号常出现在放量或涨停附近,零滑点会系统性高估
异常样本全部计入 / 剔除涨跌停无法成交的样本先全部计入跑一遍,再单独统计「买入日一字涨停」这类无法成交的样本占比
基准无基准 / 与宽基指数同期对比库里没有指数数据(stock_type=1 已过滤),基准要另外取,否则无法区分「策略有效」与「市场给的」
这段骨架不是官方工具:它是本站按项目表结构与信号定义写的教程扩展(④级),目的是让口径可复现,不产出任何收益承诺。同一份脚本换了上面任一口径,结果都会变,所以必须把口径一起写进记录。
Pitfalls

用这个库做回测会遇到哪些陷阱?

下面每一条都能让回测结果「好看」但不可用。它们的共同点:不在策略条件里,而在数据与成交假设里。

陷阱为什么发生自检方法修正做法
未来函数用信号日收盘价买入,或让特征用到当日之后的数据(例如把 rolling 写成了居中的窗口)把回测里的「买入价」临时改成次日开盘价,看收益是否大幅下滑——若下滑剧烈,说明原结果依赖未来数据买入价统一取 open.iloc[i+1];所有滚动窗口一律 shift(1) 后再 rolling
幸存者偏差股票池来自 get_all_symbols(),只含当前上市股票,退市股从未入库统计库内股票数是否明显少于同期真实上市家数在结论里显式声明股票池口径;涉及长期持有结论时补入退市样本
停牌日错位入库过滤 volume = 0,「第 N 个交易日」在停牌股上其实是第 N 个有成交的日子对命中票统计其 K 线根数与区间交易日数量的差用日期索引而不是整数位置计算持有周期;或对停牌股单独剔除
样本重叠同一只股票连续多日命中,这些「笔」的收益高度相关,不是独立样本按 (symbol, 日期) 去重后再统计一次样本量与胜率用去重后的样本给结论;连续命中视为一个持仓周期而不是多笔交易
复权口径混用库内是后复权价,收益里隐含「分红全部再投资」的假设,与实盘现金流不同抽一只高分红股票,对比后复权收益率与实盘实际到手的收益率结论里注明「后复权口径、分红再投资假设」,不要直接等同于账户收益
成本与滑点被忽略项目不含费率模型,容易顺手就按 0 成本跑把成本从 0 改成「佣金+印花税+0.1% 滑点」再跑一遍,看收益缩水幅度高频信号策略尤其要加成本;缩水后仍成立的结论才有参考价值
把命中数当成策略质量放宽阈值 → 命中变多 → 看起来「更活跃」,实际只是噪声增加同时看命中数与后续收益中位数,两者应同向变好用「命中数 + 收益分布 + 回撤」三件套评估,单一指标不作为依据
顺序建议:先修未来函数(改成交价与 shift),再修股票池(幸存者偏差),最后加成本。前两项修完结论可能直接反转,此时再谈成本才有意义。
Out of sample

样本外与参数拟合要守哪三条纪律?

阈值都是策略类里的常量,改一个数字就能让历史好看——这正是过拟合的入口。下面三条纪律能把这个风险压到最低。

  1. 先把区间切成两段,再动手改参数

    # 例:库内 2024-01-01 起,按时间切成 70% / 30%
    cut = "2025-06-30"
    train = bars[bars["date"] <= cut]     # 样本内:可以反复试参数
    test  = bars[bars["date"] >  cut]     # 样本外:只能跑一次
    # 纪律:参数只在 train 上挑,选定后到 test 上只跑一次并如实记录

    最容易违反的一点是「在样本外跑完不满意又回去调参」——那等于把样本外也用成了样本内。要改就重新切一段新的样本外区间。

  2. 记录每一次参数改动,连同改动的理由

    # 建议在策略类里保留一份改动记录,而不是只留最终值
    class MyStrategy(BaseStrategy):
        webhook_key = "my_signal"
        window = 20          # 2026-09-20 由 15 调整为 20:15 在样本内命中过多,改用月线级别
        threshold = 1.5      # 保持默认,未调

    没有理由的调参最后只剩一堆数字。半年后回看,你无法判断某个阈值是「有依据」还是「试出来的」。

  3. 用样本外结果说话,并公布失败的那部分

    print(res.groupby("hold")["ret"].agg(["count", "mean", "median"]))
    # 同时输出:去重后的样本量、无法成交样本占比、分年度收益
    # 结论里必须包含「样本外表现」与「未通过的假设」

    本站对任何策略都不给有效性结论,原因就在这里:形态类规则的有效性会随市场环境变化,样本外表现才是决定它能不能用的依据。所有结果仅供研究与学习,不构成投资建议。

Verification template

策略验证模板:怎么先把口径固定下来?

写策略容易,证明策略有用很难。下面的模板把「验证一次策略」需要固定的十二项全部列出来——先照这张表填完,再决定要不要相信任何结论。

口径项为什么必须写清本站的取值示例(本轮实测)
项目版本 / commit规则可能随时改,没有版本号的结论无法复核444c0db(2026-05-09,master HEAD)
回测区间形态有效性随市场环境变化库内 668 个交易日:2024-01-02 ~ 2026-10-09
股票池是否含退市股不含退市股会天然乐观(幸存者偏差)不含:status=1 且 type=1 过滤后 5224 只,退市股不在库内
复权口径影响收益率与形态比例库内为后复权(adjustflag=1);成交价类计算需另取不复权
信号日定义决定「什么时候算触发」K 线最后一根(df.iloc[-1]),需收盘后才成立
成交日与成交价不能用信号日收盘价成交三选一并写明:次日开盘 / 次日均价 / 次日收盘
买不到的样本怎么算一字板、停牌会让回测虚高显式剔除或按「顺延到下一可成交日」处理,并统计占比
交易成本小额样本里成本能吃掉全部差异佣金(双边)+ 印花税(卖出)+ 你估计的滑点,逐项写明数值
持有期同一信号在不同持有期结论可能相反至少给 1 / 5 / 10 / 20 个交易日四档
基准没有基准就分不清是策略有效还是大盘好沪深300 或中证全指;库内没有指数数据,需另取
样本量 / 分布样本太少时任何统计量都不稳定写清信号总数、涉及标的数、按日期分布(是否集中在少数几天)
样本内 / 样本外参数是在全样本上调出来的,必须留出验证段例如 2024-01 ~ 2025-12 定参数、2026-01 之后只验证
  1. 先把信号与成交拆成两列,再谈收益

    -- 每个信号日,取「信号日收盘」与「下一个有数据的交易日的开盘/收盘」
    WITH seq AS (
      SELECT symbol, date, close,
             LEAD(date)  OVER (PARTITION BY symbol ORDER BY date) AS exec_date,
             LEAD(open)  OVER (PARTITION BY symbol ORDER BY date) AS exec_open,
             LEAD(close) OVER (PARTITION BY symbol ORDER BY date) AS exec_close
      FROM stock_daily)
    SELECT * FROM seq WHERE exec_date IS NOT NULL;

    这张表是后面一切统计的地基。注意 exec_date 用 LEAD 取「下一根有数据的 K 线」,不是 date + 1——停牌与长假会让自然日口径失真(本站实测 21.3% 的相邻交易日间隔 ≥ 3 天)。

  2. 三条会让结论作废的做法(写报告前自查)

    做法为什么作废正确做法
    用信号日收盘价当成交价收盘价与信号同时产生,属未来函数用 exec_open / 次日均价 / exec_close,并写明三选一
    在全样本上调阈值再报全样本收益参数拟合进样本,样本外大概率失效切出样本外区间,只报样本外读数
    用「现在的股票池」回溯历史退市股不在池内,长期结论必然偏乐观声明股票池口径,或补入已退市样本
  3. 本站明确不做的事

    本站不发布任何收益率、胜率、年化、回撤数字。原因不是保守,而是:本项目没有回测引擎(仓库无 backtest 模块)、没有官方绩效数据、本站本轮也没有做样本外验证。用三五个样本算出「某个数字」并不难,难的是让那个数字经得起上面十二项口径的检查——本轮只把检查清单交给你。

怎么用这张表:如果你复现出结果并想写出来,把前十二项逐条填进方法论小节,任何人照着同一份口径重跑都应该得到同一个结论;如果换了口径结论就变,那说明结论还不成立。以官方源码与交易所规则为最终依据。
FAQ

二次开发常见问题

给 Sequoia-X 加策略必须继承 BaseStrategy 吗?

如果在 main.py 的策略列表里使用,是的——列表元素类型标注为 BaseStrategy,而基类用抽象方法强制子类实现 run()。若只是想临时算一下,也可以直接写脚本调用数据引擎,但那样不会进入每日流水线。

Sequoia-X 新策略怎么推到自己的群?

给类设一个 webhook_key(如 "my_signal"),然后在 .env 里加 STRATEGY_WEBHOOK_MY_SIGNAL。键名不匹配时会回落到 FEISHU_WEBHOOK_URL,也就是说「配错了也能跑,但会进错群」。

Sequoia-X 能用分钟线或财务数据写策略吗?

不能直接支持。本地库只存日 K(后复权),表里也没有财务字段;要用其他数据需要自己扩展取数与存储,那已经超出「加一个策略类」的范围。数据边界见数据层页。

Sequoia-X 策略执行顺序会影响结果吗?

不影响各自的判定结果——每个策略独立读库、独立返回清单,彼此不共享状态。顺序只决定日志打印与推送的先后。反过来说,同一只股票可能同时被多个策略选中,推送里会出现多条。

怎么判断 Sequoia-X 策略是不是太慢?

看日志里策略行之间的时间差。逐只读库的方式耗时与标的数成正比,全表读取则受内存与分组计算影响;如果某个策略明显拖慢整体,优先检查是否误用了逐行遍历。

改了源码要重装吗?

用 pip install . 安装的话,源码改动需要重新安装或使用可编辑安装(pip install -e .)才能生效;用 uv sync 管理环境时同理。具体以官方 README 与你的环境管理方式为准。

回测收益多少算好?

本站不给阈值,因为「多少算好」取决于你的持有周期、成本假设与基准。可操作的标准是三条:①收益中位数是否为正,而不是只看平均值;②加入佣金、印花税与滑点后是否仍然为正;③样本外区间是否与样本内方向一致。三条都过才有讨论价值,任何一条不过都应视为未验证。所有结果仅供研究,不构成投资建议,以你自己的实测数据为准。

怎么确认我的回测没有未来函数?

两个自检:①把买入价从「信号日收盘价」改成「次日开盘价」(open.iloc[i+1]),若收益大幅下滑,说明原结果依赖了当日收盘这个未来信息;②检查所有滚动窗口是否都先 shift(1) 再 rolling——本项目海龟突破的 high.shift(1).rolling(20).max() 就是标准写法。具体口径以官方源码为准。

扩展前怎么看清失败路径?

排障页把配置、数据、交易日与推送四类问题分开处理,并给出安全重跑方式。