Threaded fast trigger + incremental ingest + naked-stop guard
Three fixes from the first live session: 1) Naked-stop bug (critical): the entry-timeout guard cancelled an entry it thought was unfilled AFTER it had really filled (our order_filled events lag the 60s sync). Cancelling a bracket parent cancels its stop/TP legs, so the position went NAKED and bled 22% with no floor. Guard now checks the broker's LIVE positions (get_positions) before cancelling and never cancels a filled name. Regression test added. 2) Speed: ingest re-fetched a 240-min window for ~80 symbols every minute (~46s measured), and the single-threaded loop froze the 4s trigger that whole time -> breakouts caught once a minute, not in real time. Ingest is now incremental (heavy first pass covers the session so the opening range is present; then ~20-min refreshes, closed bars never change). Loop passes dropped from ~70s to ~2-5s. 3) Threaded trigger: the fast breakout detector/fire loop now runs on its own thread, fully decoupled from the slow scheduler. EventStore, the execution service, and the trigger book are made thread-safe (locks) so the thread and main loop can't race the shared psycopg2 connection or in-memory state. Verified live: fired EZGO/NEOV, all positions retain active stops. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
7b039ca288
commit
d5d79cccf7
5 files changed
+224
-54
No files matched your search
+33
-26
@@ -12,6 +12,7 @@ from .db import get_connection
|
||||
from uuid import uuid4
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -22,6 +23,9 @@ class EventStore:
|
||||
def __init__(self, db_path: str = "./data/momentum_events.duckdb"):
|
||||
"""Initialize event store with DuckDB connection."""
|
||||
self.con = get_connection(db_path)
|
||||
# serialize DB access: the fast trigger thread and the main loop share
|
||||
# this one psycopg2 connection, which is not safe for concurrent use.
|
||||
self._lock = threading.RLock()
|
||||
self._ensure_schema()
|
||||
|
||||
def _ensure_schema(self):
|
||||
@@ -64,26 +68,27 @@ class EventStore:
|
||||
event_id = str(uuid4())
|
||||
payload_json = json.dumps(event.model_dump(mode="json", exclude_unset=True))
|
||||
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO events
|
||||
(id, timestamp, mode, event_type, correlation_id, message, payload_json, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, current_timestamp)
|
||||
""",
|
||||
[
|
||||
event_id,
|
||||
event.timestamp,
|
||||
event.mode.value,
|
||||
event.event_type.value
|
||||
if isinstance(event.event_type, EventType)
|
||||
else str(event.event_type),
|
||||
event.correlation_id,
|
||||
event.message,
|
||||
payload_json,
|
||||
],
|
||||
)
|
||||
self.con.commit()
|
||||
with self._lock:
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO events
|
||||
(id, timestamp, mode, event_type, correlation_id, message, payload_json, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, current_timestamp)
|
||||
""",
|
||||
[
|
||||
event_id,
|
||||
event.timestamp,
|
||||
event.mode.value,
|
||||
event.event_type.value
|
||||
if isinstance(event.event_type, EventType)
|
||||
else str(event.event_type),
|
||||
event.correlation_id,
|
||||
event.message,
|
||||
payload_json,
|
||||
],
|
||||
)
|
||||
self.con.commit()
|
||||
|
||||
logger.debug(
|
||||
f"Emitted event {event_id}: {event.event_type.value} - {event.message}"
|
||||
@@ -152,9 +157,10 @@ class EventStore:
|
||||
{limit_clause}
|
||||
"""
|
||||
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
with self._lock:
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute(query, params)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
results = []
|
||||
for row in rows:
|
||||
@@ -175,9 +181,10 @@ class EventStore:
|
||||
|
||||
def count_events(self) -> int:
|
||||
"""Total number of events. Cheap change-detection signal for SSE."""
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM events")
|
||||
row = cursor.fetchone()
|
||||
with self._lock:
|
||||
cursor = self.con.cursor()
|
||||
cursor.execute("SELECT COUNT(*) FROM events")
|
||||
row = cursor.fetchone()
|
||||
return int(row[0]) if row else 0
|
||||
|
||||
def get_symbol_timeline(self, symbol: str) -> list[dict]:
|
||||
|
||||
Reference in new issue
Block a user