CryptoNAV_Tracker / utils.py
Nomnommish's picture
Update utils.py
e51b2f8 verified
Raw
History Blame Contribute Delete
80.4 kB
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,
}