alphaengine 1.0.1__tar.gz → 1.0.2__tar.gz
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.
- {alphaengine-1.0.1 → alphaengine-1.0.2}/PKG-INFO +1 -1
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/_version.py +8 -1
- alphaengine-1.0.2/src/alphaengine/agent/bakeoff.py +398 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/brief.py +44 -13
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/chart.py +44 -24
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/bakeoff.py +159 -19
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/dispatch.py +17 -1
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_bakeoff.py +46 -3
- alphaengine-1.0.1/src/alphaengine/agent/bakeoff.py +0 -240
- {alphaengine-1.0.1 → alphaengine-1.0.2}/.github/workflows/ci.yml +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/.github/workflows/publish.yml +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/.gitignore +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/AGENTS.md +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/LICENSE +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/README.md +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/SECURITY.md +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/assets/banner.png +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/assets/data_boundary.png +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/assets/session.png +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/figure_contract.md +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/workflows/evaluate_process.json +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/workflows/research_week.json +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/docs/workflows/stress_study.json +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/pyproject.toml +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/scripts/gen_docs.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/__main__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/answer.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/driver.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/agent/sim.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/auth.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/book.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/charts.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/cli.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/client/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/client/agent.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/client/executor.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/client/session.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/commands.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/complete.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/connectors/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/context.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/active.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/allocate.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/backtest.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/covariance.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/cross_section.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/factors.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/market.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/options.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/pairs.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/panel.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/performance.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/portfolio.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/process.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/profile.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/rates.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/regime.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/relations.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/risk.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/screen.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/series_shapes.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/signals.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/stress.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/technical.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/trace.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/validation.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/core/walkforward.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/demo.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/demo_book.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/demo_returns.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/demo_signal.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/demo_universe.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/events.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/loaders.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/model.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/py.typed +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/repl.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/study/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/study/report.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/study/schema.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/sweep/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/sweep/runner.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/__init__.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/app.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/mark.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/stage.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/src/alphaengine/tui/typeface.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_agent.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_answer.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_book.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_cli.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_client.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_commands.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_context.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_events.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_executor_ops.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_goldens.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_loaders.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_math_audit.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_model.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_portfolio.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_process.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_quant_maths.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_relations.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_screen.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_signals.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_sim.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_smoke.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_sweep.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_trace.py +0 -0
- {alphaengine-1.0.1 → alphaengine-1.0.2}/tests/test_tui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: alphaengine
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.2
|
|
4
4
|
Summary: Validated research tooling for investment strategies: deflation, overfitting detection, and honest trial counts.
|
|
5
5
|
Project-URL: Homepage, https://github.com/quantOSC/alphaengine
|
|
6
6
|
Project-URL: Documentation, https://github.com/quantOSC/alphaengine#readme
|
|
@@ -143,4 +143,11 @@ is what a changed figure costs. The API may still move underneath it.
|
|
|
143
143
|
# the sample-count divisor in Ledoit-Wolf shrinkage so hierarchical risk parity
|
|
144
144
|
# and risk parity differentiate weights across a 50-name panel, and writes a
|
|
145
145
|
# multi-page portfolio, scenario, Monte Carlo, and bakeoff report to Downloads.
|
|
146
|
-
|
|
146
|
+
#
|
|
147
|
+
# 1.0.2 IS THE FOUR-PASS GAN BAKEOFF. No golden in tests/test_goldens.py moves.
|
|
148
|
+
# The bakeoff tournament runs four adversarial passes (eight candidate proposals
|
|
149
|
+
# across `equal`, `hrp`, `risk_parity`, `inverse_vol`, `var_sorted`, `low_vol`,
|
|
150
|
+
# `momentum`, and `reversion` against eight regime, tail, rate, and crash-cluster
|
|
151
|
+
# attacks) under a hard twelve-call, sixty-second ceiling, feeding the referee's
|
|
152
|
+
# post-attack standing back to the board after every pass.
|
|
153
|
+
__version__ = "1.0.2"
|
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
"""Bakeoff mode. Two roles, one clock, and a referee that does not take votes.
|
|
2
|
+
|
|
3
|
+
Generators may submit a rule. An adversary may submit an attack. Neither may
|
|
4
|
+
start another bakeoff, and neither may name the survivor. The loop is four
|
|
5
|
+
passes of two proposals plus one attack after each pass: twelve model calls,
|
|
6
|
+
and then it stops even if a model asks to continue.
|
|
7
|
+
|
|
8
|
+
The clock is the other stop. The default budget is 45 seconds. Nothing can
|
|
9
|
+
stretch it past 60. When the budget is gone, the call in flight is abandoned
|
|
10
|
+
and the referee scores whatever already came back.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
from collections.abc import Callable
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from ..core.bakeoff import ATTACKS, RULES, referee, trial_paths
|
|
24
|
+
from .sim import Call, Think, Trace
|
|
25
|
+
|
|
26
|
+
DEFAULT_SECONDS = 45.0
|
|
27
|
+
HARD_CEILING = 60.0
|
|
28
|
+
MAX_ROUNDS = 4
|
|
29
|
+
MAX_SEATS = 2
|
|
30
|
+
|
|
31
|
+
_DROPPED = {"winner", "survivor", "score", "sharpe", "verdict", "deflated_sharpe"}
|
|
32
|
+
|
|
33
|
+
_GENERATOR = """BAKEOFF GENERATOR
|
|
34
|
+
Pass @@ROUND@@ of @@ROUNDS@@ (Seat @@SEAT@@ of @@SEATS@@).
|
|
35
|
+
Propose one strategy that survives the adversary's active attacks and improves on
|
|
36
|
+
the current leader's post-attack standing. Do not repeat a rule+lookback+names
|
|
37
|
+
already on the board. You do not score it, you do not name a winner, and you
|
|
38
|
+
do not start another bakeoff.
|
|
39
|
+
Rules: @@RULES@@
|
|
40
|
+
lookback (5 to 60) sets the signal window for momentum/reversion or the calibration
|
|
41
|
+
window for hrp, risk_parity, inverse_vol, var_sorted, and low_vol.
|
|
42
|
+
Optional names: a subset of 2 to 12 symbols from [@@NAMES@@].
|
|
43
|
+
Active attacks: @@ACTIVE_ATTACKS@@
|
|
44
|
+
Question:
|
|
45
|
+
@@QUESTION@@
|
|
46
|
+
Board:
|
|
47
|
+
@@BOARD@@
|
|
48
|
+
Reply with one JSON object: {"rule": "hrp", "lookback": 30, "why": "short reason"}
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
_ADVERSARY = """BAKEOFF ADVERSARY
|
|
52
|
+
Pass @@ROUND@@ of @@ROUNDS@@.
|
|
53
|
+
Name one attack to stress the current leader's regime or tail vulnerability.
|
|
54
|
+
Prefer an attack from Unused attacks so each pass adds a new stress dimension.
|
|
55
|
+
You do not propose a rule, you do not name a winner, and you do not start another bakeoff.
|
|
56
|
+
Attacks: @@ATTACKS@@
|
|
57
|
+
Active attacks: @@ACTIVE_ATTACKS@@
|
|
58
|
+
Unused attacks: @@UNUSED_ATTACKS@@
|
|
59
|
+
Question:
|
|
60
|
+
@@QUESTION@@
|
|
61
|
+
Board:
|
|
62
|
+
@@BOARD@@
|
|
63
|
+
Reply with one JSON object: {"attack": "tail_stress", "why": "short reason"}
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class Bakeoff:
|
|
69
|
+
"""The trace the session shows, and the figures a tool result is allowed to carry."""
|
|
70
|
+
|
|
71
|
+
trace: Trace
|
|
72
|
+
figures: dict[str, Any]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def run_bakeoff(
|
|
76
|
+
question: str,
|
|
77
|
+
think: Think,
|
|
78
|
+
data: Any,
|
|
79
|
+
*,
|
|
80
|
+
rounds: int = MAX_ROUNDS,
|
|
81
|
+
seats: int = MAX_SEATS,
|
|
82
|
+
seconds: float = DEFAULT_SECONDS,
|
|
83
|
+
on_call: Callable[[Call], None] | None = None,
|
|
84
|
+
) -> Bakeoff:
|
|
85
|
+
"""Run one bakeoff. Returns when the rounds end or the clock does, whichever is first."""
|
|
86
|
+
budget = _budget(seconds)
|
|
87
|
+
deadline = time.monotonic() + budget
|
|
88
|
+
n_rounds = min(max(int(rounds), 1), MAX_ROUNDS)
|
|
89
|
+
n_seats = min(max(int(seats), 1), MAX_SEATS)
|
|
90
|
+
submissions: list[dict[str, Any]] = []
|
|
91
|
+
attacks: list[str] = []
|
|
92
|
+
passes: list[dict[str, Any]] = []
|
|
93
|
+
trace = Trace(question=question)
|
|
94
|
+
stopped = "complete"
|
|
95
|
+
names = _universe_names(data)
|
|
96
|
+
|
|
97
|
+
def board() -> str:
|
|
98
|
+
if not submissions and not attacks:
|
|
99
|
+
return "nothing proposed yet"
|
|
100
|
+
scored = referee(data, submissions, attacks)
|
|
101
|
+
lines = list(scored.get("trials") or [])
|
|
102
|
+
if scored.get("survivor"):
|
|
103
|
+
lines.append(
|
|
104
|
+
f"leader {scored.get('survivor')} "
|
|
105
|
+
f"sharpe {scored.get('survivor_sharpe')} "
|
|
106
|
+
f"standing {scored.get('attacked_sharpe')}"
|
|
107
|
+
)
|
|
108
|
+
if attacks:
|
|
109
|
+
lines.append("attacks " + ", ".join(dict.fromkeys(attacks)))
|
|
110
|
+
return "\n".join(str(line) for line in lines) or "nothing proposed yet"
|
|
111
|
+
|
|
112
|
+
def emit(call: Call) -> None:
|
|
113
|
+
trace.calls.append(call)
|
|
114
|
+
if on_call is not None:
|
|
115
|
+
on_call(call)
|
|
116
|
+
|
|
117
|
+
for round_idx in range(1, n_rounds + 1):
|
|
118
|
+
for seat_idx in range(1, n_seats + 1):
|
|
119
|
+
prompt = _fill(
|
|
120
|
+
_GENERATOR,
|
|
121
|
+
question,
|
|
122
|
+
board(),
|
|
123
|
+
round_idx=round_idx,
|
|
124
|
+
n_rounds=n_rounds,
|
|
125
|
+
seat_idx=seat_idx,
|
|
126
|
+
n_seats=n_seats,
|
|
127
|
+
attacks=attacks,
|
|
128
|
+
names=names,
|
|
129
|
+
)
|
|
130
|
+
text, in_time = _ask(think, prompt, deadline)
|
|
131
|
+
if not in_time:
|
|
132
|
+
stopped = "clock"
|
|
133
|
+
break
|
|
134
|
+
submission, why = _proposal(text)
|
|
135
|
+
submission["_round"] = round_idx
|
|
136
|
+
submission["_seat"] = seat_idx
|
|
137
|
+
submissions.append(submission)
|
|
138
|
+
emit(_submit_call(data, submissions, attacks, submission, why, round_idx, seat_idx))
|
|
139
|
+
if stopped == "clock":
|
|
140
|
+
break
|
|
141
|
+
prompt = _fill(
|
|
142
|
+
_ADVERSARY,
|
|
143
|
+
question,
|
|
144
|
+
board(),
|
|
145
|
+
round_idx=round_idx,
|
|
146
|
+
n_rounds=n_rounds,
|
|
147
|
+
seat_idx=0,
|
|
148
|
+
n_seats=n_seats,
|
|
149
|
+
attacks=attacks,
|
|
150
|
+
names=names,
|
|
151
|
+
)
|
|
152
|
+
text, in_time = _ask(think, prompt, deadline)
|
|
153
|
+
if not in_time:
|
|
154
|
+
stopped = "clock"
|
|
155
|
+
break
|
|
156
|
+
attack, why = _attack(text)
|
|
157
|
+
if attack is None:
|
|
158
|
+
emit(
|
|
159
|
+
Call(
|
|
160
|
+
tool="attack",
|
|
161
|
+
args={"pass": round_idx},
|
|
162
|
+
error="that attack is not in the menu",
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
continue
|
|
166
|
+
attacks.append(attack)
|
|
167
|
+
scored_pass = referee(data, submissions, attacks)
|
|
168
|
+
passes.append(
|
|
169
|
+
{
|
|
170
|
+
"pass": round_idx,
|
|
171
|
+
"attack": attack,
|
|
172
|
+
"why": why,
|
|
173
|
+
"leader": scored_pass.get("survivor"),
|
|
174
|
+
"leader_sharpe": scored_pass.get("survivor_sharpe"),
|
|
175
|
+
"leader_standing": scored_pass.get("attacked_sharpe"),
|
|
176
|
+
"n_trials": len(submissions),
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
emit(_attack_call(scored_pass, attack, why, round_idx))
|
|
180
|
+
|
|
181
|
+
figures = referee(data, submissions, attacks)
|
|
182
|
+
elapsed = min(budget, time.monotonic() - (deadline - budget))
|
|
183
|
+
figures["seconds"] = round(elapsed, 1)
|
|
184
|
+
figures["stopped"] = stopped
|
|
185
|
+
figures["budget_seconds"] = budget
|
|
186
|
+
figures["n_passes"] = len(passes) or n_rounds
|
|
187
|
+
trace.answer = _verdict(figures)
|
|
188
|
+
curves = trial_paths(data, submissions, attacks)
|
|
189
|
+
survivor_label = str(figures.get("survivor") or "")
|
|
190
|
+
survivor_curve = next((c for c in curves if c.get("label") == survivor_label), None)
|
|
191
|
+
trace.bakeoff = {
|
|
192
|
+
"survivor": figures.get("survivor"),
|
|
193
|
+
"survivor_sharpe": figures.get("survivor_sharpe"),
|
|
194
|
+
"attacked_sharpe": figures.get("attacked_sharpe"),
|
|
195
|
+
"deflated_sharpe": figures.get("deflated_sharpe"),
|
|
196
|
+
"verdict": figures.get("verdict"),
|
|
197
|
+
"n_trials": figures.get("n_trials"),
|
|
198
|
+
"n_failed": figures.get("n_failed"),
|
|
199
|
+
"n_passes": len(passes) or n_rounds,
|
|
200
|
+
"attacks": list(figures.get("attacks") or []),
|
|
201
|
+
"trials": list(figures.get("trials") or []),
|
|
202
|
+
"passes": passes,
|
|
203
|
+
"curves": curves,
|
|
204
|
+
"survivor_weights": (survivor_curve or {}).get("weights") or {},
|
|
205
|
+
}
|
|
206
|
+
return Bakeoff(trace=trace, figures=figures)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _submit_call(
|
|
210
|
+
data: Any,
|
|
211
|
+
submissions: list[dict[str, Any]],
|
|
212
|
+
attacks: list[str],
|
|
213
|
+
submission: dict[str, Any],
|
|
214
|
+
why: str,
|
|
215
|
+
round_idx: int,
|
|
216
|
+
seat_idx: int,
|
|
217
|
+
) -> Call:
|
|
218
|
+
clean_args = {k: v for k, v in submission.items() if not str(k).startswith("_")}
|
|
219
|
+
scored = referee(data, submissions, attacks)
|
|
220
|
+
trials = list(scored.get("trials") or [])
|
|
221
|
+
last_line = str(trials[-1]) if trials else ""
|
|
222
|
+
rule_name = str(submission.get("rule") or "")
|
|
223
|
+
if not rule_name or "that rule is not in the bakeoff" in last_line:
|
|
224
|
+
return Call(
|
|
225
|
+
tool="submit",
|
|
226
|
+
args=clean_args,
|
|
227
|
+
figures={"pass": round_idx, "seat": seat_idx, "rule": rule_name or "refused"},
|
|
228
|
+
note=f"pass {round_idx}.{seat_idx} refused",
|
|
229
|
+
)
|
|
230
|
+
parts = last_line.split()
|
|
231
|
+
label = parts[1] if len(parts) > 1 else rule_name
|
|
232
|
+
call_figs: dict[str, Any] = {
|
|
233
|
+
"pass": round_idx,
|
|
234
|
+
"seat": seat_idx,
|
|
235
|
+
"proposal": label,
|
|
236
|
+
"leader": scored.get("survivor"),
|
|
237
|
+
"leader_standing": scored.get("attacked_sharpe"),
|
|
238
|
+
}
|
|
239
|
+
if "lookback" in clean_args:
|
|
240
|
+
call_figs["lookback"] = clean_args["lookback"]
|
|
241
|
+
if why:
|
|
242
|
+
call_figs["why"] = why
|
|
243
|
+
note_tail = " ".join(parts[1:]) if len(parts) > 1 else rule_name
|
|
244
|
+
note = f"pass {round_idx}.{seat_idx} {note_tail}"
|
|
245
|
+
return Call(tool="submit", args=clean_args, figures=call_figs, note=note)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _attack_call(
|
|
249
|
+
scored: dict[str, Any],
|
|
250
|
+
attack: str,
|
|
251
|
+
why: str,
|
|
252
|
+
round_idx: int,
|
|
253
|
+
) -> Call:
|
|
254
|
+
leader = scored.get("survivor") or "none"
|
|
255
|
+
standing = scored.get("attacked_sharpe")
|
|
256
|
+
call_figs: dict[str, Any] = {
|
|
257
|
+
"pass": round_idx,
|
|
258
|
+
"attack": attack,
|
|
259
|
+
"leader": leader,
|
|
260
|
+
"leader_sharpe": scored.get("survivor_sharpe"),
|
|
261
|
+
"leader_standing": standing,
|
|
262
|
+
"n_trials": scored.get("n_trials"),
|
|
263
|
+
}
|
|
264
|
+
if why:
|
|
265
|
+
call_figs["why"] = why
|
|
266
|
+
note = f"pass {round_idx} {attack} leader {leader} standing {standing}"
|
|
267
|
+
return Call(tool="attack", args={"attack": attack}, figures=call_figs, note=note)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _universe_names(data: Any) -> list[str]:
|
|
271
|
+
if not isinstance(data, dict):
|
|
272
|
+
return []
|
|
273
|
+
return [str(k) for k in list(data.keys())[:24]]
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _budget(seconds: float) -> float:
|
|
277
|
+
"""A positive request, never longer than a minute. A bad request uses the default."""
|
|
278
|
+
try:
|
|
279
|
+
number = float(seconds)
|
|
280
|
+
except (TypeError, ValueError):
|
|
281
|
+
return DEFAULT_SECONDS
|
|
282
|
+
if not math.isfinite(number) or number <= 0:
|
|
283
|
+
return DEFAULT_SECONDS
|
|
284
|
+
return min(HARD_CEILING, number)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _ask(think: Think, prompt: str, deadline: float) -> tuple[str, bool]:
|
|
288
|
+
"""One model call. The second value is false when the clock won and the loop must stop.
|
|
289
|
+
|
|
290
|
+
The worker is a daemon so a model that keeps talking after the deadline
|
|
291
|
+
cannot hold the process open. That reply is never read.
|
|
292
|
+
"""
|
|
293
|
+
remaining = deadline - time.monotonic()
|
|
294
|
+
if remaining <= 0:
|
|
295
|
+
return "", False
|
|
296
|
+
box: list[str] = []
|
|
297
|
+
|
|
298
|
+
def run() -> None:
|
|
299
|
+
try:
|
|
300
|
+
text = think(prompt)
|
|
301
|
+
except Exception: # noqa: BLE001 — a broken call is a refused turn, not a crash
|
|
302
|
+
return
|
|
303
|
+
if isinstance(text, str):
|
|
304
|
+
box.append(text)
|
|
305
|
+
|
|
306
|
+
worker = threading.Thread(target=run, daemon=True)
|
|
307
|
+
worker.start()
|
|
308
|
+
worker.join(remaining)
|
|
309
|
+
if worker.is_alive():
|
|
310
|
+
return "", False
|
|
311
|
+
return (box[0] if box else ""), True
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _fill(
|
|
315
|
+
template: str,
|
|
316
|
+
question: str,
|
|
317
|
+
board: str,
|
|
318
|
+
*,
|
|
319
|
+
round_idx: int = 1,
|
|
320
|
+
n_rounds: int = MAX_ROUNDS,
|
|
321
|
+
seat_idx: int = 1,
|
|
322
|
+
n_seats: int = MAX_SEATS,
|
|
323
|
+
attacks: list[str] | None = None,
|
|
324
|
+
names: list[str] | None = None,
|
|
325
|
+
) -> str:
|
|
326
|
+
active = list(dict.fromkeys(attacks or []))
|
|
327
|
+
unused = [a for a in ATTACKS if a not in active]
|
|
328
|
+
return (
|
|
329
|
+
template.replace("@@QUESTION@@", question)
|
|
330
|
+
.replace("@@BOARD@@", board)
|
|
331
|
+
.replace("@@ROUND@@", str(round_idx))
|
|
332
|
+
.replace("@@ROUNDS@@", str(n_rounds))
|
|
333
|
+
.replace("@@SEAT@@", str(seat_idx))
|
|
334
|
+
.replace("@@SEATS@@", str(n_seats))
|
|
335
|
+
.replace("@@RULES@@", ", ".join(RULES))
|
|
336
|
+
.replace("@@ATTACKS@@", ", ".join(ATTACKS))
|
|
337
|
+
.replace("@@ACTIVE_ATTACKS@@", ", ".join(active) or "none yet")
|
|
338
|
+
.replace("@@UNUSED_ATTACKS@@", ", ".join(unused) or "all attacks active")
|
|
339
|
+
.replace("@@NAMES@@", ", ".join(names or []) or "loaded book")
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _proposal(text: str) -> tuple[dict[str, Any], str]:
|
|
344
|
+
obj = _object(text)
|
|
345
|
+
if obj is None:
|
|
346
|
+
return {"rule": ""}, ""
|
|
347
|
+
if "bakeoff" in {str(obj.get("tool") or ""), str(obj.get("rule") or "")}:
|
|
348
|
+
return {"rule": ""}, ""
|
|
349
|
+
rule = obj.get("rule")
|
|
350
|
+
if not isinstance(rule, str):
|
|
351
|
+
return {"rule": ""}, ""
|
|
352
|
+
out: dict[str, Any] = {"rule": rule}
|
|
353
|
+
if "lookback" in obj and obj["lookback"] is not None:
|
|
354
|
+
out["lookback"] = obj["lookback"]
|
|
355
|
+
if isinstance(obj.get("names"), list):
|
|
356
|
+
out["names"] = [str(name) for name in obj["names"][:12]]
|
|
357
|
+
why = str(obj.get("why") or "").strip()[:80]
|
|
358
|
+
return out, why
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def _attack(text: str) -> tuple[str | None, str]:
|
|
362
|
+
obj = _object(text)
|
|
363
|
+
if obj is None:
|
|
364
|
+
return None, ""
|
|
365
|
+
attack = str(obj.get("attack") or "").strip().lower().replace("-", "_").replace(" ", "_")
|
|
366
|
+
if attack not in ATTACKS:
|
|
367
|
+
return None, ""
|
|
368
|
+
why = str(obj.get("why") or "").strip()[:80]
|
|
369
|
+
return attack, why
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def _object(text: str) -> dict[str, Any] | None:
|
|
373
|
+
start = text.find("{")
|
|
374
|
+
end = text.rfind("}")
|
|
375
|
+
if start < 0 or end <= start:
|
|
376
|
+
return None
|
|
377
|
+
try:
|
|
378
|
+
obj = json.loads(text[start : end + 1])
|
|
379
|
+
except json.JSONDecodeError:
|
|
380
|
+
return None
|
|
381
|
+
if not isinstance(obj, dict):
|
|
382
|
+
return None
|
|
383
|
+
nested = obj.get("args")
|
|
384
|
+
if isinstance(nested, dict) and "rule" not in obj and "attack" not in obj:
|
|
385
|
+
obj = nested
|
|
386
|
+
return {str(key): value for key, value in obj.items() if str(key) not in _DROPPED}
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _verdict(figures: dict[str, Any]) -> str:
|
|
390
|
+
survivor = figures.get("survivor")
|
|
391
|
+
if not survivor:
|
|
392
|
+
return "Referee. No proposal survived."
|
|
393
|
+
return (
|
|
394
|
+
f"Referee. {figures.get('n_trials')} candidates across {figures.get('n_passes', MAX_ROUNDS)} passes, "
|
|
395
|
+
f"source bakeoff. Survivor {survivor} (in-sample Sharpe {figures.get('survivor_sharpe')}, "
|
|
396
|
+
f"post-attack standing {figures.get('attacked_sharpe')}, "
|
|
397
|
+
f"deflated Sharpe {figures.get('deflated_sharpe')}). Verdict {figures.get('verdict')}."
|
|
398
|
+
)
|
|
@@ -216,7 +216,7 @@ def _books(data: dict[str, list[float]], question: str = "") -> list[dict[str, A
|
|
|
216
216
|
return []
|
|
217
217
|
if aligned.ndim != 2 or aligned.shape[1] < 2 or aligned.shape[0] < 8:
|
|
218
218
|
return []
|
|
219
|
-
built: dict[str,
|
|
219
|
+
built: dict[str, Any] = {}
|
|
220
220
|
for method in ("hrp", "risk_parity"):
|
|
221
221
|
try:
|
|
222
222
|
result = build_portfolio(data, method=method)
|
|
@@ -559,7 +559,7 @@ def _bakeoff(raw: Any, data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
|
559
559
|
if not isinstance(raw, dict):
|
|
560
560
|
return None
|
|
561
561
|
curves = []
|
|
562
|
-
for row in raw.get("curves") or []:
|
|
562
|
+
for idx, row in enumerate(raw.get("curves") or []):
|
|
563
563
|
if not isinstance(row, dict):
|
|
564
564
|
continue
|
|
565
565
|
growth = row.get("growth")
|
|
@@ -570,10 +570,14 @@ def _bakeoff(raw: Any, data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
|
570
570
|
stats = _stats([float(x) for x in rets])
|
|
571
571
|
curves.append(
|
|
572
572
|
{
|
|
573
|
+
"round": int(row.get("round") or (idx // 2 + 1)),
|
|
574
|
+
"seat": int(row.get("seat") or (idx % 2 + 1)),
|
|
573
575
|
"label": str(row.get("label") or ""),
|
|
574
576
|
"growth": growth,
|
|
575
577
|
"sharpe": row.get("sharpe"),
|
|
576
578
|
"standing": row.get("standing"),
|
|
579
|
+
"worst_attack": row.get("worst_attack") or "in_sample",
|
|
580
|
+
"attack_scores": row.get("attack_scores") or {},
|
|
577
581
|
"annualized_sharpe": stats.get("sharpe_annualized"),
|
|
578
582
|
"total_return_pct": stats.get("total_return_pct"),
|
|
579
583
|
"max_drawdown_pct": stats.get("max_drawdown_pct"),
|
|
@@ -583,6 +587,7 @@ def _bakeoff(raw: Any, data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
|
583
587
|
trials = [str(line) for line in (raw.get("trials") or [])]
|
|
584
588
|
if not raw.get("survivor") and not trials and not curves:
|
|
585
589
|
return None
|
|
590
|
+
passes = [p for p in (raw.get("passes") or []) if isinstance(p, dict)]
|
|
586
591
|
return {
|
|
587
592
|
"survivor": raw.get("survivor"),
|
|
588
593
|
"survivor_sharpe": raw.get("survivor_sharpe"),
|
|
@@ -590,33 +595,56 @@ def _bakeoff(raw: Any, data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
|
590
595
|
"deflated_sharpe": raw.get("deflated_sharpe"),
|
|
591
596
|
"verdict": raw.get("verdict"),
|
|
592
597
|
"n_trials": raw.get("n_trials"),
|
|
598
|
+
"n_passes": raw.get("n_passes") or max(len(passes), 4),
|
|
593
599
|
"attacks": [str(attack) for attack in (raw.get("attacks") or [])],
|
|
594
600
|
"trials": trials,
|
|
595
|
-
"
|
|
601
|
+
"passes": passes,
|
|
602
|
+
"curves": curves[:8],
|
|
603
|
+
"survivor_weights": raw.get("survivor_weights") or {},
|
|
596
604
|
}
|
|
597
605
|
|
|
598
606
|
|
|
599
607
|
def _default_bakeoff(data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
600
|
-
"""Run the referee across all
|
|
608
|
+
"""Run the 4-pass referee tournament across all eight rules when the session did not run one."""
|
|
601
609
|
try:
|
|
602
|
-
from ..core.bakeoff import
|
|
610
|
+
from ..core.bakeoff import referee, trial_paths
|
|
603
611
|
except Exception: # noqa: BLE001
|
|
604
612
|
return None
|
|
605
613
|
submissions: list[dict[str, Any]] = [
|
|
606
|
-
{"rule": "hrp"},
|
|
607
|
-
{"rule": "risk_parity"},
|
|
608
|
-
{"rule": "inverse_vol"},
|
|
609
|
-
{"rule": "
|
|
610
|
-
{"rule": "
|
|
611
|
-
{"rule": "
|
|
614
|
+
{"rule": "hrp", "_round": 1, "_seat": 1},
|
|
615
|
+
{"rule": "risk_parity", "_round": 1, "_seat": 2},
|
|
616
|
+
{"rule": "inverse_vol", "_round": 2, "_seat": 1},
|
|
617
|
+
{"rule": "var_sorted", "_round": 2, "_seat": 2},
|
|
618
|
+
{"rule": "low_vol", "_round": 3, "_seat": 1},
|
|
619
|
+
{"rule": "equal", "_round": 3, "_seat": 2},
|
|
620
|
+
{"rule": "momentum", "lookback": 20, "_round": 4, "_seat": 1},
|
|
621
|
+
{"rule": "reversion", "lookback": 20, "_round": 4, "_seat": 2},
|
|
612
622
|
]
|
|
623
|
+
pass_attacks = ["last_half", "high_vol", "tail_stress", "rate_shock"]
|
|
613
624
|
try:
|
|
614
|
-
|
|
615
|
-
|
|
625
|
+
passes: list[dict[str, Any]] = []
|
|
626
|
+
for p_idx, atk in enumerate(pass_attacks, start=1):
|
|
627
|
+
sub_slice = submissions[: p_idx * 2]
|
|
628
|
+
atk_slice = pass_attacks[:p_idx]
|
|
629
|
+
step = referee(data, sub_slice, atk_slice)
|
|
630
|
+
passes.append(
|
|
631
|
+
{
|
|
632
|
+
"pass": p_idx,
|
|
633
|
+
"attack": atk,
|
|
634
|
+
"leader": step.get("survivor"),
|
|
635
|
+
"leader_sharpe": step.get("survivor_sharpe"),
|
|
636
|
+
"leader_standing": step.get("attacked_sharpe"),
|
|
637
|
+
"n_trials": len(sub_slice),
|
|
638
|
+
}
|
|
639
|
+
)
|
|
640
|
+
figures = referee(data, submissions, pass_attacks)
|
|
641
|
+
curves = trial_paths(data, submissions, pass_attacks)
|
|
616
642
|
except Exception: # noqa: BLE001
|
|
617
643
|
return None
|
|
618
644
|
if not figures.get("survivor") and not curves:
|
|
619
645
|
return None
|
|
646
|
+
survivor_label = str(figures.get("survivor") or "")
|
|
647
|
+
survivor_curve = next((c for c in curves if c.get("label") == survivor_label), None)
|
|
620
648
|
return {
|
|
621
649
|
"survivor": figures.get("survivor"),
|
|
622
650
|
"survivor_sharpe": figures.get("survivor_sharpe"),
|
|
@@ -624,9 +652,12 @@ def _default_bakeoff(data: dict[str, list[float]]) -> dict[str, Any] | None:
|
|
|
624
652
|
"deflated_sharpe": figures.get("deflated_sharpe"),
|
|
625
653
|
"verdict": figures.get("verdict"),
|
|
626
654
|
"n_trials": figures.get("n_trials"),
|
|
655
|
+
"n_passes": 4,
|
|
627
656
|
"attacks": list(figures.get("attacks") or []),
|
|
628
657
|
"trials": list(figures.get("trials") or []),
|
|
658
|
+
"passes": passes,
|
|
629
659
|
"curves": curves,
|
|
660
|
+
"survivor_weights": (survivor_curve or {}).get("weights") or {},
|
|
630
661
|
}
|
|
631
662
|
|
|
632
663
|
|
|
@@ -792,7 +792,7 @@ def _bakeoff(
|
|
|
792
792
|
fig = plt.figure(figsize=(8.5, 11))
|
|
793
793
|
_chrome(
|
|
794
794
|
fig,
|
|
795
|
-
"Strategy Bakeoff &
|
|
795
|
+
f"Multi-Pass GAN Strategy Bakeoff & Referee Tournament ({bakeoff.get('n_passes') or 4} Passes)",
|
|
796
796
|
stamp,
|
|
797
797
|
stem,
|
|
798
798
|
_question(trace),
|
|
@@ -800,68 +800,88 @@ def _bakeoff(
|
|
|
800
800
|
banner = (
|
|
801
801
|
f"Survivor: {bakeoff.get('survivor')} | "
|
|
802
802
|
f"In-Sample Sharpe: {bakeoff.get('survivor_sharpe')} | "
|
|
803
|
-
f"
|
|
803
|
+
f"Post-Attack Standing: {bakeoff.get('attacked_sharpe')} | "
|
|
804
804
|
f"Deflated Sharpe: {bakeoff.get('deflated_sharpe')} ({bakeoff.get('verdict')})\n"
|
|
805
|
-
f"
|
|
805
|
+
f"Passes: {bakeoff.get('n_passes') or 4} | "
|
|
806
|
+
f"Candidates: {bakeoff.get('n_trials')} | "
|
|
806
807
|
f"Adversarial Attacks: {', '.join(bakeoff.get('attacks') or []) or 'none'}"
|
|
807
808
|
)
|
|
808
809
|
fig.text(0.075, 0.895, banner, fontsize=8.0, fontweight="bold", color="#0F172A", va="top")
|
|
809
810
|
|
|
810
811
|
curves = list(bakeoff.get("curves") or [])
|
|
811
812
|
if curves:
|
|
812
|
-
|
|
813
|
-
|
|
813
|
+
ax1 = fig.add_axes((0.09, 0.48, 0.44, 0.34))
|
|
814
|
+
ax1.set_title("Cumulative Wealth Across Passes", loc="left", fontsize=9.5)
|
|
814
815
|
survivor = str(bakeoff.get("survivor") or "")
|
|
815
816
|
for index, curve in enumerate(curves):
|
|
816
817
|
label = str(curve.get("label") or "")
|
|
817
818
|
is_win = label == survivor
|
|
818
|
-
|
|
819
|
+
ax1.plot(
|
|
819
820
|
curve.get("growth") or [],
|
|
820
821
|
color="#0F172A" if is_win else _COLORS[index % len(_COLORS)],
|
|
821
|
-
lw=2.
|
|
822
|
-
alpha=1.0 if is_win else 0.
|
|
823
|
-
label=f"{label}
|
|
822
|
+
lw=2.2 if is_win else 1.2,
|
|
823
|
+
alpha=1.0 if is_win else 0.84,
|
|
824
|
+
label=f"{label}*" if is_win else label,
|
|
824
825
|
)
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
826
|
+
ax1.axhline(1.0, color="#64748B", lw=0.7, ls="--")
|
|
827
|
+
ax1.set_xlabel("trading observation")
|
|
828
|
+
ax1.set_ylabel("growth of 1")
|
|
829
|
+
ax1.legend(loc="upper left", ncol=2, fontsize=6.8)
|
|
830
|
+
|
|
831
|
+
ax2 = fig.add_axes((0.62, 0.48, 0.30, 0.34))
|
|
832
|
+
labels = [str(c.get("label") or "") for c in curves][::-1]
|
|
833
|
+
in_sh = [float(c.get("sharpe") or 0.0) for c in curves][::-1]
|
|
834
|
+
st_sh = [
|
|
835
|
+
float(c.get("standing") if c.get("standing") is not None else (c.get("sharpe") or 0.0))
|
|
836
|
+
for c in curves
|
|
837
|
+
][::-1]
|
|
838
|
+
y_pos = np.arange(len(labels))
|
|
839
|
+
ax2.barh(y_pos + 0.17, in_sh, height=0.32, color="#0072B2", label="In-Sample")
|
|
840
|
+
ax2.barh(y_pos - 0.17, st_sh, height=0.32, color="#D95F02", label="Post-Attack")
|
|
841
|
+
ax2.axvline(0.0, color="#475569", lw=0.8)
|
|
842
|
+
ax2.set_yticks(y_pos, labels, fontsize=7.2)
|
|
843
|
+
ax2.set_title("In-Sample vs. Attacked Sharpe", loc="left", fontsize=9.5)
|
|
844
|
+
ax2.set_xlabel("period Sharpe")
|
|
845
|
+
ax2.legend(loc="lower right", fontsize=7.0)
|
|
829
846
|
|
|
830
847
|
tbl_rows: list[list[str]] = []
|
|
831
848
|
survivor = str(bakeoff.get("survivor") or "")
|
|
832
849
|
for idx, curve in enumerate(curves, start=1):
|
|
833
850
|
label = str(curve.get("label") or "")
|
|
834
851
|
status = "SURVIVOR" if label == survivor else "eliminated"
|
|
852
|
+
p_num = curve.get("round") or ((idx - 1) // 2 + 1)
|
|
835
853
|
tbl_rows.append(
|
|
836
854
|
[
|
|
837
|
-
f"{
|
|
855
|
+
f"P{p_num}",
|
|
838
856
|
label,
|
|
839
857
|
_shown(curve.get("sharpe")),
|
|
858
|
+
_shown(curve.get("standing")),
|
|
859
|
+
str(curve.get("worst_attack") or "in_sample"),
|
|
840
860
|
_shown(curve.get("annualized_sharpe")),
|
|
841
861
|
_fmt_pct(curve.get("total_return_pct")),
|
|
842
|
-
_fmt_pct(curve.get("volatility_annualized_pct")),
|
|
843
862
|
_fmt_pct(curve.get("max_drawdown_pct")),
|
|
844
863
|
status,
|
|
845
864
|
]
|
|
846
865
|
)
|
|
847
|
-
ax_tbl = fig.add_axes((0.075, 0.
|
|
866
|
+
ax_tbl = fig.add_axes((0.075, 0.07, 0.85, 0.34))
|
|
848
867
|
ax_tbl.axis("off")
|
|
849
|
-
ax_tbl.set_title("Referee Candidate Scorecard", loc="left", fontsize=9.5)
|
|
868
|
+
ax_tbl.set_title("Multi-Pass Referee Candidate Scorecard", loc="left", fontsize=9.5)
|
|
850
869
|
_styled_table(
|
|
851
870
|
ax_tbl,
|
|
852
871
|
[
|
|
853
|
-
"
|
|
872
|
+
"Pass",
|
|
854
873
|
"Candidate Rule",
|
|
855
|
-
"Period
|
|
856
|
-
"
|
|
874
|
+
"Period SR",
|
|
875
|
+
"Attacked SR",
|
|
876
|
+
"Worst Attack",
|
|
877
|
+
"Ann SR",
|
|
857
878
|
"Total Ret",
|
|
858
|
-
"Ann Vol",
|
|
859
879
|
"Max DD",
|
|
860
|
-
"
|
|
880
|
+
"Status",
|
|
861
881
|
],
|
|
862
882
|
tbl_rows,
|
|
863
|
-
[0.06, 0.
|
|
864
|
-
fontsize=7.
|
|
883
|
+
[0.06, 0.17, 0.10, 0.11, 0.14, 0.10, 0.10, 0.10, 0.12],
|
|
884
|
+
fontsize=7.3,
|
|
865
885
|
)
|
|
866
886
|
else:
|
|
867
887
|
trials = list(bakeoff.get("trials") or [])
|