From 083d2c521d0b9398be059039fa15c30fbbeecb32 Mon Sep 17 00:00:00 2001 From: Benj Del Mundo Date: Tue, 23 Mar 2021 21:06:35 +0800 Subject: [PATCH 1/4] Updated default signal to false, added logger --- python/fastquant/strategies/base.py | 600 ++++++++++++--------- python/fastquant/utils/strategy_logging.py | 180 +++++++ 2 files changed, 512 insertions(+), 268 deletions(-) mode change 100644 => 100755 python/fastquant/strategies/base.py create mode 100755 python/fastquant/utils/strategy_logging.py diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py old mode 100644 new mode 100755 index 01683f06..e20fab5a --- a/python/fastquant/strategies/base.py +++ b/python/fastquant/strategies/base.py @@ -10,6 +10,8 @@ from pkg_resources import resource_filename import datetime import sys +import warnings +import logging # Import modules import backtrader as bt @@ -22,6 +24,7 @@ from fastquant.notification import trigger_bot import croniter + from fastquant.config import ( INIT_CASH, COMMISSION_PER_TRANSACTION, @@ -30,27 +33,68 @@ SELL_PROP, SHORT_MAX, ) +from fastquant.utils.strategy_logging import StrategyLogger class BaseStrategy(bt.Strategy): """ Base Strategy template for all strategies to be added to fastquant + + Attributes + ---------- + init_cash + buy_proportion + sell_proportion + commission + stop_loss_percent + stop_trail_percent + take_profit_percent + execution_type + allow_short_selling + max_short_ratio + add_cash_amount + add_cash_frequency + channel + symbol + + Modified/Removed: + init_cash :initial_cash + buy_prop :buy_proportion + sell_prop :sell_proportion + stop_loss :stop_loss_percent + stop_trail :stop_trail_percent + periodic_logging + strategy_logging + allow_short : allow_short_selling + short_max : max_short_ratio + add_cash_freq : add_cash_frequency + + + + """ + + """" + init + buy_signal + sell_signal + update_logging + """ - # Strategy level arguments + # Set default parameters # After initialization, the `params` variable becomes accessible as an attribute of the strategy object # with the properties of a `named tuple` params = ( ("init_cash", INIT_CASH), + ("custom", 10), + ("custom2", 20), ("buy_prop", BUY_PROP), ("sell_prop", SELL_PROP), ("commission", COMMISSION_PER_TRANSACTION), ("stop_loss", 0), # Zero means no stop loss ("stop_trail", 0), # Zero means no stop loss - ( - "execution_type", - "close", - ), # Either open or close, to indicate if a purchase is executed based on the next open or close + # Either open or close, to indicate if a purchase is executed based on the next open or close + ("execution_type", "market"), ("periodic_logging", False), ("transaction_logging", True), ("strategy_logging", True), @@ -62,80 +106,48 @@ class BaseStrategy(bt.Strategy): ("add_cash_freq", "M"), ) - def log(self, txt, dt=None): - dt = dt or self.datas[0].datetime.date(0) - print("%s, %s" % (dt.isoformat(), txt)) - - def update_order_history(self, order): - self.order_history["dt"].append(self.datas[0].datetime.date(0)) - self.order_history["type"].append("buy" if order.isbuy() else "sell") - self.order_history["price"].append(order.executed.price) - self.order_history["size"].append(order.executed.size) - self.order_history["value"].append(order.executed.value) - self.order_history["commission"].append(order.executed.comm) - self.order_history["pnl"].append(order.executed.pnl) + def __init__(self): - def update_periodic_history(self): - self.periodic_history["dt"].append(self.datas[0].datetime.date(0)) - self.periodic_history["portfolio_value"].append(self.broker.getvalue()) - self.periodic_history["cash"].append(self.broker.getcash()) + # print("sadsads", self.params.custom) + print(type(self.params.custom2)) - def __init__(self): - # Global variables - self.init_cash = self.params.init_cash - self.buy_prop = self.params.buy_prop - self.sell_prop = self.params.sell_prop + self.initial_cash = self.params.init_cash + self.buy_proportion = self.params.buy_prop + self.sell_proportion = self.params.sell_prop self.execution_type = self.params.execution_type self.periodic_logging = self.params.periodic_logging self.transaction_logging = self.params.transaction_logging self.strategy_logging = self.params.strategy_logging self.commission = self.params.commission self.channel = self.params.channel - self.stop_loss = self.params.stop_loss - self.stop_trail = self.params.stop_trail - self.allow_short = self.params.allow_short - self.short_max = self.params.short_max + self.stop_loss_percent = self.params.stop_loss + self.stop_trail_percent = self.params.stop_trail + self.allow_short_selling = self.params.allow_short + self.max_short_ratio = self.params.short_max + self.broker.set_coc(True) - add_cash_freq = self.params.add_cash_freq + add_cash_frequency = self.params.add_cash_freq # Longer term, we plan to add `freq` like notation, similar to pandas datetime # https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html # M means add cash at the first day of each month - if add_cash_freq == "M": - self.add_cash_freq = "0 0 1 * *" + if add_cash_frequency == "M": + self.add_cash_frequency = "0 0 1 * *" # W means add cash once a week on monday - elif add_cash_freq == "W": - self.add_cash_freq = "0 0 * * 1" + elif add_cash_frequency == "W": + self.add_cash_frequency = "0 0 * * 1" # Otherwise, it assumes the input is in cron notation (no change) else: - self.add_cash_freq = add_cash_freq + self.add_cash_frequency = add_cash_frequency self.add_cash_amount = self.params.add_cash_amount # Attribute that tracks how much cash was added over time self.total_cash_added = 0 - if self.strategy_logging: - self.log("===Global level arguments===") - self.log("init_cash : {}".format(self.init_cash)) - self.log("buy_prop : {}".format(self.buy_prop)) - self.log("sell_prop : {}".format(self.sell_prop)) - self.log("commission : {}".format(self.commission)) - self.log("stop_loss : {}".format(self.stop_loss)) - self.log("stop_trail : {}".format(self.stop_trail)) - self.order_history = { - "dt": [], - "type": [], - "price": [], - "size": [], - "value": [], - "commission": [], - "pnl": [], - } - self.periodic_history = { - "dt": [], - "portfolio_value": [], - "cash": [], - } + # Create logging handler + self.logging = StrategyLogger(self) + self.logging.strategy_argments() + self.order_history_df = None self.periodic_history_df = None @@ -150,19 +162,25 @@ def __init__(self): # Sets the latest action as "buy", "sell", or "neutral" self.action = None + # Warn if user forgets to set a value to commission + if self.commission == 0: + warnings.warn( + "Comission value is set to 0. This might yield unrealistic results. Set using the `commission` paramter in `backtest()`", + UserWarning, + ) + def buy_signal(self): - return True + return False # Safer to set default action to do nothing def sell_signal(self): - return True + return False # Safer to set default action to do nothing def notify_order(self, order): if order.status in [order.Submitted, order.Accepted]: return if order.status in [order.Completed]: - # Update order history whenever an order is completed - self.update_order_history(order) + if order.isbuy(): self.action = "buy" self.buyprice = order.executed.price @@ -172,261 +190,307 @@ def notify_order(self, order): self.action = "sell" self.bar_executed = len(self) + self.logging.order_executed(order, action=self.action.upper()) - if self.transaction_logging: - self.log( - "%s EXECUTED, Price: %.2f, Cost: %.2f, Comm: %.2f, Size: %.2f" - % ( - self.action.upper(), - order.executed.price, - order.executed.value, - order.executed.comm, - order.executed.size, - ) - ) + # Update order history whenever an order is completed + self.logging.update_order_history(order) elif order.status in [order.Canceled, order.Margin, order.Rejected]: - if self.transaction_logging: - if not self.periodic_logging: - self.log("Cash %s Value %s" % (self.cash, self.value)) - self.log("Order Canceled/Margin/Rejected") - self.log("Canceled: {}".format(order.status == order.Canceled)) - self.log("Margin: {}".format(order.status == order.Margin)) - self.log("Rejected: {}".format(order.status == order.Rejected)) + self.logging.order_aborted(order) # Write down: no pending order self.order = None def notify_trade(self, trade): - if not trade.isclosed: + if trade.isclosed: + self.logging.trade_closed(trade) + else: return - if self.transaction_logging: - self.log( - "OPERATION PROFIT, GROSS: %.2f, NET: %.2f" % (trade.pnl, trade.pnlcomm) - ) def notify_cashvalue(self, cash, value): # Update cash and value every period - if self.periodic_logging: - self.log("Cash %s Value %s" % (cash, value)) self.cash = cash self.value = value + self.logging.cash_value() def stop(self): # Saving to self so it's accessible later during optimization self.final_value = self.broker.getvalue() # Note that PnL is the final portfolio value minus the initial cash balance minus the total cash added - self.pnl = round(self.final_value - self.init_cash - self.total_cash_added, 2) + self.pnl = round( + self.final_value - self.initial_cash - self.total_cash_added, 2 + ) if self.strategy_logging: - self.log("Final Portfolio Value: {}".format(self.final_value)) - self.log("Final PnL: {}".format(self.pnl)) - self.order_history_df = pd.DataFrame(self.order_history) - self.periodic_history_df = pd.DataFrame(self.periodic_history) + self.logging.log( + "Final Portfolio Value: {}".format(self.final_value) + ) + self.logging.log("Final PnL: {}".format(self.pnl)) + + # Create dataframe from logging module + self.order_history_df = pd.DataFrame(self.logging.order_history) + self.periodic_history_df = pd.DataFrame(self.logging.periodic_history) + self.trade_history_df = pd.DataFrame(self.logging.trade_history) last_date = str(self.datas[0].datetime.date(0)) if self.channel: trigger_bot( - self.symbol, self.action, last_date, + self.symbol, + self.action, + last_date, ) def start(self): # Used to signal setting the first iteration self.first_timepoint = True - def next(self): - if self.add_cash_amount: - if self.first_timepoint: - # Initialize income date iterator, and set next - start_date = self.datas[0].datetime.datetime(0) - self.cron = croniter.croniter(self.add_cash_freq, start_date) - self.next_cash_datetime = self.cron.get_next(datetime.datetime) + def periodic_cash_in(self): - if self.transaction_logging: - self.log("Start date: {}".format(start_date.strftime("%Y-%m-%d"))) - self.log( - "Next cash date: {}".format( - self.next_cash_datetime.strftime("%Y-%m-%d") - ) - ) + if self.first_timepoint: + # Change state to indicate that the cash date iterator has been set + self.first_timepoint = False - # Change state to indicate that the cash date iterator has been set - self.first_timepoint = False + # Initialize income date iterator, and set next + start_date = self.datas[0].datetime.datetime(0) + self.cron = croniter.croniter(self.add_cash_frequency, start_date) + self.next_cash_datetime = self.cron.get_next(datetime.datetime) - # Add cash to broker if date is same or later to the next income date - # This means if the dataset is only for weekdays, a date on a weekend will be executed on the next monday - if self.datas[0].datetime.datetime(0) >= self.next_cash_datetime: - self.broker.add_cash(self.add_cash_amount) - self.next_cash_datetime = self.cron.get_next(datetime.datetime) - self.total_cash_added += self.add_cash_amount + self.logging.initialize_periodic_cash_in( + start_date, self.next_cash_datetime + ) - if self.transaction_logging: - self.log("Cash added: {}".format(self.add_cash_amount)) - self.log("Total cash added: {}".format(self.total_cash_added)) - self.log( - "Next cash date: {}".format( - self.next_cash_datetime.strftime("%Y-%m-%d") - ) - ) + # Add cash to broker if date is same or later to the next income date + # This means if the dataset is only for weekdays, a date on a weekend will be executed on the next monday + if self.datas[0].datetime.datetime(0) >= self.next_cash_datetime: + self.broker.add_cash(self.add_cash_amount) + self.next_cash_datetime = self.cron.get_next(datetime.datetime) + self.total_cash_added += self.add_cash_amount + + self.logging.periodic_cash_in( + self.add_cash_amount, + self.total_cash_added, + self.next_cash_datetime, + ) + + def next(self): + if self.add_cash_amount > 0: + self.periodic_cash_in() # Trigger only when amount is specified + + self.logging.update_periodic_history() # dt, position_value, position_size, cash, - self.update_periodic_history() - if self.periodic_logging: - self.log("Close, %.2f" % self.dataclose[0]) if self.order: return - if self.periodic_logging: - self.log("CURRENT POSITION SIZE: {}".format(self.position.size)) - # Skip the last observation since purchases are based on next day closing prices (no value for the last observation) if len(self) + 1 >= self.len_data: return - # Only sell if you hold least one unit of the stock (and sell only that stock, so no short selling) - stock_value = self.value - self.cash + # Allow for backwards compatibilty; older strategies return only the signal, new strategies can return both signal and buy/sell proportion + buy_signal_response = self.buy_signal() + sell_signal_response = self.sell_signal() + if type(buy_signal_response) == bool: + buy_signal, buy_prop = buy_signal_response, None + else: + buy_signal, buy_prop = buy_signal_response - # Only buy if there is enough cash for at least one stock - if self.buy_signal(): - if self.cash >= self.dataclose[0]: + if type(sell_signal_response) == bool: + sell_signal, sell_prop = sell_signal_response, None + else: + sell_signal, sell_prop = sell_signal_response - if self.transaction_logging: - self.log("BUY CREATE, %.2f" % self.dataclose[0]) - # Take a 10% long position every time it's a buy signal (or whatever is afforded by the current cash position) - # "size" refers to the number of stocks to purchase - # Afforded size is based on closing price for the current trading day - # Margin is required for buy commission - # Add allowance to commission per transaction (avoid margin) - afforded_size = int( - self.cash / (self.dataclose[0] * (1 + self.commission + 0.001)) + if buy_signal: + self.create_buy_order(buy_prop) + # self.action will be set order is completed + + elif sell_signal: + self.create_sell_order(sell_prop) + # self.action will be set order is completed + + else: + self.action = "neutral" + + def create_buy_order(self, buy_proportion=None): + """ + Returns + ------- + order + """ + + # Buy based on the current closing price + if self.execution_type == "market": + market_price = self.dataclose[0] + + # Buy based on the next closing price + elif self.execution_type == "close": + market_price = self.dataclose[1] + + afforded_size = int(self.cash / (market_price * (1 + self.commission))) + + # Compute number of units to buy based on the proportion + if buy_proportion is None: + buy_proportion_size = int(afforded_size * self.buy_proportion) + else: + buy_proportion_size = int(afforded_size * buy_proportion) + + final_size = min(buy_proportion_size, afforded_size) + + # Log that buy signal is triggered + self.logging.buy_signal( + self.cash, + market_price, + buy_proportion_size, + afforded_size, + final_size, + ) + + # Buy only if final size is > 0 + if final_size > 0: + + # Buy on today's closing price + if self.execution_type == "market": + self.order = self.buy( + size=final_size, exectype=bt.Order.Market + ) + self.logging.order_created( + order_type_str="buy", + text="Order type: %s Price: %.2f" + % (self.execution_type, market_price), ) - buy_prop_size = int(afforded_size * self.buy_prop) - # Buy based on the closing price of the previous closing day - if self.execution_type == "close": - final_size = min(buy_prop_size, afforded_size) - if self.transaction_logging: - self.log("Cash: {}".format(self.cash)) - self.log("Price: {}".format(self.dataclose[0])) - self.log("Buy prop size: {}".format(buy_prop_size)) - self.log("Afforded size: {}".format(afforded_size)) - self.log("Final size: {}".format(final_size)) - # Explicitly setting exectype=bt.Order.Close will make the next day's closing the reference price - self.order = self.buy(size=final_size) - - # Implement stop loss at the purchase level (only this specific trade is closed) - if self.stop_loss: - stop_price = self.data.close[0] * (1.0 - self.stop_loss) - if self.transaction_logging: - self.log("Stop price: {}".format(stop_price)) - 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.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: - # Margin is required for buy commission - afforded_size = int( - self.cash / (self.dataopen[1] * (1 + self.commission + 0.001)) - ) - final_size = min(buy_prop_size, afforded_size) - if self.transaction_logging: - self.log("Buy prop size: {}".format(buy_prop_size)) - self.log("Afforded size: {}".format(afforded_size)) - self.log("Final size: {}".format(final_size)) - self.order = self.buy(size=final_size) - - # Implement stop loss at the purchase level (only this specific trade is closed) - if self.stop_loss: - stop_price = self.data.close[0] * (1.0 - self.stop_loss) - if self.transaction_logging: - self.log("Stop price: {}".format(stop_price)) - 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.sell( - exectype=bt.Order.StopTrail, - trailpercent=self.stop_trail, - size=final_size, - ) - - elif self.sell_signal(): - if self.allow_short: - - # Sell short based on the closing price of the previous day - if self.execution_type == "close": - - sell_prop_size = int( - SELL_PROP * self.broker.getvalue() / self.dataclose[1] - ) - # The max incremental short allowed is the short that would lead to a cumulative short position - # equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default) - max_position_size = max( - int(self.broker.getvalue() * self.short_max / self.dataclose[1]) - + self.position.size, - 0, - ) - if max_position_size > 0: - if self.transaction_logging: - self.log("SELL CREATE, %.2f" % self.dataclose[1]) - self.order = self.sell( - size=min(sell_prop_size, max_position_size) - ) - - # Buy based on the opening price of the next closing day (only works "open" data exists in the dataset) - else: - - sell_prop_size = int( - SELL_PROP * self.broker.getvalue() / self.dataopen[1] - ) - # The max incremental short allowed is the short that would lead to a cumulative short position - # equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default) - max_position_size = max( - int(self.broker.getvalue() * self.short_max / self.dataopen[1]) - + self.position.size, - 0, - ) - if max_position_size > 0: - if self.transaction_logging: - self.log("SELL CREATE, %.2f" % self.dataopen[1]) - self.order = self.sell( - size=min(sell_prop_size, max_position_size) - ) - elif stock_value > 0: + # Buy on tomorrow's closing price + elif self.execution_type == "close": + self.order = self.buy(size=final_size, exectype=bt.Order.Close) + self.logging.order_created( + order_type_str="buy", + text="Order type: %s Price: %.2f" + % (self.execution_type, market_price), + ) + + # create stop loss order + if self.stop_loss_percent: + stop_price = market_price * (1.0 - self.stop_loss_percent) + self.sell( + exectype=bt.Order.Stop, + price=stop_price, + size=size, + ) + self.logging.order_created( + order_type_str="buy", + text="Order type: %s Price: %.2f" + % ("Stop Order", stop_price), + ) + # create trailing stop order + if self.stop_trail_percent: + self.sell( + exectype=bt.Order.StopTrail, + trailpercent=self.stop_trail_percent, + size=final_size, + ) + self.logging.order_created( + order_type_str="buy", + text="Order type: %s Trail Percent: %.2f" + % ("Trailing Stop", self.stop_trail_percent), + ) + + def create_short_sell_order(self, sell_proportion=None): + # Sell short based on the closing price of the previous day + if self.execution_type == "close": + + sell_proportion_size = int( + self.sell_proportion + * self.broker.getvalue() + / self.dataclose[1] + ) + # The max incremental short allowed is the short that would lead to a cumulative short position + # equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default) + max_position_size = max( + int( + self.broker.getvalue() + * self.max_short_ratio + / self.dataclose[1] + ) + + self.position.size, + 0, + ) + if max_position_size > 0: if self.transaction_logging: self.log("SELL CREATE, %.2f" % self.dataclose[1]) - # Sell a 5% sell position (or whatever is afforded by the current stock holding) - # "size" refers to the number of stocks to purchase - if self.execution_type == "close": - if SELL_PROP == 1: - self.order = self.sell( - size=self.position.size, exectype=bt.Order.Close - ) - else: - # Sell based on the closing price of the previous closing day - self.order = self.sell( - size=int( - (stock_value / (self.dataclose[1])) * self.sell_prop - ), - exectype=bt.Order.Close, - ) - else: - # Sell based on the opening price of the next closing day (only works "open" data exists in the dataset) + self.order = self.sell( + size=min(sell_proportion_size, max_position_size) + ) + + # Buy based on the opening price of the next closing day (only works "open" data exists in the dataset) + else: + + sell_proportion_size = int( + self.sell_proportion + * self.broker.getvalue() + / self.dataopen[1] + ) + # The max incremental short allowed is the short that would lead to a cumulative short position + # equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default) + max_position_size = max( + int( + self.broker.getvalue() + * self.max_short_ratio + / self.dataopen[1] + ) + + self.position.size, + 0, + ) + if max_position_size > 0: + if self.transaction_logging: + self.log("SELL CREATE, %.2f" % self.dataopen[1]) + self.order = self.sell( + size=min(sell_proportion_size, max_position_size) + ) + + def create_sell_order(self, sell_proportion=None): + + if self.allow_short_selling: + self.create_short_sell_order(sell_proportion) + + # Only sell if you hold least one unit of the stock (and sell only that stock, so no short selling) + elif self.position.size > 0: + + # Verify that you could actually sell based on the sell proportion and the units owned + if sell_proportion is None: + sell_proportion = self.sell_proportion + + sell_proportion_size = int(self.position.size * sell_proportion) + + # Log that sell signal was triggered + self.logging.sell_signal( + size=sell_proportion_size, + price=( + self.dataclose[0] + if self.execution_type == "market" + else self.dataclose[1] + ), + ) + + if sell_proportion_size > 0: + + # Sell on current closing price + if self.execution_type == "market": self.order = self.sell( - size=int((self.init_cash / self.dataopen[1]) * self.sell_prop) + size=sell_proportion_size, exectype=bt.Order.Market + ) + sell_price = self.dataclose[0] + self.logging.order_created( + order_type_str="sell", + text="Order type: %s Price: %.2f" + % (self.execution_type, sell_price), ) - else: - self.action = "neutral" + # Sell on next closing price + elif self.execution_type == "close": + self.order = self.sell( + size=sell_proportion_size, exectype=bt.Order.Close + ) + sell_price = self.dataclose[1] + self.logging.order_created( + order_type_str="sell", + text="Order type: %s Price: %.2f" + % (self.execution_type, sell_price), + ) diff --git a/python/fastquant/utils/strategy_logging.py b/python/fastquant/utils/strategy_logging.py new file mode 100755 index 00000000..f71902f2 --- /dev/null +++ b/python/fastquant/utils/strategy_logging.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +import logging +import sys + + +class StrategyLogger: + + # """ + # Handles the logging throughout the strategy life-cycle. It is called within the BaseStrategy class + # Responsible for the log messages + + # """ + + def __init__(self, strategy): + self.strategy = strategy + self.order_history = { + "dt": [], + "type": [], + "price": [], + "size": [], + "value": [], + "commission": [], + "pnl": [], + } + self.periodic_history = { + "dt": [], + "portfolio_value": [], + "position_size": [], + "cash": [], + } + self.trade_history = {"dt": [], "pnl": [], "pnl_comm": []} + + self.periodic_logging = strategy.periodic_logging + self.transaction_logging = strategy.transaction_logging + + # Configure logging module + logging.basicConfig( + level=logging.INFO, # Log everything above DEBUG level + format="%(message)s", # Use display only the message + stream=sys.stdout, # use output stream instead of the default error stream (could also be set to a file output) + ) + + def log(self, txt, dt=None, level=logging.INFO): + try: + dt = dt or self.strategy.datas[0].datetime.date(0) + dt = dt.isoformat() + except: + dt = "" + + if level == logging.DEBUG: + logging.debug("%s, %s" % (dt, txt)) + elif level == logging.INFO: + logging.info("%s, %s" % (dt, txt)) + elif level == logging.WARNING: + logging.warn("%s, %s" % (dt, txt)) + + def strategy_argments(self): + strategy = self.strategy + self.log("===Global level arguments===") + self.log("init_cash : {}".format(strategy.initial_cash)) + self.log("buy_prop : {}".format(strategy.buy_proportion)) + self.log("sell_prop : {}".format(strategy.sell_proportion)) + self.log("commission : {}".format(strategy.commission)) + self.log("stop_loss_percent : {}".format(strategy.stop_loss_percent)) + self.log("stop_trail_percent : {}".format(strategy.stop_trail_percent)) + + def order_created(self, order_type_str, text): + + if self.transaction_logging: + self.log("ORDER CREATED: %s, %s" % (order_type_str.upper(), text)) + + def order_executed(self, order, action): + if self.transaction_logging: + self.log( + "ORDER EXECUTED %s , Price: %.2f, Cost: %.2f, Comm: %.2f, Size: %.2f" + % ( + action.upper(), + order.executed.price, + order.executed.value, + order.executed.comm, + order.executed.size, + ) + ) + + def order_aborted(self, order): + if self.transaction_logging: + self.log("Order Canceled/Margin/Rejected") + self.log("Canceled: {}".format(order.status == order.Canceled)) + self.log("Margin: {}".format(order.status == order.Margin)) + self.log("Rejected: {}".format(order.status == order.Rejected)) + self.log( + "Cash %s Value %s" % (self.strategy.cash, self.strategy.value) + ) + + def trade_closed(self, trade): + if self.transaction_logging: + self.log( + "TRADE CLOSED: OPERATION PROFIT, GROSS: %.2f, NET: %.2f" + % (trade.pnl, trade.pnlcomm) + ) + + def cash_value(self): + if self.periodic_logging: + self.log( + "Cash %.2f Value %.2f" + % (self.strategy.cash, self.strategy.value) + ) + + def initialize_periodic_cash_in(self, start_date, next_cash_datetime): + if self.transaction_logging: + self.log("Start date: {}".format(start_date.strftime("%Y-%m-%d"))) + self.log( + "Next cash date: {}".format( + next_cash_datetime.strftime("%Y-%m-%d") + ) + ) + + def periodic_cash_in( + self, add_cash_amount, total_cash_added, next_cash_datetime + ): + if self.transaction_logging: + self.log("Cash added: {}".format(add_cash_amount)) + self.log("Total cash added: {}".format(total_cash_added)) + self.log( + "Next cash date: {}".format( + next_cash_datetime.strftime("%Y-%m-%d") + ) + ) + + def buy_signal( + self, + cash, + market_price, + buy_proportion_size, + afforded_size, + final_size, + ): + if self.transaction_logging: + self.log("BUY SIGNAL GENERATED") + self.log("Cash: {}".format(cash)) + self.log("Price: {}".format(market_price)) + self.log("Buy prop size: {}".format(buy_proportion_size)) + self.log("Afforded size: {}".format(afforded_size)) + self.log("Final size: {}".format(final_size)) + + def sell_signal(self, size, price): + if self.transaction_logging: + self.log("SELL SIGNAL GENERATED") + self.log("Sell Size: {}".format(size)) + self.log("Sell Price: {}".format(price)) + + def update_order_history(self, order): + self.order_history["dt"].append( + self.strategy.datas[0].datetime.date(0) + ) + self.order_history["type"].append("buy" if order.isbuy() else "sell") + self.order_history["price"].append(order.executed.price) + self.order_history["size"].append(order.executed.size) + self.order_history["value"].append(order.executed.value) + self.order_history["commission"].append(order.executed.comm) + self.order_history["pnl"].append(order.executed.pnl) + + def update_periodic_history(self): + self.periodic_history["dt"].append( + self.strategy.datas[0].datetime.date(0) + ) + self.periodic_history["portfolio_value"].append( + self.strategy.broker.getvalue() + ) + self.periodic_history["position_size"].append( + self.strategy.position.size + ) + self.periodic_history["cash"].append(self.strategy.broker.getcash()) + + def update_trade_history(self, pnl, pnl_comm): + self.trade_history["dt"].append( + self.strategy.datas[0].datetime.date(0) + ) + self.trade_history["pnl"].append(pnl) + self.trade_history["pnl_comm"].append(pnl_comm) From d623785614a6ad6f55b3dfe69287162d27f98ab4 Mon Sep 17 00:00:00 2001 From: Benj Del Mundo Date: Tue, 23 Mar 2021 21:28:37 +0800 Subject: [PATCH 2/4] Fixed docstrings in BaseStrategy --- python/fastquant/strategies/base.py | 61 +++++++++++++++-------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py index e20fab5a..15c67272 100755 --- a/python/fastquant/strategies/base.py +++ b/python/fastquant/strategies/base.py @@ -42,22 +42,36 @@ class BaseStrategy(bt.Strategy): Attributes ---------- - init_cash - buy_proportion - sell_proportion - commission - stop_loss_percent - stop_trail_percent - take_profit_percent - execution_type - allow_short_selling - max_short_ratio - add_cash_amount - add_cash_frequency - channel - symbol - - Modified/Removed: + init_cash : float + inital cash + buy_prop : float + buy proportion, percentage of available units to buy, setting to 1.0 buys all afforded units + sell_prop : float + sell proportion, percentage of owned units to sell + commission: float + Percentage of comission + stop_loss : float + stop loss percent + stop_trail : float + stop trail percent + execution_type : str {`market`,`close`} (default=`market`) + If set to `market`, excecutes the action (buy or sell) at current bar (or day), + If set to `close`, executees the action on the next available bar + allow_short : bool (default=False) + allow short selling + short_max : float + The max incremental short allowed is the short that would lead to a cumulative short position + equal to the maximum short position (initial cash times the maximum short ratio, which is 1.5 by default) + add_cash_amount : int (default=0) + Added cash amount for periodic cash-ins. (can be used in cost averaging) + add_cash_frequency : str {`W` or `M`} (default=`W`) + Frequency of when to add cash (weekly or monthly) + channel: str (default=`""`) + Notification channel + symbol: str + Symbol of the asset traded + + Renamed in code: init_cash :initial_cash buy_prop :buy_proportion sell_prop :sell_proportion @@ -67,17 +81,8 @@ class BaseStrategy(bt.Strategy): strategy_logging allow_short : allow_short_selling short_max : max_short_ratio - add_cash_freq : add_cash_frequency - - - - """ - - """" - init - buy_signal - sell_signal - update_logging + add_cash_freq : + add_cash_frequency """ @@ -86,8 +91,6 @@ class BaseStrategy(bt.Strategy): # with the properties of a `named tuple` params = ( ("init_cash", INIT_CASH), - ("custom", 10), - ("custom2", 20), ("buy_prop", BUY_PROP), ("sell_prop", SELL_PROP), ("commission", COMMISSION_PER_TRANSACTION), From ce684c3c94691ef4d1d938bba6c8ce879b3f63e2 Mon Sep 17 00:00:00 2001 From: Benj Del Mundo Date: Tue, 23 Mar 2021 21:51:51 +0800 Subject: [PATCH 3/4] removed print debug messages --- python/fastquant/strategies/base.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py index 15c67272..88702200 100755 --- a/python/fastquant/strategies/base.py +++ b/python/fastquant/strategies/base.py @@ -111,9 +111,6 @@ class BaseStrategy(bt.Strategy): def __init__(self): - # print("sadsads", self.params.custom) - print(type(self.params.custom2)) - self.initial_cash = self.params.init_cash self.buy_proportion = self.params.buy_prop self.sell_proportion = self.params.sell_prop From 2f53dd7cc6bbef0f5596b47ac6df3dd29a00ae98 Mon Sep 17 00:00:00 2001 From: Benj Del Mundo Date: Sun, 28 Mar 2021 22:21:43 +0800 Subject: [PATCH 4/4] Fixed logging on Global params --- python/fastquant/strategies/base.py | 2 +- python/fastquant/utils/strategy_logging.py | 33 ++++++++++++++-------- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/python/fastquant/strategies/base.py b/python/fastquant/strategies/base.py index 88702200..1ee67d1a 100755 --- a/python/fastquant/strategies/base.py +++ b/python/fastquant/strategies/base.py @@ -193,7 +193,7 @@ def notify_order(self, order): self.logging.order_executed(order, action=self.action.upper()) # Update order history whenever an order is completed - self.logging.update_order_history(order) + self.logging.update_order_history(order, self.execution_type) elif order.status in [order.Canceled, order.Margin, order.Rejected]: self.logging.order_aborted(order) diff --git a/python/fastquant/utils/strategy_logging.py b/python/fastquant/utils/strategy_logging.py index f71902f2..8d8ae339 100755 --- a/python/fastquant/utils/strategy_logging.py +++ b/python/fastquant/utils/strategy_logging.py @@ -56,13 +56,19 @@ def log(self, txt, dt=None, level=logging.INFO): def strategy_argments(self): strategy = self.strategy - self.log("===Global level arguments===") - self.log("init_cash : {}".format(strategy.initial_cash)) - self.log("buy_prop : {}".format(strategy.buy_proportion)) - self.log("sell_prop : {}".format(strategy.sell_proportion)) - self.log("commission : {}".format(strategy.commission)) - self.log("stop_loss_percent : {}".format(strategy.stop_loss_percent)) - self.log("stop_trail_percent : {}".format(strategy.stop_trail_percent)) + + if self.transaction_logging or self.periodic_logging: + self.log("===Global level arguments===") + self.log("init_cash : {}".format(strategy.initial_cash)) + self.log("buy_prop : {}".format(strategy.buy_proportion)) + self.log("sell_prop : {}".format(strategy.sell_proportion)) + self.log("commission : {}".format(strategy.commission)) + self.log( + "stop_loss_percent : {}".format(strategy.stop_loss_percent) + ) + self.log( + "stop_trail_percent : {}".format(strategy.stop_trail_percent) + ) def order_created(self, order_type_str, text): @@ -149,10 +155,15 @@ def sell_signal(self, size, price): self.log("Sell Size: {}".format(size)) self.log("Sell Price: {}".format(price)) - def update_order_history(self, order): - self.order_history["dt"].append( - self.strategy.datas[0].datetime.date(0) - ) + def update_order_history(self, order, order_execution_type="close"): + if order_execution_type == "close": + self.order_history["dt"].append( + self.strategy.datas[0].datetime.date(0) + ) + else: # for market order, use previous day (same day as the signal was generated) + self.order_history["dt"].append( + self.strategy.datas[0].datetime.datetime(-1) + ) self.order_history["type"].append("buy" if order.isbuy() else "sell") self.order_history["price"].append(order.executed.price) self.order_history["size"].append(order.executed.size)