feat(strategy): project ledger and complete optimization reports
CI / lite (pull_request) Canceled after 0s

This commit is contained in:
ao gong
2026-10-04 11:42:23 +08:00
parent 5192370a9e
commit 7ab56e105c
7 changed files with 719 additions and 6 deletions
+1 -1
View File
@@ -32,7 +32,7 @@
- `retrospective_*_contracts` — 未发布的显式 v2 回顾性合同:区分历史业务日期与实际可得/计算时间,保留 v1 和现有金融公式,不授予历史可得性、发布或执行权限;见 [v2 接口说明](docs/RETROSPECTIVE_COMPUTATION_V2.md)
- `attribution` — 基于实际成交后持仓的隔夜 / 日内 / 交易成本逐日收益归因与闭合审计
- `metrics` — 绝对绩效 + 严格日期对齐的 TE / IR / alpha / beta 基准相对绩效
- `strategy_research` / `strategy_optimizer` / `trade_pairing` — 七策略候选、下一日开盘执行、统一账本、FIFO双边成本和有界真实优化;见 [策略合同](docs/strategy-research.md)
- `strategy_research` / `strategy_optimizer` / `trade_pairing` / `strategy_artifact` — 七策略候选、下一日开盘执行、统一账本、FIFO双边成本、有界真实优化和完整策略报告工件;见 [策略合同](docs/strategy-research.md)
- `factor_diagnostics` — 候选0.1.0:完整键配对、逐日IC/RankIC、样本与未定义值、显式日历前瞻标签;见 [诊断合同](docs/factor-diagnostics.md)
- `factor_library` — 通用方法(turnover / winsorize / IC / OLS / jb_test)
- `portfolio_decomp` — 组合分解(risk_parity / mean_variance / 因子归因)
+13 -1
View File
@@ -1,6 +1,6 @@
# Seven-strategy candidate research
The core owns the seven example strategies, sequential decisions, shared execution ledger, FIFO trade pairing and bounded exhaustive optimization. The platform supplies one selected asset's OHLC and displays results. These pure functions perform no source reads, persistence, publication or order routing. `decision_eligible` remains false. Caller dates do not establish an exchange calendar or historical data availability.
The core owns the seven example strategies, sequential decisions, shared execution ledger, FIFO trade pairing and bounded exhaustive optimization. The platform supplies one selected asset's OHLC and displays results. The reserved asset identity `CASH` cannot be used as a security, preventing collision with projected cash positions. These pure functions perform no source reads, persistence, publication or order routing. `decision_eligible` remains false. Caller dates do not establish an exchange calendar or historical data availability.
## Reuse and scope
@@ -55,6 +55,16 @@ Other absolute measures reuse `metrics.summary`: 252 supplied trading sessions p
`optimize_strategy_research` validates the entire grid before any trial. It allows at most four axes, ten candidates per axis and 100 combinations. Empty axes, unknown keys, duplicate values, non-native numbers, invalid relationships and any insufficient history fail the whole request. The size bound precedes Cartesian expansion. Every trial runs a fresh real seven-strategy pipeline and ledger with explicit fees. Targets are `total_return`, `sharpe_ratio` or `calmar_ratio`. An unavailable objective fails the ranking instead of silently skipping a candidate. Stable descending sorting preserves canonical axis order and supplied candidate order for ties; zero and negative finite scores remain valid.
## Strategy artifact projection
`strategy_artifact.build_strategy_research_artifact` accepts an actual `StrategyResearchResult` or `StrategyOptimizationResult` and returns the existing schema1.1.0 `ResearchRunArtifact`. The calculation result retains detached OHLC and benchmark-return views and explicit cost inputs; optimization retains a detached grid snapshot. The builder projects the selected result's ledger, actual fees, next-session signal links, close-marked holdings and existing metrics. It does not run a second strategy, accounting system or performance formula.
`params_json.strategy_report` uses `researchhub.strategy-research.v1`. It covers every causal signal, including `no_next_session`, unfilled targets and partial fills; complete FIFO matches, closed/open lots and net PnL; daily versus completed-trade win rates; benchmark status; exact parameters/costs; and all ranked candidate summaries. A grid artifact stores the selected first-ranked ledger plus every candidate's parameters, objective score, metrics and missing reasons, up to100 trials. It does not repeat100 full ledgers. Report contents are covered by the artifact's canonical content digest. User metadata cannot overwrite this report or its performance explanation.
The legacy factor-signal table remains empty: strategy signals have no factor score. The report explicitly locates the real strategy signals and marks factor attribution and covariance risk as not computed. Actual fills link to report signal IDs. A zero-value portfolio retains its actual zero values and an undefined weight, with the exact dates and reason recorded; it is not assigned an invented zero or full-cash weight. Non-numerical performance values carry explicit reasons keyed to their fact-table columns, and win-rate basis is completed trades. Requested empty benchmark data retains its identity and empty status, while unrequested data has no identity.
This is a storage-neutral candidate. The platform's isolated durable-file adapter must validate this report and expose undefined weights before enabling this path. This projection does not validate a production database schema, execute SQL, admit real data, or grant decision eligibility. Existing factor-artifact consumers remain unchanged.
## Verification scope
The missing public modules and sequential policy first failed actual tests. Further actual RED→GREEN regressions cover unfilled exits, legitimate zero NAV, independent ATR/exit windows, benchmark underflow, nonfinite portfolio returns, tiny residual holdings, Bollinger scale invariance, undefined Sortino, premature FIFO closure, full-exit quantity rounding and phantom residues after three accumulated entries at prices 3, 11 and 13.
@@ -64,3 +74,5 @@ Final focused verification is 93 new tests plus 85 existing execution tests, 178
Two reused read-only reviewers independently exercised causal prefixes and cash/FIFO examples and found substantive defects subsequently turned into permanent regressions. The final independent FIFO review passed after reproducing three-lot full exits, retaining real tiny balances and reconciling fee-bearing add/partial-exit/full-exit PnL with final NAV. Saved revision and central delivery status are separate evidence. These tests do not establish real data coverage, source/PIT qualification, platform task publication or formal production availability.
The reviewed implementation was saved and pushed as `7d3e840483d7f3b5d7b2d987c95fb79a5dc4a63b` to the existing PR #21 (https://gitea.puyuanfh.cn/ageorge156/quant_engine/pulls/21), advancing the original 3c97102f candidate. This receipt-only checkpoint does not change the tested code. One explicit central candidate refresh and `ship --ready --confirm-l3` is the next boundary; no prior CI wait was polled or resumed. Push is a save checkpoint, and does not establish merge or source admission.
Artifact increment verification: 26 new artifact tests plus81 strategy/optimizer and7 existing factor-artifact tests passed together (114). The final complete core suite passed1212 tests in15.59s with1170 pandas deprecation warnings. Four-file Ruff and three-module strict typing passed. The read-only reviewer confirmed the reserved-CASH rejection and fact-column explanation keys after actual regressions. The earlier5192370 central receipt remains CI pending without polling; this new increment is saved and gated separately on the same delivery.
+10
View File
@@ -1,5 +1,15 @@
# Quant OS factor diagnostics and strategy core increments
## 2026-10-04 strategy artifact increment
The existing delivery now adds a storage-neutral strategy artifact projection required by platform Draft #102. The earlier central boundary for candidate5192370a9ecfeca09ed39ee9ca3583060d8777d9 returned `waiting_on_dependency`, CI pending, receipt e28c09224685c20569a8a6f627a2c34b6a1c69f3dc43eadabe918c7458476e45. That candidate and the older3c97102f receipt remain preserved and have not been polled or resumed. This is a new independently completed code increment on the same branch, PR #21, lifecycle and sole writer.
The result keeps detached input/grid snapshots. The new builder reuses schema1.1.0 and the original ledger, projecting real fills, NAV, positions and metrics while covering all strategy signals, FIFO pairing and up to100 ranked trials in `strategy_report`. It does not fabricate factor scores, attribution or covariance risk. Zero-NAV weight is undefined with a reason. A security cannot claim reserved CASH identity. Production schema/storage and source admission remain outside this pure candidate; platform isolated validation and display still need integration.
Actual missing-module RED preceded implementation. Independent review found a reproducible cash-identity collision (run/optimizer RED) and fact-column explanation naming inconsistency (RED); both are fixed with permanent regressions and final read-only PASS. Final focused run114 passed (26 new artifact tests,81 strategy/optimizer tests,7 existing factor-artifact tests). Final whole-core run1212 passed in15.59s with1170 pandas deprecation warnings. Targeted four-file Ruff and three-source strict mypy passed. Prior financial calculations and their accepted test evidence are not recomputed by the projection. The fixed49 framework/source and delivery route remain unchanged, resume/review critical gpt-6-astra/xhigh was reused/validated, runtime observation remains unknown.
Next: save this completed increment and enter one central candidate refresh/Ready gate, then bind its saved revision in the platform isolated task/artifact path. The broad platform delivery remains WIP and formal run/optimize remains gated.
## 2026-10-04 seven-strategy core increment
The same owner, lifecycle, branch and PR #21 now include the independent seven-strategy calculation increment needed by platform Draft #102. The original 3c97102f CI-wait receipt has not been queried, resumed or treated as resolved. New code is being reviewed as a new candidate; previous fixed-archive platform consumers remain bound to their saved revisions.
+354
View File
@@ -0,0 +1,354 @@
"""Storage-neutral projection of seven-strategy research and bounded grid rankings.
All accounting and metrics are owned by the completed core result. Strategy
signals have no factor scores, so their full causal record lives in the covered
report, not the legacy factor-signal table. No source or decision admission is
granted by this projection.
"""
from __future__ import annotations
import hashlib
from collections.abc import Mapping
from dataclasses import asdict
import pandas as pd
from .artifact import (
RESEARCH_ARTIFACT_SCHEMA_VERSION,
RISK_COLUMNS,
ResearchRunArtifact,
_aware_timestamp,
_canonical_mapping_json,
_required_text,
)
from .strategy_optimizer import StrategyOptimizationResult
from .strategy_research import StrategyResearchResult
STRATEGY_REPORT_SCHEMA = "researchhub.strategy-research.v1"
_PERFORMANCE = {
"total_ret": "total_return",
"ann_ret": "ann_return",
"ann_volatility": "ann_volatility",
"sharpe": "sharpe",
"sortino": "sortino",
"max_dd": "max_drawdown",
"calmar": "calmar",
"win_rate": "trade_win_rate",
}
_RELATIVE = {
"tracking_error": "tracking_error",
"ir": "information_ratio",
"alpha": "alpha",
"beta": "beta",
}
_SIGNAL_COLUMNS = [
"run_id",
"signal_date",
"execution_date",
"asset_id",
"factor_score",
"target_weight",
]
_POSITION_COLUMNS = [
"run_id",
"trade_date",
"asset_id",
"asset_type",
"quantity",
"mark_price",
"market_value",
"weight",
]
def _report(
result: StrategyResearchResult,
run_id: str,
optimization: StrategyOptimizationResult | None,
) -> dict[str, object]:
ranking: dict[str, object] | None = None
if optimization is not None:
ranking = {
"objective": optimization.objective,
"grid": optimization.param_grid,
"trial_count": len(optimization.trials),
"selected_rank": 1,
"trials": [
{
"rank": rank,
"parameters": dict(trial.parameters),
"score": trial.score,
"metrics": dict(trial.result.metrics),
"metric_unavailable": dict(trial.result.metric_unavailable),
}
for rank, trial in enumerate(optimization.trials, start=1)
],
}
return {
"schema_version": STRATEGY_REPORT_SCHEMA,
"strategy": result.strategy,
"asset": result.asset,
"parameters": dict(result.parameters),
"costs": dict(result.cost_parameters),
"execution": {
"signal_observation": "after_close",
"fill_price": "next_session_open",
"valuation_price": "session_close",
"lag_sessions": 1,
"quantity_basis": "fractional_research",
},
"signals": [
asdict(signal)
| {
"asset_id": result.asset,
"signal_id": f"{run_id}:signal:{signal.decision_date}",
}
for signal in result.signals
],
"trade_pairing": asdict(result.pairing),
"metrics": dict(result.metrics),
"metric_unavailable": dict(result.metric_unavailable),
"benchmark": {"status": result.benchmark_status, "metrics": result.benchmark_metrics},
"optimization": ranking,
"projections": {
"signals": "strategy_report.signals",
"attribution": "not_computed",
"risk": "not_computed",
"position_weight_basis": "market_value_over_nav",
"undefined_weight_reason": "zero_portfolio_value",
"undefined_weight_dates": [
position.date
for position in result.ledger.positions
if position.portfolio_value == 0
],
},
"decision_eligible": False,
}
def _performance(
result: StrategyResearchResult,
run_id: str,
) -> tuple[pd.DataFrame, dict[str, object]]:
values: dict[str, object] = {"run_id": run_id}
reasons: dict[str, str] = {}
for target, source in _PERFORMANCE.items():
value = result.metrics[source]
values[target] = value
if value is None:
reasons[target] = result.metric_unavailable[source]
for target, source in _RELATIVE.items():
value = None if result.benchmark_metrics is None else result.benchmark_metrics[source]
values[target] = value
if value is None:
reasons[target] = (
"benchmark_" + result.benchmark_status
if result.benchmark_metrics is None
else "benchmark_metric_undefined"
)
values["n_trades"] = len(result.ledger.trades_frame)
values["n_days"] = len(result.ledger.positions)
return pd.DataFrame([values]), {
"unavailable_reasons": reasons,
"win_rate_basis": "completed_trades",
}
def _nav(result: StrategyResearchResult, run_id: str) -> pd.DataFrame:
frame = result.ledger.ledger_frame
frame.insert(0, "run_id", run_id)
frame["trade_date"] = pd.to_datetime(frame["trade_date"]).dt.date
frame["total_cost"] = [
sum(fill.total_cost for fill in day.executions) for day in result.ledger.daily_executions
]
benchmark_returns = result.benchmark_returns
if result.benchmark_nav is None or benchmark_returns is None:
frame["benchmark_nav"] = None
frame["benchmark_return"] = None
frame["excess_ret"] = None
else:
frame["benchmark_nav"] = result.benchmark_nav.to_numpy(copy=True)
frame["benchmark_return"] = benchmark_returns.to_numpy(copy=True)
frame["excess_ret"] = result.ledger.daily_returns.to_numpy() - benchmark_returns.to_numpy()
return frame
def _trades(result: StrategyResearchResult, run_id: str) -> pd.DataFrame:
frame = result.ledger.trades_frame
frame.insert(0, "run_id", run_id)
frame.insert(
1, "trade_id", [f"{run_id}:{sequence:08d}" for sequence in range(1, len(frame) + 1)]
)
signal_by_execution = {
signal.execution_date: signal.decision_date
for signal in result.signals
if signal.execution_date is not None
}
frame["signal_id"] = [
f"{run_id}:signal:{signal_by_execution[day]}" for day in frame["trade_date"]
]
frame["trade_date"] = pd.to_datetime(frame["trade_date"]).dt.date
frame["total_cost"] = frame["fee"] + frame["slippage"]
return frame
def _positions(result: StrategyResearchResult, run_id: str) -> pd.DataFrame:
rows = []
observed = result.bars
for day, position in zip(observed.index, result.ledger.positions, strict=True):
for asset, quantity in position.holdings.items():
if asset != result.asset:
raise ValueError("Strategy ledger contains an unexpected asset")
price = float(observed.at[day, "close"])
market_value = quantity * price
rows.append(
{
"run_id": run_id,
"trade_date": day.date(),
"asset_id": asset,
"asset_type": "security",
"quantity": quantity,
"mark_price": price,
"market_value": market_value,
"weight": market_value / position.portfolio_value
if position.portfolio_value
else None,
}
)
rows.append(
{
"run_id": run_id,
"trade_date": day.date(),
"asset_id": "CASH",
"asset_type": "cash",
"quantity": position.cash,
"mark_price": 1.0,
"market_value": position.cash,
"weight": position.cash / position.portfolio_value
if position.portfolio_value
else None,
}
)
return pd.DataFrame(rows, columns=_POSITION_COLUMNS)
def build_strategy_research_artifact(
result: StrategyResearchResult | StrategyOptimizationResult,
*,
run_id: str,
strategy_id: str,
strategy_name: str,
strategy_version: str,
engine_version: str,
code_revision: str,
data_snapshot_id: str,
calendar: str,
timezone: str,
started_at: str | pd.Timestamp,
finished_at: str | pd.Timestamp,
parameters: Mapping[str, object],
benchmark_id: str | None = None,
) -> ResearchRunArtifact:
"""Snapshot the chosen real ledger plus all bounded trial summaries.
``parameters`` carries task/source identity supplied by the caller. The core
reserves its report and metric explanation; callers cannot substitute them.
Empty requested benchmark data remains distinguishable from no request.
"""
optimization = result if type(result) is StrategyOptimizationResult else None
if optimization is not None:
if not 1 <= len(optimization.trials) <= 100:
raise ValueError("A bounded nonempty optimization result is required")
selected = optimization.trials[0].result
elif type(result) is StrategyResearchResult:
selected = result
else:
raise TypeError("result must be strategy research or optimization")
if selected.decision_eligible or (optimization is not None and optimization.decision_eligible):
raise ValueError("Strategy research cannot grant decision eligibility")
if selected.asset == "CASH":
raise ValueError("Strategy asset cannot use the reserved CASH identity")
normalized_run_id = _required_text(run_id, "run_id", max_length=128)
metadata = {
name: _required_text(value, name)
for name, value in {
"strategy_id": strategy_id,
"strategy_name": strategy_name,
"strategy_version": strategy_version,
"engine_version": engine_version,
"code_revision": code_revision,
"data_snapshot_id": data_snapshot_id,
"calendar": calendar,
"timezone": timezone,
}.items()
}
if not isinstance(parameters, Mapping):
raise TypeError("parameters must be a mapping")
if set(parameters) & {"strategy_report", "performance_interpretation"}:
raise ValueError("Strategy report and performance interpretation are reserved")
started, finished = (
_aware_timestamp(started_at, "started_at"),
_aware_timestamp(finished_at, "finished_at"),
)
if finished < started:
raise ValueError("finished_at must not precede started_at")
if (selected.benchmark_status == "not_requested") != (benchmark_id is None):
raise ValueError(
"Requested benchmark requires its identity; unrequested benchmark cannot have one"
)
benchmark = "" if benchmark_id is None else _required_text(benchmark_id, "benchmark_id")
performance, interpretation = _performance(selected, normalized_run_id)
params_json = _canonical_mapping_json(
dict(parameters)
| {
"strategy_report": _report(selected, normalized_run_id, optimization),
"performance_interpretation": interpretation,
}
)
dates = selected.bars.index
run = pd.DataFrame(
[
metadata
| {
"schema_version": RESEARCH_ARTIFACT_SCHEMA_VERSION,
"run_id": normalized_run_id,
"config_hash": hashlib.sha256(params_json.encode("utf-8")).hexdigest(),
"benchmark_id": benchmark,
"benchmark_alignment_policy": "exact_session_index"
if selected.benchmark_status == "present"
else "none",
"frequency": "1d",
"initial_capital": selected.ledger.initial_cash,
"start_date": dates[0].date(),
"end_date": dates[-1].date(),
"status": "success",
"started_at": started,
"finished_at": finished,
"params_json": params_json,
}
]
)
return ResearchRunArtifact(
schema_version=RESEARCH_ARTIFACT_SCHEMA_VERSION,
_run=run,
_signals=pd.DataFrame(columns=_SIGNAL_COLUMNS),
_nav=_nav(selected, normalized_run_id),
_trades=_trades(selected, normalized_run_id),
_positions=_positions(selected, normalized_run_id),
_attribution=pd.DataFrame(
columns=["run_id", "trade_date", "asset_id", "overnight", "intraday", "asset_total"]
),
_attribution_daily=pd.DataFrame(
columns=[
"run_id",
"trade_date",
"transaction_cost",
"explained_return",
"residual",
"total_return",
]
),
_risk=pd.DataFrame(columns=RISK_COLUMNS),
_performance=performance,
)
+18 -2
View File
@@ -37,8 +37,13 @@ class StrategyOptimizationResult:
strategy: str
objective: str
trials: tuple[StrategyTrial, ...]
_param_grid: dict[str, list[int | float]]
decision_eligible: bool = False
@property
def param_grid(self) -> dict[str, list[int | float]]:
return {key: list(values) for key, values in self._param_grid.items()}
def optimize_strategy_research(
strategy: str,
@@ -64,7 +69,13 @@ def optimize_strategy_research(
finite_number(min_trade_amount, "min_trade_amount")
if min_trade_amount < 0:
raise ValueError("min_trade_amount must be nonnegative")
if type(asset) is not str or not asset or asset.strip() != asset or len(asset) > 100:
if (
type(asset) is not str
or not asset
or asset == "CASH"
or asset.strip() != asset
or len(asset) > 100
):
raise ValueError("An explicit bounded asset key is required")
combinations = parameter_grid(strategy, param_grid)
observed = validate_strategy_bars(bars)
@@ -90,4 +101,9 @@ def optimize_strategy_research(
raise ValueError(f"Optimization objective {objective} is unavailable for {parameters}")
trials.append(StrategyTrial(dict(parameters), value, result))
trials.sort(key=lambda trial: trial.score, reverse=True)
return StrategyOptimizationResult(strategy, objective, tuple(trials))
return StrategyOptimizationResult(
strategy,
objective,
tuple(trials),
{key: list(values) for key, values in param_grid.items()},
)
+30 -2
View File
@@ -61,8 +61,20 @@ class StrategyResearchResult:
benchmark_status: str
benchmark_nav: pd.Series | None
benchmark_metrics: dict[str, float | None] | None
asset: str
_bars: pd.DataFrame
_benchmark_returns: pd.Series | None
cost_parameters: dict[str, float]
decision_eligible: bool = False
@property
def bars(self) -> pd.DataFrame:
return self._bars.copy(deep=True)
@property
def benchmark_returns(self) -> pd.Series | None:
return None if self._benchmark_returns is None else self._benchmark_returns.copy(deep=True)
def _calendar(index: pd.Index) -> pd.DatetimeIndex:
if (
@@ -367,7 +379,13 @@ def run_strategy_research(
finite_number(min_trade_amount, "min_trade_amount")
if min_trade_amount < 0:
raise ValueError("min_trade_amount must be nonnegative")
if type(asset) is not str or not asset or asset.strip() != asset or len(asset) > 100:
if (
type(asset) is not str
or not asset
or asset == "CASH"
or asset.strip() != asset
or len(asset) > 100
):
raise ValueError("An explicit bounded asset key is required")
observed = validate_strategy_bars(bars)
if len(observed) < required_history(strategy, parameters):
@@ -418,7 +436,7 @@ def run_strategy_research(
)
pairing = pair_ledger_trades(ledger)
metrics, unavailable = _metrics(ledger, pairing)
benchmark_nav, benchmark_metrics = None, None
benchmark_nav, benchmark_metrics, benchmark_returns = None, None, None
if benchmark_close is not None:
benchmark_nav = benchmark_close / benchmark_close.iloc[0]
benchmark_returns = benchmark_close.pct_change(fill_method=None)
@@ -441,4 +459,14 @@ def run_strategy_research(
benchmark_status,
benchmark_nav,
benchmark_metrics,
asset,
observed.copy(deep=True),
benchmark_returns,
{
"initial_cash": initial_cash,
"commission": commission,
"stamp_duty": stamp_duty,
"min_trade_amount": min_trade_amount,
"slippage_bps": 0,
},
)
+293
View File
@@ -0,0 +1,293 @@
"""Strategy reports preserve real ledger facts without factor-score fabrication."""
import json
from dataclasses import asdict
import numpy as np
import pandas as pd
import pytest
from quant_engine.strategy_artifact import build_strategy_research_artifact
from quant_engine.strategy_optimizer import optimize_strategy_research
from quant_engine.strategy_research import BenchmarkInput, run_strategy_research
def bars(closes, opens=None):
close = np.asarray(closes, dtype=float)
opening = np.asarray(opens if opens is not None else closes, dtype=float)
return pd.DataFrame(
{
"open": opening,
"high": np.maximum(close, opening),
"low": np.minimum(close, opening),
"close": close,
},
index=pd.date_range("2026-01-01", periods=len(close), freq="B"),
)
def run(strategy="BuyAndHold", feed=None, **kwargs):
return run_strategy_research(
strategy,
bars([10, 11, 12, 13]) if feed is None else feed,
asset="SYNTHETIC",
initial_cash=1000,
**kwargs,
)
def build(result, **kwargs):
metadata = {
"run_id": "strategy-run",
"strategy_id": "isolated-strategy",
"strategy_name": "Synthetic",
"strategy_version": "1",
"engine_version": "candidate",
"code_revision": "candidate",
"data_snapshot_id": "synthetic:ohlc-v1",
"calendar": "synthetic-sessions",
"timezone": "Asia/Shanghai",
"started_at": "2026-01-08T10:00:00+08:00",
"finished_at": "2026-01-08T10:00:01+08:00",
"parameters": {"synthetic": True},
}
return build_strategy_research_artifact(result, **(metadata | kwargs))
def report(artifact):
return json.loads(artifact.run.iloc[0]["params_json"])["strategy_report"]
def test_projects_same_ledger_cash_fees_positions_and_completed_trade_basis():
result = run(
"SmaCross",
bars([10, 8, 12, 6, 14, 5, 12]),
params={"fast": 1, "slow": 2},
commission=0.01,
stamp_duty=0.02,
)
artifact = build(result)
detail = report(artifact)
assert artifact.schema_version == "1.1.0"
assert artifact.nav.portfolio_value.tolist() == result.ledger.nav_series.tolist()
assert artifact.nav.pnl_pct.tolist() == result.ledger.daily_returns.tolist()
assert artifact.trades.fee.sum() == pytest.approx(result.ledger.trades_frame.fee.sum())
assert artifact.nav.total_cost.sum() == pytest.approx(artifact.trades.total_cost.sum())
assert artifact.performance.iloc[0].win_rate == result.pairing.win_rate
assert artifact.performance.iloc[0].n_trades == len(result.ledger.trades_frame)
assert detail["trade_pairing"] == json.loads(json.dumps(asdict(result.pairing)))
assert detail["costs"] == {
"initial_cash": 1000,
"commission": 0.01,
"stamp_duty": 0.02,
"min_trade_amount": 0,
"slippage_bps": 0,
}
assert detail["decision_eligible"] is False
for day, frame in artifact.positions.groupby("trade_date"):
nav = artifact.nav.loc[artifact.nav.trade_date == day].iloc[0]
assert frame.market_value.sum() == pytest.approx(nav.portfolio_value)
assert frame.weight.sum() == pytest.approx(1)
assert artifact.signals.empty
assert artifact.attribution.empty
assert artifact.risk.empty
assert detail["projections"]["signals"] == "strategy_report.signals"
assert detail["projections"]["attribution"] == "not_computed"
params = json.loads(artifact.run.iloc[0].params_json)
assert params["performance_interpretation"]["win_rate_basis"] == "completed_trades"
def test_last_signal_is_not_lost_or_fabricated_as_a_factor_signal():
result = run("SmaCross", bars([10, 8, 12]), params={"fast": 1, "slow": 2})
artifact = build(result)
signal = report(artifact)["signals"][0]
assert signal["status"] == "no_next_session"
assert signal["execution_date"] is None
assert signal["signal_id"] == "strategy-run:signal:2026-01-05"
assert artifact.trades.empty
assert artifact.signals.empty
def test_signal_ids_join_actual_fills_and_report():
artifact = build(run())
signals = {item["signal_id"]: item for item in report(artifact)["signals"]}
for fill in artifact.trades.to_dict("records"):
signal = signals[fill["signal_id"]]
assert signal["execution_date"] == fill["trade_date"].isoformat()
assert signal["decision_date"] < signal["execution_date"]
@pytest.mark.parametrize(
"name",
[
"BuyAndHold",
"SmaCross",
"MACross",
"RSI",
"BollingerBreakout",
"DualThrust",
"TurtleBreakout",
],
)
def test_all_seven_defaults_have_canonical_serializable_reports(name):
values = 10 + np.sin(np.arange(80) / 2) * 2
result = run(name, bars(values, np.r_[values[0], values[:-1]]))
artifact = build(result)
assert report(artifact)["parameters"] == result.parameters
assert json.loads(artifact.canonical_json())["schema_version"] == "1.1.0"
assert artifact.content_sha256 == build(result).content_sha256
assert len(artifact.nav) == 80
@pytest.mark.parametrize("status", ["not_requested", "empty", "present"])
def test_benchmark_states_and_original_returns_are_preserved(status):
feed = bars([10, 11, 12, 13])
closes = (
pd.Series([20, 22, 21, 23], index=feed.index)
if status == "present"
else (pd.Series(dtype=float) if status == "empty" else None)
)
result = run(feed=feed, benchmark=BenchmarkInput(status, closes))
artifact = build(result, benchmark_id="SYNTHETIC-BENCH" if status != "not_requested" else None)
assert report(artifact)["benchmark"]["status"] == status
if status == "present":
assert artifact.nav.benchmark_return.tolist() == pytest.approx(
[0, 0.1, 21 / 22 - 1, 23 / 21 - 1]
)
assert artifact.nav.benchmark_nav.tolist() == pytest.approx([1, 1.1, 1.05, 1.15])
assert artifact.run.iloc[0].benchmark_alignment_policy == "exact_session_index"
else:
assert artifact.nav.benchmark_nav.isna().all()
assert artifact.run.iloc[0].benchmark_alignment_policy == "none"
def test_zero_nav_preserves_zero_value_and_undefined_weight_with_reason():
result = run(
"SmaCross",
bars([10, 8, 12, 6, 14]),
params={"fast": 1, "slow": 2},
commission=0,
stamp_duty=1,
)
artifact = build(result)
last = artifact.positions.iloc[-1]
assert last.market_value == 0
assert pd.isna(last.weight)
assert artifact.nav.iloc[-1].nav == 0
assert report(artifact)["projections"]["undefined_weight_dates"] == ["2026-01-07"]
def test_no_closed_lot_metrics_are_null_with_covered_reason():
artifact = build(run())
metadata = json.loads(artifact.run.iloc[0].params_json)
assert report(artifact)["metrics"]["trade_win_rate"] is None
assert (
metadata["performance_interpretation"]["unavailable_reasons"]["win_rate"]
== "no_closed_lots"
)
assert pd.isna(artifact.performance.iloc[0].win_rate)
def test_result_snapshots_detach_caller_data_and_returned_views():
feed = bars([10, 11, 12, 13])
result = run(feed=feed)
before = build(result).content_sha256
feed.iloc[:] = 999
view = result.bars
view.iloc[:] = 777
artifact = build(result)
assert artifact.content_sha256 == before
positions = artifact.positions
positions["market_value"] = 0
assert artifact.content_sha256 == before
def test_grid_artifact_retains_all_ranks_and_selected_ledger_and_detaches_input():
grid = {"buy_pct": [0.2, 0.5, 1]}
result = optimize_strategy_research(
"BuyAndHold",
bars([10, 11, 12, 13]),
asset="SYNTHETIC",
param_grid=grid,
objective="total_return",
initial_cash=1000,
commission=0,
stamp_duty=0,
)
grid["buy_pct"].append(0.9)
artifact = build(result)
ranking = report(artifact)["optimization"]
assert ranking["grid"] == {"buy_pct": [0.2, 0.5, 1]}
assert ranking["trial_count"] == 3
assert ranking["selected_rank"] == 1
assert [trial["rank"] for trial in ranking["trials"]] == [1, 2, 3]
assert [trial["score"] for trial in ranking["trials"]] == [
trial.score for trial in result.trials
]
assert ranking["trials"][0]["parameters"] == {"buy_pct": 1}
assert (
artifact.nav.portfolio_value.tolist() == result.trials[0].result.ledger.nav_series.tolist()
)
assert all("ledger" not in trial for trial in ranking["trials"])
@pytest.mark.parametrize("key", ["strategy_report", "performance_interpretation"])
def test_callers_cannot_overwrite_authoritative_report_or_metric_explanation(key):
with pytest.raises(ValueError, match="reserved"):
build(run(), parameters={key: {"decision_eligible": True}})
def test_report_mutation_changes_canonical_artifact_digest():
first = build(run(commission=0))
second = build(run(commission=0.01))
assert first.content_sha256 != second.content_sha256
assert first.run.iloc[0].config_hash != second.run.iloc[0].config_hash
def test_invalid_metadata_fails_before_artifact_creation():
with pytest.raises(ValueError, match="finished_at"):
build(run(), finished_at="2026-01-07T10:00:00+08:00")
with pytest.raises(ValueError, match="benchmark"):
build(run(), benchmark_id="FAKE")
def test_missing_relative_metrics_explain_their_fact_column_names():
artifact = build(run())
reasons = json.loads(artifact.run.iloc[0].params_json)["performance_interpretation"][
"unavailable_reasons"
]
assert reasons["ir"] == "benchmark_not_requested"
assert "information_ratio" not in reasons
@pytest.mark.parametrize("producer", ["run", "optimization", "artifact"])
def test_reserved_cash_asset_cannot_collide_with_cash_position(producer):
from dataclasses import replace
operation = {
"run": lambda: run_strategy_research("BuyAndHold", bars([10, 11]), asset="CASH"),
"optimization": lambda: optimize_strategy_research(
"BuyAndHold",
bars([10, 11]),
asset="CASH",
param_grid={"buy_pct": [0.5]},
objective="total_return",
),
"artifact": lambda: build(replace(run(params={"buy_pct": 0}), asset="CASH")),
}[producer]
with pytest.raises(ValueError, match="asset"):
operation()
def test_full_100_trial_tied_grid_keeps_complete_stable_ranking():
grid = {"k1": [index / 10 for index in range(10)], "k2": [index / 10 for index in range(10)]}
result = optimize_strategy_research(
"DualThrust", bars([10] * 10), asset="SYNTHETIC", param_grid=grid, objective="total_return"
)
ranking = report(build(result))["optimization"]
assert ranking["trial_count"] == 100
assert len(ranking["trials"]) == 100
assert [trial["parameters"] for trial in ranking["trials"]] == [
trial.parameters for trial in result.trials
]
assert all(trial["score"] == 0 for trial in ranking["trials"])