@vizzor/cli 0.13.1 → 0.14.5

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 (53) hide show
  1. package/README.md +250 -192
  2. package/chronovisor-engine/pyproject.toml +31 -0
  3. package/chronovisor-engine/src/__init__.py +0 -0
  4. package/chronovisor-engine/src/inference/__init__.py +0 -0
  5. package/chronovisor-engine/src/inference/predict.py +44 -0
  6. package/chronovisor-engine/src/model_catalog.py +219 -0
  7. package/chronovisor-engine/src/models/__init__.py +0 -0
  8. package/chronovisor-engine/src/models/anomaly_detector.py +104 -0
  9. package/chronovisor-engine/src/models/blockchain_cycle_analyzer.py +217 -0
  10. package/chronovisor-engine/src/models/catalyst_event_model.py +70 -0
  11. package/chronovisor-engine/src/models/conformal_interval.py +50 -0
  12. package/chronovisor-engine/src/models/divergence_detector.py +247 -0
  13. package/chronovisor-engine/src/models/drift_monitor.py +51 -0
  14. package/chronovisor-engine/src/models/intent_classifier.py +189 -0
  15. package/chronovisor-engine/src/models/lstm_predictor.py +143 -0
  16. package/chronovisor-engine/src/models/microstructure_specialist.py +65 -0
  17. package/chronovisor-engine/src/models/narrative_detector.py +418 -0
  18. package/chronovisor-engine/src/models/portfolio_optimizer.py +162 -0
  19. package/chronovisor-engine/src/models/project_risk_scorer.py +184 -0
  20. package/chronovisor-engine/src/models/pump_detector.py +344 -0
  21. package/chronovisor-engine/src/models/regime_detector.py +127 -0
  22. package/chronovisor-engine/src/models/rug_detector.py +197 -0
  23. package/chronovisor-engine/src/models/sentiment_analyzer.py +257 -0
  24. package/chronovisor-engine/src/models/signal_classifier.py +191 -0
  25. package/chronovisor-engine/src/models/stacking_meta.py +56 -0
  26. package/chronovisor-engine/src/models/strategy_bandit.py +191 -0
  27. package/chronovisor-engine/src/models/ta_interpreter.py +341 -0
  28. package/chronovisor-engine/src/models/target_quantile.py +96 -0
  29. package/chronovisor-engine/src/models/trend_scorer.py +107 -0
  30. package/chronovisor-engine/src/models/wallet_classifier.py +261 -0
  31. package/chronovisor-engine/src/server.py +1686 -0
  32. package/chronovisor-engine/src/training/__init__.py +0 -0
  33. package/chronovisor-engine/src/training/data_loader.py +635 -0
  34. package/chronovisor-engine/src/training/pipeline.py +130 -0
  35. package/chronovisor-engine/src/training/train_catalyst.py +169 -0
  36. package/chronovisor-engine/src/training/train_classifier.py +159 -0
  37. package/chronovisor-engine/src/training/train_conformal.py +106 -0
  38. package/chronovisor-engine/src/training/train_direction.py +215 -0
  39. package/chronovisor-engine/src/training/train_drift.py +57 -0
  40. package/chronovisor-engine/src/training/train_isotonic.py +58 -0
  41. package/chronovisor-engine/src/training/train_lstm.py +217 -0
  42. package/chronovisor-engine/src/training/train_microstructure.py +102 -0
  43. package/chronovisor-engine/src/training/train_narrative.py +168 -0
  44. package/chronovisor-engine/src/training/train_pump.py +109 -0
  45. package/chronovisor-engine/src/training/train_regime.py +116 -0
  46. package/chronovisor-engine/src/training/train_rug.py +58 -0
  47. package/chronovisor-engine/src/training/train_sentiment.py +63 -0
  48. package/chronovisor-engine/src/training/train_stacking_meta.py +74 -0
  49. package/chronovisor-engine/src/training/train_target_quantile.py +115 -0
  50. package/chronovisor-engine/src/training/train_trend.py +101 -0
  51. package/dist/index.js +19124 -11698
  52. package/dist/index.js.map +1 -1
  53. package/package.json +3 -1
