☰
拆解Northstar盈富量化交易软件:回测、因子与模拟盘实战指南
2026/10/13 11:31:10 网站建设 项目流程

简介: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.py

config 目录下只有一个 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/Shanghai

mode选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同一标的使用固定 ID
open / high / low / closefloat64high >= max(open, close),low <= min(open, close)
volumeint64必须大于 0
amountfloat64可选,资金流因子会用到

数据清洗我是这样处理的:

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) - 1

register_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-5
stop_loss_pct0.02单笔止损比例结合 ATR 做动态止损更好
risk_pct0.01单笔风险预算不要超过 0.02
max_position_ratio0.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预热,最后才放真模拟盘。这么做虽然多花半小时,但省下的都是实盘上的后悔药。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询