Two faults, both introduced by the tick feed, both visible as a huge bar that flattened the price scale. A LEVEL_ONE_FUTURES update arrived carrying LAST_PRICE: 0. The parser rejected None, but 0 is not None, so a minute opened at zero — o=0.0 h=7777.25 l=0.0 — and provisional_higher carried that low into 5m, 15m, 30m, 1h and the daily bar. Non-positive prices are treated as absent now, so the last real price carries forward and the update still counts as the trade it is. Separately, store.put replaced a bar only when it matched the tail. That was sufficient while one closed bar arrived per minute, but ticks open the next minute before CHART_FUTURES delivers the previous one, so the exchange's own bar stopped matching the tail and was silently dropped — leaving the tick-built approximation, with its partial volume, in place permanently. put now searches back a bounded number of buckets for the one it belongs to, and refuses to let a provisional bar overwrite a settled one. Tests cover all three invariants: a zero price parses as a trade with no price, a late closed bar replaces its bucket and keeps the exchange's volume, and a tick cannot overwrite a settled bar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50 lines
1.9 KiB
Python
50 lines
1.9 KiB
Python
from app.bars.models import Bar, Timeframe
|
|
from app.bars.store import InMemoryBarStore
|
|
|
|
|
|
def bar(t, close=1):
|
|
return Bar(Timeframe.M1, t, 1, 2, 0, close, 10, False, "ES=F", "replay")
|
|
|
|
|
|
def test_store_replaces_forming_bar_and_bounds_history():
|
|
store = InMemoryBarStore(2)
|
|
store.put(bar(60))
|
|
store.put(bar(60, 2))
|
|
store.put(bar(120))
|
|
store.put(bar(180))
|
|
|
|
assert [value.t for value in store.get(Timeframe.M1)] == [120, 180]
|
|
assert store.get(Timeframe.M1, 1)[0].t == 180
|
|
|
|
|
|
def test_a_closed_bar_replaces_its_bucket_behind_the_tail():
|
|
# Ticks open the next minute before the exchange's own bar for the previous
|
|
# one arrives. Matching only the tail dropped it, leaving the tick-built
|
|
# approximation — with its partial volume — in place permanently.
|
|
from app.bars.models import Bar, Timeframe
|
|
from app.bars.store import InMemoryBarStore
|
|
|
|
store = InMemoryBarStore(100)
|
|
common = dict(tf=Timeframe.M1, symbol="/ES", source="schwab")
|
|
store.put(Bar(t=60, o=1, h=2, l=1, c=2, v=5, closed=False, **common))
|
|
store.put(Bar(t=120, o=2, h=3, l=2, c=3, v=1, closed=False, **common))
|
|
# The authoritative bar for the first minute, arriving late.
|
|
store.put(Bar(t=60, o=1, h=9, l=1, c=4, v=400, closed=True, **common))
|
|
|
|
held = store.get(Timeframe.M1)
|
|
assert [b.t for b in held] == [60, 120]
|
|
assert held[0].closed is True
|
|
assert held[0].v == 400, "the exchange's volume must win over the tick estimate"
|
|
|
|
|
|
def test_a_tick_cannot_overwrite_a_settled_bar():
|
|
from app.bars.models import Bar, Timeframe
|
|
from app.bars.store import InMemoryBarStore
|
|
|
|
store = InMemoryBarStore(100)
|
|
common = dict(tf=Timeframe.M1, symbol="/ES", source="schwab")
|
|
store.put(Bar(t=60, o=1, h=9, l=1, c=4, v=400, closed=True, **common))
|
|
store.put(Bar(t=60, o=1, h=2, l=1, c=2, v=5, closed=False, **common))
|
|
|
|
held = store.get(Timeframe.M1)[0]
|
|
assert held.closed is True and held.v == 400
|