tickerforge 0.1.3__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.
Files changed (32) hide show
  1. tickerforge-0.1.3/.github/workflows/ci.yml +84 -0
  2. tickerforge-0.1.3/.github/workflows/release.yml +73 -0
  3. tickerforge-0.1.3/.gitignore +211 -0
  4. tickerforge-0.1.3/.pre-commit-config.yaml +29 -0
  5. tickerforge-0.1.3/LICENSE +21 -0
  6. tickerforge-0.1.3/PKG-INFO +181 -0
  7. tickerforge-0.1.3/README.md +157 -0
  8. tickerforge-0.1.3/VERSION +1 -0
  9. tickerforge-0.1.3/docs/early-closes-integration.md +45 -0
  10. tickerforge-0.1.3/docs/first-working-version.md +52 -0
  11. tickerforge-0.1.3/docs/is_trading_session_flow.svg +89 -0
  12. tickerforge-0.1.3/docs/smart-ticker-parsing.md +127 -0
  13. tickerforge-0.1.3/docs/spec-integration.md +37 -0
  14. tickerforge-0.1.3/pyproject.toml +74 -0
  15. tickerforge-0.1.3/scripts/check_versions.py +37 -0
  16. tickerforge-0.1.3/tests/test_b3_calendar_golden.py +135 -0
  17. tickerforge-0.1.3/tests/test_contract_cycles.py +38 -0
  18. tickerforge-0.1.3/tests/test_expiration_rules.py +32 -0
  19. tickerforge-0.1.3/tests/test_spec_loading.py +73 -0
  20. tickerforge-0.1.3/tests/test_ticker_generation.py +19 -0
  21. tickerforge-0.1.3/tests/test_ticker_parsing.py +358 -0
  22. tickerforge-0.1.3/tickerforge/__init__.py +12 -0
  23. tickerforge-0.1.3/tickerforge/calendars.py +63 -0
  24. tickerforge-0.1.3/tickerforge/contract_cycle.py +28 -0
  25. tickerforge-0.1.3/tickerforge/expiration_rules.py +159 -0
  26. tickerforge-0.1.3/tickerforge/models.py +181 -0
  27. tickerforge-0.1.3/tickerforge/month_codes.py +31 -0
  28. tickerforge-0.1.3/tickerforge/py.typed +1 -0
  29. tickerforge-0.1.3/tickerforge/schedule.py +205 -0
  30. tickerforge-0.1.3/tickerforge/spec_loader.py +175 -0
  31. tickerforge-0.1.3/tickerforge/ticker_generator.py +95 -0
  32. tickerforge-0.1.3/tickerforge/ticker_parser.py +255 -0
