diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f5256 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__init__.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..499e349 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +A little package for backtesting "trading" strategies that turned out +to be useful and which I reused several times. \ No newline at end of file diff --git a/channel_bot.py b/channel_bot.py new file mode 100644 index 0000000..4650b4a --- /dev/null +++ b/channel_bot.py @@ -0,0 +1,61 @@ +from abc import ABC, abstractmethod +from model import Model +from position import Position + + +class Bot(ABC): + + def __init__(self, model: Model, nolog=False): + self.previous_obs = None + self.positions = list() + self.num_open_pos = 0 + self.nUnits_available = 0 # the number of tokens in the denominating unit that the bot can work with + self.model = model + self.nolog = nolog + + def update_num_open_pos(self): + self.num_open_pos = 0 + for p in self.positions: + if p.open: + self.num_open_pos += 1 + + @abstractmethod + def push_new_obs(self, current_price, timestamp): + """ when a new price is observed this function is called to invoke bot actions""" + # self.model.add_model_data(timestamp=timestamp, newObs=current_price) + + # update existing open positions - maybe close them + # self.update_positions(current_price=current_price, timestamp=timestamp) + + # check whether a new position needs to be opened + pass + + @abstractmethod + def open_position(self, current_price, timestamp): + """ open a new position """ + pass + + @abstractmethod + def update_positions(self, current_price, timestamp): + """ update positions in self.positions """ + # for p in self.positions: + # if p.open: + # update position returns nUnits (size of position) if a position was closed + # or None if position stays open + # nUnits_pos = p.update_position(current_price=current_price, ) + # if nUnits_pos: + # self.nUnits += nUnits_pos + + def close_all_pos(self, current_price): + for p in self.positions: + if p.open: + p.set_position_closed(current_price=current_price) + self.nUnits += p.nUnits + + def log_msg(self, msg): + """ write message to log """ + if self.nolog: + pass + else: + print(msg) + diff --git a/model.py b/model.py new file mode 100644 index 0000000..d80bc05 --- /dev/null +++ b/model.py @@ -0,0 +1,18 @@ +from abc import ABC, abstractmethod + + +class Model(ABC): + + @abstractmethod + def plot_model(self): + pass + + @abstractmethod + def update_model(self): + """ usually to be called after self.add_model_data """ + pass + + @abstractmethod + def add_model_data(self): + """ usually a daily open/high/low/close price passed in from event_loop """ + pass diff --git a/position.py b/position.py new file mode 100644 index 0000000..508c9a5 --- /dev/null +++ b/position.py @@ -0,0 +1,55 @@ +from abc import ABC, abstractmethod +import datetime + + +class Position(ABC): + + def __init__(self, fee, nolog=False): + self.nUnits = 0 # the position size in token units (in the price denominating unit) + self.fee = fee # fee for opening or closing a position (aka performing a swap) + self.open = False + self.ts_opened = None + self.ts_closed = None + self.opened_at_price = None + self.closed_at_price = None + self.performance = 0 + self.nolog = nolog + + def set_position_open(self, nUnits, current_price, current_reg_fun_vals: list): + self.open = True + self.ts_opened = datetime.datetime.now() + self.opened_at_price = current_price + self.nUnits = nUnits * (1 - self.fee) + self.log_msg(self.to_str() + + "| Position opened. nUnits: " + str(round(self.nUnits, 4)) + + " opened at price: " + str(round(self.opened_at_price, 4))) + self.set_tp_io(current_price, current_reg_fun_vals) + + def set_position_closed(self, current_price): + self.ts_closed = datetime.datetime.now() + self.closed_at_price = current_price + self.nUnits = self.nUnits * self.closed_at_price/self.opened_at_price * (1-self.fee) + self.performance = (current_price/self.opened_at_price - 1) * 100 + self.open = False + + @abstractmethod + def update_position(self): + """ check if position state needs to be changed. E.g. set to closed """ + # returns nUnits (size of position) if position is closed + pass + + def get_performance(self): + return self.performance + + def get_unrealized_profit(self, current_price): + return (current_price / self.opened_at_price - 1) * 100 + + def to_str(self): + return "POS_" + self.ts_opened.strftime("%Y%m%d%H%M%S%f") + + def log_msg(self, msg): + """ write message to log """ + if self.nolog: + pass + else: + print(msg) \ No newline at end of file