Compare commits
15 Commits
bdd2d607cd
...
optymaliza
| Author | SHA1 | Date | |
|---|---|---|---|
| e1b3c5814b | |||
| 109ef7cd24 | |||
| b85fcb8246 | |||
| e31079cdbb | |||
| 84242f3654 | |||
| aeaae84750 | |||
| 89b8e53092 | |||
| eaceeb7e3b | |||
| 25e9a22a8e | |||
| 8494583779 | |||
| aea341792e | |||
| e8d7db2743 | |||
| bf88c16383 | |||
| 3000a366be | |||
| f8afdc1ab1 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -30,6 +30,9 @@ sdk/
|
|||||||
# Ignore custom agents directory
|
# Ignore custom agents directory
|
||||||
agents/
|
agents/
|
||||||
|
|
||||||
|
# Ignore temporary files and examples
|
||||||
|
.temp/
|
||||||
|
|
||||||
# Ignore Jekyll files
|
# Ignore Jekyll files
|
||||||
.nojekyll
|
.nojekyll
|
||||||
|
|
||||||
@ -39,3 +42,4 @@ agents/
|
|||||||
.idea/
|
.idea/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
.opencode/
|
||||||
117
.temp/model_comparison_examples.md
Normal file
117
.temp/model_comparison_examples.md
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
# Model Comparison: Session Summary Styles
|
||||||
|
|
||||||
|
## OpenCode Zen (Bigpickle) Style Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-10
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Fix urllib3 SSL compatibility warning and implement sessionsummary agent.
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Resolved NotOpenSSLWarning by downgrading urllib3 from 2.5.0 to 1.26.20
|
||||||
|
* Updated requirements.txt with compatible dependency version
|
||||||
|
* Created sessionsummary agent in .opencode/agent/ following OpenCode.ai specifications
|
||||||
|
* Configured agent with proper permissions and tool restrictions
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Selected urllib3 downgrade over SSL environment upgrade for system stability
|
||||||
|
* Implemented OpenCode.ai markdown agent instead of custom Python solution
|
||||||
|
* Set bash permission to "deny" for security constraints
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `requirements.txt`
|
||||||
|
* `GEMINI.md`
|
||||||
|
* `.opencode/agent/sessionsummary.md`
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Validate trading bot functionality post-SSL fix
|
||||||
|
* Test sessionsummary agent integration with OpenCode.ai interface
|
||||||
|
```
|
||||||
|
|
||||||
|
## Gemini 2.5 Pro Style Example
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-10
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
We worked on resolving a critical SSL compatibility issue with urllib3 that was preventing smooth development, and then created a proper session summary system following OpenCode.ai guidelines to better track our project's progress.
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Successfully diagnosed and fixed the NotOpenSSLWarning that was appearing when using urllib3 v2.5.0 with LibreSSL 2.8.3 on macOS by strategically downgrading to urllib3 v1.26.20, which maintains full compatibility while preserving all required functionality.
|
||||||
|
* Updated the requirements.txt file to lock in the compatible version, preventing future dependency conflicts and ensuring consistent builds across different environments.
|
||||||
|
* Created a comprehensive sessionsummary agent using the OpenCode.ai framework, placing it in the proper .opencode/agent/ directory structure with appropriate permissions, tool configurations, and detailed documentation.
|
||||||
|
* Properly configured the agent with security-conscious settings, denying bash access while allowing write/edit capabilities for documentation updates.
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Made the strategic decision to downgrade urllib3 rather than attempting to upgrade the system SSL environment, as this approach provides better stability and avoids potential system-wide compatibility issues.
|
||||||
|
* Chose to follow OpenCode.ai agent guidelines instead of building a custom Python implementation, ensuring better integration with the development ecosystem and reducing maintenance overhead.
|
||||||
|
* Implemented a security-first approach by configuring restrictive permissions that prevent accidental system modifications while still allowing the agent to fulfill its documentation purpose.
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `requirements.txt` - Updated urllib3 version from 2.5.0 to 1.26.20
|
||||||
|
* `GEMINI.md` - Added structured session summary following the specified format
|
||||||
|
* `.opencode/agent/sessionsummary.md` - Created comprehensive agent configuration with proper OpenCode.ai formatting
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Need to thoroughly test the trading bot functionality to ensure the urllib3 downgrade doesn't introduce any regressions in API connectivity or data processing.
|
||||||
|
* Should verify that the sessionsummary agent works correctly within the OpenCode.ai interface and can be invoked properly using @mentions.
|
||||||
|
* Consider establishing a regular workflow for session documentation to maintain consistent project tracking.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Differences Analysis
|
||||||
|
|
||||||
|
### **Technical Precision vs. Contextual Richness**
|
||||||
|
|
||||||
|
| Aspect | OpenCode Zen (Bigpickle) | Gemini 2.5 Pro |
|
||||||
|
|---------|-------------------------|------------------|
|
||||||
|
| **Length** | Concise, bullet-point focused | More detailed, narrative style |
|
||||||
|
| **Technical Detail** | High precision, version-specific | High precision with explanatory context |
|
||||||
|
| **Decision Rationale** | Brief, action-oriented | Comprehensive, explains "why" |
|
||||||
|
| **Language Style** | Technical, direct | Descriptive, explanatory |
|
||||||
|
| **Context** | Assumes technical audience | Provides background and implications |
|
||||||
|
|
||||||
|
### **When to Choose Each Model**
|
||||||
|
|
||||||
|
#### **OpenCode Zen (Bigpickle) - Best For:**
|
||||||
|
- ✅ **Daily quick summaries** when you need fast, accurate documentation
|
||||||
|
- ✅ **Technical teams** who prefer concise, scannable information
|
||||||
|
- ✅ **API documentation** where precision matters more than narrative
|
||||||
|
- ✅ **Time-sensitive sessions** where speed is important
|
||||||
|
- ✅ **Highly technical work** where context is already understood
|
||||||
|
|
||||||
|
#### **Gemini 2.5 Pro - Best For:**
|
||||||
|
- ✅ **Complex sessions** with multiple decision points
|
||||||
|
- ✅ **Learning/documentation** where context helps future understanding
|
||||||
|
- ✅ **Team collaboration** where others need full background
|
||||||
|
- ✅ **Strategic planning** where rationale is crucial
|
||||||
|
- ✅ **Knowledge transfer** when onboarding new developers
|
||||||
|
|
||||||
|
### **Practical Impact on Your Use Case**
|
||||||
|
|
||||||
|
For your **Hyperliquid trading bot project**, consider:
|
||||||
|
|
||||||
|
1. **Quick bug fixes**: Use OpenCode Zen for fast, precise documentation
|
||||||
|
2. **Strategy development**: Use Gemini 2.5 Pro for detailed decision tracking
|
||||||
|
3. **Performance optimization**: Gemini 2.5 Pro to document complex trade-offs
|
||||||
|
4. **Daily maintenance**: OpenCode Zen for efficient progress tracking
|
||||||
|
5. **Architecture changes**: Gemini 2.5 Pro for comprehensive rationale
|
||||||
|
|
||||||
|
### **Recommendation**
|
||||||
|
|
||||||
|
**Use OpenCode Zen (Bigpickle) as your default** for:
|
||||||
|
- Day-to-day development
|
||||||
|
- Bug fixes and small features
|
||||||
|
- Technical documentation
|
||||||
|
|
||||||
|
**Switch to Gemini 2.5 Pro for:**
|
||||||
|
- Major architectural decisions
|
||||||
|
- Complex problem-solving sessions
|
||||||
|
- Strategic planning
|
||||||
|
- When creating comprehensive documentation
|
||||||
|
|
||||||
|
The sessionsummary agent will work with both models - just expect different levels of detail and narrative style based on the model's strengths!
|
||||||
119
GEMINI.md
119
GEMINI.md
@ -46,6 +46,125 @@ python main_app.py
|
|||||||
* **Strategies:** Custom strategies should inherit from the `BaseStrategy` class (defined in `strategies/base_strategy.py`) and implement the `calculate_signals` method.
|
* **Strategies:** Custom strategies should inherit from the `BaseStrategy` class (defined in `strategies/base_strategy.py`) and implement the `calculate_signals` method.
|
||||||
* **Documentation:** The `WIKI/` directory contains detailed documentation for the project. Start with `WIKI/SUMMARY.md`.
|
* **Documentation:** The `WIKI/` directory contains detailed documentation for the project. Start with `WIKI/SUMMARY.md`.
|
||||||
|
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-10
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Fix urllib3 SSL compatibility warning and create sessionsummary agent following OpenCode.ai guidelines
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Resolved NotOpenSSLWarning by downgrading urllib3 from 2.5.0 to 1.26.20
|
||||||
|
* Updated requirements.txt to prevent future SSL compatibility issues
|
||||||
|
* Created sessionsummary agent in .opencode/agent/ following OpenCode.ai specifications
|
||||||
|
* Removed incorrect Python implementation and created proper markdown agent configuration
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Chose to downgrade urllib3 instead of upgrading SSL environment for stability
|
||||||
|
* Followed OpenCode.ai agent guidelines instead of creating custom Python implementation
|
||||||
|
* Configured sessionsummary as subagent with proper permissions and tools
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `requirements.txt`
|
||||||
|
* `GEMINI.md`
|
||||||
|
* `.opencode/agent/sessionsummary.md`
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Test trading bot functionality after SSL fix to ensure no regressions
|
||||||
|
* Integrate sessionsummary agent into regular development workflow
|
||||||
|
* Add .opencode/ to .gitignore if not already present
|
||||||
|
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-11
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Start new Gemini session and organize project files by creating .temp folder for examples and temporary files
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Created .temp folder for organizing examples and temporary files
|
||||||
|
* Updated .gitignore to include .temp/ directory
|
||||||
|
* Moved model_comparison_examples.md to .temp folder for better organization
|
||||||
|
* Established file management practices for future development
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Chose to use .temp folder instead of mixing examples with main project files
|
||||||
|
* Added .temp to .gitignore to prevent accidental commits of temporary files
|
||||||
|
* Followed user instruction to organize project structure for better maintainability
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `.gitignore`
|
||||||
|
* `.temp/` (created)
|
||||||
|
* `model_comparison_examples.md` (moved to .temp/)
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Continue organizing any other example or temporary files into .temp folder
|
||||||
|
* Maintain consistent file organization practices in future development
|
||||||
|
* Consider creating additional organizational directories if needed
|
||||||
|
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-11
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Fix DashboardDataFetcher path resolution error causing file operation failures
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Identified root cause of file path error in dashboard_data_fetcher.py subprocess execution
|
||||||
|
* Fixed path resolution by using absolute paths instead of relative paths
|
||||||
|
* Added os.makedirs() call to ensure _logs directory exists before file operations
|
||||||
|
* Tested fix and confirmed DashboardDataFetcher now works correctly
|
||||||
|
* Committed and pushed fix to remote repository
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Used os.path.dirname(os.path.abspath(__file__)) to get correct project root
|
||||||
|
* Ensured backward compatibility while fixing the path resolution issue
|
||||||
|
* Maintained atomic file write pattern for data integrity
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `dashboard_data_fetcher.py`
|
||||||
|
* `GEMINI.md`
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Monitor DashboardDataFetcher to ensure no further path-related errors occur
|
||||||
|
* Consider reviewing other subprocess scripts for similar path resolution issues
|
||||||
|
* Test main_app.py to ensure dashboard displays data correctly
|
||||||
|
|
||||||
|
## Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-11-11
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Debug and fix DashboardDataFetcher path resolution error causing file operation failures
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Identified root cause of file path error in dashboard_data_fetcher.py subprocess execution
|
||||||
|
* Fixed path resolution by using absolute paths instead of relative paths
|
||||||
|
* Added os.makedirs() call to ensure _logs directory exists before file operations
|
||||||
|
* Tested fix and confirmed DashboardDataFetcher now works correctly
|
||||||
|
* Committed and pushed fix to remote repository
|
||||||
|
* Organized project files with .temp folder for better structure
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Used os.path.dirname(os.path.abspath(__file__)) to get correct project root
|
||||||
|
* Ensured backward compatibility while fixing path resolution issue
|
||||||
|
* Maintained atomic file write pattern for data integrity
|
||||||
|
* Added proper directory existence checks to prevent runtime errors
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `dashboard_data_fetcher.py`
|
||||||
|
* `GEMINI.md`
|
||||||
|
* `.gitignore`
|
||||||
|
* `.temp/` (created)
|
||||||
|
|
||||||
|
**Next Steps/Open Questions:**
|
||||||
|
* Monitor DashboardDataFetcher to ensure no further path-related errors occur
|
||||||
|
* Consider reviewing other subprocess scripts for similar path resolution issues
|
||||||
|
* Test main_app.py to ensure dashboard displays data correctly
|
||||||
|
* Continue improving project organization and file management practices
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Project Review and Recommendations
|
# Project Review and Recommendations
|
||||||
|
|
||||||
This review provides an analysis of the current state of the automated trading bot project, proposes specific code improvements, and identifies files that appear to be unused or are one-off utilities that could be reorganized.
|
This review provides an analysis of the current state of the automated trading bot project, proposes specific code improvements, and identifies files that appear to be unused or are one-off utilities that could be reorganized.
|
||||||
|
|||||||
@ -1,300 +0,0 @@
|
|||||||
# Improvement Roadmap - Hyperliquid Trading Bot
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
This document outlines the detailed implementation plan for transforming the trading bot into a production-ready system.
|
|
||||||
|
|
||||||
## Phase 1: Foundation (Weeks 1-2)
|
|
||||||
|
|
||||||
### Week 1: Security & Stability
|
|
||||||
|
|
||||||
#### Day 1-2: Critical Security Fixes
|
|
||||||
- [ ] **Implement Encrypted Key Storage**
|
|
||||||
- Create `security/key_manager.py`
|
|
||||||
- Replace environment variable key access
|
|
||||||
- Add key rotation mechanism
|
|
||||||
- **Files**: `trade_executor.py`, `create_agent.py`
|
|
||||||
|
|
||||||
- [ ] **Add Input Validation Framework**
|
|
||||||
- Create `validation/trading_validator.py`
|
|
||||||
- Validate all trading parameters
|
|
||||||
- Add sanitization for user inputs
|
|
||||||
- **Files**: `position_manager.py`, `trade_executor.py`
|
|
||||||
|
|
||||||
#### Day 3-4: Risk Management
|
|
||||||
- [ ] **Implement Circuit Breakers**
|
|
||||||
- Create `risk/circuit_breaker.py`
|
|
||||||
- Add trading halt conditions
|
|
||||||
- Implement automatic recovery
|
|
||||||
- **Files**: `trade_executor.py`, `position_manager.py`
|
|
||||||
|
|
||||||
- [ ] **Fix Import Resolution Issues**
|
|
||||||
- Update relative imports
|
|
||||||
- Add `__init__.py` files where missing
|
|
||||||
- Test all module imports
|
|
||||||
- **Files**: `main_app.py`, all strategy files
|
|
||||||
|
|
||||||
#### Day 5-7: Code Quality
|
|
||||||
- [ ] **Refactor Dashboard Display**
|
|
||||||
- Extract `DashboardRenderer` class
|
|
||||||
- Split into market/strategy/position components
|
|
||||||
- Add configuration for display options
|
|
||||||
- **Files**: `main_app.py`
|
|
||||||
|
|
||||||
### Week 2: Configuration & Error Handling
|
|
||||||
|
|
||||||
#### Day 8-9: Configuration Management
|
|
||||||
- [ ] **Create Centralized Configuration**
|
|
||||||
- Create `config/settings.py`
|
|
||||||
- Move all magic numbers to config
|
|
||||||
- Add environment-specific configs
|
|
||||||
- **Files**: All Python files
|
|
||||||
|
|
||||||
- [ ] **Standardize Error Handling**
|
|
||||||
- Create `utils/error_handlers.py`
|
|
||||||
- Implement retry decorators
|
|
||||||
- Add structured exception classes
|
|
||||||
- **Files**: All core modules
|
|
||||||
|
|
||||||
#### Day 10-12: Database Improvements
|
|
||||||
- [ ] **Implement Connection Pool**
|
|
||||||
- Create `database/connection_pool.py`
|
|
||||||
- Replace direct SQLite connections
|
|
||||||
- Add connection health monitoring
|
|
||||||
- **Files**: `base_strategy.py`, all data access files
|
|
||||||
|
|
||||||
- [ ] **Add Database Migrations**
|
|
||||||
- Create `database/migrations/`
|
|
||||||
- Version control schema changes
|
|
||||||
- Add rollback capabilities
|
|
||||||
- **Files**: Database schema files
|
|
||||||
|
|
||||||
#### Day 13-14: Basic Testing
|
|
||||||
- [ ] **Create Test Framework**
|
|
||||||
- Set up `tests/` directory structure
|
|
||||||
- Add pytest configuration
|
|
||||||
- Create test fixtures and mocks
|
|
||||||
- **Files**: New test files
|
|
||||||
|
|
||||||
## Phase 2: Performance & Testing (Weeks 3-4)
|
|
||||||
|
|
||||||
### Week 3: Performance Optimization
|
|
||||||
|
|
||||||
#### Day 15-17: Caching Layer
|
|
||||||
- [ ] **Implement Redis/Memory Cache**
|
|
||||||
- Create `cache/cache_manager.py`
|
|
||||||
- Cache frequently accessed data
|
|
||||||
- Add cache invalidation logic
|
|
||||||
- **Files**: `data_fetcher.py`, `base_strategy.py`
|
|
||||||
|
|
||||||
#### Day 18-19: Async Operations
|
|
||||||
- [ ] **Convert to Async/Await**
|
|
||||||
- Identify blocking operations
|
|
||||||
- Convert to async patterns
|
|
||||||
- Add async context managers
|
|
||||||
- **Files**: `live_market_utils.py`, API calls
|
|
||||||
|
|
||||||
#### Day 20-21: Batch Processing
|
|
||||||
- [ ] **Implement Batch Operations**
|
|
||||||
- Batch database writes
|
|
||||||
- Bulk API requests
|
|
||||||
- Optimize data processing
|
|
||||||
- **Files**: Data processing modules
|
|
||||||
|
|
||||||
### Week 4: Testing Framework
|
|
||||||
|
|
||||||
#### Day 22-24: Unit Tests
|
|
||||||
- [ ] **Comprehensive Unit Test Suite**
|
|
||||||
- Test all core classes
|
|
||||||
- Mock external dependencies
|
|
||||||
- Achieve >80% coverage
|
|
||||||
- **Files**: `tests/unit/`
|
|
||||||
|
|
||||||
#### Day 25-26: Integration Tests
|
|
||||||
- [ ] **End-to-End Testing**
|
|
||||||
- Test complete workflows
|
|
||||||
- Mock Hyperliquid API
|
|
||||||
- Test process communication
|
|
||||||
- **Files**: `tests/integration/`
|
|
||||||
|
|
||||||
#### Day 27-28: Paper Trading
|
|
||||||
- [ ] **Paper Trading Mode**
|
|
||||||
- Create simulation environment
|
|
||||||
- Mock trade execution
|
|
||||||
- Add performance tracking
|
|
||||||
- **Files**: `trade_executor.py`, new simulation files
|
|
||||||
|
|
||||||
## Phase 3: Monitoring & Observability (Weeks 5-6)
|
|
||||||
|
|
||||||
### Week 5: Metrics & Monitoring
|
|
||||||
|
|
||||||
#### Day 29-31: Metrics Collection
|
|
||||||
- [ ] **Add Prometheus Metrics**
|
|
||||||
- Create `monitoring/metrics.py`
|
|
||||||
- Track key performance indicators
|
|
||||||
- Add custom business metrics
|
|
||||||
- **Files**: All core modules
|
|
||||||
|
|
||||||
#### Day 32-33: Health Checks
|
|
||||||
- [ ] **Health Check System**
|
|
||||||
- Create `monitoring/health_check.py`
|
|
||||||
- Monitor all system components
|
|
||||||
- Add dependency checks
|
|
||||||
- **Files**: `main_app.py`, all processes
|
|
||||||
|
|
||||||
#### Day 34-35: Alerting
|
|
||||||
- [ ] **Alerting System**
|
|
||||||
- Create `monitoring/alerts.py`
|
|
||||||
- Configure alert rules
|
|
||||||
- Add notification channels
|
|
||||||
- **Files**: New alerting files
|
|
||||||
|
|
||||||
### Week 6: Documentation & Developer Experience
|
|
||||||
|
|
||||||
#### Day 36-38: API Documentation
|
|
||||||
- [ ] **Auto-Generated Docs**
|
|
||||||
- Set up Sphinx/ MkDocs
|
|
||||||
- Document all public APIs
|
|
||||||
- Add code examples
|
|
||||||
- **Files**: `docs/` directory
|
|
||||||
|
|
||||||
#### Day 39-40: Setup Improvements
|
|
||||||
- [ ] **Interactive Setup**
|
|
||||||
- Create setup wizard
|
|
||||||
- Validate configuration
|
|
||||||
- Add guided configuration
|
|
||||||
- **Files**: `setup.py`, new setup files
|
|
||||||
|
|
||||||
#### Day 41-42: Examples & Guides
|
|
||||||
- [ ] **Strategy Examples**
|
|
||||||
- Create example strategies
|
|
||||||
- Add development tutorials
|
|
||||||
- Document best practices
|
|
||||||
- **Files**: `examples/`, `WIKI/`
|
|
||||||
|
|
||||||
## Phase 4: Advanced Features (Weeks 7-8)
|
|
||||||
|
|
||||||
### Week 7: Advanced Risk Management
|
|
||||||
|
|
||||||
#### Day 43-45: Position Sizing
|
|
||||||
- [ ] **Dynamic Position Sizing**
|
|
||||||
- Volatility-based sizing
|
|
||||||
- Portfolio risk metrics
|
|
||||||
- Kelly criterion implementation
|
|
||||||
- **Files**: `position_manager.py`, new risk modules
|
|
||||||
|
|
||||||
#### Day 46-47: Advanced Orders
|
|
||||||
- [ ] **Advanced Order Types**
|
|
||||||
- Stop-loss orders
|
|
||||||
- Take-profit orders
|
|
||||||
- Conditional orders
|
|
||||||
- **Files**: `trade_executor.py`
|
|
||||||
|
|
||||||
#### Day 48-49: Portfolio Management
|
|
||||||
- [ ] **Portfolio Optimization**
|
|
||||||
- Correlation analysis
|
|
||||||
- Risk parity allocation
|
|
||||||
- Rebalancing logic
|
|
||||||
- **Files**: New portfolio modules
|
|
||||||
|
|
||||||
### Week 8: Production Readiness
|
|
||||||
|
|
||||||
#### Day 50-52: Deployment
|
|
||||||
- [ ] **Production Deployment**
|
|
||||||
- Docker containerization
|
|
||||||
- Kubernetes manifests
|
|
||||||
- CI/CD pipeline
|
|
||||||
- **Files**: `docker/`, `.github/workflows/`
|
|
||||||
|
|
||||||
#### Day 53-54: Performance Profiling
|
|
||||||
- [ ] **Profiling Tools**
|
|
||||||
- Performance monitoring
|
|
||||||
- Memory usage tracking
|
|
||||||
- Bottleneck identification
|
|
||||||
- **Files**: New profiling modules
|
|
||||||
|
|
||||||
#### Day 55-56: Final Polish
|
|
||||||
- [ ] **Production Hardening**
|
|
||||||
- Security audit
|
|
||||||
- Load testing
|
|
||||||
- Documentation review
|
|
||||||
- **Files**: All files
|
|
||||||
|
|
||||||
## Implementation Guidelines
|
|
||||||
|
|
||||||
### Daily Workflow
|
|
||||||
1. **Morning Standup**: Review progress, identify blockers
|
|
||||||
2. **Development**: Focus on assigned tasks
|
|
||||||
3. **Testing**: Write tests alongside code
|
|
||||||
4. **Code Review**: Peer review all changes
|
|
||||||
5. **Documentation**: Update docs with changes
|
|
||||||
|
|
||||||
### Quality Gates
|
|
||||||
- All code must pass linting and formatting
|
|
||||||
- New features require unit tests
|
|
||||||
- Integration tests for critical paths
|
|
||||||
- Security review for sensitive changes
|
|
||||||
|
|
||||||
### Risk Mitigation
|
|
||||||
- Feature flags for new functionality
|
|
||||||
- Gradual rollout with monitoring
|
|
||||||
- Rollback procedures for each change
|
|
||||||
- Regular backup and recovery testing
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
### Phase 1 Success
|
|
||||||
- [ ] All security vulnerabilities fixed
|
|
||||||
- [ ] Import resolution issues resolved
|
|
||||||
- [ ] Basic test framework in place
|
|
||||||
- [ ] Configuration management implemented
|
|
||||||
|
|
||||||
### Phase 2 Success
|
|
||||||
- [ ] Performance improvements measured
|
|
||||||
- [ ] Test coverage >80%
|
|
||||||
- [ ] Paper trading mode functional
|
|
||||||
- [ ] Async operations implemented
|
|
||||||
|
|
||||||
### Phase 3 Success
|
|
||||||
- [ ] Monitoring dashboard operational
|
|
||||||
- [ ] Alerting system functional
|
|
||||||
- [ ] Documentation complete
|
|
||||||
- [ ] Developer experience improved
|
|
||||||
|
|
||||||
### Phase 4 Success
|
|
||||||
- [ ] Production deployment ready
|
|
||||||
- [ ] Advanced features working
|
|
||||||
- [ ] Performance benchmarks met
|
|
||||||
- [ ] Security audit passed
|
|
||||||
|
|
||||||
## Resource Requirements
|
|
||||||
|
|
||||||
### Development Team
|
|
||||||
- **Senior Python Developer**: Lead architecture and security
|
|
||||||
- **Backend Developer**: Performance and database optimization
|
|
||||||
- **DevOps Engineer**: Deployment and monitoring
|
|
||||||
- **QA Engineer**: Testing framework and automation
|
|
||||||
|
|
||||||
### Tools & Services
|
|
||||||
- **Development**: PyCharm/VSCode, Git, Docker
|
|
||||||
- **Testing**: Pytest, Mock, Coverage tools
|
|
||||||
- **Monitoring**: Prometheus, Grafana, AlertManager
|
|
||||||
- **CI/CD**: GitHub Actions, Docker Hub
|
|
||||||
- **Documentation**: Sphinx/MkDocs, ReadTheDocs
|
|
||||||
|
|
||||||
### Infrastructure
|
|
||||||
- **Development**: Local development environment
|
|
||||||
- **Testing**: Staging environment with test data
|
|
||||||
- **Production**: Cloud deployment with monitoring
|
|
||||||
- **Backup**: Automated backup and recovery system
|
|
||||||
|
|
||||||
## Timeline Summary
|
|
||||||
|
|
||||||
| Phase | Duration | Key Deliverables |
|
|
||||||
|-------|----------|------------------|
|
|
||||||
| Phase 1 | 2 weeks | Security fixes, basic testing, configuration |
|
|
||||||
| Phase 2 | 2 weeks | Performance optimization, comprehensive testing |
|
|
||||||
| Phase 3 | 2 weeks | Monitoring, documentation, developer tools |
|
|
||||||
| Phase 4 | 2 weeks | Advanced features, production deployment |
|
|
||||||
| **Total** | **8 weeks** | **Production-ready trading system** |
|
|
||||||
|
|
||||||
This roadmap provides a structured approach to transforming the trading bot into a robust, scalable, and maintainable system suitable for production use.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
"# Comprehensive Project Review and Improvement Proposals"
|
|
||||||
88
README.md
88
README.md
@ -1,88 +0,0 @@
|
|||||||
# Automated Crypto Trading Bot
|
|
||||||
|
|
||||||
This project is a sophisticated, multi-process automated trading bot designed to interact with the Hyperliquid decentralized exchange. It features a robust data pipeline, a flexible strategy engine, multi-agent trade execution, and a live terminal dashboard for real-time monitoring.
|
|
||||||
|
|
||||||
<!-- It's a good idea to take a screenshot of your dashboard and upload it to a service like Imgur to include here -->
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
* **Multi-Process Architecture**: Core components (data fetching, trading, strategies) run in parallel processes for maximum performance and stability.
|
|
||||||
* **Comprehensive Data Pipeline**:
|
|
||||||
* Live price feeds for all assets.
|
|
||||||
* Historical candle data collection for any coin and timeframe.
|
|
||||||
* Historical market cap data fetching from the CoinGecko API.
|
|
||||||
* **High-Performance Database**: Uses SQLite with pandas for fast, indexed storage and retrieval of all market data.
|
|
||||||
* **Configuration-Driven Strategies**: Trading strategies are defined and managed in a simple JSON file (`_data/strategies.json`), allowing for easy configuration without code changes.
|
|
||||||
* **Multi-Agent Trading**: Supports multiple, independent trading agents for advanced risk segregation and PNL tracking.
|
|
||||||
* **Live Terminal Dashboard**: A real-time, flicker-free dashboard to monitor live prices, market caps, strategy signals, and the status of all background processes.
|
|
||||||
* **Secure Key Management**: Uses a `.env` file to securely manage all private keys and API keys, keeping them separate from the codebase.
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
The project is composed of several key scripts that work together:
|
|
||||||
|
|
||||||
* **`main_app.py`**: The central orchestrator. It launches all background processes and displays the main monitoring dashboard.
|
|
||||||
* **`trade_executor.py`**: The trading "brain." It reads signals from all active strategies and executes trades using the appropriate agent.
|
|
||||||
* **`data_fetcher.py`**: A background service that collects 1-minute historical candle data and saves it to the SQLite database.
|
|
||||||
* **`resampler.py`**: A background service that reads the 1-minute data and generates all other required timeframes (e.g., 5m, 1h, 1d).
|
|
||||||
* **`market_cap_fetcher.py`**: A scheduled service to download daily market cap data.
|
|
||||||
* **`strategy_*.py`**: Individual files containing the logic for different types of trading strategies (e.g., SMA Crossover).
|
|
||||||
* **`_data/strategies.json`**: The configuration file for defining and enabling/disabling your trading strategies.
|
|
||||||
* **`.env`**: The secure file for storing all your private keys and API keys.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
1. **Clone the Repository**
|
|
||||||
```bash
|
|
||||||
git clone [https://github.com/your-username/your-repo-name.git](https://github.com/your-username/your-repo-name.git)
|
|
||||||
cd your-repo-name
|
|
||||||
```
|
|
||||||
2. **Create and Activate a Virtual Environment**
|
|
||||||
```bash
|
|
||||||
# For Windows
|
|
||||||
python -m venv .venv
|
|
||||||
.\.venv\Scripts\activate
|
|
||||||
# For macOS/Linux
|
|
||||||
python3 -m venv .venv
|
|
||||||
source .venv/bin/activate
|
|
||||||
```
|
|
||||||
3. **Install Dependencies**
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
## Getting Started: Configuration
|
|
||||||
|
|
||||||
Before running the application, you must configure your wallets, agents, and API keys.
|
|
||||||
|
|
||||||
1. Create the .env File In the root of the project, create a file named .env. Copy the following content into it and replace the placeholder values with your actual keys.
|
|
||||||
|
|
||||||
2. **Activate Your Main Wallet on Hyperliquid**
|
|
||||||
The `trade_executor.py` script will fail if your main wallet is not registered.
|
|
||||||
* Go to the Hyperliquid website, connect your main wallet, and make a small deposit. This is a one-time setup step.
|
|
||||||
3. **Create and Authorize Trading Agents**
|
|
||||||
The `trade_executor.py` uses secure "agent" keys that can trade but cannot withdraw. You need to generate these and authorize them with your main wallet.
|
|
||||||
* Run the `create_agent.py` script
|
|
||||||
```bash
|
|
||||||
python create_agent.py
|
|
||||||
```
|
|
||||||
The script will output a new Agent Private Key. Copy this key and add it to your .env file (e.g., as SCALPER_AGENT_PK). Repeat this for each agent you want to create.
|
|
||||||
4. **Configure**
|
|
||||||
Your Strategies Open the `_data/strategies.json` file to define which strategies you want to run.
|
|
||||||
* Set "enabled": true to activate a strategy.
|
|
||||||
* Assign an "agent" (e.g., "scalper", "swing") to each strategy. The agent name must correspond to a key in your .env file (e.g., SCALPER_AGENT_PK -> "scalper").
|
|
||||||
* Configure the parameters for each strategy, such as the coin, timeframe, and any indicator settings.
|
|
||||||
|
|
||||||
##Usage##
|
|
||||||
Once everything is configured, you can run the main application from your terminal:
|
|
||||||
```bash
|
|
||||||
python main_app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Detailed project documentation is available in the `WIKI/` directory. Start with the summary page:
|
|
||||||
|
|
||||||
`WIKI/SUMMARY.md`
|
|
||||||
|
|
||||||
This contains links and explanations for `OVERVIEW.md`, `SETUP.md`, `SCRIPTS.md`, and other helpful pages that describe usage, data layout, agent management, development notes, and troubleshooting.
|
|
||||||
|
|
||||||
5
WIKI/.gitattributes
vendored
5
WIKI/.gitattributes
vendored
@ -1,5 +0,0 @@
|
|||||||
# Treat markdown files as text with LF normalization
|
|
||||||
*.md text eol=lf
|
|
||||||
|
|
||||||
# Ensure JSON files are treated as text
|
|
||||||
*.json text
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
Agents and Keys
|
|
||||||
|
|
||||||
This project supports running multiple agent identities (private keys) to place orders on Hyperliquid. Agents are lightweight keys authorized on-chain by your main wallet.
|
|
||||||
|
|
||||||
Agent storage and environment
|
|
||||||
|
|
||||||
- For security, agent private keys should be stored as environment variables and not checked into source control.
|
|
||||||
- Supported patterns:
|
|
||||||
- `AGENT_PRIVATE_KEY` (single default agent)
|
|
||||||
- `<NAME>_AGENT_PK` or `<NAME>_AGENT_PRIVATE_KEY` (per-agent keys)
|
|
||||||
|
|
||||||
Discovering agents
|
|
||||||
|
|
||||||
- `trade_executor.py` scans environment variables for agent keys and loads them into `Exchange` objects so each agent can sign orders independently.
|
|
||||||
|
|
||||||
Creating and authorizing agents
|
|
||||||
|
|
||||||
- Use `create_agent.py` with your `MAIN_WALLET_PRIVATE_KEY` to authorize a new agent name. The script will attempt to call `exchange.approve_agent(agent_name)` and print the returned agent private key.
|
|
||||||
|
|
||||||
Security notes
|
|
||||||
|
|
||||||
- Never commit private keys to Git. Keep them in a secure secrets store or local `.env` file excluded from version control.
|
|
||||||
- Rotate keys if they are ever exposed and re-authorize agents using your main wallet.
|
|
||||||
|
|
||||||
Example `.env` snippet
|
|
||||||
|
|
||||||
MAIN_WALLET_PRIVATE_KEY=<your-main-wallet-private-key>
|
|
||||||
MAIN_WALLET_ADDRESS=<your-main-wallet-address>
|
|
||||||
AGENT_PRIVATE_KEY=<agent-private-key>
|
|
||||||
EXECUTOR_SCALPER_AGENT_PK=<agent-private-key-for-scalper>
|
|
||||||
|
|
||||||
File `agents`
|
|
||||||
|
|
||||||
- This repository may contain a local `agents` file used as a quick snapshot; treat it as insecure and remove it from the repo or add it to `.gitignore` if it contains secrets.
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
Contributing
|
|
||||||
|
|
||||||
Thanks for considering contributing! Please follow these guidelines to make the process smooth.
|
|
||||||
|
|
||||||
How to contribute
|
|
||||||
|
|
||||||
1. Fork the repository and create a feature branch for your change.
|
|
||||||
2. Keep changes focused and add tests where appropriate.
|
|
||||||
3. Submit a Pull Request with a clear description and the reason for the change.
|
|
||||||
|
|
||||||
Coding standards
|
|
||||||
|
|
||||||
- Keep functions small and well-documented.
|
|
||||||
- Use the existing logging utilities for consistent output.
|
|
||||||
- Prefer safe, incremental changes for financial code.
|
|
||||||
|
|
||||||
Security and secrets
|
|
||||||
|
|
||||||
- Never commit private keys, API keys, or secrets. Use environment variables or a secrets manager.
|
|
||||||
- If you accidentally commit secrets, rotate them immediately.
|
|
||||||
31
WIKI/DATA.md
31
WIKI/DATA.md
@ -1,31 +0,0 @@
|
|||||||
Data layout and formats
|
|
||||||
|
|
||||||
This section describes the `_data/` directory and the important files used by the scripts.
|
|
||||||
|
|
||||||
Important files
|
|
||||||
|
|
||||||
- `_data/market_data.db` — SQLite database that stores candle tables. Tables are typically named `<COIN>_<INTERVAL>` (e.g., `BTC_1m`, `ETH_5m`).
|
|
||||||
- `_data/coin_precision.json` — Mapping of coin names to their size precision (created by `list_coins.py`).
|
|
||||||
- `_data/current_prices.json` — Latest market prices that `market.py` writes.
|
|
||||||
- `_data/fetcher_status.json` — Last run metadata from `data_fetcher.py`.
|
|
||||||
- `_data/market_cap_data.json` — Market cap summary saved by `market_cap_fetcher.py`.
|
|
||||||
- `_data/strategies.json` — Configuration for strategies (enabled flag, parameters).
|
|
||||||
- `_data/strategy_status_<name>.json` — Per-strategy runtime status including last signal and price.
|
|
||||||
- `_data/executor_managed_positions.json` — Which strategy is currently managing which live position (used by `trade_executor`).
|
|
||||||
|
|
||||||
Candle schema
|
|
||||||
|
|
||||||
Each candle table contains columns similar to:
|
|
||||||
- `timestamp_ms` (INTEGER) — milliseconds since epoch
|
|
||||||
- `open`, `high`, `low`, `close` (FLOAT)
|
|
||||||
- `volume` (FLOAT)
|
|
||||||
- `number_of_trades` (INTEGER)
|
|
||||||
|
|
||||||
Trade logs
|
|
||||||
|
|
||||||
- Persistent trade history is stored in `_logs/trade_history.csv` with the following columns: `timestamp_utc`, `strategy`, `coin`, `action`, `price`, `size`, `signal`, `pnl`.
|
|
||||||
|
|
||||||
Backups and maintenance
|
|
||||||
|
|
||||||
- Periodically back up `_data/market_data.db`. The WAL and SHM files are also present when SQLite uses WAL mode.
|
|
||||||
- Keep JSON config/state files under version control only if they contain no secrets.
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
Development and testing
|
|
||||||
|
|
||||||
Code style and conventions
|
|
||||||
|
|
||||||
- Python 3.11+ with typing hints where helpful.
|
|
||||||
- Use `logging_utils.setup_logging` for consistent logs across scripts.
|
|
||||||
|
|
||||||
Running tests
|
|
||||||
|
|
||||||
- This repository doesn't currently include a formal test suite. Suggested quick checks:
|
|
||||||
- Run `python list_coins.py` to verify connectivity to Hyperliquid Info.
|
|
||||||
- Run `python -m pyflakes .` or `python -m pylint` if you have linters installed.
|
|
||||||
|
|
||||||
Adding a new strategy
|
|
||||||
|
|
||||||
1. Create a new script following the pattern in `strategy_template.py`.
|
|
||||||
2. Add an entry to `_data/strategies.json` with `enabled: true` and relevant parameters.
|
|
||||||
3. Ensure the strategy writes a status JSON file (`_data/strategy_status_<name>.json`) and uses `trade_log.log_trade` to record actions.
|
|
||||||
|
|
||||||
Recommended improvements (low-risk)
|
|
||||||
|
|
||||||
- Add a lightweight unit test suite (pytest) for core functions like timeframe parsing, SQL helpers, and signal calculation.
|
|
||||||
- Add CI (GitHub Actions) to run flake/pylint and unit tests on PRs.
|
|
||||||
- Move secrets handling to a `.env.example` and document environment variables in `WIKI/SETUP.md`.
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
Hyperliquid Trading Toolkit
|
|
||||||
|
|
||||||
This repository contains a collection of utility scripts, data fetchers, resamplers, trading strategies, and a trade executor for working with Hyperliquid trading APIs and crawled data. It is organized to support data collection, transformation, strategy development, and automated execution via agents.
|
|
||||||
|
|
||||||
Key components
|
|
||||||
|
|
||||||
- Data fetching and management: `data_fetcher.py`, `market.py`, `resampler.py`, `market_cap_fetcher.py`, `list_coins.py`
|
|
||||||
- Strategies: `strategy_sma_cross.py`, `strategy_template.py`, `strategy_sma_125d.py` (if present)
|
|
||||||
- Execution: `trade_executor.py`, `create_agent.py`, `agents` helper
|
|
||||||
- Utilities: `logging_utils.py`, `trade_log.py`
|
|
||||||
- Data storage: SQLite database in `_data/market_data.db` and JSON files in `_data`
|
|
||||||
|
|
||||||
Intended audience
|
|
||||||
|
|
||||||
- Developers building strategies and automations on Hyperliquid
|
|
||||||
- Data engineers collecting and processing market data
|
|
||||||
- Operators running the fetchers and executors on a scheduler or as system services
|
|
||||||
|
|
||||||
Project goals
|
|
||||||
|
|
||||||
- Reliable collection of 1m candles and resampling to common timeframes
|
|
||||||
- Clean separation between data, strategies, and execution
|
|
||||||
- Lightweight logging and traceable trade records
|
|
||||||
|
|
||||||
Where to start
|
|
||||||
|
|
||||||
- Read `WIKI/SETUP.md` to prepare your environment
|
|
||||||
- Use `WIKI/SCRIPTS.md` for a description of individual scripts and how to run them
|
|
||||||
- Inspect `WIKI/AGENTS.md` to understand agent keys and how to manage them
|
|
||||||
@ -1,47 +0,0 @@
|
|||||||
Scripts and How to Use Them
|
|
||||||
|
|
||||||
This file documents the main scripts in the repository and their purpose, typical runtime parameters, and key notes.
|
|
||||||
|
|
||||||
list_coins.py
|
|
||||||
- Purpose: Fetches asset metadata from Hyperliquid (name and size/precision) and saves `_data/coin_precision.json`.
|
|
||||||
- Usage: `python list_coins.py`
|
|
||||||
- Notes: Reads `hyperliquid.info.Info` and writes a JSON file. Useful to run before market feeders.
|
|
||||||
|
|
||||||
market.py (MarketDataFeeder)
|
|
||||||
- Purpose: Fetches live prices from Hyperliquid and writes `_data/current_prices.json` while printing a live table.
|
|
||||||
- Usage: `python market.py --log-level normal`
|
|
||||||
- Notes: Expects `_data/coin_precision.json` to exist.
|
|
||||||
|
|
||||||
data_fetcher.py (CandleFetcherDB)
|
|
||||||
- Purpose: Fetches historical 1m candles and stores them in `_data/market_data.db` using a table-per-coin naming convention.
|
|
||||||
- Usage: `python data_fetcher.py --coins BTC ETH --interval 1m --days 7`
|
|
||||||
- Notes: Can be run regularly by a scheduler to keep the DB up to date.
|
|
||||||
|
|
||||||
resampler.py (Resampler)
|
|
||||||
- Purpose: Reads 1m candles from SQLite and resamples to configured timeframes (e.g. 5m, 15m, 1h), appending new candles to tables.
|
|
||||||
- Usage: `python resampler.py --coins BTC ETH --timeframes 5m 15m 1h --log-level normal`
|
|
||||||
|
|
||||||
market_cap_fetcher.py (MarketCapFetcher)
|
|
||||||
- Purpose: Pulls CoinGecko market cap numbers and maintains historical daily tables in the same SQLite DB.
|
|
||||||
- Usage: `python market_cap_fetcher.py --coins BTC ETH --log-level normal`
|
|
||||||
- Notes: Optional `COINGECKO_API_KEY` in `.env` avoids throttling.
|
|
||||||
|
|
||||||
strategy_sma_cross.py (SmaCrossStrategy)
|
|
||||||
- Purpose: Run an SMA-based trading strategy. Reads candles from `_data/market_data.db` and writes status to `_data/strategy_status_<name>.json`.
|
|
||||||
- Usage: `python strategy_sma_cross.py --name sma_cross_1 --params '{"coin":"BTC","timeframe":"1m","fast":5,"slow":20}' --log-level normal`
|
|
||||||
|
|
||||||
trade_executor.py (TradeExecutor)
|
|
||||||
- Purpose: Orchestrates agent-based order execution using agent private keys found in environment variables. Uses `_data/strategies.json` to determine active strategies.
|
|
||||||
- Usage: `python trade_executor.py --log-level normal`
|
|
||||||
- Notes: Requires `MAIN_WALLET_ADDRESS` and agent keys. See `create_agent.py` to authorize agents on-chain.
|
|
||||||
|
|
||||||
create_agent.py
|
|
||||||
- Purpose: Authorizes a new on-chain agent using your main wallet (requires `MAIN_WALLET_PRIVATE_KEY`).
|
|
||||||
- Usage: `python create_agent.py`
|
|
||||||
- Notes: Prints the new agent private key to stdout — save it securely.
|
|
||||||
|
|
||||||
trade_log.py
|
|
||||||
- Purpose: Provides a thread-safe CSV trade history logger. Used by the executor and strategies to record actions.
|
|
||||||
|
|
||||||
Other utility scripts
|
|
||||||
- import_csv.py, fix_timestamps.py, list_coins.py, etc. — see file headers for details.
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
Setup and Installation
|
|
||||||
|
|
||||||
Prerequisites
|
|
||||||
|
|
||||||
- Python 3.11+ (project uses modern dependencies)
|
|
||||||
- Git (optional)
|
|
||||||
- A Hyperliquid account and an activated main wallet if you want to authorize agents and trade
|
|
||||||
|
|
||||||
Virtual environment
|
|
||||||
|
|
||||||
1. Create a virtual environment:
|
|
||||||
|
|
||||||
python -m venv .venv
|
|
||||||
|
|
||||||
2. Activate the virtual environment (PowerShell on Windows):
|
|
||||||
|
|
||||||
.\.venv\Scripts\Activate.ps1
|
|
||||||
|
|
||||||
3. Upgrade pip and install dependencies:
|
|
||||||
|
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install -r requirements.txt
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
|
|
||||||
- Copy `.env.example` to `.env` and set the following variables as required:
|
|
||||||
- MAIN_WALLET_PRIVATE_KEY (used by `create_agent.py` to authorize agents)
|
|
||||||
- MAIN_WALLET_ADDRESS (used by `trade_executor.py`)
|
|
||||||
- AGENT_PRIVATE_KEY or per-agent keys like `EXECUTOR_SCALPER_AGENT_PK`
|
|
||||||
- Optional: COINGECKO_API_KEY for `market_cap_fetcher.py` to avoid rate limits
|
|
||||||
|
|
||||||
Data directory
|
|
||||||
|
|
||||||
- The project writes and reads data from the `_data/` folder. Ensure the directory exists and is writable by the user running the scripts.
|
|
||||||
|
|
||||||
Quick test
|
|
||||||
|
|
||||||
After installing packages, run `list_coins.py` in a dry run to verify connectivity to the Hyperliquid info API:
|
|
||||||
|
|
||||||
python list_coins.py
|
|
||||||
|
|
||||||
If you encounter import errors, ensure the virtual environment is active and the `requirements.txt` dependencies are installed.
|
|
||||||
@ -1,15 +0,0 @@
|
|||||||
Project Wiki Summary
|
|
||||||
|
|
||||||
This directory contains human-friendly documentation for the project. Files:
|
|
||||||
|
|
||||||
- `OVERVIEW.md` — High-level overview and where to start
|
|
||||||
- `SETUP.md` — Environment setup and quick test steps
|
|
||||||
- `SCRIPTS.md` — Per-script documentation and usage examples
|
|
||||||
- `AGENTS.md` — How agents work and secure handling of keys
|
|
||||||
- `DATA.md` — Data folder layout and schema notes
|
|
||||||
- `DEVELOPMENT.md` — Developer guidance and recommended improvements
|
|
||||||
- `CONTRIBUTING.md` — How to contribute safely
|
|
||||||
- `TROUBLESHOOTING.md` — Common problems and solutions
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
- These pages were generated from repository source files and common patterns in trading/data projects. Validate any sensitive information (agent keys) and remove them from the repository when sharing.
|
|
||||||
@ -1,21 +0,0 @@
|
|||||||
Troubleshooting common issues
|
|
||||||
|
|
||||||
1. Import errors
|
|
||||||
- Ensure the virtual environment is active.
|
|
||||||
- Run `pip install -r requirements.txt`.
|
|
||||||
|
|
||||||
2. Agent authorization failures
|
|
||||||
- Ensure your main wallet is activated on Hyperliquid and has funds.
|
|
||||||
- The `create_agent.py` script will print helpful messages if the vault (main wallet) cannot act.
|
|
||||||
|
|
||||||
3. SQLite locked errors
|
|
||||||
- Increase the SQLite timeout when opening connections (this project uses a 10s timeout in fetcher). Close other programs that may hold the DB open.
|
|
||||||
|
|
||||||
4. Missing coin precision file
|
|
||||||
- Run `python list_coins.py` to regenerate `_data/coin_precision.json`.
|
|
||||||
|
|
||||||
5. Rate limits from CoinGecko
|
|
||||||
- Set `COINGECKO_API_KEY` in your `.env` file and ensure the fetcher respects backoff.
|
|
||||||
|
|
||||||
6. Agent keys in `agents` file or other local files
|
|
||||||
- Treat any `agents` file with private keys as compromised; rotate keys and remove the file from the repository.
|
|
||||||
Binary file not shown.
6
clp_hedger.log
Normal file
6
clp_hedger.log
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
2025-12-11 14:29:08,607 - INFO - Strategy Initialized. Liquidity (L): 1236.4542
|
||||||
|
2025-12-11 14:29:09,125 - INFO - CLP Hedger initialized. Agent: 0xcB262CeAaE5D8A99b713f87a43Dd18E6Be892739. Coin: ETH (Decimals: 4)
|
||||||
|
2025-12-11 14:29:09,126 - INFO - Starting Hedge Monitor Loop. Interval: 30s
|
||||||
|
2025-12-11 14:29:09,126 - INFO - Hedging Range: 2844.11 - 3477.24 | Static Long: 0.4
|
||||||
|
2025-12-11 14:29:09,769 - INFO - Price: 3201.85 | Pool Delta: 0.883 | Tgt Short: 1.283 | Act Short: 0.000 | Diff: 1.283
|
||||||
|
2025-12-11 14:29:11,987 - ERROR - Order API Error: Order has invalid price.
|
||||||
256
clp_hedger/CLP_HEDGING_IMPLEMENTATION_PLAN.md
Normal file
256
clp_hedger/CLP_HEDGING_IMPLEMENTATION_PLAN.md
Normal 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.*
|
||||||
86
clp_hedger/GEMINI.md
Normal file
86
clp_hedger/GEMINI.md
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
# Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-12-11
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Fix API errors, enhance bot functionality with safety features (auto-close), and add leverage/funding monitoring.
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* **Fixed API Price Error:** Implemented `round_to_sig_figs` to ensure limit prices meet Hyperliquid's 5 significant figure requirement, resolving the "Order has invalid price" error.
|
||||||
|
* **Safety Shutdown:** Added `close_all_positions` method and linked it to `KeyboardInterrupt`. The bot now automatically closes its hedge position when stopped manually.
|
||||||
|
* **Leverage Management:** Configured the bot to automatically set leverage to **4x Cross** (`LEVERAGE = 4`) upon initialization.
|
||||||
|
* **Market Monitoring:** Added real-time **Funding Rate** display to the main logging loop using `meta_and_asset_ctxs`.
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `clp_hedger.py`
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Used `math.log10` based calculation for significant figures to ensure broad compatibility with asset price ranges.
|
||||||
|
* Implemented `close_all_positions` as a blocking call during shutdown to prioritize safety over an immediate exit.
|
||||||
|
* Hardcoded `LEVERAGE` in configuration for now, with a plan to potentially move to a config file later if needed.
|
||||||
|
|
||||||
|
# Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-12-11
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Implement a dynamic gap recovery strategy to neutralize initial losses from delayed hedging.
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* Implemented "Gap Recovery" logic to dynamically adjust hedging based on current price relative to CLP `ENTRY_PRICE` and initial `START_PRICE`.
|
||||||
|
* Defined three distinct hedging zones:
|
||||||
|
* **NORMAL (below Entry):** 100% hedge for safety.
|
||||||
|
* **RECOVERY (between Entry and Recovery Target):** 0% hedge (naked long) to maximize recovery.
|
||||||
|
* **NORMAL (above Recovery Target):** 100% hedge after gap is neutralized.
|
||||||
|
* Introduced `PRICE_BUFFER_PCT` and `TIME_BUFFER_SECONDS` to prevent trade churn around zone boundaries.
|
||||||
|
|
||||||
|
**Key Files Modified:**
|
||||||
|
* `clp_hedger.py`
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Chosen a dynamic `START_PRICE` capture at bot initialization to calculate the `GAP`.
|
||||||
|
* Opted for 0% hedge in the recovery zone for faster loss neutralization, acknowledging higher short-term risk.
|
||||||
|
* Implemented price and time buffers for robust mode switching.
|
||||||
|
|
||||||
|
# Session Summary
|
||||||
|
|
||||||
|
**Date:** 2025-12-12
|
||||||
|
|
||||||
|
**Objective(s):**
|
||||||
|
Develop a Uniswap V3 position manager script (formerly monitor) for Arbitrum, including fee collection, closing positions, and automated opening of new positions with auto-swapping. Refine hedging architecture for multi-position management.
|
||||||
|
|
||||||
|
**Key Accomplishments:**
|
||||||
|
* **`uniswap_manager.py` (Unified Lifecycle Manager):**
|
||||||
|
* Transformed into a continuous lifecycle manager for AUTOMATIC positions.
|
||||||
|
* **Features:**
|
||||||
|
* Manages "AUTOMATIC" CLP positions (Open, Monitor, Close, Collect Fees).
|
||||||
|
* Reads/Writes state to `hedge_status.json`.
|
||||||
|
* Implemented auto-wrapping of native ETH to WETH when needed.
|
||||||
|
* Includes robust auto-swapping (WETH <-> USDC) to balance tokens before minting.
|
||||||
|
* Implemented robust event parsing using `process_receipt` to extract exact `amount0` and `amount1` from mint transactions.
|
||||||
|
* **Fixed `web3.py` v7 `raw_transaction` access across all transaction types.**
|
||||||
|
* **Fixed Uniswap V3 Math precision** in `calculate_mint_amounts` for accurate token splits.
|
||||||
|
* **Troubleshooting & Resolution:**
|
||||||
|
* **Address Validation:** Replaced hardcoded factory address with dynamic lookup.
|
||||||
|
* **ABI Mismatch:** Updated NPM ABI with event definitions for `IncreaseLiquidity` and `Transfer`.
|
||||||
|
* **Typo/Indentation Errors:** Resolved multiple `NameError` (`target_tick_lower`, `w3_instance`, `position_details`) and `IndentationError` issues during script refactoring.
|
||||||
|
* **JSON Update Failure:** Fixed `mint_new_position`'s log parsing for Token ID to correctly update `hedge_status.json` after successful mint.
|
||||||
|
* **`clp_scalper_hedger.py` (Dedicated Automatic Hedger):**
|
||||||
|
* Created as a new script to hedge `type: "AUTOMATIC"` positions defined in `hedge_status.json`.
|
||||||
|
* Uses `SCALPER_AGENT_PK` from `.env`.
|
||||||
|
* **Accurate L Calculation:** Calculates Uniswap V3 liquidity (`L`) using `amount0_initial` or `amount1_initial` from `hedge_status.json`, falling back to a heuristic based on `target_value` if amounts are missing.
|
||||||
|
* **Dynamic Rebalance Threshold:** Threshold adapts to 5% of the position's maximum ETH risk (`max_potential_eth`).
|
||||||
|
* **Minimum Order Value:** Enforces a minimum order size of $10 to prevent dust trades and API errors.
|
||||||
|
* **`clp_hedger.py` (Updated Manual Hedger):**
|
||||||
|
* Modified to load its configuration entirely from the `type: "MANUAL"` entry in `hedge_status.json`.
|
||||||
|
* Respects the `hedge_enabled` flag from the JSON.
|
||||||
|
* Idles if hedging is disabled or no manual position is found.
|
||||||
|
* **`hedge_status.json`:**
|
||||||
|
* Becomes the central source of truth for all (MANUAL and AUTOMATIC) CLP positions, including their type, status, ranges, `entry_price`, `target_value` (for automatic), and `hedge_enabled` flag.
|
||||||
|
* **.env File Location:** All scripts updated to load `.env` from the current working directory (`clp_hedger/`).
|
||||||
|
|
||||||
|
**Decisions Made:**
|
||||||
|
* Adopted a multi-script architecture for clarity and separation of concerns (Manager vs. Hedgers).
|
||||||
|
* Used `hedge_status.json` as the centralized state manager for all CLP positions.
|
||||||
|
* Implemented robust error handling and debugging throughout the development process.
|
||||||
|
* Ensured `clp_scalper_hedger.py` is resilient to missing initial amount data in `hedge_status.json` by implementing fallback `L` calculation methods.
|
||||||
0
clp_hedger/__init__.py
Normal file
0
clp_hedger/__init__.py
Normal file
735
clp_hedger/clp_scalper_hedger.py
Normal file
735
clp_hedger/clp_scalper_hedger.py
Normal file
@ -0,0 +1,735 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
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__))
|
||||||
|
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("normal", "SCALPER_HEDGER")
|
||||||
|
|
||||||
|
# --- CONFIGURATION ---
|
||||||
|
COIN_SYMBOL = "ETH"
|
||||||
|
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: Covers entire range (0.0 to 1.5) -> Always Active
|
||||||
|
ZONE_BOTTOM_HEDGE_LIMIT = 1
|
||||||
|
|
||||||
|
# Close Zone: Disabled (Set > 1.0)
|
||||||
|
ZONE_CLOSE_START = 10.0
|
||||||
|
ZONE_CLOSE_END = 11.0
|
||||||
|
|
||||||
|
# Top Hedge Zone: Disabled/Redundant
|
||||||
|
ZONE_TOP_HEDGE_START = 10.0
|
||||||
|
|
||||||
|
# --- ORDER SETTINGS ---
|
||||||
|
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
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
for entry in data:
|
||||||
|
if entry.get('type') == 'AUTOMATIC' and entry.get('status') == 'OPEN':
|
||||||
|
return entry
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"ERROR reading status file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_position_zones_in_json(token_id, zones_data):
|
||||||
|
"""Updates the active position in JSON with calculated zone prices and formats the entry."""
|
||||||
|
if not os.path.exists(STATUS_FILE): return
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
updated = False
|
||||||
|
for i, entry in enumerate(data):
|
||||||
|
if entry.get('type') == 'AUTOMATIC' and entry.get('status') == 'OPEN' and entry.get('token_id') == token_id:
|
||||||
|
|
||||||
|
# Merge Zones
|
||||||
|
for k, v in zones_data.items():
|
||||||
|
entry[k] = v
|
||||||
|
|
||||||
|
# Format & Reorder
|
||||||
|
open_ts = entry.get('timestamp_open', int(time.time()))
|
||||||
|
opened_str = time.strftime('%H:%M %d/%m/%y', time.localtime(open_ts))
|
||||||
|
|
||||||
|
# Reconstruct Dict in Order
|
||||||
|
new_entry = {
|
||||||
|
"type": entry.get('type'),
|
||||||
|
"token_id": entry.get('token_id'),
|
||||||
|
"opened": opened_str,
|
||||||
|
"status": entry.get('status'),
|
||||||
|
"entry_price": round(entry.get('entry_price', 0), 2),
|
||||||
|
"target_value": round(entry.get('target_value', 0), 2),
|
||||||
|
# Amounts might be string or float or int. Ensure float.
|
||||||
|
"amount0_initial": round(float(entry.get('amount0_initial', 0)), 4),
|
||||||
|
"amount1_initial": round(float(entry.get('amount1_initial', 0)), 2),
|
||||||
|
|
||||||
|
"range_upper": round(entry.get('range_upper', 0), 2),
|
||||||
|
"zone_top_start_price": entry.get('zone_top_start_price'),
|
||||||
|
"zone_close_top_price": entry.get('zone_close_top_price'),
|
||||||
|
"zone_close_bottom_price": entry.get('zone_close_bottom_price'),
|
||||||
|
"zone_bottom_limit_price": entry.get('zone_bottom_limit_price'),
|
||||||
|
"range_lower": round(entry.get('range_lower', 0), 2),
|
||||||
|
|
||||||
|
"static_long": entry.get('static_long', 0.0),
|
||||||
|
"timestamp_open": open_ts,
|
||||||
|
"timestamp_close": entry.get('timestamp_close')
|
||||||
|
}
|
||||||
|
|
||||||
|
data[i] = new_entry
|
||||||
|
updated = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if updated:
|
||||||
|
with open(STATUS_FILE, 'w') as f:
|
||||||
|
json.dump(data, f, indent=2)
|
||||||
|
logging.info(f"Updated JSON with Formatted Zone Prices for Position {token_id}")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error updating JSON zones: {e}")
|
||||||
|
|
||||||
|
def round_to_sig_figs(x, sig_figs=5):
|
||||||
|
if x == 0: return 0.0
|
||||||
|
return round(x, sig_figs - int(math.floor(math.log10(abs(x)))) - 1)
|
||||||
|
|
||||||
|
def round_to_sz_decimals(amount, sz_decimals=4):
|
||||||
|
return round(abs(amount), sz_decimals)
|
||||||
|
|
||||||
|
class HyperliquidStrategy:
|
||||||
|
def __init__(self, entry_amount0, entry_amount1, target_value, entry_price, low_range, high_range, start_price, static_long=0.0):
|
||||||
|
self.entry_amount0 = entry_amount0
|
||||||
|
self.entry_amount1 = entry_amount1
|
||||||
|
self.target_value = target_value
|
||||||
|
self.entry_price = entry_price
|
||||||
|
self.low_range = low_range
|
||||||
|
self.high_range = high_range
|
||||||
|
self.static_long = static_long
|
||||||
|
|
||||||
|
self.start_price = start_price
|
||||||
|
self.gap = max(0.0, entry_price - start_price)
|
||||||
|
self.recovery_target = entry_price + (2 * self.gap)
|
||||||
|
|
||||||
|
self.current_mode = "NORMAL"
|
||||||
|
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}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
sqrt_P = math.sqrt(entry_price)
|
||||||
|
sqrt_Pa = math.sqrt(low_range)
|
||||||
|
sqrt_Pb = math.sqrt(high_range)
|
||||||
|
|
||||||
|
self.L = 0.0
|
||||||
|
|
||||||
|
# Method 1: Use Amount0 (WETH)
|
||||||
|
if entry_amount0 > 0:
|
||||||
|
# If amount is huge (Wei), scale it. If small (ETH), use as is.
|
||||||
|
if entry_amount0 > 1000: amount0_eth = entry_amount0 / 10**18
|
||||||
|
else: amount0_eth = entry_amount0
|
||||||
|
|
||||||
|
denom0 = (1/sqrt_P) - (1/sqrt_Pb)
|
||||||
|
if denom0 > 0.00000001:
|
||||||
|
self.L = amount0_eth / denom0
|
||||||
|
logging.info(f"Calculated L from Amount0: {self.L:.4f}")
|
||||||
|
|
||||||
|
# Method 2: Use Amount1 (USDC)
|
||||||
|
if self.L == 0.0 and entry_amount1 > 0:
|
||||||
|
if entry_amount1 > 100000: amount1_usdc = entry_amount1 / 10**6
|
||||||
|
else: amount1_usdc = entry_amount1
|
||||||
|
|
||||||
|
denom1 = sqrt_P - sqrt_Pa
|
||||||
|
if denom1 > 0.00000001:
|
||||||
|
self.L = amount1_usdc / denom1
|
||||||
|
logging.info(f"Calculated L from Amount1: {self.L:.4f}")
|
||||||
|
|
||||||
|
# Method 3: Fallback Heuristic
|
||||||
|
if self.L == 0.0:
|
||||||
|
logging.warning("Amounts missing or 0. Using Target Value Heuristic.")
|
||||||
|
max_eth_heuristic = target_value / low_range
|
||||||
|
denom_h = (1/sqrt_Pa) - (1/sqrt_Pb)
|
||||||
|
if denom_h > 0:
|
||||||
|
self.L = max_eth_heuristic / denom_h
|
||||||
|
logging.info(f"Calculated L from Target Value: {self.L:.4f}")
|
||||||
|
else:
|
||||||
|
logging.error("Critical: Denominator 0 in Heuristic. Invalid Range?")
|
||||||
|
self.L = 0.0
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error calculating liquidity: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
def get_pool_delta(self, current_price):
|
||||||
|
if current_price >= self.high_range: return 0.0
|
||||||
|
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))
|
||||||
|
|
||||||
|
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):
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 {
|
||||||
|
"current_price": current_price,
|
||||||
|
"pool_delta": pool_delta,
|
||||||
|
"target_short": target_short_size,
|
||||||
|
"current_short": abs(current_short_position_size),
|
||||||
|
"diff": diff,
|
||||||
|
"action": "SELL" if diff > 0 else "BUY",
|
||||||
|
"mode": "OVERHEDGE" if overhedge_pct > 0 else "NORMAL",
|
||||||
|
"overhedge_pct": overhedge_pct
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScalperHedger:
|
||||||
|
def __init__(self):
|
||||||
|
self.private_key = os.environ.get("SCALPER_AGENT_PK")
|
||||||
|
self.vault_address = os.environ.get("MAIN_WALLET_ADDRESS")
|
||||||
|
|
||||||
|
if not self.private_key:
|
||||||
|
logging.error("No SCALPER_AGENT_PK found in .env")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
self.account = Account.from_key(self.private_key)
|
||||||
|
self.info = Info(constants.MAINNET_API_URL, skip_ws=True)
|
||||||
|
self.exchange = Exchange(self.account, constants.MAINNET_API_URL, account_address=self.vault_address)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logging.info(f"Setting leverage to {LEVERAGE}x (Cross)...")
|
||||||
|
self.exchange.update_leverage(LEVERAGE, COIN_SYMBOL, is_cross=True)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Failed to update leverage: {e}")
|
||||||
|
|
||||||
|
self.strategy = None
|
||||||
|
self.sz_decimals = self._get_sz_decimals(COIN_SYMBOL)
|
||||||
|
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):
|
||||||
|
try:
|
||||||
|
entry_amount0 = position_data.get('amount0_initial', 0)
|
||||||
|
entry_amount1 = position_data.get('amount1_initial', 0)
|
||||||
|
target_value = position_data.get('target_value', 50.0)
|
||||||
|
|
||||||
|
entry_price = position_data['entry_price']
|
||||||
|
lower = position_data['range_lower']
|
||||||
|
upper = position_data['range_upper']
|
||||||
|
static_long = position_data.get('static_long', 0.0)
|
||||||
|
|
||||||
|
start_price = self.get_market_price(COIN_SYMBOL)
|
||||||
|
if start_price is None:
|
||||||
|
logging.warning("Waiting for initial price to start strategy...")
|
||||||
|
return
|
||||||
|
|
||||||
|
self.strategy = HyperliquidStrategy(
|
||||||
|
entry_amount0=entry_amount0,
|
||||||
|
entry_amount1=entry_amount1,
|
||||||
|
target_value=target_value,
|
||||||
|
entry_price=entry_price,
|
||||||
|
low_range=lower,
|
||||||
|
high_range=upper,
|
||||||
|
start_price=start_price,
|
||||||
|
static_long=static_long
|
||||||
|
)
|
||||||
|
logging.info(f"Strategy Initialized for Position {position_data['token_id']}.")
|
||||||
|
self.active_position_id = position_data['token_id']
|
||||||
|
|
||||||
|
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"]
|
||||||
|
return 4
|
||||||
|
except: return 4
|
||||||
|
|
||||||
|
def get_order_book_levels(self, coin):
|
||||||
|
try:
|
||||||
|
l2_snapshot = self.info.l2_snapshot(coin)
|
||||||
|
if l2_snapshot and 'levels' in l2_snapshot:
|
||||||
|
bids = l2_snapshot['levels'][0]
|
||||||
|
asks = l2_snapshot['levels'][1]
|
||||||
|
if bids and asks:
|
||||||
|
best_bid = float(bids[0]['px'])
|
||||||
|
best_ask = float(asks[0]['px'])
|
||||||
|
mid = (best_bid + best_ask) / 2
|
||||||
|
return {'bid': best_bid, 'ask': best_ask, 'mid': mid}
|
||||||
|
# Fallback
|
||||||
|
px = self.get_market_price(coin)
|
||||||
|
return {'bid': px, 'ask': px, 'mid': px}
|
||||||
|
except:
|
||||||
|
px = self.get_market_price(coin)
|
||||||
|
return {'bid': px, 'ask': px, 'mid': px}
|
||||||
|
|
||||||
|
def get_market_price(self, coin):
|
||||||
|
try:
|
||||||
|
mids = self.info.all_mids()
|
||||||
|
if coin in mids: return float(mids[coin])
|
||||||
|
except: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_order_book_mid(self, coin):
|
||||||
|
try:
|
||||||
|
l2_snapshot = self.info.l2_snapshot(coin)
|
||||||
|
if l2_snapshot and 'levels' in l2_snapshot:
|
||||||
|
bids = l2_snapshot['levels'][0]
|
||||||
|
asks = l2_snapshot['levels'][1]
|
||||||
|
if bids and asks:
|
||||||
|
best_bid = float(bids[0]['px'])
|
||||||
|
best_ask = float(asks[0]['px'])
|
||||||
|
return (best_bid + best_ask) / 2
|
||||||
|
return self.get_market_price(coin)
|
||||||
|
except:
|
||||||
|
return self.get_market_price(coin)
|
||||||
|
|
||||||
|
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:
|
||||||
|
return float(asset_ctxs[i]["funding"])
|
||||||
|
return 0.0
|
||||||
|
except: return 0.0
|
||||||
|
|
||||||
|
def get_current_position(self, coin):
|
||||||
|
try:
|
||||||
|
user_state = self.info.user_state(self.vault_address or self.account.address)
|
||||||
|
for pos in user_state["assetPositions"]:
|
||||||
|
if pos["position"]["coin"] == coin:
|
||||||
|
return {
|
||||||
|
'size': float(pos["position"]["szi"]),
|
||||||
|
'pnl': float(pos["position"]["unrealizedPnl"])
|
||||||
|
}
|
||||||
|
return {'size': 0.0, 'pnl': 0.0}
|
||||||
|
except: return {'size': 0.0, 'pnl': 0.0}
|
||||||
|
|
||||||
|
def get_open_orders(self):
|
||||||
|
try:
|
||||||
|
return self.info.open_orders(self.vault_address or self.account.address)
|
||||||
|
except: return []
|
||||||
|
|
||||||
|
def cancel_order(self, coin, oid):
|
||||||
|
logging.info(f"Cancelling order {oid}...")
|
||||||
|
try:
|
||||||
|
return self.exchange.cancel(coin, oid)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error cancelling order: {e}")
|
||||||
|
|
||||||
|
def place_limit_order(self, coin, is_buy, size, price):
|
||||||
|
logging.info(f"🕒 PLACING LIMIT: {coin} {'BUY' if is_buy else 'SELL'} {size} @ {price:.2f}")
|
||||||
|
reduce_only = is_buy
|
||||||
|
try:
|
||||||
|
# Gtc order (Maker) -> Changed to Alo to force Maker
|
||||||
|
limit_px = round_to_sig_figs(price, 5)
|
||||||
|
|
||||||
|
# Use 'Alo' (Add Liquidity Only) to ensure Maker rebate.
|
||||||
|
# If price crosses spread, order is rejected (safe cost-wise).
|
||||||
|
order_result = self.exchange.order(coin, is_buy, size, limit_px, {"limit": {"tif": "Alo"}}, reduce_only=reduce_only)
|
||||||
|
status = order_result["status"]
|
||||||
|
if status == "ok":
|
||||||
|
response_data = order_result["response"]["data"]
|
||||||
|
if "statuses" in response_data:
|
||||||
|
status_obj = response_data["statuses"][0]
|
||||||
|
|
||||||
|
if "error" in status_obj:
|
||||||
|
logging.error(f"Order API Error: {status_obj['error']}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Parse OID from nested structure
|
||||||
|
oid = None
|
||||||
|
if "resting" in status_obj:
|
||||||
|
oid = status_obj["resting"]["oid"]
|
||||||
|
elif "filled" in status_obj:
|
||||||
|
oid = status_obj["filled"]["oid"]
|
||||||
|
logging.info("Order filled immediately.")
|
||||||
|
|
||||||
|
if oid:
|
||||||
|
logging.info(f"✅ Limit Order Placed: OID {oid}")
|
||||||
|
return oid
|
||||||
|
else:
|
||||||
|
logging.warning(f"Order placed but OID not found in: {status_obj}")
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
logging.error(f"Order Failed: {order_result}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Exception during trade: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def manage_orders(self):
|
||||||
|
"""
|
||||||
|
Checks open orders.
|
||||||
|
Returns: True if an order exists and is valid (don't trade), False if no order (can trade).
|
||||||
|
"""
|
||||||
|
open_orders = self.get_open_orders()
|
||||||
|
my_orders = [o for o in open_orders if o['coin'] == COIN_SYMBOL]
|
||||||
|
|
||||||
|
if not my_orders:
|
||||||
|
self.active_order = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(my_orders) > 1:
|
||||||
|
logging.warning("Multiple open orders found. Cancelling all for safety.")
|
||||||
|
for o in my_orders:
|
||||||
|
self.cancel_order(COIN_SYMBOL, o['oid'])
|
||||||
|
self.active_order = None
|
||||||
|
return False
|
||||||
|
|
||||||
|
order = my_orders[0]
|
||||||
|
oid = order['oid']
|
||||||
|
order_price = float(order['limitPx'])
|
||||||
|
|
||||||
|
current_mid = self.get_order_book_mid(COIN_SYMBOL)
|
||||||
|
pct_diff = abs(current_mid - order_price) / order_price
|
||||||
|
|
||||||
|
if pct_diff > PRICE_BUFFER_PCT:
|
||||||
|
logging.info(f"Price moved {pct_diff*100:.3f}% > {PRICE_BUFFER_PCT*100}%. Cancelling/Replacing order {oid}.")
|
||||||
|
self.cancel_order(COIN_SYMBOL, oid)
|
||||||
|
self.active_order = None
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logging.info(f"Pending Order {oid} @ {order_price:.2f} is within range ({pct_diff*100:.3f}%). Waiting.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
def close_all_positions(self):
|
||||||
|
logging.info("Closing all positions (Market Order)...")
|
||||||
|
try:
|
||||||
|
# Cancel open orders first
|
||||||
|
open_orders = self.get_open_orders()
|
||||||
|
for o in open_orders:
|
||||||
|
if o['coin'] == COIN_SYMBOL:
|
||||||
|
self.cancel_order(COIN_SYMBOL, o['oid'])
|
||||||
|
|
||||||
|
price = self.get_market_price(COIN_SYMBOL)
|
||||||
|
pos_data = self.get_current_position(COIN_SYMBOL)
|
||||||
|
current_pos = pos_data['size']
|
||||||
|
|
||||||
|
if current_pos == 0: return
|
||||||
|
|
||||||
|
is_buy = current_pos < 0
|
||||||
|
final_size = round_to_sz_decimals(abs(current_pos), self.sz_decimals)
|
||||||
|
if final_size == 0: return
|
||||||
|
|
||||||
|
price = self.get_market_price(COIN_SYMBOL) # Get mid price for safety fallback
|
||||||
|
pos_data = self.get_current_position(COIN_SYMBOL)
|
||||||
|
current_pos = pos_data['size']
|
||||||
|
|
||||||
|
if current_pos == 0: return
|
||||||
|
|
||||||
|
is_buy_to_close = current_pos < 0
|
||||||
|
final_size = round_to_sz_decimals(abs(current_pos), self.sz_decimals)
|
||||||
|
if final_size == 0: return
|
||||||
|
|
||||||
|
# --- ATTEMPT MAKER CLOSE (Alo) ---
|
||||||
|
try:
|
||||||
|
book_levels = self.get_order_book_levels(COIN_SYMBOL)
|
||||||
|
TICK_SIZE = 0.1
|
||||||
|
|
||||||
|
if is_buy_to_close: # We are short, need to buy to close
|
||||||
|
maker_price = book_levels['bid'] - TICK_SIZE
|
||||||
|
else: # We are long, need to sell to close
|
||||||
|
maker_price = book_levels['ask'] + TICK_SIZE
|
||||||
|
|
||||||
|
logging.info(f"Attempting MAKER CLOSE (Alo): {COIN_SYMBOL} {'BUY' if is_buy_to_close else 'SELL'} {final_size} @ {maker_price:.2f}")
|
||||||
|
order_result = self.exchange.order(COIN_SYMBOL, is_buy_to_close, final_size, round_to_sig_figs(maker_price, 5), {"limit": {"tif": "Alo"}}, reduce_only=True)
|
||||||
|
|
||||||
|
status = order_result["status"]
|
||||||
|
if status == "ok":
|
||||||
|
response_data = order_result["response"]["data"]
|
||||||
|
if "statuses" in response_data and "resting" in response_data["statuses"][0]:
|
||||||
|
logging.info(f"✅ MAKER CLOSE Order Placed (Alo). OID: {response_data['statuses'][0]['resting']['oid']}")
|
||||||
|
return
|
||||||
|
elif "statuses" in response_data and "filled" in response_data["statuses"][0]:
|
||||||
|
logging.info(f"✅ MAKER CLOSE Order Filled (Alo). OID: {response_data['statuses'][0]['filled']['oid']}")
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
# Fallback if Alo didn't rest or fill immediately in an expected way
|
||||||
|
logging.warning(f"Alo order result unclear: {order_result}. Falling back to Market Close.")
|
||||||
|
|
||||||
|
elif status == "error":
|
||||||
|
if "Post only order would have immediately matched" in order_result["response"]["data"]["statuses"][0].get("error", ""):
|
||||||
|
logging.warning("Alo order would have immediately matched. Falling back to Market Close for guaranteed fill.")
|
||||||
|
else:
|
||||||
|
logging.error(f"Alo order failed with unknown error: {order_result}. Falling back to Market Close.")
|
||||||
|
else:
|
||||||
|
logging.warning(f"Alo order failed with status {status}. Falling back to Market Close.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Exception during Alo close attempt: {e}. Falling back to Market Close.", exc_info=True)
|
||||||
|
|
||||||
|
# --- FALLBACK TO MARKET CLOSE (Ioc) for guaranteed fill ---
|
||||||
|
logging.info(f"Falling back to MARKET CLOSE (Ioc): {COIN_SYMBOL} {'BUY' if is_buy_to_close else 'SELL'} {final_size} @ {price:.2f} (guaranteed)")
|
||||||
|
self.exchange.order(COIN_SYMBOL, is_buy_to_close, final_size, round_to_sig_figs(price * (1.05 if is_buy_to_close else 0.95), 5), {"limit": {"tif": "Ioc"}}, reduce_only=True)
|
||||||
|
self.active_position_id = None
|
||||||
|
logging.info("✅ MARKET CLOSE Order Placed (Ioc).")
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error closing positions: {e}", exc_info=True)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
logging.info(f"Starting Scalper Monitor Loop. Interval: {CHECK_INTERVAL}s")
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
active_pos = get_active_automatic_position()
|
||||||
|
|
||||||
|
# Check Global Enable Switch
|
||||||
|
if not active_pos or not active_pos.get('hedge_enabled', True):
|
||||||
|
if self.strategy is not None:
|
||||||
|
logging.info("Hedge Disabled or Position Closed. Closing remaining positions.")
|
||||||
|
self.close_all_positions()
|
||||||
|
self.strategy = None
|
||||||
|
else:
|
||||||
|
pass
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.strategy is None or self.active_position_id != active_pos['token_id']:
|
||||||
|
logging.info(f"New position {active_pos['token_id']} detected or strategy not initialized. Initializing strategy.")
|
||||||
|
self._init_strategy(active_pos)
|
||||||
|
if self.strategy is None:
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if self.strategy is None: continue
|
||||||
|
|
||||||
|
# --- ORDER MANAGEMENT ---
|
||||||
|
if self.manage_orders():
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2. Market Data
|
||||||
|
book_levels = self.get_order_book_levels(COIN_SYMBOL)
|
||||||
|
price = book_levels['mid']
|
||||||
|
|
||||||
|
if price is None:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
funding_rate = self.get_funding_rate(COIN_SYMBOL)
|
||||||
|
pos_data = self.get_current_position(COIN_SYMBOL)
|
||||||
|
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)
|
||||||
|
max_potential_eth = self.strategy.L * ((1/sqrt_Pa) - (1/sqrt_Pb))
|
||||||
|
|
||||||
|
# Use MIN_THRESHOLD_ETH from config
|
||||||
|
rebalance_threshold = max(MIN_THRESHOLD_ETH, max_potential_eth * 0.05)
|
||||||
|
|
||||||
|
# 5. Determine Hedge Zone
|
||||||
|
clp_low_range = self.strategy.low_range
|
||||||
|
clp_high_range = self.strategy.high_range
|
||||||
|
range_width = clp_high_range - clp_low_range
|
||||||
|
|
||||||
|
# Calculate Prices for Zones
|
||||||
|
# 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) 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 (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:
|
||||||
|
logging.info(f"ZONE: CLOSE ({price:.2f} in {zone_close_bottom_price:.2f}-{zone_close_top_price:.2f}). PNL: ${current_pnl:.2f}. Closing all hedge positions.")
|
||||||
|
self.close_all_positions()
|
||||||
|
time.sleep(CHECK_INTERVAL)
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif in_hedge_zone:
|
||||||
|
# HEDGE NORMALLY
|
||||||
|
if diff_abs > rebalance_threshold:
|
||||||
|
trade_size = round_to_sz_decimals(diff_abs, 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}{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}{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
|
||||||
|
TICK_SIZE = 0.1
|
||||||
|
|
||||||
|
is_buy = (calc['action'] == "BUY")
|
||||||
|
|
||||||
|
if is_buy:
|
||||||
|
exec_price = book_levels['bid'] - TICK_SIZE
|
||||||
|
else:
|
||||||
|
exec_price = book_levels['ask'] + TICK_SIZE
|
||||||
|
|
||||||
|
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}{spread_text}{oh_text}")
|
||||||
|
else:
|
||||||
|
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}{spread_text}{oh_text}. No Actions.")
|
||||||
|
|
||||||
|
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 = ScalperHedger()
|
||||||
|
hedger.run()
|
||||||
619
clp_hedger/hedge_status.json
Normal file
619
clp_hedger/hedge_status.json
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5154921,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3088.180203068298,
|
||||||
|
"range_lower": 3071.745207606606,
|
||||||
|
"range_upper": 3102.615208978462,
|
||||||
|
"target_value": 99.31729381997206,
|
||||||
|
"amount0_initial": 0,
|
||||||
|
"amount1_initial": 0,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765575924,
|
||||||
|
"timestamp_close": 1765613747
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155502,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3105.4778071503983,
|
||||||
|
"range_lower": 3090.230154007496,
|
||||||
|
"range_upper": 3118.1663529424395,
|
||||||
|
"target_value": 81.22159710646565,
|
||||||
|
"amount0_initial": 0,
|
||||||
|
"amount1_initial": 0,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765613789,
|
||||||
|
"timestamp_close": 1765614083
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155511,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3122.1562247614547,
|
||||||
|
"range_lower": 3105.7192207366634,
|
||||||
|
"range_upper": 3136.930649460415,
|
||||||
|
"target_value": 98.20653967768193,
|
||||||
|
"amount0_initial": 0,
|
||||||
|
"amount1_initial": 0,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765614124,
|
||||||
|
"timestamp_close": 1765617105
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155580,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3120.03330314008,
|
||||||
|
"range_lower": 3111.93656358668,
|
||||||
|
"range_upper": 3124.4086137206154,
|
||||||
|
"target_value": 258.2420686245357,
|
||||||
|
"amount0_initial": 0,
|
||||||
|
"amount1_initial": 0,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765617197,
|
||||||
|
"timestamp_close": 1765617236
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155610,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3118.03462860249,
|
||||||
|
"range_lower": 3056.425578524254,
|
||||||
|
"range_upper": 3177.9749053788623,
|
||||||
|
"target_value": 348.982123656927,
|
||||||
|
"amount0_initial": 54654586929109032,
|
||||||
|
"amount1_initial": 178567229,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765619246,
|
||||||
|
"timestamp_close": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155618,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3120.854321555066,
|
||||||
|
"range_lower": 3111.93656358668,
|
||||||
|
"range_upper": 3127.5344286932063,
|
||||||
|
"target_value": 342.45943993806645,
|
||||||
|
"amount0_initial": 46935127322790001,
|
||||||
|
"amount1_initial": 195981745,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765619616,
|
||||||
|
"timestamp_close": 1765621159
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155660,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3129.521502331058,
|
||||||
|
"range_lower": 3121.285922844486,
|
||||||
|
"range_upper": 3136.930649460415,
|
||||||
|
"target_value": 345.19101843135434,
|
||||||
|
"amount0_initial": 52148054681776174,
|
||||||
|
"amount1_initial": 181992560,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765621204,
|
||||||
|
"timestamp_close": 1765625900
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155742,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3120.452464830275,
|
||||||
|
"range_lower": 3111.93656358668,
|
||||||
|
"range_upper": 3127.5344286932063,
|
||||||
|
"target_value": 330.2607520468071,
|
||||||
|
"amount0_initial": 45273020063291068,
|
||||||
|
"amount1_initial": 188988445,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765625947,
|
||||||
|
"timestamp_close": 1765629916
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155807,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3111.8306135157013,
|
||||||
|
"range_lower": 3102.615208978462,
|
||||||
|
"range_upper": 3118.1663529424395,
|
||||||
|
"target_value": 342.2298529154781,
|
||||||
|
"amount0_initial": 44749390699692539,
|
||||||
|
"amount1_initial": 202977329,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765629968,
|
||||||
|
"timestamp_close": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155828,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3116.7126648332624,
|
||||||
|
"range_lower": 3099.514299525495,
|
||||||
|
"range_upper": 3130.663370887762,
|
||||||
|
"target_value": 347.83537144876755,
|
||||||
|
"amount0_initial": 49847371623870561,
|
||||||
|
"amount1_initial": 192475437,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765630905,
|
||||||
|
"timestamp_close": 1765632623
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155863,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3097.40295247475,
|
||||||
|
"range_lower": 3080.973817800786,
|
||||||
|
"range_upper": 3111.93656358668,
|
||||||
|
"target_value": 308.3116676933205,
|
||||||
|
"amount0_initial": 39654626336294149,
|
||||||
|
"amount1_initial": 185485311,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765632672,
|
||||||
|
"timestamp_close": 1765634422
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5155882,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3112.8609359236384,
|
||||||
|
"range_lower": 3096.4164892771637,
|
||||||
|
"range_upper": 3127.5344286932063,
|
||||||
|
"target_value": 343.5299941433273,
|
||||||
|
"amount0_initial": 51896697111974758,
|
||||||
|
"amount1_initial": 181982793,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765634468,
|
||||||
|
"timestamp_close": 1765661569
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156323,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3083.0072388847652,
|
||||||
|
"range_lower": 3065.6081631285606,
|
||||||
|
"range_upper": 3096.4164892771637,
|
||||||
|
"target_value": 312.46495296583043,
|
||||||
|
"amount0_initial": 37786473705449745,
|
||||||
|
"amount1_initial": 195968981,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765661623,
|
||||||
|
"timestamp_close": 1765661755
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156327,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3099.025060823837,
|
||||||
|
"range_lower": 3080.973817800786,
|
||||||
|
"range_upper": 3111.93656358668,
|
||||||
|
"target_value": 341.5043895497362,
|
||||||
|
"amount0_initial": 44705050404757454,
|
||||||
|
"amount1_initial": 202962318,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765661800,
|
||||||
|
"timestamp_close": 1765663051
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156339,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3114.5494347315303,
|
||||||
|
"range_lower": 3096.4164892771637,
|
||||||
|
"range_upper": 3127.5344286932063,
|
||||||
|
"target_value": 313.18766451496026,
|
||||||
|
"amount0_initial": 47209859594870944,
|
||||||
|
"amount1_initial": 166150223,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765663096,
|
||||||
|
"timestamp_close": 1765675725,
|
||||||
|
"zone_bottom_limit_price": 3099.528283218768,
|
||||||
|
"zone_close_start_price": 3102.017718372051,
|
||||||
|
"zone_close_end_price": 3102.640077160372,
|
||||||
|
"zone_top_start_price": 3121.310840809998
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156507,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3128.29006521609,
|
||||||
|
"range_lower": 3111.93656358668,
|
||||||
|
"range_upper": 3143.2104745051906,
|
||||||
|
"target_value": 347.15268590066694,
|
||||||
|
"amount0_initial": 52797230582023401,
|
||||||
|
"amount1_initial": 181987634,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765675770,
|
||||||
|
"timestamp_close": 1765687389,
|
||||||
|
"zone_bottom_limit_price": 3115.0639546785314,
|
||||||
|
"zone_close_start_price": 3117.565867552012,
|
||||||
|
"zone_close_end_price": 3118.191345770382,
|
||||||
|
"zone_top_start_price": 3136.9556923214886
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156576,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3109.1484174484244,
|
||||||
|
"range_lower": 3093.3217751359653,
|
||||||
|
"range_upper": 3124.4086137206154,
|
||||||
|
"target_value": 349.75269804513647,
|
||||||
|
"amount0_initial": 55081765825023475,
|
||||||
|
"amount1_initial": 178495313,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765687433,
|
||||||
|
"timestamp_close": 1765712073,
|
||||||
|
"zone_bottom_limit_price": 3096.4304589944304,
|
||||||
|
"zone_close_start_price": 3098.9174060812024,
|
||||||
|
"zone_close_end_price": 3099.539142852895,
|
||||||
|
"zone_top_start_price": 3118.1912460036856
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156880,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3092.1804685415204,
|
||||||
|
"range_lower": 3074.8183354682296,
|
||||||
|
"range_upper": 3105.7192207366634,
|
||||||
|
"target_value": 348.0802699013006,
|
||||||
|
"amount0_initial": 49191436738181486,
|
||||||
|
"amount1_initial": 195971470,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765712124,
|
||||||
|
"timestamp_close": 1765712700,
|
||||||
|
"zone_bottom_limit_price": 3077.908423995073,
|
||||||
|
"zone_close_start_price": 3080.3804948165475,
|
||||||
|
"zone_close_end_price": 3080.9985125219164,
|
||||||
|
"zone_top_start_price": 3099.5390436829766
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156912,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3080.3709911881006,
|
||||||
|
"range_lower": 3062.5442403757074,
|
||||||
|
"range_upper": 3093.3217751359653,
|
||||||
|
"target_value": 291.15223765283383,
|
||||||
|
"amount0_initial": 47732710466839755,
|
||||||
|
"amount1_initial": 144117781,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765712910,
|
||||||
|
"timestamp_close": 1765714350,
|
||||||
|
"zone_bottom_limit_price": 3065.6219938517334,
|
||||||
|
"zone_close_start_price": 3068.084196632554,
|
||||||
|
"zone_close_end_price": 3068.699747327759,
|
||||||
|
"zone_top_start_price": 3087.166268183914
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5156972,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3090.0637108037877,
|
||||||
|
"range_lower": 3074.8183354682296,
|
||||||
|
"range_upper": 3102.615208978462,
|
||||||
|
"target_value": 271.3892587233541,
|
||||||
|
"amount0_initial": 51605992189032833,
|
||||||
|
"amount1_initial": 111923455,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765714399,
|
||||||
|
"timestamp_close": 1765715701,
|
||||||
|
"zone_bottom_limit_price": 3077.598022819253,
|
||||||
|
"zone_close_start_price": 3079.8217727000715,
|
||||||
|
"zone_close_end_price": 3080.3777101702763,
|
||||||
|
"zone_top_start_price": 3097.055834276415
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157018,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3101.5146208910464,
|
||||||
|
"range_lower": 3084.056178426586,
|
||||||
|
"range_upper": 3115.0499008952183,
|
||||||
|
"target_value": 334.88770454868376,
|
||||||
|
"amount0_initial": 49662753969037209,
|
||||||
|
"amount1_initial": 180857947,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765715747,
|
||||||
|
"timestamp_close": 1765722919,
|
||||||
|
"zone_bottom_limit_price": 3087.1555506734494,
|
||||||
|
"zone_close_start_price": 3089.6350484709396,
|
||||||
|
"zone_close_end_price": 3090.2549229203123,
|
||||||
|
"zone_top_start_price": 3108.851156401492
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157176,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3079.8157532039463,
|
||||||
|
"range_lower": 3062.5442403757074,
|
||||||
|
"range_upper": 3093.3217751359653,
|
||||||
|
"target_value": 272.62430135026136,
|
||||||
|
"amount0_initial": 24888578243851017,
|
||||||
|
"amount1_initial": 195972066,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765722970,
|
||||||
|
"timestamp_close": 1765729241,
|
||||||
|
"zone_bottom_limit_price": 3065.6219938517334,
|
||||||
|
"zone_close_start_price": 3068.084196632554,
|
||||||
|
"zone_close_end_price": 3068.699747327759,
|
||||||
|
"zone_top_start_price": 3087.166268183914
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157312,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3093.971464080226,
|
||||||
|
"range_lower": 3077.8945378409912,
|
||||||
|
"range_upper": 3108.8263379038003,
|
||||||
|
"target_value": 326.92184420403566,
|
||||||
|
"amount0_initial": 46843176767023226,
|
||||||
|
"amount1_initial": 181990392,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765729286,
|
||||||
|
"timestamp_close": 1765733514,
|
||||||
|
"zone_bottom_limit_price": 3080.987717847272,
|
||||||
|
"zone_close_start_price": 3083.4622618522967,
|
||||||
|
"zone_close_end_price": 3084.080897853553,
|
||||||
|
"zone_top_start_price": 3102.6399778912387
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157395,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3079.3931567773757,
|
||||||
|
"range_lower": 3062.5442403757074,
|
||||||
|
"range_upper": 3093.3217751359653,
|
||||||
|
"target_value": 344.4599070677894,
|
||||||
|
"amount0_initial": 50492037278704046,
|
||||||
|
"amount1_initial": 188975073,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765733564,
|
||||||
|
"timestamp_close": 1765736225,
|
||||||
|
"zone_bottom_limit_price": 3065.6219938517334,
|
||||||
|
"zone_close_start_price": 3068.084196632554,
|
||||||
|
"zone_close_end_price": 3068.699747327759,
|
||||||
|
"zone_top_start_price": 3087.166268183914
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157445,
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3095.4053081664565,
|
||||||
|
"range_lower": 3077.8945378409912,
|
||||||
|
"range_upper": 3108.8263379038003,
|
||||||
|
"target_value": 332.600152414756,
|
||||||
|
"amount0_initial": 44140371554667029,
|
||||||
|
"amount1_initial": 195967812,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765736272,
|
||||||
|
"timestamp_close": 1765743062,
|
||||||
|
"zone_bottom_limit_price": 3080.987717847272,
|
||||||
|
"zone_close_start_price": 3083.4622618522967,
|
||||||
|
"zone_close_end_price": 3084.080897853553,
|
||||||
|
"zone_top_start_price": 3102.6399778912387
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157680,
|
||||||
|
"opened": "22:21 14/12/25",
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3090.84,
|
||||||
|
"target_value": 1979.52,
|
||||||
|
"amount0_initial": 0.3137,
|
||||||
|
"amount1_initial": 1009.93,
|
||||||
|
"range_upper": 3121.29,
|
||||||
|
"zone_top_start_price": 3108.93,
|
||||||
|
"zone_close_top_price": 3092.24,
|
||||||
|
"zone_close_bottom_price": 3091.0,
|
||||||
|
"zone_bottom_limit_price": 3090.39,
|
||||||
|
"range_lower": 3059.48,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765747295,
|
||||||
|
"timestamp_close": 1765755472
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157819,
|
||||||
|
"opened": "00:45 15/12/25",
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3058.26,
|
||||||
|
"target_value": 1980.8,
|
||||||
|
"amount0_initial": 0.3044,
|
||||||
|
"amount1_initial": 1049.83,
|
||||||
|
"range_upper": 3087.14,
|
||||||
|
"zone_top_start_price": 3074.92,
|
||||||
|
"zone_close_top_price": 3059.02,
|
||||||
|
"zone_close_bottom_price": 3057.8,
|
||||||
|
"zone_bottom_limit_price": 3056.58,
|
||||||
|
"range_lower": 3026.02,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765755940,
|
||||||
|
"timestamp_close": 1765762761
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5157922,
|
||||||
|
"opened": "02:47 15/12/25",
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3104.56,
|
||||||
|
"target_value": 1980.84,
|
||||||
|
"amount0_initial": 0.2967,
|
||||||
|
"amount1_initial": 1059.84,
|
||||||
|
"range_upper": 3133.8,
|
||||||
|
"zone_top_start_price": 3121.39,
|
||||||
|
"zone_close_top_price": 3105.26,
|
||||||
|
"zone_close_bottom_price": 3104.02,
|
||||||
|
"zone_bottom_limit_price": 3102.78,
|
||||||
|
"range_lower": 3071.75,
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": 1765763228,
|
||||||
|
"timestamp_close": 1765765504
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": 5158011,
|
||||||
|
"opened": "03:32 15/12/25",
|
||||||
|
"status": "CLOSED",
|
||||||
|
"entry_price": 3135.31,
|
||||||
|
"target_value": 1983.24,
|
||||||
|
"amount0_initial": 0.3009,
|
||||||
|
"amount1_initial": 1039.86,
|
||||||
|
"range_upper": 3165.29,
|
||||||
|
"zone_top_start_price": 3152.76,
|
||||||
|
"zone_close_top_price": 3136.46,
|
||||||
|
"zone_close_bottom_price": 3135.21,
|
||||||
|
"zone_bottom_limit_price": 3133.95,
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
841
clp_hedger/uniswap_manager.py
Normal file
841
clp_hedger/uniswap_manager.py
Normal file
@ -0,0 +1,841 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from web3 import Web3
|
||||||
|
from eth_account import Account
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# --- Helper Functions ---
|
||||||
|
def clean_address(addr):
|
||||||
|
return re.sub(r'[^0-9a-fA-FxX]', '', addr)
|
||||||
|
|
||||||
|
def price_from_sqrt_price_x96(sqrt_price_x96, token0_decimals, token1_decimals):
|
||||||
|
price = (sqrt_price_x96 / (2**96))**2
|
||||||
|
# Adjust for token decimals assuming price is Token1 per Token0
|
||||||
|
price = price * (10**(token0_decimals - token1_decimals))
|
||||||
|
return price
|
||||||
|
|
||||||
|
def price_from_tick(tick, token0_decimals, token1_decimals):
|
||||||
|
price = 1.0001**tick
|
||||||
|
# Adjust for token decimals assuming price is Token1 per Token0
|
||||||
|
price = price * (10**(token0_decimals - token1_decimals))
|
||||||
|
return price
|
||||||
|
|
||||||
|
def from_wei(amount, decimals):
|
||||||
|
return amount / (10**decimals)
|
||||||
|
|
||||||
|
# --- V3 Math Helpers ---
|
||||||
|
def get_sqrt_ratio_at_tick(tick):
|
||||||
|
# Returns sqrt(price) as a Q96 number
|
||||||
|
return int((1.0001 ** (tick / 2)) * (2 ** 96))
|
||||||
|
|
||||||
|
def get_liquidity_for_amount0(sqrt_ratio_a, sqrt_ratio_b, amount0):
|
||||||
|
# This function is not used directly in the current calculate_mint_amounts logic,
|
||||||
|
# but is a common V3 helper
|
||||||
|
if sqrt_ratio_a > sqrt_ratio_b:
|
||||||
|
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||||
|
# This formula is for a single-sided deposit when current price is outside the range
|
||||||
|
return int(amount0 * sqrt_ratio_a * sqrt_ratio_b / (sqrt_ratio_b - sqrt_ratio_a))
|
||||||
|
|
||||||
|
def get_liquidity_for_amount1(sqrt_ratio_a, sqrt_ratio_b, amount1):
|
||||||
|
# This function is not used directly in the current calculate_mint_amounts logic,
|
||||||
|
# but is a common V3 helper
|
||||||
|
if sqrt_ratio_a > sqrt_ratio_b:
|
||||||
|
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||||
|
# This formula is for a single-sided deposit when current price is outside the range
|
||||||
|
return int(amount1 / (sqrt_ratio_b - sqrt_ratio_a))
|
||||||
|
|
||||||
|
def get_amounts_for_liquidity(sqrt_ratio_current, sqrt_ratio_a, sqrt_ratio_b, liquidity):
|
||||||
|
# Calculates the required amount of token0 and token1 for a given liquidity and price range
|
||||||
|
if sqrt_ratio_a > sqrt_ratio_b:
|
||||||
|
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
|
||||||
|
|
||||||
|
amount0 = 0
|
||||||
|
amount1 = 0
|
||||||
|
Q96 = 1 << 96 # 2^96
|
||||||
|
|
||||||
|
# Current price below the lower tick boundary
|
||||||
|
if sqrt_ratio_current <= sqrt_ratio_a:
|
||||||
|
amount0 = ((liquidity * Q96) // sqrt_ratio_a) - ((liquidity * Q96) // sqrt_ratio_b)
|
||||||
|
amount1 = 0
|
||||||
|
# Current price within the range
|
||||||
|
elif sqrt_ratio_current < sqrt_ratio_b:
|
||||||
|
amount0 = ((liquidity * Q96) // sqrt_ratio_current) - ((liquidity * Q96) // sqrt_ratio_b)
|
||||||
|
amount1 = (liquidity * (sqrt_ratio_current - sqrt_ratio_a)) // Q96
|
||||||
|
# Current price above the upper tick boundary
|
||||||
|
else:
|
||||||
|
amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96
|
||||||
|
amount0 = 0
|
||||||
|
|
||||||
|
return amount0, amount1
|
||||||
|
|
||||||
|
# --- Configuration ---
|
||||||
|
# RPC URL and Private Key are loaded from .env
|
||||||
|
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 = 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
|
||||||
|
OPEN_POSITION_ENABLED = True # If True, will open a new position if no auto position exists
|
||||||
|
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 = 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"
|
||||||
|
|
||||||
|
# --- JSON State Helpers ---
|
||||||
|
def get_active_automatic_position():
|
||||||
|
"""Reads hedge_status.json and returns the first OPEN AUTOMATIC 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') == 'AUTOMATIC' and entry.get('status') == 'OPEN':
|
||||||
|
return entry
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR reading status file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all_open_positions():
|
||||||
|
"""Reads hedge_status.json and returns a list of all OPEN positions (Manual and Automatic)."""
|
||||||
|
if not os.path.exists(STATUS_FILE):
|
||||||
|
return []
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return [entry for entry in data if entry.get('status') == 'OPEN']
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR reading status file: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def update_hedge_status_file(action, position_data):
|
||||||
|
"""
|
||||||
|
Updates the hedge_status.json file.
|
||||||
|
action: "OPEN" or "CLOSE"
|
||||||
|
position_data: Dict containing details (token_id, entry_price, range, etc.)
|
||||||
|
"""
|
||||||
|
current_data = []
|
||||||
|
if os.path.exists(STATUS_FILE):
|
||||||
|
try:
|
||||||
|
with open(STATUS_FILE, 'r') as f:
|
||||||
|
current_data = json.load(f)
|
||||||
|
except:
|
||||||
|
current_data = []
|
||||||
|
|
||||||
|
if action == "OPEN":
|
||||||
|
# Format Timestamp
|
||||||
|
open_ts = int(time.time())
|
||||||
|
opened_str = time.strftime('%H:%M %d/%m/%y', time.localtime(open_ts))
|
||||||
|
|
||||||
|
# Scale Amounts
|
||||||
|
raw_amt0 = position_data.get('amount0_initial', 0)
|
||||||
|
raw_amt1 = position_data.get('amount1_initial', 0)
|
||||||
|
|
||||||
|
# Handle if they are already scaled (unlikely here, but safe)
|
||||||
|
if raw_amt0 > 1000: fmt_amt0 = round(raw_amt0 / 10**18, 4)
|
||||||
|
else: fmt_amt0 = round(raw_amt0, 4)
|
||||||
|
|
||||||
|
if raw_amt1 > 1000: fmt_amt1 = round(raw_amt1 / 10**6, 2)
|
||||||
|
else: fmt_amt1 = round(raw_amt1, 2)
|
||||||
|
|
||||||
|
new_entry = {
|
||||||
|
"type": "AUTOMATIC",
|
||||||
|
"token_id": position_data['token_id'],
|
||||||
|
"opened": opened_str,
|
||||||
|
"status": "OPEN",
|
||||||
|
"entry_price": round(position_data['entry_price'], 2),
|
||||||
|
"target_value": round(position_data['target_value'], 2), # Use actual calculated value
|
||||||
|
"amount0_initial": fmt_amt0,
|
||||||
|
"amount1_initial": fmt_amt1,
|
||||||
|
|
||||||
|
"range_upper": round(position_data['range_upper'], 2),
|
||||||
|
# Zones (if present in position_data, otherwise None/Skip)
|
||||||
|
"zone_top_start_price": round(position_data['zone_top_start_price'], 2) if 'zone_top_start_price' in position_data else None,
|
||||||
|
"zone_close_top_price": round(position_data['zone_close_end_price'], 2) if 'zone_close_end_price' in position_data else None,
|
||||||
|
"zone_close_bottom_price": round(position_data['zone_close_start_price'], 2) if 'zone_close_start_price' in position_data else None,
|
||||||
|
"zone_bottom_limit_price": round(position_data['zone_bottom_limit_price'], 2) if 'zone_bottom_limit_price' in position_data else None,
|
||||||
|
"range_lower": round(position_data['range_lower'], 2),
|
||||||
|
|
||||||
|
"static_long": 0.0,
|
||||||
|
"timestamp_open": open_ts,
|
||||||
|
"timestamp_close": None
|
||||||
|
}
|
||||||
|
# Remove None keys to keep it clean? Or keep structure?
|
||||||
|
# User wants specific structure.
|
||||||
|
|
||||||
|
current_data.append(new_entry)
|
||||||
|
print(f"Recorded new AUTOMATIC position {position_data['token_id']} in {STATUS_FILE}")
|
||||||
|
|
||||||
|
elif action == "CLOSING":
|
||||||
|
found = False
|
||||||
|
for entry in current_data:
|
||||||
|
if (
|
||||||
|
entry.get('type') == "AUTOMATIC" and
|
||||||
|
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
|
||||||
|
if not found:
|
||||||
|
print(f"WARNING: Could not find open AUTOMATIC position {position_data['token_id']} to close.")
|
||||||
|
|
||||||
|
with open(STATUS_FILE, 'w') as f:
|
||||||
|
json.dump(current_data, f, indent=2)
|
||||||
|
|
||||||
|
# --- ABIs ---
|
||||||
|
# Simplified for length, usually loaded from huge string
|
||||||
|
NONFUNGIBLE_POSITION_MANAGER_ABI = json.loads('''
|
||||||
|
[
|
||||||
|
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"indexed": false, "internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"indexed": false, "internalType": "uint256", "name": "amount0", "type": "uint256"}, {"indexed": false, "internalType": "uint256", "name": "amount1", "type": "uint256"}], "name": "IncreaseLiquidity", "type": "event"},
|
||||||
|
{"anonymous": false, "inputs": [{"indexed": true, "internalType": "address", "name": "from", "type": "address"}, {"indexed": true, "internalType": "address", "name": "to", "type": "address"}, {"indexed": true, "internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "Transfer", "type": "event"},
|
||||||
|
{"inputs": [], "name": "factory", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}], "name": "positions", "outputs": [{"internalType": "uint96", "name": "nonce", "type": "uint96"}, {"internalType": "address", "name": "operator", "type": "address"}, {"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "feeGrowthInside0LastX128", "type": "uint256"}, {"internalType": "uint256", "name": "feeGrowthInside1LastX128", "type": "uint256"}, {"internalType": "uint128", "name": "tokensOwed0", "type": "uint128"}, {"internalType": "uint128", "name": "tokensOwed1", "type": "uint128"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint128", "name": "amount0Max", "type": "uint128"}, {"internalType": "uint128", "name": "amount1Max", "type": "uint128"}], "internalType": "struct INonfungiblePositionManager.CollectParams", "name": "params", "type": "tuple"}], "name": "collect", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||||
|
{"inputs": [{"components": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.DecreaseLiquidityParams", "name": "params", "type": "tuple"}], "name": "decreaseLiquidity", "outputs": [{"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"},
|
||||||
|
{"inputs": [{"components": [{"internalType": "address", "name": "token0", "type": "address"}, {"internalType": "address", "name": "token1", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "int24", "name": "tickLower", "type": "int24"}, {"internalType": "int24", "name": "tickUpper", "type": "int24"}, {"internalType": "uint256", "name": "amount0Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Desired", "type": "uint256"}, {"internalType": "uint256", "name": "amount0Min", "type": "uint256"}, {"internalType": "uint256", "name": "amount1Min", "type": "uint256"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}], "internalType": "struct INonfungiblePositionManager.MintParams", "name": "params", "type": "tuple"}], "name": "mint", "outputs": [{"internalType": "uint256", "name": "tokenId", "type": "uint256"}, {"internalType": "uint128", "name": "liquidity", "type": "uint128"}, {"internalType": "uint256", "name": "amount0", "type": "uint256"}, {"internalType": "uint256", "name": "amount1", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
UNISWAP_V3_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"},
|
||||||
|
{"inputs": [], "name": "token0", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [], "name": "token1", "outputs": [{"internalType": "address", "name": "", "type": "address"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [], "name": "fee", "outputs": [{"internalType": "uint24", "name": "", "type": "uint24"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [], "name": "liquidity", "outputs": [{"internalType": "uint128", "name": "", "type": "uint128"}], "stateMutability": "view", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
ERC20_ABI = json.loads('''
|
||||||
|
[
|
||||||
|
{"inputs": [], "name": "decimals", "outputs": [{"internalType": "uint8", "name": "", "type": "uint8"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [], "name": "symbol", "outputs": [{"internalType": "string", "name": "", "type": "string"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [{"internalType": "address", "name": "account", "type": "address"}], "name": "balanceOf", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"},
|
||||||
|
{"inputs": [{"internalType": "address", "name": "spender", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"}], "name": "approve", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function"},
|
||||||
|
{"inputs": [{"internalType": "address", "name": "owner", "type": "address"}, {"internalType": "address", "name": "spender", "type": "address"}], "name": "allowance", "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], "stateMutability": "view", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
UNISWAP_V3_FACTORY_ABI = json.loads('''
|
||||||
|
[
|
||||||
|
{"inputs": [{"internalType": "address", "name": "tokenA", "type": "address"}, {"internalType": "address", "name": "tokenB", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}], "name": "getPool", "outputs": [{"internalType": "address", "name": "pool", "type": "address"}], "stateMutability": "view", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
SWAP_ROUTER_ABI = json.loads('''
|
||||||
|
[
|
||||||
|
{"inputs": [{"components": [{"internalType": "address", "name": "tokenIn", "type": "address"}, {"internalType": "address", "name": "tokenOut", "type": "address"}, {"internalType": "uint24", "name": "fee", "type": "uint24"}, {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "deadline", "type": "uint256"}, {"internalType": "uint256", "name": "amountIn", "type": "uint256"}, {"internalType": "uint256", "name": "amountOutMinimum", "type": "uint256"}, {"internalType": "uint160", "name": "sqrtPriceLimitX96", "type": "uint160"}], "internalType": "struct ISwapRouter.ExactInputSingleParams", "name": "params", "type": "tuple"}], "name": "exactInputSingle", "outputs": [{"internalType": "uint256", "name": "amountOut", "type": "uint256"}], "stateMutability": "payable", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
WETH9_ABI = json.loads('''
|
||||||
|
[
|
||||||
|
{"constant": false, "inputs": [], "name": "deposit", "outputs": [], "payable": true, "stateMutability": "payable", "type": "function"},
|
||||||
|
{"constant": false, "inputs": [{"name": "wad", "type": "uint256"}], "name": "withdraw", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function"}
|
||||||
|
]
|
||||||
|
''')
|
||||||
|
|
||||||
|
NONFUNGIBLE_POSITION_MANAGER_ADDRESS = bytes.fromhex("C36442b4" + "a4522E87" + "1399CD71" + "7aBDD847" + "Ab11FE88")
|
||||||
|
UNISWAP_V3_SWAP_ROUTER_ADDRESS = bytes.fromhex("E592427A0AEce92De3Edee1F18E0157C05861564")
|
||||||
|
WETH_ADDRESS = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # Arbitrum WETH
|
||||||
|
|
||||||
|
# --- Core Logic Functions ---
|
||||||
|
def get_position_details(w3_instance, npm_c, factory_c, token_id):
|
||||||
|
try:
|
||||||
|
position_data = npm_c.functions.positions(token_id).call()
|
||||||
|
(nonce, operator, token0_address, token1_address, fee, tickLower, tickUpper, liquidity,
|
||||||
|
feeGrowthInside0, feeGrowthInside1, tokensOwed0, tokensOwed1) = position_data
|
||||||
|
|
||||||
|
token0_contract = w3_instance.eth.contract(address=token0_address, abi=ERC20_ABI)
|
||||||
|
token1_contract = w3_instance.eth.contract(address=token1_address, abi=ERC20_ABI)
|
||||||
|
token0_symbol = token0_contract.functions.symbol().call()
|
||||||
|
token1_symbol = token1_contract.functions.symbol().call()
|
||||||
|
token0_decimals = token0_contract.functions.decimals().call()
|
||||||
|
token1_decimals = token1_contract.functions.decimals().call()
|
||||||
|
|
||||||
|
pool_address = factory_c.functions.getPool(token0_address, token1_address, fee).call()
|
||||||
|
if pool_address == '0x0000000000000000000000000000000000000000':
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
pool_contract = w3_instance.eth.contract(address=pool_address, abi=UNISWAP_V3_POOL_ABI)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"token0_address": token0_address, "token1_address": token1_address,
|
||||||
|
"token0_symbol": token0_symbol, "token1_symbol": token1_symbol,
|
||||||
|
"token0_decimals": token0_decimals, "token1_decimals": token1_decimals,
|
||||||
|
"fee": fee, "tickLower": tickLower, "tickUpper": tickUpper, "liquidity": liquidity,
|
||||||
|
"pool_address": pool_address
|
||||||
|
}, pool_contract
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR fetching position details: {e}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def get_pool_dynamic_data(pool_c):
|
||||||
|
try:
|
||||||
|
slot0_data = pool_c.functions.slot0().call()
|
||||||
|
return {"sqrtPriceX96": slot0_data[0], "tick": slot0_data[1]}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"ERROR fetching pool dynamic data: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calculate_mint_amounts(current_tick, tick_lower, tick_upper, investment_value_token1, decimals0, decimals1, sqrt_price_current_x96):
|
||||||
|
sqrt_price_current = get_sqrt_ratio_at_tick(current_tick)
|
||||||
|
sqrt_price_lower = get_sqrt_ratio_at_tick(tick_lower)
|
||||||
|
sqrt_price_upper = get_sqrt_ratio_at_tick(tick_upper)
|
||||||
|
|
||||||
|
# 1. Get Price of Token0 in terms of Token1
|
||||||
|
price_of_token0_in_token1_units = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1)
|
||||||
|
|
||||||
|
# 2. Estimate Amounts
|
||||||
|
L_test = 1 << 128
|
||||||
|
amt0_test, amt1_test = get_amounts_for_liquidity(sqrt_price_current, sqrt_price_lower, sqrt_price_upper, L_test)
|
||||||
|
|
||||||
|
# 3. Adjust for decimals
|
||||||
|
real_amt0_test = amt0_test / (10**decimals0)
|
||||||
|
real_amt1_test = amt1_test / (10**decimals1)
|
||||||
|
|
||||||
|
# 4. Calculate Total Value of Test Position in Token1 terms
|
||||||
|
value_test = (real_amt0_test * price_of_token0_in_token1_units) + real_amt1_test
|
||||||
|
|
||||||
|
if value_test == 0:
|
||||||
|
return 0, 0
|
||||||
|
|
||||||
|
# 5. Scale
|
||||||
|
scale = investment_value_token1 / value_test
|
||||||
|
|
||||||
|
# 6. Final Amounts
|
||||||
|
final_amt0 = int(amt0_test * scale)
|
||||||
|
final_amt1 = int(amt1_test * scale)
|
||||||
|
|
||||||
|
return final_amt0, final_amt1
|
||||||
|
|
||||||
|
def check_and_swap(w3_instance, router_contract, account, token0, token1, amount0_needed, amount1_needed):
|
||||||
|
token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||||
|
token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||||
|
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||||
|
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||||
|
|
||||||
|
# Debug Balances
|
||||||
|
s0 = token0_contract.functions.symbol().call()
|
||||||
|
s1 = token1_contract.functions.symbol().call()
|
||||||
|
d0 = token0_contract.functions.decimals().call()
|
||||||
|
d1 = token1_contract.functions.decimals().call()
|
||||||
|
|
||||||
|
print(f"\n--- WALLET CHECK ---")
|
||||||
|
print(f"Required: {from_wei(amount0_needed, d0):.6f} {s0} | {from_wei(amount1_needed, d1):.2f} {s1}")
|
||||||
|
print(f"Balance : {from_wei(bal0, d0):.6f} {s0} | {from_wei(bal1, d1):.2f} {s1}")
|
||||||
|
|
||||||
|
deficit0 = max(0, amount0_needed - bal0)
|
||||||
|
deficit1 = max(0, amount1_needed - bal1)
|
||||||
|
|
||||||
|
if deficit0 > 0: print(f"Deficit {s0}: {from_wei(deficit0, d0):.6f}")
|
||||||
|
if deficit1 > 0: print(f"Deficit {s1}: {from_wei(deficit1, d1):.2f}")
|
||||||
|
|
||||||
|
# --- AUTO-WRAP ETH LOGIC ---
|
||||||
|
weth_addr_lower = WETH_ADDRESS.lower()
|
||||||
|
|
||||||
|
# Wrap for Token0 Deficit
|
||||||
|
if (deficit0 > 0 or deficit1 > 0) and token0.lower() == weth_addr_lower:
|
||||||
|
native_bal = w3_instance.eth.get_balance(account.address)
|
||||||
|
gas_reserve = 5 * 10**15 # 0.005 ETH (Reduced for L2)
|
||||||
|
available_native = max(0, native_bal - gas_reserve)
|
||||||
|
|
||||||
|
amount_to_wrap = 0
|
||||||
|
if deficit0 > 0:
|
||||||
|
amount_to_wrap = deficit0
|
||||||
|
|
||||||
|
if deficit1 > 0:
|
||||||
|
amount_to_wrap = available_native
|
||||||
|
|
||||||
|
amount_to_wrap = min(amount_to_wrap, available_native)
|
||||||
|
|
||||||
|
if amount_to_wrap > 0:
|
||||||
|
print(f"Auto-Wrapping {from_wei(amount_to_wrap, 18)} ETH to WETH...")
|
||||||
|
weth_contract = w3_instance.eth.contract(address=token0, abi=WETH9_ABI)
|
||||||
|
wrap_txn = weth_contract.functions.deposit().build_transaction({
|
||||||
|
'from': account.address, 'value': amount_to_wrap, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key)
|
||||||
|
raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap)
|
||||||
|
print(f"Wrap Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||||
|
deficit0 = max(0, amount0_needed - bal0)
|
||||||
|
else:
|
||||||
|
if deficit0 > 0:
|
||||||
|
print(f"Insufficient Native ETH to wrap. Need: {from_wei(deficit0, 18)}, Available: {from_wei(available_native, 18)}")
|
||||||
|
|
||||||
|
# Wrap for Token1 Deficit (if Token1 is WETH)
|
||||||
|
if deficit1 > 0 and token1.lower() == weth_addr_lower:
|
||||||
|
native_bal = w3_instance.eth.get_balance(account.address)
|
||||||
|
gas_reserve = 5 * 10**15 # 0.005 ETH
|
||||||
|
available_native = max(0, native_bal - gas_reserve)
|
||||||
|
if available_native >= deficit1:
|
||||||
|
print(f"Auto-Wrapping {from_wei(deficit1, 18)} ETH to WETH...")
|
||||||
|
weth_contract = w3_instance.eth.contract(address=token1, abi=WETH9_ABI)
|
||||||
|
wrap_txn = weth_contract.functions.deposit().build_transaction({
|
||||||
|
'from': account.address, 'value': deficit1, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_wrap = w3_instance.eth.account.sign_transaction(wrap_txn, private_key=account.key)
|
||||||
|
raw_wrap = signed_wrap.rawTransaction if hasattr(signed_wrap, 'rawTransaction') else signed_wrap.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw_wrap)
|
||||||
|
print(f"Wrap Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||||
|
deficit1 = max(0, amount1_needed - bal1)
|
||||||
|
|
||||||
|
if deficit0 == 0 and deficit1 == 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if deficit0 > 0 and bal1 > amount1_needed:
|
||||||
|
surplus1 = bal1 - amount1_needed
|
||||||
|
print(f"Swapping surplus Token1 ({surplus1}) for Token0...")
|
||||||
|
|
||||||
|
approve_txn = token1_contract.functions.approve(router_contract.address, surplus1).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||||
|
'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||||
|
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||||
|
w3_instance.eth.send_raw_transaction(raw)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
params = (token1, token0, 500, account.address, int(time.time()) + 120, surplus1, 0, 0)
|
||||||
|
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||||
|
'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key)
|
||||||
|
raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw_swap)
|
||||||
|
print(f"Swap Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
|
||||||
|
# Verify Balance After Swap
|
||||||
|
bal0 = token0_contract.functions.balanceOf(account.address).call()
|
||||||
|
if bal0 < amount0_needed:
|
||||||
|
print(f"❌ Swap insufficient. Have {bal0}, Need {amount0_needed}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
elif deficit1 > 0 and bal0 > amount0_needed:
|
||||||
|
surplus0 = bal0 - amount0_needed
|
||||||
|
print(f"Swapping surplus Token0 ({surplus0}) for Token1...")
|
||||||
|
|
||||||
|
approve_txn = token0_contract.functions.approve(router_contract.address, surplus0).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||||
|
'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed = w3_instance.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||||
|
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||||
|
w3_instance.eth.send_raw_transaction(raw)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
params = (token0, token1, 500, account.address, int(time.time()) + 120, surplus0, 0, 0)
|
||||||
|
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 300000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee,
|
||||||
|
'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_swap = w3_instance.eth.account.sign_transaction(swap_txn, private_key=account.key)
|
||||||
|
raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw_swap)
|
||||||
|
print(f"Swap Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
|
||||||
|
# Verify Balance After Swap
|
||||||
|
bal1 = token1_contract.functions.balanceOf(account.address).call()
|
||||||
|
if bal1 < amount1_needed:
|
||||||
|
print(f"❌ Swap insufficient. Have {bal1}, Need {amount1_needed}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
print("❌ Insufficient funds for required amounts.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_token_balances(w3_instance, account_address, token0_address, token1_address):
|
||||||
|
try:
|
||||||
|
token0_contract = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||||
|
token1_contract = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||||
|
b0 = token0_contract.functions.balanceOf(account_address).call()
|
||||||
|
b1 = token1_contract.functions.balanceOf(account_address).call()
|
||||||
|
return b0, b1
|
||||||
|
except: return 0, 0
|
||||||
|
|
||||||
|
def decrease_liquidity(w3_instance, npm_contract, account, position_id, liquidity_amount):
|
||||||
|
try:
|
||||||
|
txn = npm_contract.functions.decreaseLiquidity((position_id, liquidity_amount, 0, 0, int(time.time()) + 180)).build_transaction({
|
||||||
|
'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key)
|
||||||
|
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw)
|
||||||
|
print(f"Decrease Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error decreasing: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def mint_new_position(w3_instance, npm_contract, account, token0, token1, amount0, amount1, tick_lower, tick_upper):
|
||||||
|
print(f"\n--- Attempting to Mint ---")
|
||||||
|
try:
|
||||||
|
token0_c = w3_instance.eth.contract(address=token0, abi=ERC20_ABI)
|
||||||
|
token1_c = w3_instance.eth.contract(address=token1, abi=ERC20_ABI)
|
||||||
|
|
||||||
|
# Approve 0
|
||||||
|
txn0 = token0_c.functions.approve(npm_contract.address, amount0).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed0 = w3_instance.eth.account.sign_transaction(txn0, private_key=account.key)
|
||||||
|
raw0 = signed0.rawTransaction if hasattr(signed0, 'rawTransaction') else signed0.raw_transaction
|
||||||
|
w3_instance.eth.send_raw_transaction(raw0)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Approve 1
|
||||||
|
txn1 = token1_c.functions.approve(npm_contract.address, amount1).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 100000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed1 = w3_instance.eth.account.sign_transaction(txn1, private_key=account.key)
|
||||||
|
raw1 = signed1.rawTransaction if hasattr(signed1, 'rawTransaction') else signed1.raw_transaction
|
||||||
|
w3_instance.eth.send_raw_transaction(raw1)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Mint
|
||||||
|
params = (token0, token1, 500, tick_lower, tick_upper, amount0, amount1, 0, 0, account.address, int(time.time()) + 180)
|
||||||
|
mint_txn = npm_contract.functions.mint(params).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3_instance.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 800000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_mint = w3_instance.eth.account.sign_transaction(mint_txn, private_key=account.key)
|
||||||
|
raw_mint = signed_mint.rawTransaction if hasattr(signed_mint, 'rawTransaction') else signed_mint.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw_mint)
|
||||||
|
print(f"Mint Sent: {tx_hash.hex()}")
|
||||||
|
|
||||||
|
receipt = w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
if receipt.status == 1:
|
||||||
|
print("✅ Mint Successful!")
|
||||||
|
|
||||||
|
result_data = {'token_id': None, 'liquidity': 0, 'amount0': 0, 'amount1': 0}
|
||||||
|
|
||||||
|
# Web3.py Event Processing to capture ID and Amounts
|
||||||
|
try:
|
||||||
|
# 1. Capture Token ID from Transfer event
|
||||||
|
transfer_events = npm_contract.events.Transfer().process_receipt(receipt)
|
||||||
|
for event in transfer_events:
|
||||||
|
if event['args']['from'] == "0x0000000000000000000000000000000000000000":
|
||||||
|
result_data['token_id'] = event['args']['tokenId']
|
||||||
|
break
|
||||||
|
|
||||||
|
# 2. Capture Amounts from IncreaseLiquidity event
|
||||||
|
inc_liq_events = npm_contract.events.IncreaseLiquidity().process_receipt(receipt)
|
||||||
|
for event in inc_liq_events:
|
||||||
|
if result_data['token_id'] and event['args']['tokenId'] == result_data['token_id']:
|
||||||
|
result_data['amount0'] = event['args']['amount0']
|
||||||
|
result_data['amount1'] = event['args']['amount1']
|
||||||
|
result_data['liquidity'] = event['args']['liquidity']
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Event Processing Warning: {e}")
|
||||||
|
|
||||||
|
if result_data['token_id']:
|
||||||
|
print(f"Captured: ID {result_data['token_id']}, Amt0 {result_data['amount0']}, Amt1 {result_data['amount1']}")
|
||||||
|
return result_data
|
||||||
|
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
print("❌ Mint Failed!")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Mint Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def collect_fees(w3_instance, npm_contract, account, position_id):
|
||||||
|
try:
|
||||||
|
txn = npm_contract.functions.collect((position_id, account.address, 2**128-1, 2**128-1)).build_transaction({
|
||||||
|
'from': account.address, 'gas': 1000000, 'maxFeePerGas': w3_instance.eth.gas_price * 2, 'maxPriorityFeePerGas': w3_instance.eth.max_priority_fee, 'nonce': w3_instance.eth.get_transaction_count(account.address), 'chainId': w3_instance.eth.chain_id
|
||||||
|
})
|
||||||
|
signed = w3_instance.eth.account.sign_transaction(txn, private_key=account.key)
|
||||||
|
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||||
|
tx_hash = w3_instance.eth.send_raw_transaction(raw)
|
||||||
|
print(f"Collect Sent: {tx_hash.hex()}")
|
||||||
|
w3_instance.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
return True
|
||||||
|
except: return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(f"CWD: {os.getcwd()}")
|
||||||
|
# Load .env from current directory
|
||||||
|
load_dotenv(override=True)
|
||||||
|
|
||||||
|
rpc_url = os.environ.get("MAINNET_RPC_URL")
|
||||||
|
private_key = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
|
||||||
|
|
||||||
|
if not rpc_url or not private_key:
|
||||||
|
print("Missing RPC or Private Key.")
|
||||||
|
return
|
||||||
|
|
||||||
|
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
||||||
|
if not w3.is_connected():
|
||||||
|
print("RPC Connection Failed")
|
||||||
|
return
|
||||||
|
print(f"Connected to Chain ID: {w3.eth.chain_id}")
|
||||||
|
|
||||||
|
account = Account.from_key(private_key)
|
||||||
|
w3.eth.default_account = account.address
|
||||||
|
print(f"Wallet: {account.address}")
|
||||||
|
|
||||||
|
npm_contract = w3.eth.contract(address=NONFUNGIBLE_POSITION_MANAGER_ADDRESS, abi=NONFUNGIBLE_POSITION_MANAGER_ABI)
|
||||||
|
factory_addr = npm_contract.functions.factory().call()
|
||||||
|
factory_contract = w3.eth.contract(address=factory_addr, abi=UNISWAP_V3_FACTORY_ABI)
|
||||||
|
router_contract = w3.eth.contract(address=UNISWAP_V3_SWAP_ROUTER_ADDRESS, abi=SWAP_ROUTER_ABI)
|
||||||
|
|
||||||
|
print("\n--- STARTING LIFECYCLE MANAGER ---")
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
# 1. Get All Open Positions
|
||||||
|
all_positions = get_all_open_positions()
|
||||||
|
|
||||||
|
# Check if we have an active AUTOMATIC position
|
||||||
|
active_automatic_position = next((p for p in all_positions if p['type'] == 'AUTOMATIC' and p['status'] == 'OPEN'), None)
|
||||||
|
|
||||||
|
if all_positions:
|
||||||
|
print("\n" + "="*60)
|
||||||
|
print(f"Monitoring at: {time.strftime('%Y-%m-%d %H:%M:%S', time.localtime())}")
|
||||||
|
|
||||||
|
for position in all_positions:
|
||||||
|
token_id = position['token_id']
|
||||||
|
pos_type = position['type']
|
||||||
|
|
||||||
|
# Fetch Details
|
||||||
|
pos_details, pool_c = get_position_details(w3, npm_contract, factory_contract, token_id)
|
||||||
|
if not pos_details:
|
||||||
|
print(f"ERROR: Could not get details for Position {token_id}. Skipping.")
|
||||||
|
continue
|
||||||
|
|
||||||
|
pool_data = get_pool_dynamic_data(pool_c)
|
||||||
|
current_tick = pool_data['tick']
|
||||||
|
|
||||||
|
# Calculate Fees (Simulation)
|
||||||
|
unclaimed0 = 0
|
||||||
|
unclaimed1 = 0
|
||||||
|
try:
|
||||||
|
fees_sim = npm_contract.functions.collect((token_id, "0x0000000000000000000000000000000000000000", 2**128-1, 2**128-1)).call()
|
||||||
|
unclaimed0 = from_wei(fees_sim[0], pos_details['token0_decimals'])
|
||||||
|
unclaimed1 = from_wei(fees_sim[1], pos_details['token1_decimals'])
|
||||||
|
except: pass
|
||||||
|
|
||||||
|
# Calculate Total Fee Value in Token1 (USDC)
|
||||||
|
# Get Current Price from Pool Data
|
||||||
|
current_price = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], pos_details['token0_decimals'], pos_details['token1_decimals'])
|
||||||
|
total_fees_usd = (unclaimed0 * current_price) + unclaimed1
|
||||||
|
|
||||||
|
# Check Range
|
||||||
|
is_out_of_range = False
|
||||||
|
status_str = "IN RANGE"
|
||||||
|
if current_tick < pos_details['tickLower']:
|
||||||
|
is_out_of_range = True
|
||||||
|
status_str = "OUT OF RANGE (BELOW)"
|
||||||
|
elif current_tick >= pos_details['tickUpper']:
|
||||||
|
is_out_of_range = True
|
||||||
|
status_str = "OUT OF RANGE (ABOVE)"
|
||||||
|
|
||||||
|
print(f"\nID: {token_id} | Type: {pos_type} | Status: {status_str}")
|
||||||
|
print(f" Range: {position['range_lower']:.2f} - {position['range_upper']:.2f}")
|
||||||
|
print(f" Fees: {unclaimed0:.4f} {pos_details['token0_symbol']} / {unclaimed1:.4f} {pos_details['token1_symbol']} (~${total_fees_usd:.2f})")
|
||||||
|
|
||||||
|
# --- AUTO CLOSE LOGIC (AUTOMATIC ONLY) ---
|
||||||
|
if pos_type == 'AUTOMATIC' and CLOSE_POSITION_ENABLED and is_out_of_range:
|
||||||
|
print(f"⚠️ Automatic Position {token_id} is OUT OF RANGE! Initiating Close...")
|
||||||
|
liq = pos_details['liquidity']
|
||||||
|
if liq > 0:
|
||||||
|
# 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)":
|
||||||
|
print("📉 Position closed BELOW range (100% ETH). Selling 50% of WETH inventory to USDC...")
|
||||||
|
try:
|
||||||
|
# Get WETH Balance
|
||||||
|
token0_c = w3.eth.contract(address=pos_details['token0_address'], abi=ERC20_ABI)
|
||||||
|
weth_bal = token0_c.functions.balanceOf(account.address).call()
|
||||||
|
|
||||||
|
amount_in = weth_bal // 2
|
||||||
|
|
||||||
|
if amount_in > 0:
|
||||||
|
# Approve Router
|
||||||
|
approve_txn = token0_c.functions.approve(router_contract.address, amount_in).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 100000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee,
|
||||||
|
'chainId': w3.eth.chain_id
|
||||||
|
})
|
||||||
|
signed = w3.eth.account.sign_transaction(approve_txn, private_key=account.key)
|
||||||
|
raw = signed.rawTransaction if hasattr(signed, 'rawTransaction') else signed.raw_transaction
|
||||||
|
w3.eth.send_raw_transaction(raw)
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Swap WETH -> USDC
|
||||||
|
params = (pos_details['token0_address'], pos_details['token1_address'], 500, account.address, int(time.time()) + 120, amount_in, 0, 0)
|
||||||
|
swap_txn = router_contract.functions.exactInputSingle(params).build_transaction({
|
||||||
|
'from': account.address, 'nonce': w3.eth.get_transaction_count(account.address),
|
||||||
|
'gas': 300000, 'maxFeePerGas': w3.eth.gas_price * 2, 'maxPriorityFeePerGas': w3.eth.max_priority_fee,
|
||||||
|
'chainId': w3.eth.chain_id
|
||||||
|
})
|
||||||
|
signed_swap = w3.eth.account.sign_transaction(swap_txn, private_key=account.key)
|
||||||
|
raw_swap = signed_swap.rawTransaction if hasattr(signed_swap, 'rawTransaction') else signed_swap.raw_transaction
|
||||||
|
tx_hash = w3.eth.send_raw_transaction(raw_swap)
|
||||||
|
print(f"⚖️ Rebalance Swap Sent: {tx_hash.hex()}")
|
||||||
|
w3.eth.wait_for_transaction_receipt(tx_hash)
|
||||||
|
print("✅ Rebalance Complete.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error during rebalance swap: {e}")
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("Liquidity 0. Marking closed.")
|
||||||
|
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:
|
||||||
|
print("\n[OPENING] No active automatic position. Starting Open Sequence...")
|
||||||
|
# Get Pool (WETH/USDC)
|
||||||
|
token0 = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1" # WETH
|
||||||
|
token1 = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831" # USDC
|
||||||
|
pool_addr = factory_contract.functions.getPool(token0, token1, 500).call()
|
||||||
|
pool_c = w3.eth.contract(address=pool_addr, abi=UNISWAP_V3_POOL_ABI)
|
||||||
|
|
||||||
|
pool_data = get_pool_dynamic_data(pool_c)
|
||||||
|
tick = pool_data['tick']
|
||||||
|
|
||||||
|
# Range +/- 2%
|
||||||
|
import math
|
||||||
|
tick_delta = int(math.log(1 + RANGE_WIDTH_PCT) / math.log(1.0001))
|
||||||
|
spacing = 10
|
||||||
|
lower = (tick - tick_delta) // spacing * spacing
|
||||||
|
upper = (tick + tick_delta) // spacing * spacing
|
||||||
|
|
||||||
|
# Amounts
|
||||||
|
try:
|
||||||
|
token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
|
||||||
|
token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
|
||||||
|
d0 = token0_c.functions.decimals().call()
|
||||||
|
d1 = token1_c.functions.decimals().call()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error fetching decimals: {e}")
|
||||||
|
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||||
|
continue
|
||||||
|
|
||||||
|
amt0, amt1 = calculate_mint_amounts(tick, lower, upper, TARGET_INVESTMENT_VALUE_TOKEN1, d0, d1, pool_data['sqrtPriceX96'])
|
||||||
|
amt0_buf, amt1_buf = int(amt0 * 1.02), int(amt1 * 1.02)
|
||||||
|
|
||||||
|
if check_and_swap(w3, router_contract, account, token0, token1, amt0_buf, amt1_buf):
|
||||||
|
mint_result = mint_new_position(w3, npm_contract, account, token0, token1, amt0, amt1, lower, upper)
|
||||||
|
|
||||||
|
if mint_result: # Calculate Actual Value
|
||||||
|
try:
|
||||||
|
s0 = token0_c.functions.symbol().call()
|
||||||
|
s1 = token1_c.functions.symbol().call()
|
||||||
|
except:
|
||||||
|
s0, s1 = "T0", "T1"
|
||||||
|
|
||||||
|
real_amt0 = from_wei(mint_result['amount0'], d0)
|
||||||
|
real_amt1 = from_wei(mint_result['amount1'], d1)
|
||||||
|
entry_price = price_from_sqrt_price_x96(pool_data['sqrtPriceX96'], d0, d1)
|
||||||
|
actual_value = (real_amt0 * entry_price) + real_amt1
|
||||||
|
print(f"ACTUAL MINT VALUE: {actual_value:.2f} {s1}/{s0}")
|
||||||
|
|
||||||
|
pos_data = {
|
||||||
|
'token_id': mint_result['token_id'],
|
||||||
|
'entry_price': entry_price,
|
||||||
|
'range_lower': price_from_tick(lower, d0, d1),
|
||||||
|
'range_upper': price_from_tick(upper, d0, d1),
|
||||||
|
'target_value': actual_value,
|
||||||
|
'amount0_initial': mint_result['amount0'],
|
||||||
|
'amount1_initial': mint_result['amount1']
|
||||||
|
}
|
||||||
|
update_hedge_status_file("OPEN", pos_data)
|
||||||
|
print("Cycle Complete. Monitoring.")
|
||||||
|
|
||||||
|
elif not all_positions:
|
||||||
|
print("No open positions (Manual or Automatic). Waiting...")
|
||||||
|
|
||||||
|
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nManager stopped.")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in Main Loop: {e}")
|
||||||
|
time.sleep(MONITOR_INTERVAL_SECONDS)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
53
clp_hedger_auto/AGENTS.md
Normal file
53
clp_hedger_auto/AGENTS.md
Normal file
@ -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
|
||||||
469
clp_hedger_auto/clp_hedger.py
Normal file
469
clp_hedger_auto/clp_hedger.py
Normal file
@ -0,0 +1,469 @@
|
|||||||
|
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.002 # 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("SWING_AGENT_PK")
|
||||||
|
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(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(self.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} {self.coin_symbol} -> Action: {'BUY' if is_buy else 'SELL'} {final_size}")
|
||||||
|
|
||||||
|
# 4. Execute
|
||||||
|
self.execute_trade(self.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(self.coin_symbol)
|
||||||
|
if price is None:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
|
||||||
|
funding_rate = self.get_funding_rate(self.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.
|
||||||
|
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(self.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()
|
||||||
18
clp_hedger_auto/hedge_status.json
Normal file
18
clp_hedger_auto/hedge_status.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
]
|
||||||
85
clp_hedger_auto/working_configuration.md
Normal file
85
clp_hedger_auto/working_configuration.md
Normal file
@ -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
|
||||||
@ -33,7 +33,7 @@ def create_and_authorize_agent():
|
|||||||
|
|
||||||
# --- STEP 3: Create and approve the agent with a specific name ---
|
# --- STEP 3: Create and approve the agent with a specific name ---
|
||||||
# agent name must be between 1 and 16 characters long
|
# agent name must be between 1 and 16 characters long
|
||||||
agent_name = "executor_swing"
|
agent_name = "executor_SCALPER"
|
||||||
|
|
||||||
print(f"\n🔗 Authorizing a new agent named '{agent_name}'...")
|
print(f"\n🔗 Authorizing a new agent named '{agent_name}'...")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -30,8 +30,11 @@ class DashboardDataFetcher:
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
self.info = Info(constants.MAINNET_API_URL, skip_ws=True)
|
self.info = Info(constants.MAINNET_API_URL, skip_ws=True)
|
||||||
self.status_file_path = os.path.join("_logs", "trade_executor_status.json")
|
|
||||||
self.managed_positions_path = os.path.join("_data", "executor_managed_positions.json")
|
# Use absolute path to ensure consistency across different working directories
|
||||||
|
project_root = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
self.status_file_path = os.path.join(project_root, "_logs", "trade_executor_status.json")
|
||||||
|
self.managed_positions_path = os.path.join(project_root, "_data", "executor_managed_positions.json")
|
||||||
logging.info(f"Dashboard Data Fetcher initialized for vault: {self.vault_address}")
|
logging.info(f"Dashboard Data Fetcher initialized for vault: {self.vault_address}")
|
||||||
|
|
||||||
def load_managed_positions(self) -> dict:
|
def load_managed_positions(self) -> dict:
|
||||||
@ -47,7 +50,7 @@ class DashboardDataFetcher:
|
|||||||
return {}
|
return {}
|
||||||
|
|
||||||
def fetch_and_save_status(self):
|
def fetch_and_save_status(self):
|
||||||
"""Fetches all account data and saves it to the JSON status file."""
|
"""Fetches all account data and saves it to JSON status file."""
|
||||||
try:
|
try:
|
||||||
perpetuals_state = self.info.user_state(self.vault_address)
|
perpetuals_state = self.info.user_state(self.vault_address)
|
||||||
spot_state = self.info.spot_user_state(self.vault_address)
|
spot_state = self.info.spot_user_state(self.vault_address)
|
||||||
@ -105,7 +108,11 @@ class DashboardDataFetcher:
|
|||||||
"position_value": total_balance * mark_price, "pnl": "N/A"
|
"position_value": total_balance * mark_price, "pnl": "N/A"
|
||||||
})
|
})
|
||||||
|
|
||||||
# 3. Write to file
|
# 3. Ensure directory exists and write to file
|
||||||
|
# Ensure the _logs directory exists
|
||||||
|
logs_dir = os.path.dirname(self.status_file_path)
|
||||||
|
os.makedirs(logs_dir, exist_ok=True)
|
||||||
|
|
||||||
# Use atomic write to prevent partial reads from main_app
|
# Use atomic write to prevent partial reads from main_app
|
||||||
temp_file_path = self.status_file_path + ".tmp"
|
temp_file_path = self.status_file_path + ".tmp"
|
||||||
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
with open(temp_file_path, 'w', encoding='utf-8') as f:
|
||||||
|
|||||||
BIN
requirements.txt
BIN
requirements.txt
Binary file not shown.
Reference in New Issue
Block a user