├── .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 |
4 |
5 |
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 |
--------------------------------------------------------------------------------