traderbot 0.14.72__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. traderbot/__init__.py +13 -0
  2. traderbot/analysis/__init__.py +81 -0
  3. traderbot/analysis/indicators.py +120 -0
  4. traderbot/analysis/odds.py +110 -0
  5. traderbot/analysis/portfolio.py +138 -0
  6. traderbot/analysis/registry.py +102 -0
  7. traderbot/analysis/signals.py +195 -0
  8. traderbot/auth.py +369 -0
  9. traderbot/cli/__init__.py +502 -0
  10. traderbot/cli/admin.py +601 -0
  11. traderbot/cli/auth.py +315 -0
  12. traderbot/cli/cron.py +607 -0
  13. traderbot/cli/data.py +227 -0
  14. traderbot/cli/helpers.py +213 -0
  15. traderbot/cli/market.py +320 -0
  16. traderbot/cli/news.py +510 -0
  17. traderbot/cli/profile.py +1077 -0
  18. traderbot/cli/sandbox.py +87 -0
  19. traderbot/cli/trade.py +963 -0
  20. traderbot/cli/ws.py +105 -0
  21. traderbot/cli.py +2 -0
  22. traderbot/cron_loops.py +159 -0
  23. traderbot/data/__init__.py +25 -0
  24. traderbot/data/base_provider.py +64 -0
  25. traderbot/data/base_signals.py +38 -0
  26. traderbot/data/models.py +82 -0
  27. traderbot/data/registry.py +50 -0
  28. traderbot/data/weather/__init__.py +11 -0
  29. traderbot/data/weather/nws_client.py +267 -0
  30. traderbot/data/weather/provider.py +322 -0
  31. traderbot/data/weather/signals.py +238 -0
  32. traderbot/db/__init__.py +45 -0
  33. traderbot/db/decisions.py +165 -0
  34. traderbot/db/experiment_schema.py +86 -0
  35. traderbot/db/forecast_bias.py +133 -0
  36. traderbot/db/learnings.py +354 -0
  37. traderbot/db/positions.py +175 -0
  38. traderbot/db/reconciliation.py +163 -0
  39. traderbot/db/vectors.py +142 -0
  40. traderbot/experiment/cli.py +454 -0
  41. traderbot/experiment/harness.py +400 -0
  42. traderbot/experiment/methodologies/__init__.py +1 -0
  43. traderbot/experiment/methodologies/db_utils.py +49 -0
  44. traderbot/experiment/populate.py +246 -0
  45. traderbot/experiment/registry.py +35 -0
  46. traderbot/experiment/results.py +355 -0
  47. traderbot/experiment/shared.py +98 -0
  48. traderbot/experiment/tests/__init__.py +0 -0
  49. traderbot/experiment/tests/test_env_fallback.py +187 -0
  50. traderbot/experiment/tests/test_harness.py +70 -0
  51. traderbot/experiment/tests/test_installer.py +157 -0
  52. traderbot/experiment/tests/test_nws_client.py +211 -0
  53. traderbot/experiment/tests/test_registry.py +54 -0
  54. traderbot/experiment/tests/test_results.py +80 -0
  55. traderbot/experiment/tests/test_schema.py +59 -0
  56. traderbot/experiment/tests/test_shared.py +79 -0
  57. traderbot/experiment/tests/test_trade_routing.py +110 -0
  58. traderbot/experiment/tests/test_trading_v2.py +178 -0
  59. traderbot/experiment/tests/test_treatments.py +77 -0
  60. traderbot/experiment/tests/test_wal.py +81 -0
  61. traderbot/experiment/treatments/__init__.py +6 -0
  62. traderbot/experiment/treatments/calibration_bundle.py +158 -0
  63. traderbot/experiment/treatments/control.py +37 -0
  64. traderbot/fileops.py +46 -0
  65. traderbot/heartbeat.py +610 -0
  66. traderbot/kalshi/__init__.py +8 -0
  67. traderbot/kalshi/_normalize.py +134 -0
  68. traderbot/kalshi/cache.py +300 -0
  69. traderbot/kalshi/client.py +254 -0
  70. traderbot/kalshi/config.py +60 -0
  71. traderbot/kalshi/events.py +73 -0
  72. traderbot/kalshi/exchange.py +33 -0
  73. traderbot/kalshi/history.py +93 -0
  74. traderbot/kalshi/markets.py +493 -0
  75. traderbot/kalshi/models.py +353 -0
  76. traderbot/kalshi/pinning.py +98 -0
  77. traderbot/kalshi/portfolio.py +152 -0
  78. traderbot/kalshi/provider.py +268 -0
  79. traderbot/kalshi/rate_limit.py +47 -0
  80. traderbot/kalshi/signing.py +117 -0
  81. traderbot/kalshi/trading.py +140 -0
  82. traderbot/kalshi/websocket.py +180 -0
  83. traderbot/kalshi/ws_cache.py +64 -0
  84. traderbot/kalshi/ws_daemon.py +287 -0
  85. traderbot/learning.py +346 -0
  86. traderbot/llm/__init__.py +6 -0
  87. traderbot/llm/client.py +82 -0
  88. traderbot/llm/ollama.py +75 -0
  89. traderbot/logging_config.py +75 -0
  90. traderbot/master_password.py +285 -0
  91. traderbot/news/__init__.py +32 -0
  92. traderbot/news/cache_paths.py +42 -0
  93. traderbot/news/classifier.py +449 -0
  94. traderbot/news/embeddings.py +257 -0
  95. traderbot/news/impact_assessor.py +290 -0
  96. traderbot/news/ingest.py +932 -0
  97. traderbot/news/models.py +115 -0
  98. traderbot/news/sentiment_scorer.py +129 -0
  99. traderbot/news/sources.py +2155 -0
  100. traderbot/paper.py +80 -0
  101. traderbot/paths.py +108 -0
  102. traderbot/platform_compat.py +195 -0
  103. traderbot/profiles/__init__.py +24 -0
  104. traderbot/profiles/auth.py +182 -0
  105. traderbot/profiles/config.py +265 -0
  106. traderbot/profiles/discovery.py +218 -0
  107. traderbot/profiles/injection.py +155 -0
  108. traderbot/profiles/injection_strategies.py +245 -0
  109. traderbot/profiles/isolation.py +116 -0
  110. traderbot/profiles/models.py +73 -0
  111. traderbot/profiles/openclaw_config.py +257 -0
  112. traderbot/profiles/registry.py +202 -0
  113. traderbot/profiles/runtime.py +157 -0
  114. traderbot/profiles/sysadmin.py +25 -0
  115. traderbot/profiles/tokens.py +207 -0
  116. traderbot/risk/__init__.py +139 -0
  117. traderbot/risk/agent_limits.py +97 -0
  118. traderbot/risk/audit.py +88 -0
  119. traderbot/risk/circuit_breaker.py +167 -0
  120. traderbot/risk/limits.py +169 -0
  121. traderbot/risk/sizing.py +45 -0
  122. traderbot/sandbox.py +248 -0
  123. traderbot/simulation/__init__.py +135 -0
  124. traderbot/simulation/adaptation.py +756 -0
  125. traderbot/simulation/adapter_state.py +136 -0
  126. traderbot/simulation/data_loader.py +242 -0
  127. traderbot/simulation/engine.py +433 -0
  128. traderbot/simulation/paper_trader.py +425 -0
  129. traderbot/simulation/performance.py +232 -0
  130. traderbot/simulation/profiles.py +159 -0
  131. traderbot/simulation/settlement.py +226 -0
  132. traderbot/simulation/strategies/__init__.py +110 -0
  133. traderbot/update_config.py +39 -0
  134. traderbot/updater.py +181 -0
  135. traderbot/wal.py +402 -0
  136. traderbot/windows_service.py +318 -0
  137. traderbot-0.14.72.dist-info/METADATA +180 -0
  138. traderbot-0.14.72.dist-info/RECORD +141 -0
  139. traderbot-0.14.72.dist-info/WHEEL +4 -0
  140. traderbot-0.14.72.dist-info/entry_points.txt +2 -0
  141. traderbot-0.14.72.dist-info/licenses/LICENSE +21 -0
traderbot/__init__.py ADDED
@@ -0,0 +1,13 @@
1
+ """TraderBot — Autonomous prediction market investment toolkit for Kalshi."""
2
+
3
+ import importlib.metadata
4
+ from pathlib import Path
5
+
6
+ _ver_file = Path(__file__).resolve().parent.parent.parent / "VERSION"
7
+ if _ver_file.exists():
8
+ __version__ = _ver_file.read_text().strip().lstrip("v")
9
+ else:
10
+ try:
11
+ __version__ = importlib.metadata.version("traderbot")
12
+ except importlib.metadata.PackageNotFoundError:
13
+ __version__ = "0.0.0"
@@ -0,0 +1,81 @@
1
+ """Statistical computation and signal generation for binary prediction markets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from traderbot.analysis.indicators import (
6
+ BollingerBands,
7
+ IndicatorResult,
8
+ MovingAverageResult,
9
+ bollinger_bands,
10
+ ema,
11
+ rsi,
12
+ sma,
13
+ volume_weighted_price,
14
+ )
15
+ from traderbot.analysis.odds import (
16
+ EdgeEstimate,
17
+ ImpliedProb,
18
+ KellyInputs,
19
+ compute_kelly_inputs,
20
+ detect_edge,
21
+ expected_value,
22
+ implied_probability,
23
+ )
24
+ from traderbot.analysis.portfolio import (
25
+ PortfolioMetrics,
26
+ brier_score,
27
+ calibration_curve,
28
+ calmar_ratio,
29
+ edge_realization,
30
+ max_drawdown,
31
+ sharpe_ratio,
32
+ win_rate,
33
+ )
34
+ from traderbot.analysis.registry import (
35
+ AnalysisRegistry,
36
+ CategoryAnalyzer,
37
+ CategorySignals,
38
+ GenericAnalyzer,
39
+ )
40
+ from traderbot.analysis.signals import (
41
+ CombinedSignal,
42
+ SignalSource,
43
+ combine_signals,
44
+ default_weights,
45
+ generate_signal,
46
+ )
47
+
48
+ __all__ = [
49
+ "AnalysisRegistry",
50
+ "BollingerBands",
51
+ "CategoryAnalyzer",
52
+ "CategorySignals",
53
+ "CombinedSignal",
54
+ "EdgeEstimate",
55
+ "GenericAnalyzer",
56
+ "ImpliedProb",
57
+ "IndicatorResult",
58
+ "KellyInputs",
59
+ "MovingAverageResult",
60
+ "PortfolioMetrics",
61
+ "SignalSource",
62
+ "bollinger_bands",
63
+ "brier_score",
64
+ "calibration_curve",
65
+ "calmar_ratio",
66
+ "combine_signals",
67
+ "compute_kelly_inputs",
68
+ "default_weights",
69
+ "detect_edge",
70
+ "edge_realization",
71
+ "ema",
72
+ "expected_value",
73
+ "generate_signal",
74
+ "implied_probability",
75
+ "max_drawdown",
76
+ "rsi",
77
+ "sharpe_ratio",
78
+ "sma",
79
+ "volume_weighted_price",
80
+ "win_rate",
81
+ ]
@@ -0,0 +1,120 @@
1
+ """Technical indicators adapted for binary prediction markets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ import math
10
+ from datetime import datetime # noqa: TC003
11
+
12
+ from pydantic import BaseModel, ConfigDict
13
+
14
+ from traderbot.kalshi.models import Trade # noqa: TC001
15
+
16
+
17
+ class IndicatorResult(BaseModel):
18
+ model_config = ConfigDict(strict=True, extra="forbid")
19
+
20
+ name: str
21
+ value: float
22
+ timestamp: datetime
23
+
24
+
25
+ class MovingAverageResult(BaseModel):
26
+ model_config = ConfigDict(strict=True, extra="forbid")
27
+
28
+ sma: float
29
+ ema: float
30
+ period: int
31
+
32
+
33
+ class BollingerBands(BaseModel):
34
+ model_config = ConfigDict(strict=True, extra="forbid")
35
+
36
+ lower: int
37
+ middle: int
38
+ upper: int
39
+
40
+
41
+ def sma(prices: list[int], period: int) -> float:
42
+ """Simple moving average of the last *period* prices (or all if fewer)."""
43
+ if not prices:
44
+ raise ValueError("prices must not be empty")
45
+ n = min(period, len(prices))
46
+ return sum(prices[-n:]) / n
47
+
48
+
49
+ def ema(prices: list[int], period: int) -> float:
50
+ """Exponential moving average with multiplier 2/(period+1)."""
51
+ if not prices:
52
+ raise ValueError("prices must not be empty")
53
+ mult = 2 / (period + 1)
54
+ if len(prices) == 1:
55
+ return float(prices[0])
56
+ n = min(period, len(prices))
57
+ result = sum(prices[:n]) / n
58
+ for price in prices[n:]:
59
+ result = price * mult + result * (1 - mult)
60
+ return result
61
+
62
+
63
+ def rsi(prices: list[int], period: int = 14) -> float:
64
+ """Wilder's RSI using exponential smoothing on price deltas."""
65
+ if not prices:
66
+ raise ValueError("prices must not be empty")
67
+ if len(prices) < 2:
68
+ return 50.0
69
+
70
+ deltas = [prices[i + 1] - prices[i] for i in range(len(prices) - 1)]
71
+
72
+ gains: list[float] = []
73
+ losses: list[float] = []
74
+ for d in deltas:
75
+ gains.append(float(d) if d > 0 else 0.0)
76
+ losses.append(float(-d) if d < 0 else 0.0)
77
+
78
+ n = min(period, len(deltas))
79
+ avg_gain = sum(gains[:n]) / n
80
+ avg_loss = sum(losses[:n]) / n
81
+
82
+ for i in range(n, len(deltas)):
83
+ avg_gain = (avg_gain * (period - 1) + gains[i]) / period
84
+ avg_loss = (avg_loss * (period - 1) + losses[i]) / period
85
+
86
+ if avg_gain == 0 and avg_loss == 0:
87
+ return 50.0
88
+ if avg_loss == 0:
89
+ return 100.0
90
+ if avg_gain == 0:
91
+ return 0.0
92
+
93
+ rs = avg_gain / avg_loss
94
+ return 100.0 - (100.0 / (1.0 + rs))
95
+
96
+
97
+ def bollinger_bands(prices: list[int], period: int = 20, k: float = 2.0) -> BollingerBands:
98
+ """Bollinger Bands with population standard deviation, returned as int cents."""
99
+ if not prices:
100
+ raise ValueError("prices must not be empty")
101
+ n = min(period, len(prices))
102
+ window = prices[-n:]
103
+ middle = sum(window) / n
104
+ variance = sum((p - middle) ** 2 for p in window) / n
105
+ std = math.sqrt(variance)
106
+ return BollingerBands(
107
+ lower=round(middle - k * std),
108
+ middle=round(middle),
109
+ upper=round(middle + k * std),
110
+ )
111
+
112
+
113
+ def volume_weighted_price(trades: list[Trade]) -> int:
114
+ """Volume-weighted average price, returned as int cents."""
115
+ # TODO: Wire into generate_signal()
116
+ if not trades:
117
+ raise ValueError("trades must not be empty")
118
+ total_value = sum(t.price * t.quantity for t in trades)
119
+ total_qty = sum(t.quantity for t in trades)
120
+ return round(total_value / total_qty)
@@ -0,0 +1,110 @@
1
+ """Implied probability, edge detection, and Kelly criterion for binary markets."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from typing import TYPE_CHECKING, Literal
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ from pydantic import BaseModel, ConfigDict
14
+
15
+ if TYPE_CHECKING:
16
+ from traderbot.kalshi.models import OrderBook
17
+
18
+
19
+ class ImpliedProb(BaseModel):
20
+ model_config = ConfigDict(strict=True, extra="forbid")
21
+
22
+ yes_prob: float
23
+ no_prob: float
24
+ spread_cents: int
25
+ mid_price_cents: int
26
+
27
+
28
+ class EdgeEstimate(BaseModel):
29
+ model_config = ConfigDict(strict=True, extra="forbid")
30
+
31
+ estimated_prob: float
32
+ market_prob: float
33
+ edge: float
34
+ direction: Literal["yes", "no", "neutral"]
35
+
36
+
37
+ class KellyInputs(BaseModel):
38
+ model_config = ConfigDict(strict=True, extra="forbid")
39
+
40
+ prob: float
41
+ odds: float
42
+ edge: float
43
+ kelly_fraction: float
44
+
45
+
46
+ def implied_probability(orderbook: OrderBook) -> ImpliedProb:
47
+ """Extract implied probabilities from bid data in a binary order book."""
48
+ best_yes_bid = max((lvl.price for lvl in orderbook.yes_bids), default=0)
49
+ best_no_bid = max((lvl.price for lvl in orderbook.no_bids), default=0)
50
+
51
+ if best_yes_bid == 0 and best_no_bid == 0:
52
+ raise ValueError("Order book has no bids on either side")
53
+
54
+ yes_prob = best_yes_bid / 100.0
55
+ no_prob = best_no_bid / 100.0
56
+ best_yes_ask = 100 - best_no_bid
57
+ spread_cents = best_yes_ask - best_yes_bid
58
+ mid_price_cents = max(1, round((best_yes_bid + best_yes_ask) / 2))
59
+
60
+ return ImpliedProb(
61
+ yes_prob=yes_prob,
62
+ no_prob=no_prob,
63
+ spread_cents=spread_cents,
64
+ mid_price_cents=mid_price_cents,
65
+ )
66
+
67
+
68
+ def detect_edge(estimated_prob: float, orderbook: OrderBook) -> EdgeEstimate:
69
+ """Compare estimated probability against market-implied probability."""
70
+ market_prob = implied_probability(orderbook).yes_prob
71
+ edge = estimated_prob - market_prob
72
+
73
+ if abs(edge) < 0.01:
74
+ direction: Literal["yes", "no", "neutral"] = "neutral"
75
+ elif edge > 0:
76
+ direction = "yes"
77
+ else:
78
+ direction = "no"
79
+
80
+ return EdgeEstimate(
81
+ estimated_prob=estimated_prob,
82
+ market_prob=market_prob,
83
+ edge=edge,
84
+ direction=direction,
85
+ )
86
+
87
+
88
+ def compute_kelly_inputs(estimated_prob: float, market_price_cents: int) -> KellyInputs:
89
+ """Compute Kelly criterion fraction for a binary market position."""
90
+ profit = 100 - market_price_cents
91
+ loss = market_price_cents
92
+ odds = profit / loss
93
+ edge = estimated_prob - (market_price_cents / 100.0)
94
+ kelly = (estimated_prob * odds - (1 - estimated_prob)) / odds
95
+ kelly_fraction = max(0.0, min(1.0, kelly))
96
+
97
+ return KellyInputs(
98
+ prob=estimated_prob,
99
+ odds=odds,
100
+ edge=edge,
101
+ kelly_fraction=kelly_fraction,
102
+ )
103
+
104
+
105
+ def expected_value(prob: float, market_price_cents: int, quantity: int = 1) -> int:
106
+ """Compute expected value in cents for a yes-position in a binary market."""
107
+ profit_per = 100 - market_price_cents
108
+ loss_per = market_price_cents
109
+ ev = prob * profit_per * quantity - (1 - prob) * loss_per * quantity
110
+ return round(ev)
@@ -0,0 +1,138 @@
1
+ """Portfolio analytics: calibration, risk metrics, and edge realization."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ import math
10
+
11
+ logger = logging.getLogger(__name__)
12
+ from typing import TYPE_CHECKING
13
+
14
+ from pydantic import BaseModel, ConfigDict
15
+
16
+ if TYPE_CHECKING:
17
+ from traderbot.kalshi.models import Decision
18
+
19
+
20
+ class PortfolioMetrics(BaseModel):
21
+ model_config = ConfigDict(strict=True, extra="forbid")
22
+
23
+ total_trades: int
24
+ win_rate: float
25
+ brier_score: float
26
+ sharpe_ratio: float | None
27
+ max_drawdown_pct: float
28
+ calmar_ratio: float | None
29
+ total_pnl_cents: int
30
+ avg_edge_realization: float
31
+
32
+
33
+ def win_rate(decisions: list[Decision]) -> float:
34
+ """Fraction of executed decisions that predicted correctly."""
35
+ qualifying = [d for d in decisions if d.outcome == "executed" and d.actual_result is not None]
36
+ if not qualifying:
37
+ return 0.0
38
+ wins = sum(
39
+ 1
40
+ for d in qualifying
41
+ if (d.direction == "yes" and d.actual_result is True)
42
+ or (d.direction == "no" and d.actual_result is False)
43
+ )
44
+ return wins / len(qualifying)
45
+
46
+
47
+ def brier_score(predictions: list[tuple[float, bool]]) -> float:
48
+ """Mean squared error of probabilistic predictions."""
49
+ if not predictions:
50
+ return 0.0
51
+ total = sum((prob - float(actual)) ** 2 for prob, actual in predictions)
52
+ return total / len(predictions)
53
+
54
+
55
+ def calibration_curve(
56
+ predictions: list[tuple[float, bool]], buckets: int = 10
57
+ ) -> list[tuple[float, float]]:
58
+ """Observed frequency per predicted-probability bucket."""
59
+ bin_sums: dict[int, list[tuple[float, bool]]] = {}
60
+ for prob, actual in predictions:
61
+ idx = min(int(prob * buckets), buckets - 1)
62
+ bin_sums.setdefault(idx, []).append((prob, actual))
63
+
64
+ result: list[tuple[float, float]] = []
65
+ for i in range(buckets):
66
+ if i not in bin_sums:
67
+ continue
68
+ items = bin_sums[i]
69
+ mean_pred = sum(p for p, _ in items) / len(items)
70
+ obs_freq = sum(1 for _, a in items if a) / len(items)
71
+ result.append((mean_pred, obs_freq))
72
+ return result
73
+
74
+
75
+ def sharpe_ratio(returns: list[float], risk_free: float = 0.0) -> float | None:
76
+ """Annualized Sharpe ratio from periodic returns."""
77
+ if len(returns) < 2:
78
+ return None
79
+ excess = [r - risk_free for r in returns]
80
+ mean_excess = sum(excess) / len(excess)
81
+ variance = sum((e - mean_excess) ** 2 for e in excess) / (len(excess) - 1)
82
+ if variance < 1e-15:
83
+ return None
84
+ return mean_excess / math.sqrt(variance) * math.sqrt(252)
85
+
86
+
87
+ def max_drawdown(values: list[int]) -> float:
88
+ """Maximum fractional drawdown from running peak."""
89
+ if not values:
90
+ return 0.0
91
+ peak = 0
92
+ max_dd = 0.0
93
+ for val in values:
94
+ if val > peak:
95
+ peak = val
96
+ if peak > 0:
97
+ dd = (peak - val) / peak
98
+ if dd > max_dd:
99
+ max_dd = dd
100
+ return max_dd
101
+
102
+
103
+ def calmar_ratio(annualized_return: float, max_dd: float) -> float | None:
104
+ """Annualized return divided by maximum drawdown."""
105
+ if max_dd == 0.0:
106
+ return None
107
+ return annualized_return / max_dd
108
+
109
+
110
+ def edge_realization(decisions: list[Decision]) -> float:
111
+ """Average ratio of actual-vs-expected PnL per executed decision."""
112
+ qualifying: list[tuple[int, int]] = []
113
+ for d in decisions:
114
+ if d.outcome != "executed" or d.actual_result is None:
115
+ continue
116
+ # actual PnL in cents
117
+ if d.direction == "yes":
118
+ if d.actual_result is True:
119
+ actual_pnl = (100 - d.price) * d.quantity
120
+ else:
121
+ actual_pnl = -d.price * d.quantity
122
+ elif d.direction == "no":
123
+ if d.actual_result is False:
124
+ actual_pnl = (100 - d.price) * d.quantity
125
+ else:
126
+ actual_pnl = -d.price * d.quantity
127
+ else:
128
+ continue
129
+
130
+ expected_pnl = abs(d.edge_estimate) * d.quantity * d.price
131
+ if expected_pnl == 0:
132
+ continue
133
+ qualifying.append((actual_pnl, expected_pnl))
134
+
135
+ if not qualifying:
136
+ return 0.0
137
+ realization_sum = sum((actual - expected) / abs(expected) for actual, expected in qualifying)
138
+ return realization_sum / len(qualifying)
@@ -0,0 +1,102 @@
1
+ """Category-based analysis registry with protocol-based analyzer dispatch."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+
7
+ from typing import Annotated, Protocol, runtime_checkable
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ from pydantic import BaseModel, ConfigDict, Field
14
+
15
+ from traderbot.analysis.indicators import bollinger_bands, ema, rsi
16
+ from traderbot.analysis.odds import detect_edge
17
+ from traderbot.analysis.signals import generate_signal
18
+ from traderbot.kalshi.models import MarketCategory # noqa: TC001
19
+
20
+
21
+ class CategorySignals(BaseModel):
22
+ model_config = ConfigDict(strict=True, extra="forbid")
23
+
24
+ category: MarketCategory
25
+ signals: list[str]
26
+ confidence: Annotated[float, Field(ge=0, le=1)]
27
+ data_sources: list[str]
28
+
29
+
30
+ @runtime_checkable
31
+ class CategoryAnalyzer(Protocol):
32
+ def analyze(self, market_data: dict, category: MarketCategory) -> CategorySignals: ...
33
+
34
+
35
+ class AnalysisRegistry:
36
+ def __init__(self) -> None:
37
+ self._analyzers: dict[MarketCategory, CategoryAnalyzer] = {}
38
+
39
+ def register(self, category: MarketCategory, analyzer: CategoryAnalyzer) -> None:
40
+ self._analyzers[category] = analyzer
41
+
42
+ def get(self, category: MarketCategory) -> CategoryAnalyzer:
43
+ if category in self._analyzers:
44
+ return self._analyzers[category]
45
+ return self._default
46
+
47
+ def analyze(self, market_data: dict, category: MarketCategory) -> CategorySignals:
48
+ analyzer = self.get(category)
49
+ return analyzer.analyze(market_data, category)
50
+
51
+ @property
52
+ def _default(self) -> CategoryAnalyzer:
53
+ return GenericAnalyzer()
54
+
55
+
56
+ class GenericAnalyzer:
57
+ """Wraps the existing indicator/odds/signal pipeline as the default analyzer."""
58
+
59
+ def analyze(self, market_data: dict, category: MarketCategory) -> CategorySignals:
60
+ prices: list[int] = market_data.get("prices", [])
61
+ trades = market_data.get("trades", [])
62
+ orderbook = market_data.get("orderbook")
63
+ estimated_prob: float = market_data.get("estimated_prob", 0.5)
64
+ ticker: str = market_data.get("ticker", "UNKNOWN")
65
+
66
+ signals: list[str] = []
67
+ data_sources: list[str] = []
68
+ confidence = 0.0
69
+
70
+ if len(prices) >= 2:
71
+ rsi_val = rsi(prices, period=14)
72
+ signals.append(f"RSI={rsi_val:.1f}")
73
+ data_sources.append("rsi")
74
+
75
+ bb = bollinger_bands(prices, period=20, k=2.0)
76
+ signals.append(f"BB=[{bb.lower},{bb.middle},{bb.upper}]")
77
+ data_sources.append("bollinger")
78
+
79
+ short = ema(prices, 5)
80
+ long = ema(prices, 20)
81
+ signals.append(f"EMA5={short:.1f},EMA20={long:.1f}")
82
+ data_sources.append("ema")
83
+
84
+ if orderbook is not None:
85
+ edge = detect_edge(estimated_prob, orderbook)
86
+ signals.append(f"edge={edge.edge:.3f},dir={edge.direction}")
87
+ data_sources.append("odds")
88
+
89
+ if prices and trades and orderbook is not None:
90
+ combined = generate_signal(ticker, prices, orderbook, estimated_prob)
91
+ confidence = combined.confidence
92
+ signals.append(f"combined={combined.direction},conf={combined.confidence:.2f}")
93
+ data_sources.append("signals")
94
+ else:
95
+ confidence = 0.3
96
+
97
+ return CategorySignals(
98
+ category=category,
99
+ signals=signals,
100
+ confidence=confidence,
101
+ data_sources=data_sources,
102
+ )