diff --git a/python/fastquant/data/stocks/stocks.py b/python/fastquant/data/stocks/stocks.py index 6eb4780c..d6f2cba8 100644 --- a/python/fastquant/data/stocks/stocks.py +++ b/python/fastquant/data/stocks/stocks.py @@ -52,16 +52,15 @@ def get_stock_data( if source == "yahoo": # The query is run on 'yahoo', but if the symbol isn't found, the same query is run on 'phisix'. df = get_yahoo_data(symbol, start_date, end_date, dividends) - if df is None or symbol == "JFC": - format = "c" - df = get_pse_data(symbol, start_date, end_date, format=format) + if df is None: + raise Exception(f"{symbol} could not be found on Yahoo") elif source == "phisix": # The query is run on 'phisix', but if the symbol isn't found, the same query is run on 'yahoo'. format = "c" df = get_pse_data(symbol, start_date, end_date, format=format) if df is None: - df = get_yahoo_data(symbol, start_date, end_date, dividends) + raise Exception(f"{symbol} could not be found on Phisix") else: raise Exception("Source must be either 'phisix' or 'yahoo'") diff --git a/python/fastquant/data/stocks/yahoofinance.py b/python/fastquant/data/stocks/yahoofinance.py index 14b74762..0df40a59 100644 --- a/python/fastquant/data/stocks/yahoofinance.py +++ b/python/fastquant/data/stocks/yahoofinance.py @@ -33,16 +33,23 @@ def get_yahoo_data(symbol, start_date, end_date, dividends=True): "High": "high", "Low": "low", "Close": "close", - "Adj Close": "adj_close", "Volume": "volume", "Dividends": "dividend", } + + # Multi-index dataframes are not supported in fastquant + if "Ticker" in df.columns.names: + df.columns = df.columns.droplevel("Ticker") + + # "Adj Close" is missing from the recent versions of YFinance + df = df.drop(['Adj Close'], errors='ignore') + if dividends: ticker = yf.Ticker(symbol) div_df = ticker.dividends if div_df.shape[0] > 0: - df = df.join(div_df, how="left", on="Date") + df = df.join(div_df.tz_localize(None), how="left", on="Date") else: df["dividend"] = 0 else: @@ -54,7 +61,6 @@ def get_yahoo_data(symbol, start_date, end_date, dividends=True): "high", "low", "close", - "adj_close", "volume", "dividend", ] diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py index e40a1281..96ce91f6 100644 --- a/python/fastquant/strategies/base.py +++ b/python/fastquant/strategies/base.py @@ -186,10 +186,10 @@ def __init__(self): self.price_bought = 0 # Initialize stoploss order - self.stoploss_order = None + self.stoploss_orders = [] # Initialize stoploss trail order - self.stoploss_trail_order = None + self.stoploss_trail_orders = [] def buy_signal(self): return False @@ -289,8 +289,8 @@ def start(self): def next(self): # add dividend to cash - if self.invest_div and self.datadiv is not None: - self.broker.add_cash(self.datadiv) + if self.invest_div and self.datadiv is not None and self.datadiv.get()[0] != 0: + self.broker.add_cash(self.datadiv.get()[0]) if self.add_cash_amount: if self.first_timepoint: @@ -400,25 +400,21 @@ def next(self): stop_price = self.data.close[0] * (1.0 - self.stop_loss) if self.transaction_logging: self.log("Stop price: {}".format(stop_price)) - self.stoploss_order = self.sell( + self.stoploss_orders.append(self.sell( exectype=bt.Order.Stop, price=stop_price, size=final_size, - ) + )) if self.stop_trail: # Create a stoploss trail order if None - if self.stoploss_trail_order is None: - if self.transaction_logging: - self.log("Stop trail: {}".format(self.stop_trail)) - self.stoploss_trail_order = self.sell( - exectype=bt.Order.StopTrail, - trailpercent=self.stop_trail, - size=final_size, - ) - # Cancel existing stoploss trail order - else: - self.cancel(self.stoploss_trail_order) + if self.transaction_logging: + self.log("Stop trail: {}".format(self.stop_trail)) + self.stoploss_trail_orders.append(self.sell( + exectype=bt.Order.StopTrail, + trailpercent=self.stop_trail, + size=final_size, + )) # Buy based on the opening price of the next closing day (only works "open" data exists in the dataset) else: @@ -443,20 +439,20 @@ def next(self): stop_price = self.data.close[0] * (1.0 - self.stop_loss) if self.transaction_logging: self.log("Stop price: {}".format(stop_price)) - self.stoploss_order = self.sell( + self.stoploss_orders.append(self.sell( exectype=bt.Order.Stop, price=stop_price, size=final_size, - ) + )) if self.stop_trail: if self.transaction_logging: self.log("Stop trail: {}".format(self.stop_trail)) - self.stoploss_trail_order = self.sell( + self.stoploss_trail_orders.append(self.sell( exectype=bt.Order.StopTrail, trailpercent=self.stop_trail, size=final_size, - ) + )) elif self.sell_signal() and (self.strategy_position in [1, -1, None]): self.strategy_position = 0 if self.strategy_position in [1, -1] else None @@ -531,13 +527,10 @@ def next(self): size=int((self.init_cash / self.dataopen[1]) * self.sell_prop) ) - # Explicitly cancel stoploss order - if self.stoploss_order: - self.cancel(self.stoploss_order) - - # Explicitly cancel stoploss trail order - if self.stoploss_trail_order: - self.cancel(self.stoploss_trail_order) + # Explicitly cancel active stop-loss (trail) orders + for order in self.stoploss_orders + self.stoploss_trail_orders: + if order.status not in ["Completed", "Canceled", "Expired", "Rejected"]: + self.cancel(order) elif self.take_profit_signal(): # Take profit