Skip to content

Commit d69c9db

Browse files
committed
Add debug config endpoint and webhook decision tracing
1 parent 31529dd commit d69c9db

2 files changed

Lines changed: 166 additions & 0 deletions

File tree

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import json
2+
from pathlib import Path
3+
4+
from fastapi.testclient import TestClient
5+
6+
import webhook_server_fastapi as ws
7+
from src.config.settings import get_settings
8+
9+
10+
class _Exposure:
11+
allowed = True
12+
reason = "ok"
13+
current_exposure = 0.0
14+
proposed_exposure = 1.0
15+
max_exposure = 10.0
16+
17+
18+
class _FakeRiskManager:
19+
def calculate_position_size(self, confidence, base_size=None):
20+
return 1.0
21+
22+
def check_exposure(self, proposed_trade_size, active_trades):
23+
return _Exposure()
24+
25+
def check_direction_limit(self, side, active_trades):
26+
return True, "ok"
27+
28+
29+
class _Trade:
30+
def __init__(self):
31+
self.trade_id = "trade_test_1"
32+
self.entry_price = 0.5
33+
34+
35+
class _FakePositionManager:
36+
def __init__(self):
37+
self.active_trades = {}
38+
39+
def create_trade(self, **kwargs):
40+
return _Trade()
41+
42+
43+
def test_debug_config_endpoint_gate_and_openapi(monkeypatch):
44+
settings = get_settings()
45+
ws.app.router.on_startup.clear()
46+
ws.app.router.on_shutdown.clear()
47+
48+
with TestClient(ws.app) as client:
49+
settings.DEBUG_ENDPOINTS_ENABLED = False
50+
r = client.get("/debug/config")
51+
assert r.status_code == 404
52+
53+
settings.DEBUG_ENDPOINTS_ENABLED = True
54+
r = client.get("/debug/config")
55+
assert r.status_code == 200
56+
payload = r.json()
57+
assert "trading_mode" in payload
58+
assert "active_subscriptions" in payload
59+
60+
openapi = client.get("/openapi.json").json()
61+
assert "/debug/config" in openapi["paths"]
62+
63+
64+
def test_webhook_logs_decision_and_writes_paper_trade(tmp_path, monkeypatch, caplog):
65+
settings = get_settings()
66+
ws.app.router.on_startup.clear()
67+
ws.app.router.on_shutdown.clear()
68+
settings.TRADING_MODE = "paper"
69+
settings.DRY_RUN = False
70+
settings.DEBUG_ENDPOINTS_ENABLED = True
71+
settings.ENABLE_MARKET_QUALITY_GATE = False
72+
settings.ENABLE_PATTERN_GATE = False
73+
settings.WINRATE_UPGRADE_ENABLED = False
74+
settings.MIN_CONFIDENCE = 5
75+
settings.MAX_CONFIDENCE = 5
76+
settings.PAPER_LOG_PATH = str(tmp_path / "paper_trades.jsonl")
77+
settings.SESSION_ID = "test-session"
78+
79+
monkeypatch.setattr(ws, "fetch_market_by_slug", lambda slug: {"id": "m1", "question": "q"})
80+
monkeypatch.setattr(ws, "resolve_up_down_tokens", lambda market: ("T1", "T2"))
81+
monkeypatch.setattr(
82+
ws,
83+
"_get_entry_price_for_trade",
84+
lambda token_id: {
85+
"entry_price": 0.5,
86+
"entry_method": "mid",
87+
"entry_ob_timestamp": "2026-01-01T00:00:00Z",
88+
"best_bid": 0.49,
89+
"best_ask": 0.51,
90+
"price_source": "test",
91+
"retry_used": False,
92+
},
93+
)
94+
monkeypatch.setattr(ws, "get_risk_manager", lambda: _FakeRiskManager())
95+
monkeypatch.setattr(ws, "get_position_manager", lambda: _FakePositionManager())
96+
97+
payload = {
98+
"signal": "BULL",
99+
"signal_id": "sig-1",
100+
"confidence": 5,
101+
"rawConf": 5,
102+
"session": "LONDON",
103+
}
104+
105+
with TestClient(ws.app) as client:
106+
caplog.clear()
107+
with caplog.at_level("INFO"):
108+
res = client.post("/webhook", json=payload)
109+
assert res.status_code == 200
110+
assert res.json().get("ok") is True
111+
112+
assert "SIGNAL DECISION: decision=ENTER" in caplog.text
113+
114+
log_path = Path(settings.PAPER_LOG_PATH)
115+
lines = [ln for ln in log_path.read_text(encoding="utf-8").splitlines() if ln.strip()]
116+
assert len(lines) == 1
117+
trade = json.loads(lines[0])
118+
assert trade["trade_id"] == "trade_test_1"

