Files
econ_emt/econ/exchange.py

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)