Overview

This trading client implements a systematic rebalancing strategy that capitalizes on end-of-month portfolio flows from wealth managers and institutional investors. As detailed in Kris Longmore's analysis, large asset managers typically rebalance their 60/40 stock-bond portfolios at month-end to maintain target allocations. This creates predictable selling pressure on the outperforming asset and buying pressure on the underperformer.

Strategy Logic:

The strategy anticipates that the underperforming asset will benefit from systematic buying as portfolio managers sell winners to buy losers, returning their portfolios to balanced allocations.


Part 1: General Setup

Imports and Configuration

from quantstrip import ClientBase
import logging
from Utils import calendar_utils as cu
from IBKR.ib_objects import ib_order, ib_contract
from IBKR.ib_connect import ib

The implementation begins by importing the necessary Quantstrip components:

Client Class Structure

ALLOCATION = 100000

class Client(ClientBase):
    """ A trading client executing the SPY/TLT rebalancing trade
    """
    def __init__(self, *args):
        super().__init__()
        self.display_name = "Rebalancing Flow"

The Client class extends ClientBase, inheriting the built-in scheduler and connection management. The ALLOCATION constant defines the dollar amount to deploy per trade ($100,000 in this example).

Scheduler Configuration:
The inherited self.scheduler uses a simple, intuitive syntax for job scheduling.

self.scheduler.every().day.at("15:45", "America/New_York").do(self.job)

This runs the strategy logic once daily at 3:45 PM ET, ensuring all trading logic is evaluated and orders are sent in time before market close.


Part 2: Entry Rule - Opening the Position

Identifying the 16th Trading Day

if cu.business_day_number_today(calendar = "NYSE") == 16:

The entry logic triggers on the 16th trading day of each month using Quantstrip's calendar_utils. This function automatically accounts for weekends, holidays, and exchange-specific calendars—no need to maintain your own holiday databases or write complex date arithmetic.

Fetching Historical Data and Calculating Performance

with IB() as ib:
  # Get historical prices for last 16 days
  TLT = ib.get_historical_data(TLT_contract, "", "16 D", "1 day")
  SPY = ib.get_historical_data(SPY_contract, "", "16 D", "1 day")

  # Calculate performance
  TLT_perf    = TLT.iloc[0]["close"]/TLT.iloc[-1]["close"]
  TLT_qty     = ALLOCATION / TLT.iloc[0]["close"]

  SPY_perf    = SPY.iloc[0]["close"]/SPY.iloc[-1]["close"]
  SPY_qty     = ALLOCATION / SPY.iloc[0]["close"]

The synchronous IB wrapper simplifies what would otherwise be complex asynchronous API calls:

  1. with IB() as ib: Context manager that establishes connection to TWS/IB Gateway. The client ID for the connection is generated from a pool that recycles used IDs once connection is closed.
  2. ib.get_historical_data(): Returns a pandas DataFrame with OHLC data—much easier to work with than raw API responses
  3. Performance calculation: Simple ratio of latest close to first close gives the 16-day return
  4. Position sizing: Calculates quantity based on the exit price (last close in the dataset)

Placing the Entry Order

# Open position
if TLT_perf > SPY_perf:
    contract, quantity = (SPY_contract, int(SPY_qty)) 
else:
    contract, quantity = (TLT_contract, int(TLT_qty))
    
ib.placeOrder(ib.get_next_order_id(), 
              contract, 
              ib_order(quantity, "Rebalancing", "MKT"))

The strategy selects the underperformer (if TLT outperformed, buy SPY; otherwise buy TLT) and places a market order. The ib_order() helper function constructs the order object with clean, readable parameters: quantity, tag for tracking ("Rebalancing"), and order type ("MKT").

Error Handling:
The entire entry logic is wrapped in try-except-finally blocks, ensuring the IB connection is always properly closed via ib.disconnect_client(), even if an exception occurs.


Part 3: Exit Rule - Closing the Position

Detecting Month-End

if cu.is_last_business_day_of_month(calendar = "NYSE"):

The exit trigger uses another calendar utility that identifies the last trading day of the month, accounting for the same holiday and weekend complexities.

Retrieving Current Positions and Exiting

if ib.connect_client(client_id = 1):
    positions = {p['contract'].symbol : p['position'] for p in ib.get_positions()} 
    logger.info(f"Positions {positions}")
    
    if "TLT" in positions:
        ib.placeOrder(ib.get_next_order_id(), 
                      TLT_contract, 
                      ib_order(-int(positions["TLT"]), "Rebalancing", "MKT"))
                      
    if "SPY" in positions: 
        ib.placeOrder(ib.get_next_order_id(), 
                      SPY_contract, 
                      ib_order(-int(positions["SPY"]), "Rebalancing", "MKT"))

The exit logic:

  1. Fetches current positions: ib.get_positions() returns all open positions, which are converted to a dictionary mapping symbols to quantities
  2. Checks for each symbol: Determines if either TLT or SPY is held
  3. Places closing orders: For any held position, places a market order with negative quantity (selling the position)

The same robust error handling ensures clean disconnection regardless of execution success.


Running the script

Open the Python editor and create a new file called "rebalancing_flow" in the Test folder. Paste the complete code into the editor and save it.

TWS Configuration

To simulate the client, you need to have the Interactive Brokers TWS or IB Gateway running and properly configured to accept API connections. For testing purposes it is recommended to use the TWS to get visual confirmation that orders are placed as expected. The client will connect to the standard paper trading port (7497).

In test mode you typically want the client to run only once and then exit. You can do this by overriding the scheduler:


#self.scheduler.every().day.at("15:45", "America/New_York").do(self.job)
self.scheduler.every(1).second.do(self.job)
      

At the end of the job function add the following line to stop the scheduler after one run:

  self.stop_client()

This will make the client run the job once after one second and then exit.

To force the opening and closing trades, replace the calendar utility functions with "True" in the if statements.


#if cu.business_day_number_today(calendar = "NYSE") == 16:
if True:
        

Overriding the entry rule by forcing it to "True" will create and send an order to open a position in the underperforming asset.

TWS Configuration

In live mode the client will run continuously, evaluating the job at the scheduled time each trading day.


Summary

This implementation demonstrates how Quantstrip streamlines systematic strategy development by providing:

The entire strategy—from scheduling to data retrieval to order placement—fits in under 70 lines of readable code. Whether you're implementing sophisticated quantitative strategies or simple rebalancing rules, Quantstrip's infrastructure lets you focus on strategy logic rather than plumbing.


Limitations

Though the script contains all the necessary parts to automate the rebalancing flow trading strategy, there are no internal revcords created that allows you to manage the trade-life cycle. This may be acceptable for a single trade strategy, but for more complex strategies it is recommended to implement proper trade management using Quantstrip's trade life-cycle datamodel.