A quantitative trading system for cryptocurrency markets built for the HK University Web3 Quant Trading Hackathon Competition. This autonomous bot generates trading signals from derivatives positioning and volume data, then executes spot trades on the Roostoo Mock Exchange.
- Project Structure
- Data Sources and Rationale
- Backtesting and Strategy Selection
- Signal Modeling: Momentum, Reversion, and Dispersal
- Portfolio Construction and Risk Management
- Execution Pipeline and Error Handling
- Monitoring and Logging
web3_quant/
├── trade.py # Main live trading loop
├── quanttrading/ # Core trading infrastructure
│ ├── config_manager.py # Strategy configuration and weight management
│ ├── strategies.py # Base strategy interface (BaseStrat)
│ ├── binance_fetcher.py # Factor data loading and remote API integration
│ ├── position_engine.py # Signal-to-position calculation and leverage control
│ ├── roostoo.py # Roostoo Mock Exchange API client
│ ├── monitor.py # Logging and Telegram alerting
│ ├── symbol_manager.py # Symbol info and precision handling
│ └── helper.py, log.py, tg.py
├── user_strategies/ # Concrete strategy implementations
│ ├── strat_001.py # Open Interest (OI) strategies
│ ├── strat_002.py # Derivatives positioning strategies
│ ├── strat_003.py # Market sentiment strategies
│ ├── strat_004.py # Positioning flow strategies
│ ├── strat_005.py # Volume-based strategies
│ └── strat_006.py # Market microstructure strategies
├── user_data/
│ ├── data/ # Standardized factor CSVs and configurations
│ ├── logs/ # Runtime logs
│ └── monitor/ # CSV logs for signals, positions, trades
├── df_final.csv # Final strategy configuration
└── requirements.txt # Python dependencies
trade.py: The main entry point that wires together all strategies, the position engine, the monitor, and the Roostoo client. Runs the live trading loop every ~5 minutes.quanttrading/: Core infrastructure modules handling configuration, signal generation, position sizing, leverage control, API communication, and monitoring.user_strategies/: Six strategy classes implementing factor-specific signal logic for derivatives positioning, market sentiment, and flow dynamics.user_data/data/: Standardized time-series CSVs for each factor in normalized{t, ts, value}format, plus the final strategy configurationdf_final.csv.df_final.csv: Final strategy configuration output from offline research/backtesting, including optimized factor weights and selected parameter sets.
Our system uses hourly derivatives and market microstructure data as predictive factors. All factors are normalized into a standard {t, ts, value} schema and cached locally in user_data/data/.
We employ a multi-factor approach combining:
-
Futures Open Interest (OI)
- Source: Derivatives market open interest data
- Rationale: Open interest reflects aggregate leverage and conviction in the derivatives market. Sudden changes can signal positioning imbalances or crowding, which often precede spot price reversals. When OI drops significantly while price remains stable, it suggests deleveraging that may create mean-reversion opportunities.
- Application: Primary factor for detecting positioning extremes and market structure shifts.
-
Derivatives Positioning Metrics
- Source: Various derivatives market positioning indicators
- Rationale: Captures institutional and informed trader positioning dynamics. Extreme positioning levels (whether long or short biased) often precede mean-reversion as crowded trades unwind. These metrics provide insight into market sentiment and potential inflection points.
- Application: Complementary signals for detecting sentiment extremes and positioning crowding.
-
Market Microstructure Signals
- Source: Volume flow and order book dynamics
- Rationale: Short-horizon flow patterns can indicate directional conviction and momentum shifts. Unusual volume patterns often precede price movements as they reflect changing supply/demand dynamics.
- Application: Used for both momentum-following and contrarian strategies depending on the specific signal characteristics.
- Data ingestion: Factor time series are collected from proprietary data sources and processed into standardized format.
- Normalization: Each factor CSV contains
{t, ts, value}columns, wheretis Unix epoch,tsis ISO timestamp, andvalueis the factor reading. - Runtime updates:
BinanceFetcher._load_seriesloads cached CSVs, checks freshness, and fetches recent data from a remote API service to keep factors up-to-date.
Offline research and backtesting were conducted to select robust strategies and parameter sets. The research process mirrors the production code path to ensure consistency.
-
Factor extraction: For each factor, we extract the historical time series from the standardized CSVs.
-
Parameter grid search: For each factor, we grid-search over:
short_window: rolling window for the moving averagelong_window: rolling window for standard deviation or rank calculationthreshold: the dispersal parameter controlling signal intensity (see Signal Modeling)
-
Signal simulation: We apply the exact same signal equations used in
user_strategies/(z-score or rank-based rules) to generate exposure signals. -
Performance metrics: We simulate trades against the underlying coin's hourly returns and compute:
- Sharpe Ratio: risk-adjusted return
- Sortino Ratio: downside risk-adjusted return
- Calmar Ratio: return/max drawdown
- Max Drawdown: peak-to-trough decline
- Turnover: signal change frequency
-
Robust selection: Rather than choosing a single best parameter set (which risks overfitting), we select a diversified ensemble of parameterizations per factor that show stable performance across different time periods. This ensemble approach is encoded in
df_final.csv, where each row represents one parameter set for one factor-coin pair.
The final strategy configuration contains columns:
strategy: unique identifier (e.g.,bttp_bnb_001)weight: PyPortfolioOpt-derived factor weight (see Portfolio Construction)sym: target coin (e.g.,BNB,BTC)dir: direction type—Rfor reversion strategies,Mfor momentum strategiesm: model type—Bfor z-score,Rfor rank-basedres: resolution (e.g.,1h)factor_id: factor familyp: parameter list[short_window, long_window, threshold]
Multiple rows with the same factor_id represent the ensemble of parameter sets for that factor. At runtime, these are grouped into one StratConfig per factor, and signals are averaged across parameter sets to produce a robust aggregate signal.
All strategies inherit from BaseStrat (quanttrading/strategies.py), which provides a unified interface for signal generation.
- Load factor data:
fetch_alpha()callsBinanceFetcherto load the relevant factor time series. - Compute signals per parameter set: For each parameter set in
StratConfig.params, callcalculate_signal_df(df, params, model)to generate a signal column. - Aggregate signals: Average all parameter-specific signals to produce a robust aggregate signal (range: 0 to 1).
- Persist and alert: Save signal history to
user_data/data/{id-name}.csvand send Telegram updates when signals change.
We implement two complementary statistical models for generating signals from factor time series:
- Normalizes factor values using rolling statistics
- Generates signals based on deviation from recent mean behavior
- Applied for both reversion strategies (fade extremes) and momentum strategies (follow extremes)
- Uses configurable lookback windows to capture different time horizons
- Computes percentile rankings over rolling historical windows
- Identifies tail events in the factor distribution
- Provides robustness to outliers and non-stationary factor dynamics
- Complementary to standardization approach for signal diversification
The dispersal parameter (threshold) controls signal sensitivity and conviction level:
- Higher thresholds: Fewer, higher-conviction signals triggered only by extreme factor readings
- Lower thresholds: More frequent signals capturing moderate factor movements
- Ensemble approach: We use multiple threshold values per factor to diversify signal timing and reduce overfitting to a single parameterization
Each strategy configuration in df_final.csv specifies [short_window, long_window, threshold] parameters that were selected through robust backtesting.
| Class | Type | Description |
|---|---|---|
Strat001 |
OI Reversion | Open interest reversion: buy when OI is anomalously low (log-transformed) |
Strat002 |
Positioning Reversion | Derivatives positioning reversion based on crowd positioning |
Strat003 |
Sentiment Reversion | Market sentiment reversion using positioning ratios |
Strat004 |
Flow Reversion | Positioning flow reversion strategies |
Strat005 |
Flow Momentum | Market flow momentum strategies |
Strat006 |
Microstructure | Market microstructure-based signals |
We use a multi-stage process to convert factor signals into dollar positions with strict risk controls.
-
Strategy configuration (
config_manager.create_config_from_df):- Rows in
df_final.csvare grouped byfactor_idinto oneStratConfigper factor. - Each
StratConfigholds:final_weight: normalized weight (sum across all factors = 1)params: list of parameter sets to ensemblesymbol: target cointype: reversion or momentum
- Rows in
-
Target sizing (
position_engine._calculate_target_amount):target_usd = signal * BALANCE * final_weight target_amount = target_usd / anchor_price
signal∈ [0, 1] from the strategy'sgenerate_signal()methodBALANCE = 150,000USD (hackathon starting capital)final_weightis the factor's portfolio weightanchor_priceis a reference price fromsymbol_manager
-
Aggregation by symbol (
position_engine.aggregate_target_amount_by_symbol):- Multiple strategies may target the same coin.
- We sum all per-strategy targets to get a net target per coin.
-
Leverage control:
- Calculate total notional exposure:
leverage = sum(target_amount * last_price) / BALANCE - If
leverage > MAX_LEVERAGE = 0.99, scale down all positions proportionally viaposition_engine.deleverage. - The deleverage function respects each symbol's precision constraints (rounding to correct decimal places).
- Calculate total notional exposure:
To determine final_weight for each factor, we used PyPortfolioOpt during the research phase:
-
Factor return matrix: Construct a matrix of hourly returns for each factor strategy (rows = timestamps, columns = factors).
-
Mean-variance optimization: Use
pypfopt.EfficientFrontieror similar to solve:maximize expected_return - λ * variance subject to: - long-only: weights ≥ 0 - budget: sum(weights) = 1 - box constraints: 0 ≤ weight_i ≤ max_weight - L2 regularization to avoid concentration -
Output: Optimal factor weights are computed and saved as the
weightcolumn in the strategy configuration. -
Normalization:
config_manager.compute_weightsnormalizes these weights intofinal_weightso they sum to 1 when loaded into runtime configs.
- Global leverage cap:
MAX_LEVERAGE = 0.99ensures we never exceed ~1x notional exposure. - Minimum order size:
MIN_ORDER_USD = 2.0inroostoo.tradeprevents dust orders and excessive turnover. - Long-only constraint: All signals are ∈ [0, 1], representing long exposure only (no shorting).
- Automatic flattening: When a strategy's signal drops to zero,
position_engine.calculate_delta_amountcomputes a sell order to close the position. - Precision rounding: All order quantities are rounded to the exchange's
amount_precisionto avoid rejection.
The main loop runs every ~5 minutes (300 seconds) and executes the following steps:
-
Load configuration:
df = pd.read_csv('user_data/data/df_final.csv') configs = config_manager.create_config_from_df(df) weights = config_manager.get_weights(df)
-
Instantiate strategies:
- Create 54 strategy objects (one per factor-coin-param ensemble) with a shared
BinanceFetcherandMonitor.
- Create 54 strategy objects (one per factor-coin-param ensemble) with a shared
-
Generate signals:
signals = position_engine.calculate_signals(strats)
- Each strategy fetches its factor data, computes the aggregate signal, and returns a value ∈ [0, 1].
- Signals are logged to
user_data/monitor/signals.csvand sent to Telegram.
-
Compute target positions:
target_amount_by_strat = position_engine.calculate_target_amount_by_strat( strats, signals, BALANCE, symbols_info ) target_amount_by_symbol = position_engine.aggregate_target_amount_by_symbol( target_amount_by_strat )
-
Check and enforce leverage:
leverage_ref = position_engine.calculate_leverage_ref( target_amount_by_symbol, symbols_info, BALANCE ) if leverage_ref > MAX_LEVERAGE: target_amount_by_symbol = position_engine.deleverage( target_amount_by_symbol, leverage_ref, MAX_LEVERAGE, symbols_info )
-
Fetch current positions:
current_positions = roostoo.get_current_postions()
-
Calculate deltas and trade:
delta_amounts = position_engine.calculate_delta_amount( target_amount_by_symbol, current_positions ) success_trades, error_trades = roostoo.trade( delta_amounts, binance_fetcher, last_prices )
-
Log results:
- All signals, targets, leverage, deltas, positions, and trades are logged to CSV and Telegram.
-
Sleep:
- Wait 300 seconds (5 minutes) before the next iteration, respecting the hackathon's low-frequency constraint.
The base Roostoo API wrapper (provided by competition organizers) handles HMAC-SHA256 authentication and standard endpoints. We've implemented a custom trade() function that adds intelligent execution logic:
def trade(amount_by_symbol: dict[str, float], binance_fetcher: BinanceFetcher,
last_prices: dict[str, float] | None = None) -> tuple[list[dict], list[dict]]Key features of our implementation:
-
Smart order pre-filtering:
- Fetch real-time prices for all symbols (using cached
last_priceswhen available to reduce API calls) - Calculate USD notional value:
usd_value = abs(amount) * last_price - Skip orders below
MIN_ORDER_USD = $2.0threshold to avoid:- Dust trades with high relative transaction costs
- Excessive turnover from small rebalances
- API rejection for minimum order size violations
- Fetch real-time prices for all symbols (using cached
-
Delta-based execution:
- Positive delta →
place_order(symbol, 'BUY', amount) - Negative delta →
place_order(symbol, 'SELL', abs(amount)) - Zero delta → skip (no rebalancing needed)
- Automatically handles position flattening when signals go to zero
- Positive delta →
-
Comprehensive error handling:
- Separate tracking of
success_tradesanderror_trades - Each trade result logged to CSV and sent to Telegram with full details:
- Success: status, symbol, amount, side, type, filled price
- Error: error message for debugging
- Continues trading other symbols even if one order fails
- Separate tracking of
-
Post-trade validation:
- After all orders executed, call
get_pending_count()to verify no orders remain pending - Alert via Telegram if any orders stuck (indicates API issues or configuration problems)
- Ensures all deltas resolved before next loop iteration
- After all orders executed, call
-
Rate limiting compliance:
- 2-second sleep between orders to respect API rate limits
- Total execution time scales with number of active positions
- Well within competition's 1 trade/minute maximum constraint
This implementation ensures robust execution with minimal slippage, proper error recovery, and full observability for live trading.
The BinanceFetcher manages external data dependencies with fallback mechanisms:
- Load cached CSV: Read local factor CSV from
user_data/data/. - Check freshness: Use
helper.is_data_latest()to verify the last timestamp is recent. - Fetch updates: If stale, call remote API service to fetch last 30 days of data.
- Validate and merge: Ensure last bar is closed, concatenate with cache, deduplicate, and resave.
The critical fetch_all_last_prices() method has a robust fallback:
- Primary path: Fetch current prices from remote API for all symbols.
- On success: Save to
user_data/last_prices.csvwith timestamp. - On failure:
- Load fallback CSV.
- Check age: if older than 30 minutes, abort and alert.
- Use fallback prices and send Telegram warning.
This ensures the bot can survive temporary API outages without placing orders at stale prices.
The Monitor class (quanttrading/monitor.py) provides comprehensive observability:
Every loop iteration, the monitor writes timestamped rows to:
signals.csv: Raw signal values for all 54 strategiestarget_amount_by_strat.csv: Target coin amounts per strategytarget_amount_by_symbol.csv: Aggregated target amounts per symbolleverage.csv: Real, reference, and deleveraged leverage valuesdelta_amounts.csv: Order deltas (buy/sell amounts)current_positions.csv: Positions held on Roostoo after tradescurrent_balance.csv: USD value of each position + total balancesuccess_trades.csv: Successfully filled orderserror_trades.csv: Failed orders
These logs provide a complete audit trail for ex-post analysis.
The monitor sends real-time alerts for:
- Signal updates: When a strategy's signal changes (new factor reading crossed a threshold)
- Weighted exposures by strategy: Grouped view of net exposure per factor (emoji-coded: 🟢 long, 🔴 short, ⚪ flat)
- Weighted exposures by symbol: Grouped view of net exposure per coin
- Trade confirmations: Order status, pair, side, price, quantity
- Trade errors: Error messages from failed orders
- Pending order warnings: If any orders remain unfilled after trading
- Data fetch failures: Alerts when falling back to cached prices or when cache is too old
Telegram monitoring allows real-time oversight without SSH access to the cloud server.
The CSV logs are designed for easy post-processing:
- Join trade logs with Roostoo API history to verify fills and compute realized PnL.
- Aggregate balance snapshots to compute equity curve.
- Calculate metrics: Sharpe, Sortino, Calmar ratios, max drawdown, turnover.
- Analyze signal contributions: Decompose PnL by strategy or symbol to identify top contributors.