diff --git a/backtest_lib/backtest.py b/backtest_lib/backtest.py index c90bee5..f8f8108 100644 --- a/backtest_lib/backtest.py +++ b/backtest_lib/backtest.py @@ -1,32 +1,31 @@ -import pandas +import pandas as pd import time from sql_lib import sql_interaction trade_object = {} -# Function to backtest a strategy def backtest(valid_trades_dataframe, time_orders_valid, tick_data_table_name, trade_table_name, project_settings, strategy, symbol, comment, balance_table, valid_trades_table): + """backtest a strategy""" print("Starting Backtest script") # Make sure that SettingWithCopyWarning suppressed - pandas.options.mode.chained_assignment = None - # Add status of pending to orders + pd.options.mode.chained_assignment = None + valid_trades_dataframe['status'] = "pending" valid_trades_dataframe['time_valid'] = valid_trades_dataframe['time'] + time_orders_valid # Save valid trades to postgres sql_interaction.save_dataframe(valid_trades_dataframe, valid_trades_table, project_settings) - # Setup open_orders - open_orders = pandas.DataFrame() - # Setup open positions - open_buy_positions = pandas.DataFrame() - open_sell_positions = pandas.DataFrame() - # Setup closed + + open_orders = pd.DataFrame() + open_buy_positions = pd.DataFrame() + open_sell_positions = pd.DataFrame() + print("Data retrieved, analysis starting") # Query SQL in chunks - # Create connection conn = sql_interaction.postgres_connect(project_settings) # Set up the trading object + # trade_object = {"symbol": symbol, ...} ? trade_object["backtest_settings"] = project_settings["backtest_settings"] trade_object["current_available_balance"] = trade_object["backtest_settings"]["test_balance"] trade_object["current_equity"] = 0 @@ -82,6 +81,7 @@ def backtest(valid_trades_dataframe, time_orders_valid, tick_data_table_name, tr ) # Drop from valid trades dataframe valid_trades_dataframe = valid_trades_dataframe.drop(valid_trades_dataframe[mask].index) + # Step 2: Check open orders for those which are no longer valid or have reached STOP PRICE if len(open_orders) > 0: # Check open orders for those which have expired @@ -118,6 +118,7 @@ def backtest(valid_trades_dataframe, time_orders_valid, tick_data_table_name, tr comment=comment ) open_orders = open_orders.drop(open_orders[mask].index) + # Step 3: Check open positions to check their progress # Check open buy positions if len(open_buy_positions) > 0: @@ -166,13 +167,11 @@ def backtest(valid_trades_dataframe, time_orders_valid, tick_data_table_name, tr # Return the totals back total_value = trade_object['current_available_balance'] + trade_object['current_equity'] # At the conclusion of the testing, close any open orders at the same price brought at - # Get the last tick (for close time) last_tick = sql_interaction.retrieve_last_tick( tick_table_name=tick_data_table_name, project_settings=project_settings ) - # Get the close time - close_time = last_tick[0][10]/1000 + close_time = last_tick[0][10] / 1000 # milliseconds conversion close_open_positions( open_buy_positions=open_buy_positions, open_sell_positions=open_sell_positions, @@ -229,10 +228,9 @@ def new_order(order_dataframe, new_order, row, project_settings): project_settings=project_settings ) new_order['amount_risked'] = risk['risk_amount'] - # Update status of new_order dataframe new_order['status'] = "order" - # Append to order_dataframe - order_dataframe = pandas.concat([order_dataframe, new_order]) + + order_dataframe = pd.concat([order_dataframe, new_order]) return order_dataframe @@ -244,9 +242,7 @@ def expire_order(order_dataframe, expired_order, row, project_settings): # Update balance trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ order_details['amount_risked'] - # Update equity trade_object['current_equity'] = trade_object['current_equity'] - order_details['amount_risked'] - # Update status of expired_order expired_order['status'] = "expired" # Add to SQL # Update SQL table with order @@ -274,12 +270,11 @@ def expire_order(order_dataframe, expired_order, row, project_settings): project_settings=project_settings ) updated_order_dataframe = order_dataframe.drop(expired_order.index) - # Return updated order dataframe return updated_order_dataframe -# Function to add a new position def new_position(position_dataframe, new_position, row, comment, project_settings): + """add a new position""" for position in new_position.iterrows(): position_id = position[0] position_details = position[1] @@ -300,10 +295,9 @@ def new_position(position_dataframe, new_position, row, comment, project_setting entry_price=row['bid'], comment=comment ) - # Update status of order new_position['status'] = "position" - # Append to position dataframe - position_dataframe = pandas.concat([position_dataframe, new_position]) + + position_dataframe = pd.concat([position_dataframe, new_position]) return position_dataframe @@ -360,8 +354,8 @@ def buy_take_profit_reached(position_dataframe, position, row, comment, project_ time=row['time_msc'], project_settings=project_settings ) - # Update status of position position['status'] = "closed" + # Remove from position dataframe position_dataframe = position_dataframe.drop(position.index) return position_dataframe @@ -376,12 +370,9 @@ def buy_stop_loss_reached(position_dataframe, position, row, comment, project_se trade_object=trade_object, project_settings=project_settings ) - # Calculate the volume originally purchased - vol_purchased = last_trade[0][6] - # Calculate the price sold + vol_purchased = last_trade[0][6] # the volume originally purchased price_sold = vol_purchased * row['bid'] - # Calculate the profit / loss - outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) + outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) # Calculate the profit / loss # Update the available balance with the amount risked trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ @@ -393,7 +384,6 @@ def buy_stop_loss_reached(position_dataframe, position, row, comment, project_se f"Updated Balance: {trade_object['current_available_balance']}. " f"Updated Equity: {trade_object['current_equity']}") - # Update status of position position['status'] = "closed" # Update SQL tracking sql_interaction.position_close( @@ -437,13 +427,11 @@ def sell_take_profit_reached(position_dataframe, position, row, comment, project trade_object=trade_object, project_settings=project_settings ) - # Calculate the volume originally purchased - vol_purchased = last_trade[0][6] - # Calculate the price sold + vol_purchased = last_trade[0][6] # volume originally purchased price_sold = vol_purchased * row['bid'] # Calculate the profit / loss outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) - outcome = outcome * -1 + outcome *= -1 # Update the available balance with the amount risked trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ @@ -453,7 +441,6 @@ def sell_take_profit_reached(position_dataframe, position, row, comment, project print(f"Sell Take Profit activated for {pos_tp[0]}. Outcome: {outcome}. " f"Updated Balance: {trade_object['current_available_balance']}. " f"Updated Equity: {trade_object['current_equity']}") - # Update status of position position['status'] = 'closed' # Update SQL tracking sql_interaction.position_close( @@ -497,14 +484,12 @@ def sell_stop_loss_reached(position_dataframe, position, row, comment, project_s trade_object=trade_object, project_settings=project_settings ) - # Calculate the volume originally purchased - vol_purchased = last_trade[0][6] - # Calculate the price sold + vol_purchased = last_trade[0][6] # the volume originally purchased price_sold = vol_purchased * row['bid'] # Calculate the profit / loss outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) # Reverse sign on outcome to account for down direction - outcome = outcome * -1 + outcome *= -1 # Update the available balance with the amount risked trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ @@ -514,8 +499,8 @@ def sell_stop_loss_reached(position_dataframe, position, row, comment, project_s print(f"SELL Stop Loss activated for {pos_sl[0]}. Outcome: {outcome}." f"Updated Balance: {trade_object['current_available_balance']}. " f"Updated Equity: {trade_object['current_equity']}") - # Update status of position position['status'] = 'closed' + # Update position tracking sql_interaction.position_close( trade_type=pos_sl[1]['order_type'], @@ -609,7 +594,6 @@ def close_open_positions(open_buy_positions, open_sell_positions, update_time, p ) - def calc_risk_to_dollars(): # Setup the trade settings purchase_dollars = { @@ -621,7 +605,7 @@ def calc_risk_to_dollars(): else: risk_amount = trade_object["backtest_settings"]["test_balance"] * \ trade_object["backtest_settings"]["balance_risk_percent"] / 100 - # Save to variable + purchase_dollars["risk_amount"] = risk_amount # Multiply by the leverage purchase_dollars["purchase_total"] = risk_amount * trade_object["backtest_settings"]["leverage"] diff --git a/backtest_lib/backtest_analysis.py b/backtest_lib/backtest_analysis.py index cd93fec..2477840 100644 --- a/backtest_lib/backtest_analysis.py +++ b/backtest_lib/backtest_analysis.py @@ -1,7 +1,6 @@ from sql_lib import sql_interaction import display_lib import datetime -import pandas from backtest_lib import setup_backtest, backtest from strategies import ema_cross import hashlib @@ -10,8 +9,10 @@ # Function to initiate and manage backtest def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project_settings, get_data=True, - exchange="mt5", optimize=False, display=False, variables={"risk_ratio": 3}, full_analysis=False, + exchange="mt5", optimize=False, display=False, variables=None, full_analysis=False, redo_analysis=False, regather_data=False): + if variables is None: + variables = {"risk_ratio": 3} symbol_name = symbol.split(".") # Set the table names table_name_base = strategy_name + "_" + symbol_name[0] + "_" @@ -35,6 +36,7 @@ def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project if regather_data: # todo: Delete previous data pass + # If data required if get_data: print("Getting Data") @@ -50,9 +52,11 @@ def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project tick_table_name=tick_data_table_name, balance_tracker_table=balance_tracker_table ) + if redo_analysis: # todo: Delete previous analysis tables pass + if full_analysis: # Get the raw data raw_dataframe = sql_interaction.retrieve_dataframe( @@ -78,13 +82,12 @@ def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project valid_trades_table=valid_trades_table ) # Capture outcomes - #todo: Calculate trade outcomes function - #todo: Save trade outcomes to SQL (preparation for optimization) + # todo: Calculate trade outcomes function + # todo: Save trade outcomes to SQL (preparation for optimization) # Construct the trades if optimize: - # Optimize the take profit. - + # todo: optimize the take profit pass if display: @@ -133,9 +136,8 @@ def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project return True -# Function to display backtest details to user def show_display(strategy_image, trades_outcome, proposed_trades, symbol, strategy): - # Construct the Title + """display backtest details to user""" title = symbol + " " + strategy # Add trades to strategy image strategy_with_trades = display_lib.add_trades_to_graph( @@ -145,7 +147,6 @@ def show_display(strategy_image, trades_outcome, proposed_trades, symbol, strate # Turn proposed trades into a subplot prop_trades_figure = display_lib.add_dataframe(proposed_trades) - display_lib.display_backtest( original_strategy=strategy_image, strategy_with_trades=strategy_with_trades, @@ -154,8 +155,8 @@ def show_display(strategy_image, trades_outcome, proposed_trades, symbol, strate ) -# Function to retrieve and construct trade open and sell def calculate_trades(trade_object, comment, project_settings): + """retrieve and construct trade open and sell""" # Retrieve the trades for the strategy being analyzed trades = sql_interaction.retrieve_unique_order_id( trade_object=trade_object, @@ -163,7 +164,7 @@ def calculate_trades(trade_object, comment, project_settings): project_settings=project_settings) trade_list = [] full_trades = [] - # Format the trades into a nicer list + for trade in trades: trade_list.append(trade[0]) @@ -222,28 +223,14 @@ def calculate_trades(trade_object, comment, project_settings): return summary -# Function to calculate if a trade was a win or loss and profit def calc_the_win(row): - # Set up record - outcome = { - "profit": 0, - "win": False, - "not_completed": False - } - # Branch based on order type + """calculate if a trade was a win or loss and profit""" + outcome = {"not_completed": False} + if row[3] == "BUY_STOP": - # Calculate the profit outcome["profit"] = (row[18] - row[17]) * row[6] else: - # Calculate the profit outcome["profit"] = (row[17] - row[18]) * row[6] - # Profit will be any number > 0 - if outcome["profit"] > 0: - outcome['win'] = True - else: - outcome['win'] = False - # Return outcome - return outcome - - + outcome['win'] = outcome["profit"] > 0 + return outcome diff --git a/backtest_lib/setup_backtest.py b/backtest_lib/setup_backtest.py index dc26cec..46d0783 100644 --- a/backtest_lib/setup_backtest.py +++ b/backtest_lib/setup_backtest.py @@ -1,7 +1,7 @@ import stat -import numpy -import pandas +import numpy as np +import pandas as pd import psycopg2 import exceptions @@ -30,32 +30,26 @@ def set_up_backtester(strategy_name, symbol, candle_timeframe, backtest_timefram create_backtest_tables(tick_table_name=tick_table_name, balance_tracker_table=balance_tracker_table, project_settings=project_settings) - # Get the datetime now + current_datetime = datetime.datetime.now() current_datetime = current_datetime.astimezone(datetime.timezone.utc) + # Populate the raw data based upon easily selected timeframes - if backtest_timeframe == "month": - previous_datetime = current_datetime - relativedelta(months=1) - pass - elif backtest_timeframe == "5days": - previous_datetime = current_datetime - relativedelta(days=5) - pass - elif backtest_timeframe == "6month": - previous_datetime = current_datetime - relativedelta(months=6) - pass - elif backtest_timeframe == "year": - previous_datetime = current_datetime - relativedelta(years=1) - pass - elif backtest_timeframe == "2years": - previous_datetime = current_datetime - relativedelta(years=2) - pass - elif backtest_timeframe == "5years": - previous_datetime = current_datetime - relativedelta(years=5) - pass - else: + switch = { + "month": {"months": 1}, + "5days": {"days": 5}, + "6month": {"months": 6}, + "year": {"years": 1}, + "2years": {"years": 2}, + "5years": {"years": 5} + } + + if backtest_timeframe not in switch: print("Choose correct value for backtester") raise exceptions.BacktestIncorrectBacktestTimeframeError + previous_datetime = current_datetime - relativedelta(**switch[backtest_timeframe]) + if exchange == "mt5": # Retrieve data retrieve_mt5_backtest_data( @@ -134,8 +128,9 @@ def split_time_range_in_half(start_time, finish_time): n = 2 split_timeframe = [] diff = (finish_time - start_time) // n - for idx in range(0, n): + for idx in range(n): split_timeframe.append((start_time + idx * diff)) + split_timeframe.append(finish_time) return split_timeframe @@ -149,36 +144,30 @@ def retrieve_mt5_tick_data(start_time, finish_time, symbol): symbol=symbol ) # Autoscale if Zero results retrieved - if type(tick_data) is not numpy.ndarray: + if type(tick_data) is not np.ndarray: print(f"Auto scaling tick query for {symbol}") # Split timerange into 2 split_timeframe = split_time_range_in_half(start_time=start_time, finish_time=finish_time) # Iterate through timeframe list and append start_time = split_timeframe[0] - tick_data_autoscale = pandas.DataFrame() + tick_data_autoscale = pd.DataFrame() for timeframe in split_timeframe: - if timeframe == split_timeframe[0]: - # Initial pass, so ignore - pass - - else: + if timeframe != split_timeframe[0]: tick_data_new = retrieve_mt5_tick_data(start_time=start_time, finish_time=timeframe, symbol=symbol) - tick_data_autoscale = pandas.concat([tick_data_autoscale, tick_data_new]) + tick_data_autoscale = pd.concat([tick_data_autoscale, tick_data_new]) start_time = timeframe return tick_data_autoscale # Else return value - ticks_data_frame = pandas.DataFrame(tick_data) + ticks_data_frame = pd.DataFrame(tick_data) - # Add spread ticks_data_frame['spread'] = ticks_data_frame['ask'] - ticks_data_frame['bid'] - # Add symbol ticks_data_frame['symbol'] = symbol # Format integers into signed integers (postgres doesn't support unsigned int) ticks_data_frame['time'] = ticks_data_frame['time'].astype('int64') ticks_data_frame['volume'] = ticks_data_frame['volume'].astype('int64') ticks_data_frame['time_msc'] = ticks_data_frame['time_msc'].astype('int64') - ticks_data_frame['human_time'] = pandas.to_datetime(ticks_data_frame['time'], unit='s') - ticks_data_frame['human_time_msc'] = pandas.to_datetime(ticks_data_frame['time_msc'], unit='ms') + ticks_data_frame['human_time'] = pd.to_datetime(ticks_data_frame['time'], unit='s') + ticks_data_frame['human_time_msc'] = pd.to_datetime(ticks_data_frame['time_msc'], unit='ms') return ticks_data_frame @@ -191,29 +180,26 @@ def retrieve_mt5_candle_data(start_time, finish_time, timeframe, symbol): timeframe=timeframe, symbol=symbol ) - if type(candlestick_data) is not numpy.ndarray: + if type(candlestick_data) is not np.ndarray: print(f"Auto scaling candlestick query for {symbol} and {timeframe}") # Split time range in 2 split_timeframe = split_time_range_in_half(start_time=start_time, finish_time=finish_time) # Iterate through the timeframe list and construct full list start_time = split_timeframe[0] - candlestick_data_autoscale = pandas.DataFrame() + candlestick_data_autoscale = pd.DataFrame() for time in split_timeframe: - if time == split_timeframe[0]: - # Initial pass, so ignore - pass - else: + if time != split_timeframe[0]: candlestick_data_new = retrieve_mt5_candle_data( start_time=start_time, finish_time=time, timeframe=timeframe, symbol=symbol ) - candlestick_data_autoscale = pandas.concat([candlestick_data_autoscale, candlestick_data_new]) + candlestick_data_autoscale = pd.concat([candlestick_data_autoscale, candlestick_data_new]) return candlestick_data_autoscale # Convert to a dataframe - candlestick_dataframe = pandas.DataFrame(candlestick_data) + candlestick_dataframe = pd.DataFrame(candlestick_data) # Add in symbol and timeframe columns candlestick_dataframe['symbol'] = symbol candlestick_dataframe['timeframe'] = timeframe @@ -222,15 +208,13 @@ def retrieve_mt5_candle_data(start_time, finish_time, timeframe, symbol): candlestick_dataframe['tick_volume'] = candlestick_dataframe['tick_volume'].astype('float64') candlestick_dataframe['spread'] = candlestick_dataframe['spread'].astype('int64') candlestick_dataframe['real_volume'] = candlestick_dataframe['real_volume'].astype('int64') - candlestick_dataframe['human_time'] = pandas.to_datetime(candlestick_dataframe['time'], unit='s') + candlestick_dataframe['human_time'] = pd.to_datetime(candlestick_dataframe['time'], unit='s') return candlestick_dataframe # Function to upload a dataframe to Postgres def upload_to_postgres(dataframe, table_name, project_settings): - ## Process to upload to local database: Get directory, write to csv, update CSV permissions, upload to DB, - # delete CSV - + # upload to local database: Get directory, write to csv, update CSV permissions, upload to DB, delete CSV # Specify the disk location current_user = os.getlogin() dataframe_csv = f"C:\\Users\\{current_user}\\Desktop\\ticks_data_frame.csv" @@ -238,14 +222,11 @@ def upload_to_postgres(dataframe, table_name, project_settings): # Dump the dataframe to disk dataframe.to_csv(dataframe_csv, index_label='id', header=False) - # Open the csv f = open(dataframe_csv, 'r') - # Connect to database conn = sql_interaction.postgres_connect(project_settings) cursor = conn.cursor() - # Try to upload try: cursor.copy_from(f, table_name, sep=",") conn.commit() diff --git a/binance_lib/binance_interaction.py b/binance_lib/binance_interaction.py index a4527b8..7cdbac2 100644 --- a/binance_lib/binance_interaction.py +++ b/binance_lib/binance_interaction.py @@ -2,14 +2,10 @@ from binance.spot import Spot -# Function to query Binance and retrieve status def query_binance_status(): - # Query for system status + """query Binance and retrieve status""" status = Spot().system_status() - if status['status'] == 0: - return True - else: - raise False + return status['status'] == 0 # Function to query Binance account @@ -17,15 +13,13 @@ def query_account(api_key, secret_key): return Spot(key=api_key, secret=secret_key).account() -# Function to query Binance for candlestick data def get_candlestick_data(symbol, timeframe, qty): - # Retrieve the raw data + """query Binance for candlestick data""" raw_data = Spot().klines(symbol=symbol, interval=timeframe, limit=qty) - # Set up the return array converted_data = [] + # Convert each element into a Python dictionary object, then add to converted_data for candle in raw_data: - # Dictionary object converted_candle = { 'time': candle[0], 'open': float(candle[1]), @@ -39,27 +33,24 @@ def get_candlestick_data(symbol, timeframe, qty): 'taker_buy_base_asset_volume': float(candle[9]), 'taker_buy_quote_asset_volume': float(candle[10]) } - # Add to converted_data converted_data.append(converted_candle) - # Return converted data + return converted_data -# Function to query Binance for all symbols with a base asset of BUSD def query_quote_asset_list(quote_asset_symbol): + """query Binance for all symbols with a base asset of BUSD""" # Retrieve a list of symbols from Binance. Returns as a dictionary symbol_dictionary = Spot().exchange_info() - # Convert into a dataframe symbol_dataframe = pandas.DataFrame(symbol_dictionary['symbols']) + # Extract only those symbols with a base asset of BUSD quote_symbol_dataframe = symbol_dataframe.loc[symbol_dataframe['quoteAsset'] == quote_asset_symbol] - # Return base_symbol_dataframe return quote_symbol_dataframe # Function to make a trade on Binance def make_trade(symbol, action, type, timeLimit, quantity, stop_price, stop_limit_price, project_settings): - # Develop the params params = { "symbol": symbol, "side": action, @@ -76,17 +67,12 @@ def make_trade(symbol, action, type, timeLimit, quantity, stop_price, stop_limit # See if we're testing. Default to yes. if project_settings['Testing'] == "False": print("Real Trade") - # Set the API Key api_key = project_settings['BinanceKeys']['API_Key'] - # Set the secret key secret_key = project_settings['BinanceKeys']['Secret_Key'] - # Setup the client client = Spot(key=api_key, secret=secret_key) else: print("Testing Trading") - # Set the Test API Key api_key = project_settings['TestKeys']['Test_API_Key'] - # Set the Test Secret Key secret_key = project_settings['TestKeys']['Test_Secret_Key'] client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") @@ -98,22 +84,16 @@ def make_trade(symbol, action, type, timeLimit, quantity, stop_price, stop_limit print(f"Found error. {error}") -# Function to make a trade if params provided def make_trade_with_params(params, project_settings): - # See if we're testing. Default to yes. + """make a trade if params provided""" if project_settings['Testing'] == "False": print("Real Trade") - # Set the API Key api_key = project_settings['BinanceKeys']['API_Key'] - # Set the secret key secret_key = project_settings['BinanceKeys']['Secret_Key'] - # Setup the client client = Spot(key=api_key, secret=secret_key) else: print("Testing Trading") - # Set the Test API Key api_key = project_settings['TestKeys']['Test_API_Key'] - # Set the Test Secret Key secret_key = project_settings['TestKeys']['Test_Secret_Key'] client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") @@ -125,21 +105,15 @@ def make_trade_with_params(params, project_settings): print(f"Found error. {error}") -# Function to cancel a trade def cancel_order_by_symbol(symbol, project_settings): - # See if we're testing. Default to yes. + """cancel a trade""" if project_settings['Testing'] == "False": - # Set the API Key api_key = project_settings['BinanceKeys']['API_Key'] - # Set the secret key secret_key = project_settings['BinanceKeys']['Secret_Key'] - # Setup the client client = Spot(key=api_key, secret=secret_key) else: print("Testing Trading") - # Set the Test API Key api_key = project_settings['TestKeys']['Test_API_Key'] - # Set the Test Secret Key secret_key = project_settings['TestKeys']['Test_Secret_Key'] client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") @@ -151,24 +125,17 @@ def cancel_order_by_symbol(symbol, project_settings): print(f"Found error {error}") -# Function to query open trades def query_open_trades(project_settings): - # See if we're testing. Default to yes. + """query open trades""" if project_settings['Testing'] == "False": - # Set the API Key api_key = project_settings['BinanceKeys']['API_Key'] - # Set the secret key secret_key = project_settings['BinanceKeys']['Secret_Key'] - # Setup the client client = Spot(key=api_key, secret=secret_key) else: - # Set the Test API Key api_key = project_settings['TestKeys']['Test_API_Key'] - # Set the Test Secret Key secret_key = project_settings['TestKeys']['Test_Secret_Key'] client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") - # Cancel the trade try: response = client.get_open_orders() return response @@ -177,19 +144,13 @@ def query_open_trades(project_settings): def get_balance(project_settings): - # See if we're testing. Default to yes. if project_settings['Testing'] == "False": - # Set the API Key api_key = project_settings['BinanceKeys']['API_Key'] - # Set the secret key secret_key = project_settings['BinanceKeys']['Secret_Key'] - # Setup the client client = Spot(key=api_key, secret=secret_key) else: - # Set the Test API Key api_key = project_settings['TestKeys']['Test_API_Key'] - # Set the Test Secret Key secret_key = project_settings['TestKeys']['Test_Secret_Key'] client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") - return client.account_snapshot("SPOT") \ No newline at end of file + return client.account_snapshot("SPOT") diff --git a/capture_lib/trade_capture.py b/capture_lib/trade_capture.py index 04a12b1..a588b96 100644 --- a/capture_lib/trade_capture.py +++ b/capture_lib/trade_capture.py @@ -4,25 +4,11 @@ # Function to capture order actions -def capture_order(order_type, strategy, exchange, symbol, comment, project_settings,volume=0.0, stop_loss=0.0, +def capture_order(order_type, strategy, exchange, symbol, comment, project_settings, volume=0.0, stop_loss=0.0, take_profit=0.0, price=None, paper=True, order_number="", backtest=False): """ Function to capture an order - :param order_type: String - :param strategy: String - :param exchange: String - :param symbol: String - :param comment: String - :param project_settings: JSON Object - :param volume: Float - :param stop_loss: Float - :param take_profit: Float - :param price: Float - :param paper: Bool - :param order_number: INT - :return: """ - # Format objects correctly strategy = str(strategy) order_type = str(order_type) exchange = str(exchange) @@ -31,7 +17,7 @@ def capture_order(order_type, strategy, exchange, symbol, comment, project_setti stop_loss = float(stop_loss) take_profit = float(take_profit) comment = str(comment) - # Create the Database Object + db_object = { "strategy": strategy, "exchange": exchange, @@ -48,44 +34,39 @@ def capture_order(order_type, strategy, exchange, symbol, comment, project_setti if order_type == "BUY_STOP" or order_type == "SELL_STOP": if volume <= 0: print(f"Volume must be greater than 0 for an order type of {order_type}") - raise SyntaxError # Use Pythons built in error for incorrect syntax + raise SyntaxError if take_profit <= 0: print(f"Take Profit must be greater than 0 for an order type of {order_type}") - raise ValueError # Use Pythons built in error for incorrect value + raise ValueError if stop_loss <= 0: print(f"Stop Loss must be greater than 0 for an order type of {order_type}") - raise ValueError # Use Pythons built in error for incorrect value - if price == None: + raise ValueError + if price is None: print(f"Price cannot be NONE on order_type {order_type}") - raise ValueError # Use Pythons built in error for incorrect value - else: - # Format price correctly - price = float(price) - # Add to database object - db_object['price'] = price + raise ValueError + + price = float(price) + db_object['price'] = price # If order_type == "BUY" or "SELL", price must be None if order_type == "BUY" or order_type == "SELL": if volume <= 0: print(f"Volume must be greater than 0 for an order type of {order_type}") - raise ValueError # Use Pythons built in error for incorrect value + raise ValueError if take_profit <= 0: print(f"Take Profit must be greater than 0 for an order type of {order_type}") - raise ValueError # Use Pythons built in error for incorrect value + raise ValueError if stop_loss <= 0: print(f"Stop Loss must be greater than 0 for an order type of {order_type}") - raise ValueError # Use Pythons built in error for incorrect value - if price != None: + raise ValueError + if price is not None: print(f"Price must be NONE for order_type {order_type}") - raise ValueError # Use Pythons built in error for incorrect value - else: - # Make price = 0 - price = 0 - db_object['price'] = price + raise ValueError - if backtest == True: - pass - else: + price = 0 + db_object['price'] = price + + if not backtest: # If order_type == "cancel", pass straight through into cancel order function if order_type == "CANCEL": db_object['price'] = price @@ -136,8 +117,6 @@ def capture_order(order_type, strategy, exchange, symbol, comment, project_setti # Function to capture modifications to open positions def capture_position_update(trade_type, order_number, symbol, strategy, exchange, project_settings, comment, new_stop_loss, new_take_profit, price, paper=True, volume=0.0): - - # Format the provided items correctly order_type = str(trade_type) order_number = int(order_number) symbol = str(symbol) @@ -161,8 +140,7 @@ def capture_position_update(trade_type, order_number, symbol, strategy, exchange "order_id": order_number } - # Branch based upon trade_type - if trade_type == "trailing_stop_update" or trade_type == "take_profit_update": + if trade_type in ["trailing_stop_update", "take_profit_update"]: # Branch again based upon exchange type if exchange == "mt5": # Use the modify_position function from mt5_interaction @@ -175,11 +153,11 @@ def capture_position_update(trade_type, order_number, symbol, strategy, exchange ) # Update DB Object db_object['status'] = "position_modified" - elif trade_type == "SELL" or trade_type == "BUY": + elif trade_type in ["SELL", "BUY"]: # Branch again based upon exchange type if exchange == "mt5": # Use the close_position function from mt5_interaction - #todo: implement inside a try statement + # todo: implement inside a try statement position_outcome = mt5_interaction.close_position( order_number=order_number, symbol=symbol, @@ -193,13 +171,9 @@ def capture_position_update(trade_type, order_number, symbol, strategy, exchange # Update the position only db_object['status'] = "position" - # Update SQL - # Branch based upon the table if paper: sql_interaction.insert_paper_trade_action(trade_information=db_object, project_settings=project_settings) - return True else: sql_interaction.insert_live_trade_action(trade_information=db_object, project_settings=project_settings) - return True - + return True diff --git a/coinbase_lib/get_account_details.py b/coinbase_lib/get_account_details.py index 6b2461e..61eb101 100644 --- a/coinbase_lib/get_account_details.py +++ b/coinbase_lib/get_account_details.py @@ -1,15 +1,13 @@ from coinbase.wallet.client import Client + # Function to account details from Coinbase def get_account_details(project_settings): # Retrieve the API Key api_key = project_settings['coinbase']['api_key'] api_secret = project_settings['coinbase']['api_secret'] - # Create the Coinbase Client - client = Client( - api_key=api_key, - api_secret=api_secret - ) + client = Client(api_key=api_key, api_secret=api_secret) + # Retrieve information accounts = client.get_accounts() - return accounts \ No newline at end of file + return accounts diff --git a/coinbase_lib/get_candlesticks.py b/coinbase_lib/get_candlesticks.py index 61e715c..9a2a3e7 100644 --- a/coinbase_lib/get_candlesticks.py +++ b/coinbase_lib/get_candlesticks.py @@ -1,35 +1,29 @@ import requests -import pandas +import pandas as pd + # Function to get candle data def get_candlestick_data(symbol, timeframe): - # Convert the timeframe into a Coinbase specific type. This could be done in a switch statement for Python 3.10 - if timeframe == "M1": - timeframe_converted = 60 - elif timeframe == "M5": - timeframe_converted = 300 - elif timeframe == "M15": - timeframe_converted = 900 - elif timeframe == "H1": - timeframe_converted = 3600 - elif timeframe == "H6": - timeframe_converted = 21600 - elif timeframe == "D1": - timeframe_converted = 86400 - else: - return Exception - # Construct the URL + switch = { + "M1": 60, + "M5": 300, + "M15": 900, + "H1": 3600, + "H6": 21600, + "D1": 86400 + } + + if timeframe not in switch: + raise Exception + timeframe_converted = switch[timeframe] + + # Query the API url = f"https://api.exchange.coinbase.com/products/{symbol}/candles?granularity={timeframe_converted}" - # Construct the headers headers = {"accept": "application/json"} - # Query the API - response = requests.get(url, headers=headers) - # Retrieve the data - candlestick_raw_data = response.json() - # Initialize an empty array + candlestick_raw_data = requests.get(url, headers=headers).json() + candlestick_data = [] - # Iterate through the returned data and construct a more useful data format - for candle in candlestick_raw_data: + for candle in candlestick_raw_data: # more useful data format candle_dict = { "symbol": symbol, "time": candle[0], @@ -40,9 +34,7 @@ def get_candlestick_data(symbol, timeframe): "volume": candle[5], "timeframe": timeframe } - # Append to candlestick_data candlestick_data.append(candle_dict) - # Convert candlestick_data to dataframe - candlestick_dataframe = pandas.DataFrame(candlestick_data) - # Return a dataframe + + candlestick_dataframe = pd.DataFrame(candlestick_data) return candlestick_dataframe diff --git a/display_lib.py b/display_lib.py index 5e140e6..6f1fa09 100644 --- a/display_lib.py +++ b/display_lib.py @@ -4,10 +4,8 @@ from plotly.subplots import make_subplots -# Function to retrieve back_test data and then display chart of close values def show_data(table_name, dataframe, graph_name, project_settings): - # Table Name - # Get the data + """retrieve back_test data and then display chart of close values""" dataframe = sql_interaction.retrieve_dataframe(table_name, project_settings) # Construct the figure fig = go.Figure(data=[go.Candlestick( @@ -19,11 +17,10 @@ def show_data(table_name, dataframe, graph_name, project_settings): )]) fig.add_trace(go.Scatter( - x=dataframe['human_time'], - y=dataframe['ta_sma_200'], - name='ta_sma_200' - ) - ) + x=dataframe['human_time'], + y=dataframe['ta_sma_200'], + name='ta_sma_200' + )) fig.add_trace(go.Scatter( x=dataframe['human_time'], @@ -42,15 +39,11 @@ def show_data(table_name, dataframe, graph_name, project_settings): app.layout = html.Div(children=[ html.H1(children=graph_name), html.Div("Example data"), - dcc.Graph( - id='Example Graph', - figure=fig - ) + dcc.Graph(id='Example Graph', figure=fig) ]) app.run_server(debug=True) -# Function to display a plotly graph in dash def display_graph(plotly_fig, graph_title, dash=False, upload=False): """ Function to display a plotly graph using Dash @@ -59,15 +52,11 @@ def display_graph(plotly_fig, graph_title, dash=False, upload=False): :return: None """ # Add in autoscaling for each plotly figure - plotly_fig.update_layout( - autosize=True - ) + plotly_fig.update_layout(autosize=True) plotly_fig.update_yaxes(automargin=True) plotly_fig.update_layout(xaxis_rangeslider_visible=False) - if dash: - # Create the Dash object app = Dash(__name__) # Construct view app.layout = html.Div(children=[ @@ -78,20 +67,15 @@ def display_graph(plotly_fig, graph_title, dash=False, upload=False): figure=plotly_fig ) ]) - # Run the image app.run_server(debug=True) else: plotly_fig.show() -# Function to display a backtest def display_backtest(original_strategy, strategy_with_trades, table, graph_title): - original_strategy.update_layout( - autosize=True - ) + original_strategy.update_layout(autosize=True) original_strategy.update_yaxes(automargin=True) original_strategy.update_layout(xaxis_rangeslider_visible=False) - # Create a Dash Object app = Dash(__name__) # Construct view @@ -119,7 +103,6 @@ def display_backtest(original_strategy, strategy_with_trades, table, graph_title app.run_server(debug=True) - # Function to construct base candlestick graph def construct_base_candlestick_graph(dataframe, candlestick_title): """ @@ -137,7 +120,6 @@ def construct_base_candlestick_graph(dataframe, candlestick_title): low=dataframe['low'], name=candlestick_title )]) - # Return the graph object return fig @@ -157,52 +139,46 @@ def add_line_to_graph(base_fig, dataframe, dataframe_column, line_name): y=dataframe[dataframe_column], name=line_name )) - # Return the object return base_fig # Function to display points on graph as diamond def add_markers_to_graph(base_fig, dataframe, value_column, point_names): - """ - Function to add points to a graph - :param base_fig: plotly figure - :param dataframe: pandas dataframe - :param value_column: value for Y display - :param point_names: what's being plotted - :return: updated plotly figure - """ - # Construct trace - base_fig.add_trace(go.Scatter( - mode="markers", - marker=dict(size=8, symbol="diamond"), - x=dataframe['human_time'], - y=dataframe[value_column], - name=point_names - )) - return base_fig + """ + Function to add points to a graph + :param base_fig: plotly figure + :param dataframe: pandas dataframe + :param value_column: value for Y display + :param point_names: what's being plotted + :return: updated plotly figure + """ + # Construct trace + base_fig.add_trace(go.Scatter( + mode="markers", + marker=dict(size=8, symbol="diamond"), + x=dataframe['human_time'], + y=dataframe[value_column], + name=point_names + )) + return base_fig # Function to turn a dataframe into a table def add_dataframe(dataframe): fig = go.Figure(data=[go.Table( - header=dict(values=["Time", "Order Type", "Stop Price", "Stop Loss", "Take Profit"], align='left'), - cells=dict(values=[ - dataframe['human_time'], - dataframe['order_type'], - dataframe['stop_price'], - dataframe['stop_loss'], - dataframe['take_profit'] - ]) - )] - ) + header=dict(values=["Time", "Order Type", "Stop Price", "Stop Loss", "Take Profit"], align='left'), + cells=dict(values=[ + dataframe['human_time'], + dataframe['order_type'], + dataframe['stop_price'], + dataframe['stop_loss'], + dataframe['take_profit']]) + )]) return fig -# Function to add trades to graph def add_trades_to_graph(trades_dict, base_fig): - # Create a point plot list point_plot = [] - # Create the colors buy_color = dict(color="green") sell_color = dict(color="red") # Add each set of trades @@ -224,7 +200,3 @@ def add_trades_to_graph(trades_dict, base_fig): ) ) return base_fig - - -# Function to add a table of the strategy outcomes to Plotly - diff --git a/exceptions.py b/exceptions.py index f6f82b6..7aeb4c3 100644 --- a/exceptions.py +++ b/exceptions.py @@ -1,86 +1,88 @@ # Initialize MetaTrader Error class MetaTraderInitializeError(Exception): - "MetaTrader 5 Initilization failed. Check username, password, server, path" + """MetaTrader 5 Initilization failed. Check username, password, server, path""" pass # Login to MetaTrader Error class MetaTraderLoginError(Exception): - "Error logging in to MetaTrader" + """Error logging in to MetaTrader""" pass # Incorrect symbol provided class MetaTraderSymbolDoesNotExistError(Exception): - "One of the provided symbols does not exist" + """One of the provided symbols does not exist""" pass # Symbol unable to be enabled class MetaTraderSymbolUnableToBeEnabledError(Exception): - "One of the symbols provided was not able to be enabled" + """One of the symbols provided was not able to be enabled""" pass # Algo Trading enabled on MetaTrader 5 class MetaTraderAlgoTradingNotDisabledError(Exception): - "Turn AlgoTrading off on MetaTrader terminal to use Python Trading Bot" + """Turn AlgoTrading off on MetaTrader terminal to use Python Trading Bot""" pass # Error placing order class MetaTraderOrderPlacingError(Exception): - "Error placing order on MetaTrader" + """Error placing order on MetaTrader""" pass # Error with balance check class MetaTraderOrderCheckError(Exception): - "Error checking order on MetaTrader" + """Error checking order on MetaTrader""" pass # Error canceling order class MetaTraderCancelOrderError(Exception): - "Error canceling order on MetaTrader" + """Error canceling order on MetaTrader""" pass # Error modifying a position MetaTrader class MetaTraderModifyPositionError(Exception): - "Error modifying position on MetaTrader" + """Error modifying position on MetaTrader""" pass # Error closing a position class MetaTraderClosePositionError(Exception): - "Error closing a position on MetaTrader" + """Error closing a position on MetaTrader""" pass # Error for having a zero stop price on a BUY_STOP or SELL_STOP class MetaTraderIncorrectStopPriceError(Exception): - "Cannot have a 0.00 price on a STOP order" + """Cannot have a 0.00 price on a STOP order""" pass # Error for zero ticks returned from query class MetaTraderZeroTicksDownloadedError(Exception): - "Zero ticks retrieved from MetaTrader 5 Terminal" + """Zero ticks retrieved from MetaTrader 5 Terminal""" pass # SQL Error class SQLTableCreationError(Exception): - "Error creating SQL table" + """Error creating SQL table""" pass + # SQL Back Test Trade Action Error class SQLBacktestTradeActionError(Exception): - "Error inserting SQL Trade Action" + """Error inserting SQL Trade Action""" pass + # Backtest error class BacktestIncorrectBacktestTimeframeError(Exception): - "Incorrect timeframe selected for backtest timeframe" + """Incorrect timeframe selected for backtest timeframe""" pass diff --git a/indicator_lib/bearish_engulfing.py b/indicator_lib/bearish_engulfing.py index c51638e..19322d5 100644 --- a/indicator_lib/bearish_engulfing.py +++ b/indicator_lib/bearish_engulfing.py @@ -2,7 +2,6 @@ from strategies import engulfing_candle_strategy -# Function to calculate bearish engulfing pattern def calc_bearish_engulfing(dataframe, exchange, project_settings): """ Function to detect a bearish engulfing pattern @@ -12,33 +11,22 @@ def calc_bearish_engulfing(dataframe, exchange, project_settings): :return: Bool """ - # Extract the most recent candle - len_most_recent = len(dataframe) - 1 - most_recent_candle = dataframe.loc[len_most_recent] - - # Extract the second most recent candle - len_second_most_recent = len(dataframe) - 2 - second_most_recent_candle = dataframe.loc[len_second_most_recent] + # Extract the most & second most recent candle + most_recent_candle = dataframe.loc[len(dataframe) - 1] # .loc[-1]? + second_most_recent_candle = dataframe.loc[len(dataframe) - 2] # Calculate if the second most recent candle is Green if second_most_recent_candle['close'] > second_most_recent_candle['open']: # Calculate if most recent candle is Red if most_recent_candle['close'] < most_recent_candle['open']: - # Check the Red Body > Red Body - # Red Body red_body = most_recent_candle['open'] - most_recent_candle['close'] - # Green Body green_body = second_most_recent_candle['close'] - second_most_recent_candle['open'] - # Compare if red_body > green_body: # Compare Red low and Green low if most_recent_candle['low'] < second_most_recent_candle['low']: - # Calculate the 20-EMA ema_20 = ema_calculator.calc_ema(dataframe=dataframe, ema_size=20) # Extract the second most recent candle from the new dataframe - ema_count = len(ema_20) - 2 - ema_second_most_recent = ema_20.loc[ema_count] - # Compare 20-EMA and Green Open + ema_second_most_recent = ema_20.loc[len(ema_20) - 2] if ema_second_most_recent['open'] > ema_second_most_recent['ema_20']: # Use this function if you're planning on sending it to an alert generator strategy = engulfing_candle_strategy.engulfing_candle_strategy( @@ -51,4 +39,4 @@ def calc_bearish_engulfing(dataframe, exchange, project_settings): project_settings=project_settings ) return True - return False \ No newline at end of file + return False diff --git a/indicator_lib/bollinger_bands.py b/indicator_lib/bollinger_bands.py index 483c141..0a785c9 100644 --- a/indicator_lib/bollinger_bands.py +++ b/indicator_lib/bollinger_bands.py @@ -1,13 +1,12 @@ import talib -# Function to calculate Bollinger Bands + def calc_bollinger_bands(dataframe, timeperiod, std_dev_up, std_dev_down, mattype): - # Create column titles - upper_title = "ta_bollinger_upper_" + str(timeperiod) - lower_title = "ta_bollinger_lower_" + str(timeperiod) - middle_title = "ta_bollinger_middle_" + str(timeperiod) + """calculate Bollinger Bands""" + upper_title = f"ta_bollinger_upper_{timeperiod}" + lower_title = f"ta_bollinger_lower_{timeperiod}" + middle_title = f"ta_bollinger_middle_{timeperiod}" - # Calculate dataframe[upper_title], dataframe[middle_title], dataframe[lower_title] = talib.BBANDS( close=dataframe['close'], timeperiod=timeperiod, @@ -15,5 +14,5 @@ def calc_bollinger_bands(dataframe, timeperiod, std_dev_up, std_dev_down, mattyp nbdevdn=std_dev_down, mattype=mattype ) - # Return the dataframe - return dataframe \ No newline at end of file + + return dataframe diff --git a/indicator_lib/bullish_engulfing.py b/indicator_lib/bullish_engulfing.py index 56f729f..2cb8ce8 100644 --- a/indicator_lib/bullish_engulfing.py +++ b/indicator_lib/bullish_engulfing.py @@ -2,7 +2,6 @@ from strategies import engulfing_candle_strategy -# Function to calculate bullish engulfing pattern def calc_bullish_engulfing(dataframe, exchange, project_settings): """ Function to calculate if a bullish engulfing candle has been detected @@ -11,33 +10,23 @@ def calc_bullish_engulfing(dataframe, exchange, project_settings): :param project_settings: JSON data object :return: Bool """ - # Extract the most recent candle - len_most_recent = len(dataframe) - 1 - most_recent_candle = dataframe.loc[len_most_recent] - - # Extract the second most recent candle - len_second_most_recent = len(dataframe) - 2 - second_most_recent_candle = dataframe.loc[len_second_most_recent] + # Extract the most & the second most recent candles + most_recent_candle = dataframe.loc[len(dataframe) - 1] + second_most_recent_candle = dataframe.loc[len(dataframe) - 2] # Calculate if second most recent candle Red if second_most_recent_candle['close'] < second_most_recent_candle['open']: # Calculate if most recent candle green if most_recent_candle['close'] > most_recent_candle['open']: # Check the Green Body > Red Body - # Red Body red_body = second_most_recent_candle['open'] - second_most_recent_candle['close'] - # Green Body green_body = most_recent_candle['close'] - second_most_recent_candle['open'] - # Compare if green_body > red_body: # Compare Green High > Red High if most_recent_candle['high'] > second_most_recent_candle['high']: # Calculate the 20-EMA ema_20 = ema_calculator.calc_ema(dataframe=dataframe, ema_size=20) - # Extract the second most recent candle from the new dataframe - ema_count = len(ema_20) - 2 - ema_second_most_recent = ema_20.loc[ema_count] - # Compare the EMA and Red Low + ema_second_most_recent = ema_20.loc[len(ema_20) - 2] # the second most recent candle if ema_second_most_recent['close'] < ema_second_most_recent['ema_20']: # If plugging into a strategy such as the Engulfing Candle Strategy, send to alerting mechanism strategy = engulfing_candle_strategy.engulfing_candle_strategy( @@ -49,8 +38,5 @@ def calc_bullish_engulfing(dataframe, exchange, project_settings): alert_type="bullish_engulfing", project_settings=project_settings ) - # Return true return True return False - - diff --git a/indicator_lib/calc_all_indicators.py b/indicator_lib/calc_all_indicators.py index c1fb601..d487033 100644 --- a/indicator_lib/calc_all_indicators.py +++ b/indicator_lib/calc_all_indicators.py @@ -1,32 +1,22 @@ from indicator_lib import doji_star, rsi, ta_sma, ta_ema, two_crows, three_black_crows, bollinger_bands -# Calculate all the indicator_lib currently available + def all_indicators(dataframe): - # Copy the dataframe - dataframe_copy = dataframe.copy() - # SMA's - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 5) - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 8) - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 15) - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 20) - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 50) - dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 200) - # EMA's - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 5) - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 8) - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 15) - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 20) - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 50) - dataframe_copy = ta_ema.calc_ema(dataframe_copy, 200) + """Calculate all the indicator_lib currently available""" + df = dataframe.copy() + + for val in [5, 8, 15, 20, 50, 200]: # SMA + df = ta_sma.calc_ta_sma(df, val) + + for val in [5, 8, 15, 20, 50, 200]: # EMA + df = ta_ema.calc_ema(df, val) + # Patterns - # 2 Crows - dataframe_copy = two_crows.calc_two_crows(dataframe_copy) - # Three black crows - dataframe_copy = three_black_crows.calc_three_black_crows(dataframe_copy) - # Doji Star - dataframe_copy = doji_star.doji_star(dataframe_copy) - # RSI - dataframe_copy = rsi.rsi(dataframe_copy) + df = two_crows.calc_two_crows(df) # 2 Crows + df = three_black_crows.calc_three_black_crows(df) # Three black crows + df = doji_star.doji_star(df) # Doji Star + df = rsi.rsi(df) # RSI + # Overlap Studies - #dataframe = bollinger_bands.calc_bollinger_bands(dataframe, 20, 2, 2, 0) - return dataframe_copy \ No newline at end of file + # df = bollinger_bands.calc_bollinger_bands(df, 20, 2, 2, 0) + return df diff --git a/indicator_lib/doji_star.py b/indicator_lib/doji_star.py index 90e61ca..e934135 100644 --- a/indicator_lib/doji_star.py +++ b/indicator_lib/doji_star.py @@ -1,39 +1,31 @@ import talib import display_lib + def doji_star(dataframe, display=False, fig=None): """ Function to calculate the doji star indicator. This is a candlestick pattern, more details can be found here: https://medium.com/me/stats/post/28c12f04caf6 - :param data: dataframe object where the Doji Star patterns should be detected on + :param dataframe: dataframe object where the Doji Star patterns should be detected on :param display: boolean to determine whether the Doji Star patterns should be displayed on the graph :param fig: plotly figure object to add the Doji Star patterns to :return: dataframe with Doji Star patterns identified """ - # Copy the dataframe - dataframe = dataframe.copy() - # Add doji star column to dataframe - dataframe['doji_star'] = 0 - # Calculate doji star on dataframe - - dataframe['doji_star'] = talib.CDLDOJISTAR( - dataframe['open'], - dataframe['high'], - dataframe['low'], - dataframe['close'] - ) - # If display is true, add the doji star to the graph - if display: - # Add a column to the dataframe which sets the doji_star_value to be the close price of the relevant candle if the value is not zero - dataframe['doji_star_value'] = dataframe.apply(lambda x: x['close'] if x['doji_star'] != 0 else 0, axis=1) + df = dataframe.copy() + df['doji_star'] = 0 # Add doji star column + # Calculate doji star + df['doji_star'] = talib.CDLDOJISTAR(df['open'], df['high'], df['low'], df['close']) + if display: # add the doji star to the graph + # Add a doji_star_value column to be the close price of the relevant candle if the value is not zero + df['doji_star_value'] = df.apply(lambda x: x['close'] if x['doji_star'] != 0 else 0, axis=1) # Extract the rows where doji_star_value is not zero - dataframe = dataframe[dataframe['doji_star_value'] != 0] + df = df[df['doji_star_value'] != 0] # Add doji star to graph fig = display_lib.add_markers_to_graph( base_fig=fig, - dataframe=dataframe, + dataframe=df, value_column='doji_star_value', point_names='Doji Star' ) - return dataframe + return df diff --git a/indicator_lib/ema_calculator.py b/indicator_lib/ema_calculator.py index ba97b8d..fb2df99 100644 --- a/indicator_lib/ema_calculator.py +++ b/indicator_lib/ema_calculator.py @@ -1,26 +1,20 @@ import pandas -# Define function to calculate an arbitrary EMA def calc_ema(dataframe, ema_size): - # Create column string - ema_name = "ema_" + str(ema_size) - # Create the multiplier + ema_name = f"ema_{ema_size}" # column string multiplier = 2/(ema_size + 1) # Calculate the initial value (SMA) initial_mean = dataframe['close'].head(ema_size).mean() - # Iterate through Dataframe for i in range(len(dataframe)): - # If i equals the ema_size, substitute the first value (SMA) - if i == ema_size: + if i == ema_size: # substitute the first value (SMA) dataframe.loc[i, ema_name] = initial_mean # For subsequent values, use the previous EMA value to calculate the current row EMA elif i > ema_size: ema_value = dataframe.loc[i, 'close'] * multiplier + dataframe.loc[i-1, ema_name]*(1-multiplier) dataframe.loc[i, ema_name] = ema_value - # Input a value of zero else: - dataframe.loc[i, ema_name] = 0.00 - # Once loop completed, return the updated dataframe - return dataframe \ No newline at end of file + dataframe.loc[i, ema_name] = 0.0 + + return dataframe diff --git a/indicator_lib/ema_cross.py b/indicator_lib/ema_cross.py index 241fc71..f6eb7a0 100644 --- a/indicator_lib/ema_cross.py +++ b/indicator_lib/ema_cross.py @@ -3,20 +3,18 @@ # Function to identify ema cross events def ema_cross(dataframe, ema_one, ema_two): - ''' + """ Function to identify ema cross events :param dataframe: Pandas Dataframe object :param ema_one: Column One of EMA cross :param ema_two: Column Two of EMA cross :return: - ''' - # Create a position column + """ + # Create a position & pre_position columns dataframe['position'] = dataframe[ema_one] > dataframe[ema_two] - # Create a preposition column dataframe['pre_position'] = dataframe['position'].shift(1) - # Get rid of NA values + dataframe.dropna(inplace=True) - # Define Crossover events dataframe['crossover'] = np.where(dataframe['position'] == dataframe['pre_position'], False, True) - return dataframe + return dataframe diff --git a/indicator_lib/rsi.py b/indicator_lib/rsi.py index 1a1fa96..16ea4f1 100644 --- a/indicator_lib/rsi.py +++ b/indicator_lib/rsi.py @@ -2,7 +2,6 @@ import display_lib -# Function to calculate the RSI indicator def rsi(dataframe, period=14, display=False, fig=None): """ Function to calculate the RSI indicator. More details can be found here: https://appnologyjames.medium.com/how-to-add-the-rsi-indicator-to-your-algorithmic-python-trading-bot-bf5795756365 @@ -12,20 +11,18 @@ def rsi(dataframe, period=14, display=False, fig=None): :param fig: plotly figure object to add the RSI to :return: dataframe with RSI column added """ - # Copy the dataframe - dataframe = dataframe.copy() - # Add RSI column to dataframe - dataframe['rsi'] = 0 - # Calculate RSI on dataframe - dataframe['rsi'] = talib.RSI(dataframe['close'], timeperiod=period) - # If display is true, add the RSI to the graph - if display: + df = dataframe.copy() + # Calculate RSI col on dataframe + df['rsi'] = 0 + df['rsi'] = talib.RSI(df['close'], timeperiod=period) + + if display: # add the RSI to the graph # todo: Figure out how to make a subplot with RSI - # Add RSI to graph fig = display_lib.add_line_to_graph( base_fig=fig, - dataframe=dataframe, + dataframe=df, dataframe_column='rsi', line_name='RSI' ) - return dataframe + + return df diff --git a/indicator_lib/ta_ema.py b/indicator_lib/ta_ema.py index 6c041ca..bbfba3d 100644 --- a/indicator_lib/ta_ema.py +++ b/indicator_lib/ta_ema.py @@ -1,11 +1,8 @@ import talib -# Function to calculate an EMA def calc_ema(dataframe, periods): - # Create the column title - column_title = "ta_ema_" + str(periods) - # Calculate - dataframe[column_title] = talib.EMA(dataframe['close'], periods) - # Return + """calculate EMA""" + column_title = f"ta_ema_{periods}" + dataframe[column_title] = talib.EMA(dataframe['close'], periods) # Calculate return dataframe diff --git a/indicator_lib/ta_sma.py b/indicator_lib/ta_sma.py index 14b3b83..40d63a3 100644 --- a/indicator_lib/ta_sma.py +++ b/indicator_lib/ta_sma.py @@ -1,12 +1,10 @@ import talib -# Function to calculate an SMA with TA-Lib + def calc_ta_sma(dataframe, periods): - # Copy the dataframe - dataframe = dataframe.copy() - # Define new title for column - column_title = "ta_sma_" + str(periods) - # Calculate - dataframe[column_title] = talib.SMA(dataframe['close'], periods) - # Return dataframe - return dataframe \ No newline at end of file + """calculate an SMA with TA-Lib""" + df = dataframe.copy() + column_title = f"ta_sma_{periods}" + df[column_title] = talib.SMA(df['close'], periods) # Calculate + + return df diff --git a/indicator_lib/three_black_crows.py b/indicator_lib/three_black_crows.py index 6e7e8a8..c0e0d3d 100644 --- a/indicator_lib/three_black_crows.py +++ b/indicator_lib/three_black_crows.py @@ -1,9 +1,8 @@ import talib -# Function to calculate the three black crows indicator def calc_three_black_crows(dataframe): - # Define a new title for the column + """calculate the three black crows indicator""" column_title = "ta_three_b_crows" # Calculate dataframe[column_title] = talib.CDL3BLACKCROWS( @@ -12,4 +11,5 @@ def calc_three_black_crows(dataframe): dataframe['low'], dataframe['close'] ) - return dataframe \ No newline at end of file + + return dataframe diff --git a/indicator_lib/two_crows.py b/indicator_lib/two_crows.py index b24838c..1a28c7f 100644 --- a/indicator_lib/two_crows.py +++ b/indicator_lib/two_crows.py @@ -1,8 +1,8 @@ import talib -# Function to calculate the two crows indicator + def calc_two_crows(dataframe): - # Define new title for column + """calculate the two crows indicator""" column_title = "ta_two_crows" # Calculate dataframe[column_title] = talib.CDL2CROWS( @@ -11,4 +11,4 @@ def calc_two_crows(dataframe): dataframe['low'], dataframe['close'] ) - return dataframe \ No newline at end of file + return dataframe diff --git a/main.py b/main.py index 0a65850..51b438f 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ import json import os from metatrader_lib import mt5_interaction -import pandas +import pandas as pd import display_lib from sql_lib import sql_interaction from strategies import ema_cross @@ -26,17 +26,13 @@ def get_project_settings(import_filepath): :return: JSON object with project settings """ # Test the filepath to sure it exists - if os.path.exists(import_filepath): - # Open the file - f = open(import_filepath, "r") - # Get the information from file + if not os.path.exists(import_filepath): + raise ImportError + + with open(import_filepath) as f: project_settings = json.load(f) - # Close the file - f.close() - # Return project settings to program - return project_settings - else: - return ImportError + + return project_settings def check_exchanges(project_settings): @@ -66,11 +62,9 @@ def check_exchanges(project_settings): print("MT5 Paper Connection Error") raise PermissionError - # Return True if all steps pass return True -# Function to add arguments to script def add_arguments(parser): """ Function to add arguments to the parser @@ -78,99 +72,54 @@ def add_arguments(parser): :return: updated parser object """ # Add Options - # Explore Option - parser.add_argument( - "-e", - "--Explore", - help="Use this to explore the data", - action="store_true" - ) - # Display Option - parser.add_argument( - "-d", - "--Display", - help="Use this to display the data", - action="store_true" - ) - # All Indicators Option - parser.add_argument( - "-a", - "--all_indicators", - help="Select all indicator_lib", - action="store_true" - ) - # Doji Star Option - parser.add_argument( - "--doji_star", - help="Select doji star indicator to be calculated", - action="store_true" - ) - # RSI Option - parser.add_argument( - "--rsi", - help="Select RSI indicator to be calculated", - action="store_true" - ) - - # Add Arguments - parser.add_argument( - "-x", - "--Exchange", - help="Set which exchange you will be using" - ) - # Custom Symbol - parser.add_argument( - "--symbol", - help="Use this to use a custom symbol with the Explore option" - ) - # Custom Timeframe - parser.add_argument( - "-t", - "--timeframe", - help="Select a timeframe to explore data" - ) + parser.add_argument("-e", "--Explore", help="Use this to explore the data", action="store_true") + parser.add_argument("-d", "--Display", help="Use this to display the data", action="store_true") + parser.add_argument("-a", "--all_indicators", help="Select all indicator_lib", action="store_true") + parser.add_argument("--doji_star", help="Select doji star indicator to be calculated", action="store_true") + parser.add_argument("--rsi", help="Select RSI indicator to be calculated", action="store_true") + parser.add_argument("-x", "--Exchange", help="Set which exchange you will be using") + parser.add_argument("--symbol", help="Use this to use a custom symbol with the Explore option") + parser.add_argument("-t", "--timeframe", help="Select a timeframe to explore data") return parser -# Function to parse provided options def parse_arguments(args_parser_variable): """ Function to parse provided arguments and improve from there :param args_parser_variable: :return: True when completed """ - - # Check if data exploration selected if args_parser_variable.Explore: print("Data exploration selected") # Check for exchange - if args_parser_variable.Exchange: - if args_parser_variable.Exchange == "metatrader": - global exchange - exchange = "mt5" - print(f"Exchange selected: {exchange}") - # Check for Timeframe - if args_parser_variable.timeframe: - print(f"Timeframe selected: {args_parser_variable.timeframe}") - else: - print("No timeframe selected") - raise SystemExit(1) - # Check for Symbol - if args_parser_variable.symbol: - print(f"Symbol selected: {args_parser_variable.symbol}") - else: - print("No symbol selected") - raise SystemExit(1) - return True - else: + if not args_parser_variable.Exchange: print("No exchange selected") raise SystemExit(1) + if args_parser_variable.Exchange == "metatrader": + global exchange + exchange = "mt5" + print(f"Exchange selected: {exchange}") + + # Check for Timeframe + if args_parser_variable.timeframe: + print(f"Timeframe selected: {args_parser_variable.timeframe}") + else: + print("No timeframe selected") + raise SystemExit(1) + + # Check for Symbol + if not args_parser_variable.symbol: + print("No symbol selected") + raise SystemExit(1) + + print(f"Symbol selected: {args_parser_variable.symbol}") + return True + return False -# Function to manage data exploration def manage_exploration(args): """ Function to manage data exploration when --Explore option selected @@ -184,25 +133,22 @@ def manage_exploration(args): timeframe=args.timeframe, number_of_candles=1000 ) - # Convert to a dataframe - data = pandas.DataFrame(data) + data = pd.DataFrame(data) # Retrieve whatever indicator_lib have been selected # If all indicators selected, calculate all of them if args.all_indicators: print(f"All indicators selected. Calculation may take some time") - indicator_dataframe = calc_all_indicators.all_indicators( - dataframe=data - ) + indicator_dataframe = calc_all_indicators.all_indicators(dataframe=data) return indicator_dataframe else: - # If display is true, construct the base figure - if args.Display: + if args.Display: # construct the base figure # Add a column 'human_time' to the dataframe which converts the unix time to human readable data['human_time'] = data['time'].apply(lambda x: datetime.datetime.fromtimestamp(x).strftime('%Y-%m-%d %H:%M:%S')) fig = display_lib.construct_base_candlestick_graph( dataframe=data, candlestick_title=f"{args.symbol} {args.timeframe} Data Explorer" ) + # Check for doji_star if args.doji_star and args.Display: print(f"Doji Star selected with display") @@ -211,6 +157,7 @@ def manage_exploration(args): display=True, fig=fig ) + # Check for RSI if args.rsi: print(f"RSI selected") @@ -226,6 +173,7 @@ def manage_exploration(args): indicator_dataframe = doji_star.doji_star( dataframe=data ) + # Check for RSI if args.rsi: print(f"RSI selected") @@ -242,10 +190,8 @@ def manage_exploration(args): dash=False ) - # Once all indicators have been calculated, return the dataframe + # Once all indicators have been calculated, return the dataframe return indicator_dataframe - - else: print("No exchange selected") raise SystemExit(1) @@ -253,26 +199,20 @@ def manage_exploration(args): # Press the green button in the gutter to run the script. if __name__ == '__main__': - # Import project settings project_settings = get_project_settings(import_filepath=import_filepath) - # Check exchanges check_exchanges(project_settings) # Show all columns pandas - pandas.set_option('display.max_columns', None) - #pandas.set_option('display.max_rows', None) + pd.set_option('display.max_columns', None) + # pd.set_option('display.max_rows', None) # Setup arguments to the script parser = argparse.ArgumentParser() # Update with options parser = add_arguments(parser=parser) - # Get the arguments args = parser.parse_args() explore = parse_arguments(args_parser_variable=args) - # Branch based upon options + if explore: manage_exploration(args=args) else: data = manage_exploration(args=args) print(data) - - - diff --git a/metatrader_lib/mt5_interaction.py b/metatrader_lib/mt5_interaction.py index ee3dc80..b0ed333 100644 --- a/metatrader_lib/mt5_interaction.py +++ b/metatrader_lib/mt5_interaction.py @@ -6,7 +6,6 @@ import exceptions -# Function to start Meta Trader 5 (MT5) def start_mt5(username, password, server, path): """ Initializes and logs into MT5 @@ -17,34 +16,33 @@ def start_mt5(username, password, server, path): :return: True if successful, Error if not """ # Ensure that all variables are the correct type - uname = int(username) # Username must be an int - pword = str(password) # Password must be a string - trading_server = str(server) # Server must be a string - filepath = str(path) # Filepath must be a string + uname = int(username) + pword = str(password) + trading_server = str(server) + filepath = str(path) - # Attempt to start MT5 + # start MT5 try: metaTrader_init = MetaTrader5.initialize(login=uname, password=pword, server=trading_server, path=filepath) except Exception as e: print(f"Error initializing MetaTrader: {e}") raise exceptions.MetaTraderInitializeError - # Attempt to login to MT5 + # login to MT5 if not metaTrader_init: raise exceptions.MetaTraderInitializeError - else: - try: - metaTrader_login = MetaTrader5.login(login=uname, password=pword, server=trading_server) - except Exception as e: - print(f"Error loging in to MetaTrader: {e}") - raise exceptions.MetaTraderLoginError + + try: + metaTrader_login = MetaTrader5.login(login=uname, password=pword, server=trading_server) + except Exception as e: + print(f"Error loging in to MetaTrader: {e}") + raise exceptions.MetaTraderLoginError # Return True if initialization and login are successful if metaTrader_login: return True -# Function to initialize a symbol on MT5 def initialize_symbols(symbol_array): """ Function to initialize a symbol on MT5. Note that different brokers have different symbols. @@ -54,9 +52,7 @@ def initialize_symbols(symbol_array): """ # Get a list of all symbols supported in MT5 all_symbols = MetaTrader5.symbols_get() - # Create a list to store all the symbols symbol_names = [] - # Add the retrieved symbols to the list for symbol in all_symbols: symbol_names.append(symbol.name) @@ -64,20 +60,14 @@ def initialize_symbols(symbol_array): for provided_symbol in symbol_array: if provided_symbol in symbol_names: # If it exists, enable - if MetaTrader5.symbol_select(provided_symbol, True): - pass - else: - # Print the outcome to screen. Custom Logging/Error Handling not yet created + if not MetaTrader5.symbol_select(provided_symbol, True): print(f"Error creating symbol {provided_symbol}. Symbol not enabled.") - # Return a generic value error. Custom Error Handling not yet created. raise exceptions.MetaTraderSymbolUnableToBeEnabledError else: - # Print the outcome to screen. Custom Logging/Error Handling not yet created print(f"Symbol {provided_symbol} does not exist in this MT5 implementation. Symbol not enabled.") - # Return a generic syntax error. Custom Error Handling not yet enabled raise exceptions.MetaTraderSymbolDoesNotExistError - # Return true if all symbols enabled - return True + + return True # all symbols enabled # Function to place a trade on MT5 @@ -155,23 +145,24 @@ def place_order(order_type, symbol, volume, stop_loss, take_profit, comment, dir else: # Check the order result = MetaTrader5.order_check(request) - if result[0] == 0: - # print("Balance Check Successful") # Enable to error check Balance Check - # If order check is successful, place the order. Little bit of recursion for fun. - place_order( - order_type=order_type, - symbol=symbol, - volume=volume, - price=price, - stop_loss=stop_loss, - take_profit=take_profit, - comment=comment, - direct=True - ) - else: + if result[0] != 0: print(f"Order unsucessful. Details: {result}") raise exceptions.MetaTraderOrderCheckError + # print("Balance Check Successful") # Enable to error check Balance Check + # If order check is successful, place the order. Little bit of recursion for fun. + place_order( + order_type=order_type, + symbol=symbol, + volume=volume, + price=price, + stop_loss=stop_loss, + take_profit=take_profit, + comment=comment, + direct=True + ) + + # Function to cancel an order def cancel_order(order_number): """ @@ -187,14 +178,13 @@ def cancel_order(order_number): } # Send order to MT5 order_result = MetaTrader5.order_send(request) - if order_result[0] == 10009: - return True - else: + if order_result[0] != 10009: print(f"Error cancelling order. Details: {order_result}") raise exceptions.MetaTraderCancelOrderError + return True + -# Function to modify an open position def modify_position(order_number, symbol, new_stop_loss, new_take_profit): """ Function to modify a position @@ -214,14 +204,13 @@ def modify_position(order_number, symbol, new_stop_loss, new_take_profit): } # Send order to MT5 order_result = MetaTrader5.order_send(request) - if order_result[0] == 10009: - return True - else: + if order_result[0] != 10009: print(f"Error modifying position. Details: {order_result}") raise exceptions.MetaTraderModifyPositionError + return True + -# Function to retrieve all open orders from MT5 def get_open_orders(): """ Function to retrieve a list of open orders from MetaTrader 5 @@ -234,19 +223,15 @@ def get_open_orders(): return order_array -# Function to retrieve all open positions def get_open_positions(): """ Function to retrieve a list of open orders from MetaTrader 5 :return: list of positions """ - # Get position objects positions = MetaTrader5.positions_get() - # Return position objects return positions -# Function to close an open position def close_position(order_number, symbol, volume, order_type, price, comment): """ Function to close an open position from MetaTrader 5 @@ -275,66 +260,50 @@ def close_position(order_number, symbol, volume, order_type, price, comment): # Place the order result = MetaTrader5.order_send(request) - if result[0] == 10009: - return True - else: + if result[0] != 10009: print(f"Error closing position. Details: {result}") raise exceptions.MetaTraderClosePositionError + return True + # Function to convert a timeframe string in MetaTrader 5 friendly format def set_query_timeframe(timeframe): - # Implement a Pseudo Switch statement. Note that Python 3.10 implements match / case but have kept it this way for - # backwards integration - if timeframe == "M1": - return MetaTrader5.TIMEFRAME_M1 - elif timeframe == "M2": - return MetaTrader5.TIMEFRAME_M2 - elif timeframe == "M3": - return MetaTrader5.TIMEFRAME_M3 - elif timeframe == "M4": - return MetaTrader5.TIMEFRAME_M4 - elif timeframe == "M5": - return MetaTrader5.TIMEFRAME_M5 - elif timeframe == "M6": - return MetaTrader5.TIMEFRAME_M6 - elif timeframe == "M10": - return MetaTrader5.TIMEFRAME_M10 - elif timeframe == "M12": - return MetaTrader5.TIMEFRAME_M12 - elif timeframe == "M15": - return MetaTrader5.TIMEFRAME_M15 - elif timeframe == "M20": - return MetaTrader5.TIMEFRAME_M20 - elif timeframe == "M30": - return MetaTrader5.TIMEFRAME_M30 - elif timeframe == "H1": - return MetaTrader5.TIMEFRAME_H1 - elif timeframe == "H2": - return MetaTrader5.TIMEFRAME_H2 - elif timeframe == "H3": - return MetaTrader5.TIMEFRAME_H3 - elif timeframe == "H4": - return MetaTrader5.TIMEFRAME_H4 - elif timeframe == "H6": - return MetaTrader5.TIMEFRAME_H6 - elif timeframe == "H8": - return MetaTrader5.TIMEFRAME_H8 - elif timeframe == "H12": - return MetaTrader5.TIMEFRAME_H12 - elif timeframe == "D1": - return MetaTrader5.TIMEFRAME_D1 - elif timeframe == "W1": - return MetaTrader5.TIMEFRAME_W1 - elif timeframe == "MN1": - return MetaTrader5.TIMEFRAME_MN1 - else: + switch = { + "M1": MetaTrader5.TIMEFRAME_M1, + "M2": MetaTrader5.TIMEFRAME_M2, + "M3": MetaTrader5.TIMEFRAME_M3, + "M4": MetaTrader5.TIMEFRAME_M4, + "M5": MetaTrader5.TIMEFRAME_M5, + "M6": MetaTrader5.TIMEFRAME_M6, + "M10": MetaTrader5.TIMEFRAME_M10, + "M12": MetaTrader5.TIMEFRAME_M12, + "M15": MetaTrader5.TIMEFRAME_M15, + "M20": MetaTrader5.TIMEFRAME_M20, + "M30": MetaTrader5.TIMEFRAME_M30, + + "H1": MetaTrader5.TIMEFRAME_H1, + "H2": MetaTrader5.TIMEFRAME_H2, + "H3": MetaTrader5.TIMEFRAME_H3, + "H4": MetaTrader5.TIMEFRAME_H4, + "H6": MetaTrader5.TIMEFRAME_H6, + "H8": MetaTrader5.TIMEFRAME_H8, + "H12": MetaTrader5.TIMEFRAME_H12, + + "D1": MetaTrader5.TIMEFRAME_D1, + "W1": MetaTrader5.TIMEFRAME_W1, + "MN1": MetaTrader5.TIMEFRAME_MN1, + } + + if timeframe not in switch: print(f"Incorrect timeframe provided. {timeframe}") raise ValueError + return switch[timeframe] + -# Function to query previous candlestick data from MT5 def query_historic_data(symbol, timeframe, number_of_candles): + """query previous candlestick data from MT5""" # Convert the timeframe into an MT5 friendly format mt5_timeframe = set_query_timeframe(timeframe) # Retrieve data from MT5 @@ -342,7 +311,6 @@ def query_historic_data(symbol, timeframe, number_of_candles): return rates -# Function to retrieve latest tick for a symbol def retrieve_latest_tick(symbol): """ Function to retrieve the latest tick for a symbol @@ -351,13 +319,12 @@ def retrieve_latest_tick(symbol): """ # Retrieve the tick information tick = MetaTrader5.symbol_info_tick(symbol)._asdict() - spread = tick['ask'] - tick['bid'] - tick['spread'] = spread + tick['spread'] = tick['ask'] - tick['bid'] return tick -# Function to retrieve ticks from a time range def retrieve_tick_time_range(start_time_utc, finish_time_utc, symbol, dataframe=False): + """retrieve ticks from a time range""" # Set option in MT5 terminal for Unlimited bars # Check time format of start time if type(start_time_utc) != datetime.datetime: @@ -371,11 +338,8 @@ def retrieve_tick_time_range(start_time_utc, finish_time_utc, symbol, dataframe= ticks = MetaTrader5.copy_ticks_range(symbol, start_time_utc, finish_time_utc, MetaTrader5.COPY_TICKS_INFO) # Convert into dataframe only if Dataframe set to True if dataframe: - # Convert into a dataframe ticks_data_frame = pandas.DataFrame(ticks) - # Add spread ticks_data_frame['spread'] = ticks_data_frame['ask'] - ticks_data_frame['bid'] - # Add symbol ticks_data_frame['symbol'] = symbol # Format integers into signed integers (postgres doesn't support unsigned int) ticks_data_frame['time'] = ticks_data_frame['time'].astype('int64') @@ -385,8 +349,8 @@ def retrieve_tick_time_range(start_time_utc, finish_time_utc, symbol, dataframe= return ticks -# Function to retrieve candlestick data for a specified time range def retrieve_candlestick_data_range(start_time_utc, finish_time_utc, symbol, timeframe, dataframe=False): + """retrieve candlestick data for a specified time range""" # Set option in MT5 terminal for Unlimited bars # Check time format of start time if type(start_time_utc) != datetime.datetime: @@ -401,9 +365,7 @@ def retrieve_candlestick_data_range(start_time_utc, finish_time_utc, symbol, tim # Retrieve the data candlestick_data = MetaTrader5.copy_rates_range(symbol, timeframe_value, start_time_utc, finish_time_utc) if dataframe: - # Convert to a dataframe candlestick_dataframe = pandas.DataFrame(candlestick_data) - # Add in symbol and timeframe columns candlestick_dataframe['symbol'] = symbol candlestick_dataframe['timeframe'] = timeframe # Convert integers into signed integers (postgres doesn't support unsigned int) @@ -411,9 +373,5 @@ def retrieve_candlestick_data_range(start_time_utc, finish_time_utc, symbol, tim candlestick_dataframe['tick_volume'] = candlestick_dataframe['tick_volume'].astype('float64') candlestick_dataframe['spread'] = candlestick_dataframe['spread'].astype('int64') candlestick_dataframe['real_volume'] = candlestick_dataframe['real_volume'].astype('int64') - # Return completed dataframe return candlestick_dataframe - else: - return candlestick_data - - + return candlestick_data diff --git a/sql_lib/sql_interaction.py b/sql_lib/sql_interaction.py index 6254566..2410364 100644 --- a/sql_lib/sql_interaction.py +++ b/sql_lib/sql_interaction.py @@ -1,7 +1,7 @@ import psycopg2 import psycopg2.extras from sqlalchemy import create_engine -import pandas +import pandas as pd # Function to connect to PostgreSQL database import exceptions @@ -28,7 +28,6 @@ def postgres_connect(project_settings): return False -# Function to execute SQL def sql_execute(sql_query, project_settings): """ Function to execute SQL statements @@ -40,11 +39,8 @@ def sql_execute(sql_query, project_settings): # Execute the query try: # print(sql_query) - # Create the cursor cursor = conn.cursor() - # Execute the cursor query cursor.execute(sql_query) - # Commit the changes conn.commit() return True except (Exception, psycopg2.Error) as e: @@ -56,10 +52,10 @@ def sql_execute(sql_query, project_settings): conn.close() -# Function to create a table def create_sql_table(table_name, table_details, project_settings, id=True): """ Function to create a table in SQL + :param id: ? :param table_name: String :param table_details: String :param project_settings: JSON Object @@ -72,11 +68,12 @@ def create_sql_table(table_name, table_details, project_settings, id=True): else: # Create without an auto incrementing primary key sql_query = f"CREATE TABLE {table_name} (id BIGINT NOT NULL, {table_details})" - # Execute the query + create_table = sql_execute(sql_query=sql_query, project_settings=project_settings) - if create_table: - return True - raise exceptions.SQLTableCreationError + if not create_table: + raise exceptions.SQLTableCreationError + + return True # Function to create a balance tracking table @@ -180,11 +177,9 @@ def insert_trade_action(table_name, trade_information, project_settings, backtes elif backtest: sql_query = f"INSERT INTO {table_name} " else: - # Return an exception - return Exception # Custom Error Handling Coming Soon + raise Exception # Custom Error Handling Coming Soon -# Function to insert a live trade action into SQL database def insert_live_trade_action(trade_information, project_settings): """ Function to insert a row of trade data into the table live_trade_table @@ -199,7 +194,6 @@ def insert_live_trade_action(trade_information, project_settings): ) -# Function to insert a paper trade action into SQL database def insert_paper_trade_action(trade_information, project_settings): """ Function to insert a row of trade data into the table paper_trade_table @@ -214,8 +208,8 @@ def insert_paper_trade_action(trade_information, project_settings): ) -# Function to create a backtest tick table def create_mt5_backtest_tick_table(table_name, project_settings): + """create a backtest tick table""" # Define the columns in the table table_details = f"symbol VARCHAR(100) NOT NULL," \ f"time BIGINT NOT NULL," \ @@ -305,26 +299,15 @@ def retrieve_dataframe(table_name, project_settings, chunky=False, tick_data=Fal else: sql_query = f"SELECT * FROM {table_name} ORDER BY time;" if chunky: - # Set the chunk size chunk_size = 10000 # You may need to adjust this based upon your processor - # Set up database chunking - db_connection = engine.connect().execution_options( - max_row_buffer=chunk_size - ) - # Retrieve the data - dataframe = pandas.read_sql(sql_query, db_connection, chunksize=chunk_size) - # Close the connection + db_connection = engine.connect().execution_options(max_row_buffer=chunk_size) + dataframe = pd.read_sql(sql_query, db_connection, chunksize=chunk_size) db_connection.close() - # Return the dataframe return dataframe else: - # Standard DB connection db_connection = engine.connect() - # Retrieve the data - dataframe = pandas.read_sql(sql_query, db_connection) - # Close the connection + dataframe = pd.read_sql(sql_query, db_connection) db_connection.close() - # Return the dataframe return dataframe diff --git a/strategies/ema_cross.py b/strategies/ema_cross.py index 0cbdc87..771291c 100644 --- a/strategies/ema_cross.py +++ b/strategies/ema_cross.py @@ -1,7 +1,7 @@ -''' +""" Assumptions: 1. All strategy is performed on an existing dataframe. Previous inputs define how dataframe is retrieved/created -''' +""" from indicator_lib import ema_cross import display_lib from backtest_lib import backtest_analysis @@ -74,29 +74,21 @@ def ema_cross_strategy(dataframe, risk_ratio=1, backtest=True, display=True, upl return trade_dataframe else: last_event = order_dataframe.tail(1) - if last_event['valid'] == True: + if last_event['valid']: return last_event return False # Determine order type and values def determine_order(dataframe, ema_one, ema_two, pip_size, risk_ratio, backtest=True): - """ - - :param dataframe: - :param risk_amount: - :param backtest: - :return: - """ # Set up Pip movement # Determine direction - dataframe['direction'] = dataframe[ema_one] > dataframe[ema_one].shift(1) # I.e. trending up - # Add in stop loss + dataframe['direction'] = dataframe[ema_one] > dataframe[ema_one].shift(1) # I.e. trending up dataframe['stop_loss'] = dataframe[ema_two] cross_events = dataframe # Calculate stop loss for index, row in cross_events.iterrows(): - if row['direction'] == True: + if row['direction']: # Order type will be a BUY_STOP cross_events.loc[index, 'order_type'] = "BUY_STOP" # Calculate the distance between the low and the stop loss @@ -111,7 +103,6 @@ def determine_order(dataframe, ema_one, ema_two, pip_size, risk_ratio, backtest= # Set the entry price as 10 pips above the high stop_price = row['high'] + 10 * pip_size cross_events.loc[index, 'stop_price'] = stop_price - else: # Order type will be a SELL STOP cross_events.loc[index, 'order_type'] = "SELL_STOP" @@ -128,7 +119,7 @@ def determine_order(dataframe, ema_one, ema_two, pip_size, risk_ratio, backtest= cross_events.loc[index, 'stop_price'] = stop_price for index, row in cross_events.iterrows(): - if row['crossover'] == True: + if row['crossover']: if row['order_type'] == "BUY_STOP": if row['take_profit'] > row['stop_price'] > row['stop_loss']: valid = True @@ -141,4 +132,3 @@ def determine_order(dataframe, ema_one, ema_two, pip_size, risk_ratio, backtest= cross_events.loc[index, 'valid'] = False return cross_events - diff --git a/strategies/ema_triple_cross.py b/strategies/ema_triple_cross.py index b41eee3..22ca336 100644 --- a/strategies/ema_triple_cross.py +++ b/strategies/ema_triple_cross.py @@ -1,7 +1,7 @@ -''' +""" Assumptions: 1. All strategy is performed on an existing dataframe. Previous inputs define how dataframe is retrieved/created -''' +""" from indicator_lib import ema_cross import display_lib from backtest_lib import backtest_analysis @@ -35,4 +35,3 @@ def ema_triple_cross_strategy(dataframe, risk_ratio=1, display=True, show=False) dataframe_column="ta_ema_200", line_name="EMA 200" ) - diff --git a/strategies/engulfing_candle_strategy.py b/strategies/engulfing_candle_strategy.py index 702f450..ebdf982 100644 --- a/strategies/engulfing_candle_strategy.py +++ b/strategies/engulfing_candle_strategy.py @@ -1,7 +1,3 @@ - - - -# Function to respond to engulfing candle detections and turn them into a strategy def engulfing_candle_strategy(high, low, symbol, timeframe, exchange, alert_type, project_settings): """ Function to respond to engulfing candle detections and turn them into a strategy @@ -15,26 +11,19 @@ def engulfing_candle_strategy(high, low, symbol, timeframe, exchange, alert_type :return: """ # Only apply strategy to specified timeframes - if timeframe == "M15" or timeframe == "M30" or timeframe == "H1" or timeframe == "D1": - # Respond to bullish_engulfing + if timeframe in ["M15", "M30", "H1", "D1"]: if alert_type == "bullish_engulfing": - # Set the Trade Type trade_type = "BUY" - # Set the Take Profit take_profit = high + high - low - # Set the Buy Stop entry_price = high - # Set the Stop Loss stop_loss = low elif alert_type == "bearish_engulfing": - # Set the Trade Type trade_type = "SELL" - # Set the Take Profit take_profit = low - high + low - # Set the Sell Stop entry_price = low - # Set the Stop Loss stop_loss = high - # Print the result to the screen + else: + raise Exception + print(f"Trade Signal Detected. Symbol: {symbol}, Trade Type: {trade_type}, Take Profit: {take_profit}, " f"Entry Price: {entry_price}, Stop Loss: {stop_loss}, Exchange: {exchange}") diff --git a/tests/test_mt5_interaction.py b/tests/test_mt5_interaction.py index de53afc..6b4b0aa 100644 --- a/tests/test_mt5_interaction.py +++ b/tests/test_mt5_interaction.py @@ -2,12 +2,10 @@ import exceptions import os import json - -# Libraries being tested import main from metatrader_lib import mt5_interaction -# Define project settings location + import_filepath = "test_settings.json" fake_login_details = { @@ -64,7 +62,7 @@ def test_initialize_symbols_real(): def test_place_order_wrong_order_type(): # Fake order with pytest.raises(SyntaxError) as e: - mt5_interaction.place_order("WRONG_TYPE", "BTCUSD.a", 0.0, 0.0,0.0,"comment") + mt5_interaction.place_order("WRONG_TYPE", "BTCUSD.a", 0.0, 0.0, 0.0, "comment") assert e.type == SyntaxError @@ -77,6 +75,7 @@ def test_place_order_incorrect_balance(): test_order['buy_take_profit'], "UnitTestOrder") assert e.type == exceptions.MetaTraderOrderCheckError + # Test that place_order throws an error if incorrect stop_loss def test_place_order_incorrect_stop_loss(): # Get options for a test order @@ -143,14 +142,14 @@ def test_place_order_buy(): # Get options for a test order test_order = get_a_test_order() outcome = mt5_interaction.place_order( - order_type="BUY", - symbol="BTCUSD.a", - volume=test_order['correct_volume'], - stop_loss=test_order['buy_stop_loss'], - take_profit=test_order['buy_take_profit'], - comment="TestTrade" - ) - assert outcome == None + order_type="BUY", + symbol="BTCUSD.a", + volume=test_order['correct_volume'], + stop_loss=test_order['buy_stop_loss'], + take_profit=test_order['buy_take_profit'], + comment="TestTrade" + ) + assert outcome is None # Test that placing SELL order works @@ -164,7 +163,7 @@ def test_place_order_sell(): take_profit=test_order['sell_take_profit'], comment="TestTrade" ) - assert outcome == None + assert outcome is None # Test that placing a BUY_STOP order works @@ -179,7 +178,7 @@ def test_place_order_buy_stop(): comment="TestTrade", price=test_order['correct_buy_stop'] ) - assert outcome == None + assert outcome # Test that placing a SELL_STOP order works @@ -194,7 +193,7 @@ def test_place_order_sell_stop(): comment="TestTrade", price=test_order['correct_sell_stop'] ) - assert outcome == None + assert outcome # Test that canceling a non-existing order throws an error @@ -212,7 +211,7 @@ def test_cancel_order(): # Iterate through and cancel for order in orders: outcome = mt5_interaction.cancel_order(order) - assert outcome == True + assert outcome # Test the ability to modify an open positions stop loss @@ -230,7 +229,7 @@ def test_modify_position_new_stop_loss(): new_stop_loss=new_stop_loss, new_take_profit=position[12] ) - assert outcome == True + assert outcome # Test ability to modify an open positions take profit @@ -248,7 +247,7 @@ def test_modify_position_new_take_profit(): new_stop_loss=position[11], new_take_profit=new_take_profit ) - assert outcome == True + assert outcome # Test ability to modify both take profit and stop loss simultaneously @@ -267,7 +266,8 @@ def test_modify_position(): new_stop_loss=new_stop_loss, new_take_profit=new_take_profit ) - assert outcome == True + assert outcome + # Test Modify Position throws an error def test_modify_position_error(): @@ -335,13 +335,13 @@ def test_close_position(): elif position[5] == 1: order_type = "BUY" outcome = mt5_interaction.close_position( - order_number=position[0], - symbol=position[16], - volume=position[9], - order_type=order_type, - price=position[13], - comment="TestTrade" - ) + order_number=position[0], + symbol=position[16], + volume=position[9], + order_type=order_type, + price=position[13], + comment="TestTrade" + ) assert outcome == True @@ -388,14 +388,13 @@ def test_fractional_close(): symbol=position[16], volume=position[9], order_type="SELL", - price=position[13]-100, + price=position[13] - 100, comment="ComplexTestOrderSell" ) - assert sell_order == True + assert sell_order - -### Helper functions +# Helper functions def get_a_test_order(): # Get the current BTCUSD.a price, Assume balance is not more than $100,000 current_price = mt5_interaction.retrieve_latest_tick("BTCUSD.a")['ask'] @@ -415,4 +414,3 @@ def get_a_test_order(): "correct_volume": 0.1 } return return_object -