├── r5_manual.py
├── README.md
└── autotrader.py
/r5_manual.py:
--------------------------------------------------------------------------------
1 | # Dictionary of items and their expected returns
2 | expected_returns = {
3 | "Haystacks": -0.0048,
4 | "Ranch_sauce": -0.0072,
5 | "Cacti_Needle": -0.412,
6 | "Solar_panels": -0.089,
7 | "Red_Flags": 0.509,
8 | "VR_Monocle": 0.224,
9 | "Quantum_Coffee": -0.6679,
10 | "Moonshine": 0.03,
11 | "Striped_shirts": 0.0021,
12 | }
13 |
14 | actual_returns = {
15 | "Haystacks": -0.0048,
16 | "Ranch_sauce": -0.0072,
17 | "Cacti_Needle": -0.412,
18 | "Solar_panels": -0.089,
19 | "Red_Flags": 0.509,
20 | "VR_Monocle": 0.224,
21 | "Quantum_Coffee": -0.6679,
22 | "Moonshine": 0.03,
23 | "Striped_shirts": 0.0021,
24 | }
25 |
26 | def calculate_borrowing_fee(x):
27 | """Calculate borrowing fee for amount x"""
28 | return (x**2)/((250/3)*10000)
29 |
30 | def find_optimal_amount(return_rate):
31 | """
32 | Find optimal amount for a given return rate
33 | Marginal return = return_rate
34 | Marginal cost = 2x/((250/3)*10000)
35 | At optimal point: return_rate = 2x/((250/3)*10000)
36 | Solving for x: x = return_rate * ((250/3)*10000)/2
37 | """
38 | # Calculate theoretical optimal amount
39 | optimal = abs(return_rate) * ((250/3)*10000)/2
40 |
41 | # Round to nearest 10000
42 | rounded = round(optimal/10000) * 10000
43 |
44 | # Return negative amount if return rate is negative (for shorting)
45 | return -rounded if return_rate < 0 else rounded
46 |
47 | print("Allocations based on expected returns:")
48 | total_investment = 0
49 | allocations = {}
50 | expected_pnl = 0
51 |
52 | for asset, rate in expected_returns.items():
53 | amount = find_optimal_amount(rate)
54 | if True:
55 | allocations[asset] = amount
56 | total_investment += abs(amount)
57 | expected_return = amount * rate - calculate_borrowing_fee(abs(amount))
58 | print(f"{asset}: ${amount:,} (Expected return: ${expected_return:,.2f})")
59 | expected_pnl += expected_return
60 |
61 | print(f"\nTotal investment: ${total_investment:,}")
62 | print(f"Expected total P&L: ${expected_pnl:,.2f}")
63 |
64 | # Calculate actual P&L using the same allocations
65 | actual_pnl = 0
66 | print("\nActual returns:")
67 | for asset, amount in allocations.items():
68 | actual_return = amount * actual_returns[asset] - calculate_borrowing_fee(abs(amount))
69 | print(f"{asset}: ${actual_return:,.2f}")
70 | actual_pnl += actual_return
71 |
72 | print(f"\nActual total P&L: ${actual_pnl:,.2f}")
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IMC Prosperity 3 - Deanos Crew Write-up
2 |
3 | ## Writers
4 |
5 | | [
](https://www.linkedin.com/in/jamescole05/) | [
](https://www.linkedin.com/in/tom%C3%A1s-barbosa-b47440156/) |
6 | |:---:|:---:|
7 | | [James Cole](https://www.linkedin.com/in/jamescole05/) | [Tomás Barbosa](https://www.linkedin.com/in/tom%C3%A1s-barbosa-b47440156/) |
8 |
9 |
10 |
11 | ## Result
12 |
13 |
14 | - #131 overall
15 | - #70/14,000 Manual Globally
16 | - #10 in the UK
17 | - ~14k teams
18 |
19 | ## Introduction
20 |
21 | IMC Prosperity 3 was a global algorithmic trading competition held over 15 days with over 14,000 participants competing. There were 5 different rounds each lasting 3 days with new challenges arising each round. The first round included ‘Rainforest Resin’, ‘Kelp’ as well as ‘Squid Ink’. Whilst further into the competition, new things were added such as ‘Magnificent Macarons’ and ‘Volcanic Rock’(Along with its call options). The underlying currency for the archipelago were seashells and the aim was to make the most amount of profit out of all the teams. In addition to the algorithmic section of the competition there was also a manual trading round consisting of game theory and mathematical problems.
22 |
23 | ## Core quantitative ideas
24 |
25 | Across all rounds we kept reusing a few core building blocks:
26 |
27 | ### VWAP and fair value
28 |
29 | For any order book we estimate a liquidity weighted fair value using VWAP:
30 |
31 | $$
32 | \text{VWAP}_t =
33 | \frac{\sum_i p_i q_i}{\sum_i q_i},
34 | $$
35 |
36 | where $p_i$ is the price at level $i$ and $q_i$ is the corresponding volume. Our trading mid is then
37 |
38 | $$
39 | m_t = \frac{\text{VWAP}_{\text{bid},t} + \text{VWAP}_{\text{ask},t}}{2}
40 | $$
41 |
42 | and for mean reversion style trades we look at deviations of the current mid from VWAP,
43 |
44 | $$
45 | \Delta p_t = \frac{m_t - \text{VWAP}_t}{\text{VWAP}_t}.
46 | $$
47 |
48 | Positive $\Delta p_t$ means rich, negative means cheap.
49 |
50 |
51 | ## Tutorial
52 |
53 | As for the tutorial round it was very simple - there were 2 commodities available to trade (Rainforest Resin and Kelp) to help teams get used to how the trading system on the archipelago worked and to have a somewhat head-start in developing an algorithm for these commodities. Our approach to this was very similar for both; Resin allowed for market making and market taking as the fair value was set to 10,000 seashells throughout all rounds with wide bid/ask spreads allowing us to undercut and make profit very efficiently. Kelp was not trading around a single price level, but we noticed its price movements could be somewhat predicted by the state of the orderbook, so we quickly decided to use VWAP to calculate our fair value for the commodity and continue to market make/take.
54 |
55 | ## Round 1
56 |
57 | In round 1 a new commodity to trade called Squid Ink was added, as well as the already 2 existing ones from the previous round. Fortunately, there weren't any changes that needed to be made for these, so we could pivot our focus primarily towards continuing to develop tools for the competition as well as an algorithm for squid ink.
58 | Throughout these 3 days we tried numerous basic methods to trade squid ink as we couldn’t continue to market make our way through. In the end we came to the decision to trade it using a mean reversion approach to scalp some profits. This didn’t turn out to be massively profitable. As the round finished and results were released we found we had ranked 1,200th globally which shocked us, but after further investigation we discovered our algorithm had actually been run on only 100k timestamps instead of the full 1 million like everybody else. After this had been resolved by the admins we jumped another 600 places globally at the start of round 2.
59 | For the manual round, we had to convert between some currencies a maximum of 5 times and make money from profitable conversion rates. This was a trivial problem, as there was a relatively small number of currencies to convert to, so we simply wrote a script to brute force the optimal conversions and used that.
60 |
61 | ## Round 2
62 |
63 | This round was based on ETF trading, a new concept to me entirely but I was able to conduct a sufficient amount of research and put it into practice within the first 24 hours. We approached this round using statistical arbitrage with the ETF (Picnic Baskets) against its synthetic basket composing of:
64 | Six Croissants
65 | Three Jams
66 | One Djembe
67 |
68 | We modelled the fair value of the Picnic Basket ETF as the value of its underlying basket. For basket 1 the synthetic price is
69 |
70 | $$
71 | P_t^{(1)} = 6\,P_t^{\mathrm{CROISSANTS}} + 3\,P_t^{\mathrm{JAMS}} + 1\,P_t^{\mathrm{DJEMBES}}
72 | $$
73 |
74 | and for basket 2
75 |
76 | $$
77 | P_t^{(2)} = 4\,P_t^{\mathrm{CROISSANTS}} + 2\,P_t^{\mathrm{JAMS}}
78 | $$
79 |
80 | At each timestamp we compute the spread between ETF and synthetic basket,
81 |
82 | $$
83 | S_t = P_t^{\mathrm{ETF}} - P_t^{\mathrm{synthetic}}
84 | $$
85 |
86 | and maintain a rolling mean $\mu_S$ and standard deviation $\sigma_S$ of this spread. The trading signal is the $z$-score
87 |
88 | $$
89 | z_t = \frac{S_t - \mu_S}{\sigma_S}.
90 | $$
91 |
92 | If $z_t \gg 0$ we short the rich ETF and buy the cheap basket.
93 | If $z_t \ll 0$ we buy the cheap ETF and short the rich basket.
94 |
95 | Positions are sized toward a target notional when $|z_t|$ exceeds a threshold and gradually closed as $z_t \to 0$.
96 |
97 | Although this worked very well for us we weren’t able to profitably replicate this for picnic basket 2 so ultimately decided to not trade it in our final submission. Once submission had been run we climbed higher in the global ranking to 138th with a total profit of 208k seashells.
98 |
99 | For the manual round, we had to trade flippers with turtles. They would have a reserve price and trade with any bids above their reserve price (in a first-price auction). Their reserve prices were uniformly distributed at random between 160-200 and 250-320, where the value of the flippers was 320. We were allowed two bids, but the second bid would only trade if it were also above the average of the other player's second bid. If it were below, the probability would fall cubically as a function of how far away the bid was from the average.
100 |
101 |
102 |
103 | For the manual round we had to pick one or two containers out of 10. Each container had a multiplier of a number of inhabitants. The payoff would then be 10k * multiplier / (inhabitants + % of players picking that crate). As you can see from the way the payoff is calculated, the multiplier/inhabitant ratio is a good metric to estimate how attractive a crate will be to other users, the inhabitant number by itself is a good metric for how much the payoff will change when more users select it, so we used it as a marker for risk.
104 |
105 | Tomas was handling the manual round and he was rather busy this week so he only managed to look at it 1 hour before submission and had to take a simplistic approach, 73x multiplier was the one with the highest multiplier/inhabitant ratio and 90x had the highest multiplier, we thought those would be overpicked by low effort players so went with 80x crate, it had a decent ratio and a very high number of inhabitants which signified low risk.Our model for the payoff of each crate told us that no crate was likely to pay above 50k ()which was the fee to pick 2 crates instead of 1) So we only picked one, this turned out to be a good choice.
106 |
107 | This didn’t give us a great payoff. We were probably around average, but it was a good, low-risk solution. It turned out that everyone went for high multiplier crates, so we got outperformed by low inhabitant (high risk) crate takers, which we were fine with, as we knew it was a possibility, but we didn’t want to take the extra risk.
108 |
109 |
110 | ## Round 3
111 |
112 | Round 3 brought a lot of surprises to some of us, as a new equity called Volcanic Rock had been added along-side its’ european style call options that we could trade too.
113 |
114 | For a significant part of our team, options were something new, so we chose to use a relatively simple strategy. We mapped options to implied volatilities and traded volatility in a mean-reversion manner. Because of the spread, deep ITM calls seemed to have a lot of noise when calculating the volatility because the extrinsic value was so close to 0, a 1$ increase or decrease due to the spread would make a huge difference in the IV, so we only traded ATM calls.
115 |
116 | For the Volcanic Rock call options we used the Black–Scholes model to map prices to implied volatilities. Given spot $S$, strike $K$, time to expiry $T$ and volatility $\sigma$, the call price is
117 |
118 | $$
119 | C(S, K, T, \sigma) = S\,N(d_1) - K\,N(d_2),
120 | $$
121 |
122 | with
123 |
124 | $$
125 | d_1 = \frac{\ln(S/K) + \tfrac12 \sigma^2 T}{\sigma \sqrt{T}},
126 | \qquad
127 | d_2 = d_1 - \sigma \sqrt{T},
128 | $$
129 |
130 | where $N(\cdot)$ is the standard normal CDF.
131 |
132 | For each option we invert this formula numerically to find the implied volatility $\hat\sigma_t$ such that
133 |
134 | $$
135 | C^{\text{market}}_t \approx C(S_t, K, T_t, \hat\sigma_t).
136 | $$
137 |
138 | We then keep a rolling mean of past implied vols $\bar\sigma_t$ and trade simple vol mean reversion:
139 |
140 | - if $\hat\sigma_t > \bar\sigma_t + \varepsilon$ we **sell volatility** (sell calls),
141 | - if $\hat\sigma_t < \bar\sigma_t - \varepsilon$ we **buy volatility** (buy calls),
142 |
143 | with $\varepsilon$ a small volatility band.
144 |
145 | To control risk and monetise curvature we approximate the Black–Scholes Greeks. The option delta is
146 |
147 | $$
148 | \Delta = \frac{\partial C}{\partial S} = N(d_1),
149 | $$
150 |
151 | and the gamma is
152 |
153 | $$
154 | \Gamma = \frac{\partial^2 C}{\partial S^2}.
155 | $$
156 |
157 | We keep the portfolio approximately delta neutral by computing a net delta
158 |
159 | $$
160 | \Delta_t^{\mathrm{net}} = q_t^{\mathrm{underlying}} + \sum_i q_t^{(i)} \,\Delta_t^{(i)},
161 | $$
162 |
163 | where $q_t^{\mathrm{underlying}}$ is our Volcanic Rock position and $q_t^{(i)}$ are option positions. Whenever $|\Delta_t^{\mathrm{net}}|$ moves outside a small band we trade the underlying to bring it back towards zero (delta hedging).
164 |
165 | Because the option positions are **long gamma** ($\Gamma > 0$), repeatedly re-hedging the delta as the underlying price $S$ moves lets us “gamma scalp”: for a small move $\Delta S$ the incremental P\&L from convexity is approximately
166 |
167 | $$
168 | \text{dPnL} \;\approx\; \tfrac12\,\Gamma\,(\Delta S)^2,
169 | $$
170 |
171 | so we systematically buy the underlying after down moves and sell it after up moves while the options mean-revert in implied volatility.
172 |
173 |
174 | Closer to the end, we had a hint from the organisers that it might be useful to plot the vol smile. This indeed was a more appropriate way to calculate IV, but we had previously assumed that the vol smile would be negligible because most people were rather unfamiliar with options, and we thought the organisers wouldn’t add such unnecessary complexity. With this hint we plotted the vol smile and it showed that it was indeed very relevant (As you can see in the figure below). But because our strategy relied on short term IV mean reversion and we were trading the options with different strikes independently, the moneyness shouldn’t change significantly unless there was a huge move in a short period of time so our strategy should still make sense for the most part despite not optimal and we didn’t have much time to perfect it.
175 |
176 |
177 |
178 | As the round came towards an end we had built a relatively strong (so we thought) algorithm for these options and their underlying. Not only this but some of us were able to learn so much about options and how they can be traded in different ways which had been totally foreign to them pre-prosperity. As results were released we were disappointed to discover we had dropped to 208th place. It seemed like some people had been able to integrate the volatility smile strategy faster than us, but also lots of people gambled with a directional trade on these options (most were punished on the following round where they lost a significant amount of money for trading this way).
179 |
180 | With some elementary linear algebra, one can see that if there was no game theoretic aspect to the game where we have to consider other players' bids, the optimal bids would be 200 and 285. Despite this, we modelled the punishment function below the average and I believe being 2 seashells below the average was close to as bad as being 10 seashells above. Because of the asymmetric punishment, this looked to us like the “guess ⅔ of the average” game, where everyone wants to be slightly above the average as a way to protect themselves from risk. For this reason we were a little conservative and placed our bids at 200 and 302, the average ended up being only 287. We learnt from this that most players in the competition were taking a very simplistic approach and we should expect them to continue to do so in the future. Additionally, it was also clear to us that we had to take some more risks as our conservative approaches weren’t paying off.
181 |
182 | ## Round 4
183 |
184 | Round 4 was an interesting round to say the least. A new market had been added called ‘Pristine Cuisine’ alongside a new asset called Magnificent Macarons. These had also been added with other external indicators we could use to model our algorithm such as the sunlight index, sugar price and import/export tariffs too. We thought straight away a basic arbitraging approach to this round would give us a strong foundation to work on but we soon realised it wasn’t. Either our orders weren’t getting filled (even when flipping the spread) or our pnl would be very erratic as well as being quite volatile. After 3 days of sleepless nights trying to model everything we could using sunlight and running algorithms to search for any correlations between the given sunlight index and sugar prices we did find a decent correlation with macaron rallies when the sunlight index was low. We tried implementing a strategy to take advantage of current sunlight index and even trying to predict the future index using the current slope (as you can see in the figure below the slope rarely changes), but the API for interacting with pristine cuisine was so weird and poorly documented that we decided not to submit this algorithm as it could easily go wrong (and for many teams it did). As the round concluded, we submitted a similar algorithm as the previous round with some small tweaks to certain parameters and logic. We all expected this to drop our placement significantly but it turned out we actually climbed to 38th place! This came as a massive shock to us as we were now 3rd in the UK too.
185 |
186 |
187 |
188 |
189 | For the manual portion of this round, it was very similar to round 2, except there were 20 crates and we could pick up to 3 (second with a cost of 50k and third cost 100k). We modelled that the second crate was now worth it so we picked 2 (because the % of players per crate would be lower due to more options). We followed a similar strategy to round 2, as we believed people would be greedy and gravitate to high risk crates since they paid off well in r2, we stayed in midrange attractiveness, low risk crates (70x and 80x) and we had a significantly above average payoff, confirming that our predictions were correct.
190 |
191 |
192 | ## Round 5
193 |
194 | For the manual round we had a newspaper that gave us news regarding certain assets and we were able to place trades on them with a total capital of 1MM. But we had to pay fees to pay these trades and the fees grew quadratically with the allocation size, so even if we had the direction right, it wasn’t optimal to bet maximum size.
195 |
196 | There was a similar manual challenge last year so we looked at it to understand the expected magnitude of the moves. With that, we analyzed the news and predicted how much each asset would change and then made an algorithm (available on this github) calculate the optimal allocation as a function of the predicted moves. This worked out pretty well, we made a good amount of seashells in this manual round.
197 |
198 | This round no new equities had been added but the bots that had been trading in the market amongst or with us had been de-anonymised and we were now able to look for patterns to trade with. Although during this round we all lacked a lot of time due to travelling, exams and other external factors we tried to put together an algo through mapping out all trades made between the bots. After 48 hours of near to no sleep we modelled an algorithm to trade more of the volcanic rock call options, squid ink, picnic basket 1 and the bots. We discovered Olivia always traded the very highs and lows of the market as well as Pablo profitably trading on Volcanic Rock which we were able to replicate. We tested this algorithm over all 3 million timestamps of provided data and the official IMC dashboard with extremely promising and safe results that were consistent throughout. We submitted this algorithm expecting to place in the top 25 globally, but this quickly proved to remain a dream. As we waited 72 hours for final results to be released we discovered our algorithm had actually lost a significant amount of money dropping us to 131st globally and 10th in the UK. We were extremely disappointed with our results but with the short amount of time we had to put into it as well as rigorous backtesting, we accepted our small defeat and learnt from our mistakes.
199 |
200 | 
201 |
202 |
203 |
204 | All in all we thoroughly enjoyed IMC Prosperity as it allowed us to develop python skills, mathematical skills, quantitative research and trading knowledge. We also met many new people along the way which all performed very well themselves. Although we were bitterly disappointed with our final placement we all had an amazing experience and learnt many new things, we also hope to place in the top 25 in Prosperity 4 next year!
205 |
206 | Many thanks to IMC Trading and the firm responsible for running this competition.
207 |
208 |
--------------------------------------------------------------------------------
/autotrader.py:
--------------------------------------------------------------------------------
1 | import math
2 | import numpy as np # type: ignore
3 | import string
4 | import jsonpickle
5 | import json
6 | from math import log, sqrt
7 | from statistics import NormalDist
8 | from typing import List, Dict, Tuple, Any
9 | from datamodel import (
10 | OrderDepth,
11 | TradingState,
12 | Order,
13 | Listing,
14 | Observation,
15 | ProsperityEncoder,
16 | Symbol,
17 | Trade,
18 | )
19 |
20 |
21 | # Configure which trader to copy for each asset with optional counterparty, quantity filters, and signal weights
22 | COPY_TARGETS = {
23 | "SQUID_INK": {
24 | "signals": [
25 | {"trader": "Olivia", "counterparty": "", "quantity": 0, "weight": 2, "watch_asset": "SQUID_INK"},
26 | # Medium signal
27 | ]
28 | },
29 | "CROISSANTS": {
30 | "signals": [
31 | {"trader": "Olivia", "counterparty": "", "quantity": 0, "weight": 1.0, "watch_asset": "CROISSANTS"}
32 | ]
33 | },
34 | "VOLCANIC_ROCK": {
35 | "signals": [
36 | {"trader": "Pablo", "counterparty": "", "quantity": 0, "weight": 2, "watch_asset": "VOLCANIC_ROCK"}
37 | ]
38 | },
39 | "VOLCANIC_ROCK_VOUCHER_9500": {
40 | "signals": [
41 | {"trader": "Pablo", "counterparty": "", "quantity": 0, "weight": 2, "watch_asset": "VOLCANIC_ROCK"}
42 | ]
43 | },
44 |
45 | }
46 |
47 | class Logger:
48 | def __init__(self) -> None:
49 | self.logs = ""
50 | self.max_log_length = 3750
51 |
52 | def print(self, *objects: Any, sep: str = " ", end: str = "\n") -> None:
53 | self.logs += sep.join(map(str, objects)) + end
54 |
55 | def flush(self, state: TradingState, orders: dict[Symbol, list[Order]], conversions: int, trader_data: str) -> None:
56 | base_length = len(
57 | self.to_json(
58 | [
59 | self.compress_state(state, ""),
60 | self.compress_orders(orders),
61 | conversions,
62 | "",
63 | "",
64 | ]
65 | )
66 | )
67 | # We truncate state.traderData, trader_data, and self.logs to the same max. length to fit the log limit
68 | max_item_length = (self.max_log_length - base_length) // 3
69 | print(
70 | self.to_json(
71 | [
72 | self.compress_state(state, self.truncate(state.traderData, max_item_length)),
73 | self.compress_orders(orders),
74 | conversions,
75 | self.truncate(trader_data, max_item_length),
76 | self.truncate(self.logs, max_item_length),
77 | ]
78 | )
79 | )
80 | self.logs = ""
81 |
82 | def compress_state(self, state: TradingState, trader_data: str) -> list[Any]:
83 | return [
84 | state.timestamp,
85 | trader_data,
86 | self.compress_listings(state.listings),
87 | self.compress_order_depths(state.order_depths),
88 | self.compress_trades(state.own_trades),
89 | self.compress_trades(state.market_trades),
90 | state.position,
91 | self.compress_observations(state.observations),
92 | ]
93 |
94 | def compress_listings(self, listings: dict[Symbol, Listing]) -> list[list[Any]]:
95 | compressed = []
96 | for listing in listings.values():
97 | compressed.append([listing.symbol, listing.product, listing.denomination])
98 |
99 | return compressed
100 |
101 | def compress_order_depths(self, order_depths: dict[Symbol, OrderDepth]) -> dict[Symbol, list[Any]]:
102 | compressed = {}
103 | for symbol, order_depth in order_depths.items():
104 | compressed[symbol] = [order_depth.buy_orders, order_depth.sell_orders]
105 |
106 | return compressed
107 |
108 | def compress_trades(self, trades: dict[Symbol, list[Trade]]) -> list[list[Any]]:
109 | compressed = []
110 | for arr in trades.values():
111 | for trade in arr:
112 | compressed.append(
113 | [
114 | trade.symbol,
115 | trade.price,
116 | trade.quantity,
117 | trade.buyer,
118 | trade.seller,
119 | trade.timestamp,
120 | ]
121 | )
122 | return compressed
123 |
124 | def compress_observations(self, observations: Observation) -> list[Any]:
125 | conversion_observations = {}
126 | for product, observation in observations.conversionObservations.items():
127 | conversion_observations[product] = [
128 | observation.bidPrice,
129 | observation.askPrice,
130 | observation.transportFees,
131 | observation.exportTariff,
132 | observation.importTariff,
133 | observation.sugarPrice,
134 | observation.sunlightIndex,
135 | ]
136 | return [observations.plainValueObservations, conversion_observations]
137 |
138 | def compress_orders(self, orders: dict[Symbol, list[Order]]) -> list[list[Any]]:
139 | compressed = []
140 | for arr in orders.values():
141 | for order in arr:
142 | compressed.append([order.symbol, order.price, order.quantity])
143 | return compressed
144 |
145 | def to_json(self, value: Any) -> str:
146 | return json.dumps(value, cls=ProsperityEncoder, separators=(",", ":"))
147 |
148 | def truncate(self, value: str, max_length: int) -> str:
149 | if len(value) <= max_length:
150 | return value
151 |
152 | return value[: max_length - 3] + "..."
153 |
154 |
155 | logger = Logger()
156 |
157 |
158 | class BlackScholes:
159 | @staticmethod
160 | def black_scholes_call(spot, strike, time_to_expiry, volatility):
161 | d1 = (
162 | (log(spot) - log(strike)) + (0.5 * volatility * volatility) * time_to_expiry
163 | ) / (volatility * sqrt(time_to_expiry))
164 | d2 = d1 - volatility * sqrt(time_to_expiry)
165 | call_price = spot * NormalDist().cdf(d1) - strike * NormalDist().cdf(d2)
166 | return call_price
167 |
168 | @staticmethod
169 | def black_scholes_put(spot, strike, time_to_expiry, volatility):
170 | d1 = (log(spot / strike) + (0.5 * volatility * volatility) * time_to_expiry) / (
171 | volatility * sqrt(time_to_expiry)
172 | )
173 | d2 = d1 - volatility * sqrt(time_to_expiry)
174 | put_price = strike * NormalDist().cdf(-d2) - spot * NormalDist().cdf(-d1)
175 | return put_price
176 |
177 | @staticmethod
178 | def delta(spot, strike, time_to_expiry, volatility):
179 | d1 = (
180 | (log(spot) - log(strike)) + (0.5 * volatility * volatility) * time_to_expiry
181 | ) / (volatility * sqrt(time_to_expiry))
182 | return NormalDist().cdf(d1)
183 |
184 | @staticmethod
185 | def implied_volatility(
186 | call_price, spot, strike, time_to_expiry, max_iterations=200, tolerance=1e-15
187 | ):
188 | """
189 | A binary-search approach to implied vol.
190 | We'll exit once we get close to the observed call_price,
191 | or we run out of iterations.
192 | """
193 | low_vol = 0.01
194 | high_vol = 1.0
195 | volatility = (low_vol + high_vol) / 2.0
196 | for _ in range(max_iterations):
197 | estimated_price = BlackScholes.black_scholes_call(
198 | spot, strike, time_to_expiry, volatility
199 | )
200 | diff = estimated_price - call_price
201 | if abs(diff) < tolerance:
202 | break
203 | if diff > 0:
204 | high_vol = volatility
205 | else:
206 | low_vol = volatility
207 | volatility = (low_vol + high_vol) / 2.0
208 | return volatility
209 |
210 |
211 | class Status:
212 | def __init__(self, product: str, state: TradingState, strike=None) -> None:
213 | self.product = product
214 | self._state = state
215 | self.ma_window = 10 # For the moving average of implied vol
216 | self.alpha = 0.3
217 | self.volatility = 0.16
218 | self.initial_time_to_expiry = 6 # 6 trading days remaining
219 | self.strike = strike
220 | self.price_history = [] # Track price history for trend analysis
221 | self.volatility_history = [] # Track volatility history
222 | self.last_trade_timestamp = 0 # Track when we last traded
223 | self.trade_count = 0 # Count trades to adjust aggressiveness
224 | self.profit_history = [] # Track profit/loss history
225 |
226 | @property
227 | def order_depth(self) -> OrderDepth:
228 | return self._state.order_depths[self.product]
229 |
230 | @property
231 | def bids(self) -> List[Tuple[int, int]]:
232 | return list(self._state.order_depths[self.product].buy_orders.items())
233 |
234 | @property
235 | def asks(self) -> List[Tuple[int, int]]:
236 | return list(self._state.order_depths[self.product].sell_orders.items())
237 |
238 | @property
239 | def position(self) -> int:
240 | return self._state.position.get(self.product, 0)
241 |
242 | @property
243 | def possible_buy_amt(self) -> int:
244 | return 50 - self.position
245 |
246 | @property
247 | def possible_sell_amt(self) -> int:
248 | return 50 + self.position
249 |
250 | @property
251 | def jam_possible_buy_amt(self) -> int:
252 | return 350 - self.position
253 |
254 | @property
255 | def jam_possible_sell_amt(self) -> int:
256 | return 350 + self.position
257 |
258 | @property
259 | def best_bid(self) -> int:
260 | bids = self._state.order_depths[self.product].buy_orders
261 | return max(bids.keys()) if bids else 0
262 |
263 | @property
264 | def best_ask(self) -> int:
265 | asks = self._state.order_depths[self.product].sell_orders
266 | return min(asks.keys()) if asks else float('inf')
267 |
268 | @property
269 | def maxamt_midprc(self) -> float:
270 | buy_orders = self._state.order_depths[self.product].buy_orders
271 | sell_orders = self._state.order_depths[self.product].sell_orders
272 | if not buy_orders or not sell_orders:
273 | return (self.best_bid + self.best_ask) / 2.0
274 | max_bv = 0
275 | max_bv_price = self.best_bid
276 | for p, v in buy_orders.items():
277 | if v > max_bv:
278 | max_bv = v
279 | max_bv_price = p
280 | max_sv = 0
281 | max_sv_price = self.best_ask
282 | for p, v in sell_orders.items():
283 | if -v > max_sv:
284 | max_sv = -v
285 | max_sv_price = p
286 | return (max_bv_price + max_sv_price) / 2
287 |
288 | @property
289 | def vwap(self) -> float:
290 | """
291 | Calculate Volume Weighted Average Price (VWAP) for the product.
292 | Combines bid and ask data.
293 | """
294 | buy_orders = self._state.order_depths[self.product].buy_orders
295 | sell_orders = self._state.order_depths[self.product].sell_orders
296 |
297 | total_value = 0 # Total (price * volume)
298 | total_volume = 0 # Total volume
299 |
300 | # Aggregate bid data
301 | for price, volume in buy_orders.items():
302 | total_value += price * volume
303 | total_volume += volume
304 |
305 | # Aggregate ask data
306 | for price, volume in sell_orders.items():
307 | total_value += price * abs(volume)
308 | total_volume += abs(volume)
309 |
310 | # Prevent division by zero
311 | if total_volume == 0:
312 | return (self.best_bid + self.best_ask) / 2.0 # Default to mid-price
313 |
314 | return total_value / total_volume
315 |
316 | @property
317 | def timestamp(self) -> int:
318 | return self._state.timestamp
319 |
320 | @property
321 | def order_depth(self) -> OrderDepth:
322 | return self._state.order_depths[self.product]
323 |
324 | @property
325 | def bids(self) -> List[Tuple[int, int]]:
326 | return list(self._state.order_depths[self.product].buy_orders.items())
327 |
328 | @property
329 | def asks(self) -> List[Tuple[int, int]]:
330 | return list(self._state.order_depths[self.product].sell_orders.items())
331 |
332 | @property
333 | def position(self) -> int:
334 | return self._state.position.get(self.product, 0)
335 |
336 | @property
337 | def possible_buy_amt(self) -> int:
338 | """
339 | The position limit is different for each product.
340 | We keep the logic that KELP is +/-50, vouchers are +/-200,
341 | and VOLCANIC_ROCK is +/-400.
342 | """
343 | if self.product == "KELP":
344 | return 50 - self.position
345 | elif self.product == "VOLCANIC_ROCK_VOUCHER_9500":
346 | return 200 - self.position
347 | elif self.product == "VOLCANIC_ROCK_VOUCHER_9750":
348 | return 200 - self.position
349 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10000":
350 | return 200 - self.position
351 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10250":
352 | return 200 - self.position
353 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10500":
354 | return 200 - self.position
355 | elif self.product == "VOLCANIC_ROCK":
356 | return 400 - self.position
357 |
358 | @property
359 | def possible_sell_amt(self) -> int:
360 | if self.product == "KELP":
361 | return 50 + self.position
362 | elif self.product == "VOLCANIC_ROCK_VOUCHER_9500":
363 | return 200 + self.position
364 | elif self.product == "VOLCANIC_ROCK_VOUCHER_9750":
365 | return 200 + self.position
366 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10000":
367 | return 200 + self.position
368 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10250":
369 | return 200 + self.position
370 | elif self.product == "VOLCANIC_ROCK_VOUCHER_10500":
371 | return 200 + self.position
372 | elif self.product == "VOLCANIC_ROCK":
373 | return 400 + self.position
374 |
375 | @property
376 | def best_bid(self) -> int:
377 | bids = self._state.order_depths[self.product].buy_orders
378 | return max(bids.keys()) if bids else 0
379 |
380 | @property
381 | def best_ask(self) -> int:
382 | asks = self._state.order_depths[self.product].sell_orders
383 | return min(asks.keys()) if asks else float("inf")
384 |
385 | @property
386 | def vwap(self) -> float:
387 | """
388 | Compute the VWAP, combining all current bid/ask levels.
389 | This is a safer reference point than the naive mid-price.
390 | """
391 | buy_orders = self._state.order_depths[self.product].buy_orders
392 | sell_orders = self._state.order_depths[self.product].sell_orders
393 |
394 | total_value = 0
395 | total_volume = 0
396 |
397 | for price, volume in buy_orders.items():
398 | total_value += price * volume
399 | total_volume += volume
400 |
401 | for price, volume in sell_orders.items():
402 | total_value += price * abs(volume)
403 | total_volume += abs(volume)
404 |
405 | if total_volume == 0:
406 | return (self.best_bid + self.best_ask) / 2.0
407 | return total_value / total_volume
408 |
409 | def update_price_history(self, price: float) -> None:
410 | """Update price history with current price"""
411 | self.price_history.append(price)
412 | # Keep only the last 20 prices
413 | if len(self.price_history) > 20:
414 | self.price_history = self.price_history[-20:]
415 |
416 | def update_volatility_history(self, volatility: float) -> None:
417 | """Update volatility history"""
418 | self.volatility_history.append(volatility)
419 | # Keep only the last 10 volatility readings
420 | if len(self.volatility_history) > 10:
421 | self.volatility_history = self.volatility_history[-10:]
422 |
423 | def update_profit_history(self, profit: float) -> None:
424 | """Update profit history"""
425 | self.profit_history.append(profit)
426 | # Keep only the last 5 profit readings
427 | if len(self.profit_history) > 5:
428 | self.profit_history = self.profit_history[-5:]
429 |
430 | def get_price_trend(self) -> float:
431 | """Calculate price trend based on recent history"""
432 | if len(self.price_history) < 2:
433 | return 0
434 |
435 | # Simple linear regression for trend
436 | x = list(range(len(self.price_history)))
437 | y = self.price_history
438 |
439 | # Calculate means
440 | x_mean = sum(x) / len(x)
441 | y_mean = sum(y) / len(y)
442 |
443 | # Calculate slope
444 | numerator = sum((xi - x_mean) * (yi - y_mean) for xi, yi in zip(x, y))
445 | denominator = sum((xi - x_mean) ** 2 for xi in x)
446 |
447 | if denominator == 0:
448 | return 0
449 |
450 | slope = numerator / denominator
451 | return slope
452 |
453 | def get_volatility_trend(self) -> float:
454 | """Calculate volatility trend"""
455 | if len(self.volatility_history) < 2:
456 | return 0
457 |
458 | # Simple linear regression for trend
459 | x = list(range(len(self.volatility_history)))
460 | y = self.volatility_history
461 |
462 | # Calculate means
463 | x_mean = sum(x) / len(x)
464 | y_mean = sum(y) / len(y)
465 |
466 | # Calculate slope
467 | numerator = sum((xi - x_mean) * (yi - y_mean) for xi, yi in zip(x, y))
468 | denominator = sum((xi - x_mean) ** 2 for xi in x)
469 |
470 | if denominator == 0:
471 | return 0
472 |
473 | slope = numerator / denominator
474 | return slope
475 |
476 | def get_recent_profit_trend(self) -> float:
477 | """Calculate recent profit trend"""
478 | if len(self.profit_history) < 2:
479 | return 0
480 |
481 | # Simple linear regression for trend
482 | x = list(range(len(self.profit_history)))
483 | y = self.profit_history
484 |
485 | # Calculate means
486 | x_mean = sum(x) / len(x)
487 | y_mean = sum(y) / len(y)
488 |
489 | # Calculate slope
490 | numerator = sum((xi - x_mean) * (yi - y_mean) for xi, yi in zip(x, y))
491 | denominator = sum((xi - x_mean) ** 2 for xi in x)
492 |
493 | if denominator == 0:
494 | return 0
495 |
496 | slope = numerator / denominator
497 | return slope
498 |
499 | def update_IV_history(self, underlying_price) -> None:
500 | """Refresh the stored implied vol reading."""
501 | temp_history = IV_history.get_IV_history(self.product)
502 | temp_history.append(self.IV(underlying_price))
503 | IV_history.set_IV_history(self.product, temp_history[-self.ma_window:])
504 |
505 | # Also update our volatility history
506 | self.update_volatility_history(self.IV(underlying_price))
507 |
508 | def IV(self, underlying_price) -> float:
509 | return BlackScholes.implied_volatility(
510 | call_price=self.vwap,
511 | spot=underlying_price,
512 | strike=self.strike,
513 | time_to_expiry=self.tte,
514 | )
515 |
516 | def moving_average(self, underlying_price: int) -> float:
517 | """
518 | Simple average of the last few implied vol readings.
519 | If we have no stored history yet, just seed from current IV.
520 | """
521 | hist = IV_history.get_IV_history(self.product)
522 | if not hist:
523 | return self.IV(underlying_price)
524 | return sum(hist) / len(hist)
525 |
526 | @property
527 | def tte(self) -> float:
528 | """
529 | We have 6 days left to expiry. Each "day" is effectively chunked
530 | as you proceed in your simulation. We divide by 250 so that
531 | each day is treated as 1 "trading day" in annualized terms.
532 | """
533 | # The environment's timestamp goes up to ~1,000,000 per day,
534 | # so we do (6 - dayProgress) / 250.
535 | return (self.initial_time_to_expiry - (self.timestamp / 1_000_000)) / 252.0
536 |
537 |
538 | class IV_history:
539 | def __init__(self):
540 | self.v_9500_IV_history = []
541 | self.v_9750_IV_history = []
542 | self.v_10000_IV_history = []
543 | self.v_10250_IV_history = []
544 | self.v_10500_IV_history = []
545 |
546 | def get_IV_history(self, product: str) -> List[float]:
547 | if product == "VOLCANIC_ROCK_VOUCHER_9500":
548 | return self.v_9500_IV_history
549 | elif product == "VOLCANIC_ROCK_VOUCHER_9750":
550 | return self.v_9750_IV_history
551 | elif product == "VOLCANIC_ROCK_VOUCHER_10000":
552 | return self.v_10000_IV_history
553 | elif product == "VOLCANIC_ROCK_VOUCHER_10250":
554 | return self.v_10250_IV_history
555 | elif product == "VOLCANIC_ROCK_VOUCHER_10500":
556 | return self.v_10500_IV_history
557 | return []
558 |
559 | def set_IV_history(self, product: str, IV_history: List[float]) -> None:
560 | if product == "VOLCANIC_ROCK_VOUCHER_9500":
561 | self.v_9500_IV_history = IV_history
562 | elif product == "VOLCANIC_ROCK_VOUCHER_9750":
563 | self.v_9750_IV_history = IV_history
564 | elif product == "VOLCANIC_ROCK_VOUCHER_10000":
565 | self.v_10000_IV_history = IV_history
566 | elif product == "VOLCANIC_ROCK_VOUCHER_10250":
567 | self.v_10250_IV_history = IV_history
568 | elif product == "VOLCANIC_ROCK_VOUCHER_10500":
569 | self.v_10500_IV_history = IV_history
570 |
571 |
572 | IV_history = IV_history()
573 |
574 |
575 | class Product:
576 | RAINFOREST_RESIN = 'RAINFOREST_RESIN'
577 | KELP = 'KELP'
578 | SQUID_INK = 'SQUID_INK'
579 | CROISSANTS = 'CROISSANTS'
580 | JAMS = 'JAMS'
581 | DJEMBES = 'DJEMBES'
582 | PICNIC_BASKET1 = 'PICNIC_BASKET1'
583 | PICNIC_BASKET2 = 'PICNIC_BASKET2'
584 | SYNTHETIC = "SYNTHETIC"
585 | # added from round3Kai:
586 | VOLCANIC_ROCK = 'VOLCANIC_ROCK'
587 | VOLCANIC_ROCK_VOUCHER_9500 = 'VOLCANIC_ROCK_VOUCHER_9500'
588 | VOLCANIC_ROCK_VOUCHER_9750 = 'VOLCANIC_ROCK_VOUCHER_9750'
589 | VOLCANIC_ROCK_VOUCHER_10000 = 'VOLCANIC_ROCK_VOUCHER_10000'
590 | VOLCANIC_ROCK_VOUCHER_10250 = 'VOLCANIC_ROCK_VOUCHER_10250'
591 | VOLCANIC_ROCK_VOUCHER_10500 = 'VOLCANIC_ROCK_VOUCHER_10500'
592 |
593 |
594 | PARAMS = {
595 | Product.RAINFOREST_RESIN: {
596 | "fair_value": 10000,
597 | "take_width": 1,
598 | "clear_width": 0,
599 | "disregard_edge": 0,
600 | "join_edge": 4,
601 | "default_edge": 2,
602 | "soft_position_limit": 25,
603 | },
604 | Product.KELP: {
605 | "take_width": 1,
606 | "clear_width": 0,
607 | "prevent_adverse": True,
608 | "adverse_volume": 15,
609 | "reversion_beta": -0.229,
610 | "disregard_edge": 1,
611 | "join_edge": 0,
612 | "default_edge": 1,
613 | },
614 | Product.SQUID_INK: {
615 | "take_width": 10,
616 | "clear_width": 0,
617 | "prevent_adverse": True,
618 | "adverse_volume": 15,
619 | "reversion_beta": -0.25,
620 | "disregard_edge": 1,
621 | "join_edge": 0,
622 | "default_edge": 2,
623 | "manage_position": True,
624 | "soft_position_limit": 10,
625 | },
626 | Product.PICNIC_BASKET1: {
627 | "default_spread_mean": 48.762433,
628 | "default_spread_std": 85.119451,
629 | "spread_std_window": 100,
630 | "z_score_threshold": 10,
631 | "z_score_close_threshold": 0.1,
632 | "target_position": 60,
633 | },
634 | Product.PICNIC_BASKET2: {
635 | "default_spread_mean": 30.235967,
636 | "default_spread_std": 59.849200,
637 | "spread_std_window": 50,
638 | "z_score_threshold": 15,
639 | "target_position": 100,
640 | "z_score_close_threshold": 0.2,
641 | }
642 | }
643 |
644 | BASKET1_WEIGHTS = {
645 | Product.CROISSANTS: 6,
646 | Product.JAMS: 3,
647 | Product.DJEMBES: 1,
648 | }
649 | BASKET2_WEIGHTS = {
650 | Product.CROISSANTS: 4,
651 | Product.JAMS: 2,
652 | }
653 |
654 |
655 | # -------------------------------------------------------------------
656 | # "Trade" class with static methods for each product or approach.
657 | # -------------------------------------------------------------------
658 | class Trade:
659 | """Trading strategies for different products."""
660 |
661 | @staticmethod
662 | def kelp(state: Status) -> List[Order]:
663 | """
664 | KELP is just a demonstration: we do a quote-based approach
665 | to buy below fair_value, sell above fair_value.
666 | Not strictly relevant to the VOUCHER logic.
667 | """
668 | orders = []
669 | AGGRESSOR = False
670 | QUOTER = True
671 | vwap_price = state.vwap
672 | fair_value = vwap_price
673 |
674 | if AGGRESSOR:
675 | # Omitted for brevity...
676 | pass
677 |
678 | if QUOTER:
679 | if state.bids:
680 | best_bid_level = max(state.bids, key=lambda x: x[1])
681 | bid_price, bid_vol = best_bid_level
682 | buy_qty = min(state.possible_buy_amt, bid_vol)
683 | if buy_qty > 0 and bid_price < fair_value:
684 | orders.append(Order(state.product, int(bid_price + 1), buy_qty))
685 |
686 | if state.asks:
687 | best_ask_level = max(state.asks, key=lambda x: abs(x[1]))
688 | ask_price, ask_vol = best_ask_level
689 | sell_qty = min(state.possible_sell_amt, abs(ask_vol))
690 | if sell_qty > 0 and ask_price > fair_value:
691 | orders.append(Order(state.product, int(ask_price - 1), -sell_qty))
692 |
693 | return orders
694 |
695 | @staticmethod
696 | def hedge_deltas(
697 | underlying_state: Status,
698 | v_9500_state: Status,
699 | v_9750_state: Status,
700 | v_10000_state: Status,
701 | v_10250_state: Status,
702 | v_10500_state: Status,
703 | ) -> List[Order]:
704 | """
705 | Simple net-delta hedging that restricts total net delta to ±50 at all times.
706 | """
707 | orders = []
708 |
709 | # 1. Calculate net delta: add up (position * delta) for each voucher
710 | net_delta = underlying_state.position # Start with underlying position
711 | for voucher_state, strike in [
712 | (v_9500_state, 9500),
713 | (v_9750_state, 9750),
714 | (v_10000_state, 10000),
715 | (v_10250_state, 10250),
716 | (v_10500_state, 10500),
717 | ]:
718 | if voucher_state is None:
719 | continue
720 | # Approximate delta
721 | delta_per_unit = BlackScholes.delta(
722 | spot=underlying_state.vwap,
723 | strike=strike,
724 | time_to_expiry=voucher_state.tte,
725 | volatility=voucher_state.volatility,
726 | )
727 | net_delta += delta_per_unit * voucher_state.position
728 |
729 | # 2. Check if net delta goes beyond ±50
730 | MAX_DELTA = 50
731 | if net_delta > MAX_DELTA:
732 | # Hedge quantity: bring net_delta *down* to +50
733 | hedge_quantity = int(net_delta - MAX_DELTA) # how many to sell
734 |
735 | # Make sure we do not exceed what's possible to sell
736 | # (the underlying is "VOLCANIC_ROCK")
737 | # If hedge_quantity is 20, we need to SELL 20
738 | can_sell = underlying_state.possible_sell_amt
739 | hedge_quantity = min(hedge_quantity, can_sell)
740 |
741 | # Place an order at best_bid to SELL
742 | if hedge_quantity > 0 and underlying_state.best_bid > 0:
743 | orders.append(
744 | Order(
745 | symbol=underlying_state.product,
746 | price=underlying_state.best_bid,
747 | quantity=-hedge_quantity, # negative => SELL
748 | )
749 | )
750 |
751 | elif net_delta < -MAX_DELTA:
752 | # Hedge quantity: bring net_delta *up* to -50
753 | hedge_quantity = int(abs(net_delta + MAX_DELTA)) # how many to buy
754 |
755 | # Make sure we do not exceed what's possible to buy
756 | can_buy = underlying_state.possible_buy_amt
757 | hedge_quantity = min(hedge_quantity, can_buy)
758 |
759 | # Place an order at best_ask to BUY
760 | if hedge_quantity > 0 and underlying_state.best_ask < float("inf"):
761 | orders.append(
762 | Order(
763 | symbol=underlying_state.product,
764 | price=underlying_state.best_ask,
765 | quantity=hedge_quantity, # positive => BUY
766 | )
767 | )
768 |
769 | # 3. If net_delta is between -50 and +50, do nothing (already within range)
770 | return orders
771 |
772 |
773 |
774 | @staticmethod
775 | def volcanic_rock(state: TradingState) -> List[Order]:
776 | """Generate trading orders for VOLCANIC_ROCK using a conservative mean-reversion strategy."""
777 | orders: List[Order] = []
778 |
779 | # Get current market data
780 | product = "VOLCANIC_ROCK"
781 | order_depth = state.order_depths[product]
782 | position = state.position.get(product, 0)
783 |
784 | # Calculate basic metrics
785 | best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
786 | best_ask = (
787 | min(order_depth.sell_orders.keys())
788 | if order_depth.sell_orders
789 | else float("inf")
790 | )
791 | mid_price = (
792 | (best_bid + best_ask) / 2 if best_bid and best_ask != float("inf") else None
793 | )
794 |
795 | if not mid_price:
796 | return orders
797 |
798 | # Calculate VWAP for mean reversion
799 | vwap = (
800 | sum(price * abs(qty) for price, qty in order_depth.buy_orders.items())
801 | / sum(abs(qty) for qty in order_depth.buy_orders.values())
802 | if order_depth.buy_orders
803 | else mid_price
804 | )
805 |
806 | # Conservative position limits
807 | max_position = 20 # Reduced from previous value
808 | min_position = -20
809 |
810 | # Calculate price deviation from VWAP
811 | price_deviation = (mid_price - vwap) / vwap
812 |
813 | # Trading thresholds
814 | entry_threshold = 0.002 # 0.2% deviation for entry
815 | exit_threshold = 0.001 # 0.1% deviation for exit
816 |
817 | # Position sizing based on deviation
818 | base_quantity = 5 # Reduced base quantity
819 | quantity = min(base_quantity, max_position - position)
820 |
821 | # Trading logic
822 | if price_deviation > entry_threshold and position < max_position:
823 | # Price is above VWAP - consider selling
824 | if best_ask < float("inf"):
825 | orders.append(Order(product, best_ask, -quantity))
826 |
827 | elif price_deviation < -entry_threshold and position > min_position:
828 | # Price is below VWAP - consider buying
829 | if best_bid > 0:
830 | orders.append(Order(product, best_bid, quantity))
831 |
832 | # Exit logic - more aggressive
833 | if position > 0 and price_deviation > exit_threshold:
834 | # Exit long position if price is above VWAP
835 | if best_ask < float("inf"):
836 | orders.append(Order(product, best_ask, -position))
837 |
838 | elif position < 0 and price_deviation < -exit_threshold:
839 | # Exit short position if price is below VWAP
840 | if best_bid > 0:
841 | orders.append(Order(product, best_bid, -position))
842 |
843 | return orders
844 |
845 | @staticmethod
846 | def volcanic_rock_copy(state: TradingState) -> List[Order]:
847 | """Trading strategy for VOLCANIC_ROCK"""
848 | orders = []
849 |
850 | # Get bias from Pablo's trades
851 | bias = 0.0
852 | if 'VOLCANIC_ROCK' in state.market_trades:
853 | for trade in state.market_trades['VOLCANIC_ROCK']:
854 | if trade.buyer == 'Pablo':
855 | bias = min(1.0, bias + 0.2) # Increase bullish bias
856 | elif trade.seller == 'Pablo':
857 | bias = max(-1.0, bias - 0.2) # Increase bearish bias
858 |
859 | product = "VOLCANIC_ROCK"
860 | order_depth = state.order_depths[product]
861 | position = state.position.get(product, 0)
862 |
863 | # Calculate basic metrics
864 | best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
865 | best_ask = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float("inf")
866 | mid_price = (best_bid + best_ask) / 2 if best_bid and best_ask != float("inf") else None
867 |
868 | if not mid_price:
869 | return orders
870 |
871 | # Calculate VWAP
872 | vwap = (
873 | sum(price * abs(qty) for price, qty in order_depth.buy_orders.items())
874 | / sum(abs(qty) for qty in order_depth.buy_orders.values())
875 | if order_depth.buy_orders
876 | else mid_price
877 | )
878 |
879 | # Adjust position limits based on bias
880 | max_position = int(400 * (1 + abs(bias) * 0.2)) # Increase limit when confident
881 | min_position = -max_position
882 |
883 | # Calculate price deviation from VWAP
884 | price_deviation = (mid_price - vwap) / vwap
885 |
886 | # Adjust thresholds based on bias
887 | base_entry = 0.002
888 | base_exit = 0.001
889 |
890 | if bias > 0: # Bullish
891 | entry_threshold = base_entry * (1 - bias * 0.5) # Lower threshold for buys
892 | exit_threshold = base_exit * (1 + bias * 0.5) # Higher threshold for sells
893 | else: # Bearish
894 | entry_threshold = base_entry * (1 + abs(bias) * 0.5) # Higher threshold for buys
895 | exit_threshold = base_exit * (1 - abs(bias) * 0.5) # Lower threshold for sells
896 |
897 | # Trading logic with bias adjustments
898 | base_quantity = int(5 * (1 + abs(bias))) # Increase size with confidence
899 | quantity = min(base_quantity, max_position - position)
900 |
901 | if price_deviation > entry_threshold and position < max_position:
902 | if best_ask < float("inf"):
903 | orders.append(Order(product, best_ask, -quantity))
904 |
905 | elif price_deviation < -entry_threshold and position > min_position:
906 | if best_bid > 0:
907 | orders.append(Order(product, best_bid, quantity))
908 |
909 | # Exit logic
910 | if position > 0 and price_deviation > exit_threshold:
911 | if best_ask < float("inf"):
912 | orders.append(Order(product, best_ask, -position))
913 |
914 | elif position < 0 and price_deviation < -exit_threshold:
915 | if best_bid > 0:
916 | orders.append(Order(product, best_bid, -position))
917 | print("volcanic_rock_copy orders", orders)
918 | return orders
919 |
920 | @staticmethod
921 | def voucher(state: Status, underlying_state: Status, strike: int) -> List[Order]:
922 | """
923 | Simplified volatility trading for 9750 and 10000 strikes
924 | """
925 | orders = []
926 |
927 | # Only trade 9750 and 10000 strikes
928 | if strike not in [9750, 10000]:
929 | return orders
930 |
931 | # Update price and volatility history
932 | state.update_IV_history(underlying_state.vwap)
933 | state.update_price_history(state.vwap)
934 |
935 | # Calculate current and previous implied volatility
936 | current_IV = BlackScholes.implied_volatility(
937 | call_price=state.vwap,
938 | spot=underlying_state.vwap,
939 | strike=strike,
940 | time_to_expiry=state.tte,
941 | )
942 | prev_IV = state.moving_average(underlying_state.vwap)
943 |
944 | # Get market trends
945 | price_trend = state.get_price_trend()
946 | vol_trend = state.get_volatility_trend()
947 | profit_trend = state.get_recent_profit_trend()
948 |
949 | # Base parameters
950 | base_threshold = 0.002
951 | max_position = 200
952 |
953 | # Adjust threshold based on volatility trend
954 | threshold = base_threshold
955 | if vol_trend > 0.01:
956 | threshold *= 1.2
957 | elif vol_trend < -0.01:
958 | threshold *= 0.8
959 |
960 | # Selling volatility (when current IV > previous IV + threshold)
961 | if current_IV > prev_IV + threshold:
962 | if state.bids and state.position > -max_position:
963 | # Calculate position room and base quantity
964 | position_room = max_position + state.position
965 | base_quantity = min(state.possible_sell_amt, state.bids[0][1])
966 | quantity = min(base_quantity, position_room)
967 |
968 | # Adjust quantity based on market conditions
969 | if abs(price_trend) > 0.1:
970 | quantity = int(quantity * 0.7)
971 | if vol_trend > 0.01:
972 | quantity = int(quantity * 0.8)
973 | if profit_trend < -1000:
974 | quantity = int(quantity * 0.6)
975 |
976 | # Place sell order if quantity is positive
977 | if quantity > 0:
978 | orders.append(Order(state.product, state.best_bid, -quantity))
979 | state.last_trade_timestamp = state.timestamp
980 | state.trade_count += 1
981 |
982 | # Buying volatility (when current IV < previous IV - threshold)
983 | elif current_IV < prev_IV - threshold:
984 | if state.asks and state.position < max_position:
985 | # Calculate position room and base quantity
986 | position_room = max_position - state.position
987 | base_quantity = min(state.possible_buy_amt, abs(state.asks[0][1]))
988 | quantity = min(base_quantity, position_room)
989 |
990 | # Adjust quantity based on market conditions
991 | if abs(price_trend) > 0.1:
992 | quantity = int(quantity * 0.7)
993 | if vol_trend > 0.01:
994 | quantity = int(quantity * 0.8)
995 | if profit_trend < -1000:
996 | quantity = int(quantity * 0.6)
997 |
998 | # Place buy order if quantity is positive
999 | if quantity > 0:
1000 | orders.append(Order(state.product, state.best_ask, quantity))
1001 | state.last_trade_timestamp = state.timestamp
1002 | state.trade_count += 1
1003 |
1004 | return orders
1005 |
1006 |
1007 | class Trader:
1008 | def __init__(self, params=None):
1009 | self.position = 0
1010 | self.desired_position_pct = {asset: 0.0 for asset in COPY_TARGETS.keys()}
1011 | self.position_limit = 75
1012 | self.sunlight_window = [] # Keep track of recent sunlight values
1013 | self.window_size = 10 # Increased to 10 for slope calculation
1014 | self.sunlight_threshold = 49 # Only trade when above this level
1015 | self.slope_threshold = 0.004 # Minimum slope to trigger position close
1016 | self.last_trend = None
1017 | if params is None:
1018 | params = PARAMS
1019 | self.params = params
1020 | self.LIMIT = {Product.RAINFOREST_RESIN: 50,
1021 | Product.KELP: 50,
1022 | Product.SQUID_INK: 50,
1023 | Product.CROISSANTS: 250,
1024 | Product.JAMS: 350,
1025 | Product.DJEMBES: 60,
1026 | Product.PICNIC_BASKET1: 60,
1027 | Product.PICNIC_BASKET2: 100,
1028 | Product.VOLCANIC_ROCK: 200,
1029 | Product.VOLCANIC_ROCK_VOUCHER_9500: 200,
1030 | }
1031 |
1032 | def should_copy_trade(self, trade, target_config, actual_trader):
1033 | """Determine if we should copy this trade based on filters"""
1034 | # Check if it's our target trader
1035 | if trade.buyer != actual_trader and trade.seller != actual_trader:
1036 | return False
1037 |
1038 | # Check counterparty filter if specified
1039 | if target_config["counterparty"]:
1040 | if trade.buyer == actual_trader and trade.seller != target_config["counterparty"]:
1041 | return False
1042 | if trade.seller == actual_trader and trade.buyer != target_config["counterparty"]:
1043 | return False
1044 |
1045 | # Check exact quantity match if specified (quantity = 0 means copy any quantity)
1046 | if target_config["quantity"] > 0 and trade.quantity != target_config["quantity"]:
1047 | return False
1048 |
1049 | return True
1050 |
1051 | def adjust_desired_positions(self,state: TradingState, asset: str):
1052 | for signal_config in COPY_TARGETS[asset]["signals"]:
1053 | target_trader = signal_config["trader"]
1054 | inverse_copy = target_trader.startswith("-")
1055 | actual_trader = target_trader[1:] if inverse_copy else target_trader
1056 | watch_asset = signal_config["watch_asset"]
1057 |
1058 | trades = []
1059 | try:
1060 | trades = state.market_trades[watch_asset]
1061 | except:
1062 | continue
1063 |
1064 | for trade in trades:
1065 | if self.should_copy_trade(trade, signal_config, actual_trader):
1066 | if trade.buyer == actual_trader:
1067 | # If they buy: normal copy → positive adjustment, inverse copy → negative adjustment
1068 | adjustment = -signal_config["weight"] if inverse_copy else signal_config["weight"]
1069 | self.desired_position_pct[asset] = min(1.0, max(-1.0,
1070 | self.desired_position_pct[asset] + adjustment))
1071 | if trade.seller == actual_trader:
1072 | # If they sell: normal copy → negative adjustment, inverse copy → positive adjustment
1073 | adjustment = signal_config["weight"] if inverse_copy else -signal_config["weight"]
1074 | self.desired_position_pct[asset] = min(1.0, max(-1.0,self.desired_position_pct[asset] + adjustment))
1075 |
1076 |
1077 | def calculate_slope(self, values):
1078 | if len(values) < 2:
1079 | return 0
1080 | x = np.arange(len(values))
1081 | y = np.array(values)
1082 | slope, _ = np.polyfit(x, y, 1)
1083 | return slope
1084 |
1085 | def make_orders(
1086 | self,
1087 | product,
1088 | order_depth: OrderDepth,
1089 | fair_value: float,
1090 | position: int,
1091 | buy_order_volume: int,
1092 | sell_order_volume: int,
1093 | disregard_edge: float, # disregard trades within this edge for pennying or joining
1094 | join_edge: float, # join trades within this edge
1095 | default_edge: float, # default edge to request if there are no levels to penny or join
1096 | manage_position: bool = False,
1097 | soft_position_limit: int = 0,
1098 | # will penny all other levels with higher edge
1099 | ):
1100 | orders: List[Order] = []
1101 | asks_above_fair = [
1102 | price
1103 | for price in order_depth.sell_orders.keys()
1104 | if price > fair_value + disregard_edge
1105 | ]
1106 | bids_below_fair = [
1107 | price
1108 | for price in order_depth.buy_orders.keys()
1109 | if price < fair_value - disregard_edge
1110 | ]
1111 | best_ask_above_fair = min(asks_above_fair) if len(asks_above_fair) > 0 else None
1112 | best_bid_below_fair = max(bids_below_fair) if len(bids_below_fair) > 0 else None
1113 |
1114 | ask = round(fair_value + default_edge)
1115 | if best_ask_above_fair != None:
1116 | if abs(best_ask_above_fair - fair_value) <= join_edge:
1117 | ask = best_ask_above_fair # join
1118 | else:
1119 | ask = best_ask_above_fair - 1 # penny
1120 |
1121 | bid = round(fair_value - default_edge)
1122 | if best_bid_below_fair != None:
1123 | if abs(fair_value - best_bid_below_fair) <= join_edge:
1124 | bid = best_bid_below_fair # join
1125 | else:
1126 | bid = best_bid_below_fair + 1 # penny
1127 |
1128 | if manage_position:
1129 | if position > soft_position_limit:
1130 | ask -= 1
1131 | elif position < -1 * soft_position_limit:
1132 | bid += 1
1133 |
1134 | buy_order_volume, sell_order_volume = self.market_make(
1135 | product,
1136 | orders,
1137 | bid,
1138 | ask,
1139 | position,
1140 | buy_order_volume,
1141 | sell_order_volume,
1142 | )
1143 | return orders, buy_order_volume, sell_order_volume
1144 |
1145 | def market_make(
1146 | self,
1147 | product: str,
1148 | orders: List[Order],
1149 | bid: int,
1150 | ask: int,
1151 | position: int,
1152 | buy_order_volume: int,
1153 | sell_order_volume: int,
1154 | ):
1155 | buy_quantity = self.LIMIT[product] - (position + buy_order_volume)
1156 | if buy_quantity > 0:
1157 | orders.append(Order(product, round(bid), buy_quantity)) # Buy order
1158 |
1159 | sell_quantity = self.LIMIT[product] + (position - sell_order_volume)
1160 | if sell_quantity > 0:
1161 | orders.append(Order(product, round(ask), -sell_quantity)) # Sell order
1162 |
1163 | return buy_order_volume, sell_order_volume
1164 |
1165 | def take_orders(
1166 | self,
1167 | product: str,
1168 | order_depth: OrderDepth,
1169 | fair_value: float,
1170 | take_width: float,
1171 | position: int,
1172 | prevent_adverse: bool = False,
1173 | adverse_volume: int = 0,
1174 | ):
1175 | orders: List[Order] = []
1176 | buy_order_volume = 0
1177 | sell_order_volume = 0
1178 |
1179 | buy_order_volume, sell_order_volume = self.take_best_orders(
1180 | product,
1181 | fair_value,
1182 | take_width,
1183 | orders,
1184 | order_depth,
1185 | position,
1186 | buy_order_volume,
1187 | sell_order_volume,
1188 | prevent_adverse,
1189 | adverse_volume,
1190 | )
1191 | return orders, buy_order_volume, sell_order_volume
1192 |
1193 | def take_best_orders(
1194 | self,
1195 | product: str,
1196 | fair_value: int,
1197 | take_width: float,
1198 | orders: List[Order],
1199 | order_depth: OrderDepth,
1200 | position: int,
1201 | buy_order_volume: int,
1202 | sell_order_volume: int,
1203 | prevent_adverse: bool = False,
1204 | adverse_volume: int = 0,
1205 | ):
1206 | position_limit = self.LIMIT[product]
1207 | if len(order_depth.sell_orders) != 0:
1208 | best_ask = min(order_depth.sell_orders.keys())
1209 | best_ask_amount = -1 * order_depth.sell_orders[best_ask]
1210 |
1211 | if not prevent_adverse or abs(best_ask_amount) <= adverse_volume:
1212 | if best_ask <= fair_value - take_width:
1213 | quantity = min(best_ask_amount, position_limit - position) # Max amount to buy
1214 | if quantity > 0:
1215 | orders.append(Order(product, best_ask, quantity))
1216 | buy_order_volume += quantity
1217 | order_depth.sell_orders[best_ask] += quantity
1218 | if order_depth.sell_orders[best_ask] == 0:
1219 | del order_depth.sell_orders[best_ask]
1220 |
1221 | if len(order_depth.buy_orders) != 0:
1222 | best_bid = max(order_depth.buy_orders.keys())
1223 | best_bid_amount = order_depth.buy_orders[best_bid]
1224 |
1225 | if not prevent_adverse or abs(best_bid_amount) <= adverse_volume:
1226 | if best_bid >= fair_value + take_width:
1227 | quantity = min(best_bid_amount, position_limit + position) # Max amount to sell
1228 | if quantity > 0:
1229 | orders.append(Order(product, best_bid, -1 * quantity))
1230 | sell_order_volume += quantity
1231 | order_depth.buy_orders[best_bid] -= quantity
1232 | if order_depth.buy_orders[best_bid] == 0:
1233 | del order_depth.buy_orders[best_bid]
1234 |
1235 | return buy_order_volume, sell_order_volume
1236 |
1237 | def clear_orders(
1238 | self,
1239 | product: str,
1240 | order_depth: OrderDepth,
1241 | fair_value: float,
1242 | clear_width: int,
1243 | position: int,
1244 | buy_order_volume: int,
1245 | sell_order_volume: int,
1246 | ):
1247 | orders: List[Order] = []
1248 | buy_order_volume, sell_order_volume = self.clear_position_order(
1249 | product,
1250 | fair_value,
1251 | clear_width,
1252 | orders,
1253 | order_depth,
1254 | position,
1255 | buy_order_volume,
1256 | sell_order_volume,
1257 | )
1258 | return orders, buy_order_volume, sell_order_volume
1259 |
1260 | def clear_position_order(
1261 | self,
1262 | product: str,
1263 | fair_value: float,
1264 | width: int,
1265 | orders: List[Order],
1266 | order_depth: OrderDepth,
1267 | position: int,
1268 | buy_order_volume: int,
1269 | sell_order_volume: int,
1270 | ) -> List[Order]:
1271 | position_after_take = position + buy_order_volume - sell_order_volume
1272 | fair_for_bid = round(fair_value - width)
1273 | fair_for_ask = round(fair_value + width)
1274 |
1275 | buy_quantity = self.LIMIT[product] - (position + buy_order_volume)
1276 | sell_quantity = self.LIMIT[product] + (position - sell_order_volume)
1277 |
1278 | if position_after_take > 0:
1279 | # Aggregate volume from all buy orders with price greater than fair_for_ask
1280 | clear_quantity = sum(
1281 | volume
1282 | for price, volume in order_depth.buy_orders.items()
1283 | if price >= fair_for_ask
1284 | )
1285 | clear_quantity = min(clear_quantity, position_after_take)
1286 | sent_quantity = min(sell_quantity, clear_quantity)
1287 | if sent_quantity > 0:
1288 | orders.append(Order(product, fair_for_ask, -abs(sent_quantity)))
1289 | sell_order_volume += abs(sent_quantity)
1290 |
1291 | if position_after_take < 0:
1292 | # Aggregate volume from all sell orders with price lower than fair_for_bid
1293 | clear_quantity = sum(
1294 | abs(volume)
1295 | for price, volume in order_depth.sell_orders.items()
1296 | if price <= fair_for_bid
1297 | )
1298 | clear_quantity = min(clear_quantity, abs(position_after_take))
1299 | sent_quantity = min(buy_quantity, clear_quantity)
1300 | if sent_quantity > 0:
1301 | orders.append(Order(product, fair_for_bid, abs(sent_quantity)))
1302 | buy_order_volume += abs(sent_quantity)
1303 |
1304 | return buy_order_volume, sell_order_volume
1305 |
1306 | def kelp_fair_value(self, order_depth: OrderDepth, traderObject) -> float:
1307 | if len(order_depth.sell_orders) != 0 and len(order_depth.buy_orders) != 0:
1308 | best_ask = min(order_depth.sell_orders.keys())
1309 | best_bid = max(order_depth.buy_orders.keys())
1310 | filtered_ask = [
1311 | price
1312 | for price in order_depth.sell_orders.keys()
1313 | if abs(order_depth.sell_orders[price])
1314 | >= self.params[Product.KELP]["adverse_volume"]
1315 | ]
1316 | filtered_bid = [
1317 | price
1318 | for price in order_depth.buy_orders.keys()
1319 | if abs(order_depth.buy_orders[price])
1320 | >= self.params[Product.KELP]["adverse_volume"]
1321 | ]
1322 | mm_ask = min(filtered_ask) if len(filtered_ask) > 0 else None
1323 | mm_bid = max(filtered_bid) if len(filtered_bid) > 0 else None
1324 | if mm_ask == None or mm_bid == None:
1325 | if traderObject.get("kelp_last_price", None) == None:
1326 | mmmid_price = (best_ask + best_bid) / 2
1327 | else:
1328 | mmmid_price = traderObject["kelp_last_price"]
1329 | else:
1330 | mmmid_price = (mm_ask + mm_bid) / 2
1331 |
1332 | if traderObject.get("kelp_last_price", None) != None:
1333 | last_price = traderObject["kelp_last_price"]
1334 | last_returns = (mmmid_price - last_price) / last_price
1335 | pred_returns = (
1336 | last_returns * self.params[Product.KELP]["reversion_beta"]
1337 | )
1338 | fair = mmmid_price + (mmmid_price * pred_returns)
1339 | else:
1340 | fair = mmmid_price
1341 | traderObject["kelp_last_price"] = mmmid_price
1342 | return fair
1343 | return None
1344 |
1345 | def take_best_squid_orders(
1346 | self,
1347 | product: str,
1348 | vwap: float,
1349 | take_width: float,
1350 | orders: List[Order],
1351 | order_depth: OrderDepth,
1352 | position: int,
1353 | buy_order_volume: int,
1354 | sell_order_volume: int,
1355 | prevent_adverse: bool = False,
1356 | adverse_volume: int = 0,
1357 | # New inputs for cross-product signals:
1358 | kelp_order_depth: OrderDepth = None,
1359 | baseline_mm_volume: int = 26, # Typical market maker volume
1360 | mm_tolerance: float = 0.3 # Tolerance factor (30%)
1361 | ):
1362 |
1363 | position_limit = self.LIMIT[product]
1364 |
1365 | # ---------------------------
1366 | # Detect cross-market signals from kelp.
1367 | anomaly_bullish = False
1368 | anomaly_bearish = False
1369 |
1370 | # Check for bullish signal using kelp ask side.
1371 | if kelp_order_depth is not None and len(kelp_order_depth.sell_orders) != 0:
1372 | kelp_best_ask = min(kelp_order_depth.sell_orders.keys())
1373 | kelp_best_ask_volume = abs(kelp_order_depth.sell_orders[kelp_best_ask])
1374 | # If the kelp ask volume is notably lower than the baseline, flag bullish anomaly.
1375 | if kelp_best_ask_volume < baseline_mm_volume * (1 - mm_tolerance):
1376 | anomaly_bullish = True
1377 |
1378 | # Check for bearish signal using kelp bid side.
1379 | if kelp_order_depth is not None and len(kelp_order_depth.buy_orders) != 0:
1380 | kelp_best_bid = max(kelp_order_depth.buy_orders.keys())
1381 | kelp_best_bid_volume = abs(kelp_order_depth.buy_orders[kelp_best_bid])
1382 | # If the kelp bid volume is notably lower than the baseline, flag bearish anomaly.
1383 | if kelp_best_bid_volume < baseline_mm_volume * (1 - mm_tolerance):
1384 | anomaly_bearish = True
1385 |
1386 | # Adjust effective thresholds based on detected signals.
1387 | # For bullish signals, relax buy threshold; for bearish signals, relax sell threshold.
1388 | effective_take_width_buy = take_width * (0.5 if anomaly_bullish else 1.0)
1389 | effective_take_width_sell = take_width * (0.5 if anomaly_bearish else 1.0)
1390 |
1391 | # ---------------------------
1392 | # Process Sell Orders (Buy in squid ink from the sell orders)
1393 | if len(order_depth.sell_orders) != 0:
1394 | best_ask = min(order_depth.sell_orders.keys())
1395 | best_ask_amount = -1 * order_depth.sell_orders[best_ask]
1396 |
1397 | if not prevent_adverse or abs(best_ask_amount) <= adverse_volume:
1398 | # When buying, check if price is below VWAP adjusted by effective_take_width_buy.
1399 | if best_ask <= vwap - effective_take_width_buy:
1400 | quantity = min(best_ask_amount, position_limit - position) # Maximum amount to buy
1401 | if quantity > 0:
1402 | orders.append(Order(product, best_ask, quantity))
1403 | buy_order_volume += quantity
1404 | order_depth.sell_orders[best_ask] += quantity
1405 | if order_depth.sell_orders[best_ask] == 0:
1406 | del order_depth.sell_orders[best_ask]
1407 |
1408 | # Process Buy Orders (Sell in squid ink into the bid orders)
1409 | if len(order_depth.buy_orders) != 0:
1410 | best_bid = max(order_depth.buy_orders.keys())
1411 | best_bid_amount = order_depth.buy_orders[best_bid]
1412 |
1413 | if not prevent_adverse or abs(best_bid_amount) <= adverse_volume:
1414 | # When selling, check if price is above VWAP adjusted by effective_take_width_sell.
1415 | if best_bid >= vwap + effective_take_width_sell:
1416 | quantity = min(best_bid_amount, position_limit + position) # Maximum amount to sell
1417 | if quantity > 0:
1418 | orders.append(Order(product, best_bid, -1 * quantity))
1419 | sell_order_volume += quantity
1420 | order_depth.buy_orders[best_bid] -= quantity
1421 | if order_depth.buy_orders[best_bid] == 0:
1422 | del order_depth.buy_orders[best_bid]
1423 |
1424 | return buy_order_volume, sell_order_volume
1425 |
1426 | def squid_ink_fair_value(self, order_depth: OrderDepth, traderObject) -> float:
1427 | if len(order_depth.sell_orders) != 0 and len(order_depth.buy_orders) != 0:
1428 | best_ask = min(order_depth.sell_orders.keys())
1429 | best_bid = max(order_depth.buy_orders.keys())
1430 | filtered_ask = [
1431 | price
1432 | for price in order_depth.sell_orders.keys()
1433 | if abs(order_depth.sell_orders[price])
1434 | >= self.params[Product.SQUID_INK]["adverse_volume"]
1435 | ]
1436 | filtered_bid = [
1437 | price
1438 | for price in order_depth.buy_orders.keys()
1439 | if abs(order_depth.buy_orders[price])
1440 | >= self.params[Product.SQUID_INK]["adverse_volume"]
1441 | ]
1442 | mm_ask = min(filtered_ask) if len(filtered_ask) > 0 else None
1443 | mm_bid = max(filtered_bid) if len(filtered_bid) > 0 else None
1444 | if mm_ask == None or mm_bid == None:
1445 | if traderObject.get("squid_ink_last_price", None) == None:
1446 | mm_mid_price = (best_ask + best_bid) / 2
1447 | else:
1448 | mm_mid_price = traderObject["squid_ink_last_price"]
1449 | else:
1450 | mm_mid_price = (mm_ask + mm_bid) / 2
1451 |
1452 | if "squid_ink_trade_history" not in traderObject:
1453 | traderObject["squid_ink_trade_history"] = []
1454 | vol_ask = abs(order_depth.sell_orders.get(mm_ask, 0)) if mm_ask else 0
1455 | vol_bid = abs(order_depth.buy_orders.get(mm_bid, 0)) if mm_bid else 0
1456 | volume = min(vol_ask, vol_bid)
1457 | traderObject["squid_ink_trade_history"].append((mm_mid_price, volume))
1458 |
1459 | max_window = 100
1460 | if len(traderObject["squid_ink_trade_history"]) > max_window:
1461 | traderObject["squid_ink_trade_history"].pop(0)
1462 | trade_history = traderObject["squid_ink_trade_history"]
1463 | vwap_numerator = sum(price * vol for price, vol in trade_history)
1464 | vwap_denominator = sum(vol for _, vol in trade_history)
1465 | vwap = vwap_numerator / vwap_denominator if vwap_denominator != 0 else mm_mid_price
1466 |
1467 | if traderObject.get("squid_ink_last_price", None) != None:
1468 | last_price = traderObject["squid_ink_last_price"]
1469 | last_returns = (mm_mid_price - last_price) / last_price
1470 | pred_returns = (last_returns * self.params[Product.SQUID_INK]["reversion_beta"])
1471 | fair = mm_mid_price + (mm_mid_price * pred_returns)
1472 | else:
1473 | fair = mm_mid_price
1474 | traderObject["squid_ink_last_price"] = mm_mid_price
1475 | return fair, vwap
1476 | return None
1477 |
1478 | def take_squid_orders(
1479 | self,
1480 | product: str,
1481 | vwap: int,
1482 | order_depth: OrderDepth,
1483 | take_width: float,
1484 | position: int,
1485 | prevent_adverse: bool = False,
1486 | adverse_volume: int = 0,
1487 | kelp_order_depth: OrderDepth = None,
1488 | baseline_mm_volume: int = 26,
1489 | mm_tolerance: float = 0.3
1490 | ):
1491 | orders: List[Order] = []
1492 | buy_order_volume = 0
1493 | sell_order_volume = 0
1494 |
1495 | buy_order_volume, sell_order_volume = self.take_best_squid_orders(
1496 | product,
1497 | vwap,
1498 | take_width,
1499 | orders,
1500 | order_depth,
1501 | position,
1502 | buy_order_volume,
1503 | sell_order_volume,
1504 | prevent_adverse,
1505 | adverse_volume,
1506 | kelp_order_depth,
1507 | baseline_mm_volume,
1508 | mm_tolerance
1509 | )
1510 | return orders, buy_order_volume, sell_order_volume
1511 |
1512 | def get_swmid(self, order_depth) -> float:
1513 | best_bid = max(order_depth.buy_orders.keys())
1514 | best_ask = min(order_depth.sell_orders.keys())
1515 | best_bid_vol = abs(order_depth.buy_orders[best_bid])
1516 | best_ask_vol = abs(order_depth.sell_orders[best_ask])
1517 | return (best_bid * best_ask_vol + best_ask * best_bid_vol) / (
1518 | best_bid_vol + best_ask_vol
1519 | )
1520 |
1521 | def get_synthetic_basket_order_depth(
1522 | self,
1523 | order_depths: Dict[str, OrderDepth],
1524 | basket_weights: Dict[str, int],
1525 | ) -> OrderDepth:
1526 |
1527 | synthetic_order_depth = OrderDepth()
1528 | best_bids = {}
1529 | best_asks = {}
1530 | for product in basket_weights:
1531 | if product in order_depths:
1532 | order_depth = order_depths[product]
1533 | else:
1534 | continue
1535 | best_bids[product] = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
1536 | best_asks[product] = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float("inf")
1537 |
1538 | implied_bid = sum(best_bids[product] * quantity for product, quantity in basket_weights.items())
1539 | implied_ask = sum(best_asks[product] * quantity for product, quantity in basket_weights.items())
1540 |
1541 | if implied_bid > 0:
1542 | bid_volumes = []
1543 | for product, quantity in basket_weights.items():
1544 | volume = order_depths[product].buy_orders.get(best_bids[product], 0) // quantity
1545 | bid_volumes.append(volume)
1546 | synthetic_order_depth.buy_orders[implied_bid] = min(bid_volumes)
1547 |
1548 | if implied_ask < float("inf"):
1549 | ask_volumes = []
1550 | for product, quantity in basket_weights.items():
1551 | volume = -order_depths[product].sell_orders.get(best_asks[product], 0) // quantity
1552 | ask_volumes.append(volume)
1553 | synthetic_order_depth.sell_orders[implied_ask] = -min(ask_volumes)
1554 |
1555 | return synthetic_order_depth
1556 |
1557 | def convert_synthetic_basket_orders(
1558 | self,
1559 | synthetic_orders: List[Order],
1560 | order_depths: Dict[str, OrderDepth],
1561 | basket_weights: Dict[str, int]
1562 | ) -> Dict[str, List[Order]]:
1563 | # Initialize the dictionary to store component orders
1564 | component_orders = {product: [] for product in basket_weights}
1565 |
1566 | # Get the best bid and ask for the synthetic basket
1567 | synthetic_basket_order_depth = self.get_synthetic_basket_order_depth(order_depths, basket_weights)
1568 |
1569 | best_bid = (
1570 | max(synthetic_basket_order_depth.buy_orders.keys())
1571 | if synthetic_basket_order_depth.buy_orders
1572 | else 0
1573 | )
1574 | best_ask = (
1575 | min(synthetic_basket_order_depth.sell_orders.keys())
1576 | if synthetic_basket_order_depth.sell_orders
1577 | else float("inf")
1578 | )
1579 |
1580 | # Iterate through each synthetic basket order
1581 | for order in synthetic_orders:
1582 | # Extract the price and quantity from the synthetic basket order
1583 | price = order.price
1584 | quantity = order.quantity
1585 |
1586 | # Check if the synthetic basket order aligns with the best bid or ask
1587 | if quantity > 0 and price >= best_ask: # Buy the basket
1588 | component_prices = {
1589 | product: min(order_depths[product].sell_orders.keys(), default=float("inf"))
1590 | for product in basket_weights
1591 | }
1592 |
1593 | elif quantity < 0 and price <= best_bid: # Sell the basket
1594 | component_prices = {
1595 | product: max(order_depths[product].buy_orders.keys(), default=0)
1596 | for product in basket_weights
1597 | }
1598 | else:
1599 | continue
1600 |
1601 | for product, product_price in component_prices.items():
1602 | component_orders[product].append(
1603 | Order(
1604 | product,
1605 | product_price,
1606 | quantity * basket_weights[product]
1607 | )
1608 | )
1609 |
1610 | return component_orders
1611 |
1612 | def execute_spread_orders(
1613 | self,
1614 | target_position: int,
1615 | basket_position: int,
1616 | order_depths: Dict[str, OrderDepth],
1617 | product: Product,
1618 | basket_weights: Dict[str, int]
1619 | ):
1620 |
1621 | if target_position == basket_position:
1622 | return None
1623 |
1624 | target_quantity = abs(target_position - basket_position)
1625 | basket_order_depth = order_depths[product]
1626 | synthetic_order_depth = self.get_synthetic_basket_order_depth(order_depths, basket_weights)
1627 |
1628 | if target_position > basket_position:
1629 | basket_ask_price = min(basket_order_depth.sell_orders.keys())
1630 | basket_ask_volume = abs(basket_order_depth.sell_orders[basket_ask_price])
1631 |
1632 | synthetic_bid_price = max(synthetic_order_depth.buy_orders.keys())
1633 | synthetic_bid_volume = abs(synthetic_order_depth.buy_orders[synthetic_bid_price])
1634 |
1635 | orderbook_volume = min(basket_ask_volume, synthetic_bid_volume)
1636 | execute_volume = min(orderbook_volume, target_quantity)
1637 |
1638 | basket_orders = [Order(product, basket_ask_price, execute_volume)]
1639 | synthetic_orders = [Order(Product.SYNTHETIC, synthetic_bid_price, -execute_volume)]
1640 | else:
1641 | basket_bid_price = max(basket_order_depth.buy_orders.keys())
1642 | basket_bid_volume = abs(basket_order_depth.buy_orders[basket_bid_price])
1643 |
1644 | synthetic_ask_price = min(synthetic_order_depth.sell_orders.keys())
1645 | synthetic_ask_volume = abs(synthetic_order_depth.sell_orders[synthetic_ask_price])
1646 |
1647 | orderbook_volume = min(basket_bid_volume, synthetic_ask_volume)
1648 | execute_volume = min(orderbook_volume, target_quantity)
1649 |
1650 | basket_orders = [Order(product, basket_bid_price, -execute_volume)]
1651 | synthetic_orders = [Order(Product.SYNTHETIC, synthetic_ask_price, execute_volume)]
1652 |
1653 | aggregate_orders = self.convert_synthetic_basket_orders(synthetic_orders, order_depths, basket_weights)
1654 | aggregate_orders[product] = basket_orders
1655 | return aggregate_orders
1656 |
1657 | def spread_orders(
1658 | self,
1659 | order_depths: Dict[str, OrderDepth],
1660 | product: Product,
1661 | basket_weights: Dict[str, int],
1662 | basket_position: int,
1663 | spread_data: Dict[str, Any],
1664 | ):
1665 | if product not in order_depths.keys():
1666 | return None
1667 |
1668 | basket_order_depth = order_depths[product]
1669 | synthetic_order_depth = self.get_synthetic_basket_order_depth(order_depths, basket_weights)
1670 |
1671 | basket_swmid = self.get_swmid(basket_order_depth)
1672 | synthetic_swmid = self.get_swmid(synthetic_order_depth)
1673 | spread = basket_swmid - synthetic_swmid
1674 |
1675 | spread_data["spread_history"].append(spread)
1676 |
1677 | spread_std_window = self.params[product]["spread_std_window"]
1678 | if (len(spread_data["spread_history"]) < spread_std_window):
1679 | return None
1680 | elif (len(spread_data["spread_history"]) > spread_std_window):
1681 | spread_data["spread_history"].pop(0)
1682 |
1683 | spread_std = np.std(spread_data["spread_history"])
1684 | default_mean = self.params[product]["default_spread_mean"]
1685 | z_threshold = self.params[product]["z_score_threshold"]
1686 | target_position = self.params[product]["target_position"]
1687 | zscore = (spread - default_mean) / spread_std
1688 |
1689 | if abs(zscore) < self.params[product]["z_score_close_threshold"] and basket_position != 0:
1690 | return self.execute_spread_orders(
1691 | 0, # Target position is zero to close the position
1692 | basket_position,
1693 | order_depths,
1694 | product,
1695 | basket_weights,
1696 | )
1697 |
1698 | if zscore >= z_threshold and basket_position != -target_position:
1699 | return self.execute_spread_orders(
1700 | -target_position,
1701 | basket_position,
1702 | order_depths,
1703 | product,
1704 | basket_weights,
1705 | )
1706 |
1707 | if zscore <= -z_threshold and basket_position != target_position:
1708 | return self.execute_spread_orders(
1709 | target_position,
1710 | basket_position,
1711 | order_depths,
1712 | product,
1713 | basket_weights,
1714 | )
1715 |
1716 | spread_data["prev_zscore"] = zscore
1717 | return None
1718 |
1719 | def rainforest_resin_orders(
1720 | self,
1721 | order_depth: OrderDepth,
1722 | fair_value: float,
1723 | width: float,
1724 | position: int,
1725 | position_limit: int
1726 | ) -> List[Order]:
1727 | orders: List[Order] = []
1728 | buy_order_volume = 0
1729 | sell_order_volume = 0
1730 |
1731 | # 1) pick levels to clear or penny
1732 | higher_asks = [
1733 | price for price in order_depth.sell_orders
1734 | if price > fair_value + 1
1735 | ]
1736 | lower_bids = [
1737 | price for price in order_depth.buy_orders
1738 | if price < fair_value - 1
1739 | ]
1740 | baaf = min(higher_asks) if higher_asks else fair_value + 2
1741 | bbbf = max(lower_bids) if lower_bids else fair_value - 2
1742 |
1743 | # 2) take any crosses
1744 | if order_depth.sell_orders:
1745 | best_ask = min(order_depth.sell_orders)
1746 | amt = -order_depth.sell_orders[best_ask]
1747 | if best_ask < fair_value:
1748 | q = min(amt, position_limit - position)
1749 | if q > 0:
1750 | orders.append(Order("RAINFOREST_RESIN", best_ask, q))
1751 | buy_order_volume += q
1752 |
1753 | if order_depth.buy_orders:
1754 | best_bid = max(order_depth.buy_orders)
1755 | amt = order_depth.buy_orders[best_bid]
1756 | if best_bid > fair_value:
1757 | q = min(amt, position_limit + position)
1758 | if q > 0:
1759 | orders.append(Order("RAINFOREST_RESIN", best_bid, -q))
1760 | sell_order_volume += q
1761 |
1762 | # 3) clear stale levels via clear_position_order
1763 | buy_order_volume, sell_order_volume = self.clear_position_order(
1764 | "RAINFOREST_RESIN",
1765 | fair_value,
1766 | 1,
1767 | orders,
1768 | order_depth,
1769 | position,
1770 | buy_order_volume,
1771 | sell_order_volume,
1772 | )
1773 |
1774 | # 4) repost maker quotes
1775 | buy_qty = position_limit - (position + buy_order_volume)
1776 | sell_qty = position_limit + (position - sell_order_volume)
1777 | if buy_qty > 0:
1778 | orders.append(Order("RAINFOREST_RESIN", bbbf + 1, buy_qty))
1779 | if sell_qty > 0:
1780 | orders.append(Order("RAINFOREST_RESIN", baaf - 1, -sell_qty))
1781 |
1782 | return orders
1783 |
1784 | def run(self, state: TradingState):
1785 | # 0. decode previous traderData
1786 | traderObject = {}
1787 | if state.traderData:
1788 | traderObject = jsonpickle.decode(state.traderData)
1789 |
1790 | result: Dict[str, List[Order]] = {}
1791 |
1792 | orders: Dict[str, List[Order]] = {}
1793 | orders["MAGNIFICENT_MACARONS"] = []
1794 | trade_made = "NONE"
1795 |
1796 | # Get market data
1797 | if "MAGNIFICENT_MACARONS" not in state.order_depths:
1798 | return {}, 0, ""
1799 |
1800 | order_depth = state.order_depths["MAGNIFICENT_MACARONS"]
1801 |
1802 | # Get market prices
1803 | normal_best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
1804 | normal_best_ask = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float('inf')
1805 |
1806 | # Get sunlight data
1807 | current_sunlight = state.observations.conversionObservations.get("MAGNIFICENT_MACARONS", None).sunlightIndex
1808 | self.sunlight_window.append(current_sunlight)
1809 | if len(self.sunlight_window) > self.window_size:
1810 | self.sunlight_window.pop(0)
1811 |
1812 | # Calculate current slope
1813 | current_slope = self.calculate_slope(self.sunlight_window)
1814 | print("current_slope", current_slope)
1815 |
1816 | # Determine conditions
1817 | above_threshold = current_sunlight > self.sunlight_threshold
1818 | current_trend = self.last_trend # Keep previous trend unless we see a real change
1819 |
1820 | if len(self.sunlight_window) >= 2:
1821 | if self.sunlight_window[-1] < self.sunlight_window[-2]:
1822 | current_trend = "DECREASING"
1823 | elif self.sunlight_window[-1] > self.sunlight_window[-2]:
1824 | current_trend = "INCREASING"
1825 | # If equal, keep previous trend
1826 |
1827 | # Only trade if we have a confirmed trend
1828 | if current_trend == "DECREASING" and not above_threshold and self.position < self.position_limit:
1829 | # Go max long when sunlight is decreasing while above threshold
1830 | if normal_best_ask != float('inf'):
1831 | buy_quantity = min(
1832 | self.position_limit - self.position, # Go to max position
1833 | abs(order_depth.sell_orders[normal_best_ask])
1834 | )
1835 |
1836 | if buy_quantity > 0:
1837 | self.position += buy_quantity
1838 | orders["MAGNIFICENT_MACARONS"].append(
1839 | Order("MAGNIFICENT_MACARONS", normal_best_ask, buy_quantity)
1840 | )
1841 |
1842 | elif current_trend == "INCREASING" and current_slope > self.slope_threshold and self.position > 0:
1843 | # Only close long position when sunlight is increasing with significant slope
1844 | if normal_best_bid != 0:
1845 | sell_quantity = min(
1846 | self.position, # Close entire position
1847 | abs(order_depth.buy_orders[normal_best_bid])
1848 | )
1849 |
1850 | if sell_quantity > 0:
1851 | self.position -= sell_quantity
1852 | orders["MAGNIFICENT_MACARONS"].append(
1853 | Order("MAGNIFICENT_MACARONS", normal_best_bid, -sell_quantity)
1854 | )
1855 |
1856 | self.last_trend = current_trend
1857 |
1858 | # 1. RAINFOREST_RESIN (Kai’s version)
1859 | if Product.RAINFOREST_RESIN in state.order_depths:
1860 | od = state.order_depths[Product.RAINFOREST_RESIN]
1861 | # width
1862 | if od.sell_orders and od.buy_orders:
1863 | best_ask = min(od.sell_orders)
1864 | best_bid = max(od.buy_orders)
1865 | width = (best_ask - best_bid) / 2
1866 | else:
1867 | width = 5
1868 | # fair
1869 | tot_v = sum(p * v for p, v in od.buy_orders.items()) \
1870 | + sum(p * abs(v) for p, v in od.sell_orders.items())
1871 | tot_vol = sum(od.buy_orders.values()) + sum(abs(v) for v in od.sell_orders.values())
1872 | fair = tot_v / tot_vol if tot_vol > 0 else 10000
1873 | pos = state.position.get(Product.RAINFOREST_RESIN, 0)
1874 | limit = self.LIMIT[Product.RAINFOREST_RESIN]
1875 | result[Product.RAINFOREST_RESIN] = self.rainforest_resin_orders(
1876 | od, fair, width, pos, limit
1877 | )
1878 | else:
1879 | logger.print("RAINFOREST_RESIN not found")
1880 |
1881 | # 2. KELP (Kai’s Trade.kelp)
1882 | if Product.KELP in state.order_depths:
1883 | kelp_status = Status(Product.KELP, state)
1884 | result[Product.KELP] = Trade.kelp(kelp_status)
1885 | else:
1886 | logger.print("KELP not found")
1887 |
1888 | # 3. SQUID_INK (Kai’s take_squid_orders)
1889 | if Product.SQUID_INK in state.order_depths:
1890 | self.adjust_desired_positions(state, Product.SQUID_INK)
1891 | asset = Product.SQUID_INK
1892 | position = 0
1893 | try:
1894 | position = state.position[asset]
1895 | except:
1896 | position = 0
1897 | target_position = int(self.desired_position_pct[Product.SQUID_INK] * self.LIMIT[asset])
1898 | position_difference = target_position - position
1899 | if True:#not target_position == 0:
1900 | order_depth = state.order_depths[asset]
1901 | # Get market prices
1902 | result[asset] = []
1903 | best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
1904 | best_ask = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float('inf')
1905 | if position_difference > 0:
1906 | # Need to buy
1907 | if best_ask != float('inf'):
1908 | ask_quantity = order_depth.sell_orders.get(best_ask, 0)
1909 | buy_quantity = min(
1910 | position_difference,
1911 | abs(ask_quantity)
1912 | )
1913 |
1914 | if buy_quantity > 0:
1915 | result[asset].append(
1916 | Order(asset, best_ask, buy_quantity)
1917 | )
1918 |
1919 | elif position_difference < 0:
1920 | # Need to sell
1921 | if best_bid != 0:
1922 | bid_quantity = order_depth.buy_orders.get(best_bid, 0)
1923 | sell_quantity = min(
1924 | abs(position_difference),
1925 | abs(bid_quantity)
1926 | )
1927 |
1928 | if sell_quantity > 0:
1929 | result[asset].append(
1930 | Order(asset, best_bid, -sell_quantity)
1931 | )
1932 | else:
1933 | pos = state.position.get(Product.SQUID_INK, 0)
1934 | fair, vwap = self.squid_ink_fair_value(
1935 | state.order_depths[Product.SQUID_INK], traderObject
1936 | )
1937 | orders, _, _ = self.take_squid_orders(
1938 | Product.SQUID_INK,
1939 | vwap + target_position,
1940 | state.order_depths[Product.SQUID_INK],
1941 | self.params[Product.SQUID_INK]["take_width"],
1942 | pos,
1943 | self.params[Product.SQUID_INK]["prevent_adverse"],
1944 | self.params[Product.SQUID_INK]["adverse_volume"],
1945 | state.order_depths.get(Product.KELP),
1946 | 26,
1947 | 0.3,
1948 | )
1949 | result[Product.SQUID_INK] = orders
1950 | else:
1951 | logger.print("SQUID_INK not found")
1952 |
1953 | # 4. Basket spreads (unchanged)
1954 | if Product.CROISSANTS in state.order_depths:
1955 | self.adjust_desired_positions(state, Product.CROISSANTS)
1956 | asset = Product.CROISSANTS
1957 | croissant_target_position = int(self.desired_position_pct[Product.CROISSANTS] * self.LIMIT[Product.CROISSANTS])
1958 | if croissant_target_position == 0:
1959 | for basket, weights in [
1960 | (Product.PICNIC_BASKET1, BASKET1_WEIGHTS),
1961 | (Product.PICNIC_BASKET2, BASKET2_WEIGHTS),
1962 | ]:
1963 | spread_orders = self.spread_orders(
1964 | order_depths=state.order_depths,
1965 | product=basket,
1966 | basket_weights=weights,
1967 | basket_position=state.position.get(basket, 0),
1968 | spread_data=traderObject.setdefault(basket, {"spread_history": [], "prev_zscore": 0}),
1969 | )
1970 | if spread_orders:
1971 | for prod, orders in spread_orders.items():
1972 | result.setdefault(prod, []).extend(orders)
1973 | else:
1974 | assets = [Product.PICNIC_BASKET1, Product.PICNIC_BASKET2, Product.CROISSANTS]
1975 | for asset in assets:
1976 | if asset not in state.order_depths:
1977 | continue
1978 | position = 0
1979 | try:
1980 | position = state.position[asset]
1981 | except:
1982 | position = 0
1983 | target_position = int(self.desired_position_pct[Product.CROISSANTS] * self.LIMIT[asset])
1984 | position_difference = target_position - position
1985 | if True:#not target_position == 0:
1986 | order_depth = state.order_depths[asset]
1987 | # Get market prices
1988 | result[asset] = []
1989 | best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
1990 | best_ask = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float('inf')
1991 | if position_difference > 0:
1992 | # Need to buy
1993 | if best_ask != float('inf'):
1994 | ask_quantity = order_depth.sell_orders.get(best_ask, 0)
1995 | buy_quantity = min(
1996 | position_difference,
1997 | abs(ask_quantity)
1998 | )
1999 |
2000 | if buy_quantity > 0:
2001 | result[asset].append(
2002 | Order(asset, best_ask, buy_quantity)
2003 | )
2004 |
2005 | elif position_difference < 0:
2006 | # Need to sell
2007 | if best_bid != 0:
2008 | bid_quantity = order_depth.buy_orders.get(best_bid, 0)
2009 | sell_quantity = min(
2010 | abs(position_difference),
2011 | abs(bid_quantity)
2012 | )
2013 |
2014 | if sell_quantity > 0:
2015 | result[asset].append(
2016 | Order(asset, best_bid, -sell_quantity)
2017 | )
2018 |
2019 | # 5. VOLCANIC_ROCK + vouchers
2020 | if Product.VOLCANIC_ROCK in state.order_depths:
2021 | # underlying
2022 | self.adjust_desired_positions(state, Product.VOLCANIC_ROCK)
2023 |
2024 | #result[Product.VOLCANIC_ROCK] = Trade.volcanic_rock_copy(state)
2025 | if True:
2026 | assets = [Product.VOLCANIC_ROCK, Product.VOLCANIC_ROCK_VOUCHER_9500]
2027 | for asset in assets:
2028 | if asset not in state.order_depths:
2029 | continue
2030 | v_order_depth = state.order_depths[Product.VOLCANIC_ROCK]
2031 | # Get market prices
2032 | v_best_bid = max(v_order_depth.buy_orders.keys()) if v_order_depth.buy_orders else 0
2033 | v_best_ask = min(v_order_depth.sell_orders.keys()) if v_order_depth.sell_orders else float('inf')
2034 | if asset == Product.VOLCANIC_ROCK_VOUCHER_9500 and v_best_ask < 10000:
2035 | continue
2036 | position = 0
2037 | try:
2038 | position = state.position[asset]
2039 | except:
2040 | position = 0
2041 | target_position = int(self.desired_position_pct[Product.VOLCANIC_ROCK] * self.LIMIT[asset])
2042 | position_difference = target_position - position
2043 | if True:#not target_position == 0:
2044 | result[asset] = []
2045 | order_depth = state.order_depths[asset]
2046 | # Get market prices
2047 | best_bid = max(order_depth.buy_orders.keys()) if order_depth.buy_orders else 0
2048 | best_ask = min(order_depth.sell_orders.keys()) if order_depth.sell_orders else float('inf')
2049 | if position_difference > 0:
2050 | # Need to buy
2051 | if best_ask != float('inf'):
2052 | ask_quantity = order_depth.sell_orders.get(best_ask, 0)
2053 | buy_quantity = min(
2054 | position_difference,
2055 | abs(ask_quantity)
2056 | )
2057 |
2058 | if buy_quantity > 0:
2059 | result[asset].append(
2060 | Order(asset, best_ask, buy_quantity)
2061 | )
2062 |
2063 | elif position_difference < 0:
2064 | # Need to sell
2065 | if best_bid != 0:
2066 | bid_quantity = order_depth.buy_orders.get(best_bid, 0)
2067 | sell_quantity = min(
2068 | abs(position_difference),
2069 | abs(bid_quantity)
2070 | )
2071 |
2072 | if sell_quantity > 0:
2073 | result[asset].append(
2074 | Order(asset, best_bid, -sell_quantity)
2075 | )
2076 | underlying = Status(Product.VOLCANIC_ROCK, state)
2077 |
2078 |
2079 |
2080 | # vouchers for 9750, 10000
2081 | for strike in (9750, 10000):
2082 | key = f"{Product.VOLCANIC_ROCK}_VOUCHER_{strike}"
2083 | if key in state.order_depths:
2084 | st = Status(key, state, strike)
2085 | result[key] = Trade.voucher(st, underlying, strike)
2086 | else:
2087 | logger.print("VOLCANIC_ROCK not found")
2088 |
2089 | # 6. Update profit history (for Kai’s trend logic)
2090 | for product, position in state.position.items():
2091 | if product in state.order_depths and position != 0:
2092 | # extract strike if voucher
2093 | strike = int(product.rsplit("_", 1)[-1]) if "VOUCHER" in product else None
2094 | ps = Status(product, state, strike)
2095 | profit = position * (ps.vwap - 10000) / 100
2096 | ps.update_profit_history(profit)
2097 |
2098 | # 7. Flush & return
2099 | traderData = jsonpickle.encode(traderObject)
2100 | conversions = 1
2101 | logger.flush(state, result, conversions, traderData)
2102 | return result, conversions, traderData
--------------------------------------------------------------------------------