|
| 1 | +from collections import deque |
1 | 2 | from datetime import date, timedelta |
2 | 3 | from decimal import Decimal |
3 | 4 |
|
|
13 | 14 |
|
14 | 15 |
|
15 | 16 | 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." |
| 17 | + # sort by expiry to cover near-term risk first |
| 18 | + 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 | + } |
| 22 | + covered: list[Option] = [] |
| 23 | + naked_shorts: list[Option] = [] |
| 24 | + |
| 25 | + # step 1: match spreads |
| 26 | + for short in shorts: |
| 27 | + unmatched = abs(short.quantity) |
| 28 | + # iterate our long inventory to find a cover |
| 29 | + for long, available in longs.items(): |
| 30 | + if unmatched <= 0: |
| 31 | + break |
| 32 | + if available <= 0: |
| 33 | + continue |
| 34 | + # constraint: same type, long expiry >= short expiry |
| 35 | + if long.type == short.type and long.expiration >= short.expiration: |
| 36 | + paired = min(unmatched, available) |
| 37 | + covered.append(short.model_copy(update={"quantity": -paired})) |
| 38 | + covered.append(long.model_copy(update={"quantity": paired})) |
| 39 | + unmatched -= paired |
| 40 | + longs[long] -= paired |
| 41 | + |
| 42 | + # remaining short quantity is naked |
| 43 | + if unmatched > 0: |
| 44 | + naked_shorts.append(short.model_copy(update={"quantity": -unmatched})) |
| 45 | + |
| 46 | + # step 2: match strangles |
| 47 | + strangles: list[tuple[Option, Option]] = [] |
| 48 | + naked_calls = deque(leg for leg in naked_shorts if leg.type == OptionType.CALL) |
| 49 | + naked_puts = deque(leg for leg in naked_shorts if leg.type == OptionType.PUT) |
| 50 | + |
| 51 | + while naked_calls and naked_puts: |
| 52 | + call = naked_calls.popleft() |
| 53 | + put = naked_puts.popleft() |
| 54 | + q = min(abs(call.quantity), abs(put.quantity)) |
| 55 | + strangles.append( |
| 56 | + ( |
| 57 | + call.model_copy(update={"quantity": -q}), |
| 58 | + put.model_copy(update={"quantity": -q}), |
| 59 | + ) |
39 | 60 | ) |
40 | | - return _calculate_margin_spread(legs) |
| 61 | + # handle remaining quantity |
| 62 | + if rem := abs(call.quantity) - q: |
| 63 | + naked_calls.appendleft(call.model_copy(update={"quantity": -rem})) |
| 64 | + if rem := abs(put.quantity) - q: |
| 65 | + naked_puts.appendleft(put.model_copy(update={"quantity": -rem})) |
| 66 | + |
| 67 | + # all unmatched options at this point go here |
| 68 | + naked = list(naked_calls) |
| 69 | + naked.extend(naked_puts) |
| 70 | + naked.extend( |
| 71 | + leg.model_copy(update={"quantity": q}) for leg, q in longs.items() if q |
| 72 | + ) |
| 73 | + |
| 74 | + # step 3: calculate totals |
| 75 | + total = MarginRequirements(cash_requirement=ZERO, margin_requirement=ZERO) |
| 76 | + if covered: |
| 77 | + total += _calculate_margin_spread(covered) |
| 78 | + for call, put in strangles: |
| 79 | + total += _calculate_margin_short_strangle([call, put], underlying) |
| 80 | + for leg in naked: |
| 81 | + if leg.quantity > 0: |
| 82 | + total += _calculate_margin_long_option(leg) |
| 83 | + else: |
| 84 | + total += _calculate_margin_short_option(leg, underlying) |
| 85 | + |
| 86 | + return total |
41 | 87 |
|
42 | 88 |
|
43 | 89 | def _calculate_margin_long_option(option: Option) -> MarginRequirements: |
|
0 commit comments