import copy import math class opened_trade(): """An object representing an open trade.""" def __init__(self, type, date): """Initate the trade. :param type: Type of trade :type type: float :param date: When the trade was opened :type date: datetime :return: A trade :rtype: trade """ self.type = type self.date = date def __str__(self): return "{0}\n{1}".format(self.type, self.date) class closed_trade(opened_trade): """An object representing a closed trade.""" def __init__(self, type, date, shares, entry, exit): """Initate the trade. :param type: Type of trade :type type: float :param date: When the trade was closed :type date: datetime :param shares: Number of shares :type shares: float :param entry: Entry price :type entry: float :param exit: Exit price :type exit: float :return: A trade :rtype: trade """ super().__init__(type, date) self.shares = float(shares) self.entry = float(entry) self.exit = float(exit) def __str__(self): return "{0}\n{1}\n{2}\n{3}\n{4}".format(self.type, self.date, self.shares, self.entry, self.exit) class position: """A parent object representing a position.""" def __init__(self, no, entry_price, shares, exit_price, stop_loss): """Open the position. :param no: A unique position id number :type no: float :param entry_price: Entry price at which shares are longed/shorted :type entry_price: float :param shares: Number of shares to long/short :type shares: float :param exit_price: Price at which to take profit :type exit_price: float :param stop_loss: Price at which to cut losses :type stop_loss: float :return: A position :rtype: position """ self.no = no self.type = "None" self.entry_price = float(entry_price) self.shares = float(shares) self.exit_price = float(exit_price) self.stop_loss = float(stop_loss) def show(self): print("No. {0}".format(self.no)) print("Type: {0}".format(self.type)) print("Entry: {0}".format(self.entry_price)) print("Shares: {0}".format(self.shares)) print("Exit: {0}".format(self.exit_price)) print("Stop: {0}\n".format(self.stop_loss)) class long_position(position): """A child object representing a long position.""" def __init__(self, no, entry_price, shares, exit_price=math.inf, stop_loss=0): """Open the position. :param no: A unique position id number :type no: float :param entry_price: Entry price at which shares are longed :type entry_price: float :param shares: Number of shares to long :type shares: float :param exit_price: Price at which to take profit :type exit_price: float :param stop_loss: Price at which to cut losses :type stop_loss: float :return: A long position :rtype: long_position """ if exit_price is False: exit_price = math.inf if stop_loss is False: stop_loss = 0 super().__init__(no, entry_price, shares, exit_price, stop_loss) self.type = 'long' def close(self, percent, current_price): """Close the position. :param percent: Percent of position size to close :type percent: float :param current_price: Closing price :type current_price: float :return: Amount of capital gained from closing position :rtype: float """ shares = self.shares self.shares *= 1.0 - percent return shares * percent * current_price def stop_hit(self, current_price): if current_price <= self.stop_loss: return(True) class short_position(position): """A child object representing a short position.""" def __init__(self, no, entry_price, shares, exit_price=0, stop_loss=math.inf): """Open the position. :param no: A unique position id number :type no: int :param entry_price: Entry price at which shares are shorted :type entry_price: float :param shares: Number of shares to short :type shares: float :param exit_price: Price at which to take profit :type exit_price: float :param stop_loss: Price at which to cut losses :type stop_loss: float :return: A short position :rtype: short_position """ if exit_price is False: exit_price = 0 if stop_loss is False: stop_loss = math.inf super().__init__(no, entry_price, shares, exit_price, stop_loss) self.type = 'short' def close(self, percent, current_price): """Close the position. :param percent: Percent of position size to close :type percent: float :param current_price: Closing price :type current_price: float :return: Amount of capital gained from closing position :rtype: float """ entry = self.shares * percent * self.entry_price exit = self.shares * percent * current_price self.shares *= 1.0 - percent if entry - exit + entry <= 0: return 0 else: return entry - exit + entry def stop_hit(self, current_price): if current_price >= self.stop_loss: return(True) class account(): """An object representing an exchange account.""" def __init__(self, initial_capital): """Initiate an account. :param initial_capital: Starting capital to fund account :type initial_capital: float :return: An account object :rtype: account """ self.initial_capital = float(initial_capital) self.buying_power = float(initial_capital) self.no = 0 self.date = None self.equity = [] self.positions = [] self.opened_trades = [] self.closed_trades = [] def enter_position(self, type, entry_capital, entry_price, exit_price=False, stop_loss=False, commission=0): """Open a position. :param type: Type of position e.g. ("long, short") :type type: float :param entry_price: Amount of capital invested into position :type entry_price: float :param entry_price: Entry price at which shares are longed/shorted :type entry_price: float :param exit_price: Price at which to take profit :type exit_price: float :param stop_loss: Price at which to cut losses :type stop_loss: float :param commision: Percent commission subtracted from position size :type commision: float """ entry_capital = float(entry_capital) if entry_capital < 0: raise ValueError("Error: Entry capital must be positive") elif entry_price < 0: raise ValueError("Error: Entry price cannot be negative.") elif self.buying_power < entry_capital: raise ValueError("Error: Not enough buying power to enter position") else: self.buying_power -= entry_capital if commission > 0: shares = entry_capital / (entry_price + commission * entry_price) else: shares = entry_capital / entry_price if type == 'long': self.positions.append(long_position(self.no, entry_price, shares, exit_price, stop_loss)) elif type == 'short': self.positions.append(short_position(self.no, entry_price, shares, exit_price, stop_loss)) else: raise TypeError("Error: Invalid position type.") self.opened_trades.append(opened_trade(type, self.date)) self.no += 1 def close_position(self, position, percent, current_price, commission=0): """Close a position. :param position: Position id number :type position: int :param percent: Percent of position size to close :type percent: float :param current_price: Price at which position is closed :type current_price: float :param commision: Percent commission subtracted from capital returned :type commision: float """ if percent > 1 or percent < 0: raise ValueError("Error: Percent must range between 0-1.") elif current_price < 0: raise ValueError("Error: Current price cannot be negative.") else: self.closed_trades.append(closed_trade(position.type, self.date, position.shares * percent, position.entry_price, current_price)) if commission > 0: closing_position_price = position.close(percent, current_price) self.buying_power += (closing_position_price - closing_position_price * commission) else: self.buying_power += position.close(percent, current_price) def purge_positions(self): """Delete all empty positions.""" self.positions = [p for p in self.positions if p.shares > 0] def show_positions(self): """Show all account positions.""" for p in self.positions: p.show() def total_value(self, current_price): """Calculate total value of account :param current_price: Price used to value open position sizes :type current_price: float :return: Total value of acocunt :rtype: float """ temporary = copy.deepcopy(self) for position in temporary.positions: temporary.close_position(position, 1.0, current_price) return temporary.buying_power