355 lines
12 KiB
Python
355 lines
12 KiB
Python
_bal="balance"
|
|
from lightmatchingengine.lightmatchingengine import LightMatchingEngine,Side,Trade,Order,OrderBook
|
|
import log
|
|
from influxdb_client.client.write_api import SYNCHRONOUS
|
|
from influxdb_client import InfluxDBClient, Point
|
|
import time
|
|
import pandas as pd
|
|
import uuid
|
|
import threading
|
|
|
|
software_start=int(time.time())
|
|
class Exchange():
|
|
"""
|
|
Basic Commodity exchange.
|
|
"""
|
|
|
|
def __init__(self) -> None:
|
|
|
|
self.reset()
|
|
pass
|
|
|
|
def reset(self):
|
|
self.account={}
|
|
self.escrow={}
|
|
self.lme=LightMatchingEngine()
|
|
self.order_account_map={}
|
|
self.orders={}
|
|
self.executed_trades=[]
|
|
self.order_trades_map={}
|
|
self.market_rate={}
|
|
self.best_ask={}
|
|
self.best_bid={}
|
|
self.total_demand={}
|
|
#self.demand={}
|
|
self.total_supply={}
|
|
#self.supply={}
|
|
self.traded_commoditys={}
|
|
self.lock=threading.Lock()
|
|
|
|
def add_to_account(self,account_id,resource,amount):
|
|
"""
|
|
Adds resources to account escrow
|
|
"""
|
|
#check if account exists
|
|
self.lock.acquire()
|
|
if account_id not in self.account:
|
|
self.account[account_id]={_bal: 0}
|
|
#check if ressource exists
|
|
if resource not in self.account[account_id]:
|
|
self.account[account_id][resource]=0
|
|
|
|
self.account[account_id][resource]+=amount
|
|
self.lock.release()
|
|
|
|
def remove_from_account(self,account_id,resource,amount) -> int:
|
|
"""
|
|
Remove resources from account. Returns amount retrieved
|
|
"""
|
|
#check if account exists
|
|
self.lock.acquire()
|
|
if account_id not in self.account:
|
|
self.account[account_id]={_bal: 0}
|
|
#check if ressource exists
|
|
if resource not in self.account[account_id]:
|
|
self.account[account_id][resource]=0
|
|
ret=amount
|
|
remain=self.account[account_id][resource]-amount
|
|
if remain<0:
|
|
ret=self.account[account_id][resource]
|
|
self.account[account_id][resource]=0
|
|
else:
|
|
self.account[account_id][resource]-=amount
|
|
self.lock.release()
|
|
return ret
|
|
|
|
def _move_to_escrow(self,account_id,resource,amount):
|
|
"""
|
|
Adds resources from account to escrow. Return true if done.
|
|
"""
|
|
if not self.check_account_resource(account_id,resource,amount):
|
|
return False
|
|
|
|
#check if account exists
|
|
if account_id not in self.escrow:
|
|
self.escrow[account_id]={_bal: 0}
|
|
#check if ressource exists
|
|
if resource not in self.escrow[account_id]:
|
|
self.escrow[account_id][resource]=0
|
|
|
|
self.escrow[account_id][resource]+=amount
|
|
self.account[account_id][resource]-=amount
|
|
return True
|
|
|
|
def _move_to_account(self,account_id,resource,amount):
|
|
"""
|
|
Move resources from escrow to account.
|
|
"""
|
|
ret=amount
|
|
self.escrow[account_id][resource]-=amount
|
|
|
|
|
|
if resource not in self.account[account_id]:
|
|
self.account[account_id][resource]=0
|
|
self.account[account_id][resource]+=amount
|
|
|
|
|
|
def check_account_resource(self,account_id,resource,amount):
|
|
"""
|
|
Checks if account has sufficient resources in inventory
|
|
"""
|
|
if account_id not in self.account:
|
|
self.account[account_id]={_bal: 0}
|
|
#check if ressource exists
|
|
if resource not in self.account[account_id]:
|
|
self.account[account_id][resource]=0
|
|
|
|
if self.account[account_id][resource]>=amount:
|
|
return True
|
|
return False
|
|
|
|
def submit_order(self,account_id,resource,amount,price,side :int) -> Order:
|
|
"""
|
|
Submits an order to the exchange if resources are sufficent.
|
|
Price is price per single item.
|
|
Returns an order if the order is active. Returns None if submit has failed.
|
|
"""
|
|
# calculate price for complete order fullfilment
|
|
prev=amount
|
|
amount=int(amount)
|
|
if amount<1:
|
|
# invalid order
|
|
return None
|
|
full_price=round(price*amount,2)
|
|
|
|
|
|
# Move resources into escrow
|
|
if side==Side.BUY:
|
|
escrow=self._move_to_escrow(account_id,_bal,full_price)
|
|
else:
|
|
escrow=self._move_to_escrow(account_id,resource,amount)
|
|
|
|
if not escrow:
|
|
# no sufficient resources
|
|
return None
|
|
# create order and execude any trades
|
|
self.lock.acquire()
|
|
order,trades=self.lme.add_order(resource,price,amount,side)
|
|
|
|
self.orders[order.order_id]=order
|
|
self.order_account_map[order.order_id]=account_id
|
|
self._execute_trades(trades)
|
|
self.calculate_resource_metrics(resource)
|
|
self.lock.release()
|
|
return order
|
|
|
|
def cancel_order(self,order_id):
|
|
"""
|
|
Cancel open order.
|
|
Returns True if order has been canceled.
|
|
"""
|
|
if not order_id in self.orders:
|
|
return False
|
|
order=self.orders[order_id]
|
|
if self._is_order_complete(order):
|
|
return False
|
|
self.lock.acquire()
|
|
order=self.lme.cancel_order(order_id,order.instmt)
|
|
|
|
if order_id in self.order_trades_map: # has order any kind of trades
|
|
trades=self.order_trades_map[order_id]
|
|
# return assets from escrow to account
|
|
if order.side==Side.BUY:
|
|
total_payed=0
|
|
total_submitted=order.price*order.qty
|
|
for trade in trades:
|
|
total_payed+=trade.trade_price*trade.trade_qty
|
|
total_diff=total_submitted-total_payed
|
|
self._move_to_account(self.order_account_map[order_id],_bal,total_diff)
|
|
else:
|
|
qty_traded=0
|
|
qty_submitted=order.qty
|
|
for trade in trades:
|
|
qty_traded+=trade.trade_qty
|
|
qty_diff=qty_submitted-qty_traded
|
|
self._move_to_account(self.order_account_map[order_id],order.instmt,qty_diff)
|
|
self.lock.release()
|
|
return True
|
|
|
|
|
|
|
|
def _execute_trades(self, trades):
|
|
for i in trades:
|
|
self._execute_trade(i)
|
|
|
|
def _is_order_complete(self,order: Order):
|
|
return order.leaves_qty==0
|
|
|
|
def _execute_trade(self, trade: Trade):
|
|
account_id=self.order_account_map[trade.order_id]
|
|
|
|
# calculate price for trade fullfilment
|
|
full_price=trade.trade_price*trade.trade_qty
|
|
exprected_price=self.orders[trade.order_id].price*trade.trade_qty
|
|
price_diff=exprected_price-full_price
|
|
|
|
# save trade to order trade map
|
|
if trade.order_id not in self.order_trades_map:
|
|
self.order_trades_map[trade.order_id]=[]
|
|
|
|
self.order_trades_map[trade.order_id].append(trade)
|
|
|
|
|
|
# init escrow if needet
|
|
if trade.instmt not in self.escrow[account_id]:
|
|
self.escrow[account_id][trade.instmt]=0
|
|
|
|
# first edit escrow
|
|
if trade.trade_side==Side.BUY:
|
|
self.escrow[account_id][trade.instmt]+=trade.trade_qty
|
|
self.escrow[account_id][_bal]-=full_price
|
|
# move new resources to account
|
|
self._move_to_account(account_id,trade.instmt,trade.trade_qty)
|
|
# if deal was better then expected move money back
|
|
self._move_to_account(account_id,_bal,price_diff)
|
|
else:
|
|
self.escrow[account_id][trade.instmt]-=trade.trade_qty
|
|
self.escrow[account_id][_bal]+=full_price
|
|
# move new bal into account
|
|
self._move_to_account(account_id,_bal,full_price)
|
|
|
|
self.executed_trades.append(trade)
|
|
#update market rate
|
|
self.market_rate[trade.instmt]=trade.trade_price
|
|
|
|
|
|
def calculate_resource_metrics(self, resource):
|
|
order_book = self.lme.order_books.setdefault(resource, OrderBook())
|
|
best_bid = None
|
|
best_ask = None
|
|
self.total_demand[resource]=0
|
|
#self.demand[resource]={}
|
|
for k,v in order_book.bids.items():
|
|
# self.demand[resource][k]=0
|
|
if best_bid==None:
|
|
best_bid=k
|
|
if best_bid<=k:
|
|
best_bid=k
|
|
for o in v:
|
|
# self.demand[resource][k]+=o.leaves_qty
|
|
self.total_demand[resource]+=o.leaves_qty
|
|
#self.supply[resource]={}
|
|
self.total_supply[resource]=0
|
|
for k,v in order_book.asks.items():
|
|
# self.supply[resource][k]=0
|
|
if best_ask==None:
|
|
best_ask=k
|
|
if best_ask<=k:
|
|
best_ask=k
|
|
for o in v:
|
|
# self.supply[resource][k]+=o.leaves_qty
|
|
self.total_supply[resource]+=o.leaves_qty
|
|
|
|
|
|
self.best_ask[resource]=best_ask
|
|
self.best_bid[resource]=best_bid
|
|
self.traded_commoditys[resource]=True
|
|
|
|
def get_account_resource_amount(self,account_id,resource):
|
|
"""
|
|
Returns the amount of selected resource in account
|
|
"""
|
|
if account_id in self.account:
|
|
if resource in self.account[account_id]:
|
|
return self.account[account_id][resource]
|
|
return 0
|
|
|
|
def get_total_demand(self,resource):
|
|
if resource in self.total_demand:
|
|
return self.total_demand[resource]
|
|
return 0
|
|
def get_total_supply(self,resource):
|
|
if resource in self.total_supply:
|
|
return self.total_supply[resource]
|
|
return 0
|
|
|
|
def log_step(self,name,episode,episode_length,step,autolog=True):
|
|
timepoint=episode*episode_length+step
|
|
localStepDB={}
|
|
|
|
for instrm,order in self.traded_commoditys.items():
|
|
data={}
|
|
data["cxid"]=name
|
|
data["episode"]=episode
|
|
#data["step"]=step
|
|
data["tstep"]=timepoint
|
|
data["instrm"]=instrm
|
|
data["best_ask"]=0
|
|
data["best_bid"]=0
|
|
data["total_demand"]=0
|
|
data["total_supply"]=0
|
|
data["market_rate"]=0
|
|
localStepDB[instrm]=data
|
|
|
|
## add best ask/best bid
|
|
for item , value in self.best_ask.items():
|
|
if value!=None:
|
|
localStepDB[item]["best_ask"]=int(value)
|
|
for item , value in self.best_bid.items():
|
|
if value!=None:
|
|
localStepDB[item]["best_bid"]=int(value)
|
|
#demand supply
|
|
for item , value in self.total_demand.items():
|
|
if value!=None:
|
|
localStepDB[item]["total_demand"]=int(value)
|
|
for item , value in self.total_supply.items():
|
|
if value!=None:
|
|
localStepDB[item]["total_supply"]=int(value)
|
|
#last market rate
|
|
for item , value in self.market_rate.items():
|
|
if value!=None:
|
|
localStepDB[item]["market_rate"]=int(value)
|
|
|
|
if autolog:
|
|
for id,item in localStepDB.items():
|
|
log.EXBooksData.append(item)
|
|
return localStepDB
|
|
|
|
def log_episode(self,name,episode):
|
|
transformed={}
|
|
|
|
for t in self.executed_trades:
|
|
if t.trade_side==1:
|
|
# will get logged twice so only log one side
|
|
if t.instmt not in transformed:
|
|
transformed[t.instmt]={}
|
|
|
|
if t.trade_price not in transformed[t.instmt]:
|
|
transformed[t.instmt][t.trade_price]=0
|
|
transformed[t.instmt][t.trade_price]+=t.trade_qty
|
|
|
|
for inst,stats in transformed.items():
|
|
for price,qty in stats.items():
|
|
data={}
|
|
#data["id"]=uuid.uuid4()
|
|
data["cxid"]=name
|
|
data["episode"]=int(episode)
|
|
#data["order_id"]=t.order_id
|
|
data["instmt"]=t.instmt
|
|
data["price"]=int(price)
|
|
data["qty"]=int(qty)
|
|
|
|
#data["side"]=int(t.trade_side)
|
|
log.EXTradeData.append(data)
|
|
|