settfex 0.2.1__tar.gz → 0.4.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.
- {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/release.yml +2 -8
- {settfex-0.2.1 → settfex-0.4.0}/CHANGELOG.md +42 -0
- {settfex-0.2.1 → settfex-0.4.0}/CONTRIBUTING.md +6 -0
- {settfex-0.2.1 → settfex-0.4.0}/PKG-INFO +35 -1
- {settfex-0.2.1 → settfex-0.4.0}/README.md +32 -0
- settfex-0.4.0/RELEASING.md +73 -0
- settfex-0.4.0/docs/settfex/services/set/earnings_call.md +187 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/README.md +2 -1
- settfex-0.4.0/examples/set/12_earnings_call.ipynb +217 -0
- settfex-0.4.0/examples/tfex/03_underlying_price.ipynb +564 -0
- {settfex-0.2.1 → settfex-0.4.0}/pyproject.toml +14 -1
- {settfex-0.2.1 → settfex-0.4.0}/settfex/__init__.py +1 -1
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/__init__.py +25 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/constants.py +10 -0
- settfex-0.4.0/settfex/services/set/earnings_call.py +706 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/__init__.py +11 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/constants.py +1 -0
- settfex-0.4.0/settfex/services/tfex/underlying_price.py +253 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/data_fetcher.py +47 -6
- settfex-0.4.0/tests/services/set/test_earnings_call.py +519 -0
- settfex-0.4.0/tests/services/tfex/test_underlying_price.py +143 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_data_fetcher.py +96 -0
- {settfex-0.2.1 → settfex-0.4.0}/uv.lock +8 -2
- {settfex-0.2.1 → settfex-0.4.0}/.editorconfig +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/copilot-instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/dependabot.yml +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/documentation-standards.instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/file-organization.instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/git-commit.instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Coding.prompt.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Python-Architect.prompt.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/ci.yml +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/security.yml +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.gitignore +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.pre-commit-config.yaml +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/.python-version +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/CODE_OF_CONDUCT.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/COMPREHENSIVE_AUDIT.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/LICENSE +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/MANIFEST.in +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/SECURITY.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/index.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/board_of_director.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/corporate_action.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/financial.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/highlight_data.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/list.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/price_performance.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/profile_company.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/profile_stock.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/shareholder.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/trading_stat.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/tfex/list.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/utils/data_fetcher.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/utils/session_caching.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/solution/FINDINGS.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/01_stock_list.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/02_highlight_data.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/03_stock_profile.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/04_company_profile.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/05_corporate_action.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/06_shareholder.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/07_nvdr_holder.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/08_board_of_director.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/09_trading_statistics.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/10_price_performance.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/11_financial.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/README.md +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/cpall_balance_sheet.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_banking_stocks.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_set_main_board.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_stock_universe.csv +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/tfex/01_series_list.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/examples/tfex/02_trading_statistics.ipynb +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/py.typed +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/list.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/board_of_director.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/chart_quotation.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/corporate_action.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/financial/financial.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/highlight_data.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/latest_historical_trading.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/nvdr_holder.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/price_performance.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/profile_company.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/profile_stock.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/shareholder.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/stock.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/trading_stat.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/utils.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/list.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/trading_statistics.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/http.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/logging.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/parsing.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/session_cache.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/session_manager.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/conftest.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/stock/financial/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/stock/financial/test_financial.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_board_of_director.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_client.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_corporate_action.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_historical.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_realtime.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_shareholder.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_client.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_historical.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_list.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_realtime.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_trading_statistics.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/__init__.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_formatting.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_http.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_logging.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_parsing.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_session_manager.py +0 -0
- {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_validation.py +0 -0
|
@@ -141,7 +141,6 @@ jobs:
|
|
|
141
141
|
path: dist/
|
|
142
142
|
|
|
143
143
|
- name: Extract changelog section
|
|
144
|
-
id: changelog
|
|
145
144
|
run: |
|
|
146
145
|
VERSION="${{ needs.validate.outputs.version }}"
|
|
147
146
|
python3 - "$VERSION" <<'PYEOF'
|
|
@@ -152,19 +151,14 @@ jobs:
|
|
|
152
151
|
match = re.search(pattern, content, re.DOTALL)
|
|
153
152
|
notes = match.group(1).strip() if match else f"Release v{version}"
|
|
154
153
|
with open("release_notes.txt", "w") as f:
|
|
155
|
-
f.write(notes)
|
|
154
|
+
f.write(notes + "\n")
|
|
156
155
|
PYEOF
|
|
157
|
-
{
|
|
158
|
-
echo 'notes<<CHANGELOG_EOF'
|
|
159
|
-
cat release_notes.txt
|
|
160
|
-
echo 'CHANGELOG_EOF'
|
|
161
|
-
} >> "$GITHUB_OUTPUT"
|
|
162
156
|
|
|
163
157
|
- name: Create GitHub Release
|
|
164
158
|
uses: softprops/action-gh-release@v2
|
|
165
159
|
with:
|
|
166
160
|
name: "settfex v${{ needs.validate.outputs.version }}"
|
|
167
|
-
|
|
161
|
+
body_path: release_notes.txt
|
|
168
162
|
prerelease: ${{ needs.validate.outputs.is_prerelease }}
|
|
169
163
|
files: dist/*
|
|
170
164
|
make_latest: ${{ needs.validate.outputs.is_prerelease == 'false' }}
|
|
@@ -5,6 +5,48 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.4.0] - 2026-06-19
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **SET Earnings Call (Opportunity Day) service** (`get_earnings_calls`,
|
|
15
|
+
`get_earnings_calls_dataframe`, `EarningsCallService`, `EarningsCallItem`,
|
|
16
|
+
`EarningsCallResponse`, `EarningsCallDetail`, `FilterOption`): fetches the SET
|
|
17
|
+
"Earnings Call (OPPDAY)" calendar from the stateless opportunity-day backend
|
|
18
|
+
(`POST https://api.lcp.setgroup.or.th/api/v1/investor/search/archive`). Returns typed
|
|
19
|
+
Pydantic models with derived `company_name_clean` / `youtube_video_id` / `youtube_url`
|
|
20
|
+
fields, plus an optional pandas DataFrame (`to_dataframe()`) whose default columns are
|
|
21
|
+
`stock_name, company_name, earnings_call_date, video_clip_time, youtube_url`. Includes
|
|
22
|
+
bounded auto-pagination (`fetch_all_earnings_calls`), opt-in concurrent detail enrichment
|
|
23
|
+
(`enrich=True`), seven filter helpers, and a `*_raw` variant. This host needs no
|
|
24
|
+
cookies/Incapsula bypass, so it uses a plain sessionless fetcher. Not stock-scoped (not on
|
|
25
|
+
the `Stock` class). Adds the `docs/settfex/services/set/earnings_call.md` doc and the
|
|
26
|
+
`examples/set/12_earnings_call.ipynb` notebook.
|
|
27
|
+
- **`AsyncDataFetcher` POST support**: `fetch()` / `fetch_json()` now accept keyword-only
|
|
28
|
+
`method="GET"` (default — fully backward compatible) and `json_body`. POST runs through the
|
|
29
|
+
standalone (sessionless) path and the same NaN-rejecting JSON decoder; POST via a persistent
|
|
30
|
+
session is intentionally unsupported (raises `NotImplementedError`).
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
|
|
34
|
+
- pandas is now available as an optional `dataframe` extra
|
|
35
|
+
(`pip install "settfex[dataframe]"`); it is required only for the DataFrame convenience and
|
|
36
|
+
is imported lazily, so importing the library never requires pandas.
|
|
37
|
+
|
|
38
|
+
## [0.3.0] - 2026-06-17
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **TFEX underlying-price service** (`get_underlying_price`, `TFEXUnderlyingPriceService`,
|
|
43
|
+
`UnderlyingPrice`): fetches the underlying instrument price for a TFEX series via
|
|
44
|
+
`GET /api/set/tfex/series/{symbol}/underlying-price`. For SET50 index options/futures the underlying
|
|
45
|
+
is the **SET50 index spot** — exposes last/prior/high/low, change, total volume/value, and P/E + P/BV.
|
|
46
|
+
Mirrors the existing TFEX service pattern (SessionManager/Incapsula bypass, NaN-rejecting hardened
|
|
47
|
+
parsing, `get_*` convenience function + `*_raw` variant); 100% module test coverage. Adds the
|
|
48
|
+
`verify_underlying_price.py` script and the `examples/tfex/03_underlying_price.ipynb` notebook.
|
|
49
|
+
|
|
8
50
|
## [0.2.1] - 2026-06-17
|
|
9
51
|
|
|
10
52
|
Robustness and concurrency hardening release. No public API changes — function
|
|
@@ -61,6 +61,12 @@ Detailed engineering guidelines live in [`.github/instructions/`](.github/instru
|
|
|
61
61
|
4. Ensure the quality gate passes.
|
|
62
62
|
5. Open a PR using the template and link any related issue.
|
|
63
63
|
|
|
64
|
+
## Releasing
|
|
65
|
+
|
|
66
|
+
Releases go through a single path: bump the version, update `CHANGELOG.md`, merge to `main`,
|
|
67
|
+
then push a `vX.Y.Z` git tag. The tag triggers CI to publish to PyPI (via OIDC) and create the
|
|
68
|
+
GitHub Release. See [`RELEASING.md`](RELEASING.md) for the exact steps and the one-time setup.
|
|
69
|
+
|
|
64
70
|
## Reporting security issues
|
|
65
71
|
|
|
66
72
|
Please do **not** open public issues for vulnerabilities — see
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: settfex
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Async Python library for fetching real-time and historical data from the Stock Exchange of Thailand (SET) and Thailand Futures Exchange (TFEX)
|
|
5
5
|
Project-URL: Homepage, https://github.com/lumduan/settfex
|
|
6
6
|
Project-URL: Documentation, https://github.com/lumduan/settfex#readme
|
|
@@ -30,6 +30,8 @@ Requires-Dist: curl-cffi>=0.6.0
|
|
|
30
30
|
Requires-Dist: diskcache>=5.6.0
|
|
31
31
|
Requires-Dist: loguru>=0.7.0
|
|
32
32
|
Requires-Dist: pydantic>=2.0.0
|
|
33
|
+
Provides-Extra: dataframe
|
|
34
|
+
Requires-Dist: pandas>=2.0.0; extra == 'dataframe'
|
|
33
35
|
Provides-Extra: examples
|
|
34
36
|
Requires-Dist: jupyter>=1.0.0; extra == 'examples'
|
|
35
37
|
Requires-Dist: matplotlib>=3.7.0; extra == 'examples'
|
|
@@ -122,6 +124,7 @@ Want to dig deeper? Check out our detailed guides:
|
|
|
122
124
|
- **[Financial Service](docs/settfex/services/set/financial.md)** - Balance sheet, income statement, and cash flow data
|
|
123
125
|
- **[Chart Quotation Service](docs/settfex/services/set/chart_quotation.md)** - Intraday and historical price chart data
|
|
124
126
|
- **[Latest Historical Trading Service](docs/settfex/services/set/latest_historical_trading.md)** - Latest trading day summary with OHLCV and valuation metrics
|
|
127
|
+
- **[Earnings Call (Opportunity Day) Service](docs/settfex/services/set/earnings_call.md)** - OPPDAY earnings-call calendar with YouTube links, as models or a DataFrame
|
|
125
128
|
|
|
126
129
|
### TFEX Services
|
|
127
130
|
|
|
@@ -418,6 +421,31 @@ print(f"Market Cap: {trading.market_cap:,.0f} THB")
|
|
|
418
421
|
|
|
419
422
|
---
|
|
420
423
|
|
|
424
|
+
#### 🎤 Get Earnings Call (Opportunity Day) Calendar
|
|
425
|
+
|
|
426
|
+
Fetch the SET Opportunity Day earnings-call calendar — symbol, company, date, clip duration,
|
|
427
|
+
and a ready-to-use YouTube link — as typed models or a pandas DataFrame:
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
from settfex.services.set import get_earnings_calls, get_earnings_calls_dataframe
|
|
431
|
+
|
|
432
|
+
# Typed models
|
|
433
|
+
response = await get_earnings_calls()
|
|
434
|
+
for item in response.items[:5]:
|
|
435
|
+
print(item.symbol, item.company_name_clean, item.youtube_url)
|
|
436
|
+
|
|
437
|
+
# …or straight to a DataFrame (requires: pip install "settfex[dataframe]")
|
|
438
|
+
df = await get_earnings_calls_dataframe()
|
|
439
|
+
# columns: stock_name, company_name, earnings_call_date, video_clip_time, youtube_url
|
|
440
|
+
|
|
441
|
+
# Search one company, or filter by quarter/type
|
|
442
|
+
hann = await get_earnings_calls(keyword="HANN")
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**👉 [Learn more about the Earnings Call Service](docs/settfex/services/set/earnings_call.md)**
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
421
449
|
### TFEX (Thailand Futures Exchange)
|
|
422
450
|
|
|
423
451
|
#### 📋 Get TFEX Series List
|
|
@@ -609,6 +637,12 @@ stock_list = await get_stock_list()
|
|
|
609
637
|
|
|
610
638
|
Great for debugging or monitoring in production. Default is ERROR level for clean output.
|
|
611
639
|
|
|
640
|
+
## 📋 Changelog
|
|
641
|
+
|
|
642
|
+
See **[CHANGELOG.md](CHANGELOG.md)** for the full, versioned release history (this project
|
|
643
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
|
|
644
|
+
[Semantic Versioning](https://semver.org/)).
|
|
645
|
+
|
|
612
646
|
## 🤝 Contributing
|
|
613
647
|
|
|
614
648
|
We'd love your help making settfex better! Here's how:
|
|
@@ -83,6 +83,7 @@ Want to dig deeper? Check out our detailed guides:
|
|
|
83
83
|
- **[Financial Service](docs/settfex/services/set/financial.md)** - Balance sheet, income statement, and cash flow data
|
|
84
84
|
- **[Chart Quotation Service](docs/settfex/services/set/chart_quotation.md)** - Intraday and historical price chart data
|
|
85
85
|
- **[Latest Historical Trading Service](docs/settfex/services/set/latest_historical_trading.md)** - Latest trading day summary with OHLCV and valuation metrics
|
|
86
|
+
- **[Earnings Call (Opportunity Day) Service](docs/settfex/services/set/earnings_call.md)** - OPPDAY earnings-call calendar with YouTube links, as models or a DataFrame
|
|
86
87
|
|
|
87
88
|
### TFEX Services
|
|
88
89
|
|
|
@@ -379,6 +380,31 @@ print(f"Market Cap: {trading.market_cap:,.0f} THB")
|
|
|
379
380
|
|
|
380
381
|
---
|
|
381
382
|
|
|
383
|
+
#### 🎤 Get Earnings Call (Opportunity Day) Calendar
|
|
384
|
+
|
|
385
|
+
Fetch the SET Opportunity Day earnings-call calendar — symbol, company, date, clip duration,
|
|
386
|
+
and a ready-to-use YouTube link — as typed models or a pandas DataFrame:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
from settfex.services.set import get_earnings_calls, get_earnings_calls_dataframe
|
|
390
|
+
|
|
391
|
+
# Typed models
|
|
392
|
+
response = await get_earnings_calls()
|
|
393
|
+
for item in response.items[:5]:
|
|
394
|
+
print(item.symbol, item.company_name_clean, item.youtube_url)
|
|
395
|
+
|
|
396
|
+
# …or straight to a DataFrame (requires: pip install "settfex[dataframe]")
|
|
397
|
+
df = await get_earnings_calls_dataframe()
|
|
398
|
+
# columns: stock_name, company_name, earnings_call_date, video_clip_time, youtube_url
|
|
399
|
+
|
|
400
|
+
# Search one company, or filter by quarter/type
|
|
401
|
+
hann = await get_earnings_calls(keyword="HANN")
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
**👉 [Learn more about the Earnings Call Service](docs/settfex/services/set/earnings_call.md)**
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
382
408
|
### TFEX (Thailand Futures Exchange)
|
|
383
409
|
|
|
384
410
|
#### 📋 Get TFEX Series List
|
|
@@ -570,6 +596,12 @@ stock_list = await get_stock_list()
|
|
|
570
596
|
|
|
571
597
|
Great for debugging or monitoring in production. Default is ERROR level for clean output.
|
|
572
598
|
|
|
599
|
+
## 📋 Changelog
|
|
600
|
+
|
|
601
|
+
See **[CHANGELOG.md](CHANGELOG.md)** for the full, versioned release history (this project
|
|
602
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and
|
|
603
|
+
[Semantic Versioning](https://semver.org/)).
|
|
604
|
+
|
|
573
605
|
## 🤝 Contributing
|
|
574
606
|
|
|
575
607
|
We'd love your help making settfex better! Here's how:
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Releasing settfex
|
|
2
|
+
|
|
3
|
+
settfex publishes to PyPI through **one canonical path: pushing a `vX.Y.Z` git tag.**
|
|
4
|
+
|
|
5
|
+
The tag triggers [`.github/workflows/release.yml`](.github/workflows/release.yml), which:
|
|
6
|
+
|
|
7
|
+
1. validates the tag matches `pyproject.toml`,
|
|
8
|
+
2. runs the full CI gate (ruff, format, mypy, pytest),
|
|
9
|
+
3. builds the sdist + wheel,
|
|
10
|
+
4. publishes to PyPI via an **OIDC Trusted Publisher** (no API token), and
|
|
11
|
+
5. creates the GitHub Release with the changelog body + built artifacts.
|
|
12
|
+
|
|
13
|
+
There is intentionally **no second PyPI upload path in the repo** — the workflow is the only
|
|
14
|
+
thing that runs `twine`. Its publish step uses `skip-existing: true`, so re-tagging (or a one-off
|
|
15
|
+
manual rescue) never hard-fails on an already-published file.
|
|
16
|
+
|
|
17
|
+
## Cut a release
|
|
18
|
+
|
|
19
|
+
1. Bump the version in **both** `pyproject.toml` and `settfex/__init__.py` (keep them in sync).
|
|
20
|
+
2. Add a `## [X.Y.Z] - YYYY-MM-DD` section to `CHANGELOG.md`.
|
|
21
|
+
3. Commit, open a PR, and merge to `main`.
|
|
22
|
+
4. From an up-to-date `main`, tag and push:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
git checkout main && git pull --ff-only
|
|
26
|
+
git tag -a vX.Y.Z -m "settfex X.Y.Z"
|
|
27
|
+
git push origin vX.Y.Z
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
CI does the rest — watch:
|
|
31
|
+
<https://github.com/lumduan/settfex/actions/workflows/release.yml>
|
|
32
|
+
|
|
33
|
+
> **Maintainer convenience.** A local, gitignored `scripts/publish.sh` performs the same tag
|
|
34
|
+
> push behind pre-flight checks (version sync between `pyproject.toml` and `settfex/__init__.py`,
|
|
35
|
+
> a clean/synced `main`, the CHANGELOG section, and tag uniqueness). It never runs `twine`; it
|
|
36
|
+
> only tags. It is not shipped in the repo because the tag push above is the real interface.
|
|
37
|
+
|
|
38
|
+
## One-time setup: PyPI Trusted Publisher (required for the OIDC publish)
|
|
39
|
+
|
|
40
|
+
This must be done once on pypi.org or the publish step will fail. On
|
|
41
|
+
<https://pypi.org/manage/project/settfex/settings/publishing/>, add a **GitHub Actions**
|
|
42
|
+
publisher:
|
|
43
|
+
|
|
44
|
+
| Field | Value |
|
|
45
|
+
|---|---|
|
|
46
|
+
| Owner | `lumduan` |
|
|
47
|
+
| Repository | `settfex` |
|
|
48
|
+
| Workflow name | `release.yml` |
|
|
49
|
+
| Environment | `pypi` |
|
|
50
|
+
|
|
51
|
+
Then create the `pypi` environment under **GitHub → Settings → Environments** (it can be empty;
|
|
52
|
+
the workflow references `environment: pypi`).
|
|
53
|
+
|
|
54
|
+
## Break-glass: manual publish (fallback only)
|
|
55
|
+
|
|
56
|
+
Use **only** if the tag/CI path is unavailable (e.g. an Actions outage). Requires a
|
|
57
|
+
project-scoped token in a gitignored `.env` (`PYPI_TOKEN=pypi-…`):
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
rm -rf dist
|
|
61
|
+
uv run python -m build
|
|
62
|
+
uv run twine check dist/*
|
|
63
|
+
uv run twine upload dist/* --username __token__ --password "$PYPI_TOKEN"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Prefer the tag-driven path. Because CI uses `skip-existing: true`, a later tag for the same
|
|
67
|
+
version still succeeds and just backfills the GitHub Release.
|
|
68
|
+
|
|
69
|
+
## Recommended follow-up
|
|
70
|
+
|
|
71
|
+
Single-source the version (e.g. Hatchling `[tool.hatch.version] path = "settfex/__init__.py"`
|
|
72
|
+
with `dynamic = ["version"]`) so `pyproject.toml` and the package can never drift again. The
|
|
73
|
+
release workflow's validate step reads `project.version`, so adjust it if you switch.
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# SET Earnings Call (Opportunity Day) Service
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
The Earnings Call Service fetches the SET **"Earnings Call (OPPDAY)"** calendar — the
|
|
6
|
+
Opportunity Day presentations listed at
|
|
7
|
+
<https://opportunity-day.setgroup.or.th/en/earnings-call>. It returns typed Pydantic models
|
|
8
|
+
and an optional pandas DataFrame whose columns are exactly what an earnings-call dashboard
|
|
9
|
+
needs: `stock_name`, `company_name`, `earnings_call_date`, `video_clip_time`, `youtube_url`.
|
|
10
|
+
|
|
11
|
+
Unlike the main SET API, the opportunity-day backend (`https://api.lcp.setgroup.or.th/api/v1`)
|
|
12
|
+
is **stateless** — no Incapsula bot wall, no cookies, no auth. The service therefore uses a
|
|
13
|
+
plain sessionless fetcher (no cookie warm-up).
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import asyncio
|
|
19
|
+
from settfex.services.set import get_earnings_calls, get_earnings_calls_dataframe
|
|
20
|
+
|
|
21
|
+
async def main():
|
|
22
|
+
# Typed models
|
|
23
|
+
response = await get_earnings_calls()
|
|
24
|
+
for item in response.items:
|
|
25
|
+
print(item.symbol, item.company_name_clean, item.youtube_url)
|
|
26
|
+
|
|
27
|
+
# …or straight to a DataFrame (requires: pip install settfex[dataframe])
|
|
28
|
+
df = await get_earnings_calls_dataframe()
|
|
29
|
+
print(df)
|
|
30
|
+
|
|
31
|
+
asyncio.run(main())
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## The 5 DataFrame columns
|
|
35
|
+
|
|
36
|
+
| Column | Source |
|
|
37
|
+
|---|---|
|
|
38
|
+
| `stock_name` | `symbol` (e.g. `HANN`) |
|
|
39
|
+
| `company_name` | `company_name` with the leading `"<SYMBOL>: "` prefix stripped |
|
|
40
|
+
| `earnings_call_date` | `meeting_date` (tz-aware UTC) as a `date` |
|
|
41
|
+
| `video_clip_time` | `period` — the clip **duration** shown on the card, e.g. `"45:00"` |
|
|
42
|
+
| `youtube_url` | derived from the thumbnail `image_path`, e.g. `https://www.youtube.com/watch?v=…` |
|
|
43
|
+
|
|
44
|
+
`youtube_url` (and `youtube_video_id`) are `None` for upcoming items with no recording.
|
|
45
|
+
|
|
46
|
+
> ⚠️ **The `period` field has two meanings.** In the **list** response (used for
|
|
47
|
+
> `video_clip_time`) it is the clip **duration** (`"45:00"`, MM:SS). In the **detail**
|
|
48
|
+
> response (enrichment) it is the meeting **clock-time range** (`"16:15 - 17:00"`), surfaced
|
|
49
|
+
> separately as `EarningsCallDetail.meeting_time`. The list duration is authoritative and is
|
|
50
|
+
> never overwritten by enrichment.
|
|
51
|
+
|
|
52
|
+
## Models
|
|
53
|
+
|
|
54
|
+
### `EarningsCallItem`
|
|
55
|
+
|
|
56
|
+
- `id: int`, `name: str` (Thai presentation title), `symbol: str`, `industry: str`
|
|
57
|
+
- `company_name: str` — raw, e.g. `"HANN: MUKDAHAN INTERNATIONAL HOSPITAL …"`
|
|
58
|
+
- `meeting_date: datetime` — tz-aware UTC
|
|
59
|
+
- `image_path: str | None`, `view_mode: bool | None`, `period: str | None`
|
|
60
|
+
- **Derived** (computed): `company_name_clean: str`, `youtube_video_id: str | None`,
|
|
61
|
+
`youtube_url: str | None`
|
|
62
|
+
- `detail: EarningsCallDetail | None` — populated only when `enrich=True`
|
|
63
|
+
|
|
64
|
+
### `EarningsCallResponse`
|
|
65
|
+
|
|
66
|
+
- `no_records: int` — total records across all pages
|
|
67
|
+
- `items: list[EarningsCallItem]`, `count` property
|
|
68
|
+
- `to_dataframe(columns=None) -> pandas.DataFrame` — defaults to the 5 columns above;
|
|
69
|
+
additional selectable columns: `company_name_raw`, `earnings_call_datetime`,
|
|
70
|
+
`youtube_video_id`, `id`, `name`, `industry`, `view_mode`. Unknown column → `ValueError`;
|
|
71
|
+
pandas missing → `ImportError`.
|
|
72
|
+
|
|
73
|
+
### `EarningsCallDetail` (enrichment)
|
|
74
|
+
|
|
75
|
+
`video_link`, `company_name_th`, `meeting_time` (the clock-time range), `document_link`,
|
|
76
|
+
`snapshot_link`, `round_name`, `type`, `type_id`, `year`, `round`, `has_qa`, …
|
|
77
|
+
|
|
78
|
+
### `FilterOption`
|
|
79
|
+
|
|
80
|
+
`id: int | str`, `name: str` (ids are ints for types/years/trusts/stages, string codes for
|
|
81
|
+
industries/markets/themes).
|
|
82
|
+
|
|
83
|
+
## Service Class
|
|
84
|
+
|
|
85
|
+
### `EarningsCallService(config: FetcherConfig | None = None)`
|
|
86
|
+
|
|
87
|
+
`use_session` is always coerced to `False` for this stateless host; all other `FetcherConfig`
|
|
88
|
+
settings (timeout, retries, impersonation, rate limit) are honored.
|
|
89
|
+
|
|
90
|
+
#### `fetch_earnings_calls(...) -> EarningsCallResponse`
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
response = await EarningsCallService().fetch_earnings_calls(
|
|
94
|
+
type_id=1, # 1=Earnings Call/OPPDAY, 2=Digital Roadshow, 3=C-Sign
|
|
95
|
+
quarter_id=0, # 0=all quarters; see fetch_filter_years()
|
|
96
|
+
keyword=None, # free-text symbol/company search (normalized)
|
|
97
|
+
industries_id=None,
|
|
98
|
+
start=1, # 1-based page number
|
|
99
|
+
page_size=12,
|
|
100
|
+
language="en", # "en" or "th"
|
|
101
|
+
enrich=False, # opt-in per-item detail (bounded concurrency)
|
|
102
|
+
)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `fetch_all_earnings_calls(..., max_records=None, max_pages=None, throttle=0.3)`
|
|
106
|
+
|
|
107
|
+
Auto-paginates, **bounded and polite** — reuses one fetcher, sleeps `throttle` seconds between
|
|
108
|
+
pages, and stops at the first short page, once `no_records` is reached, or when a cap is hit.
|
|
109
|
+
|
|
110
|
+
> The API honors `page_size` up to ~100 but **rejects very large values** (e.g. 500), which
|
|
111
|
+
> surfaces as a clear `ResponseParseError`/`ValidationError`. The defaults (12 for a single
|
|
112
|
+
> page, 50 for `fetch_all`) stay safely under the cap — prefer iterating pages over an
|
|
113
|
+
> oversized `page_size`.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
service = EarningsCallService()
|
|
117
|
+
response = await service.fetch_all_earnings_calls(max_records=200) # at most 200, polite
|
|
118
|
+
df = response.to_dataframe()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### `fetch_earnings_calls_raw(...) -> dict`
|
|
122
|
+
|
|
123
|
+
Raw JSON dict (no validation), for debugging.
|
|
124
|
+
|
|
125
|
+
#### Filter helpers
|
|
126
|
+
|
|
127
|
+
`fetch_filter_types()`, `fetch_filter_years()`, `fetch_filter_industries()`,
|
|
128
|
+
`fetch_filter_markets()`, `fetch_filter_themes()`, `fetch_filter_trusts()`,
|
|
129
|
+
`fetch_filter_stages()` → `list[FilterOption]`.
|
|
130
|
+
|
|
131
|
+
## Convenience Functions
|
|
132
|
+
|
|
133
|
+
- `get_earnings_calls(...) -> EarningsCallResponse`
|
|
134
|
+
- `get_earnings_calls_dataframe(..., columns=None) -> pandas.DataFrame`
|
|
135
|
+
|
|
136
|
+
Both fetch a **single page** by default (mirroring `get_stock_list`). For the full calendar,
|
|
137
|
+
use `fetch_all_earnings_calls()` then `to_dataframe()`.
|
|
138
|
+
|
|
139
|
+
## Usage Examples
|
|
140
|
+
|
|
141
|
+
### Filter by quarter and type
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
service = EarningsCallService()
|
|
145
|
+
years = await service.fetch_filter_years() # find the quarter id you want
|
|
146
|
+
q1_2026 = next(y for y in years if y.name == "Quater 1/2026").id
|
|
147
|
+
response = await service.fetch_earnings_calls(type_id=1, quarter_id=q1_2026, page_size=50)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Search a single company
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
response = await get_earnings_calls(keyword="HANN")
|
|
154
|
+
for item in response.items:
|
|
155
|
+
print(item.meeting_date.date(), item.youtube_url)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### Enrich with detail (video link, Thai name, meeting time)
|
|
159
|
+
|
|
160
|
+
```python
|
|
161
|
+
response = await get_earnings_calls(page_size=10, enrich=True)
|
|
162
|
+
for item in response.items:
|
|
163
|
+
if item.detail:
|
|
164
|
+
print(item.symbol, item.detail.meeting_time, item.detail.video_link)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Optional dependency: pandas
|
|
168
|
+
|
|
169
|
+
`to_dataframe()` / `get_earnings_calls_dataframe()` need pandas, which is an **optional**
|
|
170
|
+
extra (the rest of the service works without it):
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
pip install "settfex[dataframe]" # or: uv add pandas
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
## API Endpoints
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
POST https://api.lcp.setgroup.or.th/api/v1/investor/search/archive # list (primary)
|
|
180
|
+
GET https://api.lcp.setgroup.or.th/api/v1/investor/vdo/{id} # detail (enrich)
|
|
181
|
+
GET https://api.lcp.setgroup.or.th/api/v1/investor/filter/{name} # filter options
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## Related Services
|
|
185
|
+
|
|
186
|
+
- [Stock List Service](list.md) — discover all SET/mai stocks
|
|
187
|
+
- [AsyncDataFetcher](../../utils/data_fetcher.md) — low-level HTTP client (now supports POST)
|
|
@@ -21,8 +21,9 @@ Complete tutorial series covering all 11 SET services:
|
|
|
21
21
|
9. **Trading Statistics** - Historical performance (multi-period)
|
|
22
22
|
10. **Price Performance** - Stock vs sector vs market comparison
|
|
23
23
|
11. **Financial Statements** - Balance sheet, income, cash flow
|
|
24
|
+
12. **Earnings Call (Opportunity Day)** - OPPDAY calendar with YouTube links → DataFrame → CSV
|
|
24
25
|
|
|
25
|
-
**Total**:
|
|
26
|
+
**Total**: 12 notebooks + comprehensive guide
|
|
26
27
|
|
|
27
28
|
## Quick Start
|
|
28
29
|
|