11 Commits

Author SHA1 Message Date
84242f3654 CLP auto hedge 2025-12-12 23:49:50 +01:00
aeaae84750 remove market_data.db-shm from tracking 2025-11-11 10:56:47 +01:00
89b8e53092 Create AGENTS.md file for tracking agent usage and improvements
- Added comprehensive agent documentation and usage tracking
- Created session history table with dates and agent usage
- Documented sessionsummary agent configuration and features
- Included agent improvement ideas and maintenance guidelines
- Established framework for tracking agent effectiveness over time
- Provides centralized location for agent-related information
2025-11-11 10:25:05 +01:00
eaceeb7e3b Add final session summary for DashboardDataFetcher fix
- Documented debugging session that resolved critical path resolution error
- Added comprehensive session summary covering problem identification, solution, and testing
- Recorded decisions made and files modified during the fix
- Included next steps for ongoing monitoring and improvement
- Maintained structured format consistent with sessionsummary agent specifications
2025-11-11 10:23:19 +01:00
25e9a22a8e Fix DashboardDataFetcher path resolution error
- Use absolute path for status file to ensure consistency across subprocess execution
- Add os.makedirs() call to ensure _logs directory exists
- Prevents 'No such file or directory' error when running as subprocess
- Fixes issue: [Errno 2] No such file or directory: '_logs/trade_executor_status.json.tmp'
2025-11-11 00:38:07 +01:00
8494583779 Organize project files and add session summaries
- Create .temp folder for examples and temporary files
- Update .gitignore to include .temp/ directory
- Move model_comparison_examples.md to .temp folder
- Add session summaries to GEMINI.md for current development work
- Remove obsolete documentation files (IMPROVEMENT_ROADMAP.md, PROJECT_REVIEW_AND_PROPOSALS.md, README.md)
- Maintain clean project structure with proper file organization
2025-11-11 00:32:35 +01:00
aea341792e ci: ignore .opencode directory 2025-11-10 22:41:50 +01:00
e8d7db2743 Merge remote-tracking branch 'origin/web_socket' 2025-11-10 09:23:44 +01:00
bf88c16383 "Stop tracking sdk files" 2025-11-10 08:54:56 +01:00
3000a366be gitignore 2025-11-10 08:54:02 +01:00
f8afdc1ab1 Delete _data/strategy_status_sma_cross_2.json 2025-11-09 21:50:34 +00:00
27 changed files with 1928 additions and 663 deletions

6
.gitignore vendored
View File

@ -30,6 +30,9 @@ sdk/
# Ignore custom agents directory
agents/
# Ignore temporary files and examples
.temp/
# Ignore Jekyll files
.nojekyll
@ -38,4 +41,5 @@ agents/
.vscode/
.idea/
.DS_Store
Thumbs.db
Thumbs.db
.opencode/

View 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
View File

@ -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.
* **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
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.

View File

@ -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.

View File

@ -1 +0,0 @@
"# Comprehensive Project Review and Improvement Proposals"

View File

