- Nguyen Ngoc Duy Tan - 22125090
- Group 04
Note: Run commands from the project root directory:
momentum_microstructure_project/
This project develops a rule-based intraday trading system for VN30F1M futures using microstructure-confirmed momentum. Instead of relying only on price momentum, the strategy requires confirmation from order-book imbalance, spread quality, breakout behavior, and ATR-based risk controls. The project also includes an improved regime-adaptive strategy that trades momentum in trending regimes and allows mean-reversion entries in ranging regimes when price is stretched from VWAP and the order book supports a reversal.
The implementation covers the research-to-paper-trading workflow up to the paper trading stage. It includes data processing, feature engineering, strict and regime-adaptive signal generation, spread-aware and fee-aware backtesting, parameter optimization scaffolding, PaperBroker connectivity, Redis market-data integration, live order placement, and order/fill logging.
The latest available paper-trading logs show that the infrastructure is operational: 95 FIX orders were sent, 61 were accepted, 34 were rejected, 44 fill events were recorded, 34 unique orders were filled, 116 contracts were filled, and the total filled notional was approximately 23.50B VND. However, the current local research dataset contains only one historical feature row, so these paper-trading metrics prove execution connectivity and strategy behavior, not yet statistical profitability.
Vietnam's derivatives market gives traders an opportunity to trade VN30 index futures intraday. However, discretionary intraday trading is difficult because the market moves quickly, order-book conditions change rapidly, and manual execution can be affected by emotion, delay, and inconsistent risk control.
This project proposes an automated paper-trading pipeline for VN30F1M. The core idea is that price momentum alone is often noisy. A momentum signal is more meaningful when it is supported by market microstructure:
- price breaks out of a recent intraday range,
- short-term EMA momentum is aligned with the breakout,
- order-book imbalance confirms buying or selling pressure,
- the spread remains tradable,
- exits are controlled by ATR stop, take-profit, trailing stop, and timeout.
During development, the project was extended with a regime-adaptive layer. This layer classifies the market as trending, ranging, wide-spread, dead-flat, or high-volatility unstable. Trend-following entries are only allowed in trending conditions. Mean-reversion entries are allowed in ranging conditions when price is stretched away from VWAP/rolling mean and order-book pressure turns back toward the mean.
The Exponential Moving Average (EMA) gives more weight to recent prices. In this project, three EMAs are used:
- fast EMA: 8 bars,
- slow EMA: 21 bars,
- trend EMA: 55 bars.
The momentum gap is calculated as:
momentum_gap = fast_ema - slow_ema
The momentum slope is the change in this gap:
momentum_slope = momentum_gap.diff()
Average True Range (ATR) is used to estimate short-term volatility. The project uses an ATR window of 20 bars. ATR controls stop-loss distance, take-profit distance, and trailing-stop behavior.
Order-book imbalance measures whether visible depth is stronger on the bid side or ask side. The project uses the first three book levels:
order_imbalance = (bid_depth - ask_depth) / (bid_depth + ask_depth)
Positive imbalance suggests stronger bid-side demand. Negative imbalance suggests stronger ask-side pressure.
Spread is calculated as:
spread = ask_price_1 - bid_price_1
The strategy only trades when the spread is within a configured tick range. In the current configuration, valid spread is between 1 and 4 ticks.
The regime-adaptive strategy uses VWAP and price z-score to identify ranging-market mean-reversion opportunities:
- price below VWAP with positive imbalance may support a long mean-reversion entry,
- price above VWAP with negative imbalance may support a short mean-reversion entry.
VN30F1M intraday momentum is more reliable when price momentum is confirmed by market microstructure. A trade should only be opened when price movement, breakout behavior, order-book pressure, and spread quality all support the same direction.
VN30F1M short-horizon momentum is only worth trading when the market is in a tradable trending regime and the move is confirmed by persistent order-book imbalance plus above-normal matched volume. In ranging regimes, small mean-reversion trades are allowed only when price is stretched from VWAP/rolling mean and book pressure turns back toward the mean. Dead, high-volatility unstable, and wide-spread regimes stay flat.
- Instrument: VN30F1M futures.
- Default live symbol:
HNXDS:VN30F2605. - Tick size:
0.1. - Contract multiplier:
100000. - Default position size:
6contracts. - Only one position should be managed at a time.
- Default live entry mode:
regime_adaptive. - Project endpoint: paper trading only.
The strict strategy is implemented in
src/strategy/microstructure_momentum.py.
Open a long position when all conditions are true:
- account is flat,
- cooldown is 0,
long_breakoutis true,momentum_gap > 0,momentum_slope > 0,last_price > trend_ema,order_imbalance >= 0.12,- spread is between 1 and 4 ticks.
Open a short position when all conditions are true:
- account is flat,
- cooldown is 0,
short_breakoutis true,momentum_gap < 0,momentum_slope < 0,last_price < trend_ema,order_imbalance <= -0.12,- spread is between 1 and 4 ticks.
Exit conditions:
- ATR stop loss,
- ATR take profit,
- ATR trailing stop,
- momentum failure,
- maximum holding time.
The improved strategy is implemented in
src/strategy/regime_adaptive.py.
No entry is allowed in these regimes:
wide_spread,dead_flat,high_vol_unstable.
Trending long requires:
long_breakout,momentum_gap > 0,momentum_slope > 0,last_price > trend_ema,imbalance_ema >= 0.12,- persistent buying for 3 bars,
volume_z >= 0.25.
Trending short requires:
short_breakout,momentum_gap < 0,momentum_slope < 0,last_price < trend_ema,imbalance_ema <= -0.12,- persistent selling for 3 bars,
volume_z >= 0.25.
Ranging long requires:
price_zscore <= -1.35,vwap_deviation < 0,order_imbalance > 0.
Ranging short requires:
price_zscore >= 1.35,vwap_deviation > 0,order_imbalance < 0.
The project supports two data paths:
-
Research data:
data/raw_ticks.csv, if available.- Existing
data/intraday_features.csv, if it has enough rows. - Sample JSON from
../redis_latest.jsonas fallback.
-
Live paper-trading data:
- Redis market data through
RedisMarketDataClient. - PaperBroker order and FIX logs.
- Redis market data through
The live runner builds 1-minute bars using:
last_price,bid_price_1,bid_quantity_1,ask_price_1,ask_quantity_1,bid_price_2,bid_quantity_2,ask_price_2,ask_quantity_2,bid_price_3,bid_quantity_3,ask_price_3,ask_quantity_3,total_matched_quantity.
The processed feature file is saved as:
data/intraday_features.csv
Key generated features include:
order_imbalance,spread,mid_price,fast_ema,slow_ema,trend_ema,momentum_gap,momentum_slope,atr,rolling_high,rolling_low,long_breakout,short_breakout,volume_delta,volume_z,vwap,price_zscore,vwap_deviation,trend_strength,range_atr_ratio,imbalance_ema.
Create and activate a virtual environment:
python3 -m venv .venv
source .venv/bin/activateOn Windows PowerShell:
python -m venv .venv
.\.venv\Scripts\Activate.ps1Install dependencies:
pip install -r requirements.txtCreate an environment file:
cp .env.example .envFill in the required PaperBroker and market-data credentials.
momentum_microstructure_project/
├─ config/
│ └─ config.yaml
├─ data/
│ ├─ intraday_features.csv
│ ├─ signals.csv
│ ├─ strategy_comparison.csv
│ ├─ optimization_results.csv
│ └─ live_trade_log.csv
├─ logs/
│ ├─ paperbroker_20260423.log
│ ├─ paperbroker_20260424.log
│ ├─ paperbroker_20260427.log
│ ├─ paperbroker_20260428.log
│ ├─ paperbroker_20260429.log
│ ├─ paperbroker_20260504.log
│ └─ paperbroker_20260505.log
├─ src/
│ ├─ data/
│ │ ├─ sample_market_loader.py
│ │ └─ process_intraday.py
│ ├─ strategy/
│ │ ├─ microstructure_momentum.py
│ │ └─ regime_adaptive.py
│ ├─ backtest/
│ │ └─ intraday_backtest.py
│ ├─ papertrade/
│ │ ├─ market_data.py
│ │ └─ papertrade_client.py
│ └─ utils/
│ └─ optimize.py
├─ test/
│ ├─ close_long_5_vn30.py
│ ├─ 01_simple_login.py
│ ├─ 06_market_data_subscribe.py
│ └─ 10_max_placeable.py
├─ live_strategy_runner.py
├─ run_research_pipeline.py
├─ run_papertrade_check.py
├─ requirements.txt
└─ README.md
Run:
python run_research_pipeline.pyThis pipeline:
- loads research market data,
- builds intraday features,
- generates baseline and regime-adaptive signals,
- runs spread-aware and fee-aware backtests,
- compares long, short, and both-side evaluations,
- runs parameter optimization when enough data is available,
- writes a research report to
data/research_report.md.
The backtester is implemented in:
src/backtest/intraday_backtest.py
Backtest assumptions:
- signal from bar
i-1is executed on bari, - buy execution price is ask plus slippage,
- sell execution price is bid minus slippage,
- fee per contract is
20000VND, - slippage is
1tick, - default size is
6contracts, - daily loss limit is
1500000VND, - losing-trade cooldown is
4bars.
Reported metrics:
- total trades,
- net PnL,
- win rate,
- average PnL per trade,
- maximum drawdown,
- profit factor,
- Sharpe-like metric,
- long/short trade breakdown,
- longest losing streak.
At the current project state, the local research dataset is too small for a valid statistical conclusion:
data/intraday_features.csv: 1 data row,data/signals.csv: 1 data row,data/strategy_comparison.csv: 0 trades and 0 net PnL across tested strategies,data/optimization_results.csv: 243 parameter combinations, but all with 0 trades because the dataset has only 1 bar.
Therefore, the research pipeline is implemented, but profitability, Sharpe, drawdown control, and out-of-sample alpha are not yet proven.
The optimization module is implemented in:
src/utils/optimize.py
The grid currently tests:
| Parameter | Values |
|---|---|
fast_ema |
6, 8, 10 |
slow_ema |
18, 21, 26 |
imbalance_threshold |
0.08, 0.12, 0.16 |
breakout_lookback |
15, 20, 30 |
take_profit_atr |
2.0, 2.4, 3.0 |
The optimizer writes:
data/optimization_results.csv
The current optimization output should be interpreted only as a code-path test because the local research dataset is not yet large enough.
Run:
python run_papertrade_check.pyThis checks:
- REST reachability,
- PaperBroker environment variables,
- FIX/socket configuration,
- market-data environment variables.
It does not place orders.
Run:
python live_strategy_runner.pyUseful optional arguments:
python live_strategy_runner.py --symbol HNXDS:VN30F2605
python live_strategy_runner.py --qty 6
python live_strategy_runner.py --start-position 5 --entry-price 2018.3The live runner:
- connects to Redis market data,
- builds rolling 1-minute bars,
- computes features,
- evaluates the regime-adaptive strategy by default,
- checks broker capacity before opening,
- places limit orders through PaperBroker,
- logs order events to
data/live_trade_log.csv, - listens for accepted/rejected order events,
- uses a fee-aware profit guard for exits and reversals.
The live runner uses the following fee logic:
- fee per contract per side:
20000VND, - tick size:
0.1, - contract multiplier:
100000, - tick value:
10000VND per tick per contract, - round-trip fee break-even: 4 ticks,
- default buffer: 2 ticks,
- minimum profit threshold: 6 ticks.
The latest available logs cover:
logs/paperbroker_20260423.log
logs/paperbroker_20260424.log
logs/paperbroker_20260427.log
logs/paperbroker_20260428.log
logs/paperbroker_20260429.log
logs/paperbroker_20260504.log
logs/paperbroker_20260505.log
Across these logs:
| Metric | Value |
|---|---|
| FIX orders sent | 95 |
| Accepted new orders | 61 |
| Rejected orders | 34 |
| Fill events | 44 |
| Unique filled orders | 34 |
| Accepted-to-filled unique order rate | 55.7% |
| Total filled quantity | 116 contracts |
| Buy filled quantity | 67 contracts |
| Sell filled quantity | 49 contracts |
| Total filled notional | ~23.50B VND |
Daily breakdown:
| Date | Sent | Accepted | Fill Events | Filled Qty | Filled Notional |
|---|---|---|---|---|---|
| 2026-04-23 | 1 | 1 | 0 | 0 | 0.00B VND |
| 2026-04-24 | 4 | 0 | 0 | 0 | 0.00B VND |
| 2026-04-27 | 50 | 21 | 0 | 0 | 0.00B VND |
| 2026-04-28 | 28 | 27 | 29 | 58 | ~11.78B VND |
| 2026-04-29 | 2 | 2 | 1 | 5 | ~1.01B VND |
| 2026-05-04 | 3 | 3 | 4 | 16 | ~3.23B VND |
| 2026-05-05 | 7 | 7 | 10 | 37 | ~7.47B VND |
The live intent log is:
data/live_trade_log.csv
Current range:
2026-04-24T10:56:59 to 2026-05-05T14:04:58+07:00
Event counts:
| Event | Count |
|---|---|
signal_skipped_no_capacity |
428 |
open_skipped |
110 |
order_accepted |
56 |
order_rejected |
33 |
open_long |
12 |
open_short |
9 |
Logged strategy-reason signals:
| Signal Branch | Count |
|---|---|
mean_reversion_long |
4 |
mean_reversion_short |
2 |
momentum_sniffing_long |
3 |
momentum_sniffing_short |
2 |
Mean-reversion evidence:
| Branch | Average z-score | Average VWAP deviation | Average imbalance |
|---|---|---|---|
mean_reversion_long |
-1.975 | -1.7475 | 0.2505 |
mean_reversion_short |
1.44 | 0.87 | -0.3087 |
Momentum-sniffing evidence:
| Branch | Average gap | Average slope | Average imbalance EMA | Average volume z |
|---|---|---|---|---|
momentum_sniffing_long |
2.9965 | 0.5083 | 0.4744 | 1.1967 |
momentum_sniffing_short |
-0.5714 | -0.2050 | -0.1701 | 1.00 |
These metrics show that the live strategy is producing decisions from the intended feature logic. They do not prove profitability.
The project currently has two important limitations:
-
Research limitation:
- The local historical dataset has only 1 feature row.
- Backtest and optimization outputs are not statistically meaningful yet.
-
Execution limitation:
- Many signals are skipped due to capacity constraints.
- The live runner still needs stronger order-state and broker-position synchronization to avoid repeated open attempts when orders are pending or positions are already filled.
-
Add multi-session VN30F1M historical data to:
data/raw_ticks.csv -
Rerun:
python run_research_pipeline.py
-
Evaluate real in-sample and out-of-sample metrics:
- net PnL,
- Sharpe-like metric,
- maximum drawdown,
- win rate,
- profit factor,
- long/short breakdown,
- parameter robustness.
-
Add post-trade analytics from PaperBroker fills:
- realized PnL,
- average fill price,
- slippage,
- holding time,
- fill rate,
- rejection reasons.
-
Improve live execution state:
- prevent repeated open orders while an order is pending,
- reconcile accepted orders with actual fills,
- sync real broker position before each new entry,
- adjust position size when broker capacity is below the default 6 contracts.
This project successfully implements a VN30F1M paper-trading pipeline based on microstructure-confirmed momentum. The system can process live market data, compute intraday features, generate strict and regime-adaptive signals, send orders through PaperBroker, and record accepted/rejected/fill events.
The current strongest evidence is operational: the system has sent 95 FIX orders, filled 116 contracts, and handled approximately 23.50B VND in paper-trading filled notional. The strategy logic is also visible in logged signal reasons, including mean-reversion and momentum-sniffing branches.
The strategy is not yet proven profitable because historical research data is still insufficient. The next major milestone is to add multi-session VN30F1M intraday data and run a proper in-sample, optimization, and out-of-sample validation workflow.