├── 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 | | [James Cole](https://www.linkedin.com/in/jamescole05/) | [Tomás Barbosa](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 | Screenshot 2025-04-25 at 22 39 10 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 | Screenshot 2025-04-25 at 22 37 54 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 | Screenshot 2025-04-25 at 22 35 22 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 | ![image](https://github.com/user-attachments/assets/f552b4b2-26f6-4f0c-a0cc-03ba11dc96d7) 201 | 202 | Screenshot 2025-04-25 at 22 40 24 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 --------------------------------------------------------------------------------