quantbt-engine 1.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantbt/__init__.py +862 -0
- quantbt/adapters/__init__.py +5 -0
- quantbt/adapters/nautilus/__init__.py +42 -0
- quantbt/adapters/nautilus/_dependency.py +53 -0
- quantbt/adapters/nautilus/backend.py +639 -0
- quantbt/adapters/nautilus/instruments.py +275 -0
- quantbt/adapters/nautilus/options.py +465 -0
- quantbt/adapters/nautilus/reports.py +231 -0
- quantbt/backends/__init__.py +29 -0
- quantbt/backends/_native_event_rust.py +2168 -0
- quantbt/backends/native_event.py +5048 -0
- quantbt/backends/native_option.py +796 -0
- quantbt/backends/native_portfolio.py +928 -0
- quantbt/backends/native_vectorized.py +1299 -0
- quantbt/backtester.py +485 -0
- quantbt/benchmarks/__init__.py +1 -0
- quantbt/benchmarks/compare_phase9_parity.py +302 -0
- quantbt/benchmarks/gamma_scalping_backtestsample.py +802 -0
- quantbt/benchmarks/profile_phase7.py +415 -0
- quantbt/benchmarks/run_arbitrage_phase_e.py +160 -0
- quantbt/benchmarks/run_optimization_overhead.py +195 -0
- quantbt/benchmarks/run_options_engine.py +272 -0
- quantbt/benchmarks/run_pct_equity_nautilus_smoke.py +227 -0
- quantbt/benchmarks/run_phase12_arbitrage_cert.py +402 -0
- quantbt/benchmarks/run_phase12_benchmark_nautilus_cert.py +430 -0
- quantbt/benchmarks/run_phase13_portfolio_report.py +97 -0
- quantbt/benchmarks/run_phase13_wfo_cache.py +162 -0
- quantbt/benchmarks/run_phase14_service_loop.py +694 -0
- quantbt/benchmarks/run_phase15a_nautilus_certification.py +514 -0
- quantbt/benchmarks/run_phase15b_synthetic_depth.py +175 -0
- quantbt/benchmarks/run_phase16_performance_debt.py +389 -0
- quantbt/benchmarks/run_phase30e_reactive_runner.py +161 -0
- quantbt/benchmarks/run_phase31_intrabar.py +441 -0
- quantbt/benchmarks/run_phase34a_native_event_memory.py +179 -0
- quantbt/benchmarks/run_phase34b_native_event_prepared_score.py +173 -0
- quantbt/benchmarks/run_phase34c_native_event_single_pass.py +176 -0
- quantbt/benchmarks/run_phase7.py +654 -0
- quantbt/benchmarks/run_portfolio_real_parity.py +537 -0
- quantbt/core/__init__.py +323 -0
- quantbt/core/arbitrage.py +767 -0
- quantbt/core/basket.py +234 -0
- quantbt/core/certification.py +274 -0
- quantbt/core/constraints.py +155 -0
- quantbt/core/engine.py +1144 -0
- quantbt/core/event.py +869 -0
- quantbt/core/execution_contract.py +197 -0
- quantbt/core/execution_depth.py +613 -0
- quantbt/core/intrabar_kernel.py +1906 -0
- quantbt/core/intrabar_reference.py +907 -0
- quantbt/core/intrabar_session.py +156 -0
- quantbt/core/market_tape.py +480 -0
- quantbt/core/native_event_capabilities.py +121 -0
- quantbt/core/native_event_parity.py +336 -0
- quantbt/core/order_compiler.py +422 -0
- quantbt/core/orders.py +319 -0
- quantbt/core/portfolio.py +392 -0
- quantbt/core/preprocessor.py +256 -0
- quantbt/core/reactive.py +173 -0
- quantbt/core/results.py +341 -0
- quantbt/core/schema.py +211 -0
- quantbt/core/scopes.py +96 -0
- quantbt/core/structured_orders.py +378 -0
- quantbt/core/types.py +87 -0
- quantbt/core/vectorized.py +203 -0
- quantbt/endpoint.py +4465 -0
- quantbt/engines.py +1014 -0
- quantbt/metrics/__init__.py +45 -0
- quantbt/metrics/options_analytics.py +46 -0
- quantbt/metrics/performance.py +541 -0
- quantbt/optimization/__init__.py +94 -0
- quantbt/optimization/callbacks.py +116 -0
- quantbt/optimization/candidate_selection.py +397 -0
- quantbt/optimization/config.py +84 -0
- quantbt/optimization/constraints.py +22 -0
- quantbt/optimization/evaluator.py +21 -0
- quantbt/optimization/evaluators/__init__.py +32 -0
- quantbt/optimization/evaluators/arbitrage.py +22 -0
- quantbt/optimization/evaluators/generic.py +34 -0
- quantbt/optimization/evaluators/grid_dca.py +22 -0
- quantbt/optimization/evaluators/intrabar.py +54 -0
- quantbt/optimization/evaluators/native_event.py +51 -0
- quantbt/optimization/evaluators/options.py +22 -0
- quantbt/optimization/evaluators/portfolio.py +42 -0
- quantbt/optimization/evaluators/signal.py +43 -0
- quantbt/optimization/multiseed.py +176 -0
- quantbt/optimization/objectives.py +221 -0
- quantbt/optimization/optimizer.py +500 -0
- quantbt/optimization/result.py +80 -0
- quantbt/optimization/samplers.py +84 -0
- quantbt/optimization/space.py +223 -0
- quantbt/options/__init__.py +215 -0
- quantbt/options/cache.py +116 -0
- quantbt/options/conventions.py +158 -0
- quantbt/options/data.py +177 -0
- quantbt/options/execution.py +600 -0
- quantbt/options/fees.py +135 -0
- quantbt/options/greeks.py +173 -0
- quantbt/options/hedging.py +232 -0
- quantbt/options/iv.py +188 -0
- quantbt/options/ledger.py +263 -0
- quantbt/options/lifecycle.py +94 -0
- quantbt/options/margin.py +311 -0
- quantbt/options/packages.py +147 -0
- quantbt/options/pricing.py +191 -0
- quantbt/options/schema.py +227 -0
- quantbt/options/selectors.py +281 -0
- quantbt/options/strategy.py +249 -0
- quantbt/options/surface.py +142 -0
- quantbt/options/tape.py +226 -0
- quantbt/options/templates/__init__.py +33 -0
- quantbt/options/templates/packages.py +354 -0
- quantbt/portfolio.py +606 -0
- quantbt/py.typed +1 -0
- quantbt/reporting/__init__.py +38 -0
- quantbt/reporting/arbitrage_audit.py +211 -0
- quantbt/reporting/nautilus_bundle.py +758 -0
- quantbt/reporting/nautilus_certification.py +226 -0
- quantbt/reporting/nautilus_diagnostics.py +225 -0
- quantbt/reporting/parity.py +496 -0
- quantbt/reporting/portfolio_audit.py +206 -0
- quantbt/reporting/portfolio_nautilus.py +140 -0
- quantbt/sizing/__init__.py +3 -0
- quantbt/sizing/fast.py +67 -0
- quantbt/sizing/modes.py +162 -0
- quantbt/viz/__init__.py +4 -0
- quantbt/viz/plots.py +310 -0
- quantbt/viz/themes.py +102 -0
- quantbt/walkforward.py +3136 -0
- quantbt_engine-1.0.7.dist-info/METADATA +891 -0
- quantbt_engine-1.0.7.dist-info/RECORD +133 -0
- quantbt_engine-1.0.7.dist-info/WHEEL +5 -0
- quantbt_engine-1.0.7.dist-info/licenses/LICENSE +21 -0
- quantbt_engine-1.0.7.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""Nautilus certification artifact helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import asdict, dataclass
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict, Optional, Sequence
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
from ..core.results import BacktestResultV2
|
|
14
|
+
from .parity import build_native_nautilus_parity_report, summarize_native_nautilus_parity_report
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class NautilusToleranceProfile:
|
|
19
|
+
"""Tolerance contract for native-vs-Nautilus certification artifacts."""
|
|
20
|
+
|
|
21
|
+
fill_price_tolerance: float = 1e-9
|
|
22
|
+
fee_tolerance: float = 1e-9
|
|
23
|
+
position_tolerance: float = 1e-9
|
|
24
|
+
equity_tolerance: float = 1e-6
|
|
25
|
+
quantity_tolerance: float = 1e-9
|
|
26
|
+
slippage_tolerance: float = 1e-9
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def build_nautilus_certification_profile(
|
|
30
|
+
native_result: Optional[BacktestResultV2],
|
|
31
|
+
nautilus_result: BacktestResultV2,
|
|
32
|
+
*,
|
|
33
|
+
tolerance: NautilusToleranceProfile | Dict[str, float] | None = None,
|
|
34
|
+
workflow: str = "nautilus",
|
|
35
|
+
) -> Dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Build a compact tolerance profile for a native-vs-Nautilus run.
|
|
38
|
+
|
|
39
|
+
The profile is deliberately report-layer only. It never changes engine
|
|
40
|
+
accounting and is suitable for saved stakeholder bundles.
|
|
41
|
+
"""
|
|
42
|
+
tol = _coerce_tolerance(tolerance)
|
|
43
|
+
if native_result is None:
|
|
44
|
+
return {
|
|
45
|
+
"workflow": workflow,
|
|
46
|
+
"status": "reference_missing",
|
|
47
|
+
"passed": False,
|
|
48
|
+
"reason": "native reference result is required for tolerance certification",
|
|
49
|
+
"tolerance": asdict(tol),
|
|
50
|
+
"checks": {},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
parity = build_native_nautilus_parity_report(native_result, nautilus_result)
|
|
54
|
+
summary = summarize_native_nautilus_parity_report(
|
|
55
|
+
parity,
|
|
56
|
+
fill_price_tolerance=tol.fill_price_tolerance,
|
|
57
|
+
fee_tolerance=tol.fee_tolerance,
|
|
58
|
+
position_tolerance=tol.position_tolerance,
|
|
59
|
+
equity_tolerance=tol.equity_tolerance,
|
|
60
|
+
)
|
|
61
|
+
quantity_diff = _max_abs_quantity_diff(native_result, nautilus_result)
|
|
62
|
+
final_equity_diff = _final_equity_diff(native_result, nautilus_result)
|
|
63
|
+
max_equity_diff = max(float(summary.get("max_abs_equity_diff", 0.0)), final_equity_diff)
|
|
64
|
+
slippage_diff = float(summary.get("max_abs_fill_price_diff", 0.0))
|
|
65
|
+
checks = {
|
|
66
|
+
"fill_price_within_tolerance": float(summary["max_abs_fill_price_diff"]) <= tol.fill_price_tolerance,
|
|
67
|
+
"fee_within_tolerance": float(summary["max_abs_fee_diff"]) <= tol.fee_tolerance,
|
|
68
|
+
"position_within_tolerance": float(summary["max_abs_position_diff"]) <= tol.position_tolerance,
|
|
69
|
+
"equity_within_tolerance": max_equity_diff <= tol.equity_tolerance,
|
|
70
|
+
"quantity_within_tolerance": quantity_diff <= tol.quantity_tolerance,
|
|
71
|
+
"slippage_within_tolerance": slippage_diff <= tol.slippage_tolerance,
|
|
72
|
+
}
|
|
73
|
+
passed = bool(all(checks.values()))
|
|
74
|
+
status = "pass" if passed else _profile_status(checks)
|
|
75
|
+
return {
|
|
76
|
+
"workflow": workflow,
|
|
77
|
+
"status": status,
|
|
78
|
+
"passed": passed,
|
|
79
|
+
"tolerance": asdict(tol),
|
|
80
|
+
"checks": checks,
|
|
81
|
+
"summary": summary,
|
|
82
|
+
"max_abs_quantity_diff": float(quantity_diff),
|
|
83
|
+
"max_abs_final_equity_diff": float(final_equity_diff),
|
|
84
|
+
"max_abs_equity_diff_including_final": float(max_equity_diff),
|
|
85
|
+
"max_abs_slippage_proxy_diff": float(slippage_diff),
|
|
86
|
+
"rows": int(len(parity)),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def write_nautilus_certification_artifacts(
|
|
91
|
+
*,
|
|
92
|
+
native_result: Optional[BacktestResultV2],
|
|
93
|
+
nautilus_result: BacktestResultV2,
|
|
94
|
+
report_dir: str | Path,
|
|
95
|
+
workflow: str,
|
|
96
|
+
tolerance: NautilusToleranceProfile | Dict[str, float] | None = None,
|
|
97
|
+
known_differences: Optional[Sequence[str]] = None,
|
|
98
|
+
) -> Dict[str, Any]:
|
|
99
|
+
"""
|
|
100
|
+
Write parity, tolerance, known-difference, and summary files into a bundle.
|
|
101
|
+
"""
|
|
102
|
+
path = Path(report_dir)
|
|
103
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
tol = _coerce_tolerance(tolerance)
|
|
105
|
+
parity = (
|
|
106
|
+
build_native_nautilus_parity_report(native_result, nautilus_result)
|
|
107
|
+
if native_result is not None
|
|
108
|
+
else pd.DataFrame()
|
|
109
|
+
)
|
|
110
|
+
profile = build_nautilus_certification_profile(
|
|
111
|
+
native_result=native_result,
|
|
112
|
+
nautilus_result=nautilus_result,
|
|
113
|
+
tolerance=tol,
|
|
114
|
+
workflow=workflow,
|
|
115
|
+
)
|
|
116
|
+
differences = list(known_differences or ())
|
|
117
|
+
parity_path = path / "native_vs_nautilus_parity.csv"
|
|
118
|
+
profile_path = path / "tolerance_profile.json"
|
|
119
|
+
known_path = path / "known_differences.md"
|
|
120
|
+
summary_path = path / "certification_summary.json"
|
|
121
|
+
|
|
122
|
+
parity.to_csv(parity_path, index=False)
|
|
123
|
+
_write_json(profile_path, profile)
|
|
124
|
+
known_path.write_text(_known_differences_markdown(workflow, differences), encoding="utf-8")
|
|
125
|
+
summary = {
|
|
126
|
+
"workflow": workflow,
|
|
127
|
+
"status": profile["status"],
|
|
128
|
+
"passed": profile["passed"],
|
|
129
|
+
"report_dir": str(path),
|
|
130
|
+
"files": {
|
|
131
|
+
"native_vs_nautilus_parity": parity_path.name,
|
|
132
|
+
"tolerance_profile": profile_path.name,
|
|
133
|
+
"known_differences": known_path.name,
|
|
134
|
+
},
|
|
135
|
+
"known_differences_count": len(differences),
|
|
136
|
+
}
|
|
137
|
+
_write_json(summary_path, summary)
|
|
138
|
+
return {
|
|
139
|
+
**summary,
|
|
140
|
+
"tolerance_profile": profile,
|
|
141
|
+
"artifact_files": [parity_path.name, profile_path.name, known_path.name, summary_path.name],
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _coerce_tolerance(value: NautilusToleranceProfile | Dict[str, float] | None) -> NautilusToleranceProfile:
|
|
146
|
+
if value is None:
|
|
147
|
+
return NautilusToleranceProfile()
|
|
148
|
+
if isinstance(value, NautilusToleranceProfile):
|
|
149
|
+
return value
|
|
150
|
+
return NautilusToleranceProfile(**{key: float(val) for key, val in dict(value).items()})
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _profile_status(checks: Dict[str, bool]) -> str:
|
|
154
|
+
if not checks.get("fill_price_within_tolerance", True) or not checks.get("quantity_within_tolerance", True):
|
|
155
|
+
return "execution_diff"
|
|
156
|
+
if not checks.get("position_within_tolerance", True):
|
|
157
|
+
return "position_diff"
|
|
158
|
+
if not checks.get("fee_within_tolerance", True) or not checks.get("equity_within_tolerance", True):
|
|
159
|
+
return "accounting_diff"
|
|
160
|
+
return "diff"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _max_abs_quantity_diff(native_result: BacktestResultV2, nautilus_result: BacktestResultV2) -> float:
|
|
164
|
+
native_qty = _fill_quantities(native_result)
|
|
165
|
+
nautilus_qty = _fill_quantities(nautilus_result)
|
|
166
|
+
n = max(len(native_qty), len(nautilus_qty))
|
|
167
|
+
if n == 0:
|
|
168
|
+
return 0.0
|
|
169
|
+
left = np.zeros(n, dtype=float)
|
|
170
|
+
right = np.zeros(n, dtype=float)
|
|
171
|
+
left[: len(native_qty)] = native_qty
|
|
172
|
+
right[: len(nautilus_qty)] = nautilus_qty
|
|
173
|
+
return float(np.max(np.abs(left - right)))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _final_equity_diff(native_result: BacktestResultV2, nautilus_result: BacktestResultV2) -> float:
|
|
177
|
+
if len(native_result.equity) == 0 or len(nautilus_result.equity) == 0:
|
|
178
|
+
return 0.0
|
|
179
|
+
return float(abs(float(native_result.equity.iloc[-1]) - float(nautilus_result.equity.iloc[-1])))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _fill_quantities(result: BacktestResultV2) -> np.ndarray:
|
|
183
|
+
report = _frame((result.metadata or {}).get("fills_report"))
|
|
184
|
+
if report.empty:
|
|
185
|
+
report = _frame((result.metadata or {}).get("orders_report"))
|
|
186
|
+
if not report.empty:
|
|
187
|
+
if "status" in report:
|
|
188
|
+
report = report[report["status"].astype(str).str.upper().eq("FILLED")]
|
|
189
|
+
for col in ("filled_qty", "quantity", "qty"):
|
|
190
|
+
if col in report:
|
|
191
|
+
return pd.to_numeric(report[col], errors="coerce").fillna(0.0).to_numpy(dtype=float)
|
|
192
|
+
fills = getattr(result, "fills", ())
|
|
193
|
+
if fills:
|
|
194
|
+
return np.asarray([float(getattr(fill, "qty", 0.0)) for fill in fills], dtype=float)
|
|
195
|
+
return np.asarray([], dtype=float)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _frame(value: Any) -> pd.DataFrame:
|
|
199
|
+
if isinstance(value, pd.DataFrame):
|
|
200
|
+
return value.copy()
|
|
201
|
+
if value is None:
|
|
202
|
+
return pd.DataFrame()
|
|
203
|
+
try:
|
|
204
|
+
return pd.DataFrame(value).copy()
|
|
205
|
+
except Exception:
|
|
206
|
+
return pd.DataFrame()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _known_differences_markdown(workflow: str, differences: Sequence[str]) -> str:
|
|
210
|
+
lines = [f"# Known Differences - {workflow}", ""]
|
|
211
|
+
if differences:
|
|
212
|
+
for item in differences:
|
|
213
|
+
lines.append(f"- {item}")
|
|
214
|
+
else:
|
|
215
|
+
lines.append("- None recorded for this certification run.")
|
|
216
|
+
lines.extend(
|
|
217
|
+
[
|
|
218
|
+
"",
|
|
219
|
+
"These notes describe known adapter or venue-model differences. They do not override tolerance failures.",
|
|
220
|
+
]
|
|
221
|
+
)
|
|
222
|
+
return "\n".join(lines) + "\n"
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _write_json(path: Path, payload: Dict[str, Any]) -> None:
|
|
226
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True, default=str), encoding="utf-8")
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Nautilus validation diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from ..adapters.nautilus.instruments import SUPPORTED_BINANCE_PERP_SPECS, normalize_binance_perp_symbol
|
|
11
|
+
from ..core.results import BacktestResultV2
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def build_nautilus_pct_equity_diagnostic(
|
|
15
|
+
result: BacktestResultV2,
|
|
16
|
+
*,
|
|
17
|
+
data: pd.DataFrame,
|
|
18
|
+
signal: pd.Series,
|
|
19
|
+
native_fee_round_trip: Optional[float] = None,
|
|
20
|
+
native_fee_one_way: Optional[float] = None,
|
|
21
|
+
native_use_funding: Optional[bool] = None,
|
|
22
|
+
native_slippage: Optional[float] = None,
|
|
23
|
+
) -> Dict:
|
|
24
|
+
"""
|
|
25
|
+
Compare a Nautilus `%_equity` validation run against native expectations.
|
|
26
|
+
|
|
27
|
+
The helper is intentionally diagnostic-only: it does not claim that
|
|
28
|
+
Nautilus and native legacy should match. It highlights the most common
|
|
29
|
+
sources of divergence: fee convention/application, funding, slippage,
|
|
30
|
+
signal transitions, and Binance lot-size constraints.
|
|
31
|
+
"""
|
|
32
|
+
if not isinstance(result, BacktestResultV2):
|
|
33
|
+
raise TypeError("build_nautilus_pct_equity_diagnostic requires BacktestResultV2")
|
|
34
|
+
if "close" not in data:
|
|
35
|
+
raise ValueError("data must contain a close column")
|
|
36
|
+
|
|
37
|
+
metadata = result.metadata or {}
|
|
38
|
+
idx = _utc_index(data.index)
|
|
39
|
+
sig = _align_signal(signal, idx)
|
|
40
|
+
use_pyramiding = bool(metadata.get("use_pyramiding", _nested(metadata, "run_config", "sizing", "use_pyramiding", default=True)))
|
|
41
|
+
effective_signal = sig.astype(float) if use_pyramiding else np.sign(sig.astype(float))
|
|
42
|
+
transitions = effective_signal.ne(effective_signal.shift(1).fillna(0.0))
|
|
43
|
+
transition_report = pd.DataFrame(
|
|
44
|
+
{
|
|
45
|
+
"timestamp": idx[transitions.to_numpy()],
|
|
46
|
+
"raw_signal": sig.loc[transitions].to_numpy(dtype=float),
|
|
47
|
+
"effective_signal": effective_signal.loc[transitions].to_numpy(dtype=float),
|
|
48
|
+
"close": data.reindex(idx)["close"].loc[transitions].to_numpy(dtype=float),
|
|
49
|
+
}
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
orders_count = int(metadata.get("orders_count", len(_frame(metadata.get("orders_report", metadata.get("order_report"))))))
|
|
53
|
+
fills_count = int(metadata.get("fills_count", len(_frame(metadata.get("fills_report")))))
|
|
54
|
+
sizing_mode = str(metadata.get("sizing_mode", _nested(metadata, "run_config", "sizing", "hedge_type", default=""))).lower()
|
|
55
|
+
requested_fee = _safe_float(metadata.get("fee_rate"))
|
|
56
|
+
requested_slippage = _safe_float(metadata.get("slippage"))
|
|
57
|
+
expected_fee = native_fee_one_way
|
|
58
|
+
if expected_fee is None and native_fee_round_trip is not None:
|
|
59
|
+
expected_fee = float(native_fee_round_trip) / 2.0
|
|
60
|
+
|
|
61
|
+
constraints = _instrument_constraints(metadata)
|
|
62
|
+
lot_report = _lot_size_risk_report(
|
|
63
|
+
transition_report=transition_report,
|
|
64
|
+
initial_capital=float(result.initial_capital),
|
|
65
|
+
alloc_per_trade=float(metadata.get("trade_notional", metadata.get("alloc_per_trade", 0.0)) or 0.0),
|
|
66
|
+
constraints=constraints,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
checks = {
|
|
70
|
+
"sizing_mode_is_pct_equity": sizing_mode in {"%_equity", "pct_equity"},
|
|
71
|
+
"orders_not_more_than_signal_transitions": orders_count <= int(len(transition_report)),
|
|
72
|
+
"fills_not_more_than_orders": fills_count <= orders_count,
|
|
73
|
+
"fee_convention_matches_native": True if expected_fee is None or requested_fee is None else abs(float(expected_fee) - float(requested_fee)) <= 1e-15,
|
|
74
|
+
"custom_fee_rate_applied_to_nautilus": False,
|
|
75
|
+
"funding_matches_native": True if native_use_funding is None else bool(native_use_funding) is False,
|
|
76
|
+
"slippage_matches_native": True if native_slippage is None else abs(float(native_slippage)) <= 1e-15,
|
|
77
|
+
"custom_slippage_applied_to_nautilus": False,
|
|
78
|
+
"has_lot_size_constraints": constraints.get("qty_step") is not None,
|
|
79
|
+
}
|
|
80
|
+
recommendations = _recommendations(checks, native_use_funding=native_use_funding, native_slippage=native_slippage)
|
|
81
|
+
status = "ok" if all(checks.values()) else "diff"
|
|
82
|
+
return {
|
|
83
|
+
"status": status,
|
|
84
|
+
"checks": checks,
|
|
85
|
+
"signal": {
|
|
86
|
+
"rows": int(len(idx)),
|
|
87
|
+
"raw_transition_count": int(sig.ne(sig.shift(1).fillna(0.0)).sum()),
|
|
88
|
+
"effective_transition_count": int(len(transition_report)),
|
|
89
|
+
"use_pyramiding": use_pyramiding,
|
|
90
|
+
"signal_index_matches_data_index": bool(_utc_index(signal.index).equals(idx)),
|
|
91
|
+
"transition_report": transition_report,
|
|
92
|
+
},
|
|
93
|
+
"orders": {
|
|
94
|
+
"orders_count": orders_count,
|
|
95
|
+
"fills_count": fills_count,
|
|
96
|
+
"positions_count": int(metadata.get("positions_count", 0) or 0),
|
|
97
|
+
"missing_order_events_vs_transitions": max(0, int(len(transition_report)) - orders_count),
|
|
98
|
+
},
|
|
99
|
+
"execution_semantics": {
|
|
100
|
+
"requested_fee_rate": requested_fee,
|
|
101
|
+
"expected_native_one_way_fee_rate": expected_fee,
|
|
102
|
+
"custom_fee_rate_applied_to_nautilus": False,
|
|
103
|
+
"requested_slippage": requested_slippage,
|
|
104
|
+
"native_slippage": native_slippage,
|
|
105
|
+
"custom_slippage_applied_to_nautilus": False,
|
|
106
|
+
"native_use_funding": native_use_funding,
|
|
107
|
+
"nautilus_signal_funding_supported": False,
|
|
108
|
+
"adapter_fill_model": "NautilusTrader bar market execution with instrument maker/taker fee model",
|
|
109
|
+
},
|
|
110
|
+
"instrument_constraints": constraints,
|
|
111
|
+
"lot_size_risk": lot_report,
|
|
112
|
+
"recommendations": recommendations,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _instrument_constraints(metadata: Dict) -> Dict:
|
|
117
|
+
instrument_id = metadata.get("instrument_id") or _nested(metadata, "run_config", "nautilus", "instrument_id")
|
|
118
|
+
out = {
|
|
119
|
+
"instrument_id": instrument_id,
|
|
120
|
+
"qty_step": metadata.get("qty_step") or metadata.get("lot_size") or metadata.get("size_increment"),
|
|
121
|
+
"lot_size": metadata.get("lot_size") or metadata.get("qty_step") or metadata.get("size_increment"),
|
|
122
|
+
"min_qty": metadata.get("min_qty") or metadata.get("min_quantity"),
|
|
123
|
+
"min_notional": metadata.get("min_notional"),
|
|
124
|
+
"contract_size_note": "contract_size is a multiplier; lot_size/qty_step controls fractional crypto order acceptance",
|
|
125
|
+
}
|
|
126
|
+
if instrument_id:
|
|
127
|
+
try:
|
|
128
|
+
spec = SUPPORTED_BINANCE_PERP_SPECS[normalize_binance_perp_symbol(str(instrument_id))]
|
|
129
|
+
out.update(
|
|
130
|
+
{
|
|
131
|
+
"qty_step": out["qty_step"] or spec.size_increment,
|
|
132
|
+
"lot_size": out["lot_size"] or spec.size_increment,
|
|
133
|
+
"min_qty": out["min_qty"] or spec.min_quantity,
|
|
134
|
+
"min_notional": out["min_notional"] or "10.0",
|
|
135
|
+
"price_increment": spec.price_increment,
|
|
136
|
+
"quantity_precision": spec.size_precision,
|
|
137
|
+
}
|
|
138
|
+
)
|
|
139
|
+
except ValueError:
|
|
140
|
+
pass
|
|
141
|
+
return out
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _lot_size_risk_report(
|
|
145
|
+
*,
|
|
146
|
+
transition_report: pd.DataFrame,
|
|
147
|
+
initial_capital: float,
|
|
148
|
+
alloc_per_trade: float,
|
|
149
|
+
constraints: Dict,
|
|
150
|
+
) -> Dict:
|
|
151
|
+
qty_step = _safe_float(constraints.get("qty_step"))
|
|
152
|
+
min_qty = _safe_float(constraints.get("min_qty"))
|
|
153
|
+
if transition_report.empty or qty_step is None:
|
|
154
|
+
return {"status": "unknown", "potential_small_delta_count": 0}
|
|
155
|
+
alloc = alloc_per_trade / 100.0 if alloc_per_trade > 1.0 else alloc_per_trade
|
|
156
|
+
close = pd.to_numeric(transition_report["close"], errors="coerce").replace(0.0, np.nan)
|
|
157
|
+
signal = pd.to_numeric(transition_report["effective_signal"], errors="coerce").abs()
|
|
158
|
+
approx_qty = (initial_capital * alloc * signal / close).fillna(0.0)
|
|
159
|
+
threshold = max(qty_step, min_qty or 0.0)
|
|
160
|
+
small = approx_qty < threshold
|
|
161
|
+
return {
|
|
162
|
+
"status": "ok" if not bool(small.any()) else "risk",
|
|
163
|
+
"potential_small_delta_count": int(small.sum()),
|
|
164
|
+
"qty_step": float(qty_step),
|
|
165
|
+
"min_qty": None if min_qty is None else float(min_qty),
|
|
166
|
+
"min_transition_approx_qty": float(approx_qty.min()) if len(approx_qty) else 0.0,
|
|
167
|
+
"note": "Approximation uses initial capital only; live equity and current position can create smaller deltas later.",
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _recommendations(checks: Dict[str, bool], *, native_use_funding, native_slippage) -> List[str]:
|
|
172
|
+
out: List[str] = []
|
|
173
|
+
if not checks["fee_convention_matches_native"]:
|
|
174
|
+
out.append("Align fee convention: legacy `fee` is round-trip; Nautilus `fee_rate` is metadata one-way today.")
|
|
175
|
+
if not checks["custom_fee_rate_applied_to_nautilus"]:
|
|
176
|
+
out.append("Current Nautilus signal adapter uses Nautilus instrument maker/taker fees, not endpoint custom fee_rate.")
|
|
177
|
+
if native_use_funding:
|
|
178
|
+
out.append("Disable native funding for apples-to-apples, or implement Nautilus funding/carry adapter.")
|
|
179
|
+
if native_slippage and abs(float(native_slippage)) > 0.0:
|
|
180
|
+
out.append("Disable native slippage for apples-to-apples, or implement Nautilus slippage model.")
|
|
181
|
+
if not checks["orders_not_more_than_signal_transitions"]:
|
|
182
|
+
out.append("Inspect signal timestamp alignment and Nautilus order reports; order count exceeds transition count.")
|
|
183
|
+
return out
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _align_signal(signal: pd.Series, idx: pd.DatetimeIndex) -> pd.Series:
|
|
187
|
+
sig = signal.copy()
|
|
188
|
+
if not isinstance(sig.index, pd.DatetimeIndex):
|
|
189
|
+
sig.index = pd.to_datetime(sig.index, utc=True)
|
|
190
|
+
if sig.index.tz is None:
|
|
191
|
+
sig.index = sig.index.tz_localize("UTC")
|
|
192
|
+
else:
|
|
193
|
+
sig.index = sig.index.tz_convert("UTC")
|
|
194
|
+
return sig.reindex(idx, method="ffill").fillna(0.0)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _utc_index(index) -> pd.DatetimeIndex:
|
|
198
|
+
idx = pd.DatetimeIndex(pd.to_datetime(index, utc=True))
|
|
199
|
+
if idx.tz is None:
|
|
200
|
+
idx = idx.tz_localize("UTC")
|
|
201
|
+
else:
|
|
202
|
+
idx = idx.tz_convert("UTC")
|
|
203
|
+
return idx
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _nested(mapping: Dict, *keys, default=None):
|
|
207
|
+
cur = mapping
|
|
208
|
+
for key in keys:
|
|
209
|
+
if not isinstance(cur, dict) or key not in cur:
|
|
210
|
+
return default
|
|
211
|
+
cur = cur[key]
|
|
212
|
+
return cur
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _frame(value) -> pd.DataFrame:
|
|
216
|
+
return value if isinstance(value, pd.DataFrame) else pd.DataFrame()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _safe_float(value):
|
|
220
|
+
try:
|
|
221
|
+
if value is None:
|
|
222
|
+
return None
|
|
223
|
+
return float(value)
|
|
224
|
+
except (TypeError, ValueError):
|
|
225
|
+
return None
|