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.
Files changed (143) hide show
  1. {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/release.yml +2 -8
  2. {settfex-0.2.1 → settfex-0.4.0}/CHANGELOG.md +42 -0
  3. {settfex-0.2.1 → settfex-0.4.0}/CONTRIBUTING.md +6 -0
  4. {settfex-0.2.1 → settfex-0.4.0}/PKG-INFO +35 -1
  5. {settfex-0.2.1 → settfex-0.4.0}/README.md +32 -0
  6. settfex-0.4.0/RELEASING.md +73 -0
  7. settfex-0.4.0/docs/settfex/services/set/earnings_call.md +187 -0
  8. {settfex-0.2.1 → settfex-0.4.0}/examples/README.md +2 -1
  9. settfex-0.4.0/examples/set/12_earnings_call.ipynb +217 -0
  10. settfex-0.4.0/examples/tfex/03_underlying_price.ipynb +564 -0
  11. {settfex-0.2.1 → settfex-0.4.0}/pyproject.toml +14 -1
  12. {settfex-0.2.1 → settfex-0.4.0}/settfex/__init__.py +1 -1
  13. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/__init__.py +25 -0
  14. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/constants.py +10 -0
  15. settfex-0.4.0/settfex/services/set/earnings_call.py +706 -0
  16. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/__init__.py +11 -0
  17. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/constants.py +1 -0
  18. settfex-0.4.0/settfex/services/tfex/underlying_price.py +253 -0
  19. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/data_fetcher.py +47 -6
  20. settfex-0.4.0/tests/services/set/test_earnings_call.py +519 -0
  21. settfex-0.4.0/tests/services/tfex/test_underlying_price.py +143 -0
  22. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_data_fetcher.py +96 -0
  23. {settfex-0.2.1 → settfex-0.4.0}/uv.lock +8 -2
  24. {settfex-0.2.1 → settfex-0.4.0}/.editorconfig +0 -0
  25. {settfex-0.2.1 → settfex-0.4.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  26. {settfex-0.2.1 → settfex-0.4.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  27. {settfex-0.2.1 → settfex-0.4.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  28. {settfex-0.2.1 → settfex-0.4.0}/.github/copilot-instructions.md +0 -0
  29. {settfex-0.2.1 → settfex-0.4.0}/.github/dependabot.yml +0 -0
  30. {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/core-architectrual-principles.instructions.md +0 -0
  31. {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/documentation-standards.instructions.md +0 -0
  32. {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/file-organization.instructions.md +0 -0
  33. {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/git-commit.instructions.md +0 -0
  34. {settfex-0.2.1 → settfex-0.4.0}/.github/instructions/python-dependency-management.instructions.md +0 -0
  35. {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Coding.prompt.md +0 -0
  36. {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Git-Commit-Reviewer.prompt.md +0 -0
  37. {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Prompt-Engineer.prompt.md +0 -0
  38. {settfex-0.2.1 → settfex-0.4.0}/.github/prompts/Python-Architect.prompt.md +0 -0
  39. {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/ci.yml +0 -0
  40. {settfex-0.2.1 → settfex-0.4.0}/.github/workflows/security.yml +0 -0
  41. {settfex-0.2.1 → settfex-0.4.0}/.gitignore +0 -0
  42. {settfex-0.2.1 → settfex-0.4.0}/.pre-commit-config.yaml +0 -0
  43. {settfex-0.2.1 → settfex-0.4.0}/.python-version +0 -0
  44. {settfex-0.2.1 → settfex-0.4.0}/CODE_OF_CONDUCT.md +0 -0
  45. {settfex-0.2.1 → settfex-0.4.0}/COMPREHENSIVE_AUDIT.md +0 -0
  46. {settfex-0.2.1 → settfex-0.4.0}/LICENSE +0 -0
  47. {settfex-0.2.1 → settfex-0.4.0}/MANIFEST.in +0 -0
  48. {settfex-0.2.1 → settfex-0.4.0}/SECURITY.md +0 -0
  49. {settfex-0.2.1 → settfex-0.4.0}/docs/guide/PYTHON_LIBRARY_BEST_PRACTICES.md +0 -0
  50. {settfex-0.2.1 → settfex-0.4.0}/docs/index.md +0 -0
  51. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/API_PROTECTION_NOTE.md +0 -0
  52. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/board_of_director.md +0 -0
  53. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/corporate_action.md +0 -0
  54. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/financial.md +0 -0
  55. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/highlight_data.md +0 -0
  56. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/list.md +0 -0
  57. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/nvdr_holder.md +0 -0
  58. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/price_performance.md +0 -0
  59. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/profile_company.md +0 -0
  60. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/profile_stock.md +0 -0
  61. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/shareholder.md +0 -0
  62. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/set/trading_stat.md +0 -0
  63. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/tfex/list.md +0 -0
  64. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/services/tfex/trading_statistics.md +0 -0
  65. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/utils/data_fetcher.md +0 -0
  66. {settfex-0.2.1 → settfex-0.4.0}/docs/settfex/utils/session_caching.md +0 -0
  67. {settfex-0.2.1 → settfex-0.4.0}/docs/solution/FINDINGS.md +0 -0
  68. {settfex-0.2.1 → settfex-0.4.0}/docs/solution/Performance Boost Session Caching Implementation.md +0 -0
  69. {settfex-0.2.1 → settfex-0.4.0}/docs/solution/SESSION_CACHE_SUMMARY.md +0 -0
  70. {settfex-0.2.1 → settfex-0.4.0}/docs/solution/SOLUTION_100_PERCENT.md +0 -0
  71. {settfex-0.2.1 → settfex-0.4.0}/examples/set/01_stock_list.ipynb +0 -0
  72. {settfex-0.2.1 → settfex-0.4.0}/examples/set/02_highlight_data.ipynb +0 -0
  73. {settfex-0.2.1 → settfex-0.4.0}/examples/set/03_stock_profile.ipynb +0 -0
  74. {settfex-0.2.1 → settfex-0.4.0}/examples/set/04_company_profile.ipynb +0 -0
  75. {settfex-0.2.1 → settfex-0.4.0}/examples/set/05_corporate_action.ipynb +0 -0
  76. {settfex-0.2.1 → settfex-0.4.0}/examples/set/06_shareholder.ipynb +0 -0
  77. {settfex-0.2.1 → settfex-0.4.0}/examples/set/07_nvdr_holder.ipynb +0 -0
  78. {settfex-0.2.1 → settfex-0.4.0}/examples/set/08_board_of_director.ipynb +0 -0
  79. {settfex-0.2.1 → settfex-0.4.0}/examples/set/09_trading_statistics.ipynb +0 -0
  80. {settfex-0.2.1 → settfex-0.4.0}/examples/set/10_price_performance.ipynb +0 -0
  81. {settfex-0.2.1 → settfex-0.4.0}/examples/set/11_financial.ipynb +0 -0
  82. {settfex-0.2.1 → settfex-0.4.0}/examples/set/README.md +0 -0
  83. {settfex-0.2.1 → settfex-0.4.0}/examples/set/cpall_balance_sheet.csv +0 -0
  84. {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251005_173221.csv +0 -0
  85. {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251005_210737.csv +0 -0
  86. {settfex-0.2.1 → settfex-0.4.0}/examples/set/market_dashboard_20251006_091440.csv +0 -0
  87. {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_banking_stocks.csv +0 -0
  88. {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_set_main_board.csv +0 -0
  89. {settfex-0.2.1 → settfex-0.4.0}/examples/set/thailand_stock_universe.csv +0 -0
  90. {settfex-0.2.1 → settfex-0.4.0}/examples/tfex/01_series_list.ipynb +0 -0
  91. {settfex-0.2.1 → settfex-0.4.0}/examples/tfex/02_trading_statistics.ipynb +0 -0
  92. {settfex-0.2.1 → settfex-0.4.0}/settfex/py.typed +0 -0
  93. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/__init__.py +0 -0
  94. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/list.py +0 -0
  95. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/__init__.py +0 -0
  96. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/board_of_director.py +0 -0
  97. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/chart_quotation.py +0 -0
  98. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/corporate_action.py +0 -0
  99. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/financial/__init__.py +0 -0
  100. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/financial/financial.py +0 -0
  101. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/highlight_data.py +0 -0
  102. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/latest_historical_trading.py +0 -0
  103. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/nvdr_holder.py +0 -0
  104. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/price_performance.py +0 -0
  105. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/profile_company.py +0 -0
  106. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/profile_stock.py +0 -0
  107. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/shareholder.py +0 -0
  108. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/stock.py +0 -0
  109. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/trading_stat.py +0 -0
  110. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/set/stock/utils.py +0 -0
  111. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/list.py +0 -0
  112. {settfex-0.2.1 → settfex-0.4.0}/settfex/services/tfex/trading_statistics.py +0 -0
  113. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/__init__.py +0 -0
  114. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/http.py +0 -0
  115. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/logging.py +0 -0
  116. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/parsing.py +0 -0
  117. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/session_cache.py +0 -0
  118. {settfex-0.2.1 → settfex-0.4.0}/settfex/utils/session_manager.py +0 -0
  119. {settfex-0.2.1 → settfex-0.4.0}/tests/__init__.py +0 -0
  120. {settfex-0.2.1 → settfex-0.4.0}/tests/conftest.py +0 -0
  121. {settfex-0.2.1 → settfex-0.4.0}/tests/services/__init__.py +0 -0
  122. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/__init__.py +0 -0
  123. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/stock/financial/__init__.py +0 -0
  124. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/stock/financial/test_financial.py +0 -0
  125. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_board_of_director.py +0 -0
  126. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_client.py +0 -0
  127. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_corporate_action.py +0 -0
  128. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_historical.py +0 -0
  129. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_realtime.py +0 -0
  130. {settfex-0.2.1 → settfex-0.4.0}/tests/services/set/test_shareholder.py +0 -0
  131. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/__init__.py +0 -0
  132. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_client.py +0 -0
  133. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_historical.py +0 -0
  134. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_list.py +0 -0
  135. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_realtime.py +0 -0
  136. {settfex-0.2.1 → settfex-0.4.0}/tests/services/tfex/test_trading_statistics.py +0 -0
  137. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/__init__.py +0 -0
  138. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_formatting.py +0 -0
  139. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_http.py +0 -0
  140. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_logging.py +0 -0
  141. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_parsing.py +0 -0
  142. {settfex-0.2.1 → settfex-0.4.0}/tests/utils/test_session_manager.py +0 -0
  143. {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
- body: ${{ steps.changelog.outputs.notes }}
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.2.1
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**: 11 notebooks + comprehensive guide
26
+ **Total**: 12 notebooks + comprehensive guide
26
27
 
27
28
  ## Quick Start
28
29