Monthly Rebalancing Flow Strategy
Overview
The Monthly Rebalancing Flow strategy is an automated trading bot that exploits predictable institutional rebalancing flows at month-end. The strategy takes advantage of the fact that large institutional funds (pension funds, mutual funds, etc.) typically rebalance their portfolios back to target allocations (e.g., 60/40 stocks/bonds) at the end of each month.
Strategy Rationale
Large institutional investors maintain fixed allocation targets between different asset classes. As prices fluctuate throughout the month, their actual allocations drift away from these targets. At month-end, they must rebalance by:
- Selling the outperformer (the asset that gained more)
- Buying the underperformer (the asset that gained less or declined more)
This creates predictable buying pressure on the underperforming asset in the final days of the month. The strategy aims to front-run these flows by:
- Identifying which asset (stocks or rates) underperformed over the first 16 trading days
- Buying the underperformer on day 16
- Holding until the last business day of the month
- Selling to capture the rebalancing flow premium
Architecture
Plugin System
The strategy is implemented as a Quantstrip client plugin that inherits from the ClientBase class. This architecture allows the Quantstrip system to:
- Automatically register the strategy as a worker client
- Manage the strategy lifecycle (start, stop, error handling)
- Schedule and execute trading jobs
- Provide access to shared resources (database, settings, brokers)
Code Breakdown
1. Imports and Setup
from quantstrip import ClientBase, db_handler, settings
import logging
from Utils import calendar_utils as cu
from IBKR.ib_objects import ib_order, ib_contract
from IBKR.ib_connect import IB
Dependencies:
ClientBase: Base class providing the plugin frameworkdb_handler: Database API for position tracking and strategy eventssettings: Configuration management (separates code from config)calendar_utils: NYSE trading calendar utilitiesib_objects: IBKR contract and order wrapper classesIB: Connection manager for Interactive Brokers API
2. Configuration Loading
ALLOCATION = settings.get_by_path('Settings/Strategy Settings/Monthly Flow Rebalancing/allocation')
RATE_SYMBOL = settings.get_by_path('Settings/Strategy Settings/Monthly Flow Rebalancing/rate_symbol')
STOCK_SYMBOL = settings.get_by_path('Settings/Strategy Settings/Monthly Flow Rebalancing/stock_symbol')
Purpose: Loads strategy parameters from external JSON configuration:
ALLOCATION: Dollar amount to allocate to the trade (e.g., $10,000)RATE_SYMBOL: Ticker for the interest rate instrument (e.g., "TLT" for 20+ Year Treasury ETF)STOCK_SYMBOL: Ticker for the equity instrument (e.g., "SPY" for S&P 500 ETF)
Benefit: Configuration changes don't require code modifications, making the strategy more maintainable and flexible.
3. Client Class Definition
class Client(ClientBase):
""" A trading client executing the stock/interest rate rebalancing trade
"""
def __init__(self, *args):
super().__init__()
self.display_name = "Rebalancing Flow"
self.scheduler.every().day.at("15:45", "America/New_York").do(self.job)
Components:
- Inheritance: Extends
ClientBaseto integrate with Quantstrip framework - Display Name: Human-readable identifier for logging and monitoring
- Scheduler: Sets up daily execution at 3:45 PM Eastern Time (15 minutes before market close)
- This timing allows for:
- Final price discovery of the day
- Sufficient time to execute orders before close
- Reduced intraday volatility impact
Main Trading Logic
4. Job Execution Method
The job() method contains the core trading logic and is executed daily by the scheduler.
A. Contract Initialization
def job(self):
rate_contract = ib_contract(RATE_SYMBOL)
stock_contract = ib_contract(STOCK_SYMBOL)
Creates IBKR contract objects for both instruments being traded.
5. Opening Position Logic (Day 16)
if cu.business_day_number_today(calendar = "NYSE") == 16:
Trigger: Executes on the 16th trading day of the month.
A. Position Safety Check
current_state = db_handler.get_last_strategy_event(strategy_id = 1)
if current_state and current_state.get("position") != 0:
logger.info(f"Unexpected position found in strategy {self.display_name} (should be zero)")
Purpose: Ensures no position is already open (defensive programming to prevent double-entry).
B. Historical Data Retrieval
with IB() as ib:
rate_price = ib.get_historical_data(rate_contract, "", "16 D", "1 day")
stock_price = ib.get_historical_data(stock_contract, "", "16 D", "1 day")
Data Retrieved: - Last 16 trading days of daily close prices - Returns Pandas DataFrames with OHLC data
Context Manager: The with IB() block ensures proper connection handling (automatic connect/disconnect).
C. Performance Calculation
rate_perf = rate_price.iloc[0]["close"] / rate_price.iloc[-1]["close"]
stock_perf = stock_price.iloc[0]["close"] / stock_price.iloc[-1]["close"]
Formula: Performance = (Most Recent Close) / (Close 16 Days Ago)
- Value > 1.0: Asset gained
- Value < 1.0: Asset declined
- Value = 1.0: No change
Example: - Stock: 100 → 110 = 1.10 (gained 10%) - Rate: 50 → 48 = 0.96 (declined 4%) - Result: Buy stocks (the underperformer in relative terms)
D. Position Sizing
rate_qty = ALLOCATION / rate_price.iloc[0]["close"]
stock_qty = ALLOCATION / stock_price.iloc[0]["close"]
Calculation: Shares = Dollar Allocation / Current Price
Example: - Allocation: $10,000 - Stock Price: $450 - Quantity: 10,000 / 450 = 22 shares
E. Trade Selection
if rate_perf > stock_perf:
contract, quantity = (stock_contract, int(stock_qty))
else:
contract, quantity = (rate_contract, int(rate_qty))
Logic: - If rates outperformed stocks → Buy stocks - If stocks outperformed rates → Buy rates - Always buy the underperformer
F. Order Execution
ib.placeOrder(ib.get_next_order_id(),
contract,
ib_order(quantity, "Rebalancing", "MKT"))
Order Details: - Order Type: Market order (immediate execution) - Label: "Rebalancing" (for tracking) - Order ID: Auto-generated unique identifier
G. Database Recording
db_handler.insert_strategy_event(
strategy_id = 1,
broker_id = 1,
symbol = contract.symbol,
position = quantity,
event_type = "OPEN"
)
Purpose: Records the position in the database for: - Position tracking - Performance monitoring - Risk management - Audit trail
6. Closing Position Logic (Last Business Day)
if cu.is_last_business_day_of_month(calendar = "NYSE"):
Trigger: Executes on the last trading day of the month.
A. Position Retrieval
current_state = db_handler.get_last_strategy_event(strategy_id = 1)
if not current_state or current_state.get("position") == 0:
logger.info(f"Could not find any open position associated with {self.display_name}")
Safety Check: Ensures a position exists before attempting to close.
B. Position Details Extraction
symbol = current_state.get("symbol")
quantity = current_state.get("position")
Retrieves the symbol and quantity from the last strategy event.
C. Close Order Execution
with IB() as ib:
ib.placeOrder(ib.get_next_order_id(),
ib_contract(symbol),
ib_order(-quantity, "Rebalancing", "MKT"))
Key Detail: Negative quantity (-quantity) creates a sell order to close the long position.
D. Database Update
db_handler.insert_strategy_event(
strategy_id = 1,
broker_id = 1,
symbol = symbol,
position = 0,
event_type = "CLOSE"
)
Records the position closure with position = 0.
7. Client Shutdown
self.stop_client()
Purpose: Gracefully stops the client after job execution. The Quantstrip framework will restart it for the next trading day.
Error Handling
The strategy implements comprehensive error handling:
try:
# Trading logic
except Exception as e:
logger.error(e)
Benefits: - Prevents strategy crashes from affecting other system components - Logs errors for debugging and monitoring - Allows the system to continue operating even if one execution fails
Database Schema
The strategy uses the strategy_event table to track positions:
| Field | Type | Description |
|---|---|---|
| strategy_id | INTEGER | Strategy identifier (1 for this strategy) |
| broker_id | INTEGER | Broker identifier (1 for IBKR) |
| symbol | TEXT | Ticker symbol of the position |
| position | REAL | Current position size (0 = flat) |
| event_type | TEXT | "OPEN" or "CLOSE" |
| create_time | TIMESTAMP | Event timestamp |
Position Lifecycle:
Day 16: INSERT (symbol="SPY", position=22, event_type="OPEN")
↓
Last Day: INSERT (symbol="SPY", position=0, event_type="CLOSE")
Configuration Example
Example JSON configuration structure:
{
"Settings": {
"Strategy Settings": {
"Monthly Flow Rebalancing": {
"allocation": 10000,
"rate_symbol": "TLT",
"stock_symbol": "SPY"
}
}
}
}
Trading Timeline
Day 1-15: No action (observation period)
Day 16: Calculate performance, open position
Day 17-29: Hold position
Last Day: Close position, realize P&L
Key Features
1. Defensive Programming
- Position state validation before opening/closing
- Error handling with logging
- Database-backed position tracking
2. Configuration Separation
- External JSON configuration
- No hardcoded parameters
- Easy strategy tuning without code changes
3. Clean Resource Management
- Context managers for broker connections
- Automatic cleanup on errors
- Graceful shutdown
4. Audit Trail
- All trades recorded in database
- Strategy events tracked
- Position history maintained
Risks and Considerations
Market Risks
- Execution Risk: Market orders may experience slippage
- Timing Risk: Front-running may not always work if rebalancing flows are delayed
- Liquidity Risk: Large positions may face execution challenges
Operational Risks
- Connection Failures: Network or broker API issues could prevent trades
- Timing Errors: Incorrect trading day calculations could trigger early/late
- Position Reconciliation: Database and actual positions could diverge
Mitigations
- Error handling and logging
- Position validation checks
- Market orders for immediate execution
- Database-backed state management
Performance Monitoring
To monitor strategy performance, query the database:
-- View all strategy events
SELECT * FROM strategy_event
WHERE strategy_id = 1
ORDER BY create_time DESC;
-- Calculate monthly P&L
SELECT
strftime('%Y-%m', create_time) as month,
symbol,
event_type,
position
FROM strategy_event
WHERE strategy_id = 1;
Future Enhancements
Potential improvements to consider:
- Dynamic Position Sizing: Adjust allocation based on volatility or portfolio size
- Multiple Asset Classes: Expand beyond stocks/rates
- Limit Orders: Replace market orders with limit orders to reduce slippage
- Performance Analytics: Add P&L tracking and reporting
- Risk Controls: Maximum position size limits, stop losses
- Backtesting Framework: Historical simulation before live trading
Troubleshooting
Position Not Opening
- Check if it's exactly the 16th trading day
- Verify broker connection is active
- Check account has sufficient buying power
- Review error logs for exceptions
Position Not Closing
- Verify it's the last business day of the month
- Check database has an open position record
- Ensure broker connection is working
- Review error logs
Unexpected Position State
- Query database:
SELECT * FROM strategy_event WHERE strategy_id = 1 - Compare with broker's actual positions
- Manual intervention may be required to reconcile
Support and Resources
- Calendar Utilities: Uses NYSE trading calendar for accurate day counting
- Database Handler: Full API documentation in Quantstrip docs
- IBKR Wrapper: Interactive Brokers API wrapper documentation
- Settings Management: JSON path-based configuration access