@@ -0,0 +1,84 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ env:
12
+ COVERAGE_PYTHON: "3.12"
13
+
14
+ jobs:
15
+ check-versions:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - name: Checkout
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Verify VERSION file
22
+ run: python3 scripts/check_versions.py
23
+
24
+ lint:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - name: Checkout
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Setup Python
31
+ uses: actions/setup-python@v5
32
+ with:
33
+ python-version: "3.12"
34
+ cache: pip
35
+ cache-dependency-path: pyproject.toml
36
+
37
+ - name: Install package (editable + dev)
38
+ run: |
39
+ python -m pip install --upgrade pip
40
+ pip install -e ".[dev]"
41
+
42
+ - name: Run pre-commit
43
+ run: pre-commit run --all-files
44
+
45
+ test:
46
+ runs-on: ubuntu-latest
47
+ strategy:
48
+ fail-fast: false
49
+ matrix:
50
+ python-version: ["3.10", "3.12", "3.14"]
51
+ steps:
52
+ - name: Checkout
53
+ uses: actions/checkout@v4
54
+
55
+ - name: Fetch tickerforge-spec into spec/
56
+ run: |
57
+ git clone --depth 1 --branch main https://github.com/mesias/tickerforge-spec.git /tmp/tickerforge-spec
58
+ cp -r /tmp/tickerforge-spec/spec .
59
+
60
+ - name: Setup Python ${{ matrix.python-version }}
61
+ uses: actions/setup-python@v5
62
+ with:
63
+ python-version: ${{ matrix.python-version }}
64
+ cache: pip
65
+ cache-dependency-path: pyproject.toml
66
+
67
+ - name: Install package (editable + dev)
68
+ run: |
69
+ python -m pip install --upgrade pip
70
+ pip install -e ".[dev]"
71
+
72
+ - name: Run tests
73
+ if: matrix.python-version != env.COVERAGE_PYTHON
74
+ run: pytest
75
+
76
+ - name: Run tests and collect coverage
77
+ if: matrix.python-version == env.COVERAGE_PYTHON
78
+ run: pytest --cov=./ --cov-report=xml
79
+
80
+ - name: Upload coverage to Codecov
81
+ if: matrix.python-version == env.COVERAGE_PYTHON
82
+ uses: codecov/codecov-action@v3
83
+ with:
84
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -0,0 +1,73 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*"
7
+
8
+ jobs:
9
+ validate:
10
+ name: Validate version
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v4
15
+
16
+ - name: Setup Python
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: "3.12"
20
+
21
+ - name: Verify VERSION file
22
+ run: python3 scripts/check_versions.py
23
+
24
+ - name: Verify tag matches VERSION
25
+ run: |
26
+ set -euo pipefail
27
+ TAG="${GITHUB_REF_NAME#v}"
28
+ VER=$(tr -d '[:space:]' < VERSION)
29
+ if [ "$TAG" != "$VER" ]; then
30
+ echo "::error::Git tag v$TAG must match VERSION ($VER)"
31
+ exit 1
32
+ fi
33
+
34
+ release_pypi:
35
+ name: Publish PyPI (tickerforge)
36
+ needs: validate
37
+ runs-on: ubuntu-latest
38
+ environment: release
39
+ steps:
40
+ - name: Checkout
41
+ uses: actions/checkout@v4
42
+
43
+ - name: Setup Python
44
+ uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+
48
+ - name: Build Python artifacts
49
+ run: |
50
+ python -m pip install --upgrade pip build
51
+ python -m build
52
+
53
+ - name: Publish to PyPI
54
+ uses: pypa/gh-action-pypi-publish@release/v1
55
+ with:
56
+ user: __token__
57
+ password: ${{ secrets.PYPI_API_TOKEN }}
58
+ packages-dir: dist/
59
+ attestations: false
60
+
61
+ github_release:
62
+ name: GitHub Release
63
+ needs: release_pypi
64
+ runs-on: ubuntu-latest
65
+ permissions:
66
+ contents: write
67
+ steps:
68
+ - name: Create GitHub Release
69
+ uses: softprops/action-gh-release@v2
70
+ with:
71
+ tag_name: ${{ github.ref_name }}
72
+ name: ${{ github.ref_name }}
73
+ generate_release_notes: true
@@ -0,0 +1,211 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[codz]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py.cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+ #poetry.toml
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ # pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
114
+ # https://pdm-project.org/en/latest/usage/project/#working-with-version-control
115
+ #pdm.lock
116
+ #pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # pixi
121
+ # Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
122
+ #pixi.lock
123
+ # Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
124
+ # in the .venv directory. It is recommended not to include this directory in version control.
125
+ .pixi
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Local checkout of tickerforge-spec (clone or copy into ./spec per README). Never commit
138
+ # symlinks to absolute paths or machine-specific paths.
139
+ /spec
140
+
141
+ # Environments
142
+ .env
143
+ .envrc
144
+ .venv
145
+ env/
146
+ venv/
147
+ ENV/
148
+ env.bak/
149
+ venv.bak/
150
+
151
+ # Spyder project settings
152
+ .spyderproject
153
+ .spyproject
154
+
155
+ # Rope project settings
156
+ .ropeproject
157
+
158
+ # mkdocs documentation
159
+ /site
160
+
161
+ # mypy
162
+ .mypy_cache/
163
+ .dmypy.json
164
+ dmypy.json
165
+
166
+ # Pyre type checker
167
+ .pyre/
168
+
169
+ # pytype static type analyzer
170
+ .pytype/
171
+
172
+ # Cython debug symbols
173
+ cython_debug/
174
+
175
+ # PyCharm
176
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
177
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
178
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
179
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
180
+ #.idea/
181
+
182
+ # Abstra
183
+ # Abstra is an AI-powered process automation framework.
184
+ # Ignore directories containing user credentials, local state, and settings.
185
+ # Learn more at https://abstra.io/docs
186
+ .abstra/
187
+
188
+ # Visual Studio Code
189
+ # Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
190
+ # that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
191
+ # and can be added to the global gitignore or merged into this file. However, if you prefer,
192
+ # you could uncomment the following to ignore the entire vscode folder
193
+ # .vscode/
194
+
195
+ # Ruff stuff:
196
+ .ruff_cache/
197
+
198
+ # PyPI configuration file
199
+ .pypirc
200
+
201
+ # Cursor
202
+ # Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to
203
+ # exclude from AI features like autocomplete and code analysis. Recommended for sensitive data
204
+ # refer to https://docs.cursor.com/context/ignore-files
205
+ .cursorignore
206
+ .cursorindexingignore
207
+
208
+ # Marimo
209
+ marimo/_static/
210
+ marimo/_lsp/
211
+ __marimo__/
@@ -0,0 +1,29 @@
1
+ repos:
2
+ - repo: https://github.com/psf/black
3
+ rev: 26.3.1
4
+ hooks:
5
+ - id: black
6
+
7
+ - repo: https://github.com/PyCQA/isort
8
+ rev: 8.0.1
9
+ hooks:
10
+ - id: isort
11
+
12
+ - repo: https://github.com/astral-sh/ruff-pre-commit
13
+ rev: v0.15.7
14
+ hooks:
15
+ - id: ruff-check
16
+ args: [--fix]
17
+
18
+ - repo: https://github.com/pre-commit/mirrors-mypy
19
+ rev: v1.19.1
20
+ hooks:
21
+ - id: mypy
22
+ additional_dependencies:
23
+ - pydantic>=2
24
+ - PyYAML
25
+ - types-PyYAML
26
+ - python-dateutil
27
+ - types-python-dateutil
28
+ - exchange-calendars
29
+ - pytest
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Alejandro Mesias
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,181 @@
1
+ Metadata-Version: 2.4
2
+ Name: tickerforge
3
+ Version: 0.1.3
4
+ Summary: Generate and parse derivatives tickers from tickerforge-spec.
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.10
7
+ Requires-Dist: exchange-calendars
8
+ Requires-Dist: pydantic
9
+ Requires-Dist: python-dateutil
10
+ Requires-Dist: pyyaml
11
+ Requires-Dist: tickerforge-spec-data
12
+ Provides-Extra: dev
13
+ Requires-Dist: black; extra == 'dev'
14
+ Requires-Dist: isort; extra == 'dev'
15
+ Requires-Dist: mypy; extra == 'dev'
16
+ Requires-Dist: pandas; extra == 'dev'
17
+ Requires-Dist: pre-commit; extra == 'dev'
18
+ Requires-Dist: pytest; extra == 'dev'
19
+ Requires-Dist: pytest-cov; extra == 'dev'
20
+ Requires-Dist: ruff; extra == 'dev'
21
+ Requires-Dist: types-python-dateutil; extra == 'dev'
22
+ Requires-Dist: types-pyyaml; extra == 'dev'
23
+ Description-Content-Type: text/markdown
24
+
25
+ # tickerforge
26
+
27
+ [![codecov](https://codecov.io/gh/mesias/tickerforge-py/branch/main/graph/badge.svg)](https://codecov.io/gh/mesias/tickerforge-py)
28
+ [![CI](https://github.com/mesias/tickerforge-py/actions/workflows/ci.yml/badge.svg)](https://github.com/mesias/tickerforge-py/actions/workflows/ci.yml)
29
+ [![Python versions](https://img.shields.io/badge/python-3.10%20|%203.12%20|%203.14-3776ab?labelColor=434343&logo=python&logoColor=ffd43b)](https://github.com/mesias/tickerforge-py/blob/main/.github/workflows/ci.yml)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/mesias/tickerforge-py/blob/main/LICENSE)
31
+
32
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
33
+ [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b2?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/)
34
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
35
+ [![Checked with mypy](https://www.mypy-lang.org/static/mypy_badge.svg)](https://www.mypy-lang.org/)
36
+
37
+ [![Repo Stats](https://github-readme-stats.vercel.app/api/pin/?username=mesias&repo=tickerforge-py&theme=dark)](https://github.com/mesias/tickerforge-py)
38
+
39
+ Python library that loads [`tickerforge-spec`](https://github.com/mesias/tickerforge-spec) and
40
+ generates/parses derivatives tickers.
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install "git+https://github.com/mesias/tickerforge-py.git"
46
+ ```
47
+
48
+ `tickerforge` depends on `tickerforge-spec-data` from the same repository root (`pyproject.toml` in [`tickerforge-spec`](https://github.com/mesias/tickerforge-spec)).
49
+
50
+ ## Usage
51
+
52
+ By default, `TickerForge` / `TickerParser` use the spec bundled in the `tickerforge-spec-data` package (installed from [`tickerforge-spec`](https://github.com/mesias/tickerforge-spec) via `pip`). Pass `spec_path` only to override.
53
+
54
+ ### Generating tickers
55
+
56
+ ```python
57
+ from tickerforge import TickerForge
58
+
59
+ forge = TickerForge()
60
+ ticker = forge.generate("IND", date="2025-04-01")
61
+ print(ticker) # e.g. INDM25
62
+ ```
63
+
64
+ Custom spec directory:
65
+
66
+ ```python
67
+ forge = TickerForge(spec_path="/path/to/tickerforge-spec/spec")
68
+ ```
69
+
70
+ ### Parsing tickers (smart parsing)
71
+
72
+ The parser accepts **full tickers** (`INDM26`) or **root symbols** (`IND`).
73
+
74
+ Full tickers derive year/month directly from the string — no `reference_date` required.
75
+ Root symbols resolve the front-month contract via the generator; `reference_date` defaults to today when omitted.
76
+
77
+ ```python
78
+ from tickerforge import TickerParser, parse_ticker
79
+
80
+ # Full ticker — year and month come from the ticker itself
81
+ parsed = parse_ticker("INDM26")
82
+ print(parsed.symbol, parsed.year, parsed.month) # IND 2026 6
83
+ print(parsed.tick_size, parsed.lot_size) # 5.0 1.0
84
+
85
+ # Root symbol — resolves front-month for today
86
+ parsed = parse_ticker("IND")
87
+
88
+ # Root symbol — resolves front-month for a specific date
89
+ parsed = parse_ticker("IND", reference_date="2026-06-01")
90
+
91
+ # Using TickerParser (reuses a loaded spec)
92
+ parser = TickerParser()
93
+ parsed = parser.parse("DOLK26")
94
+ parsed = parser.parse("DOL", reference_date="2026-04-15")
95
+ ```
96
+
97
+ ### Builder pattern
98
+
99
+ `TickerParser.builder()` provides a fluent API for configuration and one-shot parsing:
100
+
101
+ ```python
102
+ from tickerforge import TickerParser
103
+
104
+ # Build a reusable parser (default spec)
105
+ parser = TickerParser.builder().build()
106
+ parsed = parser.parse("INDM26")
107
+
108
+ # Build a reusable parser (custom spec)
109
+ parser = TickerParser.builder().spec_path("/path/to/spec").build()
110
+
111
+ # One-shot parse — full ticker
112
+ parsed = TickerParser.builder().ticker("INDM26").parse()
113
+
114
+ # One-shot parse — root symbol with date
115
+ parsed = (
116
+ TickerParser.builder()
117
+ .ticker("IND")
118
+ .reference_date("2026-06-01")
119
+ .parse()
120
+ )
121
+
122
+ # One-shot parse — custom spec + date
123
+ parsed = (
124
+ TickerParser.builder()
125
+ .spec_path("/path/to/spec")
126
+ .ticker("IND")
127
+ .reference_date("2026-06-01")
128
+ .parse()
129
+ )
130
+ ```
131
+
132
+ The builder enforces that `parse()` is only available after `ticker()` has been called.
133
+
134
+ ### Contract-centric (tick, session, trading symbol)
135
+
136
+ `load_spec()` returns a repository of contracts. Each `ContractSpec` includes tick size and (after load) regular session times and exchange timezone, plus helpers that use the **bundled default spec** unless you pass `spec=…`:
137
+
138
+ ```python
139
+ from tickerforge import load_spec
140
+
141
+ spec = load_spec()
142
+ dol = spec.get_contract("DOL")
143
+
144
+ dol.tick_size
145
+ dol.regular_session_start_end() # e.g. ("09:00", "18:30")
146
+ dol.exchange_timezone
147
+ # `dol.sessions` is an ordered list of `SessionSegment`; in YAML, sessions are a map keyed by
148
+ # band name (`regular`, …) and each key is copied into `SessionSegment.name` at load time.
149
+
150
+ # Front-month ticker — default bundled spec (omit `spec`)
151
+ dol.trading_symbol_today()
152
+ dol.trading_symbol_for("2026-03-15")
153
+
154
+ # Same helpers with an explicit `SpecRepository` (e.g. custom `load_spec(path)`)
155
+ dol.trading_symbol_today(spec=spec)
156
+ dol.trading_symbol_for("2026-03-15", spec=spec)
157
+ ```
158
+
159
+ Repeated calls with the default path reload the spec each time; for hot paths, pass `spec=` once.
160
+
161
+ ## What this version supports
162
+
163
+ - Loading exchanges, contract cycles, expiration rules, and futures contracts from YAML
164
+ - Validating loaded structures with Pydantic models
165
+ - Resolving contract months by cycle
166
+ - Resolving expiration dates with spec-driven exchange calendars
167
+ - Rule-based holiday definitions (fixed dates, Easter offsets, nth-weekday) loaded from `spec/schedules/`
168
+ - Fallback to `exchange_calendars` when no spec schedule exists
169
+ - Generating futures tickers from `{symbol}{month_code}{yy}`-style templates
170
+ - Parsing tickers back to structured contract information
171
+ - Golden calendar validation for B3 WIN/IND/DOL (2023--2026)
172
+
173
+ ## Run tests
174
+
175
+ Tests load YAML and fixtures from a `spec/` directory at the project root—the same tree bundled into the [`tickerforge-spec-data`](https://github.com/mesias/tickerforge-spec) wheel from that repo’s root. Clone the spec repo once:
176
+
177
+ ```bash
178
+ git clone --depth 1 https://github.com/mesias/tickerforge-spec.git /tmp/tickerforge-spec
179
+ cp -r /tmp/tickerforge-spec/spec .
180
+ pytest
181
+ ```