def check_for_uncovered_positions(self, portfolio_positions): for symbol in portfolio_positions: call_count = max([ 0, count_short_option_positions(symbol, portfolio_positions, "C") ]) stock_count = math.floor( sum([ p.position for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) strike_limit = math.ceil( max([get_strike_limit(self.config, symbol, "C") or 0] + [ p.averageCost for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) target_calls = max([0, stock_count // 100]) maximum_new_contracts = self.config["target"][ "maximum_new_contracts"] calls_to_write = max( [0, min([target_calls - call_count, maximum_new_contracts])]) if calls_to_write > 0: click.secho( f"Need to write {calls_to_write} for {symbol}, capped at {maximum_new_contracts}, at or above strike ${strike_limit} (target_calls={target_calls}, call_count={call_count})", fg="green", ) self.write_calls( symbol, self.get_primary_exchange(symbol), calls_to_write, strike_limit, )
def check_for_uncovered_positions(self, account_summary, portfolio_positions): for symbol in portfolio_positions: call_count = max([ 0, count_short_option_positions(symbol, portfolio_positions, "C") ]) stock_count = math.floor( sum([ p.position for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) strike_limit = math.ceil( max([get_strike_limit(self.config, symbol, "C") or 0] + [ p.averageCost or 0 for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) target_calls = max([0, stock_count // 100]) new_contracts_needed = target_calls - call_count excess_calls = call_count - target_calls if excess_calls > 0: click.secho( f"Warning: {symbol} has {excess_calls} excess covered calls stock_count={stock_count}, call_count={call_count}", fg="yellow", ) maximum_new_contracts = self.get_maximum_new_contracts_for( symbol, self.get_primary_exchange(symbol), account_summary, ) calls_to_write = max( [0, min([new_contracts_needed, maximum_new_contracts])]) if calls_to_write > 0: click.secho( f"Will write {calls_to_write} calls, {new_contracts_needed} needed for {symbol}, capped at {maximum_new_contracts}, at or above strike ${strike_limit} (target_calls={target_calls}, call_count={call_count})", fg="green", ) try: self.write_calls( symbol, self.get_primary_exchange(symbol), calls_to_write, strike_limit, ) except RuntimeError as e: click.echo() click.secho(str(e), fg="red") click.secho( f"Failed to write calls for {symbol}. Continuing anyway...", fg="yellow", )
def start(config): import toml import thetagang.config_defaults as config_defaults with open(config, "r") as f: config = toml.load(f) config = normalize_config(config) validate_config(config) click.secho(f"Config:", fg="green") click.echo() click.secho(f" Account details:", fg="green") click.secho( f" Number = {config['account']['number']}", fg="cyan" ) click.secho( f" Cancel existing orders = {config['account']['cancel_orders']}", fg="cyan", ) click.secho( f" Margin usage = {config['account']['margin_usage']} ({config['account']['margin_usage'] * 100}%)", fg="cyan", ) click.secho( f" Market data type = {config['account']['market_data_type']}", fg="cyan", ) click.echo() click.secho(f" Roll options when either condition is true:", fg="green") click.secho( f" Days to expiry <= {config['roll_when']['dte']} and P&L >= {config['roll_when']['min_pnl']} ({config['roll_when']['min_pnl'] * 100}%)", fg="cyan", ) click.secho( f" P&L >= {config['roll_when']['pnl']} ({config['roll_when']['pnl'] * 100}%)", fg="cyan", ) click.echo() click.secho(f" When contracts are ITM:", fg="green") click.secho( f" Roll puts = {config['roll_when']['puts']['itm']}", fg="cyan", ) click.secho( f" Roll calls = {config['roll_when']['calls']['itm']}", fg="cyan", ) click.echo() click.secho(f" Write options with targets of:", fg="green") click.secho(f" Days to expiry >= {config['target']['dte']}", fg="cyan") click.secho( f" Default delta <= {config['target']['delta']}", fg="cyan" ) if "puts" in config["target"]: click.secho( f" Delta for puts <= {config['target']['puts']['delta']}", fg="cyan", ) if "calls" in config["target"]: click.secho( f" Delta for calls <= {config['target']['calls']['delta']}", fg="cyan", ) click.secho( f" Maximum new contracts = {config['target']['maximum_new_contracts']}", fg="cyan", ) click.secho( f" Minimum open interest = {config['target']['minimum_open_interest']}", fg="cyan", ) click.echo() click.secho(f" Symbols:", fg="green") for s in config["symbols"].keys(): c = config["symbols"][s] c_delta = f"{get_target_delta(config, s, 'C'):.2f}".rjust(4) p_delta = f"{get_target_delta(config, s, 'P'):.2f}".rjust(4) weight = f"{c['weight']:.2f}".rjust(4) weight_p = f"{(c['weight'] * 100):.1f}".rjust(4) strike_limits = "" c_limit = get_strike_limit(config, s, "C") p_limit = get_strike_limit(config, s, "P") if c_limit: strike_limits += f", call strike >= ${c_limit:.2f}" if p_limit: strike_limits += f", put strike <= ${p_limit:.2f}" click.secho( f" {s.rjust(5)} weight = {weight} ({weight_p}%), delta = {p_delta}p, {c_delta}c{strike_limits}", fg="cyan", ) assert ( sum([config["symbols"][s]["weight"] for s in config["symbols"].keys()]) == 1.0 ) click.echo() if config.get("ib_insync", {}).get("logfile"): util.logToFile(config["ib_insync"]["logfile"]) # TWS version is pinned to current stable ibc = IBC(981, **config["ibc"]) def onConnected(): portfolio_manager.manage() ib = IB() ib.connectedEvent += onConnected completion_future = asyncio.Future() portfolio_manager = PortfolioManager(config, ib, completion_future) probeContractConfig = config["watchdog"]["probeContract"] watchdogConfig = config.get("watchdog") del watchdogConfig["probeContract"] probeContract = Contract( secType=probeContractConfig["secType"], symbol=probeContractConfig["symbol"], currency=probeContractConfig["currency"], exchange=probeContractConfig["exchange"], ) watchdog = Watchdog(ibc, ib, probeContract=probeContract, **watchdogConfig) watchdog.start() ib.run(completion_future) watchdog.stop() ibc.terminate()
def roll_positions(self, positions, right, portfolio_positions={}): for position in positions: symbol = position.contract.symbol strike_limit = get_strike_limit(self.config, symbol, right) if right.startswith("C"): strike_limit = math.ceil( max([strike_limit or 0] + [ p.averageCost for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) sell_ticker = self.find_eligible_contracts( symbol, self.get_primary_exchange(symbol), right, strike_limit, excluded_expirations=[ position.contract.lastTradeDateOrContractMonth ], ) self.wait_for_midpoint_price(sell_ticker) quantity = abs(position.position) position.contract.exchange = "SMART" [buy_ticker] = self.ib.reqTickers(position.contract) self.wait_for_midpoint_price(buy_ticker) price = midpoint_or_market_price( buy_ticker) - midpoint_or_market_price(sell_ticker) # Create combo legs comboLegs = [ ComboLeg( conId=position.contract.conId, ratio=1, exchange="SMART", action="BUY", ), ComboLeg( conId=sell_ticker.contract.conId, ratio=1, exchange="SMART", action="SELL", ), ] # Create contract combo = Contract( secType="BAG", symbol=symbol, currency="USD", exchange="SMART", comboLegs=comboLegs, ) # Create order order = LimitOrder( "BUY", quantity, round(price, 2), algoStrategy="Adaptive", algoParams=[TagValue("adaptivePriority", "Patient")], tif="DAY", ) # Submit order trade = self.ib.placeOrder(combo, order) self.orders.append(trade) click.echo() click.secho("Order submitted", fg="green") click.secho(f"{trade}", fg="green")
def check_if_can_write_puts(self, account_summary, portfolio_positions): # Get stock positions stock_positions = [ position for symbol in portfolio_positions for position in portfolio_positions[symbol] if isinstance(position.contract, Stock) ] total_buying_power = math.floor( float(account_summary["NetLiquidation"].value) * self.config["account"]["margin_usage"]) click.echo() click.secho( f"Total buying power: ${total_buying_power:,.0f} at {round(self.config['account']['margin_usage'] * 100, 1)}% margin usage", fg="green", ) click.echo() stock_symbols = dict() for stock in stock_positions: symbol = stock.contract.symbol stock_symbols[symbol] = stock targets = dict() target_additional_quantity = dict() # Determine target quantity of each stock for symbol in self.config["symbols"].keys(): click.secho(f" {symbol}", fg="green") ticker = self.get_ticker_for(symbol, self.get_primary_exchange(symbol)) current_position = math.floor(stock_symbols[symbol].position if symbol in stock_symbols else 0) click.secho( f" Current position quantity: {current_position} shares", fg="cyan", ) targets[symbol] = round( self.config["symbols"][symbol]["weight"] * total_buying_power, 2) click.secho(f" Target value: ${targets[symbol]:,.0f}", fg="cyan") target_quantity = math.floor(targets[symbol] / ticker.marketPrice()) click.secho(f" Target share quantity: {target_quantity:,d}", fg="cyan") # Current number of short puts put_count = count_short_option_positions(symbol, portfolio_positions, "P") target_additional_quantity[symbol] = math.floor(target_quantity - current_position - 100 * put_count) click.secho( f" Net quantity: {target_additional_quantity[symbol]:,d} shares, {target_additional_quantity[symbol] // 100} contracts", fg="cyan", ) click.echo() # Figure out how many additional puts are needed, if they're needed for symbol in target_additional_quantity.keys(): additional_quantity = target_additional_quantity[symbol] // 100 # NOTE: it's possible there are non-standard option contract sizes, # like with futures, but we don't bother handling those cases. # Please don't use this code with futures. if additional_quantity >= 1: maximum_new_contracts = self.config["target"][ "maximum_new_contracts"] puts_to_write = min( [additional_quantity, maximum_new_contracts]) if puts_to_write > 0: strike_limit = get_strike_limit(self.config, symbol, "P") if strike_limit: click.secho( f"Preparing to write additional {puts_to_write} puts to purchase {symbol}, capped at {maximum_new_contracts}, at or below strike ${strike_limit}", fg="cyan", ) else: click.secho( f"Preparing to write additional {puts_to_write} puts to purchase {symbol}, capped at {maximum_new_contracts}", fg="cyan", ) self.write_puts( symbol, self.get_primary_exchange(symbol), puts_to_write, strike_limit, ) return
def roll_positions(self, positions, right, account_summary, portfolio_positions={}): for position in positions: try: symbol = position.contract.symbol strike_limit = get_strike_limit(self.config, symbol, right) if right.startswith("C"): strike_limit = math.ceil( max([strike_limit or 0] + [ p.averageCost for p in portfolio_positions[symbol] if isinstance(p.contract, Stock) ])) sell_ticker = self.find_eligible_contracts( symbol, self.get_primary_exchange(symbol), right, strike_limit, exclude_expirations_before=position.contract. lastTradeDateOrContractMonth, exclude_first_exp_strike=position.contract.strike, ) qty_to_roll = abs(position.position) maximum_new_contracts = self.get_maximum_new_contracts_for( symbol, self.get_primary_exchange(symbol), account_summary, ) from_dte = option_dte( position.contract.lastTradeDateOrContractMonth) roll_when_dte = self.config["roll_when"]["dte"] if from_dte > roll_when_dte: qty_to_roll = min([qty_to_roll, maximum_new_contracts]) position.contract.exchange = "SMART" buy_ticker = self.get_ticker_for(position.contract, midpoint=True) price = midpoint_or_market_price( buy_ticker) - midpoint_or_market_price(sell_ticker) # Create combo legs comboLegs = [ ComboLeg( conId=position.contract.conId, ratio=1, exchange="SMART", action="BUY", ), ComboLeg( conId=sell_ticker.contract.conId, ratio=1, exchange="SMART", action="SELL", ), ] # Create contract combo = Contract( secType="BAG", symbol=symbol, currency="USD", exchange="SMART", comboLegs=comboLegs, ) # Create order order = LimitOrder( "BUY", qty_to_roll, round(price, 2), algoStrategy="Adaptive", algoParams=[TagValue("adaptivePriority", "Patient")], tif="DAY", ) to_dte = option_dte( sell_ticker.contract.lastTradeDateOrContractMonth) from_strike = position.contract.strike to_strike = sell_ticker.contract.strike # Submit order trade = self.ib.placeOrder(combo, order) self.orders.append(trade) click.echo() click.secho( f"Order submitted, current position={abs(position.position)}, qty_to_roll={qty_to_roll}, from_dte={from_dte}, to_dte={to_dte}, from_strike={from_strike}, to_strike={to_strike}, price={round(price,2)}, trade={trade}", fg="green", ) except RuntimeError as e: click.echo() click.secho(str(e), fg="red") click.secho( "Error occurred when trying to roll position. Continuing anyway...", fg="yellow", )