forked from asavinov/intelligent-trading-bot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
trader.py
410 lines (334 loc) · 13.7 KB
/
trader.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
import os
import sys
import argparse
import math, time
from datetime import datetime
from decimal import *
import pandas as pd
import asyncio
from binance.client import Client
from binance.exceptions import *
from binance.helpers import date_to_milliseconds, interval_to_milliseconds
from binance.enums import *
from service.App import *
from common.utils import *
from service.analyzer import *
import logging
log = logging.getLogger('trader')
logging.basicConfig(
filename="trader.log", # parameter in App
level=logging.DEBUG,
#format = "%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s",
format = "%(asctime)s %(levelname)s %(message)s",
#datefmt = '%Y-%m-%d %H:%M:%S',
)
async def main_trader_task():
"""
It is a highest level task which is added to the event loop and executed normally every 1 minute and then it calls other tasks.
"""
symbol = App.config["symbol"]
startTime, endTime = get_interval("1m")
now_ts = now_timestamp()
log.info(f"===> Start trade task. Timestamp {now_ts}. Interval [{startTime},{endTime}].")
#
# Sync trade status, check running orders (orders, account etc.)
#
status = App.status
if status == "BUYING" or status == "SELLING":
# We expect that an order was created before and now we need to check if it still exists or was executed
# -----
order_status = await update_order_status()
order = App.order
# If order status executed then change the status
# Status codes: NEW PARTIALLY_FILLED FILLED CANCELED PENDING_CANCEL(currently unused) REJECTED EXPIRED
if not order or not order_status:
# No sell order exists or some problem
# TODO: Recover, reset, init/sync state (cannot trade because wrong happened with the order or connection or whatever)
# check connection (like ping), then server status, then our own account status, then funds, orders etc.
# -----
await update_trade_status()
log.error(f"Bad order or order status {order}. Full reset/init needed.")
return
if order_status == ORDER_STATUS_FILLED:
log.info(f"Limit order filled. {order}")
if status == "BUYING":
print(f"===> BOUGHT: {order}")
App.status = "BOUGHT"
elif status == "SELLING":
print(f"<=== SOLD: {order}")
App.status = "SOLD"
log.info(f'New trade mode: {App.status}')
elif order_status == ORDER_STATUS_REJECTED or order_status == ORDER_STATUS_EXPIRED or order_status == ORDER_STATUS_CANCELED:
log.error(f"Failed to fill order with order status {order_status}")
if status == "BUYING":
App.status = "SOLD"
elif status == "SELLING":
App.status = "BOUGHT"
log.info(f'New trade mode: {App.status}')
elif order_status == ORDER_STATUS_PENDING_CANCEL:
return # Currently do nothing. Check next time.
elif order_status == ORDER_STATUS_PARTIALLY_FILLED:
pass # Currently do nothing. Check next time.
elif order_status == ORDER_STATUS_NEW:
pass # Wait further for execution
else:
pass # Order still exists and is active
elif status == "BOUGHT" or status == "SOLD":
pass # Do nothing
else:
log.error(f"Wrong status value {status}.")
#
# Prepare. Kill or update existing orders (if necessary)
#
status = App.status
signal = App.signal
signal_side = signal.get("side")
# If not sold for 1 minute, then kill and then a new order will be created below if there is signal
# Essentially, this will mean price adjustment (if a new order of the same direction will be created)
# In future, we might kill only after some timeout
if status == "BUYING" or status == "SELLING": # Still not sold for 1 minute
# -----
order_status = await cancel_order()
if not order_status:
# Cancel exception (the order still exists) or the order was filled and does not exist
await update_trade_status()
return
await asyncio.sleep(1) # Wait for a second till the balance is updated
if status == "BUYING":
App.status = "SOLD"
elif status == "SELLING":
App.status = "BOUGHT"
#
# Trade by creating orders
#
status = App.status
if signal_side == "BUY":
print(f"===> BUY SIGNAL {signal}: ")
elif signal_side == "SELL":
print(f"<=== SELL SIGNAL: {signal}")
else:
print(f"SCORE: {signal.get('score'):+.3f}. PRICE: {signal.get('close_price'):.2f}")
# Update account balance etc. what is needed for trade
# -----
await update_account_balance()
if status == "SOLD" and signal_side == "BUY":
# -----
await new_limit_order(side=SIDE_BUY)
if App.config["trader"]["no_trades_only_data_processing"]:
print("SKIP TRADING due to 'no_trades_only_data_processing' parameter True")
# Never change status if orders not executed
else:
App.status = "BUYING"
elif status == "BOUGHT" and signal_side == "SELL":
# -----
await new_limit_order(side=SIDE_SELL)
if App.config["trader"]["no_trades_only_data_processing"]:
print("SKIP TRADING due to 'no_trades_only_data_processing' parameter True")
# Never change status if orders not executed
else:
App.status = "SELLING"
log.info(f"<=== End trade task.")
return
#
# Order and asset status
#
async def update_trade_status():
"""Read the account state and set the local state parameters."""
# GET /api/v3/openOrders - get current open orders
# GET /api/v3/allOrders - get all orders: active, canceled, or filled
symbol = App.config["symbol"]
# -----
try:
open_orders = App.client.get_open_orders(symbol=symbol) # By "open" orders they probably mean "NEW" or "PARTIALLY_FILLED"
# orders = App.client.get_all_orders(symbol=symbol, limit=10)
except Exception as e:
log.error(f"Binance exception in 'get_open_orders' {e}")
return
if not open_orders:
# -----
await update_account_balance()
last_kline = App.analyzer.get_last_kline(symbol)
last_close_price = to_decimal(last_kline[4]) # Close price of kline has index 4 in the list
base_quantity = App.base_quantity # BTC
btc_assets_in_usd = base_quantity * last_close_price # Cost of available BTC in USD
usd_assets = App.quote_quantity # USD
if usd_assets >= btc_assets_in_usd:
App.status = "SOLD"
else:
App.status = "BOUGHT"
elif len(open_orders) == 1:
order = open_orders[0]
if order.get("side") == SIDE_SELL:
App.status = "SELLING"
elif order.get("side") == SIDE_BUY:
App.status = "BUYING"
else:
log.error(f"Neither SELL nor BUY side of the order {order}.")
return None
else: # Many orders
log.error(f"Wrong state. More than one open order. Fix manually.")
return None
async def update_order_status():
"""
Update information about the current order and return its execution status.
ASSUMPTIONS and notes:
- Status codes: NEW PARTIALLY_FILLED FILLED CANCELED PENDING_CANCEL(currently unused) REJECTED EXPIRED
- only one or no orders can be active currently, but in future there can be many orders
- if no order id(s) is provided then retrieve all existing orders
"""
symbol = App.config["symbol"]
# Get currently active order and id (if any)
order = App.order
order_id = order.get("orderId", 0) if order else 0
if not order_id:
log.error(f"Wrong state or use: check order status cannot find the order id.")
return None
# -----
# Retrieve order from the server
try:
new_order = App.client.get_order(symbol=symbol, orderId=order_id)
except Exception as e:
log.error(f"Binance exception in 'get_order' {e}")
return
# Impose and overwrite the new order information
if new_order:
order.update(new_order)
else:
return None
# Now order["status"] contains the latest status of the order
return order["status"]
async def update_account_balance():
"""Get available assets (as decimal)."""
try:
balance = App.client.get_asset_balance(asset=App.config["base_asset"])
except Exception as e:
log.error(f"Binance exception in 'get_asset_balance' {e}")
return
App.base_quantity = Decimal(balance.get("free", "0.00000000")) # BTC
try:
balance = App.client.get_asset_balance(asset=App.config["quote_asset"])
except Exception as e:
log.error(f"Binance exception in 'get_asset_balance' {e}")
return
App.quote_quantity = Decimal(balance.get("free", "0.00000000")) # USD
pass
#
# Cancel and liquidation orders
#
async def cancel_order():
"""
Kill existing sell order. It is a blocking request, that is, it waits for the end of the operation.
Info: DELETE /api/v3/order - cancel order
"""
symbol = App.config["symbol"]
# Get currently active order and id (if any)
order = App.order
order_id = order.get("orderId", 0) if order else 0
if order_id == 0:
# TODO: Maybe retrieve all existing (sell, limit) orders
return None
# -----
try:
log.info(f"Cancelling order id {order_id}")
new_order = App.client.cancel_order(symbol=symbol, orderId=order_id)
except Exception as e:
log.error(f"Binance exception in 'cancel_order' {e}")
return None
# TODO: There is small probability that the order will be filled just before we want to kill it
# We need to somehow catch and process this case
# If we get an error (say, order does not exist and cannot be killed), then after error returned, we could do trade state reset
# Impose and overwrite the new order information
if new_order:
order.update(new_order)
else:
return None
# Now order["status"] contains the latest status of the order
return order["status"]
#
# Order creation
#
async def new_limit_order(side):
"""
Create a new limit sell order with the amount we current have.
The amount is total amount and price is determined according to our strategy (either fixed increase or increase depending on the signal).
"""
symbol = App.config["symbol"]
now_ts = now_timestamp()
#
# Find limit price (from signal, last kline and adjustment parameters)
#
last_kline = App.analyzer.get_last_kline(symbol)
last_close_price = to_decimal(last_kline[4]) # Close price of kline has index 4 in the list
if not last_close_price:
log.error(f"Cannot determine last close price in order to create a market buy order.")
return None
price_adjustment = App.config["trader"]["limit_price_adjustment"]
if side == SIDE_BUY:
price = last_close_price * Decimal(1.0 - price_adjustment) # Adjust price slightly lower
elif side == SIDE_SELL:
price = last_close_price * Decimal(1.0 + price_adjustment) # Adjust price slightly higher
price_str = round_str(price, 2)
price = Decimal(price_str) # We will use the adjusted price for computing quantity
#
# Find quantity
#
if side == SIDE_BUY:
# Find how much quantity we can buy for all available USD using the computed price
quantity = App.quote_quantity # USD
percentage_used_for_trade = App.config["trader"]["percentage_used_for_trade"]
quantity = (quantity * percentage_used_for_trade) / Decimal(100.0) # Available for trade
quantity = quantity / price # BTC to buy
# Alternatively, we can pass quoteOrderQty in USDT (how much I want to spend)
elif side == SIDE_SELL:
# All available BTCs
quantity = App.base_quantity # BTC
quantity_str = round_down_str(quantity, 6)
#
# Execute order
#
order_spec = dict(
symbol=symbol,
side=side,
type=ORDER_TYPE_LIMIT, # Alternatively, ORDER_TYPE_LIMIT_MAKER
timeInForce=TIME_IN_FORCE_GTC,
quantity=quantity_str,
price=price_str,
)
if App.config["trader"]["no_trades_only_data_processing"]:
print(f"NOT executed order spec: {order_spec}")
else:
order = execute_order(order_spec)
#
# Store/log order object in our records (only after confirmation of success)
#
App.order = order
App.order_time = now_ts
return order
def execute_order(order: dict):
"""Validate and submit order"""
# TODO: Check validity, e.g., against filters (min, max) and our own limits
if App.config["trader"]["test_order_before_submit"]:
try:
log.info(f"Submitting test order: {order}")
test_response = App.client.create_test_order(**order) # Returns {} if ok. Does not check available balances - only trade rules
except Exception as e:
log.error(f"Binance exception in 'create_test_order' {e}")
# TODO: Reset/resync whole account
return
if App.config["trader"]["simulate_order_execution"]:
# TODO: Simply store order so that later we can check conditions of its execution
print(order)
print(App.signal)
pass
else:
# -----
# Submit order
try:
log.info(f"Submitting order: {order}")
order = App.client.create_order(**order)
except Exception as e:
log.error(f"Binance exception in 'create_order' {e}")
return
if not order or not order.get("status"):
return None
return order