Skip to content

Commit 9209435

Browse files
committed
Make TACO rebound plugin notification-only
1 parent 2800911 commit 9209435

8 files changed

Lines changed: 143 additions & 104 deletions

File tree

README.md

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@ send notifications; plugin research and signal generation live here.
2626

2727
- `crisis_response_shadow`: black-swan defense observer for leveraged US equity
2828
strategies. It writes shadow-mode artifacts and never calls brokers.
29-
- `taco_rebound_shadow`: research-only rebound-budget observer. The generic
30-
runner keeps it gated until promotion criteria are met.
29+
- `taco_rebound_shadow`: TQQQ-only event-rebound context notifier. It writes
30+
manual-review artifacts and never recommends position size or changes
31+
allocations.
32+
- TACO panic-rebound research and portfolio/overlay backtests also live here;
33+
snapshot pipeline repositories keep only compatibility entrypoints.
3134

3235
## Usage
3336

@@ -46,6 +49,16 @@ qsp-build-crisis-response-shadow-signal \
4649
--output-dir data/output/tqqq_growth_income/plugins/crisis_response_shadow
4750
```
4851

52+
Build a TACO rebound notification artifact directly from a local price-history CSV:
53+
54+
```bash
55+
qsp-build-taco-rebound-shadow-signal \
56+
--prices data/input/price_history.csv \
57+
--event-set geopolitical-deescalation \
58+
--as-of 2026-05-22 \
59+
--output-dir data/output/tqqq_growth_income/plugins/taco_rebound_shadow
60+
```
61+
4962
Generated artifacts include `latest_signal.json`, dated JSON, dated CSV, and
5063
an evidence CSV. `latest_signal.json` is the file platform runtimes mount via
5164
`*_STRATEGY_PLUGIN_MOUNTS_JSON`.

README.zh-CN.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ Brokers、Schwab、LongBridge、Firstrade 等平台仓库只负责加载 artifac
2424
## 插件
2525

2626
- `crisis_response_shadow`:面向杠杆美股策略的黑天鹅防守观察插件。它只写入 shadow-mode artifact,不调用券商接口。
27-
- `taco_rebound_shadow`:研究阶段的反弹预算观察插件。通用 runner 会保持 gating,直到后续验证和晋级标准满足。
27+
- `taco_rebound_shadow`:仅适用于 TQQQ 的事件反弹上下文通知插件。它只写入人工复核 artifact,不给仓位大小建议,也不改动配置或账户分配。
28+
- TACO panic-rebound 研究、组合回测和 overlay 对比也归属本仓库;snapshot pipeline 仓库只保留兼容入口。
2829

2930
## 使用方式
3031

@@ -43,6 +44,16 @@ qsp-build-crisis-response-shadow-signal \
4344
--output-dir data/output/tqqq_growth_income/plugins/crisis_response_shadow
4445
```
4546

47+
从本地价格历史 CSV 直接生成 TACO 反弹通知 artifact:
48+
49+
```bash
50+
qsp-build-taco-rebound-shadow-signal \
51+
--prices data/input/price_history.csv \
52+
--event-set geopolitical-deescalation \
53+
--as-of 2026-05-22 \
54+
--output-dir data/output/tqqq_growth_income/plugins/taco_rebound_shadow
55+
```
56+
4657
输出包括 `latest_signal.json`、按日期归档的 JSON、按日期归档的 CSV,以及 evidence CSV。平台运行时通过 `*_STRATEGY_PLUGIN_MOUNTS_JSON` 挂载的就是 `latest_signal.json`
4758

4859
## 本地检查

docs/examples/strategy_plugins.example.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,18 @@ rate_symbols = ["IEF", "TLT"]
1919

