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.
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:
ClientBase: The foundation class from the Quantstrip module that handles client connectivity and task scheduling. By subclassing ClientBase, your strategy inherits all the connection management and scheduling infrastructure needed to run reliably.calendar_utils: A utility module that provides trading calendar functions, eliminating the need to manually track business days across different exchanges.ib_objects and ib_connect: Quantstrip's synchronous wrapper around Interactive Brokers' TWS API. Rather than wrestling with callback-based asynchronous code, these modules provide a clean, blocking interface that makes order placement and data retrieval straightforward.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.
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.
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:
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.ib.get_historical_data(): Returns a pandas DataFrame with OHLC data—much easier to work with than raw API responses# 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.
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.
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:
ib.get_positions() returns all open positions, which are converted to a dictionary mapping symbols to quantitiesThe same robust error handling ensures clean disconnection regardless of execution success.
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.
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.
In live mode the client will run continuously, evaluating the job at the scheduled time each trading day.
This implementation demonstrates how Quantstrip streamlines systematic strategy development by providing:
ClientBaseThe 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.
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.