Discrete mixture distribution market intent simulator
Project description
market-wave
Fast, lightweight synthetic market data from relative-tick intent PMFs.
English | 한국어
market-wave is a Python library for generating synthetic market paths from
market-wide entry and exit intent. It does not create individual participants.
Instead, it models aggregate buy/sell pressure, position exits, order-book depth,
cancellations, taker flow, and execution-driven price movement from probability
mass over relative ticks.
It is not a forecasting model. It is a lightweight simulation primitive for experiments, visualization, teaching, and strategy-environment prototyping.
Why market-wave?
- Aggregate intent, not agents: market participants are represented by probability mass over relative ticks, not by individual objects.
- Relative tick PMFs: entry and exit pressure are modeled as
P(relative_tick), then projected onto the current price grid. - Pluggable distributions: swap only the PMF generator with
LaplaceMixturePMF,SkewedPMF,FatTailPMF,NoisyPMF, or a custom model. - Separated shape and size: PMFs decide where intent sits; intensity decides how much order flow appears.
- Execution-driven prices: prices stay flat unless trades execute.
- Batch generation: generate many reproducible synthetic paths without
keeping every path in
market.history. - Inspectable state: every step returns a
StepInfosnapshot with PMFs, volumes, order book state, position mass, VWAP, spread, and imbalance. - Built-in plotting:
matplotlibis included, with a clean light chart style by default.
Install
pip install market-wave
For local development:
git clone https://github.com/smturtle2/market-wave.git
cd market-wave
uv sync --extra dev
Python >=3.10 is supported.
Quickstart
from market_wave import Market
market = Market(
initial_price=10_000,
gap=10,
popularity=1.0,
seed=42,
regime="auto",
augmentation_strength=0.25,
)
steps = market.step(500)
last = steps[-1]
print(last.price_before, "->", last.price_after)
print("executed:", round(last.total_executed_volume, 3))
print("imbalance:", round(last.order_flow_imbalance, 3))
print("crossed flow:", round(last.crossed_market_volume, 3))
print("residual flow:", round(last.residual_market_buy_volume, 3), round(last.residual_market_sell_volume, 3))
Market.step(n) always returns list[StepInfo] and appends the same objects to
market.history.
For high-volume generation, skip in-memory history:
steps = market.step(512, keep_history=False)
for step in market.stream(512, keep_history=False):
consume(step)
For simple export workflows, use step.to_dict(), step.to_json(), or
market.history_records().
Example output with seed=42:
10030.0 -> 10030.0
executed: 1.03
imbalance: 0.054
crossed flow: 0.693
residual flow: 0.215 0.122
Smoke Matrix
The simulator is deterministic for a fixed seed, so it is easy to run the same invariants across different market conditions:
from market_wave import Market
cases = [
("baseline", dict(initial_price=10_000, gap=10, popularity=1.0, seed=42, grid_radius=20), 500),
("busy", dict(initial_price=10_000, gap=10, popularity=2.5, seed=7, grid_radius=24), 500),
("thin", dict(initial_price=500, gap=5, popularity=0.25, seed=123, grid_radius=12), 500),
("low_price", dict(initial_price=1, gap=1, popularity=3.0, seed=17, grid_radius=8), 500),
("inactive", dict(initial_price=100, gap=1, popularity=0.0, seed=9, grid_radius=10), 100),
]
for name, kwargs, steps_count in cases:
market = Market(**kwargs)
steps = market.step(steps_count)
prices = [step.price_after for step in steps]
move_steps = sum(step.price_change != 0 for step in steps)
exec_steps = sum(step.total_executed_volume > 0 for step in steps)
print(name, min(prices), max(prices), move_steps, exec_steps, market.state.price)
Recent verification on the current implementation:
baseline range= 9930.0-10010.0 moves=235 exec_steps=500 final= 9950.0
busy range= 9920.0-10010.0 moves=248 exec_steps=500 final= 9950.0
thin range= 470.0-500.0 moves=242 exec_steps=500 final= 475.0
low_price range= 1.0-3.0 moves=236 exec_steps=500 final= 1.0
inactive range= 100.0-100.0 moves= 0 exec_steps= 0 final= 100.0
Those runs also checked that current-state PMFs stay aligned with
state.price_grid, PMFs remain normalized, prices never fall below one tick,
order book and position mass stay non-negative, and price changes only occur on
steps with executed volume.
Visualization
from market_wave import Market
market = Market(initial_price=10_000, gap=10, popularity=1.0, seed=42)
market.step(260)
fig, ax = market.plot(last=180)
The default market_wave style uses a light multi-panel chart: price/VWAP,
bid and ask orderbook depth heatmaps by simple level, executed volume, and
order-flow imbalance. To keep the legacy three-panel view, pass
orderbook=False.
Dark overlay mode is still available:
fig, ax = market.plot(layout="overlay", style="market_wave_dark")
Synthetic Data
from market_wave import compute_metrics, generate_paths
paths = generate_paths(
n_paths=100,
horizon=512,
config_sampler=lambda path_id: {
"initial_price": 10_000,
"gap": 10,
"popularity": 1.0,
"seed": 10_000 + path_id,
"regime": "auto",
"augmentation_strength": 0.35,
},
)
metrics = compute_metrics(paths)
print(metrics.return_std, metrics.volume_mean, metrics.max_drawdown)
print(paths[0].metadata.config_hash)
GeneratedPath.metadata stores seed, config_hash, package version,
regime, and augmentation_strength so synthetic runs can be traced. Pandas is
optional: install market-wave[dataframe] to use to_dataframe().
Pluggable PMFs
from market_wave import FatTailPMF, Market, NoisyPMF
market = Market(
initial_price=100,
gap=1,
distribution_model=NoisyPMF(FatTailPMF()),
augmentation_strength=0.5,
seed=7,
)
step = market.step(1)[0]
print(step.relative_ticks)
print(step.buy_entry_pmf_by_tick)
Custom models only need one method:
class MyPMF:
def pmf(self, side, intent, relative_ticks, context):
weights = [1.0 / (1.0 + abs(tick)) for tick in relative_ticks]
total = sum(weights)
return [weight / total for weight in weights]
Core Concepts
At every step, the market builds relative ticks around the current price:
relative_tick = (price - current_price) / tick_size
relative_ticks = [-grid_radius, ..., 0, ..., +grid_radius]
The simulator maintains four probability mass functions on that relative grid:
buy_entry_pmfsell_entry_pmflong_exit_pmfshort_exit_pmf
Each PMF is a normalized discrete mixture:
pmf[tick] = sum(component_weight * kernel(tick, center_tick, spread_ticks))
kernel(tick, center, spread) proportional to exp(-abs(tick - center) / spread)
Those relative PMFs are projected onto price_grid = current_price +/- k * gap
for order-book formation. pmf_inertia keeps intent from jumping abruptly:
pmf_t = pmf_inertia * pmf_prev + (1 - pmf_inertia) * pmf_new
PMFs generate aggregate intent. Intensity controls total size. The order book and execution layer then turn that intent into limit flow, taker flow, cancellations, exits, matched volume, and price changes.
Execution Guarantee
Price movement is execution-driven:
- If a step has no executed volume,
price_after == price_before. - If trades execute,
price_afteris derived from that step's execution statistics. Random quote jitter is bounded and cannot move the price by itself when executions print at the previous price. seedmakes the simulation reproducible for the same version and inputs.
This is a simulator, not a market data replay engine and not financial advice.
API Overview
from market_wave import (
Market,
LaplaceMixturePMF,
SkewedPMF,
FatTailPMF,
NoisyPMF,
generate_paths,
compute_metrics,
MarketState,
IntensityState,
LatentState,
MixtureComponent,
DiscreteMixtureDistribution,
DistributionState,
OrderBookState,
PositionMassState,
StepInfo,
)
Useful StepInfo fields include:
price_before,price_after,price_changetick_before,tick_after,tick_change,relative_ticksbuy_entry_pmf,sell_entry_pmf,long_exit_pmf,short_exit_pmfbuy_entry_pmf_by_tick,sell_entry_pmf_by_tickbuy_volume_by_price,sell_volume_by_priceexecuted_volume_by_price,total_executed_volume,trade_countmarket_buy_volume,market_sell_volume,crossed_market_volumeresidual_market_buy_volume,residual_market_sell_volumevwap_price,best_bid_before,best_ask_before,spread_afterorderbook_before,orderbook_afterposition_mass_before,position_mass_after
Development
uv sync --extra dev
uv run ruff check .
uv run pytest
uv build
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file market_wave-0.1.2.tar.gz.
File metadata
- Download URL: market_wave-0.1.2.tar.gz
- Upload date:
- Size: 2.0 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8dab010d29de768a344483fa5be62fd16db90d8f4a1c3de8a953729ab6f976a9
|
|
| MD5 |
2944f36fd603262c1401c70ba7e50b98
|
|
| BLAKE2b-256 |
895b25341ee0e671df9c10e6499efdaac95d2cb20a91c6ba5e23531b9863cd1b
|
Provenance
The following attestation bundles were made for market_wave-0.1.2.tar.gz:
Publisher:
workflow.yml on smturtle2/market-wave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
market_wave-0.1.2.tar.gz -
Subject digest:
8dab010d29de768a344483fa5be62fd16db90d8f4a1c3de8a953729ab6f976a9 - Sigstore transparency entry: 1387581938
- Sigstore integration time:
-
Permalink:
smturtle2/market-wave@e40582bba47f13b98e1c81a4ab24c3f472177a21 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/smturtle2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@e40582bba47f13b98e1c81a4ab24c3f472177a21 -
Trigger Event:
push
-
Statement type:
File details
Details for the file market_wave-0.1.2-py3-none-any.whl.
File metadata
- Download URL: market_wave-0.1.2-py3-none-any.whl
- Upload date:
- Size: 25.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a2738ce9f94ef0bd83c56714f83afcedb80b434fdf7dfb870cb089b1712fab6
|
|
| MD5 |
aa936152b3fa72774bb8015518e50114
|
|
| BLAKE2b-256 |
baaa7fb9d2bced64e47af3f5b6b98bb3c1447cad633d843ba283fa72ec5bebb9
|
Provenance
The following attestation bundles were made for market_wave-0.1.2-py3-none-any.whl:
Publisher:
workflow.yml on smturtle2/market-wave
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
market_wave-0.1.2-py3-none-any.whl -
Subject digest:
8a2738ce9f94ef0bd83c56714f83afcedb80b434fdf7dfb870cb089b1712fab6 - Sigstore transparency entry: 1387582006
- Sigstore integration time:
-
Permalink:
smturtle2/market-wave@e40582bba47f13b98e1c81a4ab24c3f472177a21 -
Branch / Tag:
refs/tags/v0.1.2 - Owner: https://github.com/smturtle2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
workflow.yml@e40582bba47f13b98e1c81a4ab24c3f472177a21 -
Trigger Event:
push
-
Statement type: