Skip to content

Commit 9302d1f

Browse files
feat(winrate): add MQ gate, confirmation store, exit safety and tests
Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 91ffc53 commit 9302d1f

4 files changed

Lines changed: 242 additions & 0 deletions

File tree

scripts/aggregate_trades.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env python3
2+
import glob, json
3+
from pathlib import Path
4+
5+
def main():
6+
base = Path("c:/Users/sefa1/agents")
7+
pattern1 = str(base / "paper_trades_legacy*.jsonl")
8+
pattern2 = str(base / "paper_trades_legacy.jsonl")
9+
files = glob.glob(pattern1) + glob.glob(pattern2)
10+
latest = {}
11+
for fp in files:
12+
try:
13+
with open(fp, "r", encoding="utf-8") as f:
14+
for line in f:
15+
line = line.strip()
16+
if not line:
17+
continue
18+
try:
19+
j = json.loads(line)
20+
except Exception:
21+
continue
22+
tid = j.get("trade_id")
23+
if not tid:
24+
continue
25+
latest[tid] = j
26+
except Exception:
27+
continue
28+
# include current file
29+
cur = base / "paper_trades.jsonl"
30+
if cur.exists():
31+
with cur.open("r", encoding="utf-8") as f:
32+
for line in f:
33+
line = line.strip()
34+
if not line:
35+
continue
36+
try:
37+
j = json.loads(line)
38+
except Exception:
39+
continue
40+
tid = j.get("trade_id")
41+
if not tid:
42+
continue
43+
latest[tid] = j
44+
45+
total = len(latest)
46+
open_count = sum(1 for r in latest.values() if r.get("status") != "closed")
47+
closed = [r for r in latest.values() if r.get("status") == "closed"]
48+
closed_count = len(closed)
49+
wins = sum(1 for r in closed if r.get("realized_pnl") is not None and float(r.get("realized_pnl")) > 0)
50+
losses = sum(1 for r in closed if r.get("realized_pnl") is not None and float(r.get("realized_pnl")) < 0)
51+
ties = sum(1 for r in closed if r.get("realized_pnl") is None)
52+
win_rate = (wins / closed_count * 100) if closed_count > 0 else 0.0
53+
54+
print(f"total:{total} open:{open_count} closed:{closed_count} wins:{wins} losses:{losses} ties:{ties} win_rate:{win_rate:.1f}%")
55+
56+
if __name__ == "__main__":
57+
main()
58+

src/config/settings.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,28 @@ class Settings:
7474
PREFIX: str = "btc-updown-15m"
7575
PAPER_LOG_PATH: str = "paper_trades.jsonl"
7676
GAMMA_API: str = "https://gamma-api.polymarket.com"
77+
78+
# Win-rate upgrade (feature flag)
79+
WINRATE_UPGRADE_ENABLED: bool = False # safe default: disabled
80+
81+
# Market Quality (entry) params
82+
MAX_SPREAD_ENTRY: float = 0.10
83+
MIN_ASK_SIZE: float = 5.0
84+
ENFORCE_DEPTH: bool = True # if True, require ask size available
85+
86+
# Entry window
87+
ENTRY_WINDOW_END_SECONDS: int = 300 # 5 minutes
88+
ENTRY_WINDOW_STRICT: bool = True
89+
90+
# Confirmation / debounce
91+
REQUIRE_CONFIRMATION: bool = True
92+
CONFIRMATION_DELAY_SECONDS: int = 60
93+
CONFIRMATION_TTL_SECONDS: int = 180
94+
PENDING_CONFIRM_PATH: str = "pending_confirmations.json"
95+
96+
# Exit safety
97+
MAX_SPREAD_EXIT: float = 0.15
98+
MAX_HOLD_SECONDS: int = 900 # 15 minutes
7799

78100

79101
_settings: Optional[Settings] = None

src/utils/winrate_upgrade.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import json
2+
import os
3+
import time
4+
from threading import Lock
5+
from typing import Any, Dict, Optional, Tuple
6+
7+
8+
class ConfirmationStore:
9+
def __init__(self, path: str):
10+
self.path = path
11+
self.lock = Lock()
12+
self._data: Dict[str, Dict[str, Any]] = {}
13+
self._load()
14+
15+
def _load(self):
16+
try:
17+
if os.path.exists(self.path):
18+
with open(self.path, "r", encoding="utf-8") as f:
19+
self._data = json.load(f)
20+
except Exception:
21+
self._data = {}
22+
23+
def _save(self):
24+
tmp = f"{self.path}.tmp"
25+
with open(tmp, "w", encoding="utf-8") as f:
26+
json.dump(self._data, f)
27+
os.replace(tmp, self.path)
28+
29+
def mark_pending(self, key: str, payload: Dict[str, Any]) -> None:
30+
with self.lock:
31+
self._data[key] = {"first_seen": time.time(), "payload": payload}
32+
try:
33+
self._save()
34+
except Exception:
35+
pass
36+
37+
def pop_if_confirmed(self, key: str, delay: int, ttl: int) -> Tuple[bool, Optional[Dict[str, Any]]]:
38+
"""
39+
Returns (confirmed, payload). If confirmed is True, pending entry is removed.
40+
"""
41+
with self.lock:
42+
entry = self._data.get(key)
43+
if not entry:
44+
return False, None
45+
now = time.time()
46+
first = entry.get("first_seen", now)
47+
if now - first > ttl:
48+
# expired
49+
del self._data[key]
50+
try:
51+
self._save()
52+
except Exception:
53+
pass
54+
return False, None
55+
if now - first >= delay:
56+
payload = entry.get("payload")
57+
del self._data[key]
58+
try:
59+
self._save()
60+
except Exception:
61+
pass
62+
return True, payload
63+
return False, None
64+
65+
def expire_all_older_than(self, ttl: int) -> int:
66+
removed = 0
67+
now = time.time()
68+
with self.lock:
69+
keys = list(self._data.keys())
70+
for k in keys:
71+
if now - self._data[k].get("first_seen", now) > ttl:
72+
del self._data[k]
73+
removed += 1
74+
if removed:
75+
try:
76+
self._save()
77+
except Exception:
78+
pass
79+
return removed
80+
81+
82+
def check_market_quality_for_entry(best_bid: Optional[float], best_ask: Optional[float], ask_size: Optional[float], settings) -> Tuple[bool, str, Dict[str, Any]]:
83+
"""
84+
Return (ok, reason, details)
85+
"""
86+
details = {
87+
"best_bid": best_bid,
88+
"best_ask": best_ask,
89+
"ask_size": ask_size,
90+
}
91+
if best_ask is None:
92+
return False, "no_entry_price", details
93+
if best_bid is None:
94+
return False, "no_best_bid", details
95+
spread = best_ask - best_bid
96+
details["spread"] = spread
97+
if spread > settings.MAX_SPREAD_ENTRY:
98+
return False, "spread_too_wide", details
99+
if settings.ENFORCE_DEPTH:
100+
if ask_size is None:
101+
return False, "ask_size_unavailable", details
102+
try:
103+
if float(ask_size) < float(settings.MIN_ASK_SIZE):
104+
return False, "ask_size_too_small", details
105+
except Exception:
106+
return False, "ask_size_unavailable", details
107+
return True, "ok", details
108+
109+
110+
def compute_time_to_market_end(market: Optional[Dict[str, Any]]) -> Tuple[Optional[int], str]:
111+
"""
112+
Returns (seconds_to_end or None, reason)
113+
"""
114+
if not market:
115+
return None, "end_time_unavailable"
116+
end_ts = market.get("end_time") or market.get("close_time") or market.get("end")
117+
if not end_ts:
118+
return None, "end_time_unavailable"
119+
try:
120+
# Expect ISO format; if numeric epoch, handle
121+
from datetime import datetime, timezone
122+
123+
if isinstance(end_ts, (int, float)):
124+
end = datetime.fromtimestamp(float(end_ts), tz=timezone.utc)
125+
else:
126+
end = datetime.fromisoformat(str(end_ts).replace("Z", "+00:00"))
127+
now = datetime.now(timezone.utc)
128+
seconds = int((end - now).total_seconds())
129+
return seconds, "ok"
130+
except Exception:
131+
return None, "end_time_unavailable"
132+

tests/test_winrate_upgrade.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import os
2+
import time
3+
from src.utils.winrate_upgrade import check_market_quality_for_entry, ConfirmationStore
4+
5+
6+
def test_check_mq_no_ask():
7+
class S: pass
8+
settings = type("S", (), {"MAX_SPREAD_ENTRY": 0.1, "MIN_ASK_SIZE": 5.0, "ENFORCE_DEPTH": True})()
9+
ok, reason, details = check_market_quality_for_entry(None, None, None, settings)
10+
assert not ok and reason == "no_entry_price"
11+
12+
13+
def test_check_mq_spread_too_wide():
14+
settings = type("S", (), {"MAX_SPREAD_ENTRY": 0.1, "MIN_ASK_SIZE": 5.0, "ENFORCE_DEPTH": False})()
15+
ok, reason, details = check_market_quality_for_entry(0.5, 0.7, 10.0, settings)
16+
assert not ok and reason == "spread_too_wide"
17+
18+
19+
def test_confirmation_store_delay_and_ttl(tmp_path):
20+
p = tmp_path / "pending.json"
21+
store = ConfirmationStore(str(p))
22+
key = "mkt|BULL|sig1"
23+
store.mark_pending(key, {"foo": "bar"})
24+
confirmed, _ = store.pop_if_confirmed(key, delay=1, ttl=5)
25+
assert not confirmed
26+
time.sleep(1.1)
27+
confirmed, payload = store.pop_if_confirmed(key, delay=1, ttl=5)
28+
assert confirmed
29+
assert payload is not None
30+

0 commit comments

Comments
 (0)