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.
@@ -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,3 @@
1
+ from orderwave.market import Market
2
+
3
+ __all__ = ["Market"]
@@ -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