@@ -0,0 +1,341 @@
1
+ """TA Interpreter — Random Forest classifier for technical signal interpretation."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import joblib
7
+ import numpy as np
8
+
9
+ MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
10
+
11
+
12
+ class TAInterpreter:
13
+ """Interprets technical indicators into actionable signals with learned weights."""
14
+
15
+ FEATURE_KEYS = [
16
+ "rsi",
17
+ "macd_histogram",
18
+ "macd_line",
19
+ "macd_signal",
20
+ "bb_percent_b",
21
+ "bb_bandwidth",
22
+ "ema12",
23
+ "ema26",
24
+ "ema_cross_pct",
25
+ "atr",
26
+ "atr_pct",
27
+ "obv",
28
+ "price_change",
29
+ ]
30
+
31
+ SIGNAL_NAMES = ["RSI", "MACD", "Bollinger Bands", "EMA Crossover", "ATR", "OBV"]
32
+
33
+ DEFAULT_WEIGHTS = {
34
+ "RSI": 20,
35
+ "MACD": 20,
36
+ "Bollinger Bands": 15,
37
+ "EMA Crossover": 20,
38
+ "ATR": 10,
39
+ "OBV": 15,
40
+ }
41
+
42
+ def __init__(self) -> None:
43
+ self.version = "0.1.0"
44
+ self.is_loaded = False
45
+ self.last_trained: str | None = None
46
+ self.accuracy: float | None = None
47
+ self.model = None
48
+
49
+ def load(self) -> None:
50
+ model_path = MODEL_DIR / "ta_interpreter.joblib"
51
+ try:
52
+ data = joblib.load(model_path)
53
+ self.model = data["model"]
54
+ self.last_trained = data.get("trained_at")
55
+ self.accuracy = data.get("accuracy")
56
+ self.is_loaded = True
57
+ except Exception:
58
+ self.model = None
59
+ self.is_loaded = True # heuristic fallback ready
60
+
61
+ def predict(self, features: dict) -> dict:
62
+ if self.model is not None:
63
+ return self._predict_model(features)
64
+ return self._predict_heuristic(features)
65
+
66
+ def _predict_model(self, features: dict) -> dict:
67
+ x = np.array([[features.get(k, 0) for k in self.FEATURE_KEYS]])
68
+
69
+ # Get probabilities for composite direction
70
+ proba = self.model.predict_proba(x)[0]
71
+ classes = list(self.model.classes_)
72
+
73
+ # Build weights from feature importances
74
+ weights = dict(self.DEFAULT_WEIGHTS)
75
+ if hasattr(self.model, "feature_importances_"):
76
+ imp = self.model.feature_importances_
77
+ # Map feature importances to signal groups
78
+ signal_imp = {
79
+ "RSI": float(imp[0]) if len(imp) > 0 else 0.2,
80
+ "MACD": float(np.mean(imp[1:4])) if len(imp) > 3 else 0.2,
81
+ "Bollinger Bands": float(np.mean(imp[4:6])) if len(imp) > 5 else 0.15,
82
+ "EMA Crossover": float(np.mean(imp[6:9])) if len(imp) > 8 else 0.2,
83
+ "ATR": float(np.mean(imp[9:11])) if len(imp) > 10 else 0.1,
84
+ "OBV": float(imp[11]) if len(imp) > 11 else 0.15,
85
+ }
86
+ total = sum(signal_imp.values()) or 1
87
+ weights = {k: round(v / total * 100, 1) for k, v in signal_imp.items()}
88
+
89
+ # Generate signals from heuristic (structure), but use ML weights/composite
90
+ signals = self._interpret_signals(features)
91
+
92
+ # Composite from ML
93
+ pred_idx = int(np.argmax(proba))
94
+ direction = classes[pred_idx] if pred_idx < len(classes) else "neutral"
95
+ confidence = float(proba[pred_idx]) * 100
96
+
97
+ # Score: positive = bullish, negative = bearish
98
+ bull_prob = proba[classes.index("bullish")] if "bullish" in classes else 0
99
+ bear_prob = proba[classes.index("bearish")] if "bearish" in classes else 0
100
+ score = (bull_prob - bear_prob) * 100
101
+
102
+ return {
103
+ "signals": signals,
104
+ "weights": weights,
105
+ "composite": {
106
+ "direction": direction,
107
+ "score": round(score, 2),
108
+ "confidence": round(min(100, confidence), 2),
109
+ },
110
+ "model": "rf-ta-interpreter",
111
+ }
112
+
113
+ def _predict_heuristic(self, features: dict) -> dict:
114
+ signals = self._interpret_signals(features)
115
+ weights = dict(self.DEFAULT_WEIGHTS)
116
+
117
+ # Composite: weighted average of signal strengths
118
+ total_weight = 0
119
+ weighted_score = 0
120
+ for sig in signals:
121
+ w = weights.get(sig["name"], 10)
122
+ total_weight += w
123
+ dir_score = (
124
+ sig["strength"]
125
+ if sig["direction"] == "bullish"
126
+ else -sig["strength"]
127
+ if sig["direction"] == "bearish"
128
+ else 0
129
+ )
130
+ weighted_score += dir_score * w
131
+
132
+ score = weighted_score / total_weight if total_weight > 0 else 0
133
+ direction = "bullish" if score > 15 else "bearish" if score < -15 else "neutral"
134
+
135
+ # Confidence from signal agreement
136
+ bull_count = sum(1 for s in signals if s["direction"] == "bullish")
137
+ bear_count = sum(1 for s in signals if s["direction"] == "bearish")
138
+ total_dir = bull_count + bear_count
139
+ agreement = max(bull_count, bear_count) / total_dir if total_dir > 0 else 0
140
+ confidence = agreement * 100 * (len(signals) / 6)
141
+
142
+ return {
143
+ "signals": signals,
144
+ "weights": {k: float(v) for k, v in weights.items()},
145
+ "composite": {
146
+ "direction": direction,
147
+ "score": round(score, 2),
148
+ "confidence": round(min(100, confidence), 2),
149
+ },
150
+ "model": "heuristic-ta-interpreter",
151
+ }
152
+
153
+ def _interpret_signals(self, features: dict) -> list[dict]:
154
+ signals = []
155
+
156
+ # RSI
157
+ rsi = features.get("rsi", 50)
158
+ if rsi > 70:
159
+ signals.append(
160
+ {
161
+ "name": "RSI",
162
+ "direction": "bearish",
163
+ "strength": min(100, 50 + (rsi - 70) * 1.5),
164
+ "description": f"RSI {rsi:.1f} — overbought territory",
165
+ }
166
+ )
167
+ elif rsi < 30:
168
+ signals.append(
169
+ {
170
+ "name": "RSI",
171
+ "direction": "bullish",
172
+ "strength": min(100, 50 + (30 - rsi) * 1.5),
173
+ "description": f"RSI {rsi:.1f} — oversold territory",
174
+ }
175
+ )
176
+ else:
177
+ direction = (
178
+ "bullish" if rsi > 60 else "bearish" if rsi < 40 else "neutral"
179
+ )
180
+ strength = 40 + abs(rsi - 50) if direction != "neutral" else 30
181
+ signals.append(
182
+ {
183
+ "name": "RSI",
184
+ "direction": direction,
185
+ "strength": strength,
186
+ "description": f"RSI {rsi:.1f} — {'bullish' if rsi > 60 else 'bearish' if rsi < 40 else 'neutral'} zone",
187
+ }
188
+ )
189
+
190
+ # MACD
191
+ histogram = features.get("macd_histogram", 0)
192
+ if histogram > 0:
193
+ signals.append(
194
+ {
195
+ "name": "MACD",
196
+ "direction": "bullish",
197
+ "strength": min(90, 50 + abs(histogram) * 100),
198
+ "description": f"MACD histogram positive ({histogram:.4f}) — bullish momentum",
199
+ }
200
+ )
201
+ elif histogram < 0:
202
+ signals.append(
203
+ {
204
+ "name": "MACD",
205
+ "direction": "bearish",
206
+ "strength": min(90, 50 + abs(histogram) * 100),
207
+ "description": f"MACD histogram negative ({histogram:.4f}) — bearish momentum",
208
+ }
209
+ )
210
+ else:
211
+ signals.append(
212
+ {
213
+ "name": "MACD",
214
+ "direction": "neutral",
215
+ "strength": 30,
216
+ "description": "MACD at signal line — no clear direction",
217
+ }
218
+ )
219
+
220
+ # Bollinger Bands
221
+ pct_b = features.get("bb_percent_b", 0.5)
222
+ if pct_b > 0.8:
223
+ signals.append(
224
+ {
225
+ "name": "Bollinger Bands",
226
+ "direction": "bearish",
227
+ "strength": 55,
228
+ "description": f"Price near upper band (%B: {pct_b:.2f}) — potential pullback",
229
+ }
230
+ )
231
+ elif pct_b < 0.2:
232
+ signals.append(
233
+ {
234
+ "name": "Bollinger Bands",
235
+ "direction": "bullish",
236
+ "strength": 55,
237
+ "description": f"Price near lower band (%B: {pct_b:.2f}) — potential bounce",
238
+ }
239
+ )
240
+ else:
241
+ signals.append(
242
+ {
243
+ "name": "Bollinger Bands",
244
+ "direction": "neutral",
245
+ "strength": 30,
246
+ "description": f"Price within bands (%B: {pct_b:.2f})",
247
+ }
248
+ )
249
+
250
+ # EMA Crossover
251
+ ema_cross = features.get("ema_cross_pct", 0)
252
+ if ema_cross > 0:
253
+ signals.append(
254
+ {
255
+ "name": "EMA Crossover",
256
+ "direction": "bullish",
257
+ "strength": min(90, 50 + abs(ema_cross) * 10),
258
+ "description": f"EMA(12) above EMA(26) by {ema_cross:.2f}% — bullish trend",
259
+ }
260
+ )
261
+ elif ema_cross < 0:
262
+ signals.append(
263
+ {
264
+ "name": "EMA Crossover",
265
+ "direction": "bearish",
266
+ "strength": min(90, 50 + abs(ema_cross) * 10),
267
+ "description": f"EMA(12) below EMA(26) by {abs(ema_cross):.2f}% — bearish trend",
268
+ }
269
+ )
270
+ else:
271
+ signals.append(
272
+ {
273
+ "name": "EMA Crossover",
274
+ "direction": "neutral",
275
+ "strength": 30,
276
+ "description": "EMA(12) = EMA(26) — no trend",
277
+ }
278
+ )
279
+
280
+ # ATR
281
+ atr_pct = features.get("atr_pct", 0)
282
+ if atr_pct > 5:
283
+ desc = f"ATR {atr_pct:.2f}% — high volatility"
284
+ elif atr_pct > 2:
285
+ desc = f"ATR {atr_pct:.2f}% — moderate volatility"
286
+ else:
287
+ desc = f"ATR {atr_pct:.2f}% — low volatility"
288
+ signals.append(
289
+ {"name": "ATR", "direction": "neutral", "strength": 40, "description": desc}
290
+ )
291
+
292
+ # OBV
293
+ obv = features.get("obv", 0)
294
+ price_change = features.get("price_change", 0)
295
+ if obv > 0 and price_change > 0:
296
+ signals.append(
297
+ {
298
+ "name": "OBV",
299
+ "direction": "bullish",
300
+ "strength": 65,
301
+ "description": "OBV positive with rising price — confirmed uptrend",
302
+ }
303
+ )
304
+ elif obv > 0 and price_change <= 0:
305
+ signals.append(
306
+ {
307
+ "name": "OBV",
308
+ "direction": "bullish",
309
+ "strength": 70,
310
+ "description": "OBV positive but price flat/down — accumulation",
311
+ }
312
+ )
313
+ elif obv < 0 and price_change < 0:
314
+ signals.append(
315
+ {
316
+ "name": "OBV",
317
+ "direction": "bearish",
318
+ "strength": 65,
319
+ "description": "OBV negative with falling price — confirmed downtrend",
320
+ }
321
+ )
322
+ elif obv < 0 and price_change >= 0:
323
+ signals.append(
324
+ {
325
+ "name": "OBV",
326
+ "direction": "bearish",
327
+ "strength": 70,
328
+ "description": "OBV negative but price flat/up — distribution",
329
+ }
330
+ )
331
+ else:
332
+ signals.append(
333
+ {
334
+ "name": "OBV",
335
+ "direction": "neutral",
336
+ "strength": 30,
337
+ "description": "OBV neutral",
338
+ }
339
+ )
340
+
341
+ return signals
@@ -0,0 +1,96 @@
1
+ """Quantile target model for price-delta forecasts."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import joblib
8
+ import numpy as np
9
+
10
+ from ..training.train_direction import ALL_FEATURE_KEYS
11
+
12
+ MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
13
+
14
+
15
+ def _horizon_to_minutes(horizon: str) -> float:
16
+ value = str(horizon or "4h").strip().lower()
17
+ total = 0.0
18
+ for amount, unit in re.findall(r"(\d+)(mo|y|w|d|h|m)", value):
19
+ quantity = float(amount or 0)
20
+ if unit == "m":
21
+ total += quantity
22
+ elif unit == "h":
23
+ total += quantity * 60
24
+ elif unit == "d":
25
+ total += quantity * 1440
26
+ elif unit == "w":
27
+ total += quantity * 10080
28
+ elif unit == "mo":
29
+ total += quantity * 43200
30
+ elif unit == "y":
31
+ total += quantity * 525600
32
+ return total if total > 0 else 240.0
33
+
34
+
35
+ class TargetQuantileModel:
36
+ FEATURE_KEYS = ALL_FEATURE_KEYS + ["probability_hint", "horizon_minutes"]
37
+
38
+ def __init__(self) -> None:
39
+ self.version = "0.1.0"
40
+ self.is_loaded = False
41
+ self.last_trained: str | None = None
42
+ self.accuracy: float | None = None
43
+ self.models: dict[str, object] = {}
44
+
45
+ def load(self) -> None:
46
+ path = MODEL_DIR / "target_xgb_quantile.joblib"
47
+ try:
48
+ data = joblib.load(path)
49
+ self.models = data.get("models", {})
50
+ self.last_trained = data.get("trained_at")
51
+ self.accuracy = data.get("accuracy")
52
+ self.is_loaded = True
53
+ except Exception:
54
+ self.models = {}
55
+ self.is_loaded = True
56
+
57
+ def predict(self, features: dict) -> dict:
58
+ if self.models:
59
+ return self._predict_model(features)
60
+ return self._predict_heuristic(features)
61
+
62
+ def _predict_model(self, features: dict) -> dict:
63
+ enriched = dict(features)
64
+ enriched["probability_hint"] = float(features.get("probability_hint", 0.5))
65
+ enriched["horizon_minutes"] = _horizon_to_minutes(str(features.get("horizon", "4h")))
66
+ x = np.array([[enriched.get(k, 0.0) for k in self.FEATURE_KEYS]], dtype=np.float32)
67
+ q10 = float(self.models["q10"].predict(x)[0])
68
+ q50 = float(self.models["q50"].predict(x)[0])
69
+ q90 = float(self.models["q90"].predict(x)[0])
70
+ low, base, high = sorted([q10, q50, q90])
71
+ return {
72
+ "low_change_pct": round(low, 4),
73
+ "base_change_pct": round(base, 4),
74
+ "high_change_pct": round(high, 4),
75
+ "direction_bias": "up" if base > 0.1 else "down" if base < -0.1 else "sideways",
76
+ "interval_width_pct": round(high - low, 4),
77
+ "model": "target_xgb_quantile",
78
+ }
79
+
80
+ def _predict_heuristic(self, features: dict) -> dict:
81
+ base = (
82
+ float(features.get("emaCrossoverPct", 0.0)) * 0.35
83
+ + float(features.get("macdHistogram", 0.0)) * 2.0
84
+ + float(features.get("rsiSlope", 0.0)) * 0.08
85
+ )
86
+ width = max(0.35, float(features.get("atrPct", 0.5)) * 1.4)
87
+ low = base - width
88
+ high = base + width
89
+ return {
90
+ "low_change_pct": round(low, 4),
91
+ "base_change_pct": round(base, 4),
92
+ "high_change_pct": round(high, 4),
93
+ "direction_bias": "up" if base > 0.1 else "down" if base < -0.1 else "sideways",
94
+ "interval_width_pct": round(high - low, 4),
95
+ "model": "heuristic-target-quantile",
96
+ }
@@ -0,0 +1,107 @@
1
+ """Trend Scorer — XGBoost regressor for market trend strength."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+ import joblib
7
+ import numpy as np
8
+
9
+ MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
10
+
11
+
12
+ class TrendScorer:
13
+ """Predicts trend strength (0-100) and direction from market features."""
14
+
15
+ FEATURE_KEYS = [
16
+ "price_change_24h",
17
+ "price_change_7d",
18
+ "volume_24h",
19
+ "market_cap",
20
+ "volume_to_mcap_ratio",
21
+ "rank",
22
+ ]
23
+
24
+ def __init__(self) -> None:
25
+ self.version = "0.1.0"
26
+ self.is_loaded = False
27
+ self.last_trained: str | None = None
28
+ self.accuracy: float | None = None
29
+ self.model = None
30
+
31
+ def load(self) -> None:
32
+ model_path = MODEL_DIR / "trend_scorer.joblib"
33
+ try:
34
+ data = joblib.load(model_path)
35
+ self.model = data["model"]
36
+ self.last_trained = data.get("trained_at")
37
+ self.accuracy = data.get("accuracy")
38
+ self.is_loaded = True
39
+ except Exception:
40
+ self.model = None
41
+ self.is_loaded = True # heuristic fallback ready
42
+
43
+ def predict(self, features: dict) -> dict:
44
+ if self.model is not None:
45
+ return self._predict_model(features)
46
+ return self._predict_heuristic(features)
47
+
48
+ def _predict_model(self, features: dict) -> dict:
49
+ x = np.array([[features.get(k, 0) for k in self.FEATURE_KEYS]])
50
+ score = float(np.clip(self.model.predict(x)[0], 0, 100))
51
+
52
+ # Feature importances
53
+ importances = {}
54
+ if hasattr(self.model, "feature_importances_"):
55
+ for key, imp in zip(self.FEATURE_KEYS, self.model.feature_importances_):
56
+ importances[key] = round(float(imp), 4)
57
+
58
+ direction = "bullish" if score > 60 else "bearish" if score < 40 else "neutral"
59
+ confidence = abs(score - 50) * 2 # 0-100 scale
60
+
61
+ return {
62
+ "score": round(score, 2),
63
+ "direction": direction,
64
+ "confidence": round(min(100, confidence), 2),
65
+ "feature_importances": importances,
66
+ "model": "xgboost-trend-scorer",
67
+ }
68
+
69
+ def _predict_heuristic(self, features: dict) -> dict:
70
+ score = 50.0
71
+
72
+ pc24h = features.get("price_change_24h", 0)
73
+ pc7d = features.get("price_change_7d", 0)
74
+ vol = features.get("volume_24h", 0)
75
+ mcap = features.get("market_cap", 0)
76
+
77
+ signals = []
78
+
79
+ if pc24h > 5:
80
+ score += 15
81
+ signals.append(f"Strong 24h gain: +{pc24h:.2f}%")
82
+ elif pc24h < -5:
83
+ score -= 15
84
+ signals.append(f"Significant 24h drop: {pc24h:.2f}%")
85
+
86
+ if pc7d > 10:
87
+ score += 20
88
+ signals.append(f"Bullish weekly trend: +{pc7d:.2f}%")
89
+ elif pc7d < -10:
90
+ score -= 20
91
+ signals.append(f"Bearish weekly trend: {pc7d:.2f}%")
92
+
93
+ if mcap > 0 and vol > mcap * 0.1:
94
+ score += 5
95
+ signals.append("High volume relative to market cap")
96
+
97
+ score = max(0, min(100, score))
98
+ direction = "bullish" if score > 60 else "bearish" if score < 40 else "neutral"
99
+ confidence = abs(score - 50) * 2
100
+
101
+ return {
102
+ "score": round(score, 2),
103
+ "direction": direction,
104
+ "confidence": round(min(100, confidence), 2),
105
+ "feature_importances": {},
106
+ "model": "heuristic-trend-scorer",
107
+ }