Skip to content

Commit

Permalink
Optionally calculate net shorts
Browse files Browse the repository at this point in the history
In the event we want to use calendar spreads, we can have ThetaGang be a
little more intelligent when calculating whether to write new positions
or roll our current short contracts by computing the net short
contracts. In effect, we count the short-dated contracts and subtract
any longer-dated positions from those, giving us the net position. We
also check that the long strikes are safely higher/lower (put/call) to
avoid any traps.

This can be enabled by setting `write_when.calculate_net_contracts =
true`.
  • Loading branch information
brndnmtthws committed Jan 18, 2024
1 parent 6c219ca commit e668c06
Show file tree
Hide file tree
Showing 9 changed files with 624 additions and 205 deletions.
354 changes: 204 additions & 150 deletions poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ autohooks-plugin-black = ">=22.11,<24.0"
autohooks-plugin-ruff = "^23.10.0"
black = "^23.1.0"
pytest = "^7.2.1"
pytest-watch = "^4.2.0"
ruff = "^0.1.1"

[tool.poetry.urls]
Expand Down
25 changes: 25 additions & 0 deletions thetagang.toml
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,31 @@ credit_only = false
# rolling puts regardless of the total quantity.
has_excess = true

[write_when]
# By default, we ignore long option positions when calculating whether we can
# write new contracts. This, however, limits the ability to utilize spreads. For
# example, we might want to buy some long-dated puts when they're cheap, and
# then we can sell additional short-dated puts (i.e., a calendar spread).
# ThetaGang won't open a calendar spread for you by purchasing longer-dated
# contracts, but if you were to buy some LEAPs yourself, ThetaGang could take
# advantage of those positions when calculating whether to write additional
# contracts.
#
# If you set this value to true, ThetaGang will calculate the net positions by
# including the long contracts in its calculations. It does this by greedily
# matching short positions with long positions such that they cancel each other
# out. ThetaGang will only match on those positions where the long leg has a
# greater DTE, and the strike is closer to the money (i.e., the strike for puts
# must be >=, and the strike for calls must be <=).
#
# IMPORTANT NOTE: ThetaGang doesn't manage long contracts at all, so you
# introduce the risk of having excess short positions when the long contracts
# eventually expire. For the time being, you need to manage the long legs
# yourself (by either closing the short side when the long positions expire, or
# rolling the long positions as they approach expiration). You can avoid rolling
# excess positions with `roll_when.calls/puts.has_excess = false`.
calculate_net_contracts = false

[write_when.calls]
# Optionally, only write calls when the underlying is green
green = true
Expand Down
1 change: 1 addition & 0 deletions thetagang/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ def validate_config(config):
"strikes": And(int, lambda n: 1 <= n),
},
Optional("write_when"): {
Optional("calculate_net_contracts"): bool,
Optional("calls"): {
Optional("green"): bool,
Optional("cap_factor"): And(float, lambda n: 0 <= n <= 1),
Expand Down
1 change: 1 addition & 0 deletions thetagang/config_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"delta": 0.3,
},
"write_when": {
"calculate_net_contracts": False,
"puts": {"red": False},
"calls": {
"green": False,
Expand Down
80 changes: 57 additions & 23 deletions thetagang/portfolio_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from thetagang.util import (
account_summary_to_dict,
algo_params_from,
calculate_net_short_positions,
count_long_option_positions,
count_short_option_positions,
get_higher_price,
Expand Down Expand Up @@ -749,14 +750,17 @@ def check_for_uncovered_positions(self, account_summary, portfolio_positions):
call_actions_table.add_column("Symbol")
call_actions_table.add_column("Action")
call_actions_table.add_column("Detail")
calculate_net_contracts = self.config["write_when"]["calculate_net_contracts"]
to_write = []
symbols = set(self.get_symbols())
for symbol in portfolio_positions:
if symbol not in symbols:
# skip positions we don't care about
continue
short_call_count = max(
[0, count_short_option_positions(symbol, portfolio_positions, "C")]
short_call_count = (
calculate_net_short_positions(symbol, portfolio_positions, "C")
if calculate_net_contracts
else count_short_option_positions(symbol, portfolio_positions, "C")
)
stock_count = math.floor(
sum(
Expand Down Expand Up @@ -985,17 +989,23 @@ def check_if_can_write_puts(self, account_summary, portfolio_positions):
targets = dict()
target_additional_quantity = dict()

calculate_net_contracts = self.config["write_when"]["calculate_net_contracts"]

positions_summary_table = Table(title="Positions summary")
positions_summary_table.add_column("Symbol")
positions_summary_table.add_column("Shares", justify="right")
positions_summary_table.add_column("Short puts", justify="right")
positions_summary_table.add_column("Long puts", justify="right")
positions_summary_table.add_column("Short calls", justify="right")
positions_summary_table.add_column("Long calls", justify="right")
positions_summary_table.add_column("Puts: short", justify="right")
positions_summary_table.add_column("Puts: long", justify="right")
if calculate_net_contracts:
positions_summary_table.add_column("Puts: net short", justify="right")
positions_summary_table.add_column("Calls: short", justify="right")
positions_summary_table.add_column("Calls: long", justify="right")
if calculate_net_contracts:
positions_summary_table.add_column("Calls: net short", justify="right")
positions_summary_table.add_column("Target value", justify="right")
positions_summary_table.add_column("Target share qty", justify="right")
positions_summary_table.add_column("Net shares", justify="right")
positions_summary_table.add_column("Net contracts", justify="right")
positions_summary_table.add_column("Net target shares", justify="right")
positions_summary_table.add_column("Net target contracts", justify="right")

actions = Table(title="Put writing summary")
actions.add_column("Symbol")
Expand All @@ -1022,42 +1032,66 @@ def check_if_can_write_puts(self, account_summary, portfolio_positions):
)

# Current number of puts
short_put_count = count_short_option_positions(
net_short_put_count = short_put_count = count_short_option_positions(
symbol, portfolio_positions, "P"
)
long_put_count = count_long_option_positions(
symbol, portfolio_positions, "P"
)
# Current number of calls
short_call_count = count_short_option_positions(
net_short_call_count = short_call_count = count_short_option_positions(
symbol, portfolio_positions, "C"
)
long_call_count = count_long_option_positions(
symbol, portfolio_positions, "C"
)

if calculate_net_contracts:
net_short_put_count = calculate_net_short_positions(
symbol, portfolio_positions, "P"
)
net_short_call_count = calculate_net_short_positions(
symbol, portfolio_positions, "C"
)

write_only_when_red = self.config["write_when"]["puts"]["red"]

qty_to_write = math.floor(
self.target_quantities[symbol]
- current_position
- 100 * short_put_count
- 100 * net_short_put_count
)
net_target_shares = qty_to_write
net_target_puts = net_target_shares // 100

positions_summary_table.add_row(
symbol,
ifmt(current_position),
ifmt(short_put_count),
ifmt(long_put_count),
ifmt(short_call_count),
ifmt(long_call_count),
dfmt(targets[symbol]),
ifmt(self.target_quantities[symbol]),
ifmt(net_target_shares),
ifmt(net_target_puts),
)
if calculate_net_contracts:
positions_summary_table.add_row(
symbol,
ifmt(current_position),
ifmt(short_put_count),
ifmt(long_put_count),
ifmt(net_short_put_count),
ifmt(short_call_count),
ifmt(long_call_count),
ifmt(net_short_call_count),
dfmt(targets[symbol]),
ifmt(self.target_quantities[symbol]),
ifmt(net_target_shares),
ifmt(net_target_puts),
)
else:
positions_summary_table.add_row(
symbol,
ifmt(current_position),
ifmt(short_put_count),
ifmt(long_put_count),
ifmt(short_call_count),
ifmt(long_call_count),
dfmt(targets[symbol]),
ifmt(self.target_quantities[symbol]),
ifmt(net_target_shares),
ifmt(net_target_puts),
)

def is_ok_to_write_puts(
symbol: str,
Expand Down
Loading

0 comments on commit e668c06

Please sign in to comment.