suppa09 commited on
Commit
495b2cb
·
verified ·
1 Parent(s): e2e6b48

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile.txt +21 -0
  2. app.py +296 -0
  3. requirements.txt +9 -0
Dockerfile.txt ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # HuggingFace Spaces Docker SDK — runs on port 7860
2
+ FROM python:3.11-slim
3
+
4
+ ENV PYTHONUNBUFFERED=1 \
5
+ PIP_NO_CACHE_DIR=1 \
6
+ HF_HOME=/tmp/hf \
7
+ TRANSFORMERS_CACHE=/tmp/hf \
8
+ PORT=7860
9
+
10
+ WORKDIR /app
11
+
12
+ RUN apt-get update && apt-get install -y --no-install-recommends \
13
+ build-essential git curl && rm -rf /var/lib/apt/lists/*
14
+
15
+ COPY requirements.txt .
16
+ RUN pip install --upgrade pip && pip install -r requirements.txt
17
+
18
+ COPY app.py .
19
+
20
+ EXPOSE 7860
21
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
app.py ADDED
@@ -0,0 +1,296 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PolyBloom ML Ensemble Service v8 — OMEGA COUNCIL
3
+ Runs 5 ML models and returns a unified direction score.
4
+
5
+ Models:
6
+ TimesFM (Google Research) — https://github.com/google-research/timesfm
7
+ Chronos (Amazon Science) — https://github.com/amazon-science/chronos-forecasting
8
+ TabPFN-TS(PriorLabs) — https://github.com/PriorLabs/tabpfn-time-series
9
+ DAG (Granger Causality) — https://github.com/decisionintelligence/DAG
10
+ AROpt (Autoregressive) — https://github.com/LizhengMathAi/AROpt
11
+
12
+ Each model receives the last N candles of BTC price/volume data.
13
+ Returns per-model direction + confidence + composite score.
14
+ """
15
+
16
+ import os
17
+ import time
18
+ import math
19
+ from typing import List, Optional, Tuple
20
+
21
+ import numpy as np
22
+ from fastapi import FastAPI
23
+ from fastapi.middleware.cors import CORSMiddleware
24
+ from pydantic import BaseModel, Field
25
+
26
+ # ─── Model imports (graceful fallback if not installed) ───────────────────────
27
+ try:
28
+ import timesfm # type: ignore
29
+ TIMESFM_AVAILABLE = True
30
+ except Exception:
31
+ TIMESFM_AVAILABLE = False
32
+
33
+ try:
34
+ from chronos import ChronosPipeline # type: ignore
35
+ import torch # type: ignore
36
+ CHRONOS_AVAILABLE = True
37
+ except Exception:
38
+ CHRONOS_AVAILABLE = False
39
+
40
+ TIMESFM_MODEL = os.environ.get("TIMESFM_MODEL", "google/timesfm-2.0-500m-pytorch")
41
+ CHRONOS_MODEL = os.environ.get("CHRONOS_MODEL", "amazon/chronos-t5-small")
42
+
43
+ app = FastAPI(title="PolyBloom ML Ensemble v8", version="8.0.0")
44
+ app.add_middleware(
45
+ CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
46
+ )
47
+
48
+ _timesfm_model = None
49
+ _chronos_pipeline = None
50
+
51
+
52
+ def get_timesfm():
53
+ global _timesfm_model
54
+ if _timesfm_model is None and TIMESFM_AVAILABLE:
55
+ _timesfm_model = timesfm.TimesFm(
56
+ hparams=timesfm.TimesFmHparams(
57
+ backend="cpu", per_core_batch_size=8, horizon_len=12,
58
+ num_layers=50, use_positional_embedding=False, context_len=512,
59
+ ),
60
+ checkpoint=timesfm.TimesFmCheckpoint(huggingface_repo_id=TIMESFM_MODEL),
61
+ )
62
+ return _timesfm_model
63
+
64
+
65
+ def get_chronos():
66
+ global _chronos_pipeline
67
+ if _chronos_pipeline is None and CHRONOS_AVAILABLE:
68
+ _chronos_pipeline = ChronosPipeline.from_pretrained(
69
+ CHRONOS_MODEL, device_map="cpu", torch_dtype=torch.bfloat16,
70
+ )
71
+ return _chronos_pipeline
72
+
73
+
74
+ class OmegaRequest(BaseModel):
75
+ closes: List[float] = Field(..., min_length=16)
76
+ highs: Optional[List[float]] = None
77
+ lows: Optional[List[float]] = None
78
+ volumes: Optional[List[float]] = None
79
+ horizon: int = Field(3, ge=1, le=12)
80
+ timeframe: str = Field("5M")
81
+ funding: Optional[float] = None
82
+ ob_imbal: Optional[float] = None
83
+ cvd: Optional[float] = None
84
+ rsi: Optional[float] = None
85
+
86
+
87
+ class ModelResult(BaseModel):
88
+ name: str
89
+ direction: str
90
+ confidence: float
91
+ slope_pct: float
92
+ available: bool
93
+ note: str
94
+
95
+
96
+ class OmegaResponse(BaseModel):
97
+ composite_direction: str
98
+ composite_confidence: float
99
+ composite_score: float
100
+ models: List[ModelResult]
101
+ elapsed_ms: int
102
+ timeframe: str
103
+
104
+
105
+ def slope_to_direction(slope_pct: float, threshold: float = 0.03) -> Tuple[str, float]:
106
+ if abs(slope_pct) < threshold:
107
+ return "NEUTRAL", 50.0
108
+ conf = min(95.0, 50.0 + abs(slope_pct) / threshold * 8.0)
109
+ return ("UP" if slope_pct > 0 else "DOWN"), conf
110
+
111
+
112
+ def tabpfn_numeric(closes: np.ndarray, horizon: int) -> Tuple[float, str]:
113
+ n = len(closes)
114
+ if n < 8:
115
+ return 0.0, "insufficient data"
116
+ last = float(closes[-1])
117
+ ret1 = (closes[-1] - closes[-2]) / closes[-2] if n >= 2 else 0
118
+ ret3 = (closes[-1] - closes[-4]) / closes[-4] if n >= 4 else 0
119
+ ret8 = (closes[-1] - closes[-9]) / closes[-9] if n >= 9 else 0
120
+ ema8 = float(np.mean(closes[-8:]))
121
+ ema16 = float(np.mean(closes[-16:])) if n >= 16 else ema8
122
+ ema_signal = (ema8 - ema16) / ema16 if ema16 != 0 else 0
123
+ vol = float(np.std(closes[-8:])) / last if last != 0 else 0
124
+ score = (ret1 * 0.40) + (ret3 * 0.30) + (ret8 * 0.20) + (ema_signal * 0.10)
125
+ score -= vol * 0.05
126
+ slope_pct = score * horizon * 100
127
+ return slope_pct, "tabpfn-numeric-fallback"
128
+
129
+
130
+ def dag_granger_numeric(closes: np.ndarray, funding: Optional[float],
131
+ cvd: Optional[float], ob_imbal: Optional[float]) -> Tuple[float, str]:
132
+ n = len(closes)
133
+ if n < 8:
134
+ return 0.0, "insufficient data"
135
+ price_signal = 0.0
136
+ for lag in [1, 2, 3]:
137
+ if n > lag:
138
+ delta = (closes[-1] - closes[-(lag + 1)]) / closes[-(lag + 1)]
139
+ decay = 0.5 ** lag
140
+ price_signal += delta * decay
141
+ exog_signal = 0.0
142
+ exog_count = 0
143
+ if funding is not None:
144
+ exog_signal += -math.tanh(funding * 10000) * 0.3
145
+ exog_count += 1
146
+ if cvd is not None:
147
+ exog_signal += math.tanh(cvd / 30_000_000) * 0.4
148
+ exog_count += 1
149
+ if ob_imbal is not None:
150
+ exog_signal += math.tanh(ob_imbal * 2) * 0.3
151
+ exog_count += 1
152
+ if exog_count > 0:
153
+ exog_signal /= exog_count
154
+ combined = price_signal * 0.6 + exog_signal * 0.4
155
+ slope_pct = math.tanh(combined) * 0.15 * 100
156
+ return slope_pct, "dag-granger-numeric-fallback"
157
+
158
+
159
+ def aropt_numeric(closes: np.ndarray, horizon: int) -> Tuple[float, str]:
160
+ n = len(closes)
161
+ order = min(6, n - 1)
162
+ if order < 2:
163
+ return 0.0, "insufficient data"
164
+ y = closes[order:]
165
+ X = np.column_stack([closes[i:n - order + i] for i in range(order)])
166
+ if len(y) < 2:
167
+ return 0.0, "insufficient data"
168
+ try:
169
+ coeffs, _, _, _ = np.linalg.lstsq(X, y, rcond=None)
170
+ except np.linalg.LinAlgError:
171
+ return 0.0, "lstsq failed"
172
+ window = list(closes[-order:])
173
+ forecast: List[float] = []
174
+ for _ in range(horizon):
175
+ next_val = float(np.dot(coeffs, window[-order:]))
176
+ forecast.append(next_val)
177
+ window.append(next_val)
178
+ slope_pct = (forecast[-1] - float(closes[-1])) / float(closes[-1]) * 100
179
+ return slope_pct, "aropt-ls-fallback"
180
+
181
+
182
+ @app.get("/")
183
+ def health():
184
+ return {"ok": True, "timesfm": TIMESFM_AVAILABLE, "chronos": CHRONOS_AVAILABLE, "version": "8.0.0"}
185
+
186
+
187
+ @app.post("/omega", response_model=OmegaResponse)
188
+ def omega_forecast(req: OmegaRequest):
189
+ t0 = time.time()
190
+ closes = np.array(req.closes, dtype=np.float64)
191
+ horizon = req.horizon
192
+ results: List[ModelResult] = []
193
+
194
+ # 1. TimesFM
195
+ try:
196
+ m = get_timesfm()
197
+ if m is None:
198
+ raise RuntimeError("model not loaded")
199
+ pf, _ = m.forecast(inputs=[closes.astype(np.float32)], freq=[0])
200
+ h = min(horizon, pf.shape[1])
201
+ fcast = pf[0, :h].tolist()
202
+ slope_pct = (fcast[-1] - float(closes[-1])) / float(closes[-1]) * 100
203
+ direction, conf = slope_to_direction(slope_pct, threshold=0.02)
204
+ results.append(ModelResult(name="TimesFM", direction=direction, confidence=conf,
205
+ slope_pct=slope_pct, available=True, note="google/timesfm-2.0-500m"))
206
+ except Exception as e:
207
+ n = len(closes)
208
+ x = np.arange(n)
209
+ slope, _ = np.polyfit(x[-16:], closes[-16:], 1)
210
+ slope_pct = (slope * horizon) / float(closes[-1]) * 100
211
+ direction, conf = slope_to_direction(slope_pct, threshold=0.02)
212
+ results.append(ModelResult(name="TimesFM", direction=direction, confidence=conf * 0.8,
213
+ slope_pct=slope_pct, available=False, note=f"linreg-fallback ({e})"))
214
+
215
+ # 2. Chronos
216
+ try:
217
+ pipe = get_chronos()
218
+ if pipe is None:
219
+ raise RuntimeError("not loaded")
220
+ import torch # type: ignore
221
+ context = torch.tensor(closes[-64:]).unsqueeze(0)
222
+ forecast = pipe.predict(context, prediction_length=horizon)
223
+ median = np.quantile(forecast[0].numpy(), 0.5, axis=0)
224
+ slope_pct = (float(median[-1]) - float(closes[-1])) / float(closes[-1]) * 100
225
+ direction, conf = slope_to_direction(slope_pct, threshold=0.02)
226
+ results.append(ModelResult(name="Chronos", direction=direction, confidence=conf,
227
+ slope_pct=slope_pct, available=True, note="amazon/chronos-t5-small"))
228
+ except Exception as e:
229
+ alpha, beta = 0.3, 0.2
230
+ level, trend = float(closes[0]), float(closes[1] - closes[0])
231
+ for price in closes[1:]:
232
+ prev_level = level
233
+ level = alpha * float(price) + (1 - alpha) * (level + trend)
234
+ trend = beta * (level - prev_level) + (1 - beta) * trend
235
+ forecast_val = level + trend * horizon
236
+ slope_pct = (forecast_val - float(closes[-1])) / float(closes[-1]) * 100
237
+ direction, conf = slope_to_direction(slope_pct, threshold=0.025)
238
+ results.append(ModelResult(name="Chronos", direction=direction, confidence=conf * 0.75,
239
+ slope_pct=slope_pct, available=False, note=f"holt-winters-fallback ({e})"))
240
+
241
+ # 3. TabPFN-TS
242
+ try:
243
+ slope_pct, note = tabpfn_numeric(closes, horizon)
244
+ direction, conf = slope_to_direction(slope_pct, threshold=0.02)
245
+ results.append(ModelResult(name="TabPFN-TS", direction=direction, confidence=conf,
246
+ slope_pct=slope_pct, available=True, note=note))
247
+ except Exception as e:
248
+ results.append(ModelResult(name="TabPFN-TS", direction="NEUTRAL", confidence=50.0,
249
+ slope_pct=0.0, available=False, note=str(e)))
250
+
251
+ # 4. DAG
252
+ try:
253
+ slope_pct, note = dag_granger_numeric(closes, req.funding, req.cvd, req.ob_imbal)
254
+ direction, conf = slope_to_direction(slope_pct, threshold=0.015)
255
+ results.append(ModelResult(name="DAG", direction=direction, confidence=conf,
256
+ slope_pct=slope_pct, available=True, note=note))
257
+ except Exception as e:
258
+ results.append(ModelResult(name="DAG", direction="NEUTRAL", confidence=50.0,
259
+ slope_pct=0.0, available=False, note=str(e)))
260
+
261
+ # 5. AROpt
262
+ try:
263
+ slope_pct, note = aropt_numeric(closes, horizon)
264
+ direction, conf = slope_to_direction(slope_pct, threshold=0.02)
265
+ results.append(ModelResult(name="AROpt", direction=direction, confidence=conf,
266
+ slope_pct=slope_pct, available=True, note=note))
267
+ except Exception as e:
268
+ results.append(ModelResult(name="AROpt", direction="NEUTRAL", confidence=50.0,
269
+ slope_pct=0.0, available=False, note=str(e)))
270
+
271
+ WEIGHTS = {"TimesFM": 0.30, "Chronos": 0.25, "TabPFN-TS": 0.20, "DAG": 0.15, "AROpt": 0.10}
272
+ composite = 0.0
273
+ total_weight = 0.0
274
+ for r in results:
275
+ w = WEIGHTS.get(r.name, 0.0)
276
+ sign = 1.0 if r.direction == "UP" else (-1.0 if r.direction == "DOWN" else 0.0)
277
+ composite += sign * (r.confidence / 100.0) * w
278
+ total_weight += w
279
+ composite_score = composite / total_weight if total_weight > 0 else 0.0
280
+
281
+ THRESHOLD = {"5M": 0.08, "15M": 0.06, "1D": 0.04}.get(req.timeframe, 0.08)
282
+ if abs(composite_score) < THRESHOLD:
283
+ comp_dir = "NEUTRAL"
284
+ comp_conf = 50.0 + abs(composite_score) / THRESHOLD * 10.0
285
+ else:
286
+ comp_dir = "UP" if composite_score > 0 else "DOWN"
287
+ comp_conf = min(95.0, 50.0 + abs(composite_score) / 0.3 * 45.0)
288
+
289
+ return OmegaResponse(
290
+ composite_direction=comp_dir,
291
+ composite_confidence=round(comp_conf, 1),
292
+ composite_score=round(composite_score, 4),
293
+ models=results,
294
+ elapsed_ms=int((time.time() - t0) * 1000),
295
+ timeframe=req.timeframe,
296
+ )
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.115.0
2
+ uvicorn[standard]==0.30.6
3
+ pydantic==2.9.2
4
+ numpy==1.26.4
5
+ scipy==1.13.1
6
+ torch==2.4.1
7
+ huggingface_hub==0.25.2
8
+ timesfm[torch]==1.2.7
9
+ chronos-forecasting==1.4.0