quant-agent 0.3.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.
quant_agent/funds.py ADDED
@@ -0,0 +1,270 @@
1
+
2
+ """
3
+ Mutual fund data from Fmarket (https://fmarket.vn): fund listings, holdings,
4
+ and NAV history. All still working as of 2026.
5
+ """
6
+
7
+ from .config import *
8
+
9
+ _SUPPORTED_LANGUAGES = {'en': 'English', 'vi': 'Tiếng Việt'}
10
+
11
+ def _normalize_lang(lang):
12
+ """Fmarket functions only support 'vi'/'en'; fall back to 'vi' with a warning."""
13
+ if lang.lower() not in _SUPPORTED_LANGUAGES:
14
+ print(f"Warning: Unsupported language '{lang}', defaulting to Vietnamese.")
15
+ return 'vi'
16
+ return lang.lower()
17
+
18
+
19
+ def funds_listing(lang='vi', fund_type="", headers=fmarket_headers):
20
+ """
21
+ Retrieve list of available funds from Fmarket. Live data is retrieved from the Fmarket. Visit https://fmarket.vn to learn more.
22
+
23
+ Parameters
24
+ ----------
25
+ lang: str
26
+ language of the column label. Supported: 'vi' (default), 'en'
27
+ fund_type: str
28
+ "" (all funds, default), "BALANCED", "BOND", or "STOCK"
29
+ headers: dict
30
+ headers of the request
31
+
32
+ Returns
33
+ -------
34
+ df: pd.DataFrame
35
+ DataFrame of all available mutual fund listed on Fmarket.
36
+ """
37
+ lang = _normalize_lang(lang)
38
+
39
+ fund_type_map = {"": [], "BALANCED": ["BALANCED"], "BOND": ["BOND"], "STOCK": ["STOCK"]}
40
+ if fund_type not in fund_type_map:
41
+ print(f"Error: Unsupported fund type '{fund_type}', defaulting to all funds.")
42
+ fund_asset_types = fund_type_map.get(fund_type, [])
43
+
44
+ payload = {
45
+ "types": ["NEW_FUND", "TRADING_FUND"],
46
+ "issuerIds": [],
47
+ "sortOrder": "DESC",
48
+ "sortField": "navTo6Months",
49
+ "page": 1,
50
+ "pageSize": 100,
51
+ "isIpo": False,
52
+ "fundAssetTypes": fund_asset_types,
53
+ "bondRemainPeriods": [],
54
+ "searchField": "",
55
+ "isBuyByReward": False,
56
+ "thirdAppIds": []
57
+ }
58
+ url = "https://api.fmarket.vn/res/products/filter"
59
+ data = fetch_json('POST', url, json=payload, headers=headers, source='Fmarket')
60
+ if data is None:
61
+ return None
62
+
63
+ print('Total number of funds currently listed on Fmarket: ', data['data']['total'])
64
+ df = json_normalize(data, record_path=['data', 'rows'])
65
+
66
+ column_subset = [
67
+ 'id', 'shortName', 'name', 'dataFundAssetType.name', 'owner.name',
68
+ 'managementFee', 'productNavChange.navTo6Months', 'productNavChange.navTo36Months',
69
+ 'nav', 'code', 'vsdFeeId',
70
+ ]
71
+ df = df[column_subset]
72
+ df = df.sort_values(by='productNavChange.navTo36Months', ascending=False)
73
+
74
+ language_mappings = {
75
+ 'vi': {
76
+ 'id': 'fundId', 'shortName': 'Tên viết tắt', 'name': 'Tên CCQ',
77
+ 'dataFundAssetType.name': 'Loại Quỹ', 'owner.name': 'Tổ chức phát hành',
78
+ 'managementFee': 'Phí quản lý (%)', 'productNavChange.navTo6Months': 'Lợi nhuận 6 tháng gần nhất (%)',
79
+ 'productNavChange.navTo36Months': 'Lợi nhuận 3 năm gần nhất (%)', 'nav': 'Giá gần nhất',
80
+ },
81
+ 'en': {
82
+ 'shortName': 'Fund short name', 'name': 'Fund name', 'owner.name': 'Fund owner',
83
+ 'dataFundAssetType.name': 'Fund asset type', 'managementFee': 'Management fee (%)',
84
+ 'productNavChange.navTo6Months': '6-month NAV change (%)',
85
+ 'productNavChange.navTo36Months': '3-year NAV change (%)', 'nav': 'NAV/Unit (VND)', 'id': 'fundId',
86
+ }
87
+ }
88
+ df = df.astype({'id': 'object'})
89
+ df.rename(columns=language_mappings[lang], inplace=True)
90
+ return df.reset_index(drop=True)
91
+
92
+
93
+ def fund_filter (payload={"types": ["NEW_FUND", "TRADING_FUND"], "pageSize": 100, "searchField": "VESAF"}, columns=['id', 'shortName', 'name', 'description'], headers=fmarket_headers):
94
+ """
95
+ Search/filter funds on Fmarket by an arbitrary payload (see Fmarket's product filter API).
96
+ Parameters:
97
+ payload (dict): filter criteria passed straight to Fmarket's `/res/products/filter` endpoint.
98
+ columns (list): subset of columns to keep from the response.
99
+ headers (dict): headers of the request.
100
+ """
101
+ url = "https://api.fmarket.vn/res/products/filter"
102
+ data = fetch_json('POST', url, headers=headers, data=json.dumps(payload), source='Fmarket')
103
+ if data is None:
104
+ return None
105
+ df = json_normalize(data, record_path=['data', 'rows'])
106
+ return df[columns]
107
+
108
+
109
+ def fund_details (symbol='SSISCA', type='top_holding_list', headers=fmarket_headers):
110
+ """
111
+ Retrieve fund details for a specific fund. Live data is retrieved from the Fmarket API.
112
+ Parameters:
113
+ symbol (str): ticker of a fund
114
+ type (str): type of data to retrieve. Default is 'top_holding_list', other options are 'industry_holding_list'
115
+ headers (dict): headers of the request. Default is fmaker_headers
116
+ Returns:
117
+ df (pd.DataFrame): DataFrame of the current top holdings of the selected fund.
118
+ """
119
+ matches = fund_filter(payload={"searchField": symbol, "pageSize": 1, "types": ["NEW_FUND", "TRADING_FUND"]})
120
+ if matches is None or matches.empty:
121
+ print(f"No fund found matching '{symbol}'.")
122
+ return None
123
+ fund_id = str(matches['id'][0])
124
+ print(f'Getting data for {symbol}')
125
+
126
+ detail_fns = {
127
+ 'top_holding_list': fund_top_holding,
128
+ 'industry_holding_list': fund_industry_holding,
129
+ 'nav_report': fund_nav_report,
130
+ 'asset_holding_list': fund_asset_holding,
131
+ }
132
+ if type not in detail_fns:
133
+ print(f"Unknown type '{type}'. Use one of: {', '.join(detail_fns)}.")
134
+ return None
135
+ df = detail_fns[type](fundId=fund_id, lang='vi', headers=headers)
136
+ if df is not None:
137
+ df['symbol'] = symbol
138
+ return df
139
+
140
+
141
+ def _fetch_fund_product(fund_id, headers):
142
+ """Shared fetcher for the per-fund `/res/products/{fundId}` endpoint used by the
143
+ holdings/asset-allocation functions below."""
144
+ url = f"https://api.fmarket.vn/res/products/{fund_id}"
145
+ return fetch_json('GET', url, headers=headers, source='Fmarket')
146
+
147
+
148
+ def fund_top_holding(fundId=23, lang='vi', headers=fmarket_headers):
149
+ """
150
+ Retrieve list of top 10 holdings in the specified fund. Live data is retrieved from the Fmarket API.
151
+
152
+ Parameters
153
+ ----------
154
+ fundId (int): id of a fund in fmarket database. Retrieved from the 'fundId_fmarket' column by calling the function mutual_fund_listing()
155
+ lang (str): language of the column label. Supported: 'vi' (default), 'en'
156
+ headers (dict): headers of the request. Default is fmaker_headers
157
+
158
+ Returns
159
+ -------
160
+ df (pd.DataFrame): DataFrame of the current top 10 holdings of the selected fund.
161
+ """
162
+ lang = _normalize_lang(lang)
163
+ data = _fetch_fund_product(fundId, headers)
164
+ if data is None:
165
+ return None
166
+
167
+ # A fund may hold equities, fixed income securities, or both.
168
+ df = pd.DataFrame()
169
+ df_stock = json_normalize(data, record_path=['data', 'productTopHoldingList'])
170
+ if not df_stock.empty:
171
+ df_stock['updateAt'] = pd.to_datetime(df_stock['updateAt'], unit='ms', utc=True).dt.strftime('%Y-%m-%d')
172
+ df = pd.concat([df, df_stock])
173
+
174
+ df_bond = json_normalize(data, record_path=['data', 'productTopHoldingBondList'])
175
+ if not df_bond.empty:
176
+ df_bond['updateAt'] = pd.to_datetime(df_bond['updateAt'], unit='ms', utc=True).dt.strftime('%Y-%m-%d')
177
+ df = pd.concat([df, df_bond])
178
+
179
+ if df.empty:
180
+ print(f"Warning: No data available for fundId {fundId}.")
181
+ return None
182
+
183
+ df['fundId'] = str(fundId)
184
+ column_subset = ['stockCode', 'industry', 'netAssetPercent', 'type', 'updateAt', 'fundId']
185
+ df = df[column_subset]
186
+
187
+ language_mappings = {
188
+ 'vi': {'stockCode': 'Tên', 'industry': 'Ngành', 'netAssetPercent': '% Giá trị tài sản', 'type': 'Loại tài sản', 'updateAt': 'Cập nhật lần cuối'},
189
+ 'en': {'stockCode': 'Stock code', 'industry': 'Industry', 'netAssetPercent': '% NAV'},
190
+ }
191
+ df.rename(columns=language_mappings[lang], inplace=True)
192
+ return df
193
+
194
+
195
+ def fund_industry_holding (fundId=23, lang='vi', headers=fmarket_headers):
196
+ """
197
+ Retrieve list of industries and fund distribution for specific fundID. Live data is retrieved from the Fmarket API.
198
+ """
199
+ lang = _normalize_lang(lang)
200
+ data = _fetch_fund_product(fundId, headers)
201
+ if data is None:
202
+ return None
203
+ df = json_normalize(data, record_path=['data', 'productIndustriesHoldingList'])
204
+ if 'id' in df.columns:
205
+ df.drop(columns=['id'], inplace=True)
206
+ language_mappings = {
207
+ 'vi': {'industry': 'Ngành', 'assetPercent': '% Giá trị tài sản'},
208
+ 'en': {'industry': 'Industry', 'assetPercent': '% NAV'},
209
+ }
210
+ df.rename(columns=language_mappings[lang], inplace=True)
211
+ return df
212
+
213
+
214
+ def fund_nav_report(fundId='23', lang='vi', headers=fmarket_headers):
215
+ """Retrieve all available daily NAV data point of the specified fund. Live data is retrieved from the Fmarket API.
216
+ Parameters
217
+ ----------
218
+ fundId: int
219
+ id of a fund in fmarket database. Retrieved from the 'fundId' column by calling the function mutual_fund_listing()
220
+ lang: str
221
+ language of the column label. Supported: 'vi' (default), 'en'
222
+ headers: dict
223
+ headers of the request. Default is fmaker_headers
224
+
225
+ Returns
226
+ -------
227
+ df: pd.DataFrame
228
+ DataFrame of all avalaible daily NAV data points of the selected fund.
229
+ """
230
+ lang = _normalize_lang(lang)
231
+ url = "https://api.fmarket.vn/res/product/get-nav-history"
232
+ payload = {
233
+ "isAllData": 1,
234
+ "productId": fundId,
235
+ "fromDate": None,
236
+ "toDate": datetime.now().strftime('%Y%m%d'),
237
+ }
238
+ data = fetch_json('POST', url, json=payload, headers=headers, source='Fmarket')
239
+ if data is None:
240
+ return None
241
+ df = json_normalize(data, record_path=['data'])
242
+ df = df[['navDate', 'nav', 'productId']]
243
+ language_mappings = {
244
+ 'vi': {'navDate': 'Ngày', 'nav': 'Giá trị tài sản ròng/CCQ (VND)', 'productId': 'fundId'},
245
+ 'en': {'navDate': 'Date', 'nav': 'NAV/Unit (VND)', 'productId': 'fundId'},
246
+ }
247
+ df.rename(columns=language_mappings[lang], inplace=True)
248
+ return df
249
+
250
+
251
+ def fund_asset_holding (fundId=23, lang='vi', headers=fmarket_headers):
252
+ """
253
+ Retrieve list of assets holding allocation for specific fundID. Live data is retrieved from the Fmarket API.
254
+ """
255
+ lang = _normalize_lang(lang)
256
+ data = _fetch_fund_product(fundId, headers)
257
+ if data is None:
258
+ return None
259
+ df = json_normalize(data, record_path=['data', 'productAssetHoldingList'])
260
+ try:
261
+ df.drop(columns=['id', 'assetType.id', 'assetType.code', 'assetType.colorCode', 'createAt'], inplace=True)
262
+ df['updateAt'] = pd.to_datetime(df['updateAt'], unit='ms', utc=True).dt.strftime('%Y-%m-%d')
263
+ except KeyError:
264
+ pass
265
+ language_mappings = {
266
+ 'vi': {'assetPercent': 'Tỉ trọng', 'assetType.name': 'Loại tài sản'},
267
+ 'en': {'assetPercent': 'assetPercent', 'assetType.name': 'assetType'},
268
+ }
269
+ df.rename(columns=language_mappings[lang], inplace=True)
270
+ return df
@@ -0,0 +1,54 @@
1
+ # DNSE API: https://www.dnse.vn
2
+ """
3
+ Data export helpers for third-party analysis tools. For DNSE's authenticated
4
+ TRADING API (login, place orders, cancel orders), see the `broker` subpackage
5
+ instead — that is a fundamentally different, higher-risk concern (it can
6
+ place real trades) and is kept separate so it's never confused with this
7
+ package's read-only market-data functions.
8
+ """
9
+
10
+ import os
11
+
12
+ from .config import *
13
+ from .technical import stock_historical_data
14
+
15
+ # AMIBROKER CSV EXPORT
16
+
17
+ def amibroker_ohlc_export (path='', symbol='TCB', start_date='2023-01-01', end_date='2023-11-08', resolution='1D', type='stock', beautify=False, source='DNSE'):
18
+ """
19
+ Get historical price data from entrade.com.vn. The default setting return dataset with unit price is thousands VND.
20
+ Parameters:
21
+ path (str): path to save the csv file. Default is the current directory
22
+ symbol (str): ticker of a stock or index. Available indices are: VNINDEX, VN30, HNX, HNX30, UPCOM, VNXALLSHARE, VN30F1M, VN30F2M, VN30F1Q, VN30F2Q
23
+ start_date (str): start date of the historical price data
24
+ end_date (str): end date of the historical price data
25
+ resolution (str): resolution of the historical price data. Default is '1D' (daily), other options are '1' (1 minute), 15 (15 minutes), 30 (30 minutes), '1H' (hourly)
26
+ type (str): stock, index, or derivative. Default is 'stock'
27
+ beautify (bool): if True, convert open, high, low, close to VND for stock symbols. Default is True which means the unit price is thousands VND
28
+ source (str): data source. Default is 'DNSE' EntradeX, other option is 'VCI' (VietCap Securities for daily longterm data)
29
+ Returns:
30
+ :obj:`pandas.DataFrame`:
31
+ | time | open | high | low | close | volume |
32
+ | ----------- | ---- | ---- | --- | ----- | ------ |
33
+ | YYYY-mm-dd | xxxx | xxxx | xxx | xxxxx | xxxxxx |
34
+ """
35
+ df = stock_historical_data(symbol=symbol, start_date=start_date, end_date=end_date, resolution=resolution, type=type, beautify=beautify, decor=False, source=source)
36
+ if df is None:
37
+ return None
38
+
39
+ df['time'] = pd.to_datetime(df['time'], format='%Y-%m-%d')
40
+ amibroker_columns = {'ticker': '<Ticker>', 'time': '<DTYYYYMMDD>', 'open': '<Open>', 'high': '<High>', 'low': '<Low>', 'close': '<Close>', 'volume': '<Volume>'}
41
+ if resolution == '1D':
42
+ df = df.rename(columns=amibroker_columns)
43
+ df['<DTYYYYMMDD>'] = df['<DTYYYYMMDD>'].dt.strftime('%Y%m%d')
44
+ df = df[['<Ticker>', '<DTYYYYMMDD>', '<Open>', '<High>', '<Low>', '<Close>', '<Volume>']]
45
+ else:
46
+ df['<Time>'] = df['time'].dt.strftime('%H:%M')
47
+ df = df.rename(columns=amibroker_columns)
48
+ df['<DTYYYYMMDD>'] = df['<DTYYYYMMDD>'].dt.strftime('%Y%m%d')
49
+ df = df[['<Ticker>', '<DTYYYYMMDD>', '<Open>', '<High>', '<Low>', '<Close>', '<Volume>', '<Time>']]
50
+
51
+ if path == '':
52
+ path = os.getcwd()
53
+ df.to_csv(f'{path}/amibroker_export_{type}_{symbol}_from_{start_date}_to{end_date}_{resolution}.csv', index=False)
54
+ print('Data preview:\n', df.head(5))
@@ -0,0 +1,34 @@
1
+ """
2
+ Everything about talking to an individual unofficial data source: its
3
+ browser-mimicking headers/cookies, and (for VCI) its session handshake.
4
+ Generic HTTP plumbing that isn't specific to any one source lives in
5
+ `http_client.py`.
6
+
7
+ To add a new source: create `sources/<name>.py` with its headers dict (and
8
+ any source-specific auth/handshake helper it needs), then add one line
9
+ below (both the `from .<name> import *` AND its public names in `__all__`).
10
+ Nothing outside this package needs to change - `config.py` re-exports this
11
+ whole package via `from .sources import *`.
12
+
13
+ `__all__` is required here, not cosmetic: without it, `import *` would also
14
+ re-export each submodule itself (e.g. a `dnse` name pointing at this file's
15
+ own `dnse.py`), which then collides with any other subpackage that happens
16
+ to have a same-named submodule (e.g. `broker/dnse.py`) once it does
17
+ `from ..config import *` and inherits that wrong name.
18
+ """
19
+
20
+ from .http_client import *
21
+ from .ssi import *
22
+ from .vps import *
23
+ from .dnse import *
24
+ from .fmarket import *
25
+ from .vci import *
26
+
27
+ __all__ = [
28
+ 'fetch', 'fetch_json',
29
+ 'ssi_headers',
30
+ 'vps_headers',
31
+ 'entrade_headers',
32
+ 'fmarket_headers',
33
+ 'vci_headers', 'vci_session_cookies',
34
+ ]
@@ -0,0 +1,18 @@
1
+
2
+ """DNSE, via its Entrade market-data gateway - historical/intraday OHLC. Working."""
3
+
4
+ entrade_headers = {
5
+ 'authority': 'services.entrade.com.vn',
6
+ 'accept': 'application/json, text/plain, */*',
7
+ 'accept-language': 'en-US,en;q=0.9',
8
+ 'dnt': '1',
9
+ 'origin': 'https://banggia.dnse.com.vn',
10
+ 'referer': 'https://banggia.dnse.com.vn/',
11
+ 'sec-ch-ua': '"Edge";v="114", "Chromium";v="114", "Not=A?Brand";v="24"',
12
+ 'sec-ch-ua-mobile': '?0',
13
+ 'sec-ch-ua-platform': '"Windows"',
14
+ 'sec-fetch-dest': 'empty',
15
+ 'sec-fetch-mode': 'cors',
16
+ 'sec-fetch-site': 'cross-site',
17
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1788.0'
18
+ }
@@ -0,0 +1,18 @@
1
+ """Fmarket (api.fmarket.vn) - mutual fund data. Working."""
2
+
3
+ fmarket_headers = {
4
+ 'authority': 'api.fmarket.vn',
5
+ 'accept': 'application/json, text/plain, */*',
6
+ 'accept-language': 'vi',
7
+ 'content-type': 'application/json',
8
+ 'dnt': '1',
9
+ 'origin': 'https://fmarket.vn',
10
+ 'referer': 'https://fmarket.vn/',
11
+ 'sec-ch-ua': '"Chromium";v="118", "Google Chrome";v="118", "Not=A?Brand";v="99"',
12
+ 'sec-ch-ua-mobile': '?0',
13
+ 'sec-ch-ua-platform': '"Windows"',
14
+ 'sec-fetch-dest': 'empty',
15
+ 'sec-fetch-mode': 'cors',
16
+ 'sec-fetch-site': 'same-site',
17
+ 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36'
18
+ }
@@ -0,0 +1,52 @@
1
+ """
2
+ Generic, source-agnostic HTTP plumbing shared by every source module in this
3
+ package. Every data-fetching function in quant_agent follows the same contract:
4
+ on success, return parsed data; on any failure (network error, non-200
5
+ status, unparsable body), print a clear message and return None instead of
6
+ raising. `fetch`/`fetch_json` centralize that contract so individual
7
+ functions only need to handle their own response shape, not repeat
8
+ status-code bookkeeping.
9
+
10
+ Source-specific concerns (headers, session handshakes) live in this
11
+ package's per-source modules (ssi.py, vci.py, dnse.py, ...), not here.
12
+ """
13
+
14
+ import requests
15
+
16
+
17
+ def fetch(method, url, *, headers=None, params=None, json=None, data=None, timeout=15, source=''):
18
+ """
19
+ Low-level request wrapper. Returns the raw `requests.Response` on a
20
+ successful (200) call, or None — after printing an error message — on a
21
+ network error or non-200 status.
22
+
23
+ Use this directly (instead of `fetch_json`) when the response body isn't
24
+ JSON, e.g. an Excel download that needs a Content-Type check first.
25
+ """
26
+ try:
27
+ response = requests.request(method, url, headers=headers, params=params, json=json, data=data, timeout=timeout)
28
+ except requests.RequestException as exc:
29
+ print(f"Error connecting to {source or url}: {exc}")
30
+ return None
31
+ if response.status_code != 200:
32
+ print(f"Error {response.status_code} when getting data from {source or url}. Details: {response.text[:500]}")
33
+ return None
34
+ return response
35
+
36
+
37
+ def fetch_json(method, url, *, headers=None, params=None, json=None, data=None, timeout=15, source=''):
38
+ """
39
+ Send a request and parse the response body as JSON.
40
+
41
+ Returns the parsed dict/list on success, or None — after printing an
42
+ error message — if the request failed, returned a non-200 status, or
43
+ the body wasn't valid JSON.
44
+ """
45
+ response = fetch(method, url, headers=headers, params=params, json=json, data=data, timeout=timeout, source=source)
46
+ if response is None:
47
+ return None
48
+ try:
49
+ return response.json()
50
+ except ValueError:
51
+ print(f"Error: {source or url} did not return valid JSON. Body preview: {response.text[:300]}")
52
+ return None
@@ -0,0 +1,27 @@
1
+
2
+ """
3
+ SSI (fiin-market / fiin-fundamental / fiin-core / iboard-query APIs).
4
+ Actively blocks automated requests with Cloudflare bot detection (403) as of
5
+ 2026 - functions using these headers may return None for that reason, which
6
+ is a source-side block this library cannot work around.
7
+ """
8
+
9
+ ssi_headers = {
10
+ 'Connection': 'keep-alive',
11
+ 'sec-ch-ua': '"Not A;Brand";v="99", "Chromium";v="98", "Google Chrome";v="98"',
12
+ 'DNT': '1',
13
+ 'sec-ch-ua-mobile': '?0',
14
+ 'X-Fiin-Key': 'KEY',
15
+ 'Content-Type': 'application/json',
16
+ 'Accept': 'application/json',
17
+ 'X-Fiin-User-ID': 'ID',
18
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/98.0.4758.102 Safari/537.36',
19
+ 'X-Fiin-Seed': 'SEED',
20
+ 'sec-ch-ua-platform': 'Windows',
21
+ 'Origin': 'https://iboard.ssi.com.vn',
22
+ 'Sec-Fetch-Site': 'same-site',
23
+ 'Sec-Fetch-Mode': 'cors',
24
+ 'Sec-Fetch-Dest': 'empty',
25
+ 'Referer': 'https://iboard.ssi.com.vn/',
26
+ 'Accept-Language': 'en-US,en;q=0.9,vi-VN;q=0.8,vi;q=0.7'
27
+ }
@@ -0,0 +1,28 @@
1
+ """
2
+ VCI (VietCap Securities) - data source for company, financial, and trading data.
3
+ """
4
+
5
+ import requests
6
+
7
+ vci_headers = {
8
+ 'Accept': 'application/json, text/plain, */*',
9
+ 'Content-Type': 'application/json',
10
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
11
+ 'Referer': 'https://trading.vietcap.com.vn/',
12
+ 'Origin': 'https://trading.vietcap.com.vn/',
13
+ }
14
+
15
+
16
+ def vci_session_cookies():
17
+ """
18
+ Some VCI endpoints (iq.vietcap.com.vn financial-statement/statistics) reject requests
19
+ without session cookies from a prior visit to trading.vietcap.com.vn. This performs a
20
+ one-time throwaway handshake to harvest those cookies. Non-fatal if it fails (returns {}),
21
+ since not every VCI endpoint requires it.
22
+ """
23
+ try:
24
+ session = requests.Session()
25
+ session.get('https://trading.vietcap.com.vn/priceboard', headers=vci_headers, timeout=10)
26
+ return session.cookies.get_dict()
27
+ except requests.RequestException:
28
+ return {}
@@ -0,0 +1,18 @@
1
+
2
+ """VPS - price depth / order book (bgapidatafeed.vps.com.vn). Working."""
3
+
4
+ vps_headers = {
5
+ 'Accept': 'application/json, text/plain, */*',
6
+ 'Accept-Language': 'en-US,en;q=0.9',
7
+ 'Connection': 'keep-alive',
8
+ 'DNT': '1',
9
+ 'Origin': 'https://banggia.vps.com.vn',
10
+ 'Referer': 'https://banggia.vps.com.vn/',
11
+ 'Sec-Fetch-Dest': 'empty',
12
+ 'Sec-Fetch-Mode': 'cors',
13
+ 'Sec-Fetch-Site': 'same-site',
14
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36',
15
+ 'sec-ch-ua': '"Not.A/Brand";v="8", "Chromium";v="114", "Google Chrome";v="114"',
16
+ 'sec-ch-ua-mobile': '?0',
17
+ 'sec-ch-ua-platform': '"Windows"'
18
+ }