Я строю агент-ориентированную модель, которая имитирует фондовый рынок, в котором разные участники ведут себя полурандомизированным образом. По какой-то причине несколько агентов одного класса ведут одинаковое торговое поведение.
Основа c Вопрос: почему все экземпляры моего класса обновляют свое состояние, а не только один экземпляр?
Ниже я публикую код, который я использую. После этого некоторые пояснительные примечания, чтобы было более понятно, что происходит.
Вот код самой модели, в которой используются некоторые элементы библиотеки моделирования на основе агента Mesa:
'''
Simulate a market. Create company and trader agents, simulate trades,
track macro variables and returns.
'''
def __init__(self,
companies=10, # TK All of these are temp values
noisetraders=2,
marketmakers=5,
fundamentaltraders=0,
systematictraders=0,
passivetraders=0,
rfr = 0.02,
days = 5000):
super().__init__()
self.time = 0
self.days = days # length of simulation
# Macro variables
self.widget_price = 10.00
self.rfr = rfr
self.companies_list = [Company(str(x), self) for x in range(companies)]
self.orderbook = []
# pre-populating trades list so market-makers start somewhere
self.tape = [(x.ticker, 100, 10.00, 0) for x in self.companies_list]
# Update traders list
self.mm_list = [MarketMaker(x, self) for x in range(marketmakers)]
self.traders_list = [NoiseTrader(x, self) for x in range(noisetraders)]
self.traders_list += [FundamentalTrader(x, self) for x in range(fundamentaltraders)]
self.traders_list += [SystematicTrader(x, self) for x in range(systematictraders)]
self.traders_list += [PassiveTrader(x, self) for x in range(passivetraders)]
def step(self):
self.time += 1
self.orderbook = [] # all orders GTC for now
if self.widget_price >= 1.00:
self.widget_price *= (1.0+ random.normalvariate(.0001, 0.01))
else:
self.widget_price += 0.01
for trader in self.traders_list:
for mm in self.mm_list:
mm.step()
trader.step()
def run_model(self):
for i in range(self.days):
self.time = i
self.step()
Вот как выглядит объект Order:
class Order:
'''
An order. All orders are limit orders, but for now traders submit
limit orders at the bid/ask.
Orders can be submitted at arbitrary precision, but must be rounded.
Orders below 0 converted to 0.01
'''
def __init__(self, otype, trader, ticker, px, qt, model):
self.otype = otype
self.trader = trader
self.ticker = ticker
self.px = max(round(px, 2), 0.01)
self.qt = qt
self.model = model
def __repr__(self):
return self.otype+' '+str(self.qt)+' '+self.ticker+' @ '+str(self.px)
def execute(self, counterparty):
if self.otype == 'buy':
self.trader.buy(self.model, counterparty, self.ticker, self.qt, self.px)
else:
self.trader.sell(self.model, counterparty, self.ticker, self.qt, self.px)
self.model.orderbook.remove(self)
Код для базового c класса трейдера:
class Trader(Agent):
'''
A high-level class from which all specific market actors inherit.
'''
def __init__(self,
unique_id,
model,
cash = 10000.00,
positions = defaultdict(int),
gross_range = (0,2.0),
net_range = (-1.0,1.0),
confidence = random.random()):
self.unique_id = unique_id
self.model = model
self.cash = cash
self.positions = positions
self.gross_range = gross_range
self.net_range = net_range
self.confidence = confidence # a general confidence indicator
self.wealth = self.cash + sum([self.positions[ticker] * self.model.last_px(ticker) for ticker in list(self.positions.keys())])
self.wealth_hist = [(0, cash)]
def buy(self, model, seller, ticker, shares, price):
"""Execute a buy trade."""
if ticker not in [x.ticker for x in self.model.companies_list]:
raise Exception("Invalid ticker")
value = shares * price
self.cash -= value
self.positions[ticker] += shares
seller.cash += value
seller.positions[ticker] -= shares
if self.positions[ticker] == 0:
del self.positions[ticker]
if seller.positions[ticker] == 0:
del seller.positions[ticker]
self.model.tape.append((ticker, shares, price, self.model.time))
def sell(self, model, buyer, ticker, shares, price):
"""Execute a sell trade."""
if ticker not in [x.ticker for x in self.model.companies_list]:
raise Exception("Invalid ticker")
value = shares * price
self.cash += value
self.positions[ticker] -= shares
buyer.cash -= value
buyer.positions[ticker] += shares
if self.positions[ticker] == 0:
del self.positions[ticker]
if buyer.positions[ticker] == 0:
del buyer.positions[ticker]
self.model.tape.append((ticker, shares, price, self.model.time))
def net_exposure(self):
'''Returns % net long position'''
net = sum([self.positions[x] * m.last_px(x) for x in self.positions])
return net / self.wealth
def gross_exposure(self):
'''Returns % gross position value'''
gross = sum([abs(self.positions[x] * m.last_px(x)) for x in self.positions])
return gross / self.wealth
def check_margin(self):
'''Return True if margin is below maintenance margin.'''
gross_long = sum([self.positions[x] * m.last_px(x) for x in self.postitions if self.positions[x] >= 0])
gross_short = sum([self.positions[x] * m.last_px(x) for x in self.postitions if self.positions[x] < 0])
if self.cash >= self.wealth:
self.gross_short / self.wealth < 3
else:
self.gross_long / self.wealth < 3
def buying_power(self):
'''Total amount investor can go long.'''
max_gross = self.wealth * self.gross_range[1] - self.gross_exposure()
max_net = self.wealth * self.net_range[1] - self.max_exposure()
return min(max_gross, max_net)
def selling_power(self):
'''Total amount trader call sell (or short).'''
min_net = self.net_exposure() - self.wealth * self.net_range[0]
def margin_liquidation(self, model):
'''
If trade is below maintenance margin, liquididate all positions.
Note: this function is overridden for market-makers.
'''
for p in self.positions:
if self.positions[p] > 0:
model.top_of_book(p, 'sell').execute(self)
if self.positions[p] < 0:
model.top_of_book(p, 'buy').execute(self)
def step(self):
'''
We allow time to start negative so companies can operate for a while
and create fundamental data before traders trade.
'''
if self.model.time < 0:
pass
else:
self.cash = self.cash*(1 + self.model.rfr) ** (1/252)
self.wealth_hist.append((self.model.time, self.wealth))
Код для шумового трейдера:
class NoiseTrader(Trader):
'''
A trader who makes trades mostly at random.
These will be our main source of noise, so we'll vary their number to
calibrate the model.
'''
def __init__(self,
unique_id,
model,
cash = 10000.00,
positions = defaultdict(int),
gross_range = (0,2.0),
net_range = (-1.0,1.0),
confidence = random.random()):
super().__init__(unique_id,
model,
cash,
positions,
gross_range,
net_range,
confidence)
def random_buy(self):
ticker = random.choice([c.ticker for c in self.model.companies_list])
try:
m.top_of_book(ticker, 'buy').execute(self)
except:
print("Buy failed! Ticker: ", ticker)
def random_sell(self):
ticker = random.choice([c.ticker for c in self.model.companies_list])
try:
m.top_of_book(ticker, 'sell').execute(self)
except:
print("Sell failed! Ticker: ", ticker)
def step(self):
self.cash = self.cash*(1 + self.model.rfr) ** (1/252)
if self.gross_exposure() > self.gross_range[1] or self.net_exposure() > self.net_range[1]:
self.random_sell()
if self.net_exposure() < self.net_range[0]:
self.random_buy()
else:
random.choice([self.random_buy(), self.random_sell()])
else:
random.choice([self.random_buy(), self.random_sell()])
self.wealth = self.cash + sum([self.positions[ticker] * self.model.last_px(ticker) for ticker in list(self.positions.keys())])
self.wealth_hist.append((self.model.time, self.wealth))
А для маркет-мейкеров:
class MarketMaker(Trader):
'''Class representing a market-maker or specialist
In this model, market-makers are responsible for posting all limit orders,
i.e. they provide 100% of liquidity. Market-makers have additional risk
criteria:
- Overnight risk represents their preference for holding a position vs
going to flat.
- Net risk represents their willingness to be net long/short.
Both preferences affect the width/direction of spreads they quote.
'''
def __init__(self,
unique_id,
model,
cash = 10000.00,
positions = defaultdict(int),
gross_range = (0,2.0),
net_range = (-1.0,1.0),
confidence = random.random(),
overnight_risk = random.random(),
spread = 0.1): # quoting a 10% wide spread initially
super().__init__(unique_id,
model,
cash,
positions,
gross_range,
net_range,
confidence)
self.confidence = random.random()
self.overnight_risk = overnight_risk
# have to run these again to get new value
# self.owned = [(c[0], self.model.last_px(c[0])) for c in self.positions]
self.spread = spread
def update_risks(self):
'''
Update risk tolerance.
Risk tolerance approaches 1 as wealth rises, or 0 as it falls.
'''
if self.wealth >= sum([x[1] for x in self.wealth_hist[:5]])/len(self.wealth_hist[:5]):
self.confidence += 0.5 * (1-self.confidence)
self.overnight_risk += 0.5 * (1-self.overnight_risk)
else:
self.confidence -= 0.5 * self.confidence
self.overnight_risk -= 0.5 * self.overnight_risk
def update_quotes(self):
'''
Market maker updates order book, quoting a spread in each name based on
their position and risk-tolerance.
'''
tape = self.model.tape
lasts = [(c.ticker, self.model.last_px(c.ticker)) for c in self.model.companies_list]
owned = [(c, self.model.last_px(c)) for c in self.positions]
unowned = [c for c in lasts if c not in owned]
if (not unowned):
pass
else:
for co in unowned:
self.model.orderbook.append(Order('buy',
self,
co[0],
co[1]-(self.spread/(2 * self.confidence)),
100,
self.model))
self.model.orderbook.append(Order('sell',
self,
co[0],
co[1]+(self.spread/(2 * self.confidence)),
100,
self.model))
if (not owned):
pass
else:
for co in owned:
if self.positions[co[0]] > 0:
adj_midpoint = co[1] - (self.spread/2 * self.confidence * self.overnight_risk)
self.model.orderbook.append(Order('buy',
self,
co[0],
adj_midpoint - (self.spread/2 * self.confidence),
100,
self.model))
self.model.orderbook.append(Order('sell',
self,
co[0],
adj_midpoint + (self.spread/2 * self.confidence),
100,
self.model))
else:
adj_midpoint = co[1] + (self.spread/2 * self.confidence * self.overnight_risk)
self.model.orderbook.append(Order('buy',
self,
co[0],
adj_midpoint - (self.spread/2 * self.confidence),
100,
self.model))
self.model.orderbook.append(Order('sell',
self,
co[0],
adj_midpoint + (self.spread/2 * self.confidence),
100,
self.model))
def margin_liquidation(self):
'''
If a market-maker runs out of maintenance margin, they liquidate by
offering extremely wide quotes on anything not in their book, and
and quoting midpoint-vs-extreme on anything still in their book.
'''
pass
def step(self):
self.cash = self.cash*(1 + self.model.rfr) ** (1/252)
self.update_risks()
self.update_quotes()
self.wealth = self.cash + sum([self.positions[ticker] * self.model.last_px(ticker) for ticker in list(self.positions.keys())])
self.wealth_hist.append((self.model.time, self.wealth))
Так как же это предполагается работать?
Чтобы упростить вещи, у меня есть два вида трейдеры: маркет-мейкеры делают то, что делают традиционные маркет-мейкеры и биржевые специалисты; для каждой акции, которую они торгуют, они указывают цену, по которой они будут покупать, и цену, по которой они будут продавать. Они зарабатывают деньги, ожидая, что заказы в значительной степени случайны. В этой модели они ведут себя с упрощенной версией реального поведения на рынке: когда они оптимистично c (т.е. зарабатывают деньги), они предлагают торговать близко к последней цене; если они пессимисты c, они все еще торгуют, но они указывают цены дальше от центра рынка.
Реализация этого заключается в том, что маркет-мейкеры создают несколько экземпляров ордеров, каждый из которых добавляется в книгу заказов модели.
Сложная структура ветвления для маркет-мейкеров предназначена для сохранения их от накопления больших позиций в отдельных акциях; по мере того, как размер их позиции увеличивается, они пытаются более агрессивно довести ее до нуля.
Шумовые трейдеры становятся более прямыми. Пока у них есть сила покупки / продажи, они торгуют. то есть они выбирают открытые ордера в книге заказов и выполняют эти ордера.
Что происходит, когда я на самом деле запускаю, так это то, что каждый раз, когда трейдер с шумом выполняет ордер, у всех трейдеров с шумом есть позиции, и все маркет-мейкеры имеют смещенную позицию