@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.
- package/README.md +250 -192
- package/chronovisor-engine/pyproject.toml +31 -0
- package/chronovisor-engine/src/__init__.py +0 -0
- package/chronovisor-engine/src/inference/__init__.py +0 -0
- package/chronovisor-engine/src/inference/predict.py +44 -0
- package/chronovisor-engine/src/model_catalog.py +219 -0
- package/chronovisor-engine/src/models/__init__.py +0 -0
- package/chronovisor-engine/src/models/anomaly_detector.py +104 -0
- package/chronovisor-engine/src/models/blockchain_cycle_analyzer.py +217 -0
- package/chronovisor-engine/src/models/catalyst_event_model.py +70 -0
- package/chronovisor-engine/src/models/conformal_interval.py +50 -0
- package/chronovisor-engine/src/models/divergence_detector.py +247 -0
- package/chronovisor-engine/src/models/drift_monitor.py +51 -0
- package/chronovisor-engine/src/models/intent_classifier.py +189 -0
- package/chronovisor-engine/src/models/lstm_predictor.py +143 -0
- package/chronovisor-engine/src/models/microstructure_specialist.py +65 -0
- package/chronovisor-engine/src/models/narrative_detector.py +418 -0
- package/chronovisor-engine/src/models/portfolio_optimizer.py +162 -0
- package/chronovisor-engine/src/models/project_risk_scorer.py +184 -0
- package/chronovisor-engine/src/models/pump_detector.py +344 -0
- package/chronovisor-engine/src/models/regime_detector.py +127 -0
- package/chronovisor-engine/src/models/rug_detector.py +197 -0
- package/chronovisor-engine/src/models/sentiment_analyzer.py +257 -0
- package/chronovisor-engine/src/models/signal_classifier.py +191 -0
- package/chronovisor-engine/src/models/stacking_meta.py +56 -0
- package/chronovisor-engine/src/models/strategy_bandit.py +191 -0
- package/chronovisor-engine/src/models/ta_interpreter.py +341 -0
- package/chronovisor-engine/src/models/target_quantile.py +96 -0
- package/chronovisor-engine/src/models/trend_scorer.py +107 -0
- package/chronovisor-engine/src/models/wallet_classifier.py +261 -0
- package/chronovisor-engine/src/server.py +1686 -0
- package/chronovisor-engine/src/training/__init__.py +0 -0
- package/chronovisor-engine/src/training/data_loader.py +635 -0
- package/chronovisor-engine/src/training/pipeline.py +130 -0
- package/chronovisor-engine/src/training/train_catalyst.py +169 -0
- package/chronovisor-engine/src/training/train_classifier.py +159 -0
- package/chronovisor-engine/src/training/train_conformal.py +106 -0
- package/chronovisor-engine/src/training/train_direction.py +215 -0
- package/chronovisor-engine/src/training/train_drift.py +57 -0
- package/chronovisor-engine/src/training/train_isotonic.py +58 -0
- package/chronovisor-engine/src/training/train_lstm.py +217 -0
- package/chronovisor-engine/src/training/train_microstructure.py +102 -0
- package/chronovisor-engine/src/training/train_narrative.py +168 -0
- package/chronovisor-engine/src/training/train_pump.py +109 -0
- package/chronovisor-engine/src/training/train_regime.py +116 -0
- package/chronovisor-engine/src/training/train_rug.py +58 -0
- package/chronovisor-engine/src/training/train_sentiment.py +63 -0
- package/chronovisor-engine/src/training/train_stacking_meta.py +74 -0
- package/chronovisor-engine/src/training/train_target_quantile.py +115 -0
- package/chronovisor-engine/src/training/train_trend.py +101 -0
- package/dist/index.js +19124 -11698
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Inference module — loads trained models and runs predictions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_all_models():
|
|
10
|
+
"""Load all available trained models from disk.
|
|
11
|
+
|
|
12
|
+
Returns dict of model_name -> loaded model object.
|
|
13
|
+
"""
|
|
14
|
+
models = {}
|
|
15
|
+
|
|
16
|
+
lstm_path = MODEL_DIR / "lstm_predictor.pt"
|
|
17
|
+
if lstm_path.exists():
|
|
18
|
+
import torch
|
|
19
|
+
models["lstm"] = torch.load(lstm_path, weights_only=True)
|
|
20
|
+
|
|
21
|
+
clf_path = MODEL_DIR / "signal_classifier.joblib"
|
|
22
|
+
if clf_path.exists():
|
|
23
|
+
import joblib
|
|
24
|
+
models["classifier"] = joblib.load(clf_path)
|
|
25
|
+
|
|
26
|
+
anomaly_path = MODEL_DIR / "anomaly_detector.joblib"
|
|
27
|
+
if anomaly_path.exists():
|
|
28
|
+
import joblib
|
|
29
|
+
models["anomaly"] = joblib.load(anomaly_path)
|
|
30
|
+
|
|
31
|
+
return models
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_model_versions() -> dict[str, str]:
|
|
35
|
+
"""Get version info for all available models."""
|
|
36
|
+
versions = {}
|
|
37
|
+
for name in ["lstm_predictor.pt", "signal_classifier.joblib", "anomaly_detector.joblib"]:
|
|
38
|
+
path = MODEL_DIR / name
|
|
39
|
+
if path.exists():
|
|
40
|
+
stat = path.stat()
|
|
41
|
+
versions[name] = f"mtime:{stat.st_mtime:.0f}"
|
|
42
|
+
else:
|
|
43
|
+
versions[name] = "not-trained"
|
|
44
|
+
return versions
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Research catalog for exactness-focused model expansion.
|
|
2
|
+
|
|
3
|
+
This module turns the ML roadmap into structured data the API can expose.
|
|
4
|
+
The intent is not to claim deterministic "exact predictions"; instead it
|
|
5
|
+
describes the model families, engines, and rollout phases required to push
|
|
6
|
+
Vizzor toward tighter directional accuracy, better price targeting, narrower
|
|
7
|
+
intervals, and better calibration.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections import Counter
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _entry(
|
|
16
|
+
model_id: str,
|
|
17
|
+
name: str,
|
|
18
|
+
category: str,
|
|
19
|
+
engine: str,
|
|
20
|
+
target: str,
|
|
21
|
+
horizons: list[str],
|
|
22
|
+
feature_groups: list[str],
|
|
23
|
+
training_cadence: str,
|
|
24
|
+
exactness_role: str,
|
|
25
|
+
rollout_phase: str,
|
|
26
|
+
status: str = "candidate",
|
|
27
|
+
) -> dict:
|
|
28
|
+
return {
|
|
29
|
+
"id": model_id,
|
|
30
|
+
"name": name,
|
|
31
|
+
"category": category,
|
|
32
|
+
"engine": engine,
|
|
33
|
+
"target": target,
|
|
34
|
+
"horizons": horizons,
|
|
35
|
+
"feature_groups": feature_groups,
|
|
36
|
+
"training_cadence": training_cadence,
|
|
37
|
+
"exactness_role": exactness_role,
|
|
38
|
+
"rollout_phase": rollout_phase,
|
|
39
|
+
"status": status,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
MODEL_CATALOG = [
|
|
44
|
+
# Directional core
|
|
45
|
+
_entry("direction_xgb_scalp", "XGBoost Direction Scalp", "directional_core", "xgboost", "3-class next-move classification", ["5m", "15m", "30m"], ["ohlcv", "technical", "derivatives", "microstructure"], "hourly", "Fast low-latency directional backbone for scalp horizons", "phase_1", "active"),
|
|
46
|
+
_entry("direction_xgb_standard", "XGBoost Direction Standard", "directional_core", "xgboost", "3-class next-move classification", ["1h", "4h"], ["ohlcv", "technical", "sentiment", "derivatives"], "6h", "Main production classifier for standard intraday horizons", "phase_1", "active"),
|
|
47
|
+
_entry("direction_xgb_position", "XGBoost Direction Position", "directional_core", "xgboost", "3-class next-move classification", ["1d", "7d"], ["ohlcv", "macro", "onchain", "prediction_markets"], "daily", "Slower higher-context classifier for swing horizons", "phase_1", "active"),
|
|
48
|
+
_entry("direction_lgbm_multihorizon", "LightGBM Direction Multi-Horizon", "directional_core", "lightgbm", "3-class multi-horizon classification", ["15m", "1h", "4h", "1d"], ["ohlcv", "technical", "market_snapshots"], "6h", "Benchmark alternative to XGBoost for tree-based direction", "phase_2"),
|
|
49
|
+
_entry("direction_catboost_contextual", "CatBoost Direction Contextual", "directional_core", "catboost", "3-class context-aware classification", ["1h", "4h", "1d"], ["technical", "onchain", "sentiment", "metadata"], "daily", "Handles mixed numeric and categorical context with less preprocessing", "phase_2"),
|
|
50
|
+
_entry("direction_temporal_cnn", "Temporal CNN Direction", "directional_core", "pytorch-temporal-cnn", "3-class sequence classification", ["5m", "15m", "1h"], ["ohlcv_sequences", "derived_indicators"], "6h", "Captures short-term local structure missed by tabular trees", "phase_2"),
|
|
51
|
+
_entry("direction_tcn", "Temporal Convolution Network Direction", "directional_core", "pytorch-tcn", "3-class sequence classification", ["15m", "1h", "4h"], ["ohlcv_sequences", "derivatives_sequences"], "6h", "Longer receptive field directional learner with predictable latency", "phase_2"),
|
|
52
|
+
_entry("direction_transformer_encoder", "Transformer Encoder Direction", "directional_core", "pytorch-transformer", "3-class sequence classification", ["1h", "4h", "1d"], ["multi_source_sequences", "event_flags"], "daily", "High-capacity sequence benchmark for cross-signal timing", "phase_3"),
|
|
53
|
+
# Target regression
|
|
54
|
+
_entry("target_xgb_quantile", "XGBoost Quantile Target", "target_regression", "xgboost", "price delta regression", ["15m", "1h", "4h"], ["technical", "ohlcv", "derivatives"], "6h", "Produces tighter bull/base/bear target ladders", "phase_1"),
|
|
55
|
+
_entry("target_lightgbm_quantile", "LightGBM Quantile Target", "target_regression", "lightgbm", "quantile target regression", ["1h", "4h", "1d"], ["technical", "onchain", "sentiment"], "6h", "Fast interval-aware target model for production A/B tests", "phase_2"),
|
|
56
|
+
_entry("target_catboost_delta", "CatBoost Target Delta", "target_regression", "catboost", "next-horizon return regression", ["1h", "4h", "1d"], ["technical", "macro", "market_metadata"], "daily", "Robust target model when metadata shifts across assets", "phase_2"),
|
|
57
|
+
_entry("target_elasticnet_baseline", "ElasticNet Target Baseline", "target_regression", "sklearn-elasticnet", "linear target regression", ["1h", "4h", "1d"], ["technical", "macro"], "daily", "Low-variance baseline and residual anchor for ensembles", "phase_1"),
|
|
58
|
+
_entry("target_random_forest_residual", "Random Forest Residual Target", "target_regression", "sklearn-rf", "residual regression", ["1h", "4h"], ["base_model_residuals", "technical"], "6h", "Learns non-linear residual corrections on top of baseline targets", "phase_2"),
|
|
59
|
+
_entry("target_ngboost_distribution", "NGBoost Return Distribution", "target_regression", "ngboost", "distributional return regression", ["1h", "4h", "1d"], ["technical", "onchain", "sentiment"], "daily", "Predicts both expected move and uncertainty together", "phase_3"),
|
|
60
|
+
_entry("target_nbeats_sequence", "N-BEATS Target Forecaster", "target_regression", "pytorch-nbeats", "multi-step point forecast", ["4h", "1d", "7d"], ["ohlcv_sequences", "seasonality"], "daily", "Sequence model for cleaner medium-horizon price trajectories", "phase_3"),
|
|
61
|
+
_entry("target_nhits_sequence", "N-HiTS Target Forecaster", "target_regression", "pytorch-nhits", "multi-step point forecast", ["4h", "1d", "7d"], ["ohlcv_sequences", "event_sequences"], "daily", "Long-horizon benchmark with strong time-series inductive bias", "phase_3"),
|
|
62
|
+
# Interval and uncertainty
|
|
63
|
+
_entry("interval_quantile_random_forest", "Quantile Random Forest Interval", "interval_uncertainty", "quantile-rf", "prediction interval estimation", ["1h", "4h", "1d"], ["technical", "residuals"], "6h", "Generates fallback bull/base/bear bands without neural infra", "phase_2"),
|
|
64
|
+
_entry("interval_conformal_calibrator", "Conformal Interval Calibrator", "interval_uncertainty", "conformal", "distribution-free interval calibration", ["15m", "1h", "4h", "1d"], ["model_errors", "residual_history"], "hourly", "Keeps target bands honest under distribution shift", "phase_1", "active"),
|
|
65
|
+
_entry("interval_tft_quantile", "Temporal Fusion Transformer Quantile", "interval_uncertainty", "pytorch-tft", "quantile time-series forecasting", ["4h", "1d", "7d"], ["multi_source_sequences", "known_future_covariates"], "daily", "Quantile forecasts with interpretable attention and gating", "phase_3"),
|
|
66
|
+
_entry("interval_deepar_distribution", "DeepAR Distribution Forecaster", "interval_uncertainty", "pytorch-deepar", "probabilistic sequence forecasting", ["4h", "1d", "7d"], ["ohlcv_sequences", "covariates"], "daily", "Probabilistic benchmark for medium-horizon path forecasts", "phase_3"),
|
|
67
|
+
_entry("interval_bsts", "Bayesian Structural Time Series", "interval_uncertainty", "bsts", "component-based probabilistic forecasting", ["1d", "7d"], ["price", "seasonality", "macro"], "daily", "Stable uncertainty model when sample sizes are small", "phase_3"),
|
|
68
|
+
_entry("interval_mdn", "Mixture Density Network", "interval_uncertainty", "pytorch-mdn", "mixture distribution regression", ["1h", "4h", "1d"], ["technical", "onchain", "sentiment"], "daily", "Captures multi-modal outcomes in event-heavy markets", "phase_4"),
|
|
69
|
+
# Regime and state
|
|
70
|
+
_entry("regime_hmm", "Hidden Markov Regime Detector", "regime_state", "hmmlearn", "latent market state classification", ["1h", "4h", "1d"], ["returns", "volatility", "volume"], "6h", "Baseline latent state engine for regime-aware routing", "phase_1", "active"),
|
|
71
|
+
_entry("regime_hsmm", "Hidden Semi-Markov Regime Detector", "regime_state", "pyhsmm", "duration-aware state classification", ["4h", "1d"], ["returns", "volatility", "macro"], "daily", "Improves state persistence and regime duration modeling", "phase_2"),
|
|
72
|
+
_entry("regime_markov_switching_ar", "Markov Switching AR", "regime_state", "statsmodels-markov", "switching autoregressive regime inference", ["4h", "1d"], ["returns", "volatility"], "daily", "Useful benchmark for interpretable bull/bear transitions", "phase_2"),
|
|
73
|
+
_entry("regime_change_point", "Bayesian Change Point Regime", "regime_state", "ruptures-bayes", "structural break detection", ["15m", "1h", "4h"], ["returns", "volume", "oi"], "hourly", "Detects sudden market structure shifts before classifiers drift", "phase_2"),
|
|
74
|
+
_entry("regime_gmm_liquidity", "Gaussian Mixture Liquidity State", "regime_state", "sklearn-gmm", "market micro-regime clustering", ["5m", "15m", "1h"], ["spread", "depth", "volume"], "hourly", "Separates liquid continuation from thin reflexive chop", "phase_3"),
|
|
75
|
+
_entry("regime_variational_encoder", "Variational Regime Encoder", "regime_state", "pytorch-vae", "unsupervised latent regime discovery", ["1h", "4h", "1d"], ["multi_source_sequences"], "daily", "Finds non-obvious latent states for expert routing", "phase_4"),
|
|
76
|
+
# Microstructure and event engines
|
|
77
|
+
_entry("micro_hawkes_flow", "Hawkes Flow Excitation", "microstructure", "hawkes", "event intensity forecasting", ["1m", "5m", "15m"], ["trades", "liquidations", "orderflow"], "15m", "Models self-exciting bursts before pumps and squeezes", "phase_3"),
|
|
78
|
+
_entry("micro_orderbook_imbalance", "Order Book Imbalance Classifier", "microstructure", "lightgbm", "short-horizon move classification", ["1m", "5m", "15m"], ["depth", "spread", "microprice"], "5m", "Pushes exactness on execution-sensitive short horizons", "phase_2"),
|
|
79
|
+
_entry("micro_liquidation_cascade", "Liquidation Cascade Detector", "microstructure", "xgboost", "cascade probability classification", ["5m", "15m", "1h"], ["liquidations", "oi", "funding"], "15m", "Flags asymmetry when leverage unwind risk dominates", "phase_2"),
|
|
80
|
+
_entry("micro_vpvr_levels", "VPVR Level Ranker", "microstructure", "lambda-mart", "support/resistance ranking", ["15m", "1h", "4h"], ["volume_profile", "market_structure"], "hourly", "Improves exact target placement around high-volume nodes", "phase_3"),
|
|
81
|
+
_entry("micro_autoencoder_volume_spike", "Volume Spike Autoencoder", "microstructure", "pytorch-autoencoder", "anomaly detection", ["1m", "5m", "15m"], ["volume_sequences", "trade_counts"], "15m", "Separates genuine breakout participation from noise", "phase_2"),
|
|
82
|
+
_entry("micro_refined_cusum", "Refined CUSUM Event Model", "microstructure", "cusum-plus-features", "pump/dump event detection", ["1m", "5m", "15m"], ["returns", "volume", "baseline_drift"], "15m", "Keeps ultra-fast manipulation detection cheap and explainable", "phase_1", "active"),
|
|
83
|
+
# Narrative and sentiment
|
|
84
|
+
_entry("nlp_crypto_finbert", "Crypto FinBERT Sentiment", "narrative_sentiment", "transformers-finbert", "headline/body sentiment classification", ["1h", "4h", "1d"], ["news", "social_posts"], "6h", "Domain-tuned sentiment signal for event-driven moves", "phase_2"),
|
|
85
|
+
_entry("nlp_deberta_impact", "DeBERTa Headline Impact", "narrative_sentiment", "transformers-deberta", "impact classification", ["1h", "4h", "1d"], ["news", "token_context"], "6h", "Estimates whether a headline matters, not just polarity", "phase_2"),
|
|
86
|
+
_entry("nlp_bertopic", "BERTopic Narrative Clusters", "narrative_sentiment", "bertopic", "topic discovery", ["4h", "1d", "7d"], ["news_corpus", "social_corpus"], "daily", "Surfaces emergent narratives before they show up in price", "phase_3"),
|
|
87
|
+
_entry("nlp_event_extractor", "Market Event Extractor", "narrative_sentiment", "transformers-seq2seq", "structured event extraction", ["1h", "4h", "1d"], ["news", "calendar", "governance_posts"], "6h", "Turns raw text into discrete catalysts for exactness layers", "phase_3"),
|
|
88
|
+
_entry("nlp_cross_source_gnn", "Cross-Source Influence Graph", "narrative_sentiment", "graph-neural-network", "propagation scoring", ["4h", "1d"], ["news", "social", "onchain_mentions"], "daily", "Measures whether a narrative is spreading into tradable attention", "phase_4"),
|
|
89
|
+
_entry("nlp_regulatory_stance", "Regulatory Stance Classifier", "narrative_sentiment", "transformers-roberta", "stance classification", ["1d", "7d"], ["regulatory_text"], "daily", "Specialized model for one of the highest-impact catalyst classes", "phase_3"),
|
|
90
|
+
_entry("nlp_source_reliability", "Source Reliability Ranker", "narrative_sentiment", "xgboost", "source quality ranking", ["1h", "4h", "1d"], ["publisher_history", "engagement", "correction_rate"], "daily", "Discounts low-quality narratives before they pollute ensembles", "phase_2"),
|
|
91
|
+
# On-chain and fundamentals
|
|
92
|
+
_entry("onchain_tvl_factor", "TVL Factor Model", "onchain_fundamental", "xgboost", "fundamental momentum regression", ["1d", "7d"], ["tvl", "revenue", "fees", "users"], "daily", "Provides slower fundamental anchor for swing predictions", "phase_2"),
|
|
93
|
+
_entry("onchain_exchange_flow", "Exchange Flow Classifier", "onchain_fundamental", "xgboost", "flow direction classification", ["1h", "4h", "1d"], ["exchange_flows", "whale_transfers"], "hourly", "Measures accumulation versus distribution pressure", "phase_2"),
|
|
94
|
+
_entry("onchain_active_address_residual", "Active Address Residual Model", "onchain_fundamental", "lightgbm", "valuation residual regression", ["1d", "7d"], ["active_addresses", "price", "volume"], "daily", "Finds divergence between price and network usage", "phase_3"),
|
|
95
|
+
_entry("onchain_mvrv_nvt", "MVRV/NVT Valuation Residual", "onchain_fundamental", "elasticnet-plus-xgb", "valuation residual regression", ["1d", "7d"], ["mvrv", "nvt", "supply_dynamics"], "daily", "Tighter medium-horizon fair-value estimates for BTC majors", "phase_2"),
|
|
96
|
+
_entry("onchain_stablecoin_liquidity", "Stablecoin Liquidity Model", "onchain_fundamental", "lightgbm", "risk-on liquidity scoring", ["4h", "1d", "7d"], ["stablecoin_supply", "flows", "dominance"], "6h", "Captures deployable buying power before spot rotation", "phase_3"),
|
|
97
|
+
_entry("onchain_cycle_composite", "Cycle Composite Fair Value", "onchain_fundamental", "ensemble-composite", "cycle phase and fair value estimation", ["1d", "7d"], ["halving", "hashrate", "mvrv", "nvt"], "daily", "Improves exact swing targets by anchoring them to cycle context", "phase_1", "active"),
|
|
98
|
+
# Risk and security
|
|
99
|
+
_entry("risk_rug_xgboost", "Calibrated Rug XGBoost", "risk_security", "xgboost", "rug probability classification", ["event"], ["bytecode", "holder_distribution", "taxes"], "daily", "Cleaner risk gating before any automated long bias", "phase_2"),
|
|
100
|
+
_entry("risk_bytecode_autoencoder", "Bytecode Autoencoder", "risk_security", "pytorch-autoencoder", "contract anomaly detection", ["event"], ["opcode_features", "selector_entropy"], "daily", "Detects novel malicious bytecode patterns beyond rules", "phase_3"),
|
|
101
|
+
_entry("risk_wallet_behavior_transformer", "Wallet Behavior Transformer", "risk_security", "pytorch-transformer", "wallet role classification", ["event"], ["transaction_sequences", "counterparty_graph"], "daily", "Separates smart money from spoofing and wash behavior", "phase_3"),
|
|
102
|
+
_entry("risk_honeypot_calibrator", "Honeypot Probability Calibrator", "risk_security", "isotonic-plus-xgboost", "honeypot probability calibration", ["event"], ["swap_failures", "taxes", "sellability"], "daily", "Reduces false positives in token safety gates", "phase_2"),
|
|
103
|
+
_entry("risk_contract_graph", "Contract Graph Risk Model", "risk_security", "graph-neural-network", "entity risk propagation", ["event"], ["contract_graph", "deployer_graph", "wallet_graph"], "daily", "Captures ecosystem-level contamination and related rugs", "phase_4"),
|
|
104
|
+
# Meta ensemble and control plane
|
|
105
|
+
_entry("meta_stacking", "Stacking Meta Learner", "meta_ensemble", "logistic-regression-plus-trees", "ensemble blending", ["15m", "1h", "4h", "1d"], ["base_model_outputs", "regime", "confidence"], "hourly", "Learns which base model to trust in each context", "phase_1"),
|
|
106
|
+
_entry("meta_bayesian_model_averaging", "Bayesian Model Averaging", "meta_ensemble", "bayesian", "probability blending", ["1h", "4h", "1d"], ["base_model_outputs", "historical_accuracy"], "hourly", "Smooths overfitting and improves calibrated ensemble probability", "phase_2"),
|
|
107
|
+
_entry("meta_isotonic", "Isotonic Probability Calibrator", "meta_ensemble", "sklearn-isotonic", "probability calibration", ["15m", "1h", "4h", "1d"], ["prediction_outcomes"], "hourly", "Turns confidence into something operationally trustworthy", "phase_1", "active"),
|
|
108
|
+
_entry("meta_temperature_scaling", "Temperature Scaling Calibrator", "meta_ensemble", "temperature-scaling", "logit calibration", ["15m", "1h", "4h"], ["classification_logits"], "hourly", "Cheap calibration layer for neural classifiers", "phase_2"),
|
|
109
|
+
_entry("meta_drift_detector", "PSI/KS Drift Detector", "meta_ensemble", "drift-monitor", "distribution shift detection", ["all"], ["feature_distributions", "prediction_errors"], "hourly", "Protects exactness by downgrading stale models automatically", "phase_1"),
|
|
110
|
+
_entry("meta_champion_challenger", "Champion-Challenger Router", "meta_ensemble", "policy-engine", "online promotion routing", ["all"], ["benchmarks", "latency", "accuracy"], "hourly", "Operational engine for safe rollout of 50+ model candidates", "phase_1"),
|
|
111
|
+
_entry("meta_residual_corrector", "Residual Error Corrector", "meta_ensemble", "lightgbm", "residual correction regression", ["1h", "4h", "1d"], ["ensemble_outputs", "recent_errors"], "hourly", "Shrinks repeated target bias without replacing the whole stack", "phase_2"),
|
|
112
|
+
_entry("meta_policy_bandit", "Policy Bandit Model Router", "meta_ensemble", "contextual-bandit", "adaptive model selection", ["15m", "1h", "4h", "1d"], ["regime", "asset_type", "recent_scores"], "hourly", "Routes symbols and horizons toward the current best expert", "phase_3"),
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
ROADMAP_PHASES = [
|
|
117
|
+
{
|
|
118
|
+
"phase": "phase_0_foundation",
|
|
119
|
+
"duration": "1-2 weeks",
|
|
120
|
+
"focus": "repair the training plumbing before adding more models",
|
|
121
|
+
"deliverables": [
|
|
122
|
+
"unify train/retrain payload contracts",
|
|
123
|
+
"fix workflow port and endpoint mismatches",
|
|
124
|
+
"make model artifact volume writable",
|
|
125
|
+
"standardize artifact naming and model reload behavior",
|
|
126
|
+
"replace synthetic training placeholders with real dataset adapters",
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"phase": "phase_1_production_baseline",
|
|
131
|
+
"duration": "2-4 weeks",
|
|
132
|
+
"focus": "establish a stable exactness baseline around direction, targets, and calibration",
|
|
133
|
+
"deliverables": [
|
|
134
|
+
"direction_xgb_* trio as production backbone",
|
|
135
|
+
"target_xgb_quantile plus conformal intervals",
|
|
136
|
+
"stacking meta learner with isotonic calibration",
|
|
137
|
+
"drift detector and champion-challenger routing",
|
|
138
|
+
"benchmark suite for latency, hit rate, and calibration error",
|
|
139
|
+
],
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
"phase": "phase_2_benchmark_matrix",
|
|
143
|
+
"duration": "4-8 weeks",
|
|
144
|
+
"focus": "run 20+ challenger models against walk-forward and asset-segmented benchmarks",
|
|
145
|
+
"deliverables": [
|
|
146
|
+
"tree-based challengers across direction and targets",
|
|
147
|
+
"microstructure specialists for short horizons",
|
|
148
|
+
"fundamental and narrative challengers for swing horizons",
|
|
149
|
+
"per-horizon promotion gates using Brier score, MAE, and interval coverage",
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"phase": "phase_3_sequence_and_probabilistic",
|
|
154
|
+
"duration": "8-12 weeks",
|
|
155
|
+
"focus": "introduce sequence models where they beat tabular baselines by clear margins",
|
|
156
|
+
"deliverables": [
|
|
157
|
+
"N-BEATS, N-HiTS, TFT, DeepAR, transformer encoders",
|
|
158
|
+
"probabilistic target distributions instead of point-only targets",
|
|
159
|
+
"specialized regulatory and catalyst NLP models",
|
|
160
|
+
"state-aware routers for regime-conditioned ensembles",
|
|
161
|
+
],
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
"phase": "phase_4_online_learning_control_plane",
|
|
165
|
+
"duration": "12+ weeks",
|
|
166
|
+
"focus": "close the loop with live learning, residual correction, and safe promotion",
|
|
167
|
+
"deliverables": [
|
|
168
|
+
"policy bandit routing",
|
|
169
|
+
"residual correctors trained on recent model bias",
|
|
170
|
+
"cross-source influence graphs and graph risk models",
|
|
171
|
+
"automated rollback on calibration drift or latency regression",
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def get_model_catalog() -> dict:
|
|
178
|
+
counts = Counter(model["category"] for model in MODEL_CATALOG)
|
|
179
|
+
phase_counts = Counter(model["rollout_phase"] for model in MODEL_CATALOG)
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
"generated_at": "static",
|
|
183
|
+
"total_models": len(MODEL_CATALOG),
|
|
184
|
+
"category_counts": dict(sorted(counts.items())),
|
|
185
|
+
"phase_counts": dict(sorted(phase_counts.items())),
|
|
186
|
+
"models": MODEL_CATALOG,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def get_training_roadmap() -> dict:
|
|
191
|
+
return {
|
|
192
|
+
"objective": "increase prediction exactness through decomposition, calibration, and champion-challenger routing",
|
|
193
|
+
"principles": [
|
|
194
|
+
"Exact predictions are probabilistic; improve exactness by separating direction, magnitude, interval, and confidence calibration.",
|
|
195
|
+
"Do not promote a new model until it wins on walk-forward accuracy, calibration, latency, and stability together.",
|
|
196
|
+
"Short horizons need microstructure specialists; long horizons need macro, on-chain, and narrative context.",
|
|
197
|
+
"Retraining without drift checks and artifact hygiene degrades the system faster than adding more models helps it.",
|
|
198
|
+
],
|
|
199
|
+
"success_metrics": [
|
|
200
|
+
"directional hit rate by horizon and asset bucket",
|
|
201
|
+
"Brier score and expected calibration error",
|
|
202
|
+
"MAE and MAPE for target deltas",
|
|
203
|
+
"interval coverage versus target width",
|
|
204
|
+
"latency budget adherence per model family",
|
|
205
|
+
"promotion win rate in champion-challenger tests",
|
|
206
|
+
],
|
|
207
|
+
"recommended_backbone": [
|
|
208
|
+
"direction_xgb_scalp",
|
|
209
|
+
"direction_xgb_standard",
|
|
210
|
+
"direction_xgb_position",
|
|
211
|
+
"target_xgb_quantile",
|
|
212
|
+
"interval_conformal_calibrator",
|
|
213
|
+
"meta_stacking",
|
|
214
|
+
"meta_isotonic",
|
|
215
|
+
"meta_drift_detector",
|
|
216
|
+
],
|
|
217
|
+
"phases": ROADMAP_PHASES,
|
|
218
|
+
"catalog_summary": get_model_catalog(),
|
|
219
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""Isolation Forest anomaly detector.
|
|
2
|
+
|
|
3
|
+
Input: whale transfer amounts, volume spikes, funding rate deviations
|
|
4
|
+
Output: anomaly scores indicating suspicious activity.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
MODEL_DIR = Path(os.getenv("MODEL_DIR", "models"))
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class AnomalyDetector:
|
|
16
|
+
def __init__(self):
|
|
17
|
+
self.version = "0.1.0"
|
|
18
|
+
self.is_loaded = False
|
|
19
|
+
self.last_trained: str | None = None
|
|
20
|
+
self.model = None
|
|
21
|
+
|
|
22
|
+
def load(self):
|
|
23
|
+
"""Load trained Isolation Forest or use threshold-based fallback."""
|
|
24
|
+
model_path = MODEL_DIR / "anomaly_detector.joblib"
|
|
25
|
+
if model_path.exists():
|
|
26
|
+
try:
|
|
27
|
+
import joblib
|
|
28
|
+
self.model = joblib.load(model_path)
|
|
29
|
+
self.is_loaded = True
|
|
30
|
+
self.last_trained = str(model_path.stat().st_mtime)
|
|
31
|
+
except Exception:
|
|
32
|
+
self._init_heuristic()
|
|
33
|
+
else:
|
|
34
|
+
self._init_heuristic()
|
|
35
|
+
|
|
36
|
+
def _init_heuristic(self):
|
|
37
|
+
self.is_loaded = True
|
|
38
|
+
self.version = "0.1.0-heuristic"
|
|
39
|
+
|
|
40
|
+
def detect(self, flow: dict) -> dict:
|
|
41
|
+
"""Detect anomaly in a token flow.
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
dict with keys: score, is_anomaly, type, details
|
|
45
|
+
"""
|
|
46
|
+
if self.model is not None:
|
|
47
|
+
return self._detect_model(flow)
|
|
48
|
+
return self._detect_heuristic(flow)
|
|
49
|
+
|
|
50
|
+
def _detect_model(self, flow: dict) -> dict:
|
|
51
|
+
"""Run inference through trained Isolation Forest."""
|
|
52
|
+
features = np.array([[
|
|
53
|
+
flow.get("amount", 0),
|
|
54
|
+
1 if flow.get("type") == "transfer" else 0,
|
|
55
|
+
]])
|
|
56
|
+
score = -self.model.score_samples(features)[0] # Higher = more anomalous
|
|
57
|
+
normalized = min(1.0, max(0.0, (score + 0.5) / 1.0))
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
"score": normalized,
|
|
61
|
+
"is_anomaly": normalized > 0.7,
|
|
62
|
+
"type": self._classify_type(flow, normalized),
|
|
63
|
+
"details": f"Isolation Forest score: {normalized:.3f}",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def _detect_heuristic(self, flow: dict) -> dict:
|
|
67
|
+
"""Threshold-based anomaly detection until model is trained."""
|
|
68
|
+
amount = flow.get("amount", 0)
|
|
69
|
+
flow_type = flow.get("type", "transfer")
|
|
70
|
+
|
|
71
|
+
score = 0.0
|
|
72
|
+
anomaly_type = "unknown"
|
|
73
|
+
details = []
|
|
74
|
+
|
|
75
|
+
# Whale threshold: transfers > $1M
|
|
76
|
+
if amount > 1_000_000:
|
|
77
|
+
score += 0.4
|
|
78
|
+
anomaly_type = "whale_transfer"
|
|
79
|
+
details.append(f"Large transfer: ${amount:,.0f}")
|
|
80
|
+
|
|
81
|
+
if amount > 10_000_000:
|
|
82
|
+
score += 0.3
|
|
83
|
+
details.append("Mega whale movement")
|
|
84
|
+
|
|
85
|
+
# Bridge transfers are inherently more suspicious
|
|
86
|
+
if flow_type == "bridge":
|
|
87
|
+
score += 0.2
|
|
88
|
+
details.append("Cross-chain bridge transfer")
|
|
89
|
+
|
|
90
|
+
normalized = min(1.0, score)
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
"score": normalized,
|
|
94
|
+
"is_anomaly": normalized > 0.5,
|
|
95
|
+
"type": anomaly_type if score > 0.3 else "unknown",
|
|
96
|
+
"details": "; ".join(details) if details else "No anomaly detected",
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
def _classify_type(self, flow: dict, score: float) -> str:
|
|
100
|
+
if flow.get("amount", 0) > 1_000_000:
|
|
101
|
+
return "whale_transfer"
|
|
102
|
+
if score > 0.8:
|
|
103
|
+
return "volume_spike"
|
|
104
|
+
return "unknown"
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Blockchain Cycle Analyzer — cycle phase detection from on-chain fundamentals."""
|
|
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
|
+
PHASES = ["accumulation", "early_markup", "late_markup", "distribution", "markdown"]
|
|
12
|
+
|
|
13
|
+
# Asymmetric phase scores (from plan)
|
|
14
|
+
PHASE_SCORES = {
|
|
15
|
+
"accumulation": 50,
|
|
16
|
+
"early_markup": 70,
|
|
17
|
+
"late_markup": 15,
|
|
18
|
+
"distribution": -45,
|
|
19
|
+
"markdown": -65,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
# Phase boundaries as % of cycle
|
|
23
|
+
PHASE_BOUNDARIES = {
|
|
24
|
+
"accumulation": (0, 35),
|
|
25
|
+
"early_markup": (35, 55),
|
|
26
|
+
"late_markup": (55, 70),
|
|
27
|
+
"distribution": (70, 85),
|
|
28
|
+
"markdown": (85, 100),
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class BlockchainCycleAnalyzer:
|
|
33
|
+
"""Detects BTC cycle phase from on-chain metrics using trained model or heuristic."""
|
|
34
|
+
|
|
35
|
+
FEATURE_KEYS = [
|
|
36
|
+
"halving_cycle_progress",
|
|
37
|
+
"days_since_halving",
|
|
38
|
+
"days_to_next_halving",
|
|
39
|
+
"block_reward",
|
|
40
|
+
"hashrate_change_30d",
|
|
41
|
+
"difficulty_change_14d",
|
|
42
|
+
"nvt_ratio",
|
|
43
|
+
"mvrv_z_score",
|
|
44
|
+
"inflation_rate",
|
|
45
|
+
"fee_revenue_share",
|
|
46
|
+
"mempool_size_mb",
|
|
47
|
+
"avg_fee_rate",
|
|
48
|
+
"hash_ribbon_signal",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
self.version = "0.1.0"
|
|
53
|
+
self.is_loaded = False
|
|
54
|
+
self.last_trained: str | None = None
|
|
55
|
+
self.accuracy: float | None = None
|
|
56
|
+
self.model = None
|
|
57
|
+
|
|
58
|
+
def load(self) -> None:
|
|
59
|
+
model_path = MODEL_DIR / "blockchain_cycle_analyzer.joblib"
|
|
60
|
+
try:
|
|
61
|
+
data = joblib.load(model_path)
|
|
62
|
+
self.model = data["model"]
|
|
63
|
+
self.last_trained = data.get("trained_at")
|
|
64
|
+
self.accuracy = data.get("accuracy")
|
|
65
|
+
self.is_loaded = True
|
|
66
|
+
except Exception:
|
|
67
|
+
self.model = None
|
|
68
|
+
self.is_loaded = True # heuristic fallback ready
|
|
69
|
+
|
|
70
|
+
def predict(self, features: dict) -> dict:
|
|
71
|
+
if self.model is not None:
|
|
72
|
+
return self._predict_model(features)
|
|
73
|
+
return self._predict_heuristic(features)
|
|
74
|
+
|
|
75
|
+
def _predict_model(self, features: dict) -> dict:
|
|
76
|
+
x = np.array([[features.get(k, 0) for k in self.FEATURE_KEYS]])
|
|
77
|
+
pred = self.model.predict(x)
|
|
78
|
+
phase_idx = int(pred[0])
|
|
79
|
+
phase = PHASES[phase_idx] if phase_idx < len(PHASES) else "accumulation"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
probs = self.model.predict_proba(x)[0]
|
|
83
|
+
confidence = float(probs[phase_idx]) * 100
|
|
84
|
+
except Exception:
|
|
85
|
+
confidence = 65.0
|
|
86
|
+
|
|
87
|
+
return self._build_response(phase, confidence, features, "xgb-blockchain-cycle")
|
|
88
|
+
|
|
89
|
+
def _predict_heuristic(self, features: dict) -> dict:
|
|
90
|
+
progress = features.get("halving_cycle_progress", 0)
|
|
91
|
+
mvrv = features.get("mvrv_z_score", 1.5)
|
|
92
|
+
nvt = features.get("nvt_ratio", 55)
|
|
93
|
+
hashrate_chg = features.get("hashrate_change_30d", 0)
|
|
94
|
+
hash_ribbon = features.get("hash_ribbon_signal", 0)
|
|
95
|
+
|
|
96
|
+
# Primary: cycle progress determines base phase
|
|
97
|
+
phase = "accumulation"
|
|
98
|
+
for p, (lo, hi) in PHASE_BOUNDARIES.items():
|
|
99
|
+
if lo <= progress < hi:
|
|
100
|
+
phase = p
|
|
101
|
+
break
|
|
102
|
+
|
|
103
|
+
# Secondary: MVRV Z-Score can override phase detection
|
|
104
|
+
confidence = 55.0
|
|
105
|
+
|
|
106
|
+
if mvrv < 0:
|
|
107
|
+
# Strong buy signal — likely accumulation regardless of progress
|
|
108
|
+
if phase not in ("accumulation", "markdown"):
|
|
109
|
+
phase = "accumulation"
|
|
110
|
+
confidence = 75.0
|
|
111
|
+
elif mvrv > 6:
|
|
112
|
+
# Extreme overvaluation — likely distribution/markdown
|
|
113
|
+
if phase in ("accumulation", "early_markup"):
|
|
114
|
+
phase = "distribution"
|
|
115
|
+
confidence = 80.0
|
|
116
|
+
elif mvrv > 4:
|
|
117
|
+
# Getting expensive
|
|
118
|
+
if phase == "early_markup":
|
|
119
|
+
phase = "late_markup"
|
|
120
|
+
confidence = 65.0
|
|
121
|
+
|
|
122
|
+
# NVT adjustment
|
|
123
|
+
if nvt > 80 and phase in ("early_markup", "late_markup"):
|
|
124
|
+
phase = "distribution"
|
|
125
|
+
confidence = max(confidence, 60.0)
|
|
126
|
+
elif nvt < 30 and phase == "markdown":
|
|
127
|
+
phase = "accumulation"
|
|
128
|
+
confidence = max(confidence, 60.0)
|
|
129
|
+
|
|
130
|
+
# Hash ribbon golden cross in accumulation = strong signal
|
|
131
|
+
if hash_ribbon == 1 and phase == "accumulation":
|
|
132
|
+
confidence = min(90.0, confidence + 15)
|
|
133
|
+
|
|
134
|
+
# Hashrate declining significantly
|
|
135
|
+
if hashrate_chg < -15 and phase in ("late_markup", "distribution"):
|
|
136
|
+
phase = "markdown"
|
|
137
|
+
confidence = max(confidence, 65.0)
|
|
138
|
+
|
|
139
|
+
return self._build_response(phase, confidence, features, "heuristic-blockchain-cycle")
|
|
140
|
+
|
|
141
|
+
def _build_response(self, phase: str, confidence: float, features: dict, model: str) -> dict:
|
|
142
|
+
mvrv = features.get("mvrv_z_score", 1.5)
|
|
143
|
+
nvt = features.get("nvt_ratio", 55)
|
|
144
|
+
inflation = features.get("inflation_rate", 0.83)
|
|
145
|
+
hashrate_chg = features.get("hashrate_change_30d", 0)
|
|
146
|
+
hash_ribbon = features.get("hash_ribbon_signal", 0)
|
|
147
|
+
|
|
148
|
+
# Fair value estimate based on MVRV and cycle phase
|
|
149
|
+
# Simple heuristic: use phase score to estimate relative value
|
|
150
|
+
phase_score = PHASE_SCORES.get(phase, 0)
|
|
151
|
+
|
|
152
|
+
# deviation from fair: positive = overvalued, negative = undervalued
|
|
153
|
+
# MVRV Z > 3 = overvalued, MVRV Z < 1 = undervalued
|
|
154
|
+
deviation = (mvrv - 1.5) * 15 # rough scaling
|
|
155
|
+
|
|
156
|
+
# Risk factors
|
|
157
|
+
risk_factors = []
|
|
158
|
+
|
|
159
|
+
if abs(mvrv) > 3:
|
|
160
|
+
risk_factors.append({
|
|
161
|
+
"factor": "mvrv_extreme",
|
|
162
|
+
"importance": 0.35,
|
|
163
|
+
"value": round(mvrv, 4),
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
if nvt > 70:
|
|
167
|
+
risk_factors.append({
|
|
168
|
+
"factor": "nvt_elevated",
|
|
169
|
+
"importance": 0.25,
|
|
170
|
+
"value": round(nvt, 4),
|
|
171
|
+
})
|
|
172
|
+
elif nvt < 30:
|
|
173
|
+
risk_factors.append({
|
|
174
|
+
"factor": "nvt_undervalued",
|
|
175
|
+
"importance": 0.20,
|
|
176
|
+
"value": round(nvt, 4),
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
if hashrate_chg < -10:
|
|
180
|
+
risk_factors.append({
|
|
181
|
+
"factor": "hashrate_declining",
|
|
182
|
+
"importance": 0.20,
|
|
183
|
+
"value": round(hashrate_chg, 4),
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
if hash_ribbon == -1:
|
|
187
|
+
risk_factors.append({
|
|
188
|
+
"factor": "hash_ribbon_death_cross",
|
|
189
|
+
"importance": 0.30,
|
|
190
|
+
"value": -1,
|
|
191
|
+
})
|
|
192
|
+
elif hash_ribbon == 1:
|
|
193
|
+
risk_factors.append({
|
|
194
|
+
"factor": "hash_ribbon_golden_cross",
|
|
195
|
+
"importance": 0.30,
|
|
196
|
+
"value": 1,
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
if inflation > 2.0:
|
|
200
|
+
risk_factors.append({
|
|
201
|
+
"factor": "high_inflation",
|
|
202
|
+
"importance": 0.10,
|
|
203
|
+
"value": round(inflation, 4),
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
# Sort by importance descending, limit to 5
|
|
207
|
+
risk_factors.sort(key=lambda x: x["importance"], reverse=True)
|
|
208
|
+
risk_factors = risk_factors[:5]
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"cycle_phase": phase,
|
|
212
|
+
"phase_confidence": round(min(100, max(0, confidence)), 2),
|
|
213
|
+
"fair_value_estimate": round(phase_score, 2),
|
|
214
|
+
"deviation_from_fair": round(deviation, 2),
|
|
215
|
+
"risk_factors": risk_factors,
|
|
216
|
+
"model": model,
|
|
217
|
+
}
|