Skip to content

Commit

Permalink
Allow specifying a minimum order price
Browse files Browse the repository at this point in the history
To avoid orders where the commission exceeds the order credit, we can
set a minimum order price, which we default to $0.05. This lets us avoid
1 or 2 penny orders where the commission is more than the credit
received from the order itself.

Also did some refactorings and fixed typings.
  • Loading branch information
brndnmtthws committed Dec 13, 2023
1 parent 280551c commit 61e7351
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 16 deletions.
5 changes: 5 additions & 0 deletions thetagang.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ exchange = "SMART"
# midpoint price if `symbol.<symbol>.adjust_midpoint_after_delay = true`.
price_update_delay = [30, 60]

# Set a minimum order price, to avoid orders where the credit (or debit) is so
# low that it doesn't even cover broker commission. We default to $0.05, but you
# can set this to 0.0 (or comment it out) if you want to permit any order price.
minimum_price = 0.05

[orders.algo]
# By default we use adaptive orders with patient priority which gives reasonable
# results. You can also experiment with TWAP or other options, however the
Expand Down
1 change: 1 addition & 0 deletions thetagang/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ def validate_config(config):
Optional("exchange"): And(str, len),
Optional("algo"): algo_settings,
Optional("price_update_delay"): And([int], lambda p: len(p) == 2),
Optional("minimum_price"): And(float, lambda n: 0 <= n),
},
"option_chains": {
"expirations": And(int, lambda n: 1 <= n),
Expand Down
1 change: 1 addition & 0 deletions thetagang/config_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"orders": {
"exchange": "SMART",
"price_update_delay": [30, 60],
"minimum_price": 0.0,
"algo": {
"strategy": "Adaptive",
"params": [["adaptivePriority", "Patient"]],
Expand Down
32 changes: 22 additions & 10 deletions thetagang/portfolio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Optional

import numpy as np
from ib_insync import Ticker, Trade, util
from ib_insync import PortfolioItem, Position, Ticker, Trade, util
from ib_insync.contract import ComboLeg, Contract, Index, Option, Stock
from ib_insync.order import LimitOrder
from more_itertools import partition
Expand All @@ -23,6 +23,7 @@
count_short_option_positions,
get_higher_price,
get_lower_price,
get_minimum_price,
get_strike_limit,
get_target_calls,
get_target_delta,
Expand Down Expand Up @@ -83,7 +84,7 @@ def get_calls(self, portfolio_positions):
def get_puts(self, portfolio_positions):
return self.get_options(portfolio_positions, "P")

def get_options(self, portfolio_positions, right):
def get_options(self, portfolio_positions: dict[str, list[Position]], right: str):
ret = []
symbols = set(self.get_symbols())
for symbol in portfolio_positions:
Expand Down Expand Up @@ -409,7 +410,7 @@ def filter_positions(self, portfolio_positions):
and item.averageCost != 0
]

def get_portfolio_positions(self):
def get_portfolio_positions(self) -> dict[str, list[PortfolioItem]]:
portfolio_positions = self.ib.portfolio(account=self.account_number)
return portfolio_positions_to_dict(self.filter_positions(portfolio_positions))

Expand Down Expand Up @@ -862,6 +863,7 @@ def write_calls(self, calls):
),
"C",
strike_limit,
minimum_price=get_minimum_price(self.config),
)
except RuntimeError:
console.print_exception()
Expand Down Expand Up @@ -904,6 +906,7 @@ def write_puts(self, puts):
),
"P",
strike_limit,
minimum_price=get_minimum_price(self.config),
)
except RuntimeError:
console.print_exception()
Expand Down Expand Up @@ -1209,9 +1212,10 @@ def roll_positions(
kind = "calls" if right.startswith("C") else "puts"

minimum_price = (
0.0
get_minimum_price(self.config)
if not self.config["roll_when"][kind]["credit_only"]
else midpoint_or_market_price(buy_ticker)
+ get_minimum_price(self.config)
)
preferred_minimum_price = midpoint_or_market_price(buy_ticker)

Expand Down Expand Up @@ -1307,12 +1311,12 @@ def roll_positions(

def find_eligible_contracts(
self,
main_contract,
right,
strike_limit,
main_contract: Contract,
right: str,
strike_limit: Optional[float],
minimum_price: float,
exclude_expirations_before=None,
exclude_exp_strike=None,
minimum_price=0.0,
preferred_minimum_price=None,
target_dte=None,
target_delta=None,
Expand Down Expand Up @@ -1732,6 +1736,7 @@ def vix_calls_should_be_closed() -> (
0,
target_delta=delta,
target_dte=target_dte,
minimum_price=get_minimum_price(self.config),
)
status.start()
price = round(get_lower_price(buy_ticker), 2)
Expand Down Expand Up @@ -1906,7 +1911,9 @@ def make_order() -> tuple[Optional[Ticker], Optional[LimitOrder]]:

console.print(Panel(Group(*to_print), title="Cash management"))

def enqueue_order(self, contract: Contract, order: LimitOrder):
def enqueue_order(self, contract: Optional[Contract], order: LimitOrder):
if not contract:
return
self.orders.append((contract, order))

def submit_orders(self):
Expand Down Expand Up @@ -1995,7 +2002,12 @@ def adjust_prices(self):
ticker, wait_time=self.api_response_wait_time()
):
(contract, order) = (trade.contract, trade.order)
updated_price = round((order.lmtPrice + ticker.midpoint()) / 2.0, 2)
updated_price = min(
[
np.sign(order.lmtPrice) * get_minimum_price(self.config),
round((order.lmtPrice + ticker.midpoint()) / 2.0, 2),
]
)
# Check if the updated price is actually any different
# before proceeding, and make sure the signs match so we
# don't switch a credit to a debit or vice versa.
Expand Down
12 changes: 12 additions & 0 deletions thetagang/thetagang.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ def start(config_path, without_ibc=False):
"=",
f"{config['orders']['algo']['params']}",
)
config_table.add_row(
"",
"Price update delay",
"=",
f"{config['orders']['price_update_delay']}",
)
config_table.add_row(
"",
"Minimum price",
"=",
f"{dfmt(config['orders']['minimum_price'])}",
)

config_table.add_section()
config_table.add_row("[spring_green1]Close option positions")
Expand Down
18 changes: 12 additions & 6 deletions thetagang/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from datetime import datetime
from typing import Optional

from ib_insync import TagValue, util
from ib_insync import PortfolioItem, Position, TagValue, util
from ib_insync.contract import Option

from thetagang.options import option_dte
Expand All @@ -15,7 +15,9 @@ def account_summary_to_dict(account_summary):
return d


def portfolio_positions_to_dict(portfolio_positions):
def portfolio_positions_to_dict(
portfolio_positions: list[Position],
) -> dict[str, list[PortfolioItem]]:
d = dict()
for p in portfolio_positions:
symbol = p.contract.symbol
Expand Down Expand Up @@ -99,7 +101,7 @@ def wait_n_seconds(pred, body, seconds_to_wait, started_at=None):
wait_n_seconds(pred, body, seconds_to_wait, started_at)


def get_higher_price(ticker):
def get_higher_price(ticker) -> float:
# Returns the highest of either the option model price, the midpoint, or the
# market price. The midpoint is usually a bit higher than the IB model's
# pricing, but we want to avoid leaving money on the table in cases where
Expand All @@ -111,14 +113,14 @@ def get_higher_price(ticker):
return midpoint_or_market_price(ticker)


def get_lower_price(ticker):
def get_lower_price(ticker) -> float:
# Same as get_highest_price(), except get the lower price instead.
if ticker.modelGreeks:
return min([midpoint_or_market_price(ticker), ticker.modelGreeks.optPrice])
return midpoint_or_market_price(ticker)


def midpoint_or_market_price(ticker):
def midpoint_or_market_price(ticker) -> float:
# As per the ib_insync docs, marketPrice returns the last price first, but
# we often prefer the midpoint over the last price. This function pulls the
# midpoint first, then falls back to marketPrice() if midpoint is nan.
Expand Down Expand Up @@ -146,7 +148,7 @@ def get_target_delta(config, symbol, right):
return config["target"]["delta"]


def get_strike_limit(config, symbol, right):
def get_strike_limit(config: dict, symbol: str, right: str) -> Optional[float]:
p_or_c = "calls" if right.upper().startswith("C") else "puts"
if (
p_or_c in config["symbols"][symbol]
Expand Down Expand Up @@ -221,3 +223,7 @@ def get_write_threshold_perc(config: dict, symbol: Optional[str], right: str) ->

def algo_params_from(params):
return [TagValue(p[0], p[1]) for p in params]


def get_minimum_price(config: dict) -> float:
return config["orders"].get("minimum_price", 0.0)

0 comments on commit 61e7351

Please sign in to comment.