Oanda demo daemon: auto-execution on practice account (XAU/USD)
New independent trading daemon (oanda_daemon.py) that places orders on an Oanda v20 practice account instead of sending Telegram alerts. Oanda-only market data (oanda_provider.py, mid candles + real bid/ask spread) through the unchanged signal pipeline; order execution and trade management via oanda_executor.py (raw requests, no new deps). Book parity with the backtester: no broker-side TP (exits = SL/TRAIL/ TIMEOUT only), bar-close-only trail ratchet with each closed bar processed exactly once, 5-bar per-TF entry cooldown, structural SL from the setup. Accepted divergence: one position total (skipped signals logged for book reconciliation). Safety/ops: fail-fast boot on rejected credentials (transient outages continue), market orders never retried (double-fill risk), entry lock against aligned-boundary races, restart recovery + adoption via oanda_state.json, hourly heartbeat, asyncio exception handler, every failure path logged. Prices formatted at instrument precision. Config: env OANDA_* -> config.json "oanda" section (validated in app_config). Tests: Module 15 (30 tests), suite 240 green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VUnCqjWBuvKRSjLwHeB9BJ
This commit is contained in:
+44
-1
@@ -85,7 +85,11 @@ _VALID_LOG_LEVELS = ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL")
|
||||
# AUTOSCAN_CONFIRMATIONS entries in telegram_bot.py).
|
||||
_SCANNABLE_TFS = ("M5", "M15", "H1", "H4")
|
||||
|
||||
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level"}
|
||||
_KNOWN_TOP = {"telegram", "data_providers", "scanning", "pairs", "log_level", "oanda"}
|
||||
_KNOWN_OANDA = {
|
||||
"account_id", "api_token", "base_url", "lot_size",
|
||||
"quality_gate", "max_hold_bars",
|
||||
}
|
||||
_KNOWN_TELEGRAM = {"token", "chat_id", "channel_id", "alert_min_quality"}
|
||||
_KNOWN_SCANNING = {"pairs", "timeframes"}
|
||||
_KNOWN_PAIR_KEYS = {"blocked_hours_utc"}
|
||||
@@ -107,6 +111,7 @@ class AppConfig:
|
||||
scan_timeframes: List[Timeframe] = field(default_factory=lambda: [
|
||||
Timeframe.M5, Timeframe.M15, Timeframe.H1, Timeframe.H4])
|
||||
pair_overrides: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
||||
oanda_settings: Dict[str, Any] = field(default_factory=dict)
|
||||
log_level: str = "INFO"
|
||||
|
||||
|
||||
@@ -212,6 +217,24 @@ def load_app_config(path: str = CONFIG_PATH) -> AppConfig:
|
||||
"be a list of integers in 0-23")
|
||||
app.pair_overrides = {s: dict(o) for s, o in pair_section.items()}
|
||||
|
||||
oanda = raw.get("oanda", {})
|
||||
_check_keys(oanda, _KNOWN_OANDA, '"oanda"')
|
||||
if "lot_size" in oanda:
|
||||
ls = oanda["lot_size"]
|
||||
if not isinstance(ls, int) or isinstance(ls, bool) or ls < 1:
|
||||
raise AppConfigError(
|
||||
"config.json: oanda.lot_size must be a positive integer")
|
||||
if "quality_gate" in oanda and oanda["quality_gate"] not in _VALID_QUALITIES:
|
||||
raise AppConfigError(
|
||||
f'config.json: oanda.quality_gate "{oanda["quality_gate"]}" '
|
||||
f"not in {_VALID_QUALITIES}")
|
||||
if "max_hold_bars" in oanda:
|
||||
mhb = oanda["max_hold_bars"]
|
||||
if not isinstance(mhb, int) or isinstance(mhb, bool) or mhb < 1:
|
||||
raise AppConfigError(
|
||||
"config.json: oanda.max_hold_bars must be a positive integer")
|
||||
app.oanda_settings = dict(oanda)
|
||||
|
||||
level = raw.get("log_level", app.log_level)
|
||||
if level not in _VALID_LOG_LEVELS:
|
||||
raise AppConfigError(
|
||||
@@ -271,6 +294,26 @@ def provider_api_key(app: Optional[AppConfig] = None) -> str:
|
||||
return str(block.get("api_key", ""))
|
||||
|
||||
|
||||
# ── Oanda resolution helpers (env → config.json → default) ───────────
|
||||
|
||||
def oanda_account_id(app: Optional[AppConfig] = None) -> str:
|
||||
app = app or get_app_config()
|
||||
return os.environ.get("OANDA_ACCOUNT_ID") or str(
|
||||
app.oanda_settings.get("account_id", ""))
|
||||
|
||||
|
||||
def oanda_api_token(app: Optional[AppConfig] = None) -> str:
|
||||
app = app or get_app_config()
|
||||
return os.environ.get("OANDA_API_TOKEN") or str(
|
||||
app.oanda_settings.get("api_token", ""))
|
||||
|
||||
|
||||
def oanda_base_url(app: Optional[AppConfig] = None) -> str:
|
||||
app = app or get_app_config()
|
||||
return os.environ.get("OANDA_BASE_URL") or str(
|
||||
app.oanda_settings.get("base_url", "https://api-fxpractice.oanda.com"))
|
||||
|
||||
|
||||
# ── PlatformConfig overlay ────────────────────────────────────────────
|
||||
|
||||
def apply_pair_overrides(
|
||||
|
||||
Reference in New Issue
Block a user