2020
[strategy_plugins.outputs]
2121
output_dir = "data/output/tqqq_growth_income/plugins/crisis_response_shadow"
22+
23+
[[strategy_plugins]]
24+
strategy = "tqqq_growth_income"
25+
plugin = "taco_rebound_shadow"
26+
enabled = true
27+
# Notification-only TACO context. The artifact may trigger manual-review alerts,
28+
# but it never recommends position size or mutates allocations.
29+
30+
[strategy_plugins.inputs]
31+
prices = "data/output/taco_rebound_shadow/input/price_history.csv"
32+
event_set = "geopolitical-deescalation"
33+
start_date = "2026-01-01"
34+
35+
[strategy_plugins.outputs]
36+
output_dir = "data/output/tqqq_growth_income/plugins/taco_rebound_shadow"

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "quant-strategy-plugins"
7-
version = "0.1.0"
7+
version = "0.1.1"
88
description = "Open sidecar strategy plugins that emit QuantPlatformKit-compatible signal artifacts."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/quant_strategy_plugins/strategy_plugin_runner.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,9 @@
2828
SUPPORTED_PLUGIN_MODES = (SHADOW_MODE,)
2929
PLUGIN_COMPATIBLE_STRATEGIES: dict[str, tuple[str, ...]] = {
3030
PLUGIN_CRISIS_RESPONSE_SHADOW: ("tqqq_growth_income", "soxl_soxx_trend_income"),
31+
PLUGIN_TACO_REBOUND_SHADOW: ("tqqq_growth_income",),
3132
}
32-
PLUGIN_RESEARCH_ONLY_REASONS: dict[str, str] = {
33-
PLUGIN_TACO_REBOUND_SHADOW: (
34-
"MAGS TACO rebound is research-only for now; use the standalone research builder/backtests, "
35-
"and promote a separate TQQQ TACO overlay plugin only after its own validation."
36-
),
37-
}
33+
PLUGIN_RESEARCH_ONLY_REASONS: dict[str, str] = {}
3834

3935

4036
@dataclass(frozen=True)
@@ -230,10 +226,6 @@ def _build_taco_rebound_kwargs(plugin_config: Mapping[str, Any]) -> dict[str, An
230226
"attack_symbol",
231227
}
232228
numeric_keys = {
233-
"tariff_softening_sleeve",
234-
"geopolitical_deescalation_sleeve",
235-
"shock_sleeve",
236-
"max_sleeve",
237229
"crisis_guard_drawdown",
238230
}
239231
integer_keys = {

src/quant_strategy_plugins/taco_rebound_shadow_plugin.py

Lines changed: 43 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,24 @@
2222
)
2323
from .taco_panic_rebound_research import (
2424
DEFAULT_EVENT_SET,
25-
EVENT_KIND_SHOCK,
2625
EVENT_KIND_SOFTENING,
2726
TRADE_WAR_EVENT_SETS,
2827
TradeWarEvent,
2928
resolve_trade_war_event_set,
3029
)
3130
from .yfinance_prices import download_price_history
3231

33-
SCHEMA_VERSION = "taco_rebound_shadow.v1"
32+
SCHEMA_VERSION = "taco_rebound_shadow.v2"
3433
SHADOW_MODE = "shadow"
3534
TACO_REBOUND_PROFILE = "taco_rebound_shadow"
3635
ROUTE_TACO_REBOUND = "taco_rebound"
37-
ACTION_INCREASE_REBOUND_BUDGET = "increase_rebound_budget"
36+
ACTION_NOTIFY_MANUAL_REVIEW = "notify_manual_review"
3837
ACTION_WATCH_ONLY = "watch_only"
3938
ACTION_NO_ACTION = "no_action"
40-
ACTION_BLOCKED = "blocked"
4139
DEFAULT_OUTPUT_DIR = "data/output/taco_rebound_shadow"
4240
DEFAULT_START_DATE = "2018-01-01"
4341
DEFAULT_MAX_PRICE_AGE_DAYS = 4
4442
DEFAULT_ACTIVE_SIGNAL_DAYS = 10
45-
DEFAULT_TARIFF_SOFTENING_SLEEVE = 0.05
46-
DEFAULT_GEOPOLITICAL_DEESCALATION_SLEEVE = 0.10
47-
DEFAULT_SHOCK_SLEEVE = 0.0
48-
DEFAULT_MAX_SLEEVE = 0.10
4943
HARD_DEFENSE_BREAK_BEAR_REGIONS = frozenset({"iran_middle_east"})
5044

5145

@@ -57,29 +51,18 @@ def _next_index_date(index: pd.DatetimeIndex, raw_date: str | pd.Timestamp) -> p
5751
return pd.Timestamp(candidates[0]).normalize()
5852

5953

60-
def _event_sleeve(
61-
event: TradeWarEvent,
62-
*,
63-
tariff_softening_sleeve: float,
64-
geopolitical_deescalation_sleeve: float,
65-
shock_sleeve: float,
66-
max_sleeve: float,
67-
) -> float:
68-
if event.kind == EVENT_KIND_SHOCK:
69-
sleeve = float(shock_sleeve)
70-
elif str(event.region).strip().lower() == "iran_middle_east":
71-
sleeve = float(geopolitical_deescalation_sleeve)
72-
else:
73-
sleeve = float(tariff_softening_sleeve)
74-
return max(0.0, min(float(max_sleeve), sleeve))
75-
76-
7754
def _event_allows_hard_defense(event: TradeWarEvent | None) -> bool:
7855
if event is None or event.kind != EVENT_KIND_SOFTENING:
7956
return False
8057
return str(event.region).strip().lower() in HARD_DEFENSE_BREAK_BEAR_REGIONS
8158

