Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions finmlkit/bar/data_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ def __init__(self,
self._data = pd.DataFrame({'timestamp': ts, 'price': px, 'amount': qty, 'id': id})

self.is_buyer_maker = is_buyer_maker
if is_buyer_maker is not None and preprocess:
if len(is_buyer_maker) != len(self._data):
raise ValueError("is_buyer_maker must have the same length as the trades")
# Carried as a column so that sorting and de-duplicating re-order it together with its trade
self._data['is_buyer_maker'] = is_buyer_maker
if side is not None:
self._data['side'] = side
self._orig_timestamp_unit = timestamp_unit if timestamp_unit else self._infer_timestamp_unit()
Expand Down Expand Up @@ -329,6 +334,10 @@ def _merge_trades(self):
"""
logger.info('Merging split trades (same timestamps) on same price level...')

if 'is_buyer_maker' in self._data.columns:
# re-ordered together with the other columns by _sort_trades
self.is_buyer_maker = self._data['is_buyer_maker'].to_numpy(dtype=np.bool_)

ts, px, am, side = merge_split_trades(
self.data['timestamp'].values.astype(np.int64),
self.data['price'].values.astype(np.float64),
Expand Down
29 changes: 29 additions & 0 deletions tests/bars/test_trades_data_side_alignment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import numpy as np

from finmlkit.bar.data_model import TradesData


def test_side_stays_with_its_trade_when_input_is_not_in_id_order():
"""Binance dumps are not always in id order; sorting must carry is_buyer_maker along with price/time/amount."""
n = 1000
ids = np.arange(1, n + 1, dtype=np.int64)
ts = (1_700_000_000_000 + ids * 5).astype(np.int64) # ms, one trade per timestamp: nothing merges
px = 100.0 + ids * 0.01
qty = np.ones(n)
buyer_maker = (ids % 3 == 0) # a side that is a function of the trade
order = np.random.default_rng(0).permutation(n) # shuffled, as in an out-of-order dump
td = TradesData(ts[order], px[order], qty[order], id=ids[order], is_buyer_maker=buyer_maker[order],
timestamp_unit="ms", preprocess=True)
out = td.data
trade_id = np.rint((out["price"].to_numpy() - 100.0) / 0.01).astype(np.int64)
expected = np.where(trade_id % 3 == 0, -1, 1) # buyer maker => seller aggressor => -1
assert (out["side"].to_numpy() == expected).all()


def test_sorted_input_is_unchanged():
n = 50
ids = np.arange(1, n + 1, dtype=np.int64)
ts = (1_700_000_000_000 + ids * 5).astype(np.int64)
td = TradesData(ts, 100.0 + ids, np.ones(n), id=ids, is_buyer_maker=(ids % 2 == 0),
timestamp_unit="ms", preprocess=True)
assert (td.data["side"].to_numpy() == np.where(ids % 2 == 0, -1, 1)).all()
Loading