Skip to content

Instantly share code, notes, and snippets.

@HanaanY
Last active April 7, 2016 00:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HanaanY/ba514d4aebef62bd4c7e to your computer and use it in GitHub Desktop.
Save HanaanY/ba514d4aebef62bd4c7e to your computer and use it in GitHub Desktop.
Game.py is the game engine and includes I/O , Tradesimulator.py creates players for the game and behaviour classes for bots
#!python3
import random
import tradesimulator as ts
import sys
behaviourarray = {
'takerisk': ts.TakeRisk(),
'avoidrisk': ts.AvoidRisk(),
'findarbs': ts.FindArbs(),
}
player1 = ts.Player(behaviourarray, 'nassim', 1000)
player2 = ts.Player(behaviourarray, 'gordon', 2000)
player3 = ts.Player(behaviourarray, 'warren')
human = ts.Player(None, input("\nInput name >> "), 1000)
traders = {
player1.name: player1,
player2.name: player2,
player3.name:player3,
human.name: human
}
inplay = [] # inplay is a deck of cards to be populated by 5 cards with random values 0-9
r = 0 # r counts game round
def exp_value(deck):
"""Used by players to calculate expected market value"""
return sum(deck) + (5 - len(deck)) * 5
def reveal_card(deck):
"""Creates a card with random value 0-9 and adds it to the deck"""
deck.append(random.randint(0,9))
return deck
def validate(msg, checkspace, errormsg):
""" Validates player inputs to make sure they exist in 'checkspace'"""
while True:
try:
userinput = input(msg).lower()
if not userinput in checkspace:
raise ValueError(errormsg)
return userinput
except ValueError as e:
print(e)
def validate_size(sizeinput, tradetype, net_position, limit, limit_name, cp_limit=False):
""" Validates player input sizes and stops a limit breach."""
while True:
try:
userinput = int(input(sizeinput))
try:
if userinput < 0:
raise ValueError("Size can not be below zero")
if tradetype == 'offer':
desired_position = userinput * -1
else:
desired_position = userinput
if abs(net_position + desired_position) > limit:
raise ValueError("This size exceeds {}".format(limit_name))
if not cp_limit:
pass
elif userinput > abs(cp_limit):
raise ValueError("The size quoted is {}, enter a lower amount".
format(abs(cp_limit)))
return userinput
except ValueError as e:
print(e)
except ValueError:
print("That doesn't seem to be a number, try again")
def player_io(net_position, position_limit):
""" Takes human player's input bid/offer prices and sizes for submit phase"""
# the dictionary collects input info. Each key has a price and a size
output = []
#loop io over 'bid' and 'offer'
for key in ['bid', 'offer']:
while True:
try: #try to get an input for price and assign it to the 1st element of the value list
price = int(input("Input {} price >> ".format(key)))
if price < 0:
print("Price can not be below zero")
else:
break
except ValueError:
print("That doesn't seem to be a number, try again.")
size = validate_size(
"Input {} size >> ".format(key), key,
net_position, position_limit, 'position limit'
)
output.extend([price,size])
return output
def player_trade(net_position, position_limit, human_name):
""" Takes players inputs for trade phase. Actions: 'H' for hit bid or 'L' for lift offer.
Counterparty: Must be from a trader in the game. Size must <= CP's quote and own position limit
"""
#strip your own listing out (we don't want a player to trade with themselves)
quotes = ts.market.copy()
del quotes[human_name]
actions = {
'h':{'action':'hit', 'cptradetype':'bid', 'tradetype':'offer'},
'l':{'action':'lift', 'cptradetype':'offer', 'tradetype':'bid'}
}
while True:
try:
player_action = validate(
"What would you like to do? [h]it bid or [l]ift offer? ", actions.keys(),
'Not a valid action')
if player_action == 'h':
if not [i['bidsize'] for i in quotes.values() if i['bidsize']]:
raise ValueError("There are no bids to hit! Select another action.")
elif player_action == 'l':
if not [i['offersize'] for i in quotes.values() if i['offersize']]:
raise ValueError("There are no offers to lift! Select another action.")
break
except ValueError as e:
print(e)
#we check to see if the counterparty is valid given the action
while True:
try:
cp = validate(
"Who do you want to {}? ".format(actions[player_action]['action']), quotes.keys(),
"That's not someone you can trade with"
)
actions['h']['sz'] = quotes[cp]['bidsize']
actions['l']['sz'] = quotes[cp]['offersize']
target = actions[player_action]['sz']
if not target:
raise ValueError("That CP has no {}! Try someone else".
format(actions[player_action]['action']))
break
except ValueError as e:
print(e)
# we specify the trade type from the players perspective and appropriate counterparty size
actions['h']['price'] = quotes[cp]['bidpx']
actions['h']['cpsize'] = quotes[cp]['bidsize']
actions['l']['price'] = quotes[cp]['offerpx']
actions['l']['cpsize'] = quotes[cp]['offersize']
size = validate_size(
"What size? ", actions[player_action]['tradetype'],
net_position, position_limit, 'position limit',
cp_limit=actions[player_action]['cpsize']
)
return [[actions[player_action]['price'], actions[player_action]['tradetype'], size,
cp, actions[player_action]['cptradetype'], traders[cp]]]
def market_output(quotes):
"""Collects the market quotes and prints them in a readible format"""
# table widths for different types of column
tw = {'namewidth':8, 'pricewidth':7, 'sizewidth': 7}
print('\n')
print('{} {} x {} {} x {}'.format(
'Name'.center(tw['namewidth']),
'Bid'.center(tw['pricewidth']),
'Offer'.center(tw['pricewidth']),
'Size'.center(tw['sizewidth']),
'Size'.center(tw['sizewidth']),
))
# print seperator '='
print('{} {} {} {} {}'.format(
'=' * tw['namewidth'],
'=' * tw['pricewidth'],
'=' * tw['pricewidth'],
'=' * tw['sizewidth'],
'=' * tw['sizewidth'],
))
# clean up the market information
def munge(n):
if n is None:
return ''
elif type(n) is str:
return n
else:
return str(n)
# print each quote held in the market information dict
for k, v in quotes.items():
name = k
bid, b_size, offer, o_size = map(munge, (v[key] for key in
['bidpx', 'bidsize', 'offerpx', 'offersize']))
print('{:.{}} {} x {} {} x {}'.format(
name.ljust(tw['namewidth']), tw['namewidth'],
bid.rjust(tw['pricewidth']),
offer.ljust(tw['pricewidth']),
b_size.rjust(tw['sizewidth']),
o_size.ljust(tw['sizewidth']),
))
print('\n')
# Start the game
print("""
Welcome {}. \nYou are trading the market value of a deck of cards.
The value of each card is revealed at the end of each round and takes random value from 0 - 9.
There are 5 rounds. You must submit a bid and offer at the beginning of each round.
You then get the chance to trade with the other players.
See if you can beat the other players!
""".format(human.name))
print("The current deck is empty\n")
while r <= 4:
presets = {
player1:{'strategy':'avoidrisk', 'round':None},
player2:{'strategy':'takerisk', 'round':r}, #in the loop to pass correct round
player3:{'strategy':'findarbs', 'round':None},
}
# # # # SUBMIT PHASE # # # #
for t in traders.values():
if t == human:
human.submit(choice=player_io(human.net_position, human.position_limit))
else:
t.submit(**presets[t], perceived_mid=exp_value(inplay))
print("\nAll of the players have submitted their bids and offers:")
market_output(ts.market) #print market data structure in readible format
# # # # TRADE PHASE # # # #
for t in traders.values():
if t == human:
print("\nThe market looks like this:")
market_output(ts.market)
human.trade(closedtrade=player_trade(human.net_position, human.position_limit, human.name))
else:
t.trade(**presets[t], perceived_mid=exp_value(inplay), traders=traders)
#track trades
# for t in traders.values():
# print(t.name, "'s trades:", t.trades)
# print(t.name,"'s net position", t.net_position)
print("All of the players have traded")
# # # # REVEAL PHASE (halt trading) # # # #
inplay = reveal_card(inplay)
print("A card is revealed! The card reads {}\n".format(inplay))
r += 1 #add to the round counter
print("\nThe current deck is "+' '.join("[{0}]".format(n) for n in inplay)+'\n')
else:
# calculate scores after all cards revealed and print
print("Game Over. Here are the P&Ls")
mv = sum(inplay) #market value
for t in traders.values():
print(t.name,"'s P&L was", t.PnL(mv))
traderlist = [t for t in traders.values()]
traderPnL= [t.PnL(mv) for t in traderlist]
winningPnL = max(traderPnL)
winner = traderlist[traderPnL.index(winningPnL)]
print("\n{} is the winner!".format(winner.name))
sys.exit()
# NOTE TO SELF: next stage is to look into pattern design & states to remove turn based element
import random
# this is where all the market info is created in the 'submit' phase and will be accessed
# and manipulated in the 'trade' phase
market = {}
# # # the three classes that follow are all types of behaviour # # #
# # # that can be called by objects of type Player. If future may # # #
# # # consider implementing an abstract base class to unite them # # #
class AvoidRisk():
"""Behaviour subtype that has actions to avoid risk in trade phase"""
def __init__(self):
self.default_sz = 200 # use for bids and offers in the submit phase
self.target_sz = 0 # target size in trade phase
self.spread = 1 #spread from mid-market rate used for bids and offers
self.tolerance = 2 #spread tolerance away from mid-market rate
def decide_l(self, net_position, perceived_mid, position_limit, *args):
"""Submit bid/offers of default size subject to limit"""
bidpx = perceived_mid - self.spread # (willing to buy at E(X) - spread)
bidsize = min([position_limit - net_position, self.default_sz])
offerpx = perceived_mid + self.spread # (willing to sell at E(X) + spread)
offersize = min([position_limit - abs(net_position), self.default_sz])
return [bidpx, bidsize, offerpx, offersize]
def sizematcher(self, searchlist, tradelist, direction, my_name, pos_tracker):
""" Goes through list from decide_t and makes trades within tolerance spread"""
if not searchlist or not sum(pos_tracker):
return tradelist
else:
x = sum(pos_tracker) - searchlist[0][2] #check residual amount after first trade
if x == 0: #first trade in list is enough
tradelist.append(searchlist[0])
pos_tracker.append((searchlist[0][2] * -1)) #track change in position
searchlist.pop(0)
return self.sizematcher(searchlist, tradelist, direction, my_name, pos_tracker)
elif x > 0: #first trade not enough,
tradelist.append(searchlist[0])
pos_tracker.append((searchlist[0][2] * -1)) #track change in position
searchlist.pop(0)
return self.sizematcher(searchlist, tradelist, direction, my_name, pos_tracker)
elif x < 0: #first trade too much, take part of the first trade
stub = -x
price, tradetype, size, cp, cptradetype, cpobject = searchlist[0]
partial = [price, tradetype, size - stub, cp, cptradetype, cpobject]
tradelist.append(partial)
pos_tracker.append((partial[2] * -1)) #track change in position
return self.sizematcher(searchlist, tradelist, direction, my_name, pos_tracker)
def decide_t(self, traders, my_name, perceived_mid, net_position):
""" Try to get net position to zero """
if not net_position:
return None
quotes = market.copy()
del quotes[my_name]
b_search = []
o_search = []
if net_position > 0: #then look for suitable bids
b_search = [[value['bidpx'], 'offer', value['bidsize'], key, 'bid', traders[key]]
for key, value in quotes.items()
if value['bidsize'] and value['bidpx'] > perceived_mid - self.tolerance]
elif net_position < 0: #then look for suitable offers
o_search = [[value['offerpx'], 'bid', value['offersize'], key, 'offer', traders[key]]
for key, value in quotes.items()
if value['offersize'] and value['offerpx'] < perceived_mid + self.tolerance]
if b_search:
b_search.sort(key = lambda x:x[0], reverse=True) #sort by bidpx highest to lowest
return self.sizematcher(b_search, [], -1, my_name, [net_position]) #generate bids
elif o_search:
o_search.sort(key = lambda x:x[0]) #sort by offer lowest to highest
return self.sizematcher(o_search, [], 1, my_name, [abs(net_position)]) #generate offers
class TakeRisk():
"""Behaviour subtype that has actions to take risk"""
def __init__(self):
self.target_sz = [300, 450, 600, 750, 900]
self.buybias = 0
self.tolerance = 2 #spread tolerance away from mid-market rate
def decide_l(self, net_position, perceived_mid, position_limit, round):
# coin toss, do i want to buy today? If not, sell.
self.buybias = random.randint(0,1)
if self.buybias:
bidpx = perceived_mid - 1
bidsize = self.target_sz[round] - net_position
return [bidpx, bidsize, None, None]
else:
offerpx = perceived_mid + 1
offersize = self.target_sz[round] + net_position
return [None, None, offerpx, offersize]
def sizematcher(self, searchlist, tradelist, sizename, my_name):
""" Goes through list from decide_t and makes trades within tolerance spread"""
if not searchlist or not market[my_name][sizename]:
return tradelist
else:
x = market[my_name][sizename] - searchlist[0][2] #check residual amount after first trade
if x == 0: #first trade in list is enough
tradelist.append(searchlist[0])
searchlist.pop(0)
market[my_name][sizename] = 0
return self.sizematcher(searchlist, tradelist, sizename, my_name)
elif x > 0: #first trade not enough,
residual = x
market[my_name][sizename] = x
tradelist.append(searchlist[0])
searchlist.pop(0)
return self.sizematcher(searchlist, tradelist, sizename, my_name)
elif x < 0: #first trade too much, take part of the first trade
stub = -x
price, tradetype, size, cp, cptradetype, cpobject = searchlist[0]
partial = [price, tradetype, size - stub, cp, cptradetype, cpobject]
market[my_name][sizename] = 0
tradelist.append(partial)
return self.sizematcher(searchlist, tradelist, sizename, my_name)
def decide_t(self, traders, my_name, perceived_mid, *args):
"""If not yet filled execute as much as possible within spread tolerance """
if not market[my_name]['bidsize'] and not market[my_name]['offersize']:
return None
quotes = market.copy()
del quotes[my_name]
b_search = []
o_search = []
if self.buybias == 0:
b_search = [[value['bidpx'], 'offer', value['bidsize'], key, 'bid', traders[key]]
for key, value in quotes.items()
if value['bidsize'] and value['bidpx'] > perceived_mid - self.tolerance]
elif self.buybias == 1:
o_search = [[value['offerpx'], 'bid', value['offersize'], key, 'offer', traders[key]]
for key, value in quotes.items()
if value['offersize'] and value['offerpx'] < perceived_mid + self.tolerance]
if b_search: #run recursive function to look for bids upto target size
b_search.sort(key = lambda x:x[0], reverse=True) #sort by bidpx highest to lowest
return self.sizematcher(b_search, [], 'offersize', my_name)
elif o_search: #run recursive function to look for offers upto target size
o_search.sort(key = lambda x:x[0]) #sort by offer lowest to highest
return self.sizematcher(o_search, [], 'bidsize', my_name)
class FindArbs():
"""Behaviour subtype that has actions to seek arbitrage"""
def decide_l(self, *args):
""" Returns None for prices and sizes for bid and offer """
return [None, None, None, None]
def sizematcher(self, b_search, o_search, arbs):
""" Goes through lists from decide_t, matches sizes to make riskless profit trades"""
if not (b_search and o_search):
return arbs #recursion ends as no opportunities left
else:
x = b_search[0][2] - o_search[0][2] #check size difference in first item of each list
if x == 0:
arbs.append(b_search[0])
arbs.append(o_search[0])
b_search.pop(0)
o_search.pop(0)
return self.sizematcher(b_search, o_search, arbs)
elif x > 0:
residual = x
price, tradetype, size, cp, cptradetype, cpobject = b_search[0]
partialfill = [price, tradetype, size - residual, cp, cptradetype, cpobject]
arbs.append(partialfill)
arbs.append(o_search[0])
b_search[0][2] = residual #edit existing item to reflect only residual size remains
o_search.pop(0)
return self.sizematcher(b_search, o_search, arbs)
elif x < 0:
residual = -x
price, tradetype, size, cp, cptradetype, cpobject = o_search[0]
partialfill = [price, tradetype, size - residual, cp, cptradetype, cpobject]
arbs.append(b_search[0])
arbs.append(partialfill)
o_search[0][2] = -residual # edit existing item to reflect stub has been traded
b_search.pop(0)
return self.sizematcher(b_search, o_search, arbs)
def decide_t(self, traders, *args):
"""Look through market bids and offers and find lists of trades that allow riskless profit"""
# first go through all existing bids and add them to a list as tuples
b_compile = [[key, value['bidpx'], value['bidsize']] for key, value in market.items()
if value['bidpx']]
max_bprice = max([i[1] for i in b_compile])
# then go through all offers and if any lower than the highest bid, add them to a list as tuples
o_search = [[value['offerpx'], 'bid', value['offersize'], key, 'offer', traders[key]]
for key, value in market.items()
if value['offersize'] and value['offerpx'] < max_bprice]
if o_search:
min_oprice = min([i[0] for i in o_search])
b_search = [[value['bidpx'], 'offer', value['bidsize'], key, 'bid', traders[key]]
for key, value in market.items()
if value['bidsize'] and value['bidpx'] > min_oprice]
b_search.sort(key = lambda x:x[0], reverse=True) #sort by bidpx highest to lowest
o_search.sort(key = lambda x:x[0]) #sort by offer lowest to highest
return self.sizematcher(b_search, o_search, []) #run recursive function to generate trades
else:
return None #no arbs found
class Player():
def __init__(self, behaviourarray=None, name=None, position_limit=None):
self.trades = [] #record executed transactions
self.behaviours = behaviourarray
self.name = name
self.position_limit = position_limit
@property
def net_position(self):
direction = {'bid':1,'offer':-1}
return sum([i[1] * direction[i[2]] for i in self.trades])
def PnL(self, market_value):
#self.trades are format (price, size, tradetype)
direction = {'bid':1,'offer':-1}
return sum([(market_value - i[0]) * i[1] * direction[i[2]] for i in self.trades])
def submit(self, strategy=None, round=None, choice=None, perceived_mid=None, **kwargs):
"""
human players pass a "choice" into this function that depends on their input
bots pass a behaviour class which determines their 'choice'
"""
if choice == None:
choice = self.behaviours[strategy].decide_l(
self.net_position, perceived_mid, self.position_limit, round
)
else:
pass
bid, bidsize, offer, offersize = choice
market[self.name] = {'bidpx':bid, 'bidsize':bidsize, 'offerpx':offer, 'offersize':offersize}
def cleanup_listing(self):
""" After trading, cleans up own market listing to prevent potential limit breaches """
for s, d in [('bidsize', 1), ('offersize', -1)]:
if market[self.name][s]: #if there is an outstanding quote
potential_pos = market[self.name][s] * d + self.net_position #calculate potential position
if abs(potential_pos) > self.position_limit: #if that potential position too high
market[self.name][s] -= abs(potential_pos) - self.position_limit #truncate
else:
continue #check next quote
else:
continue #check next quote
def trade(self, strategy= None, traders=None, closedtrade=None, perceived_mid=None, round=None, **kwargs):
if closedtrade == None: #i.e. if choice wasn't passed by a player (because they're not human)
closedtrade = self.behaviours[strategy].decide_t(
traders, self.name,
perceived_mid, self.net_position
)
else:
pass
if closedtrade == None: #i.e if the bot behaviour says do nothing
return None
for i in closedtrade:
price, tradetype, size, cp, cptradetype, cpobject = i
self.trades.append((price, size, tradetype))
cpobject.trades.append((price, size, cptradetype))
if cptradetype == 'bid':
print("{} hit {}'s bid for {}!".format(self.name, cp, size))
market[cp]['bidsize'] -= size
elif cptradetype == 'offer':
market[cp]['offersize'] -= size
print("{} lifted {}'s offer for {}!".format(self.name, cp, size))
self.cleanup_listing()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment