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/__init__.py +25 -0
- quant_agent/broker/__init__.py +20 -0
- quant_agent/broker/dnse.py +257 -0
- quant_agent/chart.py +235 -0
- quant_agent/config.py +60 -0
- quant_agent/economics.py +1 -0
- quant_agent/fundamental.py +580 -0
- quant_agent/funds.py +270 -0
- quant_agent/integration.py +54 -0
- quant_agent/sources/__init__.py +34 -0
- quant_agent/sources/dnse.py +18 -0
- quant_agent/sources/fmarket.py +18 -0
- quant_agent/sources/http_client.py +52 -0
- quant_agent/sources/ssi.py +27 -0
- quant_agent/sources/vci.py +28 -0
- quant_agent/sources/vps.py +18 -0
- quant_agent/technical.py +134 -0
- quant_agent/trading.py +110 -0
- quant_agent/utils.py +53 -0
- quant_agent-0.3.0.dist-info/METADATA +450 -0
- quant_agent-0.3.0.dist-info/RECORD +23 -0
- quant_agent-0.3.0.dist-info/WHEEL +5 -0
- quant_agent-0.3.0.dist-info/top_level.txt +1 -0
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
|
+
}
|