Clamp negative provider volume before FSP updates

Clamp negative trade sizes and cumulative bar volume in sampling,
and sanitize history and realtime volume in `tina_vwap()` and
`dolla_vlm()`. Provider resets must not create negative outliers
that hide the derived curve. Cover the history and tick paths.

(this commit msg was generated in some part by `codex` using
`gpt-6` (`openai`))
wkt/fsp_backfill_sync
Gud Boi 2026-09-15 12:47:07 -04:00
parent 07d8cf5a3d
commit 20406210b3
3 changed files with 58 additions and 7 deletions

View File

@ -670,7 +670,10 @@ async def sample_and_broadcast(
['open', 'high', 'low', 'volume']
]
new_v: float = tick.get('size', 0)
new_v: float = max(
float(tick.get('size', 0)),
0.,
)
if v == 0 and new_v:
# no trades for this bar yet so the open
@ -678,11 +681,14 @@ async def sample_and_broadcast(
o = last
if sum_tick_vlm:
volume = v + new_v
volume = max(float(v), 0.) + new_v
else:
# presume backend takes care of summing
# it's own vlm
volume = quote['volume']
volume = max(
float(quote['volume']),
0.,
)
shm.array[[
'open',

View File

@ -90,7 +90,7 @@ async def tina_vwap(
a = ohlcv.array
chl3 = (a['close'] + a['high'] + a['low']) / 3
v = a['volume']
v = np.maximum(a['volume'], 0)
h_vwap, cum_wp, cum_v = wap(chl3, v)
@ -112,7 +112,7 @@ async def tina_vwap(
# ]
# this computes tick-by-tick weightings from here forward
size = tick['size']
size = max(float(tick['size']), 0.)
price = tick['price']
v_tot += size
@ -147,7 +147,7 @@ async def dolla_vlm(
'''
a = ohlcv.array
chl3 = (a['close'] + a['high'] + a['low']) / 3
v = a['volume']
v = np.maximum(a['volume'], 0)
# on first iteration yield history
yield {
@ -170,7 +170,7 @@ async def dolla_vlm(
):
# this computes tick-by-tick weightings from here forward
size = tick['size']
size = max(float(tick['size']), 0.)
price = tick['price']
li = ohlcv.index

View File

@ -24,6 +24,7 @@ from piker.fsp._momo import (
wma,
)
from piker.fsp._volume import (
dolla_vlm,
tina_vwap,
)
from piker.accounting import MktPair
@ -415,6 +416,50 @@ def test_builtin_fsp_stream_contract(target: Fsp) -> None:
trio.run(main)
def test_dolla_vlm_rejects_negative_trade_volume() -> None:
'''
Keep a provider volume reset from hiding the zoomed-out curve.
IB can emit a large negative trade size around a session closure.
The sampler previously stored that size as bar volume and the
dollar-volume history multiplied it by price. The resulting outlier
made `Viz.maxmin()` reject the entire downsampled range, leaving a
stale cached path or no visible curve. Seed one historical negative
volume and stream one negative trade tick, then prove both historical
and realtime dollar volume remain nonnegative.
'''
shm = OhlcvShm(length=4)
shm._array['volume'][:4] = [1, 2, -2_319_096, 3]
async def source() -> AsyncIterator[FeedQuote]:
yield {
'ticks': [{
'type': 'trade',
'price': 29_495.75,
'size': -2_319_096.,
}],
}
async def main() -> None:
stream = dolla_vlm.func(
source(),
cast(ShmArray, shm),
)
history = await anext(stream)
assert isinstance(history, dict)
values = history['dolla_vlm']
assert isinstance(values, np.ndarray)
assert values[2] == 0
assert np.all(values >= 0)
realtime = await anext(stream)
assert realtime == ('dolla_vlm', 0)
await stream.aclose()
trio.run(main)
class Viz:
def __init__(self, name: str, shm: Shm) -> None:
self.name: str = name