@ -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
View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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`.

View File

@ -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

View File

@ -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.

View File

@ -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.

View File

@ -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.

View File

@ -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
View 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.

86
clp_hedger/GEMINI.md Normal file
View 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
View File

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

View File

@ -0,0 +1,387 @@
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("normal", "SCALPER_HEDGER")
# --- CONFIGURATION ---
COIN_SYMBOL = "ETH"
CHECK_INTERVAL = 10 # Faster check for scalper
LEVERAGE = 5
STATUS_FILE = "hedge_status.json"
# Gap Recovery Configuration
PRICE_BUFFER_PCT = 0.002 # 0.25% buffer
TIME_BUFFER_SECONDS = 120 # 2 minutes wait
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 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)
# Formula: L = amount0 / (1/sqrtP - 1/sqrtPb)
if entry_amount0 > 0:
amount0_eth = entry_amount0 / 10**18
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 Method 1 failed or yielded 0
# Formula: L = amount1 / (sqrtP - sqrtPa)
# Note: Price in formula is Token1/Token0? No, sqrtPrice is sqrt(Token1/Token0).
# Yes. Amount1 = L * (sqrtP - sqrtPa)
if self.L == 0.0 and entry_amount1 > 0:
amount1_usdc = entry_amount1 / 10**6 # USDC is 6 decimals
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 using Target Value
# Max ETH = Value / LowerPrice.
# L = MaxETH / (1/sqrtPa - 1/sqrtPb)
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)
raw_target_short = pool_delta + self.static_long
entry_upper = self.entry_price * (1 + PRICE_BUFFER_PCT)
entry_lower = self.entry_price * (1 - PRICE_BUFFER_PCT)
desired_mode = self.current_mode
if self.current_mode == "NORMAL":
if current_price > entry_upper and current_price < self.recovery_target:
desired_mode = "RECOVERY"
elif self.current_mode == "RECOVERY":
if current_price < entry_lower or current_price >= self.recovery_target:
desired_mode = "NORMAL"
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}")
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
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": self.current_mode
}
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
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_market_price(self, coin):
try:
mids = self.info.all_mids()
if coin in mids: return float(mids[coin])
except: pass
return None
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 float(pos["position"]["szi"])
return 0.0
except: 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}")
reduce_only = is_buy
try:
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")
else:
logging.error(f"Order Failed: {order_result}")
except Exception as e:
logging.error(f"Exception during trade: {e}")
def close_all_positions(self):
logging.info("Closing all positions (Safety/Closed State)...")
try:
price = self.get_market_price(COIN_SYMBOL)
current_pos = self.get_current_position(COIN_SYMBOL)
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
self.execute_trade(COIN_SYMBOL, is_buy, final_size, price)
self.active_position_id = None
except Exception as e:
logging.error(f"Error closing: {e}")
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
# Initialize Strategy if needed
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
# Double Check Strategy validity
if self.strategy is None:
continue
# 2. Market Data
price = self.get_market_price(COIN_SYMBOL)
if price is None:
time.sleep(5)
continue
funding_rate = self.get_funding_rate(COIN_SYMBOL)
current_pos_size = self.get_current_position(COIN_SYMBOL)
# 3. Calculate
calc = self.strategy.calculate_rebalance(price, current_pos_size)
diff_abs = abs(calc['diff'])
# 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))
min_threshold = 0.001
rebalance_threshold = max(min_threshold, max_potential_eth * 0.05)
# 5. Execute with Min Order Value check
if diff_abs > rebalance_threshold:
trade_size = round_to_sz_decimals(diff_abs, self.sz_decimals)
min_order_value_usd = 10.0
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})")
elif trade_size > 0:
logging.info(f"⚡ THRESHOLD TRIGGERED ({diff_abs:.4f} >= {rebalance_threshold:.4f})")
is_buy = (calc['action'] == "BUY")
self.execute_trade(COIN_SYMBOL, is_buy, trade_size, price)
else:
logging.info("Trade size rounds to 0. Skipping.")
else:
logging.info(f"Idle. Diff {diff_abs:.4f} < Threshold {rebalance_threshold:.4f}")
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()

View File

@ -0,0 +1,16 @@
[
{
"type": "AUTOMATIC",
"token_id": 5154921,
"status": "OPEN",
"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": null
}
]

View File

@ -0,0 +1,711 @@
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
price = price * (10**(token0_decimals - token1_decimals))
return price
def price_from_tick(tick, token0_decimals, token1_decimals):
price = 1.0001**tick
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):
return int((1.0001 ** (tick / 2)) * (2 ** 96))
def get_liquidity_for_amount0(sqrt_ratio_a, sqrt_ratio_b, amount0):
if sqrt_ratio_a > sqrt_ratio_b:
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
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):
if sqrt_ratio_a > sqrt_ratio_b:
sqrt_ratio_a, sqrt_ratio_b = sqrt_ratio_b, sqrt_ratio_a
return int(amount1 / (sqrt_ratio_b - sqrt_ratio_a))
def get_amounts_for_liquidity(sqrt_ratio_current, sqrt_ratio_a, sqrt_ratio_b, liquidity):
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
if sqrt_ratio_current <= sqrt_ratio_a:
amount0 = ((liquidity * Q96) // sqrt_ratio_a) - ((liquidity * Q96) // sqrt_ratio_b)
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
else:
amount1 = (liquidity * (sqrt_ratio_b - sqrt_ratio_a)) // Q96
return amount0, amount1
# --- Configuration ---
RPC_URL = os.environ.get("MAINNET_RPC_URL")
POSITION_TOKEN_ID = int(os.environ.get("POSITION_TOKEN_ID", "0"))
PRIVATE_KEY = os.environ.get("MAIN_WALLET_PRIVATE_KEY") or os.environ.get("PRIVATE_KEY")
MONITOR_INTERVAL_SECONDS = 30
COLLECT_FEES_ENABLED = False
CLOSE_POSITION_ENABLED = True
CLOSE_IF_OUT_OF_RANGE_ONLY = True
OPEN_POSITION_ENABLED = True
TARGET_INVESTMENT_VALUE_TOKEN1 = 100.0
RANGE_WIDTH_PCT = 0.005
STATUS_FILE = "hedge_status.json"
# --- JSON State Helpers ---
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:
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):
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":
new_entry = {
"type": "AUTOMATIC",
"token_id": position_data['token_id'],
"status": "OPEN",
"entry_price": position_data['entry_price'],
"range_lower": position_data['range_lower'],
"range_upper": position_data['range_upper'],
"target_value": position_data.get('target_value', 0.0),
"amount0_initial": position_data.get('amount0', 0),
"amount1_initial": position_data.get('amount1', 0),
"static_long": 0.0,
"timestamp_open": int(time.time()),
"timestamp_close": None
}
current_data.append(new_entry)
print(f"Recorded new AUTOMATIC position {position_data['token_id']} in {STATUS_FILE}")
elif action == "CLOSE":
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'] = "CLOSED"
entry['timestamp_close'] = int(time.time())
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('''
[
{"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 (e.g., WETH price in USDC)
price_of_token0_in_token1_units = price_from_sqrt_price_x96(sqrt_price_current_x96, decimals0, decimals1)
# 2. Estimate Amounts for a Test Liquidity (L_test)
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 test amounts for decimals to get "Real Units" (e.g., 0.1 WETH, 500 USDC)
real_amt0_test = amt0_test / (10**decimals0)
real_amt1_test = amt1_test / (10**decimals1)
# 4. Calculate Total Value of Test Position in Token1 terms (e.g., Total in USDC)
value_test = (real_amt0_test * price_of_token0_in_token1_units) + real_amt1_test
if value_test == 0:
return 0, 0
# 5. Scale to Target Investment Value
scale = investment_value_token1 / value_test
# 6. Calculate Final Amounts (raw integer units for contract call)
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()
deficit0 = max(0, amount0_needed - bal0)
deficit1 = max(0, amount1_needed - bal1)
# --- AUTO-WRAP ETH LOGIC ---
# Check if we need WETH and have Native ETH
# WETH Address Check (Case insensitive)
weth_addr_lower = WETH_ADDRESS.lower()
# Check Token0 (Deficit0)
if (deficit0 > 0 or deficit1 > 0) and token0.lower() == weth_addr_lower:
native_bal = w3_instance.eth.get_balance(account.address)
gas_reserve = 2 * 10**16 # 0.02 ETH gas reserve
available_native = max(0, native_bal - gas_reserve)
# Determine how much to wrap
# If we have deficit1 (need USDC), we likely need to wrap more WETH to swap it.
# Strategy: If deficit1 > 0, wrap ALL available native ETH (up to reasonable limit?).
# Or just wrap what we have.
amount_to_wrap = 0
if deficit0 > 0:
amount_to_wrap = deficit0
if deficit1 > 0:
# We need to buy Token1. We need surplus Token0.
# Wrap all remaining available native ETH to facilitate swap.
amount_to_wrap = available_native
# Safety clamp
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)
# Refresh Balance
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)}, Have: {from_wei(available_native, 18)}")
# Check Token1 (Deficit1) - Assuming Token1 could be WETH too
if deficit1 > 0 and token1.lower() == weth_addr_lower:
native_bal = w3_instance.eth.get_balance(account.address)
gas_reserve = 10**16
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)
# Refresh Balance
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)
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)
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_address, abi=ERC20_ABI)
token1_contract = w3_instance.eth.contract(address=token1_address, 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}
# Event Topics
transfer_topic = "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
increase_liquidity_topic = "3067048beee31b25b2f1681f88dac838c8bba36af25bfb2b7cf7473a5847e35f"
for log in receipt['logs']:
topic0 = log['topics'][0].hex().replace("0x", "")
# Parse Token ID from Transfer
if topic0 == transfer_topic and len(log['topics']) > 3:
result_data['token_id'] = int(log['topics'][3].hex(), 16)
# Parse Amounts from IncreaseLiquidity
# Event: IncreaseLiquidity(uint256 indexed tokenId, uint128 liquidity, uint256 amount0, uint256 amount1)
# Indexed args are in topics. Non-indexed in data.
# tokenId is indexed (Topic 1).
# Data: liquidity (32 bytes), amount0 (32 bytes), amount1 (32 bytes) = 96 bytes total
if topic0 == increase_liquidity_topic:
data_hex = log['data'].hex().replace("0x", "")
if len(data_hex) >= 192: # 3 * 64 chars
# Split data into 3 chunks of 64 chars (32 bytes)
liquidity_hex = data_hex[0:64]
amount0_hex = data_hex[64:128]
amount1_hex = data_hex[128:192]
result_data['liquidity'] = int(liquidity_hex, 16)
result_data['amount0'] = int(amount0_hex, 16)
result_data['amount1'] = int(amount1_hex, 16)
print(f"Captured Actual Mint Amounts: {result_data['amount0']} Token0 / {result_data['amount1']} Token1")
if result_data['token_id']:
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'), 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
# 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']}")
# --- 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:
if decrease_liquidity(w3, npm_contract, account, token_id, liq):
time.sleep(5)
collect_fees(w3, npm_contract, account, token_id)
update_hedge_status_file("CLOSE", {'token_id': token_id})
print("Position Closed & Status Updated.")
# We don't break loop here, let it finish monitoring others,
# but next main loop iteration will see it closed.
else:
print("Liquidity 0. Marking closed.")
update_hedge_status_file("CLOSE", {'token_id': token_id})
# 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
token0_c = w3.eth.contract(address=token0, abi=ERC20_ABI)
token1_c = w3.eth.contract(address=token1, abi=ERC20_ABI)
try:
d0 = token0_c.functions.decimals().call()
d1 = token1_c.functions.decimals().call()
except:
print("Error fetching decimals")
time.sleep(5)
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)
# 4. Swap & Mint
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)
# Value in Token1 terms (e.g. USDC)
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, # Save Actual Value as Target for hedging accuracy
'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()

View File

@ -33,7 +33,7 @@ def create_and_authorize_agent():
# --- STEP 3: Create and approve the agent with a specific name ---
# 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}'...")
try:

View File

@ -30,8 +30,11 @@ class DashboardDataFetcher:
sys.exit(1)
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}")
def load_managed_positions(self) -> dict:
@ -47,7 +50,7 @@ class DashboardDataFetcher:
return {}
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:
perpetuals_state = self.info.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"
})
# 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
temp_file_path = self.status_file_path + ".tmp"
with open(temp_file_path, 'w', encoding='utf-8') as f:

Binary file not shown.