Skip to content

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:

  1. Identifying which asset (stocks or rates) underperformed over the first 16 trading days
  2. Buying the underperformer on day 16
  3. Holding until the last business day of the month
  4. 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 framework
  • db_handler: Database API for position tracking and strategy events
  • settings: Configuration management (separates code from config)
  • calendar_utils: NYSE trading calendar utilities
  • ib_objects: IBKR contract and order wrapper classes
  • IB: 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 ClientBase to 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:

  1. Dynamic Position Sizing: Adjust allocation based on volatility or portfolio size
  2. Multiple Asset Classes: Expand beyond stocks/rates
  3. Limit Orders: Replace market orders with limit orders to reduce slippage
  4. Performance Analytics: Add P&L tracking and reporting
  5. Risk Controls: Maximum position size limits, stop losses
  6. 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