From 2951811603643b7796d50e617dde42ab7261009f Mon Sep 17 00:00:00 2001 From: Matthew Date: Fri, 15 May 2026 13:47:00 +0800 Subject: [PATCH] Add epsilon-learning to bots & per-symbol snapshot queue to prevent order accumulation at last symbol processed at market close --- include/velox/core/symbol_engine.hpp | 7 + include/velox/sim/env/market_sim.hpp | 15 +- include/velox/sim/strategy/bot_manager.hpp | 12 +- include/velox/sim/strategy/bots.hpp | 159 ++++++++++++++++----- src/main.cpp | 149 ++++++++++--------- src/sim/env/market_sim.cpp | 12 +- src/sim/strategy/bot_manager.cpp | 36 ++++- third_party/whirlpool | 2 +- 8 files changed, 280 insertions(+), 112 deletions(-) diff --git a/include/velox/core/symbol_engine.hpp b/include/velox/core/symbol_engine.hpp index 2421399..a6bd645 100644 --- a/include/velox/core/symbol_engine.hpp +++ b/include/velox/core/symbol_engine.hpp @@ -50,6 +50,13 @@ class SymbolEngine { m_book.set_market_price(bid, ask); } + // Drain each engine + void drain() { + for (int i = 0; i < 100; ++i) { + m_engine.run_match_cycle(); + } + } + private: OrderBook m_book; MatchingEngine m_engine; diff --git a/include/velox/sim/env/market_sim.hpp b/include/velox/sim/env/market_sim.hpp index 11b3e80..8720793 100644 --- a/include/velox/sim/env/market_sim.hpp +++ b/include/velox/sim/env/market_sim.hpp @@ -50,13 +50,14 @@ class MarketSimulator { using PriceCallback = std::function; MarketSimulator(double tick_intervals_ms = 100.0, - int64_t initial_price = 10000, + const std::unordered_map& initial_prices = {}, int64_t volatility = 50, int64_t min_spread = 5, int64_t max_spread = 50, uint32_t resting_quantity = 500) : m_tick_interval(std::chrono::milliseconds(static_cast(tick_intervals_ms))), - m_initial_price(initial_price), + m_initial_prices(initial_prices), + m_default_initial_price(10000), m_volatility(volatility), m_min_spread(min_spread), m_max_spread(max_spread), @@ -76,9 +77,13 @@ class MarketSimulator { void update_resting_orders(SymbolEngine& engine, int64_t bid_price, int64_t ask_price); void clear_resting_orders(); + // Initialize initial price per share + int64_t get_initial_price(const std::string& symbol) const; + private: std::chrono::milliseconds m_tick_interval; - int64_t m_initial_price; + std::unordered_map m_initial_prices; + int64_t m_default_initial_price; int64_t m_volatility; int64_t m_min_spread; int64_t m_max_spread; @@ -104,9 +109,7 @@ class MarketSimulator { // Per-symbol resting orders std::unordered_map m_symbol_orders; - - - + // Track active orders std::unordered_map> m_active_orders; // Shared pool for simulator orders diff --git a/include/velox/sim/strategy/bot_manager.hpp b/include/velox/sim/strategy/bot_manager.hpp index 0ab9608..fa7f1ed 100644 --- a/include/velox/sim/strategy/bot_manager.hpp +++ b/include/velox/sim/strategy/bot_manager.hpp @@ -12,6 +12,7 @@ namespace bot { class BotManager { public: using OrderQueue = lockfree::SPSCQueue; + using CancelQueue = lockfree::SPSCQueue; BotManager(); ~BotManager(); @@ -22,9 +23,13 @@ class BotManager { // Called by snapshot thread to distribute snapshots to bots void on_snapshot(const BookSnapshot& snapshot); - // Called by matching engine thread to consume orders - bool pop_order(Order& order); + // Called by matching engine thread to consume orders (per-symbol) + bool pop_order_for_symbol(const std::string& symbol, Order& order); void push_order(const Order& order); + + // Cancel queue (orders cancelled by bots) + void push_cancel(uint64_t order_id); + bool pop_cancel(uint64_t& order_id); // Statistics size_t bot_count() const { return m_bots.size(); } @@ -34,6 +39,9 @@ class BotManager { std::vector> m_bots; std::unordered_map> m_bots_by_symbol; OrderQueue m_order_queue; + CancelQueue m_cancel_queue; + // Per-symbol order queues (single snapshot thread will both produce and consume for that symbol) + std::unordered_map> m_order_queues; }; } diff --git a/include/velox/sim/strategy/bots.hpp b/include/velox/sim/strategy/bots.hpp index a66fab7..d657df0 100644 --- a/include/velox/sim/strategy/bots.hpp +++ b/include/velox/sim/strategy/bots.hpp @@ -3,6 +3,9 @@ #include "velox/book/book_snapshot.hpp" #include +/* +* NOTE: Each bot can only have 3 active orders at once to prevent flooding +*/ namespace velox { namespace bot { @@ -12,7 +15,7 @@ class BotManager; class TradingBot { public: TradingBot(const std::string& name, const std::string& symbol) - : m_name(name), m_symbol(symbol) {} + : m_name(name), m_symbol(symbol), m_gen(m_rd()), m_dist(0.0, 1.0) {} virtual ~TradingBot() = default; @@ -28,6 +31,14 @@ class TradingBot { void submit_order(const Order& order); +protected: + // RNG for stochastic behavior + std::random_device m_rd; + std::mt19937 m_gen; + std::uniform_real_distribution<> m_dist; + + double rand01() { return m_dist(m_gen); } + std::string m_symbol; private: std::string m_name; @@ -124,6 +135,12 @@ class SpreadBot : public TradingBot { , m_quantity(quantity) {} void on_snapshot(const BookSnapshot& snapshot) override { + // Expire tracked open orders TTL + for (auto it = m_open_order_ttls.begin(); it != m_open_order_ttls.end();) { + if (--(*it) <= 0) it = m_open_order_ttls.erase(it); + else ++it; + } + if (m_last_spread == 0) { m_last_spread = snapshot.spread; return; @@ -132,7 +149,7 @@ class SpreadBot : public TradingBot { int64_t spread_change = snapshot.spread - m_last_spread; if (spread_change > m_threshold) { - // Spread widened → buy + // Spread widened -> buy Order order; order.order_id = ++m_order_id; order.side = OrderSide::BUY; @@ -141,10 +158,13 @@ class SpreadBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } else if (spread_change < -m_threshold) { - // Spread narrowed → sell + // Spread narrowed -> sell Order order; order.order_id = ++m_order_id; order.side = OrderSide::SELL; @@ -153,7 +173,10 @@ class SpreadBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } m_last_spread = snapshot.spread; @@ -164,6 +187,11 @@ class SpreadBot : public TradingBot { uint32_t m_quantity; int64_t m_last_spread = 0; uint64_t m_order_id = 10000; + + // Safety limits + size_t m_max_open_orders = 3; + int m_order_ttl = 1000; // Snapshots until considered expired + std::vector m_open_order_ttls; }; // Random Walk Bot: random buy/sell decisions @@ -179,33 +207,59 @@ class RandomWalkBot : public TradingBot { , m_dist(0.0, 1.0) {} void on_snapshot(const BookSnapshot& snapshot) override { - double r = m_dist(m_gen); - + // Decrement TTLs and remove expired + for (auto it = m_open_order_ttls.begin(); it != m_open_order_ttls.end();) { + if (--(*it) <= 0) it = m_open_order_ttls.erase(it); + else ++it; + } + + double r = rand01(); + + // Normal limit orders, obey per-bot open-order limit if (r < m_buy_prob) { - Order order; - order.order_id = ++m_order_id; - order.side = OrderSide::BUY; - order.price = snapshot.best_ask; - order.quantity = m_quantity; - order.remaining_quantity = m_quantity; - order.type = OrderType::LIMIT; - std::strncpy(order.symbol, snapshot.symbol, 7); - /* - std::cout << "[RandomWalkBot " << name() << "] SUBMITTING BUY order at " - << order.price << std::endl; */ - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + Order order; + order.order_id = ++m_order_id; + order.side = OrderSide::BUY; + order.price = snapshot.best_ask; + order.quantity = m_quantity; + order.remaining_quantity = m_quantity; + order.type = OrderType::LIMIT; + std::strncpy(order.symbol, snapshot.symbol, 7); + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } else if (r < m_buy_prob + m_sell_prob) { - Order order; - order.order_id = ++m_order_id; - order.side = OrderSide::SELL; - order.price = snapshot.best_bid; - order.quantity = m_quantity; - order.remaining_quantity = m_quantity; - order.type = OrderType::LIMIT; - std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + Order order; + order.order_id = ++m_order_id; + order.side = OrderSide::SELL; + order.price = snapshot.best_bid; + order.quantity = m_quantity; + order.remaining_quantity = m_quantity; + order.type = OrderType::LIMIT; + std::strncpy(order.symbol, snapshot.symbol, 7); + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } + } + + // Rare spike: large market order with cooldown + else if (rand01() < m_spike_prob && m_spike_cooldown == 0 && m_open_order_ttls.empty()) { + Order spike; + spike.order_id = ++m_order_id; + spike.side = (rand01() < 0.5) ? OrderSide::BUY : OrderSide::SELL; + spike.type = OrderType::MARKET; + spike.quantity = m_quantity * (3 + (m_gen() % 5)); + spike.remaining_quantity = spike.quantity; + std::strncpy(spike.symbol, snapshot.symbol, 7); + submit_order(spike); + m_open_order_ttls.push_back(m_spike_ttl); + m_spike_cooldown = m_spike_cooldown_default; } + + if (m_spike_cooldown > 0) --m_spike_cooldown; } private: @@ -216,6 +270,15 @@ class RandomWalkBot : public TradingBot { std::mt19937 m_gen; std::uniform_real_distribution<> m_dist; uint64_t m_order_id = 20000; + // Safety controls + size_t m_max_open_orders = 3; + int m_order_ttl = 1000; // snapshots until considered expired + std::vector m_open_order_ttls; + // Spike controls + double m_spike_prob = 0.01; + int m_spike_cooldown = 0; + int m_spike_cooldown_default = 2000; + int m_spike_ttl = 1500; }; // Mean Reversion Bot: trades when price deviates from mean @@ -229,6 +292,12 @@ class MeanReversionBot : public TradingBot { , m_quantity(quantity) {} void on_snapshot(const BookSnapshot& snapshot) override { + // expire tracked open orders TTL + for (auto it = m_open_order_ttls.begin(); it != m_open_order_ttls.end();) { + if (--(*it) <= 0) it = m_open_order_ttls.erase(it); + else ++it; + } + // Record price m_prices.push_back(snapshot.mid_price); if (m_prices.size() > m_lookback) { @@ -254,7 +323,10 @@ class MeanReversionBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } else if (z_score < -m_z_threshold) { // Price too low → buy @@ -266,7 +338,10 @@ class MeanReversionBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } } @@ -291,6 +366,10 @@ class MeanReversionBot : public TradingBot { uint32_t m_quantity; std::vector m_prices; uint64_t m_order_id = 30000; + // Safety controls + size_t m_max_open_orders = 3; + int m_order_ttl = 1000; + std::vector m_open_order_ttls; }; // Momentum Bot: Trades based on price momentum @@ -303,6 +382,12 @@ class MomentumBot : public TradingBot { , m_quantity(quantity) {} void on_snapshot(const BookSnapshot& snapshot) override { + // Expire TTLs for tracked open orders + for (auto it = m_open_order_ttls.begin(); it != m_open_order_ttls.end();) { + if (--(*it) <= 0) it = m_open_order_ttls.erase(it); + else ++it; + } + if (m_prev_price == 0) { m_prev_price = snapshot.mid_price; return; @@ -321,7 +406,10 @@ class MomentumBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } else if (change < -m_momentum_threshold) { // Downward momentum → sell @@ -333,7 +421,10 @@ class MomentumBot : public TradingBot { order.remaining_quantity = m_quantity; order.type = OrderType::LIMIT; std::strncpy(order.symbol, snapshot.symbol, 7); - submit_order(order); + if (m_open_order_ttls.size() < m_max_open_orders) { + submit_order(order); + m_open_order_ttls.push_back(m_order_ttl); + } } } @@ -342,6 +433,10 @@ class MomentumBot : public TradingBot { uint32_t m_quantity; int64_t m_prev_price = 0; uint64_t m_order_id = 40000; + // Safety controls + size_t m_max_open_orders = 3; + int m_order_ttl = 1000; + std::vector m_open_order_ttls; }; } diff --git a/src/main.cpp b/src/main.cpp index 11ad8e2..9e784b0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -36,9 +36,6 @@ int main(int argc, char* argv[]) { auto bot_manager = std::make_unique(); - // Initialize simulated market environment - env::MarketSimulator simulator(100.0, 10000, 50, 5, 50, 500); - // Gateway workers const int num_workers = std::thread::hardware_concurrency(); for (int i = 0; i < num_workers; ++i) { @@ -48,6 +45,18 @@ int main(int argc, char* argv[]) { // Symbols std::vector symbols = {"AAPL", "MSFT", "GOOGL", "AMZN", "META"}; + // Price per share for each symbol + std::unordered_map initial_prices = { + {"AAPL", 17500}, // $175.00 + {"MSFT", 33000}, // $330.00 + {"GOOGL", 12500}, // $125.00 + {"AMZN", 13500}, // $135.00 + {"META", 30000} // $300.00 + }; + + // Initialize simulated market environment + env::MarketSimulator simulator(100.0, initial_prices, 50, 5, 50, 500); + std::vector> engines; for (const auto& s : symbols) { engines.push_back(std::make_unique( @@ -102,79 +111,81 @@ int main(int argc, char* argv[]) { }); } - // Snapshot + bot loop - std::thread snapshot_thread([&]() { - std::unordered_map> snaps; - for (auto& e : engines) { - snaps[e->symbol()] = std::make_unique(5); - } - - static lockfree::ObjectPool bot_pool; - static std::vector> bot_storage; + // Snapshot threads: one per symbol, each with its own pool and storage + std::vector snapshot_threads; + for (auto& e : engines) { + snapshot_threads.emplace_back([&, symbol = std::string(e->symbol())]() { + BookSnapshotManager sm(5); + lockfree::ObjectPool bot_pool; + std::vector> bot_storage; - while (g_running) { - for (auto& e : engines) { - auto& sm = *snaps[e->symbol()]; - sm.update(e->order_book()); + while (g_running) { + // Update snapshot for this engine only + auto eng_ptr = std::find_if(engines.begin(), engines.end(), [&](const std::unique_ptr& pe){ return symbol == pe->symbol(); }); + if (eng_ptr == engines.end()) break; + auto& eng = *eng_ptr->get(); + sm.update(eng.order_book()); const auto* snap = sm.get_snapshot(); if (snap && snap->valid()) { bot_manager->on_snapshot(*snap); sm.release_snapshot(snap); } - // Process bot orders + // Process bot orders for this symbol only Order tmp; - while (bot_manager->pop_order(tmp)) { + while (bot_manager->pop_order_for_symbol(symbol, tmp)) { auto ptr = bot_pool.acquire(); + if (!ptr) { + // Pool exhausted: drop order and continue + continue; + } *ptr = tmp; ptr->remaining_quantity = ptr->quantity; ptr->filled_quantity = 0; ptr->status = OrderStatus::NEW; - for (auto& eng : engines) { - if (strcmp(eng->symbol(), ptr->symbol) == 0) { - eng->on_market_order(ptr.get()); - bot_storage.push_back(std::move(ptr)); - break; - } - } + eng.on_market_order(ptr.get()); + bot_storage.push_back(std::move(ptr)); } - /* Periodically drain bot_storage of orders already processed (O(n)) - // Sweep bot storage array - auto it = bot_storage.begin(); - - while (it != bot_storage.end()) { - if ((*it)->status != OrderStatus::NEW) { - it = bot_storage.erase(it); - // std::cout << "[CLEAN] bot_storage slot returned to pool" << std::endl; - } else { - ++it; + // Sweep finished orders + std::vector> next_storage; + next_storage.reserve(bot_storage.size()); + for (auto& ptr : bot_storage) { + if (ptr->status == OrderStatus::NEW || ptr->status == OrderStatus::PARTIAL) { + next_storage.push_back(std::move(ptr)); } } - */ + bot_storage = std::move(next_storage); + + if (!g_running) break; + std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - // Swap processed orders to separate list & clear (faster than O(n)) - std::vector> next_storage; - next_storage.reserve(bot_storage.size()); + std::cout << "Exiting snapshot thread for " << symbol << std::endl; + }); + } - for (auto& ptr : bot_storage) { - if (ptr->status == OrderStatus::NEW || ptr->status == OrderStatus::PARTIAL) { - next_storage.push_back(std::move(ptr)); + // Cancel processing thread: centralizes cancels and routes to engines + std::thread cancel_thread([&]() { + while (g_running) { + uint64_t cancel_id; + while (bot_manager->pop_cancel(cancel_id)) { + for (auto& eng : engines) { + if (eng->cancel_order(cancel_id)) break; } - // Finished orders: ptr destructs, slot returned to pool } - - // Swap back - bot_storage = std::move(next_storage); - - if (!g_running) break; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); } - std::cout << "Exiting snapshot thread" << std::endl; + // Drain remaining cancels at shutdown + uint64_t cancel_id; + while (bot_manager->pop_cancel(cancel_id)) { + for (auto& eng : engines) { + if (eng->cancel_order(cancel_id)) break; + } + } + std::cout << "Exiting cancel thread" << std::endl; }); // Start simulator @@ -196,20 +207,34 @@ int main(int argc, char* argv[]) { g_running = false; - // Final drain - for (auto& e : engines) { - for (int i = 0; i < 10; ++i) { - e->run_match_cycle(); - } - } - + std::cout << "[SHUTDOWN] calling simulator.stop()" << std::endl; simulator.stop(); + std::cout << "[SHUTDOWN] simulator.stop() returned" << std::endl; + + // Join snapshot threads + std::cout << "[SHUTDOWN] joining snapshot threads" << std::endl; + for (auto& t : snapshot_threads) { + if (t.joinable()) t.join(); + } + std::cout << "[SHUTDOWN] snapshot threads joined" << std::endl; - if (snapshot_thread.joinable()) { - snapshot_thread.join(); + // Join cancel thread + if (cancel_thread.joinable()) { + std::cout << "[SHUTDOWN] joining cancel_thread" << std::endl; + cancel_thread.join(); + std::cout << "[SHUTDOWN] cancel_thread joined" << std::endl; } - for (auto& t : workers) t.join(); + std::cout << "[SHUTDOWN] joining " << workers.size() << " worker threads" << std::endl; + for (auto& t : workers) { + t.join(); + } + std::cout << "[SHUTDOWN] worker threads joined" << std::endl; + + // Final drain + for (auto& e : engines) { + e->drain(); + } std::cout << "\n=== FINAL STATS ===\n"; for (auto& e : engines) { @@ -218,10 +243,6 @@ int main(int argc, char* argv[]) { << " matched=" << s.orders_matched << " rejected=" << s.orders_rejected << " partial=" << s.orders_partially_filled - << " bid=" << e->order_book().best_bid() - << " ask=" << e->order_book().best_ask() - << " bid_depth="<< e->order_book().bid_depth() - << " ask_depth="<< e->order_book().ask_depth() << "\n"; } diff --git a/src/sim/env/market_sim.cpp b/src/sim/env/market_sim.cpp index 0669bbd..fd6e3a1 100644 --- a/src/sim/env/market_sim.cpp +++ b/src/sim/env/market_sim.cpp @@ -13,8 +13,11 @@ void MarketSimulator::start(std::vector>& engines, // Initialize resting orders for each symbol before starting simulation for (auto& e : engines) { - current_prices[e->symbol()] = m_initial_price; - initialize_resting_orders(*e, m_initial_price); + int64_t initial_price = get_initial_price(e->symbol()); + current_prices[e->symbol()] = initial_price; + initialize_resting_orders(*e, initial_price); + + std::cout << "[SIM] " << e->symbol() << " initial price = " << initial_price << std::endl; } std::normal_distribution delta_dist(0.0, m_volatility / 3.0); @@ -178,5 +181,10 @@ void MarketSimulator::clear_resting_orders() { m_symbol_orders.clear(); } +int64_t MarketSimulator::get_initial_price(const std::string& symbol) const { + auto it = m_initial_prices.find(symbol); + return (it != m_initial_prices.end()) ? it->second : m_default_initial_price; +} + } } \ No newline at end of file diff --git a/src/sim/strategy/bot_manager.cpp b/src/sim/strategy/bot_manager.cpp index a33b9a5..3d62cf1 100644 --- a/src/sim/strategy/bot_manager.cpp +++ b/src/sim/strategy/bot_manager.cpp @@ -31,7 +31,7 @@ void BotManager::on_snapshot(const BookSnapshot& snapshot) { return; } - //std::cout << "[BotManager] Found " << it->second.size() << " bots for " << snapshot.symbol << std::endl; + // std::cout << "[BotManager] Found " << it->second.size() << " bots for " << snapshot.symbol << std::endl; for (auto* bot : it->second) { bot->on_snapshot(snapshot); @@ -39,12 +39,36 @@ void BotManager::on_snapshot(const BookSnapshot& snapshot) { } void BotManager::push_order(const Order& order) { - m_order_queue.push(order); + auto it = m_order_queues.find(std::string(order.symbol)); + if (it == m_order_queues.end()) { + // Lazily create per-symbol queue + auto q = std::make_unique(); + auto res = q.get(); + m_order_queues.emplace(std::string(order.symbol), std::move(q)); + m_order_queues[std::string(order.symbol)]->push(order); + return; + } + it->second->push(order); +} + +void BotManager::push_cancel(uint64_t order_id) { + m_cancel_queue.push(order_id); +} + +bool BotManager::pop_cancel(uint64_t& order_id) { + auto opt = m_cancel_queue.pop(); + if (opt.has_value()) { + order_id = opt.value(); + return true; + } + return false; } -bool BotManager::pop_order(Order& order) { - auto opt = m_order_queue.pop(); +bool BotManager::pop_order_for_symbol(const std::string& symbol, Order& order) { + auto it = m_order_queues.find(symbol); + if (it == m_order_queues.end()) return false; + auto opt = it->second->pop(); if (opt.has_value()) { order = std::move(opt.value()); return true; @@ -53,7 +77,9 @@ bool BotManager::pop_order(Order& order) { } uint64_t BotManager::orders_queued() const { - return m_order_queue.size(); + uint64_t sum = 0; + for (const auto& p : m_order_queues) sum += p.second->size(); + return sum + m_cancel_queue.size(); } } diff --git a/third_party/whirlpool b/third_party/whirlpool index 36e658e..9d5e0db 160000 --- a/third_party/whirlpool +++ b/third_party/whirlpool @@ -1 +1 @@ -Subproject commit 36e658edb75ad707b6046949745cd39d0833e4d6 +Subproject commit 9d5e0db9e14cbbd506ad44feb0086e5f474e3862