Last active
April 7, 2016 00:00
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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