|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
|
|
|
|
|
from collections.abc import Iterable
|
|
from collections.abc import Iterable
|
|
|
from datetime import datetime, timezone
|
|
from datetime import datetime, timezone
|
|
|
|
|
+from math import tanh
|
|
|
from uuid import uuid4
|
|
from uuid import uuid4
|
|
|
|
|
|
|
|
from argus_mcp.models import MarketQuote, RegimeSnapshot, SignalImpact
|
|
from argus_mcp.models import MarketQuote, RegimeSnapshot, SignalImpact
|
|
@@ -13,7 +14,14 @@ def _change(quote: MarketQuote | None) -> float:
|
|
|
return float(quote.change_pct)
|
|
return float(quote.change_pct)
|
|
|
|
|
|
|
|
|
|
|
|
|
-def _score_component(value: float, weight: float) -> float:
|
|
|
|
|
|
|
+def _norm(value: float, scale: float = 1.0) -> float:
|
|
|
|
|
+ if scale <= 0:
|
|
|
|
|
+ return 0.0
|
|
|
|
|
+ return tanh(value / scale)
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+def _append_impact(impacts: list[SignalImpact], name: str, value: float, weight: float, note: str) -> float:
|
|
|
|
|
+ impacts.append(SignalImpact(name=name, value=value, weight=weight, note=note))
|
|
|
return value * weight
|
|
return value * weight
|
|
|
|
|
|
|
|
|
|
|
|
@@ -23,82 +31,141 @@ def build_regime_snapshot(quotes: Iterable[MarketQuote]) -> RegimeSnapshot:
|
|
|
|
|
|
|
|
qqq = by_symbol.get("QQQ")
|
|
qqq = by_symbol.get("QQQ")
|
|
|
spy = by_symbol.get("SPY")
|
|
spy = by_symbol.get("SPY")
|
|
|
- dxy = by_symbol.get("DXY") or by_symbol.get("UUP")
|
|
|
|
|
hyg = by_symbol.get("HYG")
|
|
hyg = by_symbol.get("HYG")
|
|
|
|
|
+ vxx = by_symbol.get("VXX")
|
|
|
|
|
+ uvxy = by_symbol.get("UVXY")
|
|
|
|
|
+ uup = by_symbol.get("UUP") or by_symbol.get("DXY")
|
|
|
|
|
+ tlt = by_symbol.get("TLT")
|
|
|
|
|
+ gld = by_symbol.get("GLD")
|
|
|
|
|
+ xle = by_symbol.get("XLE")
|
|
|
|
|
+ uso = by_symbol.get("USO")
|
|
|
|
|
+ xlk = by_symbol.get("XLK")
|
|
|
|
|
+ smh = by_symbol.get("SMH")
|
|
|
|
|
+ iyt = by_symbol.get("IYT")
|
|
|
|
|
+ jets = by_symbol.get("JETS")
|
|
|
|
|
+ zim = by_symbol.get("ZIM")
|
|
|
|
|
+ sblk = by_symbol.get("SBLK")
|
|
|
|
|
+ dht = by_symbol.get("DHT")
|
|
|
|
|
+ chrw = by_symbol.get("CHRW")
|
|
|
btc = by_symbol.get("BTCUSD") or by_symbol.get("BTC/USD")
|
|
btc = by_symbol.get("BTCUSD") or by_symbol.get("BTC/USD")
|
|
|
eth = by_symbol.get("ETHUSD") or by_symbol.get("ETH/USD")
|
|
eth = by_symbol.get("ETHUSD") or by_symbol.get("ETH/USD")
|
|
|
- vxx = by_symbol.get("VXX")
|
|
|
|
|
|
|
|
|
|
- risk = 0.0
|
|
|
|
|
- stress = 0.0
|
|
|
|
|
- liquidity = 0.0
|
|
|
|
|
- compression = 0.0
|
|
|
|
|
impacts: list[SignalImpact] = []
|
|
impacts: list[SignalImpact] = []
|
|
|
|
|
|
|
|
|
|
+ qqq_move = _norm(_change(qqq), 1.5)
|
|
|
|
|
+ spy_move = _norm(_change(spy), 1.5)
|
|
|
|
|
+ qqq_vs_spy = _norm((_change(qqq) - _change(spy)) * 2.0, 1.0)
|
|
|
|
|
+ btc_move = _norm(_change(btc), 2.5)
|
|
|
|
|
+ eth_vs_btc = _norm((_change(eth) - _change(btc)) * 2.0, 1.5)
|
|
|
|
|
+ hyg_move = _norm(_change(hyg), 0.8)
|
|
|
|
|
+ vxx_move = _norm(_change(vxx), 2.0)
|
|
|
|
|
+ uvxy_move = _norm(_change(uvxy), 3.0)
|
|
|
|
|
+ uup_move = _norm(_change(uup), 0.5)
|
|
|
|
|
+ tlt_move = _norm(_change(tlt), 0.8)
|
|
|
|
|
+ gld_move = _norm(_change(gld), 1.2)
|
|
|
|
|
+ xle_move = _norm(_change(xle), 1.8)
|
|
|
|
|
+
|
|
|
|
|
+ risk_appetite = 0.0
|
|
|
|
|
+ if qqq:
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "qqq_momentum", qqq_move, 0.22, "Growth equity momentum")
|
|
|
if qqq and spy:
|
|
if qqq and spy:
|
|
|
- spread = _change(qqq) - _change(spy)
|
|
|
|
|
- delta = _score_component(spread, 0.35)
|
|
|
|
|
- risk += delta
|
|
|
|
|
- impacts.append(SignalImpact(name="qqq_vs_spy", value=spread, weight=0.35, note="Speculative leadership spread"))
|
|
|
|
|
-
|
|
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "qqq_vs_spy", qqq_vs_spy, 0.28, "Speculative leadership over broad market")
|
|
|
if btc:
|
|
if btc:
|
|
|
- delta = _score_component(_change(btc), 0.3)
|
|
|
|
|
- risk += delta
|
|
|
|
|
- impacts.append(SignalImpact(name="btc_momentum", value=_change(btc), weight=0.3, note="Crypto bid strength"))
|
|
|
|
|
-
|
|
|
|
|
- if eth:
|
|
|
|
|
- delta = _score_component(_change(eth), 0.25)
|
|
|
|
|
- risk += delta
|
|
|
|
|
- impacts.append(SignalImpact(name="eth_momentum", value=_change(eth), weight=0.25, note="Altcoin relative strength"))
|
|
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "btc_momentum", btc_move, 0.30, "Crypto beta appetite")
|
|
|
|
|
+ if btc and eth:
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "eth_vs_btc", eth_vs_btc, 0.20, "Altcoin breadth versus BTC")
|
|
|
|
|
+ if xlk:
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "xlk_momentum", _norm(_change(xlk), 1.2), 0.14, "Nasdaq / tech leadership")
|
|
|
|
|
+ if smh:
|
|
|
|
|
+ risk_appetite += _append_impact(impacts, "smh_momentum", _norm(_change(smh), 1.4), 0.16, "Semiconductor leadership")
|
|
|
|
|
|
|
|
- if dxy:
|
|
|
|
|
- delta = _score_component(_change(dxy), 0.45)
|
|
|
|
|
- stress += delta
|
|
|
|
|
- liquidity -= delta
|
|
|
|
|
- impacts.append(SignalImpact(name="dollar_strength", value=_change(dxy), weight=0.45, note="Dollar pressure on liquidity"))
|
|
|
|
|
|
|
+ stress = 0.0
|
|
|
|
|
+ if vxx:
|
|
|
|
|
+ stress += _append_impact(impacts, "vxx_stress", vxx_move, 0.40, "Volatility demand proxy")
|
|
|
|
|
+ if uvxy:
|
|
|
|
|
+ stress += _append_impact(impacts, "uvxy_shock", uvxy_move, 0.25, "Acute volatility shock proxy")
|
|
|
|
|
+ if hyg:
|
|
|
|
|
+ stress += _append_impact(impacts, "hyg_inverse", -hyg_move, 0.20, "Credit weakness raises stress")
|
|
|
|
|
+ if tlt:
|
|
|
|
|
+ stress += _append_impact(impacts, "tlt_bid", tlt_move, 0.15, "Bond bid as defensive positioning")
|
|
|
|
|
+
|
|
|
|
|
+ transport_pressure = 0.0
|
|
|
|
|
+ if iyt:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "iyt_inverse", -_norm(_change(iyt), 1.0), 0.22, "Transportation weakness")
|
|
|
|
|
+ if jets:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "jets_inverse", -_norm(_change(jets), 1.0), 0.16, "Air travel / transport demand weakness")
|
|
|
|
|
+ if zim:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "zim_inverse", -_norm(_change(zim), 2.0), 0.20, "Container shipping weakness")
|
|
|
|
|
+ if sblk:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "sblk_inverse", -_norm(_change(sblk), 2.0), 0.14, "Dry bulk shipping weakness")
|
|
|
|
|
+ if dht:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "dht_inverse", -_norm(_change(dht), 2.0), 0.14, "Tanker shipping weakness")
|
|
|
|
|
+ if chrw:
|
|
|
|
|
+ transport_pressure += _append_impact(impacts, "chrw_inverse", -_norm(_change(chrw), 1.5), 0.14, "Logistics / freight brokerage weakness")
|
|
|
|
|
|
|
|
|
|
+ liquidity = 0.0
|
|
|
|
|
+ if uup:
|
|
|
|
|
+ liquidity += _append_impact(impacts, "uup_inverse", -uup_move, 0.45, "Softer dollar supports liquidity")
|
|
|
if hyg:
|
|
if hyg:
|
|
|
- delta = _score_component(_change(hyg), 0.35)
|
|
|
|
|
- liquidity += delta
|
|
|
|
|
- stress -= delta
|
|
|
|
|
- impacts.append(SignalImpact(name="credit_spread_proxy", value=_change(hyg), weight=0.35, note="Credit appetite proxy"))
|
|
|
|
|
|
|
+ liquidity += _append_impact(impacts, "hyg_support", hyg_move, 0.25, "Credit support for liquidity")
|
|
|
|
|
+ if qqq:
|
|
|
|
|
+ liquidity += _append_impact(impacts, "qqq_support", qqq_move, 0.15, "Equity strength confirms liquidity")
|
|
|
|
|
+ if btc:
|
|
|
|
|
+ liquidity += _append_impact(impacts, "btc_support", btc_move, 0.15, "Crypto participation confirms liquidity")
|
|
|
|
|
|
|
|
- if vxx:
|
|
|
|
|
- delta = _score_component(_change(vxx), 0.6)
|
|
|
|
|
- stress += delta
|
|
|
|
|
- compression -= abs(delta)
|
|
|
|
|
- impacts.append(SignalImpact(name="volatility_proxy", value=_change(vxx), weight=0.6, note="Stress and vol demand proxy"))
|
|
|
|
|
|
|
+ real_asset_pressure = 0.0
|
|
|
|
|
+ if gld:
|
|
|
|
|
+ real_asset_pressure += _append_impact(impacts, "gld_strength", gld_move, 0.55, "Gold strength / hard-asset demand")
|
|
|
|
|
+ if xle:
|
|
|
|
|
+ real_asset_pressure += _append_impact(impacts, "xle_strength", xle_move, 0.45, "Energy / commodity cycle pressure")
|
|
|
|
|
+ if uso:
|
|
|
|
|
+ real_asset_pressure += _append_impact(impacts, "uso_strength", _norm(_change(uso), 3.0), 0.35, "Crude oil pressure / inflation impulse")
|
|
|
|
|
+
|
|
|
|
|
+ compression = max(0.0, 1.0 - (0.40 * abs(risk_appetite) + 0.30 * abs(stress) + 0.20 * abs(liquidity) + 0.10 * abs(transport_pressure)))
|
|
|
|
|
|
|
|
if not quote_list:
|
|
if not quote_list:
|
|
|
regime = "no_data"
|
|
regime = "no_data"
|
|
|
confidence = 0.0
|
|
confidence = 0.0
|
|
|
summary = "No provider data available yet."
|
|
summary = "No provider data available yet."
|
|
|
|
|
+ regime_scores = {
|
|
|
|
|
+ "risk_on": 0.0,
|
|
|
|
|
+ "risk_off": 0.0,
|
|
|
|
|
+ "stress": 0.0,
|
|
|
|
|
+ "compression": 0.0,
|
|
|
|
|
+ "real_asset_inflation": 0.0,
|
|
|
|
|
+ }
|
|
|
else:
|
|
else:
|
|
|
- scores = {
|
|
|
|
|
- "risk_on": risk,
|
|
|
|
|
- "stress": stress,
|
|
|
|
|
- "liquidity": liquidity,
|
|
|
|
|
- "compression": compression,
|
|
|
|
|
|
|
+ regime_scores = {
|
|
|
|
|
+ "risk_on": 0.50 * risk_appetite + 0.28 * liquidity - 0.25 * stress - 0.10 * real_asset_pressure - 0.12 * transport_pressure,
|
|
|
|
|
+ "risk_off": 0.40 * stress - 0.28 * risk_appetite - 0.18 * liquidity + 0.08 * real_asset_pressure + 0.26 * transport_pressure,
|
|
|
|
|
+ "stress": 0.50 * stress + 0.42 * transport_pressure - 0.15 * liquidity - 0.10 * risk_appetite,
|
|
|
|
|
+ "compression": compression - 0.12 * abs(real_asset_pressure) - 0.10 * abs(transport_pressure),
|
|
|
|
|
+ "real_asset_inflation": 0.60 * real_asset_pressure + 0.12 * stress + 0.08 * liquidity - 0.10 * risk_appetite,
|
|
|
}
|
|
}
|
|
|
- regime = max(scores, key=scores.get)
|
|
|
|
|
- top_score = scores[regime]
|
|
|
|
|
- if abs(top_score) < 0.25:
|
|
|
|
|
|
|
+ ranked = sorted(regime_scores.items(), key=lambda item: item[1], reverse=True)
|
|
|
|
|
+ regime, top_score = ranked[0]
|
|
|
|
|
+ second_score = ranked[1][1] if len(ranked) > 1 else 0.0
|
|
|
|
|
+ separation = top_score - second_score
|
|
|
|
|
+ if top_score < 0.18 or (separation < 0.08 and top_score < 0.55):
|
|
|
regime = "neutral"
|
|
regime = "neutral"
|
|
|
- confidence = 0.1
|
|
|
|
|
- summary = "Signals are mixed or too weak to call a regime with confidence."
|
|
|
|
|
|
|
+ confidence = max(0.1, min(0.35, top_score + max(separation, 0.0)))
|
|
|
|
|
+ summary = "Signals are mixed or weak, no regime has clear control."
|
|
|
else:
|
|
else:
|
|
|
- confidence = min(1.0, max(0.1, abs(top_score) / 3.0))
|
|
|
|
|
|
|
+ confidence = max(0.15, min(0.95, 0.55 * top_score + 0.45 * separation))
|
|
|
summary = {
|
|
summary = {
|
|
|
- "risk_on": "Speculative risk appetite is leading.",
|
|
|
|
|
- "stress": "Volatility or funding stress is dominating.",
|
|
|
|
|
- "liquidity": "Liquidity support is improving.",
|
|
|
|
|
- "compression": "Market conditions look compressed and range-like.",
|
|
|
|
|
|
|
+ "risk_on": "Speculative risk appetite and liquidity are supportive for crypto.",
|
|
|
|
|
+ "risk_off": "Defensive positioning is building against crypto risk assets.",
|
|
|
|
|
+ "stress": "Volatility and macro stress are dominating the environment.",
|
|
|
|
|
+ "compression": "Cross-market signals are subdued and range-like.",
|
|
|
|
|
+ "real_asset_inflation": "Hard-asset and commodity pressure is rising relative to financial assets.",
|
|
|
}[regime]
|
|
}[regime]
|
|
|
|
|
|
|
|
components = {
|
|
components = {
|
|
|
- "risk_on": risk,
|
|
|
|
|
|
|
+ "risk_appetite": risk_appetite,
|
|
|
"stress": stress,
|
|
"stress": stress,
|
|
|
"liquidity": liquidity,
|
|
"liquidity": liquidity,
|
|
|
|
|
+ "real_asset_pressure": real_asset_pressure,
|
|
|
|
|
+ "transport_pressure": transport_pressure,
|
|
|
"compression": compression,
|
|
"compression": compression,
|
|
|
}
|
|
}
|
|
|
|
|
|