├── .gitignore ├── .idea ├── .gitignore ├── dataSources.xml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── python_trading_bot.iml ├── sqlDataSources.xml └── vcs.xml ├── README.md ├── backtest_lib ├── backtest.py ├── backtest_analysis.py └── setup_backtest.py ├── binance_lib └── binance_interaction.py ├── capture_lib └── trade_capture.py ├── coinbase_lib ├── get_account_details.py └── get_candlesticks.py ├── common_information_model.json ├── display_lib.py ├── example_settings.json ├── exceptions.py ├── indicator_lib ├── bearish_engulfing.py ├── bollinger_bands.py ├── bullish_engulfing.py ├── calc_all_indicators.py ├── doji_star.py ├── ema_calculator.py ├── ema_cross.py ├── rsi.py ├── ta_ema.py ├── ta_sma.py ├── three_black_crows.py └── two_crows.py ├── main.py ├── metatrader_lib └── mt5_interaction.py ├── sql_lib └── sql_interaction.py ├── strategies ├── ema_cross.py ├── ema_triple_cross.py └── engulfing_candle_strategy.py └── tests ├── test_mt5_interaction.py ├── test_sql_interaction.py └── test_trade_capture.py /.gitignore: -------------------------------------------------------------------------------- 1 | /settings.json 2 | /tests/test_settings.json 3 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | postgresql 6 | true 7 | org.postgresql.Driver 8 | jdbc:postgresql://localhost:5432/trading_bot_db 9 | 10 | 11 | 12 | 13 | $ProjectFileDir$ 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/python_trading_bot.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/sqlDataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Join Our Community 2 | We love connecting with our audience! Join us on the following links: 3 | 1. Discord: https://discord.gg/wNYYGaMGfd 4 | 2. Telegram: https://t.me/TradeOxySupportBot 5 | 3. TradeOxy Platform: https://www.tradeoxy.com/ 6 | 4. Upcoming Content: https://tradeoxy.notion.site/Content-Creation-Roadmap-5f896060f39341fd9539bcaced8c3b5d 7 | 5. Upcoming Features: https://tradeoxy.notion.site/3f9666718dc24e38bbd4a56a741287ae?v=d810cfa006f54bafa4bbbe3674fefa98&pvs=74 8 | 6. Custom Trading Bot development - https://tradeoxy.notion.site/Trading-Bot-Pricing-Guide-f0ff11b0604b4b998cba2b8da6a129cb?pvs=4 9 | 10 | **All trading is at your own risk :)** 11 | 12 | ## Published Content 13 | 14 | ### How to Install TALib 15 | 1. Windows: [5 Easy Steps to Add TA-Lib to Your Python Trading Bot on Windows](https://medium.com/@appnologyjames/5-easy-steps-to-add-ta-lib-to-your-python-trading-bot-on-windows-16f82fb07788) 16 | 17 | ### YouTube 18 | Our YouTube channel [TradeOxy](https://www.youtube.com/@tradeoxy) contains tons of helpful content on how 19 | to use the AutoTrading Bot or build one for yourself. Check out these episodes: 20 | 1. [Secure Setup](https://www.youtube.com/watch?v=jpw3JltNMg0) 21 | 2. [Connect To MetaTrader 5 with Python](https://www.youtube.com/watch?v=EkP7iAZoMEw&t=2s) 22 | 3. [Retrieve 50000 Candlesticks from MetaTrader](https://www.youtube.com/watch?v=KZmVek6EDCg) 23 | 4. [Add the EMA Indicator to Your Algorithmic AutoTrading Bot](https://youtu.be/QqLjXecrKhc) 24 | 5. [How to Install TALib on Windows](https://youtu.be/jnxqu9MhBIE) 25 | 6. [Build Your Own AutoTrading Bot EMA Cross Detector](https://youtu.be/lbdO_UKEzQU) 26 | 7. [How to Trade the EMA Cross Strategy with Your AutoTrading Bot](https://youtu.be/A6RTl0_13pw) 27 | 8. [How to Convert Your AutoTrading Bot Strategy into BUY and SELL Signals](https://youtu.be/21NtSVuPaZw) 28 | 9. [How to Calculate Lot Size for Your MetaTrader 5 Python Trading Bot](https://youtu.be/fveyPFreenk) 29 | 10. [How to Create Orders with Your MetaTrader Python Trading Bot](https://youtu.be/fveyPFreenk) 30 | 11. [How to Create Orders with Your MetaTrader 5 Python Trading Bot Pt 2](https://youtu.be/nn8XQgFN5W8) 31 | 12. [Advanced Order Management with MetaTrader 5 Python Trading Bot](https://youtu.be/cWfBrDQj_5s) 32 | 13. [Never Miss a CandleStick with Your MetaTrader 5 Python Trading Bot](https://youtu.be/ecK0ZbMWVIA) 33 | 14. [Manage Every Trade with Your MetaTrader 5 Python Trading Bot](https://youtu.be/Q5GQFxk1IJI) 34 | 15. [Multi-Strategy Trading Accounts with MetaTrader 5 Python Trading Bot](https://youtu.be/4NDO81n-EpA) 35 | 36 | ### MetaTrader 5 37 | 1. [Everything You Need to Connect Your Python Trading Bot to MetaTrader 5](https://medium.com/@appnologyjames/build-your-own-algorithmic-trading-bot-with-python-introduction-cb51c6db892e) 38 | 2. [7 Indispensable Trading Functions for Your MetaTrader 5 Python Trading Bot](https://medium.com/geekculture/7-indispensable-trading-functions-for-your-metatrader-5-python-trading-bot-8490d15065d9) - published in [Geek Culture](https://medium.com/geekculture) 39 | 3. [Retrieve Live Price Information and Calculate Spread](https://appnologyjames.medium.com/retrieve-live-price-information-from-metatrader-5-with-your-python-trading-bot-3128994f26d6) 40 | 4. [Level Up Your Trading Bot with Postgres](https://appnologyjames.medium.com/build-foundations-for-trading-bot-excellence-with-postgresql-python-and-metatrader-5-5328b047c2e7) 41 | 42 | ## Related Content 43 | ### Coinbase Tutorials 44 | #### How to Build a Crypto Trading Bot with Coinbase and Python Series 45 | 1. [How to Setup Your Crypto Trading Bot for Coinbase with Python](https://medium.com/@appnologyjames/how-to-connect-to-coinbase-with-python-3-97cf53856fcd). Explains how to set up your code in an extensible format. 46 | 2. [How to Connect to Coinbase with Python 3](https://medium.com/@appnologyjames/how-to-connect-to-coinbase-with-python-3-97cf53856fcd). Shows you how to connect to the Coinbase Pro API. 47 | 3. [How to Identify Engulfing Candles on Coinbase with Python](https://medium.com/@appnologyjames/how-to-identify-engulfing-candles-on-coinbase-with-python-27c6db4eda57). Shows you how to use Python to detect [Bullish Engulfing Patterns](https://www.investopedia.com/terms/b/bullishengulfingpattern.asp) and [Bearish Engulfing Patterns](https://www.investopedia.com/terms/b/bearishengulfingp.asp) in candlesticks. Introduces an advanced detection method as well. 48 | 4. [How to Implement the Engulfing Candle Strategy on Coinbase with Python](https://medium.com/@appnologyjames/how-to-implement-the-engulfing-candle-strategy-on-coinbase-with-python-23ea20dbb502). Shows you how to implement the [Engulfing Candle Strategy](https://www.thebalancemoney.com/engulfing-candle-day-trading-strategy-1030873) using Python and Coinbase Pro REST API 49 | 50 | ## Upcoming Content 51 | ### Exchanges 52 | 1. [Binance](https://www.binance.com/en) 53 | 2. [Bitfinex](https://www.bitfinex.com/) 54 | 3. [Ameritrade/ThinkOrSwim](https://www.tdameritrade.com/) 55 | 4. [Alpaca.Markets](https://alpaca.markets/) 56 | 5. [PancakeSwap DEX](https://pancakeswap.finance/) 57 | 6. [MetaTrader Webtrader](https://trade.mql5.com/trade) 58 | 7. [cTrader](https://ctrader.com/) 59 | 60 | ### Indicators 61 | 1. SMA (Simple Moving Average) 3, 8, 10, 15, 20, 50, 200 62 | 2. EMA (Exponential Moving Average) 3, 8, 10, 15, 20, 50, 200 63 | 3. Stochastic Oscillator 64 | 4. Moving Average Convergence/Divergence 65 | 5. Bollinger Bands 66 | 6. Relative Strength Index 67 | 7. Fibonacci Retracement 68 | 8. Standard Deviation 69 | 9. Average Directional Index (ADI) 70 | 10. On Balance Volume 71 | 11. Accumulation distribution line 72 | 12. Aroon Indicator 73 | 74 | ### Bullish Candlestick Patterns 75 | 1. Hammer 76 | 2. Inverse Hammer 77 | 3. Bullish Engulfing 78 | 4. Piercing Line 79 | 5. Morning Star 80 | 6. Three Soldiers 81 | 82 | ### Bearish Candlestick Patterns 83 | 1. Hanging Man 84 | 2. Shooting Star 85 | 3. Bearish Engulfing 86 | 4. Evening Star 87 | 5. Three Black Crows 88 | 6. Dark Cloud Cover 89 | 90 | ### Continuation Candlestick Patterns 91 | 1. Doji 92 | 2. Spinning Top 93 | 94 | ### Strategies 95 | 1. RSI Strategy 96 | 2. MACD/Bollinger Band Strategy 97 | 3. Moving Average Strategy 98 | 4. Reversal Candle Strategy 99 | 5. Moving Average Cross Strategy 100 | 101 | ### Sharing 102 | 1. Sharing on Twitter 103 | 2. Sharing on Discord 104 | 3. Sharing on Telegram 105 | 4. Adding a GUI 106 | -------------------------------------------------------------------------------- /backtest_lib/backtest.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | import time 3 | from sql_lib import sql_interaction 4 | 5 | trade_object = {} 6 | 7 | 8 | # Function to backtest a strategy 9 | def backtest(valid_trades_dataframe, time_orders_valid, tick_data_table_name, trade_table_name, project_settings, 10 | strategy, symbol, comment, balance_table, valid_trades_table): 11 | print("Starting Backtest script") 12 | # Make sure that SettingWithCopyWarning suppressed 13 | pandas.options.mode.chained_assignment = None 14 | # Add status of pending to orders 15 | valid_trades_dataframe['status'] = "pending" 16 | valid_trades_dataframe['time_valid'] = valid_trades_dataframe['time'] + time_orders_valid 17 | # Save valid trades to postgres 18 | sql_interaction.save_dataframe(valid_trades_dataframe, valid_trades_table, project_settings) 19 | # Setup open_orders 20 | open_orders = pandas.DataFrame() 21 | # Setup open positions 22 | open_buy_positions = pandas.DataFrame() 23 | open_sell_positions = pandas.DataFrame() 24 | # Setup closed 25 | print("Data retrieved, analysis starting") 26 | # Query SQL in chunks 27 | # Create connection 28 | conn = sql_interaction.postgres_connect(project_settings) 29 | # Set up the trading object 30 | trade_object["backtest_settings"] = project_settings["backtest_settings"] 31 | trade_object["current_available_balance"] = trade_object["backtest_settings"]["test_balance"] 32 | trade_object["current_equity"] = 0 33 | trade_object["current_profit"] = 0 34 | trade_object["trade_table_name"] = trade_table_name 35 | trade_object["strategy"] = strategy 36 | trade_object["symbol"] = symbol 37 | trade_object["comment"] = comment 38 | trade_object["balance_tracker_table"] = balance_table 39 | # Create the tables 40 | try: 41 | sql_interaction.create_mt5_backtest_trade_table(table_name=trade_table_name, project_settings=project_settings) 42 | except Exception as e: 43 | print(f"Error creating backtest trade table. {e}") 44 | with conn.cursor(name="backtest_cursor") as cursor: 45 | cursor.itersize = 1000000 46 | query = f"SELECT * FROM {tick_data_table_name} ORDER BY time_msc;" 47 | cursor.execute(query) 48 | tic = time.perf_counter() 49 | # Create the initial balance entry 50 | 51 | for raw_row in cursor: 52 | # Turn row into a dictionary 53 | row = { 54 | "index": raw_row[0], 55 | "symbol": raw_row[1], 56 | "time": raw_row[2], 57 | "bid": raw_row[3], 58 | "ask": raw_row[4], 59 | "spread": raw_row[5], 60 | "last": raw_row[6], 61 | "volume": raw_row[7], 62 | "flags": raw_row[8], 63 | "volume_real": raw_row[9], 64 | "time_msc": raw_row[10], 65 | "human_time": raw_row[11], 66 | "human_time_msc": raw_row[12] 67 | } 68 | # Format time_msc into the milliseconds it should be 69 | row['time_msc'] = row['time_msc'] / 1000 70 | # Convert into a dictionary 71 | # Step 1: Check for orders which have become valid 72 | mask = (valid_trades_dataframe['time'] < row['time_msc']) 73 | if len(valid_trades_dataframe[mask]) > 0: 74 | # Retrieve the valid order 75 | new_data_frame = valid_trades_dataframe[mask] 76 | # Update status 77 | open_orders = new_order( 78 | order_dataframe=open_orders, 79 | new_order=new_data_frame, 80 | row=row, 81 | project_settings=project_settings 82 | ) 83 | # Drop from valid trades dataframe 84 | valid_trades_dataframe = valid_trades_dataframe.drop(valid_trades_dataframe[mask].index) 85 | # Step 2: Check open orders for those which are no longer valid or have reached STOP PRICE 86 | if len(open_orders) > 0: 87 | # Check open orders for those which have expired 88 | mask = (open_orders['time_valid'] < row['time_msc']) 89 | if len(open_orders[mask]) > 0: 90 | open_orders = expire_order( 91 | order_dataframe=open_orders, 92 | expired_order=open_orders[mask], 93 | row=row, 94 | project_settings=project_settings 95 | ) 96 | # Check if BUY_STOP reached 97 | mask = (open_orders['order_type'] == "BUY_STOP") & (open_orders['stop_price'] >= row['bid']) 98 | if len(open_orders[mask]) > 0: 99 | # Add to Open Buy Positions 100 | open_buy_positions = new_position( 101 | position_dataframe=open_buy_positions, 102 | new_position=open_orders[mask], 103 | row=row, 104 | project_settings=project_settings, 105 | comment=comment 106 | ) 107 | # Drop from open orders as now a position 108 | open_orders = open_orders.drop(open_orders[mask].index) 109 | # Check if SELL_STOP reached 110 | mask = (open_orders['order_type'] == "SELL_STOP") & (open_orders['stop_price'] <= row['bid']) 111 | if len(open_orders[mask]) > 0: 112 | # Add to open_sell_positions 113 | open_sell_positions = new_position( 114 | position_dataframe=open_sell_positions, 115 | new_position=open_orders[mask], 116 | row=row, 117 | project_settings=project_settings, 118 | comment=comment 119 | ) 120 | open_orders = open_orders.drop(open_orders[mask].index) 121 | # Step 3: Check open positions to check their progress 122 | # Check open buy positions 123 | if len(open_buy_positions) > 0: 124 | # Check if any open buy positions have reached their TAKE_PROFIT 125 | mask = (open_buy_positions['take_profit'] <= row['bid']) 126 | if len(open_buy_positions[mask]) > 0: 127 | open_buy_positions = buy_take_profit_reached( 128 | position_dataframe=open_buy_positions, 129 | position=open_buy_positions[mask], 130 | row=row, 131 | project_settings=project_settings, 132 | comment=comment 133 | ) 134 | # Check if any open buy positions have reached their STOP_LOSS 135 | mask = (open_buy_positions['stop_loss'] >= row['bid']) 136 | if len(open_buy_positions[mask]) > 0: 137 | open_buy_positions = buy_stop_loss_reached( 138 | position_dataframe=open_buy_positions, 139 | position=open_buy_positions[mask], 140 | row=row, 141 | project_settings=project_settings, 142 | comment=comment 143 | ) 144 | # Check open sell positions 145 | if len(open_sell_positions) > 0: 146 | # Check if any open sell positions have reached their TAKE_PROFIT 147 | mask = (open_sell_positions['take_profit'] >= row['bid']) 148 | if len(open_sell_positions[mask]) > 0: 149 | open_sell_positions = sell_take_profit_reached( 150 | position_dataframe=open_sell_positions, 151 | position=open_sell_positions[mask], 152 | row=row, 153 | project_settings=project_settings, 154 | comment=comment 155 | ) 156 | # Check if any open sell positions have reached a stop loss 157 | mask = (open_sell_positions['stop_loss'] <= row['bid']) 158 | if len(open_sell_positions[mask]) > 0: 159 | open_sell_positions = sell_stop_loss_reached( 160 | position_dataframe=open_sell_positions, 161 | position=open_sell_positions[mask], 162 | row=row, 163 | project_settings=project_settings, 164 | comment=comment 165 | ) 166 | # Return the totals back 167 | total_value = trade_object['current_available_balance'] + trade_object['current_equity'] 168 | # At the conclusion of the testing, close any open orders at the same price brought at 169 | # Get the last tick (for close time) 170 | last_tick = sql_interaction.retrieve_last_tick( 171 | tick_table_name=tick_data_table_name, 172 | project_settings=project_settings 173 | ) 174 | # Get the close time 175 | close_time = last_tick[0][10]/1000 176 | close_open_positions( 177 | open_buy_positions=open_buy_positions, 178 | open_sell_positions=open_sell_positions, 179 | update_time=close_time, 180 | project_settings=project_settings, 181 | comment=comment 182 | ) 183 | print(f"Total value at conclusion of test: {total_value}. Breakdown: " 184 | f"Balance: {trade_object['current_available_balance']}, Equity: {trade_object['current_equity']}") 185 | toc = time.perf_counter() 186 | print(f"Time taken: {toc - tic:0.4f} seconds") 187 | 188 | 189 | # Function to add a new order 190 | def new_order(order_dataframe, new_order, row, project_settings): 191 | # Iterate through the order dataframe 192 | for order in new_order.iterrows(): 193 | order_id = order[0] 194 | order_details = order[1] 195 | # Calculate the amount risked, along with purchase 196 | risk = calc_risk_to_dollars() 197 | # Subtract the amount risked from the available balance as it's no longer available 198 | trade_object['current_available_balance'] = trade_object['current_available_balance'] - risk['risk_amount'] 199 | # Add the amount risked to the current equity 200 | trade_object['current_equity'] = trade_object['current_equity'] + risk['risk_amount'] 201 | print(f"Order Became valid. " 202 | f"Balance: {trade_object['current_available_balance']}, " 203 | f"Equity: {trade_object['current_equity']}, " 204 | f"Order ID: {order_id}, " 205 | f"Order Price: {row['bid']}," 206 | f"Balance Risked: {risk}") 207 | # Update SQL table with order 208 | sql_interaction.insert_order_update( 209 | update_time=row["time_msc"], 210 | trade_type=order_details["order_type"], 211 | stop_loss=order_details["stop_loss"], 212 | take_profit=order_details["take_profit"], 213 | price=order_details["stop_price"], 214 | order_id=order_id, 215 | trade_object=trade_object, 216 | project_settings=project_settings, 217 | status="order", 218 | comment=trade_object["comment"] 219 | ) 220 | # Update SQL balance 221 | sql_interaction.insert_balance_change( 222 | trade_object=trade_object, 223 | note="order_placed", 224 | balance=trade_object['current_available_balance'], 225 | equity=trade_object['current_equity'], 226 | profit_or_loss=0.00, 227 | order_id=order_id, 228 | time=row['time_msc'], 229 | project_settings=project_settings 230 | ) 231 | new_order['amount_risked'] = risk['risk_amount'] 232 | # Update status of new_order dataframe 233 | new_order['status'] = "order" 234 | # Append to order_dataframe 235 | order_dataframe = pandas.concat([order_dataframe, new_order]) 236 | return order_dataframe 237 | 238 | 239 | # Function to expire an order 240 | def expire_order(order_dataframe, expired_order, row, project_settings): 241 | for order in expired_order.iterrows(): 242 | order_id = order[0] 243 | order_details = order[1] 244 | # Update balance 245 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 246 | order_details['amount_risked'] 247 | # Update equity 248 | trade_object['current_equity'] = trade_object['current_equity'] - order_details['amount_risked'] 249 | # Update status of expired_order 250 | expired_order['status'] = "expired" 251 | # Add to SQL 252 | # Update SQL table with order 253 | sql_interaction.insert_order_update( 254 | update_time=row["time_msc"], 255 | trade_type=order_details["order_type"], 256 | stop_loss=order_details["stop_loss"], 257 | take_profit=order_details["take_profit"], 258 | price=order_details["stop_price"], 259 | order_id=order_id, 260 | trade_object=trade_object, 261 | project_settings=project_settings, 262 | status="order", 263 | comment=trade_object["comment"] 264 | ) 265 | # Update SQL table with balance update 266 | sql_interaction.insert_balance_change( 267 | trade_object=trade_object, 268 | note="Order expired", 269 | balance=trade_object['current_available_balance'], 270 | equity=trade_object['current_equity'], 271 | profit_or_loss=0.00, 272 | order_id=order_id, 273 | time=row['time_msc'], 274 | project_settings=project_settings 275 | ) 276 | updated_order_dataframe = order_dataframe.drop(expired_order.index) 277 | # Return updated order dataframe 278 | return updated_order_dataframe 279 | 280 | 281 | # Function to add a new position 282 | def new_position(position_dataframe, new_position, row, comment, project_settings): 283 | for position in new_position.iterrows(): 284 | position_id = position[0] 285 | position_details = position[1] 286 | print(f"Order {position_id} became a position at price {row['bid']}") 287 | volume = position_details['amount_risked'] * trade_object['backtest_settings']['leverage'] / row['bid'] 288 | # No change to balance or equity as already covered in the order 289 | sql_interaction.insert_new_position( 290 | trade_type=position_details['order_type'], 291 | status="opened", 292 | stop_loss=position_details['stop_loss'], 293 | take_profit=position_details['take_profit'], 294 | price=row['bid'], 295 | order_id=position_id, 296 | trade_object=trade_object, 297 | update_time=row['time_msc'], 298 | project_settings=project_settings, 299 | qty_purchased=volume, 300 | entry_price=row['bid'], 301 | comment=comment 302 | ) 303 | # Update status of order 304 | new_position['status'] = "position" 305 | # Append to position dataframe 306 | position_dataframe = pandas.concat([position_dataframe, new_position]) 307 | return position_dataframe 308 | 309 | 310 | # Function when a BUY Take Profit Reached 311 | def buy_take_profit_reached(position_dataframe, position, row, comment, project_settings): 312 | # Query SQL to find what the most recent price take profit was (make future compatible with trailing stop) 313 | for pos_tp in position.iterrows(): 314 | last_trade = sql_interaction.retrieve_last_position( 315 | order_id=pos_tp[0], 316 | trade_object=trade_object, 317 | project_settings=project_settings 318 | ) 319 | # Calculate the volume originally purchased 320 | vol_purchased = last_trade[0][6] 321 | # Calculate the price sold 322 | price_sold = vol_purchased * row['bid'] 323 | # Calculate the profit / loss 324 | outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) 325 | # Update the available balance with the amount risked 326 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome 327 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 328 | pos_tp[1]['amount_risked'] 329 | # Remove the equity risked 330 | trade_object['current_equity'] = trade_object['current_equity'] - pos_tp[1]['amount_risked'] 331 | print(f"BUY Take Profit activated for {pos_tp[0]}. Outcome: {outcome}. Updated Balance: " 332 | f"{trade_object['current_available_balance']}. " 333 | f"Updated Equity: {trade_object['current_equity']}") 334 | 335 | # Update SQL 336 | sql_interaction.position_close( 337 | trade_type=pos_tp[1]['order_type'], 338 | status="closed", 339 | stop_loss=pos_tp[1]['stop_loss'], 340 | take_profit=pos_tp[1]['take_profit'], 341 | price=row['bid'], 342 | order_id=pos_tp[0], 343 | trade_object=trade_object, 344 | update_time=row['time_msc'], 345 | project_settings=project_settings, 346 | entry_price=last_trade[0][17], 347 | exit_price=row['bid'], 348 | qty_purchased=vol_purchased, 349 | trade_stage="position", 350 | comment=comment 351 | ) 352 | # Update the balance 353 | sql_interaction.insert_balance_change( 354 | trade_object=trade_object, 355 | note="Take profit reached", 356 | balance=trade_object["current_available_balance"], 357 | equity=trade_object["current_equity"], 358 | profit_or_loss=outcome, 359 | order_id=pos_tp[0], 360 | time=row['time_msc'], 361 | project_settings=project_settings 362 | ) 363 | # Update status of position 364 | position['status'] = "closed" 365 | # Remove from position dataframe 366 | position_dataframe = position_dataframe.drop(position.index) 367 | return position_dataframe 368 | 369 | 370 | # Function when a BUY Stop Loss reached 371 | def buy_stop_loss_reached(position_dataframe, position, row, comment, project_settings): 372 | # todo: Update SQL Table with outcome 373 | for pos_sl in position.iterrows(): 374 | last_trade = sql_interaction.retrieve_last_position( 375 | order_id=pos_sl[0], 376 | trade_object=trade_object, 377 | project_settings=project_settings 378 | ) 379 | # Calculate the volume originally purchased 380 | vol_purchased = last_trade[0][6] 381 | # Calculate the price sold 382 | price_sold = vol_purchased * row['bid'] 383 | # Calculate the profit / loss 384 | outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) 385 | # Update the available balance with the amount risked 386 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome 387 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 388 | pos_sl[1]['amount_risked'] 389 | # Remove the equity risked 390 | trade_object['current_equity'] = trade_object['current_equity'] - pos_sl[1]['amount_risked'] 391 | print(f"BUY Stop Loss activated for {pos_sl[0]}. " 392 | f"Outcome: {outcome}, " 393 | f"Updated Balance: {trade_object['current_available_balance']}. " 394 | f"Updated Equity: {trade_object['current_equity']}") 395 | 396 | # Update status of position 397 | position['status'] = "closed" 398 | # Update SQL tracking 399 | sql_interaction.position_close( 400 | trade_type=pos_sl[1]['order_type'], 401 | status="closed", 402 | stop_loss=pos_sl[1]['stop_loss'], 403 | take_profit=pos_sl[1]['take_profit'], 404 | price=row['bid'], 405 | order_id=pos_sl[0], 406 | trade_object=trade_object, 407 | update_time=row['time_msc'], 408 | project_settings=project_settings, 409 | entry_price=last_trade[0][17], 410 | exit_price=row['bid'], 411 | qty_purchased=vol_purchased, 412 | trade_stage="position", 413 | comment=comment 414 | ) 415 | # Update the balance 416 | sql_interaction.insert_balance_change( 417 | trade_object=trade_object, 418 | note="Stop Loss reached", 419 | balance=trade_object["current_available_balance"], 420 | equity=trade_object["current_equity"], 421 | profit_or_loss=outcome, 422 | order_id=pos_sl[0], 423 | time=row['time_msc'], 424 | project_settings=project_settings 425 | ) 426 | 427 | position_dataframe = position_dataframe.drop(position.index) 428 | return position_dataframe 429 | 430 | 431 | # Function when a SELL Take Profit reached 432 | def sell_take_profit_reached(position_dataframe, position, row, comment, project_settings): 433 | # todo: Update SQL Table with outcome 434 | for pos_tp in position.iterrows(): 435 | last_trade = sql_interaction.retrieve_last_position( 436 | order_id=pos_tp[0], 437 | trade_object=trade_object, 438 | project_settings=project_settings 439 | ) 440 | # Calculate the volume originally purchased 441 | vol_purchased = last_trade[0][6] 442 | # Calculate the price sold 443 | price_sold = vol_purchased * row['bid'] 444 | # Calculate the profit / loss 445 | outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) 446 | outcome = outcome * -1 447 | # Update the available balance with the amount risked 448 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome 449 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 450 | pos_tp[1]['amount_risked'] 451 | # Remove the equity risked 452 | trade_object['current_equity'] = trade_object['current_equity'] - pos_tp[1]['amount_risked'] 453 | print(f"Sell Take Profit activated for {pos_tp[0]}. Outcome: {outcome}. " 454 | f"Updated Balance: {trade_object['current_available_balance']}. " 455 | f"Updated Equity: {trade_object['current_equity']}") 456 | # Update status of position 457 | position['status'] = 'closed' 458 | # Update SQL tracking 459 | sql_interaction.position_close( 460 | trade_type=pos_tp[1]['order_type'], 461 | status="closed", 462 | stop_loss=pos_tp[1]['stop_loss'], 463 | take_profit=pos_tp[1]['take_profit'], 464 | price=row['bid'], 465 | order_id=pos_tp[0], 466 | trade_object=trade_object, 467 | update_time=row['time_msc'], 468 | project_settings=project_settings, 469 | entry_price=last_trade[0][17], 470 | exit_price=row['bid'], 471 | qty_purchased=vol_purchased, 472 | trade_stage="position", 473 | comment=comment 474 | ) 475 | # Update the balance 476 | sql_interaction.insert_balance_change( 477 | trade_object=trade_object, 478 | note="Take profit reached", 479 | balance=trade_object["current_available_balance"], 480 | equity=trade_object["current_equity"], 481 | profit_or_loss=outcome, 482 | order_id=pos_tp[0], 483 | time=row['time_msc'], 484 | project_settings=project_settings 485 | ) 486 | 487 | position_dataframe = position_dataframe.drop(position.index) 488 | return position_dataframe 489 | 490 | 491 | # Function when a SELL Stop Loss reached 492 | def sell_stop_loss_reached(position_dataframe, position, row, comment, project_settings): 493 | # todo: Update SQL Table with outcome 494 | for pos_sl in position.iterrows(): 495 | last_trade = sql_interaction.retrieve_last_position( 496 | order_id=pos_sl[0], 497 | trade_object=trade_object, 498 | project_settings=project_settings 499 | ) 500 | # Calculate the volume originally purchased 501 | vol_purchased = last_trade[0][6] 502 | # Calculate the price sold 503 | price_sold = vol_purchased * row['bid'] 504 | # Calculate the profit / loss 505 | outcome = price_sold - (last_trade[0][6] * last_trade[0][17]) 506 | # Reverse sign on outcome to account for down direction 507 | outcome = outcome * -1 508 | # Update the available balance with the amount risked 509 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + outcome 510 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 511 | pos_sl[1]['amount_risked'] 512 | # Remove the equity risked 513 | trade_object['current_equity'] = trade_object['current_equity'] - pos_sl[1]['amount_risked'] 514 | print(f"SELL Stop Loss activated for {pos_sl[0]}. Outcome: {outcome}." 515 | f"Updated Balance: {trade_object['current_available_balance']}. " 516 | f"Updated Equity: {trade_object['current_equity']}") 517 | # Update status of position 518 | position['status'] = 'closed' 519 | # Update position tracking 520 | sql_interaction.position_close( 521 | trade_type=pos_sl[1]['order_type'], 522 | status="closed", 523 | stop_loss=pos_sl[1]['stop_loss'], 524 | take_profit=pos_sl[1]['take_profit'], 525 | price=row['bid'], 526 | order_id=pos_sl[0], 527 | trade_object=trade_object, 528 | update_time=row['time_msc'], 529 | project_settings=project_settings, 530 | entry_price=last_trade[0][17], 531 | exit_price=row['bid'], 532 | qty_purchased=vol_purchased, 533 | trade_stage="position", 534 | comment=comment 535 | ) 536 | # Update the balance 537 | sql_interaction.insert_balance_change( 538 | trade_object=trade_object, 539 | note="Stop Loss reached", 540 | balance=trade_object["current_available_balance"], 541 | equity=trade_object["current_equity"], 542 | profit_or_loss=outcome, 543 | order_id=pos_sl[0], 544 | time=row['time_msc'], 545 | project_settings=project_settings 546 | ) 547 | 548 | position_dataframe = position_dataframe.drop(position.index) 549 | return position_dataframe 550 | 551 | 552 | # Function to close open orders at conclusion of testing 553 | def close_open_positions(open_buy_positions, open_sell_positions, update_time, project_settings, comment): 554 | # Close any open buy positions 555 | if len(open_buy_positions) > 0: 556 | for row in open_buy_positions.iterrows(): 557 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 558 | row[1]['amount_risked'] 559 | trade_object['current_equity'] = trade_object['current_equity'] - row[1]['amount_risked'] 560 | last_trade = sql_interaction.retrieve_last_position( 561 | order_id=row[0], 562 | trade_object=trade_object, 563 | project_settings=project_settings 564 | ) 565 | # Close the position 566 | sql_interaction.position_close( 567 | trade_type=row[1]['order_type'], 568 | status="backtest_closed", 569 | stop_loss=row[1]['stop_loss'], 570 | take_profit=row[1]['take_profit'], 571 | price=last_trade[0][17], 572 | order_id=row[0], 573 | trade_object=trade_object, 574 | update_time=update_time, 575 | project_settings=project_settings, 576 | entry_price=last_trade[0][17], 577 | exit_price=last_trade[0][17], 578 | qty_purchased=last_trade[0][6], 579 | trade_stage="position", 580 | comment=comment 581 | ) 582 | # Close any open sell positions 583 | if len(open_sell_positions) > 0: 584 | for row in open_sell_positions.iterrows(): 585 | trade_object['current_available_balance'] = trade_object['current_available_balance'] + \ 586 | row[1]['amount_risked'] 587 | trade_object['current_equity'] = trade_object['current_equity'] - row[1]['amount_risked'] 588 | last_trade = sql_interaction.retrieve_last_position( 589 | order_id=row[0], 590 | trade_object=trade_object, 591 | project_settings=project_settings 592 | ) 593 | # Close the position 594 | sql_interaction.position_close( 595 | trade_type=row[1]['order_type'], 596 | status="backtest_closed", 597 | stop_loss=row[1]['stop_loss'], 598 | take_profit=row[1]['take_profit'], 599 | price=last_trade[0][17], 600 | order_id=row[0], 601 | trade_object=trade_object, 602 | update_time=update_time, 603 | project_settings=project_settings, 604 | entry_price=last_trade[0][17], 605 | exit_price=last_trade[0][17], 606 | qty_purchased=last_trade[0][6], 607 | trade_stage="position", 608 | comment=comment 609 | ) 610 | 611 | 612 | 613 | def calc_risk_to_dollars(): 614 | # Setup the trade settings 615 | purchase_dollars = { 616 | "risk_amount": 0.00 617 | } 618 | if trade_object["backtest_settings"]["compounding"] == "true": 619 | risk_amount = trade_object["current_available_balance"] * \ 620 | trade_object["backtest_settings"]["balance_risk_percent"] / 100 621 | else: 622 | risk_amount = trade_object["backtest_settings"]["test_balance"] * \ 623 | trade_object["backtest_settings"]["balance_risk_percent"] / 100 624 | # Save to variable 625 | purchase_dollars["risk_amount"] = risk_amount 626 | # Multiply by the leverage 627 | purchase_dollars["purchase_total"] = risk_amount * trade_object["backtest_settings"]["leverage"] 628 | return purchase_dollars 629 | -------------------------------------------------------------------------------- /backtest_lib/backtest_analysis.py: -------------------------------------------------------------------------------- 1 | from sql_lib import sql_interaction 2 | import display_lib 3 | import datetime 4 | import pandas 5 | from backtest_lib import setup_backtest, backtest 6 | from strategies import ema_cross 7 | import hashlib 8 | import pytz 9 | 10 | 11 | # Function to initiate and manage backtest 12 | def do_backtest(strategy_name, symbol, candle_timeframe, test_timeframe, project_settings, get_data=True, 13 | exchange="mt5", optimize=False, display=False, variables={"risk_ratio": 3}, full_analysis=False, 14 | redo_analysis=False, regather_data=False): 15 | symbol_name = symbol.split(".") 16 | # Set the table names 17 | table_name_base = strategy_name + "_" + symbol_name[0] + "_" 18 | raw_data_table_name = f"{table_name_base}candles".lower() 19 | tick_data_table_name = f"{table_name_base}ticks".lower() 20 | trade_table_name = f"{table_name_base}trade_actions".lower() 21 | balance_tracker_table = f"{table_name_base}balance".lower() 22 | valid_trades_table = f"{table_name_base}trades".lower() 23 | var = str(variables) 24 | comment = hashlib.sha256(var.encode("utf-8")) 25 | comment = str(comment.hexdigest()) 26 | # Make sure the summary table is created 27 | try: 28 | sql_interaction.create_summary_table(project_settings) 29 | except Exception as e: 30 | if e == 'relation "strategy_testing_outcomes" already exists': 31 | print("Failed to execute query: Strategy Testing Outcomes database ready") 32 | else: 33 | print(e) 34 | 35 | if regather_data: 36 | # todo: Delete previous data 37 | pass 38 | # If data required 39 | if get_data: 40 | print("Getting Data") 41 | # Set up backtest Postgres Tables and get raw data 42 | setup_backtest.set_up_backtester( 43 | strategy_name=strategy_name, 44 | symbol=symbol, 45 | candle_timeframe=candle_timeframe, 46 | backtest_timeframe=test_timeframe, 47 | project_settings=project_settings, 48 | exchange=exchange, 49 | candle_table_name=raw_data_table_name, 50 | tick_table_name=tick_data_table_name, 51 | balance_tracker_table=balance_tracker_table 52 | ) 53 | if redo_analysis: 54 | # todo: Delete previous analysis tables 55 | pass 56 | if full_analysis: 57 | # Get the raw data 58 | raw_dataframe = sql_interaction.retrieve_dataframe( 59 | table_name=raw_data_table_name, 60 | project_settings=project_settings 61 | ) 62 | # Construct the trades 63 | trades_dataframe = ema_cross.ema_cross_strategy( 64 | dataframe=raw_dataframe, 65 | risk_ratio=variables["risk_ratio"] 66 | ) 67 | # Run the backtest 68 | backtest.backtest( 69 | valid_trades_dataframe=trades_dataframe, 70 | time_orders_valid=1800, 71 | tick_data_table_name=tick_data_table_name, 72 | trade_table_name=trade_table_name, 73 | project_settings=project_settings, 74 | strategy=strategy_name, 75 | symbol=symbol, 76 | comment=comment, 77 | balance_table=balance_tracker_table, 78 | valid_trades_table=valid_trades_table 79 | ) 80 | # Capture outcomes 81 | #todo: Calculate trade outcomes function 82 | #todo: Save trade outcomes to SQL (preparation for optimization) 83 | 84 | # Construct the trades 85 | if optimize: 86 | # Optimize the take profit. 87 | 88 | pass 89 | 90 | if display: 91 | trade_object = { 92 | "trade_table_name": trade_table_name, 93 | "strategy": strategy_name, 94 | "comment": comment 95 | } 96 | # Retrieve raw dataframe 97 | raw_dataframe = sql_interaction.retrieve_dataframe( 98 | table_name=raw_data_table_name, 99 | project_settings=project_settings 100 | ) 101 | # Retrieve an image of events 102 | strategy_image = ema_cross.ema_cross_strategy( 103 | dataframe=raw_dataframe, 104 | risk_ratio=variables['risk_ratio'], 105 | display=True, 106 | backtest=False 107 | ) 108 | # Retrieve trades dataframe 109 | trades_dataframe = ema_cross.ema_cross_strategy( 110 | dataframe=raw_dataframe, 111 | risk_ratio=variables["risk_ratio"], 112 | backtest=True 113 | ) 114 | # Retrieve trade object 115 | trades_outcome = calculate_trades( 116 | trade_object=trade_object, 117 | comment=comment, 118 | project_settings=project_settings 119 | ) 120 | print(trades_outcome) 121 | # Add trades outcomes to graph 122 | # todo: retrieve calculated trades 123 | # todo: retrieve balance 124 | # todo: pass to display function 125 | show_display( 126 | strategy_image=strategy_image, 127 | trades_outcome=trades_outcome, 128 | proposed_trades=trades_dataframe, 129 | strategy=strategy_name, 130 | symbol=symbol 131 | ) 132 | 133 | return True 134 | 135 | 136 | # Function to display backtest details to user 137 | def show_display(strategy_image, trades_outcome, proposed_trades, symbol, strategy): 138 | # Construct the Title 139 | title = symbol + " " + strategy 140 | # Add trades to strategy image 141 | strategy_with_trades = display_lib.add_trades_to_graph( 142 | trades_dict=trades_outcome, 143 | base_fig=strategy_image 144 | ) 145 | # Turn proposed trades into a subplot 146 | prop_trades_figure = display_lib.add_dataframe(proposed_trades) 147 | 148 | 149 | display_lib.display_backtest( 150 | original_strategy=strategy_image, 151 | strategy_with_trades=strategy_with_trades, 152 | table=prop_trades_figure, 153 | graph_title=title 154 | ) 155 | 156 | 157 | # Function to retrieve and construct trade open and sell 158 | def calculate_trades(trade_object, comment, project_settings): 159 | # Retrieve the trades for the strategy being analyzed 160 | trades = sql_interaction.retrieve_unique_order_id( 161 | trade_object=trade_object, 162 | comment=comment, 163 | project_settings=project_settings) 164 | trade_list = [] 165 | full_trades = [] 166 | # Format the trades into a nicer list 167 | for trade in trades: 168 | trade_list.append(trade[0]) 169 | 170 | # Setup trackers for wins and losses 171 | summary = { 172 | "wins": 0, 173 | "losses": 0, 174 | "profit": 0, 175 | "not_completed": 0 176 | } 177 | 178 | # Retrieve full details for each trade 179 | for order in trade_list: 180 | trade_view = {'name': order} 181 | 182 | trade_details = sql_interaction.retrieve_trade_details( 183 | order_id=order, 184 | trade_object=trade_object, 185 | comment=comment, 186 | project_settings=project_settings 187 | ) 188 | trade_view['trade_type'] = trade_details[0][3] 189 | # Calculate the outcome 190 | for entry in trade_details: 191 | if entry[12] == "expired": 192 | trade['expired'] = True 193 | trade['expire_price'] = entry[10] 194 | trade['expire_time'] = datetime.datetime.fromtimestamp(entry[16], pytz.UTC) 195 | elif entry[12] == "opened": 196 | trade_view['open_price'] = entry[10] 197 | trade_view['open_time'] = datetime.datetime.fromtimestamp(entry[16], pytz.UTC) 198 | elif entry[12] == "closed": 199 | trade_view['close_price'] = entry[10] 200 | trade_view['close_time'] = datetime.datetime.fromtimestamp(entry[16], pytz.UTC) 201 | trade_view['trade_outcome'] = calc_the_win(row=entry) 202 | elif entry[12] == "order": 203 | trade_view['order_price'] = entry[10] 204 | trade_view['order_time'] = datetime.datetime.fromtimestamp(entry[16], pytz.UTC) 205 | elif entry[12] == "backtest_closed": 206 | trade_view['close_price'] = entry[10] 207 | trade_view['close_time'] = datetime.datetime.fromtimestamp(entry[16], pytz.UTC) 208 | trade_view['trade_outcome'] = {"not_completed": True} 209 | full_trades.append(trade_view) 210 | 211 | # Calculate the wins and losses 212 | for trade_outcome in full_trades: 213 | if not trade_outcome["trade_outcome"]["not_completed"]: 214 | summary['profit'] += trade_outcome['trade_outcome']['profit'] 215 | if trade_outcome['trade_outcome']['win'] is True: 216 | summary['wins'] += 1 217 | else: 218 | summary['losses'] += 1 219 | else: 220 | summary['not_completed'] += 1 221 | summary["full_trades"] = full_trades 222 | return summary 223 | 224 | 225 | # Function to calculate if a trade was a win or loss and profit 226 | def calc_the_win(row): 227 | # Set up record 228 | outcome = { 229 | "profit": 0, 230 | "win": False, 231 | "not_completed": False 232 | } 233 | # Branch based on order type 234 | if row[3] == "BUY_STOP": 235 | # Calculate the profit 236 | outcome["profit"] = (row[18] - row[17]) * row[6] 237 | else: 238 | # Calculate the profit 239 | outcome["profit"] = (row[17] - row[18]) * row[6] 240 | # Profit will be any number > 0 241 | if outcome["profit"] > 0: 242 | outcome['win'] = True 243 | else: 244 | outcome['win'] = False 245 | # Return outcome 246 | return outcome 247 | 248 | 249 | 250 | -------------------------------------------------------------------------------- /backtest_lib/setup_backtest.py: -------------------------------------------------------------------------------- 1 | import stat 2 | 3 | import numpy 4 | import pandas 5 | import psycopg2 6 | 7 | import exceptions 8 | from sql_lib import sql_interaction 9 | from metatrader_lib import mt5_interaction 10 | import datetime 11 | from sqlalchemy import create_engine 12 | from dateutil.relativedelta import relativedelta 13 | import os 14 | from indicator_lib import calc_all_indicators 15 | 16 | 17 | """ 18 | Pseudo Code: 19 | 1. Create tables -> tick, candlestick, trade, exchange 20 | 2. Get raw data 21 | 3. Add values 22 | 4. Save completed data 23 | """ 24 | 25 | 26 | # Function to set up the backtester 27 | def set_up_backtester(strategy_name, symbol, candle_timeframe, backtest_timeframe, project_settings, exchange, 28 | candle_table_name, tick_table_name, balance_tracker_table): 29 | # Create the backtest tables 30 | create_backtest_tables(tick_table_name=tick_table_name, 31 | balance_tracker_table=balance_tracker_table, 32 | project_settings=project_settings) 33 | # Get the datetime now 34 | current_datetime = datetime.datetime.now() 35 | current_datetime = current_datetime.astimezone(datetime.timezone.utc) 36 | # Populate the raw data based upon easily selected timeframes 37 | if backtest_timeframe == "month": 38 | previous_datetime = current_datetime - relativedelta(months=1) 39 | pass 40 | elif backtest_timeframe == "5days": 41 | previous_datetime = current_datetime - relativedelta(days=5) 42 | pass 43 | elif backtest_timeframe == "6month": 44 | previous_datetime = current_datetime - relativedelta(months=6) 45 | pass 46 | elif backtest_timeframe == "year": 47 | previous_datetime = current_datetime - relativedelta(years=1) 48 | pass 49 | elif backtest_timeframe == "2years": 50 | previous_datetime = current_datetime - relativedelta(years=2) 51 | pass 52 | elif backtest_timeframe == "5years": 53 | previous_datetime = current_datetime - relativedelta(years=5) 54 | pass 55 | else: 56 | print("Choose correct value for backtester") 57 | raise exceptions.BacktestIncorrectBacktestTimeframeError 58 | 59 | if exchange == "mt5": 60 | # Retrieve data 61 | retrieve_mt5_backtest_data( 62 | symbol=symbol, 63 | strategy=strategy_name, 64 | candlesticks=candle_timeframe, 65 | start_time_utc=previous_datetime, 66 | finish_time_utc=current_datetime, 67 | project_settings=project_settings, 68 | tick_table_name=tick_table_name, 69 | candlestick_table_name=candle_table_name 70 | ) 71 | 72 | 73 | # Create the backtest tables 74 | def create_backtest_tables(tick_table_name, balance_tracker_table, project_settings): 75 | # Create the tick data table 76 | try: 77 | sql_interaction.create_mt5_backtest_tick_table(table_name=tick_table_name, project_settings=project_settings) 78 | sql_interaction.create_balance_tracker_table(table_name=balance_tracker_table, project_settings=project_settings) 79 | except Exception as e: 80 | print(f"Error creating backtest tables. {e}") 81 | return True 82 | 83 | 84 | # Retrieve data 85 | def retrieve_mt5_backtest_data(symbol, strategy, project_settings, candlesticks, start_time_utc, finish_time_utc, 86 | tick_table_name, candlestick_table_name): 87 | # Create the connection object for PostgreSQL 88 | engine_string = f"postgresql://{project_settings['postgres']['user']}:{project_settings['postgres']['password']}@" \ 89 | f"{project_settings['postgres']['host']}:{project_settings['postgres']['port']}/" \ 90 | f"{project_settings['postgres']['database']}" 91 | engine = create_engine(engine_string) 92 | # Initialize MT5. Use this to prepare for multiprocessing 93 | print("Starting MT5") 94 | mt5_interaction.start_mt5( 95 | username=project_settings["mt5"]["paper"]["username"], 96 | password=project_settings["mt5"]["paper"]["password"], 97 | server=project_settings["mt5"]["paper"]["server"], 98 | path=project_settings["mt5"]["paper"]["mt5Pathway"], 99 | ) 100 | # Enable Symbol 101 | print(f"Initializing symbol {symbol}") 102 | mt5_interaction.initialize_symbols([symbol]) 103 | # Retrieve tick data 104 | print(f"Retrieving tick data for {symbol} from MT5") 105 | ticks_data_frame = retrieve_mt5_tick_data( 106 | start_time=start_time_utc, 107 | finish_time=finish_time_utc, 108 | symbol=symbol 109 | ) 110 | # Reorder to match creation 111 | ticks_data_frame = ticks_data_frame[['symbol', 'time', 'bid', 'ask', 'spread', 'last', 'volume', 'flags', 112 | 'volume_real', 'time_msc', 'human_time', 'human_time_msc']] 113 | # Write to database 114 | print(f"Writing tick data for {symbol} to local database") 115 | upload_to_postgres(ticks_data_frame, tick_table_name, project_settings) 116 | # Retrieve candlestick data 117 | for candle in candlesticks: 118 | print(f"Retrieving {candle} data for {symbol}") 119 | candlestick_data = retrieve_mt5_candle_data( 120 | start_time=start_time_utc, 121 | finish_time=finish_time_utc, 122 | timeframe=candle, 123 | symbol=symbol 124 | ) 125 | # Calculate all indicator_lib 126 | candlestick_data = calc_all_indicators.all_indicators(candlestick_data) 127 | # Write to database 128 | candlestick_data.to_sql(name=candlestick_table_name, con=engine, if_exists='append') 129 | return True 130 | 131 | 132 | # Helper function to divide a given date range by 2 133 | def split_time_range_in_half(start_time, finish_time): 134 | n = 2 135 | split_timeframe = [] 136 | diff = (finish_time - start_time) // n 137 | for idx in range(0, n): 138 | split_timeframe.append((start_time + idx * diff)) 139 | split_timeframe.append(finish_time) 140 | return split_timeframe 141 | 142 | 143 | # Function to retrieve MT5 Tick data with autoscaling options 144 | def retrieve_mt5_tick_data(start_time, finish_time, symbol): 145 | # Attempt to retrieve tick data 146 | tick_data = mt5_interaction.retrieve_tick_time_range( 147 | start_time_utc=start_time, 148 | finish_time_utc=finish_time, 149 | symbol=symbol 150 | ) 151 | # Autoscale if Zero results retrieved 152 | if type(tick_data) is not numpy.ndarray: 153 | print(f"Auto scaling tick query for {symbol}") 154 | # Split timerange into 2 155 | split_timeframe = split_time_range_in_half(start_time=start_time, finish_time=finish_time) 156 | # Iterate through timeframe list and append 157 | start_time = split_timeframe[0] 158 | tick_data_autoscale = pandas.DataFrame() 159 | for timeframe in split_timeframe: 160 | if timeframe == split_timeframe[0]: 161 | # Initial pass, so ignore 162 | pass 163 | 164 | else: 165 | tick_data_new = retrieve_mt5_tick_data(start_time=start_time, finish_time=timeframe, symbol=symbol) 166 | tick_data_autoscale = pandas.concat([tick_data_autoscale, tick_data_new]) 167 | start_time = timeframe 168 | return tick_data_autoscale 169 | # Else return value 170 | ticks_data_frame = pandas.DataFrame(tick_data) 171 | 172 | # Add spread 173 | ticks_data_frame['spread'] = ticks_data_frame['ask'] - ticks_data_frame['bid'] 174 | # Add symbol 175 | ticks_data_frame['symbol'] = symbol 176 | # Format integers into signed integers (postgres doesn't support unsigned int) 177 | ticks_data_frame['time'] = ticks_data_frame['time'].astype('int64') 178 | ticks_data_frame['volume'] = ticks_data_frame['volume'].astype('int64') 179 | ticks_data_frame['time_msc'] = ticks_data_frame['time_msc'].astype('int64') 180 | ticks_data_frame['human_time'] = pandas.to_datetime(ticks_data_frame['time'], unit='s') 181 | ticks_data_frame['human_time_msc'] = pandas.to_datetime(ticks_data_frame['time_msc'], unit='ms') 182 | return ticks_data_frame 183 | 184 | 185 | # Function to retrieve mt5 candlestick data 186 | def retrieve_mt5_candle_data(start_time, finish_time, timeframe, symbol): 187 | # Retrieve the candlestick data 188 | candlestick_data = mt5_interaction.retrieve_candlestick_data_range( 189 | start_time_utc=start_time, 190 | finish_time_utc=finish_time, 191 | timeframe=timeframe, 192 | symbol=symbol 193 | ) 194 | if type(candlestick_data) is not numpy.ndarray: 195 | print(f"Auto scaling candlestick query for {symbol} and {timeframe}") 196 | # Split time range in 2 197 | split_timeframe = split_time_range_in_half(start_time=start_time, finish_time=finish_time) 198 | # Iterate through the timeframe list and construct full list 199 | start_time = split_timeframe[0] 200 | candlestick_data_autoscale = pandas.DataFrame() 201 | for time in split_timeframe: 202 | if time == split_timeframe[0]: 203 | # Initial pass, so ignore 204 | pass 205 | else: 206 | candlestick_data_new = retrieve_mt5_candle_data( 207 | start_time=start_time, 208 | finish_time=time, 209 | timeframe=timeframe, 210 | symbol=symbol 211 | ) 212 | candlestick_data_autoscale = pandas.concat([candlestick_data_autoscale, candlestick_data_new]) 213 | return candlestick_data_autoscale 214 | 215 | # Convert to a dataframe 216 | candlestick_dataframe = pandas.DataFrame(candlestick_data) 217 | # Add in symbol and timeframe columns 218 | candlestick_dataframe['symbol'] = symbol 219 | candlestick_dataframe['timeframe'] = timeframe 220 | # Convert integers into signed integers (postgres doesn't support unsigned int) 221 | candlestick_dataframe['time'] = candlestick_dataframe['time'].astype('int64') 222 | candlestick_dataframe['tick_volume'] = candlestick_dataframe['tick_volume'].astype('float64') 223 | candlestick_dataframe['spread'] = candlestick_dataframe['spread'].astype('int64') 224 | candlestick_dataframe['real_volume'] = candlestick_dataframe['real_volume'].astype('int64') 225 | candlestick_dataframe['human_time'] = pandas.to_datetime(candlestick_dataframe['time'], unit='s') 226 | return candlestick_dataframe 227 | 228 | 229 | # Function to upload a dataframe to Postgres 230 | def upload_to_postgres(dataframe, table_name, project_settings): 231 | ## Process to upload to local database: Get directory, write to csv, update CSV permissions, upload to DB, 232 | # delete CSV 233 | 234 | # Specify the disk location 235 | current_user = os.getlogin() 236 | dataframe_csv = f"C:\\Users\\{current_user}\\Desktop\\ticks_data_frame.csv" 237 | 238 | # Dump the dataframe to disk 239 | dataframe.to_csv(dataframe_csv, index_label='id', header=False) 240 | 241 | # Open the csv 242 | f = open(dataframe_csv, 'r') 243 | 244 | # Connect to database 245 | conn = sql_interaction.postgres_connect(project_settings) 246 | cursor = conn.cursor() 247 | 248 | # Try to upload 249 | try: 250 | cursor.copy_from(f, table_name, sep=",") 251 | conn.commit() 252 | except (Exception, psycopg2.DatabaseError) as error: 253 | f.close() 254 | os.remove(dataframe_csv) 255 | print(f"Postgres upload error: {error}") 256 | conn.rollback() 257 | cursor.close() 258 | return 1 259 | print(f"Postgres upload completed") 260 | cursor.close() 261 | f.close() 262 | os.remove(dataframe_csv) 263 | return True 264 | -------------------------------------------------------------------------------- /binance_lib/binance_interaction.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | from binance.spot import Spot 3 | 4 | 5 | # Function to query Binance and retrieve status 6 | def query_binance_status(): 7 | # Query for system status 8 | status = Spot().system_status() 9 | if status['status'] == 0: 10 | return True 11 | else: 12 | raise False 13 | 14 | 15 | # Function to query Binance account 16 | def query_account(api_key, secret_key): 17 | return Spot(key=api_key, secret=secret_key).account() 18 | 19 | 20 | # Function to query Binance for candlestick data 21 | def get_candlestick_data(symbol, timeframe, qty): 22 | # Retrieve the raw data 23 | raw_data = Spot().klines(symbol=symbol, interval=timeframe, limit=qty) 24 | # Set up the return array 25 | converted_data = [] 26 | # Convert each element into a Python dictionary object, then add to converted_data 27 | for candle in raw_data: 28 | # Dictionary object 29 | converted_candle = { 30 | 'time': candle[0], 31 | 'open': float(candle[1]), 32 | 'high': float(candle[2]), 33 | 'low': float(candle[3]), 34 | 'close': float(candle[4]), 35 | 'volume': float(candle[5]), 36 | 'close_time': candle[6], 37 | 'quote_asset_volume': float(candle[7]), 38 | 'number_of_trades': int(candle[8]), 39 | 'taker_buy_base_asset_volume': float(candle[9]), 40 | 'taker_buy_quote_asset_volume': float(candle[10]) 41 | } 42 | # Add to converted_data 43 | converted_data.append(converted_candle) 44 | # Return converted data 45 | return converted_data 46 | 47 | 48 | # Function to query Binance for all symbols with a base asset of BUSD 49 | def query_quote_asset_list(quote_asset_symbol): 50 | # Retrieve a list of symbols from Binance. Returns as a dictionary 51 | symbol_dictionary = Spot().exchange_info() 52 | # Convert into a dataframe 53 | symbol_dataframe = pandas.DataFrame(symbol_dictionary['symbols']) 54 | # Extract only those symbols with a base asset of BUSD 55 | quote_symbol_dataframe = symbol_dataframe.loc[symbol_dataframe['quoteAsset'] == quote_asset_symbol] 56 | # Return base_symbol_dataframe 57 | return quote_symbol_dataframe 58 | 59 | 60 | # Function to make a trade on Binance 61 | def make_trade(symbol, action, type, timeLimit, quantity, stop_price, stop_limit_price, project_settings): 62 | # Develop the params 63 | params = { 64 | "symbol": symbol, 65 | "side": action, 66 | "type": type, 67 | "timeInForce": timeLimit, 68 | "quantity": quantity, 69 | "stopPrice": stop_price, 70 | "stopLimitPrice": stop_limit_price, 71 | "trailingDelta": project_settings['trailingStopPercent'] 72 | } 73 | 74 | # Add in the trailing stop limit 75 | 76 | # See if we're testing. Default to yes. 77 | if project_settings['Testing'] == "False": 78 | print("Real Trade") 79 | # Set the API Key 80 | api_key = project_settings['BinanceKeys']['API_Key'] 81 | # Set the secret key 82 | secret_key = project_settings['BinanceKeys']['Secret_Key'] 83 | # Setup the client 84 | client = Spot(key=api_key, secret=secret_key) 85 | else: 86 | print("Testing Trading") 87 | # Set the Test API Key 88 | api_key = project_settings['TestKeys']['Test_API_Key'] 89 | # Set the Test Secret Key 90 | secret_key = project_settings['TestKeys']['Test_Secret_Key'] 91 | client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") 92 | 93 | # Make the trade 94 | try: 95 | response = client.new_order(**params) 96 | return response 97 | except ConnectionRefusedError as error: 98 | print(f"Found error. {error}") 99 | 100 | 101 | # Function to make a trade if params provided 102 | def make_trade_with_params(params, project_settings): 103 | # See if we're testing. Default to yes. 104 | if project_settings['Testing'] == "False": 105 | print("Real Trade") 106 | # Set the API Key 107 | api_key = project_settings['BinanceKeys']['API_Key'] 108 | # Set the secret key 109 | secret_key = project_settings['BinanceKeys']['Secret_Key'] 110 | # Setup the client 111 | client = Spot(key=api_key, secret=secret_key) 112 | else: 113 | print("Testing Trading") 114 | # Set the Test API Key 115 | api_key = project_settings['TestKeys']['Test_API_Key'] 116 | # Set the Test Secret Key 117 | secret_key = project_settings['TestKeys']['Test_Secret_Key'] 118 | client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") 119 | 120 | # Make the trade 121 | try: 122 | response = client.new_order(**params) 123 | return response 124 | except ConnectionRefusedError as error: 125 | print(f"Found error. {error}") 126 | 127 | 128 | # Function to cancel a trade 129 | def cancel_order_by_symbol(symbol, project_settings): 130 | # See if we're testing. Default to yes. 131 | if project_settings['Testing'] == "False": 132 | # Set the API Key 133 | api_key = project_settings['BinanceKeys']['API_Key'] 134 | # Set the secret key 135 | secret_key = project_settings['BinanceKeys']['Secret_Key'] 136 | # Setup the client 137 | client = Spot(key=api_key, secret=secret_key) 138 | else: 139 | print("Testing Trading") 140 | # Set the Test API Key 141 | api_key = project_settings['TestKeys']['Test_API_Key'] 142 | # Set the Test Secret Key 143 | secret_key = project_settings['TestKeys']['Test_Secret_Key'] 144 | client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") 145 | 146 | # Cancel the trade 147 | try: 148 | response = client.cancel_open_orders(symbol=symbol) 149 | return response 150 | except ConnectionRefusedError as error: 151 | print(f"Found error {error}") 152 | 153 | 154 | # Function to query open trades 155 | def query_open_trades(project_settings): 156 | # See if we're testing. Default to yes. 157 | if project_settings['Testing'] == "False": 158 | # Set the API Key 159 | api_key = project_settings['BinanceKeys']['API_Key'] 160 | # Set the secret key 161 | secret_key = project_settings['BinanceKeys']['Secret_Key'] 162 | # Setup the client 163 | client = Spot(key=api_key, secret=secret_key) 164 | else: 165 | # Set the Test API Key 166 | api_key = project_settings['TestKeys']['Test_API_Key'] 167 | # Set the Test Secret Key 168 | secret_key = project_settings['TestKeys']['Test_Secret_Key'] 169 | client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") 170 | 171 | # Cancel the trade 172 | try: 173 | response = client.get_open_orders() 174 | return response 175 | except ConnectionRefusedError as error: 176 | print(f"Found error {error}") 177 | 178 | 179 | def get_balance(project_settings): 180 | # See if we're testing. Default to yes. 181 | if project_settings['Testing'] == "False": 182 | # Set the API Key 183 | api_key = project_settings['BinanceKeys']['API_Key'] 184 | # Set the secret key 185 | secret_key = project_settings['BinanceKeys']['Secret_Key'] 186 | # Setup the client 187 | client = Spot(key=api_key, secret=secret_key) 188 | else: 189 | # Set the Test API Key 190 | api_key = project_settings['TestKeys']['Test_API_Key'] 191 | # Set the Test Secret Key 192 | secret_key = project_settings['TestKeys']['Test_Secret_Key'] 193 | client = Spot(key=api_key, secret=secret_key, base_url="https://testnet.binance.vision") 194 | 195 | return client.account_snapshot("SPOT") -------------------------------------------------------------------------------- /capture_lib/trade_capture.py: -------------------------------------------------------------------------------- 1 | from metatrader_lib import mt5_interaction 2 | from sql_lib import sql_interaction 3 | import exceptions 4 | 5 | 6 | # Function to capture order actions 7 | def capture_order(order_type, strategy, exchange, symbol, comment, project_settings,volume=0.0, stop_loss=0.0, 8 | take_profit=0.0, price=None, paper=True, order_number="", backtest=False): 9 | """ 10 | Function to capture an order 11 | :param order_type: String 12 | :param strategy: String 13 | :param exchange: String 14 | :param symbol: String 15 | :param comment: String 16 | :param project_settings: JSON Object 17 | :param volume: Float 18 | :param stop_loss: Float 19 | :param take_profit: Float 20 | :param price: Float 21 | :param paper: Bool 22 | :param order_number: INT 23 | :return: 24 | """ 25 | # Format objects correctly 26 | strategy = str(strategy) 27 | order_type = str(order_type) 28 | exchange = str(exchange) 29 | symbol = str(symbol) 30 | volume = float(volume) 31 | stop_loss = float(stop_loss) 32 | take_profit = float(take_profit) 33 | comment = str(comment) 34 | # Create the Database Object 35 | db_object = { 36 | "strategy": strategy, 37 | "exchange": exchange, 38 | "trade_type": order_type, 39 | "trade_stage": "order", 40 | "symbol": symbol, 41 | "volume": volume, 42 | "stop_loss": stop_loss, 43 | "take_profit": take_profit, 44 | "comment": comment 45 | } 46 | 47 | # Check the price. If order_type == "BUY_STOP" or "SELL_STOP", price cannot be None 48 | if order_type == "BUY_STOP" or order_type == "SELL_STOP": 49 | if volume <= 0: 50 | print(f"Volume must be greater than 0 for an order type of {order_type}") 51 | raise SyntaxError # Use Pythons built in error for incorrect syntax 52 | if take_profit <= 0: 53 | print(f"Take Profit must be greater than 0 for an order type of {order_type}") 54 | raise ValueError # Use Pythons built in error for incorrect value 55 | if stop_loss <= 0: 56 | print(f"Stop Loss must be greater than 0 for an order type of {order_type}") 57 | raise ValueError # Use Pythons built in error for incorrect value 58 | if price == None: 59 | print(f"Price cannot be NONE on order_type {order_type}") 60 | raise ValueError # Use Pythons built in error for incorrect value 61 | else: 62 | # Format price correctly 63 | price = float(price) 64 | # Add to database object 65 | db_object['price'] = price 66 | 67 | # If order_type == "BUY" or "SELL", price must be None 68 | if order_type == "BUY" or order_type == "SELL": 69 | if volume <= 0: 70 | print(f"Volume must be greater than 0 for an order type of {order_type}") 71 | raise ValueError # Use Pythons built in error for incorrect value 72 | if take_profit <= 0: 73 | print(f"Take Profit must be greater than 0 for an order type of {order_type}") 74 | raise ValueError # Use Pythons built in error for incorrect value 75 | if stop_loss <= 0: 76 | print(f"Stop Loss must be greater than 0 for an order type of {order_type}") 77 | raise ValueError # Use Pythons built in error for incorrect value 78 | if price != None: 79 | print(f"Price must be NONE for order_type {order_type}") 80 | raise ValueError # Use Pythons built in error for incorrect value 81 | else: 82 | # Make price = 0 83 | price = 0 84 | db_object['price'] = price 85 | 86 | if backtest == True: 87 | pass 88 | else: 89 | # If order_type == "cancel", pass straight through into cancel order function 90 | if order_type == "CANCEL": 91 | db_object['price'] = price 92 | db_object['volume'] = volume 93 | db_object['take_profit'] = take_profit 94 | db_object['stop_loss'] = stop_loss 95 | # Branch based upon exchange 96 | if exchange == "mt5": 97 | try: 98 | order_outcome = mt5_interaction.cancel_order(order_number=order_number) 99 | if order_outcome is True: 100 | db_object['status'] = "cancelled" 101 | db_object['order_id'] = order_number 102 | else: 103 | raise exceptions.MetaTraderCancelOrderError 104 | except Exception as e: 105 | print(f"Exception cancelling order order on MT5. {e}") 106 | # Place order based upon exchange 107 | else: 108 | if exchange == "mt5": 109 | try: 110 | db_object["order_id"] = mt5_interaction.place_order( 111 | order_type=order_type, 112 | symbol=symbol, 113 | volume=volume, 114 | stop_loss=stop_loss, 115 | take_profit=take_profit, 116 | comment=comment, 117 | price=price 118 | ) 119 | # Update the status 120 | db_object["status"] = "placed" 121 | except Exception as e: 122 | print(f"Exception placing ") 123 | 124 | # Store in correct table 125 | if backtest: 126 | return True 127 | 128 | if paper: 129 | sql_interaction.insert_paper_trade_action(trade_information=db_object, project_settings=project_settings) 130 | return True 131 | 132 | sql_interaction.insert_live_trade_action(trade_information=db_object, project_settings=project_settings) 133 | return True 134 | 135 | 136 | # Function to capture modifications to open positions 137 | def capture_position_update(trade_type, order_number, symbol, strategy, exchange, project_settings, comment, 138 | new_stop_loss, new_take_profit, price, paper=True, volume=0.0): 139 | 140 | # Format the provided items correctly 141 | order_type = str(trade_type) 142 | order_number = int(order_number) 143 | symbol = str(symbol) 144 | new_stop_loss = float(new_stop_loss) 145 | new_take_profit = float(new_take_profit) 146 | strategy = str(strategy) 147 | exchange = str(exchange) 148 | 149 | # Create the db_object 150 | db_object = { 151 | "strategy": strategy, 152 | "exchange": exchange, 153 | "trade_type": order_type, 154 | "trade_stage": "order", 155 | "symbol": symbol, 156 | "volume": volume, 157 | "stop_loss": new_stop_loss, 158 | "take_profit": new_take_profit, 159 | "comment": comment, 160 | "price": price, 161 | "order_id": order_number 162 | } 163 | 164 | # Branch based upon trade_type 165 | if trade_type == "trailing_stop_update" or trade_type == "take_profit_update": 166 | # Branch again based upon exchange type 167 | if exchange == "mt5": 168 | # Use the modify_position function from mt5_interaction 169 | # todo: implement inside a try statement 170 | position_outcome = mt5_interaction.modify_position( 171 | order_number=order_number, 172 | symbol=symbol, 173 | new_stop_loss=new_stop_loss, 174 | new_take_profit=new_take_profit 175 | ) 176 | # Update DB Object 177 | db_object['status'] = "position_modified" 178 | elif trade_type == "SELL" or trade_type == "BUY": 179 | # Branch again based upon exchange type 180 | if exchange == "mt5": 181 | # Use the close_position function from mt5_interaction 182 | #todo: implement inside a try statement 183 | position_outcome = mt5_interaction.close_position( 184 | order_number=order_number, 185 | symbol=symbol, 186 | volume=volume, 187 | order_type=trade_type, 188 | price=price, 189 | comment=comment 190 | ) 191 | db_object['status'] = "position_modified" 192 | elif trade_type == "position": 193 | # Update the position only 194 | db_object['status'] = "position" 195 | 196 | 197 | # Update SQL 198 | # Branch based upon the table 199 | if paper: 200 | sql_interaction.insert_paper_trade_action(trade_information=db_object, project_settings=project_settings) 201 | return True 202 | else: 203 | sql_interaction.insert_live_trade_action(trade_information=db_object, project_settings=project_settings) 204 | return True 205 | 206 | -------------------------------------------------------------------------------- /coinbase_lib/get_account_details.py: -------------------------------------------------------------------------------- 1 | from coinbase.wallet.client import Client 2 | 3 | # Function to account details from Coinbase 4 | def get_account_details(project_settings): 5 | # Retrieve the API Key 6 | api_key = project_settings['coinbase']['api_key'] 7 | api_secret = project_settings['coinbase']['api_secret'] 8 | # Create the Coinbase Client 9 | client = Client( 10 | api_key=api_key, 11 | api_secret=api_secret 12 | ) 13 | # Retrieve information 14 | accounts = client.get_accounts() 15 | return accounts -------------------------------------------------------------------------------- /coinbase_lib/get_candlesticks.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import pandas 3 | 4 | # Function to get candle data 5 | def get_candlestick_data(symbol, timeframe): 6 | # Convert the timeframe into a Coinbase specific type. This could be done in a switch statement for Python 3.10 7 | if timeframe == "M1": 8 | timeframe_converted = 60 9 | elif timeframe == "M5": 10 | timeframe_converted = 300 11 | elif timeframe == "M15": 12 | timeframe_converted = 900 13 | elif timeframe == "H1": 14 | timeframe_converted = 3600 15 | elif timeframe == "H6": 16 | timeframe_converted = 21600 17 | elif timeframe == "D1": 18 | timeframe_converted = 86400 19 | else: 20 | return Exception 21 | # Construct the URL 22 | url = f"https://api.exchange.coinbase.com/products/{symbol}/candles?granularity={timeframe_converted}" 23 | # Construct the headers 24 | headers = {"accept": "application/json"} 25 | # Query the API 26 | response = requests.get(url, headers=headers) 27 | # Retrieve the data 28 | candlestick_raw_data = response.json() 29 | # Initialize an empty array 30 | candlestick_data = [] 31 | # Iterate through the returned data and construct a more useful data format 32 | for candle in candlestick_raw_data: 33 | candle_dict = { 34 | "symbol": symbol, 35 | "time": candle[0], 36 | "low": candle[1], 37 | "high": candle[2], 38 | "open": candle[3], 39 | "close": candle[4], 40 | "volume": candle[5], 41 | "timeframe": timeframe 42 | } 43 | # Append to candlestick_data 44 | candlestick_data.append(candle_dict) 45 | # Convert candlestick_data to dataframe 46 | candlestick_dataframe = pandas.DataFrame(candlestick_data) 47 | # Return a dataframe 48 | return candlestick_dataframe 49 | -------------------------------------------------------------------------------- /common_information_model.json: -------------------------------------------------------------------------------- 1 | { 2 | "live_trade_table": { 3 | "strategy": "String defining strategy", 4 | "exchange": "String defining the exchange being used", 5 | "trade_type": "String of the type of trade: BUY / SELL / BUY_STOP / SELL_STOP", 6 | "trade_stage": "Stage of trade: order / position", 7 | "symbol": "String of the symbol", 8 | "volume": "Float of the volume", 9 | "stop_loss": "Float of the stop loss value", 10 | "take_profit": "Float of the take profit value", 11 | "comment": "String of the comment", 12 | "status": "String of the status: CANCELLED / PLACED ", 13 | "price": "Float of the executed price", 14 | "order_id": "String of a unique identifier for the order" 15 | }, 16 | "paper_trade_table": { 17 | "strategy": "String defining strategy", 18 | "exchange": "String defining the exchange being used", 19 | "trade_type": "String of the type of trade: BUY / SELL / BUY_STOP / SELL_STOP", 20 | "trade_stage": "Stage of trade: order / position", 21 | "symbol": "String of the symbol", 22 | "volume": "Float of the volume", 23 | "stop_loss": "Float of the stop loss value", 24 | "take_profit": "Float of the take profit value", 25 | "comment": "String of the comment", 26 | "status": "String of the status: CANCELLED / PLACED ", 27 | "price": "Float of the executed price", 28 | "order_id": "String of a unique identifier for the order" 29 | } 30 | } -------------------------------------------------------------------------------- /display_lib.py: -------------------------------------------------------------------------------- 1 | from sql_lib import sql_interaction 2 | import plotly.graph_objects as go 3 | from dash import Dash, html, dcc 4 | from plotly.subplots import make_subplots 5 | 6 | 7 | # Function to retrieve back_test data and then display chart of close values 8 | def show_data(table_name, dataframe, graph_name, project_settings): 9 | # Table Name 10 | # Get the data 11 | dataframe = sql_interaction.retrieve_dataframe(table_name, project_settings) 12 | # Construct the figure 13 | fig = go.Figure(data=[go.Candlestick( 14 | x=dataframe['human_time'], 15 | open=dataframe['open'], 16 | high=dataframe['high'], 17 | close=dataframe['close'], 18 | low=dataframe['low'] 19 | )]) 20 | 21 | fig.add_trace(go.Scatter( 22 | x=dataframe['human_time'], 23 | y=dataframe['ta_sma_200'], 24 | name='ta_sma_200' 25 | ) 26 | ) 27 | 28 | fig.add_trace(go.Scatter( 29 | x=dataframe['human_time'], 30 | y=dataframe['ta_ema_200'], 31 | name='ta_ema_200' 32 | )) 33 | 34 | fig.add_trace(go.Scatter( 35 | x=dataframe['human_time'], 36 | y=dataframe['ta_ema_15'], 37 | name='ta_ema_15' 38 | )) 39 | 40 | # Create Dash 41 | app = Dash(__name__) 42 | app.layout = html.Div(children=[ 43 | html.H1(children=graph_name), 44 | html.Div("Example data"), 45 | dcc.Graph( 46 | id='Example Graph', 47 | figure=fig 48 | ) 49 | ]) 50 | app.run_server(debug=True) 51 | 52 | 53 | # Function to display a plotly graph in dash 54 | def display_graph(plotly_fig, graph_title, dash=False, upload=False): 55 | """ 56 | Function to display a plotly graph using Dash 57 | :param plotly_fig: plotly figure 58 | :param graph_title: string 59 | :return: None 60 | """ 61 | # Add in autoscaling for each plotly figure 62 | plotly_fig.update_layout( 63 | autosize=True 64 | ) 65 | plotly_fig.update_yaxes(automargin=True) 66 | plotly_fig.update_layout(xaxis_rangeslider_visible=False) 67 | 68 | 69 | if dash: 70 | # Create the Dash object 71 | app = Dash(__name__) 72 | # Construct view 73 | app.layout = html.Div(children=[ 74 | html.H1(children=graph_title), 75 | html.Div("Created by James Hinton from Creative Appnologies"), 76 | dcc.Graph( 77 | id=graph_title, 78 | figure=plotly_fig 79 | ) 80 | ]) 81 | # Run the image 82 | app.run_server(debug=True) 83 | else: 84 | plotly_fig.show() 85 | 86 | 87 | # Function to display a backtest 88 | def display_backtest(original_strategy, strategy_with_trades, table, graph_title): 89 | original_strategy.update_layout( 90 | autosize=True 91 | ) 92 | original_strategy.update_yaxes(automargin=True) 93 | original_strategy.update_layout(xaxis_rangeslider_visible=False) 94 | # Create a Dash Object 95 | app = Dash(__name__) 96 | 97 | # Construct view 98 | app.layout = html.Div(children=[ 99 | html.H1(graph_title), 100 | html.Div([ 101 | html.H1(children="Strategy With Trades"), 102 | html.Div(children='''Original Strategy'''), 103 | dcc.Graph( 104 | id="strat_with_trades", 105 | figure=strategy_with_trades, 106 | style={'height': '100vh'} 107 | ) 108 | ]), 109 | html.Div([ 110 | html.H1(children="Table of Trades"), 111 | html.Div(children='''Original Strategy'''), 112 | dcc.Graph( 113 | id="table_trades", 114 | figure=table 115 | ) 116 | ]) 117 | ]) 118 | 119 | app.run_server(debug=True) 120 | 121 | 122 | 123 | # Function to construct base candlestick graph 124 | def construct_base_candlestick_graph(dataframe, candlestick_title): 125 | """ 126 | Function to construct base candlestick graph 127 | :param candlestick_title: String 128 | :param dataframe: Pandas dataframe object 129 | :return: plotly figure 130 | """ 131 | # Construct the figure 132 | fig = go.Figure(data=[go.Candlestick( 133 | x=dataframe['human_time'], 134 | open=dataframe['open'], 135 | high=dataframe['high'], 136 | close=dataframe['close'], 137 | low=dataframe['low'], 138 | name=candlestick_title 139 | )]) 140 | # Return the graph object 141 | return fig 142 | 143 | 144 | # Function to add a line trace to a plot 145 | def add_line_to_graph(base_fig, dataframe, dataframe_column, line_name): 146 | """ 147 | Function to add a line to trace to an existing figure 148 | :param base_fig: plotly figure object 149 | :param dataframe: pandas dataframe 150 | :param dataframe_column: string of column to plot 151 | :param line_name: string title of line trace 152 | :return: updated plotly figure 153 | """ 154 | # Construct trace 155 | base_fig.add_trace(go.Scatter( 156 | x=dataframe['human_time'], 157 | y=dataframe[dataframe_column], 158 | name=line_name 159 | )) 160 | # Return the object 161 | return base_fig 162 | 163 | 164 | # Function to display points on graph as diamond 165 | def add_markers_to_graph(base_fig, dataframe, value_column, point_names): 166 | """ 167 | Function to add points to a graph 168 | :param base_fig: plotly figure 169 | :param dataframe: pandas dataframe 170 | :param value_column: value for Y display 171 | :param point_names: what's being plotted 172 | :return: updated plotly figure 173 | """ 174 | # Construct trace 175 | base_fig.add_trace(go.Scatter( 176 | mode="markers", 177 | marker=dict(size=8, symbol="diamond"), 178 | x=dataframe['human_time'], 179 | y=dataframe[value_column], 180 | name=point_names 181 | )) 182 | return base_fig 183 | 184 | 185 | # Function to turn a dataframe into a table 186 | def add_dataframe(dataframe): 187 | fig = go.Figure(data=[go.Table( 188 | header=dict(values=["Time", "Order Type", "Stop Price", "Stop Loss", "Take Profit"], align='left'), 189 | cells=dict(values=[ 190 | dataframe['human_time'], 191 | dataframe['order_type'], 192 | dataframe['stop_price'], 193 | dataframe['stop_loss'], 194 | dataframe['take_profit'] 195 | ]) 196 | )] 197 | ) 198 | return fig 199 | 200 | 201 | # Function to add trades to graph 202 | def add_trades_to_graph(trades_dict, base_fig): 203 | # Create a point plot list 204 | point_plot = [] 205 | # Create the colors 206 | buy_color = dict(color="green") 207 | sell_color = dict(color="red") 208 | # Add each set of trades 209 | trades = trades_dict["full_trades"] 210 | for trade in trades: 211 | if trade['trade_outcome']['not_completed'] is False: 212 | if trade['trade_type'] == "BUY_STOP": 213 | color = buy_color 214 | else: 215 | color = sell_color 216 | 217 | base_fig.add_trace( 218 | go.Scatter( 219 | x=[trade['order_time'], trade['open_time'], trade['close_time']], 220 | y=[trade['order_price'], trade['open_price'], trade['close_price']], 221 | name=trade['name'], 222 | legendgroup=trade['trade_type'], 223 | line=color 224 | ) 225 | ) 226 | return base_fig 227 | 228 | 229 | # Function to add a table of the strategy outcomes to Plotly 230 | 231 | -------------------------------------------------------------------------------- /example_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "coinbase": { 3 | "api_key": "your_api_key", 4 | "api_secret": "your_api_secret" 5 | }, 6 | "mt5": { 7 | "live": { 8 | "username": "Your_Username", 9 | "password": "Your Password", 10 | "server": "Your Server", 11 | "mt5Pathway": "C:/Pathway/to/terminal64.exe", 12 | "symbols": ["USDJPY.a"] 13 | }, 14 | "paper": { 15 | "username": "Your_Username", 16 | "password": "Your Password", 17 | "server": "Your Server", 18 | "mt5Pathway": "C:/Pathway/to/terminal64.exe", 19 | "symbols": ["USDJPY.a"] 20 | } 21 | 22 | }, 23 | "binance": { 24 | "live": { 25 | "API_Key": "Your API Key", 26 | "Secret_Key": "Your Secret Key" 27 | }, 28 | "paper": { 29 | "Paper_API_Key": "Your Test Key", 30 | "Paper_Secret_Key": "Your Test Secret Key" 31 | }, 32 | "Testing": "True", 33 | "symbols": ["BTCUSDT"] 34 | }, 35 | "postgres": { 36 | "host": "your_hostname", 37 | "database": "your_database", 38 | "user": "your_username", 39 | "password": "your_secret_password", 40 | "port": "port_your_db_listens_on" 41 | } 42 | } -------------------------------------------------------------------------------- /exceptions.py: -------------------------------------------------------------------------------- 1 | # Initialize MetaTrader Error 2 | class MetaTraderInitializeError(Exception): 3 | "MetaTrader 5 Initilization failed. Check username, password, server, path" 4 | pass 5 | 6 | 7 | # Login to MetaTrader Error 8 | class MetaTraderLoginError(Exception): 9 | "Error logging in to MetaTrader" 10 | pass 11 | 12 | 13 | # Incorrect symbol provided 14 | class MetaTraderSymbolDoesNotExistError(Exception): 15 | "One of the provided symbols does not exist" 16 | pass 17 | 18 | 19 | # Symbol unable to be enabled 20 | class MetaTraderSymbolUnableToBeEnabledError(Exception): 21 | "One of the symbols provided was not able to be enabled" 22 | pass 23 | 24 | 25 | # Algo Trading enabled on MetaTrader 5 26 | class MetaTraderAlgoTradingNotDisabledError(Exception): 27 | "Turn AlgoTrading off on MetaTrader terminal to use Python Trading Bot" 28 | pass 29 | 30 | 31 | # Error placing order 32 | class MetaTraderOrderPlacingError(Exception): 33 | "Error placing order on MetaTrader" 34 | pass 35 | 36 | 37 | # Error with balance check 38 | class MetaTraderOrderCheckError(Exception): 39 | "Error checking order on MetaTrader" 40 | pass 41 | 42 | 43 | # Error canceling order 44 | class MetaTraderCancelOrderError(Exception): 45 | "Error canceling order on MetaTrader" 46 | pass 47 | 48 | 49 | # Error modifying a position MetaTrader 50 | class MetaTraderModifyPositionError(Exception): 51 | "Error modifying position on MetaTrader" 52 | pass 53 | 54 | 55 | # Error closing a position 56 | class MetaTraderClosePositionError(Exception): 57 | "Error closing a position on MetaTrader" 58 | pass 59 | 60 | 61 | # Error for having a zero stop price on a BUY_STOP or SELL_STOP 62 | class MetaTraderIncorrectStopPriceError(Exception): 63 | "Cannot have a 0.00 price on a STOP order" 64 | pass 65 | 66 | 67 | # Error for zero ticks returned from query 68 | class MetaTraderZeroTicksDownloadedError(Exception): 69 | "Zero ticks retrieved from MetaTrader 5 Terminal" 70 | pass 71 | 72 | 73 | # SQL Error 74 | class SQLTableCreationError(Exception): 75 | "Error creating SQL table" 76 | pass 77 | 78 | # SQL Back Test Trade Action Error 79 | class SQLBacktestTradeActionError(Exception): 80 | "Error inserting SQL Trade Action" 81 | pass 82 | 83 | # Backtest error 84 | class BacktestIncorrectBacktestTimeframeError(Exception): 85 | "Incorrect timeframe selected for backtest timeframe" 86 | pass 87 | -------------------------------------------------------------------------------- /indicator_lib/bearish_engulfing.py: -------------------------------------------------------------------------------- 1 | from indicator_lib import ema_calculator 2 | from strategies import engulfing_candle_strategy 3 | 4 | 5 | # Function to calculate bearish engulfing pattern 6 | def calc_bearish_engulfing(dataframe, exchange, project_settings): 7 | """ 8 | Function to detect a bearish engulfing pattern 9 | :param dataframe: Pandas dataframe of candle data 10 | :param exchange: string 11 | :param project_settings: JSON data object 12 | :return: Bool 13 | """ 14 | 15 | # Extract the most recent candle 16 | len_most_recent = len(dataframe) - 1 17 | most_recent_candle = dataframe.loc[len_most_recent] 18 | 19 | # Extract the second most recent candle 20 | len_second_most_recent = len(dataframe) - 2 21 | second_most_recent_candle = dataframe.loc[len_second_most_recent] 22 | 23 | # Calculate if the second most recent candle is Green 24 | if second_most_recent_candle['close'] > second_most_recent_candle['open']: 25 | # Calculate if most recent candle is Red 26 | if most_recent_candle['close'] < most_recent_candle['open']: 27 | # Check the Red Body > Red Body 28 | # Red Body 29 | red_body = most_recent_candle['open'] - most_recent_candle['close'] 30 | # Green Body 31 | green_body = second_most_recent_candle['close'] - second_most_recent_candle['open'] 32 | # Compare 33 | if red_body > green_body: 34 | # Compare Red low and Green low 35 | if most_recent_candle['low'] < second_most_recent_candle['low']: 36 | # Calculate the 20-EMA 37 | ema_20 = ema_calculator.calc_ema(dataframe=dataframe, ema_size=20) 38 | # Extract the second most recent candle from the new dataframe 39 | ema_count = len(ema_20) - 2 40 | ema_second_most_recent = ema_20.loc[ema_count] 41 | # Compare 20-EMA and Green Open 42 | if ema_second_most_recent['open'] > ema_second_most_recent['ema_20']: 43 | # Use this function if you're planning on sending it to an alert generator 44 | strategy = engulfing_candle_strategy.engulfing_candle_strategy( 45 | high=most_recent_candle['high'], 46 | low=most_recent_candle['low'], 47 | symbol=most_recent_candle['symbol'], 48 | timeframe=most_recent_candle['timeframe'], 49 | exchange=exchange, 50 | alert_type="bearish_engulfing", 51 | project_settings=project_settings 52 | ) 53 | return True 54 | return False -------------------------------------------------------------------------------- /indicator_lib/bollinger_bands.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | # Function to calculate Bollinger Bands 4 | def calc_bollinger_bands(dataframe, timeperiod, std_dev_up, std_dev_down, mattype): 5 | # Create column titles 6 | upper_title = "ta_bollinger_upper_" + str(timeperiod) 7 | lower_title = "ta_bollinger_lower_" + str(timeperiod) 8 | middle_title = "ta_bollinger_middle_" + str(timeperiod) 9 | 10 | # Calculate 11 | dataframe[upper_title], dataframe[middle_title], dataframe[lower_title] = talib.BBANDS( 12 | close=dataframe['close'], 13 | timeperiod=timeperiod, 14 | nbdevup=std_dev_up, 15 | nbdevdn=std_dev_down, 16 | mattype=mattype 17 | ) 18 | # Return the dataframe 19 | return dataframe -------------------------------------------------------------------------------- /indicator_lib/bullish_engulfing.py: -------------------------------------------------------------------------------- 1 | from indicator_lib import ema_calculator 2 | from strategies import engulfing_candle_strategy 3 | 4 | 5 | # Function to calculate bullish engulfing pattern 6 | def calc_bullish_engulfing(dataframe, exchange, project_settings): 7 | """ 8 | Function to calculate if a bullish engulfing candle has been detected 9 | :param dataframe: Pandas dataframe of candle data 10 | :param exchange: string 11 | :param project_settings: JSON data object 12 | :return: Bool 13 | """ 14 | # Extract the most recent candle 15 | len_most_recent = len(dataframe) - 1 16 | most_recent_candle = dataframe.loc[len_most_recent] 17 | 18 | # Extract the second most recent candle 19 | len_second_most_recent = len(dataframe) - 2 20 | second_most_recent_candle = dataframe.loc[len_second_most_recent] 21 | 22 | # Calculate if second most recent candle Red 23 | if second_most_recent_candle['close'] < second_most_recent_candle['open']: 24 | # Calculate if most recent candle green 25 | if most_recent_candle['close'] > most_recent_candle['open']: 26 | # Check the Green Body > Red Body 27 | # Red Body 28 | red_body = second_most_recent_candle['open'] - second_most_recent_candle['close'] 29 | # Green Body 30 | green_body = most_recent_candle['close'] - second_most_recent_candle['open'] 31 | # Compare 32 | if green_body > red_body: 33 | # Compare Green High > Red High 34 | if most_recent_candle['high'] > second_most_recent_candle['high']: 35 | # Calculate the 20-EMA 36 | ema_20 = ema_calculator.calc_ema(dataframe=dataframe, ema_size=20) 37 | # Extract the second most recent candle from the new dataframe 38 | ema_count = len(ema_20) - 2 39 | ema_second_most_recent = ema_20.loc[ema_count] 40 | # Compare the EMA and Red Low 41 | if ema_second_most_recent['close'] < ema_second_most_recent['ema_20']: 42 | # If plugging into a strategy such as the Engulfing Candle Strategy, send to alerting mechanism 43 | strategy = engulfing_candle_strategy.engulfing_candle_strategy( 44 | high=most_recent_candle['high'], 45 | low=most_recent_candle['low'], 46 | symbol=most_recent_candle['symbol'], 47 | timeframe=most_recent_candle['timeframe'], 48 | exchange=exchange, 49 | alert_type="bullish_engulfing", 50 | project_settings=project_settings 51 | ) 52 | # Return true 53 | return True 54 | return False 55 | 56 | 57 | -------------------------------------------------------------------------------- /indicator_lib/calc_all_indicators.py: -------------------------------------------------------------------------------- 1 | from indicator_lib import doji_star, rsi, ta_sma, ta_ema, two_crows, three_black_crows, bollinger_bands 2 | 3 | # Calculate all the indicator_lib currently available 4 | def all_indicators(dataframe): 5 | # Copy the dataframe 6 | dataframe_copy = dataframe.copy() 7 | # SMA's 8 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 5) 9 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 8) 10 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 15) 11 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 20) 12 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 50) 13 | dataframe_copy = ta_sma.calc_ta_sma(dataframe_copy, 200) 14 | # EMA's 15 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 5) 16 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 8) 17 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 15) 18 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 20) 19 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 50) 20 | dataframe_copy = ta_ema.calc_ema(dataframe_copy, 200) 21 | # Patterns 22 | # 2 Crows 23 | dataframe_copy = two_crows.calc_two_crows(dataframe_copy) 24 | # Three black crows 25 | dataframe_copy = three_black_crows.calc_three_black_crows(dataframe_copy) 26 | # Doji Star 27 | dataframe_copy = doji_star.doji_star(dataframe_copy) 28 | # RSI 29 | dataframe_copy = rsi.rsi(dataframe_copy) 30 | # Overlap Studies 31 | #dataframe = bollinger_bands.calc_bollinger_bands(dataframe, 20, 2, 2, 0) 32 | return dataframe_copy -------------------------------------------------------------------------------- /indicator_lib/doji_star.py: -------------------------------------------------------------------------------- 1 | import talib 2 | import display_lib 3 | 4 | def doji_star(dataframe, display=False, fig=None): 5 | """ 6 | Function to calculate the doji star indicator. This is a candlestick pattern, more details can be found here: 7 | https://medium.com/me/stats/post/28c12f04caf6 8 | :param data: dataframe object where the Doji Star patterns should be detected on 9 | :param display: boolean to determine whether the Doji Star patterns should be displayed on the graph 10 | :param fig: plotly figure object to add the Doji Star patterns to 11 | :return: dataframe with Doji Star patterns identified 12 | """ 13 | # Copy the dataframe 14 | dataframe = dataframe.copy() 15 | # Add doji star column to dataframe 16 | dataframe['doji_star'] = 0 17 | # Calculate doji star on dataframe 18 | 19 | dataframe['doji_star'] = talib.CDLDOJISTAR( 20 | dataframe['open'], 21 | dataframe['high'], 22 | dataframe['low'], 23 | dataframe['close'] 24 | ) 25 | # If display is true, add the doji star to the graph 26 | if display: 27 | # 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 28 | dataframe['doji_star_value'] = dataframe.apply(lambda x: x['close'] if x['doji_star'] != 0 else 0, axis=1) 29 | # Extract the rows where doji_star_value is not zero 30 | dataframe = dataframe[dataframe['doji_star_value'] != 0] 31 | # Add doji star to graph 32 | fig = display_lib.add_markers_to_graph( 33 | base_fig=fig, 34 | dataframe=dataframe, 35 | value_column='doji_star_value', 36 | point_names='Doji Star' 37 | ) 38 | 39 | return dataframe 40 | -------------------------------------------------------------------------------- /indicator_lib/ema_calculator.py: -------------------------------------------------------------------------------- 1 | import pandas 2 | 3 | 4 | # Define function to calculate an arbitrary EMA 5 | def calc_ema(dataframe, ema_size): 6 | # Create column string 7 | ema_name = "ema_" + str(ema_size) 8 | # Create the multiplier 9 | multiplier = 2/(ema_size + 1) 10 | # Calculate the initial value (SMA) 11 | initial_mean = dataframe['close'].head(ema_size).mean() 12 | 13 | # Iterate through Dataframe 14 | for i in range(len(dataframe)): 15 | # If i equals the ema_size, substitute the first value (SMA) 16 | if i == ema_size: 17 | dataframe.loc[i, ema_name] = initial_mean 18 | # For subsequent values, use the previous EMA value to calculate the current row EMA 19 | elif i > ema_size: 20 | ema_value = dataframe.loc[i, 'close'] * multiplier + dataframe.loc[i-1, ema_name]*(1-multiplier) 21 | dataframe.loc[i, ema_name] = ema_value 22 | # Input a value of zero 23 | else: 24 | dataframe.loc[i, ema_name] = 0.00 25 | # Once loop completed, return the updated dataframe 26 | return dataframe -------------------------------------------------------------------------------- /indicator_lib/ema_cross.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | 4 | # Function to identify ema cross events 5 | def ema_cross(dataframe, ema_one, ema_two): 6 | ''' 7 | Function to identify ema cross events 8 | :param dataframe: Pandas Dataframe object 9 | :param ema_one: Column One of EMA cross 10 | :param ema_two: Column Two of EMA cross 11 | :return: 12 | ''' 13 | # Create a position column 14 | dataframe['position'] = dataframe[ema_one] > dataframe[ema_two] 15 | # Create a preposition column 16 | dataframe['pre_position'] = dataframe['position'].shift(1) 17 | # Get rid of NA values 18 | dataframe.dropna(inplace=True) 19 | # Define Crossover events 20 | dataframe['crossover'] = np.where(dataframe['position'] == dataframe['pre_position'], False, True) 21 | return dataframe 22 | 23 | -------------------------------------------------------------------------------- /indicator_lib/rsi.py: -------------------------------------------------------------------------------- 1 | import talib 2 | import display_lib 3 | 4 | 5 | # Function to calculate the RSI indicator 6 | def rsi(dataframe, period=14, display=False, fig=None): 7 | """ 8 | 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 9 | :param dataframe: dataframe object where the RSI should be calculated on 10 | :param period: period for the RSI calculation 11 | :param display: boolean to determine whether the RSI should be displayed on the graph 12 | :param fig: plotly figure object to add the RSI to 13 | :return: dataframe with RSI column added 14 | """ 15 | # Copy the dataframe 16 | dataframe = dataframe.copy() 17 | # Add RSI column to dataframe 18 | dataframe['rsi'] = 0 19 | # Calculate RSI on dataframe 20 | dataframe['rsi'] = talib.RSI(dataframe['close'], timeperiod=period) 21 | # If display is true, add the RSI to the graph 22 | if display: 23 | # todo: Figure out how to make a subplot with RSI 24 | # Add RSI to graph 25 | fig = display_lib.add_line_to_graph( 26 | base_fig=fig, 27 | dataframe=dataframe, 28 | dataframe_column='rsi', 29 | line_name='RSI' 30 | ) 31 | return dataframe 32 | -------------------------------------------------------------------------------- /indicator_lib/ta_ema.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | 4 | # Function to calculate an EMA 5 | def calc_ema(dataframe, periods): 6 | # Create the column title 7 | column_title = "ta_ema_" + str(periods) 8 | # Calculate 9 | dataframe[column_title] = talib.EMA(dataframe['close'], periods) 10 | # Return 11 | return dataframe 12 | -------------------------------------------------------------------------------- /indicator_lib/ta_sma.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | # Function to calculate an SMA with TA-Lib 4 | def calc_ta_sma(dataframe, periods): 5 | # Copy the dataframe 6 | dataframe = dataframe.copy() 7 | # Define new title for column 8 | column_title = "ta_sma_" + str(periods) 9 | # Calculate 10 | dataframe[column_title] = talib.SMA(dataframe['close'], periods) 11 | # Return dataframe 12 | return dataframe -------------------------------------------------------------------------------- /indicator_lib/three_black_crows.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | 4 | # Function to calculate the three black crows indicator 5 | def calc_three_black_crows(dataframe): 6 | # Define a new title for the column 7 | column_title = "ta_three_b_crows" 8 | # Calculate 9 | dataframe[column_title] = talib.CDL3BLACKCROWS( 10 | dataframe['open'], 11 | dataframe['high'], 12 | dataframe['low'], 13 | dataframe['close'] 14 | ) 15 | return dataframe -------------------------------------------------------------------------------- /indicator_lib/two_crows.py: -------------------------------------------------------------------------------- 1 | import talib 2 | 3 | # Function to calculate the two crows indicator 4 | def calc_two_crows(dataframe): 5 | # Define new title for column 6 | column_title = "ta_two_crows" 7 | # Calculate 8 | dataframe[column_title] = talib.CDL2CROWS( 9 | dataframe['open'], 10 | dataframe['high'], 11 | dataframe['low'], 12 | dataframe['close'] 13 | ) 14 | return dataframe -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | from metatrader_lib import mt5_interaction 4 | import pandas 5 | import display_lib 6 | from sql_lib import sql_interaction 7 | from strategies import ema_cross 8 | from backtest_lib import backtest, setup_backtest, backtest_analysis 9 | import argparse 10 | from indicator_lib import calc_all_indicators, doji_star, rsi 11 | import datetime 12 | 13 | # Variable for the location of settings.json 14 | import_filepath = "settings.json" 15 | 16 | # Global settings 17 | global exchange 18 | global explore 19 | 20 | 21 | # Function to import settings from settings.json 22 | def get_project_settings(import_filepath): 23 | """ 24 | Function to import settings from settings.json 25 | :param import_filepath: string to the location of settings.json 26 | :return: JSON object with project settings 27 | """ 28 | # Test the filepath to sure it exists 29 | if os.path.exists(import_filepath): 30 | # Open the file 31 | f = open(import_filepath, "r") 32 | # Get the information from file 33 | project_settings = json.load(f) 34 | # Close the file 35 | f.close() 36 | # Return project settings to program 37 | return project_settings 38 | else: 39 | return ImportError 40 | 41 | 42 | def check_exchanges(project_settings): 43 | """ 44 | Function to check if exchanges are working 45 | :param project_settings: 46 | :return: Bool 47 | """ 48 | # Check MT5 Live trading 49 | mt5_live_check = mt5_interaction.start_mt5( 50 | username=project_settings["mt5"]["live"]["username"], 51 | password=project_settings["mt5"]["live"]["password"], 52 | server=project_settings["mt5"]["live"]["server"], 53 | path=project_settings["mt5"]["live"]["mt5Pathway"], 54 | ) 55 | if not mt5_live_check: 56 | print("MT5 Live Connection Error") 57 | raise PermissionError 58 | # Check MT5 Paper Trading 59 | mt5_paper_check = mt5_interaction.start_mt5( 60 | username=project_settings["mt5"]["paper"]["username"], 61 | password=project_settings["mt5"]["paper"]["password"], 62 | server=project_settings["mt5"]["paper"]["server"], 63 | path=project_settings["mt5"]["paper"]["mt5Pathway"], 64 | ) 65 | if not mt5_paper_check: 66 | print("MT5 Paper Connection Error") 67 | raise PermissionError 68 | 69 | # Return True if all steps pass 70 | return True 71 | 72 | 73 | # Function to add arguments to script 74 | def add_arguments(parser): 75 | """ 76 | Function to add arguments to the parser 77 | :param parser: parser object 78 | :return: updated parser object 79 | """ 80 | # Add Options 81 | # Explore Option 82 | parser.add_argument( 83 | "-e", 84 | "--Explore", 85 | help="Use this to explore the data", 86 | action="store_true" 87 | ) 88 | # Display Option 89 | parser.add_argument( 90 | "-d", 91 | "--Display", 92 | help="Use this to display the data", 93 | action="store_true" 94 | ) 95 | # All Indicators Option 96 | parser.add_argument( 97 | "-a", 98 | "--all_indicators", 99 | help="Select all indicator_lib", 100 | action="store_true" 101 | ) 102 | # Doji Star Option 103 | parser.add_argument( 104 | "--doji_star", 105 | help="Select doji star indicator to be calculated", 106 | action="store_true" 107 | ) 108 | # RSI Option 109 | parser.add_argument( 110 | "--rsi", 111 | help="Select RSI indicator to be calculated", 112 | action="store_true" 113 | ) 114 | 115 | # Add Arguments 116 | parser.add_argument( 117 | "-x", 118 | "--Exchange", 119 | help="Set which exchange you will be using" 120 | ) 121 | # Custom Symbol 122 | parser.add_argument( 123 | "--symbol", 124 | help="Use this to use a custom symbol with the Explore option" 125 | ) 126 | # Custom Timeframe 127 | parser.add_argument( 128 | "-t", 129 | "--timeframe", 130 | help="Select a timeframe to explore data" 131 | ) 132 | return parser 133 | 134 | 135 | # Function to parse provided options 136 | def parse_arguments(args_parser_variable): 137 | """ 138 | Function to parse provided arguments and improve from there 139 | :param args_parser_variable: 140 | :return: True when completed 141 | """ 142 | 143 | 144 | # Check if data exploration selected 145 | if args_parser_variable.Explore: 146 | print("Data exploration selected") 147 | # Check for exchange 148 | if args_parser_variable.Exchange: 149 | if args_parser_variable.Exchange == "metatrader": 150 | global exchange 151 | exchange = "mt5" 152 | print(f"Exchange selected: {exchange}") 153 | # Check for Timeframe 154 | if args_parser_variable.timeframe: 155 | print(f"Timeframe selected: {args_parser_variable.timeframe}") 156 | else: 157 | print("No timeframe selected") 158 | raise SystemExit(1) 159 | # Check for Symbol 160 | if args_parser_variable.symbol: 161 | print(f"Symbol selected: {args_parser_variable.symbol}") 162 | else: 163 | print("No symbol selected") 164 | raise SystemExit(1) 165 | return True 166 | else: 167 | print("No exchange selected") 168 | raise SystemExit(1) 169 | 170 | return False 171 | 172 | 173 | # Function to manage data exploration 174 | def manage_exploration(args): 175 | """ 176 | Function to manage data exploration when --Explore option selected 177 | :param args: system arguments 178 | :return: dataframe 179 | """ 180 | if args.Exchange == "metatrader": 181 | # Retreive a large amount of data 182 | data = mt5_interaction.query_historic_data( 183 | symbol=args.symbol, 184 | timeframe=args.timeframe, 185 | number_of_candles=1000 186 | ) 187 | # Convert to a dataframe 188 | data = pandas.DataFrame(data) 189 | # Retrieve whatever indicator_lib have been selected 190 | # If all indicators selected, calculate all of them 191 | if args.all_indicators: 192 | print(f"All indicators selected. Calculation may take some time") 193 | indicator_dataframe = calc_all_indicators.all_indicators( 194 | dataframe=data 195 | ) 196 | return indicator_dataframe 197 | else: 198 | # If display is true, construct the base figure 199 | if args.Display: 200 | # Add a column 'human_time' to the dataframe which converts the unix time to human readable 201 | data['human_time'] = data['time'].apply(lambda x: datetime.datetime.fromtimestamp(x).strftime('%Y-%m-%d %H:%M:%S')) 202 | fig = display_lib.construct_base_candlestick_graph( 203 | dataframe=data, 204 | candlestick_title=f"{args.symbol} {args.timeframe} Data Explorer" 205 | ) 206 | # Check for doji_star 207 | if args.doji_star and args.Display: 208 | print(f"Doji Star selected with display") 209 | indicator_dataframe = doji_star.doji_star( 210 | dataframe=data, 211 | display=True, 212 | fig=fig 213 | ) 214 | # Check for RSI 215 | if args.rsi: 216 | print(f"RSI selected") 217 | indicator_dataframe = rsi.rsi( 218 | dataframe=data, 219 | display=True, 220 | fig=fig 221 | ) 222 | else: 223 | # Check for doji_star 224 | if args.doji_star: 225 | print(f"Doji Star selected") 226 | indicator_dataframe = doji_star.doji_star( 227 | dataframe=data 228 | ) 229 | # Check for RSI 230 | if args.rsi: 231 | print(f"RSI selected") 232 | indicator_dataframe = rsi.rsi( 233 | dataframe=data 234 | ) 235 | 236 | # If display is true, once all indicators have been calculated, display the figure 237 | if args.Display: 238 | print("Displaying data") 239 | display_lib.display_graph( 240 | plotly_fig=fig, 241 | graph_title=f"{args.symbol} {args.timeframe} Data Explorer", 242 | dash=False 243 | ) 244 | 245 | # Once all indicators have been calculated, return the dataframe 246 | return indicator_dataframe 247 | 248 | 249 | else: 250 | print("No exchange selected") 251 | raise SystemExit(1) 252 | 253 | 254 | # Press the green button in the gutter to run the script. 255 | if __name__ == '__main__': 256 | # Import project settings 257 | project_settings = get_project_settings(import_filepath=import_filepath) 258 | # Check exchanges 259 | check_exchanges(project_settings) 260 | # Show all columns pandas 261 | pandas.set_option('display.max_columns', None) 262 | #pandas.set_option('display.max_rows', None) 263 | # Setup arguments to the script 264 | parser = argparse.ArgumentParser() 265 | # Update with options 266 | parser = add_arguments(parser=parser) 267 | # Get the arguments 268 | args = parser.parse_args() 269 | explore = parse_arguments(args_parser_variable=args) 270 | # Branch based upon options 271 | if explore: 272 | manage_exploration(args=args) 273 | else: 274 | data = manage_exploration(args=args) 275 | print(data) 276 | 277 | 278 | 279 | -------------------------------------------------------------------------------- /metatrader_lib/mt5_interaction.py: -------------------------------------------------------------------------------- 1 | import MetaTrader5 2 | import pandas 3 | import datetime 4 | import pytz 5 | 6 | import exceptions 7 | 8 | 9 | # Function to start Meta Trader 5 (MT5) 10 | def start_mt5(username, password, server, path): 11 | """ 12 | Initializes and logs into MT5 13 | :param username: 8 digit integer 14 | :param password: string 15 | :param server: string 16 | :param path: string 17 | :return: True if successful, Error if not 18 | """ 19 | # Ensure that all variables are the correct type 20 | uname = int(username) # Username must be an int 21 | pword = str(password) # Password must be a string 22 | trading_server = str(server) # Server must be a string 23 | filepath = str(path) # Filepath must be a string 24 | 25 | # Attempt to start MT5 26 | try: 27 | metaTrader_init = MetaTrader5.initialize(login=uname, password=pword, server=trading_server, path=filepath) 28 | except Exception as e: 29 | print(f"Error initializing MetaTrader: {e}") 30 | raise exceptions.MetaTraderInitializeError 31 | 32 | # Attempt to login to MT5 33 | if not metaTrader_init: 34 | raise exceptions.MetaTraderInitializeError 35 | else: 36 | try: 37 | metaTrader_login = MetaTrader5.login(login=uname, password=pword, server=trading_server) 38 | except Exception as e: 39 | print(f"Error loging in to MetaTrader: {e}") 40 | raise exceptions.MetaTraderLoginError 41 | 42 | # Return True if initialization and login are successful 43 | if metaTrader_login: 44 | return True 45 | 46 | 47 | # Function to initialize a symbol on MT5 48 | def initialize_symbols(symbol_array): 49 | """ 50 | Function to initialize a symbol on MT5. Note that different brokers have different symbols. 51 | To read more: https://trading-data-analysis.pro/everything-you-need-to-connect-your-python-trading-bot-to-metatrader-5-de0d8fb80053 52 | :param symbol_array: List of symbols to be initialized 53 | :return: True if all symbols enabled 54 | """ 55 | # Get a list of all symbols supported in MT5 56 | all_symbols = MetaTrader5.symbols_get() 57 | # Create a list to store all the symbols 58 | symbol_names = [] 59 | # Add the retrieved symbols to the list 60 | for symbol in all_symbols: 61 | symbol_names.append(symbol.name) 62 | 63 | # Check each provided symbol in symbol_array to ensure it exists 64 | for provided_symbol in symbol_array: 65 | if provided_symbol in symbol_names: 66 | # If it exists, enable 67 | if MetaTrader5.symbol_select(provided_symbol, True): 68 | pass 69 | else: 70 | # Print the outcome to screen. Custom Logging/Error Handling not yet created 71 | print(f"Error creating symbol {provided_symbol}. Symbol not enabled.") 72 | # Return a generic value error. Custom Error Handling not yet created. 73 | raise exceptions.MetaTraderSymbolUnableToBeEnabledError 74 | else: 75 | # Print the outcome to screen. Custom Logging/Error Handling not yet created 76 | print(f"Symbol {provided_symbol} does not exist in this MT5 implementation. Symbol not enabled.") 77 | # Return a generic syntax error. Custom Error Handling not yet enabled 78 | raise exceptions.MetaTraderSymbolDoesNotExistError 79 | # Return true if all symbols enabled 80 | return True 81 | 82 | 83 | # Function to place a trade on MT5 84 | def place_order(order_type, symbol, volume, stop_loss, take_profit, comment, direct=False, price=0): 85 | """ 86 | Function to place a trade on MetaTrader 5 with option to check balance first 87 | :param order_type: String from options: SELL_STOP, BUY_STOP, SELL, BUY 88 | :param symbol: String 89 | :param volume: String or Float 90 | :param stop_loss: String or Float 91 | :param take_profit: String of Float 92 | :param comment: String 93 | :param direct: Bool, defaults to False 94 | :param price: String or Float, optional 95 | :return: Trade outcome or syntax error 96 | """ 97 | 98 | # Set up the place order request 99 | request = { 100 | "symbol": symbol, 101 | "volume": volume, 102 | "sl": round(stop_loss, 3), 103 | "tp": round(take_profit, 3), 104 | "type_time": MetaTrader5.ORDER_TIME_GTC, 105 | "comment": comment 106 | } 107 | 108 | # Create the order type based upon provided values. This can be expanded for different order types as needed. 109 | if order_type == "SELL_STOP": 110 | request['type'] = MetaTrader5.ORDER_TYPE_SELL_STOP 111 | request['action'] = MetaTrader5.TRADE_ACTION_PENDING 112 | if price <= 0: 113 | raise exceptions.MetaTraderIncorrectStopPriceError 114 | else: 115 | request['price'] = round(price, 3) 116 | request['type_filling'] = MetaTrader5.ORDER_FILLING_RETURN 117 | elif order_type == "BUY_STOP": 118 | request['type'] = MetaTrader5.ORDER_TYPE_BUY_STOP 119 | request['action'] = MetaTrader5.TRADE_ACTION_PENDING 120 | if price <= 0: 121 | raise exceptions.MetaTraderIncorrectStopPriceError 122 | else: 123 | request['price'] = round(price, 3) 124 | request['type_filling'] = MetaTrader5.ORDER_FILLING_RETURN 125 | 126 | elif order_type == "SELL": 127 | request['type'] = MetaTrader5.ORDER_TYPE_SELL 128 | request['action'] = MetaTrader5.TRADE_ACTION_DEAL 129 | request['type_filling'] = MetaTrader5.ORDER_FILLING_IOC 130 | elif order_type == "BUY": 131 | request['type'] = MetaTrader5.ORDER_TYPE_BUY 132 | request['action'] = MetaTrader5.TRADE_ACTION_DEAL 133 | request['type_filling'] = MetaTrader5.ORDER_FILLING_IOC 134 | else: 135 | print("Choose a valid order type from SELL_STOP, BUY_STOP, SELL, BUY") 136 | raise SyntaxError 137 | 138 | if direct is True: 139 | # Send the order to MT5 140 | order_result = MetaTrader5.order_send(request) 141 | # Notify based on return outcomes 142 | if order_result[0] == 10009: 143 | # Print result 144 | # print(f"Order for {symbol} successful") # Enable if error checking order_result 145 | return order_result[2] 146 | elif order_result[0] == 10027: 147 | # Turn off autotrading 148 | print(f"Turn off Algo Trading on MT5 Terminal") 149 | raise exceptions.MetaTraderAlgoTradingNotDisabledError 150 | else: 151 | # Print result 152 | print(f"Error placing order. ErrorCode {order_result[0]}, Error Details: {order_result}") 153 | raise exceptions.MetaTraderOrderPlacingError 154 | 155 | else: 156 | # Check the order 157 | result = MetaTrader5.order_check(request) 158 | if result[0] == 0: 159 | # print("Balance Check Successful") # Enable to error check Balance Check 160 | # If order check is successful, place the order. Little bit of recursion for fun. 161 | place_order( 162 | order_type=order_type, 163 | symbol=symbol, 164 | volume=volume, 165 | price=price, 166 | stop_loss=stop_loss, 167 | take_profit=take_profit, 168 | comment=comment, 169 | direct=True 170 | ) 171 | else: 172 | print(f"Order unsucessful. Details: {result}") 173 | raise exceptions.MetaTraderOrderCheckError 174 | 175 | # Function to cancel an order 176 | def cancel_order(order_number): 177 | """ 178 | Function to cancel an order 179 | :param order_number: Int 180 | :return: 181 | """ 182 | # Create the request 183 | request = { 184 | "action": MetaTrader5.TRADE_ACTION_REMOVE, 185 | "order": order_number, 186 | "comment": "Order Removed" 187 | } 188 | # Send order to MT5 189 | order_result = MetaTrader5.order_send(request) 190 | if order_result[0] == 10009: 191 | return True 192 | else: 193 | print(f"Error cancelling order. Details: {order_result}") 194 | raise exceptions.MetaTraderCancelOrderError 195 | 196 | 197 | # Function to modify an open position 198 | def modify_position(order_number, symbol, new_stop_loss, new_take_profit): 199 | """ 200 | Function to modify a position 201 | :param order_number: Int 202 | :param symbol: String 203 | :param new_stop_loss: Float 204 | :param new_take_profit: Float 205 | :return: Boolean 206 | """ 207 | # Create the request 208 | request = { 209 | "action": MetaTrader5.TRADE_ACTION_SLTP, 210 | "symbol": symbol, 211 | "sl": new_stop_loss, 212 | "tp": new_take_profit, 213 | "position": order_number 214 | } 215 | # Send order to MT5 216 | order_result = MetaTrader5.order_send(request) 217 | if order_result[0] == 10009: 218 | return True 219 | else: 220 | print(f"Error modifying position. Details: {order_result}") 221 | raise exceptions.MetaTraderModifyPositionError 222 | 223 | 224 | # Function to retrieve all open orders from MT5 225 | def get_open_orders(): 226 | """ 227 | Function to retrieve a list of open orders from MetaTrader 5 228 | :return: List of open orders 229 | """ 230 | orders = MetaTrader5.orders_get() 231 | order_array = [] 232 | for order in orders: 233 | order_array.append(order[0]) 234 | return order_array 235 | 236 | 237 | # Function to retrieve all open positions 238 | def get_open_positions(): 239 | """ 240 | Function to retrieve a list of open orders from MetaTrader 5 241 | :return: list of positions 242 | """ 243 | # Get position objects 244 | positions = MetaTrader5.positions_get() 245 | # Return position objects 246 | return positions 247 | 248 | 249 | # Function to close an open position 250 | def close_position(order_number, symbol, volume, order_type, price, comment): 251 | """ 252 | Function to close an open position from MetaTrader 5 253 | :param order_number: int 254 | :return: Boolean 255 | """ 256 | # Create the request 257 | request = { 258 | 'action': MetaTrader5.TRADE_ACTION_DEAL, 259 | 'symbol': symbol, 260 | 'volume': volume, 261 | 'position': order_number, 262 | 'price': price, 263 | 'type_time': MetaTrader5.ORDER_TIME_GTC, 264 | 'type_filling': MetaTrader5.ORDER_FILLING_IOC, 265 | 'comment': comment 266 | } 267 | 268 | if order_type == "SELL": 269 | request['type'] = MetaTrader5.ORDER_TYPE_SELL 270 | elif order_type == "BUY": 271 | request['type'] = MetaTrader5.ORDER_TYPE_BUY 272 | else: 273 | print(f"Incorrect syntax for position close {order_type}") 274 | raise SyntaxError 275 | 276 | # Place the order 277 | result = MetaTrader5.order_send(request) 278 | if result[0] == 10009: 279 | return True 280 | else: 281 | print(f"Error closing position. Details: {result}") 282 | raise exceptions.MetaTraderClosePositionError 283 | 284 | 285 | # Function to convert a timeframe string in MetaTrader 5 friendly format 286 | def set_query_timeframe(timeframe): 287 | # Implement a Pseudo Switch statement. Note that Python 3.10 implements match / case but have kept it this way for 288 | # backwards integration 289 | if timeframe == "M1": 290 | return MetaTrader5.TIMEFRAME_M1 291 | elif timeframe == "M2": 292 | return MetaTrader5.TIMEFRAME_M2 293 | elif timeframe == "M3": 294 | return MetaTrader5.TIMEFRAME_M3 295 | elif timeframe == "M4": 296 | return MetaTrader5.TIMEFRAME_M4 297 | elif timeframe == "M5": 298 | return MetaTrader5.TIMEFRAME_M5 299 | elif timeframe == "M6": 300 | return MetaTrader5.TIMEFRAME_M6 301 | elif timeframe == "M10": 302 | return MetaTrader5.TIMEFRAME_M10 303 | elif timeframe == "M12": 304 | return MetaTrader5.TIMEFRAME_M12 305 | elif timeframe == "M15": 306 | return MetaTrader5.TIMEFRAME_M15 307 | elif timeframe == "M20": 308 | return MetaTrader5.TIMEFRAME_M20 309 | elif timeframe == "M30": 310 | return MetaTrader5.TIMEFRAME_M30 311 | elif timeframe == "H1": 312 | return MetaTrader5.TIMEFRAME_H1 313 | elif timeframe == "H2": 314 | return MetaTrader5.TIMEFRAME_H2 315 | elif timeframe == "H3": 316 | return MetaTrader5.TIMEFRAME_H3 317 | elif timeframe == "H4": 318 | return MetaTrader5.TIMEFRAME_H4 319 | elif timeframe == "H6": 320 | return MetaTrader5.TIMEFRAME_H6 321 | elif timeframe == "H8": 322 | return MetaTrader5.TIMEFRAME_H8 323 | elif timeframe == "H12": 324 | return MetaTrader5.TIMEFRAME_H12 325 | elif timeframe == "D1": 326 | return MetaTrader5.TIMEFRAME_D1 327 | elif timeframe == "W1": 328 | return MetaTrader5.TIMEFRAME_W1 329 | elif timeframe == "MN1": 330 | return MetaTrader5.TIMEFRAME_MN1 331 | else: 332 | print(f"Incorrect timeframe provided. {timeframe}") 333 | raise ValueError 334 | 335 | 336 | # Function to query previous candlestick data from MT5 337 | def query_historic_data(symbol, timeframe, number_of_candles): 338 | # Convert the timeframe into an MT5 friendly format 339 | mt5_timeframe = set_query_timeframe(timeframe) 340 | # Retrieve data from MT5 341 | rates = MetaTrader5.copy_rates_from_pos(symbol, mt5_timeframe, 1, number_of_candles) 342 | return rates 343 | 344 | 345 | # Function to retrieve latest tick for a symbol 346 | def retrieve_latest_tick(symbol): 347 | """ 348 | Function to retrieve the latest tick for a symbol 349 | :param symbol: String 350 | :return: Dictionary object 351 | """ 352 | # Retrieve the tick information 353 | tick = MetaTrader5.symbol_info_tick(symbol)._asdict() 354 | spread = tick['ask'] - tick['bid'] 355 | tick['spread'] = spread 356 | return tick 357 | 358 | 359 | # Function to retrieve ticks from a time range 360 | def retrieve_tick_time_range(start_time_utc, finish_time_utc, symbol, dataframe=False): 361 | # Set option in MT5 terminal for Unlimited bars 362 | # Check time format of start time 363 | if type(start_time_utc) != datetime.datetime: 364 | print(f"Time range tick start time is in incorrect format") 365 | raise ValueError 366 | # Check time format of finish time 367 | if type(finish_time_utc) != datetime.datetime: 368 | print(f"Time range tick finish time is in incorrect format") 369 | raise ValueError 370 | # Retrieve ticks 371 | ticks = MetaTrader5.copy_ticks_range(symbol, start_time_utc, finish_time_utc, MetaTrader5.COPY_TICKS_INFO) 372 | # Convert into dataframe only if Dataframe set to True 373 | if dataframe: 374 | # Convert into a dataframe 375 | ticks_data_frame = pandas.DataFrame(ticks) 376 | # Add spread 377 | ticks_data_frame['spread'] = ticks_data_frame['ask'] - ticks_data_frame['bid'] 378 | # Add symbol 379 | ticks_data_frame['symbol'] = symbol 380 | # Format integers into signed integers (postgres doesn't support unsigned int) 381 | ticks_data_frame['time'] = ticks_data_frame['time'].astype('int64') 382 | ticks_data_frame['volume'] = ticks_data_frame['volume'].astype('int64') 383 | ticks_data_frame['time_msc'] = ticks_data_frame['time_msc'].astype('int64') 384 | return ticks_data_frame 385 | return ticks 386 | 387 | 388 | # Function to retrieve candlestick data for a specified time range 389 | def retrieve_candlestick_data_range(start_time_utc, finish_time_utc, symbol, timeframe, dataframe=False): 390 | # Set option in MT5 terminal for Unlimited bars 391 | # Check time format of start time 392 | if type(start_time_utc) != datetime.datetime: 393 | print(f"Time range tick start time is in incorrect format") 394 | raise ValueError 395 | # Check time format of finish time 396 | if type(finish_time_utc) != datetime.datetime: 397 | print(f"Time range tick finish time is in incorrect format") 398 | raise ValueError 399 | # Convert the timeframe into MT5 compatible format 400 | timeframe_value = set_query_timeframe(timeframe) 401 | # Retrieve the data 402 | candlestick_data = MetaTrader5.copy_rates_range(symbol, timeframe_value, start_time_utc, finish_time_utc) 403 | if dataframe: 404 | # Convert to a dataframe 405 | candlestick_dataframe = pandas.DataFrame(candlestick_data) 406 | # Add in symbol and timeframe columns 407 | candlestick_dataframe['symbol'] = symbol 408 | candlestick_dataframe['timeframe'] = timeframe 409 | # Convert integers into signed integers (postgres doesn't support unsigned int) 410 | candlestick_dataframe['time'] = candlestick_dataframe['time'].astype('int64') 411 | candlestick_dataframe['tick_volume'] = candlestick_dataframe['tick_volume'].astype('float64') 412 | candlestick_dataframe['spread'] = candlestick_dataframe['spread'].astype('int64') 413 | candlestick_dataframe['real_volume'] = candlestick_dataframe['real_volume'].astype('int64') 414 | # Return completed dataframe 415 | return candlestick_dataframe 416 | else: 417 | return candlestick_data 418 | 419 | 420 | -------------------------------------------------------------------------------- /sql_lib/sql_interaction.py: -------------------------------------------------------------------------------- 1 | import psycopg2 2 | import psycopg2.extras 3 | from sqlalchemy import create_engine 4 | import pandas 5 | 6 | # Function to connect to PostgreSQL database 7 | import exceptions 8 | 9 | 10 | def postgres_connect(project_settings): 11 | """ 12 | Function to connect to PostgreSQL database 13 | :param project_settings: json object 14 | :return: connection object 15 | """ 16 | # Define the connection 17 | try: 18 | conn = psycopg2.connect( 19 | database=project_settings['postgres']['database'], 20 | user=project_settings['postgres']['user'], 21 | password=project_settings['postgres']['password'], 22 | host=project_settings['postgres']['host'], 23 | port=project_settings['postgres']['port'] 24 | ) 25 | return conn 26 | except Exception as e: 27 | print(f"Error connecting to Postgres: {e}") 28 | return False 29 | 30 | 31 | # Function to execute SQL 32 | def sql_execute(sql_query, project_settings): 33 | """ 34 | Function to execute SQL statements 35 | :param sql_query: String 36 | :return: Boolean 37 | """ 38 | # Create a connection 39 | conn = postgres_connect(project_settings=project_settings) 40 | # Execute the query 41 | try: 42 | # print(sql_query) 43 | # Create the cursor 44 | cursor = conn.cursor() 45 | # Execute the cursor query 46 | cursor.execute(sql_query) 47 | # Commit the changes 48 | conn.commit() 49 | return True 50 | except (Exception, psycopg2.Error) as e: 51 | print(f"Failed to execute query: {e}") 52 | raise e 53 | finally: 54 | # If conn has completed, close 55 | if conn is not None: 56 | conn.close() 57 | 58 | 59 | # Function to create a table 60 | def create_sql_table(table_name, table_details, project_settings, id=True): 61 | """ 62 | Function to create a table in SQL 63 | :param table_name: String 64 | :param table_details: String 65 | :param project_settings: JSON Object 66 | :return: Boolean 67 | """ 68 | # Create the query string 69 | if id: 70 | # Create an auto incrementing primary key 71 | sql_query = f"CREATE TABLE {table_name} (id SERIAL PRIMARY KEY, {table_details})" 72 | else: 73 | # Create without an auto incrementing primary key 74 | sql_query = f"CREATE TABLE {table_name} (id BIGINT NOT NULL, {table_details})" 75 | # Execute the query 76 | create_table = sql_execute(sql_query=sql_query, project_settings=project_settings) 77 | if create_table: 78 | return True 79 | raise exceptions.SQLTableCreationError 80 | 81 | 82 | # Function to create a balance tracking table 83 | def create_balance_tracker_table(table_name, project_settings): 84 | table_details = "strategy VARCHAR(100) NOT NULL," \ 85 | "symbol VARCHAR(100) NOT NULL," \ 86 | "comment VARCHAR(100) NOT NULL," \ 87 | "note VARCHAR(100) NOT NULL," \ 88 | "balance FLOAT4 NOT NULL," \ 89 | "equity FLOAT4 NOT NULL," \ 90 | "profit_or_loss FLOAT4 NOT NULL," \ 91 | "order_id BIGINT NOT NULL," \ 92 | "time FLOAT4 NOT NULL" 93 | # Pass to Create Table function 94 | return create_sql_table(table_name=table_name, table_details=table_details, project_settings=project_settings) 95 | 96 | 97 | # Function to add an entry to balance tracking table 98 | def insert_balance_change(trade_object, note, balance, equity, profit_or_loss, order_id, time, project_settings): 99 | sql_query = f"INSERT INTO {trade_object['balance_tracker_table']} (strategy, symbol, comment, note, balance," \ 100 | f"equity, profit_or_loss, order_id, time) VALUES (" \ 101 | f"'{trade_object['strategy']}'," \ 102 | f"'{trade_object['symbol']}'," \ 103 | f"'{trade_object['comment']}'," \ 104 | f"'{note}'," \ 105 | f"'{balance}'," \ 106 | f"'{equity}'," \ 107 | f"'{profit_or_loss}'," \ 108 | f"'{order_id}'," \ 109 | f"'{time}'" \ 110 | f");" 111 | # Execute the query 112 | return sql_execute(sql_query=sql_query, project_settings=project_settings) 113 | 114 | 115 | # Function to retrieve data from SQL 116 | def get_data(sql_query, project_settings): 117 | conn = postgres_connect(project_settings) 118 | cur = conn.cursor() 119 | cur.execute(sql_query) 120 | result = cur.fetchall() 121 | return result 122 | 123 | 124 | # Function to create a trade table 125 | def create_trade_table(table_name, project_settings): 126 | """ 127 | Function to create a trade table in SQL 128 | :param table_name: string 129 | :param project_settings: JSON Object 130 | :return: Boolean 131 | """ 132 | # Define the table according to the CIM: 133 | # https://github.com/jimtin/python_trading_bot/blob/master/common_information_model.json 134 | table_details = f"strategy VARCHAR(100) NOT NULL," \ 135 | f"exchange VARCHAR(100) NOT NULL," \ 136 | f"trade_type VARCHAR(50) NOT NULL," \ 137 | f"trade_stage VARCHAR(50) NOT NULL," \ 138 | f"symbol VARCHAR(50) NOT NULL," \ 139 | f"volume FLOAT4 NOT NULL," \ 140 | f"stop_loss FLOAT4 NOT NULL," \ 141 | f"take_profit FLOAT4 NOT NULL," \ 142 | f"price FLOAT4 NOT NULL," \ 143 | f"comment VARCHAR(250) NOT NULL," \ 144 | f"status VARCHAR(100) NOT NULL," \ 145 | f"order_id VARCHAR(100) NOT NULL" 146 | # Pass to Create Table function 147 | return create_sql_table(table_name=table_name, table_details=table_details, project_settings=project_settings) 148 | 149 | 150 | # Function to insert a trade action into SQL database 151 | def insert_trade_action(table_name, trade_information, project_settings, backtest=False): 152 | """ 153 | Function to insert a row of trade data 154 | :param table_name: String 155 | :param trade_information: Dictionary 156 | :return: Bool 157 | """ 158 | # Make sure that only valid tables entered 159 | if table_name == "paper_trade_table" or table_name == "live_trade_table": 160 | # Make trade_information shorter 161 | ti = trade_information 162 | # Construct the SQL Query 163 | sql_query = f"INSERT INTO {table_name} (strategy, exchange, trade_type, trade_stage, symbol, volume, stop_loss, " \ 164 | f"take_profit, price, comment, status, order_id) VALUES (" \ 165 | f"'{ti['strategy']}'," \ 166 | f"'{ti['exchange']}'," \ 167 | f"'{ti['trade_type']}'," \ 168 | f"'{ti['trade_stage']}'," \ 169 | f"'{ti['symbol']}'," \ 170 | f"{ti['volume']}," \ 171 | f"{ti['stop_loss']}," \ 172 | f"{ti['take_profit']}," \ 173 | f"{ti['price']}," \ 174 | f"'{ti['comment']}'," \ 175 | f"'{ti['status']}'," \ 176 | f"'{ti['order_id']}'" \ 177 | f")" 178 | # Execute the query 179 | return sql_execute(sql_query=sql_query, project_settings=project_settings) 180 | elif backtest: 181 | sql_query = f"INSERT INTO {table_name} " 182 | else: 183 | # Return an exception 184 | return Exception # Custom Error Handling Coming Soon 185 | 186 | 187 | # Function to insert a live trade action into SQL database 188 | def insert_live_trade_action(trade_information, project_settings): 189 | """ 190 | Function to insert a row of trade data into the table live_trade_table 191 | :param trade_information: Dictionary object of trade 192 | :param project_settings: Dictionary object of project details 193 | :return: Bool 194 | """ 195 | return insert_trade_action( 196 | table_name="live_trade_table", 197 | trade_information=trade_information, 198 | project_settings=project_settings 199 | ) 200 | 201 | 202 | # Function to insert a paper trade action into SQL database 203 | def insert_paper_trade_action(trade_information, project_settings): 204 | """ 205 | Function to insert a row of trade data into the table paper_trade_table 206 | :param trade_information: Dictionary object of trade details 207 | :param project_settings: Dictionary object of project details 208 | :return: Bool 209 | """ 210 | return insert_trade_action( 211 | table_name="paper_trade_table", 212 | trade_information=trade_information, 213 | project_settings=project_settings 214 | ) 215 | 216 | 217 | # Function to create a backtest tick table 218 | def create_mt5_backtest_tick_table(table_name, project_settings): 219 | # Define the columns in the table 220 | table_details = f"symbol VARCHAR(100) NOT NULL," \ 221 | f"time BIGINT NOT NULL," \ 222 | f"bid FLOAT4 NOT NULL," \ 223 | f"ask FLOAT4 NOT NULL," \ 224 | f"spread FLOAT4 NOT NULL," \ 225 | f"last FLOAT4 NOT NULL," \ 226 | f"volume FLOAT4 NOT NULL," \ 227 | f"flags BIGINT NOT NULL," \ 228 | f"volume_real FLOAT4 NOT NULL," \ 229 | f"time_msc BIGINT NOT NULL," \ 230 | f"human_time DATE NOT NULL," \ 231 | f"human_time_msc DATE NOT NULL" 232 | # Create the table 233 | return create_sql_table(table_name=table_name, table_details=table_details, project_settings=project_settings, 234 | id=False) 235 | 236 | 237 | # Function to create a candlestick backtest table 238 | def create_mt5_backtest_raw_candlestick_table(table_name, project_settings): 239 | # Define the columns in the table 240 | table_details = f"symbol VARCHAR(100) NOT NULL," \ 241 | f"time BIGINT NOT NULL," \ 242 | f"timeframe VARCHAR(100) NOT NULL," \ 243 | f"open FLOAT4 NOT NULL," \ 244 | f"high FLOAT4 NOT NULL," \ 245 | f"low FLOAT4 NOT NULL," \ 246 | f"close FLOAT4 NOT NULL," \ 247 | f"tick_volume FLOAT4 NOT NULL," \ 248 | f"spread FLOAT4 NOT NULL," \ 249 | f"real_volume FLOAT4 NOT NULL," \ 250 | f"human_time DATE NOT NULL," \ 251 | f"human_time_msc DATE NOT NULL" 252 | # Create the table 253 | return create_sql_table(table_name=table_name, table_details=table_details, project_settings=project_settings) 254 | 255 | 256 | # Function to create a trade backtest table 257 | def create_mt5_backtest_trade_table(table_name, project_settings): 258 | # Define the table according to the CIM: 259 | # https://github.com/jimtin/python_trading_bot/blob/master/common_information_model.json 260 | table_details = f"strategy VARCHAR(100) NOT NULL," \ 261 | f"exchange VARCHAR(100) NOT NULL," \ 262 | f"trade_type VARCHAR(50) NOT NULL," \ 263 | f"trade_stage VARCHAR(50) NOT NULL," \ 264 | f"symbol VARCHAR(50) NOT NULL," \ 265 | f"qty_purchased FLOAT4 NOT NULL," \ 266 | f"leverage FLOAT4 NOT NULL," \ 267 | f"stop_loss FLOAT4 NOT NULL," \ 268 | f"take_profit FLOAT4 NOT NULL," \ 269 | f"price FLOAT4 NOT NULL," \ 270 | f"comment VARCHAR(250) NOT NULL," \ 271 | f"status VARCHAR(100) NOT NULL," \ 272 | f"order_id VARCHAR(100) NOT NULL," \ 273 | f"balance FLOAT4 NOT NULL," \ 274 | f"equity FLOAT4 NOT NULL," \ 275 | f"update_time FLOAT4 NOT NULL," \ 276 | f"entry_price FLOAT4 NOT NULL," \ 277 | f"exit_price FLOAT4 NOT NULL" 278 | return create_sql_table(table_name=table_name, table_details=table_details, project_settings=project_settings, 279 | id=True) 280 | 281 | 282 | # Function to write to SQL from csv file 283 | def upload_from_csv(csv_location, table_name, project_settings): 284 | conn = postgres_connect(project_settings) 285 | cur = conn.cursor() 286 | with open(csv_location, 'r') as f: 287 | cur.copy_from(f, table_name, ',') 288 | 289 | f.close() 290 | 291 | conn.commit() 292 | conn.close() 293 | 294 | 295 | # Function to retrieve dataframe from Postgres table 296 | def retrieve_dataframe(table_name, project_settings, chunky=False, tick_data=False): 297 | # Create the connection object for PostgreSQL 298 | engine_string = f"postgresql://{project_settings['postgres']['user']}:{project_settings['postgres']['password']}@" \ 299 | f"{project_settings['postgres']['host']}:{project_settings['postgres']['port']}/" \ 300 | f"{project_settings['postgres']['database']}" 301 | engine = create_engine(engine_string) 302 | # Create the query 303 | if tick_data: 304 | sql_query = f"SELECT * FROM {table_name} ORDER BY time_msc;" 305 | else: 306 | sql_query = f"SELECT * FROM {table_name} ORDER BY time;" 307 | if chunky: 308 | # Set the chunk size 309 | chunk_size = 10000 # You may need to adjust this based upon your processor 310 | # Set up database chunking 311 | db_connection = engine.connect().execution_options( 312 | max_row_buffer=chunk_size 313 | ) 314 | # Retrieve the data 315 | dataframe = pandas.read_sql(sql_query, db_connection, chunksize=chunk_size) 316 | # Close the connection 317 | db_connection.close() 318 | # Return the dataframe 319 | return dataframe 320 | else: 321 | # Standard DB connection 322 | db_connection = engine.connect() 323 | # Retrieve the data 324 | dataframe = pandas.read_sql(sql_query, db_connection) 325 | # Close the connection 326 | db_connection.close() 327 | # Return the dataframe 328 | return dataframe 329 | 330 | 331 | # Function to add a backtest update 332 | def insert_backtest_update(strategy, exchange, trade_type, trade_stage, symbol, qty_purchased, leverage, stop_loss, 333 | take_profit, price, comment, status, order_id, available_balance, equity, table_name, 334 | project_settings, update_time, entry_price, exit_price): 335 | # Create the SQL statement 336 | sql_query = f"INSERT INTO {table_name} (strategy, exchange, trade_type, trade_stage, symbol, qty_purchased, " \ 337 | f"leverage, stop_loss, take_profit, price, comment, status, order_id, balance, equity, update_time, " \ 338 | f"entry_price, exit_price) VALUES (" \ 339 | f"'{strategy}'," \ 340 | f"'{exchange}'," \ 341 | f"'{trade_type}'," \ 342 | f"'{trade_stage}'," \ 343 | f"'{symbol}'," \ 344 | f"'{qty_purchased}'," \ 345 | f"'{leverage}'," \ 346 | f"'{stop_loss}'," \ 347 | f"'{take_profit}'," \ 348 | f"'{price}'," \ 349 | f"'{comment}'," \ 350 | f"'{status}'," \ 351 | f"'{order_id}'," \ 352 | f"'{available_balance}'," \ 353 | f"'{equity}'," \ 354 | f"'{update_time}'," \ 355 | f"'{entry_price}'," \ 356 | f"'{exit_price}'" \ 357 | f");" 358 | try: 359 | return sql_execute(sql_query=sql_query, project_settings=project_settings) 360 | except Exception as e: 361 | raise exceptions.SQLBacktestTradeActionError 362 | 363 | 364 | # Function to add a new order from backtester 365 | def insert_order_update(trade_type, status, stop_loss, take_profit, price, order_id, trade_object, update_time, 366 | project_settings, comment): 367 | return insert_backtest_update( 368 | strategy=trade_object["strategy"], 369 | exchange="testing", 370 | trade_type=trade_type, 371 | trade_stage="order", 372 | symbol=trade_object["symbol"], 373 | qty_purchased=0.00, 374 | leverage=trade_object["backtest_settings"]["leverage"], 375 | stop_loss=stop_loss, 376 | take_profit=take_profit, 377 | price=price, 378 | comment=comment, 379 | status=status, 380 | order_id=order_id, 381 | available_balance=trade_object["current_available_balance"], 382 | equity=trade_object["current_equity"], 383 | update_time=update_time, 384 | table_name=trade_object["trade_table_name"], 385 | project_settings=project_settings, 386 | entry_price=0.00, 387 | exit_price=0.00 388 | ) 389 | 390 | 391 | def insert_new_position(trade_type, status, stop_loss, take_profit, price, order_id, trade_object, update_time, 392 | project_settings, qty_purchased, entry_price, comment): 393 | return insert_backtest_update( 394 | strategy=trade_object["strategy"], 395 | exchange="testing", 396 | trade_type=trade_type, 397 | trade_stage="position", 398 | symbol=trade_object["symbol"], 399 | qty_purchased=qty_purchased, 400 | leverage=trade_object["backtest_settings"]['leverage'], 401 | stop_loss=stop_loss, 402 | take_profit=take_profit, 403 | price=price, 404 | comment=comment, 405 | status=status, 406 | order_id=order_id, 407 | available_balance=trade_object["current_available_balance"], 408 | equity=trade_object["current_equity"], 409 | table_name=trade_object["trade_table_name"], 410 | update_time=update_time, 411 | project_settings=project_settings, 412 | entry_price=entry_price, 413 | exit_price=0.00 414 | ) 415 | 416 | 417 | def position_close(trade_type, status, stop_loss, take_profit, price, order_id, trade_object, update_time, 418 | project_settings, qty_purchased, trade_stage, entry_price, exit_price, comment): 419 | return insert_backtest_update( 420 | strategy=trade_object["strategy"], 421 | exchange="testing", 422 | trade_type=trade_type, 423 | trade_stage=trade_stage, 424 | symbol=trade_object["symbol"], 425 | qty_purchased=qty_purchased, 426 | leverage=trade_object["backtest_settings"]['leverage'], 427 | stop_loss=stop_loss, 428 | take_profit=take_profit, 429 | price=price, 430 | comment=comment, 431 | status=status, 432 | order_id=order_id, 433 | available_balance=trade_object["current_available_balance"], 434 | equity=trade_object["current_equity"], 435 | table_name=trade_object["trade_table_name"], 436 | update_time=update_time, 437 | project_settings=project_settings, 438 | entry_price=entry_price, 439 | exit_price=exit_price 440 | ) 441 | 442 | 443 | # Retrieve last take_profit entry for an order 444 | def retrieve_last_position(order_id, trade_object, project_settings): 445 | # Create the SQL query 446 | sql_query = f"SELECT * FROM {trade_object['trade_table_name']} WHERE symbol='{trade_object['symbol']}' AND " \ 447 | f"strategy='{trade_object['strategy']}' AND trade_stage='position' AND order_id='{order_id}' ORDER BY " \ 448 | f"id DESC LIMIT 1;" 449 | # Execute the SQL Query 450 | return get_data(sql_query, project_settings) 451 | 452 | 453 | # Function to save a dataframe 454 | def save_dataframe(dataframe, table_name, project_settings): 455 | # Create the connection object for PostgreSQL 456 | engine_string = f"postgresql://{project_settings['postgres']['user']}:{project_settings['postgres']['password']}@" \ 457 | f"{project_settings['postgres']['host']}:{project_settings['postgres']['port']}/" \ 458 | f"{project_settings['postgres']['database']}" 459 | engine = create_engine(engine_string) 460 | # Save 461 | dataframe.to_sql(table_name, engine, if_exists='append') 462 | 463 | 464 | # Function to retrieve the unique orders id's for a strategy 465 | def retrieve_unique_order_id(trade_object, comment, project_settings): 466 | sql_query = f"SELECT DISTINCT order_id FROM {trade_object['trade_table_name']} WHERE " \ 467 | f"strategy='{trade_object['strategy']}' and comment='{comment}';" 468 | # Execute the Query 469 | return get_data(sql_query, project_settings) 470 | 471 | 472 | # Function to retrieve all entries for an order id 473 | def retrieve_trade_details(order_id, trade_object, comment, project_settings): 474 | sql_query = f"SELECT * from {trade_object['trade_table_name']} WHERE strategy='{trade_object['strategy']}' " \ 475 | f"and comment='{comment}' and order_id='{order_id}';" 476 | return get_data(sql_query, project_settings) 477 | 478 | 479 | # Function to retrieve the last tick 480 | def retrieve_last_tick(tick_table_name, project_settings): 481 | sql_query = f"SELECT * from {tick_table_name} ORDER BY time_msc DESC LIMIT 1;" 482 | return get_data(sql_query, project_settings) 483 | 484 | 485 | # Function to create a summary table 486 | def create_summary_table(project_settings): 487 | table_details = "strategy VARCHAR(100) NOT NULL," \ 488 | "comment VARCHAR(100) NOT NULL," \ 489 | "strategy_detail JSONB NOT NULL," \ 490 | "wins BIGINT NOT NULL," \ 491 | "losses BIGINT NOT NULL," \ 492 | "profit BIGINT NOT NULL," \ 493 | "not_completed BIGINT NOT NULL" 494 | return create_sql_table("strategy_testing_outcomes", table_details, project_settings, id=True) 495 | -------------------------------------------------------------------------------- /strategies/ema_cross.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Assumptions: 3 | 1. All strategy is performed on an existing dataframe. Previous inputs define how dataframe is retrieved/created 4 | ''' 5 | from indicator_lib import ema_cross 6 | import display_lib 7 | from backtest_lib import backtest_analysis 8 | 9 | 10 | # Main display function 11 | def ema_cross_strategy(dataframe, risk_ratio=1, backtest=True, display=True, upload=False, show=False): 12 | # Determine EMA Cross Events for EMA 15 and EMA 200 13 | print("Calculating cross events for EMA 15 and EMA 200") 14 | ema_one = "ta_ema_15" 15 | ema_two = "ta_ema_200" 16 | cross_event_dataframe = ema_cross.ema_cross( 17 | dataframe=dataframe, 18 | ema_one=ema_one, 19 | ema_two=ema_two 20 | ) 21 | order_dataframe = determine_order( 22 | dataframe=cross_event_dataframe, 23 | ema_one=ema_one, 24 | ema_two=ema_two, 25 | pip_size=0.01, 26 | risk_ratio=risk_ratio 27 | ) 28 | # Extract cross events 29 | cross_events = order_dataframe[order_dataframe['crossover'] == True] 30 | # Extract valid trades from cross_events 31 | valid_trades = cross_events[cross_events['valid'] == True] 32 | # Extract invalid trades from cross events 33 | invalid_trades = cross_events[cross_events['valid'] == False] 34 | # Build the display object 35 | # Update plotting 36 | fig = display_lib.construct_base_candlestick_graph(dataframe=cross_event_dataframe, candlestick_title="BTCUSD Raw") 37 | # Add ta_ema_15 38 | fig = display_lib.add_line_to_graph( 39 | base_fig=fig, 40 | dataframe=cross_event_dataframe, 41 | dataframe_column="ta_ema_15", 42 | line_name="EMA 15" 43 | ) 44 | # Add ta_ema_200 45 | fig = display_lib.add_line_to_graph( 46 | base_fig=fig, 47 | dataframe=cross_event_dataframe, 48 | dataframe_column="ta_ema_200", 49 | line_name="EMA 200" 50 | ) 51 | # Add cross event display 52 | fig = display_lib.add_markers_to_graph( 53 | base_fig=fig, 54 | dataframe=valid_trades, 55 | value_column="close", 56 | point_names="Valid Trades Cross Events" 57 | ) 58 | # Add invalid trades 59 | fig = display_lib.add_markers_to_graph( 60 | base_fig=fig, 61 | dataframe=invalid_trades, 62 | value_column="close", 63 | point_names="Invalid Trades Cross Events" 64 | ) 65 | if backtest: 66 | # Extract trade rows 67 | trade_dataframe = valid_trades[['time', 'human_time', 'order_type', 'stop_loss', 'stop_price', 'take_profit']] 68 | return trade_dataframe 69 | elif display: 70 | return fig 71 | elif show: 72 | display_lib.display_graph(fig, "BTCUSD Raw Graph") 73 | trade_dataframe = valid_trades[['time', 'human_time', 'order_type', 'stop_loss', 'stop_price', 'take_profit']] 74 | return trade_dataframe 75 | else: 76 | last_event = order_dataframe.tail(1) 77 | if last_event['valid'] == True: 78 | return last_event 79 | return False 80 | 81 | 82 | # Determine order type and values 83 | def determine_order(dataframe, ema_one, ema_two, pip_size, risk_ratio, backtest=True): 84 | """ 85 | 86 | :param dataframe: 87 | :param risk_amount: 88 | :param backtest: 89 | :return: 90 | """ 91 | # Set up Pip movement 92 | # Determine direction 93 | dataframe['direction'] = dataframe[ema_one] > dataframe[ema_one].shift(1) # I.e. trending up 94 | # Add in stop loss 95 | dataframe['stop_loss'] = dataframe[ema_two] 96 | cross_events = dataframe 97 | # Calculate stop loss 98 | for index, row in cross_events.iterrows(): 99 | if row['direction'] == True: 100 | # Order type will be a BUY_STOP 101 | cross_events.loc[index, 'order_type'] = "BUY_STOP" 102 | # Calculate the distance between the low and the stop loss 103 | if row['low'] > row['stop_loss']: 104 | take_profit = row['low'] - row['stop_loss'] 105 | else: 106 | take_profit = row['stop_loss'] - row['low'] 107 | # Multiply the take_profit by the risk amount 108 | take_profit = take_profit * risk_ratio 109 | # Set the take profit based upon the distance 110 | cross_events.loc[index, 'take_profit'] = row['high'] + take_profit 111 | # Set the entry price as 10 pips above the high 112 | stop_price = row['high'] + 10 * pip_size 113 | cross_events.loc[index, 'stop_price'] = stop_price 114 | 115 | else: 116 | # Order type will be a SELL STOP 117 | cross_events.loc[index, 'order_type'] = "SELL_STOP" 118 | if row['high'] > row['stop_loss']: 119 | take_profit = row['high'] - row['stop_loss'] 120 | else: 121 | take_profit = row['stop_loss'] - row['high'] 122 | # Multiply the take_profit by the risk amount 123 | take_profit = take_profit * risk_ratio 124 | # Set the take profit 125 | cross_events.loc[index, 'take_profit'] = row['low'] - take_profit 126 | # Set the entry price as 10 pips below the low 127 | stop_price = row['low'] - 10 * pip_size 128 | cross_events.loc[index, 'stop_price'] = stop_price 129 | 130 | for index, row in cross_events.iterrows(): 131 | if row['crossover'] == True: 132 | if row['order_type'] == "BUY_STOP": 133 | if row['take_profit'] > row['stop_price'] > row['stop_loss']: 134 | valid = True 135 | cross_events.loc[index, 'valid'] = valid 136 | elif row['order_type'] == "SELL_STOP": 137 | if row['take_profit'] < row['stop_price'] < row['stop_loss']: 138 | valid = True 139 | cross_events.loc[index, 'valid'] = valid 140 | else: 141 | cross_events.loc[index, 'valid'] = False 142 | 143 | return cross_events 144 | 145 | -------------------------------------------------------------------------------- /strategies/ema_triple_cross.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Assumptions: 3 | 1. All strategy is performed on an existing dataframe. Previous inputs define how dataframe is retrieved/created 4 | ''' 5 | from indicator_lib import ema_cross 6 | import display_lib 7 | from backtest_lib import backtest_analysis 8 | 9 | 10 | # Main display function 11 | def ema_triple_cross_strategy(dataframe, risk_ratio=1, display=True, show=False): 12 | # Determine EMA Cross Events for EMA 15 and EMA 200 13 | print("Calculating cross events for EMA 15 and EMA 200") 14 | ema_one = "ta_ema_15" 15 | ema_two = "ta_ema_200" 16 | cross_event_dataframe = ema_cross.ema_cross( 17 | dataframe=dataframe, 18 | ema_one=ema_one, 19 | ema_two=ema_two 20 | ) 21 | # Build the display object 22 | # Update plotting 23 | fig = display_lib.construct_base_candlestick_graph(dataframe=cross_event_dataframe, candlestick_title="BTCUSD Raw") 24 | # Add ta_ema_15 25 | fig = display_lib.add_line_to_graph( 26 | base_fig=fig, 27 | dataframe=cross_event_dataframe, 28 | dataframe_column="ta_ema_15", 29 | line_name="EMA 15" 30 | ) 31 | # Add ta_ema_200 32 | fig = display_lib.add_line_to_graph( 33 | base_fig=fig, 34 | dataframe=cross_event_dataframe, 35 | dataframe_column="ta_ema_200", 36 | line_name="EMA 200" 37 | ) 38 | 39 | -------------------------------------------------------------------------------- /strategies/engulfing_candle_strategy.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Function to respond to engulfing candle detections and turn them into a strategy 5 | def engulfing_candle_strategy(high, low, symbol, timeframe, exchange, alert_type, project_settings): 6 | """ 7 | Function to respond to engulfing candle detections and turn them into a strategy 8 | :param high: float 9 | :param low: float 10 | :param symbol: string 11 | :param timeframe: string 12 | :param exchange: string 13 | :param alert_type: string 14 | :param project_settings: json dictionary object 15 | :return: 16 | """ 17 | # Only apply strategy to specified timeframes 18 | if timeframe == "M15" or timeframe == "M30" or timeframe == "H1" or timeframe == "D1": 19 | # Respond to bullish_engulfing 20 | if alert_type == "bullish_engulfing": 21 | # Set the Trade Type 22 | trade_type = "BUY" 23 | # Set the Take Profit 24 | take_profit = high + high - low 25 | # Set the Buy Stop 26 | entry_price = high 27 | # Set the Stop Loss 28 | stop_loss = low 29 | elif alert_type == "bearish_engulfing": 30 | # Set the Trade Type 31 | trade_type = "SELL" 32 | # Set the Take Profit 33 | take_profit = low - high + low 34 | # Set the Sell Stop 35 | entry_price = low 36 | # Set the Stop Loss 37 | stop_loss = high 38 | # Print the result to the screen 39 | print(f"Trade Signal Detected. Symbol: {symbol}, Trade Type: {trade_type}, Take Profit: {take_profit}, " 40 | f"Entry Price: {entry_price}, Stop Loss: {stop_loss}, Exchange: {exchange}") 41 | -------------------------------------------------------------------------------- /tests/test_mt5_interaction.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import exceptions 3 | import os 4 | import json 5 | 6 | # Libraries being tested 7 | import main 8 | from metatrader_lib import mt5_interaction 9 | 10 | # Define project settings location 11 | import_filepath = "test_settings.json" 12 | 13 | fake_login_details = { 14 | "username": 00000000, 15 | "password": "password", 16 | "server": "Test_Server", 17 | "path": "C:/MetaTrader5/terminal64.exe" 18 | } 19 | 20 | 21 | def test_start_mt5_fail(): 22 | # Test an exception raised for unable to login 23 | with pytest.raises(exceptions.MetaTraderInitializeError) as e: 24 | mt5_interaction.start_mt5( 25 | username=fake_login_details["username"], 26 | password=fake_login_details["password"], 27 | server=fake_login_details["server"], 28 | path=fake_login_details["path"] 29 | ) 30 | assert e.type == exceptions.MetaTraderInitializeError 31 | 32 | 33 | def test_start_mt5_succeed(): 34 | # Test that function passed when correct login details provided 35 | real_login_details = main.get_project_settings(import_filepath) 36 | real_login_details = real_login_details["mt5"]["paper"] 37 | value = mt5_interaction.start_mt5( 38 | username=real_login_details["username"], 39 | password=real_login_details["password"], 40 | server=real_login_details["server"], 41 | path=real_login_details["mt5Pathway"] 42 | ) 43 | assert value is True 44 | 45 | 46 | # Test a fake symbol not enabled 47 | def test_initialize_symbols_fake(): 48 | # Fake Symbol 49 | fake_symbol = ["HEYHEY"] 50 | with pytest.raises(exceptions.MetaTraderSymbolDoesNotExistError) as e: 51 | mt5_interaction.initialize_symbols(fake_symbol) 52 | assert e.type == exceptions.MetaTraderSymbolDoesNotExistError 53 | 54 | 55 | # Test a real symbol enabled 56 | def test_initialize_symbols_real(): 57 | # Real Symbol 58 | real_symbol = ["BTCUSD.a"] 59 | value = mt5_interaction.initialize_symbols(real_symbol) 60 | assert value is True 61 | 62 | 63 | # Test that place order does not accept invalid order types 64 | def test_place_order_wrong_order_type(): 65 | # Fake order 66 | with pytest.raises(SyntaxError) as e: 67 | mt5_interaction.place_order("WRONG_TYPE", "BTCUSD.a", 0.0, 0.0,0.0,"comment") 68 | assert e.type == SyntaxError 69 | 70 | 71 | # Test that place_order throws an error if a balance which is too large is placed 72 | def test_place_order_incorrect_balance(): 73 | # Get the options for a test order 74 | test_order = get_a_test_order() 75 | with pytest.raises(exceptions.MetaTraderOrderCheckError) as e: 76 | mt5_interaction.place_order("BUY", "BTCUSD.a", test_order['incorrect_volume'], test_order['buy_stop_loss'], 77 | test_order['buy_take_profit'], "UnitTestOrder") 78 | assert e.type == exceptions.MetaTraderOrderCheckError 79 | 80 | # Test that place_order throws an error if incorrect stop_loss 81 | def test_place_order_incorrect_stop_loss(): 82 | # Get options for a test order 83 | test_order = get_a_test_order() 84 | with pytest.raises(exceptions.MetaTraderOrderPlacingError) as e: 85 | mt5_interaction.place_order( 86 | order_type="BUY", 87 | symbol="BTCUSD.a", 88 | volume=test_order['correct_volume'], 89 | stop_loss=test_order['sell_stop_loss'], 90 | take_profit=test_order['buy_take_profit'], 91 | comment="TestTrade" 92 | ) 93 | assert e.type == exceptions.MetaTraderOrderPlacingError 94 | 95 | 96 | # Test that place_order throws an error if incorrect take_profit 97 | def test_place_order_incorrect_take_profit(): 98 | test_order = get_a_test_order() 99 | with pytest.raises(exceptions.MetaTraderOrderPlacingError) as e: 100 | mt5_interaction.place_order( 101 | order_type="BUY", 102 | symbol="BTCUSD.a", 103 | volume=test_order['correct_volume'], 104 | stop_loss=test_order['buy_stop_loss'], 105 | take_profit=test_order['sell_take_profit'], 106 | comment="TestTrade" 107 | ) 108 | assert e.type == exceptions.MetaTraderOrderPlacingError 109 | 110 | 111 | # Test that error thrown if price == 0 for SELL_STOP 112 | def test_place_order_incorrect_sell_stop(): 113 | test_order = get_a_test_order() 114 | with pytest.raises(exceptions.MetaTraderIncorrectStopPriceError) as e: 115 | mt5_interaction.place_order( 116 | order_type="SELL_STOP", 117 | symbol="BTCUSD.a", 118 | volume=test_order['correct_volume'], 119 | stop_loss=test_order['sell_stop_loss'], 120 | take_profit=test_order['sell_take_profit'], 121 | comment="TestTrade" 122 | ) 123 | assert e.type == exceptions.MetaTraderIncorrectStopPriceError 124 | 125 | 126 | # Test that error thrown if price == 0 for BUY_STOP 127 | def test_place_order_incorrect_buy_stop(): 128 | test_order = get_a_test_order() 129 | with pytest.raises(exceptions.MetaTraderIncorrectStopPriceError) as e: 130 | mt5_interaction.place_order( 131 | order_type="BUY_STOP", 132 | symbol="BTCUSD.a", 133 | volume=test_order['correct_volume'], 134 | stop_loss=test_order['buy_stop_loss'], 135 | take_profit=test_order['buy_take_profit'], 136 | comment="TestTrade" 137 | ) 138 | assert e.type == exceptions.MetaTraderIncorrectStopPriceError 139 | 140 | 141 | # Test that placing a BUY order works 142 | def test_place_order_buy(): 143 | # Get options for a test order 144 | test_order = get_a_test_order() 145 | outcome = mt5_interaction.place_order( 146 | order_type="BUY", 147 | symbol="BTCUSD.a", 148 | volume=test_order['correct_volume'], 149 | stop_loss=test_order['buy_stop_loss'], 150 | take_profit=test_order['buy_take_profit'], 151 | comment="TestTrade" 152 | ) 153 | assert outcome == None 154 | 155 | 156 | # Test that placing SELL order works 157 | def test_place_order_sell(): 158 | test_order = get_a_test_order() 159 | outcome = mt5_interaction.place_order( 160 | order_type="SELL", 161 | symbol="BTCUSD.a", 162 | volume=test_order['correct_volume'], 163 | stop_loss=test_order['sell_stop_loss'], 164 | take_profit=test_order['sell_take_profit'], 165 | comment="TestTrade" 166 | ) 167 | assert outcome == None 168 | 169 | 170 | # Test that placing a BUY_STOP order works 171 | def test_place_order_buy_stop(): 172 | test_order = get_a_test_order() 173 | outcome = mt5_interaction.place_order( 174 | order_type="BUY_STOP", 175 | symbol="BTCUSD.a", 176 | volume=test_order['correct_volume'], 177 | stop_loss=test_order['buy_stop_loss'], 178 | take_profit=test_order['buy_take_profit'], 179 | comment="TestTrade", 180 | price=test_order['correct_buy_stop'] 181 | ) 182 | assert outcome == None 183 | 184 | 185 | # Test that placing a SELL_STOP order works 186 | def test_place_order_sell_stop(): 187 | test_order = get_a_test_order() 188 | outcome = mt5_interaction.place_order( 189 | order_type="SELL_STOP", 190 | symbol="BTCUSD.a", 191 | volume=test_order['correct_volume'], 192 | stop_loss=test_order['sell_stop_loss'], 193 | take_profit=test_order['sell_take_profit'], 194 | comment="TestTrade", 195 | price=test_order['correct_sell_stop'] 196 | ) 197 | assert outcome == None 198 | 199 | 200 | # Test that canceling a non-existing order throws an error 201 | def test_cancel_order_incorrect(): 202 | order_number = 12345678 203 | with pytest.raises(exceptions.MetaTraderCancelOrderError) as e: 204 | mt5_interaction.cancel_order(order_number) 205 | assert e.type == exceptions.MetaTraderCancelOrderError 206 | 207 | 208 | # Test the ability to cancel an order 209 | def test_cancel_order(): 210 | # Retrieve a list of orders 211 | orders = mt5_interaction.get_open_orders() 212 | # Iterate through and cancel 213 | for order in orders: 214 | outcome = mt5_interaction.cancel_order(order) 215 | assert outcome == True 216 | 217 | 218 | # Test the ability to modify an open positions stop loss 219 | def test_modify_position_new_stop_loss(): 220 | # Retrieve a list of current positions 221 | positions = mt5_interaction.get_open_positions() 222 | # Modify the stop loss of positions with comment "TestTrade" 223 | for position in positions: 224 | if position[17] == "TestTrade": 225 | # Add $100 to stop loss, then modify 226 | new_stop_loss = position[11] + 100 227 | outcome = mt5_interaction.modify_position( 228 | order_number=position[0], 229 | symbol=position[16], 230 | new_stop_loss=new_stop_loss, 231 | new_take_profit=position[12] 232 | ) 233 | assert outcome == True 234 | 235 | 236 | # Test ability to modify an open positions take profit 237 | def test_modify_position_new_take_profit(): 238 | # Retrieve a list of current positions 239 | positions = mt5_interaction.get_open_positions() 240 | # Modify the take profit of positions with the comment "TestTrade" 241 | for position in positions: 242 | if position[17] == "TestTrade": 243 | # Add $100 to take profit, then modify 244 | new_take_profit = position[12] + 100 245 | outcome = mt5_interaction.modify_position( 246 | order_number=position[0], 247 | symbol=position[16], 248 | new_stop_loss=position[11], 249 | new_take_profit=new_take_profit 250 | ) 251 | assert outcome == True 252 | 253 | 254 | # Test ability to modify both take profit and stop loss simultaneously 255 | def test_modify_position(): 256 | # Retrieve a list of current positions 257 | positions = mt5_interaction.get_open_positions() 258 | # Modify the both take profit and stop loss positions with the comment "TestTrade" 259 | for position in positions: 260 | if position[17] == "TestTrade": 261 | # Subtract $100 to take profit, then modify 262 | new_take_profit = position[12] - 100 263 | new_stop_loss = position[11] - 100 264 | outcome = mt5_interaction.modify_position( 265 | order_number=position[0], 266 | symbol=position[16], 267 | new_stop_loss=new_stop_loss, 268 | new_take_profit=new_take_profit 269 | ) 270 | assert outcome == True 271 | 272 | # Test Modify Position throws an error 273 | def test_modify_position_error(): 274 | # Retrieve a list of current positions 275 | positions = mt5_interaction.get_open_positions() 276 | for position in positions: 277 | if position[17] == "TestTrade": 278 | with pytest.raises(exceptions.MetaTraderModifyPositionError) as e: 279 | mt5_interaction.modify_position( 280 | order_number=12345678, 281 | symbol=position[16], 282 | new_stop_loss=position[11], 283 | new_take_profit=position[12] 284 | ) 285 | assert e.type == exceptions.MetaTraderCancelOrderError 286 | 287 | 288 | # Test function to close a position 289 | def test_close_position_syntax(): 290 | # Retrieve a list of current positions 291 | positions = mt5_interaction.get_open_positions() 292 | for position in positions: 293 | if position[17] == "TestTrade": 294 | with pytest.raises(SyntaxError) as e: 295 | mt5_interaction.close_position( 296 | order_number=position[0], 297 | symbol=position[16], 298 | volume=position[9], 299 | order_type=position[5], 300 | price=position[13], 301 | comment="TestTrade" 302 | ) 303 | assert e.type == SyntaxError 304 | 305 | 306 | # Test function to attempt to close a position with a bogus order_number 307 | def test_close_position_wrong_order_number(): 308 | # Retrieve a list of current positions 309 | positions = mt5_interaction.get_open_positions() 310 | for position in positions: 311 | if position[17] == "TestTrade": 312 | with pytest.raises(exceptions.MetaTraderClosePositionError) as e: 313 | if position[5] == 0: 314 | order_type = "SELL" 315 | elif position[5] == 1: 316 | order_type = "BUY" 317 | mt5_interaction.close_position( 318 | order_number=12345678, 319 | symbol=position[16], 320 | volume=position[9], 321 | order_type=order_type, 322 | price=position[13], 323 | comment="TestTrade" 324 | ) 325 | 326 | 327 | # Test the close position function works 328 | def test_close_position(): 329 | # Retreive a list of current positions 330 | positions = mt5_interaction.get_open_positions() 331 | for position in positions: 332 | if position[17] == "TestTrade": 333 | if position[5] == 0: 334 | order_type = "SELL" 335 | elif position[5] == 1: 336 | order_type = "BUY" 337 | outcome = mt5_interaction.close_position( 338 | order_number=position[0], 339 | symbol=position[16], 340 | volume=position[9], 341 | order_type=order_type, 342 | price=position[13], 343 | comment="TestTrade" 344 | ) 345 | assert outcome == True 346 | 347 | 348 | ### Complex test 349 | def test_fractional_close(): 350 | # Step 1: Make a trade with a volume of 0.2 351 | # Retrieve details for a test order 352 | test_order = get_a_test_order() 353 | # Update volume 354 | test_order['correct_volume'] = 0.2 355 | # Place a BUY order 356 | buy_order = mt5_interaction.place_order( 357 | order_type="BUY", 358 | symbol="BTCUSD.a", 359 | volume=test_order['correct_volume'], 360 | stop_loss=test_order['buy_stop_loss'], 361 | take_profit=test_order['buy_take_profit'], 362 | comment="ComplexTestOrder" 363 | ) 364 | # Retrieve a list of current positions 365 | positions = mt5_interaction.get_open_positions() 366 | for position in positions: 367 | if position[17] == "ComplexTestOrder": 368 | # Get 50% of volume 369 | volume = position[9] / 2 370 | # Get price less 100 371 | price = position[13] - 100 372 | sell_order = mt5_interaction.close_position( 373 | order_number=position[0], 374 | symbol=position[16], 375 | volume=volume, 376 | order_type="SELL", 377 | price=price, 378 | comment="ComplexTestOrderSell" 379 | ) 380 | assert sell_order == True 381 | 382 | # Now fully close out the positions to complete 383 | positions = mt5_interaction.get_open_positions() 384 | for position in positions: 385 | if position[17] == "ComplexTestOrder": 386 | sell_order = mt5_interaction.close_position( 387 | order_number=position[0], 388 | symbol=position[16], 389 | volume=position[9], 390 | order_type="SELL", 391 | price=position[13]-100, 392 | comment="ComplexTestOrderSell" 393 | ) 394 | assert sell_order == True 395 | 396 | 397 | 398 | ### Helper functions 399 | def get_a_test_order(): 400 | # Get the current BTCUSD.a price, Assume balance is not more than $100,000 401 | current_price = mt5_interaction.retrieve_latest_tick("BTCUSD.a")['ask'] 402 | return_object = { 403 | "current_price": current_price, 404 | "correct_buy": current_price, 405 | "incorrect_buy": current_price - 1000, 406 | "buy_stop_loss": current_price - 2000, 407 | "sell_stop_loss": current_price + 2000, 408 | "buy_take_profit": current_price + 2000, 409 | "sell_take_profit": current_price - 2000, 410 | "correct_buy_stop": current_price + 1000, 411 | "incorrect_buy_stop": current_price - 1000, 412 | "correct_sell_stop": current_price - 1000, 413 | "incorrect_sell_stop": current_price + 1000, 414 | "incorrect_volume": (100000 / float(current_price)) + 1, 415 | "correct_volume": 0.1 416 | } 417 | return return_object 418 | 419 | -------------------------------------------------------------------------------- /tests/test_sql_interaction.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimtin/python_trading_bot/778e98dbe9cc812481836887321c9e4fd709dfbc/tests/test_sql_interaction.py -------------------------------------------------------------------------------- /tests/test_trade_capture.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class MyTestCase(unittest.TestCase): 5 | def test_something(self): 6 | self.assertEqual(True, False) # add assertion here 7 | 8 | 9 | if __name__ == '__main__': 10 | unittest.main() 11 | --------------------------------------------------------------------------------