Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/research/crypto_combo_accounting_correction_20261003.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Crypto combo 历史收益口径修正(2026-10-03)

`crypto_combo_backtest_20260628.json` 是旧版资金流口径的只读历史结果。它把每日 DCA 入金混入账户权益百分比变化,因此其中的年化收益、Sharpe、回撤和总收益均不代表投资绩效;本说明不覆盖或重写该 artifact。

当前 Crypto combo runner 以现金流调整后的单位净值收益计算 TWR、CAGR、Sharpe 和回撤,并另外报告 XIRR、期初/期末权益、累计注资、累计提现及净利润。该研究代理明确假设每笔 DCA 在当日收盘时到达,并按同一收盘价买入;窗口化指标使用窗口前一日的实际权益作为期初资本。现金流计时必须在同一评估序列中保持一致,TWR 依现金流时点切分并链乘。

XIRR 将同一自然日的现金流先合并并移除净额为零的日期。若净现金流存在多次正负号变化,XIRR 标为歧义并留空,不从数值搜索找到的单个候选根推断唯一解;该搜索不保证找到全部根。这样可避免非传统现金流因网格漏检而输出看似唯一的回报率。

combo 中的山寨币仍是由 ETH 收益缩放并加噪声生成的 synthetic proxy。它不是实际策略重放,也不提供真实成交、结算或成本证据。当前 proxy 未建模费用与滑点,结果显式标记 `cost_status=not_modelled`;为保持既有消费者数值兼容而输出的零成本数值不代表已测量成本。旧 artifact 没有在本次修正中重跑,也不得用于晋级或证明策略效果。

另有独立 `backtest.combo_replay` 研究入口,直接调用真实 `crypto_equity_combo.build_target_weights`,消费调用方提供并带 as-of/available-at 的 indicators、universe、benchmark 快照及 OHLC;它不生成 ETH 收益代理。信号只在 t 日收盘形成,t+1 自然日开盘按 `_rebalance_holdings` 调仓,随后以 t+1 收盘价估值;策略组合权益来自信号日收盘时已知的现金和持仓。显式 `fee_bps` / `slippage_bps` 是合成执行假设,按成交名义金额从现金扣除,并非测得的实际成本;本入口不支持外部现金流。当前工程案例仅用 synthetic 输入验证时序与账本,不构成策略表现、晋级或 live 证据。

