Remove Oanda auto-execution daemon/executor (not auto-trading)

Deletes oanda_daemon.py, oanda_executor.py, their 28 tests and the daemon-only
config keys (oanda.lot_size/quality_gate/max_hold_bars). The Oanda data
provider and credentials stay — it is the live candle feed. 402 tests.

Claude-Session: https://claude.ai/code/session_01FC4s6VCURFte4dub4xKxYL
This commit is contained in:
Harwinder Sekhon
2026-09-03 21:00:21 -04:00
parent 9a8a2c1aa1
commit 354778683e
8 changed files with 24 additions and 1980 deletions
+9 -852
View File
@@ -3464,14 +3464,12 @@ def _():
# ══════════════════════════════════════════════════════════════════════
# MODULE 15: Oanda provider / executor / daemon
# MODULE 15: Oanda provider
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 15: Oanda provider / executor / daemon ═══")
print("\n═══ Module 15: Oanda provider ═══")
import providers.oanda
import oanda_executor
import oanda_daemon
@test("providers.oanda: OandaDataProvider parses Oanda candle JSON into correct DataFrame")
@@ -3647,568 +3645,20 @@ def _():
assert spread == 0.30, f"Expected default spread 0.30, got {spread}"
@test("oanda_executor: market order body construction")
def _():
executor = oanda_executor.OandaExecutor.__new__(
oanda_executor.OandaExecutor)
executor._account_id = "test-account"
executor._base_url = "https://test"
executor._lot_size = 1
executor._price_precision = 2
executor._session = None
captured = {}
def mock_request(self, method, url, **kwargs):
captured["method"] = method
captured["url"] = url
captured["json"] = kwargs.get("json")
return type("R", (), {
"status_code": 201,
"json": lambda self: {
"orderFillTransaction": {
"price": "2400.50",
"tradeOpened": {"tradeID": "123"},
}
},
})()
executor._session = type("S", (), {
"request": mock_request,
"headers": {},
})()
result = executor.place_market_order(
instrument="XAU_USD",
direction="BUY",
stop_loss=2392.50,
take_profit=2420.00,
)
body = captured["json"]
order = body["order"]
assert order["type"] == "MARKET"
assert order["instrument"] == "XAU_USD"
assert order["units"] == "1"
# XAU price precision is 2 — Oanda rejects excess decimals.
assert order["stopLossOnFill"]["price"] == "2392.50"
assert order["takeProfitOnFill"]["price"] == "2420.00"
fill = result.get("orderFillTransaction")
assert fill is not None
assert fill["tradeOpened"]["tradeID"] == "123"
@test("oanda_executor: SELL order uses negative units")
def _():
executor = oanda_executor.OandaExecutor.__new__(
oanda_executor.OandaExecutor)
executor._account_id = "test-account"
executor._base_url = "https://test"
executor._lot_size = 1
executor._price_precision = 2
captured = {}
def mock_request(self, method, url, **kwargs):
captured["json"] = kwargs.get("json")
return type("R", (), {
"status_code": 201,
"json": lambda self: {
"orderFillTransaction": {
"price": "2400.50",
"tradeOpened": {"tradeID": "456"},
}
},
})()
executor._session = type("S", (), {
"request": mock_request,
"headers": {},
})()
executor.place_market_order(
instrument="XAU_USD", direction="SELL", stop_loss=2410.00)
order = captured["json"]["order"]
assert order["units"] == "-1", f"SELL should be -1, got {order['units']}"
assert "takeProfitOnFill" not in order, "No TP should be set when None"
@test("oanda_executor: 4xx error raises immediately (no retry)")
def _():
executor = oanda_executor.OandaExecutor.__new__(
oanda_executor.OandaExecutor)
executor._account_id = "test"
executor._base_url = "https://test"
executor._lot_size = 1
call_count = {"n": 0}
def mock_request(self, method, url, **kwargs):
call_count["n"] += 1
return type("R", (), {
"status_code": 400,
"text": "INVALID_ORDER",
})()
executor._session = type("S", (), {
"request": mock_request,
"headers": {},
})()
try:
executor._request_with_retry("POST", "https://test/orders")
assert False, "Should have raised OandaAPIError"
except oanda_executor.OandaAPIError as e:
assert e.status_code == 400
assert call_count["n"] == 1, "4xx should not retry"
@test("oanda_daemon: trailing stop ratchet matches backtester logic")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
info_buy = {
"direction": "BUY",
"entry": 2400.0,
"sl_initial": 2384.0,
"current_sl": 2384.0,
"risk": 16.0,
"peak": 2400.0,
"trail_armed": False,
"trail_arm_r": 0.6,
"trail_dist_r": 0.75,
}
# Bar 1: price moves up but not enough to arm (< 0.6R = $9.60)
new_sl = daemon._compute_trail(info_buy, bar_high=2408.0, bar_low=2399.0)
assert new_sl is None, "Should not arm yet (excursion 0.5R)"
assert info_buy["peak"] == 2408.0
# Bar 2: excursion reaches 0.625R = $10 — arms, trail = 2408 - 0.75*16 = 2396
new_sl = daemon._compute_trail(info_buy, bar_high=2410.0, bar_low=2403.0)
assert new_sl is not None, "Should arm at 0.625R"
assert abs(new_sl - 2398.0) < 0.01, f"Expected trail ~2398, got {new_sl}"
assert info_buy["trail_armed"] is True
# Bar 3: price goes higher, trail ratchets up
info_buy["current_sl"] = new_sl
new_sl2 = daemon._compute_trail(info_buy, bar_high=2420.0, bar_low=2410.0)
assert new_sl2 is not None
assert new_sl2 > new_sl, "Trail must ratchet up"
expected = 2420.0 - 0.75 * 16.0 # 2408.0
assert abs(new_sl2 - expected) < 0.01, f"Expected {expected}, got {new_sl2}"
# Bar 4: price pulls back — trail must NOT ratchet down
info_buy["current_sl"] = new_sl2
new_sl3 = daemon._compute_trail(info_buy, bar_high=2412.0, bar_low=2405.0)
assert new_sl3 is None, "Trail must not ratchet backwards"
@test("oanda_daemon: trailing stop for SELL direction")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
info_sell = {
"direction": "SELL",
"entry": 2400.0,
"sl_initial": 2416.0,
"current_sl": 2416.0,
"risk": 16.0,
"peak": 2400.0,
"trail_armed": False,
"trail_arm_r": 0.6,
"trail_dist_r": 0.75,
}
# Not enough excursion
new_sl = daemon._compute_trail(info_sell, bar_high=2401.0, bar_low=2393.0)
assert new_sl is None, "Should not arm yet"
assert info_sell["peak"] == 2393.0
# Arms: peak at 2390, excursion = 10/16 = 0.625R
new_sl = daemon._compute_trail(info_sell, bar_high=2395.0, bar_low=2390.0)
assert new_sl is not None
expected = 2390.0 + 0.75 * 16.0 # 2402.0
assert abs(new_sl - expected) < 0.01, f"Expected {expected}, got {new_sl}"
assert info_sell["trail_armed"] is True
# Ratchets down
info_sell["current_sl"] = new_sl
new_sl2 = daemon._compute_trail(info_sell, bar_high=2392.0, bar_low=2380.0)
assert new_sl2 is not None
assert new_sl2 < new_sl, "SELL trail must ratchet down"
@test("oanda_daemon: _should_execute respects quality gate")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._quality_gate = "B"
daemon._state = {"trades": {}, "last_entries": {}}
daemon._state_lock = threading.Lock()
from signal_aggregator import TradeSetup
setup = TradeSetup(
pair="XAU/USD", display_name="XAU/USD", direction="BUY",
entry_price=2400.0, stop_loss=2384.0,
take_profit_1=2416.0, take_profit_2=2432.0,
risk_reward=1.0, risk_pips=160.0, reward_pips=160.0,
pip_value=0.10)
report_c = SignalReport(
symbol="XAU/USD", display_name="XAU/USD", timestamp="2026-07-20")
report_c.final_signal = SignalStrength.BUY
report_c.signal_quality = "C"
report_c.trade_setup = setup
assert not daemon._should_execute(report_c, Timeframe.H1), \
"C should be blocked by gate=B"
report_a = SignalReport(
symbol="XAU/USD", display_name="XAU/USD", timestamp="2026-07-20")
report_a.final_signal = SignalStrength.BUY
report_a.signal_quality = "A"
report_a.trade_setup = setup
assert daemon._should_execute(report_a, Timeframe.H1), \
"A should pass gate=B"
@test("oanda_daemon: _should_execute blocks when position exists")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._quality_gate = "C"
daemon._state = {"trades": {"12345": {"direction": "BUY"}},
"last_entries": {}}
daemon._state_lock = threading.Lock()
from signal_aggregator import TradeSetup
setup = TradeSetup(
pair="XAU/USD", display_name="XAU/USD", direction="BUY",
entry_price=2400.0, stop_loss=2384.0,
take_profit_1=2416.0, take_profit_2=2432.0,
risk_reward=1.0, risk_pips=160.0, reward_pips=160.0,
pip_value=0.10)
report = SignalReport(
symbol="XAU/USD", display_name="XAU/USD", timestamp="2026-07-20")
report.final_signal = SignalStrength.BUY
report.signal_quality = "A"
report.trade_setup = setup
assert not daemon._should_execute(report, Timeframe.H1), \
"Should block when a position already exists"
@test("oanda_daemon: NEUTRAL signal is never executed")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._quality_gate = "C"
daemon._state = {"trades": {}, "last_entries": {}}
daemon._state_lock = threading.Lock()
report = SignalReport(
symbol="XAU/USD", display_name="XAU/USD", timestamp="2026-07-20")
report.final_signal = SignalStrength.NEUTRAL
report.signal_quality = "F"
report.trade_setup = None
assert not daemon._should_execute(report, Timeframe.H1)
def _make_daemon_report(quality="A", direction="BUY"):
from signal_aggregator import TradeSetup
setup = TradeSetup(
pair="XAU/USD", display_name="XAU/USD", direction=direction,
entry_price=2400.0, stop_loss=2384.0,
take_profit_1=2416.0, take_profit_2=2432.0,
risk_reward=1.0, risk_pips=160.0, reward_pips=160.0,
pip_value=0.10, trail_arm_r=0.6, trail_dist_r=0.85)
report = SignalReport(
symbol="XAU/USD", display_name="XAU/USD",
timestamp="2026-07-20 10:00:00+00:00")
report.final_signal = (SignalStrength.BUY if direction == "BUY"
else SignalStrength.SELL)
report.signal_quality = quality
report.trade_setup = setup
return report
@test("oanda_daemon: 5-bar entry cooldown blocks re-entry (backtest parity)")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._quality_gate = "C"
daemon._state = {"trades": {}, "last_entries": {}}
daemon._state_lock = threading.Lock()
report = _make_daemon_report()
# Entry 2 minutes ago on M5 → inside 5×5min cooldown → blocked
recent = (datetime.now(timezone.utc) - timedelta(minutes=2)).isoformat()
daemon._state["last_entries"]["5min"] = recent
assert not daemon._should_execute(report, Timeframe.M5), \
"Entry 2min ago must block M5 (cooldown 25min)"
# Entry 30 minutes ago on M5 → past 5×5min cooldown → allowed
old = (datetime.now(timezone.utc) - timedelta(minutes=30)).isoformat()
daemon._state["last_entries"]["5min"] = old
assert daemon._should_execute(report, Timeframe.M5), \
"Entry 30min ago must clear the 25min M5 cooldown"
# M5 cooldown must not block H1
daemon._state["last_entries"] = {"5min": recent}
assert daemon._should_execute(report, Timeframe.H1), \
"Cooldown is per-timeframe"
@test("oanda_daemon: orders carry NO take-profit (trailing book parity)")
@test("app_config: unknown oanda key raises AppConfigError")
def _():
import tempfile
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._state = {"trades": {}, "last_entries": {}}
daemon._state_lock = threading.Lock()
daemon._entry_lock = threading.Lock()
captured = {}
class MockExecutor:
def place_market_order(self, instrument, direction, stop_loss,
take_profit=None, units=None):
captured["take_profit"] = take_profit
captured["stop_loss"] = stop_loss
return {
"orderFillTransaction": {
"price": "2400.60",
"tradeOpened": {"tradeID": "777"},
}
}
daemon._executor = MockExecutor()
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
tmp_path = f.name
orig_state_file = oanda_daemon.STATE_FILE
try:
oanda_daemon.STATE_FILE = tmp_path
daemon._execute_trade(_make_daemon_report(), Timeframe.M15)
finally:
oanda_daemon.STATE_FILE = orig_state_file
os.unlink(tmp_path)
assert captured.get("take_profit") is None, \
"Backtested trailing model has no hard TP — order must not carry one"
assert captured["stop_loss"] == 2384.0
trade = daemon._state["trades"]["777"]
assert trade["entry"] == 2400.60, "Risk must be computed from actual fill"
assert abs(trade["risk"] - 16.60) < 0.01
assert trade["trail_arm_r"] == 0.6 and trade["trail_dist_r"] == 0.85, \
"Trail params must come from the setup stamp (per-TF factory values)"
assert daemon._state["last_entries"]["15min"], \
"Entry must be recorded for the cooldown"
@test("oanda_daemon: entry-lock recheck prevents double entry from concurrent scans")
def _():
import tempfile
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
# A concurrent TF loop already opened a trade after our _should_execute
daemon._state = {"trades": {"111": {"direction": "BUY"}},
"last_entries": {}}
daemon._state_lock = threading.Lock()
daemon._entry_lock = threading.Lock()
placed = {"n": 0}
class MockExecutor:
def place_market_order(self, *a, **k):
placed["n"] += 1
return {}
daemon._executor = MockExecutor()
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
tmp_path = f.name
orig_state_file = oanda_daemon.STATE_FILE
try:
oanda_daemon.STATE_FILE = tmp_path
daemon._execute_trade(_make_daemon_report(), Timeframe.M5)
finally:
oanda_daemon.STATE_FILE = orig_state_file
os.unlink(tmp_path)
assert placed["n"] == 0, \
"Recheck under the entry lock must skip when a position exists"
@test("oanda_daemon: same closed bar is never processed twice")
def _():
import tempfile
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._max_hold_bars = 300
daemon._price_precision = 2
daemon._state_lock = threading.Lock()
bar_time = pd.Timestamp("2026-07-20 10:00:00", tz="UTC")
df = pd.DataFrame(
{"Open": [2400.0], "High": [2405.0], "Low": [2398.0],
"Close": [2403.0], "Volume": [100.0]},
index=pd.DatetimeIndex([bar_time], name="Datetime"))
class MockProvider:
def fetch_ohlcv(self, symbol, tf):
return df
daemon._provider = MockProvider()
daemon._executor = None # trail must not arm on this bar
daemon._state = {
"trades": {
"555": {
"tf": "5min", "direction": "BUY", "entry": 2400.0,
"sl_initial": 2384.0, "current_sl": 2384.0, "risk": 16.0,
"peak": 2400.0, "trail_armed": False,
"trail_arm_r": 0.6, "trail_dist_r": 0.85,
"bars_held": 0, "last_bar_ts": None, "signal_bar_ts": None,
}
},
"last_entries": {},
}
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
tmp_path = f.name
orig_state_file = oanda_daemon.STATE_FILE
try:
oanda_daemon.STATE_FILE = tmp_path
daemon._manage_trailing(Timeframe.M5)
daemon._manage_trailing(Timeframe.M5) # same bar re-served (weekend/lag)
daemon._manage_trailing(Timeframe.M5)
finally:
oanda_daemon.STATE_FILE = orig_state_file
os.unlink(tmp_path)
assert daemon._state["trades"]["555"]["bars_held"] == 1, \
"A re-served bar must not inflate bars_held (weekend timeout bug)"
@test("oanda_daemon: failed timeout-close keeps the trade tracked")
def _():
import tempfile
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._max_hold_bars = 1
daemon._price_precision = 2
daemon._state_lock = threading.Lock()
bar_time = pd.Timestamp("2026-07-20 10:00:00", tz="UTC")
df = pd.DataFrame(
{"Open": [2400.0], "High": [2405.0], "Low": [2398.0],
"Close": [2403.0], "Volume": [100.0]},
index=pd.DatetimeIndex([bar_time], name="Datetime"))
class MockProvider:
def fetch_ohlcv(self, symbol, tf):
return df
class MockExecutor:
def close_trade(self, trade_id):
raise oanda_executor.OandaAPIError("market halted", 503)
daemon._provider = MockProvider()
daemon._executor = MockExecutor()
daemon._state = {
"trades": {
"666": {
"tf": "5min", "direction": "BUY", "entry": 2400.0,
"sl_initial": 2384.0, "current_sl": 2384.0, "risk": 16.0,
"peak": 2400.0, "trail_armed": False,
"trail_arm_r": 0.6, "trail_dist_r": 0.85,
"bars_held": 0, "last_bar_ts": None, "signal_bar_ts": None,
}
},
"last_entries": {},
}
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
tmp_path = f.name
orig_state_file = oanda_daemon.STATE_FILE
try:
oanda_daemon.STATE_FILE = tmp_path
daemon._manage_trailing(Timeframe.M5)
finally:
oanda_daemon.STATE_FILE = orig_state_file
os.unlink(tmp_path)
assert "666" in daemon._state["trades"], \
"A failed broker close must keep the trade tracked for retry"
@test("app_config: invalid oanda section values raise AppConfigError")
def _():
import tempfile
for bad in (
{"oanda": {"quality_gate": "Z"}},
{"oanda": {"lot_size": 0}},
{"oanda": {"max_hold_bars": -5}},
{"oanda": {"unknown_key": 1}},
):
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False) as f:
json.dump(bad, f)
tmp_path = f.name
try:
app_config.load_app_config(tmp_path)
assert False, f"Should have raised for {bad}"
except app_config.AppConfigError:
pass
finally:
os.unlink(tmp_path)
@test("oanda_daemon: boundary timing matches telegram_bot")
def _():
from telegram_bot import _seconds_until_boundary as tb_boundary
from oanda_daemon import _seconds_until_boundary as od_boundary
for interval in (5, 15, 60, 240):
tb_val = tb_boundary(interval, buffer=10)
od_val = od_boundary(interval, buffer=10)
assert abs(tb_val - od_val) < 0.01, \
f"Boundary timing mismatch for {interval}min: {tb_val} vs {od_val}"
@test("oanda_daemon: state file round-trip")
def _():
import tempfile
state = {
"trades": {
"999": {
"tf": "M15",
"direction": "BUY",
"entry": 2400.0,
"sl_initial": 2384.0,
"current_sl": 2392.0,
"risk": 16.0,
"peak": 2412.0,
"trail_armed": True,
"trail_arm_r": 0.6,
"trail_dist_r": 0.75,
"bars_held": 25,
}
}
}
bad = {"oanda": {"unknown_key": 1}}
with tempfile.NamedTemporaryFile(
mode="w", suffix=".json", delete=False) as f:
json.dump(bad, f)
tmp_path = f.name
orig_state_file = oanda_daemon.STATE_FILE
try:
oanda_daemon.STATE_FILE = tmp_path
oanda_daemon._save_state(state)
loaded = oanda_daemon._load_state()
assert loaded["trades"]["999"]["entry"] == 2400.0
assert loaded["trades"]["999"]["trail_armed"] is True
assert loaded["trades"]["999"]["bars_held"] == 25
app_config.load_app_config(tmp_path)
assert False, f"Should have raised for {bad}"
except app_config.AppConfigError:
pass
finally:
oanda_daemon.STATE_FILE = orig_state_file
os.unlink(tmp_path)
@@ -4301,78 +3751,6 @@ def _():
f"Retry failure must keep the original 250-bar df, got {len(result)}"
@test("oanda_executor: non-JSON response raises OandaAPIError with status")
def _():
def _bad_json(self):
raise ValueError("Expecting value: line 1 column 1 (char 0)")
mock_resp = type("MockResp", (), {
"status_code": 200,
"text": "<html>gateway error</html>",
"json": _bad_json,
})()
executor = oanda_executor.OandaExecutor.__new__(
oanda_executor.OandaExecutor)
executor._account_id = "test-account"
executor._base_url = "https://test"
executor._session = type("S", (), {
"request": lambda self, method, url, **kwargs: mock_resp,
"headers": {},
})()
try:
executor.get_open_trades()
assert False, "Should have raised OandaAPIError"
except oanda_executor.OandaAPIError as e:
assert e.status_code == 200, f"Expected status 200, got {e.status_code}"
assert "non-JSON" in str(e), f"Expected non-JSON in message: {e}"
@test("oanda_daemon: boot fails fast on rejected credentials (4xx)")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
class MockExecutor401:
def get_account_summary(self):
raise oanda_executor.OandaAPIError("unauthorized", 401)
daemon._executor = MockExecutor401()
try:
daemon._verify_account_access()
assert False, "A 4xx credential rejection must raise SystemExit"
except SystemExit:
pass
class MockExecutorConnFail:
def get_account_summary(self):
raise oanda_executor.OandaAPIError("conn refused", 0)
daemon._executor = MockExecutorConnFail()
daemon._verify_account_access() # transient — must NOT raise
@test("oanda_daemon: heartbeat never raises and logs on account failure")
def _():
daemon = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
daemon._state = {"trades": {}, "last_entries": {}}
daemon._state_lock = threading.Lock()
class MockExecutorDown:
def get_account_summary(self):
raise oanda_executor.OandaAPIError("down", 503)
daemon._executor = MockExecutorDown()
daemon._heartbeat_once() # must not raise
class MockExecutorUp:
def get_account_summary(self):
return {"balance": "1000"}
daemon._executor = MockExecutorUp()
daemon._heartbeat_once() # must not raise
# ══════════════════════════════════════════════════════════════════════
# MODULE 16: Provider abstraction (providers/ package)
# ══════════════════════════════════════════════════════════════════════
@@ -5827,227 +5205,6 @@ def _():
# ══════════════════════════════════════════════════════════════════════
print("\n═══ Module 21: 2026-08-04 audit fixes — execution lifecycle ═══")
import threading as _thr21
def _m21_daemon(state=None, quality_gate="C", max_hold=300):
d = oanda_daemon.TradingDaemon.__new__(oanda_daemon.TradingDaemon)
d._state = state if state is not None else {"trades": {}, "last_entries": {}}
d._state_lock = _thr21.Lock()
d._entry_lock = _thr21.Lock()
d._quality_gate = quality_gate
d._max_hold_bars = max_hold
d._price_precision = 2
d._running = True
return d
def _m21_bar(ts="2026-08-04 10:00:00+00:00", high=2410.0, low=2395.0):
idx = pd.DatetimeIndex([pd.Timestamp(ts)])
return pd.DataFrame({"Open": [2400.0], "High": [high], "Low": [low],
"Close": [2405.0], "Volume": [1.0]}, index=idx)
def _m21_trade(**over):
base = {"tf": "5min", "direction": "BUY", "entry": 2400.0,
"sl_initial": 2384.0, "current_sl": 2384.0, "risk": 16.0,
"peak": 2400.0, "trail_armed": False, "trail_arm_r": 0.6,
"trail_dist_r": 0.85, "bars_held": 0,
"signal_bar_ts": None, "last_bar_ts": None}
base.update(over)
return base
@test("executor: close_trade raises when the close order did not fill")
def _():
ex = oanda_executor.OandaExecutor.__new__(oanda_executor.OandaExecutor)
ex._account_id, ex._base_url, ex._price_precision = "acc", "http://x", 2
class _Resp:
status_code = 200
text = "{}"
def json(self):
return {"orderCreateTransaction": {"id": "1"},
"orderCancelTransaction": {"reason": "MARKET_HALTED"}}
ex._request_with_retry = lambda *a, **k: _Resp()
try:
ex.close_trade("T1")
assert False, "close_trade must raise on a no-fill close response"
except oanda_executor.OandaAPIError as e:
assert "MARKET_HALTED" in str(e)
@test("daemon: timeout close that did not fill keeps the trade tracked")
def _():
d = _m21_daemon({"trades": {"T1": _m21_trade(bars_held=299)},
"last_entries": {}}, max_hold=300)
class _Exec:
def close_trade(self, tid):
raise oanda_executor.OandaAPIError(
"Close of trade T1 did not fill: MARKET_HALTED", 200, "")
class _Prov:
def fetch_ohlcv(self, sym, tf):
return _m21_bar()
d._executor, d._provider = _Exec(), _Prov()
d._manage_trailing(Timeframe.M5)
assert "T1" in d._state["trades"], \
"un-filled close must NOT pop the trade from state"
@test("daemon: fill without tradeOpened triggers reconciliation, not silence")
def _():
d = _m21_daemon()
recon = []
d._reconcile_positions = lambda reason: recon.append(reason)
class _Exec:
def place_market_order(self, **k):
return {"orderFillTransaction": {
"id": "5001", "price": "2400.5",
"tradesClosed": [{"tradeID": "OLD1"}]}}
d._executor = _Exec()
ts = TradeSetup(pair="XAU_USD", display_name="XAU/USD",
direction="SELL", entry_price=2400.0, stop_loss=2416.0,
take_profit_1=2384.0, take_profit_2=2376.0,
risk_reward=1.0, risk_pips=160.0, reward_pips=160.0,
pip_value=0.1)
rep = SignalReport(symbol="XAU_USD", display_name="XAU/USD",
timestamp="2026-08-04 09:55:00+00:00")
rep.trade_setup, rep.signal_quality = ts, "B"
rep.final_signal = SignalStrength.SELL
d._execute_trade(rep, Timeframe.M5)
assert recon, "unexpected fill shape must trigger _reconcile_positions"
assert not d._state["trades"]
@test("daemon: placement OandaAPIError triggers reconciliation")
def _():
d = _m21_daemon()
recon = []
d._reconcile_positions = lambda reason: recon.append(reason)
class _Exec:
def place_market_order(self, **k):
raise oanda_executor.OandaAPIError("timeout after retries")
d._executor = _Exec()
ts = TradeSetup(pair="XAU_USD", display_name="XAU/USD",
direction="BUY", entry_price=2400.0, stop_loss=2384.0,
take_profit_1=2416.0, take_profit_2=2432.0,
risk_reward=1.0, risk_pips=160.0, reward_pips=160.0,
pip_value=0.1)
rep = SignalReport(symbol="XAU_USD", display_name="XAU/USD",
timestamp="2026-08-04 09:55:00+00:00")
rep.trade_setup, rep.signal_quality = ts, "B"
rep.final_signal = SignalStrength.BUY
d._execute_trade(rep, Timeframe.M5)
assert recon, "placement error must trigger _reconcile_positions " \
"(server may have filled before the error)"
@test("daemon: reconcile adopts naked position with risk=0 and no fake SL")
def _():
d = _m21_daemon()
class _Exec:
def get_open_trades(self, instrument=None):
return [{"id": "N1", "instrument": "XAU_USD",
"currentUnits": "1", "price": "2400.0"}] # no stopLossOrder
d._executor = _Exec()
d._reconcile_positions("test")
info = d._state["trades"]["N1"]
assert info["risk"] == 0.0, "risk must be 0 (undefined), not entry-diff"
assert d._compute_trail(dict(info), 2500.0, 2400.0) is None, \
"risk=0 must never arm the trail (no invented stop)"
@test("daemon: trade popped mid-trailing is NOT resurrected by write-back")
def _():
d = _m21_daemon({"trades": {"R1": _m21_trade()}, "last_entries": {}})
class _Exec:
def get_open_trades(self, instrument=None):
return []
def modify_trade_sl(self, tid, sl):
return {}
d._executor = _Exec()
class _Prov:
def fetch_ohlcv(self, sym, tf):
d._check_closed_trades() # interleave: broker closed R1
assert "R1" not in d._state["trades"]
return _m21_bar()
d._provider = _Prov()
d._manage_trailing(Timeframe.M5)
assert "R1" not in d._state["trades"], \
"write-back must not re-insert a trade closed by the broker"
@test("daemon: trailing snapshot no longer aliases live state dicts")
def _():
d = _m21_daemon({"trades": {"S1": _m21_trade()}, "last_entries": {}})
with d._state_lock:
snap = {tid: dict(info)
for tid, info in d._state.get("trades", {}).items()}
assert snap["S1"] is not d._state["trades"]["S1"]
@test("daemon: unknown quality gate refuses to start (fail closed)")
def _():
real_init = oanda_daemon.TradingDaemon.__init__
# Drive just the validation logic the ctor now applies.
gate = "Bb"
assert gate not in oanda_daemon.QUALITY_RANK
d = _m21_daemon(quality_gate="Bb")
# _should_execute is unreachable in prod with a bad gate (ctor exits);
# the ctor check is the contract:
src = open(os.path.join(os.path.dirname(__file__),
"oanda_daemon.py"), encoding="utf-8").read()
assert "refusing to start" in src and "not in QUALITY_RANK" in src
@test("executor: market orders request positionFill=OPEN_ONLY")
def _():
ex = oanda_executor.OandaExecutor.__new__(oanda_executor.OandaExecutor)
ex._account_id, ex._base_url, ex._price_precision = "acc", "http://x", 2
ex._lot_size = 1
sent = {}
class _Resp:
status_code = 200
text = "{}"
def json(self):
return {"orderFillTransaction": {
"price": "2400.0", "tradeOpened": {"tradeID": "T9"}}}
def _req(method, url, retries=None, **kw):
sent.update(kw.get("json", {}))
return _Resp()
ex._request_with_retry = _req
ex.place_market_order("XAU_USD", "BUY", stop_loss=2384.0)
assert sent["order"]["positionFill"] == "OPEN_ONLY"
@test("daemon: _oanda_setting_int rejects a non-integer env value cleanly")
def _():
os.environ["OANDA_LOT_SIZE"] = "one"
try:
oanda_daemon._oanda_setting_int("OANDA_LOT_SIZE", "lot_size", 1)
assert False, "must SystemExit on a non-integer env value"
except SystemExit as e:
assert "not an integer" in str(e)
finally:
os.environ.pop("OANDA_LOT_SIZE", None)
@test("GPU parity: ALL indicators + composite identical on every prefix")
def _():