简介: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 字段如下:
| 字段 | 类型 | 要求 |
|---|---|---|
| datetime | datetime64[ns] | 必须带日期,分钟线要精确到秒 |
| symbol | str | 同一标的使用固定 ID |
| open / high / low / close | float64 | high >= max(open, close),low <= min(open, close) |
| volume | int64 | 必须大于 0 |
| amount | float64 | 可选,资金流因子会用到 |
数据清洗我是这样处理的:
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'], keep='last' ) df = df[(df['volume'] > 0) & (df['close'] > 0)].reset_index(drop=True) df.to_parquet('data/clean/sample_daily.parquet')第一行强制把字符串日期解析成带时区的北京时间,避免后面回测时 datetime 变成 naive 导致 bar 错位。sort_values和drop_duplicates是为了处理盘中重复推送的历史数据。volume > 0会过滤掉没有成交的 bar,这种 bar 在真实数据里很常见,但样例数据里通常没有,新手容易忽略。close > 0是为了排除停牌后异常价格。
如果你做的是期货,还要额外检查 high 和 low 是否为 0,很多近月合约在涨跌停板时会出现 close=high=low=volume=0 的假 bar。这种 bar 不清理,事件驱动模式下会被当成真实成交价,直接污染回测净值。
3.2 把自定义因子注册进 Northstar 因子库
这个包内置了几个因子,但真正要跑自己的策略,你得会注册新因子。在 strategies 目录下新建一个策略文件,然后这样写:
# strategies/example_momentum.py from northstar.core.factor import register_factor @register_factor(name='momentum_20d', category='trend') def momentum_20d(df, window=20): 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 里的参数是回测的地基。以下是几个影响最大的参数:
| 参数 | 默认值 | 作用 | 我的建议 |
|---|---|---|---|
| capital | 1000000 | 初始权益 | 模拟盘与实盘保持一致 |
| commission_rate | 0.0003 | 单边手续费 | 按你的券商实际值改 |
| slippage_bps | 1.0 | 滑点成本 | 日线给 1-2,分钟线给 2-5 |
| stop_loss_pct | 0.02 | 单笔止损比例 | 结合 ATR 做动态止损更好 |
| risk_pct | 0.01 | 单笔风险预算 | 不要超过 0.02 |
| max_position_ratio | 0.8 | 单标的最大仓位 | 多标的组合建议降到 0.5 以下 |
资金管理模块在 core/risk.py 里,核心的仓位计算逻辑大概是这样的:
from northstar.core.risk import RiskManager risk_manager = RiskManager( capital=1_000_000, commission_rate=0.0003, slippage_bps=1.0, stop_loss_pct=0.02, risk_pct=0.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_pct=0.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_window=fast, slow_window=slow, stop_loss_pct=stop) result = run_backtest('example_momentum', **cfg) results.append((result.sharpe, cfg)) results.sort(reverse=True) 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( symbol='000300', qty=100, trigger_price=entry_price * 0.98, )这个坑很隐蔽,因为我第一次拆这套软件时也习惯性地直接改仓位,直到模拟盘持仓和日志对不上才意识到 broker 层被绕过了。从那以后我给自己定了个规矩:策略里只允许产生信号,订单必须交出去。
5.5 多品种回测内存耗尽
现象:加载 300 只股票日线跑回测,跑到一半直接 MemoryError,进程被杀。
原因:把每只股票的完整 DataFrame 都 append 到一个 list,最后用pd.concat合并。数据量一大,内存峰值是原始数据的 3 到 5 倍,尤其在 factor 计算时会生成多份中间副本。
解决:按 symbol 分片存储 parquet,datafeed 读取时只加载当前需要的标的。
# core/datafeed.py 改造片段 def load_symbol(code): path = f'data/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_path='data/clean', mode='paper') broker = PaperBroker(initial_cash=1_000_000) risk_manager = RiskManager(capital=1_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(f"bar={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预热,最后才放真模拟盘。这么做虽然多花半小时,但省下的都是实盘上的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取