diff --git a/clp_hedger/CLP_HEDGING_IMPLEMENTATION_PLAN.md b/clp_hedger/CLP_HEDGING_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..8ed21df --- /dev/null +++ b/clp_hedger/CLP_HEDGING_IMPLEMENTATION_PLAN.md @@ -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.* \ No newline at end of file diff --git a/clp_hedger/clp_scalper_hedger.py b/clp_hedger/clp_scalper_hedger.py index 811f6a4..e5939a1 100644 --- a/clp_hedger/clp_scalper_hedger.py +++ b/clp_hedger/clp_scalper_hedger.py @@ -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) diff --git a/clp_hedger/hedge_status.json b/clp_hedger/hedge_status.json index 87efb3f..99871d2 100644 --- a/clp_hedger/hedge_status.json +++ b/clp_hedger/hedge_status.json @@ -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 } ] \ No newline at end of file diff --git a/clp_hedger/uniswap_manager.py b/clp_hedger/uniswap_manager.py index c4aca59..8a21781 100644 --- a/clp_hedger/uniswap_manager.py +++ b/clp_hedger/uniswap_manager.py @@ -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: diff --git a/clp_hedger_auto/AGENTS.md b/clp_hedger_auto/AGENTS.md new file mode 100644 index 0000000..c20a0f5 --- /dev/null +++ b/clp_hedger_auto/AGENTS.md @@ -0,0 +1,53 @@ +# AGENTS.md - CLP Hedger Project Guide + +## Development Commands + +### Installation +```bash +pip install -r requirements.txt +``` + +### Running the Application +```bash +# Main hedger bot +python clp_hedger.py + +# Development with debug logging +python -c "from logging_utils import setup_logging; setup_logging('debug', 'CLP_HEDGER'); import clp_hedger" +``` + +### Testing +No formal test framework. Manual testing: +```bash +# Check configuration +python -c "import clp_hedger; print(clp_hedger.get_manual_position_config())" +``` + +## Code Style Guidelines + +### Imports +- Order: standard library → third-party → local modules +- Add project root to sys.path for local imports +- Use absolute imports from project root + +### Environment & Logging +- Use `.env` files with python-dotenv +- Use `setup_logging("normal"/"debug", "MODULE_NAME")` convention +- Include emojis: 🚀, ✅, ⚡, 🔄 + +### Architecture +- PascalCase classes (HyperliquidStrategy, CLPHedger) +- Private methods start with underscore (_init_strategy) +- Module-level constants: UPPER_SNAKE_CASE +- Functions/variables: snake_case + +### Error Handling +- Wrap API calls in try/except blocks +- Log errors with context +- Return None/0.0 for non-critical failures +- Use sys.exit(1) for critical failures + +### Mathematical Operations +- Use math.sqrt() for square roots +- Implement proper rounding for API requirements +- Handle floating-point precision appropriately \ No newline at end of file diff --git a/clp_hedger/clp_hedger.py b/clp_hedger_auto/clp_hedger.py similarity index 96% rename from clp_hedger/clp_hedger.py rename to clp_hedger_auto/clp_hedger.py index a71c5ef..6cddd91 100644 --- a/clp_hedger/clp_hedger.py +++ b/clp_hedger_auto/clp_hedger.py @@ -36,7 +36,7 @@ LEVERAGE = 5 STATUS_FILE = "hedge_status.json" # Gap Recovery Configuration -PRICE_BUFFER_PCT = 0.004 # 0.5% buffer to prevent churn +PRICE_BUFFER_PCT = 0.002 # 0.5% buffer to prevent churn TIME_BUFFER_SECONDS = 120 # 2 minutes wait between mode switches def get_manual_position_config(): @@ -181,7 +181,7 @@ def round_to_sig_figs(x, sig_figs=5): class CLPHedger: def __init__(self): - self.private_key = os.environ.get("HEDGER_PRIVATE_KEY") or os.environ.get("AGENT_PRIVATE_KEY") + self.private_key = os.environ.get("SWING_AGENT_PK") self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS") if not self.private_key: @@ -351,13 +351,13 @@ class CLPHedger: logging.info("Attempting to close all open positions...") try: # 1. Get latest price - price = self.get_market_price(COIN_SYMBOL) + price = self.get_market_price(self.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) + current_pos = self.get_current_position(self.coin_symbol) if current_pos == 0: logging.info("No open positions to close.") return @@ -375,10 +375,10 @@ class CLPHedger: 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}") + logging.info(f"Closing Position: {current_pos} {self.coin_symbol} -> Action: {'BUY' if is_buy else 'SELL'} {final_size}") # 4. Execute - self.execute_trade(COIN_SYMBOL, is_buy, final_size, price) + self.execute_trade(self.coin_symbol, is_buy, final_size, price) except Exception as e: logging.error(f"Error during close_all_positions: {e}") @@ -415,14 +415,14 @@ class CLPHedger: continue # 1. Get Data - price = self.get_market_price(COIN_SYMBOL) + price = self.get_market_price(self.coin_symbol) if price is None: time.sleep(5) continue - funding_rate = self.get_funding_rate(COIN_SYMBOL) + funding_rate = self.get_funding_rate(self.coin_symbol) - current_pos_size = self.get_current_position(COIN_SYMBOL) + current_pos_size = self.get_current_position(self.coin_symbol) # 2. Calculate Logic # Pass raw size (e.g. -1.5). The strategy handles the logic. @@ -450,7 +450,7 @@ class CLPHedger: 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) + self.execute_trade(self.coin_symbol, is_buy, trade_size, price) else: logging.info("Trade size rounds to 0. Skipping.") diff --git a/clp_hedger_auto/hedge_status.json b/clp_hedger_auto/hedge_status.json new file mode 100644 index 0000000..2098378 --- /dev/null +++ b/clp_hedger_auto/hedge_status.json @@ -0,0 +1,18 @@ +[ + { + "type": "MANUAL", + "token_id": 5147464, + "status": "OPEN", + "hedge_enabled": true, + "coin_symbol": "ETH", + "entry_price": 3332.66, + "range_lower": 2844.11, + "range_upper": 3477.24, + "target_value": 6938.95, + "amount0_initial": 0.45, + "amount1_initial": 5439.23, + "static_long": 0.0, + "timestamp_open": 1765575924, + "timestamp_close": null + } +] \ No newline at end of file diff --git a/clp_hedger_auto/working_configuration.md b/clp_hedger_auto/working_configuration.md new file mode 100644 index 0000000..6214f1d --- /dev/null +++ b/clp_hedger_auto/working_configuration.md @@ -0,0 +1,85 @@ +# CLP Hedger - Working Configuration Summary + +## Current Setup Status +✅ **ACTIVE**: Hedger is running and successfully trading on Hyperliquid + +## Position Configuration (`hedge_status.json`) +```json +{ + "type": "MANUAL", + "token_id": 5147464, + "status": "OPEN", + "hedge_enabled": true, + "coin_symbol": "ETH", + "entry_price": 3332.66, + "range_lower": 2844.11, + "range_upper": 3477.24, + "target_value": 6938.95, + "amount0_initial": 0.45, + "amount1_initial": 5439.23, + "static_long": 0.0, + "timestamp_open": 1765575924, + "timestamp_close": null +} +``` + +## Trading Parameters +- **Coin**: ETH +- **Leverage**: 5x (Cross) +- **Entry Price**: $3,332.66 +- **Price Range**: $2,844.11 - $3,477.24 +- **Position Size**: 0.45 ETH +- **Static Long**: 0% (fully hedged) +- **Target Value**: $6,938.95 + +## Hedger Configuration (`clp_hedger.py`) +- **Check Interval**: 30 seconds +- **Rebalance Threshold**: 0.15 ETH +- **Price Buffer**: 0.2% (prevents churn) +- **Time Buffer**: 120 seconds (between mode switches) +- **Status File**: `hedge_status.json` + +## Strategy Parameters +- **Entry WETH**: 0.45 ETH +- **Low Range**: $2,844.11 +- **High Range**: $3,477.24 +- **Start Price**: $3,332.66 +- **Static Long Ratio**: 0.0 (0% static long exposure) + +## Gap Recovery Settings +- **Current Mode**: NORMAL (100% hedge) +- **Gap Recovery**: Enabled +- **Recovery Target**: Entry price + (2 × Gap) +- **Price Buffer**: 0.2% +- **Mode Switch Delay**: 120 seconds + +## Environment +- **Wallet**: 0xcb262ceaae5d8a99b713f87a43dd18e6be892739 +- **Network**: Hyperliquid Mainnet +- **Logging Level**: Normal +- **Virtual Environment**: Active + +## Last Status +- ✅ API Connection: Working +- ✅ Price Feed: Active +- ✅ Position Tracking: Enabled +- ✅ Hedge Logic: Operational +- ✅ Order Execution: Successful + +## Key Files +- `clp_hedger.py`: Main hedger bot +- `hedge_status.json`: Position configuration +- `.env`: API credentials (not shown for security) + +## Monitoring +The hedger runs a continuous loop every 30 seconds, checking: +1. Current market price +2. Position size deviation +3. Gap recovery conditions +4. Funding rate opportunities +5. Automatic rebalancing needs + +## Operations +- **Normal Mode**: Maintains 100% hedge against ETH exposure +- **Recovery Mode**: Reduces hedge to 0% when gap recovery conditions are met +- **Auto-Rebalancing**: Triggers when position deviates by >0.15 ETH \ No newline at end of file