import os from dataclasses import dataclass, field from pathlib import Path from typing import Optional _PROJECT_DIR = Path(__file__).resolve().parent try: from dotenv import load_dotenv load_dotenv() except ImportError: pass @dataclass class IBConfig: host: str = os.getenv("IB_HOST", "127.0.0.1") port: int = int(os.getenv("IB_PORT", "4001")) client_id: int = int(os.getenv("IB_CLIENT_ID", "1")) account: Optional[str] = os.getenv("IB_ACCOUNT", "U4845070") is_paper: bool = os.getenv("IB_PAPER", "false").lower() == "true" @property def port_label(self) -> str: if self.port == 4002: return "Gateway Paper (4002)" elif self.port == 4001: return "Gateway Live (4001)" elif self.port == 7497: return "TWS Paper (7497)" elif self.port == 7496: return "TWS Live (7496)" return f"Unknown ({self.port})" @dataclass class StockStrategyConfig: enabled: bool = False # MAStock disabled 2026-08-11: +101.66(ShortTerm)/-19.98(MeanRev)/-65.47(MAStock) since 08-03 symbols: list[str] = field(default_factory=lambda: ["AMZN", "AAPL", "NVDA", "NOK"]) currency: str = "USD" exchange: str = "SMART" bar_size: str = "1 min" lookback_days: int = 5 fast_ma_period: int = 20 slow_ma_period: int = 50 trade_value_usd: float = 2000.0 # per-trade dollar amount; qty = floor(value / price) min_profit_pct: float = 1.5 stop_loss_pct: float = 2.5 use_hard_stop: bool = True # place real GTC STP orders at the exchange entry_confirm_bars: int = 2 # entry signal must hold for N consecutive bars stop_cooldown_minutes: int = 30 # no re-entry on same symbol for N min after a stop-out use_trailing_stop: bool = True # once profit >= min_profit, trail the stop below the peak high trailing_stop_pct: float = 2.0 # trailing stop distance below peak high adx_min: float = 30.0 # ADX trend filter for entries (was 25; 08-06 raised to avoid weak-trend whipsaw) require_slow_ma_slope: bool = True # only enter if the slow MA is itself rising (no knife-catching in a downtrend) high @dataclass class ShortTermConfig: enabled: bool = True symbols: list[str] = field(default_factory=lambda: ["AMZN", "AAPL", "NVDA", "NOK"]) currency: str = "USD" exchange: str = "SMART" bar_size: str = "1 min" lookback_days: int = 5 fast_ma_period: int = 5 slow_ma_period: int = 10 trade_value_usd: float = 2000.0 max_hold_days: int = 0 # 0 = unlimited hold (08-05 撤销 5 天上限) min_profit_pct: float = 1.5 stop_loss_pct: float = 2.5 use_hard_stop: bool = True entry_confirm_bars: int = 2 stop_cooldown_minutes: int = 30 use_trailing_stop: bool = True trailing_stop_pct: float = 2.0 @dataclass class ForexStrategyConfig: enabled: bool = False pairs: list[str] = field(default_factory=lambda: ["EUR.USD", "GBP.USD"]) exchange: str = "IDEALPRO" bar_size: str = "1 min" lookback_days: int = 5 fast_ma_period: int = 12 slow_ma_period: int = 26 trade_units: int = 3000 stop_loss_pct: float = 1.0 @dataclass class MeanReversionConfig: enabled: bool = True symbols: list[str] = field(default_factory=lambda: ["AMZN", "AAPL", "NVDA", "NOK"]) currency: str = "USD" exchange: str = "SMART" bar_size: str = "1 min" lookback_days: int = 5 trade_value_usd: float = 2000.0 rsi_period: int = 14 rsi_oversold: float = 30.0 rsi_overbought: float = 70.0 bb_period: int = 20 bb_std: float = 2.0 rapid_drop_pct: float = 1.0 rapid_drop_bars: int = 5 recovery_pct: float = 1.5 min_profit_pct: float = 1.5 stop_loss_pct: float = 2.5 use_hard_stop: bool = True entry_confirm_bars: int = 2 stop_cooldown_minutes: int = 30 use_trailing_stop: bool = True trailing_stop_pct: float = 2.0 trend_ma_period: int = 50 # mean-reversion buys only above this SMA @dataclass class AppConfig: ib: IBConfig = field(default_factory=IBConfig) stock: StockStrategyConfig = field(default_factory=StockStrategyConfig) short_term: ShortTermConfig = field(default_factory=ShortTermConfig) forex: ForexStrategyConfig = field(default_factory=ForexStrategyConfig) mean_reversion: MeanReversionConfig = field(default_factory=MeanReversionConfig) max_retries: int = 5 retry_delay: int = 5 loop_interval: float = 60.0 order_timeout: float = 30.0 max_positions: int = 12 # global cap on concurrent open lots across all strategies max_daily_loss: float = 150.0 # stop opening new positions for the day beyond this realized loss sell_only: bool = False # liquidation mode: no new buys, strategies only manage exits max_symbol_value_usd: float = 2300.0 # per-symbol total position value cap (all strategies combined) sell_cooldown_minutes: int = 30 # global: no strategy may (re)buy a symbol within N min of ANY sell sell_improvement_pct: float = 0.5 # beyond the cooldown, re-buy only if price is >=0.5% below last sell sell_improvement_window_minutes: int = 120 # how long the price-improvement rule applies after a sell symbol_trade_value_usd: dict[str, float] = field( # per-symbol per-trade dollar value override (halved for gap-prone NOK, 08-06) default_factory=lambda: {"NOK": 1000.0} ) # absolute path: independent of the working directory the bot is started from state_file: str = str(_PROJECT_DIR / "bot_state.json") config = AppConfig()