Spaces:
Running
Running
| import time | |
| import math | |
| import requests | |
| import numpy as np | |
| import pandas as pd | |
| from sklearn.linear_model import Ridge | |
| try: | |
| from quant_model import quant_multi_horizon_forecast, fuse_rule_signal_with_quant | |
| QUANT_MODEL_AVAILABLE = True | |
| except Exception: | |
| QUANT_MODEL_AVAILABLE = False | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # ASSET UNIVERSE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _make_assets(category, symbols, network="default", exchanges="BN", coinbase_symbols=None): | |
| coinbase_symbols = set(coinbase_symbols or []) | |
| out = [] | |
| for sym in symbols.split(): | |
| ex = exchanges | |
| if sym in coinbase_symbols: | |
| ex = "CB " + ex | |
| out.append({ | |
| "label": f"{sym} · {category}", | |
| "symbol": sym, | |
| "coinbase_pair": f"{sym}-USD", | |
| "binance_pair": f"{sym}USDT", | |
| "network": network, | |
| "category": category, | |
| "exchanges": list(dict.fromkeys(ex.split())), | |
| }) | |
| return out | |
| _CB_MAJOR = { | |
| "BTC", "ETH", "SOL", "ADA", "AVAX", "LINK", "DOGE", "SHIB", "DOT", "UNI", | |
| "AAVE", "MATIC", "NEAR", "APT", "ARB", "OP", "ATOM", "FIL", "ICP", "INJ", | |
| "RNDR", "FET", "SUI", "LTC", "BCH", "XLM", "ETC", "ALGO", "XTZ", "GRT", | |
| "SAND", "MANA", "AXS", "APE", "IMX", "STX", "LDO", "CRV", "SNX", "COMP", | |
| "MKR", "YFI", "BAT", "ZEC", "DASH", "EOS", "FLOW", "CHZ", "ENS", "DYDX", | |
| "JASMY", "BONK", "PEPE", "SEI", "TIA", "WLD", "ONDO", "JUP", "PYTH" | |
| } | |
| ASSET_UNIVERSE = [] | |
| ASSET_UNIVERSE += _make_assets( | |
| "Major Coins", | |
| "BTC ETH BNB SOL XRP ADA DOGE TRX TON AVAX SHIB DOT LINK BCH LTC NEAR UNI ICP APT ETC XLM ATOM FIL HBAR", | |
| "major", | |
| "BN WX CD ZP", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Layer 1", | |
| "SUI SEI INJ TIA ALGO VET EGLD KAS FTM XTZ EOS FLOW MINA ROSE ONE KAVA CELO ZIL QTUM IOTA NEO WAVES XEC", | |
| "layer1", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Layer 2", | |
| "ARB OP MATIC IMX STRK ZK METIS MANTA LRC SKL BOBA CELR CKB MOVR GLMR", | |
| "ethereum", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Meme Coins", | |
| "PEPE FLOKI BONK WIF BRETT TURBO MOG BOME MEW POPCAT NEIRO DOGS GIGA PONKE SLERF MYRO MEME LADYS BABYDOGE ELON TOSHI COQ", | |
| "meme", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "DeFi", | |
| "AAVE UNI MKR LDO PENDLE ONDO ENA JTO CRV SNX COMP YFI SUSHI 1INCH CAKE RUNE GMX BAL CVX RPL FXS LQTY RDNT COW", | |
| "ethereum", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "AI / DePIN", | |
| "FET TAO RNDR WLD ARKM GRT IO AKT OCEAN NMR AGIX GLM NOS PHB RLC CTXC AIOZ HIVE DUSK", | |
| "ai", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Gaming / Metaverse", | |
| "AXS SAND MANA GALA APE RON ILV PIXEL PORTAL YGG MAGIC ENJ GMT BIGTIME ALICE TLM DAR PYR", | |
| "gaming", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "RWA / Infrastructure", | |
| "LINK PYTH API3 BAND TRB UMA QNT HNT FIL AR STORJ SC ANKR LPT AUDIO MASK HIGH", | |
| "infrastructure", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Privacy / Security", | |
| "ZEC DASH SCRT MINA DCR XVG ZEN KEEP NKN", | |
| "privacy", | |
| "BN WX CD", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Exchange / Utility", | |
| "BNB OKB GT KCS CRO LEO BGB MX WRX TWT SFP C98 LIT MDT REQ COTI ACH", | |
| "utility", | |
| "BN WX CD ZP", | |
| _CB_MAJOR, | |
| ) | |
| ASSET_UNIVERSE += _make_assets( | |
| "Indian Exchange Popular", | |
| "BTC ETH SOL XRP DOGE SHIB TRX MATIC ADA LINK DOT AVAX LTC BCH UNI AAVE FET RNDR PEPE BONK FLOKI WIF NEAR ATOM FIL", | |
| "india", | |
| "WX CD ZP BN", | |
| _CB_MAJOR, | |
| ) | |
| _seen = set() | |
| _clean = [] | |
| for asset in ASSET_UNIVERSE: | |
| if asset["symbol"] not in _seen: | |
| _seen.add(asset["symbol"]) | |
| _clean.append(asset) | |
| ASSET_UNIVERSE = _clean | |
| _SESSION = requests.Session() | |
| _SESSION.headers.update({ | |
| "Accept": "application/json", | |
| "User-Agent": "CryptoNav-Terminal/6.0-Beginner-Quant", | |
| }) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # DATA FETCHING | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _normalize_symbol(asset_or_pair: str) -> str: | |
| return ( | |
| str(asset_or_pair) | |
| .replace("-USD", "") | |
| .replace("USDT", "") | |
| .replace("/", "") | |
| .upper() | |
| .strip() | |
| ) | |
| def _coinbase_candles(pair: str, granularity: int) -> pd.DataFrame: | |
| try: | |
| url = f"https://api.exchange.coinbase.com/products/{pair}/candles" | |
| response = _SESSION.get(url, params={"granularity": granularity}, timeout=12) | |
| response.raise_for_status() | |
| data = response.json() | |
| if not data or not isinstance(data, list): | |
| return pd.DataFrame() | |
| df = pd.DataFrame(data, columns=["time", "low", "high", "open", "close", "volume"]) | |
| df["time"] = pd.to_datetime(df["time"], unit="s") | |
| df.set_index("time", inplace=True) | |
| df.sort_index(inplace=True) | |
| df.rename( | |
| columns={ | |
| "open": "Open", | |
| "high": "High", | |
| "low": "Low", | |
| "close": "Close", | |
| "volume": "Volume", | |
| }, | |
| inplace=True, | |
| ) | |
| for col in ["Open", "High", "Low", "Close", "Volume"]: | |
| df[col] = df[col].astype(float) | |
| return df | |
| except Exception: | |
| return pd.DataFrame() | |
| def _binance_candles(pair: str, interval: str, limit: int = 300) -> pd.DataFrame: | |
| try: | |
| url = "https://api.binance.com/api/v3/klines" | |
| response = _SESSION.get( | |
| url, | |
| params={ | |
| "symbol": pair, | |
| "interval": interval, | |
| "limit": limit, | |
| }, | |
| timeout=12, | |
| ) | |
| response.raise_for_status() | |
| data = response.json() | |
| if not data or not isinstance(data, list): | |
| return pd.DataFrame() | |
| df = pd.DataFrame( | |
| data, | |
| columns=[ | |
| "open_time", | |
| "Open", | |
| "High", | |
| "Low", | |
| "Close", | |
| "Volume", | |
| "close_time", | |
| "quote_asset_volume", | |
| "number_of_trades", | |
| "taker_buy_base", | |
| "taker_buy_quote", | |
| "ignore", | |
| ], | |
| ) | |
| df["time"] = pd.to_datetime(df["open_time"], unit="ms") | |
| df.set_index("time", inplace=True) | |
| df = df[["Open", "High", "Low", "Close", "Volume"]].copy() | |
| for col in ["Open", "High", "Low", "Close", "Volume"]: | |
| df[col] = df[col].astype(float) | |
| return df | |
| except Exception: | |
| return pd.DataFrame() | |
| def get_candles(symbol: str, timeframe: str = "1h", limit: int = 300) -> pd.DataFrame: | |
| symbol = _normalize_symbol(symbol) | |
| coinbase_map = { | |
| "15m": 900, | |
| "1h": 3600, | |
| "4h": 21600, | |
| "1d": 86400, | |
| } | |
| binance_map = { | |
| "15m": "15m", | |
| "1h": "1h", | |
| "4h": "4h", | |
| "1d": "1d", | |
| } | |
| df = _coinbase_candles(f"{symbol}-USD", coinbase_map.get(timeframe, 3600)) | |
| if not df.empty: | |
| return df.tail(limit) | |
| return _binance_candles(f"{symbol}USDT", binance_map.get(timeframe, "1h"), limit) | |
| def get_crypto_data(asset_or_pair: str) -> pd.DataFrame: | |
| return get_candles(asset_or_pair, "1h", 300) | |
| def get_daily_crypto_data(asset_or_pair: str) -> pd.DataFrame: | |
| return get_candles(asset_or_pair, "1d", 300) | |
| def get_current_price(asset_or_pair: str) -> float | None: | |
| symbol = _normalize_symbol(asset_or_pair) | |
| try: | |
| url = f"https://api.exchange.coinbase.com/products/{symbol}-USD/ticker" | |
| response = _SESSION.get(url, timeout=6) | |
| response.raise_for_status() | |
| return float(response.json()["price"]) | |
| except Exception: | |
| pass | |
| try: | |
| url = "https://api.binance.com/api/v3/ticker/price" | |
| response = _SESSION.get(url, params={"symbol": f"{symbol}USDT"}, timeout=6) | |
| response.raise_for_status() | |
| return float(response.json()["price"]) | |
| except Exception: | |
| return None | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # BASIC MATH HELPERS | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _returns(prices: np.ndarray): | |
| prices = np.maximum(np.asarray(prices, dtype=float), 1e-12) | |
| return np.diff(np.log(prices)) | |
| def _prices_from_returns(current_price: float, pred_returns: np.ndarray): | |
| pred_returns = np.asarray(pred_returns, dtype=float) | |
| return current_price * np.exp(np.cumsum(pred_returns)) | |
| def _normal_cdf(z): | |
| return 0.5 * (1.0 + math.erf(float(z) / math.sqrt(2.0))) | |
| def calc_rsi(closes: np.ndarray, period: int = 14) -> float: | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < period + 1: | |
| return 50.0 | |
| delta = np.diff(closes) | |
| gains = np.where(delta > 0, delta, 0.0) | |
| losses = np.where(delta < 0, -delta, 0.0) | |
| avg_gain = pd.Series(gains).ewm(alpha=1 / period, adjust=False).mean().iloc[-1] | |
| avg_loss = pd.Series(losses).ewm(alpha=1 / period, adjust=False).mean().iloc[-1] | |
| if avg_loss == 0: | |
| return 100.0 | |
| rs = avg_gain / avg_loss | |
| return float(100 - (100 / (1 + rs))) | |
| def calc_macd(closes: np.ndarray): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 35: | |
| return 0.0, 0.0, 0.0 | |
| series = pd.Series(closes) | |
| ema_12 = series.ewm(span=12, adjust=False).mean() | |
| ema_26 = series.ewm(span=26, adjust=False).mean() | |
| macd_line = ema_12 - ema_26 | |
| signal_line = macd_line.ewm(span=9, adjust=False).mean() | |
| histogram = macd_line - signal_line | |
| return float(macd_line.iloc[-1]), float(signal_line.iloc[-1]), float(histogram.iloc[-1]) | |
| def calc_atr(highs: np.ndarray, lows: np.ndarray, closes: np.ndarray, period: int = 14) -> float: | |
| highs = np.asarray(highs, dtype=float) | |
| lows = np.asarray(lows, dtype=float) | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 2: | |
| return 0.0 | |
| true_range = np.maximum( | |
| highs[1:] - lows[1:], | |
| np.maximum( | |
| np.abs(highs[1:] - closes[:-1]), | |
| np.abs(lows[1:] - closes[:-1]), | |
| ), | |
| ) | |
| if len(true_range) == 0: | |
| return 0.0 | |
| return float(pd.Series(true_range).ewm(span=period, adjust=False).mean().iloc[-1]) | |
| def find_support_resistance(closes: np.ndarray, window: int = 72): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) == 0: | |
| return 0.0, 0.0 | |
| current = float(closes[-1]) | |
| recent = closes[-min(window, len(closes)):] | |
| support_candidates = recent[recent <= current] | |
| resistance_candidates = recent[recent >= current] | |
| support = ( | |
| float(np.percentile(support_candidates, 15)) | |
| if len(support_candidates) | |
| else current * 0.95 | |
| ) | |
| resistance = ( | |
| float(np.percentile(resistance_candidates, 85)) | |
| if len(resistance_candidates) | |
| else current * 1.05 | |
| ) | |
| if resistance <= support: | |
| support = current * 0.96 | |
| resistance = current * 1.04 | |
| return float(support), float(resistance) | |
| def calc_bollinger(closes: np.ndarray, period: int = 20): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) == 0: | |
| return 0.0, 0.0, 0.0 | |
| recent = closes[-min(period, len(closes)):] | |
| middle = float(np.mean(recent)) | |
| deviation = float(np.std(recent)) | |
| return ( | |
| float(middle + 2 * deviation), | |
| middle, | |
| float(middle - 2 * deviation), | |
| ) | |
| def calc_vwap(highs, lows, closes, volumes, period: int = 24): | |
| highs = np.asarray(highs, dtype=float) | |
| lows = np.asarray(lows, dtype=float) | |
| closes = np.asarray(closes, dtype=float) | |
| volumes = np.asarray(volumes, dtype=float) | |
| if len(closes) == 0: | |
| return 0.0 | |
| n = min(period, len(closes)) | |
| typical_price = (highs[-n:] + lows[-n:] + closes[-n:]) / 3 | |
| volume_slice = volumes[-n:] | |
| total_volume = np.sum(volume_slice) | |
| if total_volume <= 0: | |
| return float(closes[-1]) | |
| return float(np.sum(typical_price * volume_slice) / total_volume) | |
| def calc_volume_trend(volumes): | |
| volumes = np.asarray(volumes, dtype=float) | |
| if len(volumes) < 48: | |
| return 0.0, "Quiet" | |
| recent = float(np.mean(volumes[-24:])) | |
| previous = float(np.mean(volumes[-48:-24])) | |
| if previous <= 0: | |
| return 0.0, "Quiet" | |
| pct_change = ((recent - previous) / previous) * 100 | |
| if pct_change > 40: | |
| return float(pct_change), "Very Active" | |
| if pct_change > 15: | |
| return float(pct_change), "Picking Up" | |
| if pct_change < -25: | |
| return float(pct_change), "Drying Up" | |
| return float(pct_change), "Normal" | |
| def relative_volume(volumes, short=24, long=120): | |
| volumes = np.asarray(volumes, dtype=float) | |
| if len(volumes) < short + 5: | |
| return 1.0 | |
| recent = np.mean(volumes[-short:]) | |
| baseline = np.mean(volumes[-min(long, len(volumes)):]) | |
| if baseline <= 0: | |
| return 1.0 | |
| return float(recent / baseline) | |
| def liquidity_score(volumes): | |
| volumes = np.asarray(volumes, dtype=float) | |
| if len(volumes) < 50: | |
| return 50.0 | |
| recent = float(np.mean(volumes[-24:])) | |
| baseline = float(np.mean(volumes[-120:])) if len(volumes) >= 120 else float(np.mean(volumes)) | |
| if baseline <= 0: | |
| return 50.0 | |
| score = (recent / baseline) * 100 | |
| return float(max(0, min(150, score))) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # FORECASTING ENGINE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _ridge_return_forecast(closes, steps: int, lookback: int = 160): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 40: | |
| return np.repeat(closes[-1], steps), 0.0 | |
| returns = _returns(closes) | |
| n = min(lookback, len(returns)) | |
| y = returns[-n:] | |
| x = np.arange(n).reshape(-1, 1) | |
| future_x = np.arange(n, n + steps).reshape(-1, 1) | |
| weights = np.exp(np.linspace(-4.0, 0, n)) | |
| model = Ridge(alpha=3.0) | |
| model.fit(x, y, sample_weight=weights) | |
| predicted_returns = model.predict(future_x).flatten() | |
| slope = float(model.coef_[0]) | |
| predicted_prices = _prices_from_returns(float(closes[-1]), predicted_returns) | |
| return predicted_prices, slope | |
| def _drift_forecast(closes, steps: int, span: int = 48): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 30: | |
| return np.repeat(closes[-1], steps) | |
| returns = _returns(closes) | |
| mu = float( | |
| pd.Series(returns) | |
| .ewm(span=min(span, len(returns)), adjust=False) | |
| .mean() | |
| .iloc[-1] | |
| ) | |
| t = np.arange(1, steps + 1) | |
| dampener = 1 / (1 + 0.035 * t) | |
| predicted_returns = mu * dampener | |
| return _prices_from_returns(float(closes[-1]), predicted_returns) | |
| def _mean_reversion_forecast(closes, steps: int, lookback: int = 80): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 30: | |
| return np.repeat(closes[-1], steps) | |
| recent = closes[-min(lookback, len(closes)):] | |
| current = float(closes[-1]) | |
| fair_value = float( | |
| pd.Series(recent) | |
| .ewm(span=min(30, len(recent)), adjust=False) | |
| .mean() | |
| .iloc[-1] | |
| ) | |
| gap = fair_value - current | |
| path = [] | |
| for t in range(1, steps + 1): | |
| pull = 1 - np.exp(-0.055 * t) | |
| path.append(current + gap * pull) | |
| return np.array(path, dtype=float) | |
| def _momentum_forecast(closes, steps: int, lookback: int = 24): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < lookback + 2: | |
| return np.repeat(closes[-1], steps) | |
| returns = _returns(closes) | |
| recent = returns[-lookback:] | |
| momentum = float(np.mean(recent)) | |
| volatility = float(np.std(recent)) if np.std(recent) > 0 else 0.0 | |
| adjusted = momentum / (1 + 8 * volatility) | |
| t = np.arange(1, steps + 1) | |
| decay = np.exp(-0.035 * t) | |
| predicted_returns = adjusted * decay | |
| return _prices_from_returns(float(closes[-1]), predicted_returns) | |
| def _volume_pressure_forecast(closes, volumes, steps: int): | |
| closes = np.asarray(closes, dtype=float) | |
| volumes = np.asarray(volumes, dtype=float) | |
| if len(closes) < 50 or len(volumes) < 50: | |
| return np.repeat(closes[-1], steps) | |
| returns = _returns(closes) | |
| volume_change, _ = calc_volume_trend(volumes) | |
| pressure = np.tanh(volume_change / 100) * float(np.mean(returns[-12:])) | |
| t = np.arange(1, steps + 1) | |
| decay = np.exp(-0.045 * t) | |
| predicted_returns = pressure * decay | |
| return _prices_from_returns(float(closes[-1]), predicted_returns) | |
| def _forecast_error_metrics(actual, predicted): | |
| actual = np.asarray(actual, dtype=float) | |
| predicted = np.asarray(predicted, dtype=float) | |
| if len(actual) == 0 or len(actual) != len(predicted): | |
| return { | |
| "mae_pct": 99.0, | |
| "direction_accuracy": 0.0, | |
| "score": 0.01, | |
| } | |
| errors = np.abs((predicted - actual) / np.maximum(actual, 1e-12)) * 100 | |
| mae_pct = float(np.mean(errors)) | |
| actual_direction = np.sign(np.diff(actual)) | |
| predicted_direction = np.sign(np.diff(predicted)) | |
| direction_accuracy = ( | |
| float(np.mean(actual_direction == predicted_direction) * 100) | |
| if len(actual_direction) | |
| else 0.0 | |
| ) | |
| score = (direction_accuracy / 100) / (1 + mae_pct) | |
| return { | |
| "mae_pct": mae_pct, | |
| "direction_accuracy": direction_accuracy, | |
| "score": float(max(score, 0.001)), | |
| } | |
| def walk_forward_backtest(closes, volumes, test_points: int = 60): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 120: | |
| return { | |
| "direction_accuracy": 0.0, | |
| "mae_pct": 0.0, | |
| "confidence": 35.0, | |
| "model_scores": { | |
| "ridge": 0.25, | |
| "drift": 0.25, | |
| "mean": 0.25, | |
| "momentum": 0.25, | |
| }, | |
| } | |
| start = max(60, len(closes) - test_points) | |
| actual = [] | |
| ridge = [] | |
| drift = [] | |
| mean = [] | |
| momentum = [] | |
| for i in range(start, len(closes) - 1): | |
| train = closes[:i] | |
| if len(train) < 60: | |
| continue | |
| actual.append(closes[i + 1]) | |
| ridge.append(_ridge_return_forecast(train, 1)[0][0]) | |
| drift.append(_drift_forecast(train, 1)[0]) | |
| mean.append(_mean_reversion_forecast(train, 1)[0]) | |
| momentum.append(_momentum_forecast(train, 1)[0]) | |
| actual = np.array(actual, dtype=float) | |
| metrics = { | |
| "ridge": _forecast_error_metrics(actual, np.array(ridge)), | |
| "drift": _forecast_error_metrics(actual, np.array(drift)), | |
| "mean": _forecast_error_metrics(actual, np.array(mean)), | |
| "momentum": _forecast_error_metrics(actual, np.array(momentum)), | |
| } | |
| raw_scores = np.array([ | |
| metrics["ridge"]["score"], | |
| metrics["drift"]["score"], | |
| metrics["mean"]["score"], | |
| metrics["momentum"]["score"], | |
| ]) | |
| if raw_scores.sum() > 0: | |
| weights = raw_scores / raw_scores.sum() | |
| else: | |
| weights = np.ones(4) / 4 | |
| model_scores = { | |
| "ridge": float(weights[0]), | |
| "drift": float(weights[1]), | |
| "mean": float(weights[2]), | |
| "momentum": float(weights[3]), | |
| } | |
| weighted_mae = sum(metrics[k]["mae_pct"] * model_scores[k] for k in model_scores) | |
| weighted_direction = sum(metrics[k]["direction_accuracy"] * model_scores[k] for k in model_scores) | |
| confidence = (weighted_direction * 0.70) + (max(0, 100 - weighted_mae * 8) * 0.30) | |
| return { | |
| "direction_accuracy": float(weighted_direction), | |
| "mae_pct": float(weighted_mae), | |
| "confidence": float(max(20, min(92, confidence))), | |
| "model_scores": model_scores, | |
| "raw_metrics": metrics, | |
| } | |
| def ensemble_forecast(closes, volumes, steps: int): | |
| ridge_path, slope = _ridge_return_forecast(closes, steps) | |
| drift_path = _drift_forecast(closes, steps) | |
| mean_path = _mean_reversion_forecast(closes, steps) | |
| momentum_path = _momentum_forecast(closes, steps) | |
| volume_path = _volume_pressure_forecast(closes, volumes, steps) | |
| backtest = walk_forward_backtest(closes, volumes) | |
| weights = backtest["model_scores"] | |
| volume_weight = 0.10 | |
| core_weight = 0.90 | |
| final_path = ( | |
| core_weight * ( | |
| weights["ridge"] * ridge_path + | |
| weights["drift"] * drift_path + | |
| weights["mean"] * mean_path + | |
| weights["momentum"] * momentum_path | |
| ) | |
| + volume_weight * volume_path | |
| ) | |
| returns = _returns(closes) | |
| recent_vol = float( | |
| pd.Series(returns) | |
| .ewm(span=min(48, len(returns)), adjust=False) | |
| .std() | |
| .iloc[-1] | |
| ) | |
| if np.isnan(recent_vol) or recent_vol <= 0: | |
| recent_vol = float(np.std(returns)) if np.std(returns) > 0 else 0.01 | |
| t = np.sqrt(np.arange(1, steps + 1)) | |
| uncertainty = recent_vol * t | |
| display_weights = { | |
| "ridge": float(core_weight * weights["ridge"]), | |
| "drift": float(core_weight * weights["drift"]), | |
| "mean": float(core_weight * weights["mean"]), | |
| "momentum": float(core_weight * weights["momentum"]), | |
| "volume": float(volume_weight), | |
| } | |
| return { | |
| "expected": final_path, | |
| "lower_80": final_path * np.exp(-0.85 * uncertainty), | |
| "upper_80": final_path * np.exp(0.85 * uncertainty), | |
| "lower_95": final_path * np.exp(-1.65 * uncertainty), | |
| "upper_95": final_path * np.exp(1.65 * uncertainty), | |
| "slope": slope, | |
| "backtest": backtest, | |
| "weights": display_weights, | |
| } | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # FUTURE PRESSURE ENGINE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def _volume_future_forecast(volumes, steps: int): | |
| volumes = np.maximum(np.asarray(volumes, dtype=float), 1e-12) | |
| if len(volumes) < 40: | |
| return np.repeat(volumes[-1], steps) | |
| log_volume = np.log(volumes) | |
| growth = np.diff(log_volume) | |
| recent_growth = float( | |
| pd.Series(growth) | |
| .ewm(span=min(36, len(growth)), adjust=False) | |
| .mean() | |
| .iloc[-1] | |
| ) | |
| t = np.arange(1, steps + 1) | |
| decay = 1 / (1 + 0.04 * t) | |
| projected_growth = recent_growth * decay | |
| return volumes[-1] * np.exp(np.cumsum(projected_growth)) | |
| def future_pressure_engine( | |
| closes, | |
| volumes, | |
| forecast, | |
| btc_pressure_data, | |
| timeframe_alignment, | |
| futures_data, | |
| liquidity, | |
| steps: int, | |
| ): | |
| closes = np.asarray(closes, dtype=float) | |
| volumes = np.asarray(volumes, dtype=float) | |
| current_price = float(closes[-1]) | |
| current_volume = float(max(volumes[-1], 1e-12)) | |
| predicted_path = np.asarray(forecast["expected"], dtype=float) | |
| volume_path = _volume_future_forecast(volumes, steps) | |
| returns = _returns(closes) | |
| if len(returns) >= 20: | |
| volatility = float( | |
| pd.Series(returns) | |
| .ewm(span=min(48, len(returns)), adjust=False) | |
| .std() | |
| .iloc[-1] | |
| ) | |
| else: | |
| volatility = float(np.std(returns)) if len(returns) else 0.01 | |
| if np.isnan(volatility) or volatility <= 0: | |
| volatility = 0.01 | |
| btc_component = float(btc_pressure_data.get("score", 0)) / 100 | |
| timeframe_component = (float(timeframe_alignment.get("alignment_pct", 50)) - 50) / 50 | |
| if futures_data.get("label") == "Crowded Longs": | |
| futures_component = -0.25 | |
| elif futures_data.get("label") == "Crowded Shorts": | |
| futures_component = 0.18 | |
| else: | |
| futures_component = 0.0 | |
| liquidity_component = (float(liquidity) - 50) / 100 | |
| horizons = [3, 6, 12, 24, 48, 72] | |
| horizons = [h for h in horizons if h <= steps] | |
| pressure_rows = [] | |
| pressure_path = [] | |
| for h in horizons: | |
| future_price = float(predicted_path[h - 1]) | |
| future_volume = float(volume_path[h - 1]) | |
| expected_return = np.log(max(future_price, 1e-12) / max(current_price, 1e-12)) | |
| expected_volume_change = (future_volume - current_volume) / current_volume | |
| volatility_unit = max(volatility * np.sqrt(h), 0.0001) | |
| price_pressure = np.tanh(expected_return / volatility_unit) | |
| volume_pressure = np.tanh(expected_volume_change) | |
| combined = ( | |
| 0.42 * price_pressure + | |
| 0.20 * volume_pressure + | |
| 0.16 * btc_component + | |
| 0.10 * timeframe_component + | |
| 0.07 * liquidity_component + | |
| 0.05 * futures_component | |
| ) | |
| pressure_score = float(np.clip(combined * 100, -100, 100)) | |
| if pressure_score >= 45: | |
| label = "Strong Buy Pressure" | |
| elif pressure_score >= 15: | |
| label = "Buy Pressure Building" | |
| elif pressure_score <= -45: | |
| label = "Strong Sell Pressure" | |
| elif pressure_score <= -15: | |
| label = "Sell Pressure Building" | |
| else: | |
| label = "Balanced Pressure" | |
| pressure_rows.append({ | |
| "Horizon": f"{h}h", | |
| "Pressure Score": pressure_score, | |
| "Pressure Label": label, | |
| "Expected Price": future_price, | |
| "Expected Volume Change %": expected_volume_change * 100, | |
| }) | |
| pressure_path.append(pressure_score) | |
| final_score = pressure_path[-1] if pressure_path else 0.0 | |
| if final_score >= 45: | |
| final_label = "Strong Buy Pressure" | |
| bias = "Upside pressure is expected to strengthen." | |
| elif final_score >= 15: | |
| final_label = "Buy Pressure Building" | |
| bias = "Future pressure is leaning upward, but not aggressively." | |
| elif final_score <= -45: | |
| final_label = "Strong Sell Pressure" | |
| bias = "Downside pressure is expected to strengthen." | |
| elif final_score <= -15: | |
| final_label = "Sell Pressure Building" | |
| bias = "Future pressure is leaning downward." | |
| else: | |
| final_label = "Balanced Pressure" | |
| bias = "Future pressure is mixed or undecided." | |
| final_volume_index = min(steps, len(volume_path)) - 1 | |
| future_volume_trend_pct = ( | |
| ((float(volume_path[final_volume_index]) - current_volume) / current_volume) * 100 | |
| if current_volume > 0 and final_volume_index >= 0 | |
| else 0.0 | |
| ) | |
| return { | |
| "future_pressure_score": float(final_score), | |
| "future_pressure_label": final_label, | |
| "future_pressure_bias": bias, | |
| "future_buy_pressure_pct": float(max(0, final_score)), | |
| "future_sell_pressure_pct": float(max(0, -final_score)), | |
| "future_volume_trend_pct": float(future_volume_trend_pct), | |
| "pressure_path": pressure_path, | |
| "pressure_table": pressure_rows, | |
| } | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # INSTITUTIONAL ORDER FLOW ENGINE | |
| # OFI + CVD + GAUSSIAN PUMP/DUMP PROBABILITY | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def order_flow_imbalance_engine(df: pd.DataFrame): | |
| if df is None or df.empty or len(df) < 80: | |
| return { | |
| "ofi_score": 0.0, | |
| "ofi_label": "Unavailable", | |
| "cvd_6h": 0.0, | |
| "cvd_12h": 0.0, | |
| "cvd_6h_z": 0.0, | |
| "cvd_12h_z": 0.0, | |
| "pump_probability_6h": 50.0, | |
| "dump_probability_6h": 50.0, | |
| "pump_probability_12h": 50.0, | |
| "dump_probability_12h": 50.0, | |
| "capital_pressure_label": "Insufficient Data", | |
| "capital_pressure_score": 0.0, | |
| "cvd_table": [], | |
| } | |
| work = df.copy() | |
| for col in ["Open", "High", "Low", "Close", "Volume"]: | |
| work[col] = work[col].astype(float) | |
| candle_range = (work["High"] - work["Low"]).replace(0, np.nan) | |
| close_position = (work["Close"] - work["Low"]) / candle_range | |
| close_position = close_position.clip(0, 1).fillna(0.5) | |
| work["Buy Volume"] = work["Volume"] * close_position | |
| work["Sell Volume"] = work["Volume"] * (1 - close_position) | |
| work["Volume Delta"] = work["Buy Volume"] - work["Sell Volume"] | |
| work["OFI"] = work["Volume Delta"] / work["Volume"].replace(0, np.nan) | |
| work["OFI"] = work["OFI"].replace([np.inf, -np.inf], 0).fillna(0) | |
| work["CVD 6H"] = work["Volume Delta"].rolling(6, min_periods=3).sum() | |
| work["CVD 12H"] = work["Volume Delta"].rolling(12, min_periods=6).sum() | |
| baseline_window = min(720, max(80, len(work) - 1)) | |
| cvd6_hist = work["CVD 6H"].dropna().iloc[-baseline_window:] | |
| cvd12_hist = work["CVD 12H"].dropna().iloc[-baseline_window:] | |
| current_cvd_6h = ( | |
| float(work["CVD 6H"].dropna().iloc[-1]) | |
| if not work["CVD 6H"].dropna().empty | |
| else 0.0 | |
| ) | |
| current_cvd_12h = ( | |
| float(work["CVD 12H"].dropna().iloc[-1]) | |
| if not work["CVD 12H"].dropna().empty | |
| else 0.0 | |
| ) | |
| cvd6_mean = float(cvd6_hist.mean()) if len(cvd6_hist) else 0.0 | |
| cvd12_mean = float(cvd12_hist.mean()) if len(cvd12_hist) else 0.0 | |
| cvd6_std_raw = cvd6_hist.std() if len(cvd6_hist) else 0.0 | |
| cvd12_std_raw = cvd12_hist.std() if len(cvd12_hist) else 0.0 | |
| cvd6_std = float(cvd6_std_raw) if cvd6_std_raw and cvd6_std_raw > 0 else 1.0 | |
| cvd12_std = float(cvd12_std_raw) if cvd12_std_raw and cvd12_std_raw > 0 else 1.0 | |
| cvd_6h_z = (current_cvd_6h - cvd6_mean) / cvd6_std | |
| cvd_12h_z = (current_cvd_12h - cvd12_mean) / cvd12_std | |
| cvd_6h_z = float(np.clip(cvd_6h_z, -3.5, 3.5)) | |
| cvd_12h_z = float(np.clip(cvd_12h_z, -3.5, 3.5)) | |
| ofi_6h = float(work["OFI"].tail(6).mean()) | |
| ofi_12h = float(work["OFI"].tail(12).mean()) | |
| ofi_score = float(np.clip(((ofi_6h * 0.60) + (ofi_12h * 0.40)) * 100, -100, 100)) | |
| pump_probability_6h = _normal_cdf(cvd_6h_z) * 100 | |
| dump_probability_6h = 100 - pump_probability_6h | |
| pump_probability_12h = _normal_cdf(cvd_12h_z) * 100 | |
| dump_probability_12h = 100 - pump_probability_12h | |
| capital_pressure_score = ( | |
| 0.35 * ofi_score + | |
| 0.35 * ((pump_probability_6h - 50) * 2) + | |
| 0.30 * ((pump_probability_12h - 50) * 2) | |
| ) | |
| capital_pressure_score = float(np.clip(capital_pressure_score, -100, 100)) | |
| if capital_pressure_score >= 60: | |
| capital_pressure_label = "Institutional Buy Pressure" | |
| elif capital_pressure_score >= 25: | |
| capital_pressure_label = "Buy Flow Building" | |
| elif capital_pressure_score <= -60: | |
| capital_pressure_label = "Institutional Sell Pressure" | |
| elif capital_pressure_score <= -25: | |
| capital_pressure_label = "Sell Flow Building" | |
| else: | |
| capital_pressure_label = "Balanced Capital Flow" | |
| if ofi_score >= 35: | |
| ofi_label = "Aggressive Buyers" | |
| elif ofi_score <= -35: | |
| ofi_label = "Aggressive Sellers" | |
| else: | |
| ofi_label = "Balanced Tape" | |
| recent = work.tail(12).copy() | |
| cvd_table = [] | |
| for idx, row in recent.iterrows(): | |
| cvd_table.append({ | |
| "Time": str(idx), | |
| "Close": float(row["Close"]), | |
| "Volume": float(row["Volume"]), | |
| "Buy Volume": float(row["Buy Volume"]), | |
| "Sell Volume": float(row["Sell Volume"]), | |
| "Volume Delta": float(row["Volume Delta"]), | |
| "OFI": float(row["OFI"]), | |
| }) | |
| return { | |
| "ofi_score": float(ofi_score), | |
| "ofi_label": ofi_label, | |
| "cvd_6h": float(current_cvd_6h), | |
| "cvd_12h": float(current_cvd_12h), | |
| "cvd_6h_z": float(cvd_6h_z), | |
| "cvd_12h_z": float(cvd_12h_z), | |
| "pump_probability_6h": float(pump_probability_6h), | |
| "dump_probability_6h": float(dump_probability_6h), | |
| "pump_probability_12h": float(pump_probability_12h), | |
| "dump_probability_12h": float(dump_probability_12h), | |
| "capital_pressure_label": capital_pressure_label, | |
| "capital_pressure_score": float(capital_pressure_score), | |
| "cvd_table": cvd_table, | |
| } | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # MARKET CONTEXT | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def historical_risk_engine(closes): | |
| returns = _returns(closes) | |
| if len(returns) < 20: | |
| return { | |
| "var_95": 0.0, | |
| "var_drop": 0.0, | |
| "expected_shortfall": 0.0, | |
| "expected_shortfall_drop": 0.0, | |
| } | |
| current = float(closes[-1]) | |
| var_threshold = float(np.percentile(returns, 5)) | |
| tail = returns[returns <= var_threshold] | |
| expected_shortfall = abs(float(np.mean(tail))) if len(tail) else abs(var_threshold) | |
| return { | |
| "var_95": abs(var_threshold), | |
| "var_drop": current * abs(var_threshold), | |
| "expected_shortfall": expected_shortfall, | |
| "expected_shortfall_drop": current * expected_shortfall, | |
| } | |
| def detect_market_regime(closes, volumes, slope, volume_trend_pct): | |
| returns = _returns(closes) | |
| recent_volatility = ( | |
| float(np.std(returns[-48:])) | |
| if len(returns) >= 48 | |
| else float(np.std(returns)) | |
| ) | |
| if len(returns) >= 100: | |
| volatility_history = [ | |
| np.std(returns[i - 48:i]) | |
| for i in range(48, len(returns)) | |
| ] | |
| else: | |
| volatility_history = [] | |
| threshold = ( | |
| np.percentile(volatility_history, 80) | |
| if volatility_history | |
| else recent_volatility * 1.4 | |
| ) | |
| if recent_volatility > threshold: | |
| return "High Volatility" | |
| if abs(slope) < 1e-6: | |
| return "Sideways" | |
| if slope > 0 and volume_trend_pct > 5: | |
| return "Trending Up" | |
| if slope < 0 and volume_trend_pct > 5: | |
| return "Trending Down" | |
| if slope > 0: | |
| return "Slow Uptrend" | |
| if slope < 0: | |
| return "Slow Downtrend" | |
| return "Unclear" | |
| def detect_squeeze(closes): | |
| closes = np.asarray(closes, dtype=float) | |
| if len(closes) < 60: | |
| return { | |
| "active": False, | |
| "label": "Unknown", | |
| "width_pct": 0.0, | |
| } | |
| widths = [] | |
| for i in range(20, len(closes)): | |
| upper, middle, lower = calc_bollinger(closes[i - 20:i]) | |
| widths.append((upper - lower) / max(middle, 1e-12)) | |
| current_width = widths[-1] | |
| threshold = np.percentile(widths, 20) | |
| return { | |
| "active": bool(current_width <= threshold), | |
| "label": "Active" if current_width <= threshold else "Not Active", | |
| "width_pct": float(current_width * 100), | |
| } | |
| def detect_fake_breakout(price, resistance, rsi, macd_hist, volume_trend_pct): | |
| is_fake = ( | |
| price > resistance and | |
| volume_trend_pct <= 0 and | |
| rsi > 70 and | |
| macd_hist <= 0 | |
| ) | |
| return { | |
| "active": bool(is_fake), | |
| "label": "Possible Fake Breakout" if is_fake else "No fake breakout warning", | |
| } | |
| def classify_setup(price, support, resistance, rsi, macd_hist, volume_trend_pct, squeeze_active, fake_breakout): | |
| if fake_breakout: | |
| return "Exhaustion Risk" | |
| if squeeze_active: | |
| return "Volatility Squeeze" | |
| if price <= support * 1.025 and rsi < 45: | |
| return "Dip Buy" | |
| if price > resistance and volume_trend_pct > 15: | |
| return "Breakout Attempt" | |
| if macd_hist > 0 and volume_trend_pct > 5: | |
| return "Momentum Continuation" | |
| if rsi < 35: | |
| return "Mean Reversion" | |
| return "No Clean Setup" | |
| def timeframe_signal_from_df(df): | |
| if df is None or df.empty or len(df) < 40: | |
| return 0.0, "Unknown" | |
| closes = df["Close"].astype(float).to_numpy() | |
| volumes = df["Volume"].astype(float).to_numpy() | |
| rsi = calc_rsi(closes) | |
| _, _, histogram = calc_macd(closes) | |
| volume_pct, _ = calc_volume_trend(volumes) | |
| score = 0.0 | |
| score += 1 if rsi < 35 else -1 if rsi > 70 else 0 | |
| score += 1 if histogram > 0 else -1 if histogram < 0 else 0 | |
| score += 1 if closes[-1] > np.mean(closes[-20:]) else -1 | |
| score += 0.5 if volume_pct > 5 else -0.5 if volume_pct < -20 else 0 | |
| if score >= 1.5: | |
| label = "Bullish" | |
| elif score <= -1.5: | |
| label = "Bearish" | |
| else: | |
| label = "Neutral" | |
| return float(score), label | |
| def analyze_timeframe_alignment(symbol): | |
| frames = {} | |
| total = 0.0 | |
| for timeframe in ["15m", "1h", "4h", "1d"]: | |
| df = get_candles(symbol, timeframe, 220) | |
| score, label = timeframe_signal_from_df(df) | |
| frames[timeframe] = { | |
| "score": score, | |
| "label": label, | |
| } | |
| total += score | |
| max_score = 4 * 3.5 | |
| alignment = ((total + max_score) / (2 * max_score)) * 100 | |
| alignment = float(max(0, min(100, alignment))) | |
| if alignment >= 65: | |
| final_label = "Bullish Alignment" | |
| elif alignment <= 35: | |
| final_label = "Bearish Alignment" | |
| else: | |
| final_label = "Mixed Alignment" | |
| return { | |
| "alignment_pct": alignment, | |
| "label": final_label, | |
| "frames": frames, | |
| } | |
| def btc_market_pressure(): | |
| df = get_candles("BTC", "1h", 250) | |
| if df.empty: | |
| return { | |
| "label": "Unknown", | |
| "score": 0.0, | |
| "note": "BTC data unavailable", | |
| } | |
| closes = df["Close"].astype(float).to_numpy() | |
| volumes = df["Volume"].astype(float).to_numpy() | |
| rsi = calc_rsi(closes) | |
| _, _, histogram = calc_macd(closes) | |
| volume_pct, _ = calc_volume_trend(volumes) | |
| ma50 = np.mean(closes[-50:]) | |
| price_vs_ma = ((closes[-1] - ma50) / ma50) * 100 if ma50 > 0 else 0 | |
| score = 0.0 | |
| score += 30 if histogram > 0 else -30 | |
| score += 25 if price_vs_ma > 0 else -25 | |
| score += 15 if volume_pct > 10 and price_vs_ma > 0 else -15 if volume_pct > 10 and price_vs_ma < 0 else 0 | |
| score += 10 if 40 <= rsi <= 65 else -10 if rsi > 75 else 0 | |
| if score > 30: | |
| label = "Positive" | |
| elif score < -30: | |
| label = "Negative" | |
| else: | |
| label = "Neutral" | |
| return { | |
| "label": label, | |
| "score": float(score), | |
| "note": f"BTC RSI {rsi:.0f}, MA gap {price_vs_ma:+.2f}%", | |
| } | |
| def funding_futures_pressure(symbol): | |
| symbol = _normalize_symbol(symbol) | |
| if symbol in {"USDT", "USDC", "DAI"}: | |
| return { | |
| "available": False, | |
| "label": "Unavailable", | |
| "funding": 0.0, | |
| "open_interest": 0.0, | |
| "note": "Stablecoin", | |
| } | |
| pair = f"{symbol}USDT" | |
| try: | |
| funding_url = "https://fapi.binance.com/fapi/v1/premiumIndex" | |
| open_interest_url = "https://fapi.binance.com/fapi/v1/openInterest" | |
| funding_response = _SESSION.get(funding_url, params={"symbol": pair}, timeout=8) | |
| open_interest_response = _SESSION.get(open_interest_url, params={"symbol": pair}, timeout=8) | |
| funding_response.raise_for_status() | |
| open_interest_response.raise_for_status() | |
| funding = float(funding_response.json().get("lastFundingRate", 0)) * 100 | |
| open_interest = float(open_interest_response.json().get("openInterest", 0)) | |
| if funding > 0.05: | |
| label = "Crowded Longs" | |
| elif funding < -0.03: | |
| label = "Crowded Shorts" | |
| else: | |
| label = "Balanced" | |
| return { | |
| "available": True, | |
| "label": label, | |
| "funding": funding, | |
| "open_interest": open_interest, | |
| "note": f"Funding {funding:+.4f}%", | |
| } | |
| except Exception: | |
| return { | |
| "available": False, | |
| "label": "Unavailable", | |
| "funding": 0.0, | |
| "open_interest": 0.0, | |
| "note": "No futures data", | |
| } | |
| _UNLOCK_WATCH = { | |
| "APT": "High", | |
| "ARB": "High", | |
| "OP": "High", | |
| "SUI": "High", | |
| "SEI": "Medium", | |
| "TIA": "High", | |
| "STRK": "High", | |
| "WLD": "Medium", | |
| "IMX": "Medium", | |
| "APE": "Medium", | |
| "ENA": "Medium", | |
| } | |
| def token_unlock_risk(symbol): | |
| risk = _UNLOCK_WATCH.get(_normalize_symbol(symbol), "Low/Unknown") | |
| if risk == "High": | |
| score = -18 | |
| elif risk == "Medium": | |
| score = -8 | |
| else: | |
| score = 0 | |
| return { | |
| "risk": risk, | |
| "score_penalty": score, | |
| } | |
| def slippage_pct_for_asset(category, liquidity): | |
| base = 0.05 | |
| if category == "meme": | |
| base = 0.55 | |
| elif category in {"major", "india"}: | |
| base = 0.08 | |
| elif category in {"layer1", "ethereum"}: | |
| base = 0.15 | |
| elif category in {"ai", "gaming"}: | |
| base = 0.25 | |
| if liquidity < 40: | |
| base *= 2.5 | |
| elif liquidity < 70: | |
| base *= 1.5 | |
| return float(min(base, 2.5)) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # RELIEF BOUNCE / MEAN REVERSION ENGINE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def relief_bounce_engine(df, price, rsi, atr, support, resistance, vwap): | |
| work = df.copy() | |
| closes = work["Close"].astype(float) | |
| highs = work["High"].astype(float) | |
| if len(work) < 80 or price <= 0: | |
| return { | |
| "relief_bounce_active": False, | |
| "relief_bounce_label": "Inactive", | |
| "relief_target": price, | |
| "relief_target_pct": 0.0, | |
| "relief_confirmation": "No setup", | |
| "relief_invalidation": support, | |
| "relief_probability": 0.0, | |
| "relief_setup_score": 0.0, | |
| } | |
| recent_high = float(highs.tail(60).max()) | |
| ma20 = float(closes.rolling(20).mean().iloc[-1]) | |
| ma50 = float(closes.rolling(50).mean().iloc[-1]) | |
| ma100 = float(closes.rolling(100).mean().iloc[-1]) if len(closes) >= 100 else ma50 | |
| drawdown_from_60h_high = ((price - recent_high) / recent_high) * 100 if recent_high > 0 else 0 | |
| distance_from_ma20 = ((price - ma20) / ma20) * 100 if ma20 > 0 else 0 | |
| distance_from_ma50 = ((price - ma50) / ma50) * 100 if ma50 > 0 else 0 | |
| oversold_points = 0 | |
| if rsi <= 30: | |
| oversold_points += 35 | |
| elif rsi <= 38: | |
| oversold_points += 20 | |
| if drawdown_from_60h_high <= -10: | |
| oversold_points += 25 | |
| elif drawdown_from_60h_high <= -6: | |
| oversold_points += 15 | |
| if distance_from_ma20 <= -4: | |
| oversold_points += 15 | |
| if distance_from_ma50 <= -6: | |
| oversold_points += 15 | |
| near_support = price <= support * 1.035 | |
| if near_support: | |
| oversold_points += 10 | |
| possible_targets = [ | |
| resistance, | |
| ma20, | |
| ma50, | |
| ma100, | |
| vwap, | |
| price + (3.0 * atr), | |
| price * 1.20, | |
| ] | |
| valid_targets = [t for t in possible_targets if t > price * 1.025] | |
| if valid_targets: | |
| relief_target = min(valid_targets, key=lambda x: abs(x - price * 1.18)) | |
| else: | |
| relief_target = max(resistance, price * 1.04) | |
| relief_target_pct = ((relief_target - price) / price) * 100 if price > 0 else 0.0 | |
| reclaim_1 = price + atr | |
| reclaim_2 = max(ma20, support + (2 * atr)) | |
| invalidation = min(support, price - (1.5 * atr)) | |
| confirmation_score = 0 | |
| if price > support: | |
| confirmation_score += 20 | |
| if closes.iloc[-1] > closes.iloc[-2]: | |
| confirmation_score += 20 | |
| if rsi > 30: | |
| confirmation_score += 15 | |
| if price > reclaim_1: | |
| confirmation_score += 25 | |
| if price > ma20: | |
| confirmation_score += 20 | |
| relief_probability = min(85, max(5, (oversold_points * 0.55) + (confirmation_score * 0.45))) | |
| active = oversold_points >= 45 and relief_target_pct >= 6 | |
| if active and confirmation_score >= 60: | |
| label = "Relief Bounce Confirming" | |
| confirmation = f"Price is trying to confirm above {reclaim_1:.4f} and {reclaim_2:.4f}." | |
| elif active: | |
| label = "Relief Bounce Setup Active" | |
| confirmation = f"Needs reclaim above {reclaim_1:.4f}, then {reclaim_2:.4f}." | |
| else: | |
| label = "Inactive" | |
| confirmation = "No clean oversold bounce setup." | |
| return { | |
| "relief_bounce_active": bool(active), | |
| "relief_bounce_label": label, | |
| "relief_target": float(relief_target), | |
| "relief_target_pct": float(relief_target_pct), | |
| "relief_confirmation": confirmation, | |
| "relief_invalidation": float(invalidation), | |
| "relief_probability": float(relief_probability), | |
| "relief_setup_score": float(oversold_points), | |
| } | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # BEGINNER SIGNAL ENGINE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def advanced_score_to_beginner_signal(score, probability_up=None, edge_ratio=None, blockers=None): | |
| blockers = blockers or [] | |
| if "Low model confidence" in blockers and score > 0: | |
| return "WAIT" | |
| if "Weak liquidity" in blockers and score > 0: | |
| return "WAIT" | |
| if probability_up is not None: | |
| if probability_up >= 62 and score >= 8: | |
| return "BUY" | |
| if probability_up <= 38 and score <= -8: | |
| return "SELL" | |
| if edge_ratio is not None: | |
| if edge_ratio >= 1.0 and score >= 8: | |
| return "BUY" | |
| if edge_ratio <= -0.6 and score <= -8: | |
| return "SELL" | |
| if score >= 18: | |
| return "BUY" | |
| if score <= -18: | |
| return "SELL" | |
| return "WAIT" | |
| def beginner_signal_reason(signal): | |
| if signal == "BUY": | |
| return "The app thinks this coin has enough positive clues to practice buying carefully." | |
| if signal == "SELL": | |
| return "The app thinks the coin is weak or risky, so selling or staying out may be safer." | |
| return "The app thinks the coin is unclear, so waiting is the safest lesson." | |
| def signal_to_action(score): | |
| if score >= 18: | |
| return "BUY" | |
| if score <= -18: | |
| return "SELL" | |
| return "WAIT" | |
| def detailed_signal_from_score(score): | |
| if score >= 55: | |
| return "STRONG BUY" | |
| if score >= 25: | |
| return "BUY" | |
| if score >= 8: | |
| return "LEAN BUY" | |
| if score > -8: | |
| return "WAIT" | |
| if score > -25: | |
| return "LEAN SELL" | |
| if score > -55: | |
| return "SELL" | |
| return "STRONG SELL" | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # SIGNAL + RISK ENGINE | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def smart_stop_loss(price, atr, support, rsi, var_drop, expected_shortfall_drop): | |
| if price <= 0: | |
| return 0.0 | |
| atr_multiplier = 1.45 if rsi > 65 else 2.15 if rsi < 35 else 1.80 | |
| risk = max( | |
| atr * atr_multiplier, | |
| var_drop, | |
| expected_shortfall_drop, | |
| ) | |
| volatility_stop = price - risk | |
| structure_stop = support - (0.004 * price) | |
| stop = max(volatility_stop, structure_stop) | |
| if stop >= price: | |
| stop = price * 0.975 | |
| return float(max(stop, price * 0.70)) | |
| def smart_take_profit(price, stop_loss, atr, resistance): | |
| if price <= 0: | |
| return 0.0 | |
| risk = max(price - stop_loss, price * 0.01) | |
| reward_target = price + 2.4 * risk | |
| structure_target = resistance + 0.18 * max(resistance - price, atr) | |
| return float(max(reward_target, structure_target)) | |
| def recommended_position_size(portfolio_value, price, stop_loss, max_risk_pct=2.0, max_alloc_pct=25.0): | |
| if portfolio_value <= 0 or price <= 0: | |
| return 0.0 | |
| risk_per_unit = price - stop_loss | |
| if risk_per_unit <= 0: | |
| return 0.0 | |
| max_risk = portfolio_value * max_risk_pct / 100 | |
| risk_units = max_risk / risk_per_unit | |
| cap_units = (portfolio_value * max_alloc_pct / 100) / price | |
| return float(max(0.0, min(risk_units, cap_units))) | |
| _GAS_FEES = { | |
| "bitcoin": (8.0, 22.0), | |
| "ethereum": (5.0, 35.0), | |
| "solana": (0.001, 0.02), | |
| "bsc": (0.10, 0.80), | |
| "polygon": (0.01, 0.20), | |
| "avalanche": (0.15, 1.50), | |
| "layer1": (0.05, 1.50), | |
| "major": (0.10, 5.00), | |
| "meme": (0.25, 8.00), | |
| "ai": (0.25, 5.00), | |
| "gaming": (0.10, 4.00), | |
| "india": (0.05, 2.00), | |
| "default": (0.25, 3.00), | |
| } | |
| def estimate_gas(network): | |
| return _GAS_FEES.get(network, _GAS_FEES["default"]) | |
| def min_profitable_investment(price, take_profit, network): | |
| gas_low, gas_high = estimate_gas(network) | |
| avg_fee = (gas_low + gas_high) / 2 | |
| if price <= 0: | |
| return 0.0 | |
| profit_pct = (take_profit - price) / price | |
| if profit_pct <= 0: | |
| return 0.0 | |
| return float(avg_fee / profit_pct) | |
| def score_trade( | |
| rsi, | |
| macd_hist, | |
| price, | |
| support, | |
| resistance, | |
| bb_lower, | |
| bb_upper, | |
| vwap, | |
| slope, | |
| volume_trend_pct, | |
| net_expected_return, | |
| liquidity, | |
| confidence, | |
| btc_pressure, | |
| timeframe_alignment, | |
| unlock_penalty, | |
| fake_breakout, | |
| future_pressure_score, | |
| capital_pressure_score, | |
| relief_probability=0.0, | |
| relief_active=False, | |
| quant_score=0.0, | |
| ): | |
| score = 0.0 | |
| if rsi < 30: | |
| score += 28 | |
| elif rsi < 40: | |
| score += 14 | |
| elif rsi > 75: | |
| score -= 30 | |
| elif rsi > 65: | |
| score -= 15 | |
| score += 18 if macd_hist > 0 else -18 if macd_hist < 0 else 0 | |
| score += 14 if slope > 0 else -14 if slope < 0 else 0 | |
| spread = max(resistance - support, price * 0.01) | |
| position = (price - support) / spread | |
| if position < 0.25: | |
| score += 16 | |
| elif position > 0.75: | |
| score -= 16 | |
| bb_spread = max(bb_upper - bb_lower, price * 0.01) | |
| bb_position = (price - bb_lower) / bb_spread | |
| if bb_position < 0.20: | |
| score += 10 | |
| elif bb_position > 0.80: | |
| score -= 10 | |
| if vwap > 0: | |
| gap = ((price - vwap) / vwap) * 100 | |
| if gap < -2: | |
| score += 8 | |
| elif gap > 2: | |
| score -= 8 | |
| if volume_trend_pct > 20 and slope > 0: | |
| score += 8 | |
| elif volume_trend_pct > 20 and slope < 0: | |
| score -= 8 | |
| if net_expected_return > 2: | |
| score += 10 | |
| elif net_expected_return < -1: | |
| score -= 12 | |
| if liquidity < 40: | |
| score -= 18 | |
| elif liquidity > 90: | |
| score += 5 | |
| if btc_pressure["label"] == "Positive": | |
| score += 10 | |
| elif btc_pressure["label"] == "Negative": | |
| score -= 16 | |
| if timeframe_alignment["alignment_pct"] >= 65: | |
| score += 8 | |
| elif timeframe_alignment["alignment_pct"] <= 35: | |
| score -= 12 | |
| score += unlock_penalty | |
| if fake_breakout: | |
| score -= 22 | |
| if future_pressure_score > 45: | |
| score += 14 | |
| elif future_pressure_score > 15: | |
| score += 7 | |
| elif future_pressure_score < -45: | |
| score -= 14 | |
| elif future_pressure_score < -15: | |
| score -= 7 | |
| if capital_pressure_score > 60: | |
| score += 18 | |
| elif capital_pressure_score > 25: | |
| score += 10 | |
| elif capital_pressure_score < -60: | |
| score -= 18 | |
| elif capital_pressure_score < -25: | |
| score -= 10 | |
| if relief_active and relief_probability >= 65: | |
| score += 8 | |
| elif relief_active and relief_probability < 45: | |
| score -= 4 | |
| if quant_score: | |
| score = (0.75 * score) + (0.25 * quant_score) | |
| if confidence < 45: | |
| score *= 0.65 | |
| elif confidence > 70: | |
| score *= 1.05 | |
| return float(max(-100, min(100, score))) | |
| def do_not_trade_rules(analysis): | |
| blockers = [] | |
| if analysis["model_confidence"] < 45: | |
| blockers.append("Low model confidence") | |
| if analysis["liquidity_score"] < 25: | |
| blockers.append("Very weak liquidity") | |
| if analysis["risk_reward"] < 1.1: | |
| blockers.append("Poor risk/reward") | |
| if analysis["net_expected_return"] <= -1.5: | |
| blockers.append("Expected move is negative after costs") | |
| if analysis["future_pressure_score"] < -25 and analysis["signal_score"] > 0: | |
| blockers.append("Future pressure is leaning against the trade") | |
| if analysis["capital_pressure_score"] < -35 and analysis["signal_score"] > 0: | |
| blockers.append("Order-flow capital pressure is against the trade") | |
| if analysis["fake_breakout_warning"]: | |
| blockers.append("Possible fake breakout") | |
| if analysis["unlock_risk"] == "High": | |
| blockers.append("High token-unlock risk") | |
| return blockers | |
| def why_not_buy_reasons(analysis): | |
| reasons = [] | |
| if analysis["future_pressure_score"] < 0: | |
| reasons.append("Future pressure is leaning against the trade.") | |
| if analysis["capital_pressure_score"] < 0: | |
| reasons.append("Order-flow capital pressure is leaning against the trade.") | |
| if analysis["btc_pressure"] == "Negative": | |
| reasons.append("BTC market pressure is negative.") | |
| if analysis["volume_trend_pct"] < 0: | |
| reasons.append("Current volume is falling, so crowd support is weaker.") | |
| if analysis["future_volume_trend_pct"] < 0: | |
| reasons.append("Future volume pressure is expected to fade.") | |
| if analysis["pump_probability_6h"] < 50: | |
| reasons.append("6-hour Gaussian CVD probability does not favor upward continuation.") | |
| if analysis["pump_probability_12h"] < 50: | |
| reasons.append("12-hour Gaussian CVD probability does not favor upward continuation.") | |
| if analysis.get("relief_bounce_active") and analysis.get("capital_pressure_score", 0) < 0: | |
| reasons.append("Relief bounce setup exists, but capital flow has not confirmed yet.") | |
| if analysis["liquidity_score"] < 60: | |
| reasons.append("Liquidity is not clean enough for aggressive sizing.") | |
| if analysis["rsi"] > 70: | |
| reasons.append("RSI is hot, so chasing may be risky.") | |
| if analysis["expected_shortfall"] > analysis["var_95"] * 1.5: | |
| reasons.append("Tail risk is heavier than normal.") | |
| if analysis["fake_breakout_warning"]: | |
| reasons.append("Price behavior looks like a possible fake breakout.") | |
| if analysis["risk_reward"] < 1.8: | |
| reasons.append("Risk/reward is not attractive enough.") | |
| if not reasons: | |
| reasons.append("No major rejection reason detected, but always respect the stop-loss.") | |
| return reasons | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # BEGINNER EDUCATION / GAME SIMULATOR HELPERS | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def child_friendly_signal_text(signal): | |
| if signal == "BUY": | |
| return "BUY means the app thinks this coin has more green clues than red clues. In the game, you may practice buying a small amount." | |
| if signal == "SELL": | |
| return "SELL means the app sees danger signs. In the game, you may practice selling or staying away." | |
| return "WAIT means the clues are mixed. In the game, the smartest move may be to watch first." | |
| def stop_loss_explanation_for_child(): | |
| return ( | |
| "A stop-loss is like a safety helmet. If the coin falls too much, " | |
| "the game sells it to protect your practice money from a bigger fall." | |
| ) | |
| def trailing_stop_explanation_for_child(): | |
| return ( | |
| "A trailing stop-loss follows the price upward. If your coin goes up, " | |
| "the safety line also moves up. If the coin then falls, the game can sell and protect some profit." | |
| ) | |
| def auto_sell_explanation_for_child(): | |
| return ( | |
| "Auto-sell is like setting a robot helper. You tell it: sell if the price reaches my goal, " | |
| "or sell if the price falls to my safety line." | |
| ) | |
| def apy_explanation_for_child(): | |
| return ( | |
| "APY means Annual Percentage Yield. It shows how much money could grow in one year " | |
| "if the same growth kept repeating. In this simulator, APY is only a learning score, " | |
| "not a promise. Example: if your practice money grows fast in a few days, the app converts " | |
| "that speed into a yearly-style number so you can compare performance." | |
| ) | |
| def calculate_practice_apy(start_value, current_value, elapsed_hours): | |
| start_value = float(start_value) | |
| current_value = float(current_value) | |
| elapsed_hours = float(elapsed_hours) | |
| if start_value <= 0 or current_value <= 0 or elapsed_hours <= 0: | |
| return 0.0 | |
| years = elapsed_hours / (24 * 365) | |
| if years <= 0: | |
| return 0.0 | |
| growth = current_value / start_value | |
| try: | |
| apy = (growth ** (1 / years) - 1) * 100 | |
| except Exception: | |
| apy = 0.0 | |
| return float(max(-100, min(9999, apy))) | |
| def game_feedback_message(pnl_pct, sold_by="manual"): | |
| pnl_pct = float(pnl_pct) | |
| if sold_by == "stop_loss": | |
| return "The safety helmet worked. You lost a little in the game, but avoided a bigger fall." | |
| if sold_by == "trailing_stop": | |
| return "Great lesson. The trailing stop protected your gains after the price moved up." | |
| if sold_by == "take_profit": | |
| return "Nice. You reached your target and practiced taking profit instead of getting greedy." | |
| if pnl_pct > 5: | |
| return "Good practice trade. You made a strong gain. Remember: in real markets, always manage risk." | |
| if pnl_pct > 0: | |
| return "You made a small gain. Good job learning patience and selling with a plan." | |
| if pnl_pct == 0: | |
| return "You ended flat. That is still a lesson: sometimes protecting money is the win." | |
| return "This trade lost practice money. That is part of learning. Check the stop-loss, entry, and signal before trying again." | |
| def evaluate_spot_game_position(position, current_price): | |
| entry = float(position.get("entry_price", current_price)) | |
| current_price = float(current_price) | |
| stop_loss = float(position.get("stop_loss", entry * 0.95)) | |
| take_profit = float(position.get("take_profit", entry * 1.10)) | |
| trailing_enabled = bool(position.get("trailing_enabled", False)) | |
| trailing_pct = float(position.get("trailing_pct", 3.0)) | |
| auto_sell_enabled = bool(position.get("auto_sell_enabled", True)) | |
| highest_price = float(position.get("highest_price", entry)) | |
| highest_price = max(highest_price, current_price) | |
| updated_stop = stop_loss | |
| if trailing_enabled: | |
| trailing_stop = highest_price * (1 - trailing_pct / 100) | |
| updated_stop = max(stop_loss, trailing_stop) | |
| sell_now = False | |
| sell_reason = "none" | |
| if auto_sell_enabled: | |
| if current_price <= updated_stop: | |
| sell_now = True | |
| sell_reason = "trailing_stop" if trailing_enabled and updated_stop > stop_loss else "stop_loss" | |
| elif current_price >= take_profit: | |
| sell_now = True | |
| sell_reason = "take_profit" | |
| pnl_pct = ((current_price - entry) / entry) * 100 if entry > 0 else 0.0 | |
| return { | |
| "sell_now": bool(sell_now), | |
| "sell_reason": sell_reason, | |
| "highest_price": float(highest_price), | |
| "updated_stop_loss": float(updated_stop), | |
| "pnl_pct": float(pnl_pct), | |
| "feedback": game_feedback_message(pnl_pct, sell_reason), | |
| } | |
| def beginner_explanation(analysis): | |
| signal = analysis["action"] | |
| opener = child_friendly_signal_text(signal) | |
| parts = [ | |
| f"🧠 Simple Signal: **{signal}**. {analysis['beginner_signal_reason']}", | |
| f"🔮 Future pressure: **{analysis['future_pressure_label']}**. {analysis['future_pressure_bias']}", | |
| f"🏦 Capital pressure: **{analysis['capital_pressure_label']}** with OFI score {analysis['ofi_score']:+.0f}/100.", | |
| f"📊 Gaussian probability: {analysis['pump_probability_6h']:.1f}% pump probability over 6h and {analysis['pump_probability_12h']:.1f}% over 12h based on CVD pressure.", | |
| f"🛟 Relief bounce: **{analysis['relief_bounce_label']}**. Target zone is about {analysis['relief_target_pct']:+.1f}% above current price, with {analysis['relief_probability']:.0f}% setup probability.", | |
| f"🌦️ Market regime: **{analysis['market_regime']}**.", | |
| f"🧭 Timeframe alignment: **{analysis['timeframe_alignment_label']}** at {analysis['timeframe_alignment_pct']:.0f}%.", | |
| f"₿ BTC pressure is **{analysis['btc_pressure']}**. Many coins follow BTC pressure.", | |
| f"🧩 Setup type: **{analysis['setup_type']}**.", | |
| f"🔊 Current volume is {analysis['volume_label'].lower()} ({analysis['volume_trend_pct']:+.1f}%).", | |
| f"📡 Future volume pressure is estimated at {analysis['future_volume_trend_pct']:+.2f}%.", | |
| f"🧯 Stop-loss lesson: {stop_loss_explanation_for_child()}", | |
| f"🤖 Auto-sell lesson: {auto_sell_explanation_for_child()}", | |
| f"📈 APY lesson: {apy_explanation_for_child()}", | |
| ] | |
| quant_text = analysis.get("quant_explanation", "") | |
| if quant_text: | |
| parts.append(f"🧪 Quant model: {quant_text}") | |
| return opener + "\n\n" + "\n\n".join(parts) | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # MAIN ANALYSIS FUNCTION | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def generate_predictions_and_signals( | |
| df: pd.DataFrame, | |
| forecast_hours: int = 24, | |
| symbol: str = "BTC", | |
| category: str = "major", | |
| ): | |
| if df is None or df.empty or len(df) < 80: | |
| return None | |
| closes = df["Close"].astype(float).to_numpy() | |
| highs = df["High"].astype(float).to_numpy() | |
| lows = df["Low"].astype(float).to_numpy() | |
| volumes = df["Volume"].astype(float).to_numpy() | |
| price = float(closes[-1]) | |
| rsi = calc_rsi(closes) | |
| macd_line, macd_signal, macd_hist = calc_macd(closes) | |
| atr = calc_atr(highs, lows, closes) | |
| support, resistance = find_support_resistance(closes) | |
| bb_upper, bb_middle, bb_lower = calc_bollinger(closes) | |
| vwap = calc_vwap(highs, lows, closes, volumes) | |
| volume_trend_pct, volume_label = calc_volume_trend(volumes) | |
| rvol = relative_volume(volumes) | |
| forecast = ensemble_forecast(closes, volumes, forecast_hours) | |
| predictions = forecast["expected"] | |
| slope = forecast["slope"] | |
| backtest = forecast["backtest"] | |
| confidence = backtest["confidence"] | |
| regime = detect_market_regime(closes, volumes, slope, volume_trend_pct) | |
| risk = historical_risk_engine(closes) | |
| squeeze = detect_squeeze(closes) | |
| fake = detect_fake_breakout( | |
| price, | |
| resistance, | |
| rsi, | |
| macd_hist, | |
| volume_trend_pct, | |
| ) | |
| setup_type = classify_setup( | |
| price, | |
| support, | |
| resistance, | |
| rsi, | |
| macd_hist, | |
| volume_trend_pct, | |
| squeeze["active"], | |
| fake["active"], | |
| ) | |
| stop_loss = smart_stop_loss( | |
| price, | |
| atr, | |
| support, | |
| rsi, | |
| risk["var_drop"], | |
| risk["expected_shortfall_drop"], | |
| ) | |
| take_profit = smart_take_profit(price, stop_loss, atr, resistance) | |
| pct_change_24h = ( | |
| ((price - closes[-24]) / closes[-24]) * 100 | |
| if len(closes) >= 24 and closes[-24] > 0 | |
| else 0.0 | |
| ) | |
| forecast_change = ( | |
| ((predictions[-1] - price) / price) * 100 | |
| if price > 0 | |
| else 0.0 | |
| ) | |
| liq = liquidity_score(volumes) | |
| slippage = slippage_pct_for_asset(category, liq) | |
| estimated_fee_pct = 0.20 | |
| net_expected_return = forecast_change - estimated_fee_pct - slippage | |
| timeframe = analyze_timeframe_alignment(symbol) | |
| btc = btc_market_pressure() | |
| futures = funding_futures_pressure(symbol) | |
| unlock = token_unlock_risk(symbol) | |
| future_pressure = future_pressure_engine( | |
| closes=closes, | |
| volumes=volumes, | |
| forecast=forecast, | |
| btc_pressure_data=btc, | |
| timeframe_alignment=timeframe, | |
| futures_data=futures, | |
| liquidity=liq, | |
| steps=forecast_hours, | |
| ) | |
| order_flow = order_flow_imbalance_engine(df) | |
| relief = relief_bounce_engine( | |
| df, | |
| price, | |
| rsi, | |
| atr, | |
| support, | |
| resistance, | |
| vwap, | |
| ) | |
| quant_results = {} | |
| quant_main = { | |
| "available": False, | |
| "risk_adjusted_score": 0.0, | |
| "probability_up": 50.0, | |
| "edge_ratio": 0.0, | |
| "quant_decision": "QUANT UNAVAILABLE", | |
| "expected_return": 0.0, | |
| "expected_risk": 0.0, | |
| "expected_drawdown": 0.0, | |
| "reliability": "Insufficient History", | |
| "walk_forward_accuracy": 0.0, | |
| "high_confidence_accuracy": 0.0, | |
| "brier_score": 0.0, | |
| "calibration_error": 0.0, | |
| "samples_tested": 0, | |
| "feature_importance": [], | |
| "calibration_table": [], | |
| "quant_explanation": "", | |
| } | |
| if QUANT_MODEL_AVAILABLE: | |
| try: | |
| quant_results = quant_multi_horizon_forecast(df, horizons=(6, 12, 24)) | |
| quant_main = quant_results.get(f"{forecast_hours}h") or quant_results.get("24h") or quant_results.get("12h") or quant_main | |
| except Exception: | |
| quant_results = {} | |
| preliminary_score = score_trade( | |
| rsi, | |
| macd_hist, | |
| price, | |
| support, | |
| resistance, | |
| bb_lower, | |
| bb_upper, | |
| vwap, | |
| slope, | |
| volume_trend_pct, | |
| net_expected_return, | |
| liq, | |
| confidence, | |
| btc, | |
| timeframe, | |
| unlock["score_penalty"], | |
| fake["active"], | |
| future_pressure["future_pressure_score"], | |
| order_flow["capital_pressure_score"], | |
| relief["relief_probability"], | |
| relief["relief_bounce_active"], | |
| quant_main.get("risk_adjusted_score", 0.0), | |
| ) | |
| fused = { | |
| "final_score": preliminary_score, | |
| "final_label": detailed_signal_from_score(preliminary_score), | |
| "quant_weight": 0.0, | |
| } | |
| if QUANT_MODEL_AVAILABLE and quant_main.get("available"): | |
| try: | |
| fused = fuse_rule_signal_with_quant(preliminary_score, quant_main) | |
| except Exception: | |
| pass | |
| score = float(fused["final_score"]) | |
| detailed_action = detailed_signal_from_score(score) | |
| if forecast_change > 5: | |
| status = "Upside Bias" | |
| elif forecast_change < -5: | |
| status = "Downside Bias" | |
| else: | |
| status = "Range Bound" | |
| risk_reward = float((take_profit - price) / max(price - stop_loss, price * 0.001)) | |
| base_analysis_for_blockers = { | |
| "model_confidence": float(confidence), | |
| "liquidity_score": float(liq), | |
| "risk_reward": risk_reward, | |
| "net_expected_return": float(net_expected_return), | |
| "future_pressure_score": future_pressure["future_pressure_score"], | |
| "capital_pressure_score": order_flow["capital_pressure_score"], | |
| "signal_score": float(score), | |
| "fake_breakout_warning": fake["active"], | |
| "unlock_risk": unlock["risk"], | |
| } | |
| blockers = do_not_trade_rules({ | |
| **base_analysis_for_blockers, | |
| "btc_pressure": btc["label"], | |
| "market_regime": regime, | |
| }) | |
| beginner_signal = advanced_score_to_beginner_signal( | |
| score=score, | |
| probability_up=quant_main.get("probability_up") if quant_main.get("available") else None, | |
| edge_ratio=quant_main.get("edge_ratio") if quant_main.get("available") else None, | |
| blockers=blockers, | |
| ) | |
| beginner_reason = beginner_signal_reason(beginner_signal) | |
| analysis = { | |
| "current_price": float(price), | |
| "predicted_trend": predictions, | |
| "lower_80": forecast["lower_80"], | |
| "upper_80": forecast["upper_80"], | |
| "lower_95": forecast["lower_95"], | |
| "upper_95": forecast["upper_95"], | |
| "forecast_change": float(forecast_change), | |
| "net_expected_return": float(net_expected_return), | |
| "future_pressure_score": future_pressure["future_pressure_score"], | |
| "combined_future_pressure_score": float( | |
| future_pressure["future_pressure_score"] + order_flow["capital_pressure_score"] * 0.35 | |
| ), | |
| "future_pressure_label": future_pressure["future_pressure_label"], | |
| "future_pressure_bias": future_pressure["future_pressure_bias"], | |
| "future_buy_pressure_pct": future_pressure["future_buy_pressure_pct"], | |
| "future_sell_pressure_pct": future_pressure["future_sell_pressure_pct"], | |
| "future_volume_trend_pct": future_pressure["future_volume_trend_pct"], | |
| "pressure_path": future_pressure["pressure_path"], | |
| "pressure_table": future_pressure["pressure_table"], | |
| "ofi_score": order_flow["ofi_score"], | |
| "ofi_label": order_flow["ofi_label"], | |
| "cvd_6h": order_flow["cvd_6h"], | |
| "cvd_12h": order_flow["cvd_12h"], | |
| "cvd_6h_z": order_flow["cvd_6h_z"], | |
| "cvd_12h_z": order_flow["cvd_12h_z"], | |
| "pump_probability_6h": order_flow["pump_probability_6h"], | |
| "dump_probability_6h": order_flow["dump_probability_6h"], | |
| "pump_probability_12h": order_flow["pump_probability_12h"], | |
| "dump_probability_12h": order_flow["dump_probability_12h"], | |
| "capital_pressure_label": order_flow["capital_pressure_label"], | |
| "capital_pressure_score": order_flow["capital_pressure_score"], | |
| "cvd_table": order_flow["cvd_table"], | |
| "relief_bounce_active": relief["relief_bounce_active"], | |
| "relief_bounce_label": relief["relief_bounce_label"], | |
| "relief_target": relief["relief_target"], | |
| "relief_target_pct": relief["relief_target_pct"], | |
| "relief_confirmation": relief["relief_confirmation"], | |
| "relief_invalidation": relief["relief_invalidation"], | |
| "relief_probability": relief["relief_probability"], | |
| "relief_setup_score": relief["relief_setup_score"], | |
| "quant_available": bool(quant_main.get("available", False)), | |
| "quant_decision": quant_main.get("quant_decision", "QUANT UNAVAILABLE"), | |
| "quant_probability_up": float(quant_main.get("probability_up", 50.0)), | |
| "quant_probability_down": float(quant_main.get("probability_down", 50.0)), | |
| "quant_expected_return": float(quant_main.get("expected_return", 0.0)), | |
| "quant_expected_risk": float(quant_main.get("expected_risk", 0.0)), | |
| "quant_expected_drawdown": float(quant_main.get("expected_drawdown", 0.0)), | |
| "quant_edge_ratio": float(quant_main.get("edge_ratio", 0.0)), | |
| "quant_risk_adjusted_score": float(quant_main.get("risk_adjusted_score", 0.0)), | |
| "quant_reliability": quant_main.get("reliability", "Insufficient History"), | |
| "quant_walk_forward_accuracy": float(quant_main.get("walk_forward_accuracy", 0.0)), | |
| "quant_high_confidence_accuracy": float(quant_main.get("high_confidence_accuracy", 0.0)), | |
| "quant_brier_score": float(quant_main.get("brier_score", 0.0)), | |
| "quant_calibration_error": float(quant_main.get("calibration_error", 0.0)), | |
| "quant_samples_tested": int(quant_main.get("samples_tested", 0)), | |
| "quant_feature_importance": quant_main.get("feature_importance", []), | |
| "quant_calibration_table": quant_main.get("calibration_table", []), | |
| "quant_explanation": quant_main.get("quant_explanation", ""), | |
| "quant_multi_horizon": quant_results, | |
| "quant_weight": float(fused.get("quant_weight", 0.0)), | |
| "stop_loss": float(stop_loss), | |
| "take_profit": float(take_profit), | |
| "tp1": float(price + max(price - stop_loss, price * 0.01)), | |
| "tp2": float(price + 2 * max(price - stop_loss, price * 0.01)), | |
| "risk_reward": risk_reward, | |
| "default_trailing_stop_pct": 3.0 if category not in {"meme", "ai", "gaming"} else 5.0, | |
| "auto_sell_default": True, | |
| "stop_loss_lesson": stop_loss_explanation_for_child(), | |
| "trailing_stop_lesson": trailing_stop_explanation_for_child(), | |
| "auto_sell_lesson": auto_sell_explanation_for_child(), | |
| "apy_lesson": apy_explanation_for_child(), | |
| "status": status, | |
| "market_regime": regime, | |
| "pct_change_24h": float(pct_change_24h), | |
| "var_95": float(risk["var_95"] * 100), | |
| "var_drop": float(risk["var_drop"]), | |
| "expected_shortfall": float(risk["expected_shortfall"] * 100), | |
| "expected_shortfall_drop": float(risk["expected_shortfall_drop"]), | |
| "model_confidence": float(confidence), | |
| "backtest_direction_accuracy": float(backtest["direction_accuracy"]), | |
| "backtest_mae_pct": float(backtest["mae_pct"]), | |
| "ensemble_weights": forecast["weights"], | |
| "liquidity_score": float(liq), | |
| "relative_volume": float(rvol), | |
| "slippage_pct": float(slippage), | |
| "rsi": float(rsi), | |
| "macd_line": float(macd_line), | |
| "macd_signal": float(macd_signal), | |
| "macd_hist": float(macd_hist), | |
| "atr": float(atr), | |
| "support": float(support), | |
| "resistance": float(resistance), | |
| "bb_upper": float(bb_upper), | |
| "bb_middle": float(bb_middle), | |
| "bb_lower": float(bb_lower), | |
| "vwap": float(vwap), | |
| "signal_score": float(score), | |
| "detailed_action": detailed_action, | |
| "action": beginner_signal, | |
| "beginner_signal": beginner_signal, | |
| "beginner_signal_reason": beginner_reason, | |
| "slope": float(slope), | |
| "volume_trend_pct": float(volume_trend_pct), | |
| "volume_label": volume_label, | |
| "timeframe_alignment_pct": timeframe["alignment_pct"], | |
| "timeframe_alignment_label": timeframe["label"], | |
| "timeframe_frames": timeframe["frames"], | |
| "btc_pressure": btc["label"], | |
| "btc_pressure_score": btc["score"], | |
| "btc_note": btc["note"], | |
| "futures_pressure": futures["label"], | |
| "funding_rate": futures["funding"], | |
| "open_interest": futures["open_interest"], | |
| "futures_note": futures["note"], | |
| "unlock_risk": unlock["risk"], | |
| "volatility_squeeze": squeeze["label"], | |
| "squeeze_active": squeeze["active"], | |
| "squeeze_width_pct": squeeze["width_pct"], | |
| "fake_breakout_warning": fake["active"], | |
| "fake_breakout_label": fake["label"], | |
| "setup_type": setup_type, | |
| "updated_at": int(time.time()), | |
| } | |
| analysis["do_not_trade"] = do_not_trade_rules(analysis) | |
| analysis["why_not_buy"] = why_not_buy_reasons(analysis) | |
| analysis["suggestion"] = beginner_explanation(analysis) | |
| return analysis | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| # 30-DAY OUTLOOK | |
| # ═════════════════════════════════════════════════════════════════════════════ | |
| def calculate_macro_forecast(df_daily: pd.DataFrame): | |
| if df_daily is None or df_daily.empty or len(df_daily) < 30: | |
| return None | |
| closes = df_daily["Close"].astype(float).to_numpy() | |
| volumes = df_daily["Volume"].astype(float).to_numpy() | |
| current = float(closes[-1]) | |
| returns = _returns(closes) | |
| if len(returns) < 10: | |
| return None | |
| recent = returns[-min(90, len(returns)):] | |
| drift = float( | |
| pd.Series(recent) | |
| .ewm(span=min(30, len(recent)), adjust=False) | |
| .mean() | |
| .iloc[-1] | |
| ) | |
| volatility = float( | |
| pd.Series(recent) | |
| .ewm(span=min(30, len(recent)), adjust=False) | |
| .std() | |
| .iloc[-1] | |
| ) | |
| if np.isnan(volatility) or volatility <= 0: | |
| volatility = float(np.std(recent)) if np.std(recent) > 0 else 0.01 | |
| days = np.arange(1, 31) | |
| expected = current * np.exp(drift * days) | |
| lower = current * np.exp((drift - 0.75 * volatility) * days) | |
| upper = current * np.exp((drift + 0.75 * volatility) * days) | |
| ridge, _ = _ridge_return_forecast(closes, 30, 90) | |
| final = 0.62 * expected + 0.38 * ridge | |
| if len(volumes) >= 20 and volumes[-1] > 0: | |
| volume_path = _volume_future_forecast(volumes, 30) | |
| volume_trend_30d = ((float(volume_path[-1]) - float(volumes[-1])) / float(volumes[-1])) * 100 | |
| else: | |
| volume_trend_30d = 0.0 | |
| forecast_df = pd.DataFrame({ | |
| "Day": days, | |
| "Expected Price": final, | |
| "Lower Zone": np.minimum(lower, final), | |
| "Upper Zone": np.maximum(upper, final), | |
| }) | |
| trend = ((float(final[-1]) - current) / current) * 100 if current > 0 else 0.0 | |
| if trend > 12: | |
| label = "Bullish" | |
| elif trend > 3: | |
| label = "Slightly Bullish" | |
| elif trend < -12: | |
| label = "Bearish" | |
| elif trend < -3: | |
| label = "Slightly Bearish" | |
| else: | |
| label = "Sideways" | |
| return { | |
| "current": current, | |
| "day_3": float(final[2]), | |
| "day_6": float(final[5]), | |
| "day_9": float(final[8]), | |
| "day_15": float(final[14]), | |
| "day_30": float(final[29]), | |
| "low_30": float(np.minimum(lower, final)[29]), | |
| "high_30": float(np.maximum(upper, final)[29]), | |
| "trend_30d_pct": float(trend), | |
| "trend_label": label, | |
| "volume_trend_30d": float(volume_trend_30d), | |
| "forecast_df": forecast_df, | |
| } |