alloc-context 0.1.0__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 (85) hide show
  1. alloc_context-0.1.0.dist-info/METADATA +154 -0
  2. alloc_context-0.1.0.dist-info/RECORD +85 -0
  3. alloc_context-0.1.0.dist-info/WHEEL +5 -0
  4. alloc_context-0.1.0.dist-info/entry_points.txt +4 -0
  5. alloc_context-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. alloc_context-0.1.0.dist-info/top_level.txt +1 -0
  7. alloccontext/__init__.py +3 -0
  8. alloccontext/__main__.py +149 -0
  9. alloccontext/config.py +415 -0
  10. alloccontext/horizon.py +30 -0
  11. alloccontext/ingest/__init__.py +1 -0
  12. alloccontext/ingest/cf_benchmarks.py +38 -0
  13. alloccontext/ingest/cf_history.py +65 -0
  14. alloccontext/ingest/coinbase_client.py +234 -0
  15. alloccontext/ingest/coinbase_portfolio.py +53 -0
  16. alloccontext/ingest/coingecko.py +148 -0
  17. alloccontext/ingest/coinmarketcap.py +135 -0
  18. alloccontext/ingest/env_keys.py +12 -0
  19. alloccontext/ingest/etf_flows.py +282 -0
  20. alloccontext/ingest/exchange/__init__.py +4 -0
  21. alloccontext/ingest/exchange/coinbase_adapter.py +64 -0
  22. alloccontext/ingest/exchange/kraken_adapter.py +66 -0
  23. alloccontext/ingest/exchange/live.py +95 -0
  24. alloccontext/ingest/exchange/portfolio.py +8 -0
  25. alloccontext/ingest/exchange/registry.py +27 -0
  26. alloccontext/ingest/exchange/types.py +5 -0
  27. alloccontext/ingest/exchange_http.py +28 -0
  28. alloccontext/ingest/fear_greed.py +89 -0
  29. alloccontext/ingest/fred.py +138 -0
  30. alloccontext/ingest/http_errors.py +29 -0
  31. alloccontext/ingest/kalshi.py +84 -0
  32. alloccontext/ingest/kalshi_api.py +199 -0
  33. alloccontext/ingest/kalshi_client.py +95 -0
  34. alloccontext/ingest/kalshi_files.py +44 -0
  35. alloccontext/ingest/kalshi_state.py +67 -0
  36. alloccontext/ingest/kraken_client.py +177 -0
  37. alloccontext/ingest/kraken_portfolio.py +161 -0
  38. alloccontext/ingest/macro_calendar.py +310 -0
  39. alloccontext/ingest/macro_normalize.py +98 -0
  40. alloccontext/ingest/market_snapshots.py +113 -0
  41. alloccontext/ingest/outcome.py +110 -0
  42. alloccontext/ingest/parse_helpers.py +23 -0
  43. alloccontext/ingest/runner.py +148 -0
  44. alloccontext/mcp/__init__.py +1 -0
  45. alloccontext/mcp/assets.py +153 -0
  46. alloccontext/mcp/bazaar.py +630 -0
  47. alloccontext/mcp/contracts.py +286 -0
  48. alloccontext/mcp/handlers.py +487 -0
  49. alloccontext/mcp/http.py +250 -0
  50. alloccontext/mcp/payment_middleware.py +211 -0
  51. alloccontext/mcp/server.py +319 -0
  52. alloccontext/mcp/staleness.py +30 -0
  53. alloccontext/mcp/validation.py +56 -0
  54. alloccontext/mcp/x402_bazaar_dynamic.py +104 -0
  55. alloccontext/mcp/x402_config.py +131 -0
  56. alloccontext/mcp/x402_pricing.py +55 -0
  57. alloccontext/mcp/x402_stables.py +179 -0
  58. alloccontext/rollup/__init__.py +1 -0
  59. alloccontext/rollup/band.py +50 -0
  60. alloccontext/rollup/breadth.py +45 -0
  61. alloccontext/rollup/cf_math.py +103 -0
  62. alloccontext/rollup/cluster.py +149 -0
  63. alloccontext/rollup/cluster_config.py +86 -0
  64. alloccontext/rollup/comparison.py +67 -0
  65. alloccontext/rollup/context.py +118 -0
  66. alloccontext/rollup/delta.py +109 -0
  67. alloccontext/rollup/etf.py +113 -0
  68. alloccontext/rollup/fear_greed.py +61 -0
  69. alloccontext/rollup/macro.py +185 -0
  70. alloccontext/rollup/portfolio.py +137 -0
  71. alloccontext/rollup/rebalance.py +125 -0
  72. alloccontext/rollup/regime.py +188 -0
  73. alloccontext/rollup/sentiment.py +118 -0
  74. alloccontext/rollup/snapshots.py +64 -0
  75. alloccontext/rollup/tape.py +176 -0
  76. alloccontext/status_report.py +321 -0
  77. alloccontext/store/__init__.py +0 -0
  78. alloccontext/store/db.py +216 -0
  79. alloccontext/store/jsonutil.py +10 -0
  80. alloccontext/store/meta.py +20 -0
  81. alloccontext/store/retention.py +63 -0
  82. alloccontext/store/status.py +89 -0
  83. alloccontext/timeutil.py +11 -0
  84. alloccontext/x402_production_check.py +193 -0
  85. alloccontext/x402_smoke_redact.py +41 -0
