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/metrics/st.py ADDED
@@ -0,0 +1,569 @@
1
+ import logging
2
+ from abc import abstractmethod
3
+ from typing import TypedDict, List, Dict, Union
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import matplotlib.pyplot as plt
8
+
9
+
10
+ class HistoryRecord(TypedDict):
11
+ time: pd.Timestamp
12
+ current_tick: int
13
+ signal: float
14
+ action: str
15
+ amount: float
16
+ value: float
17
+ price: float
18
+ fee: float
19
+ equity: float
20
+ bm_equity: float
21
+ step_ret: float
22
+ cum_ret: float
23
+ bm_step_ret: float
24
+ bm_cum_ret: float
25
+
26
+
27
+ def round_to_lot(value: float, lot_size: int) -> int:
28
+ remainder = value % lot_size
29
+ if remainder < lot_size / 2:
30
+ return int(value - remainder)
31
+ else:
32
+ return int(value + (lot_size - remainder))
33
+
34
+
35
+ class Algorithm:
36
+ _price_scale = 1
37
+
38
+ def __init__(self):
39
+ self._name = None
40
+ self._init_cash = 1_000_000_000
41
+ self._slippage = 0.0
42
+ self._resolution = "D"
43
+ self._ticker = None
44
+ self._from_time = None
45
+ self._to_time = None
46
+ self._df_ticker = pd.DataFrame()
47
+ self._init_price: float | None = None
48
+
49
+ self._bm_open_size: int | None = None
50
+ self._bm_equity: float | None = None
51
+
52
+ self._current_time_idx: int | None = None
53
+ self._current_time: pd.Timestamp | None = None
54
+ self._current_position: float | None = None
55
+ self._current_open_size: int | None = None
56
+ self._current_equity: float | None = None
57
+
58
+ self._ht_times: List[pd.Timestamp] = []
59
+ self._ht_prices: List[float] = []
60
+ self._bt_results: List[HistoryRecord] = []
61
+ self._bt_df: Union[pd.DataFrame, None] = None
62
+ self._bt_columns = list(HistoryRecord.__annotations__.keys())
63
+ self.performance: Dict[str, float] = {}
64
+ self._signals: pd.Series | None = None
65
+
66
+ # -------------------- Data helpers --------------------
67
+ @property
68
+ def df_ticker(self) -> pd.DataFrame:
69
+ return self._df_ticker
70
+
71
+ @df_ticker.setter
72
+ def df_ticker(self, df: pd.DataFrame):
73
+ self._df_ticker = df
74
+
75
+ def load_csv(self, csv_path: str, symbol: str = "TICKER"):
76
+ df = pd.read_csv(csv_path, parse_dates=["Date"]).rename(columns={"Date": "time"})
77
+ df = df.set_index("time").sort_index()
78
+ required = {"Open", "High", "Low", "Close"}
79
+ missing = required - set(df.columns)
80
+ if missing:
81
+ raise ValueError(f"CSV is missing columns: {sorted(missing)}")
82
+ if "Volume" not in df.columns:
83
+ df["Volume"] = 0
84
+ df["Symbol"] = symbol
85
+ self.df_ticker = df
86
+
87
+ # -------------------- Run lifecycle --------------------
88
+ def __reset__(self):
89
+ if self.df_ticker.empty:
90
+ raise ValueError("No data loaded. Set df_ticker or call load_csv().")
91
+ self._init_price = float(self.df_ticker['Close'].values[0] * self._price_scale)
92
+
93
+ self._current_time_idx = 0
94
+ self._current_position = 0.0
95
+ self._current_open_size = 0
96
+ self._ht_times.clear()
97
+ self._ht_prices.clear()
98
+ self._bt_results.clear()
99
+ self._signals = pd.Series(dtype=float)
100
+ self._ht_times = self.df_ticker.index.tolist()
101
+ self._ht_prices = (self.df_ticker['Close'].values * self._price_scale).tolist()
102
+
103
+ self._current_time = self.df_ticker.index[0]
104
+ self._bm_equity = self._init_cash
105
+ self._current_equity = self._init_cash
106
+ self._bt_df = pd.DataFrame(columns=self._bt_columns)
107
+ self._signals = pd.Series(0.0, index=self.df_ticker.index, dtype=float)
108
+
109
+ @abstractmethod
110
+ def __step__(self, time_idx: int):
111
+ self._current_time_idx = time_idx
112
+
113
+ @abstractmethod
114
+ def __setup__(self):
115
+ raise NotImplementedError
116
+
117
+ @abstractmethod
118
+ def __algorithm__(self):
119
+ raise NotImplementedError
120
+
121
+ def hold(self, conditions, weight: float = 0.0):
122
+ self._signals[conditions] = weight
123
+
124
+ def buy(self, conditions, weight: float = 1.0):
125
+ self._signals[conditions] = weight
126
+
127
+ def sell(self, conditions, weight: float = 1.0):
128
+ self._signals[conditions] = -weight
129
+
130
+ @property
131
+ def Open(self):
132
+ return self.df_ticker['Open'].values
133
+
134
+ @property
135
+ def High(self):
136
+ return self.df_ticker['High'].values
137
+
138
+ @property
139
+ def Low(self):
140
+ return self.df_ticker['Low'].values
141
+
142
+ @property
143
+ def Close(self):
144
+ return self.df_ticker['Close'].values
145
+
146
+ @property
147
+ def Volume(self):
148
+ return self.df_ticker['Volume'].values
149
+
150
+ def run(self):
151
+ self.__setup__()
152
+ self.__load_data__()
153
+ self.__reset__()
154
+ # attach features wrapper on the full DataFrame
155
+ self._features = TimeseriesFeatures(self.df_ticker)
156
+ self.__algorithm__()
157
+ if self._signals is None:
158
+ raise ValueError("Trading signals were not generated.")
159
+ for time_idx in range(len(self._signals)):
160
+ self.__step__(time_idx)
161
+ self.__done__()
162
+ return self
163
+
164
+ def __done__(self):
165
+ logging.info(f"Algorithm {self._name} run completed. Total records: {len(self._bt_results)}")
166
+ self._bt_df = pd.DataFrame(self._bt_results, columns=self._bt_columns)
167
+ equity = self._bt_df['equity'].values
168
+ bm_equity = self._bt_df['bm_equity'].values
169
+ step_ret = np.zeros_like(equity, dtype=np.float64)
170
+ step_ret[1:] = (equity[1:] - equity[:-1]) / equity[:-1]
171
+ cum_ret = np.cumprod(1 + step_ret) - 1
172
+ bm_step_ret = np.zeros_like(bm_equity, dtype=np.float64)
173
+ bm_step_ret[1:] = (bm_equity[1:] - bm_equity[:-1]) / bm_equity[:-1]
174
+ bm_cum_ret = np.cumprod(1 + bm_step_ret) - 1
175
+ self._bt_df['step_ret'] = step_ret
176
+ self._bt_df['cum_ret'] = cum_ret
177
+ self._bt_df['bm_step_ret'] = bm_step_ret
178
+ self._bt_df['bm_cum_ret'] = bm_cum_ret
179
+ self._bt_df.set_index('time', inplace=True)
180
+ return self
181
+
182
+ # -------------------- Indicators helpers --------------------
183
+ @classmethod
184
+ def current(cls, series):
185
+ if isinstance(series, pd.Series):
186
+ return series.values
187
+ elif isinstance(series, np.ndarray):
188
+ return series
189
+ else:
190
+ raise TypeError(f"Unsupported type {type(series)} for current()")
191
+
192
+ @classmethod
193
+ def previous(cls, series, periods: int = 1):
194
+ if isinstance(series, pd.Series):
195
+ return series.shift(periods).values
196
+ elif isinstance(series, np.ndarray):
197
+ arr = series
198
+ if periods <= 0:
199
+ return arr
200
+ pad = np.full(periods, np.nan)
201
+ return np.concatenate([pad, arr[:-periods]])
202
+ else:
203
+ raise TypeError(f"Unsupported type {type(series)} for previous()")
204
+
205
+ # -------------------- Data loading --------------------
206
+ def __load_data__(self):
207
+ if not self.df_ticker.empty:
208
+ return
209
+ if self._ticker is None:
210
+ raise ValueError("No data loaded. Either call load_csv() or set _ticker and install yfinance.")
211
+ try:
212
+ import yfinance as yf
213
+ except Exception as e:
214
+ raise RuntimeError("yfinance is required for automatic fetching. Install via pip install yfinance or load via CSV.") from e
215
+ start = str(self._from_time) if self._from_time is not None else None
216
+ end = str(self._to_time) if self._to_time is not None else None
217
+ df = yf.download(self._ticker, start=start, end=end, interval="1d")
218
+ if df.empty:
219
+ raise ValueError(f"No data fetched for ticker {self._ticker}")
220
+ df = df.rename(columns={"Adj Close": "AdjClose"})
221
+ if "Volume" not in df.columns:
222
+ df["Volume"] = 0
223
+ self.df_ticker = df[["Open", "High", "Low", "Close", "Volume"]]
224
+
225
+ def visualize(self):
226
+ visualizer = StrategyVisualizer(self._bt_df)
227
+ visualizer.name = self._name
228
+ visualizer.visualize()
229
+
230
+
231
+ class StockAlgorithm(Algorithm):
232
+ _stock_lot_size = 100
233
+ _price_scale = 1000
234
+
235
+ def __init__(self):
236
+ super().__init__()
237
+ self._init_fee = 0.001
238
+ self._t0_size: float = 0.0
239
+ self._t1_size: float = 0.0
240
+ self._t2_size: float = 0.0
241
+ self._sell_size: float = 0.0
242
+ self._pending_sell_pos: float = 0.0
243
+
244
+ @abstractmethod
245
+ def __setup__(self):
246
+ raise NotImplementedError("StockAlgorithm must implement _setup_ method")
247
+
248
+ @abstractmethod
249
+ def __algorithm__(self):
250
+ raise NotImplementedError("StockAlgorithm must implement _algorithm_ method")
251
+
252
+ def __reset__(self):
253
+ super().__reset__()
254
+ self._bm_open_size = round_to_lot(self._init_cash // self._init_price, self._stock_lot_size)
255
+ bm_fee = self._init_price * self._bm_open_size * self._init_fee
256
+ self._bm_equity -= bm_fee
257
+
258
+ def __step__(self, time_idx: int):
259
+ super().__step__(time_idx)
260
+ current_action = "H"
261
+ current_signal = 0.0
262
+ current_trade_size = 0.0
263
+ current_fee = 0.0
264
+ current_price = self._ht_prices[self._current_time_idx]
265
+ current_time = self._ht_times[self._current_time_idx]
266
+ sig: float = float(self._signals.values[self._current_time_idx])
267
+ current_max_shares = round_to_lot(self._init_cash // current_price, self._stock_lot_size)
268
+
269
+ prev_price = self._ht_prices[self._current_time_idx - 1] if self._current_time_idx > 0 else current_price
270
+ current_pnl = self._current_open_size * (current_price - prev_price)
271
+ bm_pnl = self._bm_open_size * (current_price - prev_price)
272
+
273
+ prev_time = self._ht_times[self._current_time_idx - 1] if self._current_time_idx > 0 else current_time
274
+ day_diff = (current_time - prev_time).days
275
+ if day_diff > 0:
276
+ logging.debug(
277
+ f"Update T0, T1, T2 for {current_time}, T0: {self._t0_size}, T1: {self._t1_size}, T2: {self._t2_size}, Sell Position: {self._sell_size}"
278
+ )
279
+ self._sell_size += self._t2_size
280
+ self._t2_size = self._t1_size
281
+ self._t1_size = self._t0_size
282
+ self._t0_size = 0
283
+
284
+ if sig > 0:
285
+ updated_position = min(sig - self._current_position, 1 - self._current_position)
286
+ elif sig < 0:
287
+ if self._current_position > 0:
288
+ updated_position = max(sig - self._current_position, -self._current_position)
289
+ else:
290
+ updated_position = 0.0
291
+ else:
292
+ updated_position = 0.0
293
+
294
+ if updated_position == 0:
295
+ pass
296
+ elif updated_position < 0 or self._pending_sell_pos > 0:
297
+ logging.debug(f"Entering sell logic at {current_time} with weight {sig}")
298
+ if self._sell_size == 0:
299
+ logging.warning(
300
+ f"Sell position is 0, but trying to sell {sig} at {current_time}. This will be ignored, please wait for the next timestamp to sell."
301
+ )
302
+ self._pending_sell_pos += abs(sig)
303
+ else:
304
+ can_sell_position = max(self._pending_sell_pos, abs(updated_position))
305
+ current_trade_size = min(
306
+ self._sell_size,
307
+ round_to_lot(can_sell_position * self._current_open_size, self._stock_lot_size),
308
+ )
309
+ self._sell_size -= current_trade_size
310
+ self._current_open_size -= current_trade_size
311
+ current_signal = -can_sell_position
312
+ self._current_position -= can_sell_position
313
+ self._pending_sell_pos = max(self._pending_sell_pos - can_sell_position, 0)
314
+ current_action = "S"
315
+ current_fee = current_price * current_trade_size * self._init_fee
316
+ current_pnl -= current_fee
317
+ elif updated_position > 0:
318
+ logging.debug(f"Entering buy logic at {current_time} with weight {sig}")
319
+ self._current_position += updated_position
320
+ current_trade_size = round_to_lot(updated_position * current_max_shares, self._stock_lot_size)
321
+ self._t0_size += current_trade_size
322
+ self._current_open_size += current_trade_size
323
+ current_action = "B"
324
+ current_signal = updated_position
325
+ current_fee = current_price * current_trade_size * self._init_fee
326
+ current_pnl -= current_fee
327
+
328
+ self._current_equity += current_pnl
329
+ self._bm_equity += bm_pnl
330
+ self._bt_results.append(
331
+ HistoryRecord(
332
+ time=current_time,
333
+ current_tick=self._current_time_idx,
334
+ action=current_action,
335
+ signal=current_signal,
336
+ amount=current_trade_size,
337
+ price=current_price,
338
+ value=self._current_open_size * current_price,
339
+ fee=current_fee,
340
+ equity=self._current_equity,
341
+ bm_equity=self._bm_equity,
342
+ step_ret=0,
343
+ cum_ret=0,
344
+ bm_step_ret=0,
345
+ bm_cum_ret=0,
346
+ )
347
+ )
348
+
349
+
350
+ class TimeseriesFeatures:
351
+ def __init__(self, df: pd.DataFrame):
352
+ self.df = df
353
+
354
+ def bbands(self, timeperiod: int = 20, nbdevup: float = 2.0, nbdevdn: float = 2.0):
355
+ close = self.df['Close']
356
+ ma = close.rolling(timeperiod).mean()
357
+ std = close.rolling(timeperiod).std(ddof=0)
358
+ upper = ma + nbdevup * std
359
+ lower = ma - nbdevdn * std
360
+ return upper, ma, lower
361
+
362
+ def rsi(self, timeperiod: int = 14):
363
+ close = self.df['Close']
364
+ delta = close.diff()
365
+ gain = (delta.where(delta > 0, 0.0)).ewm(alpha=1/timeperiod, adjust=False).mean()
366
+ loss = (-delta.where(delta < 0, 0.0)).ewm(alpha=1/timeperiod, adjust=False).mean()
367
+ rs = gain / loss.replace(0, np.nan)
368
+ rsi = 100 - (100 / (1 + rs))
369
+ return rsi
370
+
371
+ def macd(self, fastperiod: int = 12, slowperiod: int = 26, signalperiod: int = 9):
372
+ close = self.df['Close']
373
+ ema_fast = close.ewm(span=fastperiod, adjust=False).mean()
374
+ ema_slow = close.ewm(span=slowperiod, adjust=False).mean()
375
+ macd = ema_fast - ema_slow
376
+ signal = macd.ewm(span=signalperiod, adjust=False).mean()
377
+ hist = macd - signal
378
+ return macd, signal, hist
379
+
380
+
381
+ class StrategyPerformance:
382
+ def __init__(self, returns: pd.Series | np.ndarray):
383
+ import quantstats as qs
384
+ if isinstance(returns, np.ndarray):
385
+ returns = pd.Series(returns)
386
+ if not isinstance(returns, pd.Series):
387
+ raise TypeError("Returns must be a pandas Series or numpy array.")
388
+ self.qs = qs
389
+ self.returns = returns.replace([np.inf, -np.inf], np.nan).dropna()
390
+ self.trading_days = 252
391
+
392
+ @property
393
+ def summary(self):
394
+ qs = self.qs
395
+ r = self.returns
396
+ return {
397
+ "avg_return": qs.stats.avg_return(r),
398
+ "cumulative_return": qs.stats.comp(r),
399
+ "cvar": qs.stats.cvar(r),
400
+ "gain_to_pain_ratio": qs.stats.gain_to_pain_ratio(r),
401
+ "kelly_criterion": qs.stats.kelly_criterion(r),
402
+ "max_drawdown": qs.stats.max_drawdown(r),
403
+ "omega": qs.stats.omega(r),
404
+ "profit_factor": qs.stats.profit_factor(r),
405
+ "recovery_factor": qs.stats.recovery_factor(r),
406
+ "sharpe": qs.stats.sharpe(r),
407
+ "sortino": qs.stats.sortino(r),
408
+ "tail_ratio": qs.stats.tail_ratio(r),
409
+ "ulcer_index": qs.stats.ulcer_index(r),
410
+ "var": qs.stats.value_at_risk(r),
411
+ "volatility": qs.stats.volatility(r),
412
+ "win_loss_ratio": qs.stats.win_loss_ratio(r),
413
+ "win_rate": qs.stats.win_rate(r),
414
+ }
415
+
416
+
417
+ class StrategyVisualizer:
418
+ def __init__(self, df: pd.DataFrame):
419
+ self._bt_df = df
420
+ self.name = None
421
+
422
+ def performance_summary(self) -> Dict[str, float]:
423
+ if not self._bt_df.empty:
424
+ pf = StrategyPerformance(self._bt_df['step_ret'])
425
+ return pf.summary
426
+ else:
427
+ logging.warning("No backtest data available for performance summary.")
428
+ return {}
429
+
430
+ def visualize(self):
431
+ if self._bt_df is None or self._bt_df.empty:
432
+ logging.warning("No backtest data available to visualize.")
433
+ return
434
+
435
+ df = self._bt_df.sort_index()
436
+ performance = self.performance_summary()
437
+
438
+ metric_names = list(sorted(performance.keys()))
439
+ metric_values = [f"{performance[m]:.4f}" if isinstance(performance[m], float) else "-" for m in metric_names]
440
+
441
+ import plotly.graph_objects as go
442
+ from plotly.subplots import make_subplots
443
+
444
+ fig = make_subplots(
445
+ rows=2, cols=2,
446
+ shared_xaxes=True,
447
+ vertical_spacing=0.01,
448
+ horizontal_spacing=0.01,
449
+ column_widths=[0.8, 0.2],
450
+ row_heights=[0.4, 0.6],
451
+ subplot_titles=("Strategy vs Benchmark", "", "Price vs Signals", ""),
452
+ specs=[[{"type": "xy"}, {"type": "table"}],
453
+ [{"type": "xy"}, None]]
454
+ )
455
+
456
+ # Row 1: Strategy vs Benchmark
457
+ fig.add_trace(go.Scatter(
458
+ x=df.index, y=df['cum_ret'], mode='lines', name='Strategy',
459
+ line=dict(color='blue')
460
+ ), row=1, col=1)
461
+
462
+ fig.add_trace(go.Scatter(
463
+ x=df.index, y=df['bm_cum_ret'], mode='lines', name='Benchmark',
464
+ line=dict(color='gray', dash='dot')
465
+ ), row=1, col=1)
466
+
467
+ # Row 2: Price with Buy/Sell signals
468
+ fig.add_trace(go.Scatter(
469
+ x=df.index, y=df['price'], mode='lines', name='Price',
470
+ line=dict(color='black')
471
+ ), row=2, col=1)
472
+
473
+ # Buy markers
474
+ buy_df = df[df['action'] == 'B'].copy()
475
+ buy_df['date_str'] = buy_df.index.strftime('%Y-%m-%d')
476
+ buy_df['time_str'] = buy_df.index.strftime('%H:%M:%S')
477
+ buy_df['balance_str'] = buy_df['equity'].apply(lambda x: f"{x:,.2f}")
478
+ buy_df['amount_str'] = buy_df['amount'].apply(lambda x: f"{x:.2f}")
479
+ buy_df['fee_str'] = buy_df['fee'].apply(lambda x: f"{x:.2f}")
480
+ buy_df['price_str'] = buy_df['price'].apply(lambda x: f"{x:.2f}")
481
+ fig.add_trace(go.Scatter(
482
+ x=buy_df.index, y=buy_df['price'], mode='markers', name='Buy',
483
+ marker=dict(symbol='triangle-up', color='green', size=10),
484
+ hovertemplate="Buy [%{customdata[0]}]<br>"
485
+ "Time: %{customdata[1]}<br>"
486
+ "Price: %{customdata[2]}<br>"
487
+ "Amount: %{customdata[3]}<br>"
488
+ "Fee: %{customdata[4]}<br>"
489
+ "Balance: %{customdata[5]}"
490
+ "<extra></extra>",
491
+ customdata=buy_df[['date_str', 'time_str', 'price_str', 'amount_str', 'fee_str', 'balance_str']].values
492
+ ), row=2, col=1)
493
+
494
+ # Sell markers
495
+ sell_df = df[df['action'] == 'S'].copy()
496
+ sell_df['date_str'] = sell_df.index.strftime('%Y-%m-%d')
497
+ sell_df['time_str'] = sell_df.index.strftime('%H:%M:%S')
498
+ sell_df['balance_str'] = sell_df['equity'].apply(lambda x: f"{x:,.2f}")
499
+ sell_df['amount_str'] = sell_df['amount'].apply(lambda x: f"{x:.2f}")
500
+ sell_df['fee_str'] = sell_df['fee'].apply(lambda x: f"{x:.2f}")
501
+ sell_df['price_str'] = sell_df['price'].apply(lambda x: f"{x:.2f}")
502
+ fig.add_trace(go.Scatter(
503
+ x=sell_df.index, y=sell_df['price'], mode='markers', name='Sell',
504
+ marker=dict(symbol='triangle-down', color='red', size=10),
505
+ hovertemplate="Sell [%{customdata[0]}]<br>"
506
+ "Time: %{customdata[1]}<br>"
507
+ "Price: %{customdata[2]}<br>"
508
+ "Amount: %{customdata[3]}<br>"
509
+ "Fee: %{customdata[4]}<br>"
510
+ "Balance: %{customdata[5]}"
511
+ "<extra></extra>",
512
+ customdata=buy_df[['date_str', 'time_str', 'price_str', 'amount_str', 'fee_str', 'balance_str']].values
513
+ ), row=2, col=1)
514
+
515
+ # Latest annotation
516
+ last_row = df.iloc[-1]
517
+ fig.add_trace(go.Scatter(
518
+ x=[last_row.index], y=[last_row['cum_ret']],
519
+ mode='text', text=[f" - {last_row['cum_ret']:.2f}"],
520
+ textposition='middle right',
521
+ textfont=dict(color='blue', size=12), showlegend=False
522
+ ), row=1, col=1)
523
+
524
+ fig.add_trace(go.Scatter(
525
+ x=[last_row.index], y=[last_row['bm_cum_ret']],
526
+ mode='text', text=[f" - {last_row['bm_cum_ret']:.2f}"],
527
+ textposition='middle right',
528
+ textfont=dict(color='gray', size=12), showlegend=False
529
+ ), row=1, col=1)
530
+
531
+ fig.add_trace(go.Scatter(
532
+ x=[last_row.index], y=[last_row['price']],
533
+ mode='text', text=[f" - {last_row['price']:.2f}"],
534
+ textposition='middle right',
535
+ textfont=dict(color='black', size=12), showlegend=False
536
+ ), row=2, col=1)
537
+
538
+ # Table
539
+ fig.add_trace(go.Table(
540
+ header=dict(
541
+ values=["<b>Metric</b>", "<b>Value</b>"],
542
+ fill_color="lightgray", align="left",
543
+ font=dict(size=12)
544
+ ),
545
+ cells=dict(
546
+ values=[metric_names, metric_values],
547
+ fill_color="white", align="left",
548
+ font=dict(size=12)
549
+ )
550
+ ), row=1, col=2)
551
+
552
+ fig.update_layout(
553
+ title=f"Trading Strategy Performance: {self.name}",
554
+ margin=dict(l=10, r=10, t=40, b=10),
555
+ legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1),
556
+ xaxis2_title="Time",
557
+ yaxis1_title="Cumulative Return",
558
+ yaxis2_title="Price",
559
+ template='plotly_white',
560
+ hovermode='closest'
561
+ )
562
+
563
+ import IPython
564
+ if IPython.get_ipython():
565
+ fig.show()
566
+ else:
567
+ from IPython.utils import io
568
+ with io.capture_output() as captured:
569
+ fig.show()
File without changes
File without changes
File without changes
quantvn/vn/__init__.py ADDED
@@ -0,0 +1 @@
1
+ from quantvn.vn import data, metrics