diff --git a/.gitignore b/.gitignore index 6b012ec0f..31a6ad94f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,8 @@ batch_output/ cache/ local/ log/ +.idea/ venv/ +.idea/ +.ipynb/ +.ipynb_checkpoints/ \ No newline at end of file diff --git a/Kernel.py b/Kernel.py index ed49183ce..9244db8dd 100644 --- a/Kernel.py +++ b/Kernel.py @@ -1,11 +1,12 @@ import numpy as np import pandas as pd -import datetime, os, queue, sys -from message.Message import Message, MessageType +import os, queue, sys +from message.Message import MessageType from util.util import log_print + class Kernel: def __init__(self, kernel_name, random_state = None): diff --git a/agent/ExchangeAgent.py b/agent/ExchangeAgent.py index 4c7c15999..ad872db72 100644 --- a/agent/ExchangeAgent.py +++ b/agent/ExchangeAgent.py @@ -7,7 +7,7 @@ from agent.FinancialAgent import FinancialAgent from message.Message import Message from util.OrderBook import OrderBook -from util.util import log_print +from util.util import log_print, delist import jsons as js import pandas as pd @@ -15,6 +15,7 @@ from copy import deepcopy + class ExchangeAgent(FinancialAgent): def __init__(self, id, name, type, mkt_open, mkt_close, symbols, book_freq='S', pipeline_delay = 40000, @@ -49,7 +50,7 @@ def __init__(self, id, name, type, mkt_open, mkt_close, symbols, book_freq='S', for symbol in symbols: self.order_books[symbol] = OrderBook(self, symbol) - + # At what frequency will we archive the order books for visualization and analysis? self.book_freq = book_freq @@ -84,69 +85,68 @@ def kernelTerminating (self): dfFund = pd.DataFrame(self.oracle.f_log[symbol]) dfFund.set_index('FundamentalTime', inplace=True) self.writeLog(dfFund, filename='fundamental_{}'.format(symbol)) + print("Fundamental archival complete.") - print ("Fundamental archival complete.") - - # Skip order book dump if requested. - if self.book_freq is None: return - - # Iterate over the order books controlled by this exchange. - for symbol in self.order_books: - book = self.order_books[symbol] - - # Log full depth quotes (price, volume) from this order book at some pre-determined frequency. - # Here we are looking at the actual log for this order book (i.e. are there snapshots to export, - # independent of the requested frequency). - if book.book_log: - - # This must already be sorted by time because it was a list of order book snapshots and time - # only increases in our simulation. BUT it can have duplicates if multiple orders happen - # in the same nanosecond. (This particularly happens if using nanoseconds as the discrete - # but fine-grained unit for more theoretic studies.) - dfLog = pd.DataFrame(book.book_log) - dfLog.set_index('QuoteTime', inplace=True) - - # With multiple quotes in a nanosecond, use the last one, then resample to the requested freq. - dfLog = dfLog[~dfLog.index.duplicated(keep='last')] - dfLog.sort_index(inplace=True) - dfLog = dfLog.resample(self.book_freq).ffill() - dfLog.sort_index(inplace=True) - - # Create a fully populated index at the desired frequency from market open to close. - # Then project the logged data into this complete index. - time_idx = pd.date_range(self.mkt_open, self.mkt_close, freq=self.book_freq, closed='right') - dfLog = dfLog.reindex(time_idx, method='ffill') - dfLog.sort_index(inplace=True) - - dfLog = dfLog.stack() - dfLog.sort_index(inplace=True) - - # Get the full range of quotes at the finest possible resolution. - quotes = sorted(dfLog.index.get_level_values(1).unique()) - min_quote = quotes[0] - max_quote = quotes[-1] - quotes = range(min_quote, max_quote+1) - - # Restructure the log to have multi-level rows of all possible pairs of time and quote - # with volume as the only column. - filledIndex = pd.MultiIndex.from_product([time_idx, quotes], names=['time','quote']) - dfLog = dfLog.reindex(filledIndex) - dfLog.fillna(0, inplace=True) - - dfLog.rename('Volume') - - df = pd.DataFrame(index=dfLog.index) - df['Volume'] = dfLog - - - # Archive the order book snapshots directly to a file named with the symbol, rather than - # to the exchange agent log. - self.writeLog(df, filename='orderbook_{}'.format(symbol)) - - print ("Order book archival complete.") - - - def receiveMessage (self, currentTime, msg): + if self.book_freq is None: + return + elif self.book_freq == 'all': # log all orderbook updates + self.logOrderBook() + else: + # Iterate over the order books controlled by this exchange. + for symbol in self.order_books: + book = self.order_books[symbol] + + # Log full depth quotes (price, volume) from this order book at some pre-determined frequency. + # Here we are looking at the actual log for this order book (i.e. are there snapshots to export, + # independent of the requested frequency). + if book.book_log: + + # This must already be sorted by time because it was a list of order book snapshots and time + # only increases in our simulation. BUT it can have duplicates if multiple orders happen + # in the same nanosecond. (This particularly happens if using nanoseconds as the discrete + # but fine-grained unit for more theoretic studies.) + dfLog = pd.DataFrame(book.book_log) + dfLog.set_index('QuoteTime', inplace=True) + + # With multiple quotes in a nanosecond, use the last one, then resample to the requested freq. + dfLog = dfLog[~dfLog.index.duplicated(keep='last')] + dfLog.sort_index(inplace=True) + dfLog = dfLog.resample(self.book_freq).ffill() + dfLog.sort_index(inplace=True) + + # Create a fully populated index at the desired frequency from market open to close. + # Then project the logged data into this complete index. + time_idx = pd.date_range(self.mkt_open, self.mkt_close, freq=self.book_freq, closed='right') + dfLog = dfLog.reindex(time_idx, method='ffill') + dfLog.sort_index(inplace=True) + + dfLog = dfLog.stack() + dfLog.sort_index(inplace=True) + + # Get the full range of quotes at the finest possible resolution. + quotes = sorted(dfLog.index.get_level_values(1).unique()) + min_quote = quotes[0] + max_quote = quotes[-1] + quotes = range(min_quote, max_quote+1) + + # Restructure the log to have multi-level rows of all possible pairs of time and quote + # with volume as the only column. + filledIndex = pd.MultiIndex.from_product([time_idx, quotes], names=['time','quote']) + dfLog = dfLog.reindex(filledIndex) + dfLog.fillna(0, inplace=True) + + dfLog.rename('Volume') + + df = pd.DataFrame(index=dfLog.index) + df['Volume'] = dfLog + + + # Archive the order book snapshots directly to a file named with the symbol, rather than + # to the exchange agent log. + self.writeLog(df, filename='orderbook_{}'.format(symbol)) + print ("Order book archival complete.") + + def receiveMessage(self, currentTime, msg): super().receiveMessage(currentTime, msg) # Unless the intent of an experiment is to examine computational issues within an Exchange, @@ -163,8 +163,8 @@ def receiveMessage (self, currentTime, msg): # Most messages after close will receive a 'MKT_CLOSED' message in response. A few things # might still be processed, like requests for final trade prices or such. if msg.body['msg'] in ['LIMIT_ORDER', 'CANCEL_ORDER']: - log_print ("{} received {}: {}", self.name, msg.body['msg'], msg.body['order']) - self.sendMessage(msg.body['sender'], Message({ "msg": "MKT_CLOSED" })) + log_print("{} received {}: {}", self.name, msg.body['msg'], msg.body['order']) + self.sendMessage(msg.body['sender'], Message({"msg": "MKT_CLOSED"})) # Don't do any further processing on these messages! return @@ -173,8 +173,8 @@ def receiveMessage (self, currentTime, msg): # final trade of the day as their "daily close" price for a symbol. pass else: - log_print ("{} received {}, discarded: market is closed.", self.name, msg.body['msg']) - self.sendMessage(msg.body['sender'], Message({ "msg": "MKT_CLOSED" })) + log_print("{} received {}, discarded: market is closed.", self.name, msg.body['msg']) + self.sendMessage(msg.body['sender'], Message({"msg": "MKT_CLOSED"})) # Don't do any further processing on these messages! return @@ -187,73 +187,78 @@ def receiveMessage (self, currentTime, msg): # Handle all message types understood by this exchange. if msg.body['msg'] == "WHEN_MKT_OPEN": - log_print ("{} received WHEN_MKT_OPEN request from agent {}", self.name, msg.body['sender']) + log_print("{} received WHEN_MKT_OPEN request from agent {}", self.name, msg.body['sender']) # The exchange is permitted to respond to requests for simple immutable data (like "what are your # hours?") instantly. This does NOT include anything that queries mutable data, like equity # quotes or trades. self.setComputationDelay(0) - self.sendMessage(msg.body['sender'], Message({ "msg": "WHEN_MKT_OPEN", "data": self.mkt_open })) + self.sendMessage(msg.body['sender'], Message({"msg": "WHEN_MKT_OPEN", "data": self.mkt_open})) elif msg.body['msg'] == "WHEN_MKT_CLOSE": - log_print ("{} received WHEN_MKT_CLOSE request from agent {}", self.name, msg.body['sender']) + log_print("{} received WHEN_MKT_CLOSE request from agent {}", self.name, msg.body['sender']) # The exchange is permitted to respond to requests for simple immutable data (like "what are your # hours?") instantly. This does NOT include anything that queries mutable data, like equity # quotes or trades. self.setComputationDelay(0) - self.sendMessage(msg.body['sender'], Message({ "msg": "WHEN_MKT_CLOSE", "data": self.mkt_close })) + self.sendMessage(msg.body['sender'], Message({"msg": "WHEN_MKT_CLOSE", "data": self.mkt_close})) elif msg.body['msg'] == "QUERY_LAST_TRADE": symbol = msg.body['symbol'] if symbol not in self.order_books: - log_print ("Last trade request discarded. Unknown symbol: {}", symbol) + log_print("Last trade request discarded. Unknown symbol: {}", symbol) else: - log_print ("{} received QUERY_LAST_TRADE ({}) request from agent {}", self.name, symbol, msg.body['sender']) + log_print("{} received QUERY_LAST_TRADE ({}) request from agent {}", self.name, symbol, msg.body['sender']) # Return the single last executed trade price (currently not volume) for the requested symbol. # This will return the average share price if multiple executions resulted from a single order. - self.sendMessage(msg.body['sender'], Message({ "msg": "QUERY_LAST_TRADE", "symbol": symbol, - "data": self.order_books[symbol].last_trade, "mkt_closed": True if currentTime > self.mkt_close else False })) + self.sendMessage(msg.body['sender'], Message({"msg": "QUERY_LAST_TRADE", "symbol": symbol, + "data": self.order_books[symbol].last_trade, + "mkt_closed": True if currentTime > self.mkt_close else False})) elif msg.body['msg'] == "QUERY_SPREAD": symbol = msg.body['symbol'] depth = msg.body['depth'] if symbol not in self.order_books: - log_print ("Bid-ask spread request discarded. Unknown symbol: {}", symbol) + log_print("Bid-ask spread request discarded. Unknown symbol: {}", symbol) else: - log_print ("{} received QUERY_SPREAD ({}:{}) request from agent {}", self.name, symbol, depth, msg.body['sender']) + log_print("{} received QUERY_SPREAD ({}:{}) request from agent {}", self.name, symbol, depth, + msg.body['sender']) # Return the requested depth on both sides of the order book for the requested symbol. # Returns price levels and aggregated volume at each level (not individual orders). - self.sendMessage(msg.body['sender'], Message({ "msg": "QUERY_SPREAD", "symbol": symbol, "depth": depth, - "bids": self.order_books[symbol].getInsideBids(depth), "asks": self.order_books[symbol].getInsideAsks(depth), - "data": self.order_books[symbol].last_trade, "mkt_closed": True if currentTime > self.mkt_close else False, - "book": '' })) - - # It is possible to also send the pretty-printed order book to the agent for logging, but forcing pretty-printing - # of a large order book is very slow, so we should only do it with good reason. We don't currently - # have a configurable option for it. - #"book": self.order_books[symbol].prettyPrint(silent=True) })) + self.sendMessage(msg.body['sender'], Message({"msg": "QUERY_SPREAD", "symbol": symbol, "depth": depth, + "bids": self.order_books[symbol].getInsideBids(depth), + "asks": self.order_books[symbol].getInsideAsks(depth), + "data": self.order_books[symbol].last_trade, + "mkt_closed": True if currentTime > self.mkt_close else False, + "book": ''})) + + # It is possible to also send the pretty-printed order book to the agent for logging, but forcing pretty-printing + # of a large order book is very slow, so we should only do it with good reason. We don't currently + # have a configurable option for it. + # "book": self.order_books[symbol].prettyPrint(silent=True) })) elif msg.body['msg'] == "QUERY_ORDER_STREAM": symbol = msg.body['symbol'] length = msg.body['length'] if symbol not in self.order_books: - log_print ("Order stream request discarded. Unknown symbol: {}", symbol) + log_print("Order stream request discarded. Unknown symbol: {}", symbol) else: - log_print ("{} received QUERY_ORDER_STREAM ({}:{}) request from agent {}", self.name, symbol, length, msg.body['sender']) - + log_print("{} received QUERY_ORDER_STREAM ({}:{}) request from agent {}", self.name, symbol, length, + msg.body['sender']) + # We return indices [1:length] inclusive because the agent will want "orders leading up to the last # L trades", and the items under index 0 are more recent than the last trade. - self.sendMessage(msg.body['sender'], Message({ "msg": "QUERY_ORDER_STREAM", "symbol": symbol, "length": length, - "mkt_closed": True if currentTime > self.mkt_close else False, - "orders": self.order_books[symbol].history[1:length+1] - })) + self.sendMessage(msg.body['sender'], Message({"msg": "QUERY_ORDER_STREAM", "symbol": symbol, "length": length, + "mkt_closed": True if currentTime > self.mkt_close else False, + "orders": self.order_books[symbol].history[1:length + 1] + })) elif msg.body['msg'] == "LIMIT_ORDER": order = msg.body['order'] - log_print ("{} received LIMIT_ORDER: {}", self.name, order) + log_print("{} received LIMIT_ORDER: {}", self.name, order) if order.symbol not in self.order_books: - log_print ("Order discarded. Unknown symbol: {}", order.symbol) + log_print("Order discarded. Unknown symbol: {}", order.symbol) else: # Hand the order to the order book for processing. self.order_books[order.symbol].handleLimitOrder(deepcopy(order)) @@ -263,9 +268,9 @@ def receiveMessage (self, currentTime, msg): # then successfully cancel, but receive the cancel confirmation first. Things to think about # for later... order = msg.body['order'] - log_print ("{} received CANCEL_ORDER: {}", self.name, order) + log_print("{} received CANCEL_ORDER: {}", self.name, order) if order.symbol not in self.order_books: - log_print ("Cancellation request discarded. Unknown symbol: {}", order.symbol) + log_print("Cancellation request discarded. Unknown symbol: {}", order.symbol) else: # Hand the order to the order book for processing. self.order_books[order.symbol].cancelOrder(deepcopy(order)) @@ -278,13 +283,12 @@ def receiveMessage (self, currentTime, msg): # happens. order = msg.body['order'] new_order = msg.body['new_order'] - log_print ("{} received MODIFY_ORDER: {}, new order: {}".format(self.name, order, new_order)) + log_print("{} received MODIFY_ORDER: {}, new order: {}".format(self.name, order, new_order)) if order.symbol not in self.order_books: - log_print ("Modification request discarded. Unknown symbol: {}".format(order.symbol)) + log_print("Modification request discarded. Unknown symbol: {}".format(order.symbol)) else: self.order_books[order.symbol].modifyOrder(deepcopy(order), deepcopy(new_order)) - def sendMessage (self, recipientID, msg): # The ExchangeAgent automatically applies appropriate parallel processing pipeline delay # to those message types which require it. @@ -301,6 +305,38 @@ def sendMessage (self, recipientID, msg): super().sendMessage(recipientID, msg) + + + # Logs the orderbook as a pandas dataframe. This method produces the orderbook with timestamps equal to the timestamps + # the orderbook was updated (tick by tick) + def logOrderBook(self): + for symbol in self.order_books: + depth = 10 + book = self.order_books[symbol] + dfLog = pd.DataFrame([book.bid_levels_price_dict, book.bid_levels_size_dict, + book.ask_levels_price_dict, book.ask_levels_size_dict]).T + dfLog.columns = ['bid_level_prices', 'bid_level_sizes', 'ask_level_prices', 'ask_level_sizes'] + cols = delist([["ask_price_{}".format(level), "ask_size_{}".format(level), "bid_price_{}".format(level), + "bid_size_{}".format(level)] for level in range(1, depth+1)]) + orderbook_df = pd.DataFrame(columns=cols, index=dfLog.index) + for index, row in orderbook_df.iterrows(): + ask_level_prices_dict = dfLog.loc[index].ask_level_prices + ask_level_sizes_dict = dfLog.loc[index].ask_level_sizes + bid_level_prices_dict = dfLog.loc[index].bid_level_prices + bid_level_sizes_dict = dfLog.loc[index].bid_level_sizes + for i in range(1, depth+1): + try: + row['ask_price_{}'.format(i)], row['ask_size_{}'.format(i)] = ask_level_prices_dict[i], \ + ask_level_sizes_dict[i] + except KeyError: + log_print("One Orderbook side empty") + try: + row['bid_price_{}'.format(i)], row['bid_size_{}'.format(i)] = bid_level_prices_dict[i], \ + bid_level_sizes_dict[i] + except KeyError: + log_print("One Orderbook side empty") + self.writeLog(orderbook_df, filename='orderbook_{}'.format(symbol)) + # Simple accessor methods for the market open and close times. def getMarketOpen(self): return self.__mkt_open diff --git a/agent/ExperimentalAgent.py b/agent/ExperimentalAgent.py deleted file mode 100644 index 923254d9c..000000000 --- a/agent/ExperimentalAgent.py +++ /dev/null @@ -1,37 +0,0 @@ -from agent.TradingAgent import TradingAgent - - -class ExperimentalAgent(TradingAgent): - - def __init__(self, id, name, symbol, - starting_cash, execution_timestamp, quantity, is_buy_order, limit_price, - log_orders = False, random_state = None): - super().__init__(id, name, starting_cash, random_state) - self.symbol = symbol - self.execution_timestamp = execution_timestamp - self.quantity = quantity - self.is_buy_order = is_buy_order - self.limit_price = limit_price - self.log_orders = log_orders - - def kernelStarting(self, startTime): - super().kernelStarting(startTime) - - def wakeup(self, currentTime): - super().wakeup(currentTime) - self.last_trade[self.symbol] = 0 - if not self.mkt_open or not self.mkt_close: - return - elif (currentTime > self.mkt_open) and (currentTime < self.mkt_close): - if currentTime == self.execution_timestamp: - self.placeLimitOrder(self.symbol, self.quantity, self.is_buy_order, self.limit_price) - if self.log_orders: self.logEvent('LIMIT_ORDER', {'agent_id': self.id, 'fill_price': None, - 'is_buy_order': self.is_buy_order, 'limit_price': self.limit_price, - 'order_id': 1, 'quantity': self.quantity, 'symbol': self.symbol, - 'time_placed': str(currentTime)}) - - def receiveMessage(self, currentTime, msg): - super().receiveMessage(currentTime, msg) - - def getWakeFrequency(self): - return self.execution_timestamp - self.mkt_open diff --git a/agent/HeuristicBeliefLearningAgent.py b/agent/HeuristicBeliefLearningAgent.py index 0194bc61d..2b4826912 100644 --- a/agent/HeuristicBeliefLearningAgent.py +++ b/agent/HeuristicBeliefLearningAgent.py @@ -9,168 +9,166 @@ np.set_printoptions(threshold=np.inf) -class HeuristicBeliefLearningAgent(ZeroIntelligenceAgent): - - def __init__(self, id, name, type, symbol='IBM', starting_cash=100000, sigma_n=1000, - r_bar=100000, kappa=0.05, sigma_s=100000, q_max=10, - sigma_pv=5000000, R_min = 0, R_max = 250, eta = 1.0, - lambda_a = 0.005, L = 8, log_orders = False, random_state = None): - - # Base class init. - super().__init__(id, name, type, symbol=symbol, starting_cash=starting_cash, sigma_n=sigma_n, - r_bar=r_bar, kappa=kappa, sigma_s=sigma_s, q_max=q_max, - sigma_pv=sigma_pv, R_min=R_min, R_max=R_max, eta=eta, - lambda_a = lambda_a, log_orders = log_orders, random_state = random_state) - - # Store important parameters particular to the HBL agent. - self.L = L # length of order book history to use (number of transactions) - - - def wakeup (self, currentTime): - # Parent class handles discovery of exchange times and market_open wakeup call. - # Also handles ZI-style "background agent" needs that are not specific to HBL. - super().wakeup(currentTime) - - # Only if the superclass leaves the state as ACTIVE should we proceed with our - # trading strategy. - if self.state != 'ACTIVE': return - - # To make trade decisions, the HBL agent requires recent order stream information. - self.getOrderStream(self.symbol, length=self.L) - self.state = 'AWAITING_STREAM' - - - - def placeOrder (self): - # Called when it is time for the agent to determine a limit price and place an order. - # This method implements the HBL strategy and falls back to the ZI (superclass) - # strategy if there is not enough information for the HBL strategy. - - # See if there is enough history for HBL. If not, we will _exactly_ perform the - # ZI placeOrder(). If so, we will use parts of ZI but compute our limit price - # differently. Note that we are not given orders more recent than the most recent - # trade. - - if len(self.stream_history[self.symbol]) < self.L: - # Not enough history for HBL. - log_print ("Insufficient history for HBL: length {}, L {}", len(self.stream_history[self.symbol]), self.L) - super().placeOrder() - return - - # There is enough history for HBL. - - # Use the superclass (ZI) method to obtain an observation, update internal estimate - # parameters, decide to buy or sell, and calculate the total unit valuation, because - # all of this logic is identical to ZI. - v, buy = self.updateEstimates() - - - # Walk through the visible order history and accumulate values needed for HBL's - # estimation of successful transaction by limit price. - low_p = sys.maxsize - high_p = 0 - - # Find the lowest and highest observed prices in the order history. - for h in self.stream_history[self.symbol]: - for id, order in h.items(): - p = order['limit_price'] - if p < low_p: low_p = p - if p > high_p: high_p = p - - # Set up the ndarray we will use for our computation. - # idx 0-7 are sa, sb, ua, ub, num, denom, Pr, Es - nd = np.zeros((high_p-low_p+1,8)) - - - # Iterate through the history and compile our observations. - for h in self.stream_history[self.symbol]: - # h follows increasing "transactions into the past", with index zero being orders - # after the most recent transaction. - for id, order in h.items(): - p = order['limit_price'] - if p < low_p: low_p = p - if p > high_p: high_p = p +class HeuristicBeliefLearningAgent(ZeroIntelligenceAgent): - # For now if there are any transactions, consider the order successful. For single - # unit orders, this is sufficient. For multi-unit orders, - # we may wish to switch to a proportion of shares executed. - if order['is_buy_order']: - if order['transactions']: nd[p-low_p,1] += 1 - else: nd[p-low_p,3] += 1 + def __init__(self, id, name, type, symbol='IBM', starting_cash=100000, sigma_n=1000, + r_bar=100000, kappa=0.05, sigma_s=100000, q_max=10, sigma_pv=5000000, R_min=0, + R_max=250, eta=1.0, lambda_a=0.005, L=8, log_orders=False, + random_state=None): + + # Base class init. + super().__init__(id, name, type, symbol=symbol, starting_cash=starting_cash, sigma_n=sigma_n, + r_bar=r_bar, kappa=kappa, sigma_s=sigma_s, q_max=q_max, sigma_pv=sigma_pv, R_min=R_min, + R_max=R_max, eta=eta, lambda_a=lambda_a, log_orders=log_orders, + random_state=random_state) + + # Store important parameters particular to the HBL agent. + self.L = L # length of order book history to use (number of transactions) + + def wakeup(self, currentTime): + # Parent class handles discovery of exchange times and market_open wakeup call. + # Also handles ZI-style "background agent" needs that are not specific to HBL. + super().wakeup(currentTime) + + # Only if the superclass leaves the state as ACTIVE should we proceed with our + # trading strategy. + if self.state != 'ACTIVE': return + + # To make trade decisions, the HBL agent requires recent order stream information. + self.getOrderStream(self.symbol, length=self.L) + self.state = 'AWAITING_STREAM' + + def placeOrder(self): + # Called when it is time for the agent to determine a limit price and place an order. + # This method implements the HBL strategy and falls back to the ZI (superclass) + # strategy if there is not enough information for the HBL strategy. + + # See if there is enough history for HBL. If not, we will _exactly_ perform the + # ZI placeOrder(). If so, we will use parts of ZI but compute our limit price + # differently. Note that we are not given orders more recent than the most recent + # trade. + + if len(self.stream_history[self.symbol]) < self.L: + # Not enough history for HBL. + log_print("Insufficient history for HBL: length {}, L {}", len(self.stream_history[self.symbol]), self.L) + super().placeOrder() + return + + # There is enough history for HBL. + + # Use the superclass (ZI) method to obtain an observation, update internal estimate + # parameters, decide to buy or sell, and calculate the total unit valuation, because + # all of this logic is identical to ZI. + v, buy = self.updateEstimates() + + # Walk through the visible order history and accumulate values needed for HBL's + # estimation of successful transaction by limit price. + low_p = sys.maxsize + high_p = 0 + + # Find the lowest and highest observed prices in the order history. + for h in self.stream_history[self.symbol]: + for id, order in h.items(): + p = order['limit_price'] + if p < low_p: low_p = p + if p > high_p: high_p = p + + # Set up the ndarray we will use for our computation. + # idx 0-7 are sa, sb, ua, ub, num, denom, Pr, Es + nd = np.zeros((high_p - low_p + 1, 8)) + + # Iterate through the history and compile our observations. + for h in self.stream_history[self.symbol]: + # h follows increasing "transactions into the past", with index zero being orders + # after the most recent transaction. + for id, order in h.items(): + p = order['limit_price'] + if p < low_p: low_p = p + if p > high_p: high_p = p + + # For now if there are any transactions, consider the order successful. For single + # unit orders, this is sufficient. For multi-unit orders, + # we may wish to switch to a proportion of shares executed. + if order['is_buy_order']: + if order['transactions']: + nd[p - low_p, 1] += 1 + else: + nd[p - low_p, 3] += 1 + else: + if order['transactions']: + nd[p - low_p, 0] += 1 + else: + nd[p - low_p, 2] += 1 + + # Compute the sums and cumulative sums required, from our observations, + # to drive the HBL's transaction probability estimates. + if buy: + nd[:, [0, 1, 2]] = np.cumsum(nd[:, [0, 1, 2]], axis=0) + nd[::-1, 3] = np.cumsum(nd[::-1, 3], axis=0) + nd[:, 4] = np.sum(nd[:, [0, 1, 2]], axis=1) else: - if order['transactions']: nd[p-low_p,0] += 1 - else: nd[p-low_p,2] += 1 - - # Compute the sums and cumulative sums required, from our observations, - # to drive the HBL's transaction probability estimates. - if buy: - nd[:,[0,1,2]] = np.cumsum(nd[:,[0,1,2]], axis=0) - nd[::-1,3] = np.cumsum(nd[::-1,3], axis=0) - nd[:,4] = np.sum(nd[:,[0,1,2]], axis=1) - else: - nd[::-1,[0,1,3]] = np.cumsum(nd[::-1,[0,1,3]], axis=0) - nd[:,2] = np.cumsum(nd[:,2], axis=0) - nd[:,4] = np.sum(nd[:,[0,1,3]], axis=1) - - nd[:,5] = np.sum(nd[:,0:4], axis=1) + nd[::-1, [0, 1, 3]] = np.cumsum(nd[::-1, [0, 1, 3]], axis=0) + nd[:, 2] = np.cumsum(nd[:, 2], axis=0) + nd[:, 4] = np.sum(nd[:, [0, 1, 3]], axis=1) - # Okay to ignore divide by zero errors here because we expect that in - # some cases (0/0 can happen) and we immediately convert the resulting - # nan to zero, which is the right answer for us. + nd[:, 5] = np.sum(nd[:, 0:4], axis=1) - # Compute probability estimates for successful transaction at all price levels. - with np.errstate(divide='ignore',invalid='ignore'): - nd[:,6] = np.nan_to_num(np.divide(nd[:,4], nd[:,5])) + # Okay to ignore divide by zero errors here because we expect that in + # some cases (0/0 can happen) and we immediately convert the resulting + # nan to zero, which is the right answer for us. - # Compute expected surplus for all price levels. - if buy: nd[:,7] = nd[:,6] * (v - np.arange(low_p,high_p+1)) - else: nd[:,7] = nd[:,6] * (np.arange(low_p,high_p+1) - v) + # Compute probability estimates for successful transaction at all price levels. + with np.errstate(divide='ignore', invalid='ignore'): + nd[:, 6] = np.nan_to_num(np.divide(nd[:, 4], nd[:, 5])) - # Extract the price and other data for the maximum expected surplus. - best_idx = np.argmax(nd[:,7]) - best_Es, best_Pr = nd[best_idx,[7,6]] - best_p = low_p + best_idx - - # If the best expected surplus is positive, go for it. - if best_Es > 0: - log_print ("Numpy: {} selects limit price {} with expected surplus {} (Pr = {:0.4f})", self.name, best_p, int(round(best_Es)), best_Pr) - - # Place the constructed order. - self.placeLimitOrder(self.symbol, 1, buy, int(round(best_p))) - else: - # Do nothing if best limit price has negative expected surplus with below code. - log_print ("Numpy: {} elects not to place an order (best expected surplus <= 0)", self.name) - - # OTHER OPTION 1: Allow negative expected surplus with below code. - #log_print ("Numpy: {} placing undesirable order (best expected surplus <= 0)", self.name) - #self.placeLimitOrder(self.symbol, 1, buy, int(round(best_p))) + # Compute expected surplus for all price levels. + if buy: + nd[:, 7] = nd[:, 6] * (v - np.arange(low_p, high_p + 1)) + else: + nd[:, 7] = nd[:, 6] * (np.arange(low_p, high_p + 1) - v) - # OTHER OPTION 2: Force fallback to ZI logic on negative surplus with below code (including return). - #log_print ("Numpy: no desirable order for {}, acting as ZI", self.name) - #super().placeOrder() + # Extract the price and other data for the maximum expected surplus. + best_idx = np.argmax(nd[:, 7]) + best_Es, best_Pr = nd[best_idx, [7, 6]] + best_p = low_p + best_idx + # If the best expected surplus is positive, go for it. + if best_Es > 0: + log_print("Numpy: {} selects limit price {} with expected surplus {} (Pr = {:0.4f})", self.name, best_p, + int(round(best_Es)), best_Pr) + # Place the constructed order. + self.placeLimitOrder(self.symbol, 100, buy, int(round(best_p))) + else: + # Do nothing if best limit price has negative expected surplus with below code. + log_print("Numpy: {} elects not to place an order (best expected surplus <= 0)", self.name) - def receiveMessage (self, currentTime, msg): + # OTHER OPTION 1: Allow negative expected surplus with below code. + # log_print ("Numpy: {} placing undesirable order (best expected surplus <= 0)", self.name) + # self.placeLimitOrder(self.symbol, 1, buy, int(round(best_p))) - # We have been awakened by something other than our scheduled wakeup. - # If our internal state indicates we were waiting for a particular event, - # check if we can transition to a new state. + # OTHER OPTION 2: Force fallback to ZI logic on negative surplus with below code (including return). + # log_print ("Numpy: no desirable order for {}, acting as ZI", self.name) + # super().placeOrder() - # Allow parent class to handle state + message combinations it understands. - super().receiveMessage(currentTime, msg) + def receiveMessage(self, currentTime, msg): - # Do our special stuff. - if self.state == 'AWAITING_STREAM': - # We were waiting to receive the recent order stream. - if msg.body['msg'] == 'QUERY_ORDER_STREAM': - # This is what we were waiting for. + # We have been awakened by something other than our scheduled wakeup. + # If our internal state indicates we were waiting for a particular event, + # check if we can transition to a new state. - # But if the market is now closed, don't advance. - if self.mkt_closed: return + # Allow parent class to handle state + message combinations it understands. + super().receiveMessage(currentTime, msg) - self.getCurrentSpread(self.symbol) - self.state = 'AWAITING_SPREAD' + # Do our special stuff. + if self.state == 'AWAITING_STREAM': + # We were waiting to receive the recent order stream. + if msg.body['msg'] == 'QUERY_ORDER_STREAM': + # This is what we were waiting for. + # But if the market is now closed, don't advance. + if self.mkt_closed: return + self.getCurrentSpread(self.symbol) + self.state = 'AWAITING_SPREAD' \ No newline at end of file diff --git a/agent/MarketReplayAgent.py b/agent/MarketReplayAgent.py deleted file mode 100644 index 3e62199d4..000000000 --- a/agent/MarketReplayAgent.py +++ /dev/null @@ -1,88 +0,0 @@ -import pandas as pd - -from agent.TradingAgent import TradingAgent -from util.order.LimitOrder import LimitOrder -from util.util import log_print - - -class MarketReplayAgent(TradingAgent): - - - def __init__(self, id, name, type, symbol, date, starting_cash, log_orders = False, random_state = None): - super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state = random_state) - self.symbol = symbol - self.date = date - self.log_orders = log_orders - self.state = 'AWAITING_WAKEUP' - - - def kernelStarting(self, startTime): - super().kernelStarting(startTime) - self.oracle = self.kernel.oracle - - def kernelStopping (self): - super().kernelStopping() - - def wakeup (self, currentTime): - self.state = 'INACTIVE' - try: - super().wakeup(currentTime) - self.last_trade[self.symbol] = self.oracle.getDailyOpenPrice(self.symbol, self.mkt_open) - if not self.mkt_open or not self.mkt_close: - return - order = self.oracle.trades_df.loc[self.oracle.trades_df.timestamp == currentTime] - wake_up_time = self.oracle.trades_df.loc[self.oracle.trades_df.timestamp > currentTime].iloc[0].timestamp - if (currentTime > self.mkt_open) and (currentTime < self.mkt_close): - self.state = 'ACTIVE' - try: - self.placeOrder(currentTime, order) - except Exception as e: - log_print(e) - self.setWakeup(wake_up_time) - except Exception as e: - log_print(str(e)) - - - def receiveMessage (self, currentTime, msg): - super().receiveMessage(currentTime, msg) - - - def placeOrder(self, currentTime, order): - if len(order) == 1: - type = order.type.item() - id = order.order_id.item() - direction = order.direction.item() - price = order.price.item() - vol = order.vol.item() - - existing_order = self.orders.get(id) - - if type == 'NEW': - self.placeLimitOrder(self.symbol, vol, direction == 'BUY', int(price), order_id=id) - elif type in ['CANCELLATION', 'PARTIAL_CANCELLATION']: - if existing_order: - if type == 'CANCELLATION': - self.cancelOrder(existing_order) - elif type == 'PARTIAL_CANCELLATION': - new_order = LimitOrder(self.id, currentTime, self.symbol, vol, direction == 'BUY', int(price), order_id=id) - self.modifyOrder(existing_order, new_order) - elif type in ['EXECUTE_VISIBLE', 'EXECUTE_HIDDEN']: - if existing_order: - if existing_order.quantity == vol: - self.cancelOrder(existing_order) - else: - new_vol = existing_order.quantity - vol - if new_vol == 0: - self.cancelOrder(existing_order) - else: - executed_order = LimitOrder(self.id, currentTime, self.symbol, new_vol, direction == 'BUY', int(price), order_id=id) - self.modifyOrder(existing_order, executed_order) - self.orders.get(id).quantity = new_vol - else: - orders = self.oracle.trades_df.loc[self.oracle.trades_df.timestamp == currentTime] - for index, order in orders.iterrows(): - self.placeOrder(currentTime, order = pd.DataFrame(order).T) - - - def getWakeFrequency(self): - return self.oracle.trades_df.iloc[0].timestamp - self.mkt_open \ No newline at end of file diff --git a/agent/MomentumAgent.py b/agent/MomentumAgent.py deleted file mode 100644 index 2821a2b1e..000000000 --- a/agent/MomentumAgent.py +++ /dev/null @@ -1,56 +0,0 @@ -from agent.TradingAgent import TradingAgent -from message.Message import Message -from util.util import print - -import numpy as np -import pandas as pd - -class MomentumAgent(TradingAgent): - - def __init__(self, id, name, symbol, startingCash, lookback): - # Base class init. - super().__init__(id, name, startingCash) - - self.symbol = symbol - self.lookback = lookback - self.state = "AWAITING_WAKEUP" - - self.freq = '1m' - self.trades = [] - - - def wakeup (self, currentTime): - can_trade = super().wakeup(currentTime) - - if not can_trade: return - - self.getLastTrade(self.symbol) - self.state = "AWAITING_LAST_TRADE" - - - def receiveMessage (self, currentTime, msg): - super().receiveMessage(currentTime, msg) - - if self.state == "AWAITING_LAST_TRADE" and msg.body['msg'] == "QUERY_LAST_TRADE": - last = self.last_trade[self.symbol] - - self.trades = (self.trades + [last])[:self.lookback] - - if len(self.trades) >= self.lookback: - - m, b = np.polyfit(range(len(self.trades)), self.trades, 1) - pred = self.lookback * m + b - - holdings = self.getHoldings(self.symbol) - - if pred > last: - self.placeLimitOrder(self.symbol, 100-holdings, True, self.MKT_BUY) - else: - self.placeLimitOrder(self.symbol, 100+holdings, False, self.MKT_SELL) - - self.setWakeup(currentTime + pd.Timedelta(self.freq)) - self.state = 'AWAITING_WAKEUP' - - def getWakeFrequency (self): - return pd.Timedelta(np.random.randint(low = 0, high = pd.Timedelta(self.freq) / np.timedelta64(1, 'ns')), unit='ns') - diff --git a/agent/RandomAgent.py b/agent/RandomAgent.py deleted file mode 100644 index acca41fc0..000000000 --- a/agent/RandomAgent.py +++ /dev/null @@ -1,43 +0,0 @@ -from agent.TradingAgent import TradingAgent -import numpy as np -import pandas as pd - - -class RandomAgent(TradingAgent): - - - def __init__(self, id, name, symbol, startingCash, - buy_price_range = [90, 105], sell_price_range = [95, 110], quantity_range = [50, 500], - random_state = None): - super().__init__(id, name, startingCash, random_state) - self.symbol = symbol - self.buy_price_range = buy_price_range - self.sell_price_range = sell_price_range - self.quantity_range = quantity_range - - - def kernelStarting(self, startTime): - super().kernelStarting(startTime) - - - def wakeup(self, currentTime): - super().wakeup(currentTime) - self.last_trade[self.symbol] = 0 - if not self.mkt_open or not self.mkt_close: - return - elif (currentTime > self.mkt_open) and (currentTime < self.mkt_close): - direction = np.random.randint(0, 2) - price = np.random.randint(self.buy_price_range[0], self.buy_price_range[1]) \ - if direction == 1 else np.random.randint(self.sell_price_range[0], self.sell_price_range[1]) - quantity = np.random.randint(self.quantity_range[0], self.quantity_range[1]) - self.placeLimitOrder(self.symbol, quantity, direction, price, dollar=False) - delta_time = self.random_state.exponential(scale=1.0 / 0.005) - self.setWakeup(currentTime + pd.Timedelta('{}ms'.format(int(round(delta_time))))) - - - def receiveMessage(self, currentTime, msg): - super().receiveMessage(currentTime, msg) - - - def getWakeFrequency(self): - return pd.Timedelta('1ms') diff --git a/agent/TradingAgent.py b/agent/TradingAgent.py index f68911903..03e6e4c5c 100644 --- a/agent/TradingAgent.py +++ b/agent/TradingAgent.py @@ -6,8 +6,6 @@ from copy import deepcopy import jsons as js -import numpy as np -import pandas as pd import sys # The TradingAgent class (via FinancialAgent, via Agent) is intended as the @@ -57,6 +55,9 @@ def __init__(self, id, name, type, random_state=None, starting_cash=100000, log_ # When a last trade price comes in after market close, the trading agent # automatically records it as the daily close price for a symbol. self.daily_close_price = {} + + self.nav_diff = 0 + self.basket_size = 0 # The agent remembers the last known bids and asks (with variable depth, # showing only aggregate volume at each price level) when it receives @@ -152,7 +153,6 @@ def wakeup (self, currentTime): # the market open and closed times, and is the market not already closed. return (self.mkt_open and self.mkt_close) and not self.mkt_closed - def receiveMessage (self, currentTime, msg): super().receiveMessage(currentTime, msg) @@ -243,7 +243,8 @@ def getLastTrade (self, symbol): # This activity is not logged. def getCurrentSpread (self, symbol, depth=1): self.sendMessage(self.exchangeID, Message({ "msg" : "QUERY_SPREAD", "sender": self.id, - "symbol" : symbol, "depth" : depth })) + "symbol" : symbol, "depth" : depth })) + # Used by any Trading Agent subclass to query the recent order stream for a symbol. def getOrderStream (self, symbol, length=1): @@ -267,12 +268,12 @@ def placeLimitOrder (self, symbol, quantity, is_buy_order, limit_price, order_id if order.symbol in new_holdings: new_holdings[order.symbol] += q else: new_holdings[order.symbol] = q - # Compute before and after at-risk capital. - at_risk = self.markToMarket(self.holdings) - self.holdings['CASH'] - new_at_risk = self.markToMarket(new_holdings) - new_holdings['CASH'] - # If at_risk is lower, always allow. Otherwise, new_at_risk must be below starting cash. if not ignore_risk: + # Compute before and after at-risk capital. + at_risk = self.markToMarket(self.holdings) - self.holdings['CASH'] + new_at_risk = self.markToMarket(new_holdings) - new_holdings['CASH'] + if (new_at_risk > at_risk) and (new_at_risk > self.starting_cash): log_print ("TradingAgent ignored limit order due to at-risk constraints: {}\n{}", order, self.fmtHoldings(self.holdings)) return @@ -440,15 +441,18 @@ def queryOrderStream (self, symbol, orders): # particular strategy. - # Extract the current known best bid and ask. This does NOT request new information. - def getKnownBidAsk (self, symbol) : - bid = self.known_bids[symbol][0][0] if self.known_bids[symbol] else None - ask = self.known_asks[symbol][0][0] if self.known_asks[symbol] else None - - bid_vol = self.known_bids[symbol][0][1] if self.known_bids[symbol] else 0 - ask_vol = self.known_asks[symbol][0][1] if self.known_asks[symbol] else 0 - - return bid, bid_vol, ask, ask_vol + # Extract the current known bid and asks. This does NOT request new information. + def getKnownBidAsk (self, symbol, best=True): + if best: + bid = self.known_bids[symbol][0][0] if self.known_bids[symbol] else None + ask = self.known_asks[symbol][0][0] if self.known_asks[symbol] else None + bid_vol = self.known_bids[symbol][0][1] if self.known_bids[symbol] else 0 + ask_vol = self.known_asks[symbol][0][1] if self.known_asks[symbol] else 0 + return bid, bid_vol, ask, ask_vol + else: + bids = self.known_bids[symbol] if self.known_bids[symbol] else None + asks = self.known_asks[symbol] if self.known_asks[symbol] else None + return bids, asks # Extract the current bid and ask liquidity within a certain proportion of the @@ -488,6 +492,8 @@ def getBookLiquidity (self, book, within): # Marks holdings to market (including cash). def markToMarket (self, holdings): cash = holdings['CASH'] + + cash += self.basket_size * self.nav_diff for symbol, shares in holdings.items(): if symbol == 'CASH': continue diff --git a/agent/ZeroIntelligenceAgent.py b/agent/ZeroIntelligenceAgent.py index bef2729ca..217155670 100644 --- a/agent/ZeroIntelligenceAgent.py +++ b/agent/ZeroIntelligenceAgent.py @@ -1,336 +1,320 @@ from agent.TradingAgent import TradingAgent -from message.Message import Message from util.util import log_print from math import sqrt import numpy as np import pandas as pd -import sys -class ZeroIntelligenceAgent(TradingAgent): - - def __init__(self, id, name, type, symbol='IBM', starting_cash=100000, sigma_n=1000, - r_bar=100000, kappa=0.05, sigma_s=100000, q_max=10, - sigma_pv=5000000, R_min = 0, R_max = 250, eta = 1.0, - lambda_a = 0.005, 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) - - # Store important parameters particular to the ZI agent. - self.symbol = symbol # symbol to trade - self.sigma_n = sigma_n # observation noise variance - self.r_bar = r_bar # true mean fundamental value - self.kappa = kappa # mean reversion parameter - self.sigma_s = sigma_s # shock variance - self.q_max = q_max # max unit holdings - self.sigma_pv = sigma_pv # private value variance - 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 - - # 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 maintains two priors: r_t and sigma_t (value and error estimates). - self.r_t = r_bar - self.sigma_t = 0 - - # The agent must track its previous wake time, so it knows how many time - # units have passed. - self.prev_wake_time = None - - # The agent has a private value for each incremental unit. - self.theta = [int(x) for x in sorted( - np.round(self.random_state.normal(loc=0, scale=sqrt(sigma_pv), size=(q_max*2))).tolist(), - reverse=True)] - - - def kernelStarting(self, startTime): - # self.kernel is set in Agent.kernelInitializing() - # self.exchangeID is set in TradingAgent.kernelStarting() - - super().kernelStarting(startTime) - - self.oracle = self.kernel.oracle - - - def kernelStopping (self): - # Always call parent method to be safe. - super().kernelStopping() - - # Print end of day valuation. - H = self.getHoldings(self.symbol) - - # May request real fundamental value from oracle as part of final cleanup/stats. - rT = self.oracle.observePrice(self.symbol, 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 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() - - - # The ZI agent doesn't try to maintain a zero position, so there is no need to exit positions - # as some "active trading" agents might. It might exit a position based on its order logic, - # but this will be as a natural consequence of its beliefs. - - - # In order to use the "strategic threshold" parameter (eta), the ZI agent needs the current - # spread (inside bid/ask quote). It would not otherwise need any trade/quote information. - - # If the calling agent is a subclass, don't initiate the strategy section of wakeup(), as it - # may want to do something different. - - if type(self) == ZeroIntelligenceAgent: - self.getCurrentSpread(self.symbol) - self.state = 'AWAITING_SPREAD' - else: - self.state = 'ACTIVE' - - - def updateEstimates (self): - # Called by a background agent that wishes to obtain a new fundamental observation, - # update its internal estimation parameters, and compute a new total valuation for the - # action it is considering. - - # The agent obtains a new noisy observation of the current fundamental value - # and uses this to update its internal estimates in a Bayesian manner. - obs_t = self.oracle.observePrice(self.symbol, self.currentTime, sigma_n = self.sigma_n, random_state = self.random_state) - log_print ("{} observed {} at {}", self.name, obs_t, self.currentTime) - - - # Flip a coin to decide if we will buy or sell a unit at this time. - q = self.getHoldings(self.symbol) - - if q >= self.q_max: - buy = False - log_print ("Long holdings limit: agent will SELL") - elif q <= -self.q_max: - buy = True - log_print ("Short holdings limit: agent will BUY") - else: - buy = bool(self.random_state.randint(0,2)) - log_print ("Coin flip: agent will {}", "BUY" if buy else "SELL") - - - # Update internal estimates of the current fundamental value and our error of same. - - # If this is our first estimate, treat the previous wake time as "market open". - if self.prev_wake_time is None: self.prev_wake_time = self.mkt_open - - # First, obtain an intermediate estimate of the fundamental value by advancing - # time from the previous wake time to the current time, performing mean - # reversion at each time step. - - # delta must be integer time steps since last wake - delta = (self.currentTime - self.prev_wake_time) / np.timedelta64(1, 'ns') - - # Update r estimate for time advancement. - r_tprime = (1 - (1 - self.kappa) ** delta) * self.r_bar - r_tprime += ((1 - self.kappa) ** delta) * self.r_t - - # Update sigma estimate for time advancement. - sigma_tprime = ((1 - self.kappa) ** (2*delta)) * self.sigma_t - sigma_tprime += ((1 - (1 - self.kappa)**(2*delta)) / (1 - (1 - self.kappa)**2)) * self.sigma_s - - # Apply the new observation, with "confidence" in the observation inversely proportional - # to the observation noise, and "confidence" in the previous estimate inversely proportional - # to the shock variance. - self.r_t = (self.sigma_n / (self.sigma_n + sigma_tprime)) * r_tprime - self.r_t += (sigma_tprime / (self.sigma_n + sigma_tprime)) * obs_t - - self.sigma_t = (self.sigma_n * self.sigma_t) / (self.sigma_n + self.sigma_t) - - # Now having a best estimate of the fundamental at time t, we can make our best estimate - # of the final fundamental (for time T) as of current time t. Delta is now the number - # of time steps remaining until the simulated exchange closes. - delta = max(0, (self.mkt_close - self.currentTime) / np.timedelta64(1, 'ns')) - - # IDEA: instead of letting agent "imagine time forward" to the end of the day, - # impose a maximum forward delta, like ten minutes or so. This could make - # them think more like traders and less like long-term investors. Add - # this line of code (keeping the max() line above) to try it. - #delta = min(delta, 1000000000 * 60 * 10) - - r_T = (1 - (1 - self.kappa) ** delta) * self.r_bar - r_T += ((1 - self.kappa) ** delta) * r_tprime - - # Our final fundamental estimate should be quantized to whole units of value. - r_T = int(round(r_T)) - - # Finally (for the final fundamental estimation section) remember the current - # time as the previous wake time. - self.prev_wake_time = self.currentTime - - log_print ("{} estimates r_T = {} as of {}", self.name, r_T, self.currentTime) - - - # Determine the agent's total valuation. - q += (self.q_max - 1) - theta = self.theta[q+1 if buy else q] - v = r_T + theta - - log_print ("{} total unit valuation is {} (theta = {})", self.name, v, theta) - - - # Return values needed to implement strategy and select limit price. - return v, buy - - - - 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 - - - # 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) - - - - # Place the order. - self.placeLimitOrder(self.symbol, 1, buy, p) - - - - def receiveMessage (self, currentTime, msg): - # Parent class schedules market open wakeup call once market open/close times are known. - super().receiveMessage(currentTime, msg) - - # We have been awakened by something other than our scheduled wakeup. - # If our internal state indicates we were waiting for a particular event, - # check if we can transition to a new state. - - if self.state == 'AWAITING_SPREAD': - # We were waiting to receive the current spread/book. Since we don't currently - # track timestamps on retained information, we rely on actually seeing a - # QUERY_SPREAD response message. - - if msg.body['msg'] == 'QUERY_SPREAD': - # This is what we were waiting for. - - # But if the market is now closed, don't advance to placing orders. - if self.mkt_closed: return +class ZeroIntelligenceAgent(TradingAgent): - # We now have the information needed to place a limit order with the eta - # strategic threshold parameter. - self.placeOrder() + def __init__(self, id, name, type, symbol='IBM', starting_cash=100000, sigma_n=1000, + r_bar=100000, kappa=0.05, sigma_s=100000, q_max=10, + sigma_pv=5000000, R_min=0, R_max=250, eta=1.0, + lambda_a=0.005, 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) + + # Store important parameters particular to the ZI agent. + self.symbol = symbol # symbol to trade + self.sigma_n = sigma_n # observation noise variance + self.r_bar = r_bar # true mean fundamental value + self.kappa = kappa # mean reversion parameter + self.sigma_s = sigma_s # shock variance + self.q_max = q_max # max unit holdings + self.sigma_pv = sigma_pv # private value variance + 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 + + # 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 maintains two priors: r_t and sigma_t (value and error estimates). + self.r_t = r_bar + self.sigma_t = 0 + + # The agent must track its previous wake time, so it knows how many time + # units have passed. + self.prev_wake_time = None + + # The agent has a private value for each incremental unit. + self.theta = [int(x) for x in sorted( + np.round(self.random_state.normal(loc=0, scale=sqrt(sigma_pv), size=(q_max * 2))).tolist(), + reverse=True)] + + def kernelStarting(self, startTime): + # self.kernel is set in Agent.kernelInitializing() + # self.exchangeID is set in TradingAgent.kernelStarting() - # Internal state and logic specific to this agent subclass. - - # Cancel all open orders. - # Return value: did we issue any cancellation requests? - def cancelOrders (self): - if not self.orders: return False - - for id, order in self.orders.items(): - self.cancelOrder(order) - - return True - - def getWakeFrequency (self): - return pd.Timedelta(self.random_state.randint(low = 0, high = 100), unit='ns') + super().kernelStarting(startTime) + + self.oracle = self.kernel.oracle + + 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 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() + + # The ZI agent doesn't try to maintain a zero position, so there is no need to exit positions + # as some "active trading" agents might. It might exit a position based on its order logic, + # but this will be as a natural consequence of its beliefs. + + # In order to use the "strategic threshold" parameter (eta), the ZI agent needs the current + # spread (inside bid/ask quote). It would not otherwise need any trade/quote information. + + # If the calling agent is a subclass, don't initiate the strategy section of wakeup(), as it + # may want to do something different. + + if type(self) == ZeroIntelligenceAgent: + self.getCurrentSpread(self.symbol) + self.state = 'AWAITING_SPREAD' + else: + self.state = 'ACTIVE' + + def updateEstimates(self): + # Called by a background agent that wishes to obtain a new fundamental observation, + # update its internal estimation parameters, and compute a new total valuation for the + # action it is considering. + + # The agent obtains a new noisy observation of the current fundamental value + # and uses this to update its internal estimates in a Bayesian manner. + obs_t = self.oracle.observePrice(self.symbol, self.currentTime, sigma_n=self.sigma_n, + random_state=self.random_state) + + log_print("{} observed {} at {}", self.name, obs_t, self.currentTime) + + # Flip a coin to decide if we will buy or sell a unit at this time. + q = int(self.getHoldings(self.symbol) / 100) # q now represents an index to how many 100 lots are held + + if q >= self.q_max: + buy = False + log_print("Long holdings limit: agent will SELL") + elif q <= -self.q_max: + buy = True + log_print("Short holdings limit: agent will BUY") + else: + buy = bool(self.random_state.randint(0, 2)) + log_print("Coin flip: agent will {}", "BUY" if buy else "SELL") + + # Update internal estimates of the current fundamental value and our error of same. + + # If this is our first estimate, treat the previous wake time as "market open". + if self.prev_wake_time is None: self.prev_wake_time = self.mkt_open + + # First, obtain an intermediate estimate of the fundamental value by advancing + # time from the previous wake time to the current time, performing mean + # reversion at each time step. + + # delta must be integer time steps since last wake + delta = (self.currentTime - self.prev_wake_time) / np.timedelta64(1, 'ns') + + # Update r estimate for time advancement. + r_tprime = (1 - (1 - self.kappa) ** delta) * self.r_bar + r_tprime += ((1 - self.kappa) ** delta) * self.r_t + + # Update sigma estimate for time advancement. + sigma_tprime = ((1 - self.kappa) ** (2 * delta)) * self.sigma_t + sigma_tprime += ((1 - (1 - self.kappa) ** (2 * delta)) / (1 - (1 - self.kappa) ** 2)) * self.sigma_s + + # Apply the new observation, with "confidence" in the observation inversely proportional + # to the observation noise, and "confidence" in the previous estimate inversely proportional + # to the shock variance. + self.r_t = (self.sigma_n / (self.sigma_n + sigma_tprime)) * r_tprime + self.r_t += (sigma_tprime / (self.sigma_n + sigma_tprime)) * obs_t + + self.sigma_t = (self.sigma_n * self.sigma_t) / (self.sigma_n + self.sigma_t) + + # Now having a best estimate of the fundamental at time t, we can make our best estimate + # of the final fundamental (for time T) as of current time t. Delta is now the number + # of time steps remaining until the simulated exchange closes. + delta = max(0, (self.mkt_close - self.currentTime) / np.timedelta64(1, 'ns')) + + # IDEA: instead of letting agent "imagine time forward" to the end of the day, + # impose a maximum forward delta, like ten minutes or so. This could make + # them think more like traders and less like long-term investors. Add + # this line of code (keeping the max() line above) to try it. + # delta = min(delta, 1000000000 * 60 * 10) + + r_T = (1 - (1 - self.kappa) ** delta) * self.r_bar + r_T += ((1 - self.kappa) ** delta) * self.r_t + + # Our final fundamental estimate should be quantized to whole units of value. + r_T = int(round(r_T)) + + # Finally (for the final fundamental estimation section) remember the current + # time as the previous wake time. + self.prev_wake_time = self.currentTime + + log_print("{} estimates r_T = {} as of {}", self.name, r_T, self.currentTime) + + # Determine the agent's total valuation. + q += (self.q_max - 1) + theta = self.theta[q + 1 if buy else q] + v = r_T + theta + + log_print("{} total unit valuation is {} (theta = {})", self.name, v, theta) + + # Return values needed to implement strategy and select limit price. + return v, buy + + 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 + + # 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) + + # Place the order. + size = 100 + self.placeLimitOrder(self.symbol, size, buy, p) + + def receiveMessage(self, currentTime, msg): + # Parent class schedules market open wakeup call once market open/close times are known. + super().receiveMessage(currentTime, msg) + # We have been awakened by something other than our scheduled wakeup. + # If our internal state indicates we were waiting for a particular event, + # check if we can transition to a new state. + + if self.state == 'AWAITING_SPREAD': + # We were waiting to receive the current spread/book. Since we don't currently + # track timestamps on retained information, we rely on actually seeing a + # QUERY_SPREAD response message. + + if msg.body['msg'] == 'QUERY_SPREAD': + # This is what we were waiting for. + + # But if the market is now closed, don't advance to placing orders. + if self.mkt_closed: return + + # We now have the information needed to place a limit order with the eta + # strategic threshold parameter. + self.placeOrder() + self.state = 'AWAITING_WAKEUP' + + # Internal state and logic specific to this agent subclass. + + # Cancel all open orders. + # Return value: did we issue any cancellation requests? + def cancelOrders(self): + if not self.orders: return False + + for id, order in self.orders.items(): + self.cancelOrder(order) + + return True + + def getWakeFrequency(self): + return pd.Timedelta(self.random_state.randint(low=0, high=100), unit='ns') diff --git a/agent/etf/EtfArbAgent.py b/agent/etf/EtfArbAgent.py new file mode 100644 index 000000000..5017c5488 --- /dev/null +++ b/agent/etf/EtfArbAgent.py @@ -0,0 +1,197 @@ +from agent.TradingAgent import TradingAgent +from util.util import log_print + +import numpy as np +import pandas as pd + + +class EtfArbAgent(TradingAgent): + + def __init__(self, id, name, type, portfolio = {}, gamma = 0, starting_cash=100000, lambda_a = 0.005, + 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) + + # Store important parameters particular to the ETF arbitrage agent. + self.inPrime = False # Determines if the agent also participates in the Primary ETF market + self.portfolio = portfolio # ETF portfolio + self.gamma = gamma # Threshold for difference between ETF and index to trade + self.messageCount = len(self.portfolio) + 1 # Tracks the number of messages sent so all limit orders are sent after + # are sent after all mid prices are calculated + #self.q_max = q_max # max unit holdings + self.lambda_a = lambda_a # mean arrival rate of ETF arb agents (eventually change to a subscription) + + # NEED TO TEST IF THERE ARE SYMBOLS IN PORTFOLIO + + # 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' + + def kernelStarting(self, startTime): + # self.kernel is set in Agent.kernelInitializing() + # self.exchangeID is set in TradingAgent.kernelStarting() + + super().kernelStarting(startTime) + + self.oracle = self.kernel.oracle + + + def kernelStopping (self): + # Always call parent method to be safe. + super().kernelStopping() + + # Print end of day valuation. + H = {} + H['ETF'] = self.getHoldings('ETF') + for i,s in enumerate(self.portfolio): + H[s] = self.getHoldings(s) + print(H) + print(self.daily_close_price) + + + 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: + 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 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 the calling agent is a subclass, don't initiate the strategy section of wakeup(), as it + # may want to do something different. + if self.mkt_closed and not self.inPrime: + for i,s in enumerate(self.portfolio): + if s not in self.daily_close_price: + self.getLastTrade(s) + self.state = 'AWAITING_LAST_TRADE' + if 'ETF' not in self.daily_close_price: + self.getLastTrade('ETF') + self.state = 'AWAITING_LAST_TRADE' + 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. + elif not self.inPrime: + delta_time = self.random_state.exponential(scale = 1.0 / self.lambda_a) + self.setWakeup(currentTime + pd.Timedelta('{}ns'.format(int(round(delta_time))))) + + # 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() + + + # The ETF arb agent DOES try to maintain a zero position, so there IS need to exit positions + # as some "active trading" agents might. It might exit a position based on its order logic, + # but this will be as a natural consequence of its beliefs... but it submits marketable orders so... + for i,s in enumerate(self.portfolio): + self.getCurrentSpread(s) + self.getCurrentSpread('ETF') + self.state = 'AWAITING_SPREAD' + + else: + self.state = 'ACTIVE' + + def getPriceEstimates(self): + index_mids = np.empty(len(self.portfolio)) + index_p = {} + empty_mid = False + for i,s in enumerate(self.portfolio): + bid, bid_vol, ask, ask_vol = self.getKnownBidAsk(s) + if bid != None and ask != None: + index_p[s] = {'bid': bid, 'ask': ask} + mid = 0.5 * (int(bid) + int(ask)) + else: + mid = float() + index_p[s] = {'bid': float(), 'ask': float()} + empty_mid = True + index_mids[i] = mid + bid, bid_vol, ask, ask_vol = self.getKnownBidAsk('ETF') + etf_p = {'bid': bid, 'ask': ask} + if bid != None and ask != None: + etf_mid = 0.5 * (int(bid) + int(ask)) + else: + etf_mid = float() + empty_mid = True + index_mid = np.sum(index_mids) + return etf_mid, index_mid, etf_p, index_p, empty_mid + + def placeOrder(self): + etf_mid, index_mid, etf_p, index_p, empty_mid = self.getPriceEstimates() + if empty_mid: + #print('no move because index or ETF was missing part of NBBO') + pass + elif (index_mid - etf_mid) > self.gamma: + self.placeLimitOrder('ETF', 1, True, etf_p['ask']) + elif (etf_mid - index_mid) > self.gamma: + self.placeLimitOrder('ETF', 1, False, etf_p['bid']) + else: + pass + #print('no move because abs(index - ETF mid) < gamma') + + def receiveMessage(self, currentTime, msg): + # Parent class schedules market open wakeup call once market open/close times are known. + super().receiveMessage(currentTime, msg) + + # We have been awakened by something other than our scheduled wakeup. + # If our internal state indicates we were waiting for a particular event, + # check if we can transition to a new state. + + if self.state == 'AWAITING_SPREAD': + # We were waiting to receive the current spread/book. Since we don't currently + # track timestamps on retained information, we rely on actually seeing a + # QUERY_SPREAD response message. + + if msg.body['msg'] == 'QUERY_SPREAD': + # This is what we were waiting for. + self.messageCount -= 1 + + # But if the market is now closed, don't advance to placing orders. + if self.mkt_closed: return + + # We now have the information needed to place a limit order with the eta + # strategic threshold parameter. + if self.messageCount == 0: + self.placeOrder() + self.messageCount = len(self.portfolio) + 1 + self.state = 'AWAITING_WAKEUP' + + + # Internal state and logic specific to this agent subclass. + + # Cancel all open orders. + # Return value: did we issue any cancellation requests? + def cancelOrders (self): + if not self.orders: return False + + for id, order in self.orders.items(): + self.cancelOrder(order) + + return True + + def getWakeFrequency (self): + return pd.Timedelta(self.random_state.randint(low = 0, high = 100), unit='ns') \ No newline at end of file diff --git a/agent/etf/EtfMarketMakerAgent.py b/agent/etf/EtfMarketMakerAgent.py new file mode 100644 index 000000000..e6d534ad2 --- /dev/null +++ b/agent/etf/EtfMarketMakerAgent.py @@ -0,0 +1,241 @@ +from agent.etf.EtfArbAgent import EtfArbAgent +from agent.etf.EtfPrimaryAgent import EtfPrimaryAgent +from message.Message import Message +from util.order.etf.BasketOrder import BasketOrder +from util.order.BasketOrder import BasketOrder +from util.util import log_print + +import pandas as pd +import sys + +class EtfMarketMakerAgent(EtfArbAgent): + + def __init__(self, id, name, type, portfolio = {}, gamma = 0, starting_cash=100000, lambda_a = 0.005, + log_orders = False, random_state = None): + + # Base class init. + super().__init__(id, name, type, portfolio = portfolio, gamma = gamma, starting_cash=starting_cash, + lambda_a = lambda_a, log_orders=log_orders, random_state = random_state) + + # NEED TO TEST IF THERE ARE SYMBOLS IN PORTFOLIO + + # Store important parameters particular to the ETF arbitrage agent. + self.inPrime = True # Determines if the agent also participates in the Primary ETF market + + # We don't yet know when the primary opens or closes. + self.prime_open = None + self.prime_close = None + + # Remember whether we have already passed the primary close time, as far + # as we know. + self.prime_closed = False + self.switched_mkt = False + + def kernelStarting(self, startTime): + # self.kernel is set in Agent.kernelInitializing() + # self.exchangeID is set in TradingAgent.kernelStarting() + + self.primeID = self.kernel.findAgentByType(EtfPrimaryAgent) + + log_print ("Agent {} requested agent of type Agent.EtfPrimaryAgent. Given Agent ID: {}", + self.id, self.primeID) + + super().kernelStarting(startTime) + + + def wakeup (self, currentTime): + # Parent class handles discovery of exchange times and market_open wakeup call. + super().wakeup(currentTime) + + # Only if the superclass leaves the state as ACTIVE should we proceed with our + # trading strategy. + if self.state != 'ACTIVE': return + + if not self.prime_open: + # Ask our primary when it opens and closes, exchange is handled in TradingAgent + self.sendMessage(self.primeID, Message({ "msg" : "WHEN_PRIME_OPEN", "sender": self.id })) + self.sendMessage(self.primeID, Message({ "msg" : "WHEN_PRIME_CLOSE", "sender": self.id })) + + + # Steady state wakeup behavior starts here. + if not self.mkt_closed and self.prime_closed: + print('The prime closed before the exchange') + sys.exit() + + elif self.mkt_closed and self.prime_closed: + return + + # If we've been told the market has closed for the day, we will only request + # final price information, then stop. + # 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. + elif self.mkt_closed and not self.prime_closed: + if self.switched_mkt and self.currentTime >= self.prime_open: + self.getEtfNav() + self.state = 'AWAITING_NAV' + elif not self.switched_mkt: + for i,s in enumerate(self.portfolio): + if s not in self.daily_close_price: + self.getLastTrade(s) + self.state = 'AWAITING_LAST_TRADE' + if 'ETF' not in self.daily_close_price: + self.getLastTrade('ETF') + self.state = 'AWAITING_LAST_TRADE' + + print('holdings before primary: ' + str(self.holdings)) + + self.setWakeup(self.prime_open) + self.switched_mkt = True + else: + self.setWakeup(self.prime_open) + 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. + else: + delta_time = self.random_state.exponential(scale = 1.0 / self.lambda_a) + self.setWakeup(currentTime + pd.Timedelta('{}ns'.format(int(round(delta_time))))) + + # 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() + + + # The ETF arb agent DOES try to maintain a zero position, so there IS need to exit positions + # as some "active trading" agents might. It might exit a position based on its order logic, + # but this will be as a natural consequence of its beliefs... but it submits marketable orders so... + + + # If the calling agent is a subclass, don't initiate the strategy section of wakeup(), as it + # may want to do something different. + # FIGURE OUT WHAT TO DO WITH MULTIPLE SPREADS... + for i,s in enumerate(self.portfolio): + self.getCurrentSpread(s) + self.getCurrentSpread('ETF') + self.state = 'AWAITING_SPREAD' + + def placeOrder(self): + etf_mid, index_mid, etf_p, index_p, empty_mid = self.getPriceEstimates() + if empty_mid: + #print('no move because index or ETF was missing part of NBBO') + pass + elif (index_mid - etf_mid) > self.gamma: + #print('buy ETF') + for i,s in enumerate(self.portfolio): + self.placeLimitOrder(s, 1, False, index_p[s]['bid']) + self.placeLimitOrder('ETF', 1, True, etf_p['ask']) + elif (etf_mid - index_mid) > self.gamma: + #print('sell ETF') + for i,s in enumerate(self.portfolio): + self.placeLimitOrder(s, 1, True, index_p[s]['ask']) + self.placeLimitOrder('ETF', 1, False, etf_p['bid']) + else: + pass + #print('no move because abs(index - ETF mid) < gamma') + + def decideBasket(self): + print(self.portfolio) + index_est = 0 + for i,s in enumerate(self.portfolio): + index_est += self.daily_close_price[s] + + H = {} + for i,s in enumerate(self.portfolio): + H[s] = self.getHoldings(s) + etf_h = self.getHoldings('ETF') + + self.nav_diff = self.nav - index_est + if self.nav_diff > 0: + if min(H.values()) > 0 and etf_h < 0: + print("send creation basket") + self.placeBasketOrder(min(H.values()), True) + else: print('wrong side for basket') + elif self.nav_diff < 0: + if etf_h > 0 and max(H.values()) < 0: + print("submit redemption basket") + self.placeBasketOrder(etf_h, False) + else: print('wrong side for basket') + else: + if min(H.values()) > 0 and etf_h < 0: + print("send creation basket") + self.placeBasketOrder(min(H.values()), True) + elif etf_h > 0 and max(H.values()) < 0: + print("submit redemption basket") + self.placeBasketOrder(etf_h, False) + + + def receiveMessage(self, currentTime, msg): + # Parent class schedules market open wakeup call once market open/close times are known. + super().receiveMessage(currentTime, msg) + + # We have been awakened by something other than our scheduled wakeup. + # If our internal state indicates we were waiting for a particular event, + # check if we can transition to a new state. + + # Record market open or close times. + if msg.body['msg'] == "WHEN_PRIME_OPEN": + self.prime_open = msg.body['data'] + + log_print ("Recorded primary open: {}", self.kernel.fmtTime(self.prime_open)) + + elif msg.body['msg'] == "WHEN_PRIME_CLOSE": + self.prime_close = msg.body['data'] + + log_print ("Recorded primary close: {}", self.kernel.fmtTime(self.prime_close)) + + if self.state == 'AWAITING_NAV': + if msg.body['msg'] == 'QUERY_NAV': + if msg.body['prime_closed']: self.prime_closed = True + self.queryEtfNav(msg.body['nav']) + + # But if the market is now closed, don't advance to placing orders. + if self.prime_closed: return + + # We now have the information needed to place a C/R basket. + self.decideBasket() + + elif self.state == 'AWAITING_BASKET': + if msg.body['msg'] == 'BASKET_EXECUTED': + order = msg.body['order'] + # We now have the information needed to place a C/R basket. + for i,s in enumerate(self.portfolio): + if order.is_buy_order: self.holdings[s] -= order.quantity + else: self.holdings[s] += order.quantity + if order.is_buy_order: + self.holdings['ETF'] += order.quantity + self.basket_size = order.quantity + else: + self.holdings['ETF'] -= order.quantity + self.basket_size = -1 * order.quantity + + self.state = 'INACTIVE' + + + # Internal state and logic specific to this agent subclass. + + # Used by any ETF Arb Agent subclass to query the Net Assest Value (NAV) of the ETF. + # This activity is not logged. + def getEtfNav (self): + self.sendMessage(self.primeID, Message({ "msg" : "QUERY_NAV", "sender": self.id })) + + # Used by ETF Arb Agent subclass to place a basket order. + # This activity is not logged. + def placeBasketOrder (self, quantity, is_create_order): + order = BasketOrder(self.id, self.currentTime, 'ETF', quantity, is_create_order) + print('BASKET ORDER PLACED: ' + str(order)) + self.sendMessage(self.primeID, Message({ "msg" : "BASKET_ORDER", "sender": self.id, + "order" : order })) + self.state = 'AWAITING_BASKET' + + # Handles QUERY NAV messages from primary + def queryEtfNav(self, nav): + self.nav = nav + log_print ("Received NAV of ETF.") \ No newline at end of file diff --git a/agent/etf/EtfPrimaryAgent.py b/agent/etf/EtfPrimaryAgent.py new file mode 100644 index 000000000..eecb12bfc --- /dev/null +++ b/agent/etf/EtfPrimaryAgent.py @@ -0,0 +1,166 @@ +# The ExchangeAgent expects a numeric agent id, printable name, agent type, timestamp to open and close trading, +# a list of equity symbols for which it should create order books, a frequency at which to archive snapshots +# of its order books, a pipeline delay (in ns) for order activity, the exchange computation delay (in ns), +# the levels of order stream history to maintain per symbol (maintains all orders that led to the last N trades), +# whether to log all order activity to the agent log, and a random state object (already seeded) to use +# for stochasticity. +from agent.FinancialAgent import FinancialAgent +from agent.ExchangeAgent import ExchangeAgent +from message.Message import Message +from util.util import log_print + +import pandas as pd +pd.set_option('display.max_rows', 500) + + +class EtfPrimaryAgent(FinancialAgent): + + def __init__(self, id, name, type, prime_open, prime_close, symbol, pipeline_delay = 40000, + computation_delay = 1, random_state = None): + + super().__init__(id, name, type, random_state) + + # Do not request repeated wakeup calls. + self.reschedule = False + + # Store this exchange's open and close times. + self.prime_open = prime_open + self.prime_close = prime_close + + self.mkt_close = None + + self.nav = 0 + self.create = 0 + self.redeem = 0 + + self.symbol = symbol + + # Right now, only the exchange agent has a parallel processing pipeline delay. This is an additional + # delay added only to order activity (placing orders, etc) and not simple inquiries (market operating + # hours, etc). + self.pipeline_delay = pipeline_delay + + # Computation delay is applied on every wakeup call or message received. + self.computation_delay = computation_delay + + def kernelStarting(self, startTime): + # Find an exchange with which we can place orders. It is guaranteed + # to exist by now (if there is one). + self.exchangeID = self.kernel.findAgentByType(ExchangeAgent) + + log_print ("Agent {} requested agent of type Agent.ExchangeAgent. Given Agent ID: {}", + self.id, self.exchangeID) + + # Request a wake-up call as in the base Agent. + super().kernelStarting(startTime) + + + def kernelStopping (self): + # Always call parent method to be safe. + super().kernelStopping() + + print ("Final C/R baskets for {}: {} creation baskets. {} redemption baskets".format(self.name, + self.create, self.redeem)) + + + # Simulation participation messages. + + def wakeup (self, currentTime): + super().wakeup(currentTime) + + if self.mkt_close is None: + # Ask our exchange when it opens and closes. + self.sendMessage(self.exchangeID, Message({ "msg" : "WHEN_MKT_CLOSE", "sender": self.id })) + + else: + # Get close price of ETF/nav + self.getLastTrade(self.symbol) + + def receiveMessage (self, currentTime, msg): + super().receiveMessage(currentTime, msg) + + # Unless the intent of an experiment is to examine computational issues within an Exchange, + # it will typically have either 1 ns delay (near instant but cannot process multiple orders + # in the same atomic time unit) or 0 ns delay (can process any number of orders, always in + # the atomic time unit in which they are received). This is separate from, and additional + # to, any parallel pipeline delay imposed for order book activity. + + # Note that computation delay MUST be updated before any calls to sendMessage. + self.setComputationDelay(self.computation_delay) + + # Is the exchange closed? (This block only affects post-close, not pre-open.) + if currentTime > self.prime_close: + # Most messages after close will receive a 'PRIME_CLOSED' message in response. + log_print ("{} received {}, discarded: prime is closed.", self.name, msg.body['msg']) + self.sendMessage(msg.body['sender'], Message({ "msg": "PRIME_CLOSED" })) + # Don't do any further processing on these messages! + return + + + if msg.body['msg'] == "WHEN_MKT_CLOSE": + self.mkt_close = msg.body['data'] + log_print ("Recorded market close: {}", self.kernel.fmtTime(self.mkt_close)) + self.setWakeup(self.mkt_close) + return + + elif msg.body['msg'] == 'QUERY_LAST_TRADE': + # Call the queryLastTrade method. + self.queryLastTrade(msg.body['symbol'], msg.body['data']) + return + + self.logEvent(msg.body['msg'], msg.body['sender']) + + # Handle all message types understood by this exchange. + if msg.body['msg'] == "WHEN_PRIME_OPEN": + log_print ("{} received WHEN_PRIME_OPEN request from agent {}", self.name, msg.body['sender']) + + # The exchange is permitted to respond to requests for simple immutable data (like "what are your + # hours?") instantly. This does NOT include anything that queries mutable data, like equity + # quotes or trades. + self.setComputationDelay(0) + + self.sendMessage(msg.body['sender'], Message({ "msg": "WHEN_PRIME_OPEN", "data": self.prime_open })) + + elif msg.body['msg'] == "WHEN_PRIME_CLOSE": + log_print ("{} received WHEN_PRIME_CLOSE request from agent {}", self.name, msg.body['sender']) + + # The exchange is permitted to respond to requests for simple immutable data (like "what are your + # hours?") instantly. This does NOT include anything that queries mutable data, like equity + # quotes or trades. + self.setComputationDelay(0) + + self.sendMessage(msg.body['sender'], Message({ "msg": "WHEN_PRIME_CLOSE", "data": self.prime_close })) + + elif msg.body['msg'] == "QUERY_NAV": + log_print ("{} received QUERY_NAV ({}) request from agent {}", self.name, msg.body['sender']) + + # Return the NAV for the requested symbol. + self.sendMessage(msg.body['sender'], Message({ "msg": "QUERY_NAV", + "nav": self.nav, "prime_closed": True if currentTime > self.prime_close else False })) + + elif msg.body['msg'] == "BASKET_ORDER": + order = msg.body['order'] + log_print ("{} received BASKET_ORDER: {}", self.name, order) + if order.is_buy_order: self.create += 1 + else: self.redeem += 1 + order.fill_price = self.nav + self.sendMessage(msg.body['sender'], Message({ "msg": "BASKET_EXECUTED", "order": order})) + + + # Handles QUERY_LAST_TRADE messages from an exchange agent. + def queryLastTrade (self, symbol, price): + self.nav = price + log_print ("Received daily close price or nav of {} for {}.", price, symbol) + + # Used by any Trading Agent subclass to query the last trade price for a symbol. + # This activity is not logged. + def getLastTrade (self, symbol): + self.sendMessage(self.exchangeID, Message({ "msg" : "QUERY_LAST_TRADE", "sender": self.id, + "symbol" : symbol })) + + # Simple accessor methods for the market open and close times. + def getPrimeOpen(self): + return self.__prime_open + + def getPrimeClose(self): + return self.__prime_close \ No newline at end of file diff --git a/agent/etf/__init__.py b/agent/etf/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/agent/ImpactAgent.py b/agent/examples/ImpactAgent.py similarity index 98% rename from agent/ImpactAgent.py rename to agent/examples/ImpactAgent.py index 870d0e419..5f88b20a1 100644 --- a/agent/ImpactAgent.py +++ b/agent/examples/ImpactAgent.py @@ -1,10 +1,7 @@ from agent.TradingAgent import TradingAgent -from message.Message import Message -from util.util import log_print -import numpy as np import pandas as pd -import sys + class ImpactAgent(TradingAgent): diff --git a/agent/examples/MarketMakerAgent.py b/agent/examples/MarketMakerAgent.py new file mode 100644 index 000000000..ebe79fc5a --- /dev/null +++ b/agent/examples/MarketMakerAgent.py @@ -0,0 +1,79 @@ +from agent.TradingAgent import TradingAgent +import pandas as pd + + +class MarketMakerAgent(TradingAgent): + """ + Simple market maker agent that attempts to provide liquidity in the orderbook by placing orders on both sides every + time it wakes up. The agent starts off by cancelling any existing orders, it then queries the current spread to + determine the trades to be placed. The order size is chosen at random between min_size and max_size which are parameters + of the agent. The agent places orders in 1-5 price levels randomly with sizes determined by the levels_quote_dict. + """ + + def __init__(self, id, name, type, symbol, starting_cash, min_size, max_size , wake_up_freq='10s', + log_orders=False, random_state=None): + + super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state=random_state) + self.symbol = symbol # Symbol traded + self.min_size = min_size # Minimum order size + self.max_size = max_size # Maximum order size + self.size = round(self.random_state.randint(self.min_size, self.max_size) / 2) # order size per LOB side + self.wake_up_freq = wake_up_freq # Frequency of agent wake up + self.log_orders = log_orders + self.state = "AWAITING_WAKEUP" + # Percentage of the order size to be placed at different levels is determined by levels_quote_dict + self.levels_quote_dict = {1: [1, 0, 0, 0, 0], + 2: [.5, .5, 0, 0, 0], + 3: [.34, .33, .33, 0, 0], + 4: [.25, .25, .25, .25, 0], + 5: [.20, .20, .20, .20, .20]} + + def kernelStarting(self, startTime): + super().kernelStarting(startTime) + + def wakeup(self, currentTime): + """ Agent wakeup is determined by self.wake_up_freq """ + can_trade = super().wakeup(currentTime) + if not can_trade: return + self.cancelOrders() + self.getCurrentSpread(self.symbol, depth=5) + self.state = 'AWAITING_SPREAD' + + def receiveMessage(self, currentTime, msg): + """ Market Maker actions are determined after obtaining the bids and asks in the LOB """ + super().receiveMessage(currentTime, msg) + if self.state == 'AWAITING_SPREAD' and msg.body['msg'] == 'QUERY_SPREAD': + bids, asks = self.getKnownBidAsk(self.symbol, best=False) + num_levels = self.random_state.randint(1, 6) # Number of price levels to place the trades in + size_split = self.levels_quote_dict.get(num_levels) # % of the order size to be placed at different levels + + if bids and asks: + buy_quotes, sell_quotes = {}, {} + for i in range(num_levels): + vol = round(size_split[i] * self.size) + try: + buy_quotes[bids[i][0]] = vol + except IndexError: + # Orderbook price level i empty so create a new price level with bid price 1 CENT below + buy_quotes[bids[-1][0] - 1] = vol + + try: + sell_quotes[asks[i][0]] = vol + except IndexError: + # Orderbook price level i empty so create a new price level with bid price 1 CENT above + sell_quotes[asks[-1][0] + 1] = vol + + for price, vol in buy_quotes.items(): + self.placeLimitOrder(self.symbol, vol, True, price) + for price, vol in sell_quotes.items(): + self.placeLimitOrder(self.symbol, vol, False, price) + self.setWakeup(currentTime + self.getWakeFrequency()) + self.state = 'AWAITING_WAKEUP' + + def cancelOrders(self): + """ cancels all resting limit orders placed by the market maker """ + for _, order in self.orders.items(): + self.cancelOrder(order) + + def getWakeFrequency(self): + return pd.Timedelta(self.wake_up_freq) \ No newline at end of file diff --git a/agent/examples/MarketReplayAgent.py b/agent/examples/MarketReplayAgent.py new file mode 100644 index 000000000..2ee06af4f --- /dev/null +++ b/agent/examples/MarketReplayAgent.py @@ -0,0 +1,63 @@ +from agent.TradingAgent import TradingAgent +from util.order.LimitOrder import LimitOrder +from util.util import log_print + + +class MarketReplayAgent(TradingAgent): + + def __init__(self, id, name, type, symbol, date, starting_cash, log_orders=False, random_state=None): + super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state=random_state) + self.symbol = symbol + self.date = date + self.log_orders = log_orders + self.executed_trades = dict() + self.state = 'AWAITING_WAKEUP' + self.orders_dict = None + self.wakeup_times = None + + def kernelStarting(self, startTime): + super().kernelStarting(startTime) + self.orders_dict = self.kernel.oracle.orders_dict + self.wakeup_times = self.kernel.oracle.wakeup_times + + def kernelStopping(self): + super().kernelStopping() + + def wakeup(self, currentTime): + super().wakeup(currentTime) + if not self.mkt_open or not self.mkt_close: + return + try: + self.setWakeup(self.wakeup_times[0]) + self.wakeup_times.pop(0) + self.placeOrder(currentTime, self.orders_dict[currentTime]) + except IndexError: + log_print(f"Market Replay Agent submitted all orders - last order @ {currentTime}") + + def receiveMessage(self, currentTime, msg): + super().receiveMessage(currentTime, msg) + if msg.body['msg'] == 'ORDER_EXECUTED': + order = msg.body['order'] + self.executed_trades[currentTime] = [order.fill_price, order.quantity] + + def placeOrder(self, currentTime, order): + if len(order) == 1: + order = order[0] + order_id = order['ORDER_ID'] + existing_order = self.orders.get(order_id) + if not existing_order: + self.placeLimitOrder(self.symbol, order['SIZE'], order['BUY_SELL_FLAG'] == 'BUY', order['PRICE'], + order_id=order_id) + elif existing_order and order['SIZE'] == 0: + self.cancelOrder(existing_order) + elif existing_order: + self.modifyOrder(existing_order, LimitOrder(self.id, currentTime, self.symbol, order['SIZE'], + order['BUY_SELL_FLAG'] == 'BUY', order['PRICE'], + order_id=order_id)) + else: + for ind_order in order: + self.placeOrder(currentTime, order=[ind_order]) + + def getWakeFrequency(self): + log_print(f"Market Replay Agent first wake up: {self.kernel.oracle.first_wakeup}") + return self.kernel.oracle.first_wakeup - self.mkt_open diff --git a/agent/examples/MomentumAgent.py b/agent/examples/MomentumAgent.py new file mode 100644 index 000000000..38b3e51c3 --- /dev/null +++ b/agent/examples/MomentumAgent.py @@ -0,0 +1,61 @@ +from agent.TradingAgent import TradingAgent +import pandas as pd +import numpy as np + + +class MomentumAgent(TradingAgent): + """ + Simple Trading Agent that compares the 20 past mid-price observations with the 50 past observations and places a + buy limit order if the 20 mid-price average >= 50 mid-price average or a + sell limit order if the 20 mid-price average < 50 mid-price average + """ + + def __init__(self, id, name, type, symbol, starting_cash, + min_size, max_size, wake_up_freq='60s', + log_orders=False, random_state=None): + + super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state=random_state) + self.symbol = symbol + self.min_size = min_size # Minimum order size + self.max_size = max_size # Maximum order size + self.size = self.random_state.randint(self.min_size, self.max_size) + self.wake_up_freq = wake_up_freq + self.mid_list, self.avg_20_list, self.avg_50_list = [], [], [] + self.log_orders = log_orders + self.state = "AWAITING_WAKEUP" + + def kernelStarting(self, startTime): + super().kernelStarting(startTime) + + def wakeup(self, currentTime): + """ Agent wakeup is determined by self.wake_up_freq """ + can_trade = super().wakeup(currentTime) + if not can_trade: return + self.getCurrentSpread(self.symbol) + self.state = 'AWAITING_SPREAD' + + def receiveMessage(self, currentTime, msg): + """ Momentum agent actions are determined after obtaining the best bid and ask in the LOB """ + super().receiveMessage(currentTime, msg) + if self.state == 'AWAITING_SPREAD' and msg.body['msg'] == 'QUERY_SPREAD': + bid, _, ask, _ = self.getKnownBidAsk(self.symbol) + if bid and ask: + self.mid_list.append((bid + ask) / 2) + if len(self.mid_list) > 20: self.avg_20_list.append(MomentumAgent.ma(self.mid_list, n=20)[-1].round(2)) + if len(self.mid_list) > 50: self.avg_50_list.append(MomentumAgent.ma(self.mid_list, n=50)[-1].round(2)) + if len(self.avg_20_list) > 0 and len(self.avg_50_list) > 0: + if self.avg_20_list[-1] >= self.avg_50_list[-1]: + self.placeLimitOrder(self.symbol, quantity=self.size, is_buy_order=True, limit_price=ask) + else: + self.placeLimitOrder(self.symbol, quantity=self.size, is_buy_order=False, limit_price=bid) + self.setWakeup(currentTime + self.getWakeFrequency()) + self.state = 'AWAITING_WAKEUP' + + def getWakeFrequency(self): + return pd.Timedelta(self.wake_up_freq) + + @staticmethod + def ma(a, n=20): + ret = np.cumsum(a, dtype=float) + ret[n:] = ret[n:] - ret[:-n] + return ret[n - 1:] / n \ No newline at end of file diff --git a/agent/examples/ShockAgent.py b/agent/examples/ShockAgent.py new file mode 100644 index 000000000..22507b540 --- /dev/null +++ b/agent/examples/ShockAgent.py @@ -0,0 +1,171 @@ +from agent.TradingAgent import TradingAgent + +import pandas as pd + + +# Extends Impact agent to fire large order evenly over a predetermined time period +# Need to add: (1) duration, (2) number of wakeups, (3) desired execution size +class ImpactAgent(TradingAgent): + + def __init__(self, id, name, type, symbol = None, starting_cash = None, within = 0.01, + impact = True, impact_time = None, impact_duration = 0, impact_trades = 0, + impact_vol = None, random_state = None): + # Base class init. + super().__init__(id, name, type, starting_cash = starting_cash, random_state = random_state) + + self.symbol = symbol # symbol to trade + self.trading = False # ready to trade + self.traded = False # has made its t trade + + # The amount of available "nearby" liquidity to consume when placing its order. + self.within = within # within this range of the inside price + + self.impact_time = impact_time # When should we make the impact trade? + self.impact_duration = impact_duration # How long the agent should wait to submit the next trade + self.impact_trades = impact_trades # The number of trades to execute across + self.impact_vol = impact_vol # The total volume to execute across all trades + + # The agent begins in its "complete" state, not waiting for + # any special event or condition. + self.state = 'AWAITING_WAKEUP' + + # Controls whether the impact trade is actually placed. + self.impact = impact + + + def wakeup (self, currentTime): + # Parent class handles discovery of exchange times and market_open wakeup call. + super().wakeup(currentTime) + + 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! + print ("{} is ready to start trading now.".format(self.name)) + + + # Steady state wakeup behavior starts here. + + # First, see if we have received a MKT_CLOSED message for the day. If so, + # there's nothing to do except clean-up. + if self.mkt_closed and (self.symbol in self.daily_close_price): + # Market is closed and we already got the daily close price. + return + + + ### Impact agent operates at a specific time. + if currentTime < self.impact_time: + print ("Impact agent waiting for impact_time {}".format(self.impact_time)) + self.setWakeup(self.impact_time) + return + + + ### The impact agent only trades once, but we will monitor prices for + ### the sake of performance. + self.setWakeup(currentTime + pd.Timedelta('30m')) + + + # If the market is 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. + # + # Also, if we already made our one trade, do nothing except monitor prices. + #if self.traded >= self.impact_trades or (self.mkt_closed and (not self.symbol in self.daily_close_price)): + if self.traded or (self.mkt_closed and (not self.symbol in self.daily_close_price)): + self.getLastTrade() + self.state = 'AWAITING_LAST_TRADE' + return + + #if self.traded < self.impact_trades: + #self.setWakeup(currentTime + impact_duration) + + # The impact agent will place one order based on the current spread. + self.getCurrentSpread() + self.state = 'AWAITING_SPREAD' + + + def receiveMessage (self, currentTime, msg): + # Parent class schedules market open wakeup call once market open/close times are known. + super().receiveMessage(currentTime, msg) + + # We have been awakened by something other than our scheduled wakeup. + # If our internal state indicates we were waiting for a particular event, + # check if we can transition to a new state. + + if self.state == 'AWAITING_SPREAD': + # We were waiting for current spread information to make our trade. + # If the message we just received is QUERY_SPREAD, that means we just got it. + if msg.body['msg'] == 'QUERY_SPREAD': + # Place our one trade. + bid, bid_vol, ask, ask_vol = self.getKnownBidAsk(self.symbol) + #bid_liq, ask_liq = self.getKnownLiquidity(self.symbol, within=self.within) + print('within: ' + str(self.within)) + bid_liq, ask_liq = self.getKnownLiquidity(self.symbol, within=0.75) + + # Buy order. + #direction, shares, price = True, int(round(ask_liq * self.greed)), ask + + # Sell order. This should be a parameter, but isn't yet. + #direction, shares = False, int(round(bid_liq * self.greed)) + direction, shares = False, int(round(bid_liq * 0.5)) + + # Compute the limit price we must offer to ensure our order executes immediately. + # This is essentially a workaround for the lack of true market orders in our + # current simulation. + price = self.computeRequiredPrice(direction, shares) + + # Actually place the order only if self.impact is true. + if self.impact: + print ("Impact agent firing: {} {} @ {} @ {}".format('BUY' if direction else 'SELL', shares, self.dollarize(price), currentTime)) + self.placeLimitOrder (self.symbol, shares, direction, price) + else: + print ("Impact agent would fire: {} {} @ {} (but self.impact = False)".format('BUY' if direction else 'SELL', shares, self.dollarize(price))) + + self.traded = True + self.state = 'AWAITING_WAKEUP' + + + # Internal state and logic specific to this agent. + + def placeLimitOrder (self, symbol, quantity, is_buy_order, limit_price): + super().placeLimitOrder(symbol, quantity, is_buy_order, limit_price, ignore_risk = True) + + # Computes required limit price to immediately execute a trade for the specified quantity + # of shares. + def computeRequiredPrice (self, direction, shares): + book = self.known_asks[self.symbol] if direction else self.known_bids[self.symbol] + + # Start at the inside and add up the shares. + t = 0 + + for i in range(len(book)): + p,v = book[i] + t += v + + # If we have accumulated enough shares, return this price. + # Need to also return if greater than the number of desired shares + if t >= shares: return p + + # Not enough shares. Just return worst price (highest ask, lowest bid). + return book[-1][0] + + + # Request the last trade price for our symbol. + def getLastTrade (self): + super().getLastTrade(self.symbol) + + + # Request the spread for our symbol. + def getCurrentSpread (self): + # Impact agent gets depth 10000 on each side (probably everything). + super().getCurrentSpread(self.symbol, 10000) + + + def getWakeFrequency (self): + return (pd.Timedelta('1ns')) + + diff --git a/agent/SumClientAgent.py b/agent/examples/SumClientAgent.py similarity index 97% rename from agent/SumClientAgent.py rename to agent/examples/SumClientAgent.py index 290b8b530..f358f6bb5 100644 --- a/agent/SumClientAgent.py +++ b/agent/examples/SumClientAgent.py @@ -1,5 +1,5 @@ from agent.Agent import Agent -from agent.SumServiceAgent import SumServiceAgent +from agent.examples.SumServiceAgent import SumServiceAgent from message.Message import Message from util.util import log_print diff --git a/agent/SumServiceAgent.py b/agent/examples/SumServiceAgent.py similarity index 100% rename from agent/SumServiceAgent.py rename to agent/examples/SumServiceAgent.py diff --git a/agent/examples/__init__.py b/agent/examples/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/cli/intraday_index.py b/cli/intraday_index.py new file mode 100644 index 000000000..85ecd3847 --- /dev/null +++ b/cli/intraday_index.py @@ -0,0 +1,255 @@ +# This file calculates the intraday index in a similar method to the Dow, +# after the conclusion of a simulation run. It takes the average price +# of the symbols in the index at each timestep, and graphs it. The +# price refers to the mid. +# Also graphs the mid of the underlying symbols + +import ast +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt +import pandas as pd +import os +import sys +import numpy as np + +from joblib import Memory + +# Auto-detect terminal width. +pd.options.display.width = None +pd.options.display.max_rows = 1000 +pd.options.display.max_colwidth = 200 + +# Initialize a persistent memcache. +mem_hist = Memory(cachedir='./.cached_plot_hist', verbose=0) +mem_sim = Memory(cachedir='./.cached_plot_sim', verbose=0) + + +PRINT_BASELINE = False +PRINT_DELTA_ONLY = False + +BETWEEN_START = pd.to_datetime('09:30').time() +BETWEEN_END = pd.to_datetime('09:30:00.000001').time() + +# Linewidth for plots. +LW = 2 + +# Used to read and cache simulated quotes. +# Doesn't actually pay attention to symbols yet. +#@mem_sim.cache +def read_simulated_quotes (file): + print ("Simulated quotes were not cached. This will take a minute.") + df = pd.read_pickle(file, compression='bz2') + df['Timestamp'] = df.index + + df_bid = df[df['EventType'] == 'BEST_BID'].copy() + df_ask = df[df['EventType'] == 'BEST_ASK'].copy() + + if len(df) <= 0: + print ("There appear to be no simulated quotes.") + sys.exit() + + df_bid['SYM'] = [s for s,b,bv in df_bid['Event'].str.split(',')] + df_bid['BEST_BID'] = [b for s,b,bv in df_bid['Event'].str.split(',')] + df_bid['BEST_BID_VOL'] = [bv for s,b,bv in df_bid['Event'].str.split(',')] + df_ask['SYM'] = [s for s,a,av in df_ask['Event'].str.split(',')] + df_ask['BEST_ASK'] = [a for s,a,av in df_ask['Event'].str.split(',')] + df_ask['BEST_ASK_VOL'] = [av for s,a,av in df_ask['Event'].str.split(',')] + + df_bid['BEST_BID'] = df_bid['BEST_BID'].str.replace('$','').astype('float64') + df_ask['BEST_ASK'] = df_ask['BEST_ASK'].str.replace('$','').astype('float64') + + df_bid['BEST_BID_VOL'] = df_bid['BEST_BID_VOL'].astype('float64') + df_ask['BEST_ASK_VOL'] = df_ask['BEST_ASK_VOL'].astype('float64') + + + # Keep only the last bid and last ask event at each timestamp. + df_bid = df_bid.drop_duplicates(subset=['Timestamp', 'SYM'], keep='last') + df_ask = df_ask.drop_duplicates(subset=['Timestamp', 'SYM'], keep='last') + + #df = df_bid.join(df_ask, how='outer', lsuffix='.bid', rsuffix='.ask') + + # THIS ISN'T TRUE, YOU CAN'T GET THE MID FROM FUTURE ORDERS!!! + #df['BEST_BID'] = df['BEST_BID'].ffill().bfill() + #df['BEST_ASK'] = df['BEST_ASK'].ffill().bfill() + #df['BEST_BID_VOL'] = df['BEST_BID_VOL'].ffill().bfill() + #df['BEST_ASK_VOL'] = df['BEST_ASK_VOL'].ffill().bfill() + df = pd.merge(df_bid, df_ask, how='left', left_on=['Timestamp','SYM'], right_on = ['Timestamp','SYM']) + + df['MIDPOINT'] = (df['BEST_BID'] + df['BEST_ASK']) / 2.0 + + #ts = df['Timestamp.bid'] + ts = df['Timestamp'] + #print(df) + df['INTRADAY_INDEX'] = 0 + #df['Timestamp.bid'] = df.index.to_series() + #df['Timestamp'] = df.index.to_series() + #df['SYM.bid'] = df['SYM.bid'].fillna(df['SYM.ask']) + #symbols = df['SYM.bid'].unique() + symbols = df['SYM'].unique() + #print(df) + for i,x in enumerate(symbols): + #df_one_sym = df[df['SYM.bid']==x] + df_one_sym = df[df['SYM']==x] + #df_one_sym = df_one_sym[['Timestamp.bid','MIDPOINT','BEST_BID','BEST_ASK']] + df_one_sym = df_one_sym[['Timestamp','MIDPOINT','BEST_BID','BEST_ASK']] + #md = pd.merge_asof(ts, df_one_sym, on='Timestamp.bid') + #print(ts) + #print(df_one_sym) + md = pd.merge_asof(ts, df_one_sym, on='Timestamp') + md = md.set_index(df.index) + df['MIDPOINT.' + x] = md['MIDPOINT'] + df['BID.' + x] = md['BEST_BID'] + df['ASK.' + x] = md['BEST_ASK'] + if x != 'ETF': + df['INTRADAY_INDEX'] = df['INTRADAY_INDEX'] + md['MIDPOINT'] + df['MSE'] = (df['INTRADAY_INDEX'] - df['MIDPOINT.ETF'])**2 + + #del df['Timestamp.bid'] + #del df['Timestamp.ask'] + df = df.set_index(df['Timestamp']) + del df['Timestamp'] + + return df + + + +# Main program starts here. + +if len(sys.argv) < 2: + print ("Usage: python midpoint_plot.py ") + sys.exit() + +# TODO: only really works for one symbol right now. + +#symbols = sys.argv[1] +sim_file = sys.argv[1] + +print ("Visualizing simulated {} from {}".format(12,sim_file)) +df_sim = read_simulated_quotes(sim_file) + +if PRINT_BASELINE: + baseline_file = os.path.join(os.path.dirname(sim_file) + '_baseline', os.path.basename(sim_file)) + print (baseline_file) + df_baseline = read_simulated_quotes(baseline_file) + +plt.rcParams.update({'font.size': 12}) + + +# Use to restrict time to plot. +df_sim = df_sim.between_time(BETWEEN_START, BETWEEN_END) + +if PRINT_BASELINE: + df_baseline = df_baseline.between_time(BETWEEN_START, BETWEEN_END) + +fig,ax = plt.subplots(figsize=(12,9), nrows=1, ncols=1) +axes = [ax] + +# For smoothing... +#hist_window = 100 +#sim_window = 100 + +hist_window = 1 +sim_window = 1 + +if PRINT_BASELINE: + # For nanosecond experiments, turn it into int index. Pandas gets weird if all + # the times vary only by a few nanoseconds. + rng = pd.date_range(start=df_sim.index[0], end=df_sim.index[-1], freq='1N') + + df_baseline = df_baseline[~df_baseline.index.duplicated(keep='last')] + df_baseline = df_baseline.reindex(rng,method='ffill') + df_baseline = df_baseline.reset_index(drop=True) + + df_sim = df_sim[~df_sim.index.duplicated(keep='last')] + df_sim = df_sim.reindex(rng,method='ffill') + df_sim = df_sim.reset_index(drop=True) + + # Print both separately. + if PRINT_DELTA_ONLY: + # Print the difference as a single series. + df_diff = df_sim['MIDPOINT'] - df_baseline['MIDPOINT'] + + # Smoothing. + df_diff = df_diff.rolling(window=10).mean() + + df_diff.plot(color='C0', grid=True, linewidth=LW, ax=axes[0]) + + axes[0].legend(['Bid-ask Midpoint Delta']) + else: + df_baseline['MIDPOINT'].plot(color='C0', grid=True, linewidth=LW, ax=axes[0]) + df_sim['MIDPOINT'].plot(color='C1', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + + axes[0].legend(['Baseline', 'With Impact']) + +else: + #df_sim['PRICE'] = df_sim['PRICE'].rolling(window=sim_window).mean() + + # For nanosecond experiments, turn it into int index. Pandas gets weird if all + # the times vary only by a few nanoseconds. + rng = pd.date_range(start=df_sim.index[0], end=df_sim.index[-1], freq='1N') + df_sim = df_sim[~df_sim.index.duplicated(keep='last')] + df_sim = df_sim.reindex(rng,method='ffill') + df_time = df_sim.copy() + df_sim = df_sim.reset_index(drop=True) + + #symbols = df_sim['SYM.bid'].unique() + symbols = df_sim['SYM'].unique() + for i,x in enumerate(symbols): + #df_sim[df_sim['SYM.bid']==x]['MIDPOINT.' + x].plot(color='C1', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + df_sim['MIDPOINT.' + x].plot(color='C1', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + #df_sim['BID.' + x].plot(color='C2', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + #df_sim['ASK.' + x].plot(color='C3', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + if x != 'ETF': + axes[0].legend(['Simulated']) + + plt.suptitle('Bid-Ask Midpoint: {}'.format(x)) + + axes[0].set_ylabel('Quote Price') + axes[0].set_xlabel('Quote Time') + + plt.savefig('graphs/background_' + str(x) + '_{}.png'.format('png')) + plt.cla() + + if x == 'ETF': + df_sim['MIDPOINT.' + x].plot(color='C1', grid=True, linewidth=LW, alpha=0.9, ax=axes[0], label = 'ETF Mid') + i = np.argwhere(symbols=='ETF') + symbols_portfolio = np.delete(symbols, i) + df_sim['INTRADAY_INDEX'].plot(color='C4', grid=True, linewidth=LW, alpha=0.9, ax=axes[0], label = 'Index') + #axes[0].legend(['Simulated']) + plt.suptitle('65 ZI, 0 ETF Arb, gamma = 500: {}'.format(symbols_portfolio)) + + axes[0].set_ylabel('Quote Price') + axes[0].set_xlabel('Quote Time') + axes[0].legend() + ymin = 249000 + ymax = 251000 + axes[0].set_ylim([ymin,ymax]) + + plt.savefig('graphs/index_vs_etf_ten_arb_gamma_500'.format('png')) + plt.cla() + + i = np.argwhere(symbols=='ETF') + symbols_portfolio = np.delete(symbols, i) + df_sim['INTRADAY_INDEX'].plot(color='C4', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + axes[0].legend(['Simulated']) + plt.suptitle('Intraday Index: {}'.format(symbols_portfolio)) + + plt.savefig('graphs/intraday_index_' + str(symbols_portfolio) + '_{}.png'.format('png')) + plt.cla() + + df_sim['MSE'].plot(color='C5', grid=True, linewidth=LW, alpha=0.9, ax=axes[0]) + #axes[0].legend(['Simulated']) + plt.suptitle('65 ZI, 10 ETF Arb, gamma = 500') + axes[0].set_ylabel('Mean Squared Error') + axes[0].set_xlabel('Quote Time') + ymin = -1000 + ymax = 700000 + axes[0].set_ylim([ymin,ymax]) + + plt.savefig('graphs/mse_index_etf_ten_arb_gamma_500'.format('png')) + plt.close() + #df_time.to_csv('test.csv') + +#plt.show() + diff --git a/config/impact.py b/config/impact.py index 08ed60e66..24d12d531 100644 --- a/config/impact.py +++ b/config/impact.py @@ -1,7 +1,7 @@ from Kernel import Kernel from agent.ExchangeAgent import ExchangeAgent from agent.HeuristicBeliefLearningAgent import HeuristicBeliefLearningAgent -from agent.ImpactAgent import ImpactAgent +from agent.examples.ImpactAgent import ImpactAgent from agent.ZeroIntelligenceAgent import ZeroIntelligenceAgent from util.order import LimitOrder from util.oracle.MeanRevertingOracle import MeanRevertingOracle diff --git a/config/marketreplay.py b/config/marketreplay.py index 7d7641f5e..e95cc4ff0 100644 --- a/config/marketreplay.py +++ b/config/marketreplay.py @@ -1,144 +1,147 @@ -from Kernel import Kernel - -from agent.MarketReplayAgent import MarketReplayAgent -from agent.ExchangeAgent import ExchangeAgent -from agent.ExperimentalAgent import ExperimentalAgent +import argparse +import numpy as np +import pandas as pd +import sys +import os +import datetime as dt +from Kernel import Kernel from util import util -from util.oracle.RandomOrderBookOracle import RandomOrderBookOracle from util.order import LimitOrder +from util.oracle.OrderBookOracle import OrderBookOracle +from agent.ExchangeAgent import ExchangeAgent +from agent.examples.MarketReplayAgent import MarketReplayAgent -import datetime as dt -import numpy as np -import pandas as pd -import sys -import argparse +######################################################################################################################## +############################################### GENERAL CONFIG ######################################################### -parser = argparse.ArgumentParser(description='Options for Market Replay Agent Config.') +parser = argparse.ArgumentParser(description='Detailed options for market replay config.') -# General Config for all agents -parser.add_argument('-c', '--config', required=True, +parser.add_argument('-c', + '--config', + required=True, help='Name of config file to execute') -parser.add_argument('-s', '--seed', type=int, default=None, - help='numpy.random.seed() for simulation') -parser.add_argument('-l', '--log_dir', default=None, +parser.add_argument('-l', + '--log_dir', + default=None, help='Log directory name (default: unix timestamp at program start)') -parser.add_argument('-v', '--verbose', action='store_true', - help='Maximum verbosity!') -parser.add_argument('-o', '--log_orders', action='store_true', +parser.add_argument('-o', + '--log_orders', + action='store_true', help='Log every order-related action by every agent.') -parser.add_argument('--config_help', action='store_true', +parser.add_argument('-s', + '--seed', + type=int, + default=None, + help='numpy.random.seed() for simulation') +parser.add_argument('-v', + '--verbose', + action='store_true', + help='Maximum verbosity!') +parser.add_argument('--config_help', + action='store_true', help='Print argument options for this config file') args, remaining_args = parser.parse_known_args() -log_orders = args.log_orders - if args.config_help: - parser.print_help() - sys.exit() + parser.print_help() + sys.exit() -# Simulation Start Time -simulation_start_time = dt.datetime.now() -print ("Simulation Start Time: {}".format(simulation_start_time)) - -# Random Seed Config -seed = args.seed -if not seed: seed = int(pd.Timestamp.now().timestamp() * 1000000) % (2**32 - 1) +log_dir = args.log_dir # Requested log directory. +seed = args.seed # Random seed specification on the command line. +log_orders = args.log_orders +if not seed: seed = int(pd.Timestamp.now().timestamp() * 1000000) % (2 ** 32 - 1) np.random.seed(seed) -print ("Configuration seed: {}".format(seed)) - -random_state = np.random.RandomState(seed=np.random.randint(low=1)) util.silent_mode = not args.verbose LimitOrder.silent_mode = not args.verbose -print ("Silent mode: {}".format(util.silent_mode)) - -######################## Agents Config ######################################################################### - -# 1) Symbols -symbols = ['AAPL'] -print("Symbols traded: {}".format(symbols)) - -# 2) Historical Date to simulate -date = '2019-06-19' -date_pd = pd.to_datetime(date) -print("Historical Simulation Date: {}".format(date)) - -agents = [] - -# 3) ExchangeAgent Config -num_exchanges = 1 -mkt_open = date_pd + pd.to_timedelta('09:30:00') -mkt_close = date_pd + pd.to_timedelta('09:35:00') -print("ExchangeAgent num_exchanges: {}".format(num_exchanges)) -print("ExchangeAgent mkt_open: {}".format(mkt_open)) -print("ExchangeAgent mkt_close: {}".format(mkt_close)) - -ea = ExchangeAgent(id = 0, - name = 'Exchange_Agent', - type = 'ExchangeAgent', - mkt_open = mkt_open, - mkt_close = mkt_close, - symbols = symbols, - log_orders = log_orders, - book_freq = None, - pipeline_delay = 0, - computation_delay = 0, - stream_history = 10, - random_state = random_state) - -agents.extend([ea]) - -# 4) MarketReplayAgent Config -market_replay_agents = [MarketReplayAgent(id = 1, - name = "Market_Replay_Agent", - type = 'MarketReplayAgent', - symbol = symbols[0], - log_orders = log_orders, - date = date, - starting_cash = 0, - random_state = random_state)] -agents.extend(market_replay_agents) - -# 5) ExperimentalAgent Config -experimental_agents = [ExperimentalAgent(id = 2, - name = "Experimental_Agent", - symbol = symbols[0], - starting_cash = 10000000, - log_orders = log_orders, - execution_timestamp = pd.Timestamp("2019-06-19 09:32:00"), - quantity = 1000, - is_buy_order = True, - limit_price = 50000, - random_state = random_state)] -agents.extend(experimental_agents) -####################################################################################################################### - -# 6) Kernel Parameters -kernel = Kernel("Market Replay Kernel", random_state = random_state) - -kernelStartTime = date_pd + pd.to_timedelta('09:30:00') -kernelStopTime = date_pd + pd.to_timedelta('09:35:00') + +simulation_start_time = dt.datetime.now() +print("Simulation Start Time: {}".format(simulation_start_time)) +print("Configuration seed: {}\n".format(seed)) +######################################################################################################################## +############################################### AGENTS CONFIG ########################################################## + +# Historical date to simulate. +historical_date = pd.to_datetime('2019-06-28') +symbol = 'JPM' + +agent_count, agents, agent_types = 0, [], [] + +# 1) Exchange Agent +mkt_open = historical_date + pd.to_timedelta('09:30:00') +mkt_close = historical_date + pd.to_timedelta('16:00:00') +agents.extend([ExchangeAgent(id=0, + name="Exchange Agent", + type="ExchangeAgent", + mkt_open=mkt_open, + mkt_close=mkt_close, + symbols=[symbol], + log_orders=log_orders, + pipeline_delay=0, + computation_delay=0, + stream_history=10, + book_freq=0, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64')))]) +agent_types.extend("ExchangeAgent") +agent_count += 1 + +# 2) Market Replay Agent +""" +oracle = OrderBookOracle (symbol = symbol, + start_time = mkt_open, + end_time = mkt_close, + orders_file_path = os.getcwd() + '\data\sample_orders_file.csv') +""" + +symbol = 'JPM' +date = '20190628' +file_name = f'DOW30/{symbol}/{symbol}.{date}' +orders_file_path = f'/efs/data/{file_name}' + +oracle = OrderBookOracle(symbol=symbol, + date=historical_date, + start_time=mkt_open, + end_time=mkt_close, + orders_file_path=orders_file_path) + +agents.extend([MarketReplayAgent(id=1, + name="MARKET_REPLAY_AGENT", + type='MarketReplayAgent', + symbol=symbol, + log_orders=log_orders, + date=historical_date, + starting_cash=0, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64')))]) +agent_types.extend("MarketReplayAgent") +agent_count += 1 + +######################################################################################################################## +########################################### KERNEL AND OTHER CONFIG #################################################### + +kernel = Kernel("Market Replay Kernel", random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + +kernelStartTime = mkt_open +kernelStopTime = historical_date + pd.to_timedelta('16:01:00') + defaultComputationDelay = 0 -latency = np.zeros((3, 3)) -noise = [ 0.0 ] - -oracle = RandomOrderBookOracle(symbol = 'AAPL', - market_open_ts = mkt_open, - market_close_ts = mkt_close, - buy_price_range = [9000, 10500], - sell_price_range = [9500, 11000], - quantity_range = [5000, 50000], - seed=seed) - -kernel.runner(agents = agents, startTime = kernelStartTime, - stopTime = kernelStopTime, agentLatency = latency, - latencyNoise = noise, - defaultComputationDelay = defaultComputationDelay, +latency = np.zeros((agent_count, agent_count)) +noise = [0.0] + +kernel.runner(agents=agents, + startTime=kernelStartTime, + stopTime=kernelStopTime, + agentLatency=latency, + latencyNoise=noise, + defaultComputationDelay=defaultComputationDelay, defaultLatency=0, - oracle = oracle, log_dir = args.log_dir) + oracle=oracle, + log_dir=args.log_dir) simulation_end_time = dt.datetime.now() -print ("Simulation End Time: {}".format(simulation_end_time)) -print ("Time taken to run simulation: {}".format(simulation_end_time - simulation_start_time)) \ No newline at end of file +print("Simulation End Time: {}".format(simulation_end_time)) +print("Time taken to run simulation: {}".format(simulation_end_time - simulation_start_time)) diff --git a/config/rmsc01.py b/config/rmsc01.py new file mode 100755 index 000000000..5a13c54ff --- /dev/null +++ b/config/rmsc01.py @@ -0,0 +1,244 @@ +# RMSC-1 (Reference Market Simulation Configuration): +# - 1 Exchange Agent +# - 1 Market Maker Agent +# - 50 ZI Agent +# - 25 HBL Agent +# - 24 Momentum Agent + +import argparse +import numpy as np +import pandas as pd +import sys +import datetime as dt +import importlib + +from Kernel import Kernel +from util import util +from util.order import LimitOrder +from util.oracle.SparseMeanRevertingOracle import SparseMeanRevertingOracle + +from agent.ExchangeAgent import ExchangeAgent +from agent.examples.MarketMakerAgent import MarketMakerAgent +from agent.examples.MomentumAgent import MomentumAgent + +from agent.ZeroIntelligenceAgent import ZeroIntelligenceAgent +from agent.HeuristicBeliefLearningAgent import HeuristicBeliefLearningAgent + +######################################################################################################################## +############################################### GENERAL CONFIG ######################################################### + +parser = argparse.ArgumentParser( + description='Detailed options for RMSC-1 (Reference Market Simulation Configuration) config.') + +parser.add_argument('-c', + '--config', + required=True, + help='Name of config file to execute') +parser.add_argument('-l', + '--log_dir', + default=None, + help='Log directory name (default: unix timestamp at program start)') +parser.add_argument('-s', + '--seed', + type=int, + default=None, + help='numpy.random.seed() for simulation') +parser.add_argument('-v', + '--verbose', + action='store_true', + help='Maximum verbosity!') +parser.add_argument('--config_help', + action='store_true', + help='Print argument options for this config file') +parser.add_argument('-a', + '--agent_name', + default=None, + help='Specify the agent to test with') + +args, remaining_args = parser.parse_known_args() + +if args.config_help: + parser.print_help() + sys.exit() + + +log_dir = args.log_dir # Requested log directory. +seed = args.seed # Random seed specification on the command line. +if not seed: seed = int(pd.Timestamp.now().timestamp() * 1000000) % (2 ** 32 - 1) +np.random.seed(seed) + +util.silent_mode = not args.verbose +LimitOrder.silent_mode = not args.verbose + +simulation_start_time = dt.datetime.now() +print("Simulation Start Time: {}".format(simulation_start_time)) +print("Configuration seed: {}\n".format(seed)) +######################################################################################################################## +############################################### AGENTS CONFIG ########################################################## + +# Historical date to simulate. +historical_date = pd.to_datetime('2019-06-28') +symbol = 'JPM' + +agent_count, agents, agent_types = 0, [], [] +starting_cash = 10000000 # Cash in this simulator is always in CENTS. + +# 1) 1 Exchange Agent +mkt_open = historical_date + pd.to_timedelta('09:30:00') +mkt_close = historical_date + pd.to_timedelta('16:00:00') + +agents.extend([ExchangeAgent(id=0, + name="EXCHANGE_AGENT", + type="ExchangeAgent", + mkt_open=mkt_open, + mkt_close=mkt_close, + symbols=[symbol], + log_orders=False, + pipeline_delay=0, + computation_delay=0, + stream_history=10, + book_freq='all', + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64')))]) +agent_types.extend("ExchangeAgent") +agent_count += 1 + +# 2) 1 Market Maker Agent +num_mm_agents = 1 +agents.extend([MarketMakerAgent(id=j, + name="MARKET_MAKER_AGENT_{}".format(j), + type='MarketMakerAgent', + symbol=symbol, + starting_cash=starting_cash, + min_size=500, + max_size=1000, + log_orders=False, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + for j in range(agent_count, agent_count + num_mm_agents)]) + +agent_types.extend('MarketMakerAgent') +agent_count += num_mm_agents + +# 3) 50 Zero Intelligence Agents +symbols = {symbol: {'r_bar': 1e5, + 'kappa': 1.67e-12, + 'agent_kappa': 1.67e-15, + 'sigma_s': 0, + 'fund_vol': 1e-4, + 'megashock_lambda_a': 2.77778e-13, + 'megashock_mean': 1e3, + 'megashock_var': 5e4, + 'random_state': np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))}} +oracle = SparseMeanRevertingOracle(mkt_open, mkt_close, symbols) + +num_zi_agents = 50 +agents.extend([ZeroIntelligenceAgent(id=j, + name="ZI_AGENT_{}".format(j), + type="ZeroIntelligenceAgent", + symbol=symbol, + starting_cash=starting_cash, + sigma_n=10000, + sigma_s=symbols[symbol]['fund_vol'], + kappa=symbols[symbol]['agent_kappa'], + r_bar=symbols[symbol]['r_bar'], + q_max=10, + sigma_pv=5e4, + R_min=0, + R_max=100, + eta=1, + lambda_a=1e-12, + log_orders=False, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + for j in range(agent_count, agent_count + num_zi_agents)]) +agent_types.extend("ZeroIntelligenceAgent") +agent_count += num_zi_agents + +# 4) 25 Heuristic Belief Learning Agents +num_hbl_agents = 25 +agents.extend([HeuristicBeliefLearningAgent(id=j, + name="HBL_AGENT_{}".format(j), + type="HeuristicBeliefLearningAgent", + symbol=symbol, + starting_cash=starting_cash, + sigma_n=10000, + sigma_s=symbols[symbol]['fund_vol'], + kappa=symbols[symbol]['agent_kappa'], + r_bar=symbols[symbol]['r_bar'], + q_max=10, + sigma_pv=5e4, + R_min=0, + R_max=100, + eta=1, + lambda_a=1e-12, + L=2, + log_orders=False, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + for j in range(agent_count, agent_count + num_hbl_agents)]) +agent_types.extend("HeuristicBeliefLearningAgent") +agent_count += num_hbl_agents + +# 5) 24 Momentum Agents: +num_momentum_agents = 24 +agents.extend([MomentumAgent(id=j, + name="MOMENTUM_AGENT_{}".format(j), + type="MomentumAgent", + symbol=symbol, + starting_cash=starting_cash, + min_size=1, + max_size=10, + log_orders=False, + random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + for j in range(agent_count, agent_count + num_momentum_agents)]) +agent_types.extend("MomentumAgent") +agent_count += num_momentum_agents + +# 6) User defined agent +# Load the agent to evaluate against the market +if args.agent_name: + mod_name = args.agent_name.rsplit('.', 1)[0] + class_name = args.agent_name.split('.')[-1] + m = importlib.import_module(args.agent_name, package=None) + testagent = getattr(m, class_name) + + agents.extend([testagent(id=agent_count, + name=args.agent_name, + type="AgentUnderTest", + symbol=symbol, + starting_cash=starting_cash, + min_size=1, + max_size=10, + log_orders=False, + random_state=np.random.RandomState(seed=np.random.randint(low=0,high=2**32,dtype='uint64')))]) + agent_count += 1 + +######################################################################################################################## +########################################### KERNEL AND OTHER CONFIG #################################################### + +kernel = Kernel("Market Replay Kernel", random_state=np.random.RandomState(seed=np.random.randint(low=0, high=2 ** 32, + dtype='uint64'))) + +kernelStartTime = mkt_open +kernelStopTime = historical_date + pd.to_timedelta('16:01:00') + +defaultComputationDelay = 0 +latency = np.zeros((agent_count, agent_count)) +noise = [0.0] + +kernel.runner(agents=agents, + startTime=kernelStartTime, + stopTime=kernelStopTime, + agentLatency=latency, + latencyNoise=noise, + defaultComputationDelay=defaultComputationDelay, + defaultLatency=0, + oracle=oracle, + log_dir=args.log_dir) + +simulation_end_time = dt.datetime.now() +print("Simulation End Time: {}".format(simulation_end_time)) +print("Time taken to run simulation: {}".format(simulation_end_time - simulation_start_time)) diff --git a/config/sparse_zi.py b/config/sparse_zi_100.py old mode 100644 new mode 100755 similarity index 87% rename from config/sparse_zi.py rename to config/sparse_zi_100.py index d2fc1ee04..e3114d211 --- a/config/sparse_zi.py +++ b/config/sparse_zi_100.py @@ -39,7 +39,7 @@ sys.exit() # Historical date to simulate. Required even if not relevant. -historical_date = pd.to_datetime('2014-01-28') +historical_date = pd.to_datetime('2019-06-28') # Requested log directory. log_dir = args.log_dir @@ -118,7 +118,7 @@ # This is a list of symbols the exchange should trade. It can handle any number. # It keeps a separate order book for each symbol. The example data includes -# only IBM. This config uses generated data, so the symbol doesn't really matter. +# only JPM. This config uses generated data, so the symbol doesn't really matter. # megashock_lambda_a is used to select spacing for megashocks (using an exponential # distribution equivalent to a centralized Poisson process). Megashock mean @@ -126,11 +126,11 @@ # Note: sigma_s is no longer used by the agents or the fundamental (for sparse discrete simulation). -symbols = { 'IBM' : { 'r_bar' : 1e5, 'kappa' : 1.67e-12, 'agent_kappa' : 1.67e-15, 'sigma_s' : 0, 'fund_vol' : 1e-4, 'megashock_lambda_a' : 2.77778e-13, 'megashock_mean' : 1e3, 'megashock_var' : 5e4, 'random_state' : np.random.RandomState(seed=np.random.randint(low=0,high=2**32)) } } +symbols = { 'JPM' : { 'r_bar' : 1e5, 'kappa' : 1.67e-12, 'agent_kappa' : 1.67e-15, 'sigma_s' : 0, 'fund_vol' : 1e-4, 'megashock_lambda_a' : 2.77778e-13, 'megashock_mean' : 1e3, 'megashock_var' : 5e4, 'random_state' : np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64')) } } ### Configure the Kernel. -kernel = Kernel("Base Kernel", random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) +kernel = Kernel("Base Kernel", random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64'))) @@ -157,7 +157,7 @@ # Create the exchange. num_exchanges = 1 -agents.extend([ ExchangeAgent(j, "Exchange Agent {}".format(j), "ExchangeAgent", mkt_open, mkt_close, [s for s in symbols], log_orders=log_orders, book_freq=book_freq, pipeline_delay = 0, computation_delay = 0, stream_history = 10, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) +agents.extend([ ExchangeAgent(j, "Exchange Agent {}".format(j), "ExchangeAgent", mkt_open, mkt_close, [s for s in symbols], log_orders=log_orders, book_freq=book_freq, pipeline_delay = 0, computation_delay = 0, stream_history = 10, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64'))) for j in range(agent_count, agent_count + num_exchanges) ]) agent_types.extend(["ExchangeAgent" for j in range(num_exchanges)]) agent_count += num_exchanges @@ -170,38 +170,22 @@ starting_cash = 10000000 # Here are the zero intelligence agents. -symbol = 'IBM' +symbol = 'JPM' s = symbols[symbol] # Tuples are: (# agents, R_min, R_max, eta). # Some configs for ZI agents only (among seven parameter settings). -# 4 agents -#zi = [ (1, 0, 250, 1), (1, 0, 500, 1), (1, 0, 1000, 0.8), (1, 0, 1000, 1), (0, 0, 2000, 0.8), (0, 250, 500, 0.8), (0, 250, 500, 1) ] - -# 28 agents -#zi = [ (4, 0, 250, 1), (4, 0, 500, 1), (4, 0, 1000, 0.8), (4, 0, 1000, 1), (4, 0, 2000, 0.8), (4, 250, 500, 0.8), (4, 250, 500, 1) ] - -# 65 agents -#zi = [ (10, 0, 250, 1), (10, 0, 500, 1), (9, 0, 1000, 0.8), (9, 0, 1000, 1), (9, 0, 2000, 0.8), (9, 250, 500, 0.8), (9, 250, 500, 1) ] - # 100 agents zi = [ (15, 0, 250, 1), (15, 0, 500, 1), (14, 0, 1000, 0.8), (14, 0, 1000, 1), (14, 0, 2000, 0.8), (14, 250, 500, 0.8), (14, 250, 500, 1) ] -# 1000 agents -#zi = [ (143, 0, 250, 1), (143, 0, 500, 1), (143, 0, 1000, 0.8), (143, 0, 1000, 1), (143, 0, 2000, 0.8), (143, 250, 500, 0.8), (142, 250, 500, 1) ] - -# 10000 agents -#zi = [ (1429, 0, 250, 1), (1429, 0, 500, 1), (1429, 0, 1000, 0.8), (1429, 0, 1000, 1), (1428, 0, 2000, 0.8), (1428, 250, 500, 0.8), (1428, 250, 500, 1) ] - - # ZI strategy split. Note that agent arrival rates are quite small, because our minimum # time step is a nanosecond, and we want the agents to arrive more on the order of # minutes. for i,x in enumerate(zi): strat_name = "Type {} [{} <= R <= {}, eta={}]".format(i+1, x[1], x[2], x[3]) - agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)),log_orders=log_orders, symbol=symbol, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s['r_bar'], kappa=s['agent_kappa'], sigma_s=s['fund_vol'], q_max=10, sigma_pv=5e6, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=1e-12) for j in range(agent_count,agent_count+x[0]) ]) + agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64')),log_orders=log_orders, symbol=symbol, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s['r_bar'], kappa=s['agent_kappa'], sigma_s=s['fund_vol'], q_max=10, sigma_pv=5e6, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=1e-12) for j in range(agent_count,agent_count+x[0]) ]) agent_types.extend([ "ZeroIntelligenceAgent {}".format(strat_name) for j in range(x[0]) ]) agent_count += x[0] diff --git a/config/sparse_zi_1000.py b/config/sparse_zi_1000.py new file mode 100755 index 000000000..12e2c1b33 --- /dev/null +++ b/config/sparse_zi_1000.py @@ -0,0 +1,235 @@ +from Kernel import Kernel +from agent.ExchangeAgent import ExchangeAgent +from agent.ZeroIntelligenceAgent import ZeroIntelligenceAgent +from util.order import LimitOrder +from util.oracle.SparseMeanRevertingOracle import SparseMeanRevertingOracle +from util import util + +import numpy as np +import pandas as pd +import sys + + +# Some config files require additional command line parameters to easily +# control agent or simulation hyperparameters during coarse parallelization. +import argparse + +parser = argparse.ArgumentParser(description='Detailed options for sparse_zi config.') +parser.add_argument('-b', '--book_freq', default=None, + help='Frequency at which to archive order book for visualization') +parser.add_argument('-c', '--config', required=True, + help='Name of config file to execute') +parser.add_argument('-l', '--log_dir', default=None, + help='Log directory name (default: unix timestamp at program start)') +parser.add_argument('-n', '--obs_noise', type=float, default=1000000, + help='Observation noise variance for zero intelligence agents (sigma^2_n)') +parser.add_argument('-o', '--log_orders', action='store_true', + help='Log every order-related action by every agent.') +parser.add_argument('-s', '--seed', type=int, default=None, + help='numpy.random.seed() for simulation') +parser.add_argument('-v', '--verbose', action='store_true', + help='Maximum verbosity!') +parser.add_argument('--config_help', action='store_true', + help='Print argument options for this config file') + +args, remaining_args = parser.parse_known_args() + +if args.config_help: + parser.print_help() + sys.exit() + +# Historical date to simulate. Required even if not relevant. +historical_date = pd.to_datetime('2019-06-28') + +# Requested log directory. +log_dir = args.log_dir + +# Requested order book snapshot archive frequency. +book_freq = args.book_freq + +# Observation noise variance for zero intelligence agents. +# This is a property of the agents, not the stock. +# Later, it could be a matrix across both. +sigma_n = args.obs_noise + +# Random seed specification on the command line. Default: None (by clock). +# If none, we select one via a specific random method and pass it to seed() +# so we can record it for future use. (You cannot reasonably obtain the +# automatically generated seed when seed() is called without a parameter.) + +# Note that this seed is used to (1) make any random decisions within this +# config file itself and (2) to generate random number seeds for the +# (separate) Random objects given to each agent. This ensure that when +# the agent population is appended, prior agents will continue to behave +# in the same manner save for influences by the new agents. (i.e. all prior +# agents still have their own separate PRNG sequence, and it is the same as +# before) + +seed = args.seed +if not seed: seed = int(pd.Timestamp.now().timestamp() * 1000000) % (2**32 - 1) +np.random.seed(seed) + +# Config parameter that causes util.util.print to suppress most output. +# Also suppresses formatting of limit orders (which is time consuming). +util.silent_mode = not args.verbose +LimitOrder.silent_mode = not args.verbose + +# Config parameter that causes every order-related action to be logged by +# every agent. Activate only when really needed as there is a significant +# time penalty to all that object serialization! +log_orders = args.log_orders + + +print ("Silent mode: {}".format(util.silent_mode)) +print ("Logging orders: {}".format(log_orders)) +print ("Book freq: {}".format(book_freq)) +print ("ZeroIntelligenceAgent noise: {:0.4f}".format(sigma_n)) +print ("Configuration seed: {}\n".format(seed)) + + + +# Since the simulator often pulls historical data, we use a real-world +# nanosecond timestamp (pandas.Timestamp) for our discrete time "steps", +# which are considered to be nanoseconds. For other (or abstract) time +# units, one can either configure the Timestamp interval, or simply +# interpret the nanoseconds as something else. + +# What is the earliest available time for an agent to act during the +# simulation? +midnight = historical_date +kernelStartTime = midnight + +# When should the Kernel shut down? (This should be after market close.) +# Here we go for 5 PM the same day. +kernelStopTime = midnight + pd.to_timedelta('17:00:00') + +# This will configure the kernel with a default computation delay +# (time penalty) for each agent's wakeup and recvMsg. An agent +# can change this at any time for itself. (nanoseconds) +defaultComputationDelay = 1000000000 # one second + + +# IMPORTANT NOTE CONCERNING AGENT IDS: the id passed to each agent must: +# 1. be unique +# 2. equal its index in the agents list +# This is to avoid having to call an extra getAgentListIndexByID() +# in the kernel every single time an agent must be referenced. + + +# This is a list of symbols the exchange should trade. It can handle any number. +# It keeps a separate order book for each symbol. The example data includes +# only JPM. This config uses generated data, so the symbol doesn't really matter. + +# megashock_lambda_a is used to select spacing for megashocks (using an exponential +# distribution equivalent to a centralized Poisson process). Megashock mean +# and variance control the size (bimodal distribution) of the individual shocks. + +# Note: sigma_s is no longer used by the agents or the fundamental (for sparse discrete simulation). + +symbols = { 'JPM' : { 'r_bar' : 1e5, 'kappa' : 1.67e-12, 'agent_kappa' : 1.67e-15, 'sigma_s' : 0, 'fund_vol' : 1e-4, 'megashock_lambda_a' : 2.77778e-13, 'megashock_mean' : 1e3, 'megashock_var' : 5e4, 'random_state' : np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64')) } } + + +### Configure the Kernel. +kernel = Kernel("Base Kernel", random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64'))) + + + +### Configure the agents. When conducting "agent of change" experiments, the +### new agents should be added at the END only. +agent_count = 0 +agents = [] +agent_types = [] + + +### Configure an exchange agent. + +# Let's open the exchange at 9:30 AM. +mkt_open = midnight + pd.to_timedelta('09:30:00') + +# And close it at 4:00 PM. +mkt_close = midnight + pd.to_timedelta('16:00:00') + + +# Configure an appropriate oracle for all traded stocks. +# All agents requiring the same type of Oracle will use the same oracle instance. +oracle = SparseMeanRevertingOracle(mkt_open, mkt_close, symbols) + + +# Create the exchange. +num_exchanges = 1 +agents.extend([ ExchangeAgent(j, "Exchange Agent {}".format(j), "ExchangeAgent", mkt_open, mkt_close, [s for s in symbols], log_orders=log_orders, book_freq=book_freq, pipeline_delay = 0, computation_delay = 0, stream_history = 10, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64'))) + for j in range(agent_count, agent_count + num_exchanges) ]) +agent_types.extend(["ExchangeAgent" for j in range(num_exchanges)]) +agent_count += num_exchanges + + + +### Configure some zero intelligence agents. + +# Cash in this simulator is always in CENTS. +starting_cash = 10000000 + +# Here are the zero intelligence agents. +symbol = 'JPM' +s = symbols[symbol] + +# Tuples are: (# agents, R_min, R_max, eta). + +# Some configs for ZI agents only (among seven parameter settings). + +# 1000 agents +zi = [ (143, 0, 250, 1), (143, 0, 500, 1), (143, 0, 1000, 0.8), (143, 0, 1000, 1), (143, 0, 2000, 0.8), (143, 250, 500, 0.8), (142, 250, 500, 1) ] + +# ZI strategy split. Note that agent arrival rates are quite small, because our minimum +# time step is a nanosecond, and we want the agents to arrive more on the order of +# minutes. +for i,x in enumerate(zi): + strat_name = "Type {} [{} <= R <= {}, eta={}]".format(i+1, x[1], x[2], x[3]) + agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32, dtype='uint64')),log_orders=log_orders, symbol=symbol, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s['r_bar'], kappa=s['agent_kappa'], sigma_s=s['fund_vol'], q_max=10, sigma_pv=5e6, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=1e-12) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "ZeroIntelligenceAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + + +### Configure a simple message latency matrix for the agents. Each entry is the minimum +### nanosecond delay on communication [from][to] agent ID. + +# Square numpy array with dimensions equal to total agent count. Most agents are handled +# at init, drawn from a uniform distribution from: +# Times Square (3.9 miles from NYSE, approx. 21 microseconds at the speed of light) to: +# Pike Place Starbucks in Seattle, WA (2402 miles, approx. 13 ms at the speed of light). +# Other agents can be explicitly set afterward (and the mirror half of the matrix is also). + +# This configures all agents to a starting latency as described above. +latency = np.random.uniform(low = 21000, high = 13000000, size=(len(agent_types),len(agent_types))) + +# Overriding the latency for certain agent pairs happens below, as does forcing mirroring +# of the matrix to be symmetric. +for i, t1 in zip(range(latency.shape[0]), agent_types): + for j, t2 in zip(range(latency.shape[1]), agent_types): + # Three cases for symmetric array. Set latency when j > i, copy it when i > j, same agent when i == j. + if j > i: + # Presently, strategy agents shouldn't be talking to each other, so we set them to extremely high latency. + if (t1 == "ZeroIntelligenceAgent" and t2 == "ZeroIntelligenceAgent"): + latency[i,j] = 1000000000 * 60 * 60 * 24 # Twenty-four hours. + elif i > j: + # This "bottom" half of the matrix simply mirrors the top. + latency[i,j] = latency[j,i] + else: + # This is the same agent. How long does it take to reach localhost? In our data center, it actually + # takes about 20 microseconds. + latency[i,j] = 20000 + + +# Configure a simple latency noise model for the agents. +# Index is ns extra delay, value is probability of this delay being applied. +noise = [ 0.25, 0.25, 0.20, 0.15, 0.10, 0.05 ] + + + +# Start the kernel running. +kernel.runner(agents = agents, startTime = kernelStartTime, + stopTime = kernelStopTime, agentLatency = latency, + latencyNoise = noise, + defaultComputationDelay = defaultComputationDelay, + oracle = oracle, log_dir = log_dir) + diff --git a/config/sum.py b/config/sum.py index ec8919fde..32a3fab53 100644 --- a/config/sum.py +++ b/config/sum.py @@ -1,9 +1,8 @@ from Kernel import Kernel -from agent.SumClientAgent import SumClientAgent -from agent.SumServiceAgent import SumServiceAgent +from agent.examples.SumClientAgent import SumClientAgent +from agent.examples.SumServiceAgent import SumServiceAgent from util import util -import datetime as dt import numpy as np import pandas as pd import sys diff --git a/config/twoSymbols.py b/config/twoSymbols.py new file mode 100644 index 000000000..e947cb26e --- /dev/null +++ b/config/twoSymbols.py @@ -0,0 +1,456 @@ +from Kernel import Kernel +from agent.ExchangeAgent import ExchangeAgent +from agent.etf.EtfPrimaryAgent import EtfPrimaryAgent +from agent.HeuristicBeliefLearningAgent import HeuristicBeliefLearningAgent +from agent.examples.ImpactAgent import ImpactAgent +from agent.ZeroIntelligenceAgent import ZeroIntelligenceAgent +from agent.examples.MomentumAgent import MomentumAgent +from agent.etf.EtfArbAgent import EtfArbAgent +from agent.etf.EtfMarketMakerAgent import EtfMarketMakerAgent +from util.order import LimitOrder +from util.oracle.MeanRevertingOracle import MeanRevertingOracle +from util import util + +import numpy as np +import pandas as pd +import sys + + +DATA_DIR = "~/data" + + +# Some config files require additional command line parameters to easily +# control agent or simulation hyperparameters during coarse parallelization. +import argparse + +parser = argparse.ArgumentParser(description='Detailed options for momentum config.') +parser.add_argument('-b', '--book_freq', default=None, + help='Frequency at which to archive order book for visualization') +parser.add_argument('-c', '--config', required=True, + help='Name of config file to execute') +parser.add_argument('-g', '--greed', type=float, default=0.25, + help='Impact agent greed') +parser.add_argument('-i', '--impact', action='store_false', + help='Do not actually fire an impact trade.') +parser.add_argument('-l', '--log_dir', default=None, + help='Log directory name (default: unix timestamp at program start)') +parser.add_argument('-n', '--obs_noise', type=float, default=1000000, + help='Observation noise variance for zero intelligence agents (sigma^2_n)') +parser.add_argument('-r', '--shock_variance', type=float, default=500000, + help='Shock variance for mean reversion process (sigma^2_s)') +parser.add_argument('-o', '--log_orders', action='store_true', + help='Log every order-related action by every agent.') +parser.add_argument('-s', '--seed', type=int, default=None, + help='numpy.random.seed() for simulation') +parser.add_argument('-v', '--verbose', action='store_true', + help='Maximum verbosity!') +parser.add_argument('--config_help', action='store_true', + help='Print argument options for this config file') + +args, remaining_args = parser.parse_known_args() + +if args.config_help: + parser.print_help() + sys.exit() + +# Historical date to simulate. Required even if not relevant. +historical_date = pd.to_datetime('2014-01-28') + +# Requested log directory. +log_dir = args.log_dir + +# Requested order book snapshot archive frequency. +book_freq = args.book_freq + +# Observation noise variance for zero intelligence agents. +sigma_n = args.obs_noise + +# Shock variance of mean reversion process. +sigma_s = args.shock_variance + +# Impact agent greed. +greed = args.greed + +# Should the impact agent actually trade? +impact = args.impact + +# Random seed specification on the command line. Default: None (by clock). +# If none, we select one via a specific random method and pass it to seed() +# so we can record it for future use. (You cannot reasonably obtain the +# automatically generated seed when seed() is called without a parameter.) + +# Note that this seed is used to (1) make any random decisions within this +# config file itself and (2) to generate random number seeds for the +# (separate) Random objects given to each agent. This ensure that when +# the agent population is appended, prior agents will continue to behave +# in the same manner save for influences by the new agents. (i.e. all prior +# agents still have their own separate PRNG sequence, and it is the same as +# before) + +seed = args.seed +if not seed: seed = int(pd.Timestamp.now().timestamp() * 1000000) % (2**32 - 1) +np.random.seed(seed) + +# Config parameter that causes util.util.print to suppress most output. +# Also suppresses formatting of limit orders (which is time consuming). +util.silent_mode = not args.verbose +LimitOrder.silent_mode = not args.verbose + +# Config parameter that causes every order-related action to be logged by +# every agent. Activate only when really needed as there is a significant +# time penalty to all that object serialization! +log_orders = args.log_orders + + +print ("Silent mode: {}".format(util.silent_mode)) +print ("Logging orders: {}".format(log_orders)) +print ("Book freq: {}".format(book_freq)) +print ("ZeroIntelligenceAgent noise: {:0.4f}".format(sigma_n)) +print ("ImpactAgent greed: {:0.2f}".format(greed)) +print ("ImpactAgent firing: {}".format(impact)) +print ("Shock variance: {:0.4f}".format(sigma_s)) +print ("Configuration seed: {}\n".format(seed)) + + + +# Since the simulator often pulls historical data, we use a real-world +# nanosecond timestamp (pandas.Timestamp) for our discrete time "steps", +# which are considered to be nanoseconds. For other (or abstract) time +# units, one can either configure the Timestamp interval, or simply +# interpret the nanoseconds as something else. + +# What is the earliest available time for an agent to act during the +# simulation? +midnight = historical_date +kernelStartTime = midnight + +# When should the Kernel shut down? (This should be after market close.) +# Here we go for 8:00 PM the same day to reflect the ETF primary market +kernelStopTime = midnight + pd.to_timedelta('20:00:00') + +# This will configure the kernel with a default computation delay +# (time penalty) for each agent's wakeup and recvMsg. An agent +# can change this at any time for itself. (nanoseconds) +defaultComputationDelay = 0 # no delay for this config + + +# IMPORTANT NOTE CONCERNING AGENT IDS: the id passed to each agent must: +# 1. be unique +# 2. equal its index in the agents list +# This is to avoid having to call an extra getAgentListIndexByID() +# in the kernel every single time an agent must be referenced. + + +# This is a list of symbols the exchange should trade. It can handle any number. +# It keeps a separate order book for each symbol. The example data includes +# only IBM. This config uses generated data, so the symbol doesn't really matter. + +# If shock variance must differ for each traded symbol, it can be overridden here. +symbols = { 'SYM1' : { 'r_bar' : 100000, 'kappa' : 0.05, 'sigma_s' : sigma_s }, 'SYM2': { 'r_bar' : 150000, 'kappa' : 0.05, 'sigma_s' : sigma_s } , 'ETF' : { 'r_bar' : 250000, 'kappa' : 0.10, 'sigma_s' : np.sqrt(2) * sigma_s, 'portfolio': ['SYM1', 'SYM2']}} +#symbols = { 'IBM' : { 'r_bar' : 100000, 'kappa' : 0.05, 'sigma_s' : sigma_s }, 'GOOG' : { 'r_bar' : 150000, 'kappa' : 0.05, 'sigma_s' : sigma_s } } +symbols_full = symbols.copy() + +#seed=np.random.randint(low=0,high=2**32) +#seed = 2000 + +### Configure the Kernel. +kernel = Kernel("Base Kernel", random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) + + + +### Configure the agents. When conducting "agent of change" experiments, the +### new agents should be added at the END only. +agent_count = 0 +agents = [] +agent_types = [] + + +### Configure an exchange agent. + +# Let's open the exchange at 9:30 AM. +mkt_open = midnight + pd.to_timedelta('09:30:00') + +# And close it at 9:30:00.000001 (i.e. 1,000 nanoseconds or "time steps") +#mkt_close = midnight + pd.to_timedelta('09:30:00.001') +mkt_close = midnight + pd.to_timedelta('9:30:00.000001') + + +# Configure an appropriate oracle for all traded stocks. +# All agents requiring the same type of Oracle will use the same oracle instance. +oracle = MeanRevertingOracle(mkt_open, mkt_close, symbols) + + +# Create the exchange. +num_exchanges = 1 +agents.extend([ ExchangeAgent(j, "Exchange Agent {}".format(j), "ExchangeAgent", mkt_open, mkt_close, [s for s in symbols_full], log_orders=log_orders, book_freq=book_freq, pipeline_delay = 0, computation_delay = 0, stream_history = 10, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) + for j in range(agent_count, agent_count + num_exchanges) ]) +agent_types.extend(["ExchangeAgent" for j in range(num_exchanges)]) +agent_count += num_exchanges + + +# Let's open the exchange at 5:00 PM. +prime_open = midnight + pd.to_timedelta('17:00:00') + +# And close it at 5:00:01 PM +prime_close = midnight + pd.to_timedelta('17:00:01') + +# Create the primary. +num_primes = 1 +agents.extend([ EtfPrimaryAgent(j, "ETF Primary Agent {}".format(j), "EtfPrimaryAgent", prime_open, prime_close, 'ETF', pipeline_delay = 0, computation_delay = 0, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) + for j in range(agent_count, agent_count + num_primes) ]) +agent_types.extend(["EtfPrimeAgent" for j in range(num_primes)]) +agent_count += num_primes + + +### Configure some zero intelligence agents. + +# Cash in this simulator is always in CENTS. +starting_cash = 10000000 + +# Here are the zero intelligence agents. +symbol1 = 'SYM1' +symbol2 = 'SYM2' +symbol3 = 'ETF' +print(symbols_full) +s1 = symbols_full[symbol1] +s2 = symbols_full[symbol2] +s3 = symbols_full[symbol3] + +# Tuples are: (# agents, R_min, R_max, eta, L). L for HBL only. + +# Some configs for ZI agents only (among seven parameter settings). + +# 4 agents +#zi = [ (1, 0, 250, 1), (1, 0, 500, 1), (1, 0, 1000, 0.8), (1, 0, 1000, 1), (0, 0, 2000, 0.8), (0, 250, 500, 0.8), (0, 250, 500, 1) ] +#hbl = [] + +# 28 agents +#zi = [ (4, 0, 250, 1), (4, 0, 500, 1), (4, 0, 1000, 0.8), (4, 0, 1000, 1), (4, 0, 2000, 0.8), (4, 250, 500, 0.8), (4, 250, 500, 1) ] +#hbl = [] + +# 65 agents +#zi = [ (10, 0, 250, 1), (10, 0, 500, 1), (9, 0, 1000, 0.8), (9, 0, 1000, 1), (9, 0, 2000, 0.8), (9, 250, 500, 0.8), (9, 250, 500, 1) ] +#hbl = [] + +# 100 agents +#zi = [ (15, 0, 250, 1), (15, 0, 500, 1), (14, 0, 1000, 0.8), (14, 0, 1000, 1), (14, 0, 2000, 0.8), (14, 250, 500, 0.8), (14, 250, 500, 1) ] +#hbl = [] + +# 1000 agents +#zi = [ (143, 0, 250, 1), (143, 0, 500, 1), (143, 0, 1000, 0.8), (143, 0, 1000, 1), (143, 0, 2000, 0.8), (143, 250, 500, 0.8), (142, 250, 500, 1) ] +#hbl = [] + +# 10000 agents +#zi = [ (1429, 0, 250, 1), (1429, 0, 500, 1), (1429, 0, 1000, 0.8), (1429, 0, 1000, 1), (1428, 0, 2000, 0.8), (1428, 250, 500, 0.8), (1428, 250, 500, 1) ] +#hbl = [] + + +# Some configs for HBL agents only (among four parameter settings). + +# 4 agents +#zi = [] +#hbl = [ (1, 250, 500, 1, 2), (1, 250, 500, 1, 3), (1, 250, 500, 1, 5), (1, 250, 500, 1, 8) ] + +# 28 agents +#zi = [] +#hbl = [ (7, 250, 500, 1, 2), (7, 250, 500, 1, 3), (7, 250, 500, 1, 5), (7, 250, 500, 1, 8) ] + +# 1000 agents +#zi = [] +#hbl = [ (250, 250, 500, 1, 2), (250, 250, 500, 1, 3), (250, 250, 500, 1, 5), (250, 250, 500, 1, 8) ] + + +# Some configs that mix both types of agents. + +# 28 agents +#zi = [ (3, 0, 250, 1), (3, 0, 500, 1), (3, 0, 1000, 0.8), (3, 0, 1000, 1), (3, 0, 2000, 0.8), (3, 250, 500, 0.8), (2, 250, 500, 1) ] +#hbl = [ (2, 250, 500, 1, 2), (2, 250, 500, 1, 3), (2, 250, 500, 1, 5), (2, 250, 500, 1, 8) ] + +# 65 agents +#zi = [ (7, 0, 250, 1), (7, 0, 500, 1), (7, 0, 1000, 0.8), (7, 0, 1000, 1), (7, 0, 2000, 0.8), (7, 250, 500, 0.8), (7, 250, 500, 1) ] +#hbl = [ (4, 250, 500, 1, 2), (4, 250, 500, 1, 3), (4, 250, 500, 1, 5), (4, 250, 500, 1, 8) ] + +# 1000 agents +zi = [ (100, 0, 250, 1), (100, 0, 500, 1), (100, 0, 10000, 0.8), (100, 0, 10000, 1), (100, 0, 2000, 0.8), (100, 250, 500, 0.8), (100, 250, 500, 1) ] +hbl = [ (75, 250, 500, 1, 2), (75, 250, 500, 1, 3), (75, 250, 500, 1, 5), (75, 250, 500, 1, 8) ] + + + +# ZI strategy split. +for i,x in enumerate(zi): + strat_name = "Type {} [{} <= R <= {}, eta={}]".format(i+1, x[1], x[2], x[3]) + agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)),log_orders=log_orders, symbol=symbol1, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s1['r_bar'], q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "ZeroIntelligenceAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +for i,x in enumerate(zi): + strat_name = "Type {} [{} <= R <= {}, eta={}]".format(i+1, x[1], x[2], x[3]) + agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)),log_orders=log_orders, symbol=symbol2, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s2['r_bar'], q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "ZeroIntelligenceAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +for i,x in enumerate(zi): + strat_name = "Type {} [{} <= R <= {}, eta={}]".format(i+1, x[1], x[2], x[3]) + agents.extend([ ZeroIntelligenceAgent(j, "ZI Agent {} {}".format(j, strat_name), "ZeroIntelligenceAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)),log_orders=log_orders, symbol=symbol3, starting_cash=starting_cash, sigma_n=sigma_n, portfolio = {'SYM1':s1['r_bar'], 'SYM2': s2['r_bar']}, q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "ZeroIntelligenceAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + + +# HBL strategy split. +for i,x in enumerate(hbl): + strat_name = "Type {} [{} <= R <= {}, eta={}, L={}]".format(i+1, x[1], x[2], x[3], x[4]) + agents.extend([ HeuristicBeliefLearningAgent(j, "HBL Agent {} {}".format(j, strat_name), "HeuristicBeliefLearningAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)), log_orders=log_orders, symbol=symbol1, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s1['r_bar'], q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005, L=x[4]) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "HeuristicBeliefLearningAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +for i,x in enumerate(hbl): + strat_name = "Type {} [{} <= R <= {}, eta={}, L={}]".format(i+1, x[1], x[2], x[3], x[4]) + agents.extend([ HeuristicBeliefLearningAgent(j, "HBL Agent {} {}".format(j, strat_name), "HeuristicBeliefLearningAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)), log_orders=log_orders, symbol=symbol2, starting_cash=starting_cash, sigma_n=sigma_n, r_bar=s2['r_bar'], q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005, L=x[4]) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "HeuristicBeliefLearningAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +for i,x in enumerate(hbl): + strat_name = "Type {} [{} <= R <= {}, eta={}, L={}]".format(i+1, x[1], x[2], x[3], x[4]) + agents.extend([ HeuristicBeliefLearningAgent(j, "HBL Agent {} {}".format(j, strat_name), "HeuristicBeliefLearningAgent {}".format(strat_name), random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)), log_orders=log_orders, symbol=symbol3, starting_cash=starting_cash, sigma_n=sigma_n, portfolio = {'SYM1':s1['r_bar'], 'SYM2': s2['r_bar']}, q_max=10, sigma_pv=5000000, R_min=x[1], R_max=x[2], eta=x[3], lambda_a=0.005, L=x[4]) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "HeuristicBeliefLearningAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +# Trend followers agent +i = agent_count +lookback = 10 +num_tf = 20 +for j in range(num_tf): + agents.append(MomentumAgent(i, "Momentum Agent {}".format(i), symbol=symbol1, starting_cash = starting_cash, lookback=lookback, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)), log_orders = log_orders)) + agent_types.append("MomentumAgent {}".format(i)) + i+=1 +agent_count += num_tf + +#for j in range(num_tf): + #agents.append(MomentumAgent(i, "Momentum Agent {}".format(i), symbol=symbol2, startingCash = starting_cash, lookback=lookback)) + #agent_types.append("MomentumAgent {}".format(i)) + #i+=1 +#agent_count += num_tf + +#for j in range(num_tf): + #agents.append(MomentumAgent(i, "Momentum Agent {}".format(i), symbol=symbol3, startingCash = starting_cash, lookback=lookback)) + #agent_types.append("MomentumAgent {}".format(i)) + #i+=1 +#agent_count += num_tf + +# ETF arbitrage agent +i = agent_count +gamma = 0 +num_arb = 25 +for j in range(num_arb): + agents.append(EtfArbAgent(i, "Etf Arb Agent {}".format(i), "EtfArbAgent", portfolio = ['SYM1','SYM2'], gamma = gamma, starting_cash = starting_cash, lambda_a=0.005, log_orders=log_orders, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)))) + agent_types.append("EtfArbAgent {}".format(i)) + i+=1 +agent_count += num_arb + +# ETF market maker agent +#i = agent_count +#gamma = 100 +#num_mm = 10 +mm = [(5,0),(5,50),(5,100),(5,200),(5,300)] +#for j in range(num_mm): + #agents.append(EtfMarketMakerAgent(i, "Etf MM Agent {}".format(i), "EtfMarketMakerAgent", portfolio = ['IBM','GOOG'], gamma = gamma, starting_cash = starting_cash, lambda_a=0.005, log_orders=log_orders, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)))) + #agent_types.append("EtfMarketMakerAgent {}".format(i)) + #i+=1 +#agent_count += num_mm + +for i,x in enumerate(mm): + strat_name = "Type {} [gamma = {}]".format(i+1, x[1]) + print(strat_name) + agents.extend([ EtfMarketMakerAgent(j, "Etf MM Agent {} {}".format(j, strat_name), "EtfMarketMakerAgent {}".format(strat_name), portfolio = ['SYM1','SYM2'], gamma = x[1], starting_cash = starting_cash, lambda_a=0.005, log_orders=log_orders, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32))) for j in range(agent_count,agent_count+x[0]) ]) + agent_types.extend([ "EtfMarketMakerAgent {}".format(strat_name) for j in range(x[0]) ]) + agent_count += x[0] + +# Impact agent. + +# 200 time steps in... +impact_time = midnight + pd.to_timedelta('09:30:00.0000002') + +i = agent_count +agents.append(ImpactAgent(i, "Impact Agent1 {}".format(i), "ImpactAgent1", symbol = "SYM1", starting_cash = starting_cash, impact = impact, impact_time = impact_time, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)))) +agent_types.append("ImpactAgent 1 {}".format(i)) +agent_count += 1 + +impact_time = midnight + pd.to_timedelta('09:30:00.0000005') +i = agent_count +agents.append(ImpactAgent(i, "Impact Agent2 {}".format(i), "ImpactAgent2", symbol = "SYM1", starting_cash = starting_cash, impact = impact, impact_time = impact_time, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)))) +agent_types.append("ImpactAgent 2 {}".format(i)) +agent_count += 1 + +#i = agent_count +#agents.append(ImpactAgent(i, "Impact Agent3 {}".format(i), "ImpactAgent3", symbol = "ETF", starting_cash = starting_cash, greed = greed, impact = impact, impact_time = impact_time, random_state = np.random.RandomState(seed=np.random.randint(low=0,high=2**32)))) +#agent_types.append("ImpactAgent 3 {}".format(i)) +#agent_count += 1 + +### Configure a simple message latency matrix for the agents. Each entry is the minimum +### nanosecond delay on communication [from][to] agent ID. + +# Square numpy array with dimensions equal to total agent count. Most agents are handled +# at init, drawn from a uniform distribution from: +# Times Square (3.9 miles from NYSE, approx. 21 microseconds at the speed of light) to: +# Pike Place Starbucks in Seattle, WA (2402 miles, approx. 13 ms at the speed of light). +# Other agents can be explicitly set afterward (and the mirror half of the matrix is also). + +# This configures all agents to a starting latency as described above. +#latency = np.random.uniform(low = 21000, high = 13000000, size=(len(agent_types),len(agent_types))) +latency = np.random.uniform(low = 10, high = 100, size=(len(agent_types),len(agent_types))) + +# Overriding the latency for certain agent pairs happens below, as does forcing mirroring +# of the matrix to be symmetric. +for i, t1 in zip(range(latency.shape[0]), agent_types): + for j, t2 in zip(range(latency.shape[1]), agent_types): + # Three cases for symmetric array. Set latency when j > i, copy it when i > j, same agent when i == j. + if j > i: + # Arb agents should be the fastest in the market. + if (("ExchangeAgent" in t1 and "EtfArbAgent" in t2) + or ("ExchangeAgent" in t2 and "EtfArbAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 5 + elif (("ExchangeAgent" in t1 and "EtfMarketMakerAgent" in t2) + or ("ExchangeAgent" in t2 and "EtfMarketMakerAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 1 + elif (("ExchangeAgent" in t1 and "ImpactAgent" in t2) + or ("ExchangeAgent" in t2 and "ImpactAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 1 + + elif i > j: + # This "bottom" half of the matrix simply mirrors the top. + if (("ExchangeAgent" in t1 and "EtfArbAgent" in t2) + or ("ExchangeAgent" in t2 and "EtfArbAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 5 + elif (("ExchangeAgent" in t1 and "EtfMarketMakerAgent" in t2) + or ("ExchangeAgent" in t2 and "EtfMarketMakerAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 1 + elif (("ExchangeAgent" in t1 and "ImpactAgent" in t2) + or ("ExchangeAgent" in t2 and "ImpactAgent" in t1)): + #latency[i,j] = 20000 + latency[i,j] = 1 + else: latency[i,j] = latency[j,i] + else: + # This is the same agent. How long does it take to reach localhost? In our data center, it actually + # takes about 20 microseconds. + #latency[i,j] = 10000 + latency[i,j] = 1 + +# Configure a simple latency noise model for the agents. +# Index is ns extra delay, value is probability of this delay being applied. +# In this config, there is no latency (noisy or otherwise). +noise = [ 0.0 ] + + + +# Start the kernel running. +kernel.runner(agents = agents, startTime = kernelStartTime, + stopTime = kernelStopTime, agentLatency = latency, + latencyNoise = noise, + defaultComputationDelay = defaultComputationDelay, + oracle = oracle, log_dir = log_dir) + diff --git a/contributed_traders/SimpleAgent.py b/contributed_traders/SimpleAgent.py new file mode 100644 index 000000000..b100769c1 --- /dev/null +++ b/contributed_traders/SimpleAgent.py @@ -0,0 +1,60 @@ +from agent.TradingAgent import TradingAgent +import pandas as pd +import numpy as np + + +class SimpleAgent(TradingAgent): + """ + Simple Trading Agent that compares the past mid-price observations and places a + buy limit order if the first window mid-price exponential average >= the second window mid-price exponential average or a + sell limit order if the first window mid-price exponential average < the second window mid-price exponential average + """ + + def __init__(self, id, name, type, symbol, starting_cash, + min_size, max_size, wake_up_freq='60s', + log_orders=False, random_state=None): + + super().__init__(id, name, type, starting_cash=starting_cash, log_orders=log_orders, random_state=random_state) + self.symbol = symbol + self.min_size = min_size # Minimum order size + self.max_size = max_size # Maximum order size + self.size = self.random_state.randint(self.min_size, self.max_size) + self.wake_up_freq = wake_up_freq + self.mid_list, self.avg_win1_list, self.avg_win2_list = [], [], [] + self.log_orders = log_orders + self.state = "AWAITING_WAKEUP" + self.window1 = 100 + self.window2 = 5 + + def kernelStarting(self, startTime): + super().kernelStarting(startTime) + + def wakeup(self, currentTime): + """ Agent wakeup is determined by self.wake_up_freq """ + can_trade = super().wakeup(currentTime) + if not can_trade: return + self.getCurrentSpread(self.symbol) + self.state = 'AWAITING_SPREAD' + + def receiveMessage(self, currentTime, msg): + """ Momentum agent actions are determined after obtaining the best bid and ask in the LOB """ + super().receiveMessage(currentTime, msg) + if self.state == 'AWAITING_SPREAD' and msg.body['msg'] == 'QUERY_SPREAD': + bid, _, ask, _ = self.getKnownBidAsk(self.symbol) + if bid and ask: + self.mid_list.append((bid + ask) / 2) + if len(self.mid_list) > self.window1: self.avg_win1_list.append(pd.Series(self.mid_list).ewm(span=self.window1).mean().values[-1].round(2)) + if len(self.mid_list) > self.window2: self.avg_win2_list.append(pd.Series(self.mid_list).ewm(span=self.window2).mean().values[-1].round(2)) + if len(self.avg_win1_list) > 0 and len(self.avg_win2_list) > 0: + if self.avg_win1_list[-1] >= self.avg_win2_list[-1]: + # Check that we have enough cash to place the order + if self.holdings['CASH'] >= (self.size * ask): + self.placeLimitOrder(self.symbol, quantity=self.size, is_buy_order=True, limit_price=ask) + else: + if self.symbol in self.holdings and self.holdings[self.symbol] > 0: + self.placeLimitOrder(self.symbol, quantity=self.size, is_buy_order=False, limit_price=bid) + self.setWakeup(currentTime + self.getWakeFrequency()) + self.state = 'AWAITING_WAKEUP' + + def getWakeFrequency(self): + return pd.Timedelta(self.wake_up_freq) diff --git a/contributed_traders/__init__.py b/contributed_traders/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/data/random_trades.bgz b/data/random_trades.bgz deleted file mode 100644 index d06791075..000000000 Binary files a/data/random_trades.bgz and /dev/null differ diff --git a/data/sample_orders_file.csv b/data/sample_orders_file.csv new file mode 100644 index 000000000..811a66cce --- /dev/null +++ b/data/sample_orders_file.csv @@ -0,0 +1,11 @@ +TIMESTAMP,ORDER_ID,PRICE,SIZE,BUY_SELL_FLAG +20190603093000.028558000,100001,101,100,SELL +20190603093000.028558000,100002,100,100,BUY +20190603093000.512880000,100003,102,100,SELL +20190603093002.253224000,100004,101,100,BUY +20190603093009.342659000,100005,102,100,SELL +20190603093009.520242000,100001,101,0,BUY +20190603093022.367835000,100007,103,100,BUY +20190603093022.367987000,100003,100,20,SELL +20190603093036.927515000,100009,100,100,SELL +20190603093036.927660000,100005,102,0,SELL diff --git a/scripts/book.sh b/scripts/book.sh old mode 100644 new mode 100755 diff --git a/scripts/capture_profile.sh b/scripts/capture_profile.sh old mode 100644 new mode 100755 diff --git a/scripts/dump.sh b/scripts/dump.sh old mode 100644 new mode 100755 diff --git a/scripts/hardware_macos.sh b/scripts/hardware_macos.sh new file mode 100644 index 000000000..32721f462 --- /dev/null +++ b/scripts/hardware_macos.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +system_profiler SPHardwareDataType diff --git a/scripts/hardware_ubuntu.sh b/scripts/hardware_ubuntu.sh new file mode 100644 index 000000000..359809e3a --- /dev/null +++ b/scripts/hardware_ubuntu.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +sudo lshw diff --git a/scripts/impact_baseline.sh b/scripts/impact_baseline.sh old mode 100644 new mode 100755 diff --git a/scripts/impact_study.sh b/scripts/impact_study.sh old mode 100644 new mode 100755 diff --git a/scripts/marketreplay.sh b/scripts/marketreplay.sh new file mode 100755 index 000000000..0d3a9392f --- /dev/null +++ b/scripts/marketreplay.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python -u abides.py -c marketreplay -l marketreplay -s 123456789 \ No newline at end of file diff --git a/scripts/rmsc.sh b/scripts/rmsc.sh new file mode 100755 index 000000000..6cb2f0d0a --- /dev/null +++ b/scripts/rmsc.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +seed=123456789 +config=rmsc01 +log=rmsc01 + +if [ $# -eq 0 ]; then + python -u abides.py -c $config -l $log -s $seed +else + agent=$1 + python -u abides.py -c $config -l $log -s $seed -a $agent +fi + diff --git a/scripts/sparse_zi.sh b/scripts/sparse_zi.sh new file mode 100755 index 000000000..857eaf07e --- /dev/null +++ b/scripts/sparse_zi.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +python -u abides.py -c sparse_zi_1000 -l sparse_zi_1000 -b 'all' -s 123456789 diff --git a/scripts/sparse_zi_study.sh b/scripts/sparse_zi_study.sh deleted file mode 100644 index 91eb2f6af..000000000 --- a/scripts/sparse_zi_study.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -if [ $# -eq 0 ]; then - echo $0: 'usage: sparse_zi_study.sh ' - exit 1 -fi - -if [ $# -ge 2 ]; then - echo $0: 'usage: sparse_zi_study.sh ' - exit 1 -fi - -if [ $# -eq 1 ]; then - count=$1 - dt=`date +%s` - for i in `seq 1 ${count}`; - do - echo "Launching simulation $i" - python -u abides.py -c sparse_zi -l sparse_zi_${i} -s ${i} > ./batch_output/sparse_zi_${i} & - sleep 0.5 - done -fi diff --git a/scripts/stats.sh b/scripts/stats.sh old mode 100644 new mode 100755 diff --git a/scripts/timeit.sh b/scripts/timeit.sh old mode 100644 new mode 100755 diff --git a/tests/rmsc01.txt b/tests/rmsc01.txt new file mode 100644 index 000000000..608422bbc --- /dev/null +++ b/tests/rmsc01.txt @@ -0,0 +1,129 @@ +--------------- +Hardware Specs: +--------------- +Mac (system_profiler SPHardwareDataType) or lshw -short + +Processor Name: Intel Core i7 +Processor Speed: 2.6 GHz +Number of Processors: 1 +Total Number of Cores: 6 +L2 Cache (per Core): 256 KB +L3 Cache: 12 MB +Memory: 16 GB + +------------------ +Simulation Results: +------------------ +./scripts/rmsc.sh + +Configuration seed: 123456789 + +Event Queue elapsed: 0 days 00:00:21.622589, messages: 128918, messages per second: 5962.2 +Mean ending value by agent type: + MarketMakerAgent: 7126691 + ZeroIntelligenceAgent: 120720 + HeuristicBeliefLearningAgent: 194437 + MomentumAgent: -779277 + +Time taken to run simulation: 0:00:25.482590 + +Final holdings for MARKET_MAKER_AGENT_1: { JPM: -1305, CASH: 145973256 }. Marked to market: 17126691 +Final holdings for ZI_AGENT_2: { JPM: 181, CASH: -7659044 }. Marked to market: 10250544 +Final holdings for ZI_AGENT_3: { JPM: 600, CASH: -49023600 }. Marked to market: 10216200 +Final holdings for ZI_AGENT_4: { JPM: -100, CASH: 20394600 }. Marked to market: 10526200 +Final holdings for ZI_AGENT_5: { JPM: -400, CASH: 50004600 }. Marked to market: 10497000 +Final holdings for ZI_AGENT_6: { JPM: -200, CASH: 29967600 }. Marked to market: 10178000 +Final holdings for ZI_AGENT_7: { JPM: 100, CASH: -276200 }. Marked to market: 9592500 +Final holdings for ZI_AGENT_8: { JPM: -17, CASH: 12552041 }. Marked to market: 10880380 +Final holdings for ZI_AGENT_9: { CASH: 10119400 }. Marked to market: 10119400 +Final holdings for ZI_AGENT_10: { CASH: 10135500 }. Marked to market: 10135500 +Final holdings for ZI_AGENT_11: { JPM: 116, CASH: -1245665 }. Marked to market: 10205623 +Final holdings for ZI_AGENT_12: { JPM: 416, CASH: -31561819 }. Marked to market: 9487813 +Final holdings for ZI_AGENT_13: { JPM: 200, CASH: -10251600 }. Marked to market: 9453000 +Final holdings for ZI_AGENT_14: { JPM: -67, CASH: 16487854 }. Marked to market: 9861085 +Final holdings for ZI_AGENT_15: { JPM: -100, CASH: 20399900 }. Marked to market: 10531500 +Final holdings for ZI_AGENT_16: { JPM: 399, CASH: -29531239 }. Marked to market: 9836096 +Final holdings for ZI_AGENT_17: { JPM: -200, CASH: 29664700 }. Marked to market: 9960100 +Final holdings for ZI_AGENT_18: { JPM: 200, CASH: -9828600 }. Marked to market: 9876000 +Final holdings for ZI_AGENT_19: { JPM: -300, CASH: 40180700 }. Marked to market: 10496300 +Final holdings for ZI_AGENT_20: { JPM: 200, CASH: -10200600 }. Marked to market: 9537000 +Final holdings for ZI_AGENT_21: { JPM: 200, CASH: -9482362 }. Marked to market: 10264238 +Final holdings for ZI_AGENT_22: { JPM: 417, CASH: -31171407 }. Marked to market: 9833454 +Final holdings for ZI_AGENT_23: { JPM: 414, CASH: -31672932 }. Marked to market: 9183486 +Final holdings for ZI_AGENT_24: { JPM: -500, CASH: 60487600 }. Marked to market: 11155100 +Final holdings for ZI_AGENT_25: { JPM: 200, CASH: -9887300 }. Marked to market: 9884300 +Final holdings for ZI_AGENT_26: { JPM: -300, CASH: 40445800 }. Marked to market: 10945900 +Final holdings for ZI_AGENT_27: { JPM: -100, CASH: 20310600 }. Marked to market: 10481500 +Final holdings for ZI_AGENT_28: { CASH: 9815600 }. Marked to market: 9815600 +Final holdings for ZI_AGENT_29: { JPM: -500, CASH: 60540600 }. Marked to market: 11208100 +Final holdings for ZI_AGENT_30: { JPM: 600, CASH: -49555100 }. Marked to market: 9707500 +Final holdings for ZI_AGENT_31: { JPM: 412, CASH: -31271686 }. Marked to market: 9406310 +Final holdings for ZI_AGENT_32: { JPM: -199, CASH: 30177597 }. Marked to market: 10393415 +Final holdings for ZI_AGENT_33: { JPM: -400, CASH: 50074500 }. Marked to market: 10600900 +Final holdings for ZI_AGENT_34: { JPM: -482, CASH: 58343840 }. Marked to market: 10650904 +Final holdings for ZI_AGENT_35: { CASH: 9918300 }. Marked to market: 9918300 +Final holdings for ZI_AGENT_36: { JPM: -200, CASH: 30114200 }. Marked to market: 10409400 +Final holdings for ZI_AGENT_37: { JPM: -67, CASH: 16485869 }. Marked to market: 9870758 +Final holdings for ZI_AGENT_38: { JPM: 200, CASH: -9724200 }. Marked to market: 9942400 +Final holdings for ZI_AGENT_39: { JPM: -301, CASH: 39807427 }. Marked to market: 10209194 +Final holdings for ZI_AGENT_40: { JPM: 215, CASH: -10843459 }. Marked to market: 10298136 +Final holdings for ZI_AGENT_41: { JPM: 317, CASH: -21181937 }. Marked to market: 10101842 +Final holdings for ZI_AGENT_42: { JPM: 200, CASH: -9821500 }. Marked to market: 9915300 +Final holdings for ZI_AGENT_43: { JPM: 351, CASH: -24150317 }. Marked to market: 10431256 +Final holdings for ZI_AGENT_44: { JPM: -234, CASH: 33620912 }. Marked to market: 10583846 +Final holdings for ZI_AGENT_45: { JPM: -299, CASH: 39539517 }. Marked to market: 9983068 +Final holdings for ZI_AGENT_46: { JPM: 100, CASH: -150600 }. Marked to market: 9744200 +Final holdings for ZI_AGENT_47: { JPM: -199, CASH: 30509360 }. Marked to market: 10964575 +Final holdings for ZI_AGENT_48: { JPM: 401, CASH: -29340394 }. Marked to market: 10301664 +Final holdings for ZI_AGENT_49: { JPM: 513, CASH: -41111357 }. Marked to market: 9311926 +Final holdings for ZI_AGENT_50: { JPM: -34, CASH: 13187232 }. Marked to market: 9834084 +Final holdings for ZI_AGENT_51: { JPM: 499, CASH: -40215161 }. Marked to market: 9045121 +Final holdings for HBL_AGENT_52: { JPM: -300, CASH: 40322700 }. Marked to market: 10723200 +Final holdings for HBL_AGENT_53: { JPM: 500, CASH: -39778400 }. Marked to market: 9554100 +Final holdings for HBL_AGENT_54: { CASH: 10408637 }. Marked to market: 10408637 +Final holdings for HBL_AGENT_55: { JPM: -601, CASH: 70577971 }. Marked to market: 11248453 +Final holdings for HBL_AGENT_56: { JPM: -100, CASH: 20418600 }. Marked to market: 10549800 +Final holdings for HBL_AGENT_57: { JPM: -384, CASH: 48182600 }. Marked to market: 10202312 +Final holdings for HBL_AGENT_58: { JPM: 201, CASH: -9955167 }. Marked to market: 9897603 +Final holdings for HBL_AGENT_59: { JPM: 97, CASH: 616707 }. Marked to market: 10189443 +Final holdings for HBL_AGENT_60: { JPM: 99, CASH: 269174 }. Marked to market: 10004141 +Final holdings for HBL_AGENT_61: { JPM: 3, CASH: 9543309 }. Marked to market: 9840153 +Final holdings for HBL_AGENT_62: { JPM: 100, CASH: 246400 }. Marked to market: 10075500 +Final holdings for HBL_AGENT_63: { JPM: 3, CASH: 9679226 }. Marked to market: 9974099 +Final holdings for HBL_AGENT_64: { JPM: 134, CASH: -2849042 }. Marked to market: 10353040 +Final holdings for HBL_AGENT_65: { JPM: -232, CASH: 33458626 }. Marked to market: 10523570 +Final holdings for HBL_AGENT_66: { JPM: -300, CASH: 39559600 }. Marked to market: 9902200 +Final holdings for HBL_AGENT_67: { CASH: 10150200 }. Marked to market: 10150200 +Final holdings for HBL_AGENT_68: { JPM: -100, CASH: 19557100 }. Marked to market: 9723800 +Final holdings for HBL_AGENT_69: { JPM: -121, CASH: 22721058 }. Marked to market: 10778116 +Final holdings for HBL_AGENT_70: { JPM: -631, CASH: 72800224 }. Marked to market: 10509166 +Final holdings for HBL_AGENT_71: { JPM: -28, CASH: 12904744 }. Marked to market: 10146100 +Final holdings for HBL_AGENT_72: { JPM: 214, CASH: -11092596 }. Marked to market: 10012512 +Final holdings for HBL_AGENT_73: { JPM: 231, CASH: -12711682 }. Marked to market: 10084322 +Final holdings for HBL_AGENT_74: { JPM: 300, CASH: -19988400 }. Marked to market: 9627000 +Final holdings for HBL_AGENT_75: { JPM: -400, CASH: 50197168 }. Marked to market: 10692768 +Final holdings for HBL_AGENT_76: { JPM: 396, CASH: -29401648 }. Marked to market: 9690680 +Final holdings for MOMENTUM_AGENT_77: { CASH: 8679226 }. Marked to market: 8679226 +Final holdings for MOMENTUM_AGENT_78: { CASH: 9056590 }. Marked to market: 9056590 +Final holdings for MOMENTUM_AGENT_79: { CASH: 9622636 }. Marked to market: 9622636 +Final holdings for MOMENTUM_AGENT_80: { CASH: 9056590 }. Marked to market: 9056590 +Final holdings for MOMENTUM_AGENT_81: { CASH: 9433954 }. Marked to market: 9433954 +Final holdings for MOMENTUM_AGENT_82: { CASH: 9245272 }. Marked to market: 9245272 +Final holdings for MOMENTUM_AGENT_83: { CASH: 8490544 }. Marked to market: 8490544 +Final holdings for MOMENTUM_AGENT_84: { CASH: 9245272 }. Marked to market: 9245272 +Final holdings for MOMENTUM_AGENT_85: { CASH: 9811318 }. Marked to market: 9811318 +Final holdings for MOMENTUM_AGENT_86: { CASH: 9811318 }. Marked to market: 9811318 +Final holdings for MOMENTUM_AGENT_87: { CASH: 9433954 }. Marked to market: 9433954 +Final holdings for MOMENTUM_AGENT_88: { CASH: 9811318 }. Marked to market: 9811318 +Final holdings for MOMENTUM_AGENT_89: { CASH: 9245272 }. Marked to market: 9245272 +Final holdings for MOMENTUM_AGENT_90: { CASH: 9056590 }. Marked to market: 9056590 +Final holdings for MOMENTUM_AGENT_91: { CASH: 8679226 }. Marked to market: 8679226 +Final holdings for MOMENTUM_AGENT_92: { CASH: 8679226 }. Marked to market: 8679226 +Final holdings for MOMENTUM_AGENT_93: { CASH: 8477952 }. Marked to market: 8477952 +Final holdings for MOMENTUM_AGENT_94: { CASH: 9809701 }. Marked to market: 9809701 +Final holdings for MOMENTUM_AGENT_95: { CASH: 8858206 }. Marked to market: 8858206 +Final holdings for MOMENTUM_AGENT_96: { CASH: 9429103 }. Marked to market: 9429103 +Final holdings for MOMENTUM_AGENT_97: { JPM: -18, CASH: 10082809 }. Marked to market: 8305615 +Final holdings for MOMENTUM_AGENT_98: { JPM: -6, CASH: 10027603 }. Marked to market: 9435205 +Final holdings for MOMENTUM_AGENT_99: { JPM: -2, CASH: 10009201 }. Marked to market: 9811735 +Final holdings for MOMENTUM_AGENT_100: { JPM: -2, CASH: 10008984 }. Marked to market: 9811518 \ No newline at end of file diff --git a/tests/sparse_zi_100.txt b/tests/sparse_zi_100.txt new file mode 100644 index 000000000..890016c27 --- /dev/null +++ b/tests/sparse_zi_100.txt @@ -0,0 +1,131 @@ +--------------- +Hardware Specs: +--------------- +Mac (system_profiler SPHardwareDataType) or lshw -short + +Processor Name: Intel Core i7 +Processor Speed: 2.6 GHz +Number of Processors: 1 +Total Number of Cores: 6 +L2 Cache (per Core): 256 KB +L3 Cache: 12 MB +Memory: 16 GB + +------------------ +Simulation Results: +------------------ +./scripts/sparse_zi.sh + +ZeroIntelligenceAgent noise: 1000000.0000 +Configuration seed: 123456789 + +Event Queue elapsed: 0 days 00:00:05.209118, messages: 18677, messages per second: 3585.4 +Mean ending value by agent type: + ZeroIntelligenceAgent Type 1 [0 <= R <= 250, eta=1]: -3807 + ZeroIntelligenceAgent Type 2 [0 <= R <= 500, eta=1]: -30933 + ZeroIntelligenceAgent Type 3 [0 <= R <= 1000, eta=0.8]: 81193 + ZeroIntelligenceAgent Type 4 [0 <= R <= 1000, eta=1]: 4221 + ZeroIntelligenceAgent Type 5 [0 <= R <= 2000, eta=0.8]: 28121 + ZeroIntelligenceAgent Type 6 [250 <= R <= 500, eta=0.8]: 37279 + ZeroIntelligenceAgent Type 7 [250 <= R <= 500, eta=1]: -124036 + +Final holdings for ZI Agent 1 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20127200 }. Marked to market: 10052500 +Final holdings for ZI Agent 2 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 50227200 }. Marked to market: 9987600 +Final holdings for ZI Agent 3 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20508700 }. Marked to market: 10448800 +Final holdings for ZI Agent 4 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20275700 }. Marked to market: 9904000 +Final holdings for ZI Agent 5 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39836600 }. Marked to market: 9656900 +Final holdings for ZI Agent 6 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 190800 }. Marked to market: 10250700 +Final holdings for ZI Agent 7 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40159900 }. Marked to market: 9980200 +Final holdings for ZI Agent 8 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10009400 }. Marked to market: 10110400 +Final holdings for ZI Agent 9 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10006400 }. Marked to market: 10006400 +Final holdings for ZI Agent 10 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39896800 }. Marked to market: 9717100 +Final holdings for ZI Agent 11 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19463700 }. Marked to market: 9403800 +Final holdings for ZI Agent 12 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9902300 }. Marked to market: 10217500 +Final holdings for ZI Agent 13 Type 1 [0 <= R <= 250, eta=1]: { JPM: -600, CASH: 70570700 }. Marked to market: 10211300 +Final holdings for ZI Agent 14 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -233000 }. Marked to market: 9826900 +Final holdings for ZI Agent 15 Type 1 [0 <= R <= 250, eta=1]: { JPM: 800, CASH: -70310400 }. Marked to market: 10168800 +Final holdings for ZI Agent 16 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20399100 }. Marked to market: 9780600 +Final holdings for ZI Agent 17 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9596700 }. Marked to market: 9596700 +Final holdings for ZI Agent 18 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 39633600 }. Marked to market: 9453900 +Final holdings for ZI Agent 19 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20349600 }. Marked to market: 9830100 +Final holdings for ZI Agent 20 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19791900 }. Marked to market: 10387800 +Final holdings for ZI Agent 21 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10237700 }. Marked to market: 10237700 +Final holdings for ZI Agent 22 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 135800 }. Marked to market: 10195700 +Final holdings for ZI Agent 23 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10309700 }. Marked to market: 10309700 +Final holdings for ZI Agent 24 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30231700 }. Marked to market: 10111900 +Final holdings for ZI Agent 25 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19861600 }. Marked to market: 9801700 +Final holdings for ZI Agent 26 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10247900 }. Marked to market: 9871900 +Final holdings for ZI Agent 27 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10088800 }. Marked to market: 10031000 +Final holdings for ZI Agent 28 Type 2 [0 <= R <= 500, eta=1]: { JPM: 400, CASH: -30247800 }. Marked to market: 9991800 +Final holdings for ZI Agent 29 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9907700 }. Marked to market: 9907700 +Final holdings for ZI Agent 30 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10027800 }. Marked to market: 10027800 +Final holdings for ZI Agent 31 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 50472400 }. Marked to market: 10232800 +Final holdings for ZI Agent 32 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30336100 }. Marked to market: 10216300 +Final holdings for ZI Agent 33 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20036900 }. Marked to market: 10142800 +Final holdings for ZI Agent 34 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10091800 }. Marked to market: 10028000 +Final holdings for ZI Agent 35 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30080700 }. Marked to market: 9960900 +Final holdings for ZI Agent 36 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19967000 }. Marked to market: 9907100 +Final holdings for ZI Agent 37 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9874900 }. Marked to market: 9874900 +Final holdings for ZI Agent 38 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30105000 }. Marked to market: 9985200 +Final holdings for ZI Agent 39 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 40320200 }. Marked to market: 10140500 +Final holdings for ZI Agent 40 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -8400 }. Marked to market: 10051500 +Final holdings for ZI Agent 41 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10103100 }. Marked to market: 10103100 +Final holdings for ZI Agent 42 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20397300 }. Marked to market: 10337400 +Final holdings for ZI Agent 43 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20144000 }. Marked to market: 10084100 +Final holdings for ZI Agent 44 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20107600 }. Marked to market: 10072100 +Final holdings for ZI Agent 45 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50284100 }. Marked to market: 10044500 +Final holdings for ZI Agent 46 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -10296300 }. Marked to market: 9823500 +Final holdings for ZI Agent 47 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9907700 }. Marked to market: 9907700 +Final holdings for ZI Agent 48 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30148900 }. Marked to market: 10029100 +Final holdings for ZI Agent 49 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19679700 }. Marked to market: 9619800 +Final holdings for ZI Agent 50 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -10197700 }. Marked to market: 9922100 +Final holdings for ZI Agent 51 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30255800 }. Marked to market: 10136000 +Final holdings for ZI Agent 52 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 17500 }. Marked to market: 10027900 +Final holdings for ZI Agent 53 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10142100 }. Marked to market: 10142100 +Final holdings for ZI Agent 54 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19768400 }. Marked to market: 9708500 +Final holdings for ZI Agent 55 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -27100 }. Marked to market: 10032800 +Final holdings for ZI Agent 56 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20117200 }. Marked to market: 10062500 +Final holdings for ZI Agent 57 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19917000 }. Marked to market: 10262700 +Final holdings for ZI Agent 58 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20399800 }. Marked to market: 10339900 +Final holdings for ZI Agent 59 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30038000 }. Marked to market: 9918200 +Final holdings for ZI Agent 60 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40309400 }. Marked to market: 10129700 +Final holdings for ZI Agent 61 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9948200 }. Marked to market: 9948200 +Final holdings for ZI Agent 62 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19918200 }. Marked to market: 9858300 +Final holdings for ZI Agent 63 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: -227200 }. Marked to market: 9832700 +Final holdings for ZI Agent 64 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30055000 }. Marked to market: 9935200 +Final holdings for ZI Agent 65 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 7100 }. Marked to market: 10067000 +Final holdings for ZI Agent 66 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10005200 }. Marked to market: 10114600 +Final holdings for ZI Agent 67 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40102300 }. Marked to market: 9922600 +Final holdings for ZI Agent 68 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30213400 }. Marked to market: 10093600 +Final holdings for ZI Agent 69 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: -26000 }. Marked to market: 10033900 +Final holdings for ZI Agent 70 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -400, CASH: 50254500 }. Marked to market: 10014900 +Final holdings for ZI Agent 71 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9811900 }. Marked to market: 10307900 +Final holdings for ZI Agent 72 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20276800 }. Marked to market: 10216900 +Final holdings for ZI Agent 73 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -30111500 }. Marked to market: 10128100 +Final holdings for ZI Agent 74 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -100900 }. Marked to market: 9959000 +Final holdings for ZI Agent 75 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -28200 }. Marked to market: 9998400 +Final holdings for ZI Agent 76 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10012800 }. Marked to market: 10107000 +Final holdings for ZI Agent 77 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10002600 }. Marked to market: 10053800 +Final holdings for ZI Agent 78 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 49976900 }. Marked to market: 9737300 +Final holdings for ZI Agent 79 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 49985900 }. Marked to market: 9746300 +Final holdings for ZI Agent 80 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9988000 }. Marked to market: 10131800 +Final holdings for ZI Agent 81 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9901700 }. Marked to market: 9901700 +Final holdings for ZI Agent 82 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40313800 }. Marked to market: 10134100 +Final holdings for ZI Agent 83 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 500, CASH: -40132200 }. Marked to market: 10167300 +Final holdings for ZI Agent 84 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19825600 }. Marked to market: 10354100 +Final holdings for ZI Agent 85 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9865300 }. Marked to market: 9865300 +Final holdings for ZI Agent 86 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 177800 }. Marked to market: 10237700 +Final holdings for ZI Agent 87 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19775600 }. Marked to market: 9715700 +Final holdings for ZI Agent 88 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30024700 }. Marked to market: 9904900 +Final holdings for ZI Agent 89 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30052200 }. Marked to market: 9932400 +Final holdings for ZI Agent 90 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19867900 }. Marked to market: 9808000 +Final holdings for ZI Agent 91 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19800300 }. Marked to market: 9740400 +Final holdings for ZI Agent 92 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10227000 }. Marked to market: 9892800 +Final holdings for ZI Agent 93 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39962100 }. Marked to market: 9782400 +Final holdings for ZI Agent 94 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20152600 }. Marked to market: 10027100 +Final holdings for ZI Agent 95 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -30282700 }. Marked to market: 9956900 +Final holdings for ZI Agent 96 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50018900 }. Marked to market: 9779300 +Final holdings for ZI Agent 97 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9704600 }. Marked to market: 10415200 +Final holdings for ZI Agent 98 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30088200 }. Marked to market: 9968400 +Final holdings for ZI Agent 99 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39770800 }. Marked to market: 9591100 +Final holdings for ZI Agent 100 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -30490700 }. Marked to market: 9748900 diff --git a/tests/sparse_zi_1000.txt b/tests/sparse_zi_1000.txt new file mode 100644 index 000000000..e1fb74cb3 --- /dev/null +++ b/tests/sparse_zi_1000.txt @@ -0,0 +1,1031 @@ +--------------- +Hardware Specs: +--------------- +Mac (system_profiler SPHardwareDataType) or lshw -short + +Processor Name: Intel Core i7 +Processor Speed: 2.6 GHz +Number of Processors: 1 +Total Number of Cores: 6 +L2 Cache (per Core): 256 KB +L3 Cache: 12 MB +Memory: 16 GB + +------------------ +Simulation Results: +------------------ +./scripts/sparse_zi.sh + +ZeroIntelligenceAgent noise: 1000000.0000 +Configuration seed: 123456789 + +Event Queue elapsed: 0 days 00:00:59.733625, messages: 185200, messages per second: 3100.4 +Mean ending value by agent type: + ZeroIntelligenceAgent Type 1 [0 <= R <= 250, eta=1]: -19781 + ZeroIntelligenceAgent Type 2 [0 <= R <= 500, eta=1]: -19338 + ZeroIntelligenceAgent Type 3 [0 <= R <= 1000, eta=0.8]: -18593 + ZeroIntelligenceAgent Type 4 [0 <= R <= 1000, eta=1]: 30137 + ZeroIntelligenceAgent Type 5 [0 <= R <= 2000, eta=0.8]: 641 + ZeroIntelligenceAgent Type 6 [250 <= R <= 500, eta=0.8]: 5634 + ZeroIntelligenceAgent Type 7 [250 <= R <= 500, eta=1]: 27887 + +Final holdings for ZI Agent 1 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30025600 }. Marked to market: 9586000 +Final holdings for ZI Agent 2 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39844700 }. Marked to market: 10136000 +Final holdings for ZI Agent 3 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30053400 }. Marked to market: 10247600 +Final holdings for ZI Agent 4 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10022000 }. Marked to market: 9783800 +Final holdings for ZI Agent 5 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29904300 }. Marked to market: 10098500 +Final holdings for ZI Agent 6 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -58400 }. Marked to market: 9844500 +Final holdings for ZI Agent 7 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30172400 }. Marked to market: 10366600 +Final holdings for ZI Agent 8 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10014200 }. Marked to market: 9791600 +Final holdings for ZI Agent 9 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19945000 }. Marked to market: 10042100 +Final holdings for ZI Agent 10 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29885200 }. Marked to market: 10079400 +Final holdings for ZI Agent 11 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19720900 }. Marked to market: 9818000 +Final holdings for ZI Agent 12 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9942800 }. Marked to market: 9863000 +Final holdings for ZI Agent 13 Type 1 [0 <= R <= 250, eta=1]: { JPM: -500, CASH: 59824900 }. Marked to market: 10310400 +Final holdings for ZI Agent 14 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9883500 }. Marked to market: 9883500 +Final holdings for ZI Agent 15 Type 1 [0 <= R <= 250, eta=1]: { JPM: 700, CASH: -60090700 }. Marked to market: 9229600 +Final holdings for ZI Agent 16 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10055200 }. Marked to market: 10055200 +Final holdings for ZI Agent 17 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10199500 }. Marked to market: 10199500 +Final holdings for ZI Agent 18 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29960900 }. Marked to market: 10155100 +Final holdings for ZI Agent 19 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20144400 }. Marked to market: 9564300 +Final holdings for ZI Agent 20 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9957100 }. Marked to market: 9848700 +Final holdings for ZI Agent 21 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30012600 }. Marked to market: 10206800 +Final holdings for ZI Agent 22 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30104600 }. Marked to market: 9507000 +Final holdings for ZI Agent 23 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39771300 }. Marked to market: 10062600 +Final holdings for ZI Agent 24 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9818900 }. Marked to market: 9986900 +Final holdings for ZI Agent 25 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39888900 }. Marked to market: 10180200 +Final holdings for ZI Agent 26 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9933600 }. Marked to market: 9872200 +Final holdings for ZI Agent 27 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -48600 }. Marked to market: 9854300 +Final holdings for ZI Agent 28 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 26400 }. Marked to market: 9929300 +Final holdings for ZI Agent 29 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 98300 }. Marked to market: 10001200 +Final holdings for ZI Agent 30 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20218400 }. Marked to market: 10315500 +Final holdings for ZI Agent 31 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39820400 }. Marked to market: 10111700 +Final holdings for ZI Agent 32 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30016900 }. Marked to market: 10211100 +Final holdings for ZI Agent 33 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10143500 }. Marked to market: 9662300 +Final holdings for ZI Agent 34 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19925800 }. Marked to market: 10022900 +Final holdings for ZI Agent 35 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20028300 }. Marked to market: 10125400 +Final holdings for ZI Agent 36 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39833500 }. Marked to market: 10124800 +Final holdings for ZI Agent 37 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39861700 }. Marked to market: 10153000 +Final holdings for ZI Agent 38 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9815700 }. Marked to market: 9815700 +Final holdings for ZI Agent 39 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40025500 }. Marked to market: 10316800 +Final holdings for ZI Agent 40 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -34500 }. Marked to market: 9868400 +Final holdings for ZI Agent 41 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9965800 }. Marked to market: 9840000 +Final holdings for ZI Agent 42 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10229000 }. Marked to market: 9576800 +Final holdings for ZI Agent 43 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40034800 }. Marked to market: 10326100 +Final holdings for ZI Agent 44 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30109200 }. Marked to market: 9502400 +Final holdings for ZI Agent 45 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39923400 }. Marked to market: 10214700 +Final holdings for ZI Agent 46 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30002700 }. Marked to market: 9608900 +Final holdings for ZI Agent 47 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -184200 }. Marked to market: 9718700 +Final holdings for ZI Agent 48 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19979400 }. Marked to market: 10076500 +Final holdings for ZI Agent 49 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10100100 }. Marked to market: 10100100 +Final holdings for ZI Agent 50 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -65000 }. Marked to market: 9837900 +Final holdings for ZI Agent 51 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29825200 }. Marked to market: 10019400 +Final holdings for ZI Agent 52 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10069400 }. Marked to market: 9736400 +Final holdings for ZI Agent 53 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30244700 }. Marked to market: 9366900 +Final holdings for ZI Agent 54 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29813800 }. Marked to market: 10008000 +Final holdings for ZI Agent 55 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20257200 }. Marked to market: 9451500 +Final holdings for ZI Agent 56 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10201700 }. Marked to market: 10201700 +Final holdings for ZI Agent 57 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20130700 }. Marked to market: 9578000 +Final holdings for ZI Agent 58 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20104800 }. Marked to market: 10201900 +Final holdings for ZI Agent 59 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 50077800 }. Marked to market: 10466200 +Final holdings for ZI Agent 60 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49888500 }. Marked to market: 10276900 +Final holdings for ZI Agent 61 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9850800 }. Marked to market: 9850800 +Final holdings for ZI Agent 62 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20097200 }. Marked to market: 10194300 +Final holdings for ZI Agent 63 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -29793800 }. Marked to market: 9817800 +Final holdings for ZI Agent 64 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29894200 }. Marked to market: 10088400 +Final holdings for ZI Agent 65 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 4100 }. Marked to market: 9907000 +Final holdings for ZI Agent 66 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10110300 }. Marked to market: 10110300 +Final holdings for ZI Agent 67 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49958500 }. Marked to market: 10346900 +Final holdings for ZI Agent 68 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39777900 }. Marked to market: 10069200 +Final holdings for ZI Agent 69 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40000100 }. Marked to market: 10291400 +Final holdings for ZI Agent 70 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40125100 }. Marked to market: 10416400 +Final holdings for ZI Agent 71 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9715600 }. Marked to market: 9715600 +Final holdings for ZI Agent 72 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9796400 }. Marked to market: 9796400 +Final holdings for ZI Agent 73 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -19938600 }. Marked to market: 9770100 +Final holdings for ZI Agent 74 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9976300 }. Marked to market: 9976300 +Final holdings for ZI Agent 75 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 372600 }. Marked to market: 10289800 +Final holdings for ZI Agent 76 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10134400 }. Marked to market: 9671400 +Final holdings for ZI Agent 77 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9997000 }. Marked to market: 9941200 +Final holdings for ZI Agent 78 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40097900 }. Marked to market: 10389200 +Final holdings for ZI Agent 79 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39971000 }. Marked to market: 10262300 +Final holdings for ZI Agent 80 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9954300 }. Marked to market: 9851500 +Final holdings for ZI Agent 81 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10131400 }. Marked to market: 10131400 +Final holdings for ZI Agent 82 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40172900 }. Marked to market: 10464200 +Final holdings for ZI Agent 83 Type 1 [0 <= R <= 250, eta=1]: { JPM: 700, CASH: -60034100 }. Marked to market: 9286200 +Final holdings for ZI Agent 84 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10079700 }. Marked to market: 9726100 +Final holdings for ZI Agent 85 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9941600 }. Marked to market: 9941600 +Final holdings for ZI Agent 86 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -32700 }. Marked to market: 9870200 +Final holdings for ZI Agent 87 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19661000 }. Marked to market: 9758100 +Final holdings for ZI Agent 88 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29997600 }. Marked to market: 10191800 +Final holdings for ZI Agent 89 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29982200 }. Marked to market: 10176400 +Final holdings for ZI Agent 90 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19763800 }. Marked to market: 9860900 +Final holdings for ZI Agent 91 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20069200 }. Marked to market: 10166300 +Final holdings for ZI Agent 92 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10231600 }. Marked to market: 9574200 +Final holdings for ZI Agent 93 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49955300 }. Marked to market: 10343700 +Final holdings for ZI Agent 94 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -19991900 }. Marked to market: 9716800 +Final holdings for ZI Agent 95 Type 1 [0 <= R <= 250, eta=1]: { JPM: 400, CASH: -30005000 }. Marked to market: 9606600 +Final holdings for ZI Agent 96 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49761800 }. Marked to market: 10150200 +Final holdings for ZI Agent 97 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10133800 }. Marked to market: 9672000 +Final holdings for ZI Agent 98 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20063500 }. Marked to market: 10160600 +Final holdings for ZI Agent 99 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40015600 }. Marked to market: 10306900 +Final holdings for ZI Agent 100 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20066600 }. Marked to market: 9642100 +Final holdings for ZI Agent 101 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30031800 }. Marked to market: 10226000 +Final holdings for ZI Agent 102 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 5500 }. Marked to market: 9999600 +Final holdings for ZI Agent 103 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10042200 }. Marked to market: 9763600 +Final holdings for ZI Agent 104 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10098900 }. Marked to market: 10098900 +Final holdings for ZI Agent 105 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30054600 }. Marked to market: 10248800 +Final holdings for ZI Agent 106 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9978500 }. Marked to market: 9827300 +Final holdings for ZI Agent 107 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -228700 }. Marked to market: 9674200 +Final holdings for ZI Agent 108 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 41800 }. Marked to market: 9944700 +Final holdings for ZI Agent 109 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9886800 }. Marked to market: 9886800 +Final holdings for ZI Agent 110 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 50033400 }. Marked to market: 10421800 +Final holdings for ZI Agent 111 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9877700 }. Marked to market: 9928100 +Final holdings for ZI Agent 112 Type 1 [0 <= R <= 250, eta=1]: { JPM: 500, CASH: -39875500 }. Marked to market: 9639000 +Final holdings for ZI Agent 113 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49980900 }. Marked to market: 10369300 +Final holdings for ZI Agent 114 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9924800 }. Marked to market: 9924800 +Final holdings for ZI Agent 115 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10195100 }. Marked to market: 9610700 +Final holdings for ZI Agent 116 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 56000 }. Marked to market: 9958900 +Final holdings for ZI Agent 117 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10150600 }. Marked to market: 10150600 +Final holdings for ZI Agent 118 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20179600 }. Marked to market: 9529100 +Final holdings for ZI Agent 119 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -19957900 }. Marked to market: 9750800 +Final holdings for ZI Agent 120 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10151600 }. Marked to market: 10151600 +Final holdings for ZI Agent 121 Type 1 [0 <= R <= 250, eta=1]: { JPM: -500, CASH: 59992100 }. Marked to market: 10477600 +Final holdings for ZI Agent 122 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -19756400 }. Marked to market: 9952300 +Final holdings for ZI Agent 123 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29989600 }. Marked to market: 10183800 +Final holdings for ZI Agent 124 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -225400 }. Marked to market: 9677500 +Final holdings for ZI Agent 125 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 30019400 }. Marked to market: 10213600 +Final holdings for ZI Agent 126 Type 1 [0 <= R <= 250, eta=1]: { JPM: 300, CASH: -20182500 }. Marked to market: 9616800 +Final holdings for ZI Agent 127 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9819900 }. Marked to market: 9985900 +Final holdings for ZI Agent 128 Type 1 [0 <= R <= 250, eta=1]: { JPM: -500, CASH: 60087500 }. Marked to market: 10573000 +Final holdings for ZI Agent 129 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: 23700 }. Marked to market: 9926600 +Final holdings for ZI Agent 130 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 19951100 }. Marked to market: 10048200 +Final holdings for ZI Agent 131 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29993300 }. Marked to market: 10187500 +Final holdings for ZI Agent 132 Type 1 [0 <= R <= 250, eta=1]: { CASH: 10080700 }. Marked to market: 10080700 +Final holdings for ZI Agent 133 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 39791300 }. Marked to market: 10082600 +Final holdings for ZI Agent 134 Type 1 [0 <= R <= 250, eta=1]: { JPM: -400, CASH: 49931100 }. Marked to market: 10319500 +Final holdings for ZI Agent 135 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -9891600 }. Marked to market: 9914200 +Final holdings for ZI Agent 136 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9944600 }. Marked to market: 9944600 +Final holdings for ZI Agent 137 Type 1 [0 <= R <= 250, eta=1]: { CASH: 9954100 }. Marked to market: 9954100 +Final holdings for ZI Agent 138 Type 1 [0 <= R <= 250, eta=1]: { JPM: -300, CASH: 40011800 }. Marked to market: 10303100 +Final holdings for ZI Agent 139 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10077900 }. Marked to market: 9727900 +Final holdings for ZI Agent 140 Type 1 [0 <= R <= 250, eta=1]: { JPM: -200, CASH: 29908100 }. Marked to market: 10102300 +Final holdings for ZI Agent 141 Type 1 [0 <= R <= 250, eta=1]: { JPM: 100, CASH: -37200 }. Marked to market: 9865700 +Final holdings for ZI Agent 142 Type 1 [0 <= R <= 250, eta=1]: { JPM: 200, CASH: -10008600 }. Marked to market: 9797200 +Final holdings for ZI Agent 143 Type 1 [0 <= R <= 250, eta=1]: { JPM: -100, CASH: 20191800 }. Marked to market: 10288900 +Final holdings for ZI Agent 144 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19752700 }. Marked to market: 9849800 +Final holdings for ZI Agent 145 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -128900 }. Marked to market: 9774000 +Final holdings for ZI Agent 146 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20049200 }. Marked to market: 10146300 +Final holdings for ZI Agent 147 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9864800 }. Marked to market: 9941000 +Final holdings for ZI Agent 148 Type 2 [0 <= R <= 500, eta=1]: { JPM: -600, CASH: 69878700 }. Marked to market: 10461300 +Final holdings for ZI Agent 149 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -54100 }. Marked to market: 9848800 +Final holdings for ZI Agent 150 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -78500 }. Marked to market: 9824400 +Final holdings for ZI Agent 151 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10317700 }. Marked to market: 10317700 +Final holdings for ZI Agent 152 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20143100 }. Marked to market: 10240200 +Final holdings for ZI Agent 153 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9979300 }. Marked to market: 9979300 +Final holdings for ZI Agent 154 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19972000 }. Marked to market: 9736700 +Final holdings for ZI Agent 155 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10141000 }. Marked to market: 10141000 +Final holdings for ZI Agent 156 Type 2 [0 <= R <= 500, eta=1]: { JPM: -400, CASH: 50173800 }. Marked to market: 10562200 +Final holdings for ZI Agent 157 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19868400 }. Marked to market: 9965500 +Final holdings for ZI Agent 158 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10029800 }. Marked to market: 10029800 +Final holdings for ZI Agent 159 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9866200 }. Marked to market: 9939600 +Final holdings for ZI Agent 160 Type 2 [0 <= R <= 500, eta=1]: { JPM: 500, CASH: -39913700 }. Marked to market: 9600800 +Final holdings for ZI Agent 161 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 40026200 }. Marked to market: 10317500 +Final holdings for ZI Agent 162 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 39963100 }. Marked to market: 10254400 +Final holdings for ZI Agent 163 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19734200 }. Marked to market: 9974500 +Final holdings for ZI Agent 164 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 40137600 }. Marked to market: 10428900 +Final holdings for ZI Agent 165 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -87300 }. Marked to market: 9815600 +Final holdings for ZI Agent 166 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19992100 }. Marked to market: 9880400 +Final holdings for ZI Agent 167 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20081300 }. Marked to market: 10178400 +Final holdings for ZI Agent 168 Type 2 [0 <= R <= 500, eta=1]: { JPM: -500, CASH: 59922900 }. Marked to market: 10408400 +Final holdings for ZI Agent 169 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -204500 }. Marked to market: 9698400 +Final holdings for ZI Agent 170 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9975500 }. Marked to market: 9975500 +Final holdings for ZI Agent 171 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9796900 }. Marked to market: 10008900 +Final holdings for ZI Agent 172 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9902400 }. Marked to market: 9902400 +Final holdings for ZI Agent 173 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20177800 }. Marked to market: 9530900 +Final holdings for ZI Agent 174 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10111000 }. Marked to market: 10111000 +Final holdings for ZI Agent 175 Type 2 [0 <= R <= 500, eta=1]: { JPM: -500, CASH: 60070500 }. Marked to market: 10556000 +Final holdings for ZI Agent 176 Type 2 [0 <= R <= 500, eta=1]: { JPM: 500, CASH: -40189700 }. Marked to market: 9324800 +Final holdings for ZI Agent 177 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20063600 }. Marked to market: 9645100 +Final holdings for ZI Agent 178 Type 2 [0 <= R <= 500, eta=1]: { JPM: -600, CASH: 69863200 }. Marked to market: 10445800 +Final holdings for ZI Agent 179 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9868900 }. Marked to market: 9868900 +Final holdings for ZI Agent 180 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29924300 }. Marked to market: 10118500 +Final holdings for ZI Agent 181 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9740200 }. Marked to market: 10065600 +Final holdings for ZI Agent 182 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30032800 }. Marked to market: 10227000 +Final holdings for ZI Agent 183 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19998500 }. Marked to market: 10095600 +Final holdings for ZI Agent 184 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29947400 }. Marked to market: 10141600 +Final holdings for ZI Agent 185 Type 2 [0 <= R <= 500, eta=1]: { JPM: -400, CASH: 50090800 }. Marked to market: 10479200 +Final holdings for ZI Agent 186 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19937800 }. Marked to market: 10034900 +Final holdings for ZI Agent 187 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20003600 }. Marked to market: 9705100 +Final holdings for ZI Agent 188 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30013900 }. Marked to market: 10208100 +Final holdings for ZI Agent 189 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19936200 }. Marked to market: 9772500 +Final holdings for ZI Agent 190 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 39955600 }. Marked to market: 10246900 +Final holdings for ZI Agent 191 Type 2 [0 <= R <= 500, eta=1]: { JPM: 600, CASH: -50181700 }. Marked to market: 9235700 +Final holdings for ZI Agent 192 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19830300 }. Marked to market: 9927400 +Final holdings for ZI Agent 193 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29953700 }. Marked to market: 10147900 +Final holdings for ZI Agent 194 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19910700 }. Marked to market: 10007800 +Final holdings for ZI Agent 195 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20152300 }. Marked to market: 10203200 +Final holdings for ZI Agent 196 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 194900 }. Marked to market: 10097800 +Final holdings for ZI Agent 197 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9977900 }. Marked to market: 9827900 +Final holdings for ZI Agent 198 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10128000 }. Marked to market: 10128000 +Final holdings for ZI Agent 199 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20226400 }. Marked to market: 10323500 +Final holdings for ZI Agent 200 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9945800 }. Marked to market: 9945800 +Final holdings for ZI Agent 201 Type 2 [0 <= R <= 500, eta=1]: { JPM: -400, CASH: 49972600 }. Marked to market: 10361000 +Final holdings for ZI Agent 202 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20171700 }. Marked to market: 10268800 +Final holdings for ZI Agent 203 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10117100 }. Marked to market: 9688700 +Final holdings for ZI Agent 204 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9969200 }. Marked to market: 9969200 +Final holdings for ZI Agent 205 Type 2 [0 <= R <= 500, eta=1]: { JPM: 500, CASH: -40045600 }. Marked to market: 9468900 +Final holdings for ZI Agent 206 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10123600 }. Marked to market: 10123600 +Final holdings for ZI Agent 207 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9936100 }. Marked to market: 9936100 +Final holdings for ZI Agent 208 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20296400 }. Marked to market: 10393500 +Final holdings for ZI Agent 209 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19926500 }. Marked to market: 10023600 +Final holdings for ZI Agent 210 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19972300 }. Marked to market: 10069400 +Final holdings for ZI Agent 211 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19792500 }. Marked to market: 9889600 +Final holdings for ZI Agent 212 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19939900 }. Marked to market: 9768800 +Final holdings for ZI Agent 213 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10087700 }. Marked to market: 9718100 +Final holdings for ZI Agent 214 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 170000 }. Marked to market: 10072900 +Final holdings for ZI Agent 215 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -32300 }. Marked to market: 9870600 +Final holdings for ZI Agent 216 Type 2 [0 <= R <= 500, eta=1]: { JPM: -500, CASH: 60085800 }. Marked to market: 10571300 +Final holdings for ZI Agent 217 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20072800 }. Marked to market: 9635900 +Final holdings for ZI Agent 218 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10221100 }. Marked to market: 9584700 +Final holdings for ZI Agent 219 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9651800 }. Marked to market: 9651800 +Final holdings for ZI Agent 220 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 28200 }. Marked to market: 9931100 +Final holdings for ZI Agent 221 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19991800 }. Marked to market: 10088900 +Final holdings for ZI Agent 222 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10027700 }. Marked to market: 9778100 +Final holdings for ZI Agent 223 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9983000 }. Marked to market: 9983000 +Final holdings for ZI Agent 224 Type 2 [0 <= R <= 500, eta=1]: { JPM: -600, CASH: 69949400 }. Marked to market: 10532000 +Final holdings for ZI Agent 225 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10115200 }. Marked to market: 9690600 +Final holdings for ZI Agent 226 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20139900 }. Marked to market: 9568800 +Final holdings for ZI Agent 227 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -12800 }. Marked to market: 9890100 +Final holdings for ZI Agent 228 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -19200 }. Marked to market: 9883700 +Final holdings for ZI Agent 229 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19996300 }. Marked to market: 9712400 +Final holdings for ZI Agent 230 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19871000 }. Marked to market: 9968100 +Final holdings for ZI Agent 231 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29973000 }. Marked to market: 10167200 +Final holdings for ZI Agent 232 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9915800 }. Marked to market: 9915800 +Final holdings for ZI Agent 233 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19951200 }. Marked to market: 10048300 +Final holdings for ZI Agent 234 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9790000 }. Marked to market: 9790000 +Final holdings for ZI Agent 235 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19798200 }. Marked to market: 9895300 +Final holdings for ZI Agent 236 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19772800 }. Marked to market: 9935900 +Final holdings for ZI Agent 237 Type 2 [0 <= R <= 500, eta=1]: { JPM: -400, CASH: 49831100 }. Marked to market: 10219500 +Final holdings for ZI Agent 238 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19959800 }. Marked to market: 10056900 +Final holdings for ZI Agent 239 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20055200 }. Marked to market: 9653500 +Final holdings for ZI Agent 240 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 216400 }. Marked to market: 10119300 +Final holdings for ZI Agent 241 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20092200 }. Marked to market: 9616500 +Final holdings for ZI Agent 242 Type 2 [0 <= R <= 500, eta=1]: { JPM: -600, CASH: 69769500 }. Marked to market: 10352100 +Final holdings for ZI Agent 243 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29778500 }. Marked to market: 9972700 +Final holdings for ZI Agent 244 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 40180900 }. Marked to market: 10472200 +Final holdings for ZI Agent 245 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20314200 }. Marked to market: 9394500 +Final holdings for ZI Agent 246 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9894700 }. Marked to market: 9894700 +Final holdings for ZI Agent 247 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19748900 }. Marked to market: 9846000 +Final holdings for ZI Agent 248 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10223400 }. Marked to market: 9582400 +Final holdings for ZI Agent 249 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9869200 }. Marked to market: 9869200 +Final holdings for ZI Agent 250 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29914600 }. Marked to market: 10108800 +Final holdings for ZI Agent 251 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9741600 }. Marked to market: 9741600 +Final holdings for ZI Agent 252 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -9973100 }. Marked to market: 9832700 +Final holdings for ZI Agent 253 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10017900 }. Marked to market: 9787900 +Final holdings for ZI Agent 254 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 19961300 }. Marked to market: 10058400 +Final holdings for ZI Agent 255 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 40020900 }. Marked to market: 10312200 +Final holdings for ZI Agent 256 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 173700 }. Marked to market: 10076600 +Final holdings for ZI Agent 257 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19858500 }. Marked to market: 9850200 +Final holdings for ZI Agent 258 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -20100600 }. Marked to market: 9608100 +Final holdings for ZI Agent 259 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 39963800 }. Marked to market: 10255100 +Final holdings for ZI Agent 260 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10040900 }. Marked to market: 9764900 +Final holdings for ZI Agent 261 Type 2 [0 <= R <= 500, eta=1]: { CASH: 10178600 }. Marked to market: 10178600 +Final holdings for ZI Agent 262 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: 86100 }. Marked to market: 9989000 +Final holdings for ZI Agent 263 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30039900 }. Marked to market: 10234100 +Final holdings for ZI Agent 264 Type 2 [0 <= R <= 500, eta=1]: { JPM: -600, CASH: 69983100 }. Marked to market: 10565700 +Final holdings for ZI Agent 265 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30065900 }. Marked to market: 10260100 +Final holdings for ZI Agent 266 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30106600 }. Marked to market: 10300800 +Final holdings for ZI Agent 267 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20068600 }. Marked to market: 10165700 +Final holdings for ZI Agent 268 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10004400 }. Marked to market: 9801400 +Final holdings for ZI Agent 269 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9703100 }. Marked to market: 9703100 +Final holdings for ZI Agent 270 Type 2 [0 <= R <= 500, eta=1]: { JPM: 200, CASH: -10009500 }. Marked to market: 9796300 +Final holdings for ZI Agent 271 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 30077200 }. Marked to market: 10271400 +Final holdings for ZI Agent 272 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9998900 }. Marked to market: 9998900 +Final holdings for ZI Agent 273 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9893300 }. Marked to market: 9893300 +Final holdings for ZI Agent 274 Type 2 [0 <= R <= 500, eta=1]: { JPM: -200, CASH: 29773900 }. Marked to market: 9968100 +Final holdings for ZI Agent 275 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9946300 }. Marked to market: 9946300 +Final holdings for ZI Agent 276 Type 2 [0 <= R <= 500, eta=1]: { JPM: 400, CASH: -30096900 }. Marked to market: 9514700 +Final holdings for ZI Agent 277 Type 2 [0 <= R <= 500, eta=1]: { JPM: 500, CASH: -40038000 }. Marked to market: 9476500 +Final holdings for ZI Agent 278 Type 2 [0 <= R <= 500, eta=1]: { JPM: 400, CASH: -29913200 }. Marked to market: 9698400 +Final holdings for ZI Agent 279 Type 2 [0 <= R <= 500, eta=1]: { JPM: 300, CASH: -19911700 }. Marked to market: 9797000 +Final holdings for ZI Agent 280 Type 2 [0 <= R <= 500, eta=1]: { JPM: 500, CASH: -40098300 }. Marked to market: 9416200 +Final holdings for ZI Agent 281 Type 2 [0 <= R <= 500, eta=1]: { JPM: -500, CASH: 60039700 }. Marked to market: 10525200 +Final holdings for ZI Agent 282 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9801600 }. Marked to market: 9801600 +Final holdings for ZI Agent 283 Type 2 [0 <= R <= 500, eta=1]: { JPM: 100, CASH: -389000 }. Marked to market: 9513900 +Final holdings for ZI Agent 284 Type 2 [0 <= R <= 500, eta=1]: { JPM: -300, CASH: 40042500 }. Marked to market: 10333800 +Final holdings for ZI Agent 285 Type 2 [0 <= R <= 500, eta=1]: { JPM: -100, CASH: 20117500 }. Marked to market: 10214600 +Final holdings for ZI Agent 286 Type 2 [0 <= R <= 500, eta=1]: { CASH: 9953900 }. Marked to market: 9953900 +Final holdings for ZI Agent 287 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29989600 }. Marked to market: 10183800 +Final holdings for ZI Agent 288 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9973200 }. Marked to market: 9973200 +Final holdings for ZI Agent 289 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9863200 }. Marked to market: 9942600 +Final holdings for ZI Agent 290 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39867800 }. Marked to market: 10159100 +Final holdings for ZI Agent 291 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20149200 }. Marked to market: 10246300 +Final holdings for ZI Agent 292 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -29851800 }. Marked to market: 9759800 +Final holdings for ZI Agent 293 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39949500 }. Marked to market: 10240800 +Final holdings for ZI Agent 294 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: 21100 }. Marked to market: 9924000 +Final holdings for ZI Agent 295 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9875100 }. Marked to market: 9875100 +Final holdings for ZI Agent 296 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -19838300 }. Marked to market: 9870400 +Final holdings for ZI Agent 297 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9800200 }. Marked to market: 9800200 +Final holdings for ZI Agent 298 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -26700 }. Marked to market: 9876200 +Final holdings for ZI Agent 299 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 40096900 }. Marked to market: 10388200 +Final holdings for ZI Agent 300 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29975800 }. Marked to market: 10170000 +Final holdings for ZI Agent 301 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9887000 }. Marked to market: 9887000 +Final holdings for ZI Agent 302 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9930500 }. Marked to market: 9875300 +Final holdings for ZI Agent 303 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20019500 }. Marked to market: 10116600 +Final holdings for ZI Agent 304 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30018800 }. Marked to market: 10213000 +Final holdings for ZI Agent 305 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -30200 }. Marked to market: 9872700 +Final holdings for ZI Agent 306 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20021100 }. Marked to market: 9687600 +Final holdings for ZI Agent 307 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29998000 }. Marked to market: 10192200 +Final holdings for ZI Agent 308 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10056500 }. Marked to market: 9749300 +Final holdings for ZI Agent 309 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30057800 }. Marked to market: 10252000 +Final holdings for ZI Agent 310 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20295100 }. Marked to market: 10392200 +Final holdings for ZI Agent 311 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9943500 }. Marked to market: 9943500 +Final holdings for ZI Agent 312 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20143500 }. Marked to market: 10240600 +Final holdings for ZI Agent 313 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29989300 }. Marked to market: 10183500 +Final holdings for ZI Agent 314 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -29994500 }. Marked to market: 9617100 +Final holdings for ZI Agent 315 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10155800 }. Marked to market: 9650000 +Final holdings for ZI Agent 316 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39939400 }. Marked to market: 10230700 +Final holdings for ZI Agent 317 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19824400 }. Marked to market: 9921500 +Final holdings for ZI Agent 318 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9995700 }. Marked to market: 9810100 +Final holdings for ZI Agent 319 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20024800 }. Marked to market: 9683900 +Final holdings for ZI Agent 320 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 50109100 }. Marked to market: 10497500 +Final holdings for ZI Agent 321 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 49886100 }. Marked to market: 10274500 +Final holdings for ZI Agent 322 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9838400 }. Marked to market: 9967400 +Final holdings for ZI Agent 323 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39975000 }. Marked to market: 10266300 +Final holdings for ZI Agent 324 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29965900 }. Marked to market: 10160100 +Final holdings for ZI Agent 325 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9804000 }. Marked to market: 10001800 +Final holdings for ZI Agent 326 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -19815500 }. Marked to market: 9893200 +Final holdings for ZI Agent 327 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -30424900 }. Marked to market: 9186700 +Final holdings for ZI Agent 328 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10306400 }. Marked to market: 10306400 +Final holdings for ZI Agent 329 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20087100 }. Marked to market: 9621600 +Final holdings for ZI Agent 330 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20178400 }. Marked to market: 10275500 +Final holdings for ZI Agent 331 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9990400 }. Marked to market: 9815400 +Final holdings for ZI Agent 332 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9973600 }. Marked to market: 9973600 +Final holdings for ZI Agent 333 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20113700 }. Marked to market: 10210800 +Final holdings for ZI Agent 334 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: 29700 }. Marked to market: 9932600 +Final holdings for ZI Agent 335 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 50044100 }. Marked to market: 10432500 +Final holdings for ZI Agent 336 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -108000 }. Marked to market: 9794900 +Final holdings for ZI Agent 337 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9947900 }. Marked to market: 9857900 +Final holdings for ZI Agent 338 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29886100 }. Marked to market: 10080300 +Final holdings for ZI Agent 339 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -97300 }. Marked to market: 9805600 +Final holdings for ZI Agent 340 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -138200 }. Marked to market: 9764700 +Final holdings for ZI Agent 341 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20084000 }. Marked to market: 10181100 +Final holdings for ZI Agent 342 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20050000 }. Marked to market: 9837500 +Final holdings for ZI Agent 343 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9987100 }. Marked to market: 9818700 +Final holdings for ZI Agent 344 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -30153100 }. Marked to market: 9458500 +Final holdings for ZI Agent 345 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: 29400 }. Marked to market: 9932300 +Final holdings for ZI Agent 346 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20025500 }. Marked to market: 10122600 +Final holdings for ZI Agent 347 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 600, CASH: -50150400 }. Marked to market: 9267000 +Final holdings for ZI Agent 348 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10277500 }. Marked to market: 10277500 +Final holdings for ZI Agent 349 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -44100 }. Marked to market: 9858800 +Final holdings for ZI Agent 350 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20005500 }. Marked to market: 10102600 +Final holdings for ZI Agent 351 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10218100 }. Marked to market: 9587700 +Final holdings for ZI Agent 352 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19823500 }. Marked to market: 9920600 +Final holdings for ZI Agent 353 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30066900 }. Marked to market: 10261100 +Final holdings for ZI Agent 354 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19858400 }. Marked to market: 9955500 +Final holdings for ZI Agent 355 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9969500 }. Marked to market: 9969500 +Final holdings for ZI Agent 356 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 600, CASH: -49938700 }. Marked to market: 9478700 +Final holdings for ZI Agent 357 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20002700 }. Marked to market: 9706000 +Final holdings for ZI Agent 358 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20229600 }. Marked to market: 9479100 +Final holdings for ZI Agent 359 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9830800 }. Marked to market: 9830800 +Final holdings for ZI Agent 360 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -500, CASH: 60233700 }. Marked to market: 10719200 +Final holdings for ZI Agent 361 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30043200 }. Marked to market: 10237400 +Final holdings for ZI Agent 362 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20117200 }. Marked to market: 9591500 +Final holdings for ZI Agent 363 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -51400 }. Marked to market: 9851500 +Final holdings for ZI Agent 364 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9860000 }. Marked to market: 9860000 +Final holdings for ZI Agent 365 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9836900 }. Marked to market: 9968900 +Final holdings for ZI Agent 366 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9902500 }. Marked to market: 9902500 +Final holdings for ZI Agent 367 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29878400 }. Marked to market: 10072600 +Final holdings for ZI Agent 368 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19854800 }. Marked to market: 9951900 +Final holdings for ZI Agent 369 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 49782700 }. Marked to market: 10171100 +Final holdings for ZI Agent 370 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30191400 }. Marked to market: 10175000 +Final holdings for ZI Agent 371 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10069200 }. Marked to market: 10069200 +Final holdings for ZI Agent 372 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20176100 }. Marked to market: 10273200 +Final holdings for ZI Agent 373 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -600 }. Marked to market: 9902300 +Final holdings for ZI Agent 374 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20145500 }. Marked to market: 10242600 +Final holdings for ZI Agent 375 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20061300 }. Marked to market: 9647400 +Final holdings for ZI Agent 376 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10338500 }. Marked to market: 10338500 +Final holdings for ZI Agent 377 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -30052700 }. Marked to market: 9558900 +Final holdings for ZI Agent 378 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9880400 }. Marked to market: 9925400 +Final holdings for ZI Agent 379 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30308300 }. Marked to market: 10502500 +Final holdings for ZI Agent 380 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -126100 }. Marked to market: 9776800 +Final holdings for ZI Agent 381 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -500, CASH: 59982700 }. Marked to market: 10468200 +Final holdings for ZI Agent 382 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -400, CASH: 49922800 }. Marked to market: 10311200 +Final holdings for ZI Agent 383 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30062600 }. Marked to market: 10256800 +Final holdings for ZI Agent 384 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19961200 }. Marked to market: 10058300 +Final holdings for ZI Agent 385 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: 158300 }. Marked to market: 10061200 +Final holdings for ZI Agent 386 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9964000 }. Marked to market: 9964000 +Final holdings for ZI Agent 387 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -30175800 }. Marked to market: 9435800 +Final holdings for ZI Agent 388 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9910800 }. Marked to market: 9910800 +Final holdings for ZI Agent 389 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10132300 }. Marked to market: 9673500 +Final holdings for ZI Agent 390 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10063000 }. Marked to market: 9742800 +Final holdings for ZI Agent 391 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10119100 }. Marked to market: 9686700 +Final holdings for ZI Agent 392 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -29801500 }. Marked to market: 9810100 +Final holdings for ZI Agent 393 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39876300 }. Marked to market: 10167600 +Final holdings for ZI Agent 394 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -20500 }. Marked to market: 9882400 +Final holdings for ZI Agent 395 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9984400 }. Marked to market: 9984400 +Final holdings for ZI Agent 396 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -38000 }. Marked to market: 9864900 +Final holdings for ZI Agent 397 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20014300 }. Marked to market: 10111400 +Final holdings for ZI Agent 398 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 600, CASH: -49923600 }. Marked to market: 9493800 +Final holdings for ZI Agent 399 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9992700 }. Marked to market: 9813100 +Final holdings for ZI Agent 400 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9977100 }. Marked to market: 9977100 +Final holdings for ZI Agent 401 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 40006700 }. Marked to market: 10298000 +Final holdings for ZI Agent 402 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20120200 }. Marked to market: 9588500 +Final holdings for ZI Agent 403 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 39930500 }. Marked to market: 10221800 +Final holdings for ZI Agent 404 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -300, CASH: 40029100 }. Marked to market: 10320400 +Final holdings for ZI Agent 405 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19987500 }. Marked to market: 10084600 +Final holdings for ZI Agent 406 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 29928800 }. Marked to market: 10123000 +Final holdings for ZI Agent 407 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19990300 }. Marked to market: 10087400 +Final holdings for ZI Agent 408 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20006300 }. Marked to market: 9702400 +Final holdings for ZI Agent 409 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: 185100 }. Marked to market: 10088000 +Final holdings for ZI Agent 410 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -200, CASH: 30016700 }. Marked to market: 10210900 +Final holdings for ZI Agent 411 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20230700 }. Marked to market: 10327800 +Final holdings for ZI Agent 412 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 19930600 }. Marked to market: 10027700 +Final holdings for ZI Agent 413 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -10031900 }. Marked to market: 9773900 +Final holdings for ZI Agent 414 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20045600 }. Marked to market: 10142700 +Final holdings for ZI Agent 415 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20139300 }. Marked to market: 10236400 +Final holdings for ZI Agent 416 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9950300 }. Marked to market: 9950300 +Final holdings for ZI Agent 417 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 200, CASH: -9960300 }. Marked to market: 9845500 +Final holdings for ZI Agent 418 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 100, CASH: -147100 }. Marked to market: 9755800 +Final holdings for ZI Agent 419 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20063100 }. Marked to market: 10160200 +Final holdings for ZI Agent 420 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -30006500 }. Marked to market: 9605100 +Final holdings for ZI Agent 421 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 400, CASH: -29701400 }. Marked to market: 9910200 +Final holdings for ZI Agent 422 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20069100 }. Marked to market: 9639600 +Final holdings for ZI Agent 423 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -100, CASH: 20112700 }. Marked to market: 10209800 +Final holdings for ZI Agent 424 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: 300, CASH: -20066600 }. Marked to market: 9642100 +Final holdings for ZI Agent 425 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9890600 }. Marked to market: 9890600 +Final holdings for ZI Agent 426 Type 3 [0 <= R <= 1000, eta=0.8]: { JPM: -700, CASH: 79940500 }. Marked to market: 10620200 +Final holdings for ZI Agent 427 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 9981000 }. Marked to market: 9981000 +Final holdings for ZI Agent 428 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10207200 }. Marked to market: 10207200 +Final holdings for ZI Agent 429 Type 3 [0 <= R <= 1000, eta=0.8]: { CASH: 10184200 }. Marked to market: 10184200 +Final holdings for ZI Agent 430 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -44400 }. Marked to market: 9858500 +Final holdings for ZI Agent 431 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50044800 }. Marked to market: 10433200 +Final holdings for ZI Agent 432 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 39824400 }. Marked to market: 10115700 +Final holdings for ZI Agent 433 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -235800 }. Marked to market: 9667100 +Final holdings for ZI Agent 434 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19865600 }. Marked to market: 9962700 +Final holdings for ZI Agent 435 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50071100 }. Marked to market: 10459500 +Final holdings for ZI Agent 436 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40037000 }. Marked to market: 10328300 +Final holdings for ZI Agent 437 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19937100 }. Marked to market: 10034200 +Final holdings for ZI Agent 438 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10134200 }. Marked to market: 10134200 +Final holdings for ZI Agent 439 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29910000 }. Marked to market: 10104200 +Final holdings for ZI Agent 440 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 49873700 }. Marked to market: 10262100 +Final holdings for ZI Agent 441 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19831100 }. Marked to market: 9877600 +Final holdings for ZI Agent 442 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10133100 }. Marked to market: 10133100 +Final holdings for ZI Agent 443 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -10116200 }. Marked to market: 9689600 +Final holdings for ZI Agent 444 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 62000 }. Marked to market: 9964900 +Final holdings for ZI Agent 445 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19990100 }. Marked to market: 10087200 +Final holdings for ZI Agent 446 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 38800 }. Marked to market: 9941700 +Final holdings for ZI Agent 447 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20001600 }. Marked to market: 9707100 +Final holdings for ZI Agent 448 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50029600 }. Marked to market: 10418000 +Final holdings for ZI Agent 449 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19943200 }. Marked to market: 9974600 +Final holdings for ZI Agent 450 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9871100 }. Marked to market: 9871100 +Final holdings for ZI Agent 451 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10067700 }. Marked to market: 10067700 +Final holdings for ZI Agent 452 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -28300 }. Marked to market: 9874600 +Final holdings for ZI Agent 453 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20022000 }. Marked to market: 10119100 +Final holdings for ZI Agent 454 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -30033700 }. Marked to market: 9577900 +Final holdings for ZI Agent 455 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10295300 }. Marked to market: 10295300 +Final holdings for ZI Agent 456 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20030300 }. Marked to market: 10127400 +Final holdings for ZI Agent 457 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10119800 }. Marked to market: 10119800 +Final holdings for ZI Agent 458 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9951600 }. Marked to market: 9854200 +Final holdings for ZI Agent 459 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -30183800 }. Marked to market: 9427800 +Final holdings for ZI Agent 460 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -29896400 }. Marked to market: 9715200 +Final holdings for ZI Agent 461 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -30013900 }. Marked to market: 9597700 +Final holdings for ZI Agent 462 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9994000 }. Marked to market: 9994000 +Final holdings for ZI Agent 463 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29893500 }. Marked to market: 10087700 +Final holdings for ZI Agent 464 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30015700 }. Marked to market: 10209900 +Final holdings for ZI Agent 465 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30272000 }. Marked to market: 10466200 +Final holdings for ZI Agent 466 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30072200 }. Marked to market: 10266400 +Final holdings for ZI Agent 467 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -32700 }. Marked to market: 9870200 +Final holdings for ZI Agent 468 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19934700 }. Marked to market: 9774000 +Final holdings for ZI Agent 469 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29923000 }. Marked to market: 10117200 +Final holdings for ZI Agent 470 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 11400 }. Marked to market: 9914300 +Final holdings for ZI Agent 471 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 117700 }. Marked to market: 10020600 +Final holdings for ZI Agent 472 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29853900 }. Marked to market: 10048100 +Final holdings for ZI Agent 473 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20127500 }. Marked to market: 10218600 +Final holdings for ZI Agent 474 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30090100 }. Marked to market: 10284300 +Final holdings for ZI Agent 475 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 5100 }. Marked to market: 9908000 +Final holdings for ZI Agent 476 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30076000 }. Marked to market: 10270200 +Final holdings for ZI Agent 477 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50039900 }. Marked to market: 10428300 +Final holdings for ZI Agent 478 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19993200 }. Marked to market: 10090300 +Final holdings for ZI Agent 479 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50048900 }. Marked to market: 10437300 +Final holdings for ZI Agent 480 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19859600 }. Marked to market: 9956700 +Final holdings for ZI Agent 481 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10067000 }. Marked to market: 10067000 +Final holdings for ZI Agent 482 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10186900 }. Marked to market: 10186900 +Final holdings for ZI Agent 483 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30070200 }. Marked to market: 10264400 +Final holdings for ZI Agent 484 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 49955100 }. Marked to market: 10343500 +Final holdings for ZI Agent 485 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9983500 }. Marked to market: 9822300 +Final holdings for ZI Agent 486 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19874100 }. Marked to market: 9834600 +Final holdings for ZI Agent 487 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9965000 }. Marked to market: 9840800 +Final holdings for ZI Agent 488 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20030200 }. Marked to market: 10127300 +Final holdings for ZI Agent 489 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9733400 }. Marked to market: 10072400 +Final holdings for ZI Agent 490 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -203800 }. Marked to market: 9699100 +Final holdings for ZI Agent 491 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 35600 }. Marked to market: 9938500 +Final holdings for ZI Agent 492 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20042700 }. Marked to market: 10139800 +Final holdings for ZI Agent 493 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50017500 }. Marked to market: 10405900 +Final holdings for ZI Agent 494 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30120200 }. Marked to market: 10314400 +Final holdings for ZI Agent 495 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40140300 }. Marked to market: 10413600 +Final holdings for ZI Agent 496 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19945000 }. Marked to market: 9763700 +Final holdings for ZI Agent 497 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9977800 }. Marked to market: 9977800 +Final holdings for ZI Agent 498 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 304400 }. Marked to market: 10207300 +Final holdings for ZI Agent 499 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 9400 }. Marked to market: 9912300 +Final holdings for ZI Agent 500 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19910600 }. Marked to market: 10007700 +Final holdings for ZI Agent 501 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9945700 }. Marked to market: 9860100 +Final holdings for ZI Agent 502 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 500, CASH: -40112700 }. Marked to market: 9401800 +Final holdings for ZI Agent 503 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -10128000 }. Marked to market: 9677800 +Final holdings for ZI Agent 504 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19908500 }. Marked to market: 10005600 +Final holdings for ZI Agent 505 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10055200 }. Marked to market: 10055200 +Final holdings for ZI Agent 506 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40123900 }. Marked to market: 10415200 +Final holdings for ZI Agent 507 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 39823300 }. Marked to market: 10114600 +Final holdings for ZI Agent 508 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20004200 }. Marked to market: 10101300 +Final holdings for ZI Agent 509 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20107400 }. Marked to market: 10204500 +Final holdings for ZI Agent 510 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 63800 }. Marked to market: 9966700 +Final holdings for ZI Agent 511 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20000300 }. Marked to market: 10097400 +Final holdings for ZI Agent 512 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19915000 }. Marked to market: 9793700 +Final holdings for ZI Agent 513 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19970600 }. Marked to market: 9738100 +Final holdings for ZI Agent 514 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20243000 }. Marked to market: 9465700 +Final holdings for ZI Agent 515 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -29915700 }. Marked to market: 9695900 +Final holdings for ZI Agent 516 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 400, CASH: -29928300 }. Marked to market: 9683300 +Final holdings for ZI Agent 517 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20133200 }. Marked to market: 10230300 +Final holdings for ZI Agent 518 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10138500 }. Marked to market: 10138500 +Final holdings for ZI Agent 519 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9983700 }. Marked to market: 9822100 +Final holdings for ZI Agent 520 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9949400 }. Marked to market: 9949400 +Final holdings for ZI Agent 521 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 39982900 }. Marked to market: 10274200 +Final holdings for ZI Agent 522 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20066300 }. Marked to market: 10163400 +Final holdings for ZI Agent 523 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20182300 }. Marked to market: 10279400 +Final holdings for ZI Agent 524 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -10117000 }. Marked to market: 9688800 +Final holdings for ZI Agent 525 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 39983400 }. Marked to market: 10274700 +Final holdings for ZI Agent 526 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29965100 }. Marked to market: 10159300 +Final holdings for ZI Agent 527 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 39891100 }. Marked to market: 10182400 +Final holdings for ZI Agent 528 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29982100 }. Marked to market: 10176300 +Final holdings for ZI Agent 529 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -167900 }. Marked to market: 9735000 +Final holdings for ZI Agent 530 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10124500 }. Marked to market: 10124500 +Final holdings for ZI Agent 531 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19995400 }. Marked to market: 10092500 +Final holdings for ZI Agent 532 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9930900 }. Marked to market: 9930900 +Final holdings for ZI Agent 533 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50263700 }. Marked to market: 10652100 +Final holdings for ZI Agent 534 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 49996200 }. Marked to market: 10384600 +Final holdings for ZI Agent 535 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 41200 }. Marked to market: 9944100 +Final holdings for ZI Agent 536 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -26900 }. Marked to market: 9876000 +Final holdings for ZI Agent 537 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9799200 }. Marked to market: 9799200 +Final holdings for ZI Agent 538 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10090500 }. Marked to market: 10090500 +Final holdings for ZI Agent 539 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29957500 }. Marked to market: 10151700 +Final holdings for ZI Agent 540 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40080900 }. Marked to market: 10372200 +Final holdings for ZI Agent 541 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19904800 }. Marked to market: 9803900 +Final holdings for ZI Agent 542 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20029400 }. Marked to market: 9679300 +Final holdings for ZI Agent 543 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20049800 }. Marked to market: 9658900 +Final holdings for ZI Agent 544 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 500, CASH: -40006000 }. Marked to market: 9508500 +Final holdings for ZI Agent 545 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 96800 }. Marked to market: 9999700 +Final holdings for ZI Agent 546 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50116800 }. Marked to market: 10505200 +Final holdings for ZI Agent 547 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9736400 }. Marked to market: 10069400 +Final holdings for ZI Agent 548 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 500, CASH: -40075400 }. Marked to market: 9439100 +Final holdings for ZI Agent 549 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9984300 }. Marked to market: 9984300 +Final holdings for ZI Agent 550 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -19997800 }. Marked to market: 9710900 +Final holdings for ZI Agent 551 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 500, CASH: -39853900 }. Marked to market: 9660600 +Final holdings for ZI Agent 552 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10071600 }. Marked to market: 10071600 +Final holdings for ZI Agent 553 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29817200 }. Marked to market: 10011400 +Final holdings for ZI Agent 554 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10148300 }. Marked to market: 10148300 +Final holdings for ZI Agent 555 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20062500 }. Marked to market: 10159600 +Final holdings for ZI Agent 556 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -400, CASH: 50010600 }. Marked to market: 10399000 +Final holdings for ZI Agent 557 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: 255500 }. Marked to market: 10207400 +Final holdings for ZI Agent 558 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 200, CASH: -9954800 }. Marked to market: 9851000 +Final holdings for ZI Agent 559 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10024200 }. Marked to market: 10024200 +Final holdings for ZI Agent 560 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 30125500 }. Marked to market: 10319700 +Final holdings for ZI Agent 561 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -14800 }. Marked to market: 9888100 +Final holdings for ZI Agent 562 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9866600 }. Marked to market: 9866600 +Final holdings for ZI Agent 563 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20192500 }. Marked to market: 10289600 +Final holdings for ZI Agent 564 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 100, CASH: -46200 }. Marked to market: 9856700 +Final holdings for ZI Agent 565 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40178000 }. Marked to market: 10469300 +Final holdings for ZI Agent 566 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -300, CASH: 40109600 }. Marked to market: 10400900 +Final holdings for ZI Agent 567 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 10156400 }. Marked to market: 10156400 +Final holdings for ZI Agent 568 Type 4 [0 <= R <= 1000, eta=1]: { CASH: 9948300 }. Marked to market: 9948300 +Final holdings for ZI Agent 569 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 20248200 }. Marked to market: 10345300 +Final holdings for ZI Agent 570 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -100, CASH: 19915000 }. Marked to market: 10012100 +Final holdings for ZI Agent 571 Type 4 [0 <= R <= 1000, eta=1]: { JPM: 300, CASH: -20054500 }. Marked to market: 9654200 +Final holdings for ZI Agent 572 Type 4 [0 <= R <= 1000, eta=1]: { JPM: -200, CASH: 29919800 }. Marked to market: 9938200 +Final holdings for ZI Agent 573 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10072200 }. Marked to market: 9733600 +Final holdings for ZI Agent 574 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9989200 }. Marked to market: 9989200 +Final holdings for ZI Agent 575 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9944200 }. Marked to market: 9944200 +Final holdings for ZI Agent 576 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29915800 }. Marked to market: 10110000 +Final holdings for ZI Agent 577 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9969900 }. Marked to market: 9969900 +Final holdings for ZI Agent 578 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30045400 }. Marked to market: 10239600 +Final holdings for ZI Agent 579 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20083700 }. Marked to market: 10180800 +Final holdings for ZI Agent 580 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10055200 }. Marked to market: 9750600 +Final holdings for ZI Agent 581 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: -85800 }. Marked to market: 9817100 +Final holdings for ZI Agent 582 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9974100 }. Marked to market: 9974100 +Final holdings for ZI Agent 583 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10025400 }. Marked to market: 10025400 +Final holdings for ZI Agent 584 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9956000 }. Marked to market: 9956000 +Final holdings for ZI Agent 585 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9999800 }. Marked to market: 9999800 +Final holdings for ZI Agent 586 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10048000 }. Marked to market: 9757800 +Final holdings for ZI Agent 587 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9932100 }. Marked to market: 9932100 +Final holdings for ZI Agent 588 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10157000 }. Marked to market: 10157000 +Final holdings for ZI Agent 589 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19992800 }. Marked to market: 10089900 +Final holdings for ZI Agent 590 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9915000 }. Marked to market: 9915000 +Final holdings for ZI Agent 591 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19904100 }. Marked to market: 9804600 +Final holdings for ZI Agent 592 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29998600 }. Marked to market: 10192800 +Final holdings for ZI Agent 593 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10078000 }. Marked to market: 10078000 +Final holdings for ZI Agent 594 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29888700 }. Marked to market: 10082900 +Final holdings for ZI Agent 595 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9987400 }. Marked to market: 9818400 +Final holdings for ZI Agent 596 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10019200 }. Marked to market: 10019200 +Final holdings for ZI Agent 597 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19958500 }. Marked to market: 9750200 +Final holdings for ZI Agent 598 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10016700 }. Marked to market: 10016700 +Final holdings for ZI Agent 599 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19990000 }. Marked to market: 10050600 +Final holdings for ZI Agent 600 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -500, CASH: 59872300 }. Marked to market: 10357800 +Final holdings for ZI Agent 601 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 43300 }. Marked to market: 9946200 +Final holdings for ZI Agent 602 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 180800 }. Marked to market: 10083700 +Final holdings for ZI Agent 603 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10020800 }. Marked to market: 10020800 +Final holdings for ZI Agent 604 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29961000 }. Marked to market: 10155200 +Final holdings for ZI Agent 605 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20084600 }. Marked to market: 10181700 +Final holdings for ZI Agent 606 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20291300 }. Marked to market: 10388400 +Final holdings for ZI Agent 607 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29965700 }. Marked to market: 10159900 +Final holdings for ZI Agent 608 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20035500 }. Marked to market: 10132600 +Final holdings for ZI Agent 609 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20043200 }. Marked to market: 10140300 +Final holdings for ZI Agent 610 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -20120000 }. Marked to market: 9588700 +Final holdings for ZI Agent 611 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 181200 }. Marked to market: 10084100 +Final holdings for ZI Agent 612 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10043300 }. Marked to market: 10043300 +Final holdings for ZI Agent 613 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9859700 }. Marked to market: 9859700 +Final holdings for ZI Agent 614 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 61300 }. Marked to market: 9964200 +Final holdings for ZI Agent 615 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20127700 }. Marked to market: 10224800 +Final holdings for ZI Agent 616 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 39959900 }. Marked to market: 10251200 +Final holdings for ZI Agent 617 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9955900 }. Marked to market: 9955900 +Final holdings for ZI Agent 618 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20006800 }. Marked to market: 10103900 +Final holdings for ZI Agent 619 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 12200 }. Marked to market: 9934200 +Final holdings for ZI Agent 620 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20012800 }. Marked to market: 10109900 +Final holdings for ZI Agent 621 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20089600 }. Marked to market: 10186700 +Final holdings for ZI Agent 622 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10020200 }. Marked to market: 10020200 +Final holdings for ZI Agent 623 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30070400 }. Marked to market: 10264600 +Final holdings for ZI Agent 624 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10016700 }. Marked to market: 10016700 +Final holdings for ZI Agent 625 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19929100 }. Marked to market: 9779600 +Final holdings for ZI Agent 626 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10054400 }. Marked to market: 9751400 +Final holdings for ZI Agent 627 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9971300 }. Marked to market: 9971300 +Final holdings for ZI Agent 628 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -20089200 }. Marked to market: 9619500 +Final holdings for ZI Agent 629 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19936200 }. Marked to market: 9772500 +Final holdings for ZI Agent 630 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10011400 }. Marked to market: 9794400 +Final holdings for ZI Agent 631 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30050500 }. Marked to market: 10244700 +Final holdings for ZI Agent 632 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10094200 }. Marked to market: 10094200 +Final holdings for ZI Agent 633 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29828800 }. Marked to market: 10023000 +Final holdings for ZI Agent 634 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 37500 }. Marked to market: 9940400 +Final holdings for ZI Agent 635 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40156600 }. Marked to market: 10447900 +Final holdings for ZI Agent 636 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 500, CASH: -40143500 }. Marked to market: 9371000 +Final holdings for ZI Agent 637 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9880400 }. Marked to market: 9880400 +Final holdings for ZI Agent 638 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -400, CASH: 50002800 }. Marked to market: 10391200 +Final holdings for ZI Agent 639 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19961600 }. Marked to market: 10058700 +Final holdings for ZI Agent 640 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19985600 }. Marked to market: 10082700 +Final holdings for ZI Agent 641 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10000500 }. Marked to market: 9805300 +Final holdings for ZI Agent 642 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9998200 }. Marked to market: 9998200 +Final holdings for ZI Agent 643 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 400, CASH: -30117300 }. Marked to market: 9494300 +Final holdings for ZI Agent 644 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 49200 }. Marked to market: 9952100 +Final holdings for ZI Agent 645 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 39872100 }. Marked to market: 10163400 +Final holdings for ZI Agent 646 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19996100 }. Marked to market: 10093200 +Final holdings for ZI Agent 647 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10002300 }. Marked to market: 10002300 +Final holdings for ZI Agent 648 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: -7200 }. Marked to market: 9895700 +Final holdings for ZI Agent 649 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10005900 }. Marked to market: 9799900 +Final holdings for ZI Agent 650 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9928900 }. Marked to market: 9876900 +Final holdings for ZI Agent 651 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 0 }. Marked to market: 9902900 +Final holdings for ZI Agent 652 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19948400 }. Marked to market: 10045500 +Final holdings for ZI Agent 653 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40119300 }. Marked to market: 10410600 +Final holdings for ZI Agent 654 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19891600 }. Marked to market: 9927800 +Final holdings for ZI Agent 655 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9947100 }. Marked to market: 9947100 +Final holdings for ZI Agent 656 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40029400 }. Marked to market: 10320700 +Final holdings for ZI Agent 657 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -20029200 }. Marked to market: 9679500 +Final holdings for ZI Agent 658 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10065000 }. Marked to market: 9740800 +Final holdings for ZI Agent 659 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 97500 }. Marked to market: 10000400 +Final holdings for ZI Agent 660 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20046000 }. Marked to market: 10143100 +Final holdings for ZI Agent 661 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19879500 }. Marked to market: 9829200 +Final holdings for ZI Agent 662 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 70400 }. Marked to market: 9973300 +Final holdings for ZI Agent 663 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 500, CASH: -40243600 }. Marked to market: 9270900 +Final holdings for ZI Agent 664 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19866100 }. Marked to market: 9842600 +Final holdings for ZI Agent 665 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10149800 }. Marked to market: 10149800 +Final holdings for ZI Agent 666 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 121400 }. Marked to market: 10024300 +Final holdings for ZI Agent 667 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -400, CASH: 49951000 }. Marked to market: 10339400 +Final holdings for ZI Agent 668 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29962400 }. Marked to market: 10156600 +Final holdings for ZI Agent 669 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10064100 }. Marked to market: 10064100 +Final holdings for ZI Agent 670 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 19000 }. Marked to market: 9921900 +Final holdings for ZI Agent 671 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 66100 }. Marked to market: 9969000 +Final holdings for ZI Agent 672 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10129000 }. Marked to market: 10129000 +Final holdings for ZI Agent 673 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10058300 }. Marked to market: 10058300 +Final holdings for ZI Agent 674 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 42800 }. Marked to market: 9945700 +Final holdings for ZI Agent 675 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -20112900 }. Marked to market: 9595800 +Final holdings for ZI Agent 676 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 102700 }. Marked to market: 10005600 +Final holdings for ZI Agent 677 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9946900 }. Marked to market: 9858900 +Final holdings for ZI Agent 678 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10259500 }. Marked to market: 10259500 +Final holdings for ZI Agent 679 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9992900 }. Marked to market: 9992900 +Final holdings for ZI Agent 680 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -400, CASH: 50235200 }. Marked to market: 10623600 +Final holdings for ZI Agent 681 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29943300 }. Marked to market: 10045100 +Final holdings for ZI Agent 682 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19951600 }. Marked to market: 10048700 +Final holdings for ZI Agent 683 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9995500 }. Marked to market: 9810300 +Final holdings for ZI Agent 684 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 9917500 }. Marked to market: 9917500 +Final holdings for ZI Agent 685 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29937500 }. Marked to market: 10131700 +Final holdings for ZI Agent 686 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 29942000 }. Marked to market: 10136200 +Final holdings for ZI Agent 687 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19988500 }. Marked to market: 9720200 +Final holdings for ZI Agent 688 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10073600 }. Marked to market: 10073600 +Final holdings for ZI Agent 689 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -20006600 }. Marked to market: 9702100 +Final holdings for ZI Agent 690 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 39877800 }. Marked to market: 10169100 +Final holdings for ZI Agent 691 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10141600 }. Marked to market: 9664200 +Final holdings for ZI Agent 692 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30015200 }. Marked to market: 10209400 +Final holdings for ZI Agent 693 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30096000 }. Marked to market: 10290200 +Final holdings for ZI Agent 694 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 25900 }. Marked to market: 9928800 +Final holdings for ZI Agent 695 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19936300 }. Marked to market: 10033400 +Final holdings for ZI Agent 696 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -600, CASH: 69936500 }. Marked to market: 10519100 +Final holdings for ZI Agent 697 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 23400 }. Marked to market: 9926300 +Final holdings for ZI Agent 698 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19944100 }. Marked to market: 9764600 +Final holdings for ZI Agent 699 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -200, CASH: 30116200 }. Marked to market: 10310400 +Final holdings for ZI Agent 700 Type 5 [0 <= R <= 2000, eta=0.8]: { CASH: 10044100 }. Marked to market: 10044100 +Final holdings for ZI Agent 701 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 19953700 }. Marked to market: 10050800 +Final holdings for ZI Agent 702 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 39959000 }. Marked to market: 10250300 +Final holdings for ZI Agent 703 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10036600 }. Marked to market: 10014400 +Final holdings for ZI Agent 704 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 40030800 }. Marked to market: 10322100 +Final holdings for ZI Agent 705 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -300, CASH: 39938600 }. Marked to market: 9898700 +Final holdings for ZI Agent 706 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19899300 }. Marked to market: 9809400 +Final holdings for ZI Agent 707 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 300, CASH: -19933500 }. Marked to market: 9775200 +Final holdings for ZI Agent 708 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20070900 }. Marked to market: 10168000 +Final holdings for ZI Agent 709 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 81100 }. Marked to market: 9984000 +Final holdings for ZI Agent 710 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -10011200 }. Marked to market: 9794600 +Final holdings for ZI Agent 711 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 100, CASH: 143400 }. Marked to market: 10046300 +Final holdings for ZI Agent 712 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 500, CASH: -40214700 }. Marked to market: 9299800 +Final holdings for ZI Agent 713 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20142800 }. Marked to market: 10239900 +Final holdings for ZI Agent 714 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: 200, CASH: -9960700 }. Marked to market: 9845100 +Final holdings for ZI Agent 715 Type 5 [0 <= R <= 2000, eta=0.8]: { JPM: -100, CASH: 20091400 }. Marked to market: 10184800 +Final holdings for ZI Agent 716 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -72800 }. Marked to market: 9830100 +Final holdings for ZI Agent 717 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 50009600 }. Marked to market: 10398000 +Final holdings for ZI Agent 718 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 89300 }. Marked to market: 9992200 +Final holdings for ZI Agent 719 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20020600 }. Marked to market: 10117700 +Final holdings for ZI Agent 720 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19840100 }. Marked to market: 9937200 +Final holdings for ZI Agent 721 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19952900 }. Marked to market: 10050000 +Final holdings for ZI Agent 722 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9888500 }. Marked to market: 9888500 +Final holdings for ZI Agent 723 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 119700 }. Marked to market: 10022600 +Final holdings for ZI Agent 724 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20167200 }. Marked to market: 10264300 +Final holdings for ZI Agent 725 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19924200 }. Marked to market: 10021300 +Final holdings for ZI Agent 726 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -29822200 }. Marked to market: 9789400 +Final holdings for ZI Agent 727 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9933900 }. Marked to market: 9871900 +Final holdings for ZI Agent 728 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10028700 }. Marked to market: 10028700 +Final holdings for ZI Agent 729 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 500, CASH: -39967100 }. Marked to market: 9547400 +Final holdings for ZI Agent 730 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19943800 }. Marked to market: 10040900 +Final holdings for ZI Agent 731 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10187000 }. Marked to market: 10187000 +Final holdings for ZI Agent 732 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -94800 }. Marked to market: 9808100 +Final holdings for ZI Agent 733 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20002000 }. Marked to market: 9706700 +Final holdings for ZI Agent 734 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19947500 }. Marked to market: 10044600 +Final holdings for ZI Agent 735 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40070900 }. Marked to market: 10362200 +Final holdings for ZI Agent 736 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -500, CASH: 60020200 }. Marked to market: 10505700 +Final holdings for ZI Agent 737 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20013700 }. Marked to market: 10110800 +Final holdings for ZI Agent 738 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20168300 }. Marked to market: 9540400 +Final holdings for ZI Agent 739 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -15500 }. Marked to market: 9887400 +Final holdings for ZI Agent 740 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30068500 }. Marked to market: 10262700 +Final holdings for ZI Agent 741 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30198100 }. Marked to market: 10392300 +Final holdings for ZI Agent 742 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9955900 }. Marked to market: 9849900 +Final holdings for ZI Agent 743 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10036100 }. Marked to market: 10036100 +Final holdings for ZI Agent 744 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20020500 }. Marked to market: 10117600 +Final holdings for ZI Agent 745 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 81700 }. Marked to market: 9984600 +Final holdings for ZI Agent 746 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 54600 }. Marked to market: 9957500 +Final holdings for ZI Agent 747 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29785900 }. Marked to market: 9980100 +Final holdings for ZI Agent 748 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10048100 }. Marked to market: 10048100 +Final holdings for ZI Agent 749 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10090800 }. Marked to market: 10090800 +Final holdings for ZI Agent 750 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -30165900 }. Marked to market: 9445700 +Final holdings for ZI Agent 751 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19827400 }. Marked to market: 9924500 +Final holdings for ZI Agent 752 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19990800 }. Marked to market: 9990000 +Final holdings for ZI Agent 753 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9839300 }. Marked to market: 9966500 +Final holdings for ZI Agent 754 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -600, CASH: 69916900 }. Marked to market: 10499500 +Final holdings for ZI Agent 755 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9984100 }. Marked to market: 9821700 +Final holdings for ZI Agent 756 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10147000 }. Marked to market: 10147000 +Final holdings for ZI Agent 757 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -29916700 }. Marked to market: 9694900 +Final holdings for ZI Agent 758 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30066300 }. Marked to market: 10260500 +Final holdings for ZI Agent 759 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40190100 }. Marked to market: 10481400 +Final holdings for ZI Agent 760 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -94500 }. Marked to market: 9808400 +Final holdings for ZI Agent 761 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19802500 }. Marked to market: 9899600 +Final holdings for ZI Agent 762 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19660600 }. Marked to market: 9757700 +Final holdings for ZI Agent 763 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29934000 }. Marked to market: 10128200 +Final holdings for ZI Agent 764 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30131500 }. Marked to market: 10325700 +Final holdings for ZI Agent 765 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -39800 }. Marked to market: 9863100 +Final holdings for ZI Agent 766 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 53100 }. Marked to market: 9956000 +Final holdings for ZI Agent 767 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19911200 }. Marked to market: 9797500 +Final holdings for ZI Agent 768 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10146500 }. Marked to market: 9659300 +Final holdings for ZI Agent 769 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -29953900 }. Marked to market: 9657700 +Final holdings for ZI Agent 770 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39972500 }. Marked to market: 10263800 +Final holdings for ZI Agent 771 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 175200 }. Marked to market: 10078100 +Final holdings for ZI Agent 772 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9939000 }. Marked to market: 9866800 +Final holdings for ZI Agent 773 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10009900 }. Marked to market: 10009900 +Final holdings for ZI Agent 774 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 50307000 }. Marked to market: 10695400 +Final holdings for ZI Agent 775 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10283100 }. Marked to market: 10283100 +Final holdings for ZI Agent 776 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19992900 }. Marked to market: 9715800 +Final holdings for ZI Agent 777 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9971900 }. Marked to market: 9971900 +Final holdings for ZI Agent 778 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10188700 }. Marked to market: 10188700 +Final holdings for ZI Agent 779 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19875700 }. Marked to market: 9972800 +Final holdings for ZI Agent 780 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10176200 }. Marked to market: 10176200 +Final holdings for ZI Agent 781 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20069200 }. Marked to market: 9639500 +Final holdings for ZI Agent 782 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10145600 }. Marked to market: 9660200 +Final holdings for ZI Agent 783 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19921400 }. Marked to market: 10018500 +Final holdings for ZI Agent 784 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9838600 }. Marked to market: 9967200 +Final holdings for ZI Agent 785 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 50102200 }. Marked to market: 10490600 +Final holdings for ZI Agent 786 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19945400 }. Marked to market: 10042500 +Final holdings for ZI Agent 787 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29872200 }. Marked to market: 10066400 +Final holdings for ZI Agent 788 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40264200 }. Marked to market: 10555500 +Final holdings for ZI Agent 789 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30133800 }. Marked to market: 10328000 +Final holdings for ZI Agent 790 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -30102900 }. Marked to market: 9508700 +Final holdings for ZI Agent 791 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39779900 }. Marked to market: 10071200 +Final holdings for ZI Agent 792 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20076900 }. Marked to market: 10174000 +Final holdings for ZI Agent 793 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -30140600 }. Marked to market: 9471000 +Final holdings for ZI Agent 794 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 79900 }. Marked to market: 9982800 +Final holdings for ZI Agent 795 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 56000 }. Marked to market: 9958900 +Final holdings for ZI Agent 796 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -68200 }. Marked to market: 9834700 +Final holdings for ZI Agent 797 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -4400 }. Marked to market: 9898500 +Final holdings for ZI Agent 798 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 49730700 }. Marked to market: 10119100 +Final holdings for ZI Agent 799 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10088000 }. Marked to market: 10088000 +Final holdings for ZI Agent 800 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19937100 }. Marked to market: 9771600 +Final holdings for ZI Agent 801 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10168200 }. Marked to market: 10168200 +Final holdings for ZI Agent 802 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29905500 }. Marked to market: 10099700 +Final holdings for ZI Agent 803 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 6500 }. Marked to market: 9909400 +Final holdings for ZI Agent 804 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40136900 }. Marked to market: 10428200 +Final holdings for ZI Agent 805 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19867200 }. Marked to market: 9964300 +Final holdings for ZI Agent 806 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 58700 }. Marked to market: 9961600 +Final holdings for ZI Agent 807 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30153500 }. Marked to market: 10347700 +Final holdings for ZI Agent 808 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -46900 }. Marked to market: 9856000 +Final holdings for ZI Agent 809 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10009200 }. Marked to market: 9796600 +Final holdings for ZI Agent 810 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 40165100 }. Marked to market: 10456400 +Final holdings for ZI Agent 811 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10104700 }. Marked to market: 10104700 +Final holdings for ZI Agent 812 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20040100 }. Marked to market: 9668600 +Final holdings for ZI Agent 813 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 800, CASH: -70153900 }. Marked to market: 9069300 +Final holdings for ZI Agent 814 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9921500 }. Marked to market: 9884300 +Final holdings for ZI Agent 815 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39981500 }. Marked to market: 10272800 +Final holdings for ZI Agent 816 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20129800 }. Marked to market: 9578900 +Final holdings for ZI Agent 817 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20126400 }. Marked to market: 10223500 +Final holdings for ZI Agent 818 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30143800 }. Marked to market: 10338000 +Final holdings for ZI Agent 819 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 99200 }. Marked to market: 10002100 +Final holdings for ZI Agent 820 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10002900 }. Marked to market: 10002900 +Final holdings for ZI Agent 821 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 500, CASH: -39778300 }. Marked to market: 9736200 +Final holdings for ZI Agent 822 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10249800 }. Marked to market: 9556000 +Final holdings for ZI Agent 823 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19878900 }. Marked to market: 9829800 +Final holdings for ZI Agent 824 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9797700 }. Marked to market: 9797700 +Final holdings for ZI Agent 825 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39879300 }. Marked to market: 10170600 +Final holdings for ZI Agent 826 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30047500 }. Marked to market: 10241700 +Final holdings for ZI Agent 827 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -500, CASH: 60011200 }. Marked to market: 10496700 +Final holdings for ZI Agent 828 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 30147400 }. Marked to market: 10341600 +Final holdings for ZI Agent 829 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10029200 }. Marked to market: 10029200 +Final holdings for ZI Agent 830 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 49000 }. Marked to market: 9951900 +Final holdings for ZI Agent 831 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 47900 }. Marked to market: 9950800 +Final holdings for ZI Agent 832 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -19986100 }. Marked to market: 9722600 +Final holdings for ZI Agent 833 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 9906900 }. Marked to market: 9906900 +Final holdings for ZI Agent 834 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 400, CASH: -30067000 }. Marked to market: 9544600 +Final holdings for ZI Agent 835 Type 6 [250 <= R <= 500, eta=0.8]: { CASH: 10022000 }. Marked to market: 10022000 +Final holdings for ZI Agent 836 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29997500 }. Marked to market: 10191700 +Final holdings for ZI Agent 837 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 50216000 }. Marked to market: 10604400 +Final holdings for ZI Agent 838 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20106500 }. Marked to market: 9602200 +Final holdings for ZI Agent 839 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: 261900 }. Marked to market: 10257100 +Final holdings for ZI Agent 840 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 100, CASH: -207000 }. Marked to market: 9695900 +Final holdings for ZI Agent 841 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10046400 }. Marked to market: 9759400 +Final holdings for ZI Agent 842 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39917600 }. Marked to market: 10208900 +Final holdings for ZI Agent 843 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9892500 }. Marked to market: 9913300 +Final holdings for ZI Agent 844 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -200, CASH: 29975200 }. Marked to market: 10169400 +Final holdings for ZI Agent 845 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20095800 }. Marked to market: 10192900 +Final holdings for ZI Agent 846 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39734500 }. Marked to market: 10025800 +Final holdings for ZI Agent 847 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10010400 }. Marked to market: 9795400 +Final holdings for ZI Agent 848 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -300, CASH: 39989500 }. Marked to market: 10280800 +Final holdings for ZI Agent 849 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -10009300 }. Marked to market: 9796500 +Final holdings for ZI Agent 850 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19994400 }. Marked to market: 10091500 +Final holdings for ZI Agent 851 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -400, CASH: 49897100 }. Marked to market: 10285500 +Final holdings for ZI Agent 852 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20119200 }. Marked to market: 10216300 +Final holdings for ZI Agent 853 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20032800 }. Marked to market: 10129900 +Final holdings for ZI Agent 854 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 20200300 }. Marked to market: 10297400 +Final holdings for ZI Agent 855 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: -100, CASH: 19995800 }. Marked to market: 10092900 +Final holdings for ZI Agent 856 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 200, CASH: -9788300 }. Marked to market: 10017500 +Final holdings for ZI Agent 857 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20196000 }. Marked to market: 9512700 +Final holdings for ZI Agent 858 Type 6 [250 <= R <= 500, eta=0.8]: { JPM: 300, CASH: -20099100 }. Marked to market: 9609600 +Final holdings for ZI Agent 859 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40166300 }. Marked to market: 10457600 +Final holdings for ZI Agent 860 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -29745400 }. Marked to market: 9866200 +Final holdings for ZI Agent 861 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9893100 }. Marked to market: 9893100 +Final holdings for ZI Agent 862 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29767300 }. Marked to market: 9961500 +Final holdings for ZI Agent 863 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -254700 }. Marked to market: 9648200 +Final holdings for ZI Agent 864 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 107300 }. Marked to market: 10010200 +Final holdings for ZI Agent 865 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10012700 }. Marked to market: 10012700 +Final holdings for ZI Agent 866 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -63000 }. Marked to market: 9839900 +Final holdings for ZI Agent 867 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10215700 }. Marked to market: 9590100 +Final holdings for ZI Agent 868 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -19895000 }. Marked to market: 9813700 +Final holdings for ZI Agent 869 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9841200 }. Marked to market: 9841200 +Final holdings for ZI Agent 870 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20006200 }. Marked to market: 9702500 +Final holdings for ZI Agent 871 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49870400 }. Marked to market: 10258800 +Final holdings for ZI Agent 872 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50207600 }. Marked to market: 10596000 +Final holdings for ZI Agent 873 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9946200 }. Marked to market: 9859600 +Final holdings for ZI Agent 874 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9926100 }. Marked to market: 9926100 +Final holdings for ZI Agent 875 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10196900 }. Marked to market: 10196900 +Final holdings for ZI Agent 876 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9719600 }. Marked to market: 9719600 +Final holdings for ZI Agent 877 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20032000 }. Marked to market: 10129100 +Final holdings for ZI Agent 878 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 105700 }. Marked to market: 10008600 +Final holdings for ZI Agent 879 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50185600 }. Marked to market: 10574000 +Final holdings for ZI Agent 880 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20043800 }. Marked to market: 9664900 +Final holdings for ZI Agent 881 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20103300 }. Marked to market: 9605400 +Final holdings for ZI Agent 882 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20099300 }. Marked to market: 9609400 +Final holdings for ZI Agent 883 Type 7 [250 <= R <= 500, eta=1]: { JPM: -500, CASH: 60040200 }. Marked to market: 10525700 +Final holdings for ZI Agent 884 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39897500 }. Marked to market: 10188800 +Final holdings for ZI Agent 885 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10027700 }. Marked to market: 10027700 +Final holdings for ZI Agent 886 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40204000 }. Marked to market: 10495300 +Final holdings for ZI Agent 887 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40006200 }. Marked to market: 10297500 +Final holdings for ZI Agent 888 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50060800 }. Marked to market: 10449200 +Final holdings for ZI Agent 889 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -79900 }. Marked to market: 9823000 +Final holdings for ZI Agent 890 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30038700 }. Marked to market: 10232900 +Final holdings for ZI Agent 891 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 81400 }. Marked to market: 9984300 +Final holdings for ZI Agent 892 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39975800 }. Marked to market: 10267100 +Final holdings for ZI Agent 893 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 268200 }. Marked to market: 10171100 +Final holdings for ZI Agent 894 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -154100 }. Marked to market: 9748800 +Final holdings for ZI Agent 895 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 86700 }. Marked to market: 9989600 +Final holdings for ZI Agent 896 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50049200 }. Marked to market: 10437600 +Final holdings for ZI Agent 897 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -30327200 }. Marked to market: 9284400 +Final holdings for ZI Agent 898 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30049800 }. Marked to market: 10244000 +Final holdings for ZI Agent 899 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10036200 }. Marked to market: 9769600 +Final holdings for ZI Agent 900 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9994600 }. Marked to market: 9811200 +Final holdings for ZI Agent 901 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40036700 }. Marked to market: 10328000 +Final holdings for ZI Agent 902 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10026100 }. Marked to market: 10026100 +Final holdings for ZI Agent 903 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -30023800 }. Marked to market: 9587800 +Final holdings for ZI Agent 904 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10069600 }. Marked to market: 10069600 +Final holdings for ZI Agent 905 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40084300 }. Marked to market: 10375600 +Final holdings for ZI Agent 906 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 318800 }. Marked to market: 10221700 +Final holdings for ZI Agent 907 Type 7 [250 <= R <= 500, eta=1]: { JPM: -800, CASH: 89845000 }. Marked to market: 10621800 +Final holdings for ZI Agent 908 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29836400 }. Marked to market: 10030600 +Final holdings for ZI Agent 909 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19866000 }. Marked to market: 9963100 +Final holdings for ZI Agent 910 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 75500 }. Marked to market: 9978400 +Final holdings for ZI Agent 911 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9901900 }. Marked to market: 9903900 +Final holdings for ZI Agent 912 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10076300 }. Marked to market: 10076300 +Final holdings for ZI Agent 913 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10017500 }. Marked to market: 10017500 +Final holdings for ZI Agent 914 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10052000 }. Marked to market: 10052000 +Final holdings for ZI Agent 915 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20047900 }. Marked to market: 10145000 +Final holdings for ZI Agent 916 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19803100 }. Marked to market: 9900200 +Final holdings for ZI Agent 917 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -19845900 }. Marked to market: 9862800 +Final holdings for ZI Agent 918 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20109500 }. Marked to market: 10206600 +Final holdings for ZI Agent 919 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29937400 }. Marked to market: 10131600 +Final holdings for ZI Agent 920 Type 7 [250 <= R <= 500, eta=1]: { JPM: -500, CASH: 60016400 }. Marked to market: 10501900 +Final holdings for ZI Agent 921 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10051800 }. Marked to market: 9754000 +Final holdings for ZI Agent 922 Type 7 [250 <= R <= 500, eta=1]: { JPM: -500, CASH: 60100600 }. Marked to market: 10586100 +Final holdings for ZI Agent 923 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49885300 }. Marked to market: 10273700 +Final holdings for ZI Agent 924 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 68400 }. Marked to market: 9971300 +Final holdings for ZI Agent 925 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39873700 }. Marked to market: 10165000 +Final holdings for ZI Agent 926 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10048300 }. Marked to market: 10048300 +Final holdings for ZI Agent 927 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9808400 }. Marked to market: 9997400 +Final holdings for ZI Agent 928 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39995700 }. Marked to market: 10287000 +Final holdings for ZI Agent 929 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19747000 }. Marked to market: 9844100 +Final holdings for ZI Agent 930 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 137500 }. Marked to market: 10040400 +Final holdings for ZI Agent 931 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -125200 }. Marked to market: 9777700 +Final holdings for ZI Agent 932 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9995600 }. Marked to market: 9995600 +Final holdings for ZI Agent 933 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 71500 }. Marked to market: 9974400 +Final holdings for ZI Agent 934 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29809800 }. Marked to market: 10004000 +Final holdings for ZI Agent 935 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20019400 }. Marked to market: 9689300 +Final holdings for ZI Agent 936 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 34700 }. Marked to market: 9937600 +Final holdings for ZI Agent 937 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9836200 }. Marked to market: 9836200 +Final holdings for ZI Agent 938 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 101000 }. Marked to market: 10003900 +Final holdings for ZI Agent 939 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -19933100 }. Marked to market: 9775600 +Final holdings for ZI Agent 940 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19826200 }. Marked to market: 9923300 +Final holdings for ZI Agent 941 Type 7 [250 <= R <= 500, eta=1]: { JPM: 400, CASH: -30000600 }. Marked to market: 9611000 +Final holdings for ZI Agent 942 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9896400 }. Marked to market: 9909400 +Final holdings for ZI Agent 943 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20057600 }. Marked to market: 10154700 +Final holdings for ZI Agent 944 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -19973400 }. Marked to market: 9773700 +Final holdings for ZI Agent 945 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49895000 }. Marked to market: 10283400 +Final holdings for ZI Agent 946 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19936100 }. Marked to market: 10033200 +Final holdings for ZI Agent 947 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30075700 }. Marked to market: 10269900 +Final holdings for ZI Agent 948 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19977700 }. Marked to market: 10074800 +Final holdings for ZI Agent 949 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20136900 }. Marked to market: 10234000 +Final holdings for ZI Agent 950 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40196700 }. Marked to market: 10488000 +Final holdings for ZI Agent 951 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40054200 }. Marked to market: 10345500 +Final holdings for ZI Agent 952 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 39921200 }. Marked to market: 10212500 +Final holdings for ZI Agent 953 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10162200 }. Marked to market: 10162200 +Final holdings for ZI Agent 954 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10031000 }. Marked to market: 10031000 +Final holdings for ZI Agent 955 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30078000 }. Marked to market: 10272200 +Final holdings for ZI Agent 956 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30052900 }. Marked to market: 10247100 +Final holdings for ZI Agent 957 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10124700 }. Marked to market: 9681100 +Final holdings for ZI Agent 958 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -68300 }. Marked to market: 9834600 +Final holdings for ZI Agent 959 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40102800 }. Marked to market: 10394100 +Final holdings for ZI Agent 960 Type 7 [250 <= R <= 500, eta=1]: { JPM: 500, CASH: -40128000 }. Marked to market: 9386500 +Final holdings for ZI Agent 961 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19946100 }. Marked to market: 10043200 +Final holdings for ZI Agent 962 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9793900 }. Marked to market: 9793900 +Final holdings for ZI Agent 963 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49850600 }. Marked to market: 10239000 +Final holdings for ZI Agent 964 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19964400 }. Marked to market: 10061500 +Final holdings for ZI Agent 965 Type 7 [250 <= R <= 500, eta=1]: { JPM: 500, CASH: -39944200 }. Marked to market: 9570300 +Final holdings for ZI Agent 966 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30027900 }. Marked to market: 10222100 +Final holdings for ZI Agent 967 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9969000 }. Marked to market: 9836800 +Final holdings for ZI Agent 968 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 79900 }. Marked to market: 9982800 +Final holdings for ZI Agent 969 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20056200 }. Marked to market: 10153300 +Final holdings for ZI Agent 970 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29923900 }. Marked to market: 10118100 +Final holdings for ZI Agent 971 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9864800 }. Marked to market: 10068200 +Final holdings for ZI Agent 972 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49891900 }. Marked to market: 10280300 +Final holdings for ZI Agent 973 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 58000 }. Marked to market: 9960900 +Final holdings for ZI Agent 974 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30109200 }. Marked to market: 10303400 +Final holdings for ZI Agent 975 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9927200 }. Marked to market: 9878600 +Final holdings for ZI Agent 976 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29950100 }. Marked to market: 10144300 +Final holdings for ZI Agent 977 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29803500 }. Marked to market: 9997700 +Final holdings for ZI Agent 978 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20038000 }. Marked to market: 9670700 +Final holdings for ZI Agent 979 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10070300 }. Marked to market: 9735500 +Final holdings for ZI Agent 980 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9948500 }. Marked to market: 9948500 +Final holdings for ZI Agent 981 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -10138400 }. Marked to market: 9667400 +Final holdings for ZI Agent 982 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 26300 }. Marked to market: 9929200 +Final holdings for ZI Agent 983 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30145800 }. Marked to market: 10340000 +Final holdings for ZI Agent 984 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -20128100 }. Marked to market: 9580600 +Final holdings for ZI Agent 985 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 30230800 }. Marked to market: 10425000 +Final holdings for ZI Agent 986 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 49991600 }. Marked to market: 10380000 +Final holdings for ZI Agent 987 Type 7 [250 <= R <= 500, eta=1]: { JPM: 300, CASH: -19881900 }. Marked to market: 9826800 +Final holdings for ZI Agent 988 Type 7 [250 <= R <= 500, eta=1]: { JPM: 600, CASH: -49916100 }. Marked to market: 9501300 +Final holdings for ZI Agent 989 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9793300 }. Marked to market: 9793300 +Final holdings for ZI Agent 990 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 20003300 }. Marked to market: 10100400 +Final holdings for ZI Agent 991 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29974500 }. Marked to market: 10168700 +Final holdings for ZI Agent 992 Type 7 [250 <= R <= 500, eta=1]: { JPM: -300, CASH: 40037000 }. Marked to market: 10328300 +Final holdings for ZI Agent 993 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: -143500 }. Marked to market: 9759400 +Final holdings for ZI Agent 994 Type 7 [250 <= R <= 500, eta=1]: { JPM: -400, CASH: 50033900 }. Marked to market: 10422300 +Final holdings for ZI Agent 995 Type 7 [250 <= R <= 500, eta=1]: { JPM: -200, CASH: 29893200 }. Marked to market: 10087400 +Final holdings for ZI Agent 996 Type 7 [250 <= R <= 500, eta=1]: { JPM: -100, CASH: 19942200 }. Marked to market: 10039300 +Final holdings for ZI Agent 997 Type 7 [250 <= R <= 500, eta=1]: { CASH: 9853200 }. Marked to market: 9853200 +Final holdings for ZI Agent 998 Type 7 [250 <= R <= 500, eta=1]: { CASH: 10021400 }. Marked to market: 10021400 +Final holdings for ZI Agent 999 Type 7 [250 <= R <= 500, eta=1]: { JPM: 100, CASH: 177000 }. Marked to market: 10079900 +Final holdings for ZI Agent 1000 Type 7 [250 <= R <= 500, eta=1]: { JPM: 200, CASH: -9976300 }. Marked to market: 9829500 diff --git a/util/OrderBook.py b/util/OrderBook.py index aaa1a9fa3..e7e48eebe 100644 --- a/util/OrderBook.py +++ b/util/OrderBook.py @@ -8,404 +8,420 @@ from copy import deepcopy -class OrderBook: - - # An OrderBook requires an owning agent object, which it will use to send messages - # outbound via the simulator Kernel (notifications of order creation, rejection, - # cancellation, execution, etc). - def __init__ (self, owner, symbol): - self.owner = owner - self.symbol = symbol - self.bids = [] - self.asks = [] - self.last_trade = None - - # Create an empty list of dictionaries to log the full order book depth (price and volume) each time it changes. - self.book_log = [] - self.quotes_seen = set() - - # Create an order history for the exchange to report to certain agent types. - self.history = [{}] - - - def handleLimitOrder (self, order): - # Matches a limit order or adds it to the order book. Handles partial matches piecewise, - # consuming all possible shares at the best price before moving on, without regard to - # order size "fit" or minimizing number of transactions. Sends one notification per - # match. - 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 - - # Add the order under index 0 of history: orders since the most recent trade. - self.history[0][order.order_id] = { 'entry_time' : self.owner.currentTime, - 'quantity' : order.quantity, 'is_buy_order' : order.is_buy_order, - 'limit_price' : order.limit_price, 'transactions' : [], - 'cancellations' : [] } - - matching = True - - self.prettyPrint() - - executed = [] - - while matching: - matched_order = deepcopy(self.executeOrder(order)) - - if matched_order: - # Decrement quantity on new order and notify traders of execution. - filled_order = deepcopy(order) - filled_order.quantity = matched_order.quantity - filled_order.fill_price = matched_order.fill_price - - order.quantity -= filled_order.quantity - - log_print ("MATCHED: new order {} vs old order {}", filled_order, matched_order) - log_print ("SENT: notifications of order execution to agents {} and {} for orders {} and {}", - filled_order.agent_id, matched_order.agent_id, filled_order.order_id, matched_order.order_id) - - self.owner.sendMessage(order.agent_id, Message({ "msg": "ORDER_EXECUTED", "order": filled_order })) - self.owner.sendMessage(matched_order.agent_id, Message({ "msg": "ORDER_EXECUTED", "order": matched_order })) - - # Accumulate the volume and average share price of the currently executing inbound trade. - executed.append( ( filled_order.quantity, filled_order.fill_price ) ) - - if order.quantity <= 0: - matching = False - - else: - # No matching order was found, so the new order enters the order book. Notify the agent. - self.enterOrder(deepcopy(order)) - - log_print ("ACCEPTED: new order {}", order) - log_print ("SENT: notifications of order acceptance to agent {} for order {}", - order.agent_id, order.order_id) - - self.owner.sendMessage(order.agent_id, Message({ "msg": "ORDER_ACCEPTED", "order": order })) - - matching = False - - if not matching: - # Now that we are done executing or accepting this order, log the new best bid and ask. - if self.bids: - self.owner.logEvent('BEST_BID', "{},{},{}".format(self.symbol, - self.bids[0][0].limit_price, - sum([o.quantity for o in self.bids[0]]))) - - if self.asks: - self.owner.logEvent('BEST_ASK', "{},{},{}".format(self.symbol, - self.asks[0][0].limit_price, - sum([o.quantity for o in self.asks[0]]))) - - # Also log the last trade (total share quantity, average share price). - if executed: - trade_qty = 0 - trade_price = 0 - for q, p in executed: - log_print ("Executed: {} @ {}", q, p) - trade_qty += q - trade_price += (p*q) - - avg_price = int(round(trade_price / trade_qty)) - log_print ("Avg: {} @ ${:0.4f}", trade_qty, avg_price) - self.owner.logEvent('LAST_TRADE', "{},${:0.4f}".format(trade_qty, avg_price)) - - self.last_trade = avg_price - - # Transaction occurred, so advance indices. - self.history.insert(0, {}) - - # Truncate history to required length. - self.history = self.history[:self.owner.stream_history+1] - - - # Finally, log the full depth of the order book, ONLY if we have been requested to store the order book - # for later visualization. (This is slow.) - if self.owner.book_freq is not None: - row = { 'QuoteTime' : self.owner.currentTime } - for quote in self.quotes_seen: - row[quote] = 0 - for quote, volume in self.getInsideBids(): - row[quote] = -volume - self.quotes_seen.add(quote) - for quote, volume in self.getInsideAsks(): - if quote in row: - if row[quote] != 0: - print ("WARNING: THIS IS A REAL PROBLEM: an order book contains bids and asks at the same quote price!") - row[quote] = volume - self.quotes_seen.add(quote) - self.book_log.append(row) - - self.prettyPrint() - - - def executeOrder (self, order): - # Finds a single best match for this order, without regard for quantity. - # Returns the matched order or None if no match found. DOES remove, - # or decrement quantity from, the matched order from the order book - # (i.e. executes at least a partial trade, if possible). - - # Track which (if any) existing order was matched with the current order. - if order.is_buy_order: - book = self.asks - else: - book = self.bids - - # TODO: Simplify? It is ever possible to actually select an execution match - # other than the best bid or best ask? We may not need these execute loops. - - # First, examine the correct side of the order book for a match. - if not book: - # No orders on this side. - return None - elif not self.isMatch(order, book[0][0]): - # There were orders on the right side, but the prices do not overlap. - # Or: bid could not match with best ask, or vice versa. - # Or: bid offer is below the lowest asking price, or vice versa. - return None - else: - # There are orders on the right side, and the new order's price does fall - # somewhere within them. We can/will only match against the oldest order - # among those with the best price. (i.e. best price, then FIFO) - - # Note that book[i] is a LIST of all orders (oldest at index book[i][0]) at the same price. - - # The matched order might be only partially filled. (i.e. new order is smaller) - if order.quantity >= book[0][0].quantity: - # Consumed entire matched order. - matched_order = book[0].pop(0) - - # If the matched price now has no orders, remove it completely. - if not book[0]: - del book[0] - - else: - # Consumed only part of matched order. - matched_order = deepcopy(book[0][0]) - matched_order.quantity = order.quantity - - book[0][0].quantity -= matched_order.quantity - - # When two limit orders are matched, they execute at the price that - # was being "advertised" in the order book. - matched_order.fill_price = matched_order.limit_price - - # Record the transaction in the order history and push the indices - # out one, possibly truncating to the maximum history length. - - # The incoming order is guaranteed to exist under index 0. - self.history[0][order.order_id]['transactions'].append( (self.owner.currentTime, order.quantity) ) - - # The pre-existing order may or may not still be in the recent history. - for idx, orders in enumerate(self.history): - if matched_order.order_id not in orders: continue - - # Found the matched order in history. Update it with this transaction. - self.history[idx][matched_order.order_id]['transactions'].append( - (self.owner.currentTime, matched_order.quantity) ) - - # Return (only the executed portion of) the matched order. - return matched_order - - - - def isMatch (self, order, o): - # Returns True if order 'o' can be matched against input 'order'. - if order.is_buy_order == o.is_buy_order: - print ("WARNING: isMatch() called on orders of same type: {} vs {}".format(order, o)) - return False - - if order.is_buy_order and (order.limit_price >= o.limit_price): - return True - - if not order.is_buy_order and (order.limit_price <= o.limit_price): - return True - - return False - - - def enterOrder (self, order): - # Enters a limit order into the OrderBook in the appropriate location. - # This does not test for matching/executing orders -- this function - # should only be called after a failed match/execution attempt. - - if order.is_buy_order: - book = self.bids - else: - book = self.asks - - if not book: - # There were no orders on this side of the book. - book.append([order]) - elif not self.isBetterPrice(order, book[-1][0]) and not self.isEqualPrice(order, book[-1][0]): - # There were orders on this side, but this order is worse than all of them. - # (New lowest bid or highest ask.) - book.append([order]) - else: - # There are orders on this side. Insert this order in the correct position in the list. - # Note that o is a LIST of all orders (oldest at index 0) at this same price. - for i, o in enumerate(book): - if self.isBetterPrice(order, o[0]): - book.insert(i, [order]) - break - elif self.isEqualPrice(order, o[0]): - book[i].append(order) - break - - - def cancelOrder (self, order): - # Attempts to cancel (the remaining, unexecuted portion of) a trade in the order book. - # By definition, this pretty much has to be a limit order. If the order cannot be found - # in the order book (probably because it was already fully executed), presently there is - # no message back to the agent. This should possibly change to some kind of failed - # cancellation message. (?) Otherwise, the agent receives ORDER_CANCELLED with the - # order as the message body, with the cancelled quantity correctly represented as the - # number of shares that had not already been executed. - - if order.is_buy_order: - book = self.bids - else: - book = self.asks - - # If there are no orders on this side of the book, there is nothing to do. - if not book: return - - # There are orders on this side. Find the price level of the order to cancel, - # then find the exact order and cancel it. - # Note that o is a LIST of all orders (oldest at index 0) at this same price. - for i, o in enumerate(book): - if self.isEqualPrice(order, o[0]): - # This is the correct price level. - for ci, co in enumerate(book[i]): - if order.order_id == co.order_id: - # Cancel this order. - cancelled_order = book[i].pop(ci) - - # Record cancellation of the order if it is still present in the recent history structure. - for idx, orders in enumerate(self.history): - if cancelled_order.order_id not in orders: continue - - # Found the cancelled order in history. Update it with the cancelation. - self.history[idx][cancelled_order.order_id]['cancellations'].append( - (self.owner.currentTime, cancelled_order.quantity) ) - - - # If the cancelled price now has no orders, remove it completely. - if not book[i]: - del book[i] - log_print ("CANCELLED: order {}", order) - log_print ("SENT: notifications of order cancellation to agent {} for order {}", - cancelled_order.agent_id, cancelled_order.order_id) - - self.owner.sendMessage(order.agent_id, Message({ "msg": "ORDER_CANCELLED", "order": cancelled_order })) +class OrderBook: - # We found the order and cancelled it, so stop looking. + # An OrderBook requires an owning agent object, which it will use to send messages + # outbound via the simulator Kernel (notifications of order creation, rejection, + # cancellation, execution, etc). + def __init__(self, owner, symbol): + self.owner = owner + self.symbol = symbol + self.bids = [] + self.asks = [] + self.last_trade = None + + # Create an empty list of dictionaries to log the full order book depth (price and volume) each time it changes. + self.book_log = [] + self.quotes_seen = set() + + # Create an order history for the exchange to report to certain agent types. + self.history = [{}] + + self.mid_price_dict, self.bid_levels_price_dict, self.bid_levels_size_dict, self.ask_levels_price_dict, \ + self.ask_levels_size_dict = dict(), dict(), dict(), dict(), dict() + + def handleLimitOrder(self, order): + # Matches a limit order or adds it to the order book. Handles partial matches piecewise, + # consuming all possible shares at the best price before moving on, without regard to + # order size "fit" or minimizing number of transactions. Sends one notification per + # match. + if order.symbol != self.symbol: + log_print("{} order discarded. Does not match OrderBook symbol: {}", order.symbol, self.symbol) return - 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]['transactions'].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 - - - # Get the inside bid price(s) and share volume available at each price, to a limit - # of "depth". (i.e. inside price, inside 2 prices) Returns a list of tuples: - # list index is best bids (0 is best); each tuple is (price, total shares). - def getInsideBids (self, depth=sys.maxsize): - book = [] - for i in range(min(depth, len(self.bids))): - qty = 0 - price = self.bids[i][0].limit_price - for o in self.bids[i]: - qty += o.quantity - book.append( (price, qty) ) - - return book - - - # As above, except for ask price(s). - def getInsideAsks (self, depth=sys.maxsize): - book = [] - for i in range(min(depth, len(self.asks))): - qty = 0 - price = self.asks[i][0].limit_price - for o in self.asks[i]: - qty += o.quantity - book.append( (price, qty) ) - - return book - - - # These could be moved to the LimitOrder class. We could even operator overload them - # into >, <, ==, etc. - def isBetterPrice (self, order, o): - # Returns True if order has a 'better' price than o. (That is, a higher bid - # or a lower ask.) Must be same order type. - if order.is_buy_order != o.is_buy_order: - print ("WARNING: isBetterPrice() called on orders of different type: {} vs {}".format(order, o)) - return False - - if order.is_buy_order and (order.limit_price > o.limit_price): - return True - - if not order.is_buy_order and (order.limit_price < o.limit_price): - return True - - return False - - - def isEqualPrice (self, order, o): - return order.limit_price == o.limit_price - - - def isSameOrder (self, order, new_order): - return order.order_id == new_order.order_id - - - # Print a nicely-formatted view of the current order book. - def prettyPrint (self, silent=False): - # Start at the highest ask price and move down. Then switch to the highest bid price and move down. - # Show the total volume at each price. If silent is True, return the accumulated string and print nothing. - - # If the global silent flag is set, skip prettyPrinting entirely, as it takes a LOT of time. - if be_silent: return '' - - book = "{} order book as of {}\n".format(self.symbol, self.owner.currentTime) - book += "Last trades: simulated {:d}, historical {:d}\n".format(self.last_trade, - self.owner.oracle.observePrice(self.symbol, self.owner.currentTime, sigma_n = 0, - random_state = self.owner.random_state)) - - book += "{:10s}{:10s}{:10s}\n".format('BID','PRICE','ASK') - book += "{:10s}{:10s}{:10s}\n".format('---','-----','---') - - for quote, volume in self.getInsideAsks()[-1::-1]: - book += "{:10s}{:10s}{:10s}\n".format("", "{:d}".format(quote), "{:d}".format(volume)) - - for quote, volume in self.getInsideBids(): - book += "{:10s}{:10s}{:10s}\n".format("{:d}".format(volume), "{:d}".format(quote), "") - - if silent: return book + 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 - log_print (book) + # Add the order under index 0 of history: orders since the most recent trade. + self.history[0][order.order_id] = {'entry_time': self.owner.currentTime, + 'quantity': order.quantity, 'is_buy_order': order.is_buy_order, + 'limit_price': order.limit_price, 'transactions': [], + 'cancellations': []} + + matching = True + + self.prettyPrint() + + executed = [] + + while matching: + matched_order = deepcopy(self.executeOrder(order)) + + if matched_order: + # Decrement quantity on new order and notify traders of execution. + filled_order = deepcopy(order) + filled_order.quantity = matched_order.quantity + filled_order.fill_price = matched_order.fill_price + + order.quantity -= filled_order.quantity + + log_print("MATCHED: new order {} vs old order {}", filled_order, matched_order) + log_print("SENT: notifications of order execution to agents {} and {} for orders {} and {}", + filled_order.agent_id, matched_order.agent_id, filled_order.order_id, matched_order.order_id) + + self.owner.sendMessage(order.agent_id, Message({"msg": "ORDER_EXECUTED", "order": filled_order})) + self.owner.sendMessage(matched_order.agent_id, + Message({"msg": "ORDER_EXECUTED", "order": matched_order})) + + # Accumulate the volume and average share price of the currently executing inbound trade. + executed.append((filled_order.quantity, filled_order.fill_price)) + + if order.quantity <= 0: + matching = False + + else: + # No matching order was found, so the new order enters the order book. Notify the agent. + self.enterOrder(deepcopy(order)) + + log_print("ACCEPTED: new order {}", order) + log_print("SENT: notifications of order acceptance to agent {} for order {}", + order.agent_id, order.order_id) + + self.owner.sendMessage(order.agent_id, Message({"msg": "ORDER_ACCEPTED", "order": order})) + + matching = False + + if not matching: + # Now that we are done executing or accepting this order, log the new best bid and ask. + if self.bids: + self.owner.logEvent('BEST_BID', "{},{},{}".format(self.symbol, + self.bids[0][0].limit_price, + sum([o.quantity for o in self.bids[0]]))) + + if self.asks: + self.owner.logEvent('BEST_ASK', "{},{},{}".format(self.symbol, + self.asks[0][0].limit_price, + sum([o.quantity for o in self.asks[0]]))) + + # Also log the last trade (total share quantity, average share price). + if executed: + trade_qty = 0 + trade_price = 0 + for q, p in executed: + log_print("Executed: {} @ {}", q, p) + trade_qty += q + trade_price += (p * q) + + avg_price = int(round(trade_price / trade_qty)) + log_print("Avg: {} @ ${:0.4f}", trade_qty, avg_price) + self.owner.logEvent('LAST_TRADE', "{},${:0.4f}".format(trade_qty, avg_price)) + + self.last_trade = avg_price + + # Transaction occurred, so advance indices. + self.history.insert(0, {}) + + # Truncate history to required length. + self.history = self.history[:self.owner.stream_history + 1] + + # Finally, log the full depth of the order book, ONLY if we have been requested to store the order book + # for later visualization. (This is slow.) + if self.owner.book_freq is not None: + row = {'QuoteTime': self.owner.currentTime} + for quote in self.quotes_seen: + row[quote] = 0 + for quote, volume in self.getInsideBids(): + row[quote] = -volume + self.quotes_seen.add(quote) + for quote, volume in self.getInsideAsks(): + if quote in row: + if row[quote] != 0: + print( + "WARNING: THIS IS A REAL PROBLEM: an order book contains bids and asks at the same quote price!") + row[quote] = volume + self.quotes_seen.add(quote) + self.book_log.append(row) + self.updateOrderbookLevelDicts() + self.prettyPrint() + + def executeOrder(self, order): + # Finds a single best match for this order, without regard for quantity. + # Returns the matched order or None if no match found. DOES remove, + # or decrement quantity from, the matched order from the order book + # (i.e. executes at least a partial trade, if possible). + + # Track which (if any) existing order was matched with the current order. + if order.is_buy_order: + book = self.asks + else: + book = self.bids + + # TODO: Simplify? It is ever possible to actually select an execution match + # other than the best bid or best ask? We may not need these execute loops. + + # First, examine the correct side of the order book for a match. + if not book: + # No orders on this side. + return None + elif not self.isMatch(order, book[0][0]): + # There were orders on the right side, but the prices do not overlap. + # Or: bid could not match with best ask, or vice versa. + # Or: bid offer is below the lowest asking price, or vice versa. + return None + else: + # There are orders on the right side, and the new order's price does fall + # somewhere within them. We can/will only match against the oldest order + # among those with the best price. (i.e. best price, then FIFO) + + # Note that book[i] is a LIST of all orders (oldest at index book[i][0]) at the same price. + + # The matched order might be only partially filled. (i.e. new order is smaller) + if order.quantity >= book[0][0].quantity: + # Consumed entire matched order. + matched_order = book[0].pop(0) + + # If the matched price now has no orders, remove it completely. + if not book[0]: + del book[0] + + else: + # Consumed only part of matched order. + matched_order = deepcopy(book[0][0]) + matched_order.quantity = order.quantity + + book[0][0].quantity -= matched_order.quantity + + # When two limit orders are matched, they execute at the price that + # was being "advertised" in the order book. + matched_order.fill_price = matched_order.limit_price + + # Record the transaction in the order history and push the indices + # out one, possibly truncating to the maximum history length. + + # The incoming order is guaranteed to exist under index 0. + self.history[0][order.order_id]['transactions'].append((self.owner.currentTime, order.quantity)) + + # The pre-existing order may or may not still be in the recent history. + for idx, orders in enumerate(self.history): + if matched_order.order_id not in orders: continue + + # Found the matched order in history. Update it with this transaction. + self.history[idx][matched_order.order_id]['transactions'].append( + (self.owner.currentTime, matched_order.quantity)) + + # Return (only the executed portion of) the matched order. + return matched_order + + def isMatch(self, order, o): + # Returns True if order 'o' can be matched against input 'order'. + if order.is_buy_order == o.is_buy_order: + print("WARNING: isMatch() called on orders of same type: {} vs {}".format(order, o)) + return False + + if order.is_buy_order and (order.limit_price >= o.limit_price): + return True + + if not order.is_buy_order and (order.limit_price <= o.limit_price): + return True + + return False + + def enterOrder(self, order): + # Enters a limit order into the OrderBook in the appropriate location. + # This does not test for matching/executing orders -- this function + # should only be called after a failed match/execution attempt. + + if order.is_buy_order: + book = self.bids + else: + book = self.asks + + if not book: + # There were no orders on this side of the book. + book.append([order]) + elif not self.isBetterPrice(order, book[-1][0]) and not self.isEqualPrice(order, book[-1][0]): + # There were orders on this side, but this order is worse than all of them. + # (New lowest bid or highest ask.) + book.append([order]) + else: + # There are orders on this side. Insert this order in the correct position in the list. + # Note that o is a LIST of all orders (oldest at index 0) at this same price. + for i, o in enumerate(book): + if self.isBetterPrice(order, o[0]): + book.insert(i, [order]) + break + elif self.isEqualPrice(order, o[0]): + book[i].append(order) + break + + def cancelOrder(self, order): + # Attempts to cancel (the remaining, unexecuted portion of) a trade in the order book. + # By definition, this pretty much has to be a limit order. If the order cannot be found + # in the order book (probably because it was already fully executed), presently there is + # no message back to the agent. This should possibly change to some kind of failed + # cancellation message. (?) Otherwise, the agent receives ORDER_CANCELLED with the + # order as the message body, with the cancelled quantity correctly represented as the + # number of shares that had not already been executed. + + if order.is_buy_order: + book = self.bids + else: + book = self.asks + + # If there are no orders on this side of the book, there is nothing to do. + if not book: return + + # There are orders on this side. Find the price level of the order to cancel, + # then find the exact order and cancel it. + # Note that o is a LIST of all orders (oldest at index 0) at this same price. + for i, o in enumerate(book): + if self.isEqualPrice(order, o[0]): + # This is the correct price level. + for ci, co in enumerate(book[i]): + if order.order_id == co.order_id: + # Cancel this order. + cancelled_order = book[i].pop(ci) + + # Record cancellation of the order if it is still present in the recent history structure. + for idx, orders in enumerate(self.history): + if cancelled_order.order_id not in orders: continue + + # Found the cancelled order in history. Update it with the cancelation. + self.history[idx][cancelled_order.order_id]['cancellations'].append( + (self.owner.currentTime, cancelled_order.quantity)) + + # If the cancelled price now has no orders, remove it completely. + if not book[i]: + del book[i] + + log_print("CANCELLED: order {}", order) + log_print("SENT: notifications of order cancellation to agent {} for order {}", + cancelled_order.agent_id, cancelled_order.order_id) + + self.owner.sendMessage(order.agent_id, + Message({"msg": "ORDER_CANCELLED", "order": cancelled_order})) + # We found the order and cancelled it, so stop looking. + self.updateOrderbookLevelDicts() + return + + 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]['transactions'].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.updateOrderbookLevelDicts() + self.last_update_ts = self.owner.currentTime + + # Get the inside bid price(s) and share volume available at each price, to a limit + # of "depth". (i.e. inside price, inside 2 prices) Returns a list of tuples: + # list index is best bids (0 is best); each tuple is (price, total shares). + def getInsideBids(self, depth=sys.maxsize): + book = [] + for i in range(min(depth, len(self.bids))): + qty = 0 + price = self.bids[i][0].limit_price + for o in self.bids[i]: + qty += o.quantity + book.append((price, qty)) + + return book + + # As above, except for ask price(s). + def getInsideAsks(self, depth=sys.maxsize): + book = [] + for i in range(min(depth, len(self.asks))): + qty = 0 + price = self.asks[i][0].limit_price + for o in self.asks[i]: + qty += o.quantity + book.append((price, qty)) + + return book + + # These could be moved to the LimitOrder class. We could even operator overload them + # into >, <, ==, etc. + def isBetterPrice(self, order, o): + # Returns True if order has a 'better' price than o. (That is, a higher bid + # or a lower ask.) Must be same order type. + if order.is_buy_order != o.is_buy_order: + print("WARNING: isBetterPrice() called on orders of different type: {} vs {}".format(order, o)) + return False + + if order.is_buy_order and (order.limit_price > o.limit_price): + return True + + if not order.is_buy_order and (order.limit_price < o.limit_price): + return True + + return False + + def isEqualPrice(self, order, o): + return order.limit_price == o.limit_price + + def isSameOrder(self, order, new_order): + return order.order_id == new_order.order_id + + # Print a nicely-formatted view of the current order book. + def prettyPrint(self, silent=False): + # Start at the highest ask price and move down. Then switch to the highest bid price and move down. + # Show the total volume at each price. If silent is True, return the accumulated string and print nothing. + + # If the global silent flag is set, skip prettyPrinting entirely, as it takes a LOT of time. + if be_silent: return '' + + book = "{} order book as of {}\n".format(self.symbol, self.owner.currentTime) + book += "Last trades: simulated {:d}, historical {:d}\n".format(self.last_trade, + self.owner.oracle.observePrice(self.symbol, + self.owner.currentTime, + sigma_n=0, + random_state=self.owner.random_state)) + + book += "{:10s}{:10s}{:10s}\n".format('BID', 'PRICE', 'ASK') + book += "{:10s}{:10s}{:10s}\n".format('---', '-----', '---') + + for quote, volume in self.getInsideAsks()[-1::-1]: + book += "{:10s}{:10s}{:10s}\n".format("", "{:d}".format(quote), "{:d}".format(volume)) + for quote, volume in self.getInsideBids(): + book += "{:10s}{:10s}{:10s}\n".format("{:d}".format(volume), "{:d}".format(quote), "") + + if silent: return book + + log_print(book) + + def updateOrderbookLevelDicts(self): + depth = 10 + if self.asks and self.bids: + self.mid_price_dict[self.owner.currentTime] = (self.asks[0][0].limit_price + self.bids[0][0].limit_price) / 2 + bid_list, ask_list = self.getInsideBids(depth), self.getInsideAsks(depth) + bldp, blds, sldp, slds = {}, {}, {}, {} + for level, order in enumerate(ask_list): + level += 1 + sldp[level], slds[level] = order[0], order[1] + self.ask_levels_price_dict[self.owner.currentTime] = sldp + self.ask_levels_size_dict[self.owner.currentTime] = slds + for level, order in enumerate(bid_list): + level += 1 + bldp[level], blds[level] = order[0], order[1] + self.bid_levels_price_dict[self.owner.currentTime] = bldp + self.bid_levels_size_dict[self.owner.currentTime] = blds \ No newline at end of file diff --git a/util/oracle/MeanRevertingOracle.py b/util/oracle/MeanRevertingOracle.py index c13bdb055..554a54d88 100644 --- a/util/oracle/MeanRevertingOracle.py +++ b/util/oracle/MeanRevertingOracle.py @@ -40,13 +40,12 @@ def __init__(self, mkt_open, mkt_close, 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 generate_fundamental_value_series(self, symbol, r_bar, kappa, sigma_s): # Generates the fundamental value series for a single stock symbol. r_bar is the # mean fundamental value, kappa is the mean reversion coefficient, and sigma_s @@ -127,5 +126,4 @@ def observePrice(self, symbol, currentTime, sigma_n = 1000, random_state = None) log_print ("Oracle: giving client value observation {}", obs) # Reminder: all simulator prices are specified in integer cents. - return obs - + return obs \ No newline at end of file diff --git a/util/oracle/OrderBookOracle.py b/util/oracle/OrderBookOracle.py new file mode 100644 index 000000000..966d5eda0 --- /dev/null +++ b/util/oracle/OrderBookOracle.py @@ -0,0 +1,52 @@ +import pandas as pd +from datetime import datetime +from util.util import log_print + + +class OrderBookOracle: + COLUMNS = ['TIMESTAMP', 'ORDER_ID', 'PRICE', 'SIZE', 'BUY_SELL_FLAG'] + DIRECTION = {0: 'BUY', 1: 'SELL'} + + # Oracle for reading historical exchange orders stream + def __init__(self, symbol, date, start_time, end_time, orders_file_path): + self.symbol = symbol + self.date = date + self.start_time = start_time + self.end_time = end_time + self.orders_file_path = orders_file_path + self.orders_dict = self.processOrders() + self.wakeup_times = [*self.orders_dict] + self.first_wakeup = self.wakeup_times[0] + + print(f"Orders File Path: {self.orders_file_path}") + print(f"Oracle initialized for {self.symbol} and date {self.date.date} " + f"between {str(self.start_time.hour)}: {str(self.start_time.minute)} " + f"and {str(self.end_time.hour)}: {str(self.end_time.minute)}") + + def processOrders(self): + def convertDate(date_str): + try: + return datetime.strptime(date_str, '%Y%m%d%H%M%S.%f') + except ValueError: + return convertDate(date_str[:-1]) + + orders_df = pd.read_csv(self.orders_file_path).iloc[1:] + all_columns = orders_df.columns[0].split('|') + orders_df = orders_df[orders_df.columns[0]].str.split('|', 16, expand=True) + orders_df.columns = all_columns + orders_df = orders_df[OrderBookOracle.COLUMNS] + orders_df['BUY_SELL_FLAG'] = orders_df['BUY_SELL_FLAG'].astype(int).replace(OrderBookOracle.DIRECTION) + orders_df['TIMESTAMP'] = orders_df['TIMESTAMP'].astype(str).apply(convertDate) + orders_df['SIZE'] = orders_df['SIZE'].astype(int) + orders_df['PRICE'] = orders_df['PRICE'].astype(float) + orders_df = orders_df.loc[(orders_df.TIMESTAMP >= self.start_time) & (orders_df.TIMESTAMP < self.end_time)] + orders_df.set_index('TIMESTAMP', inplace=True) + log_print(f"Number of Orders: {len(orders_df)}") + orders_dict = {k: g.to_dict(orient='records') for k, g in orders_df.groupby(level=0)} + return orders_dict + + def getDailyOpenPrice(self, symbol, mkt_open): + return self.orders_dict[list(self.orders_dict.keys())[0]][0]['PRICE'] + + def observePrice(self, symbol, currentTime, sigma_n=0): + return self.getDailyOpenPrice(symbol, mkt_open=currentTime) diff --git a/util/oracle/RandomOrderBookOracle.py b/util/oracle/RandomOrderBookOracle.py deleted file mode 100644 index dd2afbe9d..000000000 --- a/util/oracle/RandomOrderBookOracle.py +++ /dev/null @@ -1,59 +0,0 @@ -import numpy as np -import pandas as pd - -from util.util import log_print - -class RandomOrderBookOracle: - order_id = 0 - - def __init__(self, symbol, - market_open_ts = pd.Timestamp("2019-06-18 09:30:00"), - market_close_ts = pd.Timestamp("2019-06-18 09:35:00"), - buy_price_range = [90, 105], sell_price_range = [95, 110], quantity_range = [50, 500], - seed=None): - self.symbol = symbol - self.market_open_ts = market_open_ts - self.market_close_ts = market_close_ts - self.buy_price_range = buy_price_range - self.sell_price_range = sell_price_range - self.quantity_range = quantity_range - self.random_state = np.random.RandomState(seed=seed) - np.random.seed(seed) - self.trades_df = self.generateTradesDataframe() - log_print("RandomOrderBookOracle initialized for {} and date: {}".format(self.symbol, - str(market_open_ts.date()))) - - def generateRandomTimestamps(self): - start_timestamp = self.market_open_ts + pd.Timedelta('1ms') - timestamp_list = [] - timestamp_list.append(start_timestamp) - current_timestamp = start_timestamp - while current_timestamp < self.market_close_ts: - delta_time = self.random_state.exponential(scale=1.0 / 0.005) - current_timestamp = current_timestamp + pd.Timedelta('{}ms'.format(int(round(delta_time)))) - timestamp_list.append(current_timestamp) - del timestamp_list[-1] - return timestamp_list - - def generateTradesDataframe(self): - trades_df = pd.DataFrame(columns=['timestamp', 'type', 'order_id', 'vol', 'price', 'direction']) - trades_df.timestamp = self.generateRandomTimestamps() - trades_df.set_index('timestamp', inplace=True) - trades_df.type = 'NEW' - for index, row in trades_df.iterrows(): - row['order_id'] = RandomOrderBookOracle.order_id - RandomOrderBookOracle.order_id += 1 - direction = np.random.randint(0, 2) - row['direction'] = 'BUY' if direction == 1 else 'SELL' - row['price'] = np.random.randint(self.buy_price_range[0], self.buy_price_range[1]) \ - if direction == 1 else np.random.randint(self.sell_price_range[0], self.sell_price_range[1]) - row['vol'] = np.random.randint(self.quantity_range[0], self.quantity_range[1]) - RandomOrderBookOracle.order_id = 0 - trades_df.reset_index(inplace=True) - log_print("RandomOrderBookOracle generated with {} synthetic random trades".format(len(trades_df))) - return trades_df - - def getDailyOpenPrice(self, symbol, mkt_open): - price = self.trades_df.iloc[0]['price'] - log_print("Opening price at {} for {}".format(mkt_open, symbol)) - return price \ No newline at end of file diff --git a/util/oracle/SparseMeanRevertingOracle.py b/util/oracle/SparseMeanRevertingOracle.py index 65f114624..e201667b0 100644 --- a/util/oracle/SparseMeanRevertingOracle.py +++ b/util/oracle/SparseMeanRevertingOracle.py @@ -58,7 +58,6 @@ def __init__(self, mkt_open, mkt_close, symbols): for symbol in symbols: s = symbols[symbol] log_print ("SparseMeanRevertingOracle computing initial fundamental value for {}", symbol) - 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'] }] @@ -226,5 +225,4 @@ def observePrice(self, symbol, currentTime, sigma_n = 1000, random_state = None) log_print ("Oracle: giving client value observation {}", obs) # Reminder: all simulator prices are specified in integer cents. - return obs - + return obs \ No newline at end of file diff --git a/util/order/etf/BasketOrder.py b/util/order/etf/BasketOrder.py new file mode 100644 index 000000000..fe4794e0f --- /dev/null +++ b/util/order/etf/BasketOrder.py @@ -0,0 +1,41 @@ +# BasketOrder class, inherits from Order class. These are the +# Orders that typically go in a Primary Exchange and immediately get filled. +# A buy order translates to a creation order for an ETF share +# A sell order translates to a redemption order for shares of the underlying. + +from util.order.Order import Order +from Kernel import Kernel +from agent.FinancialAgent import dollarize + +import sys + +# Module level variable that can be changed by config files. +silent_mode = False + +class BasketOrder (Order): + + def __init__ (self, agent_id, time_placed, symbol, quantity, is_buy_order, dollar=True, order_id=None): + super().__init__(agent_id, time_placed, symbol, quantity, is_buy_order, order_id) + self.dollar = dollar + + def __str__ (self): + if silent_mode: return '' + + filled = '' + if self.dollar: + if self.fill_price: filled = " (filled @ {})".format(dollarize(self.fill_price)) + else: + if self.fill_price: filled = " (filled @ {})".format(self.fill_price) + + # Until we make explicit market orders, we make a few assumptions that EXTREME prices on limit + # orders are trying to represent a market order. This only affects printing - they still hit + # the order book like limit orders, which is wrong. + return "(Order_ID: {} Agent {} @ {}) : {} {} {} @ {}{}".format(self.order_id, self.agent_id, + Kernel.fmtTime(self.time_placed), + "CREATE" if self.is_buy_order else "REDEEM", + self.quantity, self.symbol, + filled, self.fill_price) + + def __repr__ (self): + if silent_mode: return '' + return self.__str__() diff --git a/util/order/etf/__init__.py b/util/order/etf/__init__.py new file mode 100644 index 000000000..e69de29bb