webhook_server_fastapi.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1519,6 +1519,15 @@ def log_decision(
15191519
logger.warning(f"[{request_id}] Failed to log decision: {e}")
15201520

15211521

1522+
def log_signal_decision(request_id: str, decision: str, reason: str, **fields) -> None:
1523+
"""Emit one concise decision line for accepted webhook signals."""
1524+
parts = [f"[{request_id}] SIGNAL DECISION: decision={decision}", f"reason={reason}"]
1525+
for key, value in fields.items():
1526+
if value is not None:
1527+
parts.append(f"{key}={value}")
1528+
logger.info(" | ".join(parts))
1529+
1530+
15221531
def is_phase2_trade(trade: dict) -> bool:
15231532
"""
15241533
Prüft ob ein Trade ein Phase-2 Trade ist.
@@ -3337,6 +3346,37 @@ async def get_state():
33373346
"mode": get_trading_mode_str(),
33383347
}
33393348

3349+
3350+
@app.get("/debug/config")
3351+
async def debug_config():
3352+
"""Runtime config/state for troubleshooting. Hidden when debug endpoints are disabled."""
3353+
if not getattr(settings, "DEBUG_ENDPOINTS_ENABLED", False):
3354+
raise HTTPException(status_code=404, detail="Not Found")
3355+
3356+
adapter = _get_market_data_adapter(app)
3357+
subs = getattr(adapter, "_subs", set()) if adapter else set()
3358+
try:
3359+
active_subscriptions = len(subs)
3360+
except Exception:
3361+
active_subscriptions = 0
3362+
3363+
trading_info = get_trading_mode_info()
3364+
return {
3365+
"ok": True,
3366+
"trading_mode": trading_info["trading_mode_effective"],
3367+
"dry_run": bool(getattr(settings, "DRY_RUN", False)),
3368+
"paper_mode": is_paper_trading(),
3369+
"execution_enabled": bool(getattr(settings, "EXECUTION_ENABLED", False)),
3370+
"market_data_ws_enabled": bool(getattr(settings, "MARKET_DATA_WS_ENABLED", False)),
3371+
"market_data_rtds_enabled": bool(getattr(settings, "MARKET_DATA_RTDS_ENABLED", False)),
3372+
"min_confidence": int(getattr(settings, "MIN_CONFIDENCE", 0)),
3373+
"allow_conf_4": bool(getattr(settings, "ALLOW_CONF_4", False)),
3374+
"kill_switch_enabled": bool(trading_info.get("kill_switch_enabled", False)),
3375+
"live_allowed_now": bool(trading_info.get("live_allowed_now", False)),
3376+
"active_subscriptions": active_subscriptions,
3377+
"adapter_initialized": adapter is not None,
3378+
}
3379+
33403380
@app.post("/test")
33413381
def test():
33423382
return {"ok": True, "test": "simple endpoint works"}
@@ -4233,6 +4273,7 @@ def webhook(payload: WebhookPayload):
42334273
if settings.REQUIRE_RAWCONF:
42344274
_blocked_conf_missing += 1
42354275
logger.info(f"[{request_id}] BLOCKED: rawConf missing (REQUIRE_RAWCONF=True)")
4276+
log_signal_decision(request_id, "SKIP", "rawconf_missing")
42364277
try:
42374278
if confirmed_conf_key:
42384279
cleared = _confirmation_store.clear(confirmed_conf_key)
@@ -4251,6 +4292,7 @@ def webhook(payload: WebhookPayload):
42514292
else:
42524293
_blocked_conf_missing += 1
42534294
logger.info(f"[{request_id}] BLOCKED: rawConf missing (MISSING_RAWCONF_ACTION=block)")
4295+
log_signal_decision(request_id, "SKIP", "rawconf_missing")
42544296
try:
42554297
if confirmed_conf_key:
42564298
cleared = _confirmation_store.clear(confirmed_conf_key)
@@ -4269,6 +4311,7 @@ def webhook(payload: WebhookPayload):
42694311
global _blocked_conf_low
42704312
_blocked_conf_low += 1
42714313
logger.info(f"[{request_id}] BLOCKED: rawConf={raw_conf} < MIN_CONFIDENCE={settings.MIN_CONFIDENCE}")
4314+
log_signal_decision(request_id, "SKIP", "rawconf_low", raw_conf=raw_conf)
42724315
try:
42734316
if confirmed_conf_key:
42744317
cleared = _confirmation_store.clear(confirmed_conf_key)
@@ -4289,6 +4332,7 @@ def webhook(payload: WebhookPayload):
42894332
global _blocked_conf_high
42904333
_blocked_conf_high += 1
42914334
logger.info(f"[{request_id}] BLOCKED: rawConf={raw_conf} > MAX_CONFIDENCE={settings.MAX_CONFIDENCE}")
4335+
log_signal_decision(request_id, "SKIP", "rawconf_high", raw_conf=raw_conf)
42924336
try:
42934337
if confirmed_conf_key:
42944338
cleared = _confirmation_store.clear(confirmed_conf_key)
@@ -4527,6 +4571,7 @@ def webhook(payload: WebhookPayload):
45274571

45284572
if not exposure_check.allowed:
45294573
logger.warning(f"[{request_id}] TRADE SKIPPED: {exposure_check.reason}")
4574+
log_signal_decision(request_id, "SKIP", "max_exposure", detail=exposure_check.reason)
45304575
return {
45314576
"ok": True,
45324577
"ignored": True,
@@ -4547,6 +4592,7 @@ def webhook(payload: WebhookPayload):
45474592

45484593
if not direction_allowed:
45494594
logger.warning(f"[{request_id}] TRADE SKIPPED: {direction_reason}")
4595+
log_signal_decision(request_id, "SKIP", "direction_limit", detail=direction_reason)
45504596
return {
45514597
"ok": True,
45524598
"ignored": True,
@@ -5095,6 +5141,7 @@ def webhook(payload: WebhookPayload):
50955141
# in Datei loggen (entry_price is guaranteed to be set at this point)
50965142
append_jsonl(settings.PAPER_LOG_PATH, would_order)
50975143
logger.info(f"[{request_id}] PAPER LOGGED TO: {settings.PAPER_LOG_PATH}")
5144+
log_signal_decision(request_id, "ENTER", "paper_trade_logged", action=action, token_id=(chosen_token[:18] + "...") if chosen_token else None)
50985145
logger.debug(f"[{request_id}] PAPER ORDER: {would_order}")
50995146
# debug instrumentation H4 - paper trade logged
51005147
try:
@@ -5119,6 +5166,7 @@ def webhook(payload: WebhookPayload):
51195166
)
51205167
else:
51215168
logger.warning(f"[{request_id}] NO PAPER ORDER (missing token or action)")
5169+
log_signal_decision(request_id, "SKIP", "missing_token_or_action", action=action, has_token=bool(chosen_token))
51225170

51235171
clob_raw = None
51245172
if market:

0 commit comments

Comments
 (0)