227 lines
13 KiB
Python
227 lines
13 KiB
Python
"""
|
|
╔══════════════════════════════════════════════════════════════════╗
|
|
║ TRADING SYSTEM — Configuration Module ║
|
|
║ 19 Strategies · 4 Instruments · Full Automation ║
|
|
╚══════════════════════════════════════════════════════════════════╝
|
|
"""
|
|
from __future__ import annotations
|
|
import os
|
|
from pathlib import Path
|
|
from dataclasses import dataclass, field
|
|
from typing import Dict, List, Optional
|
|
from dotenv import load_dotenv
|
|
|
|
# ── Load .env ────────────────────────────────────────────────────
|
|
_env_path = Path(__file__).parent / ".env"
|
|
if _env_path.exists():
|
|
load_dotenv(_env_path)
|
|
else:
|
|
_example = Path(__file__).parent / ".env.example"
|
|
if _example.exists():
|
|
load_dotenv(_example)
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────
|
|
def _env(key: str, default: str = "") -> str:
|
|
return os.getenv(key, default)
|
|
|
|
def _env_float(key: str, default: float = 0.0) -> float:
|
|
return float(os.getenv(key, str(default)))
|
|
|
|
def _env_int(key: str, default: int = 0) -> int:
|
|
return int(os.getenv(key, str(default)))
|
|
|
|
def _env_bool(key: str, default: bool = False) -> bool:
|
|
return os.getenv(key, str(default)).lower() in ("true", "1", "yes")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# SYMBOL MAP
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class SymbolMap:
|
|
gold: str = _env("SYMBOL_GOLD", "XAUUSD")
|
|
nas100: str = _env("SYMBOL_NAS100", "USTEC")
|
|
us500: str = _env("SYMBOL_US500", "US500")
|
|
us30: str = _env("SYMBOL_US30", "US30")
|
|
dxy: str = _env("SYMBOL_DXY", "USDX")
|
|
btc: str = _env("SYMBOL_BTC", "BTCUSD")
|
|
eth: str = _env("SYMBOL_ETH", "ETHUSD")
|
|
enable_crypto: bool = _env_bool("ENABLE_CRYPTO", False)
|
|
|
|
def all_trading(self) -> List[str]:
|
|
symbols = [self.gold, self.nas100, self.us500, self.us30]
|
|
if self.enable_crypto:
|
|
symbols.extend([self.btc, self.eth])
|
|
return symbols
|
|
|
|
def name_for(self, symbol: str) -> str:
|
|
_map = {
|
|
self.gold: "GOLD", self.nas100: "NAS100",
|
|
self.us500: "US500", self.us30: "US30", self.dxy: "DXY",
|
|
self.btc: "BTC", self.eth: "ETH",
|
|
}
|
|
return _map.get(symbol, symbol)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# RISK CONFIG
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class RiskConfig:
|
|
max_risk_pct: float = _env_float("MAX_RISK_PER_TRADE", 1.0)
|
|
max_daily_loss_pct: float = _env_float("MAX_DAILY_LOSS", 3.0)
|
|
max_weekly_loss_pct: float = _env_float("MAX_WEEKLY_LOSS", 6.0)
|
|
max_monthly_dd_pct: float = _env_float("MAX_MONTHLY_DRAWDOWN", 10.0)
|
|
max_concurrent: int = _env_int("MAX_CONCURRENT_TRADES", 3)
|
|
consecutive_loss_pause: int = 3 # pause after N consecutive losses
|
|
pause_minutes: int = 60 # how long to pause
|
|
reduce_risk_after_loss_day: float = 0.5 # factor to reduce next day
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# ALERT CONFIG
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class AlertConfig:
|
|
desktop: bool = _env_bool("ENABLE_DESKTOP_ALERTS", True)
|
|
sound: bool = _env_bool("ENABLE_SOUND_ALERTS", True)
|
|
telegram: bool = _env_bool("ENABLE_TELEGRAM", False)
|
|
telegram_token: str = _env("TELEGRAM_BOT_TOKEN", "")
|
|
telegram_chat_id: str = _env("TELEGRAM_CHAT_ID", "")
|
|
|
|
# Confluence scoring threshold — signals below this score are suppressed
|
|
confluence_min_score: int = _env_int("CONFLUENCE_MIN_SCORE", 60)
|
|
|
|
# Discord webhook notifications
|
|
discord: bool = _env_bool("ENABLE_DISCORD", False)
|
|
discord_webhook_url: str = _env("DISCORD_WEBHOOK_URL", "")
|
|
discord_rate_limit: int = _env_int("DISCORD_RATE_LIMIT_PER_30SEC", 30)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# TIMEFRAME CONSTANTS
|
|
# ══════════════════════════════════════════════════════════════════
|
|
class TF:
|
|
"""MT5 timeframe constants (mirrors MetaTrader5 module)."""
|
|
M1 = 1
|
|
M5 = 5
|
|
M15 = 15
|
|
M30 = 30
|
|
H1 = 16385
|
|
H4 = 16388
|
|
D1 = 16408
|
|
W1 = 32769
|
|
MN1 = 49153
|
|
|
|
_NAMES = {
|
|
1: "M1", 5: "M5", 15: "M15", 30: "M30",
|
|
16385: "H1", 16388: "H4", 16408: "D1",
|
|
32769: "W1", 49153: "MN1",
|
|
}
|
|
|
|
@classmethod
|
|
def name(cls, tf: int) -> str:
|
|
return cls._NAMES.get(tf, f"TF({tf})")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# SESSION WINDOWS (UTC)
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class SessionWindow:
|
|
name: str
|
|
start_hour: int
|
|
start_minute: int = 0
|
|
end_hour: int = 0
|
|
end_minute: int = 0
|
|
|
|
SESSIONS = {
|
|
"asian": SessionWindow("Asian", 0, 0, 8, 0),
|
|
"asian_range": SessionWindow("Asian Range", 0, 0, 6, 0),
|
|
"london_open": SessionWindow("London Open", 7, 0, 9, 0),
|
|
"london": SessionWindow("London", 7, 0, 16, 0),
|
|
"ny_open": SessionWindow("NY Open", 12, 0, 14, 0),
|
|
"ny": SessionWindow("New York", 12, 0, 21, 0),
|
|
"silver_bullet_1":SessionWindow("Silver Bullet AM", 15, 0, 16, 0), # 10-11 AM ET
|
|
"silver_bullet_2":SessionWindow("Silver Bullet PM", 19, 0, 20, 0), # 2-3 PM ET
|
|
"london_ny_overlap": SessionWindow("Overlap", 12, 0, 16, 0),
|
|
}
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# STRATEGY METADATA
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class StrategyMeta:
|
|
id: str
|
|
name: str
|
|
instrument: str # "gold", "nas100", "us500", "us30"
|
|
style: str # "scalp", "day", "swing"
|
|
win_rate: tuple # (low, high) as decimals
|
|
risk_reward: tuple # (1, target)
|
|
timeframes: List[int]
|
|
sessions: List[str]
|
|
enabled: bool = True
|
|
|
|
STRATEGY_REGISTRY: Dict[str, StrategyMeta] = {
|
|
"A": StrategyMeta("A", "London Breakout Trap", "gold", "scalp", (0.60, 0.68), (1, 2), [TF.M5, TF.M15], ["london_open"]),
|
|
"B": StrategyMeta("B", "VWAP+OB Sniper", "gold", "scalp", (0.55, 0.62), (1, 2), [TF.M1, TF.M5], ["ny_open"]),
|
|
"C": StrategyMeta("C", "News Spike Fade", "gold", "scalp", (0.55, 0.60), (1, 1.5), [TF.M1, TF.M5], ["ny_open", "london"]),
|
|
"D": StrategyMeta("D", "DXY Divergence", "gold", "day", (0.62, 0.70), (1, 2), [TF.M15, TF.H1], ["london_ny_overlap"]),
|
|
"E": StrategyMeta("E", "Session Transition", "gold", "day", (0.58, 0.65), (1, 2), [TF.M15, TF.H1], ["ny_open"]),
|
|
"F": StrategyMeta("F", "Trendline Break+Retest", "gold", "day", (0.55, 0.62), (1, 2.5), [TF.M15, TF.H1], ["london", "ny"]),
|
|
"G": StrategyMeta("G", "Weekly Institutional", "gold", "swing", (0.55, 0.60), (1, 3), [TF.H4, TF.D1, TF.W1], ["london", "ny"]),
|
|
"H": StrategyMeta("H", "Fibonacci Cluster", "gold", "swing", (0.55, 0.60), (1, 4), [TF.H4, TF.D1], ["london", "ny"]),
|
|
"I": StrategyMeta("I", "ORB (Opening Range)", "nas100", "scalp", (0.58, 0.70), (1, 2), [TF.M5, TF.M15], ["ny_open"]),
|
|
"J": StrategyMeta("J", "EMA Ribbon Scalp", "nas100", "scalp", (0.55, 0.60), (1, 1.5), [TF.M1, TF.M5], ["ny_open", "london"]),
|
|
"K": StrategyMeta("K", "Power of 3 (ICT)", "nas100", "scalp", (0.55, 0.65), (1, 3), [TF.M5, TF.M15], ["ny_open"]),
|
|
"L": StrategyMeta("L", "Gap Fill", "nas100", "day", (0.65, 0.72), (1, 1.5), [TF.M15, TF.H1], ["ny_open"]),
|
|
"M": StrategyMeta("M", "20/50 EMA Pullback", "nas100", "swing", (0.55, 0.62), (1, 2.5), [TF.H4, TF.D1], ["ny"]),
|
|
"N": StrategyMeta("N", "VWAP Mean Reversion", "us500", "scalp", (0.63, 0.72), (1, 1.75), [TF.M1, TF.M5], ["ny_open"]),
|
|
"O": StrategyMeta("O", "Multi-TF Trend", "us500", "day", (0.58, 0.64), (1, 2), [TF.M5, TF.M15, TF.H1], ["london", "ny"]),
|
|
"P": StrategyMeta("P", "RSI + Structure", "us500", "swing", (0.55, 0.62), (1, 2.5), [TF.D1, TF.W1], ["ny"]),
|
|
"Q": StrategyMeta("Q", "Round Number Bounce", "us30", "scalp", (0.60, 0.66), (1, 2), [TF.M5, TF.M15], ["ny_open", "london"]),
|
|
"R": StrategyMeta("R", "ICT Silver Bullet", "us30", "day", (0.55, 0.65), (1, 3), [TF.M5, TF.M15], ["silver_bullet_1", "silver_bullet_2"]),
|
|
"S": StrategyMeta("S", "Breakout-Retest", "us30", "swing", (0.60, 0.70), (1, 3), [TF.H4, TF.D1], ["ny"]),
|
|
"T": StrategyMeta("T", "BTC Round Number Bounce","btc", "scalp", (0.60, 0.70), (1, 3), [TF.M5], []), # 24/7 trading
|
|
"U": StrategyMeta("U", "ETH Trend Following", "eth", "day", (0.58, 0.65), (1, 2.5), [TF.H4], []), # 24/7 trading
|
|
# Crypto strategies (US6)
|
|
"V": StrategyMeta("V", "BTC Momentum Breakout", "btc", "scalp", (0.58, 0.68), (1, 3), [TF.M15], []),
|
|
"W": StrategyMeta("W", "BTC RSI Divergence", "btc", "day", (0.60, 0.68), (1, 2.5), [TF.H1], []),
|
|
"X": StrategyMeta("X", "ETH Order Block Sniper", "eth", "scalp", (0.58, 0.66), (1, 2), [TF.M5], []),
|
|
"Y": StrategyMeta("Y", "ETH Multi-TF Confluence","eth", "swing", (0.55, 0.63), (1, 3), [TF.H4, TF.D1], []),
|
|
}
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# CORRELATION MATRIX — blocks simultaneous same-direction trades
|
|
# ══════════════════════════════════════════════════════════════════
|
|
CORRELATION_BLOCKS = [
|
|
({"nas100", "us500"}, 0.92),
|
|
({"us500", "us30"}, 0.95),
|
|
]
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════
|
|
# MASTER CONFIG
|
|
# ══════════════════════════════════════════════════════════════════
|
|
@dataclass
|
|
class AppConfig:
|
|
symbols: SymbolMap = field(default_factory=SymbolMap)
|
|
risk: RiskConfig = field(default_factory=RiskConfig)
|
|
alerts: AlertConfig = field(default_factory=AlertConfig)
|
|
|
|
mt5_path: str = _env("MT5_PATH", "")
|
|
db_path: str = _env("DB_PATH", "data/journal.db")
|
|
log_path: str = _env("LOG_PATH", "data/logs")
|
|
poll_interval: int = _env_int("POLL_INTERVAL_SECONDS", 2)
|
|
|
|
dash_host: str = _env("DASH_HOST", "127.0.0.1")
|
|
dash_port: int = _env_int("DASH_PORT", 8050)
|
|
dash_debug: bool = _env_bool("DASH_DEBUG", False)
|
|
|
|
|
|
# Singleton
|
|
CONFIG = AppConfig()
|
|
SYMBOLS = CONFIG.symbols
|