feat: derivatives open-interest, flow & liquidation indicators (part 2 of 3) (#127)

* feat(derivatives): OIPriceDivergence indicator (core)

* feat(derivatives): OIWeighted indicator (core)

* feat(derivatives): LongShortRatio indicator (core)

* feat(derivatives): TakerBuySellRatio indicator (core)

* feat(derivatives): LiquidationFeatures multi-output indicator (core)

* feat(derivatives): Python, Node and WASM bindings for OI, flow & liquidation indicators

* test(derivatives): Python and Node tests for OI, flow & liquidation indicators

* fuzz(derivatives): drive OI, flow & liquidation indicators in derivatives target

* docs(derivatives): README row + counter 237->242, CHANGELOG part 2
This commit is contained in:
kingchenc
2026-06-01 21:50:35 +02:00
committed by GitHub
parent 5eb820a9c7
commit 8e5bfd07ce
20 changed files with 2078 additions and 27 deletions
@@ -258,3 +258,13 @@ def test_funding_basis_non_positive_index_raises():
def test_funding_rate_non_finite_raises():
with pytest.raises(ValueError):
ta.FundingRate().update(float("nan"))
def test_oi_price_divergence_zero_window_raises():
with pytest.raises(ValueError):
ta.OIPriceDivergence(0)
def test_oi_weighted_non_positive_mark_raises():
with pytest.raises(ValueError):
ta.OIWeighted().update(0.0, 100.0)
@@ -979,3 +979,37 @@ def test_open_interest_delta_reference_value():
assert oid.update(1000.0) is None # seeds the previous OI
assert oid.update(1250.0) == pytest.approx(250.0)
assert oid.update(1100.0) == pytest.approx(-150.0)
def test_oi_price_divergence_reference_value():
div = ta.OIPriceDivergence(1)
assert div.update(1000.0, 100.0) is None # warming up
# OI +10% while price flat -> divergence +0.1.
assert div.update(1100.0, 100.0) == pytest.approx(0.1)
def test_oi_weighted_reference_value():
oiw = ta.OIWeighted()
assert oiw.update(100.0, 10.0) == pytest.approx(100.0)
# (100·10 + 110·30) / 40 = 107.5.
assert oiw.update(110.0, 30.0) == pytest.approx(107.5)
def test_long_short_ratio_reference_value():
# 600 longs vs 400 shorts -> 1.5.
assert ta.LongShortRatio().update(600.0, 400.0) == pytest.approx(1.5)
# No short side -> 0.0.
assert ta.LongShortRatio().update(600.0, 0.0) == pytest.approx(0.0)
def test_taker_buy_sell_ratio_reference_value():
# 60 taker buys vs 40 taker sells -> 1.5.
assert ta.TakerBuySellRatio().update(60.0, 40.0) == pytest.approx(1.5)
# No taker sell volume -> 0.0.
assert ta.TakerBuySellRatio().update(60.0, 0.0) == pytest.approx(0.0)
def test_liquidation_features_reference_value():
# 30 long vs 10 short: (long, short, net, total, imbalance).
out = ta.LiquidationFeatures().update(30.0, 10.0)
assert out == pytest.approx((30.0, 10.0, 20.0, 40.0, 0.5))
@@ -1994,3 +1994,56 @@ def test_open_interest_delta_streaming_equals_batch():
streamed = np.array([streamer.update(oi[i]) for i in range(n)], dtype=np.float64)
assert batch.shape == (n,)
assert _eq_nan(batch, streamed)
def test_oi_flow_indicators_streaming_equals_batch():
n = 40
oi = np.array([1000.0 + 50.0 * math.sin(i * 0.2) for i in range(n)], dtype=np.float64)
mark = np.array([100.0 + math.cos(i * 0.3) for i in range(n)], dtype=np.float64)
long_sz = np.array([500.0 + 20.0 * math.sin(i * 0.25) for i in range(n)], dtype=np.float64)
short_sz = np.array([400.0 + 20.0 * math.cos(i * 0.25) for i in range(n)], dtype=np.float64)
# OIPriceDivergence carries a window; update(open_interest, mark_price).
batch = ta.OIPriceDivergence(5).batch(oi, mark)
streamer = ta.OIPriceDivergence(5)
streamed = np.array(
[streamer.update(oi[i], mark[i]) for i in range(n)], dtype=np.float64
)
assert batch.shape == (n,)
assert _eq_nan(batch, streamed)
# OIWeighted; update(mark_price, open_interest).
batch = ta.OIWeighted().batch(mark, oi)
streamer = ta.OIWeighted()
streamed = np.array(
[streamer.update(mark[i], oi[i]) for i in range(n)], dtype=np.float64
)
assert _eq_nan(batch, streamed)
# LongShortRatio; update(long_size, short_size).
batch = ta.LongShortRatio().batch(long_sz, short_sz)
streamer = ta.LongShortRatio()
streamed = np.array(
[streamer.update(long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64
)
assert _eq_nan(batch, streamed)
# TakerBuySellRatio; update(taker_buy_volume, taker_sell_volume).
batch = ta.TakerBuySellRatio().batch(long_sz, short_sz)
streamer = ta.TakerBuySellRatio()
streamed = np.array(
[streamer.update(long_sz[i], short_sz[i]) for i in range(n)], dtype=np.float64
)
assert _eq_nan(batch, streamed)
def test_liquidation_features_streaming_equals_batch():
n = 30
long_liq = np.array([abs(50.0 * math.sin(i * 0.4)) for i in range(n)], dtype=np.float64)
short_liq = np.array([abs(40.0 * math.cos(i * 0.3)) for i in range(n)], dtype=np.float64)
batch = ta.LiquidationFeatures().batch(long_liq, short_liq)
streamer = ta.LiquidationFeatures()
assert batch.shape == (n, 5)
for i in range(n):
row = streamer.update(long_liq[i], short_liq[i])
assert tuple(batch[i]) == pytest.approx(row)