简介Northstar盈富量化交易软件是一套面向程序员的自动交易平台覆盖期货、股票、外汇与加密货币等市场已对接国内期货CTP接口和老虎证券美股港股通道。它提供一站式交易所适配、灵活策略框架、多账户跨市套利、高精度历史行情回放、自动化模组管理与私有化部署能力适合具备基本Java知识、希望编写并运行个性化量化策略的中高级开发者。压缩包共1344个文件主体为1157个Java源码另有60个JS脚本、24个Vue组件、XML/JSON/yml配置以及DLL/SO行情交易动态库、sh/bat启动脚本和说明文档整体约14.25MB。目前已有932人学习下载解压后可直接研读多种策略编写范例结合接口定义、协议文件与示例配置搭建自己的自动交易环境也可按需扩展CTP、老虎证券等通道并基于内置风控与回放功能进行策略验证二次开发参考价值较高。1. Northstar盈富量化交易软件到底值不值得拆先说结论再给路线第一次解压 Northstar盈富量化交易软件.rar 之前我以为它又是一堆策略模板的离线包。真正跑完一遍才发现它的价值不在那几个策略文件而在从行情清洗、因子注册、回测引擎到订单执行模块的完整闭环。和我之前拆过的很多“只有策略代码”的仓库不同这套软件把数据层和交易层也接好了适合有 Python、pandas 基础想直接上手做量化回测但不想从零搭框架的人。下面我把解压后的目录角色、参数怎么改、数据怎么喂、寻优和实盘对接的坑拆开讲尽量让你照着能复现。2. Northstar盈富量化交易软件到底包含什么解压结构与核心模块2.1 压缩包里的目录层级与文件角色我拿到的这个包是 rar 压缩格式解压后主目录结构是这样的Northstar/ ├── config/ │ └── settings.yaml ├── core/ │ ├── datafeed.py │ ├── factor.py │ ├── risk.py │ └── broker.py ├── strategies/ │ ├── example_momentum.py │ └── example_meanrevert.py ├── data/ │ ├── raw/ │ └── clean/ ├── scripts/ │ ├── run_backtest.py │ ├── optimize.py │ └── start_paper.py ├── tests/ │ └── smoke_test.py ├── requirements.txt └── northstar.pyconfig 目录下只有一个 settings.yaml所有全局参数都从它读包括初始资金、手续费、滑点、时区和行情路径。core 里面几个文件是整套软件的地基datafeed.py 负责把 CSV 或 parquet 转成内部统一的 bar 序列factor.py 管理因子注册和计算结果缓存risk.py 处理止损止盈、仓位上限和单笔风险预算broker.py 负责订单路由模拟盘和实盘接的是同一套接口。strategies 目录里放的是策略模板不是拿来就能赚钱的“圣杯”你需要在这里写自己的信号逻辑。data 下的 raw 和 clean 分别对应原始行情和清洗后的数据。sample_daily.csv 只是一个样例跑正式回测前要把自己的历史行情放进来。scripts 里三个入口脚本分别对回测、参数寻优和模拟盘tests/smoke_test.py 是安装完环境后必跑的冒烟测试。我不太纠结压缩格式但这里有个实际区别rar 包解压后保留了 core 目录下脚本的可执行权限部分 Linux 环境下直接./northstar.py就能起服务如果日后拿到 zip 版本第一件事是给northstar.py和 scripts 下的脚本加上可执行权限否则会先遇到 “permission denied”。2.2 引擎模式向量化回测和事件驱动回测为什么先问清楚再跑这套软件的 settings.yaml 里有一个核心开关决定你后面所有回测行为。它是这样配置的# config/settings.yaml关键片段 engine: mode: vector_backtest # vector_backtest / event_driven capital: 1000000 # 初始资金 commission_rate: 0.0003 # 单边手续费 slippage_bps: 1.0 # 滑点单位 bp data_path: ./data/raw/sample_daily.csv timezone: Asia/Shanghaimode选vector_backtest时引擎会对 pandas 的 Series 做整段计算速度很快适合日线、单标的、策略信号比较简单的场景。它的缺点是撮合精度低所有订单默认按当前 bar 的收盘价成交手续费和滑点只是事后计提。mode选event_driven时引擎会一根 bar 一根 bar 地推进每个 bar 都触发行情事件、风控检查和订单结算适合分钟线、多标的、需要模拟排队成交的场景代价是速度明显变慢。我见过很多人一上来直接选 event_driven 跑日线结果回测一个品种要几十秒还因为止损单逻辑没写对得到一堆假信号。我的习惯是先用 vector 模式把信号逻辑跑通确认收益来源合理再切成 event_driven 做压力测试。两个模式对比如下模式适用场景撮合精度速度风控模块介入vector_backtest日线、单标的收盘价成交较快不做逐笔风控event_driven分钟线、多标的下一根 bar 撮合较慢每个 bar 检查core/datafeed.py 在 event_driven 模式下会把行情文件切成一根根 bar然后回调给策略模块。策略模块返回信号后risk.py 先检查broker.py 再下单。这种“行情驱动”的设计和很多以策略文件为主、交易接口靠后补的插件式工具不一样它把回测和模拟盘的执行路径统一了。这也是我愿意继续往下拆的原因。3. 喂数据与跑通回测CSV 格式、因子注册和一条回测命令3.1 历史行情字段与清洗标准Northstar 的 datafeed 接受的标准 CSV 字段如下字段类型要求datetimedatetime64[ns]必须带日期分钟线要精确到秒symbolstr同一标的使用固定 IDopen / high / low / closefloat64high max(open, close)low min(open, close)volumeint64必须大于 0amountfloat64可选资金流因子会用到数据清洗我是这样处理的import pandas as pd df pd.read_csv(data/raw/sample_daily.csv, parse_dates[datetime]) df[datetime] df[datetime].dt.tz_localize(Asia/Shanghai) df df.sort_values([symbol, datetime]).drop_duplicates( subset[symbol, datetime], keeplast ) df df[(df[volume] 0) (df[close] 0)].reset_index(dropTrue) df.to_parquet(data/clean/sample_daily.parquet)第一行强制把字符串日期解析成带时区的北京时间避免后面回测时 datetime 变成 naive 导致 bar 错位。sort_values和drop_duplicates是为了处理盘中重复推送的历史数据。volume 0会过滤掉没有成交的 bar这种 bar 在真实数据里很常见但样例数据里通常没有新手容易忽略。close 0是为了排除停牌后异常价格。如果你做的是期货还要额外检查 high 和 low 是否为 0很多近月合约在涨跌停板时会出现 closehighlowvolume0 的假 bar。这种 bar 不清理事件驱动模式下会被当成真实成交价直接污染回测净值。3.2 把自定义因子注册进 Northstar 因子库这个包内置了几个因子但真正要跑自己的策略你得会注册新因子。在 strategies 目录下新建一个策略文件然后这样写# strategies/example_momentum.py from northstar.core.factor import register_factor register_factor(namemomentum_20d, categorytrend) def momentum_20d(df, window20): return df[close] / df[close].shift(window) - 1register_factor是一个装饰器底层逻辑在 core/factor.py 里维护一个全局因子字典。策略文件被导入时装饰器会自动把函数登记到这个字典里。name是配置和策略里引用的名字category只用于日志分组。window是因子参数默认 20之后可以通过回测配置覆盖。这里有个容易忽略的点因子函数只接收一个 DataFrame 并返回一个 Series不要在函数内部去改动传入的 DataFrame也不要调用to_parquet之类写文件的操作。因子引擎会把每个因子的结果缓存到内存如果你的函数有副作用会在多标的回测时串数据。注册完因子后跑一回测只需要一条命令python scripts/run_backtest.py \ --config config/settings.yaml \ --strategy example_momentum \ --params {fast_window: 10, slow_window: 30, stop_loss_pct: 0.02}命令执行后会在日志目录生成回测报告包含净值曲线、最大回撤、Sharpe、Sortino 和交易次数。你要先确认这些指标和自己预期是否一致再做参数寻优。如果日志里出现RuntimeWarning: invalid value encountered in divide且净值曲线断掉多半是前 20 根 bar 的因子值还是 NaN引擎默认把 NaN 当成空仓处理这也是正常的。3.3 前复权还是后复权这个选择直接影响因子数值除权除息多的标的前复权价格看起来连续但历史价格会随着最新分红而动态变化后复权价格是单调累积的不会出现负值或跳空。我一般用后复权数据做因子计算和策略回测最后展示净值曲线时再用前复权对齐当前市场价这样既保证信号稳定又不影响实盘下单时对价格的直觉。Northstar 的 datafeed 支持两个额外的字段factor和adj_close。如果你的原始数据里只有 raw 价格需要在前置脚本里自己计算复权因子。标准做法是df[factor] df[adj_close] / df[close] df[close_adj] df[close] * df[factor]然后因子和信号都基于close_adj计算。不要小看这一步A 股的分红频率不算低银行股和白酒股尤其明显。如果用不复权数据做长期回测会在每个除权日凭空造出一个假跌策略的止损单会被批量触发回测结果直接失真。4. 参数寻优与资金管理让回测结果不只是一条好看的资金曲线4.1 核心参数表手续费、滑点、风控阈值settings.yaml 里的参数是回测的地基。以下是几个影响最大的参数参数默认值作用我的建议capital1000000初始权益模拟盘与实盘保持一致commission_rate0.0003单边手续费按你的券商实际值改slippage_bps1.0滑点成本日线给 1-2分钟线给 2-5stop_loss_pct0.02单笔止损比例结合 ATR 做动态止损更好risk_pct0.01单笔风险预算不要超过 0.02max_position_ratio0.8单标的最大仓位多标的组合建议降到 0.5 以下资金管理模块在 core/risk.py 里核心的仓位计算逻辑大概是这样的from northstar.core.risk import RiskManager risk_manager RiskManager( capital1_000_000, commission_rate0.0003, slippage_bps1.0, stop_loss_pct0.02, risk_pct0.01, ) equity 1_000_000 entry_price 50.0 shares risk_manager.calc_position(equity, entry_price)calc_position的逻辑并不复杂先算出单笔允许亏损的金额equity * risk_pct再把它除以每股市的止损距离entry_price * stop_loss_pct得到应该买的股数。这样设置的仓位既不会在波动大时重仓也不会在波动小时连手续费都覆盖不掉。很多新手把 stop_loss_pct 和 risk_pct 混为一谈。stop_loss_pct 是“价格跌多少我认输”risk_pct 是“这笔单子亏多少我能接受”。如果一个策略的止损距离是 5%你让 risk_pct0.05那单笔亏损就是净值的一大块回测里看起来胜率很高实盘扛不住连续三次止损。4.2 参数寻优网格、随机和贝叶斯怎么选Northstar 的 scripts/optimize.py 支持三种寻优方式网格搜索、随机搜索和贝叶斯搜索。网格搜索适合两到三个参数的小组合参数多了以后组合数量爆炸没必要跑。下面的代码演示了网格搜索的关键逻辑import itertools from northstar.backtest import run_backtest params_grid { fast_window: [5, 10, 20], slow_window: [30, 60, 120], stop_loss_pct: [0.02, 0.05], } results [] for fast, slow, stop in itertools.product(*params_grid.values()): cfg dict(fast_windowfast, slow_windowslow, stop_loss_pctstop) result run_backtest(example_momentum, **cfg) results.append((result.sharpe, cfg)) results.sort(reverseTrue) print(results[:5])这里用itertools.product生成所有参数组合每一组跑一次回测最后按 Sharpe 排序。不要只看年化收益率排序收益高但回撤大的策略实盘体验非常差。我一般优先看 Sharpe 和 Calmar 比率也就是年化收益除以最大回撤。如果参数空间超过四个网格搜索会非常慢。此时用随机搜索指定迭代次数python scripts/optimize.py \ --method random \ --iterations 200 \ --param-space {fast_window: [5, 20], slow_window: [30, 120], stop_loss_pct: [0.01, 0.05]}随机搜索在 200 次迭代内覆盖参数空间的大致分布虽然不一定找到全局最优但能找到“足够好”的区域。贝叶斯搜索更适合连续参数它会根据历史回测结果动态调整下一组候选参数迭代效率最高但要注意贝叶斯优化对噪声敏感如果策略本身不稳定它容易在几个局部最优附近反复试探。寻优最容易犯的错是只看样本内表现。我跑完寻优之后会做一步滚动前推前两年数据当训练窗后一年当验证窗验证窗完全不参与寻优。如果训练窗 Sharpe 0.8验证窗变成 -0.2这组参数大概率是过拟合直接放弃不要再尝试把它拉回来。5. 常见问题与避坑排查这五个坑不绕开回测就是自欺欺人5.1 回测资金曲线很漂亮模拟盘一挂就变形现象同一套策略历史回测年化 30%模拟盘第一周就跑不平成交价总比信号价差一截。原因回测配置里默认execute_on_next_bar是 false等于当前 bar 收盘就立刻成交但模拟盘 broker 是按下一根 bar 的开盘价撮合。这个时间差里市场已经波动了几个基点再加上滑点没有设置导致回测和模拟盘天然错位。解决在 settings.yaml 里把执行方式改成下一根 bar并加上滑点。engine: execute_on_next_bar: true slippage_bps: 2.0我自己的习惯是低频日线策略也给 2 个 bp 的滑点不要觉得数据好就默认 0 滑点。0 滑点的回测结果只能证明信号存在不能证明你实盘能赚到。5.2 最后一根 bar 神秘消失现象回测结束日期是今天但最后一天的交易记录完全没有。原因动量因子用了 shift(20)最后 20 根 bar 的因子值是 NaN。策略在计算信号前没有做缺失值处理引擎默认把 NaN 当成没有信号于是最后一天整个交易日被跳过。实盘里最后一根 bar 是正在形成的处理逻辑不能这样一刀切。解决在策略里单独判断最后一根 bar 的因子是否有效有效才允许下单。import numpy as np last_factor momentum_20d(df).iloc[-1] if np.isfinite(last_factor): signal 1 else: signal 0同时把回测引擎的drop_nan选项关掉让数据尾部保留到最后一根已完成 bar。5.3 时间戳偏移 8 小时现象策略在 15:00 收盘前最后一分钟发单回测记录里却显示 07:00 成交。原因CSV 里的 datetime 字符串是北京时间但 pandas 解析后得到的是不带时区的 naive 时间。引擎内部按 UTC 转 timestamp 后所有 bar 都被往后推了 8 小时。解决清洗阶段把时间列显式设置为北京时间时区并在 settings.yaml 里固定 timezone。df[datetime] pd.to_datetime(df[datetime]).dt.tz_localize(Asia/Shanghai)数据量大的情况下不要先转成 UTC 再转回来直接在入口处统一用本地时间减少不必要的时区换算。5.4 止损单不成交现象策略已经触发了止损日志里也有风控记录但查看持仓发现仓位一点没变。原因在策略代码里直接写了positions[symbol] 0来修改持仓绕过了 broker 和 risk 模块。event_driven 模式下风控只对通过 risk.py 提交的委托单生效直接改持仓字典不会触发任何订单路由。解决所有平仓动作都走 risk 模块暴露的接口不要自己操作持仓字典。risk_manager.submit_stop_order( symbol000300, qty100, trigger_priceentry_price * 0.98, )这个坑很隐蔽因为我第一次拆这套软件时也习惯性地直接改仓位直到模拟盘持仓和日志对不上才意识到 broker 层被绕过了。从那以后我给自己定了个规矩策略里只允许产生信号订单必须交出去。5.5 多品种回测内存耗尽现象加载 300 只股票日线跑回测跑到一半直接 MemoryError进程被杀。原因把每只股票的完整 DataFrame 都 append 到一个 list最后用pd.concat合并。数据量一大内存峰值是原始数据的 3 到 5 倍尤其在 factor 计算时会生成多份中间副本。解决按 symbol 分片存储 parquetdatafeed 读取时只加载当前需要的标的。# core/datafeed.py 改造片段 def load_symbol(code): path fdata/clean/{code}.parquet return pd.read_parquet(path, filters[(symbol, , code)])如果单标的内的分钟线也很大再把 chunk 改成按月读取每个 bar 事件处理完后就释放 DataFrame 引用。多品种回测的瓶颈通常不是 CPU而是内存里的中间变量。6. 把策略挂到模拟盘事件循环、运行日志与健康检查回测和参数寻优都跑完之后最后一步是把策略挂到模拟盘上。核心看三件事事件循环是否按 bar 推进、日志里有没有心跳、断线重连后状态能不能恢复。Northstar 的 scripts/start_paper.py 把 datafeed、broker 和 risk 串成了一个循环# scripts/start_paper.py from northstar.core.datafeed import Feed from northstar.core.broker import PaperBroker from northstar.core.risk import RiskManager feed Feed(data_pathdata/clean, modepaper) broker PaperBroker(initial_cash1_000_000) risk_manager RiskManager(capital1_000_000) while feed.has_next_bar(): bar feed.next_bar() signals strategy.on_bar(bar) for sig in signals: risk_manager.check(sig) broker.place_order(sig) broker.settle(bar) logger.info(fbar{bar.time} equity{broker.equity:.2f})has_next_bar()是循环条件数据源推完当天行情后自动退出。on_bar返回策略信号信号先经过risk_manager.check做风控检查通过后由broker.place_order下单。每个 bar 结束后的logger.info是最重要的心跳权益曲线就是靠这一行日志续命的。我发现很多策略挂模拟盘翻车不是策略问题而是断线重连的姿势不对。数据源断开后重新连上策略里的因子缓冲是空的第一根 bar 会给出一堆伪信号。我现在的做法是在启动命令里加一个--replay参数先把最近 100 根历史 bar 喂给策略但不真实下单等因子缓冲填满后再开始交易python scripts/start_paper.py --replay --bars 100这个参数会预跑最近 100 根已完成的 bar让策略状态恢复到和实盘连续运行一致的水平。我还会在启动脚本里加一个--max_quiet_bars 5的告警参数如果连续 5 根 bar 都没有订单就把当前持仓和 pending 订单全部打到日志里超过 30 分钟没有行情心跳直接进程退出并通知不让半死状态的程序硬撑。验证模拟盘状态是否正常最直接的方法就是盯日志里的权益和持仓数量。我一般每天开盘前先跑一次tests/smoke_test.py它会把样例数据完整跑一遍回测验证环境依赖和核心模块没有坏掉再启动真正的模拟盘。从那以后我每次把新策略挂模拟盘都强制走一遍先 vector 回测再 event_driven 压力测试然后--replay 100预热最后才放真模拟盘。这么做虽然多花半小时但省下的都是实盘上的后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?