occ-symbol 0.1.0__tar.gz
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.
- occ_symbol-0.1.0/.gitignore +9 -0
- occ_symbol-0.1.0/LICENSE +21 -0
- occ_symbol-0.1.0/PKG-INFO +108 -0
- occ_symbol-0.1.0/README.md +83 -0
- occ_symbol-0.1.0/pyproject.toml +49 -0
- occ_symbol-0.1.0/src/occ_symbol/__init__.py +95 -0
- occ_symbol-0.1.0/src/occ_symbol/py.typed +0 -0
- occ_symbol-0.1.0/tests/test_occ_symbol.py +130 -0
occ_symbol-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Murphy
|
|
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.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: occ-symbol
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Parse, validate, and build OCC option symbols — SPY260825C00500000 ⇄ {underlying, expiration, right, strike}. Zero dependencies, stdlib only.
|
|
5
|
+
Project-URL: Homepage, https://github.com/chiefsmurph/occ-symbol
|
|
6
|
+
Project-URL: Repository, https://github.com/chiefsmurph/occ-symbol
|
|
7
|
+
Project-URL: Issues, https://github.com/chiefsmurph/occ-symbol/issues
|
|
8
|
+
Author-email: John Murphy <chiefsmurph@gmail.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: derivatives,finance,occ,option-parser,option-symbol,options,options-trading,osi,trading
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# occ-symbol
|
|
27
|
+
|
|
28
|
+
**Parse, validate, and build OCC option symbols.** A tiny, dependency-free
|
|
29
|
+
Python module (stdlib only) that turns the broker-agnostic listed-option string
|
|
30
|
+
(the OSI symbol) into structured parts — and back again.
|
|
31
|
+
|
|
32
|
+
> Python port of the [`occ-symbol`](https://github.com/chiefsmurph/occ-symbol)
|
|
33
|
+
> TypeScript package. Same behavior, same test cases.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from occ_symbol import parse_occ_symbol, format_occ_symbol, is_occ_symbol, OccParts
|
|
37
|
+
|
|
38
|
+
parse_occ_symbol("SPY260825C00500000")
|
|
39
|
+
# OccParts(underlying='SPY', expiration='2026-08-25', right='call', strike=500.0)
|
|
40
|
+
|
|
41
|
+
format_occ_symbol(OccParts(underlying="SPY", expiration="2026-08-25", right="call", strike=500))
|
|
42
|
+
# "SPY260825C00500000"
|
|
43
|
+
|
|
44
|
+
is_occ_symbol("SPY260825C00500000") # True
|
|
45
|
+
is_occ_symbol("SPY") # False
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## The format
|
|
49
|
+
|
|
50
|
+
An OCC (a.k.a. OSI) option symbol packs four fields into one string:
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
SPY260825C00500000
|
|
54
|
+
└┬┘└──┬─┘│└───┬──┘
|
|
55
|
+
│ │ │ └── strike × 1000, 8 digits, zero-padded (00500000 = 500.000)
|
|
56
|
+
│ │ └─────── C = call, P = put
|
|
57
|
+
│ └────────── expiration YYMMDD (20YY)
|
|
58
|
+
└─────────────── underlying / root
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Install
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install occ-symbol
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
| Function | Description |
|
|
70
|
+
|---|---|
|
|
71
|
+
| `parse_occ_symbol(symbol)` | Parse a symbol into `OccParts`, or `None` if it isn't a valid OCC string. Tolerates space-padded roots (Schwab-style `"AAPL 240119C00150000"`). |
|
|
72
|
+
| `format_occ_symbol(parts)` | Build a canonical (unpadded) OCC symbol from `OccParts`. The inverse of `parse_occ_symbol`. |
|
|
73
|
+
| `is_occ_symbol(symbol)` | `True` if the string is a valid OCC option symbol (padded or not). |
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
from dataclasses import dataclass
|
|
77
|
+
from typing import Literal
|
|
78
|
+
|
|
79
|
+
OptionRight = Literal["call", "put"]
|
|
80
|
+
|
|
81
|
+
@dataclass(frozen=True)
|
|
82
|
+
class OccParts:
|
|
83
|
+
underlying: str
|
|
84
|
+
expiration: str # 'YYYY-MM-DD'
|
|
85
|
+
right: OptionRight
|
|
86
|
+
strike: float
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`parse_occ_symbol` and `format_occ_symbol` round-trip: parsing a symbol and
|
|
90
|
+
formatting the result yields the canonical symbol, and formatting parts then
|
|
91
|
+
parsing them returns the original parts.
|
|
92
|
+
|
|
93
|
+
## Scope & limitations
|
|
94
|
+
|
|
95
|
+
- Two-digit years are interpreted as `20YY`, matching current listed-option conventions.
|
|
96
|
+
- `format_occ_symbol` emits the canonical **unpadded** root; space-padded input is normalized on the round trip.
|
|
97
|
+
- Strikes are scaled by 1000 (rounded to the nearest thousandth), so fractional strikes down to a tenth of a cent are preserved.
|
|
98
|
+
|
|
99
|
+
## Develop
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
pip install -e ".[dev]"
|
|
103
|
+
pytest
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## License
|
|
107
|
+
|
|
108
|
+
MIT © John Murphy
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# occ-symbol
|
|
2
|
+
|
|
3
|
+
**Parse, validate, and build OCC option symbols.** A tiny, dependency-free
|
|
4
|
+
Python module (stdlib only) that turns the broker-agnostic listed-option string
|
|
5
|
+
(the OSI symbol) into structured parts — and back again.
|
|
6
|
+
|
|
7
|
+
> Python port of the [`occ-symbol`](https://github.com/chiefsmurph/occ-symbol)
|
|
8
|
+
> TypeScript package. Same behavior, same test cases.
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from occ_symbol import parse_occ_symbol, format_occ_symbol, is_occ_symbol, OccParts
|
|
12
|
+
|
|
13
|
+
parse_occ_symbol("SPY260825C00500000")
|
|
14
|
+
# OccParts(underlying='SPY', expiration='2026-08-25', right='call', strike=500.0)
|
|
15
|
+
|
|
16
|
+
format_occ_symbol(OccParts(underlying="SPY", expiration="2026-08-25", right="call", strike=500))
|
|
17
|
+
# "SPY260825C00500000"
|
|
18
|
+
|
|
19
|
+
is_occ_symbol("SPY260825C00500000") # True
|
|
20
|
+
is_occ_symbol("SPY") # False
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## The format
|
|
24
|
+
|
|
25
|
+
An OCC (a.k.a. OSI) option symbol packs four fields into one string:
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
SPY260825C00500000
|
|
29
|
+
└┬┘└──┬─┘│└───┬──┘
|
|
30
|
+
│ │ │ └── strike × 1000, 8 digits, zero-padded (00500000 = 500.000)
|
|
31
|
+
│ │ └─────── C = call, P = put
|
|
32
|
+
│ └────────── expiration YYMMDD (20YY)
|
|
33
|
+
└─────────────── underlying / root
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install occ-symbol
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## API
|
|
43
|
+
|
|
44
|
+
| Function | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `parse_occ_symbol(symbol)` | Parse a symbol into `OccParts`, or `None` if it isn't a valid OCC string. Tolerates space-padded roots (Schwab-style `"AAPL 240119C00150000"`). |
|
|
47
|
+
| `format_occ_symbol(parts)` | Build a canonical (unpadded) OCC symbol from `OccParts`. The inverse of `parse_occ_symbol`. |
|
|
48
|
+
| `is_occ_symbol(symbol)` | `True` if the string is a valid OCC option symbol (padded or not). |
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from dataclasses import dataclass
|
|
52
|
+
from typing import Literal
|
|
53
|
+
|
|
54
|
+
OptionRight = Literal["call", "put"]
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class OccParts:
|
|
58
|
+
underlying: str
|
|
59
|
+
expiration: str # 'YYYY-MM-DD'
|
|
60
|
+
right: OptionRight
|
|
61
|
+
strike: float
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`parse_occ_symbol` and `format_occ_symbol` round-trip: parsing a symbol and
|
|
65
|
+
formatting the result yields the canonical symbol, and formatting parts then
|
|
66
|
+
parsing them returns the original parts.
|
|
67
|
+
|
|
68
|
+
## Scope & limitations
|
|
69
|
+
|
|
70
|
+
- Two-digit years are interpreted as `20YY`, matching current listed-option conventions.
|
|
71
|
+
- `format_occ_symbol` emits the canonical **unpadded** root; space-padded input is normalized on the round trip.
|
|
72
|
+
- Strikes are scaled by 1000 (rounded to the nearest thousandth), so fractional strikes down to a tenth of a cent are preserved.
|
|
73
|
+
|
|
74
|
+
## Develop
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
pip install -e ".[dev]"
|
|
78
|
+
pytest
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT © John Murphy
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "occ-symbol"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Parse, validate, and build OCC option symbols — SPY260825C00500000 ⇄ {underlying, expiration, right, strike}. Zero dependencies, stdlib only."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "John Murphy", email = "chiefsmurph@gmail.com" }]
|
|
13
|
+
keywords = [
|
|
14
|
+
"occ",
|
|
15
|
+
"option-symbol",
|
|
16
|
+
"options",
|
|
17
|
+
"options-trading",
|
|
18
|
+
"osi",
|
|
19
|
+
"option-parser",
|
|
20
|
+
"derivatives",
|
|
21
|
+
"finance",
|
|
22
|
+
"trading",
|
|
23
|
+
]
|
|
24
|
+
classifiers = [
|
|
25
|
+
"Development Status :: 4 - Beta",
|
|
26
|
+
"Intended Audience :: Financial and Insurance Industry",
|
|
27
|
+
"Intended Audience :: Developers",
|
|
28
|
+
"License :: OSI Approved :: MIT License",
|
|
29
|
+
"Operating System :: OS Independent",
|
|
30
|
+
"Programming Language :: Python :: 3",
|
|
31
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
32
|
+
"Topic :: Office/Business :: Financial :: Investment",
|
|
33
|
+
"Typing :: Typed",
|
|
34
|
+
]
|
|
35
|
+
dependencies = []
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/chiefsmurph/occ-symbol"
|
|
39
|
+
Repository = "https://github.com/chiefsmurph/occ-symbol"
|
|
40
|
+
Issues = "https://github.com/chiefsmurph/occ-symbol/issues"
|
|
41
|
+
|
|
42
|
+
[project.optional-dependencies]
|
|
43
|
+
dev = ["pytest>=7"]
|
|
44
|
+
|
|
45
|
+
[tool.hatch.build.targets.wheel]
|
|
46
|
+
packages = ["src/occ_symbol"]
|
|
47
|
+
|
|
48
|
+
[tool.pytest.ini_options]
|
|
49
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""OCC option-symbol parsing and building.
|
|
2
|
+
|
|
3
|
+
The broker-agnostic listed-option format, also known as the OSI symbol::
|
|
4
|
+
|
|
5
|
+
SPY260825C00500000 = <ROOT><YYMMDD><C|P><STRIKE*1000, 8 digits, zero-padded>
|
|
6
|
+
|
|
7
|
+
e.g. ``SPY260825C00500000`` -> ``SPY``, ``2026-08-25``, call, strike 500.
|
|
8
|
+
|
|
9
|
+
The layout is: an alphabetic underlying/root, a 6-digit YYMMDD expiration (the
|
|
10
|
+
2-digit year is interpreted as 20YY), a single ``C``/``P`` for call/put, and an
|
|
11
|
+
8-digit strike price scaled by 1000 (so ``00500000`` = 500.000,
|
|
12
|
+
``00500500`` = 500.5).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import re
|
|
18
|
+
from dataclasses import dataclass
|
|
19
|
+
from typing import Literal, Optional
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"OptionRight",
|
|
23
|
+
"OccParts",
|
|
24
|
+
"is_occ_symbol",
|
|
25
|
+
"parse_occ_symbol",
|
|
26
|
+
"format_occ_symbol",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
OptionRight = Literal["call", "put"]
|
|
30
|
+
|
|
31
|
+
_OCC_RE = re.compile(r"^([A-Z]+)(\d{6})([CP])(\d{8})$")
|
|
32
|
+
_WHITESPACE_RE = re.compile(r"\s+")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class OccParts:
|
|
37
|
+
"""The four structured fields packed into an OCC/OSI option symbol."""
|
|
38
|
+
|
|
39
|
+
underlying: str
|
|
40
|
+
expiration: str # 'YYYY-MM-DD'
|
|
41
|
+
right: OptionRight
|
|
42
|
+
strike: float
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_occ_symbol(symbol: str) -> bool:
|
|
46
|
+
"""Return ``True`` if ``symbol`` is a valid OCC option symbol (padded or not).
|
|
47
|
+
|
|
48
|
+
Some brokers space-pad the root to 6 chars (Schwab:
|
|
49
|
+
``"AAPL 240119C00150000"``); whitespace is stripped so the same matcher
|
|
50
|
+
handles both padded and unpadded forms.
|
|
51
|
+
"""
|
|
52
|
+
return _OCC_RE.match(_WHITESPACE_RE.sub("", symbol)) is not None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def parse_occ_symbol(symbol: str) -> Optional[OccParts]:
|
|
56
|
+
"""Parse a symbol into :class:`OccParts`, or ``None`` if it isn't valid.
|
|
57
|
+
|
|
58
|
+
Tolerates space-padded roots (Schwab-style ``"AAPL 240119C00150000"``).
|
|
59
|
+
"""
|
|
60
|
+
m = _OCC_RE.match(_WHITESPACE_RE.sub("", symbol))
|
|
61
|
+
if m is None:
|
|
62
|
+
return None
|
|
63
|
+
underlying, ymd, cp, strike_str = m.group(1), m.group(2), m.group(3), m.group(4)
|
|
64
|
+
strike = int(strike_str) / 1000
|
|
65
|
+
return OccParts(
|
|
66
|
+
underlying=underlying,
|
|
67
|
+
expiration=f"20{ymd[0:2]}-{ymd[2:4]}-{ymd[4:6]}",
|
|
68
|
+
right="call" if cp == "C" else "put",
|
|
69
|
+
strike=strike,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def format_occ_symbol(parts: OccParts) -> str:
|
|
74
|
+
"""Build a canonical (unpadded) OCC symbol from parts.
|
|
75
|
+
|
|
76
|
+
The inverse of :func:`parse_occ_symbol`. Strikes are scaled by 1000 and
|
|
77
|
+
rounded to the nearest thousandth, matching the OSI convention.
|
|
78
|
+
"""
|
|
79
|
+
ymd = parts.expiration[2:4] + parts.expiration[5:7] + parts.expiration[8:10]
|
|
80
|
+
cp = "C" if parts.right == "call" else "P"
|
|
81
|
+
strike_str = str(_round_half_away_from_zero(parts.strike * 1000)).zfill(8)
|
|
82
|
+
return f"{parts.underlying}{ymd}{cp}{strike_str}"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _round_half_away_from_zero(value: float) -> int:
|
|
86
|
+
"""Match JavaScript ``Math.round`` semantics (round half toward +infinity).
|
|
87
|
+
|
|
88
|
+
Python's built-in ``round`` uses banker's rounding, which would disagree
|
|
89
|
+
with the JS reference implementation on exact ``*.5`` thousandths. Strikes
|
|
90
|
+
are never negative in practice, so half-up and half-away-from-zero coincide;
|
|
91
|
+
we implement half toward +infinity to mirror ``Math.round`` exactly.
|
|
92
|
+
"""
|
|
93
|
+
import math
|
|
94
|
+
|
|
95
|
+
return math.floor(value + 0.5)
|
|
File without changes
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Ported from occ-symbol/src/index.test.ts — same fixtures and expected values."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from occ_symbol import (
|
|
6
|
+
OccParts,
|
|
7
|
+
format_occ_symbol,
|
|
8
|
+
is_occ_symbol,
|
|
9
|
+
parse_occ_symbol,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# --- parseOccSymbol ---------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
def test_parses_a_standard_occ_call():
|
|
16
|
+
assert parse_occ_symbol("SMR260919C00009000") == OccParts(
|
|
17
|
+
underlying="SMR",
|
|
18
|
+
expiration="2026-09-19",
|
|
19
|
+
right="call",
|
|
20
|
+
strike=9,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_parses_a_put_with_a_fractional_strike():
|
|
25
|
+
assert parse_occ_symbol("SPY260825P00500500") == OccParts(
|
|
26
|
+
underlying="SPY",
|
|
27
|
+
expiration="2026-08-25",
|
|
28
|
+
right="put",
|
|
29
|
+
strike=500.5,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_tolerates_a_space_padded_root_schwab_style():
|
|
34
|
+
assert parse_occ_symbol("AAPL 240119C00150000") == OccParts(
|
|
35
|
+
underlying="AAPL",
|
|
36
|
+
expiration="2024-01-19",
|
|
37
|
+
right="call",
|
|
38
|
+
strike=150,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_returns_none_for_a_plain_equity_symbol():
|
|
43
|
+
assert parse_occ_symbol("AAPL") is None
|
|
44
|
+
assert is_occ_symbol("AAPL") is False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_is_occ_symbol_detects_occ_strings_padded_or_not():
|
|
48
|
+
assert is_occ_symbol("SMR260919C00009000") is True
|
|
49
|
+
assert is_occ_symbol("AAPL 240119C00150000") is True
|
|
50
|
+
assert is_occ_symbol("SMR") is False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# --- formatOccSymbol --------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
def test_builds_a_standard_occ_call():
|
|
56
|
+
assert (
|
|
57
|
+
format_occ_symbol(
|
|
58
|
+
OccParts(
|
|
59
|
+
underlying="SPY",
|
|
60
|
+
expiration="2026-08-25",
|
|
61
|
+
right="call",
|
|
62
|
+
strike=500,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
== "SPY260825C00500000"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_builds_a_put():
|
|
70
|
+
assert (
|
|
71
|
+
format_occ_symbol(
|
|
72
|
+
OccParts(
|
|
73
|
+
underlying="SPY",
|
|
74
|
+
expiration="2026-08-25",
|
|
75
|
+
right="put",
|
|
76
|
+
strike=500,
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
== "SPY260825P00500000"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def test_handles_a_fractional_strike():
|
|
84
|
+
assert (
|
|
85
|
+
format_occ_symbol(
|
|
86
|
+
OccParts(
|
|
87
|
+
underlying="SPY",
|
|
88
|
+
expiration="2026-08-25",
|
|
89
|
+
right="put",
|
|
90
|
+
strike=500.5,
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
== "SPY260825P00500500"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# --- round-trip -------------------------------------------------------------
|
|
98
|
+
|
|
99
|
+
PARTS_CASES = [
|
|
100
|
+
OccParts(underlying="SPY", expiration="2026-08-25", right="call", strike=500),
|
|
101
|
+
OccParts(underlying="SPY", expiration="2026-08-25", right="put", strike=500.5),
|
|
102
|
+
OccParts(underlying="SMR", expiration="2026-09-19", right="call", strike=9),
|
|
103
|
+
OccParts(underlying="AAPL", expiration="2024-01-19", right="call", strike=150),
|
|
104
|
+
]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@pytest.mark.parametrize("parts", PARTS_CASES, ids=[p.underlying for p in PARTS_CASES])
|
|
108
|
+
def test_parse_of_format_deep_equals_parts(parts):
|
|
109
|
+
assert parse_occ_symbol(format_occ_symbol(parts)) == parts
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
SYM_CASES = [
|
|
113
|
+
"SPY260825C00500000",
|
|
114
|
+
"SPY260825P00500500",
|
|
115
|
+
"SMR260919C00009000",
|
|
116
|
+
"AAPL240119C00150000",
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize("sym", SYM_CASES)
|
|
121
|
+
def test_format_of_parse_equals_the_normalized_symbol(sym):
|
|
122
|
+
parsed = parse_occ_symbol(sym)
|
|
123
|
+
assert parsed is not None
|
|
124
|
+
assert format_occ_symbol(parsed) == sym
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_normalizes_a_space_padded_symbol_through_parse_format():
|
|
128
|
+
parsed = parse_occ_symbol("AAPL 240119C00150000")
|
|
129
|
+
assert parsed is not None
|
|
130
|
+
assert format_occ_symbol(parsed) == "AAPL240119C00150000"
|