@@ -0,0 +1,138 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import sqlite3
6
+ import urllib.error
7
+ import urllib.parse
8
+ import urllib.request
9
+ from datetime import date, datetime, timedelta, timezone
10
+ from typing import Any
11
+
12
+ FRED_OBSERVATIONS_URL = "https://api.stlouisfed.org/fred/series/observations"
13
+
14
+
15
+ def fetch_series_observations(
16
+ *,
17
+ series_id: str,
18
+ api_key: str,
19
+ observation_start: date,
20
+ observation_end: date,
21
+ timeout: float,
22
+ ) -> list[dict[str, Any]]:
23
+ params = urllib.parse.urlencode(
24
+ {
25
+ "series_id": series_id,
26
+ "api_key": api_key,
27
+ "file_type": "json",
28
+ "observation_start": observation_start.isoformat(),
29
+ "observation_end": observation_end.isoformat(),
30
+ "sort_order": "asc",
31
+ }
32
+ )
33
+ url = f"{FRED_OBSERVATIONS_URL}?{params}"
34
+ request = urllib.request.Request(url, headers={"User-Agent": "alloc-context/0.1"})
35
+ try:
36
+ with urllib.request.urlopen(request, timeout=timeout) as response:
37
+ payload = json.loads(response.read().decode("utf-8"))
38
+ except urllib.error.HTTPError as exc:
39
+ from alloccontext.ingest.http_errors import http_error_message
40
+
41
+ raise ValueError(
42
+ http_error_message(exc, context=f"fred series {series_id}")
43
+ ) from exc
44
+ if not isinstance(payload, dict):
45
+ raise ValueError(f"invalid FRED payload for {series_id}")
46
+ rows = payload.get("observations") or []
47
+ if not isinstance(rows, list):
48
+ raise ValueError(f"invalid FRED observations for {series_id}")
49
+ return [row for row in rows if isinstance(row, dict)]
50
+
51
+
52
+ def _parse_observation(row: dict[str, Any]) -> tuple[str, float | None] | None:
53
+ obs_date = str(row.get("date") or "").strip()
54
+ if not obs_date:
55
+ return None
56
+ raw_value = row.get("value")
57
+ if raw_value is None or raw_value == ".":
58
+ return obs_date, None
59
+ try:
60
+ return obs_date, float(raw_value)
61
+ except (TypeError, ValueError):
62
+ return obs_date, None
63
+
64
+
65
+ def upsert_fred_observations(
66
+ conn: sqlite3.Connection,
67
+ *,
68
+ series_id: str,
69
+ observations: list[dict[str, Any]],
70
+ ) -> int:
71
+ fetched_at = datetime.now(timezone.utc).replace(microsecond=0).isoformat()
72
+ count = 0
73
+ for row in observations:
74
+ parsed = _parse_observation(row)
75
+ if parsed is None:
76
+ continue
77
+ obs_date, value = parsed
78
+ conn.execute(
79
+ """
80
+ INSERT INTO fred_observations(series_id, obs_date, value, fetched_at)
81
+ VALUES (?, ?, ?, ?)
82
+ ON CONFLICT(series_id, obs_date) DO UPDATE SET
83
+ value=excluded.value,
84
+ fetched_at=excluded.fetched_at
85
+ """,
86
+ (series_id, obs_date, value, fetched_at),
87
+ )
88
+ count += 1
89
+ return count
90
+
91
+
92
+ def refresh_fred(conn: sqlite3.Connection, config) -> dict[str, Any]:
93
+ api_key = os.environ.get("FRED_API_KEY")
94
+ if not api_key:
95
+ return {
96
+ "ok": True,
97
+ "rows": 0,
98
+ "skipped": True,
99
+ "reason": "FRED_API_KEY not set",
100
+ }
101
+
102
+ fred = config.fred
103
+ if not fred.series:
104
+ return {
105
+ "ok": True,
106
+ "rows": 0,
107
+ "skipped": True,
108
+ "reason": "no_fred_series_configured",
109
+ }
110
+
111
+ today = datetime.now(timezone.utc).date()
112
+ start = today - timedelta(days=fred.lookback_days)
113
+ total = 0
114
+ series_ids: list[str] = []
115
+
116
+ for spec in fred.series:
117
+ try:
118
+ observations = fetch_series_observations(
119
+ series_id=spec.id,
120
+ api_key=api_key,
121
+ observation_start=start,
122
+ observation_end=today,
123
+ timeout=fred.timeout_seconds,
124
+ )
125
+ total += upsert_fred_observations(conn, series_id=spec.id, observations=observations)
126
+ series_ids.append(spec.id)
127
+ except (
128
+ urllib.error.URLError,
129
+ TimeoutError,
130
+ ValueError,
131
+ json.JSONDecodeError,
132
+ RuntimeError,
133
+ ) as exc:
134
+ conn.rollback()
135
+ return {"ok": False, "error": f"{spec.id}: {exc}", "rows": 0}
136
+
137
+ conn.commit()
138
+ return {"ok": True, "rows": total, "series": series_ids}
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ import urllib.error
4
+ from urllib.parse import parse_qsl, urlencode, urlparse, urlunparse
5
+
6
+ _REDACTED_PARAMS = frozenset(
7
+ {
8
+ "token",
9
+ "apikey",
10
+ "api_key",
11
+ }
12
+ )
13
+
14
+
15
+ def redact_url_secrets(url: str) -> str:
16
+ """Remove common API key query params before logging or error text."""
17
+ parsed = urlparse(url)
18
+ if not parsed.query:
19
+ return url
20
+ query = [
21
+ (key, "***" if key.lower() in _REDACTED_PARAMS else value)
22
+ for key, value in parse_qsl(parsed.query, keep_blank_values=True)
23
+ ]
24
+ return urlunparse(parsed._replace(query=urlencode(query)))
25
+
26
+
27
+ def http_error_message(exc: urllib.error.HTTPError, *, context: str) -> str:
28
+ """HTTP failure text without embedding request URLs or secrets."""
29
+ return f"{context} HTTP {exc.code}"
@@ -0,0 +1,84 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from typing import Any
6
+
7
+ from alloccontext.ingest.kalshi_files import load_tactical_snapshot
8
+ from alloccontext.ingest.kalshi_state import (
9
+ extract_cf_price_history,
10
+ extract_market_quotes_from_state,
11
+ load_state_json,
12
+ tactical_to_storage,
13
+ )
14
+ from alloccontext.store.meta import set_meta
15
+
16
+
17
+ def upsert_kalshi_snapshot(conn: sqlite3.Connection, row: dict[str, Any]) -> None:
18
+ conn.execute(
19
+ """
20
+ INSERT INTO kalshi_snapshots(ts, tape_summary, cluster_json, raw_json)
21
+ VALUES (?, ?, ?, ?)
22
+ """,
23
+ (
24
+ str(row["ts"]),
25
+ row.get("tape_summary"),
26
+ row.get("cluster_json"),
27
+ row.get("raw_json"),
28
+ ),
29
+ )
30
+
31
+
32
+ def _refresh_kalshi_files(conn: sqlite3.Connection, config) -> dict[str, Any]:
33
+ tactical_path = config.kalshi.fallback_tactical_snapshot
34
+ if tactical_path is None or not tactical_path.is_file():
35
+ return {"ok": False, "error": "missing_kalshi_tactical_snapshot", "rows": 0}
36
+
37
+ raw = load_state_json(tactical_path)
38
+ if raw is None:
39
+ return {"ok": False, "error": "invalid_tactical_snapshot", "rows": 0}
40
+
41
+ snapshot = load_tactical_snapshot(tactical_path)
42
+ if snapshot is None:
43
+ return {"ok": False, "error": "invalid_tactical_snapshot", "rows": 0}
44
+
45
+ storage = tactical_to_storage(snapshot, raw)
46
+ upsert_kalshi_snapshot(conn, storage)
47
+
48
+ state_path = config.kalshi.fallback_state
49
+ if state_path is not None and state_path.is_file():
50
+ state = load_state_json(state_path)
51
+ if state:
52
+ cf = extract_cf_price_history(state)
53
+ if cf:
54
+ set_meta(conn, "cf_price_history", json.dumps(cf))
55
+ quotes = extract_market_quotes_from_state(state)
56
+ if quotes:
57
+ set_meta(conn, "kalshi_markets", json.dumps(quotes))
58
+
59
+ conn.commit()
60
+ return {
61
+ "ok": True,
62
+ "rows": 1,
63
+ "ts": storage["ts"],
64
+ "tape_summary": storage.get("tape_summary"),
65
+ "source": "kalshi_files",
66
+ }
67
+
68
+
69
+ def refresh_kalshi(conn: sqlite3.Connection, config) -> dict[str, Any]:
70
+ if config.kalshi.use_api:
71
+ from alloccontext.ingest.kalshi_api import refresh_kalshi_api
72
+
73
+ result = refresh_kalshi_api(conn, config)
74
+ if result.get("ok"):
75
+ return result
76
+ fallback = config.kalshi.fallback_tactical_snapshot
77
+ if fallback is not None and fallback.is_file():
78
+ file_result = _refresh_kalshi_files(conn, config)
79
+ if file_result.get("ok"):
80
+ file_result["api_error"] = result.get("error")
81
+ return file_result
82
+ return result
83
+
84
+ return _refresh_kalshi_files(conn, config)
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import sqlite3
5
+ from datetime import datetime, timezone
6
+ from typing import Any
7
+
8
+ from alloccontext.ingest.cf_benchmarks import CFBenchmarksPriceError, fetch_prices
9
+ from alloccontext.ingest.cf_history import load_cf_history, record_cf_price_samples, save_cf_history
10
+ from alloccontext.ingest.kalshi_client import KalshiAPIError, KalshiClient, no_ask_cents_from_row, price_cents_from_row
11
+ from alloccontext.ingest.kalshi_state import tactical_to_storage
12
+ from alloccontext.rollup.cluster import MarketQuote, sentiment_up_fraction
13
+ from alloccontext.rollup.cluster_config import RollupConfig
14
+ from alloccontext.rollup.tape import build_live_tape_context, format_tape_summary
15
+ from alloccontext.store.meta import set_meta
16
+
17
+
18
+ def _series_tickers(config) -> list[str]:
19
+ return [row.series for row in config.kalshi.series if row.series]
20
+
21
+
22
+ def _cf_indices(config) -> list[str]:
23
+ seen: set[str] = set()
24
+ indices: list[str] = []
25
+ for row in config.kalshi.series:
26
+ if row.cf_index and row.cf_index not in seen:
27
+ seen.add(row.cf_index)
28
+ indices.append(row.cf_index)
29
+ return indices
30
+
31
+
32
+ def fetch_series_market_quotes(
33
+ client: KalshiClient, series_tickers: list[str]
34
+ ) -> list[MarketQuote]:
35
+ quotes: list[MarketQuote] = []
36
+ seen: set[str] = set()
37
+ for series in series_tickers:
38
+ payload = client.get_markets(status="open", limit=100, series_ticker=series)
39
+ for row in payload.get("markets") or []:
40
+ if not isinstance(row, dict):
41
+ continue
42
+ ticker = str(row.get("ticker") or "")
43
+ if not ticker or ticker in seen:
44
+ continue
45
+ if not ticker.upper().startswith(series.upper()):
46
+ continue
47
+ yes_bid, yes_ask = price_cents_from_row(row)
48
+ no_ask = no_ask_cents_from_row(row)
49
+ if yes_ask is None and no_ask is None:
50
+ continue
51
+ quotes.append(
52
+ MarketQuote(
53
+ ticker=ticker,
54
+ yes_ask_cents=yes_ask if yes_ask is not None else yes_bid,
55
+ no_ask_cents=no_ask,
56
+ )
57
+ )
58
+ seen.add(ticker)
59
+ return quotes
60
+
61
+
62
+ def _markets_meta_rows(markets: list[MarketQuote]) -> list[dict[str, Any]]:
63
+ return [
64
+ {
65
+ "ticker": market.ticker,
66
+ "yes_ask_cents": market.yes_ask_cents,
67
+ "no_ask_cents": market.no_ask_cents,
68
+ }
69
+ for market in markets
70
+ ]
71
+
72
+
73
+ def build_api_tactical_payload(
74
+ rollup: RollupConfig,
75
+ *,
76
+ cf_history: dict[str, list[dict[str, Any]]],
77
+ markets: list[MarketQuote],
78
+ now: datetime,
79
+ ) -> dict[str, Any]:
80
+ computed = build_live_tape_context(
81
+ rollup,
82
+ cf_history=cf_history or None,
83
+ markets=markets,
84
+ now=now,
85
+ )
86
+ if computed is not None:
87
+ tape_ctx, cluster_ctx, at = computed
88
+ return {
89
+ "at": at.astimezone(timezone.utc).replace(microsecond=0).isoformat(),
90
+ "tape_summary": tape_ctx.get("summary"),
91
+ "trend_by_asset_60m": tape_ctx.get("trend_60m_pct") or {},
92
+ "trend_by_asset_15m": tape_ctx.get("trend_15m_pct") or {},
93
+ "volatility_regime": tape_ctx.get("volatility_regime"),
94
+ "cluster": cluster_ctx,
95
+ "daily_stats": {},
96
+ "source": "kalshi_api",
97
+ }
98
+
99
+ sentiment_up_frac, sentiment_sample = sentiment_up_fraction(markets)
100
+ cluster: dict[str, Any] = {}
101
+ if sentiment_sample:
102
+ cluster["sentiment_sample"] = sentiment_sample
103
+ if sentiment_up_frac is not None:
104
+ cluster["sentiment_up_frac"] = round(sentiment_up_frac, 4)
105
+
106
+ return {
107
+ "at": now.astimezone(timezone.utc).replace(microsecond=0).isoformat(),
108
+ "tape_summary": format_tape_summary(
109
+ trend_60m={},
110
+ vol_regime=None,
111
+ sentiment_up_frac=sentiment_up_frac,
112
+ ),
113
+ "trend_by_asset_60m": {},
114
+ "trend_by_asset_15m": {},
115
+ "volatility_regime": None,
116
+ "cluster": cluster,
117
+ "daily_stats": {},
118
+ "source": "kalshi_api",
119
+ }
120
+
121
+
122
+ def refresh_kalshi_api(conn: sqlite3.Connection, config) -> dict[str, Any]:
123
+ kalshi = config.kalshi
124
+ series = _series_tickers(config)
125
+ if not series:
126
+ return {"ok": False, "error": "no_kalshi_series_configured", "rows": 0}
127
+
128
+ now = datetime.now(timezone.utc)
129
+ client = KalshiClient(kalshi.base_url, timeout=kalshi.timeout_seconds)
130
+
131
+ try:
132
+ markets = fetch_series_market_quotes(client, series)
133
+ except KalshiAPIError as exc:
134
+ return {"ok": False, "error": str(exc), "rows": 0}
135
+
136
+ cf_history = load_cf_history(conn)
137
+ indices = _cf_indices(config)
138
+ cf_errors: dict[str, str] = {}
139
+ if indices:
140
+ try:
141
+ prices = fetch_prices(indices, timeout=kalshi.timeout_seconds)
142
+ cf_history = record_cf_price_samples(
143
+ cf_history,
144
+ prices,
145
+ now,
146
+ max_age_minutes=float(kalshi.cf_history_max_age_minutes),
147
+ )
148
+ save_cf_history(conn, cf_history)
149
+ except CFBenchmarksPriceError as exc:
150
+ cf_errors["cf_benchmarks"] = str(exc)
151
+
152
+ if markets:
153
+ set_meta(conn, "kalshi_markets", json.dumps(_markets_meta_rows(markets)))
154
+
155
+ payload = build_api_tactical_payload(
156
+ config.rollup,
157
+ cf_history=cf_history,
158
+ markets=markets,
159
+ now=now,
160
+ )
161
+ if not markets and not cf_history:
162
+ conn.rollback()
163
+ return {
164
+ "ok": False,
165
+ "error": "no_kalshi_markets_or_cf_history",
166
+ "rows": 0,
167
+ "feed_errors": cf_errors,
168
+ }
169
+
170
+ from alloccontext.ingest.kalshi_files import KalshiTacticalSnapshot
171
+
172
+ snapshot = KalshiTacticalSnapshot(
173
+ at=payload.get("at"),
174
+ tape_summary=payload.get("tape_summary"),
175
+ trend_by_asset_60m=dict(payload.get("trend_by_asset_60m") or {}),
176
+ trend_by_asset_15m=dict(payload.get("trend_by_asset_15m") or {}),
177
+ volatility_regime=payload.get("volatility_regime"),
178
+ sentiment_up_frac=(payload.get("cluster") or {}).get("sentiment_up_frac"),
179
+ sentiment_sample=(payload.get("cluster") or {}).get("sentiment_sample"),
180
+ daily_stats=dict(payload.get("daily_stats") or {}),
181
+ )
182
+ storage = tactical_to_storage(snapshot, payload)
183
+
184
+ from alloccontext.ingest.kalshi import upsert_kalshi_snapshot
185
+
186
+ upsert_kalshi_snapshot(conn, storage)
187
+
188
+ result: dict[str, Any] = {
189
+ "ok": True,
190
+ "rows": 1,
191
+ "ts": storage["ts"],
192
+ "tape_summary": storage.get("tape_summary"),
193
+ "markets_sampled": len(markets),
194
+ "source": "kalshi_api",
195
+ }
196
+ if cf_errors:
197
+ result["feed_errors"] = cf_errors
198
+ conn.commit()
199
+ return result
@@ -0,0 +1,95 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ import urllib.error
6
+ import urllib.parse
7
+ import urllib.request
8
+ from typing import Any
9
+
10
+
11
+ class KalshiAPIError(RuntimeError):
12
+ def __init__(self, message: str, status_code: int | None = None):
13
+ super().__init__(message)
14
+ self.status_code = status_code
15
+
16
+
17
+ class KalshiClient:
18
+ """Read-only Kalshi REST client (public market endpoints)."""
19
+
20
+ def __init__(self, base_url: str, *, timeout: float = 20.0):
21
+ self.base_url = base_url.rstrip("/")
22
+ self.timeout = timeout
23
+
24
+ def _request(self, method: str, path: str, *, params: dict[str, Any] | None = None) -> dict[str, Any]:
25
+ if not path.startswith("/"):
26
+ path = f"/{path}"
27
+ query = f"?{urllib.parse.urlencode(params)}" if params else ""
28
+ url = f"{self.base_url}{path}{query}"
29
+ request = urllib.request.Request(
30
+ url,
31
+ headers={"Accept": "application/json", "User-Agent": "alloc-context/0.1"},
32
+ method=method.upper(),
33
+ )
34
+ for attempt in range(4):
35
+ try:
36
+ with urllib.request.urlopen(request, timeout=self.timeout) as response:
37
+ body = response.read().decode("utf-8")
38
+ except urllib.error.HTTPError as exc:
39
+ if exc.code == 429 and attempt < 3:
40
+ time.sleep(2**attempt)
41
+ continue
42
+ raise KalshiAPIError(
43
+ f"Kalshi API {method.upper()} {path} failed: {exc.code}",
44
+ status_code=exc.code,
45
+ ) from exc
46
+ except (urllib.error.URLError, TimeoutError) as exc:
47
+ raise KalshiAPIError(f"Kalshi API {method.upper()} {path} failed: {exc}") from exc
48
+ break
49
+ if not body:
50
+ return {}
51
+ parsed = json.loads(body)
52
+ return parsed if isinstance(parsed, dict) else {}
53
+
54
+ def get_markets(
55
+ self,
56
+ *,
57
+ status: str | None = None,
58
+ limit: int = 100,
59
+ cursor: str | None = None,
60
+ series_ticker: str | None = None,
61
+ ) -> dict[str, Any]:
62
+ params: dict[str, Any] = {"limit": limit}
63
+ if status:
64
+ params["status"] = status
65
+ if cursor:
66
+ params["cursor"] = cursor
67
+ if series_ticker:
68
+ params["series_ticker"] = series_ticker
69
+ return self._request("GET", "/markets", params=params)
70
+
71
+
72
+ def dollars_to_cents(value: str | int | float | None) -> int | None:
73
+ if value is None or value == "":
74
+ return None
75
+ return max(1, min(99, round(float(value) * 100)))
76
+
77
+
78
+ def price_cents_from_row(row: dict[str, Any]) -> tuple[int | None, int | None]:
79
+ if row.get("yes_bid") is not None or row.get("yes_ask") is not None:
80
+ bid = row.get("yes_bid")
81
+ ask = row.get("yes_ask")
82
+ return (
83
+ int(bid) if bid is not None else None,
84
+ int(ask) if ask is not None else None,
85
+ )
86
+ return (
87
+ dollars_to_cents(row.get("yes_bid_dollars")),
88
+ dollars_to_cents(row.get("yes_ask_dollars")),
89
+ )
90
+
91
+
92
+ def no_ask_cents_from_row(row: dict[str, Any]) -> int | None:
93
+ if row.get("no_ask") is not None:
94
+ return int(row["no_ask"])
95
+ return dollars_to_cents(row.get("no_ask_dollars"))
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class KalshiTacticalSnapshot:
11
+ at: str | None
12
+ tape_summary: str | None
13
+ trend_by_asset_60m: dict[str, float | None]
14
+ trend_by_asset_15m: dict[str, float | None]
15
+ volatility_regime: str | None
16
+ sentiment_up_frac: float | None
17
+ sentiment_sample: int | None
18
+ daily_stats: dict[str, Any]
19
+
20
+
21
+ def _read_json(path: Path) -> Any | None:
22
+ if not path.exists():
23
+ return None
24
+ try:
25
+ return json.loads(path.read_text(encoding="utf-8"))
26
+ except (json.JSONDecodeError, OSError):
27
+ return None
28
+
29
+
30
+ def load_tactical_snapshot(path: Path) -> KalshiTacticalSnapshot | None:
31
+ raw = _read_json(path)
32
+ if not isinstance(raw, dict):
33
+ return None
34
+ cluster = raw.get("cluster") if isinstance(raw.get("cluster"), dict) else {}
35
+ return KalshiTacticalSnapshot(
36
+ at=raw.get("at"),
37
+ tape_summary=raw.get("tape_summary"),
38
+ trend_by_asset_60m=dict(raw.get("trend_by_asset_60m") or {}),
39
+ trend_by_asset_15m=dict(raw.get("trend_by_asset_15m") or {}),
40
+ volatility_regime=raw.get("volatility_regime"),
41
+ sentiment_up_frac=cluster.get("sentiment_up_frac"),
42
+ sentiment_sample=cluster.get("sentiment_sample"),
43
+ daily_stats=dict(raw.get("daily_stats") or {}),
44
+ )
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from alloccontext.ingest.kalshi_files import KalshiTacticalSnapshot, load_tactical_snapshot
8
+
9
+
10
+ def extract_cf_price_history(raw: dict[str, Any]) -> dict[str, Any]:
11
+ history = raw.get("cf_price_history")
12
+ if isinstance(history, dict):
13
+ return history
14
+ return {}
15
+
16
+
17
+ def extract_market_quotes_from_state(raw: dict[str, Any]) -> list[dict[str, Any]]:
18
+ by_ticker: dict[str, dict[str, Any]] = {}
19
+ for source in ("opportunity_log_expiry", "journal_expiry"):
20
+ for entry in raw.get(source) or []:
21
+ ticker = entry.get("ticker")
22
+ if not ticker:
23
+ continue
24
+ upper = str(ticker).upper()
25
+ if not any(tag in upper for tag in ("BTCD", "ETHD", "15M")):
26
+ continue
27
+ yes_ask = entry.get("yes_ask_cents")
28
+ no_ask = entry.get("no_ask_cents")
29
+ if yes_ask is None and no_ask is None:
30
+ continue
31
+ ts = str(entry.get("at") or "")
32
+ existing = by_ticker.get(str(ticker))
33
+ if existing is None or ts >= str(existing.get("at") or ""):
34
+ by_ticker[str(ticker)] = {
35
+ "ticker": str(ticker),
36
+ "yes_ask_cents": yes_ask,
37
+ "no_ask_cents": no_ask,
38
+ "at": ts,
39
+ }
40
+ return [
41
+ {
42
+ "ticker": row["ticker"],
43
+ "yes_ask_cents": row.get("yes_ask_cents"),
44
+ "no_ask_cents": row.get("no_ask_cents"),
45
+ }
46
+ for row in by_ticker.values()
47
+ ]
48
+
49
+
50
+ def load_state_json(path: Path) -> dict[str, Any] | None:
51
+ if not path.is_file():
52
+ return None
53
+ try:
54
+ raw = json.loads(path.read_text(encoding="utf-8"))
55
+ except (json.JSONDecodeError, OSError):
56
+ return None
57
+ return raw if isinstance(raw, dict) else None
58
+
59
+
60
+ def tactical_to_storage(snapshot: KalshiTacticalSnapshot, raw: dict[str, Any]) -> dict[str, Any]:
61
+ cluster = raw.get("cluster") if isinstance(raw.get("cluster"), dict) else {}
62
+ return {
63
+ "ts": snapshot.at or raw.get("at"),
64
+ "tape_summary": snapshot.tape_summary,
65
+ "cluster_json": json.dumps(cluster),
66
+ "raw_json": json.dumps(raw),
67
+ }