-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathround_4.py
697 lines (615 loc) · 35.4 KB
/
round_4.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
import math
import statistics
from typing import List, Dict, Tuple, Any
from collections import deque, OrderedDict
import jsonpickle
from datamodel import *
class Strategy:
"""
Base Class for Strategy Objects
"""
def __init__(self, state: TradingState, product_config: dict):
# product configuration
self.symbol: Symbol = product_config['SYMBOL']
self.product: Product = product_config['PRODUCT']
self.position_limit: Position = product_config['POSITION_LIMIT']
# extract information from TradingState
self.timestamp = state.timestamp
self.position = state.position.get(self.product, 0)
# prevent reshuffling of order depth
self.bids = OrderedDict(state.order_depths[self.symbol].buy_orders)
self.asks = OrderedDict(state.order_depths[self.symbol].sell_orders)
# build order book features
try:
self.best_bid = max(self.bids.keys())
self.worst_bid = min(self.bids.keys())
except ValueError:
self.best_bid = min(self.asks.keys()) # prevent data corruption from empty bid
self.worst_bid = max(self.asks.keys())
try:
self.best_ask = min(self.asks.keys())
self.worst_ask = max(self.asks.keys())
except ValueError:
self.best_ask = min(self.bids.keys())
self.worst_ask = max(self.bids.keys())
self.bid_volume = sum(self.bids.values()) # sum of all quantities of bids
self.ask_volume = sum(self.asks.values())
self.bid_sweep = sum(p * q for p, q in self.bids.items()) # amount needed to sweep all bids
self.ask_sweep = sum(p * q for p, q in self.asks.items())
try:
self.bid_vwap = self.bid_sweep / self.bid_volume # volume weighted average (VWAP) of bids
except ZeroDivisionError:
self.bid_vwap = self.worst_bid # some occurrence of zero volume
try:
self.ask_vwap = self.ask_sweep / self.ask_volume
except ZeroDivisionError:
self.ask_vwap = self.worst_ask
self.mid_vwap = (self.bid_vwap + self.ask_vwap) / 2 # de-noised mid-price
# initialize variables for orders
self.orders: List[Order] = [] # append orders for this product here
self.expected_position = self.position # expected position after market taking
self.sum_buy_qty = 0 # check whether if buy order exceeds limit
self.sum_sell_qty = 0
class MarketMaking(Strategy):
"""
Market making strategy with fair value.\n
Sub-Strategy 1: Scratch by market taking for under / par valued orders\n
Sub-Strategy 2: Stop loss if inventory piles over certain level\n
Sub-Strategy 3: Market make around fair value with inventory management
"""
def __init__(self, state: TradingState, product_config: dict, strategy_config: dict):
super().__init__(state, product_config)
# strategy configuration
self.fair_value: float = strategy_config['FAIR_VALUE'] # initial or fixed fair value for market making
self.sl_inventory: Position = strategy_config['SL_INVENTORY'] # acceptable inventory range
self.sl_spread: int = strategy_config['SL_SPREAD'] # acceptable spread to take for stop loss
self.mm_spread: int = strategy_config['MM_SPREAD'] # spread for market making
self.order_skew: float = strategy_config['ORDER_SKEW'] # extra skewing order quantity when market making
def scratch_under_valued(self, mid_vwap=False):
"""
Scratch any under-valued or par-valued orders by aggressing against bots
"""
reserve_price = self.mid_vwap if mid_vwap else self.fair_value
if -self.sl_inventory <= self.position <= self.sl_inventory:
# use this strategy only when position is within stop loss inventory level
if self.best_bid >= reserve_price and len(self.bids) >= 2:
# trade (sell) against bots trying to buy too expensive but not against worst bid
order_quantity = min(max(-self.bids[self.best_bid],
-self.position_limit - min(self.position, 0)), 0)
self.orders.append(Order(self.symbol, self.best_bid, order_quantity))
self.expected_position += order_quantity
self.sum_sell_qty += order_quantity
print(f"Scratch Sell {order_quantity} X @ {self.best_bid}")
elif self.best_ask <= reserve_price and len(self.asks) >= 2:
# trade (buy) against bots trying to sell to cheap but not against worst ask
order_quantity = max(min(-self.asks[self.best_ask],
self.position_limit - max(self.position, 0)), 0)
self.orders.append(Order(self.symbol, self.best_ask, order_quantity))
self.expected_position += order_quantity
self.sum_buy_qty += order_quantity
print(f"Scratch Buy {order_quantity} X @ {self.best_ask}")
def stop_loss(self, ignore_worst=True):
"""
Stop loss when inventory over acceptable level
"""
if self.position > self.sl_inventory and self.best_bid >= self.fair_value - self.sl_spread:
# stop loss sell not too cheap when in long position over acceptable inventory
if len(self.bids) >= int(ignore_worst) + 1:
# do not take worst bid which is also best bid if ignore worst
order_quantity = max(-self.bids[self.best_bid], -self.position + self.sl_inventory)
self.orders.append(Order(self.symbol, self.best_bid, order_quantity))
self.expected_position += order_quantity
self.sum_sell_qty += order_quantity
print(f"Stop Loss Sell {order_quantity} X @ {self.best_bid}")
elif self.position < -self.sl_inventory and self.best_ask <= self.fair_value + self.sl_spread:
# stop loss buy not too expensive when in short position over acceptable inventory
if len(self.asks) >= int(ignore_worst) + 1:
# do not take worst ask which is also best ask if ignore worst
order_quantity = min(-self.asks[self.best_ask], -self.position - self.sl_inventory)
self.orders.append(Order(self.symbol, self.best_ask, order_quantity))
self.expected_position += order_quantity
self.sum_buy_qty += order_quantity
print(f"Stop Loss Buy {order_quantity} X @ {self.best_ask}")
def market_make(self):
"""
Market make with fixed spread around fair value
"""
# for limit consider position, expected position and single-sided aggregate
bid_limit = max(min(self.position_limit,
self.position_limit - self.position,
self.position_limit - self.expected_position,
self.position_limit - self.sum_buy_qty - self.position), 0)
ask_limit = min(max(-self.position_limit,
-self.position_limit - self.position,
-self.position_limit - self.expected_position,
-self.position_limit - self.sum_sell_qty - self.position), 0)
# natural order skew due to limit + extra skewing to prevent further adverse selection
bid_skew = math.ceil(self.order_skew * max(self.expected_position, 0))
ask_skew = math.floor(self.order_skew * min(self.expected_position, 0))
bid_quantity = min(max(bid_limit - bid_skew, 0), bid_limit)
ask_quantity = max(min(ask_limit - ask_skew, 0), ask_limit)
# determine price for market making using fair value as reserve price
bid_price = math.ceil(self.fair_value - self.mm_spread)
ask_price = math.floor(self.fair_value + self.mm_spread)
self.orders.append(Order(self.symbol, bid_price, bid_quantity))
self.orders.append(Order(self.symbol, ask_price, ask_quantity))
print(f"Market Make Bid {bid_quantity} X @ {bid_price} Ask {ask_quantity} X @ {ask_price}")
def aggregate_orders(self, ignore_worst_sl=True) -> List[Order]:
"""
Aggregate all orders from all sub strategies under market making
:rtype: List[Order]
:return: List of orders generated for product
"""
print(f"{self.symbol} Position {self.position}")
self.scratch_under_valued()
self.stop_loss(ignore_worst_sl)
self.market_make()
return self.orders
class LinearRegressionMM(MarketMaking):
"""
Market making based on prediction of price with simple linear regression of price over time
"""
def __init__(self, state: TradingState,
product_config: dict, strategy_config: dict):
super().__init__(state, product_config, strategy_config)
# strategy configuration
self.min_window_size = strategy_config['MIN_WINDOW_SIZE']
self.max_window_size = strategy_config['MAX_WINDOW_SIZE']
self.predict_shift = strategy_config['PREDICT_SHIFT']
def predict_price(self, price_history: deque):
"""
Predict price value after n timestamp shift with linear regression and update fair value
:param price_history: (deque) Array of historical prices
"""
n = len(price_history)
if n >= self.min_window_size:
t = int(self.timestamp / 100)
xs = [100 * i for i in range(t - n + 1, t + 1)]
ys = list(price_history)
slope, intercept = statistics.linear_regression(xs, ys)
y_hat = slope * (self.timestamp + 100 * self.predict_shift) + intercept
self.fair_value = y_hat
else:
self.fair_value = self.mid_vwap
class OTCArbitrage(Strategy):
"""
Arbitrage Between OTC and Exchange comparing with estimated fair value
Sub-Strategy 1: Take orders from exchange which provide arbitrage opportunity
Sub-Strategy 2: Market make so that we secure margin over arbitrage free pricing
Sub-Strategy 3: Convert remaining position to exit arbitrage position
"""
def __init__(self, state: TradingState,
product_config: dict, strategy_config: dict):
super().__init__(state, product_config)
self.unit_cost_storing = product_config['COST_STORING']
# extract information from conversion observation
self.observation = state.observations.conversionObservations[self.symbol]
self.otc_bid = self.observation.bidPrice
self.otc_ask = self.observation.askPrice
self.cost_import = self.observation.transportFees + self.observation.importTariff
self.cost_export = self.observation.transportFees + self.observation.exportTariff
self.sunlight = self.observation.sunlight
self.humidity = self.observation.humidity
# initialize variables for conversions
self.conversions = 0 # reset every timestamp
# strategy configuration
self.expected_storage_time = strategy_config['EXP_STORAGE_TIME']
self.effective_cost_export = self.cost_export + self.expected_storage_time * self.unit_cost_storing
self.min_edge = strategy_config['MIN_EDGE'] # only try market taking arbitrage over this edge
self.mm_edge = strategy_config['MM_EDGE'] # edge added to arbitrage free pricing for market making
def arbitrage_exchange_enter(self):
"""
Long Arbitrage: take exchange good ask (buy) then take next otc bid (sell)\n
Short Arbitrage: take exchange good bid (sell) then take next otc ask (buy)\n
Note you pay export storing cost for long arb but only import cost for short arb
"""
# calculate effective import and export cost then get arbitrage edge of each side
long_arb_edge = self.otc_bid - self.best_ask - self.effective_cost_export
short_arb_edge = self.best_bid - self.otc_ask - self.cost_import
edges = {"Long": long_arb_edge, "Short": short_arb_edge}
max_key = max(edges, key=lambda k: edges[k]) # choose best side
if max_key == "Long" and edges[max_key] >= self.min_edge:
for price, quantity in self.asks.items():
if self.otc_bid - price - self.effective_cost_export >= self.min_edge:
order_quantity = max(min(-quantity,
self.position_limit - max(self.expected_position, 0)), 0)
self.orders.append(Order(self.symbol, price, order_quantity))
print(f"{max_key} Arbitrage {order_quantity} X @ {self.best_ask}")
self.expected_position += order_quantity
self.sum_buy_qty += order_quantity
else:
break
elif max_key == "Short" and edges[max_key] >= self.min_edge:
for price, quantity in self.bids.items():
if price - self.otc_ask - self.cost_import >= self.min_edge:
order_quantity = min(max(-self.bids[self.best_bid],
-self.position_limit - min(self.expected_position, 0)), 0)
self.orders.append(Order(self.symbol, self.best_bid, order_quantity))
print(f"{max_key} Arbitrage Enter {order_quantity} X @ {self.best_bid}")
self.expected_position += order_quantity
self.sum_sell_qty += order_quantity
else:
break
def market_make(self):
"""
Make bid low enough to take bid (sell) arbitrage-freely in otc considering cost\n
Make ask high enough to take ask (buy) arbitrage-freely in otc considering cost
"""
# for limit consider position, expected position and single-sided aggregate
bid_quantity = max(min(self.position_limit,
self.position_limit - self.position,
self.position_limit - self.expected_position,
self.position_limit - self.sum_buy_qty - self.position), 0)
ask_quantity = min(max(-self.position_limit,
-self.position_limit - self.position,
-self.position_limit - self.expected_position,
-self.position_limit - self.sum_sell_qty - self.position), 0)
# determine price for market making by adding edge to arbitrage free price
bid_arb_free = self.otc_bid - self.effective_cost_export
ask_arb_free = self.otc_ask + self.cost_import
bid_price = math.floor(bid_arb_free - self.mm_edge)
ask_price = math.ceil(ask_arb_free + self.mm_edge)
self.orders.append(Order(self.symbol, bid_price, bid_quantity))
self.orders.append(Order(self.symbol, ask_price, ask_quantity))
print(f"Market Make Bid {bid_quantity} X @ {bid_price} Ask {ask_quantity} X @ {ask_price}")
def arbitrage_otc_exit(self):
"""
Exit position from arbitrage strategy by converting position in otc
"""
self.conversions = -self.position
if self.conversions > 0:
print(f"Short Arbitrage Exit {self.conversions} X @ {self.otc_ask}")
elif self.conversions < 0:
print(f"Long Arbitrage Exit {self.conversions} X @ {self.otc_bid}")
def aggregate_orders_conversions(self) -> Tuple[List[Order], int]:
"""
Aggregate all orders from all sub strategies under OTC Arbitrage
:rtype: List[Order]
:return: List of orders generated for product
"""
print(f"{self.symbol} Position {self.position}")
self.arbitrage_exchange_enter()
self.market_make()
self.arbitrage_otc_exit()
return self.orders, self.conversions
class BasketTrading:
"""
Basket trading based on pricing with NAV of constituents\n
Basket is traded alone in market making style but with pricing from constituent NAV\n
Sub-Strategy 1: Calculate fair value considering spread between basket and NAV\n
Sub-Strategy 2: Market make around fair value\n
Sub-Strategy 3: Follow trends with constituents which provide smaller spread to take
"""
def __init__(self, state: TradingState,
basket_config: dict, constituent_config: Dict[Symbol, dict], strategy_config: dict):
# initialize basket and each constituent as Strategy Object
self.basket = MarketMaking(state, basket_config, strategy_config)
self.constituent = {symbol: Strategy(state, config) for symbol, config in constituent_config.items()}
# configure basket information
self.c_symbols = list(self.constituent.keys())
self.c_ratios = {symbol: config['PER_BASKET'] for symbol, config in constituent_config.items()}
self.c_limits = {symbol: strategy.position_limit for symbol, strategy in self.constituent.items()}
self.c_mid_vwap = {symbol: strategy.mid_vwap for symbol, strategy in self.constituent.items()}
self.c_w_bid = {symbol: strategy.worst_bid for symbol, strategy in self.constituent.items()}
self.c_w_ask = {symbol: strategy.worst_ask for symbol, strategy in self.constituent.items()}
self.c_positions = {symbol: strategy.position for symbol, strategy in self.constituent.items()}
self.basket_limit = math.floor(min(self.c_limits[symbol] / self.c_ratios[symbol] for symbol in self.c_symbols))
self.basket_limit = min(self.basket_limit, self.basket.position_limit) # max basket constituent pairs
# strategy configuration
self.premium_mean = strategy_config['PREMIUM_MEAN'] # long-term mean premium of basket over constituents
self.premium_std = strategy_config['PREMIUM_STD'] # long-term standard deviation of premium
self.alpha = strategy_config['LINEAR_SENSITIVITY'] # sensitivity for linear term of z-score
self.beta = strategy_config['QUADRATIC_SENSITIVITY'] # sensitivity for quadratic term of z-score
self.sl_target = strategy_config['SL_TARGET'] # stop loss up to sl target position level
self.carry = strategy_config['CARRY'] # amount of delta to carry for trend following
self.basket.fair_value = self.basket.mid_vwap # initialize
self.basket.position_limit = self.basket_limit # change to maximum possible
# build basket features
self.basket_nav = sum(self.c_ratios[symbol] * self.c_mid_vwap[symbol] for symbol in self.c_symbols)
self.premium = self.basket.mid_vwap - self.basket_nav # basket - constituent net asset value
self.z_score = (self.premium - self.premium_mean) / self.premium_std # z-score of premium
def calculate_fair_value(self):
"""
Calculate fair value of basket using the basket premium value and its z-score
"""
demeaned_premium = self.premium - self.premium_mean # center the variable with long term mean
scaling_coefficient = self.alpha * abs(self.z_score) + self.beta * self.z_score ** 2
pricing_shift = -demeaned_premium * scaling_coefficient # premium is mean-reverting
self.basket.fair_value = self.basket.fair_value + pricing_shift
def aggressive_stop_loss(self):
"""
Take aggressive stop loss to control inventory with stop loss trigger and target inventory level
"""
if self.basket.position > self.basket.sl_inventory:
# stop loss sell when too much positive inventory, sl up to sl target, max volume worst price
order_quantity = max(-self.basket.bid_volume, self.sl_target - self.basket.position)
order_price = self.basket.worst_bid
self.basket.orders.append(Order(self.basket.symbol, order_price, order_quantity))
self.basket.expected_position += order_quantity
self.basket.sum_sell_qty += order_quantity
print(f"Stop Loss Sell {order_quantity} X @ {order_price}")
elif self.basket.position < -self.basket.sl_inventory:
# stop buy sell when too much negative inventory, sl up to sl target, max volume worst price
order_quantity = min(-self.basket.ask_volume, -self.sl_target - self.basket.position)
order_price = self.basket.worst_ask
self.basket.orders.append(Order(self.basket.symbol, order_price, order_quantity))
self.basket.expected_position += order_quantity
self.basket.sum_buy_qty += order_quantity
print(f"Stop Loss Buy {order_quantity} X @ {order_price}")
def aggregate_basket_orders(self) -> List[Order]:
"""
Aggregate all orders from market making basket product
:rtype: List[Order]
:return: List of orders generated for product
"""
print(f"{self.basket.symbol} Position {self.basket.position}")
self.calculate_fair_value()
self.basket.scratch_under_valued(mid_vwap=True)
self.aggressive_stop_loss()
self.basket.market_make()
return self.basket.orders
class OptionTrading:
"""
Trade option with delta neutral strategy mainly exposing to vega
Sub-Strategy 1: Calculate rolling z-score of implied volatility
Sub-Strategy 2: Trade IV mean-reversion based on IV z-score, pyramid-style position
Sub-Strategy 3: Neutralized delta with underlying asset
"""
def __init__(self, state: TradingState,
underlying_config: dict, option_config: dict, strategy_config: dict):
# initialize underlying and option as Strategy Object
self.underlying = Strategy(state, underlying_config)
self.option = Strategy(state, option_config)
# config option specification
self.type = strategy_config['TYPE']
self.K = float(strategy_config['STRIKE'])
self.T = strategy_config['MATURITY']
self.trading_days = strategy_config['TRADING_DAYS']
# config strategy
self.min_window_size = strategy_config['MIN_WINDOW_SIZE']
self.max_window_size = strategy_config['MAX_WINDOW_SIZE']
self.min_z = strategy_config['MIN_Z']
self.max_z = strategy_config['MAX_Z']
# build option features
self.log_moneyness = math.log(self.underlying.mid_vwap / self.K)
self.root_tau = math.sqrt((self.T + 1 - self.option.timestamp / 1000000) / self.trading_days)
self.iv = self.implied_volatility()
self.d1, self.d2 = self.calculate_d1_d2(self.iv)
self.delta = statistics.NormalDist().cdf(self.d1) # delta = N(d1)
self.option_limit = self.underlying.position_limit / self.delta
self.iv_zscore = 0.0
def implied_volatility(self) -> float:
"""
Calculate implied volatility using closed-form estimator by Hallerbach (2004).\n
Risk-free rate is assumed to be zero.
:return: (float) Black-Scholes-Merton based implied volatility
"""
s = self.underlying.mid_vwap
c = self.option.mid_vwap
factor = math.sqrt(2 * math.pi) / (2 * (s + self.K))
a = 2 * c + self.K - s
b = 1.85 * (s + self.K) * (self.K - s) ** 2 / (math.pi * math.sqrt(self.K * s))
sigma_root_tau = factor * (a + math.sqrt(a ** 2 - b))
iv = sigma_root_tau / self.root_tau
return iv
def calculate_d1_d2(self, sigma) -> Tuple[float, float]:
"""
Calculate d1, d2 value of Black-Scholes-Merton Model.\n
Risk-free rate is assumed to be zero.
:param sigma: volatility input for BSM d1, d2
:return: (Tuple[float, float]) d1, d2 value of BSM
"""
numerator = self.log_moneyness + 0.5 * (sigma * self.root_tau) ** 2
denominator = sigma * self.root_tau
d1_value = numerator / denominator
d2_value = d1_value - sigma * self.root_tau
return d1_value, d2_value
def rolling_iv_z_score(self, data: deque):
"""
Calculate and update rolling z score of implied volatility
:param data: (deque) array of implied volatility values
"""
if len(data) >= self.min_window_size:
mu = statistics.mean(data)
std = statistics.stdev(data)
self.iv_zscore = (self.iv - mu) / std
def iv_mean_reversion(self):
"""
Generate order for implied volatility mean reversion strategy by market taking.\n
Pyramid position with respect to z-score value when over threshold.
"""
# clip out z-score so that if the absolute value exceeds max_z its capped to max_z
clip_z = min(self.iv_zscore, self.max_z) if self.iv_zscore > 0 else max(self.iv_zscore, -self.max_z)
# target position is proportion of z-score to max_z reversed (mean reversion)
target_position = int(-self.option_limit * clip_z / self.max_z)
qty = target_position - self.option.position # calculate required adjustment in position
order_quantity = min(self.option.bid_volume, qty) if qty > 0 else max(self.option.ask_volume, qty)
order_price = self.option.worst_ask if order_quantity > 0 else self.option.worst_bid
if order_quantity != 0 and abs(self.iv_zscore) > self.min_z:
# only enter with z-score over signal threshold of min_z
self.option.orders.append(Order(self.option.symbol, order_price, order_quantity))
print(f"IV: {self.iv:.4f} Z-Score {self.iv_zscore:.2f} Take {order_quantity} X @ {order_price}")
self.option.expected_position += order_quantity
def delta_hedge(self, target_delta: float = 0.0):
"""
Generate order to hedge delta exposure of portfolio
:param target_delta: (float) target delta of portfolio after adjusting
"""
# try to hedge delta simultaneously based on expected option position and hedge any remaining open delta
current_delta = self.delta * self.option.expected_position + self.underlying.position
qty = int(target_delta - current_delta)
hedge_quantity = min(self.underlying.bid_volume, qty) if qty > 0 else max(self.underlying.ask_volume, qty)
order_price = self.underlying.worst_ask if hedge_quantity > 0 else self.underlying.worst_bid
if hedge_quantity != 0:
self.underlying.orders.append(Order(self.underlying.symbol, order_price, hedge_quantity))
print(f"Delta Hedge {hedge_quantity} X @ {order_price}")
def aggregate_option_orders(self) -> List[Order]:
"""
Aggregate orders for underlying from option trading strategies
:rtype: List[Order]
:return: List of orders generated for underlying and option
"""
print(f"{self.option.symbol} Current Position {self.option.position}")
self.iv_mean_reversion()
return self.option.orders
def aggregate_underlying_orders(self) -> List[Order]:
"""
Aggregate orders for underlying from option trading strategies
:rtype: List[Order]
:return: List of orders generated for underlying
"""
print(f"{self.underlying.symbol} Current Position {self.underlying.position}")
# use volatility reversion as a signal to trade underlying same direction
self.delta_hedge(target_delta=2 * self.delta * self.option.expected_position)
return self.underlying.orders
class Trader:
"""
Class containing data and sending and receiving data with the trading server
"""
symbols = ['AMETHYSTS', 'STARFRUIT', # Round 1
'ORCHIDS', # Round 2
'GIFT_BASKET', 'CHOCOLATE', 'STRAWBERRIES', 'ROSES', # Round 3
'COCONUT', 'COCONUT_COUPON' # Round 4
]
data = {"STARFRUIT": deque(),
"COCONUT": deque()}
config = {'PRODUCT': {'AMETHYSTS': {'SYMBOL': 'AMETHYSTS',
'PRODUCT': 'AMETHYSTS',
'POSITION_LIMIT': 20},
'STARFRUIT': {'SYMBOL': 'STARFRUIT',
'PRODUCT': 'STARFRUIT',
'POSITION_LIMIT': 20},
'ORCHIDS': {'SYMBOL': 'ORCHIDS',
'PRODUCT': 'ORCHIDS',
'POSITION_LIMIT': 100,
'COST_STORING': 0.1},
'GIFT_BASKET': {'SYMBOL': 'GIFT_BASKET',
'PRODUCT': 'GIFT_BASKET',
'POSITION_LIMIT': 60},
'CHOCOLATE': {'SYMBOL': 'CHOCOLATE',
'PRODUCT': 'CHOCOLATE',
'POSITION_LIMIT': 250,
'PER_BASKET': 4},
'STRAWBERRIES': {'SYMBOL': 'STRAWBERRIES',
'PRODUCT': 'STRAWBERRIES',
'POSITION_LIMIT': 350,
'PER_BASKET': 6},
'ROSES': {'SYMBOL': 'ROSES',
'PRODUCT': 'ROSES',
'POSITION_LIMIT': 60,
'PER_BASKET': 1},
'COCONUT': {'SYMBOL': 'COCONUT',
'PRODUCT': 'COCONUT',
'POSITION_LIMIT': 300},
'COCONUT_COUPON': {'SYMBOL': 'COCONUT_COUPON',
'PRODUCT': 'COCONUT_COUPON',
'POSITION_LIMIT': 600}},
'STRATEGY': {'AMETHYSTS': {'FAIR_VALUE': 10000.0,
'SL_INVENTORY': 20,
'SL_SPREAD': 1,
'MM_SPREAD': 2,
'ORDER_SKEW': 1.0},
'STARFRUIT': {'FAIR_VALUE': 5000.0,
'SL_INVENTORY': 10,
'SL_SPREAD': 1,
'MM_SPREAD': 2,
'ORDER_SKEW': 1.0,
'MIN_WINDOW_SIZE': 5,
'MAX_WINDOW_SIZE': 10,
'PREDICT_SHIFT': 1},
'ORCHIDS': {'EXP_STORAGE_TIME': 1,
'MIN_EDGE': 1.0,
'MM_EDGE': 1.5},
'GIFT_BASKET': {'PREMIUM_MEAN': 385.0,
'PREMIUM_STD': 75.0,
'FAIR_VALUE': 70000.0,
'SL_INVENTORY': 57,
'SL_SPREAD': 1,
'MM_SPREAD': 2.0,
'ORDER_SKEW': 1.0,
'LINEAR_SENSITIVITY': 1.0,
'QUADRATIC_SENSITIVITY': 0.0,
'SL_TARGET': 0,
'CARRY': 2.0},
'COCONUT': {'TYPE': 'CALL',
'STRIKE': 10000,
'MATURITY': 246,
'TRADING_DAYS': 250,
'MIN_WINDOW_SIZE': 200,
'MAX_WINDOW_SIZE': 300,
'MIN_Z': 1.5,
'MAX_Z': 1.5}
}
}
def restore_data(self, timestamp, encoded_data):
"""
Restore data by decoding traderData with jsonpickle if loss in data is found
:param timestamp: (int) current timestamp
:param encoded_data: (str) traderData from previous timestamp encoded with jsonpickle
"""
data_loss = any([bool(v) for v in self.data.values()])
# restore only if any empty data except 0 timestamp
if timestamp >= 100 and data_loss:
self.data = jsonpickle.decode(encoded_data, keys=True)
def store_data(self, symbol: Symbol, value: Any, max_size: int = None):
"""
Store new mid vwap data for Starfruit to class variable as queue
:param symbol: (Symbol) Symbol of which data belongs to
:param value: (Any) Value to be stored in data
:param max_size: (int) Maximum size of the array, default None
"""
if max_size:
while len(self.data[symbol]) >= max_size:
self.data[symbol].popleft()
self.data[symbol].append(value)
def run(self, state: TradingState) -> Tuple[Dict[Symbol, List[Order]], int, str]:
"""
Trading algorithm that will be iterated for every timestamp
:param state: (TradingState) State of each timestamp
:return: result, conversions, traderData: (Tuple[[Dict[Symbol, List[Order]], int, str])
Results (dict of orders, conversion number, and data) of algorithms to send to the server
"""
# restore data from traderData of last timestamp
self.restore_data(state.timestamp, state.traderData)
config_p = self.config['PRODUCT']
config_s = self.config['STRATEGY']
# aggregate orders in this result dictionary
result: Dict[Symbol, List[Order]] = {}
conversions: int = 0
# Round 1: AMETHYSTS and STARFRUIT (Market Making)
# Symbol 0: AMETHYSTS (Fixed Fair Value Market Making)
symbol = self.symbols[0]
fixed_mm = MarketMaking(state, config_p[symbol], config_s[symbol])
result[symbol] = fixed_mm.aggregate_orders()
# Symbol 1: STARFRUIT (Linear Regression Market Making)
symbol = self.symbols[1]
lr_mm = LinearRegressionMM(state, config_p[symbol], config_s[symbol])
self.store_data(lr_mm.symbol, lr_mm.mid_vwap, lr_mm.max_window_size) # update data
lr_mm.predict_price(self.data[symbol]) # update fair value
result[symbol] = lr_mm.aggregate_orders()
# Round 2: OTC-Exchange Arbitrage
# Symbol 2: ORCHIDS
symbol = self.symbols[2]
otc_arb = OTCArbitrage(state, config_p[symbol], config_s[symbol])
result[symbol], conversions = otc_arb.aggregate_orders_conversions()
# Round 3: Basket Trading
# Symbol 3: GIFT_BASKET, 4 ~ 6: CHOCOLATE, STRAWBERRIES, ROSES
symbol_basket = self.symbols[3]
symbols_constituent = [self.symbols[i] for i in range(4, 7)]
basket_trading = BasketTrading(state, config_p[symbol_basket],
{s: config_p[s] for s in symbols_constituent}, config_s[symbol_basket])
result[symbol_basket] = basket_trading.aggregate_basket_orders()
# Round 4: Option Trading
# Symbol 7: COCONUT, 8: COCONUT_COUPON
symbol_underlying = self.symbols[7]
symbol_option = self.symbols[8]
option_trading = OptionTrading(state, config_p[symbol_underlying],
config_p[symbol_option], config_s[symbol_underlying])
self.store_data(symbol_underlying, option_trading.iv, option_trading.max_window_size) # update data
option_trading.rolling_iv_z_score(self.data[symbol_underlying]) # update z score
result[symbol_option] = option_trading.aggregate_option_orders() # trade option with iv mean reversion
result[symbol_underlying] = option_trading.aggregate_underlying_orders() # trade with same direction
# Save Data to traderData and pass to next timestamp
traderData = jsonpickle.encode(self.data, keys=True)
# logger.flush(state, result, conversions, traderData)
return result, conversions, traderData