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.

Claude-Session: https://claude.ai/code/session_01VUnCqjWBuvKRSjLwHeB9BJ
This commit is contained in:
Harwinder Sekhon
2026-07-24 14:10:45 -04:00
parent c3329d6cd5
commit d669227ddc
7 changed files with 2245 additions and 1 deletions
+44 -1
View File
@@ -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(