From 61905f813a7243563a52aca2659af5c0b8a059c8 Mon Sep 17 00:00:00 2001 From: Alex Reed Date: Sat, 29 Aug 2020 10:27:53 -0700 Subject: [PATCH] add indicator comparison functionality --- README.md | 2 +- pyrobot/indicators.py | 66 ++++++++++- pyrobot/robot.py | 23 ++-- pyrobot/stock_frame.py | 113 +++++++++++++----- pyrobot/trades.py | 123 ++++++++++---------- samples/trading_robot.py | 2 +- samples/trading_robot_indicators.py | 32 ++--- samples/trading_robot_indicators_compare.py | 108 +++++++++++++++++ setup.py | 2 +- 9 files changed, 345 insertions(+), 126 deletions(-) create mode 100644 samples/trading_robot_indicators_compare.py diff --git a/README.md b/README.md index cc2b1c7..cc000c9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ ## Overview -Current Version: **0.1.0** +Current Version: **0.1.1** A trading robot written in Python that can run automated strategies using a technical analysis. The robot is designed to mimic a few common scenarios: diff --git a/pyrobot/indicators.py b/pyrobot/indicators.py index 43aa569..002d1d2 100644 --- a/pyrobot/indicators.py +++ b/pyrobot/indicators.py @@ -46,6 +46,9 @@ def __init__(self, price_data_frame: StockFrame) -> None: self._current_indicators = {} self._indicator_signals = {} self._frame = self._stock_frame.frame + + self._indicators_comp_key = [] + self._indicators_key = [] if self.is_multi_index: True @@ -66,11 +69,10 @@ def get_indicator_signal(self, indicator: Optional[str]= None) -> Dict: return self._indicator_signals[indicator] else: return self._indicator_signals - def set_indicator_signal(self, indicator: str, buy: float, sell: float, condition_buy: Any, condition_sell: Any, - buy_max: float = None, sell_max: float = None, condition_buy_max: Any = None, condition_sell_max: Any = None) -> None: - """Return the raw Pandas Dataframe Object. + buy_max: float = None, sell_max: float = None, condition_buy_max: Any = None, condition_sell_max: Any = None) -> None: + """Used to set an indicator where one indicator crosses above or below a certain numerical threshold. Arguments: ---- @@ -102,7 +104,8 @@ def set_indicator_signal(self, indicator: str, buy: float, sell: float, conditio # Add the key if it doesn't exist. if indicator not in self._indicator_signals: self._indicator_signals[indicator] = {} - + self._indicators_key.append(indicator) + # Add the signals. self._indicator_signals[indicator]['buy'] = buy self._indicator_signals[indicator]['sell'] = sell @@ -115,6 +118,52 @@ def set_indicator_signal(self, indicator: str, buy: float, sell: float, conditio self._indicator_signals[indicator]['buy_operator_max'] = condition_buy_max self._indicator_signals[indicator]['sell_operator_max'] = condition_sell_max + def set_indicator_signal_compare(self, indicator_1: str, indicator_2: str, condition_buy: Any, condition_sell: Any) -> None: + """Used to set an indicator where one indicator is compared to another indicator. + + Overview: + ---- + Some trading strategies depend on comparing one indicator to another indicator. + For example, the Simple Moving Average crossing above or below the Exponential + Moving Average. This will be used to help build those strategies that depend + on this type of structure. + + Arguments: + ---- + indicator_1 {str} -- The first indicator key, for example `ema` or `sma`. + + indicator_2 {str} -- The second indicator key, this is the indicator we will compare to. For example, + is the `sma` greater than the `ema`. + + condition_buy {str} -- The operator which is used to evaluate the `buy` condition. For example, `">"` would + represent greater than or from the `operator` module it would represent `operator.gt`. + + condition_sell {str} -- The operator which is used to evaluate the `sell` condition. For example, `">"` would + represent greater than or from the `operator` module it would represent `operator.gt`. + """ + + # Define the key. + key = "{ind_1}_comp_{ind_2}".format( + ind_1=indicator_1, + ind_2=indicator_2 + ) + + # Add the key if it doesn't exist. + if key not in self._indicator_signals: + self._indicator_signals[key] = {} + self._indicators_comp_key.append(key) + + # Grab the dictionary. + indicator_dict = self._indicator_signals[key] + + # Add the signals. + indicator_dict['type'] = 'comparison' + indicator_dict['indicator_1'] = indicator_1 + indicator_dict['indicator_2'] = indicator_2 + indicator_dict['buy_operator'] = condition_buy + indicator_dict['sell_operator'] = condition_sell + + @property def price_data_frame(self) -> pd.DataFrame: """Return the raw Pandas Dataframe Object. @@ -1010,6 +1059,9 @@ def refresh(self): # Update the function. indicator_function(**indicator_argument) + + def grab_comparison_indicator(self) -> dict: + pass def check_signals(self) -> Union[pd.DataFrame, None]: """Checks to see if any signals have been generated. @@ -1020,7 +1072,11 @@ def check_signals(self) -> Union[pd.DataFrame, None]: is returned otherwise nothing is returned. """ - signals_df = self._stock_frame._check_signals(indicators=self._indicator_signals) + signals_df = self._stock_frame._check_signals( + indicators=self._indicator_signals, + indciators_comp_key=self._indicators_comp_key, + indicators_key=self._indicators_key + ) return signals_df diff --git a/pyrobot/robot.py b/pyrobot/robot.py index 510b480..8a6d761 100644 --- a/pyrobot/robot.py +++ b/pyrobot/robot.py @@ -3,7 +3,6 @@ import pprint import pathlib import pandas as pd -import pkg_resources from datetime import time from datetime import datetime @@ -19,15 +18,11 @@ from pyrobot.portfolio import Portfolio from pyrobot.stock_frame import StockFrame -current_td_version = pkg_resources.get_distribution('td-ameritrade-python-api').version - from td.client import TDClient +from td.utils import TDUtilities -if current_td_version == '0.3.0': - from td.utils import TDUtilities - milliseconds_since_epoch = TDUtilities().milliseconds_since_epoch -else: - from td.utils import milliseconds_since_epoch +# We are going to be doing some timestamp conversions. +milliseconds_since_epoch = TDUtilities().milliseconds_since_epoch class PyRobot(): @@ -557,8 +552,8 @@ def get_latest_bar(self) -> List[dict]: bar_type = self._bar_type # Define the start and end date. - start_date = datetime.today() - end_date = start_date - timedelta(minutes=bar_size * 15) + end_date = datetime.today() + start_date = end_date - timedelta(days=1) start = str(milliseconds_since_epoch(dt_object=start_date)) end = str(milliseconds_since_epoch(dt_object=end_date)) @@ -636,11 +631,11 @@ def wait_till_next_bar(self, last_bar_timestamp: pd.DatetimeIndex) -> None: print("-"*80) print("Curr Time: {time_curr}".format( time_curr=curr_bar_time.strftime("%Y-%m-%d %H:%M:%S") - ) + ) ) print("Next Time: {time_next}".format( time_next=next_bar_time.strftime("%Y-%m-%d %H:%M:%S") - ) + ) ) print("Sleep Time: {seconds}".format(seconds=time_to_wait_now)) print("-"*80) @@ -691,8 +686,8 @@ def execute_signals(self, signals: List[pd.Series], trades_to_execute: dict) -> } >>> signals = indicator_client.check_signals() >>> trading_robot.execute_signals( - signals=signals, - trades_to_execute=trades_dict + signals=signals, + trades_to_execute=trades_dict ) """ diff --git a/pyrobot/stock_frame.py b/pyrobot/stock_frame.py index 0f578cb..ff5b48e 100644 --- a/pyrobot/stock_frame.py +++ b/pyrobot/stock_frame.py @@ -23,8 +23,8 @@ def __init__(self, data: List[Dict]) -> None: ---- data {List[Dict]} -- The data to convert to a frame. Normally, this is returned from the historical prices endpoint. - """ - + """ + self._data = data self._frame: pd.DataFrame = self.create_frame() self._symbol_groups = None @@ -37,7 +37,7 @@ def frame(self) -> pd.DataFrame: Returns: ---- pd.DataFrame -- A pandas data frame with the price data. - """ + """ return self._frame @property @@ -53,9 +53,9 @@ def symbol_groups(self) -> DataFrameGroupBy: Returns: ---- {DataFrameGroupBy} -- A `pandas.core.groupby.GroupBy` object with each symbol. - """ + """ - # Group by Symbol. + # Group by Symbol. self._symbol_groups: DataFrameGroupBy = self._frame.groupby( by='symbol', as_index=False, @@ -80,10 +80,10 @@ def symbol_rolling_groups(self, size: int) -> RollingGroupby: if not self._symbol_groups: self.symbol_groups - self._symbol_rolling_groups: RollingGroupby = self._symbol_groups.rolling(size) - - return self._symbol_rolling_groups + self._symbol_rolling_groups: RollingGroupby = self._symbol_groups.rolling( + size) + return self._symbol_rolling_groups def create_frame(self) -> pd.DataFrame: """Creates a new data frame with the data passed through. @@ -91,8 +91,8 @@ def create_frame(self) -> pd.DataFrame: Returns: ---- {pd.DataFrame} -- A pandas dataframe. - """ - + """ + # Make a data frame. price_df = pd.DataFrame(data=self._data) price_df = self._parse_datetime_column(price_df=price_df) @@ -111,12 +111,16 @@ def _parse_datetime_column(self, price_df: pd.DataFrame) -> pd.DataFrame: Returns: ---- {pd.DataFrame} -- A pandas dataframe. - """ + """ - price_df['datetime'] = pd.to_datetime(price_df['datetime'], unit='ms', origin='unix') + price_df['datetime'] = pd.to_datetime( + price_df['datetime'], + unit='ms', + origin='unix' + ) return price_df - + def _set_multi_index(self, price_df: pd.DataFrame) -> pd.DataFrame: """Converts the dataframe to a multi-index data frame. @@ -127,9 +131,9 @@ def _set_multi_index(self, price_df: pd.DataFrame) -> pd.DataFrame: Returns: ---- pd.DataFrame -- A pandas dataframe. - """ + """ - price_df = price_df.set_index(keys=['symbol','datetime']) + price_df = price_df.set_index(keys=['symbol', 'datetime']) return price_df @@ -157,7 +161,7 @@ def add_rows(self, data: Dict) -> None: } >>> # Add to the Stock Frame. >>> stock_frame.add_rows(data=fake_data) - """ + """ column_names = ['open', 'close', 'high', 'low', 'volume'] @@ -183,8 +187,8 @@ def add_rows(self, data: Dict) -> None: ] # Create a new row. - new_row = pd.Series(data=row_values) - + new_row = pd.Series(data=row_values) + # Add the row. self.frame.loc[row_id, column_names] = new_row.values @@ -192,7 +196,7 @@ def add_rows(self, data: Dict) -> None: def do_indicator_exist(self, column_names: List[str]) -> bool: """Checks to see if the indicator columns specified exist. - + Overview: ---- The user can add multiple indicator columns to their StockFrame object @@ -217,10 +221,11 @@ def do_indicator_exist(self, column_names: List[str]) -> bool: return True else: raise KeyError("The following indicator columns are missing from the StockFrame: {missing_columns}".format( - missing_columns=set(column_names).difference(self._frame.columns) - )) + missing_columns=set(column_names).difference( + self._frame.columns) + )) - def _check_signals(self, indicators: dict) -> Union[pd.DataFrame, None]: + def _check_signals(self, indicators: dict, indciators_comp_key: List[str], indicators_key: List[str]) -> Union[pd.DataFrame, None]: """Returns the last row of the StockFrame if conditions are met. Overview: @@ -238,21 +243,28 @@ def _check_signals(self, indicators: dict) -> Union[pd.DataFrame, None]: indicators {dict} -- A dictionary containing all the indicators to be checked along with their buy and sell criteria. + indicators_comp_key List[str] -- A list of the indicators where we are comparing + one indicator to another indicator. + + indicators_key List[str] -- A list of the indicators where we are comparing + one indicator to a numerical value. + Returns: ---- {Union[pd.DataFrame, None]} -- If signals are generated then, a pandas.DataFrame object will be returned. If no signals are found then nothing will be returned. - """ + """ # Grab the last rows. last_rows = self._symbol_groups.tail(1) + # Define a list of conditions. conditions = [] # Check to see if all the columns exist. - if self.do_indicator_exist(column_names=indicators.keys()): + if self.do_indicator_exist(column_names=indicators_key): - for indicator in indicators: + for indicator in indicators_key: column = last_rows[indicator] @@ -262,13 +274,54 @@ def _check_signals(self, indicators: dict) -> Union[pd.DataFrame, None]: buy_condition_operator = indicators[indicator]['buy_operator'] sell_condition_operator = indicators[indicator]['sell_operator'] - condition_1: pd.Series = buy_condition_operator(column, buy_condition_target) - condition_2: pd.Series = sell_condition_operator(column, sell_condition_target) + condition_1: pd.Series = buy_condition_operator( + column, buy_condition_target + ) + condition_2: pd.Series = sell_condition_operator( + column, sell_condition_target + ) - condition_1 = condition_1.where(lambda x : x == True).dropna() - condition_2 = condition_2.where(lambda x : x == True).dropna() + condition_1 = condition_1.where(lambda x: x == True).dropna() + condition_2 = condition_2.where(lambda x: x == True).dropna() conditions.append(('buys', condition_1)) conditions.append(('sells', condition_2)) + + check_indicators = [] + + for indicator in indciators_comp_key: + parts = indicator.split('_comp_') + check_indicators += parts + + if self.do_indicator_exist(column_names=check_indicators): + + for indicator in indciators_comp_key: + + # Split the indicators. + parts = indicator.split('_comp_') + + # Grab the indicators that need to be compared. + indicator_1 = last_rows[parts[0]] + indicator_2 = last_rows[parts[1]] + + if indicators[indicator]['buy_operator']: + buy_condition_operator = indicators[indicator]['buy_operator'] + + condition_1: pd.Series = buy_condition_operator( + indicator_1, indicator_2 + ) + + condition_1 = condition_1.where(lambda x: x == True).dropna() + conditions.append(('buys', condition_1)) + + if indicators[indicator]['sell_operator']: + sell_condition_operator = indicators[indicator]['sell_operator'] + + condition_2: pd.Series = sell_condition_operator( + indicator_1, indicator_2 + ) + + condition_2 = condition_2.where(lambda x: x == True).dropna() + conditions.append(('sells', condition_2)) - return conditions \ No newline at end of file + return conditions diff --git a/pyrobot/trades.py b/pyrobot/trades.py index 36d4f7c..af55a8a 100644 --- a/pyrobot/trades.py +++ b/pyrobot/trades.py @@ -4,6 +4,7 @@ from typing import Union from typing import Optional + class Trade(): """ @@ -18,8 +19,8 @@ class Trade(): """ def __init__(self): - """Initalizes a new order.""" - + """Initalizes a new order.""" + self.order = {} self.trade_id = "" @@ -38,7 +39,7 @@ def new_trade(self, trade_id: str, order_type: str, side: str, enter_or_exit: st A trade object is a template that can be used to help build complex trades that normally are prone to errors when writing the JSON. Additionally, it will help the process of storing trades easier. - + Arguments: ---- order_type {str} -- The type of order you would like to create. Can be @@ -50,7 +51,7 @@ def new_trade(self, trade_id: str, order_type: str, side: str, enter_or_exit: st enter_or_exit {str} -- Specifices whether this trade will enter a new position or exit an existing position. If used to enter then specify, 'enter'. If used to exit a trade specify, 'exit'. - + Returns: ---- {dict} -- [description] @@ -59,21 +60,21 @@ def new_trade(self, trade_id: str, order_type: str, side: str, enter_or_exit: st self.trade_id = trade_id self.order_types = { - 'mkt':'MARKET', - 'lmt':'LIMIT', - 'stop':'STOP', - 'stop_lmt':'STOP_LIMIT', - 'trailing_stop':'TRAILING_STOP' + 'mkt': 'MARKET', + 'lmt': 'LIMIT', + 'stop': 'STOP', + 'stop_lmt': 'STOP_LIMIT', + 'trailing_stop': 'TRAILING_STOP' } self.order_instructions = { - 'enter':{ - 'long':'BUY', - 'short':'SELL_SHORT' + 'enter': { + 'long': 'BUY', + 'short': 'SELL_SHORT' }, - 'exit':{ - 'long':'SELL', - 'short':'BUY_TO_COVER' + 'exit': { + 'long': 'SELL', + 'short': 'BUY_TO_COVER' } } @@ -150,19 +151,19 @@ def new_trade(self, trade_id: str, order_type: str, side: str, enter_or_exit: st def instrument(self, symbol: str, quantity: int, asset_type: str, sub_asset_type: str = None, order_leg_id: int = 0) -> dict: """Adds an instrument to a trade. - + Arguments: ---- symbol {str} -- The instrument ticker symbol. quantity {int} -- The quantity of shares to be purchased. - + asset_type {str} -- The instrument asset type. For example, `EQUITY`. - + Keyword Arguments: ---- sub_asset_type {str} -- The instrument sub-asset type, not always needed. For example, `ETF`. (default: {None}) - + Returns: ---- {dict} -- A dictionary with the instrument. @@ -186,7 +187,7 @@ def add_option_instrument(self, symbol: str, quantity: int, order_leg_id: int = Args: ---- symbol (str): The option symbol to be added. - + quantity (int): The number of option contracts to purchase or sell. order_leg_id (int, optional): The position of the instrument within the @@ -205,12 +206,12 @@ def add_option_instrument(self, symbol: str, quantity: int, order_leg_id: int = ) leg = self.order['orderLegCollection'][order_leg_id] - + return leg def good_till_cancel(self, cancel_time: datetime) -> None: """Converts an order to a `Good Till Cancel` order. - + Arguments: ---- cancel_time {datetime.datetime} -- A datetime object representing the @@ -220,14 +221,14 @@ def good_till_cancel(self, cancel_time: datetime) -> None: self.order['duration'] = 'GOOD_TILL_CANCEL' self.order['cancelTime'] = cancel_time.isoformat() - def modify_side(self, side: Optional[str] , leg_id: int = 0) -> None: + def modify_side(self, side: Optional[str], leg_id: int = 0) -> None: """Modifies the Side the order takes. Arguments: ---- side {str} -- The side to be set. Can be one of the following: `['buy', 'sell', 'sell_short', 'buy_to_cover']`. - + Keyword Arguments: ---- leg_id {int} -- The leg you want to adjust. (default: {0}) @@ -243,14 +244,14 @@ def modify_side(self, side: Optional[str] , leg_id: int = 0) -> None: raise ValueError( "The side you have specified is not valid. Please choose a valid side: ['buy', 'sell', 'sell_short', 'buy_to_cover','sell_to_close', 'buy_to_open']" ) - + # Set the Order. if side: self.order['orderLegCollection'][leg_id]['instruction'] = side.upper() else: self.order['orderLegCollection'][leg_id]['instruction'] = self.order_instructions[self.enter_or_exit][self.side_opposite] - def add_box_range(self, profit_size: float = 0.00, percentage: bool = False, stop_limit: bool = False): + def add_box_range(self, profit_size: float = 0.00, percentage: bool = False, stop_limit: bool = False): """Adds a Stop Loss(or Stop-Limit order), and a limit Order Arguments: @@ -263,8 +264,8 @@ def add_box_range(self, profit_size: float = 0.00, percentage: bool = False, sto Keyword Arguments: ---- stop_limit {bool} -- If `True` makes the stop-loss a stop-limit. (default: {False}) - """ - + """ + if not self._triggered_added: self._convert_to_trigger() @@ -295,19 +296,21 @@ def add_stop_loss(self, stop_size: float, percentage: bool = False) -> bool: if not self._triggered_added: self._convert_to_trigger() - + if self.order_type == 'mkt': # Have to make a call to Get Quotes. price = self.price elif self.order_type == 'lmt': price = self.price - + if percentage: adjustment = 1.0 - stop_size - new_price = self._calculate_new_price(price=price, adjustment=adjustment, percentage=True) + new_price = self._calculate_new_price( + price=price, adjustment=adjustment, percentage=True) else: adjustment = -stop_size - new_price = self._calculate_new_price(price=price, adjustment=adjustment, percentage=False) + new_price = self._calculate_new_price( + price=price, adjustment=adjustment, percentage=False) stop_loss_order = { "orderType": "STOP", @@ -359,15 +362,15 @@ def add_stop_limit(self, stop_size: float, limit_size: float, stop_percentage: b # Check to see if there is a trigger. if not self._triggered_added: self._convert_to_trigger() - + # Grab the price. if self.order_type == 'mkt': # Have to make a call to Get Quotes. price = self.price - + elif self.order_type == 'lmt': price = self.price - + # Calculate the Stop Price. if stop_percentage: adjustment = 1.0 - stop_size @@ -405,7 +408,7 @@ def add_stop_limit(self, stop_size: float, limit_size: float, stop_percentage: b "orderType": "STOP_LIMIT", "session": "NORMAL", "duration": "DAY", - "price":limit_price, + "price": limit_price, "stopPrice": stop_price, "orderStrategyType": "SINGLE", "orderLegCollection": [ @@ -423,7 +426,7 @@ def add_stop_limit(self, stop_size: float, limit_size: float, stop_percentage: b self.stop_limit_order = stop_limit_order self.order['childOrderStrategies'].append(self.stop_limit_order) - return True + return True def _calculate_new_price(self, price: float, adjustment: float, percentage: bool) -> float: """Calculates an adjusted price given an old price. @@ -431,9 +434,9 @@ def _calculate_new_price(self, price: float, adjustment: float, percentage: bool Arguments: ---- price {float} -- The original price. - + adjustment {float} -- The adjustment to be made to the new price. - + percentage {bool} -- Specifies whether the adjustment is a percentage adjustment `True` or an absolute dollar adjustment `False`. @@ -449,12 +452,12 @@ def _calculate_new_price(self, price: float, adjustment: float, percentage: bool # For orders below $1.00, can only have 4 decimal places. if new_price < 1: - new_price = round(new_price,4) + new_price = round(new_price, 4) # For orders above $1.00, can only have 2 decimal places. else: new_price = round(new_price, 2) - + return new_price def add_take_profit(self, profit_size: float, percentage: bool = False) -> bool: @@ -473,8 +476,8 @@ def add_take_profit(self, profit_size: float, percentage: bool = False) -> bool: Returns: ---- {bool} -- `True` if the order was added. - """ - + """ + # Check to see if we have a trigger order. if not self._triggered_added: self._convert_to_trigger() @@ -485,7 +488,7 @@ def add_take_profit(self, profit_size: float, percentage: bool = False) -> bool: price = self.price elif self.order_type == 'lmt': price = self.price - + # Calculate the new price. if percentage: adjustment = 1.0 + profit_size @@ -535,8 +538,8 @@ def _convert_to_trigger(self): Trigger orders can be used to have a stop loss orders, or take profit orders placed right after the main order has been placed. This helps protect the order when possible and take profit when thresholds are reached. - """ - + """ + # Only convert to a trigger order, if it already isn't one. if self.order and self._triggered_added == False: self.order['orderStrategyType'] = 'TRIGGER' @@ -560,7 +563,7 @@ def modify_session(self, session: str) -> None: 2. 'pm' - This is for post-market hours. 3. 'normal' - This is for normal market hours. 4. 'seamless' - This makes the order active all of the sessions. - + Arguments: ---- session {str} -- The session you want the order to be active. Possible values @@ -570,8 +573,9 @@ def modify_session(self, session: str) -> None: if session in ['am', 'pm', 'normal', 'seamless']: self.order['session'] = session.upper() else: - raise ValueError('Invalid session, choose either am, pm, normal, or seamless') - + raise ValueError( + 'Invalid session, choose either am, pm, normal, or seamless') + @property def order_response(self) -> dict: """Returns the order response from submitting an order. @@ -600,11 +604,11 @@ def _generate_order_id(self) -> str: Returns: ---- {str} -- The order ID that was generated. - """ + """ # If we have an order, then generate it. if self.order: - + order_id = "{symbol}_{side}_{enter_or_exit}_{timestamp}" order_id = order_id.format( @@ -621,7 +625,7 @@ def _generate_order_id(self) -> str: def add_leg(self, order_leg_id: int, symbol: str, quantity: int, asset_type: str, sub_asset_type: str = None) -> List[dict]: """Adds an instrument to a trade. - + Arguments: ---- order_leg_id {int} -- The position you want the new leg to be in the leg collection. @@ -629,13 +633,13 @@ def add_leg(self, order_leg_id: int, symbol: str, quantity: int, asset_type: str symbol {str} -- The instrument ticker symbol. quantity {int} -- The quantity of shares to be purchased. - + asset_type {str} -- The instrument asset type. For example, `EQUITY`. - + Keyword Arguments: ---- sub_asset_type {str} -- The instrument sub-asset type, not always needed. For example, `ETF`. (default: {None}) - + Returns: ---- {dict} -- The order's order leg collection. @@ -650,7 +654,6 @@ def add_leg(self, order_leg_id: int, symbol: str, quantity: int, asset_type: str if sub_asset_type: leg['instrument']['subAssetType'] = sub_asset_type - # If 0, call instrument. if order_leg_id == 0: self.instrument( @@ -674,7 +677,7 @@ def number_of_legs(self) -> int: Returns: ---- int: The count of legs in the collection. - """ + """ return len(self.order['orderLegCollection']) @@ -684,7 +687,7 @@ def modify_price(self, new_price: float, price_type: str) -> None: Arguments: ---- new_price (float): The new price to be set. - + price_type (str): The type of price that should be modified. Can be one of the following: [ 'price', @@ -717,7 +720,7 @@ def is_stop_order(self) -> bool: Returns: ---- bool: `True` if the order is a Stop order, `False` otherwise. - """ + """ if self.order_type != 'stop': return False @@ -750,4 +753,4 @@ def is_limit_order(self) -> bool: if self.order_type != 'lmt': return False else: - return True \ No newline at end of file + return True diff --git a/samples/trading_robot.py b/samples/trading_robot.py index d19db72..6f6f83c 100644 --- a/samples/trading_robot.py +++ b/samples/trading_robot.py @@ -215,7 +215,7 @@ # Execute Trades. trading_robot.execute_signals( - signals=signals, + signals=signals, trades_to_execute=trades_dict ) diff --git a/samples/trading_robot_indicators.py b/samples/trading_robot_indicators.py index f910abf..aaa4739 100644 --- a/samples/trading_robot_indicators.py +++ b/samples/trading_robot_indicators.py @@ -49,7 +49,9 @@ ) # Convert data to a Data Frame. -stock_frame = trading_robot.create_stock_frame(data=historical_prices['aggregated']) +stock_frame = trading_robot.create_stock_frame( + data=historical_prices['aggregated'] +) # We can also add the stock frame to the Portfolio object. trading_robot.portfolio.stock_frame = stock_frame @@ -87,17 +89,17 @@ # Add the Mass Index. indicator_client.mass_index(period=9) -# Add the K-Oscillator -indicator_client.kst_oscillator( - r1=1, - r2=2, - r3=3, - r4=4, - n1=1, - n2=2, - n3=3, - n4=4 -) +# # Add the K-Oscillator +# indicator_client.kst_oscillator( +# r1=1, +# r2=2, +# r3=3, +# r4=4, +# n1=1, +# n2=2, +# n3=3, +# n4=4 +# ) while True: @@ -121,7 +123,9 @@ signals = indicator_client.check_signals() # Grab the last bar. - last_bar_timestamp = trading_robot.stock_frame.frame.tail(1).index.get_level_values(1) + last_bar_timestamp = trading_robot.stock_frame.frame.tail( + n=1 + ).index.get_level_values(1) # Wait till the next bar. - trading_robot.wait_till_next_bar(last_bar_timestamp=last_bar_timestamp) \ No newline at end of file + trading_robot.wait_till_next_bar(last_bar_timestamp=last_bar_timestamp) diff --git a/samples/trading_robot_indicators_compare.py b/samples/trading_robot_indicators_compare.py new file mode 100644 index 0000000..bf7e0ac --- /dev/null +++ b/samples/trading_robot_indicators_compare.py @@ -0,0 +1,108 @@ +import pprint +import operator + +from datetime import datetime +from datetime import timedelta +from configparser import ConfigParser + +from pyrobot.robot import PyRobot +from pyrobot.indicators import Indicators + +# Grab configuration values. +config = ConfigParser() +config.read('configs/config.ini') + +CLIENT_ID = config.get('main', 'CLIENT_ID') +REDIRECT_URI = config.get('main', 'REDIRECT_URI') +CREDENTIALS_PATH = config.get('main', 'JSON_PATH') +ACCOUNT_NUMBER = config.get('main', 'ACCOUNT_NUMBER') + +# Initalize the robot. +trading_robot = PyRobot( + client_id=CLIENT_ID, + redirect_uri=REDIRECT_URI, + credentials_path=CREDENTIALS_PATH, + paper_trading=True +) + +# Create a Portfolio +trading_robot_portfolio = trading_robot.create_portfolio() + +# Add a single position +trading_robot_portfolio.add_position( + symbol='MSFT', + quantity=10, + purchase_price=10, + asset_type='equity', + purchase_date='2020-04-01' +) + +# Grab historical prices, first define the start date and end date. +start_date = datetime.today() +end_date = start_date - timedelta(days=30) + +# Grab the historical prices. +historical_prices = trading_robot.grab_historical_prices( + start=end_date, + end=start_date, + bar_size=1, + bar_type='minute' +) + +# Convert data to a Data Frame. +stock_frame = trading_robot.create_stock_frame( + data=historical_prices['aggregated'] +) + +# We can also add the stock frame to the Portfolio object. +trading_robot.portfolio.stock_frame = stock_frame + +# Additionally the historical prices can be set as well. +trading_robot.portfolio.historical_prices = historical_prices + +# Create an indicator Object. +indicator_client = Indicators(price_data_frame=stock_frame) + +# Add the RSI Indicator. +indicator_client.rsi(period=14) + +# Add the 200 day simple moving average. +indicator_client.sma(period=200) + +# Add the 50 day exponentials moving average. +indicator_client.ema(period=50) + +# Add the Bollinger Bands. +indicator_client.bollinger_bands(period=20) + +# Add the Rate of Change. +indicator_client.rate_of_change(period=1) + +# Add the Average True Range. +indicator_client.average_true_range(period=14) + +# Add the Stochastic Oscillator. +indicator_client.stochastic_oscillator() + +# Add the MACD. +indicator_client.macd(fast_period=12, slow_period=26) + +# Add the Mass Index. +indicator_client.mass_index(period=9) + +# Add a signal to check for. +indicator_client.set_indicator_signal_compare( + indicator_1='sma', + indicator_2='ema', + condition_buy=operator.ge, + condition_sell=None +) + +# Check for signals. +signals = indicator_client.check_signals() + +# Print the Head. +print(trading_robot.stock_frame.frame.head()) + +# Print the Signals. +pprint.pprint(signals) diff --git a/setup.py b/setup.py index a5a987a..d4ed6fc 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ author_email='coding.sigma@gmail.com', - version='0.1.0', + version='0.1.1', description='A trading robot built for Python that uses the TD Ameritrade API.',