def kernelTerminating(self): super().kernelTerminating() # If the oracle supports writing the fundamental value series for its # symbols, write them to disk. if hasattr(self.oracle, 'f_log'): for symbol in self.oracle.f_log: dfFund = pd.DataFrame(self.oracle.f_log[symbol]) if not dfFund.empty: dfFund.set_index('FundamentalTime', inplace=True) self.writeLog(dfFund, filename='fundamental_{}'.format(symbol)) log_print("Fundamental archival complete.") if self.book_freq is None: return else: # Iterate over the order books controlled by this exchange. for symbol in self.order_books: start_time = dt.datetime.now() self.logOrderBookSnapshots(symbol) end_time = dt.datetime.now() print( "Time taken to log the order book: {}".format(end_time - start_time)) print("Order book archival complete.")
def __init__(self, kernel_name, random_state=None): # kernel_name is for human readers only. self.name = kernel_name self.random_state = random_state if not random_state: raise ValueError( "A valid, seeded np.random.RandomState object is required " + "for the Kernel", self.name) sys.exit() # A single message queue to keep everything organized by increasing # delivery timestamp. self.messages = queue.PriorityQueue() # currentTime is None until after kernelStarting() event completes # for all agents. This is a pd.Timestamp that includes the date. self.currentTime = None # Timestamp at which the Kernel was created. Primarily used to # create a unique log directory for this run. Also used to # print some elapsed time and messages per second statistics. self.kernelWallClockStart = pd.Timestamp('now') # TODO: This is financial, and so probably should not be here... self.meanResultByAgentType = {} self.agentCountByType = {} # The Kernel maintains a summary log to which agents can write # information that should be centralized for very fast access # by separate statistical summary programs. Detailed event # logging should go only to the agent's individual log. This # is for things like "final position value" and such. self.summaryLog = [] log_print("Kernel initialized: {}", self.name)
def observePrice(self, symbol, currentTime, sigma_n=1000, random_state=None): # If the request is made after market close, return the close price. if currentTime >= self.mkt_close: r_t = self.advance_fundamental_value_series( self.mkt_close - pd.Timedelta('1ns'), symbol) else: r_t = self.advance_fundamental_value_series(currentTime, symbol) # Generate a noisy observation of fundamental value at the current time. if sigma_n == 0: obs = r_t else: obs = int(round(random_state.normal(loc=r_t, scale=sqrt(sigma_n)))) log_print("Oracle: current fundamental value is {} at {}", r_t, currentTime) log_print("Oracle: giving client value observation {}", obs) # Reminder: all simulator prices are specified in integer cents. return obs
def modifyOrder(self, order, new_order): # Modifies the quantity of an existing limit order in the order book if not self.isSameOrder(order, new_order): return book = self.bids if order.is_buy_order else self.asks if not book: return for i, o in enumerate(book): if self.isEqualPrice(order, o[0]): for mi, mo in enumerate(book[i]): if order.order_id == mo.order_id: book[i][0] = new_order for idx, orders in enumerate(self.history): if new_order.order_id not in orders: continue self.history[idx][new_order.order_id]['modifications'].append( (self.owner.currentTime, new_order.quantity)) log_print("MODIFIED: order {}", order) log_print("SENT: notifications of order modification to agent {} for order {}", new_order.agent_id, new_order.order_id) self.owner.sendMessage(order.agent_id, Message({"msg": "ORDER_MODIFIED", "new_order": new_order})) if order.is_buy_order: self.bids = book else: self.asks = book self.last_update_ts = self.owner.currentTime
def __init__(self, mkt_open, mkt_close, symbols): # Symbols must be a dictionary of dictionaries with outer keys as symbol names and # inner keys: r_bar, kappa, sigma_s. self.mkt_open = mkt_open self.mkt_close = mkt_close self.symbols = symbols self.f_log = {} # The dictionary r holds the most recent fundamental values for each symbol. self.r = {} # The dictionary megashocks holds the time series of megashocks for each symbol. # The last one will always be in the future (relative to the current simulation time). # # Without these, the OU process just makes a noisy return to the mean and then stays there # with relatively minor noise. Here we want them to follow a Poisson process, so we sample # from an exponential distribution for the separation intervals. self.megashocks = {} then = dt.datetime.now() # Note that each value in the self.r dictionary is a 2-tuple of the timestamp at # which the series was computed and the true fundamental value at that time. for symbol in symbols: s = symbols[symbol] log_print( "SparseMeanRevertingOracle computing initial fundamental value for {}", symbol) self.r[symbol] = (mkt_open, s['r_bar']) self.f_log[symbol] = [{ 'FundamentalTime': mkt_open, 'FundamentalValue': s['r_bar'] }] # Compute the time and value of the first megashock. Note that while the values are # mean-zero, they are intentionally bimodal (i.e. we always want to push the stock # some, but we will tend to cancel out via pushes in opposite directions). ms_time_delta = np.random.exponential(scale=1.0 / s['megashock_lambda_a']) mst = self.mkt_open + pd.Timedelta(ms_time_delta, unit='ns') msv = s['random_state'].normal(loc=s['megashock_mean'], scale=sqrt(s['megashock_var'])) msv = msv if s['random_state'].randint(2) == 0 else -msv self.megashocks[symbol] = [{ 'MegashockTime': mst, 'MegashockValue': msv }] now = dt.datetime.now() log_print("SparseMeanRevertingOracle initialized for symbols {}", symbols) log_print("SparseMeanRevertingOracle initialization took {}", now - then)
def placeOrder(self): # Called when it is time for the agent to determine a limit price and place an order. # updateEstimates() returns the agent's current total valuation for the share it # is considering to trade and whether it will buy or sell that share. v, buy = self.updateEstimates() # Select a requested surplus for this trade. R = self.random_state.randint(self.R_min, self.R_max + 1) # Determine the limit price. p = v # - R if buy else v + R """ if self.counter % 50 == 0: print("\nZI current limit price: ", p, "\n") self.counter += 1 """ # Either place the constructed order, or if the agent could secure (eta * R) surplus # immediately by taking the inside bid/ask, do that instead. bid, bid_vol, ask, ask_vol = self.getKnownBidAsk(self.symbol) if buy and ask_vol > 0: R_ask = v - ask if R_ask >= (self.eta * R): log_print( "{} desired R = {}, but took R = {} at ask = {} due to eta", self.name, R, R_ask, ask) p = ask else: log_print("{} demands R = {}, limit price {}", self.name, R, p) elif (not buy) and bid_vol > 0: R_bid = bid - v if R_bid >= (self.eta * R): log_print( "{} desired R = {}, but took R = {} at bid = {} due to eta", self.name, R, R_bid, bid) p = bid else: log_print("{} demands R = {}, limit price {}", self.name, R, p) # determine order size using the getOrderSize function self.getOrderSize() # Place the order. self.placeLimitOrder(self.symbol, self.order_size, buy, p)
def handleOrderExecution(self, currentTime, msg): executed_order = msg.body['order'] self.executed_orders.append(executed_order) executed_qty = sum(executed_order.quantity for executed_order in self.executed_orders) self.rem_quantity = self.quantity - executed_qty log_print( f'[---- {self.name} - {currentTime} ----]: LIMIT ORDER EXECUTED - {executed_order.quantity} @ {executed_order.fill_price}' ) log_print( f'[---- {self.name} - {currentTime} ----]: EXECUTED QUANTITY: {executed_qty}' ) log_print( f'[---- {self.name} - {currentTime} ----]: REMAINING QUANTITY (NOT EXECUTED): {self.rem_quantity}' ) log_print( f'[---- {self.name} - {currentTime} ----]: % EXECUTED: {round((1 - self.rem_quantity / self.quantity) * 100, 2)} \n' )
def getPriceAtTime(self, symbol, query_time): """ Get the true price of a symbol at the requested time. :param symbol: which symbol to query :type symbol: str :param time: at this time :type time: pd.Timestamp """ log_print("Oracle: client requested {} as of {}", symbol, query_time) fundamental_series = self.fundamentals[symbol] time_of_query = pd.Timestamp(query_time) series_open_time = fundamental_series.index[0] series_close_time = fundamental_series.index[-1] if time_of_query < series_open_time: # time queried before open return fundamental_series[0] elif time_of_query > series_close_time: # time queried after close return fundamental_series[-1] else: # time queried during trading # find indices either side of requested time lower_idx = bisect_left(fundamental_series.index, time_of_query) - 1 upper_idx = lower_idx + 1 if lower_idx < len( fundamental_series.index) - 1 else lower_idx # interpolate between values lower_val = fundamental_series[lower_idx] upper_val = fundamental_series[upper_idx] log_print( f"DEBUG: lower_idx: {lower_idx}, lower_val: {lower_val}, upper_idx: {upper_idx}, upper_val: {upper_val}" ) interpolated_price = self.getInterpolatedPrice( query_time, fundamental_series.index[lower_idx], fundamental_series.index[upper_idx], lower_val, upper_val) log_print( "Oracle: latest historical trade was {} at {}. Next historical trade is {}. " "Interpolated price is {}", lower_val, query_time, upper_val, interpolated_price) self.f_log[symbol].append({ 'FundamentalTime': query_time, 'FundamentalValue': interpolated_price }) return interpolated_price
def load_fundamentals(self): """ Method extracts fundamentals for each symbol into DataFrames. Note that input files must be of the form generated by util/formatting/mid_price_from_orderbook.py. """ fundamentals = dict() log_print("Oracle: loading fundamental price series...") for symbol, params_dict in self.symbols.items(): fundamental_file_path = params_dict['fundamental_file_path'] log_print("Oracle: loading {}", fundamental_file_path) fundamental_df = pd.read_pickle(fundamental_file_path) fundamentals.update({symbol: fundamental_df}) log_print("Oracle: loading fundamental price series complete!") return fundamentals
def __init__(self, mkt_open, mkt_close, symbols): # Symbols must be a dictionary of dictionaries with outer keys as symbol names and # inner keys: r_bar, kappa, sigma_s. self.mkt_open = mkt_open self.mkt_close = mkt_close self.symbols = symbols # The dictionary r holds the fundamenal value series for each symbol. self.r = {} then = dt.datetime.now() # divide symbols upon their type stock_symbols = [] etf_symbols = [] for s in symbols: if symbols[s]["type"] == util.SymbolType.Stock: stock_symbols += [s] elif symbols[s]["type"] == util.SymbolType.ETF: etf_symbols += [s] else: raise NameError('Type ' + str(symbols[s]["type"]) + " is unkwonw") for symbol in stock_symbols: s = symbols[symbol] r_bar, kappa, sigma_s = s["r_bar"], s["kappa"], s["sigma_s"] log_print( "MeanRevertingOracle computing fundamental value series for {}", symbol) self.r[symbol] = self.generate_fundamental_value_series( symbol, r_bar, kappa, sigma_s) for etf_symbol in etf_symbols: underline_symbols = symbols[etf_symbol]["portfolio"] out_fvalue = None for un_sym in underline_symbols: out_fvalue = np.asarray( self.r[un_sym] ) if out_fvalue is None else out_fvalue + self.r[un_sym] self.r[etf_symbol] = out_fvalue #for s in symbols: # print("Fondamnetal ", s, self.r[s]) now = dt.datetime.now() log_print("MeanRevertingOracle initialized for symbols {}", symbols) log_print("MeanRevertingOracle initialization took {}", now - then)
def generate_schedule(self): if self.volume_profile_path is None: volume_profile = VWAPExecutionAgent.synthetic_volume_profile(self.start_time, self.freq) else: volume_profile = pd.read_pickle(self.volume_profile_path).to_dict() schedule = {} bins = pd.interval_range(start=self.start_time, end=self.end_time, freq=self.freq) for b in bins: schedule[b] = round(volume_profile[b.left] * self.quantity) log_print(f'[---- {self.name} - Schedule ----]:') log_print(f'[---- {self.name} - Total Number of Orders ----]: {len(schedule)}') for t, q in schedule.items(): log_print(f"Start: {t.left.time()}, End: {t.right.time()}, Quantity: {q}") return schedule
def generate_schedule(self): if self.volume_profile_path is None: volume_profile = VWAPExecutionAgent.synthetic_volume_profile(self.start_time, self.freq) else: volume_profile = pd.read_pickle(self.volume_profile_path).to_dict() schedule = {} bins = pd.interval_range(start=self.start_time, end=self.end_time, freq=self.freq) for b in bins: schedule[b] = round(volume_profile[b.left] * self.quantity) log_print('[---- {} {} - Schedule ----]:'.format(self.name, self.currentTime)) log_print('[---- {} {} - Total Number of Orders ----]: {}'.format(self.name, self.currentTime, len(schedule))) for t, q in schedule.items(): log_print("From: {}, To: {}, Quantity: {}".format(t.left.time(), t.right.time(), q)) return schedule
def generate_schedule(self): schedule = {} bins = pd.interval_range(start=self.start_time, end=self.end_time, freq=self.freq) child_quantity = int(self.quantity / len(self.execution_time_horizon)) for b in bins: schedule[b] = child_quantity log_print('[---- {} {} - Schedule ----]:'.format( self.name, self.currentTime)) log_print('[---- {} {} - Total Number of Orders ----]: {}'.format( self.name, self.currentTime, len(schedule))) for t, q in schedule.items(): log_print("From: {}, To: {}, Quantity: {}".format( t.left.time(), t.right.time(), q)) return schedule
def generate_schedule(self): schedule = {} bins = pd.interval_range(start=self.start_time, end=self.end_time, freq=self.freq) child_quantity = int(self.quantity / len(self.execution_time_horizon)) for b in bins: schedule[b] = child_quantity log_print(f'[---- {self.name} - Schedule ----]:') log_print( f'[---- {self.name} - Total Number of Orders ----]: {len(schedule)}' ) for t, q in schedule.items(): log_print( f"From: {t.left.time()}, To: {t.right.time()}, Quantity: {q}") return schedule
def placeOrders(self, mid): """ Given a mid-price, compute new orders that need to be placed, then send the orders to the Exchange. :param mid: mid-price :type mid: int """ bid_orders, ask_orders = self.computeOrdersToPlace(mid) if self.backstop_quantity is not None: bid_price = bid_orders[0] log_print('{}: Placing BUY limit order of size {} @ price {}', self.name, self.backstop_quantity, bid_price) self.placeLimitOrder(self.symbol, self.backstop_quantity, True, bid_price) bid_orders = bid_orders[1:] ask_price = ask_orders[-1] log_print('{}: Placing SELL limit order of size {} @ price {}', self.name, self.backstop_quantity, ask_price) self.placeLimitOrder(self.symbol, self.backstop_quantity, False, ask_price) ask_orders = ask_orders[:-1] for bid_price in bid_orders: log_print('{}: Placing BUY limit order of size {} @ price {}', self.name, self.buy_order_size, bid_price) self.placeLimitOrder(self.symbol, self.buy_order_size, True, bid_price) for ask_price in ask_orders: log_print('{}: Placing SELL limit order of size {} @ price {}', self.name, self.sell_order_size, ask_price) self.placeLimitOrder(self.symbol, self.sell_order_size, False, ask_price)
def kernelStopping(self): # Always call parent method to be safe. super().kernelStopping() # Print end of day valuation. H = int(round(self.getHoldings(self.symbol), -2) / 100) # May request real fundamental value from oracle as part of final cleanup/stats. if self.symbol != 'ETF': rT = self.oracle.observePrice(self.symbol, self.currentTime, sigma_n=0, random_state=self.random_state) else: portfolio_rT, rT = self.oracle.observePortfolioPrice( self.symbol, self.portfolio, self.currentTime, sigma_n=0, random_state=self.random_state) # Start with surplus as private valuation of shares held. if H > 0: surplus = sum( [self.theta[x + self.q_max - 1] for x in range(1, H + 1)]) elif H < 0: surplus = -sum( [self.theta[x + self.q_max - 1] for x in range(H + 1, 1)]) else: surplus = 0 log_print("surplus init: {}", surplus) # Add final (real) fundamental value times shares held. surplus += rT * H log_print("surplus after holdings: {}", surplus) # Add ending cash value and subtract starting cash value. surplus += self.holdings['CASH'] - self.starting_cash self.logEvent('FINAL_VALUATION', surplus, True) log_print( "{} final report. Holdings {}, end cash {}, start cash {}, final fundamental {}, preferences {}, surplus {}", self.name, H, self.holdings['CASH'], self.starting_cash, rT, self.theta, surplus)
def receiveMessage(self, currentTime, msg): super().receiveMessage(currentTime, msg) if msg.body['msg'] == 'ORDER_EXECUTED': self.handleOrderExecution(currentTime, msg) elif msg.body['msg'] == 'ORDER_ACCEPTED': self.handleOrderAcceptance(currentTime, msg) if currentTime > self.end_time: log_print( f'[---- {self.name} - {currentTime} ----]: current time {currentTime} is after specified end time of POV order ' f'{self.end_time}. TRADING CONCLUDED. ') return if self.rem_quantity > 0 and \ self.state == 'AWAITING_TRANSACTED_VOLUME' \ and msg.body['msg'] == 'QUERY_TRANSACTED_VOLUME' \ and self.transacted_volume[self.symbol] is not None\ and currentTime > self.start_time: qty = round(self.pov * self.transacted_volume[self.symbol]) self.cancelOrders() self.placeMarketOrder(self.symbol, qty, self.direction == 'BUY') log_print(f'[---- {self.name} - {currentTime} ----]: TOTAL TRANSACTED VOLUME IN THE LAST {self.look_back_period} = {self.transacted_volume[self.symbol]}') log_print(f'[---- {self.name} - {currentTime} ----]: MARKET ORDER PLACED - {qty}')
def orderExecuted(self, order): log_print("Received notification of execution for: {}", order) # Log this activity. if self.log_orders: self.logEvent('ORDER_EXECUTED', js.dump(order, strip_privates=True)) # At the very least, we must update CASH and holdings at execution time. qty = order.quantity if order.is_buy_order else -1 * order.quantity sym = order.symbol if sym in self.holdings: self.holdings[sym] += qty else: self.holdings[sym] = qty if self.holdings[sym] == 0: del self.holdings[sym] # As with everything else, CASH holdings are in CENTS. self.holdings['CASH'] -= (qty * order.fill_price) # If this original order is now fully executed, remove it from the open orders list. # Otherwise, decrement by the quantity filled just now. It is _possible_ that due # to timing issues, it might not be in the order list (i.e. we issued a cancellation # but it was executed first, or something). if order.order_id in self.orders: o = self.orders[order.order_id] if order.quantity >= o.quantity: del self.orders[order.order_id] else: o.quantity -= order.quantity else: log_print("Execution received for order not in orders list: {}", order) log_print("After execution, agent open orders: {}", self.orders) # After execution, log holdings. self.logEvent('HOLDINGS_UPDATED', self.holdings)
def load_fundamentals(self): """ Method extracts fundamentals for each symbol into DataFrames. Note that input files must be of the form generated by util/formatting/mid_price_from_orderbook.py. """ fundamentals = dict() log_print("Oracle: loading fundamental price series...") """ for symbol, params_dict in self.symbols.items(): fundamental_file_path = params_dict["\\..\\..\\data\\IBM.bz2"] log_print("Oracle: loading {}", fundamental_file_path) fundamental_df = pd.read_pickle(fundamental_file_path) fundamentals.update({symbol: fundamental_df}) """ dirname = os.path.dirname(__file__) fundamental_file_path = os.path.join(dirname, "../../data//BTC.bz2") log_print("Oracle: loading {}", fundamental_file_path) fundamental_df = pd.read_pickle(fundamental_file_path) fundamentals.update({"BTC": fundamental_df}) log_print("Oracle: loading fundamental price series complete!") return fundamentals
def handleMarketOrder(self, order): if order.symbol != self.symbol: log_print( "{} order discarded. Does not match OrderBook symbol: {}", order.symbol, self.symbol) return if (order.quantity <= 0) or (int(order.quantity) != order.quantity): log_print( "{} order discarded. Quantity ({}) must be a positive integer.", order.symbol, order.quantity) return orderbook_side = self.getInsideAsks( ) if order.is_buy_order else self.getInsideBids() limit_orders = { } # limit orders to be placed (key=price, value=quantity) order_quantity = order.quantity for price_level in orderbook_side: price, size = price_level[0], price_level[1] if order_quantity <= size: limit_orders[ price] = order_quantity #i.e. the top of the book has enough volume for the full order break else: limit_orders[ price] = size # i.e. not enough liquidity at the top of the book for the full order # therefore walk through the book until all the quantities are matched order_quantity -= size continue log_print("{} placing market order as multiple limit orders", order.symbol, order.quantity) for lo in limit_orders.items(): p, q = lo[0], lo[1] limit_order = LimitOrder(order.agent_id, order.time_placed, order.symbol, q, order.is_buy_order, p) self.handleLimitOrder(limit_order)
def wakeup (self, currentTime): # Allow the base Agent to do whatever it needs to. super().wakeup(currentTime) # This agent only needs one wakeup call at simulation start. At this time, # each client agent will send a number to each agent in its peer list. # Each number will be sampled independently. That is, client agent 1 will # send n2 to agent 2, n3 to agent 3, and so forth. # Once a client agent has received these initial random numbers from all # agents in the peer list, it will make its first request from the sum # service. Afterwards, it will simply request new sums when answers are # delivered to previous queries. # At the first wakeup, initiate peer exchange. if not self.peer_exchange_complete: n = [self.random_state.randint(low = 0, high = 100) for i in range(len(self.peer_list))] log_print ("agent {} peer list: {}", self.id, self.peer_list) log_print ("agent {} numbers to exchange: {}", self.id, n) for idx, peer in enumerate(self.peer_list): self.sendMessage(peer, Message({ "msg" : "PEER_EXCHANGE", "sender": self.id, "n" : n[idx] })) else: # For subsequent (self-induced) wakeups, place a sum query. n1, n2 = [self.random_state.randint(low = 0, high = 100) for i in range(2)] log_print ("agent {} transmitting numbers {} and {} with peer sum {}", self.id, n1, n2, self.peer_sum) # Add the sum of the peer exchange values to both numbers. n1 += self.peer_sum n2 += self.peer_sum self.sendMessage(self.serviceAgentID, Message({ "msg" : "SUM_QUERY", "sender": self.id, "n1" : n1, "n2" : n2 })) return
def __init__(self, mkt_open, mkt_close, symbols): # Symbols must be a dictionary of dictionaries with outer keys as symbol names and # inner keys: r_bar, kappa, sigma_s. self.mkt_open = mkt_open self.mkt_close = mkt_close self.symbols = symbols # The dictionary r holds the fundamenal value series for each symbol. self.r = {} then = dt.datetime.now() for symbol in symbols: s = symbols[symbol] log_print( "MeanRevertingOracle computing fundamental value series for {}", symbol) self.r[symbol] = self.generate_fundamental_value_series( symbol=symbol, **s) now = dt.datetime.now() log_print("MeanRevertingOracle initialized for symbols {}", symbols) log_print("MeanRevertingOracle initialization took {}", now - then)
def orderAccepted(self, order): log_print("Received notification of acceptance for: {}", order) # Log this activity. if self.log_orders: self.logEvent('ORDER_ACCEPTED', js.dump(order))
def receiveMessage(self, currentTime, msg): super().receiveMessage(currentTime, msg) # Do we know the market hours? had_mkt_hours = self.mkt_open is not None and self.mkt_close is not None # Record market open or close times. if msg.body['msg'] == "WHEN_MKT_OPEN": self.mkt_open = msg.body['data'] log_print("Recorded market open: {}", self.kernel.fmtTime(self.mkt_open)) elif msg.body['msg'] == "WHEN_MKT_CLOSE": self.mkt_close = msg.body['data'] log_print("Recorded market close: {}", self.kernel.fmtTime(self.mkt_close)) elif msg.body['msg'] == "ORDER_EXECUTED": # Call the orderExecuted method, which subclasses should extend. This parent # class could implement default "portfolio tracking" or "returns tracking" # behavior. order = msg.body['order'] self.orderExecuted(order) elif msg.body['msg'] == "ORDER_ACCEPTED": # Call the orderAccepted method, which subclasses should extend. order = msg.body['order'] self.orderAccepted(order) elif msg.body['msg'] == "ORDER_CANCELLED": # Call the orderCancelled method, which subclasses should extend. order = msg.body['order'] self.orderCancelled(order) elif msg.body['msg'] == "MKT_CLOSED": # We've tried to ask the exchange for something after it closed. Remember this # so we stop asking for things that can't happen. self.marketClosed() elif msg.body['msg'] == 'QUERY_LAST_TRADE': # Call the queryLastTrade method, which subclasses may extend. # Also note if the market is closed. if msg.body['mkt_closed']: self.mkt_closed = True self.queryLastTrade(msg.body['symbol'], msg.body['data']) elif msg.body['msg'] == 'QUERY_SPREAD': # Call the querySpread method, which subclasses may extend. # Also note if the market is closed. if msg.body['mkt_closed']: self.mkt_closed = True self.querySpread(msg.body['symbol'], msg.body['data'], msg.body['bids'], msg.body['asks'], msg.body['book']) elif msg.body['msg'] == 'QUERY_ORDER_STREAM': # Call the queryOrderStream method, which subclasses may extend. # Also note if the market is closed. if msg.body['mkt_closed']: self.mkt_closed = True self.queryOrderStream(msg.body['symbol'], msg.body['orders']) # Now do we know the market hours? have_mkt_hours = self.mkt_open is not None and self.mkt_close is not None # Once we know the market open and close times, schedule a wakeup call for market open. # Only do this once, when we first have both items. if have_mkt_hours and not had_mkt_hours: # Agents are asked to generate a wake offset from the market open time. We structure # this as a subclass request so each agent can supply an appropriate offset relative # to its trading frequency. ns_offset = self.getWakeFrequency() self.setWakeup(self.mkt_open + ns_offset)
def receiveMessage(self, currentTime, msg): super().receiveMessage(currentTime, msg) if msg.body['msg'] == 'MARKET_DATA': self.cancelOrders() self.last_market_data_update = currentTime bids, asks = msg.body['bids'], msg.body['asks'] bid_liq = sum(x[1] for x in bids) ask_liq = sum(x[1] for x in asks) log_print("bid, ask levels: {}", len(bids), len(asks)) log_print("bids: {}, asks: {}", bids, asks) # OBI strategy. target = 0 if bid_liq == 0 or ask_liq == 0: log_print("OBI agent inactive: zero bid or ask liquidity") return else: # bid_pct encapsulates both sides of the question, as a normalized expression # representing what fraction of total visible volume is on the buy side. bid_pct = bid_liq / (bid_liq + ask_liq) # If we are short, we need to decide if we should hold or exit. if self.is_short: # Update trailing stop. if bid_pct - self.trail_dist > self.trailing_stop: log_print( "Trailing stop updated: new > old ({:2f} > {:2f})", bid_pct - self.trail_dist, self.trailing_stop) self.trailing_stop = bid_pct - self.trail_dist else: log_print( "Trailing stop remains: potential < old ({:2f} < {:2f})", bid_pct - self.trail_dist, self.trailing_stop) # Check the trailing stop. if bid_pct < self.trailing_stop: log_print( "OBI agent exiting short position: bid_pct < trailing_stop ({:2f} < {:2f})", bid_pct, self.trailing_stop) target = 0 self.is_short = False self.trailing_stop = None else: log_print( "OBI agent holding short position: bid_pct > trailing_stop ({:2f} > {:2f})", bid_pct, self.trailing_stop) target = -100 # If we are long, we need to decide if we should hold or exit. elif self.is_long: if bid_pct + self.trail_dist < self.trailing_stop: log_print( "Trailing stop updated: new < old ({:2f} < {:2f})", bid_pct + self.trail_dist, self.trailing_stop) self.trailing_stop = bid_pct + self.trail_dist else: log_print( "Trailing stop remains: potential > old ({:2f} > {:2f})", bid_pct + self.trail_dist, self.trailing_stop) # Check the trailing stop. if bid_pct > self.trailing_stop: log_print( "OBI agent exiting long position: bid_pct > trailing_stop ({:2f} > {:2f})", bid_pct, self.trailing_stop) target = 0 self.is_long = False self.trailing_stop = None else: log_print( "OBI agent holding long position: bid_pct < trailing_stop ({:2f} < {:2f})", bid_pct, self.trailing_stop) target = 100 # If we are flat, we need to decide if we should enter (long or short). else: if bid_pct < (0.5 - self.entry_threshold): log_print( "OBI agent entering long position: bid_pct < entry_threshold ({:2f} < {:2f})", bid_pct, 0.5 - self.entry_threshold) target = 100 self.is_long = True self.trailing_stop = bid_pct + self.trail_dist log_print("Initial trailing stop: {:2f}", self.trailing_stop) elif bid_pct > (0.5 + self.entry_threshold): log_print( "OBI agent entering short position: bid_pct > entry_threshold ({:2f} > {:2f})", bid_pct, 0.5 + self.entry_threshold) target = -100 self.is_short = True self.trailing_stop = bid_pct - self.trail_dist log_print("Initial trailing stop: {:2f}", self.trailing_stop) else: log_print( "OBI agent staying flat: long_entry < bid_pct < short_entry ({:2f} < {:2f} < {:2f})", 0.5 - self.entry_threshold, bid_pct, 0.5 + self.entry_threshold) target = 0 self.plotme.append({ 'currentTime': self.currentTime, 'midpoint': (asks[0][0] + bids[0][0]) / 2, 'bid_pct': bid_pct }) # Adjust holdings to target. holdings = self.holdings[ self.symbol] if self.symbol in self.holdings else 0 delta = target - holdings direction = True if delta > 0 else False price = self.computeRequiredPrice(direction, abs(delta), bids, asks) log_print("Current holdings: {}", self.holdings) if delta == 0: log_print("No adjustments to holdings needed.") else: log_print("Adjusting holdings by {}", delta) self.placeLimitOrder(self.symbol, abs(delta), direction, price)
def runner(self, agents=[], startTime=None, stopTime=None, num_simulations=1, defaultComputationDelay=1, defaultLatency=1, agentLatency=None, latencyNoise=[1.0], agentLatencyModel=None, skip_log=False, seed=None, oracle=None, log_dir=None): # agents must be a list of agents for the simulation, # based on class agent.Agent self.agents = agents # Simulation custom state in a freeform dictionary. Allows config files # that drive multiple simulations, or require the ability to generate # special logs after simulation, to obtain needed output without special # case code in the Kernel. Per-agent state should be handled using the # provided updateAgentState() method. self.custom_state = {} # The kernel start and stop time (first and last timestamp in # the simulation, separate from anything like exchange open/close). self.startTime = startTime self.stopTime = stopTime # The global seed, NOT used for anything agent-related. self.seed = seed # Should the Kernel skip writing agent logs? self.skip_log = skip_log # The data oracle for this simulation, if needed. self.oracle = oracle # If a log directory was not specified, use the initial wallclock. if log_dir: self.log_dir = log_dir else: self.log_dir = str(int(self.kernelWallClockStart.timestamp())) # The kernel maintains a current time for each agent to allow # simulation of per-agent computation delays. The agent's time # is pushed forward (see below) each time it awakens, and it # cannot receive new messages/wakeups until the global time # reaches the agent's time. (i.e. it cannot act again while # it is still "in the future") # This also nicely enforces agents being unable to act before # the simulation startTime. self.agentCurrentTimes = [self.startTime] * len(agents) # agentComputationDelays is in nanoseconds, starts with a default # value from config, and can be changed by any agent at any time # (for itself only). It represents the time penalty applied to # an agent each time it is awakened (wakeup or recvMsg). The # penalty applies _after_ the agent acts, before it may act again. # TODO: this might someday change to pd.Timedelta objects. self.agentComputationDelays = [defaultComputationDelay] * len(agents) # If an agentLatencyModel is defined, it will be used instead of # the older, non-model-based attributes. self.agentLatencyModel = agentLatencyModel # If an agentLatencyModel is NOT defined, the older parameters: # agentLatency (or defaultLatency) and latencyNoise should be specified. # These should be considered deprecated and will be removed in the future. # If agentLatency is not defined, define it using the defaultLatency. # This matrix defines the communication delay between every pair of # agents. if agentLatency is None: self.agentLatency = [[defaultLatency] * len(agents)] * len(agents) else: self.agentLatency = agentLatency # There is a noise model for latency, intended to be a one-sided # distribution with the peak at zero. By default there is no noise # (100% chance to add zero ns extra delay). Format is a list with # list index = ns extra delay, value = probability of this delay. self.latencyNoise = latencyNoise # The kernel maintains an accumulating additional delay parameter # for the current agent. This is applied to each message sent # and upon return from wakeup/receiveMessage, in addition to the # agent's standard computation delay. However, it never carries # over to future wakeup/receiveMessage calls. It is useful for # staggering of sent messages. self.currentAgentAdditionalDelay = 0 log_print("Kernel started: {}", self.name) log_print("Simulation started!") # Note that num_simulations has not yet been really used or tested # for anything. Instead we have been running multiple simulations # with coarse parallelization from a shell script. for sim in range(num_simulations): log_print("Starting sim {}", sim) # Event notification for kernel init (agents should not try to # communicate with other agents, as order is unknown). Agents # should initialize any internal resources that may be needed # to communicate with other agents during agent.kernelStarting(). # Kernel passes self-reference for agents to retain, so they can # communicate with the kernel in the future (as it does not have # an agentID). log_print("\n--- Agent.kernelInitializing() ---") for agent in self.agents: agent.kernelInitializing(self) # Event notification for kernel start (agents may set up # communications or references to other agents, as all agents # are guaranteed to exist now). Agents should obtain references # to other agents they require for proper operation (exchanges, # brokers, subscription services...). Note that we generally # don't (and shouldn't) permit agents to get direct references # to other agents (like the exchange) as they could then bypass # the Kernel, and therefore simulation "physics" to send messages # directly and instantly or to perform disallowed direct inspection # of the other agent's state. Agents should instead obtain the # agent ID of other agents, and communicate with them only via # the Kernel. Direct references to utility objects that are not # agents are acceptable (e.g. oracles). log_print("\n--- Agent.kernelStarting() ---") for agent in self.agents: agent.kernelStarting(self.startTime) # Set the kernel to its startTime. self.currentTime = self.startTime log_print("\n--- Kernel Clock started ---") log_print("Kernel.currentTime is now {}", self.currentTime) # Start processing the Event Queue. log_print("\n--- Kernel Event Queue begins ---") log_print( "Kernel will start processing messages. Queue length: {}", len(self.messages.queue)) # Track starting wall clock time and total message count for stats at the end. eventQueueWallClockStart = pd.Timestamp('now') ttl_messages = 0 # Process messages until there aren't any (at which point there never can # be again, because agents only "wake" in response to messages), or until # the kernel stop time is reached. while not self.messages.empty() and self.currentTime and ( self.currentTime <= self.stopTime): # Get the next message in timestamp order (delivery time) and extract it. self.currentTime, event = self.messages.get() msg_recipient, msg_type, msg = event # Periodically print the simulation time and total messages, even if muted. if ttl_messages % 100000 == 0: print( "\n--- Simulation time: {}, messages processed: {}, wallclock elapsed: {} ---\n" .format(self.fmtTime(self.currentTime), ttl_messages, pd.Timestamp('now') - eventQueueWallClockStart)) log_print("\n--- Kernel Event Queue pop ---") log_print("Kernel handling {} message for agent {} at time {}", msg_type, msg_recipient, self.fmtTime(self.currentTime)) ttl_messages += 1 # In between messages, always reset the currentAgentAdditionalDelay. self.currentAgentAdditionalDelay = 0 # Dispatch message to agent. if msg_type == MessageType.WAKEUP: # Who requested this wakeup call? agent = msg_recipient # Test to see if the agent is already in the future. If so, # delay the wakeup until the agent can act again. if self.agentCurrentTimes[agent] > self.currentTime: # Push the wakeup call back into the PQ with a new time. self.messages.put((self.agentCurrentTimes[agent], (msg_recipient, msg_type, msg))) log_print("Agent in future: wakeup requeued for {}", self.fmtTime(self.agentCurrentTimes[agent])) continue # Set agent's current time to global current time for start # of processing. self.agentCurrentTimes[agent] = self.currentTime # Wake the agent. agents[agent].wakeup(self.currentTime) # Delay the agent by its computation delay plus any transient additional delay requested. self.agentCurrentTimes[agent] += pd.Timedelta( self.agentComputationDelays[agent] + self.currentAgentAdditionalDelay) log_print( "After wakeup return, agent {} delayed from {} to {}", agent, self.fmtTime(self.currentTime), self.fmtTime(self.agentCurrentTimes[agent])) elif msg_type == MessageType.MESSAGE: # Who is receiving this message? agent = msg_recipient # Test to see if the agent is already in the future. If so, # delay the message until the agent can act again. if self.agentCurrentTimes[agent] > self.currentTime: # Push the message back into the PQ with a new time. self.messages.put((self.agentCurrentTimes[agent], (msg_recipient, msg_type, msg))) log_print("Agent in future: message requeued for {}", self.fmtTime(self.agentCurrentTimes[agent])) continue # Set agent's current time to global current time for start # of processing. self.agentCurrentTimes[agent] = self.currentTime # Deliver the message. agents[agent].receiveMessage(self.currentTime, msg) # Delay the agent by its computation delay plus any transient additional delay requested. self.agentCurrentTimes[agent] += pd.Timedelta( self.agentComputationDelays[agent] + self.currentAgentAdditionalDelay) log_print( "After receiveMessage return, agent {} delayed from {} to {}", agent, self.fmtTime(self.currentTime), self.fmtTime(self.agentCurrentTimes[agent])) else: raise ValueError("Unknown message type found in queue", "currentTime:", self.currentTime, "messageType:", self.msg.type) if self.messages.empty(): log_print("\n--- Kernel Event Queue empty ---") if self.currentTime and (self.currentTime > self.stopTime): log_print("\n--- Kernel Stop Time surpassed ---") # Record wall clock stop time and elapsed time for stats at the end. eventQueueWallClockStop = pd.Timestamp('now') eventQueueWallClockElapsed = eventQueueWallClockStop - eventQueueWallClockStart # Event notification for kernel end (agents may communicate with # other agents, as all agents are still guaranteed to exist). # Agents should not destroy resources they may need to respond # to final communications from other agents. log_print("\n--- Agent.kernelStopping() ---") for agent in agents: agent.kernelStopping() # Event notification for kernel termination (agents should not # attempt communication with other agents, as order of termination # is unknown). Agents should clean up all used resources as the # simulation program may not actually terminate if num_simulations > 1. log_print("\n--- Agent.kernelTerminating() ---") for agent in agents: agent.kernelTerminating() print( "Event Queue elapsed: {}, messages: {}, messages per second: {:0.1f}" .format( eventQueueWallClockElapsed, ttl_messages, ttl_messages / (eventQueueWallClockElapsed / (np.timedelta64(1, 's'))))) log_print("Ending sim {}", sim) # The Kernel adds a handful of custom state results for all simulations, # which configurations may use, print, log, or discard. self.custom_state[ 'kernel_event_queue_elapsed_wallclock'] = eventQueueWallClockElapsed self.custom_state['kernel_slowest_agent_finish_time'] = max( self.agentCurrentTimes) # Agents will request the Kernel to serialize their agent logs, usually # during kernelTerminating, but the Kernel must write out the summary # log itself. self.writeSummaryLog() # This should perhaps be elsewhere, as it is explicitly financial, but it # is convenient to have a quick summary of the results for now. print("Mean ending value by agent type:") for a in self.meanResultByAgentType: value = self.meanResultByAgentType[a] count = self.agentCountByType[a] print("{}: {:d}".format(a, int(round(value / count)))) print("Simulation ending!") return self.custom_state
def sendMessage(self, sender=None, recipient=None, msg=None, delay=0): # Called by an agent to send a message to another agent. The kernel # supplies its own currentTime (i.e. "now") to prevent possible # abuse by agents. The kernel will handle computational delay penalties # and/or network latency. The message must derive from the message.Message class. # The optional delay parameter represents an agent's request for ADDITIONAL # delay (beyond the Kernel's mandatory computation + latency delays) to represent # parallel pipeline processing delays (that should delay the transmission of messages # but do not make the agent "busy" and unable to respond to new messages). if sender is None: raise ValueError("sendMessage() called without valid sender ID", "sender:", sender, "recipient:", recipient, "msg:", msg) if recipient is None: raise ValueError("sendMessage() called without valid recipient ID", "sender:", sender, "recipient:", recipient, "msg:", msg) if msg is None: raise ValueError("sendMessage() called with message == None", "sender:", sender, "recipient:", recipient, "msg:", msg) # Apply the agent's current computation delay to effectively "send" the message # at the END of the agent's current computation period when it is done "thinking". # NOTE: sending multiple messages on a single wake will transmit all at the same # time, at the end of computation. To avoid this, use Agent.delay() to accumulate # a temporary delay (current cycle only) that will also stagger messages. # The optional pipeline delay parameter DOES push the send time forward, since it # represents "thinking" time before the message would be sent. We don't use this # for much yet, but it could be important later. # This means message delay (before latency) is the agent's standard computation delay # PLUS any accumulated delay for this wake cycle PLUS any one-time requested delay # for this specific message only. sentTime = self.currentTime + pd.Timedelta( self.agentComputationDelays[sender] + self.currentAgentAdditionalDelay + delay) # Apply communication delay per the agentLatencyModel, if defined, or the # agentLatency matrix [sender][recipient] otherwise. if self.agentLatencyModel is not None: latency = self.agentLatencyModel.get_latency( sender_id=sender, recipient_id=recipient) deliverAt = sentTime + pd.Timedelta(latency) log_print( "Kernel applied latency {}, accumulated delay {}, one-time delay {} on sendMessage from: {} to {}, scheduled for {}", latency, self.currentAgentAdditionalDelay, delay, self.agents[sender].name, self.agents[recipient].name, self.fmtTime(deliverAt)) else: latency = self.agentLatency[sender][recipient] noise = self.random_state.choice(len(self.latencyNoise), 1, self.latencyNoise)[0] deliverAt = sentTime + pd.Timedelta(latency + noise) log_print( "Kernel applied latency {}, noise {}, accumulated delay {}, one-time delay {} on sendMessage from: {} to {}, scheduled for {}", latency, noise, self.currentAgentAdditionalDelay, delay, self.agents[sender].name, self.agents[recipient].name, self.fmtTime(deliverAt)) # Finally drop the message in the queue with priority == delivery time. self.messages.put((deliverAt, (recipient, MessageType.MESSAGE, msg))) log_print("Sent time: {}, current time {}, computation delay {}", sentTime, self.currentTime, self.agentComputationDelays[sender]) log_print("Message queued: {}", msg)
def placeOrder(self): # Called when it is time for the agent to determine a limit price and place an order. limit_price = self.limit_price history = self.getKnownStreamHistory(self.symbol) # volume should be adjusted on the go until a particular transaction at a given process # is completed if self.buy: current_holding = self.getHoldings(self.symbol) else: current_holding = -1 * self.getHoldings(self.symbol) #prev_order_size = copy.deepcopy(self.order_size) # if all designated units have been traded if current_holding == self.total_order: """ print("\nCurrent holding is: ", current_holding, "\nOrders so far adds up to: ", self.total_order, "\nPrevious order size was: ", prev_order_size) """ # reset profit margin self.profit_margin = self.initial_profit_margin # choose new order size self.getOrderSize() # order is fully transacted, thus add new order size to the total self.total_order += self.order_size """ print("Order has been fully executed. New order size is: ", self.order_size) """ # receive new price if self.buy: self.limit_price = self.oracle.observePrice( self.symbol, self.currentTime, sigma_n=self.sigma_n, random_state=self.random_state) else: self.limit_price = self.oracle.observePrice( self.symbol, self.currentTime, sigma_n=self.sigma_n, random_state=self.random_state) # if transaction has not yet completed at this price else: self.order_size = self.total_order - current_holding """ print("\nCurrent holding is: ", current_holding, "\nOrders so far adds up to: ", self.total_order, "\nOrder has not been fully executed. New order size is: ", self.order_size, "\nPrevious order size was: ", prev_order_size) """ # Determine the limit price. shout_price = limit_price * ( 1 - self.profit_margin) if self.buy else limit_price * ( 1 + self.profit_margin) def upwards_adjustment(last_shout): # set target price self.target_price = self.rel_change_up * last_shout + self.abs_change_up # using target price and limit price, calculate widrow WH delta self.hoff_delta = self.learning_rate * (self.target_price - shout_price) # Add momentum based update self.momentum_target_price = self.momentum * self.momentum_target_price + ( 1 - self.momentum) * self.hoff_delta # update margin based on this information self.profit_margin = (shout_price + self.momentum_target_price) / limit_price - 1 return None def downwards_adjustment(last_shout): # set target price self.target_price = self.rel_change_down * last_shout + self.abs_change_down # using target price and limit price, calculate widrow WH delta self.hoff_delta = self.learning_rate * (self.target_price - shout_price) # Add momentum based update self.momentum_target_price = self.momentum * self.momentum_target_price + ( 1 - self.momentum) * self.hoff_delta # update margin based on this information self.profit_margin = (shout_price + self.momentum_target_price) / limit_price - 1 return None # Either place the constructed order, or if the agent could secure (eta * R) surplus # immediately by taking the inside bid/ask, do that instead. bid, bid_vol, ask, ask_vol = self.getKnownBidAsk(self.symbol) if self.buy and ask_vol > 0: R_ask = limit_price - ask if R_ask >= (limit_price * self.profit_margin): log_print( "{} desired R = {}, but took R = {} at ask = {} due to eta", self.name, self.profit_margin, R_ask, ask) shout_price = ask else: log_print("{} demands R = {}, limit price {}", self.name, self.profit_margin, shout_price) elif (not self.buy) and bid_vol > 0: R_bid = bid - limit_price if R_bid >= (limit_price * self.profit_margin): log_print( "{} desired R = {}, but took R = {} at bid = {} due to eta", self.name, self.profit_margin, R_bid, bid) shout_price = bid else: log_print("{} demands R = {}, limit price {}", self.name, self.profit_margin, shout_price) # Place the order. self.placeLimitOrder(self.symbol, self.order_size, self.buy, int(shout_price)) # Update margin if history.empty == False: last_shout = history.loc[ 0, 'limit_price'] # note that "limit_price" here refers to the column name. Therefore the "last_shout" is the first entry of this column # for a seller if self.buy == False: # if the transaction value is not empty - i.e. if the transaction occurred if history.loc[0, 'transactions'] != []: # if the ask price is too low, seller raises the ask price if shout_price < last_shout: upwards_adjustment(last_shout) # otherwise, if the last shout was a bid, seller lowers the ask price elif history.loc[0, 'is_buy_order'] == True: downwards_adjustment(last_shout) # if the last shout did not undergo transaction else: # if the last shout was an ask if history.loc[0, 'is_buy_order'] == False: downwards_adjustment(last_shout) # for a buyer elif self.buy == True: # if the transaction value is not empty - i.e. if the transaction occurred if history.loc[0, 'transactions'] != []: # if the bid price is too high, buyer lowers bid price if shout_price > last_shout: downwards_adjustment(last_shout) # otherwise, if the last shout was an ask, buyer raises bid price elif history.loc[0, 'is_buy_order'] == False: upwards_adjustment(last_shout) # if the last shout did not undergo transaction else: # if the last shout was a bid, buyer raises bid price if history.loc[0, 'is_buy_order'] == True: upwards_adjustment(last_shout)
def wakeup(self, currentTime): # Parent class handles discovery of exchange times and market_open wakeup call. super().wakeup(currentTime) self.state = 'INACTIVE' if not self.mkt_open or not self.mkt_close: # TradingAgent handles discovery of exchange times. return else: if not self.trading: self.trading = True # Time to start trading! log_print("{} is ready to start trading now.", self.name) # Steady state wakeup behavior starts here. # If we've been told the market has closed for the day, we will only request # final price information, then stop. if self.mkt_closed and (self.symbol in self.daily_close_price): # Market is closed and we already got the daily close price. return # Schedule a wakeup for the next time this agent should arrive at the market # (following the conclusion of its current activity cycle). # We do this early in case some of our expected message responses don't arrive. # Agents should arrive according to a Poisson process. This is equivalent to # each agent independently sampling its next arrival time from an exponential # distribution in alternate Beta formation with Beta = 1 / lambda, where lambda # is the mean arrival rate of the Poisson process. #delta_time = self.random_state.exponential(scale=1.0 / self.lambda_a) #self.setWakeup(currentTime + pd.Timedelta('{}ns'.format(int(round(delta_time))))) # If the market has closed and we haven't obtained the daily close price yet, # do that before we cease activity for the day. Don't do any other behavior # after market close. if self.mkt_closed and (not self.symbol in self.daily_close_price): self.getCurrentSpread(self.symbol) self.state = 'AWAITING_SPREAD' return # Issue cancel requests for any open orders. Don't wait for confirmation, as presently # the only reason it could fail is that the order already executed. (But requests won't # be generated for those, anyway, unless something strange has happened.) self.cancelOrders() # 20200304 Chris Cho - data_dummy added to send out different messages at each wakeup if type(self) == ZeroIntelligencePlus: # this is the msssage to query spread if self.data_dummy == 0: self.getCurrentSpread(self.symbol) self.state = 'AWAITING_SPREAD' # set wakeup to earliest time possible to query order stream self.setWakeup(currentTime + pd.Timedelta('{}ns'.format(1e9))) self.data_dummy = 1 # this is the msssage to query order stream elif self.data_dummy == 1: self.getOrderStream(self.symbol, length=20) self.state = 'AWAITING_ORDER_STREAM' # Order arrival time can be fit into exponential distribution wake_time = self.modifyWakeFrequency(currentTime, self.getWakeFrequency()) self.setWakeup(currentTime + wake_time) self.data_dummy = 0 else: self.state = 'ACTIVE'
def __init__(self, id, name, type, mkt_open_time, mkt_close_time, symbol='IBM', starting_cash=100000, sigma_n=1000, q_max=100, R_min=0, R_max=250, eta=1.0, lambda_a=0.05, log_orders=False, random_state=None): # Base class init. super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state=random_state) # determine whether the agent is a "buy" agent or a "sell" agent self.buy = bool(self.random_state.randint(0, 2)) log_print("Coin flip: this agent is a {} agent", "BUY" if self.buy else "SELL") # Store important parameters particular to the ZI agent self.symbol = symbol # symbol to trade self.sigma_n = sigma_n # observation noise variance self.q_max = q_max # max unit holdings self.R_min = R_min # min requested surplus self.R_max = R_max # max requested surplus self.eta = eta # strategic threshold self.lambda_a = lambda_a # mean arrival rate of ZI agents - this is the exp. distribution parameter to be tuned self.mkt_open_time = mkt_open_time self.mkt_close_time = mkt_close_time self.order_size = np.ceil( 70 / np.random.power(3.5) ) #order size is fixed at 100 to start - the order size needs to be tuned self.total_order = self.order_size # std of 500 should be plenty self.limit_price = 0 self.counter = 0 """ # we are not querying from an oracle right now self.limit_price = self.oracle.observePrice(self.symbol, startTime, sigma_n=self.sigma_n,random_state=self.random_state) """ # ZIP update parameters self.target_price = 0 #target price just needs to exist self.momentum_target_price = 0 #initial condition self.hoff_delta = 0 #hoff delta needs to exist self.learning_rate = 0.1 + 0.4 * np.random.rand( ) #uniform [0.1,0.5] fixed per agent self.momentum = 0.2 + 0.6 * np.random.rand( ) #uniform [0.2,0.8] fixed per agent self.abs_change_up = 10 * np.random.rand( ) #uniform [0,0.05] fixed per agent - temporarily changed to 10 cents self.rel_change_up = 1 + 0.05 * np.random.rand( ) #uniform [1,1.05] fixed per agent self.abs_change_down = -10 * np.random.rand( ) #uniform [-0.05,0] fixed per agent - temporarily changed to 10 cents self.rel_change_down = 1 - 0.05 * np.random.rand( ) #uniform [0.95,1] fixed per agent self.initial_profit_margin = 0.3 + 0.05 * np.random.rand( ) #uniform [0.3,0.35] fixed per agent self.profit_margin = self.initial_profit_margin # the agent uses this to determine which data to query from the orderbook self.data_dummy = 0 # The agent uses this to track whether it has begun its strategy or is still # handling pre-market tasks. self.trading = False # The agent begins in its "complete" state, not waiting for # any special event or condition. self.state = 'AWAITING_WAKEUP' # The agent must track its previous wake time, so it knows how many time # units have passed. self.prev_wake_time = None