现金流调整与绩效报告口径参照 [GIPS Standards Handbook for Firms](https://www.gipsstandards.org/standards/gips-standards-for-firms/gips-standards-handbook-for-firms/);此处仅声明本模拟器的现金流时间约定,不代表 GIPS 合规认证。
99 changes: 82 additions & 17 deletions scripts/research_crypto_combo_backtest.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,9 @@ def run_backtest(
"orchestrator": {
"equity": pd.Series(dtype=float),
"metrics": payload["metrics"],
"accounting": payload["accounting"],
"cost_status": payload["cost_status"],
"simulation_model": payload["simulation_model"],
"profile": payload["profile"],
"source": payload["source"],
}
Expand Down Expand Up @@ -293,9 +296,24 @@ def run_backtest(
metrics_c = _compute_metrics(equity_c, "Dynamic Combo")

return {
"Pure BTC DCA": {"equity": equity_a, "metrics": metrics_a},
"Static Combo": {"equity": equity_b, "metrics": metrics_b},
"Dynamic Combo": {"equity": equity_c, "metrics": metrics_c},
"Pure BTC DCA": {
"equity": equity_a,
"metrics": metrics_a,
"simulation_model": "historical_btc_dca_no_cost_model",
"cost_status": "not_modelled",
},
"Static Combo": {
"equity": equity_b,
"metrics": metrics_b,
"simulation_model": "synthetic_alt_proxy_not_strategy_replay",
"cost_status": "not_modelled",
},
"Dynamic Combo": {
"equity": equity_c,
"metrics": metrics_c,
"simulation_model": "synthetic_alt_proxy_not_strategy_replay",
"cost_status": "not_modelled",
},
}


Expand All @@ -310,7 +328,10 @@ def _simulate_btc_dca(
price = btc_close.reindex(idx)
btc_units = pd.Series(DCA_AMOUNT_USD / price, index=idx)
cum_units = btc_units.cumsum()
return cum_units * price
equity = cum_units * price
equity.attrs["external_flows"] = pd.Series(DCA_AMOUNT_USD, index=idx)
equity.attrs["flow_timing"] = "end_of_day"
return equity


def _simulate_combo(
Expand Down Expand Up @@ -385,15 +406,26 @@ def _simulate_combo(
)
portfolio_values.append(btc_value + alt_value + cash_held)

return pd.Series(portfolio_values, index=idx)
equity = pd.Series(portfolio_values, index=idx)
equity.attrs["external_flows"] = pd.Series(DCA_AMOUNT_USD, index=idx)
equity.attrs["flow_timing"] = "end_of_day"
equity.attrs["simulation_model"] = "synthetic_alt_proxy_not_strategy_replay"
return equity


def _compute_metrics(
equity: pd.Series,
label: str,
) -> dict[str, Any]:
"""Compute per-period metrics for a single equity curve."""
"""Compute cashflow-adjusted per-period metrics for a single equity curve."""
from crypto_strategies.backtest.live_pool_simulator import (
_cashflow_accounting_metrics,
_cashflow_adjusted_returns,
_performance_metrics,
)

periods_metrics: dict[str, Any] = {}
all_flows = equity.attrs.get("external_flows", pd.Series(0.0, index=equity.index))
for period_name, (start_str, end_str) in PERIODS.items():
start_ts = pd.Timestamp(start_str)
end_ts = pd.Timestamp(end_str)
Expand All @@ -404,21 +436,47 @@ def _compute_metrics(
"max_drawdown": 0.0,
"sharpe": 0.0,
"total_return": 0.0,
"cumulative_contributions": 0.0,
"cumulative_withdrawals": 0.0,
"net_profit": 0.0,
"xirr": None,
"xirr_status": "unavailable_empty_period",
"cost_status": "not_modelled",
}
continue

start_val = sub.iloc[0]
end_val = sub.iloc[-1]
total_ret = end_val / start_val - 1.0 if start_val > 0 else 0.0
ann_ret = _safe_annual_return(sub)
mdd = _safe_max_drawdown(sub)
sharpe = _safe_sharpe(sub)
flows = all_flows.reindex(sub.index).fillna(0.0).astype(float)
prior = equity.loc[equity.index < sub.index[0]]
initial_equity = float(prior.iloc[-1]) if not prior.empty else 0.0
returns = _cashflow_adjusted_returns(
sub,
flows,
initial_equity=initial_equity,
flow_timing="end",
)
perf = _performance_metrics(returns)
accounting = _cashflow_accounting_metrics(
sub,
flows,
initial_equity=initial_equity,
initial_equity_date=prior.index[-1] if not prior.empty else None,
)

periods_metrics[period_name] = {
"annual_return": round(float(ann_ret), 4),
"max_drawdown": round(float(mdd), 4),
"sharpe": round(float(sharpe), 4),
"total_return": round(float(total_ret), 4),
"annual_return": round(float(perf["CAGR"]), 4),
"max_drawdown": round(float(perf["Max Drawdown"]), 4),
"sharpe": round(float(perf["Sharpe"]), 4),
"total_return": round(float(perf["total_return"]), 4),
"twr_total_return": round(float((1.0 + returns).prod() - 1.0), 4),
"xirr": accounting["xirr"],
"xirr_status": accounting["xirr_status"],
"initial_equity": round(float(accounting["initial_equity"]), 4),
"ending_equity": round(float(accounting["ending_equity"]), 4),
"cumulative_contributions": round(float(accounting["cumulative_contributions"]), 4),
"cumulative_withdrawals": round(float(accounting["cumulative_withdrawals"]), 4),
"net_profit": round(float(accounting["net_profit"]), 4),
"flow_timing": accounting["flow_timing"],
"cost_status": "not_modelled",
}

return periods_metrics
Expand Down Expand Up @@ -532,6 +590,9 @@ def main() -> None:
"profile": payload["profile"],
"metrics": payload["metrics"],
"source": payload["source"],
"accounting": payload["accounting"],
"cost_status": payload["cost_status"],
"simulation_model": payload["simulation_model"],
"orchestrator": True,
},
indent=2,
Expand All @@ -543,7 +604,11 @@ def main() -> None:
# Strip equity curves for JSON output (too large)
json_results: dict[str, Any] = {}
for strat_name, data in results.items():
json_results[strat_name] = {"metrics": data["metrics"]}
json_results[strat_name] = {
"metrics": data["metrics"],
"simulation_model": data["simulation_model"],
"cost_status": data["cost_status"],
}
json.dump(json_results, sys.stdout, indent=2)
print()
else:
Expand Down
Loading
Loading