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
parent
07d8cf5a3d
commit
20406210b3
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue