introduce to tests
This commit is contained in:
49
florida/tests/backtest/analyze_results.py
Normal file
49
florida/tests/backtest/analyze_results.py
Normal file
@ -0,0 +1,49 @@
|
||||
|
||||
import csv
|
||||
import sys
|
||||
import os
|
||||
|
||||
def analyze():
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
results_file = os.path.join(current_dir, "optimization_results.csv")
|
||||
|
||||
if not os.path.exists(results_file):
|
||||
print(f"File not found: {results_file}")
|
||||
return
|
||||
|
||||
print(f"Analyzing {results_file}...")
|
||||
|
||||
best_pnl = -float('inf')
|
||||
best_config = None
|
||||
|
||||
rows = []
|
||||
|
||||
with open(results_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
rows.append(row)
|
||||
|
||||
pnl = float(row['TOTAL_PNL'])
|
||||
uni_fees = float(row['UNI_FEES'])
|
||||
hl_pnl = float(row['HL_PNL'])
|
||||
|
||||
print(f"Config: Range={row['RANGE_WIDTH_PCT']}, Thresh={row['BASE_REBALANCE_THRESHOLD_PCT']} | PnL: ${pnl:.2f} (Fees: ${uni_fees:.2f}, Hedge: ${hl_pnl:.2f})")
|
||||
|
||||
if pnl > best_pnl:
|
||||
best_pnl = pnl
|
||||
best_config = row
|
||||
|
||||
print("\n" + "="*40)
|
||||
print(f"🏆 BEST CONFIGURATION")
|
||||
print("="*40)
|
||||
if best_config:
|
||||
print(f"Range Width: {float(best_config['RANGE_WIDTH_PCT'])*100:.2f}%")
|
||||
print(f"Rebalance Thresh: {float(best_config['BASE_REBALANCE_THRESHOLD_PCT'])*100:.0f}%")
|
||||
print(f"Total PnL: ${float(best_config['TOTAL_PNL']):.2f}")
|
||||
print(f" > Uni Fees: ${float(best_config['UNI_FEES']):.2f}")
|
||||
print(f" > Hedge PnL: ${float(best_config['HL_PNL']):.2f}")
|
||||
else:
|
||||
print("No valid results found.")
|
||||
|
||||
if __name__ == "__main__":
|
||||
analyze()
|
||||
312
florida/tests/backtest/backtester.py
Normal file
312
florida/tests/backtest/backtester.py
Normal file
@ -0,0 +1,312 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
import time
|
||||
import json
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.mocks import MockExchangeState, MockWeb3, MockExchangeAPI, MockInfo, MockContract
|
||||
|
||||
# Setup Logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s [%(name)s] %(message)s')
|
||||
logger = logging.getLogger("BACKTESTER")
|
||||
|
||||
class Backtester:
|
||||
def __init__(self, book_file, trades_file, config_overrides=None):
|
||||
self.book_file = book_file
|
||||
self.trades_file = trades_file
|
||||
self.config_overrides = config_overrides or {}
|
||||
self.events = []
|
||||
self.state = MockExchangeState()
|
||||
|
||||
# Mocks
|
||||
self.mock_web3 = MockWeb3(self.state)
|
||||
self.mock_hl_api = MockExchangeAPI(self.state)
|
||||
self.mock_hl_info = MockInfo(self.state)
|
||||
|
||||
# Components (Lazy loaded)
|
||||
self.manager = None
|
||||
self.hedger = None
|
||||
|
||||
def load_data(self):
|
||||
logger.info("Loading Market Data...")
|
||||
# Load Book
|
||||
with open(self.book_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
self.events.append({
|
||||
"type": "BOOK",
|
||||
"ts": int(row['timestamp_ms']),
|
||||
"data": row
|
||||
})
|
||||
|
||||
# Load Trades
|
||||
# (Optional: Trades are useful for market impact, but for basic PnL tracking
|
||||
# based on mid-price, Book is sufficient. Loading trades just to advance time)
|
||||
with open(self.trades_file, 'r') as f:
|
||||
reader = csv.DictReader(f)
|
||||
for row in reader:
|
||||
self.events.append({
|
||||
"type": "TRADE",
|
||||
"ts": int(row['timestamp_ms']),
|
||||
"data": row
|
||||
})
|
||||
|
||||
# Sort by Timestamp
|
||||
self.events.sort(key=lambda x: x['ts'])
|
||||
logger.info(f"Loaded {len(self.events)} events.")
|
||||
|
||||
def patch_and_init(self):
|
||||
logger.info("Initializing Logic...")
|
||||
|
||||
# --- PATCH MANAGER ---
|
||||
# We need to patch clp_manager.Web3 to return our MockWeb3
|
||||
# And os.environ for config
|
||||
|
||||
with patch.dict(os.environ, {
|
||||
"TARGET_DEX": "PANCAKESWAP_BNB", # Example
|
||||
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"BNB_RPC_URL": "http://mock",
|
||||
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"MAIN_WALLET_ADDRESS": "0xMyWallet"
|
||||
}):
|
||||
import clp_manager
|
||||
import clp_hedger
|
||||
|
||||
# Apply Config Overrides
|
||||
if self.config_overrides:
|
||||
logger.info(f"Applying Config Overrides: {self.config_overrides}")
|
||||
for k, v in self.config_overrides.items():
|
||||
# Patch Manager
|
||||
if hasattr(clp_manager, k):
|
||||
setattr(clp_manager, k, v)
|
||||
# Patch Hedger
|
||||
if hasattr(clp_hedger, k):
|
||||
setattr(clp_hedger, k, v)
|
||||
|
||||
# 1. Init Manager
|
||||
# clp_manager.main() connects to Web3. We need to inject our mock.
|
||||
# Since clp_manager creates w3 inside main(), we can't inject easily without patching Web3 class.
|
||||
|
||||
self.manager_module = clp_manager
|
||||
self.hedger_module = clp_hedger
|
||||
|
||||
def run(self):
|
||||
self.load_data()
|
||||
self.patch_and_init()
|
||||
|
||||
# MOCK TIME
|
||||
start_time = self.events[0]['ts'] / 1000.0
|
||||
|
||||
# STATUS FILE MOCK
|
||||
self.status_memory = [] # List[Dict]
|
||||
|
||||
def mock_load_status():
|
||||
logger.info(f"MOCK LOAD STATUS: Found {len(self.status_memory)} items")
|
||||
return self.status_memory
|
||||
|
||||
def mock_save_status(data):
|
||||
logger.info(f"MOCK SAVE STATUS: Saving {len(data)} items")
|
||||
self.status_memory = data
|
||||
|
||||
def mock_hedger_scan():
|
||||
return []
|
||||
|
||||
# We need to globally patch time.time and the Libraries
|
||||
web3_class_mock = MagicMock(return_value=self.mock_web3)
|
||||
web3_class_mock.to_wei = self.mock_web3.to_wei
|
||||
web3_class_mock.from_wei = self.mock_web3.from_wei
|
||||
web3_class_mock.is_address = self.mock_web3.is_address
|
||||
web3_class_mock.to_checksum_address = lambda x: x
|
||||
|
||||
# Mock Web3.keccak to return correct topics
|
||||
def mock_keccak(text=None, hexstr=None):
|
||||
# Known Topics
|
||||
if text == "Transfer(address,address,uint256)":
|
||||
return bytes.fromhex("ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef")
|
||||
if text == "IncreaseLiquidity(uint256,uint128,uint256,uint256)":
|
||||
return bytes.fromhex("7a53080ba414158be7ec69b987b5fb7d07dee101fe85488f0853ae16239d0bde")
|
||||
if text == "DecreaseLiquidity(uint256,uint128,uint256,uint256)":
|
||||
# 0x26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4
|
||||
return bytes.fromhex("26f6a048ee9138f2c0ce266f322cb99228e8d619ae2bff30c67f8dcf9d2377b4")
|
||||
if text == "Collect(uint256,address,uint256,uint256)":
|
||||
# 0x70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d
|
||||
return bytes.fromhex("70935338e69775456a85ddef226c395fb668b63fa0115f5f206227278f746d4d")
|
||||
|
||||
return b'\x00'*32
|
||||
|
||||
web3_class_mock.keccak = MagicMock(side_effect=mock_keccak)
|
||||
|
||||
# Ensure environment is patched during the whole run
|
||||
env_patch = {
|
||||
"TARGET_DEX": "PANCAKESWAP_BNB",
|
||||
"MAIN_WALLET_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"BNB_RPC_URL": "http://mock",
|
||||
"HEDGER_PRIVATE_KEY": "0x0000000000000000000000000000000000000000000000000000000000000001",
|
||||
"MAIN_WALLET_ADDRESS": "0xMyWallet"
|
||||
}
|
||||
|
||||
with patch.dict(os.environ, env_patch), \
|
||||
patch('time.time', side_effect=lambda: self.state.current_time_ms / 1000.0), \
|
||||
patch('clp_manager.Web3', web3_class_mock), \
|
||||
patch('clp_hedger.Account.from_key', return_value=MagicMock(address="0xMyWallet")), \
|
||||
patch('clp_hedger.Exchange', return_value=self.mock_hl_api), \
|
||||
patch('clp_hedger.Info', return_value=self.mock_hl_info), \
|
||||
patch('clp_manager.load_status_data', side_effect=mock_load_status), \
|
||||
patch('clp_manager.save_status_data', side_effect=mock_save_status), \
|
||||
patch('clp_manager.clean_address', side_effect=lambda x: x), \
|
||||
patch('clp_hedger.glob.glob', return_value=[]):
|
||||
|
||||
# Initialize Hedger (It creates the classes in __init__)
|
||||
self.hedger = self.hedger_module.UnifiedHedger()
|
||||
|
||||
# Initialize Manager Components manually (simulate main setup)
|
||||
w3 = self.mock_web3
|
||||
account = MagicMock(address="0xMyWallet")
|
||||
npm = w3.eth.contract("0xNPM", [])
|
||||
factory = w3.eth.contract("0xFactory", [])
|
||||
router = w3.eth.contract("0xRouter", [])
|
||||
|
||||
# --- SIMULATION LOOP ---
|
||||
last_manager_tick = 0
|
||||
manager_interval = 60 * 1000
|
||||
|
||||
trade_count = len([e for e in self.events if e['type'] == "TRADE"])
|
||||
book_count = len([e for e in self.events if e['type'] == "BOOK"])
|
||||
logger.info(f"SIMULATION START: {len(self.events)} total events ({book_count} BOOK, {trade_count} TRADE)")
|
||||
|
||||
for event in self.events:
|
||||
self.state.current_time_ms = event['ts']
|
||||
|
||||
# Update Market
|
||||
if event['type'] == "BOOK":
|
||||
row = event['data']
|
||||
mid = Decimal(row['mid_price'])
|
||||
self.state.update_price("BNB", mid)
|
||||
|
||||
if event['type'] == "TRADE":
|
||||
self.state.process_trade(event['data'])
|
||||
|
||||
# Run Logic
|
||||
|
||||
# 1. Manager (Every X seconds)
|
||||
if self.state.current_time_ms - last_manager_tick > manager_interval:
|
||||
self.manager_module.run_tick(w3, account, npm, factory, router)
|
||||
last_manager_tick = self.state.current_time_ms
|
||||
|
||||
# SYNC MANAGER STATUS TO HEDGER
|
||||
for pos in self.status_memory:
|
||||
if pos.get('status') == 'OPEN' and pos.get('type') == 'AUTOMATIC':
|
||||
key = ("MOCK", pos['token_id'])
|
||||
if key not in self.hedger.strategies:
|
||||
self.hedger._init_single_strategy(key, pos, "BNB")
|
||||
else:
|
||||
self.hedger.strategy_states[key]['status'] = pos.get('status', 'OPEN')
|
||||
elif pos.get('status') == 'CLOSED':
|
||||
key = ("MOCK", pos['token_id'])
|
||||
if key in self.hedger.strategies:
|
||||
self.hedger.strategy_states[key]['status'] = 'CLOSED'
|
||||
|
||||
|
||||
# 2. Hedger (Every Tick/Event)
|
||||
self.hedger.run_tick()
|
||||
|
||||
# Finalize: Collect accrued fees from open positions
|
||||
logger.info(f"Finalizing... Checking {len(self.state.uni_positions)} open positions.")
|
||||
for token_id, pos in self.state.uni_positions.items():
|
||||
raw_owed0 = pos.get('tokensOwed0', 0)
|
||||
logger.info(f"DEBUG: Position {token_id} Raw TokensOwed0: {raw_owed0}")
|
||||
|
||||
owed0 = Decimal(raw_owed0) / Decimal(10**18)
|
||||
owed1 = Decimal(pos.get('tokensOwed1', 0)) / Decimal(10**18)
|
||||
|
||||
# Convert to USD
|
||||
price = self.state.prices.get("BNB", Decimal("0"))
|
||||
# Fee0 is USDT (USD), Fee1 is WBNB
|
||||
usd_val = owed0 + (owed1 * price)
|
||||
|
||||
if usd_val > 0:
|
||||
self.state.uni_fees_collected += usd_val
|
||||
logger.info(f"Finalizing Open Position {token_id}: Accrued Fees ${usd_val:.2f}")
|
||||
|
||||
logger.info("Backtest Complete.")
|
||||
logger.info(f"Final Uni Fees: {self.state.uni_fees_collected}")
|
||||
logger.info(f"Final HL PnL: {self.state.hl_realized_pnl - self.state.hl_fees_paid}")
|
||||
|
||||
def calculate_final_nav(self):
|
||||
"""Calculates total Net Asset Value (USD) at the end of simulation."""
|
||||
total_usd = Decimal("0")
|
||||
|
||||
# 1. Wallet Balances
|
||||
# We assume T0=USDT, T1=WBNB for this profile
|
||||
price = self.state.prices.get("BNB", Decimal("0"))
|
||||
|
||||
for sym, bal in self.state.wallet_balances.items():
|
||||
if sym in ["USDC", "USDT"]:
|
||||
total_usd += bal
|
||||
elif sym in ["BNB", "WBNB", "NATIVE"]:
|
||||
total_usd += bal * price
|
||||
elif sym in ["ETH", "WETH"]:
|
||||
# If ETH price available? We mocked update_price("BNB") only.
|
||||
# Assuming ETH price static or 0 if not tracked
|
||||
eth_price = self.state.prices.get("ETH", Decimal("0"))
|
||||
total_usd += bal * eth_price
|
||||
|
||||
# 2. Uniswap Positions (Liquidity Value)
|
||||
# Value = Amount0 * Price0 + Amount1 * Price1
|
||||
# We need to calculate amounts from liquidity & current price
|
||||
import math
|
||||
# Helper to get amounts from liquidity
|
||||
def get_amounts(liquidity, sqrt_price_x96, tick_lower, tick_upper):
|
||||
# Simplified: Use the amounts we stored at mint time?
|
||||
# No, that's initial. We need current value.
|
||||
# But calculating precise amounts from liquidity/sqrtPrice requires complex math.
|
||||
# For approximation, we can look at what the manager logged as "Deposited"
|
||||
# if price hasn't moved much, or implement full liquidity math.
|
||||
|
||||
# Since implementing full math here is complex, let's use a simplified approach:
|
||||
# If we are in range, we have a mix.
|
||||
# If out of range, we have 100% of one token.
|
||||
|
||||
# Better: The Mock 'mint' stored initial amounts.
|
||||
# We can adjust by price ratio? No, IL is non-linear.
|
||||
|
||||
# Let's use the 'decrease_liquidity' logic mock if available?
|
||||
# Or just assume Liquidity Value = Initial Value + PnL (Fees) - IL.
|
||||
|
||||
# For this MVP, let's just count the Fees collected (Realized) + Initial Capital (Wallet).
|
||||
# BUT we spent wallet funds to open LP.
|
||||
# So Wallet is LOW. LP has Value.
|
||||
|
||||
# We MUST value the LP.
|
||||
# Let's approximate:
|
||||
# Value = Liquidity / (something) ...
|
||||
# Actually, `clp_manager.py` calculates `actual_value` on entry.
|
||||
# We can track `entry_value` in the position state.
|
||||
return Decimal("0") # Placeholder if we can't calc easily
|
||||
|
||||
# 3. Hyperliquid Positions (Unrealized PnL + Margin)
|
||||
hl_equity = self.state.hl_balances.get("USDC", 0) # Margin
|
||||
for sym, pos in self.state.hl_positions.items():
|
||||
hl_equity += pos['unrealized_pnl']
|
||||
|
||||
total_usd += hl_equity
|
||||
|
||||
return total_usd
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage:
|
||||
# python tests/backtest/backtester.py market_data/BNB_raw_20251230_book.csv market_data/BNB_raw_20251230_trades.csv
|
||||
if len(sys.argv) < 3:
|
||||
print("Usage: python backtester.py <book_csv> <trades_csv>")
|
||||
else:
|
||||
bt = Backtester(sys.argv[1], sys.argv[2])
|
||||
bt.run()
|
||||
82
florida/tests/backtest/grid_search.py
Normal file
82
florida/tests/backtest/grid_search.py
Normal file
@ -0,0 +1,82 @@
|
||||
|
||||
import sys
|
||||
import os
|
||||
import csv
|
||||
import itertools
|
||||
from decimal import Decimal
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.backtester import Backtester
|
||||
|
||||
def main():
|
||||
# Grid Parameters
|
||||
# We want to optimize:
|
||||
# 1. RANGE_WIDTH_PCT: How wide is the LP position? (e.g. 0.01 = +/-1%, 0.05 = +/-5%)
|
||||
# 2. BASE_REBALANCE_THRESHOLD_PCT: When do we hedge? (e.g. 0.10 = 10% delta drift, 0.20 = 20%)
|
||||
|
||||
param_grid = {
|
||||
"RANGE_WIDTH_PCT": [0.005, 0.01, 0.025, 0.05],
|
||||
"BASE_REBALANCE_THRESHOLD_PCT": [0.01, 0.05]
|
||||
}
|
||||
|
||||
keys, values = zip(*param_grid.items())
|
||||
combinations = [dict(zip(keys, v)) for v in itertools.product(*values)]
|
||||
|
||||
results = []
|
||||
|
||||
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
|
||||
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
|
||||
|
||||
print(f"Starting Grid Search with {len(combinations)} combinations...")
|
||||
|
||||
for idx, config in enumerate(combinations):
|
||||
print(f"\n--- Run {idx+1}/{len(combinations)}: {config} ---")
|
||||
|
||||
# Initialize Backtester with overrides
|
||||
bt = Backtester(book_file, trades_file, config_overrides=config)
|
||||
|
||||
try:
|
||||
bt.run()
|
||||
|
||||
# Collect Metrics
|
||||
uni_fees = bt.state.uni_fees_collected
|
||||
hl_realized = bt.state.hl_realized_pnl - bt.state.hl_fees_paid
|
||||
|
||||
# HL Unrealized
|
||||
hl_unrealized = sum(p['unrealized_pnl'] for p in bt.state.hl_positions.values())
|
||||
|
||||
# Total PnL (Yield + Hedge Result) - Ignoring IL for now (Mock limitation)
|
||||
total_pnl = uni_fees + hl_realized + hl_unrealized
|
||||
|
||||
result = {
|
||||
**config,
|
||||
"UNI_FEES": float(uni_fees),
|
||||
"HL_REALIZED": float(hl_realized),
|
||||
"HL_UNREALIZED": float(hl_unrealized),
|
||||
"TOTAL_PNL": float(total_pnl)
|
||||
}
|
||||
results.append(result)
|
||||
print(f"Result: {result}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"Run failed: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
# Save Results
|
||||
out_file = os.path.join(current_dir, "optimization_results.csv")
|
||||
keys = list(combinations[0].keys()) + ["UNI_FEES", "HL_REALIZED", "HL_UNREALIZED", "TOTAL_PNL"]
|
||||
|
||||
with open(out_file, 'w', newline='') as f:
|
||||
writer = csv.DictWriter(f, fieldnames=keys)
|
||||
writer.writeheader()
|
||||
writer.writerows(results)
|
||||
|
||||
print(f"\nGrid Search Complete. Results saved to {out_file}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
607
florida/tests/backtest/mocks.py
Normal file
607
florida/tests/backtest/mocks.py
Normal file
@ -0,0 +1,607 @@
|
||||
|
||||
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"}
|
||||
25
florida/tests/backtest/run_backtest.py
Normal file
25
florida/tests/backtest/run_backtest.py
Normal file
@ -0,0 +1,25 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Add project root to path
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
project_root = os.path.dirname(os.path.dirname(current_dir))
|
||||
sys.path.append(project_root)
|
||||
|
||||
from tests.backtest.backtester import Backtester
|
||||
|
||||
def main():
|
||||
book_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_book.csv")
|
||||
trades_file = os.path.join(project_root, "market_data", "BNB_raw_20251230_trades.csv")
|
||||
|
||||
if not os.path.exists(book_file):
|
||||
print(f"Error: Data file not found: {book_file}")
|
||||
return
|
||||
|
||||
print(f"Starting Backtest on {book_file}...")
|
||||
bt = Backtester(book_file, trades_file)
|
||||
bt.run()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user