Skip to content

Commit be70be4

Browse files
committed
add code to cancel opposing quantities
1 parent 4530dea commit be70be4

File tree

4 files changed

+100
-58
lines changed

4 files changed

+100
-58
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "margin-estimator"
7-
version = "0.2"
7+
version = "0.3"
88
description = "Calculate estimated margin requirements for equities options, based on CBOE margining."
99
readme = "README.md"
1010
requires-python = ">=3.11"

src/margin_estimator/margin.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@
1414

1515

1616
def calculate_margin(legs: list[Option], underlying: Underlying) -> MarginRequirements:
17+
# step 0: cancel out opposing positions
18+
netted: dict[tuple[date, Decimal, str], Option] = {}
19+
for leg in legs:
20+
key = (leg.expiration, leg.strike, leg.type.value)
21+
if key in netted:
22+
netted[key] = netted[key].model_copy(
23+
update={"quantity": netted[key].quantity + leg.quantity}
24+
)
25+
else:
26+
netted[key] = leg
27+
legs = [leg for leg in netted.values() if leg.quantity != 0]
28+
1729
# sort by expiry to cover near-term risk first
1830
shorts = sorted(leg for leg in legs if leg.quantity < 0)
19-
longs = {
20-
leg: leg.quantity for leg in sorted([leg for leg in legs if leg.quantity > 0])
21-
}
31+
longs = {leg: leg.quantity for leg in sorted(l for l in legs if l.quantity > 0)}
2232
covered: list[Option] = []
2333
naked_shorts: list[Option] = []
2434

test.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
Underlying,
1010
calculate_margin,
1111
)
12+
from margin_estimator.margin import ZERO
13+
from margin_estimator.models import MarginRequirements
1214

1315

1416
def test_long_option():
@@ -914,3 +916,32 @@ def test_complex_multi_strategy_position():
914916
underlying,
915917
)
916918
assert total == vertical + orphan_spread + strangle + mismatched_spread
919+
920+
921+
def test_cancel_opposing_positions():
922+
underlying = Underlying(price=500)
923+
long = Option(
924+
expiration=date.today(), price=10, quantity=1, strike=510, type=OptionType.CALL
925+
)
926+
short = Option(
927+
expiration=date.today(), price=10, quantity=-1, strike=510, type=OptionType.CALL
928+
)
929+
nothing = MarginRequirements(cash_requirement=ZERO, margin_requirement=ZERO)
930+
assert calculate_margin([long, short], underlying) == nothing
931+
assert calculate_margin([long, short, short, long], underlying) == nothing
932+
assert (
933+
calculate_margin([short, long, long, short, long, short], underlying) == nothing
934+
)
935+
936+
937+
def test_cancel_some_opposing_positions():
938+
underlying = Underlying(price=500)
939+
long = Option(
940+
expiration=date.today(), price=10, quantity=1, strike=510, type=OptionType.CALL
941+
)
942+
short = Option(
943+
expiration=date.today(), price=10, quantity=-2, strike=510, type=OptionType.CALL
944+
)
945+
assert calculate_margin([long, short], underlying) == calculate_margin(
946+
[short.model_copy(update={"quantity": -1})], underlying
947+
)

0 commit comments

Comments
 (0)