settfex 0.1.0__py3-none-any.whl
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/__init__.py +63 -0
- settfex/py.typed +0 -0
- settfex/services/__init__.py +1 -0
- settfex/services/set/__init__.py +130 -0
- settfex/services/set/constants.py +19 -0
- settfex/services/set/list.py +207 -0
- settfex/services/set/stock/__init__.py +114 -0
- settfex/services/set/stock/board_of_director.py +240 -0
- settfex/services/set/stock/corporate_action.py +310 -0
- settfex/services/set/stock/financial/__init__.py +25 -0
- settfex/services/set/stock/financial/financial.py +442 -0
- settfex/services/set/stock/highlight_data.py +282 -0
- settfex/services/set/stock/nvdr_holder.py +264 -0
- settfex/services/set/stock/price_performance.py +290 -0
- settfex/services/set/stock/profile_company.py +326 -0
- settfex/services/set/stock/profile_stock.py +290 -0
- settfex/services/set/stock/shareholder.py +278 -0
- settfex/services/set/stock/stock.py +196 -0
- settfex/services/set/stock/trading_stat.py +279 -0
- settfex/services/set/stock/utils.py +78 -0
- settfex/services/tfex/__init__.py +34 -0
- settfex/services/tfex/constants.py +8 -0
- settfex/services/tfex/list.py +307 -0
- settfex/services/tfex/trading_statistics.py +243 -0
- settfex/utils/__init__.py +18 -0
- settfex/utils/data_fetcher.py +376 -0
- settfex/utils/http.py +214 -0
- settfex/utils/logging.py +95 -0
- settfex/utils/session_cache.py +265 -0
- settfex/utils/session_manager.py +493 -0
- settfex-0.1.0.dist-info/METADATA +597 -0
- settfex-0.1.0.dist-info/RECORD +34 -0
- settfex-0.1.0.dist-info/WHEEL +4 -0
- settfex-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""SET Board of Director Service - Fetch board of directors for individual stock symbols."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from loguru import logger
|
|
6
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
7
|
+
|
|
8
|
+
from settfex.services.set.constants import SET_BASE_URL, SET_BOARD_OF_DIRECTOR_ENDPOINT
|
|
9
|
+
from settfex.services.set.stock.utils import normalize_language, normalize_symbol
|
|
10
|
+
from settfex.utils.data_fetcher import AsyncDataFetcher, FetcherConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class Director(BaseModel):
|
|
14
|
+
"""Model for individual board member/director information."""
|
|
15
|
+
|
|
16
|
+
name: str = Field(description="Director's full name")
|
|
17
|
+
positions: list[str] = Field(description="List of positions held by the director")
|
|
18
|
+
|
|
19
|
+
model_config = ConfigDict(
|
|
20
|
+
str_strip_whitespace=True, # Strip whitespace from strings
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BoardOfDirectorService:
|
|
25
|
+
"""
|
|
26
|
+
Service for fetching board of directors data from SET API.
|
|
27
|
+
|
|
28
|
+
This service provides async methods to fetch board of directors and management
|
|
29
|
+
information for individual stock symbols from the Stock Exchange of Thailand (SET).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, config: FetcherConfig | None = None) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Initialize the board of director service.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: Optional fetcher configuration (uses defaults if None)
|
|
38
|
+
|
|
39
|
+
Example:
|
|
40
|
+
>>> # Default: Uses SessionManager for automatic cookie handling
|
|
41
|
+
>>> service = BoardOfDirectorService()
|
|
42
|
+
"""
|
|
43
|
+
self.config = config or FetcherConfig()
|
|
44
|
+
self.base_url = SET_BASE_URL
|
|
45
|
+
logger.info(f"BoardOfDirectorService initialized with base_url={self.base_url}")
|
|
46
|
+
|
|
47
|
+
async def fetch_board_of_directors(
|
|
48
|
+
self, symbol: str, lang: str = "en"
|
|
49
|
+
) -> list[Director]:
|
|
50
|
+
"""
|
|
51
|
+
Fetch board of directors for a specific stock symbol.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
symbol: Stock symbol (e.g., "CPALL", "PTT", "mint")
|
|
55
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List of Director objects containing name and positions
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
ValueError: If symbol is empty or language is invalid
|
|
62
|
+
Exception: If request fails or response cannot be parsed
|
|
63
|
+
|
|
64
|
+
Example:
|
|
65
|
+
>>> service = BoardOfDirectorService()
|
|
66
|
+
>>> directors = await service.fetch_board_of_directors("MINT", lang="en")
|
|
67
|
+
>>> for director in directors:
|
|
68
|
+
... print(f"{director.name}: {', '.join(director.positions)}")
|
|
69
|
+
"""
|
|
70
|
+
# Normalize and validate inputs
|
|
71
|
+
symbol = normalize_symbol(symbol)
|
|
72
|
+
lang = normalize_language(lang)
|
|
73
|
+
|
|
74
|
+
if not symbol:
|
|
75
|
+
error_msg = "Stock symbol cannot be empty"
|
|
76
|
+
logger.error(error_msg)
|
|
77
|
+
raise ValueError(error_msg)
|
|
78
|
+
|
|
79
|
+
# Build URL with symbol and language parameters
|
|
80
|
+
endpoint = SET_BOARD_OF_DIRECTOR_ENDPOINT.format(symbol=symbol)
|
|
81
|
+
url = f"{self.base_url}{endpoint}?lang={lang}"
|
|
82
|
+
|
|
83
|
+
logger.info(
|
|
84
|
+
f"Fetching board of directors for symbol '{symbol}' (lang={lang}) from {url}"
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
async with AsyncDataFetcher(config=self.config) as fetcher:
|
|
88
|
+
# Get optimized headers for SET API with symbol-specific referer
|
|
89
|
+
# This is critical for bypassing Incapsula bot detection
|
|
90
|
+
referer = f"https://www.set.or.th/en/market/product/stock/quote/{symbol}/overview"
|
|
91
|
+
headers = AsyncDataFetcher.get_set_api_headers(referer=referer)
|
|
92
|
+
|
|
93
|
+
# Fetch raw response - SessionManager handles cookies automatically
|
|
94
|
+
response = await fetcher.fetch(url, headers=headers)
|
|
95
|
+
|
|
96
|
+
# Check for errors
|
|
97
|
+
if response.status_code != 200:
|
|
98
|
+
error_msg = (
|
|
99
|
+
f"Failed to fetch board of directors for {symbol}: "
|
|
100
|
+
f"HTTP {response.status_code}"
|
|
101
|
+
)
|
|
102
|
+
logger.error(error_msg)
|
|
103
|
+
raise Exception(error_msg)
|
|
104
|
+
|
|
105
|
+
# Parse JSON
|
|
106
|
+
import json
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
data = json.loads(response.text)
|
|
110
|
+
except json.JSONDecodeError as e:
|
|
111
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
112
|
+
logger.debug(f"Response text: {response.text[:500]}")
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
# Validate that data is a list
|
|
116
|
+
if not isinstance(data, list):
|
|
117
|
+
error_msg = (
|
|
118
|
+
f"Expected list response but got {type(data).__name__}: "
|
|
119
|
+
f"{str(data)[:200]}"
|
|
120
|
+
)
|
|
121
|
+
logger.error(error_msg)
|
|
122
|
+
raise Exception(error_msg)
|
|
123
|
+
|
|
124
|
+
# Parse and validate response using Pydantic
|
|
125
|
+
directors = [Director(**director_data) for director_data in data]
|
|
126
|
+
|
|
127
|
+
logger.info(
|
|
128
|
+
f"Successfully fetched {len(directors)} board members for {symbol}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return directors
|
|
132
|
+
|
|
133
|
+
async def fetch_board_of_directors_raw(
|
|
134
|
+
self, symbol: str, lang: str = "en"
|
|
135
|
+
) -> list[dict[str, Any]]:
|
|
136
|
+
"""
|
|
137
|
+
Fetch board of directors as raw list of dictionaries without Pydantic validation.
|
|
138
|
+
|
|
139
|
+
Useful for debugging or when you need the raw API response.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
symbol: Stock symbol (e.g., "CPALL", "PTT", "mint")
|
|
143
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Raw list of dictionaries from API
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
ValueError: If symbol is empty or language is invalid
|
|
150
|
+
Exception: If request fails
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> service = BoardOfDirectorService()
|
|
154
|
+
>>> raw_data = await service.fetch_board_of_directors_raw("MINT")
|
|
155
|
+
>>> print(f"Found {len(raw_data)} directors")
|
|
156
|
+
>>> print(raw_data[0].keys())
|
|
157
|
+
"""
|
|
158
|
+
# Normalize and validate inputs
|
|
159
|
+
symbol = normalize_symbol(symbol)
|
|
160
|
+
lang = normalize_language(lang)
|
|
161
|
+
|
|
162
|
+
if not symbol:
|
|
163
|
+
error_msg = "Stock symbol cannot be empty"
|
|
164
|
+
logger.error(error_msg)
|
|
165
|
+
raise ValueError(error_msg)
|
|
166
|
+
|
|
167
|
+
# Build URL with symbol and language parameters
|
|
168
|
+
endpoint = SET_BOARD_OF_DIRECTOR_ENDPOINT.format(symbol=symbol)
|
|
169
|
+
url = f"{self.base_url}{endpoint}?lang={lang}"
|
|
170
|
+
|
|
171
|
+
logger.info(
|
|
172
|
+
f"Fetching raw board of directors for '{symbol}' (lang={lang}) from {url}"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async with AsyncDataFetcher(config=self.config) as fetcher:
|
|
176
|
+
# Get optimized headers for SET API with symbol-specific referer
|
|
177
|
+
referer = f"https://www.set.or.th/en/market/product/stock/quote/{symbol}/overview"
|
|
178
|
+
headers = AsyncDataFetcher.get_set_api_headers(referer=referer)
|
|
179
|
+
|
|
180
|
+
# Fetch raw response - SessionManager handles cookies automatically
|
|
181
|
+
response = await fetcher.fetch(url, headers=headers)
|
|
182
|
+
|
|
183
|
+
# Check for errors
|
|
184
|
+
if response.status_code != 200:
|
|
185
|
+
error_msg = (
|
|
186
|
+
f"Failed to fetch board of directors for {symbol}: "
|
|
187
|
+
f"HTTP {response.status_code}"
|
|
188
|
+
)
|
|
189
|
+
logger.error(error_msg)
|
|
190
|
+
raise Exception(error_msg)
|
|
191
|
+
|
|
192
|
+
# Parse JSON
|
|
193
|
+
import json
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
data = json.loads(response.text)
|
|
197
|
+
except json.JSONDecodeError as e:
|
|
198
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
199
|
+
logger.debug(f"Response text: {response.text[:500]}")
|
|
200
|
+
raise
|
|
201
|
+
|
|
202
|
+
# Validate that data is a list
|
|
203
|
+
if not isinstance(data, list):
|
|
204
|
+
error_msg = (
|
|
205
|
+
f"Expected list response but got {type(data).__name__}: "
|
|
206
|
+
f"{str(data)[:200]}"
|
|
207
|
+
)
|
|
208
|
+
logger.error(error_msg)
|
|
209
|
+
raise Exception(error_msg)
|
|
210
|
+
|
|
211
|
+
logger.debug(f"Raw response: {len(data)} directors")
|
|
212
|
+
return data
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Convenience function for quick access
|
|
216
|
+
async def get_board_of_directors(
|
|
217
|
+
symbol: str,
|
|
218
|
+
lang: str = "en",
|
|
219
|
+
config: FetcherConfig | None = None,
|
|
220
|
+
) -> list[Director]:
|
|
221
|
+
"""
|
|
222
|
+
Convenience function to fetch board of directors.
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
symbol: Stock symbol (e.g., "CPALL", "PTT", "mint")
|
|
226
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
227
|
+
config: Optional fetcher configuration
|
|
228
|
+
|
|
229
|
+
Returns:
|
|
230
|
+
List of Director objects with name and positions
|
|
231
|
+
|
|
232
|
+
Example:
|
|
233
|
+
>>> from settfex.services.set.stock import get_board_of_directors
|
|
234
|
+
>>> # Uses SessionManager for automatic cookie handling
|
|
235
|
+
>>> directors = await get_board_of_directors("MINT")
|
|
236
|
+
>>> for director in directors[:5]:
|
|
237
|
+
... print(f"{director.name}: {', '.join(director.positions)}")
|
|
238
|
+
"""
|
|
239
|
+
service = BoardOfDirectorService(config=config)
|
|
240
|
+
return await service.fetch_board_of_directors(symbol=symbol, lang=lang)
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"""SET Stock Corporate Action Service - Fetch corporate action data for stock symbols."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from loguru import logger
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
|
+
|
|
9
|
+
from settfex.services.set.constants import SET_BASE_URL, SET_CORPORATE_ACTION_ENDPOINT
|
|
10
|
+
from settfex.services.set.stock.utils import normalize_language, normalize_symbol
|
|
11
|
+
from settfex.utils.data_fetcher import AsyncDataFetcher, FetcherConfig
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CorporateAction(BaseModel):
|
|
15
|
+
"""Model for individual corporate action."""
|
|
16
|
+
|
|
17
|
+
symbol: str = Field(description="Stock symbol/ticker")
|
|
18
|
+
name: str = Field(description="Company name (may be empty)")
|
|
19
|
+
ca_type: str = Field(alias="caType", description="Corporate action type (e.g., XD, XM)")
|
|
20
|
+
type: str = Field(description="Action type")
|
|
21
|
+
|
|
22
|
+
# Common fields
|
|
23
|
+
book_close_date: datetime | None = Field(
|
|
24
|
+
default=None, alias="bookCloseDate", description="Book closure date"
|
|
25
|
+
)
|
|
26
|
+
record_date: datetime | None = Field(
|
|
27
|
+
default=None, alias="recordDate", description="Record date"
|
|
28
|
+
)
|
|
29
|
+
remark: str | None = Field(default=None, description="Additional remarks or notes")
|
|
30
|
+
x_date: datetime | None = Field(
|
|
31
|
+
default=None, alias="xdate", description="Ex-date (ex-dividend or ex-rights)"
|
|
32
|
+
)
|
|
33
|
+
x_session: str | None = Field(default=None, alias="xSession", description="Ex-date session")
|
|
34
|
+
|
|
35
|
+
# Dividend-specific fields (XD type)
|
|
36
|
+
payment_date: datetime | None = Field(
|
|
37
|
+
default=None, alias="paymentDate", description="Dividend payment date"
|
|
38
|
+
)
|
|
39
|
+
begin_operation: datetime | None = Field(
|
|
40
|
+
default=None, alias="beginOperation", description="Operation period start date"
|
|
41
|
+
)
|
|
42
|
+
end_operation: datetime | None = Field(
|
|
43
|
+
default=None, alias="endOperation", description="Operation period end date"
|
|
44
|
+
)
|
|
45
|
+
source_of_dividend: str | None = Field(
|
|
46
|
+
default=None, alias="sourceOfDividend", description="Source of dividend (e.g., Net Profit)"
|
|
47
|
+
)
|
|
48
|
+
dividend: float | None = Field(default=None, description="Dividend amount per share")
|
|
49
|
+
currency: str | None = Field(default=None, description="Currency code (e.g., Baht)")
|
|
50
|
+
ratio: str | None = Field(default=None, description="Dividend ratio (e.g., '15 : 1')")
|
|
51
|
+
dividend_type: str | None = Field(
|
|
52
|
+
default=None, alias="dividendType", description="Type of dividend (e.g., Cash Dividend)"
|
|
53
|
+
)
|
|
54
|
+
approximate_payment_date: datetime | None = Field(
|
|
55
|
+
default=None, alias="approximatePaymentDate", description="Approximate payment date"
|
|
56
|
+
)
|
|
57
|
+
tentative_dividend_flag: bool | None = Field(
|
|
58
|
+
default=None, alias="tentativeDividendFlag", description="Flag for tentative dividend"
|
|
59
|
+
)
|
|
60
|
+
tentative_dividend: float | None = Field(
|
|
61
|
+
default=None, alias="tentativeDividend", description="Tentative dividend amount"
|
|
62
|
+
)
|
|
63
|
+
dividend_payment: str | None = Field(
|
|
64
|
+
default=None, alias="dividendPayment", description="Dividend payment amount as string"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Meeting-specific fields (XM type)
|
|
68
|
+
meeting_date: datetime | None = Field(
|
|
69
|
+
default=None, alias="meetingDate", description="Meeting date and time"
|
|
70
|
+
)
|
|
71
|
+
agenda: str | None = Field(default=None, description="Meeting agenda items")
|
|
72
|
+
venue: str | None = Field(default=None, description="Meeting venue location")
|
|
73
|
+
meeting_type: str | None = Field(
|
|
74
|
+
default=None, alias="meetingType", description="Type of meeting (e.g., AGM, EGM)"
|
|
75
|
+
)
|
|
76
|
+
inquiry_date: datetime | None = Field(
|
|
77
|
+
default=None, alias="inquiryDate", description="Inquiry date"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
model_config = ConfigDict(
|
|
81
|
+
populate_by_name=True, # Allow both field name and alias
|
|
82
|
+
str_strip_whitespace=True, # Strip whitespace from strings
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class CorporateActionService:
|
|
87
|
+
"""
|
|
88
|
+
Service for fetching corporate action data from SET API.
|
|
89
|
+
|
|
90
|
+
This service provides async methods to fetch corporate action data for individual
|
|
91
|
+
stock symbols from the Stock Exchange of Thailand (SET), including dividend
|
|
92
|
+
announcements (XD), shareholder meetings (XM), and other corporate events.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, config: FetcherConfig | None = None) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Initialize the corporate action service.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
config: Optional fetcher configuration (uses defaults if None)
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
>>> # Default: Uses SessionManager for automatic cookie handling
|
|
104
|
+
>>> service = CorporateActionService()
|
|
105
|
+
"""
|
|
106
|
+
self.config = config or FetcherConfig()
|
|
107
|
+
self.base_url = SET_BASE_URL
|
|
108
|
+
logger.info(f"CorporateActionService initialized with base_url={self.base_url}")
|
|
109
|
+
|
|
110
|
+
async def fetch_corporate_actions(
|
|
111
|
+
self, symbol: str, lang: str = "en"
|
|
112
|
+
) -> list[CorporateAction]:
|
|
113
|
+
"""
|
|
114
|
+
Fetch corporate actions for a specific stock symbol.
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
symbol: Stock symbol (e.g., "AOT", "PTT", "cpall")
|
|
118
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
List of CorporateAction objects with corporate action data
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ValueError: If symbol is empty or language is invalid
|
|
125
|
+
Exception: If request fails or response cannot be parsed
|
|
126
|
+
|
|
127
|
+
Example:
|
|
128
|
+
>>> service = CorporateActionService()
|
|
129
|
+
>>> actions = await service.fetch_corporate_actions("AOT", lang="en")
|
|
130
|
+
>>> for action in actions:
|
|
131
|
+
... if action.ca_type == "XD":
|
|
132
|
+
... print(f"Dividend: {action.dividend} {action.currency}")
|
|
133
|
+
... print(f"XD Date: {action.x_date}")
|
|
134
|
+
... elif action.ca_type == "XM":
|
|
135
|
+
... print(f"Meeting: {action.meeting_type}")
|
|
136
|
+
... print(f"Agenda: {action.agenda}")
|
|
137
|
+
"""
|
|
138
|
+
# Normalize and validate inputs
|
|
139
|
+
symbol = normalize_symbol(symbol)
|
|
140
|
+
lang = normalize_language(lang)
|
|
141
|
+
|
|
142
|
+
if not symbol:
|
|
143
|
+
error_msg = "Stock symbol cannot be empty"
|
|
144
|
+
logger.error(error_msg)
|
|
145
|
+
raise ValueError(error_msg)
|
|
146
|
+
|
|
147
|
+
# Build URL with symbol and language parameters
|
|
148
|
+
endpoint = SET_CORPORATE_ACTION_ENDPOINT.format(symbol=symbol)
|
|
149
|
+
url = f"{self.base_url}{endpoint}?lang={lang}"
|
|
150
|
+
|
|
151
|
+
logger.info(f"Fetching corporate actions for symbol '{symbol}' (lang={lang}) from {url}")
|
|
152
|
+
|
|
153
|
+
async with AsyncDataFetcher(config=self.config) as fetcher:
|
|
154
|
+
# Get optimized headers for SET API with symbol-specific referer
|
|
155
|
+
# This is critical for bypassing Incapsula bot detection
|
|
156
|
+
referer = f"https://www.set.or.th/en/market/product/stock/quote/{symbol}/overview"
|
|
157
|
+
headers = AsyncDataFetcher.get_set_api_headers(referer=referer)
|
|
158
|
+
|
|
159
|
+
# Fetch raw response - SessionManager handles cookies automatically
|
|
160
|
+
response = await fetcher.fetch(url, headers=headers)
|
|
161
|
+
|
|
162
|
+
# Check for errors
|
|
163
|
+
if response.status_code != 200:
|
|
164
|
+
error_msg = (
|
|
165
|
+
f"Failed to fetch corporate actions for {symbol}: "
|
|
166
|
+
f"HTTP {response.status_code}"
|
|
167
|
+
)
|
|
168
|
+
logger.error(error_msg)
|
|
169
|
+
raise Exception(error_msg)
|
|
170
|
+
|
|
171
|
+
# Parse JSON
|
|
172
|
+
import json
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
data = json.loads(response.text)
|
|
176
|
+
except json.JSONDecodeError as e:
|
|
177
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
178
|
+
logger.debug(f"Response text: {response.text[:500]}")
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
# Data should be a list
|
|
182
|
+
if not isinstance(data, list):
|
|
183
|
+
error_msg = f"Expected list response, got {type(data).__name__}"
|
|
184
|
+
logger.error(error_msg)
|
|
185
|
+
raise ValueError(error_msg)
|
|
186
|
+
|
|
187
|
+
# Parse and validate each corporate action using Pydantic
|
|
188
|
+
corporate_actions = [CorporateAction(**item) for item in data]
|
|
189
|
+
|
|
190
|
+
logger.info(
|
|
191
|
+
f"Successfully fetched {len(corporate_actions)} corporate action(s) for {symbol}"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Log summary by type
|
|
195
|
+
type_counts: dict[str, int] = {}
|
|
196
|
+
for action in corporate_actions:
|
|
197
|
+
type_counts[action.ca_type] = type_counts.get(action.ca_type, 0) + 1
|
|
198
|
+
|
|
199
|
+
if type_counts:
|
|
200
|
+
summary = ", ".join([f"{k}={v}" for k, v in type_counts.items()])
|
|
201
|
+
logger.debug(f"Corporate action summary for {symbol}: {summary}")
|
|
202
|
+
|
|
203
|
+
return corporate_actions
|
|
204
|
+
|
|
205
|
+
async def fetch_corporate_actions_raw(
|
|
206
|
+
self, symbol: str, lang: str = "en"
|
|
207
|
+
) -> list[dict[str, Any]]:
|
|
208
|
+
"""
|
|
209
|
+
Fetch corporate actions as raw list of dictionaries without Pydantic validation.
|
|
210
|
+
|
|
211
|
+
Useful for debugging or when you need the raw API response.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
symbol: Stock symbol (e.g., "AOT", "PTT", "cpall")
|
|
215
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
216
|
+
|
|
217
|
+
Returns:
|
|
218
|
+
Raw list of dictionaries from API
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
ValueError: If symbol is empty or language is invalid
|
|
222
|
+
Exception: If request fails
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> service = CorporateActionService()
|
|
226
|
+
>>> raw_data = await service.fetch_corporate_actions_raw("AOT")
|
|
227
|
+
>>> print(f"Found {len(raw_data)} actions")
|
|
228
|
+
>>> for action in raw_data:
|
|
229
|
+
... print(action.keys())
|
|
230
|
+
"""
|
|
231
|
+
# Normalize and validate inputs
|
|
232
|
+
symbol = normalize_symbol(symbol)
|
|
233
|
+
lang = normalize_language(lang)
|
|
234
|
+
|
|
235
|
+
if not symbol:
|
|
236
|
+
error_msg = "Stock symbol cannot be empty"
|
|
237
|
+
logger.error(error_msg)
|
|
238
|
+
raise ValueError(error_msg)
|
|
239
|
+
|
|
240
|
+
# Build URL with symbol and language parameters
|
|
241
|
+
endpoint = SET_CORPORATE_ACTION_ENDPOINT.format(symbol=symbol)
|
|
242
|
+
url = f"{self.base_url}{endpoint}?lang={lang}"
|
|
243
|
+
|
|
244
|
+
logger.info(
|
|
245
|
+
f"Fetching raw corporate actions for '{symbol}' (lang={lang}) from {url}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
async with AsyncDataFetcher(config=self.config) as fetcher:
|
|
249
|
+
# Get optimized headers for SET API with symbol-specific referer
|
|
250
|
+
referer = f"https://www.set.or.th/en/market/product/stock/quote/{symbol}/overview"
|
|
251
|
+
headers = AsyncDataFetcher.get_set_api_headers(referer=referer)
|
|
252
|
+
|
|
253
|
+
# Fetch raw response - SessionManager handles cookies automatically
|
|
254
|
+
response = await fetcher.fetch(url, headers=headers)
|
|
255
|
+
|
|
256
|
+
# Check for errors
|
|
257
|
+
if response.status_code != 200:
|
|
258
|
+
error_msg = (
|
|
259
|
+
f"Failed to fetch corporate actions for {symbol}: "
|
|
260
|
+
f"HTTP {response.status_code}"
|
|
261
|
+
)
|
|
262
|
+
logger.error(error_msg)
|
|
263
|
+
raise Exception(error_msg)
|
|
264
|
+
|
|
265
|
+
# Parse JSON
|
|
266
|
+
import json
|
|
267
|
+
|
|
268
|
+
try:
|
|
269
|
+
data = json.loads(response.text)
|
|
270
|
+
except json.JSONDecodeError as e:
|
|
271
|
+
logger.error(f"Failed to parse JSON response: {e}")
|
|
272
|
+
logger.debug(f"Response text: {response.text[:500]}")
|
|
273
|
+
raise
|
|
274
|
+
|
|
275
|
+
# Data should be a list
|
|
276
|
+
if not isinstance(data, list):
|
|
277
|
+
error_msg = f"Expected list response, got {type(data).__name__}"
|
|
278
|
+
logger.error(error_msg)
|
|
279
|
+
raise ValueError(error_msg)
|
|
280
|
+
|
|
281
|
+
logger.debug(f"Raw response contains {len(data)} corporate action(s)")
|
|
282
|
+
return data
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
# Convenience function for quick access
|
|
286
|
+
async def get_corporate_actions(
|
|
287
|
+
symbol: str,
|
|
288
|
+
lang: str = "en",
|
|
289
|
+
config: FetcherConfig | None = None,
|
|
290
|
+
) -> list[CorporateAction]:
|
|
291
|
+
"""
|
|
292
|
+
Convenience function to fetch corporate action data.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
symbol: Stock symbol (e.g., "AOT", "PTT", "cpall")
|
|
296
|
+
lang: Language for response ('en' or 'th', default: 'en')
|
|
297
|
+
config: Optional fetcher configuration
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
List of CorporateAction objects with corporate action data
|
|
301
|
+
|
|
302
|
+
Example:
|
|
303
|
+
>>> from settfex.services.set.stock import get_corporate_actions
|
|
304
|
+
>>> # Uses SessionManager for automatic cookie handling
|
|
305
|
+
>>> actions = await get_corporate_actions("AOT")
|
|
306
|
+
>>> for action in actions:
|
|
307
|
+
... print(f"{action.ca_type}: {action.x_date}")
|
|
308
|
+
"""
|
|
309
|
+
service = CorporateActionService(config=config)
|
|
310
|
+
return await service.fetch_corporate_actions(symbol=symbol, lang=lang)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Financial services for fetching balance sheet, income statement, and cash flow data."""
|
|
2
|
+
|
|
3
|
+
from settfex.services.set.stock.financial.financial import (
|
|
4
|
+
Account,
|
|
5
|
+
BalanceSheet,
|
|
6
|
+
CashFlow,
|
|
7
|
+
FinancialService,
|
|
8
|
+
FinancialStatement,
|
|
9
|
+
IncomeStatement,
|
|
10
|
+
get_balance_sheet,
|
|
11
|
+
get_cash_flow,
|
|
12
|
+
get_income_statement,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"Account",
|
|
17
|
+
"FinancialStatement",
|
|
18
|
+
"BalanceSheet",
|
|
19
|
+
"IncomeStatement",
|
|
20
|
+
"CashFlow",
|
|
21
|
+
"FinancialService",
|
|
22
|
+
"get_balance_sheet",
|
|
23
|
+
"get_income_statement",
|
|
24
|
+
"get_cash_flow",
|
|
25
|
+
]
|