Lk / config.py
Kashaf1's picture
Upload moirai_forecast_app contents
ef20ebe
Raw History Blame Contribute Delete
10.3 kB
"""
Central configuration for the Moirai-MoE / AutoARIMA / Chronos-2
Forex/Crypto forecasting system. All thresholds, feature schema, and
resource limits live here so they are auditable and change in one
place (spec sections 19-20, 30, 42, 85, 111).
"""
from dataclasses import dataclass
from typing import Optional
# --- Predictive models ---------------------------------------------------
# Moirai-MoE was the sole model through Aug 2026. AutoARIMA and
# Chronos-2 were added alongside it as fully independent, selectable
# models (app.py's model picker) -- never blended, never a fallback for
# one another (see forecast_types.py's docstring for what "independent"
# means structurally). MODEL_CHECKPOINT is kept as a standalone constant
# (rather than folded into the registry below) because moirai_model.py
# and existing tests already reference it directly.
MODEL_CHECKPOINT = "Salesforce/moirai-moe-1.0-R-small"
# key -> (display name, module.ClassName as a string for lazy import,
# real pretrained checkpoint or None for a classical/no-checkpoint model)
AVAILABLE_MODELS = {
"moirai": {"display_name": "Moirai-MoE-1.0-R-Small", "checkpoint": MODEL_CHECKPOINT},
"autoarima": {"display_name": "AutoARIMA", "checkpoint": None},
"chronos2": {"display_name": "Chronos-2", "checkpoint": "amazon/chronos-2"},
}
DEFAULT_MODEL_KEY = "moirai"
# --- Timeframes ---------------------------------------------------------
# native = directly retrievable from yfinance
# derived = built via causal resampling from a finer native timeframe (section 10-11)
NATIVE_INTRADAY = ["1m", "2m", "5m", "15m", "30m", "1h"]
NATIVE_OTHER = ["1d", "1wk", "1mo"]
DERIVED_MAP = {
# target_tf: (source_tf, bars_per_bin)
"10m": ("5m", 2),
"2h": ("1h", 2),
"4h": ("1h", 4),
}
ALL_TIMEFRAMES = NATIVE_INTRADAY + list(DERIVED_MAP.keys()) + NATIVE_OTHER
TIMEFRAME_MINUTES = {
"1m": 1, "2m": 2, "5m": 5, "10m": 10, "15m": 15, "30m": 30,
"1h": 60, "2h": 120, "4h": 240, "1d": 1440, "1wk": 10080, "1mo": 43200,
}
# Approximate Yahoo intraday lookback ceilings in days (source-enforced,
# see spec section 8; yfinance will simply return less data than
# requested rather than erroring, so we surface this explicitly)
YAHOO_INTRADAY_MAX_DAYS = {
"1m": 7, "2m": 60, "5m": 60, "15m": 60, "30m": 60, "1h": 730,
}
# Pandas frequency alias for each timeframe. This is passed EXPLICITLY to
# gluonts' PandasDataset(freq=...) in moirai_model.py rather than relying
# on pd.infer_freq() on the raw timestamps -- real Yahoo data has gaps
# (weekends, exchange closures, occasional missing bars) that break
# automatic frequency inference deterministically, on every call, which
# is exactly the "Model errors: 11200/11200" failure mode this fixes.
TIMEFRAME_TO_PANDAS_FREQ = {
"1m": "1min", "2m": "2min", "5m": "5min", "10m": "10min",
"15m": "15min", "30m": "30min",
"1h": "1h", "2h": "2h", "4h": "4h",
"1d": "1D", "1wk": "1W", "1mo": "1MS",
}
# --- History window choices (UI-facing) --------------------------------
# Fine-grained day counts plus month counts, so the person can pick from
# a dropdown instead of typing a period string. data_layer.resolve_history_window()
# turns a label from this list into an actual start date (or "max").
HISTORY_WINDOW_CHOICES = (
[f"{d} day" if d == 1 else f"{d} days" for d in range(1, 31)]
+ [f"{m} months" for m in range(2, 13)]
+ ["1 year", "2 years", "5 years", "max"]
)
# --- Symbol choices (UI-facing) -----------------------------------------
# A curated, non-exhaustive list of common Yahoo Finance Forex/Crypto
# tickers for convenient selection (spec section 12 wants search/
# autocomplete "when feasible" -- yfinance itself exposes no symbol
# directory, so this is that best-effort list). The UI Dropdown also
# allows a custom value, so any Yahoo-supported symbol still works even
# if it isn't in this list -- this list is a convenience, not a limit.
FOREX_SYMBOLS = [
"EURUSD=X", "GBPUSD=X", "USDJPY=X", "USDCHF=X", "AUDUSD=X",
"USDCAD=X", "NZDUSD=X", "EURGBP=X", "EURJPY=X", "GBPJPY=X",
"EURCHF=X", "AUDJPY=X", "EURAUD=X", "CHFJPY=X", "GBPCHF=X",
"USDCNY=X", "USDINR=X", "USDMXN=X", "USDZAR=X", "USDTRY=X",
"USDSEK=X", "USDNOK=X", "USDSGD=X", "USDHKD=X",
]
CRYPTO_SYMBOLS = [
"BTC-USD", "ETH-USD", "BNB-USD", "XRP-USD", "SOL-USD",
"ADA-USD", "DOGE-USD", "AVAX-USD", "DOT-USD", "LTC-USD",
"LINK-USD", "MATIC-USD", "TRX-USD", "SHIB-USD", "BCH-USD",
"XLM-USD", "ATOM-USD", "UNI-USD", "ETC-USD", "XMR-USD",
]
ALL_SYMBOLS = FOREX_SYMBOLS + CRYPTO_SYMBOLS
@dataclass
class IndicatorConfig:
rsi_period: int = 14
macd_fast: int = 12
macd_slow: int = 26
macd_signal: int = 9
bb_period: int = 20
bb_std: float = 2.0
sma_periods: tuple = (20, 50)
ema_periods: tuple = (12, 26)
stoch_k: int = 14
stoch_d: int = 3
atr_period: int = 14
adx_period: int = 14
roc_period: int = 12
# Deterministic feature schema — order matters and is persisted with every
# forecast/model artifact (spec section 85). Never silently reordered.
PAST_DYNAMIC_FEATURES = [
"open", "high", "low", "volume",
"rsi",
"macd", "macd_signal", "macd_hist",
"bb_mid", "bb_upper", "bb_lower",
"sma_20", "sma_50",
"ema_12", "ema_26",
"stoch_k", "stoch_d",
"atr", "adx", "roc", "obv",
]
TARGET_FEATURE = "close"
@dataclass
class DecisionConfig:
cost_bps: float = 5.0 # default round-trip cost estimate, basis points
buy_threshold: float = 0.0015 # expected edge required to BUY, post-cost
sell_threshold: float = -0.0015
min_probability: float = 0.55 # P(return favorable) required to act
max_interval_width_pct: float = 0.05 # HOLD if forecast too uncertain (P90-P10)
@dataclass
class BacktestCostProfile:
spread_bps: float
commission_bps: float
slippage_bps: float
execution_delay_bars: int = 1
@dataclass
class RiskManagement:
"""Optional intra-trade exit rules, checked against intrabar high/low
between entry and the horizon exit (see backtest_engine.py). Added
Aug 2026 after real backtest data showed the previously-only exit
rule -- always hold the full fixed horizon, no matter what happens
in between -- was exactly why losing trades ran larger than winning
ones: a trade that turns adverse right after entry still rides out
the entire horizon before closing, with no way to cut it short.
None (the default for both fields) means "no early exit at all",
which reproduces every prior backtest's numbers exactly -- this is
opt-in, not a change to existing behavior."""
stop_loss_frac: Optional[float] = None # e.g. 0.003 = exit if price moves 0.3% against the position
take_profit_frac: Optional[float] = None # e.g. 0.006 = exit if price moves 0.6% in favor of the position
@dataclass
class WalkForwardConfig:
"""Controls for walk_forward.py's train/validation/test split and
the (deliberately small, bounded) parameter search -- spec: "do not
search thousands of combinations", "avoid parameter values that
produce only a handful of trades". Every field here is a knob a
person can see and change, not a hidden magic number (section 25)."""
train_frac: float = 0.5
val_frac: float = 0.25
# remaining ~0.25 is test, computed as 1 - train_frac - val_frac
min_probability_grid: tuple = (0.55, 0.60, 0.65, 0.70)
buy_threshold_grid: tuple = (0.0010, 0.0015, 0.0020, 0.0030)
min_trades_for_acceptance: int = 30 # item 13: reject configs "profitable" on a handful of trades
min_trades_for_calibration: int = 50 # separate, usually-larger floor for calibration itself (needs more pairs than a single accept/reject check)
max_single_period_profit_share: float = 0.70 # item 18: reject if one sub-period explains >70% of total profit
# Forex and crypto have different microstructure -> different cost profiles
# (spec section 30). These are reasonable defaults, not measured values —
# override them per-symbol if you have real spread/fee data.
COST_PROFILES = {
"forex": BacktestCostProfile(spread_bps=1.5, commission_bps=0.0, slippage_bps=0.5),
"crypto": BacktestCostProfile(spread_bps=2.0, commission_bps=10.0, slippage_bps=2.0),
}
def round_trip_cost_frac(profile: BacktestCostProfile) -> float:
"""Round-trip cost (spread+commission+slippage) as a return fraction.
Lives here (not in backtest_engine.py) because it's a pure function of
a COST_PROFILES entry, and both the backtest engine and the Live tab's
decide() call need the same, real, asset-class-specific number rather
than each guessing independently (spec section 30)."""
return (profile.spread_bps + profile.commission_bps + profile.slippage_bps) / 10_000
def infer_asset_class(symbol: str) -> str:
"""Best-effort classification of a Yahoo ticker into a COST_PROFILES
key. The Live tab has no explicit asset-class selector (unlike the
Backtest tab's `b_class` radio), so without this it silently fell back
to a flat, asset-unaware cost estimate for every symbol -- wrong for
both forex (too high) and crypto (too low). Checks the curated lists
first, then falls back to Yahoo's own ticker conventions ('=X' suffix
for forex pairs, a '-USD'/'-USDT'/etc. suffix for crypto pairs) for any
other symbol typed via allow_custom_value, since those conventions are
Yahoo's, not a guess of ours. Defaults to 'forex' (the cheaper, more
conservative profile) only if nothing matches."""
if symbol in FOREX_SYMBOLS:
return "forex"
if symbol in CRYPTO_SYMBOLS:
return "crypto"
s = symbol.strip().upper()
if s.endswith("=X"):
return "forex"
quote = s.split("-")[-1] if "-" in s else ""
if quote in ("USD", "USDT", "USDC", "BTC", "ETH"):
return "crypto"
return "forex"
# Context length ceiling — validate against the ACTUAL loaded checkpoint's
# max_seq_len at runtime (moirai_model.py does this); this is a
# conservative fallback default, not an assumption of unlimited context
# (spec section 86).
MODEL_MAX_CONTEXT_DEFAULT = 512
RESOURCE_LIMITS = dict(cpu_cores=2, ram_gb=16, disk_gb=50, max_forecast_samples=100, max_workers=2)