chart/tests/test_store.py
Chris Amow 039e91b4e4 Stop a zero-price tick and a late exchange bar corrupting the store
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>
2026-08-10 17:10:24 -05:00

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