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.
@@ -0,0 +1,9 @@
1
+ # Python build & test artifacts
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .pytest_cache/
8
+ .venv/
9
+ venv/
@@ -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"