17 Commits

Author SHA1 Message Date
Simon Moisy
65f30a4020 Enhance backtesting framework with static task processing and progress management. Introduced static task processing for parallel execution, improved error handling, and added a progress manager for better task tracking. Updated BacktestRunner to support progress callbacks and optimized worker allocation based on system resources. Added new configuration files for flexible backtesting setups. 2025-07-10 10:23:41 +08:00
Simon Moisy
be331ed631 Remove unused GSheetBatchPusher class and xgboost model file to streamline codebase and eliminate deprecated components. 2025-06-25 13:11:17 +08:00
Simon Moisy
6c5dcc1183 Implement backtesting framework with modular architecture for data loading, processing, and result management. Introduced BacktestRunner, ConfigManager, and ResultProcessor classes for improved maintainability and error handling. Updated main execution script to utilize new components and added comprehensive logging. Enhanced README with detailed project overview and usage instructions. 2025-06-25 13:08:07 +08:00
Simon Moisy
02e5db2a36 Added comprehensive rules for global development standards, architecture guidelines, code review processes, context management, PRD creation, documentation standards, enhanced task list management, task generation, iterative workflow, project-specific rules, refactoring practices, and task list management. These rules aim to improve code quality, maintainability, and integration of AI-assisted development. 2025-06-25 13:07:14 +08:00
Simon Moisy
a877f14e65 loo on features 2025-05-30 20:06:28 +08:00
Simon Moisy
082a2835b6 Implemented Supertrend indicators for feature engineering in main.py, including caching of computed features. Updated plotting functions in plot_results.py to save charts in a dedicated directory and added new functions for directional accuracy and prediction transition heatmaps. 2025-05-30 18:14:42 +08:00
Simon Moisy
ada6150413 Added multiple technical indicators for feature engineering, including ADX, TRIX, Vortex, KAMA, Force Index, EOM, MFI, ADI, TEMA, StochRSI, and Awesome Oscillator. Improved NaN handling and implemented leave-one-out feature evaluation with results saved to CSV. 2025-05-30 17:59:09 +08:00
Simon Moisy
ced64825bd reverted to sequential computing for features, added one distribution visualization graph 2025-05-30 15:54:48 +08:00
Simon Moisy
2f98463df8 more uv updates 2025-05-30 15:54:14 +08:00
Simon Moisy
2a52ffde9a cleanup and uv updates 2025-05-30 15:36:43 +08:00
Simon Moisy
a22914731f gitignore updated, model file 2025-05-30 12:31:20 +08:00
Simon Moisy
81e4b640a7 model updated 2025-05-30 12:29:37 +08:00
Simon Moisy
2dba88b620 Added mode indicators, still WIP 2025-05-29 12:45:45 +08:00
Simon Moisy
de67b27e37 XGBoost first iteration 2025-05-29 18:28:53 +08:00
Simon Moisy
1284549106 progress print 2025-05-29 11:04:03 +08:00
Simon Moisy
5f03524d6a never fallback to default values for fee_usd 2025-05-28 02:50:40 +08:00
Simon Moisy
74c8048ed5 shifted one day back on the metatrend to avoid lookahead bias, reverted metatrend calculus to use no cpu optimization for readability 2025-05-27 17:49:55 +08:00
149 changed files with 6760 additions and 34710 deletions

View File

@@ -1,8 +0,0 @@
---
description:
globs:
alwaysApply: true
---
- use UV for package management
- ./docs folder for the documetation and the modules description, update related files if logic changed

View File

@@ -0,0 +1,61 @@
---
description: Global development standards and AI interaction principles
globs:
alwaysApply: true
---
# Rule: Always Apply - Global Development Standards
## AI Interaction Principles
### Step-by-Step Development
- **NEVER** generate large blocks of code without explanation
- **ALWAYS** ask "provide your plan in a concise bullet list and wait for my confirmation before proceeding"
- Break complex tasks into smaller, manageable pieces (≤250 lines per file, ≤50 lines per function)
- Explain your reasoning step-by-step before writing code
- Wait for explicit approval before moving to the next sub-task
### Context Awareness
- **ALWAYS** reference existing code patterns and data structures before suggesting new approaches
- Ask about existing conventions before implementing new functionality
- Preserve established architectural decisions unless explicitly asked to change them
- Maintain consistency with existing naming conventions and code style
## Code Quality Standards
### File and Function Limits
- **Maximum file size**: 250 lines
- **Maximum function size**: 50 lines
- **Maximum complexity**: If a function does more than one main thing, break it down
- **Naming**: Use clear, descriptive names that explain purpose
### Documentation Requirements
- **Every public function** must have a docstring explaining purpose, parameters, and return value
- **Every class** must have a class-level docstring
- **Complex logic** must have inline comments explaining the "why", not just the "what"
- **API endpoints** must be documented with request/response examples
### Error Handling
- **ALWAYS** include proper error handling for external dependencies
- **NEVER** use bare except clauses
- Provide meaningful error messages that help with debugging
- Log errors appropriately for the application context
## Security and Best Practices
- **NEVER** hardcode credentials, API keys, or sensitive data
- **ALWAYS** validate user inputs
- Use parameterized queries for database operations
- Follow the principle of least privilege
- Implement proper authentication and authorization
## Testing Requirements
- **Every implementation** should have corresponding unit tests
- **Every API endpoint** should have integration tests
- Test files should be placed alongside the code they test
- Use descriptive test names that explain what is being tested
## Response Format
- Be concise and avoid unnecessary repetition
- Focus on actionable information
- Provide examples when explaining complex concepts
- Ask clarifying questions when requirements are ambiguous

View File

@@ -0,0 +1,237 @@
---
description: Modular design principles and architecture guidelines for scalable development
globs:
alwaysApply: false
---
# Rule: Architecture and Modular Design
## Goal
Maintain a clean, modular architecture that scales effectively and prevents the complexity issues that arise in AI-assisted development.
## Core Architecture Principles
### 1. Modular Design
- **Single Responsibility**: Each module has one clear purpose
- **Loose Coupling**: Modules depend on interfaces, not implementations
- **High Cohesion**: Related functionality is grouped together
- **Clear Boundaries**: Module interfaces are well-defined and stable
### 2. Size Constraints
- **Files**: Maximum 250 lines per file
- **Functions**: Maximum 50 lines per function
- **Classes**: Maximum 300 lines per class
- **Modules**: Maximum 10 public functions/classes per module
### 3. Dependency Management
- **Layer Dependencies**: Higher layers depend on lower layers only
- **No Circular Dependencies**: Modules cannot depend on each other cyclically
- **Interface Segregation**: Depend on specific interfaces, not broad ones
- **Dependency Injection**: Pass dependencies rather than creating them internally
## Modular Architecture Patterns
### Layer Structure
```
src/
├── presentation/ # UI, API endpoints, CLI interfaces
├── application/ # Business logic, use cases, workflows
├── domain/ # Core business entities and rules
├── infrastructure/ # Database, external APIs, file systems
└── shared/ # Common utilities, constants, types
```
### Module Organization
```
module_name/
├── __init__.py # Public interface exports
├── core.py # Main module logic
├── types.py # Type definitions and interfaces
├── utils.py # Module-specific utilities
├── tests/ # Module tests
└── README.md # Module documentation
```
## Design Patterns for AI Development
### 1. Repository Pattern
Separate data access from business logic:
```python
# Domain interface
class UserRepository:
def get_by_id(self, user_id: str) -> User: ...
def save(self, user: User) -> None: ...
# Infrastructure implementation
class SqlUserRepository(UserRepository):
def get_by_id(self, user_id: str) -> User:
# Database-specific implementation
pass
```
### 2. Service Pattern
Encapsulate business logic in focused services:
```python
class UserService:
def __init__(self, user_repo: UserRepository):
self._user_repo = user_repo
def create_user(self, data: UserData) -> User:
# Validation and business logic
# Single responsibility: user creation
pass
```
### 3. Factory Pattern
Create complex objects with clear interfaces:
```python
class DatabaseFactory:
@staticmethod
def create_connection(config: DatabaseConfig) -> Connection:
# Handle different database types
# Encapsulate connection complexity
pass
```
## Architecture Decision Guidelines
### When to Create New Modules
Create a new module when:
- **Functionality** exceeds size constraints (250 lines)
- **Responsibility** is distinct from existing modules
- **Dependencies** would create circular references
- **Reusability** would benefit other parts of the system
- **Testing** requires isolated test environments
### When to Split Existing Modules
Split modules when:
- **File size** exceeds 250 lines
- **Multiple responsibilities** are evident
- **Testing** becomes difficult due to complexity
- **Dependencies** become too numerous
- **Change frequency** differs significantly between parts
### Module Interface Design
```python
# Good: Clear, focused interface
class PaymentProcessor:
def process_payment(self, amount: Money, method: PaymentMethod) -> PaymentResult:
"""Process a single payment transaction."""
pass
# Bad: Unfocused, kitchen-sink interface
class PaymentManager:
def process_payment(self, ...): pass
def validate_card(self, ...): pass
def send_receipt(self, ...): pass
def update_inventory(self, ...): pass # Wrong responsibility!
```
## Architecture Validation
### Architecture Review Checklist
- [ ] **Dependencies flow in one direction** (no cycles)
- [ ] **Layers are respected** (presentation doesn't call infrastructure directly)
- [ ] **Modules have single responsibility**
- [ ] **Interfaces are stable** and well-defined
- [ ] **Size constraints** are maintained
- [ ] **Testing** is straightforward for each module
### Red Flags
- **God Objects**: Classes/modules that do too many things
- **Circular Dependencies**: Modules that depend on each other
- **Deep Inheritance**: More than 3 levels of inheritance
- **Large Interfaces**: Interfaces with more than 7 methods
- **Tight Coupling**: Modules that know too much about each other's internals
## Refactoring Guidelines
### When to Refactor
- Module exceeds size constraints
- Code duplication across modules
- Difficult to test individual components
- New features require changing multiple unrelated modules
- Performance bottlenecks due to poor separation
### Refactoring Process
1. **Identify** the specific architectural problem
2. **Design** the target architecture
3. **Create tests** to verify current behavior
4. **Implement changes** incrementally
5. **Validate** that tests still pass
6. **Update documentation** to reflect changes
### Safe Refactoring Practices
- **One change at a time**: Don't mix refactoring with new features
- **Tests first**: Ensure comprehensive test coverage before refactoring
- **Incremental changes**: Small steps with verification at each stage
- **Backward compatibility**: Maintain existing interfaces during transition
- **Documentation updates**: Keep architecture documentation current
## Architecture Documentation
### Architecture Decision Records (ADRs)
Document significant decisions in `./docs/decisions/`:
```markdown
# ADR-003: Service Layer Architecture
## Status
Accepted
## Context
As the application grows, business logic is scattered across controllers and models.
## Decision
Implement a service layer to encapsulate business logic.
## Consequences
**Positive:**
- Clear separation of concerns
- Easier testing of business logic
- Better reusability across different interfaces
**Negative:**
- Additional abstraction layer
- More files to maintain
```
### Module Documentation Template
```markdown
# Module: [Name]
## Purpose
What this module does and why it exists.
## Dependencies
- **Imports from**: List of modules this depends on
- **Used by**: List of modules that depend on this one
- **External**: Third-party dependencies
## Public Interface
```python
# Key functions and classes exposed by this module
```
## Architecture Notes
- Design patterns used
- Important architectural decisions
- Known limitations or constraints
```
## Migration Strategies
### Legacy Code Integration
- **Strangler Fig Pattern**: Gradually replace old code with new modules
- **Adapter Pattern**: Create interfaces to integrate old and new code
- **Facade Pattern**: Simplify complex legacy interfaces
### Gradual Modernization
1. **Identify boundaries** in existing code
2. **Extract modules** one at a time
3. **Create interfaces** for each extracted module
4. **Test thoroughly** at each step
5. **Update documentation** continuously

View File

@@ -0,0 +1,123 @@
---
description: AI-generated code review checklist and quality assurance guidelines
globs:
alwaysApply: false
---
# Rule: Code Review and Quality Assurance
## Goal
Establish systematic review processes for AI-generated code to maintain quality, security, and maintainability standards.
## AI Code Review Checklist
### Pre-Implementation Review
Before accepting any AI-generated code:
1. **Understand the Code**
- [ ] Can you explain what the code does in your own words?
- [ ] Do you understand each function and its purpose?
- [ ] Are there any "magic" values or unexplained logic?
- [ ] Does the code solve the actual problem stated?
2. **Architecture Alignment**
- [ ] Does the code follow established project patterns?
- [ ] Is it consistent with existing data structures?
- [ ] Does it integrate cleanly with existing components?
- [ ] Are new dependencies justified and necessary?
3. **Code Quality**
- [ ] Are functions smaller than 50 lines?
- [ ] Are files smaller than 250 lines?
- [ ] Are variable and function names descriptive?
- [ ] Is the code DRY (Don't Repeat Yourself)?
### Security Review
- [ ] **Input Validation**: All user inputs are validated and sanitized
- [ ] **Authentication**: Proper authentication checks are in place
- [ ] **Authorization**: Access controls are implemented correctly
- [ ] **Data Protection**: Sensitive data is handled securely
- [ ] **SQL Injection**: Database queries use parameterized statements
- [ ] **XSS Prevention**: Output is properly escaped
- [ ] **Error Handling**: Errors don't leak sensitive information
### Integration Review
- [ ] **Existing Functionality**: New code doesn't break existing features
- [ ] **Data Consistency**: Database changes maintain referential integrity
- [ ] **API Compatibility**: Changes don't break existing API contracts
- [ ] **Performance Impact**: New code doesn't introduce performance bottlenecks
- [ ] **Testing Coverage**: Appropriate tests are included
## Review Process
### Step 1: Initial Code Analysis
1. **Read through the entire generated code** before running it
2. **Identify patterns** that don't match existing codebase
3. **Check dependencies** - are new packages really needed?
4. **Verify logic flow** - does the algorithm make sense?
### Step 2: Security and Error Handling Review
1. **Trace data flow** from input to output
2. **Identify potential failure points** and verify error handling
3. **Check for security vulnerabilities** using the security checklist
4. **Verify proper logging** and monitoring implementation
### Step 3: Integration Testing
1. **Test with existing code** to ensure compatibility
2. **Run existing test suite** to verify no regressions
3. **Test edge cases** and error conditions
4. **Verify performance** under realistic conditions
## Common AI Code Issues to Watch For
### Overcomplication Patterns
- **Unnecessary abstractions**: AI creating complex patterns for simple tasks
- **Over-engineering**: Solutions that are more complex than needed
- **Redundant code**: AI recreating existing functionality
- **Inappropriate design patterns**: Using patterns that don't fit the use case
### Context Loss Indicators
- **Inconsistent naming**: Different conventions from existing code
- **Wrong data structures**: Using different patterns than established
- **Ignored existing functions**: Reimplementing existing functionality
- **Architectural misalignment**: Code that doesn't fit the overall design
### Technical Debt Indicators
- **Magic numbers**: Hardcoded values without explanation
- **Poor error messages**: Generic or unhelpful error handling
- **Missing documentation**: Code without adequate comments
- **Tight coupling**: Components that are too interdependent
## Quality Gates
### Mandatory Reviews
All AI-generated code must pass these gates before acceptance:
1. **Security Review**: No security vulnerabilities detected
2. **Integration Review**: Integrates cleanly with existing code
3. **Performance Review**: Meets performance requirements
4. **Maintainability Review**: Code can be easily modified by team members
5. **Documentation Review**: Adequate documentation is provided
### Acceptance Criteria
- [ ] Code is understandable by any team member
- [ ] Integration requires minimal changes to existing code
- [ ] Security review passes all checks
- [ ] Performance meets established benchmarks
- [ ] Documentation is complete and accurate
## Rejection Criteria
Reject AI-generated code if:
- Security vulnerabilities are present
- Code is too complex for the problem being solved
- Integration requires major refactoring of existing code
- Code duplicates existing functionality without justification
- Documentation is missing or inadequate
## Review Documentation
For each review, document:
- Issues found and how they were resolved
- Performance impact assessment
- Security concerns and mitigations
- Integration challenges and solutions
- Recommendations for future similar tasks

View File

@@ -0,0 +1,93 @@
---
description: Context management for maintaining codebase awareness and preventing context drift
globs:
alwaysApply: false
---
# Rule: Context Management
## Goal
Maintain comprehensive project context to prevent context drift and ensure AI-generated code integrates seamlessly with existing codebase patterns and architecture.
## Context Documentation Requirements
### PRD.md file documentation
1. **Project Overview**
- Business objectives and goals
- Target users and use cases
- Key success metrics
### CONTEXT.md File Structure
Every project must maintain a `CONTEXT.md` file in the root directory with:
1. **Architecture Overview**
- High-level system architecture
- Key design patterns used
- Database schema overview
- API structure and conventions
2. **Technology Stack**
- Programming languages and versions
- Frameworks and libraries
- Database systems
- Development and deployment tools
3. **Coding Conventions**
- Naming conventions
- File organization patterns
- Code structure preferences
- Import/export patterns
4. **Current Implementation Status**
- Completed features
- Work in progress
- Known technical debt
- Planned improvements
## Context Maintenance Protocol
### Before Every Coding Session
1. **Review CONTEXT.md and PRD.md** to understand current project state
2. **Scan recent changes** in git history to understand latest patterns
3. **Identify existing patterns** for similar functionality before implementing new features
4. **Ask for clarification** if existing patterns are unclear or conflicting
### During Development
1. **Reference existing code** when explaining implementation approaches
2. **Maintain consistency** with established patterns and conventions
3. **Update CONTEXT.md** when making architectural decisions
4. **Document deviations** from established patterns with reasoning
### Context Preservation Strategies
- **Incremental development**: Build on existing patterns rather than creating new ones
- **Pattern consistency**: Use established data structures and function signatures
- **Integration awareness**: Consider how new code affects existing functionality
- **Dependency management**: Understand existing dependencies before adding new ones
## Context Prompting Best Practices
### Effective Context Sharing
- Include relevant sections of CONTEXT.md in prompts for complex tasks
- Reference specific existing files when asking for similar functionality
- Provide examples of existing patterns when requesting new implementations
- Share recent git commit messages to understand latest changes
### Context Window Optimization
- Prioritize most relevant context for current task
- Use @filename references to include specific files
- Break large contexts into focused, task-specific chunks
- Update context references as project evolves
## Red Flags - Context Loss Indicators
- AI suggests patterns that conflict with existing code
- New implementations ignore established conventions
- Proposed solutions don't integrate with existing architecture
- Code suggestions require significant refactoring of existing functionality
## Recovery Protocol
When context loss is detected:
1. **Stop development** and review CONTEXT.md
2. **Analyze existing codebase** for established patterns
3. **Update context documentation** with missing information
4. **Restart task** with proper context provided
5. **Test integration** with existing code before proceeding

View File

@@ -1,10 +1,10 @@
---
description:
description: Creating PRD for a project or specific task/function
globs:
alwaysApply: false
---
---
description:
description: Creating PRD for a project or specific task/function
globs:
alwaysApply: false
---

View File

@@ -0,0 +1,244 @@
---
description: Documentation standards for code, architecture, and development decisions
globs:
alwaysApply: false
---
# Rule: Documentation Standards
## Goal
Maintain comprehensive, up-to-date documentation that supports development, onboarding, and long-term maintenance of the codebase.
## Documentation Hierarchy
### 1. Project Level Documentation (in ./docs/)
- **README.md**: Project overview, setup instructions, basic usage
- **CONTEXT.md**: Current project state, architecture decisions, patterns
- **CHANGELOG.md**: Version history and significant changes
- **CONTRIBUTING.md**: Development guidelines and processes
- **API.md**: API endpoints, request/response formats, authentication
### 2. Module Level Documentation (in ./docs/modules/)
- **[module-name].md**: Purpose, public interfaces, usage examples
- **dependencies.md**: External dependencies and their purposes
- **architecture.md**: Module relationships and data flow
### 3. Code Level Documentation
- **Docstrings**: Function and class documentation
- **Inline comments**: Complex logic explanations
- **Type hints**: Clear parameter and return types
- **README files**: Directory-specific instructions
## Documentation Standards
### Code Documentation
```python
def process_user_data(user_id: str, data: dict) -> UserResult:
"""
Process and validate user data before storage.
Args:
user_id: Unique identifier for the user
data: Dictionary containing user information to process
Returns:
UserResult: Processed user data with validation status
Raises:
ValidationError: When user data fails validation
DatabaseError: When storage operation fails
Example:
>>> result = process_user_data("123", {"name": "John", "email": "john@example.com"})
>>> print(result.status)
'valid'
"""
```
### API Documentation Format
```markdown
### POST /api/users
Create a new user account.
**Request:**
```json
{
"name": "string (required)",
"email": "string (required, valid email)",
"age": "number (optional, min: 13)"
}
```
**Response (201):**
```json
{
"id": "uuid",
"name": "string",
"email": "string",
"created_at": "iso_datetime"
}
```
**Errors:**
- 400: Invalid input data
- 409: Email already exists
```
### Architecture Decision Records (ADRs)
Document significant architecture decisions in `./docs/decisions/`:
```markdown
# ADR-001: Database Choice - PostgreSQL
## Status
Accepted
## Context
We need to choose a database for storing user data and application state.
## Decision
We will use PostgreSQL as our primary database.
## Consequences
**Positive:**
- ACID compliance ensures data integrity
- Rich query capabilities with SQL
- Good performance for our expected load
**Negative:**
- More complex setup than simpler alternatives
- Requires SQL knowledge from team members
## Alternatives Considered
- MongoDB: Rejected due to consistency requirements
- SQLite: Rejected due to scalability needs
```
## Documentation Maintenance
### When to Update Documentation
#### Always Update:
- **API changes**: Any modification to public interfaces
- **Architecture changes**: New patterns, data structures, or workflows
- **Configuration changes**: Environment variables, deployment settings
- **Dependencies**: Adding, removing, or upgrading packages
- **Business logic changes**: Core functionality modifications
#### Update Weekly:
- **CONTEXT.md**: Current development status and priorities
- **Known issues**: Bug reports and workarounds
- **Performance notes**: Bottlenecks and optimization opportunities
#### Update per Release:
- **CHANGELOG.md**: User-facing changes and improvements
- **Version documentation**: Breaking changes and migration guides
- **Examples and tutorials**: Keep sample code current
### Documentation Quality Checklist
#### Completeness
- [ ] Purpose and scope clearly explained
- [ ] All public interfaces documented
- [ ] Examples provided for complex usage
- [ ] Error conditions and handling described
- [ ] Dependencies and requirements listed
#### Accuracy
- [ ] Code examples are tested and working
- [ ] Links point to correct locations
- [ ] Version numbers are current
- [ ] Screenshots reflect current UI
#### Clarity
- [ ] Written for the intended audience
- [ ] Technical jargon is explained
- [ ] Step-by-step instructions are clear
- [ ] Visual aids used where helpful
## Documentation Automation
### Auto-Generated Documentation
- **API docs**: Generate from code annotations
- **Type documentation**: Extract from type hints
- **Module dependencies**: Auto-update from imports
- **Test coverage**: Include coverage reports
### Documentation Testing
```python
# Test that code examples in documentation work
def test_documentation_examples():
"""Verify code examples in docs actually work."""
# Test examples from README.md
# Test API examples from docs/API.md
# Test configuration examples
```
## Documentation Templates
### New Module Documentation Template
```markdown
# Module: [Name]
## Purpose
Brief description of what this module does and why it exists.
## Public Interface
### Functions
- `function_name(params)`: Description and example
### Classes
- `ClassName`: Purpose and basic usage
## Usage Examples
```python
# Basic usage example
```
## Dependencies
- Internal: List of internal modules this depends on
- External: List of external packages required
## Testing
How to run tests for this module.
## Known Issues
Current limitations or bugs.
```
### API Endpoint Template
```markdown
### [METHOD] /api/endpoint
Brief description of what this endpoint does.
**Authentication:** Required/Optional
**Rate Limiting:** X requests per minute
**Request:**
- Headers required
- Body schema
- Query parameters
**Response:**
- Success response format
- Error response format
- Status codes
**Example:**
Working request/response example
```
## Review and Maintenance Process
### Documentation Review
- Include documentation updates in code reviews
- Verify examples still work with code changes
- Check for broken links and outdated information
- Ensure consistency with current implementation
### Regular Audits
- Monthly review of documentation accuracy
- Quarterly assessment of documentation completeness
- Annual review of documentation structure and organization

View File

@@ -0,0 +1,207 @@
---
description: Enhanced task list management with quality gates and iterative workflow integration
globs:
alwaysApply: false
---
# Rule: Enhanced Task List Management
## Goal
Manage task lists with integrated quality gates and iterative workflow to prevent context loss and ensure sustainable development.
## Task Implementation Protocol
### Pre-Implementation Check
Before starting any sub-task:
- [ ] **Context Review**: Have you reviewed CONTEXT.md and relevant documentation?
- [ ] **Pattern Identification**: Do you understand existing patterns to follow?
- [ ] **Integration Planning**: Do you know how this will integrate with existing code?
- [ ] **Size Validation**: Is this task small enough (≤50 lines, ≤250 lines per file)?
### Implementation Process
1. **One sub-task at a time**: Do **NOT** start the next subtask until you ask the user for permission and they say "yes" or "y"
2. **Step-by-step execution**:
- Plan the approach in bullet points
- Wait for approval
- Implement the specific sub-task
- Test the implementation
- Update documentation if needed
3. **Quality validation**: Run through the code review checklist before marking complete
### Completion Protocol
When you finish a **subtask**:
1. **Immediate marking**: Change `[ ]` to `[x]`
2. **Quality check**: Verify the implementation meets quality standards
3. **Integration test**: Ensure new code works with existing functionality
4. **Documentation update**: Update relevant files if needed
5. **Parent task check**: If **all** subtasks underneath a parent task are now `[x]`, also mark the **parent task** as completed
6. **Stop and wait**: Get user approval before proceeding to next sub-task
## Enhanced Task List Structure
### Task File Header
```markdown
# Task List: [Feature Name]
**Source PRD**: `prd-[feature-name].md`
**Status**: In Progress / Complete / Blocked
**Context Last Updated**: [Date]
**Architecture Review**: Required / Complete / N/A
## Quick Links
- [Context Documentation](./CONTEXT.md)
- [Architecture Guidelines](./docs/architecture.md)
- [Related Files](#relevant-files)
```
### Task Format with Quality Gates
```markdown
- [ ] 1.0 Parent Task Title
- **Quality Gate**: Architecture review required
- **Dependencies**: List any dependencies
- [ ] 1.1 [Sub-task description 1.1]
- **Size estimate**: [Small/Medium/Large]
- **Pattern reference**: [Reference to existing pattern]
- **Test requirements**: [Unit/Integration/Both]
- [ ] 1.2 [Sub-task description 1.2]
- **Integration points**: [List affected components]
- **Risk level**: [Low/Medium/High]
```
## Relevant Files Management
### Enhanced File Tracking
```markdown
## Relevant Files
### Implementation Files
- `path/to/file1.ts` - Brief description of purpose and role
- **Status**: Created / Modified / Needs Review
- **Last Modified**: [Date]
- **Review Status**: Pending / Approved / Needs Changes
### Test Files
- `path/to/file1.test.ts` - Unit tests for file1.ts
- **Coverage**: [Percentage or status]
- **Last Run**: [Date and result]
### Documentation Files
- `docs/module-name.md` - Module documentation
- **Status**: Up to date / Needs update / Missing
- **Last Updated**: [Date]
### Configuration Files
- `config/setting.json` - Configuration changes
- **Environment**: [Dev/Staging/Prod affected]
- **Backup**: [Location of backup]
```
## Task List Maintenance
### During Development
1. **Regular updates**: Update task status after each significant change
2. **File tracking**: Add new files as they are created or modified
3. **Dependency tracking**: Note when new dependencies between tasks emerge
4. **Risk assessment**: Flag tasks that become more complex than anticipated
### Quality Checkpoints
At 25%, 50%, 75%, and 100% completion:
- [ ] **Architecture alignment**: Code follows established patterns
- [ ] **Performance impact**: No significant performance degradation
- [ ] **Security review**: No security vulnerabilities introduced
- [ ] **Documentation current**: All changes are documented
### Weekly Review Process
1. **Completion assessment**: What percentage of tasks are actually complete?
2. **Quality assessment**: Are completed tasks meeting quality standards?
3. **Process assessment**: Is the iterative workflow being followed?
4. **Risk assessment**: Are there emerging risks or blockers?
## Task Status Indicators
### Status Levels
- `[ ]` **Not Started**: Task not yet begun
- `[~]` **In Progress**: Currently being worked on
- `[?]` **Blocked**: Waiting for dependencies or decisions
- `[!]` **Needs Review**: Implementation complete but needs quality review
- `[x]` **Complete**: Finished and quality approved
### Quality Indicators
- ✅ **Quality Approved**: Passed all quality gates
- ⚠️ **Quality Concerns**: Has issues but functional
- ❌ **Quality Failed**: Needs rework before approval
- 🔄 **Under Review**: Currently being reviewed
### Integration Status
- 🔗 **Integrated**: Successfully integrated with existing code
- 🔧 **Integration Issues**: Problems with existing code integration
- ⏳ **Integration Pending**: Ready for integration testing
## Emergency Procedures
### When Tasks Become Too Complex
If a sub-task grows beyond expected scope:
1. **Stop implementation** immediately
2. **Document current state** and what was discovered
3. **Break down** the task into smaller pieces
4. **Update task list** with new sub-tasks
5. **Get approval** for the new breakdown before proceeding
### When Context is Lost
If AI seems to lose track of project patterns:
1. **Pause development**
2. **Review CONTEXT.md** and recent changes
3. **Update context documentation** with current state
4. **Restart** with explicit pattern references
5. **Reduce task size** until context is re-established
### When Quality Gates Fail
If implementation doesn't meet quality standards:
1. **Mark task** with `[!]` status
2. **Document specific issues** found
3. **Create remediation tasks** if needed
4. **Don't proceed** until quality issues are resolved
## AI Instructions Integration
### Context Awareness Commands
```markdown
**Before starting any task, run these checks:**
1. @CONTEXT.md - Review current project state
2. @architecture.md - Understand design principles
3. @code-review.md - Know quality standards
4. Look at existing similar code for patterns
```
### Quality Validation Commands
```markdown
**After completing any sub-task:**
1. Run code review checklist
2. Test integration with existing code
3. Update documentation if needed
4. Mark task complete only after quality approval
```
### Workflow Commands
```markdown
**For each development session:**
1. Review incomplete tasks and their status
2. Identify next logical sub-task to work on
3. Check dependencies and blockers
4. Follow iterative workflow process
5. Update task list with progress and findings
```
## Success Metrics
### Daily Success Indicators
- Tasks are completed according to quality standards
- No sub-tasks are started without completing previous ones
- File tracking remains accurate and current
- Integration issues are caught early
### Weekly Success Indicators
- Overall task completion rate is sustainable
- Quality issues are decreasing over time
- Context loss incidents are rare
- Team confidence in codebase remains high

View File

@@ -1,5 +1,5 @@
---
description:
description: Generate a task list or TODO for a user requirement or implementation.
globs:
alwaysApply: false
---

View File

@@ -0,0 +1,236 @@
---
description: Iterative development workflow for AI-assisted coding
globs:
alwaysApply: false
---
# Rule: Iterative Development Workflow
## Goal
Establish a structured, iterative development process that prevents the chaos and complexity that can arise from uncontrolled AI-assisted development.
## Development Phases
### Phase 1: Planning and Design
**Before writing any code:**
1. **Understand the Requirement**
- Break down the task into specific, measurable objectives
- Identify existing code patterns that should be followed
- List dependencies and integration points
- Define acceptance criteria
2. **Design Review**
- Propose approach in bullet points
- Wait for explicit approval before proceeding
- Consider how the solution fits existing architecture
- Identify potential risks and mitigation strategies
### Phase 2: Incremental Implementation
**One small piece at a time:**
1. **Micro-Tasks** (≤ 50 lines each)
- Implement one function or small class at a time
- Test immediately after implementation
- Ensure integration with existing code
- Document decisions and patterns used
2. **Validation Checkpoints**
- After each micro-task, verify it works correctly
- Check that it follows established patterns
- Confirm it integrates cleanly with existing code
- Get approval before moving to next micro-task
### Phase 3: Integration and Testing
**Ensuring system coherence:**
1. **Integration Testing**
- Test new code with existing functionality
- Verify no regressions in existing features
- Check performance impact
- Validate error handling
2. **Documentation Update**
- Update relevant documentation
- Record any new patterns or decisions
- Update context files if architecture changed
## Iterative Prompting Strategy
### Step 1: Context Setting
```
Before implementing [feature], help me understand:
1. What existing patterns should I follow?
2. What existing functions/classes are relevant?
3. How should this integrate with [specific existing component]?
4. What are the potential architectural impacts?
```
### Step 2: Plan Creation
```
Based on the context, create a detailed plan for implementing [feature]:
1. Break it into micro-tasks (≤50 lines each)
2. Identify dependencies and order of implementation
3. Specify integration points with existing code
4. List potential risks and mitigation strategies
Wait for my approval before implementing.
```
### Step 3: Incremental Implementation
```
Implement only the first micro-task: [specific task]
- Use existing patterns from [reference file/function]
- Keep it under 50 lines
- Include error handling
- Add appropriate tests
- Explain your implementation choices
Stop after this task and wait for approval.
```
## Quality Gates
### Before Each Implementation
- [ ] **Purpose is clear**: Can explain what this piece does and why
- [ ] **Pattern is established**: Following existing code patterns
- [ ] **Size is manageable**: Implementation is small enough to understand completely
- [ ] **Integration is planned**: Know how it connects to existing code
### After Each Implementation
- [ ] **Code is understood**: Can explain every line of implemented code
- [ ] **Tests pass**: All existing and new tests are passing
- [ ] **Integration works**: New code works with existing functionality
- [ ] **Documentation updated**: Changes are reflected in relevant documentation
### Before Moving to Next Task
- [ ] **Current task complete**: All acceptance criteria met
- [ ] **No regressions**: Existing functionality still works
- [ ] **Clean state**: No temporary code or debugging artifacts
- [ ] **Approval received**: Explicit go-ahead for next task
- [ ] **Documentaion updated**: If relevant changes to module was made.
## Anti-Patterns to Avoid
### Large Block Implementation
**Don't:**
```
Implement the entire user management system with authentication,
CRUD operations, and email notifications.
```
**Do:**
```
First, implement just the User model with basic fields.
Stop there and let me review before continuing.
```
### Context Loss
**Don't:**
```
Create a new authentication system.
```
**Do:**
```
Looking at the existing auth patterns in auth.py, implement
password validation following the same structure as the
existing email validation function.
```
### Over-Engineering
**Don't:**
```
Build a flexible, extensible user management framework that
can handle any future requirements.
```
**Do:**
```
Implement user creation functionality that matches the existing
pattern in customer.py, focusing only on the current requirements.
```
## Progress Tracking
### Task Status Indicators
- 🔄 **In Planning**: Requirements gathering and design
- ⏳ **In Progress**: Currently implementing
- ✅ **Complete**: Implemented, tested, and integrated
- 🚫 **Blocked**: Waiting for decisions or dependencies
- 🔧 **Needs Refactor**: Working but needs improvement
### Weekly Review Process
1. **Progress Assessment**
- What was completed this week?
- What challenges were encountered?
- How well did the iterative process work?
2. **Process Adjustment**
- Were task sizes appropriate?
- Did context management work effectively?
- What improvements can be made?
3. **Architecture Review**
- Is the code remaining maintainable?
- Are patterns staying consistent?
- Is technical debt accumulating?
## Emergency Procedures
### When Things Go Wrong
If development becomes chaotic or problematic:
1. **Stop Development**
- Don't continue adding to the problem
- Take time to assess the situation
- Don't rush to "fix" with more AI-generated code
2. **Assess the Situation**
- What specific problems exist?
- How far has the code diverged from established patterns?
- What parts are still working correctly?
3. **Recovery Process**
- Roll back to last known good state
- Update context documentation with lessons learned
- Restart with smaller, more focused tasks
- Get explicit approval for each step of recovery
### Context Recovery
When AI seems to lose track of project patterns:
1. **Context Refresh**
- Review and update CONTEXT.md
- Include examples of current code patterns
- Clarify architectural decisions
2. **Pattern Re-establishment**
- Show AI examples of existing, working code
- Explicitly state patterns to follow
- Start with very small, pattern-matching tasks
3. **Gradual Re-engagement**
- Begin with simple, low-risk tasks
- Verify pattern adherence at each step
- Gradually increase task complexity as consistency returns
## Success Metrics
### Short-term (Daily)
- Code is understandable and well-integrated
- No major regressions introduced
- Development velocity feels sustainable
- Team confidence in codebase remains high
### Medium-term (Weekly)
- Technical debt is not accumulating
- New features integrate cleanly
- Development patterns remain consistent
- Documentation stays current
### Long-term (Monthly)
- Codebase remains maintainable as it grows
- New team members can understand and contribute
- AI assistance enhances rather than hinders development
- Architecture remains clean and purposeful

24
.cursor/rules/project.mdc Normal file
View File

@@ -0,0 +1,24 @@
---
description:
globs:
alwaysApply: true
---
# Rule: Project specific rules
## Goal
Unify the project structure and interraction with tools and console
### System tools
- **ALWAYS** use UV for package management
- **ALWAYS** use windows PowerShell command for terminal
### Coding patterns
- **ALWYAS** check the arguments and methods before use to avoid errors with whron parameters or names
- If in doubt, check [CONTEXT.md](mdc:CONTEXT.md) file and [architecture.md](mdc:docs/architecture.md)
- **PREFER** ORM pattern for databases with SQLAclhemy.
- **DO NOT USE** emoji in code and comments
### Testing
- Use UV for test in format *uv run pytest [filename]*

View File

@@ -0,0 +1,237 @@
---
description: Code refactoring and technical debt management for AI-assisted development
globs:
alwaysApply: false
---
# Rule: Code Refactoring and Technical Debt Management
## Goal
Guide AI in systematic code refactoring to improve maintainability, reduce complexity, and prevent technical debt accumulation in AI-assisted development projects.
## When to Apply This Rule
- Code complexity has increased beyond manageable levels
- Duplicate code patterns are detected
- Performance issues are identified
- New features are difficult to integrate
- Code review reveals maintainability concerns
- Weekly technical debt assessment indicates refactoring needs
## Pre-Refactoring Assessment
Before starting any refactoring, the AI MUST:
1. **Context Analysis:**
- Review existing `CONTEXT.md` for architectural decisions
- Analyze current code patterns and conventions
- Identify all files that will be affected (search the codebase for use)
- Check for existing tests that verify current behavior
2. **Scope Definition:**
- Clearly define what will and will not be changed
- Identify the specific refactoring pattern to apply
- Estimate the blast radius of changes
- Plan rollback strategy if needed
3. **Documentation Review:**
- Check `./docs/` for relevant module documentation
- Review any existing architectural diagrams
- Identify dependencies and integration points
- Note any known constraints or limitations
## Refactoring Process
### Phase 1: Planning and Safety
1. **Create Refactoring Plan:**
- Document the current state and desired end state
- Break refactoring into small, atomic steps
- Identify tests that must pass throughout the process
- Plan verification steps for each change
2. **Establish Safety Net:**
- Ensure comprehensive test coverage exists
- If tests are missing, create them BEFORE refactoring
- Document current behavior that must be preserved
- Create backup of current implementation approach
3. **Get Approval:**
- Present the refactoring plan to the user
- Wait for explicit "Go" or "Proceed" confirmation
- Do NOT start refactoring without approval
### Phase 2: Incremental Implementation
4. **One Change at a Time:**
- Implement ONE refactoring step per iteration
- Run tests after each step to ensure nothing breaks
- Update documentation if interfaces change
- Mark progress in the refactoring plan
5. **Verification Protocol:**
- Run all relevant tests after each change
- Verify functionality works as expected
- Check performance hasn't degraded
- Ensure no new linting or type errors
6. **User Checkpoint:**
- After each significant step, pause for user review
- Present what was changed and current status
- Wait for approval before continuing
- Address any concerns before proceeding
### Phase 3: Completion and Documentation
7. **Final Verification:**
- Run full test suite to ensure nothing is broken
- Verify all original functionality is preserved
- Check that new code follows project conventions
- Confirm performance is maintained or improved
8. **Documentation Update:**
- Update `CONTEXT.md` with new patterns/decisions
- Update module documentation in `./docs/`
- Document any new conventions established
- Note lessons learned for future refactoring
## Common Refactoring Patterns
### Extract Method/Function
```
WHEN: Functions/methods exceed 50 lines or have multiple responsibilities
HOW:
1. Identify logical groupings within the function
2. Extract each group into a well-named helper function
3. Ensure each function has a single responsibility
4. Verify tests still pass
```
### Extract Module/Class
```
WHEN: Files exceed 250 lines or handle multiple concerns
HOW:
1. Identify cohesive functionality groups
2. Create new files for each group
3. Move related functions/classes together
4. Update imports and dependencies
5. Verify module boundaries are clean
```
### Eliminate Duplication
```
WHEN: Similar code appears in multiple places
HOW:
1. Identify the common pattern or functionality
2. Extract to a shared utility function or module
3. Update all usage sites to use the shared code
4. Ensure the abstraction is not over-engineered
```
### Improve Data Structures
```
WHEN: Complex nested objects or unclear data flow
HOW:
1. Define clear interfaces/types for data structures
2. Create transformation functions between different representations
3. Ensure data flow is unidirectional where possible
4. Add validation at boundaries
```
### Reduce Coupling
```
WHEN: Modules are tightly interconnected
HOW:
1. Identify dependencies between modules
2. Extract interfaces for external dependencies
3. Use dependency injection where appropriate
4. Ensure modules can be tested in isolation
```
## Quality Gates
Every refactoring must pass these gates:
### Technical Quality
- [ ] All existing tests pass
- [ ] No new linting errors introduced
- [ ] Code follows established project conventions
- [ ] No performance regression detected
- [ ] File sizes remain under 250 lines
- [ ] Function sizes remain under 50 lines
### Maintainability
- [ ] Code is more readable than before
- [ ] Duplicated code has been reduced
- [ ] Module responsibilities are clearer
- [ ] Dependencies are explicit and minimal
- [ ] Error handling is consistent
### Documentation
- [ ] Public interfaces are documented
- [ ] Complex logic has explanatory comments
- [ ] Architectural decisions are recorded
- [ ] Examples are provided where helpful
## AI Instructions for Refactoring
1. **Always ask for permission** before starting any refactoring work
2. **Start with tests** - ensure comprehensive coverage before changing code
3. **Work incrementally** - make small changes and verify each step
4. **Preserve behavior** - functionality must remain exactly the same
5. **Update documentation** - keep all docs current with changes
6. **Follow conventions** - maintain consistency with existing codebase
7. **Stop and ask** if any step fails or produces unexpected results
8. **Explain changes** - clearly communicate what was changed and why
## Anti-Patterns to Avoid
### Over-Engineering
- Don't create abstractions for code that isn't duplicated
- Avoid complex inheritance hierarchies
- Don't optimize prematurely
### Breaking Changes
- Never change public APIs without explicit approval
- Don't remove functionality, even if it seems unused
- Avoid changing behavior "while we're here"
### Scope Creep
- Stick to the defined refactoring scope
- Don't add new features during refactoring
- Resist the urge to "improve" unrelated code
## Success Metrics
Track these metrics to ensure refactoring effectiveness:
### Code Quality
- Reduced cyclomatic complexity
- Lower code duplication percentage
- Improved test coverage
- Fewer linting violations
### Developer Experience
- Faster time to understand code
- Easier integration of new features
- Reduced bug introduction rate
- Higher developer confidence in changes
### Maintainability
- Clearer module boundaries
- More predictable behavior
- Easier debugging and troubleshooting
- Better performance characteristics
## Output Files
When refactoring is complete, update:
- `refactoring-log-[date].md` - Document what was changed and why
- `CONTEXT.md` - Update with new patterns and decisions
- `./docs/` - Update relevant module documentation
- Task lists - Mark refactoring tasks as complete
## Final Verification
Before marking refactoring complete:
1. Run full test suite and verify all tests pass
2. Check that code follows all project conventions
3. Verify documentation is up to date
4. Confirm user is satisfied with the results
5. Record lessons learned for future refactoring efforts

View File

@@ -1,5 +1,5 @@
---
description:
description: TODO list task implementation
globs:
alwaysApply: false
---

10
.gitignore vendored
View File

@@ -1,10 +1,13 @@
# ---> Python
/data/*.db
/credentials/*.json
*.csv
*.png
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
/data/*.npy
# C extensions
*.so
@@ -174,8 +177,5 @@ An introduction to trading cycles.pdf
An introduction to trading cycles.txt
README.md
.vscode/launch.json
data/*
frontend/
results/*
test/results/*
data/btcusd_1-day_data.csv
data/btcusd_1-min_data.csv

View File

@@ -1 +0,0 @@
3.10

View File

@@ -1,107 +0,0 @@
"""
IncrementalTrader - A modular incremental trading system
This module provides a complete framework for incremental trading strategies,
including real-time data processing, backtesting, and strategy development tools.
Key Components:
- strategies: Incremental trading strategies and indicators
- trader: Trading execution and position management
- backtester: Backtesting framework and configuration
- utils: Utility functions for timeframe aggregation and data management
Example:
from IncrementalTrader import IncTrader, IncBacktester
from IncrementalTrader.strategies import MetaTrendStrategy
from IncrementalTrader.utils import MinuteDataBuffer, aggregate_minute_data_to_timeframe
# Create strategy
strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"})
# Create trader
trader = IncTrader(strategy, initial_usd=10000)
# Use timeframe utilities
buffer = MinuteDataBuffer(max_size=1440)
# Run backtest
backtester = IncBacktester()
results = backtester.run_single_strategy(strategy)
"""
__version__ = "1.0.0"
__author__ = "Cycles Trading Team"
# Import main components for easy access
# Note: These are now available after migration
try:
from .trader import IncTrader, TradeRecord, PositionManager, MarketFees
except ImportError:
IncTrader = None
TradeRecord = None
PositionManager = None
MarketFees = None
try:
from .backtester import IncBacktester, BacktestConfig, OptimizationConfig
except ImportError:
IncBacktester = None
BacktestConfig = None
OptimizationConfig = None
# Import strategy framework (now available)
from .strategies import IncStrategyBase, IncStrategySignal, TimeframeAggregator
# Import available strategies
from .strategies import (
MetaTrendStrategy,
IncMetaTrendStrategy, # Compatibility alias
RandomStrategy,
IncRandomStrategy, # Compatibility alias
BBRSStrategy,
IncBBRSStrategy, # Compatibility alias
)
# Import timeframe utilities (new)
from .utils import (
aggregate_minute_data_to_timeframe,
parse_timeframe_to_minutes,
get_latest_complete_bar,
MinuteDataBuffer,
TimeframeError
)
# Public API
__all__ = [
# Core components (now available after migration)
"IncTrader",
"IncBacktester",
"BacktestConfig",
"OptimizationConfig",
"TradeRecord",
"PositionManager",
"MarketFees",
# Strategy framework (available now)
"IncStrategyBase",
"IncStrategySignal",
"TimeframeAggregator",
# Available strategies
"MetaTrendStrategy",
"IncMetaTrendStrategy", # Compatibility alias
"RandomStrategy",
"IncRandomStrategy", # Compatibility alias
"BBRSStrategy",
"IncBBRSStrategy", # Compatibility alias
# Timeframe utilities (new)
"aggregate_minute_data_to_timeframe",
"parse_timeframe_to_minutes",
"get_latest_complete_bar",
"MinuteDataBuffer",
"TimeframeError",
# Version info
"__version__",
]

View File

@@ -1,48 +0,0 @@
"""
Incremental Backtesting Framework
This module provides comprehensive backtesting capabilities for incremental trading strategies.
It includes configuration management, data loading, parallel execution, and result analysis.
Components:
- IncBacktester: Main backtesting engine
- BacktestConfig: Configuration management for backtests
- OptimizationConfig: Configuration for parameter optimization
- DataLoader: Data loading and validation utilities
- SystemUtils: System resource management
- ResultsSaver: Result saving and reporting utilities
Example:
from IncrementalTrader.backtester import IncBacktester, BacktestConfig
from IncrementalTrader.strategies import MetaTrendStrategy
# Configure backtest
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000
)
# Run single strategy
strategy = MetaTrendStrategy("metatrend")
backtester = IncBacktester(config)
results = backtester.run_single_strategy(strategy)
# Parameter optimization
param_grid = {"timeframe": ["5min", "15min", "30min"]}
results = backtester.optimize_parameters(MetaTrendStrategy, param_grid)
"""
from .backtester import IncBacktester
from .config import BacktestConfig, OptimizationConfig
from .utils import DataLoader, SystemUtils, ResultsSaver
__all__ = [
"IncBacktester",
"BacktestConfig",
"OptimizationConfig",
"DataLoader",
"SystemUtils",
"ResultsSaver",
]

View File

@@ -1,524 +0,0 @@
"""
Incremental Backtester for testing incremental strategies.
This module provides the IncBacktester class that orchestrates multiple IncTraders
for parallel testing, handles data loading and feeding, and supports multiprocessing
for parameter optimization.
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Any, Callable, Union, Tuple
import logging
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
from itertools import product
import multiprocessing as mp
from datetime import datetime
# Use try/except for imports to handle both relative and absolute import scenarios
try:
from ..trader.trader import IncTrader
from ..strategies.base import IncStrategyBase
from .config import BacktestConfig, OptimizationConfig
from .utils import DataLoader, SystemUtils, ResultsSaver
except ImportError:
# Fallback for direct execution
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from trader.trader import IncTrader
from strategies.base import IncStrategyBase
from config import BacktestConfig, OptimizationConfig
from utils import DataLoader, SystemUtils, ResultsSaver
logger = logging.getLogger(__name__)
def _worker_function(args: Tuple[type, Dict, Dict, BacktestConfig]) -> Dict[str, Any]:
"""
Worker function for multiprocessing parameter optimization.
This function must be at module level to be picklable for multiprocessing.
Args:
args: Tuple containing (strategy_class, strategy_params, trader_params, config)
Returns:
Dict containing backtest results
"""
try:
strategy_class, strategy_params, trader_params, config = args
# Create new backtester instance for this worker
worker_backtester = IncBacktester(config)
# Create strategy instance
strategy = strategy_class(params=strategy_params)
# Run backtest
result = worker_backtester.run_single_strategy(strategy, trader_params)
result["success"] = True
return result
except Exception as e:
logger.error(f"Worker error for {strategy_params}, {trader_params}: {e}")
return {
"strategy_params": strategy_params,
"trader_params": trader_params,
"error": str(e),
"success": False
}
class IncBacktester:
"""
Incremental backtester for testing incremental strategies.
This class orchestrates multiple IncTraders for parallel testing:
- Loads data using the integrated DataLoader
- Creates multiple IncTrader instances with different parameters
- Feeds data sequentially to all traders
- Collects and aggregates results
- Supports multiprocessing for parallel execution
- Uses SystemUtils for optimal worker count determination
The backtester can run multiple strategies simultaneously or test
parameter combinations across multiple CPU cores.
Example:
# Single strategy backtest
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000
)
strategy = RandomStrategy("random", params={"timeframe": "15min"})
backtester = IncBacktester(config)
results = backtester.run_single_strategy(strategy)
# Multiple strategies
strategies = [strategy1, strategy2, strategy3]
results = backtester.run_multiple_strategies(strategies)
# Parameter optimization
param_grid = {
"timeframe": ["5min", "15min", "30min"],
"stop_loss_pct": [0.01, 0.02, 0.03]
}
results = backtester.optimize_parameters(strategy_class, param_grid)
"""
def __init__(self, config: BacktestConfig):
"""
Initialize the incremental backtester.
Args:
config: Backtesting configuration
"""
self.config = config
# Initialize utilities
self.data_loader = DataLoader(config.data_dir)
self.system_utils = SystemUtils()
self.results_saver = ResultsSaver(config.results_dir)
# State management
self.data = None
self.results_cache = {}
# Track all actions performed during backtesting
self.action_log = []
self.session_start_time = datetime.now()
logger.info(f"IncBacktester initialized: {config.data_file}, "
f"{config.start_date} to {config.end_date}")
self._log_action("backtester_initialized", {
"config": config.to_dict(),
"session_start": self.session_start_time.isoformat(),
"system_info": self.system_utils.get_system_info()
})
def _log_action(self, action_type: str, details: Dict[str, Any]) -> None:
"""Log an action performed during backtesting."""
self.action_log.append({
"timestamp": datetime.now().isoformat(),
"action_type": action_type,
"details": details
})
def load_data(self) -> pd.DataFrame:
"""
Load and prepare data for backtesting.
Returns:
pd.DataFrame: Loaded OHLCV data with DatetimeIndex
"""
if self.data is None:
logger.info(f"Loading data from {self.config.data_file}...")
start_time = time.time()
self.data = self.data_loader.load_data(
self.config.data_file,
self.config.start_date,
self.config.end_date
)
load_time = time.time() - start_time
logger.info(f"Data loaded: {len(self.data)} rows in {load_time:.2f}s")
# Validate data
if self.data.empty:
raise ValueError(f"No data loaded for the specified date range")
if not self.data_loader.validate_data(self.data):
raise ValueError("Data validation failed")
self._log_action("data_loaded", {
"file": self.config.data_file,
"rows": len(self.data),
"load_time_seconds": load_time,
"date_range": f"{self.config.start_date} to {self.config.end_date}",
"columns": list(self.data.columns)
})
return self.data
def run_single_strategy(self, strategy: IncStrategyBase,
trader_params: Optional[Dict] = None) -> Dict[str, Any]:
"""
Run backtest for a single strategy.
Args:
strategy: Incremental strategy instance
trader_params: Additional trader parameters
Returns:
Dict containing backtest results
"""
data = self.load_data()
# Merge trader parameters
final_trader_params = {
"stop_loss_pct": self.config.stop_loss_pct,
"take_profit_pct": self.config.take_profit_pct
}
if trader_params:
final_trader_params.update(trader_params)
# Create trader
trader = IncTrader(
strategy=strategy,
initial_usd=self.config.initial_usd,
params=final_trader_params
)
# Run backtest
logger.info(f"Starting backtest for {strategy.name}...")
start_time = time.time()
self._log_action("single_strategy_backtest_started", {
"strategy_name": strategy.name,
"strategy_params": strategy.params,
"trader_params": final_trader_params,
"data_points": len(data)
})
for timestamp, row in data.iterrows():
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
trader.process_data_point(timestamp, ohlcv_data)
# Finalize and get results
trader.finalize()
results = trader.get_results()
backtest_time = time.time() - start_time
results["backtest_duration_seconds"] = backtest_time
results["data_points"] = len(data)
results["config"] = self.config.to_dict()
logger.info(f"Backtest completed for {strategy.name} in {backtest_time:.2f}s: "
f"${results['final_usd']:.2f} ({results['profit_ratio']*100:.2f}%), "
f"{results['n_trades']} trades")
self._log_action("single_strategy_backtest_completed", {
"strategy_name": strategy.name,
"backtest_duration_seconds": backtest_time,
"final_usd": results['final_usd'],
"profit_ratio": results['profit_ratio'],
"n_trades": results['n_trades'],
"win_rate": results['win_rate']
})
return results
def run_multiple_strategies(self, strategies: List[IncStrategyBase],
trader_params: Optional[Dict] = None) -> List[Dict[str, Any]]:
"""
Run backtest for multiple strategies simultaneously.
Args:
strategies: List of incremental strategy instances
trader_params: Additional trader parameters
Returns:
List of backtest results for each strategy
"""
self._log_action("multiple_strategies_backtest_started", {
"strategy_count": len(strategies),
"strategy_names": [s.name for s in strategies]
})
results = []
for strategy in strategies:
try:
result = self.run_single_strategy(strategy, trader_params)
results.append(result)
except Exception as e:
logger.error(f"Error running strategy {strategy.name}: {e}")
# Add error result
error_result = {
"strategy_name": strategy.name,
"error": str(e),
"success": False
}
results.append(error_result)
self._log_action("strategy_error", {
"strategy_name": strategy.name,
"error": str(e)
})
self._log_action("multiple_strategies_backtest_completed", {
"total_strategies": len(strategies),
"successful_strategies": len([r for r in results if r.get("success", True)]),
"failed_strategies": len([r for r in results if not r.get("success", True)])
})
return results
def optimize_parameters(self, strategy_class: type, param_grid: Dict[str, List],
trader_param_grid: Optional[Dict[str, List]] = None,
max_workers: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Optimize strategy parameters using grid search with multiprocessing.
Args:
strategy_class: Strategy class to instantiate
param_grid: Grid of strategy parameters to test
trader_param_grid: Grid of trader parameters to test
max_workers: Maximum number of worker processes (uses SystemUtils if None)
Returns:
List of results for each parameter combination
"""
# Generate parameter combinations
strategy_combinations = list(self._generate_param_combinations(param_grid))
trader_combinations = list(self._generate_param_combinations(trader_param_grid or {}))
# If no trader param grid, use default
if not trader_combinations:
trader_combinations = [{}]
# Create all combinations
all_combinations = []
for strategy_params in strategy_combinations:
for trader_params in trader_combinations:
all_combinations.append((strategy_params, trader_params))
logger.info(f"Starting parameter optimization: {len(all_combinations)} combinations")
# Determine number of workers using SystemUtils
if max_workers is None:
max_workers = self.system_utils.get_optimal_workers()
else:
max_workers = min(max_workers, len(all_combinations))
self._log_action("parameter_optimization_started", {
"strategy_class": strategy_class.__name__,
"total_combinations": len(all_combinations),
"max_workers": max_workers,
"strategy_param_grid": param_grid,
"trader_param_grid": trader_param_grid or {}
})
# Run optimization
if max_workers == 1 or len(all_combinations) == 1:
# Single-threaded execution
results = []
for strategy_params, trader_params in all_combinations:
result = self._run_single_combination(strategy_class, strategy_params, trader_params)
results.append(result)
else:
# Multi-threaded execution
results = self._run_parallel_optimization(
strategy_class, all_combinations, max_workers
)
# Sort results by profit ratio
valid_results = [r for r in results if r.get("success", True)]
valid_results.sort(key=lambda x: x.get("profit_ratio", -float('inf')), reverse=True)
logger.info(f"Parameter optimization completed: {len(valid_results)} successful runs")
self._log_action("parameter_optimization_completed", {
"total_runs": len(results),
"successful_runs": len(valid_results),
"failed_runs": len(results) - len(valid_results),
"best_profit_ratio": valid_results[0]["profit_ratio"] if valid_results else None,
"worst_profit_ratio": valid_results[-1]["profit_ratio"] if valid_results else None
})
return results
def _generate_param_combinations(self, param_grid: Dict[str, List]) -> List[Dict]:
"""Generate all parameter combinations from grid."""
if not param_grid:
return [{}]
keys = list(param_grid.keys())
values = list(param_grid.values())
combinations = []
for combination in product(*values):
param_dict = dict(zip(keys, combination))
combinations.append(param_dict)
return combinations
def _run_single_combination(self, strategy_class: type, strategy_params: Dict,
trader_params: Dict) -> Dict[str, Any]:
"""Run backtest for a single parameter combination."""
try:
# Create strategy instance
strategy = strategy_class(params=strategy_params)
# Run backtest
result = self.run_single_strategy(strategy, trader_params)
result["success"] = True
return result
except Exception as e:
logger.error(f"Error in parameter combination {strategy_params}, {trader_params}: {e}")
return {
"strategy_params": strategy_params,
"trader_params": trader_params,
"error": str(e),
"success": False
}
def _run_parallel_optimization(self, strategy_class: type, combinations: List,
max_workers: int) -> List[Dict[str, Any]]:
"""Run parameter optimization in parallel."""
results = []
# Prepare arguments for worker function
worker_args = []
for strategy_params, trader_params in combinations:
args = (strategy_class, strategy_params, trader_params, self.config)
worker_args.append(args)
# Execute in parallel
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# Submit all jobs
future_to_params = {
executor.submit(_worker_function, args): args[1:3] # strategy_params, trader_params
for args in worker_args
}
# Collect results as they complete
for future in as_completed(future_to_params):
combo = future_to_params[future]
try:
result = future.result()
results.append(result)
if result.get("success", True):
logger.info(f"Completed: {combo[0]} -> "
f"${result.get('final_usd', 0):.2f} "
f"({result.get('profit_ratio', 0)*100:.2f}%)")
except Exception as e:
logger.error(f"Worker error for {combo}: {e}")
results.append({
"strategy_params": combo[0],
"trader_params": combo[1],
"error": str(e),
"success": False
})
return results
def get_summary_statistics(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Calculate summary statistics across multiple backtest results.
Args:
results: List of backtest results
Returns:
Dict containing summary statistics
"""
return self.results_saver._calculate_summary_statistics(results)
def save_results(self, results: List[Dict[str, Any]], filename: str) -> None:
"""
Save backtest results to CSV file.
Args:
results: List of backtest results
filename: Output filename
"""
self.results_saver.save_results_csv(results, filename)
def save_comprehensive_results(self, results: List[Dict[str, Any]],
base_filename: str,
summary: Optional[Dict[str, Any]] = None) -> None:
"""
Save comprehensive backtest results including summary, individual results, and action log.
Args:
results: List of backtest results
base_filename: Base filename (without extension)
summary: Optional summary statistics
"""
self.results_saver.save_comprehensive_results(
results=results,
base_filename=base_filename,
summary=summary,
action_log=self.action_log,
session_start_time=self.session_start_time
)
def get_action_log(self) -> List[Dict[str, Any]]:
"""Get the complete action log for this session."""
return self.action_log.copy()
def reset_session(self) -> None:
"""Reset the backtester session (clear cache and logs)."""
self.data = None
self.results_cache.clear()
self.action_log.clear()
self.session_start_time = datetime.now()
logger.info("Backtester session reset")
self._log_action("session_reset", {
"reset_time": self.session_start_time.isoformat()
})
def __repr__(self) -> str:
"""String representation of the backtester."""
return (f"IncBacktester(data_file={self.config.data_file}, "
f"date_range={self.config.start_date} to {self.config.end_date}, "
f"initial_usd=${self.config.initial_usd})")

View File

@@ -1,207 +0,0 @@
"""
Backtester Configuration
This module provides configuration classes and utilities for backtesting
incremental trading strategies.
"""
import os
import pandas as pd
from dataclasses import dataclass
from typing import Optional, Dict, Any, List
import logging
logger = logging.getLogger(__name__)
@dataclass
class BacktestConfig:
"""
Configuration for backtesting runs.
This class encapsulates all configuration parameters needed for running
backtests, including data settings, trading parameters, and performance options.
Attributes:
data_file: Path to the data file (relative to data directory)
start_date: Start date for backtesting (YYYY-MM-DD format)
end_date: End date for backtesting (YYYY-MM-DD format)
initial_usd: Initial USD balance for trading
timeframe: Data timeframe (e.g., "1min", "5min", "15min")
stop_loss_pct: Default stop loss percentage (0.0 to disable)
take_profit_pct: Default take profit percentage (0.0 to disable)
max_workers: Maximum number of worker processes for parallel execution
chunk_size: Chunk size for data processing
data_dir: Directory containing data files
results_dir: Directory for saving results
Example:
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000,
stop_loss_pct=0.02
)
"""
data_file: str
start_date: str
end_date: str
initial_usd: float = 10000
timeframe: str = "1min"
# Risk management parameters
stop_loss_pct: float = 0.0
take_profit_pct: float = 0.0
# Performance settings
max_workers: Optional[int] = None
chunk_size: int = 1000
# Directory settings
data_dir: str = "data"
results_dir: str = "results"
def __post_init__(self):
"""Validate configuration after initialization."""
self._validate_config()
self._ensure_directories()
def _validate_config(self):
"""Validate configuration parameters."""
# Validate dates
try:
start_dt = pd.to_datetime(self.start_date)
end_dt = pd.to_datetime(self.end_date)
if start_dt >= end_dt:
raise ValueError("start_date must be before end_date")
except Exception as e:
raise ValueError(f"Invalid date format: {e}")
# Validate financial parameters
if self.initial_usd <= 0:
raise ValueError("initial_usd must be positive")
if not (0 <= self.stop_loss_pct <= 1):
raise ValueError("stop_loss_pct must be between 0 and 1")
if not (0 <= self.take_profit_pct <= 1):
raise ValueError("take_profit_pct must be between 0 and 1")
# Validate performance parameters
if self.max_workers is not None and self.max_workers <= 0:
raise ValueError("max_workers must be positive")
if self.chunk_size <= 0:
raise ValueError("chunk_size must be positive")
def _ensure_directories(self):
"""Ensure required directories exist."""
os.makedirs(self.data_dir, exist_ok=True)
os.makedirs(self.results_dir, exist_ok=True)
def get_data_path(self) -> str:
"""Get full path to data file."""
return os.path.join(self.data_dir, self.data_file)
def get_results_path(self, filename: str) -> str:
"""Get full path for results file."""
return os.path.join(self.results_dir, filename)
def to_dict(self) -> Dict[str, Any]:
"""Convert configuration to dictionary."""
return {
"data_file": self.data_file,
"start_date": self.start_date,
"end_date": self.end_date,
"initial_usd": self.initial_usd,
"timeframe": self.timeframe,
"stop_loss_pct": self.stop_loss_pct,
"take_profit_pct": self.take_profit_pct,
"max_workers": self.max_workers,
"chunk_size": self.chunk_size,
"data_dir": self.data_dir,
"results_dir": self.results_dir
}
@classmethod
def from_dict(cls, config_dict: Dict[str, Any]) -> 'BacktestConfig':
"""Create configuration from dictionary."""
return cls(**config_dict)
def copy(self, **kwargs) -> 'BacktestConfig':
"""Create a copy of the configuration with optional parameter overrides."""
config_dict = self.to_dict()
config_dict.update(kwargs)
return self.from_dict(config_dict)
def __repr__(self) -> str:
"""String representation of the configuration."""
return (f"BacktestConfig(data_file={self.data_file}, "
f"date_range={self.start_date} to {self.end_date}, "
f"initial_usd=${self.initial_usd})")
class OptimizationConfig:
"""
Configuration for parameter optimization runs.
This class provides additional configuration options specifically for
parameter optimization and grid search operations.
"""
def __init__(self,
base_config: BacktestConfig,
strategy_param_grid: Dict[str, List],
trader_param_grid: Optional[Dict[str, List]] = None,
max_workers: Optional[int] = None,
save_individual_results: bool = True,
save_detailed_logs: bool = False):
"""
Initialize optimization configuration.
Args:
base_config: Base backtesting configuration
strategy_param_grid: Grid of strategy parameters to test
trader_param_grid: Grid of trader parameters to test
max_workers: Maximum number of worker processes
save_individual_results: Whether to save individual strategy results
save_detailed_logs: Whether to save detailed action logs
"""
self.base_config = base_config
self.strategy_param_grid = strategy_param_grid
self.trader_param_grid = trader_param_grid or {}
self.max_workers = max_workers
self.save_individual_results = save_individual_results
self.save_detailed_logs = save_detailed_logs
def get_total_combinations(self) -> int:
"""Calculate total number of parameter combinations."""
from itertools import product
# Calculate strategy combinations
strategy_values = list(self.strategy_param_grid.values())
strategy_combinations = len(list(product(*strategy_values))) if strategy_values else 1
# Calculate trader combinations
trader_values = list(self.trader_param_grid.values())
trader_combinations = len(list(product(*trader_values))) if trader_values else 1
return strategy_combinations * trader_combinations
def to_dict(self) -> Dict[str, Any]:
"""Convert optimization configuration to dictionary."""
return {
"base_config": self.base_config.to_dict(),
"strategy_param_grid": self.strategy_param_grid,
"trader_param_grid": self.trader_param_grid,
"max_workers": self.max_workers,
"save_individual_results": self.save_individual_results,
"save_detailed_logs": self.save_detailed_logs,
"total_combinations": self.get_total_combinations()
}
def __repr__(self) -> str:
"""String representation of the optimization configuration."""
return (f"OptimizationConfig(combinations={self.get_total_combinations()}, "
f"max_workers={self.max_workers})")

View File

@@ -1,480 +0,0 @@
"""
Backtester Utilities
This module provides utility functions for data loading, system resource management,
and result saving for the incremental backtesting framework.
"""
import os
import json
import pandas as pd
import numpy as np
import psutil
from typing import Dict, List, Any, Optional
import logging
from datetime import datetime
logger = logging.getLogger(__name__)
class DataLoader:
"""
Data loading utilities for backtesting.
This class handles loading and preprocessing of market data from various formats
including CSV and JSON files.
"""
def __init__(self, data_dir: str = "data"):
"""
Initialize data loader.
Args:
data_dir: Directory containing data files
"""
self.data_dir = data_dir
os.makedirs(self.data_dir, exist_ok=True)
def load_data(self, file_path: str, start_date: str, end_date: str) -> pd.DataFrame:
"""
Load data with optimized dtypes and filtering, supporting CSV and JSON input.
Args:
file_path: Path to the data file (relative to data_dir)
start_date: Start date for filtering (YYYY-MM-DD format)
end_date: End date for filtering (YYYY-MM-DD format)
Returns:
pd.DataFrame: Loaded OHLCV data with DatetimeIndex
"""
full_path = os.path.join(self.data_dir, file_path)
if not os.path.exists(full_path):
raise FileNotFoundError(f"Data file not found: {full_path}")
# Determine file type
_, ext = os.path.splitext(file_path)
ext = ext.lower()
try:
if ext == ".json":
return self._load_json_data(full_path, start_date, end_date)
else:
return self._load_csv_data(full_path, start_date, end_date)
except Exception as e:
logger.error(f"Error loading data from {file_path}: {e}")
# Return an empty DataFrame with a DatetimeIndex
return pd.DataFrame(index=pd.to_datetime([]))
def _load_json_data(self, file_path: str, start_date: str, end_date: str) -> pd.DataFrame:
"""Load data from JSON file."""
with open(file_path, 'r') as f:
raw = json.load(f)
data = pd.DataFrame(raw["Data"])
# Convert columns to lowercase
data.columns = data.columns.str.lower()
# Convert timestamp to datetime
data["timestamp"] = pd.to_datetime(data["timestamp"], unit="s")
# Filter by date range
data = data[(data["timestamp"] >= start_date) & (data["timestamp"] <= end_date)]
logger.info(f"JSON data loaded: {len(data)} rows for {start_date} to {end_date}")
return data.set_index("timestamp")
def _load_csv_data(self, file_path: str, start_date: str, end_date: str) -> pd.DataFrame:
"""Load data from CSV file."""
# Define optimized dtypes
dtypes = {
'Open': 'float32',
'High': 'float32',
'Low': 'float32',
'Close': 'float32',
'Volume': 'float32'
}
# Read data with original capitalized column names
data = pd.read_csv(file_path, dtype=dtypes)
# Handle timestamp column
if 'Timestamp' in data.columns:
data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s')
# Filter by date range
data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= end_date)]
# Convert column names to lowercase
data.columns = data.columns.str.lower()
logger.info(f"CSV data loaded: {len(data)} rows for {start_date} to {end_date}")
return data.set_index('timestamp')
else:
# Attempt to use the first column if 'Timestamp' is not present
data.rename(columns={data.columns[0]: 'timestamp'}, inplace=True)
data['timestamp'] = pd.to_datetime(data['timestamp'], unit='s')
data = data[(data['timestamp'] >= start_date) & (data['timestamp'] <= end_date)]
data.columns = data.columns.str.lower()
logger.info(f"CSV data loaded (first column as timestamp): {len(data)} rows for {start_date} to {end_date}")
return data.set_index('timestamp')
def validate_data(self, data: pd.DataFrame) -> bool:
"""
Validate loaded data for required columns and basic integrity.
Args:
data: DataFrame to validate
Returns:
bool: True if data is valid
"""
if data.empty:
logger.error("Data is empty")
return False
required_columns = ['open', 'high', 'low', 'close', 'volume']
missing_columns = [col for col in required_columns if col not in data.columns]
if missing_columns:
logger.error(f"Missing required columns: {missing_columns}")
return False
# Check for NaN values
if data[required_columns].isnull().any().any():
logger.warning("Data contains NaN values")
# Check for negative prices
price_columns = ['open', 'high', 'low', 'close']
if (data[price_columns] <= 0).any().any():
logger.warning("Data contains non-positive prices")
# Check OHLC consistency
if not ((data['low'] <= data['open']) &
(data['low'] <= data['close']) &
(data['high'] >= data['open']) &
(data['high'] >= data['close'])).all():
logger.warning("Data contains OHLC inconsistencies")
return True
class SystemUtils:
"""
System resource management utilities.
This class provides methods for determining optimal system resource usage
for parallel processing and performance optimization.
"""
def __init__(self):
"""Initialize system utilities."""
pass
def get_optimal_workers(self) -> int:
"""
Determine optimal number of worker processes based on system resources.
Returns:
int: Optimal number of worker processes
"""
cpu_count = os.cpu_count() or 4
memory_gb = psutil.virtual_memory().total / (1024**3)
# Heuristic: Use 75% of cores, but cap based on available memory
# Assume each worker needs ~2GB for large datasets
workers_by_memory = max(1, int(memory_gb / 2))
workers_by_cpu = max(1, int(cpu_count * 0.75))
optimal_workers = min(workers_by_cpu, workers_by_memory)
logger.info(f"System resources: {cpu_count} CPUs, {memory_gb:.1f}GB RAM")
logger.info(f"Using {optimal_workers} workers for processing")
return optimal_workers
def get_system_info(self) -> Dict[str, Any]:
"""
Get comprehensive system information.
Returns:
Dict containing system information
"""
memory = psutil.virtual_memory()
return {
"cpu_count": os.cpu_count(),
"memory_total_gb": memory.total / (1024**3),
"memory_available_gb": memory.available / (1024**3),
"memory_percent": memory.percent,
"optimal_workers": self.get_optimal_workers()
}
class ResultsSaver:
"""
Results saving utilities for backtesting.
This class handles saving backtest results in various formats including
CSV, JSON, and comprehensive reports.
"""
def __init__(self, results_dir: str = "results"):
"""
Initialize results saver.
Args:
results_dir: Directory for saving results
"""
self.results_dir = results_dir
os.makedirs(self.results_dir, exist_ok=True)
def save_results_csv(self, results: List[Dict[str, Any]], filename: str) -> None:
"""
Save backtest results to CSV file.
Args:
results: List of backtest results
filename: Output filename
"""
try:
# Convert results to DataFrame for easy saving
df_data = []
for result in results:
if result.get("success", True):
row = {
"strategy_name": result.get("strategy_name", ""),
"profit_ratio": result.get("profit_ratio", 0),
"final_usd": result.get("final_usd", 0),
"n_trades": result.get("n_trades", 0),
"win_rate": result.get("win_rate", 0),
"max_drawdown": result.get("max_drawdown", 0),
"avg_trade": result.get("avg_trade", 0),
"total_fees_usd": result.get("total_fees_usd", 0),
"backtest_duration_seconds": result.get("backtest_duration_seconds", 0),
"data_points_processed": result.get("data_points_processed", 0)
}
# Add strategy parameters
strategy_params = result.get("strategy_params", {})
for key, value in strategy_params.items():
row[f"strategy_{key}"] = value
# Add trader parameters
trader_params = result.get("trader_params", {})
for key, value in trader_params.items():
row[f"trader_{key}"] = value
df_data.append(row)
# Save to CSV
df = pd.DataFrame(df_data)
full_path = os.path.join(self.results_dir, filename)
df.to_csv(full_path, index=False)
logger.info(f"Results saved to {full_path}: {len(df_data)} rows")
except Exception as e:
logger.error(f"Error saving results to {filename}: {e}")
raise
def save_comprehensive_results(self, results: List[Dict[str, Any]],
base_filename: str,
summary: Optional[Dict[str, Any]] = None,
action_log: Optional[List[Dict[str, Any]]] = None,
session_start_time: Optional[datetime] = None) -> None:
"""
Save comprehensive backtest results including summary, individual results, and logs.
Args:
results: List of backtest results
base_filename: Base filename (without extension)
summary: Optional summary statistics
action_log: Optional action log
session_start_time: Optional session start time
"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
session_start = session_start_time or datetime.now()
# 1. Save summary report
if summary is None:
summary = self._calculate_summary_statistics(results)
summary_data = {
"session_info": {
"timestamp": timestamp,
"session_start": session_start.isoformat(),
"session_duration_seconds": (datetime.now() - session_start).total_seconds()
},
"summary_statistics": summary,
"action_log_summary": {
"total_actions": len(action_log) if action_log else 0,
"action_types": list(set(action["action_type"] for action in action_log)) if action_log else []
}
}
summary_filename = f"{base_filename}_summary_{timestamp}.json"
self._save_json(summary_data, summary_filename)
# 2. Save detailed results CSV
self.save_results_csv(results, f"{base_filename}_detailed_{timestamp}.csv")
# 3. Save individual strategy results
valid_results = [r for r in results if r.get("success", True)]
for i, result in enumerate(valid_results):
strategy_filename = f"{base_filename}_strategy_{i+1}_{result['strategy_name']}_{timestamp}.json"
strategy_data = self._format_strategy_result(result)
self._save_json(strategy_data, strategy_filename)
# 4. Save action log if provided
if action_log:
action_log_filename = f"{base_filename}_actions_{timestamp}.json"
action_log_data = {
"session_info": {
"timestamp": timestamp,
"session_start": session_start.isoformat(),
"total_actions": len(action_log)
},
"actions": action_log
}
self._save_json(action_log_data, action_log_filename)
# 5. Create master index file
index_filename = f"{base_filename}_index_{timestamp}.json"
index_data = self._create_index_file(base_filename, timestamp, valid_results, summary)
self._save_json(index_data, index_filename)
# Print summary
print(f"\n📊 Comprehensive results saved:")
print(f" 📋 Summary: {self.results_dir}/{summary_filename}")
print(f" 📈 Detailed CSV: {self.results_dir}/{base_filename}_detailed_{timestamp}.csv")
if action_log:
print(f" 📝 Action Log: {self.results_dir}/{action_log_filename}")
print(f" 📁 Individual Strategies: {len(valid_results)} files")
print(f" 🗂️ Master Index: {self.results_dir}/{index_filename}")
except Exception as e:
logger.error(f"Error saving comprehensive results: {e}")
raise
def _save_json(self, data: Dict[str, Any], filename: str) -> None:
"""Save data to JSON file."""
full_path = os.path.join(self.results_dir, filename)
with open(full_path, 'w') as f:
json.dump(data, f, indent=2, default=str)
logger.info(f"JSON saved: {full_path}")
def _calculate_summary_statistics(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Calculate summary statistics from results."""
valid_results = [r for r in results if r.get("success", True)]
if not valid_results:
return {
"total_runs": len(results),
"successful_runs": 0,
"failed_runs": len(results),
"error": "No valid results to summarize"
}
# Extract metrics
profit_ratios = [r["profit_ratio"] for r in valid_results]
final_balances = [r["final_usd"] for r in valid_results]
n_trades_list = [r["n_trades"] for r in valid_results]
win_rates = [r["win_rate"] for r in valid_results]
max_drawdowns = [r["max_drawdown"] for r in valid_results]
return {
"total_runs": len(results),
"successful_runs": len(valid_results),
"failed_runs": len(results) - len(valid_results),
"profit_ratio": {
"mean": np.mean(profit_ratios),
"std": np.std(profit_ratios),
"min": np.min(profit_ratios),
"max": np.max(profit_ratios),
"median": np.median(profit_ratios)
},
"final_usd": {
"mean": np.mean(final_balances),
"std": np.std(final_balances),
"min": np.min(final_balances),
"max": np.max(final_balances),
"median": np.median(final_balances)
},
"n_trades": {
"mean": np.mean(n_trades_list),
"std": np.std(n_trades_list),
"min": np.min(n_trades_list),
"max": np.max(n_trades_list),
"median": np.median(n_trades_list)
},
"win_rate": {
"mean": np.mean(win_rates),
"std": np.std(win_rates),
"min": np.min(win_rates),
"max": np.max(win_rates),
"median": np.median(win_rates)
},
"max_drawdown": {
"mean": np.mean(max_drawdowns),
"std": np.std(max_drawdowns),
"min": np.min(max_drawdowns),
"max": np.max(max_drawdowns),
"median": np.median(max_drawdowns)
},
"best_run": max(valid_results, key=lambda x: x["profit_ratio"]),
"worst_run": min(valid_results, key=lambda x: x["profit_ratio"])
}
def _format_strategy_result(self, result: Dict[str, Any]) -> Dict[str, Any]:
"""Format individual strategy result for saving."""
return {
"strategy_info": {
"name": result['strategy_name'],
"params": result.get('strategy_params', {}),
"trader_params": result.get('trader_params', {})
},
"performance": {
"initial_usd": result['initial_usd'],
"final_usd": result['final_usd'],
"profit_ratio": result['profit_ratio'],
"n_trades": result['n_trades'],
"win_rate": result['win_rate'],
"max_drawdown": result['max_drawdown'],
"avg_trade": result['avg_trade'],
"total_fees_usd": result['total_fees_usd']
},
"execution": {
"backtest_duration_seconds": result.get('backtest_duration_seconds', 0),
"data_points_processed": result.get('data_points_processed', 0),
"warmup_complete": result.get('warmup_complete', False)
},
"trades": result.get('trades', [])
}
def _create_index_file(self, base_filename: str, timestamp: str,
valid_results: List[Dict[str, Any]],
summary: Dict[str, Any]) -> Dict[str, Any]:
"""Create master index file."""
return {
"session_info": {
"timestamp": timestamp,
"base_filename": base_filename,
"total_strategies": len(valid_results)
},
"files": {
"summary": f"{base_filename}_summary_{timestamp}.json",
"detailed_csv": f"{base_filename}_detailed_{timestamp}.csv",
"individual_strategies": [
f"{base_filename}_strategy_{i+1}_{result['strategy_name']}_{timestamp}.json"
for i, result in enumerate(valid_results)
]
},
"quick_stats": {
"best_profit": summary.get("profit_ratio", {}).get("max", 0) if summary.get("profit_ratio") else 0,
"worst_profit": summary.get("profit_ratio", {}).get("min", 0) if summary.get("profit_ratio") else 0,
"avg_profit": summary.get("profit_ratio", {}).get("mean", 0) if summary.get("profit_ratio") else 0,
"total_successful_runs": summary.get("successful_runs", 0),
"total_failed_runs": summary.get("failed_runs", 0)
}
}

View File

@@ -1,255 +0,0 @@
# Architecture Overview
## Design Philosophy
IncrementalTrader is built around the principle of **incremental computation** - processing new data points efficiently without recalculating the entire history. This approach provides significant performance benefits for real-time trading applications.
### Core Principles
1. **Modularity**: Clear separation of concerns between strategies, execution, and testing
2. **Efficiency**: Constant memory usage and minimal computational overhead
3. **Extensibility**: Easy to add new strategies, indicators, and features
4. **Reliability**: Robust error handling and comprehensive testing
5. **Simplicity**: Clean APIs that are easy to understand and use
## System Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ IncrementalTrader │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Strategies │ │ Trader │ │ Backtester │ │
│ │ │ │ │ │ │ │
│ │ • Base │ │ • Execution │ │ • Configuration │ │
│ │ • MetaTrend │ │ • Position │ │ • Results │ │
│ │ • Random │ │ • Tracking │ │ • Optimization │ │
│ │ • BBRS │ │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ Indicators │ │ │ │ │ │
│ │ • Supertrend│ │ │ │ │ │
│ │ • Bollinger │ │ │ │ │ │
│ │ • RSI │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
## Component Details
### Strategies Module
The strategies module contains all trading logic and signal generation:
- **Base Classes**: `IncStrategyBase` provides the foundation for all strategies
- **Timeframe Aggregation**: Built-in support for multiple timeframes
- **Signal Generation**: Standardized signal types (BUY, SELL, HOLD)
- **Incremental Indicators**: Memory-efficient technical indicators
#### Strategy Lifecycle
```python
# 1. Initialize strategy with parameters
strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"})
# 2. Process data points sequentially
for timestamp, ohlcv in data_stream:
signal = strategy.process_data_point(timestamp, ohlcv)
# 3. Get current state and signals
current_signal = strategy.get_current_signal()
```
### Trader Module
The trader module handles trade execution and position management:
- **Trade Execution**: Converts strategy signals into trades
- **Position Management**: Tracks USD/coin balances and position state
- **Risk Management**: Stop-loss and take-profit handling
- **Performance Tracking**: Real-time performance metrics
#### Trading Workflow
```python
# 1. Create trader with strategy
trader = IncTrader(strategy, initial_usd=10000)
# 2. Process data and execute trades
for timestamp, ohlcv in data_stream:
trader.process_data_point(timestamp, ohlcv)
# 3. Get final results
results = trader.get_results()
```
### Backtester Module
The backtester module provides comprehensive testing capabilities:
- **Single Strategy Testing**: Test individual strategies
- **Parameter Optimization**: Systematic parameter sweeps
- **Multiprocessing**: Parallel execution for faster testing
- **Results Analysis**: Comprehensive performance metrics
#### Backtesting Process
```python
# 1. Configure backtest
config = BacktestConfig(
initial_usd=10000,
stop_loss_pct=0.03,
start_date="2024-01-01",
end_date="2024-12-31"
)
# 2. Run backtest
backtester = IncBacktester()
results = backtester.run_single_strategy(strategy, config)
# 3. Analyze results
performance = results['performance_metrics']
```
## Data Flow
### Real-time Processing
```
Market Data → Strategy → Signal → Trader → Trade Execution
↓ ↓ ↓ ↓ ↓
OHLCV Indicators BUY/SELL Position Portfolio
Data Updates Signals Updates Updates
```
### Backtesting Flow
```
Historical Data → Backtester → Multiple Traders → Results Aggregation
↓ ↓ ↓ ↓
Time Series Strategy Trade Records Performance
OHLCV Instances Collections Metrics
```
## Memory Management
### Incremental Computation
Traditional batch processing recalculates everything for each new data point:
```python
# Batch approach - O(n) memory, O(n) computation
def calculate_sma(prices, period):
return [sum(prices[i:i+period])/period for i in range(len(prices)-period+1)]
```
Incremental approach maintains only necessary state:
```python
# Incremental approach - O(1) memory, O(1) computation
class IncrementalSMA:
def __init__(self, period):
self.period = period
self.values = deque(maxlen=period)
self.sum = 0
def update(self, value):
if len(self.values) == self.period:
self.sum -= self.values[0]
self.values.append(value)
self.sum += value
def get_value(self):
return self.sum / len(self.values) if self.values else 0
```
### Benefits
- **Constant Memory**: Memory usage doesn't grow with data history
- **Fast Updates**: New data points processed in constant time
- **Real-time Capable**: Suitable for live trading applications
- **Scalable**: Performance independent of history length
## Error Handling
### Strategy Level
- Input validation for all parameters
- Graceful handling of missing or invalid data
- Fallback mechanisms for indicator failures
### Trader Level
- Position state validation
- Trade execution error handling
- Balance consistency checks
### System Level
- Comprehensive logging at all levels
- Exception propagation with context
- Recovery mechanisms for transient failures
## Performance Characteristics
### Computational Complexity
| Operation | Batch Approach | Incremental Approach |
|-----------|----------------|---------------------|
| Memory Usage | O(n) | O(1) |
| Update Time | O(n) | O(1) |
| Initialization | O(1) | O(k) where k = warmup period |
### Benchmarks
- **Processing Speed**: ~10x faster than batch recalculation
- **Memory Usage**: ~100x less memory for long histories
- **Latency**: Sub-millisecond processing for new data points
## Extensibility
### Adding New Strategies
1. Inherit from `IncStrategyBase`
2. Implement `process_data_point()` method
3. Return appropriate `IncStrategySignal` objects
4. Register in strategy module
### Adding New Indicators
1. Implement incremental update logic
2. Maintain minimal state for calculations
3. Provide consistent API (update/get_value)
4. Add comprehensive tests
### Integration Points
- **Data Sources**: Easy to connect different data feeds
- **Execution Engines**: Pluggable trade execution backends
- **Risk Management**: Configurable risk management rules
- **Reporting**: Extensible results and analytics framework
## Testing Strategy
### Unit Tests
- Individual component testing
- Mock data for isolated testing
- Edge case validation
### Integration Tests
- End-to-end workflow testing
- Real data validation
- Performance benchmarking
### Accuracy Validation
- Comparison with batch implementations
- Historical data validation
- Signal timing verification
---
This architecture provides a solid foundation for building efficient, scalable, and maintainable trading systems while keeping the complexity manageable and the interfaces clean.

View File

@@ -1,636 +0,0 @@
# Timeframe Aggregation Usage Guide
## Overview
This guide covers how to use the new timeframe aggregation utilities in the IncrementalTrader framework. The new system provides mathematically correct aggregation with proper timestamp handling to prevent future data leakage.
## Key Features
### ✅ **Fixed Critical Issues**
- **No Future Data Leakage**: Bar timestamps represent END of period
- **Mathematical Correctness**: Results match pandas resampling exactly
- **Trading Industry Standard**: Uses standard bar grouping conventions
- **Proper OHLCV Aggregation**: Correct first/max/min/last/sum rules
### 🚀 **New Capabilities**
- **MinuteDataBuffer**: Efficient real-time data management
- **Flexible Timestamp Modes**: Support for both bar start and end timestamps
- **Memory Bounded**: Automatic buffer size management
- **Performance Optimized**: Fast aggregation for real-time use
## Quick Start
### Basic Usage
```python
from IncrementalTrader.utils.timeframe_utils import aggregate_minute_data_to_timeframe
# Sample minute data
minute_data = [
{
'timestamp': pd.Timestamp('2024-01-01 09:00:00'),
'open': 50000.0, 'high': 50050.0, 'low': 49950.0, 'close': 50025.0, 'volume': 1000
},
{
'timestamp': pd.Timestamp('2024-01-01 09:01:00'),
'open': 50025.0, 'high': 50075.0, 'low': 50000.0, 'close': 50050.0, 'volume': 1200
},
# ... more minute data
]
# Aggregate to 15-minute bars
bars_15m = aggregate_minute_data_to_timeframe(minute_data, "15min")
# Result: bars with END timestamps (no future data leakage)
for bar in bars_15m:
print(f"Bar ending at {bar['timestamp']}: OHLCV = {bar['open']}, {bar['high']}, {bar['low']}, {bar['close']}, {bar['volume']}")
```
### Using MinuteDataBuffer for Real-Time Strategies
```python
from IncrementalTrader.utils.timeframe_utils import MinuteDataBuffer
class MyStrategy(IncStrategyBase):
def __init__(self, name: str = "my_strategy", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.timeframe = self.params.get("timeframe", "15min")
self.minute_buffer = MinuteDataBuffer(max_size=1440) # 24 hours
self.last_processed_bar_timestamp = None
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
# Add to buffer
self.minute_buffer.add(timestamp, new_data_point)
# Get latest complete bar
latest_bar = self.minute_buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed_bar_timestamp:
# Process new complete bar
self.last_processed_bar_timestamp = latest_bar['timestamp']
self._process_complete_bar(latest_bar)
def _process_complete_bar(self, bar: Dict[str, float]) -> None:
# Your strategy logic here
# bar['timestamp'] is the END of the bar period (no future data)
pass
```
## Core Functions
### aggregate_minute_data_to_timeframe()
**Purpose**: Aggregate minute-level OHLCV data to higher timeframes
**Signature**:
```python
def aggregate_minute_data_to_timeframe(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> List[Dict[str, Union[float, pd.Timestamp]]]
```
**Parameters**:
- `minute_data`: List of minute OHLCV dictionaries with 'timestamp' field
- `timeframe`: Target timeframe ("1min", "5min", "15min", "1h", "4h", "1d")
- `timestamp_mode`: "end" (default) for bar end timestamps, "start" for bar start
**Returns**: List of aggregated OHLCV dictionaries with proper timestamps
**Example**:
```python
# Aggregate to 5-minute bars with end timestamps
bars_5m = aggregate_minute_data_to_timeframe(minute_data, "5min", "end")
# Aggregate to 1-hour bars with start timestamps
bars_1h = aggregate_minute_data_to_timeframe(minute_data, "1h", "start")
```
### get_latest_complete_bar()
**Purpose**: Get the latest complete bar for real-time processing
**Signature**:
```python
def get_latest_complete_bar(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> Optional[Dict[str, Union[float, pd.Timestamp]]]
```
**Example**:
```python
# Get latest complete 15-minute bar
latest_15m = get_latest_complete_bar(minute_data, "15min")
if latest_15m:
print(f"Latest complete bar: {latest_15m['timestamp']}")
```
### parse_timeframe_to_minutes()
**Purpose**: Parse timeframe strings to minutes
**Signature**:
```python
def parse_timeframe_to_minutes(timeframe: str) -> int
```
**Supported Formats**:
- Minutes: "1min", "5min", "15min", "30min"
- Hours: "1h", "2h", "4h", "6h", "12h"
- Days: "1d", "7d"
- Weeks: "1w", "2w"
**Example**:
```python
minutes = parse_timeframe_to_minutes("15min") # Returns 15
minutes = parse_timeframe_to_minutes("1h") # Returns 60
minutes = parse_timeframe_to_minutes("1d") # Returns 1440
```
## MinuteDataBuffer Class
### Overview
The `MinuteDataBuffer` class provides efficient buffer management for minute-level data with automatic aggregation capabilities.
### Key Features
- **Memory Bounded**: Configurable maximum size (default: 1440 minutes = 24 hours)
- **Automatic Cleanup**: Old data automatically removed when buffer is full
- **Thread Safe**: Safe for use in multi-threaded environments
- **Efficient Access**: Fast data retrieval and aggregation methods
### Basic Usage
```python
from IncrementalTrader.utils.timeframe_utils import MinuteDataBuffer
# Create buffer for 24 hours of data
buffer = MinuteDataBuffer(max_size=1440)
# Add minute data
buffer.add(timestamp, {
'open': 50000.0,
'high': 50050.0,
'low': 49950.0,
'close': 50025.0,
'volume': 1000
})
# Get aggregated data
bars_15m = buffer.aggregate_to_timeframe("15min", lookback_bars=4)
latest_bar = buffer.get_latest_complete_bar("15min")
# Buffer management
print(f"Buffer size: {buffer.size()}")
print(f"Is full: {buffer.is_full()}")
print(f"Time range: {buffer.get_time_range()}")
```
### Methods
#### add(timestamp, ohlcv_data)
Add new minute data point to the buffer.
```python
buffer.add(pd.Timestamp('2024-01-01 09:00:00'), {
'open': 50000.0, 'high': 50050.0, 'low': 49950.0, 'close': 50025.0, 'volume': 1000
})
```
#### get_data(lookback_minutes=None)
Get data from buffer.
```python
# Get all data
all_data = buffer.get_data()
# Get last 60 minutes
recent_data = buffer.get_data(lookback_minutes=60)
```
#### aggregate_to_timeframe(timeframe, lookback_bars=None, timestamp_mode="end")
Aggregate buffer data to specified timeframe.
```python
# Get last 4 bars of 15-minute data
bars = buffer.aggregate_to_timeframe("15min", lookback_bars=4)
# Get all available 1-hour bars
bars = buffer.aggregate_to_timeframe("1h")
```
#### get_latest_complete_bar(timeframe, timestamp_mode="end")
Get the latest complete bar for the specified timeframe.
```python
latest_bar = buffer.get_latest_complete_bar("15min")
if latest_bar:
print(f"Latest complete bar ends at: {latest_bar['timestamp']}")
```
## Timestamp Modes
### "end" Mode (Default - Recommended)
- **Bar timestamps represent the END of the bar period**
- **Prevents future data leakage**
- **Safe for real-time trading**
```python
# 5-minute bar from 09:00-09:04 is timestamped 09:05
bars = aggregate_minute_data_to_timeframe(data, "5min", "end")
```
### "start" Mode
- **Bar timestamps represent the START of the bar period**
- **Matches some external data sources**
- **Use with caution in real-time systems**
```python
# 5-minute bar from 09:00-09:04 is timestamped 09:00
bars = aggregate_minute_data_to_timeframe(data, "5min", "start")
```
## Best Practices
### 1. Always Use "end" Mode for Real-Time Trading
```python
# ✅ GOOD: Prevents future data leakage
bars = aggregate_minute_data_to_timeframe(data, "15min", "end")
# ❌ RISKY: Could lead to future data leakage
bars = aggregate_minute_data_to_timeframe(data, "15min", "start")
```
### 2. Use MinuteDataBuffer for Strategies
```python
# ✅ GOOD: Efficient memory management
class MyStrategy(IncStrategyBase):
def __init__(self, ...):
self.buffer = MinuteDataBuffer(max_size=1440) # 24 hours
def calculate_on_data(self, data, timestamp):
self.buffer.add(timestamp, data)
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
# Process latest_bar...
# ❌ INEFFICIENT: Keeping all data in memory
class BadStrategy(IncStrategyBase):
def __init__(self, ...):
self.all_data = [] # Grows indefinitely
```
### 3. Check for Complete Bars
```python
# ✅ GOOD: Only process complete bars
latest_bar = buffer.get_latest_complete_bar("15min")
if latest_bar and latest_bar['timestamp'] != self.last_processed:
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
# ❌ BAD: Processing incomplete bars
bars = buffer.aggregate_to_timeframe("15min")
if bars:
self.process_bar(bars[-1]) # Might be incomplete!
```
### 4. Handle Edge Cases
```python
# ✅ GOOD: Robust error handling
try:
bars = aggregate_minute_data_to_timeframe(data, timeframe)
if bars:
# Process bars...
else:
logger.warning("No complete bars available")
except TimeframeError as e:
logger.error(f"Invalid timeframe: {e}")
except ValueError as e:
logger.error(f"Invalid data: {e}")
# ❌ BAD: No error handling
bars = aggregate_minute_data_to_timeframe(data, timeframe)
latest_bar = bars[-1] # Could crash if bars is empty!
```
### 5. Optimize Buffer Size
```python
# ✅ GOOD: Size buffer based on strategy needs
# For 15min strategy needing 20 bars lookback: 20 * 15 = 300 minutes
buffer = MinuteDataBuffer(max_size=300)
# For daily strategy: 24 * 60 = 1440 minutes
buffer = MinuteDataBuffer(max_size=1440)
# ❌ WASTEFUL: Oversized buffer
buffer = MinuteDataBuffer(max_size=10080) # 1 week for 15min strategy
```
## Performance Considerations
### Memory Usage
- **MinuteDataBuffer**: ~1KB per minute of data
- **1440 minutes (24h)**: ~1.4MB memory usage
- **Automatic cleanup**: Old data removed when buffer is full
### Processing Speed
- **Small datasets (< 500 minutes)**: < 5ms aggregation time
- **Large datasets (2000+ minutes)**: < 15ms aggregation time
- **Real-time processing**: < 2ms per minute update
### Optimization Tips
1. **Use appropriate buffer sizes** - don't keep more data than needed
2. **Process complete bars only** - avoid reprocessing incomplete bars
3. **Cache aggregated results** - don't re-aggregate the same data
4. **Use lookback_bars parameter** - limit returned data to what you need
```python
# ✅ OPTIMIZED: Only get what you need
recent_bars = buffer.aggregate_to_timeframe("15min", lookback_bars=20)
# ❌ INEFFICIENT: Getting all data every time
all_bars = buffer.aggregate_to_timeframe("15min")
recent_bars = all_bars[-20:] # Wasteful
```
## Common Patterns
### Pattern 1: Simple Strategy with Buffer
```python
class TrendStrategy(IncStrategyBase):
def __init__(self, name: str = "trend", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.timeframe = self.params.get("timeframe", "15min")
self.lookback_period = self.params.get("lookback_period", 20)
# Calculate buffer size: lookback_period * timeframe_minutes
timeframe_minutes = parse_timeframe_to_minutes(self.timeframe)
buffer_size = self.lookback_period * timeframe_minutes
self.buffer = MinuteDataBuffer(max_size=buffer_size)
self.last_processed_timestamp = None
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
# Add to buffer
self.buffer.add(timestamp, new_data_point)
# Get latest complete bar
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed_timestamp:
# Get historical bars for analysis
historical_bars = self.buffer.aggregate_to_timeframe(
self.timeframe,
lookback_bars=self.lookback_period
)
if len(historical_bars) >= self.lookback_period:
signal = self._analyze_trend(historical_bars)
if signal:
self._generate_signal(signal, latest_bar['timestamp'])
self.last_processed_timestamp = latest_bar['timestamp']
def _analyze_trend(self, bars: List[Dict]) -> Optional[str]:
# Your trend analysis logic here
closes = [bar['close'] for bar in bars]
# ... analysis ...
return "BUY" if trend_up else "SELL" if trend_down else None
```
### Pattern 2: Multi-Timeframe Strategy
```python
class MultiTimeframeStrategy(IncStrategyBase):
def __init__(self, name: str = "multi_tf", weight: float = 1.0, params: Optional[Dict] = None):
super().__init__(name, weight, params)
self.primary_timeframe = self.params.get("primary_timeframe", "15min")
self.secondary_timeframe = self.params.get("secondary_timeframe", "1h")
# Buffer size for the largest timeframe needed
max_timeframe_minutes = max(
parse_timeframe_to_minutes(self.primary_timeframe),
parse_timeframe_to_minutes(self.secondary_timeframe)
)
buffer_size = 50 * max_timeframe_minutes # 50 bars of largest timeframe
self.buffer = MinuteDataBuffer(max_size=buffer_size)
self.last_processed = {
self.primary_timeframe: None,
self.secondary_timeframe: None
}
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
self.buffer.add(timestamp, new_data_point)
# Check both timeframes
for timeframe in [self.primary_timeframe, self.secondary_timeframe]:
latest_bar = self.buffer.get_latest_complete_bar(timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed[timeframe]:
self._process_timeframe(timeframe, latest_bar)
self.last_processed[timeframe] = latest_bar['timestamp']
def _process_timeframe(self, timeframe: str, latest_bar: Dict) -> None:
if timeframe == self.primary_timeframe:
# Primary timeframe logic
pass
elif timeframe == self.secondary_timeframe:
# Secondary timeframe logic
pass
```
### Pattern 3: Backtesting with Historical Data
```python
def backtest_strategy(strategy_class, historical_data: List[Dict], params: Dict):
"""Run backtest with historical minute data."""
strategy = strategy_class("backtest", params=params)
signals = []
# Process data chronologically
for data_point in historical_data:
timestamp = data_point['timestamp']
ohlcv = {k: v for k, v in data_point.items() if k != 'timestamp'}
# Process data point
signal = strategy.process_data_point(timestamp, ohlcv)
if signal and signal.signal_type != "HOLD":
signals.append({
'timestamp': timestamp,
'signal_type': signal.signal_type,
'confidence': signal.confidence
})
return signals
# Usage
historical_data = load_historical_data("BTCUSD", "2024-01-01", "2024-01-31")
signals = backtest_strategy(TrendStrategy, historical_data, {"timeframe": "15min"})
```
## Error Handling
### Common Errors and Solutions
#### TimeframeError
```python
try:
bars = aggregate_minute_data_to_timeframe(data, "invalid_timeframe")
except TimeframeError as e:
logger.error(f"Invalid timeframe: {e}")
# Use default timeframe
bars = aggregate_minute_data_to_timeframe(data, "15min")
```
#### ValueError (Invalid Data)
```python
try:
buffer.add(timestamp, ohlcv_data)
except ValueError as e:
logger.error(f"Invalid data: {e}")
# Skip this data point
continue
```
#### Empty Data
```python
bars = aggregate_minute_data_to_timeframe(minute_data, "15min")
if not bars:
logger.warning("No complete bars available")
return
latest_bar = get_latest_complete_bar(minute_data, "15min")
if latest_bar is None:
logger.warning("No complete bar available")
return
```
## Migration from Old System
### Before (Old TimeframeAggregator)
```python
# Old approach - potential future data leakage
class OldStrategy(IncStrategyBase):
def __init__(self, ...):
self.aggregator = TimeframeAggregator(timeframe="15min")
def calculate_on_data(self, data, timestamp):
# Potential issues:
# - Bar timestamps might represent start (future data leakage)
# - Inconsistent aggregation logic
# - Memory not bounded
pass
```
### After (New Utilities)
```python
# New approach - safe and efficient
class NewStrategy(IncStrategyBase):
def __init__(self, ...):
self.buffer = MinuteDataBuffer(max_size=1440)
self.timeframe = "15min"
self.last_processed = None
def calculate_on_data(self, data, timestamp):
self.buffer.add(timestamp, data)
latest_bar = self.buffer.get_latest_complete_bar(self.timeframe)
if latest_bar and latest_bar['timestamp'] != self.last_processed:
# Safe: bar timestamp is END of period (no future data)
# Efficient: bounded memory usage
# Correct: matches pandas resampling
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
```
### Migration Checklist
- [ ] Replace `TimeframeAggregator` with `MinuteDataBuffer`
- [ ] Update timestamp handling to use "end" mode
- [ ] Add checks for complete bars only
- [ ] Set appropriate buffer sizes
- [ ] Update error handling
- [ ] Test with historical data
- [ ] Verify no future data leakage
## Troubleshooting
### Issue: No bars returned
**Cause**: Not enough data for complete bars
**Solution**: Check data length vs timeframe requirements
```python
timeframe_minutes = parse_timeframe_to_minutes("15min") # 15
if len(minute_data) < timeframe_minutes:
logger.warning(f"Need at least {timeframe_minutes} minutes for {timeframe} bars")
```
### Issue: Memory usage growing
**Cause**: Buffer size too large or not using buffer
**Solution**: Optimize buffer size
```python
# Calculate optimal buffer size
lookback_bars = 20
timeframe_minutes = parse_timeframe_to_minutes("15min")
optimal_size = lookback_bars * timeframe_minutes # 300 minutes
buffer = MinuteDataBuffer(max_size=optimal_size)
```
### Issue: Signals generated too frequently
**Cause**: Processing incomplete bars
**Solution**: Only process complete bars
```python
# ✅ CORRECT: Only process new complete bars
if latest_bar and latest_bar['timestamp'] != self.last_processed:
self.process_bar(latest_bar)
self.last_processed = latest_bar['timestamp']
# ❌ WRONG: Processing every minute
self.process_bar(latest_bar) # Processes same bar multiple times
```
### Issue: Inconsistent results
**Cause**: Using "start" mode or wrong pandas comparison
**Solution**: Use "end" mode and trading standard comparison
```python
# ✅ CORRECT: Trading standard with end timestamps
bars = aggregate_minute_data_to_timeframe(data, "15min", "end")
# ❌ INCONSISTENT: Start mode can cause confusion
bars = aggregate_minute_data_to_timeframe(data, "15min", "start")
```
---
## Summary
The new timeframe aggregation system provides:
- **✅ Mathematical Correctness**: Matches pandas resampling exactly
- **✅ No Future Data Leakage**: Bar end timestamps prevent future data usage
- **✅ Trading Industry Standard**: Compatible with major trading platforms
- **✅ Memory Efficient**: Bounded buffer management
- **✅ Performance Optimized**: Fast real-time processing
- **✅ Easy to Use**: Simple, intuitive API
Use this guide to implement robust, efficient timeframe aggregation in your trading strategies!

View File

@@ -1,59 +0,0 @@
"""
Incremental Trading Strategies Framework
This module provides the strategy framework and implementations for incremental trading.
All strategies inherit from IncStrategyBase and support real-time data processing
with constant memory usage.
Available Components:
- Base Framework: IncStrategyBase, IncStrategySignal, TimeframeAggregator
- Strategies: MetaTrendStrategy, RandomStrategy, BBRSStrategy
- Indicators: Complete indicator framework in .indicators submodule
Example:
from IncrementalTrader.strategies import MetaTrendStrategy, IncStrategySignal
# Create strategy
strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"})
# Process data
strategy.process_data_point(timestamp, ohlcv_data)
# Get signals
entry_signal = strategy.get_entry_signal()
if entry_signal.action == "BUY":
print(f"Entry signal with confidence: {entry_signal.confidence}")
"""
# Base strategy framework (already migrated)
from .base import (
IncStrategyBase,
IncStrategySignal,
TimeframeAggregator,
)
# Migrated strategies
from .metatrend import MetaTrendStrategy, IncMetaTrendStrategy
from .random import RandomStrategy, IncRandomStrategy
from .bbrs import BBRSStrategy, IncBBRSStrategy
# Indicators submodule
from . import indicators
__all__ = [
# Base framework
"IncStrategyBase",
"IncStrategySignal",
"TimeframeAggregator",
# Available strategies
"MetaTrendStrategy",
"IncMetaTrendStrategy", # Compatibility alias
"RandomStrategy",
"IncRandomStrategy", # Compatibility alias
"BBRSStrategy",
"IncBBRSStrategy", # Compatibility alias
# Indicators submodule
"indicators",
]

View File

@@ -1,690 +0,0 @@
"""
Base classes for the incremental strategy system.
This module contains the fundamental building blocks for all incremental trading strategies:
- IncStrategySignal: Represents trading signals with confidence and metadata
- IncStrategyBase: Abstract base class that all incremental strategies must inherit from
- TimeframeAggregator: Built-in timeframe aggregation for minute-level data processing
The incremental approach allows strategies to:
- Process new data points without full recalculation
- Maintain bounded memory usage regardless of data history length
- Provide real-time performance with minimal latency
- Support both initialization and incremental modes
- Accept minute-level data and internally aggregate to any timeframe
"""
import pandas as pd
from abc import ABC, abstractmethod
from typing import Dict, Optional, List, Union, Any
from collections import deque
import logging
import time
# Import new timeframe utilities
from ..utils.timeframe_utils import (
aggregate_minute_data_to_timeframe,
parse_timeframe_to_minutes,
get_latest_complete_bar,
MinuteDataBuffer,
TimeframeError
)
logger = logging.getLogger(__name__)
class IncStrategySignal:
"""
Represents a trading signal from an incremental strategy.
A signal encapsulates the strategy's recommendation along with confidence
level, optional price target, and additional metadata.
Attributes:
signal_type (str): Type of signal - "ENTRY", "EXIT", or "HOLD"
confidence (float): Confidence level from 0.0 to 1.0
price (Optional[float]): Optional specific price for the signal
metadata (Dict): Additional signal data and context
Example:
# Entry signal with high confidence
signal = IncStrategySignal("ENTRY", confidence=0.8)
# Exit signal with stop loss price
signal = IncStrategySignal("EXIT", confidence=1.0, price=50000,
metadata={"type": "STOP_LOSS"})
"""
def __init__(self, signal_type: str, confidence: float = 1.0,
price: Optional[float] = None, metadata: Optional[Dict] = None):
"""
Initialize a strategy signal.
Args:
signal_type: Type of signal ("ENTRY", "EXIT", "HOLD")
confidence: Confidence level (0.0 to 1.0)
price: Optional specific price for the signal
metadata: Additional signal data and context
"""
self.signal_type = signal_type
self.confidence = max(0.0, min(1.0, confidence)) # Clamp to [0,1]
self.price = price
self.metadata = metadata or {}
@classmethod
def BUY(cls, confidence: float = 1.0, price: Optional[float] = None, **metadata):
"""Create a BUY signal."""
return cls("ENTRY", confidence, price, metadata)
@classmethod
def SELL(cls, confidence: float = 1.0, price: Optional[float] = None, **metadata):
"""Create a SELL signal."""
return cls("EXIT", confidence, price, metadata)
@classmethod
def HOLD(cls, confidence: float = 0.0, **metadata):
"""Create a HOLD signal."""
return cls("HOLD", confidence, None, metadata)
def __repr__(self) -> str:
"""String representation of the signal."""
return (f"IncStrategySignal(type={self.signal_type}, "
f"confidence={self.confidence:.2f}, "
f"price={self.price}, metadata={self.metadata})")
class TimeframeAggregator:
"""
Handles real-time aggregation of minute data to higher timeframes.
This class accumulates minute-level OHLCV data and produces complete
bars when a timeframe period is completed. Now uses the new timeframe
utilities for mathematically correct aggregation that matches pandas
resampling behavior.
Key improvements:
- Uses bar END timestamps (prevents future data leakage)
- Proper OHLCV aggregation (first/max/min/last/sum)
- Mathematical equivalence to pandas resampling
- Memory-efficient buffer management
"""
def __init__(self, timeframe: str = "15min", max_buffer_size: int = 1440):
"""
Initialize timeframe aggregator.
Args:
timeframe: Target timeframe string (e.g., "15min", "1h", "4h")
max_buffer_size: Maximum minute data buffer size (default: 1440 = 24h)
"""
self.timeframe = timeframe
self.timeframe_minutes = parse_timeframe_to_minutes(timeframe)
# Use MinuteDataBuffer for efficient minute data management
self.minute_buffer = MinuteDataBuffer(max_size=max_buffer_size)
# Track last processed bar to avoid reprocessing
self.last_processed_bar_timestamp = None
# Performance tracking
self._bars_completed = 0
self._minute_points_processed = 0
def update(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, float]]:
"""
Update with new minute data and return completed bar if timeframe is complete.
Args:
timestamp: Timestamp of the minute data
ohlcv_data: OHLCV data dictionary
Returns:
Completed OHLCV bar if timeframe period ended, None otherwise
"""
try:
# Add minute data to buffer
self.minute_buffer.add(timestamp, ohlcv_data)
self._minute_points_processed += 1
# Get latest complete bar using new utilities
latest_bar = get_latest_complete_bar(
self.minute_buffer.get_data(),
self.timeframe
)
if latest_bar is None:
return None
# Check if this is a new bar (avoid reprocessing)
bar_timestamp = latest_bar['timestamp']
if self.last_processed_bar_timestamp == bar_timestamp:
return None # Already processed this bar
# Update tracking
self.last_processed_bar_timestamp = bar_timestamp
self._bars_completed += 1
return latest_bar
except TimeframeError as e:
logger.error(f"Timeframe aggregation error: {e}")
return None
except Exception as e:
logger.error(f"Unexpected error in timeframe aggregation: {e}")
return None
def get_current_bar(self) -> Optional[Dict[str, float]]:
"""
Get the current incomplete bar (for debugging).
Returns:
Current incomplete bar data or None
"""
try:
# Get recent data and try to aggregate
recent_data = self.minute_buffer.get_data(lookback_minutes=self.timeframe_minutes)
if not recent_data:
return None
# Aggregate to get current (possibly incomplete) bar
bars = aggregate_minute_data_to_timeframe(recent_data, self.timeframe, "end")
if bars:
return bars[-1] # Return most recent bar
return None
except Exception as e:
logger.debug(f"Error getting current bar: {e}")
return None
def reset(self):
"""Reset aggregator state."""
self.minute_buffer = MinuteDataBuffer(max_size=self.minute_buffer.max_size)
self.last_processed_bar_timestamp = None
self._bars_completed = 0
self._minute_points_processed = 0
def get_stats(self) -> Dict[str, Any]:
"""Get aggregator statistics."""
return {
'timeframe': self.timeframe,
'timeframe_minutes': self.timeframe_minutes,
'minute_points_processed': self._minute_points_processed,
'bars_completed': self._bars_completed,
'buffer_size': len(self.minute_buffer.get_data()),
'last_processed_bar': self.last_processed_bar_timestamp
}
class IncStrategyBase(ABC):
"""
Abstract base class for all incremental trading strategies.
This class defines the interface that all incremental strategies must implement:
- get_minimum_buffer_size(): Specify minimum data requirements
- process_data_point(): Process new data points incrementally
- supports_incremental_calculation(): Whether strategy supports incremental mode
- get_entry_signal(): Generate entry signals
- get_exit_signal(): Generate exit signals
The incremental approach allows strategies to:
- Process new data points without full recalculation
- Maintain bounded memory usage regardless of data history length
- Provide real-time performance with minimal latency
- Support both initialization and incremental modes
- Accept minute-level data and internally aggregate to any timeframe
New Features:
- Built-in TimeframeAggregator for minute-level data processing
- update_minute_data() method for real-time trading systems
- Automatic timeframe detection and aggregation
- Backward compatibility with existing update() methods
Attributes:
name (str): Strategy name
weight (float): Strategy weight for combination
params (Dict): Strategy parameters
calculation_mode (str): Current mode ('initialization' or 'incremental')
is_warmed_up (bool): Whether strategy has sufficient data for reliable signals
timeframe_buffers (Dict): Rolling buffers for different timeframes
indicator_states (Dict): Internal indicator calculation states
timeframe_aggregator (TimeframeAggregator): Built-in aggregator for minute data
Example:
class MyIncStrategy(IncStrategyBase):
def get_minimum_buffer_size(self):
return {"15min": 50} # Strategy works on 15min timeframe
def process_data_point(self, timestamp, ohlcv_data):
# Process new data incrementally
self._update_indicators(ohlcv_data)
return self.get_current_signal()
def get_entry_signal(self):
# Generate signal based on current state
if self._should_enter():
return IncStrategySignal.BUY(confidence=0.8)
return IncStrategySignal.HOLD()
# Usage with minute-level data:
strategy = MyIncStrategy(params={"timeframe_minutes": 15})
for minute_data in live_stream:
signal = strategy.process_data_point(minute_data['timestamp'], minute_data)
"""
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
"""
Initialize the incremental strategy base.
Args:
name: Strategy name/identifier
weight: Strategy weight for combination (default: 1.0)
params: Strategy-specific parameters
"""
self.name = name
self.weight = weight
self.params = params or {}
# Calculation state
self._calculation_mode = "initialization"
self._is_warmed_up = False
self._data_points_received = 0
# Data management
self._timeframe_buffers = {}
self._timeframe_last_update = {}
self._indicator_states = {}
self._last_signals = {}
self._signal_history = deque(maxlen=100) # Keep last 100 signals
# Performance tracking
self._performance_metrics = {
'update_times': deque(maxlen=1000),
'signal_generation_times': deque(maxlen=1000),
'state_validation_failures': 0,
'data_gaps_handled': 0,
'minute_data_points_processed': 0,
'timeframe_bars_completed': 0
}
# Configuration
self._buffer_size_multiplier = 1.5 # Extra buffer for safety
self._state_validation_enabled = True
self._max_acceptable_gap = pd.Timedelta(minutes=5)
# Timeframe aggregation - Updated to use new utilities
self._primary_timeframe = self.params.get("timeframe", "1min")
self._timeframe_aggregator = None
# Only create aggregator if timeframe is not 1min (minute data processing)
if self._primary_timeframe != "1min":
try:
self._timeframe_aggregator = TimeframeAggregator(
timeframe=self._primary_timeframe,
max_buffer_size=1440 # 24 hours of minute data
)
logger.info(f"Created timeframe aggregator for {self._primary_timeframe}")
except TimeframeError as e:
logger.error(f"Failed to create timeframe aggregator: {e}")
self._timeframe_aggregator = None
logger.info(f"Initialized incremental strategy: {self.name} (timeframe: {self._primary_timeframe})")
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[IncStrategySignal]:
"""
Process a new data point and return signal if generated.
This is the main entry point for incremental processing. It handles
timeframe aggregation, buffer updates, and signal generation.
Args:
timestamp: Timestamp of the data point
ohlcv_data: OHLCV data dictionary
Returns:
IncStrategySignal if a signal is generated, None otherwise
"""
start_time = time.time()
try:
# Update performance metrics
self._performance_metrics['minute_data_points_processed'] += 1
self._data_points_received += 1
# Handle timeframe aggregation if needed
if self._timeframe_aggregator is not None:
completed_bar = self._timeframe_aggregator.update(timestamp, ohlcv_data)
if completed_bar is not None:
# Process the completed timeframe bar
self._performance_metrics['timeframe_bars_completed'] += 1
return self._process_timeframe_bar(completed_bar['timestamp'], completed_bar)
else:
# No complete bar yet, return None
return None
else:
# Process minute data directly
return self._process_timeframe_bar(timestamp, ohlcv_data)
except Exception as e:
logger.error(f"Error processing data point in {self.name}: {e}")
return None
finally:
# Track processing time
processing_time = time.time() - start_time
self._performance_metrics['update_times'].append(processing_time)
def _process_timeframe_bar(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[IncStrategySignal]:
"""Process a complete timeframe bar and generate signals."""
# Update timeframe buffers
self._update_timeframe_buffers(ohlcv_data, timestamp)
# Call strategy-specific calculation
self.calculate_on_data(ohlcv_data, timestamp)
# Check if strategy is warmed up
if not self._is_warmed_up:
self._check_warmup_status()
# Generate signal if warmed up
if self._is_warmed_up:
signal_start = time.time()
signal = self.get_current_signal()
signal_time = time.time() - signal_start
self._performance_metrics['signal_generation_times'].append(signal_time)
# Store signal in history
if signal and signal.signal_type != "HOLD":
self._signal_history.append({
'timestamp': timestamp,
'signal': signal,
'strategy_state': self.get_current_state_summary()
})
return signal
return None
def _check_warmup_status(self):
"""Check if strategy has enough data to be considered warmed up."""
min_buffer_sizes = self.get_minimum_buffer_size()
for timeframe, min_size in min_buffer_sizes.items():
buffer = self._timeframe_buffers.get(timeframe, deque())
if len(buffer) < min_size:
return # Not enough data yet
# All buffers have sufficient data
self._is_warmed_up = True
self._calculation_mode = "incremental"
logger.info(f"Strategy {self.name} is now warmed up after {self._data_points_received} data points")
def get_current_signal(self) -> IncStrategySignal:
"""Get the current signal based on strategy state."""
# Try entry signal first
entry_signal = self.get_entry_signal()
if entry_signal and entry_signal.signal_type != "HOLD":
return entry_signal
# Check exit signal
exit_signal = self.get_exit_signal()
if exit_signal and exit_signal.signal_type != "HOLD":
return exit_signal
# Default to hold
return IncStrategySignal.HOLD()
def get_current_incomplete_bar(self) -> Optional[Dict[str, float]]:
"""Get current incomplete timeframe bar (for debugging)."""
if self._timeframe_aggregator is not None:
return self._timeframe_aggregator.get_current_bar()
return None
def get_timeframe_aggregator_stats(self) -> Optional[Dict[str, Any]]:
"""Get timeframe aggregator statistics."""
if self._timeframe_aggregator is not None:
return self._timeframe_aggregator.get_stats()
return None
def create_minute_data_buffer(self, max_size: int = 1440) -> MinuteDataBuffer:
"""
Create a MinuteDataBuffer for strategies that need direct minute data management.
Args:
max_size: Maximum buffer size in minutes (default: 1440 = 24h)
Returns:
MinuteDataBuffer instance
"""
return MinuteDataBuffer(max_size=max_size)
def aggregate_minute_data(self, minute_data: List[Dict[str, float]],
timeframe: str, timestamp_mode: str = "end") -> List[Dict[str, float]]:
"""
Helper method to aggregate minute data to specified timeframe.
Args:
minute_data: List of minute OHLCV data
timeframe: Target timeframe (e.g., "5min", "15min", "1h")
timestamp_mode: "end" (default) or "start" for bar timestamps
Returns:
List of aggregated OHLCV bars
"""
try:
return aggregate_minute_data_to_timeframe(minute_data, timeframe, timestamp_mode)
except TimeframeError as e:
logger.error(f"Error aggregating minute data in {self.name}: {e}")
return []
# Properties
@property
def calculation_mode(self) -> str:
"""Get current calculation mode."""
return self._calculation_mode
@property
def is_warmed_up(self) -> bool:
"""Check if strategy is warmed up."""
return self._is_warmed_up
# Abstract methods that must be implemented by strategies
@abstractmethod
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Get minimum buffer sizes for each timeframe.
This method specifies how much historical data the strategy needs
for each timeframe to generate reliable signals.
Returns:
Dict[str, int]: Mapping of timeframe to minimum buffer size
Example:
return {"15min": 50, "1h": 24} # 50 15min bars, 24 1h bars
"""
pass
@abstractmethod
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process new data point and update internal indicators.
This method is called for each new timeframe bar and should update
all internal indicators and strategy state incrementally.
Args:
new_data_point: New OHLCV data point
timestamp: Timestamp of the data point
"""
pass
@abstractmethod
def supports_incremental_calculation(self) -> bool:
"""
Check if strategy supports incremental calculation.
Returns:
bool: True if strategy can process data incrementally
"""
pass
@abstractmethod
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate entry signal based on current strategy state.
This method should use the current internal state to determine
whether an entry signal should be generated.
Returns:
IncStrategySignal: Entry signal with confidence level
"""
pass
@abstractmethod
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate exit signal based on current strategy state.
This method should use the current internal state to determine
whether an exit signal should be generated.
Returns:
IncStrategySignal: Exit signal with confidence level
"""
pass
# Utility methods
def get_confidence(self) -> float:
"""
Get strategy confidence for the current market state.
Default implementation returns 1.0. Strategies can override
this to provide dynamic confidence based on market conditions.
Returns:
float: Confidence level (0.0 to 1.0)
"""
return 1.0
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
self._calculation_mode = "initialization"
self._is_warmed_up = False
self._data_points_received = 0
self._timeframe_buffers.clear()
self._timeframe_last_update.clear()
self._indicator_states.clear()
self._last_signals.clear()
self._signal_history.clear()
# Reset timeframe aggregator
if self._timeframe_aggregator is not None:
self._timeframe_aggregator.reset()
# Reset performance metrics
for key in self._performance_metrics:
if isinstance(self._performance_metrics[key], deque):
self._performance_metrics[key].clear()
else:
self._performance_metrics[key] = 0
def get_current_state_summary(self) -> Dict[str, Any]:
"""Get summary of current calculation state for debugging."""
return {
'strategy_name': self.name,
'calculation_mode': self._calculation_mode,
'is_warmed_up': self._is_warmed_up,
'data_points_received': self._data_points_received,
'timeframes': list(self._timeframe_buffers.keys()),
'buffer_sizes': {tf: len(buf) for tf, buf in self._timeframe_buffers.items()},
'indicator_states': {name: state.get_state_summary() if hasattr(state, 'get_state_summary') else str(state)
for name, state in self._indicator_states.items()},
'last_signals': self._last_signals,
'timeframe_aggregator': {
'enabled': self._timeframe_aggregator is not None,
'primary_timeframe': self._primary_timeframe,
'current_incomplete_bar': self.get_current_incomplete_bar()
},
'performance_metrics': {
'avg_update_time': sum(self._performance_metrics['update_times']) / len(self._performance_metrics['update_times'])
if self._performance_metrics['update_times'] else 0,
'avg_signal_time': sum(self._performance_metrics['signal_generation_times']) / len(self._performance_metrics['signal_generation_times'])
if self._performance_metrics['signal_generation_times'] else 0,
'validation_failures': self._performance_metrics['state_validation_failures'],
'data_gaps_handled': self._performance_metrics['data_gaps_handled'],
'minute_data_points_processed': self._performance_metrics['minute_data_points_processed'],
'timeframe_bars_completed': self._performance_metrics['timeframe_bars_completed']
}
}
def _update_timeframe_buffers(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""Update all timeframe buffers with new data point."""
# Get minimum buffer sizes
min_buffer_sizes = self.get_minimum_buffer_size()
for timeframe in min_buffer_sizes.keys():
# Calculate actual buffer size with multiplier
min_size = min_buffer_sizes[timeframe]
actual_buffer_size = int(min_size * self._buffer_size_multiplier)
# Initialize buffer if needed
if timeframe not in self._timeframe_buffers:
self._timeframe_buffers[timeframe] = deque(maxlen=actual_buffer_size)
self._timeframe_last_update[timeframe] = None
# Add data point to buffer
data_point = new_data_point.copy()
data_point['timestamp'] = timestamp
self._timeframe_buffers[timeframe].append(data_point)
self._timeframe_last_update[timeframe] = timestamp
def _get_timeframe_buffer(self, timeframe: str) -> pd.DataFrame:
"""Get current buffer for specific timeframe as DataFrame."""
if timeframe not in self._timeframe_buffers:
return pd.DataFrame()
buffer_data = list(self._timeframe_buffers[timeframe])
if not buffer_data:
return pd.DataFrame()
df = pd.DataFrame(buffer_data)
if 'timestamp' in df.columns:
df = df.set_index('timestamp')
return df
def handle_data_gap(self, gap_duration: pd.Timedelta) -> None:
"""Handle gaps in data stream."""
self._performance_metrics['data_gaps_handled'] += 1
if gap_duration > self._max_acceptable_gap:
logger.warning(f"Data gap {gap_duration} exceeds maximum acceptable gap {self._max_acceptable_gap}")
self._trigger_reinitialization()
else:
logger.info(f"Handling acceptable data gap: {gap_duration}")
# For small gaps, continue with current state
def _trigger_reinitialization(self) -> None:
"""Trigger strategy reinitialization due to data gap or corruption."""
logger.info(f"Triggering reinitialization for strategy {self.name}")
self.reset_calculation_state()
# Compatibility methods for original strategy interface
def get_timeframes(self) -> List[str]:
"""Get required timeframes (compatibility method)."""
return list(self.get_minimum_buffer_size().keys())
def initialize(self, backtester) -> None:
"""Initialize strategy (compatibility method)."""
# This method provides compatibility with the original strategy interface
# The actual initialization happens through the incremental interface
self.initialized = True
logger.info(f"Incremental strategy {self.name} initialized in compatibility mode")
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"{self.__class__.__name__}(name={self.name}, "
f"weight={self.weight}, mode={self._calculation_mode}, "
f"warmed_up={self._is_warmed_up}, "
f"data_points={self._data_points_received})")

View File

@@ -1,517 +0,0 @@
"""
Incremental BBRS Strategy (Bollinger Bands + RSI Strategy)
This module implements an incremental version of the Bollinger Bands + RSI Strategy (BBRS)
for real-time data processing. It maintains constant memory usage and provides
identical results to the batch implementation after the warm-up period.
Key Features:
- Accepts minute-level data input for real-time compatibility
- Internal timeframe aggregation (1min, 5min, 15min, 1h, etc.)
- Incremental Bollinger Bands calculation
- Incremental RSI calculation with Wilder's smoothing
- Market regime detection (trending vs sideways)
- Real-time signal generation
- Constant memory usage
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any, Tuple, Union
import logging
from collections import deque
from .base import IncStrategyBase, IncStrategySignal
from .indicators.bollinger_bands import BollingerBandsState
from .indicators.rsi import RSIState
logger = logging.getLogger(__name__)
class BBRSStrategy(IncStrategyBase):
"""
Incremental BBRS (Bollinger Bands + RSI) strategy implementation.
This strategy combines Bollinger Bands and RSI indicators to detect market
conditions and generate trading signals. It adapts its behavior based on
market regime detection (trending vs sideways markets).
The strategy uses different Bollinger Band multipliers and RSI thresholds
for different market regimes:
- Trending markets: Breakout strategy with higher BB multiplier
- Sideways markets: Mean reversion strategy with lower BB multiplier
Parameters:
timeframe (str): Primary timeframe for analysis (default: "1h")
bb_period (int): Bollinger Bands period (default: 20)
rsi_period (int): RSI period (default: 14)
bb_width_threshold (float): BB width threshold for regime detection (default: 0.05)
trending_bb_multiplier (float): BB multiplier for trending markets (default: 2.5)
sideways_bb_multiplier (float): BB multiplier for sideways markets (default: 1.8)
trending_rsi_thresholds (list): RSI thresholds for trending markets (default: [30, 70])
sideways_rsi_thresholds (list): RSI thresholds for sideways markets (default: [40, 60])
squeeze_strategy (bool): Enable squeeze strategy (default: True)
enable_logging (bool): Enable detailed logging (default: False)
Example:
strategy = BBRSStrategy("bbrs", weight=1.0, params={
"timeframe": "1h",
"bb_period": 20,
"rsi_period": 14,
"bb_width_threshold": 0.05,
"trending_bb_multiplier": 2.5,
"sideways_bb_multiplier": 1.8,
"trending_rsi_thresholds": [30, 70],
"sideways_rsi_thresholds": [40, 60],
"squeeze_strategy": True
})
"""
def __init__(self, name: str = "bbrs", weight: float = 1.0, params: Optional[Dict] = None):
"""Initialize the incremental BBRS strategy."""
super().__init__(name, weight, params)
# Strategy configuration
self.primary_timeframe = self.params.get("timeframe", "1h")
self.bb_period = self.params.get("bb_period", 20)
self.rsi_period = self.params.get("rsi_period", 14)
self.bb_width_threshold = self.params.get("bb_width_threshold", 0.05)
# Market regime specific parameters
self.trending_bb_multiplier = self.params.get("trending_bb_multiplier", 2.5)
self.sideways_bb_multiplier = self.params.get("sideways_bb_multiplier", 1.8)
self.trending_rsi_thresholds = tuple(self.params.get("trending_rsi_thresholds", [30, 70]))
self.sideways_rsi_thresholds = tuple(self.params.get("sideways_rsi_thresholds", [40, 60]))
self.squeeze_strategy = self.params.get("squeeze_strategy", True)
self.enable_logging = self.params.get("enable_logging", False)
# Configure logging level
if self.enable_logging:
logger.setLevel(logging.DEBUG)
# Initialize indicators with different multipliers for regime detection
self.bb_trending = BollingerBandsState(self.bb_period, self.trending_bb_multiplier)
self.bb_sideways = BollingerBandsState(self.bb_period, self.sideways_bb_multiplier)
self.bb_reference = BollingerBandsState(self.bb_period, 2.0) # For regime detection
self.rsi = RSIState(self.rsi_period)
# Volume tracking for volume analysis
self.volume_history = deque(maxlen=20) # 20-period volume MA
self.volume_sum = 0.0
self.volume_ma = None
# Strategy state
self.current_price = None
self.current_volume = None
self.current_market_regime = "trending" # Default to trending
self.last_bb_result = None
self.last_rsi_value = None
# Signal generation state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Performance tracking
self._update_count = 0
self._last_update_time = None
logger.info(f"BBRSStrategy initialized: timeframe={self.primary_timeframe}, "
f"bb_period={self.bb_period}, rsi_period={self.rsi_period}, "
f"aggregation_enabled={self._timeframe_aggregator is not None}")
if self.enable_logging:
logger.info(f"Using new timeframe utilities with mathematically correct aggregation")
logger.info(f"Volume aggregation now uses proper sum() for accurate volume spike detection")
if self._timeframe_aggregator:
stats = self.get_timeframe_aggregator_stats()
logger.debug(f"Timeframe aggregator stats: {stats}")
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for reliable BBRS calculations.
Returns:
Dict[str, int]: {timeframe: min_points} mapping
"""
# Need enough data for BB, RSI, and volume MA
min_buffer_size = max(self.bb_period, self.rsi_period, 20) * 2 + 10
return {self.primary_timeframe: min_buffer_size}
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
try:
self._update_count += 1
self._last_update_time = timestamp
if self.enable_logging:
logger.debug(f"Processing data point {self._update_count} at {timestamp}")
close_price = float(new_data_point['close'])
volume = float(new_data_point['volume'])
# Update indicators
bb_trending_result = self.bb_trending.update(close_price)
bb_sideways_result = self.bb_sideways.update(close_price)
bb_reference_result = self.bb_reference.update(close_price)
rsi_value = self.rsi.update(close_price)
# Update volume tracking
self._update_volume_tracking(volume)
# Determine market regime
self.current_market_regime = self._determine_market_regime(bb_reference_result)
# Select appropriate BB values based on regime
if self.current_market_regime == "sideways":
self.last_bb_result = bb_sideways_result
else: # trending
self.last_bb_result = bb_trending_result
# Store current state
self.current_price = close_price
self.current_volume = volume
self.last_rsi_value = rsi_value
self._data_points_received += 1
# Update warm-up status
if not self._is_warmed_up and self.is_warmed_up():
self._is_warmed_up = True
logger.info(f"BBRSStrategy warmed up after {self._update_count} data points")
if self.enable_logging and self._update_count % 10 == 0:
logger.debug(f"BBRS state: price=${close_price:.2f}, "
f"regime={self.current_market_regime}, "
f"rsi={rsi_value:.1f}, "
f"bb_width={bb_reference_result.get('bandwidth', 0):.4f}")
except Exception as e:
logger.error(f"Error in calculate_on_data: {e}")
raise
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Returns:
bool: True (this strategy is fully incremental)
"""
return True
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate entry signal based on BBRS strategy logic.
Returns:
IncStrategySignal: Entry signal if conditions are met, hold signal otherwise
"""
if not self.is_warmed_up():
return IncStrategySignal.HOLD()
# Check for entry condition
if self._check_entry_condition():
self._signal_count["entry"] += 1
self._last_entry_signal = {
'timestamp': self._last_update_time,
'price': self.current_price,
'market_regime': self.current_market_regime,
'rsi': self.last_rsi_value,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"ENTRY SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['entry']})")
return IncStrategySignal.BUY(confidence=1.0, metadata={
"market_regime": self.current_market_regime,
"rsi": self.last_rsi_value,
"bb_position": self._get_bb_position(),
"signal_count": self._signal_count["entry"]
})
return IncStrategySignal.HOLD()
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate exit signal based on BBRS strategy logic.
Returns:
IncStrategySignal: Exit signal if conditions are met, hold signal otherwise
"""
if not self.is_warmed_up():
return IncStrategySignal.HOLD()
# Check for exit condition
if self._check_exit_condition():
self._signal_count["exit"] += 1
self._last_exit_signal = {
'timestamp': self._last_update_time,
'price': self.current_price,
'market_regime': self.current_market_regime,
'rsi': self.last_rsi_value,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"EXIT SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['exit']})")
return IncStrategySignal.SELL(confidence=1.0, metadata={
"market_regime": self.current_market_regime,
"rsi": self.last_rsi_value,
"bb_position": self._get_bb_position(),
"signal_count": self._signal_count["exit"]
})
return IncStrategySignal.HOLD()
def get_confidence(self) -> float:
"""
Get strategy confidence based on signal strength.
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.is_warmed_up():
return 0.0
# Higher confidence when signals are clear
if self._check_entry_condition() or self._check_exit_condition():
return 1.0
# Medium confidence during normal operation
return 0.5
def _update_volume_tracking(self, volume: float) -> None:
"""Update volume moving average tracking."""
# Update rolling sum
if len(self.volume_history) == 20: # maxlen reached
self.volume_sum -= self.volume_history[0]
self.volume_history.append(volume)
self.volume_sum += volume
# Calculate moving average
if len(self.volume_history) > 0:
self.volume_ma = self.volume_sum / len(self.volume_history)
else:
self.volume_ma = volume
def _determine_market_regime(self, bb_reference: Dict[str, float]) -> str:
"""
Determine market regime based on Bollinger Band width.
Args:
bb_reference: Reference BB result for regime detection
Returns:
"sideways" or "trending"
"""
if not self.bb_reference.is_warmed_up():
return "trending" # Default to trending during warm-up
bb_width = bb_reference['bandwidth']
if bb_width < self.bb_width_threshold:
return "sideways"
else:
return "trending"
def _check_volume_spike(self) -> bool:
"""Check if current volume represents a spike (≥1.5× average)."""
if self.volume_ma is None or self.volume_ma == 0 or self.current_volume is None:
return False
return self.current_volume >= 1.5 * self.volume_ma
def _get_bb_position(self) -> str:
"""Get current price position relative to Bollinger Bands."""
if not self.last_bb_result or self.current_price is None:
return 'unknown'
upper_band = self.last_bb_result['upper_band']
lower_band = self.last_bb_result['lower_band']
if self.current_price > upper_band:
return 'above_upper'
elif self.current_price < lower_band:
return 'below_lower'
else:
return 'between_bands'
def _check_entry_condition(self) -> bool:
"""
Check if entry condition is met based on market regime.
Returns:
bool: True if entry condition is met
"""
if not self.is_warmed_up() or self.last_bb_result is None:
return False
if np.isnan(self.last_rsi_value):
return False
upper_band = self.last_bb_result['upper_band']
lower_band = self.last_bb_result['lower_band']
if self.current_market_regime == "sideways":
# Sideways market (Mean Reversion)
rsi_low, rsi_high = self.sideways_rsi_thresholds
buy_condition = (self.current_price <= lower_band) and (self.last_rsi_value <= rsi_low)
if self.squeeze_strategy:
# Add volume contraction filter for sideways markets
volume_contraction = self.current_volume < 0.7 * (self.volume_ma or self.current_volume)
buy_condition = buy_condition and volume_contraction
return buy_condition
else: # trending
# Trending market (Breakout Mode)
volume_spike = self._check_volume_spike()
buy_condition = (self.current_price < lower_band) and (self.last_rsi_value < 50) and volume_spike
return buy_condition
def _check_exit_condition(self) -> bool:
"""
Check if exit condition is met based on market regime.
Returns:
bool: True if exit condition is met
"""
if not self.is_warmed_up() or self.last_bb_result is None:
return False
if np.isnan(self.last_rsi_value):
return False
upper_band = self.last_bb_result['upper_band']
lower_band = self.last_bb_result['lower_band']
if self.current_market_regime == "sideways":
# Sideways market (Mean Reversion)
rsi_low, rsi_high = self.sideways_rsi_thresholds
sell_condition = (self.current_price >= upper_band) and (self.last_rsi_value >= rsi_high)
if self.squeeze_strategy:
# Add volume contraction filter for sideways markets
volume_contraction = self.current_volume < 0.7 * (self.volume_ma or self.current_volume)
sell_condition = sell_condition and volume_contraction
return sell_condition
else: # trending
# Trending market (Breakout Mode)
volume_spike = self._check_volume_spike()
sell_condition = (self.current_price > upper_band) and (self.last_rsi_value > 50) and volume_spike
return sell_condition
def is_warmed_up(self) -> bool:
"""
Check if strategy is warmed up and ready for reliable signals.
Returns:
True if all indicators are warmed up
"""
return (self.bb_trending.is_warmed_up() and
self.bb_sideways.is_warmed_up() and
self.bb_reference.is_warmed_up() and
self.rsi.is_warmed_up() and
len(self.volume_history) >= 20)
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
super().reset_calculation_state()
# Reset indicators
self.bb_trending.reset()
self.bb_sideways.reset()
self.bb_reference.reset()
self.rsi.reset()
# Reset volume tracking
self.volume_history.clear()
self.volume_sum = 0.0
self.volume_ma = None
# Reset strategy state
self.current_price = None
self.current_volume = None
self.current_market_regime = "trending"
self.last_bb_result = None
self.last_rsi_value = None
# Reset signal state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Reset performance tracking
self._update_count = 0
self._last_update_time = None
logger.info("BBRSStrategy state reset")
def get_current_state_summary(self) -> Dict[str, Any]:
"""Get detailed state summary for debugging and monitoring."""
base_summary = super().get_current_state_summary()
# Add BBRS-specific state
base_summary.update({
'primary_timeframe': self.primary_timeframe,
'current_price': self.current_price,
'current_volume': self.current_volume,
'volume_ma': self.volume_ma,
'current_market_regime': self.current_market_regime,
'last_rsi_value': self.last_rsi_value,
'bb_position': self._get_bb_position(),
'volume_spike': self._check_volume_spike(),
'signal_counts': self._signal_count.copy(),
'update_count': self._update_count,
'last_update_time': str(self._last_update_time) if self._last_update_time else None,
'last_entry_signal': self._last_entry_signal,
'last_exit_signal': self._last_exit_signal,
'indicators_warmed_up': {
'bb_trending': self.bb_trending.is_warmed_up(),
'bb_sideways': self.bb_sideways.is_warmed_up(),
'bb_reference': self.bb_reference.is_warmed_up(),
'rsi': self.rsi.is_warmed_up(),
'volume_tracking': len(self.volume_history) >= 20
},
'config': {
'bb_period': self.bb_period,
'rsi_period': self.rsi_period,
'bb_width_threshold': self.bb_width_threshold,
'trending_bb_multiplier': self.trending_bb_multiplier,
'sideways_bb_multiplier': self.sideways_bb_multiplier,
'trending_rsi_thresholds': self.trending_rsi_thresholds,
'sideways_rsi_thresholds': self.sideways_rsi_thresholds,
'squeeze_strategy': self.squeeze_strategy
}
})
return base_summary
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"BBRSStrategy(timeframe={self.primary_timeframe}, "
f"bb_period={self.bb_period}, rsi_period={self.rsi_period}, "
f"regime={self.current_market_regime}, "
f"warmed_up={self.is_warmed_up()}, "
f"updates={self._update_count})")
# Compatibility alias for easier imports
IncBBRSStrategy = BBRSStrategy

View File

@@ -1,91 +0,0 @@
"""
Incremental Indicators Framework
This module provides incremental indicator implementations for real-time trading strategies.
All indicators maintain constant memory usage and provide identical results to traditional
batch calculations.
Available Indicators:
- Base classes: IndicatorState, SimpleIndicatorState, OHLCIndicatorState
- Moving Averages: MovingAverageState, ExponentialMovingAverageState
- Volatility: ATRState, SimpleATRState
- Trend: SupertrendState, SupertrendCollection
- Bollinger Bands: BollingerBandsState, BollingerBandsOHLCState
- RSI: RSIState, SimpleRSIState
Example:
from IncrementalTrader.strategies.indicators import SupertrendState, ATRState
# Create indicators
atr = ATRState(period=14)
supertrend = SupertrendState(period=10, multiplier=3.0)
# Update with OHLC data
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
atr_value = atr.update(ohlc)
st_result = supertrend.update(ohlc)
"""
# Base indicator classes
from .base import (
IndicatorState,
SimpleIndicatorState,
OHLCIndicatorState,
)
# Moving average indicators
from .moving_average import (
MovingAverageState,
ExponentialMovingAverageState,
)
# Volatility indicators
from .atr import (
ATRState,
SimpleATRState,
)
# Trend indicators
from .supertrend import (
SupertrendState,
SupertrendCollection,
)
# Bollinger Bands indicators
from .bollinger_bands import (
BollingerBandsState,
BollingerBandsOHLCState,
)
# RSI indicators
from .rsi import (
RSIState,
SimpleRSIState,
)
__all__ = [
# Base classes
"IndicatorState",
"SimpleIndicatorState",
"OHLCIndicatorState",
# Moving averages
"MovingAverageState",
"ExponentialMovingAverageState",
# Volatility indicators
"ATRState",
"SimpleATRState",
# Trend indicators
"SupertrendState",
"SupertrendCollection",
# Bollinger Bands
"BollingerBandsState",
"BollingerBandsOHLCState",
# RSI indicators
"RSIState",
"SimpleRSIState",
]

View File

@@ -1,254 +0,0 @@
"""
Average True Range (ATR) Indicator State
This module implements incremental ATR calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. ATR is used by
Supertrend and other volatility-based indicators.
"""
from typing import Dict, Union, Optional
from .base import OHLCIndicatorState
from .moving_average import ExponentialMovingAverageState
class ATRState(OHLCIndicatorState):
"""
Incremental Average True Range calculation state.
ATR measures market volatility by calculating the average of true ranges over
a specified period. True Range is the maximum of:
1. Current High - Current Low
2. |Current High - Previous Close|
3. |Current Low - Previous Close|
This implementation uses exponential moving average for smoothing, which is
more responsive than simple moving average and requires less memory.
Attributes:
period (int): The ATR period
ema_state (ExponentialMovingAverageState): EMA state for smoothing true ranges
previous_close (float): Previous period's close price
Example:
atr = ATRState(period=14)
# Add OHLC data incrementally
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
atr_value = atr.update(ohlc) # Returns current ATR value
# Check if warmed up
if atr.is_warmed_up():
current_atr = atr.get_current_value()
"""
def __init__(self, period: int = 14):
"""
Initialize ATR state.
Args:
period: Number of periods for ATR calculation (default: 14)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.ema_state = ExponentialMovingAverageState(period)
self.previous_close = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> float:
"""
Update ATR with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Current ATR value
Raises:
ValueError: If OHLC data is invalid
TypeError: If ohlc_data is not a dictionary
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Calculate True Range
if self.previous_close is None:
# First period - True Range is just High - Low
true_range = high - low
else:
# True Range is the maximum of:
# 1. Current High - Current Low
# 2. |Current High - Previous Close|
# 3. |Current Low - Previous Close|
tr1 = high - low
tr2 = abs(high - self.previous_close)
tr3 = abs(low - self.previous_close)
true_range = max(tr1, tr2, tr3)
# Update EMA with the true range
atr_value = self.ema_state.update(true_range)
# Store current close as previous close for next calculation
self.previous_close = close
self.values_received += 1
# Store current ATR value
self._current_values = {'atr': atr_value}
return atr_value
def is_warmed_up(self) -> bool:
"""
Check if ATR has enough data for reliable values.
Returns:
True if EMA state is warmed up (has enough true range values)
"""
return self.ema_state.is_warmed_up()
def reset(self) -> None:
"""Reset ATR state to initial conditions."""
self.ema_state.reset()
self.previous_close = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[float]:
"""
Get current ATR value without updating.
Returns:
Current ATR value, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self.ema_state.get_current_value()
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'ema_state': self.ema_state.get_state_summary(),
'current_atr': self.get_current_value()
})
return base_summary
class SimpleATRState(OHLCIndicatorState):
"""
Simple ATR implementation using simple moving average instead of EMA.
This version uses a simple moving average for smoothing true ranges,
which matches some traditional ATR implementations but requires more memory.
"""
def __init__(self, period: int = 14):
"""
Initialize simple ATR state.
Args:
period: Number of periods for ATR calculation (default: 14)
"""
super().__init__(period)
from collections import deque
self.true_ranges = deque(maxlen=period)
self.tr_sum = 0.0
self.previous_close = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> float:
"""
Update simple ATR with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Current ATR value
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Calculate True Range
if self.previous_close is None:
true_range = high - low
else:
tr1 = high - low
tr2 = abs(high - self.previous_close)
tr3 = abs(low - self.previous_close)
true_range = max(tr1, tr2, tr3)
# Update rolling sum
if len(self.true_ranges) == self.period:
self.tr_sum -= self.true_ranges[0] # Remove oldest value
self.true_ranges.append(true_range)
self.tr_sum += true_range
# Calculate ATR
atr_value = self.tr_sum / len(self.true_ranges)
# Store current close as previous close for next calculation
self.previous_close = close
self.values_received += 1
# Store current ATR value
self._current_values = {'atr': atr_value}
return atr_value
def is_warmed_up(self) -> bool:
"""
Check if simple ATR has enough data for reliable values.
Returns:
True if we have at least 'period' number of true range values
"""
return len(self.true_ranges) >= self.period
def reset(self) -> None:
"""Reset simple ATR state to initial conditions."""
self.true_ranges.clear()
self.tr_sum = 0.0
self.previous_close = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[float]:
"""
Get current simple ATR value without updating.
Returns:
Current ATR value, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self.tr_sum / len(self.true_ranges)
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'tr_sum': self.tr_sum,
'true_ranges_count': len(self.true_ranges),
'current_atr': self.get_current_value()
})
return base_summary

View File

@@ -1,197 +0,0 @@
"""
Base Indicator State Class
This module contains the abstract base class for all incremental indicator states.
All indicator implementations must inherit from IndicatorState and implement
the required methods for incremental calculation.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Union
import numpy as np
class IndicatorState(ABC):
"""
Abstract base class for maintaining indicator calculation state.
This class defines the interface that all incremental indicators must implement.
Indicators maintain their internal state and can be updated incrementally with
new data points, providing constant memory usage and high performance.
Attributes:
period (int): The period/window size for the indicator
values_received (int): Number of values processed so far
is_initialized (bool): Whether the indicator has been initialized
Example:
class MyIndicator(IndicatorState):
def __init__(self, period: int):
super().__init__(period)
self._sum = 0.0
def update(self, new_value: float) -> float:
self._sum += new_value
self.values_received += 1
return self._sum / min(self.values_received, self.period)
"""
def __init__(self, period: int):
"""
Initialize the indicator state.
Args:
period: The period/window size for the indicator calculation
Raises:
ValueError: If period is not a positive integer
"""
if not isinstance(period, int) or period <= 0:
raise ValueError(f"Period must be a positive integer, got {period}")
self.period = period
self.values_received = 0
self.is_initialized = False
@abstractmethod
def update(self, new_value: Union[float, Dict[str, float]]) -> Union[float, Dict[str, float]]:
"""
Update indicator with new value and return current indicator value.
This method processes a new data point and updates the internal state
of the indicator. It returns the current indicator value after the update.
Args:
new_value: New data point (can be single value or OHLCV dict)
Returns:
Current indicator value after update (single value or dict)
Raises:
ValueError: If new_value is invalid or incompatible
"""
pass
@abstractmethod
def is_warmed_up(self) -> bool:
"""
Check whether indicator has enough data for reliable values.
Returns:
True if indicator has received enough data points for reliable calculation
"""
pass
@abstractmethod
def reset(self) -> None:
"""
Reset indicator state to initial conditions.
This method clears all internal state and resets the indicator
as if it was just initialized.
"""
pass
@abstractmethod
def get_current_value(self) -> Union[float, Dict[str, float], None]:
"""
Get the current indicator value without updating.
Returns:
Current indicator value, or None if not warmed up
"""
pass
def get_state_summary(self) -> Dict[str, Any]:
"""
Get summary of current indicator state for debugging.
Returns:
Dictionary containing indicator state information
"""
return {
'indicator_type': self.__class__.__name__,
'period': self.period,
'values_received': self.values_received,
'is_warmed_up': self.is_warmed_up(),
'is_initialized': self.is_initialized,
'current_value': self.get_current_value()
}
def validate_input(self, value: Union[float, Dict[str, float]]) -> None:
"""
Validate input value for the indicator.
Args:
value: Input value to validate
Raises:
ValueError: If value is invalid
TypeError: If value type is incorrect
"""
if isinstance(value, (int, float)):
if not np.isfinite(value):
raise ValueError(f"Input value must be finite, got {value}")
elif isinstance(value, dict):
required_keys = ['open', 'high', 'low', 'close']
for key in required_keys:
if key not in value:
raise ValueError(f"OHLCV dict missing required key: {key}")
if not np.isfinite(value[key]):
raise ValueError(f"OHLCV value for {key} must be finite, got {value[key]}")
# Validate OHLC relationships
if not (value['low'] <= value['open'] <= value['high'] and
value['low'] <= value['close'] <= value['high']):
raise ValueError(f"Invalid OHLC relationships: {value}")
else:
raise TypeError(f"Input value must be float or OHLCV dict, got {type(value)}")
def __repr__(self) -> str:
"""String representation of the indicator state."""
return (f"{self.__class__.__name__}(period={self.period}, "
f"values_received={self.values_received}, "
f"warmed_up={self.is_warmed_up()})")
class SimpleIndicatorState(IndicatorState):
"""
Base class for simple single-value indicators.
This class provides common functionality for indicators that work with
single float values and maintain a simple rolling calculation.
"""
def __init__(self, period: int):
"""Initialize simple indicator state."""
super().__init__(period)
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""Get current indicator value."""
return self._current_value if self.is_warmed_up() else None
def is_warmed_up(self) -> bool:
"""Check if indicator is warmed up."""
return self.values_received >= self.period
class OHLCIndicatorState(IndicatorState):
"""
Base class for OHLC-based indicators.
This class provides common functionality for indicators that work with
OHLC data (Open, High, Low, Close) and may return multiple values.
"""
def __init__(self, period: int):
"""Initialize OHLC indicator state."""
super().__init__(period)
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""Get current indicator values."""
return self._current_values.copy() if self.is_warmed_up() else None
def is_warmed_up(self) -> bool:
"""Check if indicator is warmed up."""
return self.values_received >= self.period

View File

@@ -1,325 +0,0 @@
"""
Bollinger Bands Indicator State
This module implements incremental Bollinger Bands calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. Used by the BBRSStrategy.
"""
from typing import Dict, Union, Optional
from collections import deque
import math
from .base import OHLCIndicatorState
from .moving_average import MovingAverageState
class BollingerBandsState(OHLCIndicatorState):
"""
Incremental Bollinger Bands calculation state.
Bollinger Bands consist of:
- Middle Band: Simple Moving Average of close prices
- Upper Band: Middle Band + (Standard Deviation * multiplier)
- Lower Band: Middle Band - (Standard Deviation * multiplier)
This implementation maintains a rolling window for standard deviation calculation
while using the MovingAverageState for the middle band.
Attributes:
period (int): Period for moving average and standard deviation
std_dev_multiplier (float): Multiplier for standard deviation
ma_state (MovingAverageState): Moving average state for middle band
close_values (deque): Rolling window of close prices for std dev calculation
close_sum_sq (float): Sum of squared close values for variance calculation
Example:
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
# Add price data incrementally
result = bb.update(103.5) # Close price
upper_band = result['upper_band']
middle_band = result['middle_band']
lower_band = result['lower_band']
bandwidth = result['bandwidth']
"""
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0):
"""
Initialize Bollinger Bands state.
Args:
period: Period for moving average and standard deviation (default: 20)
std_dev_multiplier: Multiplier for standard deviation (default: 2.0)
Raises:
ValueError: If period is not positive or multiplier is not positive
"""
super().__init__(period)
if std_dev_multiplier <= 0:
raise ValueError(f"Standard deviation multiplier must be positive, got {std_dev_multiplier}")
self.std_dev_multiplier = std_dev_multiplier
self.ma_state = MovingAverageState(period)
# For incremental standard deviation calculation
self.close_values = deque(maxlen=period)
self.close_sum_sq = 0.0 # Sum of squared values
self.is_initialized = True
def update(self, close_price: Union[float, int]) -> Dict[str, float]:
"""
Update Bollinger Bands with new close price.
Args:
close_price: New closing price
Returns:
Dictionary with 'upper_band', 'middle_band', 'lower_band', 'bandwidth', 'std_dev'
Raises:
ValueError: If close_price is not finite
TypeError: If close_price is not numeric
"""
# Validate input
if not isinstance(close_price, (int, float)):
raise TypeError(f"close_price must be numeric, got {type(close_price)}")
self.validate_input(close_price)
close_price = float(close_price)
# Update moving average (middle band)
middle_band = self.ma_state.update(close_price)
# Update rolling window for standard deviation
if len(self.close_values) == self.period:
# Remove oldest value from sum of squares
old_value = self.close_values[0]
self.close_sum_sq -= old_value * old_value
# Add new value
self.close_values.append(close_price)
self.close_sum_sq += close_price * close_price
# Calculate standard deviation
n = len(self.close_values)
if n < 2:
# Not enough data for standard deviation
std_dev = 0.0
else:
# Incremental variance calculation: Var = (sum_sq - n*mean^2) / (n-1)
mean = middle_band
variance = (self.close_sum_sq - n * mean * mean) / (n - 1)
std_dev = math.sqrt(max(variance, 0.0)) # Ensure non-negative
# Calculate bands
upper_band = middle_band + (self.std_dev_multiplier * std_dev)
lower_band = middle_band - (self.std_dev_multiplier * std_dev)
# Calculate bandwidth (normalized band width)
if middle_band != 0:
bandwidth = (upper_band - lower_band) / middle_band
else:
bandwidth = 0.0
self.values_received += 1
# Store current values
result = {
'upper_band': upper_band,
'middle_band': middle_band,
'lower_band': lower_band,
'bandwidth': bandwidth,
'std_dev': std_dev
}
self._current_values = result
return result
def is_warmed_up(self) -> bool:
"""
Check if Bollinger Bands has enough data for reliable values.
Returns:
True if we have at least 'period' number of values
"""
return self.ma_state.is_warmed_up()
def reset(self) -> None:
"""Reset Bollinger Bands state to initial conditions."""
self.ma_state.reset()
self.close_values.clear()
self.close_sum_sq = 0.0
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""
Get current Bollinger Bands values without updating.
Returns:
Dictionary with current BB values, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self._current_values.copy() if self._current_values else None
def get_squeeze_status(self, squeeze_threshold: float = 0.05) -> bool:
"""
Check if Bollinger Bands are in a squeeze condition.
Args:
squeeze_threshold: Bandwidth threshold for squeeze detection
Returns:
True if bandwidth is below threshold (squeeze condition)
"""
if not self.is_warmed_up() or not self._current_values:
return False
bandwidth = self._current_values.get('bandwidth', float('inf'))
return bandwidth < squeeze_threshold
def get_position_relative_to_bands(self, current_price: float) -> str:
"""
Get current price position relative to Bollinger Bands.
Args:
current_price: Current price to evaluate
Returns:
'above_upper', 'between_bands', 'below_lower', or 'unknown'
"""
if not self.is_warmed_up() or not self._current_values:
return 'unknown'
upper_band = self._current_values['upper_band']
lower_band = self._current_values['lower_band']
if current_price > upper_band:
return 'above_upper'
elif current_price < lower_band:
return 'below_lower'
else:
return 'between_bands'
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'std_dev_multiplier': self.std_dev_multiplier,
'close_values_count': len(self.close_values),
'close_sum_sq': self.close_sum_sq,
'ma_state': self.ma_state.get_state_summary(),
'current_squeeze': self.get_squeeze_status() if self.is_warmed_up() else None
})
return base_summary
class BollingerBandsOHLCState(OHLCIndicatorState):
"""
Bollinger Bands implementation that works with OHLC data.
This version can calculate Bollinger Bands based on different price types
(close, typical price, etc.) and provides additional OHLC-based analysis.
"""
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0, price_type: str = 'close'):
"""
Initialize OHLC Bollinger Bands state.
Args:
period: Period for calculation
std_dev_multiplier: Standard deviation multiplier
price_type: Price type to use ('close', 'typical', 'median', 'weighted')
"""
super().__init__(period)
if price_type not in ['close', 'typical', 'median', 'weighted']:
raise ValueError(f"Invalid price_type: {price_type}")
self.std_dev_multiplier = std_dev_multiplier
self.price_type = price_type
self.bb_state = BollingerBandsState(period, std_dev_multiplier)
self.is_initialized = True
def _extract_price(self, ohlc_data: Dict[str, float]) -> float:
"""Extract price based on price_type setting."""
if self.price_type == 'close':
return ohlc_data['close']
elif self.price_type == 'typical':
return (ohlc_data['high'] + ohlc_data['low'] + ohlc_data['close']) / 3.0
elif self.price_type == 'median':
return (ohlc_data['high'] + ohlc_data['low']) / 2.0
elif self.price_type == 'weighted':
return (ohlc_data['high'] + ohlc_data['low'] + 2 * ohlc_data['close']) / 4.0
else:
return ohlc_data['close']
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
"""
Update Bollinger Bands with OHLC data.
Args:
ohlc_data: Dictionary with OHLC data
Returns:
Dictionary with Bollinger Bands values plus OHLC analysis
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
# Extract price based on type
price = self._extract_price(ohlc_data)
# Update underlying BB state
bb_result = self.bb_state.update(price)
# Add OHLC-specific analysis
high = ohlc_data['high']
low = ohlc_data['low']
close = ohlc_data['close']
# Check if high/low touched bands
upper_band = bb_result['upper_band']
lower_band = bb_result['lower_band']
bb_result.update({
'high_above_upper': high > upper_band,
'low_below_lower': low < lower_band,
'close_position': self.bb_state.get_position_relative_to_bands(close),
'price_type': self.price_type,
'extracted_price': price
})
self.values_received += 1
self._current_values = bb_result
return bb_result
def is_warmed_up(self) -> bool:
"""Check if OHLC Bollinger Bands is warmed up."""
return self.bb_state.is_warmed_up()
def reset(self) -> None:
"""Reset OHLC Bollinger Bands state."""
self.bb_state.reset()
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""Get current OHLC Bollinger Bands values."""
return self.bb_state.get_current_value()
def get_state_summary(self) -> dict:
"""Get detailed state summary."""
base_summary = super().get_state_summary()
base_summary.update({
'price_type': self.price_type,
'bb_state': self.bb_state.get_state_summary()
})
return base_summary

View File

@@ -1,228 +0,0 @@
"""
Moving Average Indicator State
This module implements incremental moving average calculation that maintains
constant memory usage and provides identical results to traditional batch calculations.
"""
from collections import deque
from typing import Union
from .base import SimpleIndicatorState
class MovingAverageState(SimpleIndicatorState):
"""
Incremental moving average calculation state.
This class maintains the state for calculating a simple moving average
incrementally. It uses a rolling window approach with constant memory usage.
Attributes:
period (int): The moving average period
values (deque): Rolling window of values (max length = period)
sum (float): Current sum of values in the window
Example:
ma = MovingAverageState(period=20)
# Add values incrementally
ma_value = ma.update(100.0) # Returns current MA value
ma_value = ma.update(105.0) # Updates and returns new MA value
# Check if warmed up (has enough values)
if ma.is_warmed_up():
current_ma = ma.get_current_value()
"""
def __init__(self, period: int):
"""
Initialize moving average state.
Args:
period: Number of periods for the moving average
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.values = deque(maxlen=period)
self.sum = 0.0
self.is_initialized = True
def update(self, new_value: Union[float, int]) -> float:
"""
Update moving average with new value.
Args:
new_value: New price/value to add to the moving average
Returns:
Current moving average value
Raises:
ValueError: If new_value is not finite
TypeError: If new_value is not numeric
"""
# Validate input
if not isinstance(new_value, (int, float)):
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
self.validate_input(new_value)
# If deque is at max capacity, subtract the value being removed
if len(self.values) == self.period:
self.sum -= self.values[0] # Will be automatically removed by deque
# Add new value
self.values.append(float(new_value))
self.sum += float(new_value)
self.values_received += 1
# Calculate current moving average
current_count = len(self.values)
self._current_value = self.sum / current_count
return self._current_value
def is_warmed_up(self) -> bool:
"""
Check if moving average has enough data for reliable values.
Returns:
True if we have at least 'period' number of values
"""
return len(self.values) >= self.period
def reset(self) -> None:
"""Reset moving average state to initial conditions."""
self.values.clear()
self.sum = 0.0
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Union[float, None]:
"""
Get current moving average value without updating.
Returns:
Current moving average value, or None if not enough data
"""
if len(self.values) == 0:
return None
return self.sum / len(self.values)
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'window_size': len(self.values),
'sum': self.sum,
'values_in_window': list(self.values) if len(self.values) <= 10 else f"[{len(self.values)} values]"
})
return base_summary
class ExponentialMovingAverageState(SimpleIndicatorState):
"""
Incremental exponential moving average calculation state.
This class maintains the state for calculating an exponential moving average (EMA)
incrementally. EMA gives more weight to recent values and requires minimal memory.
Attributes:
period (int): The EMA period (used to calculate smoothing factor)
alpha (float): Smoothing factor (2 / (period + 1))
ema_value (float): Current EMA value
Example:
ema = ExponentialMovingAverageState(period=20)
# Add values incrementally
ema_value = ema.update(100.0) # Returns current EMA value
ema_value = ema.update(105.0) # Updates and returns new EMA value
"""
def __init__(self, period: int):
"""
Initialize exponential moving average state.
Args:
period: Number of periods for the EMA (used to calculate alpha)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.alpha = 2.0 / (period + 1) # Smoothing factor
self.ema_value = None
self.is_initialized = True
def update(self, new_value: Union[float, int]) -> float:
"""
Update exponential moving average with new value.
Args:
new_value: New price/value to add to the EMA
Returns:
Current EMA value
Raises:
ValueError: If new_value is not finite
TypeError: If new_value is not numeric
"""
# Validate input
if not isinstance(new_value, (int, float)):
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
self.validate_input(new_value)
new_value = float(new_value)
if self.ema_value is None:
# First value - initialize EMA
self.ema_value = new_value
else:
# EMA formula: EMA = alpha * new_value + (1 - alpha) * previous_EMA
self.ema_value = self.alpha * new_value + (1 - self.alpha) * self.ema_value
self.values_received += 1
self._current_value = self.ema_value
return self.ema_value
def is_warmed_up(self) -> bool:
"""
Check if EMA has enough data for reliable values.
For EMA, we consider it warmed up after receiving 'period' number of values,
though it starts producing values immediately.
Returns:
True if we have received at least 'period' number of values
"""
return self.values_received >= self.period
def reset(self) -> None:
"""Reset EMA state to initial conditions."""
self.ema_value = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Union[float, None]:
"""
Get current EMA value without updating.
Returns:
Current EMA value, or None if no values received yet
"""
return self.ema_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'alpha': self.alpha,
'ema_value': self.ema_value
})
return base_summary

View File

@@ -1,289 +0,0 @@
"""
RSI (Relative Strength Index) Indicator State
This module implements incremental RSI calculation that maintains constant memory usage
and provides identical results to traditional batch calculations.
"""
from typing import Union, Optional
from .base import SimpleIndicatorState
from .moving_average import ExponentialMovingAverageState
class RSIState(SimpleIndicatorState):
"""
Incremental RSI calculation state using Wilder's smoothing.
RSI measures the speed and magnitude of price changes to evaluate overbought
or oversold conditions. It oscillates between 0 and 100.
RSI = 100 - (100 / (1 + RS))
where RS = Average Gain / Average Loss over the specified period
This implementation uses Wilder's smoothing (alpha = 1/period) to match
the original pandas implementation exactly.
Attributes:
period (int): The RSI period (typically 14)
alpha (float): Wilder's smoothing factor (1/period)
avg_gain (float): Current average gain
avg_loss (float): Current average loss
previous_close (float): Previous period's close price
Example:
rsi = RSIState(period=14)
# Add price data incrementally
rsi_value = rsi.update(100.0) # Returns current RSI value
rsi_value = rsi.update(105.0) # Updates and returns new RSI value
# Check if warmed up
if rsi.is_warmed_up():
current_rsi = rsi.get_current_value()
"""
def __init__(self, period: int = 14):
"""
Initialize RSI state.
Args:
period: Number of periods for RSI calculation (default: 14)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.alpha = 1.0 / period # Wilder's smoothing factor
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.is_initialized = True
def update(self, new_close: Union[float, int]) -> float:
"""
Update RSI with new close price using Wilder's smoothing.
Args:
new_close: New closing price
Returns:
Current RSI value (0-100), or NaN if not warmed up
Raises:
ValueError: If new_close is not finite
TypeError: If new_close is not numeric
"""
# Validate input - accept numpy types as well
import numpy as np
if not isinstance(new_close, (int, float, np.integer, np.floating)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(float(new_close))
new_close = float(new_close)
if self.previous_close is None:
# First value - no gain/loss to calculate
self.previous_close = new_close
self.values_received += 1
# Return NaN until warmed up (matches original behavior)
self._current_value = float('nan')
return self._current_value
# Calculate price change
price_change = new_close - self.previous_close
# Separate gains and losses
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
if self.avg_gain is None:
# Initialize with first gain/loss
self.avg_gain = gain
self.avg_loss = loss
else:
# Wilder's smoothing: avg = alpha * new_value + (1 - alpha) * previous_avg
self.avg_gain = self.alpha * gain + (1 - self.alpha) * self.avg_gain
self.avg_loss = self.alpha * loss + (1 - self.alpha) * self.avg_loss
# Calculate RSI only if warmed up
# RSI should start when we have 'period' price changes (not including the first value)
if self.values_received > self.period:
if self.avg_loss == 0.0:
# Avoid division by zero - all gains, no losses
if self.avg_gain > 0:
rsi_value = 100.0
else:
rsi_value = 50.0 # Neutral when both are zero
else:
rs = self.avg_gain / self.avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
else:
# Not warmed up yet - return NaN
rsi_value = float('nan')
# Store state
self.previous_close = new_close
self.values_received += 1
self._current_value = rsi_value
return rsi_value
def is_warmed_up(self) -> bool:
"""
Check if RSI has enough data for reliable values.
Returns:
True if we have enough price changes for RSI calculation
"""
return self.values_received > self.period
def reset(self) -> None:
"""Reset RSI state to initial conditions."""
self.alpha = 1.0 / self.period
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""
Get current RSI value without updating.
Returns:
Current RSI value (0-100), or None if not enough data
"""
if not self.is_warmed_up():
return None
return self._current_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'alpha': self.alpha,
'previous_close': self.previous_close,
'avg_gain': self.avg_gain,
'avg_loss': self.avg_loss,
'current_rsi': self.get_current_value()
})
return base_summary
class SimpleRSIState(SimpleIndicatorState):
"""
Simple RSI implementation using simple moving averages instead of EMAs.
This version uses simple moving averages for gain and loss smoothing,
which matches traditional RSI implementations but requires more memory.
"""
def __init__(self, period: int = 14):
"""
Initialize simple RSI state.
Args:
period: Number of periods for RSI calculation (default: 14)
"""
super().__init__(period)
from collections import deque
self.gains = deque(maxlen=period)
self.losses = deque(maxlen=period)
self.gain_sum = 0.0
self.loss_sum = 0.0
self.previous_close = None
self.is_initialized = True
def update(self, new_close: Union[float, int]) -> float:
"""
Update simple RSI with new close price.
Args:
new_close: New closing price
Returns:
Current RSI value (0-100)
"""
# Validate input
if not isinstance(new_close, (int, float)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(new_close)
new_close = float(new_close)
if self.previous_close is None:
# First value
self.previous_close = new_close
self.values_received += 1
self._current_value = 50.0
return self._current_value
# Calculate price change
price_change = new_close - self.previous_close
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
# Update rolling sums
if len(self.gains) == self.period:
self.gain_sum -= self.gains[0]
self.loss_sum -= self.losses[0]
self.gains.append(gain)
self.losses.append(loss)
self.gain_sum += gain
self.loss_sum += loss
# Calculate RSI
if len(self.gains) == 0:
rsi_value = 50.0
else:
avg_gain = self.gain_sum / len(self.gains)
avg_loss = self.loss_sum / len(self.losses)
if avg_loss == 0.0:
rsi_value = 100.0
else:
rs = avg_gain / avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
# Store state
self.previous_close = new_close
self.values_received += 1
self._current_value = rsi_value
return rsi_value
def is_warmed_up(self) -> bool:
"""Check if simple RSI is warmed up."""
return len(self.gains) >= self.period
def reset(self) -> None:
"""Reset simple RSI state."""
self.gains.clear()
self.losses.clear()
self.gain_sum = 0.0
self.loss_sum = 0.0
self.previous_close = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""Get current simple RSI value."""
if self.values_received == 0:
return None
return self._current_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'gains_window_size': len(self.gains),
'losses_window_size': len(self.losses),
'gain_sum': self.gain_sum,
'loss_sum': self.loss_sum,
'current_rsi': self.get_current_value()
})
return base_summary

View File

@@ -1,316 +0,0 @@
"""
Supertrend Indicator State
This module implements incremental Supertrend calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. Supertrend is used by
the DefaultStrategy for trend detection.
"""
from typing import Dict, Union, Optional
from .base import OHLCIndicatorState
from .atr import ATRState
class SupertrendState(OHLCIndicatorState):
"""
Incremental Supertrend calculation state.
Supertrend is a trend-following indicator that uses Average True Range (ATR)
to calculate dynamic support and resistance levels. It provides clear trend
direction signals: +1 for uptrend, -1 for downtrend.
The calculation involves:
1. Calculate ATR for the given period
2. Calculate basic upper and lower bands using ATR and multiplier
3. Calculate final upper and lower bands with trend logic
4. Determine trend direction based on price vs bands
Attributes:
period (int): ATR period for Supertrend calculation
multiplier (float): Multiplier for ATR in band calculation
atr_state (ATRState): ATR calculation state
previous_close (float): Previous period's close price
previous_trend (int): Previous trend direction (+1 or -1)
final_upper_band (float): Current final upper band
final_lower_band (float): Current final lower band
Example:
supertrend = SupertrendState(period=10, multiplier=3.0)
# Add OHLC data incrementally
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
result = supertrend.update(ohlc)
trend = result['trend'] # +1 or -1
supertrend_value = result['supertrend'] # Supertrend line value
"""
def __init__(self, period: int = 10, multiplier: float = 3.0):
"""
Initialize Supertrend state.
Args:
period: ATR period for Supertrend calculation (default: 10)
multiplier: Multiplier for ATR in band calculation (default: 3.0)
Raises:
ValueError: If period is not positive or multiplier is not positive
"""
super().__init__(period)
if multiplier <= 0:
raise ValueError(f"Multiplier must be positive, got {multiplier}")
self.multiplier = multiplier
self.atr_state = ATRState(period)
# State variables
self.previous_close = None
self.previous_trend = None # Don't assume initial trend, let first calculation determine it
self.final_upper_band = None
self.final_lower_band = None
# Current values
self.current_trend = None
self.current_supertrend = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
"""
Update Supertrend with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Dictionary with 'trend', 'supertrend', 'upper_band', 'lower_band' keys
Raises:
ValueError: If OHLC data is invalid
TypeError: If ohlc_data is not a dictionary
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Update ATR
atr_value = self.atr_state.update(ohlc_data)
# Calculate HL2 (typical price)
hl2 = (high + low) / 2.0
# Calculate basic upper and lower bands
basic_upper_band = hl2 + (self.multiplier * atr_value)
basic_lower_band = hl2 - (self.multiplier * atr_value)
# Calculate final upper band
if self.final_upper_band is None or basic_upper_band < self.final_upper_band or self.previous_close > self.final_upper_band:
final_upper_band = basic_upper_band
else:
final_upper_band = self.final_upper_band
# Calculate final lower band
if self.final_lower_band is None or basic_lower_band > self.final_lower_band or self.previous_close < self.final_lower_band:
final_lower_band = basic_lower_band
else:
final_lower_band = self.final_lower_band
# Determine trend
if self.previous_close is None:
# First calculation - match original logic
# If close <= upper_band, trend is -1 (downtrend), else trend is 1 (uptrend)
trend = -1 if close <= basic_upper_band else 1
else:
# Trend logic for subsequent calculations
if self.previous_trend == 1 and close <= final_lower_band:
trend = -1
elif self.previous_trend == -1 and close >= final_upper_band:
trend = 1
else:
trend = self.previous_trend
# Calculate Supertrend value
if trend == 1:
supertrend_value = final_lower_band
else:
supertrend_value = final_upper_band
# Store current state
self.previous_close = close
self.previous_trend = trend
self.final_upper_band = final_upper_band
self.final_lower_band = final_lower_band
self.current_trend = trend
self.current_supertrend = supertrend_value
self.values_received += 1
# Prepare result
result = {
'trend': trend,
'supertrend': supertrend_value,
'upper_band': final_upper_band,
'lower_band': final_lower_band,
'atr': atr_value
}
self._current_values = result
return result
def is_warmed_up(self) -> bool:
"""
Check if Supertrend has enough data for reliable values.
Returns:
True if ATR state is warmed up
"""
return self.atr_state.is_warmed_up()
def reset(self) -> None:
"""Reset Supertrend state to initial conditions."""
self.atr_state.reset()
self.previous_close = None
self.previous_trend = None
self.final_upper_band = None
self.final_lower_band = None
self.current_trend = None
self.current_supertrend = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""
Get current Supertrend values without updating.
Returns:
Dictionary with current Supertrend values, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self._current_values.copy() if self._current_values else None
def get_current_trend(self) -> int:
"""
Get current trend direction.
Returns:
Current trend (+1 for uptrend, -1 for downtrend, 0 if not warmed up)
"""
return self.current_trend if self.current_trend is not None else 0
def get_current_supertrend_value(self) -> Optional[float]:
"""
Get current Supertrend line value.
Returns:
Current Supertrend value, or None if not warmed up
"""
return self.current_supertrend
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'multiplier': self.multiplier,
'previous_close': self.previous_close,
'previous_trend': self.previous_trend,
'current_trend': self.current_trend,
'current_supertrend': self.current_supertrend,
'final_upper_band': self.final_upper_band,
'final_lower_band': self.final_lower_band,
'atr_state': self.atr_state.get_state_summary()
})
return base_summary
class SupertrendCollection:
"""
Collection of multiple Supertrend indicators for meta-trend calculation.
This class manages multiple Supertrend indicators with different parameters
and provides meta-trend calculation based on their agreement.
"""
def __init__(self, supertrend_configs: list):
"""
Initialize collection of Supertrend indicators.
Args:
supertrend_configs: List of (period, multiplier) tuples
"""
self.supertrends = []
self.configs = supertrend_configs
for period, multiplier in supertrend_configs:
supertrend = SupertrendState(period=period, multiplier=multiplier)
self.supertrends.append(supertrend)
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, Union[int, list]]:
"""
Update all Supertrend indicators and calculate meta-trend.
Args:
ohlc_data: OHLC data dictionary
Returns:
Dictionary with 'meta_trend' and 'trends' keys
"""
trends = []
# Update each Supertrend and collect trends
for supertrend in self.supertrends:
result = supertrend.update(ohlc_data)
trends.append(result['trend'])
# Calculate meta-trend
meta_trend = self.get_current_meta_trend()
return {
'meta_trend': meta_trend,
'trends': trends
}
def is_warmed_up(self) -> bool:
"""Check if all Supertrend indicators are warmed up."""
return all(st.is_warmed_up() for st in self.supertrends)
def reset(self) -> None:
"""Reset all Supertrend indicators."""
for supertrend in self.supertrends:
supertrend.reset()
def get_current_meta_trend(self) -> int:
"""
Calculate current meta-trend from all Supertrend indicators.
Meta-trend logic:
- If all trends agree, return that trend
- If trends disagree, return 0 (neutral)
Returns:
Meta-trend value (1, -1, or 0)
"""
if not self.is_warmed_up():
return 0
trends = [st.get_current_trend() for st in self.supertrends]
# Check if all trends agree
if all(trend == trends[0] for trend in trends):
return trends[0] # All agree: return the common trend
else:
return 0 # Neutral when trends disagree
def get_state_summary(self) -> dict:
"""Get detailed state summary for all Supertrend indicators."""
return {
'configs': self.configs,
'meta_trend': self.get_current_meta_trend(),
'is_warmed_up': self.is_warmed_up(),
'supertrends': [st.get_state_summary() for st in self.supertrends]
}

View File

@@ -1,430 +0,0 @@
"""
Incremental MetaTrend Strategy
This module implements an incremental version of the DefaultStrategy that processes
real-time data efficiently while producing identical meta-trend signals to the
original batch-processing implementation.
The strategy uses 3 Supertrend indicators with parameters:
- Supertrend 1: period=12, multiplier=3.0
- Supertrend 2: period=10, multiplier=1.0
- Supertrend 3: period=11, multiplier=2.0
Meta-trend calculation:
- Meta-trend = 1 when all 3 Supertrends agree on uptrend
- Meta-trend = -1 when all 3 Supertrends agree on downtrend
- Meta-trend = 0 when Supertrends disagree (neutral)
Signal generation:
- Entry: meta-trend changes from != 1 to == 1
- Exit: meta-trend changes from != -1 to == -1
Stop-loss handling is delegated to the trader layer.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
import logging
from .base import IncStrategyBase, IncStrategySignal
from .indicators.supertrend import SupertrendCollection
logger = logging.getLogger(__name__)
class MetaTrendStrategy(IncStrategyBase):
"""
Incremental MetaTrend strategy implementation.
This strategy uses multiple Supertrend indicators to determine market direction
and generates entry/exit signals based on meta-trend changes. It processes
data incrementally for real-time performance while maintaining mathematical
equivalence to the original DefaultStrategy.
The strategy is designed to work with any timeframe but defaults to the
timeframe specified in parameters (or 15min if not specified).
Parameters:
timeframe (str): Primary timeframe for analysis (default: "15min")
buffer_size_multiplier (float): Buffer size multiplier for memory management (default: 2.0)
enable_logging (bool): Enable detailed logging (default: False)
Example:
strategy = MetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "15min",
"enable_logging": True
})
"""
def __init__(self, name: str = "metatrend", weight: float = 1.0, params: Optional[Dict] = None):
"""
Initialize the incremental MetaTrend strategy.
Args:
name: Strategy name/identifier
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters
"""
super().__init__(name, weight, params)
# Strategy configuration - now handled by base class timeframe aggregation
self.primary_timeframe = self.params.get("timeframe", "15min")
self.enable_logging = self.params.get("enable_logging", False)
# Configure logging level
if self.enable_logging:
logger.setLevel(logging.DEBUG)
# Initialize Supertrend collection with exact parameters from original strategy
self.supertrend_configs = [
(12, 3.0), # period=12, multiplier=3.0
(10, 1.0), # period=10, multiplier=1.0
(11, 2.0) # period=11, multiplier=2.0
]
self.supertrend_collection = SupertrendCollection(self.supertrend_configs)
# Meta-trend state
self.current_meta_trend = 0
self.previous_meta_trend = 0
self._meta_trend_history = [] # For debugging/analysis
# Signal generation state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Performance tracking
self._update_count = 0
self._last_update_time = None
logger.info(f"MetaTrendStrategy initialized: timeframe={self.primary_timeframe}, "
f"aggregation_enabled={self._timeframe_aggregator is not None}")
if self.enable_logging:
logger.info(f"Using new timeframe utilities with mathematically correct aggregation")
logger.info(f"Bar timestamps use 'end' mode to prevent future data leakage")
if self._timeframe_aggregator:
stats = self.get_timeframe_aggregator_stats()
logger.debug(f"Timeframe aggregator stats: {stats}")
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for reliable Supertrend calculations.
With the new base class timeframe aggregation, we only need to specify
the minimum buffer size for our primary timeframe. The base class
handles minute-level data aggregation automatically.
Returns:
Dict[str, int]: {timeframe: min_points} mapping
"""
# Find the largest period among all Supertrend configurations
max_period = max(config[0] for config in self.supertrend_configs)
# Add buffer for ATR warmup (ATR typically needs ~2x period for stability)
min_buffer_size = max_period * 2 + 10 # Extra 10 points for safety
# With new base class, we only specify our primary timeframe
# The base class handles minute-level aggregation automatically
return {self.primary_timeframe: min_buffer_size}
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
This method updates the Supertrend indicators and recalculates the meta-trend
based on the new data point.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
try:
self._update_count += 1
self._last_update_time = timestamp
if self.enable_logging:
logger.debug(f"Processing data point {self._update_count} at {timestamp}")
logger.debug(f"OHLC: O={new_data_point.get('open', 0):.2f}, "
f"H={new_data_point.get('high', 0):.2f}, "
f"L={new_data_point.get('low', 0):.2f}, "
f"C={new_data_point.get('close', 0):.2f}")
# Store previous meta-trend for change detection
self.previous_meta_trend = self.current_meta_trend
# Update Supertrend collection with new data
supertrend_results = self.supertrend_collection.update(new_data_point)
# Calculate new meta-trend
self.current_meta_trend = self._calculate_meta_trend(supertrend_results)
# Store meta-trend history for analysis
self._meta_trend_history.append({
'timestamp': timestamp,
'meta_trend': self.current_meta_trend,
'individual_trends': supertrend_results['trends'].copy(),
'update_count': self._update_count
})
# Limit history size to prevent memory growth
if len(self._meta_trend_history) > 1000:
self._meta_trend_history = self._meta_trend_history[-500:] # Keep last 500
# Log meta-trend changes
if self.enable_logging and self.current_meta_trend != self.previous_meta_trend:
logger.info(f"Meta-trend changed: {self.previous_meta_trend} -> {self.current_meta_trend} "
f"at {timestamp} (update #{self._update_count})")
logger.debug(f"Individual trends: {supertrend_results['trends']}")
# Update warmup status
if not self._is_warmed_up and self.supertrend_collection.is_warmed_up():
self._is_warmed_up = True
logger.info(f"Strategy warmed up after {self._update_count} data points")
except Exception as e:
logger.error(f"Error in calculate_on_data: {e}")
raise
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Returns:
bool: True (this strategy is fully incremental)
"""
return True
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate entry signal based on meta-trend direction change.
Entry occurs when meta-trend changes from != 1 to == 1, indicating
all Supertrend indicators now agree on upward direction.
Returns:
IncStrategySignal: Entry signal if trend aligns, hold signal otherwise
"""
if not self.is_warmed_up:
return IncStrategySignal.HOLD()
# Check for meta-trend entry condition
if self._check_entry_condition():
self._signal_count["entry"] += 1
self._last_entry_signal = {
'timestamp': self._last_update_time,
'meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"ENTRY SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['entry']})")
return IncStrategySignal.BUY(confidence=1.0, metadata={
"meta_trend": self.current_meta_trend,
"previous_meta_trend": self.previous_meta_trend,
"signal_count": self._signal_count["entry"]
})
return IncStrategySignal.HOLD()
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate exit signal based on meta-trend reversal.
Exit occurs when meta-trend changes from != -1 to == -1, indicating
trend reversal to downward direction.
Returns:
IncStrategySignal: Exit signal if trend reverses, hold signal otherwise
"""
if not self.is_warmed_up:
return IncStrategySignal.HOLD()
# Check for meta-trend exit condition
if self._check_exit_condition():
self._signal_count["exit"] += 1
self._last_exit_signal = {
'timestamp': self._last_update_time,
'meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"EXIT SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['exit']})")
return IncStrategySignal.SELL(confidence=1.0, metadata={
"type": "META_TREND_EXIT",
"meta_trend": self.current_meta_trend,
"previous_meta_trend": self.previous_meta_trend,
"signal_count": self._signal_count["exit"]
})
return IncStrategySignal.HOLD()
def get_confidence(self) -> float:
"""
Get strategy confidence based on meta-trend strength.
Higher confidence when meta-trend is strongly directional,
lower confidence during neutral periods.
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.is_warmed_up:
return 0.0
# High confidence for strong directional signals
if self.current_meta_trend == 1 or self.current_meta_trend == -1:
return 1.0
# Lower confidence for neutral trend
return 0.3
def _calculate_meta_trend(self, supertrend_results: Dict) -> int:
"""
Calculate meta-trend from SupertrendCollection results.
Meta-trend logic (matching original DefaultStrategy):
- All 3 Supertrends must agree for directional signal
- If all trends are the same, meta-trend = that trend
- If trends disagree, meta-trend = 0 (neutral)
Args:
supertrend_results: Results from SupertrendCollection.update()
Returns:
int: Meta-trend value (1, -1, or 0)
"""
trends = supertrend_results['trends']
# Check if all trends agree
if all(trend == trends[0] for trend in trends):
return trends[0] # All agree: return the common trend
else:
return 0 # Neutral when trends disagree
def _check_entry_condition(self) -> bool:
"""
Check if meta-trend entry condition is met.
Entry condition: meta-trend changes from != 1 to == 1
Returns:
bool: True if entry condition is met
"""
return (self.previous_meta_trend != 1 and
self.current_meta_trend == 1)
def _check_exit_condition(self) -> bool:
"""
Check if meta-trend exit condition is met.
Exit condition: meta-trend changes from != 1 to == -1
(Modified to match original strategy behavior)
Returns:
bool: True if exit condition is met
"""
return (self.previous_meta_trend != 1 and
self.current_meta_trend == -1)
def get_current_state_summary(self) -> Dict[str, Any]:
"""
Get detailed state summary for debugging and monitoring.
Returns:
Dict with current strategy state information
"""
base_summary = super().get_current_state_summary()
# Add MetaTrend-specific state
base_summary.update({
'primary_timeframe': self.primary_timeframe,
'current_meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'supertrend_collection_warmed_up': self.supertrend_collection.is_warmed_up(),
'supertrend_configs': self.supertrend_configs,
'signal_counts': self._signal_count.copy(),
'update_count': self._update_count,
'last_update_time': str(self._last_update_time) if self._last_update_time else None,
'meta_trend_history_length': len(self._meta_trend_history),
'last_entry_signal': self._last_entry_signal,
'last_exit_signal': self._last_exit_signal
})
# Add Supertrend collection state
if hasattr(self.supertrend_collection, 'get_state_summary'):
base_summary['supertrend_collection_state'] = self.supertrend_collection.get_state_summary()
return base_summary
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
super().reset_calculation_state()
# Reset Supertrend collection
self.supertrend_collection.reset()
# Reset meta-trend state
self.current_meta_trend = 0
self.previous_meta_trend = 0
self._meta_trend_history.clear()
# Reset signal state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Reset performance tracking
self._update_count = 0
self._last_update_time = None
logger.info("MetaTrendStrategy state reset")
def get_meta_trend_history(self, limit: Optional[int] = None) -> List[Dict]:
"""
Get meta-trend history for analysis.
Args:
limit: Maximum number of recent entries to return
Returns:
List of meta-trend history entries
"""
if limit is None:
return self._meta_trend_history.copy()
else:
return self._meta_trend_history[-limit:] if limit > 0 else []
def get_current_meta_trend(self) -> int:
"""
Get current meta-trend value.
Returns:
int: Current meta-trend (1, -1, or 0)
"""
return self.current_meta_trend
def get_individual_supertrend_states(self) -> List[Dict]:
"""
Get current state of individual Supertrend indicators.
Returns:
List of Supertrend state summaries
"""
if hasattr(self.supertrend_collection, 'get_state_summary'):
collection_state = self.supertrend_collection.get_state_summary()
return collection_state.get('supertrends', [])
return []
# Compatibility alias for easier imports
IncMetaTrendStrategy = MetaTrendStrategy

View File

@@ -1,336 +0,0 @@
"""
Incremental Random Strategy for Testing
This strategy generates random entry and exit signals for testing the incremental strategy system.
It's useful for verifying that the incremental strategy framework is working correctly.
"""
import random
import logging
import time
from typing import Dict, Optional, Any
import pandas as pd
from .base import IncStrategyBase, IncStrategySignal
logger = logging.getLogger(__name__)
class RandomStrategy(IncStrategyBase):
"""
Incremental random signal generator strategy for testing.
This strategy generates random entry and exit signals with configurable
probability and confidence levels. It's designed to test the incremental
strategy framework and signal processing system.
The incremental version maintains minimal state and processes each new
data point independently, making it ideal for testing real-time performance.
Parameters:
entry_probability: Probability of generating an entry signal (0.0-1.0)
exit_probability: Probability of generating an exit signal (0.0-1.0)
min_confidence: Minimum confidence level for signals
max_confidence: Maximum confidence level for signals
timeframe: Timeframe to operate on (default: "1min")
signal_frequency: How often to generate signals (every N bars)
random_seed: Optional seed for reproducible random signals
Example:
strategy = RandomStrategy(
name="random_test",
weight=1.0,
params={
"entry_probability": 0.1,
"exit_probability": 0.15,
"min_confidence": 0.7,
"max_confidence": 0.9,
"signal_frequency": 5,
"random_seed": 42 # For reproducible testing
}
)
"""
def __init__(self, name: str = "random", weight: float = 1.0, params: Optional[Dict] = None):
"""Initialize the incremental random strategy."""
super().__init__(name, weight, params)
# Strategy parameters with defaults
self.entry_probability = self.params.get("entry_probability", 0.05) # 5% chance per bar
self.exit_probability = self.params.get("exit_probability", 0.1) # 10% chance per bar
self.min_confidence = self.params.get("min_confidence", 0.6)
self.max_confidence = self.params.get("max_confidence", 0.9)
self.timeframe = self.params.get("timeframe", "1min")
self.signal_frequency = self.params.get("signal_frequency", 1) # Every bar
# Create separate random instance for this strategy
self._random = random.Random()
random_seed = self.params.get("random_seed")
if random_seed is not None:
self._random.seed(random_seed)
logger.info(f"RandomStrategy: Set random seed to {random_seed}")
# Internal state (minimal for random strategy)
self._bar_count = 0
self._last_signal_bar = -1
self._current_price = None
self._last_timestamp = None
logger.info(f"RandomStrategy initialized with entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
f"aggregation_enabled={self._timeframe_aggregator is not None}")
if self._timeframe_aggregator is not None:
logger.info(f"Using new timeframe utilities with mathematically correct aggregation")
logger.info(f"Random signals will be generated on complete {self.timeframe} bars only")
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for each timeframe.
Random strategy doesn't need any historical data for calculations,
so we only need 1 data point to start generating signals.
With the new base class timeframe aggregation, we only specify
our primary timeframe.
Returns:
Dict[str, int]: Minimal buffer requirements
"""
return {self.timeframe: 1} # Only need current data point
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Random strategy is ideal for incremental mode since it doesn't
depend on historical calculations.
Returns:
bool: Always True for random strategy
"""
return True
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
For random strategy, we just update our internal state with the
current price. The base class now handles timeframe aggregation
automatically, so we only receive data when a complete timeframe
bar is formed.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
start_time = time.perf_counter()
try:
# Update internal state - base class handles timeframe aggregation
self._current_price = new_data_point['close']
self._last_timestamp = timestamp
self._data_points_received += 1
# Increment bar count for each processed timeframe bar
self._bar_count += 1
# Debug logging every 10 bars
if self._bar_count % 10 == 0:
logger.debug(f"RandomStrategy: Processing bar {self._bar_count}, "
f"price=${self._current_price:.2f}, timestamp={timestamp}")
# Update warm-up status
if not self._is_warmed_up and self._data_points_received >= 1:
self._is_warmed_up = True
self._calculation_mode = "incremental"
logger.info(f"RandomStrategy: Warmed up after {self._data_points_received} data points")
# Record performance metrics
update_time = time.perf_counter() - start_time
self._performance_metrics['update_times'].append(update_time)
except Exception as e:
logger.error(f"RandomStrategy: Error in calculate_on_data: {e}")
self._performance_metrics['state_validation_failures'] += 1
raise
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate random entry signals based on current state.
Returns:
IncStrategySignal: Entry signal with confidence level
"""
if not self._is_warmed_up:
return IncStrategySignal.HOLD()
start_time = time.perf_counter()
try:
# Check if we should generate a signal based on frequency
if (self._bar_count - self._last_signal_bar) < self.signal_frequency:
return IncStrategySignal.HOLD()
# Generate random entry signal using strategy's random instance
random_value = self._random.random()
if random_value < self.entry_probability:
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
self._last_signal_bar = self._bar_count
logger.info(f"RandomStrategy: Generated ENTRY signal at bar {self._bar_count}, "
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
f"random_value={random_value:.3f}")
signal = IncStrategySignal.BUY(
confidence=confidence,
price=self._current_price,
metadata={
"strategy": "random",
"bar_count": self._bar_count,
"timeframe": self.timeframe,
"random_value": random_value,
"timestamp": self._last_timestamp
}
)
# Record performance metrics
signal_time = time.perf_counter() - start_time
self._performance_metrics['signal_generation_times'].append(signal_time)
return signal
return IncStrategySignal.HOLD()
except Exception as e:
logger.error(f"RandomStrategy: Error in get_entry_signal: {e}")
return IncStrategySignal.HOLD()
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate random exit signals based on current state.
Returns:
IncStrategySignal: Exit signal with confidence level
"""
if not self._is_warmed_up:
return IncStrategySignal.HOLD()
start_time = time.perf_counter()
try:
# Generate random exit signal using strategy's random instance
random_value = self._random.random()
if random_value < self.exit_probability:
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
# Randomly choose exit type
exit_types = ["SELL_SIGNAL", "TAKE_PROFIT", "STOP_LOSS"]
exit_type = self._random.choice(exit_types)
logger.info(f"RandomStrategy: Generated EXIT signal at bar {self._bar_count}, "
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
f"type={exit_type}, random_value={random_value:.3f}")
signal = IncStrategySignal.SELL(
confidence=confidence,
price=self._current_price,
metadata={
"type": exit_type,
"strategy": "random",
"bar_count": self._bar_count,
"timeframe": self.timeframe,
"random_value": random_value,
"timestamp": self._last_timestamp
}
)
# Record performance metrics
signal_time = time.perf_counter() - start_time
self._performance_metrics['signal_generation_times'].append(signal_time)
return signal
return IncStrategySignal.HOLD()
except Exception as e:
logger.error(f"RandomStrategy: Error in get_exit_signal: {e}")
return IncStrategySignal.HOLD()
def get_confidence(self) -> float:
"""
Return random confidence level for current market state.
Returns:
float: Random confidence level between min and max confidence
"""
if not self._is_warmed_up:
return 0.0
return self._random.uniform(self.min_confidence, self.max_confidence)
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
super().reset_calculation_state()
# Reset random strategy specific state
self._bar_count = 0
self._last_signal_bar = -1
self._current_price = None
self._last_timestamp = None
# Reset random state if seed was provided
random_seed = self.params.get("random_seed")
if random_seed is not None:
self._random.seed(random_seed)
logger.info("RandomStrategy: Calculation state reset")
def _reinitialize_from_buffers(self) -> None:
"""
Reinitialize indicators from available buffer data.
For random strategy, we just need to restore the current price
from the latest data point in the buffer.
"""
try:
# Get the latest data point from 1min buffer
buffer_1min = self._timeframe_buffers.get("1min")
if buffer_1min and len(buffer_1min) > 0:
latest_data = buffer_1min[-1]
self._current_price = latest_data['close']
self._last_timestamp = latest_data.get('timestamp')
self._bar_count = len(buffer_1min)
logger.info(f"RandomStrategy: Reinitialized from buffer with {self._bar_count} bars")
else:
logger.warning("RandomStrategy: No buffer data available for reinitialization")
except Exception as e:
logger.error(f"RandomStrategy: Error reinitializing from buffers: {e}")
raise
def get_current_state_summary(self) -> Dict[str, Any]:
"""Get summary of current calculation state for debugging."""
base_summary = super().get_current_state_summary()
base_summary.update({
'entry_probability': self.entry_probability,
'exit_probability': self.exit_probability,
'bar_count': self._bar_count,
'last_signal_bar': self._last_signal_bar,
'current_price': self._current_price,
'last_timestamp': self._last_timestamp,
'signal_frequency': self.signal_frequency,
'timeframe': self.timeframe
})
return base_summary
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"RandomStrategy(entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
f"mode={self._calculation_mode}, warmed_up={self._is_warmed_up}, "
f"bars={self._bar_count})")
# Compatibility alias for easier imports
IncRandomStrategy = RandomStrategy

View File

@@ -1,35 +0,0 @@
"""
Incremental Trading Execution
This module provides trading execution and position management for incremental strategies.
It handles real-time trade execution, risk management, and performance tracking.
Components:
- IncTrader: Main trader class for strategy execution
- PositionManager: Position state and trade execution management
- TradeRecord: Data structure for completed trades
- MarketFees: Fee calculation utilities
Example:
from IncrementalTrader.trader import IncTrader, PositionManager
from IncrementalTrader.strategies import MetaTrendStrategy
strategy = MetaTrendStrategy("metatrend")
trader = IncTrader(strategy, initial_usd=10000)
# Process data stream
for timestamp, ohlcv in data_stream:
trader.process_data_point(timestamp, ohlcv)
results = trader.get_results()
"""
from .trader import IncTrader
from .position import PositionManager, TradeRecord, MarketFees
__all__ = [
"IncTrader",
"PositionManager",
"TradeRecord",
"MarketFees",
]

View File

@@ -1,301 +0,0 @@
"""
Position Management for Incremental Trading
This module handles position state, balance tracking, and trade calculations
for the incremental trading system.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
from dataclasses import dataclass
import logging
logger = logging.getLogger(__name__)
@dataclass
class TradeRecord:
"""Record of a completed trade."""
entry_time: pd.Timestamp
exit_time: pd.Timestamp
entry_price: float
exit_price: float
entry_fee: float
exit_fee: float
profit_pct: float
exit_reason: str
strategy_name: str
class MarketFees:
"""Market fee calculations for different exchanges."""
@staticmethod
def calculate_okx_taker_maker_fee(amount: float, is_maker: bool = True) -> float:
"""Calculate OKX trading fees."""
fee_rate = 0.0008 if is_maker else 0.0010
return amount * fee_rate
@staticmethod
def calculate_binance_fee(amount: float, is_maker: bool = True) -> float:
"""Calculate Binance trading fees."""
fee_rate = 0.001 if is_maker else 0.001
return amount * fee_rate
class PositionManager:
"""
Manages trading position state and calculations.
This class handles:
- USD/coin balance tracking
- Position state management
- Trade execution calculations
- Fee calculations
- Performance metrics
"""
def __init__(self, initial_usd: float = 10000, fee_calculator=None):
"""
Initialize position manager.
Args:
initial_usd: Initial USD balance
fee_calculator: Fee calculation function (defaults to OKX)
"""
self.initial_usd = initial_usd
self.fee_calculator = fee_calculator or MarketFees.calculate_okx_taker_maker_fee
# Position state
self.usd = initial_usd
self.coin = 0.0
self.position = 0 # 0 = no position, 1 = long position
self.entry_price = 0.0
self.entry_time = None
# Performance tracking
self.max_balance = initial_usd
self.drawdowns = []
self.trade_records = []
logger.debug(f"PositionManager initialized with ${initial_usd}")
def is_in_position(self) -> bool:
"""Check if currently in a position."""
return self.position == 1
def get_current_balance(self, current_price: float) -> float:
"""Get current total balance value."""
if self.position == 0:
return self.usd
else:
return self.coin * current_price
def execute_entry(self, entry_price: float, timestamp: pd.Timestamp,
strategy_name: str) -> Dict[str, Any]:
"""
Execute entry trade.
Args:
entry_price: Entry price
timestamp: Entry timestamp
strategy_name: Name of the strategy
Returns:
Dict with entry details
"""
if self.position == 1:
raise ValueError("Cannot enter position: already in position")
# Calculate fees
entry_fee = self.fee_calculator(self.usd, is_maker=False)
usd_after_fee = self.usd - entry_fee
# Execute entry
self.coin = usd_after_fee / entry_price
self.entry_price = entry_price
self.entry_time = timestamp
self.usd = 0.0
self.position = 1
entry_details = {
'entry_price': entry_price,
'entry_time': timestamp,
'entry_fee': entry_fee,
'coin_amount': self.coin,
'strategy_name': strategy_name
}
logger.debug(f"ENTRY executed: ${entry_price:.2f}, fee=${entry_fee:.2f}")
return entry_details
def execute_exit(self, exit_price: float, timestamp: pd.Timestamp,
exit_reason: str, strategy_name: str) -> Dict[str, Any]:
"""
Execute exit trade.
Args:
exit_price: Exit price
timestamp: Exit timestamp
exit_reason: Reason for exit
strategy_name: Name of the strategy
Returns:
Dict with exit details and trade record
"""
if self.position == 0:
raise ValueError("Cannot exit position: not in position")
# Calculate exit
usd_gross = self.coin * exit_price
exit_fee = self.fee_calculator(usd_gross, is_maker=False)
self.usd = usd_gross - exit_fee
# Calculate profit
profit_pct = (exit_price - self.entry_price) / self.entry_price
# Calculate entry fee (for record keeping)
entry_fee = self.fee_calculator(self.coin * self.entry_price, is_maker=False)
# Create trade record
trade_record = TradeRecord(
entry_time=self.entry_time,
exit_time=timestamp,
entry_price=self.entry_price,
exit_price=exit_price,
entry_fee=entry_fee,
exit_fee=exit_fee,
profit_pct=profit_pct,
exit_reason=exit_reason,
strategy_name=strategy_name
)
self.trade_records.append(trade_record)
# Reset position
coin_amount = self.coin
self.coin = 0.0
self.position = 0
entry_price = self.entry_price
entry_time = self.entry_time
self.entry_price = 0.0
self.entry_time = None
exit_details = {
'exit_price': exit_price,
'exit_time': timestamp,
'exit_fee': exit_fee,
'profit_pct': profit_pct,
'exit_reason': exit_reason,
'trade_record': trade_record,
'final_usd': self.usd
}
logger.debug(f"EXIT executed: ${exit_price:.2f}, reason={exit_reason}, "
f"profit={profit_pct*100:.2f}%, fee=${exit_fee:.2f}")
return exit_details
def update_performance_metrics(self, current_price: float) -> None:
"""Update performance tracking metrics."""
current_balance = self.get_current_balance(current_price)
# Update max balance and drawdown
if current_balance > self.max_balance:
self.max_balance = current_balance
drawdown = (self.max_balance - current_balance) / self.max_balance
self.drawdowns.append(drawdown)
def check_stop_loss(self, current_price: float, stop_loss_pct: float) -> bool:
"""Check if stop loss should be triggered."""
if self.position == 0 or stop_loss_pct <= 0:
return False
stop_loss_price = self.entry_price * (1 - stop_loss_pct)
return current_price <= stop_loss_price
def check_take_profit(self, current_price: float, take_profit_pct: float) -> bool:
"""Check if take profit should be triggered."""
if self.position == 0 or take_profit_pct <= 0:
return False
take_profit_price = self.entry_price * (1 + take_profit_pct)
return current_price >= take_profit_price
def get_performance_summary(self) -> Dict[str, Any]:
"""Get performance summary statistics."""
final_balance = self.usd
n_trades = len(self.trade_records)
# Calculate statistics
if n_trades > 0:
profits = [trade.profit_pct for trade in self.trade_records]
wins = [p for p in profits if p > 0]
win_rate = len(wins) / n_trades
avg_trade = np.mean(profits)
total_fees = sum(trade.entry_fee + trade.exit_fee for trade in self.trade_records)
else:
win_rate = 0.0
avg_trade = 0.0
total_fees = 0.0
max_drawdown = max(self.drawdowns) if self.drawdowns else 0.0
profit_ratio = (final_balance - self.initial_usd) / self.initial_usd
return {
"initial_usd": self.initial_usd,
"final_usd": final_balance,
"profit_ratio": profit_ratio,
"n_trades": n_trades,
"win_rate": win_rate,
"max_drawdown": max_drawdown,
"avg_trade": avg_trade,
"total_fees_usd": total_fees
}
def get_trades_as_dicts(self) -> List[Dict[str, Any]]:
"""Convert trade records to dictionaries."""
trades = []
for trade in self.trade_records:
trades.append({
'entry_time': trade.entry_time,
'exit_time': trade.exit_time,
'entry': trade.entry_price,
'exit': trade.exit_price,
'profit_pct': trade.profit_pct,
'type': trade.exit_reason,
'fee_usd': trade.entry_fee + trade.exit_fee,
'strategy': trade.strategy_name
})
return trades
def get_current_state(self) -> Dict[str, Any]:
"""Get current position state."""
return {
"position": self.position,
"usd": self.usd,
"coin": self.coin,
"entry_price": self.entry_price,
"entry_time": self.entry_time,
"n_trades": len(self.trade_records),
"max_balance": self.max_balance
}
def reset(self) -> None:
"""Reset position manager to initial state."""
self.usd = self.initial_usd
self.coin = 0.0
self.position = 0
self.entry_price = 0.0
self.entry_time = None
self.max_balance = self.initial_usd
self.drawdowns.clear()
self.trade_records.clear()
logger.debug("PositionManager reset to initial state")
def __repr__(self) -> str:
"""String representation of position manager."""
return (f"PositionManager(position={self.position}, "
f"usd=${self.usd:.2f}, coin={self.coin:.6f}, "
f"trades={len(self.trade_records)})")

View File

@@ -1,301 +0,0 @@
"""
Incremental Trader for backtesting incremental strategies.
This module provides the IncTrader class that manages a single incremental strategy
during backtesting, handling strategy execution, trade decisions, and performance tracking.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
import logging
# Use try/except for imports to handle both relative and absolute import scenarios
try:
from ..strategies.base import IncStrategyBase, IncStrategySignal
from .position import PositionManager, TradeRecord
except ImportError:
# Fallback for direct execution
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from strategies.base import IncStrategyBase, IncStrategySignal
from position import PositionManager, TradeRecord
logger = logging.getLogger(__name__)
class IncTrader:
"""
Incremental trader that manages a single strategy during backtesting.
This class handles:
- Strategy initialization and data feeding
- Trade decision logic based on strategy signals
- Risk management (stop loss, take profit)
- Performance tracking and metrics collection
The trader processes data points sequentially, feeding them to the strategy
and executing trades based on the generated signals.
Example:
from IncrementalTrader.strategies import MetaTrendStrategy
from IncrementalTrader.trader import IncTrader
strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"})
trader = IncTrader(
strategy=strategy,
initial_usd=10000,
params={"stop_loss_pct": 0.02}
)
# Process data sequentially
for timestamp, ohlcv_data in data_stream:
trader.process_data_point(timestamp, ohlcv_data)
# Get results
results = trader.get_results()
"""
def __init__(self, strategy: IncStrategyBase, initial_usd: float = 10000,
params: Optional[Dict] = None):
"""
Initialize the incremental trader.
Args:
strategy: Incremental strategy instance
initial_usd: Initial USD balance
params: Trader parameters (stop_loss_pct, take_profit_pct, etc.)
"""
self.strategy = strategy
self.initial_usd = initial_usd
self.params = params or {}
# Initialize position manager
self.position_manager = PositionManager(initial_usd)
# Current state
self.current_timestamp = None
self.current_price = None
# Strategy state tracking
self.data_points_processed = 0
self.warmup_complete = False
# Risk management parameters
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.0)
self.take_profit_pct = self.params.get("take_profit_pct", 0.0)
# Performance tracking
self.portfolio_history = []
logger.info(f"IncTrader initialized: strategy={strategy.name}, "
f"initial_usd=${initial_usd}, stop_loss={self.stop_loss_pct*100:.1f}%")
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Process a single data point through the strategy and handle trading logic.
Args:
timestamp: Data point timestamp
ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume
"""
self.current_timestamp = timestamp
self.current_price = ohlcv_data['close']
self.data_points_processed += 1
try:
# Feed data to strategy and get signal
signal = self.strategy.process_data_point(timestamp, ohlcv_data)
# Check if strategy is warmed up
if not self.warmup_complete and self.strategy.is_warmed_up:
self.warmup_complete = True
logger.info(f"Strategy {self.strategy.name} warmed up after "
f"{self.data_points_processed} data points")
# Only process signals if strategy is warmed up
if self.warmup_complete:
self._process_trading_logic(signal)
# Update performance tracking
self._update_performance_tracking()
except Exception as e:
logger.error(f"Error processing data point at {timestamp}: {e}")
raise
def _process_trading_logic(self, signal: Optional[IncStrategySignal]) -> None:
"""Process trading logic based on current position and strategy signals."""
if not self.position_manager.is_in_position():
# No position - check for entry signals
self._check_entry_signals(signal)
else:
# In position - check for exit signals
self._check_exit_signals(signal)
def _check_entry_signals(self, signal: Optional[IncStrategySignal]) -> None:
"""Check for entry signals when not in position."""
try:
# Check if we have a valid entry signal
if signal and signal.signal_type == "ENTRY" and signal.confidence > 0:
self._execute_entry(signal)
except Exception as e:
logger.error(f"Error checking entry signals: {e}")
def _check_exit_signals(self, signal: Optional[IncStrategySignal]) -> None:
"""Check for exit signals when in position."""
try:
# Check strategy exit signals first
if signal and signal.signal_type == "EXIT" and signal.confidence > 0:
exit_reason = signal.metadata.get("type", "STRATEGY_EXIT")
exit_price = signal.price if signal.price else self.current_price
self._execute_exit(exit_reason, exit_price)
return
# Check stop loss
if self.position_manager.check_stop_loss(self.current_price, self.stop_loss_pct):
self._execute_exit("STOP_LOSS", self.current_price)
return
# Check take profit
if self.position_manager.check_take_profit(self.current_price, self.take_profit_pct):
self._execute_exit("TAKE_PROFIT", self.current_price)
return
except Exception as e:
logger.error(f"Error checking exit signals: {e}")
def _execute_entry(self, signal: IncStrategySignal) -> None:
"""Execute entry trade."""
entry_price = signal.price if signal.price else self.current_price
try:
entry_details = self.position_manager.execute_entry(
entry_price, self.current_timestamp, self.strategy.name
)
logger.info(f"ENTRY: {self.strategy.name} at ${entry_price:.2f}, "
f"confidence={signal.confidence:.2f}, "
f"fee=${entry_details['entry_fee']:.2f}")
except Exception as e:
logger.error(f"Error executing entry: {e}")
raise
def _execute_exit(self, exit_reason: str, exit_price: Optional[float] = None) -> None:
"""Execute exit trade."""
exit_price = exit_price if exit_price else self.current_price
try:
exit_details = self.position_manager.execute_exit(
exit_price, self.current_timestamp, exit_reason, self.strategy.name
)
logger.info(f"EXIT: {self.strategy.name} at ${exit_price:.2f}, "
f"reason={exit_reason}, "
f"profit={exit_details['profit_pct']*100:.2f}%, "
f"fee=${exit_details['exit_fee']:.2f}")
except Exception as e:
logger.error(f"Error executing exit: {e}")
raise
def _update_performance_tracking(self) -> None:
"""Update performance tracking metrics."""
# Update position manager metrics
self.position_manager.update_performance_metrics(self.current_price)
# Track portfolio value over time
current_balance = self.position_manager.get_current_balance(self.current_price)
self.portfolio_history.append({
'timestamp': self.current_timestamp,
'balance': current_balance,
'price': self.current_price,
'position': self.position_manager.position
})
def finalize(self) -> None:
"""Finalize trading session (close any open positions)."""
if self.position_manager.is_in_position():
self._execute_exit("EOD", self.current_price)
logger.info(f"Closed final position for {self.strategy.name} at EOD")
def get_results(self) -> Dict[str, Any]:
"""
Get comprehensive trading results.
Returns:
Dict containing performance metrics, trade records, and statistics
"""
# Get performance summary from position manager
performance = self.position_manager.get_performance_summary()
# Get trades as dictionaries
trades = self.position_manager.get_trades_as_dicts()
# Build comprehensive results
results = {
"strategy_name": self.strategy.name,
"strategy_params": self.strategy.params,
"trader_params": self.params,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"trades": trades,
"portfolio_history": self.portfolio_history,
**performance # Include all performance metrics
}
# Add first and last trade info if available
if len(trades) > 0:
results["first_trade"] = {
"entry_time": trades[0]["entry_time"],
"entry": trades[0]["entry"]
}
results["last_trade"] = {
"exit_time": trades[-1]["exit_time"],
"exit": trades[-1]["exit"]
}
# Add final balance for compatibility
results["final_balance"] = performance["final_usd"]
return results
def get_current_state(self) -> Dict[str, Any]:
"""Get current trader state for debugging."""
position_state = self.position_manager.get_current_state()
return {
"strategy": self.strategy.name,
"current_price": self.current_price,
"current_timestamp": self.current_timestamp,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"strategy_state": self.strategy.get_current_state_summary(),
**position_state # Include all position state
}
def get_portfolio_value(self) -> float:
"""Get current portfolio value."""
return self.position_manager.get_current_balance(self.current_price)
def reset(self) -> None:
"""Reset trader to initial state."""
self.position_manager.reset()
self.strategy.reset_calculation_state()
self.current_timestamp = None
self.current_price = None
self.data_points_processed = 0
self.warmup_complete = False
self.portfolio_history.clear()
logger.info(f"IncTrader reset for strategy {self.strategy.name}")
def __repr__(self) -> str:
"""String representation of the trader."""
return (f"IncTrader(strategy={self.strategy.name}, "
f"position={self.position_manager.position}, "
f"balance=${self.position_manager.get_current_balance(self.current_price or 0):.2f}, "
f"trades={len(self.position_manager.trade_records)})")

View File

@@ -1,23 +0,0 @@
"""
Utility modules for the IncrementalTrader framework.
This package contains utility functions and classes that support the core
trading functionality, including timeframe aggregation, data management,
and helper utilities.
"""
from .timeframe_utils import (
aggregate_minute_data_to_timeframe,
parse_timeframe_to_minutes,
get_latest_complete_bar,
MinuteDataBuffer,
TimeframeError
)
__all__ = [
'aggregate_minute_data_to_timeframe',
'parse_timeframe_to_minutes',
'get_latest_complete_bar',
'MinuteDataBuffer',
'TimeframeError'
]

View File

@@ -1,455 +0,0 @@
"""
Timeframe aggregation utilities for the IncrementalTrader framework.
This module provides utilities for aggregating minute-level OHLCV data to higher
timeframes with mathematical correctness and proper timestamp handling.
Key Features:
- Uses pandas resampling for mathematical correctness
- Supports bar end timestamps (default) to prevent future data leakage
- Proper OHLCV aggregation rules (first/max/min/last/sum)
- MinuteDataBuffer for efficient real-time data management
- Comprehensive error handling and validation
Critical Fixes:
1. Bar timestamps represent END of period (no future data leakage)
2. Correct OHLCV aggregation matching pandas resampling
3. Proper handling of incomplete bars and edge cases
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Union, Any
from collections import deque
import logging
import re
logger = logging.getLogger(__name__)
class TimeframeError(Exception):
"""Exception raised for timeframe-related errors."""
pass
def parse_timeframe_to_minutes(timeframe: str) -> int:
"""
Parse timeframe string to minutes.
Args:
timeframe: Timeframe string (e.g., "1min", "5min", "15min", "1h", "4h", "1d")
Returns:
Number of minutes in the timeframe
Raises:
TimeframeError: If timeframe format is invalid
Examples:
>>> parse_timeframe_to_minutes("15min")
15
>>> parse_timeframe_to_minutes("1h")
60
>>> parse_timeframe_to_minutes("1d")
1440
"""
if not isinstance(timeframe, str):
raise TimeframeError(f"Timeframe must be a string, got {type(timeframe)}")
timeframe = timeframe.lower().strip()
# Handle common timeframe formats
patterns = {
r'^(\d+)min$': lambda m: int(m.group(1)),
r'^(\d+)h$': lambda m: int(m.group(1)) * 60,
r'^(\d+)d$': lambda m: int(m.group(1)) * 1440,
r'^(\d+)w$': lambda m: int(m.group(1)) * 10080, # 7 * 24 * 60
}
for pattern, converter in patterns.items():
match = re.match(pattern, timeframe)
if match:
minutes = converter(match)
if minutes <= 0:
raise TimeframeError(f"Timeframe must be positive, got {minutes} minutes")
return minutes
raise TimeframeError(f"Invalid timeframe format: {timeframe}. "
f"Supported formats: Nmin, Nh, Nd, Nw (e.g., 15min, 1h, 1d)")
def aggregate_minute_data_to_timeframe(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> List[Dict[str, Union[float, pd.Timestamp]]]:
"""
Aggregate minute-level OHLCV data to specified timeframe using pandas resampling.
This function provides mathematically correct aggregation that matches pandas
resampling behavior, with proper timestamp handling to prevent future data leakage.
Args:
minute_data: List of minute OHLCV dictionaries with 'timestamp' field
timeframe: Target timeframe ("1min", "5min", "15min", "1h", "4h", "1d")
timestamp_mode: "end" (default) for bar end timestamps, "start" for bar start
Returns:
List of aggregated OHLCV dictionaries with proper timestamps
Raises:
TimeframeError: If timeframe format is invalid or data is malformed
ValueError: If minute_data is empty or contains invalid data
Examples:
>>> minute_data = [
... {'timestamp': pd.Timestamp('2024-01-01 09:00'), 'open': 100, 'high': 102, 'low': 99, 'close': 101, 'volume': 1000},
... {'timestamp': pd.Timestamp('2024-01-01 09:01'), 'open': 101, 'high': 103, 'low': 100, 'close': 102, 'volume': 1200},
... ]
>>> result = aggregate_minute_data_to_timeframe(minute_data, "15min")
>>> len(result)
1
>>> result[0]['timestamp'] # Bar end timestamp
Timestamp('2024-01-01 09:15:00')
"""
if not minute_data:
return []
if not isinstance(minute_data, list):
raise ValueError("minute_data must be a list of dictionaries")
if timestamp_mode not in ["end", "start"]:
raise ValueError("timestamp_mode must be 'end' or 'start'")
# Validate timeframe
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
# If requesting 1min data, return as-is (with timestamp mode adjustment)
if timeframe_minutes == 1:
if timestamp_mode == "end":
# Adjust timestamps to represent bar end (add 1 minute)
result = []
for data_point in minute_data:
adjusted_point = data_point.copy()
adjusted_point['timestamp'] = data_point['timestamp'] + pd.Timedelta(minutes=1)
result.append(adjusted_point)
return result
else:
return minute_data.copy()
# Validate data structure
required_fields = ['timestamp', 'open', 'high', 'low', 'close', 'volume']
for i, data_point in enumerate(minute_data):
if not isinstance(data_point, dict):
raise ValueError(f"Data point {i} must be a dictionary")
for field in required_fields:
if field not in data_point:
raise ValueError(f"Data point {i} missing required field: {field}")
# Validate timestamp
if not isinstance(data_point['timestamp'], pd.Timestamp):
try:
data_point['timestamp'] = pd.Timestamp(data_point['timestamp'])
except Exception as e:
raise ValueError(f"Invalid timestamp in data point {i}: {e}")
try:
# Convert to DataFrame for pandas resampling
df = pd.DataFrame(minute_data)
df = df.set_index('timestamp')
# Sort by timestamp to ensure proper ordering
df = df.sort_index()
# Use pandas resampling for mathematical correctness
freq_str = f'{timeframe_minutes}min'
# Use trading industry standard grouping: label='left', closed='left'
# This means 5min bar starting at 09:00 includes minutes 09:00-09:04
resampled = df.resample(freq_str, label='left', closed='left').agg({
'open': 'first', # First open in the period
'high': 'max', # Maximum high in the period
'low': 'min', # Minimum low in the period
'close': 'last', # Last close in the period
'volume': 'sum' # Sum of volume in the period
})
# Remove any rows with NaN values (incomplete periods)
resampled = resampled.dropna()
# Convert back to list of dictionaries
result = []
for timestamp, row in resampled.iterrows():
# Adjust timestamp based on mode
if timestamp_mode == "end":
# Convert bar start timestamp to bar end timestamp
bar_end_timestamp = timestamp + pd.Timedelta(minutes=timeframe_minutes)
final_timestamp = bar_end_timestamp
else:
# Keep bar start timestamp
final_timestamp = timestamp
result.append({
'timestamp': final_timestamp,
'open': float(row['open']),
'high': float(row['high']),
'low': float(row['low']),
'close': float(row['close']),
'volume': float(row['volume'])
})
return result
except Exception as e:
raise TimeframeError(f"Failed to aggregate data to {timeframe}: {e}")
def get_latest_complete_bar(
minute_data: List[Dict[str, Union[float, pd.Timestamp]]],
timeframe: str,
timestamp_mode: str = "end"
) -> Optional[Dict[str, Union[float, pd.Timestamp]]]:
"""
Get the latest complete bar from minute data for the specified timeframe.
This function is useful for real-time processing where you only want to
process complete bars and avoid using incomplete/future data.
Args:
minute_data: List of minute OHLCV dictionaries with 'timestamp' field
timeframe: Target timeframe ("1min", "5min", "15min", "1h", "4h", "1d")
timestamp_mode: "end" (default) for bar end timestamps, "start" for bar start
Returns:
Latest complete bar dictionary, or None if no complete bars available
Examples:
>>> minute_data = [...] # 30 minutes of data
>>> latest_15m = get_latest_complete_bar(minute_data, "15min")
>>> latest_15m['timestamp'] # Will be 15 minutes ago (complete bar)
"""
if not minute_data:
return None
# Get all aggregated bars
aggregated_bars = aggregate_minute_data_to_timeframe(minute_data, timeframe, timestamp_mode)
if not aggregated_bars:
return None
# For real-time processing, we need to ensure the bar is truly complete
# This means the bar's end time should be before the current time
latest_minute_timestamp = max(data['timestamp'] for data in minute_data)
# Filter out incomplete bars
complete_bars = []
for bar in aggregated_bars:
if timestamp_mode == "end":
# Bar timestamp is the end time, so it should be <= latest minute + 1 minute
if bar['timestamp'] <= latest_minute_timestamp + pd.Timedelta(minutes=1):
complete_bars.append(bar)
else:
# Bar timestamp is the start time, check if enough time has passed
timeframe_minutes = parse_timeframe_to_minutes(timeframe)
bar_end_time = bar['timestamp'] + pd.Timedelta(minutes=timeframe_minutes)
if bar_end_time <= latest_minute_timestamp + pd.Timedelta(minutes=1):
complete_bars.append(bar)
return complete_bars[-1] if complete_bars else None
class MinuteDataBuffer:
"""
Helper class for managing minute data buffers in real-time strategies.
This class provides efficient buffer management for minute-level data with
automatic aggregation capabilities. It's designed for use in incremental
strategies that need to maintain a rolling window of minute data.
Features:
- Automatic buffer size management with configurable limits
- Efficient data access and aggregation methods
- Memory-bounded operation (doesn't grow indefinitely)
- Thread-safe operations for real-time use
- Comprehensive validation and error handling
Example:
>>> buffer = MinuteDataBuffer(max_size=1440) # 24 hours
>>> buffer.add(timestamp, {'open': 100, 'high': 102, 'low': 99, 'close': 101, 'volume': 1000})
>>> bars_15m = buffer.aggregate_to_timeframe("15min", lookback_bars=4)
>>> latest_bar = buffer.get_latest_complete_bar("15min")
"""
def __init__(self, max_size: int = 1440):
"""
Initialize minute data buffer.
Args:
max_size: Maximum number of minute data points to keep (default: 1440 = 24 hours)
"""
if max_size <= 0:
raise ValueError("max_size must be positive")
self.max_size = max_size
self._buffer = deque(maxlen=max_size)
self._last_timestamp = None
logger.debug(f"Initialized MinuteDataBuffer with max_size={max_size}")
def add(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Add new minute data point to the buffer.
Args:
timestamp: Timestamp of the data point
ohlcv_data: OHLCV data dictionary (open, high, low, close, volume)
Raises:
ValueError: If data is invalid or timestamp is out of order
"""
if not isinstance(timestamp, pd.Timestamp):
try:
timestamp = pd.Timestamp(timestamp)
except Exception as e:
raise ValueError(f"Invalid timestamp: {e}")
# Validate OHLCV data
required_fields = ['open', 'high', 'low', 'close', 'volume']
for field in required_fields:
if field not in ohlcv_data:
raise ValueError(f"Missing required field: {field}")
if not isinstance(ohlcv_data[field], (int, float)):
raise ValueError(f"Field {field} must be numeric, got {type(ohlcv_data[field])}")
# Check timestamp ordering (allow equal timestamps for updates)
if self._last_timestamp is not None and timestamp < self._last_timestamp:
logger.warning(f"Out-of-order timestamp: {timestamp} < {self._last_timestamp}")
# Create data point
data_point = ohlcv_data.copy()
data_point['timestamp'] = timestamp
# Add to buffer
self._buffer.append(data_point)
self._last_timestamp = timestamp
logger.debug(f"Added data point at {timestamp}, buffer size: {len(self._buffer)}")
def get_data(self, lookback_minutes: Optional[int] = None) -> List[Dict[str, Union[float, pd.Timestamp]]]:
"""
Get data from buffer.
Args:
lookback_minutes: Number of minutes to look back (None for all data)
Returns:
List of minute data dictionaries
"""
if not self._buffer:
return []
if lookback_minutes is None:
return list(self._buffer)
if lookback_minutes <= 0:
raise ValueError("lookback_minutes must be positive")
# Get data from the last N minutes
if len(self._buffer) <= lookback_minutes:
return list(self._buffer)
return list(self._buffer)[-lookback_minutes:]
def aggregate_to_timeframe(
self,
timeframe: str,
lookback_bars: Optional[int] = None,
timestamp_mode: str = "end"
) -> List[Dict[str, Union[float, pd.Timestamp]]]:
"""
Aggregate buffer data to specified timeframe.
Args:
timeframe: Target timeframe ("5min", "15min", "1h", etc.)
lookback_bars: Number of bars to return (None for all available)
timestamp_mode: "end" (default) for bar end timestamps, "start" for bar start
Returns:
List of aggregated OHLCV bars
"""
if not self._buffer:
return []
# Get all buffer data
minute_data = list(self._buffer)
# Aggregate to timeframe
aggregated_bars = aggregate_minute_data_to_timeframe(minute_data, timeframe, timestamp_mode)
# Apply lookback limit
if lookback_bars is not None and lookback_bars > 0:
aggregated_bars = aggregated_bars[-lookback_bars:]
return aggregated_bars
def get_latest_complete_bar(
self,
timeframe: str,
timestamp_mode: str = "end"
) -> Optional[Dict[str, Union[float, pd.Timestamp]]]:
"""
Get the latest complete bar for the specified timeframe.
Args:
timeframe: Target timeframe ("5min", "15min", "1h", etc.)
timestamp_mode: "end" (default) for bar end timestamps, "start" for bar start
Returns:
Latest complete bar dictionary, or None if no complete bars available
"""
if not self._buffer:
return None
minute_data = list(self._buffer)
return get_latest_complete_bar(minute_data, timeframe, timestamp_mode)
def size(self) -> int:
"""Get current buffer size."""
return len(self._buffer)
def is_full(self) -> bool:
"""Check if buffer is at maximum capacity."""
return len(self._buffer) >= self.max_size
def clear(self) -> None:
"""Clear all data from buffer."""
self._buffer.clear()
self._last_timestamp = None
logger.debug("Buffer cleared")
def get_time_range(self) -> Optional[tuple]:
"""
Get the time range of data in the buffer.
Returns:
Tuple of (start_time, end_time) or None if buffer is empty
"""
if not self._buffer:
return None
timestamps = [data['timestamp'] for data in self._buffer]
return (min(timestamps), max(timestamps))
def __len__(self) -> int:
"""Get buffer size."""
return len(self._buffer)
def __repr__(self) -> str:
"""String representation of buffer."""
time_range = self.get_time_range()
if time_range:
start, end = time_range
return f"MinuteDataBuffer(size={len(self._buffer)}, range={start} to {end})"
else:
return f"MinuteDataBuffer(size=0, empty)"

611
README.md
View File

@@ -1,177 +1,512 @@
# Cycles - Advanced Trading Strategy Backtesting Framework
# Cycles - Cryptocurrency Trading Strategy Backtesting Framework
A sophisticated Python framework for backtesting cryptocurrency trading strategies with multi-timeframe analysis, strategy combination, and advanced signal processing.
A comprehensive Python framework for backtesting cryptocurrency trading strategies using technical indicators, with advanced features like machine learning price prediction to eliminate lookahead bias.
## Table of Contents
- [Overview](#overview)
- [Features](#features)
- [Quick Start](#quick-start)
- [Project Structure](#project-structure)
- [Core Modules](#core-modules)
- [Configuration](#configuration)
- [Usage Examples](#usage-examples)
- [API Documentation](#api-documentation)
- [Testing](#testing)
- [Contributing](#contributing)
- [License](#license)
## Overview
Cycles is a sophisticated backtesting framework designed specifically for cryptocurrency trading strategies. It provides robust tools for:
- **Strategy Backtesting**: Test trading strategies across multiple timeframes with comprehensive metrics
- **Technical Analysis**: Built-in indicators including SuperTrend, RSI, Bollinger Bands, and more
- **Machine Learning Integration**: Eliminate lookahead bias using XGBoost price prediction
- **Multi-timeframe Analysis**: Support for various timeframes from 1-minute to daily data
- **Performance Analytics**: Detailed reporting with profit ratios, drawdowns, win rates, and fee calculations
### Key Goals
1. **Realistic Trading Simulation**: Eliminate common backtesting pitfalls like lookahead bias
2. **Modular Architecture**: Easy to extend with new indicators and strategies
3. **Performance Optimization**: Parallel processing for efficient large-scale backtesting
4. **Comprehensive Analysis**: Rich reporting and visualization capabilities
## Features
- **Multi-Strategy Architecture**: Combine multiple trading strategies with configurable weights and rules
- **Multi-Timeframe Analysis**: Strategies can operate on different timeframes (1min, 5min, 15min, 1h, etc.)
- **Advanced Strategies**:
- **Default Strategy**: Meta-trend analysis using multiple Supertrend indicators
- **BBRS Strategy**: Bollinger Bands + RSI with market regime detection
- **Flexible Signal Combination**: Weighted consensus, majority voting, any/all combinations
- **Precise Stop-Loss**: 1-minute precision for accurate risk management
- **Comprehensive Backtesting**: Detailed performance metrics and trade analysis
- **Data Visualization**: Interactive charts and performance plots
### 🚀 Core Features
- **Multi-Strategy Backtesting**: Test multiple trading strategies simultaneously
- **Advanced Stop Loss Management**: Precise stop-loss execution using 1-minute data
- **Fee Integration**: Realistic trading fee calculations (OKX exchange fees)
- **Parallel Processing**: Efficient multi-core backtesting execution
- **Rich Analytics**: Comprehensive performance metrics and reporting
### 📊 Technical Indicators
- **SuperTrend**: Multi-parameter SuperTrend indicator with meta-trend analysis
- **RSI**: Relative Strength Index with customizable periods
- **Bollinger Bands**: Configurable period and standard deviation multipliers
- **Extensible Framework**: Easy to add new technical indicators
### 🤖 Machine Learning
- **Price Prediction**: XGBoost-based closing price prediction
- **Lookahead Bias Elimination**: Realistic trading simulations
- **Feature Engineering**: Advanced technical feature extraction
- **Model Persistence**: Save and load trained models
### 📈 Data Management
- **Multiple Data Sources**: Support for various cryptocurrency exchanges
- **Flexible Timeframes**: 1-minute to daily data aggregation
- **Efficient Storage**: Optimized data loading and caching
- **Google Sheets Integration**: External data source connectivity
## Quick Start
### Prerequisites
- Python 3.8+
- [uv](https://github.com/astral-sh/uv) package manager (recommended)
- Python 3.10 or higher
- UV package manager (recommended)
- Git
### Installation
```bash
# Clone the repository
git clone <repository-url>
cd Cycles
1. **Clone the repository**:
```bash
git clone <repository-url>
cd Cycles
```
# Install dependencies with uv
uv sync
2. **Install dependencies**:
```bash
uv sync
```
# Or install with pip
pip install -r requirements.txt
```
3. **Activate virtual environment**:
```bash
source .venv/bin/activate # Linux/Mac
# or
.venv\Scripts\activate # Windows
```
### Running Backtests
### Basic Usage
Use the `uv run` command to execute backtests with different configurations:
1. **Prepare your configuration file** (`config.json`):
```json
{
"start_date": "2023-01-01",
"stop_date": "2023-12-31",
"initial_usd": 10000,
"timeframes": ["5T", "15T", "1H", "4H"],
"stop_loss_pcts": [0.02, 0.05, 0.10]
}
```
```bash
# Run default strategy on 5-minute timeframe
uv run .\main.py .\configs\config_default_5min.json
2. **Run a backtest**:
```bash
uv run python main.py --config config.json
```
# Run default strategy on 15-minute timeframe
uv run .\main.py .\configs\config_default.json
# Run BBRS strategy with market regime detection
uv run .\main.py .\configs\config_bbrs.json
# Run combined strategies
uv run .\main.py .\configs\config_combined.json
```
### Configuration Examples
#### Default Strategy (5-minute timeframe)
```bash
uv run .\main.py .\configs\config_default_5min.json
```
#### BBRS Strategy with Multi-timeframe Analysis
```bash
uv run .\main.py .\configs\config_bbrs_multi_timeframe.json
```
#### Combined Strategies with Weighted Consensus
```bash
uv run .\main.py .\configs\config_combined.json
```
## Configuration
Strategies are configured using JSON files in the `configs/` directory:
```json
{
"start_date": "2024-01-01",
"stop_date": "2024-01-31",
"initial_usd": 10000,
"timeframes": ["15min"],
"stop_loss_pcts": [0.03, 0.05],
"strategies": [
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "15min"
}
}
],
"combination_rules": {
"entry": "any",
"exit": "any",
"min_confidence": 0.5
}
}
```
### Available Strategies
1. **Default Strategy**: Meta-trend analysis using Supertrend indicators
2. **BBRS Strategy**: Bollinger Bands + RSI with market regime detection
### Combination Rules
- **Entry**: `any`, `all`, `majority`, `weighted_consensus`
- **Exit**: `any`, `all`, `priority` (prioritizes stop-loss signals)
3. **View results**:
Results will be saved in timestamped CSV files with comprehensive metrics.
## Project Structure
```
Cycles/
├── configs/ # Configuration files
├── cycles/ # Core framework
│ ├── strategies/ # Strategy implementation
│ │ ├── base.py # Base strategy classes
│ │ ── default_strategy.py
│ ├── bbrs_strategy.py
│ │ ── manager.py # Strategy manager
│ ├── Analysis/ # Technical analysis
│ ├── utils/ # Utilities
│ └── charts.py # Visualization
├── docs/ # Documentation
├── data/ # Market data
├── results/ # Backtest results
└── main.py # Main entry point
├── cycles/ # Core library modules
│ ├── Analysis/ # Technical analysis indicators
│ ├── boillinger_band.py
│ │ ├── rsi.py
│ │ ── __init__.py
│ ├── utils/ # Utility modules
│ │ ── storage.py # Data storage and management
│ ├── system.py # System utilities
│ ├── data_utils.py # Data processing utilities
│ └── gsheets.py # Google Sheets integration
│ ├── backtest.py # Core backtesting engine
│ ├── supertrend.py # SuperTrend indicator implementation
│ ├── charts.py # Visualization utilities
│ ├── market_fees.py # Trading fee calculations
│ └── __init__.py
├── docs/ # Documentation
│ ├── analysis.md # Analysis module documentation
│ ├── utils_storage.md # Storage utilities documentation
│ └── utils_system.md # System utilities documentation
├── data/ # Data directory (not in repo)
├── results/ # Backtest results (not in repo)
├── xgboost/ # Machine learning components
├── OHLCVPredictor/ # Price prediction module
├── main.py # Main execution script
├── test_bbrsi.py # Example strategy test
├── pyproject.toml # Project configuration
├── requirements.txt # Dependencies
├── uv.lock # UV lock file
└── README.md # This file
```
## Documentation
## Core Modules
Detailed documentation is available in the `docs/` directory:
### Backtest Engine (`cycles/backtest.py`)
- **[Strategy Manager](./docs/strategy_manager.md)** - Multi-strategy orchestration and signal combination
- **[Strategies](./docs/strategies.md)** - Individual strategy implementations and usage
- **[Timeframe System](./docs/timeframe_system.md)** - Advanced timeframe management and multi-timeframe strategies
- **[Analysis](./docs/analysis.md)** - Technical analysis components
- **[Storage Utils](./docs/utils_storage.md)** - Data storage and retrieval
- **[System Utils](./docs/utils_system.md)** - System utilities
The heart of the framework, providing comprehensive backtesting capabilities:
## Examples
```python
from cycles.backtest import Backtest
results = Backtest.run(
min1_df=minute_data,
df=timeframe_data,
initial_usd=10000,
stop_loss_pct=0.05,
debug=False
)
```
**Key Features**:
- Meta-SuperTrend strategy implementation
- Precise stop-loss execution using 1-minute data
- Comprehensive trade logging and statistics
- Fee-aware profit calculations
### Technical Analysis (`cycles/Analysis/`)
Modular technical indicator implementations:
#### RSI (Relative Strength Index)
```python
from cycles.Analysis.rsi import RSI
rsi_calculator = RSI(period=14)
data_with_rsi = rsi_calculator.calculate(df, price_column='close')
```
#### Bollinger Bands
```python
from cycles.Analysis.boillinger_band import BollingerBands
bb = BollingerBands(period=20, std_dev_multiplier=2.0)
data_with_bb = bb.calculate(df)
```
### Data Management (`cycles/utils/storage.py`)
Efficient data loading, processing, and result storage:
```python
from cycles.utils.storage import Storage
storage = Storage(data_dir='./data', logging=logging)
data = storage.load_data('btcusd_1-min_data.csv', '2023-01-01', '2023-12-31')
```
## Configuration
### Backtest Configuration
Create a `config.json` file with the following structure:
```json
{
"start_date": "2023-01-01",
"stop_date": "2023-12-31",
"initial_usd": 10000,
"timeframes": [
"1T", // 1 minute
"5T", // 5 minutes
"15T", // 15 minutes
"1H", // 1 hour
"4H", // 4 hours
"1D" // 1 day
],
"stop_loss_pcts": [0.02, 0.05, 0.10, 0.15]
}
```
### Environment Variables
Set the following environment variables for enhanced functionality:
### Single Strategy Backtest
```bash
# Test default strategy on different timeframes
uv run .\main.py .\configs\config_default.json # 15min
uv run .\main.py .\configs\config_default_5min.json # 5min
# Google Sheets integration (optional)
export GOOGLE_SHEETS_CREDENTIALS_PATH="/path/to/credentials.json"
# Data directory (optional, defaults to ./data)
export DATA_DIR="/path/to/data"
# Results directory (optional, defaults to ./results)
export RESULTS_DIR="/path/to/results"
```
### Multi-Strategy Backtest
## Usage Examples
### Basic Backtest
```python
import json
from cycles.utils.storage import Storage
from cycles.backtest import Backtest
# Load configuration
with open('config.json', 'r') as f:
config = json.load(f)
# Initialize storage
storage = Storage(data_dir='./data')
# Load data
data_1min = storage.load_data(
'btcusd_1-min_data.csv',
config['start_date'],
config['stop_date']
)
# Run backtest
results = Backtest.run(
min1_df=data_1min,
df=data_1min, # Same data for 1-minute strategy
initial_usd=config['initial_usd'],
stop_loss_pct=0.05,
debug=True
)
print(f"Final USD: {results['final_usd']:.2f}")
print(f"Number of trades: {results['n_trades']}")
print(f"Win rate: {results['win_rate']:.2%}")
```
### Multi-Timeframe Analysis
```python
from main import process
# Define timeframes to test
timeframes = ['5T', '15T', '1H', '4H']
stop_loss_pcts = [0.02, 0.05, 0.10]
# Create tasks for parallel processing
tasks = [
(timeframe, data_1min, stop_loss_pct, 10000)
for timeframe in timeframes
for stop_loss_pct in stop_loss_pcts
]
# Process each task
for task in tasks:
results, trades = process(task, debug=False)
print(f"Timeframe: {task[0]}, Stop Loss: {task[2]:.1%}")
for result in results:
print(f" Final USD: {result['final_usd']:.2f}")
```
### Custom Strategy Development
```python
from cycles.Analysis.rsi import RSI
from cycles.Analysis.boillinger_band import BollingerBands
def custom_strategy(df):
"""Example custom trading strategy using RSI and Bollinger Bands"""
# Calculate indicators
rsi = RSI(period=14)
bb = BollingerBands(period=20, std_dev_multiplier=2.0)
df_with_rsi = rsi.calculate(df.copy())
df_with_bb = bb.calculate(df_with_rsi)
# Define signals
buy_signals = (
(df_with_bb['close'] < df_with_bb['LowerBand']) &
(df_with_bb['RSI'] < 30)
)
sell_signals = (
(df_with_bb['close'] > df_with_bb['UpperBand']) &
(df_with_bb['RSI'] > 70)
)
return buy_signals, sell_signals
```
## API Documentation
### Core Classes
#### `Backtest`
Main backtesting engine with static methods for strategy execution.
**Methods**:
- `run(min1_df, df, initial_usd, stop_loss_pct, debug=False)`: Execute backtest
- `check_stop_loss(...)`: Check stop-loss conditions using 1-minute data
- `handle_entry(...)`: Process trade entry logic
- `handle_exit(...)`: Process trade exit logic
#### `Storage`
Data management and persistence utilities.
**Methods**:
- `load_data(filename, start_date, stop_date)`: Load and filter historical data
- `save_data(df, filename)`: Save processed data
- `write_backtest_results(...)`: Save backtest results to CSV
#### `SystemUtils`
System optimization and resource management.
**Methods**:
- `get_optimal_workers()`: Determine optimal number of parallel workers
- `get_memory_usage()`: Monitor memory consumption
### Configuration Parameters
| Parameter | Type | Description | Default |
|-----------|------|-------------|---------|
| `start_date` | string | Backtest start date (YYYY-MM-DD) | Required |
| `stop_date` | string | Backtest end date (YYYY-MM-DD) | Required |
| `initial_usd` | float | Starting capital in USD | Required |
| `timeframes` | array | List of timeframes to test | Required |
| `stop_loss_pcts` | array | Stop-loss percentages to test | Required |
## Testing
### Running Tests
```bash
# Combine multiple strategies with different weights
uv run .\main.py .\configs\config_combined.json
# Run all tests
uv run pytest
# Run specific test file
uv run pytest test_bbrsi.py
# Run with verbose output
uv run pytest -v
# Run with coverage
uv run pytest --cov=cycles
```
### Custom Configuration
Create your own configuration file and run:
```bash
uv run .\main.py .\configs\your_config.json
### Test Structure
- `test_bbrsi.py`: Example strategy testing with RSI and Bollinger Bands
- Unit tests for individual modules (add as needed)
- Integration tests for complete workflows
### Example Test
```python
# test_bbrsi.py demonstrates strategy testing
from cycles.Analysis.rsi import RSI
from cycles.Analysis.boillinger_band import BollingerBands
def test_strategy_signals():
# Load test data
storage = Storage()
data = storage.load_data('test_data.csv', '2023-01-01', '2023-02-01')
# Calculate indicators
rsi = RSI(period=14)
bb = BollingerBands(period=20)
data_with_indicators = bb.calculate(rsi.calculate(data))
# Test signal generation
assert 'RSI' in data_with_indicators.columns
assert 'UpperBand' in data_with_indicators.columns
assert 'LowerBand' in data_with_indicators.columns
```
## Output
Backtests generate:
- **CSV Results**: Detailed performance metrics per timeframe/strategy
- **Trade Log**: Individual trade records with entry/exit details
- **Performance Charts**: Visual analysis of strategy performance (in debug mode)
- **Log Files**: Detailed execution logs
## License
[Add your license information here]
## Contributing
[Add contributing guidelines here]
### Development Setup
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/new-indicator`
3. Install development dependencies: `uv sync --dev`
4. Make your changes following the coding standards
5. Add tests for new functionality
6. Run tests: `uv run pytest`
7. Submit a pull request
### Coding Standards
- **Maximum file size**: 250 lines
- **Maximum function size**: 50 lines
- **Documentation**: All public functions must have docstrings
- **Type hints**: Use type hints for all function parameters and returns
- **Error handling**: Include proper error handling and meaningful error messages
- **No emoji**: Avoid emoji in code and comments
### Adding New Indicators
1. Create a new file in `cycles/Analysis/`
2. Follow the existing pattern (see `rsi.py` or `boillinger_band.py`)
3. Include comprehensive docstrings and type hints
4. Add tests for the new indicator
5. Update documentation
## Performance Considerations
### Optimization Tips
1. **Parallel Processing**: Use the built-in parallel processing for multiple timeframes
2. **Data Caching**: Cache frequently used calculations
3. **Memory Management**: Monitor memory usage for large datasets
4. **Efficient Data Types**: Use appropriate pandas data types
### Benchmarks
Typical performance on modern hardware:
- **1-minute data**: ~1M candles processed in 2-3 minutes
- **Multiple timeframes**: 4 timeframes × 4 stop-loss values in 5-10 minutes
- **Memory usage**: ~2-4GB for 1 year of 1-minute BTC data
## Troubleshooting
### Common Issues
1. **Memory errors with large datasets**:
- Reduce date range or use data chunking
- Increase system RAM or use swap space
2. **Slow performance**:
- Enable parallel processing
- Reduce number of timeframes/stop-loss values
- Use SSD storage for data files
3. **Missing data errors**:
- Verify data file format and column names
- Check date range availability in data
- Ensure proper data cleaning
### Debug Mode
Enable debug mode for detailed logging:
```python
# Set debug=True for detailed output
results = Backtest.run(..., debug=True)
```
## License
This project is licensed under the MIT License. See the LICENSE file for details.
## Changelog
### Version 0.1.0 (Current)
- Initial release
- Core backtesting framework
- SuperTrend strategy implementation
- Technical indicators (RSI, Bollinger Bands)
- Multi-timeframe analysis
- Machine learning price prediction
- Parallel processing support
---
For more detailed documentation, see the `docs/` directory or visit our [documentation website](link-to-docs).
**Support**: For questions or issues, please create an issue on GitHub or contact the development team.

462
backtest_runner.py Normal file
View File

@@ -0,0 +1,462 @@
import pandas as pd
import concurrent.futures
import logging
from typing import List, Tuple, Dict, Any, Optional
from cycles.utils.storage import Storage
from cycles.utils.system import SystemUtils
from cycles.utils.progress_manager import ProgressManager
from result_processor import ResultProcessor
def _process_single_task_static(task: Tuple[str, str, pd.DataFrame, float, float], progress_callback=None) -> Tuple[List[Dict], List[Dict]]:
"""
Static version of _process_single_task for use with ProcessPoolExecutor
Args:
task: Tuple of (task_id, timeframe, data_1min, stop_loss_pct, initial_usd)
progress_callback: Optional progress callback function
Returns:
Tuple of (results, trades)
"""
task_id, timeframe, data_1min, stop_loss_pct, initial_usd = task
try:
if timeframe == "1T" or timeframe == "1min":
df = data_1min.copy()
else:
df = _resample_data_static(data_1min, timeframe)
# Create required components for processing
from cycles.utils.storage import Storage
from result_processor import ResultProcessor
# Create storage with default paths (for subprocess)
storage = Storage()
result_processor = ResultProcessor(storage)
results, trades = result_processor.process_timeframe_results(
data_1min,
df,
[stop_loss_pct],
timeframe,
initial_usd,
progress_callback=progress_callback
)
return results, trades
except Exception as e:
error_msg = f"Failed to process {timeframe} with stop loss {stop_loss_pct}: {e}"
raise RuntimeError(error_msg) from e
def _resample_data_static(data_1min: pd.DataFrame, timeframe: str) -> pd.DataFrame:
"""
Static function to resample 1-minute data to specified timeframe
Args:
data_1min: 1-minute data DataFrame
timeframe: Target timeframe string
Returns:
Resampled DataFrame
"""
try:
agg_dict = {
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
if 'predicted_close_price' in data_1min.columns:
agg_dict['predicted_close_price'] = 'last'
resampled = data_1min.resample(timeframe).agg(agg_dict).dropna()
return resampled.reset_index()
except Exception as e:
error_msg = f"Failed to resample data to {timeframe}: {e}"
raise ValueError(error_msg) from e
class BacktestRunner:
"""Handles the execution of backtests across multiple timeframes and parameters"""
def __init__(
self,
storage: Storage,
system_utils: SystemUtils,
result_processor: ResultProcessor,
logging_instance: Optional[logging.Logger] = None,
show_progress: bool = True
):
"""
Initialize backtest runner
Args:
storage: Storage instance for data operations
system_utils: System utilities for resource management
result_processor: Result processor for handling outputs
logging_instance: Optional logging instance
show_progress: Whether to show visual progress bars
"""
self.storage = storage
self.system_utils = system_utils
self.result_processor = result_processor
self.logging = logging_instance
self.show_progress = show_progress
self.progress_manager = ProgressManager() if show_progress else None
def run_backtests(
self,
data_1min: pd.DataFrame,
timeframes: List[str],
stop_loss_pcts: List[float],
initial_usd: float,
debug: bool = False
) -> Tuple[List[Dict], List[Dict]]:
"""
Run backtests across all timeframe and stop loss combinations
Args:
data_1min: 1-minute data DataFrame
timeframes: List of timeframe strings (e.g., ['1D', '6h'])
stop_loss_pcts: List of stop loss percentages
initial_usd: Initial USD amount
debug: Whether to enable debug mode
Returns:
Tuple of (all_results, all_trades)
"""
# Create tasks for all combinations
tasks = self._create_tasks(timeframes, stop_loss_pcts, data_1min, initial_usd)
if self.logging:
self.logging.info(f"Starting {len(tasks)} backtest tasks")
if debug:
return self._run_sequential(tasks)
else:
return self._run_parallel(tasks)
def _create_tasks(
self,
timeframes: List[str],
stop_loss_pcts: List[float],
data_1min: pd.DataFrame,
initial_usd: float
) -> List[Tuple]:
"""Create task tuples for processing"""
tasks = []
for timeframe in timeframes:
for stop_loss_pct in stop_loss_pcts:
task_id = f"{timeframe}_{stop_loss_pct}"
task = (task_id, timeframe, data_1min, stop_loss_pct, initial_usd)
tasks.append(task)
return tasks
def _run_sequential(self, tasks: List[Tuple]) -> Tuple[List[Dict], List[Dict]]:
"""Run tasks sequentially (for debug mode)"""
# Initialize progress tracking if enabled
if self.progress_manager:
for task in tasks:
task_id, timeframe, data_1min, stop_loss_pct, initial_usd = task
# Calculate actual DataFrame size that will be processed
if timeframe == "1T" or timeframe == "1min":
actual_df_size = len(data_1min)
else:
# Get the actual resampled DataFrame size
temp_df = self._resample_data(data_1min, timeframe)
actual_df_size = len(temp_df)
task_name = f"{timeframe} SL:{stop_loss_pct:.0%}"
self.progress_manager.start_task(task_id, task_name, actual_df_size)
self.progress_manager.start_display()
all_results = []
all_trades = []
try:
for task in tasks:
try:
# Get progress callback for this task if available
progress_callback = None
if self.progress_manager:
progress_callback = self.progress_manager.get_task_progress_callback(task[0])
results, trades = self._process_single_task(task, progress_callback)
if results:
all_results.extend(results)
if trades:
all_trades.extend(trades)
# Mark task as completed
if self.progress_manager:
self.progress_manager.complete_task(task[0])
except Exception as e:
error_msg = f"Error processing task {task[1]} with stop loss {task[3]}: {e}"
if self.logging:
self.logging.error(error_msg)
raise RuntimeError(error_msg) from e
finally:
# Stop progress display
if self.progress_manager:
self.progress_manager.stop_display()
return all_results, all_trades
def _run_parallel(self, tasks: List[Tuple]) -> Tuple[List[Dict], List[Dict]]:
"""Run tasks in parallel using ProcessPoolExecutor"""
workers = self.system_utils.get_optimal_workers()
if self.logging:
self.logging.info(f"Running {len(tasks)} tasks with {workers} workers")
# OPTIMIZATION: Disable progress manager for parallel execution to reduce overhead
# Progress tracking adds significant overhead in multiprocessing
if self.progress_manager and self.logging:
self.logging.info("Progress tracking disabled for parallel execution (performance optimization)")
all_results = []
all_trades = []
completed_tasks = 0
try:
with concurrent.futures.ProcessPoolExecutor(max_workers=workers) as executor:
future_to_task = {
executor.submit(_process_single_task_static, task): task
for task in tasks
}
for future in concurrent.futures.as_completed(future_to_task):
task = future_to_task[future]
try:
results, trades = future.result()
if results:
all_results.extend(results)
if trades:
all_trades.extend(trades)
completed_tasks += 1
if self.logging:
self.logging.info(f"Completed task {task[0]} ({completed_tasks}/{len(tasks)})")
except Exception as e:
error_msg = f"Task {task[1]} with stop loss {task[3]} failed: {e}"
if self.logging:
self.logging.error(error_msg)
raise RuntimeError(error_msg) from e
except Exception as e:
error_msg = f"Parallel execution failed: {e}"
if self.logging:
self.logging.error(error_msg)
raise RuntimeError(error_msg) from e
finally:
# Stop progress display
if self.progress_manager:
self.progress_manager.stop_display()
if self.logging:
self.logging.info(f"All {len(tasks)} tasks completed successfully")
return all_results, all_trades
def _process_single_task(
self,
task: Tuple[str, str, pd.DataFrame, float, float],
progress_callback=None
) -> Tuple[List[Dict], List[Dict]]:
"""
Process a single backtest task
Args:
task: Tuple of (task_id, timeframe, data_1min, stop_loss_pct, initial_usd)
progress_callback: Optional progress callback function
Returns:
Tuple of (results, trades)
"""
task_id, timeframe, data_1min, stop_loss_pct, initial_usd = task
try:
if timeframe == "1T" or timeframe == "1min":
df = data_1min.copy()
else:
df = self._resample_data(data_1min, timeframe)
results, trades = self.result_processor.process_timeframe_results(
data_1min,
df,
[stop_loss_pct],
timeframe,
initial_usd,
progress_callback=progress_callback
)
# OPTIMIZATION: Skip individual trade file saving during parallel execution
# Trade files will be saved in batch at the end
# if trades:
# self.result_processor.save_trade_file(trades, timeframe, stop_loss_pct)
if self.logging:
self.logging.info(f"Completed task {task_id}: {len(results)} results, {len(trades)} trades")
return results, trades
except Exception as e:
error_msg = f"Failed to process {timeframe} with stop loss {stop_loss_pct}: {e}"
if self.logging:
self.logging.error(error_msg)
raise RuntimeError(error_msg) from e
def _resample_data(self, data_1min: pd.DataFrame, timeframe: str) -> pd.DataFrame:
"""
Resample 1-minute data to specified timeframe
Args:
data_1min: 1-minute data DataFrame
timeframe: Target timeframe string
Returns:
Resampled DataFrame
"""
try:
agg_dict = {
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}
if 'predicted_close_price' in data_1min.columns:
agg_dict['predicted_close_price'] = 'last'
resampled = data_1min.resample(timeframe).agg(agg_dict).dropna()
return resampled.reset_index()
except Exception as e:
error_msg = f"Failed to resample data to {timeframe}: {e}"
if self.logging:
self.logging.error(error_msg)
raise ValueError(error_msg) from e
def _get_timeframe_factor(self, timeframe: str) -> int:
"""
Get the factor by which data is reduced when resampling to timeframe
Args:
timeframe: Target timeframe string (e.g., '1h', '4h', '1D')
Returns:
Factor for estimating data size after resampling
"""
timeframe_factors = {
'1T': 1, '1min': 1,
'5T': 5, '5min': 5,
'15T': 15, '15min': 15,
'30T': 30, '30min': 30,
'1h': 60, '1H': 60,
'2h': 120, '2H': 120,
'4h': 240, '4H': 240,
'6h': 360, '6H': 360,
'8h': 480, '8H': 480,
'12h': 720, '12H': 720,
'1D': 1440, '1d': 1440,
'2D': 2880, '2d': 2880,
'3D': 4320, '3d': 4320,
'1W': 10080, '1w': 10080
}
return timeframe_factors.get(timeframe, 60) # Default to 1 hour if unknown
def load_data(self, filename: str, start_date: str, stop_date: str) -> pd.DataFrame:
"""
Load and validate data for backtesting
Args:
filename: Name of data file
start_date: Start date string
stop_date: Stop date string
Returns:
Loaded and validated DataFrame
Raises:
ValueError: If data is empty or invalid
"""
try:
data = self.storage.load_data(filename, start_date, stop_date)
if data.empty:
raise ValueError(f"No data loaded for period {start_date} to {stop_date}")
required_columns = ['open', 'high', 'low', 'close', 'volume']
if 'predicted_close_price' in data.columns:
required_columns.append('predicted_close_price')
missing_columns = [col for col in required_columns if col not in data.columns]
if missing_columns:
raise ValueError(f"Missing required columns: {missing_columns}")
if self.logging:
self.logging.info(f"Loaded {len(data)} rows of data from {filename}")
return data
except Exception as e:
error_msg = f"Failed to load data from {filename}: {e}"
if self.logging:
self.logging.error(error_msg)
raise RuntimeError(error_msg) from e
def validate_inputs(
self,
timeframes: List[str],
stop_loss_pcts: List[float],
initial_usd: float
) -> None:
"""
Validate backtest input parameters
Args:
timeframes: List of timeframe strings
stop_loss_pcts: List of stop loss percentages
initial_usd: Initial USD amount
Raises:
ValueError: If any input is invalid
"""
if not timeframes:
raise ValueError("At least one timeframe must be specified")
if not stop_loss_pcts:
raise ValueError("At least one stop loss percentage must be specified")
for pct in stop_loss_pcts:
if not 0 < pct < 1:
raise ValueError(f"Stop loss percentage must be between 0 and 1, got: {pct}")
if initial_usd <= 0:
raise ValueError("Initial USD must be positive")
if self.logging:
self.logging.info("Input validation completed successfully")

175
config_manager.py Normal file
View File

@@ -0,0 +1,175 @@
import json
import datetime
import logging
from typing import Dict, List, Optional, Any
from pathlib import Path
class ConfigManager:
"""Manages configuration loading, validation, and default values for backtest operations"""
DEFAULT_CONFIG = {
"start_date": "2025-05-01",
"stop_date": datetime.datetime.today().strftime('%Y-%m-%d'),
"initial_usd": 10000,
"timeframes": ["1D", "6h", "3h", "1h", "30m", "15m", "5m", "1m"],
"stop_loss_pcts": [0.01, 0.02, 0.03, 0.05],
"data_dir": "../data",
"results_dir": "results"
}
def __init__(self, logging_instance: Optional[logging.Logger] = None):
"""
Initialize configuration manager
Args:
logging_instance: Optional logging instance for output
"""
self.logging = logging_instance
self.config = {}
def load_config(self, config_path: Optional[str] = None) -> Dict[str, Any]:
"""
Load configuration from file or interactive input
Args:
config_path: Path to JSON config file, if None prompts for interactive input
Returns:
Dictionary containing validated configuration
Raises:
FileNotFoundError: If config file doesn't exist
json.JSONDecodeError: If config file has invalid JSON
ValueError: If configuration values are invalid
"""
if config_path:
self.config = self._load_from_file(config_path)
else:
self.config = self._load_interactive()
self._validate_config()
return self.config
def _load_from_file(self, config_path: str) -> Dict[str, Any]:
"""Load configuration from JSON file"""
try:
config_file = Path(config_path)
if not config_file.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
with open(config_file, 'r') as f:
config = json.load(f)
if self.logging:
self.logging.info(f"Configuration loaded from {config_path}")
return config
except json.JSONDecodeError as e:
error_msg = f"Invalid JSON in configuration file {config_path}: {e}"
if self.logging:
self.logging.error(error_msg)
raise json.JSONDecodeError(error_msg, e.doc, e.pos)
def _load_interactive(self) -> Dict[str, Any]:
"""Load configuration through interactive prompts"""
print("No config file provided. Please enter the following values (press Enter to use default):")
config = {}
# Start date
start_date = input(f"Start date [{self.DEFAULT_CONFIG['start_date']}]: ") or self.DEFAULT_CONFIG['start_date']
config['start_date'] = start_date
# Stop date
stop_date = input(f"Stop date [{self.DEFAULT_CONFIG['stop_date']}]: ") or self.DEFAULT_CONFIG['stop_date']
config['stop_date'] = stop_date
# Initial USD
initial_usd_str = input(f"Initial USD [{self.DEFAULT_CONFIG['initial_usd']}]: ") or str(self.DEFAULT_CONFIG['initial_usd'])
try:
config['initial_usd'] = float(initial_usd_str)
except ValueError:
raise ValueError(f"Invalid initial USD value: {initial_usd_str}")
# Timeframes
timeframes_str = input(f"Timeframes (comma separated) [{', '.join(self.DEFAULT_CONFIG['timeframes'])}]: ") or ','.join(self.DEFAULT_CONFIG['timeframes'])
config['timeframes'] = [tf.strip() for tf in timeframes_str.split(',') if tf.strip()]
# Stop loss percentages
stop_loss_pcts_str = input(f"Stop loss pcts (comma separated) [{', '.join(str(x) for x in self.DEFAULT_CONFIG['stop_loss_pcts'])}]: ") or ','.join(str(x) for x in self.DEFAULT_CONFIG['stop_loss_pcts'])
try:
config['stop_loss_pcts'] = [float(x.strip()) for x in stop_loss_pcts_str.split(',') if x.strip()]
except ValueError:
raise ValueError(f"Invalid stop loss percentages: {stop_loss_pcts_str}")
# Add default directories
config['data_dir'] = self.DEFAULT_CONFIG['data_dir']
config['results_dir'] = self.DEFAULT_CONFIG['results_dir']
return config
def _validate_config(self) -> None:
"""
Validate configuration values
Raises:
ValueError: If any configuration value is invalid
"""
# Validate initial USD
if self.config.get('initial_usd', 0) <= 0:
raise ValueError("Initial USD must be positive")
# Validate stop loss percentages
stop_loss_pcts = self.config.get('stop_loss_pcts', [])
for pct in stop_loss_pcts:
if not 0 < pct < 1:
raise ValueError(f"Stop loss percentage must be between 0 and 1, got: {pct}")
# Validate dates
try:
datetime.datetime.strptime(self.config['start_date'], '%Y-%m-%d')
datetime.datetime.strptime(self.config['stop_date'], '%Y-%m-%d')
except ValueError as e:
raise ValueError(f"Invalid date format (should be YYYY-MM-DD): {e}")
# Validate timeframes
timeframes = self.config.get('timeframes', [])
if not timeframes:
raise ValueError("At least one timeframe must be specified")
# Validate directories exist or can be created
for dir_key in ['data_dir', 'results_dir']:
dir_path = Path(self.config.get(dir_key, ''))
try:
dir_path.mkdir(parents=True, exist_ok=True)
except Exception as e:
raise ValueError(f"Cannot create directory {dir_path}: {e}")
if self.logging:
self.logging.info("Configuration validation completed successfully")
def get_config(self) -> Dict[str, Any]:
"""Return the current configuration"""
return self.config.copy()
def save_config(self, output_path: str) -> None:
"""
Save current configuration to file
Args:
output_path: Path where to save the configuration
"""
try:
with open(output_path, 'w') as f:
json.dump(self.config, f, indent=2)
if self.logging:
self.logging.info(f"Configuration saved to {output_path}")
except Exception as e:
error_msg = f"Failed to save configuration to {output_path}: {e}"
if self.logging:
self.logging.error(error_msg)
raise

View File

@@ -1,29 +0,0 @@
{
"start_date": "2025-01-01",
"stop_date": null,
"initial_usd": 10000,
"timeframes": ["1min"],
"strategies": [
{
"name": "bbrs",
"weight": 1.0,
"params": {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"trending_rsi_threshold": [30, 70],
"trending_bb_multiplier": 2.5,
"sideways_rsi_threshold": [40, 60],
"sideways_bb_multiplier": 1.8,
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": true,
"stop_loss_pct": 0.05
}
}
],
"combination_rules": {
"entry": "any",
"exit": "any",
"min_confidence": 0.5
}
}

View File

@@ -1,29 +0,0 @@
{
"start_date": "2024-01-01",
"stop_date": "2024-01-31",
"initial_usd": 10000,
"timeframes": ["1min"],
"stop_loss_pcts": [0.05],
"strategies": [
{
"name": "bbrs",
"weight": 1.0,
"params": {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"trending_rsi_threshold": [30, 70],
"trending_bb_multiplier": 2.5,
"sideways_rsi_threshold": [40, 60],
"sideways_bb_multiplier": 1.8,
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": true
}
}
],
"combination_rules": {
"entry": "any",
"exit": "any",
"min_confidence": 0.5
}
}

View File

@@ -1,37 +0,0 @@
{
"start_date": "2025-03-01",
"stop_date": "2025-03-15",
"initial_usd": 10000,
"timeframes": ["15min"],
"strategies": [
{
"name": "default",
"weight": 0.6,
"params": {
"timeframe": "15min",
"stop_loss_pct": 0.03
}
},
{
"name": "bbrs",
"weight": 0.4,
"params": {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"trending_rsi_threshold": [30, 70],
"trending_bb_multiplier": 2.5,
"sideways_rsi_threshold": [40, 60],
"sideways_bb_multiplier": 1.8,
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": true,
"stop_loss_pct": 0.05
}
}
],
"combination_rules": {
"entry": "weighted_consensus",
"exit": "any",
"min_confidence": 0.6
}
}

View File

@@ -1,21 +0,0 @@
{
"start_date": "2025-01-01",
"stop_date": "2025-05-01",
"initial_usd": 10000,
"timeframes": ["15min"],
"strategies": [
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "15min",
"stop_loss_pct": 0.03
}
}
],
"combination_rules": {
"entry": "any",
"exit": "any",
"min_confidence": 0.5
}
}

View File

@@ -1,21 +0,0 @@
{
"start_date": "2024-01-01",
"stop_date": "2024-01-31",
"initial_usd": 10000,
"timeframes": ["5min"],
"strategies": [
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "5min",
"stop_loss_pct": 0.03
}
}
],
"combination_rules": {
"entry": "any",
"exit": "any",
"min_confidence": 0.5
}
}

View File

@@ -0,0 +1,10 @@
{
"start_date": "2021-11-01",
"stop_date": "2024-04-01",
"initial_usd": 10000,
"timeframes": ["1min", "2min", "3min", "4min", "5min", "10min", "15min", "30min", "1h", "2h", "4h", "6h", "8h", "12h", "1d"],
"stop_loss_pcts": [0.01, 0.02, 0.03, 0.04, 0.05, 0.1],
"data_dir": "../data",
"results_dir": "../results",
"debug": 0
}

10
configs/full_config.json Normal file
View File

@@ -0,0 +1,10 @@
{
"start_date": "2020-01-01",
"stop_date": "2025-07-08",
"initial_usd": 10000,
"timeframes": ["1h", "4h", "15ME", "5ME", "1ME"],
"stop_loss_pcts": [0.01, 0.02, 0.03, 0.05],
"data_dir": "../data",
"results_dir": "../results",
"debug": 1
}

View File

@@ -0,0 +1,10 @@
{
"start_date": "2023-01-01",
"stop_date": "2025-01-15",
"initial_usd": 10000,
"timeframes": ["4h"],
"stop_loss_pcts": [0.05],
"data_dir": "../data",
"results_dir": "../results",
"debug": 0
}

View File

@@ -1,416 +0,0 @@
import pandas as pd
import numpy as np
from cycles.Analysis.boillinger_band import BollingerBands
from cycles.Analysis.rsi import RSI
from cycles.utils.data_utils import aggregate_to_daily, aggregate_to_hourly, aggregate_to_minutes
class BollingerBandsStrategy:
def __init__(self, config = None, logging = None):
if config is None:
raise ValueError("Config must be provided.")
self.config = config
self.logging = logging
def _ensure_datetime_index(self, data):
"""
Ensure the DataFrame has a DatetimeIndex for proper time-series operations.
If the DataFrame has a 'timestamp' column but not a DatetimeIndex, convert it.
Args:
data (DataFrame): Input DataFrame
Returns:
DataFrame: DataFrame with proper DatetimeIndex
"""
if data.empty:
return data
# Check if we have a DatetimeIndex already
if isinstance(data.index, pd.DatetimeIndex):
return data
# Check if we have a 'timestamp' column that we can use as index
if 'timestamp' in data.columns:
data_copy = data.copy()
# Convert timestamp column to datetime if it's not already
if not pd.api.types.is_datetime64_any_dtype(data_copy['timestamp']):
data_copy['timestamp'] = pd.to_datetime(data_copy['timestamp'])
# Set timestamp as index and drop the column
data_copy = data_copy.set_index('timestamp')
if self.logging:
self.logging.info("Converted 'timestamp' column to DatetimeIndex for strategy processing.")
return data_copy
# If we have a regular index but it might be datetime strings, try to convert
try:
if data.index.dtype == 'object':
data_copy = data.copy()
data_copy.index = pd.to_datetime(data_copy.index)
if self.logging:
self.logging.info("Converted index to DatetimeIndex for strategy processing.")
return data_copy
except:
pass
# If we can't create a proper DatetimeIndex, warn and return as-is
if self.logging:
self.logging.warning("Could not create DatetimeIndex for strategy processing. Time-based operations may fail.")
return data
def run(self, data, strategy_name):
# Ensure proper DatetimeIndex before processing
data = self._ensure_datetime_index(data)
if strategy_name == "MarketRegimeStrategy":
result = self.MarketRegimeStrategy(data)
return self.standardize_output(result, strategy_name)
elif strategy_name == "CryptoTradingStrategy":
result = self.CryptoTradingStrategy(data)
return self.standardize_output(result, strategy_name)
else:
if self.logging is not None:
self.logging.warning(f"Strategy {strategy_name} not found. Using no_strategy instead.")
return self.no_strategy(data)
def standardize_output(self, data, strategy_name):
"""
Standardize column names across different strategies to ensure consistent plotting and analysis
Args:
data (DataFrame): Strategy output DataFrame
strategy_name (str): Name of the strategy that generated this data
Returns:
DataFrame: Data with standardized column names
"""
if data.empty:
return data
# Create a copy to avoid modifying the original
standardized = data.copy()
# Standardize column names based on strategy
if strategy_name == "MarketRegimeStrategy":
# MarketRegimeStrategy already has standard column names for most fields
# Just ensure all standard columns exist
pass
elif strategy_name == "CryptoTradingStrategy":
# Map strategy-specific column names to standard names
column_mapping = {
'UpperBand_15m': 'UpperBand',
'LowerBand_15m': 'LowerBand',
'SMA_15m': 'SMA',
'RSI_15m': 'RSI',
'VolumeMA_15m': 'VolumeMA',
# Keep StopLoss and TakeProfit as they are
}
# Add standard columns from mapped columns
for old_col, new_col in column_mapping.items():
if old_col in standardized.columns and new_col not in standardized.columns:
standardized[new_col] = standardized[old_col]
# Add additional strategy-specific data as metadata columns
if 'UpperBand_1h' in standardized.columns:
standardized['UpperBand_1h_meta'] = standardized['UpperBand_1h']
if 'LowerBand_1h' in standardized.columns:
standardized['LowerBand_1h_meta'] = standardized['LowerBand_1h']
# Ensure all strategies have BBWidth if possible
if 'BBWidth' not in standardized.columns and 'UpperBand' in standardized.columns and 'LowerBand' in standardized.columns:
standardized['BBWidth'] = (standardized['UpperBand'] - standardized['LowerBand']) / standardized['SMA'] if 'SMA' in standardized.columns else np.nan
return standardized
def no_strategy(self, data):
"""No strategy: returns False for both buy and sell conditions"""
buy_condition = pd.Series([False] * len(data), index=data.index)
sell_condition = pd.Series([False] * len(data), index=data.index)
return buy_condition, sell_condition
def rsi_bollinger_confirmation(self, rsi, window=14, std_mult=1.5):
"""Calculate RSI Bollinger Bands for confirmation
Args:
rsi (Series): RSI values
window (int): Rolling window for SMA
std_mult (float): Standard deviation multiplier
Returns:
tuple: (oversold condition, overbought condition)
"""
valid_rsi = ~rsi.isna()
if not valid_rsi.any():
# Return empty Series if no valid RSI data
return pd.Series(False, index=rsi.index), pd.Series(False, index=rsi.index)
rsi_sma = rsi.rolling(window).mean()
rsi_std = rsi.rolling(window).std()
upper_rsi_band = rsi_sma + std_mult * rsi_std
lower_rsi_band = rsi_sma - std_mult * rsi_std
return (rsi < lower_rsi_band), (rsi > upper_rsi_band)
def MarketRegimeStrategy(self, data):
"""Optimized Bollinger Bands + RSI Strategy for Crypto Trading (Including Sideways Markets)
with adaptive Bollinger Bands
This advanced strategy combines volatility analysis, momentum confirmation, and regime detection
to adapt to Bitcoin's unique market conditions.
Entry Conditions:
- Trending Market (Breakout Mode):
Buy: Price < Lower Band ∧ RSI < 50 ∧ Volume Spike (≥1.5× 20D Avg)
Sell: Price > Upper Band ∧ RSI > 50 ∧ Volume Spike
- Sideways Market (Mean Reversion):
Buy: Price ≤ Lower Band ∧ RSI ≤ 40
Sell: Price ≥ Upper Band ∧ RSI ≥ 60
Enhanced with RSI Bollinger Squeeze for signal confirmation when enabled.
Returns:
DataFrame: A unified DataFrame containing original data, BB, RSI, and signals.
"""
# data = aggregate_to_hourly(data, 1)
# data = aggregate_to_daily(data)
data = aggregate_to_minutes(data, 15)
# Calculate Bollinger Bands
bb_calculator = BollingerBands(config=self.config)
# Ensure we are working with a copy to avoid modifying the original DataFrame upstream
data_bb = bb_calculator.calculate(data.copy())
# Calculate RSI
rsi_calculator = RSI(config=self.config)
# Use the original data's copy for RSI calculation as well, to maintain index integrity
data_with_rsi = rsi_calculator.calculate(data.copy(), price_column='close')
# Combine BB and RSI data into a single DataFrame for signal generation
# Ensure indices are aligned; they should be as both are from data.copy()
if 'RSI' in data_with_rsi.columns:
data_bb['RSI'] = data_with_rsi['RSI']
else:
# If RSI wasn't calculated (e.g., not enough data), create a dummy column with NaNs
# to prevent errors later, though signals won't be generated.
data_bb['RSI'] = pd.Series(index=data_bb.index, dtype=float)
if self.logging:
self.logging.warning("RSI column not found or not calculated. Signals relying on RSI may not be generated.")
# Initialize conditions as all False
buy_condition = pd.Series(False, index=data_bb.index)
sell_condition = pd.Series(False, index=data_bb.index)
# Create masks for different market regimes
# MarketRegime is expected to be in data_bb from BollingerBands calculation
sideways_mask = data_bb['MarketRegime'] > 0
trending_mask = data_bb['MarketRegime'] <= 0
valid_data_mask = ~data_bb['MarketRegime'].isna() # Handle potential NaN values
# Calculate volume spike (≥1.5× 20D Avg)
# 'volume' column should be present in the input 'data', and thus in 'data_bb'
if 'volume' in data_bb.columns:
volume_20d_avg = data_bb['volume'].rolling(window=20).mean()
volume_spike = data_bb['volume'] >= 1.5 * volume_20d_avg
# Additional volume contraction filter for sideways markets
volume_30d_avg = data_bb['volume'].rolling(window=30).mean()
volume_contraction = data_bb['volume'] < 0.7 * volume_30d_avg
else:
# If volume data is not available, assume no volume spike
volume_spike = pd.Series(False, index=data_bb.index)
volume_contraction = pd.Series(False, index=data_bb.index)
if self.logging is not None:
self.logging.warning("Volume data not available. Volume conditions will not be triggered.")
# Calculate RSI Bollinger Squeeze confirmation
# RSI column is now part of data_bb
if 'RSI' in data_bb.columns and not data_bb['RSI'].isna().all():
oversold_rsi, overbought_rsi = self.rsi_bollinger_confirmation(data_bb['RSI'])
else:
oversold_rsi = pd.Series(False, index=data_bb.index)
overbought_rsi = pd.Series(False, index=data_bb.index)
if self.logging is not None and ('RSI' not in data_bb.columns or data_bb['RSI'].isna().all()):
self.logging.warning("RSI data not available or all NaN. RSI Bollinger Squeeze will not be triggered.")
# Calculate conditions for sideways market (Mean Reversion)
if sideways_mask.any():
sideways_buy = (data_bb['close'] <= data_bb['LowerBand']) & (data_bb['RSI'] <= 40)
sideways_sell = (data_bb['close'] >= data_bb['UpperBand']) & (data_bb['RSI'] >= 60)
# Add enhanced confirmation for sideways markets
if self.config.get("SqueezeStrategy", False):
sideways_buy = sideways_buy & oversold_rsi & volume_contraction
sideways_sell = sideways_sell & overbought_rsi & volume_contraction
# Apply only where market is sideways and data is valid
buy_condition = buy_condition | (sideways_buy & sideways_mask & valid_data_mask)
sell_condition = sell_condition | (sideways_sell & sideways_mask & valid_data_mask)
# Calculate conditions for trending market (Breakout Mode)
if trending_mask.any():
trending_buy = (data_bb['close'] < data_bb['LowerBand']) & (data_bb['RSI'] < 50) & volume_spike
trending_sell = (data_bb['close'] > data_bb['UpperBand']) & (data_bb['RSI'] > 50) & volume_spike
# Add enhanced confirmation for trending markets
if self.config.get("SqueezeStrategy", False):
trending_buy = trending_buy & oversold_rsi
trending_sell = trending_sell & overbought_rsi
# Apply only where market is trending and data is valid
buy_condition = buy_condition | (trending_buy & trending_mask & valid_data_mask)
sell_condition = sell_condition | (trending_sell & trending_mask & valid_data_mask)
# Add buy/sell conditions as columns to the DataFrame
data_bb['BuySignal'] = buy_condition
data_bb['SellSignal'] = sell_condition
return data_bb
# Helper functions for CryptoTradingStrategy
def _volume_confirmation_crypto(self, current_volume, volume_ma):
"""Check volume surge against moving average for crypto strategy"""
if pd.isna(current_volume) or pd.isna(volume_ma) or volume_ma == 0:
return False
return current_volume > 1.5 * volume_ma
def _multi_timeframe_signal_crypto(self, current_price, rsi_value,
lower_band_15m, lower_band_1h,
upper_band_15m, upper_band_1h):
"""Generate signals with multi-timeframe confirmation for crypto strategy"""
# Ensure all inputs are not NaN before making comparisons
if any(pd.isna(val) for val in [current_price, rsi_value, lower_band_15m, lower_band_1h, upper_band_15m, upper_band_1h]):
return False, False
buy_signal = (current_price <= lower_band_15m and
current_price <= lower_band_1h and
rsi_value < 35)
sell_signal = (current_price >= upper_band_15m and
current_price >= upper_band_1h and
rsi_value > 65)
return buy_signal, sell_signal
def CryptoTradingStrategy(self, data):
"""Core trading algorithm with risk management
- Multi-Timeframe Confirmation: Combines 15-minute and 1-hour Bollinger Bands
- Adaptive Volatility Filtering: Uses ATR for dynamic stop-loss/take-profit
- Volume Spike Detection: Requires 1.5× average volume for confirmation
- EMA-Smoothed RSI: Reduces false signals in choppy markets
- Regime-Adaptive Parameters:
- Trending: 2σ bands, RSI 35/65 thresholds
- Sideways: 1.8σ bands, RSI 40/60 thresholds
- Strategy Logic:
- Long Entry: Price ≤ both 15m & 1h lower bands + RSI < 35 + Volume surge
- Short Entry: Price ≥ both 15m & 1h upper bands + RSI > 65 + Volume surge
- Exit: 2:1 risk-reward ratio with ATR-based stops
"""
if data.empty or 'close' not in data.columns or 'volume' not in data.columns:
if self.logging:
self.logging.warning("CryptoTradingStrategy: Input data is empty or missing 'close'/'volume' columns.")
return pd.DataFrame() # Return empty DataFrame if essential data is missing
print(f"data: {data.head()}")
# Aggregate data
data_15m = aggregate_to_minutes(data.copy(), 15)
data_1h = aggregate_to_hourly(data.copy(), 1)
if data_15m.empty or data_1h.empty:
if self.logging:
self.logging.warning("CryptoTradingStrategy: Not enough data for 15m or 1h aggregation.")
return pd.DataFrame() # Return original data if aggregation fails
# --- Calculate indicators for 15m timeframe ---
# Ensure 'close' and 'volume' exist before trying to access them
if 'close' not in data_15m.columns or 'volume' not in data_15m.columns:
if self.logging: self.logging.warning("CryptoTradingStrategy: 15m data missing close or volume.")
return data # Or an empty DF
price_data_15m = data_15m['close']
volume_data_15m = data_15m['volume']
upper_15m, sma_15m, lower_15m = BollingerBands.calculate_custom_bands(price_data_15m, window=20, num_std=2, min_periods=1)
# Use the static method from RSI class
rsi_15m = RSI.calculate_custom_rsi(price_data_15m, window=14, smoothing='EMA')
volume_ma_15m = volume_data_15m.rolling(window=20, min_periods=1).mean()
# Add 15m indicators to data_15m DataFrame
data_15m['UpperBand_15m'] = upper_15m
data_15m['SMA_15m'] = sma_15m
data_15m['LowerBand_15m'] = lower_15m
data_15m['RSI_15m'] = rsi_15m
data_15m['VolumeMA_15m'] = volume_ma_15m
# --- Calculate indicators for 1h timeframe ---
if 'close' not in data_1h.columns:
if self.logging: self.logging.warning("CryptoTradingStrategy: 1h data missing close.")
return data_15m # Return 15m data as 1h failed
price_data_1h = data_1h['close']
# Use the static method from BollingerBands class, setting min_periods to 1 explicitly
upper_1h, _, lower_1h = BollingerBands.calculate_custom_bands(price_data_1h, window=50, num_std=1.8, min_periods=1)
# Add 1h indicators to a temporary DataFrame to be merged
df_1h_indicators = pd.DataFrame(index=data_1h.index)
df_1h_indicators['UpperBand_1h'] = upper_1h
df_1h_indicators['LowerBand_1h'] = lower_1h
# Merge 1h indicators into 15m DataFrame
# Use reindex and ffill to propagate 1h values to 15m intervals
data_15m = pd.merge(data_15m, df_1h_indicators, left_index=True, right_index=True, how='left')
data_15m['UpperBand_1h'] = data_15m['UpperBand_1h'].ffill()
data_15m['LowerBand_1h'] = data_15m['LowerBand_1h'].ffill()
# --- Generate Signals ---
buy_signals = pd.Series(False, index=data_15m.index)
sell_signals = pd.Series(False, index=data_15m.index)
stop_loss_levels = pd.Series(np.nan, index=data_15m.index)
take_profit_levels = pd.Series(np.nan, index=data_15m.index)
# ATR calculation needs a rolling window, apply to 'high', 'low', 'close' if available
# Using a simplified ATR for now: std of close prices over the last 4 15-min periods (1 hour)
if 'close' in data_15m.columns:
atr_series = price_data_15m.rolling(window=4, min_periods=1).std()
else:
atr_series = pd.Series(0, index=data_15m.index) # No ATR if close is missing
for i in range(len(data_15m)):
if i == 0: continue # Skip first row for volume_ma_15m[i-1]
current_price = data_15m['close'].iloc[i]
current_volume = data_15m['volume'].iloc[i]
rsi_val = data_15m['RSI_15m'].iloc[i]
lb_15m = data_15m['LowerBand_15m'].iloc[i]
ub_15m = data_15m['UpperBand_15m'].iloc[i]
lb_1h = data_15m['LowerBand_1h'].iloc[i]
ub_1h = data_15m['UpperBand_1h'].iloc[i]
vol_ma = data_15m['VolumeMA_15m'].iloc[i-1] # Use previous period's MA
atr = atr_series.iloc[i]
vol_confirm = self._volume_confirmation_crypto(current_volume, vol_ma)
buy_signal, sell_signal = self._multi_timeframe_signal_crypto(
current_price, rsi_val, lb_15m, lb_1h, ub_15m, ub_1h
)
if buy_signal and vol_confirm:
buy_signals.iloc[i] = True
if not pd.isna(atr) and atr > 0:
stop_loss_levels.iloc[i] = current_price - 2 * atr
take_profit_levels.iloc[i] = current_price + 4 * atr
elif sell_signal and vol_confirm:
sell_signals.iloc[i] = True
if not pd.isna(atr) and atr > 0:
stop_loss_levels.iloc[i] = current_price + 2 * atr
take_profit_levels.iloc[i] = current_price - 4 * atr
data_15m['BuySignal'] = buy_signals
data_15m['SellSignal'] = sell_signals
data_15m['StopLoss'] = stop_loss_levels
data_15m['TakeProfit'] = take_profit_levels
return data_15m

View File

@@ -1,29 +1,26 @@
import pandas as pd
import numpy as np
class BollingerBands:
"""
Calculates Bollinger Bands for given financial data.
"""
def __init__(self, config):
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0):
"""
Initializes the BollingerBands calculator.
Args:
period (int): The period for the moving average and standard deviation.
std_dev_multiplier (float): The number of standard deviations for the upper and lower bands.
bb_width (float): The width of the Bollinger Bands.
"""
if config['bb_period'] <= 0:
if period <= 0:
raise ValueError("Period must be a positive integer.")
if config['trending']['bb_std_dev_multiplier'] <= 0 or config['sideways']['bb_std_dev_multiplier'] <= 0:
if std_dev_multiplier <= 0:
raise ValueError("Standard deviation multiplier must be positive.")
if config['bb_width'] <= 0:
raise ValueError("BB width must be positive.")
self.config = config
self.period = period
self.std_dev_multiplier = std_dev_multiplier
def calculate(self, data_df: pd.DataFrame, price_column: str = 'close', squeeze = False) -> pd.DataFrame:
def calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame:
"""
Calculates Bollinger Bands and adds them to the DataFrame.
@@ -37,109 +34,17 @@ class BollingerBands:
'UpperBand',
'LowerBand'.
"""
# Work on a copy to avoid modifying the original DataFrame passed to the function
data_df = data_df.copy()
if price_column not in data_df.columns:
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
if not squeeze:
period = self.config['bb_period']
bb_width_threshold = self.config['bb_width']
trending_std_multiplier = self.config['trending']['bb_std_dev_multiplier']
sideways_std_multiplier = self.config['sideways']['bb_std_dev_multiplier']
# Calculate SMA
data_df['SMA'] = data_df[price_column].rolling(window=self.period).mean()
# Calculate SMA
data_df['SMA'] = data_df[price_column].rolling(window=period).mean()
# Calculate Standard Deviation
std_dev = data_df[price_column].rolling(window=self.period).std()
# Calculate Standard Deviation
std_dev = data_df[price_column].rolling(window=period).std()
# Calculate reference Upper and Lower Bands for BBWidth calculation (e.g., using 2.0 std dev)
# This ensures BBWidth is calculated based on a consistent band definition before applying adaptive multipliers.
ref_upper_band = data_df['SMA'] + (2.0 * std_dev)
ref_lower_band = data_df['SMA'] - (2.0 * std_dev)
# Calculate the width of the Bollinger Bands
# Avoid division by zero or NaN if SMA is zero or NaN by replacing with np.nan
data_df['BBWidth'] = np.where(data_df['SMA'] != 0, (ref_upper_band - ref_lower_band) / data_df['SMA'], np.nan)
# Calculate the market regime (1 = sideways, 0 = trending)
# Handle NaN in BBWidth: if BBWidth is NaN, MarketRegime should also be NaN or a default (e.g. trending)
data_df['MarketRegime'] = np.where(data_df['BBWidth'].isna(), np.nan,
(data_df['BBWidth'] < bb_width_threshold).astype(float)) # Use float for NaN compatibility
# Determine the std dev multiplier for each row based on its market regime
conditions = [
data_df['MarketRegime'] == 1, # Sideways market
data_df['MarketRegime'] == 0 # Trending market
]
choices = [
sideways_std_multiplier,
trending_std_multiplier
]
# Default multiplier if MarketRegime is NaN (e.g., use trending or a neutral default like 2.0)
# For now, let's use trending_std_multiplier as default if MarketRegime is NaN.
# This can be adjusted based on desired behavior for periods where regime is undetermined.
row_specific_std_multiplier = np.select(conditions, choices, default=trending_std_multiplier)
# Calculate final Upper and Lower Bands using the row-specific multiplier
data_df['UpperBand'] = data_df['SMA'] + (row_specific_std_multiplier * std_dev)
data_df['LowerBand'] = data_df['SMA'] - (row_specific_std_multiplier * std_dev)
else: # squeeze is True
price_series = data_df[price_column]
# Use the static method for the squeeze case with fixed parameters
upper_band, sma, lower_band = self.calculate_custom_bands(
price_series,
window=14,
num_std=1.5,
min_periods=14 # Match typical squeeze behavior where bands appear after full period
)
data_df['SMA'] = sma
data_df['UpperBand'] = upper_band
data_df['LowerBand'] = lower_band
# BBWidth and MarketRegime are not typically calculated/used in a simple squeeze context by this method
# If needed, they could be added, but the current structure implies they are part of the non-squeeze path.
data_df['BBWidth'] = np.nan
data_df['MarketRegime'] = np.nan
# Calculate Upper and Lower Bands
data_df['UpperBand'] = data_df['SMA'] + (self.std_dev_multiplier * std_dev)
data_df['LowerBand'] = data_df['SMA'] - (self.std_dev_multiplier * std_dev)
return data_df
@staticmethod
def calculate_custom_bands(price_series: pd.Series, window: int = 20, num_std: float = 2.0, min_periods: int = None) -> tuple[pd.Series, pd.Series, pd.Series]:
"""
Calculates Bollinger Bands with specified window and standard deviation multiplier.
Args:
price_series (pd.Series): Series of prices.
window (int): The period for the moving average and standard deviation.
num_std (float): The number of standard deviations for the upper and lower bands.
min_periods (int, optional): Minimum number of observations in window required to have a value.
Defaults to `window` if None.
Returns:
tuple[pd.Series, pd.Series, pd.Series]: Upper band, SMA, Lower band.
"""
if not isinstance(price_series, pd.Series):
raise TypeError("price_series must be a pandas Series.")
if not isinstance(window, int) or window <= 0:
raise ValueError("window must be a positive integer.")
if not isinstance(num_std, (int, float)) or num_std <= 0:
raise ValueError("num_std must be a positive number.")
if min_periods is not None and (not isinstance(min_periods, int) or min_periods <= 0):
raise ValueError("min_periods must be a positive integer if provided.")
actual_min_periods = window if min_periods is None else min_periods
sma = price_series.rolling(window=window, min_periods=actual_min_periods).mean()
std = price_series.rolling(window=window, min_periods=actual_min_periods).std()
# Replace NaN std with 0 to avoid issues if sma is present but std is not (e.g. constant price in window)
std = std.fillna(0)
upper_band = sma + (std * num_std)
lower_band = sma - (std * num_std)
return upper_band, sma, lower_band

View File

@@ -5,7 +5,7 @@ class RSI:
"""
A class to calculate the Relative Strength Index (RSI).
"""
def __init__(self, config):
def __init__(self, period: int = 14):
"""
Initializes the RSI calculator.
@@ -13,13 +13,13 @@ class RSI:
period (int): The period for RSI calculation. Default is 14.
Must be a positive integer.
"""
if not isinstance(config['rsi_period'], int) or config['rsi_period'] <= 0:
if not isinstance(period, int) or period <= 0:
raise ValueError("Period must be a positive integer.")
self.period = config['rsi_period']
self.period = period
def calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame:
"""
Calculates the RSI (using Wilder's smoothing) and adds it as a column to the input DataFrame.
Calculates the RSI and adds it as a column to the input DataFrame.
Args:
data_df (pd.DataFrame): DataFrame with historical price data.
@@ -35,79 +35,75 @@ class RSI:
if price_column not in data_df.columns:
raise ValueError(f"Price column '{price_column}' not found in DataFrame.")
# Check if data is sufficient for calculation (need period + 1 for one diff calculation)
if len(data_df) < self.period + 1:
print(f"Warning: Data length ({len(data_df)}) is less than RSI period ({self.period}) + 1. RSI will not be calculated meaningfully.")
df_copy = data_df.copy()
df_copy['RSI'] = np.nan # Add an RSI column with NaNs
return df_copy
if len(data_df) < self.period:
print(f"Warning: Data length ({len(data_df)}) is less than RSI period ({self.period}). RSI will not be calculated.")
return data_df.copy()
df = data_df.copy() # Work on a copy
df = data_df.copy()
delta = df[price_column].diff(1)
price_series = df[price_column]
gain = delta.where(delta > 0, 0)
loss = -delta.where(delta < 0, 0) # Ensure loss is positive
# Call the static custom RSI calculator, defaulting to EMA for Wilder's smoothing
rsi_series = self.calculate_custom_rsi(price_series, window=self.period, smoothing='EMA')
# Calculate initial average gain and loss (SMA)
avg_gain = gain.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period]
avg_loss = loss.rolling(window=self.period, min_periods=self.period).mean().iloc[self.period -1:self.period]
df['RSI'] = rsi_series
# Calculate subsequent average gains and losses (EMA-like)
# Pre-allocate lists for gains and losses to avoid repeated appending to Series
gains = [0.0] * len(df)
losses = [0.0] * len(df)
if not avg_gain.empty:
gains[self.period -1] = avg_gain.iloc[0]
if not avg_loss.empty:
losses[self.period -1] = avg_loss.iloc[0]
for i in range(self.period, len(df)):
gains[i] = ((gains[i-1] * (self.period - 1)) + gain.iloc[i]) / self.period
losses[i] = ((losses[i-1] * (self.period - 1)) + loss.iloc[i]) / self.period
df['avg_gain'] = pd.Series(gains, index=df.index)
df['avg_loss'] = pd.Series(losses, index=df.index)
# Calculate RS
# Handle division by zero: if avg_loss is 0, RS is undefined or infinite.
# If avg_loss is 0 and avg_gain is also 0, RSI is conventionally 50.
# If avg_loss is 0 and avg_gain > 0, RSI is conventionally 100.
rs = df['avg_gain'] / df['avg_loss']
# Calculate RSI
# RSI = 100 - (100 / (1 + RS))
# If avg_loss is 0:
# If avg_gain > 0, RS -> inf, RSI -> 100
# If avg_gain == 0, RS -> NaN (0/0), RSI -> 50 (conventionally, or could be 0 or 100 depending on interpretation)
# We will use a common convention where RSI is 100 if avg_loss is 0 and avg_gain > 0,
# and RSI is 0 if avg_loss is 0 and avg_gain is 0 (or 50, let's use 0 to indicate no strength if both are 0).
# However, to avoid NaN from 0/0, it's better to calculate RSI directly with conditions.
rsi_values = []
for i in range(len(df)):
avg_g = df['avg_gain'].iloc[i]
avg_l = df['avg_loss'].iloc[i]
if i < self.period -1 : # Not enough data for initial SMA
rsi_values.append(np.nan)
continue
if avg_l == 0:
if avg_g == 0:
rsi_values.append(50) # Or 0, or np.nan depending on how you want to treat this. 50 implies neutrality.
else:
rsi_values.append(100) # Max strength
else:
rs_val = avg_g / avg_l
rsi_values.append(100 - (100 / (1 + rs_val)))
df['RSI'] = pd.Series(rsi_values, index=df.index)
# Remove intermediate columns if desired, or keep them for debugging
# df.drop(columns=['avg_gain', 'avg_loss'], inplace=True)
return df
@staticmethod
def calculate_custom_rsi(price_series: pd.Series, window: int = 14, smoothing: str = 'SMA') -> pd.Series:
"""
Calculates RSI with specified window and smoothing (SMA or EMA).
Args:
price_series (pd.Series): Series of prices.
window (int): The period for RSI calculation. Must be a positive integer.
smoothing (str): Smoothing method, 'SMA' or 'EMA'. Defaults to 'SMA'.
Returns:
pd.Series: Series containing the RSI values.
"""
if not isinstance(price_series, pd.Series):
raise TypeError("price_series must be a pandas Series.")
if not isinstance(window, int) or window <= 0:
raise ValueError("window must be a positive integer.")
if smoothing not in ['SMA', 'EMA']:
raise ValueError("smoothing must be either 'SMA' or 'EMA'.")
if len(price_series) < window + 1: # Need at least window + 1 prices for one diff
# print(f"Warning: Data length ({len(price_series)}) is less than RSI window ({window}) + 1. RSI will be all NaN.")
return pd.Series(np.nan, index=price_series.index)
delta = price_series.diff()
# The first delta is NaN. For gain/loss calculations, it can be treated as 0.
# However, subsequent rolling/ewm will handle NaNs appropriately if min_periods is set.
gain = delta.where(delta > 0, 0.0)
loss = -delta.where(delta < 0, 0.0) # Ensure loss is positive
# Ensure gain and loss Series have the same index as price_series for rolling/ewm
# This is important if price_series has missing dates/times
gain = gain.reindex(price_series.index, fill_value=0.0)
loss = loss.reindex(price_series.index, fill_value=0.0)
if smoothing == 'EMA':
# adjust=False for Wilder's smoothing used in RSI
avg_gain = gain.ewm(alpha=1/window, adjust=False, min_periods=window).mean()
avg_loss = loss.ewm(alpha=1/window, adjust=False, min_periods=window).mean()
else: # SMA
avg_gain = gain.rolling(window=window, min_periods=window).mean()
avg_loss = loss.rolling(window=window, min_periods=window).mean()
# Handle division by zero for RS calculation
# If avg_loss is 0, RS can be considered infinite (if avg_gain > 0) or undefined (if avg_gain also 0)
rs = avg_gain / avg_loss.replace(0, 1e-9) # Replace 0 with a tiny number to avoid direct division by zero warning
rsi = 100 - (100 / (1 + rs))
# Correct RSI values for edge cases where avg_loss was 0
# If avg_loss is 0 and avg_gain is > 0, RSI is 100.
# If avg_loss is 0 and avg_gain is 0, RSI is 50 (neutral).
rsi[avg_loss == 0] = np.where(avg_gain[avg_loss == 0] > 0, 100, 50)
# Ensure RSI is NaN where avg_gain or avg_loss is NaN (due to min_periods)
rsi[avg_gain.isna() | avg_loss.isna()] = np.nan
return rsi

View File

@@ -1,336 +0,0 @@
import pandas as pd
import numpy as np
import logging
from scipy.signal import find_peaks
from matplotlib.patches import Rectangle
from scipy import stats
import concurrent.futures
from functools import partial
from functools import lru_cache
import matplotlib.pyplot as plt
# Color configuration
# Plot colors
DARK_BG_COLOR = '#181C27'
LEGEND_BG_COLOR = '#333333'
TITLE_COLOR = 'white'
AXIS_LABEL_COLOR = 'white'
# Candlestick colors
CANDLE_UP_COLOR = '#089981' # Green
CANDLE_DOWN_COLOR = '#F23645' # Red
# Marker colors
MIN_COLOR = 'red'
MAX_COLOR = 'green'
# Line style colors
MIN_LINE_STYLE = 'g--' # Green dashed
MAX_LINE_STYLE = 'r--' # Red dashed
SMA7_LINE_STYLE = 'y-' # Yellow solid
SMA15_LINE_STYLE = 'm-' # Magenta solid
# SuperTrend colors
ST_COLOR_UP = 'g-'
ST_COLOR_DOWN = 'r-'
# Cache the calculation results by function parameters
@lru_cache(maxsize=32)
def cached_supertrend_calculation(period, multiplier, data_tuple):
# Convert tuple back to numpy arrays
high = np.array(data_tuple[0])
low = np.array(data_tuple[1])
close = np.array(data_tuple[2])
# Calculate TR and ATR using vectorized operations
tr = np.zeros_like(close)
tr[0] = high[0] - low[0]
hc_range = np.abs(high[1:] - close[:-1])
lc_range = np.abs(low[1:] - close[:-1])
hl_range = high[1:] - low[1:]
tr[1:] = np.maximum.reduce([hl_range, hc_range, lc_range])
# Use numpy's exponential moving average
atr = np.zeros_like(tr)
atr[0] = tr[0]
multiplier_ema = 2.0 / (period + 1)
for i in range(1, len(tr)):
atr[i] = (tr[i] * multiplier_ema) + (atr[i-1] * (1 - multiplier_ema))
# Calculate bands
upper_band = np.zeros_like(close)
lower_band = np.zeros_like(close)
for i in range(len(close)):
hl_avg = (high[i] + low[i]) / 2
upper_band[i] = hl_avg + (multiplier * atr[i])
lower_band[i] = hl_avg - (multiplier * atr[i])
final_upper = np.zeros_like(close)
final_lower = np.zeros_like(close)
supertrend = np.zeros_like(close)
trend = np.zeros_like(close)
final_upper[0] = upper_band[0]
final_lower[0] = lower_band[0]
if close[0] <= upper_band[0]:
supertrend[0] = upper_band[0]
trend[0] = -1
else:
supertrend[0] = lower_band[0]
trend[0] = 1
for i in range(1, len(close)):
if (upper_band[i] < final_upper[i-1]) or (close[i-1] > final_upper[i-1]):
final_upper[i] = upper_band[i]
else:
final_upper[i] = final_upper[i-1]
if (lower_band[i] > final_lower[i-1]) or (close[i-1] < final_lower[i-1]):
final_lower[i] = lower_band[i]
else:
final_lower[i] = final_lower[i-1]
if supertrend[i-1] == final_upper[i-1] and close[i] <= final_upper[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
elif supertrend[i-1] == final_upper[i-1] and close[i] > final_upper[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] >= final_lower[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] < final_lower[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
return {
'supertrend': supertrend,
'trend': trend,
'upper_band': final_upper,
'lower_band': final_lower
}
def calculate_supertrend_external(data, period, multiplier):
# Convert DataFrame columns to hashable tuples
high_tuple = tuple(data['high'])
low_tuple = tuple(data['low'])
close_tuple = tuple(data['close'])
# Call the cached function
return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple))
class Supertrends:
def __init__(self, data, verbose=False, display=False):
"""
Initialize the TrendDetectorSimple class.
Parameters:
- data: pandas DataFrame containing price data
- verbose: boolean, whether to display detailed logging information
- display: boolean, whether to enable display/plotting features
"""
self.data = data
self.verbose = verbose
self.display = display
# Only define display-related variables if display is True
if self.display:
# Plot style configuration
self.plot_style = 'dark_background'
self.bg_color = DARK_BG_COLOR
self.plot_size = (12, 8)
# Candlestick configuration
self.candle_width = 0.6
self.candle_up_color = CANDLE_UP_COLOR
self.candle_down_color = CANDLE_DOWN_COLOR
self.candle_alpha = 0.8
self.wick_width = 1
# Marker configuration
self.min_marker = '^'
self.min_color = MIN_COLOR
self.min_size = 100
self.max_marker = 'v'
self.max_color = MAX_COLOR
self.max_size = 100
self.marker_zorder = 100
# Line configuration
self.line_width = 1
self.min_line_style = MIN_LINE_STYLE
self.max_line_style = MAX_LINE_STYLE
self.sma7_line_style = SMA7_LINE_STYLE
self.sma15_line_style = SMA15_LINE_STYLE
# Text configuration
self.title_size = 14
self.title_color = TITLE_COLOR
self.axis_label_size = 12
self.axis_label_color = AXIS_LABEL_COLOR
# Legend configuration
self.legend_loc = 'best'
self.legend_bg_color = LEGEND_BG_COLOR
# Configure logging
logging.basicConfig(level=logging.INFO if verbose else logging.WARNING,
format='%(asctime)s - %(levelname)s - %(message)s')
self.logger = logging.getLogger('TrendDetectorSimple')
# Convert data to pandas DataFrame if it's not already
if not isinstance(self.data, pd.DataFrame):
if isinstance(self.data, list):
self.data = pd.DataFrame({'close': self.data})
else:
raise ValueError("Data must be a pandas DataFrame or a list")
def calculate_tr(self):
"""
Calculate True Range (TR) for the price data.
True Range is the greatest of:
1. Current high - current low
2. |Current high - previous close|
3. |Current low - previous close|
Returns:
- Numpy array of TR values
"""
df = self.data.copy()
high = df['high'].values
low = df['low'].values
close = df['close'].values
tr = np.zeros_like(close)
tr[0] = high[0] - low[0] # First TR is just the first day's range
for i in range(1, len(close)):
# Current high - current low
hl_range = high[i] - low[i]
# |Current high - previous close|
hc_range = abs(high[i] - close[i-1])
# |Current low - previous close|
lc_range = abs(low[i] - close[i-1])
# TR is the maximum of these three values
tr[i] = max(hl_range, hc_range, lc_range)
return tr
def calculate_atr(self, period=14):
"""
Calculate Average True Range (ATR) for the price data.
ATR is the exponential moving average of the True Range over a specified period.
Parameters:
- period: int, the period for the ATR calculation (default: 14)
Returns:
- Numpy array of ATR values
"""
tr = self.calculate_tr()
atr = np.zeros_like(tr)
# First ATR value is just the first TR
atr[0] = tr[0]
# Calculate exponential moving average (EMA) of TR
multiplier = 2.0 / (period + 1)
for i in range(1, len(tr)):
atr[i] = (tr[i] * multiplier) + (atr[i-1] * (1 - multiplier))
return atr
def detect_trends(self):
"""
Detect trends by identifying local minima and maxima in the price data
using scipy.signal.find_peaks.
Parameters:
- prominence: float, required prominence of peaks (relative to the price range)
- width: int, required width of peaks in data points
Returns:
- DataFrame with columns for timestamps, prices, and trend indicators
- Dictionary containing analysis results including linear regression, SMAs, and SuperTrend indicators
"""
df = self.data
# close_prices = df['close'].values
# max_peaks, _ = find_peaks(close_prices)
# min_peaks, _ = find_peaks(-close_prices)
# df['is_min'] = False
# df['is_max'] = False
# for peak in max_peaks:
# df.at[peak, 'is_max'] = True
# for peak in min_peaks:
# df.at[peak, 'is_min'] = True
# result = df[['timestamp', 'close', 'is_min', 'is_max']].copy()
# Perform linear regression on min_peaks and max_peaks
# min_prices = df['close'].iloc[min_peaks].values
# max_prices = df['close'].iloc[max_peaks].values
# Linear regression for min peaks if we have at least 2 points
# min_slope, min_intercept, min_r_value, _, _ = stats.linregress(min_peaks, min_prices)
# Linear regression for max peaks if we have at least 2 points
# max_slope, max_intercept, max_r_value, _, _ = stats.linregress(max_peaks, max_prices)
# Calculate Simple Moving Averages (SMA) for 7 and 15 periods
# sma_7 = pd.Series(close_prices).rolling(window=7, min_periods=1).mean().values
# sma_15 = pd.Series(close_prices).rolling(window=15, min_periods=1).mean().values
analysis_results = {}
# analysis_results['linear_regression'] = {
# 'min': {
# 'slope': min_slope,
# 'intercept': min_intercept,
# 'r_squared': min_r_value ** 2
# },
# 'max': {
# 'slope': max_slope,
# 'intercept': max_intercept,
# 'r_squared': max_r_value ** 2
# }
# }
# analysis_results['sma'] = {
# '7': sma_7,
# '15': sma_15
# }
# Calculate SuperTrend indicators
supertrend_results_list = self._calculate_supertrend_indicators()
analysis_results['supertrend'] = supertrend_results_list
return analysis_results
def calculate_supertrend_indicators(self):
"""
Calculate SuperTrend indicators with different parameter sets in parallel.
Returns:
- list, the SuperTrend results
"""
supertrend_params = [
{"period": 12, "multiplier": 3.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN},
{"period": 10, "multiplier": 1.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN},
{"period": 11, "multiplier": 2.0, "color_up": ST_COLOR_UP, "color_down": ST_COLOR_DOWN}
]
data = self.data.copy()
# For just 3 calculations, direct calculation might be faster than process pool
results = []
for p in supertrend_params:
result = calculate_supertrend_external(data, p["period"], p["multiplier"])
results.append(result)
supertrend_results_list = []
for params, result in zip(supertrend_params, results):
supertrend_results_list.append({
"results": result,
"params": params
})
return supertrend_results_list

View File

@@ -1,460 +0,0 @@
# Incremental Backtester
A high-performance backtesting system for incremental trading strategies with multiprocessing support for parameter optimization.
## Overview
The Incremental Backtester provides a complete solution for testing incremental trading strategies:
- **IncTrader**: Manages a single strategy during backtesting
- **IncBacktester**: Orchestrates multiple traders and parameter optimization
- **Multiprocessing Support**: Parallel execution across CPU cores
- **Memory Efficient**: Bounded memory usage regardless of data length
- **Real-time Compatible**: Same interface as live trading systems
## Quick Start
### 1. Basic Single Strategy Backtest
```python
from cycles.IncStrategies import (
IncBacktester, BacktestConfig, IncRandomStrategy
)
# Configure backtest
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000,
stop_loss_pct=0.02, # 2% stop loss
take_profit_pct=0.05 # 5% take profit
)
# Create strategy
strategy = IncRandomStrategy(params={
"timeframe": "15min",
"entry_probability": 0.1,
"exit_probability": 0.15
})
# Run backtest
backtester = IncBacktester(config)
results = backtester.run_single_strategy(strategy)
print(f"Profit: {results['profit_ratio']*100:.2f}%")
print(f"Trades: {results['n_trades']}")
print(f"Win Rate: {results['win_rate']*100:.1f}%")
```
### 2. Multiple Strategies
```python
strategies = [
IncRandomStrategy(params={"timeframe": "15min"}),
IncRandomStrategy(params={"timeframe": "30min"}),
IncMetaTrendStrategy(params={"timeframe": "15min"})
]
results = backtester.run_multiple_strategies(strategies)
for result in results:
print(f"{result['strategy_name']}: {result['profit_ratio']*100:.2f}%")
```
### 3. Parameter Optimization
```python
# Define parameter grids
strategy_param_grid = {
"timeframe": ["15min", "30min", "1h"],
"entry_probability": [0.05, 0.1, 0.15],
"exit_probability": [0.1, 0.15, 0.2]
}
trader_param_grid = {
"stop_loss_pct": [0.01, 0.02, 0.03],
"take_profit_pct": [0.03, 0.05, 0.07]
}
# Run optimization (uses all CPU cores)
results = backtester.optimize_parameters(
strategy_class=IncRandomStrategy,
param_grid=strategy_param_grid,
trader_param_grid=trader_param_grid,
max_workers=8 # Use 8 CPU cores
)
# Get summary statistics
summary = backtester.get_summary_statistics(results)
print(f"Best profit: {summary['profit_ratio']['max']*100:.2f}%")
# Save results
backtester.save_results(results, "optimization_results.csv")
```
## Architecture
### IncTrader Class
Manages a single strategy during backtesting:
```python
trader = IncTrader(
strategy=strategy,
initial_usd=10000,
params={
"stop_loss_pct": 0.02,
"take_profit_pct": 0.05
}
)
# Process data sequentially
for timestamp, ohlcv_data in data_stream:
trader.process_data_point(timestamp, ohlcv_data)
# Get results
results = trader.get_results()
```
**Key Features:**
- Position management (USD/coin balance)
- Trade execution based on strategy signals
- Stop loss and take profit handling
- Performance tracking and metrics
- Fee calculation using existing MarketFees
### IncBacktester Class
Orchestrates multiple traders and handles data loading:
```python
backtester = IncBacktester(config, storage)
# Single strategy
results = backtester.run_single_strategy(strategy)
# Multiple strategies
results = backtester.run_multiple_strategies(strategies)
# Parameter optimization
results = backtester.optimize_parameters(strategy_class, param_grid)
```
**Key Features:**
- Data loading using existing Storage class
- Multiprocessing for parameter optimization
- Result aggregation and analysis
- Summary statistics calculation
- CSV export functionality
### BacktestConfig Class
Configuration for backtesting runs:
```python
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000,
timeframe="1min",
# Trader parameters
stop_loss_pct=0.02,
take_profit_pct=0.05,
# Performance settings
max_workers=None, # Auto-detect CPU cores
chunk_size=1000
)
```
## Data Requirements
### Input Data Format
The backtester expects minute-level OHLCV data in CSV format:
```csv
timestamp,open,high,low,close,volume
1672531200,16625.1,16634.5,16620.0,16628.3,125.45
1672531260,16628.3,16635.2,16625.8,16631.7,98.32
...
```
**Requirements:**
- Timestamp column (Unix timestamp or datetime)
- OHLCV columns: open, high, low, close, volume
- Minute-level frequency (strategies handle timeframe aggregation)
- Sorted by timestamp (ascending)
### Data Loading
Uses the existing Storage class for data loading:
```python
from cycles.utils.storage import Storage
storage = Storage()
data = storage.load_data(
"btc_1min_2023.csv",
"2023-01-01",
"2023-12-31"
)
```
## Performance Features
### Multiprocessing Support
Parameter optimization automatically distributes work across CPU cores:
```python
# Automatic CPU detection
results = backtester.optimize_parameters(strategy_class, param_grid)
# Manual worker count
results = backtester.optimize_parameters(
strategy_class, param_grid, max_workers=4
)
# Single-threaded (for debugging)
results = backtester.optimize_parameters(
strategy_class, param_grid, max_workers=1
)
```
### Memory Efficiency
- **Bounded Memory**: Strategy buffers have fixed size limits
- **Incremental Processing**: No need to load entire datasets into memory
- **Efficient Data Structures**: Optimized for sequential processing
### Performance Monitoring
Built-in performance tracking:
```python
results = backtester.run_single_strategy(strategy)
print(f"Backtest duration: {results['backtest_duration_seconds']:.2f}s")
print(f"Data points processed: {results['data_points_processed']}")
print(f"Processing rate: {results['data_points']/results['backtest_duration_seconds']:.0f} points/sec")
```
## Result Analysis
### Individual Results
Each backtest returns comprehensive metrics:
```python
{
"strategy_name": "IncRandomStrategy",
"strategy_params": {"timeframe": "15min", ...},
"trader_params": {"stop_loss_pct": 0.02, ...},
"initial_usd": 10000.0,
"final_usd": 10250.0,
"profit_ratio": 0.025,
"n_trades": 15,
"win_rate": 0.6,
"max_drawdown": 0.08,
"avg_trade": 0.0167,
"total_fees_usd": 45.32,
"trades": [...], # Individual trade records
"backtest_duration_seconds": 2.45
}
```
### Summary Statistics
For parameter optimization runs:
```python
summary = backtester.get_summary_statistics(results)
{
"total_runs": 108,
"successful_runs": 105,
"failed_runs": 3,
"profit_ratio": {
"mean": 0.023,
"std": 0.045,
"min": -0.12,
"max": 0.18,
"median": 0.019
},
"best_run": {...},
"worst_run": {...}
}
```
### Export Results
Save results to CSV for further analysis:
```python
backtester.save_results(results, "backtest_results.csv")
```
Output includes:
- Strategy and trader parameters
- Performance metrics
- Trade statistics
- Execution timing
## Integration with Existing System
### Compatibility
The incremental backtester integrates seamlessly with existing components:
- **Storage Class**: Uses existing data loading infrastructure
- **MarketFees**: Uses existing fee calculation
- **Strategy Interface**: Compatible with incremental strategies
- **Result Format**: Similar to existing Backtest class
### Migration from Original Backtester
```python
# Original backtester
from cycles.backtest import Backtest
# Incremental backtester
from cycles.IncStrategies import IncBacktester, BacktestConfig
# Similar interface, enhanced capabilities
config = BacktestConfig(...)
backtester = IncBacktester(config)
results = backtester.run_single_strategy(strategy)
```
## Testing
### Synthetic Data Testing
Test with synthetic data before using real market data:
```python
from cycles.IncStrategies.test_inc_backtester import main
# Run all tests
main()
```
### Unit Tests
Individual component testing:
```python
# Test IncTrader
from cycles.IncStrategies.test_inc_backtester import test_inc_trader
test_inc_trader()
# Test IncBacktester
from cycles.IncStrategies.test_inc_backtester import test_inc_backtester
test_inc_backtester()
```
## Examples
See `example_backtest.py` for comprehensive usage examples:
```python
from cycles.IncStrategies.example_backtest import (
example_single_strategy_backtest,
example_parameter_optimization,
example_custom_analysis
)
# Run examples
example_single_strategy_backtest()
example_parameter_optimization()
```
## Best Practices
### 1. Data Preparation
- Ensure data quality (no gaps, correct format)
- Use appropriate date ranges for testing
- Consider market conditions in test periods
### 2. Parameter Optimization
- Start with small parameter grids for testing
- Use representative time periods
- Consider overfitting risks
- Validate results on out-of-sample data
### 3. Performance Optimization
- Use multiprocessing for large parameter grids
- Monitor memory usage for long backtests
- Profile bottlenecks for optimization
### 4. Result Validation
- Compare with original backtester for validation
- Check trade logic manually for small samples
- Verify fee calculations and position management
## Troubleshooting
### Common Issues
1. **Data Loading Errors**
- Check file path and format
- Verify date range availability
- Ensure required columns exist
2. **Strategy Errors**
- Check strategy initialization
- Verify parameter validity
- Monitor warmup period completion
3. **Performance Issues**
- Reduce parameter grid size
- Limit worker count for memory constraints
- Use shorter time periods for testing
### Debug Mode
Enable detailed logging:
```python
import logging
logging.basicConfig(level=logging.DEBUG)
# Run with detailed output
results = backtester.run_single_strategy(strategy)
```
### Memory Monitoring
Monitor memory usage during optimization:
```python
import psutil
import os
process = psutil.Process(os.getpid())
print(f"Memory usage: {process.memory_info().rss / 1024 / 1024:.1f} MB")
```
## Future Enhancements
- **Live Trading Integration**: Direct connection to trading systems
- **Advanced Analytics**: Risk metrics, Sharpe ratio, etc.
- **Visualization**: Built-in plotting and analysis tools
- **Database Support**: Direct database connectivity
- **Strategy Combinations**: Multi-strategy portfolio testing
## Support
For issues and questions:
1. Check the test scripts for working examples
2. Review the TODO.md for known limitations
3. Examine the base strategy implementations
4. Use debug logging for detailed troubleshooting

View File

@@ -1,71 +0,0 @@
"""
Incremental Strategies Module
This module contains the incremental calculation implementation of trading strategies
that support real-time data processing with efficient memory usage and performance.
The incremental strategies are designed to:
- Process new data points incrementally without full recalculation
- Maintain bounded memory usage regardless of data history length
- Provide identical results to batch calculations
- Support real-time trading with minimal latency
Classes:
IncStrategyBase: Base class for all incremental strategies
IncRandomStrategy: Incremental implementation of random strategy for testing
IncMetaTrendStrategy: Incremental implementation of the MetaTrend strategy
IncDefaultStrategy: Incremental implementation of the default Supertrend strategy
IncBBRSStrategy: Incremental implementation of Bollinger Bands + RSI strategy
IncStrategyManager: Manager for coordinating multiple incremental strategies
IncTrader: Trader that manages a single strategy during backtesting
IncBacktester: Backtester for testing incremental strategies with multiprocessing
BacktestConfig: Configuration class for backtesting runs
"""
from .base import IncStrategyBase, IncStrategySignal
from .random_strategy import IncRandomStrategy
from .metatrend_strategy import IncMetaTrendStrategy, MetaTrendStrategy
from .inc_trader import IncTrader, TradeRecord
from .inc_backtester import IncBacktester, BacktestConfig
# Note: These will be implemented in subsequent phases
# from .default_strategy import IncDefaultStrategy
# from .bbrs_strategy import IncBBRSStrategy
# from .manager import IncStrategyManager
# Strategy registry for easy access
AVAILABLE_STRATEGIES = {
'random': IncRandomStrategy,
'metatrend': IncMetaTrendStrategy,
'meta_trend': IncMetaTrendStrategy, # Alternative name
# 'default': IncDefaultStrategy,
# 'bbrs': IncBBRSStrategy,
}
__all__ = [
# Base classes
'IncStrategyBase',
'IncStrategySignal',
# Strategies
'IncRandomStrategy',
'IncMetaTrendStrategy',
'MetaTrendStrategy',
# Backtesting components
'IncTrader',
'IncBacktester',
'BacktestConfig',
'TradeRecord',
# Registry
'AVAILABLE_STRATEGIES'
# Future implementations
# 'IncDefaultStrategy',
# 'IncBBRSStrategy',
# 'IncStrategyManager'
]
__version__ = '1.0.0'

View File

@@ -1,649 +0,0 @@
"""
Base classes for the incremental strategy system.
This module contains the fundamental building blocks for all incremental trading strategies:
- IncStrategySignal: Represents trading signals with confidence and metadata
- IncStrategyBase: Abstract base class that all incremental strategies must inherit from
- TimeframeAggregator: Built-in timeframe aggregation for minute-level data processing
"""
import pandas as pd
from abc import ABC, abstractmethod
from typing import Dict, Optional, List, Union, Any
from collections import deque
import logging
# Import the original signal class for compatibility
from ..strategies.base import StrategySignal
# Create alias for consistency
IncStrategySignal = StrategySignal
class TimeframeAggregator:
"""
Handles real-time aggregation of minute data to higher timeframes.
This class accumulates minute-level OHLCV data and produces complete
bars when a timeframe period is completed. Integrated into IncStrategyBase
to provide consistent minute-level data processing across all strategies.
"""
def __init__(self, timeframe_minutes: int = 15):
"""
Initialize timeframe aggregator.
Args:
timeframe_minutes: Target timeframe in minutes (e.g., 60 for 1h, 15 for 15min)
"""
self.timeframe_minutes = timeframe_minutes
self.current_bar = None
self.current_bar_start = None
self.last_completed_bar = None
def update(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, float]]:
"""
Update with new minute data and return completed bar if timeframe is complete.
Args:
timestamp: Timestamp of the data
ohlcv_data: OHLCV data dictionary
Returns:
Completed OHLCV bar if timeframe period ended, None otherwise
"""
# Calculate which timeframe bar this timestamp belongs to
bar_start = self._get_bar_start_time(timestamp)
# Check if we're starting a new bar
if self.current_bar_start != bar_start:
# Save the completed bar (if any)
completed_bar = self.current_bar.copy() if self.current_bar is not None else None
# Start new bar
self.current_bar_start = bar_start
self.current_bar = {
'timestamp': bar_start,
'open': ohlcv_data['close'], # Use current close as open for new bar
'high': ohlcv_data['close'],
'low': ohlcv_data['close'],
'close': ohlcv_data['close'],
'volume': ohlcv_data['volume']
}
# Return the completed bar (if any)
if completed_bar is not None:
self.last_completed_bar = completed_bar
return completed_bar
else:
# Update current bar with new data
if self.current_bar is not None:
self.current_bar['high'] = max(self.current_bar['high'], ohlcv_data['high'])
self.current_bar['low'] = min(self.current_bar['low'], ohlcv_data['low'])
self.current_bar['close'] = ohlcv_data['close']
self.current_bar['volume'] += ohlcv_data['volume']
return None # No completed bar yet
def _get_bar_start_time(self, timestamp: pd.Timestamp) -> pd.Timestamp:
"""Calculate the start time of the timeframe bar for given timestamp.
This method now aligns with pandas resampling to ensure consistency
with the original strategy's bar boundaries.
"""
# Use pandas-style resampling alignment
# This ensures bars align to standard boundaries (e.g., 00:00, 00:15, 00:30, 00:45)
freq_str = f'{self.timeframe_minutes}min'
# Create a temporary series with the timestamp and resample to get the bar start
temp_series = pd.Series([1], index=[timestamp])
resampled = temp_series.resample(freq_str)
# Get the first group's name (which is the bar start time)
for bar_start, _ in resampled:
return bar_start
# Fallback to original method if resampling fails
minutes_since_midnight = timestamp.hour * 60 + timestamp.minute
bar_minutes = (minutes_since_midnight // self.timeframe_minutes) * self.timeframe_minutes
return timestamp.replace(
hour=bar_minutes // 60,
minute=bar_minutes % 60,
second=0,
microsecond=0
)
def get_current_bar(self) -> Optional[Dict[str, float]]:
"""Get the current incomplete bar (for debugging)."""
return self.current_bar.copy() if self.current_bar is not None else None
def reset(self):
"""Reset aggregator state."""
self.current_bar = None
self.current_bar_start = None
self.last_completed_bar = None
class IncStrategyBase(ABC):
"""
Abstract base class for all incremental trading strategies.
This class defines the interface that all incremental strategies must implement:
- get_minimum_buffer_size(): Specify minimum data requirements
- calculate_on_data(): Process new data points incrementally
- supports_incremental_calculation(): Whether strategy supports incremental mode
- get_entry_signal(): Generate entry signals
- get_exit_signal(): Generate exit signals
The incremental approach allows strategies to:
- Process new data points without full recalculation
- Maintain bounded memory usage regardless of data history length
- Provide real-time performance with minimal latency
- Support both initialization and incremental modes
- Accept minute-level data and internally aggregate to any timeframe
New Features:
- Built-in TimeframeAggregator for minute-level data processing
- update_minute_data() method for real-time trading systems
- Automatic timeframe detection and aggregation
- Backward compatibility with existing update() methods
Attributes:
name (str): Strategy name
weight (float): Strategy weight for combination
params (Dict): Strategy parameters
calculation_mode (str): Current mode ('initialization' or 'incremental')
is_warmed_up (bool): Whether strategy has sufficient data for reliable signals
timeframe_buffers (Dict): Rolling buffers for different timeframes
indicator_states (Dict): Internal indicator calculation states
timeframe_aggregator (TimeframeAggregator): Built-in aggregator for minute data
Example:
class MyIncStrategy(IncStrategyBase):
def get_minimum_buffer_size(self):
return {"15min": 50} # Strategy works on 15min timeframe
def calculate_on_data(self, new_data_point, timestamp):
# Process new data incrementally
self._update_indicators(new_data_point)
def get_entry_signal(self):
# Generate signal based on current state
if self._should_enter():
return IncStrategySignal("ENTRY", confidence=0.8)
return IncStrategySignal("HOLD", confidence=0.0)
# Usage with minute-level data:
strategy = MyIncStrategy(params={"timeframe_minutes": 15})
for minute_data in live_stream:
result = strategy.update_minute_data(minute_data['timestamp'], minute_data)
if result is not None: # Complete 15min bar formed
entry_signal = strategy.get_entry_signal()
"""
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
"""
Initialize the incremental strategy base.
Args:
name: Strategy name/identifier
weight: Strategy weight for combination (default: 1.0)
params: Strategy-specific parameters
"""
self.name = name
self.weight = weight
self.params = params or {}
# Calculation state
self._calculation_mode = "initialization"
self._is_warmed_up = False
self._data_points_received = 0
# Timeframe management
self._timeframe_buffers = {}
self._timeframe_last_update = {}
self._buffer_size_multiplier = self.params.get("buffer_size_multiplier", 2.0)
# Built-in timeframe aggregation
self._primary_timeframe_minutes = self._extract_timeframe_minutes()
self._timeframe_aggregator = None
if self._primary_timeframe_minutes > 1:
self._timeframe_aggregator = TimeframeAggregator(self._primary_timeframe_minutes)
# Indicator states (strategy-specific)
self._indicator_states = {}
# Signal generation state
self._last_signals = {}
self._signal_history = deque(maxlen=100)
# Error handling
self._max_acceptable_gap = pd.Timedelta(self.params.get("max_acceptable_gap", "5min"))
self._state_validation_enabled = self.params.get("enable_state_validation", True)
# Performance monitoring
self._performance_metrics = {
'update_times': deque(maxlen=1000),
'signal_generation_times': deque(maxlen=1000),
'state_validation_failures': 0,
'data_gaps_handled': 0,
'minute_data_points_processed': 0,
'timeframe_bars_completed': 0
}
# Compatibility with original strategy interface
self.initialized = False
self.timeframes_data = {}
def _extract_timeframe_minutes(self) -> int:
"""
Extract timeframe in minutes from strategy parameters.
Looks for timeframe configuration in various parameter formats:
- timeframe_minutes: Direct specification in minutes
- timeframe: String format like "15min", "1h", etc.
Returns:
int: Timeframe in minutes (default: 1 for minute-level processing)
"""
# Direct specification
if "timeframe_minutes" in self.params:
return self.params["timeframe_minutes"]
# String format parsing
timeframe_str = self.params.get("timeframe", "1min")
if timeframe_str.endswith("min"):
return int(timeframe_str[:-3])
elif timeframe_str.endswith("h"):
return int(timeframe_str[:-1]) * 60
elif timeframe_str.endswith("d"):
return int(timeframe_str[:-1]) * 60 * 24
else:
# Default to 1 minute if can't parse
return 1
def update_minute_data(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, Any]]:
"""
Update strategy with minute-level OHLCV data.
This method provides a standardized interface for real-time trading systems
that receive minute-level data. It internally aggregates to the strategy's
configured timeframe and only processes indicators when complete bars are formed.
Args:
timestamp: Timestamp of the minute data
ohlcv_data: Dictionary with 'open', 'high', 'low', 'close', 'volume'
Returns:
Strategy processing result if timeframe bar completed, None otherwise
Example:
# Process live minute data
result = strategy.update_minute_data(
timestamp=pd.Timestamp('2024-01-01 10:15:00'),
ohlcv_data={
'open': 100.0,
'high': 101.0,
'low': 99.5,
'close': 100.5,
'volume': 1000.0
}
)
if result is not None:
# A complete timeframe bar was formed and processed
entry_signal = strategy.get_entry_signal()
"""
self._performance_metrics['minute_data_points_processed'] += 1
# If no aggregator (1min strategy), process directly
if self._timeframe_aggregator is None:
self.calculate_on_data(ohlcv_data, timestamp)
return {
'timestamp': timestamp,
'timeframe_minutes': 1,
'processed_directly': True,
'is_warmed_up': self.is_warmed_up
}
# Use aggregator to accumulate minute data
completed_bar = self._timeframe_aggregator.update(timestamp, ohlcv_data)
if completed_bar is not None:
# A complete timeframe bar was formed
self._performance_metrics['timeframe_bars_completed'] += 1
# Process the completed bar
self.calculate_on_data(completed_bar, completed_bar['timestamp'])
# Return processing result
return {
'timestamp': completed_bar['timestamp'],
'timeframe_minutes': self._primary_timeframe_minutes,
'bar_data': completed_bar,
'is_warmed_up': self.is_warmed_up,
'processed_bar': True
}
# No complete bar yet
return None
def get_current_incomplete_bar(self) -> Optional[Dict[str, float]]:
"""
Get the current incomplete timeframe bar (for monitoring).
Useful for debugging and monitoring the aggregation process.
Returns:
Current incomplete bar data or None if no aggregator
"""
if self._timeframe_aggregator is not None:
return self._timeframe_aggregator.get_current_bar()
return None
@property
def calculation_mode(self) -> str:
"""Current calculation mode: 'initialization' or 'incremental'"""
return self._calculation_mode
@property
def is_warmed_up(self) -> bool:
"""Whether strategy has sufficient data for reliable signals"""
return self._is_warmed_up
@abstractmethod
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for each timeframe.
This method must be implemented by each strategy to specify how much
historical data is required for reliable calculations.
Returns:
Dict[str, int]: {timeframe: min_points} mapping
Example:
return {"15min": 50, "1min": 750} # 50 15min candles = 750 1min candles
"""
pass
@abstractmethod
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
This method is called for each new data point and should update
the strategy's internal state incrementally.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
pass
@abstractmethod
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Returns:
bool: True if incremental mode supported, False for fallback to batch mode
"""
pass
@abstractmethod
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate entry signal based on current strategy state.
This method should use the current internal state to determine
whether an entry signal should be generated.
Returns:
IncStrategySignal: Entry signal with confidence level
"""
pass
@abstractmethod
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate exit signal based on current strategy state.
This method should use the current internal state to determine
whether an exit signal should be generated.
Returns:
IncStrategySignal: Exit signal with confidence level
"""
pass
def get_confidence(self) -> float:
"""
Get strategy confidence for the current market state.
Default implementation returns 1.0. Strategies can override
this to provide dynamic confidence based on market conditions.
Returns:
float: Confidence level (0.0 to 1.0)
"""
return 1.0
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
self._calculation_mode = "initialization"
self._is_warmed_up = False
self._data_points_received = 0
self._timeframe_buffers.clear()
self._timeframe_last_update.clear()
self._indicator_states.clear()
self._last_signals.clear()
self._signal_history.clear()
# Reset timeframe aggregator
if self._timeframe_aggregator is not None:
self._timeframe_aggregator.reset()
# Reset performance metrics
for key in self._performance_metrics:
if isinstance(self._performance_metrics[key], deque):
self._performance_metrics[key].clear()
else:
self._performance_metrics[key] = 0
def get_current_state_summary(self) -> Dict[str, Any]:
"""Get summary of current calculation state for debugging."""
return {
'strategy_name': self.name,
'calculation_mode': self._calculation_mode,
'is_warmed_up': self._is_warmed_up,
'data_points_received': self._data_points_received,
'timeframes': list(self._timeframe_buffers.keys()),
'buffer_sizes': {tf: len(buf) for tf, buf in self._timeframe_buffers.items()},
'indicator_states': {name: state.get_state_summary() if hasattr(state, 'get_state_summary') else str(state)
for name, state in self._indicator_states.items()},
'last_signals': self._last_signals,
'timeframe_aggregator': {
'enabled': self._timeframe_aggregator is not None,
'primary_timeframe_minutes': self._primary_timeframe_minutes,
'current_incomplete_bar': self.get_current_incomplete_bar()
},
'performance_metrics': {
'avg_update_time': sum(self._performance_metrics['update_times']) / len(self._performance_metrics['update_times'])
if self._performance_metrics['update_times'] else 0,
'avg_signal_time': sum(self._performance_metrics['signal_generation_times']) / len(self._performance_metrics['signal_generation_times'])
if self._performance_metrics['signal_generation_times'] else 0,
'validation_failures': self._performance_metrics['state_validation_failures'],
'data_gaps_handled': self._performance_metrics['data_gaps_handled'],
'minute_data_points_processed': self._performance_metrics['minute_data_points_processed'],
'timeframe_bars_completed': self._performance_metrics['timeframe_bars_completed']
}
}
def _update_timeframe_buffers(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""Update all timeframe buffers with new data point."""
# Get minimum buffer sizes
min_buffer_sizes = self.get_minimum_buffer_size()
for timeframe in min_buffer_sizes.keys():
# Calculate actual buffer size with multiplier
min_size = min_buffer_sizes[timeframe]
actual_buffer_size = int(min_size * self._buffer_size_multiplier)
# Initialize buffer if needed
if timeframe not in self._timeframe_buffers:
self._timeframe_buffers[timeframe] = deque(maxlen=actual_buffer_size)
self._timeframe_last_update[timeframe] = None
# Check if this timeframe should be updated
if self._should_update_timeframe(timeframe, timestamp):
# For 1min timeframe, add data directly
if timeframe == "1min":
data_point = new_data_point.copy()
data_point['timestamp'] = timestamp
self._timeframe_buffers[timeframe].append(data_point)
self._timeframe_last_update[timeframe] = timestamp
else:
# For other timeframes, we need to aggregate from 1min data
self._aggregate_to_timeframe(timeframe, new_data_point, timestamp)
def _should_update_timeframe(self, timeframe: str, timestamp: pd.Timestamp) -> bool:
"""Check if timeframe should be updated based on timestamp."""
if timeframe == "1min":
return True # Always update 1min
last_update = self._timeframe_last_update.get(timeframe)
if last_update is None:
return True # First update
# Calculate timeframe interval
if timeframe.endswith("min"):
minutes = int(timeframe[:-3])
interval = pd.Timedelta(minutes=minutes)
elif timeframe.endswith("h"):
hours = int(timeframe[:-1])
interval = pd.Timedelta(hours=hours)
else:
return True # Unknown timeframe, update anyway
# Check if enough time has passed
return timestamp >= last_update + interval
def _aggregate_to_timeframe(self, timeframe: str, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""Aggregate 1min data to specified timeframe."""
# This is a simplified aggregation - in practice, you might want more sophisticated logic
buffer = self._timeframe_buffers[timeframe]
# If buffer is empty or we're starting a new period, add new candle
if not buffer or self._should_update_timeframe(timeframe, timestamp):
aggregated_point = new_data_point.copy()
aggregated_point['timestamp'] = timestamp
buffer.append(aggregated_point)
self._timeframe_last_update[timeframe] = timestamp
else:
# Update the last candle in the buffer
last_candle = buffer[-1]
last_candle['high'] = max(last_candle['high'], new_data_point['high'])
last_candle['low'] = min(last_candle['low'], new_data_point['low'])
last_candle['close'] = new_data_point['close']
last_candle['volume'] += new_data_point['volume']
def _get_timeframe_buffer(self, timeframe: str) -> pd.DataFrame:
"""Get current buffer for specific timeframe as DataFrame."""
if timeframe not in self._timeframe_buffers:
return pd.DataFrame()
buffer_data = list(self._timeframe_buffers[timeframe])
if not buffer_data:
return pd.DataFrame()
df = pd.DataFrame(buffer_data)
if 'timestamp' in df.columns:
df = df.set_index('timestamp')
return df
def _validate_calculation_state(self) -> bool:
"""Validate internal calculation state consistency."""
if not self._state_validation_enabled:
return True
try:
# Check that all required buffers exist
min_buffer_sizes = self.get_minimum_buffer_size()
for timeframe in min_buffer_sizes.keys():
if timeframe not in self._timeframe_buffers:
logging.warning(f"Missing buffer for timeframe {timeframe}")
return False
# Check that indicator states are valid
for name, state in self._indicator_states.items():
if hasattr(state, 'is_initialized') and not state.is_initialized:
logging.warning(f"Indicator {name} not initialized")
return False
return True
except Exception as e:
logging.error(f"State validation failed: {e}")
self._performance_metrics['state_validation_failures'] += 1
return False
def _recover_from_state_corruption(self) -> None:
"""Recover from corrupted calculation state."""
logging.warning(f"Recovering from state corruption in strategy {self.name}")
# Reset to initialization mode
self._calculation_mode = "initialization"
self._is_warmed_up = False
# Try to recalculate from available buffer data
try:
self._reinitialize_from_buffers()
except Exception as e:
logging.error(f"Failed to recover from buffers: {e}")
# Complete reset as last resort
self.reset_calculation_state()
def _reinitialize_from_buffers(self) -> None:
"""Reinitialize indicators from available buffer data."""
# This method should be overridden by specific strategies
# to implement their own recovery logic
pass
def handle_data_gap(self, gap_duration: pd.Timedelta) -> None:
"""Handle gaps in data stream."""
self._performance_metrics['data_gaps_handled'] += 1
if gap_duration > self._max_acceptable_gap:
logging.warning(f"Data gap {gap_duration} exceeds maximum acceptable gap {self._max_acceptable_gap}")
self._trigger_reinitialization()
else:
logging.info(f"Handling acceptable data gap: {gap_duration}")
# For small gaps, continue with current state
def _trigger_reinitialization(self) -> None:
"""Trigger strategy reinitialization due to data gap or corruption."""
logging.info(f"Triggering reinitialization for strategy {self.name}")
self.reset_calculation_state()
# Compatibility methods for original strategy interface
def get_timeframes(self) -> List[str]:
"""Get required timeframes (compatibility method)."""
return list(self.get_minimum_buffer_size().keys())
def initialize(self, backtester) -> None:
"""Initialize strategy (compatibility method)."""
# This method provides compatibility with the original strategy interface
# The actual initialization happens through the incremental interface
self.initialized = True
logging.info(f"Incremental strategy {self.name} initialized in compatibility mode")
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"{self.__class__.__name__}(name={self.name}, "
f"weight={self.weight}, mode={self._calculation_mode}, "
f"warmed_up={self._is_warmed_up}, "
f"data_points={self._data_points_received})")

View File

@@ -1,532 +0,0 @@
"""
Incremental BBRS Strategy
This module implements an incremental version of the Bollinger Bands + RSI Strategy (BBRS)
for real-time data processing. It maintains constant memory usage and provides
identical results to the batch implementation after the warm-up period.
Key Features:
- Accepts minute-level data input for real-time compatibility
- Internal timeframe aggregation (1min, 5min, 15min, 1h, etc.)
- Incremental Bollinger Bands calculation
- Incremental RSI calculation with Wilder's smoothing
- Market regime detection (trending vs sideways)
- Real-time signal generation
- Constant memory usage
"""
from typing import Dict, Optional, Union, Tuple
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
from .indicators.bollinger_bands import BollingerBandsState
from .indicators.rsi import RSIState
class TimeframeAggregator:
"""
Handles real-time aggregation of minute data to higher timeframes.
This class accumulates minute-level OHLCV data and produces complete
bars when a timeframe period is completed.
"""
def __init__(self, timeframe_minutes: int = 15):
"""
Initialize timeframe aggregator.
Args:
timeframe_minutes: Target timeframe in minutes (e.g., 60 for 1h, 15 for 15min)
"""
self.timeframe_minutes = timeframe_minutes
self.current_bar = None
self.current_bar_start = None
self.last_completed_bar = None
def update(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, float]]:
"""
Update with new minute data and return completed bar if timeframe is complete.
Args:
timestamp: Timestamp of the data
ohlcv_data: OHLCV data dictionary
Returns:
Completed OHLCV bar if timeframe period ended, None otherwise
"""
# Calculate which timeframe bar this timestamp belongs to
bar_start = self._get_bar_start_time(timestamp)
# Check if we're starting a new bar
if self.current_bar_start != bar_start:
# Save the completed bar (if any)
completed_bar = self.current_bar.copy() if self.current_bar is not None else None
# Start new bar
self.current_bar_start = bar_start
self.current_bar = {
'timestamp': bar_start,
'open': ohlcv_data['close'], # Use current close as open for new bar
'high': ohlcv_data['close'],
'low': ohlcv_data['close'],
'close': ohlcv_data['close'],
'volume': ohlcv_data['volume']
}
# Return the completed bar (if any)
if completed_bar is not None:
self.last_completed_bar = completed_bar
return completed_bar
else:
# Update current bar with new data
if self.current_bar is not None:
self.current_bar['high'] = max(self.current_bar['high'], ohlcv_data['high'])
self.current_bar['low'] = min(self.current_bar['low'], ohlcv_data['low'])
self.current_bar['close'] = ohlcv_data['close']
self.current_bar['volume'] += ohlcv_data['volume']
return None # No completed bar yet
def _get_bar_start_time(self, timestamp: pd.Timestamp) -> pd.Timestamp:
"""Calculate the start time of the timeframe bar for given timestamp."""
# Round down to the nearest timeframe boundary
minutes_since_midnight = timestamp.hour * 60 + timestamp.minute
bar_minutes = (minutes_since_midnight // self.timeframe_minutes) * self.timeframe_minutes
return timestamp.replace(
hour=bar_minutes // 60,
minute=bar_minutes % 60,
second=0,
microsecond=0
)
def get_current_bar(self) -> Optional[Dict[str, float]]:
"""Get the current incomplete bar (for debugging)."""
return self.current_bar.copy() if self.current_bar is not None else None
def reset(self):
"""Reset aggregator state."""
self.current_bar = None
self.current_bar_start = None
self.last_completed_bar = None
class BBRSIncrementalState:
"""
Incremental BBRS strategy state for real-time processing.
This class maintains all the state needed for the BBRS strategy and can
process new minute-level price data incrementally, internally aggregating
to the configured timeframe before running indicators.
Attributes:
timeframe_minutes (int): Strategy timeframe in minutes (default: 60 for 1h)
bb_period (int): Bollinger Bands period
rsi_period (int): RSI period
bb_width_threshold (float): BB width threshold for market regime detection
trending_bb_multiplier (float): BB multiplier for trending markets
sideways_bb_multiplier (float): BB multiplier for sideways markets
trending_rsi_thresholds (tuple): RSI thresholds for trending markets (low, high)
sideways_rsi_thresholds (tuple): RSI thresholds for sideways markets (low, high)
squeeze_strategy (bool): Enable squeeze strategy
Example:
# Initialize strategy for 1-hour timeframe
config = {
"timeframe_minutes": 60, # 1 hour bars
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"bb_std_dev_multiplier": 2.5,
"rsi_threshold": [30, 70]
},
"sideways": {
"bb_std_dev_multiplier": 1.8,
"rsi_threshold": [40, 60]
},
"SqueezeStrategy": True
}
strategy = BBRSIncrementalState(config)
# Process minute-level data in real-time
for minute_data in live_data_stream:
result = strategy.update_minute_data(minute_data['timestamp'], minute_data)
if result is not None: # New timeframe bar completed
if result['buy_signal']:
print("Buy signal generated!")
"""
def __init__(self, config: Dict):
"""
Initialize incremental BBRS strategy.
Args:
config: Strategy configuration dictionary
"""
# Store configuration
self.timeframe_minutes = config.get("timeframe_minutes", 60) # Default to 1 hour
self.bb_period = config.get("bb_period", 20)
self.rsi_period = config.get("rsi_period", 14)
self.bb_width_threshold = config.get("bb_width", 0.05)
# Market regime specific parameters
trending_config = config.get("trending", {})
sideways_config = config.get("sideways", {})
self.trending_bb_multiplier = trending_config.get("bb_std_dev_multiplier", 2.5)
self.sideways_bb_multiplier = sideways_config.get("bb_std_dev_multiplier", 1.8)
self.trending_rsi_thresholds = tuple(trending_config.get("rsi_threshold", [30, 70]))
self.sideways_rsi_thresholds = tuple(sideways_config.get("rsi_threshold", [40, 60]))
self.squeeze_strategy = config.get("SqueezeStrategy", True)
# Initialize timeframe aggregator
self.aggregator = TimeframeAggregator(self.timeframe_minutes)
# Initialize indicators with different multipliers for regime detection
self.bb_trending = BollingerBandsState(self.bb_period, self.trending_bb_multiplier)
self.bb_sideways = BollingerBandsState(self.bb_period, self.sideways_bb_multiplier)
self.bb_reference = BollingerBandsState(self.bb_period, 2.0) # For regime detection
self.rsi = RSIState(self.rsi_period)
# State tracking
self.bars_processed = 0
self.current_price = None
self.current_volume = None
self.volume_ma = None
self.volume_sum = 0.0
self.volume_history = [] # For volume MA calculation
# Signal state
self.last_buy_signal = False
self.last_sell_signal = False
self.last_result = None
def update_minute_data(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, Union[float, bool]]]:
"""
Update strategy with new minute-level OHLCV data.
This method accepts minute-level data and internally aggregates to the
configured timeframe. It only processes indicators and generates signals
when a complete timeframe bar is formed.
Args:
timestamp: Timestamp of the minute data
ohlcv_data: Dictionary with 'open', 'high', 'low', 'close', 'volume'
Returns:
Strategy result dictionary if a timeframe bar completed, None otherwise
"""
# Validate input
required_keys = ['open', 'high', 'low', 'close', 'volume']
for key in required_keys:
if key not in ohlcv_data:
raise ValueError(f"Missing required key: {key}")
# Update timeframe aggregator
completed_bar = self.aggregator.update(timestamp, ohlcv_data)
if completed_bar is not None:
# Process the completed timeframe bar
return self._process_timeframe_bar(completed_bar)
return None # No completed bar yet
def update(self, ohlcv_data: Dict[str, float]) -> Dict[str, Union[float, bool]]:
"""
Update strategy with pre-aggregated timeframe data (for testing/compatibility).
This method is for backward compatibility and testing with pre-aggregated data.
For real-time use, prefer update_minute_data().
Args:
ohlcv_data: Dictionary with 'open', 'high', 'low', 'close', 'volume'
Returns:
Strategy result dictionary
"""
# Create a fake timestamp for compatibility
fake_timestamp = pd.Timestamp.now()
# Process directly as a completed bar
completed_bar = {
'timestamp': fake_timestamp,
'open': ohlcv_data['open'],
'high': ohlcv_data['high'],
'low': ohlcv_data['low'],
'close': ohlcv_data['close'],
'volume': ohlcv_data['volume']
}
return self._process_timeframe_bar(completed_bar)
def _process_timeframe_bar(self, bar_data: Dict[str, float]) -> Dict[str, Union[float, bool]]:
"""
Process a completed timeframe bar and generate signals.
Args:
bar_data: Completed timeframe bar data
Returns:
Strategy result dictionary
"""
close_price = float(bar_data['close'])
volume = float(bar_data['volume'])
# Update indicators
bb_trending_result = self.bb_trending.update(close_price)
bb_sideways_result = self.bb_sideways.update(close_price)
bb_reference_result = self.bb_reference.update(close_price)
rsi_value = self.rsi.update(close_price)
# Update volume tracking
self._update_volume_tracking(volume)
# Determine market regime
market_regime = self._determine_market_regime(bb_reference_result)
# Select appropriate BB values based on regime
if market_regime == "sideways":
bb_result = bb_sideways_result
rsi_thresholds = self.sideways_rsi_thresholds
else: # trending
bb_result = bb_trending_result
rsi_thresholds = self.trending_rsi_thresholds
# Generate signals
buy_signal, sell_signal = self._generate_signals(
close_price, volume, bb_result, rsi_value,
market_regime, rsi_thresholds
)
# Update state
self.current_price = close_price
self.current_volume = volume
self.bars_processed += 1
self.last_buy_signal = buy_signal
self.last_sell_signal = sell_signal
# Create comprehensive result
result = {
# Timeframe info
'timestamp': bar_data['timestamp'],
'timeframe_minutes': self.timeframe_minutes,
# Price data
'open': bar_data['open'],
'high': bar_data['high'],
'low': bar_data['low'],
'close': close_price,
'volume': volume,
# Bollinger Bands (regime-specific)
'upper_band': bb_result['upper_band'],
'middle_band': bb_result['middle_band'],
'lower_band': bb_result['lower_band'],
'bb_width': bb_result['bandwidth'],
# RSI
'rsi': rsi_value,
# Market regime
'market_regime': market_regime,
'bb_width_reference': bb_reference_result['bandwidth'],
# Volume analysis
'volume_ma': self.volume_ma,
'volume_spike': self._check_volume_spike(volume),
# Signals
'buy_signal': buy_signal,
'sell_signal': sell_signal,
# Strategy metadata
'is_warmed_up': self.is_warmed_up(),
'bars_processed': self.bars_processed,
'rsi_thresholds': rsi_thresholds,
'bb_multiplier': bb_result.get('std_dev', self.trending_bb_multiplier)
}
self.last_result = result
return result
def _update_volume_tracking(self, volume: float) -> None:
"""Update volume moving average tracking."""
# Simple moving average for volume (20 periods)
volume_period = 20
if len(self.volume_history) >= volume_period:
# Remove oldest volume
self.volume_sum -= self.volume_history[0]
self.volume_history.pop(0)
# Add new volume
self.volume_history.append(volume)
self.volume_sum += volume
# Calculate moving average
if len(self.volume_history) > 0:
self.volume_ma = self.volume_sum / len(self.volume_history)
else:
self.volume_ma = volume
def _determine_market_regime(self, bb_reference: Dict[str, float]) -> str:
"""
Determine market regime based on Bollinger Band width.
Args:
bb_reference: Reference BB result for regime detection
Returns:
"sideways" or "trending"
"""
if not self.bb_reference.is_warmed_up():
return "trending" # Default to trending during warm-up
bb_width = bb_reference['bandwidth']
if bb_width < self.bb_width_threshold:
return "sideways"
else:
return "trending"
def _check_volume_spike(self, current_volume: float) -> bool:
"""Check if current volume represents a spike (≥1.5× average)."""
if self.volume_ma is None or self.volume_ma == 0:
return False
return current_volume >= 1.5 * self.volume_ma
def _generate_signals(self, price: float, volume: float, bb_result: Dict[str, float],
rsi_value: float, market_regime: str,
rsi_thresholds: Tuple[float, float]) -> Tuple[bool, bool]:
"""
Generate buy/sell signals based on strategy logic.
Args:
price: Current close price
volume: Current volume
bb_result: Bollinger Bands result
rsi_value: Current RSI value
market_regime: "sideways" or "trending"
rsi_thresholds: (low_threshold, high_threshold)
Returns:
(buy_signal, sell_signal)
"""
# Don't generate signals during warm-up
if not self.is_warmed_up():
return False, False
# Don't generate signals if RSI is NaN
if np.isnan(rsi_value):
return False, False
upper_band = bb_result['upper_band']
lower_band = bb_result['lower_band']
rsi_low, rsi_high = rsi_thresholds
volume_spike = self._check_volume_spike(volume)
buy_signal = False
sell_signal = False
if market_regime == "sideways":
# Sideways market (Mean Reversion)
buy_condition = (price <= lower_band) and (rsi_value <= rsi_low)
sell_condition = (price >= upper_band) and (rsi_value >= rsi_high)
if self.squeeze_strategy:
# Add volume contraction filter for sideways markets
volume_contraction = volume < 0.7 * (self.volume_ma or volume)
buy_condition = buy_condition and volume_contraction
sell_condition = sell_condition and volume_contraction
buy_signal = buy_condition
sell_signal = sell_condition
else: # trending
# Trending market (Breakout Mode)
buy_condition = (price < lower_band) and (rsi_value < 50) and volume_spike
sell_condition = (price > upper_band) and (rsi_value > 50) and volume_spike
buy_signal = buy_condition
sell_signal = sell_condition
return buy_signal, sell_signal
def is_warmed_up(self) -> bool:
"""
Check if strategy is warmed up and ready for reliable signals.
Returns:
True if all indicators are warmed up
"""
return (self.bb_trending.is_warmed_up() and
self.bb_sideways.is_warmed_up() and
self.bb_reference.is_warmed_up() and
self.rsi.is_warmed_up() and
len(self.volume_history) >= 20)
def get_current_incomplete_bar(self) -> Optional[Dict[str, float]]:
"""
Get the current incomplete timeframe bar (for monitoring).
Returns:
Current incomplete bar data or None
"""
return self.aggregator.get_current_bar()
def reset(self) -> None:
"""Reset strategy state to initial conditions."""
self.aggregator.reset()
self.bb_trending.reset()
self.bb_sideways.reset()
self.bb_reference.reset()
self.rsi.reset()
self.bars_processed = 0
self.current_price = None
self.current_volume = None
self.volume_ma = None
self.volume_sum = 0.0
self.volume_history.clear()
self.last_buy_signal = False
self.last_sell_signal = False
self.last_result = None
def get_state_summary(self) -> Dict:
"""Get comprehensive state summary for debugging."""
return {
'strategy_type': 'BBRS_Incremental',
'timeframe_minutes': self.timeframe_minutes,
'bars_processed': self.bars_processed,
'is_warmed_up': self.is_warmed_up(),
'current_price': self.current_price,
'current_volume': self.current_volume,
'volume_ma': self.volume_ma,
'current_incomplete_bar': self.get_current_incomplete_bar(),
'last_signals': {
'buy': self.last_buy_signal,
'sell': self.last_sell_signal
},
'indicators': {
'bb_trending': self.bb_trending.get_state_summary(),
'bb_sideways': self.bb_sideways.get_state_summary(),
'bb_reference': self.bb_reference.get_state_summary(),
'rsi': self.rsi.get_state_summary()
},
'config': {
'bb_period': self.bb_period,
'rsi_period': self.rsi_period,
'bb_width_threshold': self.bb_width_threshold,
'trending_bb_multiplier': self.trending_bb_multiplier,
'sideways_bb_multiplier': self.sideways_bb_multiplier,
'trending_rsi_thresholds': self.trending_rsi_thresholds,
'sideways_rsi_thresholds': self.sideways_rsi_thresholds,
'squeeze_strategy': self.squeeze_strategy
}
}

View File

@@ -1,556 +0,0 @@
# BBRS Strategy Documentation
## Overview
The `BBRSIncrementalState` implements a sophisticated trading strategy combining Bollinger Bands and RSI indicators with market regime detection. It adapts its parameters based on market conditions (trending vs sideways) and provides real-time signal generation with volume analysis.
## Class: `BBRSIncrementalState`
### Purpose
- **Market Regime Detection**: Automatically detects trending vs sideways markets
- **Adaptive Parameters**: Uses different BB/RSI thresholds based on market regime
- **Volume Analysis**: Incorporates volume spikes for signal confirmation
- **Real-time Processing**: Processes minute-level data with timeframe aggregation
### Key Features
- **Dual Bollinger Bands**: Different multipliers for trending/sideways markets
- **RSI Integration**: Wilder's smoothing RSI with regime-specific thresholds
- **Volume Confirmation**: Volume spike detection for signal validation
- **Perfect Accuracy**: 100% accuracy after warm-up period
- **Squeeze Strategy**: Optional squeeze detection for breakout signals
## Strategy Logic
### Market Regime Detection
```python
# Trending market: BB width > threshold
if bb_width > bb_width_threshold:
regime = "trending"
bb_multiplier = 2.5
rsi_thresholds = [30, 70]
else:
regime = "sideways"
bb_multiplier = 1.8
rsi_thresholds = [40, 60]
```
### Signal Generation
- **Buy Signal**: Price touches lower BB + RSI below lower threshold + volume spike
- **Sell Signal**: Price touches upper BB + RSI above upper threshold + volume spike
- **Regime Adaptation**: Parameters automatically adjust based on market conditions
## Configuration Parameters
```python
config = {
"timeframe_minutes": 60, # 1-hour bars
"bb_period": 20, # Bollinger Bands period
"rsi_period": 14, # RSI period
"bb_width": 0.05, # BB width threshold for regime detection
"trending": {
"bb_std_dev_multiplier": 2.5,
"rsi_threshold": [30, 70]
},
"sideways": {
"bb_std_dev_multiplier": 1.8,
"rsi_threshold": [40, 60]
},
"SqueezeStrategy": True # Enable squeeze detection
}
```
## Real-time Usage Example
### Basic Implementation
```python
from cycles.IncStrategies.bbrs_incremental import BBRSIncrementalState
import pandas as pd
from datetime import datetime, timedelta
import random
# Initialize BBRS strategy
config = {
"timeframe_minutes": 60, # 1-hour bars
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"bb_std_dev_multiplier": 2.5,
"rsi_threshold": [30, 70]
},
"sideways": {
"bb_std_dev_multiplier": 1.8,
"rsi_threshold": [40, 60]
},
"SqueezeStrategy": True
}
strategy = BBRSIncrementalState(config)
# Simulate real-time minute data stream
def simulate_market_data():
"""Generate realistic market data with regime changes"""
base_price = 45000.0 # Starting price (e.g., BTC)
timestamp = datetime.now()
market_regime = "trending" # Start in trending mode
regime_counter = 0
while True:
# Simulate regime changes
regime_counter += 1
if regime_counter % 200 == 0: # Change regime every 200 minutes
market_regime = "sideways" if market_regime == "trending" else "trending"
print(f"📊 Market regime changed to: {market_regime.upper()}")
# Generate price movement based on regime
if market_regime == "trending":
# Trending: larger moves, more directional
price_change = random.gauss(0, 0.015) * base_price # ±1.5% std dev
else:
# Sideways: smaller moves, more mean-reverting
price_change = random.gauss(0, 0.008) * base_price # ±0.8% std dev
close = base_price + price_change
high = close + random.random() * 0.005 * base_price
low = close - random.random() * 0.005 * base_price
open_price = base_price
# Volume varies with volatility
base_volume = 1000
volume_multiplier = 1 + abs(price_change / base_price) * 10 # Higher volume with bigger moves
volume = int(base_volume * volume_multiplier * random.uniform(0.5, 2.0))
yield {
'timestamp': timestamp,
'open': open_price,
'high': high,
'low': low,
'close': close,
'volume': volume
}
base_price = close
timestamp += timedelta(minutes=1)
# Process real-time data
print("🚀 Starting BBRS Strategy Real-time Processing...")
print("📊 Waiting for 1-hour bars to form...")
for minute_data in simulate_market_data():
# Strategy handles minute-to-hour aggregation automatically
result = strategy.update_minute_data(
timestamp=pd.Timestamp(minute_data['timestamp']),
ohlcv_data=minute_data
)
# Check if a complete 1-hour bar was formed
if result is not None:
current_price = minute_data['close']
timestamp = minute_data['timestamp']
print(f"\n⏰ Complete 1h bar at {timestamp}")
print(f"💰 Price: ${current_price:,.2f}")
# Get strategy state
state = strategy.get_state_summary()
print(f"📈 Market Regime: {state.get('market_regime', 'Unknown')}")
print(f"🔍 BB Width: {state.get('bb_width', 0):.4f}")
print(f"📊 RSI: {state.get('rsi_value', 0):.2f}")
print(f"📈 Volume MA Ratio: {state.get('volume_ma_ratio', 0):.2f}")
# Check for signals only if strategy is warmed up
if strategy.is_warmed_up():
# Process buy signals
if result.get('buy_signal', False):
print(f"🟢 BUY SIGNAL GENERATED!")
print(f" 💵 Price: ${current_price:,.2f}")
print(f" 📊 RSI: {state.get('rsi_value', 0):.2f}")
print(f" 📈 BB Position: Lower band touch")
print(f" 🔊 Volume Spike: {state.get('volume_spike', False)}")
print(f" 🎯 Market Regime: {state.get('market_regime', 'Unknown')}")
# execute_buy_order(result)
# Process sell signals
if result.get('sell_signal', False):
print(f"🔴 SELL SIGNAL GENERATED!")
print(f" 💵 Price: ${current_price:,.2f}")
print(f" 📊 RSI: {state.get('rsi_value', 0):.2f}")
print(f" 📈 BB Position: Upper band touch")
print(f" 🔊 Volume Spike: {state.get('volume_spike', False)}")
print(f" 🎯 Market Regime: {state.get('market_regime', 'Unknown')}")
# execute_sell_order(result)
else:
warmup_progress = strategy.bars_processed
min_required = max(strategy.bb_period, strategy.rsi_period) + 10
print(f"🔄 Warming up... ({warmup_progress}/{min_required} bars)")
```
### Advanced Trading System Integration
```python
class BBRSTradingSystem:
def __init__(self, initial_capital=10000):
self.config = {
"timeframe_minutes": 60,
"bb_period": 20,
"rsi_period": 14,
"bb_width": 0.05,
"trending": {
"bb_std_dev_multiplier": 2.5,
"rsi_threshold": [30, 70]
},
"sideways": {
"bb_std_dev_multiplier": 1.8,
"rsi_threshold": [40, 60]
},
"SqueezeStrategy": True
}
self.strategy = BBRSIncrementalState(self.config)
self.capital = initial_capital
self.position = None
self.trades = []
self.equity_curve = []
def process_market_data(self, timestamp, ohlcv_data):
"""Process incoming market data and manage positions"""
# Update strategy
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None and self.strategy.is_warmed_up():
self._check_signals(timestamp, ohlcv_data['close'], result)
self._update_equity(timestamp, ohlcv_data['close'])
def _check_signals(self, timestamp, current_price, result):
"""Check for trading signals and execute trades"""
# Handle buy signals
if result.get('buy_signal', False) and self.position is None:
self._execute_entry(timestamp, current_price, 'BUY', result)
# Handle sell signals
if result.get('sell_signal', False) and self.position is not None:
self._execute_exit(timestamp, current_price, 'SELL', result)
def _execute_entry(self, timestamp, price, signal_type, result):
"""Execute entry trade"""
# Calculate position size (risk 2% of capital)
risk_amount = self.capital * 0.02
shares = risk_amount / price
state = self.strategy.get_state_summary()
self.position = {
'entry_time': timestamp,
'entry_price': price,
'shares': shares,
'signal_type': signal_type,
'market_regime': state.get('market_regime'),
'rsi_value': state.get('rsi_value'),
'bb_width': state.get('bb_width'),
'volume_spike': state.get('volume_spike', False)
}
print(f"🟢 {signal_type} POSITION OPENED")
print(f" 📅 Time: {timestamp}")
print(f" 💵 Price: ${price:,.2f}")
print(f" 📊 Shares: {shares:.4f}")
print(f" 🎯 Market Regime: {self.position['market_regime']}")
print(f" 📈 RSI: {self.position['rsi_value']:.2f}")
print(f" 🔊 Volume Spike: {self.position['volume_spike']}")
def _execute_exit(self, timestamp, price, signal_type, result):
"""Execute exit trade"""
if self.position:
# Calculate P&L
pnl = (price - self.position['entry_price']) * self.position['shares']
pnl_percent = (pnl / (self.position['entry_price'] * self.position['shares'])) * 100
# Update capital
self.capital += pnl
state = self.strategy.get_state_summary()
# Record trade
trade = {
'entry_time': self.position['entry_time'],
'exit_time': timestamp,
'entry_price': self.position['entry_price'],
'exit_price': price,
'shares': self.position['shares'],
'pnl': pnl,
'pnl_percent': pnl_percent,
'duration': timestamp - self.position['entry_time'],
'entry_regime': self.position['market_regime'],
'exit_regime': state.get('market_regime'),
'entry_rsi': self.position['rsi_value'],
'exit_rsi': state.get('rsi_value'),
'entry_volume_spike': self.position['volume_spike'],
'exit_volume_spike': state.get('volume_spike', False)
}
self.trades.append(trade)
print(f"🔴 {signal_type} POSITION CLOSED")
print(f" 📅 Time: {timestamp}")
print(f" 💵 Exit Price: ${price:,.2f}")
print(f" 💰 P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)")
print(f" ⏱️ Duration: {trade['duration']}")
print(f" 🎯 Regime: {trade['entry_regime']}{trade['exit_regime']}")
print(f" 💼 New Capital: ${self.capital:,.2f}")
self.position = None
def _update_equity(self, timestamp, current_price):
"""Update equity curve"""
if self.position:
unrealized_pnl = (current_price - self.position['entry_price']) * self.position['shares']
current_equity = self.capital + unrealized_pnl
else:
current_equity = self.capital
self.equity_curve.append({
'timestamp': timestamp,
'equity': current_equity,
'position': self.position is not None
})
def get_performance_summary(self):
"""Get trading performance summary"""
if not self.trades:
return {"message": "No completed trades yet"}
trades_df = pd.DataFrame(self.trades)
total_trades = len(trades_df)
winning_trades = len(trades_df[trades_df['pnl'] > 0])
losing_trades = len(trades_df[trades_df['pnl'] < 0])
win_rate = (winning_trades / total_trades) * 100
total_pnl = trades_df['pnl'].sum()
avg_win = trades_df[trades_df['pnl'] > 0]['pnl'].mean() if winning_trades > 0 else 0
avg_loss = trades_df[trades_df['pnl'] < 0]['pnl'].mean() if losing_trades > 0 else 0
# Regime-specific performance
trending_trades = trades_df[trades_df['entry_regime'] == 'trending']
sideways_trades = trades_df[trades_df['entry_regime'] == 'sideways']
return {
'total_trades': total_trades,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'win_rate': win_rate,
'total_pnl': total_pnl,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': abs(avg_win / avg_loss) if avg_loss != 0 else float('inf'),
'final_capital': self.capital,
'trending_trades': len(trending_trades),
'sideways_trades': len(sideways_trades),
'trending_win_rate': (len(trending_trades[trending_trades['pnl'] > 0]) / len(trending_trades) * 100) if len(trending_trades) > 0 else 0,
'sideways_win_rate': (len(sideways_trades[sideways_trades['pnl'] > 0]) / len(sideways_trades) * 100) if len(sideways_trades) > 0 else 0
}
# Usage Example
trading_system = BBRSTradingSystem(initial_capital=10000)
print("🚀 BBRS Trading System Started")
print("💰 Initial Capital: $10,000")
# Simulate live trading
for market_data in simulate_market_data():
trading_system.process_market_data(
timestamp=pd.Timestamp(market_data['timestamp']),
ohlcv_data=market_data
)
# Print performance summary every 100 bars
if len(trading_system.equity_curve) % 100 == 0 and trading_system.trades:
performance = trading_system.get_performance_summary()
print(f"\n📊 Performance Summary (after {len(trading_system.equity_curve)} bars):")
print(f" 💼 Capital: ${performance['final_capital']:,.2f}")
print(f" 📈 Total Trades: {performance['total_trades']}")
print(f" 🎯 Win Rate: {performance['win_rate']:.1f}%")
print(f" 💰 Total P&L: ${performance['total_pnl']:,.2f}")
print(f" 📊 Trending Trades: {performance['trending_trades']} (WR: {performance['trending_win_rate']:.1f}%)")
print(f" 📊 Sideways Trades: {performance['sideways_trades']} (WR: {performance['sideways_win_rate']:.1f}%)")
```
### Backtesting Example
```python
def backtest_bbrs_strategy(historical_data, config):
"""Comprehensive backtesting of BBRS strategy"""
strategy = BBRSIncrementalState(config)
signals = []
trades = []
current_position = None
print(f"🔄 Backtesting BBRS Strategy on {config['timeframe_minutes']}min timeframe...")
print(f"📊 Data period: {historical_data.index[0]} to {historical_data.index[-1]}")
# Process historical data
for timestamp, row in historical_data.iterrows():
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update strategy
result = strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None and strategy.is_warmed_up():
state = strategy.get_state_summary()
# Record buy signals
if result.get('buy_signal', False):
signals.append({
'timestamp': timestamp,
'type': 'BUY',
'price': row['close'],
'rsi': state.get('rsi_value'),
'bb_width': state.get('bb_width'),
'market_regime': state.get('market_regime'),
'volume_spike': state.get('volume_spike', False)
})
# Open position if none exists
if current_position is None:
current_position = {
'entry_time': timestamp,
'entry_price': row['close'],
'entry_regime': state.get('market_regime'),
'entry_rsi': state.get('rsi_value')
}
# Record sell signals
if result.get('sell_signal', False):
signals.append({
'timestamp': timestamp,
'type': 'SELL',
'price': row['close'],
'rsi': state.get('rsi_value'),
'bb_width': state.get('bb_width'),
'market_regime': state.get('market_regime'),
'volume_spike': state.get('volume_spike', False)
})
# Close position if exists
if current_position is not None:
pnl = row['close'] - current_position['entry_price']
pnl_percent = (pnl / current_position['entry_price']) * 100
trades.append({
'entry_time': current_position['entry_time'],
'exit_time': timestamp,
'entry_price': current_position['entry_price'],
'exit_price': row['close'],
'pnl': pnl,
'pnl_percent': pnl_percent,
'duration': timestamp - current_position['entry_time'],
'entry_regime': current_position['entry_regime'],
'exit_regime': state.get('market_regime'),
'entry_rsi': current_position['entry_rsi'],
'exit_rsi': state.get('rsi_value')
})
current_position = None
# Convert to DataFrames for analysis
signals_df = pd.DataFrame(signals)
trades_df = pd.DataFrame(trades)
# Calculate performance metrics
if len(trades_df) > 0:
total_trades = len(trades_df)
winning_trades = len(trades_df[trades_df['pnl'] > 0])
win_rate = (winning_trades / total_trades) * 100
total_return = trades_df['pnl_percent'].sum()
avg_return = trades_df['pnl_percent'].mean()
max_win = trades_df['pnl_percent'].max()
max_loss = trades_df['pnl_percent'].min()
# Regime-specific analysis
trending_trades = trades_df[trades_df['entry_regime'] == 'trending']
sideways_trades = trades_df[trades_df['entry_regime'] == 'sideways']
print(f"\n📊 Backtest Results:")
print(f" 📈 Total Signals: {len(signals_df)}")
print(f" 💼 Total Trades: {total_trades}")
print(f" 🎯 Win Rate: {win_rate:.1f}%")
print(f" 💰 Total Return: {total_return:.2f}%")
print(f" 📊 Average Return: {avg_return:.2f}%")
print(f" 🚀 Max Win: {max_win:.2f}%")
print(f" 📉 Max Loss: {max_loss:.2f}%")
print(f" 📈 Trending Trades: {len(trending_trades)} ({len(trending_trades[trending_trades['pnl'] > 0])} wins)")
print(f" 📊 Sideways Trades: {len(sideways_trades)} ({len(sideways_trades[sideways_trades['pnl'] > 0])} wins)")
return signals_df, trades_df
else:
print("❌ No completed trades in backtest period")
return signals_df, pd.DataFrame()
# Run backtest (example)
# historical_data = pd.read_csv('btc_1min_data.csv', index_col='timestamp', parse_dates=True)
# config = {
# "timeframe_minutes": 60,
# "bb_period": 20,
# "rsi_period": 14,
# "bb_width": 0.05,
# "trending": {"bb_std_dev_multiplier": 2.5, "rsi_threshold": [30, 70]},
# "sideways": {"bb_std_dev_multiplier": 1.8, "rsi_threshold": [40, 60]},
# "SqueezeStrategy": True
# }
# signals, trades = backtest_bbrs_strategy(historical_data, config)
```
## Performance Characteristics
### Timing Benchmarks
- **Update Time**: <1ms per 1-hour bar
- **Signal Generation**: <0.5ms per signal
- **Memory Usage**: ~8MB constant
- **Accuracy**: 100% after warm-up period
### Signal Quality
- **Regime Adaptation**: Automatically adjusts to market conditions
- **Volume Confirmation**: Reduces false signals by ~40%
- **Signal Match Rate**: 95.45% vs original implementation
- **False Signal Reduction**: Adaptive thresholds reduce noise
## Best Practices
1. **Timeframe Selection**: 1h-4h timeframes work best for BB/RSI combination
2. **Regime Monitoring**: Track market regime changes for strategy performance
3. **Volume Analysis**: Use volume spikes for signal confirmation
4. **Parameter Tuning**: Adjust BB width threshold based on asset volatility
5. **Risk Management**: Implement proper position sizing and stop-losses
## Troubleshooting
### Common Issues
1. **No Signals**: Check if strategy is warmed up (needs ~30+ bars)
2. **Too Many Signals**: Increase BB width threshold or RSI thresholds
3. **Poor Performance**: Verify market regime detection is working correctly
4. **Memory Usage**: Monitor volume history buffer size
### Debug Information
```python
# Get detailed strategy state
state = strategy.get_state_summary()
print(f"Strategy State: {state}")
# Check current incomplete bar
current_bar = strategy.get_current_incomplete_bar()
if current_bar:
print(f"Current Bar: {current_bar}")
# Monitor regime changes
print(f"Market Regime: {state.get('market_regime')}")
print(f"BB Width: {state.get('bb_width'):.4f} (threshold: {strategy.bb_width_threshold})")
```

View File

@@ -1,470 +0,0 @@
# MetaTrend Strategy Documentation
## Overview
The `IncMetaTrendStrategy` implements a sophisticated trend-following strategy using multiple Supertrend indicators to determine market direction. It generates entry/exit signals based on meta-trend changes, providing robust trend detection with reduced false signals.
## Class: `IncMetaTrendStrategy`
### Purpose
- **Trend Detection**: Uses 3 Supertrend indicators to identify strong trends
- **Meta-trend Analysis**: Combines multiple timeframes for robust signal generation
- **Real-time Processing**: Processes minute-level data with configurable timeframe aggregation
### Key Features
- **Multi-Supertrend Analysis**: 3 Supertrend indicators with different parameters
- **Meta-trend Logic**: Signals only when all indicators agree
- **High Accuracy**: 98.5% accuracy vs corrected original implementation
- **Fast Processing**: <1ms updates, sub-millisecond signal generation
## Strategy Logic
### Supertrend Configuration
```python
supertrend_configs = [
(12, 3.0), # period=12, multiplier=3.0 (Conservative)
(10, 1.0), # period=10, multiplier=1.0 (Sensitive)
(11, 2.0) # period=11, multiplier=2.0 (Balanced)
]
```
### Meta-trend Calculation
- **Meta-trend = 1**: All 3 Supertrends indicate uptrend (BUY condition)
- **Meta-trend = -1**: All 3 Supertrends indicate downtrend (SELL condition)
- **Meta-trend = 0**: Supertrends disagree (NEUTRAL - no action)
### Signal Generation
- **Entry Signal**: Meta-trend changes from != 1 to == 1
- **Exit Signal**: Meta-trend changes from != -1 to == -1
## Configuration Parameters
```python
params = {
"timeframe": "15min", # Primary analysis timeframe
"enable_logging": False, # Enable detailed logging
"buffer_size_multiplier": 2.0 # Memory management multiplier
}
```
## Real-time Usage Example
### Basic Implementation
```python
from cycles.IncStrategies.metatrend_strategy import IncMetaTrendStrategy
import pandas as pd
from datetime import datetime, timedelta
import random
# Initialize MetaTrend strategy
strategy = IncMetaTrendStrategy(
name="metatrend",
weight=1.0,
params={
"timeframe": "15min", # 15-minute analysis
"enable_logging": True # Enable detailed logging
}
)
# Simulate real-time minute data stream
def simulate_market_data():
"""Generate realistic market data with trends"""
base_price = 50000.0 # Starting price (e.g., BTC)
timestamp = datetime.now()
trend_direction = 1 # 1 for up, -1 for down
trend_strength = 0.001 # Trend strength
while True:
# Add trend and noise
trend_move = trend_direction * trend_strength * base_price
noise = (random.random() - 0.5) * 0.002 * base_price # ±0.2% noise
price_change = trend_move + noise
close = base_price + price_change
high = close + random.random() * 0.001 * base_price
low = close - random.random() * 0.001 * base_price
open_price = base_price
volume = random.randint(100, 1000)
# Occasionally change trend direction
if random.random() < 0.01: # 1% chance per minute
trend_direction *= -1
print(f"📈 Trend direction changed to {'UP' if trend_direction > 0 else 'DOWN'}")
yield {
'timestamp': timestamp,
'open': open_price,
'high': high,
'low': low,
'close': close,
'volume': volume
}
base_price = close
timestamp += timedelta(minutes=1)
# Process real-time data
print("🚀 Starting MetaTrend Strategy Real-time Processing...")
print("📊 Waiting for 15-minute bars to form...")
for minute_data in simulate_market_data():
# Strategy handles minute-to-15min aggregation automatically
result = strategy.update_minute_data(
timestamp=pd.Timestamp(minute_data['timestamp']),
ohlcv_data=minute_data
)
# Check if a complete 15-minute bar was formed
if result is not None:
current_price = minute_data['close']
timestamp = minute_data['timestamp']
print(f"\n⏰ Complete 15min bar at {timestamp}")
print(f"💰 Price: ${current_price:,.2f}")
# Get current meta-trend state
meta_trend = strategy.get_current_meta_trend()
individual_trends = strategy.get_individual_supertrend_states()
print(f"📈 Meta-trend: {meta_trend}")
print(f"🔍 Individual Supertrends: {[s['trend'] for s in individual_trends]}")
# Check for signals only if strategy is warmed up
if strategy.is_warmed_up:
entry_signal = strategy.get_entry_signal()
exit_signal = strategy.get_exit_signal()
# Process entry signals
if entry_signal.signal_type == "ENTRY":
print(f"🟢 ENTRY SIGNAL GENERATED!")
print(f" 💪 Confidence: {entry_signal.confidence:.2f}")
print(f" 💵 Price: ${entry_signal.price:,.2f}")
print(f" 📊 Meta-trend: {entry_signal.metadata.get('meta_trend')}")
print(f" 🎯 All Supertrends aligned for UPTREND")
# execute_buy_order(entry_signal)
# Process exit signals
if exit_signal.signal_type == "EXIT":
print(f"🔴 EXIT SIGNAL GENERATED!")
print(f" 💪 Confidence: {exit_signal.confidence:.2f}")
print(f" 💵 Price: ${exit_signal.price:,.2f}")
print(f" 📊 Meta-trend: {exit_signal.metadata.get('meta_trend')}")
print(f" 🎯 All Supertrends aligned for DOWNTREND")
# execute_sell_order(exit_signal)
else:
warmup_progress = len(strategy._meta_trend_history)
min_required = max(strategy.get_minimum_buffer_size().values())
print(f"🔄 Warming up... ({warmup_progress}/{min_required} bars)")
```
### Advanced Trading System Integration
```python
class MetaTrendTradingSystem:
def __init__(self, initial_capital=10000):
self.strategy = IncMetaTrendStrategy(
name="metatrend_live",
weight=1.0,
params={
"timeframe": "15min",
"enable_logging": False # Disable for production
}
)
self.capital = initial_capital
self.position = None
self.trades = []
self.equity_curve = []
def process_market_data(self, timestamp, ohlcv_data):
"""Process incoming market data and manage positions"""
# Update strategy
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None and self.strategy.is_warmed_up:
self._check_signals(timestamp, ohlcv_data['close'])
self._update_equity(timestamp, ohlcv_data['close'])
def _check_signals(self, timestamp, current_price):
"""Check for trading signals and execute trades"""
entry_signal = self.strategy.get_entry_signal()
exit_signal = self.strategy.get_exit_signal()
# Handle entry signals
if entry_signal.signal_type == "ENTRY" and self.position is None:
self._execute_entry(timestamp, entry_signal)
# Handle exit signals
if exit_signal.signal_type == "EXIT" and self.position is not None:
self._execute_exit(timestamp, exit_signal)
def _execute_entry(self, timestamp, signal):
"""Execute entry trade"""
# Calculate position size (risk 2% of capital)
risk_amount = self.capital * 0.02
# Simple position sizing - could be more sophisticated
shares = risk_amount / signal.price
self.position = {
'entry_time': timestamp,
'entry_price': signal.price,
'shares': shares,
'confidence': signal.confidence,
'meta_trend': signal.metadata.get('meta_trend'),
'individual_trends': signal.metadata.get('individual_trends', [])
}
print(f"🟢 LONG POSITION OPENED")
print(f" 📅 Time: {timestamp}")
print(f" 💵 Price: ${signal.price:,.2f}")
print(f" 📊 Shares: {shares:.4f}")
print(f" 💪 Confidence: {signal.confidence:.2f}")
print(f" 📈 Meta-trend: {self.position['meta_trend']}")
def _execute_exit(self, timestamp, signal):
"""Execute exit trade"""
if self.position:
# Calculate P&L
pnl = (signal.price - self.position['entry_price']) * self.position['shares']
pnl_percent = (pnl / (self.position['entry_price'] * self.position['shares'])) * 100
# Update capital
self.capital += pnl
# Record trade
trade = {
'entry_time': self.position['entry_time'],
'exit_time': timestamp,
'entry_price': self.position['entry_price'],
'exit_price': signal.price,
'shares': self.position['shares'],
'pnl': pnl,
'pnl_percent': pnl_percent,
'duration': timestamp - self.position['entry_time'],
'entry_confidence': self.position['confidence'],
'exit_confidence': signal.confidence
}
self.trades.append(trade)
print(f"🔴 LONG POSITION CLOSED")
print(f" 📅 Time: {timestamp}")
print(f" 💵 Exit Price: ${signal.price:,.2f}")
print(f" 💰 P&L: ${pnl:,.2f} ({pnl_percent:+.2f}%)")
print(f" ⏱️ Duration: {trade['duration']}")
print(f" 💼 New Capital: ${self.capital:,.2f}")
self.position = None
def _update_equity(self, timestamp, current_price):
"""Update equity curve"""
if self.position:
unrealized_pnl = (current_price - self.position['entry_price']) * self.position['shares']
current_equity = self.capital + unrealized_pnl
else:
current_equity = self.capital
self.equity_curve.append({
'timestamp': timestamp,
'equity': current_equity,
'position': self.position is not None
})
def get_performance_summary(self):
"""Get trading performance summary"""
if not self.trades:
return {"message": "No completed trades yet"}
trades_df = pd.DataFrame(self.trades)
total_trades = len(trades_df)
winning_trades = len(trades_df[trades_df['pnl'] > 0])
losing_trades = len(trades_df[trades_df['pnl'] < 0])
win_rate = (winning_trades / total_trades) * 100
total_pnl = trades_df['pnl'].sum()
avg_win = trades_df[trades_df['pnl'] > 0]['pnl'].mean() if winning_trades > 0 else 0
avg_loss = trades_df[trades_df['pnl'] < 0]['pnl'].mean() if losing_trades > 0 else 0
return {
'total_trades': total_trades,
'winning_trades': winning_trades,
'losing_trades': losing_trades,
'win_rate': win_rate,
'total_pnl': total_pnl,
'avg_win': avg_win,
'avg_loss': avg_loss,
'profit_factor': abs(avg_win / avg_loss) if avg_loss != 0 else float('inf'),
'final_capital': self.capital
}
# Usage Example
trading_system = MetaTrendTradingSystem(initial_capital=10000)
print("🚀 MetaTrend Trading System Started")
print("💰 Initial Capital: $10,000")
# Simulate live trading
for market_data in simulate_market_data():
trading_system.process_market_data(
timestamp=pd.Timestamp(market_data['timestamp']),
ohlcv_data=market_data
)
# Print performance summary every 100 bars
if len(trading_system.equity_curve) % 100 == 0 and trading_system.trades:
performance = trading_system.get_performance_summary()
print(f"\n📊 Performance Summary (after {len(trading_system.equity_curve)} bars):")
print(f" 💼 Capital: ${performance['final_capital']:,.2f}")
print(f" 📈 Total Trades: {performance['total_trades']}")
print(f" 🎯 Win Rate: {performance['win_rate']:.1f}%")
print(f" 💰 Total P&L: ${performance['total_pnl']:,.2f}")
```
### Backtesting Example
```python
def backtest_metatrend_strategy(historical_data, timeframe="15min"):
"""Comprehensive backtesting of MetaTrend strategy"""
strategy = IncMetaTrendStrategy(
name="metatrend_backtest",
weight=1.0,
params={
"timeframe": timeframe,
"enable_logging": False
}
)
signals = []
trades = []
current_position = None
print(f"🔄 Backtesting MetaTrend Strategy on {timeframe} timeframe...")
print(f"📊 Data period: {historical_data.index[0]} to {historical_data.index[-1]}")
# Process historical data
for timestamp, row in historical_data.iterrows():
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update strategy
result = strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None and strategy.is_warmed_up:
entry_signal = strategy.get_entry_signal()
exit_signal = strategy.get_exit_signal()
# Record entry signals
if entry_signal.signal_type == "ENTRY":
signals.append({
'timestamp': timestamp,
'type': 'ENTRY',
'price': entry_signal.price,
'confidence': entry_signal.confidence,
'meta_trend': entry_signal.metadata.get('meta_trend')
})
# Open position if none exists
if current_position is None:
current_position = {
'entry_time': timestamp,
'entry_price': entry_signal.price,
'confidence': entry_signal.confidence
}
# Record exit signals
if exit_signal.signal_type == "EXIT":
signals.append({
'timestamp': timestamp,
'type': 'EXIT',
'price': exit_signal.price,
'confidence': exit_signal.confidence,
'meta_trend': exit_signal.metadata.get('meta_trend')
})
# Close position if exists
if current_position is not None:
pnl = exit_signal.price - current_position['entry_price']
pnl_percent = (pnl / current_position['entry_price']) * 100
trades.append({
'entry_time': current_position['entry_time'],
'exit_time': timestamp,
'entry_price': current_position['entry_price'],
'exit_price': exit_signal.price,
'pnl': pnl,
'pnl_percent': pnl_percent,
'duration': timestamp - current_position['entry_time'],
'entry_confidence': current_position['confidence'],
'exit_confidence': exit_signal.confidence
})
current_position = None
# Convert to DataFrames for analysis
signals_df = pd.DataFrame(signals)
trades_df = pd.DataFrame(trades)
# Calculate performance metrics
if len(trades_df) > 0:
total_trades = len(trades_df)
winning_trades = len(trades_df[trades_df['pnl'] > 0])
win_rate = (winning_trades / total_trades) * 100
total_return = trades_df['pnl_percent'].sum()
avg_return = trades_df['pnl_percent'].mean()
max_win = trades_df['pnl_percent'].max()
max_loss = trades_df['pnl_percent'].min()
print(f"\n📊 Backtest Results:")
print(f" 📈 Total Signals: {len(signals_df)}")
print(f" 💼 Total Trades: {total_trades}")
print(f" 🎯 Win Rate: {win_rate:.1f}%")
print(f" 💰 Total Return: {total_return:.2f}%")
print(f" 📊 Average Return: {avg_return:.2f}%")
print(f" 🚀 Max Win: {max_win:.2f}%")
print(f" 📉 Max Loss: {max_loss:.2f}%")
return signals_df, trades_df
else:
print("❌ No completed trades in backtest period")
return signals_df, pd.DataFrame()
# Run backtest (example)
# historical_data = pd.read_csv('btc_1min_data.csv', index_col='timestamp', parse_dates=True)
# signals, trades = backtest_metatrend_strategy(historical_data, timeframe="15min")
```
## Performance Characteristics
### Timing Benchmarks
- **Update Time**: <1ms per 15-minute bar
- **Signal Generation**: <0.5ms per signal
- **Memory Usage**: ~5MB constant
- **Accuracy**: 98.5% vs original implementation
## Troubleshooting
### Common Issues
1. **No Signals**: Check if strategy is warmed up (needs ~50+ bars)
2. **Conflicting Trends**: Normal behavior - wait for alignment
3. **Late Signals**: Meta-trend prioritizes accuracy over speed
4. **Memory Usage**: Monitor buffer sizes in long-running systems
### Debug Information
```python
# Get detailed strategy state
state = strategy.get_current_state_summary()
print(f"Strategy State: {state}")
# Get meta-trend history
history = strategy.get_meta_trend_history(limit=10)
for entry in history:
print(f"{entry['timestamp']}: Meta-trend={entry['meta_trend']}, Trends={entry['individual_trends']}")
```

View File

@@ -1,342 +0,0 @@
# RandomStrategy Documentation
## Overview
The `IncRandomStrategy` is a testing strategy that generates random entry and exit signals with configurable probability and confidence levels. It's designed to test the incremental strategy framework and signal processing system while providing a baseline for performance comparisons.
## Class: `IncRandomStrategy`
### Purpose
- **Testing Framework**: Validates incremental strategy system functionality
- **Performance Baseline**: Provides minimal processing overhead for benchmarking
- **Signal Testing**: Tests signal generation and processing pipelines
### Key Features
- **Minimal Processing**: Extremely fast updates (0.006ms)
- **Configurable Randomness**: Adjustable signal probabilities and confidence levels
- **Reproducible Results**: Optional random seed for consistent testing
- **Real-time Compatible**: Processes minute-level data with timeframe aggregation
## Configuration Parameters
```python
params = {
"entry_probability": 0.05, # 5% chance of entry signal per bar
"exit_probability": 0.1, # 10% chance of exit signal per bar
"min_confidence": 0.6, # Minimum signal confidence
"max_confidence": 0.9, # Maximum signal confidence
"timeframe": "1min", # Operating timeframe
"signal_frequency": 1, # Signal every N bars
"random_seed": 42 # Optional seed for reproducibility
}
```
## Real-time Usage Example
### Basic Implementation
```python
from cycles.IncStrategies.random_strategy import IncRandomStrategy
import pandas as pd
from datetime import datetime, timedelta
# Initialize strategy
strategy = IncRandomStrategy(
weight=1.0,
params={
"entry_probability": 0.1, # 10% chance per bar
"exit_probability": 0.15, # 15% chance per bar
"min_confidence": 0.7,
"max_confidence": 0.9,
"timeframe": "5min", # 5-minute bars
"signal_frequency": 3, # Signal every 3 bars
"random_seed": 42 # Reproducible for testing
}
)
# Simulate real-time minute data stream
def simulate_live_data():
"""Simulate live minute-level OHLCV data"""
base_price = 100.0
timestamp = datetime.now()
while True:
# Generate realistic OHLCV data
price_change = (random.random() - 0.5) * 2 # ±1 price movement
close = base_price + price_change
high = close + random.random() * 0.5
low = close - random.random() * 0.5
open_price = base_price
volume = random.randint(1000, 5000)
yield {
'timestamp': timestamp,
'open': open_price,
'high': high,
'low': low,
'close': close,
'volume': volume
}
base_price = close
timestamp += timedelta(minutes=1)
# Process real-time data
for minute_data in simulate_live_data():
# Strategy handles timeframe aggregation (1min -> 5min)
result = strategy.update_minute_data(
timestamp=pd.Timestamp(minute_data['timestamp']),
ohlcv_data=minute_data
)
# Check if a complete 5-minute bar was formed
if result is not None:
print(f"Complete 5min bar at {minute_data['timestamp']}")
# Get signals
entry_signal = strategy.get_entry_signal()
exit_signal = strategy.get_exit_signal()
# Process entry signals
if entry_signal.signal_type == "ENTRY":
print(f"🟢 ENTRY Signal - Confidence: {entry_signal.confidence:.2f}")
print(f" Price: ${entry_signal.price:.2f}")
print(f" Metadata: {entry_signal.metadata}")
# execute_buy_order(entry_signal)
# Process exit signals
if exit_signal.signal_type == "EXIT":
print(f"🔴 EXIT Signal - Confidence: {exit_signal.confidence:.2f}")
print(f" Price: ${exit_signal.price:.2f}")
print(f" Metadata: {exit_signal.metadata}")
# execute_sell_order(exit_signal)
# Monitor strategy state
if strategy.is_warmed_up:
state = strategy.get_current_state_summary()
print(f"Strategy State: {state}")
```
### Integration with Trading System
```python
class LiveTradingSystem:
def __init__(self):
self.strategy = IncRandomStrategy(
weight=1.0,
params={
"entry_probability": 0.08,
"exit_probability": 0.12,
"min_confidence": 0.75,
"max_confidence": 0.95,
"timeframe": "15min",
"random_seed": None # True randomness for live trading
}
)
self.position = None
self.orders = []
def process_market_data(self, timestamp, ohlcv_data):
"""Process incoming market data"""
# Update strategy with new data
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None: # Complete timeframe bar
self._check_signals()
def _check_signals(self):
"""Check for trading signals"""
entry_signal = self.strategy.get_entry_signal()
exit_signal = self.strategy.get_exit_signal()
# Handle entry signals
if entry_signal.signal_type == "ENTRY" and self.position is None:
self._execute_entry(entry_signal)
# Handle exit signals
if exit_signal.signal_type == "EXIT" and self.position is not None:
self._execute_exit(exit_signal)
def _execute_entry(self, signal):
"""Execute entry order"""
order = {
'type': 'BUY',
'price': signal.price,
'confidence': signal.confidence,
'timestamp': signal.metadata.get('timestamp'),
'strategy': 'random'
}
print(f"Executing BUY order: {order}")
self.orders.append(order)
self.position = order
def _execute_exit(self, signal):
"""Execute exit order"""
if self.position:
order = {
'type': 'SELL',
'price': signal.price,
'confidence': signal.confidence,
'timestamp': signal.metadata.get('timestamp'),
'entry_price': self.position['price'],
'pnl': signal.price - self.position['price']
}
print(f"Executing SELL order: {order}")
self.orders.append(order)
self.position = None
# Usage
trading_system = LiveTradingSystem()
# Connect to live data feed
for market_tick in live_market_feed:
trading_system.process_market_data(
timestamp=market_tick['timestamp'],
ohlcv_data=market_tick
)
```
### Backtesting Example
```python
import pandas as pd
def backtest_random_strategy(historical_data):
"""Backtest RandomStrategy on historical data"""
strategy = IncRandomStrategy(
weight=1.0,
params={
"entry_probability": 0.05,
"exit_probability": 0.08,
"min_confidence": 0.8,
"max_confidence": 0.95,
"timeframe": "1h",
"random_seed": 123 # Reproducible results
}
)
signals = []
positions = []
current_position = None
# Process historical data
for timestamp, row in historical_data.iterrows():
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
# Update strategy (assuming data is already in target timeframe)
result = strategy.update_minute_data(timestamp, ohlcv_data)
if result is not None and strategy.is_warmed_up:
entry_signal = strategy.get_entry_signal()
exit_signal = strategy.get_exit_signal()
# Record signals
if entry_signal.signal_type == "ENTRY":
signals.append({
'timestamp': timestamp,
'type': 'ENTRY',
'price': entry_signal.price,
'confidence': entry_signal.confidence
})
if current_position is None:
current_position = {
'entry_time': timestamp,
'entry_price': entry_signal.price,
'confidence': entry_signal.confidence
}
if exit_signal.signal_type == "EXIT" and current_position:
signals.append({
'timestamp': timestamp,
'type': 'EXIT',
'price': exit_signal.price,
'confidence': exit_signal.confidence
})
# Close position
pnl = exit_signal.price - current_position['entry_price']
positions.append({
'entry_time': current_position['entry_time'],
'exit_time': timestamp,
'entry_price': current_position['entry_price'],
'exit_price': exit_signal.price,
'pnl': pnl,
'duration': timestamp - current_position['entry_time']
})
current_position = None
return pd.DataFrame(signals), pd.DataFrame(positions)
# Run backtest
# historical_data = pd.read_csv('historical_data.csv', index_col='timestamp', parse_dates=True)
# signals_df, positions_df = backtest_random_strategy(historical_data)
# print(f"Generated {len(signals_df)} signals and {len(positions_df)} completed trades")
```
## Performance Characteristics
### Timing Benchmarks
- **Update Time**: ~0.006ms per data point
- **Signal Generation**: ~0.048ms per signal
- **Memory Usage**: <1MB constant
- **Throughput**: >100,000 updates/second
## Testing and Validation
### Unit Tests
```python
def test_random_strategy():
"""Test RandomStrategy functionality"""
strategy = IncRandomStrategy(
params={
"entry_probability": 1.0, # Always generate signals
"exit_probability": 1.0,
"random_seed": 42
}
)
# Test data
test_data = {
'open': 100.0,
'high': 101.0,
'low': 99.0,
'close': 100.5,
'volume': 1000
}
timestamp = pd.Timestamp('2024-01-01 10:00:00')
# Process data
result = strategy.update_minute_data(timestamp, test_data)
# Verify signals
entry_signal = strategy.get_entry_signal()
exit_signal = strategy.get_exit_signal()
assert entry_signal.signal_type == "ENTRY"
assert exit_signal.signal_type == "EXIT"
assert 0.6 <= entry_signal.confidence <= 0.9
assert 0.6 <= exit_signal.confidence <= 0.9
# Run test
test_random_strategy()
print("✅ RandomStrategy tests passed")
```
## Use Cases
1. **Framework Testing**: Validate incremental strategy system
2. **Performance Benchmarking**: Baseline for strategy comparison
3. **Signal Pipeline Testing**: Test signal processing and execution
4. **Load Testing**: High-frequency signal generation testing
5. **Integration Testing**: Verify trading system integration

View File

@@ -1,520 +0,0 @@
# Real-Time Strategy Implementation Plan - Option 1: Incremental Calculation Architecture
## Implementation Overview
This document outlines the step-by-step implementation plan for updating the trading strategy system to support real-time data processing with incremental calculations. The implementation is divided into phases to ensure stability and backward compatibility.
## Phase 1: Foundation and Base Classes (Week 1-2) ✅ COMPLETED
### 1.1 Create Indicator State Classes ✅ COMPLETED
**Priority: HIGH**
**Files created:**
- `cycles/IncStrategies/indicators/`
- `__init__.py`
- `base.py` - Base IndicatorState class ✅
- `moving_average.py` - MovingAverageState ✅
- `rsi.py` - RSIState ✅
- `supertrend.py` - SupertrendState ✅
- `bollinger_bands.py` - BollingerBandsState ✅
- `atr.py` - ATRState (for Supertrend) ✅
**Tasks:**
- [x] Create `IndicatorState` abstract base class
- [x] Implement `MovingAverageState` with incremental calculation
- [x] Implement `RSIState` with incremental calculation
- [x] Implement `ATRState` for Supertrend calculations
- [x] Implement `SupertrendState` with incremental calculation
- [x] Implement `BollingerBandsState` with incremental calculation
- [x] Add comprehensive unit tests for each indicator state ✅
- [x] Validate accuracy against traditional batch calculations ✅
**Acceptance Criteria:**
- ✅ All indicator states produce identical results to batch calculations (within 0.01% tolerance)
- ✅ Memory usage is constant regardless of data length
- ✅ Update time is <0.1ms per data point
- ✅ All indicators handle edge cases (NaN, zero values, etc.)
### 1.2 Update Base Strategy Class ✅ COMPLETED
**Priority: HIGH**
**Files created:**
- `cycles/IncStrategies/base.py`
**Tasks:**
- [x] Add new abstract methods to `IncStrategyBase`:
- `get_minimum_buffer_size()`
- `calculate_on_data()`
- `supports_incremental_calculation()`
- [x] Add new properties:
- `calculation_mode`
- `is_warmed_up`
- [x] Add internal state management:
- `_calculation_mode`
- `_is_warmed_up`
- `_data_points_received`
- `_timeframe_buffers`
- `_timeframe_last_update`
- `_indicator_states`
- `_last_signals`
- `_signal_history`
- [x] Implement buffer management methods:
- `_update_timeframe_buffers()`
- `_should_update_timeframe()`
- `_get_timeframe_buffer()`
- [x] Add error handling and recovery methods:
- `_validate_calculation_state()`
- `_recover_from_state_corruption()`
- `handle_data_gap()`
- [x] Provide default implementations for backward compatibility
**Acceptance Criteria:**
- ✅ Existing strategies continue to work without modification (compatibility layer)
- ✅ New interface is fully documented
- ✅ Buffer management is memory-efficient
- ✅ Error recovery mechanisms are robust
### 1.3 Create Configuration System ✅ COMPLETED
**Priority: MEDIUM**
**Files created:**
- Configuration integrated into base classes ✅
**Tasks:**
- [x] Define strategy configuration dataclass (integrated into base class)
- [x] Add incremental calculation settings
- [x] Add buffer size configuration
- [x] Add performance monitoring settings
- [x] Add error handling configuration
## Phase 2: Strategy Implementation (Week 3-4) ✅ COMPLETED
### 2.1 Update RandomStrategy (Simplest) ✅ COMPLETED
**Priority: HIGH**
**Files created:**
- `cycles/IncStrategies/random_strategy.py`
- `cycles/IncStrategies/test_random_strategy.py`
**Tasks:**
- [x] Implement `get_minimum_buffer_size()` (return {"1min": 1})
- [x] Implement `calculate_on_data()` (minimal processing)
- [x] Implement `supports_incremental_calculation()` (return True)
- [x] Update signal generation to work without pre-calculated arrays
- [x] Add comprehensive testing
- [x] Validate against current implementation
**Acceptance Criteria:**
- ✅ RandomStrategy works in both batch and incremental modes
- ✅ Signal generation is identical between modes
- ✅ Memory usage is minimal
- ✅ Performance is optimal (0.006ms update, 0.048ms signal generation)
### 2.2 Update MetaTrend Strategy (Supertrend-based) ✅ COMPLETED
**Priority: HIGH**
**Files created:**
- `cycles/IncStrategies/metatrend_strategy.py`
- `test_metatrend_comparison.py`
- `plot_original_vs_incremental.py`
**Tasks:**
- [x] Implement `get_minimum_buffer_size()` based on timeframe
- [x] Implement `_initialize_indicator_states()` for three Supertrend indicators
- [x] Implement `calculate_on_data()` with incremental Supertrend updates
- [x] Update `get_entry_signal()` to work with current state instead of arrays
- [x] Update `get_exit_signal()` to work with current state instead of arrays
- [x] Implement meta-trend calculation from current Supertrend states
- [x] Add state validation and recovery
- [x] Comprehensive testing against current implementation
- [x] Visual comparison plotting with signal analysis
- [x] Bug discovery and validation in original DefaultStrategy
**Implementation Details:**
- **SupertrendCollection**: Manages 3 Supertrend indicators with parameters (12,3.0), (10,1.0), (11,2.0)
- **Meta-trend Logic**: Uptrend when all agree (+1), Downtrend when all agree (-1), Neutral otherwise (0)
- **Signal Generation**: Entry on meta-trend change to +1, Exit on meta-trend change to -1
- **Performance**: <1ms updates, 17 signals vs 106 (original buggy), mathematically accurate
**Testing Results:**
- ✅ 98.5% accuracy vs corrected original strategy (99.5% vs buggy original)
- ✅ Comprehensive visual comparison with 525,601 data points (2022-2023)
- ✅ Bug discovery in original DefaultStrategy exit condition
- ✅ Production-ready incremental implementation validated
**Acceptance Criteria:**
- ✅ Supertrend calculations are identical to batch mode
- ✅ Meta-trend logic produces correct signals (bug-free)
- ✅ Memory usage is bounded by buffer size
- ✅ Performance meets <1ms update target
- ✅ Visual validation confirms correct behavior
### 2.3 Update BBRSStrategy (Bollinger Bands + RSI) ✅ COMPLETED
**Priority: HIGH**
**Files created:**
- `cycles/IncStrategies/bbrs_incremental.py`
- `test_bbrs_incremental.py`
- `test_realtime_bbrs.py`
- `test_incremental_indicators.py`
**Tasks:**
- [x] Implement `get_minimum_buffer_size()` based on BB and RSI periods
- [x] Implement `_initialize_indicator_states()` for BB, RSI, and market regime
- [x] Implement `calculate_on_data()` with incremental indicator updates
- [x] Update signal generation to work with current indicator states
- [x] Implement market regime detection with incremental updates
- [x] Add state validation and recovery
- [x] Comprehensive testing against current implementation
- [x] Add real-time minute-level data processing with timeframe aggregation
- [x] Implement TimeframeAggregator for internal data aggregation
- [x] Validate incremental indicators (BB, RSI) against original implementations
- [x] Test real-time simulation with different timeframes (15min, 1h)
- [x] Verify consistency between minute-level and pre-aggregated processing
**Implementation Details:**
- **TimeframeAggregator**: Handles real-time aggregation of minute data to higher timeframes
- **BBRSIncrementalState**: Complete incremental BBRS strategy with market regime detection
- **Real-time Compatibility**: Accepts minute-level data, internally aggregates to configured timeframe
- **Market Regime Logic**: Trending vs Sideways detection based on Bollinger Band width
- **Signal Generation**: Regime-specific buy/sell logic with volume analysis
- **Performance**: Constant memory usage, O(1) updates per data point
**Testing Results:**
- ✅ Perfect accuracy (0.000000 difference) vs original implementation after warm-up
- ✅ Real-time processing: 2,881 minutes → 192 15min bars (exact match)
- ✅ Real-time processing: 2,881 minutes → 48 1h bars (exact match)
- ✅ Incremental indicators validated: BB (perfect), RSI (0.04 mean difference after warm-up)
- ✅ Signal generation: 95.45% match rate for buy/sell signals
- ✅ Market regime detection working correctly
- ✅ Visual comparison plots generated and validated
**Acceptance Criteria:**
- ✅ BB and RSI calculations match batch mode exactly (after warm-up period)
- ✅ Market regime detection works incrementally
- ✅ Signal generation is identical between modes (95.45% match rate)
- ✅ Performance meets targets (constant memory, fast updates)
- ✅ Real-time minute-level data processing works correctly
- ✅ Internal timeframe aggregation produces identical results to pre-aggregated data
## Phase 3: Strategy Manager Updates (Week 5) 📋 PENDING
### 3.1 Update StrategyManager
**Priority: HIGH**
**Files to create:**
- `cycles/IncStrategies/manager.py`
**Tasks:**
- [ ] Add `process_new_data()` method for coordinating incremental updates
- [ ] Add buffer size calculation across all strategies
- [ ] Add initialization mode detection and coordination
- [ ] Update signal combination to work with incremental mode
- [ ] Add performance monitoring and metrics collection
- [ ] Add error handling for strategy failures
- [ ] Add configuration management
**Acceptance Criteria:**
- Manager coordinates multiple strategies efficiently
- Buffer sizes are calculated correctly
- Error handling is robust
- Performance monitoring works
### 3.2 Add Performance Monitoring
**Priority: MEDIUM**
**Files to create:**
- `cycles/IncStrategies/monitoring.py`
**Tasks:**
- [ ] Create performance metrics collection
- [ ] Add latency measurement
- [ ] Add memory usage tracking
- [ ] Add signal generation frequency tracking
- [ ] Add error rate monitoring
- [ ] Create performance reporting
## Phase 4: Integration and Testing (Week 6) 📋 PENDING
### 4.1 Update StrategyTrader Integration
**Priority: HIGH**
**Files to modify:**
- `TraderFrontend/trader/strategy_trader.py`
**Tasks:**
- [ ] Update `_process_strategies()` to use incremental mode
- [ ] Add buffer management for real-time data
- [ ] Update initialization to support incremental mode
- [ ] Add performance monitoring integration
- [ ] Add error recovery mechanisms
- [ ] Update configuration handling
**Acceptance Criteria:**
- Real-time trading works with incremental strategies
- Performance is significantly improved
- Memory usage is bounded
- Error recovery works correctly
### 4.2 Update Backtesting Integration
**Priority: MEDIUM**
**Files to modify:**
- `cycles/backtest.py`
- `main.py`
**Tasks:**
- [ ] Add support for incremental mode in backtesting
- [ ] Maintain backward compatibility with batch mode
- [ ] Add performance comparison between modes
- [ ] Update configuration handling
**Acceptance Criteria:**
- Backtesting works in both modes
- Results are identical between modes
- Performance comparison is available
### 4.3 Comprehensive Testing ✅ COMPLETED (MetaTrend)
**Priority: HIGH**
**Files created:**
- `test_metatrend_comparison.py`
- `plot_original_vs_incremental.py`
- `SIGNAL_COMPARISON_SUMMARY.md`
**Tasks:**
- [x] Create unit tests for MetaTrend indicator states
- [x] Create integration tests for MetaTrend strategy implementation
- [x] Create performance benchmarks
- [x] Create accuracy validation tests
- [x] Create memory usage tests
- [x] Create error recovery tests
- [x] Create real-time simulation tests
- [x] Create visual comparison and analysis tools
- [ ] Extend testing to other strategies (BBRSStrategy, etc.)
**Acceptance Criteria:**
- ✅ MetaTrend tests pass with 98.5% accuracy
- ✅ Performance targets are met (<1ms updates)
- ✅ Memory usage is within bounds
- ✅ Error recovery works correctly
- ✅ Visual validation confirms correct behavior
## Phase 5: Optimization and Documentation (Week 7) 🔄 IN PROGRESS
### 5.1 Performance Optimization ✅ COMPLETED (MetaTrend)
**Priority: MEDIUM**
**Tasks:**
- [x] Profile and optimize MetaTrend indicator calculations
- [x] Optimize buffer management
- [x] Optimize signal generation
- [x] Add caching where appropriate
- [x] Optimize memory allocation patterns
- [ ] Extend optimization to other strategies
### 5.2 Documentation ✅ COMPLETED (MetaTrend)
**Priority: MEDIUM**
**Tasks:**
- [x] Update MetaTrend strategy docstrings
- [x] Create MetaTrend implementation guide
- [x] Create performance analysis documentation
- [x] Create visual comparison documentation
- [x] Update README files for MetaTrend
- [ ] Extend documentation to other strategies
### 5.3 Configuration and Monitoring ✅ COMPLETED (MetaTrend)
**Priority: LOW**
**Tasks:**
- [x] Add MetaTrend configuration validation
- [x] Add runtime configuration updates
- [x] Add monitoring for MetaTrend performance
- [x] Add alerting for performance issues
- [ ] Extend to other strategies
## Implementation Status Summary
### ✅ Completed (Phase 1, 2.1, 2.2, 2.3)
- **Foundation Infrastructure**: Complete incremental indicator system
- **Base Classes**: Full `IncStrategyBase` with buffer management and error handling
- **Indicator States**: All required indicators (MA, RSI, ATR, Supertrend, Bollinger Bands)
- **Memory Management**: Bounded buffer system with configurable sizes
- **Error Handling**: State validation, corruption recovery, data gap handling
- **Performance Monitoring**: Built-in metrics collection and timing
- **IncRandomStrategy**: Complete implementation with testing (0.006ms updates, 0.048ms signals)
- **IncMetaTrendStrategy**: Complete implementation with comprehensive testing and validation
- 98.5% accuracy vs corrected original strategy
- Visual comparison tools and analysis
- Bug discovery in original DefaultStrategy
- Production-ready with <1ms updates
- **BBRSIncrementalStrategy**: Complete implementation with real-time processing capabilities
- Perfect accuracy (0.000000 difference) vs original implementation after warm-up
- Real-time minute-level data processing with internal timeframe aggregation
- Market regime detection (trending vs sideways) working correctly
- 95.45% signal match rate with comprehensive testing
- TimeframeAggregator for seamless real-time data handling
- Production-ready for live trading systems
### 🔄 Current Focus (Phase 3)
- **Strategy Manager**: Coordinating multiple incremental strategies
- **Integration Testing**: Ensuring all components work together
- **Performance Optimization**: Fine-tuning for production deployment
### 📋 Remaining Work
- Strategy manager updates
- Integration with existing systems
- Comprehensive testing suite for strategy combinations
- Performance optimization for multi-strategy scenarios
- Documentation updates for deployment guides
## Implementation Details
### MetaTrend Strategy Implementation ✅
#### Buffer Size Calculations
```python
def get_minimum_buffer_size(self) -> Dict[str, int]:
primary_tf = self.params.get("timeframe", "1min")
# Supertrend needs warmup period for reliable calculation
if primary_tf == "15min":
return {"15min": 50, "1min": 750} # 50 * 15 = 750 minutes
elif primary_tf == "5min":
return {"5min": 50, "1min": 250} # 50 * 5 = 250 minutes
elif primary_tf == "30min":
return {"30min": 50, "1min": 1500} # 50 * 30 = 1500 minutes
elif primary_tf == "1h":
return {"1h": 50, "1min": 3000} # 50 * 60 = 3000 minutes
else: # 1min
return {"1min": 50}
```
#### Supertrend Parameters
- ST1: Period=12, Multiplier=3.0
- ST2: Period=10, Multiplier=1.0
- ST3: Period=11, Multiplier=2.0
#### Meta-trend Logic
- **Uptrend (+1)**: All 3 Supertrends agree on uptrend
- **Downtrend (-1)**: All 3 Supertrends agree on downtrend
- **Neutral (0)**: Supertrends disagree
#### Signal Generation
- **Entry**: Meta-trend changes from != 1 to == 1
- **Exit**: Meta-trend changes from != -1 to == -1
### BBRSStrategy Implementation ✅
#### Buffer Size Calculations
```python
def get_minimum_buffer_size(self) -> Dict[str, int]:
bb_period = self.params.get("bb_period", 20)
rsi_period = self.params.get("rsi_period", 14)
volume_ma_period = 20
# Need max of all periods plus warmup
min_periods = max(bb_period, rsi_period, volume_ma_period) + 20
return {"1min": min_periods}
```
#### Timeframe Aggregation
- **TimeframeAggregator**: Handles real-time aggregation of minute data to higher timeframes
- **Configurable Timeframes**: 1min, 5min, 15min, 30min, 1h, etc.
- **OHLCV Aggregation**: Proper open/high/low/close/volume aggregation
- **Bar Completion**: Only processes indicators when complete timeframe bars are formed
#### Market Regime Detection
- **Trending Market**: BB width >= threshold (default 0.05)
- **Sideways Market**: BB width < threshold
- **Adaptive Parameters**: Different BB multipliers and RSI thresholds per regime
#### Signal Generation Logic
```python
# Sideways Market (Mean Reversion)
buy_condition = (price <= lower_band) and (rsi_value <= rsi_low)
sell_condition = (price >= upper_band) and (rsi_value >= rsi_high)
# Trending Market (Breakout Mode)
buy_condition = (price < lower_band) and (rsi_value < 50) and volume_spike
sell_condition = (price > upper_band) and (rsi_value > 50) and volume_spike
```
#### Real-time Processing Flow
1. **Minute Data Input**: Accept live minute-level OHLCV data
2. **Timeframe Aggregation**: Accumulate into configured timeframe bars
3. **Indicator Updates**: Update BB, RSI, volume MA when bar completes
4. **Market Regime**: Determine trending vs sideways based on BB width
5. **Signal Generation**: Apply regime-specific buy/sell logic
6. **State Management**: Maintain constant memory usage
### Error Recovery Strategy
1. **State Validation**: Periodic validation of indicator states ✅
2. **Graceful Degradation**: Fall back to batch calculation if incremental fails ✅
3. **Automatic Recovery**: Reinitialize from buffer data when corruption detected ✅
4. **Monitoring**: Track error rates and performance metrics ✅
### Performance Targets
- **Incremental Update**: <1ms per data point ✅
- **Signal Generation**: <10ms per strategy ✅
- **Memory Usage**: <100MB per strategy (bounded by buffer size) ✅
- **Accuracy**: 99.99% identical to batch calculations ✅ (98.5% for MetaTrend due to original bug)
### Testing Strategy
1. **Unit Tests**: Test each component in isolation ✅ (MetaTrend)
2. **Integration Tests**: Test strategy combinations ✅ (MetaTrend)
3. **Performance Tests**: Benchmark against current implementation ✅ (MetaTrend)
4. **Accuracy Tests**: Validate against known good results ✅ (MetaTrend)
5. **Stress Tests**: Test with high-frequency data ✅ (MetaTrend)
6. **Memory Tests**: Validate memory usage bounds ✅ (MetaTrend)
7. **Visual Tests**: Create comparison plots and analysis ✅ (MetaTrend)
## Risk Mitigation
### Technical Risks
- **Accuracy Issues**: Comprehensive testing and validation ✅
- **Performance Regression**: Benchmarking and optimization ✅
- **Memory Leaks**: Careful buffer management and testing ✅
- **State Corruption**: Validation and recovery mechanisms ✅
### Implementation Risks
- **Complexity**: Phased implementation with incremental testing ✅
- **Breaking Changes**: Backward compatibility layer ✅
- **Timeline**: Conservative estimates with buffer time ✅
### Operational Risks
- **Production Issues**: Gradual rollout with monitoring ✅
- **Data Quality**: Robust error handling and validation ✅
- **System Load**: Performance monitoring and alerting ✅
## Success Criteria
### Functional Requirements
- [x] MetaTrend strategy works in incremental mode ✅
- [x] Signal generation is mathematically correct (bug-free) ✅
- [x] Real-time performance is significantly improved ✅
- [x] Memory usage is bounded and predictable ✅
- [ ] All strategies work in incremental mode (BBRSStrategy pending)
### Performance Requirements
- [x] 10x improvement in processing speed for real-time data ✅
- [x] 90% reduction in memory usage for long-running systems ✅
- [x] <1ms latency for incremental updates ✅
- [x] <10ms latency for signal generation ✅
### Quality Requirements
- [x] 100% test coverage for MetaTrend strategy ✅
- [x] 98.5% accuracy compared to corrected batch calculations ✅
- [x] Zero memory leaks in long-running tests ✅
- [x] Robust error handling and recovery ✅
- [ ] Extend quality requirements to remaining strategies
## Key Achievements
### MetaTrend Strategy Success ✅
- **Bug Discovery**: Found and documented critical bug in original DefaultStrategy exit condition
- **Mathematical Accuracy**: Achieved 98.5% signal match with corrected implementation
- **Performance**: <1ms updates, suitable for high-frequency trading
- **Visual Validation**: Comprehensive plotting and analysis tools created
- **Production Ready**: Fully tested and validated for live trading systems
### Architecture Success ✅
- **Unified Interface**: All incremental strategies follow consistent `IncStrategyBase` pattern
- **Memory Efficiency**: Bounded buffer system prevents memory growth
- **Error Recovery**: Robust state validation and recovery mechanisms
- **Performance Monitoring**: Built-in metrics and timing analysis
This implementation plan provides a structured approach to implementing the incremental calculation architecture while maintaining system stability and backward compatibility. The MetaTrend strategy implementation serves as a proven template for future strategy conversions.

View File

@@ -1,342 +0,0 @@
# Real-Time Strategy Architecture - Technical Specification
## Overview
This document outlines the technical specification for updating the trading strategy system to support real-time data processing with incremental calculations. The current architecture processes entire datasets during initialization, which is inefficient for real-time trading where new data arrives continuously.
## Current Architecture Issues
### Problems with Current Implementation
1. **Initialization-Heavy Design**: All calculations performed during `initialize()` method
2. **Full Dataset Processing**: Entire historical dataset processed on each initialization
3. **Memory Inefficient**: Stores complete calculation history in arrays
4. **No Incremental Updates**: Cannot add new data without full recalculation
5. **Performance Bottleneck**: Recalculating years of data for each new candle
6. **Index-Based Access**: Signal generation relies on pre-calculated arrays with fixed indices
### Current Strategy Flow
```
Data → initialize() → Full Calculation → Store Arrays → get_signal(index)
```
## Target Architecture: Incremental Calculation
### New Strategy Flow
```
Initial Data → initialize() → Warm-up Calculation → Ready State
New Data Point → calculate_on_data() → Update State → get_signal()
```
## Technical Requirements
### 1. Base Strategy Interface Updates
#### New Abstract Methods
```python
@abstractmethod
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for each timeframe.
Returns:
Dict[str, int]: {timeframe: min_points} mapping
Example:
{"15min": 50, "1min": 750} # 50 15min candles = 750 1min candles
"""
pass
@abstractmethod
def calculate_on_data(self, new_data_point: Dict, timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
pass
@abstractmethod
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Returns:
bool: True if incremental mode supported
"""
pass
```
#### New Properties and Methods
```python
@property
def calculation_mode(self) -> str:
"""Current calculation mode: 'initialization' or 'incremental'"""
return self._calculation_mode
@property
def is_warmed_up(self) -> bool:
"""Whether strategy has sufficient data for reliable signals"""
return self._is_warmed_up
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization"""
pass
def get_current_state_summary(self) -> Dict:
"""Get summary of current calculation state for debugging"""
pass
```
### 2. Internal State Management
#### State Variables
Each strategy must maintain:
```python
class StrategyBase:
def __init__(self, ...):
# Calculation state
self._calculation_mode = "initialization" # or "incremental"
self._is_warmed_up = False
self._data_points_received = 0
# Timeframe-specific buffers
self._timeframe_buffers = {} # {timeframe: deque(maxlen=buffer_size)}
self._timeframe_last_update = {} # {timeframe: timestamp}
# Indicator states (strategy-specific)
self._indicator_states = {}
# Signal generation state
self._last_signals = {} # Cache recent signals
self._signal_history = deque(maxlen=100) # Recent signal history
```
#### Buffer Management
```python
def _update_timeframe_buffers(self, new_data_point: Dict, timestamp: pd.Timestamp):
"""Update all timeframe buffers with new data point"""
def _should_update_timeframe(self, timeframe: str, timestamp: pd.Timestamp) -> bool:
"""Check if timeframe should be updated based on timestamp"""
def _get_timeframe_buffer(self, timeframe: str) -> pd.DataFrame:
"""Get current buffer for specific timeframe"""
```
### 3. Strategy-Specific Requirements
#### DefaultStrategy (Supertrend-based)
```python
class DefaultStrategy(StrategyBase):
def get_minimum_buffer_size(self) -> Dict[str, int]:
primary_tf = self.params.get("timeframe", "15min")
if primary_tf == "15min":
return {"15min": 50, "1min": 750}
elif primary_tf == "5min":
return {"5min": 50, "1min": 250}
# ... other timeframes
def _initialize_indicator_states(self):
"""Initialize Supertrend calculation states"""
self._supertrend_states = [
SupertrendState(period=10, multiplier=3.0),
SupertrendState(period=11, multiplier=2.0),
SupertrendState(period=12, multiplier=1.0)
]
def _update_supertrend_incrementally(self, ohlc_data):
"""Update Supertrend calculations with new data"""
# Incremental ATR calculation
# Incremental Supertrend calculation
# Update meta-trend based on all three Supertrends
```
#### BBRSStrategy (Bollinger Bands + RSI)
```python
class BBRSStrategy(StrategyBase):
def get_minimum_buffer_size(self) -> Dict[str, int]:
bb_period = self.params.get("bb_period", 20)
rsi_period = self.params.get("rsi_period", 14)
min_periods = max(bb_period, rsi_period) + 10 # +10 for warmup
return {"1min": min_periods}
def _initialize_indicator_states(self):
"""Initialize BB and RSI calculation states"""
self._bb_state = BollingerBandsState(period=self.params.get("bb_period", 20))
self._rsi_state = RSIState(period=self.params.get("rsi_period", 14))
self._market_regime_state = MarketRegimeState()
def _update_indicators_incrementally(self, price_data):
"""Update BB, RSI, and market regime with new data"""
# Incremental moving average for BB
# Incremental RSI calculation
# Market regime detection update
```
#### RandomStrategy
```python
class RandomStrategy(StrategyBase):
def get_minimum_buffer_size(self) -> Dict[str, int]:
return {"1min": 1} # No indicators needed
def supports_incremental_calculation(self) -> bool:
return True # Always supports incremental
```
### 4. Indicator State Classes
#### Base Indicator State
```python
class IndicatorState(ABC):
"""Base class for maintaining indicator calculation state"""
@abstractmethod
def update(self, new_value: float) -> float:
"""Update indicator with new value and return current indicator value"""
pass
@abstractmethod
def is_warmed_up(self) -> bool:
"""Whether indicator has enough data for reliable values"""
pass
@abstractmethod
def reset(self) -> None:
"""Reset indicator state"""
pass
```
#### Specific Indicator States
```python
class MovingAverageState(IndicatorState):
"""Maintains state for incremental moving average calculation"""
class RSIState(IndicatorState):
"""Maintains state for incremental RSI calculation"""
class SupertrendState(IndicatorState):
"""Maintains state for incremental Supertrend calculation"""
class BollingerBandsState(IndicatorState):
"""Maintains state for incremental Bollinger Bands calculation"""
```
### 5. Data Flow Architecture
#### Initialization Phase
```
1. Strategy.initialize(backtester)
2. Strategy._resample_data(original_data)
3. Strategy._initialize_indicator_states()
4. Strategy._warm_up_with_historical_data()
5. Strategy._calculation_mode = "incremental"
6. Strategy._is_warmed_up = True
```
#### Real-Time Processing Phase
```
1. New data arrives → StrategyManager.process_new_data()
2. StrategyManager → Strategy.calculate_on_data(new_point)
3. Strategy._update_timeframe_buffers()
4. Strategy._update_indicators_incrementally()
5. Strategy ready for get_entry_signal()/get_exit_signal()
```
### 6. Performance Requirements
#### Memory Efficiency
- Maximum buffer size per timeframe: configurable (default: 200 periods)
- Use `collections.deque` with `maxlen` for automatic buffer management
- Store only essential state, not full calculation history
#### Processing Speed
- Target: <1ms per data point for incremental updates
- Target: <10ms for signal generation
- Batch processing support for multiple data points
#### Accuracy Requirements
- Incremental calculations must match batch calculations within 0.01% tolerance
- Indicator values must be identical to traditional calculation methods
- Signal timing must be preserved exactly
### 7. Error Handling and Recovery
#### State Corruption Recovery
```python
def _validate_calculation_state(self) -> bool:
"""Validate internal calculation state consistency"""
def _recover_from_state_corruption(self) -> None:
"""Recover from corrupted calculation state"""
# Reset to initialization mode
# Recalculate from available buffer data
# Resume incremental mode
```
#### Data Gap Handling
```python
def handle_data_gap(self, gap_duration: pd.Timedelta) -> None:
"""Handle gaps in data stream"""
if gap_duration > self._max_acceptable_gap:
self._trigger_reinitialization()
else:
self._interpolate_missing_data()
```
### 8. Backward Compatibility
#### Compatibility Layer
- Existing `initialize()` method continues to work
- New methods are optional with default implementations
- Gradual migration path for existing strategies
- Fallback to batch calculation if incremental not supported
#### Migration Strategy
1. Phase 1: Add new interface with default implementations
2. Phase 2: Implement incremental calculation for each strategy
3. Phase 3: Optimize and remove batch calculation fallbacks
4. Phase 4: Make incremental calculation mandatory
### 9. Testing Requirements
#### Unit Tests
- Test incremental vs. batch calculation accuracy
- Test state management and recovery
- Test buffer management and memory usage
- Test performance benchmarks
#### Integration Tests
- Test with real-time data streams
- Test strategy manager coordination
- Test error recovery scenarios
- Test memory usage over extended periods
#### Performance Tests
- Benchmark incremental vs. batch processing
- Memory usage profiling
- Latency measurements for signal generation
- Stress testing with high-frequency data
### 10. Configuration and Monitoring
#### Configuration Options
```python
STRATEGY_CONFIG = {
"calculation_mode": "incremental", # or "batch"
"buffer_size_multiplier": 2.0, # multiply minimum buffer size
"max_acceptable_gap": "5min", # max data gap before reinitialization
"enable_state_validation": True, # enable periodic state validation
"performance_monitoring": True # enable performance metrics
}
```
#### Monitoring Metrics
- Calculation latency per strategy
- Memory usage per strategy
- State validation failures
- Data gap occurrences
- Signal generation frequency
This specification provides the foundation for implementing efficient real-time strategy processing while maintaining accuracy and reliability.

View File

@@ -1,447 +0,0 @@
"""
Example usage of the Incremental Backtester.
This script demonstrates how to use the IncBacktester for various scenarios:
1. Single strategy backtesting
2. Multiple strategy comparison
3. Parameter optimization with multiprocessing
4. Custom analysis and result saving
5. Comprehensive result logging and action tracking
Run this script to see the backtester in action with real or synthetic data.
"""
import pandas as pd
import numpy as np
import logging
from datetime import datetime, timedelta
import os
from cycles.IncStrategies import (
IncBacktester, BacktestConfig, IncRandomStrategy
)
from cycles.utils.storage import Storage
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def ensure_results_directory():
"""Ensure the results directory exists."""
results_dir = "results"
if not os.path.exists(results_dir):
os.makedirs(results_dir)
logger.info(f"Created results directory: {results_dir}")
return results_dir
def create_sample_data(days: int = 30) -> pd.DataFrame:
"""
Create sample OHLCV data for demonstration.
Args:
days: Number of days of data to generate
Returns:
pd.DataFrame: Sample OHLCV data
"""
# Create date range
end_date = datetime.now()
start_date = end_date - timedelta(days=days)
timestamps = pd.date_range(start=start_date, end=end_date, freq='1min')
# Generate realistic price data
np.random.seed(42)
n_points = len(timestamps)
# Start with a base price
base_price = 45000
# Generate price movements with trend and volatility
trend = np.linspace(0, 0.1, n_points) # Slight upward trend
volatility = np.random.normal(0, 0.002, n_points) # 0.2% volatility
# Calculate prices
log_returns = trend + volatility
prices = base_price * np.exp(np.cumsum(log_returns))
# Generate OHLCV data
data = []
for i, (timestamp, close_price) in enumerate(zip(timestamps, prices)):
# Generate realistic OHLC
intrabar_vol = close_price * 0.001
open_price = close_price + np.random.normal(0, intrabar_vol)
high_price = max(open_price, close_price) + abs(np.random.normal(0, intrabar_vol))
low_price = min(open_price, close_price) - abs(np.random.normal(0, intrabar_vol))
volume = np.random.uniform(50, 500)
data.append({
'open': open_price,
'high': high_price,
'low': low_price,
'close': close_price,
'volume': volume
})
df = pd.DataFrame(data, index=timestamps)
return df
def example_single_strategy():
"""Example 1: Single strategy backtesting with comprehensive results."""
print("\n" + "="*60)
print("EXAMPLE 1: Single Strategy Backtesting")
print("="*60)
# Create sample data
data = create_sample_data(days=7) # 1 week of data
# Save data
storage = Storage()
data_file = "sample_data_single.csv"
storage.save_data(data, data_file)
# Configure backtest
config = BacktestConfig(
data_file=data_file,
start_date=data.index[0].strftime("%Y-%m-%d"),
end_date=data.index[-1].strftime("%Y-%m-%d"),
initial_usd=10000,
stop_loss_pct=0.02,
take_profit_pct=0.05
)
# Create strategy
strategy = IncRandomStrategy(params={
"timeframe": "15min",
"entry_probability": 0.15,
"exit_probability": 0.2,
"random_seed": 42
})
# Run backtest
backtester = IncBacktester(config, storage)
results = backtester.run_single_strategy(strategy)
# Print results
print(f"\nResults:")
print(f" Strategy: {results['strategy_name']}")
print(f" Profit: {results['profit_ratio']*100:.2f}%")
print(f" Final Balance: ${results['final_usd']:,.2f}")
print(f" Trades: {results['n_trades']}")
print(f" Win Rate: {results['win_rate']*100:.1f}%")
print(f" Max Drawdown: {results['max_drawdown']*100:.2f}%")
# Save comprehensive results
backtester.save_comprehensive_results([results], "example_single_strategy")
# Cleanup
if os.path.exists(f"data/{data_file}"):
os.remove(f"data/{data_file}")
return results
def example_multiple_strategies():
"""Example 2: Multiple strategy comparison with comprehensive results."""
print("\n" + "="*60)
print("EXAMPLE 2: Multiple Strategy Comparison")
print("="*60)
# Create sample data
data = create_sample_data(days=10) # 10 days of data
# Save data
storage = Storage()
data_file = "sample_data_multiple.csv"
storage.save_data(data, data_file)
# Configure backtest
config = BacktestConfig(
data_file=data_file,
start_date=data.index[0].strftime("%Y-%m-%d"),
end_date=data.index[-1].strftime("%Y-%m-%d"),
initial_usd=10000,
stop_loss_pct=0.015
)
# Create multiple strategies with different parameters
strategies = [
IncRandomStrategy(params={
"timeframe": "5min",
"entry_probability": 0.1,
"exit_probability": 0.15,
"random_seed": 42
}),
IncRandomStrategy(params={
"timeframe": "15min",
"entry_probability": 0.12,
"exit_probability": 0.18,
"random_seed": 123
}),
IncRandomStrategy(params={
"timeframe": "30min",
"entry_probability": 0.08,
"exit_probability": 0.12,
"random_seed": 456
}),
IncRandomStrategy(params={
"timeframe": "1h",
"entry_probability": 0.06,
"exit_probability": 0.1,
"random_seed": 789
})
]
# Run backtest
backtester = IncBacktester(config, storage)
results = backtester.run_multiple_strategies(strategies)
# Print comparison
print(f"\nStrategy Comparison:")
print(f"{'Strategy':<20} {'Timeframe':<10} {'Profit %':<10} {'Trades':<8} {'Win Rate %':<12}")
print("-" * 70)
for i, result in enumerate(results):
if result.get("success", True):
timeframe = result['strategy_params']['timeframe']
profit = result['profit_ratio'] * 100
trades = result['n_trades']
win_rate = result['win_rate'] * 100
print(f"Strategy {i+1:<13} {timeframe:<10} {profit:<10.2f} {trades:<8} {win_rate:<12.1f}")
# Get summary statistics
summary = backtester.get_summary_statistics(results)
print(f"\nSummary Statistics:")
print(f" Best Profit: {summary['profit_ratio']['max']*100:.2f}%")
print(f" Worst Profit: {summary['profit_ratio']['min']*100:.2f}%")
print(f" Average Profit: {summary['profit_ratio']['mean']*100:.2f}%")
print(f" Profit Std Dev: {summary['profit_ratio']['std']*100:.2f}%")
# Save comprehensive results
backtester.save_comprehensive_results(results, "example_multiple_strategies", summary)
# Cleanup
if os.path.exists(f"data/{data_file}"):
os.remove(f"data/{data_file}")
return results, summary
def example_parameter_optimization():
"""Example 3: Parameter optimization with multiprocessing and comprehensive results."""
print("\n" + "="*60)
print("EXAMPLE 3: Parameter Optimization")
print("="*60)
# Create sample data
data = create_sample_data(days=5) # 5 days for faster optimization
# Save data
storage = Storage()
data_file = "sample_data_optimization.csv"
storage.save_data(data, data_file)
# Configure backtest
config = BacktestConfig(
data_file=data_file,
start_date=data.index[0].strftime("%Y-%m-%d"),
end_date=data.index[-1].strftime("%Y-%m-%d"),
initial_usd=10000
)
# Define parameter grids
strategy_param_grid = {
"timeframe": ["5min", "15min", "30min"],
"entry_probability": [0.08, 0.12, 0.16],
"exit_probability": [0.1, 0.15, 0.2],
"random_seed": [42] # Keep seed constant for fair comparison
}
trader_param_grid = {
"stop_loss_pct": [0.01, 0.015, 0.02],
"take_profit_pct": [0.0, 0.03, 0.05]
}
# Run optimization (will use SystemUtils to determine optimal workers)
backtester = IncBacktester(config, storage)
print(f"Starting optimization with {len(strategy_param_grid['timeframe']) * len(strategy_param_grid['entry_probability']) * len(strategy_param_grid['exit_probability']) * len(trader_param_grid['stop_loss_pct']) * len(trader_param_grid['take_profit_pct'])} combinations...")
results = backtester.optimize_parameters(
strategy_class=IncRandomStrategy,
param_grid=strategy_param_grid,
trader_param_grid=trader_param_grid,
max_workers=None # Use SystemUtils for optimal worker count
)
# Get summary
summary = backtester.get_summary_statistics(results)
# Print optimization results
print(f"\nOptimization Results:")
print(f" Total Combinations: {summary['total_runs']}")
print(f" Successful Runs: {summary['successful_runs']}")
print(f" Failed Runs: {summary['failed_runs']}")
if summary['successful_runs'] > 0:
print(f" Best Profit: {summary['profit_ratio']['max']*100:.2f}%")
print(f" Worst Profit: {summary['profit_ratio']['min']*100:.2f}%")
print(f" Average Profit: {summary['profit_ratio']['mean']*100:.2f}%")
# Show top 3 configurations
valid_results = [r for r in results if r.get("success", True)]
valid_results.sort(key=lambda x: x["profit_ratio"], reverse=True)
print(f"\nTop 3 Configurations:")
for i, result in enumerate(valid_results[:3]):
print(f" {i+1}. Profit: {result['profit_ratio']*100:.2f}% | "
f"Timeframe: {result['strategy_params']['timeframe']} | "
f"Entry Prob: {result['strategy_params']['entry_probability']} | "
f"Stop Loss: {result['trader_params']['stop_loss_pct']*100:.1f}%")
# Save comprehensive results
backtester.save_comprehensive_results(results, "example_parameter_optimization", summary)
# Cleanup
if os.path.exists(f"data/{data_file}"):
os.remove(f"data/{data_file}")
return results, summary
def example_custom_analysis():
"""Example 4: Custom analysis with detailed result examination."""
print("\n" + "="*60)
print("EXAMPLE 4: Custom Analysis")
print("="*60)
# Create sample data with more volatility for interesting results
data = create_sample_data(days=14) # 2 weeks
# Save data
storage = Storage()
data_file = "sample_data_analysis.csv"
storage.save_data(data, data_file)
# Configure backtest
config = BacktestConfig(
data_file=data_file,
start_date=data.index[0].strftime("%Y-%m-%d"),
end_date=data.index[-1].strftime("%Y-%m-%d"),
initial_usd=25000, # Larger starting capital
stop_loss_pct=0.025,
take_profit_pct=0.04
)
# Create strategy with specific parameters for analysis
strategy = IncRandomStrategy(params={
"timeframe": "30min",
"entry_probability": 0.1,
"exit_probability": 0.15,
"random_seed": 42
})
# Run backtest
backtester = IncBacktester(config, storage)
results = backtester.run_single_strategy(strategy)
# Detailed analysis
print(f"\nDetailed Analysis:")
print(f" Strategy: {results['strategy_name']}")
print(f" Timeframe: {results['strategy_params']['timeframe']}")
print(f" Data Period: {config.start_date} to {config.end_date}")
print(f" Data Points: {results['data_points']:,}")
print(f" Processing Time: {results['backtest_duration_seconds']:.2f}s")
print(f"\nPerformance Metrics:")
print(f" Initial Capital: ${results['initial_usd']:,.2f}")
print(f" Final Balance: ${results['final_usd']:,.2f}")
print(f" Total Return: {results['profit_ratio']*100:.2f}%")
print(f" Total Trades: {results['n_trades']}")
if results['n_trades'] > 0:
print(f" Win Rate: {results['win_rate']*100:.1f}%")
print(f" Average Trade: ${results['avg_trade']:.2f}")
print(f" Max Drawdown: {results['max_drawdown']*100:.2f}%")
print(f" Total Fees: ${results['total_fees_usd']:.2f}")
# Calculate additional metrics
days_traded = (pd.to_datetime(config.end_date) - pd.to_datetime(config.start_date)).days
annualized_return = (1 + results['profit_ratio']) ** (365 / days_traded) - 1
print(f" Annualized Return: {annualized_return*100:.2f}%")
# Risk metrics
if results['max_drawdown'] > 0:
calmar_ratio = annualized_return / results['max_drawdown']
print(f" Calmar Ratio: {calmar_ratio:.2f}")
# Save comprehensive results with custom analysis
backtester.save_comprehensive_results([results], "example_custom_analysis")
# Cleanup
if os.path.exists(f"data/{data_file}"):
os.remove(f"data/{data_file}")
return results
def main():
"""Run all examples."""
print("Incremental Backtester Examples")
print("="*60)
print("This script demonstrates various features of the IncBacktester:")
print("1. Single strategy backtesting")
print("2. Multiple strategy comparison")
print("3. Parameter optimization with multiprocessing")
print("4. Custom analysis and metrics")
print("5. Comprehensive result saving and action logging")
# Ensure results directory exists
ensure_results_directory()
try:
# Run all examples
single_results = example_single_strategy()
multiple_results, multiple_summary = example_multiple_strategies()
optimization_results, optimization_summary = example_parameter_optimization()
analysis_results = example_custom_analysis()
print("\n" + "="*60)
print("ALL EXAMPLES COMPLETED SUCCESSFULLY!")
print("="*60)
print("\n📊 Comprehensive results have been saved to the 'results' directory.")
print("Each example generated multiple files:")
print(" 📋 Summary JSON with session info and statistics")
print(" 📈 Detailed CSV with all backtest results")
print(" 📝 Action log JSON with all operations performed")
print(" 📁 Individual strategy JSON files with trades and details")
print(" 🗂️ Master index JSON for easy navigation")
print(f"\n🎯 Key Insights:")
print(f" • Single strategy achieved {single_results['profit_ratio']*100:.2f}% return")
print(f" • Multiple strategies: best {multiple_summary['profit_ratio']['max']*100:.2f}%, worst {multiple_summary['profit_ratio']['min']*100:.2f}%")
print(f" • Optimization tested {optimization_summary['total_runs']} combinations")
print(f" • Custom analysis provided detailed risk metrics")
print(f"\n🔧 System Performance:")
print(f" • Used SystemUtils for optimal CPU core utilization")
print(f" • All actions logged for reproducibility")
print(f" • Results saved in multiple formats for analysis")
print(f"\n✅ The incremental backtester is ready for production use!")
except Exception as e:
logger.error(f"Example failed: {e}")
print(f"\nError: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
main()

View File

@@ -1,736 +0,0 @@
"""
Incremental Backtester for testing incremental strategies.
This module provides the IncBacktester class that orchestrates multiple IncTraders
for parallel testing, handles data loading and feeding, and supports multiprocessing
for parameter optimization.
"""
import pandas as pd
import numpy as np
from typing import Dict, List, Optional, Any, Callable, Union, Tuple
import logging
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
from itertools import product
import multiprocessing as mp
from dataclasses import dataclass
import json
from datetime import datetime
from .inc_trader import IncTrader
from .base import IncStrategyBase
from ..utils.storage import Storage
from ..utils.system import SystemUtils
logger = logging.getLogger(__name__)
def _worker_function(args: Tuple[type, Dict, Dict, 'BacktestConfig', str]) -> Dict[str, Any]:
"""
Worker function for multiprocessing parameter optimization.
This function must be at module level to be picklable for multiprocessing.
Args:
args: Tuple containing (strategy_class, strategy_params, trader_params, config, data_file)
Returns:
Dict containing backtest results
"""
try:
strategy_class, strategy_params, trader_params, config, data_file = args
# Create new storage and backtester instance for this worker
storage = Storage()
worker_backtester = IncBacktester(config, storage)
# Create strategy instance
strategy = strategy_class(params=strategy_params)
# Run backtest
result = worker_backtester.run_single_strategy(strategy, trader_params)
result["success"] = True
return result
except Exception as e:
logger.error(f"Worker error for {strategy_params}, {trader_params}: {e}")
return {
"strategy_params": strategy_params,
"trader_params": trader_params,
"error": str(e),
"success": False
}
@dataclass
class BacktestConfig:
"""Configuration for backtesting runs."""
data_file: str
start_date: str
end_date: str
initial_usd: float = 10000
timeframe: str = "1min"
# Trader parameters
stop_loss_pct: float = 0.0
take_profit_pct: float = 0.0
# Performance settings
max_workers: Optional[int] = None
chunk_size: int = 1000
class IncBacktester:
"""
Incremental backtester for testing incremental strategies.
This class orchestrates multiple IncTraders for parallel testing:
- Loads data using the existing Storage class
- Creates multiple IncTrader instances with different parameters
- Feeds data sequentially to all traders
- Collects and aggregates results
- Supports multiprocessing for parallel execution
- Uses SystemUtils for optimal worker count determination
The backtester can run multiple strategies simultaneously or test
parameter combinations across multiple CPU cores.
Example:
# Single strategy backtest
config = BacktestConfig(
data_file="btc_1min_2023.csv",
start_date="2023-01-01",
end_date="2023-12-31",
initial_usd=10000
)
strategy = IncRandomStrategy(params={"timeframe": "15min"})
backtester = IncBacktester(config)
results = backtester.run_single_strategy(strategy)
# Multiple strategies
strategies = [strategy1, strategy2, strategy3]
results = backtester.run_multiple_strategies(strategies)
# Parameter optimization
param_grid = {
"timeframe": ["5min", "15min", "30min"],
"stop_loss_pct": [0.01, 0.02, 0.03]
}
results = backtester.optimize_parameters(strategy_class, param_grid)
"""
def __init__(self, config: BacktestConfig, storage: Optional[Storage] = None):
"""
Initialize the incremental backtester.
Args:
config: Backtesting configuration
storage: Storage instance for data loading (creates new if None)
"""
self.config = config
self.storage = storage or Storage()
self.system_utils = SystemUtils(logging=logger)
self.data = None
self.results_cache = {}
# Track all actions performed during backtesting
self.action_log = []
self.session_start_time = datetime.now()
logger.info(f"IncBacktester initialized: {config.data_file}, "
f"{config.start_date} to {config.end_date}")
self._log_action("backtester_initialized", {
"config": config.__dict__,
"session_start": self.session_start_time.isoformat()
})
def _log_action(self, action_type: str, details: Dict[str, Any]) -> None:
"""Log an action performed during backtesting."""
self.action_log.append({
"timestamp": datetime.now().isoformat(),
"action_type": action_type,
"details": details
})
def load_data(self) -> pd.DataFrame:
"""
Load and prepare data for backtesting.
Returns:
pd.DataFrame: Loaded OHLCV data with DatetimeIndex
"""
if self.data is None:
logger.info(f"Loading data from {self.config.data_file}...")
start_time = time.time()
self.data = self.storage.load_data(
self.config.data_file,
self.config.start_date,
self.config.end_date
)
load_time = time.time() - start_time
logger.info(f"Data loaded: {len(self.data)} rows in {load_time:.2f}s")
# Validate data
if self.data.empty:
raise ValueError(f"No data loaded for the specified date range")
required_columns = ['open', 'high', 'low', 'close', 'volume']
missing_columns = [col for col in required_columns if col not in self.data.columns]
if missing_columns:
raise ValueError(f"Missing required columns: {missing_columns}")
self._log_action("data_loaded", {
"file": self.config.data_file,
"rows": len(self.data),
"load_time_seconds": load_time,
"date_range": f"{self.config.start_date} to {self.config.end_date}",
"columns": list(self.data.columns)
})
return self.data
def run_single_strategy(self, strategy: IncStrategyBase,
trader_params: Optional[Dict] = None) -> Dict[str, Any]:
"""
Run backtest for a single strategy.
Args:
strategy: Incremental strategy instance
trader_params: Additional trader parameters
Returns:
Dict containing backtest results
"""
data = self.load_data()
# Merge trader parameters
final_trader_params = {
"stop_loss_pct": self.config.stop_loss_pct,
"take_profit_pct": self.config.take_profit_pct
}
if trader_params:
final_trader_params.update(trader_params)
# Create trader
trader = IncTrader(
strategy=strategy,
initial_usd=self.config.initial_usd,
params=final_trader_params
)
# Run backtest
logger.info(f"Starting backtest for {strategy.name}...")
start_time = time.time()
self._log_action("single_strategy_backtest_started", {
"strategy_name": strategy.name,
"strategy_params": strategy.params,
"trader_params": final_trader_params,
"data_points": len(data)
})
for timestamp, row in data.iterrows():
ohlcv_data = {
'open': row['open'],
'high': row['high'],
'low': row['low'],
'close': row['close'],
'volume': row['volume']
}
trader.process_data_point(timestamp, ohlcv_data)
# Finalize and get results
trader.finalize()
results = trader.get_results()
backtest_time = time.time() - start_time
results["backtest_duration_seconds"] = backtest_time
results["data_points"] = len(data)
results["config"] = self.config.__dict__
logger.info(f"Backtest completed for {strategy.name} in {backtest_time:.2f}s: "
f"${results['final_usd']:.2f} ({results['profit_ratio']*100:.2f}%), "
f"{results['n_trades']} trades")
self._log_action("single_strategy_backtest_completed", {
"strategy_name": strategy.name,
"backtest_duration_seconds": backtest_time,
"final_usd": results['final_usd'],
"profit_ratio": results['profit_ratio'],
"n_trades": results['n_trades'],
"win_rate": results['win_rate']
})
return results
def run_multiple_strategies(self, strategies: List[IncStrategyBase],
trader_params: Optional[Dict] = None) -> List[Dict[str, Any]]:
"""
Run backtest for multiple strategies simultaneously.
Args:
strategies: List of incremental strategy instances
trader_params: Additional trader parameters
Returns:
List of backtest results for each strategy
"""
self._log_action("multiple_strategies_backtest_started", {
"strategy_count": len(strategies),
"strategy_names": [s.name for s in strategies]
})
results = []
for strategy in strategies:
try:
result = self.run_single_strategy(strategy, trader_params)
results.append(result)
except Exception as e:
logger.error(f"Error running strategy {strategy.name}: {e}")
# Add error result
error_result = {
"strategy_name": strategy.name,
"error": str(e),
"success": False
}
results.append(error_result)
self._log_action("strategy_error", {
"strategy_name": strategy.name,
"error": str(e)
})
self._log_action("multiple_strategies_backtest_completed", {
"total_strategies": len(strategies),
"successful_strategies": len([r for r in results if r.get("success", True)]),
"failed_strategies": len([r for r in results if not r.get("success", True)])
})
return results
def optimize_parameters(self, strategy_class: type, param_grid: Dict[str, List],
trader_param_grid: Optional[Dict[str, List]] = None,
max_workers: Optional[int] = None) -> List[Dict[str, Any]]:
"""
Optimize strategy parameters using grid search with multiprocessing.
Args:
strategy_class: Strategy class to instantiate
param_grid: Grid of strategy parameters to test
trader_param_grid: Grid of trader parameters to test
max_workers: Maximum number of worker processes (uses SystemUtils if None)
Returns:
List of results for each parameter combination
"""
# Generate parameter combinations
strategy_combinations = list(self._generate_param_combinations(param_grid))
trader_combinations = list(self._generate_param_combinations(trader_param_grid or {}))
# If no trader param grid, use default
if not trader_combinations:
trader_combinations = [{}]
# Create all combinations
all_combinations = []
for strategy_params in strategy_combinations:
for trader_params in trader_combinations:
all_combinations.append((strategy_params, trader_params))
logger.info(f"Starting parameter optimization: {len(all_combinations)} combinations")
# Determine number of workers using SystemUtils
if max_workers is None:
max_workers = self.system_utils.get_optimal_workers()
else:
max_workers = min(max_workers, len(all_combinations))
self._log_action("parameter_optimization_started", {
"strategy_class": strategy_class.__name__,
"total_combinations": len(all_combinations),
"max_workers": max_workers,
"strategy_param_grid": param_grid,
"trader_param_grid": trader_param_grid or {}
})
# Run optimization
if max_workers == 1 or len(all_combinations) == 1:
# Single-threaded execution
results = []
for strategy_params, trader_params in all_combinations:
result = self._run_single_combination(strategy_class, strategy_params, trader_params)
results.append(result)
else:
# Multi-threaded execution
results = self._run_parallel_optimization(
strategy_class, all_combinations, max_workers
)
# Sort results by profit ratio
valid_results = [r for r in results if r.get("success", True)]
valid_results.sort(key=lambda x: x.get("profit_ratio", -float('inf')), reverse=True)
logger.info(f"Parameter optimization completed: {len(valid_results)} successful runs")
self._log_action("parameter_optimization_completed", {
"total_runs": len(results),
"successful_runs": len(valid_results),
"failed_runs": len(results) - len(valid_results),
"best_profit_ratio": valid_results[0]["profit_ratio"] if valid_results else None,
"worst_profit_ratio": valid_results[-1]["profit_ratio"] if valid_results else None
})
return results
def _generate_param_combinations(self, param_grid: Dict[str, List]) -> List[Dict]:
"""Generate all parameter combinations from grid."""
if not param_grid:
return [{}]
keys = list(param_grid.keys())
values = list(param_grid.values())
combinations = []
for combination in product(*values):
param_dict = dict(zip(keys, combination))
combinations.append(param_dict)
return combinations
def _run_single_combination(self, strategy_class: type, strategy_params: Dict,
trader_params: Dict) -> Dict[str, Any]:
"""Run backtest for a single parameter combination."""
try:
# Create strategy instance
strategy = strategy_class(params=strategy_params)
# Run backtest
result = self.run_single_strategy(strategy, trader_params)
result["success"] = True
return result
except Exception as e:
logger.error(f"Error in parameter combination {strategy_params}, {trader_params}: {e}")
return {
"strategy_params": strategy_params,
"trader_params": trader_params,
"error": str(e),
"success": False
}
def _run_parallel_optimization(self, strategy_class: type, combinations: List,
max_workers: int) -> List[Dict[str, Any]]:
"""Run parameter optimization in parallel."""
results = []
# Prepare arguments for worker function
worker_args = []
for strategy_params, trader_params in combinations:
args = (strategy_class, strategy_params, trader_params, self.config, self.config.data_file)
worker_args.append(args)
# Execute in parallel
with ProcessPoolExecutor(max_workers=max_workers) as executor:
# Submit all jobs
future_to_params = {
executor.submit(_worker_function, args): args[1:3] # strategy_params, trader_params
for args in worker_args
}
# Collect results as they complete
for future in as_completed(future_to_params):
combo = future_to_params[future]
try:
result = future.result()
results.append(result)
if result.get("success", True):
logger.info(f"Completed: {combo[0]} -> "
f"${result.get('final_usd', 0):.2f} "
f"({result.get('profit_ratio', 0)*100:.2f}%)")
except Exception as e:
logger.error(f"Worker error for {combo}: {e}")
results.append({
"strategy_params": combo[0],
"trader_params": combo[1],
"error": str(e),
"success": False
})
return results
def get_summary_statistics(self, results: List[Dict[str, Any]]) -> Dict[str, Any]:
"""
Calculate summary statistics across multiple backtest results.
Args:
results: List of backtest results
Returns:
Dict containing summary statistics
"""
valid_results = [r for r in results if r.get("success", True)]
if not valid_results:
return {
"total_runs": len(results),
"successful_runs": 0,
"failed_runs": len(results),
"error": "No valid results to summarize"
}
# Extract metrics
profit_ratios = [r["profit_ratio"] for r in valid_results]
final_balances = [r["final_usd"] for r in valid_results]
n_trades_list = [r["n_trades"] for r in valid_results]
win_rates = [r["win_rate"] for r in valid_results]
max_drawdowns = [r["max_drawdown"] for r in valid_results]
summary = {
"total_runs": len(results),
"successful_runs": len(valid_results),
"failed_runs": len(results) - len(valid_results),
# Profit statistics
"profit_ratio": {
"mean": np.mean(profit_ratios),
"std": np.std(profit_ratios),
"min": np.min(profit_ratios),
"max": np.max(profit_ratios),
"median": np.median(profit_ratios)
},
# Balance statistics
"final_usd": {
"mean": np.mean(final_balances),
"std": np.std(final_balances),
"min": np.min(final_balances),
"max": np.max(final_balances),
"median": np.median(final_balances)
},
# Trading statistics
"n_trades": {
"mean": np.mean(n_trades_list),
"std": np.std(n_trades_list),
"min": np.min(n_trades_list),
"max": np.max(n_trades_list),
"median": np.median(n_trades_list)
},
# Performance statistics
"win_rate": {
"mean": np.mean(win_rates),
"std": np.std(win_rates),
"min": np.min(win_rates),
"max": np.max(win_rates),
"median": np.median(win_rates)
},
"max_drawdown": {
"mean": np.mean(max_drawdowns),
"std": np.std(max_drawdowns),
"min": np.min(max_drawdowns),
"max": np.max(max_drawdowns),
"median": np.median(max_drawdowns)
},
# Best performing run
"best_run": max(valid_results, key=lambda x: x["profit_ratio"]),
"worst_run": min(valid_results, key=lambda x: x["profit_ratio"])
}
return summary
def save_comprehensive_results(self, results: List[Dict[str, Any]],
base_filename: str,
summary: Optional[Dict[str, Any]] = None) -> None:
"""
Save comprehensive backtest results including summary, individual results, and action log.
Args:
results: List of backtest results
base_filename: Base filename (without extension)
summary: Optional summary statistics
"""
try:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
# 1. Save summary report
if summary is None:
summary = self.get_summary_statistics(results)
summary_data = {
"session_info": {
"timestamp": timestamp,
"session_start": self.session_start_time.isoformat(),
"session_duration_seconds": (datetime.now() - self.session_start_time).total_seconds(),
"config": self.config.__dict__
},
"summary_statistics": summary,
"action_log_summary": {
"total_actions": len(self.action_log),
"action_types": list(set(action["action_type"] for action in self.action_log))
}
}
summary_filename = f"{base_filename}_summary_{timestamp}.json"
with open(f"results/{summary_filename}", 'w') as f:
json.dump(summary_data, f, indent=2, default=str)
logger.info(f"Summary saved to results/{summary_filename}")
# 2. Save detailed results CSV
self.save_results(results, f"{base_filename}_detailed_{timestamp}.csv")
# 3. Save individual strategy results
valid_results = [r for r in results if r.get("success", True)]
for i, result in enumerate(valid_results):
strategy_filename = f"{base_filename}_strategy_{i+1}_{result['strategy_name']}_{timestamp}.json"
# Include trades and detailed info
strategy_data = {
"strategy_info": {
"name": result['strategy_name'],
"params": result.get('strategy_params', {}),
"trader_params": result.get('trader_params', {})
},
"performance": {
"initial_usd": result['initial_usd'],
"final_usd": result['final_usd'],
"profit_ratio": result['profit_ratio'],
"n_trades": result['n_trades'],
"win_rate": result['win_rate'],
"max_drawdown": result['max_drawdown'],
"avg_trade": result['avg_trade'],
"total_fees_usd": result['total_fees_usd']
},
"execution": {
"backtest_duration_seconds": result.get('backtest_duration_seconds', 0),
"data_points_processed": result.get('data_points_processed', 0),
"warmup_complete": result.get('warmup_complete', False)
},
"trades": result.get('trades', [])
}
with open(f"results/{strategy_filename}", 'w') as f:
json.dump(strategy_data, f, indent=2, default=str)
logger.info(f"Strategy {i+1} details saved to results/{strategy_filename}")
# 4. Save complete action log
action_log_filename = f"{base_filename}_actions_{timestamp}.json"
action_log_data = {
"session_info": {
"timestamp": timestamp,
"session_start": self.session_start_time.isoformat(),
"total_actions": len(self.action_log)
},
"actions": self.action_log
}
with open(f"results/{action_log_filename}", 'w') as f:
json.dump(action_log_data, f, indent=2, default=str)
logger.info(f"Action log saved to results/{action_log_filename}")
# 5. Create a master index file
index_filename = f"{base_filename}_index_{timestamp}.json"
index_data = {
"session_info": {
"timestamp": timestamp,
"base_filename": base_filename,
"total_strategies": len(valid_results),
"session_duration_seconds": (datetime.now() - self.session_start_time).total_seconds()
},
"files": {
"summary": summary_filename,
"detailed_csv": f"{base_filename}_detailed_{timestamp}.csv",
"action_log": action_log_filename,
"individual_strategies": [
f"{base_filename}_strategy_{i+1}_{result['strategy_name']}_{timestamp}.json"
for i, result in enumerate(valid_results)
]
},
"quick_stats": {
"best_profit": summary.get("profit_ratio", {}).get("max", 0) if summary.get("profit_ratio") else 0,
"worst_profit": summary.get("profit_ratio", {}).get("min", 0) if summary.get("profit_ratio") else 0,
"avg_profit": summary.get("profit_ratio", {}).get("mean", 0) if summary.get("profit_ratio") else 0,
"total_successful_runs": summary.get("successful_runs", 0),
"total_failed_runs": summary.get("failed_runs", 0)
}
}
with open(f"results/{index_filename}", 'w') as f:
json.dump(index_data, f, indent=2, default=str)
logger.info(f"Master index saved to results/{index_filename}")
print(f"\n📊 Comprehensive results saved:")
print(f" 📋 Summary: results/{summary_filename}")
print(f" 📈 Detailed CSV: results/{base_filename}_detailed_{timestamp}.csv")
print(f" 📝 Action Log: results/{action_log_filename}")
print(f" 📁 Individual Strategies: {len(valid_results)} files")
print(f" 🗂️ Master Index: results/{index_filename}")
except Exception as e:
logger.error(f"Error saving comprehensive results: {e}")
raise
def save_results(self, results: List[Dict[str, Any]], filename: str) -> None:
"""
Save backtest results to file.
Args:
results: List of backtest results
filename: Output filename
"""
try:
# Convert results to DataFrame for easy saving
df_data = []
for result in results:
if result.get("success", True):
row = {
"strategy_name": result.get("strategy_name", ""),
"profit_ratio": result.get("profit_ratio", 0),
"final_usd": result.get("final_usd", 0),
"n_trades": result.get("n_trades", 0),
"win_rate": result.get("win_rate", 0),
"max_drawdown": result.get("max_drawdown", 0),
"avg_trade": result.get("avg_trade", 0),
"total_fees_usd": result.get("total_fees_usd", 0),
"backtest_duration_seconds": result.get("backtest_duration_seconds", 0),
"data_points_processed": result.get("data_points_processed", 0)
}
# Add strategy parameters
strategy_params = result.get("strategy_params", {})
for key, value in strategy_params.items():
row[f"strategy_{key}"] = value
# Add trader parameters
trader_params = result.get("trader_params", {})
for key, value in trader_params.items():
row[f"trader_{key}"] = value
df_data.append(row)
# Save to CSV
df = pd.DataFrame(df_data)
self.storage.save_data(df, filename)
logger.info(f"Results saved to {filename}: {len(df_data)} rows")
except Exception as e:
logger.error(f"Error saving results to {filename}: {e}")
raise
def __repr__(self) -> str:
"""String representation of the backtester."""
return (f"IncBacktester(data_file={self.config.data_file}, "
f"date_range={self.config.start_date} to {self.config.end_date}, "
f"initial_usd=${self.config.initial_usd})")

View File

@@ -1,344 +0,0 @@
"""
Incremental Trader for backtesting incremental strategies.
This module provides the IncTrader class that manages a single incremental strategy
during backtesting, handling position state, trade execution, and performance tracking.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
import logging
from dataclasses import dataclass
from .base import IncStrategyBase, IncStrategySignal
from ..market_fees import MarketFees
logger = logging.getLogger(__name__)
@dataclass
class TradeRecord:
"""Record of a completed trade."""
entry_time: pd.Timestamp
exit_time: pd.Timestamp
entry_price: float
exit_price: float
entry_fee: float
exit_fee: float
profit_pct: float
exit_reason: str
strategy_name: str
class IncTrader:
"""
Incremental trader that manages a single strategy during backtesting.
This class handles:
- Strategy initialization and data feeding
- Position management (USD/coin balance)
- Trade execution based on strategy signals
- Performance tracking and metrics collection
- Fee calculation and trade logging
The trader processes data points sequentially, feeding them to the strategy
and executing trades based on the generated signals.
Example:
strategy = IncRandomStrategy(params={"timeframe": "15min"})
trader = IncTrader(
strategy=strategy,
initial_usd=10000,
params={"stop_loss_pct": 0.02}
)
# Process data sequentially
for timestamp, ohlcv_data in data_stream:
trader.process_data_point(timestamp, ohlcv_data)
# Get results
results = trader.get_results()
"""
def __init__(self, strategy: IncStrategyBase, initial_usd: float = 10000,
params: Optional[Dict] = None):
"""
Initialize the incremental trader.
Args:
strategy: Incremental strategy instance
initial_usd: Initial USD balance
params: Trader parameters (stop_loss_pct, take_profit_pct, etc.)
"""
self.strategy = strategy
self.initial_usd = initial_usd
self.params = params or {}
# Position state
self.usd = initial_usd
self.coin = 0.0
self.position = 0 # 0 = no position, 1 = long position
self.entry_price = 0.0
self.entry_time = None
# Performance tracking
self.max_balance = initial_usd
self.drawdowns = []
self.trade_records = []
self.current_timestamp = None
self.current_price = None
# Strategy state
self.data_points_processed = 0
self.warmup_complete = False
# Parameters
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.0)
self.take_profit_pct = self.params.get("take_profit_pct", 0.0)
logger.info(f"IncTrader initialized: strategy={strategy.name}, "
f"initial_usd=${initial_usd}, stop_loss={self.stop_loss_pct*100:.1f}%")
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> None:
"""
Process a single data point through the strategy and handle trading logic.
Args:
timestamp: Data point timestamp
ohlcv_data: OHLCV data dictionary with keys: open, high, low, close, volume
"""
self.current_timestamp = timestamp
self.current_price = ohlcv_data['close']
self.data_points_processed += 1
try:
# Feed data to strategy (handles timeframe aggregation internally)
result = self.strategy.update_minute_data(timestamp, ohlcv_data)
# Check if strategy is warmed up
if not self.warmup_complete and self.strategy.is_warmed_up:
self.warmup_complete = True
logger.info(f"Strategy {self.strategy.name} warmed up after "
f"{self.data_points_processed} data points")
# Only process signals if strategy is warmed up and we have a complete timeframe bar
if self.warmup_complete and result is not None:
self._process_trading_logic()
# Update performance tracking
self._update_performance_metrics()
except Exception as e:
logger.error(f"Error processing data point at {timestamp}: {e}")
raise
def _process_trading_logic(self) -> None:
"""Process trading logic based on current position and strategy signals."""
if self.position == 0:
# No position - check for entry signals
self._check_entry_signals()
else:
# In position - check for exit signals
self._check_exit_signals()
def _check_entry_signals(self) -> None:
"""Check for entry signals when not in position."""
try:
entry_signal = self.strategy.get_entry_signal()
if entry_signal.signal_type == "ENTRY" and entry_signal.confidence > 0:
self._execute_entry(entry_signal)
except Exception as e:
logger.error(f"Error checking entry signals: {e}")
def _check_exit_signals(self) -> None:
"""Check for exit signals when in position."""
try:
# Check strategy exit signals
exit_signal = self.strategy.get_exit_signal()
if exit_signal.signal_type == "EXIT" and exit_signal.confidence > 0:
exit_reason = exit_signal.metadata.get("type", "STRATEGY_EXIT")
self._execute_exit(exit_reason, exit_signal.price)
return
# Check stop loss
if self.stop_loss_pct > 0:
stop_loss_price = self.entry_price * (1 - self.stop_loss_pct)
if self.current_price <= stop_loss_price:
self._execute_exit("STOP_LOSS", self.current_price)
return
# Check take profit
if self.take_profit_pct > 0:
take_profit_price = self.entry_price * (1 + self.take_profit_pct)
if self.current_price >= take_profit_price:
self._execute_exit("TAKE_PROFIT", self.current_price)
return
except Exception as e:
logger.error(f"Error checking exit signals: {e}")
def _execute_entry(self, signal: IncStrategySignal) -> None:
"""Execute entry trade."""
entry_price = signal.price if signal.price else self.current_price
entry_fee = MarketFees.calculate_okx_taker_maker_fee(self.usd, is_maker=False)
usd_after_fee = self.usd - entry_fee
self.coin = usd_after_fee / entry_price
self.entry_price = entry_price
self.entry_time = self.current_timestamp
self.usd = 0.0
self.position = 1
logger.info(f"ENTRY: {self.strategy.name} at ${entry_price:.2f}, "
f"confidence={signal.confidence:.2f}, fee=${entry_fee:.2f}")
def _execute_exit(self, exit_reason: str, exit_price: Optional[float] = None) -> None:
"""Execute exit trade."""
exit_price = exit_price if exit_price else self.current_price
usd_gross = self.coin * exit_price
exit_fee = MarketFees.calculate_okx_taker_maker_fee(usd_gross, is_maker=False)
self.usd = usd_gross - exit_fee
# Calculate profit
profit_pct = (exit_price - self.entry_price) / self.entry_price
# Record trade
trade_record = TradeRecord(
entry_time=self.entry_time,
exit_time=self.current_timestamp,
entry_price=self.entry_price,
exit_price=exit_price,
entry_fee=MarketFees.calculate_okx_taker_maker_fee(
self.coin * self.entry_price, is_maker=False
),
exit_fee=exit_fee,
profit_pct=profit_pct,
exit_reason=exit_reason,
strategy_name=self.strategy.name
)
self.trade_records.append(trade_record)
# Reset position
self.coin = 0.0
self.position = 0
self.entry_price = 0.0
self.entry_time = None
logger.info(f"EXIT: {self.strategy.name} at ${exit_price:.2f}, "
f"reason={exit_reason}, profit={profit_pct*100:.2f}%, fee=${exit_fee:.2f}")
def _update_performance_metrics(self) -> None:
"""Update performance tracking metrics."""
# Calculate current balance
if self.position == 0:
current_balance = self.usd
else:
current_balance = self.coin * self.current_price
# Update max balance and drawdown
if current_balance > self.max_balance:
self.max_balance = current_balance
drawdown = (self.max_balance - current_balance) / self.max_balance
self.drawdowns.append(drawdown)
def finalize(self) -> None:
"""Finalize trading session (close any open positions)."""
if self.position == 1:
self._execute_exit("EOD", self.current_price)
logger.info(f"Closed final position for {self.strategy.name} at EOD")
def get_results(self) -> Dict[str, Any]:
"""
Get comprehensive trading results.
Returns:
Dict containing performance metrics, trade records, and statistics
"""
final_balance = self.usd
n_trades = len(self.trade_records)
# Calculate statistics
if n_trades > 0:
profits = [trade.profit_pct for trade in self.trade_records]
wins = [p for p in profits if p > 0]
win_rate = len(wins) / n_trades
avg_trade = np.mean(profits)
total_fees = sum(trade.entry_fee + trade.exit_fee for trade in self.trade_records)
else:
win_rate = 0.0
avg_trade = 0.0
total_fees = 0.0
max_drawdown = max(self.drawdowns) if self.drawdowns else 0.0
profit_ratio = (final_balance - self.initial_usd) / self.initial_usd
# Convert trade records to dictionaries
trades = []
for trade in self.trade_records:
trades.append({
'entry_time': trade.entry_time,
'exit_time': trade.exit_time,
'entry': trade.entry_price,
'exit': trade.exit_price,
'profit_pct': trade.profit_pct,
'type': trade.exit_reason,
'fee_usd': trade.entry_fee + trade.exit_fee,
'strategy': trade.strategy_name
})
results = {
"strategy_name": self.strategy.name,
"strategy_params": self.strategy.params,
"trader_params": self.params,
"initial_usd": self.initial_usd,
"final_usd": final_balance,
"profit_ratio": profit_ratio,
"n_trades": n_trades,
"win_rate": win_rate,
"max_drawdown": max_drawdown,
"avg_trade": avg_trade,
"total_fees_usd": total_fees,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"trades": trades
}
# Add first and last trade info if available
if n_trades > 0:
results["first_trade"] = {
"entry_time": self.trade_records[0].entry_time,
"entry": self.trade_records[0].entry_price
}
results["last_trade"] = {
"exit_time": self.trade_records[-1].exit_time,
"exit": self.trade_records[-1].exit_price
}
return results
def get_current_state(self) -> Dict[str, Any]:
"""Get current trader state for debugging."""
return {
"strategy": self.strategy.name,
"position": self.position,
"usd": self.usd,
"coin": self.coin,
"current_price": self.current_price,
"entry_price": self.entry_price,
"data_points_processed": self.data_points_processed,
"warmup_complete": self.warmup_complete,
"n_trades": len(self.trade_records),
"strategy_state": self.strategy.get_current_state_summary()
}
def __repr__(self) -> str:
"""String representation of the trader."""
return (f"IncTrader(strategy={self.strategy.name}, "
f"position={self.position}, usd=${self.usd:.2f}, "
f"trades={len(self.trade_records)})")

View File

@@ -1,36 +0,0 @@
"""
Incremental Indicator States Module
This module contains indicator state classes that maintain calculation state
for incremental processing of technical indicators.
All indicator states implement the IndicatorState interface and provide:
- Incremental updates with new data points
- Constant memory usage regardless of data history
- Identical results to traditional batch calculations
- Warm-up detection for reliable indicator values
Classes:
IndicatorState: Abstract base class for all indicator states
MovingAverageState: Incremental moving average calculation
RSIState: Incremental RSI calculation
ATRState: Incremental Average True Range calculation
SupertrendState: Incremental Supertrend calculation
BollingerBandsState: Incremental Bollinger Bands calculation
"""
from .base import IndicatorState
from .moving_average import MovingAverageState
from .rsi import RSIState
from .atr import ATRState
from .supertrend import SupertrendState
from .bollinger_bands import BollingerBandsState
__all__ = [
'IndicatorState',
'MovingAverageState',
'RSIState',
'ATRState',
'SupertrendState',
'BollingerBandsState'
]

View File

@@ -1,242 +0,0 @@
"""
Average True Range (ATR) Indicator State
This module implements incremental ATR calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. ATR is used by
Supertrend and other volatility-based indicators.
"""
from typing import Dict, Union, Optional
from .base import OHLCIndicatorState
from .moving_average import ExponentialMovingAverageState
class ATRState(OHLCIndicatorState):
"""
Incremental Average True Range calculation state.
ATR measures market volatility by calculating the average of true ranges over
a specified period. True Range is the maximum of:
1. Current High - Current Low
2. |Current High - Previous Close|
3. |Current Low - Previous Close|
This implementation uses exponential moving average for smoothing, which is
more responsive than simple moving average and requires less memory.
Attributes:
period (int): The ATR period
ema_state (ExponentialMovingAverageState): EMA state for smoothing true ranges
previous_close (float): Previous period's close price
Example:
atr = ATRState(period=14)
# Add OHLC data incrementally
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
atr_value = atr.update(ohlc) # Returns current ATR value
# Check if warmed up
if atr.is_warmed_up():
current_atr = atr.get_current_value()
"""
def __init__(self, period: int = 14):
"""
Initialize ATR state.
Args:
period: Number of periods for ATR calculation (default: 14)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.ema_state = ExponentialMovingAverageState(period)
self.previous_close = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> float:
"""
Update ATR with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Current ATR value
Raises:
ValueError: If OHLC data is invalid
TypeError: If ohlc_data is not a dictionary
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Calculate True Range
if self.previous_close is None:
# First period - True Range is just High - Low
true_range = high - low
else:
# True Range is the maximum of:
# 1. Current High - Current Low
# 2. |Current High - Previous Close|
# 3. |Current Low - Previous Close|
tr1 = high - low
tr2 = abs(high - self.previous_close)
tr3 = abs(low - self.previous_close)
true_range = max(tr1, tr2, tr3)
# Update EMA with the true range
atr_value = self.ema_state.update(true_range)
# Store current close as previous close for next calculation
self.previous_close = close
self.values_received += 1
# Store current ATR value
self._current_values = {'atr': atr_value}
return atr_value
def is_warmed_up(self) -> bool:
"""
Check if ATR has enough data for reliable values.
Returns:
True if EMA state is warmed up (has enough true range values)
"""
return self.ema_state.is_warmed_up()
def reset(self) -> None:
"""Reset ATR state to initial conditions."""
self.ema_state.reset()
self.previous_close = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[float]:
"""
Get current ATR value without updating.
Returns:
Current ATR value, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self.ema_state.get_current_value()
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'ema_state': self.ema_state.get_state_summary(),
'current_atr': self.get_current_value()
})
return base_summary
class SimpleATRState(OHLCIndicatorState):
"""
Simple ATR implementation using simple moving average instead of EMA.
This version uses a simple moving average for smoothing true ranges,
which matches some traditional ATR implementations but requires more memory.
"""
def __init__(self, period: int = 14):
"""
Initialize simple ATR state.
Args:
period: Number of periods for ATR calculation (default: 14)
"""
super().__init__(period)
from collections import deque
self.true_ranges = deque(maxlen=period)
self.tr_sum = 0.0
self.previous_close = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> float:
"""
Update simple ATR with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Current ATR value
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Calculate True Range
if self.previous_close is None:
true_range = high - low
else:
tr1 = high - low
tr2 = abs(high - self.previous_close)
tr3 = abs(low - self.previous_close)
true_range = max(tr1, tr2, tr3)
# Update rolling sum
if len(self.true_ranges) == self.period:
self.tr_sum -= self.true_ranges[0] # Remove oldest value
self.true_ranges.append(true_range)
self.tr_sum += true_range
# Calculate ATR as simple moving average
atr_value = self.tr_sum / len(self.true_ranges)
# Store state
self.previous_close = close
self.values_received += 1
self._current_values = {'atr': atr_value}
return atr_value
def is_warmed_up(self) -> bool:
"""Check if simple ATR is warmed up."""
return len(self.true_ranges) >= self.period
def reset(self) -> None:
"""Reset simple ATR state."""
self.true_ranges.clear()
self.tr_sum = 0.0
self.previous_close = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[float]:
"""Get current simple ATR value."""
if not self.is_warmed_up():
return None
return self.tr_sum / len(self.true_ranges)
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'tr_window_size': len(self.true_ranges),
'tr_sum': self.tr_sum,
'current_atr': self.get_current_value()
})
return base_summary

View File

@@ -1,197 +0,0 @@
"""
Base Indicator State Class
This module contains the abstract base class for all incremental indicator states.
All indicator implementations must inherit from IndicatorState and implement
the required methods for incremental calculation.
"""
from abc import ABC, abstractmethod
from typing import Any, Dict, Optional, Union
import numpy as np
class IndicatorState(ABC):
"""
Abstract base class for maintaining indicator calculation state.
This class defines the interface that all incremental indicators must implement.
Indicators maintain their internal state and can be updated incrementally with
new data points, providing constant memory usage and high performance.
Attributes:
period (int): The period/window size for the indicator
values_received (int): Number of values processed so far
is_initialized (bool): Whether the indicator has been initialized
Example:
class MyIndicator(IndicatorState):
def __init__(self, period: int):
super().__init__(period)
self._sum = 0.0
def update(self, new_value: float) -> float:
self._sum += new_value
self.values_received += 1
return self._sum / min(self.values_received, self.period)
"""
def __init__(self, period: int):
"""
Initialize the indicator state.
Args:
period: The period/window size for the indicator calculation
Raises:
ValueError: If period is not a positive integer
"""
if not isinstance(period, int) or period <= 0:
raise ValueError(f"Period must be a positive integer, got {period}")
self.period = period
self.values_received = 0
self.is_initialized = False
@abstractmethod
def update(self, new_value: Union[float, Dict[str, float]]) -> Union[float, Dict[str, float]]:
"""
Update indicator with new value and return current indicator value.
This method processes a new data point and updates the internal state
of the indicator. It returns the current indicator value after the update.
Args:
new_value: New data point (can be single value or OHLCV dict)
Returns:
Current indicator value after update (single value or dict)
Raises:
ValueError: If new_value is invalid or incompatible
"""
pass
@abstractmethod
def is_warmed_up(self) -> bool:
"""
Check whether indicator has enough data for reliable values.
Returns:
True if indicator has received enough data points for reliable calculation
"""
pass
@abstractmethod
def reset(self) -> None:
"""
Reset indicator state to initial conditions.
This method clears all internal state and resets the indicator
as if it was just initialized.
"""
pass
@abstractmethod
def get_current_value(self) -> Union[float, Dict[str, float], None]:
"""
Get the current indicator value without updating.
Returns:
Current indicator value, or None if not warmed up
"""
pass
def get_state_summary(self) -> Dict[str, Any]:
"""
Get summary of current indicator state for debugging.
Returns:
Dictionary containing indicator state information
"""
return {
'indicator_type': self.__class__.__name__,
'period': self.period,
'values_received': self.values_received,
'is_warmed_up': self.is_warmed_up(),
'is_initialized': self.is_initialized,
'current_value': self.get_current_value()
}
def validate_input(self, value: Union[float, Dict[str, float]]) -> None:
"""
Validate input value for the indicator.
Args:
value: Input value to validate
Raises:
ValueError: If value is invalid
TypeError: If value type is incorrect
"""
if isinstance(value, (int, float)):
if not np.isfinite(value):
raise ValueError(f"Input value must be finite, got {value}")
elif isinstance(value, dict):
required_keys = ['open', 'high', 'low', 'close']
for key in required_keys:
if key not in value:
raise ValueError(f"OHLCV dict missing required key: {key}")
if not np.isfinite(value[key]):
raise ValueError(f"OHLCV value for {key} must be finite, got {value[key]}")
# Validate OHLC relationships
if not (value['low'] <= value['open'] <= value['high'] and
value['low'] <= value['close'] <= value['high']):
raise ValueError(f"Invalid OHLC relationships: {value}")
else:
raise TypeError(f"Input value must be float or OHLCV dict, got {type(value)}")
def __repr__(self) -> str:
"""String representation of the indicator state."""
return (f"{self.__class__.__name__}(period={self.period}, "
f"values_received={self.values_received}, "
f"warmed_up={self.is_warmed_up()})")
class SimpleIndicatorState(IndicatorState):
"""
Base class for simple single-value indicators.
This class provides common functionality for indicators that work with
single float values and maintain a simple rolling calculation.
"""
def __init__(self, period: int):
"""Initialize simple indicator state."""
super().__init__(period)
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""Get current indicator value."""
return self._current_value if self.is_warmed_up() else None
def is_warmed_up(self) -> bool:
"""Check if indicator is warmed up."""
return self.values_received >= self.period
class OHLCIndicatorState(IndicatorState):
"""
Base class for OHLC-based indicators.
This class provides common functionality for indicators that work with
OHLC data (Open, High, Low, Close) and may return multiple values.
"""
def __init__(self, period: int):
"""Initialize OHLC indicator state."""
super().__init__(period)
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""Get current indicator values."""
return self._current_values.copy() if self.is_warmed_up() else None
def is_warmed_up(self) -> bool:
"""Check if indicator is warmed up."""
return self.values_received >= self.period

View File

@@ -1,325 +0,0 @@
"""
Bollinger Bands Indicator State
This module implements incremental Bollinger Bands calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. Used by the BBRSStrategy.
"""
from typing import Dict, Union, Optional
from collections import deque
import math
from .base import OHLCIndicatorState
from .moving_average import MovingAverageState
class BollingerBandsState(OHLCIndicatorState):
"""
Incremental Bollinger Bands calculation state.
Bollinger Bands consist of:
- Middle Band: Simple Moving Average of close prices
- Upper Band: Middle Band + (Standard Deviation * multiplier)
- Lower Band: Middle Band - (Standard Deviation * multiplier)
This implementation maintains a rolling window for standard deviation calculation
while using the MovingAverageState for the middle band.
Attributes:
period (int): Period for moving average and standard deviation
std_dev_multiplier (float): Multiplier for standard deviation
ma_state (MovingAverageState): Moving average state for middle band
close_values (deque): Rolling window of close prices for std dev calculation
close_sum_sq (float): Sum of squared close values for variance calculation
Example:
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
# Add price data incrementally
result = bb.update(103.5) # Close price
upper_band = result['upper_band']
middle_band = result['middle_band']
lower_band = result['lower_band']
bandwidth = result['bandwidth']
"""
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0):
"""
Initialize Bollinger Bands state.
Args:
period: Period for moving average and standard deviation (default: 20)
std_dev_multiplier: Multiplier for standard deviation (default: 2.0)
Raises:
ValueError: If period is not positive or multiplier is not positive
"""
super().__init__(period)
if std_dev_multiplier <= 0:
raise ValueError(f"Standard deviation multiplier must be positive, got {std_dev_multiplier}")
self.std_dev_multiplier = std_dev_multiplier
self.ma_state = MovingAverageState(period)
# For incremental standard deviation calculation
self.close_values = deque(maxlen=period)
self.close_sum_sq = 0.0 # Sum of squared values
self.is_initialized = True
def update(self, close_price: Union[float, int]) -> Dict[str, float]:
"""
Update Bollinger Bands with new close price.
Args:
close_price: New closing price
Returns:
Dictionary with 'upper_band', 'middle_band', 'lower_band', 'bandwidth', 'std_dev'
Raises:
ValueError: If close_price is not finite
TypeError: If close_price is not numeric
"""
# Validate input
if not isinstance(close_price, (int, float)):
raise TypeError(f"close_price must be numeric, got {type(close_price)}")
self.validate_input(close_price)
close_price = float(close_price)
# Update moving average (middle band)
middle_band = self.ma_state.update(close_price)
# Update rolling window for standard deviation
if len(self.close_values) == self.period:
# Remove oldest value from sum of squares
old_value = self.close_values[0]
self.close_sum_sq -= old_value * old_value
# Add new value
self.close_values.append(close_price)
self.close_sum_sq += close_price * close_price
# Calculate standard deviation
n = len(self.close_values)
if n < 2:
# Not enough data for standard deviation
std_dev = 0.0
else:
# Incremental variance calculation: Var = (sum_sq - n*mean^2) / (n-1)
mean = middle_band
variance = (self.close_sum_sq - n * mean * mean) / (n - 1)
std_dev = math.sqrt(max(variance, 0.0)) # Ensure non-negative
# Calculate bands
upper_band = middle_band + (self.std_dev_multiplier * std_dev)
lower_band = middle_band - (self.std_dev_multiplier * std_dev)
# Calculate bandwidth (normalized band width)
if middle_band != 0:
bandwidth = (upper_band - lower_band) / middle_band
else:
bandwidth = 0.0
self.values_received += 1
# Store current values
result = {
'upper_band': upper_band,
'middle_band': middle_band,
'lower_band': lower_band,
'bandwidth': bandwidth,
'std_dev': std_dev
}
self._current_values = result
return result
def is_warmed_up(self) -> bool:
"""
Check if Bollinger Bands has enough data for reliable values.
Returns:
True if we have at least 'period' number of values
"""
return self.ma_state.is_warmed_up()
def reset(self) -> None:
"""Reset Bollinger Bands state to initial conditions."""
self.ma_state.reset()
self.close_values.clear()
self.close_sum_sq = 0.0
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""
Get current Bollinger Bands values without updating.
Returns:
Dictionary with current BB values, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self._current_values.copy() if self._current_values else None
def get_squeeze_status(self, squeeze_threshold: float = 0.05) -> bool:
"""
Check if Bollinger Bands are in a squeeze condition.
Args:
squeeze_threshold: Bandwidth threshold for squeeze detection
Returns:
True if bandwidth is below threshold (squeeze condition)
"""
if not self.is_warmed_up() or not self._current_values:
return False
bandwidth = self._current_values.get('bandwidth', float('inf'))
return bandwidth < squeeze_threshold
def get_position_relative_to_bands(self, current_price: float) -> str:
"""
Get current price position relative to Bollinger Bands.
Args:
current_price: Current price to evaluate
Returns:
'above_upper', 'between_bands', 'below_lower', or 'unknown'
"""
if not self.is_warmed_up() or not self._current_values:
return 'unknown'
upper_band = self._current_values['upper_band']
lower_band = self._current_values['lower_band']
if current_price > upper_band:
return 'above_upper'
elif current_price < lower_band:
return 'below_lower'
else:
return 'between_bands'
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'std_dev_multiplier': self.std_dev_multiplier,
'close_values_count': len(self.close_values),
'close_sum_sq': self.close_sum_sq,
'ma_state': self.ma_state.get_state_summary(),
'current_squeeze': self.get_squeeze_status() if self.is_warmed_up() else None
})
return base_summary
class BollingerBandsOHLCState(OHLCIndicatorState):
"""
Bollinger Bands implementation that works with OHLC data.
This version can calculate Bollinger Bands based on different price types
(close, typical price, etc.) and provides additional OHLC-based analysis.
"""
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0, price_type: str = 'close'):
"""
Initialize OHLC Bollinger Bands state.
Args:
period: Period for calculation
std_dev_multiplier: Standard deviation multiplier
price_type: Price type to use ('close', 'typical', 'median', 'weighted')
"""
super().__init__(period)
if price_type not in ['close', 'typical', 'median', 'weighted']:
raise ValueError(f"Invalid price_type: {price_type}")
self.std_dev_multiplier = std_dev_multiplier
self.price_type = price_type
self.bb_state = BollingerBandsState(period, std_dev_multiplier)
self.is_initialized = True
def _extract_price(self, ohlc_data: Dict[str, float]) -> float:
"""Extract price based on price_type setting."""
if self.price_type == 'close':
return ohlc_data['close']
elif self.price_type == 'typical':
return (ohlc_data['high'] + ohlc_data['low'] + ohlc_data['close']) / 3.0
elif self.price_type == 'median':
return (ohlc_data['high'] + ohlc_data['low']) / 2.0
elif self.price_type == 'weighted':
return (ohlc_data['high'] + ohlc_data['low'] + 2 * ohlc_data['close']) / 4.0
else:
return ohlc_data['close']
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
"""
Update Bollinger Bands with OHLC data.
Args:
ohlc_data: Dictionary with OHLC data
Returns:
Dictionary with Bollinger Bands values plus OHLC analysis
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
# Extract price based on type
price = self._extract_price(ohlc_data)
# Update underlying BB state
bb_result = self.bb_state.update(price)
# Add OHLC-specific analysis
high = ohlc_data['high']
low = ohlc_data['low']
close = ohlc_data['close']
# Check if high/low touched bands
upper_band = bb_result['upper_band']
lower_band = bb_result['lower_band']
bb_result.update({
'high_above_upper': high > upper_band,
'low_below_lower': low < lower_band,
'close_position': self.bb_state.get_position_relative_to_bands(close),
'price_type': self.price_type,
'extracted_price': price
})
self.values_received += 1
self._current_values = bb_result
return bb_result
def is_warmed_up(self) -> bool:
"""Check if OHLC Bollinger Bands is warmed up."""
return self.bb_state.is_warmed_up()
def reset(self) -> None:
"""Reset OHLC Bollinger Bands state."""
self.bb_state.reset()
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""Get current OHLC Bollinger Bands values."""
return self.bb_state.get_current_value()
def get_state_summary(self) -> dict:
"""Get detailed state summary."""
base_summary = super().get_state_summary()
base_summary.update({
'price_type': self.price_type,
'bb_state': self.bb_state.get_state_summary()
})
return base_summary

View File

@@ -1,228 +0,0 @@
"""
Moving Average Indicator State
This module implements incremental moving average calculation that maintains
constant memory usage and provides identical results to traditional batch calculations.
"""
from collections import deque
from typing import Union
from .base import SimpleIndicatorState
class MovingAverageState(SimpleIndicatorState):
"""
Incremental moving average calculation state.
This class maintains the state for calculating a simple moving average
incrementally. It uses a rolling window approach with constant memory usage.
Attributes:
period (int): The moving average period
values (deque): Rolling window of values (max length = period)
sum (float): Current sum of values in the window
Example:
ma = MovingAverageState(period=20)
# Add values incrementally
ma_value = ma.update(100.0) # Returns current MA value
ma_value = ma.update(105.0) # Updates and returns new MA value
# Check if warmed up (has enough values)
if ma.is_warmed_up():
current_ma = ma.get_current_value()
"""
def __init__(self, period: int):
"""
Initialize moving average state.
Args:
period: Number of periods for the moving average
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.values = deque(maxlen=period)
self.sum = 0.0
self.is_initialized = True
def update(self, new_value: Union[float, int]) -> float:
"""
Update moving average with new value.
Args:
new_value: New price/value to add to the moving average
Returns:
Current moving average value
Raises:
ValueError: If new_value is not finite
TypeError: If new_value is not numeric
"""
# Validate input
if not isinstance(new_value, (int, float)):
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
self.validate_input(new_value)
# If deque is at max capacity, subtract the value being removed
if len(self.values) == self.period:
self.sum -= self.values[0] # Will be automatically removed by deque
# Add new value
self.values.append(float(new_value))
self.sum += float(new_value)
self.values_received += 1
# Calculate current moving average
current_count = len(self.values)
self._current_value = self.sum / current_count
return self._current_value
def is_warmed_up(self) -> bool:
"""
Check if moving average has enough data for reliable values.
Returns:
True if we have at least 'period' number of values
"""
return len(self.values) >= self.period
def reset(self) -> None:
"""Reset moving average state to initial conditions."""
self.values.clear()
self.sum = 0.0
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Union[float, None]:
"""
Get current moving average value without updating.
Returns:
Current moving average value, or None if not enough data
"""
if len(self.values) == 0:
return None
return self.sum / len(self.values)
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'window_size': len(self.values),
'sum': self.sum,
'values_in_window': list(self.values) if len(self.values) <= 10 else f"[{len(self.values)} values]"
})
return base_summary
class ExponentialMovingAverageState(SimpleIndicatorState):
"""
Incremental exponential moving average calculation state.
This class maintains the state for calculating an exponential moving average (EMA)
incrementally. EMA gives more weight to recent values and requires minimal memory.
Attributes:
period (int): The EMA period (used to calculate smoothing factor)
alpha (float): Smoothing factor (2 / (period + 1))
ema_value (float): Current EMA value
Example:
ema = ExponentialMovingAverageState(period=20)
# Add values incrementally
ema_value = ema.update(100.0) # Returns current EMA value
ema_value = ema.update(105.0) # Updates and returns new EMA value
"""
def __init__(self, period: int):
"""
Initialize exponential moving average state.
Args:
period: Number of periods for the EMA (used to calculate alpha)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.alpha = 2.0 / (period + 1) # Smoothing factor
self.ema_value = None
self.is_initialized = True
def update(self, new_value: Union[float, int]) -> float:
"""
Update exponential moving average with new value.
Args:
new_value: New price/value to add to the EMA
Returns:
Current EMA value
Raises:
ValueError: If new_value is not finite
TypeError: If new_value is not numeric
"""
# Validate input
if not isinstance(new_value, (int, float)):
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
self.validate_input(new_value)
new_value = float(new_value)
if self.ema_value is None:
# First value - initialize EMA
self.ema_value = new_value
else:
# EMA formula: EMA = alpha * new_value + (1 - alpha) * previous_EMA
self.ema_value = self.alpha * new_value + (1 - self.alpha) * self.ema_value
self.values_received += 1
self._current_value = self.ema_value
return self.ema_value
def is_warmed_up(self) -> bool:
"""
Check if EMA has enough data for reliable values.
For EMA, we consider it warmed up after receiving 'period' number of values,
though it starts producing values immediately.
Returns:
True if we have at least 'period' number of values
"""
return self.values_received >= self.period
def reset(self) -> None:
"""Reset EMA state to initial conditions."""
self.ema_value = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Union[float, None]:
"""
Get current EMA value without updating.
Returns:
Current EMA value, or None if no data received
"""
return self.ema_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'alpha': self.alpha,
'ema_value': self.ema_value
})
return base_summary

View File

@@ -1,289 +0,0 @@
"""
RSI (Relative Strength Index) Indicator State
This module implements incremental RSI calculation that maintains constant memory usage
and provides identical results to traditional batch calculations.
"""
from typing import Union, Optional
from .base import SimpleIndicatorState
from .moving_average import ExponentialMovingAverageState
class RSIState(SimpleIndicatorState):
"""
Incremental RSI calculation state using Wilder's smoothing.
RSI measures the speed and magnitude of price changes to evaluate overbought
or oversold conditions. It oscillates between 0 and 100.
RSI = 100 - (100 / (1 + RS))
where RS = Average Gain / Average Loss over the specified period
This implementation uses Wilder's smoothing (alpha = 1/period) to match
the original pandas implementation exactly.
Attributes:
period (int): The RSI period (typically 14)
alpha (float): Wilder's smoothing factor (1/period)
avg_gain (float): Current average gain
avg_loss (float): Current average loss
previous_close (float): Previous period's close price
Example:
rsi = RSIState(period=14)
# Add price data incrementally
rsi_value = rsi.update(100.0) # Returns current RSI value
rsi_value = rsi.update(105.0) # Updates and returns new RSI value
# Check if warmed up
if rsi.is_warmed_up():
current_rsi = rsi.get_current_value()
"""
def __init__(self, period: int = 14):
"""
Initialize RSI state.
Args:
period: Number of periods for RSI calculation (default: 14)
Raises:
ValueError: If period is not a positive integer
"""
super().__init__(period)
self.alpha = 1.0 / period # Wilder's smoothing factor
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.is_initialized = True
def update(self, new_close: Union[float, int]) -> float:
"""
Update RSI with new close price using Wilder's smoothing.
Args:
new_close: New closing price
Returns:
Current RSI value (0-100), or NaN if not warmed up
Raises:
ValueError: If new_close is not finite
TypeError: If new_close is not numeric
"""
# Validate input - accept numpy types as well
import numpy as np
if not isinstance(new_close, (int, float, np.integer, np.floating)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(float(new_close))
new_close = float(new_close)
if self.previous_close is None:
# First value - no gain/loss to calculate
self.previous_close = new_close
self.values_received += 1
# Return NaN until warmed up (matches original behavior)
self._current_value = float('nan')
return self._current_value
# Calculate price change
price_change = new_close - self.previous_close
# Separate gains and losses
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
if self.avg_gain is None:
# Initialize with first gain/loss
self.avg_gain = gain
self.avg_loss = loss
else:
# Wilder's smoothing: avg = alpha * new_value + (1 - alpha) * previous_avg
self.avg_gain = self.alpha * gain + (1 - self.alpha) * self.avg_gain
self.avg_loss = self.alpha * loss + (1 - self.alpha) * self.avg_loss
# Calculate RSI only if warmed up
# RSI should start when we have 'period' price changes (not including the first value)
if self.values_received > self.period:
if self.avg_loss == 0.0:
# Avoid division by zero - all gains, no losses
if self.avg_gain > 0:
rsi_value = 100.0
else:
rsi_value = 50.0 # Neutral when both are zero
else:
rs = self.avg_gain / self.avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
else:
# Not warmed up yet - return NaN
rsi_value = float('nan')
# Store state
self.previous_close = new_close
self.values_received += 1
self._current_value = rsi_value
return rsi_value
def is_warmed_up(self) -> bool:
"""
Check if RSI has enough data for reliable values.
Returns:
True if we have enough price changes for RSI calculation
"""
return self.values_received > self.period
def reset(self) -> None:
"""Reset RSI state to initial conditions."""
self.alpha = 1.0 / self.period
self.avg_gain = None
self.avg_loss = None
self.previous_close = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""
Get current RSI value without updating.
Returns:
Current RSI value (0-100), or None if not enough data
"""
if not self.is_warmed_up():
return None
return self._current_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'alpha': self.alpha,
'previous_close': self.previous_close,
'avg_gain': self.avg_gain,
'avg_loss': self.avg_loss,
'current_rsi': self.get_current_value()
})
return base_summary
class SimpleRSIState(SimpleIndicatorState):
"""
Simple RSI implementation using simple moving averages instead of EMAs.
This version uses simple moving averages for gain and loss smoothing,
which matches traditional RSI implementations but requires more memory.
"""
def __init__(self, period: int = 14):
"""
Initialize simple RSI state.
Args:
period: Number of periods for RSI calculation (default: 14)
"""
super().__init__(period)
from collections import deque
self.gains = deque(maxlen=period)
self.losses = deque(maxlen=period)
self.gain_sum = 0.0
self.loss_sum = 0.0
self.previous_close = None
self.is_initialized = True
def update(self, new_close: Union[float, int]) -> float:
"""
Update simple RSI with new close price.
Args:
new_close: New closing price
Returns:
Current RSI value (0-100)
"""
# Validate input
if not isinstance(new_close, (int, float)):
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
self.validate_input(new_close)
new_close = float(new_close)
if self.previous_close is None:
# First value
self.previous_close = new_close
self.values_received += 1
self._current_value = 50.0
return self._current_value
# Calculate price change
price_change = new_close - self.previous_close
gain = max(price_change, 0.0)
loss = max(-price_change, 0.0)
# Update rolling sums
if len(self.gains) == self.period:
self.gain_sum -= self.gains[0]
self.loss_sum -= self.losses[0]
self.gains.append(gain)
self.losses.append(loss)
self.gain_sum += gain
self.loss_sum += loss
# Calculate RSI
if len(self.gains) == 0:
rsi_value = 50.0
else:
avg_gain = self.gain_sum / len(self.gains)
avg_loss = self.loss_sum / len(self.losses)
if avg_loss == 0.0:
rsi_value = 100.0
else:
rs = avg_gain / avg_loss
rsi_value = 100.0 - (100.0 / (1.0 + rs))
# Store state
self.previous_close = new_close
self.values_received += 1
self._current_value = rsi_value
return rsi_value
def is_warmed_up(self) -> bool:
"""Check if simple RSI is warmed up."""
return len(self.gains) >= self.period
def reset(self) -> None:
"""Reset simple RSI state."""
self.gains.clear()
self.losses.clear()
self.gain_sum = 0.0
self.loss_sum = 0.0
self.previous_close = None
self.values_received = 0
self._current_value = None
def get_current_value(self) -> Optional[float]:
"""Get current simple RSI value."""
if self.values_received == 0:
return None
return self._current_value
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'previous_close': self.previous_close,
'gains_window_size': len(self.gains),
'losses_window_size': len(self.losses),
'gain_sum': self.gain_sum,
'loss_sum': self.loss_sum,
'current_rsi': self.get_current_value()
})
return base_summary

View File

@@ -1,333 +0,0 @@
"""
Supertrend Indicator State
This module implements incremental Supertrend calculation that maintains constant memory usage
and provides identical results to traditional batch calculations. Supertrend is used by
the DefaultStrategy for trend detection.
"""
from typing import Dict, Union, Optional
from .base import OHLCIndicatorState
from .atr import ATRState
class SupertrendState(OHLCIndicatorState):
"""
Incremental Supertrend calculation state.
Supertrend is a trend-following indicator that uses Average True Range (ATR)
to calculate dynamic support and resistance levels. It provides clear trend
direction signals: +1 for uptrend, -1 for downtrend.
The calculation involves:
1. Calculate ATR for the given period
2. Calculate basic upper and lower bands using ATR and multiplier
3. Calculate final upper and lower bands with trend logic
4. Determine trend direction based on price vs bands
Attributes:
period (int): ATR period for Supertrend calculation
multiplier (float): Multiplier for ATR in band calculation
atr_state (ATRState): ATR calculation state
previous_close (float): Previous period's close price
previous_trend (int): Previous trend direction (+1 or -1)
final_upper_band (float): Current final upper band
final_lower_band (float): Current final lower band
Example:
supertrend = SupertrendState(period=10, multiplier=3.0)
# Add OHLC data incrementally
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
result = supertrend.update(ohlc)
trend = result['trend'] # +1 or -1
supertrend_value = result['supertrend'] # Supertrend line value
"""
def __init__(self, period: int = 10, multiplier: float = 3.0):
"""
Initialize Supertrend state.
Args:
period: ATR period for Supertrend calculation (default: 10)
multiplier: Multiplier for ATR in band calculation (default: 3.0)
Raises:
ValueError: If period is not positive or multiplier is not positive
"""
super().__init__(period)
if multiplier <= 0:
raise ValueError(f"Multiplier must be positive, got {multiplier}")
self.multiplier = multiplier
self.atr_state = ATRState(period)
# State variables
self.previous_close = None
self.previous_trend = None # Don't assume initial trend, let first calculation determine it
self.final_upper_band = None
self.final_lower_band = None
# Current values
self.current_trend = None
self.current_supertrend = None
self.is_initialized = True
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
"""
Update Supertrend with new OHLC data.
Args:
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
Returns:
Dictionary with 'trend', 'supertrend', 'upper_band', 'lower_band' keys
Raises:
ValueError: If OHLC data is invalid
TypeError: If ohlc_data is not a dictionary
"""
# Validate input
if not isinstance(ohlc_data, dict):
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
self.validate_input(ohlc_data)
high = float(ohlc_data['high'])
low = float(ohlc_data['low'])
close = float(ohlc_data['close'])
# Update ATR
atr_value = self.atr_state.update(ohlc_data)
# Calculate HL2 (typical price)
hl2 = (high + low) / 2.0
# Calculate basic upper and lower bands
basic_upper_band = hl2 + (self.multiplier * atr_value)
basic_lower_band = hl2 - (self.multiplier * atr_value)
# Calculate final upper band
if self.final_upper_band is None or basic_upper_band < self.final_upper_band or self.previous_close > self.final_upper_band:
final_upper_band = basic_upper_band
else:
final_upper_band = self.final_upper_band
# Calculate final lower band
if self.final_lower_band is None or basic_lower_band > self.final_lower_band or self.previous_close < self.final_lower_band:
final_lower_band = basic_lower_band
else:
final_lower_band = self.final_lower_band
# Determine trend
if self.previous_close is None:
# First calculation - match original logic
# If close <= upper_band, trend is -1 (downtrend), else trend is 1 (uptrend)
trend = -1 if close <= basic_upper_band else 1
else:
# Trend logic for subsequent calculations
if self.previous_trend == 1 and close <= final_lower_band:
trend = -1
elif self.previous_trend == -1 and close >= final_upper_band:
trend = 1
else:
trend = self.previous_trend
# Calculate Supertrend value
if trend == 1:
supertrend_value = final_lower_band
else:
supertrend_value = final_upper_band
# Store current state
self.previous_close = close
self.previous_trend = trend
self.final_upper_band = final_upper_band
self.final_lower_band = final_lower_band
self.current_trend = trend
self.current_supertrend = supertrend_value
self.values_received += 1
# Prepare result
result = {
'trend': trend,
'supertrend': supertrend_value,
'upper_band': final_upper_band,
'lower_band': final_lower_band,
'atr': atr_value
}
self._current_values = result
return result
def is_warmed_up(self) -> bool:
"""
Check if Supertrend has enough data for reliable values.
Returns:
True if ATR state is warmed up
"""
return self.atr_state.is_warmed_up()
def reset(self) -> None:
"""Reset Supertrend state to initial conditions."""
self.atr_state.reset()
self.previous_close = None
self.previous_trend = None
self.final_upper_band = None
self.final_lower_band = None
self.current_trend = None
self.current_supertrend = None
self.values_received = 0
self._current_values = {}
def get_current_value(self) -> Optional[Dict[str, float]]:
"""
Get current Supertrend values without updating.
Returns:
Dictionary with current Supertrend values, or None if not warmed up
"""
if not self.is_warmed_up():
return None
return self._current_values.copy() if self._current_values else None
def get_current_trend(self) -> int:
"""
Get current trend direction.
Returns:
Current trend: +1 for uptrend, -1 for downtrend, 0 if not initialized
"""
return self.current_trend if self.current_trend is not None else 0
def get_current_supertrend_value(self) -> Optional[float]:
"""
Get current Supertrend line value.
Returns:
Current Supertrend value, or None if not available
"""
return self.current_supertrend
def get_state_summary(self) -> dict:
"""Get detailed state summary for debugging."""
base_summary = super().get_state_summary()
base_summary.update({
'multiplier': self.multiplier,
'previous_close': self.previous_close,
'previous_trend': self.previous_trend,
'current_trend': self.current_trend,
'current_supertrend': self.current_supertrend,
'final_upper_band': self.final_upper_band,
'final_lower_band': self.final_lower_band,
'atr_state': self.atr_state.get_state_summary()
})
return base_summary
class SupertrendCollection:
"""
Collection of multiple Supertrend indicators with different parameters.
This class manages multiple Supertrend indicators and provides meta-trend
calculation based on agreement between different Supertrend configurations.
Used by the DefaultStrategy for robust trend detection.
Example:
# Create collection with three Supertrend indicators
collection = SupertrendCollection([
(10, 3.0), # period=10, multiplier=3.0
(11, 2.0), # period=11, multiplier=2.0
(12, 1.0) # period=12, multiplier=1.0
])
# Update all indicators
results = collection.update(ohlc_data)
meta_trend = results['meta_trend'] # 1, -1, or 0 (neutral)
"""
def __init__(self, supertrend_configs: list):
"""
Initialize Supertrend collection.
Args:
supertrend_configs: List of (period, multiplier) tuples
"""
self.supertrends = []
for period, multiplier in supertrend_configs:
self.supertrends.append(SupertrendState(period, multiplier))
self.values_received = 0
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, Union[int, list]]:
"""
Update all Supertrend indicators and calculate meta-trend.
Args:
ohlc_data: OHLC data dictionary
Returns:
Dictionary with individual trends and meta-trend
"""
trends = []
results = []
# Update each Supertrend
for supertrend in self.supertrends:
result = supertrend.update(ohlc_data)
trends.append(result['trend'])
results.append(result)
# Calculate meta-trend: all must agree for directional signal
if all(trend == trends[0] for trend in trends):
meta_trend = trends[0] # All agree
else:
meta_trend = 0 # Neutral when trends don't agree
self.values_received += 1
return {
'trends': trends,
'meta_trend': meta_trend,
'results': results
}
def is_warmed_up(self) -> bool:
"""Check if all Supertrend indicators are warmed up."""
return all(st.is_warmed_up() for st in self.supertrends)
def reset(self) -> None:
"""Reset all Supertrend indicators."""
for supertrend in self.supertrends:
supertrend.reset()
self.values_received = 0
def get_current_meta_trend(self) -> int:
"""
Get current meta-trend without updating.
Returns:
Current meta-trend: +1, -1, or 0
"""
if not self.is_warmed_up():
return 0
trends = [st.get_current_trend() for st in self.supertrends]
if all(trend == trends[0] for trend in trends):
return trends[0]
else:
return 0
def get_state_summary(self) -> dict:
"""Get detailed state summary for all Supertrends."""
return {
'num_supertrends': len(self.supertrends),
'values_received': self.values_received,
'is_warmed_up': self.is_warmed_up(),
'current_meta_trend': self.get_current_meta_trend(),
'supertrends': [st.get_state_summary() for st in self.supertrends]
}

View File

@@ -1,423 +0,0 @@
"""
Incremental MetaTrend Strategy
This module implements an incremental version of the DefaultStrategy that processes
real-time data efficiently while producing identical meta-trend signals to the
original batch-processing implementation.
The strategy uses 3 Supertrend indicators with parameters:
- Supertrend 1: period=12, multiplier=3.0
- Supertrend 2: period=10, multiplier=1.0
- Supertrend 3: period=11, multiplier=2.0
Meta-trend calculation:
- Meta-trend = 1 when all 3 Supertrends agree on uptrend
- Meta-trend = -1 when all 3 Supertrends agree on downtrend
- Meta-trend = 0 when Supertrends disagree (neutral)
Signal generation:
- Entry: meta-trend changes from != 1 to == 1
- Exit: meta-trend changes from != -1 to == -1
Stop-loss handling is delegated to the trader layer.
"""
import pandas as pd
import numpy as np
from typing import Dict, Optional, List, Any
import logging
from .base import IncStrategyBase, IncStrategySignal
from .indicators.supertrend import SupertrendCollection
logger = logging.getLogger(__name__)
class IncMetaTrendStrategy(IncStrategyBase):
"""
Incremental MetaTrend strategy implementation.
This strategy uses multiple Supertrend indicators to determine market direction
and generates entry/exit signals based on meta-trend changes. It processes
data incrementally for real-time performance while maintaining mathematical
equivalence to the original DefaultStrategy.
The strategy is designed to work with any timeframe but defaults to the
timeframe specified in parameters (or 15min if not specified).
Parameters:
timeframe (str): Primary timeframe for analysis (default: "15min")
buffer_size_multiplier (float): Buffer size multiplier for memory management (default: 2.0)
enable_logging (bool): Enable detailed logging (default: False)
Example:
strategy = IncMetaTrendStrategy("metatrend", weight=1.0, params={
"timeframe": "15min",
"enable_logging": True
})
"""
def __init__(self, name: str = "metatrend", weight: float = 1.0, params: Optional[Dict] = None):
"""
Initialize the incremental MetaTrend strategy.
Args:
name: Strategy name/identifier
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters
"""
super().__init__(name, weight, params)
# Strategy configuration - now handled by base class timeframe aggregation
self.primary_timeframe = self.params.get("timeframe", "15min")
self.enable_logging = self.params.get("enable_logging", False)
# Configure logging level
if self.enable_logging:
logger.setLevel(logging.DEBUG)
# Initialize Supertrend collection with exact parameters from original strategy
self.supertrend_configs = [
(12, 3.0), # period=12, multiplier=3.0
(10, 1.0), # period=10, multiplier=1.0
(11, 2.0) # period=11, multiplier=2.0
]
self.supertrend_collection = SupertrendCollection(self.supertrend_configs)
# Meta-trend state
self.current_meta_trend = 0
self.previous_meta_trend = 0
self._meta_trend_history = [] # For debugging/analysis
# Signal generation state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Performance tracking
self._update_count = 0
self._last_update_time = None
logger.info(f"IncMetaTrendStrategy initialized: timeframe={self.primary_timeframe}, "
f"aggregation_enabled={self._timeframe_aggregator is not None}")
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for reliable Supertrend calculations.
With the new base class timeframe aggregation, we only need to specify
the minimum buffer size for our primary timeframe. The base class
handles minute-level data aggregation automatically.
Returns:
Dict[str, int]: {timeframe: min_points} mapping
"""
# Find the largest period among all Supertrend configurations
max_period = max(config[0] for config in self.supertrend_configs)
# Add buffer for ATR warmup (ATR typically needs ~2x period for stability)
min_buffer_size = max_period * 2 + 10 # Extra 10 points for safety
# With new base class, we only specify our primary timeframe
# The base class handles minute-level aggregation automatically
return {self.primary_timeframe: min_buffer_size}
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
This method updates the Supertrend indicators and recalculates the meta-trend
based on the new data point.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
try:
self._update_count += 1
self._last_update_time = timestamp
if self.enable_logging:
logger.debug(f"Processing data point {self._update_count} at {timestamp}")
logger.debug(f"OHLC: O={new_data_point.get('open', 0):.2f}, "
f"H={new_data_point.get('high', 0):.2f}, "
f"L={new_data_point.get('low', 0):.2f}, "
f"C={new_data_point.get('close', 0):.2f}")
# Store previous meta-trend for change detection
self.previous_meta_trend = self.current_meta_trend
# Update Supertrend collection with new data
supertrend_results = self.supertrend_collection.update(new_data_point)
# Calculate new meta-trend
self.current_meta_trend = self._calculate_meta_trend(supertrend_results)
# Store meta-trend history for analysis
self._meta_trend_history.append({
'timestamp': timestamp,
'meta_trend': self.current_meta_trend,
'individual_trends': supertrend_results['trends'].copy(),
'update_count': self._update_count
})
# Limit history size to prevent memory growth
if len(self._meta_trend_history) > 1000:
self._meta_trend_history = self._meta_trend_history[-500:] # Keep last 500
# Log meta-trend changes
if self.enable_logging and self.current_meta_trend != self.previous_meta_trend:
logger.info(f"Meta-trend changed: {self.previous_meta_trend} -> {self.current_meta_trend} "
f"at {timestamp} (update #{self._update_count})")
logger.debug(f"Individual trends: {supertrend_results['trends']}")
# Update warmup status
if not self._is_warmed_up and self.supertrend_collection.is_warmed_up():
self._is_warmed_up = True
logger.info(f"Strategy warmed up after {self._update_count} data points")
except Exception as e:
logger.error(f"Error in calculate_on_data: {e}")
raise
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Returns:
bool: True (this strategy is fully incremental)
"""
return True
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate entry signal based on meta-trend direction change.
Entry occurs when meta-trend changes from != 1 to == 1, indicating
all Supertrend indicators now agree on upward direction.
Returns:
IncStrategySignal: Entry signal if trend aligns, hold signal otherwise
"""
if not self.is_warmed_up:
return IncStrategySignal("HOLD", confidence=0.0)
# Check for meta-trend entry condition
if self._check_entry_condition():
self._signal_count["entry"] += 1
self._last_entry_signal = {
'timestamp': self._last_update_time,
'meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"ENTRY SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['entry']})")
return IncStrategySignal("ENTRY", confidence=1.0, metadata={
"meta_trend": self.current_meta_trend,
"previous_meta_trend": self.previous_meta_trend,
"signal_count": self._signal_count["entry"]
})
return IncStrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate exit signal based on meta-trend reversal.
Exit occurs when meta-trend changes from != -1 to == -1, indicating
trend reversal to downward direction.
Returns:
IncStrategySignal: Exit signal if trend reverses, hold signal otherwise
"""
if not self.is_warmed_up:
return IncStrategySignal("HOLD", confidence=0.0)
# Check for meta-trend exit condition
if self._check_exit_condition():
self._signal_count["exit"] += 1
self._last_exit_signal = {
'timestamp': self._last_update_time,
'meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'update_count': self._update_count
}
if self.enable_logging:
logger.info(f"EXIT SIGNAL generated at {self._last_update_time} "
f"(signal #{self._signal_count['exit']})")
return IncStrategySignal("EXIT", confidence=1.0, metadata={
"type": "META_TREND_EXIT",
"meta_trend": self.current_meta_trend,
"previous_meta_trend": self.previous_meta_trend,
"signal_count": self._signal_count["exit"]
})
return IncStrategySignal("HOLD", confidence=0.0)
def get_confidence(self) -> float:
"""
Get strategy confidence based on meta-trend strength.
Higher confidence when meta-trend is strongly directional,
lower confidence during neutral periods.
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.is_warmed_up:
return 0.0
# High confidence for strong directional signals
if self.current_meta_trend == 1 or self.current_meta_trend == -1:
return 1.0
# Lower confidence for neutral trend
return 0.3
def _calculate_meta_trend(self, supertrend_results: Dict) -> int:
"""
Calculate meta-trend from SupertrendCollection results.
Meta-trend logic (matching original DefaultStrategy):
- All 3 Supertrends must agree for directional signal
- If all trends are the same, meta-trend = that trend
- If trends disagree, meta-trend = 0 (neutral)
Args:
supertrend_results: Results from SupertrendCollection.update()
Returns:
int: Meta-trend value (1, -1, or 0)
"""
trends = supertrend_results['trends']
# Check if all trends agree
if all(trend == trends[0] for trend in trends):
return trends[0] # All agree: return the common trend
else:
return 0 # Neutral when trends disagree
def _check_entry_condition(self) -> bool:
"""
Check if meta-trend entry condition is met.
Entry condition: meta-trend changes from != 1 to == 1
Returns:
bool: True if entry condition is met
"""
return (self.previous_meta_trend != 1 and
self.current_meta_trend == 1)
def _check_exit_condition(self) -> bool:
"""
Check if meta-trend exit condition is met.
Exit condition: meta-trend changes from != 1 to == -1
(Modified to match original strategy behavior)
Returns:
bool: True if exit condition is met
"""
return (self.previous_meta_trend != 1 and
self.current_meta_trend == -1)
def get_current_state_summary(self) -> Dict[str, Any]:
"""
Get detailed state summary for debugging and monitoring.
Returns:
Dict with current strategy state information
"""
base_summary = super().get_current_state_summary()
# Add MetaTrend-specific state
base_summary.update({
'primary_timeframe': self.primary_timeframe,
'current_meta_trend': self.current_meta_trend,
'previous_meta_trend': self.previous_meta_trend,
'supertrend_collection_warmed_up': self.supertrend_collection.is_warmed_up(),
'supertrend_configs': self.supertrend_configs,
'signal_counts': self._signal_count.copy(),
'update_count': self._update_count,
'last_update_time': str(self._last_update_time) if self._last_update_time else None,
'meta_trend_history_length': len(self._meta_trend_history),
'last_entry_signal': self._last_entry_signal,
'last_exit_signal': self._last_exit_signal
})
# Add Supertrend collection state
if hasattr(self.supertrend_collection, 'get_state_summary'):
base_summary['supertrend_collection_state'] = self.supertrend_collection.get_state_summary()
return base_summary
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
super().reset_calculation_state()
# Reset Supertrend collection
self.supertrend_collection.reset()
# Reset meta-trend state
self.current_meta_trend = 0
self.previous_meta_trend = 0
self._meta_trend_history.clear()
# Reset signal state
self._last_entry_signal = None
self._last_exit_signal = None
self._signal_count = {"entry": 0, "exit": 0}
# Reset performance tracking
self._update_count = 0
self._last_update_time = None
logger.info("IncMetaTrendStrategy state reset")
def get_meta_trend_history(self, limit: Optional[int] = None) -> List[Dict]:
"""
Get meta-trend history for analysis.
Args:
limit: Maximum number of recent entries to return
Returns:
List of meta-trend history entries
"""
if limit is None:
return self._meta_trend_history.copy()
else:
return self._meta_trend_history[-limit:] if limit > 0 else []
def get_current_meta_trend(self) -> int:
"""
Get current meta-trend value.
Returns:
int: Current meta-trend (1, -1, or 0)
"""
return self.current_meta_trend
def get_individual_supertrend_states(self) -> List[Dict]:
"""
Get current state of individual Supertrend indicators.
Returns:
List of Supertrend state summaries
"""
if hasattr(self.supertrend_collection, 'get_state_summary'):
collection_state = self.supertrend_collection.get_state_summary()
return collection_state.get('supertrends', [])
return []
# Compatibility alias for easier imports
MetaTrendStrategy = IncMetaTrendStrategy

View File

@@ -1,329 +0,0 @@
"""
Incremental Random Strategy for Testing
This strategy generates random entry and exit signals for testing the incremental strategy system.
It's useful for verifying that the incremental strategy framework is working correctly.
"""
import random
import logging
import time
from typing import Dict, Optional
import pandas as pd
from .base import IncStrategyBase, IncStrategySignal
logger = logging.getLogger(__name__)
class IncRandomStrategy(IncStrategyBase):
"""
Incremental random signal generator strategy for testing.
This strategy generates random entry and exit signals with configurable
probability and confidence levels. It's designed to test the incremental
strategy framework and signal processing system.
The incremental version maintains minimal state and processes each new
data point independently, making it ideal for testing real-time performance.
Parameters:
entry_probability: Probability of generating an entry signal (0.0-1.0)
exit_probability: Probability of generating an exit signal (0.0-1.0)
min_confidence: Minimum confidence level for signals
max_confidence: Maximum confidence level for signals
timeframe: Timeframe to operate on (default: "1min")
signal_frequency: How often to generate signals (every N bars)
random_seed: Optional seed for reproducible random signals
Example:
strategy = IncRandomStrategy(
weight=1.0,
params={
"entry_probability": 0.1,
"exit_probability": 0.15,
"min_confidence": 0.7,
"max_confidence": 0.9,
"signal_frequency": 5,
"random_seed": 42 # For reproducible testing
}
)
"""
def __init__(self, weight: float = 1.0, params: Optional[Dict] = None):
"""Initialize the incremental random strategy."""
super().__init__("inc_random", weight, params)
# Strategy parameters with defaults
self.entry_probability = self.params.get("entry_probability", 0.05) # 5% chance per bar
self.exit_probability = self.params.get("exit_probability", 0.1) # 10% chance per bar
self.min_confidence = self.params.get("min_confidence", 0.6)
self.max_confidence = self.params.get("max_confidence", 0.9)
self.timeframe = self.params.get("timeframe", "1min")
self.signal_frequency = self.params.get("signal_frequency", 1) # Every bar
# Create separate random instance for this strategy
self._random = random.Random()
random_seed = self.params.get("random_seed")
if random_seed is not None:
self._random.seed(random_seed)
logger.info(f"IncRandomStrategy: Set random seed to {random_seed}")
# Internal state (minimal for random strategy)
self._bar_count = 0
self._last_signal_bar = -1
self._current_price = None
self._last_timestamp = None
logger.info(f"IncRandomStrategy initialized with entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
f"aggregation_enabled={self._timeframe_aggregator is not None}")
def get_minimum_buffer_size(self) -> Dict[str, int]:
"""
Return minimum data points needed for each timeframe.
Random strategy doesn't need any historical data for calculations,
so we only need 1 data point to start generating signals.
With the new base class timeframe aggregation, we only specify
our primary timeframe.
Returns:
Dict[str, int]: Minimal buffer requirements
"""
return {self.timeframe: 1} # Only need current data point
def supports_incremental_calculation(self) -> bool:
"""
Whether strategy supports incremental calculation.
Random strategy is ideal for incremental mode since it doesn't
depend on historical calculations.
Returns:
bool: Always True for random strategy
"""
return True
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
"""
Process a single new data point incrementally.
For random strategy, we just update our internal state with the
current price. The base class now handles timeframe aggregation
automatically, so we only receive data when a complete timeframe
bar is formed.
Args:
new_data_point: OHLCV data point {open, high, low, close, volume}
timestamp: Timestamp of the data point
"""
start_time = time.perf_counter()
try:
# Update internal state - base class handles timeframe aggregation
self._current_price = new_data_point['close']
self._last_timestamp = timestamp
self._data_points_received += 1
# Increment bar count for each processed timeframe bar
self._bar_count += 1
# Debug logging every 10 bars
if self._bar_count % 10 == 0:
logger.debug(f"IncRandomStrategy: Processing bar {self._bar_count}, "
f"price=${self._current_price:.2f}, timestamp={timestamp}")
# Update warm-up status
if not self._is_warmed_up and self._data_points_received >= 1:
self._is_warmed_up = True
self._calculation_mode = "incremental"
logger.info(f"IncRandomStrategy: Warmed up after {self._data_points_received} data points")
# Record performance metrics
update_time = time.perf_counter() - start_time
self._performance_metrics['update_times'].append(update_time)
except Exception as e:
logger.error(f"IncRandomStrategy: Error in calculate_on_data: {e}")
self._performance_metrics['state_validation_failures'] += 1
raise
def get_entry_signal(self) -> IncStrategySignal:
"""
Generate random entry signals based on current state.
Returns:
IncStrategySignal: Entry signal with confidence level
"""
if not self._is_warmed_up:
return IncStrategySignal("HOLD", 0.0)
start_time = time.perf_counter()
try:
# Check if we should generate a signal based on frequency
if (self._bar_count - self._last_signal_bar) < self.signal_frequency:
return IncStrategySignal("HOLD", 0.0)
# Generate random entry signal using strategy's random instance
random_value = self._random.random()
if random_value < self.entry_probability:
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
self._last_signal_bar = self._bar_count
logger.info(f"IncRandomStrategy: Generated ENTRY signal at bar {self._bar_count}, "
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
f"random_value={random_value:.3f}")
signal = IncStrategySignal(
"ENTRY",
confidence=confidence,
price=self._current_price,
metadata={
"strategy": "inc_random",
"bar_count": self._bar_count,
"timeframe": self.timeframe,
"random_value": random_value,
"timestamp": self._last_timestamp
}
)
# Record performance metrics
signal_time = time.perf_counter() - start_time
self._performance_metrics['signal_generation_times'].append(signal_time)
return signal
return IncStrategySignal("HOLD", 0.0)
except Exception as e:
logger.error(f"IncRandomStrategy: Error in get_entry_signal: {e}")
return IncStrategySignal("HOLD", 0.0)
def get_exit_signal(self) -> IncStrategySignal:
"""
Generate random exit signals based on current state.
Returns:
IncStrategySignal: Exit signal with confidence level
"""
if not self._is_warmed_up:
return IncStrategySignal("HOLD", 0.0)
start_time = time.perf_counter()
try:
# Generate random exit signal using strategy's random instance
random_value = self._random.random()
if random_value < self.exit_probability:
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
# Randomly choose exit type
exit_types = ["SELL_SIGNAL", "TAKE_PROFIT", "STOP_LOSS"]
exit_type = self._random.choice(exit_types)
logger.info(f"IncRandomStrategy: Generated EXIT signal at bar {self._bar_count}, "
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
f"type={exit_type}, random_value={random_value:.3f}")
signal = IncStrategySignal(
"EXIT",
confidence=confidence,
price=self._current_price,
metadata={
"type": exit_type,
"strategy": "inc_random",
"bar_count": self._bar_count,
"timeframe": self.timeframe,
"random_value": random_value,
"timestamp": self._last_timestamp
}
)
# Record performance metrics
signal_time = time.perf_counter() - start_time
self._performance_metrics['signal_generation_times'].append(signal_time)
return signal
return IncStrategySignal("HOLD", 0.0)
except Exception as e:
logger.error(f"IncRandomStrategy: Error in get_exit_signal: {e}")
return IncStrategySignal("HOLD", 0.0)
def get_confidence(self) -> float:
"""
Return random confidence level for current market state.
Returns:
float: Random confidence level between min and max confidence
"""
if not self._is_warmed_up:
return 0.0
return self._random.uniform(self.min_confidence, self.max_confidence)
def reset_calculation_state(self) -> None:
"""Reset internal calculation state for reinitialization."""
super().reset_calculation_state()
# Reset random strategy specific state
self._bar_count = 0
self._last_signal_bar = -1
self._current_price = None
self._last_timestamp = None
# Reset random state if seed was provided
random_seed = self.params.get("random_seed")
if random_seed is not None:
self._random.seed(random_seed)
logger.info("IncRandomStrategy: Calculation state reset")
def _reinitialize_from_buffers(self) -> None:
"""
Reinitialize indicators from available buffer data.
For random strategy, we just need to restore the current price
from the latest data point in the buffer.
"""
try:
# Get the latest data point from 1min buffer
buffer_1min = self._timeframe_buffers.get("1min")
if buffer_1min and len(buffer_1min) > 0:
latest_data = buffer_1min[-1]
self._current_price = latest_data['close']
self._last_timestamp = latest_data.get('timestamp')
self._bar_count = len(buffer_1min)
logger.info(f"IncRandomStrategy: Reinitialized from buffer with {self._bar_count} bars")
else:
logger.warning("IncRandomStrategy: No buffer data available for reinitialization")
except Exception as e:
logger.error(f"IncRandomStrategy: Error reinitializing from buffers: {e}")
raise
def get_current_state_summary(self) -> Dict[str, any]:
"""Get summary of current calculation state for debugging."""
base_summary = super().get_current_state_summary()
base_summary.update({
'entry_probability': self.entry_probability,
'exit_probability': self.exit_probability,
'bar_count': self._bar_count,
'last_signal_bar': self._last_signal_bar,
'current_price': self._current_price,
'last_timestamp': self._last_timestamp,
'signal_frequency': self.signal_frequency,
'timeframe': self.timeframe
})
return base_summary
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"IncRandomStrategy(entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
f"mode={self._calculation_mode}, warmed_up={self._is_warmed_up}, "
f"bars={self._bar_count})")

View File

@@ -1,167 +1,332 @@
import pandas as pd
import numpy as np
import time
from cycles.supertrend import Supertrends
from cycles.market_fees import MarketFees
class Backtest:
def __init__(self, initial_usd, df, min1_df, init_strategy_fields) -> None:
self.initial_usd = initial_usd
self.usd = initial_usd
self.max_balance = initial_usd
self.coin = 0
self.position = 0
self.entry_price = 0
self.entry_time = None
self.current_trade_min1_start_idx = None
self.current_min1_end_idx = None
self.price_open = None
self.price_close = None
self.current_date = None
self.strategies = {}
self.df = df
self.min1_df = min1_df
self.trade_log = []
self.drawdowns = []
self.trades = []
self = init_strategy_fields(self)
def run(self, entry_strategy, exit_strategy, debug=False):
@staticmethod
def run(min1_df, df, initial_usd, stop_loss_pct, progress_callback=None, verbose=False):
"""
Runs the backtest using provided entry and exit strategy functions.
The method iterates over the main DataFrame (self.df), simulating trades based on the entry and exit strategies.
It tracks balances, drawdowns, and logs each trade, including fees. At the end, it returns a dictionary of performance statistics.
Backtest a simple strategy using the meta supertrend (all three supertrends agree).
Buys when meta supertrend is positive, sells when negative, applies a percentage stop loss.
Parameters:
- entry_strategy: function, determines when to enter a trade. Should accept (self, i) and return True to enter.
- exit_strategy: function, determines when to exit a trade. Should accept (self, i) and return (exit_reason, sell_price) or (None, None) to hold.
- debug: bool, whether to print debug info (default: False)
Returns:
- dict with keys: initial_usd, final_usd, n_trades, win_rate, max_drawdown, avg_trade, trade_log, trades, total_fees_usd, and optionally first_trade and last_trade.
- min1_df: pandas DataFrame, 1-minute timeframe data for more accurate stop loss checking (optional)
- df: pandas DataFrame, main timeframe data for signals
- initial_usd: float, starting USD amount
- stop_loss_pct: float, stop loss as a fraction (e.g. 0.05 for 5%)
- progress_callback: callable, optional callback function to report progress (current_step)
- verbose: bool, enable debug logging for stop loss checks
"""
_df = df.copy().reset_index()
for i in range(1, len(self.df)):
self.price_open = self.df['open'].iloc[i]
self.price_close = self.df['close'].iloc[i]
# Ensure we have a timestamp column regardless of original index name
if 'timestamp' not in _df.columns:
# If reset_index() created a column with the original index name, rename it
if len(_df.columns) > 0 and _df.columns[0] not in ['open', 'high', 'low', 'close', 'volume', 'predicted_close_price']:
_df = _df.rename(columns={_df.columns[0]: 'timestamp'})
else:
raise ValueError("Unable to identify timestamp column in DataFrame")
self.current_date = self.df['timestamp'].iloc[i]
_df['timestamp'] = pd.to_datetime(_df['timestamp'])
# check if we are in buy/sell position
if self.position == 0:
if entry_strategy(self, i):
self.handle_entry()
elif self.position == 1:
exit_test_results, sell_price = exit_strategy(self, i)
supertrends = Supertrends(_df, verbose=False, close_column='predicted_close_price')
if exit_test_results is not None:
self.handle_exit(exit_test_results, sell_price)
supertrend_results_list = supertrends.calculate_supertrend_indicators()
trends = [st['results']['trend'] for st in supertrend_results_list]
trends_arr = np.stack(trends, axis=1)
meta_trend = np.where((trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
trends_arr[:,0], 0)
# Shift meta_trend by one to avoid lookahead bias
meta_trend_signal = np.roll(meta_trend, 1)
meta_trend_signal[0] = 0 # or np.nan, but 0 means 'no signal' for first bar
position = 0 # 0 = no position, 1 = long
entry_price = 0
usd = initial_usd
coin = 0
trade_log = []
max_balance = initial_usd
drawdowns = []
trades = []
entry_time = None
stop_loss_count = 0 # Track number of stop losses
# Ensure min1_df has proper DatetimeIndex
if min1_df is not None and not min1_df.empty:
min1_df.index = pd.to_datetime(min1_df.index)
for i in range(1, len(_df)):
# Report progress if callback is provided
if progress_callback:
# Update more frequently for better responsiveness
update_frequency = max(1, len(_df) // 50) # Update every 2% of dataset (50 updates total)
if i % update_frequency == 0 or i == len(_df) - 1: # Always update on last iteration
if verbose: # Only print in verbose mode to avoid spam
print(f"DEBUG: Progress callback called with i={i}, total={len(_df)-1}")
progress_callback(i)
price_open = _df['open'].iloc[i]
price_close = _df['close'].iloc[i]
date = _df['timestamp'].iloc[i]
prev_mt = meta_trend_signal[i-1]
curr_mt = meta_trend_signal[i]
# Check stop loss if in position
if position == 1:
stop_loss_result = Backtest.check_stop_loss(
min1_df,
entry_time,
date,
entry_price,
stop_loss_pct,
coin,
verbose=verbose
)
if stop_loss_result is not None:
trade_log_entry, position, coin, entry_price, usd = stop_loss_result
trade_log.append(trade_log_entry)
stop_loss_count += 1
continue
# Entry: only if not in position and signal changes to 1
if position == 0 and prev_mt != 1 and curr_mt == 1:
entry_result = Backtest.handle_entry(usd, price_open, date)
coin, entry_price, entry_time, usd, position, trade_log_entry = entry_result
trade_log.append(trade_log_entry)
# Exit: only if in position and signal changes from 1 to -1
elif position == 1 and prev_mt == 1 and curr_mt == -1:
exit_result = Backtest.handle_exit(coin, price_open, entry_price, entry_time, date)
usd, coin, position, entry_price, trade_log_entry = exit_result
trade_log.append(trade_log_entry)
# Track drawdown
balance = self.usd if self.position == 0 else self.coin * self.price_close
balance = usd if position == 0 else coin * price_close
if balance > max_balance:
max_balance = balance
drawdown = (max_balance - balance) / max_balance
drawdowns.append(drawdown)
if balance > self.max_balance:
self.max_balance = balance
drawdown = (self.max_balance - balance) / self.max_balance
self.drawdowns.append(drawdown)
# Report completion if callback is provided
if progress_callback:
progress_callback(len(_df) - 1)
# If still in position at end, sell at last close
if self.position == 1:
self.handle_exit("EOD", None)
if position == 1:
exit_result = Backtest.handle_exit(coin, _df['close'].iloc[-1], entry_price, entry_time, _df['timestamp'].iloc[-1])
usd, coin, position, entry_price, trade_log_entry = exit_result
trade_log.append(trade_log_entry)
# Calculate statistics
final_balance = self.usd
n_trades = len(self.trade_log)
wins = [1 for t in self.trade_log if t['exit'] is not None and t['exit'] > t['entry']]
final_balance = usd
n_trades = len(trade_log)
wins = [1 for t in trade_log if t['exit'] is not None and t['exit'] > t['entry']]
win_rate = len(wins) / n_trades if n_trades > 0 else 0
max_drawdown = max(self.drawdowns) if self.drawdowns else 0
avg_trade = np.mean([t['exit']/t['entry']-1 for t in self.trade_log if t['exit'] is not None]) if self.trade_log else 0
max_drawdown = max(drawdowns) if drawdowns else 0
avg_trade = np.mean([t['exit']/t['entry']-1 for t in trade_log if t['exit'] is not None]) if trade_log else 0
trades = []
total_fees_usd = 0.0
for trade in self.trade_log:
for trade in trade_log:
if trade['exit'] is not None:
profit_pct = (trade['exit'] - trade['entry']) / trade['entry']
else:
profit_pct = 0.0
# Validate fee_usd field
if 'fee_usd' not in trade:
raise ValueError(f"Trade missing required field 'fee_usd': {trade}")
fee_usd = trade['fee_usd']
if fee_usd is None:
raise ValueError(f"Trade fee_usd is None: {trade}")
# Validate trade type field
if 'type' not in trade:
raise ValueError(f"Trade missing required field 'type': {trade}")
trade_type = trade['type']
if trade_type is None:
raise ValueError(f"Trade type is None: {trade}")
trades.append({
'entry_time': trade['entry_time'],
'exit_time': trade['exit_time'],
'entry': trade['entry'],
'exit': trade['exit'],
'profit_pct': profit_pct,
'type': trade['type'],
'fee_usd': trade['fee_usd']
'type': trade_type,
'fee_usd': fee_usd
})
fee_usd = trade.get('fee_usd')
total_fees_usd += fee_usd
results = {
"initial_usd": self.initial_usd,
"initial_usd": initial_usd,
"final_usd": final_balance,
"n_trades": n_trades,
"n_stop_loss": stop_loss_count, # Add stop loss count
"win_rate": win_rate,
"max_drawdown": max_drawdown,
"avg_trade": avg_trade,
"trade_log": self.trade_log,
"trade_log": trade_log,
"trades": trades,
"total_fees_usd": total_fees_usd,
}
if n_trades > 0:
results["first_trade"] = {
"entry_time": self.trade_log[0]['entry_time'],
"entry": self.trade_log[0]['entry']
"entry_time": trade_log[0]['entry_time'],
"entry": trade_log[0]['entry']
}
results["last_trade"] = {
"exit_time": self.trade_log[-1]['exit_time'],
"exit": self.trade_log[-1]['exit']
"exit_time": trade_log[-1]['exit_time'],
"exit": trade_log[-1]['exit']
}
return results
def handle_entry(self):
entry_fee = MarketFees.calculate_okx_taker_maker_fee(self.usd, is_maker=False)
usd_after_fee = self.usd - entry_fee
@staticmethod
def check_stop_loss(min1_df, entry_time, current_time, entry_price, stop_loss_pct, coin, verbose=False):
"""
Check if stop loss should be triggered based on 1-minute data
self.coin = usd_after_fee / self.price_open
self.entry_price = self.price_open
self.entry_time = self.current_date
self.usd = 0
self.position = 1
Args:
min1_df: 1-minute DataFrame with DatetimeIndex
entry_time: Entry timestamp
current_time: Current timestamp
entry_price: Entry price
stop_loss_pct: Stop loss percentage (e.g. 0.05 for 5%)
coin: Current coin position
verbose: Enable debug logging
Returns:
Tuple of (trade_log_entry, position, coin, entry_price, usd) if stop loss triggered, None otherwise
"""
if min1_df is None or min1_df.empty:
if verbose:
print("Warning: No 1-minute data available for stop loss checking")
return None
stop_price = entry_price * (1 - stop_loss_pct)
try:
# Ensure min1_df has a DatetimeIndex
if not isinstance(min1_df.index, pd.DatetimeIndex):
if verbose:
print("Warning: min1_df does not have DatetimeIndex")
return None
# Convert entry_time and current_time to pandas Timestamps for comparison
entry_ts = pd.to_datetime(entry_time)
current_ts = pd.to_datetime(current_time)
if verbose:
print(f"Checking stop loss from {entry_ts} to {current_ts}, stop_price: {stop_price:.2f}")
# Handle edge case where entry and current time are the same (1-minute timeframe)
if entry_ts == current_ts:
if verbose:
print("Entry and current time are the same, no range to check")
return None
# Find the range of 1-minute data to check (exclusive of entry time, inclusive of current time)
# We start from the candle AFTER entry to avoid checking the entry candle itself
start_check_time = entry_ts + pd.Timedelta(minutes=1)
# Get the slice of data to check for stop loss
mask = (min1_df.index > entry_ts) & (min1_df.index <= current_ts)
min1_slice = min1_df.loc[mask]
if len(min1_slice) == 0:
if verbose:
print(f"No 1-minute data found between {start_check_time} and {current_ts}")
return None
if verbose:
print(f"Checking {len(min1_slice)} candles for stop loss")
# Check if any low price in the slice hits the stop loss
stop_triggered = (min1_slice['low'] <= stop_price).any()
if stop_triggered:
# Find the exact candle where stop loss was triggered
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
if verbose:
print(f"Stop loss triggered at {stop_candle.name}, low: {stop_candle['low']:.2f}")
# More realistic fill: if open < stop, fill at open, else at stop
if stop_candle['open'] < stop_price:
sell_price = stop_candle['open']
if verbose:
print(f"Filled at open price: {sell_price:.2f}")
else:
sell_price = stop_price
if verbose:
print(f"Filled at stop price: {sell_price:.2f}")
btc_to_sell = coin
usd_gross = btc_to_sell * sell_price
exit_fee = MarketFees.calculate_okx_taker_maker_fee(usd_gross, is_maker=False)
usd_after_stop = usd_gross - exit_fee
trade_log_entry = {
'type': 'STOP',
'entry': entry_price,
'exit': sell_price,
'entry_time': entry_time,
'exit_time': stop_candle.name,
'fee_usd': exit_fee
}
# After stop loss, reset position and entry, return USD balance
return trade_log_entry, 0, 0, 0, usd_after_stop
elif verbose:
print(f"No stop loss triggered, min low in range: {min1_slice['low'].min():.2f}")
except Exception as e:
# In case of any error, don't trigger stop loss but log the issue
error_msg = f"Warning: Stop loss check failed: {e}"
print(error_msg)
if verbose:
import traceback
print(traceback.format_exc())
return None
return None
@staticmethod
def handle_entry(usd, price_open, date):
entry_fee = MarketFees.calculate_okx_taker_maker_fee(usd, is_maker=False)
usd_after_fee = usd - entry_fee
coin = usd_after_fee / price_open
entry_price = price_open
entry_time = date
usd = 0
position = 1
trade_log_entry = {
'type': 'BUY',
'entry': self.entry_price,
'entry': entry_price,
'exit': None,
'entry_time': self.entry_time,
'entry_time': entry_time,
'exit_time': None,
'fee_usd': entry_fee
}
self.trade_log.append(trade_log_entry)
return coin, entry_price, entry_time, usd, position, trade_log_entry
def handle_exit(self, exit_reason, sell_price):
btc_to_sell = self.coin
exit_price = sell_price if sell_price is not None else self.price_open
usd_gross = btc_to_sell * exit_price
@staticmethod
def handle_exit(coin, price_open, entry_price, entry_time, date):
btc_to_sell = coin
usd_gross = btc_to_sell * price_open
exit_fee = MarketFees.calculate_okx_taker_maker_fee(usd_gross, is_maker=False)
self.usd = usd_gross - exit_fee
exit_log_entry = {
'type': exit_reason,
'entry': self.entry_price,
'exit': exit_price,
'entry_time': self.entry_time,
'exit_time': self.current_date,
usd = usd_gross - exit_fee
trade_log_entry = {
'type': 'SELL',
'entry': entry_price,
'exit': price_open,
'entry_time': entry_time,
'exit_time': date,
'fee_usd': exit_fee
}
self.coin = 0
self.position = 0
self.entry_price = 0
self.trade_log.append(exit_log_entry)
coin = 0
position = 0
entry_price = 0
return usd, coin, position, entry_price, trade_log_entry

View File

@@ -1,453 +1,86 @@
import os
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
class BacktestCharts:
@staticmethod
def plot(df, meta_trend):
def __init__(self, charts_dir="charts"):
self.charts_dir = charts_dir
os.makedirs(self.charts_dir, exist_ok=True)
def plot_profit_ratio_vs_stop_loss(self, results, filename="profit_ratio_vs_stop_loss.png"):
"""
Plot close price line chart with a bar at the bottom: green when trend is 1, red when trend is 0.
The bar stays at the bottom even when zooming/panning.
- df: DataFrame with columns ['close', ...] and a datetime index or 'timestamp' column.
- meta_trend: array-like, same length as df, values 1 (green) or 0 (red).
Plots profit ratio vs stop loss percentage for each timeframe.
Parameters:
- results: list of dicts, each with keys: 'timeframe', 'stop_loss_pct', 'profit_ratio'
- filename: output filename (will be saved in charts_dir)
"""
fig, (ax_price, ax_bar) = plt.subplots(
nrows=2, ncols=1, figsize=(16, 8), sharex=True,
gridspec_kw={'height_ratios': [12, 1]}
)
sns.lineplot(x=df.index, y=df['close'], label='Close Price', color='blue', ax=ax_price)
ax_price.set_title('Close Price with Trend Bar (Green=1, Red=0)')
ax_price.set_ylabel('Price')
ax_price.grid(True, alpha=0.3)
ax_price.legend()
# Clean meta_trend: ensure only 0/1, handle NaNs by forward-fill then fill remaining with 0
meta_trend_arr = np.asarray(meta_trend)
if not np.issubdtype(meta_trend_arr.dtype, np.number):
meta_trend_arr = pd.Series(meta_trend_arr).astype(float).to_numpy()
if np.isnan(meta_trend_arr).any():
meta_trend_arr = pd.Series(meta_trend_arr).fillna(method='ffill').fillna(0).astype(int).to_numpy()
else:
meta_trend_arr = meta_trend_arr.astype(int)
meta_trend_arr = np.where(meta_trend_arr != 1, 0, 1) # force only 0 or 1
if hasattr(df.index, 'to_numpy'):
x_vals = df.index.to_numpy()
else:
x_vals = np.array(df.index)
# Find contiguous regions
regions = []
start = 0
for i in range(1, len(meta_trend_arr)):
if meta_trend_arr[i] != meta_trend_arr[i-1]:
regions.append((start, i-1, meta_trend_arr[i-1]))
start = i
regions.append((start, len(meta_trend_arr)-1, meta_trend_arr[-1]))
# Draw red vertical lines at the start of each new region (except the first)
for region_idx in range(1, len(regions)):
region_start = regions[region_idx][0]
ax_price.axvline(x=x_vals[region_start], color='black', linestyle='--', alpha=0.7, linewidth=1)
for start, end, trend in regions:
color = '#089981' if trend == 1 else '#F23645'
# Offset by 1 on x: span from x_vals[start] to x_vals[end+1] if possible
x_start = x_vals[start]
x_end = x_vals[end+1] if end+1 < len(x_vals) else x_vals[end]
ax_bar.axvspan(x_start, x_end, color=color, alpha=1, ymin=0, ymax=1)
ax_bar.set_ylim(0, 1)
ax_bar.set_yticks([])
ax_bar.set_ylabel('Trend')
ax_bar.set_xlabel('Time')
ax_bar.grid(False)
ax_bar.set_title('Meta Trend')
plt.tight_layout(h_pad=0.1)
plt.show()
@staticmethod
def format_strategy_data_with_trades(strategy_data, backtest_results):
"""
Format strategy data for universal plotting with actual executed trades.
Converts strategy output into the expected column format: "x_type_name"
Args:
strategy_data (DataFrame): Output from strategy with columns like 'close', 'UpperBand', 'LowerBand', 'RSI'
backtest_results (dict): Results from backtest.run() containing actual executed trades
Returns:
DataFrame: Formatted data ready for plot_data function
"""
formatted_df = pd.DataFrame(index=strategy_data.index)
# Plot 1: Price data with Bollinger Bands and actual trade signals
if 'close' in strategy_data.columns:
formatted_df['1_line_close'] = strategy_data['close']
# Bollinger Bands area (prefer standard names, fallback to timeframe-specific)
upper_band_col = None
lower_band_col = None
sma_col = None
# Check for standard BB columns first
if 'UpperBand' in strategy_data.columns and 'LowerBand' in strategy_data.columns:
upper_band_col = 'UpperBand'
lower_band_col = 'LowerBand'
# Check for 15m BB columns
elif 'UpperBand_15m' in strategy_data.columns and 'LowerBand_15m' in strategy_data.columns:
upper_band_col = 'UpperBand_15m'
lower_band_col = 'LowerBand_15m'
if upper_band_col and lower_band_col:
formatted_df['1_area_bb_upper'] = strategy_data[upper_band_col]
formatted_df['1_area_bb_lower'] = strategy_data[lower_band_col]
# SMA/Moving Average line
if 'SMA' in strategy_data.columns:
sma_col = 'SMA'
elif 'SMA_15m' in strategy_data.columns:
sma_col = 'SMA_15m'
if sma_col:
formatted_df['1_line_sma'] = strategy_data[sma_col]
# Strategy buy/sell signals (all signals from strategy) as smaller scatter points
if 'BuySignal' in strategy_data.columns and 'close' in strategy_data.columns:
strategy_buy_points = strategy_data['close'].where(strategy_data['BuySignal'], np.nan)
formatted_df['1_scatter_strategy_buy'] = strategy_buy_points
if 'SellSignal' in strategy_data.columns and 'close' in strategy_data.columns:
strategy_sell_points = strategy_data['close'].where(strategy_data['SellSignal'], np.nan)
formatted_df['1_scatter_strategy_sell'] = strategy_sell_points
# Actual executed trades from backtest results (larger, more prominent)
if 'trades' in backtest_results and backtest_results['trades']:
# Create series for buy and sell points
buy_points = pd.Series(np.nan, index=strategy_data.index)
sell_points = pd.Series(np.nan, index=strategy_data.index)
for trade in backtest_results['trades']:
entry_time = trade.get('entry_time')
exit_time = trade.get('exit_time')
entry_price = trade.get('entry')
exit_price = trade.get('exit')
# Find closest index for entry time
if entry_time is not None and entry_price is not None:
try:
if isinstance(entry_time, str):
entry_time = pd.to_datetime(entry_time)
# Find the closest index to entry_time
closest_entry_idx = strategy_data.index.get_indexer([entry_time], method='nearest')[0]
if closest_entry_idx >= 0:
buy_points.iloc[closest_entry_idx] = entry_price
except (ValueError, IndexError, TypeError):
pass # Skip if can't find matching time
# Find closest index for exit time
if exit_time is not None and exit_price is not None:
try:
if isinstance(exit_time, str):
exit_time = pd.to_datetime(exit_time)
# Find the closest index to exit_time
closest_exit_idx = strategy_data.index.get_indexer([exit_time], method='nearest')[0]
if closest_exit_idx >= 0:
sell_points.iloc[closest_exit_idx] = exit_price
except (ValueError, IndexError, TypeError):
pass # Skip if can't find matching time
formatted_df['1_scatter_actual_buy'] = buy_points
formatted_df['1_scatter_actual_sell'] = sell_points
# Stop Loss and Take Profit levels
if 'StopLoss' in strategy_data.columns:
formatted_df['1_line_stop_loss'] = strategy_data['StopLoss']
if 'TakeProfit' in strategy_data.columns:
formatted_df['1_line_take_profit'] = strategy_data['TakeProfit']
# Plot 2: RSI
rsi_col = None
if 'RSI' in strategy_data.columns:
rsi_col = 'RSI'
elif 'RSI_15m' in strategy_data.columns:
rsi_col = 'RSI_15m'
if rsi_col:
formatted_df['2_line_rsi'] = strategy_data[rsi_col]
# Add RSI overbought/oversold levels
formatted_df['2_line_rsi_overbought'] = 70
formatted_df['2_line_rsi_oversold'] = 30
# Plot 3: Volume (if available)
if 'volume' in strategy_data.columns:
formatted_df['3_bar_volume'] = strategy_data['volume']
# Add volume moving average if available
if 'VolumeMA_15m' in strategy_data.columns:
formatted_df['3_line_volume_ma'] = strategy_data['VolumeMA_15m']
return formatted_df
@staticmethod
def format_strategy_data(strategy_data):
"""
Format strategy data for universal plotting (without trade signals).
Converts strategy output into the expected column format: "x_type_name"
Args:
strategy_data (DataFrame): Output from strategy with columns like 'close', 'UpperBand', 'LowerBand', 'RSI'
Returns:
DataFrame: Formatted data ready for plot_data function
"""
formatted_df = pd.DataFrame(index=strategy_data.index)
# Plot 1: Price data with Bollinger Bands
if 'close' in strategy_data.columns:
formatted_df['1_line_close'] = strategy_data['close']
# Bollinger Bands area (prefer standard names, fallback to timeframe-specific)
upper_band_col = None
lower_band_col = None
sma_col = None
# Check for standard BB columns first
if 'UpperBand' in strategy_data.columns and 'LowerBand' in strategy_data.columns:
upper_band_col = 'UpperBand'
lower_band_col = 'LowerBand'
# Check for 15m BB columns
elif 'UpperBand_15m' in strategy_data.columns and 'LowerBand_15m' in strategy_data.columns:
upper_band_col = 'UpperBand_15m'
lower_band_col = 'LowerBand_15m'
if upper_band_col and lower_band_col:
formatted_df['1_area_bb_upper'] = strategy_data[upper_band_col]
formatted_df['1_area_bb_lower'] = strategy_data[lower_band_col]
# SMA/Moving Average line
if 'SMA' in strategy_data.columns:
sma_col = 'SMA'
elif 'SMA_15m' in strategy_data.columns:
sma_col = 'SMA_15m'
if sma_col:
formatted_df['1_line_sma'] = strategy_data[sma_col]
# Stop Loss and Take Profit levels
if 'StopLoss' in strategy_data.columns:
formatted_df['1_line_stop_loss'] = strategy_data['StopLoss']
if 'TakeProfit' in strategy_data.columns:
formatted_df['1_line_take_profit'] = strategy_data['TakeProfit']
# Plot 2: RSI
rsi_col = None
if 'RSI' in strategy_data.columns:
rsi_col = 'RSI'
elif 'RSI_15m' in strategy_data.columns:
rsi_col = 'RSI_15m'
if rsi_col:
formatted_df['2_line_rsi'] = strategy_data[rsi_col]
# Add RSI overbought/oversold levels
formatted_df['2_line_rsi_overbought'] = 70
formatted_df['2_line_rsi_oversold'] = 30
# Plot 3: Volume (if available)
if 'volume' in strategy_data.columns:
formatted_df['3_bar_volume'] = strategy_data['volume']
# Add volume moving average if available
if 'VolumeMA_15m' in strategy_data.columns:
formatted_df['3_line_volume_ma'] = strategy_data['VolumeMA_15m']
return formatted_df
@staticmethod
def plot_data(df):
"""
Universal plot function for any formatted data.
- df: DataFrame with column names in format "x_type_name" where:
x = plot number (subplot)
type = plot type (line, area, scatter, bar, etc.)
name = descriptive name for the data series
"""
if df.empty:
print("No data to plot")
return
# Parse all columns
plot_info = []
for column in df.columns:
parts = column.split('_', 2) # Split into max 3 parts
if len(parts) < 3:
print(f"Warning: Skipping column '{column}' - invalid format. Expected 'x_type_name'")
continue
try:
plot_number = int(parts[0])
plot_type = parts[1].lower()
plot_name = parts[2]
plot_info.append((plot_number, plot_type, plot_name, column))
except ValueError:
print(f"Warning: Skipping column '{column}' - invalid plot number")
continue
if not plot_info:
print("No valid columns found for plotting")
return
# Group by plot number
plots = {}
for plot_num, plot_type, plot_name, column in plot_info:
if plot_num not in plots:
plots[plot_num] = []
plots[plot_num].append((plot_type, plot_name, column))
# Sort plot numbers
plot_numbers = sorted(plots.keys())
n_plots = len(plot_numbers)
# Create subplots
fig, axs = plt.subplots(n_plots, 1, figsize=(16, 6 * n_plots), sharex=True)
if n_plots == 1:
axs = [axs] # Ensure axs is always a list
# Plot each subplot
for i, plot_num in enumerate(plot_numbers):
ax = axs[i]
plot_items = plots[plot_num]
# Handle Bollinger Bands area first (needs special handling)
bb_upper = None
bb_lower = None
for plot_type, plot_name, column in plot_items:
if plot_type == 'area' and 'bb_upper' in plot_name:
bb_upper = df[column]
elif plot_type == 'area' and 'bb_lower' in plot_name:
bb_lower = df[column]
# Plot Bollinger Bands area if both bounds exist
if bb_upper is not None and bb_lower is not None:
ax.fill_between(df.index, bb_upper, bb_lower, alpha=0.2, color='gray', label='Bollinger Bands')
# Plot other items
for plot_type, plot_name, column in plot_items:
if plot_type == 'area' and ('bb_upper' in plot_name or 'bb_lower' in plot_name):
continue # Already handled above
data = df[column].dropna() # Remove NaN values for cleaner plots
if plot_type == 'line':
color = None
linestyle = '-'
alpha = 1.0
# Special styling for different line types
if 'overbought' in plot_name:
color = 'red'
linestyle = '--'
alpha = 0.7
elif 'oversold' in plot_name:
color = 'green'
linestyle = '--'
alpha = 0.7
elif 'stop_loss' in plot_name:
color = 'red'
linestyle = ':'
alpha = 0.8
elif 'take_profit' in plot_name:
color = 'green'
linestyle = ':'
alpha = 0.8
elif 'sma' in plot_name:
color = 'orange'
alpha = 0.8
elif 'volume_ma' in plot_name:
color = 'purple'
alpha = 0.7
ax.plot(data.index, data, label=plot_name.replace('_', ' ').title(),
color=color, linestyle=linestyle, alpha=alpha)
elif plot_type == 'scatter':
color = 'green' if 'buy' in plot_name else 'red' if 'sell' in plot_name else 'blue'
marker = '^' if 'buy' in plot_name else 'v' if 'sell' in plot_name else 'o'
size = 100 if 'buy' in plot_name or 'sell' in plot_name else 50
alpha = 0.8
zorder = 5
label_name = plot_name.replace('_', ' ').title()
# Special styling for different signal types
if 'actual_buy' in plot_name:
color = 'darkgreen'
marker = '^'
size = 120
alpha = 1.0
zorder = 10 # Higher z-order to appear on top
label_name = 'Actual Buy Trades'
elif 'actual_sell' in plot_name:
color = 'darkred'
marker = 'v'
size = 120
alpha = 1.0
zorder = 10 # Higher z-order to appear on top
label_name = 'Actual Sell Trades'
elif 'strategy_buy' in plot_name:
color = 'lightgreen'
marker = '^'
size = 60
alpha = 0.6
zorder = 3 # Lower z-order to appear behind actual trades
label_name = 'Strategy Buy Signals'
elif 'strategy_sell' in plot_name:
color = 'lightcoral'
marker = 'v'
size = 60
alpha = 0.6
zorder = 3 # Lower z-order to appear behind actual trades
label_name = 'Strategy Sell Signals'
ax.scatter(data.index, data, label=label_name,
color=color, marker=marker, s=size, alpha=alpha, zorder=zorder)
elif plot_type == 'area':
ax.fill_between(data.index, data, alpha=0.5, label=plot_name.replace('_', ' ').title())
elif plot_type == 'bar':
ax.bar(data.index, data, alpha=0.7, label=plot_name.replace('_', ' ').title())
else:
print(f"Warning: Plot type '{plot_type}' not supported for column '{column}'")
# Customize subplot
ax.grid(True, alpha=0.3)
ax.legend()
# Set titles and labels
if plot_num == 1:
ax.set_title('Price Chart with Bollinger Bands and Signals')
ax.set_ylabel('Price')
elif plot_num == 2:
ax.set_title('RSI Indicator')
ax.set_ylabel('RSI')
ax.set_ylim(0, 100)
elif plot_num == 3:
ax.set_title('Volume')
ax.set_ylabel('Volume')
else:
ax.set_title(f'Plot {plot_num}')
# Set x-axis label only on the bottom subplot
axs[-1].set_xlabel('Time')
# Organize data by timeframe
from collections import defaultdict
data = defaultdict(lambda: {"stop_loss_pct": [], "profit_ratio": []})
for row in results:
tf = row["timeframe"]
data[tf]["stop_loss_pct"].append(row["stop_loss_pct"])
data[tf]["profit_ratio"].append(row["profit_ratio"])
plt.figure(figsize=(10, 6))
for tf, vals in data.items():
# Sort by stop_loss_pct for smooth lines
sorted_pairs = sorted(zip(vals["stop_loss_pct"], vals["profit_ratio"]))
stop_loss, profit_ratio = zip(*sorted_pairs)
plt.plot(
[s * 100 for s in stop_loss], # Convert to percent
profit_ratio,
marker="o",
label=tf
)
plt.xlabel("Stop Loss (%)")
plt.ylabel("Profit Ratio")
plt.title("Profit Ratio vs Stop Loss (%) per Timeframe")
plt.legend(title="Timeframe")
plt.grid(True, linestyle="--", alpha=0.5)
plt.tight_layout()
plt.show()
output_path = os.path.join(self.charts_dir, filename)
plt.savefig(output_path)
plt.close()
def plot_average_trade_vs_stop_loss(self, results, filename="average_trade_vs_stop_loss.png"):
"""
Plots average trade vs stop loss percentage for each timeframe.
Parameters:
- results: list of dicts, each with keys: 'timeframe', 'stop_loss_pct', 'average_trade'
- filename: output filename (will be saved in charts_dir)
"""
from collections import defaultdict
data = defaultdict(lambda: {"stop_loss_pct": [], "average_trade": []})
for row in results:
tf = row["timeframe"]
if "average_trade" not in row:
continue # Skip rows without average_trade
data[tf]["stop_loss_pct"].append(row["stop_loss_pct"])
data[tf]["average_trade"].append(row["average_trade"])
plt.figure(figsize=(10, 6))
for tf, vals in data.items():
# Sort by stop_loss_pct for smooth lines
sorted_pairs = sorted(zip(vals["stop_loss_pct"], vals["average_trade"]))
stop_loss, average_trade = zip(*sorted_pairs)
plt.plot(
[s * 100 for s in stop_loss], # Convert to percent
average_trade,
marker="o",
label=tf
)
plt.xlabel("Stop Loss (%)")
plt.ylabel("Average Trade")
plt.title("Average Trade vs Stop Loss (%) per Timeframe")
plt.legend(title="Timeframe")
plt.grid(True, linestyle="--", alpha=0.5)
plt.tight_layout()
output_path = os.path.join(self.charts_dir, filename)
plt.savefig(output_path)
plt.close()

View File

@@ -2,6 +2,6 @@ import pandas as pd
class MarketFees:
@staticmethod
def calculate_okx_taker_maker_fee(amount, is_maker=True) -> float:
def calculate_okx_taker_maker_fee(amount, is_maker=True):
fee_rate = 0.0008 if is_maker else 0.0010
return amount * fee_rate

View File

@@ -1,42 +0,0 @@
"""
Strategies Module
This module contains the strategy management system for trading strategies.
It provides a flexible framework for implementing, combining, and managing multiple trading strategies.
Components:
- StrategyBase: Abstract base class for all strategies
- DefaultStrategy: Meta-trend based strategy
- BBRSStrategy: Bollinger Bands + RSI strategy
- StrategyManager: Orchestrates multiple strategies
- StrategySignal: Represents trading signals with confidence levels
Usage:
from cycles.strategies import StrategyManager, create_strategy_manager
# Create strategy manager from config
strategy_manager = create_strategy_manager(config)
# Or create individual strategies
from cycles.strategies import DefaultStrategy, BBRSStrategy
default_strategy = DefaultStrategy(weight=1.0, params={})
"""
from .base import StrategyBase, StrategySignal
from .default_strategy import DefaultStrategy
from .bbrs_strategy import BBRSStrategy
from .random_strategy import RandomStrategy
from .manager import StrategyManager, create_strategy_manager
__all__ = [
'StrategyBase',
'StrategySignal',
'DefaultStrategy',
'BBRSStrategy',
'RandomStrategy',
'StrategyManager',
'create_strategy_manager'
]
__version__ = '1.0.0'
__author__ = 'TCP Cycles Team'

View File

@@ -1,250 +0,0 @@
"""
Base classes for the strategy management system.
This module contains the fundamental building blocks for all trading strategies:
- StrategySignal: Represents trading signals with confidence and metadata
- StrategyBase: Abstract base class that all strategies must inherit from
"""
import pandas as pd
from abc import ABC, abstractmethod
from typing import Dict, Optional, List, Union
class StrategySignal:
"""
Represents a trading signal from a strategy.
A signal encapsulates the strategy's recommendation along with confidence
level, optional price target, and additional metadata.
Attributes:
signal_type (str): Type of signal - "ENTRY", "EXIT", or "HOLD"
confidence (float): Confidence level from 0.0 to 1.0
price (Optional[float]): Optional specific price for the signal
metadata (Dict): Additional signal data and context
Example:
# Entry signal with high confidence
signal = StrategySignal("ENTRY", confidence=0.8)
# Exit signal with stop loss price
signal = StrategySignal("EXIT", confidence=1.0, price=50000,
metadata={"type": "STOP_LOSS"})
"""
def __init__(self, signal_type: str, confidence: float = 1.0,
price: Optional[float] = None, metadata: Optional[Dict] = None):
"""
Initialize a strategy signal.
Args:
signal_type: Type of signal ("ENTRY", "EXIT", "HOLD")
confidence: Confidence level (0.0 to 1.0)
price: Optional specific price for the signal
metadata: Additional signal data and context
"""
self.signal_type = signal_type
self.confidence = max(0.0, min(1.0, confidence)) # Clamp to [0,1]
self.price = price
self.metadata = metadata or {}
def __repr__(self) -> str:
"""String representation of the signal."""
return (f"StrategySignal(type={self.signal_type}, "
f"confidence={self.confidence:.2f}, "
f"price={self.price}, metadata={self.metadata})")
class StrategyBase(ABC):
"""
Abstract base class for all trading strategies.
This class defines the interface that all strategies must implement:
- get_timeframes(): Specify required timeframes for the strategy
- initialize(): Setup strategy with backtester data
- get_entry_signal(): Generate entry signals
- get_exit_signal(): Generate exit signals
- get_confidence(): Optional confidence calculation
Attributes:
name (str): Strategy name
weight (float): Strategy weight for combination
params (Dict): Strategy parameters
initialized (bool): Whether strategy has been initialized
timeframes_data (Dict): Resampled data for different timeframes
Example:
class MyStrategy(StrategyBase):
def get_timeframes(self):
return ["15min"] # This strategy works on 15-minute data
def initialize(self, backtester):
# Setup strategy indicators using self.timeframes_data["15min"]
self.initialized = True
def get_entry_signal(self, backtester, df_index):
# Return StrategySignal based on analysis
if should_enter:
return StrategySignal("ENTRY", confidence=0.7)
return StrategySignal("HOLD", confidence=0.0)
"""
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
"""
Initialize the strategy base.
Args:
name: Strategy name/identifier
weight: Strategy weight for combination (default: 1.0)
params: Strategy-specific parameters
"""
self.name = name
self.weight = weight
self.params = params or {}
self.initialized = False
self.timeframes_data = {} # Will store resampled data for each timeframe
def get_timeframes(self) -> List[str]:
"""
Get the list of timeframes required by this strategy.
Override this method to specify which timeframes your strategy needs.
The base class will automatically resample the 1-minute data to these timeframes
and make them available in self.timeframes_data.
Returns:
List[str]: List of timeframe strings (e.g., ["1min", "15min", "1h"])
Example:
def get_timeframes(self):
return ["15min"] # Strategy needs 15-minute data
def get_timeframes(self):
return ["5min", "15min", "1h"] # Multi-timeframe strategy
"""
return ["1min"] # Default to 1-minute data
def _resample_data(self, original_data: pd.DataFrame) -> None:
"""
Resample the original 1-minute data to all required timeframes.
This method is called automatically during initialization to create
resampled versions of the data for each timeframe the strategy needs.
Args:
original_data: Original 1-minute OHLCV data with DatetimeIndex
"""
self.timeframes_data = {}
for timeframe in self.get_timeframes():
if timeframe == "1min":
# For 1-minute data, just use the original
self.timeframes_data[timeframe] = original_data.copy()
else:
# Resample to the specified timeframe
resampled = original_data.resample(timeframe).agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
}).dropna()
self.timeframes_data[timeframe] = resampled
def get_data_for_timeframe(self, timeframe: str) -> Optional[pd.DataFrame]:
"""
Get resampled data for a specific timeframe.
Args:
timeframe: Timeframe string (e.g., "15min", "1h")
Returns:
pd.DataFrame: Resampled OHLCV data or None if timeframe not available
"""
return self.timeframes_data.get(timeframe)
def get_primary_timeframe_data(self) -> pd.DataFrame:
"""
Get data for the primary (first) timeframe.
Returns:
pd.DataFrame: Data for the first timeframe in get_timeframes() list
"""
primary_timeframe = self.get_timeframes()[0]
return self.timeframes_data[primary_timeframe]
@abstractmethod
def initialize(self, backtester) -> None:
"""
Initialize strategy with backtester data.
This method is called once before backtesting begins.
The original 1-minute data will already be resampled to all required timeframes
and available in self.timeframes_data.
Strategies should setup indicators, validate data, and
set self.initialized = True when complete.
Args:
backtester: Backtest instance with data and configuration
"""
pass
@abstractmethod
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate entry signal for the given data index.
The df_index refers to the index in the backtester's working dataframe,
which corresponds to the primary timeframe data.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Entry signal with confidence level
"""
pass
@abstractmethod
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal for the given data index.
The df_index refers to the index in the backtester's working dataframe,
which corresponds to the primary timeframe data.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Exit signal with confidence level
"""
pass
def get_confidence(self, backtester, df_index: int) -> float:
"""
Get strategy confidence for the current market state.
Default implementation returns 1.0. Strategies can override
this to provide dynamic confidence based on market conditions.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
float: Confidence level (0.0 to 1.0)
"""
return 1.0
def __repr__(self) -> str:
"""String representation of the strategy."""
timeframes = self.get_timeframes()
return (f"{self.__class__.__name__}(name={self.name}, "
f"weight={self.weight}, timeframes={timeframes}, "
f"initialized={self.initialized})")

View File

@@ -1,344 +0,0 @@
"""
Bollinger Bands + RSI Strategy (BBRS)
This module implements a sophisticated trading strategy that combines Bollinger Bands
and RSI indicators with market regime detection. The strategy adapts its parameters
based on whether the market is trending or moving sideways.
Key Features:
- Dynamic parameter adjustment based on market regime
- Bollinger Band squeeze detection
- RSI overbought/oversold conditions
- Market regime-specific thresholds
- Multi-timeframe analysis support
"""
import pandas as pd
import numpy as np
import logging
from typing import Tuple, Optional, List
from .base import StrategyBase, StrategySignal
class BBRSStrategy(StrategyBase):
"""
Bollinger Bands + RSI Strategy implementation.
This strategy uses Bollinger Bands and RSI indicators with market regime detection
to generate trading signals. It adapts its parameters based on whether the market
is in a trending or sideways regime.
The strategy works with 1-minute data as input and lets the underlying Strategy class
handle internal resampling to the timeframes it needs (typically 15min and 1h).
Stop-loss execution uses 1-minute precision.
Parameters:
bb_width (float): Bollinger Band width threshold (default: 0.05)
bb_period (int): Bollinger Band period (default: 20)
rsi_period (int): RSI calculation period (default: 14)
trending_rsi_threshold (list): RSI thresholds for trending market [low, high]
trending_bb_multiplier (float): BB multiplier for trending market
sideways_rsi_threshold (list): RSI thresholds for sideways market [low, high]
sideways_bb_multiplier (float): BB multiplier for sideways market
strategy_name (str): Strategy implementation name ("MarketRegimeStrategy" or "CryptoTradingStrategy")
SqueezeStrategy (bool): Enable squeeze strategy
stop_loss_pct (float): Stop loss percentage (default: 0.05)
Example:
params = {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"strategy_name": "MarketRegimeStrategy",
"SqueezeStrategy": true
}
strategy = BBRSStrategy(weight=1.0, params=params)
"""
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
"""
Initialize the BBRS strategy.
Args:
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters for Bollinger Bands and RSI
"""
super().__init__("bbrs", weight, params)
def get_timeframes(self) -> List[str]:
"""
Get the timeframes required by the BBRS strategy.
BBRS strategy uses 1-minute data as input and lets the Strategy class
handle internal resampling to the timeframes it needs (15min, 1h, etc.).
We still include 1min for stop-loss precision.
Returns:
List[str]: List of timeframes needed for the strategy
"""
# BBRS strategy works with 1-minute data and lets Strategy class handle resampling
return ["1min"]
def initialize(self, backtester) -> None:
"""
Initialize BBRS strategy with signal processing.
Sets up the strategy by:
1. Using 1-minute data directly (Strategy class handles internal resampling)
2. Running the BBRS strategy processing on 1-minute data
3. Creating signals aligned with backtester expectations
Args:
backtester: Backtest instance with OHLCV data
"""
# Resample to get 1-minute data (which should be the original data)
self._resample_data(backtester.original_df)
# Get 1-minute data for strategy processing - Strategy class will handle internal resampling
min1_data = self.get_data_for_timeframe("1min")
# Initialize empty signal series for backtester compatibility
# Note: These will be populated after strategy processing
backtester.strategies["buy_signals"] = pd.Series(False, index=range(len(min1_data)))
backtester.strategies["sell_signals"] = pd.Series(False, index=range(len(min1_data)))
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.05)
backtester.strategies["primary_timeframe"] = "1min"
# Run strategy processing on 1-minute data
self._run_strategy_processing(backtester)
self.initialized = True
def _run_strategy_processing(self, backtester) -> None:
"""
Run the actual BBRS strategy processing.
Uses the Strategy class from cycles.Analysis.strategies to process
the 1-minute data. The Strategy class will handle internal resampling
to the timeframes it needs (15min, 1h, etc.) and generate buy/sell signals.
Args:
backtester: Backtest instance with timeframes_data available
"""
from cycles.Analysis.bb_rsi import BollingerBandsStrategy
# Get 1-minute data for strategy processing - let Strategy class handle resampling
strategy_data = self.get_data_for_timeframe("1min")
# Configure strategy parameters with defaults
config_strategy = {
"bb_width": self.params.get("bb_width", 0.05),
"bb_period": self.params.get("bb_period", 20),
"rsi_period": self.params.get("rsi_period", 14),
"trending": {
"rsi_threshold": self.params.get("trending_rsi_threshold", [30, 70]),
"bb_std_dev_multiplier": self.params.get("trending_bb_multiplier", 2.5),
},
"sideways": {
"rsi_threshold": self.params.get("sideways_rsi_threshold", [40, 60]),
"bb_std_dev_multiplier": self.params.get("sideways_bb_multiplier", 1.8),
},
"strategy_name": self.params.get("strategy_name", "MarketRegimeStrategy"),
"SqueezeStrategy": self.params.get("SqueezeStrategy", True)
}
# Run strategy processing on 1-minute data - Strategy class handles internal resampling
strategy = BollingerBandsStrategy(config=config_strategy, logging=logging)
processed_data = strategy.run(strategy_data, config_strategy["strategy_name"])
# Store processed data for plotting and analysis
backtester.processed_data = processed_data
if processed_data.empty:
# If strategy processing failed, keep empty signals
return
# Extract signals from processed data
buy_signals_raw = processed_data.get('BuySignal', pd.Series(False, index=processed_data.index)).astype(bool)
sell_signals_raw = processed_data.get('SellSignal', pd.Series(False, index=processed_data.index)).astype(bool)
# The processed_data will be on whatever timeframe the Strategy class outputs
# We need to map these signals back to 1-minute resolution for backtesting
original_1min_data = self.get_data_for_timeframe("1min")
# Reindex signals to 1-minute resolution using forward-fill
buy_signals_1min = buy_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
sell_signals_1min = sell_signals_raw.reindex(original_1min_data.index, method='ffill').fillna(False)
# Convert to integer index to match backtester expectations
backtester.strategies["buy_signals"] = pd.Series(buy_signals_1min.values, index=range(len(buy_signals_1min)))
backtester.strategies["sell_signals"] = pd.Series(sell_signals_1min.values, index=range(len(sell_signals_1min)))
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate entry signal based on BBRS buy signals.
Entry occurs when the BBRS strategy processing has generated
a buy signal based on Bollinger Bands and RSI conditions on
the primary timeframe.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Entry signal if buy condition met, hold otherwise
"""
if not self.initialized:
return StrategySignal("HOLD", confidence=0.0)
if df_index >= len(backtester.strategies["buy_signals"]):
return StrategySignal("HOLD", confidence=0.0)
if backtester.strategies["buy_signals"].iloc[df_index]:
# High confidence for BBRS buy signals
confidence = self._calculate_signal_confidence(backtester, df_index, "entry")
return StrategySignal("ENTRY", confidence=confidence)
return StrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal based on BBRS sell signals or stop loss.
Exit occurs when:
1. BBRS strategy generates a sell signal
2. Stop loss is triggered based on price movement
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Exit signal with type and price, or hold signal
"""
if not self.initialized:
return StrategySignal("HOLD", confidence=0.0)
if df_index >= len(backtester.strategies["sell_signals"]):
return StrategySignal("HOLD", confidence=0.0)
# Check for sell signal
if backtester.strategies["sell_signals"].iloc[df_index]:
confidence = self._calculate_signal_confidence(backtester, df_index, "exit")
return StrategySignal("EXIT", confidence=confidence,
metadata={"type": "SELL_SIGNAL"})
# Check for stop loss using 1-minute data for precision
stop_loss_result, sell_price = self._check_stop_loss(backtester)
if stop_loss_result:
return StrategySignal("EXIT", confidence=1.0, price=sell_price,
metadata={"type": "STOP_LOSS"})
return StrategySignal("HOLD", confidence=0.0)
def get_confidence(self, backtester, df_index: int) -> float:
"""
Get strategy confidence based on signal strength and market conditions.
Confidence can be enhanced by analyzing multiple timeframes and
market regime consistency.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.initialized:
return 0.0
# Check for active signals
has_buy_signal = (df_index < len(backtester.strategies["buy_signals"]) and
backtester.strategies["buy_signals"].iloc[df_index])
has_sell_signal = (df_index < len(backtester.strategies["sell_signals"]) and
backtester.strategies["sell_signals"].iloc[df_index])
if has_buy_signal or has_sell_signal:
signal_type = "entry" if has_buy_signal else "exit"
return self._calculate_signal_confidence(backtester, df_index, signal_type)
# Moderate confidence during neutral periods
return 0.5
def _calculate_signal_confidence(self, backtester, df_index: int, signal_type: str) -> float:
"""
Calculate confidence level for a signal based on multiple factors.
Can consider multiple timeframes, market regime, volatility, etc.
Args:
backtester: Backtest instance
df_index: Current index
signal_type: "entry" or "exit"
Returns:
float: Confidence level (0.0 to 1.0)
"""
base_confidence = 1.0
# TODO: Implement multi-timeframe confirmation
# For now, return high confidence for primary signals
# Future enhancements could include:
# - Checking confirmation from additional timeframes
# - Analyzing market regime consistency
# - Considering volatility levels
# - RSI and BB position analysis
return base_confidence
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
"""
Check if stop loss is triggered using 1-minute data for precision.
Uses 1-minute data regardless of primary timeframe to ensure
accurate stop loss execution.
Args:
backtester: Backtest instance with current trade state
Returns:
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
"""
# Calculate stop loss price
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
# Use 1-minute data for precise stop loss checking
min1_data = self.get_data_for_timeframe("1min")
if min1_data is None:
# Fallback to original_df if 1min timeframe not available
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
min1_index = min1_data.index
# Find data range from entry to current time
start_candidates = min1_index[min1_index >= backtester.entry_time]
if len(start_candidates) == 0:
return False, None
backtester.current_trade_min1_start_idx = start_candidates[0]
end_candidates = min1_index[min1_index <= backtester.current_date]
if len(end_candidates) == 0:
return False, None
backtester.current_min1_end_idx = end_candidates[-1]
# Check if any candle in the range triggered stop loss
min1_slice = min1_data.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
if (min1_slice['low'] <= stop_price).any():
# Find the first candle that triggered stop loss
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
# Use open price if it gapped below stop, otherwise use stop price
if stop_candle['open'] < stop_price:
sell_price = stop_candle['open']
else:
sell_price = stop_price
return True, sell_price
return False, None

View File

@@ -1,349 +0,0 @@
"""
Default Meta-Trend Strategy
This module implements the default trading strategy based on meta-trend analysis
using multiple Supertrend indicators. The strategy enters when trends align
and exits on trend reversal or stop loss.
The meta-trend is calculated by comparing three Supertrend indicators:
- Entry: When meta-trend changes from != 1 to == 1
- Exit: When meta-trend changes to -1 or stop loss is triggered
"""
import numpy as np
from typing import Tuple, Optional, List
from .base import StrategyBase, StrategySignal
class DefaultStrategy(StrategyBase):
"""
Default meta-trend strategy implementation.
This strategy uses multiple Supertrend indicators to determine market direction.
It generates entry signals when all three Supertrend indicators align in an
upward direction, and exit signals when they reverse or stop loss is triggered.
The strategy works best on 15-minute timeframes but can be configured for other timeframes.
Parameters:
stop_loss_pct (float): Stop loss percentage (default: 0.03)
timeframe (str): Preferred timeframe for analysis (default: "15min")
Example:
strategy = DefaultStrategy(weight=1.0, params={"stop_loss_pct": 0.05, "timeframe": "15min"})
"""
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
"""
Initialize the default strategy.
Args:
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters including stop_loss_pct and timeframe
"""
super().__init__("default", weight, params)
def get_timeframes(self) -> List[str]:
"""
Get the timeframes required by the default strategy.
The default strategy works on a single timeframe (typically 15min)
but also needs 1min data for precise stop-loss execution.
Returns:
List[str]: List containing primary timeframe and 1min for stop-loss
"""
primary_timeframe = self.params.get("timeframe", "15min")
# Always include 1min for stop-loss precision, avoid duplicates
timeframes = [primary_timeframe]
if primary_timeframe != "1min":
timeframes.append("1min")
return timeframes
def initialize(self, backtester) -> None:
"""
Initialize meta trend calculation using Supertrend indicators.
Calculates the meta-trend by comparing three Supertrend indicators.
When all three agree on direction, meta-trend follows that direction.
Otherwise, meta-trend is neutral (0).
Args:
backtester: Backtest instance with OHLCV data
"""
try:
import threading
import time
from cycles.Analysis.supertrend import Supertrends
# First, resample the original 1-minute data to required timeframes
self._resample_data(backtester.original_df)
# Get the primary timeframe data for strategy calculations
primary_timeframe = self.get_timeframes()[0]
strategy_data = self.get_data_for_timeframe(primary_timeframe)
if strategy_data is None or len(strategy_data) < 50:
# Not enough data for reliable Supertrend calculation
self.meta_trend = np.zeros(len(strategy_data) if strategy_data is not None else 1)
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
print(f"DefaultStrategy: Insufficient data ({len(strategy_data) if strategy_data is not None else 0} points), using fallback")
return
# Limit data size to prevent excessive computation time
# original_length = len(strategy_data)
# if len(strategy_data) > 200:
# strategy_data = strategy_data.tail(200)
# print(f"DefaultStrategy: Limited data from {original_length} to {len(strategy_data)} points for faster computation")
# Use a timeout mechanism for Supertrend calculation
result_container = {}
exception_container = {}
def calculate_supertrend():
try:
# Calculate Supertrend indicators on the primary timeframe
supertrends = Supertrends(strategy_data, verbose=False)
supertrend_results_list = supertrends.calculate_supertrend_indicators()
result_container['supertrend_results'] = supertrend_results_list
except Exception as e:
exception_container['error'] = e
# Run Supertrend calculation in a separate thread with timeout
calc_thread = threading.Thread(target=calculate_supertrend)
calc_thread.daemon = True
calc_thread.start()
# Wait for calculation with timeout
calc_thread.join(timeout=15.0) # 15 second timeout
if calc_thread.is_alive():
# Calculation timed out
print(f"DefaultStrategy: Supertrend calculation timed out, using fallback")
self.meta_trend = np.zeros(len(strategy_data))
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
return
if 'error' in exception_container:
# Calculation failed
raise exception_container['error']
if 'supertrend_results' not in result_container:
# No result returned
print(f"DefaultStrategy: No Supertrend results, using fallback")
self.meta_trend = np.zeros(len(strategy_data))
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
return
# Process successful results
supertrend_results_list = result_container['supertrend_results']
# Extract trend arrays from each Supertrend
trends = [st['results']['trend'] for st in supertrend_results_list]
trends_arr = np.stack(trends, axis=1)
# Calculate meta-trend: all three must agree for direction signal
meta_trend = np.where(
(trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
trends_arr[:,0],
0 # Neutral when trends don't agree
)
# Store data internally instead of relying on backtester.strategies
self.meta_trend = meta_trend
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
# Also store in backtester if it has strategies attribute (for compatibility)
if hasattr(backtester, 'strategies'):
if not isinstance(backtester.strategies, dict):
backtester.strategies = {}
backtester.strategies["meta_trend"] = meta_trend
backtester.strategies["stop_loss_pct"] = self.stop_loss_pct
backtester.strategies["primary_timeframe"] = primary_timeframe
self.initialized = True
print(f"DefaultStrategy: Successfully initialized with {len(meta_trend)} data points")
except Exception as e:
# Handle any other errors gracefully
print(f"DefaultStrategy initialization failed: {e}")
primary_timeframe = self.get_timeframes()[0]
strategy_data = self.get_data_for_timeframe(primary_timeframe)
data_length = len(strategy_data) if strategy_data is not None else 1
# Create a simple fallback
self.meta_trend = np.zeros(data_length)
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate entry signal based on meta-trend direction change.
Entry occurs when meta-trend changes from != 1 to == 1, indicating
all Supertrend indicators now agree on upward direction.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Entry signal if trend aligns, hold signal otherwise
"""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend entry condition
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.meta_trend[df_index]
if prev_trend != 1 and curr_trend == 1:
# Strong confidence when all indicators align for entry
return StrategySignal("ENTRY", confidence=1.0)
return StrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal based on meta-trend reversal or stop loss.
Exit occurs when:
1. Meta-trend changes to -1 (trend reversal)
2. Stop loss is triggered based on price movement
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
StrategySignal: Exit signal with type and price, or hold signal
"""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend exit signal
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.meta_trend[df_index]
if prev_trend != 1 and curr_trend == -1:
return StrategySignal("EXIT", confidence=1.0,
metadata={"type": "META_TREND_EXIT_SIGNAL"})
# Check for stop loss using 1-minute data for precision
# Note: Stop loss checking requires active trade context which may not be available in StrategyTrader
# For now, skip stop loss checking in signal generation
# stop_loss_result, sell_price = self._check_stop_loss(backtester)
# if stop_loss_result:
# return StrategySignal("EXIT", confidence=1.0, price=sell_price,
# metadata={"type": "STOP_LOSS"})
return StrategySignal("HOLD", confidence=0.0)
def get_confidence(self, backtester, df_index: int) -> float:
"""
Get strategy confidence based on meta-trend strength.
Higher confidence when meta-trend is strongly directional,
lower confidence during neutral periods.
Args:
backtester: Backtest instance with current state
df_index: Current index in the primary timeframe dataframe
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.initialized:
return 0.0
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return 0.0
curr_trend = self.meta_trend[df_index]
# High confidence for strong directional signals
if curr_trend == 1 or curr_trend == -1:
return 1.0
# Low confidence for neutral trend
return 0.3
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
"""
Check if stop loss is triggered based on price movement.
Uses 1-minute data for precise stop loss checking regardless of
the primary timeframe used for strategy signals.
Args:
backtester: Backtest instance with current trade state
Returns:
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
"""
# Calculate stop loss price
stop_price = backtester.entry_price * (1 - self.stop_loss_pct)
# Use 1-minute data for precise stop loss checking
min1_data = self.get_data_for_timeframe("1min")
if min1_data is None:
# Fallback to original_df if 1min timeframe not available
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
min1_index = min1_data.index
# Find data range from entry to current time
start_candidates = min1_index[min1_index >= backtester.entry_time]
if len(start_candidates) == 0:
return False, None
backtester.current_trade_min1_start_idx = start_candidates[0]
end_candidates = min1_index[min1_index <= backtester.current_date]
if len(end_candidates) == 0:
return False, None
backtester.current_min1_end_idx = end_candidates[-1]
# Check if any candle in the range triggered stop loss
min1_slice = min1_data.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
if (min1_slice['low'] <= stop_price).any():
# Find the first candle that triggered stop loss
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
# Use open price if it gapped below stop, otherwise use stop price
if stop_candle['open'] < stop_price:
sell_price = stop_candle['open']
else:
sell_price = stop_price
return True, sell_price
return False, None

View File

@@ -1,397 +0,0 @@
"""
Strategy Manager
This module contains the StrategyManager class that orchestrates multiple trading strategies
and combines their signals using configurable aggregation rules.
The StrategyManager supports various combination methods for entry and exit signals:
- Entry: any, all, majority, weighted_consensus
- Exit: any, all, priority (with stop loss prioritization)
"""
from typing import Dict, List, Tuple, Optional
import logging
from .base import StrategyBase, StrategySignal
from .default_strategy import DefaultStrategy
from .bbrs_strategy import BBRSStrategy
from .random_strategy import RandomStrategy
class StrategyManager:
"""
Manages multiple strategies and combines their signals.
The StrategyManager loads multiple strategies from configuration,
initializes them with backtester data, and combines their signals
using configurable aggregation rules.
Attributes:
strategies (List[StrategyBase]): List of loaded strategies
combination_rules (Dict): Rules for combining signals
initialized (bool): Whether manager has been initialized
Example:
config = {
"strategies": [
{"name": "default", "weight": 0.6, "params": {}},
{"name": "bbrs", "weight": 0.4, "params": {"bb_width": 0.05}}
],
"combination_rules": {
"entry": "weighted_consensus",
"exit": "any",
"min_confidence": 0.6
}
}
manager = StrategyManager(config["strategies"], config["combination_rules"])
"""
def __init__(self, strategies_config: List[Dict], combination_rules: Optional[Dict] = None):
"""
Initialize the strategy manager.
Args:
strategies_config: List of strategy configurations
combination_rules: Rules for combining signals
"""
self.strategies = self._load_strategies(strategies_config)
self.combination_rules = combination_rules or {
"entry": "weighted_consensus",
"exit": "any",
"min_confidence": 0.5
}
self.initialized = False
def _load_strategies(self, strategies_config: List[Dict]) -> List[StrategyBase]:
"""
Load strategies from configuration.
Creates strategy instances based on configuration and registers
them with the manager. Supports extensible strategy registration.
Args:
strategies_config: List of strategy configurations
Returns:
List[StrategyBase]: List of instantiated strategies
Raises:
ValueError: If unknown strategy name is specified
"""
strategies = []
for config in strategies_config:
name = config.get("name", "").lower()
weight = config.get("weight", 1.0)
params = config.get("params", {})
if name == "default":
strategies.append(DefaultStrategy(weight, params))
elif name == "bbrs":
strategies.append(BBRSStrategy(weight, params))
elif name == "random":
strategies.append(RandomStrategy(weight, params))
else:
raise ValueError(f"Unknown strategy: {name}. "
f"Available strategies: default, bbrs, random")
return strategies
def initialize(self, backtester) -> None:
"""
Initialize all strategies with backtester data.
Calls the initialize method on each strategy, allowing them
to set up indicators, validate data, and prepare for trading.
Each strategy will handle its own timeframe resampling.
Args:
backtester: Backtest instance with OHLCV data
"""
for strategy in self.strategies:
try:
strategy.initialize(backtester)
# Log strategy timeframe information
timeframes = strategy.get_timeframes()
logging.info(f"Initialized strategy: {strategy.name} with timeframes: {timeframes}")
except Exception as e:
logging.error(f"Failed to initialize strategy {strategy.name}: {e}")
raise
self.initialized = True
logging.info(f"Strategy manager initialized with {len(self.strategies)} strategies")
# Log summary of all timeframes being used
all_timeframes = set()
for strategy in self.strategies:
all_timeframes.update(strategy.get_timeframes())
logging.info(f"Total unique timeframes in use: {sorted(all_timeframes)}")
def get_entry_signal(self, backtester, df_index: int) -> bool:
"""
Get combined entry signal from all strategies.
Collects entry signals from all strategies and combines them
according to the configured combination rules.
Args:
backtester: Backtest instance with current state
df_index: Current index in the dataframe
Returns:
bool: True if combined signal suggests entry, False otherwise
"""
if not self.initialized:
return False
# Collect signals from all strategies
signals = {}
for strategy in self.strategies:
try:
signal = strategy.get_entry_signal(backtester, df_index)
signals[strategy.name] = {
"signal": signal,
"weight": strategy.weight,
"confidence": signal.confidence
}
except Exception as e:
logging.warning(f"Strategy {strategy.name} entry signal failed: {e}")
signals[strategy.name] = {
"signal": StrategySignal("HOLD", 0.0),
"weight": strategy.weight,
"confidence": 0.0
}
return self._combine_entry_signals(signals)
def get_exit_signal(self, backtester, df_index: int) -> Tuple[Optional[str], Optional[float]]:
"""
Get combined exit signal from all strategies.
Collects exit signals from all strategies and combines them
according to the configured combination rules.
Args:
backtester: Backtest instance with current state
df_index: Current index in the dataframe
Returns:
Tuple[Optional[str], Optional[float]]: (exit_type, exit_price) or (None, None)
"""
if not self.initialized:
return None, None
# Collect signals from all strategies
signals = {}
for strategy in self.strategies:
try:
signal = strategy.get_exit_signal(backtester, df_index)
signals[strategy.name] = {
"signal": signal,
"weight": strategy.weight,
"confidence": signal.confidence
}
except Exception as e:
logging.warning(f"Strategy {strategy.name} exit signal failed: {e}")
signals[strategy.name] = {
"signal": StrategySignal("HOLD", 0.0),
"weight": strategy.weight,
"confidence": 0.0
}
return self._combine_exit_signals(signals)
def _combine_entry_signals(self, signals: Dict) -> bool:
"""
Combine entry signals based on combination rules.
Supports multiple combination methods:
- any: Enter if ANY strategy signals entry
- all: Enter only if ALL strategies signal entry
- majority: Enter if majority of strategies signal entry
- weighted_consensus: Enter based on weighted average confidence
Args:
signals: Dictionary of strategy signals with weights and confidence
Returns:
bool: Combined entry decision
"""
method = self.combination_rules.get("entry", "weighted_consensus")
min_confidence = self.combination_rules.get("min_confidence", 0.5)
# Filter for entry signals above minimum confidence
entry_signals = [
s for s in signals.values()
if s["signal"].signal_type == "ENTRY" and s["signal"].confidence >= min_confidence
]
if not entry_signals:
return False
if method == "any":
# Enter if any strategy signals entry
return len(entry_signals) > 0
elif method == "all":
# Enter only if all strategies signal entry
return len(entry_signals) == len(self.strategies)
elif method == "majority":
# Enter if majority of strategies signal entry
return len(entry_signals) > len(self.strategies) / 2
elif method == "weighted_consensus":
# Enter based on weighted average confidence
total_weight = sum(s["weight"] for s in entry_signals)
if total_weight == 0:
return False
weighted_confidence = sum(
s["signal"].confidence * s["weight"]
for s in entry_signals
) / total_weight
return weighted_confidence >= min_confidence
else:
logging.warning(f"Unknown entry combination method: {method}, using 'any'")
return len(entry_signals) > 0
def _combine_exit_signals(self, signals: Dict) -> Tuple[Optional[str], Optional[float]]:
"""
Combine exit signals based on combination rules.
Supports multiple combination methods:
- any: Exit if ANY strategy signals exit (recommended for risk management)
- all: Exit only if ALL strategies agree on exit
- priority: Exit based on priority order (STOP_LOSS > SELL_SIGNAL > others)
Args:
signals: Dictionary of strategy signals with weights and confidence
Returns:
Tuple[Optional[str], Optional[float]]: (exit_type, exit_price) or (None, None)
"""
method = self.combination_rules.get("exit", "any")
# Filter for exit signals
exit_signals = [
s for s in signals.values()
if s["signal"].signal_type == "EXIT"
]
if not exit_signals:
return None, None
if method == "any":
# Exit if any strategy signals exit (first one found)
for signal_data in exit_signals:
signal = signal_data["signal"]
exit_type = signal.metadata.get("type", "EXIT")
return exit_type, signal.price
elif method == "all":
# Exit only if all strategies agree on exit
if len(exit_signals) == len(self.strategies):
signal = exit_signals[0]["signal"]
exit_type = signal.metadata.get("type", "EXIT")
return exit_type, signal.price
elif method == "priority":
# Priority order: STOP_LOSS > SELL_SIGNAL > others
stop_loss_signals = [
s for s in exit_signals
if s["signal"].metadata.get("type") == "STOP_LOSS"
]
if stop_loss_signals:
signal = stop_loss_signals[0]["signal"]
return "STOP_LOSS", signal.price
sell_signals = [
s for s in exit_signals
if s["signal"].metadata.get("type") == "SELL_SIGNAL"
]
if sell_signals:
signal = sell_signals[0]["signal"]
return "SELL_SIGNAL", signal.price
# Return first available exit signal
signal = exit_signals[0]["signal"]
exit_type = signal.metadata.get("type", "EXIT")
return exit_type, signal.price
else:
logging.warning(f"Unknown exit combination method: {method}, using 'any'")
# Fallback to 'any' method
signal = exit_signals[0]["signal"]
exit_type = signal.metadata.get("type", "EXIT")
return exit_type, signal.price
return None, None
def get_strategy_summary(self) -> Dict:
"""
Get summary of loaded strategies and their configuration.
Returns:
Dict: Summary of strategies, weights, combination rules, and timeframes
"""
return {
"strategies": [
{
"name": strategy.name,
"weight": strategy.weight,
"params": strategy.params,
"timeframes": strategy.get_timeframes(),
"initialized": strategy.initialized
}
for strategy in self.strategies
],
"combination_rules": self.combination_rules,
"total_strategies": len(self.strategies),
"initialized": self.initialized,
"all_timeframes": list(set().union(*[strategy.get_timeframes() for strategy in self.strategies]))
}
def __repr__(self) -> str:
"""String representation of the strategy manager."""
strategy_names = [s.name for s in self.strategies]
return (f"StrategyManager(strategies={strategy_names}, "
f"initialized={self.initialized})")
def create_strategy_manager(config: Dict) -> StrategyManager:
"""
Factory function to create StrategyManager from configuration.
Provides a convenient way to create a StrategyManager instance
from a configuration dictionary.
Args:
config: Configuration dictionary with strategies and combination_rules
Returns:
StrategyManager: Configured strategy manager instance
Example:
config = {
"strategies": [
{"name": "default", "weight": 1.0, "params": {}}
],
"combination_rules": {
"entry": "any",
"exit": "any"
}
}
manager = create_strategy_manager(config)
"""
strategies_config = config.get("strategies", [])
combination_rules = config.get("combination_rules", {})
if not strategies_config:
raise ValueError("No strategies specified in configuration")
return StrategyManager(strategies_config, combination_rules)

View File

@@ -1,218 +0,0 @@
"""
Random Strategy for Testing
This strategy generates random entry and exit signals for testing the strategy system.
It's useful for verifying that the strategy framework is working correctly.
"""
import random
import logging
from typing import Dict, List, Optional
import pandas as pd
from .base import StrategyBase, StrategySignal
logger = logging.getLogger(__name__)
class RandomStrategy(StrategyBase):
"""
Random signal generator strategy for testing.
This strategy generates random entry and exit signals with configurable
probability and confidence levels. It's designed to test the strategy
framework and signal processing system.
Parameters:
entry_probability: Probability of generating an entry signal (0.0-1.0)
exit_probability: Probability of generating an exit signal (0.0-1.0)
min_confidence: Minimum confidence level for signals
max_confidence: Maximum confidence level for signals
timeframe: Timeframe to operate on (default: "1min")
signal_frequency: How often to generate signals (every N bars)
"""
def __init__(self, weight: float = 1.0, params: Optional[Dict] = None):
"""Initialize the random strategy."""
super().__init__("random", weight, params)
# Strategy parameters with defaults
self.entry_probability = self.params.get("entry_probability", 0.05) # 5% chance per bar
self.exit_probability = self.params.get("exit_probability", 0.1) # 10% chance per bar
self.min_confidence = self.params.get("min_confidence", 0.6)
self.max_confidence = self.params.get("max_confidence", 0.9)
self.timeframe = self.params.get("timeframe", "1min")
self.signal_frequency = self.params.get("signal_frequency", 1) # Every bar
# Internal state
self.bar_count = 0
self.last_signal_bar = -1
self.last_processed_timestamp = None # Track last processed timestamp to avoid duplicates
logger.info(f"RandomStrategy initialized with entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}")
def get_timeframes(self) -> List[str]:
"""Return required timeframes for this strategy."""
return [self.timeframe, "1min"] # Always include 1min for precision
def initialize(self, backtester) -> None:
"""Initialize strategy with backtester data."""
try:
logger.info(f"RandomStrategy: Starting initialization...")
# Resample data to required timeframes
self._resample_data(backtester.original_df)
# Get primary timeframe data
self.df = self.get_primary_timeframe_data()
if self.df is None or self.df.empty:
raise ValueError(f"No data available for timeframe {self.timeframe}")
# Reset internal state
self.bar_count = 0
self.last_signal_bar = -1
self.initialized = True
logger.info(f"RandomStrategy initialized with {len(self.df)} bars on {self.timeframe}")
logger.info(f"RandomStrategy: Data range from {self.df.index[0]} to {self.df.index[-1]}")
except Exception as e:
logger.error(f"Failed to initialize RandomStrategy: {e}")
logger.error(f"RandomStrategy: backtester.original_df shape: {backtester.original_df.shape if hasattr(backtester, 'original_df') else 'No original_df'}")
raise
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""Generate random entry signals."""
if not self.initialized:
logger.warning(f"RandomStrategy: get_entry_signal called but not initialized")
return StrategySignal("HOLD", 0.0)
try:
# Get current timestamp to avoid duplicate signals
current_timestamp = None
if hasattr(backtester, 'original_df') and not backtester.original_df.empty:
current_timestamp = backtester.original_df.index[-1]
# Skip if we already processed this timestamp
if current_timestamp and self.last_processed_timestamp == current_timestamp:
return StrategySignal("HOLD", 0.0)
self.bar_count += 1
# Debug logging every 10 bars
if self.bar_count % 10 == 0:
logger.info(f"RandomStrategy: Processing bar {self.bar_count}, df_index={df_index}, timestamp={current_timestamp}")
# Check if we should generate a signal based on frequency
if (self.bar_count - self.last_signal_bar) < self.signal_frequency:
return StrategySignal("HOLD", 0.0)
# Generate random entry signal
random_value = random.random()
if random_value < self.entry_probability:
confidence = random.uniform(self.min_confidence, self.max_confidence)
self.last_signal_bar = self.bar_count
self.last_processed_timestamp = current_timestamp # Update last processed timestamp
# Get current price from backtester's original data (more reliable)
try:
if hasattr(backtester, 'original_df') and not backtester.original_df.empty:
# Use the last available price from the original data
current_price = backtester.original_df['close'].iloc[-1]
elif hasattr(backtester, 'df') and not backtester.df.empty:
# Fallback to backtester's main dataframe
current_price = backtester.df['close'].iloc[min(df_index, len(backtester.df)-1)]
else:
# Last resort: use our internal dataframe
current_price = self.df.iloc[min(df_index, len(self.df)-1)]['close']
except (IndexError, KeyError) as e:
logger.warning(f"RandomStrategy: Error getting current price: {e}, using fallback")
current_price = self.df.iloc[-1]['close'] if not self.df.empty else 50000.0
logger.info(f"RandomStrategy: Generated ENTRY signal at bar {self.bar_count}, "
f"price=${current_price:.2f}, confidence={confidence:.2f}, random_value={random_value:.3f}")
return StrategySignal(
"ENTRY",
confidence=confidence,
price=current_price,
metadata={
"strategy": "random",
"bar_count": self.bar_count,
"timeframe": self.timeframe
}
)
# Update timestamp even if no signal generated
if current_timestamp:
self.last_processed_timestamp = current_timestamp
return StrategySignal("HOLD", 0.0)
except Exception as e:
logger.error(f"RandomStrategy entry signal error: {e}")
return StrategySignal("HOLD", 0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""Generate random exit signals."""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
try:
# Only generate exit signals if we have an open position
# This is handled by the strategy trader, but we can add logic here
# Generate random exit signal
if random.random() < self.exit_probability:
confidence = random.uniform(self.min_confidence, self.max_confidence)
# Get current price from backtester's original data (more reliable)
try:
if hasattr(backtester, 'original_df') and not backtester.original_df.empty:
# Use the last available price from the original data
current_price = backtester.original_df['close'].iloc[-1]
elif hasattr(backtester, 'df') and not backtester.df.empty:
# Fallback to backtester's main dataframe
current_price = backtester.df['close'].iloc[min(df_index, len(backtester.df)-1)]
else:
# Last resort: use our internal dataframe
current_price = self.df.iloc[min(df_index, len(self.df)-1)]['close']
except (IndexError, KeyError) as e:
logger.warning(f"RandomStrategy: Error getting current price for exit: {e}, using fallback")
current_price = self.df.iloc[-1]['close'] if not self.df.empty else 50000.0
# Randomly choose exit type
exit_types = ["SELL_SIGNAL", "TAKE_PROFIT", "STOP_LOSS"]
exit_type = random.choice(exit_types)
logger.info(f"RandomStrategy: Generated EXIT signal at bar {self.bar_count}, "
f"price=${current_price:.2f}, confidence={confidence:.2f}, type={exit_type}")
return StrategySignal(
"EXIT",
confidence=confidence,
price=current_price,
metadata={
"type": exit_type,
"strategy": "random",
"bar_count": self.bar_count,
"timeframe": self.timeframe
}
)
return StrategySignal("HOLD", 0.0)
except Exception as e:
logger.error(f"RandomStrategy exit signal error: {e}")
return StrategySignal("HOLD", 0.0)
def get_confidence(self, backtester, df_index: int) -> float:
"""Return random confidence level."""
return random.uniform(self.min_confidence, self.max_confidence)
def __repr__(self) -> str:
"""String representation of the strategy."""
return (f"RandomStrategy(entry_prob={self.entry_probability}, "
f"exit_prob={self.exit_probability}, timeframe={self.timeframe})")

215
cycles/supertrend.py Normal file
View File

@@ -0,0 +1,215 @@
import pandas as pd
import numpy as np
import logging
from functools import lru_cache
@lru_cache(maxsize=32)
def cached_supertrend_calculation(period, multiplier, data_tuple):
high = np.array(data_tuple[0])
low = np.array(data_tuple[1])
close = np.array(data_tuple[2])
tr = np.zeros_like(close)
tr[0] = high[0] - low[0]
hc_range = np.abs(high[1:] - close[:-1])
lc_range = np.abs(low[1:] - close[:-1])
hl_range = high[1:] - low[1:]
tr[1:] = np.maximum.reduce([hl_range, hc_range, lc_range])
atr = np.zeros_like(tr)
atr[0] = tr[0]
multiplier_ema = 2.0 / (period + 1)
for i in range(1, len(tr)):
atr[i] = (tr[i] * multiplier_ema) + (atr[i-1] * (1 - multiplier_ema))
upper_band = np.zeros_like(close)
lower_band = np.zeros_like(close)
for i in range(len(close)):
hl_avg = (high[i] + low[i]) / 2
upper_band[i] = hl_avg + (multiplier * atr[i])
lower_band[i] = hl_avg - (multiplier * atr[i])
final_upper = np.zeros_like(close)
final_lower = np.zeros_like(close)
supertrend = np.zeros_like(close)
trend = np.zeros_like(close)
final_upper[0] = upper_band[0]
final_lower[0] = lower_band[0]
if close[0] <= upper_band[0]:
supertrend[0] = upper_band[0]
trend[0] = -1
else:
supertrend[0] = lower_band[0]
trend[0] = 1
for i in range(1, len(close)):
if (upper_band[i] < final_upper[i-1]) or (close[i-1] > final_upper[i-1]):
final_upper[i] = upper_band[i]
else:
final_upper[i] = final_upper[i-1]
if (lower_band[i] > final_lower[i-1]) or (close[i-1] < final_lower[i-1]):
final_lower[i] = lower_band[i]
else:
final_lower[i] = final_lower[i-1]
if supertrend[i-1] == final_upper[i-1] and close[i] <= final_upper[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
elif supertrend[i-1] == final_upper[i-1] and close[i] > final_upper[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] >= final_lower[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] < final_lower[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
return {
'supertrend': supertrend,
'trend': trend,
'upper_band': final_upper,
'lower_band': final_lower
}
def calculate_supertrend_external(data, period, multiplier, close_column='close'):
"""
External function to calculate SuperTrend with configurable close column
Parameters:
- data: DataFrame with OHLC data
- period: int, period for ATR calculation
- multiplier: float, multiplier for ATR
- close_column: str, name of the column to use as close price (default: 'close')
"""
high_tuple = tuple(data['high'])
low_tuple = tuple(data['low'])
close_tuple = tuple(data[close_column])
return cached_supertrend_calculation(period, multiplier, (high_tuple, low_tuple, close_tuple))
class Supertrends:
def __init__(self, data, close_column='close', verbose=False, display=False):
"""
Initialize Supertrends calculator
Parameters:
- data: pandas DataFrame with OHLC data or list of prices
- close_column: str, name of the column to use as close price (default: 'close')
- verbose: bool, enable verbose logging
- display: bool, display mode (currently unused)
"""
self.close_column = close_column
self.data = data
self.verbose = verbose
logging.basicConfig(level=logging.INFO if verbose else logging.WARNING,
format='%(asctime)s - %(levelname)s - %(message)s')
self.logger = logging.getLogger('TrendDetectorSimple')
if not isinstance(self.data, pd.DataFrame):
if isinstance(self.data, list):
self.data = pd.DataFrame({self.close_column: self.data})
else:
raise ValueError("Data must be a pandas DataFrame or a list")
# Validate that required columns exist
required_columns = ['high', 'low', self.close_column]
missing_columns = [col for col in required_columns if col not in self.data.columns]
if missing_columns:
raise ValueError(f"Missing required columns: {missing_columns}")
def calculate_tr(self):
"""Calculate True Range using the configured close column"""
df = self.data.copy()
high = df['high'].values
low = df['low'].values
close = df[self.close_column].values
tr = np.zeros_like(close)
tr[0] = high[0] - low[0]
for i in range(1, len(close)):
hl_range = high[i] - low[i]
hc_range = abs(high[i] - close[i-1])
lc_range = abs(low[i] - close[i-1])
tr[i] = max(hl_range, hc_range, lc_range)
return tr
def calculate_atr(self, period=14):
"""Calculate Average True Range"""
tr = self.calculate_tr()
atr = np.zeros_like(tr)
atr[0] = tr[0]
multiplier = 2.0 / (period + 1)
for i in range(1, len(tr)):
atr[i] = (tr[i] * multiplier) + (atr[i-1] * (1 - multiplier))
return atr
def calculate_supertrend(self, period=10, multiplier=3.0):
"""
Calculate SuperTrend indicator for the price data using the configured close column.
SuperTrend is a trend-following indicator that uses ATR to determine the trend direction.
Parameters:
- period: int, the period for the ATR calculation (default: 10)
- multiplier: float, the multiplier for the ATR (default: 3.0)
Returns:
- Dictionary containing SuperTrend values, trend direction, and upper/lower bands
"""
df = self.data.copy()
high = df['high'].values
low = df['low'].values
close = df[self.close_column].values
atr = self.calculate_atr(period)
upper_band = np.zeros_like(close)
lower_band = np.zeros_like(close)
for i in range(len(close)):
hl_avg = (high[i] + low[i]) / 2
upper_band[i] = hl_avg + (multiplier * atr[i])
lower_band[i] = hl_avg - (multiplier * atr[i])
final_upper = np.zeros_like(close)
final_lower = np.zeros_like(close)
supertrend = np.zeros_like(close)
trend = np.zeros_like(close)
final_upper[0] = upper_band[0]
final_lower[0] = lower_band[0]
if close[0] <= upper_band[0]:
supertrend[0] = upper_band[0]
trend[0] = -1
else:
supertrend[0] = lower_band[0]
trend[0] = 1
for i in range(1, len(close)):
if (upper_band[i] < final_upper[i-1]) or (close[i-1] > final_upper[i-1]):
final_upper[i] = upper_band[i]
else:
final_upper[i] = final_upper[i-1]
if (lower_band[i] > final_lower[i-1]) or (close[i-1] < final_lower[i-1]):
final_lower[i] = lower_band[i]
else:
final_lower[i] = final_lower[i-1]
if supertrend[i-1] == final_upper[i-1] and close[i] <= final_upper[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
elif supertrend[i-1] == final_upper[i-1] and close[i] > final_upper[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] >= final_lower[i]:
supertrend[i] = final_lower[i]
trend[i] = 1
elif supertrend[i-1] == final_lower[i-1] and close[i] < final_lower[i]:
supertrend[i] = final_upper[i]
trend[i] = -1
supertrend_results = {
'supertrend': supertrend,
'trend': trend,
'upper_band': final_upper,
'lower_band': final_lower
}
return supertrend_results
def calculate_supertrend_indicators(self):
supertrend_params = [
{"period": 12, "multiplier": 3.0},
{"period": 10, "multiplier": 1.0},
{"period": 11, "multiplier": 2.0}
]
results = []
for p in supertrend_params:
result = self.calculate_supertrend(period=p["period"], multiplier=p["multiplier"])
results.append({
"results": result,
"params": p
})
return results

152
cycles/utils/data_loader.py Normal file
View File

@@ -0,0 +1,152 @@
import os
import json
import pandas as pd
from typing import Union, Optional
import logging
from .storage_utils import (
_parse_timestamp_column,
_filter_by_date_range,
_normalize_column_names,
TimestampParsingError,
DataLoadingError
)
class DataLoader:
"""Handles loading and preprocessing of data from various file formats"""
def __init__(self, data_dir: str, logging_instance: Optional[logging.Logger] = None):
"""Initialize data loader
Args:
data_dir: Directory containing data files
logging_instance: Optional logging instance
"""
self.data_dir = data_dir
self.logging = logging_instance
def load_data(self, file_path: str, start_date: Union[str, pd.Timestamp],
stop_date: Union[str, pd.Timestamp]) -> pd.DataFrame:
"""Load data with optimized dtypes and filtering, supporting CSV and JSON input
Args:
file_path: path to the data file
start_date: start date (string or datetime-like)
stop_date: stop date (string or datetime-like)
Returns:
pandas DataFrame with timestamp index
Raises:
DataLoadingError: If data loading fails
"""
try:
# Convert string dates to pandas datetime objects for proper comparison
start_date = pd.to_datetime(start_date)
stop_date = pd.to_datetime(stop_date)
# Determine file type
_, ext = os.path.splitext(file_path)
ext = ext.lower()
if ext == ".json":
return self._load_json_data(file_path, start_date, stop_date)
else:
return self._load_csv_data(file_path, start_date, stop_date)
except Exception as e:
error_msg = f"Error loading data from {file_path}: {e}"
if self.logging is not None:
self.logging.error(error_msg)
# Return an empty DataFrame with a DatetimeIndex
return pd.DataFrame(index=pd.to_datetime([]))
def _load_json_data(self, file_path: str, start_date: pd.Timestamp,
stop_date: pd.Timestamp) -> pd.DataFrame:
"""Load and process JSON data file
Args:
file_path: Path to JSON file
start_date: Start date for filtering
stop_date: Stop date for filtering
Returns:
Processed DataFrame with timestamp index
"""
with open(os.path.join(self.data_dir, file_path), 'r') as f:
raw = json.load(f)
data = pd.DataFrame(raw["Data"])
data = _normalize_column_names(data)
# Convert timestamp to datetime
data["timestamp"] = pd.to_datetime(data["timestamp"], unit="s")
# Filter by date range
data = _filter_by_date_range(data, "timestamp", start_date, stop_date)
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}")
return data.set_index("timestamp")
def _load_csv_data(self, file_path: str, start_date: pd.Timestamp,
stop_date: pd.Timestamp) -> pd.DataFrame:
"""Load and process CSV data file
Args:
file_path: Path to CSV file
start_date: Start date for filtering
stop_date: Stop date for filtering
Returns:
Processed DataFrame with timestamp index
"""
# Define optimized dtypes
dtypes = {
'Open': 'float32',
'High': 'float32',
'Low': 'float32',
'Close': 'float32',
'Volume': 'float32'
}
# Read data with original capitalized column names
data = pd.read_csv(os.path.join(self.data_dir, file_path), dtype=dtypes)
return self._process_csv_timestamps(data, start_date, stop_date, file_path)
def _process_csv_timestamps(self, data: pd.DataFrame, start_date: pd.Timestamp,
stop_date: pd.Timestamp, file_path: str) -> pd.DataFrame:
"""Process timestamps in CSV data and filter by date range
Args:
data: DataFrame with CSV data
start_date: Start date for filtering
stop_date: Stop date for filtering
file_path: Original file path for logging
Returns:
Processed DataFrame with timestamp index
"""
if 'Timestamp' in data.columns:
data = _parse_timestamp_column(data, 'Timestamp')
data = _filter_by_date_range(data, 'Timestamp', start_date, stop_date)
data = _normalize_column_names(data)
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}")
return data.set_index('timestamp')
else:
# Attempt to use the first column if 'Timestamp' is not present
data.rename(columns={data.columns[0]: 'timestamp'}, inplace=True)
data = _parse_timestamp_column(data, 'timestamp')
data = _filter_by_date_range(data, 'timestamp', start_date, stop_date)
data = _normalize_column_names(data)
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} (using first column as timestamp) for date range {start_date} to {stop_date}")
return data.set_index('timestamp')

106
cycles/utils/data_saver.py Normal file
View File

@@ -0,0 +1,106 @@
import os
import pandas as pd
from typing import Optional
import logging
from .storage_utils import DataSavingError
class DataSaver:
"""Handles saving data to various file formats"""
def __init__(self, data_dir: str, logging_instance: Optional[logging.Logger] = None):
"""Initialize data saver
Args:
data_dir: Directory for saving data files
logging_instance: Optional logging instance
"""
self.data_dir = data_dir
self.logging = logging_instance
def save_data(self, data: pd.DataFrame, file_path: str) -> None:
"""Save processed data to a CSV file.
If the DataFrame has a DatetimeIndex, it's converted to float Unix timestamps
(seconds since epoch) before saving. The index is saved as a column named 'timestamp'.
Args:
data: DataFrame to save
file_path: path to the data file relative to the data_dir
Raises:
DataSavingError: If saving fails
"""
try:
data_to_save = data.copy()
data_to_save = self._prepare_data_for_saving(data_to_save)
# Save to CSV, ensuring the 'timestamp' column (if created) is written
full_path = os.path.join(self.data_dir, file_path)
data_to_save.to_csv(full_path, index=False)
if self.logging is not None:
self.logging.info(f"Data saved to {full_path} with Unix timestamp column.")
except Exception as e:
error_msg = f"Failed to save data to {file_path}: {e}"
if self.logging is not None:
self.logging.error(error_msg)
raise DataSavingError(error_msg) from e
def _prepare_data_for_saving(self, data: pd.DataFrame) -> pd.DataFrame:
"""Prepare DataFrame for saving by handling different index types
Args:
data: DataFrame to prepare
Returns:
DataFrame ready for saving
"""
if isinstance(data.index, pd.DatetimeIndex):
return self._convert_datetime_index_to_timestamp(data)
elif pd.api.types.is_numeric_dtype(data.index.dtype):
return self._convert_numeric_index_to_timestamp(data)
else:
# For other index types, save with the current index
return data
def _convert_datetime_index_to_timestamp(self, data: pd.DataFrame) -> pd.DataFrame:
"""Convert DatetimeIndex to Unix timestamp column
Args:
data: DataFrame with DatetimeIndex
Returns:
DataFrame with timestamp column
"""
# Convert DatetimeIndex to Unix timestamp (float seconds since epoch)
data['timestamp'] = data.index.astype('int64') / 1e9
data.reset_index(drop=True, inplace=True)
# Ensure 'timestamp' is the first column if other columns exist
if 'timestamp' in data.columns and len(data.columns) > 1:
cols = ['timestamp'] + [col for col in data.columns if col != 'timestamp']
data = data[cols]
return data
def _convert_numeric_index_to_timestamp(self, data: pd.DataFrame) -> pd.DataFrame:
"""Convert numeric index to timestamp column
Args:
data: DataFrame with numeric index
Returns:
DataFrame with timestamp column
"""
# If index is already numeric (e.g. float Unix timestamps from a previous save/load cycle)
data['timestamp'] = data.index
data.reset_index(drop=True, inplace=True)
# Ensure 'timestamp' is the first column if other columns exist
if 'timestamp' in data.columns and len(data.columns) > 1:
cols = ['timestamp'] + [col for col in data.columns if col != 'timestamp']
data = data[cols]
return data

View File

@@ -1,80 +1,5 @@
import pandas as pd
def check_data(data_df: pd.DataFrame) -> bool:
"""
Checks if the input DataFrame has a DatetimeIndex.
Args:
data_df (pd.DataFrame): DataFrame to check.
Returns:
bool: True if the DataFrame has a DatetimeIndex, False otherwise.
"""
if not isinstance(data_df.index, pd.DatetimeIndex):
print("Warning: Input DataFrame must have a DatetimeIndex.")
return False
agg_rules = {}
# Define aggregation rules based on available columns
if 'open' in data_df.columns:
agg_rules['open'] = 'first'
if 'high' in data_df.columns:
agg_rules['high'] = 'max'
if 'low' in data_df.columns:
agg_rules['low'] = 'min'
if 'close' in data_df.columns:
agg_rules['close'] = 'last'
if 'volume' in data_df.columns:
agg_rules['volume'] = 'sum'
if not agg_rules:
print("Warning: No standard OHLCV columns (open, high, low, close, volume) found for daily aggregation.")
return False
return agg_rules
def aggregate_to_weekly(data_df: pd.DataFrame, weeks: int = 1) -> pd.DataFrame:
"""
Aggregates time-series financial data to weekly OHLCV format.
The input DataFrame is expected to have a DatetimeIndex.
'open' will be the first 'open' price of the week.
'close' will be the last 'close' price of the week.
'high' will be the maximum 'high' price of the week.
'low' will be the minimum 'low' price of the week.
'volume' (if present) will be the sum of volumes for the week.
Args:
data_df (pd.DataFrame): DataFrame with a DatetimeIndex and columns
like 'open', 'high', 'low', 'close', and optionally 'volume'.
weeks (int): The number of weeks to aggregate to. Default is 1.
Returns:
pd.DataFrame: DataFrame aggregated to weekly OHLCV data.
The index will be a DatetimeIndex with the time set to the start of the week.
Returns an empty DataFrame if no relevant OHLCV columns are found.
"""
agg_rules = check_data(data_df)
if not agg_rules:
print("Warning: No standard OHLCV columns (open, high, low, close, volume) found for weekly aggregation.")
return pd.DataFrame(index=pd.to_datetime([]))
# Resample to weekly frequency and apply aggregation rules
weekly_data = data_df.resample(f'{weeks}W').agg(agg_rules)
weekly_data.dropna(how='all', inplace=True)
# Adjust timestamps to the start of the week
if not weekly_data.empty and isinstance(weekly_data.index, pd.DatetimeIndex):
weekly_data.index = weekly_data.index.floor('W')
return weekly_data
def aggregate_to_daily(data_df: pd.DataFrame) -> pd.DataFrame:
"""
Aggregates time-series financial data to daily OHLCV format.
@@ -99,8 +24,22 @@ def aggregate_to_daily(data_df: pd.DataFrame) -> pd.DataFrame:
Raises:
ValueError: If the input DataFrame does not have a DatetimeIndex.
"""
if not isinstance(data_df.index, pd.DatetimeIndex):
raise ValueError("Input DataFrame must have a DatetimeIndex.")
agg_rules = check_data(data_df)
agg_rules = {}
# Define aggregation rules based on available columns
if 'open' in data_df.columns:
agg_rules['open'] = 'first'
if 'high' in data_df.columns:
agg_rules['high'] = 'max'
if 'low' in data_df.columns:
agg_rules['low'] = 'min'
if 'close' in data_df.columns:
agg_rules['close'] = 'last'
if 'volume' in data_df.columns:
agg_rules['volume'] = 'sum'
if not agg_rules:
# Log a warning or raise an error if no relevant columns are found
@@ -119,81 +58,3 @@ def aggregate_to_daily(data_df: pd.DataFrame) -> pd.DataFrame:
daily_data.dropna(how='all', inplace=True)
return daily_data
def aggregate_to_hourly(data_df: pd.DataFrame, hours: int = 1) -> pd.DataFrame:
"""
Aggregates time-series financial data to hourly OHLCV format.
The input DataFrame is expected to have a DatetimeIndex.
'open' will be the first 'open' price of the hour.
'close' will be the last 'close' price of the hour.
'high' will be the maximum 'high' price of the hour.
'low' will be the minimum 'low' price of the hour.
'volume' (if present) will be the sum of volumes for the hour.
Args:
data_df (pd.DataFrame): DataFrame with a DatetimeIndex and columns
like 'open', 'high', 'low', 'close', and optionally 'volume'.
hours (int): The number of hours to aggregate to. Default is 1.
Returns:
pd.DataFrame: DataFrame aggregated to hourly OHLCV data.
The index will be a DatetimeIndex with the time set to the start of the hour.
Returns an empty DataFrame if no relevant OHLCV columns are found.
"""
agg_rules = check_data(data_df)
if not agg_rules:
print("Warning: No standard OHLCV columns (open, high, low, close, volume) found for hourly aggregation.")
return pd.DataFrame(index=pd.to_datetime([]))
# Resample to hourly frequency and apply aggregation rules
hourly_data = data_df.resample(f'{hours}h').agg(agg_rules)
hourly_data.dropna(how='all', inplace=True)
# Adjust timestamps to the start of the hour
if not hourly_data.empty and isinstance(hourly_data.index, pd.DatetimeIndex):
hourly_data.index = hourly_data.index.floor('h')
return hourly_data
def aggregate_to_minutes(data_df: pd.DataFrame, minutes: int) -> pd.DataFrame:
"""
Aggregates time-series financial data to N-minute OHLCV format.
The input DataFrame is expected to have a DatetimeIndex.
'open' will be the first 'open' price of the N-minute interval.
'close' will be the last 'close' price of the N-minute interval.
'high' will be the maximum 'high' price of the N-minute interval.
'low' will be the minimum 'low' price of the N-minute interval.
'volume' (if present) will be the sum of volumes for the N-minute interval.
Args:
data_df (pd.DataFrame): DataFrame with a DatetimeIndex and columns
like 'open', 'high', 'low', 'close', and optionally 'volume'.
minutes (int): The number of minutes to aggregate to.
Returns:
pd.DataFrame: DataFrame aggregated to N-minute OHLCV data.
The index will be a DatetimeIndex.
Returns an empty DataFrame if no relevant OHLCV columns are found or
if the input DataFrame does not have a DatetimeIndex.
"""
agg_rules_obj = check_data(data_df) # check_data returns rules or False
if not agg_rules_obj:
# check_data already prints a warning if index is not DatetimeIndex or no OHLCV columns
# Ensure an empty DataFrame with a DatetimeIndex is returned for consistency
return pd.DataFrame(index=pd.to_datetime([]))
# Resample to N-minute frequency and apply aggregation rules
# Using .agg(agg_rules_obj) where agg_rules_obj is the dict from check_data
resampled_data = data_df.resample(f'{minutes}min').agg(agg_rules_obj)
resampled_data.dropna(how='all', inplace=True)
return resampled_data

View File

@@ -1,128 +0,0 @@
import threading
import time
import queue
from google.oauth2.service_account import Credentials
import gspread
import math
import numpy as np
from collections import defaultdict
class GSheetBatchPusher(threading.Thread):
def __init__(self, queue, timestamp, spreadsheet_name, interval=60, logging=None):
super().__init__(daemon=True)
self.queue = queue
self.timestamp = timestamp
self.spreadsheet_name = spreadsheet_name
self.interval = interval
self._stop_event = threading.Event()
self.logging = logging
def run(self):
while not self._stop_event.is_set():
self.push_all()
time.sleep(self.interval)
# Final push on stop
self.push_all()
def stop(self):
self._stop_event.set()
def push_all(self):
batch_results = []
batch_trades = []
while True:
try:
results, trades = self.queue.get_nowait()
batch_results.extend(results)
batch_trades.extend(trades)
except queue.Empty:
break
if batch_results or batch_trades:
self.write_results_per_combination_gsheet(batch_results, batch_trades, self.timestamp, self.spreadsheet_name)
def write_results_per_combination_gsheet(self, results_rows, trade_rows, timestamp, spreadsheet_name="GlimBit Backtest Results"):
scopes = [
"https://www.googleapis.com/auth/spreadsheets",
"https://www.googleapis.com/auth/drive"
]
creds = Credentials.from_service_account_file('credentials/service_account.json', scopes=scopes)
gc = gspread.authorize(creds)
sh = gc.open(spreadsheet_name)
try:
worksheet = sh.worksheet("Results")
except gspread.exceptions.WorksheetNotFound:
worksheet = sh.add_worksheet(title="Results", rows="1000", cols="20")
# Clear the worksheet before writing new results
worksheet.clear()
# Updated fieldnames to match your data rows
fieldnames = [
"timeframe", "stop_loss_pct", "n_trades", "n_stop_loss", "win_rate",
"max_drawdown", "avg_trade", "profit_ratio", "initial_usd", "final_usd"
]
def to_native(val):
if isinstance(val, (np.generic, np.ndarray)):
val = val.item()
if hasattr(val, 'isoformat'):
return val.isoformat()
# Handle inf, -inf, nan
if isinstance(val, float):
if math.isinf(val):
return "" if val > 0 else "-∞"
if math.isnan(val):
return ""
return val
# Write header if sheet is empty
if len(worksheet.get_all_values()) == 0:
worksheet.append_row(fieldnames)
for row in results_rows:
values = [to_native(row.get(field, "")) for field in fieldnames]
worksheet.append_row(values)
trades_fieldnames = [
"entry_time", "exit_time", "entry_price", "exit_price", "profit_pct", "type"
]
trades_by_combo = defaultdict(list)
for trade in trade_rows:
tf = trade.get("timeframe")
sl = trade.get("stop_loss_pct")
trades_by_combo[(tf, sl)].append(trade)
for (tf, sl), trades in trades_by_combo.items():
sl_percent = int(round(sl * 100))
sheet_name = f"Trades_{tf}_ST{sl_percent}%"
try:
trades_ws = sh.worksheet(sheet_name)
except gspread.exceptions.WorksheetNotFound:
trades_ws = sh.add_worksheet(title=sheet_name, rows="1000", cols="20")
# Clear the trades worksheet before writing new trades
trades_ws.clear()
if len(trades_ws.get_all_values()) == 0:
trades_ws.append_row(trades_fieldnames)
for trade in trades:
trade_row = [to_native(trade.get(field, "")) for field in trades_fieldnames]
try:
trades_ws.append_row(trade_row)
except gspread.exceptions.APIError as e:
if '429' in str(e):
if self.logging is not None:
self.logging.warning(f"Google Sheets API quota exceeded (429). Please wait one minute. Will retry on next batch push. Sheet: {sheet_name}")
# Re-queue the failed batch for retry
self.queue.put((results_rows, trade_rows))
return # Stop pushing for this batch, will retry next interval
else:
raise

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env python3
"""
Progress Manager for tracking multiple parallel backtest tasks
"""
import threading
import time
import sys
from typing import Dict, Optional, Callable
from dataclasses import dataclass
@dataclass
class TaskProgress:
"""Represents progress information for a single task"""
task_id: str
name: str
current: int
total: int
start_time: float
last_update: float
@property
def percentage(self) -> float:
"""Calculate completion percentage"""
if self.total == 0:
return 0.0
return (self.current / self.total) * 100
@property
def elapsed_time(self) -> float:
"""Calculate elapsed time in seconds"""
return time.time() - self.start_time
@property
def eta(self) -> Optional[float]:
"""Estimate time to completion in seconds"""
if self.current == 0 or self.percentage >= 100:
return None
elapsed = self.elapsed_time
rate = self.current / elapsed
remaining = self.total - self.current
return remaining / rate if rate > 0 else None
class ProgressManager:
"""Manages progress tracking for multiple parallel tasks"""
def __init__(self, update_interval: float = 1.0, display_width: int = 50):
"""
Initialize progress manager
Args:
update_interval: How often to update display (seconds)
display_width: Width of progress bar in characters
"""
self.tasks: Dict[str, TaskProgress] = {}
self.update_interval = update_interval
self.display_width = display_width
self.lock = threading.Lock()
self.display_thread: Optional[threading.Thread] = None
self.running = False
self.last_display_height = 0
def start_task(self, task_id: str, name: str, total: int) -> None:
"""
Start tracking a new task
Args:
task_id: Unique identifier for the task
name: Human-readable name for the task
total: Total number of steps in the task
"""
with self.lock:
self.tasks[task_id] = TaskProgress(
task_id=task_id,
name=name,
current=0,
total=total,
start_time=time.time(),
last_update=time.time()
)
def update_progress(self, task_id: str, current: int) -> None:
"""
Update progress for a specific task
Args:
task_id: Task identifier
current: Current progress value
"""
with self.lock:
if task_id in self.tasks:
self.tasks[task_id].current = current
self.tasks[task_id].last_update = time.time()
def complete_task(self, task_id: str) -> None:
"""
Mark a task as completed
Args:
task_id: Task identifier
"""
with self.lock:
if task_id in self.tasks:
task = self.tasks[task_id]
task.current = task.total
task.last_update = time.time()
def start_display(self) -> None:
"""Start the progress display thread"""
if not self.running:
self.running = True
self.display_thread = threading.Thread(target=self._display_loop, daemon=True)
self.display_thread.start()
def stop_display(self) -> None:
"""Stop the progress display thread"""
self.running = False
if self.display_thread:
self.display_thread.join(timeout=1.0)
self._clear_display()
def _display_loop(self) -> None:
"""Main loop for updating the progress display"""
while self.running:
self._update_display()
time.sleep(self.update_interval)
def _update_display(self) -> None:
"""Update the console display with current progress"""
with self.lock:
if not self.tasks:
return
# Clear previous display
self._clear_display()
# Build display lines
lines = []
for task in sorted(self.tasks.values(), key=lambda t: t.task_id):
line = self._format_progress_line(task)
lines.append(line)
# Print all lines
for line in lines:
print(line, flush=True)
self.last_display_height = len(lines)
def _clear_display(self) -> None:
"""Clear the previous progress display"""
if self.last_display_height > 0:
# Move cursor up and clear lines
for _ in range(self.last_display_height):
sys.stdout.write('\033[F') # Move cursor up one line
sys.stdout.write('\033[K') # Clear line
sys.stdout.flush()
def _format_progress_line(self, task: TaskProgress) -> str:
"""
Format a single progress line for display
Args:
task: TaskProgress instance
Returns:
Formatted progress string
"""
# Progress bar
filled_width = int(task.percentage / 100 * self.display_width)
bar = '' * filled_width + '' * (self.display_width - filled_width)
# Time information
elapsed_str = self._format_time(task.elapsed_time)
eta_str = self._format_time(task.eta) if task.eta else "N/A"
# Format line
line = (f"{task.name:<25}{bar}"
f"{task.percentage:5.1f}% "
f"({task.current:,}/{task.total:,}) "
f"{elapsed_str} ETA: {eta_str}")
return line
def _format_time(self, seconds: float) -> str:
"""
Format time duration for display
Args:
seconds: Time in seconds
Returns:
Formatted time string
"""
if seconds < 60:
return f"{seconds:.0f}s"
elif seconds < 3600:
minutes = seconds / 60
return f"{minutes:.1f}m"
else:
hours = seconds / 3600
return f"{hours:.1f}h"
def get_task_progress_callback(self, task_id: str) -> Callable[[int], None]:
"""
Get a progress callback function for a specific task
Args:
task_id: Task identifier
Returns:
Callback function that updates progress for this task
"""
def callback(current: int) -> None:
self.update_progress(task_id, current)
return callback
def all_tasks_completed(self) -> bool:
"""Check if all tasks are completed"""
with self.lock:
return all(task.current >= task.total for task in self.tasks.values())
def get_summary(self) -> str:
"""Get a summary of all tasks"""
with self.lock:
total_tasks = len(self.tasks)
completed_tasks = sum(1 for task in self.tasks.values()
if task.current >= task.total)
return f"Tasks: {completed_tasks}/{total_tasks} completed"

View File

@@ -0,0 +1,179 @@
import os
import csv
from typing import Dict, List, Optional, Any
from collections import defaultdict
import logging
from .storage_utils import DataSavingError
class ResultFormatter:
"""Handles formatting and writing of backtest results to CSV files"""
def __init__(self, results_dir: str, logging_instance: Optional[logging.Logger] = None):
"""Initialize result formatter
Args:
results_dir: Directory for saving result files
logging_instance: Optional logging instance
"""
self.results_dir = results_dir
self.logging = logging_instance
def format_row(self, row: Dict[str, Any]) -> Dict[str, str]:
"""Format a row for a combined results CSV file
Args:
row: Dictionary containing row data
Returns:
Dictionary with formatted values
"""
return {
"timeframe": row["timeframe"],
"stop_loss_pct": f"{row['stop_loss_pct']*100:.2f}%",
"n_trades": row["n_trades"],
"n_stop_loss": row["n_stop_loss"],
"win_rate": f"{row['win_rate']*100:.2f}%",
"max_drawdown": f"{row['max_drawdown']*100:.2f}%",
"avg_trade": f"{row['avg_trade']*100:.2f}%",
"profit_ratio": f"{row['profit_ratio']*100:.2f}%",
"final_usd": f"{row['final_usd']:.2f}",
"total_fees_usd": f"{row['total_fees_usd']:.2f}",
}
def write_results_chunk(self, filename: str, fieldnames: List[str],
rows: List[Dict], write_header: bool = False,
initial_usd: Optional[float] = None) -> None:
"""Write a chunk of results to a CSV file
Args:
filename: filename to write to
fieldnames: list of fieldnames
rows: list of rows
write_header: whether to write the header
initial_usd: initial USD value for header comment
Raises:
DataSavingError: If writing fails
"""
try:
mode = 'w' if write_header else 'a'
with open(filename, mode, newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if write_header:
if initial_usd is not None:
csvfile.write(f"# initial_usd: {initial_usd}\n")
writer.writeheader()
for row in rows:
# Only keep keys that are in fieldnames
filtered_row = {k: v for k, v in row.items() if k in fieldnames}
writer.writerow(filtered_row)
except Exception as e:
error_msg = f"Failed to write results chunk to {filename}: {e}"
if self.logging is not None:
self.logging.error(error_msg)
raise DataSavingError(error_msg) from e
def write_backtest_results(self, filename: str, fieldnames: List[str],
rows: List[Dict], metadata_lines: Optional[List[str]] = None) -> str:
"""Write combined backtest results to a CSV file
Args:
filename: filename to write to
fieldnames: list of fieldnames
rows: list of result dictionaries
metadata_lines: optional list of strings to write as header comments
Returns:
Full path to the written file
Raises:
DataSavingError: If writing fails
"""
try:
fname = os.path.join(self.results_dir, filename)
with open(fname, "w", newline="") as csvfile:
if metadata_lines:
for line in metadata_lines:
csvfile.write(f"{line}\n")
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter='\t')
writer.writeheader()
for row in rows:
writer.writerow(self.format_row(row))
if self.logging is not None:
self.logging.info(f"Combined results written to {fname}")
return fname
except Exception as e:
error_msg = f"Failed to write backtest results to {filename}: {e}"
if self.logging is not None:
self.logging.error(error_msg)
raise DataSavingError(error_msg) from e
def write_trades(self, all_trade_rows: List[Dict], trades_fieldnames: List[str]) -> None:
"""Write trades to separate CSV files grouped by timeframe and stop loss
Args:
all_trade_rows: list of trade dictionaries
trades_fieldnames: list of trade fieldnames
Raises:
DataSavingError: If writing fails
"""
try:
trades_by_combo = self._group_trades_by_combination(all_trade_rows)
for (tf, sl), trades in trades_by_combo.items():
self._write_single_trade_file(tf, sl, trades, trades_fieldnames)
except Exception as e:
error_msg = f"Failed to write trades: {e}"
if self.logging is not None:
self.logging.error(error_msg)
raise DataSavingError(error_msg) from e
def _group_trades_by_combination(self, all_trade_rows: List[Dict]) -> Dict:
"""Group trades by timeframe and stop loss combination
Args:
all_trade_rows: List of trade dictionaries
Returns:
Dictionary grouped by (timeframe, stop_loss_pct) tuples
"""
trades_by_combo = defaultdict(list)
for trade in all_trade_rows:
tf = trade.get("timeframe")
sl = trade.get("stop_loss_pct")
trades_by_combo[(tf, sl)].append(trade)
return trades_by_combo
def _write_single_trade_file(self, timeframe: str, stop_loss_pct: float,
trades: List[Dict], trades_fieldnames: List[str]) -> None:
"""Write trades for a single timeframe/stop-loss combination
Args:
timeframe: Timeframe identifier
stop_loss_pct: Stop loss percentage
trades: List of trades for this combination
trades_fieldnames: List of field names for trades
"""
sl_percent = int(round(stop_loss_pct * 100))
trades_filename = os.path.join(self.results_dir, f"trades_{timeframe}_ST{sl_percent}pct.csv")
with open(trades_filename, "w", newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=trades_fieldnames)
writer.writeheader()
for trade in trades:
writer.writerow({k: trade.get(k, "") for k in trades_fieldnames})
if self.logging is not None:
self.logging.info(f"Trades written to {trades_filename}")

View File

@@ -1,17 +1,32 @@
import os
import json
import pandas as pd
import csv
from collections import defaultdict
from typing import Optional, Union, Dict, Any, List
import logging
from .data_loader import DataLoader
from .data_saver import DataSaver
from .result_formatter import ResultFormatter
from .storage_utils import DataLoadingError, DataSavingError
RESULTS_DIR = "../results"
DATA_DIR = "../data"
RESULTS_DIR = "results"
DATA_DIR = "data"
class Storage:
"""Unified storage interface for data and results operations
Acts as a coordinator for DataLoader, DataSaver, and ResultFormatter components,
maintaining backward compatibility while providing a clean separation of concerns.
"""
"""Storage class for storing and loading results and data"""
def __init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR):
"""Initialize storage with component instances
Args:
logging: Optional logging instance
results_dir: Directory for results files
data_dir: Directory for data files
"""
self.results_dir = results_dir
self.data_dir = data_dir
self.logging = logging
@@ -20,196 +35,89 @@ class Storage:
os.makedirs(self.results_dir, exist_ok=True)
os.makedirs(self.data_dir, exist_ok=True)
def load_data(self, file_path, start_date, stop_date):
# Initialize component instances
self.data_loader = DataLoader(data_dir, logging)
self.data_saver = DataSaver(data_dir, logging)
self.result_formatter = ResultFormatter(results_dir, logging)
def load_data(self, file_path: str, start_date: Union[str, pd.Timestamp],
stop_date: Union[str, pd.Timestamp]) -> pd.DataFrame:
"""Load data with optimized dtypes and filtering, supporting CSV and JSON input
Args:
file_path: path to the data file
start_date: start date
stop_date: stop date
start_date: start date (string or datetime-like)
stop_date: stop date (string or datetime-like)
Returns:
pandas DataFrame
pandas DataFrame with timestamp index
Raises:
DataLoadingError: If data loading fails
"""
# Determine file type
_, ext = os.path.splitext(file_path)
ext = ext.lower()
try:
if ext == ".json":
with open(os.path.join(self.data_dir, file_path), 'r') as f:
raw = json.load(f)
data = pd.DataFrame(raw["Data"])
# Convert columns to lowercase
data.columns = data.columns.str.lower()
# Convert timestamp to datetime
data["timestamp"] = pd.to_datetime(data["timestamp"], unit="s")
# Filter by date range
data = data[(data["timestamp"] >= start_date) & (data["timestamp"] <= stop_date)]
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}")
return data.set_index("timestamp")
else:
# Define optimized dtypes
dtypes = {
'Open': 'float32',
'High': 'float32',
'Low': 'float32',
'Close': 'float32',
'Volume': 'float32'
}
# Read data with original capitalized column names
data = pd.read_csv(os.path.join(self.data_dir, file_path), dtype=dtypes)
return self.data_loader.load_data(file_path, start_date, stop_date)
# Convert timestamp to datetime
if 'Timestamp' in data.columns:
data['Timestamp'] = pd.to_datetime(data['Timestamp'], unit='s')
# Filter by date range
data = data[(data['Timestamp'] >= start_date) & (data['Timestamp'] <= stop_date)]
# Now convert column names to lowercase
data.columns = data.columns.str.lower()
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} for date range {start_date} to {stop_date}")
return data.set_index('timestamp')
else: # Attempt to use the first column if 'Timestamp' is not present
data.rename(columns={data.columns[0]: 'timestamp'}, inplace=True)
data['timestamp'] = pd.to_datetime(data['timestamp'], unit='s')
data = data[(data['timestamp'] >= start_date) & (data['timestamp'] <= stop_date)]
data.columns = data.columns.str.lower() # Ensure all other columns are lower
if self.logging is not None:
self.logging.info(f"Data loaded from {file_path} (using first column as timestamp) for date range {start_date} to {stop_date}")
return data.set_index('timestamp')
except Exception as e:
if self.logging is not None:
self.logging.error(f"Error loading data from {file_path}: {e}")
# Return an empty DataFrame with a DatetimeIndex
return pd.DataFrame(index=pd.to_datetime([]))
def save_data(self, data: pd.DataFrame, file_path: str):
"""Save processed data to a CSV file.
If the DataFrame has a DatetimeIndex, it's converted to float Unix timestamps
(seconds since epoch) before saving. The index is saved as a column named 'timestamp'.
def save_data(self, data: pd.DataFrame, file_path: str) -> None:
"""Save processed data to a CSV file
Args:
data (pd.DataFrame): data to save.
file_path (str): path to the data file relative to the data_dir.
data: DataFrame to save
file_path: path to the data file relative to the data_dir
Raises:
DataSavingError: If saving fails
"""
data_to_save = data.copy()
self.data_saver.save_data(data, file_path)
if isinstance(data_to_save.index, pd.DatetimeIndex):
# Convert DatetimeIndex to Unix timestamp (float seconds since epoch)
# and make it a column named 'timestamp'.
data_to_save['timestamp'] = data_to_save.index.astype('int64') / 1e9
# Reset index so 'timestamp' column is saved and old DatetimeIndex is not saved as a column.
# We want the 'timestamp' column to be the first one.
data_to_save.reset_index(drop=True, inplace=True)
# Ensure 'timestamp' is the first column if other columns exist
if 'timestamp' in data_to_save.columns and len(data_to_save.columns) > 1:
cols = ['timestamp'] + [col for col in data_to_save.columns if col != 'timestamp']
data_to_save = data_to_save[cols]
elif pd.api.types.is_numeric_dtype(data_to_save.index.dtype):
# If index is already numeric (e.g. float Unix timestamps from a previous save/load cycle),
# make it a column named 'timestamp'.
data_to_save['timestamp'] = data_to_save.index
data_to_save.reset_index(drop=True, inplace=True)
if 'timestamp' in data_to_save.columns and len(data_to_save.columns) > 1:
cols = ['timestamp'] + [col for col in data_to_save.columns if col != 'timestamp']
data_to_save = data_to_save[cols]
else:
# For other index types, or if no index that we want to specifically handle,
# save with the current index. pandas to_csv will handle it.
# This branch might be removed if we strictly expect either DatetimeIndex or a numeric one from previous save.
pass # data_to_save remains as is, to_csv will write its index if index=True
# Save to CSV, ensuring the 'timestamp' column (if created) is written, and not the DataFrame's active index.
full_path = os.path.join(self.data_dir, file_path)
data_to_save.to_csv(full_path, index=False) # index=False because timestamp is now a column
if self.logging is not None:
self.logging.info(f"Data saved to {full_path} with Unix timestamp column.")
def format_row(self, row):
def format_row(self, row: Dict[str, Any]) -> Dict[str, str]:
"""Format a row for a combined results CSV file
Args:
row: row to format
row: Dictionary containing row data
Returns:
formatted row
Dictionary with formatted values
"""
return self.result_formatter.format_row(row)
return {
"timeframe": row["timeframe"],
"stop_loss_pct": f"{row['stop_loss_pct']*100:.2f}%",
"n_trades": row["n_trades"],
"n_stop_loss": row["n_stop_loss"],
"win_rate": f"{row['win_rate']*100:.2f}%",
"max_drawdown": f"{row['max_drawdown']*100:.2f}%",
"avg_trade": f"{row['avg_trade']*100:.2f}%",
"profit_ratio": f"{row['profit_ratio']*100:.2f}%",
"final_usd": f"{row['final_usd']:.2f}",
"total_fees_usd": f"{row['total_fees_usd']:.2f}",
}
def write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None):
def write_results_chunk(self, filename: str, fieldnames: List[str],
rows: List[Dict], write_header: bool = False,
initial_usd: Optional[float] = None) -> None:
"""Write a chunk of results to a CSV file
Args:
filename: filename to write to
fieldnames: list of fieldnames
rows: list of rows
write_header: whether to write the header
initial_usd: initial USD
initial_usd: initial USD value for header comment
"""
mode = 'w' if write_header else 'a'
self.result_formatter.write_results_chunk(
filename, fieldnames, rows, write_header, initial_usd
)
with open(filename, mode, newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
if write_header:
csvfile.write(f"# initial_usd: {initial_usd}\n")
writer.writeheader()
def write_backtest_results(self, filename: str, fieldnames: List[str],
rows: List[Dict], metadata_lines: Optional[List[str]] = None) -> str:
"""Write combined backtest results to a CSV file
for row in rows:
# Only keep keys that are in fieldnames
filtered_row = {k: v for k, v in row.items() if k in fieldnames}
writer.writerow(filtered_row)
def write_backtest_results(self, filename, fieldnames, rows, metadata_lines=None):
"""Write a combined results to a CSV file
Args:
filename: filename to write to
fieldnames: list of fieldnames
rows: list of rows
rows: list of result dictionaries
metadata_lines: optional list of strings to write as header comments
"""
fname = os.path.join(self.results_dir, filename)
with open(fname, "w", newline="") as csvfile:
if metadata_lines:
for line in metadata_lines:
csvfile.write(f"{line}\n")
writer = csv.DictWriter(csvfile, fieldnames=fieldnames, delimiter='\t')
writer.writeheader()
for row in rows:
writer.writerow(self.format_row(row))
if self.logging is not None:
self.logging.info(f"Combined results written to {fname}")
def write_trades(self, all_trade_rows, trades_fieldnames):
"""Write trades to a CSV file
Returns:
Full path to the written file
"""
return self.result_formatter.write_backtest_results(
filename, fieldnames, rows, metadata_lines
)
def write_trades(self, all_trade_rows: List[Dict], trades_fieldnames: List[str]) -> None:
"""Write trades to separate CSV files grouped by timeframe and stop loss
Args:
all_trade_rows: list of trade rows
all_trade_rows: list of trade dictionaries
trades_fieldnames: list of trade fieldnames
logging: logging object
"""
trades_by_combo = defaultdict(list)
for trade in all_trade_rows:
tf = trade.get("timeframe")
sl = trade.get("stop_loss_pct")
trades_by_combo[(tf, sl)].append(trade)
for (tf, sl), trades in trades_by_combo.items():
sl_percent = int(round(sl * 100))
trades_filename = os.path.join(self.results_dir, f"trades_{tf}_ST{sl_percent}pct.csv")
with open(trades_filename, "w", newline="") as csvfile:
writer = csv.DictWriter(csvfile, fieldnames=trades_fieldnames)
writer.writeheader()
for trade in trades:
writer.writerow({k: trade.get(k, "") for k in trades_fieldnames})
if self.logging is not None:
self.logging.info(f"Trades written to {trades_filename}")
self.result_formatter.write_trades(all_trade_rows, trades_fieldnames)

View File

@@ -0,0 +1,73 @@
import pandas as pd
class TimestampParsingError(Exception):
"""Custom exception for timestamp parsing errors"""
pass
class DataLoadingError(Exception):
"""Custom exception for data loading errors"""
pass
class DataSavingError(Exception):
"""Custom exception for data saving errors"""
pass
def _parse_timestamp_column(data: pd.DataFrame, column_name: str) -> pd.DataFrame:
"""Parse timestamp column handling both Unix timestamps and datetime strings
Args:
data: DataFrame containing the timestamp column
column_name: Name of the timestamp column
Returns:
DataFrame with parsed timestamp column
Raises:
TimestampParsingError: If timestamp parsing fails
"""
try:
sample_timestamp = str(data[column_name].iloc[0])
try:
# Check if it's a Unix timestamp (numeric)
float(sample_timestamp)
# It's a Unix timestamp, convert using unit='s'
data[column_name] = pd.to_datetime(data[column_name], unit='s')
except ValueError:
# It's already in datetime string format, convert without unit
data[column_name] = pd.to_datetime(data[column_name])
return data
except Exception as e:
raise TimestampParsingError(f"Failed to parse timestamp column '{column_name}': {e}")
def _filter_by_date_range(data: pd.DataFrame, timestamp_col: str,
start_date: pd.Timestamp, stop_date: pd.Timestamp) -> pd.DataFrame:
"""Filter DataFrame by date range
Args:
data: DataFrame to filter
timestamp_col: Name of timestamp column
start_date: Start date for filtering
stop_date: Stop date for filtering
Returns:
Filtered DataFrame
"""
return data[(data[timestamp_col] >= start_date) & (data[timestamp_col] <= stop_date)]
def _normalize_column_names(data: pd.DataFrame) -> pd.DataFrame:
"""Convert all column names to lowercase
Args:
data: DataFrame to normalize
Returns:
DataFrame with lowercase column names
"""
data.columns = data.columns.str.lower()
return data

View File

@@ -10,10 +10,12 @@ class SystemUtils:
"""Determine optimal number of worker processes based on system resources"""
cpu_count = os.cpu_count() or 4
memory_gb = psutil.virtual_memory().total / (1024**3)
# Heuristic: Use 75% of cores, but cap based on available memory
# Assume each worker needs ~2GB for large datasets
workers_by_memory = max(1, int(memory_gb / 2))
workers_by_cpu = max(1, int(cpu_count * 0.75))
# OPTIMIZATION: More aggressive worker allocation for better performance
workers_by_memory = max(1, int(memory_gb / 2)) # 2GB per worker
workers_by_cpu = max(1, int(cpu_count * 0.8)) # Use 80% of CPU cores
optimal_workers = min(workers_by_cpu, workers_by_memory, 8) # Cap at 8 workers
if self.logging is not None:
self.logging.info(f"Using {min(workers_by_cpu, workers_by_memory)} workers for processing")
return min(workers_by_cpu, workers_by_memory)
self.logging.info(f"Using {optimal_workers} workers for processing (CPU-based: {workers_by_cpu}, Memory-based: {workers_by_memory})")
return optimal_workers

View File

@@ -1,3 +0,0 @@
- trading signal (add optional description, would have the type as 'METATREND','STOP LOSS', and so on, for entry and exit signals)
- stop loss and take profit maybe add separate module and update calculation with max from the entry, not only entry data, we can call them as a function name or class name when we create the trader

View File

@@ -8,7 +8,6 @@ The `Analysis` module includes classes for calculating common technical indicato
- **Relative Strength Index (RSI)**: Implemented in `cycles/Analysis/rsi.py`.
- **Bollinger Bands**: Implemented in `cycles/Analysis/boillinger_band.py`.
- Note: Trading strategies are detailed in `strategies.md`.
## Class: `RSI`
@@ -16,91 +15,64 @@ Found in `cycles/Analysis/rsi.py`.
Calculates the Relative Strength Index.
### Mathematical Model
The standard RSI calculation typically involves Wilder's smoothing for average gains and losses.
1. **Price Change (Delta)**: Difference between consecutive closing prices.
2. **Gain and Loss**: Separate positive (gain) and negative (loss, expressed as positive) price changes.
3. **Average Gain (AvgU)** and **Average Loss (AvgD)**: Smoothed averages of gains and losses over the RSI period. Wilder's smoothing is a specific type of exponential moving average (EMA):
- Initial AvgU/AvgD: Simple Moving Average (SMA) over the first `period` values.
- Subsequent AvgU: `(Previous AvgU * (period - 1) + Current Gain) / period`
- Subsequent AvgD: `(Previous AvgD * (period - 1) + Current Loss) / period`
4. **Relative Strength (RS)**:
1. **Average Gain (AvgU)** and **Average Loss (AvgD)** over 14 periods:
$$
RS = \\frac{\\text{AvgU}}{\\text{AvgD}}
\text{AvgU} = \frac{\sum \text{Upward Price Changes}}{14}, \quad \text{AvgD} = \frac{\sum \text{Downward Price Changes}}{14}
$$
5. **RSI**:
2. **Relative Strength (RS)**:
$$
RSI = 100 - \\frac{100}{1 + RS}
RS = \frac{\text{AvgU}}{\text{AvgD}}
$$
3. **RSI**:
$$
RSI = 100 - \frac{100}{1 + RS}
$$
Special conditions:
- If AvgD is 0: RSI is 100 if AvgU > 0, or 50 if AvgU is also 0 (neutral).
### `__init__(self, config: dict)`
### `__init__(self, period: int = 14)`
- **Description**: Initializes the RSI calculator.
- **Parameters**:\n - `config` (dict): Configuration dictionary. Must contain an `'rsi_period'` key with a positive integer value (e.g., `{'rsi_period': 14}`).
- **Parameters**:
- `period` (int, optional): The period for RSI calculation. Defaults to 14. Must be a positive integer.
### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame`
- **Description**: Calculates the RSI (using Wilder's smoothing by default) and adds it as an 'RSI' column to the input DataFrame. This method utilizes `calculate_custom_rsi` internally with `smoothing='EMA'`.
- **Parameters**:\n - `data_df` (pd.DataFrame): DataFrame with historical price data. Must contain the `price_column`.\n - `price_column` (str, optional): The name of the column containing price data. Defaults to 'close'.
- **Returns**: `pd.DataFrame` - A copy of the input DataFrame with an added 'RSI' column. If data length is insufficient for the period, the 'RSI' column will contain `np.nan`.
### `calculate_custom_rsi(price_series: pd.Series, window: int = 14, smoothing: str = 'SMA') -> pd.Series` (Static Method)
- **Description**: Calculates RSI with a specified window and smoothing method (SMA or EMA). This is the core calculation engine.
- **Description**: Calculates the RSI and adds it as an 'RSI' column to the input DataFrame. Handles cases where data length is less than the period by returning the original DataFrame with a warning.
- **Parameters**:
- `price_series` (pd.Series): Series of prices.
- `window` (int, optional): The period for RSI calculation. Defaults to 14. Must be a positive integer.
- `smoothing` (str, optional): Smoothing method, can be 'SMA' (Simple Moving Average) or 'EMA' (Exponential Moving Average, specifically Wilder's smoothing when `alpha = 1/window`). Defaults to 'SMA'.
- **Returns**: `pd.Series` - Series containing the RSI values. Returns a series of NaNs if data length is insufficient.
- `data_df` (pd.DataFrame): DataFrame with historical price data. Must contain the `price_column`.
- `price_column` (str, optional): The name of the column containing price data. Defaults to 'close'.
- **Returns**: `pd.DataFrame` - The input DataFrame with an added 'RSI' column (containing `np.nan` for initial periods where RSI cannot be calculated). Returns a copy of the original DataFrame if the period is larger than the number of data points.
## Class: `BollingerBands`
Found in `cycles/Analysis/boillinger_band.py`.
Calculates Bollinger Bands.
## **Bollinger Bands**
### Mathematical Model
1. **Middle Band**: Simple Moving Average (SMA) over `period`.
1. **Middle Band**: 20-day Simple Moving Average (SMA)
$$
\\text{Middle Band} = \\text{SMA}(\\text{price}, \\text{period})
\text{Middle Band} = \frac{1}{20} \sum_{i=1}^{20} \text{Close}_{t-i}
$$
2. **Standard Deviation (σ)**: Standard deviation of price over `period`.
3. **Upper Band**: Middle Band + `num_std` × σ
2. **Upper Band**: Middle Band + 2 × 20-day Standard Deviation (σ)
$$
\\text{Upper Band} = \\text{Middle Band} + \\text{num_std} \\times \\sigma_{\\text{period}}
\text{Upper Band} = \text{Middle Band} + 2 \times \sigma_{20}
$$
4. **Lower Band**: Middle Band `num_std` × σ
3. **Lower Band**: Middle Band 2 × 20-day Standard Deviation (σ)
$$
\\text{Lower Band} = \\text{Middle Band} - \\text{num_std} \\times \\sigma_{\\text{period}}
\text{Lower Band} = \text{Middle Band} - 2 \times \sigma_{20}
$$
For the adaptive calculation in the `calculate` method (when `squeeze=False`):
- **BBWidth**: `(Reference Upper Band - Reference Lower Band) / SMA`, where reference bands are typically calculated using a 2.0 standard deviation multiplier.
- **MarketRegime**: Determined by comparing `BBWidth` to a threshold from the configuration. `1` for sideways, `0` for trending.
- The `num_std` used for the final Upper and Lower Bands then varies based on this `MarketRegime` and the `bb_std_dev_multiplier` values for "trending" and "sideways" markets from the configuration, applied row-wise.
### `__init__(self, config: dict)`
### `__init__(self, period: int = 20, std_dev_multiplier: float = 2.0)`
- **Description**: Initializes the BollingerBands calculator.
- **Parameters**:\n - `config` (dict): Configuration dictionary. It must contain:
- `'bb_period'` (int): Positive integer for the moving average and standard deviation period.
- `'trending'` (dict): Containing `'bb_std_dev_multiplier'` (float, positive) for trending markets.
- `'sideways'` (dict): Containing `'bb_std_dev_multiplier'` (float, positive) for sideways markets.
- `'bb_width'` (float): Positive float threshold for determining market regime.
### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close', squeeze: bool = False) -> pd.DataFrame`
- **Description**: Calculates Bollinger Bands and adds relevant columns to the DataFrame.
- If `squeeze` is `False` (default): Calculates adaptive Bollinger Bands. It determines the market regime (trending/sideways) based on `BBWidth` and applies different standard deviation multipliers (from the `config`) on a row-by-row basis. Adds 'SMA', 'UpperBand', 'LowerBand', 'BBWidth', and 'MarketRegime' columns.
- If `squeeze` is `True`: Calculates simpler Bollinger Bands with a fixed window of 14 and a standard deviation multiplier of 1.5 by calling `calculate_custom_bands`. Adds 'SMA', 'UpperBand', 'LowerBand' columns; 'BBWidth' and 'MarketRegime' will be `NaN`.
- **Parameters**:\n - `data_df` (pd.DataFrame): DataFrame with price data. Must include the `price_column`.\n - `price_column` (str, optional): The name of the column containing the price data. Defaults to 'close'.\n - `squeeze` (bool, optional): If `True`, calculates bands with fixed parameters (window 14, std 1.5). Defaults to `False`.
- **Returns**: `pd.DataFrame` - A copy of the original DataFrame with added Bollinger Band related columns.
### `calculate_custom_bands(price_series: pd.Series, window: int = 20, num_std: float = 2.0, min_periods: int = None) -> tuple[pd.Series, pd.Series, pd.Series]` (Static Method)
- **Description**: Calculates Bollinger Bands with a specified window, standard deviation multiplier, and minimum periods.
- **Parameters**:
- `price_series` (pd.Series): Series of prices.
- `window` (int, optional): The period for the moving average and standard deviation. Defaults to 20.
- `num_std` (float, optional): The number of standard deviations for the upper and lower bands. Defaults to 2.0.
- `min_periods` (int, optional): Minimum number of observations in window required to have a value. Defaults to `window` if `None`.
- **Returns**: `tuple[pd.Series, pd.Series, pd.Series]` - A tuple containing the Upper band, SMA, and Lower band series.
- `period` (int, optional): The period for the moving average and standard deviation. Defaults to 20. Must be a positive integer.
- `std_dev_multiplier` (float, optional): The number of standard deviations for the upper and lower bands. Defaults to 2.0. Must be positive.
### `calculate(self, data_df: pd.DataFrame, price_column: str = 'close') -> pd.DataFrame`
- **Description**: Calculates Bollinger Bands and adds 'SMA' (Simple Moving Average), 'UpperBand', and 'LowerBand' columns to the DataFrame.
- **Parameters**:
- `data_df` (pd.DataFrame): DataFrame with price data. Must include the `price_column`.
- `price_column` (str, optional): The name of the column containing the price data (e.g., 'close'). Defaults to 'close'.
- **Returns**: `pd.DataFrame` - The original DataFrame with added columns: 'SMA', 'UpperBand', 'LowerBand'.

View File

@@ -1,405 +0,0 @@
# Strategies Documentation
## Overview
The Cycles framework implements advanced trading strategies with sophisticated timeframe management, signal processing, and multi-strategy combination capabilities. Each strategy can operate on its preferred timeframes while maintaining precise execution control.
## Architecture
### Strategy System Components
1. **StrategyBase**: Abstract base class with timeframe management
2. **Individual Strategies**: DefaultStrategy, BBRSStrategy implementations
3. **StrategyManager**: Multi-strategy orchestration and signal combination
4. **Timeframe System**: Automatic data resampling and signal mapping
### New Timeframe Management
Each strategy now controls its own timeframe requirements:
```python
class MyStrategy(StrategyBase):
def get_timeframes(self):
return ["15min", "1h"] # Strategy specifies needed timeframes
def initialize(self, backtester):
# Framework automatically resamples data
self._resample_data(backtester.original_df)
# Access resampled data
data_15m = self.get_data_for_timeframe("15min")
data_1h = self.get_data_for_timeframe("1h")
```
## Available Strategies
### 1. Default Strategy (Meta-Trend Analysis)
**Purpose**: Meta-trend analysis using multiple Supertrend indicators
**Timeframe Behavior**:
- **Configurable Primary Timeframe**: Set via `params["timeframe"]` (default: "15min")
- **1-Minute Precision**: Always includes 1min data for precise stop-loss execution
- **Example Timeframes**: `["15min", "1min"]` or `["5min", "1min"]`
**Configuration**:
```json
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "15min", // Configurable: "5min", "15min", "1h", etc.
"stop_loss_pct": 0.03 // Stop loss percentage
}
}
```
**Algorithm**:
1. Calculate 3 Supertrend indicators with different parameters on primary timeframe
2. Determine meta-trend: all three must agree for directional signal
3. **Entry**: Meta-trend changes from != 1 to == 1 (all trends align upward)
4. **Exit**: Meta-trend changes to -1 (trend reversal) or stop-loss triggered
5. **Stop-Loss**: 1-minute precision using percentage-based threshold
**Strengths**:
- Robust trend following with multiple confirmations
- Configurable for different market timeframes
- Precise risk management
- Low false signals in trending markets
**Best Use Cases**:
- Medium to long-term trend following
- Markets with clear directional movements
- Risk-conscious trading with defined exits
### 2. BBRS Strategy (Bollinger Bands + RSI)
**Purpose**: Market regime-adaptive strategy combining Bollinger Bands and RSI
**Timeframe Behavior**:
- **1-Minute Input**: Strategy receives 1-minute data
- **Internal Resampling**: Underlying Strategy class handles resampling to 15min/1h
- **No Double-Resampling**: Avoids conflicts with existing resampling logic
- **Signal Mapping**: Results mapped back to 1-minute resolution
**Configuration**:
```json
{
"name": "bbrs",
"weight": 1.0,
"params": {
"bb_width": 0.05, // Bollinger Band width threshold
"bb_period": 20, // Bollinger Band period
"rsi_period": 14, // RSI calculation period
"trending_rsi_threshold": [30, 70], // RSI thresholds for trending market
"trending_bb_multiplier": 2.5, // BB multiplier for trending market
"sideways_rsi_threshold": [40, 60], // RSI thresholds for sideways market
"sideways_bb_multiplier": 1.8, // BB multiplier for sideways market
"strategy_name": "MarketRegimeStrategy", // Implementation variant
"SqueezeStrategy": true, // Enable squeeze detection
"stop_loss_pct": 0.05 // Stop loss percentage
}
}
```
**Algorithm**:
**MarketRegimeStrategy** (Primary Implementation):
1. **Market Regime Detection**: Determines if market is trending or sideways
2. **Adaptive Parameters**: Adjusts BB/RSI thresholds based on market regime
3. **Trending Market Entry**: Price < Lower Band ∧ RSI < 50 ∧ Volume Spike
4. **Sideways Market Entry**: Price ≤ Lower Band ∧ RSI ≤ 40
5. **Exit Conditions**: Opposite band touch, RSI reversal, or stop-loss
6. **Volume Confirmation**: Requires 1.5× average volume for trending signals
**CryptoTradingStrategy** (Alternative Implementation):
1. **Multi-Timeframe Analysis**: Combines 15-minute and 1-hour Bollinger Bands
2. **Entry**: Price ≤ both 15m & 1h lower bands + RSI < 35 + Volume surge
3. **Exit**: 2:1 risk-reward ratio with ATR-based stops
4. **Adaptive Volatility**: Uses ATR for dynamic stop-loss/take-profit
**Strengths**:
- Adapts to different market regimes
- Multiple timeframe confirmation (internal)
- Volume analysis for signal quality
- Sophisticated entry/exit conditions
**Best Use Cases**:
- Volatile cryptocurrency markets
- Markets with alternating trending/sideways periods
- Short to medium-term trading
## Strategy Combination
### Multi-Strategy Architecture
The StrategyManager allows combining multiple strategies with configurable rules:
```json
{
"strategies": [
{
"name": "default",
"weight": 0.6,
"params": {"timeframe": "15min"}
},
{
"name": "bbrs",
"weight": 0.4,
"params": {"strategy_name": "MarketRegimeStrategy"}
}
],
"combination_rules": {
"entry": "weighted_consensus",
"exit": "any",
"min_confidence": 0.6
}
}
```
### Signal Combination Methods
**Entry Combinations**:
- **`any`**: Enter if ANY strategy signals entry
- **`all`**: Enter only if ALL strategies signal entry
- **`majority`**: Enter if majority of strategies signal entry
- **`weighted_consensus`**: Enter based on weighted confidence average
**Exit Combinations**:
- **`any`**: Exit if ANY strategy signals exit (recommended for risk management)
- **`all`**: Exit only if ALL strategies agree
- **`priority`**: Prioritized exit (STOP_LOSS > SELL_SIGNAL > others)
## Performance Characteristics
### Default Strategy Performance
**Strengths**:
- **Trend Accuracy**: High accuracy in strong trending markets
- **Risk Management**: Defined stop-losses with 1-minute precision
- **Low Noise**: Multiple Supertrend confirmation reduces false signals
- **Adaptable**: Works across different timeframes
**Weaknesses**:
- **Sideways Markets**: May generate false signals in ranging markets
- **Lag**: Multiple confirmations can delay entry/exit signals
- **Whipsaws**: Vulnerable to rapid trend reversals
**Optimal Conditions**:
- Clear trending markets
- Medium to low volatility trending
- Sufficient data history for Supertrend calculation
### BBRS Strategy Performance
**Strengths**:
- **Market Adaptation**: Automatically adjusts to market regime
- **Volume Confirmation**: Reduces false signals with volume analysis
- **Multi-Timeframe**: Internal analysis across multiple timeframes
- **Volatility Handling**: Designed for cryptocurrency volatility
**Weaknesses**:
- **Complexity**: More parameters to optimize
- **Market Noise**: Can be sensitive to short-term noise
- **Volume Dependency**: Requires reliable volume data
**Optimal Conditions**:
- High-volume cryptocurrency markets
- Markets with clear regime shifts
- Sufficient data for regime detection
## Usage Examples
### Single Strategy Backtests
```bash
# Default strategy on 15-minute timeframe
uv run .\main.py .\configs\config_default.json
# Default strategy on 5-minute timeframe
uv run .\main.py .\configs\config_default_5min.json
# BBRS strategy with market regime detection
uv run .\main.py .\configs\config_bbrs.json
```
### Multi-Strategy Backtests
```bash
# Combined strategies with weighted consensus
uv run .\main.py .\configs\config_combined.json
```
### Custom Configurations
**Aggressive Default Strategy**:
```json
{
"name": "default",
"params": {
"timeframe": "5min", // Faster signals
"stop_loss_pct": 0.02 // Tighter stop-loss
}
}
```
**Conservative BBRS Strategy**:
```json
{
"name": "bbrs",
"params": {
"bb_width": 0.03, // Tighter BB width
"stop_loss_pct": 0.07, // Wider stop-loss
"SqueezeStrategy": false // Disable squeeze for simplicity
}
}
```
## Development Guidelines
### Creating New Strategies
1. **Inherit from StrategyBase**:
```python
from cycles.strategies.base import StrategyBase, StrategySignal
class NewStrategy(StrategyBase):
def __init__(self, weight=1.0, params=None):
super().__init__("new_strategy", weight, params)
```
2. **Specify Timeframes**:
```python
def get_timeframes(self):
return ["1h"] # Specify required timeframes
```
3. **Implement Core Methods**:
```python
def initialize(self, backtester):
self._resample_data(backtester.original_df)
# Calculate indicators...
self.initialized = True
def get_entry_signal(self, backtester, df_index):
# Entry logic...
return StrategySignal("ENTRY", confidence=0.8)
def get_exit_signal(self, backtester, df_index):
# Exit logic...
return StrategySignal("EXIT", confidence=1.0)
```
4. **Register Strategy**:
```python
# In StrategyManager._load_strategies()
elif name == "new_strategy":
strategies.append(NewStrategy(weight, params))
```
### Timeframe Best Practices
1. **Minimize Timeframe Requirements**:
```python
def get_timeframes(self):
return ["15min"] # Only what's needed
```
2. **Include 1min for Stop-Loss**:
```python
def get_timeframes(self):
primary_tf = self.params.get("timeframe", "15min")
timeframes = [primary_tf]
if "1min" not in timeframes:
timeframes.append("1min")
return timeframes
```
3. **Handle Multi-Timeframe Synchronization**:
```python
def get_entry_signal(self, backtester, df_index):
# Get current timestamp from primary timeframe
primary_data = self.get_primary_timeframe_data()
current_time = primary_data.index[df_index]
# Map to other timeframes
hourly_data = self.get_data_for_timeframe("1h")
h1_idx = hourly_data.index.get_indexer([current_time], method='ffill')[0]
```
## Testing and Validation
### Strategy Testing Workflow
1. **Individual Strategy Testing**:
- Test each strategy independently
- Validate on different timeframes
- Check edge cases and data sufficiency
2. **Multi-Strategy Testing**:
- Test strategy combinations
- Validate combination rules
- Monitor for signal conflicts
3. **Timeframe Validation**:
- Ensure consistent behavior across timeframes
- Validate data alignment
- Check memory usage with large datasets
### Performance Monitoring
```python
# Get strategy summary
summary = strategy_manager.get_strategy_summary()
print(f"Strategies: {[s['name'] for s in summary['strategies']]}")
print(f"Timeframes: {summary['all_timeframes']}")
# Monitor individual strategy performance
for strategy in strategy_manager.strategies:
print(f"{strategy.name}: {strategy.get_timeframes()}")
```
## Advanced Topics
### Multi-Timeframe Strategy Development
For strategies requiring multiple timeframes:
```python
class MultiTimeframeStrategy(StrategyBase):
def get_timeframes(self):
return ["5min", "15min", "1h"]
def get_entry_signal(self, backtester, df_index):
# Analyze multiple timeframes
data_5m = self.get_data_for_timeframe("5min")
data_15m = self.get_data_for_timeframe("15min")
data_1h = self.get_data_for_timeframe("1h")
# Synchronize across timeframes
current_time = data_5m.index[df_index]
idx_15m = data_15m.index.get_indexer([current_time], method='ffill')[0]
idx_1h = data_1h.index.get_indexer([current_time], method='ffill')[0]
# Multi-timeframe logic
short_signal = self._analyze_5min(data_5m, df_index)
medium_signal = self._analyze_15min(data_15m, idx_15m)
long_signal = self._analyze_1h(data_1h, idx_1h)
# Combine signals with appropriate confidence
if short_signal and medium_signal and long_signal:
return StrategySignal("ENTRY", confidence=0.9)
elif short_signal and medium_signal:
return StrategySignal("ENTRY", confidence=0.7)
else:
return StrategySignal("HOLD", confidence=0.0)
```
### Strategy Optimization
1. **Parameter Optimization**: Systematic testing of strategy parameters
2. **Timeframe Optimization**: Finding optimal timeframes for each strategy
3. **Combination Optimization**: Optimizing weights and combination rules
4. **Market Regime Adaptation**: Adapting strategies to different market conditions
For detailed timeframe system documentation, see [Timeframe System](./timeframe_system.md).

View File

@@ -1,390 +0,0 @@
# Strategy Manager Documentation
## Overview
The Strategy Manager is a sophisticated orchestration system that enables the combination of multiple trading strategies with configurable signal aggregation rules. It supports multi-timeframe analysis, weighted consensus voting, and flexible signal combination methods.
## Architecture
### Core Components
1. **StrategyBase**: Abstract base class defining the strategy interface
2. **StrategySignal**: Encapsulates trading signals with confidence levels
3. **StrategyManager**: Orchestrates multiple strategies and combines signals
4. **Strategy Implementations**: DefaultStrategy, BBRSStrategy, etc.
### New Timeframe System
The framework now supports strategy-level timeframe management:
- **Strategy-Controlled Timeframes**: Each strategy specifies its required timeframes
- **Automatic Data Resampling**: Framework automatically resamples 1-minute data to strategy needs
- **Multi-Timeframe Support**: Strategies can use multiple timeframes simultaneously
- **Precision Stop-Loss**: All strategies maintain 1-minute data for precise execution
```python
class MyStrategy(StrategyBase):
def get_timeframes(self):
return ["15min", "1h"] # Strategy needs both timeframes
def initialize(self, backtester):
# Access resampled data
data_15m = self.get_data_for_timeframe("15min")
data_1h = self.get_data_for_timeframe("1h")
# Setup indicators...
```
## Strategy Interface
### StrategyBase Class
All strategies must inherit from `StrategyBase` and implement:
```python
from cycles.strategies.base import StrategyBase, StrategySignal
class MyStrategy(StrategyBase):
def get_timeframes(self) -> List[str]:
"""Specify required timeframes"""
return ["15min"]
def initialize(self, backtester) -> None:
"""Setup strategy with data"""
self._resample_data(backtester.original_df)
# Calculate indicators...
self.initialized = True
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""Generate entry signals"""
if condition_met:
return StrategySignal("ENTRY", confidence=0.8)
return StrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""Generate exit signals"""
if exit_condition:
return StrategySignal("EXIT", confidence=1.0,
metadata={"type": "SELL_SIGNAL"})
return StrategySignal("HOLD", confidence=0.0)
```
### StrategySignal Class
Encapsulates trading signals with metadata:
```python
# Entry signal with high confidence
entry_signal = StrategySignal("ENTRY", confidence=0.9)
# Exit signal with specific price
exit_signal = StrategySignal("EXIT", confidence=1.0, price=50000,
metadata={"type": "STOP_LOSS"})
# Hold signal
hold_signal = StrategySignal("HOLD", confidence=0.0)
```
## Available Strategies
### 1. Default Strategy
Meta-trend analysis using multiple Supertrend indicators.
**Features:**
- Uses 3 Supertrend indicators with different parameters
- Configurable timeframe (default: 15min)
- Entry when all trends align upward
- Exit on trend reversal or stop-loss
**Configuration:**
```json
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "15min",
"stop_loss_pct": 0.03
}
}
```
**Timeframes:**
- Primary: Configurable (default 15min)
- Stop-loss: Always includes 1min for precision
### 2. BBRS Strategy
Bollinger Bands + RSI with market regime detection.
**Features:**
- Market regime detection (trending vs sideways)
- Adaptive parameters based on market conditions
- Volume analysis and confirmation
- Multi-timeframe internal analysis (1min → 15min/1h)
**Configuration:**
```json
{
"name": "bbrs",
"weight": 1.0,
"params": {
"bb_width": 0.05,
"bb_period": 20,
"rsi_period": 14,
"strategy_name": "MarketRegimeStrategy",
"stop_loss_pct": 0.05
}
}
```
**Timeframes:**
- Input: 1min (Strategy class handles internal resampling)
- Internal: 15min, 1h (handled by underlying Strategy class)
- Output: Mapped back to 1min for backtesting
## Signal Combination
### Entry Signal Combination
```python
combination_rules = {
"entry": "weighted_consensus", # or "any", "all", "majority"
"min_confidence": 0.6
}
```
**Methods:**
- **`any`**: Enter if ANY strategy signals entry
- **`all`**: Enter only if ALL strategies signal entry
- **`majority`**: Enter if majority of strategies signal entry
- **`weighted_consensus`**: Enter based on weighted average confidence
### Exit Signal Combination
```python
combination_rules = {
"exit": "priority" # or "any", "all"
}
```
**Methods:**
- **`any`**: Exit if ANY strategy signals exit (recommended for risk management)
- **`all`**: Exit only if ALL strategies agree
- **`priority`**: Prioritized exit (STOP_LOSS > SELL_SIGNAL > others)
## Configuration
### Basic Strategy Manager Setup
```json
{
"strategies": [
{
"name": "default",
"weight": 0.6,
"params": {
"timeframe": "15min",
"stop_loss_pct": 0.03
}
},
{
"name": "bbrs",
"weight": 0.4,
"params": {
"bb_width": 0.05,
"strategy_name": "MarketRegimeStrategy"
}
}
],
"combination_rules": {
"entry": "weighted_consensus",
"exit": "any",
"min_confidence": 0.5
}
}
```
### Timeframe Examples
**Single Timeframe Strategy:**
```json
{
"name": "default",
"params": {
"timeframe": "5min" # Strategy works on 5-minute data
}
}
```
**Multi-Timeframe Strategy (Future Enhancement):**
```json
{
"name": "multi_tf_strategy",
"params": {
"timeframes": ["5min", "15min", "1h"], # Multiple timeframes
"primary_timeframe": "15min"
}
}
```
## Usage Examples
### Create Strategy Manager
```python
from cycles.strategies import create_strategy_manager
config = {
"strategies": [
{"name": "default", "weight": 1.0, "params": {"timeframe": "15min"}}
],
"combination_rules": {
"entry": "any",
"exit": "any"
}
}
strategy_manager = create_strategy_manager(config)
```
### Initialize and Use
```python
# Initialize with backtester
strategy_manager.initialize(backtester)
# Get signals during backtesting
entry_signal = strategy_manager.get_entry_signal(backtester, df_index)
exit_signal, exit_price = strategy_manager.get_exit_signal(backtester, df_index)
# Get strategy summary
summary = strategy_manager.get_strategy_summary()
print(f"Loaded strategies: {[s['name'] for s in summary['strategies']]}")
print(f"All timeframes: {summary['all_timeframes']}")
```
## Extending the System
### Adding New Strategies
1. **Create Strategy Class:**
```python
class NewStrategy(StrategyBase):
def get_timeframes(self):
return ["1h"] # Specify required timeframes
def initialize(self, backtester):
self._resample_data(backtester.original_df)
# Setup indicators...
self.initialized = True
def get_entry_signal(self, backtester, df_index):
# Implement entry logic
pass
def get_exit_signal(self, backtester, df_index):
# Implement exit logic
pass
```
2. **Register in StrategyManager:**
```python
# In StrategyManager._load_strategies()
elif name == "new_strategy":
strategies.append(NewStrategy(weight, params))
```
### Multi-Timeframe Strategy Development
For strategies requiring multiple timeframes:
```python
class MultiTimeframeStrategy(StrategyBase):
def get_timeframes(self):
return ["5min", "15min", "1h"]
def initialize(self, backtester):
self._resample_data(backtester.original_df)
# Access different timeframes
data_5m = self.get_data_for_timeframe("5min")
data_15m = self.get_data_for_timeframe("15min")
data_1h = self.get_data_for_timeframe("1h")
# Calculate indicators on each timeframe
# ...
def _calculate_signal_confidence(self, backtester, df_index):
# Analyze multiple timeframes for confidence
primary_signal = self._get_primary_signal(df_index)
confirmation = self._get_timeframe_confirmation(df_index)
return primary_signal * confirmation
```
## Performance Considerations
### Timeframe Management
- **Efficient Resampling**: Each strategy resamples data once during initialization
- **Memory Usage**: Only required timeframes are kept in memory
- **Signal Mapping**: Efficient mapping between timeframes using pandas reindex
### Strategy Combination
- **Lazy Evaluation**: Signals calculated only when needed
- **Error Handling**: Individual strategy failures don't crash the system
- **Logging**: Comprehensive logging for debugging and monitoring
## Best Practices
1. **Strategy Design:**
- Specify minimal required timeframes
- Include 1min for stop-loss precision
- Use confidence levels effectively
2. **Signal Combination:**
- Use `any` for exits (risk management)
- Use `weighted_consensus` for entries
- Set appropriate minimum confidence levels
3. **Error Handling:**
- Implement robust initialization checks
- Handle missing data gracefully
- Log strategy-specific warnings
4. **Testing:**
- Test strategies individually before combining
- Validate timeframe requirements
- Monitor memory usage with large datasets
## Troubleshooting
### Common Issues
1. **Timeframe Mismatches:**
- Ensure strategy specifies correct timeframes
- Check data availability for all timeframes
2. **Signal Conflicts:**
- Review combination rules
- Adjust confidence thresholds
- Monitor strategy weights
3. **Performance Issues:**
- Minimize timeframe requirements
- Optimize indicator calculations
- Use efficient pandas operations
### Debugging Tips
- Enable detailed logging: `logging.basicConfig(level=logging.DEBUG)`
- Use strategy summary: `manager.get_strategy_summary()`
- Test individual strategies before combining
- Monitor signal confidence levels
---
**Version**: 1.0.0
**Last Updated**: January 2025
**TCP Cycles Project**

View File

@@ -1,488 +0,0 @@
# Timeframe System Documentation
## Overview
The Cycles framework features a sophisticated timeframe management system that allows strategies to operate on their preferred timeframes while maintaining precise execution control. This system supports both single-timeframe and multi-timeframe strategies with automatic data resampling and intelligent signal mapping.
## Architecture
### Core Concepts
1. **Strategy-Controlled Timeframes**: Each strategy specifies its required timeframes
2. **Automatic Resampling**: Framework resamples 1-minute data to strategy needs
3. **Precision Execution**: All strategies maintain 1-minute data for accurate stop-loss execution
4. **Signal Mapping**: Intelligent mapping between different timeframe resolutions
### Data Flow
```
Original 1min Data
Strategy.get_timeframes() → ["15min", "1h"]
Automatic Resampling
Strategy Logic (15min + 1h analysis)
Signal Generation
Map to Working Timeframe
Backtesting Engine
```
## Strategy Timeframe Interface
### StrategyBase Methods
All strategies inherit timeframe capabilities from `StrategyBase`:
```python
class MyStrategy(StrategyBase):
def get_timeframes(self) -> List[str]:
"""Specify required timeframes for this strategy"""
return ["15min", "1h"] # Strategy needs both timeframes
def initialize(self, backtester) -> None:
# Automatic resampling happens here
self._resample_data(backtester.original_df)
# Access resampled data
data_15m = self.get_data_for_timeframe("15min")
data_1h = self.get_data_for_timeframe("1h")
# Calculate indicators on each timeframe
self.indicators_15m = self._calculate_indicators(data_15m)
self.indicators_1h = self._calculate_indicators(data_1h)
self.initialized = True
```
### Data Access Methods
```python
# Get data for specific timeframe
data_15m = strategy.get_data_for_timeframe("15min")
# Get primary timeframe data (first in list)
primary_data = strategy.get_primary_timeframe_data()
# Check available timeframes
timeframes = strategy.get_timeframes()
```
## Supported Timeframes
### Standard Timeframes
- **`"1min"`**: 1-minute bars (original resolution)
- **`"5min"`**: 5-minute bars
- **`"15min"`**: 15-minute bars
- **`"30min"`**: 30-minute bars
- **`"1h"`**: 1-hour bars
- **`"4h"`**: 4-hour bars
- **`"1d"`**: Daily bars
### Custom Timeframes
Any pandas-compatible frequency string is supported:
- **`"2min"`**: 2-minute bars
- **`"10min"`**: 10-minute bars
- **`"2h"`**: 2-hour bars
- **`"12h"`**: 12-hour bars
## Strategy Examples
### Single Timeframe Strategy
```python
class SingleTimeframeStrategy(StrategyBase):
def get_timeframes(self):
return ["15min"] # Only needs 15-minute data
def initialize(self, backtester):
self._resample_data(backtester.original_df)
# Work with 15-minute data
data = self.get_primary_timeframe_data()
self.indicators = self._calculate_indicators(data)
self.initialized = True
def get_entry_signal(self, backtester, df_index):
# df_index refers to 15-minute data
if self.indicators['signal'][df_index]:
return StrategySignal("ENTRY", confidence=0.8)
return StrategySignal("HOLD", confidence=0.0)
```
### Multi-Timeframe Strategy
```python
class MultiTimeframeStrategy(StrategyBase):
def get_timeframes(self):
return ["15min", "1h", "4h"] # Multiple timeframes
def initialize(self, backtester):
self._resample_data(backtester.original_df)
# Access different timeframes
self.data_15m = self.get_data_for_timeframe("15min")
self.data_1h = self.get_data_for_timeframe("1h")
self.data_4h = self.get_data_for_timeframe("4h")
# Calculate indicators on each timeframe
self.trend_4h = self._calculate_trend(self.data_4h)
self.momentum_1h = self._calculate_momentum(self.data_1h)
self.entry_signals_15m = self._calculate_entries(self.data_15m)
self.initialized = True
def get_entry_signal(self, backtester, df_index):
# Primary timeframe is 15min (first in list)
# Map df_index to other timeframes for confirmation
# Get current 15min timestamp
current_time = self.data_15m.index[df_index]
# Find corresponding indices in other timeframes
h1_idx = self.data_1h.index.get_indexer([current_time], method='ffill')[0]
h4_idx = self.data_4h.index.get_indexer([current_time], method='ffill')[0]
# Multi-timeframe confirmation
trend_ok = self.trend_4h[h4_idx] > 0
momentum_ok = self.momentum_1h[h1_idx] > 0.5
entry_signal = self.entry_signals_15m[df_index]
if trend_ok and momentum_ok and entry_signal:
confidence = 0.9 # High confidence with all timeframes aligned
return StrategySignal("ENTRY", confidence=confidence)
return StrategySignal("HOLD", confidence=0.0)
```
### Configurable Timeframe Strategy
```python
class ConfigurableStrategy(StrategyBase):
def get_timeframes(self):
# Strategy timeframe configurable via parameters
primary_tf = self.params.get("timeframe", "15min")
return [primary_tf, "1min"] # Primary + 1min for stop-loss
def initialize(self, backtester):
self._resample_data(backtester.original_df)
primary_tf = self.get_timeframes()[0]
self.data = self.get_data_for_timeframe(primary_tf)
# Indicator parameters can also be timeframe-dependent
if primary_tf == "5min":
self.ma_period = 20
elif primary_tf == "15min":
self.ma_period = 14
else:
self.ma_period = 10
self.indicators = self._calculate_indicators(self.data)
self.initialized = True
```
## Built-in Strategy Timeframe Behavior
### Default Strategy
**Timeframes**: Configurable primary + 1min for stop-loss
```python
# Configuration
{
"name": "default",
"params": {
"timeframe": "5min" # Configurable timeframe
}
}
# Resulting timeframes: ["5min", "1min"]
```
**Features**:
- Supertrend analysis on configured timeframe
- 1-minute precision for stop-loss execution
- Optimized for 15-minute default, but works on any timeframe
### BBRS Strategy
**Timeframes**: 1min input (internal resampling)
```python
# Configuration
{
"name": "bbrs",
"params": {
"strategy_name": "MarketRegimeStrategy"
}
}
# Resulting timeframes: ["1min"]
```
**Features**:
- Uses 1-minute data as input
- Internal resampling to 15min/1h by Strategy class
- Signals mapped back to 1-minute resolution
- No double-resampling issues
## Advanced Features
### Timeframe Synchronization
When working with multiple timeframes, synchronization is crucial:
```python
def _get_synchronized_signals(self, df_index, primary_timeframe="15min"):
"""Get signals synchronized across timeframes"""
# Get timestamp from primary timeframe
primary_data = self.get_data_for_timeframe(primary_timeframe)
current_time = primary_data.index[df_index]
signals = {}
for tf in self.get_timeframes():
if tf == primary_timeframe:
signals[tf] = df_index
else:
# Find corresponding index in other timeframe
tf_data = self.get_data_for_timeframe(tf)
tf_idx = tf_data.index.get_indexer([current_time], method='ffill')[0]
signals[tf] = tf_idx
return signals
```
### Dynamic Timeframe Selection
Strategies can adapt timeframes based on market conditions:
```python
class AdaptiveStrategy(StrategyBase):
def get_timeframes(self):
# Fixed set of timeframes strategy might need
return ["5min", "15min", "1h"]
def _select_active_timeframe(self, market_volatility):
"""Select timeframe based on market conditions"""
if market_volatility > 0.8:
return "5min" # High volatility -> shorter timeframe
elif market_volatility > 0.4:
return "15min" # Medium volatility -> medium timeframe
else:
return "1h" # Low volatility -> longer timeframe
def get_entry_signal(self, backtester, df_index):
# Calculate market volatility
volatility = self._calculate_volatility(df_index)
# Select appropriate timeframe
active_tf = self._select_active_timeframe(volatility)
# Generate signal on selected timeframe
return self._generate_signal_for_timeframe(active_tf, df_index)
```
## Configuration Examples
### Single Timeframe Configuration
```json
{
"strategies": [
{
"name": "default",
"weight": 1.0,
"params": {
"timeframe": "15min",
"stop_loss_pct": 0.03
}
}
]
}
```
### Multi-Timeframe Configuration
```json
{
"strategies": [
{
"name": "multi_timeframe_strategy",
"weight": 1.0,
"params": {
"primary_timeframe": "15min",
"confirmation_timeframes": ["1h", "4h"],
"signal_timeframe": "5min"
}
}
]
}
```
### Mixed Strategy Configuration
```json
{
"strategies": [
{
"name": "default",
"weight": 0.6,
"params": {
"timeframe": "15min"
}
},
{
"name": "bbrs",
"weight": 0.4,
"params": {
"strategy_name": "MarketRegimeStrategy"
}
}
]
}
```
## Performance Considerations
### Memory Usage
- Only required timeframes are resampled and stored
- Original 1-minute data shared across all strategies
- Efficient pandas resampling with minimal memory overhead
### Processing Speed
- Resampling happens once during initialization
- No repeated resampling during backtesting
- Vectorized operations on pre-computed timeframes
### Data Alignment
- All timeframes aligned to original 1-minute timestamps
- Forward-fill resampling ensures data availability
- Intelligent handling of missing data points
## Best Practices
### 1. Minimize Timeframe Requirements
```python
# Good - minimal timeframes
def get_timeframes(self):
return ["15min"]
# Less optimal - unnecessary timeframes
def get_timeframes(self):
return ["1min", "5min", "15min", "1h", "4h", "1d"]
```
### 2. Use Appropriate Timeframes for Strategy Logic
```python
# Good - timeframe matches strategy logic
class TrendStrategy(StrategyBase):
def get_timeframes(self):
return ["1h"] # Trend analysis works well on hourly data
class ScalpingStrategy(StrategyBase):
def get_timeframes(self):
return ["1min", "5min"] # Scalping needs fine-grained data
```
### 3. Include 1min for Stop-Loss Precision
```python
def get_timeframes(self):
primary_tf = self.params.get("timeframe", "15min")
timeframes = [primary_tf]
# Always include 1min for precise stop-loss
if "1min" not in timeframes:
timeframes.append("1min")
return timeframes
```
### 4. Handle Timeframe Edge Cases
```python
def get_entry_signal(self, backtester, df_index):
# Check bounds for all timeframes
if df_index >= len(self.get_primary_timeframe_data()):
return StrategySignal("HOLD", confidence=0.0)
# Robust timeframe indexing
try:
signal = self._calculate_signal(df_index)
return signal
except IndexError:
return StrategySignal("HOLD", confidence=0.0)
```
## Troubleshooting
### Common Issues
1. **Index Out of Bounds**
```python
# Problem: Different timeframes have different lengths
# Solution: Always check bounds
if df_index < len(self.data_1h):
signal = self.data_1h[df_index]
```
2. **Timeframe Misalignment**
```python
# Problem: Assuming same index across timeframes
# Solution: Use timestamp-based alignment
current_time = primary_data.index[df_index]
h1_idx = hourly_data.index.get_indexer([current_time], method='ffill')[0]
```
3. **Memory Issues with Large Datasets**
```python
# Solution: Only include necessary timeframes
def get_timeframes(self):
# Return minimal set
return ["15min"] # Not ["1min", "5min", "15min", "1h"]
```
### Debugging Tips
```python
# Log timeframe information
def initialize(self, backtester):
self._resample_data(backtester.original_df)
for tf in self.get_timeframes():
data = self.get_data_for_timeframe(tf)
print(f"Timeframe {tf}: {len(data)} bars, "
f"from {data.index[0]} to {data.index[-1]}")
self.initialized = True
```
## Future Enhancements
### Planned Features
1. **Dynamic Timeframe Switching**: Strategies adapt timeframes based on market conditions
2. **Timeframe Confidence Weighting**: Different confidence levels per timeframe
3. **Cross-Timeframe Signal Validation**: Automatic signal confirmation across timeframes
4. **Optimized Memory Management**: Lazy loading and caching for large datasets
### Extension Points
The timeframe system is designed for easy extension:
- Custom resampling methods
- Alternative timeframe synchronization strategies
- Market-specific timeframe preferences
- Real-time timeframe adaptation

View File

@@ -1,73 +1,207 @@
# Storage Utilities
This document describes the storage utility functions found in `cycles/utils/storage.py`.
This document describes the refactored storage utilities found in `cycles/utils/` that provide modular, maintainable data and results management.
## Overview
The `storage.py` module provides a `Storage` class designed for handling the loading and saving of data and results. It supports operations with CSV and JSON files and integrates with pandas DataFrames for data manipulation. The class also manages the creation of necessary `results` and `data` directories.
The storage utilities have been refactored into a modular architecture with clear separation of concerns:
- **`Storage`** - Main coordinator class providing unified interface (backward compatible)
- **`DataLoader`** - Handles loading data from various file formats
- **`DataSaver`** - Manages saving data with proper format handling
- **`ResultFormatter`** - Formats and writes backtest results to CSV files
- **`storage_utils`** - Shared utilities and custom exceptions
This design improves maintainability, testability, and follows the single responsibility principle.
## Constants
- `RESULTS_DIR`: Defines the default directory name for storing results (default: "results").
- `DATA_DIR`: Defines the default directory name for storing input data (default: "data").
- `RESULTS_DIR`: Default directory for storing results (default: "../results")
- `DATA_DIR`: Default directory for storing input data (default: "../data")
## Class: `Storage`
## Main Classes
Handles storage operations for data and results.
### `Storage` (Coordinator Class)
### `__init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR)`
The main interface that coordinates all storage operations while maintaining backward compatibility.
- **Description**: Initializes the `Storage` class. It creates the results and data directories if they don't already exist.
- **Parameters**:
- `logging` (optional): A logging instance for outputting information. Defaults to `None`.
- `results_dir` (str, optional): Path to the directory for storing results. Defaults to `RESULTS_DIR`.
- `data_dir` (str, optional): Path to the directory for storing data. Defaults to `DATA_DIR`.
#### `__init__(self, logging=None, results_dir=RESULTS_DIR, data_dir=DATA_DIR)`
### `load_data(self, file_path, start_date, stop_date)`
**Description**: Initializes the Storage coordinator with component instances.
- **Description**: Loads data from a specified file (CSV or JSON), performs type optimization, filters by date range, and converts column names to lowercase. The timestamp column is set as the DataFrame index.
- **Parameters**:
- `file_path` (str): Path to the data file (relative to `data_dir`).
- `start_date` (datetime-like): The start date for filtering data.
- `stop_date` (datetime-like): The end date for filtering data.
- **Returns**: `pandas.DataFrame` - The loaded and processed data, with a `timestamp` index. Returns an empty DataFrame on error.
**Parameters**:
- `logging` (optional): A logging instance for outputting information
- `results_dir` (str, optional): Path to the directory for storing results
- `data_dir` (str, optional): Path to the directory for storing data
### `save_data(self, data: pd.DataFrame, file_path: str)`
**Creates**: Component instances for DataLoader, DataSaver, and ResultFormatter
- **Description**: Saves a pandas DataFrame to a CSV file within the `data_dir`. If the DataFrame has a DatetimeIndex, it's converted to a Unix timestamp (seconds since epoch) and stored in a column named 'timestamp', which becomes the first column in the CSV. The DataFrame's active index is not saved if a 'timestamp' column is created.
- **Parameters**:
- `data` (pd.DataFrame): The DataFrame to save.
- `file_path` (str): Path to the data file (relative to `data_dir`).
#### `load_data(self, file_path: str, start_date: Union[str, pd.Timestamp], stop_date: Union[str, pd.Timestamp]) -> pd.DataFrame`
### `format_row(self, row)`
**Description**: Loads data with optimized dtypes and filtering, supporting CSV and JSON input.
- **Description**: Formats a dictionary row for output to a combined results CSV file, applying specific string formatting for percentages and float values.
- **Parameters**:
- `row` (dict): The row of data to format.
- **Returns**: `dict` - The formatted row.
**Parameters**:
- `file_path` (str): Path to the data file (relative to `data_dir`)
- `start_date` (datetime-like): The start date for filtering data
- `stop_date` (datetime-like): The end date for filtering data
### `write_results_chunk(self, filename, fieldnames, rows, write_header=False, initial_usd=None)`
**Returns**: `pandas.DataFrame` with timestamp index
- **Description**: Writes a chunk of results (list of dictionaries) to a CSV file. Can append to an existing file or write a new one with a header. An optional `initial_usd` can be written as a comment in the header.
- **Parameters**:
- `filename` (str): The name of the file to write to (path is absolute or relative to current working dir).
- `fieldnames` (list): A list of strings representing the CSV header/column names.
- `rows` (list): A list of dictionaries, where each dictionary is a row.
- `write_header` (bool, optional): If `True`, writes the header. Defaults to `False`.
- `initial_usd` (numeric, optional): If provided and `write_header` is `True`, this value is written as a comment in the CSV header. Defaults to `None`.
**Raises**: `DataLoadingError` if loading fails
### `write_results_combined(self, filename, fieldnames, rows)`
#### `save_data(self, data: pd.DataFrame, file_path: str) -> None`
- **Description**: Writes combined results to a CSV file in the `results_dir`. Uses tab as a delimiter and formats rows using `format_row`.
- **Parameters**:
- `filename` (str): The name of the file to write to (relative to `results_dir`).
- `fieldnames` (list): A list of strings representing the CSV header/column names.
- `rows` (list): A list of dictionaries, where each dictionary is a row.
**Description**: Saves processed data to a CSV file with proper timestamp handling.
### `write_trades(self, all_trade_rows, trades_fieldnames)`
**Parameters**:
- `data` (pd.DataFrame): The DataFrame to save
- `file_path` (str): Path to the data file (relative to `data_dir`)
- **Description**: Writes trade data to separate CSV files based on timeframe and stop-loss percentage. Files are named `trades_{tf}_ST{sl_percent}pct.csv` and stored in `results_dir`.
- **Parameters**:
- `all_trade_rows` (list): A list of dictionaries, where each dictionary represents a trade.
- `trades_fieldnames` (list): A list of strings for the CSV header of trade files.
**Raises**: `DataSavingError` if saving fails
#### `format_row(self, row: Dict[str, Any]) -> Dict[str, str]`
**Description**: Formats a dictionary row for output to results CSV files.
**Parameters**:
- `row` (dict): The row of data to format
**Returns**: `dict` with formatted values (percentages, currency, etc.)
#### `write_results_chunk(self, filename: str, fieldnames: List[str], rows: List[Dict], write_header: bool = False, initial_usd: Optional[float] = None) -> None`
**Description**: Writes a chunk of results to a CSV file with optional header.
**Parameters**:
- `filename` (str): The name of the file to write to
- `fieldnames` (list): CSV header/column names
- `rows` (list): List of dictionaries representing rows
- `write_header` (bool, optional): Whether to write the header
- `initial_usd` (float, optional): Initial USD value for header comment
#### `write_backtest_results(self, filename: str, fieldnames: List[str], rows: List[Dict], metadata_lines: Optional[List[str]] = None) -> str`
**Description**: Writes combined backtest results to a CSV file with metadata.
**Parameters**:
- `filename` (str): Name of the file to write to (relative to `results_dir`)
- `fieldnames` (list): CSV header/column names
- `rows` (list): List of result dictionaries
- `metadata_lines` (list, optional): Header comment lines
**Returns**: Full path to the written file
#### `write_trades(self, all_trade_rows: List[Dict], trades_fieldnames: List[str]) -> None`
**Description**: Writes trade data to separate CSV files grouped by timeframe and stop-loss.
**Parameters**:
- `all_trade_rows` (list): List of trade dictionaries
- `trades_fieldnames` (list): CSV header for trade files
**Files Created**: `trades_{timeframe}_ST{sl_percent}pct.csv` in `results_dir`
### `DataLoader`
Handles loading and preprocessing of data from various file formats.
#### Key Features:
- Supports CSV and JSON formats
- Optimized pandas dtypes for financial data
- Intelligent timestamp parsing (Unix timestamps and datetime strings)
- Date range filtering
- Column name normalization (lowercase)
- Comprehensive error handling
#### Methods:
- `load_data()` - Main loading interface
- `_load_json_data()` - JSON-specific loading logic
- `_load_csv_data()` - CSV-specific loading logic
- `_process_csv_timestamps()` - Timestamp parsing for CSV data
### `DataSaver`
Manages saving data with proper format handling and index conversion.
#### Key Features:
- Converts DatetimeIndex to Unix timestamps for CSV compatibility
- Handles numeric indexes appropriately
- Ensures 'timestamp' column is first in output
- Comprehensive error handling and logging
#### Methods:
- `save_data()` - Main saving interface
- `_prepare_data_for_saving()` - Data preparation logic
- `_convert_datetime_index_to_timestamp()` - DatetimeIndex conversion
- `_convert_numeric_index_to_timestamp()` - Numeric index conversion
### `ResultFormatter`
Handles formatting and writing of backtest results to CSV files.
#### Key Features:
- Consistent formatting for percentages and currency
- Grouped trade file writing by timeframe/stop-loss
- Metadata header support
- Tab-delimited output for results
- Error handling for all write operations
#### Methods:
- `format_row()` - Format individual result rows
- `write_results_chunk()` - Write result chunks with headers
- `write_backtest_results()` - Write combined results with metadata
- `write_trades()` - Write grouped trade files
## Utility Functions and Exceptions
### Custom Exceptions
- **`TimestampParsingError`** - Raised when timestamp parsing fails
- **`DataLoadingError`** - Raised when data loading operations fail
- **`DataSavingError`** - Raised when data saving operations fail
### Utility Functions
- **`_parse_timestamp_column()`** - Parse timestamp columns with format detection
- **`_filter_by_date_range()`** - Filter DataFrames by date range
- **`_normalize_column_names()`** - Convert column names to lowercase
## Architecture Benefits
### Separation of Concerns
- Each class has a single, well-defined responsibility
- Data loading, saving, and result formatting are cleanly separated
- Shared utilities are extracted to prevent code duplication
### Maintainability
- All files are under 250 lines (quality gate)
- All methods are under 50 lines (quality gate)
- Clear interfaces and comprehensive documentation
- Type hints for better IDE support and clarity
### Error Handling
- Custom exceptions for different error types
- Consistent error logging patterns
- Graceful degradation (empty DataFrames on load failure)
### Backward Compatibility
- Storage class maintains exact same public interface
- All existing code continues to work unchanged
- Component classes are available for advanced usage
## Migration Notes
The refactoring maintains full backward compatibility. Existing code using `Storage` will continue to work unchanged. For new code, consider using the component classes directly for more focused functionality:
```python
# Existing pattern (still works)
from cycles.utils.storage import Storage
storage = Storage(logging=logger)
data = storage.load_data('file.csv', start, end)
# New pattern for focused usage
from cycles.utils.data_loader import DataLoader
loader = DataLoader(data_dir, logger)
data = loader.load_data('file.csv', start, end)
```

Some files were not shown because too many files have changed in this diff Show More