orderwave 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orderwave-0.1.0/PKG-INFO +175 -0
- orderwave-0.1.0/README.md +161 -0
- orderwave-0.1.0/orderwave/__init__.py +3 -0
- orderwave-0.1.0/orderwave/book.py +182 -0
- orderwave-0.1.0/orderwave/config.py +268 -0
- orderwave-0.1.0/orderwave/history.py +69 -0
- orderwave-0.1.0/orderwave/market.py +325 -0
- orderwave-0.1.0/orderwave/metrics.py +124 -0
- orderwave-0.1.0/orderwave/model.py +347 -0
- orderwave-0.1.0/orderwave/utils.py +61 -0
- orderwave-0.1.0/orderwave.egg-info/PKG-INFO +175 -0
- orderwave-0.1.0/orderwave.egg-info/SOURCES.txt +17 -0
- orderwave-0.1.0/orderwave.egg-info/dependency_links.txt +1 -0
- orderwave-0.1.0/orderwave.egg-info/requires.txt +5 -0
- orderwave-0.1.0/orderwave.egg-info/top_level.txt +1 -0
- orderwave-0.1.0/pyproject.toml +33 -0
- orderwave-0.1.0/setup.cfg +4 -0
- orderwave-0.1.0/tests/test_book.py +40 -0
- orderwave-0.1.0/tests/test_market.py +145 -0
orderwave-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orderwave
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: An order-flow-driven synthetic market simulator.
|
|
5
|
+
Project-URL: Homepage, https://github.com/smturtle2/quoteflow
|
|
6
|
+
Project-URL: Repository, https://github.com/smturtle2/quoteflow
|
|
7
|
+
Project-URL: Issues, https://github.com/smturtle2/quoteflow/issues
|
|
8
|
+
Requires-Python: >=3.11
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: numpy>=1.26
|
|
11
|
+
Requires-Dist: pandas>=2.2
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# orderwave
|
|
16
|
+
|
|
17
|
+
`orderwave` is an order-flow-driven market simulator.
|
|
18
|
+
|
|
19
|
+
It does not random-walk price directly. Instead, it simulates a limit order book with stochastic limit arrivals, marketable flow, cancellations, and inside-spread quote improvement, then lets price emerge from those book changes.
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install orderwave
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
For local development:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install -e .[dev]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from orderwave import Market
|
|
37
|
+
|
|
38
|
+
market = Market(seed=42)
|
|
39
|
+
|
|
40
|
+
market.step()
|
|
41
|
+
market.gen(steps=1_000)
|
|
42
|
+
|
|
43
|
+
snapshot = market.get()
|
|
44
|
+
history = market.get_history()
|
|
45
|
+
|
|
46
|
+
print(snapshot["mid_price"], snapshot["best_bid"], snapshot["best_ask"])
|
|
47
|
+
print(history.tail())
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Why orderwave
|
|
51
|
+
|
|
52
|
+
- Minimal public API: `from orderwave import Market`
|
|
53
|
+
- Price is an outcome of book dynamics, not a separately sampled process
|
|
54
|
+
- Hidden fair value and regime shifts bias order flow without directly overwriting price
|
|
55
|
+
- Deterministic paths under the same seed
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from orderwave import Market
|
|
61
|
+
|
|
62
|
+
market = Market(
|
|
63
|
+
init_price=100.0,
|
|
64
|
+
tick_size=0.01,
|
|
65
|
+
levels=5,
|
|
66
|
+
seed=42,
|
|
67
|
+
config={"preset": "balanced"},
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Methods:
|
|
72
|
+
|
|
73
|
+
- `step()` returns the latest snapshot after one micro-batch
|
|
74
|
+
- `gen(steps=n)` advances `n` steps and returns the latest snapshot
|
|
75
|
+
- `get()` returns the current snapshot
|
|
76
|
+
- `get_history()` returns a compact `pandas.DataFrame`
|
|
77
|
+
|
|
78
|
+
Supported presets:
|
|
79
|
+
|
|
80
|
+
- `balanced`
|
|
81
|
+
- `trend`
|
|
82
|
+
- `volatile`
|
|
83
|
+
|
|
84
|
+
`config` accepts either a plain `dict` or `orderwave.config.MarketConfig`.
|
|
85
|
+
|
|
86
|
+
## Snapshot
|
|
87
|
+
|
|
88
|
+
`Market.get()` returns:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
{
|
|
92
|
+
"step": int,
|
|
93
|
+
"last_price": float,
|
|
94
|
+
"mid_price": float,
|
|
95
|
+
"microprice": float,
|
|
96
|
+
"best_bid": float,
|
|
97
|
+
"best_ask": float,
|
|
98
|
+
"spread": float,
|
|
99
|
+
"bids": [{"price": float, "qty": float}, ...],
|
|
100
|
+
"asks": [{"price": float, "qty": float}, ...],
|
|
101
|
+
"last_trade_side": "buy" | "sell" | None,
|
|
102
|
+
"last_trade_qty": float,
|
|
103
|
+
"buy_aggr_volume": float,
|
|
104
|
+
"sell_aggr_volume": float,
|
|
105
|
+
"trade_strength": float,
|
|
106
|
+
"depth_imbalance": float,
|
|
107
|
+
"regime": str,
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`last_price` is the last executed trade price. If the book changes without a trade, `mid_price` can move while `last_price` stays fixed.
|
|
112
|
+
|
|
113
|
+
## Model
|
|
114
|
+
|
|
115
|
+
Each `step()` is a micro-batch:
|
|
116
|
+
|
|
117
|
+
1. Compute state features from the current book
|
|
118
|
+
2. Update regime: `calm`, `directional`, or `stressed`
|
|
119
|
+
3. Update hidden fair value
|
|
120
|
+
4. Sample limit orders, marketable flow, and cancellations
|
|
121
|
+
5. Shuffle events and apply them to the book
|
|
122
|
+
6. Record the snapshot and compact history row
|
|
123
|
+
|
|
124
|
+
Price moves only through book mechanics:
|
|
125
|
+
|
|
126
|
+
- market buy removes ask liquidity
|
|
127
|
+
- market sell removes bid liquidity
|
|
128
|
+
- cancellation depletes the best quote
|
|
129
|
+
- a new limit order improves the quote inside the spread
|
|
130
|
+
|
|
131
|
+
## Diagnostics Example
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
from orderwave import Market
|
|
135
|
+
|
|
136
|
+
market = Market(seed=7, config={"preset": "trend"})
|
|
137
|
+
market.gen(steps=5_000)
|
|
138
|
+
history = market.get_history()
|
|
139
|
+
|
|
140
|
+
mid_ret = history["mid_price"].diff().fillna(0.0)
|
|
141
|
+
abs_ret = mid_ret.abs()
|
|
142
|
+
spread_mean = history["spread"].mean()
|
|
143
|
+
imbalance_lead_corr = history["depth_imbalance"].corr(mid_ret.shift(-1).fillna(0.0))
|
|
144
|
+
vol_cluster = abs_ret.autocorr(lag=1)
|
|
145
|
+
|
|
146
|
+
print("spread mean:", spread_mean)
|
|
147
|
+
print("imbalance -> next return corr:", imbalance_lead_corr)
|
|
148
|
+
print("|return| lag-1 autocorr:", vol_cluster)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Maintainer Release
|
|
152
|
+
|
|
153
|
+
PyPI publishing is wired through [`workflow.yml`](https://github.com/smturtle2/quoteflow/blob/main/.github/workflows/workflow.yml).
|
|
154
|
+
|
|
155
|
+
On 2026-03-06, [GitHub Actions release event docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release) document that `release.types: [published]` triggers when a release is published, while drafts themselves do not trigger workflows. [PyPI trusted publishing docs](https://docs.pypi.org/trusted-publishers/using-a-publisher/) document the `id-token: write` flow used by the publish job.
|
|
156
|
+
|
|
157
|
+
Release flow:
|
|
158
|
+
|
|
159
|
+
1. Update `version` in `pyproject.toml`
|
|
160
|
+
2. Commit and push to `main`
|
|
161
|
+
3. In GitHub, open `Releases`
|
|
162
|
+
4. Click `Draft a new release`
|
|
163
|
+
5. Create a tag like `v0.1.0`
|
|
164
|
+
6. Set the release title, then click `Publish release`
|
|
165
|
+
7. GitHub Actions runs tests, builds the distributions, and publishes to PyPI
|
|
166
|
+
|
|
167
|
+
Trusted Publisher settings for PyPI:
|
|
168
|
+
|
|
169
|
+
- PyPI project name: `orderwave`
|
|
170
|
+
- Repository owner: `smturtle2`
|
|
171
|
+
- Repository name: `quoteflow`
|
|
172
|
+
- Workflow filename: `.github/workflows/workflow.yml`
|
|
173
|
+
- Environment name: `pypi`
|
|
174
|
+
|
|
175
|
+
If `orderwave` does not exist on PyPI yet, create the project through a pending publisher first. PyPI notes that a pending publisher does not reserve the name until the first successful publish.
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# orderwave
|
|
2
|
+
|
|
3
|
+
`orderwave` is an order-flow-driven market simulator.
|
|
4
|
+
|
|
5
|
+
It does not random-walk price directly. Instead, it simulates a limit order book with stochastic limit arrivals, marketable flow, cancellations, and inside-spread quote improvement, then lets price emerge from those book changes.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install orderwave
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For local development:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install -e .[dev]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from orderwave import Market
|
|
23
|
+
|
|
24
|
+
market = Market(seed=42)
|
|
25
|
+
|
|
26
|
+
market.step()
|
|
27
|
+
market.gen(steps=1_000)
|
|
28
|
+
|
|
29
|
+
snapshot = market.get()
|
|
30
|
+
history = market.get_history()
|
|
31
|
+
|
|
32
|
+
print(snapshot["mid_price"], snapshot["best_bid"], snapshot["best_ask"])
|
|
33
|
+
print(history.tail())
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why orderwave
|
|
37
|
+
|
|
38
|
+
- Minimal public API: `from orderwave import Market`
|
|
39
|
+
- Price is an outcome of book dynamics, not a separately sampled process
|
|
40
|
+
- Hidden fair value and regime shifts bias order flow without directly overwriting price
|
|
41
|
+
- Deterministic paths under the same seed
|
|
42
|
+
|
|
43
|
+
## API
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from orderwave import Market
|
|
47
|
+
|
|
48
|
+
market = Market(
|
|
49
|
+
init_price=100.0,
|
|
50
|
+
tick_size=0.01,
|
|
51
|
+
levels=5,
|
|
52
|
+
seed=42,
|
|
53
|
+
config={"preset": "balanced"},
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Methods:
|
|
58
|
+
|
|
59
|
+
- `step()` returns the latest snapshot after one micro-batch
|
|
60
|
+
- `gen(steps=n)` advances `n` steps and returns the latest snapshot
|
|
61
|
+
- `get()` returns the current snapshot
|
|
62
|
+
- `get_history()` returns a compact `pandas.DataFrame`
|
|
63
|
+
|
|
64
|
+
Supported presets:
|
|
65
|
+
|
|
66
|
+
- `balanced`
|
|
67
|
+
- `trend`
|
|
68
|
+
- `volatile`
|
|
69
|
+
|
|
70
|
+
`config` accepts either a plain `dict` or `orderwave.config.MarketConfig`.
|
|
71
|
+
|
|
72
|
+
## Snapshot
|
|
73
|
+
|
|
74
|
+
`Market.get()` returns:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
{
|
|
78
|
+
"step": int,
|
|
79
|
+
"last_price": float,
|
|
80
|
+
"mid_price": float,
|
|
81
|
+
"microprice": float,
|
|
82
|
+
"best_bid": float,
|
|
83
|
+
"best_ask": float,
|
|
84
|
+
"spread": float,
|
|
85
|
+
"bids": [{"price": float, "qty": float}, ...],
|
|
86
|
+
"asks": [{"price": float, "qty": float}, ...],
|
|
87
|
+
"last_trade_side": "buy" | "sell" | None,
|
|
88
|
+
"last_trade_qty": float,
|
|
89
|
+
"buy_aggr_volume": float,
|
|
90
|
+
"sell_aggr_volume": float,
|
|
91
|
+
"trade_strength": float,
|
|
92
|
+
"depth_imbalance": float,
|
|
93
|
+
"regime": str,
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
`last_price` is the last executed trade price. If the book changes without a trade, `mid_price` can move while `last_price` stays fixed.
|
|
98
|
+
|
|
99
|
+
## Model
|
|
100
|
+
|
|
101
|
+
Each `step()` is a micro-batch:
|
|
102
|
+
|
|
103
|
+
1. Compute state features from the current book
|
|
104
|
+
2. Update regime: `calm`, `directional`, or `stressed`
|
|
105
|
+
3. Update hidden fair value
|
|
106
|
+
4. Sample limit orders, marketable flow, and cancellations
|
|
107
|
+
5. Shuffle events and apply them to the book
|
|
108
|
+
6. Record the snapshot and compact history row
|
|
109
|
+
|
|
110
|
+
Price moves only through book mechanics:
|
|
111
|
+
|
|
112
|
+
- market buy removes ask liquidity
|
|
113
|
+
- market sell removes bid liquidity
|
|
114
|
+
- cancellation depletes the best quote
|
|
115
|
+
- a new limit order improves the quote inside the spread
|
|
116
|
+
|
|
117
|
+
## Diagnostics Example
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from orderwave import Market
|
|
121
|
+
|
|
122
|
+
market = Market(seed=7, config={"preset": "trend"})
|
|
123
|
+
market.gen(steps=5_000)
|
|
124
|
+
history = market.get_history()
|
|
125
|
+
|
|
126
|
+
mid_ret = history["mid_price"].diff().fillna(0.0)
|
|
127
|
+
abs_ret = mid_ret.abs()
|
|
128
|
+
spread_mean = history["spread"].mean()
|
|
129
|
+
imbalance_lead_corr = history["depth_imbalance"].corr(mid_ret.shift(-1).fillna(0.0))
|
|
130
|
+
vol_cluster = abs_ret.autocorr(lag=1)
|
|
131
|
+
|
|
132
|
+
print("spread mean:", spread_mean)
|
|
133
|
+
print("imbalance -> next return corr:", imbalance_lead_corr)
|
|
134
|
+
print("|return| lag-1 autocorr:", vol_cluster)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Maintainer Release
|
|
138
|
+
|
|
139
|
+
PyPI publishing is wired through [`workflow.yml`](https://github.com/smturtle2/quoteflow/blob/main/.github/workflows/workflow.yml).
|
|
140
|
+
|
|
141
|
+
On 2026-03-06, [GitHub Actions release event docs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release) document that `release.types: [published]` triggers when a release is published, while drafts themselves do not trigger workflows. [PyPI trusted publishing docs](https://docs.pypi.org/trusted-publishers/using-a-publisher/) document the `id-token: write` flow used by the publish job.
|
|
142
|
+
|
|
143
|
+
Release flow:
|
|
144
|
+
|
|
145
|
+
1. Update `version` in `pyproject.toml`
|
|
146
|
+
2. Commit and push to `main`
|
|
147
|
+
3. In GitHub, open `Releases`
|
|
148
|
+
4. Click `Draft a new release`
|
|
149
|
+
5. Create a tag like `v0.1.0`
|
|
150
|
+
6. Set the release title, then click `Publish release`
|
|
151
|
+
7. GitHub Actions runs tests, builds the distributions, and publishes to PyPI
|
|
152
|
+
|
|
153
|
+
Trusted Publisher settings for PyPI:
|
|
154
|
+
|
|
155
|
+
- PyPI project name: `orderwave`
|
|
156
|
+
- Repository owner: `smturtle2`
|
|
157
|
+
- Repository name: `quoteflow`
|
|
158
|
+
- Workflow filename: `.github/workflows/workflow.yml`
|
|
159
|
+
- Environment name: `pypi`
|
|
160
|
+
|
|
161
|
+
If `orderwave` does not exist on PyPI yet, create the project through a pending publisher first. PyPI notes that a pending publisher does not reserve the name until the first successful publish.
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from typing import Literal
|
|
5
|
+
|
|
6
|
+
BookSide = Literal["bid", "ask"]
|
|
7
|
+
AggressorSide = Literal["buy", "sell"]
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class ExecutionResult:
|
|
12
|
+
aggressor_side: AggressorSide
|
|
13
|
+
requested_qty: int
|
|
14
|
+
filled_qty: int = 0
|
|
15
|
+
last_fill_tick: int | None = None
|
|
16
|
+
fills: list[tuple[int, int]] = field(default_factory=list)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class OrderBook:
|
|
20
|
+
def __init__(self, tick_size: float) -> None:
|
|
21
|
+
self.tick_size = float(tick_size)
|
|
22
|
+
self.bid_book: dict[int, int] = {}
|
|
23
|
+
self.ask_book: dict[int, int] = {}
|
|
24
|
+
self.bid_staleness: dict[int, int] = {}
|
|
25
|
+
self.ask_staleness: dict[int, int] = {}
|
|
26
|
+
self.best_bid_tick: int | None = None
|
|
27
|
+
self.best_ask_tick: int | None = None
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def spread_ticks(self) -> int:
|
|
31
|
+
if self.best_bid_tick is None or self.best_ask_tick is None:
|
|
32
|
+
return 0
|
|
33
|
+
return self.best_ask_tick - self.best_bid_tick
|
|
34
|
+
|
|
35
|
+
def set_level(self, side: BookSide, tick: int, qty: int) -> None:
|
|
36
|
+
book, staleness = self._book_and_age(side)
|
|
37
|
+
if qty <= 0:
|
|
38
|
+
book.pop(tick, None)
|
|
39
|
+
staleness.pop(tick, None)
|
|
40
|
+
self._refresh_best(side)
|
|
41
|
+
return
|
|
42
|
+
book[tick] = int(qty)
|
|
43
|
+
staleness[tick] = 0
|
|
44
|
+
self._refresh_best(side)
|
|
45
|
+
|
|
46
|
+
def add_limit(self, side: BookSide, tick: int, qty: int) -> None:
|
|
47
|
+
if qty <= 0:
|
|
48
|
+
return
|
|
49
|
+
book, staleness = self._book_and_age(side)
|
|
50
|
+
book[tick] = int(book.get(tick, 0) + qty)
|
|
51
|
+
staleness[tick] = 0
|
|
52
|
+
self._refresh_best(side)
|
|
53
|
+
|
|
54
|
+
def apply_limit_relative(self, side: BookSide, level: int, qty: int) -> int | None:
|
|
55
|
+
tick = self.resolve_limit_tick(side, level)
|
|
56
|
+
if tick is None:
|
|
57
|
+
return None
|
|
58
|
+
self.add_limit(side, tick, qty)
|
|
59
|
+
return tick
|
|
60
|
+
|
|
61
|
+
def resolve_limit_tick(self, side: BookSide, level: int) -> int | None:
|
|
62
|
+
if self.best_bid_tick is None or self.best_ask_tick is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
if side == "bid":
|
|
66
|
+
if level == -1:
|
|
67
|
+
if self.spread_ticks <= 1:
|
|
68
|
+
return None
|
|
69
|
+
target_tick = self.best_bid_tick + 1
|
|
70
|
+
else:
|
|
71
|
+
target_tick = self.best_bid_tick - int(level)
|
|
72
|
+
if target_tick < 0 or target_tick >= self.best_ask_tick:
|
|
73
|
+
return None
|
|
74
|
+
return target_tick
|
|
75
|
+
|
|
76
|
+
if level == -1:
|
|
77
|
+
if self.spread_ticks <= 1:
|
|
78
|
+
return None
|
|
79
|
+
target_tick = self.best_ask_tick - 1
|
|
80
|
+
else:
|
|
81
|
+
target_tick = self.best_ask_tick + int(level)
|
|
82
|
+
if target_tick <= self.best_bid_tick:
|
|
83
|
+
return None
|
|
84
|
+
return target_tick
|
|
85
|
+
|
|
86
|
+
def cancel_level(self, side: BookSide, tick: int, qty: int) -> int:
|
|
87
|
+
if qty <= 0:
|
|
88
|
+
return 0
|
|
89
|
+
book, staleness = self._book_and_age(side)
|
|
90
|
+
resting = book.get(tick, 0)
|
|
91
|
+
canceled = min(resting, int(qty))
|
|
92
|
+
if canceled <= 0:
|
|
93
|
+
return 0
|
|
94
|
+
remaining = resting - canceled
|
|
95
|
+
if remaining > 0:
|
|
96
|
+
book[tick] = remaining
|
|
97
|
+
staleness[tick] = 0
|
|
98
|
+
else:
|
|
99
|
+
book.pop(tick, None)
|
|
100
|
+
staleness.pop(tick, None)
|
|
101
|
+
self._refresh_best(side)
|
|
102
|
+
return canceled
|
|
103
|
+
|
|
104
|
+
def execute_market(self, aggressor_side: AggressorSide, qty: int) -> ExecutionResult:
|
|
105
|
+
result = ExecutionResult(aggressor_side=aggressor_side, requested_qty=int(qty))
|
|
106
|
+
if qty <= 0:
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
if aggressor_side == "buy":
|
|
110
|
+
book = self.ask_book
|
|
111
|
+
staleness = self.ask_staleness
|
|
112
|
+
side = "ask"
|
|
113
|
+
best_key = lambda: self.best_ask_tick
|
|
114
|
+
else:
|
|
115
|
+
book = self.bid_book
|
|
116
|
+
staleness = self.bid_staleness
|
|
117
|
+
side = "bid"
|
|
118
|
+
best_key = lambda: self.best_bid_tick
|
|
119
|
+
|
|
120
|
+
remaining = int(qty)
|
|
121
|
+
while remaining > 0:
|
|
122
|
+
best_tick = best_key()
|
|
123
|
+
if best_tick is None:
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
resting = book[best_tick]
|
|
127
|
+
taken = min(resting, remaining)
|
|
128
|
+
remaining -= taken
|
|
129
|
+
result.filled_qty += taken
|
|
130
|
+
result.last_fill_tick = best_tick
|
|
131
|
+
result.fills.append((best_tick, taken))
|
|
132
|
+
|
|
133
|
+
new_qty = resting - taken
|
|
134
|
+
if new_qty > 0:
|
|
135
|
+
book[best_tick] = new_qty
|
|
136
|
+
staleness[best_tick] = 0
|
|
137
|
+
else:
|
|
138
|
+
book.pop(best_tick, None)
|
|
139
|
+
staleness.pop(best_tick, None)
|
|
140
|
+
self._refresh_best(side)
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
|
|
144
|
+
def increment_staleness(self) -> None:
|
|
145
|
+
for staleness in (self.bid_staleness, self.ask_staleness):
|
|
146
|
+
for tick in list(staleness):
|
|
147
|
+
staleness[tick] += 1
|
|
148
|
+
|
|
149
|
+
def top_levels(self, side: BookSide, depth: int) -> list[tuple[int, int]]:
|
|
150
|
+
return self.all_levels(side)[: max(0, depth)]
|
|
151
|
+
|
|
152
|
+
def all_levels(self, side: BookSide) -> list[tuple[int, int]]:
|
|
153
|
+
book, _ = self._book_and_age(side)
|
|
154
|
+
ticks = sorted(book, reverse=(side == "bid"))
|
|
155
|
+
return [(tick, int(book[tick])) for tick in ticks]
|
|
156
|
+
|
|
157
|
+
def best_qty(self, side: BookSide) -> int:
|
|
158
|
+
if side == "bid":
|
|
159
|
+
if self.best_bid_tick is None:
|
|
160
|
+
return 0
|
|
161
|
+
return int(self.bid_book[self.best_bid_tick])
|
|
162
|
+
if self.best_ask_tick is None:
|
|
163
|
+
return 0
|
|
164
|
+
return int(self.ask_book[self.best_ask_tick])
|
|
165
|
+
|
|
166
|
+
def total_depth(self, side: BookSide, depth: int) -> int:
|
|
167
|
+
return sum(qty for _, qty in self.top_levels(side, depth))
|
|
168
|
+
|
|
169
|
+
def level_age(self, side: BookSide, tick: int) -> int:
|
|
170
|
+
_, staleness = self._book_and_age(side)
|
|
171
|
+
return int(staleness.get(tick, 0))
|
|
172
|
+
|
|
173
|
+
def _book_and_age(self, side: BookSide) -> tuple[dict[int, int], dict[int, int]]:
|
|
174
|
+
if side == "bid":
|
|
175
|
+
return self.bid_book, self.bid_staleness
|
|
176
|
+
return self.ask_book, self.ask_staleness
|
|
177
|
+
|
|
178
|
+
def _refresh_best(self, side: BookSide) -> None:
|
|
179
|
+
if side == "bid":
|
|
180
|
+
self.best_bid_tick = max(self.bid_book) if self.bid_book else None
|
|
181
|
+
return
|
|
182
|
+
self.best_ask_tick = min(self.ask_book) if self.ask_book else None
|