8259

60+
def _event_notice_priority(event: TradeWarEvent) -> tuple[int, int, str]:
61+
kind_priority = 1 if event.kind == EVENT_KIND_SOFTENING else 0
62+
region_priority = 1 if str(event.region).strip().lower() in HARD_DEFENSE_BREAK_BEAR_REGIONS else 0
63+
return kind_priority, region_priority, str(event.event_date)
64+
65+
8366
def _active_recognized_events(
8467
events: Sequence[TradeWarEvent],
8568
*,
@@ -107,10 +90,6 @@ def build_taco_rebound_shadow_signal(
10790
benchmark_symbol: str = DEFAULT_BENCHMARK_SYMBOL,
10891
attack_symbol: str = DEFAULT_ATTACK_SYMBOL,
10992
active_signal_days: int = DEFAULT_ACTIVE_SIGNAL_DAYS,
110-
tariff_softening_sleeve: float = DEFAULT_TARIFF_SOFTENING_SLEEVE,
111-
geopolitical_deescalation_sleeve: float = DEFAULT_GEOPOLITICAL_DEESCALATION_SLEEVE,
112-
shock_sleeve: float = DEFAULT_SHOCK_SLEEVE,
113-
max_sleeve: float = DEFAULT_MAX_SLEEVE,
11493
suppress_when_price_crisis_guard_active: bool = True,
11594
crisis_guard_drawdown: float = DEFAULT_PRICE_CRISIS_GUARD_DRAWDOWN,
11695
crisis_guard_ma_days: int = DEFAULT_PRICE_CRISIS_GUARD_MA_DAYS,
@@ -171,43 +150,40 @@ def build_taco_rebound_shadow_signal(
171150

172151
selected_event: TradeWarEvent | None = None
173152
selected_event_signal_date: pd.Timestamp | None = None
174-
sleeve = 0.0
175153
for event, event_signal_date in active_events:
176-
candidate_sleeve = _event_sleeve(
177-
event,
178-
tariff_softening_sleeve=float(tariff_softening_sleeve),
179-
geopolitical_deescalation_sleeve=float(geopolitical_deescalation_sleeve),
180-
shock_sleeve=float(shock_sleeve),
181-
max_sleeve=float(max_sleeve),
182-
)
183-
if candidate_sleeve >= sleeve:
154+
if selected_event is None or _event_notice_priority(event) >= _event_notice_priority(selected_event):
184155
selected_event = event
185156
selected_event_signal_date = event_signal_date
186-
sleeve = candidate_sleeve
187157

188-
canonical_route = ROUTE_TACO_REBOUND if sleeve > 0.0 else "no_action"
189-
suggested_action = ACTION_INCREASE_REBOUND_BUDGET if sleeve > 0.0 else ACTION_NO_ACTION
190-
would_trade_if_enabled = sleeve > 0.0
191-
allow_hard_defense = bool(would_trade_if_enabled and _event_allows_hard_defense(selected_event))
158+
rebound_context_active = bool(
159+
selected_event is not None and selected_event.kind == EVENT_KIND_SOFTENING and not crisis_guard_active
160+
)
161+
manual_review_required = rebound_context_active
162+
canonical_route = ROUTE_TACO_REBOUND if manual_review_required else "no_action"
163+
suggested_action = ACTION_NOTIFY_MANUAL_REVIEW if manual_review_required else ACTION_NO_ACTION
164+
would_trade_if_enabled = False
165+
event_rebound_break_bear = bool(manual_review_required and _event_allows_hard_defense(selected_event))
192166
suppression_reason = ""
193-
if active_events and sleeve <= 0.0:
167+
notification_reason = "event rebound context active" if manual_review_required else ""
168+
if active_events and not manual_review_required:
194169
suggested_action = ACTION_WATCH_ONLY
195-
suppression_reason = "active event has zero configured sleeve"
196-
allow_hard_defense = False
170+
suppression_reason = "active event is not a softening/de-escalation rebound context"
197171
if crisis_guard_active:
198172
canonical_route = "no_action"
199-
suggested_action = ACTION_BLOCKED
200-
would_trade_if_enabled = False
201-
sleeve = 0.0
202-
allow_hard_defense = False
173+
suggested_action = ACTION_WATCH_ONLY
174+
manual_review_required = False
175+
rebound_context_active = False
176+
event_rebound_break_bear = False
203177
suppression_reason = "price crisis guard active"
178+
notification_reason = ""
204179
if kill_reasons:
205180
canonical_route = "no_action"
206-
suggested_action = ACTION_BLOCKED
207-
would_trade_if_enabled = False
208-
sleeve = 0.0
209-
allow_hard_defense = False
181+
suggested_action = ACTION_WATCH_ONLY
182+
manual_review_required = False
183+
rebound_context_active = False
184+
event_rebound_break_bear = False
210185
suppression_reason = "; ".join(kill_reasons)
186+
notification_reason = ""
211187

212188
generated_at = datetime.now(timezone.utc).isoformat()
213189
payload = {
@@ -217,9 +193,10 @@ def build_taco_rebound_shadow_signal(
217193
"profile": TACO_REBOUND_PROFILE,
218194
"canonical_route": canonical_route,
219195
"suggested_action": suggested_action,
220-
"sleeve_suggestion": sleeve if sleeve > 0.0 else None,
221-
"allow_hard_defense": allow_hard_defense,
222-
"event_rebound_break_bear": allow_hard_defense,
196+
"manual_review_required": manual_review_required,
197+
"notification_reason": notification_reason,
198+
"rebound_context_active": rebound_context_active,
199+
"event_rebound_break_bear": event_rebound_break_bear,
223200
"would_trade_if_enabled": would_trade_if_enabled,
224201
"price_stress_scan_active": scan_active,
225202
"price_crisis_guard_active": crisis_guard_active,
@@ -255,10 +232,12 @@ def build_taco_rebound_shadow_signal(
255232
"broker_order_allowed": False,
256233
"live_allocation_mutation_allowed": False,
257234
"log_namespace": TACO_REBOUND_PROFILE,
258-
"notification_profile": "shadow_only",
259-
"intended_strategy_role": "left_side_rebound_budget_modifier",
235+
"notification_profile": "manual_review_only",
236+
"intended_strategy_role": "event_rebound_notification",
260237
"selection_allowed": False,
261-
"hard_defense_override_signal_allowed": allow_hard_defense,
238+
"position_sizing_allowed": False,
239+
"allocation_recommendation_allowed": False,
240+
"hard_defense_override_signal_allowed": False,
262241
},
263242
"generated_at": generated_at,
264243
}
@@ -285,8 +264,9 @@ def write_taco_rebound_shadow_outputs(payload: Mapping[str, Any], output_dir: st
285264
"as_of": payload.get("as_of"),
286265
"canonical_route": payload.get("canonical_route"),
287266
"suggested_action": payload.get("suggested_action"),
288-
"sleeve_suggestion": payload.get("sleeve_suggestion"),
289-
"allow_hard_defense": payload.get("allow_hard_defense"),
267+
"manual_review_required": payload.get("manual_review_required"),
268+
"notification_reason": payload.get("notification_reason"),
269+
"rebound_context_active": payload.get("rebound_context_active"),
290270
"event_rebound_break_bear": payload.get("event_rebound_break_bear"),
291271
**flatten_for_csv(payload.get("data_freshness", {})),
292272
**flatten_for_csv(payload.get("selected_event") or {}),
@@ -316,14 +296,6 @@ def build_parser() -> argparse.ArgumentParser:
316296
parser.add_argument("--benchmark-symbol", default=DEFAULT_BENCHMARK_SYMBOL)
317297
parser.add_argument("--attack-symbol", default=DEFAULT_ATTACK_SYMBOL)
318298
parser.add_argument("--active-signal-days", type=int, default=DEFAULT_ACTIVE_SIGNAL_DAYS)
319-
parser.add_argument("--tariff-softening-sleeve", type=float, default=DEFAULT_TARIFF_SOFTENING_SLEEVE)
320-
parser.add_argument(
321-
"--geopolitical-deescalation-sleeve",
322-
type=float,
323-
default=DEFAULT_GEOPOLITICAL_DEESCALATION_SLEEVE,
324-
)
325-
parser.add_argument("--shock-sleeve", type=float, default=DEFAULT_SHOCK_SLEEVE)
326-
parser.add_argument("--max-sleeve", type=float, default=DEFAULT_MAX_SLEEVE)
327299
parser.add_argument("--output-dir", default=DEFAULT_OUTPUT_DIR)
328300
return parser
329301

@@ -355,10 +327,6 @@ def main(argv: Sequence[str] | None = None) -> int:
355327
benchmark_symbol=args.benchmark_symbol,
356328
attack_symbol=args.attack_symbol,
357329
active_signal_days=args.active_signal_days,
358-
tariff_softening_sleeve=args.tariff_softening_sleeve,
359-
geopolitical_deescalation_sleeve=args.geopolitical_deescalation_sleeve,
360-
shock_sleeve=args.shock_sleeve,
361-
max_sleeve=args.max_sleeve,
362330
)
363331
paths = write_taco_rebound_shadow_outputs(payload, args.output_dir)
364332
print(
@@ -370,10 +338,7 @@ def main(argv: Sequence[str] | None = None) -> int:
370338

371339

372340
__all__ = [
373-
"ACTION_INCREASE_REBOUND_BUDGET",
374-
"DEFAULT_GEOPOLITICAL_DEESCALATION_SLEEVE",
375-
"DEFAULT_MAX_SLEEVE",
376-
"DEFAULT_TARIFF_SOFTENING_SLEEVE",
341+
"ACTION_NOTIFY_MANUAL_REVIEW",
377342
"ROUTE_TACO_REBOUND",
378343
"SCHEMA_VERSION",
379344
"TACO_REBOUND_PROFILE",

tests/test_strategy_plugin_runner.py

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,43 @@ def test_strategy_plugin_runner_filters_by_strategy(tmp_path) -> None:
302302
).exists()
303303

304304

305-
def test_strategy_plugin_runner_rejects_taco_rebound_runtime_mount_while_research_only(tmp_path) -> None:
305+
def test_strategy_plugin_runner_runs_taco_rebound_notification_mount_for_tqqq(tmp_path) -> None:
306+
prices_path = tmp_path / "taco_prices.csv"
307+
output_dir = tmp_path / STRATEGY_NAME / "plugins" / PLUGIN_TACO_REBOUND_SHADOW
308+
_taco_rebound_prices().to_csv(prices_path, index=False)
309+
config = {
310+
"output_dir": str(tmp_path / "runner"),
311+
"default_mode": "shadow",
312+
"strategy_plugins": [
313+
{
314+
"strategy": STRATEGY_NAME,
315+
"plugin": PLUGIN_TACO_REBOUND_SHADOW,
316+
"enabled": True,
317+
"inputs": {
318+
"prices": str(prices_path),
319+
"event_set": "geopolitical-deescalation",
320+
"as_of": "2026-04-02",
321+
"start_date": "2026-03-20",
322+
},
323+
"outputs": {"output_dir": str(output_dir)},
324+
}
325+
],
326+
}
327+
328+
summary = run_configured_plugins(config)
329+
330+
result = summary["strategy_plugins"][0]
331+
assert result["strategy"] == STRATEGY_NAME
332+
assert result["plugin"] == PLUGIN_TACO_REBOUND_SHADOW
333+
assert result["status"] == "ok"
334+
assert "route=taco_rebound action=notify_manual_review" in result["message"]
335+
latest = json.loads((output_dir / "latest_signal.json").read_text(encoding="utf-8"))
336+
assert latest["manual_review_required"] is True
337+
assert latest["would_trade_if_enabled"] is False
338+
assert "sleeve_suggestion" not in latest
339+
340+
341+
def test_strategy_plugin_runner_rejects_taco_rebound_for_non_tqqq_strategy(tmp_path) -> None:
306342
prices_path = tmp_path / "taco_prices.csv"
307343
output_dir = tmp_path / LEFT_SIDE_STRATEGY_NAME / "plugins" / PLUGIN_TACO_REBOUND_SHADOW
308344
_taco_rebound_prices().to_csv(prices_path, index=False)
@@ -325,13 +361,13 @@ def test_strategy_plugin_runner_rejects_taco_rebound_runtime_mount_while_researc
325361
],
326362
}
327363

328-
with pytest.raises(ValueError, match="research-only"):
364+
with pytest.raises(ValueError, match="strategy-limited"):
329365
run_configured_plugins(config)
330366

331367
assert not (output_dir / "latest_signal.json").exists()
332368

333369

334-
def test_strategy_plugin_runner_can_skip_disabled_research_only_taco_mount(tmp_path) -> None:
370+
def test_strategy_plugin_runner_can_skip_disabled_taco_notification_mount(tmp_path) -> None:
335371
output_dir = tmp_path / LEFT_SIDE_STRATEGY_NAME / "plugins" / PLUGIN_TACO_REBOUND_SHADOW
336372
config = {
337373
"output_dir": str(tmp_path / "runner"),

0 commit comments

Comments
 (0)