695 lines
29 KiB
Python
695 lines
29 KiB
Python
import json
|
|
import uuid
|
|
|
|
import requests
|
|
from datetime import datetime
|
|
|
|
|
|
class Trade:
|
|
def __init__(self, dest, symbol, side, price, qty, order_type='MARKET', tif='GTC', unique_id=None, status=None,
|
|
stats=None, order=None, fee=0.1):
|
|
"""
|
|
This object contains the information,value and statistics for individual active_trades.
|
|
Flags and stats can be set in the constructor in order to load previous active_trades.
|
|
Creating a trade does not execute it on the exchange.
|
|
|
|
:param dest: <str> - ['exchange'|'backtester'] The destination for this trade.
|
|
:param symbol: <str> - The symbol of the trading pair.
|
|
:param side: <str> - BUY|SELL
|
|
:param price: <float> - Asking price for LIMIT orders.
|
|
:param qty: <float> - Base quantity to buy
|
|
:param order_type: <str> - LIMIT|MARKET
|
|
:param tif: <str> - ['GTC'|'IOC'|'FOK'] Time-in-force. Defaults to GTC. This flag is used externally.
|
|
:param unique_id <hex> - Identifier
|
|
:param status: <str> ['inactive'|'unfilled'|'filled'|'part-filled'|'closed'] - order status.
|
|
:param stats: <dic> ['opening_price']:float | ['current_price']:float | ['opening_value']:float |
|
|
['current_value']:float | ['profit'] | ['profit_pct']:float
|
|
- Price and P/L values and percentages for the trade.
|
|
:param fee: <float> - the fee rate charged when trading in or out.
|
|
"""
|
|
|
|
# Set the ID or generate one if not provided.
|
|
# Trade ID should be the same as order id.
|
|
if unique_id is None:
|
|
self.unique_id = uuid.uuid4().hex
|
|
else:
|
|
self.unique_id = unique_id
|
|
|
|
# Store the destination for the trade. ['exchange'|'backtester']
|
|
self.dest = dest
|
|
|
|
# Set the symbol of the pair being traded.
|
|
self.symbol = symbol
|
|
|
|
# Set the side - buy or sell
|
|
self.side = side
|
|
|
|
# Set the order_type. Defaults to 'MARKET'. This flag is used externally when placing the order.
|
|
# LIMIT: Places an order at a set asking price.
|
|
# MARKET: Accept an offer off the table.
|
|
# TRAILING: Follow the price with LIMIT orders a set value
|
|
# from the price until it fills.
|
|
self.order_type = order_type
|
|
|
|
# Set Time-in-force. Defaults to GTC. This flag is used externally.
|
|
# GTC (Good-Till-Cancel) - leave it on the orderbook.
|
|
# IOC (Immediate-Or-Cancel) - if it can't at least partially fill immediately cancel the order.
|
|
# FOK (Fill-Or-Kill) - If it can't fully fill immediately cancel the order.
|
|
self.time_in_force = tif
|
|
|
|
# Set the quantity to be traded.
|
|
self.base_order_qty = qty
|
|
|
|
# Set the fee rate the exchange is charging.
|
|
self.fee = fee
|
|
|
|
# If status is None this is a new trade. Set the default
|
|
# values.
|
|
if status is None:
|
|
# Trade status flag. ['inactive'|'unfilled'|'filled'|'part_filled'|'closed']
|
|
self.status = 'inactive'
|
|
|
|
# Trade statistics.
|
|
self.stats = {
|
|
'opening_price': price,
|
|
'opening_value': (qty * price),
|
|
'current_price': price,
|
|
'current_value': (qty * price),
|
|
'settled_price': 0,
|
|
'settled_value': 0,
|
|
'qty_filled': 0,
|
|
'qty_settled': 0,
|
|
'profit': 0,
|
|
'profit_pct': 0,
|
|
'fee_paid': 0
|
|
}
|
|
else:
|
|
# Loading an existing trade. Set the values.
|
|
self.status = status
|
|
self.stats = stats
|
|
self.order = order
|
|
|
|
def get_position_size(self):
|
|
"""
|
|
Position_size is the value of the trade in the quote currency.
|
|
eg .1 BTC @ $10,000 BTCUSD is equal to $1,000 USD.
|
|
"""
|
|
return self.stats['current_value']
|
|
|
|
def get_pl(self):
|
|
"""
|
|
Return: <float> - a positive profit or a negative loss.
|
|
"""
|
|
return self.stats['profit']
|
|
|
|
def get_pl_pct(self):
|
|
"""
|
|
Return: <float> - a percentage - positive profit or a negative loss.
|
|
"""
|
|
return self.stats['profit_pct']
|
|
|
|
def get_status(self):
|
|
"""
|
|
Return: <str> - Returns the status of the trade.
|
|
"""
|
|
return self.status
|
|
|
|
# def place_order(self):
|
|
# """
|
|
# :return: on success: order, on fail: <str>'failed'
|
|
# """
|
|
#
|
|
# try:
|
|
# print('Trade:place_order(): Attempting to place order')
|
|
# print(f"symbol={self.symbol}, side={self.side}, type={self.order_type}, timeInForce='GTC',",
|
|
# f" quantity={self.position_size}, price={self.stats['opening_price']}")
|
|
#
|
|
# # Market orders immediately fill with best price available in the orderbook.
|
|
# if self.order_type == 'MARKET':
|
|
# order = self.exchange.client.create_test_order(symbol=self.symbol,
|
|
# side=self.side,
|
|
# type=self.order_type,
|
|
# quoteOrderQty=self.quoteOrderQty,
|
|
# newClientOrderId=self.unique_id)
|
|
# # Limit orders are posted in the orderbook.
|
|
# # The order's behavior is modified by setting time_in_force.
|
|
# # GTC (Good-Till-Cancel), IOC (Immediate-Or-Cancel), FOK (Fill-Or-Kill)
|
|
# elif self.order_type == 'LIMIT':
|
|
# order = self.exchange.client.create_test_order(symbol=self.symbol,
|
|
# side=self.side,
|
|
# type=self.order_type,
|
|
# timeInForce=self.time_in_force,
|
|
# quantity=self.position_size,
|
|
# price=self.stats['opening_price'],
|
|
# newClientOrderId=self.unique_id)
|
|
# # This is here incase I want to make a custom order type.
|
|
# else:
|
|
# print(f'Trade: No Implementation for trade.order: {self.order_type}')
|
|
# return None
|
|
# # If we get here an exception was not thrown by the exchange.
|
|
# print('Trade:place_order(): Order successfully created.')
|
|
# self.flags['order_placed'] = True
|
|
# return order
|
|
# # Report error if order fails
|
|
# except Exception as e:
|
|
# print(e, "error")
|
|
# return 'failed'
|
|
|
|
def update_values(self, current_price):
|
|
"""
|
|
Update the P/L values and percentages.
|
|
|
|
:param current_price - current price
|
|
:return: None
|
|
"""
|
|
|
|
def percent(part, whole):
|
|
# Return a percentage.
|
|
if whole == 0:
|
|
return 0
|
|
pct = 100 * float(part) / float(whole)
|
|
return pct
|
|
|
|
# Update the current price.
|
|
self.stats['current_price'] = current_price
|
|
|
|
# Get the value at entry into the position.
|
|
initial_value = self.stats['opening_value']
|
|
|
|
# Calculate the current value in the quote currency.
|
|
self.stats['current_value'] = float(self.stats['qty_filled'] * current_price)
|
|
|
|
# Calculate and set the profit or loss.
|
|
self.stats['profit'] = self.stats['current_value'] - initial_value
|
|
|
|
# If it is a short, reverse the sign.
|
|
if self.side == 'sell':
|
|
self.stats['profit'] *= -1
|
|
|
|
# Subtract the trade in/out fees that would be paid if settled at the current price.
|
|
projected_fees = (self.stats['current_value'] * self.fee) + (self.stats['opening_value'] * self.fee)
|
|
self.stats['profit'] -= projected_fees
|
|
|
|
# Calculate and set the profit/loss percentage.
|
|
self.stats['profit_pct'] = percent(self.stats['profit'], initial_value)
|
|
|
|
return
|
|
|
|
def update(self, current_price):
|
|
"""
|
|
Update the trade and return the status of the trade.
|
|
|
|
:param current_price: <float> - Most recent price update.
|
|
:return: <str> - Trade status.
|
|
"""
|
|
|
|
# If the trade has been closed return status without updating.
|
|
if self.status == 'closed':
|
|
return self.status
|
|
|
|
# Update all price dependent values in the trade.
|
|
self.update_values(current_price)
|
|
|
|
# If everything is updated and trade is live return status.
|
|
return self.status
|
|
|
|
def order_placed(self, order):
|
|
"""
|
|
Set the status and store the order after it has been placed on the exchange.
|
|
|
|
:param order: The order received in response to placing an order on the exchange.
|
|
:return: None
|
|
"""
|
|
self.status = 'unfilled'
|
|
self.order = order
|
|
return
|
|
|
|
def trade_filled(self, qty, price):
|
|
"""
|
|
Records the quantity filled and updates the values and sets the status.
|
|
|
|
:param qty <float> - The quantity filled.
|
|
:param price <float> - The price paid.
|
|
:return: None
|
|
"""
|
|
# Todo maybe inactive flag is un-necessary.
|
|
# If the trade is being filled it must have been active.
|
|
if self.status == 'inactive':
|
|
self.status = 'unfilled'
|
|
|
|
# If none of the trade had been filled yet.
|
|
if self.status == 'unfilled':
|
|
# Record the quantity that has been filled.
|
|
self.stats['qty_filled'] = qty
|
|
# Record the price actually paid.
|
|
self.stats['opening_price'] = price
|
|
else:
|
|
# If adding to a partially filled trade, calculate and record
|
|
# the weighted average of the trade as the opening price.
|
|
sum_of_values = (qty * price) + self.stats['opening_value']
|
|
t_qty = self.stats['qty_filled'] + qty
|
|
weighted_average = sum_of_values / t_qty
|
|
self.stats['opening_price'] = weighted_average
|
|
# Add to the quantity that has been filled.
|
|
self.stats['qty_filled'] += qty
|
|
|
|
# Adjust the opening_value record.
|
|
self.stats['opening_value'] = self.stats['qty_filled'] * self.stats['opening_price']
|
|
|
|
# Set the appropriate status for the trade.
|
|
if self.stats['qty_filled'] == self.base_order_qty:
|
|
self.status = 'filled'
|
|
else:
|
|
self.status = 'part_filled'
|
|
|
|
def settle(self, qty, price):
|
|
"""
|
|
Settle all or part of the trade.
|
|
|
|
:param: qty - The quantity that was settled.
|
|
:param: price - The price the trade was settled at.
|
|
:return: None
|
|
"""
|
|
# If noting has been settled yet.
|
|
if self.stats['qty_settled'] == 0:
|
|
self.stats['settled_price'] = price
|
|
self.stats['settled_value'] = qty * price
|
|
self.stats['qty_settled'] = qty
|
|
else:
|
|
# If settling previously reduced trade, calculate and record.
|
|
# the weighted average of the settled trade as the settled price.
|
|
sum_of_values = (qty * price) + self.stats['settled_value']
|
|
t_qty = self.stats['qty_settled'] + qty
|
|
weighted_average = sum_of_values / t_qty
|
|
self.stats['settled_price'] = weighted_average
|
|
# Add to the quantity that has been settled.
|
|
self.stats['qty_settled'] += qty
|
|
|
|
# Adjust the settled value record.
|
|
self.stats['settled_value'] = self.stats['qty_settled'] * self.stats['settled_price']
|
|
|
|
# If the entire trade is settled, mark the trade as closed.
|
|
if self.stats['qty_settled'] == self.base_order_qty:
|
|
self.status = 'closed'
|
|
|
|
return
|
|
|
|
# TODO left over from changes
|
|
# Check if the trade has been filled.
|
|
# if self.flags['order_filled'] is False:
|
|
# # if it hasn't been filled cancel the order.
|
|
# resp = self.exchange.client.cancel_order(self.unique_id)
|
|
# if resp["status"] == "CANCELED":
|
|
# return True
|
|
# else:
|
|
# # Toggle the side settings.
|
|
# if self.side == 'buy':
|
|
# self.side = 'sell'
|
|
# else:
|
|
# self.side = 'buy'
|
|
# # option MARKET closed the trade for what ever it can get.
|
|
# if option == 'MARKET':
|
|
# self.order_type = 'MARKET'
|
|
# self.place_order()
|
|
|
|
|
|
class Trades:
|
|
def __init__(self, loaded_trades=None):
|
|
"""
|
|
This class receives, executes, tracks and stores all active_trades.
|
|
:param loaded_trades: <list?dict?> A bunch of active_trades to create and store.
|
|
"""
|
|
# Object that maintains exchange and account data
|
|
self.exchange = None
|
|
|
|
# Socket connection to the exchange.
|
|
self.client = None
|
|
|
|
# Exchange fees. Will be fetched from the server and over writen on connection.
|
|
self.exchange_fees = {'maker': 0.01, 'taker': 0.05}
|
|
# Hedge mode allows long and shorts to be placed simultaneously.
|
|
self.hedge_mode = False
|
|
# If hedge mode is disabled this is either {'buy','sell'}.
|
|
self.side = None
|
|
# A list of active trades.
|
|
self.active_trades = []
|
|
# A list of trades that have been closed.
|
|
self.settled_trades = []
|
|
self.stats = {'num_trades': 0, 'total_position': 0, 'total_position_value': 0}
|
|
|
|
# Load any trades that were passed into the constructor.
|
|
if loaded_trades is not None:
|
|
# Create the active_trades loaded from file.
|
|
self.load_trades(loaded_trades)
|
|
|
|
def connect_exchange(self, exchange):
|
|
"""
|
|
Connect an exchange.
|
|
:param exchange: <Exchange obj> - The exchange to connect.
|
|
:return: None
|
|
"""
|
|
self.exchange = exchange
|
|
self.client = self.exchange.client
|
|
|
|
# TODO figure out if I need anything like this.
|
|
# Find out if hedge-mode is enabled and set a local flag.
|
|
# pm = self.client.futures_get_position_mode()
|
|
# self.hedge_mode = pm.dualSidePosition
|
|
return
|
|
|
|
def exchange_connected(self):
|
|
"""
|
|
Report if an exchange has been connected to ths object.
|
|
"""
|
|
if self.exchange is None:
|
|
return False
|
|
else:
|
|
return True
|
|
|
|
def get_trades(self, form):
|
|
# Return a python object of all the trades stored in this instance.
|
|
if form == 'obj':
|
|
return self.active_trades
|
|
# Return a JSON object of all the trades stored in this instance.
|
|
elif form == 'json':
|
|
trades = self.active_trades
|
|
json_str = []
|
|
for trade in trades:
|
|
json_str.append(trade.to_json())
|
|
return json_str
|
|
# Return a dictionary object of all the trades stored in this instance.
|
|
elif form == 'dict':
|
|
trades = self.active_trades
|
|
t_list = []
|
|
for trade in trades:
|
|
dic = trade.__dict__
|
|
t_list.append(dic)
|
|
return t_list
|
|
|
|
def get_trades_by_status(self, status):
|
|
"""
|
|
Evaluate all active_trades. Return a list of active_trades with the specified
|
|
status.
|
|
|
|
:param status <str>: status to evaluate - ['inactive'|'unfilled'|'filled'|'part_filled'|'closed']
|
|
:return: List of any trade that have the flag set.
|
|
"""
|
|
ret_list = []
|
|
for trade in self.active_trades:
|
|
if trade.status is status:
|
|
ret_list.append(trade)
|
|
return ret_list
|
|
|
|
def get_trade_by_id(self, trade_id):
|
|
"""
|
|
Returns 1st trade found that matches the ID specified.
|
|
|
|
:param trade_id: unique id
|
|
:return: trade object
|
|
"""
|
|
for trade in self.active_trades:
|
|
if trade.unique_id == trade_id:
|
|
return trade
|
|
|
|
def load_trades(self, trades):
|
|
"""
|
|
Creates trade objects from a list trades and attributes.
|
|
|
|
:param trades: a list of active_trades to load.
|
|
|
|
"""
|
|
# Loop through the list of active_trades provided.
|
|
for trade in trades:
|
|
# Append a local list of active_trades with a new trade created trade object.
|
|
# Pass the properties of trade and a reference of the exchange into the constructor.
|
|
self.active_trades.append(Trade(self.exchange, **trade))
|
|
return
|
|
|
|
def place_order(self, trade):
|
|
"""
|
|
Execute a place order command on the exchange.
|
|
|
|
:param trade: Trade object
|
|
:return: on success: order, on fail: <str>'failed'
|
|
"""
|
|
|
|
try:
|
|
# Log the action to the command line.
|
|
print('\nTrades:place_order(): Attempting to place order on the exchange.')
|
|
print(f"symbol={trade.symbol}, side={trade.side}, type={trade.order_type}, timeInForce=trade.timeInForce,",
|
|
f" quantity={trade.base_order_qty}, price={trade.stats['opening_price']}")
|
|
|
|
# Market orders immediately fill with best price available in the orderbook.
|
|
if trade.order_type == 'MARKET':
|
|
order = self.exchange.client.create_test_order(symbol=trade.symbol,
|
|
side=trade.side,
|
|
type=trade.order_type,
|
|
quantity=trade.base_order_qty,
|
|
newClientOrderId=trade.unique_id)
|
|
# Limit orders are posted in the orderbook.
|
|
# The order's behavior is modified by setting time_in_force.
|
|
# GTC (Good-Till-Cancel), IOC (Immediate-Or-Cancel), FOK (Fill-Or-Kill)
|
|
elif trade.order_type == 'LIMIT':
|
|
order = self.exchange.client.create_test_order(symbol=trade.symbol,
|
|
side=trade.side,
|
|
type=trade.order_type,
|
|
timeInForce=trade.time_in_force,
|
|
quantity=trade.base_order_qty,
|
|
price=trade.stats['opening_price'],
|
|
newClientOrderId=trade.unique_id)
|
|
# This is here incase I want to make a custom order type.
|
|
else:
|
|
print(f'Trade: No Implementation for trade.order: {trade.order_type}')
|
|
return 'failed'
|
|
# If we get here an exception was not thrown by the exchange.
|
|
# Log the success to the command line.
|
|
print('Trade:place_order(): Order successfully created.')
|
|
print(order)
|
|
# Store the order and set the status flag in the trade object.
|
|
trade.order_placed(order)
|
|
return 'success'
|
|
# Report error if order fails.
|
|
except Exception as e:
|
|
# Log the error to the commandline.
|
|
print(e, "error")
|
|
return 'failed'
|
|
|
|
def update(self, price):
|
|
"""
|
|
Updates the price all active trades.
|
|
:param price: current.
|
|
:return: list of updated trade data
|
|
"""
|
|
r_update = []
|
|
for trade in self.active_trades:
|
|
|
|
# If the trade hasn't been filled check the status on the exchange.
|
|
if trade.status == 'unfilled' or trade.status == 'part-filled':
|
|
# Get an updated version of the order from the exchange.
|
|
posted_order = self.client.get_order(symbol=trade.symbol, orderId=trade.order.orderId)
|
|
# Check to see if the order is filled.
|
|
if posted_order.orderStatus == 'FILLED':
|
|
# If the order is filled update the trade object.
|
|
trade.trade_filled(posted_order.toAmount, posted_order.fromAmount)
|
|
else:
|
|
# If the order is not filled, do nothing.
|
|
pass
|
|
if trade.status == 'inactive':
|
|
# Check to see if this trade is supposed to be posted on the exchange.
|
|
if trade.dest == 'exchange':
|
|
# Todo: Not sure if a use-case exist where the order will fail to be placed in new_trade.
|
|
# If the trade is still inactive new_trade must have failed to place the order.
|
|
# Check to see if the order actually doesn't exist.
|
|
open_orders = self.client.get_open_orders(symbol=trade.symbol)
|
|
order_not_placed = True
|
|
for order in open_orders:
|
|
if order.orderId is trade.unique_id:
|
|
order_not_placed = False
|
|
if order_not_placed:
|
|
# TODO: Log error encountered in new_trade to avoid failing repeatedly.
|
|
# Attempt to place the order again.
|
|
result = self.place_order(trade)
|
|
if result == 'success':
|
|
print(f'Trades:Update(): Order successfully placed on exchange.')
|
|
|
|
ret = trade.update(price)
|
|
if ret == 'updated' or ret == 'trade_filled':
|
|
r_update.append({
|
|
'status': ret,
|
|
'id': trade.unique_id,
|
|
'pl': trade.stats['profit'],
|
|
'pl_pct': trade.stats['profit_pct']
|
|
})
|
|
else:
|
|
r_update.append({'id': trade.unique_id, 'status': ret})
|
|
return r_update
|
|
|
|
def new_trade(self, target, symbol, side, order_type, qty, price=None, offset=None):
|
|
"""
|
|
Return a reference to a newly created a Trade object.
|
|
|
|
:param target: <str> - ['exchange'|'backtester']The destination for this trade.
|
|
:param symbol - Symbol of the trading pair.
|
|
:param side - Buy|Sell
|
|
:param order_type - Market|Limit|Trailing
|
|
:param qty - Quantity of order
|
|
:param price - Price to buy at. A relative price may be set by passing a string preceded with a +/-
|
|
:param offset - Is an amount to offset a trailing limit order.
|
|
"""
|
|
|
|
# For now, you must be connected to an exchange to make a trade.
|
|
if not self.exchange_connected():
|
|
# Todo: There are some values pulled from the exchange here that make this cumbersum.
|
|
# I think this should be able to que trades and place them after connection.
|
|
# Clarify whether or not all references to precision, min and max
|
|
# values could be move to the place_order() method. To de-couple this method from Exchange.
|
|
return 'Error', 'No exchange connected.'
|
|
|
|
# The required level of precision for this trading pair.
|
|
precision = self.exchange.get_precision(symbol)
|
|
|
|
# Function to convert a value to a float set to the desired precision.
|
|
def f_val(val):
|
|
return float(f"{val:.{precision}f}")
|
|
|
|
# The minimum value aloud to be traded.
|
|
minimum_n_qty = f_val(self.exchange.get_min_notional_qty(symbol))
|
|
# The minimum quantity aloud to be traded.
|
|
minimum_qty = f_val(self.exchange.get_min_qty(symbol))
|
|
|
|
# If quantity isn't supplied set it to the smallest allowable trade.
|
|
try:
|
|
qty = f_val(qty)
|
|
except (Exception,):
|
|
qty = minimum_qty
|
|
# If quantity was supplied, but it's too low, return an error.
|
|
if qty < minimum_qty:
|
|
return 'Error', 'Order-size too low.'
|
|
|
|
# TODO: A hedge mode toggle complicates the behavior of settling and reducing trades.
|
|
# Leaving it out for now.
|
|
# Notes - A behavior diagram would be helpful to sort out the logic here.
|
|
# When do you define a side? Trading out is not a side-change.
|
|
# If hedge mode is disabled return False if trade is on opposite side.
|
|
# if self.hedge_mode is False:
|
|
# if self.side is None:
|
|
# self.side = side
|
|
# if self.side != side:
|
|
# return 'Error', 'Wrong side. Settle trade or enable hedge mode.'
|
|
|
|
# Ensure offset is at least set to the smallest allowable value of the quote asset.
|
|
if offset is None or offset < minimum_n_qty:
|
|
offset = minimum_n_qty
|
|
|
|
# If no price is given, set relative offset to the current price.
|
|
if price is None:
|
|
if side == 'buy':
|
|
price = f'-{offset}'
|
|
else:
|
|
price = f'+{offset}'
|
|
|
|
# If price is a string process and convert it to a float.
|
|
if type(price) == str:
|
|
# If it is proceeded with a +/-, processes as a relative limit order.
|
|
if ('+' in price) or ('-' in price):
|
|
# Covert the relative value into an absolute offset of the current price.
|
|
price = self.exchange.get_price(symbol) + float(price)
|
|
# Convert the string into a float.
|
|
price = f_val(price)
|
|
|
|
# position_size is the value of the trade in the parent currency.
|
|
# eg .1 BTC @ $10,000 USD/BTC is equal to $1,000 USD.
|
|
position_size = f_val(qty * price)
|
|
|
|
# Log to terminal for development. Todo remove when stable.
|
|
print('\nTrades:new_trade(): Creating a trade object.')
|
|
print(f'Order amount: {position_size:.6f}')
|
|
print(f'Minimum usd: {minimum_n_qty:.2f}')
|
|
|
|
# Create a trade.
|
|
trade = Trade(dest=target, symbol=symbol, side=side, price=price, qty=qty, order_type=order_type, tif='GTC',
|
|
unique_id=None, status=None, stats=None, order=None)
|
|
|
|
# If the trade is meant to be on the exchange. Call the method to place the trade on the exchange.
|
|
if target == 'exchange':
|
|
# Attempt to place the order.
|
|
result = self.place_order(trade)
|
|
if result == 'success':
|
|
print(f'Trades:new_trade(): Order successfully placed on exchange.')
|
|
|
|
# Add the trade to a list of active_trades instance.
|
|
self.active_trades.append(trade)
|
|
|
|
# Return the trade ID
|
|
return 'Success', trade.unique_id
|
|
|
|
def close_trade(self, trade_id):
|
|
"""
|
|
Close the trade.
|
|
:param trade_id: <hex> - The unique ID of the trade.
|
|
:return: <bool> - Return True if successful.
|
|
"""
|
|
trade = self.get_trade_by_id(trade_id)
|
|
qty = trade.base_order_qty
|
|
price = self.exchange.get_price(trade.symbol)
|
|
trade.settle(qty=qty, price=price)
|
|
if trade.status == 'closed':
|
|
self.active_trades.remove(trade)
|
|
self.settled_trades.append(trade)
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def reduce_trade(self, trade_id, qty):
|
|
"""
|
|
Reduce the position of a trade.
|
|
|
|
:param trade_id: <hex> - The Unique id of a tradee
|
|
:param qty: <float> - The quantity to reduce the trade by.
|
|
:return: <float> - The base quantity left over.
|
|
"""
|
|
# Fetch the trade object.
|
|
trade = self.get_trade_by_id(trade_id)
|
|
if trade.status == 'closed':
|
|
# Can't reduce a trade that's been closed.
|
|
return None
|
|
if trade.status == 'inactive' or trade.status == 'unfilled':
|
|
# If it has been placed but not filled.
|
|
if trade.status == 'unfilled':
|
|
# Cancel the order.
|
|
self.client.cancel_order(symbol=trade.symbol, orderId=trade.unique_id)
|
|
|
|
# The order hasn't been placed so just reduce the order quantity.
|
|
# Make sure you are not reducing more than the original order.
|
|
if qty > trade.base_order_qty:
|
|
qty = trade.base_order_qty
|
|
trade.base_order_qty -= qty
|
|
# Adjust the opening value.
|
|
trade.stats['opening_value'] = (trade.base_order_qty * trade.stats['opening_price'])
|
|
# Re-calculate values so the PL will represent the new value - fees.
|
|
trade.update_values(trade.stats['opening_price'])
|
|
|
|
# If it has been previously placed but not filled and now canceled.
|
|
if trade.status == 'unfilled':
|
|
# Replace the order.
|
|
self.place_order(trade=trade)
|
|
# Return the new total quantity to buy.
|
|
return trade.base_order_qty
|
|
|
|
# You can't settle more than you own.
|
|
if qty > trade.stats['qty_filled']:
|
|
qty = trade.stats['qty_filled']
|
|
# Retrieve the current price.
|
|
price = self.exchange.get_price(trade.symbol)
|
|
# Settle the quantity requested.
|
|
trade.settle(qty=qty, price=price)
|
|
# If the trade is closed remove from the list of open active_trades.
|
|
if trade.status == 'closed':
|
|
self.active_trades.remove(trade)
|
|
self.settled_trades.append(trade)
|
|
return 0
|
|
else:
|
|
# If the trade is not closed then return the total left open.
|
|
left = trade.stats['qty_filled'] - trade.stats['qty_settled']
|
|
return float(f"{left:.3f}")
|