Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow specifying a minimum order price #339

Merged
merged 2 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
34 changes: 24 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, 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,9 @@ 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[PortfolioItem]], right: str
):
ret = []
symbols = set(self.get_symbols())
for symbol in portfolio_positions:
Expand Down Expand Up @@ -409,7 +412,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 +865,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 +908,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 +1214,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 +1313,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 +1738,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 +1913,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 +2004,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, 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[PortfolioItem],
) -> 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)