hedge and auto hedger in separate folders

This commit is contained in:
2025-12-16 14:19:24 +01:00
parent 109ef7cd24
commit e1b3c5814b
8 changed files with 749 additions and 59 deletions

View File

@ -0,0 +1,256 @@
# CLP Hedging Zone Strategy Implementation Plan
*Generated: 2025-12-16*
*Session Focus: Risk analysis and zone-based hedge optimization*
## Executive Summary
This plan implements a zone-based hedging strategy for narrow CLP ranges (+/- 0.3%) with $100 position size and $10 minimum trade constraints. The strategy maintains the existing 7.5-minute hedge delay for mean reversion while adding preparation zones for potential CLP closing.
## Current System Analysis
### Scripts & Configuration
- **uniswap_manager.py**: CLP lifecycle management (451-second interval)
- **clp_scalper_hedger.py**: Active hedging (4-second interval)
- **Strategy**: Mean reversion with intentional 7.5-minute unhedged period
- **Position Size**: $100 CLP position
- **Range Width**: +/- 0.3% (extremely narrow, requiring precise zone management)
- **Minimum Trade**: $10 (10% of position size - significant constraint)
### Risk Assessment
- **Strategic Risk**: Intentional unhedged exposure during 7.5-minute delay (accepted)
- **Technical Risks**: JSON file corruption, price source divergence, oscillation
- **Financial Impact**: $10 minimum trades create risk of overshooting hedge targets
## Proposed Zone Strategy
### Zone Structure
```
Range Position (% from bottom):
├── TOP PREPARE ZONE (90-100%): Gradual reduction 100% → 0%
├── TOP HYSTERESIS ZONE (85-90%): Maintain current hedge
├── MIDDLE NORMAL ZONE (10-85%): Normal hedge (100%)
├── BOTTOM HYSTERESIS ZONE (5-10%): Maintain current hedge
└── BOTTOM MAX ZONE (0-5%): Enhanced over-hedge (112.5%)
```
### Zone Rationale
- **90% Preparation Start**: Adequate preparation time while minimizing whipsaw risk
- **85-90% Hysteresis Buffer**: Prevents oscillation near top boundary
- **5-10% Bottom Buffer**: Reduces frequency of over-hedge adjustments
- **0-5% Enhanced Over-hedge**: Maximum protection when CLP is fully WETH
## Implementation Details
### Configuration Updates
```python
# Zone Boundaries for Narrow Range
TOP_PREPARE_START = 0.90 # Start unhedging at 90%
TOP_HYSTERESIS_START = 0.85 # Hysteresis buffer zone
BOTTOM_HYSTERESIS_END = 0.10 # Bottom hysteresis buffer
BOTTOM_MAX_ZONE_END = 0.05 # Enhanced over-hedge until 5%
# $10 Minimum Trade Controls
MIN_PRICE_MOVEMENT_PCT = 0.10 # 10% range movement before adjustment
MIN_TIME_BETWEEN_ADJUSTMENTS = 60 # 1 minute minimum between trades
MIN_TRADE_SIZE_USD = 10.0 # $10 minimum trade size
# Hedge Multipliers
TOP_PREPARE_MULTIPLIER = 0.0 # 0% hedge in prepare zone
NORMAL_HEDGE_MULTIPLIER = 1.0 # 100% normal hedge
BOTTOM_MAX_MULTIPLIER = 1.125 # 112.5% over-hedge
# Risk Management
MAX_DAILY_TRADES = 3 # Maximum trades per day
MAX_DAILY_EXPOSURE_USD = 30.0 # Maximum daily trade exposure
OVERSHOOT_TOLERANCE_PCT = 0.05 # 5% tolerance on $10 trades
```
### Core Methods to Implement
#### 1. Zone Calculation Method
```python
def calculate_zone_multiplier(self, price_pct):
"""
Calculate hedge multiplier based on price position within CLP range.
Implements gradual transitions and hysteresis.
"""
if price_pct >= 0.90: # 90-100%: Gradual reduction
return (1.0 - (price_pct - 0.90) / 0.10)
elif price_pct <= 0.05: # 0-5%: Enhanced over-hedge
return 1.0 + (0.05 - price_pct) * 0.25 # 112.5% at 0%, 100% at 5%
else: # 5-90%: Normal hedge
return 1.0
```
#### 2. Hysteresis Control
```python
def should_adjust_hedge(self, current_price_pct, last_adjustment_pct, last_adjustment_time):
"""
Prevent frequent small adjustments due to $10 minimum trade constraint.
"""
# Minimum price movement (equivalent to $10 trade)
if abs(current_price_pct - last_adjustment_pct) < self.MIN_PRICE_MOVEMENT_PCT:
return False
# Minimum time between adjustments
if time.time() - last_adjustment_time < self.MIN_TIME_BETWEEN_ADJUSTMENTS:
return False
return True
```
#### 3. Trade Size Optimization
```python
def calculate_optimal_trade_size(self, diff, position_value):
"""
Round trades to $10 increments and enforce minimum trade size.
"""
trade_value_usd = abs(diff * position_value)
# Skip if below minimum
if trade_value_usd < self.MIN_TRADE_SIZE_USD:
return 0
# Round to nearest $10 increment for efficiency
rounded_trade_value = round(trade_value_usd / 10.0) * 10.0
# Convert back to position units
return rounded_trade_value / position_value
```
### Files to Modify
#### Primary: clp_scalper_hedger.py
**Lines to Update:**
- **44-53**: Zone configuration constants
- **252-284**: Core `calculate_rebalance()` method
- **255-265**: Integrate with existing over-hedge logic
**Methods to Add:**
- `calculate_zone_multiplier()` - Zone-based hedge calculation
- `should_adjust_hedge()` - $10 minimum trade logic
- `calculate_optimal_trade_size()` - Rounding to $10 increments
- `update_zone_state()` - Hysteresis zone management
#### Secondary: hedge_status.json (runtime)
- Add zone transition tracking fields
- Add last adjustment timestamps
- Add daily trade count tracking
## Risk Management Strategy
### Financial Risk Controls
- **Position Size Limit**: $100 maximum CLP position
- **Daily Trade Limit**: Maximum 3 trades ($30 exposure)
- **Over-hedge Cap**: 125% absolute maximum (vs 112.5% target)
- **Transaction Cost Budget**: $5 maximum daily trading costs
### Technical Risk Mitigation
- **JSON File Locking**: Prevent concurrent access corruption
- **Hysteresis Implementation**: Prevent oscillation trading
- **Position Validation**: Verify hedge calculations before execution
- **Emergency Stops**: Circuit breakers on extreme market moves
### Operational Risk Controls
- **Time-based Limits**: Minimum intervals between adjustments
- **Movement Thresholds**: Minimum price changes before trading
- **Overshoot Protection**: Tolerance bands around target hedge ratios
- **Daily Cumulative Limits**: Maximum position change per day
## Implementation Sequence
### Phase 1: Core Zone Logic (Priority 1)
1. **Implement zone calculation method**
2. **Add hysteresis controls**
3. **Integrate with existing over-hedge logic**
4. **Update configuration constants**
### Phase 2: Trade Optimization (Priority 2)
1. **Implement $10 minimum trade logic**
2. **Add rounding to nearest $10 increment**
3. **Add minimum time between trades**
4. **Integrate with existing `manage_orders()` method**
### Phase 3: Risk Controls (Priority 3)
1. **Add daily trade count limits**
2. **Implement overshoot protection**
3. **Add position validation checks**
4. **Create monitoring/logging for zone transitions**
### Phase 4: Live Deployment & Optimization (Priority 4)
1. **Deploy with $100 position**
2. **Monitor zone transition frequency**
3. **Adjust zone boundaries based on observations**
4. **Optimize trade timing and size**
## Key Questions for Finalization
### Configuration Preferences
1. **Zone Boundaries**: Are 90%/85%/10%/5% boundaries optimal, or should they be adjusted?
2. **Trade Frequency**: Is 3 trades per day acceptable, or prefer fewer/larger trades?
3. **Over-hedge Level**: Is 112.5% multiplier appropriate, or more/less aggressive?
4. **Time Buffers**: Is 1-minute minimum between trades sufficient?
### Risk Tolerance
5. **Maximum Daily Exposure**: Is $30 daily trade exposure acceptable?
6. **Overshoot Tolerance**: Is 5% tolerance on $10 trades appropriate?
7. **Position Size**: Should we start with smaller position during testing?
### Strategy Behavior
8. **Zone Entry Logic**: Should we implement different thresholds for entering vs exiting zones?
9. **Trade Timing**: Should trades occur immediately on zone entry or wait for confirmation?
10. **Market Conditions**: Should zones adapt based on volatility or time of day?
## Success Metrics
### Primary Metrics
- **Oscillation Frequency**: < 2 zone changes per hour
- **Trade Efficiency**: > 80% of trades executed at optimal size ($10+)
- **Hedge Accuracy**: Average hedge ratio within 5% of target
- **Transaction Costs**: < 3% of position value per day
### Secondary Metrics
- **Zone Transition Smoothness**: Gradual transitions without sudden jumps
- **Risk Control Compliance**: No violations of daily limits
- **System Stability**: No JSON corruption or sync issues
- **Strategy Performance**: Improvement over current baseline
## Monitoring & Alerts
### Real-time Monitoring
- Zone transition logging
- Hedge ratio tracking
- Trade execution verification
- Price source divergence detection
### Alert Conditions
- Excessive oscillation (> 5 zone changes/hour)
- Approaching daily trade limits
- Large hedge ratio deviations (> 10% from target)
- JSON file access conflicts
## Rollback Plan
### Immediate Rollback Triggers
- Financial losses > 15% of position value
- System instability or crashes
- Excessive trading frequency (> 5 trades/hour)
- Hedge calculation errors
### Rollback Procedure
1. Stop both scripts
2. Restore original configuration
3. Verify position status
4. Resume with baseline strategy
5. Analyze failure causes
## Next Steps
1. **Confirm Final Configuration**: Zone boundaries, trade limits, risk tolerances
2. **Implement Core Logic**: Zone calculation and hysteresis methods
3. **Integrate with Existing Code**: Update calculate_rebalance() method
4. **Test with Small Position**: Validate with $100 position
5. **Monitor and Optimize**: Adjust based on observed behavior
---
*This plan serves as the complete technical specification for implementing zone-based hedging strategy with $10 minimum trade constraints. The solution maintains the existing mean reversion strategy while adding sophisticated preparation zones for CLP closing scenarios.*

View File

@ -1,469 +0,0 @@
import os
import time
import logging
import sys
import math
import json
from dotenv import load_dotenv
# --- FIX: Add project root to sys.path to import local modules ---
current_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(current_dir)
sys.path.append(project_root)
# Now we can import from root
from logging_utils import setup_logging
from eth_account import Account
from hyperliquid.exchange import Exchange
from hyperliquid.info import Info
from hyperliquid.utils import constants
# Load environment variables from .env in current directory
dotenv_path = os.path.join(current_dir, '.env')
if os.path.exists(dotenv_path):
load_dotenv(dotenv_path)
else:
# Fallback to default search
load_dotenv()
# Setup Logging using project convention
setup_logging("normal", "CLP_HEDGER")
# --- CONFIGURATION DEFAULTS (Can be overridden by JSON) ---
REBALANCE_THRESHOLD = 0.15 # ETH
CHECK_INTERVAL = 30 # Seconds
LEVERAGE = 5
STATUS_FILE = "hedge_status.json"
# Gap Recovery Configuration
PRICE_BUFFER_PCT = 0.004 # 0.5% buffer to prevent churn
TIME_BUFFER_SECONDS = 120 # 2 minutes wait between mode switches
def get_manual_position_config():
"""Reads hedge_status.json and returns the first OPEN MANUAL position dict, or None."""
if not os.path.exists(STATUS_FILE):
return None
try:
with open(STATUS_FILE, 'r') as f:
data = json.load(f)
for entry in data:
if entry.get('type') == 'MANUAL' and entry.get('status') == 'OPEN':
return entry
except Exception as e:
logging.error(f"ERROR reading status file: {e}")
return None
class HyperliquidStrategy:
def __init__(self, entry_weth, entry_price, low_range, high_range, start_price, static_long=0.4):
# Your Pool Configuration
self.entry_weth = entry_weth
self.entry_price = entry_price
self.low_range = low_range
self.high_range = high_range
self.static_long = static_long
# Gap Recovery State
self.start_price = start_price
# GAP = max(0, ENTRY - START). If Start > Entry (we are winning), Gap is 0.
self.gap = max(0.0, entry_price - start_price)
self.recovery_target = entry_price + (2 * self.gap)
self.current_mode = "NORMAL" # "NORMAL" (100% Hedge) or "RECOVERY" (0% Hedge)
self.last_switch_time = 0
logging.info(f"Strategy Init. Start Px: {start_price:.2f} | Gap: {self.gap:.2f} | Recovery Tgt: {self.recovery_target:.2f}")
# Calculate Constant Liquidity (L) once
# Formula: L = x / (1/sqrt(P) - 1/sqrt(Pb))
try:
sqrt_P = math.sqrt(entry_price)
sqrt_Pb = math.sqrt(high_range)
self.L = entry_weth / ((1/sqrt_P) - (1/sqrt_Pb))
logging.info(f"Liquidity (L): {self.L:.4f}")
except Exception as e:
logging.error(f"Error calculating liquidity: {e}")
sys.exit(1)
def get_pool_delta(self, current_price):
"""Calculates how much ETH the pool currently holds (The Risk)"""
# If price is above range, you hold 0 ETH (100% USDC)
if current_price >= self.high_range:
return 0.0
# If price is below range, you hold Max ETH
if current_price <= self.low_range:
sqrt_Pa = math.sqrt(self.low_range)
sqrt_Pb = math.sqrt(self.high_range)
return self.L * ((1/sqrt_Pa) - (1/sqrt_Pb))
# If in range, calculate active ETH
sqrt_P = math.sqrt(current_price)
sqrt_Pb = math.sqrt(self.high_range)
return self.L * ((1/sqrt_P) - (1/sqrt_Pb))
def calculate_rebalance(self, current_price, current_short_position_size):
"""
Determines if we need to trade and the exact order size.
"""
# 1. Base Target (Full Hedge)
pool_delta = self.get_pool_delta(current_price)
raw_target_short = pool_delta + self.static_long
# 2. Determine Mode (Normal vs Recovery)
# Buffers
entry_upper = self.entry_price * (1 + PRICE_BUFFER_PCT)
entry_lower = self.entry_price * (1 - PRICE_BUFFER_PCT)
desired_mode = self.current_mode # Default to staying same
if self.current_mode == "NORMAL":
# Switch to RECOVERY if:
# Price > Entry + Buffer AND Price < Recovery Target
if current_price > entry_upper and current_price < self.recovery_target:
desired_mode = "RECOVERY"
elif self.current_mode == "RECOVERY":
# Switch back to NORMAL if:
# Price < Entry - Buffer (Fell back down) OR Price > Recovery Target (Finished)
if current_price < entry_lower or current_price >= self.recovery_target:
desired_mode = "NORMAL"
# 3. Apply Time Buffer
now = time.time()
if desired_mode != self.current_mode:
if (now - self.last_switch_time) >= TIME_BUFFER_SECONDS:
logging.info(f"🔄 MODE SWITCH: {self.current_mode} -> {desired_mode} (Px: {current_price:.2f})")
self.current_mode = desired_mode
self.last_switch_time = now
else:
logging.info(f"⏳ Mode Switch Delayed (Time Buffer). Pending: {desired_mode}")
# 4. Set Final Target based on Mode
if self.current_mode == "RECOVERY":
target_short_size = 0.0
logging.info(f"🩹 RECOVERY MODE ACTIVE (0% Hedge). Target: {self.recovery_target:.2f}")
else:
target_short_size = raw_target_short
# 5. Calculate Difference
diff = target_short_size - abs(current_short_position_size)
return {
"current_price": current_price,
"pool_delta": pool_delta,
"target_short": target_short_size,
"raw_target": raw_target_short,
"current_short": abs(current_short_position_size),
"diff": diff, # Positive = SELL more (Add Short), Negative = BUY (Reduce Short)
"action": "SELL" if diff > 0 else "BUY",
"mode": self.current_mode
}
def round_to_sz_decimals(amount, sz_decimals=4):
"""
Hyperliquid requires specific rounding 'szDecimals'.
For ETH, this is usually 4 (e.g., 1.2345).
"""
factor = 10 ** sz_decimals
# Use floor to avoid rounding up into money you don't have,
# but strictly simply rounding is often sufficient for small adjustments.
# Using round() standard here.
return round(abs(amount), sz_decimals)
def round_to_sig_figs(x, sig_figs=5):
"""
Rounds a number to a specified number of significant figures.
Hyperliquid prices generally require 5 significant figures.
"""
if x == 0:
return 0.0
return round(x, sig_figs - int(math.floor(math.log10(abs(x)))) - 1)
class CLPHedger:
def __init__(self):
self.private_key = os.environ.get("HEDGER_PRIVATE_KEY") or os.environ.get("AGENT_PRIVATE_KEY")
self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS")
if not self.private_key:
logging.error("No private key found (HEDGER_PRIVATE_KEY or AGENT_PRIVATE_KEY) in .env")
sys.exit(1)
if not self.vault_address:
logging.warning("MAIN_WALLET_ADDRESS not found in .env. Assuming Agent is the Vault (not strictly recommended for CLPs).")
self.account = Account.from_key(self.private_key)
# API Connection
self.info = Info(constants.MAINNET_API_URL, skip_ws=True)
# Note: If this agent is trading on behalf of a Vault (Main Account),
# the exchange object needs the vault's address as `account_address`.
self.exchange = Exchange(self.account, constants.MAINNET_API_URL, account_address=self.vault_address)
# Load Manual Config from JSON
self.manual_config = get_manual_position_config()
self.coin_symbol = "ETH" # Default, but will try to read from JSON
self.sz_decimals = 4
self.strategy = None
if self.manual_config:
self.coin_symbol = self.manual_config.get('coin_symbol', 'ETH')
if self.manual_config.get('hedge_enabled', False):
self._init_strategy()
else:
logging.warning("MANUAL position found but 'hedge_enabled' is FALSE. Hedger will remain idle.")
else:
logging.warning("No MANUAL position found in hedge_status.json. Hedger will remain idle.")
# Set Leverage on Initialization (if coin symbol known)
try:
logging.info(f"Setting leverage to {LEVERAGE}x (Cross) for {self.coin_symbol}...")
self.exchange.update_leverage(LEVERAGE, self.coin_symbol, is_cross=True)
except Exception as e:
logging.error(f"Failed to update leverage: {e}")
# Fetch meta once to get szDecimals
self.sz_decimals = self._get_sz_decimals(self.coin_symbol)
logging.info(f"CLP Hedger initialized. Agent: {self.account.address}. Coin: {self.coin_symbol} (Decimals: {self.sz_decimals})")
def _init_strategy(self):
try:
entry_p = self.manual_config['entry_price']
lower = self.manual_config['range_lower']
upper = self.manual_config['range_upper']
static_long = self.manual_config.get('static_long', 0.0)
# Require entry_amount0 (or entry_weth)
entry_weth = self.manual_config.get('entry_amount0', 0.45) # Default to 0.45 if missing for now
start_price = self.get_market_price(self.coin_symbol)
if start_price is None:
logging.warning("Waiting for initial price to start strategy...")
# Logic will retry in run loop
return
self.strategy = HyperliquidStrategy(
entry_weth=entry_weth,
entry_price=entry_p,
low_range=lower,
high_range=upper,
start_price=start_price,
static_long=static_long
)
logging.info(f"Strategy Initialized for {self.coin_symbol}.")
except Exception as e:
logging.error(f"Failed to init strategy: {e}")
self.strategy = None
def _get_sz_decimals(self, coin):
try:
meta = self.info.meta()
for asset in meta["universe"]:
if asset["name"] == coin:
return asset["szDecimals"]
logging.warning(f"Could not find szDecimals for {coin}, defaulting to 4.")
return 4
except Exception as e:
logging.error(f"Failed to fetch meta: {e}")
return 4
def get_funding_rate(self, coin):
try:
meta, asset_ctxs = self.info.meta_and_asset_ctxs()
for i, asset in enumerate(meta["universe"]):
if asset["name"] == coin:
# Funding rate is in the asset context at same index
return float(asset_ctxs[i]["funding"])
return 0.0
except Exception as e:
logging.error(f"Error fetching funding rate: {e}")
return 0.0
def get_market_price(self, coin):
try:
# Get all mids is efficient
mids = self.info.all_mids()
if coin in mids:
return float(mids[coin])
else:
logging.error(f"Price for {coin} not found in all_mids.")
return None
except Exception as e:
logging.error(f"Error fetching price: {e}")
return None
def get_current_position(self, coin):
try:
# We need the User State of the Vault (or the account we are trading for)
user_state = self.info.user_state(self.vault_address or self.account.address)
for pos in user_state["assetPositions"]:
if pos["position"]["coin"] == coin:
# szi is the size. Positive = Long, Negative = Short.
return float(pos["position"]["szi"])
return 0.0 # No position
except Exception as e:
logging.error(f"Error fetching position: {e}")
return 0.0
def execute_trade(self, coin, is_buy, size, price):
logging.info(f"🚀 EXECUTING: {coin} {'BUY' if is_buy else 'SELL'} {size} @ ~{price}")
# Check for reduceOnly logic
# If we are BUYING to reduce a SHORT, it is reduceOnly.
# If we are SELLING to increase a SHORT, it is NOT reduceOnly.
# Since we are essentially managing a Short hedge:
# Action BUY = Reducing Hedge -> reduceOnly=True
# Action SELL = Increasing Hedge -> reduceOnly=False
reduce_only = is_buy
try:
# Market order (limit with aggressive TIF or just widely crossing limit)
# Hyperliquid SDK 'order' method parameters: coin, is_buy, sz, limit_px, order_type, reduce_only
# We use a limit price slightly better than market to ensure fill or just use market price logic
# Using a simplistic "Market" approach by setting limit far away
slippage = 0.05 # 5% slippage tolerance
raw_limit_px = price * (1.05 if is_buy else 0.95)
limit_px = round_to_sig_figs(raw_limit_px, 5)
order_result = self.exchange.order(
coin,
is_buy,
size,
limit_px,
{"limit": {"tif": "Ioc"}},
reduce_only=reduce_only
)
status = order_result["status"]
if status == "ok":
response_data = order_result["response"]["data"]
if "statuses" in response_data and "error" in response_data["statuses"][0]:
logging.error(f"Order API Error: {response_data['statuses'][0]['error']}")
else:
logging.info(f"✅ Trade Success: {response_data}")
else:
logging.error(f"Order Failed: {order_result}")
except Exception as e:
logging.error(f"Exception during trade execution: {e}")
def close_all_positions(self):
logging.info("Attempting to close all open positions...")
try:
# 1. Get latest price
price = self.get_market_price(COIN_SYMBOL)
if price is None:
logging.error("Could not fetch price to close positions. Aborting close.")
return
# 2. Get current position
current_pos = self.get_current_position(COIN_SYMBOL)
if current_pos == 0:
logging.info("No open positions to close.")
return
# 3. Determine Side and Size
# If Short (-), we need to Buy (+).
# If Long (+), we need to Sell (-).
is_buy = current_pos < 0
abs_size = abs(current_pos)
# Ensure size is rounded correctly for the API
final_size = round_to_sz_decimals(abs_size, self.sz_decimals)
if final_size == 0:
logging.info("Position size effectively 0 after rounding.")
return
logging.info(f"Closing Position: {current_pos} {COIN_SYMBOL} -> Action: {'BUY' if is_buy else 'SELL'} {final_size}")
# 4. Execute
self.execute_trade(COIN_SYMBOL, is_buy, final_size, price)
except Exception as e:
logging.error(f"Error during close_all_positions: {e}")
def run(self):
logging.info(f"Starting Hedge Monitor Loop. Interval: {CHECK_INTERVAL}s")
while True:
try:
# Reload Config periodically
self.manual_config = get_manual_position_config()
# Check Global Enable Switch
if not self.manual_config or not self.manual_config.get('hedge_enabled', False):
# If previously active, close?
# Yes, safety first.
if self.strategy is not None:
logging.info("Hedge Disabled. Closing any remaining positions.")
self.close_all_positions()
self.strategy = None
else:
# Just idle check to keep connection alive or log occasionally
# logging.info("Idle. Hedge Disabled.")
pass
time.sleep(CHECK_INTERVAL)
continue
# If enabled but strategy not init, Init it.
if self.strategy is None:
self._init_strategy()
if self.strategy is None: # Init failed
time.sleep(CHECK_INTERVAL)
continue
# 1. Get Data
price = self.get_market_price(COIN_SYMBOL)
if price is None:
time.sleep(5)
continue
funding_rate = self.get_funding_rate(COIN_SYMBOL)
current_pos_size = self.get_current_position(COIN_SYMBOL)
# 2. Calculate Logic
# Pass raw size (e.g. -1.5). The strategy handles the logic.
calc = self.strategy.calculate_rebalance(price, current_pos_size)
diff_abs = abs(calc['diff'])
trade_size = round_to_sz_decimals(diff_abs, self.sz_decimals)
# Logging Status
status_msg = (
f"Price: {price:.2f} | Fund: {funding_rate:.6f} | "
f"Mode: {calc['mode']} | "
f"Pool Delta: {calc['pool_delta']:.3f} | "
f"Tgt Short: {calc['target_short']:.3f} | "
f"Act Short: {calc['current_short']:.3f} | "
f"Diff: {calc['diff']:.3f}"
)
if calc.get('is_recovering'):
status_msg += f" | 🩹 REC MODE ({calc['raw_target']:.3f} -> {calc['target_short']:.3f})"
logging.info(status_msg)
# 3. Check Threshold
if diff_abs >= REBALANCE_THRESHOLD:
if trade_size > 0:
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.3f} >= {REBALANCE_THRESHOLD})")
is_buy = (calc['action'] == "BUY")
self.execute_trade(COIN_SYMBOL, is_buy, trade_size, price)
else:
logging.info("Trade size rounds to 0. Skipping.")
time.sleep(CHECK_INTERVAL)
except KeyboardInterrupt:
logging.info("Stopping Hedger...")
self.close_all_positions()
break
except Exception as e:
logging.error(f"Loop Error: {e}", exc_info=True)
time.sleep(10)
if __name__ == "__main__":
hedger = CLPHedger()
hedger.run()

