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,286 @@
1
+ """Stable required-key contracts for MCP tool JSON responses."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from alloccontext.mcp.bazaar import mcp_tool_specs
8
+
9
+ STALENESS_KEYS = ("as_of", "age_seconds")
10
+
11
+ PORTFOLIO_AVAILABLE_KEYS = (
12
+ "available",
13
+ "nav_usd",
14
+ "cash_usd",
15
+ "allocation_pct",
16
+ "target_allocation_pct",
17
+ "drift",
18
+ "rebalance_hint",
19
+ "outside_band",
20
+ "max_drift",
21
+ "band",
22
+ )
23
+
24
+ REGIME_AVAILABLE_KEYS = (
25
+ "available",
26
+ "summary",
27
+ "hints",
28
+ "allocation",
29
+ )
30
+
31
+ MCP_TOOL_NAMES = frozenset(spec["tool_name"] for spec in mcp_tool_specs())
32
+
33
+
34
+ def _missing_keys(payload: dict[str, Any], required: tuple[str, ...]) -> list[str]:
35
+ return [key for key in required if key not in payload]
36
+
37
+
38
+ def assert_has_keys(
39
+ payload: dict[str, Any],
40
+ required: tuple[str, ...],
41
+ *,
42
+ label: str,
43
+ ) -> None:
44
+ missing = _missing_keys(payload, required)
45
+ if missing:
46
+ joined = ", ".join(missing)
47
+ raise ValueError(f"{label} missing keys: {joined}")
48
+
49
+
50
+ def assert_available_block(
51
+ block: dict[str, Any],
52
+ *,
53
+ label: str,
54
+ when_available: tuple[str, ...] = (),
55
+ when_unavailable: tuple[str, ...] = ("available",),
56
+ ) -> None:
57
+ assert_has_keys(block, ("available",), label=label)
58
+ if block.get("available"):
59
+ if when_available:
60
+ assert_has_keys(block, when_available, label=f"{label} (available)")
61
+ else:
62
+ assert_has_keys(block, when_unavailable, label=f"{label} (unavailable)")
63
+
64
+
65
+ LIVE_INGEST_UNAVAILABLE_KEYS = (
66
+ "available",
67
+ "reason",
68
+ "fatal_errors",
69
+ "ingest",
70
+ "freshness",
71
+ *STALENESS_KEYS,
72
+ )
73
+
74
+
75
+ def validate_context_bundle(payload: dict[str, Any]) -> None:
76
+ if payload.get("available") is False:
77
+ assert_has_keys(
78
+ payload,
79
+ LIVE_INGEST_UNAVAILABLE_KEYS,
80
+ label="get_context_bundle",
81
+ )
82
+ return
83
+ assert_has_keys(
84
+ payload,
85
+ (
86
+ "bundle_id",
87
+ "scope",
88
+ "as_of",
89
+ "horizon_days",
90
+ "portfolio",
91
+ "market",
92
+ "sentiment",
93
+ "macro",
94
+ "delta",
95
+ "regime",
96
+ "freshness",
97
+ *STALENESS_KEYS,
98
+ ),
99
+ label="get_context_bundle",
100
+ )
101
+ assert_available_block(
102
+ payload["portfolio"],
103
+ label="portfolio",
104
+ when_available=PORTFOLIO_AVAILABLE_KEYS,
105
+ )
106
+ assert_available_block(
107
+ payload["regime"],
108
+ label="regime",
109
+ when_available=REGIME_AVAILABLE_KEYS,
110
+ )
111
+
112
+
113
+ def validate_market_context(payload: dict[str, Any]) -> None:
114
+ if payload.get("available") is False:
115
+ assert_has_keys(
116
+ payload,
117
+ LIVE_INGEST_UNAVAILABLE_KEYS,
118
+ label="get_market_context",
119
+ )
120
+ return
121
+ assert_has_keys(
122
+ payload,
123
+ (
124
+ "scope",
125
+ "freshness",
126
+ "market",
127
+ "sentiment",
128
+ "macro",
129
+ "etf",
130
+ "breadth",
131
+ *STALENESS_KEYS,
132
+ ),
133
+ label="get_market_context",
134
+ )
135
+ for name in ("market", "sentiment", "macro", "etf", "breadth"):
136
+ assert_available_block(payload[name], label=name)
137
+
138
+
139
+ def validate_rebalance_plan(payload: dict[str, Any]) -> None:
140
+ assert_has_keys(payload, (*STALENESS_KEYS,), label="get_rebalance_plan")
141
+ if payload.get("available") is False:
142
+ assert_has_keys(payload, ("available", "reason"), label="get_rebalance_plan")
143
+ return
144
+ assert_has_keys(
145
+ payload,
146
+ (
147
+ "available",
148
+ "exchange",
149
+ "nav_usd",
150
+ "allocation_pct",
151
+ "target_pct",
152
+ "moves",
153
+ "delta_usd",
154
+ *STALENESS_KEYS,
155
+ ),
156
+ label="get_rebalance_plan",
157
+ )
158
+
159
+
160
+ def validate_check_allocation_band(payload: dict[str, Any]) -> None:
161
+ assert_has_keys(
162
+ payload,
163
+ ("outside_band", "hint", "max_drift", "drift", *STALENESS_KEYS),
164
+ label="check_allocation_band",
165
+ )
166
+
167
+
168
+ def validate_check_allocation_bands(payload: dict[str, Any]) -> None:
169
+ assert_has_keys(
170
+ payload,
171
+ ("allocation_pct", "scenarios", *STALENESS_KEYS),
172
+ label="check_allocation_bands",
173
+ )
174
+ for index, scenario in enumerate(payload["scenarios"]):
175
+ assert_has_keys(
176
+ scenario,
177
+ ("name", "target_pct", "band", "outside_band", "hint"),
178
+ label=f"check_allocation_bands.scenarios[{index}]",
179
+ )
180
+
181
+
182
+ CONTEXT_AT_FOUND_KEYS = (
183
+ "bundle_id",
184
+ "scope",
185
+ "as_of",
186
+ "horizon_days",
187
+ "portfolio",
188
+ "market",
189
+ "sentiment",
190
+ "macro",
191
+ "delta",
192
+ "regime",
193
+ "snapshot_as_of",
194
+ "requested_as_of",
195
+ "match",
196
+ )
197
+
198
+
199
+ def validate_context_at(payload: dict[str, Any]) -> None:
200
+ if payload.get("available") is False:
201
+ assert_has_keys(
202
+ payload,
203
+ ("available", "reason", "scope", "requested_as_of", "match"),
204
+ label="get_context_at",
205
+ )
206
+ return
207
+ assert_has_keys(payload, CONTEXT_AT_FOUND_KEYS, label="get_context_at")
208
+ assert_available_block(
209
+ payload["portfolio"],
210
+ label="portfolio",
211
+ when_available=PORTFOLIO_AVAILABLE_KEYS,
212
+ )
213
+ assert_available_block(
214
+ payload["regime"],
215
+ label="regime",
216
+ when_available=REGIME_AVAILABLE_KEYS,
217
+ )
218
+
219
+
220
+ def validate_context_delta(payload: dict[str, Any]) -> None:
221
+ if payload.get("available") is False:
222
+ assert_has_keys(
223
+ payload,
224
+ ("available", "reason", "scope", "prior_as_of"),
225
+ label="get_context_delta",
226
+ )
227
+ return
228
+ assert_has_keys(
229
+ payload,
230
+ (
231
+ "scope",
232
+ "prior_as_of",
233
+ "current_as_of",
234
+ "notable_shifts",
235
+ "prior_snapshot_as_of",
236
+ "current_snapshot_as_of",
237
+ ),
238
+ label="get_context_delta",
239
+ )
240
+
241
+
242
+ def validate_portfolio_state(payload: dict[str, Any]) -> None:
243
+ assert_has_keys(payload, (*STALENESS_KEYS,), label="get_portfolio_state")
244
+ if not payload.get("available"):
245
+ assert_has_keys(
246
+ payload,
247
+ ("available", "exchange", "source", "reason", *STALENESS_KEYS),
248
+ label="get_portfolio_state",
249
+ )
250
+ return
251
+ assert_has_keys(
252
+ payload,
253
+ (
254
+ "available",
255
+ "exchange",
256
+ "source",
257
+ "nav_usd",
258
+ "allocation_pct",
259
+ "target_allocation_pct",
260
+ "drift",
261
+ "rebalance_hint",
262
+ *STALENESS_KEYS,
263
+ ),
264
+ label="get_portfolio_state",
265
+ )
266
+
267
+
268
+ _VALIDATORS = {
269
+ "get_context_bundle": validate_context_bundle,
270
+ "get_market_context": validate_market_context,
271
+ "get_rebalance_plan": validate_rebalance_plan,
272
+ "check_allocation_band": validate_check_allocation_band,
273
+ "check_allocation_bands": validate_check_allocation_bands,
274
+ "get_context_at": validate_context_at,
275
+ "get_context_delta": validate_context_delta,
276
+ "get_portfolio_state": validate_portfolio_state,
277
+ }
278
+
279
+
280
+ def validate_tool_response(tool_name: str, payload: dict[str, Any]) -> None:
281
+ if tool_name not in MCP_TOOL_NAMES:
282
+ raise ValueError(f"unknown MCP tool: {tool_name}")
283
+ validator = _VALIDATORS.get(tool_name)
284
+ if validator is None:
285
+ raise ValueError(f"no contract validator for tool: {tool_name}")
286
+ validator(payload)