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']
|
['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:
|
if v == 0 and new_v:
|
||||||
# no trades for this bar yet so the open
|
# no trades for this bar yet so the open
|
||||||
|
|
@ -678,11 +681,14 @@ async def sample_and_broadcast(
|
||||||
o = last
|
o = last
|
||||||
|
|
||||||
if sum_tick_vlm:
|
if sum_tick_vlm:
|
||||||
volume = v + new_v
|
volume = max(float(v), 0.) + new_v
|
||||||
else:
|
else:
|
||||||
# presume backend takes care of summing
|
# presume backend takes care of summing
|
||||||
# it's own vlm
|
# it's own vlm
|
||||||
volume = quote['volume']
|
volume = max(
|
||||||
|
float(quote['volume']),
|
||||||
|
0.,
|
||||||
|
)
|
||||||
|
|
||||||
shm.array[[
|
shm.array[[
|
||||||
'open',
|
'open',
|
||||||
|
|
|
||||||
|
|
@ -90,7 +90,7 @@ async def tina_vwap(
|
||||||
|
|
||||||
a = ohlcv.array
|
a = ohlcv.array
|
||||||
chl3 = (a['close'] + a['high'] + a['low']) / 3
|
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)
|
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
|
# this computes tick-by-tick weightings from here forward
|
||||||
size = tick['size']
|
size = max(float(tick['size']), 0.)
|
||||||
price = tick['price']
|
price = tick['price']
|
||||||
|
|
||||||
v_tot += size
|
v_tot += size
|
||||||
|
|
@ -147,7 +147,7 @@ async def dolla_vlm(
|
||||||
'''
|
'''
|
||||||
a = ohlcv.array
|
a = ohlcv.array
|
||||||
chl3 = (a['close'] + a['high'] + a['low']) / 3
|
chl3 = (a['close'] + a['high'] + a['low']) / 3
|
||||||
v = a['volume']
|
v = np.maximum(a['volume'], 0)
|
||||||
|
|
||||||
# on first iteration yield history
|
# on first iteration yield history
|
||||||
yield {
|
yield {
|
||||||
|
|
@ -170,7 +170,7 @@ async def dolla_vlm(
|
||||||
):
|
):
|
||||||
|
|
||||||
# this computes tick-by-tick weightings from here forward
|
# this computes tick-by-tick weightings from here forward
|
||||||
size = tick['size']
|
size = max(float(tick['size']), 0.)
|
||||||
price = tick['price']
|
price = tick['price']
|
||||||
|
|
||||||
li = ohlcv.index
|
li = ohlcv.index
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ from piker.fsp._momo import (
|
||||||
wma,
|
wma,
|
||||||
)
|
)
|
||||||
from piker.fsp._volume import (
|
from piker.fsp._volume import (
|
||||||
|
dolla_vlm,
|
||||||
tina_vwap,
|
tina_vwap,
|
||||||
)
|
)
|
||||||
from piker.accounting import MktPair
|
from piker.accounting import MktPair
|
||||||
|
|
@ -415,6 +416,50 @@ def test_builtin_fsp_stream_contract(target: Fsp) -> None:
|
||||||
trio.run(main)
|
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:
|
class Viz:
|
||||||
def __init__(self, name: str, shm: Shm) -> None:
|
def __init__(self, name: str, shm: Shm) -> None:
|
||||||
self.name: str = name
|
self.name: str = name
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue