Cont/Hawkes order-flow estimates plus a Gymnasium environment for selling a child order with price-time queue priority.
The policy is a small numeric network (REINFORCE by default, optional Stable-Baselines3 PPO). This is not LLM / verl / GRPO training.
L2 ticks / orders / trades (kept local)
→ Cont λ(i), μ, θ(i)·x (+ Hawkes μ for thin names)
→ fill odds / waiting time
↓
Gymnasium LimitOrderEnv
numeric state: depth, imbalance, queue, leftover, time, NAV
actions: wait | ask+0/+1/+2 | cancel | market 50% | market 100%
↓
REINFORCE or SB3 PPO
vs TWAP / VWAP / random (implementation shortfall)
Libraries used as references, not forks:
- Cont, Stoikov, Talreja (2010) intensity model
- Gymnasium environment API
- Stable-Baselines3 PPO trainer
- Qlib RL order-execution example for IS / TWAP / OPE structure
- LOB price-time mechanics (queue ahead of the agent)
cd etf-execution
pip install -e ".[dev]"
pytest -q
python -m etf_execution eval
python -m etf_execution train --episodes 40PPO (optional):
pip install -e ".[train]"
python -m etf_execution train --ppo --episodes 40The agent is a seller. Each step it can rest on the ask, cancel, or cross the spread. Background flow arrives from estimated Cont intensities; market-order intensity can self-excite with a Hawkes kernel. A buy that walks the ask fills the agent in price-time order.
Reward is minus implementation shortfall versus arrival mid, minus leftover inventory at the horizon, minus a small NAV residual and taker penalty. Unfilled size is dumped as a market order at the last step so fill rate is comparable to TWAP.
Column names follow a Cont-style notebook: time, price, size, side, orderid on orders; bidorderid / askorderid / price on trades; bid1…ask5, bsize1…asize5 on ticks. The aggressor of a trade is the later order id.
from etf_execution.cont.estimate import estimate_intensities
from etf_execution.data.synthetic import make_synthetic_day
orders, trades, ticks = make_synthetic_day()
ix = estimate_intensities(orders, trades, ticks)
print(ix.mu, ix.lambda_by_level, ix.fill_probability(1))The public repo ships synthetic tapes only. Do not commit exchange L2 dumps. Drop private CSVs under data/raw/ (gitignored).
verl, GRPO, LLM policies, FinRL forks, and publishing proprietary A-share L2. Matching/gateway C++ stays outside this Python research env.