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.
- traderbot/__init__.py +13 -0
- traderbot/analysis/__init__.py +81 -0
- traderbot/analysis/indicators.py +120 -0
- traderbot/analysis/odds.py +110 -0
- traderbot/analysis/portfolio.py +138 -0
- traderbot/analysis/registry.py +102 -0
- traderbot/analysis/signals.py +195 -0
- traderbot/auth.py +369 -0
- traderbot/cli/__init__.py +502 -0
- traderbot/cli/admin.py +601 -0
- traderbot/cli/auth.py +315 -0
- traderbot/cli/cron.py +607 -0
- traderbot/cli/data.py +227 -0
- traderbot/cli/helpers.py +213 -0
- traderbot/cli/market.py +320 -0
- traderbot/cli/news.py +510 -0
- traderbot/cli/profile.py +1077 -0
- traderbot/cli/sandbox.py +87 -0
- traderbot/cli/trade.py +963 -0
- traderbot/cli/ws.py +105 -0
- traderbot/cli.py +2 -0
- traderbot/cron_loops.py +159 -0
- traderbot/data/__init__.py +25 -0
- traderbot/data/base_provider.py +64 -0
- traderbot/data/base_signals.py +38 -0
- traderbot/data/models.py +82 -0
- traderbot/data/registry.py +50 -0
- traderbot/data/weather/__init__.py +11 -0
- traderbot/data/weather/nws_client.py +267 -0
- traderbot/data/weather/provider.py +322 -0
- traderbot/data/weather/signals.py +238 -0
- traderbot/db/__init__.py +45 -0
- traderbot/db/decisions.py +165 -0
- traderbot/db/experiment_schema.py +86 -0
- traderbot/db/forecast_bias.py +133 -0
- traderbot/db/learnings.py +354 -0
- traderbot/db/positions.py +175 -0
- traderbot/db/reconciliation.py +163 -0
- traderbot/db/vectors.py +142 -0
- traderbot/experiment/cli.py +454 -0
- traderbot/experiment/harness.py +400 -0
- traderbot/experiment/methodologies/__init__.py +1 -0
- traderbot/experiment/methodologies/db_utils.py +49 -0
- traderbot/experiment/populate.py +246 -0
- traderbot/experiment/registry.py +35 -0
- traderbot/experiment/results.py +355 -0
- traderbot/experiment/shared.py +98 -0
- traderbot/experiment/tests/__init__.py +0 -0
- traderbot/experiment/tests/test_env_fallback.py +187 -0
- traderbot/experiment/tests/test_harness.py +70 -0
- traderbot/experiment/tests/test_installer.py +157 -0
- traderbot/experiment/tests/test_nws_client.py +211 -0
- traderbot/experiment/tests/test_registry.py +54 -0
- traderbot/experiment/tests/test_results.py +80 -0
- traderbot/experiment/tests/test_schema.py +59 -0
- traderbot/experiment/tests/test_shared.py +79 -0
- traderbot/experiment/tests/test_trade_routing.py +110 -0
- traderbot/experiment/tests/test_trading_v2.py +178 -0
- traderbot/experiment/tests/test_treatments.py +77 -0
- traderbot/experiment/tests/test_wal.py +81 -0
- traderbot/experiment/treatments/__init__.py +6 -0
- traderbot/experiment/treatments/calibration_bundle.py +158 -0
- traderbot/experiment/treatments/control.py +37 -0
- traderbot/fileops.py +46 -0
- traderbot/heartbeat.py +610 -0
- traderbot/kalshi/__init__.py +8 -0
- traderbot/kalshi/_normalize.py +134 -0
- traderbot/kalshi/cache.py +300 -0
- traderbot/kalshi/client.py +254 -0
- traderbot/kalshi/config.py +60 -0
- traderbot/kalshi/events.py +73 -0
- traderbot/kalshi/exchange.py +33 -0
- traderbot/kalshi/history.py +93 -0
- traderbot/kalshi/markets.py +493 -0
- traderbot/kalshi/models.py +353 -0
- traderbot/kalshi/pinning.py +98 -0
- traderbot/kalshi/portfolio.py +152 -0
- traderbot/kalshi/provider.py +268 -0
- traderbot/kalshi/rate_limit.py +47 -0
- traderbot/kalshi/signing.py +117 -0
- traderbot/kalshi/trading.py +140 -0
- traderbot/kalshi/websocket.py +180 -0
- traderbot/kalshi/ws_cache.py +64 -0
- traderbot/kalshi/ws_daemon.py +287 -0
- traderbot/learning.py +346 -0
- traderbot/llm/__init__.py +6 -0
- traderbot/llm/client.py +82 -0
- traderbot/llm/ollama.py +75 -0
- traderbot/logging_config.py +75 -0
- traderbot/master_password.py +285 -0
- traderbot/news/__init__.py +32 -0
- traderbot/news/cache_paths.py +42 -0
- traderbot/news/classifier.py +449 -0
- traderbot/news/embeddings.py +257 -0
- traderbot/news/impact_assessor.py +290 -0
- traderbot/news/ingest.py +932 -0
- traderbot/news/models.py +115 -0
- traderbot/news/sentiment_scorer.py +129 -0
- traderbot/news/sources.py +2155 -0
- traderbot/paper.py +80 -0
- traderbot/paths.py +108 -0
- traderbot/platform_compat.py +195 -0
- traderbot/profiles/__init__.py +24 -0
- traderbot/profiles/auth.py +182 -0
- traderbot/profiles/config.py +265 -0
- traderbot/profiles/discovery.py +218 -0
- traderbot/profiles/injection.py +155 -0
- traderbot/profiles/injection_strategies.py +245 -0
- traderbot/profiles/isolation.py +116 -0
- traderbot/profiles/models.py +73 -0
- traderbot/profiles/openclaw_config.py +257 -0
- traderbot/profiles/registry.py +202 -0
- traderbot/profiles/runtime.py +157 -0
- traderbot/profiles/sysadmin.py +25 -0
- traderbot/profiles/tokens.py +207 -0
- traderbot/risk/__init__.py +139 -0
- traderbot/risk/agent_limits.py +97 -0
- traderbot/risk/audit.py +88 -0
- traderbot/risk/circuit_breaker.py +167 -0
- traderbot/risk/limits.py +169 -0
- traderbot/risk/sizing.py +45 -0
- traderbot/sandbox.py +248 -0
- traderbot/simulation/__init__.py +135 -0
- traderbot/simulation/adaptation.py +756 -0
- traderbot/simulation/adapter_state.py +136 -0
- traderbot/simulation/data_loader.py +242 -0
- traderbot/simulation/engine.py +433 -0
- traderbot/simulation/paper_trader.py +425 -0
- traderbot/simulation/performance.py +232 -0
- traderbot/simulation/profiles.py +159 -0
- traderbot/simulation/settlement.py +226 -0
- traderbot/simulation/strategies/__init__.py +110 -0
- traderbot/update_config.py +39 -0
- traderbot/updater.py +181 -0
- traderbot/wal.py +402 -0
- traderbot/windows_service.py +318 -0
- traderbot-0.14.72.dist-info/METADATA +180 -0
- traderbot-0.14.72.dist-info/RECORD +141 -0
- traderbot-0.14.72.dist-info/WHEEL +4 -0
- traderbot-0.14.72.dist-info/entry_points.txt +2 -0
- 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
|
+
)
|