SIMULATION · PERFORMANCE
Optopsy 的 simulate():从逐笔交易到权益曲线
默认的策略函数只给你按区间分组的统计。要拿到「一笔一笔按时间推进、有资金约束」的结果,要用 simulate()——它输出交易明细、权益曲线与一整套绩效指标。
- 三个输出:
trade_log(逐笔)、equity_curve(按出场日索引)、summary(绩效汇总)。 - selector 决定「同一入场日选哪一笔」:四选一或传自定义函数。
max_positions控制并发:为 1 时是不重叠的贪心策略。- 12 个风险指标:有明确口径,也有明确的空数据返回值。
raw=True 取到单笔交易,不直接用分组统计max_positions 过滤重叠持仓trade_log / equity_curve / summaryoptopsy/simulator.py 的实现绘制的示意图;非官方流程图,不代表任何收益结果。SIGNATURE
Optopsy simulate() 参数表:每个参数改变什么
先看 Optopsy 的签名,再逐项理解。所有策略参数(DTE、Delta、滑点、手续费等)都通过 **strategy_kwargs 原样透传给策略函数。
| 参数 | 类型 / 默认 | 作用 | 改变它会怎样 | 适用场景 | 注意点 |
|---|---|---|---|---|---|
data | DataFrame | 期权链数据 | 直接影响候选合约 | 必填 | 与策略函数要求同一套列契约 |
strategy | 可调用对象 | 要模拟的策略函数 | 换策略即换整条结果 | 必填 | 传函数本身(如 op.short_puts),不要传调用结果 |
capital | float,默认 100000.0 | 起始资金 | 影响可用仓位与 summary 中的资金相关指标 | 想模拟小资金账户或大账户时 | 资金耗尽(权益跌至 0 及以下)时交易日志会被截断 |
quantity | int,默认 1 | 每笔合约数量 | 放大每笔盈亏的绝对额 | 想模拟多张合约 | 与 multiplier 一起决定名义规模 |
max_positions | int,默认 1 | 并发持仓数上限 | 从「不重叠的单笔」变成「可同时持有 N 笔」 | 想同时铺多个到期日或分批建仓时 | 为 1 时按入场日 ≥ 上一笔出场日做贪心不重叠过滤 |
multiplier | int,默认 100 | 合约乘数 | 改变每笔的现金影响 | 股指期权通常是 100 | 不同市场的乘数不同,按你的标的确认真实值 |
selector | str 或可调用,默认 'nearest' | 同一入场日有多笔候选时选哪一笔 | 直接改变模拟的交易序列 | 想对比不同选仓规则的影响时 | 可选 nearest / highest_premium / lowest_premium / first,也可传自定义函数 |
**strategy_kwargs | 透传 | 策略参数(DTE、Delta、滑点、手续费、早退等) | 与直接调用策略时一致 | 所有场景 | 参数校验同样走 Pydantic,类型必须严格匹配 |
依据 optopsy/simulator.py 的 simulate() 签名与官方 examples 文档整理。
注意 strategy 传的是函数对象而不是调用结果。写成 simulate(data, op.short_puts(...)) 是常见错误——要先传函数,参数走 kwargs。
RUN IT
Optopsy 怎么跑出首张权益曲线:四步方法
每一步都给可复制代码与预期输出。第四步是本站建议的核对动作。
写法取自官方 examples 文档;本站未在 Python 3.12+ 环境实跑,属源码级核验。
import optopsy as op
data = op.csv_data("SPX_2023.csv")
result = op.simulate(
data,
op.short_puts,
capital=100_000,
quantity=1,
max_positions=2,
multiplier=100,
selector="nearest",
max_entry_dte=45,
exit_dte=14,
)
selector="nearest" 表示同一入场日有多笔候选时,选最接近平值的那一笔(实现上按 Delta 最接近 0.50 判断)。其余策略参数与直接调用策略时写法一致。
预期输出:result 是一个 SimulationResult 对象
print(result.summary)
summary 是一个扁平字典,既含基础交易统计(笔数、胜率、盈亏比等),也含从 metrics 模块算出的风险调整指标(夏普、索提诺、VaR、CVaR、Calmar 等)。
预期输出:包含 win rate、profit factor、回撤幅度与风险调整类指标
print(result.trade_log.head())
交易日志每行一笔已完成交易,含入场日、出场日、到期日、入场成本、出场收入、收益率与 exit_type。这是核对「模拟到底做了什么」的主要依据。
预期输出:列里可看到入场/出场日期与现金流字段
eq = result.equity_curve
print(eq.head(), eq.tail())
# 曲线按“出场日”索引;每笔平仓后记录当时的权益值
权益曲线按出场日索引,不是每个自然日都有一点。如果你需要逐日曲线,要先按日重采样。曲线的起始值反映了开头那笔交易平仓后的权益。
预期输出:序列按时间递增,最后一个值即最终权益
selector 的四种取值
nearest:最接近平值(Delta 最接近 0.50);highest_premium:选收到的权利金偏高的那笔;lowest_premium:选付出成本最低的那笔;first:直接取候选列表里的那一条,用于可复现的确定性测试。
仓位约束怎么生效
max_positions=1 时,下一笔的入场日必须不早于上一笔的出场日(贪心不重叠)。大于 1 时会跟踪未平仓区间,允许同时持有若干笔。这决定了同一份数据能产生多少笔交易。
资金耗尽会怎样
当权益跌到 0 或以下,交易日志会在那一笔被截断——这是一种「破产」处理,而不是继续加杠杆。所以曲线末尾突然变短是有含义的,不要当成数据缺失。
OUTPUT CONTRACT
Optopsy 三件套输出:每一列/每个键代表什么
把这三样东西的分工搞清楚,就能判断一个结果该不该信。
| 输出 | 结构 | 关键字段 / 键 | 用途 | 注意点 |
|---|---|---|---|---|
trade_log | DataFrame,一行一笔 | 入场日、出场日、到期日、标的、入场成本、出场收入、收益率、exit_type | 核对模拟细节;自己算任意自定义指标 | 入场成本与出场收入是带符号的现金流,不是价格 |
equity_curve | 按出场日索引的序列 | 每个出场日对应的权益值 | 画资金曲线;算回撤幅度(基于权益而非收益率) | 不是逐日序列;需要逐日要先重采样 |
summary | 扁平字典 | 交易统计 + 风险调整指标 | 快速比较不同参数或不同策略 | 指标口径见下一节;空数据或标准差为 0 时部分指标返回 0 |
SimulationResult | 数据类 | 包含上面三者 | 统一承载模拟结果 | 注意与 PortfolioResult 区分 |
PortfolioResult | 数据类 | trade_log(含 leg 列)、equity_curve、summary、leg_results | 组合模拟的返回对象 | 组合日志额外有 leg 列用来区分来源腿 |
依据 optopsy/simulator.py 中两个结果数据类的 docstring 与官方 examples 文档整理。
组合模拟:simulate_portfolio(legs, capital) 接收一个腿的列表,每条腿指定自己的数据、策略、权重与参数,各自独立跑一遍再合并。合并时每条腿的权益曲线会用自己分配到的起始资金播种,这样「还没开仓的空闲现金」也会体现在组合总额里——否则组合曲线会在早期被低估。
METRIC DEFINITIONS
Optopsy 12 个风险指标说明:官方文档没有集中列过的口径
这些指标可以单独调用,也由 simulate() 自动算进 summary。口径按源码文档整理。
| 函数 | 含义与口径 | 空数据 / 边界情况返回什么 | 适用场景 | 注意点 |
|---|---|---|---|---|
sharpe_ratio | 年化夏普比率,默认年化因子 252、无风险利率 0 | 标准差为 0 或数据为空时返回 0.0 | 跨策略比较风险调整后收益 | 默认无风险利率为 0,与实务口径可能不同 |
sortino_ratio | 年化索提诺比率,只惩罚下行波动 | 同上 | 期权收益分布不对称时比夏普更合适 | 源码注释明确说明它对不对称收益更合适 |
max_drawdown | 基于权益曲线的回撤幅度,返回负数 | 曲线为空或单调上升时返回 0.0 | 评估资金曲线的痛感 | 输入是绝对权益值,不是收益率 |
max_drawdown_from_returns | 基于收益率序列算回撤幅度,返回负数 | 空数据返回 0.0 | 手上只有逐笔收益率时 | initial_capital 参数保留兼容,实际不参与计算 |
value_at_risk | 历史 VaR:给定置信度下的分位点。默认 0.95 即取第 5 百分位 | 空数据返回 0.0 | 衡量极端亏损的量级 | 返回值通常为负数 |
conditional_value_at_risk | CVaR / 期望损失:低于 VaR 那部分收益的均值 | 空数据返回 0.0 | 比 VaR 更关注尾部 | 同样通常为负 |
win_rate | 盈利笔数占比,返回 0–1 | 空数据返回 0.0 | 最直观的胜率 | 胜率高不等于盈利,要结合盈亏比看 |
profit_factor | 总盈利 / 总亏损的绝对值 | 无亏损时返回 inf;无盈利或空数据返回 0.0 | 衡量盈亏结构 | 出现 inf 时不要直接放进图表,先处理 |
calmar_ratio | 年化收益 / 回撤幅度 | 回撤幅度为 0 或数据不足时返回 0.0 | 关注回撤的效率指标 | 既涉及年化又涉及回撤,两处口径都要一致才好比较 |
omega_ratio | Omega 比率:相对阈值收益的概率加权盈亏比,>1 表示收益占优 | 空数据返回 0.0 | 想看整个收益分布而非只均值方差 | 对阈值的选择敏感 |
tail_ratio | 右尾(95 分位)/ 左尾(5 分位)之比,>1 表示右尾更厚 | 空数据返回 0.0 | 期权策略的尾部特征尤其重要 | 分位数在样本很少时不稳定 |
compute_risk_metrics | 一次性算齐上面各项,返回扁平字典 | 按各子函数各自的规则 | 想一次拿全指标 | 可同时传 equity,否则回撤类指标从收益率推导 |
依据 optopsy/metrics.py 各函数的 docstring(含默认参数与边界返回值)整理;指标计算委托给 empyrical-reloaded 库。
比较指标前先对齐三件事:样本区间、年化因子(默认 252)、无风险利率(默认 0)。否则两个数字看起来可比,其实口径不同。profit_factor 返回 inf 是正常情况下就会发生的(某段区间没有亏损笔),做图表时要单独处理。
READING THE RESULT
Optopsy 结果怎么读:五个容易被误读的地方
1. 分组统计不等于模拟结果
默认策略输出是按 DTE 与 Delta 区间分组的统计,区间之间互相独立;simulate() 才引入了时间顺序与资金约束。同一个策略的「平均收益率」和「模拟出的资金曲线」本来就不会一致。
2. equity_curve 是按出场日索引的
不是逐日序列。如果你的分析需要逐日风险指标(如逐日波动率),必须先在日期轴上重采样,否则会把间隔不均的点当成等间隔序列。
3. 回撤来自权益而非收益率
两个函数一个吃权益、一个吃收益率,返回的都是负数。混用会得到量级完全不同的数字。
4. 交易笔数受 max_positions 影响
同一份数据,max_positions 从 1 改成 3,交易笔数可能显著增加。比较不同参数的结果前,先确认笔数差异是不是来自这个参数。
| 你想回答的问题 | 该用哪个输出 | 别用哪个 | 为什么 |
|---|---|---|---|
| 这笔策略在历史上平均赚多少 | raw=True 的逐笔收益率分布 | 只看 summary 里的单一均值 | 分布往往高度不对称,均值会掩盖尾部 |
| 按 45 DTE 入场是否比按 30 DTE 更好 | 两种参数的策略输出分组统计对比 | 不同参数下的 summary 直接比 | 分组统计能按 DTE 分桶看差异,越过了分桶细节 |
| 资金曲线承受了多大回撤 | equity_curve + max_drawdown | 用 max_drawdown_from_returns 代替 | 两者输入语义不同,且都返回负数易混淆 |
| 多策略组合是否更稳 | simulate_portfolio() 的组合曲线与腿级结果 | 把各策略的 summary 手工平均 | 组合有资金分配与并发效应,平均值不代表组合 |
| 某笔交易为什么亏 | trade_log 单笔行的入场/出场与 exit_type | 回看策略的分组统计 | 分组统计里没有单笔信息 |
| 结果是否对未来有参考意义 | 本页以外的判断框架 | 任何回测数字 | 官方免责声明明确:历史结果不代表未来,且未覆盖全部现实因素 |
依据官方 examples 文档的输出说明、simulator 与 metrics 模块的源码实现整理。
FAQ
Optopsy 模拟与绩效常见问题
simulate() 和直接用策略函数有什么区别?
直接调用策略函数得到的是按 DTE 与 Delta 区间分组的统计,各区间彼此独立;simulate() 会按时间顺序逐笔推进,并引入起始资金、合约数量、并发仓位上限与权益曲线。前者回答「这批交易长什么样」,后者回答「如果按时间一笔笔做,资金会怎么走」。
selector 该选哪个?
取决于你想检验什么。nearest(默认,选最接近平值的一笔)是最中性的起点;highest_premium 与 lowest_premium 用来检验「选贵的还是选便宜的」对结果的影响;first 用于需要严格可复现的测试场景。也可以传自定义函数实现自己的选仓规则。
max_positions 设成 1 和设成 3 差别大吗?
通常差别很大。为 1 时要求下一笔的入场日不早于上一笔的出场日,等于强制交易不重叠;设大之后允许同时持有若干笔,交易笔数会增加,曲线形态也会变。比较不同参数的结果前,先确认笔数差异是不是来自这个参数。
为什么 profit_factor 会显示为无穷大?
当统计区间内没有任何亏损交易时,总亏损为 0,源码明确在这种情况下返回 inf。这是正常的边界行为,不是错误。做图表或排序时要单独处理这个值。
权益曲线为什么不是每天都有一个点?
equity_curve 按出场日索引,只有交易平仓时才会记录一个权益值。需要逐日序列时,要自己在日期轴上重采样。以源码实现为准。
模拟结果是负数就说明策略不行吗?
不一定,但要非常谨慎地解读。结果受数据区间、成交假设(滑点、手续费)、max_positions 与 selector 的共同影响。正确做法是先固定除一个变量外的所有条件,再看单个变量带来的差异。回测结果不构成任何投资建议,官方免责声明也明确历史结果不代表未来。
simulate_portfolio() 的权重是怎么用的?
每条腿按自己的 weight 分到一部分起始资金,然后各自独立跑一次模拟;组合权益曲线由各腿曲线按日相加得到,并且每条腿的曲线会用自己的起始资金播种,以便把「还没开仓的空闲现金」也计入组合总额。组合交易日志额外带一个 leg 列来区分来源。