quantvn 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.
Potentially problematic release.
This version of quantvn might be problematic. Click here for more details.
- quantvn/__init__.py +2 -0
- quantvn/crypto/__init__.py +1 -0
- quantvn/crypto/data/__init__.py +31 -0
- quantvn/crypto/data/const.py +26 -0
- quantvn/crypto/data/core.py +82 -0
- quantvn/crypto/data/derivatives.py +22 -0
- quantvn/crypto/data/utils.py +93 -0
- quantvn/metrics/__init__.py +3 -0
- quantvn/metrics/portfolio.py +0 -0
- quantvn/metrics/single_asset.py +419 -0
- quantvn/metrics/st.py +569 -0
- quantvn/paper/__init__.py +0 -0
- quantvn/paper/portfolio.py +0 -0
- quantvn/paper/single_asset.py +0 -0
- quantvn/vn/__init__.py +1 -0
- quantvn/vn/data/__init__.py +146 -0
- quantvn/vn/data/const.py +26 -0
- quantvn/vn/data/core.py +904 -0
- quantvn/vn/data/derivatives.py +62 -0
- quantvn/vn/data/stocks.py +1281 -0
- quantvn/vn/data/utils.py +56 -0
- quantvn/vn/metrics/__init__.py +4 -0
- quantvn/vn/metrics/backtest.py +323 -0
- quantvn/vn/metrics/metrics.py +185 -0
- quantvn-0.1.0.dist-info/METADATA +25 -0
- quantvn-0.1.0.dist-info/RECORD +29 -0
- quantvn-0.1.0.dist-info/WHEEL +5 -0
- quantvn-0.1.0.dist-info/licenses/LICENSE +21 -0
- quantvn-0.1.0.dist-info/top_level.txt +1 -0
quantvn/vn/data/utils.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class APIKeyNotSetError(ValueError):
|
|
7
|
+
"""Raised when API key has not been set."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Config:
|
|
11
|
+
"""
|
|
12
|
+
Configuration class for managing the API key and providing the API endpoint.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
_api_key (str | None): The stored API key. Defaults to None.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
_api_key: Optional[str] = None
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def set_api_key(cls, apikey: str):
|
|
22
|
+
"""
|
|
23
|
+
Set the API key without validation.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: Empty key.
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(apikey, str) or not apikey.strip():
|
|
29
|
+
raise ValueError("API key must be a non-empty string.")
|
|
30
|
+
cls._api_key = apikey.strip()
|
|
31
|
+
|
|
32
|
+
@classmethod
|
|
33
|
+
def get_api_key(cls) -> str:
|
|
34
|
+
"""
|
|
35
|
+
Retrieve the currently set API key.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
APIKeyNotSetError: If the API key has not been set.
|
|
39
|
+
"""
|
|
40
|
+
if cls._api_key is None:
|
|
41
|
+
raise APIKeyNotSetError(
|
|
42
|
+
"API key is not set. Use client(apikey=...) to set it."
|
|
43
|
+
)
|
|
44
|
+
return cls._api_key
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def get_link(cls) -> str:
|
|
48
|
+
"""Return the API base URL."""
|
|
49
|
+
return "https://d207hp2u5nyjgn.cloudfront.net"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def client(apikey: str):
|
|
53
|
+
"""
|
|
54
|
+
Convenience function to set the API key.
|
|
55
|
+
"""
|
|
56
|
+
Config.set_api_key(apikey)
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
# ===== Backtest_Stock & helpers (migrated from your “second block”) =====
|
|
2
|
+
import logging
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from typing import TypedDict, List, Dict, Union
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
class Backtest_Derivates:
|
|
10
|
+
"""
|
|
11
|
+
A class for backtesting derivatives trading strategies.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
df : pandas.DataFrame
|
|
16
|
+
Dataframe containing historical data with columns ['Date', 'time', 'Close', 'position'].
|
|
17
|
+
pnl_type : str, optional
|
|
18
|
+
Type of PNL calculation ('raw' or 'after_fees'), by default 'after_fees'.
|
|
19
|
+
|
|
20
|
+
Raises
|
|
21
|
+
------
|
|
22
|
+
ValueError
|
|
23
|
+
If pnl_type is not 'raw' or 'after_fees'.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, df, pnl_type="after_fees"):
|
|
27
|
+
"""
|
|
28
|
+
Initializes the BacktestDerivates class.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
df : pd.DataFrame
|
|
33
|
+
Data containing trade details.
|
|
34
|
+
pnl_type : str, optional
|
|
35
|
+
Type of PNL calculation ('raw' or 'after_fees'), by default "after_fees".
|
|
36
|
+
"""
|
|
37
|
+
if pnl_type not in ["raw", "after_fees"]:
|
|
38
|
+
raise ValueError("Invalid pnl_type. Choose 'raw' or 'after_fees'.")
|
|
39
|
+
|
|
40
|
+
self.df = df.copy()
|
|
41
|
+
self.pnl_type = pnl_type
|
|
42
|
+
self.df["datetime"] = pd.to_datetime(self.df["Date"] + " " + self.df["time"])
|
|
43
|
+
self.df.set_index("datetime", inplace=True)
|
|
44
|
+
self.df.sort_index(inplace=True)
|
|
45
|
+
|
|
46
|
+
# Calculate raw PNL
|
|
47
|
+
self.df["pnl_raw"] = self.df["Close"].diff().shift(-1) * self.df["position"]
|
|
48
|
+
self.df["pnl_raw"].fillna(0, inplace=True)
|
|
49
|
+
|
|
50
|
+
# Calculate PNL after fees
|
|
51
|
+
transaction_fee = 2700 / 100000 # VND per contract
|
|
52
|
+
overnight_fee = 2550 / 100000 # VND per contract per day if held overnight
|
|
53
|
+
|
|
54
|
+
self.df["transaction_fee"] = self.df["position"].diff().abs() * transaction_fee
|
|
55
|
+
|
|
56
|
+
# Identify overnight holdings
|
|
57
|
+
self.df["date"] = self.df.index.date
|
|
58
|
+
self.df["overnight"] = (self.df["position"] > 0) & (
|
|
59
|
+
self.df["date"] != self.df["date"].shift()
|
|
60
|
+
)
|
|
61
|
+
self.df["overnight_fee"] = self.df["overnight"] * overnight_fee
|
|
62
|
+
|
|
63
|
+
self.df["total_fee"] = self.df["transaction_fee"].fillna(0) + self.df[
|
|
64
|
+
"overnight_fee"
|
|
65
|
+
].fillna(0)
|
|
66
|
+
self.df["pnl_after_fees"] = self.df["pnl_raw"] - self.df["total_fee"]
|
|
67
|
+
|
|
68
|
+
def PNL(self):
|
|
69
|
+
"""
|
|
70
|
+
Calculate cumulative PNL based on selected pnl_type.
|
|
71
|
+
|
|
72
|
+
Returns
|
|
73
|
+
-------
|
|
74
|
+
pandas.Series
|
|
75
|
+
Cumulative PNL.
|
|
76
|
+
"""
|
|
77
|
+
return self.df[f"pnl_{self.pnl_type}"].cumsum()
|
|
78
|
+
|
|
79
|
+
def daily_PNL(self):
|
|
80
|
+
"""
|
|
81
|
+
Calculate daily PNL based on selected pnl_type.
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
pandas.Series
|
|
86
|
+
Daily cumulative PNL.
|
|
87
|
+
"""
|
|
88
|
+
daily_pnl = (
|
|
89
|
+
self.df.groupby(self.df.index.date)[f"pnl_{self.pnl_type}"].sum().cumsum()
|
|
90
|
+
)
|
|
91
|
+
return daily_pnl
|
|
92
|
+
|
|
93
|
+
def estimate_minimum_capital(self):
|
|
94
|
+
"""
|
|
95
|
+
Estimate the minimum capital required to run the strategy.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
float
|
|
100
|
+
Minimum capital required.
|
|
101
|
+
"""
|
|
102
|
+
self.df["cumulative_pnl"] = (
|
|
103
|
+
self.df[f"pnl_{self.pnl_type}"].cumsum().shift().fillna(0)
|
|
104
|
+
)
|
|
105
|
+
self.df["capital_required"] = (
|
|
106
|
+
self.df["position"].abs() * self.df["Close"]
|
|
107
|
+
) - self.df["cumulative_pnl"]
|
|
108
|
+
|
|
109
|
+
return max(self.df["capital_required"].max(), 0)
|
|
110
|
+
|
|
111
|
+
def PNL_percentage(self):
|
|
112
|
+
"""
|
|
113
|
+
Calculate PNL percentage relative to minimum required capital.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
float
|
|
118
|
+
PNL percentage.
|
|
119
|
+
"""
|
|
120
|
+
min_capital = self.estimate_minimum_capital()
|
|
121
|
+
if min_capital == 0:
|
|
122
|
+
return np.nan # Avoid division by zero
|
|
123
|
+
return self.daily_PNL() / min_capital
|
|
124
|
+
|
|
125
|
+
def avg_pos(self):
|
|
126
|
+
"""
|
|
127
|
+
Calculate average daily pos enter
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
-------
|
|
131
|
+
float
|
|
132
|
+
Average pos enter per day
|
|
133
|
+
"""
|
|
134
|
+
return abs(self.df['position'].diff().dropna()).sum()/len(self.daily_PNL())
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class Backtest_Stock:
|
|
138
|
+
"""
|
|
139
|
+
Backtest cổ phiếu (long-only):
|
|
140
|
+
- Phí giao dịch 0.1% trên giá trị khớp (mua + bán)
|
|
141
|
+
- Ép giữ tối thiểu 'min_hold_days' phiên (T+2.5 ~ 3 phiên)
|
|
142
|
+
Kỳ vọng input df có cột: ['Date','time','Close','position'].
|
|
143
|
+
'position' = số lượng cổ phiếu mong muốn (âm sẽ bị cắt về 0).
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
def __init__(self, df: pd.DataFrame, pnl_type: str = "after_fees", min_hold_days: int = 3):
|
|
147
|
+
if pnl_type not in ["raw", "after_fees"]:
|
|
148
|
+
raise ValueError("Invalid pnl_type. Choose 'raw' or 'after_fees'.")
|
|
149
|
+
|
|
150
|
+
self.pnl_type = pnl_type
|
|
151
|
+
self.min_hold_days = int(min_hold_days)
|
|
152
|
+
|
|
153
|
+
# Chuẩn hóa thời gian & index
|
|
154
|
+
self.df = df.copy()
|
|
155
|
+
self.df["datetime"] = pd.to_datetime(self.df["Date"].astype(str) + " " + self.df["time"].astype(str), errors="coerce")
|
|
156
|
+
self.df = self.df.dropna(subset=["datetime"])
|
|
157
|
+
self.df.set_index("datetime", inplace=True)
|
|
158
|
+
self.df.sort_index(inplace=True)
|
|
159
|
+
|
|
160
|
+
# Long-only ý định
|
|
161
|
+
self.df["Close"] = pd.to_numeric(self.df["Close"], errors="coerce")
|
|
162
|
+
self.df = self.df.dropna(subset=["Close"])
|
|
163
|
+
self.df["position_intent"] = pd.to_numeric(self.df["position"], errors="coerce").fillna(0).clip(lower=0).astype(float)
|
|
164
|
+
|
|
165
|
+
# Xây effective position tôn trọng min_hold theo SỐ PHIÊN
|
|
166
|
+
eff_pos, trade_qty = self._build_effective_position_with_min_hold(
|
|
167
|
+
index=self.df.index,
|
|
168
|
+
desired_positions=self.df["position_intent"].to_numpy(dtype=float),
|
|
169
|
+
min_hold_days=self.min_hold_days,
|
|
170
|
+
)
|
|
171
|
+
# Trả về numpy → gán theo vị trí, tránh lệch index
|
|
172
|
+
self.df["effective_position"] = eff_pos
|
|
173
|
+
self.df["trade_qty"] = trade_qty
|
|
174
|
+
|
|
175
|
+
# PnL: giữ vị thế từ bar t -> t+1
|
|
176
|
+
self.df["pnl_raw"] = self.df["Close"].diff().shift(-1).fillna(0) * self.df["effective_position"]
|
|
177
|
+
|
|
178
|
+
# Phí giao dịch: 0.1% notional mỗi lần khớp
|
|
179
|
+
fee_rate = 0.001
|
|
180
|
+
notional_traded = np.abs(self.df["trade_qty"].to_numpy()) * self.df["Close"].to_numpy()
|
|
181
|
+
self.df["transaction_fee"] = notional_traded * fee_rate
|
|
182
|
+
|
|
183
|
+
self.df["pnl_after_fees"] = self.df["pnl_raw"] - self.df["transaction_fee"]
|
|
184
|
+
|
|
185
|
+
@staticmethod
|
|
186
|
+
def _build_effective_position_with_min_hold(
|
|
187
|
+
index: pd.DatetimeIndex,
|
|
188
|
+
desired_positions: np.ndarray,
|
|
189
|
+
min_hold_days: int = 3,
|
|
190
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
191
|
+
"""
|
|
192
|
+
Tạo chuỗi vị thế thực sự (long-only) với ràng buộc giữ tối thiểu N phiên.
|
|
193
|
+
Dùng FIFO lots: mỗi lot có (qty_remaining, day_id_entry).
|
|
194
|
+
|
|
195
|
+
- index: DatetimeIndex (để nhóm ngày/phiên)
|
|
196
|
+
- desired_positions: mảng số lượng mong muốn theo bar (>=0)
|
|
197
|
+
- min_hold_days: số phiên tối thiểu cần giữ mới được bán
|
|
198
|
+
"""
|
|
199
|
+
n = len(desired_positions)
|
|
200
|
+
if len(index) != n:
|
|
201
|
+
raise ValueError("index and desired_positions must have the same length")
|
|
202
|
+
|
|
203
|
+
# Ánh xạ mỗi timestamp sang mã phiên tăng dần (0,1,2,...) theo NGÀY (calendar day)
|
|
204
|
+
# Nếu cần đúng 'trading days' theo lịch nghỉ lễ, hãy thay bằng lịch giao dịch VN.
|
|
205
|
+
dates = pd.Index(index.date)
|
|
206
|
+
day_change = np.r_[True, dates[1:] != dates[:-1]]
|
|
207
|
+
day_id = np.cumsum(day_change) - 1 # 0-based day id
|
|
208
|
+
|
|
209
|
+
effective_pos = np.zeros(n, dtype=float)
|
|
210
|
+
trade_qty = np.zeros(n, dtype=float)
|
|
211
|
+
|
|
212
|
+
# FIFO lots: list of [qty_remaining, entry_day_id]
|
|
213
|
+
lots: list[list[float | int]] = []
|
|
214
|
+
prev_effective = 0.0
|
|
215
|
+
|
|
216
|
+
for i in range(n):
|
|
217
|
+
desired = float(max(0.0, desired_positions[i]))
|
|
218
|
+
|
|
219
|
+
if i == 0:
|
|
220
|
+
# Mua vào nếu cần ở bar đầu
|
|
221
|
+
buy_qty = max(0.0, desired - prev_effective)
|
|
222
|
+
if buy_qty > 0:
|
|
223
|
+
lots.append([buy_qty, int(day_id[i])])
|
|
224
|
+
trade_qty[i] = buy_qty
|
|
225
|
+
prev_effective += buy_qty
|
|
226
|
+
# Luôn chốt effective_pos cho bar này
|
|
227
|
+
effective_pos[i] = prev_effective
|
|
228
|
+
continue
|
|
229
|
+
|
|
230
|
+
if desired > prev_effective:
|
|
231
|
+
# Cần mua thêm
|
|
232
|
+
buy_qty = desired - prev_effective
|
|
233
|
+
if buy_qty > 1e-12:
|
|
234
|
+
lots.append([buy_qty, int(day_id[i])])
|
|
235
|
+
trade_qty[i] = buy_qty
|
|
236
|
+
prev_effective += buy_qty
|
|
237
|
+
|
|
238
|
+
elif desired < prev_effective:
|
|
239
|
+
# Cần bán bớt, nhưng chỉ bán các lot đã đủ số phiên
|
|
240
|
+
to_sell = prev_effective - desired
|
|
241
|
+
if to_sell > 1e-12:
|
|
242
|
+
# Bán theo FIFO, chỉ những lot đủ điều kiện
|
|
243
|
+
sell_now_total = 0.0
|
|
244
|
+
for lot in lots:
|
|
245
|
+
if to_sell <= 1e-12:
|
|
246
|
+
break
|
|
247
|
+
qty_rem, d_entry = lot
|
|
248
|
+
if qty_rem <= 1e-12:
|
|
249
|
+
continue
|
|
250
|
+
# Đủ điều kiện nếu số PHIÊN đã qua >= min_hold_days
|
|
251
|
+
if (int(day_id[i]) - int(d_entry)) >= min_hold_days:
|
|
252
|
+
sell_amt = min(qty_rem, to_sell)
|
|
253
|
+
lot[0] = qty_rem - sell_amt
|
|
254
|
+
to_sell -= sell_amt
|
|
255
|
+
sell_now_total += sell_amt
|
|
256
|
+
# Dọn các lot trống
|
|
257
|
+
lots = [lot for lot in lots if lot[0] > 1e-12]
|
|
258
|
+
if sell_now_total > 1e-12:
|
|
259
|
+
trade_qty[i] = -sell_now_total
|
|
260
|
+
prev_effective -= sell_now_total
|
|
261
|
+
|
|
262
|
+
# Luôn set effective_pos ở CUỐI vòng lặp
|
|
263
|
+
effective_pos[i] = prev_effective
|
|
264
|
+
|
|
265
|
+
return effective_pos, trade_qty
|
|
266
|
+
|
|
267
|
+
# ======= Các API kết quả =======
|
|
268
|
+
def PNL(self) -> pd.Series:
|
|
269
|
+
return self.df[f"pnl_{self.pnl_type}"].cumsum()
|
|
270
|
+
|
|
271
|
+
def daily_PNL(self) -> pd.Series:
|
|
272
|
+
ser = self.df.groupby(self.df.index.date)[f"pnl_{self.pnl_type}"].sum()
|
|
273
|
+
return ser.cumsum()
|
|
274
|
+
|
|
275
|
+
def estimate_minimum_capital(self) -> float:
|
|
276
|
+
# Ước lượng nhu cầu vốn tối thiểu thô: notional giữ - lũy kế PnL tại mỗi thời điểm
|
|
277
|
+
cum_pnl = self.df[f"pnl_{self.pnl_type}"].cumsum().shift().fillna(0.0)
|
|
278
|
+
capital_required = (self.df["effective_position"].abs() * self.df["Close"]) - cum_pnl
|
|
279
|
+
return float(max(capital_required.max(), 0.0))
|
|
280
|
+
|
|
281
|
+
def PNL_percentage(self) -> pd.Series:
|
|
282
|
+
min_capital = self.estimate_minimum_capital()
|
|
283
|
+
if min_capital == 0:
|
|
284
|
+
return pd.Series(dtype=float)
|
|
285
|
+
return self.daily_PNL() / min_capital
|
|
286
|
+
|
|
287
|
+
def avg_pos(self) -> float:
|
|
288
|
+
# Trung bình thay đổi vị thế theo ngày (từ effective_position)
|
|
289
|
+
d = self.df["effective_position"].diff().abs().dropna().sum()
|
|
290
|
+
days = max(len(self.daily_PNL()), 1)
|
|
291
|
+
return float(d / days)
|
|
292
|
+
|
|
293
|
+
# ======= Plot giống backtest_derivative (giá + vị thế + equity + volume giao dịch) =======
|
|
294
|
+
def plot_PNL(self, daily: bool = False, title: str = "Backtest Stock"):
|
|
295
|
+
"""
|
|
296
|
+
Vẽ duy nhất 1 đường equity sau phí.
|
|
297
|
+
- daily=False: tích lũy theo từng bar
|
|
298
|
+
- daily=True : gộp theo ngày rồi mới tích lũy
|
|
299
|
+
"""
|
|
300
|
+
if "pnl_after_fees" not in self.df.columns:
|
|
301
|
+
raise ValueError("Chưa có cột 'pnl_after_fees' trong self.df")
|
|
302
|
+
|
|
303
|
+
if daily:
|
|
304
|
+
eq = self.df.groupby(self.df.index.date)["pnl_after_fees"].sum().cumsum()
|
|
305
|
+
x = pd.to_datetime(eq.index)
|
|
306
|
+
y = eq.values
|
|
307
|
+
x_label = "Date"
|
|
308
|
+
else:
|
|
309
|
+
eq = self.df["pnl_after_fees"].cumsum()
|
|
310
|
+
x = eq.index
|
|
311
|
+
y = eq.values
|
|
312
|
+
x_label = "Time"
|
|
313
|
+
|
|
314
|
+
plt.figure(figsize=(10, 4))
|
|
315
|
+
plt.plot(x, y, linewidth=1.4)
|
|
316
|
+
plt.title(title)
|
|
317
|
+
plt.xlabel(x_label)
|
|
318
|
+
plt.ylabel("Cumulative PnL (after fees)")
|
|
319
|
+
plt.grid(True, alpha=0.3)
|
|
320
|
+
plt.tight_layout()
|
|
321
|
+
plt.show()
|
|
322
|
+
|
|
323
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Metrics:
|
|
5
|
+
"""
|
|
6
|
+
A class for calculating performance metrics from backtest results.
|
|
7
|
+
|
|
8
|
+
Parameters
|
|
9
|
+
----------
|
|
10
|
+
backtest : BacktestDerivatives
|
|
11
|
+
An instance of BacktestDerivatives containing PNL data.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __init__(self, backtest):
|
|
15
|
+
"""
|
|
16
|
+
Initializes the Metrics class.
|
|
17
|
+
|
|
18
|
+
Parameters
|
|
19
|
+
----------
|
|
20
|
+
backtest : BacktestDerivates
|
|
21
|
+
Instance of backtest results.
|
|
22
|
+
"""
|
|
23
|
+
self.backtest = backtest
|
|
24
|
+
self.daily_pnl = backtest.daily_PNL().diff().dropna()
|
|
25
|
+
|
|
26
|
+
def avg_loss(self):
|
|
27
|
+
"""
|
|
28
|
+
Compute the average loss from daily PNL.
|
|
29
|
+
|
|
30
|
+
Returns
|
|
31
|
+
-------
|
|
32
|
+
float
|
|
33
|
+
Average loss.
|
|
34
|
+
"""
|
|
35
|
+
losses = self.daily_pnl[self.daily_pnl < 0]
|
|
36
|
+
return losses.mean()
|
|
37
|
+
|
|
38
|
+
def avg_return(self):
|
|
39
|
+
"""
|
|
40
|
+
Compute the average return from daily PNL.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
float
|
|
45
|
+
Average return.
|
|
46
|
+
"""
|
|
47
|
+
return self.daily_pnl.mean()
|
|
48
|
+
|
|
49
|
+
def avg_win(self):
|
|
50
|
+
"""
|
|
51
|
+
Compute the average win from daily PNL.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
float
|
|
56
|
+
Average win.
|
|
57
|
+
"""
|
|
58
|
+
wins = self.daily_pnl[self.daily_pnl > 0]
|
|
59
|
+
return wins.mean()
|
|
60
|
+
|
|
61
|
+
def max_drawdown(self):
|
|
62
|
+
"""
|
|
63
|
+
Compute the maximum drawdown.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
float
|
|
68
|
+
Maximum drawdown as a percentage of minimum capital.
|
|
69
|
+
"""
|
|
70
|
+
cumulative = self.daily_pnl.cumsum()
|
|
71
|
+
peak = cumulative.cummax()
|
|
72
|
+
drawdown = cumulative - peak
|
|
73
|
+
return drawdown.min() / self.backtest.estimate_minimum_capital()
|
|
74
|
+
|
|
75
|
+
def win_rate(self):
|
|
76
|
+
"""
|
|
77
|
+
Compute the win rate.
|
|
78
|
+
|
|
79
|
+
Returns
|
|
80
|
+
-------
|
|
81
|
+
float
|
|
82
|
+
Win rate.
|
|
83
|
+
"""
|
|
84
|
+
wins = (self.daily_pnl > 0).sum()
|
|
85
|
+
total = len(self.daily_pnl)
|
|
86
|
+
return wins / total if total > 0 else 0
|
|
87
|
+
|
|
88
|
+
def volatility(self):
|
|
89
|
+
"""
|
|
90
|
+
Compute the standard deviation of daily PNL.
|
|
91
|
+
|
|
92
|
+
Returns
|
|
93
|
+
-------
|
|
94
|
+
float
|
|
95
|
+
Volatility.
|
|
96
|
+
"""
|
|
97
|
+
return self.daily_pnl.std()
|
|
98
|
+
|
|
99
|
+
def sharpe(self, risk_free_rate=0.0):
|
|
100
|
+
"""
|
|
101
|
+
Compute the Sharpe ratio.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
float
|
|
106
|
+
Sharpe ratio.
|
|
107
|
+
"""
|
|
108
|
+
return (self.avg_return() - risk_free_rate) / self.volatility() * np.sqrt(252)
|
|
109
|
+
|
|
110
|
+
def sortino(self):
|
|
111
|
+
"""
|
|
112
|
+
Compute the Sortino ratio.
|
|
113
|
+
|
|
114
|
+
Returns
|
|
115
|
+
-------
|
|
116
|
+
float
|
|
117
|
+
Sortino ratio.
|
|
118
|
+
"""
|
|
119
|
+
downside_std = self.daily_pnl[self.daily_pnl < 0].std()
|
|
120
|
+
return (
|
|
121
|
+
np.sqrt(252) * self.avg_return() / downside_std
|
|
122
|
+
if downside_std > 0
|
|
123
|
+
else np.nan
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
def calmar(self):
|
|
127
|
+
"""
|
|
128
|
+
Compute the Calmar ratio.
|
|
129
|
+
|
|
130
|
+
Returns
|
|
131
|
+
-------
|
|
132
|
+
float
|
|
133
|
+
Calmar ratio.
|
|
134
|
+
"""
|
|
135
|
+
return (
|
|
136
|
+
np.sqrt(252) * self.avg_return() / abs(self.max_drawdown())
|
|
137
|
+
if self.max_drawdown() != 0
|
|
138
|
+
else np.nan
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def profit_factor(self):
|
|
142
|
+
"""
|
|
143
|
+
Compute the profit factor.
|
|
144
|
+
|
|
145
|
+
Returns
|
|
146
|
+
-------
|
|
147
|
+
float
|
|
148
|
+
Profit factor.
|
|
149
|
+
"""
|
|
150
|
+
total_gain = self.daily_pnl[self.daily_pnl > 0].sum()
|
|
151
|
+
total_loss = abs(self.daily_pnl[self.daily_pnl < 0].sum())
|
|
152
|
+
return total_gain / total_loss if total_loss != 0 else np.nan
|
|
153
|
+
|
|
154
|
+
def risk_of_ruin(self):
|
|
155
|
+
"""
|
|
156
|
+
Compute risk of ruin.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
float
|
|
161
|
+
Risk of ruin.
|
|
162
|
+
"""
|
|
163
|
+
win_rate = self.win_rate()
|
|
164
|
+
loss_rate = 1 - win_rate
|
|
165
|
+
return (
|
|
166
|
+
(loss_rate / win_rate) ** (1 / self.avg_loss())
|
|
167
|
+
if self.avg_loss() != 0
|
|
168
|
+
else np.nan
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def value_at_risk(self, confidence_level=0.05):
|
|
172
|
+
"""
|
|
173
|
+
Compute Value at Risk (VaR).
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
confidence_level : float, optional
|
|
178
|
+
Confidence level for VaR, by default 0.05.
|
|
179
|
+
|
|
180
|
+
Returns
|
|
181
|
+
-------
|
|
182
|
+
float
|
|
183
|
+
Value at Risk (VaR).
|
|
184
|
+
"""
|
|
185
|
+
return self.daily_pnl.quantile(confidence_level)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantvn
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: QuantVN API Library for Financial Data Analysis
|
|
5
|
+
Author: quantvn
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: pandas
|
|
18
|
+
Requires-Dist: matplotlib
|
|
19
|
+
Dynamic: author
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
Dynamic: requires-dist
|
|
24
|
+
Dynamic: requires-python
|
|
25
|
+
Dynamic: summary
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
quantvn/__init__.py,sha256=9YPnywBFZuYcjgMWWMsnRavq1PnxmDylns5KMWjxuBo,56
|
|
2
|
+
quantvn/crypto/__init__.py,sha256=90HTV3rYM6sJ6l6VXXFdffOT0ZTHXnE-UeXJzX1KBGk,32
|
|
3
|
+
quantvn/crypto/data/__init__.py,sha256=7i4ylyHDdyt9XDO9SXre67_nuUCUhQxbHqBq2Cniz8g,685
|
|
4
|
+
quantvn/crypto/data/const.py,sha256=qLTAycWZHRwtj-mSOT2RB9YFbGFPiFUFnNtuknbbUGE,1143
|
|
5
|
+
quantvn/crypto/data/core.py,sha256=ajcV3xRG3CrkfB71-QZjwqsJ5HfMrLwVXciDxO0onYM,2465
|
|
6
|
+
quantvn/crypto/data/derivatives.py,sha256=HsaOScXpRx91w8hDdSueTNyLt0VcdCeSSPgnALMYHgY,806
|
|
7
|
+
quantvn/crypto/data/utils.py,sha256=x2qAmr_00yZ4LpEqN1WJzKvuhfd-NFZCY9aWS8Z5Ieo,3911
|
|
8
|
+
quantvn/metrics/__init__.py,sha256=RkItghoyBNiZ9m7ZUqydlGEnr0z8G80se4yQZnyxX_4,151
|
|
9
|
+
quantvn/metrics/portfolio.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
+
quantvn/metrics/single_asset.py,sha256=tVrg3Hw8H-lkpUlklvn9LLcE_RrsCOf9csEiGS_nZx8,12265
|
|
11
|
+
quantvn/metrics/st.py,sha256=VTAZXrrnhwdSpwPlufvYWjizLMsnFaGrPQe7GZayekI,22004
|
|
12
|
+
quantvn/paper/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
13
|
+
quantvn/paper/portfolio.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
quantvn/paper/single_asset.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
+
quantvn/vn/__init__.py,sha256=TUU3zltaExQdwUukUPxwGX5XOLQ1kTssdilZkTAiYkM,37
|
|
16
|
+
quantvn/vn/data/__init__.py,sha256=4l2RwFBOjSzn-H1tUNFTWd8QMcxBv9s6BuoA9N3uRvQ,3467
|
|
17
|
+
quantvn/vn/data/const.py,sha256=4owcC4Ik8Fw8kBhXrLEd9Ux9_MPriSh52OOZQWI795E,1144
|
|
18
|
+
quantvn/vn/data/core.py,sha256=zzuBLBBOuplD-cBDIFEICLbRPXy1_sLHhL4vCTS6M78,35631
|
|
19
|
+
quantvn/vn/data/derivatives.py,sha256=h_PeYj8r7pPgS4Z1AJmtZgXiQWiadQL15yKHE0wXx3Q,1698
|
|
20
|
+
quantvn/vn/data/stocks.py,sha256=cTOERdmCEaLx-mG2tXGxV9b1Hmm7u6EPnVV4d36BiCQ,41772
|
|
21
|
+
quantvn/vn/data/utils.py,sha256=mI0gN_zi21WLDXTGkZCv6V57HwS-LDGadMXBHvR_CLo,1376
|
|
22
|
+
quantvn/vn/metrics/__init__.py,sha256=QGxaESHiAnI24yWPjLCYti1O1twU5pU31yKA5L9QPgk,185
|
|
23
|
+
quantvn/vn/metrics/backtest.py,sha256=w9nG1opPcs8J6g_zfbKrkrd3xrCXeqlW0P7_-eOR_FE,12257
|
|
24
|
+
quantvn/vn/metrics/metrics.py,sha256=9NTiucx7PNI2PhGSJ8Jju_R0LOA7fl9i0776OoaM6rg,4216
|
|
25
|
+
quantvn-0.1.0.dist-info/licenses/LICENSE,sha256=JHlulH_fR5VN3pNTj_4p4w-I-HjxDGNjX2i6qAzNEJw,1064
|
|
26
|
+
quantvn-0.1.0.dist-info/METADATA,sha256=L2f56qCncqD0dZAq3fgYe9jRGGE3k0iVlL_Tm7k_LVg,780
|
|
27
|
+
quantvn-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
28
|
+
quantvn-0.1.0.dist-info/top_level.txt,sha256=04ce9JXFm3R7Lpv3jcyrEa8UYAgWpJNDvn2SJzfhkMs,8
|
|
29
|
+
quantvn-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 XNO API
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
quantvn
|