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/technical.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Historical & intraday OHLC price data.
|
|
3
|
+
- `ohlc_data` / `stock_historical_data(source='DNSE')` -> DNSE/Entrade.
|
|
4
|
+
- `longterm_ohlc_data` / `stock_historical_data(source='VCI')` -> VCI (VietCap Securities).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .config import *
|
|
8
|
+
|
|
9
|
+
def stock_historical_data (symbol='TCB', start_date='2023-06-01', end_date='2023-06-17', resolution='1D', type='stock', beautify=True, decor=False, source='DNSE'):
|
|
10
|
+
"""
|
|
11
|
+
Get historical price data. The unit price is VND.
|
|
12
|
+
Parameters:
|
|
13
|
+
symbol (str): ticker of a stock or index. Available indices are: VNINDEX, VN30, HNX, HNX30, UPCOM, VNXALLSHARE, VN30F1M, VN30F2M, VN30F1Q, VN30F2Q
|
|
14
|
+
start_date (str): start date of the historical price data
|
|
15
|
+
end_date (str): end date of the historical price data
|
|
16
|
+
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). For stock, the limit of 90 days is applied to resolution 1, 15, 30, 1H.
|
|
17
|
+
type (str): stock, index, or derivative. Default is 'stock'
|
|
18
|
+
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
|
|
19
|
+
decor (bool): if True, rename columns to Title Case (Open, High, Low, Close instead of open, high, low, close) and set Time column as index. Default is False. This option helps integrate quant_agent with other libraries such as TA-Lib out of the box.
|
|
20
|
+
source (str): data source. Default is 'DNSE' (EntradeX); other option is 'VCI' (VietCap Securities) for longterm daily data.
|
|
21
|
+
Returns:
|
|
22
|
+
:obj:`pandas.DataFrame`:
|
|
23
|
+
| time | open | high | low | close | volume |
|
|
24
|
+
| ----------- | ---- | ---- | --- | ----- | ------ |
|
|
25
|
+
| YYYY-mm-dd | xxxx | xxxx | xxx | xxxxx | xxxxxx |
|
|
26
|
+
"""
|
|
27
|
+
if source.upper() == 'DNSE':
|
|
28
|
+
df = ohlc_data(symbol, start_date, end_date, resolution, type, headers=entrade_headers)
|
|
29
|
+
elif source.upper() == 'VCI':
|
|
30
|
+
if resolution != '1D':
|
|
31
|
+
print("VCI only supports longterm daily data. Please set resolution to '1D'")
|
|
32
|
+
return None
|
|
33
|
+
df = longterm_ohlc_data(symbol, start_date, end_date, 'D', type)
|
|
34
|
+
else:
|
|
35
|
+
print(f"Unknown source '{source}'. Use 'DNSE' or 'VCI'.")
|
|
36
|
+
return None
|
|
37
|
+
if df is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
df = df[['time', 'open', 'high', 'low', 'close', 'volume', 'ticker']]
|
|
41
|
+
if beautify and type == 'stock':
|
|
42
|
+
df[['open', 'high', 'low', 'close']] = (df[['open', 'high', 'low', 'close']] * 1000).astype(int)
|
|
43
|
+
if decor:
|
|
44
|
+
df.columns = df.columns.str.title()
|
|
45
|
+
df = df.set_index('Time')
|
|
46
|
+
return df
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_VCI_TIMEFRAME_MAP = {'D': 'ONE_DAY', '1D': 'ONE_DAY', 'H': 'ONE_HOUR', '1H': 'ONE_HOUR', '1': 'ONE_MINUTE'}
|
|
50
|
+
|
|
51
|
+
def longterm_ohlc_data (symbol='REE', start_date='2022-01-01', end_date='2023-10-31', resolution='D', type='stock', headers=None):
|
|
52
|
+
"""
|
|
53
|
+
Get longterm OHLC data.
|
|
54
|
+
Data source: VCI (VietCap Securities) chart endpoint. Returns up
|
|
55
|
+
to 1000 bars per request in a single call.
|
|
56
|
+
Parameters:
|
|
57
|
+
symbol (str): ticker of a stock or index, default is 'REE'.
|
|
58
|
+
start_date (str): start date, 'YYYY-MM-DD'.
|
|
59
|
+
end_date (str): end date, 'YYYY-MM-DD'.
|
|
60
|
+
resolution (str): 'D' (daily, default), 'H' (hourly), or '1' (1-minute).
|
|
61
|
+
type (str): stock, index, or derivative.
|
|
62
|
+
"""
|
|
63
|
+
timeframe = _VCI_TIMEFRAME_MAP.get(resolution, 'ONE_DAY')
|
|
64
|
+
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
|
|
65
|
+
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
|
|
66
|
+
count_back = (end_dt - start_dt).days + 5 # small buffer for non-trading days
|
|
67
|
+
to_timestamp = int(end_dt.timestamp())
|
|
68
|
+
|
|
69
|
+
url = 'https://trading.vietcap.com.vn/api/chart/OHLCChart/gap-chart'
|
|
70
|
+
payload = {'timeFrame': timeframe, 'symbols': [symbol], 'to': to_timestamp, 'countBack': count_back}
|
|
71
|
+
data = fetch_json('POST', url, headers=vci_headers, json=payload, source='VCI')
|
|
72
|
+
if not data:
|
|
73
|
+
print(f'No OHLC data returned by VCI for {symbol}.')
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
bars = data[0]
|
|
77
|
+
df = pd.DataFrame({
|
|
78
|
+
'time': pd.to_datetime([int(t) for t in bars['t']], unit='s').strftime('%Y-%m-%d'),
|
|
79
|
+
'open': bars['o'], 'high': bars['h'], 'low': bars['l'], 'close': bars['c'], 'volume': bars['v'],
|
|
80
|
+
})
|
|
81
|
+
df['ticker'] = symbol
|
|
82
|
+
df = df[(df['time'] >= start_date) & (df['time'] <= end_date)]
|
|
83
|
+
if type == 'stock':
|
|
84
|
+
df[['open', 'high', 'low', 'close']] = round(df[['open', 'high', 'low', 'close']] / 1000, 2)
|
|
85
|
+
df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].astype(float)
|
|
86
|
+
df['volume'] = df['volume'].astype(int)
|
|
87
|
+
return df.reset_index(drop=True)
|
|
88
|
+
|
|
89
|
+
def ohlc_data (symbol, start_date='2023-06-01', end_date='2023-06-17', resolution='1D', type='stock', headers=entrade_headers):
|
|
90
|
+
"""
|
|
91
|
+
Get historical price data from entrade.com.vn. The unit price is VND.
|
|
92
|
+
Parameters:
|
|
93
|
+
symbol (str): ticker of a stock or index. Available indices are: VNINDEX, VN30, HNX, HNX30, UPCOM, VNXALLSHARE, VN30F1M, VN30F2M, VN30F1Q, VN30F2Q
|
|
94
|
+
start_date (str): start date of the historical price data
|
|
95
|
+
end_date (str): end date of the historical price data
|
|
96
|
+
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)
|
|
97
|
+
type (str): stock or index. Default is 'stock'
|
|
98
|
+
headers (dict): headers of the request
|
|
99
|
+
Returns:
|
|
100
|
+
:obj:`pandas.DataFrame`:
|
|
101
|
+
| time | open | high | low | close | volume |
|
|
102
|
+
| ----------- | ---- | ---- | --- | ----- | ------ |
|
|
103
|
+
| YYYY-mm-dd | xxxx | xxxx | xxx | xxxxx | xxxxxx |
|
|
104
|
+
"""
|
|
105
|
+
end_date = (datetime.strptime(end_date, '%Y-%m-%d') + timedelta(days=1)).strftime('%Y-%m-%d')
|
|
106
|
+
start_timestamp = int(datetime.strptime(start_date, '%Y-%m-%d').timestamp())
|
|
107
|
+
end_timestamp = int(datetime.strptime(end_date, '%Y-%m-%d').timestamp())
|
|
108
|
+
|
|
109
|
+
# Resolutions finer than 1 day are limited to the most recent 90 days.
|
|
110
|
+
if resolution != '1D':
|
|
111
|
+
ninety_days_ago = int(datetime.now().timestamp()) - 90 * 24 * 60 * 60
|
|
112
|
+
if end_timestamp < ninety_days_ago:
|
|
113
|
+
print("The 'end_date' value in the report should be no more than 90 days from today for all resolutions shorter than 1 day.\n")
|
|
114
|
+
elif ninety_days_ago > start_timestamp:
|
|
115
|
+
start_timestamp = ninety_days_ago
|
|
116
|
+
print("The retrieval of stock data is restricted to the most recent 90 days from today for all resolutions shorter than 1 day.\n")
|
|
117
|
+
|
|
118
|
+
url = f"https://services.entrade.com.vn/chart-api/v2/ohlcs/{type}?from={start_timestamp}&to={end_timestamp}&symbol={symbol}&resolution={resolution}"
|
|
119
|
+
data = fetch_json('GET', url, headers=headers, source='DNSE')
|
|
120
|
+
if data is None:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
df = pd.DataFrame(data)
|
|
124
|
+
df['t'] = pd.to_datetime(df['t'], unit='s')
|
|
125
|
+
df = df.rename(columns={'t': 'time', 'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'volume'}).drop(columns=['nextTime'])
|
|
126
|
+
df['ticker'] = symbol
|
|
127
|
+
df['time'] = df['time'].dt.tz_localize('UTC').dt.tz_convert('Asia/Ho_Chi_Minh')
|
|
128
|
+
if resolution == '1D':
|
|
129
|
+
df['time'] = df['time'].dt.date
|
|
130
|
+
else:
|
|
131
|
+
df['time'] = df['time'].dt.strftime('%Y-%m-%d %H:%M:%S')
|
|
132
|
+
df[['open', 'high', 'low', 'close']] = df[['open', 'high', 'low', 'close']].astype(float)
|
|
133
|
+
df['volume'] = df['volume'].astype(int)
|
|
134
|
+
return df
|
quant_agent/trading.py
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Trading-side data: live price board, price depth (order book), and intraday
|
|
3
|
+
matched trades.
|
|
4
|
+
|
|
5
|
+
Data sources:
|
|
6
|
+
- Price depth (order book) -> VPS.
|
|
7
|
+
- Price board, intraday trades -> VCI (VietCap Securities).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .config import *
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# =============================================================================
|
|
14
|
+
# TRADING PRICE TABLE
|
|
15
|
+
# =============================================================================
|
|
16
|
+
|
|
17
|
+
def price_depth (stock_list='VPB,TCB', headers=vps_headers):
|
|
18
|
+
"""
|
|
19
|
+
This function returns the trading price board of a target stocks list.
|
|
20
|
+
Args:
|
|
21
|
+
stock_list (:obj:`str`, required): STRING list of symbols separated by "," without any space. Ex: "TCB,SSI,BID"
|
|
22
|
+
"""
|
|
23
|
+
url = f"https://bgapidatafeed.vps.com.vn/getliststockdata/{stock_list}"
|
|
24
|
+
data = fetch_json('GET', url, headers=headers, data={}, source='VPS')
|
|
25
|
+
if data is None:
|
|
26
|
+
return None
|
|
27
|
+
df = json_normalize(data)
|
|
28
|
+
df.rename(columns={'sym': 'Mã CP', 'c': 'Giá Trần', 'f': 'Giá Sàn', 'r': 'Giá tham chiếu', 'lot': 'Tổng Khối Lượng', 'highPrice': 'Giá cao', 'lowPrice': 'Giá thấp', 'avePrice': 'Giá TB', 'lastPrice': 'Giá khớp lệnh', 'lastVolume': 'KL Khớp lệnh', 'ot': '+/- (Khớp lệnh)', 'changePc': '% (Khớp lệnh)', 'fBVol': 'ĐTNN Mua', 'fSVolume': 'ĐTNN Bán', 'fRoom': 'ĐTNN Room'}, inplace=True)
|
|
29
|
+
# columns g1..g6 pack "price|volume" for 3 bid + 3 ask levels
|
|
30
|
+
df['Giá mua 3'] = df['g1'].str.split('|').str[0]
|
|
31
|
+
df['KL mua 3'] = df['g1'].str.split('|').str[1]
|
|
32
|
+
df['Giá mua 2'] = df['g2'].str.split('|').str[0]
|
|
33
|
+
df['KL mua 2'] = df['g2'].str.split('|').str[1]
|
|
34
|
+
df['Giá mua 1'] = df['g3'].str.split('|').str[0]
|
|
35
|
+
df['KL mua 1'] = df['g3'].str.split('|').str[1]
|
|
36
|
+
df['Giá bán 1'] = df['g4'].str.split('|').str[0]
|
|
37
|
+
df['KL bán 1'] = df['g4'].str.split('|').str[1]
|
|
38
|
+
df['Giá bán 2'] = df['g5'].str.split('|').str[0]
|
|
39
|
+
df['KL bán 2'] = df['g5'].str.split('|').str[1]
|
|
40
|
+
df['Giá bán 3'] = df['g6'].str.split('|').str[0]
|
|
41
|
+
df['KL bán 3'] = df['g6'].str.split('|').str[1]
|
|
42
|
+
df.drop(columns=['id', 'mc', 'g1', 'g2', 'g3', 'g4', 'g5', 'g6'], inplace=True)
|
|
43
|
+
df = df[['Mã CP', 'Giá tham chiếu', 'Giá Trần', 'Giá Sàn', 'Giá mua 3', 'KL mua 3', 'Giá mua 2', 'KL mua 2', 'Giá mua 1', 'KL mua 1', 'Giá khớp lệnh', 'KL Khớp lệnh', 'Giá bán 1', 'KL bán 1', 'Giá bán 2', 'KL bán 2', 'Giá bán 3', 'KL bán 3', 'Tổng Khối Lượng', 'ĐTNN Mua', 'ĐTNN Bán', 'ĐTNN Room']]
|
|
44
|
+
# "Giá*" columns are quoted in thousands VND; convert to whole VND
|
|
45
|
+
for col in df.columns:
|
|
46
|
+
if 'Giá' in col:
|
|
47
|
+
try:
|
|
48
|
+
df[col] = (df[col].astype(float) * 1000).astype(int)
|
|
49
|
+
except (ValueError, TypeError):
|
|
50
|
+
pass
|
|
51
|
+
return df
|
|
52
|
+
|
|
53
|
+
def price_board (symbol_ls):
|
|
54
|
+
"""
|
|
55
|
+
This function returns the trading price board of a target stocks list.
|
|
56
|
+
Data source: VCI (VietCap Securities). Live bid/ask ladder + match price.
|
|
57
|
+
Args:
|
|
58
|
+
symbol_ls (:obj:`str`, required): STRING list of symbols separated by "," without any space. Ex: "TCB,SSI,BID"
|
|
59
|
+
"""
|
|
60
|
+
symbols = [s.strip() for s in symbol_ls.split(',') if s.strip()] if isinstance(symbol_ls, str) else list(symbol_ls)
|
|
61
|
+
url = "https://trading.vietcap.com.vn/api/price/symbols/getList"
|
|
62
|
+
data = fetch_json('POST', url, headers=vci_headers, data=json.dumps({"symbols": symbols}), source='VCI')
|
|
63
|
+
if not data:
|
|
64
|
+
print(f'No price board data returned by VCI for {symbols}.')
|
|
65
|
+
return None
|
|
66
|
+
rows = []
|
|
67
|
+
for item in data:
|
|
68
|
+
row = {}
|
|
69
|
+
listing = item.get('listingInfo', {}) or {}
|
|
70
|
+
row['ticker'] = listing.get('ticker')
|
|
71
|
+
row['exchange'] = listing.get('board')
|
|
72
|
+
match = item.get('matchPrice', {}) or {}
|
|
73
|
+
row['matchPrice'] = match.get('matchPrice')
|
|
74
|
+
row['matchVolume'] = match.get('matchVol')
|
|
75
|
+
bid_ask = item.get('bidAsk', {}) or {}
|
|
76
|
+
for i, level in enumerate(bid_ask.get('bidPrices', [])[:3], start=1):
|
|
77
|
+
row[f'bidPrice{i}'] = level.get('price')
|
|
78
|
+
row[f'bidVolume{i}'] = level.get('volume')
|
|
79
|
+
for i, level in enumerate(bid_ask.get('askPrices', [])[:3], start=1):
|
|
80
|
+
row[f'askPrice{i}'] = level.get('price')
|
|
81
|
+
row[f'askVolume{i}'] = level.get('volume')
|
|
82
|
+
rows.append(row)
|
|
83
|
+
return pd.DataFrame(rows)
|
|
84
|
+
|
|
85
|
+
def stock_intraday_data (symbol='ACB', page_size=100, page=0, investor_segment=True, headers=None):
|
|
86
|
+
"""
|
|
87
|
+
Get intraday matched-trade data for a stock.
|
|
88
|
+
Data source: VCI (VietCap Securities).
|
|
89
|
+
Parameters:
|
|
90
|
+
symbol (str): ticker of the stock
|
|
91
|
+
page_size (int): number of matched trades to fetch (VCI returns them most-recent-first). Default is 100.
|
|
92
|
+
page (int): unused.
|
|
93
|
+
investor_segment (bool): unused.
|
|
94
|
+
headers (dict): unused.
|
|
95
|
+
"""
|
|
96
|
+
url = "https://trading.vietcap.com.vn/api/market-watch/LEData/getAll"
|
|
97
|
+
payload = {"symbol": symbol, "limit": page_size, "truncTime": None}
|
|
98
|
+
records = fetch_json('POST', url, headers=vci_headers, data=json.dumps(payload), source='VCI')
|
|
99
|
+
if not records:
|
|
100
|
+
print(f'No intraday data returned by VCI for {symbol}.')
|
|
101
|
+
return None
|
|
102
|
+
df = pd.DataFrame(records)
|
|
103
|
+
df = df.rename(columns={'truncTime': 'time', 'matchPrice': 'price', 'matchVol': 'volume', 'matchType': 'orderType'})
|
|
104
|
+
df['time'] = pd.to_datetime(df['time'].astype(float), unit='s').dt.tz_localize('UTC').dt.tz_convert('Asia/Ho_Chi_Minh')
|
|
105
|
+
df['ticker'] = symbol
|
|
106
|
+
for col in ['price', 'volume']:
|
|
107
|
+
if col in df.columns:
|
|
108
|
+
df[col] = df[col].astype(float)
|
|
109
|
+
keep_cols = [c for c in ['ticker', 'time', 'orderType', 'volume', 'price'] if c in df.columns]
|
|
110
|
+
return df[keep_cols].reset_index(drop=True)
|
quant_agent/utils.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic date helper not tied to any specific data source. For the
|
|
3
|
+
`today`/`last_xd`/`start_xm` helpers used as default `start_date`/`end_date`
|
|
4
|
+
values elsewhere in this package, see `config.py` instead.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime, timedelta
|
|
8
|
+
from dateutil.relativedelta import relativedelta
|
|
9
|
+
|
|
10
|
+
def get_date(n, unit):
|
|
11
|
+
"""
|
|
12
|
+
Return YYYY-mm-dd value from today to n days, months or years in the past
|
|
13
|
+
Parameters:
|
|
14
|
+
n: number of days, months or years
|
|
15
|
+
unit: 'day', 'month' or 'year'
|
|
16
|
+
"""
|
|
17
|
+
if unit == 'day':
|
|
18
|
+
return (datetime.now() - timedelta(days=n)).strftime('%Y-%m-%d')
|
|
19
|
+
elif unit == 'month':
|
|
20
|
+
return (datetime.now() - relativedelta(months=n)).strftime('%Y-%m-%d')
|
|
21
|
+
elif unit == 'year':
|
|
22
|
+
return (datetime.now() - relativedelta(years=n)).strftime('%Y-%m-%d')
|
|
23
|
+
|
|
24
|
+
# def previous_weekday (date):
|
|
25
|
+
# date_value = datetime.strptime(date, '%Y-%m-%d')
|
|
26
|
+
# weekday_name = date_value.strftime('%a')
|
|
27
|
+
# if weekday_name == 'Sun':
|
|
28
|
+
# prev_weekday_date = date_value - timedelta(days=2)
|
|
29
|
+
# elif weekday_name == 'Mon':
|
|
30
|
+
# prev_weekday_date = date_value - timedelta(days=3)
|
|
31
|
+
# else:
|
|
32
|
+
# prev_weekday_date = date_value - timedelta(days=1)
|
|
33
|
+
# return prev_weekday_date
|
|
34
|
+
|
|
35
|
+
# def countBack (start_date, end_date):
|
|
36
|
+
# fd_value = datetime.strptime(start_date, '%Y-%m-%d')
|
|
37
|
+
# td_value = datetime.strptime(end_date, '%Y-%m-%d')
|
|
38
|
+
# diff = td_value - fd_value # Calculate the difference
|
|
39
|
+
# seconds = diff.total_seconds() # Get the total number of seconds
|
|
40
|
+
# # Define the number of seconds in each scenario
|
|
41
|
+
# minute = 60
|
|
42
|
+
# quarter_hour = 15 * minute
|
|
43
|
+
# hour = 60 * minute
|
|
44
|
+
# day = 60 * 6 * minute # trading time from 9AM to 3PM which is 6 hours
|
|
45
|
+
# week = 6 * day
|
|
46
|
+
# month = 23 * day # maximum of 23 working days in a month
|
|
47
|
+
# # Calculate the diff in each scenario
|
|
48
|
+
# diff_minute = round(seconds / minute)
|
|
49
|
+
# diff_quarter_hour = round(seconds / quarter_hour)
|
|
50
|
+
# diff_hour = round(seconds / hour)
|
|
51
|
+
# diff_week = round(seconds / week)
|
|
52
|
+
# diff_month = round(seconds / month)
|
|
53
|
+
# return diff_minute, diff_quarter_hour, diff_hour, diff_week, diff_month
|