import time import logging from decimal import Decimal from typing import Dict, List, Any, Optional from hexbytes import HexBytes logger = logging.getLogger("BACKTEST_MOCK") class MockExchangeState: """ Central source of truth for the simulation. Acts as the "Blockchain" and the "CEX Engine". """ def __init__(self): self.current_time_ms = 0 self.prices = {} # symbol -> price (Decimal) self.ticks = {} # symbol -> current tick (int) # Balances self.wallet_balances = { "NATIVE": Decimal("100.0"), # ETH or BNB "ETH": Decimal("100.0"), "USDC": Decimal("100000.0"), "WETH": Decimal("100.0"), "BNB": Decimal("100.0"), "WBNB": Decimal("100.0"), "USDT": Decimal("100000.0") } self.hl_balances = {"USDC": Decimal("10000.0"), "USDT": Decimal("10000.0")} self.hl_positions = {} # symbol -> {size, entry_px, unrealized_pnl} # Uniswap Positions self.next_token_id = 1000 self.uni_positions = {} # token_id -> {liquidity, tickLower, tickUpper, ...} # Hyperliquid Orders self.hl_orders = [] # List of {oid, coin, side, limitPx, sz, timestamp} self.next_oid = 1 # Fees/PnL Tracking self.uni_fees_collected = Decimal("0.0") self.hl_fees_paid = Decimal("0.0") self.hl_realized_pnl = Decimal("0.0") # Pending TXs (Simulating Mempool/Execution) self.pending_txs = [] def update_price(self, symbol: str, price: Decimal, tick: int = 0): self.prices[symbol] = price if tick: self.ticks[symbol] = tick # Update PnL for open positions if symbol in self.hl_positions: pos = self.hl_positions[symbol] size = pos['size'] if size != 0: # Long: (Price - Entry) * Size # Short: (Entry - Price) * abs(Size) if size > 0: pos['unrealized_pnl'] = (price - pos['entry_px']) * size else: pos['unrealized_pnl'] = (pos['entry_px'] - price) * abs(size) def process_trade(self, trade_data): """Simulate fee accumulation from market trades.""" # trade_data: {price, size, ...} from CSV try: # DEBUG: Confirm entry if getattr(self, '_debug_trade_entry', False) is False: logger.info(f"DEBUG: Processing Trades... Positions: {len(self.uni_positions)}") self._debug_trade_entry = True price = Decimal(trade_data['price']) size = Decimal(trade_data['size']) # Amount in Base Token (BNB) # Simple Fee Logic: # If trade price is within a position's range, it earns fees. # Fee = Volume * 0.05% (Fee Tier) * MarketShare (Assume 10%) fee_tier = Decimal("0.0005") # 0.05% # Realistic Market Share Simulation # Assume Pool Depth in active ticks is $5,000,000 # Our Position is approx $1,000 # Share = 1,000 / 5,000,000 = 0.0002 (0.02%) market_share = Decimal("0.0002") import math # Current Tick of the trade try: # price = 1.0001^tick -> tick = log(price) / log(1.0001) # Note: If T0=USDT, Price T0/T1 = 1/Price_USD. # But our TickLower/Upper in Mock are generated based on T0=USDT logic? # clp_manager calculates ticks based on price_from_tick logic. # If T0=USDT, T1=WBNB. Price (T0/T1) ~ 0.00116. # Ticks will be negative. # Trade Price is 860. # We need to invert price to get tick if the pool is inverted. # For BNB tests, we know T0=USDT. # Invert price for tick calc inv_price = Decimal("1") / price tick = int(math.log(float(inv_price)) / math.log(1.0001)) except: tick = 0 # Iterate all OPEN Uniswap positions for token_id, pos in self.uni_positions.items(): # Check Range if pos['tickLower'] <= tick <= pos['tickUpper']: vol_usd = price * size fee_earned = vol_usd * fee_tier * market_share pos['tokensOwed0'] = pos.get('tokensOwed0', 0) + int(fee_earned * 10**18) # Debug logging (Disabled for production runs) # if getattr(self, '_debug_fee_log_count', 0) < 10: # logger.info(f"DEBUG: Fee Earned! Tick {tick} inside {pos['tickLower']} <-> {pos['tickUpper']}. Fee: {fee_earned}") # self._debug_fee_log_count = getattr(self, '_debug_fee_log_count', 0) + 1 else: # Debug logging (Disabled) # if getattr(self, '_debug_tick_log_count', 0) < 10: # logger.info(f"DEBUG: Trade Tick {tick} OUTSIDE {pos['tickLower']} <-> {pos['tickUpper']} (Price: {price})") # self._debug_tick_log_count = getattr(self, '_debug_tick_log_count', 0) + 1 pass except Exception as e: logger.error(f"Error processing trade: {e}") def process_transaction(self, tx_data): """Executes a pending transaction and updates state.""" func = tx_data['func'] args = tx_data['args'] value = tx_data.get('value', 0) contract_addr = tx_data['contract'] logger.info(f"PROCESSING TX: {func} on {contract_addr} Val: {value}") # 1. DEPOSIT (Wrap) if func == "deposit": # Wrap Native -> Wrapped # Assume contract_addr is the wrapped token # In mocks, we map address to symbol # But we don't have the instance here easily, so we guess. # If value > 0, it's a wrap. amount = Decimal(value) / Decimal(10**18) if self.wallet_balances.get("NATIVE", 0) >= amount: self.wallet_balances["NATIVE"] -= amount # Find which token this is. # If it's the WETH/WBNB address target_token = "WBNB" # Default assumption for this test profile if contract_addr == "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": target_token = "WETH" self.wallet_balances[target_token] = self.wallet_balances.get(target_token, 0) + amount logger.info(f"Wrapped {amount} NATIVE to {target_token}") else: logger.error("Insufficient NATIVE balance for wrap") # 2. MINT (New Position) elif func == "mint": # Params: (token0, token1, fee, tickLower, tickUpper, amount0Desired, amount1Desired, ...) params = args[0] token0 = params[0] token1 = params[1] amount0 = Decimal(params[5]) / Decimal(10**18) # Approx decimals amount1 = Decimal(params[6]) / Decimal(10**18) logger.info(f"Minting Position: {amount0} T0, {amount1} T1") # Deduct Balances (Simplified: assuming we have enough) # In real mock we should check symbols self.wallet_balances["WBNB"] = max(0, self.wallet_balances.get("WBNB", 0) - amount0) self.wallet_balances["USDT"] = max(0, self.wallet_balances.get("USDT", 0) - amount1) # Create Position token_id = self.next_token_id self.next_token_id += 1 self.uni_positions[token_id] = { "token0": token0, "token1": token1, "tickLower": params[3], "tickUpper": params[4], "liquidity": 1000000 # Dummy liquidity } logger.info(f"Minted TokenID: {token_id}") self.last_minted_token_id = token_id return token_id # Helper return # 3. COLLECT (Fees) elif func == "collect": # Params: (params) -> (tokenId, recipient, amount0Max, amount1Max) params = args[0] token_id = params[0] # Retrieve accumulated fees if token_id in self.uni_positions: pos = self.uni_positions[token_id] owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18) owed0 = Decimal(pos.get('tokensOwed0', 0)) / Decimal(10**18) # Reset pos['tokensOwed1'] = 0 pos['tokensOwed0'] = 0 fee0 = owed0 fee1 = owed1 else: fee0 = Decimal("0") fee1 = Decimal("0") self.wallet_balances["WBNB"] = self.wallet_balances.get("WBNB", 0) + fee0 self.wallet_balances["USDT"] = self.wallet_balances.get("USDT", 0) + fee1 # Calculate USD Value of fees # T0 = USDT, T1 = WBNB # fee0 is USDT, fee1 is WBNB price = self.state.prices.get("BNB", Decimal("0")) usd_val = fee0 + (fee1 * price) self.uni_fees_collected += usd_val logger.info(f"Collected Fees for {token_id}: {fee0:.4f} T0 + {fee1:.4f} T1 = ${usd_val:.2f}") # 4. SWAP (ExactInputSingle) elif func == "exactInputSingle": # Params: (params) -> struct # struct ExactInputSingleParams { # address tokenIn; address tokenOut; fee; recipient; deadline; amountIn; amountOutMinimum; sqrtPriceLimitX96; # } # Since args[0] is the struct (tuple/list) # We need to guess indices or check ABI. # Standard: tokenIn(0), tokenOut(1), fee(2), recipient(3), deadline(4), amountIn(5), minOut(6) params = args[0] token_in_addr = params[0] token_out_addr = params[1] amount_in_wei = params[5] # Map address to symbol # We can't access contract instance here easily, so use known map or iterate sym_map = { "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC", "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB", "0x55d398326f99059fF775485246999027B3197955": "USDT" } sym_in = sym_map.get(token_in_addr, "UNKNOWN") sym_out = sym_map.get(token_out_addr, "UNKNOWN") amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Approx if sym_in in ["USDC", "USDT"]: amount_in = Decimal(amount_in_wei) / Decimal(10**18) # Mock usually 18 dec for simplicity unless specified # Price calculation # If swapping Base (WBNB) -> Quote (USDT), Price is ~300 # If Quote -> Base, Price is 1/300 price = self.prices.get("BNB", Decimal("300")) amount_out = 0 if sym_in == "WBNB" and sym_out == "USDT": amount_out = amount_in * price elif sym_in == "USDT" and sym_out == "WBNB": amount_out = amount_in / price else: amount_out = amount_in # 1:1 fallback self.wallet_balances[sym_in] = max(0, self.wallet_balances.get(sym_in, 0) - amount_in) self.wallet_balances[sym_out] = self.wallet_balances.get(sym_out, 0) + amount_out logger.info(f"SWAP: {amount_in:.4f} {sym_in} -> {amount_out:.4f} {sym_out}") def match_orders(self): """Simple order matching against current price.""" # In a real backtest, we'd check High/Low of the candle or Orderbook depth. # Here we assume perfect liquidity at current price for simplicity, # or implement simple slippage. pass # --- WEB3 MOCKS --- class MockContractFunction: def __init__(self, name, parent_contract, state: MockExchangeState): self.name = name self.contract = parent_contract self.state = state self.args = [] def __call__(self, *args, **kwargs): self.args = args return self def call(self, transaction=None): # SIMULATE READS if self.name == "slot0": # Determine Pair symbol = "BNB" if "BNB" in self.state.prices else "ETH" price = self.state.prices.get(symbol, Decimal("300")) # For BNB Chain, T0 is USDT (0x55d), T1 is WBNB (0xbb4) # Price of T0 (USDT) in T1 (WBNB) is 1 / Price if symbol == "BNB": if price > 0: price = Decimal("1") / price else: price = Decimal("0") sqrt_px = price.sqrt() * (2**96) # Tick import math try: # price = 1.0001^tick tick = int(math.log(float(price)) / math.log(1.0001)) except: tick = 0 return (int(sqrt_px), tick, 0, 0, 0, 0, True) if self.name == "positions": token_id = self.args[0] if token_id in self.state.uni_positions: p = self.state.uni_positions[token_id] return (0, "", p['token0'], p['token1'], 500, p['tickLower'], p['tickUpper'], p['liquidity'], 0, 0, p.get('tokensOwed0', 0), p.get('tokensOwed1', 0)) else: raise Exception("Position not found") if self.name == "balanceOf": addr = self.args[0] # Hacky: detect token by contract address symbol = self.contract.symbol_map.get(self.contract.address, "UNKNOWN") return int(self.state.wallet_balances.get(symbol, 0) * (10**self.contract.decimals_val)) if self.name == "decimals": return self.contract.decimals_val if self.name == "symbol": return self.contract.symbol_val if self.name == "allowance": return 10**50 # Infinite allowance # Pool Methods if self.name == "tickSpacing": return 10 if self.name == "token0": # Return USDT for BNB profile (0x55d < 0xbb4) if "BNB" in self.state.prices: return "0x55d398326f99059fF775485246999027B3197955" return "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH if self.name == "token1": # Return WBNB for BNB profile if "BNB" in self.state.prices: return "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c" return "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC if self.name == "fee": return 500 if self.name == "liquidity": return 1000000000000000000 return None def build_transaction(self, tx_params): # Queue Transaction for Execution self.state.pending_txs.append({ "func": self.name, "args": self.args, "contract": self.contract.address, "value": tx_params.get("value", 0) }) return {"data": "0xMOCK", "to": self.contract.address, "value": tx_params.get("value", 0)} def estimate_gas(self, tx_params): return 100000 class MockContract: def __init__(self, address, abi, state: MockExchangeState): self.address = address self.abi = abi self.state = state self.functions = self # Meta for simulation self.symbol_map = { "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1": "WETH", "0xaf88d065e77c8cC2239327C5EDb3A432268e5831": "USDC", # BNB Chain "0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c": "WBNB", # FIXED: Was BNB, but address is WBNB "0x55d398326f99059fF775485246999027B3197955": "USDT" } symbol = self.symbol_map.get(address, "MOCK") is_stable = symbol in ["USDC", "USDT"] self.decimals_val = 18 if not is_stable else 6 if symbol == "USDT": self.decimals_val = 18 # BNB USDT is 18 self.symbol_val = symbol def __getattr__(self, name): return MockContractFunction(name, self, self.state) class MockEth: def __init__(self, state: MockExchangeState): self.state = state self.chain_id = 42161 self.max_priority_fee = 100000000 def contract(self, address, abi): return MockContract(address, abi, self.state) def get_block(self, block_identifier): return {'baseFeePerGas': 100000000, 'timestamp': self.state.current_time_ms // 1000} def get_balance(self, address): # Native balance return int(self.state.wallet_balances.get("NATIVE", 0) * 10**18) def get_transaction_count(self, account, block_identifier=None): return 1 def send_raw_transaction(self, raw_tx): # EXECUTE PENDING TX if self.state.pending_txs: tx_data = self.state.pending_txs.pop(0) res = self.state.process_transaction(tx_data) return b'\x00' * 32 def wait_for_transaction_receipt(self, tx_hash, timeout=120): # MOCK LOGS GENERATION # We assume every tx is a successful Mint for now to test the flow # In a real engine we'd inspect the tx data to determine the event # Transfer Topic: 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef transfer_topic = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" # IncreaseLiquidity Topic: 0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde increase_topic = "0x7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde" # Use the actual minted ID if available, else dummy real_token_id = getattr(self.state, 'last_minted_token_id', 123456) token_id_hex = hex(real_token_id)[2:].zfill(64) # Liquidity + Amount0 + Amount1 # 1000 Liquidity, 1 ETH (18 dec), 3000 USDC (6 dec) # 1 ETH = 1e18, 3000 USDC = 3e9 data_liq = "00000000000000000000000000000000000000000000000000000000000003e8" # 1000 data_amt0 = "0000000000000000000000000000000000000000000000000de0b6b3a7640000" # 1e18 data_amt1 = "00000000000000000000000000000000000000000000000000000000b2d05e00" # 3e9 class Receipt: status = 1 blockNumber = 12345 logs = [ { 'topics': [ HexBytes(transfer_topic), HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # From HexBytes("0x0000000000000000000000000000000000000000000000000000000000000000"), # To HexBytes("0x" + token_id_hex) # TokenID ], 'data': b'' }, { 'topics': [ HexBytes(increase_topic) ], 'data': bytes.fromhex(data_liq + data_amt0 + data_amt1) } ] return Receipt() class MockWeb3: def __init__(self, state: MockExchangeState): self.eth = MockEth(state) self.middleware_onion = type('obj', (object,), {'inject': lambda *args, **kwargs: None}) def is_connected(self): return True def to_checksum_address(self, addr): return addr def is_address(self, addr): return True @staticmethod def to_wei(val, unit): if unit == 'gwei': return int(val * 10**9) if unit == 'ether': return int(val * 10**18) return int(val) @staticmethod def from_wei(val, unit): if unit == 'gwei': return Decimal(val) / Decimal(10**9) if unit == 'ether': return Decimal(val) / Decimal(10**18) return Decimal(val) @staticmethod def keccak(text=None): return b'\x00'*32 # Dummy # --- HYPERLIQUID MOCKS --- class MockInfo: def __init__(self, state: MockExchangeState): self.state = state def all_mids(self): # Return string prices as per API return {k: str(v) for k, v in self.state.prices.items()} def user_state(self, address): positions = [] for sym, pos in self.state.hl_positions.items(): positions.append({ "position": { "coin": sym, "szi": str(pos['size']), "entryPx": str(pos['entry_px']), "unrealizedPnl": str(pos['unrealized_pnl']) } }) return { "marginSummary": { "accountValue": str(self.state.hl_balances.get("USDC", 0)), "totalMarginUsed": "0", "totalNtlPos": "0", "totalRawUsd": "0" }, "assetPositions": positions } def open_orders(self, address): return self.state.hl_orders def user_fills(self, address): return [] # TODO: Store fills in state def l2_snapshot(self, coin): # Generate artificial orderbook around mid price price = self.state.prices.get(coin, Decimal("0")) if price == 0: return {'levels': [[], []]} # Spread 0.05% bid = price * Decimal("0.99975") ask = price * Decimal("1.00025") return { "levels": [ [{"px": str(bid), "sz": "100.0", "n": 1}], # Bids [{"px": str(ask), "sz": "100.0", "n": 1}] # Asks ] } class MockExchangeAPI: def __init__(self, state: MockExchangeState): self.state = state def order(self, coin, is_buy, sz, limit_px, order_type, reduce_only=False): # Execute immediately for IO/Market, or add to book # Simulating Fill price = Decimal(str(limit_px)) size = Decimal(str(sz)) cost = price * size # Fee (Taker 0.035%) fee = cost * Decimal("0.00035") self.state.hl_fees_paid += fee # Update Position if coin not in self.state.hl_positions: self.state.hl_positions[coin] = {'size': Decimal(0), 'entry_px': Decimal(0), 'unrealized_pnl': Decimal(0)} pos = self.state.hl_positions[coin] current_size = pos['size'] # Update Entry Price (Weighted Average) # New Entry = (OldSize * OldEntry + NewSize * NewPrice) / (OldSize + NewSize) signed_size = size if is_buy else -size new_size = current_size + signed_size if new_size == 0: pos['entry_px'] = 0 elif (current_size > 0 and signed_size > 0) or (current_size < 0 and signed_size < 0): # Increasing position val_old = abs(current_size) * pos['entry_px'] val_new = size * price pos['entry_px'] = (val_old + val_new) / abs(new_size) else: # Closing/Reducing - Entry Price doesn't change, PnL is realized # Fraction closed closed_ratio = min(abs(signed_size), abs(current_size)) / abs(current_size) # This logic is simplified, real PnL logic is complex pos['size'] = new_size logger.info(f"MOCK HL EXEC: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price}. New Size: {new_size}") return {"status": "ok", "response": {"data": {"statuses": [{"filled": {"oid": 123}}]}}} def cancel(self, coin, oid): self.state.hl_orders = [o for o in self.state.hl_orders if o['oid'] != oid] return {"status": "ok"}