Dmitry Beresnev commited on
Commit
debae55
·
1 Parent(s): a71dd82
src/core/event_detector.py CHANGED
@@ -1,4 +1,4 @@
1
- from typing import Dict, Optional
2
  import numpy as np
3
 
4
  from .config import K_SIGMA, MIN_WINDOW_POINTS
@@ -11,8 +11,16 @@ def adaptive_threshold(window) -> Optional[float]:
11
  returns = np.diff(prices) / prices[:-1]
12
  if returns.size == 0:
13
  return None
14
- sigma = float(np.std(returns))
15
- return -K_SIGMA * sigma * 100.0
 
 
 
 
 
 
 
 
16
 
17
 
18
  def detect_event(delta_pct: float, threshold: float) -> bool:
 
1
+ from typing import Optional
2
  import numpy as np
3
 
4
  from .config import K_SIGMA, MIN_WINDOW_POINTS
 
11
  returns = np.diff(prices) / prices[:-1]
12
  if returns.size == 0:
13
  return None
14
+ # Robust sigma (MAD): a plain std is contaminated by the very crash we
15
+ # are trying to detect, inflating the threshold past the crash itself.
16
+ mad = float(np.median(np.abs(returns - np.median(returns))))
17
+ sigma = 1.4826 * mad
18
+ if sigma <= 0.0:
19
+ return None
20
+ # delta_pct spans the whole window (returns.size steps), so scale the
21
+ # single-step sigma to the window horizon before applying K_SIGMA.
22
+ horizon_sigma = sigma * float(np.sqrt(returns.size))
23
+ return -K_SIGMA * horizon_sigma * 100.0
24
 
25
 
26
  def detect_event(delta_pct: float, threshold: float) -> bool:
src/core/main.py CHANGED
@@ -17,7 +17,7 @@ from .topics import TOPICS
17
  from .telemetry import Telemetry
18
 
19
 
20
- def run_once(pubsub, loader, universe, queue, notifier):
21
  for venue_key, tickers in universe.items():
22
  if not tickers:
23
  continue
@@ -25,7 +25,7 @@ def run_once(pubsub, loader, universe, queue, notifier):
25
  venue = venue_key
26
  pubsub.publish(TOPICS["universe_updated"], loader.build_event(asset_class, venue, tickers))
27
 
28
- ingestor = PriceIngestor(venue=venue, asset_class=asset_class)
29
  price_event = ingestor.fetch_prices(tickers)
30
  pubsub.publish(TOPICS["prices_snapshot"], price_event)
31
 
@@ -66,25 +66,42 @@ def build_runtime():
66
  snapshot = loader.build_snapshot(universe)
67
  pubsub.publish(TOPICS["universe_snapshot"], snapshot)
68
 
 
 
 
 
 
69
  return {
70
  "pubsub": pubsub,
71
  "notifier": notifier,
72
  "queue": queue,
73
  "loader": loader,
74
  "universe": universe,
 
75
  }
76
 
77
 
 
 
 
 
 
 
 
 
 
 
 
78
  def run_single_cycle(runtime=None):
79
  state = runtime or build_runtime()
80
- run_once(state["pubsub"], state["loader"], state["universe"], state["queue"], state["notifier"])
81
  return state
82
 
83
 
84
  def run_forever():
85
  state = build_runtime()
86
  while True:
87
- run_once(state["pubsub"], state["loader"], state["universe"], state["queue"], state["notifier"])
88
  time.sleep(POLL_INTERVAL_SEC)
89
 
90
 
 
17
  from .telemetry import Telemetry
18
 
19
 
20
+ def run_once(pubsub, loader, universe, queue, notifier, ingestors):
21
  for venue_key, tickers in universe.items():
22
  if not tickers:
23
  continue
 
25
  venue = venue_key
26
  pubsub.publish(TOPICS["universe_updated"], loader.build_event(asset_class, venue, tickers))
27
 
28
+ ingestor = ingestors[venue_key]
29
  price_event = ingestor.fetch_prices(tickers)
30
  pubsub.publish(TOPICS["prices_snapshot"], price_event)
31
 
 
66
  snapshot = loader.build_snapshot(universe)
67
  pubsub.publish(TOPICS["universe_snapshot"], snapshot)
68
 
69
+ ingestors = {
70
+ venue_key: PriceIngestor(venue=venue_key, asset_class=_infer_asset_class(venue_key))
71
+ for venue_key in universe
72
+ }
73
+
74
  return {
75
  "pubsub": pubsub,
76
  "notifier": notifier,
77
  "queue": queue,
78
  "loader": loader,
79
  "universe": universe,
80
+ "ingestors": ingestors,
81
  }
82
 
83
 
84
+ def _run_cycle(state):
85
+ run_once(
86
+ state["pubsub"],
87
+ state["loader"],
88
+ state["universe"],
89
+ state["queue"],
90
+ state["notifier"],
91
+ state["ingestors"],
92
+ )
93
+
94
+
95
  def run_single_cycle(runtime=None):
96
  state = runtime or build_runtime()
97
+ _run_cycle(state)
98
  return state
99
 
100
 
101
  def run_forever():
102
  state = build_runtime()
103
  while True:
104
+ _run_cycle(state)
105
  time.sleep(POLL_INTERVAL_SEC)
106
 
107
 
src/core/price_ingestor.py CHANGED
@@ -69,6 +69,9 @@ class PriceIngestor:
69
  def __init__(self, venue: str, asset_class: str) -> None:
70
  self.venue = venue
71
  self.asset_class = asset_class
 
 
 
72
 
73
  def fetch_prices(self, tickers: List[str]) -> Dict:
74
  chunks = _chunk_list(tickers, BATCH_SIZE)
@@ -86,7 +89,7 @@ class PriceIngestor:
86
  except Exception:
87
  continue
88
 
89
- sequence_id = next(_SEQ)
90
 
91
  return {
92
  "event_id": str(uuid.uuid4()),
@@ -104,6 +107,3 @@ class PriceIngestor:
104
  for t, p in prices.items()
105
  ],
106
  }
107
-
108
-
109
- _SEQ = itertools.count(1)
 
69
  def __init__(self, venue: str, asset_class: str) -> None:
70
  self.venue = venue
71
  self.asset_class = asset_class
72
+ # Per-venue sequence: DataGapDetector treats any per-ticker jump > 1
73
+ # as a gap, so venues must not share a counter.
74
+ self._seq = itertools.count(1)
75
 
76
  def fetch_prices(self, tickers: List[str]) -> Dict:
77
  chunks = _chunk_list(tickers, BATCH_SIZE)
 
89
  except Exception:
90
  continue
91
 
92
+ sequence_id = next(self._seq)
93
 
94
  return {
95
  "event_id": str(uuid.uuid4()),
 
107
  for t, p in prices.items()
108
  ],
109
  }
 
 
 
src/core/risk_regime.py CHANGED
@@ -34,8 +34,9 @@ class RiskRegimeStateMachine:
34
  target = "normal"
35
 
36
  if target == self._state:
37
- if target == "normal":
38
- self._calm_streak = 0
 
39
  return self._state, False
40
 
41
  if self._rank(target) > self._rank(self._state):
 
34
  target = "normal"
35
 
36
  if target == self._state:
37
+ # Re-confirmed at the current level: not a calm snapshot, so the
38
+ # downgrade streak must restart.
39
+ self._calm_streak = 0
40
  return self._state, False
41
 
42
  if self._rank(target) > self._rank(self._state):
uv.lock ADDED
The diff for this file is too large to render. See raw diff