View File

@ -4,7 +4,9 @@ import logging
import sys
import math
import json
import threading
from dotenv import load_dotenv
from web3 import Web3
# --- FIX: Add project root to sys.path to import local modules ---
current_dir = os.path.dirname(os.path.abspath(__file__))
@ -30,29 +32,75 @@ setup_logging("normal", "SCALPER_HEDGER")
# --- CONFIGURATION ---
COIN_SYMBOL = "ETH"
CHECK_INTERVAL = 5 # Optimized for cost/noise reduction (was 1)
CHECK_INTERVAL = 4 # Optimized for speed (was 5)
LEVERAGE = 5 # 3x Leverage
STATUS_FILE = "hedge_status.json"
RPC_URL = os.environ.get("MAINNET_RPC_URL") # Required for Uniswap Monitor
# Uniswap V3 Pool (Arbitrum WETH/USDC 0.05%)
UNISWAP_POOL_ADDRESS = "0xC31E54c7a869B9FcBEcc14363CF510d1c41fa443"
UNISWAP_POOL_ABI = json.loads('[{"inputs":[],"name":"slot0","outputs":[{"internalType":"uint160","name":"sqrtPriceX96","type":"uint160"},{"internalType":"int24","name":"tick","type":"int24"},{"internalType":"uint16","name":"observationIndex","type":"uint16"},{"internalType":"uint16","name":"observationCardinality","type":"uint16"},{"internalType":"uint16","name":"observationCardinalityNext","type":"uint16"},{"internalType":"uint8","name":"feeProtocol","type":"uint8"},{"internalType":"bool","name":"unlocked","type":"bool"}],"stateMutability":"view","type":"function"}]')
# --- STRATEGY ZONES (Percent of Range Width) ---
# Bottom Hedge Zone: 0% to 15% -> Active Hedging
ZONE_BOTTOM_HEDGE_LIMIT = 0.5
# Bottom Hedge Zone: Covers entire range (0.0 to 1.5) -> Always Active
ZONE_BOTTOM_HEDGE_LIMIT = 1
# Close Zone: 15% to 20% -> Close All Hedges (Flatten)
ZONE_CLOSE_START = 0.52
ZONE_CLOSE_END = 0.54
# Close Zone: Disabled (Set > 1.0)
ZONE_CLOSE_START = 10.0
ZONE_CLOSE_END = 11.0
# Middle Zone: 20% to 85% -> Idle (No new orders, keep existing)
# Implied by gaps between other zones.
# Top Hedge Zone: 85% to 100% -> Active Hedging
ZONE_TOP_HEDGE_START = 0.8
# Top Hedge Zone: Disabled/Redundant
ZONE_TOP_HEDGE_START = 10.0
# --- ORDER SETTINGS ---
PRICE_BUFFER_PCT = 0.002 # 0.2% price move triggers order update (Relaxed for cost)
MIN_THRESHOLD_ETH = 0.02 # Minimum trade size in ETH (~$60, Reduced frequency)
PRICE_BUFFER_PCT = 0.0001 # 0.2% price move triggers order update (Relaxed for cost)
MIN_THRESHOLD_ETH = 0.0025 # Minimum trade size in ETH (~$60, Reduced frequency)
MIN_ORDER_VALUE_USD = 10.0 # Minimum order value for API safety
class UniswapPriceMonitor:
def __init__(self, rpc_url, pool_address):
self.w3 = Web3(Web3.HTTPProvider(rpc_url))
self.pool_contract = self.w3.eth.contract(address=pool_address, abi=UNISWAP_POOL_ABI)
self.latest_price = None
self.running = True
self.thread = threading.Thread(target=self._loop, daemon=True)
self.thread.start()
def _loop(self):
logging.info("Uniswap Monitor Started.")
while self.running:
try:
slot0 = self.pool_contract.functions.slot0().call()
sqrt_price_x96 = slot0[0]
# Price = (sqrtPriceX96 / 2^96)^2 * 10^(18-6) (WETH/USDC)
# But typically WETH is token1? Let's verify standard Arbitrum Pool.
# 0xC31E... Token0=WETH, Token1=USDC.
# Price = (sqrt / 2^96)^2 * (10^12) -> This gives USDC per ETH? No, Token1/Token0.
# Wait, usually Token0 is WETH (18) and Token1 is USDC (6).
# P = (1.0001^tick) * 10^(decimals0 - decimals1)? No.
# Standard conversion: Price = (sqrtRatioX96 / Q96) ** 2
# Adjusted for decimals: Price = Price_raw / (10**(Dec0 - Dec1)) ? No.
# Price (Quote/Base) = (sqrt / Q96)^2 * 10^(BaseDec - QuoteDec)
# Let's rely on standard logic: Price = (sqrt / 2^96)^2 * 10^(12) for ETH(18)/USDC(6)
raw_price = (sqrt_price_x96 / (2**96)) ** 2
price = raw_price * (10**(18-6)) # 10^12
# If Token0 is WETH, price is USDC per WETH.
# Note: If the pool is inverted (USDC/WETH), we invert.
# On Arb, WETH is usually Token0?
# 0x82aF... < 0xaf88... (WETH < USDC). So WETH is Token0.
# Price is Token1 per Token0.
self.latest_price = 1 / price if price < 1 else price # Sanity check, ETH should be > 2000
except Exception as e:
# logging.error(f"Uniswap Monitor Error: {e}")
pass
time.sleep(5)
def get_price(self):
return self.latest_price
def get_active_automatic_position():
if not os.path.exists(STATUS_FILE):
return None
@ -203,9 +251,25 @@ class HyperliquidStrategy:
def calculate_rebalance(self, current_price, current_short_position_size):
pool_delta = self.get_pool_delta(current_price)
# --- Over-Hedge Logic ---
overhedge_pct = 0.0
range_width = self.high_range - self.low_range
if range_width > 0:
price_pct = (current_price - self.low_range) / range_width
# If below 0.8 (80%) of range
if price_pct < 0.8:
# Formula: 0.75% boost for every 0.1 drop below 0.8
# Example: At 0.6 (60%), diff is 0.2. (0.2/0.1)*0.0075 = 0.015 (1.5%)
overhedge_pct = ((0.8 - max(0.0, price_pct)) / 0.1) * 0.0075
raw_target_short = pool_delta + self.static_long
target_short_size = raw_target_short
# Apply Boost
adjusted_target_short = raw_target_short * (1.0 + overhedge_pct)
target_short_size = adjusted_target_short
diff = target_short_size - abs(current_short_position_size)
return {
@ -215,7 +279,8 @@ class HyperliquidStrategy:
"current_short": abs(current_short_position_size),
"diff": diff,
"action": "SELL" if diff > 0 else "BUY",
"mode": "NORMAL"
"mode": "OVERHEDGE" if overhedge_pct > 0 else "NORMAL",
"overhedge_pct": overhedge_pct
}
class ScalperHedger:
@ -242,6 +307,9 @@ class ScalperHedger:
self.active_position_id = None
self.active_order = None
# --- Start Uniswap Monitor ---
self.uni_monitor = UniswapPriceMonitor(RPC_URL, UNISWAP_POOL_ADDRESS)
logging.info(f"Scalper Hedger initialized. Agent: {self.account.address}")
def _init_strategy(self, position_data):
@ -553,10 +621,23 @@ class ScalperHedger:
current_pos_size = pos_data['size']
current_pnl = pos_data['pnl']
# --- SPREAD MONITOR LOG ---
uni_price = self.uni_monitor.get_price()
spread_text = ""
if uni_price:
diff = price - uni_price
pct = (diff / uni_price) * 100
spread_text = f" | Sprd: {pct:+.2f}% (H:{price:.0f}/U:{uni_price:.0f})"
# 3. Calculate Logic
calc = self.strategy.calculate_rebalance(price, current_pos_size)
diff_abs = abs(calc['diff'])
# --- LOGGING OVERHEDGE ---
oh_text = ""
if calc.get('overhedge_pct', 0) > 0:
oh_text = f" | 🔥 OH: +{calc['overhedge_pct']*100:.2f}%"
# 4. Dynamic Threshold Calculation
sqrt_Pa = math.sqrt(self.strategy.low_range)
sqrt_Pb = math.sqrt(self.strategy.high_range)
@ -571,23 +652,32 @@ class ScalperHedger:
range_width = clp_high_range - clp_low_range
# Calculate Prices for Zones
zone_bottom_limit_price = clp_low_range + (range_width * ZONE_BOTTOM_HEDGE_LIMIT)
zone_close_bottom_price = clp_low_range + (range_width * ZONE_CLOSE_START)
zone_close_top_price = clp_low_range + (range_width * ZONE_CLOSE_END)
zone_top_start_price = clp_low_range + (range_width * ZONE_TOP_HEDGE_START)
# If config > 9, set to None (Disabled Zone)
zone_bottom_limit_price = (clp_low_range + (range_width * ZONE_BOTTOM_HEDGE_LIMIT)) if ZONE_BOTTOM_HEDGE_LIMIT <= 9 else None
zone_close_bottom_price = (clp_low_range + (range_width * ZONE_CLOSE_START)) if ZONE_CLOSE_START <= 9 else None
zone_close_top_price = (clp_low_range + (range_width * ZONE_CLOSE_END)) if ZONE_CLOSE_END <= 9 else None
zone_top_start_price = (clp_low_range + (range_width * ZONE_TOP_HEDGE_START)) if ZONE_TOP_HEDGE_START <= 9 else None
# Update JSON with zone prices if they are None (initially set by uniswap_manager.py)
if active_pos.get('zone_bottom_limit_price') is None:
update_position_zones_in_json(active_pos['token_id'], {
'zone_top_start_price': round(zone_top_start_price, 2),
'zone_close_top_price': round(zone_close_top_price, 2),
'zone_close_bottom_price': round(zone_close_bottom_price, 2),
'zone_bottom_limit_price': round(zone_bottom_limit_price, 2)
'zone_top_start_price': round(zone_top_start_price, 2) if zone_top_start_price else None,
'zone_close_top_price': round(zone_close_top_price, 2) if zone_close_top_price else None,
'zone_close_bottom_price': round(zone_close_bottom_price, 2) if zone_close_bottom_price else None,
'zone_bottom_limit_price': round(zone_bottom_limit_price, 2) if zone_bottom_limit_price else None
})
# Check Zones
in_close_zone = (price >= zone_close_bottom_price and price <= zone_close_top_price)
in_hedge_zone = (price <= zone_bottom_limit_price) or (price >= zone_top_start_price)
# Check Zones (Handle None)
# If zone price is None, condition fails safe (False)
in_close_zone = False
if zone_close_bottom_price is not None and zone_close_top_price is not None:
in_close_zone = (price >= zone_close_bottom_price and price <= zone_close_top_price)
in_hedge_zone = False
if zone_bottom_limit_price is not None and price <= zone_bottom_limit_price:
in_hedge_zone = True
if zone_top_start_price is not None and price >= zone_top_start_price:
in_hedge_zone = True
# --- Execute Logic ---
if in_close_zone:
@ -601,18 +691,12 @@ class ScalperHedger:
if diff_abs > rebalance_threshold:
trade_size = round_to_sz_decimals(diff_abs, self.sz_decimals)
# --- SOFT START LOGIC (Bottom Zone Only) ---
# If in Bottom Zone, opening a NEW Short (SELL), and current position is 0 -> Cut size by 50%
if (price <= zone_bottom_limit_price) and (current_pos_size == 0) and (calc['action'] == "SELL"):
logging.info(f"🔰 SOFT START: Reducing initial hedge size by 50% in Bottom Zone.")
trade_size = round_to_sz_decimals(trade_size * 0.5, self.sz_decimals)
min_trade_size = MIN_ORDER_VALUE_USD / price
if trade_size < min_trade_size:
logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${MIN_ORDER_VALUE_USD:.2f}). PNL: ${current_pnl:.2f}")
logging.info(f"Idle. Trade size {trade_size} < Min Order Size {min_trade_size:.4f} (${MIN_ORDER_VALUE_USD:.2f}). PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
elif trade_size > 0:
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone. PNL: ${current_pnl:.2f}")
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f}). In Hedge Zone. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
# Execute Passively for Alo
# Force 1 tick offset (0.1) away from BBO to ensure rounding doesn't cause cross
# Sell at Ask + 0.1, Buy at Bid - 0.1
@ -627,14 +711,14 @@ class ScalperHedger:
self.place_limit_order(COIN_SYMBOL, is_buy, trade_size, exec_price)
else:
logging.info(f"Trade size rounds to 0. Skipping. PNL: ${current_pnl:.2f}")
logging.info(f"Trade size rounds to 0. Skipping. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
else:
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone. PNL: ${current_pnl:.2f}")
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}. In Hedge Zone. PNL: ${current_pnl:.2f}{spread_text}{oh_text}")
else:
# MIDDLE ZONE (IDLE)
pct_position = (price - clp_low_range) / range_width
logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). PNL: ${current_pnl:.2f}. No Actions.")
logging.info(f"Idle. In Middle Zone ({pct_position*100:.1f}%). PNL: ${current_pnl:.2f}{spread_text}{oh_text}. No Actions.")
time.sleep(CHECK_INTERVAL)

