Files
2026-06-04 18:12:47 -03:00

144 lines
6.7 KiB
Python

import sys
import os
import unittest
# Add root folder to path so we can import config and backend
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
from config import settings
from backend.aggregator import Aggregator, FootprintCluster, classify_tick
class TestAggregator(unittest.TestCase):
def test_tick_classification(self):
# TICK_FLAG_BUY = 0x2
# TICK_FLAG_SELL = 0x4
# 1. With flags
self.assertTrue(classify_tick(last=10.0, bid=9.0, ask=11.0, flags=0x2)) # Buy flag
self.assertFalse(classify_tick(last=10.0, bid=9.0, ask=11.0, flags=0x4)) # Sell flag
# Buy flag should take priority even if last <= bid
self.assertTrue(classify_tick(last=8.0, bid=9.0, ask=11.0, flags=0x2))
# 2. Fallbacks (no flags or flag=0)
self.assertTrue(classify_tick(last=11.5, bid=9.0, ask=11.0, flags=0)) # last >= ask
self.assertFalse(classify_tick(last=8.5, bid=9.0, ask=11.0, flags=0)) # last <= bid
# Mid-spread fallbacks
self.assertTrue(classify_tick(last=10.5, bid=9.0, ask=11.0, flags=0)) # closer to ask
self.assertFalse(classify_tick(last=9.5, bid=9.0, ask=11.0, flags=0)) # closer to bid
def test_poc_calculation_and_tie(self):
cluster = FootprintCluster(tick_size=1.0)
# Add ticks
cluster.add_tick(price=10.0, volume=100.0, is_buy=True, timestamp_msc=1000)
cluster.add_tick(price=11.0, volume=150.0, is_buy=False, timestamp_msc=1100)
cluster.add_tick(price=12.0, volume=50.0, is_buy=True, timestamp_msc=1200)
# POC should be 11.0 (highest volume 150)
self.assertEqual(cluster.poc, 11.0)
# Add more volume to 10.0 to create a tie of 150.0 with 11.0
cluster.add_tick(price=10.0, volume=50.0, is_buy=True, timestamp_msc=1300)
# 10.0 total = 150.0. 11.0 total = 150.0.
# Tie breaker rule: highest price level wins, so 11.0 should still be POC
self.assertEqual(cluster.poc, 11.0)
# Now let 12.0 tie with 150.0 (currently 50.0, add 100.0)
cluster.add_tick(price=12.0, volume=100.0, is_buy=False, timestamp_msc=1400)
# Tie between 10.0, 11.0, and 12.0. Highest price level is 12.0
self.assertEqual(cluster.poc, 12.0)
def test_diagonal_imbalances(self):
cluster = FootprintCluster(tick_size=1.0)
# Let's seed level 10.0 and 11.0
# At level 10.0, bid_vol = 10.0
# At level 11.0, ask_vol = 30.0
# ask_vol[11.0] (30.0) >= 3.0 * bid_vol[10.0] (10.0) -> Buy imbalance at 11.0!
cluster.add_tick(price=10.0, volume=10.0, is_buy=False, timestamp_msc=1000)
cluster.add_tick(price=11.0, volume=30.0, is_buy=True, timestamp_msc=1010)
levels_data = cluster.to_json()["levels"]
self.assertEqual(levels_data["11.00"]["imbalance"], "buy")
# At level 12.0, ask_vol = 50.0
# At level 11.0, bid_vol = 150.0
# bid_vol[11.0] (150.0) >= 3.0 * ask_vol[12.0] (50.0) -> Sell imbalance at 11.0!
cluster.add_tick(price=12.0, volume=50.0, is_buy=True, timestamp_msc=1020)
cluster.add_tick(price=11.0, volume=150.0, is_buy=False, timestamp_msc=1030)
levels_data = cluster.to_json()["levels"]
self.assertEqual(levels_data["11.00"]["imbalance"], "both")
def test_stacked_imbalances(self):
cluster = FootprintCluster(tick_size=1.0)
# Seed bid levels
cluster.add_tick(price=9.0, volume=10.0, is_buy=False, timestamp_msc=1000)
cluster.add_tick(price=10.0, volume=10.0, is_buy=False, timestamp_msc=1000)
cluster.add_tick(price=11.0, volume=10.0, is_buy=False, timestamp_msc=1000)
# Seed ask levels to trigger buy imbalances at 10.0, 11.0, 12.0
# ask[10.0] >= 3 * bid[9.0] -> ask[10.0] >= 30
cluster.add_tick(price=10.0, volume=30.0, is_buy=True, timestamp_msc=1000)
# ask[11.0] >= 3 * bid[10.0] -> ask[11.0] >= 30
cluster.add_tick(price=11.0, volume=30.0, is_buy=True, timestamp_msc=1000)
# ask[12.0] >= 3 * bid[11.0] -> ask[12.0] >= 30
cluster.add_tick(price=12.0, volume=30.0, is_buy=True, timestamp_msc=1000)
res = cluster.to_json()
self.assertTrue(res["stacked"]["buy"])
self.assertIn(10.0, res["stacked"]["price_range"])
self.assertIn(11.0, res["stacked"]["price_range"])
self.assertIn(12.0, res["stacked"]["price_range"])
def test_closure_criteria(self):
# Override settings programmatically
settings.CLUSTER_RANGE_POINTS = 10
settings.CLUSTER_VOLUME_MAX = 1000
settings.CLUSTER_DELTA_MAX = 500
settings.CLUSTER_TIME_SECONDS = 60
agg = Aggregator(tick_size=1.0)
# 1. Test closure by range
active, closed = agg.process_tick(price=100.0, volume=1.0, is_buy=True, timestamp_msc=1000)
self.assertIsNone(closed)
active, closed = agg.process_tick(price=110.0, volume=1.0, is_buy=True, timestamp_msc=1050)
self.assertIsNotNone(closed)
self.assertEqual(closed["close_reason"], "range")
# 2. Test closure by volume
agg = Aggregator(tick_size=1.0)
# Add 400 buy (delta=400, vol=400)
active, closed = agg.process_tick(price=100.0, volume=400.0, is_buy=True, timestamp_msc=1000)
self.assertIsNone(closed)
# Add 400 sell (delta=0, vol=800)
active, closed = agg.process_tick(price=100.0, volume=400.0, is_buy=False, timestamp_msc=1010)
self.assertIsNone(closed)
# Add 200 buy (delta=200, vol=1000) -> Should close due to volume
active, closed = agg.process_tick(price=100.0, volume=200.0, is_buy=True, timestamp_msc=1020)
self.assertIsNotNone(closed)
self.assertEqual(closed["close_reason"], "volume")
# 3. Test closure by delta
agg = Aggregator(tick_size=1.0)
active, closed = agg.process_tick(price=100.0, volume=499.0, is_buy=True, timestamp_msc=1000)
self.assertIsNone(closed)
active, closed = agg.process_tick(price=100.0, volume=1.0, is_buy=True, timestamp_msc=1010)
self.assertIsNotNone(closed)
self.assertEqual(closed["close_reason"], "delta")
# 4. Test closure by time
agg = Aggregator(tick_size=1.0)
active, closed = agg.process_tick(price=100.0, volume=1.0, is_buy=True, timestamp_msc=1000)
self.assertIsNone(closed)
active, closed = agg.process_tick(price=100.0, volume=1.0, is_buy=True, timestamp_msc=1000 + 60000)
self.assertIsNotNone(closed)
self.assertEqual(closed["close_reason"], "time")
if __name__ == "__main__":
unittest.main()