margin-estimator 0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ from .margin import calculate_margin
2
+ from .models import ETFType, Option, OptionType, Underlying
3
+
4
+
5
+ __all__ = [
6
+ "ETFType",
7
+ "Option",
8
+ "OptionType",
9
+ "Underlying",
10
+ "calculate_margin",
11
+ ]
@@ -0,0 +1,214 @@
1
+ from datetime import date, timedelta
2
+ from decimal import Decimal
3
+
4
+ from .models import (
5
+ ETFType,
6
+ MarginRequirements,
7
+ Option,
8
+ OptionType,
9
+ Underlying,
10
+ )
11
+
12
+ ZERO = Decimal(0)
13
+
14
+
15
+ def calculate_margin(legs: list[Option], underlying: Underlying) -> MarginRequirements:
16
+ """
17
+ Calculate margin for an arbitrary order according to CBOE's Margin Manual.
18
+ """
19
+ if len(legs) == 1:
20
+ if legs[0].quantity > 0:
21
+ return _calculate_margin_long_option(legs[0])
22
+ return _calculate_margin_short_option(legs[0], underlying)
23
+ if len(legs) == 2 and legs[0].quantity < 0 and legs[1].quantity < 0:
24
+ return _calculate_margin_short_strangle(legs, underlying)
25
+ if len(legs) == 2 and legs[0].expiration != legs[1].expiration:
26
+ short = legs[0] if legs[0].quantity < 0 else legs[1]
27
+ long = legs[1] if legs[1] != short else legs[0]
28
+ if short.expiration > long.expiration:
29
+ margin = _calculate_margin_short_option(short, underlying)
30
+ return margin + _calculate_margin_long_option(long)
31
+ calls = [leg for leg in legs if leg.type == OptionType.CALL]
32
+ puts = [leg for leg in legs if leg.type == OptionType.PUT]
33
+ extra_puts = sum(c.quantity for c in calls)
34
+ extra_calls = sum(p.quantity for p in puts)
35
+ if extra_puts or extra_calls:
36
+ raise Exception(
37
+ "Ratio spreads/complex orders not supported! Try splitting your order into "
38
+ "smaller components and summing the results."
39
+ )
40
+ return _calculate_margin_spread(legs)
41
+
42
+
43
+ def _calculate_margin_long_option(option: Option) -> MarginRequirements:
44
+ """
45
+ Calculate margin for a single long option.
46
+ Source: CBOE Margin Manual
47
+ """
48
+ if option.expiration < date.today() + timedelta(days=90):
49
+ return MarginRequirements(
50
+ # Pay for each put or call in full.
51
+ cash_requirement=option.price * 100 * option.quantity,
52
+ # Pay for each put or call in full.
53
+ margin_requirement=option.price * 100 * option.quantity,
54
+ )
55
+ reduced_requirement = round(option.price * 3 / 4, 2)
56
+ return MarginRequirements(
57
+ # Pay for each put or call in full.
58
+ cash_requirement=option.price * 100 * option.quantity,
59
+ # Listed: 75% of the total cost of the option.
60
+ margin_requirement=reduced_requirement * 100 * option.quantity,
61
+ )
62
+
63
+
64
+ def _calculate_margin_short_option(
65
+ option: Option, underlying: Underlying
66
+ ) -> MarginRequirements:
67
+ """
68
+ Calculate margin for a single short option.
69
+ Source: CBOE Margin Manual
70
+ """
71
+ if option.type == OptionType.PUT:
72
+ otm_distance = max(ZERO, underlying.price - option.strike)
73
+ else:
74
+ otm_distance = max(ZERO, option.strike - underlying.price)
75
+ # broad-based ETFs/indices
76
+ if underlying.etf_type == ETFType.BROAD:
77
+ if option.type == OptionType.PUT:
78
+ minimum = round(
79
+ option.price + option.strike / 10 * underlying.leverage_factor, 2
80
+ )
81
+ base = round(
82
+ option.price
83
+ + underlying.price * 3 / 20 * underlying.leverage_factor
84
+ - otm_distance,
85
+ 2,
86
+ )
87
+ # 100% of option proceeds plus 15% of underlying index value less
88
+ # out-of-the money amount, if any, to a minimum for puts of option
89
+ # proceeds plus 10% of the put’s exercise price.
90
+ margin_requirement = max(minimum, base)
91
+ # Deposit cash or cash equivalents equal to aggregate exercise price
92
+ cash_requirement = (option.strike - option.price) * 100
93
+ else: # OptionType.CALL
94
+ minimum = round(
95
+ option.price + underlying.price / 10 * underlying.leverage_factor, 2
96
+ )
97
+ base = round(
98
+ option.price
99
+ + underlying.price * 3 / 20 * underlying.leverage_factor
100
+ - otm_distance,
101
+ 2,
102
+ )
103
+ # 100% of option proceeds plus 15% of underlying index value less
104
+ # out-of-the money amount, if any, to a minimum for calls of option
105
+ # proceeds plus 10% of the underlying index value.
106
+ margin_requirement = max(minimum, base)
107
+ # Deposit cash or cash equivalents equal to aggregate exercise price
108
+ cash_requirement = (option.strike - option.price) * 100
109
+ # narrow-based ETFs/indices, volatility indices, equities
110
+ else:
111
+ if option.type == OptionType.PUT:
112
+ minimum = round(
113
+ option.price + option.strike / 10 * underlying.leverage_factor, 2
114
+ )
115
+ base = round(
116
+ option.price
117
+ + underlying.price / 5 * underlying.leverage_factor
118
+ - otm_distance,
119
+ 2,
120
+ )
121
+ # 100% of option proceeds plus 20% of underlying security / index value
122
+ # less out-of-the-money amount, if any, to a minimum for puts of option
123
+ # proceeds plus 10% of the put’s exercise price.
124
+ margin_requirement = max(minimum, base)
125
+ # Deposit cash or cash equivalents equal to aggregate exercise price.
126
+ cash_requirement = (option.strike - option.price) * 100
127
+ else: # OptionType.CALL
128
+ minimum = round(
129
+ option.price + underlying.price / 10 * underlying.leverage_factor, 2
130
+ )
131
+ base = round(
132
+ option.price
133
+ + underlying.price / 5 * underlying.leverage_factor
134
+ - otm_distance,
135
+ 2,
136
+ )
137
+ # 100% of option proceeds plus 20% of underlying security / index value
138
+ # less out-of-the-money amount, if any, to a minimum for puts of option
139
+ # proceeds plus 10% of the underlying security/index value.
140
+ margin_requirement = max(minimum, base)
141
+ # Deposit underlying security.
142
+ cash_requirement = (underlying.price - option.price) * 100
143
+ margin_requirement *= 100 * abs(option.quantity)
144
+ return MarginRequirements(
145
+ cash_requirement=cash_requirement,
146
+ margin_requirement=margin_requirement,
147
+ )
148
+
149
+
150
+ def _calculate_margin_short_strangle(
151
+ legs: list[Option], underlying: Underlying
152
+ ) -> MarginRequirements:
153
+ """
154
+ Calculate margin for a short strangle.
155
+ Source: CBOE Margin Manual
156
+ """
157
+ # Deposit an escrow agreement for each option.
158
+ margin1 = _calculate_margin_short_option(legs[0], underlying)
159
+ margin2 = _calculate_margin_short_option(legs[1], underlying)
160
+ # For the same underlying security, short put or short call requirement whichever
161
+ # is greater, plus the option proceeds of the other side.
162
+ if margin1.margin_requirement > margin2.margin_requirement:
163
+ margin_requirement = margin1.margin_requirement + legs[1].price * 100
164
+ else:
165
+ margin_requirement = margin2.margin_requirement + legs[0].price * 100
166
+ margin_requirement *= abs(legs[0].quantity)
167
+ return MarginRequirements(
168
+ cash_requirement=margin1.cash_requirement + margin2.cash_requirement,
169
+ margin_requirement=margin_requirement,
170
+ )
171
+
172
+
173
+ def _calculate_loss_for(leg: Option, price: Decimal) -> Decimal:
174
+ """
175
+ Calculate value at expiration for option at given price.
176
+ """
177
+ if leg.type == OptionType.CALL:
178
+ itm_distance = max(ZERO, price - leg.strike)
179
+ else:
180
+ itm_distance = max(ZERO, leg.strike - price)
181
+ return itm_distance * leg.quantity * 100
182
+
183
+
184
+ def _get_net_credit_or_debit(legs: list[Option]) -> Decimal:
185
+ """
186
+ Calculate total debit/credit paid/collected for the order.
187
+ """
188
+ total = ZERO
189
+ for leg in legs:
190
+ total += leg.quantity * leg.price * 100
191
+ return total
192
+
193
+
194
+ def _calculate_margin_spread(legs: list[Option]) -> MarginRequirements:
195
+ """
196
+ Calculate margin for a credit spread.
197
+ Source: CBOE Margin Manual
198
+ """
199
+ strikes = set(leg.strike for leg in legs)
200
+ pnl = _get_net_credit_or_debit(legs)
201
+ losses = []
202
+ for strike in strikes:
203
+ points = [_calculate_loss_for(leg, strike) for leg in legs]
204
+ losses.append(sum(points)) # type: ignore
205
+ margin_requirement = abs(min(losses)) + pnl
206
+
207
+ return MarginRequirements(
208
+ # deposit and maintain cash or cash equivalents equal to the spread’s maximum
209
+ # potential loss
210
+ cash_requirement=margin_requirement,
211
+ # the lesser of the amount required for the short option(s), or the spread’s
212
+ # maximum potential loss
213
+ margin_requirement=margin_requirement,
214
+ )
@@ -0,0 +1,41 @@
1
+ from datetime import date
2
+ from decimal import Decimal
3
+ from enum import StrEnum
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class ETFType(StrEnum):
9
+ BROAD = "broad-based"
10
+ NARROW = "narrow-based"
11
+ VOLATILITY = "volatility"
12
+
13
+
14
+ class OptionType(StrEnum):
15
+ CALL = "C"
16
+ PUT = "P"
17
+
18
+
19
+ class Option(BaseModel):
20
+ expiration: date
21
+ price: Decimal
22
+ quantity: int
23
+ strike: Decimal
24
+ type: OptionType
25
+
26
+
27
+ class MarginRequirements(BaseModel):
28
+ cash_requirement: Decimal
29
+ margin_requirement: Decimal
30
+
31
+ def __add__(self, other: "MarginRequirements"):
32
+ return MarginRequirements(
33
+ cash_requirement=self.cash_requirement + other.cash_requirement,
34
+ margin_requirement=self.margin_requirement + other.margin_requirement,
35
+ )
36
+
37
+
38
+ class Underlying(BaseModel):
39
+ etf_type: ETFType | None = None
40
+ leverage_factor: Decimal = Decimal(1)
41
+ price: Decimal
File without changes
@@ -0,0 +1,151 @@
1
+ Metadata-Version: 2.3
2
+ Name: margin-estimator
3
+ Version: 0.1
4
+ Summary: Calculate estimated margin requirements for equities, options, futures, and futures options. Based on CBOE and CME margining.
5
+ Project-URL: Homepage, https://github.com/tastyware/margin-estimator
6
+ Author-email: Graeme Holliday <graeme.holliday@pm.me>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2024 tastyware
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+ License-File: LICENSE
29
+ Requires-Python: >=3.11
30
+ Requires-Dist: pydantic>=2.9.2
31
+ Description-Content-Type: text/markdown
32
+
33
+ # margin-estimator
34
+ Calculate estimated margin requirements for equities, options, futures, and futures options. Based on CBOE and CME margining.
35
+
36
+ > [!NOTE]
37
+ > Not all features are available yet, pending further development.
38
+ > Currently, equity/ETF/index options are supported, for any trade
39
+ > type other than ratio spreads, box spreads, and jaze lizards.
40
+ > Contributions welcome!
41
+
42
+ ## Installation
43
+
44
+ ```console
45
+ $ pip install margin_estimator
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ Simply pass a list of legs to the `calculate_margin` function along with an `Underlying` object containing information on the underlying, and you'll get back margin requirement estimates for cash and margin accounts!
51
+
52
+ ```python
53
+ from datetime import date
54
+ from decimal import Decimal
55
+ from margin_estimator import (
56
+ ETFType,
57
+ Option,
58
+ OptionType,
59
+ Underlying,
60
+ calculate_margin,
61
+ )
62
+
63
+ # a SPY iron condor
64
+ # make sure to pass `ETFType.BROAD` for broad-based indices
65
+ underlying = Underlying(price=Decimal("587.88"), etf_type=ETFType.BROAD)
66
+ expiration = date(2024, 12, 20)
67
+ long_put = Option(
68
+ expiration=expiration,
69
+ price=Decimal("4.78"),
70
+ quantity=1,
71
+ strike=Decimal(567),
72
+ type=OptionType.PUT,
73
+ )
74
+ short_put = Option(
75
+ expiration=expiration,
76
+ price=Decimal("5.61"),
77
+ quantity=-1,
78
+ strike=Decimal(572),
79
+ type=OptionType.PUT,
80
+ )
81
+ short_call = Option(
82
+ expiration=expiration,
83
+ price=Decimal("5.23"),
84
+ quantity=-1,
85
+ strike=Decimal(602),
86
+ type=OptionType.CALL,
87
+ )
88
+ long_call = Option(
89
+ expiration=expiration,
90
+ price=Decimal("3.68"),
91
+ quantity=1,
92
+ strike=Decimal(607),
93
+ type=OptionType.CALL,
94
+ )
95
+ margin = calculate_margin(
96
+ [long_put, short_put, long_call, short_call], underlying
97
+ )
98
+ print(margin)
99
+ ```
100
+
101
+ ```python
102
+ >>> cash_requirement=Decimal('262.00') margin_requirement=Decimal('262.00')
103
+ ```
104
+
105
+ For normal equities you can omit the `etf_type` parameter:
106
+
107
+ ```python
108
+ # a short F put
109
+ underlying = Underlying(price=Decimal("11.03"))
110
+ expiration = date(2024, 12, 20)
111
+ put = Option(
112
+ expiration=expiration,
113
+ price=Decimal("0.45"),
114
+ quantity=-1,
115
+ strike=Decimal(11),
116
+ type=OptionType.PUT,
117
+ )
118
+ margin = calculate_margin([put], underlying)
119
+ print(margin)
120
+ ```
121
+
122
+ ```python
123
+ >>> cash_requirement=Decimal('1055.00') margin_requirement=Decimal('263.00')
124
+ ```
125
+
126
+ And for leveraged products, you'll need to pass in the `leverage_factor`:
127
+
128
+ ```python
129
+ # a naked TQQQ call
130
+ underlying = Underlying(
131
+ price=Decimal("77.35"),
132
+ etf_type=ETFType.BROAD,
133
+ leverage_factor=Decimal(3),
134
+ )
135
+ expiration = date(2024, 12, 20)
136
+ call = Option(
137
+ expiration=expiration,
138
+ price=Decimal("4.45"),
139
+ quantity=-1,
140
+ strike=Decimal(80),
141
+ type=OptionType.CALL,
142
+ )
143
+ margin = calculate_margin([call], underlying)
144
+ print(margin)
145
+ ```
146
+
147
+ ```python
148
+ >>> cash_requirement=Decimal('7555.00') margin_requirement=Decimal('3661.00')
149
+ ```
150
+
151
+ Please note that all numbers are baseline estimates based on CBOE/CME guidelines and individual broker margins will likely vary significantly.
@@ -0,0 +1,8 @@
1
+ margin_estimator/__init__.py,sha256=jicOQATyGlqpsGfXsCIoVouspMVHO1HRShIsn3xCzDI,202
2
+ margin_estimator/margin.py,sha256=3RSxHFpWQNPHcD9Mj6YBDeY0dXdYqeWnhuG6KUqH8Gw,8573
3
+ margin_estimator/models.py,sha256=lMjUyL1ufrjXOSgkUUBL3c5bZwOKn3AsFOmGZkZVfF8,892
4
+ margin_estimator/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ margin_estimator-0.1.dist-info/METADATA,sha256=fK7E5YVtF0RMfV1ap64WX31ikrQt8r52-mRE3OQGKCM,4624
6
+ margin_estimator-0.1.dist-info/WHEEL,sha256=1yFddiXMmvYK7QYTqtRNtX66WJ0Mz8PYEiEUoOUUxRY,87
7
+ margin_estimator-0.1.dist-info/licenses/LICENSE,sha256=DxJk7vwH5bPnqpXaMQ06BWBM6BVfBOx8GzFmqpKm8oQ,1066
8
+ margin_estimator-0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.25.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 tastyware
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.