View File

@ -454,7 +454,7 @@
"type": "AUTOMATIC",
"token_id": 5158011,
"opened": "03:32 15/12/25",
"status": "OPEN",
"status": "CLOSED",
"entry_price": 3135.31,
"target_value": 1983.24,
"amount0_initial": 0.3009,
@ -467,6 +467,153 @@
"range_lower": 3102.62,
"static_long": 0.0,
"timestamp_open": 1765765971,
"timestamp_close": 1765794574,
"fees_collected_usd": 6.69,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5158409,
"opened": "11:37 15/12/25",
"status": "CLOSED",
"entry_price": 3166.4,
"target_value": 1921.57,
"amount0_initial": 0.2816,
"amount1_initial": 1029.9,
"range_upper": 3197.1,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 3228.75,
"range_lower": 3133.8,
"static_long": 0.0,
"timestamp_open": 1765795041,
"timestamp_close": 1765808903,
"fees_collected_usd": 4.36,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5158857,
"opened": "15:36 15/12/25",
"status": "CLOSED",
"entry_price": 3127.7,
"target_value": 1956.0,
"amount0_initial": 0.2889,
"amount1_initial": 1052.48,
"range_upper": 3155.81,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 3185.51,
"range_lower": 3096.42,
"static_long": 0.0,
"timestamp_open": 1765809371,
"timestamp_close": 1765810294,
"fees_collected_usd": 3.06,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5158950,
"opened": "15:59 15/12/25",
"status": "CLOSED",
"entry_price": 3054.98,
"target_value": 1973.85,
"amount0_initial": 0.3079,
"amount1_initial": 1033.2,
"range_upper": 3099.51,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 3099.51,
"range_lower": 3007.91,
"static_long": 0.0,
"timestamp_open": 1765810753,
"timestamp_close": 1765812125,
"fees_collected_usd": 4.94,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5159085,
"opened": "16:29 15/12/25",
"status": "CLOSED",
"entry_price": 3003.17,
"target_value": 1985.39,
"amount0_initial": 0.3193,
"amount1_initial": 1026.56,
"range_upper": 3047.27,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 3047.27,
"range_lower": 2957.21,
"static_long": 0.0,
"timestamp_open": 1765812592,
"timestamp_close": 1765820307,
"fees_collected_usd": 9.28,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5159604,
"opened": "18:46 15/12/25",
"status": "CLOSED",
"entry_price": 2956.0,
"target_value": 1977.09,
"amount0_initial": 0.3271,
"amount1_initial": 1010.26,
"range_upper": 2998.9,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 2998.9,
"range_lower": 2910.28,
"static_long": 0.0,
"timestamp_open": 1765820775,
"timestamp_close": 1765860714,
"fees_collected_usd": 20.27,
"closed_position_value_usd": 0.0
},
{
"type": "AUTOMATIC",
"token_id": 5160824,
"opened": "05:59 16/12/25",
"status": "CLOSED",
"entry_price": 2917.24,
"target_value": 1989.32,
"amount0_initial": 0.3323,
"amount1_initial": 1019.88,
"range_upper": 2960.17,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 2960.17,
"range_lower": 2872.69,
"static_long": 0.0,
"timestamp_open": 1765861181,
"timestamp_close": null
},
{
"type": "AUTOMATIC",
"token_id": 5161116,
"opened": "09:37 16/12/25",
"status": "CLOSED",
"entry_price": 2931.06,
"target_value": 199.06,
"amount0_initial": 0.0327,
"amount1_initial": 103.33,
"range_upper": 2939.53,
"zone_top_start_price": null,
"zone_close_top_price": null,
"zone_close_bottom_price": null,
"zone_bottom_limit_price": 2939.53,
"range_lower": 2921.94,
"static_long": 0.0,
"timestamp_open": 1765874274,
"timestamp_close": 1765881607,
"fees_collected_usd": 0.7,
"closed_position_value_usd": 0.0
}
]

