Skip to content

Commit 22e5a10

Browse files
Cody (Founding Engineer)Paperclip-Paperclip
authored andcommitted
fix: guard against anomalous ask prices in backtest fill engine
Corrupted Dukascopy ask data files (raw decimal instead of pipettes) cause wildly incorrect fill pricing. Add a 5% relative-divergence guard in _compute_fill_price that falls back to bid-only pricing when ask/bid divergence exceeds the threshold. Co-Authored-By: Paperclip <[email protected]>
1 parent 8c0569e commit 22e5a10

2 files changed

Lines changed: 45 additions & 0 deletions

File tree

tests/execution/backtest/test_bid_ask_fills.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,3 +239,32 @@ async def capture(ev: PositionClosedEvent) -> None:
239239
assert ev.exit_commission_cost == pytest.approx(0.5)
240240

241241
dispatcher.unsubscribe(PositionClosedEvent, capture)
242+
243+
244+
@pytest.mark.asyncio
245+
async def test_anomalous_ask_price_falls_back_to_bid(caplog: pytest.LogCaptureFixture) -> None:
246+
"""Ask prices diverging >5% from bid are treated as corrupted data."""
247+
# bid=10250, ask=1.025 simulates corrupted Dukascopy decimal-vs-pipette data
248+
client = _client_with_bid_ask(bid_close=10250.0, ask_close=1.025)
249+
await client.start()
250+
251+
with caplog.at_level(logging.WARNING, logger="tradedesk.execution.backtest.client"):
252+
result = await client.place_market_order("INST", "BUY", size=1.0)
253+
254+
# Should fall back to bid-only pricing
255+
assert result["price"] == 10250.0
256+
assert client.trades[0].spread_cost == 0.0
257+
assert client.trades[0].raw_price == 10250.0
258+
assert any("Anomalous ask price" in r.message for r in caplog.records)
259+
260+
261+
@pytest.mark.asyncio
262+
async def test_normal_spread_not_rejected() -> None:
263+
"""Normal bid/ask spreads within 5% are used as-is."""
264+
# ~0.5% spread — well within threshold
265+
client = _client_with_bid_ask(bid_close=100.0, ask_close=100.5)
266+
await client.start()
267+
268+
result = await client.place_market_order("INST", "BUY", size=1.0)
269+
270+
assert result["price"] == 100.5 # fills at ask, not fallback

tradedesk/execution/backtest/client.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,22 @@ def _compute_fill_price(
360360
bid_price = self._get_mark_price(instrument)
361361
ask_price = self._ask_price.get(instrument)
362362

363+
# Guard: reject anomalous ask prices (e.g. corrupted Dukascopy data
364+
# where prices are in raw decimal instead of pipettes).
365+
if ask_price is not None and bid_price != 0:
366+
divergence = abs(ask_price - bid_price) / abs(bid_price)
367+
if divergence > 0.05:
368+
log.warning(
369+
"Anomalous ask price for %s at %s: bid=%.6f ask=%.6f "
370+
"(divergence=%.2f%%); falling back to bid-only pricing",
371+
instrument,
372+
self._current_timestamp,
373+
bid_price,
374+
ask_price,
375+
divergence * 100,
376+
)
377+
ask_price = None
378+
363379
# Determine executable side
364380
if ask_price is not None:
365381
# Bid/ask pricing available

0 commit comments

Comments
 (0)