View File

@ -76,7 +76,7 @@ RPC_URL = os.environ.get("MAINNET_RPC_URL")
PRIVATE_KEY = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
# Script behavior flags
MONITOR_INTERVAL_SECONDS = 451
MONITOR_INTERVAL_SECONDS = 120
COLLECT_FEES_ENABLED = False # If True, will attempt to collect fees once and exit if no open auto position
CLOSE_POSITION_ENABLED = True # If True, will attempt to close auto position when out of range
CLOSE_IF_OUT_OF_RANGE_ONLY = True # If True, closes only if out of range; if False, closes immediately
@ -84,8 +84,8 @@ OPEN_POSITION_ENABLED = True # If True, will open a new position if no auto posi
REBALANCE_ON_CLOSE_BELOW_RANGE = False # If True, will sell 50% of WETH to USDC when closing below range
# New Position Parameters
TARGET_INVESTMENT_VALUE_TOKEN1 = 2000.0 # Target total investment value in Token1 terms (e.g. 350 USDC)
RANGE_WIDTH_PCT = 0.01 # +/- 2% range for new positions
TARGET_INVESTMENT_VALUE_TOKEN1 = 200 # Target total investment value in Token1 terms (e.g. 350 USDC)
RANGE_WIDTH_PCT = 0.003 # +/- 2% range for new positions
# JSON File for tracking position state
STATUS_FILE = "hedge_status.json"
@ -153,7 +153,7 @@ def update_hedge_status_file(action, position_data):
"opened": opened_str,
"status": "OPEN",
"entry_price": round(position_data['entry_price'], 2),
"target_value": round(position_data.get('target_value', 0.0), 2),
"target_value": round(position_data['target_value'], 2), # Use actual calculated value
"amount0_initial": fmt_amt0,
"amount1_initial": fmt_amt1,
@ -175,7 +175,7 @@ def update_hedge_status_file(action, position_data):
current_data.append(new_entry)
print(f"Recorded new AUTOMATIC position {position_data['token_id']} in {STATUS_FILE}")
elif action == "CLOSE":
elif action == "CLOSING":
found = False
for entry in current_data:
if (
@ -183,9 +183,31 @@ def update_hedge_status_file(action, position_data):
entry.get('status') == "OPEN" and
entry.get('token_id') == position_data['token_id']
):
entry['status'] = "CLOSING"
found = True
print(f"Marked position {entry['token_id']} as CLOSING in {STATUS_FILE}")
break
if not found:
print(f"WARNING: Could not find open AUTOMATIC position {position_data['token_id']} to mark closing.")
elif action == "CLOSE":
found = False
for entry in current_data:
if (
entry.get('type') == "AUTOMATIC" and
(entry.get('status') == "OPEN" or entry.get('status') == "CLOSING") and
entry.get('token_id') == position_data['token_id']
):
entry['status'] = "CLOSED"
entry['timestamp_close'] = int(time.time())
# Add Closing Stats if provided
if 'fees_collected_usd' in position_data:
entry['fees_collected_usd'] = round(position_data['fees_collected_usd'], 2)
if 'closed_position_value_usd' in position_data:
entry['closed_position_value_usd'] = round(position_data['closed_position_value_usd'], 2)
found = True
print(f"Marked position {entry['token_id']} as CLOSED in {STATUS_FILE}")
break
@ -669,11 +691,36 @@ def main():
print(f"⚠️ Automatic Position {token_id} is OUT OF RANGE! Initiating Close...")
liq = pos_details['liquidity']
if liq > 0:
if decrease_liquidity(w3, npm_contract, account, token_id, liq):
time.sleep(5)
collect_fees(w3, npm_contract, account, token_id)
update_hedge_status_file("CLOSE", {'token_id': token_id})
print("Position Closed & Status Updated.")
# Mark as CLOSING immediately to notify Hedger
update_hedge_status_file("CLOSING", {'token_id': token_id})
# Capture Balances Before Close
b0_start, b1_start = get_token_balances(w3, account.address, pos_details['token0_address'], pos_details['token1_address'])
# Execute Close
decrease_success = decrease_liquidity(w3, npm_contract, account, token_id, liq)
time.sleep(2)
collect_fees(w3, npm_contract, account, token_id)
if decrease_success:
# Capture Balances After Close
b0_end, b1_end = get_token_balances(w3, account.address, pos_details['token0_address'], pos_details['token1_address'])
# Calculate Deltas (Principal + Fees)
delta0 = from_wei(b0_end - b0_start, pos_details['token0_decimals'])
delta1 = from_wei(b1_end - b1_start, pos_details['token1_decimals'])
# Calculate Values
total_exit_usd = (delta0 * current_price) + delta1
# We calculated total_fees_usd earlier in the loop
update_data = {
'token_id': token_id,
'fees_collected_usd': total_fees_usd,
'closed_position_value_usd': total_exit_usd
}
update_hedge_status_file("CLOSE", update_data)
print(f"Position Closed. Value: ${total_exit_usd:.2f}, Fees: ${total_fees_usd:.2f}")
# --- REBALANCE ON CLOSE (If Price Dropped) ---
if REBALANCE_ON_CLOSE_BELOW_RANGE and status_str == "OUT OF RANGE (BELOW)":
@ -715,7 +762,7 @@ def main():
else:
print("Liquidity 0. Marking closed.")
update_hedge_status_file("CLOSE", {'token_id': token_id})
update_hedge_status_file("CLOSE", {'token_id': token_id, 'fees_collected_usd': 0.0, 'closed_position_value_usd': 0.0})
# 2. Opening Logic (If no active automatic position)
if not active_automatic_position and OPEN_POSITION_ENABLED: