Spaces:
Sleeping
Sleeping
Upload 465 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +18 -0
- README.md +27 -5
- app.py +45 -0
- backtrader/mcp_output/README_MCP.md +78 -0
- backtrader/mcp_output/analysis.json +0 -0
- backtrader/mcp_output/diff_report.md +77 -0
- backtrader/mcp_output/mcp_plugin/__init__.py +0 -0
- backtrader/mcp_output/mcp_plugin/adapter.py +194 -0
- backtrader/mcp_output/mcp_plugin/main.py +13 -0
- backtrader/mcp_output/mcp_plugin/mcp_service.py +302 -0
- backtrader/mcp_output/requirements.txt +8 -0
- backtrader/mcp_output/start_mcp.py +30 -0
- backtrader/mcp_output/workflow_summary.json +215 -0
- backtrader/source/.travis.yml +22 -0
- backtrader/source/LICENSE +674 -0
- backtrader/source/README.rst +170 -0
- backtrader/source/__init__.py +4 -0
- backtrader/source/backtrader/__init__.py +90 -0
- backtrader/source/backtrader/analyzer.py +446 -0
- backtrader/source/backtrader/analyzers/__init__.py +43 -0
- backtrader/source/backtrader/analyzers/annualreturn.py +89 -0
- backtrader/source/backtrader/analyzers/calmar.py +113 -0
- backtrader/source/backtrader/analyzers/drawdown.py +197 -0
- backtrader/source/backtrader/analyzers/leverage.py +71 -0
- backtrader/source/backtrader/analyzers/logreturnsrolling.py +140 -0
- backtrader/source/backtrader/analyzers/periodstats.py +112 -0
- backtrader/source/backtrader/analyzers/positions.py +85 -0
- backtrader/source/backtrader/analyzers/pyfolio.py +163 -0
- backtrader/source/backtrader/analyzers/returns.py +155 -0
- backtrader/source/backtrader/analyzers/sharpe.py +221 -0
- backtrader/source/backtrader/analyzers/sqn.py +85 -0
- backtrader/source/backtrader/analyzers/timereturn.py +142 -0
- backtrader/source/backtrader/analyzers/tradeanalyzer.py +208 -0
- backtrader/source/backtrader/analyzers/transactions.py +103 -0
- backtrader/source/backtrader/analyzers/vwr.py +173 -0
- backtrader/source/backtrader/broker.py +168 -0
- backtrader/source/backtrader/brokers/__init__.py +42 -0
- backtrader/source/backtrader/brokers/bbroker.py +1237 -0
- backtrader/source/backtrader/brokers/ibbroker.py +575 -0
- backtrader/source/backtrader/brokers/oandabroker.py +357 -0
- backtrader/source/backtrader/brokers/vcbroker.py +466 -0
- backtrader/source/backtrader/btrun/__init__.py +24 -0
- backtrader/source/backtrader/btrun/btrun.py +743 -0
- backtrader/source/backtrader/cerebro.py +1716 -0
- backtrader/source/backtrader/comminfo.py +328 -0
- backtrader/source/backtrader/commissions/__init__.py +64 -0
- backtrader/source/backtrader/dataseries.py +211 -0
- backtrader/source/backtrader/errors.py +51 -0
- backtrader/source/backtrader/feed.py +813 -0
- backtrader/source/backtrader/feeds/__init__.py +54 -0
Dockerfile
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
|
| 4 |
+
USER user
|
| 5 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
| 6 |
+
|
| 7 |
+
WORKDIR /app
|
| 8 |
+
|
| 9 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
| 10 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
| 11 |
+
|
| 12 |
+
COPY --chown=user . /app
|
| 13 |
+
ENV MCP_TRANSPORT=http
|
| 14 |
+
ENV MCP_PORT=7860
|
| 15 |
+
|
| 16 |
+
EXPOSE 7860
|
| 17 |
+
|
| 18 |
+
CMD ["python", "backtrader/mcp_output/start_mcp.py"]
|
README.md
CHANGED
|
@@ -1,10 +1,32 @@
|
|
| 1 |
---
|
| 2 |
-
title: Backtrader
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Backtrader MCP
|
| 3 |
+
emoji: 🤖
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: docker
|
| 7 |
+
sdk_version: "4.26.0"
|
| 8 |
+
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
---
|
| 11 |
|
| 12 |
+
# Backtrader MCP Service
|
| 13 |
+
|
| 14 |
+
Auto-generated MCP service for backtrader.
|
| 15 |
+
|
| 16 |
+
## Usage
|
| 17 |
+
|
| 18 |
+
```
|
| 19 |
+
https://None-backtrader-mcp.hf.space/mcp
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Connect with Cursor
|
| 23 |
+
|
| 24 |
+
```json
|
| 25 |
+
{
|
| 26 |
+
"mcpServers": {
|
| 27 |
+
"backtrader": {
|
| 28 |
+
"url": "https://None-backtrader-mcp.hf.space/mcp"
|
| 29 |
+
}
|
| 30 |
+
}
|
| 31 |
+
}
|
| 32 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI
|
| 2 |
+
import os
|
| 3 |
+
import sys
|
| 4 |
+
|
| 5 |
+
mcp_plugin_path = os.path.join(os.path.dirname(__file__), "backtrader", "mcp_output", "mcp_plugin")
|
| 6 |
+
sys.path.insert(0, mcp_plugin_path)
|
| 7 |
+
|
| 8 |
+
app = FastAPI(
|
| 9 |
+
title="Backtrader MCP Service",
|
| 10 |
+
description="Auto-generated MCP service for backtrader",
|
| 11 |
+
version="1.0.0"
|
| 12 |
+
)
|
| 13 |
+
|
| 14 |
+
@app.get("/")
|
| 15 |
+
def root():
|
| 16 |
+
return {
|
| 17 |
+
"service": "Backtrader MCP Service",
|
| 18 |
+
"version": "1.0.0",
|
| 19 |
+
"status": "running",
|
| 20 |
+
"transport": os.environ.get("MCP_TRANSPORT", "http")
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
@app.get("/health")
|
| 24 |
+
def health_check():
|
| 25 |
+
return {"status": "healthy", "service": "backtrader MCP"}
|
| 26 |
+
|
| 27 |
+
@app.get("/tools")
|
| 28 |
+
def list_tools():
|
| 29 |
+
try:
|
| 30 |
+
from mcp_service import create_app
|
| 31 |
+
mcp_app = create_app()
|
| 32 |
+
tools = []
|
| 33 |
+
for tool_name, tool_func in mcp_app.tools.items():
|
| 34 |
+
tools.append({
|
| 35 |
+
"name": tool_name,
|
| 36 |
+
"description": tool_func.__doc__ or "No description available"
|
| 37 |
+
})
|
| 38 |
+
return {"tools": tools}
|
| 39 |
+
except Exception as e:
|
| 40 |
+
return {"error": f"Failed to load tools: {str(e)}"}
|
| 41 |
+
|
| 42 |
+
if __name__ == "__main__":
|
| 43 |
+
import uvicorn
|
| 44 |
+
port = int(os.environ.get("PORT", 7860))
|
| 45 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
backtrader/mcp_output/README_MCP.md
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backtrader MCP (Model Context Protocol) Service
|
| 2 |
+
|
| 3 |
+
## Project Introduction
|
| 4 |
+
|
| 5 |
+
Backtrader is a comprehensive Python framework designed for developing, testing, and executing financial trading strategies. It provides a complete environment for backtesting strategies against historical data, optimizing parameters, and running strategies in live trading environments. The framework supports both backtesting on historical data and live trading through various broker integrations.
|
| 6 |
+
|
| 7 |
+
## Installation Method
|
| 8 |
+
|
| 9 |
+
To install Backtrader, ensure you have Python 3.2 or above. The basic installation can be done via pip:
|
| 10 |
+
|
| 11 |
+
- Basic installation:
|
| 12 |
+
pip install backtrader
|
| 13 |
+
|
| 14 |
+
- With plotting support:
|
| 15 |
+
pip install backtrader[plotting]
|
| 16 |
+
|
| 17 |
+
### Dependencies
|
| 18 |
+
|
| 19 |
+
- Required: numpy, pandas, matplotlib
|
| 20 |
+
- Optional: scipy, pyfolio
|
| 21 |
+
- For specific functionalities:
|
| 22 |
+
- IbPy for Interactive Brokers integration
|
| 23 |
+
- oandapy for Oanda integration
|
| 24 |
+
- pytz for timezone support
|
| 25 |
+
- pandas/blaze for additional data handling
|
| 26 |
+
|
| 27 |
+
## Quick Start
|
| 28 |
+
|
| 29 |
+
To get started with Backtrader, you can create a simple strategy and run it using the Cerebro engine. Here's a brief example of how to set up and execute a strategy:
|
| 30 |
+
|
| 31 |
+
1. Create a strategy class inheriting from `bt.Strategy`.
|
| 32 |
+
2. Define the `__init__` method to initialize indicators.
|
| 33 |
+
3. Implement the `next` method to define the trading logic.
|
| 34 |
+
4. Instantiate a `Cerebro` object, add your strategy, and run it.
|
| 35 |
+
|
| 36 |
+
Example:
|
| 37 |
+
|
| 38 |
+
```python
|
| 39 |
+
import backtrader as bt
|
| 40 |
+
|
| 41 |
+
class MyStrategy(bt.Strategy):
|
| 42 |
+
def __init__(self):
|
| 43 |
+
self.sma = bt.indicators.SimpleMovingAverage(self.data.close, period=15)
|
| 44 |
+
|
| 45 |
+
def next(self):
|
| 46 |
+
if self.data.close[0] > self.sma[0]:
|
| 47 |
+
self.buy()
|
| 48 |
+
elif self.data.close[0] < self.sma[0]:
|
| 49 |
+
self.sell()
|
| 50 |
+
|
| 51 |
+
cerebro = bt.Cerebro()
|
| 52 |
+
cerebro.addstrategy(MyStrategy)
|
| 53 |
+
cerebro.run()
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
## Available Tools and Endpoints List
|
| 57 |
+
|
| 58 |
+
- **Cerebro**: The central orchestrator that manages the entire backtesting or live trading process.
|
| 59 |
+
- **Strategy**: Contains the trading logic defined by the user.
|
| 60 |
+
- **Data Feed**: Provides market data from various sources.
|
| 61 |
+
- **Broker**: Simulates or connects to real brokers for order execution.
|
| 62 |
+
- **Indicator**: Technical analysis tools for trading signals or visualization.
|
| 63 |
+
- **Analyzer**: Evaluates strategy performance with metrics.
|
| 64 |
+
- **Observer**: Monitors and records the state of the system during execution.
|
| 65 |
+
|
| 66 |
+
## Common Issues and Notes
|
| 67 |
+
|
| 68 |
+
- Ensure all dependencies are installed, especially if using optional features like plotting or specific broker integrations.
|
| 69 |
+
- Performance can vary based on the complexity of strategies and the volume of data processed.
|
| 70 |
+
- For live trading, ensure proper configuration of broker connections and API keys.
|
| 71 |
+
|
| 72 |
+
## Reference Links or Documentation
|
| 73 |
+
|
| 74 |
+
- [Backtrader GitHub Repository](https://github.com/mementum/backtrader)
|
| 75 |
+
- [Backtrader Documentation](https://www.backtrader.com/docu/)
|
| 76 |
+
- [Backtrader Community](https://community.backtrader.com/)
|
| 77 |
+
|
| 78 |
+
For more detailed information about specific subsystems, refer to the respective documentation pages.
|
backtrader/mcp_output/analysis.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
backtrader/mcp_output/diff_report.md
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Backtrader Project Difference Report
|
| 2 |
+
|
| 3 |
+
**Date:** February 5, 2026
|
| 4 |
+
**Time:** 11:22:19
|
| 5 |
+
**Repository:** Backtrader
|
| 6 |
+
**Project Type:** Python Library
|
| 7 |
+
**Main Features:** Basic Functionality
|
| 8 |
+
**Intrusiveness:** None
|
| 9 |
+
**Workflow Status:** Success
|
| 10 |
+
**Test Status:** Failed
|
| 11 |
+
|
| 12 |
+
## Project Overview
|
| 13 |
+
|
| 14 |
+
Backtrader is a Python library designed for backtesting trading strategies. It provides a flexible and user-friendly environment for traders and developers to simulate trading strategies using historical data. The library supports various data feeds, indicators, and execution models, making it a popular choice for quantitative trading research.
|
| 15 |
+
|
| 16 |
+
## Difference Analysis
|
| 17 |
+
|
| 18 |
+
### New Files Added
|
| 19 |
+
|
| 20 |
+
In this update, 8 new files have been introduced to the repository. These files likely contain new features, enhancements, or additional documentation. However, no existing files have been modified, indicating that the new additions are supplementary rather than alterations to the core functionality.
|
| 21 |
+
|
| 22 |
+
### Modified Files
|
| 23 |
+
|
| 24 |
+
There are no modified files in this update, suggesting that the existing codebase remains unchanged. This could imply that the new files are designed to extend the library's capabilities without affecting the current functionality.
|
| 25 |
+
|
| 26 |
+
### Workflow and Test Status
|
| 27 |
+
|
| 28 |
+
- **Workflow Status:** Success
|
| 29 |
+
The workflow status indicates that the integration and deployment processes were executed successfully, with no errors encountered during the build and deployment stages.
|
| 30 |
+
|
| 31 |
+
- **Test Status:** Failed
|
| 32 |
+
Despite the successful workflow, the test status is marked as failed. This suggests that the new additions may have introduced issues or that existing tests do not cover the new functionality adequately.
|
| 33 |
+
|
| 34 |
+
## Technical Analysis
|
| 35 |
+
|
| 36 |
+
The introduction of 8 new files without any modifications to existing files suggests a modular approach to extending the library's capabilities. However, the failure in testing indicates potential issues that need to be addressed:
|
| 37 |
+
|
| 38 |
+
- **Potential Causes for Test Failures:**
|
| 39 |
+
- Insufficient test coverage for new features.
|
| 40 |
+
- Incompatibility between new files and existing code.
|
| 41 |
+
- Errors or bugs within the new files themselves.
|
| 42 |
+
|
| 43 |
+
## Recommendations and Improvements
|
| 44 |
+
|
| 45 |
+
1. **Enhance Test Coverage:**
|
| 46 |
+
- Develop comprehensive test cases for the new files to ensure they function as intended.
|
| 47 |
+
- Integrate these tests into the existing test suite to maintain overall code quality.
|
| 48 |
+
|
| 49 |
+
2. **Review New Files:**
|
| 50 |
+
- Conduct a thorough code review of the new files to identify any potential issues or areas for improvement.
|
| 51 |
+
- Ensure that the new files adhere to the project's coding standards and best practices.
|
| 52 |
+
|
| 53 |
+
3. **Debug and Resolve Test Failures:**
|
| 54 |
+
- Investigate the cause of the test failures and implement necessary fixes.
|
| 55 |
+
- Re-run the test suite to confirm that all issues have been resolved.
|
| 56 |
+
|
| 57 |
+
## Deployment Information
|
| 58 |
+
|
| 59 |
+
The successful workflow status indicates that the deployment process was completed without any issues. However, given the test failures, it is advisable to hold off on deploying the new version to production until all test issues are resolved.
|
| 60 |
+
|
| 61 |
+
## Future Planning
|
| 62 |
+
|
| 63 |
+
- **Short-term Goals:**
|
| 64 |
+
- Address the current test failures and ensure all new features are stable and reliable.
|
| 65 |
+
- Update documentation to reflect the new features and provide guidance for users.
|
| 66 |
+
|
| 67 |
+
- **Long-term Goals:**
|
| 68 |
+
- Continue to expand the library's functionality while maintaining code quality and stability.
|
| 69 |
+
- Explore opportunities for community engagement to gather feedback and contributions.
|
| 70 |
+
|
| 71 |
+
## Conclusion
|
| 72 |
+
|
| 73 |
+
The recent update to the Backtrader project introduces new features through 8 additional files. While the workflow was successful, the test failures highlight the need for further testing and debugging. By addressing these issues and enhancing test coverage, the project can continue to provide a robust platform for backtesting trading strategies.
|
| 74 |
+
|
| 75 |
+
---
|
| 76 |
+
|
| 77 |
+
This report provides a comprehensive overview of the recent changes to the Backtrader project, along with recommendations for addressing current challenges and planning for future development.
|
backtrader/mcp_output/mcp_plugin/__init__.py
ADDED
|
File without changes
|
backtrader/mcp_output/mcp_plugin/adapter.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
# Path settings
|
| 5 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
# Import statements
|
| 9 |
+
try:
|
| 10 |
+
from contrib.samples.pair_trading.pair_trading import parse_args as pair_trading_parse_args, runstrategy as pair_trading_runstrategy, PairTradingStrategy
|
| 11 |
+
from contrib.utils.influxdb_import import InfluxDBTool
|
| 12 |
+
from contrib.utils.iqfeed_to_influxdb import IQFeedTool
|
| 13 |
+
from samples.analyzer_annualreturn.analyzer_annualreturn import parse_args as annualreturn_parse_args, runstrategy as annualreturn_runstrategy, LongShortStrategy
|
| 14 |
+
from samples.bidask_to_ohlc.bidask_to_ohlc import parse_args as bidask_parse_args, runstrat as bidask_runstrat, St
|
| 15 |
+
from samples.bracket.bracket import parse_args as bracket_parse_args
|
| 16 |
+
except ImportError as e:
|
| 17 |
+
print(f"Import failed: {e}. Please ensure all modules are available in the source directory.")
|
| 18 |
+
|
| 19 |
+
# Adapter class
|
| 20 |
+
class Adapter:
|
| 21 |
+
"""
|
| 22 |
+
Adapter class to interface with various components of the backtrader plugin.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
def __init__(self):
|
| 26 |
+
self.mode = "import"
|
| 27 |
+
|
| 28 |
+
# Pair Trading Module
|
| 29 |
+
# -------------------------------------------------------------------------
|
| 30 |
+
def pair_trading_parse_args(self, *args, **kwargs):
|
| 31 |
+
"""
|
| 32 |
+
Parse arguments for pair trading strategy.
|
| 33 |
+
|
| 34 |
+
Returns:
|
| 35 |
+
dict: Status of the operation.
|
| 36 |
+
"""
|
| 37 |
+
try:
|
| 38 |
+
result = pair_trading_parse_args(*args, **kwargs)
|
| 39 |
+
return {"status": "success", "result": result}
|
| 40 |
+
except Exception as e:
|
| 41 |
+
return {"status": "error", "message": str(e)}
|
| 42 |
+
|
| 43 |
+
def pair_trading_runstrategy(self, *args, **kwargs):
|
| 44 |
+
"""
|
| 45 |
+
Run the pair trading strategy.
|
| 46 |
+
|
| 47 |
+
Returns:
|
| 48 |
+
dict: Status of the operation.
|
| 49 |
+
"""
|
| 50 |
+
try:
|
| 51 |
+
result = pair_trading_runstrategy(*args, **kwargs)
|
| 52 |
+
return {"status": "success", "result": result}
|
| 53 |
+
except Exception as e:
|
| 54 |
+
return {"status": "error", "message": str(e)}
|
| 55 |
+
|
| 56 |
+
def create_pair_trading_strategy(self):
|
| 57 |
+
"""
|
| 58 |
+
Create an instance of PairTradingStrategy.
|
| 59 |
+
|
| 60 |
+
Returns:
|
| 61 |
+
dict: Status of the operation.
|
| 62 |
+
"""
|
| 63 |
+
try:
|
| 64 |
+
strategy = PairTradingStrategy()
|
| 65 |
+
return {"status": "success", "strategy": strategy}
|
| 66 |
+
except Exception as e:
|
| 67 |
+
return {"status": "error", "message": str(e)}
|
| 68 |
+
|
| 69 |
+
# InfluxDB Tool Module
|
| 70 |
+
# -------------------------------------------------------------------------
|
| 71 |
+
def create_influxdb_tool(self):
|
| 72 |
+
"""
|
| 73 |
+
Create an instance of InfluxDBTool.
|
| 74 |
+
|
| 75 |
+
Returns:
|
| 76 |
+
dict: Status of the operation.
|
| 77 |
+
"""
|
| 78 |
+
try:
|
| 79 |
+
tool = InfluxDBTool()
|
| 80 |
+
return {"status": "success", "tool": tool}
|
| 81 |
+
except Exception as e:
|
| 82 |
+
return {"status": "error", "message": str(e)}
|
| 83 |
+
|
| 84 |
+
# IQFeed Tool Module
|
| 85 |
+
# -------------------------------------------------------------------------
|
| 86 |
+
def create_iqfeed_tool(self):
|
| 87 |
+
"""
|
| 88 |
+
Create an instance of IQFeedTool.
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
dict: Status of the operation.
|
| 92 |
+
"""
|
| 93 |
+
try:
|
| 94 |
+
tool = IQFeedTool()
|
| 95 |
+
return {"status": "success", "tool": tool}
|
| 96 |
+
except Exception as e:
|
| 97 |
+
return {"status": "error", "message": str(e)}
|
| 98 |
+
|
| 99 |
+
# Annual Return Analyzer Module
|
| 100 |
+
# -------------------------------------------------------------------------
|
| 101 |
+
def annualreturn_parse_args(self, *args, **kwargs):
|
| 102 |
+
"""
|
| 103 |
+
Parse arguments for annual return strategy.
|
| 104 |
+
|
| 105 |
+
Returns:
|
| 106 |
+
dict: Status of the operation.
|
| 107 |
+
"""
|
| 108 |
+
try:
|
| 109 |
+
result = annualreturn_parse_args(*args, **kwargs)
|
| 110 |
+
return {"status": "success", "result": result}
|
| 111 |
+
except Exception as e:
|
| 112 |
+
return {"status": "error", "message": str(e)}
|
| 113 |
+
|
| 114 |
+
def annualreturn_runstrategy(self, *args, **kwargs):
|
| 115 |
+
"""
|
| 116 |
+
Run the annual return strategy.
|
| 117 |
+
|
| 118 |
+
Returns:
|
| 119 |
+
dict: Status of the operation.
|
| 120 |
+
"""
|
| 121 |
+
try:
|
| 122 |
+
result = annualreturn_runstrategy(*args, **kwargs)
|
| 123 |
+
return {"status": "success", "result": result}
|
| 124 |
+
except Exception as e:
|
| 125 |
+
return {"status": "error", "message": str(e)}
|
| 126 |
+
|
| 127 |
+
def create_long_short_strategy(self):
|
| 128 |
+
"""
|
| 129 |
+
Create an instance of LongShortStrategy.
|
| 130 |
+
|
| 131 |
+
Returns:
|
| 132 |
+
dict: Status of the operation.
|
| 133 |
+
"""
|
| 134 |
+
try:
|
| 135 |
+
strategy = LongShortStrategy()
|
| 136 |
+
return {"status": "success", "strategy": strategy}
|
| 137 |
+
except Exception as e:
|
| 138 |
+
return {"status": "error", "message": str(e)}
|
| 139 |
+
|
| 140 |
+
# Bid-Ask to OHLC Module
|
| 141 |
+
# -------------------------------------------------------------------------
|
| 142 |
+
def bidask_parse_args(self, *args, **kwargs):
|
| 143 |
+
"""
|
| 144 |
+
Parse arguments for bid-ask to OHLC strategy.
|
| 145 |
+
|
| 146 |
+
Returns:
|
| 147 |
+
dict: Status of the operation.
|
| 148 |
+
"""
|
| 149 |
+
try:
|
| 150 |
+
result = bidask_parse_args(*args, **kwargs)
|
| 151 |
+
return {"status": "success", "result": result}
|
| 152 |
+
except Exception as e:
|
| 153 |
+
return {"status": "error", "message": str(e)}
|
| 154 |
+
|
| 155 |
+
def bidask_runstrat(self, *args, **kwargs):
|
| 156 |
+
"""
|
| 157 |
+
Run the bid-ask to OHLC strategy.
|
| 158 |
+
|
| 159 |
+
Returns:
|
| 160 |
+
dict: Status of the operation.
|
| 161 |
+
"""
|
| 162 |
+
try:
|
| 163 |
+
result = bidask_runstrat(*args, **kwargs)
|
| 164 |
+
return {"status": "success", "result": result}
|
| 165 |
+
except Exception as e:
|
| 166 |
+
return {"status": "error", "message": str(e)}
|
| 167 |
+
|
| 168 |
+
def create_st(self):
|
| 169 |
+
"""
|
| 170 |
+
Create an instance of St.
|
| 171 |
+
|
| 172 |
+
Returns:
|
| 173 |
+
dict: Status of the operation.
|
| 174 |
+
"""
|
| 175 |
+
try:
|
| 176 |
+
st_instance = St()
|
| 177 |
+
return {"status": "success", "st_instance": st_instance}
|
| 178 |
+
except Exception as e:
|
| 179 |
+
return {"status": "error", "message": str(e)}
|
| 180 |
+
|
| 181 |
+
# Bracket Module
|
| 182 |
+
# -------------------------------------------------------------------------
|
| 183 |
+
def bracket_parse_args(self, *args, **kwargs):
|
| 184 |
+
"""
|
| 185 |
+
Parse arguments for bracket strategy.
|
| 186 |
+
|
| 187 |
+
Returns:
|
| 188 |
+
dict: Status of the operation.
|
| 189 |
+
"""
|
| 190 |
+
try:
|
| 191 |
+
result = bracket_parse_args(*args, **kwargs)
|
| 192 |
+
return {"status": "success", "result": result}
|
| 193 |
+
except Exception as e:
|
| 194 |
+
return {"status": "error", "message": str(e)}
|
backtrader/mcp_output/mcp_plugin/main.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
MCP Service Auto-Wrapper - Auto-generated
|
| 3 |
+
"""
|
| 4 |
+
from mcp_service import create_app
|
| 5 |
+
|
| 6 |
+
def main():
|
| 7 |
+
"""Main entry point"""
|
| 8 |
+
app = create_app()
|
| 9 |
+
return app
|
| 10 |
+
|
| 11 |
+
if __name__ == "__main__":
|
| 12 |
+
app = main()
|
| 13 |
+
app.run()
|
backtrader/mcp_output/mcp_plugin/mcp_service.py
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
|
| 5 |
+
if source_path not in sys.path:
|
| 6 |
+
sys.path.insert(0, source_path)
|
| 7 |
+
|
| 8 |
+
from fastmcp import FastMCP
|
| 9 |
+
|
| 10 |
+
from contrib.samples.pair-trading import parse_args, runstrategy, PairTradingStrategy
|
| 11 |
+
from contrib.utils.influxdb-import import InfluxDBTool
|
| 12 |
+
from contrib.utils.iqfeed-to-influxdb import IQFeedTool
|
| 13 |
+
from samples.analyzer-annualreturn import parse_args, LongShortStrategy, runstrategy
|
| 14 |
+
from samples.bidask-to-ohlc import runstrat, parse_args, St
|
| 15 |
+
from samples.bracket import parse_args
|
| 16 |
+
|
| 17 |
+
mcp = FastMCP("unknown_service")
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
|
| 21 |
+
def parse_args(payload: dict):
|
| 22 |
+
try:
|
| 23 |
+
if parse_args is None:
|
| 24 |
+
return {"success": False, "result": None, "error": "Function parse_args is not available"}
|
| 25 |
+
result = parse_args(**payload)
|
| 26 |
+
return {"success": True, "result": result, "error": None}
|
| 27 |
+
except Exception as e:
|
| 28 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 29 |
+
|
| 30 |
+
@mcp.tool(name="runstrategy", description="Auto-wrapped function runstrategy")
|
| 31 |
+
def runstrategy(payload: dict):
|
| 32 |
+
try:
|
| 33 |
+
if runstrategy is None:
|
| 34 |
+
return {"success": False, "result": None, "error": "Function runstrategy is not available"}
|
| 35 |
+
result = runstrategy(**payload)
|
| 36 |
+
return {"success": True, "result": result, "error": None}
|
| 37 |
+
except Exception as e:
|
| 38 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 39 |
+
|
| 40 |
+
@mcp.tool(name="pairtradingstrategy", description="PairTradingStrategy class")
|
| 41 |
+
def pairtradingstrategy(*args, **kwargs):
|
| 42 |
+
"""PairTradingStrategy class"""
|
| 43 |
+
try:
|
| 44 |
+
if PairTradingStrategy is None:
|
| 45 |
+
return {"success": False, "result": None, "error": "Class PairTradingStrategy is not available, path may need adjustment"}
|
| 46 |
+
|
| 47 |
+
# MCP parameter type conversion
|
| 48 |
+
converted_args = []
|
| 49 |
+
converted_kwargs = kwargs.copy()
|
| 50 |
+
|
| 51 |
+
# Handle position argument type conversion
|
| 52 |
+
for arg in args:
|
| 53 |
+
if isinstance(arg, str):
|
| 54 |
+
# Try to convert to numeric type
|
| 55 |
+
try:
|
| 56 |
+
if '.' in arg:
|
| 57 |
+
converted_args.append(float(arg))
|
| 58 |
+
else:
|
| 59 |
+
converted_args.append(int(arg))
|
| 60 |
+
except ValueError:
|
| 61 |
+
converted_args.append(arg)
|
| 62 |
+
else:
|
| 63 |
+
converted_args.append(arg)
|
| 64 |
+
|
| 65 |
+
# Handle keyword argument type conversion
|
| 66 |
+
for key, value in converted_kwargs.items():
|
| 67 |
+
if isinstance(value, str):
|
| 68 |
+
try:
|
| 69 |
+
if '.' in value:
|
| 70 |
+
converted_kwargs[key] = float(value)
|
| 71 |
+
else:
|
| 72 |
+
converted_kwargs[key] = int(value)
|
| 73 |
+
except ValueError:
|
| 74 |
+
pass
|
| 75 |
+
|
| 76 |
+
instance = PairTradingStrategy(*converted_args, **converted_kwargs)
|
| 77 |
+
return {"success": True, "result": str(instance), "error": None}
|
| 78 |
+
except Exception as e:
|
| 79 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 80 |
+
|
| 81 |
+
@mcp.tool(name="influxdbtool", description="InfluxDBTool class")
|
| 82 |
+
def influxdbtool(*args, **kwargs):
|
| 83 |
+
"""InfluxDBTool class"""
|
| 84 |
+
try:
|
| 85 |
+
if InfluxDBTool is None:
|
| 86 |
+
return {"success": False, "result": None, "error": "Class InfluxDBTool is not available, path may need adjustment"}
|
| 87 |
+
|
| 88 |
+
# MCP parameter type conversion
|
| 89 |
+
converted_args = []
|
| 90 |
+
converted_kwargs = kwargs.copy()
|
| 91 |
+
|
| 92 |
+
# Handle position argument type conversion
|
| 93 |
+
for arg in args:
|
| 94 |
+
if isinstance(arg, str):
|
| 95 |
+
# Try to convert to numeric type
|
| 96 |
+
try:
|
| 97 |
+
if '.' in arg:
|
| 98 |
+
converted_args.append(float(arg))
|
| 99 |
+
else:
|
| 100 |
+
converted_args.append(int(arg))
|
| 101 |
+
except ValueError:
|
| 102 |
+
converted_args.append(arg)
|
| 103 |
+
else:
|
| 104 |
+
converted_args.append(arg)
|
| 105 |
+
|
| 106 |
+
# Handle keyword argument type conversion
|
| 107 |
+
for key, value in converted_kwargs.items():
|
| 108 |
+
if isinstance(value, str):
|
| 109 |
+
try:
|
| 110 |
+
if '.' in value:
|
| 111 |
+
converted_kwargs[key] = float(value)
|
| 112 |
+
else:
|
| 113 |
+
converted_kwargs[key] = int(value)
|
| 114 |
+
except ValueError:
|
| 115 |
+
pass
|
| 116 |
+
|
| 117 |
+
instance = InfluxDBTool(*converted_args, **converted_kwargs)
|
| 118 |
+
return {"success": True, "result": str(instance), "error": None}
|
| 119 |
+
except Exception as e:
|
| 120 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 121 |
+
|
| 122 |
+
@mcp.tool(name="iqfeedtool", description="IQFeedTool class")
|
| 123 |
+
def iqfeedtool(*args, **kwargs):
|
| 124 |
+
"""IQFeedTool class"""
|
| 125 |
+
try:
|
| 126 |
+
if IQFeedTool is None:
|
| 127 |
+
return {"success": False, "result": None, "error": "Class IQFeedTool is not available, path may need adjustment"}
|
| 128 |
+
|
| 129 |
+
# MCP parameter type conversion
|
| 130 |
+
converted_args = []
|
| 131 |
+
converted_kwargs = kwargs.copy()
|
| 132 |
+
|
| 133 |
+
# Handle position argument type conversion
|
| 134 |
+
for arg in args:
|
| 135 |
+
if isinstance(arg, str):
|
| 136 |
+
# Try to convert to numeric type
|
| 137 |
+
try:
|
| 138 |
+
if '.' in arg:
|
| 139 |
+
converted_args.append(float(arg))
|
| 140 |
+
else:
|
| 141 |
+
converted_args.append(int(arg))
|
| 142 |
+
except ValueError:
|
| 143 |
+
converted_args.append(arg)
|
| 144 |
+
else:
|
| 145 |
+
converted_args.append(arg)
|
| 146 |
+
|
| 147 |
+
# Handle keyword argument type conversion
|
| 148 |
+
for key, value in converted_kwargs.items():
|
| 149 |
+
if isinstance(value, str):
|
| 150 |
+
try:
|
| 151 |
+
if '.' in value:
|
| 152 |
+
converted_kwargs[key] = float(value)
|
| 153 |
+
else:
|
| 154 |
+
converted_kwargs[key] = int(value)
|
| 155 |
+
except ValueError:
|
| 156 |
+
pass
|
| 157 |
+
|
| 158 |
+
instance = IQFeedTool(*converted_args, **converted_kwargs)
|
| 159 |
+
return {"success": True, "result": str(instance), "error": None}
|
| 160 |
+
except Exception as e:
|
| 161 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 162 |
+
|
| 163 |
+
@mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
|
| 164 |
+
def parse_args(payload: dict):
|
| 165 |
+
try:
|
| 166 |
+
if parse_args is None:
|
| 167 |
+
return {"success": False, "result": None, "error": "Function parse_args is not available"}
|
| 168 |
+
result = parse_args(**payload)
|
| 169 |
+
return {"success": True, "result": result, "error": None}
|
| 170 |
+
except Exception as e:
|
| 171 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 172 |
+
|
| 173 |
+
@mcp.tool(name="runstrategy", description="Auto-wrapped function runstrategy")
|
| 174 |
+
def runstrategy(payload: dict):
|
| 175 |
+
try:
|
| 176 |
+
if runstrategy is None:
|
| 177 |
+
return {"success": False, "result": None, "error": "Function runstrategy is not available"}
|
| 178 |
+
result = runstrategy(**payload)
|
| 179 |
+
return {"success": True, "result": result, "error": None}
|
| 180 |
+
except Exception as e:
|
| 181 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 182 |
+
|
| 183 |
+
@mcp.tool(name="longshortstrategy", description="LongShortStrategy class")
|
| 184 |
+
def longshortstrategy(*args, **kwargs):
|
| 185 |
+
"""LongShortStrategy class"""
|
| 186 |
+
try:
|
| 187 |
+
if LongShortStrategy is None:
|
| 188 |
+
return {"success": False, "result": None, "error": "Class LongShortStrategy is not available, path may need adjustment"}
|
| 189 |
+
|
| 190 |
+
# MCP parameter type conversion
|
| 191 |
+
converted_args = []
|
| 192 |
+
converted_kwargs = kwargs.copy()
|
| 193 |
+
|
| 194 |
+
# Handle position argument type conversion
|
| 195 |
+
for arg in args:
|
| 196 |
+
if isinstance(arg, str):
|
| 197 |
+
# Try to convert to numeric type
|
| 198 |
+
try:
|
| 199 |
+
if '.' in arg:
|
| 200 |
+
converted_args.append(float(arg))
|
| 201 |
+
else:
|
| 202 |
+
converted_args.append(int(arg))
|
| 203 |
+
except ValueError:
|
| 204 |
+
converted_args.append(arg)
|
| 205 |
+
else:
|
| 206 |
+
converted_args.append(arg)
|
| 207 |
+
|
| 208 |
+
# Handle keyword argument type conversion
|
| 209 |
+
for key, value in converted_kwargs.items():
|
| 210 |
+
if isinstance(value, str):
|
| 211 |
+
try:
|
| 212 |
+
if '.' in value:
|
| 213 |
+
converted_kwargs[key] = float(value)
|
| 214 |
+
else:
|
| 215 |
+
converted_kwargs[key] = int(value)
|
| 216 |
+
except ValueError:
|
| 217 |
+
pass
|
| 218 |
+
|
| 219 |
+
instance = LongShortStrategy(*converted_args, **converted_kwargs)
|
| 220 |
+
return {"success": True, "result": str(instance), "error": None}
|
| 221 |
+
except Exception as e:
|
| 222 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 223 |
+
|
| 224 |
+
@mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
|
| 225 |
+
def parse_args(payload: dict):
|
| 226 |
+
try:
|
| 227 |
+
if parse_args is None:
|
| 228 |
+
return {"success": False, "result": None, "error": "Function parse_args is not available"}
|
| 229 |
+
result = parse_args(**payload)
|
| 230 |
+
return {"success": True, "result": result, "error": None}
|
| 231 |
+
except Exception as e:
|
| 232 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 233 |
+
|
| 234 |
+
@mcp.tool(name="runstrat", description="Auto-wrapped function runstrat")
|
| 235 |
+
def runstrat(payload: dict):
|
| 236 |
+
try:
|
| 237 |
+
if runstrat is None:
|
| 238 |
+
return {"success": False, "result": None, "error": "Function runstrat is not available"}
|
| 239 |
+
result = runstrat(**payload)
|
| 240 |
+
return {"success": True, "result": result, "error": None}
|
| 241 |
+
except Exception as e:
|
| 242 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 243 |
+
|
| 244 |
+
@mcp.tool(name="st", description="St class")
|
| 245 |
+
def st(*args, **kwargs):
|
| 246 |
+
"""St class"""
|
| 247 |
+
try:
|
| 248 |
+
if St is None:
|
| 249 |
+
return {"success": False, "result": None, "error": "Class St is not available, path may need adjustment"}
|
| 250 |
+
|
| 251 |
+
# MCP parameter type conversion
|
| 252 |
+
converted_args = []
|
| 253 |
+
converted_kwargs = kwargs.copy()
|
| 254 |
+
|
| 255 |
+
# Handle position argument type conversion
|
| 256 |
+
for arg in args:
|
| 257 |
+
if isinstance(arg, str):
|
| 258 |
+
# Try to convert to numeric type
|
| 259 |
+
try:
|
| 260 |
+
if '.' in arg:
|
| 261 |
+
converted_args.append(float(arg))
|
| 262 |
+
else:
|
| 263 |
+
converted_args.append(int(arg))
|
| 264 |
+
except ValueError:
|
| 265 |
+
converted_args.append(arg)
|
| 266 |
+
else:
|
| 267 |
+
converted_args.append(arg)
|
| 268 |
+
|
| 269 |
+
# Handle keyword argument type conversion
|
| 270 |
+
for key, value in converted_kwargs.items():
|
| 271 |
+
if isinstance(value, str):
|
| 272 |
+
try:
|
| 273 |
+
if '.' in value:
|
| 274 |
+
converted_kwargs[key] = float(value)
|
| 275 |
+
else:
|
| 276 |
+
converted_kwargs[key] = int(value)
|
| 277 |
+
except ValueError:
|
| 278 |
+
pass
|
| 279 |
+
|
| 280 |
+
instance = St(*converted_args, **converted_kwargs)
|
| 281 |
+
return {"success": True, "result": str(instance), "error": None}
|
| 282 |
+
except Exception as e:
|
| 283 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 284 |
+
|
| 285 |
+
@mcp.tool(name="parse_args", description="Auto-wrapped function parse_args")
|
| 286 |
+
def parse_args(payload: dict):
|
| 287 |
+
try:
|
| 288 |
+
if parse_args is None:
|
| 289 |
+
return {"success": False, "result": None, "error": "Function parse_args is not available"}
|
| 290 |
+
result = parse_args(**payload)
|
| 291 |
+
return {"success": True, "result": result, "error": None}
|
| 292 |
+
except Exception as e:
|
| 293 |
+
return {"success": False, "result": None, "error": str(e)}
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
|
| 297 |
+
def create_app():
|
| 298 |
+
"""Create and return FastMCP application instance"""
|
| 299 |
+
return mcp
|
| 300 |
+
|
| 301 |
+
if __name__ == "__main__":
|
| 302 |
+
mcp.run(transport="http", host="0.0.0.0", port=8000)
|
backtrader/mcp_output/requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastmcp
|
| 2 |
+
fastapi
|
| 3 |
+
uvicorn[standard]
|
| 4 |
+
pydantic>=2.0.0
|
| 5 |
+
six
|
| 6 |
+
numpy
|
| 7 |
+
pandas
|
| 8 |
+
matplotlib
|
backtrader/mcp_output/start_mcp.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
"""
|
| 3 |
+
MCP Service Startup Entry
|
| 4 |
+
"""
|
| 5 |
+
import sys
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
project_root = os.path.dirname(os.path.abspath(__file__))
|
| 9 |
+
mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
|
| 10 |
+
if mcp_plugin_dir not in sys.path:
|
| 11 |
+
sys.path.insert(0, mcp_plugin_dir)
|
| 12 |
+
|
| 13 |
+
from mcp_service import create_app
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
"""Start FastMCP service"""
|
| 17 |
+
app = create_app()
|
| 18 |
+
# Use environment variable to configure port, default 8000
|
| 19 |
+
port = int(os.environ.get("MCP_PORT", "8000"))
|
| 20 |
+
|
| 21 |
+
# Choose transport mode based on environment variable
|
| 22 |
+
transport = os.environ.get("MCP_TRANSPORT", "stdio")
|
| 23 |
+
if transport == "http":
|
| 24 |
+
app.run(transport="http", host="0.0.0.0", port=port)
|
| 25 |
+
else:
|
| 26 |
+
# Default to STDIO mode
|
| 27 |
+
app.run()
|
| 28 |
+
|
| 29 |
+
if __name__ == "__main__":
|
| 30 |
+
main()
|
backtrader/mcp_output/workflow_summary.json
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"repository": {
|
| 3 |
+
"name": "backtrader",
|
| 4 |
+
"url": "https://github.com/mementum/backtrader",
|
| 5 |
+
"local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/backtrader",
|
| 6 |
+
"description": "Python library",
|
| 7 |
+
"features": "Basic functionality",
|
| 8 |
+
"tech_stack": "Python",
|
| 9 |
+
"stars": 0,
|
| 10 |
+
"forks": 0,
|
| 11 |
+
"language": "Python",
|
| 12 |
+
"last_updated": "",
|
| 13 |
+
"complexity": "medium",
|
| 14 |
+
"intrusiveness_risk": "low"
|
| 15 |
+
},
|
| 16 |
+
"execution": {
|
| 17 |
+
"start_time": 1770261550.299908,
|
| 18 |
+
"end_time": 1770261656.5187547,
|
| 19 |
+
"duration": 106.21884679794312,
|
| 20 |
+
"status": "success",
|
| 21 |
+
"workflow_status": "success",
|
| 22 |
+
"nodes_executed": [
|
| 23 |
+
"download",
|
| 24 |
+
"analysis",
|
| 25 |
+
"env",
|
| 26 |
+
"generate",
|
| 27 |
+
"run",
|
| 28 |
+
"review",
|
| 29 |
+
"finalize"
|
| 30 |
+
],
|
| 31 |
+
"total_files_processed": 16,
|
| 32 |
+
"environment_type": "unknown",
|
| 33 |
+
"llm_calls": 0,
|
| 34 |
+
"deepwiki_calls": 0
|
| 35 |
+
},
|
| 36 |
+
"tests": {
|
| 37 |
+
"original_project": {
|
| 38 |
+
"passed": false,
|
| 39 |
+
"details": {},
|
| 40 |
+
"test_coverage": "100%",
|
| 41 |
+
"execution_time": 0,
|
| 42 |
+
"test_files": []
|
| 43 |
+
},
|
| 44 |
+
"mcp_plugin": {
|
| 45 |
+
"passed": true,
|
| 46 |
+
"details": {},
|
| 47 |
+
"service_health": "healthy",
|
| 48 |
+
"startup_time": 0,
|
| 49 |
+
"transport_mode": "stdio",
|
| 50 |
+
"fastmcp_version": "unknown",
|
| 51 |
+
"mcp_version": "unknown"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"analysis": {
|
| 55 |
+
"structure": {
|
| 56 |
+
"packages": [
|
| 57 |
+
"source.backtrader",
|
| 58 |
+
"source.backtrader.analyzers",
|
| 59 |
+
"source.backtrader.brokers",
|
| 60 |
+
"source.backtrader.btrun",
|
| 61 |
+
"source.backtrader.commissions",
|
| 62 |
+
"source.backtrader.feeds",
|
| 63 |
+
"source.backtrader.filters",
|
| 64 |
+
"source.backtrader.indicators",
|
| 65 |
+
"source.backtrader.observers",
|
| 66 |
+
"source.backtrader.plot",
|
| 67 |
+
"source.backtrader.signals",
|
| 68 |
+
"source.backtrader.sizers",
|
| 69 |
+
"source.backtrader.stores",
|
| 70 |
+
"source.backtrader.strategies",
|
| 71 |
+
"source.backtrader.studies",
|
| 72 |
+
"source.backtrader.utils"
|
| 73 |
+
]
|
| 74 |
+
},
|
| 75 |
+
"dependencies": {
|
| 76 |
+
"has_environment_yml": false,
|
| 77 |
+
"has_requirements_txt": false,
|
| 78 |
+
"pyproject": false,
|
| 79 |
+
"setup_cfg": false,
|
| 80 |
+
"setup_py": true
|
| 81 |
+
},
|
| 82 |
+
"entry_points": {
|
| 83 |
+
"imports": [],
|
| 84 |
+
"cli": [],
|
| 85 |
+
"modules": []
|
| 86 |
+
},
|
| 87 |
+
"risk_assessment": {
|
| 88 |
+
"import_feasibility": 0.9,
|
| 89 |
+
"intrusiveness_risk": "low",
|
| 90 |
+
"complexity": "medium"
|
| 91 |
+
},
|
| 92 |
+
"deepwiki_analysis": {
|
| 93 |
+
"repo_url": "https://github.com/mementum/backtrader",
|
| 94 |
+
"repo_name": "backtrader",
|
| 95 |
+
"content": "mementum/backtrader\nCore Architecture\nOrders and Trades\nData Handling System\nLine Series and Buffers\nResampling and Replaying\nDate and Time Handling\nTechnical Indicators\nBasic Operations and Common Indicators\nMoving Averages and Oscillators\nCreating Custom Indicators\nPerformance Analysis\nVisualization\nWriters and Output\nLive Trading\nInteractive Brokers Integration\nOanda Integration\nVolume Filling and Slippage\nCommand Line Tools\nTesting and Development\n.travis.yml\nbacktrader/__init__.py\nbacktrader/broker.py\nbacktrader/cerebro.py\nbacktrader/order.py\nbacktrader/plot/__init__.py\nbacktrader/signal.py\nbacktrader/strategy.py\nbacktrader/trade.py\nbacktrader/version.py\nchangelog.txt\nsamples/stop-trading/stop-loss-approaches.py\ntests/test_order.py\nBacktrader is a feature-rich Python framework for developing, testing, and executing financial trading strategies. It provides a complete environment for backtesting strategies against historical data, optimizing parameters, and running strategies in live trading environments.\nThis wiki covers the key components, architecture, and features of the Backtrader framework. For detailed information about specific subsystems, refer to the respective wiki pages likeCore Architecture,Data Handling System, orTechnical Indicators.\nFramework Purpose\nBacktrader allows traders and developers to:\nDevelop and test trading strategies against historical market data\nPerform strategy optimization through parameter variations\nExecute strategies in real-time with live market data\nAnalyze trading performance with built-in metrics\nVisualize results with customizable plots\nThe framework supports both backtesting on historical data and live trading through various broker integrations.\nSources:README.rst69-101backtrader/version.py\nCore Components\n\"runs\"\"provides data to\"\"manages\"\"evaluates\"\"monitors\"\"uses\"\"places orders through\"\"stores data in\"\"stores results in\"\"stores observations in\"Cerebro+run()+plot()+addstrategy()+adddata()+addanalyzer()+addobserver()+setbroker()Strategy+next()+buy()+sell()+notify_order()+notify_trade()DataFeed+start()+_load()+next()Broker+buy()+sell()+getcash()+getvalue()+getposition()Analyzer+start()+stop()+get_analysis()Observer+lines+next()Indicator+lines+next()LineBuffer+getitem()+setitem()+forward()+backwards()\n\"provides data to\"\n\"evaluates\"\n\"places orders through\"\n\"stores data in\"\n\"stores results in\"\n\"stores observations in\"\n+addstrategy()\n+addanalyzer()\n+addobserver()\n+setbroker()\n+notify_order()\n+notify_trade()\n+getvalue()\n+getposition()\n+get_analysis()\n+backwards()\nThe main components of Backtrader include:\nCerebro: The central orchestrator that manages the entire backtesting or live trading process. It connects all other components and controls execution flow.\nCerebro: The central orchestrator that manages the entire backtesting or live trading process. It connects all other components and controls execution flow.\nStrategy: Contains the trading logic defined by the user. Strategies analyze data and indicators to determine when to enter or exit positions.\nStrategy: Contains the trading logic defined by the user. Strategies analyze data and indicators to determine when to enter or exit positions.\nData Feed: Provides market data (prices, volume, etc.) from various sources like CSV files, databases, or live feeds from brokers.\nData Feed: Provides market data (prices, volume, etc.) from various sources like CSV files, databases, or live feeds from brokers.\nBroker: Simulates or connects to real brokers for order execution. Handles order management, position tracking, and cash calculations.\nBroker: Simulates or connects to real brokers for order execution. Handles order management, position tracking, and cash calculations.\nIndicator: Technical analysis tools that transform raw data into trading signals or visualization aids.\nIndicator: Technical analysis tools that transform raw data into trading signals or visualization aids.\nAnalyzer: Evaluates strategy performance with metrics like Sharpe ratio, returns, drawdowns, etc.\nAnalyzer: Evaluates strategy performance with metrics like Sharpe ratio, returns, drawdowns, etc.\nObserver: Monitors and records the state of the system during execution (e.g., cash levels, equity value, trades).\nObserver: Monitors and records the state of the system during execution (e.g., cash levels, equity value, trades).\nLine Buffer: Core data structure that handles time-series data storage and access throughout the system.\nLine Buffer: Core data structure that handles time-series data storage and access throughout the system.\nSources:backtrader/cerebro.py60-294backtrader/strategy.py107-168backtrader/broker.py49-167\nData Flow Architecture\nAnalysis & OutputCore EngineData ProcessingData SourcesCSV FilesYahoo FinanceOanda APIInteractive BrokersOther SourcesData FeedFiltersResamplerReplayerCerebroDataSeriesStrategyBrokerIndicatorsObserversAnalyzersPlotWriter\nAnalysis & Output\nCore Engine\nData Processing\nData Sources\nYahoo Finance\nInteractive Brokers\nOther Sources\nThe diagram above illustrates how data flows through the Backtrader system:\nData sourcesprovide market information through various data feeds\nData processingcomponents transform and prepare the data (filtering, resampling)\nThecore engine(Cerebro) coordinates the interaction between data, strategy, and broker\nAnalysis and outputcomponents evaluate performance and generate visualizations\nSources:backtrader/__init__.py30-91backtrader/cerebro.py752-775\nExecution Flow\nAnalyzersBrokerStrategyDataFeedCerebroUserAnalyzersBrokerStrategyDataFeedCerebroUserloop[For each bar]createadddata(DataFeed)addstrategy(Strategy)addanalyzer(Analyzer)run()preload()data loadedstart()next()new barnext()buy()/sell()notify_order()notify_trade()stop()get_analysis()resultsreturn resultsplot()display charts\nThe sequence diagram above shows how a typical backtest executes:\nThe user creates a Cerebro instance and adds components (data, strategy, analyzers)\nCerebro loads the data and initializes the strategy\nFor each bar of data, Cerebro calls the strategy'snext()method\nThe strategy analyzes data and may place orders through the broker\nThe broker executes orders and notifies the strategy\nAfter all data is processed, Cerebro collects results from analyzers\nThe user can then plot the results or process them further\nSources:backtrader/cerebro.py1574-1662backtrader/strategy.py346-353\nOrder Types and Handling\nBacktrader supports a variety of order types for both backtesting and live trading:\nThe broker component handles order execution according to the order type and current market conditions.\nSources:backtrader/order.py242-245samples/stop-trading/stop-loss-approaches.py\nSystem Features\nKey features that make Backtrader a powerful trading platform:\nMultiple Data Feeds: Support for various data sources and the ability to use multiple data feeds simultaneously.\nMultiple Data Feeds: Support for various data sources and the ability to use multiple data feeds simultaneously.\nLine-based Data Management: Efficient handling of time series data through a specialized line-based system.\nLine-based Data Management: Efficient handling of time series data through a specialized line-based system.\nRich Indicator Library: Over 100 built-in technical indicators with support for custom indicators.\nRich Indicator Library: Over 100 built-in technical indicators with support for custom indicators.\nStrategy Development: Flexible framework for creating and testing trading strategies.\nStrategy Development: Flexible framework for creating and testing trading strategies.\nPerformance Analysis: Comprehensive tools for analyzing trading performance.\nPerformance Analysis: Comprehensive tools for analyzing trading performance.\nVisualization: Built-in plotting capabilities for strategies, indicators, and results.\nVisualization: Built-in plotting capabilities for strategies, indicators, and results.\nOptimization: Parameter optimization through parallel processing.\nOptimization: Parameter optimization through parallel processing.\nLive Trading: Integration with brokers for real-time trading.\nLive Trading: Integration with brokers for real-time trading.\nCustomization: Extensible architecture that allows for customization at various levels.\nCustomization: Extensible architecture that allows for customization at various levels.\nSources:README.rst66-101changelog.txt1-50\nSystem Requirements and Installation\nBacktrader supports:\nPython 3.2 and above\nOptional dependency on matplotlib (version 1.4.1 or higher) for plotting capabilities\nAdditional dependencies for specific functionality:IbPy for Interactive Brokers integrationoandapy for Oanda integrationpytz for timezone supportpandas/blaze for additional data handling\nIbPy for Interactive Brokers integration\noandapy for Oanda integration\npytz for timezone support\npandas/blaze for additional data handling\nInstallation:\npip install backtrader # Basic installation\npip install backtrader[plotting] # With plotting support\npip install backtrader # Basic installation\npip install backtrader[plotting] # With plotting support\nSources:README.rst118-153setup.py43-137\nVersion Information\nBacktrader uses a version numbering scheme: X.Y.Z.I\nX: Major version number (changes for significant alterations)\nY: Minor version number (new features or API changes)\nZ: Revision number (documentation updates, small changes, bug fixes)\nI: Number of indicators built into the platform\nThe current version is defined in the version.py file.\nSources:README.rst159-170backtrader/version.py25-27\nRefresh this wiki\nOn this page\nFramework Purpose\nCore Components\nData Flow Architecture\nExecution Flow\nOrder Types and Handling\nSystem Features\nSystem Requirements and Installation\nVersion Information",
|
| 96 |
+
"model": "gpt-4o-2024-08-06",
|
| 97 |
+
"source": "selenium",
|
| 98 |
+
"success": true
|
| 99 |
+
},
|
| 100 |
+
"code_complexity": {
|
| 101 |
+
"cyclomatic_complexity": "medium",
|
| 102 |
+
"cognitive_complexity": "medium",
|
| 103 |
+
"maintainability_index": 75
|
| 104 |
+
},
|
| 105 |
+
"security_analysis": {
|
| 106 |
+
"vulnerabilities_found": 0,
|
| 107 |
+
"security_score": 85,
|
| 108 |
+
"recommendations": []
|
| 109 |
+
}
|
| 110 |
+
},
|
| 111 |
+
"plugin_generation": {
|
| 112 |
+
"files_created": [
|
| 113 |
+
"mcp_output/start_mcp.py",
|
| 114 |
+
"mcp_output/mcp_plugin/__init__.py",
|
| 115 |
+
"mcp_output/mcp_plugin/mcp_service.py",
|
| 116 |
+
"mcp_output/mcp_plugin/adapter.py",
|
| 117 |
+
"mcp_output/mcp_plugin/main.py",
|
| 118 |
+
"mcp_output/requirements.txt",
|
| 119 |
+
"mcp_output/README_MCP.md"
|
| 120 |
+
],
|
| 121 |
+
"main_entry": "start_mcp.py",
|
| 122 |
+
"requirements": [
|
| 123 |
+
"fastmcp>=0.1.0",
|
| 124 |
+
"pydantic>=2.0.0"
|
| 125 |
+
],
|
| 126 |
+
"readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/backtrader/mcp_output/README_MCP.md",
|
| 127 |
+
"adapter_mode": "import",
|
| 128 |
+
"total_lines_of_code": 0,
|
| 129 |
+
"generated_files_size": 0,
|
| 130 |
+
"tool_endpoints": 0,
|
| 131 |
+
"supported_features": [
|
| 132 |
+
"Basic functionality"
|
| 133 |
+
],
|
| 134 |
+
"generated_tools": [
|
| 135 |
+
"Basic tools",
|
| 136 |
+
"Health check tools",
|
| 137 |
+
"Version info tools"
|
| 138 |
+
]
|
| 139 |
+
},
|
| 140 |
+
"code_review": {},
|
| 141 |
+
"errors": [],
|
| 142 |
+
"warnings": [],
|
| 143 |
+
"recommendations": [
|
| 144 |
+
"Improve test coverage by adding more unit tests",
|
| 145 |
+
"Implement continuous integration using GitHub Actions or Travis CI",
|
| 146 |
+
"Add a requirements.txt file to manage dependencies",
|
| 147 |
+
"Consider using a setup.cfg for configuration to simplify setup.py",
|
| 148 |
+
"Improve documentation for better understanding of the codebase",
|
| 149 |
+
"Optimize large files for better performance",
|
| 150 |
+
"Refactor code to improve readability and maintainability",
|
| 151 |
+
"Implement code linting tools like flake8 or pylint",
|
| 152 |
+
"Enhance error handling and logging mechanisms",
|
| 153 |
+
"Update dependencies to their latest versions to ensure security and compatibility",
|
| 154 |
+
"Consider adding type annotations for better code clarity and error checking",
|
| 155 |
+
"Improve the modularity of the code by breaking down large modules into smaller",
|
| 156 |
+
"more manageable components",
|
| 157 |
+
"Conduct a security audit to identify and fix potential vulnerabilities",
|
| 158 |
+
"Enhance the project's README with more detailed setup and usage instructions",
|
| 159 |
+
"Consider adding a CONTRIBUTING.md file to guide new contributors."
|
| 160 |
+
],
|
| 161 |
+
"performance_metrics": {
|
| 162 |
+
"memory_usage_mb": 0,
|
| 163 |
+
"cpu_usage_percent": 0,
|
| 164 |
+
"response_time_ms": 0,
|
| 165 |
+
"throughput_requests_per_second": 0
|
| 166 |
+
},
|
| 167 |
+
"deployment_info": {
|
| 168 |
+
"supported_platforms": [
|
| 169 |
+
"Linux",
|
| 170 |
+
"Windows",
|
| 171 |
+
"macOS"
|
| 172 |
+
],
|
| 173 |
+
"python_versions": [
|
| 174 |
+
"3.8",
|
| 175 |
+
"3.9",
|
| 176 |
+
"3.10",
|
| 177 |
+
"3.11",
|
| 178 |
+
"3.12"
|
| 179 |
+
],
|
| 180 |
+
"deployment_methods": [
|
| 181 |
+
"Docker",
|
| 182 |
+
"pip",
|
| 183 |
+
"conda"
|
| 184 |
+
],
|
| 185 |
+
"monitoring_support": true,
|
| 186 |
+
"logging_configuration": "structured"
|
| 187 |
+
},
|
| 188 |
+
"execution_analysis": {
|
| 189 |
+
"success_factors": [
|
| 190 |
+
"Comprehensive analysis of the repository structure and dependencies",
|
| 191 |
+
"Successful generation of MCP service files with no errors or warnings"
|
| 192 |
+
],
|
| 193 |
+
"failure_reasons": [],
|
| 194 |
+
"overall_assessment": "good",
|
| 195 |
+
"node_performance": {
|
| 196 |
+
"download_time": "Efficient download and setup of the repository",
|
| 197 |
+
"analysis_time": "Detailed analysis completed in a reasonable timeframe",
|
| 198 |
+
"generation_time": "Code generation was successful and timely",
|
| 199 |
+
"test_time": "Testing was limited; original project tests did not pass"
|
| 200 |
+
},
|
| 201 |
+
"resource_usage": {
|
| 202 |
+
"memory_efficiency": "Memory usage was not explicitly measured",
|
| 203 |
+
"cpu_efficiency": "CPU usage was not explicitly measured",
|
| 204 |
+
"disk_usage": "Disk usage was efficient given the size of the repository"
|
| 205 |
+
}
|
| 206 |
+
},
|
| 207 |
+
"technical_quality": {
|
| 208 |
+
"code_quality_score": 75,
|
| 209 |
+
"architecture_score": 80,
|
| 210 |
+
"performance_score": 70,
|
| 211 |
+
"maintainability_score": 75,
|
| 212 |
+
"security_score": 85,
|
| 213 |
+
"scalability_score": 70
|
| 214 |
+
}
|
| 215 |
+
}
|
backtrader/source/.travis.yml
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
dist: xenial
|
| 2 |
+
language: python
|
| 3 |
+
python:
|
| 4 |
+
- "3.6"
|
| 5 |
+
- "3.7"
|
| 6 |
+
- "3.8"
|
| 7 |
+
- "nightly"
|
| 8 |
+
- "pypy"
|
| 9 |
+
- "pypy3"
|
| 10 |
+
|
| 11 |
+
matrix:
|
| 12 |
+
allow_failures:
|
| 13 |
+
python: "3.8-dev"
|
| 14 |
+
python: "nightly"
|
| 15 |
+
|
| 16 |
+
# command to install dependencies
|
| 17 |
+
# install:
|
| 18 |
+
# - pip install your_package
|
| 19 |
+
# pip install git+https://github.com/blampe/IbPy.git
|
| 20 |
+
|
| 21 |
+
# command to run tests
|
| 22 |
+
script: cd tests && nosetests -v -v
|
backtrader/source/LICENSE
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GNU GENERAL PUBLIC LICENSE
|
| 2 |
+
Version 3, 29 June 2007
|
| 3 |
+
|
| 4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
| 5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 6 |
+
of this license document, but changing it is not allowed.
|
| 7 |
+
|
| 8 |
+
Preamble
|
| 9 |
+
|
| 10 |
+
The GNU General Public License is a free, copyleft license for
|
| 11 |
+
software and other kinds of works.
|
| 12 |
+
|
| 13 |
+
The licenses for most software and other practical works are designed
|
| 14 |
+
to take away your freedom to share and change the works. By contrast,
|
| 15 |
+
the GNU General Public License is intended to guarantee your freedom to
|
| 16 |
+
share and change all versions of a program--to make sure it remains free
|
| 17 |
+
software for all its users. We, the Free Software Foundation, use the
|
| 18 |
+
GNU General Public License for most of our software; it applies also to
|
| 19 |
+
any other work released this way by its authors. You can apply it to
|
| 20 |
+
your programs, too.
|
| 21 |
+
|
| 22 |
+
When we speak of free software, we are referring to freedom, not
|
| 23 |
+
price. Our General Public Licenses are designed to make sure that you
|
| 24 |
+
have the freedom to distribute copies of free software (and charge for
|
| 25 |
+
them if you wish), that you receive source code or can get it if you
|
| 26 |
+
want it, that you can change the software or use pieces of it in new
|
| 27 |
+
free programs, and that you know you can do these things.
|
| 28 |
+
|
| 29 |
+
To protect your rights, we need to prevent others from denying you
|
| 30 |
+
these rights or asking you to surrender the rights. Therefore, you have
|
| 31 |
+
certain responsibilities if you distribute copies of the software, or if
|
| 32 |
+
you modify it: responsibilities to respect the freedom of others.
|
| 33 |
+
|
| 34 |
+
For example, if you distribute copies of such a program, whether
|
| 35 |
+
gratis or for a fee, you must pass on to the recipients the same
|
| 36 |
+
freedoms that you received. You must make sure that they, too, receive
|
| 37 |
+
or can get the source code. And you must show them these terms so they
|
| 38 |
+
know their rights.
|
| 39 |
+
|
| 40 |
+
Developers that use the GNU GPL protect your rights with two steps:
|
| 41 |
+
(1) assert copyright on the software, and (2) offer you this License
|
| 42 |
+
giving you legal permission to copy, distribute and/or modify it.
|
| 43 |
+
|
| 44 |
+
For the developers' and authors' protection, the GPL clearly explains
|
| 45 |
+
that there is no warranty for this free software. For both users' and
|
| 46 |
+
authors' sake, the GPL requires that modified versions be marked as
|
| 47 |
+
changed, so that their problems will not be attributed erroneously to
|
| 48 |
+
authors of previous versions.
|
| 49 |
+
|
| 50 |
+
Some devices are designed to deny users access to install or run
|
| 51 |
+
modified versions of the software inside them, although the manufacturer
|
| 52 |
+
can do so. This is fundamentally incompatible with the aim of
|
| 53 |
+
protecting users' freedom to change the software. The systematic
|
| 54 |
+
pattern of such abuse occurs in the area of products for individuals to
|
| 55 |
+
use, which is precisely where it is most unacceptable. Therefore, we
|
| 56 |
+
have designed this version of the GPL to prohibit the practice for those
|
| 57 |
+
products. If such problems arise substantially in other domains, we
|
| 58 |
+
stand ready to extend this provision to those domains in future versions
|
| 59 |
+
of the GPL, as needed to protect the freedom of users.
|
| 60 |
+
|
| 61 |
+
Finally, every program is threatened constantly by software patents.
|
| 62 |
+
States should not allow patents to restrict development and use of
|
| 63 |
+
software on general-purpose computers, but in those that do, we wish to
|
| 64 |
+
avoid the special danger that patents applied to a free program could
|
| 65 |
+
make it effectively proprietary. To prevent this, the GPL assures that
|
| 66 |
+
patents cannot be used to render the program non-free.
|
| 67 |
+
|
| 68 |
+
The precise terms and conditions for copying, distribution and
|
| 69 |
+
modification follow.
|
| 70 |
+
|
| 71 |
+
TERMS AND CONDITIONS
|
| 72 |
+
|
| 73 |
+
0. Definitions.
|
| 74 |
+
|
| 75 |
+
"This License" refers to version 3 of the GNU General Public License.
|
| 76 |
+
|
| 77 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
| 78 |
+
works, such as semiconductor masks.
|
| 79 |
+
|
| 80 |
+
"The Program" refers to any copyrightable work licensed under this
|
| 81 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
| 82 |
+
"recipients" may be individuals or organizations.
|
| 83 |
+
|
| 84 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
| 85 |
+
in a fashion requiring copyright permission, other than the making of an
|
| 86 |
+
exact copy. The resulting work is called a "modified version" of the
|
| 87 |
+
earlier work or a work "based on" the earlier work.
|
| 88 |
+
|
| 89 |
+
A "covered work" means either the unmodified Program or a work based
|
| 90 |
+
on the Program.
|
| 91 |
+
|
| 92 |
+
To "propagate" a work means to do anything with it that, without
|
| 93 |
+
permission, would make you directly or secondarily liable for
|
| 94 |
+
infringement under applicable copyright law, except executing it on a
|
| 95 |
+
computer or modifying a private copy. Propagation includes copying,
|
| 96 |
+
distribution (with or without modification), making available to the
|
| 97 |
+
public, and in some countries other activities as well.
|
| 98 |
+
|
| 99 |
+
To "convey" a work means any kind of propagation that enables other
|
| 100 |
+
parties to make or receive copies. Mere interaction with a user through
|
| 101 |
+
a computer network, with no transfer of a copy, is not conveying.
|
| 102 |
+
|
| 103 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
| 104 |
+
to the extent that it includes a convenient and prominently visible
|
| 105 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
| 106 |
+
tells the user that there is no warranty for the work (except to the
|
| 107 |
+
extent that warranties are provided), that licensees may convey the
|
| 108 |
+
work under this License, and how to view a copy of this License. If
|
| 109 |
+
the interface presents a list of user commands or options, such as a
|
| 110 |
+
menu, a prominent item in the list meets this criterion.
|
| 111 |
+
|
| 112 |
+
1. Source Code.
|
| 113 |
+
|
| 114 |
+
The "source code" for a work means the preferred form of the work
|
| 115 |
+
for making modifications to it. "Object code" means any non-source
|
| 116 |
+
form of a work.
|
| 117 |
+
|
| 118 |
+
A "Standard Interface" means an interface that either is an official
|
| 119 |
+
standard defined by a recognized standards body, or, in the case of
|
| 120 |
+
interfaces specified for a particular programming language, one that
|
| 121 |
+
is widely used among developers working in that language.
|
| 122 |
+
|
| 123 |
+
The "System Libraries" of an executable work include anything, other
|
| 124 |
+
than the work as a whole, that (a) is included in the normal form of
|
| 125 |
+
packaging a Major Component, but which is not part of that Major
|
| 126 |
+
Component, and (b) serves only to enable use of the work with that
|
| 127 |
+
Major Component, or to implement a Standard Interface for which an
|
| 128 |
+
implementation is available to the public in source code form. A
|
| 129 |
+
"Major Component", in this context, means a major essential component
|
| 130 |
+
(kernel, window system, and so on) of the specific operating system
|
| 131 |
+
(if any) on which the executable work runs, or a compiler used to
|
| 132 |
+
produce the work, or an object code interpreter used to run it.
|
| 133 |
+
|
| 134 |
+
The "Corresponding Source" for a work in object code form means all
|
| 135 |
+
the source code needed to generate, install, and (for an executable
|
| 136 |
+
work) run the object code and to modify the work, including scripts to
|
| 137 |
+
control those activities. However, it does not include the work's
|
| 138 |
+
System Libraries, or general-purpose tools or generally available free
|
| 139 |
+
programs which are used unmodified in performing those activities but
|
| 140 |
+
which are not part of the work. For example, Corresponding Source
|
| 141 |
+
includes interface definition files associated with source files for
|
| 142 |
+
the work, and the source code for shared libraries and dynamically
|
| 143 |
+
linked subprograms that the work is specifically designed to require,
|
| 144 |
+
such as by intimate data communication or control flow between those
|
| 145 |
+
subprograms and other parts of the work.
|
| 146 |
+
|
| 147 |
+
The Corresponding Source need not include anything that users
|
| 148 |
+
can regenerate automatically from other parts of the Corresponding
|
| 149 |
+
Source.
|
| 150 |
+
|
| 151 |
+
The Corresponding Source for a work in source code form is that
|
| 152 |
+
same work.
|
| 153 |
+
|
| 154 |
+
2. Basic Permissions.
|
| 155 |
+
|
| 156 |
+
All rights granted under this License are granted for the term of
|
| 157 |
+
copyright on the Program, and are irrevocable provided the stated
|
| 158 |
+
conditions are met. This License explicitly affirms your unlimited
|
| 159 |
+
permission to run the unmodified Program. The output from running a
|
| 160 |
+
covered work is covered by this License only if the output, given its
|
| 161 |
+
content, constitutes a covered work. This License acknowledges your
|
| 162 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
| 163 |
+
|
| 164 |
+
You may make, run and propagate covered works that you do not
|
| 165 |
+
convey, without conditions so long as your license otherwise remains
|
| 166 |
+
in force. You may convey covered works to others for the sole purpose
|
| 167 |
+
of having them make modifications exclusively for you, or provide you
|
| 168 |
+
with facilities for running those works, provided that you comply with
|
| 169 |
+
the terms of this License in conveying all material for which you do
|
| 170 |
+
not control copyright. Those thus making or running the covered works
|
| 171 |
+
for you must do so exclusively on your behalf, under your direction
|
| 172 |
+
and control, on terms that prohibit them from making any copies of
|
| 173 |
+
your copyrighted material outside their relationship with you.
|
| 174 |
+
|
| 175 |
+
Conveying under any other circumstances is permitted solely under
|
| 176 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
| 177 |
+
makes it unnecessary.
|
| 178 |
+
|
| 179 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 180 |
+
|
| 181 |
+
No covered work shall be deemed part of an effective technological
|
| 182 |
+
measure under any applicable law fulfilling obligations under article
|
| 183 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
| 184 |
+
similar laws prohibiting or restricting circumvention of such
|
| 185 |
+
measures.
|
| 186 |
+
|
| 187 |
+
When you convey a covered work, you waive any legal power to forbid
|
| 188 |
+
circumvention of technological measures to the extent such circumvention
|
| 189 |
+
is effected by exercising rights under this License with respect to
|
| 190 |
+
the covered work, and you disclaim any intention to limit operation or
|
| 191 |
+
modification of the work as a means of enforcing, against the work's
|
| 192 |
+
users, your or third parties' legal rights to forbid circumvention of
|
| 193 |
+
technological measures.
|
| 194 |
+
|
| 195 |
+
4. Conveying Verbatim Copies.
|
| 196 |
+
|
| 197 |
+
You may convey verbatim copies of the Program's source code as you
|
| 198 |
+
receive it, in any medium, provided that you conspicuously and
|
| 199 |
+
appropriately publish on each copy an appropriate copyright notice;
|
| 200 |
+
keep intact all notices stating that this License and any
|
| 201 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
| 202 |
+
keep intact all notices of the absence of any warranty; and give all
|
| 203 |
+
recipients a copy of this License along with the Program.
|
| 204 |
+
|
| 205 |
+
You may charge any price or no price for each copy that you convey,
|
| 206 |
+
and you may offer support or warranty protection for a fee.
|
| 207 |
+
|
| 208 |
+
5. Conveying Modified Source Versions.
|
| 209 |
+
|
| 210 |
+
You may convey a work based on the Program, or the modifications to
|
| 211 |
+
produce it from the Program, in the form of source code under the
|
| 212 |
+
terms of section 4, provided that you also meet all of these conditions:
|
| 213 |
+
|
| 214 |
+
a) The work must carry prominent notices stating that you modified
|
| 215 |
+
it, and giving a relevant date.
|
| 216 |
+
|
| 217 |
+
b) The work must carry prominent notices stating that it is
|
| 218 |
+
released under this License and any conditions added under section
|
| 219 |
+
7. This requirement modifies the requirement in section 4 to
|
| 220 |
+
"keep intact all notices".
|
| 221 |
+
|
| 222 |
+
c) You must license the entire work, as a whole, under this
|
| 223 |
+
License to anyone who comes into possession of a copy. This
|
| 224 |
+
License will therefore apply, along with any applicable section 7
|
| 225 |
+
additional terms, to the whole of the work, and all its parts,
|
| 226 |
+
regardless of how they are packaged. This License gives no
|
| 227 |
+
permission to license the work in any other way, but it does not
|
| 228 |
+
invalidate such permission if you have separately received it.
|
| 229 |
+
|
| 230 |
+
d) If the work has interactive user interfaces, each must display
|
| 231 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
| 232 |
+
interfaces that do not display Appropriate Legal Notices, your
|
| 233 |
+
work need not make them do so.
|
| 234 |
+
|
| 235 |
+
A compilation of a covered work with other separate and independent
|
| 236 |
+
works, which are not by their nature extensions of the covered work,
|
| 237 |
+
and which are not combined with it such as to form a larger program,
|
| 238 |
+
in or on a volume of a storage or distribution medium, is called an
|
| 239 |
+
"aggregate" if the compilation and its resulting copyright are not
|
| 240 |
+
used to limit the access or legal rights of the compilation's users
|
| 241 |
+
beyond what the individual works permit. Inclusion of a covered work
|
| 242 |
+
in an aggregate does not cause this License to apply to the other
|
| 243 |
+
parts of the aggregate.
|
| 244 |
+
|
| 245 |
+
6. Conveying Non-Source Forms.
|
| 246 |
+
|
| 247 |
+
You may convey a covered work in object code form under the terms
|
| 248 |
+
of sections 4 and 5, provided that you also convey the
|
| 249 |
+
machine-readable Corresponding Source under the terms of this License,
|
| 250 |
+
in one of these ways:
|
| 251 |
+
|
| 252 |
+
a) Convey the object code in, or embodied in, a physical product
|
| 253 |
+
(including a physical distribution medium), accompanied by the
|
| 254 |
+
Corresponding Source fixed on a durable physical medium
|
| 255 |
+
customarily used for software interchange.
|
| 256 |
+
|
| 257 |
+
b) Convey the object code in, or embodied in, a physical product
|
| 258 |
+
(including a physical distribution medium), accompanied by a
|
| 259 |
+
written offer, valid for at least three years and valid for as
|
| 260 |
+
long as you offer spare parts or customer support for that product
|
| 261 |
+
model, to give anyone who possesses the object code either (1) a
|
| 262 |
+
copy of the Corresponding Source for all the software in the
|
| 263 |
+
product that is covered by this License, on a durable physical
|
| 264 |
+
medium customarily used for software interchange, for a price no
|
| 265 |
+
more than your reasonable cost of physically performing this
|
| 266 |
+
conveying of source, or (2) access to copy the
|
| 267 |
+
Corresponding Source from a network server at no charge.
|
| 268 |
+
|
| 269 |
+
c) Convey individual copies of the object code with a copy of the
|
| 270 |
+
written offer to provide the Corresponding Source. This
|
| 271 |
+
alternative is allowed only occasionally and noncommercially, and
|
| 272 |
+
only if you received the object code with such an offer, in accord
|
| 273 |
+
with subsection 6b.
|
| 274 |
+
|
| 275 |
+
d) Convey the object code by offering access from a designated
|
| 276 |
+
place (gratis or for a charge), and offer equivalent access to the
|
| 277 |
+
Corresponding Source in the same way through the same place at no
|
| 278 |
+
further charge. You need not require recipients to copy the
|
| 279 |
+
Corresponding Source along with the object code. If the place to
|
| 280 |
+
copy the object code is a network server, the Corresponding Source
|
| 281 |
+
may be on a different server (operated by you or a third party)
|
| 282 |
+
that supports equivalent copying facilities, provided you maintain
|
| 283 |
+
clear directions next to the object code saying where to find the
|
| 284 |
+
Corresponding Source. Regardless of what server hosts the
|
| 285 |
+
Corresponding Source, you remain obligated to ensure that it is
|
| 286 |
+
available for as long as needed to satisfy these requirements.
|
| 287 |
+
|
| 288 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
| 289 |
+
you inform other peers where the object code and Corresponding
|
| 290 |
+
Source of the work are being offered to the general public at no
|
| 291 |
+
charge under subsection 6d.
|
| 292 |
+
|
| 293 |
+
A separable portion of the object code, whose source code is excluded
|
| 294 |
+
from the Corresponding Source as a System Library, need not be
|
| 295 |
+
included in conveying the object code work.
|
| 296 |
+
|
| 297 |
+
A "User Product" is either (1) a "consumer product", which means any
|
| 298 |
+
tangible personal property which is normally used for personal, family,
|
| 299 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
| 300 |
+
into a dwelling. In determining whether a product is a consumer product,
|
| 301 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
| 302 |
+
product received by a particular user, "normally used" refers to a
|
| 303 |
+
typical or common use of that class of product, regardless of the status
|
| 304 |
+
of the particular user or of the way in which the particular user
|
| 305 |
+
actually uses, or expects or is expected to use, the product. A product
|
| 306 |
+
is a consumer product regardless of whether the product has substantial
|
| 307 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
| 308 |
+
the only significant mode of use of the product.
|
| 309 |
+
|
| 310 |
+
"Installation Information" for a User Product means any methods,
|
| 311 |
+
procedures, authorization keys, or other information required to install
|
| 312 |
+
and execute modified versions of a covered work in that User Product from
|
| 313 |
+
a modified version of its Corresponding Source. The information must
|
| 314 |
+
suffice to ensure that the continued functioning of the modified object
|
| 315 |
+
code is in no case prevented or interfered with solely because
|
| 316 |
+
modification has been made.
|
| 317 |
+
|
| 318 |
+
If you convey an object code work under this section in, or with, or
|
| 319 |
+
specifically for use in, a User Product, and the conveying occurs as
|
| 320 |
+
part of a transaction in which the right of possession and use of the
|
| 321 |
+
User Product is transferred to the recipient in perpetuity or for a
|
| 322 |
+
fixed term (regardless of how the transaction is characterized), the
|
| 323 |
+
Corresponding Source conveyed under this section must be accompanied
|
| 324 |
+
by the Installation Information. But this requirement does not apply
|
| 325 |
+
if neither you nor any third party retains the ability to install
|
| 326 |
+
modified object code on the User Product (for example, the work has
|
| 327 |
+
been installed in ROM).
|
| 328 |
+
|
| 329 |
+
The requirement to provide Installation Information does not include a
|
| 330 |
+
requirement to continue to provide support service, warranty, or updates
|
| 331 |
+
for a work that has been modified or installed by the recipient, or for
|
| 332 |
+
the User Product in which it has been modified or installed. Access to a
|
| 333 |
+
network may be denied when the modification itself materially and
|
| 334 |
+
adversely affects the operation of the network or violates the rules and
|
| 335 |
+
protocols for communication across the network.
|
| 336 |
+
|
| 337 |
+
Corresponding Source conveyed, and Installation Information provided,
|
| 338 |
+
in accord with this section must be in a format that is publicly
|
| 339 |
+
documented (and with an implementation available to the public in
|
| 340 |
+
source code form), and must require no special password or key for
|
| 341 |
+
unpacking, reading or copying.
|
| 342 |
+
|
| 343 |
+
7. Additional Terms.
|
| 344 |
+
|
| 345 |
+
"Additional permissions" are terms that supplement the terms of this
|
| 346 |
+
License by making exceptions from one or more of its conditions.
|
| 347 |
+
Additional permissions that are applicable to the entire Program shall
|
| 348 |
+
be treated as though they were included in this License, to the extent
|
| 349 |
+
that they are valid under applicable law. If additional permissions
|
| 350 |
+
apply only to part of the Program, that part may be used separately
|
| 351 |
+
under those permissions, but the entire Program remains governed by
|
| 352 |
+
this License without regard to the additional permissions.
|
| 353 |
+
|
| 354 |
+
When you convey a copy of a covered work, you may at your option
|
| 355 |
+
remove any additional permissions from that copy, or from any part of
|
| 356 |
+
it. (Additional permissions may be written to require their own
|
| 357 |
+
removal in certain cases when you modify the work.) You may place
|
| 358 |
+
additional permissions on material, added by you to a covered work,
|
| 359 |
+
for which you have or can give appropriate copyright permission.
|
| 360 |
+
|
| 361 |
+
Notwithstanding any other provision of this License, for material you
|
| 362 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
| 363 |
+
that material) supplement the terms of this License with terms:
|
| 364 |
+
|
| 365 |
+
a) Disclaiming warranty or limiting liability differently from the
|
| 366 |
+
terms of sections 15 and 16 of this License; or
|
| 367 |
+
|
| 368 |
+
b) Requiring preservation of specified reasonable legal notices or
|
| 369 |
+
author attributions in that material or in the Appropriate Legal
|
| 370 |
+
Notices displayed by works containing it; or
|
| 371 |
+
|
| 372 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
| 373 |
+
requiring that modified versions of such material be marked in
|
| 374 |
+
reasonable ways as different from the original version; or
|
| 375 |
+
|
| 376 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
| 377 |
+
authors of the material; or
|
| 378 |
+
|
| 379 |
+
e) Declining to grant rights under trademark law for use of some
|
| 380 |
+
trade names, trademarks, or service marks; or
|
| 381 |
+
|
| 382 |
+
f) Requiring indemnification of licensors and authors of that
|
| 383 |
+
material by anyone who conveys the material (or modified versions of
|
| 384 |
+
it) with contractual assumptions of liability to the recipient, for
|
| 385 |
+
any liability that these contractual assumptions directly impose on
|
| 386 |
+
those licensors and authors.
|
| 387 |
+
|
| 388 |
+
All other non-permissive additional terms are considered "further
|
| 389 |
+
restrictions" within the meaning of section 10. If the Program as you
|
| 390 |
+
received it, or any part of it, contains a notice stating that it is
|
| 391 |
+
governed by this License along with a term that is a further
|
| 392 |
+
restriction, you may remove that term. If a license document contains
|
| 393 |
+
a further restriction but permits relicensing or conveying under this
|
| 394 |
+
License, you may add to a covered work material governed by the terms
|
| 395 |
+
of that license document, provided that the further restriction does
|
| 396 |
+
not survive such relicensing or conveying.
|
| 397 |
+
|
| 398 |
+
If you add terms to a covered work in accord with this section, you
|
| 399 |
+
must place, in the relevant source files, a statement of the
|
| 400 |
+
additional terms that apply to those files, or a notice indicating
|
| 401 |
+
where to find the applicable terms.
|
| 402 |
+
|
| 403 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
| 404 |
+
form of a separately written license, or stated as exceptions;
|
| 405 |
+
the above requirements apply either way.
|
| 406 |
+
|
| 407 |
+
8. Termination.
|
| 408 |
+
|
| 409 |
+
You may not propagate or modify a covered work except as expressly
|
| 410 |
+
provided under this License. Any attempt otherwise to propagate or
|
| 411 |
+
modify it is void, and will automatically terminate your rights under
|
| 412 |
+
this License (including any patent licenses granted under the third
|
| 413 |
+
paragraph of section 11).
|
| 414 |
+
|
| 415 |
+
However, if you cease all violation of this License, then your
|
| 416 |
+
license from a particular copyright holder is reinstated (a)
|
| 417 |
+
provisionally, unless and until the copyright holder explicitly and
|
| 418 |
+
finally terminates your license, and (b) permanently, if the copyright
|
| 419 |
+
holder fails to notify you of the violation by some reasonable means
|
| 420 |
+
prior to 60 days after the cessation.
|
| 421 |
+
|
| 422 |
+
Moreover, your license from a particular copyright holder is
|
| 423 |
+
reinstated permanently if the copyright holder notifies you of the
|
| 424 |
+
violation by some reasonable means, this is the first time you have
|
| 425 |
+
received notice of violation of this License (for any work) from that
|
| 426 |
+
copyright holder, and you cure the violation prior to 30 days after
|
| 427 |
+
your receipt of the notice.
|
| 428 |
+
|
| 429 |
+
Termination of your rights under this section does not terminate the
|
| 430 |
+
licenses of parties who have received copies or rights from you under
|
| 431 |
+
this License. If your rights have been terminated and not permanently
|
| 432 |
+
reinstated, you do not qualify to receive new licenses for the same
|
| 433 |
+
material under section 10.
|
| 434 |
+
|
| 435 |
+
9. Acceptance Not Required for Having Copies.
|
| 436 |
+
|
| 437 |
+
You are not required to accept this License in order to receive or
|
| 438 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
| 439 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
| 440 |
+
to receive a copy likewise does not require acceptance. However,
|
| 441 |
+
nothing other than this License grants you permission to propagate or
|
| 442 |
+
modify any covered work. These actions infringe copyright if you do
|
| 443 |
+
not accept this License. Therefore, by modifying or propagating a
|
| 444 |
+
covered work, you indicate your acceptance of this License to do so.
|
| 445 |
+
|
| 446 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 447 |
+
|
| 448 |
+
Each time you convey a covered work, the recipient automatically
|
| 449 |
+
receives a license from the original licensors, to run, modify and
|
| 450 |
+
propagate that work, subject to this License. You are not responsible
|
| 451 |
+
for enforcing compliance by third parties with this License.
|
| 452 |
+
|
| 453 |
+
An "entity transaction" is a transaction transferring control of an
|
| 454 |
+
organization, or substantially all assets of one, or subdividing an
|
| 455 |
+
organization, or merging organizations. If propagation of a covered
|
| 456 |
+
work results from an entity transaction, each party to that
|
| 457 |
+
transaction who receives a copy of the work also receives whatever
|
| 458 |
+
licenses to the work the party's predecessor in interest had or could
|
| 459 |
+
give under the previous paragraph, plus a right to possession of the
|
| 460 |
+
Corresponding Source of the work from the predecessor in interest, if
|
| 461 |
+
the predecessor has it or can get it with reasonable efforts.
|
| 462 |
+
|
| 463 |
+
You may not impose any further restrictions on the exercise of the
|
| 464 |
+
rights granted or affirmed under this License. For example, you may
|
| 465 |
+
not impose a license fee, royalty, or other charge for exercise of
|
| 466 |
+
rights granted under this License, and you may not initiate litigation
|
| 467 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
| 468 |
+
any patent claim is infringed by making, using, selling, offering for
|
| 469 |
+
sale, or importing the Program or any portion of it.
|
| 470 |
+
|
| 471 |
+
11. Patents.
|
| 472 |
+
|
| 473 |
+
A "contributor" is a copyright holder who authorizes use under this
|
| 474 |
+
License of the Program or a work on which the Program is based. The
|
| 475 |
+
work thus licensed is called the contributor's "contributor version".
|
| 476 |
+
|
| 477 |
+
A contributor's "essential patent claims" are all patent claims
|
| 478 |
+
owned or controlled by the contributor, whether already acquired or
|
| 479 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
| 480 |
+
by this License, of making, using, or selling its contributor version,
|
| 481 |
+
but do not include claims that would be infringed only as a
|
| 482 |
+
consequence of further modification of the contributor version. For
|
| 483 |
+
purposes of this definition, "control" includes the right to grant
|
| 484 |
+
patent sublicenses in a manner consistent with the requirements of
|
| 485 |
+
this License.
|
| 486 |
+
|
| 487 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
| 488 |
+
patent license under the contributor's essential patent claims, to
|
| 489 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
| 490 |
+
propagate the contents of its contributor version.
|
| 491 |
+
|
| 492 |
+
In the following three paragraphs, a "patent license" is any express
|
| 493 |
+
agreement or commitment, however denominated, not to enforce a patent
|
| 494 |
+
(such as an express permission to practice a patent or covenant not to
|
| 495 |
+
sue for patent infringement). To "grant" such a patent license to a
|
| 496 |
+
party means to make such an agreement or commitment not to enforce a
|
| 497 |
+
patent against the party.
|
| 498 |
+
|
| 499 |
+
If you convey a covered work, knowingly relying on a patent license,
|
| 500 |
+
and the Corresponding Source of the work is not available for anyone
|
| 501 |
+
to copy, free of charge and under the terms of this License, through a
|
| 502 |
+
publicly available network server or other readily accessible means,
|
| 503 |
+
then you must either (1) cause the Corresponding Source to be so
|
| 504 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
| 505 |
+
patent license for this particular work, or (3) arrange, in a manner
|
| 506 |
+
consistent with the requirements of this License, to extend the patent
|
| 507 |
+
license to downstream recipients. "Knowingly relying" means you have
|
| 508 |
+
actual knowledge that, but for the patent license, your conveying the
|
| 509 |
+
covered work in a country, or your recipient's use of the covered work
|
| 510 |
+
in a country, would infringe one or more identifiable patents in that
|
| 511 |
+
country that you have reason to believe are valid.
|
| 512 |
+
|
| 513 |
+
If, pursuant to or in connection with a single transaction or
|
| 514 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
| 515 |
+
covered work, and grant a patent license to some of the parties
|
| 516 |
+
receiving the covered work authorizing them to use, propagate, modify
|
| 517 |
+
or convey a specific copy of the covered work, then the patent license
|
| 518 |
+
you grant is automatically extended to all recipients of the covered
|
| 519 |
+
work and works based on it.
|
| 520 |
+
|
| 521 |
+
A patent license is "discriminatory" if it does not include within
|
| 522 |
+
the scope of its coverage, prohibits the exercise of, or is
|
| 523 |
+
conditioned on the non-exercise of one or more of the rights that are
|
| 524 |
+
specifically granted under this License. You may not convey a covered
|
| 525 |
+
work if you are a party to an arrangement with a third party that is
|
| 526 |
+
in the business of distributing software, under which you make payment
|
| 527 |
+
to the third party based on the extent of your activity of conveying
|
| 528 |
+
the work, and under which the third party grants, to any of the
|
| 529 |
+
parties who would receive the covered work from you, a discriminatory
|
| 530 |
+
patent license (a) in connection with copies of the covered work
|
| 531 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
| 532 |
+
for and in connection with specific products or compilations that
|
| 533 |
+
contain the covered work, unless you entered into that arrangement,
|
| 534 |
+
or that patent license was granted, prior to 28 March 2007.
|
| 535 |
+
|
| 536 |
+
Nothing in this License shall be construed as excluding or limiting
|
| 537 |
+
any implied license or other defenses to infringement that may
|
| 538 |
+
otherwise be available to you under applicable patent law.
|
| 539 |
+
|
| 540 |
+
12. No Surrender of Others' Freedom.
|
| 541 |
+
|
| 542 |
+
If conditions are imposed on you (whether by court order, agreement or
|
| 543 |
+
otherwise) that contradict the conditions of this License, they do not
|
| 544 |
+
excuse you from the conditions of this License. If you cannot convey a
|
| 545 |
+
covered work so as to satisfy simultaneously your obligations under this
|
| 546 |
+
License and any other pertinent obligations, then as a consequence you may
|
| 547 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
| 548 |
+
to collect a royalty for further conveying from those to whom you convey
|
| 549 |
+
the Program, the only way you could satisfy both those terms and this
|
| 550 |
+
License would be to refrain entirely from conveying the Program.
|
| 551 |
+
|
| 552 |
+
13. Use with the GNU Affero General Public License.
|
| 553 |
+
|
| 554 |
+
Notwithstanding any other provision of this License, you have
|
| 555 |
+
permission to link or combine any covered work with a work licensed
|
| 556 |
+
under version 3 of the GNU Affero General Public License into a single
|
| 557 |
+
combined work, and to convey the resulting work. The terms of this
|
| 558 |
+
License will continue to apply to the part which is the covered work,
|
| 559 |
+
but the special requirements of the GNU Affero General Public License,
|
| 560 |
+
section 13, concerning interaction through a network will apply to the
|
| 561 |
+
combination as such.
|
| 562 |
+
|
| 563 |
+
14. Revised Versions of this License.
|
| 564 |
+
|
| 565 |
+
The Free Software Foundation may publish revised and/or new versions of
|
| 566 |
+
the GNU General Public License from time to time. Such new versions will
|
| 567 |
+
be similar in spirit to the present version, but may differ in detail to
|
| 568 |
+
address new problems or concerns.
|
| 569 |
+
|
| 570 |
+
Each version is given a distinguishing version number. If the
|
| 571 |
+
Program specifies that a certain numbered version of the GNU General
|
| 572 |
+
Public License "or any later version" applies to it, you have the
|
| 573 |
+
option of following the terms and conditions either of that numbered
|
| 574 |
+
version or of any later version published by the Free Software
|
| 575 |
+
Foundation. If the Program does not specify a version number of the
|
| 576 |
+
GNU General Public License, you may choose any version ever published
|
| 577 |
+
by the Free Software Foundation.
|
| 578 |
+
|
| 579 |
+
If the Program specifies that a proxy can decide which future
|
| 580 |
+
versions of the GNU General Public License can be used, that proxy's
|
| 581 |
+
public statement of acceptance of a version permanently authorizes you
|
| 582 |
+
to choose that version for the Program.
|
| 583 |
+
|
| 584 |
+
Later license versions may give you additional or different
|
| 585 |
+
permissions. However, no additional obligations are imposed on any
|
| 586 |
+
author or copyright holder as a result of your choosing to follow a
|
| 587 |
+
later version.
|
| 588 |
+
|
| 589 |
+
15. Disclaimer of Warranty.
|
| 590 |
+
|
| 591 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
| 592 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
| 593 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
| 594 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
| 595 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 596 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
| 597 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
| 598 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 599 |
+
|
| 600 |
+
16. Limitation of Liability.
|
| 601 |
+
|
| 602 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
| 603 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
| 604 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
| 605 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
| 606 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
| 607 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
| 608 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
| 609 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
| 610 |
+
SUCH DAMAGES.
|
| 611 |
+
|
| 612 |
+
17. Interpretation of Sections 15 and 16.
|
| 613 |
+
|
| 614 |
+
If the disclaimer of warranty and limitation of liability provided
|
| 615 |
+
above cannot be given local legal effect according to their terms,
|
| 616 |
+
reviewing courts shall apply local law that most closely approximates
|
| 617 |
+
an absolute waiver of all civil liability in connection with the
|
| 618 |
+
Program, unless a warranty or assumption of liability accompanies a
|
| 619 |
+
copy of the Program in return for a fee.
|
| 620 |
+
|
| 621 |
+
END OF TERMS AND CONDITIONS
|
| 622 |
+
|
| 623 |
+
How to Apply These Terms to Your New Programs
|
| 624 |
+
|
| 625 |
+
If you develop a new program, and you want it to be of the greatest
|
| 626 |
+
possible use to the public, the best way to achieve this is to make it
|
| 627 |
+
free software which everyone can redistribute and change under these terms.
|
| 628 |
+
|
| 629 |
+
To do so, attach the following notices to the program. It is safest
|
| 630 |
+
to attach them to the start of each source file to most effectively
|
| 631 |
+
state the exclusion of warranty; and each file should have at least
|
| 632 |
+
the "copyright" line and a pointer to where the full notice is found.
|
| 633 |
+
|
| 634 |
+
{one line to give the program's name and a brief idea of what it does.}
|
| 635 |
+
Copyright (C) {year} {name of author}
|
| 636 |
+
|
| 637 |
+
This program is free software: you can redistribute it and/or modify
|
| 638 |
+
it under the terms of the GNU General Public License as published by
|
| 639 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 640 |
+
(at your option) any later version.
|
| 641 |
+
|
| 642 |
+
This program is distributed in the hope that it will be useful,
|
| 643 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 644 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 645 |
+
GNU General Public License for more details.
|
| 646 |
+
|
| 647 |
+
You should have received a copy of the GNU General Public License
|
| 648 |
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 649 |
+
|
| 650 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 651 |
+
|
| 652 |
+
If the program does terminal interaction, make it output a short
|
| 653 |
+
notice like this when it starts in an interactive mode:
|
| 654 |
+
|
| 655 |
+
{project} Copyright (C) {year} {fullname}
|
| 656 |
+
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
| 657 |
+
This is free software, and you are welcome to redistribute it
|
| 658 |
+
under certain conditions; type `show c' for details.
|
| 659 |
+
|
| 660 |
+
The hypothetical commands `show w' and `show c' should show the appropriate
|
| 661 |
+
parts of the General Public License. Of course, your program's commands
|
| 662 |
+
might be different; for a GUI interface, you would use an "about box".
|
| 663 |
+
|
| 664 |
+
You should also get your employer (if you work as a programmer) or school,
|
| 665 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
| 666 |
+
For more information on this, and how to apply and follow the GNU GPL, see
|
| 667 |
+
<http://www.gnu.org/licenses/>.
|
| 668 |
+
|
| 669 |
+
The GNU General Public License does not permit incorporating your program
|
| 670 |
+
into proprietary programs. If your program is a subroutine library, you
|
| 671 |
+
may consider it more useful to permit linking proprietary applications with
|
| 672 |
+
the library. If this is what you want to do, use the GNU Lesser General
|
| 673 |
+
Public License instead of this License. But first, please read
|
| 674 |
+
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
backtrader/source/README.rst
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
backtrader
|
| 2 |
+
==========
|
| 3 |
+
|
| 4 |
+
.. image:: https://img.shields.io/pypi/v/backtrader.svg
|
| 5 |
+
:alt: PyPi Version
|
| 6 |
+
:scale: 100%
|
| 7 |
+
:target: https://pypi.python.org/pypi/backtrader/
|
| 8 |
+
|
| 9 |
+
.. .. image:: https://img.shields.io/pypi/dm/backtrader.svg
|
| 10 |
+
:alt: PyPi Monthly Donwloads
|
| 11 |
+
:scale: 100%
|
| 12 |
+
:target: https://pypi.python.org/pypi/backtrader/
|
| 13 |
+
|
| 14 |
+
.. image:: https://img.shields.io/pypi/l/backtrader.svg
|
| 15 |
+
:alt: License
|
| 16 |
+
:scale: 100%
|
| 17 |
+
:target: https://github.com/backtrader/backtrader/blob/master/LICENSE
|
| 18 |
+
.. image:: https://travis-ci.org/backtrader/backtrader.png?branch=master
|
| 19 |
+
:alt: Travis-ci Build Status
|
| 20 |
+
:scale: 100%
|
| 21 |
+
:target: https://travis-ci.org/backtrader/backtrader
|
| 22 |
+
.. image:: https://img.shields.io/pypi/pyversions/backtrader.svg
|
| 23 |
+
:alt: Python versions
|
| 24 |
+
:scale: 100%
|
| 25 |
+
:target: https://pypi.python.org/pypi/backtrader/
|
| 26 |
+
|
| 27 |
+
**Yahoo API Note**:
|
| 28 |
+
|
| 29 |
+
[2018-11-16] After some testing it would seem that data downloads can be
|
| 30 |
+
again relied upon over the web interface (or API ``v7``)
|
| 31 |
+
|
| 32 |
+
**Tickets**
|
| 33 |
+
|
| 34 |
+
The ticket system is (was, actually) more often than not abused to ask for
|
| 35 |
+
advice about samples.
|
| 36 |
+
|
| 37 |
+
For **feedback/questions/...** use the `Community <https://community.backtrader.com>`_
|
| 38 |
+
|
| 39 |
+
Here a snippet of a Simple Moving Average CrossOver. It can be done in several
|
| 40 |
+
different ways. Use the docs (and examples) Luke!
|
| 41 |
+
::
|
| 42 |
+
|
| 43 |
+
from datetime import datetime
|
| 44 |
+
import backtrader as bt
|
| 45 |
+
|
| 46 |
+
class SmaCross(bt.SignalStrategy):
|
| 47 |
+
def __init__(self):
|
| 48 |
+
sma1, sma2 = bt.ind.SMA(period=10), bt.ind.SMA(period=30)
|
| 49 |
+
crossover = bt.ind.CrossOver(sma1, sma2)
|
| 50 |
+
self.signal_add(bt.SIGNAL_LONG, crossover)
|
| 51 |
+
|
| 52 |
+
cerebro = bt.Cerebro()
|
| 53 |
+
cerebro.addstrategy(SmaCross)
|
| 54 |
+
|
| 55 |
+
data0 = bt.feeds.YahooFinanceData(dataname='MSFT', fromdate=datetime(2011, 1, 1),
|
| 56 |
+
todate=datetime(2012, 12, 31))
|
| 57 |
+
cerebro.adddata(data0)
|
| 58 |
+
|
| 59 |
+
cerebro.run()
|
| 60 |
+
cerebro.plot()
|
| 61 |
+
|
| 62 |
+
Including a full featured chart. Give it a try! This is included in the samples
|
| 63 |
+
as ``sigsmacross/sigsmacross2.py``. Along it is ``sigsmacross.py`` which can be
|
| 64 |
+
parametrized from the command line.
|
| 65 |
+
|
| 66 |
+
Features:
|
| 67 |
+
=========
|
| 68 |
+
|
| 69 |
+
Live Trading and backtesting platform written in Python.
|
| 70 |
+
|
| 71 |
+
- Live Data Feed and Trading with
|
| 72 |
+
|
| 73 |
+
- Interactive Brokers (needs ``IbPy`` and benefits greatly from an
|
| 74 |
+
installed ``pytz``)
|
| 75 |
+
- *Visual Chart* (needs a fork of ``comtypes`` until a pull request is
|
| 76 |
+
integrated in the release and benefits from ``pytz``)
|
| 77 |
+
- *Oanda* (needs ``oandapy``) (REST API Only - v20 did not support
|
| 78 |
+
streaming when implemented)
|
| 79 |
+
|
| 80 |
+
- Data feeds from csv/files, online sources or from *pandas* and *blaze*
|
| 81 |
+
- Filters for datas, like breaking a daily bar into chunks to simulate
|
| 82 |
+
intraday or working with Renko bricks
|
| 83 |
+
- Multiple data feeds and multiple strategies supported
|
| 84 |
+
- Multiple timeframes at once
|
| 85 |
+
- Integrated Resampling and Replaying
|
| 86 |
+
- Step by Step backtesting or at once (except in the evaluation of the Strategy)
|
| 87 |
+
- Integrated battery of indicators
|
| 88 |
+
- *TA-Lib* indicator support (needs python *ta-lib* / check the docs)
|
| 89 |
+
- Easy development of custom indicators
|
| 90 |
+
- Analyzers (for example: TimeReturn, Sharpe Ratio, SQN) and ``pyfolio``
|
| 91 |
+
integration (**deprecated**)
|
| 92 |
+
- Flexible definition of commission schemes
|
| 93 |
+
- Integrated broker simulation with *Market*, *Close*, *Limit*, *Stop*,
|
| 94 |
+
*StopLimit*, *StopTrail*, *StopTrailLimit*and *OCO* orders, bracket order,
|
| 95 |
+
slippage, volume filling strategies and continuous cash adjustmet for
|
| 96 |
+
future-like instruments
|
| 97 |
+
- Sizers for automated staking
|
| 98 |
+
- Cheat-on-Close and Cheat-on-Open modes
|
| 99 |
+
- Schedulers
|
| 100 |
+
- Trading Calendars
|
| 101 |
+
- Plotting (requires matplotlib)
|
| 102 |
+
|
| 103 |
+
Documentation
|
| 104 |
+
=============
|
| 105 |
+
|
| 106 |
+
The blog:
|
| 107 |
+
|
| 108 |
+
- `Blog <http://www.backtrader.com/blog>`_
|
| 109 |
+
|
| 110 |
+
Read the full documentation at:
|
| 111 |
+
|
| 112 |
+
- `Documentation <http://www.backtrader.com/docu>`_
|
| 113 |
+
|
| 114 |
+
List of built-in Indicators (122)
|
| 115 |
+
|
| 116 |
+
- `Indicators Reference <http://www.backtrader.com/docu/indautoref.html>`_
|
| 117 |
+
|
| 118 |
+
Python 2/3 Support
|
| 119 |
+
==================
|
| 120 |
+
|
| 121 |
+
- Python >= ``3.2``
|
| 122 |
+
|
| 123 |
+
- It also works with ``pypy`` and ``pypy3`` (no plotting - ``matplotlib`` is
|
| 124 |
+
not supported under *pypy*)
|
| 125 |
+
|
| 126 |
+
Installation
|
| 127 |
+
============
|
| 128 |
+
|
| 129 |
+
``backtrader`` is self-contained with no external dependencies (except if you
|
| 130 |
+
want to plot)
|
| 131 |
+
|
| 132 |
+
From *pypi*:
|
| 133 |
+
|
| 134 |
+
- ``pip install backtrader``
|
| 135 |
+
|
| 136 |
+
- ``pip install backtrader[plotting]``
|
| 137 |
+
|
| 138 |
+
If ``matplotlib`` is not installed and you wish to do some plotting
|
| 139 |
+
|
| 140 |
+
.. note:: The minimum matplotlib version is ``1.4.1``
|
| 141 |
+
|
| 142 |
+
An example for *IB* Data Feeds/Trading:
|
| 143 |
+
|
| 144 |
+
- ``IbPy`` doesn't seem to be in PyPi. Do either::
|
| 145 |
+
|
| 146 |
+
pip install git+https://github.com/blampe/IbPy.git
|
| 147 |
+
|
| 148 |
+
or (if ``git`` is not available in your system)::
|
| 149 |
+
|
| 150 |
+
pip install https://github.com/blampe/IbPy/archive/master.zip
|
| 151 |
+
|
| 152 |
+
For other functionalities like: ``Visual Chart``, ``Oanda``, ``TA-Lib``, check
|
| 153 |
+
the dependencies in the documentation.
|
| 154 |
+
|
| 155 |
+
From source:
|
| 156 |
+
|
| 157 |
+
- Place the *backtrader* directory found in the sources inside your project
|
| 158 |
+
|
| 159 |
+
Version numbering
|
| 160 |
+
=================
|
| 161 |
+
|
| 162 |
+
X.Y.Z.I
|
| 163 |
+
|
| 164 |
+
- X: Major version number. Should stay stable unless something big is changed
|
| 165 |
+
like an overhaul to use ``numpy``
|
| 166 |
+
- Y: Minor version number. To be changed upon adding a complete new feature or
|
| 167 |
+
(god forbids) an incompatible API change.
|
| 168 |
+
- Z: Revision version number. To be changed for documentation updates, small
|
| 169 |
+
changes, small bug fixes
|
| 170 |
+
- I: Number of Indicators already built into the platform
|
backtrader/source/__init__.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
backtrader Project Package Initialization File
|
| 4 |
+
"""
|
backtrader/source/backtrader/__init__.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from .version import __version__, __btversion__
|
| 25 |
+
|
| 26 |
+
from .errors import *
|
| 27 |
+
from . import errors as errors
|
| 28 |
+
|
| 29 |
+
from .utils import num2date, date2num, time2num, num2time
|
| 30 |
+
|
| 31 |
+
from .linebuffer import *
|
| 32 |
+
from .functions import *
|
| 33 |
+
|
| 34 |
+
from .order import *
|
| 35 |
+
from .comminfo import *
|
| 36 |
+
from .trade import *
|
| 37 |
+
from .position import *
|
| 38 |
+
|
| 39 |
+
from .store import Store
|
| 40 |
+
|
| 41 |
+
from . import broker as broker
|
| 42 |
+
from .broker import *
|
| 43 |
+
|
| 44 |
+
from .lineseries import *
|
| 45 |
+
|
| 46 |
+
from .dataseries import *
|
| 47 |
+
from .feed import *
|
| 48 |
+
from .resamplerfilter import *
|
| 49 |
+
|
| 50 |
+
from .lineiterator import *
|
| 51 |
+
from .indicator import *
|
| 52 |
+
from .analyzer import *
|
| 53 |
+
from .observer import *
|
| 54 |
+
from .sizer import *
|
| 55 |
+
from .sizers import SizerFix # old sizer for compatibility
|
| 56 |
+
from .strategy import *
|
| 57 |
+
|
| 58 |
+
from .writer import *
|
| 59 |
+
|
| 60 |
+
from .signal import *
|
| 61 |
+
|
| 62 |
+
from .cerebro import *
|
| 63 |
+
from .timer import *
|
| 64 |
+
from .flt import *
|
| 65 |
+
|
| 66 |
+
from . import utils as utils
|
| 67 |
+
|
| 68 |
+
from . import feeds as feeds
|
| 69 |
+
from . import indicators as indicators
|
| 70 |
+
from . import indicators as ind
|
| 71 |
+
from . import studies as studies
|
| 72 |
+
from . import strategies as strategies
|
| 73 |
+
from . import strategies as strats
|
| 74 |
+
from . import observers as observers
|
| 75 |
+
from . import observers as obs
|
| 76 |
+
from . import analyzers as analyzers
|
| 77 |
+
from . import commissions as commissions
|
| 78 |
+
from . import commissions as comms
|
| 79 |
+
from . import filters as filters
|
| 80 |
+
from . import signals as signals
|
| 81 |
+
from . import sizers as sizers
|
| 82 |
+
from . import stores as stores
|
| 83 |
+
from . import brokers as brokers
|
| 84 |
+
from . import timer as timer
|
| 85 |
+
|
| 86 |
+
from . import talib as talib
|
| 87 |
+
|
| 88 |
+
# Load contributed indicators and studies
|
| 89 |
+
import backtrader.indicators.contrib
|
| 90 |
+
import backtrader.studies.contrib
|
backtrader/source/backtrader/analyzer.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import calendar
|
| 25 |
+
from collections import OrderedDict
|
| 26 |
+
import datetime
|
| 27 |
+
import pprint as pp
|
| 28 |
+
|
| 29 |
+
import backtrader as bt
|
| 30 |
+
from backtrader import TimeFrame
|
| 31 |
+
from backtrader.utils.py3 import MAXINT, with_metaclass
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class MetaAnalyzer(bt.MetaParams):
|
| 35 |
+
def donew(cls, *args, **kwargs):
|
| 36 |
+
'''
|
| 37 |
+
Intercept the strategy parameter
|
| 38 |
+
'''
|
| 39 |
+
# Create the object and set the params in place
|
| 40 |
+
_obj, args, kwargs = super(MetaAnalyzer, cls).donew(*args, **kwargs)
|
| 41 |
+
|
| 42 |
+
_obj._children = list()
|
| 43 |
+
|
| 44 |
+
_obj.strategy = strategy = bt.metabase.findowner(_obj, bt.Strategy)
|
| 45 |
+
_obj._parent = bt.metabase.findowner(_obj, Analyzer)
|
| 46 |
+
|
| 47 |
+
# Register with a master observer if created inside one
|
| 48 |
+
masterobs = bt.metabase.findowner(_obj, bt.Observer)
|
| 49 |
+
if masterobs is not None:
|
| 50 |
+
masterobs._register_analyzer(_obj)
|
| 51 |
+
|
| 52 |
+
_obj.datas = strategy.datas
|
| 53 |
+
|
| 54 |
+
# For each data add aliases: for first data: data and data0
|
| 55 |
+
if _obj.datas:
|
| 56 |
+
_obj.data = data = _obj.datas[0]
|
| 57 |
+
|
| 58 |
+
for l, line in enumerate(data.lines):
|
| 59 |
+
linealias = data._getlinealias(l)
|
| 60 |
+
if linealias:
|
| 61 |
+
setattr(_obj, 'data_%s' % linealias, line)
|
| 62 |
+
setattr(_obj, 'data_%d' % l, line)
|
| 63 |
+
|
| 64 |
+
for d, data in enumerate(_obj.datas):
|
| 65 |
+
setattr(_obj, 'data%d' % d, data)
|
| 66 |
+
|
| 67 |
+
for l, line in enumerate(data.lines):
|
| 68 |
+
linealias = data._getlinealias(l)
|
| 69 |
+
if linealias:
|
| 70 |
+
setattr(_obj, 'data%d_%s' % (d, linealias), line)
|
| 71 |
+
setattr(_obj, 'data%d_%d' % (d, l), line)
|
| 72 |
+
|
| 73 |
+
_obj.create_analysis()
|
| 74 |
+
|
| 75 |
+
# Return to the normal chain
|
| 76 |
+
return _obj, args, kwargs
|
| 77 |
+
|
| 78 |
+
def dopostinit(cls, _obj, *args, **kwargs):
|
| 79 |
+
_obj, args, kwargs = \
|
| 80 |
+
super(MetaAnalyzer, cls).dopostinit(_obj, *args, **kwargs)
|
| 81 |
+
|
| 82 |
+
if _obj._parent is not None:
|
| 83 |
+
_obj._parent._register(_obj)
|
| 84 |
+
|
| 85 |
+
# Return to the normal chain
|
| 86 |
+
return _obj, args, kwargs
|
| 87 |
+
|
| 88 |
+
|
| 89 |
+
class Analyzer(with_metaclass(MetaAnalyzer, object)):
|
| 90 |
+
'''Analyzer base class. All analyzers are subclass of this one
|
| 91 |
+
|
| 92 |
+
An Analyzer instance operates in the frame of a strategy and provides an
|
| 93 |
+
analysis for that strategy.
|
| 94 |
+
|
| 95 |
+
Automagically set member attributes:
|
| 96 |
+
|
| 97 |
+
- ``self.strategy`` (giving access to the *strategy* and anything
|
| 98 |
+
accessible from it)
|
| 99 |
+
|
| 100 |
+
- ``self.datas[x]`` giving access to the array of data feeds present in
|
| 101 |
+
the the system, which could also be accessed via the strategy reference
|
| 102 |
+
|
| 103 |
+
- ``self.data``, giving access to ``self.datas[0]``
|
| 104 |
+
|
| 105 |
+
- ``self.dataX`` -> ``self.datas[X]``
|
| 106 |
+
|
| 107 |
+
- ``self.dataX_Y`` -> ``self.datas[X].lines[Y]``
|
| 108 |
+
|
| 109 |
+
- ``self.dataX_name`` -> ``self.datas[X].name``
|
| 110 |
+
|
| 111 |
+
- ``self.data_name`` -> ``self.datas[0].name``
|
| 112 |
+
|
| 113 |
+
- ``self.data_Y`` -> ``self.datas[0].lines[Y]``
|
| 114 |
+
|
| 115 |
+
This is not a *Lines* object, but the methods and operation follow the same
|
| 116 |
+
design
|
| 117 |
+
|
| 118 |
+
- ``__init__`` during instantiation and initial setup
|
| 119 |
+
|
| 120 |
+
- ``start`` / ``stop`` to signal the begin and end of operations
|
| 121 |
+
|
| 122 |
+
- ``prenext`` / ``nextstart`` / ``next`` family of methods that follow
|
| 123 |
+
the calls made to the same methods in the strategy
|
| 124 |
+
|
| 125 |
+
- ``notify_trade`` / ``notify_order`` / ``notify_cashvalue`` /
|
| 126 |
+
``notify_fund`` which receive the same notifications as the equivalent
|
| 127 |
+
methods of the strategy
|
| 128 |
+
|
| 129 |
+
The mode of operation is open and no pattern is preferred. As such the
|
| 130 |
+
analysis can be generated with the ``next`` calls, at the end of operations
|
| 131 |
+
during ``stop`` and even with a single method like ``notify_trade``
|
| 132 |
+
|
| 133 |
+
The important thing is to override ``get_analysis`` to return a *dict-like*
|
| 134 |
+
object containing the results of the analysis (the actual format is
|
| 135 |
+
implementation dependent)
|
| 136 |
+
|
| 137 |
+
'''
|
| 138 |
+
csv = True
|
| 139 |
+
|
| 140 |
+
def __len__(self):
|
| 141 |
+
'''Support for invoking ``len`` on analyzers by actually returning the
|
| 142 |
+
current length of the strategy the analyzer operates on'''
|
| 143 |
+
return len(self.strategy)
|
| 144 |
+
|
| 145 |
+
def _register(self, child):
|
| 146 |
+
self._children.append(child)
|
| 147 |
+
|
| 148 |
+
def _prenext(self):
|
| 149 |
+
for child in self._children:
|
| 150 |
+
child._prenext()
|
| 151 |
+
|
| 152 |
+
self.prenext()
|
| 153 |
+
|
| 154 |
+
def _notify_cashvalue(self, cash, value):
|
| 155 |
+
for child in self._children:
|
| 156 |
+
child._notify_cashvalue(cash, value)
|
| 157 |
+
|
| 158 |
+
self.notify_cashvalue(cash, value)
|
| 159 |
+
|
| 160 |
+
def _notify_fund(self, cash, value, fundvalue, shares):
|
| 161 |
+
for child in self._children:
|
| 162 |
+
child._notify_fund(cash, value, fundvalue, shares)
|
| 163 |
+
|
| 164 |
+
self.notify_fund(cash, value, fundvalue, shares)
|
| 165 |
+
|
| 166 |
+
def _notify_trade(self, trade):
|
| 167 |
+
for child in self._children:
|
| 168 |
+
child._notify_trade(trade)
|
| 169 |
+
|
| 170 |
+
self.notify_trade(trade)
|
| 171 |
+
|
| 172 |
+
def _notify_order(self, order):
|
| 173 |
+
for child in self._children:
|
| 174 |
+
child._notify_order(order)
|
| 175 |
+
|
| 176 |
+
self.notify_order(order)
|
| 177 |
+
|
| 178 |
+
def _nextstart(self):
|
| 179 |
+
for child in self._children:
|
| 180 |
+
child._nextstart()
|
| 181 |
+
|
| 182 |
+
self.nextstart()
|
| 183 |
+
|
| 184 |
+
def _next(self):
|
| 185 |
+
for child in self._children:
|
| 186 |
+
child._next()
|
| 187 |
+
|
| 188 |
+
self.next()
|
| 189 |
+
|
| 190 |
+
def _start(self):
|
| 191 |
+
for child in self._children:
|
| 192 |
+
child._start()
|
| 193 |
+
|
| 194 |
+
self.start()
|
| 195 |
+
|
| 196 |
+
def _stop(self):
|
| 197 |
+
for child in self._children:
|
| 198 |
+
child._stop()
|
| 199 |
+
|
| 200 |
+
self.stop()
|
| 201 |
+
|
| 202 |
+
def notify_cashvalue(self, cash, value):
|
| 203 |
+
'''Receives the cash/value notification before each next cycle'''
|
| 204 |
+
pass
|
| 205 |
+
|
| 206 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 207 |
+
'''Receives the current cash, value, fundvalue and fund shares'''
|
| 208 |
+
pass
|
| 209 |
+
|
| 210 |
+
def notify_order(self, order):
|
| 211 |
+
'''Receives order notifications before each next cycle'''
|
| 212 |
+
pass
|
| 213 |
+
|
| 214 |
+
def notify_trade(self, trade):
|
| 215 |
+
'''Receives trade notifications before each next cycle'''
|
| 216 |
+
pass
|
| 217 |
+
|
| 218 |
+
def next(self):
|
| 219 |
+
'''Invoked for each next invocation of the strategy, once the minum
|
| 220 |
+
preiod of the strategy has been reached'''
|
| 221 |
+
pass
|
| 222 |
+
|
| 223 |
+
def prenext(self):
|
| 224 |
+
'''Invoked for each prenext invocation of the strategy, until the minimum
|
| 225 |
+
period of the strategy has been reached
|
| 226 |
+
|
| 227 |
+
The default behavior for an analyzer is to invoke ``next``
|
| 228 |
+
'''
|
| 229 |
+
self.next()
|
| 230 |
+
|
| 231 |
+
def nextstart(self):
|
| 232 |
+
'''Invoked exactly once for the nextstart invocation of the strategy,
|
| 233 |
+
when the minimum period has been first reached
|
| 234 |
+
'''
|
| 235 |
+
self.next()
|
| 236 |
+
|
| 237 |
+
def start(self):
|
| 238 |
+
'''Invoked to indicate the start of operations, giving the analyzer
|
| 239 |
+
time to setup up needed things'''
|
| 240 |
+
pass
|
| 241 |
+
|
| 242 |
+
def stop(self):
|
| 243 |
+
'''Invoked to indicate the end of operations, giving the analyzer
|
| 244 |
+
time to shut down needed things'''
|
| 245 |
+
pass
|
| 246 |
+
|
| 247 |
+
def create_analysis(self):
|
| 248 |
+
'''Meant to be overriden by subclasses. Gives a chance to create the
|
| 249 |
+
structures that hold the analysis.
|
| 250 |
+
|
| 251 |
+
The default behaviour is to create a ``OrderedDict`` named ``rets``
|
| 252 |
+
'''
|
| 253 |
+
self.rets = OrderedDict()
|
| 254 |
+
|
| 255 |
+
def get_analysis(self):
|
| 256 |
+
'''Returns a *dict-like* object with the results of the analysis
|
| 257 |
+
|
| 258 |
+
The keys and format of analysis results in the dictionary is
|
| 259 |
+
implementation dependent.
|
| 260 |
+
|
| 261 |
+
It is not even enforced that the result is a *dict-like object*, just
|
| 262 |
+
the convention
|
| 263 |
+
|
| 264 |
+
The default implementation returns the default OrderedDict ``rets``
|
| 265 |
+
created by the default ``create_analysis`` method
|
| 266 |
+
|
| 267 |
+
'''
|
| 268 |
+
return self.rets
|
| 269 |
+
|
| 270 |
+
def print(self, *args, **kwargs):
|
| 271 |
+
'''Prints the results returned by ``get_analysis`` via a standard
|
| 272 |
+
``Writerfile`` object, which defaults to writing things to standard
|
| 273 |
+
output
|
| 274 |
+
'''
|
| 275 |
+
writer = bt.WriterFile(*args, **kwargs)
|
| 276 |
+
writer.start()
|
| 277 |
+
pdct = dict()
|
| 278 |
+
pdct[self.__class__.__name__] = self.get_analysis()
|
| 279 |
+
writer.writedict(pdct)
|
| 280 |
+
writer.stop()
|
| 281 |
+
|
| 282 |
+
def pprint(self, *args, **kwargs):
|
| 283 |
+
'''Prints the results returned by ``get_analysis`` using the pretty
|
| 284 |
+
print Python module (*pprint*)
|
| 285 |
+
'''
|
| 286 |
+
pp.pprint(self.get_analysis(), *args, **kwargs)
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
class MetaTimeFrameAnalyzerBase(Analyzer.__class__):
|
| 290 |
+
def __new__(meta, name, bases, dct):
|
| 291 |
+
# Hack to support original method name
|
| 292 |
+
if '_on_dt_over' in dct:
|
| 293 |
+
dct['on_dt_over'] = dct.pop('_on_dt_over') # rename method
|
| 294 |
+
|
| 295 |
+
return super(MetaTimeFrameAnalyzerBase, meta).__new__(meta, name,
|
| 296 |
+
bases, dct)
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
class TimeFrameAnalyzerBase(with_metaclass(MetaTimeFrameAnalyzerBase,
|
| 300 |
+
Analyzer)):
|
| 301 |
+
params = (
|
| 302 |
+
('timeframe', None),
|
| 303 |
+
('compression', None),
|
| 304 |
+
('_doprenext', True),
|
| 305 |
+
)
|
| 306 |
+
|
| 307 |
+
def _start(self):
|
| 308 |
+
# Override to add specific attributes
|
| 309 |
+
self.timeframe = self.p.timeframe or self.data._timeframe
|
| 310 |
+
self.compression = self.p.compression or self.data._compression
|
| 311 |
+
|
| 312 |
+
self.dtcmp, self.dtkey = self._get_dt_cmpkey(datetime.datetime.min)
|
| 313 |
+
super(TimeFrameAnalyzerBase, self)._start()
|
| 314 |
+
|
| 315 |
+
def _prenext(self):
|
| 316 |
+
for child in self._children:
|
| 317 |
+
child._prenext()
|
| 318 |
+
|
| 319 |
+
if self._dt_over():
|
| 320 |
+
self.on_dt_over()
|
| 321 |
+
|
| 322 |
+
if self.p._doprenext:
|
| 323 |
+
self.prenext()
|
| 324 |
+
|
| 325 |
+
def _nextstart(self):
|
| 326 |
+
for child in self._children:
|
| 327 |
+
child._nextstart()
|
| 328 |
+
|
| 329 |
+
if self._dt_over() or not self.p._doprenext: # exec if no prenext
|
| 330 |
+
self.on_dt_over()
|
| 331 |
+
|
| 332 |
+
self.nextstart()
|
| 333 |
+
|
| 334 |
+
def _next(self):
|
| 335 |
+
for child in self._children:
|
| 336 |
+
child._next()
|
| 337 |
+
|
| 338 |
+
if self._dt_over():
|
| 339 |
+
self.on_dt_over()
|
| 340 |
+
|
| 341 |
+
self.next()
|
| 342 |
+
|
| 343 |
+
def on_dt_over(self):
|
| 344 |
+
pass
|
| 345 |
+
|
| 346 |
+
def _dt_over(self):
|
| 347 |
+
if self.timeframe == TimeFrame.NoTimeFrame:
|
| 348 |
+
dtcmp, dtkey = MAXINT, datetime.datetime.max
|
| 349 |
+
else:
|
| 350 |
+
# With >= 1.9.x the system datetime is in the strategy
|
| 351 |
+
dt = self.strategy.datetime.datetime()
|
| 352 |
+
dtcmp, dtkey = self._get_dt_cmpkey(dt)
|
| 353 |
+
|
| 354 |
+
if self.dtcmp is None or dtcmp > self.dtcmp:
|
| 355 |
+
self.dtkey, self.dtkey1 = dtkey, self.dtkey
|
| 356 |
+
self.dtcmp, self.dtcmp1 = dtcmp, self.dtcmp
|
| 357 |
+
return True
|
| 358 |
+
|
| 359 |
+
return False
|
| 360 |
+
|
| 361 |
+
def _get_dt_cmpkey(self, dt):
|
| 362 |
+
if self.timeframe == TimeFrame.NoTimeFrame:
|
| 363 |
+
return None, None
|
| 364 |
+
|
| 365 |
+
if self.timeframe == TimeFrame.Years:
|
| 366 |
+
dtcmp = dt.year
|
| 367 |
+
dtkey = datetime.date(dt.year, 12, 31)
|
| 368 |
+
|
| 369 |
+
elif self.timeframe == TimeFrame.Months:
|
| 370 |
+
dtcmp = dt.year * 100 + dt.month
|
| 371 |
+
_, lastday = calendar.monthrange(dt.year, dt.month)
|
| 372 |
+
dtkey = datetime.datetime(dt.year, dt.month, lastday)
|
| 373 |
+
|
| 374 |
+
elif self.timeframe == TimeFrame.Weeks:
|
| 375 |
+
isoyear, isoweek, isoweekday = dt.isocalendar()
|
| 376 |
+
dtcmp = isoyear * 100 + isoweek
|
| 377 |
+
sunday = dt + datetime.timedelta(days=7 - isoweekday)
|
| 378 |
+
dtkey = datetime.datetime(sunday.year, sunday.month, sunday.day)
|
| 379 |
+
|
| 380 |
+
elif self.timeframe == TimeFrame.Days:
|
| 381 |
+
dtcmp = dt.year * 10000 + dt.month * 100 + dt.day
|
| 382 |
+
dtkey = datetime.datetime(dt.year, dt.month, dt.day)
|
| 383 |
+
|
| 384 |
+
else:
|
| 385 |
+
dtcmp, dtkey = self._get_subday_cmpkey(dt)
|
| 386 |
+
|
| 387 |
+
return dtcmp, dtkey
|
| 388 |
+
|
| 389 |
+
def _get_subday_cmpkey(self, dt):
|
| 390 |
+
# Calculate intraday position
|
| 391 |
+
point = dt.hour * 60 + dt.minute
|
| 392 |
+
|
| 393 |
+
if self.timeframe < TimeFrame.Minutes:
|
| 394 |
+
point = point * 60 + dt.second
|
| 395 |
+
|
| 396 |
+
if self.timeframe < TimeFrame.Seconds:
|
| 397 |
+
point = point * 1e6 + dt.microsecond
|
| 398 |
+
|
| 399 |
+
# Apply compression to update point position (comp 5 -> 200 // 5)
|
| 400 |
+
point = point // self.compression
|
| 401 |
+
|
| 402 |
+
# Move to next boundary
|
| 403 |
+
point += 1
|
| 404 |
+
|
| 405 |
+
# Restore point to the timeframe units by de-applying compression
|
| 406 |
+
point *= self.compression
|
| 407 |
+
|
| 408 |
+
# Get hours, minutes, seconds and microseconds
|
| 409 |
+
if self.timeframe == TimeFrame.Minutes:
|
| 410 |
+
ph, pm = divmod(point, 60)
|
| 411 |
+
ps = 0
|
| 412 |
+
pus = 0
|
| 413 |
+
elif self.timeframe == TimeFrame.Seconds:
|
| 414 |
+
ph, pm = divmod(point, 60 * 60)
|
| 415 |
+
pm, ps = divmod(pm, 60)
|
| 416 |
+
pus = 0
|
| 417 |
+
elif self.timeframe == TimeFrame.MicroSeconds:
|
| 418 |
+
ph, pm = divmod(point, 60 * 60 * 1e6)
|
| 419 |
+
pm, psec = divmod(pm, 60 * 1e6)
|
| 420 |
+
ps, pus = divmod(psec, 1e6)
|
| 421 |
+
|
| 422 |
+
extradays = 0
|
| 423 |
+
if ph > 23: # went over midnight:
|
| 424 |
+
extradays = ph // 24
|
| 425 |
+
ph %= 24
|
| 426 |
+
|
| 427 |
+
# moving 1 minor unit to the left to be in the boundary
|
| 428 |
+
# pm -= self.timeframe == TimeFrame.Minutes
|
| 429 |
+
# ps -= self.timeframe == TimeFrame.Seconds
|
| 430 |
+
# pus -= self.timeframe == TimeFrame.MicroSeconds
|
| 431 |
+
|
| 432 |
+
tadjust = datetime.timedelta(
|
| 433 |
+
minutes=self.timeframe == TimeFrame.Minutes,
|
| 434 |
+
seconds=self.timeframe == TimeFrame.Seconds,
|
| 435 |
+
microseconds=self.timeframe == TimeFrame.MicroSeconds)
|
| 436 |
+
|
| 437 |
+
# Add extra day if present
|
| 438 |
+
if extradays:
|
| 439 |
+
dt += datetime.timedelta(days=extradays)
|
| 440 |
+
|
| 441 |
+
# Replace intraday parts with the calculated ones and update it
|
| 442 |
+
dtcmp = dt.replace(hour=ph, minute=pm, second=ps, microsecond=pus)
|
| 443 |
+
dtcmp -= tadjust
|
| 444 |
+
dtkey = dtcmp
|
| 445 |
+
|
| 446 |
+
return dtcmp, dtkey
|
backtrader/source/backtrader/analyzers/__init__.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
# The modules below should/must define __all__ with the objects wishes
|
| 25 |
+
# or prepend an "_" (underscore) to private classes/variables
|
| 26 |
+
|
| 27 |
+
from .annualreturn import *
|
| 28 |
+
from .drawdown import *
|
| 29 |
+
from .timereturn import *
|
| 30 |
+
from .sharpe import *
|
| 31 |
+
from .tradeanalyzer import *
|
| 32 |
+
from .sqn import *
|
| 33 |
+
from .leverage import *
|
| 34 |
+
from .positions import *
|
| 35 |
+
from .transactions import *
|
| 36 |
+
from .pyfolio import *
|
| 37 |
+
from .returns import *
|
| 38 |
+
from .vwr import *
|
| 39 |
+
|
| 40 |
+
from .logreturnsrolling import *
|
| 41 |
+
|
| 42 |
+
from .calmar import *
|
| 43 |
+
from .periodstats import *
|
backtrader/source/backtrader/analyzers/annualreturn.py
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from collections import OrderedDict
|
| 25 |
+
|
| 26 |
+
from backtrader.utils.py3 import range
|
| 27 |
+
from backtrader import Analyzer
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class AnnualReturn(Analyzer):
|
| 31 |
+
'''
|
| 32 |
+
This analyzer calculates the AnnualReturns by looking at the beginning
|
| 33 |
+
and end of the year
|
| 34 |
+
|
| 35 |
+
Params:
|
| 36 |
+
|
| 37 |
+
- (None)
|
| 38 |
+
|
| 39 |
+
Member Attributes:
|
| 40 |
+
|
| 41 |
+
- ``rets``: list of calculated annual returns
|
| 42 |
+
|
| 43 |
+
- ``ret``: dictionary (key: year) of annual returns
|
| 44 |
+
|
| 45 |
+
**get_analysis**:
|
| 46 |
+
|
| 47 |
+
- Returns a dictionary of annual returns (key: year)
|
| 48 |
+
'''
|
| 49 |
+
|
| 50 |
+
def stop(self):
|
| 51 |
+
# Must have stats.broker
|
| 52 |
+
cur_year = -1
|
| 53 |
+
|
| 54 |
+
value_start = 0.0
|
| 55 |
+
value_cur = 0.0
|
| 56 |
+
value_end = 0.0
|
| 57 |
+
|
| 58 |
+
self.rets = list()
|
| 59 |
+
self.ret = OrderedDict()
|
| 60 |
+
|
| 61 |
+
for i in range(len(self.data) - 1, -1, -1):
|
| 62 |
+
dt = self.data.datetime.date(-i)
|
| 63 |
+
value_cur = self.strategy.stats.broker.value[-i]
|
| 64 |
+
|
| 65 |
+
if dt.year > cur_year:
|
| 66 |
+
if cur_year >= 0:
|
| 67 |
+
annualret = (value_end / value_start) - 1.0
|
| 68 |
+
self.rets.append(annualret)
|
| 69 |
+
self.ret[cur_year] = annualret
|
| 70 |
+
|
| 71 |
+
# changing between real years, use last value as new start
|
| 72 |
+
value_start = value_end
|
| 73 |
+
else:
|
| 74 |
+
# No value set whatsoever, use the currently loaded value
|
| 75 |
+
value_start = value_cur
|
| 76 |
+
|
| 77 |
+
cur_year = dt.year
|
| 78 |
+
|
| 79 |
+
# No matter what, the last value is always the last loaded value
|
| 80 |
+
value_end = value_cur
|
| 81 |
+
|
| 82 |
+
if cur_year not in self.ret:
|
| 83 |
+
# finish calculating pending data
|
| 84 |
+
annualret = (value_end / value_start) - 1.0
|
| 85 |
+
self.rets.append(annualret)
|
| 86 |
+
self.ret[cur_year] = annualret
|
| 87 |
+
|
| 88 |
+
def get_analysis(self):
|
| 89 |
+
return self.ret
|
backtrader/source/backtrader/analyzers/calmar.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import backtrader as bt
|
| 25 |
+
from . import TimeDrawDown
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
__all__ = ['Calmar']
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Calmar(bt.TimeFrameAnalyzerBase):
|
| 32 |
+
'''This analyzer calculates the CalmarRatio
|
| 33 |
+
timeframe which can be different from the one used in the underlying data
|
| 34 |
+
Params:
|
| 35 |
+
|
| 36 |
+
- ``timeframe`` (default: ``None``)
|
| 37 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 38 |
+
used
|
| 39 |
+
|
| 40 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 41 |
+
time constraints
|
| 42 |
+
|
| 43 |
+
- ``compression`` (default: ``None``)
|
| 44 |
+
|
| 45 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 46 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 47 |
+
|
| 48 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 49 |
+
used
|
| 50 |
+
- *None*
|
| 51 |
+
|
| 52 |
+
- ``fund`` (default: ``None``)
|
| 53 |
+
|
| 54 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 55 |
+
be autodetected to decide if the returns are based on the total net
|
| 56 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 57 |
+
documentation
|
| 58 |
+
|
| 59 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 60 |
+
|
| 61 |
+
See also:
|
| 62 |
+
|
| 63 |
+
- https://en.wikipedia.org/wiki/Calmar_ratio
|
| 64 |
+
|
| 65 |
+
Methods:
|
| 66 |
+
- ``get_analysis``
|
| 67 |
+
|
| 68 |
+
Returns a OrderedDict with a key for the time period and the
|
| 69 |
+
corresponding rolling Calmar ratio
|
| 70 |
+
|
| 71 |
+
Attributes:
|
| 72 |
+
- ``calmar`` the latest calculated calmar ratio
|
| 73 |
+
'''
|
| 74 |
+
|
| 75 |
+
packages = ('collections', 'math',)
|
| 76 |
+
|
| 77 |
+
params = (
|
| 78 |
+
('timeframe', bt.TimeFrame.Months), # default in calmar
|
| 79 |
+
('period', 36),
|
| 80 |
+
('fund', None),
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
def __init__(self):
|
| 84 |
+
self._maxdd = TimeDrawDown(timeframe=self.p.timeframe,
|
| 85 |
+
compression=self.p.compression)
|
| 86 |
+
|
| 87 |
+
def start(self):
|
| 88 |
+
self._mdd = float('-inf')
|
| 89 |
+
self._values = collections.deque([float('Nan')] * self.p.period,
|
| 90 |
+
maxlen=self.p.period)
|
| 91 |
+
if self.p.fund is None:
|
| 92 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 93 |
+
else:
|
| 94 |
+
self._fundmode = self.p.fund
|
| 95 |
+
|
| 96 |
+
if not self._fundmode:
|
| 97 |
+
self._values.append(self.strategy.broker.getvalue())
|
| 98 |
+
else:
|
| 99 |
+
self._values.append(self.strategy.broker.fundvalue)
|
| 100 |
+
|
| 101 |
+
def on_dt_over(self):
|
| 102 |
+
self._mdd = max(self._mdd, self._maxdd.maxdd)
|
| 103 |
+
if not self._fundmode:
|
| 104 |
+
self._values.append(self.strategy.broker.getvalue())
|
| 105 |
+
else:
|
| 106 |
+
self._values.append(self.strategy.broker.fundvalue)
|
| 107 |
+
rann = math.log(self._values[-1] / self._values[0]) / len(self._values)
|
| 108 |
+
self.calmar = calmar = rann / (self._mdd or float('Inf'))
|
| 109 |
+
|
| 110 |
+
self.rets[self.dtkey] = calmar
|
| 111 |
+
|
| 112 |
+
def stop(self):
|
| 113 |
+
self.on_dt_over() # update last values
|
backtrader/source/backtrader/analyzers/drawdown.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import backtrader as bt
|
| 25 |
+
from backtrader.utils import AutoOrderedDict
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
__all__ = ['DrawDown', 'TimeDrawDown']
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DrawDown(bt.Analyzer):
|
| 32 |
+
'''This analyzer calculates trading system drawdowns stats such as drawdown
|
| 33 |
+
values in %s and in dollars, max drawdown in %s and in dollars, drawdown
|
| 34 |
+
length and drawdown max length
|
| 35 |
+
|
| 36 |
+
Params:
|
| 37 |
+
|
| 38 |
+
- ``fund`` (default: ``None``)
|
| 39 |
+
|
| 40 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 41 |
+
be autodetected to decide if the returns are based on the total net
|
| 42 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 43 |
+
documentation
|
| 44 |
+
|
| 45 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 46 |
+
|
| 47 |
+
Methods:
|
| 48 |
+
|
| 49 |
+
- ``get_analysis``
|
| 50 |
+
|
| 51 |
+
Returns a dictionary (with . notation support and subdctionaries) with
|
| 52 |
+
drawdown stats as values, the following keys/attributes are available:
|
| 53 |
+
|
| 54 |
+
- ``drawdown`` - drawdown value in 0.xx %
|
| 55 |
+
- ``moneydown`` - drawdown value in monetary units
|
| 56 |
+
- ``len`` - drawdown length
|
| 57 |
+
|
| 58 |
+
- ``max.drawdown`` - max drawdown value in 0.xx %
|
| 59 |
+
- ``max.moneydown`` - max drawdown value in monetary units
|
| 60 |
+
- ``max.len`` - max drawdown length
|
| 61 |
+
'''
|
| 62 |
+
|
| 63 |
+
params = (
|
| 64 |
+
('fund', None),
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
def start(self):
|
| 68 |
+
super(DrawDown, self).start()
|
| 69 |
+
if self.p.fund is None:
|
| 70 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 71 |
+
else:
|
| 72 |
+
self._fundmode = self.p.fund
|
| 73 |
+
|
| 74 |
+
def create_analysis(self):
|
| 75 |
+
self.rets = AutoOrderedDict() # dict with . notation
|
| 76 |
+
|
| 77 |
+
self.rets.len = 0
|
| 78 |
+
self.rets.drawdown = 0.0
|
| 79 |
+
self.rets.moneydown = 0.0
|
| 80 |
+
|
| 81 |
+
self.rets.max.len = 0.0
|
| 82 |
+
self.rets.max.drawdown = 0.0
|
| 83 |
+
self.rets.max.moneydown = 0.0
|
| 84 |
+
|
| 85 |
+
self._maxvalue = float('-inf') # any value will outdo it
|
| 86 |
+
|
| 87 |
+
def stop(self):
|
| 88 |
+
self.rets._close() # . notation cannot create more keys
|
| 89 |
+
|
| 90 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 91 |
+
if not self._fundmode:
|
| 92 |
+
self._value = value # record current value
|
| 93 |
+
self._maxvalue = max(self._maxvalue, value) # update peak value
|
| 94 |
+
else:
|
| 95 |
+
self._value = fundvalue # record current value
|
| 96 |
+
self._maxvalue = max(self._maxvalue, fundvalue) # update peak
|
| 97 |
+
|
| 98 |
+
def next(self):
|
| 99 |
+
r = self.rets
|
| 100 |
+
|
| 101 |
+
# calculate current drawdown values
|
| 102 |
+
r.moneydown = moneydown = self._maxvalue - self._value
|
| 103 |
+
r.drawdown = drawdown = 100.0 * moneydown / self._maxvalue
|
| 104 |
+
|
| 105 |
+
# maxximum drawdown values
|
| 106 |
+
r.max.moneydown = max(r.max.moneydown, moneydown)
|
| 107 |
+
r.max.drawdown = maxdrawdown = max(r.max.drawdown, drawdown)
|
| 108 |
+
|
| 109 |
+
r.len = r.len + 1 if drawdown else 0
|
| 110 |
+
r.max.len = max(r.max.len, r.len)
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
class TimeDrawDown(bt.TimeFrameAnalyzerBase):
|
| 114 |
+
'''This analyzer calculates trading system drawdowns on the chosen
|
| 115 |
+
timeframe which can be different from the one used in the underlying data
|
| 116 |
+
Params:
|
| 117 |
+
|
| 118 |
+
- ``timeframe`` (default: ``None``)
|
| 119 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 120 |
+
used
|
| 121 |
+
|
| 122 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 123 |
+
time constraints
|
| 124 |
+
|
| 125 |
+
- ``compression`` (default: ``None``)
|
| 126 |
+
|
| 127 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 128 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 129 |
+
|
| 130 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 131 |
+
used
|
| 132 |
+
- *None*
|
| 133 |
+
|
| 134 |
+
- ``fund`` (default: ``None``)
|
| 135 |
+
|
| 136 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 137 |
+
be autodetected to decide if the returns are based on the total net
|
| 138 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 139 |
+
documentation
|
| 140 |
+
|
| 141 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 142 |
+
|
| 143 |
+
Methods:
|
| 144 |
+
|
| 145 |
+
- ``get_analysis``
|
| 146 |
+
|
| 147 |
+
Returns a dictionary (with . notation support and subdctionaries) with
|
| 148 |
+
drawdown stats as values, the following keys/attributes are available:
|
| 149 |
+
|
| 150 |
+
- ``drawdown`` - drawdown value in 0.xx %
|
| 151 |
+
- ``maxdrawdown`` - drawdown value in monetary units
|
| 152 |
+
- ``maxdrawdownperiod`` - drawdown length
|
| 153 |
+
|
| 154 |
+
- Those are available during runs as attributes
|
| 155 |
+
- ``dd``
|
| 156 |
+
- ``maxdd``
|
| 157 |
+
- ``maxddlen``
|
| 158 |
+
'''
|
| 159 |
+
|
| 160 |
+
params = (
|
| 161 |
+
('fund', None),
|
| 162 |
+
)
|
| 163 |
+
|
| 164 |
+
def start(self):
|
| 165 |
+
super(TimeDrawDown, self).start()
|
| 166 |
+
if self.p.fund is None:
|
| 167 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 168 |
+
else:
|
| 169 |
+
self._fundmode = self.p.fund
|
| 170 |
+
self.dd = 0.0
|
| 171 |
+
self.maxdd = 0.0
|
| 172 |
+
self.maxddlen = 0
|
| 173 |
+
self.peak = float('-inf')
|
| 174 |
+
self.ddlen = 0
|
| 175 |
+
|
| 176 |
+
def on_dt_over(self):
|
| 177 |
+
if not self._fundmode:
|
| 178 |
+
value = self.strategy.broker.getvalue()
|
| 179 |
+
else:
|
| 180 |
+
value = self.strategy.broker.fundvalue
|
| 181 |
+
|
| 182 |
+
# update the maximum seen peak
|
| 183 |
+
if value > self.peak:
|
| 184 |
+
self.peak = value
|
| 185 |
+
self.ddlen = 0 # start of streak
|
| 186 |
+
|
| 187 |
+
# calculate the current drawdown
|
| 188 |
+
self.dd = dd = 100.0 * (self.peak - value) / self.peak
|
| 189 |
+
self.ddlen += bool(dd) # if peak == value -> dd = 0
|
| 190 |
+
|
| 191 |
+
# update the maxdrawdown if needed
|
| 192 |
+
self.maxdd = max(self.maxdd, dd)
|
| 193 |
+
self.maxddlen = max(self.maxddlen, self.ddlen)
|
| 194 |
+
|
| 195 |
+
def stop(self):
|
| 196 |
+
self.rets['maxdrawdown'] = self.maxdd
|
| 197 |
+
self.rets['maxdrawdownperiod'] = self.maxddlen
|
backtrader/source/backtrader/analyzers/leverage.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import backtrader as bt
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class GrossLeverage(bt.Analyzer):
|
| 28 |
+
'''This analyzer calculates the Gross Leverage of the current strategy
|
| 29 |
+
on a timeframe basis
|
| 30 |
+
|
| 31 |
+
Params:
|
| 32 |
+
|
| 33 |
+
- ``fund`` (default: ``None``)
|
| 34 |
+
|
| 35 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 36 |
+
be autodetected to decide if the returns are based on the total net
|
| 37 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 38 |
+
documentation
|
| 39 |
+
|
| 40 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 41 |
+
|
| 42 |
+
Methods:
|
| 43 |
+
|
| 44 |
+
- get_analysis
|
| 45 |
+
|
| 46 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 47 |
+
each return as keys
|
| 48 |
+
'''
|
| 49 |
+
|
| 50 |
+
params = (
|
| 51 |
+
('fund', None),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def start(self):
|
| 55 |
+
if self.p.fund is None:
|
| 56 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 57 |
+
else:
|
| 58 |
+
self._fundmode = self.p.fund
|
| 59 |
+
|
| 60 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 61 |
+
self._cash = cash
|
| 62 |
+
if not self._fundmode:
|
| 63 |
+
self._value = value
|
| 64 |
+
else:
|
| 65 |
+
self._value = fundvalue
|
| 66 |
+
|
| 67 |
+
def next(self):
|
| 68 |
+
# Updates the leverage for "dtkey" (see base class) for each cycle
|
| 69 |
+
# 0.0 if 100% in cash, 1.0 if no short selling and fully invested
|
| 70 |
+
lev = (self._value - self._cash) / self._value
|
| 71 |
+
self.rets[self.data0.datetime.datetime()] = lev
|
backtrader/source/backtrader/analyzers/logreturnsrolling.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
import math
|
| 26 |
+
|
| 27 |
+
import backtrader as bt
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
__all__ = ['LogReturnsRolling']
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class LogReturnsRolling(bt.TimeFrameAnalyzerBase):
|
| 34 |
+
'''This analyzer calculates rolling returns for a given timeframe and
|
| 35 |
+
compression
|
| 36 |
+
|
| 37 |
+
Params:
|
| 38 |
+
|
| 39 |
+
- ``timeframe`` (default: ``None``)
|
| 40 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 41 |
+
used
|
| 42 |
+
|
| 43 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 44 |
+
time constraints
|
| 45 |
+
|
| 46 |
+
- ``compression`` (default: ``None``)
|
| 47 |
+
|
| 48 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 49 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 50 |
+
|
| 51 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 52 |
+
used
|
| 53 |
+
|
| 54 |
+
- ``data`` (default: ``None``)
|
| 55 |
+
|
| 56 |
+
Reference asset to track instead of the portfolio value.
|
| 57 |
+
|
| 58 |
+
.. note:: this data must have been added to a ``cerebro`` instance with
|
| 59 |
+
``addata``, ``resampledata`` or ``replaydata``
|
| 60 |
+
|
| 61 |
+
- ``firstopen`` (default: ``True``)
|
| 62 |
+
|
| 63 |
+
When tracking the returns of a ``data`` the following is done when
|
| 64 |
+
crossing a timeframe boundary, for example ``Years``:
|
| 65 |
+
|
| 66 |
+
- Last ``close`` of previous year is used as the reference price to
|
| 67 |
+
see the return in the current year
|
| 68 |
+
|
| 69 |
+
The problem is the 1st calculation, because the data has** no
|
| 70 |
+
previous** closing price. As such and when this parameter is ``True``
|
| 71 |
+
the *opening* price will be used for the 1st calculation.
|
| 72 |
+
|
| 73 |
+
This requires the data feed to have an ``open`` price (for ``close``
|
| 74 |
+
the standard [0] notation will be used without reference to a field
|
| 75 |
+
price)
|
| 76 |
+
|
| 77 |
+
Else the initial close will be used.
|
| 78 |
+
|
| 79 |
+
- ``fund`` (default: ``None``)
|
| 80 |
+
|
| 81 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 82 |
+
be autodetected to decide if the returns are based on the total net
|
| 83 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 84 |
+
documentation
|
| 85 |
+
|
| 86 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 87 |
+
|
| 88 |
+
Methods:
|
| 89 |
+
|
| 90 |
+
- get_analysis
|
| 91 |
+
|
| 92 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 93 |
+
each return as keys
|
| 94 |
+
'''
|
| 95 |
+
|
| 96 |
+
params = (
|
| 97 |
+
('data', None),
|
| 98 |
+
('firstopen', True),
|
| 99 |
+
('fund', None),
|
| 100 |
+
)
|
| 101 |
+
|
| 102 |
+
def start(self):
|
| 103 |
+
super(LogReturnsRolling, self).start()
|
| 104 |
+
if self.p.fund is None:
|
| 105 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 106 |
+
else:
|
| 107 |
+
self._fundmode = self.p.fund
|
| 108 |
+
|
| 109 |
+
self._values = collections.deque([float('Nan')] * self.compression,
|
| 110 |
+
maxlen=self.compression)
|
| 111 |
+
|
| 112 |
+
if self.p.data is None:
|
| 113 |
+
# keep the initial portfolio value if not tracing a data
|
| 114 |
+
if not self._fundmode:
|
| 115 |
+
self._lastvalue = self.strategy.broker.getvalue()
|
| 116 |
+
else:
|
| 117 |
+
self._lastvalue = self.strategy.broker.fundvalue
|
| 118 |
+
|
| 119 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 120 |
+
if not self._fundmode:
|
| 121 |
+
self._value = value if self.p.data is None else self.p.data[0]
|
| 122 |
+
else:
|
| 123 |
+
self._value = fundvalue if self.p.data is None else self.p.data[0]
|
| 124 |
+
|
| 125 |
+
def _on_dt_over(self):
|
| 126 |
+
# next is called in a new timeframe period
|
| 127 |
+
if self.p.data is None or len(self.p.data) > 1:
|
| 128 |
+
# Not tracking a data feed or data feed has data already
|
| 129 |
+
vst = self._lastvalue # update value_start to last
|
| 130 |
+
else:
|
| 131 |
+
# The 1st tick has no previous reference, use the opening price
|
| 132 |
+
vst = self.p.data.open[0] if self.p.firstopen else self.p.data[0]
|
| 133 |
+
|
| 134 |
+
self._values.append(vst) # push values backwards (and out)
|
| 135 |
+
|
| 136 |
+
def next(self):
|
| 137 |
+
# Calculate the return
|
| 138 |
+
super(LogReturnsRolling, self).next()
|
| 139 |
+
self.rets[self.dtkey] = math.log(self._value / self._values[0])
|
| 140 |
+
self._lastvalue = self._value # keep last value
|
backtrader/source/backtrader/analyzers/periodstats.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
import backtrader as bt
|
| 26 |
+
from backtrader.utils.py3 import itervalues
|
| 27 |
+
from backtrader.mathsupport import average, standarddev
|
| 28 |
+
from . import TimeReturn
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
__all__ = ['PeriodStats']
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
class PeriodStats(bt.Analyzer):
|
| 35 |
+
'''Calculates basic statistics for given timeframe
|
| 36 |
+
|
| 37 |
+
Params:
|
| 38 |
+
|
| 39 |
+
- ``timeframe`` (default: ``Years``)
|
| 40 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 41 |
+
used
|
| 42 |
+
|
| 43 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 44 |
+
time constraints
|
| 45 |
+
|
| 46 |
+
- ``compression`` (default: ``1``)
|
| 47 |
+
|
| 48 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 49 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 50 |
+
|
| 51 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 52 |
+
used
|
| 53 |
+
|
| 54 |
+
- ``fund`` (default: ``None``)
|
| 55 |
+
|
| 56 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 57 |
+
be autodetected to decide if the returns are based on the total net
|
| 58 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 59 |
+
documentation
|
| 60 |
+
|
| 61 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
``get_analysis`` returns a dictionary containing the keys:
|
| 65 |
+
|
| 66 |
+
- ``average``
|
| 67 |
+
- ``stddev``
|
| 68 |
+
- ``positive``
|
| 69 |
+
- ``negative``
|
| 70 |
+
- ``nochange``
|
| 71 |
+
- ``best``
|
| 72 |
+
- ``worst``
|
| 73 |
+
|
| 74 |
+
If the parameter ``zeroispos`` is set to ``True``, periods with no change
|
| 75 |
+
will be counted as positive
|
| 76 |
+
'''
|
| 77 |
+
|
| 78 |
+
params = (
|
| 79 |
+
('timeframe', bt.TimeFrame.Years),
|
| 80 |
+
('compression', 1),
|
| 81 |
+
('zeroispos', False),
|
| 82 |
+
('fund', None),
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
def __init__(self):
|
| 86 |
+
self._tr = TimeReturn(timeframe=self.p.timeframe,
|
| 87 |
+
compression=self.p.compression, fund=self.p.fund)
|
| 88 |
+
|
| 89 |
+
def stop(self):
|
| 90 |
+
trets = self._tr.get_analysis() # dict key = date, value = ret
|
| 91 |
+
pos = nul = neg = 0
|
| 92 |
+
trets = list(itervalues(trets))
|
| 93 |
+
for tret in trets:
|
| 94 |
+
if tret > 0.0:
|
| 95 |
+
pos += 1
|
| 96 |
+
elif tret < 0.0:
|
| 97 |
+
neg += 1
|
| 98 |
+
else:
|
| 99 |
+
if self.p.zeroispos:
|
| 100 |
+
pos += tret == 0.0
|
| 101 |
+
else:
|
| 102 |
+
nul += tret == 0.0
|
| 103 |
+
|
| 104 |
+
self.rets['average'] = avg = average(trets)
|
| 105 |
+
self.rets['stddev'] = standarddev(trets, avg)
|
| 106 |
+
|
| 107 |
+
self.rets['positive'] = pos
|
| 108 |
+
self.rets['negative'] = neg
|
| 109 |
+
self.rets['nochange'] = nul
|
| 110 |
+
|
| 111 |
+
self.rets['best'] = max(trets)
|
| 112 |
+
self.rets['worst'] = min(trets)
|
backtrader/source/backtrader/analyzers/positions.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
import backtrader as bt
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class PositionsValue(bt.Analyzer):
|
| 29 |
+
'''This analyzer reports the value of the positions of the current set of
|
| 30 |
+
datas
|
| 31 |
+
|
| 32 |
+
Params:
|
| 33 |
+
|
| 34 |
+
- timeframe (default: ``None``)
|
| 35 |
+
If ``None`` then the timeframe of the 1st data of the system will be
|
| 36 |
+
used
|
| 37 |
+
|
| 38 |
+
- compression (default: ``None``)
|
| 39 |
+
|
| 40 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 41 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 42 |
+
|
| 43 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 44 |
+
used
|
| 45 |
+
|
| 46 |
+
- headers (default: ``False``)
|
| 47 |
+
|
| 48 |
+
Add an initial key to the dictionary holding the results with the names
|
| 49 |
+
of the datas ('Datetime' as key
|
| 50 |
+
|
| 51 |
+
- cash (default: ``False``)
|
| 52 |
+
|
| 53 |
+
Include the actual cash as an extra position (for the header 'cash'
|
| 54 |
+
will be used as name)
|
| 55 |
+
|
| 56 |
+
Methods:
|
| 57 |
+
|
| 58 |
+
- get_analysis
|
| 59 |
+
|
| 60 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 61 |
+
each return as keys
|
| 62 |
+
'''
|
| 63 |
+
params = (
|
| 64 |
+
('headers', False),
|
| 65 |
+
('cash', False),
|
| 66 |
+
)
|
| 67 |
+
|
| 68 |
+
def start(self):
|
| 69 |
+
if self.p.headers:
|
| 70 |
+
headers = [d._name or 'Data%d' % i
|
| 71 |
+
for i, d in enumerate(self.datas)]
|
| 72 |
+
self.rets['Datetime'] = headers + ['cash'] * self.p.cash
|
| 73 |
+
|
| 74 |
+
tf = min(d._timeframe for d in self.datas)
|
| 75 |
+
self._usedate = tf >= bt.TimeFrame.Days
|
| 76 |
+
|
| 77 |
+
def next(self):
|
| 78 |
+
pvals = [self.strategy.broker.get_value([d]) for d in self.datas]
|
| 79 |
+
if self.p.cash:
|
| 80 |
+
pvals.append(self.strategy.broker.get_cash())
|
| 81 |
+
|
| 82 |
+
if self._usedate:
|
| 83 |
+
self.rets[self.strategy.datetime.date()] = pvals
|
| 84 |
+
else:
|
| 85 |
+
self.rets[self.strategy.datetime.datetime()] = pvals
|
backtrader/source/backtrader/analyzers/pyfolio.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
import collections
|
| 26 |
+
|
| 27 |
+
import backtrader as bt
|
| 28 |
+
from backtrader.utils.py3 import items, iteritems
|
| 29 |
+
|
| 30 |
+
from . import TimeReturn, PositionsValue, Transactions, GrossLeverage
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class PyFolio(bt.Analyzer):
|
| 34 |
+
'''This analyzer uses 4 children analyzers to collect data and transforms it
|
| 35 |
+
in to a data set compatible with ``pyfolio``
|
| 36 |
+
|
| 37 |
+
Children Analyzer
|
| 38 |
+
|
| 39 |
+
- ``TimeReturn``
|
| 40 |
+
|
| 41 |
+
Used to calculate the returns of the global portfolio value
|
| 42 |
+
|
| 43 |
+
- ``PositionsValue``
|
| 44 |
+
|
| 45 |
+
Used to calculate the value of the positions per data. It sets the
|
| 46 |
+
``headers`` and ``cash`` parameters to ``True``
|
| 47 |
+
|
| 48 |
+
- ``Transactions``
|
| 49 |
+
|
| 50 |
+
Used to record each transaction on a data (size, price, value). Sets
|
| 51 |
+
the ``headers`` parameter to ``True``
|
| 52 |
+
|
| 53 |
+
- ``GrossLeverage``
|
| 54 |
+
|
| 55 |
+
Keeps track of the gross leverage (how much the strategy is invested)
|
| 56 |
+
|
| 57 |
+
Params:
|
| 58 |
+
These are passed transparently to the children
|
| 59 |
+
|
| 60 |
+
- timeframe (default: ``bt.TimeFrame.Days``)
|
| 61 |
+
|
| 62 |
+
If ``None`` then the timeframe of the 1st data of the system will be
|
| 63 |
+
used
|
| 64 |
+
|
| 65 |
+
- compression (default: `1``)
|
| 66 |
+
|
| 67 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 68 |
+
used
|
| 69 |
+
|
| 70 |
+
Both ``timeframe`` and ``compression`` are set following the default
|
| 71 |
+
behavior of ``pyfolio`` which is working with *daily* data and upsample it
|
| 72 |
+
to obtaine values like yearly returns.
|
| 73 |
+
|
| 74 |
+
Methods:
|
| 75 |
+
|
| 76 |
+
- get_analysis
|
| 77 |
+
|
| 78 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 79 |
+
each return as keys
|
| 80 |
+
'''
|
| 81 |
+
params = (
|
| 82 |
+
('timeframe', bt.TimeFrame.Days),
|
| 83 |
+
('compression', 1)
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def __init__(self):
|
| 87 |
+
dtfcomp = dict(timeframe=self.p.timeframe,
|
| 88 |
+
compression=self.p.compression)
|
| 89 |
+
|
| 90 |
+
self._returns = TimeReturn(**dtfcomp)
|
| 91 |
+
self._positions = PositionsValue(headers=True, cash=True)
|
| 92 |
+
self._transactions = Transactions(headers=True)
|
| 93 |
+
self._gross_lev = GrossLeverage()
|
| 94 |
+
|
| 95 |
+
def stop(self):
|
| 96 |
+
super(PyFolio, self).stop()
|
| 97 |
+
self.rets['returns'] = self._returns.get_analysis()
|
| 98 |
+
self.rets['positions'] = self._positions.get_analysis()
|
| 99 |
+
self.rets['transactions'] = self._transactions.get_analysis()
|
| 100 |
+
self.rets['gross_lev'] = self._gross_lev.get_analysis()
|
| 101 |
+
|
| 102 |
+
def get_pf_items(self):
|
| 103 |
+
'''Returns a tuple of 4 elements which can be used for further processing with
|
| 104 |
+
``pyfolio``
|
| 105 |
+
|
| 106 |
+
returns, positions, transactions, gross_leverage
|
| 107 |
+
|
| 108 |
+
Because the objects are meant to be used as direct input to ``pyfolio``
|
| 109 |
+
this method makes a local import of ``pandas`` to convert the internal
|
| 110 |
+
*backtrader* results to *pandas DataFrames* which is the expected input
|
| 111 |
+
by, for example, ``pyfolio.create_full_tear_sheet``
|
| 112 |
+
|
| 113 |
+
The method will break if ``pandas`` is not installed
|
| 114 |
+
'''
|
| 115 |
+
# keep import local to avoid disturbing installations with no pandas
|
| 116 |
+
import pandas
|
| 117 |
+
from pandas import DataFrame as DF
|
| 118 |
+
|
| 119 |
+
#
|
| 120 |
+
# Returns
|
| 121 |
+
cols = ['index', 'return']
|
| 122 |
+
returns = DF.from_records(iteritems(self.rets['returns']),
|
| 123 |
+
index=cols[0], columns=cols)
|
| 124 |
+
returns.index = pandas.to_datetime(returns.index)
|
| 125 |
+
returns.index = returns.index.tz_localize('UTC')
|
| 126 |
+
rets = returns['return']
|
| 127 |
+
#
|
| 128 |
+
# Positions
|
| 129 |
+
pss = self.rets['positions']
|
| 130 |
+
ps = [[k] + v[-2:] for k, v in iteritems(pss)]
|
| 131 |
+
cols = ps.pop(0) # headers are in the first entry
|
| 132 |
+
positions = DF.from_records(ps, index=cols[0], columns=cols)
|
| 133 |
+
positions.index = pandas.to_datetime(positions.index)
|
| 134 |
+
positions.index = positions.index.tz_localize('UTC')
|
| 135 |
+
|
| 136 |
+
#
|
| 137 |
+
# Transactions
|
| 138 |
+
txss = self.rets['transactions']
|
| 139 |
+
txs = list()
|
| 140 |
+
# The transactions have a common key (date) and can potentially happend
|
| 141 |
+
# for several assets. The dictionary has a single key and a list of
|
| 142 |
+
# lists. Each sublist contains the fields of a transaction
|
| 143 |
+
# Hence the double loop to undo the list indirection
|
| 144 |
+
for k, v in iteritems(txss):
|
| 145 |
+
for v2 in v:
|
| 146 |
+
txs.append([k] + v2)
|
| 147 |
+
|
| 148 |
+
cols = txs.pop(0) # headers are in the first entry
|
| 149 |
+
transactions = DF.from_records(txs, index=cols[0], columns=cols)
|
| 150 |
+
transactions.index = pandas.to_datetime(transactions.index)
|
| 151 |
+
transactions.index = transactions.index.tz_localize('UTC')
|
| 152 |
+
|
| 153 |
+
# Gross Leverage
|
| 154 |
+
cols = ['index', 'gross_lev']
|
| 155 |
+
gross_lev = DF.from_records(iteritems(self.rets['gross_lev']),
|
| 156 |
+
index=cols[0], columns=cols)
|
| 157 |
+
|
| 158 |
+
gross_lev.index = pandas.to_datetime(gross_lev.index)
|
| 159 |
+
gross_lev.index = gross_lev.index.tz_localize('UTC')
|
| 160 |
+
glev = gross_lev['gross_lev']
|
| 161 |
+
|
| 162 |
+
# Return all together
|
| 163 |
+
return rets, positions, transactions, glev
|
backtrader/source/backtrader/analyzers/returns.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import math
|
| 25 |
+
|
| 26 |
+
import backtrader as bt
|
| 27 |
+
from backtrader import TimeFrameAnalyzerBase
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class Returns(TimeFrameAnalyzerBase):
|
| 31 |
+
'''Total, Average, Compound and Annualized Returns calculated using a
|
| 32 |
+
logarithmic approach
|
| 33 |
+
|
| 34 |
+
See:
|
| 35 |
+
|
| 36 |
+
- https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/
|
| 37 |
+
|
| 38 |
+
Params:
|
| 39 |
+
|
| 40 |
+
- ``timeframe`` (default: ``None``)
|
| 41 |
+
|
| 42 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 43 |
+
used
|
| 44 |
+
|
| 45 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 46 |
+
time constraints
|
| 47 |
+
|
| 48 |
+
- ``compression`` (default: ``None``)
|
| 49 |
+
|
| 50 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 51 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 52 |
+
|
| 53 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 54 |
+
used
|
| 55 |
+
|
| 56 |
+
- ``tann`` (default: ``None``)
|
| 57 |
+
|
| 58 |
+
Number of periods to use for the annualization (normalization) of the
|
| 59 |
+
|
| 60 |
+
namely:
|
| 61 |
+
|
| 62 |
+
- ``days: 252``
|
| 63 |
+
- ``weeks: 52``
|
| 64 |
+
- ``months: 12``
|
| 65 |
+
- ``years: 1``
|
| 66 |
+
|
| 67 |
+
- ``fund`` (default: ``None``)
|
| 68 |
+
|
| 69 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 70 |
+
be autodetected to decide if the returns are based on the total net
|
| 71 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 72 |
+
documentation
|
| 73 |
+
|
| 74 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 75 |
+
|
| 76 |
+
Methods:
|
| 77 |
+
|
| 78 |
+
- get_analysis
|
| 79 |
+
|
| 80 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 81 |
+
each return as keys
|
| 82 |
+
|
| 83 |
+
The returned dict the following keys:
|
| 84 |
+
|
| 85 |
+
- ``rtot``: Total compound return
|
| 86 |
+
- ``ravg``: Average return for the entire period (timeframe specific)
|
| 87 |
+
- ``rnorm``: Annualized/Normalized return
|
| 88 |
+
- ``rnorm100``: Annualized/Normalized return expressed in 100%
|
| 89 |
+
|
| 90 |
+
'''
|
| 91 |
+
|
| 92 |
+
params = (
|
| 93 |
+
('tann', None),
|
| 94 |
+
('fund', None),
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
_TANN = {
|
| 98 |
+
bt.TimeFrame.Days: 252.0,
|
| 99 |
+
bt.TimeFrame.Weeks: 52.0,
|
| 100 |
+
bt.TimeFrame.Months: 12.0,
|
| 101 |
+
bt.TimeFrame.Years: 1.0,
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
def start(self):
|
| 105 |
+
super(Returns, self).start()
|
| 106 |
+
if self.p.fund is None:
|
| 107 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 108 |
+
else:
|
| 109 |
+
self._fundmode = self.p.fund
|
| 110 |
+
|
| 111 |
+
if not self._fundmode:
|
| 112 |
+
self._value_start = self.strategy.broker.getvalue()
|
| 113 |
+
else:
|
| 114 |
+
self._value_start = self.strategy.broker.fundvalue
|
| 115 |
+
|
| 116 |
+
self._tcount = 0
|
| 117 |
+
|
| 118 |
+
def stop(self):
|
| 119 |
+
super(Returns, self).stop()
|
| 120 |
+
|
| 121 |
+
if not self._fundmode:
|
| 122 |
+
self._value_end = self.strategy.broker.getvalue()
|
| 123 |
+
else:
|
| 124 |
+
self._value_end = self.strategy.broker.fundvalue
|
| 125 |
+
|
| 126 |
+
# Compound return
|
| 127 |
+
try:
|
| 128 |
+
nlrtot = self._value_end / self._value_start
|
| 129 |
+
except ZeroDivisionError:
|
| 130 |
+
rtot = float('-inf')
|
| 131 |
+
else:
|
| 132 |
+
if nlrtot < 0.0:
|
| 133 |
+
rtot = float('-inf')
|
| 134 |
+
else:
|
| 135 |
+
rtot = math.log(nlrtot)
|
| 136 |
+
|
| 137 |
+
self.rets['rtot'] = rtot
|
| 138 |
+
|
| 139 |
+
# Average return
|
| 140 |
+
self.rets['ravg'] = ravg = rtot / self._tcount
|
| 141 |
+
|
| 142 |
+
# Annualized normalized return
|
| 143 |
+
tann = self.p.tann or self._TANN.get(self.timeframe, None)
|
| 144 |
+
if tann is None:
|
| 145 |
+
tann = self._TANN.get(self.data._timeframe, 1.0) # assign default
|
| 146 |
+
|
| 147 |
+
if ravg > float('-inf'):
|
| 148 |
+
self.rets['rnorm'] = rnorm = math.expm1(ravg * tann)
|
| 149 |
+
else:
|
| 150 |
+
self.rets['rnorm'] = rnorm = ravg
|
| 151 |
+
|
| 152 |
+
self.rets['rnorm100'] = rnorm * 100.0 # human readable %
|
| 153 |
+
|
| 154 |
+
def _on_dt_over(self):
|
| 155 |
+
self._tcount += 1 # count the subperiod
|
backtrader/source/backtrader/analyzers/sharpe.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import math
|
| 25 |
+
|
| 26 |
+
from backtrader.utils.py3 import itervalues
|
| 27 |
+
|
| 28 |
+
from backtrader import Analyzer, TimeFrame
|
| 29 |
+
from backtrader.mathsupport import average, standarddev
|
| 30 |
+
from backtrader.analyzers import TimeReturn, AnnualReturn
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class SharpeRatio(Analyzer):
|
| 34 |
+
'''This analyzer calculates the SharpeRatio of a strategy using a risk free
|
| 35 |
+
asset which is simply an interest rate
|
| 36 |
+
|
| 37 |
+
See also:
|
| 38 |
+
|
| 39 |
+
- https://en.wikipedia.org/wiki/Sharpe_ratio
|
| 40 |
+
|
| 41 |
+
Params:
|
| 42 |
+
|
| 43 |
+
- ``timeframe``: (default: ``TimeFrame.Years``)
|
| 44 |
+
|
| 45 |
+
- ``compression`` (default: ``1``)
|
| 46 |
+
|
| 47 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 48 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 49 |
+
|
| 50 |
+
- ``riskfreerate`` (default: 0.01 -> 1%)
|
| 51 |
+
|
| 52 |
+
Expressed in annual terms (see ``convertrate`` below)
|
| 53 |
+
|
| 54 |
+
- ``convertrate`` (default: ``True``)
|
| 55 |
+
|
| 56 |
+
Convert the ``riskfreerate`` from annual to monthly, weekly or daily
|
| 57 |
+
rate. Sub-day conversions are not supported
|
| 58 |
+
|
| 59 |
+
- ``factor`` (default: ``None``)
|
| 60 |
+
|
| 61 |
+
If ``None``, the conversion factor for the riskfree rate from *annual*
|
| 62 |
+
to the chosen timeframe will be chosen from a predefined table
|
| 63 |
+
|
| 64 |
+
Days: 252, Weeks: 52, Months: 12, Years: 1
|
| 65 |
+
|
| 66 |
+
Else the specified value will be used
|
| 67 |
+
|
| 68 |
+
- ``annualize`` (default: ``False``)
|
| 69 |
+
|
| 70 |
+
If ``convertrate`` is ``True``, the *SharpeRatio* will be delivered in
|
| 71 |
+
the ``timeframe`` of choice.
|
| 72 |
+
|
| 73 |
+
In most occasions the SharpeRatio is delivered in annualized form.
|
| 74 |
+
Convert the ``riskfreerate`` from annual to monthly, weekly or daily
|
| 75 |
+
rate. Sub-day conversions are not supported
|
| 76 |
+
|
| 77 |
+
- ``stddev_sample`` (default: ``False``)
|
| 78 |
+
|
| 79 |
+
If this is set to ``True`` the *standard deviation* will be calculated
|
| 80 |
+
decreasing the denominator in the mean by ``1``. This is used when
|
| 81 |
+
calculating the *standard deviation* if it's considered that not all
|
| 82 |
+
samples are used for the calculation. This is known as the *Bessels'
|
| 83 |
+
correction*
|
| 84 |
+
|
| 85 |
+
- ``daysfactor`` (default: ``None``)
|
| 86 |
+
|
| 87 |
+
Old naming for ``factor``. If set to anything else than ``None`` and
|
| 88 |
+
the ``timeframe`` is ``TimeFrame.Days`` it will be assumed this is old
|
| 89 |
+
code and the value will be used
|
| 90 |
+
|
| 91 |
+
- ``legacyannual`` (default: ``False``)
|
| 92 |
+
|
| 93 |
+
Use the ``AnnualReturn`` return analyzer, which as the name implies
|
| 94 |
+
only works on years
|
| 95 |
+
|
| 96 |
+
- ``fund`` (default: ``None``)
|
| 97 |
+
|
| 98 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 99 |
+
be autodetected to decide if the returns are based on the total net
|
| 100 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 101 |
+
documentation
|
| 102 |
+
|
| 103 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 104 |
+
|
| 105 |
+
Methods:
|
| 106 |
+
|
| 107 |
+
- get_analysis
|
| 108 |
+
|
| 109 |
+
Returns a dictionary with key "sharperatio" holding the ratio
|
| 110 |
+
|
| 111 |
+
'''
|
| 112 |
+
params = (
|
| 113 |
+
('timeframe', TimeFrame.Years),
|
| 114 |
+
('compression', 1),
|
| 115 |
+
('riskfreerate', 0.01),
|
| 116 |
+
('factor', None),
|
| 117 |
+
('convertrate', True),
|
| 118 |
+
('annualize', False),
|
| 119 |
+
('stddev_sample', False),
|
| 120 |
+
|
| 121 |
+
# old behavior
|
| 122 |
+
('daysfactor', None),
|
| 123 |
+
('legacyannual', False),
|
| 124 |
+
('fund', None),
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
RATEFACTORS = {
|
| 128 |
+
TimeFrame.Days: 252,
|
| 129 |
+
TimeFrame.Weeks: 52,
|
| 130 |
+
TimeFrame.Months: 12,
|
| 131 |
+
TimeFrame.Years: 1,
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
def __init__(self):
|
| 135 |
+
if self.p.legacyannual:
|
| 136 |
+
self.anret = AnnualReturn()
|
| 137 |
+
else:
|
| 138 |
+
self.timereturn = TimeReturn(
|
| 139 |
+
timeframe=self.p.timeframe,
|
| 140 |
+
compression=self.p.compression,
|
| 141 |
+
fund=self.p.fund)
|
| 142 |
+
|
| 143 |
+
def stop(self):
|
| 144 |
+
super(SharpeRatio, self).stop()
|
| 145 |
+
if self.p.legacyannual:
|
| 146 |
+
rate = self.p.riskfreerate
|
| 147 |
+
retavg = average([r - rate for r in self.anret.rets])
|
| 148 |
+
retdev = standarddev(self.anret.rets)
|
| 149 |
+
|
| 150 |
+
self.ratio = retavg / retdev
|
| 151 |
+
else:
|
| 152 |
+
# Get the returns from the subanalyzer
|
| 153 |
+
returns = list(itervalues(self.timereturn.get_analysis()))
|
| 154 |
+
|
| 155 |
+
rate = self.p.riskfreerate #
|
| 156 |
+
|
| 157 |
+
factor = None
|
| 158 |
+
|
| 159 |
+
# Hack to identify old code
|
| 160 |
+
if self.p.timeframe == TimeFrame.Days and \
|
| 161 |
+
self.p.daysfactor is not None:
|
| 162 |
+
|
| 163 |
+
factor = self.p.daysfactor
|
| 164 |
+
|
| 165 |
+
else:
|
| 166 |
+
if self.p.factor is not None:
|
| 167 |
+
factor = self.p.factor # user specified factor
|
| 168 |
+
elif self.p.timeframe in self.RATEFACTORS:
|
| 169 |
+
# Get the conversion factor from the default table
|
| 170 |
+
factor = self.RATEFACTORS[self.p.timeframe]
|
| 171 |
+
|
| 172 |
+
if factor is not None:
|
| 173 |
+
# A factor was found
|
| 174 |
+
|
| 175 |
+
if self.p.convertrate:
|
| 176 |
+
# Standard: downgrade annual returns to timeframe factor
|
| 177 |
+
rate = pow(1.0 + rate, 1.0 / factor) - 1.0
|
| 178 |
+
else:
|
| 179 |
+
# Else upgrade returns to yearly returns
|
| 180 |
+
returns = [pow(1.0 + x, factor) - 1.0 for x in returns]
|
| 181 |
+
|
| 182 |
+
lrets = len(returns) - self.p.stddev_sample
|
| 183 |
+
# Check if the ratio can be calculated
|
| 184 |
+
if lrets:
|
| 185 |
+
# Get the excess returns - arithmetic mean - original sharpe
|
| 186 |
+
ret_free = [r - rate for r in returns]
|
| 187 |
+
ret_free_avg = average(ret_free)
|
| 188 |
+
retdev = standarddev(ret_free, avgx=ret_free_avg,
|
| 189 |
+
bessel=self.p.stddev_sample)
|
| 190 |
+
|
| 191 |
+
try:
|
| 192 |
+
ratio = ret_free_avg / retdev
|
| 193 |
+
|
| 194 |
+
if factor is not None and \
|
| 195 |
+
self.p.convertrate and self.p.annualize:
|
| 196 |
+
|
| 197 |
+
ratio = math.sqrt(factor) * ratio
|
| 198 |
+
except (ValueError, TypeError, ZeroDivisionError):
|
| 199 |
+
ratio = None
|
| 200 |
+
else:
|
| 201 |
+
# no returns or stddev_sample was active and 1 return
|
| 202 |
+
ratio = None
|
| 203 |
+
|
| 204 |
+
self.ratio = ratio
|
| 205 |
+
|
| 206 |
+
self.rets['sharperatio'] = self.ratio
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class SharpeRatio_A(SharpeRatio):
|
| 210 |
+
'''Extension of the SharpeRatio which returns the Sharpe Ratio directly in
|
| 211 |
+
annualized form
|
| 212 |
+
|
| 213 |
+
The following param has been changed from ``SharpeRatio``
|
| 214 |
+
|
| 215 |
+
- ``annualize`` (default: ``True``)
|
| 216 |
+
|
| 217 |
+
'''
|
| 218 |
+
|
| 219 |
+
params = (
|
| 220 |
+
('annualize', True),
|
| 221 |
+
)
|
backtrader/source/backtrader/analyzers/sqn.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import math
|
| 25 |
+
|
| 26 |
+
from backtrader import Analyzer
|
| 27 |
+
from backtrader.mathsupport import average, standarddev
|
| 28 |
+
from backtrader.utils import AutoOrderedDict
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SQN(Analyzer):
|
| 32 |
+
'''SQN or SystemQualityNumber. Defined by Van K. Tharp to categorize trading
|
| 33 |
+
systems.
|
| 34 |
+
|
| 35 |
+
- 1.6 - 1.9 Below average
|
| 36 |
+
- 2.0 - 2.4 Average
|
| 37 |
+
- 2.5 - 2.9 Good
|
| 38 |
+
- 3.0 - 5.0 Excellent
|
| 39 |
+
- 5.1 - 6.9 Superb
|
| 40 |
+
- 7.0 - Holy Grail?
|
| 41 |
+
|
| 42 |
+
The formula:
|
| 43 |
+
|
| 44 |
+
- SquareRoot(NumberTrades) * Average(TradesProfit) / StdDev(TradesProfit)
|
| 45 |
+
|
| 46 |
+
The sqn value should be deemed reliable when the number of trades >= 30
|
| 47 |
+
|
| 48 |
+
Methods:
|
| 49 |
+
|
| 50 |
+
- get_analysis
|
| 51 |
+
|
| 52 |
+
Returns a dictionary with keys "sqn" and "trades" (number of
|
| 53 |
+
considered trades)
|
| 54 |
+
|
| 55 |
+
'''
|
| 56 |
+
alias = ('SystemQualityNumber',)
|
| 57 |
+
|
| 58 |
+
def create_analysis(self):
|
| 59 |
+
'''Replace default implementation to instantiate an AutoOrdereDict
|
| 60 |
+
rather than an OrderedDict'''
|
| 61 |
+
self.rets = AutoOrderedDict()
|
| 62 |
+
|
| 63 |
+
def start(self):
|
| 64 |
+
super(SQN, self).start()
|
| 65 |
+
self.pnl = list()
|
| 66 |
+
self.count = 0
|
| 67 |
+
|
| 68 |
+
def notify_trade(self, trade):
|
| 69 |
+
if trade.status == trade.Closed:
|
| 70 |
+
self.pnl.append(trade.pnlcomm)
|
| 71 |
+
self.count += 1
|
| 72 |
+
|
| 73 |
+
def stop(self):
|
| 74 |
+
if self.count > 1:
|
| 75 |
+
pnl_av = average(self.pnl)
|
| 76 |
+
pnl_stddev = standarddev(self.pnl)
|
| 77 |
+
try:
|
| 78 |
+
sqn = math.sqrt(len(self.pnl)) * pnl_av / pnl_stddev
|
| 79 |
+
except ZeroDivisionError:
|
| 80 |
+
sqn = None
|
| 81 |
+
else:
|
| 82 |
+
sqn = 0
|
| 83 |
+
|
| 84 |
+
self.rets.sqn = sqn
|
| 85 |
+
self.rets.trades = self.count
|
backtrader/source/backtrader/analyzers/timereturn.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from backtrader import TimeFrameAnalyzerBase
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class TimeReturn(TimeFrameAnalyzerBase):
|
| 28 |
+
'''This analyzer calculates the Returns by looking at the beginning
|
| 29 |
+
and end of the timeframe
|
| 30 |
+
|
| 31 |
+
Params:
|
| 32 |
+
|
| 33 |
+
- ``timeframe`` (default: ``None``)
|
| 34 |
+
If ``None`` the ``timeframe`` of the 1st data in the system will be
|
| 35 |
+
used
|
| 36 |
+
|
| 37 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 38 |
+
time constraints
|
| 39 |
+
|
| 40 |
+
- ``compression`` (default: ``None``)
|
| 41 |
+
|
| 42 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 43 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 44 |
+
|
| 45 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 46 |
+
used
|
| 47 |
+
|
| 48 |
+
- ``data`` (default: ``None``)
|
| 49 |
+
|
| 50 |
+
Reference asset to track instead of the portfolio value.
|
| 51 |
+
|
| 52 |
+
.. note:: this data must have been added to a ``cerebro`` instance with
|
| 53 |
+
``addata``, ``resampledata`` or ``replaydata``
|
| 54 |
+
|
| 55 |
+
- ``firstopen`` (default: ``True``)
|
| 56 |
+
|
| 57 |
+
When tracking the returns of a ``data`` the following is done when
|
| 58 |
+
crossing a timeframe boundary, for example ``Years``:
|
| 59 |
+
|
| 60 |
+
- Last ``close`` of previous year is used as the reference price to
|
| 61 |
+
see the return in the current year
|
| 62 |
+
|
| 63 |
+
The problem is the 1st calculation, because the data has** no
|
| 64 |
+
previous** closing price. As such and when this parameter is ``True``
|
| 65 |
+
the *opening* price will be used for the 1st calculation.
|
| 66 |
+
|
| 67 |
+
This requires the data feed to have an ``open`` price (for ``close``
|
| 68 |
+
the standard [0] notation will be used without reference to a field
|
| 69 |
+
price)
|
| 70 |
+
|
| 71 |
+
Else the initial close will be used.
|
| 72 |
+
|
| 73 |
+
- ``fund`` (default: ``None``)
|
| 74 |
+
|
| 75 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 76 |
+
be autodetected to decide if the returns are based on the total net
|
| 77 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 78 |
+
documentation
|
| 79 |
+
|
| 80 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 81 |
+
|
| 82 |
+
Methods:
|
| 83 |
+
|
| 84 |
+
- get_analysis
|
| 85 |
+
|
| 86 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 87 |
+
each return as keys
|
| 88 |
+
'''
|
| 89 |
+
|
| 90 |
+
params = (
|
| 91 |
+
('data', None),
|
| 92 |
+
('firstopen', True),
|
| 93 |
+
('fund', None),
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
def start(self):
|
| 97 |
+
super(TimeReturn, self).start()
|
| 98 |
+
if self.p.fund is None:
|
| 99 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 100 |
+
else:
|
| 101 |
+
self._fundmode = self.p.fund
|
| 102 |
+
|
| 103 |
+
self._value_start = 0.0
|
| 104 |
+
self._lastvalue = None
|
| 105 |
+
if self.p.data is None:
|
| 106 |
+
# keep the initial portfolio value if not tracing a data
|
| 107 |
+
if not self._fundmode:
|
| 108 |
+
self._lastvalue = self.strategy.broker.getvalue()
|
| 109 |
+
else:
|
| 110 |
+
self._lastvalue = self.strategy.broker.fundvalue
|
| 111 |
+
|
| 112 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 113 |
+
if not self._fundmode:
|
| 114 |
+
# Record current value
|
| 115 |
+
if self.p.data is None:
|
| 116 |
+
self._value = value # the portofolio value if tracking no data
|
| 117 |
+
else:
|
| 118 |
+
self._value = self.p.data[0] # the data value if tracking data
|
| 119 |
+
else:
|
| 120 |
+
if self.p.data is None:
|
| 121 |
+
self._value = fundvalue # the fund value if tracking no data
|
| 122 |
+
else:
|
| 123 |
+
self._value = self.p.data[0] # the data value if tracking data
|
| 124 |
+
|
| 125 |
+
def on_dt_over(self):
|
| 126 |
+
# next is called in a new timeframe period
|
| 127 |
+
# if self.p.data is None or len(self.p.data) > 1:
|
| 128 |
+
if self.p.data is None or self._lastvalue is not None:
|
| 129 |
+
self._value_start = self._lastvalue # update value_start to last
|
| 130 |
+
|
| 131 |
+
else:
|
| 132 |
+
# The 1st tick has no previous reference, use the opening price
|
| 133 |
+
if self.p.firstopen:
|
| 134 |
+
self._value_start = self.p.data.open[0]
|
| 135 |
+
else:
|
| 136 |
+
self._value_start = self.p.data[0]
|
| 137 |
+
|
| 138 |
+
def next(self):
|
| 139 |
+
# Calculate the return
|
| 140 |
+
super(TimeReturn, self).next()
|
| 141 |
+
self.rets[self.dtkey] = (self._value / self._value_start) - 1.0
|
| 142 |
+
self._lastvalue = self._value # keep last value
|
backtrader/source/backtrader/analyzers/tradeanalyzer.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import sys
|
| 25 |
+
|
| 26 |
+
from backtrader import Analyzer
|
| 27 |
+
from backtrader.utils import AutoOrderedDict, AutoDict
|
| 28 |
+
from backtrader.utils.py3 import MAXINT
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class TradeAnalyzer(Analyzer):
|
| 32 |
+
'''
|
| 33 |
+
Provides statistics on closed trades (keeps also the count of open ones)
|
| 34 |
+
|
| 35 |
+
- Total Open/Closed Trades
|
| 36 |
+
|
| 37 |
+
- Streak Won/Lost Current/Longest
|
| 38 |
+
|
| 39 |
+
- ProfitAndLoss Total/Average
|
| 40 |
+
|
| 41 |
+
- Won/Lost Count/ Total PNL/ Average PNL / Max PNL
|
| 42 |
+
|
| 43 |
+
- Long/Short Count/ Total PNL / Average PNL / Max PNL
|
| 44 |
+
|
| 45 |
+
- Won/Lost Count/ Total PNL/ Average PNL / Max PNL
|
| 46 |
+
|
| 47 |
+
- Length (bars in the market)
|
| 48 |
+
|
| 49 |
+
- Total/Average/Max/Min
|
| 50 |
+
|
| 51 |
+
- Won/Lost Total/Average/Max/Min
|
| 52 |
+
|
| 53 |
+
- Long/Short Total/Average/Max/Min
|
| 54 |
+
|
| 55 |
+
- Won/Lost Total/Average/Max/Min
|
| 56 |
+
|
| 57 |
+
Note:
|
| 58 |
+
|
| 59 |
+
The analyzer uses an "auto"dict for the fields, which means that if no
|
| 60 |
+
trades are executed, no statistics will be generated.
|
| 61 |
+
|
| 62 |
+
In that case there will be a single field/subfield in the dictionary
|
| 63 |
+
returned by ``get_analysis``, namely:
|
| 64 |
+
|
| 65 |
+
- dictname['total']['total'] which will have a value of 0 (the field is
|
| 66 |
+
also reachable with dot notation dictname.total.total
|
| 67 |
+
'''
|
| 68 |
+
def create_analysis(self):
|
| 69 |
+
self.rets = AutoOrderedDict()
|
| 70 |
+
self.rets.total.total = 0
|
| 71 |
+
|
| 72 |
+
def stop(self):
|
| 73 |
+
super(TradeAnalyzer, self).stop()
|
| 74 |
+
self.rets._close()
|
| 75 |
+
|
| 76 |
+
def notify_trade(self, trade):
|
| 77 |
+
if trade.justopened:
|
| 78 |
+
# Trade just opened
|
| 79 |
+
self.rets.total.total += 1
|
| 80 |
+
self.rets.total.open += 1
|
| 81 |
+
|
| 82 |
+
elif trade.status == trade.Closed:
|
| 83 |
+
trades = self.rets
|
| 84 |
+
|
| 85 |
+
res = AutoDict()
|
| 86 |
+
# Trade just closed
|
| 87 |
+
|
| 88 |
+
won = res.won = int(trade.pnlcomm >= 0.0)
|
| 89 |
+
lost = res.lost = int(not won)
|
| 90 |
+
tlong = res.tlong = trade.long
|
| 91 |
+
tshort = res.tshort = not trade.long
|
| 92 |
+
|
| 93 |
+
trades.total.open -= 1
|
| 94 |
+
trades.total.closed += 1
|
| 95 |
+
|
| 96 |
+
# Streak
|
| 97 |
+
for wlname in ['won', 'lost']:
|
| 98 |
+
wl = res[wlname]
|
| 99 |
+
|
| 100 |
+
trades.streak[wlname].current *= wl
|
| 101 |
+
trades.streak[wlname].current += wl
|
| 102 |
+
|
| 103 |
+
ls = trades.streak[wlname].longest or 0
|
| 104 |
+
trades.streak[wlname].longest = \
|
| 105 |
+
max(ls, trades.streak[wlname].current)
|
| 106 |
+
|
| 107 |
+
trpnl = trades.pnl
|
| 108 |
+
trpnl.gross.total += trade.pnl
|
| 109 |
+
trpnl.gross.average = trades.pnl.gross.total / trades.total.closed
|
| 110 |
+
trpnl.net.total += trade.pnlcomm
|
| 111 |
+
trpnl.net.average = trades.pnl.net.total / trades.total.closed
|
| 112 |
+
|
| 113 |
+
# Won/Lost statistics
|
| 114 |
+
for wlname in ['won', 'lost']:
|
| 115 |
+
wl = res[wlname]
|
| 116 |
+
trwl = trades[wlname]
|
| 117 |
+
|
| 118 |
+
trwl.total += wl # won.total / lost.total
|
| 119 |
+
|
| 120 |
+
trwlpnl = trwl.pnl
|
| 121 |
+
pnlcomm = trade.pnlcomm * wl
|
| 122 |
+
|
| 123 |
+
trwlpnl.total += pnlcomm
|
| 124 |
+
trwlpnl.average = trwlpnl.total / (trwl.total or 1.0)
|
| 125 |
+
|
| 126 |
+
wm = trwlpnl.max or 0.0
|
| 127 |
+
func = max if wlname == 'won' else min
|
| 128 |
+
trwlpnl.max = func(wm, pnlcomm)
|
| 129 |
+
|
| 130 |
+
# Long/Short statistics
|
| 131 |
+
for tname in ['long', 'short']:
|
| 132 |
+
trls = trades[tname]
|
| 133 |
+
ls = res['t' + tname]
|
| 134 |
+
|
| 135 |
+
trls.total += ls # long.total / short.total
|
| 136 |
+
trls.pnl.total += trade.pnlcomm * ls
|
| 137 |
+
trls.pnl.average = trls.pnl.total / (trls.total or 1.0)
|
| 138 |
+
|
| 139 |
+
for wlname in ['won', 'lost']:
|
| 140 |
+
wl = res[wlname]
|
| 141 |
+
pnlcomm = trade.pnlcomm * wl * ls
|
| 142 |
+
|
| 143 |
+
trls[wlname] += wl * ls # long.won / short.won
|
| 144 |
+
|
| 145 |
+
trls.pnl[wlname].total += pnlcomm
|
| 146 |
+
trls.pnl[wlname].average = \
|
| 147 |
+
trls.pnl[wlname].total / (trls[wlname] or 1.0)
|
| 148 |
+
|
| 149 |
+
wm = trls.pnl[wlname].max or 0.0
|
| 150 |
+
func = max if wlname == 'won' else min
|
| 151 |
+
trls.pnl[wlname].max = func(wm, pnlcomm)
|
| 152 |
+
|
| 153 |
+
# Length
|
| 154 |
+
trades.len.total += trade.barlen
|
| 155 |
+
trades.len.average = trades.len.total / trades.total.closed
|
| 156 |
+
ml = trades.len.max or 0
|
| 157 |
+
trades.len.max = max(ml, trade.barlen)
|
| 158 |
+
|
| 159 |
+
ml = trades.len.min or MAXINT
|
| 160 |
+
trades.len.min = min(ml, trade.barlen)
|
| 161 |
+
|
| 162 |
+
# Length Won/Lost
|
| 163 |
+
for wlname in ['won', 'lost']:
|
| 164 |
+
trwl = trades.len[wlname]
|
| 165 |
+
wl = res[wlname]
|
| 166 |
+
|
| 167 |
+
trwl.total += trade.barlen * wl
|
| 168 |
+
trwl.average = trwl.total / (trades[wlname].total or 1.0)
|
| 169 |
+
|
| 170 |
+
m = trwl.max or 0
|
| 171 |
+
trwl.max = max(m, trade.barlen * wl)
|
| 172 |
+
if trade.barlen * wl:
|
| 173 |
+
m = trwl.min or MAXINT
|
| 174 |
+
trwl.min = min(m, trade.barlen * wl)
|
| 175 |
+
|
| 176 |
+
# Length Long/Short
|
| 177 |
+
for lsname in ['long', 'short']:
|
| 178 |
+
trls = trades.len[lsname] # trades.len.long
|
| 179 |
+
ls = res['t' + lsname] # tlong/tshort
|
| 180 |
+
|
| 181 |
+
barlen = trade.barlen * ls
|
| 182 |
+
|
| 183 |
+
trls.total += barlen # trades.len.long.total
|
| 184 |
+
total_ls = trades[lsname].total # trades.long.total
|
| 185 |
+
trls.average = trls.total / (total_ls or 1.0)
|
| 186 |
+
|
| 187 |
+
# max/min
|
| 188 |
+
m = trls.max or 0
|
| 189 |
+
trls.max = max(m, barlen)
|
| 190 |
+
m = trls.min or MAXINT
|
| 191 |
+
trls.min = min(m, barlen or m)
|
| 192 |
+
|
| 193 |
+
for wlname in ['won', 'lost']:
|
| 194 |
+
wl = res[wlname] # won/lost
|
| 195 |
+
|
| 196 |
+
barlen2 = trade.barlen * ls * wl
|
| 197 |
+
|
| 198 |
+
trls_wl = trls[wlname] # trades.len.long.won
|
| 199 |
+
trls_wl.total += barlen2 # trades.len.long.won.total
|
| 200 |
+
|
| 201 |
+
trls_wl.average = \
|
| 202 |
+
trls_wl.total / (trades[lsname][wlname] or 1.0)
|
| 203 |
+
|
| 204 |
+
# max/min
|
| 205 |
+
m = trls_wl.max or 0
|
| 206 |
+
trls_wl.max = max(m, barlen2)
|
| 207 |
+
m = trls_wl.min or MAXINT
|
| 208 |
+
trls_wl.min = min(m, barlen2 or m)
|
backtrader/source/backtrader/analyzers/transactions.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
import collections
|
| 26 |
+
|
| 27 |
+
import backtrader as bt
|
| 28 |
+
from backtrader import Order, Position
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class Transactions(bt.Analyzer):
|
| 32 |
+
'''This analyzer reports the transactions occurred with each an every data in
|
| 33 |
+
the system
|
| 34 |
+
|
| 35 |
+
It looks at the order execution bits to create a ``Position`` starting from
|
| 36 |
+
0 during each ``next`` cycle.
|
| 37 |
+
|
| 38 |
+
The result is used during next to record the transactions
|
| 39 |
+
|
| 40 |
+
Params:
|
| 41 |
+
|
| 42 |
+
- headers (default: ``True``)
|
| 43 |
+
|
| 44 |
+
Add an initial key to the dictionary holding the results with the names
|
| 45 |
+
of the datas
|
| 46 |
+
|
| 47 |
+
This analyzer was modeled to facilitate the integration with
|
| 48 |
+
``pyfolio`` and the header names are taken from the samples used for
|
| 49 |
+
it::
|
| 50 |
+
|
| 51 |
+
'date', 'amount', 'price', 'sid', 'symbol', 'value'
|
| 52 |
+
|
| 53 |
+
Methods:
|
| 54 |
+
|
| 55 |
+
- get_analysis
|
| 56 |
+
|
| 57 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 58 |
+
each return as keys
|
| 59 |
+
'''
|
| 60 |
+
params = (
|
| 61 |
+
('headers', False),
|
| 62 |
+
('_pfheaders', ('date', 'amount', 'price', 'sid', 'symbol', 'value')),
|
| 63 |
+
)
|
| 64 |
+
|
| 65 |
+
def start(self):
|
| 66 |
+
super(Transactions, self).start()
|
| 67 |
+
if self.p.headers:
|
| 68 |
+
self.rets[self.p._pfheaders[0]] = [list(self.p._pfheaders[1:])]
|
| 69 |
+
|
| 70 |
+
self._positions = collections.defaultdict(Position)
|
| 71 |
+
self._idnames = list(enumerate(self.strategy.getdatanames()))
|
| 72 |
+
|
| 73 |
+
def notify_order(self, order):
|
| 74 |
+
# An order could have several partial executions per cycle (unlikely
|
| 75 |
+
# but possible) and therefore: collect each new execution notification
|
| 76 |
+
# and let the work for next
|
| 77 |
+
|
| 78 |
+
# We use a fresh Position object for each round to get summary of what
|
| 79 |
+
# the execution bits have done in that round
|
| 80 |
+
if order.status not in [Order.Partial, Order.Completed]:
|
| 81 |
+
return # It's not an execution
|
| 82 |
+
|
| 83 |
+
pos = self._positions[order.data._name]
|
| 84 |
+
for exbit in order.executed.iterpending():
|
| 85 |
+
if exbit is None:
|
| 86 |
+
break # end of pending reached
|
| 87 |
+
|
| 88 |
+
pos.update(exbit.size, exbit.price)
|
| 89 |
+
|
| 90 |
+
def next(self):
|
| 91 |
+
# super(Transactions, self).next() # let dtkey update
|
| 92 |
+
entries = []
|
| 93 |
+
for i, dname in self._idnames:
|
| 94 |
+
pos = self._positions.get(dname, None)
|
| 95 |
+
if pos is not None:
|
| 96 |
+
size, price = pos.size, pos.price
|
| 97 |
+
if size:
|
| 98 |
+
entries.append([size, price, i, dname, -size * price])
|
| 99 |
+
|
| 100 |
+
if entries:
|
| 101 |
+
self.rets[self.strategy.datetime.datetime()] = entries
|
| 102 |
+
|
| 103 |
+
self._positions.clear()
|
backtrader/source/backtrader/analyzers/vwr.py
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import math
|
| 25 |
+
|
| 26 |
+
import backtrader as bt
|
| 27 |
+
from backtrader import TimeFrameAnalyzerBase
|
| 28 |
+
from . import Returns
|
| 29 |
+
from ..mathsupport import standarddev
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class VWR(TimeFrameAnalyzerBase):
|
| 33 |
+
'''Variability-Weighted Return: Better SharpeRatio with Log Returns
|
| 34 |
+
|
| 35 |
+
Alias:
|
| 36 |
+
|
| 37 |
+
- VariabilityWeightedReturn
|
| 38 |
+
|
| 39 |
+
See:
|
| 40 |
+
|
| 41 |
+
- https://www.crystalbull.com/sharpe-ratio-better-with-log-returns/
|
| 42 |
+
|
| 43 |
+
Params:
|
| 44 |
+
|
| 45 |
+
- ``timeframe`` (default: ``None``)
|
| 46 |
+
If ``None`` then the complete return over the entire backtested period
|
| 47 |
+
will be reported
|
| 48 |
+
|
| 49 |
+
Pass ``TimeFrame.NoTimeFrame`` to consider the entire dataset with no
|
| 50 |
+
time constraints
|
| 51 |
+
|
| 52 |
+
- ``compression`` (default: ``None``)
|
| 53 |
+
|
| 54 |
+
Only used for sub-day timeframes to for example work on an hourly
|
| 55 |
+
timeframe by specifying "TimeFrame.Minutes" and 60 as compression
|
| 56 |
+
|
| 57 |
+
If ``None`` then the compression of the 1st data of the system will be
|
| 58 |
+
used
|
| 59 |
+
|
| 60 |
+
- ``tann`` (default: ``None``)
|
| 61 |
+
|
| 62 |
+
Number of periods to use for the annualization (normalization) of the
|
| 63 |
+
average returns. If ``None``, then standard ``t`` values will be used,
|
| 64 |
+
namely:
|
| 65 |
+
|
| 66 |
+
- ``days: 252``
|
| 67 |
+
- ``weeks: 52``
|
| 68 |
+
- ``months: 12``
|
| 69 |
+
- ``years: 1``
|
| 70 |
+
|
| 71 |
+
- ``tau`` (default: ``2.0``)
|
| 72 |
+
|
| 73 |
+
factor for the calculation (see the literature)
|
| 74 |
+
|
| 75 |
+
- ``sdev_max`` (default: ``0.20``)
|
| 76 |
+
|
| 77 |
+
max standard deviation (see the literature)
|
| 78 |
+
|
| 79 |
+
- ``fund`` (default: ``None``)
|
| 80 |
+
|
| 81 |
+
If ``None`` the actual mode of the broker (fundmode - True/False) will
|
| 82 |
+
be autodetected to decide if the returns are based on the total net
|
| 83 |
+
asset value or on the fund value. See ``set_fundmode`` in the broker
|
| 84 |
+
documentation
|
| 85 |
+
|
| 86 |
+
Set it to ``True`` or ``False`` for a specific behavior
|
| 87 |
+
|
| 88 |
+
Methods:
|
| 89 |
+
|
| 90 |
+
- get_analysis
|
| 91 |
+
|
| 92 |
+
Returns a dictionary with returns as values and the datetime points for
|
| 93 |
+
each return as keys
|
| 94 |
+
|
| 95 |
+
The returned dict contains the following keys:
|
| 96 |
+
|
| 97 |
+
- ``vwr``: Variability-Weighted Return
|
| 98 |
+
'''
|
| 99 |
+
|
| 100 |
+
params = (
|
| 101 |
+
('tann', None),
|
| 102 |
+
('tau', 0.20),
|
| 103 |
+
('sdev_max', 2.0),
|
| 104 |
+
('fund', None),
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
_TANN = {
|
| 108 |
+
bt.TimeFrame.Days: 252.0,
|
| 109 |
+
bt.TimeFrame.Weeks: 52.0,
|
| 110 |
+
bt.TimeFrame.Months: 12.0,
|
| 111 |
+
bt.TimeFrame.Years: 1.0,
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
def __init__(self):
|
| 115 |
+
# Children log return analyzer
|
| 116 |
+
self._returns = Returns(timeframe=self.p.timeframe,
|
| 117 |
+
compression=self.p.compression,
|
| 118 |
+
tann=self.p.tann)
|
| 119 |
+
|
| 120 |
+
def start(self):
|
| 121 |
+
super(VWR, self).start()
|
| 122 |
+
# Add an initial placeholder for [-1] operation
|
| 123 |
+
if self.p.fund is None:
|
| 124 |
+
self._fundmode = self.strategy.broker.fundmode
|
| 125 |
+
else:
|
| 126 |
+
self._fundmode = self.p.fund
|
| 127 |
+
|
| 128 |
+
if not self._fundmode:
|
| 129 |
+
self._pis = [self.strategy.broker.getvalue()] # keep initial value
|
| 130 |
+
else:
|
| 131 |
+
self._pis = [self.strategy.broker.fundvalue] # keep initial value
|
| 132 |
+
|
| 133 |
+
self._pns = [None] # keep final prices (value)
|
| 134 |
+
|
| 135 |
+
def stop(self):
|
| 136 |
+
super(VWR, self).stop()
|
| 137 |
+
# Check if no value has been seen after the last 'dt_over'
|
| 138 |
+
# If so, there is one 'pi' out of place and a None 'pn'. Purge
|
| 139 |
+
if self._pns[-1] is None:
|
| 140 |
+
self._pis.pop()
|
| 141 |
+
self._pns.pop()
|
| 142 |
+
|
| 143 |
+
# Get results from children
|
| 144 |
+
rs = self._returns.get_analysis()
|
| 145 |
+
ravg = rs['ravg']
|
| 146 |
+
rnorm100 = rs['rnorm100']
|
| 147 |
+
|
| 148 |
+
# make n 1 based in enumerate (number of periods and not index)
|
| 149 |
+
# skip initial placeholders for synchronization
|
| 150 |
+
dts = []
|
| 151 |
+
for n, pipn in enumerate(zip(self._pis, self._pns), 1):
|
| 152 |
+
pi, pn = pipn
|
| 153 |
+
|
| 154 |
+
dt = pn / (pi * math.exp(ravg * n)) - 1.0
|
| 155 |
+
dts.append(dt)
|
| 156 |
+
|
| 157 |
+
sdev_p = standarddev(dts, bessel=True)
|
| 158 |
+
|
| 159 |
+
vwr = rnorm100 * (1.0 - pow(sdev_p / self.p.sdev_max, self.p.tau))
|
| 160 |
+
self.rets['vwr'] = vwr
|
| 161 |
+
|
| 162 |
+
def notify_fund(self, cash, value, fundvalue, shares):
|
| 163 |
+
if not self._fundmode:
|
| 164 |
+
self._pns[-1] = value # annotate last seen pn for current period
|
| 165 |
+
else:
|
| 166 |
+
self._pns[-1] = fundvalue # annotate last pn for current period
|
| 167 |
+
|
| 168 |
+
def _on_dt_over(self):
|
| 169 |
+
self._pis.append(self._pns[-1]) # last pn is pi in next period
|
| 170 |
+
self._pns.append(None) # placeholder for [-1] operation
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
VariabilityWeightedReturn = VWR
|
backtrader/source/backtrader/broker.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from backtrader.comminfo import CommInfoBase
|
| 25 |
+
from backtrader.metabase import MetaParams
|
| 26 |
+
from backtrader.utils.py3 import with_metaclass
|
| 27 |
+
|
| 28 |
+
from . import fillers as fillers
|
| 29 |
+
from . import fillers as filler
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
class MetaBroker(MetaParams):
|
| 33 |
+
def __init__(cls, name, bases, dct):
|
| 34 |
+
'''
|
| 35 |
+
Class has already been created ... fill missing methods if needed be
|
| 36 |
+
'''
|
| 37 |
+
# Initialize the class
|
| 38 |
+
super(MetaBroker, cls).__init__(name, bases, dct)
|
| 39 |
+
translations = {
|
| 40 |
+
'get_cash': 'getcash',
|
| 41 |
+
'get_value': 'getvalue',
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
for attr, trans in translations.items():
|
| 45 |
+
if not hasattr(cls, attr):
|
| 46 |
+
setattr(cls, name, getattr(cls, trans))
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class BrokerBase(with_metaclass(MetaBroker, object)):
|
| 50 |
+
params = (
|
| 51 |
+
('commission', CommInfoBase(percabs=True)),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
def __init__(self):
|
| 55 |
+
self.comminfo = dict()
|
| 56 |
+
self.init()
|
| 57 |
+
|
| 58 |
+
def init(self):
|
| 59 |
+
# called from init and from start
|
| 60 |
+
if None not in self.comminfo:
|
| 61 |
+
self.comminfo = dict({None: self.p.commission})
|
| 62 |
+
|
| 63 |
+
def start(self):
|
| 64 |
+
self.init()
|
| 65 |
+
|
| 66 |
+
def stop(self):
|
| 67 |
+
pass
|
| 68 |
+
|
| 69 |
+
def add_order_history(self, orders, notify=False):
|
| 70 |
+
'''Add order history. See cerebro for details'''
|
| 71 |
+
raise NotImplementedError
|
| 72 |
+
|
| 73 |
+
def set_fund_history(self, fund):
|
| 74 |
+
'''Add fund history. See cerebro for details'''
|
| 75 |
+
raise NotImplementedError
|
| 76 |
+
|
| 77 |
+
def getcommissioninfo(self, data):
|
| 78 |
+
'''Retrieves the ``CommissionInfo`` scheme associated with the given
|
| 79 |
+
``data``'''
|
| 80 |
+
if data._name in self.comminfo:
|
| 81 |
+
return self.comminfo[data._name]
|
| 82 |
+
|
| 83 |
+
return self.comminfo[None]
|
| 84 |
+
|
| 85 |
+
def setcommission(self,
|
| 86 |
+
commission=0.0, margin=None, mult=1.0,
|
| 87 |
+
commtype=None, percabs=True, stocklike=False,
|
| 88 |
+
interest=0.0, interest_long=False, leverage=1.0,
|
| 89 |
+
automargin=False,
|
| 90 |
+
name=None):
|
| 91 |
+
|
| 92 |
+
'''This method sets a `` CommissionInfo`` object for assets managed in
|
| 93 |
+
the broker with the parameters. Consult the reference for
|
| 94 |
+
``CommInfoBase``
|
| 95 |
+
|
| 96 |
+
If name is ``None``, this will be the default for assets for which no
|
| 97 |
+
other ``CommissionInfo`` scheme can be found
|
| 98 |
+
'''
|
| 99 |
+
|
| 100 |
+
comm = CommInfoBase(commission=commission, margin=margin, mult=mult,
|
| 101 |
+
commtype=commtype, stocklike=stocklike,
|
| 102 |
+
percabs=percabs,
|
| 103 |
+
interest=interest, interest_long=interest_long,
|
| 104 |
+
leverage=leverage, automargin=automargin)
|
| 105 |
+
self.comminfo[name] = comm
|
| 106 |
+
|
| 107 |
+
def addcommissioninfo(self, comminfo, name=None):
|
| 108 |
+
'''Adds a ``CommissionInfo`` object that will be the default for all assets if
|
| 109 |
+
``name`` is ``None``'''
|
| 110 |
+
self.comminfo[name] = comminfo
|
| 111 |
+
|
| 112 |
+
def getcash(self):
|
| 113 |
+
raise NotImplementedError
|
| 114 |
+
|
| 115 |
+
def getvalue(self, datas=None):
|
| 116 |
+
raise NotImplementedError
|
| 117 |
+
|
| 118 |
+
def get_fundshares(self):
|
| 119 |
+
'''Returns the current number of shares in the fund-like mode'''
|
| 120 |
+
return 1.0 # the abstract mode has only 1 share
|
| 121 |
+
|
| 122 |
+
fundshares = property(get_fundshares)
|
| 123 |
+
|
| 124 |
+
def get_fundvalue(self):
|
| 125 |
+
return self.getvalue()
|
| 126 |
+
|
| 127 |
+
fundvalue = property(get_fundvalue)
|
| 128 |
+
|
| 129 |
+
def set_fundmode(self, fundmode, fundstartval=None):
|
| 130 |
+
'''Set the actual fundmode (True or False)
|
| 131 |
+
|
| 132 |
+
If the argument fundstartval is not ``None``, it will used
|
| 133 |
+
'''
|
| 134 |
+
pass # do nothing, not all brokers can support this
|
| 135 |
+
|
| 136 |
+
def get_fundmode(self):
|
| 137 |
+
'''Returns the actual fundmode (True or False)'''
|
| 138 |
+
return False
|
| 139 |
+
|
| 140 |
+
fundmode = property(get_fundmode, set_fundmode)
|
| 141 |
+
|
| 142 |
+
def getposition(self, data):
|
| 143 |
+
raise NotImplementedError
|
| 144 |
+
|
| 145 |
+
def submit(self, order):
|
| 146 |
+
raise NotImplementedError
|
| 147 |
+
|
| 148 |
+
def cancel(self, order):
|
| 149 |
+
raise NotImplementedError
|
| 150 |
+
|
| 151 |
+
def buy(self, owner, data, size, price=None, plimit=None,
|
| 152 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 153 |
+
trailamount=None, trailpercent=None,
|
| 154 |
+
**kwargs):
|
| 155 |
+
|
| 156 |
+
raise NotImplementedError
|
| 157 |
+
|
| 158 |
+
def sell(self, owner, data, size, price=None, plimit=None,
|
| 159 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 160 |
+
trailamount=None, trailpercent=None,
|
| 161 |
+
**kwargs):
|
| 162 |
+
|
| 163 |
+
raise NotImplementedError
|
| 164 |
+
|
| 165 |
+
def next(self):
|
| 166 |
+
pass
|
| 167 |
+
|
| 168 |
+
# __all__ = ['BrokerBase', 'fillers', 'filler']
|
backtrader/source/backtrader/brokers/__init__.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
# The modules below should/must define __all__ with the objects wishes
|
| 25 |
+
# or prepend an "_" (underscore) to private classes/variables
|
| 26 |
+
|
| 27 |
+
from .bbroker import BackBroker, BrokerBack
|
| 28 |
+
|
| 29 |
+
try:
|
| 30 |
+
from .ibbroker import IBBroker
|
| 31 |
+
except ImportError:
|
| 32 |
+
pass # The user may not have ibpy installed
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
from .vcbroker import VCBroker
|
| 36 |
+
except ImportError:
|
| 37 |
+
pass # The user may not have something installed
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
from .oandabroker import OandaBroker
|
| 41 |
+
except ImportError as e:
|
| 42 |
+
pass # The user may not have something installed
|
backtrader/source/backtrader/brokers/bbroker.py
ADDED
|
@@ -0,0 +1,1237 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
import datetime
|
| 26 |
+
|
| 27 |
+
import backtrader as bt
|
| 28 |
+
from backtrader.comminfo import CommInfoBase
|
| 29 |
+
from backtrader.order import Order, BuyOrder, SellOrder
|
| 30 |
+
from backtrader.position import Position
|
| 31 |
+
from backtrader.utils.py3 import string_types, integer_types
|
| 32 |
+
|
| 33 |
+
__all__ = ['BackBroker', 'BrokerBack']
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class BackBroker(bt.BrokerBase):
|
| 37 |
+
'''Broker Simulator
|
| 38 |
+
|
| 39 |
+
The simulation supports different order types, checking a submitted order
|
| 40 |
+
cash requirements against current cash, keeping track of cash and value
|
| 41 |
+
for each iteration of ``cerebro`` and keeping the current position on
|
| 42 |
+
different datas.
|
| 43 |
+
|
| 44 |
+
*cash* is adjusted on each iteration for instruments like ``futures`` for
|
| 45 |
+
which a price change implies in real brokers the addition/substracion of
|
| 46 |
+
cash.
|
| 47 |
+
|
| 48 |
+
Supported order types:
|
| 49 |
+
|
| 50 |
+
- ``Market``: to be executed with the 1st tick of the next bar (namely
|
| 51 |
+
the ``open`` price)
|
| 52 |
+
|
| 53 |
+
- ``Close``: meant for intraday in which the order is executed with the
|
| 54 |
+
closing price of the last bar of the session
|
| 55 |
+
|
| 56 |
+
- ``Limit``: executes if the given limit price is seen during the
|
| 57 |
+
session
|
| 58 |
+
|
| 59 |
+
- ``Stop``: executes a ``Market`` order if the given stop price is seen
|
| 60 |
+
|
| 61 |
+
- ``StopLimit``: sets a ``Limit`` order in motion if the given stop
|
| 62 |
+
price is seen
|
| 63 |
+
|
| 64 |
+
Because the broker is instantiated by ``Cerebro`` and there should be
|
| 65 |
+
(mostly) no reason to replace the broker, the params are not controlled
|
| 66 |
+
by the user for the instance. To change this there are two options:
|
| 67 |
+
|
| 68 |
+
1. Manually create an instance of this class with the desired params
|
| 69 |
+
and use ``cerebro.broker = instance`` to set the instance as the
|
| 70 |
+
broker for the ``run`` execution
|
| 71 |
+
|
| 72 |
+
2. Use the ``set_xxx`` to set the value using
|
| 73 |
+
``cerebro.broker.set_xxx`` where ```xxx`` stands for the name of the
|
| 74 |
+
parameter to set
|
| 75 |
+
|
| 76 |
+
.. note::
|
| 77 |
+
|
| 78 |
+
``cerebro.broker`` is a *property* supported by the ``getbroker``
|
| 79 |
+
and ``setbroker`` methods of ``Cerebro``
|
| 80 |
+
|
| 81 |
+
Params:
|
| 82 |
+
|
| 83 |
+
- ``cash`` (default: ``10000``): starting cash
|
| 84 |
+
|
| 85 |
+
- ``commission`` (default: ``CommInfoBase(percabs=True)``)
|
| 86 |
+
base commission scheme which applies to all assets
|
| 87 |
+
|
| 88 |
+
- ``checksubmit`` (default: ``True``)
|
| 89 |
+
check margin/cash before accepting an order into the system
|
| 90 |
+
|
| 91 |
+
- ``eosbar`` (default: ``False``):
|
| 92 |
+
With intraday bars consider a bar with the same ``time`` as the end
|
| 93 |
+
of session to be the end of the session. This is not usually the
|
| 94 |
+
case, because some bars (final auction) are produced by many
|
| 95 |
+
exchanges for many products for a couple of minutes after the end of
|
| 96 |
+
the session
|
| 97 |
+
|
| 98 |
+
- ``filler`` (default: ``None``)
|
| 99 |
+
|
| 100 |
+
A callable with signature: ``callable(order, price, ago)``
|
| 101 |
+
|
| 102 |
+
- ``order``: obviously the order in execution. This provides access
|
| 103 |
+
to the *data* (and with it the *ohlc* and *volume* values), the
|
| 104 |
+
*execution type*, remaining size (``order.executed.remsize``) and
|
| 105 |
+
others.
|
| 106 |
+
|
| 107 |
+
Please check the ``Order`` documentation and reference for things
|
| 108 |
+
available inside an ``Order`` instance
|
| 109 |
+
|
| 110 |
+
- ``price`` the price at which the order is going to be executed in
|
| 111 |
+
the ``ago`` bar
|
| 112 |
+
|
| 113 |
+
- ``ago``: index meant to be used with ``order.data`` for the
|
| 114 |
+
extraction of the *ohlc* and *volume* prices. In most cases this
|
| 115 |
+
will be ``0`` but on a corner case for ``Close`` orders, this
|
| 116 |
+
will be ``-1``.
|
| 117 |
+
|
| 118 |
+
In order to get the bar volume (for example) do: ``volume =
|
| 119 |
+
order.data.voluume[ago]``
|
| 120 |
+
|
| 121 |
+
The callable must return the *executed size* (a value >= 0)
|
| 122 |
+
|
| 123 |
+
The callable may of course be an object with ``__call__`` matching
|
| 124 |
+
the aforementioned signature
|
| 125 |
+
|
| 126 |
+
With the default ``None`` orders will be completely executed in a
|
| 127 |
+
single shot
|
| 128 |
+
|
| 129 |
+
- ``slip_perc`` (default: ``0.0``) Percentage in absolute termns (and
|
| 130 |
+
positive) that should be used to slip prices up/down for buy/sell
|
| 131 |
+
orders
|
| 132 |
+
|
| 133 |
+
Note:
|
| 134 |
+
|
| 135 |
+
- ``0.01`` is ``1%``
|
| 136 |
+
|
| 137 |
+
- ``0.001`` is ``0.1%``
|
| 138 |
+
|
| 139 |
+
- ``slip_fixed`` (default: ``0.0``) Percentage in units (and positive)
|
| 140 |
+
that should be used to slip prices up/down for buy/sell orders
|
| 141 |
+
|
| 142 |
+
Note: if ``slip_perc`` is non zero, it takes precendence over this.
|
| 143 |
+
|
| 144 |
+
- ``slip_open`` (default: ``False``) whether to slip prices for order
|
| 145 |
+
execution which would specifically used the *opening* price of the
|
| 146 |
+
next bar. An example would be ``Market`` order which is executed with
|
| 147 |
+
the next available tick, i.e: the opening price of the bar.
|
| 148 |
+
|
| 149 |
+
This also applies to some of the other executions, because the logic
|
| 150 |
+
tries to detect if the *opening* price would match the requested
|
| 151 |
+
price/execution type when moving to a new bar.
|
| 152 |
+
|
| 153 |
+
- ``slip_match`` (default: ``True``)
|
| 154 |
+
|
| 155 |
+
If ``True`` the broker will offer a match by capping slippage at
|
| 156 |
+
``high/low`` prices in case they would be exceeded.
|
| 157 |
+
|
| 158 |
+
If ``False`` the broker will not match the order with the current
|
| 159 |
+
prices and will try execution during the next iteration
|
| 160 |
+
|
| 161 |
+
- ``slip_limit`` (default: ``True``)
|
| 162 |
+
|
| 163 |
+
``Limit`` orders, given the exact match price requested, will be
|
| 164 |
+
matched even if ``slip_match`` is ``False``.
|
| 165 |
+
|
| 166 |
+
This option controls that behavior.
|
| 167 |
+
|
| 168 |
+
If ``True``, then ``Limit`` orders will be matched by capping prices
|
| 169 |
+
to the ``limit`` / ``high/low`` prices
|
| 170 |
+
|
| 171 |
+
If ``False`` and slippage exceeds the cap, then there will be no
|
| 172 |
+
match
|
| 173 |
+
|
| 174 |
+
- ``slip_out`` (default: ``False``)
|
| 175 |
+
|
| 176 |
+
Provide *slippage* even if the price falls outside the ``high`` -
|
| 177 |
+
``low`` range.
|
| 178 |
+
|
| 179 |
+
- ``coc`` (default: ``False``)
|
| 180 |
+
|
| 181 |
+
*Cheat-On-Close* Setting this to ``True`` with ``set_coc`` enables
|
| 182 |
+
matching a ``Market`` order to the closing price of the bar in which
|
| 183 |
+
the order was issued. This is actually *cheating*, because the bar
|
| 184 |
+
is *closed* and any order should first be matched against the prices
|
| 185 |
+
in the next bar
|
| 186 |
+
|
| 187 |
+
- ``coo`` (default: ``False``)
|
| 188 |
+
|
| 189 |
+
*Cheat-On-Open* Setting this to ``True`` with ``set_coo`` enables
|
| 190 |
+
matching a ``Market`` order to the opening price, by for example
|
| 191 |
+
using a timer with ``cheat`` set to ``True``, because such a timer
|
| 192 |
+
gets executed before the broker has evaluated
|
| 193 |
+
|
| 194 |
+
- ``int2pnl`` (default: ``True``)
|
| 195 |
+
|
| 196 |
+
Assign generated interest (if any) to the profit and loss of
|
| 197 |
+
operation that reduces a position (be it long or short). There may be
|
| 198 |
+
cases in which this is undesired, because different strategies are
|
| 199 |
+
competing and the interest would be assigned on a non-deterministic
|
| 200 |
+
basis to any of them.
|
| 201 |
+
|
| 202 |
+
- ``shortcash`` (default: ``True``)
|
| 203 |
+
|
| 204 |
+
If True then cash will be increased when a stocklike asset is shorted
|
| 205 |
+
and the calculated value for the asset will be negative.
|
| 206 |
+
|
| 207 |
+
If ``False`` then the cash will be deducted as operation cost and the
|
| 208 |
+
calculated value will be positive to end up with the same amount
|
| 209 |
+
|
| 210 |
+
- ``fundstartval`` (default: ``100.0``)
|
| 211 |
+
|
| 212 |
+
This parameter controls the start value for measuring the performance
|
| 213 |
+
in a fund-like way, i.e.: cash can be added and deducted increasing
|
| 214 |
+
the amount of shares. Performance is not measured using the net
|
| 215 |
+
asset value of the porftoflio but using the value of the fund
|
| 216 |
+
|
| 217 |
+
- ``fundmode`` (default: ``False``)
|
| 218 |
+
|
| 219 |
+
If this is set to ``True`` analyzers like ``TimeReturn`` can
|
| 220 |
+
automatically calculate returns based on the fund value and not on
|
| 221 |
+
the total net asset value
|
| 222 |
+
|
| 223 |
+
'''
|
| 224 |
+
params = (
|
| 225 |
+
('cash', 10000.0),
|
| 226 |
+
('checksubmit', True),
|
| 227 |
+
('eosbar', False),
|
| 228 |
+
('filler', None),
|
| 229 |
+
# slippage options
|
| 230 |
+
('slip_perc', 0.0),
|
| 231 |
+
('slip_fixed', 0.0),
|
| 232 |
+
('slip_open', False),
|
| 233 |
+
('slip_match', True),
|
| 234 |
+
('slip_limit', True),
|
| 235 |
+
('slip_out', False),
|
| 236 |
+
('coc', False),
|
| 237 |
+
('coo', False),
|
| 238 |
+
('int2pnl', True),
|
| 239 |
+
('shortcash', True),
|
| 240 |
+
('fundstartval', 100.0),
|
| 241 |
+
('fundmode', False),
|
| 242 |
+
)
|
| 243 |
+
|
| 244 |
+
def __init__(self):
|
| 245 |
+
super(BackBroker, self).__init__()
|
| 246 |
+
self._userhist = []
|
| 247 |
+
self._fundhist = []
|
| 248 |
+
# share_value, net asset value
|
| 249 |
+
self._fhistlast = [float('NaN'), float('NaN')]
|
| 250 |
+
|
| 251 |
+
def init(self):
|
| 252 |
+
super(BackBroker, self).init()
|
| 253 |
+
self.startingcash = self.cash = self.p.cash
|
| 254 |
+
self._value = self.cash
|
| 255 |
+
self._valuemkt = 0.0 # no open position
|
| 256 |
+
|
| 257 |
+
self._valuelever = 0.0 # no open position
|
| 258 |
+
self._valuemktlever = 0.0 # no open position
|
| 259 |
+
|
| 260 |
+
self._leverage = 1.0 # initially nothing is open
|
| 261 |
+
self._unrealized = 0.0 # no open position
|
| 262 |
+
|
| 263 |
+
self.orders = list() # will only be appending
|
| 264 |
+
self.pending = collections.deque() # popleft and append(right)
|
| 265 |
+
self._toactivate = collections.deque() # to activate in next cycle
|
| 266 |
+
|
| 267 |
+
self.positions = collections.defaultdict(Position)
|
| 268 |
+
self.d_credit = collections.defaultdict(float) # credit per data
|
| 269 |
+
self.notifs = collections.deque()
|
| 270 |
+
|
| 271 |
+
self.submitted = collections.deque()
|
| 272 |
+
|
| 273 |
+
# to keep dependent orders if needed
|
| 274 |
+
self._pchildren = collections.defaultdict(collections.deque)
|
| 275 |
+
|
| 276 |
+
self._ocos = dict()
|
| 277 |
+
self._ocol = collections.defaultdict(list)
|
| 278 |
+
|
| 279 |
+
self._fundval = self.p.fundstartval
|
| 280 |
+
self._fundshares = self.p.cash / self._fundval
|
| 281 |
+
self._cash_addition = collections.deque()
|
| 282 |
+
|
| 283 |
+
def get_notification(self):
|
| 284 |
+
try:
|
| 285 |
+
return self.notifs.popleft()
|
| 286 |
+
except IndexError:
|
| 287 |
+
pass
|
| 288 |
+
|
| 289 |
+
return None
|
| 290 |
+
|
| 291 |
+
def set_fundmode(self, fundmode, fundstartval=None):
|
| 292 |
+
'''Set the actual fundmode (True or False)
|
| 293 |
+
|
| 294 |
+
If the argument fundstartval is not ``None``, it will used
|
| 295 |
+
'''
|
| 296 |
+
self.p.fundmode = fundmode
|
| 297 |
+
if fundstartval is not None:
|
| 298 |
+
self.set_fundstartval(fundstartval)
|
| 299 |
+
|
| 300 |
+
def get_fundmode(self):
|
| 301 |
+
'''Returns the actual fundmode (True or False)'''
|
| 302 |
+
return self.p.fundmode
|
| 303 |
+
|
| 304 |
+
fundmode = property(get_fundmode, set_fundmode)
|
| 305 |
+
|
| 306 |
+
def set_fundstartval(self, fundstartval):
|
| 307 |
+
'''Set the starting value of the fund-like performance tracker'''
|
| 308 |
+
self.p.fundstartval = fundstartval
|
| 309 |
+
|
| 310 |
+
def set_int2pnl(self, int2pnl):
|
| 311 |
+
'''Configure assignment of interest to profit and loss'''
|
| 312 |
+
self.p.int2pnl = int2pnl
|
| 313 |
+
|
| 314 |
+
def set_coc(self, coc):
|
| 315 |
+
'''Configure the Cheat-On-Close method to buy the close on order bar'''
|
| 316 |
+
self.p.coc = coc
|
| 317 |
+
|
| 318 |
+
def set_coo(self, coo):
|
| 319 |
+
'''Configure the Cheat-On-Open method to buy the close on order bar'''
|
| 320 |
+
self.p.coo = coo
|
| 321 |
+
|
| 322 |
+
def set_shortcash(self, shortcash):
|
| 323 |
+
'''Configure the shortcash parameters'''
|
| 324 |
+
self.p.shortcash = shortcash
|
| 325 |
+
|
| 326 |
+
def set_slippage_perc(self, perc,
|
| 327 |
+
slip_open=True, slip_limit=True,
|
| 328 |
+
slip_match=True, slip_out=False):
|
| 329 |
+
'''Configure slippage to be percentage based'''
|
| 330 |
+
self.p.slip_perc = perc
|
| 331 |
+
self.p.slip_fixed = 0.0
|
| 332 |
+
self.p.slip_open = slip_open
|
| 333 |
+
self.p.slip_limit = slip_limit
|
| 334 |
+
self.p.slip_match = slip_match
|
| 335 |
+
self.p.slip_out = slip_out
|
| 336 |
+
|
| 337 |
+
def set_slippage_fixed(self, fixed,
|
| 338 |
+
slip_open=True, slip_limit=True,
|
| 339 |
+
slip_match=True, slip_out=False):
|
| 340 |
+
'''Configure slippage to be fixed points based'''
|
| 341 |
+
self.p.slip_perc = 0.0
|
| 342 |
+
self.p.slip_fixed = fixed
|
| 343 |
+
self.p.slip_open = slip_open
|
| 344 |
+
self.p.slip_limit = slip_limit
|
| 345 |
+
self.p.slip_match = slip_match
|
| 346 |
+
self.p.slip_out = slip_out
|
| 347 |
+
|
| 348 |
+
def set_filler(self, filler):
|
| 349 |
+
'''Sets a volume filler for volume filling execution'''
|
| 350 |
+
self.p.filler = filler
|
| 351 |
+
|
| 352 |
+
def set_checksubmit(self, checksubmit):
|
| 353 |
+
'''Sets the checksubmit parameter'''
|
| 354 |
+
self.p.checksubmit = checksubmit
|
| 355 |
+
|
| 356 |
+
def set_eosbar(self, eosbar):
|
| 357 |
+
'''Sets the eosbar parameter (alias: ``seteosbar``'''
|
| 358 |
+
self.p.eosbar = eosbar
|
| 359 |
+
|
| 360 |
+
seteosbar = set_eosbar
|
| 361 |
+
|
| 362 |
+
def get_cash(self):
|
| 363 |
+
'''Returns the current cash (alias: ``getcash``)'''
|
| 364 |
+
return self.cash
|
| 365 |
+
|
| 366 |
+
getcash = get_cash
|
| 367 |
+
|
| 368 |
+
def set_cash(self, cash):
|
| 369 |
+
'''Sets the cash parameter (alias: ``setcash``)'''
|
| 370 |
+
self.startingcash = self.cash = self.p.cash = cash
|
| 371 |
+
self._value = cash
|
| 372 |
+
|
| 373 |
+
setcash = set_cash
|
| 374 |
+
|
| 375 |
+
def add_cash(self, cash):
|
| 376 |
+
'''Add/Remove cash to the system (use a negative value to remove)'''
|
| 377 |
+
self._cash_addition.append(cash)
|
| 378 |
+
|
| 379 |
+
def get_fundshares(self):
|
| 380 |
+
'''Returns the current number of shares in the fund-like mode'''
|
| 381 |
+
return self._fundshares
|
| 382 |
+
|
| 383 |
+
fundshares = property(get_fundshares)
|
| 384 |
+
|
| 385 |
+
def get_fundvalue(self):
|
| 386 |
+
'''Returns the Fund-like share value'''
|
| 387 |
+
return self._fundval
|
| 388 |
+
|
| 389 |
+
fundvalue = property(get_fundvalue)
|
| 390 |
+
|
| 391 |
+
def cancel(self, order, bracket=False):
|
| 392 |
+
try:
|
| 393 |
+
self.pending.remove(order)
|
| 394 |
+
except ValueError:
|
| 395 |
+
# If the list didn't have the element we didn't cancel anything
|
| 396 |
+
return False
|
| 397 |
+
|
| 398 |
+
order.cancel()
|
| 399 |
+
self.notify(order)
|
| 400 |
+
self._ococheck(order)
|
| 401 |
+
if not bracket:
|
| 402 |
+
self._bracketize(order, cancel=True)
|
| 403 |
+
return True
|
| 404 |
+
|
| 405 |
+
def get_value(self, datas=None, mkt=False, lever=False):
|
| 406 |
+
'''Returns the portfolio value of the given datas (if datas is ``None``, then
|
| 407 |
+
the total portfolio value will be returned (alias: ``getvalue``)
|
| 408 |
+
'''
|
| 409 |
+
if datas is None:
|
| 410 |
+
if mkt:
|
| 411 |
+
return self._valuemkt if not lever else self._valuemktlever
|
| 412 |
+
|
| 413 |
+
return self._value if not lever else self._valuelever
|
| 414 |
+
|
| 415 |
+
return self._get_value(datas=datas, lever=lever)
|
| 416 |
+
|
| 417 |
+
getvalue = get_value
|
| 418 |
+
|
| 419 |
+
def get_value_lever(self, datas=None, mkt=False):
|
| 420 |
+
return self.get_value(datas=datas, mkt=mkt)
|
| 421 |
+
|
| 422 |
+
def _get_value(self, datas=None, lever=False):
|
| 423 |
+
pos_value = 0.0
|
| 424 |
+
pos_value_unlever = 0.0
|
| 425 |
+
unrealized = 0.0
|
| 426 |
+
|
| 427 |
+
while self._cash_addition:
|
| 428 |
+
c = self._cash_addition.popleft()
|
| 429 |
+
self._fundshares += c / self._fundval
|
| 430 |
+
self.cash += c
|
| 431 |
+
|
| 432 |
+
for data in datas or self.positions:
|
| 433 |
+
comminfo = self.getcommissioninfo(data)
|
| 434 |
+
position = self.positions[data]
|
| 435 |
+
# use valuesize: returns raw value, rather than negative adj val
|
| 436 |
+
if not self.p.shortcash:
|
| 437 |
+
dvalue = comminfo.getvalue(position, data.close[0])
|
| 438 |
+
else:
|
| 439 |
+
dvalue = comminfo.getvaluesize(position.size, data.close[0])
|
| 440 |
+
|
| 441 |
+
dunrealized = comminfo.profitandloss(position.size, position.price,
|
| 442 |
+
data.close[0])
|
| 443 |
+
if datas and len(datas) == 1:
|
| 444 |
+
if lever and dvalue > 0:
|
| 445 |
+
dvalue -= dunrealized
|
| 446 |
+
return (dvalue / comminfo.get_leverage()) + dunrealized
|
| 447 |
+
return dvalue # raw data value requested, short selling is neg
|
| 448 |
+
|
| 449 |
+
if not self.p.shortcash:
|
| 450 |
+
dvalue = abs(dvalue) # short selling adds value in this case
|
| 451 |
+
|
| 452 |
+
pos_value += dvalue
|
| 453 |
+
unrealized += dunrealized
|
| 454 |
+
|
| 455 |
+
if dvalue > 0: # long position - unlever
|
| 456 |
+
dvalue -= dunrealized
|
| 457 |
+
pos_value_unlever += (dvalue / comminfo.get_leverage())
|
| 458 |
+
pos_value_unlever += dunrealized
|
| 459 |
+
else:
|
| 460 |
+
pos_value_unlever += dvalue
|
| 461 |
+
|
| 462 |
+
if not self._fundhist:
|
| 463 |
+
self._value = v = self.cash + pos_value_unlever
|
| 464 |
+
self._fundval = self._value / self._fundshares # update fundvalue
|
| 465 |
+
else:
|
| 466 |
+
# Try to fetch a value
|
| 467 |
+
fval, fvalue = self._process_fund_history()
|
| 468 |
+
|
| 469 |
+
self._value = fvalue
|
| 470 |
+
self.cash = fvalue - pos_value_unlever
|
| 471 |
+
self._fundval = fval
|
| 472 |
+
self._fundshares = fvalue / fval
|
| 473 |
+
lev = pos_value / (pos_value_unlever or 1.0)
|
| 474 |
+
|
| 475 |
+
# update the calculated values above to the historical values
|
| 476 |
+
pos_value_unlever = fvalue
|
| 477 |
+
pos_value = fvalue * lev
|
| 478 |
+
|
| 479 |
+
self._valuemkt = pos_value_unlever
|
| 480 |
+
|
| 481 |
+
self._valuelever = self.cash + pos_value
|
| 482 |
+
self._valuemktlever = pos_value
|
| 483 |
+
|
| 484 |
+
self._leverage = pos_value / (pos_value_unlever or 1.0)
|
| 485 |
+
self._unrealized = unrealized
|
| 486 |
+
|
| 487 |
+
return self._value if not lever else self._valuelever
|
| 488 |
+
|
| 489 |
+
def get_leverage(self):
|
| 490 |
+
return self._leverage
|
| 491 |
+
|
| 492 |
+
def get_orders_open(self, safe=False):
|
| 493 |
+
'''Returns an iterable with the orders which are still open (either not
|
| 494 |
+
executed or partially executed
|
| 495 |
+
|
| 496 |
+
The orders returned must not be touched.
|
| 497 |
+
|
| 498 |
+
If order manipulation is needed, set the parameter ``safe`` to True
|
| 499 |
+
'''
|
| 500 |
+
if safe:
|
| 501 |
+
os = [x.clone() for x in self.pending]
|
| 502 |
+
else:
|
| 503 |
+
os = [x for x in self.pending]
|
| 504 |
+
|
| 505 |
+
return os
|
| 506 |
+
|
| 507 |
+
def getposition(self, data):
|
| 508 |
+
'''Returns the current position status (a ``Position`` instance) for
|
| 509 |
+
the given ``data``'''
|
| 510 |
+
return self.positions[data]
|
| 511 |
+
|
| 512 |
+
def orderstatus(self, order):
|
| 513 |
+
try:
|
| 514 |
+
o = self.orders.index(order)
|
| 515 |
+
except ValueError:
|
| 516 |
+
o = order
|
| 517 |
+
|
| 518 |
+
return o.status
|
| 519 |
+
|
| 520 |
+
def _take_children(self, order):
|
| 521 |
+
oref = order.ref
|
| 522 |
+
pref = getattr(order.parent, 'ref', oref) # parent ref or self
|
| 523 |
+
|
| 524 |
+
if oref != pref:
|
| 525 |
+
if pref not in self._pchildren:
|
| 526 |
+
order.reject() # parent not there - may have been rejected
|
| 527 |
+
self.notify(order) # reject child, notify
|
| 528 |
+
return None
|
| 529 |
+
|
| 530 |
+
return pref
|
| 531 |
+
|
| 532 |
+
def submit(self, order, check=True):
|
| 533 |
+
pref = self._take_children(order)
|
| 534 |
+
if pref is None: # order has not been taken
|
| 535 |
+
return order
|
| 536 |
+
|
| 537 |
+
pc = self._pchildren[pref]
|
| 538 |
+
pc.append(order) # store in parent/children queue
|
| 539 |
+
|
| 540 |
+
if order.transmit: # if single order, sent and queue cleared
|
| 541 |
+
# if parent-child, the parent will be sent, the other kept
|
| 542 |
+
rets = [self.transmit(x, check=check) for x in pc]
|
| 543 |
+
return rets[-1] # last one is the one triggering transmission
|
| 544 |
+
|
| 545 |
+
return order
|
| 546 |
+
|
| 547 |
+
def transmit(self, order, check=True):
|
| 548 |
+
if check and self.p.checksubmit:
|
| 549 |
+
order.submit()
|
| 550 |
+
self.submitted.append(order)
|
| 551 |
+
self.orders.append(order)
|
| 552 |
+
self.notify(order)
|
| 553 |
+
else:
|
| 554 |
+
self.submit_accept(order)
|
| 555 |
+
|
| 556 |
+
return order
|
| 557 |
+
|
| 558 |
+
def check_submitted(self):
|
| 559 |
+
cash = self.cash
|
| 560 |
+
positions = dict()
|
| 561 |
+
|
| 562 |
+
while self.submitted:
|
| 563 |
+
order = self.submitted.popleft()
|
| 564 |
+
|
| 565 |
+
if self._take_children(order) is None: # children not taken
|
| 566 |
+
continue
|
| 567 |
+
|
| 568 |
+
comminfo = self.getcommissioninfo(order.data)
|
| 569 |
+
|
| 570 |
+
position = positions.setdefault(
|
| 571 |
+
order.data, self.positions[order.data].clone())
|
| 572 |
+
|
| 573 |
+
# pseudo-execute the order to get the remaining cash after exec
|
| 574 |
+
cash = self._execute(order, cash=cash, position=position)
|
| 575 |
+
|
| 576 |
+
if cash >= 0.0:
|
| 577 |
+
self.submit_accept(order)
|
| 578 |
+
continue
|
| 579 |
+
|
| 580 |
+
order.margin()
|
| 581 |
+
self.notify(order)
|
| 582 |
+
self._ococheck(order)
|
| 583 |
+
self._bracketize(order, cancel=True)
|
| 584 |
+
|
| 585 |
+
def submit_accept(self, order):
|
| 586 |
+
order.pannotated = None
|
| 587 |
+
order.submit()
|
| 588 |
+
order.accept()
|
| 589 |
+
self.pending.append(order)
|
| 590 |
+
self.notify(order)
|
| 591 |
+
|
| 592 |
+
def _bracketize(self, order, cancel=False):
|
| 593 |
+
oref = order.ref
|
| 594 |
+
pref = getattr(order.parent, 'ref', oref)
|
| 595 |
+
parent = oref == pref
|
| 596 |
+
|
| 597 |
+
pc = self._pchildren[pref] # defdict - guaranteed
|
| 598 |
+
if cancel or not parent: # cancel left or child exec -> cancel other
|
| 599 |
+
while pc:
|
| 600 |
+
self.cancel(pc.popleft(), bracket=True) # idempotent
|
| 601 |
+
|
| 602 |
+
del self._pchildren[pref] # defdict guaranteed
|
| 603 |
+
|
| 604 |
+
else: # not cancel -> parent exec'd
|
| 605 |
+
pc.popleft() # remove parent
|
| 606 |
+
for o in pc: # activate childnre
|
| 607 |
+
self._toactivate.append(o)
|
| 608 |
+
|
| 609 |
+
def _ococheck(self, order):
|
| 610 |
+
# ocoref = self._ocos[order.ref] or order.ref # a parent or self
|
| 611 |
+
parentref = self._ocos[order.ref]
|
| 612 |
+
ocoref = self._ocos.get(parentref, None)
|
| 613 |
+
ocol = self._ocol.pop(ocoref, None)
|
| 614 |
+
if ocol:
|
| 615 |
+
for i in range(len(self.pending) - 1, -1, -1):
|
| 616 |
+
o = self.pending[i]
|
| 617 |
+
if o is not None and o.ref in ocol:
|
| 618 |
+
del self.pending[i]
|
| 619 |
+
o.cancel()
|
| 620 |
+
self.notify(o)
|
| 621 |
+
|
| 622 |
+
def _ocoize(self, order, oco):
|
| 623 |
+
oref = order.ref
|
| 624 |
+
if oco is None:
|
| 625 |
+
self._ocos[oref] = oref # current order is parent
|
| 626 |
+
self._ocol[oref].append(oref) # create ocogroup
|
| 627 |
+
else:
|
| 628 |
+
ocoref = self._ocos[oco.ref] # ref to group leader
|
| 629 |
+
self._ocos[oref] = ocoref # ref to group leader
|
| 630 |
+
self._ocol[ocoref].append(oref) # add to group
|
| 631 |
+
|
| 632 |
+
def add_order_history(self, orders, notify=True):
|
| 633 |
+
oiter = iter(orders)
|
| 634 |
+
o = next(oiter, None)
|
| 635 |
+
self._userhist.append([o, oiter, notify])
|
| 636 |
+
|
| 637 |
+
def set_fund_history(self, fund):
|
| 638 |
+
# iterable with the following pro item
|
| 639 |
+
# [datetime, share_value, net asset value]
|
| 640 |
+
fiter = iter(fund)
|
| 641 |
+
f = list(next(fiter)) # must not be empty
|
| 642 |
+
self._fundhist = [f, fiter]
|
| 643 |
+
# self._fhistlast = f[1:]
|
| 644 |
+
|
| 645 |
+
self.set_cash(float(f[2]))
|
| 646 |
+
|
| 647 |
+
def buy(self, owner, data,
|
| 648 |
+
size, price=None, plimit=None,
|
| 649 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 650 |
+
trailamount=None, trailpercent=None,
|
| 651 |
+
parent=None, transmit=True,
|
| 652 |
+
histnotify=False, _checksubmit=True,
|
| 653 |
+
**kwargs):
|
| 654 |
+
|
| 655 |
+
order = BuyOrder(owner=owner, data=data,
|
| 656 |
+
size=size, price=price, pricelimit=plimit,
|
| 657 |
+
exectype=exectype, valid=valid, tradeid=tradeid,
|
| 658 |
+
trailamount=trailamount, trailpercent=trailpercent,
|
| 659 |
+
parent=parent, transmit=transmit,
|
| 660 |
+
histnotify=histnotify)
|
| 661 |
+
|
| 662 |
+
order.addinfo(**kwargs)
|
| 663 |
+
self._ocoize(order, oco)
|
| 664 |
+
|
| 665 |
+
return self.submit(order, check=_checksubmit)
|
| 666 |
+
|
| 667 |
+
def sell(self, owner, data,
|
| 668 |
+
size, price=None, plimit=None,
|
| 669 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 670 |
+
trailamount=None, trailpercent=None,
|
| 671 |
+
parent=None, transmit=True,
|
| 672 |
+
histnotify=False, _checksubmit=True,
|
| 673 |
+
**kwargs):
|
| 674 |
+
|
| 675 |
+
order = SellOrder(owner=owner, data=data,
|
| 676 |
+
size=size, price=price, pricelimit=plimit,
|
| 677 |
+
exectype=exectype, valid=valid, tradeid=tradeid,
|
| 678 |
+
trailamount=trailamount, trailpercent=trailpercent,
|
| 679 |
+
parent=parent, transmit=transmit,
|
| 680 |
+
histnotify=histnotify)
|
| 681 |
+
|
| 682 |
+
order.addinfo(**kwargs)
|
| 683 |
+
self._ocoize(order, oco)
|
| 684 |
+
|
| 685 |
+
return self.submit(order, check=_checksubmit)
|
| 686 |
+
|
| 687 |
+
def _execute(self, order, ago=None, price=None, cash=None, position=None,
|
| 688 |
+
dtcoc=None):
|
| 689 |
+
# ago = None is used a flag for pseudo execution
|
| 690 |
+
if ago is not None and price is None:
|
| 691 |
+
return # no psuedo exec no price - no execution
|
| 692 |
+
|
| 693 |
+
if self.p.filler is None or ago is None:
|
| 694 |
+
# Order gets full size or pseudo-execution
|
| 695 |
+
size = order.executed.remsize
|
| 696 |
+
else:
|
| 697 |
+
# Execution depends on volume filler
|
| 698 |
+
size = self.p.filler(order, price, ago)
|
| 699 |
+
if not order.isbuy():
|
| 700 |
+
size = -size
|
| 701 |
+
|
| 702 |
+
# Get comminfo object for the data
|
| 703 |
+
comminfo = self.getcommissioninfo(order.data)
|
| 704 |
+
|
| 705 |
+
# Check if something has to be compensated
|
| 706 |
+
if order.data._compensate is not None:
|
| 707 |
+
data = order.data._compensate
|
| 708 |
+
cinfocomp = self.getcommissioninfo(data) # for actual commission
|
| 709 |
+
else:
|
| 710 |
+
data = order.data
|
| 711 |
+
cinfocomp = comminfo
|
| 712 |
+
|
| 713 |
+
# Adjust position with operation size
|
| 714 |
+
if ago is not None:
|
| 715 |
+
# Real execution with date
|
| 716 |
+
position = self.positions[data]
|
| 717 |
+
pprice_orig = position.price
|
| 718 |
+
|
| 719 |
+
psize, pprice, opened, closed = position.pseudoupdate(size, price)
|
| 720 |
+
|
| 721 |
+
# if part/all of a position has been closed, then there has been
|
| 722 |
+
# a profitandloss ... record it
|
| 723 |
+
pnl = comminfo.profitandloss(-closed, pprice_orig, price)
|
| 724 |
+
cash = self.cash
|
| 725 |
+
else:
|
| 726 |
+
pnl = 0
|
| 727 |
+
if not self.p.coo:
|
| 728 |
+
price = pprice_orig = order.created.price
|
| 729 |
+
else:
|
| 730 |
+
# When doing cheat on open, the price to be considered for a
|
| 731 |
+
# market order is the opening price and not the default closing
|
| 732 |
+
# price with which the order was created
|
| 733 |
+
if order.exectype == Order.Market:
|
| 734 |
+
price = pprice_orig = order.data.open[0]
|
| 735 |
+
else:
|
| 736 |
+
price = pprice_orig = order.created.price
|
| 737 |
+
|
| 738 |
+
psize, pprice, opened, closed = position.update(size, price)
|
| 739 |
+
|
| 740 |
+
# "Closing" totally or partially is possible. Cash may be re-injected
|
| 741 |
+
if closed:
|
| 742 |
+
# Adjust to returned value for closed items & acquired opened items
|
| 743 |
+
if self.p.shortcash:
|
| 744 |
+
closedvalue = comminfo.getvaluesize(-closed, pprice_orig)
|
| 745 |
+
else:
|
| 746 |
+
closedvalue = comminfo.getoperationcost(closed, pprice_orig)
|
| 747 |
+
|
| 748 |
+
closecash = closedvalue
|
| 749 |
+
if closedvalue > 0: # long position closed
|
| 750 |
+
closecash /= comminfo.get_leverage() # inc cash with lever
|
| 751 |
+
|
| 752 |
+
cash += closecash + pnl * comminfo.stocklike
|
| 753 |
+
# Calculate and substract commission
|
| 754 |
+
closedcomm = comminfo.getcommission(closed, price)
|
| 755 |
+
cash -= closedcomm
|
| 756 |
+
|
| 757 |
+
if ago is not None:
|
| 758 |
+
# Cashadjust closed contracts: prev close vs exec price
|
| 759 |
+
# The operation can inject or take cash out
|
| 760 |
+
cash += comminfo.cashadjust(-closed,
|
| 761 |
+
position.adjbase,
|
| 762 |
+
price)
|
| 763 |
+
|
| 764 |
+
# Update system cash
|
| 765 |
+
self.cash = cash
|
| 766 |
+
else:
|
| 767 |
+
closedvalue = closedcomm = 0.0
|
| 768 |
+
|
| 769 |
+
popened = opened
|
| 770 |
+
if opened:
|
| 771 |
+
if self.p.shortcash:
|
| 772 |
+
openedvalue = comminfo.getvaluesize(opened, price)
|
| 773 |
+
else:
|
| 774 |
+
openedvalue = comminfo.getoperationcost(opened, price)
|
| 775 |
+
|
| 776 |
+
opencash = openedvalue
|
| 777 |
+
if openedvalue > 0: # long position being opened
|
| 778 |
+
opencash /= comminfo.get_leverage() # dec cash with level
|
| 779 |
+
|
| 780 |
+
cash -= opencash # original behavior
|
| 781 |
+
|
| 782 |
+
openedcomm = cinfocomp.getcommission(opened, price)
|
| 783 |
+
cash -= openedcomm
|
| 784 |
+
|
| 785 |
+
if cash < 0.0:
|
| 786 |
+
# execution is not possible - nullify
|
| 787 |
+
opened = 0
|
| 788 |
+
openedvalue = openedcomm = 0.0
|
| 789 |
+
|
| 790 |
+
elif ago is not None: # real execution
|
| 791 |
+
if abs(psize) > abs(opened):
|
| 792 |
+
# some futures were opened - adjust the cash of the
|
| 793 |
+
# previously existing futures to the operation price and
|
| 794 |
+
# use that as new adjustment base, because it already is
|
| 795 |
+
# for the new futures At the end of the cycle the
|
| 796 |
+
# adjustment to the close price will be done for all open
|
| 797 |
+
# futures from a common base price with regards to the
|
| 798 |
+
# close price
|
| 799 |
+
adjsize = psize - opened
|
| 800 |
+
cash += comminfo.cashadjust(adjsize,
|
| 801 |
+
position.adjbase, price)
|
| 802 |
+
|
| 803 |
+
# record adjust price base for end of bar cash adjustment
|
| 804 |
+
position.adjbase = price
|
| 805 |
+
|
| 806 |
+
# update system cash - checking if opened is still != 0
|
| 807 |
+
self.cash = cash
|
| 808 |
+
else:
|
| 809 |
+
openedvalue = openedcomm = 0.0
|
| 810 |
+
|
| 811 |
+
if ago is None:
|
| 812 |
+
# return cash from pseudo-execution
|
| 813 |
+
return cash
|
| 814 |
+
|
| 815 |
+
execsize = closed + opened
|
| 816 |
+
|
| 817 |
+
if execsize:
|
| 818 |
+
# Confimrm the operation to the comminfo object
|
| 819 |
+
comminfo.confirmexec(execsize, price)
|
| 820 |
+
|
| 821 |
+
# do a real position update if something was executed
|
| 822 |
+
position.update(execsize, price, data.datetime.datetime())
|
| 823 |
+
|
| 824 |
+
if closed and self.p.int2pnl: # Assign accumulated interest data
|
| 825 |
+
closedcomm += self.d_credit.pop(data, 0.0)
|
| 826 |
+
|
| 827 |
+
# Execute and notify the order
|
| 828 |
+
order.execute(dtcoc or data.datetime[ago],
|
| 829 |
+
execsize, price,
|
| 830 |
+
closed, closedvalue, closedcomm,
|
| 831 |
+
opened, openedvalue, openedcomm,
|
| 832 |
+
comminfo.margin, pnl,
|
| 833 |
+
psize, pprice)
|
| 834 |
+
|
| 835 |
+
order.addcomminfo(comminfo)
|
| 836 |
+
|
| 837 |
+
self.notify(order)
|
| 838 |
+
self._ococheck(order)
|
| 839 |
+
|
| 840 |
+
if popened and not opened:
|
| 841 |
+
# opened was not executed - not enough cash
|
| 842 |
+
order.margin()
|
| 843 |
+
self.notify(order)
|
| 844 |
+
self._ococheck(order)
|
| 845 |
+
self._bracketize(order, cancel=True)
|
| 846 |
+
|
| 847 |
+
def notify(self, order):
|
| 848 |
+
self.notifs.append(order.clone())
|
| 849 |
+
|
| 850 |
+
def _try_exec_historical(self, order):
|
| 851 |
+
self._execute(order, ago=0, price=order.created.price)
|
| 852 |
+
|
| 853 |
+
def _try_exec_market(self, order, popen, phigh, plow):
|
| 854 |
+
ago = 0
|
| 855 |
+
if self.p.coc and order.info.get('coc', True):
|
| 856 |
+
dtcoc = order.created.dt
|
| 857 |
+
exprice = order.created.pclose
|
| 858 |
+
else:
|
| 859 |
+
if not self.p.coo and order.data.datetime[0] <= order.created.dt:
|
| 860 |
+
return # can only execute after creation time
|
| 861 |
+
|
| 862 |
+
dtcoc = None
|
| 863 |
+
exprice = popen
|
| 864 |
+
|
| 865 |
+
if order.isbuy():
|
| 866 |
+
p = self._slip_up(phigh, exprice, doslip=self.p.slip_open)
|
| 867 |
+
else:
|
| 868 |
+
p = self._slip_down(plow, exprice, doslip=self.p.slip_open)
|
| 869 |
+
|
| 870 |
+
self._execute(order, ago=0, price=p, dtcoc=dtcoc)
|
| 871 |
+
|
| 872 |
+
def _try_exec_close(self, order, pclose):
|
| 873 |
+
# pannotated allows to keep track of the closing bar if there is no
|
| 874 |
+
# information which lets us know that the current bar is the closing
|
| 875 |
+
# bar (like matching end of session bar)
|
| 876 |
+
# The actual matching will be done one bar afterwards but using the
|
| 877 |
+
# information from the actual closing bar
|
| 878 |
+
|
| 879 |
+
dt0 = order.data.datetime[0]
|
| 880 |
+
# don't use "len" -> in replay the close can be reached with same len
|
| 881 |
+
if dt0 > order.created.dt: # can only execute after creation time
|
| 882 |
+
# or (self.p.eosbar and dt0 == order.dteos):
|
| 883 |
+
if dt0 >= order.dteos:
|
| 884 |
+
# past the end of session or right at it and eosbar is True
|
| 885 |
+
if order.pannotated and dt0 > order.dteos:
|
| 886 |
+
ago = -1
|
| 887 |
+
execprice = order.pannotated
|
| 888 |
+
else:
|
| 889 |
+
ago = 0
|
| 890 |
+
execprice = pclose
|
| 891 |
+
|
| 892 |
+
self._execute(order, ago=ago, price=execprice)
|
| 893 |
+
return
|
| 894 |
+
|
| 895 |
+
# If no exexcution has taken place ... annotate the closing price
|
| 896 |
+
order.pannotated = pclose
|
| 897 |
+
|
| 898 |
+
def _try_exec_limit(self, order, popen, phigh, plow, plimit):
|
| 899 |
+
if order.isbuy():
|
| 900 |
+
if plimit >= popen:
|
| 901 |
+
# open smaller/equal than requested - buy cheaper
|
| 902 |
+
pmax = min(phigh, plimit)
|
| 903 |
+
p = self._slip_up(pmax, popen, doslip=self.p.slip_open,
|
| 904 |
+
lim=True)
|
| 905 |
+
self._execute(order, ago=0, price=p)
|
| 906 |
+
elif plimit >= plow:
|
| 907 |
+
# day low below req price ... match limit price
|
| 908 |
+
self._execute(order, ago=0, price=plimit)
|
| 909 |
+
|
| 910 |
+
else: # Sell
|
| 911 |
+
if plimit <= popen:
|
| 912 |
+
# open greater/equal than requested - sell more expensive
|
| 913 |
+
pmin = max(plow, plimit)
|
| 914 |
+
p = self._slip_down(plimit, popen, doslip=self.p.slip_open,
|
| 915 |
+
lim=True)
|
| 916 |
+
self._execute(order, ago=0, price=p)
|
| 917 |
+
elif plimit <= phigh:
|
| 918 |
+
# day high above req price ... match limit price
|
| 919 |
+
self._execute(order, ago=0, price=plimit)
|
| 920 |
+
|
| 921 |
+
def _try_exec_stop(self, order, popen, phigh, plow, pcreated, pclose):
|
| 922 |
+
if order.isbuy():
|
| 923 |
+
if popen >= pcreated:
|
| 924 |
+
# price penetrated with an open gap - use open
|
| 925 |
+
p = self._slip_up(phigh, popen, doslip=self.p.slip_open)
|
| 926 |
+
self._execute(order, ago=0, price=p)
|
| 927 |
+
elif phigh >= pcreated:
|
| 928 |
+
# price penetrated during the session - use trigger price
|
| 929 |
+
p = self._slip_up(phigh, pcreated)
|
| 930 |
+
self._execute(order, ago=0, price=p)
|
| 931 |
+
|
| 932 |
+
else: # Sell
|
| 933 |
+
if popen <= pcreated:
|
| 934 |
+
# price penetrated with an open gap - use open
|
| 935 |
+
p = self._slip_down(plow, popen, doslip=self.p.slip_open)
|
| 936 |
+
self._execute(order, ago=0, price=p)
|
| 937 |
+
elif plow <= pcreated:
|
| 938 |
+
# price penetrated during the session - use trigger price
|
| 939 |
+
p = self._slip_down(plow, pcreated)
|
| 940 |
+
self._execute(order, ago=0, price=p)
|
| 941 |
+
|
| 942 |
+
# not (completely) executed and trailing stop
|
| 943 |
+
if order.alive() and order.exectype == Order.StopTrail:
|
| 944 |
+
order.trailadjust(pclose)
|
| 945 |
+
|
| 946 |
+
def _try_exec_stoplimit(self, order,
|
| 947 |
+
popen, phigh, plow, pclose,
|
| 948 |
+
pcreated, plimit):
|
| 949 |
+
if order.isbuy():
|
| 950 |
+
if popen >= pcreated:
|
| 951 |
+
order.triggered = True
|
| 952 |
+
self._try_exec_limit(order, popen, phigh, plow, plimit)
|
| 953 |
+
|
| 954 |
+
elif phigh >= pcreated:
|
| 955 |
+
# price penetrated upwards during the session
|
| 956 |
+
order.triggered = True
|
| 957 |
+
# can calculate execution for a few cases - datetime is fixed
|
| 958 |
+
if popen > pclose:
|
| 959 |
+
if plimit >= pcreated: # limit above stop trigger
|
| 960 |
+
p = self._slip_up(phigh, pcreated, lim=True)
|
| 961 |
+
self._execute(order, ago=0, price=p)
|
| 962 |
+
elif plimit >= pclose:
|
| 963 |
+
self._execute(order, ago=0, price=plimit)
|
| 964 |
+
else: # popen < pclose
|
| 965 |
+
if plimit >= pcreated:
|
| 966 |
+
p = self._slip_up(phigh, pcreated, lim=True)
|
| 967 |
+
self._execute(order, ago=0, price=p)
|
| 968 |
+
else: # Sell
|
| 969 |
+
if popen <= pcreated:
|
| 970 |
+
# price penetrated downwards with an open gap
|
| 971 |
+
order.triggered = True
|
| 972 |
+
self._try_exec_limit(order, popen, phigh, plow, plimit)
|
| 973 |
+
|
| 974 |
+
elif plow <= pcreated:
|
| 975 |
+
# price penetrated downwards during the session
|
| 976 |
+
order.triggered = True
|
| 977 |
+
# can calculate execution for a few cases - datetime is fixed
|
| 978 |
+
if popen <= pclose:
|
| 979 |
+
if plimit <= pcreated:
|
| 980 |
+
p = self._slip_down(plow, pcreated, lim=True)
|
| 981 |
+
self._execute(order, ago=0, price=p)
|
| 982 |
+
elif plimit <= pclose:
|
| 983 |
+
self._execute(order, ago=0, price=plimit)
|
| 984 |
+
else:
|
| 985 |
+
# popen > pclose
|
| 986 |
+
if plimit <= pcreated:
|
| 987 |
+
p = self._slip_down(plow, pcreated, lim=True)
|
| 988 |
+
self._execute(order, ago=0, price=p)
|
| 989 |
+
|
| 990 |
+
# not (completely) executed and trailing stop
|
| 991 |
+
if order.alive() and order.exectype == Order.StopTrailLimit:
|
| 992 |
+
order.trailadjust(pclose)
|
| 993 |
+
|
| 994 |
+
def _slip_up(self, pmax, price, doslip=True, lim=False):
|
| 995 |
+
if not doslip:
|
| 996 |
+
return price
|
| 997 |
+
|
| 998 |
+
slip_perc = self.p.slip_perc
|
| 999 |
+
slip_fixed = self.p.slip_fixed
|
| 1000 |
+
if slip_perc:
|
| 1001 |
+
pslip = price * (1 + slip_perc)
|
| 1002 |
+
elif slip_fixed:
|
| 1003 |
+
pslip = price + slip_fixed
|
| 1004 |
+
else:
|
| 1005 |
+
return price
|
| 1006 |
+
|
| 1007 |
+
if pslip <= pmax: # slipping can return price
|
| 1008 |
+
return pslip
|
| 1009 |
+
elif self.p.slip_match or (lim and self.p.slip_limit):
|
| 1010 |
+
if not self.p.slip_out:
|
| 1011 |
+
return pmax
|
| 1012 |
+
|
| 1013 |
+
return pslip # non existent price
|
| 1014 |
+
|
| 1015 |
+
return None # no price can be returned
|
| 1016 |
+
|
| 1017 |
+
def _slip_down(self, pmin, price, doslip=True, lim=False):
|
| 1018 |
+
if not doslip:
|
| 1019 |
+
return price
|
| 1020 |
+
|
| 1021 |
+
slip_perc = self.p.slip_perc
|
| 1022 |
+
slip_fixed = self.p.slip_fixed
|
| 1023 |
+
if slip_perc:
|
| 1024 |
+
pslip = price * (1 - slip_perc)
|
| 1025 |
+
elif slip_fixed:
|
| 1026 |
+
pslip = price - slip_fixed
|
| 1027 |
+
else:
|
| 1028 |
+
return price
|
| 1029 |
+
|
| 1030 |
+
if pslip >= pmin: # slipping can return price
|
| 1031 |
+
return pslip
|
| 1032 |
+
elif self.p.slip_match or (lim and self.p.slip_limit):
|
| 1033 |
+
if not self.p.slip_out:
|
| 1034 |
+
return pmin
|
| 1035 |
+
|
| 1036 |
+
return pslip # non existent price
|
| 1037 |
+
|
| 1038 |
+
return None # no price can be returned
|
| 1039 |
+
|
| 1040 |
+
def _try_exec(self, order):
|
| 1041 |
+
data = order.data
|
| 1042 |
+
|
| 1043 |
+
popen = getattr(data, 'tick_open', None)
|
| 1044 |
+
if popen is None:
|
| 1045 |
+
popen = data.open[0]
|
| 1046 |
+
phigh = getattr(data, 'tick_high', None)
|
| 1047 |
+
if phigh is None:
|
| 1048 |
+
phigh = data.high[0]
|
| 1049 |
+
plow = getattr(data, 'tick_low', None)
|
| 1050 |
+
if plow is None:
|
| 1051 |
+
plow = data.low[0]
|
| 1052 |
+
pclose = getattr(data, 'tick_close', None)
|
| 1053 |
+
if pclose is None:
|
| 1054 |
+
pclose = data.close[0]
|
| 1055 |
+
|
| 1056 |
+
pcreated = order.created.price
|
| 1057 |
+
plimit = order.created.pricelimit
|
| 1058 |
+
|
| 1059 |
+
if order.exectype == Order.Market:
|
| 1060 |
+
self._try_exec_market(order, popen, phigh, plow)
|
| 1061 |
+
|
| 1062 |
+
elif order.exectype == Order.Close:
|
| 1063 |
+
self._try_exec_close(order, pclose)
|
| 1064 |
+
|
| 1065 |
+
elif order.exectype == Order.Limit:
|
| 1066 |
+
self._try_exec_limit(order, popen, phigh, plow, pcreated)
|
| 1067 |
+
|
| 1068 |
+
elif (order.triggered and
|
| 1069 |
+
order.exectype in [Order.StopLimit, Order.StopTrailLimit]):
|
| 1070 |
+
self._try_exec_limit(order, popen, phigh, plow, plimit)
|
| 1071 |
+
|
| 1072 |
+
elif order.exectype in [Order.Stop, Order.StopTrail]:
|
| 1073 |
+
self._try_exec_stop(order, popen, phigh, plow, pcreated, pclose)
|
| 1074 |
+
|
| 1075 |
+
elif order.exectype in [Order.StopLimit, Order.StopTrailLimit]:
|
| 1076 |
+
self._try_exec_stoplimit(order,
|
| 1077 |
+
popen, phigh, plow, pclose,
|
| 1078 |
+
pcreated, plimit)
|
| 1079 |
+
|
| 1080 |
+
elif order.exectype == Order.Historical:
|
| 1081 |
+
self._try_exec_historical(order)
|
| 1082 |
+
|
| 1083 |
+
def _process_fund_history(self):
|
| 1084 |
+
fhist = self._fundhist # [last element, iterator]
|
| 1085 |
+
f, funds = fhist
|
| 1086 |
+
if not f:
|
| 1087 |
+
return self._fhistlast
|
| 1088 |
+
|
| 1089 |
+
dt = f[0] # date/datetime instance
|
| 1090 |
+
if isinstance(dt, string_types):
|
| 1091 |
+
dtfmt = '%Y-%m-%d'
|
| 1092 |
+
if 'T' in dt:
|
| 1093 |
+
dtfmt += 'T%H:%M:%S'
|
| 1094 |
+
if '.' in dt:
|
| 1095 |
+
dtfmt += '.%f'
|
| 1096 |
+
dt = datetime.datetime.strptime(dt, dtfmt)
|
| 1097 |
+
f[0] = dt # update value
|
| 1098 |
+
|
| 1099 |
+
elif isinstance(dt, datetime.datetime):
|
| 1100 |
+
pass
|
| 1101 |
+
elif isinstance(dt, datetime.date):
|
| 1102 |
+
dt = datetime.datetime(year=dt.year, month=dt.month, day=dt.day)
|
| 1103 |
+
f[0] = dt # Update the value
|
| 1104 |
+
|
| 1105 |
+
# Synchronization with the strategy is not possible because the broker
|
| 1106 |
+
# is called before the strategy advances. The 2 lines below would do it
|
| 1107 |
+
# if possible
|
| 1108 |
+
# st0 = self.cerebro.runningstrats[0]
|
| 1109 |
+
# if dt <= st0.datetime.datetime():
|
| 1110 |
+
if dt <= self.cerebro._dtmaster:
|
| 1111 |
+
self._fhistlast = f[1:]
|
| 1112 |
+
fhist[0] = list(next(funds, []))
|
| 1113 |
+
|
| 1114 |
+
return self._fhistlast
|
| 1115 |
+
|
| 1116 |
+
def _process_order_history(self):
|
| 1117 |
+
for uhist in self._userhist:
|
| 1118 |
+
uhorder, uhorders, uhnotify = uhist
|
| 1119 |
+
while uhorder is not None:
|
| 1120 |
+
uhorder = list(uhorder) # to support assignment (if tuple)
|
| 1121 |
+
try:
|
| 1122 |
+
dataidx = uhorder[3] # 2nd field
|
| 1123 |
+
except IndexError:
|
| 1124 |
+
dataidx = None # Field not present, use default
|
| 1125 |
+
|
| 1126 |
+
if dataidx is None:
|
| 1127 |
+
d = self.cerebro.datas[0]
|
| 1128 |
+
elif isinstance(dataidx, integer_types):
|
| 1129 |
+
d = self.cerebro.datas[dataidx]
|
| 1130 |
+
else: # assume string
|
| 1131 |
+
d = self.cerebro.datasbyname[dataidx]
|
| 1132 |
+
|
| 1133 |
+
if not len(d):
|
| 1134 |
+
break # may start later as oter data feeds
|
| 1135 |
+
|
| 1136 |
+
dt = uhorder[0] # date/datetime instance
|
| 1137 |
+
if isinstance(dt, string_types):
|
| 1138 |
+
dtfmt = '%Y-%m-%d'
|
| 1139 |
+
if 'T' in dt:
|
| 1140 |
+
dtfmt += 'T%H:%M:%S'
|
| 1141 |
+
if '.' in dt:
|
| 1142 |
+
dtfmt += '.%f'
|
| 1143 |
+
dt = datetime.datetime.strptime(dt, dtfmt)
|
| 1144 |
+
uhorder[0] = dt
|
| 1145 |
+
elif isinstance(dt, datetime.datetime):
|
| 1146 |
+
pass
|
| 1147 |
+
elif isinstance(dt, datetime.date):
|
| 1148 |
+
dt = datetime.datetime(year=dt.year,
|
| 1149 |
+
month=dt.month,
|
| 1150 |
+
day=dt.day)
|
| 1151 |
+
uhorder[0] = dt
|
| 1152 |
+
|
| 1153 |
+
if dt > d.datetime.datetime():
|
| 1154 |
+
break # cannot execute yet 1st in queue, stop processing
|
| 1155 |
+
|
| 1156 |
+
size = uhorder[1]
|
| 1157 |
+
price = uhorder[2]
|
| 1158 |
+
owner = self.cerebro.runningstrats[0]
|
| 1159 |
+
if size > 0:
|
| 1160 |
+
o = self.buy(owner=owner, data=d,
|
| 1161 |
+
size=size, price=price,
|
| 1162 |
+
exectype=Order.Historical,
|
| 1163 |
+
histnotify=uhnotify,
|
| 1164 |
+
_checksubmit=False)
|
| 1165 |
+
|
| 1166 |
+
elif size < 0:
|
| 1167 |
+
o = self.sell(owner=owner, data=d,
|
| 1168 |
+
size=abs(size), price=price,
|
| 1169 |
+
exectype=Order.Historical,
|
| 1170 |
+
histnotify=uhnotify,
|
| 1171 |
+
_checksubmit=False)
|
| 1172 |
+
|
| 1173 |
+
# update to next potential order
|
| 1174 |
+
uhist[0] = uhorder = next(uhorders, None)
|
| 1175 |
+
|
| 1176 |
+
def next(self):
|
| 1177 |
+
while self._toactivate:
|
| 1178 |
+
self._toactivate.popleft().activate()
|
| 1179 |
+
|
| 1180 |
+
if self.p.checksubmit:
|
| 1181 |
+
self.check_submitted()
|
| 1182 |
+
|
| 1183 |
+
# Discount any cash for positions hold
|
| 1184 |
+
credit = 0.0
|
| 1185 |
+
for data, pos in self.positions.items():
|
| 1186 |
+
if pos:
|
| 1187 |
+
comminfo = self.getcommissioninfo(data)
|
| 1188 |
+
dt0 = data.datetime.datetime()
|
| 1189 |
+
dcredit = comminfo.get_credit_interest(data, pos, dt0)
|
| 1190 |
+
self.d_credit[data] += dcredit
|
| 1191 |
+
credit += dcredit
|
| 1192 |
+
pos.datetime = dt0 # mark last credit operation
|
| 1193 |
+
|
| 1194 |
+
self.cash -= credit
|
| 1195 |
+
|
| 1196 |
+
self._process_order_history()
|
| 1197 |
+
|
| 1198 |
+
# Iterate once over all elements of the pending queue
|
| 1199 |
+
self.pending.append(None)
|
| 1200 |
+
while True:
|
| 1201 |
+
order = self.pending.popleft()
|
| 1202 |
+
if order is None:
|
| 1203 |
+
break
|
| 1204 |
+
|
| 1205 |
+
if order.expire():
|
| 1206 |
+
self.notify(order)
|
| 1207 |
+
self._ococheck(order)
|
| 1208 |
+
self._bracketize(order, cancel=True)
|
| 1209 |
+
|
| 1210 |
+
elif not order.active():
|
| 1211 |
+
self.pending.append(order) # cannot yet be processed
|
| 1212 |
+
|
| 1213 |
+
else:
|
| 1214 |
+
self._try_exec(order)
|
| 1215 |
+
if order.alive():
|
| 1216 |
+
self.pending.append(order)
|
| 1217 |
+
|
| 1218 |
+
elif order.status == Order.Completed:
|
| 1219 |
+
# a bracket parent order may have been executed
|
| 1220 |
+
self._bracketize(order)
|
| 1221 |
+
|
| 1222 |
+
# Operations have been executed ... adjust cash end of bar
|
| 1223 |
+
for data, pos in self.positions.items():
|
| 1224 |
+
# futures change cash every bar
|
| 1225 |
+
if pos:
|
| 1226 |
+
comminfo = self.getcommissioninfo(data)
|
| 1227 |
+
self.cash += comminfo.cashadjust(pos.size,
|
| 1228 |
+
pos.adjbase,
|
| 1229 |
+
data.close[0])
|
| 1230 |
+
# record the last adjustment price
|
| 1231 |
+
pos.adjbase = data.close[0]
|
| 1232 |
+
|
| 1233 |
+
self._get_value() # update value
|
| 1234 |
+
|
| 1235 |
+
|
| 1236 |
+
# Alias
|
| 1237 |
+
BrokerBack = BackBroker
|
backtrader/source/backtrader/brokers/ibbroker.py
ADDED
|
@@ -0,0 +1,575 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
from copy import copy
|
| 26 |
+
from datetime import date, datetime, timedelta
|
| 27 |
+
import threading
|
| 28 |
+
import uuid
|
| 29 |
+
|
| 30 |
+
import ib.ext.Order
|
| 31 |
+
import ib.opt as ibopt
|
| 32 |
+
|
| 33 |
+
from backtrader.feed import DataBase
|
| 34 |
+
from backtrader import (TimeFrame, num2date, date2num, BrokerBase,
|
| 35 |
+
Order, OrderBase, OrderData)
|
| 36 |
+
from backtrader.utils.py3 import bytes, bstr, with_metaclass, queue, MAXFLOAT
|
| 37 |
+
from backtrader.metabase import MetaParams
|
| 38 |
+
from backtrader.comminfo import CommInfoBase
|
| 39 |
+
from backtrader.position import Position
|
| 40 |
+
from backtrader.stores import ibstore
|
| 41 |
+
from backtrader.utils import AutoDict, AutoOrderedDict
|
| 42 |
+
from backtrader.comminfo import CommInfoBase
|
| 43 |
+
|
| 44 |
+
bytes = bstr # py2/3 need for ibpy
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class IBOrderState(object):
|
| 48 |
+
# wraps OrderState object and can print it
|
| 49 |
+
_fields = ['status', 'initMargin', 'maintMargin', 'equityWithLoan',
|
| 50 |
+
'commission', 'minCommission', 'maxCommission',
|
| 51 |
+
'commissionCurrency', 'warningText']
|
| 52 |
+
|
| 53 |
+
def __init__(self, orderstate):
|
| 54 |
+
for f in self._fields:
|
| 55 |
+
fname = 'm_' + f
|
| 56 |
+
setattr(self, fname, getattr(orderstate, fname))
|
| 57 |
+
|
| 58 |
+
def __str__(self):
|
| 59 |
+
txt = list()
|
| 60 |
+
txt.append('--- ORDERSTATE BEGIN')
|
| 61 |
+
for f in self._fields:
|
| 62 |
+
fname = 'm_' + f
|
| 63 |
+
txt.append('{}: {}'.format(f.capitalize(), getattr(self, fname)))
|
| 64 |
+
txt.append('--- ORDERSTATE END')
|
| 65 |
+
return '\n'.join(txt)
|
| 66 |
+
|
| 67 |
+
|
| 68 |
+
class IBOrder(OrderBase, ib.ext.Order.Order):
|
| 69 |
+
'''Subclasses the IBPy order to provide the minimum extra functionality
|
| 70 |
+
needed to be compatible with the internally defined orders
|
| 71 |
+
|
| 72 |
+
Once ``OrderBase`` has processed the parameters, the __init__ method takes
|
| 73 |
+
over to use the parameter values and set the appropriate values in the
|
| 74 |
+
ib.ext.Order.Order object
|
| 75 |
+
|
| 76 |
+
Any extra parameters supplied with kwargs are applied directly to the
|
| 77 |
+
ib.ext.Order.Order object, which could be used as follows::
|
| 78 |
+
|
| 79 |
+
Example: if the 4 order execution types directly supported by
|
| 80 |
+
``backtrader`` are not enough, in the case of for example
|
| 81 |
+
*Interactive Brokers* the following could be passed as *kwargs*::
|
| 82 |
+
|
| 83 |
+
orderType='LIT', lmtPrice=10.0, auxPrice=9.8
|
| 84 |
+
|
| 85 |
+
This would override the settings created by ``backtrader`` and
|
| 86 |
+
generate a ``LIMIT IF TOUCHED`` order with a *touched* price of 9.8
|
| 87 |
+
and a *limit* price of 10.0.
|
| 88 |
+
|
| 89 |
+
This would be done almost always from the ``Buy`` and ``Sell`` methods of
|
| 90 |
+
the ``Strategy`` subclass being used in ``Cerebro``
|
| 91 |
+
'''
|
| 92 |
+
|
| 93 |
+
def __str__(self):
|
| 94 |
+
'''Get the printout from the base class and add some ib.Order specific
|
| 95 |
+
fields'''
|
| 96 |
+
basetxt = super(IBOrder, self).__str__()
|
| 97 |
+
tojoin = [basetxt]
|
| 98 |
+
tojoin.append('Ref: {}'.format(self.ref))
|
| 99 |
+
tojoin.append('orderId: {}'.format(self.m_orderId))
|
| 100 |
+
tojoin.append('Action: {}'.format(self.m_action))
|
| 101 |
+
tojoin.append('Size (ib): {}'.format(self.m_totalQuantity))
|
| 102 |
+
tojoin.append('Lmt Price: {}'.format(self.m_lmtPrice))
|
| 103 |
+
tojoin.append('Aux Price: {}'.format(self.m_auxPrice))
|
| 104 |
+
tojoin.append('OrderType: {}'.format(self.m_orderType))
|
| 105 |
+
tojoin.append('Tif (Time in Force): {}'.format(self.m_tif))
|
| 106 |
+
tojoin.append('GoodTillDate: {}'.format(self.m_goodTillDate))
|
| 107 |
+
return '\n'.join(tojoin)
|
| 108 |
+
|
| 109 |
+
# Map backtrader order types to the ib specifics
|
| 110 |
+
_IBOrdTypes = {
|
| 111 |
+
None: bytes('MKT'), # default
|
| 112 |
+
Order.Market: bytes('MKT'),
|
| 113 |
+
Order.Limit: bytes('LMT'),
|
| 114 |
+
Order.Close: bytes('MOC'),
|
| 115 |
+
Order.Stop: bytes('STP'),
|
| 116 |
+
Order.StopLimit: bytes('STPLMT'),
|
| 117 |
+
Order.StopTrail: bytes('TRAIL'),
|
| 118 |
+
Order.StopTrailLimit: bytes('TRAIL LIMIT'),
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
def __init__(self, action, **kwargs):
|
| 122 |
+
|
| 123 |
+
# Marker to indicate an openOrder has been seen with
|
| 124 |
+
# PendinCancel/Cancelled which is indication of an upcoming
|
| 125 |
+
# cancellation
|
| 126 |
+
self._willexpire = False
|
| 127 |
+
|
| 128 |
+
self.ordtype = self.Buy if action == 'BUY' else self.Sell
|
| 129 |
+
|
| 130 |
+
super(IBOrder, self).__init__()
|
| 131 |
+
ib.ext.Order.Order.__init__(self) # Invoke 2nd base class
|
| 132 |
+
|
| 133 |
+
# Now fill in the specific IB parameters
|
| 134 |
+
self.m_orderType = self._IBOrdTypes[self.exectype]
|
| 135 |
+
self.m_permid = 0
|
| 136 |
+
|
| 137 |
+
# 'B' or 'S' should be enough
|
| 138 |
+
self.m_action = bytes(action)
|
| 139 |
+
|
| 140 |
+
# Set the prices
|
| 141 |
+
self.m_lmtPrice = 0.0
|
| 142 |
+
self.m_auxPrice = 0.0
|
| 143 |
+
|
| 144 |
+
if self.exectype == self.Market: # is it really needed for Market?
|
| 145 |
+
pass
|
| 146 |
+
elif self.exectype == self.Close: # is it ireally needed for Close?
|
| 147 |
+
pass
|
| 148 |
+
elif self.exectype == self.Limit:
|
| 149 |
+
self.m_lmtPrice = self.price
|
| 150 |
+
elif self.exectype == self.Stop:
|
| 151 |
+
self.m_auxPrice = self.price # stop price / exec is market
|
| 152 |
+
elif self.exectype == self.StopLimit:
|
| 153 |
+
self.m_lmtPrice = self.pricelimit # req limit execution
|
| 154 |
+
self.m_auxPrice = self.price # trigger price
|
| 155 |
+
elif self.exectype == self.StopTrail:
|
| 156 |
+
if self.trailamount is not None:
|
| 157 |
+
self.m_auxPrice = self.trailamount
|
| 158 |
+
elif self.trailpercent is not None:
|
| 159 |
+
# value expected in % format ... multiply 100.0
|
| 160 |
+
self.m_trailingPercent = self.trailpercent * 100.0
|
| 161 |
+
elif self.exectype == self.StopTrailLimit:
|
| 162 |
+
self.m_trailStopPrice = self.m_lmtPrice = self.price
|
| 163 |
+
# The limit offset is set relative to the price difference in TWS
|
| 164 |
+
self.m_lmtPrice = self.pricelimit
|
| 165 |
+
if self.trailamount is not None:
|
| 166 |
+
self.m_auxPrice = self.trailamount
|
| 167 |
+
elif self.trailpercent is not None:
|
| 168 |
+
# value expected in % format ... multiply 100.0
|
| 169 |
+
self.m_trailingPercent = self.trailpercent * 100.0
|
| 170 |
+
|
| 171 |
+
self.m_totalQuantity = abs(self.size) # ib takes only positives
|
| 172 |
+
|
| 173 |
+
self.m_transmit = self.transmit
|
| 174 |
+
if self.parent is not None:
|
| 175 |
+
self.m_parentId = self.parent.m_orderId
|
| 176 |
+
|
| 177 |
+
# Time In Force: DAY, GTC, IOC, GTD
|
| 178 |
+
if self.valid is None:
|
| 179 |
+
tif = 'GTC' # Good til cancelled
|
| 180 |
+
elif isinstance(self.valid, (datetime, date)):
|
| 181 |
+
tif = 'GTD' # Good til date
|
| 182 |
+
self.m_goodTillDate = bytes(self.valid.strftime('%Y%m%d %H:%M:%S'))
|
| 183 |
+
elif isinstance(self.valid, (timedelta,)):
|
| 184 |
+
if self.valid == self.DAY:
|
| 185 |
+
tif = 'DAY'
|
| 186 |
+
else:
|
| 187 |
+
tif = 'GTD' # Good til date
|
| 188 |
+
valid = datetime.now() + self.valid # .now, using localtime
|
| 189 |
+
self.m_goodTillDate = bytes(valid.strftime('%Y%m%d %H:%M:%S'))
|
| 190 |
+
|
| 191 |
+
elif self.valid == 0:
|
| 192 |
+
tif = 'DAY'
|
| 193 |
+
else:
|
| 194 |
+
tif = 'GTD' # Good til date
|
| 195 |
+
valid = num2date(self.valid)
|
| 196 |
+
self.m_goodTillDate = bytes(valid.strftime('%Y%m%d %H:%M:%S'))
|
| 197 |
+
|
| 198 |
+
self.m_tif = bytes(tif)
|
| 199 |
+
|
| 200 |
+
# OCA
|
| 201 |
+
self.m_ocaType = 1 # Cancel all remaining orders with block
|
| 202 |
+
|
| 203 |
+
# pass any custom arguments to the order
|
| 204 |
+
for k in kwargs:
|
| 205 |
+
setattr(self, (not hasattr(self, k)) * 'm_' + k, kwargs[k])
|
| 206 |
+
|
| 207 |
+
|
| 208 |
+
class IBCommInfo(CommInfoBase):
|
| 209 |
+
'''
|
| 210 |
+
Commissions are calculated by ib, but the trades calculations in the
|
| 211 |
+
```Strategy`` rely on the order carrying a CommInfo object attached for the
|
| 212 |
+
calculation of the operation cost and value.
|
| 213 |
+
|
| 214 |
+
These are non-critical informations, but removing them from the trade could
|
| 215 |
+
break existing usage and it is better to provide a CommInfo objet which
|
| 216 |
+
enables those calculations even if with approvimate values.
|
| 217 |
+
|
| 218 |
+
The margin calculation is not a known in advance information with IB
|
| 219 |
+
(margin impact can be gotten from OrderState objects) and therefore it is
|
| 220 |
+
left as future exercise to get it'''
|
| 221 |
+
|
| 222 |
+
def getvaluesize(self, size, price):
|
| 223 |
+
# In real life the margin approaches the price
|
| 224 |
+
return abs(size) * price
|
| 225 |
+
|
| 226 |
+
def getoperationcost(self, size, price):
|
| 227 |
+
'''Returns the needed amount of cash an operation would cost'''
|
| 228 |
+
# Same reasoning as above
|
| 229 |
+
return abs(size) * price
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
class MetaIBBroker(BrokerBase.__class__):
|
| 233 |
+
def __init__(cls, name, bases, dct):
|
| 234 |
+
'''Class has already been created ... register'''
|
| 235 |
+
# Initialize the class
|
| 236 |
+
super(MetaIBBroker, cls).__init__(name, bases, dct)
|
| 237 |
+
ibstore.IBStore.BrokerCls = cls
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
class IBBroker(with_metaclass(MetaIBBroker, BrokerBase)):
|
| 241 |
+
'''Broker implementation for Interactive Brokers.
|
| 242 |
+
|
| 243 |
+
This class maps the orders/positions from Interactive Brokers to the
|
| 244 |
+
internal API of ``backtrader``.
|
| 245 |
+
|
| 246 |
+
Notes:
|
| 247 |
+
|
| 248 |
+
- ``tradeid`` is not really supported, because the profit and loss are
|
| 249 |
+
taken directly from IB. Because (as expected) calculates it in FIFO
|
| 250 |
+
manner, the pnl is not accurate for the tradeid.
|
| 251 |
+
|
| 252 |
+
- Position
|
| 253 |
+
|
| 254 |
+
If there is an open position for an asset at the beginning of
|
| 255 |
+
operaitons or orders given by other means change a position, the trades
|
| 256 |
+
calculated in the ``Strategy`` in cerebro will not reflect the reality.
|
| 257 |
+
|
| 258 |
+
To avoid this, this broker would have to do its own position
|
| 259 |
+
management which would also allow tradeid with multiple ids (profit and
|
| 260 |
+
loss would also be calculated locally), but could be considered to be
|
| 261 |
+
defeating the purpose of working with a live broker
|
| 262 |
+
'''
|
| 263 |
+
params = ()
|
| 264 |
+
|
| 265 |
+
def __init__(self, **kwargs):
|
| 266 |
+
super(IBBroker, self).__init__()
|
| 267 |
+
|
| 268 |
+
self.ib = ibstore.IBStore(**kwargs)
|
| 269 |
+
|
| 270 |
+
self.startingcash = self.cash = 0.0
|
| 271 |
+
self.startingvalue = self.value = 0.0
|
| 272 |
+
|
| 273 |
+
self._lock_orders = threading.Lock() # control access
|
| 274 |
+
self.orderbyid = dict() # orders by order id
|
| 275 |
+
self.executions = dict() # notified executions
|
| 276 |
+
self.ordstatus = collections.defaultdict(dict)
|
| 277 |
+
self.notifs = queue.Queue() # holds orders which are notified
|
| 278 |
+
self.tonotify = collections.deque() # hold oids to be notified
|
| 279 |
+
|
| 280 |
+
def start(self):
|
| 281 |
+
super(IBBroker, self).start()
|
| 282 |
+
self.ib.start(broker=self)
|
| 283 |
+
|
| 284 |
+
if self.ib.connected():
|
| 285 |
+
self.ib.reqAccountUpdates()
|
| 286 |
+
self.startingcash = self.cash = self.ib.get_acc_cash()
|
| 287 |
+
self.startingvalue = self.value = self.ib.get_acc_value()
|
| 288 |
+
else:
|
| 289 |
+
self.startingcash = self.cash = 0.0
|
| 290 |
+
self.startingvalue = self.value = 0.0
|
| 291 |
+
|
| 292 |
+
def stop(self):
|
| 293 |
+
super(IBBroker, self).stop()
|
| 294 |
+
self.ib.stop()
|
| 295 |
+
|
| 296 |
+
def getcash(self):
|
| 297 |
+
# This call cannot block if no answer is available from ib
|
| 298 |
+
self.cash = self.ib.get_acc_cash()
|
| 299 |
+
return self.cash
|
| 300 |
+
|
| 301 |
+
def getvalue(self, datas=None):
|
| 302 |
+
self.value = self.ib.get_acc_value()
|
| 303 |
+
return self.value
|
| 304 |
+
|
| 305 |
+
def getposition(self, data, clone=True):
|
| 306 |
+
return self.ib.getposition(data.tradecontract, clone=clone)
|
| 307 |
+
|
| 308 |
+
def cancel(self, order):
|
| 309 |
+
try:
|
| 310 |
+
o = self.orderbyid[order.m_orderId]
|
| 311 |
+
except (ValueError, KeyError):
|
| 312 |
+
return # not found ... not cancellable
|
| 313 |
+
|
| 314 |
+
if order.status == Order.Cancelled: # already cancelled
|
| 315 |
+
return
|
| 316 |
+
|
| 317 |
+
self.ib.cancelOrder(order.m_orderId)
|
| 318 |
+
|
| 319 |
+
def orderstatus(self, order):
|
| 320 |
+
try:
|
| 321 |
+
o = self.orderbyid[order.m_orderId]
|
| 322 |
+
except (ValueError, KeyError):
|
| 323 |
+
o = order
|
| 324 |
+
|
| 325 |
+
return o.status
|
| 326 |
+
|
| 327 |
+
def submit(self, order):
|
| 328 |
+
order.submit(self)
|
| 329 |
+
|
| 330 |
+
# ocoize if needed
|
| 331 |
+
if order.oco is None: # Generate a UniqueId
|
| 332 |
+
order.m_ocaGroup = bytes(uuid.uuid4())
|
| 333 |
+
else:
|
| 334 |
+
order.m_ocaGroup = self.orderbyid[order.oco.m_orderId].m_ocaGroup
|
| 335 |
+
|
| 336 |
+
self.orderbyid[order.m_orderId] = order
|
| 337 |
+
self.ib.placeOrder(order.m_orderId, order.data.tradecontract, order)
|
| 338 |
+
self.notify(order)
|
| 339 |
+
|
| 340 |
+
return order
|
| 341 |
+
|
| 342 |
+
def getcommissioninfo(self, data):
|
| 343 |
+
contract = data.tradecontract
|
| 344 |
+
try:
|
| 345 |
+
mult = float(contract.m_multiplier)
|
| 346 |
+
except (ValueError, TypeError):
|
| 347 |
+
mult = 1.0
|
| 348 |
+
|
| 349 |
+
stocklike = contract.m_secType not in ('FUT', 'OPT', 'FOP',)
|
| 350 |
+
|
| 351 |
+
return IBCommInfo(mult=mult, stocklike=stocklike)
|
| 352 |
+
|
| 353 |
+
def _makeorder(self, action, owner, data,
|
| 354 |
+
size, price=None, plimit=None,
|
| 355 |
+
exectype=None, valid=None,
|
| 356 |
+
tradeid=0, **kwargs):
|
| 357 |
+
|
| 358 |
+
order = IBOrder(action, owner=owner, data=data,
|
| 359 |
+
size=size, price=price, pricelimit=plimit,
|
| 360 |
+
exectype=exectype, valid=valid,
|
| 361 |
+
tradeid=tradeid,
|
| 362 |
+
m_clientId=self.ib.clientId,
|
| 363 |
+
m_orderId=self.ib.nextOrderId(),
|
| 364 |
+
**kwargs)
|
| 365 |
+
|
| 366 |
+
order.addcomminfo(self.getcommissioninfo(data))
|
| 367 |
+
return order
|
| 368 |
+
|
| 369 |
+
def buy(self, owner, data,
|
| 370 |
+
size, price=None, plimit=None,
|
| 371 |
+
exectype=None, valid=None, tradeid=0,
|
| 372 |
+
**kwargs):
|
| 373 |
+
|
| 374 |
+
order = self._makeorder(
|
| 375 |
+
'BUY',
|
| 376 |
+
owner, data, size, price, plimit, exectype, valid, tradeid,
|
| 377 |
+
**kwargs)
|
| 378 |
+
|
| 379 |
+
return self.submit(order)
|
| 380 |
+
|
| 381 |
+
def sell(self, owner, data,
|
| 382 |
+
size, price=None, plimit=None,
|
| 383 |
+
exectype=None, valid=None, tradeid=0,
|
| 384 |
+
**kwargs):
|
| 385 |
+
|
| 386 |
+
order = self._makeorder(
|
| 387 |
+
'SELL',
|
| 388 |
+
owner, data, size, price, plimit, exectype, valid, tradeid,
|
| 389 |
+
**kwargs)
|
| 390 |
+
|
| 391 |
+
return self.submit(order)
|
| 392 |
+
|
| 393 |
+
def notify(self, order):
|
| 394 |
+
self.notifs.put(order.clone())
|
| 395 |
+
|
| 396 |
+
def get_notification(self):
|
| 397 |
+
try:
|
| 398 |
+
return self.notifs.get(False)
|
| 399 |
+
except queue.Empty:
|
| 400 |
+
pass
|
| 401 |
+
|
| 402 |
+
return None
|
| 403 |
+
|
| 404 |
+
def next(self):
|
| 405 |
+
self.notifs.put(None) # mark notificatino boundary
|
| 406 |
+
|
| 407 |
+
# Order statuses in msg
|
| 408 |
+
(SUBMITTED, FILLED, CANCELLED, INACTIVE,
|
| 409 |
+
PENDINGSUBMIT, PENDINGCANCEL, PRESUBMITTED) = (
|
| 410 |
+
'Submitted', 'Filled', 'Cancelled', 'Inactive',
|
| 411 |
+
'PendingSubmit', 'PendingCancel', 'PreSubmitted',)
|
| 412 |
+
|
| 413 |
+
def push_orderstatus(self, msg):
|
| 414 |
+
# Cancelled and Submitted with Filled = 0 can be pushed immediately
|
| 415 |
+
try:
|
| 416 |
+
order = self.orderbyid[msg.orderId]
|
| 417 |
+
except KeyError:
|
| 418 |
+
return # not found, it was not an order
|
| 419 |
+
|
| 420 |
+
if msg.status == self.SUBMITTED and msg.filled == 0:
|
| 421 |
+
if order.status == order.Accepted: # duplicate detection
|
| 422 |
+
return
|
| 423 |
+
|
| 424 |
+
order.accept(self)
|
| 425 |
+
self.notify(order)
|
| 426 |
+
|
| 427 |
+
elif msg.status == self.CANCELLED:
|
| 428 |
+
# duplicate detection
|
| 429 |
+
if order.status in [order.Cancelled, order.Expired]:
|
| 430 |
+
return
|
| 431 |
+
|
| 432 |
+
if order._willexpire:
|
| 433 |
+
# An openOrder has been seen with PendingCancel/Cancelled
|
| 434 |
+
# and this happens when an order expires
|
| 435 |
+
order.expire()
|
| 436 |
+
else:
|
| 437 |
+
# Pure user cancellation happens without an openOrder
|
| 438 |
+
order.cancel()
|
| 439 |
+
self.notify(order)
|
| 440 |
+
|
| 441 |
+
elif msg.status == self.PENDINGCANCEL:
|
| 442 |
+
# In theory this message should not be seen according to the docs,
|
| 443 |
+
# but other messages like PENDINGSUBMIT which are similarly
|
| 444 |
+
# described in the docs have been received in the demo
|
| 445 |
+
if order.status == order.Cancelled: # duplicate detection
|
| 446 |
+
return
|
| 447 |
+
|
| 448 |
+
# We do nothing because the situation is handled with the 202 error
|
| 449 |
+
# code if no orderStatus with CANCELLED is seen
|
| 450 |
+
# order.cancel()
|
| 451 |
+
# self.notify(order)
|
| 452 |
+
|
| 453 |
+
elif msg.status == self.INACTIVE:
|
| 454 |
+
# This is a tricky one, because the instances seen have led to
|
| 455 |
+
# order rejection in the demo, but according to the docs there may
|
| 456 |
+
# be a number of reasons and it seems like it could be reactivated
|
| 457 |
+
if order.status == order.Rejected: # duplicate detection
|
| 458 |
+
return
|
| 459 |
+
|
| 460 |
+
order.reject(self)
|
| 461 |
+
self.notify(order)
|
| 462 |
+
|
| 463 |
+
elif msg.status in [self.SUBMITTED, self.FILLED]:
|
| 464 |
+
# These two are kept inside the order until execdetails and
|
| 465 |
+
# commission are all in place - commission is the last to come
|
| 466 |
+
self.ordstatus[msg.orderId][msg.filled] = msg
|
| 467 |
+
|
| 468 |
+
elif msg.status in [self.PENDINGSUBMIT, self.PRESUBMITTED]:
|
| 469 |
+
# According to the docs, these statuses can only be set by the
|
| 470 |
+
# programmer but the demo account sent it back at random times with
|
| 471 |
+
# "filled"
|
| 472 |
+
if msg.filled:
|
| 473 |
+
self.ordstatus[msg.orderId][msg.filled] = msg
|
| 474 |
+
else: # Unknown status ...
|
| 475 |
+
pass
|
| 476 |
+
|
| 477 |
+
def push_execution(self, ex):
|
| 478 |
+
self.executions[ex.m_execId] = ex
|
| 479 |
+
|
| 480 |
+
def push_commissionreport(self, cr):
|
| 481 |
+
with self._lock_orders:
|
| 482 |
+
ex = self.executions.pop(cr.m_execId)
|
| 483 |
+
oid = ex.m_orderId
|
| 484 |
+
order = self.orderbyid[oid]
|
| 485 |
+
ostatus = self.ordstatus[oid].pop(ex.m_cumQty)
|
| 486 |
+
|
| 487 |
+
position = self.getposition(order.data, clone=False)
|
| 488 |
+
pprice_orig = position.price
|
| 489 |
+
size = ex.m_shares if ex.m_side[0] == 'B' else -ex.m_shares
|
| 490 |
+
price = ex.m_price
|
| 491 |
+
# use pseudoupdate and let the updateportfolio do the real update?
|
| 492 |
+
psize, pprice, opened, closed = position.update(size, price)
|
| 493 |
+
|
| 494 |
+
# split commission between closed and opened
|
| 495 |
+
comm = cr.m_commission
|
| 496 |
+
closedcomm = comm * closed / size
|
| 497 |
+
openedcomm = comm - closedcomm
|
| 498 |
+
|
| 499 |
+
comminfo = order.comminfo
|
| 500 |
+
closedvalue = comminfo.getoperationcost(closed, pprice_orig)
|
| 501 |
+
openedvalue = comminfo.getoperationcost(opened, price)
|
| 502 |
+
|
| 503 |
+
# default in m_pnl is MAXFLOAT
|
| 504 |
+
pnl = cr.m_realizedPNL if closed else 0.0
|
| 505 |
+
|
| 506 |
+
# The internal broker calc should yield the same result
|
| 507 |
+
# pnl = comminfo.profitandloss(-closed, pprice_orig, price)
|
| 508 |
+
|
| 509 |
+
# Use the actual time provided by the execution object
|
| 510 |
+
# The report from TWS is in actual local time, not the data's tz
|
| 511 |
+
dt = date2num(datetime.strptime(ex.m_time, '%Y%m%d %H:%M:%S'))
|
| 512 |
+
|
| 513 |
+
# Need to simulate a margin, but it plays no role, because it is
|
| 514 |
+
# controlled by a real broker. Let's set the price of the item
|
| 515 |
+
margin = order.data.close[0]
|
| 516 |
+
|
| 517 |
+
order.execute(dt, size, price,
|
| 518 |
+
closed, closedvalue, closedcomm,
|
| 519 |
+
opened, openedvalue, openedcomm,
|
| 520 |
+
margin, pnl,
|
| 521 |
+
psize, pprice)
|
| 522 |
+
|
| 523 |
+
if ostatus.status == self.FILLED:
|
| 524 |
+
order.completed()
|
| 525 |
+
self.ordstatus.pop(oid) # nothing left to be reported
|
| 526 |
+
else:
|
| 527 |
+
order.partial()
|
| 528 |
+
|
| 529 |
+
if oid not in self.tonotify: # Lock needed
|
| 530 |
+
self.tonotify.append(oid)
|
| 531 |
+
|
| 532 |
+
def push_portupdate(self):
|
| 533 |
+
# If the IBStore receives a Portfolio update, then this method will be
|
| 534 |
+
# indicated. If the execution of an order is split in serveral lots,
|
| 535 |
+
# updatePortfolio messages will be intermixed, which is used as a
|
| 536 |
+
# signal to indicate that the strategy can be notified
|
| 537 |
+
with self._lock_orders:
|
| 538 |
+
while self.tonotify:
|
| 539 |
+
oid = self.tonotify.popleft()
|
| 540 |
+
order = self.orderbyid[oid]
|
| 541 |
+
self.notify(order)
|
| 542 |
+
|
| 543 |
+
def push_ordererror(self, msg):
|
| 544 |
+
with self._lock_orders:
|
| 545 |
+
try:
|
| 546 |
+
order = self.orderbyid[msg.id]
|
| 547 |
+
except (KeyError, AttributeError):
|
| 548 |
+
return # no order or no id in error
|
| 549 |
+
|
| 550 |
+
if msg.errorCode == 202:
|
| 551 |
+
if not order.alive():
|
| 552 |
+
return
|
| 553 |
+
order.cancel()
|
| 554 |
+
|
| 555 |
+
elif msg.errorCode == 201: # rejected
|
| 556 |
+
if order.status == order.Rejected:
|
| 557 |
+
return
|
| 558 |
+
order.reject()
|
| 559 |
+
|
| 560 |
+
else:
|
| 561 |
+
order.reject() # default for all other cases
|
| 562 |
+
|
| 563 |
+
self.notify(order)
|
| 564 |
+
|
| 565 |
+
def push_orderstate(self, msg):
|
| 566 |
+
with self._lock_orders:
|
| 567 |
+
try:
|
| 568 |
+
order = self.orderbyid[msg.orderId]
|
| 569 |
+
except (KeyError, AttributeError):
|
| 570 |
+
return # no order or no id in error
|
| 571 |
+
|
| 572 |
+
if msg.orderState.m_status in ['PendingCancel', 'Cancelled',
|
| 573 |
+
'Canceled']:
|
| 574 |
+
# This is most likely due to an expiration]
|
| 575 |
+
order._willexpire = True
|
backtrader/source/backtrader/brokers/oandabroker.py
ADDED
|
@@ -0,0 +1,357 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
from copy import copy
|
| 26 |
+
from datetime import date, datetime, timedelta
|
| 27 |
+
import threading
|
| 28 |
+
|
| 29 |
+
from backtrader.feed import DataBase
|
| 30 |
+
from backtrader import (TimeFrame, num2date, date2num, BrokerBase,
|
| 31 |
+
Order, BuyOrder, SellOrder, OrderBase, OrderData)
|
| 32 |
+
from backtrader.utils.py3 import bytes, with_metaclass, MAXFLOAT
|
| 33 |
+
from backtrader.metabase import MetaParams
|
| 34 |
+
from backtrader.comminfo import CommInfoBase
|
| 35 |
+
from backtrader.position import Position
|
| 36 |
+
from backtrader.stores import oandastore
|
| 37 |
+
from backtrader.utils import AutoDict, AutoOrderedDict
|
| 38 |
+
from backtrader.comminfo import CommInfoBase
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class OandaCommInfo(CommInfoBase):
|
| 42 |
+
def getvaluesize(self, size, price):
|
| 43 |
+
# In real life the margin approaches the price
|
| 44 |
+
return abs(size) * price
|
| 45 |
+
|
| 46 |
+
def getoperationcost(self, size, price):
|
| 47 |
+
'''Returns the needed amount of cash an operation would cost'''
|
| 48 |
+
# Same reasoning as above
|
| 49 |
+
return abs(size) * price
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class MetaOandaBroker(BrokerBase.__class__):
|
| 53 |
+
def __init__(cls, name, bases, dct):
|
| 54 |
+
'''Class has already been created ... register'''
|
| 55 |
+
# Initialize the class
|
| 56 |
+
super(MetaOandaBroker, cls).__init__(name, bases, dct)
|
| 57 |
+
oandastore.OandaStore.BrokerCls = cls
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class OandaBroker(with_metaclass(MetaOandaBroker, BrokerBase)):
|
| 61 |
+
'''Broker implementation for Oanda.
|
| 62 |
+
|
| 63 |
+
This class maps the orders/positions from Oanda to the
|
| 64 |
+
internal API of ``backtrader``.
|
| 65 |
+
|
| 66 |
+
Params:
|
| 67 |
+
|
| 68 |
+
- ``use_positions`` (default:``True``): When connecting to the broker
|
| 69 |
+
provider use the existing positions to kickstart the broker.
|
| 70 |
+
|
| 71 |
+
Set to ``False`` during instantiation to disregard any existing
|
| 72 |
+
position
|
| 73 |
+
'''
|
| 74 |
+
params = (
|
| 75 |
+
('use_positions', True),
|
| 76 |
+
('commission', OandaCommInfo(mult=1.0, stocklike=False)),
|
| 77 |
+
)
|
| 78 |
+
|
| 79 |
+
def __init__(self, **kwargs):
|
| 80 |
+
super(OandaBroker, self).__init__()
|
| 81 |
+
|
| 82 |
+
self.o = oandastore.OandaStore(**kwargs)
|
| 83 |
+
|
| 84 |
+
self.orders = collections.OrderedDict() # orders by order id
|
| 85 |
+
self.notifs = collections.deque() # holds orders which are notified
|
| 86 |
+
|
| 87 |
+
self.opending = collections.defaultdict(list) # pending transmission
|
| 88 |
+
self.brackets = dict() # confirmed brackets
|
| 89 |
+
|
| 90 |
+
self.startingcash = self.cash = 0.0
|
| 91 |
+
self.startingvalue = self.value = 0.0
|
| 92 |
+
self.positions = collections.defaultdict(Position)
|
| 93 |
+
|
| 94 |
+
def start(self):
|
| 95 |
+
super(OandaBroker, self).start()
|
| 96 |
+
self.o.start(broker=self)
|
| 97 |
+
self.startingcash = self.cash = cash = self.o.get_cash()
|
| 98 |
+
self.startingvalue = self.value = self.o.get_value()
|
| 99 |
+
|
| 100 |
+
if self.p.use_positions:
|
| 101 |
+
for p in self.o.get_positions():
|
| 102 |
+
print('position for instrument:', p['instrument'])
|
| 103 |
+
is_sell = p['side'] == 'sell'
|
| 104 |
+
size = p['units']
|
| 105 |
+
if is_sell:
|
| 106 |
+
size = -size
|
| 107 |
+
price = p['avgPrice']
|
| 108 |
+
self.positions[p['instrument']] = Position(size, price)
|
| 109 |
+
|
| 110 |
+
def data_started(self, data):
|
| 111 |
+
pos = self.getposition(data)
|
| 112 |
+
|
| 113 |
+
if pos.size < 0:
|
| 114 |
+
order = SellOrder(data=data,
|
| 115 |
+
size=pos.size, price=pos.price,
|
| 116 |
+
exectype=Order.Market,
|
| 117 |
+
simulated=True)
|
| 118 |
+
|
| 119 |
+
order.addcomminfo(self.getcommissioninfo(data))
|
| 120 |
+
order.execute(0, pos.size, pos.price,
|
| 121 |
+
0, 0.0, 0.0,
|
| 122 |
+
pos.size, 0.0, 0.0,
|
| 123 |
+
0.0, 0.0,
|
| 124 |
+
pos.size, pos.price)
|
| 125 |
+
|
| 126 |
+
order.completed()
|
| 127 |
+
self.notify(order)
|
| 128 |
+
|
| 129 |
+
elif pos.size > 0:
|
| 130 |
+
order = BuyOrder(data=data,
|
| 131 |
+
size=pos.size, price=pos.price,
|
| 132 |
+
exectype=Order.Market,
|
| 133 |
+
simulated=True)
|
| 134 |
+
|
| 135 |
+
order.addcomminfo(self.getcommissioninfo(data))
|
| 136 |
+
order.execute(0, pos.size, pos.price,
|
| 137 |
+
0, 0.0, 0.0,
|
| 138 |
+
pos.size, 0.0, 0.0,
|
| 139 |
+
0.0, 0.0,
|
| 140 |
+
pos.size, pos.price)
|
| 141 |
+
|
| 142 |
+
order.completed()
|
| 143 |
+
self.notify(order)
|
| 144 |
+
|
| 145 |
+
def stop(self):
|
| 146 |
+
super(OandaBroker, self).stop()
|
| 147 |
+
self.o.stop()
|
| 148 |
+
|
| 149 |
+
def getcash(self):
|
| 150 |
+
# This call cannot block if no answer is available from oanda
|
| 151 |
+
self.cash = cash = self.o.get_cash()
|
| 152 |
+
return cash
|
| 153 |
+
|
| 154 |
+
def getvalue(self, datas=None):
|
| 155 |
+
self.value = self.o.get_value()
|
| 156 |
+
return self.value
|
| 157 |
+
|
| 158 |
+
def getposition(self, data, clone=True):
|
| 159 |
+
# return self.o.getposition(data._dataname, clone=clone)
|
| 160 |
+
pos = self.positions[data._dataname]
|
| 161 |
+
if clone:
|
| 162 |
+
pos = pos.clone()
|
| 163 |
+
|
| 164 |
+
return pos
|
| 165 |
+
|
| 166 |
+
def orderstatus(self, order):
|
| 167 |
+
o = self.orders[order.ref]
|
| 168 |
+
return o.status
|
| 169 |
+
|
| 170 |
+
def _submit(self, oref):
|
| 171 |
+
order = self.orders[oref]
|
| 172 |
+
order.submit(self)
|
| 173 |
+
self.notify(order)
|
| 174 |
+
for o in self._bracketnotif(order):
|
| 175 |
+
o.submit(self)
|
| 176 |
+
self.notify(o)
|
| 177 |
+
|
| 178 |
+
def _reject(self, oref):
|
| 179 |
+
order = self.orders[oref]
|
| 180 |
+
order.reject(self)
|
| 181 |
+
self.notify(order)
|
| 182 |
+
self._bracketize(order, cancel=True)
|
| 183 |
+
|
| 184 |
+
def _accept(self, oref):
|
| 185 |
+
order = self.orders[oref]
|
| 186 |
+
order.accept()
|
| 187 |
+
self.notify(order)
|
| 188 |
+
for o in self._bracketnotif(order):
|
| 189 |
+
o.accept(self)
|
| 190 |
+
self.notify(o)
|
| 191 |
+
|
| 192 |
+
def _cancel(self, oref):
|
| 193 |
+
order = self.orders[oref]
|
| 194 |
+
order.cancel()
|
| 195 |
+
self.notify(order)
|
| 196 |
+
self._bracketize(order, cancel=True)
|
| 197 |
+
|
| 198 |
+
def _expire(self, oref):
|
| 199 |
+
order = self.orders[oref]
|
| 200 |
+
order.expire()
|
| 201 |
+
self.notify(order)
|
| 202 |
+
self._bracketize(order, cancel=True)
|
| 203 |
+
|
| 204 |
+
def _bracketnotif(self, order):
|
| 205 |
+
pref = getattr(order.parent, 'ref', order.ref) # parent ref or self
|
| 206 |
+
br = self.brackets.get(pref, None) # to avoid recursion
|
| 207 |
+
return br[-2:] if br is not None else []
|
| 208 |
+
|
| 209 |
+
def _bracketize(self, order, cancel=False):
|
| 210 |
+
pref = getattr(order.parent, 'ref', order.ref) # parent ref or self
|
| 211 |
+
br = self.brackets.pop(pref, None) # to avoid recursion
|
| 212 |
+
if br is None:
|
| 213 |
+
return
|
| 214 |
+
|
| 215 |
+
if not cancel:
|
| 216 |
+
if len(br) == 3: # all 3 orders in place, parent was filled
|
| 217 |
+
br = br[1:] # discard index 0, parent
|
| 218 |
+
for o in br:
|
| 219 |
+
o.activate() # simulate activate for children
|
| 220 |
+
self.brackets[pref] = br # not done - reinsert children
|
| 221 |
+
|
| 222 |
+
elif len(br) == 2: # filling a children
|
| 223 |
+
oidx = br.index(order) # find index to filled (0 or 1)
|
| 224 |
+
self._cancel(br[1 - oidx].ref) # cancel remaining (1 - 0 -> 1)
|
| 225 |
+
else:
|
| 226 |
+
# Any cancellation cancel the others
|
| 227 |
+
for o in br:
|
| 228 |
+
if o.alive():
|
| 229 |
+
self._cancel(o.ref)
|
| 230 |
+
|
| 231 |
+
def _fill(self, oref, size, price, ttype, **kwargs):
|
| 232 |
+
order = self.orders[oref]
|
| 233 |
+
|
| 234 |
+
if not order.alive(): # can be a bracket
|
| 235 |
+
pref = getattr(order.parent, 'ref', order.ref)
|
| 236 |
+
if pref not in self.brackets:
|
| 237 |
+
msg = ('Order fill received for {}, with price {} and size {} '
|
| 238 |
+
'but order is no longer alive and is not a bracket. '
|
| 239 |
+
'Unknown situation')
|
| 240 |
+
msg.format(order.ref, price, size)
|
| 241 |
+
self.put_notification(msg, order, price, size)
|
| 242 |
+
return
|
| 243 |
+
|
| 244 |
+
# [main, stopside, takeside], neg idx to array are -3, -2, -1
|
| 245 |
+
if ttype == 'STOP_LOSS_FILLED':
|
| 246 |
+
order = self.brackets[pref][-2]
|
| 247 |
+
elif ttype == 'TAKE_PROFIT_FILLED':
|
| 248 |
+
order = self.brackets[pref][-1]
|
| 249 |
+
else:
|
| 250 |
+
msg = ('Order fill received for {}, with price {} and size {} '
|
| 251 |
+
'but order is no longer alive and is a bracket. '
|
| 252 |
+
'Unknown situation')
|
| 253 |
+
msg.format(order.ref, price, size)
|
| 254 |
+
self.put_notification(msg, order, price, size)
|
| 255 |
+
return
|
| 256 |
+
|
| 257 |
+
data = order.data
|
| 258 |
+
pos = self.getposition(data, clone=False)
|
| 259 |
+
psize, pprice, opened, closed = pos.update(size, price)
|
| 260 |
+
|
| 261 |
+
comminfo = self.getcommissioninfo(data)
|
| 262 |
+
|
| 263 |
+
closedvalue = closedcomm = 0.0
|
| 264 |
+
openedvalue = openedcomm = 0.0
|
| 265 |
+
margin = pnl = 0.0
|
| 266 |
+
|
| 267 |
+
order.execute(data.datetime[0], size, price,
|
| 268 |
+
closed, closedvalue, closedcomm,
|
| 269 |
+
opened, openedvalue, openedcomm,
|
| 270 |
+
margin, pnl,
|
| 271 |
+
psize, pprice)
|
| 272 |
+
|
| 273 |
+
if order.executed.remsize:
|
| 274 |
+
order.partial()
|
| 275 |
+
self.notify(order)
|
| 276 |
+
else:
|
| 277 |
+
order.completed()
|
| 278 |
+
self.notify(order)
|
| 279 |
+
self._bracketize(order)
|
| 280 |
+
|
| 281 |
+
def _transmit(self, order):
|
| 282 |
+
oref = order.ref
|
| 283 |
+
pref = getattr(order.parent, 'ref', oref) # parent ref or self
|
| 284 |
+
|
| 285 |
+
if order.transmit:
|
| 286 |
+
if oref != pref: # children order
|
| 287 |
+
# Put parent in orders dict, but add stopside and takeside
|
| 288 |
+
# to order creation. Return the takeside order, to have 3s
|
| 289 |
+
takeside = order # alias for clarity
|
| 290 |
+
parent, stopside = self.opending.pop(pref)
|
| 291 |
+
for o in parent, stopside, takeside:
|
| 292 |
+
self.orders[o.ref] = o # write them down
|
| 293 |
+
|
| 294 |
+
self.brackets[pref] = [parent, stopside, takeside]
|
| 295 |
+
self.o.order_create(parent, stopside, takeside)
|
| 296 |
+
return takeside # parent was already returned
|
| 297 |
+
|
| 298 |
+
else: # Parent order, which is not being transmitted
|
| 299 |
+
self.orders[order.ref] = order
|
| 300 |
+
return self.o.order_create(order)
|
| 301 |
+
|
| 302 |
+
# Not transmitting
|
| 303 |
+
self.opending[pref].append(order)
|
| 304 |
+
return order
|
| 305 |
+
|
| 306 |
+
def buy(self, owner, data,
|
| 307 |
+
size, price=None, plimit=None,
|
| 308 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 309 |
+
trailamount=None, trailpercent=None,
|
| 310 |
+
parent=None, transmit=True,
|
| 311 |
+
**kwargs):
|
| 312 |
+
|
| 313 |
+
order = BuyOrder(owner=owner, data=data,
|
| 314 |
+
size=size, price=price, pricelimit=plimit,
|
| 315 |
+
exectype=exectype, valid=valid, tradeid=tradeid,
|
| 316 |
+
trailamount=trailamount, trailpercent=trailpercent,
|
| 317 |
+
parent=parent, transmit=transmit)
|
| 318 |
+
|
| 319 |
+
order.addinfo(**kwargs)
|
| 320 |
+
order.addcomminfo(self.getcommissioninfo(data))
|
| 321 |
+
return self._transmit(order)
|
| 322 |
+
|
| 323 |
+
def sell(self, owner, data,
|
| 324 |
+
size, price=None, plimit=None,
|
| 325 |
+
exectype=None, valid=None, tradeid=0, oco=None,
|
| 326 |
+
trailamount=None, trailpercent=None,
|
| 327 |
+
parent=None, transmit=True,
|
| 328 |
+
**kwargs):
|
| 329 |
+
|
| 330 |
+
order = SellOrder(owner=owner, data=data,
|
| 331 |
+
size=size, price=price, pricelimit=plimit,
|
| 332 |
+
exectype=exectype, valid=valid, tradeid=tradeid,
|
| 333 |
+
trailamount=trailamount, trailpercent=trailpercent,
|
| 334 |
+
parent=parent, transmit=transmit)
|
| 335 |
+
|
| 336 |
+
order.addinfo(**kwargs)
|
| 337 |
+
order.addcomminfo(self.getcommissioninfo(data))
|
| 338 |
+
return self._transmit(order)
|
| 339 |
+
|
| 340 |
+
def cancel(self, order):
|
| 341 |
+
o = self.orders[order.ref]
|
| 342 |
+
if order.status == Order.Cancelled: # already cancelled
|
| 343 |
+
return
|
| 344 |
+
|
| 345 |
+
return self.o.order_cancel(order)
|
| 346 |
+
|
| 347 |
+
def notify(self, order):
|
| 348 |
+
self.notifs.append(order.clone())
|
| 349 |
+
|
| 350 |
+
def get_notification(self):
|
| 351 |
+
if not self.notifs:
|
| 352 |
+
return None
|
| 353 |
+
|
| 354 |
+
return self.notifs.popleft()
|
| 355 |
+
|
| 356 |
+
def next(self):
|
| 357 |
+
self.notifs.append(None) # mark notification boundary
|
backtrader/source/backtrader/brokers/vcbroker.py
ADDED
|
@@ -0,0 +1,466 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
from datetime import date, datetime, timedelta
|
| 26 |
+
import threading
|
| 27 |
+
|
| 28 |
+
from backtrader import BrokerBase, Order, BuyOrder, SellOrder
|
| 29 |
+
from backtrader.comminfo import CommInfoBase
|
| 30 |
+
from backtrader.feed import DataBase
|
| 31 |
+
from backtrader.metabase import MetaParams
|
| 32 |
+
from backtrader.position import Position
|
| 33 |
+
from backtrader.utils.py3 import with_metaclass
|
| 34 |
+
|
| 35 |
+
from backtrader.stores import vcstore
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class VCCommInfo(CommInfoBase):
|
| 39 |
+
'''
|
| 40 |
+
Commissions are calculated by ib, but the trades calculations in the
|
| 41 |
+
```Strategy`` rely on the order carrying a CommInfo object attached for the
|
| 42 |
+
calculation of the operation cost and value.
|
| 43 |
+
|
| 44 |
+
These are non-critical informations, but removing them from the trade could
|
| 45 |
+
break existing usage and it is better to provide a CommInfo objet which
|
| 46 |
+
enables those calculations even if with approvimate values.
|
| 47 |
+
|
| 48 |
+
The margin calculation is not a known in advance information with IB
|
| 49 |
+
(margin impact can be gotten from OrderState objects) and therefore it is
|
| 50 |
+
left as future exercise to get it'''
|
| 51 |
+
|
| 52 |
+
def getvaluesize(self, size, price):
|
| 53 |
+
# In real life the margin approaches the price
|
| 54 |
+
return abs(size) * price
|
| 55 |
+
|
| 56 |
+
def getoperationcost(self, size, price):
|
| 57 |
+
'''Returns the needed amount of cash an operation would cost'''
|
| 58 |
+
# Same reasoning as above
|
| 59 |
+
return abs(size) * price
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
class MetaVCBroker(BrokerBase.__class__):
|
| 63 |
+
def __init__(cls, name, bases, dct):
|
| 64 |
+
'''Class has already been created ... register'''
|
| 65 |
+
# Initialize the class
|
| 66 |
+
super(MetaVCBroker, cls).__init__(name, bases, dct)
|
| 67 |
+
vcstore.VCStore.BrokerCls = cls
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
class VCBroker(with_metaclass(MetaVCBroker, BrokerBase)):
|
| 71 |
+
'''Broker implementation for VisualChart.
|
| 72 |
+
|
| 73 |
+
This class maps the orders/positions from VisualChart to the
|
| 74 |
+
internal API of ``backtrader``.
|
| 75 |
+
|
| 76 |
+
Params:
|
| 77 |
+
|
| 78 |
+
- ``account`` (default: None)
|
| 79 |
+
|
| 80 |
+
VisualChart supports several accounts simultaneously on the broker. If
|
| 81 |
+
the default ``None`` is in place the 1st account in the ComTrader
|
| 82 |
+
``Accounts`` collection will be used.
|
| 83 |
+
|
| 84 |
+
If an account name is provided, the ``Accounts`` collection will be
|
| 85 |
+
checked and used if present
|
| 86 |
+
|
| 87 |
+
- ``commission`` (default: None)
|
| 88 |
+
|
| 89 |
+
An object will be autogenerated if no commission-scheme is passed as
|
| 90 |
+
parameter
|
| 91 |
+
|
| 92 |
+
See the notes below for further explanations
|
| 93 |
+
|
| 94 |
+
Notes:
|
| 95 |
+
|
| 96 |
+
- Position
|
| 97 |
+
|
| 98 |
+
VisualChart reports "OpenPositions" updates through the ComTrader
|
| 99 |
+
interface but only when the position has a "size". An update to
|
| 100 |
+
indicate a position has moved to ZERO is reported by the absence of
|
| 101 |
+
such position. This forces to keep accounting of the positions by
|
| 102 |
+
looking at the execution events, just like the simulation broker does
|
| 103 |
+
|
| 104 |
+
- Commission
|
| 105 |
+
|
| 106 |
+
The ComTrader interface of VisualChart does not report commissions and
|
| 107 |
+
as such the auto-generated CommissionInfo object cannot use
|
| 108 |
+
non-existent commissions to properly account for them. In order to
|
| 109 |
+
support commissions a ``commission`` parameter has to be passed with
|
| 110 |
+
the appropriate commission schemes.
|
| 111 |
+
|
| 112 |
+
The documentation on Commission Schemes details how to do this
|
| 113 |
+
|
| 114 |
+
- Expiration Timing
|
| 115 |
+
|
| 116 |
+
The ComTrader interface (or is it the comtypes module?) discards
|
| 117 |
+
``time`` information from ``datetime`` objects and expiration dates are
|
| 118 |
+
always full dates.
|
| 119 |
+
|
| 120 |
+
- Expiration Reporting
|
| 121 |
+
|
| 122 |
+
At the moment no heuristic is in place to determine when a cancelled
|
| 123 |
+
order has been cancelled due to expiration. And therefore expired
|
| 124 |
+
orders are reported as cancelled.
|
| 125 |
+
'''
|
| 126 |
+
params = (
|
| 127 |
+
('account', None),
|
| 128 |
+
('commission', None),
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def __init__(self, **kwargs):
|
| 132 |
+
super(VCBroker, self).__init__()
|
| 133 |
+
|
| 134 |
+
self.store = vcstore.VCStore(**kwargs)
|
| 135 |
+
|
| 136 |
+
# Account data
|
| 137 |
+
self._acc_name = None
|
| 138 |
+
self.startingcash = self.cash = 0.0
|
| 139 |
+
self.startingvalue = self.value = 0.0
|
| 140 |
+
|
| 141 |
+
# Position accounting
|
| 142 |
+
self._lock_pos = threading.Lock() # sync account updates
|
| 143 |
+
self.positions = collections.defaultdict(Position) # actual positions
|
| 144 |
+
|
| 145 |
+
# Order storage
|
| 146 |
+
self._lock_orders = threading.Lock() # control access
|
| 147 |
+
self.orderbyid = dict() # orders by order id
|
| 148 |
+
|
| 149 |
+
# Notifications
|
| 150 |
+
self.notifs = collections.deque()
|
| 151 |
+
|
| 152 |
+
# Dictionaries of values for order mapping
|
| 153 |
+
self._otypes = {
|
| 154 |
+
Order.Market: self.store.vcctmod.OT_Market,
|
| 155 |
+
Order.Close: self.store.vcctmod.OT_Market,
|
| 156 |
+
Order.Limit: self.store.vcctmod.OT_Limit,
|
| 157 |
+
Order.Stop: self.store.vcctmod.OT_StopMarket,
|
| 158 |
+
Order.StopLimit: self.store.vcctmod.OT_StopLimit,
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
self._osides = {
|
| 162 |
+
Order.Buy: self.store.vcctmod.OS_Buy,
|
| 163 |
+
Order.Sell: self.store.vcctmod.OS_Sell,
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
self._otrestriction = {
|
| 167 |
+
Order.T_None: self.store.vcctmod.TR_NoRestriction,
|
| 168 |
+
Order.T_Date: self.store.vcctmod.TR_Date,
|
| 169 |
+
Order.T_Close: self.store.vcctmod.TR_CloseAuction,
|
| 170 |
+
Order.T_Day: self.store.vcctmod.TR_Session,
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
self._ovrestriction = {
|
| 174 |
+
Order.V_None: self.store.vcctmod.VR_NoRestriction,
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
self._futlikes = (
|
| 178 |
+
self.store.vcdsmod.IT_Future, self.store.vcdsmod.IT_Option,
|
| 179 |
+
self.store.vcdsmod.IT_Fund,
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
def start(self):
|
| 183 |
+
super(VCBroker, self).start()
|
| 184 |
+
self.store.start(broker=self)
|
| 185 |
+
|
| 186 |
+
def stop(self):
|
| 187 |
+
super(VCBroker, self).stop()
|
| 188 |
+
self.store.stop()
|
| 189 |
+
|
| 190 |
+
def getcash(self):
|
| 191 |
+
# This call cannot block if no answer is available from ib
|
| 192 |
+
return self.cash
|
| 193 |
+
|
| 194 |
+
def getvalue(self, datas=None):
|
| 195 |
+
return self.value
|
| 196 |
+
|
| 197 |
+
def get_notification(self):
|
| 198 |
+
return self.notifs.popleft() # at leat a None is present
|
| 199 |
+
|
| 200 |
+
def notify(self, order):
|
| 201 |
+
self.notifs.append(order.clone())
|
| 202 |
+
|
| 203 |
+
def next(self):
|
| 204 |
+
self.notifs.append(None) # mark notificatino boundary
|
| 205 |
+
|
| 206 |
+
def getposition(self, data, clone=True):
|
| 207 |
+
with self._lock_pos:
|
| 208 |
+
pos = self.positions[data._tradename]
|
| 209 |
+
if clone:
|
| 210 |
+
return pos.clone()
|
| 211 |
+
|
| 212 |
+
return pos
|
| 213 |
+
|
| 214 |
+
def getcommissioninfo(self, data):
|
| 215 |
+
if data._tradename in self.comminfo:
|
| 216 |
+
return self.comminfo[data._tradename]
|
| 217 |
+
|
| 218 |
+
comminfo = self.comminfo[None]
|
| 219 |
+
if comminfo is not None:
|
| 220 |
+
return comminfo
|
| 221 |
+
|
| 222 |
+
stocklike = data._syminfo.Type in self._futlikes
|
| 223 |
+
|
| 224 |
+
return VCCommInfo(mult=data._syminfo.PointValue, stocklike=stocklike)
|
| 225 |
+
|
| 226 |
+
def _makeorder(self, ordtype, owner, data,
|
| 227 |
+
size, price=None, plimit=None,
|
| 228 |
+
exectype=None, valid=None,
|
| 229 |
+
tradeid=0, **kwargs):
|
| 230 |
+
|
| 231 |
+
order = self.store.vcctmod.Order()
|
| 232 |
+
order.Account = self._acc_name
|
| 233 |
+
order.SymbolCode = data._tradename
|
| 234 |
+
order.OrderType = self._otypes[exectype]
|
| 235 |
+
order.OrderSide = self._osides[ordtype]
|
| 236 |
+
|
| 237 |
+
order.VolumeRestriction = self._ovrestriction[Order.V_None]
|
| 238 |
+
order.HideVolume = 0
|
| 239 |
+
order.MinVolume = 0
|
| 240 |
+
|
| 241 |
+
# order.UserName = 'danjrod' # str(tradeid)
|
| 242 |
+
# order.OrderId = 'a' * 50 # str(tradeid)
|
| 243 |
+
order.UserOrderId = ''
|
| 244 |
+
if tradeid:
|
| 245 |
+
order.ExtendedInfo = 'TradeId {}'.format(tradeid)
|
| 246 |
+
else:
|
| 247 |
+
order.ExtendedInfo = ''
|
| 248 |
+
|
| 249 |
+
order.Volume = abs(size)
|
| 250 |
+
|
| 251 |
+
order.StopPrice = 0.0
|
| 252 |
+
order.Price = 0.0
|
| 253 |
+
if exectype == Order.Market:
|
| 254 |
+
pass
|
| 255 |
+
elif exectype == Order.Limit:
|
| 256 |
+
order.Price = price or plimit # cover naming confusion cases
|
| 257 |
+
elif exectype == Order.Close:
|
| 258 |
+
pass
|
| 259 |
+
elif exectype == Order.Stop:
|
| 260 |
+
order.StopPrice = price
|
| 261 |
+
elif exectype == Order.StopLimit:
|
| 262 |
+
order.StopPrice = price
|
| 263 |
+
order.Price = plimit
|
| 264 |
+
|
| 265 |
+
order.ValidDate = None
|
| 266 |
+
if exectype == Order.Close:
|
| 267 |
+
order.TimeRestriction = self._otrestriction[Order.T_Close]
|
| 268 |
+
else:
|
| 269 |
+
if valid is None:
|
| 270 |
+
order.TimeRestriction = self._otrestriction[Order.T_None]
|
| 271 |
+
elif isinstance(valid, (datetime, date)):
|
| 272 |
+
order.TimeRestriction = self._otrestriction[Order.T_Date]
|
| 273 |
+
order.ValidDate = valid
|
| 274 |
+
elif isinstance(valid, (timedelta,)):
|
| 275 |
+
if valid == Order.DAY:
|
| 276 |
+
order.TimeRestriction = self._otrestriction[Order.T_Day]
|
| 277 |
+
else:
|
| 278 |
+
order.TimeRestriction = self._otrestriction[Order.T_Date]
|
| 279 |
+
order.ValidDate = datetime.now() + valid
|
| 280 |
+
|
| 281 |
+
elif not self.valid: # DAY
|
| 282 |
+
order.TimeRestriction = self._otrestriction[Order.T_Day]
|
| 283 |
+
|
| 284 |
+
# Support for custom user arguments
|
| 285 |
+
for k in kwargs:
|
| 286 |
+
if hasattr(order, k):
|
| 287 |
+
setattr(order, k, kwargs[k])
|
| 288 |
+
|
| 289 |
+
return order
|
| 290 |
+
|
| 291 |
+
def submit(self, order, vcorder):
|
| 292 |
+
order.submit(self)
|
| 293 |
+
|
| 294 |
+
vco = vcorder
|
| 295 |
+
oid = self.store.vcct.SendOrder(
|
| 296 |
+
vco.Account, vco.SymbolCode,
|
| 297 |
+
vco.OrderType, vco.OrderSide, vco.Volume, vco.Price, vco.StopPrice,
|
| 298 |
+
vco.VolumeRestriction, vco.TimeRestriction,
|
| 299 |
+
ValidDate=vco.ValidDate
|
| 300 |
+
)
|
| 301 |
+
|
| 302 |
+
order.vcorder = oid
|
| 303 |
+
order.addcomminfo(self.getcommissioninfo(order.data))
|
| 304 |
+
|
| 305 |
+
with self._lock_orders:
|
| 306 |
+
self.orderbyid[oid] = order
|
| 307 |
+
self.notify(order)
|
| 308 |
+
return order
|
| 309 |
+
|
| 310 |
+
def buy(self, owner, data,
|
| 311 |
+
size, price=None, plimit=None,
|
| 312 |
+
exectype=None, valid=None, tradeid=0,
|
| 313 |
+
**kwargs):
|
| 314 |
+
|
| 315 |
+
order = BuyOrder(owner=owner, data=data,
|
| 316 |
+
size=size, price=price, pricelimit=plimit,
|
| 317 |
+
exectype=exectype, valid=valid, tradeid=tradeid)
|
| 318 |
+
|
| 319 |
+
order.addinfo(**kwargs)
|
| 320 |
+
|
| 321 |
+
vcorder = self._makeorder(order.ordtype, owner, data, size, price,
|
| 322 |
+
plimit, exectype, valid, tradeid,
|
| 323 |
+
**kwargs)
|
| 324 |
+
|
| 325 |
+
return self.submit(order, vcorder)
|
| 326 |
+
|
| 327 |
+
def sell(self, owner, data,
|
| 328 |
+
size, price=None, plimit=None,
|
| 329 |
+
exectype=None, valid=None, tradeid=0,
|
| 330 |
+
**kwargs):
|
| 331 |
+
|
| 332 |
+
order = SellOrder(owner=owner, data=data,
|
| 333 |
+
size=size, price=price, pricelimit=plimit,
|
| 334 |
+
exectype=exectype, valid=valid, tradeid=tradeid)
|
| 335 |
+
|
| 336 |
+
order.addinfo(**kwargs)
|
| 337 |
+
|
| 338 |
+
vcorder = self._makeorder(order.ordtype, owner, data, size, price,
|
| 339 |
+
plimit, exectype, valid, tradeid,
|
| 340 |
+
**kwargs)
|
| 341 |
+
|
| 342 |
+
return self.submit(order, vcorder)
|
| 343 |
+
|
| 344 |
+
#
|
| 345 |
+
# COM Events implementation
|
| 346 |
+
#
|
| 347 |
+
def __call__(self, trader):
|
| 348 |
+
# Called to start the process, call in sub-thread. only the passed
|
| 349 |
+
# trader can be used in the thread
|
| 350 |
+
self.trader = trader
|
| 351 |
+
|
| 352 |
+
for acc in trader.Accounts:
|
| 353 |
+
if self.p.account is None or self.p.account == acc.Account:
|
| 354 |
+
self.startingcash = self.cash = acc.Balance.Cash
|
| 355 |
+
self.startingvalue = self.value = acc.Balance.NetWorth
|
| 356 |
+
self._acc_name = acc.Account
|
| 357 |
+
break # found the account
|
| 358 |
+
|
| 359 |
+
return self
|
| 360 |
+
|
| 361 |
+
def OnChangedBalance(self, Account):
|
| 362 |
+
if self._acc_name is None or self._acc_name != Account:
|
| 363 |
+
return # skip notifs for other accounts
|
| 364 |
+
|
| 365 |
+
for acc in self.trader.Accounts:
|
| 366 |
+
if acc.Account == Account:
|
| 367 |
+
# Update store values
|
| 368 |
+
self.cash = acc.Balance.Cash
|
| 369 |
+
self.value = acc.Balance.NetWorth
|
| 370 |
+
break
|
| 371 |
+
|
| 372 |
+
def OnModifiedOrder(self, Order):
|
| 373 |
+
# We are not expecting this: unless backtrader starts implementing
|
| 374 |
+
# modify order method
|
| 375 |
+
pass
|
| 376 |
+
|
| 377 |
+
def OnCancelledOrder(self, Order):
|
| 378 |
+
with self._lock_orders:
|
| 379 |
+
try:
|
| 380 |
+
border = self.orderbyid[Order.OrderId]
|
| 381 |
+
except KeyError:
|
| 382 |
+
return # possibly external order
|
| 383 |
+
|
| 384 |
+
border.cancel()
|
| 385 |
+
self.notify(border)
|
| 386 |
+
|
| 387 |
+
def OnTotalExecutedOrder(self, Order):
|
| 388 |
+
self.OnExecutedOrder(Order, partial=False)
|
| 389 |
+
|
| 390 |
+
def OnPartialExecutedOrder(self, Order):
|
| 391 |
+
self.OnExecutedOrder(Order, partial=True)
|
| 392 |
+
|
| 393 |
+
def OnExecutedOrder(self, Order, partial):
|
| 394 |
+
with self._lock_orders:
|
| 395 |
+
try:
|
| 396 |
+
border = self.orderbyid[Order.OrderId]
|
| 397 |
+
except KeyError:
|
| 398 |
+
return # possibly external order
|
| 399 |
+
|
| 400 |
+
price = Order.Price
|
| 401 |
+
size = Order.Volume
|
| 402 |
+
if border.issell():
|
| 403 |
+
size *= -1
|
| 404 |
+
|
| 405 |
+
# Find position and do a real update - accounting happens here
|
| 406 |
+
position = self.getposition(border.data, clone=False)
|
| 407 |
+
pprice_orig = position.price
|
| 408 |
+
psize, pprice, opened, closed = position.update(size, price)
|
| 409 |
+
|
| 410 |
+
comminfo = border.comminfo
|
| 411 |
+
closedvalue = comminfo.getoperationcost(closed, pprice_orig)
|
| 412 |
+
closedcomm = comminfo.getcommission(closed, price)
|
| 413 |
+
|
| 414 |
+
openedvalue = comminfo.getoperationcost(opened, price)
|
| 415 |
+
openedcomm = comminfo.getcommission(opened, price)
|
| 416 |
+
|
| 417 |
+
pnl = comminfo.profitandloss(-closed, pprice_orig, price)
|
| 418 |
+
margin = comminfo.getvaluesize(size, price)
|
| 419 |
+
|
| 420 |
+
# NOTE: No commission information available in the Trader interface
|
| 421 |
+
# CHECK: Use reported time instead of last data time?
|
| 422 |
+
border.execute(border.data.datetime[0],
|
| 423 |
+
size, price,
|
| 424 |
+
closed, closedvalue, closedcomm,
|
| 425 |
+
opened, openedvalue, openedcomm,
|
| 426 |
+
margin, pnl,
|
| 427 |
+
psize, pprice) # pnl
|
| 428 |
+
|
| 429 |
+
if partial:
|
| 430 |
+
border.partial()
|
| 431 |
+
else:
|
| 432 |
+
border.completed()
|
| 433 |
+
|
| 434 |
+
self.notify(border)
|
| 435 |
+
|
| 436 |
+
def OnOrderInMarket(self, Order):
|
| 437 |
+
# Other is in ther market ... therefore "accepted"
|
| 438 |
+
with self._lock_orders:
|
| 439 |
+
try:
|
| 440 |
+
border = self.orderbyid[Order.OrderId]
|
| 441 |
+
except KeyError:
|
| 442 |
+
return # possibly external order
|
| 443 |
+
|
| 444 |
+
border.accept()
|
| 445 |
+
self.notify(border)
|
| 446 |
+
|
| 447 |
+
def OnNewOrderLocation(self, Order):
|
| 448 |
+
# Can be used for "submitted", but the status is set manually
|
| 449 |
+
pass
|
| 450 |
+
|
| 451 |
+
def OnChangedOpenPositions(self, Account):
|
| 452 |
+
# This would be useful if it reported a position moving back to 0. In
|
| 453 |
+
# this case the report contains a no-position and this doesn't help in
|
| 454 |
+
# the accounting. That's why the accounting is delegated to the
|
| 455 |
+
# reception of order execution
|
| 456 |
+
pass
|
| 457 |
+
|
| 458 |
+
def OnNewClosedOperations(self, Account):
|
| 459 |
+
# This call-back has not been seen
|
| 460 |
+
pass
|
| 461 |
+
|
| 462 |
+
def OnServerShutDown(self):
|
| 463 |
+
pass
|
| 464 |
+
|
| 465 |
+
def OnInternalEvent(self, p1, p2, p3):
|
| 466 |
+
pass
|
backtrader/source/backtrader/btrun/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from .btrun import btrun
|
backtrader/source/backtrader/btrun/btrun.py
ADDED
|
@@ -0,0 +1,743 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import argparse
|
| 25 |
+
import datetime
|
| 26 |
+
import inspect
|
| 27 |
+
import itertools
|
| 28 |
+
import random
|
| 29 |
+
import string
|
| 30 |
+
import sys
|
| 31 |
+
|
| 32 |
+
import backtrader as bt
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
DATAFORMATS = dict(
|
| 36 |
+
btcsv=bt.feeds.BacktraderCSVData,
|
| 37 |
+
vchartcsv=bt.feeds.VChartCSVData,
|
| 38 |
+
vcfile=bt.feeds.VChartFile,
|
| 39 |
+
sierracsv=bt.feeds.SierraChartCSVData,
|
| 40 |
+
mt4csv=bt.feeds.MT4CSVData,
|
| 41 |
+
yahoocsv=bt.feeds.YahooFinanceCSVData,
|
| 42 |
+
yahoocsv_unreversed=bt.feeds.YahooFinanceCSVData,
|
| 43 |
+
yahoo=bt.feeds.YahooFinanceData,
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
try:
|
| 47 |
+
DATAFORMATS['vcdata'] = bt.feeds.VCData
|
| 48 |
+
except AttributeError:
|
| 49 |
+
pass # no comtypes available
|
| 50 |
+
|
| 51 |
+
try:
|
| 52 |
+
DATAFORMATS['ibdata'] = bt.feeds.IBData,
|
| 53 |
+
except AttributeError:
|
| 54 |
+
pass # no ibpy available
|
| 55 |
+
|
| 56 |
+
try:
|
| 57 |
+
DATAFORMATS['oandadata'] = bt.feeds.OandaData,
|
| 58 |
+
except AttributeError:
|
| 59 |
+
pass # no oandapy available
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
TIMEFRAMES = dict(
|
| 63 |
+
microseconds=bt.TimeFrame.MicroSeconds,
|
| 64 |
+
seconds=bt.TimeFrame.Seconds,
|
| 65 |
+
minutes=bt.TimeFrame.Minutes,
|
| 66 |
+
days=bt.TimeFrame.Days,
|
| 67 |
+
weeks=bt.TimeFrame.Weeks,
|
| 68 |
+
months=bt.TimeFrame.Months,
|
| 69 |
+
years=bt.TimeFrame.Years,
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def btrun(pargs=''):
|
| 74 |
+
args = parse_args(pargs)
|
| 75 |
+
|
| 76 |
+
if args.flush:
|
| 77 |
+
import backtrader.utils.flushfile
|
| 78 |
+
|
| 79 |
+
stdstats = not args.nostdstats
|
| 80 |
+
|
| 81 |
+
cer_kwargs_str = args.cerebro
|
| 82 |
+
cer_kwargs = eval('dict(' + cer_kwargs_str + ')')
|
| 83 |
+
if 'stdstats' not in cer_kwargs:
|
| 84 |
+
cer_kwargs.update(stdstats=stdstats)
|
| 85 |
+
|
| 86 |
+
cerebro = bt.Cerebro(**cer_kwargs)
|
| 87 |
+
|
| 88 |
+
if args.resample is not None or args.replay is not None:
|
| 89 |
+
if args.resample is not None:
|
| 90 |
+
tfcp = args.resample.split(':')
|
| 91 |
+
elif args.replay is not None:
|
| 92 |
+
tfcp = args.replay.split(':')
|
| 93 |
+
|
| 94 |
+
# compression may be skipped and it will default to 1
|
| 95 |
+
if len(tfcp) == 1 or tfcp[1] == '':
|
| 96 |
+
tf, cp = tfcp[0], 1
|
| 97 |
+
else:
|
| 98 |
+
tf, cp = tfcp
|
| 99 |
+
|
| 100 |
+
cp = int(cp) # convert any value to int
|
| 101 |
+
tf = TIMEFRAMES.get(tf, None)
|
| 102 |
+
|
| 103 |
+
for data in getdatas(args):
|
| 104 |
+
if args.resample is not None:
|
| 105 |
+
cerebro.resampledata(data, timeframe=tf, compression=cp)
|
| 106 |
+
elif args.replay is not None:
|
| 107 |
+
cerebro.replaydata(data, timeframe=tf, compression=cp)
|
| 108 |
+
else:
|
| 109 |
+
cerebro.adddata(data)
|
| 110 |
+
|
| 111 |
+
# get and add signals
|
| 112 |
+
signals = getobjects(args.signals, bt.Indicator, bt.signals, issignal=True)
|
| 113 |
+
for sig, kwargs, sigtype in signals:
|
| 114 |
+
stype = getattr(bt.signal, 'SIGNAL_' + sigtype.upper())
|
| 115 |
+
cerebro.add_signal(stype, sig, **kwargs)
|
| 116 |
+
|
| 117 |
+
# get and add strategies
|
| 118 |
+
strategies = getobjects(args.strategies, bt.Strategy, bt.strategies)
|
| 119 |
+
for strat, kwargs in strategies:
|
| 120 |
+
cerebro.addstrategy(strat, **kwargs)
|
| 121 |
+
|
| 122 |
+
inds = getobjects(args.indicators, bt.Indicator, bt.indicators)
|
| 123 |
+
for ind, kwargs in inds:
|
| 124 |
+
cerebro.addindicator(ind, **kwargs)
|
| 125 |
+
|
| 126 |
+
obs = getobjects(args.observers, bt.Observer, bt.observers)
|
| 127 |
+
for ob, kwargs in obs:
|
| 128 |
+
cerebro.addobserver(ob, **kwargs)
|
| 129 |
+
|
| 130 |
+
ans = getobjects(args.analyzers, bt.Analyzer, bt.analyzers)
|
| 131 |
+
for an, kwargs in ans:
|
| 132 |
+
cerebro.addanalyzer(an, **kwargs)
|
| 133 |
+
|
| 134 |
+
setbroker(args, cerebro)
|
| 135 |
+
|
| 136 |
+
for wrkwargs_str in args.writers or []:
|
| 137 |
+
wrkwargs = eval('dict(' + wrkwargs_str + ')')
|
| 138 |
+
cerebro.addwriter(bt.WriterFile, **wrkwargs)
|
| 139 |
+
|
| 140 |
+
ans = getfunctions(args.hooks, bt.Cerebro)
|
| 141 |
+
for hook, kwargs in ans:
|
| 142 |
+
hook(cerebro, **kwargs)
|
| 143 |
+
runsts = cerebro.run()
|
| 144 |
+
runst = runsts[0] # single strategy and no optimization
|
| 145 |
+
|
| 146 |
+
if args.pranalyzer or args.ppranalyzer:
|
| 147 |
+
if runst.analyzers:
|
| 148 |
+
print('====================')
|
| 149 |
+
print('== Analyzers')
|
| 150 |
+
print('====================')
|
| 151 |
+
for name, analyzer in runst.analyzers.getitems():
|
| 152 |
+
if args.pranalyzer:
|
| 153 |
+
analyzer.print()
|
| 154 |
+
elif args.ppranalyzer:
|
| 155 |
+
print('##########')
|
| 156 |
+
print(name)
|
| 157 |
+
print('##########')
|
| 158 |
+
analyzer.pprint()
|
| 159 |
+
|
| 160 |
+
if args.plot:
|
| 161 |
+
pkwargs = dict(style='bar')
|
| 162 |
+
if args.plot is not True:
|
| 163 |
+
# evaluates to True but is not "True" - args were passed
|
| 164 |
+
ekwargs = eval('dict(' + args.plot + ')')
|
| 165 |
+
pkwargs.update(ekwargs)
|
| 166 |
+
|
| 167 |
+
# cerebro.plot(numfigs=args.plotfigs, style=args.plotstyle)
|
| 168 |
+
cerebro.plot(**pkwargs)
|
| 169 |
+
|
| 170 |
+
|
| 171 |
+
def setbroker(args, cerebro):
|
| 172 |
+
broker = cerebro.getbroker()
|
| 173 |
+
|
| 174 |
+
if args.cash is not None:
|
| 175 |
+
broker.setcash(args.cash)
|
| 176 |
+
|
| 177 |
+
commkwargs = dict()
|
| 178 |
+
if args.commission is not None:
|
| 179 |
+
commkwargs['commission'] = args.commission
|
| 180 |
+
if args.margin is not None:
|
| 181 |
+
commkwargs['margin'] = args.margin
|
| 182 |
+
if args.mult is not None:
|
| 183 |
+
commkwargs['mult'] = args.mult
|
| 184 |
+
if args.interest is not None:
|
| 185 |
+
commkwargs['interest'] = args.interest
|
| 186 |
+
if args.interest_long is not None:
|
| 187 |
+
commkwargs['interest_long'] = args.interest_long
|
| 188 |
+
|
| 189 |
+
if commkwargs:
|
| 190 |
+
broker.setcommission(**commkwargs)
|
| 191 |
+
|
| 192 |
+
if args.slip_perc is not None:
|
| 193 |
+
cerebro.broker.set_slippage_perc(args.slip_perc,
|
| 194 |
+
slip_open=args.slip_open,
|
| 195 |
+
slip_match=not args.no_slip_match,
|
| 196 |
+
slip_out=args.slip_out)
|
| 197 |
+
elif args.slip_fixed is not None:
|
| 198 |
+
cerebro.broker.set_slippage_fixed(args.slip_fixed,
|
| 199 |
+
slip_open=args.slip_open,
|
| 200 |
+
slip_match=not args.no_slip_match,
|
| 201 |
+
slip_out=args.slip_out)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
def getdatas(args):
|
| 205 |
+
# Get the data feed class from the global dictionary
|
| 206 |
+
dfcls = DATAFORMATS[args.format]
|
| 207 |
+
|
| 208 |
+
# Prepare some args
|
| 209 |
+
dfkwargs = dict()
|
| 210 |
+
if args.format == 'yahoo_unreversed':
|
| 211 |
+
dfkwargs['reverse'] = True
|
| 212 |
+
|
| 213 |
+
fmtstr = '%Y-%m-%d'
|
| 214 |
+
if args.fromdate:
|
| 215 |
+
dtsplit = args.fromdate.split('T')
|
| 216 |
+
if len(dtsplit) > 1:
|
| 217 |
+
fmtstr += 'T%H:%M:%S'
|
| 218 |
+
|
| 219 |
+
fromdate = datetime.datetime.strptime(args.fromdate, fmtstr)
|
| 220 |
+
dfkwargs['fromdate'] = fromdate
|
| 221 |
+
|
| 222 |
+
fmtstr = '%Y-%m-%d'
|
| 223 |
+
if args.todate:
|
| 224 |
+
dtsplit = args.todate.split('T')
|
| 225 |
+
if len(dtsplit) > 1:
|
| 226 |
+
fmtstr += 'T%H:%M:%S'
|
| 227 |
+
todate = datetime.datetime.strptime(args.todate, fmtstr)
|
| 228 |
+
dfkwargs['todate'] = todate
|
| 229 |
+
|
| 230 |
+
if args.timeframe is not None:
|
| 231 |
+
dfkwargs['timeframe'] = TIMEFRAMES[args.timeframe]
|
| 232 |
+
|
| 233 |
+
if args.compression is not None:
|
| 234 |
+
dfkwargs['compression'] = args.compression
|
| 235 |
+
|
| 236 |
+
datas = list()
|
| 237 |
+
for dname in args.data:
|
| 238 |
+
dfkwargs['dataname'] = dname
|
| 239 |
+
data = dfcls(**dfkwargs)
|
| 240 |
+
datas.append(data)
|
| 241 |
+
|
| 242 |
+
return datas
|
| 243 |
+
|
| 244 |
+
|
| 245 |
+
def getmodclasses(mod, clstype, clsname=None):
|
| 246 |
+
clsmembers = inspect.getmembers(mod, inspect.isclass)
|
| 247 |
+
|
| 248 |
+
clslist = list()
|
| 249 |
+
for name, cls in clsmembers:
|
| 250 |
+
if not issubclass(cls, clstype):
|
| 251 |
+
continue
|
| 252 |
+
|
| 253 |
+
if clsname:
|
| 254 |
+
if clsname == name:
|
| 255 |
+
clslist.append(cls)
|
| 256 |
+
break
|
| 257 |
+
else:
|
| 258 |
+
clslist.append(cls)
|
| 259 |
+
|
| 260 |
+
return clslist
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
def getmodfunctions(mod, funcname=None):
|
| 264 |
+
members = inspect.getmembers(mod, inspect.isfunction) + \
|
| 265 |
+
inspect.getmembers(mod, inspect.ismethod)
|
| 266 |
+
|
| 267 |
+
funclist = list()
|
| 268 |
+
for name, member in members:
|
| 269 |
+
if funcname:
|
| 270 |
+
if name == funcname:
|
| 271 |
+
funclist.append(member)
|
| 272 |
+
break
|
| 273 |
+
else:
|
| 274 |
+
funclist.append(member)
|
| 275 |
+
|
| 276 |
+
return funclist
|
| 277 |
+
|
| 278 |
+
|
| 279 |
+
def loadmodule(modpath, modname=''):
|
| 280 |
+
# generate a random name for the module
|
| 281 |
+
|
| 282 |
+
if not modpath.endswith('.py'):
|
| 283 |
+
modpath += '.py'
|
| 284 |
+
|
| 285 |
+
if not modname:
|
| 286 |
+
chars = string.ascii_uppercase + string.digits
|
| 287 |
+
modname = ''.join(random.choice(chars) for _ in range(10))
|
| 288 |
+
|
| 289 |
+
version = (sys.version_info[0], sys.version_info[1])
|
| 290 |
+
|
| 291 |
+
if version < (3, 3):
|
| 292 |
+
mod, e = loadmodule2(modpath, modname)
|
| 293 |
+
else:
|
| 294 |
+
mod, e = loadmodule3(modpath, modname)
|
| 295 |
+
|
| 296 |
+
return mod, e
|
| 297 |
+
|
| 298 |
+
|
| 299 |
+
def loadmodule2(modpath, modname):
|
| 300 |
+
import imp
|
| 301 |
+
|
| 302 |
+
try:
|
| 303 |
+
mod = imp.load_source(modname, modpath)
|
| 304 |
+
except Exception as e:
|
| 305 |
+
return (None, e)
|
| 306 |
+
|
| 307 |
+
return (mod, None)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
def loadmodule3(modpath, modname):
|
| 311 |
+
import importlib.machinery
|
| 312 |
+
|
| 313 |
+
try:
|
| 314 |
+
loader = importlib.machinery.SourceFileLoader(modname, modpath)
|
| 315 |
+
mod = loader.load_module()
|
| 316 |
+
except Exception as e:
|
| 317 |
+
return (None, e)
|
| 318 |
+
|
| 319 |
+
return (mod, None)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def getobjects(iterable, clsbase, modbase, issignal=False):
|
| 323 |
+
retobjects = list()
|
| 324 |
+
|
| 325 |
+
for item in iterable or []:
|
| 326 |
+
if issignal:
|
| 327 |
+
sigtokens = item.split('+', 1)
|
| 328 |
+
if len(sigtokens) == 1: # no + seen
|
| 329 |
+
sigtype = 'longshort'
|
| 330 |
+
else:
|
| 331 |
+
sigtype, item = sigtokens
|
| 332 |
+
|
| 333 |
+
tokens = item.split(':', 1)
|
| 334 |
+
|
| 335 |
+
if len(tokens) == 1:
|
| 336 |
+
modpath = tokens[0]
|
| 337 |
+
name = ''
|
| 338 |
+
kwargs = dict()
|
| 339 |
+
else:
|
| 340 |
+
modpath, name = tokens
|
| 341 |
+
kwtokens = name.split(':', 1)
|
| 342 |
+
if len(kwtokens) == 1:
|
| 343 |
+
# no '(' found
|
| 344 |
+
kwargs = dict()
|
| 345 |
+
else:
|
| 346 |
+
name = kwtokens[0]
|
| 347 |
+
kwtext = 'dict(' + kwtokens[1] + ')'
|
| 348 |
+
kwargs = eval(kwtext)
|
| 349 |
+
|
| 350 |
+
if modpath:
|
| 351 |
+
mod, e = loadmodule(modpath)
|
| 352 |
+
|
| 353 |
+
if not mod:
|
| 354 |
+
print('')
|
| 355 |
+
print('Failed to load module %s:' % modpath, e)
|
| 356 |
+
sys.exit(1)
|
| 357 |
+
else:
|
| 358 |
+
mod = modbase
|
| 359 |
+
|
| 360 |
+
loaded = getmodclasses(mod=mod, clstype=clsbase, clsname=name)
|
| 361 |
+
|
| 362 |
+
if not loaded:
|
| 363 |
+
print('No class %s / module %s' % (str(name), modpath))
|
| 364 |
+
sys.exit(1)
|
| 365 |
+
|
| 366 |
+
if issignal:
|
| 367 |
+
retobjects.append((loaded[0], kwargs, sigtype))
|
| 368 |
+
else:
|
| 369 |
+
retobjects.append((loaded[0], kwargs))
|
| 370 |
+
|
| 371 |
+
return retobjects
|
| 372 |
+
|
| 373 |
+
def getfunctions(iterable, modbase):
|
| 374 |
+
retfunctions = list()
|
| 375 |
+
|
| 376 |
+
for item in iterable or []:
|
| 377 |
+
tokens = item.split(':', 1)
|
| 378 |
+
|
| 379 |
+
if len(tokens) == 1:
|
| 380 |
+
modpath = tokens[0]
|
| 381 |
+
name = ''
|
| 382 |
+
kwargs = dict()
|
| 383 |
+
else:
|
| 384 |
+
modpath, name = tokens
|
| 385 |
+
kwtokens = name.split(':', 1)
|
| 386 |
+
if len(kwtokens) == 1:
|
| 387 |
+
# no '(' found
|
| 388 |
+
kwargs = dict()
|
| 389 |
+
else:
|
| 390 |
+
name = kwtokens[0]
|
| 391 |
+
kwtext = 'dict(' + kwtokens[1] + ')'
|
| 392 |
+
kwargs = eval(kwtext)
|
| 393 |
+
|
| 394 |
+
if modpath:
|
| 395 |
+
mod, e = loadmodule(modpath)
|
| 396 |
+
|
| 397 |
+
if not mod:
|
| 398 |
+
print('')
|
| 399 |
+
print('Failed to load module %s:' % modpath, e)
|
| 400 |
+
sys.exit(1)
|
| 401 |
+
else:
|
| 402 |
+
mod = modbase
|
| 403 |
+
|
| 404 |
+
loaded = getmodfunctions(mod=mod, funcname=name)
|
| 405 |
+
|
| 406 |
+
if not loaded:
|
| 407 |
+
print('No function %s / module %s' % (str(name), modpath))
|
| 408 |
+
sys.exit(1)
|
| 409 |
+
|
| 410 |
+
retfunctions.append((loaded[0], kwargs))
|
| 411 |
+
|
| 412 |
+
return retfunctions
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def parse_args(pargs=''):
|
| 416 |
+
parser = argparse.ArgumentParser(
|
| 417 |
+
description='Backtrader Run Script',
|
| 418 |
+
formatter_class=argparse.RawTextHelpFormatter,
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
group = parser.add_argument_group(title='Data options')
|
| 422 |
+
# Data options
|
| 423 |
+
group.add_argument('--data', '-d', action='append', required=True,
|
| 424 |
+
help='Data files to be added to the system')
|
| 425 |
+
|
| 426 |
+
group = parser.add_argument_group(title='Cerebro options')
|
| 427 |
+
group.add_argument(
|
| 428 |
+
'--cerebro', '-cer',
|
| 429 |
+
metavar='kwargs',
|
| 430 |
+
required=False, const='', default='', nargs='?',
|
| 431 |
+
help=('The argument can be specified with the following form:\n'
|
| 432 |
+
'\n'
|
| 433 |
+
' - kwargs\n'
|
| 434 |
+
'\n'
|
| 435 |
+
' Example: "preload=True" which set its to True\n'
|
| 436 |
+
'\n'
|
| 437 |
+
'The passed kwargs will be passed directly to the cerebro\n'
|
| 438 |
+
'instance created for the execution\n'
|
| 439 |
+
'\n'
|
| 440 |
+
'The available kwargs to cerebro are:\n'
|
| 441 |
+
' - preload (default: True)\n'
|
| 442 |
+
' - runonce (default: True)\n'
|
| 443 |
+
' - maxcpus (default: None)\n'
|
| 444 |
+
' - stdstats (default: True)\n'
|
| 445 |
+
' - live (default: False)\n'
|
| 446 |
+
' - exactbars (default: False)\n'
|
| 447 |
+
' - preload (default: True)\n'
|
| 448 |
+
' - writer (default False)\n'
|
| 449 |
+
' - oldbuysell (default False)\n'
|
| 450 |
+
' - tradehistory (default False)\n')
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
group.add_argument('--nostdstats', action='store_true',
|
| 454 |
+
help='Disable the standard statistics observers')
|
| 455 |
+
|
| 456 |
+
datakeys = list(DATAFORMATS)
|
| 457 |
+
group.add_argument('--format', '--csvformat', '-c', required=False,
|
| 458 |
+
default='btcsv', choices=datakeys,
|
| 459 |
+
help='CSV Format')
|
| 460 |
+
|
| 461 |
+
group.add_argument('--fromdate', '-f', required=False, default=None,
|
| 462 |
+
help='Starting date in YYYY-MM-DD[THH:MM:SS] format')
|
| 463 |
+
|
| 464 |
+
group.add_argument('--todate', '-t', required=False, default=None,
|
| 465 |
+
help='Ending date in YYYY-MM-DD[THH:MM:SS] format')
|
| 466 |
+
|
| 467 |
+
group.add_argument('--timeframe', '-tf', required=False, default='days',
|
| 468 |
+
choices=TIMEFRAMES.keys(),
|
| 469 |
+
help='Ending date in YYYY-MM-DD[THH:MM:SS] format')
|
| 470 |
+
|
| 471 |
+
group.add_argument('--compression', '-cp', required=False, default=1,
|
| 472 |
+
type=int,
|
| 473 |
+
help='Ending date in YYYY-MM-DD[THH:MM:SS] format')
|
| 474 |
+
|
| 475 |
+
group = parser.add_mutually_exclusive_group(required=False)
|
| 476 |
+
|
| 477 |
+
group.add_argument('--resample', '-rs', required=False, default=None,
|
| 478 |
+
help='resample with timeframe:compression values')
|
| 479 |
+
|
| 480 |
+
group.add_argument('--replay', '-rp', required=False, default=None,
|
| 481 |
+
help='replay with timeframe:compression values')
|
| 482 |
+
|
| 483 |
+
group.add_argument(
|
| 484 |
+
'--hook', dest='hooks',
|
| 485 |
+
action='append', required=False,
|
| 486 |
+
metavar='module:hookfunction:kwargs',
|
| 487 |
+
help=('This option can be specified multiple times.\n'
|
| 488 |
+
'\n'
|
| 489 |
+
'The argument can be specified with the following form:\n'
|
| 490 |
+
'\n'
|
| 491 |
+
' - module:hookfunction:kwargs\n'
|
| 492 |
+
'\n'
|
| 493 |
+
' Example: mymod:myhook:a=1,b=2\n'
|
| 494 |
+
'\n'
|
| 495 |
+
'kwargs is optional\n'
|
| 496 |
+
'\n'
|
| 497 |
+
'If module is omitted then hookfunction will be sought\n'
|
| 498 |
+
'as the built-in cerebro method. Example:\n'
|
| 499 |
+
'\n'
|
| 500 |
+
' - :addtz:tz=America/St_Johns\n'
|
| 501 |
+
'\n'
|
| 502 |
+
'If name is omitted, then the 1st function found in the\n'
|
| 503 |
+
'mod will be used. Such as in:\n'
|
| 504 |
+
'\n'
|
| 505 |
+
' - module or module::kwargs\n'
|
| 506 |
+
'\n'
|
| 507 |
+
'The function specified will be called, with cerebro\n'
|
| 508 |
+
'instance passed as the first argument together with\n'
|
| 509 |
+
'kwargs, if any were specified. This allows to customize\n'
|
| 510 |
+
'cerebro, beyond options provided by this script\n\n')
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
# Module where to read the strategy from
|
| 514 |
+
group = parser.add_argument_group(title='Strategy options')
|
| 515 |
+
group.add_argument(
|
| 516 |
+
'--strategy', '-st', dest='strategies',
|
| 517 |
+
action='append', required=False,
|
| 518 |
+
metavar='module:name:kwargs',
|
| 519 |
+
help=('This option can be specified multiple times.\n'
|
| 520 |
+
'\n'
|
| 521 |
+
'The argument can be specified with the following form:\n'
|
| 522 |
+
'\n'
|
| 523 |
+
' - module:classname:kwargs\n'
|
| 524 |
+
'\n'
|
| 525 |
+
' Example: mymod:myclass:a=1,b=2\n'
|
| 526 |
+
'\n'
|
| 527 |
+
'kwargs is optional\n'
|
| 528 |
+
'\n'
|
| 529 |
+
'If module is omitted then class name will be sought in\n'
|
| 530 |
+
'the built-in strategies module. Such as in:\n'
|
| 531 |
+
'\n'
|
| 532 |
+
' - :name:kwargs or :name\n'
|
| 533 |
+
'\n'
|
| 534 |
+
'If name is omitted, then the 1st strategy found in the mod\n'
|
| 535 |
+
'will be used. Such as in:\n'
|
| 536 |
+
'\n'
|
| 537 |
+
' - module or module::kwargs')
|
| 538 |
+
)
|
| 539 |
+
|
| 540 |
+
# Module where to read the strategy from
|
| 541 |
+
group = parser.add_argument_group(title='Signals')
|
| 542 |
+
group.add_argument(
|
| 543 |
+
'--signal', '-sig', dest='signals',
|
| 544 |
+
action='append', required=False,
|
| 545 |
+
metavar='module:signaltype:name:kwargs',
|
| 546 |
+
help=('This option can be specified multiple times.\n'
|
| 547 |
+
'\n'
|
| 548 |
+
'The argument can be specified with the following form:\n'
|
| 549 |
+
'\n'
|
| 550 |
+
' - signaltype:module:signaltype:classname:kwargs\n'
|
| 551 |
+
'\n'
|
| 552 |
+
' Example: longshort+mymod:myclass:a=1,b=2\n'
|
| 553 |
+
'\n'
|
| 554 |
+
'signaltype may be ommited: longshort will be used\n'
|
| 555 |
+
'\n'
|
| 556 |
+
' Example: mymod:myclass:a=1,b=2\n'
|
| 557 |
+
'\n'
|
| 558 |
+
'kwargs is optional\n'
|
| 559 |
+
'\n'
|
| 560 |
+
'signaltype will be uppercased to match the defintions\n'
|
| 561 |
+
'fromt the backtrader.signal module\n'
|
| 562 |
+
'\n'
|
| 563 |
+
'If module is omitted then class name will be sought in\n'
|
| 564 |
+
'the built-in signals module. Such as in:\n'
|
| 565 |
+
'\n'
|
| 566 |
+
' - LONGSHORT::name:kwargs or :name\n'
|
| 567 |
+
'\n'
|
| 568 |
+
'If name is omitted, then the 1st signal found in the mod\n'
|
| 569 |
+
'will be used. Such as in:\n'
|
| 570 |
+
'\n'
|
| 571 |
+
' - module or module:::kwargs')
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
# Observers
|
| 575 |
+
group = parser.add_argument_group(title='Observers and statistics')
|
| 576 |
+
group.add_argument(
|
| 577 |
+
'--observer', '-ob', dest='observers',
|
| 578 |
+
action='append', required=False,
|
| 579 |
+
metavar='module:name:kwargs',
|
| 580 |
+
help=('This option can be specified multiple times.\n'
|
| 581 |
+
'\n'
|
| 582 |
+
'The argument can be specified with the following form:\n'
|
| 583 |
+
'\n'
|
| 584 |
+
' - module:classname:kwargs\n'
|
| 585 |
+
'\n'
|
| 586 |
+
' Example: mymod:myclass:a=1,b=2\n'
|
| 587 |
+
'\n'
|
| 588 |
+
'kwargs is optional\n'
|
| 589 |
+
'\n'
|
| 590 |
+
'If module is omitted then class name will be sought in\n'
|
| 591 |
+
'the built-in observers module. Such as in:\n'
|
| 592 |
+
'\n'
|
| 593 |
+
' - :name:kwargs or :name\n'
|
| 594 |
+
'\n'
|
| 595 |
+
'If name is omitted, then the 1st observer found in the\n'
|
| 596 |
+
'will be used. Such as in:\n'
|
| 597 |
+
'\n'
|
| 598 |
+
' - module or module::kwargs')
|
| 599 |
+
)
|
| 600 |
+
# Analyzers
|
| 601 |
+
group = parser.add_argument_group(title='Analyzers')
|
| 602 |
+
group.add_argument(
|
| 603 |
+
'--analyzer', '-an', dest='analyzers',
|
| 604 |
+
action='append', required=False,
|
| 605 |
+
metavar='module:name:kwargs',
|
| 606 |
+
help=('This option can be specified multiple times.\n'
|
| 607 |
+
'\n'
|
| 608 |
+
'The argument can be specified with the following form:\n'
|
| 609 |
+
'\n'
|
| 610 |
+
' - module:classname:kwargs\n'
|
| 611 |
+
'\n'
|
| 612 |
+
' Example: mymod:myclass:a=1,b=2\n'
|
| 613 |
+
'\n'
|
| 614 |
+
'kwargs is optional\n'
|
| 615 |
+
'\n'
|
| 616 |
+
'If module is omitted then class name will be sought in\n'
|
| 617 |
+
'the built-in analyzers module. Such as in:\n'
|
| 618 |
+
'\n'
|
| 619 |
+
' - :name:kwargs or :name\n'
|
| 620 |
+
'\n'
|
| 621 |
+
'If name is omitted, then the 1st analyzer found in the\n'
|
| 622 |
+
'will be used. Such as in:\n'
|
| 623 |
+
'\n'
|
| 624 |
+
' - module or module::kwargs')
|
| 625 |
+
)
|
| 626 |
+
|
| 627 |
+
# Analyzer - Print
|
| 628 |
+
group = parser.add_mutually_exclusive_group(required=False)
|
| 629 |
+
group.add_argument('--pranalyzer', '-pralyzer',
|
| 630 |
+
required=False, action='store_true',
|
| 631 |
+
help=('Automatically print analyzers'))
|
| 632 |
+
|
| 633 |
+
group.add_argument('--ppranalyzer', '-ppralyzer',
|
| 634 |
+
required=False, action='store_true',
|
| 635 |
+
help=('Automatically PRETTY print analyzers'))
|
| 636 |
+
|
| 637 |
+
# Indicators
|
| 638 |
+
group = parser.add_argument_group(title='Indicators')
|
| 639 |
+
group.add_argument(
|
| 640 |
+
'--indicator', '-ind', dest='indicators',
|
| 641 |
+
metavar='module:name:kwargs',
|
| 642 |
+
action='append', required=False,
|
| 643 |
+
help=('This option can be specified multiple times.\n'
|
| 644 |
+
'\n'
|
| 645 |
+
'The argument can be specified with the following form:\n'
|
| 646 |
+
'\n'
|
| 647 |
+
' - module:classname:kwargs\n'
|
| 648 |
+
'\n'
|
| 649 |
+
' Example: mymod:myclass:a=1,b=2\n'
|
| 650 |
+
'\n'
|
| 651 |
+
'kwargs is optional\n'
|
| 652 |
+
'\n'
|
| 653 |
+
'If module is omitted then class name will be sought in\n'
|
| 654 |
+
'the built-in analyzers module. Such as in:\n'
|
| 655 |
+
'\n'
|
| 656 |
+
' - :name:kwargs or :name\n'
|
| 657 |
+
'\n'
|
| 658 |
+
'If name is omitted, then the 1st analyzer found in the\n'
|
| 659 |
+
'will be used. Such as in:\n'
|
| 660 |
+
'\n'
|
| 661 |
+
' - module or module::kwargs')
|
| 662 |
+
)
|
| 663 |
+
|
| 664 |
+
# Writer
|
| 665 |
+
group = parser.add_argument_group(title='Writers')
|
| 666 |
+
group.add_argument(
|
| 667 |
+
'--writer', '-wr',
|
| 668 |
+
dest='writers', metavar='kwargs', nargs='?',
|
| 669 |
+
action='append', required=False, const='',
|
| 670 |
+
help=('This option can be specified multiple times.\n'
|
| 671 |
+
'\n'
|
| 672 |
+
'The argument can be specified with the following form:\n'
|
| 673 |
+
'\n'
|
| 674 |
+
' - kwargs\n'
|
| 675 |
+
'\n'
|
| 676 |
+
' Example: a=1,b=2\n'
|
| 677 |
+
'\n'
|
| 678 |
+
'kwargs is optional\n'
|
| 679 |
+
'\n'
|
| 680 |
+
'It creates a system wide writer which outputs run data\n'
|
| 681 |
+
'\n'
|
| 682 |
+
'Please see the documentation for the available kwargs')
|
| 683 |
+
)
|
| 684 |
+
|
| 685 |
+
# Broker/Commissions
|
| 686 |
+
group = parser.add_argument_group(title='Cash and Commission Scheme Args')
|
| 687 |
+
group.add_argument('--cash', '-cash', required=False, type=float,
|
| 688 |
+
help='Cash to set to the broker')
|
| 689 |
+
group.add_argument('--commission', '-comm', required=False, type=float,
|
| 690 |
+
help='Commission value to set')
|
| 691 |
+
group.add_argument('--margin', '-marg', required=False, type=float,
|
| 692 |
+
help='Margin type to set')
|
| 693 |
+
group.add_argument('--mult', '-mul', required=False, type=float,
|
| 694 |
+
help='Multiplier to use')
|
| 695 |
+
|
| 696 |
+
group.add_argument('--interest', required=False, type=float,
|
| 697 |
+
default=None,
|
| 698 |
+
help='Credit Interest rate to apply (0.0x)')
|
| 699 |
+
|
| 700 |
+
group.add_argument('--interest_long', action='store_true',
|
| 701 |
+
required=False, default=None,
|
| 702 |
+
help='Apply credit interest to long positions')
|
| 703 |
+
|
| 704 |
+
group.add_argument('--slip_perc', required=False, default=None,
|
| 705 |
+
type=float,
|
| 706 |
+
help='Enable slippage with a percentage value')
|
| 707 |
+
group.add_argument('--slip_fixed', required=False, default=None,
|
| 708 |
+
type=float,
|
| 709 |
+
help='Enable slippage with a fixed point value')
|
| 710 |
+
|
| 711 |
+
group.add_argument('--slip_open', required=False, action='store_true',
|
| 712 |
+
help='enable slippage for when matching opening prices')
|
| 713 |
+
|
| 714 |
+
group.add_argument('--no-slip_match', required=False, action='store_true',
|
| 715 |
+
help=('Disable slip_match, ie: matching capped at \n'
|
| 716 |
+
'high-low if slippage goes over those limits'))
|
| 717 |
+
group.add_argument('--slip_out', required=False, action='store_true',
|
| 718 |
+
help='with slip_match enabled, match outside high-low')
|
| 719 |
+
|
| 720 |
+
# Output flushing
|
| 721 |
+
group.add_argument('--flush', required=False, action='store_true',
|
| 722 |
+
help='flush the output - useful under win32 systems')
|
| 723 |
+
|
| 724 |
+
# Plot options
|
| 725 |
+
parser.add_argument(
|
| 726 |
+
'--plot', '-p', nargs='?',
|
| 727 |
+
metavar='kwargs',
|
| 728 |
+
default=False, const=True, required=False,
|
| 729 |
+
help=('Plot the read data applying any kwargs passed\n'
|
| 730 |
+
'\n'
|
| 731 |
+
'For example:\n'
|
| 732 |
+
'\n'
|
| 733 |
+
' --plot style="candle" (to plot candlesticks)\n')
|
| 734 |
+
)
|
| 735 |
+
|
| 736 |
+
if pargs:
|
| 737 |
+
return parser.parse_args(pargs)
|
| 738 |
+
|
| 739 |
+
return parser.parse_args()
|
| 740 |
+
|
| 741 |
+
|
| 742 |
+
if __name__ == '__main__':
|
| 743 |
+
btrun()
|
backtrader/source/backtrader/cerebro.py
ADDED
|
@@ -0,0 +1,1716 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import datetime
|
| 25 |
+
import collections
|
| 26 |
+
import itertools
|
| 27 |
+
import multiprocessing
|
| 28 |
+
|
| 29 |
+
try: # For new Python versions
|
| 30 |
+
collectionsAbc = collections.abc # collections.Iterable -> collections.abc.Iterable
|
| 31 |
+
except AttributeError: # For old Python versions
|
| 32 |
+
collectionsAbc = collections # Используем collections.Iterable
|
| 33 |
+
|
| 34 |
+
import backtrader as bt
|
| 35 |
+
from .utils.py3 import (map, range, zip, with_metaclass, string_types,
|
| 36 |
+
integer_types)
|
| 37 |
+
|
| 38 |
+
from . import linebuffer
|
| 39 |
+
from . import indicator
|
| 40 |
+
from .brokers import BackBroker
|
| 41 |
+
from .metabase import MetaParams
|
| 42 |
+
from . import observers
|
| 43 |
+
from .writer import WriterFile
|
| 44 |
+
from .utils import OrderedDict, tzparse, num2date, date2num
|
| 45 |
+
from .strategy import Strategy, SignalStrategy
|
| 46 |
+
from .tradingcal import (TradingCalendarBase, TradingCalendar,
|
| 47 |
+
PandasMarketCalendar)
|
| 48 |
+
from .timer import Timer
|
| 49 |
+
|
| 50 |
+
# Defined here to make it pickable. Ideally it could be defined inside Cerebro
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
class OptReturn(object):
|
| 54 |
+
def __init__(self, params, **kwargs):
|
| 55 |
+
self.p = self.params = params
|
| 56 |
+
for k, v in kwargs.items():
|
| 57 |
+
setattr(self, k, v)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class Cerebro(with_metaclass(MetaParams, object)):
|
| 61 |
+
'''Params:
|
| 62 |
+
|
| 63 |
+
- ``preload`` (default: ``True``)
|
| 64 |
+
|
| 65 |
+
Whether to preload the different ``data feeds`` passed to cerebro for
|
| 66 |
+
the Strategies
|
| 67 |
+
|
| 68 |
+
- ``runonce`` (default: ``True``)
|
| 69 |
+
|
| 70 |
+
Run ``Indicators`` in vectorized mode to speed up the entire system.
|
| 71 |
+
Strategies and Observers will always be run on an event based basis
|
| 72 |
+
|
| 73 |
+
- ``live`` (default: ``False``)
|
| 74 |
+
|
| 75 |
+
If no data has reported itself as *live* (via the data's ``islive``
|
| 76 |
+
method but the end user still want to run in ``live`` mode, this
|
| 77 |
+
parameter can be set to true
|
| 78 |
+
|
| 79 |
+
This will simultaneously deactivate ``preload`` and ``runonce``. It
|
| 80 |
+
will have no effect on memory saving schemes.
|
| 81 |
+
|
| 82 |
+
Run ``Indicators`` in vectorized mode to speed up the entire system.
|
| 83 |
+
Strategies and Observers will always be run on an event based basis
|
| 84 |
+
|
| 85 |
+
- ``maxcpus`` (default: None -> all available cores)
|
| 86 |
+
|
| 87 |
+
How many cores to use simultaneously for optimization
|
| 88 |
+
|
| 89 |
+
- ``stdstats`` (default: ``True``)
|
| 90 |
+
|
| 91 |
+
If True default Observers will be added: Broker (Cash and Value),
|
| 92 |
+
Trades and BuySell
|
| 93 |
+
|
| 94 |
+
- ``oldbuysell`` (default: ``False``)
|
| 95 |
+
|
| 96 |
+
If ``stdstats`` is ``True`` and observers are getting automatically
|
| 97 |
+
added, this switch controls the main behavior of the ``BuySell``
|
| 98 |
+
observer
|
| 99 |
+
|
| 100 |
+
- ``False``: use the modern behavior in which the buy / sell signals
|
| 101 |
+
are plotted below / above the low / high prices respectively to avoid
|
| 102 |
+
cluttering the plot
|
| 103 |
+
|
| 104 |
+
- ``True``: use the deprecated behavior in which the buy / sell signals
|
| 105 |
+
are plotted where the average price of the order executions for the
|
| 106 |
+
given moment in time is. This will of course be on top of an OHLC bar
|
| 107 |
+
or on a Line on Cloe bar, difficulting the recognition of the plot.
|
| 108 |
+
|
| 109 |
+
- ``oldtrades`` (default: ``False``)
|
| 110 |
+
|
| 111 |
+
If ``stdstats`` is ``True`` and observers are getting automatically
|
| 112 |
+
added, this switch controls the main behavior of the ``Trades``
|
| 113 |
+
observer
|
| 114 |
+
|
| 115 |
+
- ``False``: use the modern behavior in which trades for all datas are
|
| 116 |
+
plotted with different markers
|
| 117 |
+
|
| 118 |
+
- ``True``: use the old Trades observer which plots the trades with the
|
| 119 |
+
same markers, differentiating only if they are positive or negative
|
| 120 |
+
|
| 121 |
+
- ``exactbars`` (default: ``False``)
|
| 122 |
+
|
| 123 |
+
With the default value each and every value stored in a line is kept in
|
| 124 |
+
memory
|
| 125 |
+
|
| 126 |
+
Possible values:
|
| 127 |
+
- ``True`` or ``1``: all "lines" objects reduce memory usage to the
|
| 128 |
+
automatically calculated minimum period.
|
| 129 |
+
|
| 130 |
+
If a Simple Moving Average has a period of 30, the underlying data
|
| 131 |
+
will have always a running buffer of 30 bars to allow the
|
| 132 |
+
calculation of the Simple Moving Average
|
| 133 |
+
|
| 134 |
+
- This setting will deactivate ``preload`` and ``runonce``
|
| 135 |
+
- Using this setting also deactivates **plotting**
|
| 136 |
+
|
| 137 |
+
- ``-1``: datafreeds and indicators/operations at strategy level will
|
| 138 |
+
keep all data in memory.
|
| 139 |
+
|
| 140 |
+
For example: a ``RSI`` internally uses the indicator ``UpDay`` to
|
| 141 |
+
make calculations. This subindicator will not keep all data in
|
| 142 |
+
memory
|
| 143 |
+
|
| 144 |
+
- This allows to keep ``plotting`` and ``preloading`` active.
|
| 145 |
+
|
| 146 |
+
- ``runonce`` will be deactivated
|
| 147 |
+
|
| 148 |
+
- ``-2``: data feeds and indicators kept as attributes of the
|
| 149 |
+
strategy will keep all points in memory.
|
| 150 |
+
|
| 151 |
+
For example: a ``RSI`` internally uses the indicator ``UpDay`` to
|
| 152 |
+
make calculations. This subindicator will not keep all data in
|
| 153 |
+
memory
|
| 154 |
+
|
| 155 |
+
If in the ``__init__`` something like
|
| 156 |
+
``a = self.data.close - self.data.high`` is defined, then ``a``
|
| 157 |
+
will not keep all data in memory
|
| 158 |
+
|
| 159 |
+
- This allows to keep ``plotting`` and ``preloading`` active.
|
| 160 |
+
|
| 161 |
+
- ``runonce`` will be deactivated
|
| 162 |
+
|
| 163 |
+
- ``objcache`` (default: ``False``)
|
| 164 |
+
|
| 165 |
+
Experimental option to implement a cache of lines objects and reduce
|
| 166 |
+
the amount of them. Example from UltimateOscillator::
|
| 167 |
+
|
| 168 |
+
bp = self.data.close - TrueLow(self.data)
|
| 169 |
+
tr = TrueRange(self.data) # -> creates another TrueLow(self.data)
|
| 170 |
+
|
| 171 |
+
If this is ``True`` the 2nd ``TrueLow(self.data)`` inside ``TrueRange``
|
| 172 |
+
matches the signature of the one in the ``bp`` calculation. It will be
|
| 173 |
+
reused.
|
| 174 |
+
|
| 175 |
+
Corner cases may happen in which this drives a line object off its
|
| 176 |
+
minimum period and breaks things and it is therefore disabled.
|
| 177 |
+
|
| 178 |
+
- ``writer`` (default: ``False``)
|
| 179 |
+
|
| 180 |
+
If set to ``True`` a default WriterFile will be created which will
|
| 181 |
+
print to stdout. It will be added to the strategy (in addition to any
|
| 182 |
+
other writers added by the user code)
|
| 183 |
+
|
| 184 |
+
- ``tradehistory`` (default: ``False``)
|
| 185 |
+
|
| 186 |
+
If set to ``True``, it will activate update event logging in each trade
|
| 187 |
+
for all strategies. This can also be accomplished on a per strategy
|
| 188 |
+
basis with the strategy method ``set_tradehistory``
|
| 189 |
+
|
| 190 |
+
- ``optdatas`` (default: ``True``)
|
| 191 |
+
|
| 192 |
+
If ``True`` and optimizing (and the system can ``preload`` and use
|
| 193 |
+
``runonce``, data preloading will be done only once in the main process
|
| 194 |
+
to save time and resources.
|
| 195 |
+
|
| 196 |
+
The tests show an approximate ``20%`` speed-up moving from a sample
|
| 197 |
+
execution in ``83`` seconds to ``66``
|
| 198 |
+
|
| 199 |
+
- ``optreturn`` (default: ``True``)
|
| 200 |
+
|
| 201 |
+
If ``True`` the optimization results will not be full ``Strategy``
|
| 202 |
+
objects (and all *datas*, *indicators*, *observers* ...) but and object
|
| 203 |
+
with the following attributes (same as in ``Strategy``):
|
| 204 |
+
|
| 205 |
+
- ``params`` (or ``p``) the strategy had for the execution
|
| 206 |
+
- ``analyzers`` the strategy has executed
|
| 207 |
+
|
| 208 |
+
In most occassions, only the *analyzers* and with which *params* are
|
| 209 |
+
the things needed to evaluate a the performance of a strategy. If
|
| 210 |
+
detailed analysis of the generated values for (for example)
|
| 211 |
+
*indicators* is needed, turn this off
|
| 212 |
+
|
| 213 |
+
The tests show a ``13% - 15%`` improvement in execution time. Combined
|
| 214 |
+
with ``optdatas`` the total gain increases to a total speed-up of
|
| 215 |
+
``32%`` in an optimization run.
|
| 216 |
+
|
| 217 |
+
- ``oldsync`` (default: ``False``)
|
| 218 |
+
|
| 219 |
+
Starting with release 1.9.0.99 the synchronization of multiple datas
|
| 220 |
+
(same or different timeframes) has been changed to allow datas of
|
| 221 |
+
different lengths.
|
| 222 |
+
|
| 223 |
+
If the old behavior with data0 as the master of the system is wished,
|
| 224 |
+
set this parameter to true
|
| 225 |
+
|
| 226 |
+
- ``tz`` (default: ``None``)
|
| 227 |
+
|
| 228 |
+
Adds a global timezone for strategies. The argument ``tz`` can be
|
| 229 |
+
|
| 230 |
+
- ``None``: in this case the datetime displayed by strategies will be
|
| 231 |
+
in UTC, which has been always the standard behavior
|
| 232 |
+
|
| 233 |
+
- ``pytz`` instance. It will be used as such to convert UTC times to
|
| 234 |
+
the chosen timezone
|
| 235 |
+
|
| 236 |
+
- ``string``. Instantiating a ``pytz`` instance will be attempted.
|
| 237 |
+
|
| 238 |
+
- ``integer``. Use, for the strategy, the same timezone as the
|
| 239 |
+
corresponding ``data`` in the ``self.datas`` iterable (``0`` would
|
| 240 |
+
use the timezone from ``data0``)
|
| 241 |
+
|
| 242 |
+
- ``cheat_on_open`` (default: ``False``)
|
| 243 |
+
|
| 244 |
+
The ``next_open`` method of strategies will be called. This happens
|
| 245 |
+
before ``next`` and before the broker has had a chance to evaluate
|
| 246 |
+
orders. The indicators have not yet been recalculated. This allows
|
| 247 |
+
issuing an orde which takes into account the indicators of the previous
|
| 248 |
+
day but uses the ``open`` price for stake calculations
|
| 249 |
+
|
| 250 |
+
For cheat_on_open order execution, it is also necessary to make the
|
| 251 |
+
call ``cerebro.broker.set_coo(True)`` or instantite a broker with
|
| 252 |
+
``BackBroker(coo=True)`` (where *coo* stands for cheat-on-open) or set
|
| 253 |
+
the ``broker_coo`` parameter to ``True``. Cerebro will do it
|
| 254 |
+
automatically unless disabled below.
|
| 255 |
+
|
| 256 |
+
- ``broker_coo`` (default: ``True``)
|
| 257 |
+
|
| 258 |
+
This will automatically invoke the ``set_coo`` method of the broker
|
| 259 |
+
with ``True`` to activate ``cheat_on_open`` execution. Will only do it
|
| 260 |
+
if ``cheat_on_open`` is also ``True``
|
| 261 |
+
|
| 262 |
+
- ``quicknotify`` (default: ``False``)
|
| 263 |
+
|
| 264 |
+
Broker notifications are delivered right before the delivery of the
|
| 265 |
+
*next* prices. For backtesting this has no implications, but with live
|
| 266 |
+
brokers a notification can take place long before the bar is
|
| 267 |
+
delivered. When set to ``True`` notifications will be delivered as soon
|
| 268 |
+
as possible (see ``qcheck`` in live feeds)
|
| 269 |
+
|
| 270 |
+
Set to ``False`` for compatibility. May be changed to ``True``
|
| 271 |
+
|
| 272 |
+
'''
|
| 273 |
+
|
| 274 |
+
params = (
|
| 275 |
+
('preload', True),
|
| 276 |
+
('runonce', True),
|
| 277 |
+
('maxcpus', None),
|
| 278 |
+
('stdstats', True),
|
| 279 |
+
('oldbuysell', False),
|
| 280 |
+
('oldtrades', False),
|
| 281 |
+
('lookahead', 0),
|
| 282 |
+
('exactbars', False),
|
| 283 |
+
('optdatas', True),
|
| 284 |
+
('optreturn', True),
|
| 285 |
+
('objcache', False),
|
| 286 |
+
('live', False),
|
| 287 |
+
('writer', False),
|
| 288 |
+
('tradehistory', False),
|
| 289 |
+
('oldsync', False),
|
| 290 |
+
('tz', None),
|
| 291 |
+
('cheat_on_open', False),
|
| 292 |
+
('broker_coo', True),
|
| 293 |
+
('quicknotify', False),
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
def __init__(self):
|
| 297 |
+
self._dolive = False
|
| 298 |
+
self._doreplay = False
|
| 299 |
+
self._dooptimize = False
|
| 300 |
+
self.stores = list()
|
| 301 |
+
self.feeds = list()
|
| 302 |
+
self.datas = list()
|
| 303 |
+
self.datasbyname = collections.OrderedDict()
|
| 304 |
+
self.strats = list()
|
| 305 |
+
self.optcbs = list() # holds a list of callbacks for opt strategies
|
| 306 |
+
self.observers = list()
|
| 307 |
+
self.analyzers = list()
|
| 308 |
+
self.indicators = list()
|
| 309 |
+
self.sizers = dict()
|
| 310 |
+
self.writers = list()
|
| 311 |
+
self.storecbs = list()
|
| 312 |
+
self.datacbs = list()
|
| 313 |
+
self.signals = list()
|
| 314 |
+
self._signal_strat = (None, None, None)
|
| 315 |
+
self._signal_concurrent = False
|
| 316 |
+
self._signal_accumulate = False
|
| 317 |
+
|
| 318 |
+
self._dataid = itertools.count(1)
|
| 319 |
+
|
| 320 |
+
self._broker = BackBroker()
|
| 321 |
+
self._broker.cerebro = self
|
| 322 |
+
|
| 323 |
+
self._tradingcal = None # TradingCalendar()
|
| 324 |
+
|
| 325 |
+
self._pretimers = list()
|
| 326 |
+
self._ohistory = list()
|
| 327 |
+
self._fhistory = None
|
| 328 |
+
|
| 329 |
+
@staticmethod
|
| 330 |
+
def iterize(iterable):
|
| 331 |
+
'''Handy function which turns things into things that can be iterated upon
|
| 332 |
+
including iterables
|
| 333 |
+
'''
|
| 334 |
+
niterable = list()
|
| 335 |
+
for elem in iterable:
|
| 336 |
+
if isinstance(elem, string_types):
|
| 337 |
+
elem = (elem,)
|
| 338 |
+
elif not isinstance(elem, collectionsAbc.Iterable): # Different functions will be called for different Python versions
|
| 339 |
+
elem = (elem,)
|
| 340 |
+
|
| 341 |
+
niterable.append(elem)
|
| 342 |
+
|
| 343 |
+
return niterable
|
| 344 |
+
|
| 345 |
+
def set_fund_history(self, fund):
|
| 346 |
+
'''
|
| 347 |
+
Add a history of orders to be directly executed in the broker for
|
| 348 |
+
performance evaluation
|
| 349 |
+
|
| 350 |
+
- ``fund``: is an iterable (ex: list, tuple, iterator, generator)
|
| 351 |
+
in which each element will be also an iterable (with length) with
|
| 352 |
+
the following sub-elements (2 formats are possible)
|
| 353 |
+
|
| 354 |
+
``[datetime, share_value, net asset value]``
|
| 355 |
+
|
| 356 |
+
**Note**: it must be sorted (or produce sorted elements) by
|
| 357 |
+
datetime ascending
|
| 358 |
+
|
| 359 |
+
where:
|
| 360 |
+
|
| 361 |
+
- ``datetime`` is a python ``date/datetime`` instance or a string
|
| 362 |
+
with format YYYY-MM-DD[THH:MM:SS[.us]] where the elements in
|
| 363 |
+
brackets are optional
|
| 364 |
+
- ``share_value`` is an float/integer
|
| 365 |
+
- ``net_asset_value`` is a float/integer
|
| 366 |
+
'''
|
| 367 |
+
self._fhistory = fund
|
| 368 |
+
|
| 369 |
+
def add_order_history(self, orders, notify=True):
|
| 370 |
+
'''
|
| 371 |
+
Add a history of orders to be directly executed in the broker for
|
| 372 |
+
performance evaluation
|
| 373 |
+
|
| 374 |
+
- ``orders``: is an iterable (ex: list, tuple, iterator, generator)
|
| 375 |
+
in which each element will be also an iterable (with length) with
|
| 376 |
+
the following sub-elements (2 formats are possible)
|
| 377 |
+
|
| 378 |
+
``[datetime, size, price]`` or ``[datetime, size, price, data]``
|
| 379 |
+
|
| 380 |
+
**Note**: it must be sorted (or produce sorted elements) by
|
| 381 |
+
datetime ascending
|
| 382 |
+
|
| 383 |
+
where:
|
| 384 |
+
|
| 385 |
+
- ``datetime`` is a python ``date/datetime`` instance or a string
|
| 386 |
+
with format YYYY-MM-DD[THH:MM:SS[.us]] where the elements in
|
| 387 |
+
brackets are optional
|
| 388 |
+
- ``size`` is an integer (positive to *buy*, negative to *sell*)
|
| 389 |
+
- ``price`` is a float/integer
|
| 390 |
+
- ``data`` if present can take any of the following values
|
| 391 |
+
|
| 392 |
+
- *None* - The 1st data feed will be used as target
|
| 393 |
+
- *integer* - The data with that index (insertion order in
|
| 394 |
+
**Cerebro**) will be used
|
| 395 |
+
- *string* - a data with that name, assigned for example with
|
| 396 |
+
``cerebro.addata(data, name=value)``, will be the target
|
| 397 |
+
|
| 398 |
+
- ``notify`` (default: *True*)
|
| 399 |
+
|
| 400 |
+
If ``True`` the 1st strategy inserted in the system will be
|
| 401 |
+
notified of the artificial orders created following the information
|
| 402 |
+
from each order in ``orders``
|
| 403 |
+
|
| 404 |
+
**Note**: Implicit in the description is the need to add a data feed
|
| 405 |
+
which is the target of the orders. This is for example needed by
|
| 406 |
+
analyzers which track for example the returns
|
| 407 |
+
'''
|
| 408 |
+
self._ohistory.append((orders, notify))
|
| 409 |
+
|
| 410 |
+
def notify_timer(self, timer, when, *args, **kwargs):
|
| 411 |
+
'''Receives a timer notification where ``timer`` is the timer which was
|
| 412 |
+
returned by ``add_timer``, and ``when`` is the calling time. ``args``
|
| 413 |
+
and ``kwargs`` are any additional arguments passed to ``add_timer``
|
| 414 |
+
|
| 415 |
+
The actual ``when`` time can be later, but the system may have not be
|
| 416 |
+
able to call the timer before. This value is the timer value and no the
|
| 417 |
+
system time.
|
| 418 |
+
'''
|
| 419 |
+
pass
|
| 420 |
+
|
| 421 |
+
def _add_timer(self, owner, when,
|
| 422 |
+
offset=datetime.timedelta(), repeat=datetime.timedelta(),
|
| 423 |
+
weekdays=[], weekcarry=False,
|
| 424 |
+
monthdays=[], monthcarry=True,
|
| 425 |
+
allow=None,
|
| 426 |
+
tzdata=None, strats=False, cheat=False,
|
| 427 |
+
*args, **kwargs):
|
| 428 |
+
'''Internal method to really create the timer (not started yet) which
|
| 429 |
+
can be called by cerebro instances or other objects which can access
|
| 430 |
+
cerebro'''
|
| 431 |
+
|
| 432 |
+
timer = Timer(
|
| 433 |
+
tid=len(self._pretimers),
|
| 434 |
+
owner=owner, strats=strats,
|
| 435 |
+
when=when, offset=offset, repeat=repeat,
|
| 436 |
+
weekdays=weekdays, weekcarry=weekcarry,
|
| 437 |
+
monthdays=monthdays, monthcarry=monthcarry,
|
| 438 |
+
allow=allow,
|
| 439 |
+
tzdata=tzdata, cheat=cheat,
|
| 440 |
+
*args, **kwargs
|
| 441 |
+
)
|
| 442 |
+
|
| 443 |
+
self._pretimers.append(timer)
|
| 444 |
+
return timer
|
| 445 |
+
|
| 446 |
+
def add_timer(self, when,
|
| 447 |
+
offset=datetime.timedelta(), repeat=datetime.timedelta(),
|
| 448 |
+
weekdays=[], weekcarry=False,
|
| 449 |
+
monthdays=[], monthcarry=True,
|
| 450 |
+
allow=None,
|
| 451 |
+
tzdata=None, strats=False, cheat=False,
|
| 452 |
+
*args, **kwargs):
|
| 453 |
+
'''
|
| 454 |
+
Schedules a timer to invoke ``notify_timer``
|
| 455 |
+
|
| 456 |
+
Arguments:
|
| 457 |
+
|
| 458 |
+
- ``when``: can be
|
| 459 |
+
|
| 460 |
+
- ``datetime.time`` instance (see below ``tzdata``)
|
| 461 |
+
- ``bt.timer.SESSION_START`` to reference a session start
|
| 462 |
+
- ``bt.timer.SESSION_END`` to reference a session end
|
| 463 |
+
|
| 464 |
+
- ``offset`` which must be a ``datetime.timedelta`` instance
|
| 465 |
+
|
| 466 |
+
Used to offset the value ``when``. It has a meaningful use in
|
| 467 |
+
combination with ``SESSION_START`` and ``SESSION_END``, to indicated
|
| 468 |
+
things like a timer being called ``15 minutes`` after the session
|
| 469 |
+
start.
|
| 470 |
+
|
| 471 |
+
- ``repeat`` which must be a ``datetime.timedelta`` instance
|
| 472 |
+
|
| 473 |
+
Indicates if after a 1st call, further calls will be scheduled
|
| 474 |
+
within the same session at the scheduled ``repeat`` delta
|
| 475 |
+
|
| 476 |
+
Once the timer goes over the end of the session it is reset to the
|
| 477 |
+
original value for ``when``
|
| 478 |
+
|
| 479 |
+
- ``weekdays``: a **sorted** iterable with integers indicating on
|
| 480 |
+
which days (iso codes, Monday is 1, Sunday is 7) the timers can
|
| 481 |
+
be actually invoked
|
| 482 |
+
|
| 483 |
+
If not specified, the timer will be active on all days
|
| 484 |
+
|
| 485 |
+
- ``weekcarry`` (default: ``False``). If ``True`` and the weekday was
|
| 486 |
+
not seen (ex: trading holiday), the timer will be executed on the
|
| 487 |
+
next day (even if in a new week)
|
| 488 |
+
|
| 489 |
+
- ``monthdays``: a **sorted** iterable with integers indicating on
|
| 490 |
+
which days of the month a timer has to be executed. For example
|
| 491 |
+
always on day *15* of the month
|
| 492 |
+
|
| 493 |
+
If not specified, the timer will be active on all days
|
| 494 |
+
|
| 495 |
+
- ``monthcarry`` (default: ``True``). If the day was not seen
|
| 496 |
+
(weekend, trading holiday), the timer will be executed on the next
|
| 497 |
+
available day.
|
| 498 |
+
|
| 499 |
+
- ``allow`` (default: ``None``). A callback which receives a
|
| 500 |
+
`datetime.date`` instance and returns ``True`` if the date is
|
| 501 |
+
allowed for timers or else returns ``False``
|
| 502 |
+
|
| 503 |
+
- ``tzdata`` which can be either ``None`` (default), a ``pytz``
|
| 504 |
+
instance or a ``data feed`` instance.
|
| 505 |
+
|
| 506 |
+
``None``: ``when`` is interpreted at face value (which translates
|
| 507 |
+
to handling it as if it where UTC even if it's not)
|
| 508 |
+
|
| 509 |
+
``pytz`` instance: ``when`` will be interpreted as being specified
|
| 510 |
+
in the local time specified by the timezone instance.
|
| 511 |
+
|
| 512 |
+
``data feed`` instance: ``when`` will be interpreted as being
|
| 513 |
+
specified in the local time specified by the ``tz`` parameter of
|
| 514 |
+
the data feed instance.
|
| 515 |
+
|
| 516 |
+
**Note**: If ``when`` is either ``SESSION_START`` or
|
| 517 |
+
``SESSION_END`` and ``tzdata`` is ``None``, the 1st *data feed*
|
| 518 |
+
in the system (aka ``self.data0``) will be used as the reference
|
| 519 |
+
to find out the session times.
|
| 520 |
+
|
| 521 |
+
- ``strats`` (default: ``False``) call also the ``notify_timer`` of
|
| 522 |
+
strategies
|
| 523 |
+
|
| 524 |
+
- ``cheat`` (default ``False``) if ``True`` the timer will be called
|
| 525 |
+
before the broker has a chance to evaluate the orders. This opens
|
| 526 |
+
the chance to issue orders based on opening price for example right
|
| 527 |
+
before the session starts
|
| 528 |
+
- ``*args``: any extra args will be passed to ``notify_timer``
|
| 529 |
+
|
| 530 |
+
- ``**kwargs``: any extra kwargs will be passed to ``notify_timer``
|
| 531 |
+
|
| 532 |
+
Return Value:
|
| 533 |
+
|
| 534 |
+
- The created timer
|
| 535 |
+
|
| 536 |
+
'''
|
| 537 |
+
return self._add_timer(
|
| 538 |
+
owner=self, when=when, offset=offset, repeat=repeat,
|
| 539 |
+
weekdays=weekdays, weekcarry=weekcarry,
|
| 540 |
+
monthdays=monthdays, monthcarry=monthcarry,
|
| 541 |
+
allow=allow,
|
| 542 |
+
tzdata=tzdata, strats=strats, cheat=cheat,
|
| 543 |
+
*args, **kwargs)
|
| 544 |
+
|
| 545 |
+
def addtz(self, tz):
|
| 546 |
+
'''
|
| 547 |
+
This can also be done with the parameter ``tz``
|
| 548 |
+
|
| 549 |
+
Adds a global timezone for strategies. The argument ``tz`` can be
|
| 550 |
+
|
| 551 |
+
- ``None``: in this case the datetime displayed by strategies will be
|
| 552 |
+
in UTC, which has been always the standard behavior
|
| 553 |
+
|
| 554 |
+
- ``pytz`` instance. It will be used as such to convert UTC times to
|
| 555 |
+
the chosen timezone
|
| 556 |
+
|
| 557 |
+
- ``string``. Instantiating a ``pytz`` instance will be attempted.
|
| 558 |
+
|
| 559 |
+
- ``integer``. Use, for the strategy, the same timezone as the
|
| 560 |
+
corresponding ``data`` in the ``self.datas`` iterable (``0`` would
|
| 561 |
+
use the timezone from ``data0``)
|
| 562 |
+
|
| 563 |
+
'''
|
| 564 |
+
self.p.tz = tz
|
| 565 |
+
|
| 566 |
+
def addcalendar(self, cal):
|
| 567 |
+
'''Adds a global trading calendar to the system. Individual data feeds
|
| 568 |
+
may have separate calendars which override the global one
|
| 569 |
+
|
| 570 |
+
``cal`` can be an instance of ``TradingCalendar`` a string or an
|
| 571 |
+
instance of ``pandas_market_calendars``. A string will be will be
|
| 572 |
+
instantiated as a ``PandasMarketCalendar`` (which needs the module
|
| 573 |
+
``pandas_market_calendar`` installed in the system.
|
| 574 |
+
|
| 575 |
+
If a subclass of `TradingCalendarBase` is passed (not an instance) it
|
| 576 |
+
will be instantiated
|
| 577 |
+
'''
|
| 578 |
+
if isinstance(cal, string_types):
|
| 579 |
+
cal = PandasMarketCalendar(calendar=cal)
|
| 580 |
+
elif hasattr(cal, 'valid_days'):
|
| 581 |
+
cal = PandasMarketCalendar(calendar=cal)
|
| 582 |
+
|
| 583 |
+
else:
|
| 584 |
+
try:
|
| 585 |
+
if issubclass(cal, TradingCalendarBase):
|
| 586 |
+
cal = cal()
|
| 587 |
+
except TypeError: # already an instance
|
| 588 |
+
pass
|
| 589 |
+
|
| 590 |
+
self._tradingcal = cal
|
| 591 |
+
|
| 592 |
+
def add_signal(self, sigtype, sigcls, *sigargs, **sigkwargs):
|
| 593 |
+
'''Adds a signal to the system which will be later added to a
|
| 594 |
+
``SignalStrategy``'''
|
| 595 |
+
self.signals.append((sigtype, sigcls, sigargs, sigkwargs))
|
| 596 |
+
|
| 597 |
+
def signal_strategy(self, stratcls, *args, **kwargs):
|
| 598 |
+
'''Adds a SignalStrategy subclass which can accept signals'''
|
| 599 |
+
self._signal_strat = (stratcls, args, kwargs)
|
| 600 |
+
|
| 601 |
+
def signal_concurrent(self, onoff):
|
| 602 |
+
'''If signals are added to the system and the ``concurrent`` value is
|
| 603 |
+
set to True, concurrent orders will be allowed'''
|
| 604 |
+
self._signal_concurrent = onoff
|
| 605 |
+
|
| 606 |
+
def signal_accumulate(self, onoff):
|
| 607 |
+
'''If signals are added to the system and the ``accumulate`` value is
|
| 608 |
+
set to True, entering the market when already in the market, will be
|
| 609 |
+
allowed to increase a position'''
|
| 610 |
+
self._signal_accumulate = onoff
|
| 611 |
+
|
| 612 |
+
def addstore(self, store):
|
| 613 |
+
'''Adds an ``Store`` instance to the if not already present'''
|
| 614 |
+
if store not in self.stores:
|
| 615 |
+
self.stores.append(store)
|
| 616 |
+
|
| 617 |
+
def addwriter(self, wrtcls, *args, **kwargs):
|
| 618 |
+
'''Adds an ``Writer`` class to the mix. Instantiation will be done at
|
| 619 |
+
``run`` time in cerebro
|
| 620 |
+
'''
|
| 621 |
+
self.writers.append((wrtcls, args, kwargs))
|
| 622 |
+
|
| 623 |
+
def addsizer(self, sizercls, *args, **kwargs):
|
| 624 |
+
'''Adds a ``Sizer`` class (and args) which is the default sizer for any
|
| 625 |
+
strategy added to cerebro
|
| 626 |
+
'''
|
| 627 |
+
self.sizers[None] = (sizercls, args, kwargs)
|
| 628 |
+
|
| 629 |
+
def addsizer_byidx(self, idx, sizercls, *args, **kwargs):
|
| 630 |
+
'''Adds a ``Sizer`` class by idx. This idx is a reference compatible to
|
| 631 |
+
the one returned by ``addstrategy``. Only the strategy referenced by
|
| 632 |
+
``idx`` will receive this size
|
| 633 |
+
'''
|
| 634 |
+
self.sizers[idx] = (sizercls, args, kwargs)
|
| 635 |
+
|
| 636 |
+
def addindicator(self, indcls, *args, **kwargs):
|
| 637 |
+
'''
|
| 638 |
+
Adds an ``Indicator`` class to the mix. Instantiation will be done at
|
| 639 |
+
``run`` time in the passed strategies
|
| 640 |
+
'''
|
| 641 |
+
self.indicators.append((indcls, args, kwargs))
|
| 642 |
+
|
| 643 |
+
def addanalyzer(self, ancls, *args, **kwargs):
|
| 644 |
+
'''
|
| 645 |
+
Adds an ``Analyzer`` class to the mix. Instantiation will be done at
|
| 646 |
+
``run`` time
|
| 647 |
+
'''
|
| 648 |
+
self.analyzers.append((ancls, args, kwargs))
|
| 649 |
+
|
| 650 |
+
def addobserver(self, obscls, *args, **kwargs):
|
| 651 |
+
'''
|
| 652 |
+
Adds an ``Observer`` class to the mix. Instantiation will be done at
|
| 653 |
+
``run`` time
|
| 654 |
+
'''
|
| 655 |
+
self.observers.append((False, obscls, args, kwargs))
|
| 656 |
+
|
| 657 |
+
def addobservermulti(self, obscls, *args, **kwargs):
|
| 658 |
+
'''
|
| 659 |
+
Adds an ``Observer`` class to the mix. Instantiation will be done at
|
| 660 |
+
``run`` time
|
| 661 |
+
|
| 662 |
+
It will be added once per "data" in the system. A use case is a
|
| 663 |
+
buy/sell observer which observes individual datas.
|
| 664 |
+
|
| 665 |
+
A counter-example is the CashValue, which observes system-wide values
|
| 666 |
+
'''
|
| 667 |
+
self.observers.append((True, obscls, args, kwargs))
|
| 668 |
+
|
| 669 |
+
def addstorecb(self, callback):
|
| 670 |
+
'''Adds a callback to get messages which would be handled by the
|
| 671 |
+
notify_store method
|
| 672 |
+
|
| 673 |
+
The signature of the callback must support the following:
|
| 674 |
+
|
| 675 |
+
- callback(msg, \*args, \*\*kwargs)
|
| 676 |
+
|
| 677 |
+
The actual ``msg``, ``*args`` and ``**kwargs`` received are
|
| 678 |
+
implementation defined (depend entirely on the *data/broker/store*) but
|
| 679 |
+
in general one should expect them to be *printable* to allow for
|
| 680 |
+
reception and experimentation.
|
| 681 |
+
'''
|
| 682 |
+
self.storecbs.append(callback)
|
| 683 |
+
|
| 684 |
+
def _notify_store(self, msg, *args, **kwargs):
|
| 685 |
+
for callback in self.storecbs:
|
| 686 |
+
callback(msg, *args, **kwargs)
|
| 687 |
+
|
| 688 |
+
self.notify_store(msg, *args, **kwargs)
|
| 689 |
+
|
| 690 |
+
def notify_store(self, msg, *args, **kwargs):
|
| 691 |
+
'''Receive store notifications in cerebro
|
| 692 |
+
|
| 693 |
+
This method can be overridden in ``Cerebro`` subclasses
|
| 694 |
+
|
| 695 |
+
The actual ``msg``, ``*args`` and ``**kwargs`` received are
|
| 696 |
+
implementation defined (depend entirely on the *data/broker/store*) but
|
| 697 |
+
in general one should expect them to be *printable* to allow for
|
| 698 |
+
reception and experimentation.
|
| 699 |
+
'''
|
| 700 |
+
pass
|
| 701 |
+
|
| 702 |
+
def _storenotify(self):
|
| 703 |
+
for store in self.stores:
|
| 704 |
+
for notif in store.get_notifications():
|
| 705 |
+
msg, args, kwargs = notif
|
| 706 |
+
|
| 707 |
+
self._notify_store(msg, *args, **kwargs)
|
| 708 |
+
for strat in self.runningstrats:
|
| 709 |
+
strat.notify_store(msg, *args, **kwargs)
|
| 710 |
+
|
| 711 |
+
def adddatacb(self, callback):
|
| 712 |
+
'''Adds a callback to get messages which would be handled by the
|
| 713 |
+
notify_data method
|
| 714 |
+
|
| 715 |
+
The signature of the callback must support the following:
|
| 716 |
+
|
| 717 |
+
- callback(data, status, \*args, \*\*kwargs)
|
| 718 |
+
|
| 719 |
+
The actual ``*args`` and ``**kwargs`` received are implementation
|
| 720 |
+
defined (depend entirely on the *data/broker/store*) but in general one
|
| 721 |
+
should expect them to be *printable* to allow for reception and
|
| 722 |
+
experimentation.
|
| 723 |
+
'''
|
| 724 |
+
self.datacbs.append(callback)
|
| 725 |
+
|
| 726 |
+
def _datanotify(self):
|
| 727 |
+
for data in self.datas:
|
| 728 |
+
for notif in data.get_notifications():
|
| 729 |
+
status, args, kwargs = notif
|
| 730 |
+
self._notify_data(data, status, *args, **kwargs)
|
| 731 |
+
for strat in self.runningstrats:
|
| 732 |
+
strat.notify_data(data, status, *args, **kwargs)
|
| 733 |
+
|
| 734 |
+
def _notify_data(self, data, status, *args, **kwargs):
|
| 735 |
+
for callback in self.datacbs:
|
| 736 |
+
callback(data, status, *args, **kwargs)
|
| 737 |
+
|
| 738 |
+
self.notify_data(data, status, *args, **kwargs)
|
| 739 |
+
|
| 740 |
+
def notify_data(self, data, status, *args, **kwargs):
|
| 741 |
+
'''Receive data notifications in cerebro
|
| 742 |
+
|
| 743 |
+
This method can be overridden in ``Cerebro`` subclasses
|
| 744 |
+
|
| 745 |
+
The actual ``*args`` and ``**kwargs`` received are
|
| 746 |
+
implementation defined (depend entirely on the *data/broker/store*) but
|
| 747 |
+
in general one should expect them to be *printable* to allow for
|
| 748 |
+
reception and experimentation.
|
| 749 |
+
'''
|
| 750 |
+
pass
|
| 751 |
+
|
| 752 |
+
def adddata(self, data, name=None):
|
| 753 |
+
'''
|
| 754 |
+
Adds a ``Data Feed`` instance to the mix.
|
| 755 |
+
|
| 756 |
+
If ``name`` is not None it will be put into ``data._name`` which is
|
| 757 |
+
meant for decoration/plotting purposes.
|
| 758 |
+
'''
|
| 759 |
+
if name is not None:
|
| 760 |
+
data._name = name
|
| 761 |
+
|
| 762 |
+
data._id = next(self._dataid)
|
| 763 |
+
data.setenvironment(self)
|
| 764 |
+
|
| 765 |
+
self.datas.append(data)
|
| 766 |
+
self.datasbyname[data._name] = data
|
| 767 |
+
feed = data.getfeed()
|
| 768 |
+
if feed and feed not in self.feeds:
|
| 769 |
+
self.feeds.append(feed)
|
| 770 |
+
|
| 771 |
+
if data.islive():
|
| 772 |
+
self._dolive = True
|
| 773 |
+
|
| 774 |
+
return data
|
| 775 |
+
|
| 776 |
+
def chaindata(self, *args, **kwargs):
|
| 777 |
+
'''
|
| 778 |
+
Chains several data feeds into one
|
| 779 |
+
|
| 780 |
+
If ``name`` is passed as named argument and is not None it will be put
|
| 781 |
+
into ``data._name`` which is meant for decoration/plotting purposes.
|
| 782 |
+
|
| 783 |
+
If ``None``, then the name of the 1st data will be used
|
| 784 |
+
'''
|
| 785 |
+
dname = kwargs.pop('name', None)
|
| 786 |
+
if dname is None:
|
| 787 |
+
dname = args[0]._dataname
|
| 788 |
+
d = bt.feeds.Chainer(dataname=dname, *args)
|
| 789 |
+
self.adddata(d, name=dname)
|
| 790 |
+
|
| 791 |
+
return d
|
| 792 |
+
|
| 793 |
+
def rolloverdata(self, *args, **kwargs):
|
| 794 |
+
'''Chains several data feeds into one
|
| 795 |
+
|
| 796 |
+
If ``name`` is passed as named argument and is not None it will be put
|
| 797 |
+
into ``data._name`` which is meant for decoration/plotting purposes.
|
| 798 |
+
|
| 799 |
+
If ``None``, then the name of the 1st data will be used
|
| 800 |
+
|
| 801 |
+
Any other kwargs will be passed to the RollOver class
|
| 802 |
+
|
| 803 |
+
'''
|
| 804 |
+
dname = kwargs.pop('name', None)
|
| 805 |
+
if dname is None:
|
| 806 |
+
dname = args[0]._dataname
|
| 807 |
+
d = bt.feeds.RollOver(dataname=dname, *args, **kwargs)
|
| 808 |
+
self.adddata(d, name=dname)
|
| 809 |
+
|
| 810 |
+
return d
|
| 811 |
+
|
| 812 |
+
def replaydata(self, dataname, name=None, **kwargs):
|
| 813 |
+
'''
|
| 814 |
+
Adds a ``Data Feed`` to be replayed by the system
|
| 815 |
+
|
| 816 |
+
If ``name`` is not None it will be put into ``data._name`` which is
|
| 817 |
+
meant for decoration/plotting purposes.
|
| 818 |
+
|
| 819 |
+
Any other kwargs like ``timeframe``, ``compression``, ``todate`` which
|
| 820 |
+
are supported by the replay filter will be passed transparently
|
| 821 |
+
'''
|
| 822 |
+
if any(dataname is x for x in self.datas):
|
| 823 |
+
dataname = dataname.clone()
|
| 824 |
+
|
| 825 |
+
dataname.replay(**kwargs)
|
| 826 |
+
self.adddata(dataname, name=name)
|
| 827 |
+
self._doreplay = True
|
| 828 |
+
|
| 829 |
+
return dataname
|
| 830 |
+
|
| 831 |
+
def resampledata(self, dataname, name=None, **kwargs):
|
| 832 |
+
'''
|
| 833 |
+
Adds a ``Data Feed`` to be resample by the system
|
| 834 |
+
|
| 835 |
+
If ``name`` is not None it will be put into ``data._name`` which is
|
| 836 |
+
meant for decoration/plotting purposes.
|
| 837 |
+
|
| 838 |
+
Any other kwargs like ``timeframe``, ``compression``, ``todate`` which
|
| 839 |
+
are supported by the resample filter will be passed transparently
|
| 840 |
+
'''
|
| 841 |
+
if any(dataname is x for x in self.datas):
|
| 842 |
+
dataname = dataname.clone()
|
| 843 |
+
|
| 844 |
+
dataname.resample(**kwargs)
|
| 845 |
+
self.adddata(dataname, name=name)
|
| 846 |
+
self._doreplay = True
|
| 847 |
+
|
| 848 |
+
return dataname
|
| 849 |
+
|
| 850 |
+
def optcallback(self, cb):
|
| 851 |
+
'''
|
| 852 |
+
Adds a *callback* to the list of callbacks that will be called with the
|
| 853 |
+
optimizations when each of the strategies has been run
|
| 854 |
+
|
| 855 |
+
The signature: cb(strategy)
|
| 856 |
+
'''
|
| 857 |
+
self.optcbs.append(cb)
|
| 858 |
+
|
| 859 |
+
def optstrategy(self, strategy, *args, **kwargs):
|
| 860 |
+
'''
|
| 861 |
+
Adds a ``Strategy`` class to the mix for optimization. Instantiation
|
| 862 |
+
will happen during ``run`` time.
|
| 863 |
+
|
| 864 |
+
args and kwargs MUST BE iterables which hold the values to check.
|
| 865 |
+
|
| 866 |
+
Example: if a Strategy accepts a parameter ``period``, for optimization
|
| 867 |
+
purposes the call to ``optstrategy`` looks like:
|
| 868 |
+
|
| 869 |
+
- cerebro.optstrategy(MyStrategy, period=(15, 25))
|
| 870 |
+
|
| 871 |
+
This will execute an optimization for values 15 and 25. Whereas
|
| 872 |
+
|
| 873 |
+
- cerebro.optstrategy(MyStrategy, period=range(15, 25))
|
| 874 |
+
|
| 875 |
+
will execute MyStrategy with ``period`` values 15 -> 25 (25 not
|
| 876 |
+
included, because ranges are semi-open in Python)
|
| 877 |
+
|
| 878 |
+
If a parameter is passed but shall not be optimized the call looks
|
| 879 |
+
like:
|
| 880 |
+
|
| 881 |
+
- cerebro.optstrategy(MyStrategy, period=(15,))
|
| 882 |
+
|
| 883 |
+
Notice that ``period`` is still passed as an iterable ... of just 1
|
| 884 |
+
element
|
| 885 |
+
|
| 886 |
+
``backtrader`` will anyhow try to identify situations like:
|
| 887 |
+
|
| 888 |
+
- cerebro.optstrategy(MyStrategy, period=15)
|
| 889 |
+
|
| 890 |
+
and will create an internal pseudo-iterable if possible
|
| 891 |
+
'''
|
| 892 |
+
self._dooptimize = True
|
| 893 |
+
args = self.iterize(args)
|
| 894 |
+
optargs = itertools.product(*args)
|
| 895 |
+
|
| 896 |
+
optkeys = list(kwargs)
|
| 897 |
+
|
| 898 |
+
vals = self.iterize(kwargs.values())
|
| 899 |
+
optvals = itertools.product(*vals)
|
| 900 |
+
|
| 901 |
+
okwargs1 = map(zip, itertools.repeat(optkeys), optvals)
|
| 902 |
+
|
| 903 |
+
optkwargs = map(dict, okwargs1)
|
| 904 |
+
|
| 905 |
+
it = itertools.product([strategy], optargs, optkwargs)
|
| 906 |
+
self.strats.append(it)
|
| 907 |
+
|
| 908 |
+
def addstrategy(self, strategy, *args, **kwargs):
|
| 909 |
+
'''
|
| 910 |
+
Adds a ``Strategy`` class to the mix for a single pass run.
|
| 911 |
+
Instantiation will happen during ``run`` time.
|
| 912 |
+
|
| 913 |
+
args and kwargs will be passed to the strategy as they are during
|
| 914 |
+
instantiation.
|
| 915 |
+
|
| 916 |
+
Returns the index with which addition of other objects (like sizers)
|
| 917 |
+
can be referenced
|
| 918 |
+
'''
|
| 919 |
+
self.strats.append([(strategy, args, kwargs)])
|
| 920 |
+
return len(self.strats) - 1
|
| 921 |
+
|
| 922 |
+
def setbroker(self, broker):
|
| 923 |
+
'''
|
| 924 |
+
Sets a specific ``broker`` instance for this strategy, replacing the
|
| 925 |
+
one inherited from cerebro.
|
| 926 |
+
'''
|
| 927 |
+
self._broker = broker
|
| 928 |
+
broker.cerebro = self
|
| 929 |
+
return broker
|
| 930 |
+
|
| 931 |
+
def getbroker(self):
|
| 932 |
+
'''
|
| 933 |
+
Returns the broker instance.
|
| 934 |
+
|
| 935 |
+
This is also available as a ``property`` by the name ``broker``
|
| 936 |
+
'''
|
| 937 |
+
return self._broker
|
| 938 |
+
|
| 939 |
+
broker = property(getbroker, setbroker)
|
| 940 |
+
|
| 941 |
+
def plot(self, plotter=None, numfigs=1, iplot=True, start=None, end=None,
|
| 942 |
+
width=16, height=9, dpi=300, tight=True, use=None,
|
| 943 |
+
**kwargs):
|
| 944 |
+
'''
|
| 945 |
+
Plots the strategies inside cerebro
|
| 946 |
+
|
| 947 |
+
If ``plotter`` is None a default ``Plot`` instance is created and
|
| 948 |
+
``kwargs`` are passed to it during instantiation.
|
| 949 |
+
|
| 950 |
+
``numfigs`` split the plot in the indicated number of charts reducing
|
| 951 |
+
chart density if wished
|
| 952 |
+
|
| 953 |
+
``iplot``: if ``True`` and running in a ``notebook`` the charts will be
|
| 954 |
+
displayed inline
|
| 955 |
+
|
| 956 |
+
``use``: set it to the name of the desired matplotlib backend. It will
|
| 957 |
+
take precedence over ``iplot``
|
| 958 |
+
|
| 959 |
+
``start``: An index to the datetime line array of the strategy or a
|
| 960 |
+
``datetime.date``, ``datetime.datetime`` instance indicating the start
|
| 961 |
+
of the plot
|
| 962 |
+
|
| 963 |
+
``end``: An index to the datetime line array of the strategy or a
|
| 964 |
+
``datetime.date``, ``datetime.datetime`` instance indicating the end
|
| 965 |
+
of the plot
|
| 966 |
+
|
| 967 |
+
``width``: in inches of the saved figure
|
| 968 |
+
|
| 969 |
+
``height``: in inches of the saved figure
|
| 970 |
+
|
| 971 |
+
``dpi``: quality in dots per inches of the saved figure
|
| 972 |
+
|
| 973 |
+
``tight``: only save actual content and not the frame of the figure
|
| 974 |
+
'''
|
| 975 |
+
if self._exactbars > 0:
|
| 976 |
+
return
|
| 977 |
+
|
| 978 |
+
if not plotter:
|
| 979 |
+
from . import plot
|
| 980 |
+
if self.p.oldsync:
|
| 981 |
+
plotter = plot.Plot_OldSync(**kwargs)
|
| 982 |
+
else:
|
| 983 |
+
plotter = plot.Plot(**kwargs)
|
| 984 |
+
|
| 985 |
+
# pfillers = {self.datas[i]: self._plotfillers[i]
|
| 986 |
+
# for i, x in enumerate(self._plotfillers)}
|
| 987 |
+
|
| 988 |
+
# pfillers2 = {self.datas[i]: self._plotfillers2[i]
|
| 989 |
+
# for i, x in enumerate(self._plotfillers2)}
|
| 990 |
+
|
| 991 |
+
figs = []
|
| 992 |
+
for stratlist in self.runstrats:
|
| 993 |
+
for si, strat in enumerate(stratlist):
|
| 994 |
+
rfig = plotter.plot(strat, figid=si * 100,
|
| 995 |
+
numfigs=numfigs, iplot=iplot,
|
| 996 |
+
start=start, end=end, use=use)
|
| 997 |
+
# pfillers=pfillers2)
|
| 998 |
+
|
| 999 |
+
figs.append(rfig)
|
| 1000 |
+
|
| 1001 |
+
plotter.show()
|
| 1002 |
+
|
| 1003 |
+
return figs
|
| 1004 |
+
|
| 1005 |
+
def __call__(self, iterstrat):
|
| 1006 |
+
'''
|
| 1007 |
+
Used during optimization to pass the cerebro over the multiprocesing
|
| 1008 |
+
module without complains
|
| 1009 |
+
'''
|
| 1010 |
+
|
| 1011 |
+
predata = self.p.optdatas and self._dopreload and self._dorunonce
|
| 1012 |
+
return self.runstrategies(iterstrat, predata=predata)
|
| 1013 |
+
|
| 1014 |
+
def __getstate__(self):
|
| 1015 |
+
'''
|
| 1016 |
+
Used during optimization to prevent optimization result `runstrats`
|
| 1017 |
+
from being pickled to subprocesses
|
| 1018 |
+
'''
|
| 1019 |
+
|
| 1020 |
+
rv = vars(self).copy()
|
| 1021 |
+
if 'runstrats' in rv:
|
| 1022 |
+
del(rv['runstrats'])
|
| 1023 |
+
return rv
|
| 1024 |
+
|
| 1025 |
+
def runstop(self):
|
| 1026 |
+
'''If invoked from inside a strategy or anywhere else, including other
|
| 1027 |
+
threads the execution will stop as soon as possible.'''
|
| 1028 |
+
self._event_stop = True # signal a stop has been requested
|
| 1029 |
+
|
| 1030 |
+
def run(self, **kwargs):
|
| 1031 |
+
'''The core method to perform backtesting. Any ``kwargs`` passed to it
|
| 1032 |
+
will affect the value of the standard parameters ``Cerebro`` was
|
| 1033 |
+
instantiated with.
|
| 1034 |
+
|
| 1035 |
+
If ``cerebro`` has not datas the method will immediately bail out.
|
| 1036 |
+
|
| 1037 |
+
It has different return values:
|
| 1038 |
+
|
| 1039 |
+
- For No Optimization: a list contanining instances of the Strategy
|
| 1040 |
+
classes added with ``addstrategy``
|
| 1041 |
+
|
| 1042 |
+
- For Optimization: a list of lists which contain instances of the
|
| 1043 |
+
Strategy classes added with ``addstrategy``
|
| 1044 |
+
'''
|
| 1045 |
+
self._event_stop = False # Stop is requested
|
| 1046 |
+
|
| 1047 |
+
if not self.datas:
|
| 1048 |
+
return [] # nothing can be run
|
| 1049 |
+
|
| 1050 |
+
pkeys = self.params._getkeys()
|
| 1051 |
+
for key, val in kwargs.items():
|
| 1052 |
+
if key in pkeys:
|
| 1053 |
+
setattr(self.params, key, val)
|
| 1054 |
+
|
| 1055 |
+
# Manage activate/deactivate object cache
|
| 1056 |
+
linebuffer.LineActions.cleancache() # clean cache
|
| 1057 |
+
indicator.Indicator.cleancache() # clean cache
|
| 1058 |
+
|
| 1059 |
+
linebuffer.LineActions.usecache(self.p.objcache)
|
| 1060 |
+
indicator.Indicator.usecache(self.p.objcache)
|
| 1061 |
+
|
| 1062 |
+
self._dorunonce = self.p.runonce
|
| 1063 |
+
self._dopreload = self.p.preload
|
| 1064 |
+
self._exactbars = int(self.p.exactbars)
|
| 1065 |
+
|
| 1066 |
+
if self._exactbars:
|
| 1067 |
+
self._dorunonce = False # something is saving memory, no runonce
|
| 1068 |
+
self._dopreload = self._dopreload and self._exactbars < 1
|
| 1069 |
+
|
| 1070 |
+
self._doreplay = self._doreplay or any(x.replaying for x in self.datas)
|
| 1071 |
+
if self._doreplay:
|
| 1072 |
+
# preloading is not supported with replay. full timeframe bars
|
| 1073 |
+
# are constructed in realtime
|
| 1074 |
+
self._dopreload = False
|
| 1075 |
+
|
| 1076 |
+
if self._dolive or self.p.live:
|
| 1077 |
+
# in this case both preload and runonce must be off
|
| 1078 |
+
self._dorunonce = False
|
| 1079 |
+
self._dopreload = False
|
| 1080 |
+
|
| 1081 |
+
self.runwriters = list()
|
| 1082 |
+
|
| 1083 |
+
# Add the system default writer if requested
|
| 1084 |
+
if self.p.writer is True:
|
| 1085 |
+
wr = WriterFile()
|
| 1086 |
+
self.runwriters.append(wr)
|
| 1087 |
+
|
| 1088 |
+
# Instantiate any other writers
|
| 1089 |
+
for wrcls, wrargs, wrkwargs in self.writers:
|
| 1090 |
+
wr = wrcls(*wrargs, **wrkwargs)
|
| 1091 |
+
self.runwriters.append(wr)
|
| 1092 |
+
|
| 1093 |
+
# Write down if any writer wants the full csv output
|
| 1094 |
+
self.writers_csv = any(map(lambda x: x.p.csv, self.runwriters))
|
| 1095 |
+
|
| 1096 |
+
self.runstrats = list()
|
| 1097 |
+
|
| 1098 |
+
if self.signals: # allow processing of signals
|
| 1099 |
+
signalst, sargs, skwargs = self._signal_strat
|
| 1100 |
+
if signalst is None:
|
| 1101 |
+
# Try to see if the 1st regular strategy is a signal strategy
|
| 1102 |
+
try:
|
| 1103 |
+
signalst, sargs, skwargs = self.strats.pop(0)
|
| 1104 |
+
except IndexError:
|
| 1105 |
+
pass # Nothing there
|
| 1106 |
+
else:
|
| 1107 |
+
if not isinstance(signalst, SignalStrategy):
|
| 1108 |
+
# no signal ... reinsert at the beginning
|
| 1109 |
+
self.strats.insert(0, (signalst, sargs, skwargs))
|
| 1110 |
+
signalst = None # flag as not presetn
|
| 1111 |
+
|
| 1112 |
+
if signalst is None: # recheck
|
| 1113 |
+
# Still None, create a default one
|
| 1114 |
+
signalst, sargs, skwargs = SignalStrategy, tuple(), dict()
|
| 1115 |
+
|
| 1116 |
+
# Add the signal strategy
|
| 1117 |
+
self.addstrategy(signalst,
|
| 1118 |
+
_accumulate=self._signal_accumulate,
|
| 1119 |
+
_concurrent=self._signal_concurrent,
|
| 1120 |
+
signals=self.signals,
|
| 1121 |
+
*sargs,
|
| 1122 |
+
**skwargs)
|
| 1123 |
+
|
| 1124 |
+
if not self.strats: # Datas are present, add a strategy
|
| 1125 |
+
self.addstrategy(Strategy)
|
| 1126 |
+
|
| 1127 |
+
iterstrats = itertools.product(*self.strats)
|
| 1128 |
+
if not self._dooptimize or self.p.maxcpus == 1:
|
| 1129 |
+
# If no optimmization is wished ... or 1 core is to be used
|
| 1130 |
+
# let's skip process "spawning"
|
| 1131 |
+
for iterstrat in iterstrats:
|
| 1132 |
+
runstrat = self.runstrategies(iterstrat)
|
| 1133 |
+
self.runstrats.append(runstrat)
|
| 1134 |
+
if self._dooptimize:
|
| 1135 |
+
for cb in self.optcbs:
|
| 1136 |
+
cb(runstrat) # callback receives finished strategy
|
| 1137 |
+
else:
|
| 1138 |
+
if self.p.optdatas and self._dopreload and self._dorunonce:
|
| 1139 |
+
for data in self.datas:
|
| 1140 |
+
data.reset()
|
| 1141 |
+
if self._exactbars < 1: # datas can be full length
|
| 1142 |
+
data.extend(size=self.params.lookahead)
|
| 1143 |
+
data._start()
|
| 1144 |
+
if self._dopreload:
|
| 1145 |
+
data.preload()
|
| 1146 |
+
|
| 1147 |
+
pool = multiprocessing.Pool(self.p.maxcpus or None)
|
| 1148 |
+
for r in pool.imap(self, iterstrats):
|
| 1149 |
+
self.runstrats.append(r)
|
| 1150 |
+
for cb in self.optcbs:
|
| 1151 |
+
cb(r) # callback receives finished strategy
|
| 1152 |
+
|
| 1153 |
+
pool.close()
|
| 1154 |
+
|
| 1155 |
+
if self.p.optdatas and self._dopreload and self._dorunonce:
|
| 1156 |
+
for data in self.datas:
|
| 1157 |
+
data.stop()
|
| 1158 |
+
|
| 1159 |
+
if not self._dooptimize:
|
| 1160 |
+
# avoid a list of list for regular cases
|
| 1161 |
+
return self.runstrats[0]
|
| 1162 |
+
|
| 1163 |
+
return self.runstrats
|
| 1164 |
+
|
| 1165 |
+
def _init_stcount(self):
|
| 1166 |
+
self.stcount = itertools.count(0)
|
| 1167 |
+
|
| 1168 |
+
def _next_stid(self):
|
| 1169 |
+
return next(self.stcount)
|
| 1170 |
+
|
| 1171 |
+
def runstrategies(self, iterstrat, predata=False):
|
| 1172 |
+
'''
|
| 1173 |
+
Internal method invoked by ``run``` to run a set of strategies
|
| 1174 |
+
'''
|
| 1175 |
+
self._init_stcount()
|
| 1176 |
+
|
| 1177 |
+
self.runningstrats = runstrats = list()
|
| 1178 |
+
for store in self.stores:
|
| 1179 |
+
store.start()
|
| 1180 |
+
|
| 1181 |
+
if self.p.cheat_on_open and self.p.broker_coo:
|
| 1182 |
+
# try to activate in broker
|
| 1183 |
+
if hasattr(self._broker, 'set_coo'):
|
| 1184 |
+
self._broker.set_coo(True)
|
| 1185 |
+
|
| 1186 |
+
if self._fhistory is not None:
|
| 1187 |
+
self._broker.set_fund_history(self._fhistory)
|
| 1188 |
+
|
| 1189 |
+
for orders, onotify in self._ohistory:
|
| 1190 |
+
self._broker.add_order_history(orders, onotify)
|
| 1191 |
+
|
| 1192 |
+
self._broker.start()
|
| 1193 |
+
|
| 1194 |
+
for feed in self.feeds:
|
| 1195 |
+
feed.start()
|
| 1196 |
+
|
| 1197 |
+
if self.writers_csv:
|
| 1198 |
+
wheaders = list()
|
| 1199 |
+
for data in self.datas:
|
| 1200 |
+
if data.csv:
|
| 1201 |
+
wheaders.extend(data.getwriterheaders())
|
| 1202 |
+
|
| 1203 |
+
for writer in self.runwriters:
|
| 1204 |
+
if writer.p.csv:
|
| 1205 |
+
writer.addheaders(wheaders)
|
| 1206 |
+
|
| 1207 |
+
# self._plotfillers = [list() for d in self.datas]
|
| 1208 |
+
# self._plotfillers2 = [list() for d in self.datas]
|
| 1209 |
+
|
| 1210 |
+
if not predata:
|
| 1211 |
+
for data in self.datas:
|
| 1212 |
+
data.reset()
|
| 1213 |
+
if self._exactbars < 1: # datas can be full length
|
| 1214 |
+
data.extend(size=self.params.lookahead)
|
| 1215 |
+
data._start()
|
| 1216 |
+
if self._dopreload:
|
| 1217 |
+
data.preload()
|
| 1218 |
+
|
| 1219 |
+
for stratcls, sargs, skwargs in iterstrat:
|
| 1220 |
+
sargs = self.datas + list(sargs)
|
| 1221 |
+
try:
|
| 1222 |
+
strat = stratcls(*sargs, **skwargs)
|
| 1223 |
+
except bt.errors.StrategySkipError:
|
| 1224 |
+
continue # do not add strategy to the mix
|
| 1225 |
+
|
| 1226 |
+
if self.p.oldsync:
|
| 1227 |
+
strat._oldsync = True # tell strategy to use old clock update
|
| 1228 |
+
if self.p.tradehistory:
|
| 1229 |
+
strat.set_tradehistory()
|
| 1230 |
+
runstrats.append(strat)
|
| 1231 |
+
|
| 1232 |
+
tz = self.p.tz
|
| 1233 |
+
if isinstance(tz, integer_types):
|
| 1234 |
+
tz = self.datas[tz]._tz
|
| 1235 |
+
else:
|
| 1236 |
+
tz = tzparse(tz)
|
| 1237 |
+
|
| 1238 |
+
if runstrats:
|
| 1239 |
+
# loop separated for clarity
|
| 1240 |
+
defaultsizer = self.sizers.get(None, (None, None, None))
|
| 1241 |
+
for idx, strat in enumerate(runstrats):
|
| 1242 |
+
if self.p.stdstats:
|
| 1243 |
+
strat._addobserver(False, observers.Broker)
|
| 1244 |
+
if self.p.oldbuysell:
|
| 1245 |
+
strat._addobserver(True, observers.BuySell)
|
| 1246 |
+
else:
|
| 1247 |
+
strat._addobserver(True, observers.BuySell,
|
| 1248 |
+
barplot=True)
|
| 1249 |
+
|
| 1250 |
+
if self.p.oldtrades or len(self.datas) == 1:
|
| 1251 |
+
strat._addobserver(False, observers.Trades)
|
| 1252 |
+
else:
|
| 1253 |
+
strat._addobserver(False, observers.DataTrades)
|
| 1254 |
+
|
| 1255 |
+
for multi, obscls, obsargs, obskwargs in self.observers:
|
| 1256 |
+
strat._addobserver(multi, obscls, *obsargs, **obskwargs)
|
| 1257 |
+
|
| 1258 |
+
for indcls, indargs, indkwargs in self.indicators:
|
| 1259 |
+
strat._addindicator(indcls, *indargs, **indkwargs)
|
| 1260 |
+
|
| 1261 |
+
for ancls, anargs, ankwargs in self.analyzers:
|
| 1262 |
+
strat._addanalyzer(ancls, *anargs, **ankwargs)
|
| 1263 |
+
|
| 1264 |
+
sizer, sargs, skwargs = self.sizers.get(idx, defaultsizer)
|
| 1265 |
+
if sizer is not None:
|
| 1266 |
+
strat._addsizer(sizer, *sargs, **skwargs)
|
| 1267 |
+
|
| 1268 |
+
strat._settz(tz)
|
| 1269 |
+
strat._start()
|
| 1270 |
+
|
| 1271 |
+
for writer in self.runwriters:
|
| 1272 |
+
if writer.p.csv:
|
| 1273 |
+
writer.addheaders(strat.getwriterheaders())
|
| 1274 |
+
|
| 1275 |
+
if not predata:
|
| 1276 |
+
for strat in runstrats:
|
| 1277 |
+
strat.qbuffer(self._exactbars, replaying=self._doreplay)
|
| 1278 |
+
|
| 1279 |
+
for writer in self.runwriters:
|
| 1280 |
+
writer.start()
|
| 1281 |
+
|
| 1282 |
+
# Prepare timers
|
| 1283 |
+
self._timers = []
|
| 1284 |
+
self._timerscheat = []
|
| 1285 |
+
for timer in self._pretimers:
|
| 1286 |
+
# preprocess tzdata if needed
|
| 1287 |
+
timer.start(self.datas[0])
|
| 1288 |
+
|
| 1289 |
+
if timer.params.cheat:
|
| 1290 |
+
self._timerscheat.append(timer)
|
| 1291 |
+
else:
|
| 1292 |
+
self._timers.append(timer)
|
| 1293 |
+
|
| 1294 |
+
if self._dopreload and self._dorunonce:
|
| 1295 |
+
if self.p.oldsync:
|
| 1296 |
+
self._runonce_old(runstrats)
|
| 1297 |
+
else:
|
| 1298 |
+
self._runonce(runstrats)
|
| 1299 |
+
else:
|
| 1300 |
+
if self.p.oldsync:
|
| 1301 |
+
self._runnext_old(runstrats)
|
| 1302 |
+
else:
|
| 1303 |
+
self._runnext(runstrats)
|
| 1304 |
+
|
| 1305 |
+
for strat in runstrats:
|
| 1306 |
+
strat._stop()
|
| 1307 |
+
|
| 1308 |
+
self._broker.stop()
|
| 1309 |
+
|
| 1310 |
+
if not predata:
|
| 1311 |
+
for data in self.datas:
|
| 1312 |
+
data.stop()
|
| 1313 |
+
|
| 1314 |
+
for feed in self.feeds:
|
| 1315 |
+
feed.stop()
|
| 1316 |
+
|
| 1317 |
+
for store in self.stores:
|
| 1318 |
+
store.stop()
|
| 1319 |
+
|
| 1320 |
+
self.stop_writers(runstrats)
|
| 1321 |
+
|
| 1322 |
+
if self._dooptimize and self.p.optreturn:
|
| 1323 |
+
# Results can be optimized
|
| 1324 |
+
results = list()
|
| 1325 |
+
for strat in runstrats:
|
| 1326 |
+
for a in strat.analyzers:
|
| 1327 |
+
a.strategy = None
|
| 1328 |
+
a._parent = None
|
| 1329 |
+
for attrname in dir(a):
|
| 1330 |
+
if attrname.startswith('data'):
|
| 1331 |
+
setattr(a, attrname, None)
|
| 1332 |
+
|
| 1333 |
+
oreturn = OptReturn(strat.params, analyzers=strat.analyzers, strategycls=type(strat))
|
| 1334 |
+
results.append(oreturn)
|
| 1335 |
+
|
| 1336 |
+
return results
|
| 1337 |
+
|
| 1338 |
+
return runstrats
|
| 1339 |
+
|
| 1340 |
+
def stop_writers(self, runstrats):
|
| 1341 |
+
cerebroinfo = OrderedDict()
|
| 1342 |
+
datainfos = OrderedDict()
|
| 1343 |
+
|
| 1344 |
+
for i, data in enumerate(self.datas):
|
| 1345 |
+
datainfos['Data%d' % i] = data.getwriterinfo()
|
| 1346 |
+
|
| 1347 |
+
cerebroinfo['Datas'] = datainfos
|
| 1348 |
+
|
| 1349 |
+
stratinfos = dict()
|
| 1350 |
+
for strat in runstrats:
|
| 1351 |
+
stname = strat.__class__.__name__
|
| 1352 |
+
stratinfos[stname] = strat.getwriterinfo()
|
| 1353 |
+
|
| 1354 |
+
cerebroinfo['Strategies'] = stratinfos
|
| 1355 |
+
|
| 1356 |
+
for writer in self.runwriters:
|
| 1357 |
+
writer.writedict(dict(Cerebro=cerebroinfo))
|
| 1358 |
+
writer.stop()
|
| 1359 |
+
|
| 1360 |
+
def _brokernotify(self):
|
| 1361 |
+
'''
|
| 1362 |
+
Internal method which kicks the broker and delivers any broker
|
| 1363 |
+
notification to the strategy
|
| 1364 |
+
'''
|
| 1365 |
+
self._broker.next()
|
| 1366 |
+
while True:
|
| 1367 |
+
order = self._broker.get_notification()
|
| 1368 |
+
if order is None:
|
| 1369 |
+
break
|
| 1370 |
+
|
| 1371 |
+
owner = order.owner
|
| 1372 |
+
if owner is None:
|
| 1373 |
+
owner = self.runningstrats[0] # default
|
| 1374 |
+
|
| 1375 |
+
owner._addnotification(order, quicknotify=self.p.quicknotify)
|
| 1376 |
+
|
| 1377 |
+
def _runnext_old(self, runstrats):
|
| 1378 |
+
'''
|
| 1379 |
+
Actual implementation of run in full next mode. All objects have its
|
| 1380 |
+
``next`` method invoke on each data arrival
|
| 1381 |
+
'''
|
| 1382 |
+
data0 = self.datas[0]
|
| 1383 |
+
d0ret = True
|
| 1384 |
+
while d0ret or d0ret is None:
|
| 1385 |
+
lastret = False
|
| 1386 |
+
# Notify anything from the store even before moving datas
|
| 1387 |
+
# because datas may not move due to an error reported by the store
|
| 1388 |
+
self._storenotify()
|
| 1389 |
+
if self._event_stop: # stop if requested
|
| 1390 |
+
return
|
| 1391 |
+
self._datanotify()
|
| 1392 |
+
if self._event_stop: # stop if requested
|
| 1393 |
+
return
|
| 1394 |
+
|
| 1395 |
+
d0ret = data0.next()
|
| 1396 |
+
if d0ret:
|
| 1397 |
+
for data in self.datas[1:]:
|
| 1398 |
+
if not data.next(datamaster=data0): # no delivery
|
| 1399 |
+
data._check(forcedata=data0) # check forcing output
|
| 1400 |
+
data.next(datamaster=data0) # retry
|
| 1401 |
+
|
| 1402 |
+
elif d0ret is None:
|
| 1403 |
+
# meant for things like live feeds which may not produce a bar
|
| 1404 |
+
# at the moment but need the loop to run for notifications and
|
| 1405 |
+
# getting resample and others to produce timely bars
|
| 1406 |
+
data0._check()
|
| 1407 |
+
for data in self.datas[1:]:
|
| 1408 |
+
data._check()
|
| 1409 |
+
else:
|
| 1410 |
+
lastret = data0._last()
|
| 1411 |
+
for data in self.datas[1:]:
|
| 1412 |
+
lastret += data._last(datamaster=data0)
|
| 1413 |
+
|
| 1414 |
+
if not lastret:
|
| 1415 |
+
# Only go extra round if something was changed by "lasts"
|
| 1416 |
+
break
|
| 1417 |
+
|
| 1418 |
+
# Datas may have generated a new notification after next
|
| 1419 |
+
self._datanotify()
|
| 1420 |
+
if self._event_stop: # stop if requested
|
| 1421 |
+
return
|
| 1422 |
+
|
| 1423 |
+
self._brokernotify()
|
| 1424 |
+
if self._event_stop: # stop if requested
|
| 1425 |
+
return
|
| 1426 |
+
|
| 1427 |
+
if d0ret or lastret: # bars produced by data or filters
|
| 1428 |
+
for strat in runstrats:
|
| 1429 |
+
strat._next()
|
| 1430 |
+
if self._event_stop: # stop if requested
|
| 1431 |
+
return
|
| 1432 |
+
|
| 1433 |
+
self._next_writers(runstrats)
|
| 1434 |
+
|
| 1435 |
+
# Last notification chance before stopping
|
| 1436 |
+
self._datanotify()
|
| 1437 |
+
if self._event_stop: # stop if requested
|
| 1438 |
+
return
|
| 1439 |
+
self._storenotify()
|
| 1440 |
+
if self._event_stop: # stop if requested
|
| 1441 |
+
return
|
| 1442 |
+
|
| 1443 |
+
def _runonce_old(self, runstrats):
|
| 1444 |
+
'''
|
| 1445 |
+
Actual implementation of run in vector mode.
|
| 1446 |
+
Strategies are still invoked on a pseudo-event mode in which ``next``
|
| 1447 |
+
is called for each data arrival
|
| 1448 |
+
'''
|
| 1449 |
+
for strat in runstrats:
|
| 1450 |
+
strat._once()
|
| 1451 |
+
|
| 1452 |
+
# The default once for strategies does nothing and therefore
|
| 1453 |
+
# has not moved forward all datas/indicators/observers that
|
| 1454 |
+
# were homed before calling once, Hence no "need" to do it
|
| 1455 |
+
# here again, because pointers are at 0
|
| 1456 |
+
data0 = self.datas[0]
|
| 1457 |
+
datas = self.datas[1:]
|
| 1458 |
+
for i in range(data0.buflen()):
|
| 1459 |
+
data0.advance()
|
| 1460 |
+
for data in datas:
|
| 1461 |
+
data.advance(datamaster=data0)
|
| 1462 |
+
|
| 1463 |
+
self._brokernotify()
|
| 1464 |
+
if self._event_stop: # stop if requested
|
| 1465 |
+
return
|
| 1466 |
+
|
| 1467 |
+
for strat in runstrats:
|
| 1468 |
+
# data0.datetime[0] for compat. w/ new strategy's oncepost
|
| 1469 |
+
strat._oncepost(data0.datetime[0])
|
| 1470 |
+
if self._event_stop: # stop if requested
|
| 1471 |
+
return
|
| 1472 |
+
|
| 1473 |
+
self._next_writers(runstrats)
|
| 1474 |
+
|
| 1475 |
+
def _next_writers(self, runstrats):
|
| 1476 |
+
if not self.runwriters:
|
| 1477 |
+
return
|
| 1478 |
+
|
| 1479 |
+
if self.writers_csv:
|
| 1480 |
+
wvalues = list()
|
| 1481 |
+
for data in self.datas:
|
| 1482 |
+
if data.csv:
|
| 1483 |
+
wvalues.extend(data.getwritervalues())
|
| 1484 |
+
|
| 1485 |
+
for strat in runstrats:
|
| 1486 |
+
wvalues.extend(strat.getwritervalues())
|
| 1487 |
+
|
| 1488 |
+
for writer in self.runwriters:
|
| 1489 |
+
if writer.p.csv:
|
| 1490 |
+
writer.addvalues(wvalues)
|
| 1491 |
+
|
| 1492 |
+
writer.next()
|
| 1493 |
+
|
| 1494 |
+
def _disable_runonce(self):
|
| 1495 |
+
'''API for lineiterators to disable runonce (see HeikinAshi)'''
|
| 1496 |
+
self._dorunonce = False
|
| 1497 |
+
|
| 1498 |
+
def _runnext(self, runstrats):
|
| 1499 |
+
'''
|
| 1500 |
+
Actual implementation of run in full next mode. All objects have its
|
| 1501 |
+
``next`` method invoke on each data arrival
|
| 1502 |
+
'''
|
| 1503 |
+
datas = sorted(self.datas,
|
| 1504 |
+
key=lambda x: (x._timeframe, x._compression))
|
| 1505 |
+
datas1 = datas[1:]
|
| 1506 |
+
data0 = datas[0]
|
| 1507 |
+
d0ret = True
|
| 1508 |
+
|
| 1509 |
+
rs = [i for i, x in enumerate(datas) if x.resampling]
|
| 1510 |
+
rp = [i for i, x in enumerate(datas) if x.replaying]
|
| 1511 |
+
rsonly = [i for i, x in enumerate(datas)
|
| 1512 |
+
if x.resampling and not x.replaying]
|
| 1513 |
+
onlyresample = len(datas) == len(rsonly)
|
| 1514 |
+
noresample = not rsonly
|
| 1515 |
+
|
| 1516 |
+
clonecount = sum(d._clone for d in datas)
|
| 1517 |
+
ldatas = len(datas)
|
| 1518 |
+
ldatas_noclones = ldatas - clonecount
|
| 1519 |
+
lastqcheck = False
|
| 1520 |
+
dt0 = date2num(datetime.datetime.max) - 2 # default at max
|
| 1521 |
+
while d0ret or d0ret is None:
|
| 1522 |
+
# if any has live data in the buffer, no data will wait anything
|
| 1523 |
+
newqcheck = not any(d.haslivedata() for d in datas)
|
| 1524 |
+
if not newqcheck:
|
| 1525 |
+
# If no data has reached the live status or all, wait for
|
| 1526 |
+
# the next incoming data
|
| 1527 |
+
livecount = sum(d._laststatus == d.LIVE for d in datas)
|
| 1528 |
+
newqcheck = not livecount or livecount == ldatas_noclones
|
| 1529 |
+
|
| 1530 |
+
lastret = False
|
| 1531 |
+
# Notify anything from the store even before moving datas
|
| 1532 |
+
# because datas may not move due to an error reported by the store
|
| 1533 |
+
self._storenotify()
|
| 1534 |
+
if self._event_stop: # stop if requested
|
| 1535 |
+
return
|
| 1536 |
+
self._datanotify()
|
| 1537 |
+
if self._event_stop: # stop if requested
|
| 1538 |
+
return
|
| 1539 |
+
|
| 1540 |
+
# record starting time and tell feeds to discount the elapsed time
|
| 1541 |
+
# from the qcheck value
|
| 1542 |
+
drets = []
|
| 1543 |
+
qstart = datetime.datetime.utcnow()
|
| 1544 |
+
for d in datas:
|
| 1545 |
+
qlapse = datetime.datetime.utcnow() - qstart
|
| 1546 |
+
d.do_qcheck(newqcheck, qlapse.total_seconds())
|
| 1547 |
+
drets.append(d.next(ticks=False))
|
| 1548 |
+
|
| 1549 |
+
d0ret = any((dret for dret in drets))
|
| 1550 |
+
if not d0ret and any((dret is None for dret in drets)):
|
| 1551 |
+
d0ret = None
|
| 1552 |
+
|
| 1553 |
+
if d0ret:
|
| 1554 |
+
dts = []
|
| 1555 |
+
for i, ret in enumerate(drets):
|
| 1556 |
+
dts.append(datas[i].datetime[0] if ret else None)
|
| 1557 |
+
|
| 1558 |
+
# Get index to minimum datetime
|
| 1559 |
+
if onlyresample or noresample:
|
| 1560 |
+
dt0 = min((d for d in dts if d is not None))
|
| 1561 |
+
else:
|
| 1562 |
+
dt0 = min((d for i, d in enumerate(dts)
|
| 1563 |
+
if d is not None and i not in rsonly))
|
| 1564 |
+
|
| 1565 |
+
dmaster = datas[dts.index(dt0)] # and timemaster
|
| 1566 |
+
self._dtmaster = dmaster.num2date(dt0)
|
| 1567 |
+
self._udtmaster = num2date(dt0)
|
| 1568 |
+
|
| 1569 |
+
# slen = len(runstrats[0])
|
| 1570 |
+
# Try to get something for those that didn't return
|
| 1571 |
+
for i, ret in enumerate(drets):
|
| 1572 |
+
if ret: # dts already contains a valid datetime for this i
|
| 1573 |
+
continue
|
| 1574 |
+
|
| 1575 |
+
# try to get a data by checking with a master
|
| 1576 |
+
d = datas[i]
|
| 1577 |
+
d._check(forcedata=dmaster) # check to force output
|
| 1578 |
+
if d.next(datamaster=dmaster, ticks=False): # retry
|
| 1579 |
+
dts[i] = d.datetime[0] # good -> store
|
| 1580 |
+
# self._plotfillers2[i].append(slen) # mark as fill
|
| 1581 |
+
else:
|
| 1582 |
+
# self._plotfillers[i].append(slen) # mark as empty
|
| 1583 |
+
pass
|
| 1584 |
+
|
| 1585 |
+
# make sure only those at dmaster level end up delivering
|
| 1586 |
+
for i, dti in enumerate(dts):
|
| 1587 |
+
if dti is not None:
|
| 1588 |
+
di = datas[i]
|
| 1589 |
+
rpi = False and di.replaying # to check behavior
|
| 1590 |
+
if dti > dt0:
|
| 1591 |
+
if not rpi: # must see all ticks ...
|
| 1592 |
+
di.rewind() # cannot deliver yet
|
| 1593 |
+
# self._plotfillers[i].append(slen)
|
| 1594 |
+
elif not di.replaying:
|
| 1595 |
+
# Replay forces tick fill, else force here
|
| 1596 |
+
di._tick_fill(force=True)
|
| 1597 |
+
|
| 1598 |
+
# self._plotfillers2[i].append(slen) # mark as fill
|
| 1599 |
+
|
| 1600 |
+
elif d0ret is None:
|
| 1601 |
+
# meant for things like live feeds which may not produce a bar
|
| 1602 |
+
# at the moment but need the loop to run for notifications and
|
| 1603 |
+
# getting resample and others to produce timely bars
|
| 1604 |
+
for data in datas:
|
| 1605 |
+
data._check()
|
| 1606 |
+
else:
|
| 1607 |
+
lastret = data0._last()
|
| 1608 |
+
for data in datas1:
|
| 1609 |
+
lastret += data._last(datamaster=data0)
|
| 1610 |
+
|
| 1611 |
+
if not lastret:
|
| 1612 |
+
# Only go extra round if something was changed by "lasts"
|
| 1613 |
+
break
|
| 1614 |
+
|
| 1615 |
+
# Datas may have generated a new notification after next
|
| 1616 |
+
self._datanotify()
|
| 1617 |
+
if self._event_stop: # stop if requested
|
| 1618 |
+
return
|
| 1619 |
+
|
| 1620 |
+
if d0ret or lastret: # if any bar, check timers before broker
|
| 1621 |
+
self._check_timers(runstrats, dt0, cheat=True)
|
| 1622 |
+
if self.p.cheat_on_open:
|
| 1623 |
+
for strat in runstrats:
|
| 1624 |
+
strat._next_open()
|
| 1625 |
+
if self._event_stop: # stop if requested
|
| 1626 |
+
return
|
| 1627 |
+
|
| 1628 |
+
self._brokernotify()
|
| 1629 |
+
if self._event_stop: # stop if requested
|
| 1630 |
+
return
|
| 1631 |
+
|
| 1632 |
+
if d0ret or lastret: # bars produced by data or filters
|
| 1633 |
+
self._check_timers(runstrats, dt0, cheat=False)
|
| 1634 |
+
for strat in runstrats:
|
| 1635 |
+
strat._next()
|
| 1636 |
+
if self._event_stop: # stop if requested
|
| 1637 |
+
return
|
| 1638 |
+
|
| 1639 |
+
self._next_writers(runstrats)
|
| 1640 |
+
|
| 1641 |
+
# Last notification chance before stopping
|
| 1642 |
+
self._datanotify()
|
| 1643 |
+
if self._event_stop: # stop if requested
|
| 1644 |
+
return
|
| 1645 |
+
self._storenotify()
|
| 1646 |
+
if self._event_stop: # stop if requested
|
| 1647 |
+
return
|
| 1648 |
+
|
| 1649 |
+
def _runonce(self, runstrats):
|
| 1650 |
+
'''
|
| 1651 |
+
Actual implementation of run in vector mode.
|
| 1652 |
+
|
| 1653 |
+
Strategies are still invoked on a pseudo-event mode in which ``next``
|
| 1654 |
+
is called for each data arrival
|
| 1655 |
+
'''
|
| 1656 |
+
for strat in runstrats:
|
| 1657 |
+
strat._once()
|
| 1658 |
+
strat.reset() # strat called next by next - reset lines
|
| 1659 |
+
|
| 1660 |
+
# The default once for strategies does nothing and therefore
|
| 1661 |
+
# has not moved forward all datas/indicators/observers that
|
| 1662 |
+
# were homed before calling once, Hence no "need" to do it
|
| 1663 |
+
# here again, because pointers are at 0
|
| 1664 |
+
datas = sorted(self.datas,
|
| 1665 |
+
key=lambda x: (x._timeframe, x._compression))
|
| 1666 |
+
|
| 1667 |
+
while True:
|
| 1668 |
+
# Check next incoming date in the datas
|
| 1669 |
+
dts = [d.advance_peek() for d in datas]
|
| 1670 |
+
dt0 = min(dts)
|
| 1671 |
+
if dt0 == float('inf'):
|
| 1672 |
+
break # no data delivers anything
|
| 1673 |
+
|
| 1674 |
+
# Timemaster if needed be
|
| 1675 |
+
# dmaster = datas[dts.index(dt0)] # and timemaster
|
| 1676 |
+
slen = len(runstrats[0])
|
| 1677 |
+
for i, dti in enumerate(dts):
|
| 1678 |
+
if dti <= dt0:
|
| 1679 |
+
datas[i].advance()
|
| 1680 |
+
# self._plotfillers2[i].append(slen) # mark as fill
|
| 1681 |
+
else:
|
| 1682 |
+
# self._plotfillers[i].append(slen)
|
| 1683 |
+
pass
|
| 1684 |
+
|
| 1685 |
+
self._check_timers(runstrats, dt0, cheat=True)
|
| 1686 |
+
|
| 1687 |
+
if self.p.cheat_on_open:
|
| 1688 |
+
for strat in runstrats:
|
| 1689 |
+
strat._oncepost_open()
|
| 1690 |
+
if self._event_stop: # stop if requested
|
| 1691 |
+
return
|
| 1692 |
+
|
| 1693 |
+
self._brokernotify()
|
| 1694 |
+
if self._event_stop: # stop if requested
|
| 1695 |
+
return
|
| 1696 |
+
|
| 1697 |
+
self._check_timers(runstrats, dt0, cheat=False)
|
| 1698 |
+
|
| 1699 |
+
for strat in runstrats:
|
| 1700 |
+
strat._oncepost(dt0)
|
| 1701 |
+
if self._event_stop: # stop if requested
|
| 1702 |
+
return
|
| 1703 |
+
|
| 1704 |
+
self._next_writers(runstrats)
|
| 1705 |
+
|
| 1706 |
+
def _check_timers(self, runstrats, dt0, cheat=False):
|
| 1707 |
+
timers = self._timers if not cheat else self._timerscheat
|
| 1708 |
+
for t in timers:
|
| 1709 |
+
if not t.check(dt0):
|
| 1710 |
+
continue
|
| 1711 |
+
|
| 1712 |
+
t.params.owner.notify_timer(t, t.lastwhen, *t.args, **t.kwargs)
|
| 1713 |
+
|
| 1714 |
+
if t.params.strats:
|
| 1715 |
+
for strat in runstrats:
|
| 1716 |
+
strat.notify_timer(t, t.lastwhen, *t.args, **t.kwargs)
|
backtrader/source/backtrader/comminfo.py
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import datetime
|
| 25 |
+
|
| 26 |
+
from .utils.py3 import with_metaclass
|
| 27 |
+
from .metabase import MetaParams
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
class CommInfoBase(with_metaclass(MetaParams)):
|
| 31 |
+
'''Base Class for the Commission Schemes.
|
| 32 |
+
|
| 33 |
+
Params:
|
| 34 |
+
|
| 35 |
+
- ``commission`` (def: ``0.0``): base commission value in percentage or
|
| 36 |
+
monetary units
|
| 37 |
+
|
| 38 |
+
- ``mult`` (def ``1.0``): multiplier applied to the asset for
|
| 39 |
+
value/profit
|
| 40 |
+
|
| 41 |
+
- ``margin`` (def: ``None``): amount of monetary units needed to
|
| 42 |
+
open/hold an operation. It only applies if the final ``_stocklike``
|
| 43 |
+
attribute in the class is set to ``False``
|
| 44 |
+
|
| 45 |
+
- ``automargin`` (def: ``False``): Used by the method ``get_margin``
|
| 46 |
+
to automatically calculate the margin/guarantees needed with the
|
| 47 |
+
following policy
|
| 48 |
+
|
| 49 |
+
- Use param ``margin`` if param ``automargin`` evaluates to ``False``
|
| 50 |
+
|
| 51 |
+
- Use param ``mult`` * ``price`` if ``automargin < 0``
|
| 52 |
+
|
| 53 |
+
- Use param ``automargin`` * ``price`` if ``automargin > 0``
|
| 54 |
+
|
| 55 |
+
- ``commtype`` (def: ``None``): Supported values are
|
| 56 |
+
``CommInfoBase.COMM_PERC`` (commission to be understood as %) and
|
| 57 |
+
``CommInfoBase.COMM_FIXED`` (commission to be understood as monetary
|
| 58 |
+
units)
|
| 59 |
+
|
| 60 |
+
The default value of ``None`` is a supported value to retain
|
| 61 |
+
compatibility with the legacy ``CommissionInfo`` object. If
|
| 62 |
+
``commtype`` is set to None, then the following applies:
|
| 63 |
+
|
| 64 |
+
- ``margin`` is ``None``: Internal ``_commtype`` is set to
|
| 65 |
+
``COMM_PERC`` and ``_stocklike`` is set to ``True`` (Operating
|
| 66 |
+
%-wise with Stocks)
|
| 67 |
+
|
| 68 |
+
- ``margin`` is not ``None``: ``_commtype`` set to ``COMM_FIXED`` and
|
| 69 |
+
``_stocklike`` set to ``False`` (Operating with fixed rount-trip
|
| 70 |
+
commission with Futures)
|
| 71 |
+
|
| 72 |
+
If this param is set to something else than ``None``, then it will be
|
| 73 |
+
passed to the internal ``_commtype`` attribute and the same will be
|
| 74 |
+
done with the param ``stocklike`` and the internal attribute
|
| 75 |
+
``_stocklike``
|
| 76 |
+
|
| 77 |
+
- ``stocklike`` (def: ``False``): Indicates if the instrument is
|
| 78 |
+
Stock-like or Futures-like (see the ``commtype`` discussion above)
|
| 79 |
+
|
| 80 |
+
- ``percabs`` (def: ``False``): when ``commtype`` is set to COMM_PERC,
|
| 81 |
+
whether the parameter ``commission`` has to be understood as XX% or
|
| 82 |
+
0.XX
|
| 83 |
+
|
| 84 |
+
If this param is ``True``: 0.XX
|
| 85 |
+
If this param is ``False``: XX%
|
| 86 |
+
|
| 87 |
+
- ``interest`` (def: ``0.0``)
|
| 88 |
+
|
| 89 |
+
If this is non-zero, this is the yearly interest charged for holding a
|
| 90 |
+
short selling position. This is mostly meant for stock short-selling
|
| 91 |
+
|
| 92 |
+
The formula: ``days * price * abs(size) * (interest / 365)``
|
| 93 |
+
|
| 94 |
+
It must be specified in absolute terms: 0.05 -> 5%
|
| 95 |
+
|
| 96 |
+
.. note:: the behavior can be changed by overriding the method:
|
| 97 |
+
``_get_credit_interest``
|
| 98 |
+
|
| 99 |
+
- ``interest_long`` (def: ``False``)
|
| 100 |
+
|
| 101 |
+
Some products like ETFs get charged on interest for short and long
|
| 102 |
+
positions. If ths is ``True`` and ``interest`` is non-zero the interest
|
| 103 |
+
will be charged on both directions
|
| 104 |
+
|
| 105 |
+
- ``leverage`` (def: ``1.0``)
|
| 106 |
+
|
| 107 |
+
Amount of leverage for the asset with regards to the needed cash
|
| 108 |
+
|
| 109 |
+
Attributes:
|
| 110 |
+
|
| 111 |
+
- ``_stocklike``: Final value to use for Stock-like/Futures-like behavior
|
| 112 |
+
- ``_commtype``: Final value to use for PERC vs FIXED commissions
|
| 113 |
+
|
| 114 |
+
This two are used internally instead of the declared params to enable the
|
| 115 |
+
compatibility check described above for the legacy ``CommissionInfo``
|
| 116 |
+
object
|
| 117 |
+
|
| 118 |
+
'''
|
| 119 |
+
|
| 120 |
+
COMM_PERC, COMM_FIXED = range(2)
|
| 121 |
+
|
| 122 |
+
params = (
|
| 123 |
+
('commission', 0.0), ('mult', 1.0), ('margin', None),
|
| 124 |
+
('commtype', None),
|
| 125 |
+
('stocklike', False),
|
| 126 |
+
('percabs', False),
|
| 127 |
+
('interest', 0.0),
|
| 128 |
+
('interest_long', False),
|
| 129 |
+
('leverage', 1.0),
|
| 130 |
+
('automargin', False),
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
def __init__(self):
|
| 134 |
+
super(CommInfoBase, self).__init__()
|
| 135 |
+
|
| 136 |
+
self._stocklike = self.p.stocklike
|
| 137 |
+
self._commtype = self.p.commtype
|
| 138 |
+
|
| 139 |
+
# The intial block checks for the behavior of the original
|
| 140 |
+
# CommissionInfo in which the commission scheme (perc/fixed) was
|
| 141 |
+
# determined by parameter "margin" evaluating to False/True
|
| 142 |
+
# If the parameter "commtype" is None, this behavior is emulated
|
| 143 |
+
# else, the parameter values are used
|
| 144 |
+
|
| 145 |
+
if self._commtype is None: # original CommissionInfo behavior applies
|
| 146 |
+
if self.p.margin:
|
| 147 |
+
self._stocklike = False
|
| 148 |
+
self._commtype = self.COMM_FIXED
|
| 149 |
+
else:
|
| 150 |
+
self._stocklike = True
|
| 151 |
+
self._commtype = self.COMM_PERC
|
| 152 |
+
|
| 153 |
+
if not self._stocklike and not self.p.margin:
|
| 154 |
+
self.p.margin = 1.0 # avoid having None/0
|
| 155 |
+
|
| 156 |
+
if self._commtype == self.COMM_PERC and not self.p.percabs:
|
| 157 |
+
self.p.commission /= 100.0
|
| 158 |
+
|
| 159 |
+
self._creditrate = self.p.interest / 365.0
|
| 160 |
+
|
| 161 |
+
@property
|
| 162 |
+
def margin(self):
|
| 163 |
+
return self.p.margin
|
| 164 |
+
|
| 165 |
+
@property
|
| 166 |
+
def stocklike(self):
|
| 167 |
+
return self._stocklike
|
| 168 |
+
|
| 169 |
+
def get_margin(self, price):
|
| 170 |
+
'''Returns the actual margin/guarantees needed for a single item of the
|
| 171 |
+
asset at the given price. The default implementation has this policy:
|
| 172 |
+
|
| 173 |
+
- Use param ``margin`` if param ``automargin`` evaluates to ``False``
|
| 174 |
+
|
| 175 |
+
- Use param ``mult`` * ``price`` if ``automargin < 0``
|
| 176 |
+
|
| 177 |
+
- Use param ``automargin`` * ``price`` if ``automargin > 0``
|
| 178 |
+
'''
|
| 179 |
+
if not self.p.automargin:
|
| 180 |
+
return self.p.margin
|
| 181 |
+
|
| 182 |
+
elif self.p.automargin < 0:
|
| 183 |
+
return price * self.p.mult
|
| 184 |
+
|
| 185 |
+
return price * self.p.automargin # int/float expected
|
| 186 |
+
|
| 187 |
+
def get_leverage(self):
|
| 188 |
+
|
| 189 |
+
'''Returns the level of leverage allowed for this comission scheme'''
|
| 190 |
+
return self.p.leverage
|
| 191 |
+
|
| 192 |
+
def getsize(self, price, cash):
|
| 193 |
+
'''Returns the needed size to meet a cash operation at a given price'''
|
| 194 |
+
if not self._stocklike:
|
| 195 |
+
return int(self.p.leverage * (cash // self.get_margin(price)))
|
| 196 |
+
|
| 197 |
+
return int(self.p.leverage * (cash // price))
|
| 198 |
+
|
| 199 |
+
def getoperationcost(self, size, price):
|
| 200 |
+
'''Returns the needed amount of cash an operation would cost'''
|
| 201 |
+
if not self._stocklike:
|
| 202 |
+
return abs(size) * self.get_margin(price)
|
| 203 |
+
|
| 204 |
+
return abs(size) * price
|
| 205 |
+
|
| 206 |
+
def getvaluesize(self, size, price):
|
| 207 |
+
'''Returns the value of size for given a price. For future-like
|
| 208 |
+
objects it is fixed at size * margin'''
|
| 209 |
+
if not self._stocklike:
|
| 210 |
+
return abs(size) * self.get_margin(price)
|
| 211 |
+
|
| 212 |
+
return size * price
|
| 213 |
+
|
| 214 |
+
def getvalue(self, position, price):
|
| 215 |
+
'''Returns the value of a position given a price. For future-like
|
| 216 |
+
objects it is fixed at size * margin'''
|
| 217 |
+
if not self._stocklike:
|
| 218 |
+
return abs(position.size) * self.get_margin(price)
|
| 219 |
+
|
| 220 |
+
size = position.size
|
| 221 |
+
if size >= 0:
|
| 222 |
+
return size * price
|
| 223 |
+
|
| 224 |
+
# With stocks, a short position is worth more as the price goes down
|
| 225 |
+
value = position.price * size # original value
|
| 226 |
+
value += (position.price - price) * size # increased value
|
| 227 |
+
return value
|
| 228 |
+
|
| 229 |
+
def _getcommission(self, size, price, pseudoexec):
|
| 230 |
+
'''Calculates the commission of an operation at a given price
|
| 231 |
+
|
| 232 |
+
pseudoexec: if True the operation has not yet been executed
|
| 233 |
+
'''
|
| 234 |
+
if self._commtype == self.COMM_PERC:
|
| 235 |
+
return abs(size) * self.p.commission * price
|
| 236 |
+
|
| 237 |
+
return abs(size) * self.p.commission
|
| 238 |
+
|
| 239 |
+
def getcommission(self, size, price):
|
| 240 |
+
'''Calculates the commission of an operation at a given price
|
| 241 |
+
'''
|
| 242 |
+
return self._getcommission(size, price, pseudoexec=True)
|
| 243 |
+
|
| 244 |
+
def confirmexec(self, size, price):
|
| 245 |
+
return self._getcommission(size, price, pseudoexec=False)
|
| 246 |
+
|
| 247 |
+
def profitandloss(self, size, price, newprice):
|
| 248 |
+
'''Return actual profit and loss a position has'''
|
| 249 |
+
return size * (newprice - price) * self.p.mult
|
| 250 |
+
|
| 251 |
+
def cashadjust(self, size, price, newprice):
|
| 252 |
+
'''Calculates cash adjustment for a given price difference'''
|
| 253 |
+
if not self._stocklike:
|
| 254 |
+
return size * (newprice - price) * self.p.mult
|
| 255 |
+
|
| 256 |
+
return 0.0
|
| 257 |
+
|
| 258 |
+
def get_credit_interest(self, data, pos, dt):
|
| 259 |
+
'''Calculates the credit due for short selling or product specific'''
|
| 260 |
+
size, price = pos.size, pos.price
|
| 261 |
+
|
| 262 |
+
if size > 0 and not self.p.interest_long:
|
| 263 |
+
return 0.0 # long positions not charged
|
| 264 |
+
|
| 265 |
+
dt0 = dt.date()
|
| 266 |
+
dt1 = pos.datetime.date()
|
| 267 |
+
|
| 268 |
+
if dt0 <= dt1:
|
| 269 |
+
return 0.0
|
| 270 |
+
|
| 271 |
+
return self._get_credit_interest(data, size, price,
|
| 272 |
+
(dt0 - dt1).days, dt0, dt1)
|
| 273 |
+
|
| 274 |
+
def _get_credit_interest(self, data, size, price, days, dt0, dt1):
|
| 275 |
+
'''
|
| 276 |
+
This method returns the cost in terms of credit interest charged by
|
| 277 |
+
the broker.
|
| 278 |
+
|
| 279 |
+
In the case of ``size > 0`` this method will only be called if the
|
| 280 |
+
parameter to the class ``interest_long`` is ``True``
|
| 281 |
+
|
| 282 |
+
The formulat for the calculation of the credit interest rate is:
|
| 283 |
+
|
| 284 |
+
The formula: ``days * price * abs(size) * (interest / 365)``
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
Params:
|
| 288 |
+
- ``data``: data feed for which interest is charged
|
| 289 |
+
|
| 290 |
+
- ``size``: current position size. > 0 for long positions and < 0 for
|
| 291 |
+
short positions (this parameter will not be ``0``)
|
| 292 |
+
|
| 293 |
+
- ``price``: current position price
|
| 294 |
+
|
| 295 |
+
- ``days``: number of days elapsed since last credit calculation
|
| 296 |
+
(this is (dt0 - dt1).days)
|
| 297 |
+
|
| 298 |
+
- ``dt0``: (datetime.datetime) current datetime
|
| 299 |
+
|
| 300 |
+
- ``dt1``: (datetime.datetime) datetime of previous calculation
|
| 301 |
+
|
| 302 |
+
``dt0`` and ``dt1`` are not used in the default implementation and are
|
| 303 |
+
provided as extra input for overridden methods
|
| 304 |
+
'''
|
| 305 |
+
return days * self._creditrate * abs(size) * price
|
| 306 |
+
|
| 307 |
+
|
| 308 |
+
class CommissionInfo(CommInfoBase):
|
| 309 |
+
'''Base Class for the actual Commission Schemes.
|
| 310 |
+
|
| 311 |
+
CommInfoBase was created to keep suppor for the original, incomplete,
|
| 312 |
+
support provided by *backtrader*. New commission schemes derive from this
|
| 313 |
+
class which subclasses ``CommInfoBase``.
|
| 314 |
+
|
| 315 |
+
The default value of ``percabs`` is also changed to ``True``
|
| 316 |
+
|
| 317 |
+
Params:
|
| 318 |
+
|
| 319 |
+
- ``percabs`` (def: True): when ``commtype`` is set to COMM_PERC, whether
|
| 320 |
+
the parameter ``commission`` has to be understood as XX% or 0.XX
|
| 321 |
+
|
| 322 |
+
If this param is True: 0.XX
|
| 323 |
+
If this param is False: XX%
|
| 324 |
+
|
| 325 |
+
'''
|
| 326 |
+
params = (
|
| 327 |
+
('percabs', True), # Original CommissionInfo took 0.xx for percentages
|
| 328 |
+
)
|
backtrader/source/backtrader/commissions/__init__.py
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
from ..comminfo import CommInfoBase
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class CommInfo(CommInfoBase):
|
| 28 |
+
pass # clone of CommissionInfo but with xx% instead of 0.xx
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class CommInfo_Futures(CommInfoBase):
|
| 32 |
+
params = (
|
| 33 |
+
('stocklike', False),
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class CommInfo_Futures_Perc(CommInfo_Futures):
|
| 38 |
+
params = (
|
| 39 |
+
('commtype', CommInfoBase.COMM_PERC),
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class CommInfo_Futures_Fixed(CommInfo_Futures):
|
| 44 |
+
params = (
|
| 45 |
+
('commtype', CommInfoBase.COMM_FIXED),
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class CommInfo_Stocks(CommInfoBase):
|
| 50 |
+
params = (
|
| 51 |
+
('stocklike', True),
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class CommInfo_Stocks_Perc(CommInfo_Stocks):
|
| 56 |
+
params = (
|
| 57 |
+
('commtype', CommInfoBase.COMM_PERC),
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
class CommInfo_Stocks_Fixed(CommInfo_Stocks):
|
| 62 |
+
params = (
|
| 63 |
+
('commtype', CommInfoBase.COMM_FIXED),
|
| 64 |
+
)
|
backtrader/source/backtrader/dataseries.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import datetime as _datetime
|
| 25 |
+
from datetime import datetime
|
| 26 |
+
import inspect
|
| 27 |
+
|
| 28 |
+
from .utils.py3 import range, with_metaclass
|
| 29 |
+
from .lineseries import LineSeries
|
| 30 |
+
from .utils import AutoOrderedDict, OrderedDict, date2num
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class TimeFrame(object):
|
| 34 |
+
(Ticks, MicroSeconds, Seconds, Minutes,
|
| 35 |
+
Days, Weeks, Months, Years, NoTimeFrame) = range(1, 10)
|
| 36 |
+
|
| 37 |
+
Names = ['', 'Ticks', 'MicroSeconds', 'Seconds', 'Minutes',
|
| 38 |
+
'Days', 'Weeks', 'Months', 'Years', 'NoTimeFrame']
|
| 39 |
+
|
| 40 |
+
names = Names # support old naming convention
|
| 41 |
+
|
| 42 |
+
@classmethod
|
| 43 |
+
def getname(cls, tframe, compression=None):
|
| 44 |
+
tname = cls.Names[tframe]
|
| 45 |
+
if compression > 1 or tname == cls.Names[-1]:
|
| 46 |
+
return tname # for plural or 'NoTimeFrame' return plain entry
|
| 47 |
+
|
| 48 |
+
# return singular if compression is 1
|
| 49 |
+
return cls.Names[tframe][:-1]
|
| 50 |
+
|
| 51 |
+
@classmethod
|
| 52 |
+
def TFrame(cls, name):
|
| 53 |
+
return getattr(cls, name)
|
| 54 |
+
|
| 55 |
+
@classmethod
|
| 56 |
+
def TName(cls, tframe):
|
| 57 |
+
return cls.Names[tframe]
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class DataSeries(LineSeries):
|
| 61 |
+
plotinfo = dict(plot=True, plotind=True, plotylimited=True)
|
| 62 |
+
|
| 63 |
+
_name = ''
|
| 64 |
+
_compression = 1
|
| 65 |
+
_timeframe = TimeFrame.Days
|
| 66 |
+
|
| 67 |
+
Close, Low, High, Open, Volume, OpenInterest, DateTime = range(7)
|
| 68 |
+
|
| 69 |
+
LineOrder = [DateTime, Open, High, Low, Close, Volume, OpenInterest]
|
| 70 |
+
|
| 71 |
+
def getwriterheaders(self):
|
| 72 |
+
headers = [self._name, 'len']
|
| 73 |
+
|
| 74 |
+
for lo in self.LineOrder:
|
| 75 |
+
headers.append(self._getlinealias(lo))
|
| 76 |
+
|
| 77 |
+
morelines = self.getlinealiases()[len(self.LineOrder):]
|
| 78 |
+
headers.extend(morelines)
|
| 79 |
+
|
| 80 |
+
return headers
|
| 81 |
+
|
| 82 |
+
def getwritervalues(self):
|
| 83 |
+
l = len(self)
|
| 84 |
+
values = [self._name, l]
|
| 85 |
+
|
| 86 |
+
if l:
|
| 87 |
+
values.append(self.datetime.datetime(0))
|
| 88 |
+
for line in self.LineOrder[1:]:
|
| 89 |
+
values.append(self.lines[line][0])
|
| 90 |
+
for i in range(len(self.LineOrder), self.lines.size()):
|
| 91 |
+
values.append(self.lines[i][0])
|
| 92 |
+
else:
|
| 93 |
+
values.extend([''] * self.lines.size()) # no values yet
|
| 94 |
+
|
| 95 |
+
return values
|
| 96 |
+
|
| 97 |
+
def getwriterinfo(self):
|
| 98 |
+
# returns dictionary with information
|
| 99 |
+
info = OrderedDict()
|
| 100 |
+
info['Name'] = self._name
|
| 101 |
+
info['Timeframe'] = TimeFrame.TName(self._timeframe)
|
| 102 |
+
info['Compression'] = self._compression
|
| 103 |
+
|
| 104 |
+
return info
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
class OHLC(DataSeries):
|
| 108 |
+
lines = ('close', 'low', 'high', 'open', 'volume', 'openinterest',)
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
class OHLCDateTime(OHLC):
|
| 112 |
+
lines = (('datetime'),)
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
class SimpleFilterWrapper(object):
|
| 116 |
+
'''Wrapper for filters added via .addfilter to turn them
|
| 117 |
+
into processors.
|
| 118 |
+
|
| 119 |
+
Filters are callables which
|
| 120 |
+
|
| 121 |
+
- Take a ``data`` as an argument
|
| 122 |
+
- Return False if the current bar has not triggered the filter
|
| 123 |
+
- Return True if the current bar must be filtered
|
| 124 |
+
|
| 125 |
+
The wrapper takes the return value and executes the bar removal
|
| 126 |
+
if needed be
|
| 127 |
+
'''
|
| 128 |
+
def __init__(self, data, ffilter, *args, **kwargs):
|
| 129 |
+
if inspect.isclass(ffilter):
|
| 130 |
+
ffilter = ffilter(data, *args, **kwargs)
|
| 131 |
+
args = []
|
| 132 |
+
kwargs = {}
|
| 133 |
+
|
| 134 |
+
self.ffilter = ffilter
|
| 135 |
+
self.args = args
|
| 136 |
+
self.kwargs = kwargs
|
| 137 |
+
|
| 138 |
+
def __call__(self, data):
|
| 139 |
+
if self.ffilter(data, *self.args, **self.kwargs):
|
| 140 |
+
data.backwards()
|
| 141 |
+
return True
|
| 142 |
+
|
| 143 |
+
return False
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
class _Bar(AutoOrderedDict):
|
| 147 |
+
'''
|
| 148 |
+
This class is a placeholder for the values of the standard lines of a
|
| 149 |
+
DataBase class (from OHLCDateTime)
|
| 150 |
+
|
| 151 |
+
It inherits from AutoOrderedDict to be able to easily return the values as
|
| 152 |
+
an iterable and address the keys as attributes
|
| 153 |
+
|
| 154 |
+
Order of definition is important and must match that of the lines
|
| 155 |
+
definition in DataBase (which directly inherits from OHLCDateTime)
|
| 156 |
+
'''
|
| 157 |
+
replaying = False
|
| 158 |
+
|
| 159 |
+
# Without - 1 ... converting back to time will not work
|
| 160 |
+
# Need another -1 to support timezones which may move the time forward
|
| 161 |
+
MAXDATE = date2num(_datetime.datetime.max) - 2
|
| 162 |
+
|
| 163 |
+
def __init__(self, maxdate=False):
|
| 164 |
+
super(_Bar, self).__init__()
|
| 165 |
+
self.bstart(maxdate=maxdate)
|
| 166 |
+
|
| 167 |
+
def bstart(self, maxdate=False):
|
| 168 |
+
'''Initializes a bar to the default not-updated vaues'''
|
| 169 |
+
# Order is important: defined in DataSeries/OHLC/OHLCDateTime
|
| 170 |
+
self.close = float('NaN')
|
| 171 |
+
self.low = float('inf')
|
| 172 |
+
self.high = float('-inf')
|
| 173 |
+
self.open = float('NaN')
|
| 174 |
+
self.volume = 0.0
|
| 175 |
+
self.openinterest = 0.0
|
| 176 |
+
self.datetime = self.MAXDATE if maxdate else None
|
| 177 |
+
|
| 178 |
+
def isopen(self):
|
| 179 |
+
'''Returns if a bar has already been updated
|
| 180 |
+
|
| 181 |
+
Uses the fact that NaN is the value which is not equal to itself
|
| 182 |
+
and ``open`` is initialized to NaN
|
| 183 |
+
'''
|
| 184 |
+
o = self.open
|
| 185 |
+
return o == o # False if NaN, True in other cases
|
| 186 |
+
|
| 187 |
+
def bupdate(self, data, reopen=False):
|
| 188 |
+
'''Updates a bar with the values from data
|
| 189 |
+
|
| 190 |
+
Returns True if the update was the 1st on a bar (just opened)
|
| 191 |
+
|
| 192 |
+
Returns False otherwise
|
| 193 |
+
'''
|
| 194 |
+
if reopen:
|
| 195 |
+
self.bstart()
|
| 196 |
+
|
| 197 |
+
self.datetime = data.datetime[0]
|
| 198 |
+
|
| 199 |
+
self.high = max(self.high, data.high[0])
|
| 200 |
+
self.low = min(self.low, data.low[0])
|
| 201 |
+
self.close = data.close[0]
|
| 202 |
+
|
| 203 |
+
self.volume += data.volume[0]
|
| 204 |
+
self.openinterest = data.openinterest[0]
|
| 205 |
+
|
| 206 |
+
o = self.open
|
| 207 |
+
if reopen or not o == o:
|
| 208 |
+
self.open = data.open[0]
|
| 209 |
+
return True # just opened the bar
|
| 210 |
+
|
| 211 |
+
return False
|
backtrader/source/backtrader/errors.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
__all__ = ['BacktraderError', 'StrategySkipError']
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class BacktraderError(Exception):
|
| 29 |
+
'''Base exception for all other exceptions'''
|
| 30 |
+
pass
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class StrategySkipError(BacktraderError):
|
| 34 |
+
'''Requests the platform to skip this strategy for backtesting. To be
|
| 35 |
+
raised during the initialization (``__init__``) phase of the instance'''
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class ModuleImportError(BacktraderError):
|
| 40 |
+
'''Raised if a class requests a module to be present to work and it cannot
|
| 41 |
+
be imported'''
|
| 42 |
+
def __init__(self, message, *args):
|
| 43 |
+
super(ModuleImportError, self).__init__(message)
|
| 44 |
+
self.args = args
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class FromModuleImportError(ModuleImportError):
|
| 48 |
+
'''Raised if a class requests a module to be present to work and it cannot
|
| 49 |
+
be imported'''
|
| 50 |
+
def __init__(self, message, *args):
|
| 51 |
+
super(FromModuleImportError, self).__init__(message, *args)
|
backtrader/source/backtrader/feed.py
ADDED
|
@@ -0,0 +1,813 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
import collections
|
| 25 |
+
import datetime
|
| 26 |
+
import inspect
|
| 27 |
+
import io
|
| 28 |
+
import os.path
|
| 29 |
+
|
| 30 |
+
import backtrader as bt
|
| 31 |
+
from backtrader import (date2num, num2date, time2num, TimeFrame, dataseries,
|
| 32 |
+
metabase)
|
| 33 |
+
|
| 34 |
+
from backtrader.utils.py3 import with_metaclass, zip, range, string_types
|
| 35 |
+
from backtrader.utils import tzparse
|
| 36 |
+
from .dataseries import SimpleFilterWrapper
|
| 37 |
+
from .resamplerfilter import Resampler, Replayer
|
| 38 |
+
from .tradingcal import PandasMarketCalendar
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
class MetaAbstractDataBase(dataseries.OHLCDateTime.__class__):
|
| 42 |
+
_indcol = dict()
|
| 43 |
+
|
| 44 |
+
def __init__(cls, name, bases, dct):
|
| 45 |
+
'''
|
| 46 |
+
Class has already been created ... register subclasses
|
| 47 |
+
'''
|
| 48 |
+
# Initialize the class
|
| 49 |
+
super(MetaAbstractDataBase, cls).__init__(name, bases, dct)
|
| 50 |
+
|
| 51 |
+
if not cls.aliased and \
|
| 52 |
+
name != 'DataBase' and not name.startswith('_'):
|
| 53 |
+
cls._indcol[name] = cls
|
| 54 |
+
|
| 55 |
+
def dopreinit(cls, _obj, *args, **kwargs):
|
| 56 |
+
_obj, args, kwargs = \
|
| 57 |
+
super(MetaAbstractDataBase, cls).dopreinit(_obj, *args, **kwargs)
|
| 58 |
+
|
| 59 |
+
# Find the owner and store it
|
| 60 |
+
_obj._feed = metabase.findowner(_obj, FeedBase)
|
| 61 |
+
|
| 62 |
+
_obj.notifs = collections.deque() # store notifications for cerebro
|
| 63 |
+
|
| 64 |
+
_obj._dataname = _obj.p.dataname
|
| 65 |
+
_obj._name = ''
|
| 66 |
+
return _obj, args, kwargs
|
| 67 |
+
|
| 68 |
+
def dopostinit(cls, _obj, *args, **kwargs):
|
| 69 |
+
_obj, args, kwargs = \
|
| 70 |
+
super(MetaAbstractDataBase, cls).dopostinit(_obj, *args, **kwargs)
|
| 71 |
+
|
| 72 |
+
# Either set by subclass or the parameter or use the dataname (ticker)
|
| 73 |
+
_obj._name = _obj._name or _obj.p.name
|
| 74 |
+
if not _obj._name and isinstance(_obj.p.dataname, string_types):
|
| 75 |
+
_obj._name = _obj.p.dataname
|
| 76 |
+
_obj._compression = _obj.p.compression
|
| 77 |
+
_obj._timeframe = _obj.p.timeframe
|
| 78 |
+
|
| 79 |
+
if isinstance(_obj.p.sessionstart, datetime.datetime):
|
| 80 |
+
_obj.p.sessionstart = _obj.p.sessionstart.time()
|
| 81 |
+
|
| 82 |
+
elif _obj.p.sessionstart is None:
|
| 83 |
+
_obj.p.sessionstart = datetime.time.min
|
| 84 |
+
|
| 85 |
+
if isinstance(_obj.p.sessionend, datetime.datetime):
|
| 86 |
+
_obj.p.sessionend = _obj.p.sessionend.time()
|
| 87 |
+
|
| 88 |
+
elif _obj.p.sessionend is None:
|
| 89 |
+
# remove 9 to avoid precision rounding errors
|
| 90 |
+
_obj.p.sessionend = datetime.time(23, 59, 59, 999990)
|
| 91 |
+
|
| 92 |
+
if isinstance(_obj.p.fromdate, datetime.date):
|
| 93 |
+
# push it to the end of the day, or else intraday
|
| 94 |
+
# values before the end of the day would be gone
|
| 95 |
+
if not hasattr(_obj.p.fromdate, 'hour'):
|
| 96 |
+
_obj.p.fromdate = datetime.datetime.combine(
|
| 97 |
+
_obj.p.fromdate, _obj.p.sessionstart)
|
| 98 |
+
|
| 99 |
+
if isinstance(_obj.p.todate, datetime.date):
|
| 100 |
+
# push it to the end of the day, or else intraday
|
| 101 |
+
# values before the end of the day would be gone
|
| 102 |
+
if not hasattr(_obj.p.todate, 'hour'):
|
| 103 |
+
_obj.p.todate = datetime.datetime.combine(
|
| 104 |
+
_obj.p.todate, _obj.p.sessionend)
|
| 105 |
+
|
| 106 |
+
_obj._barstack = collections.deque() # for filter operations
|
| 107 |
+
_obj._barstash = collections.deque() # for filter operations
|
| 108 |
+
|
| 109 |
+
_obj._filters = list()
|
| 110 |
+
_obj._ffilters = list()
|
| 111 |
+
for fp in _obj.p.filters:
|
| 112 |
+
if inspect.isclass(fp):
|
| 113 |
+
fp = fp(_obj)
|
| 114 |
+
if hasattr(fp, 'last'):
|
| 115 |
+
_obj._ffilters.append((fp, [], {}))
|
| 116 |
+
|
| 117 |
+
_obj._filters.append((fp, [], {}))
|
| 118 |
+
|
| 119 |
+
return _obj, args, kwargs
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class AbstractDataBase(with_metaclass(MetaAbstractDataBase,
|
| 123 |
+
dataseries.OHLCDateTime)):
|
| 124 |
+
|
| 125 |
+
params = (
|
| 126 |
+
('dataname', None),
|
| 127 |
+
('name', ''),
|
| 128 |
+
('compression', 1),
|
| 129 |
+
('timeframe', TimeFrame.Days),
|
| 130 |
+
('fromdate', None),
|
| 131 |
+
('todate', None),
|
| 132 |
+
('sessionstart', None),
|
| 133 |
+
('sessionend', None),
|
| 134 |
+
('filters', []),
|
| 135 |
+
('tz', None),
|
| 136 |
+
('tzinput', None),
|
| 137 |
+
('qcheck', 0.0), # timeout in seconds (float) to check for events
|
| 138 |
+
('calendar', None),
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
(CONNECTED, DISCONNECTED, CONNBROKEN, DELAYED,
|
| 142 |
+
LIVE, NOTSUBSCRIBED, NOTSUPPORTED_TF, UNKNOWN) = range(8)
|
| 143 |
+
|
| 144 |
+
_NOTIFNAMES = [
|
| 145 |
+
'CONNECTED', 'DISCONNECTED', 'CONNBROKEN', 'DELAYED',
|
| 146 |
+
'LIVE', 'NOTSUBSCRIBED', 'NOTSUPPORTED_TIMEFRAME', 'UNKNOWN']
|
| 147 |
+
|
| 148 |
+
@classmethod
|
| 149 |
+
def _getstatusname(cls, status):
|
| 150 |
+
return cls._NOTIFNAMES[status]
|
| 151 |
+
|
| 152 |
+
_compensate = None
|
| 153 |
+
_feed = None
|
| 154 |
+
_store = None
|
| 155 |
+
|
| 156 |
+
_clone = False
|
| 157 |
+
_qcheck = 0.0
|
| 158 |
+
|
| 159 |
+
_tmoffset = datetime.timedelta()
|
| 160 |
+
|
| 161 |
+
# Set to non 0 if resampling/replaying
|
| 162 |
+
resampling = 0
|
| 163 |
+
replaying = 0
|
| 164 |
+
|
| 165 |
+
_started = False
|
| 166 |
+
|
| 167 |
+
def _start_finish(self):
|
| 168 |
+
# A live feed (for example) may have learnt something about the
|
| 169 |
+
# timezones after the start and that's why the date/time related
|
| 170 |
+
# parameters are converted at this late stage
|
| 171 |
+
# Get the output timezone (if any)
|
| 172 |
+
self._tz = self._gettz()
|
| 173 |
+
# Lines have already been create, set the tz
|
| 174 |
+
self.lines.datetime._settz(self._tz)
|
| 175 |
+
|
| 176 |
+
# This should probably be also called from an override-able method
|
| 177 |
+
self._tzinput = bt.utils.date.Localizer(self._gettzinput())
|
| 178 |
+
|
| 179 |
+
# Convert user input times to the output timezone (or min/max)
|
| 180 |
+
if self.p.fromdate is None:
|
| 181 |
+
self.fromdate = float('-inf')
|
| 182 |
+
else:
|
| 183 |
+
self.fromdate = self.date2num(self.p.fromdate)
|
| 184 |
+
|
| 185 |
+
if self.p.todate is None:
|
| 186 |
+
self.todate = float('inf')
|
| 187 |
+
else:
|
| 188 |
+
self.todate = self.date2num(self.p.todate)
|
| 189 |
+
|
| 190 |
+
# FIXME: These two are never used and could be removed
|
| 191 |
+
self.sessionstart = time2num(self.p.sessionstart)
|
| 192 |
+
self.sessionend = time2num(self.p.sessionend)
|
| 193 |
+
|
| 194 |
+
self._calendar = cal = self.p.calendar
|
| 195 |
+
if cal is None:
|
| 196 |
+
self._calendar = self._env._tradingcal
|
| 197 |
+
elif isinstance(cal, string_types):
|
| 198 |
+
self._calendar = PandasMarketCalendar(calendar=cal)
|
| 199 |
+
|
| 200 |
+
self._started = True
|
| 201 |
+
|
| 202 |
+
def _start(self):
|
| 203 |
+
self.start()
|
| 204 |
+
|
| 205 |
+
if not self._started:
|
| 206 |
+
self._start_finish()
|
| 207 |
+
|
| 208 |
+
def _timeoffset(self):
|
| 209 |
+
return self._tmoffset
|
| 210 |
+
|
| 211 |
+
def _getnexteos(self):
|
| 212 |
+
'''Returns the next eos using a trading calendar if available'''
|
| 213 |
+
if self._clone:
|
| 214 |
+
return self.data._getnexteos()
|
| 215 |
+
|
| 216 |
+
if not len(self):
|
| 217 |
+
return datetime.datetime.min, 0.0
|
| 218 |
+
|
| 219 |
+
dt = self.lines.datetime[0]
|
| 220 |
+
dtime = num2date(dt)
|
| 221 |
+
if self._calendar is None:
|
| 222 |
+
nexteos = datetime.datetime.combine(dtime, self.p.sessionend)
|
| 223 |
+
nextdteos = self.date2num(nexteos) # locl'ed -> utc-like
|
| 224 |
+
nexteos = num2date(nextdteos) # utc
|
| 225 |
+
while dtime > nexteos:
|
| 226 |
+
nexteos += datetime.timedelta(days=1) # already utc-like
|
| 227 |
+
|
| 228 |
+
nextdteos = date2num(nexteos) # -> utc-like
|
| 229 |
+
|
| 230 |
+
else:
|
| 231 |
+
# returns times in utc
|
| 232 |
+
_, nexteos = self._calendar.schedule(dtime, self._tz)
|
| 233 |
+
nextdteos = date2num(nexteos) # nextos is already utc
|
| 234 |
+
|
| 235 |
+
return nexteos, nextdteos
|
| 236 |
+
|
| 237 |
+
def _gettzinput(self):
|
| 238 |
+
'''Can be overriden by classes to return a timezone for input'''
|
| 239 |
+
return tzparse(self.p.tzinput)
|
| 240 |
+
|
| 241 |
+
def _gettz(self):
|
| 242 |
+
'''To be overriden by subclasses which may auto-calculate the
|
| 243 |
+
timezone'''
|
| 244 |
+
return tzparse(self.p.tz)
|
| 245 |
+
|
| 246 |
+
def date2num(self, dt):
|
| 247 |
+
if self._tz is not None:
|
| 248 |
+
return date2num(self._tz.localize(dt))
|
| 249 |
+
|
| 250 |
+
return date2num(dt)
|
| 251 |
+
|
| 252 |
+
def num2date(self, dt=None, tz=None, naive=True):
|
| 253 |
+
if dt is None:
|
| 254 |
+
return num2date(self.lines.datetime[0], tz or self._tz, naive)
|
| 255 |
+
|
| 256 |
+
return num2date(dt, tz or self._tz, naive)
|
| 257 |
+
|
| 258 |
+
def haslivedata(self):
|
| 259 |
+
return False # must be overriden for those that can
|
| 260 |
+
|
| 261 |
+
def do_qcheck(self, onoff, qlapse):
|
| 262 |
+
# if onoff is True the data will wait p.qcheck for incoming live data
|
| 263 |
+
# on its queue.
|
| 264 |
+
qwait = self.p.qcheck if onoff else 0.0
|
| 265 |
+
qwait = max(0.0, qwait - qlapse)
|
| 266 |
+
self._qcheck = qwait
|
| 267 |
+
|
| 268 |
+
def islive(self):
|
| 269 |
+
'''If this returns True, ``Cerebro`` will deactivate ``preload`` and
|
| 270 |
+
``runonce`` because a live data source must be fetched tick by tick (or
|
| 271 |
+
bar by bar)'''
|
| 272 |
+
return False
|
| 273 |
+
|
| 274 |
+
def put_notification(self, status, *args, **kwargs):
|
| 275 |
+
'''Add arguments to notification queue'''
|
| 276 |
+
if self._laststatus != status:
|
| 277 |
+
self.notifs.append((status, args, kwargs))
|
| 278 |
+
self._laststatus = status
|
| 279 |
+
|
| 280 |
+
def get_notifications(self):
|
| 281 |
+
'''Return the pending "store" notifications'''
|
| 282 |
+
# The background thread could keep on adding notifications. The None
|
| 283 |
+
# mark allows to identify which is the last notification to deliver
|
| 284 |
+
self.notifs.append(None) # put a mark
|
| 285 |
+
notifs = list()
|
| 286 |
+
while True:
|
| 287 |
+
notif = self.notifs.popleft()
|
| 288 |
+
if notif is None: # mark is reached
|
| 289 |
+
break
|
| 290 |
+
notifs.append(notif)
|
| 291 |
+
|
| 292 |
+
return notifs
|
| 293 |
+
|
| 294 |
+
def getfeed(self):
|
| 295 |
+
return self._feed
|
| 296 |
+
|
| 297 |
+
def qbuffer(self, savemem=0, replaying=False):
|
| 298 |
+
extrasize = self.resampling or replaying
|
| 299 |
+
for line in self.lines:
|
| 300 |
+
line.qbuffer(savemem=savemem, extrasize=extrasize)
|
| 301 |
+
|
| 302 |
+
def start(self):
|
| 303 |
+
self._barstack = collections.deque()
|
| 304 |
+
self._barstash = collections.deque()
|
| 305 |
+
self._laststatus = self.CONNECTED
|
| 306 |
+
|
| 307 |
+
def stop(self):
|
| 308 |
+
pass
|
| 309 |
+
|
| 310 |
+
def clone(self, **kwargs):
|
| 311 |
+
return DataClone(dataname=self, **kwargs)
|
| 312 |
+
|
| 313 |
+
def copyas(self, _dataname, **kwargs):
|
| 314 |
+
d = DataClone(dataname=self, **kwargs)
|
| 315 |
+
d._dataname = _dataname
|
| 316 |
+
d._name = _dataname
|
| 317 |
+
return d
|
| 318 |
+
|
| 319 |
+
def setenvironment(self, env):
|
| 320 |
+
'''Keep a reference to the environment'''
|
| 321 |
+
self._env = env
|
| 322 |
+
|
| 323 |
+
def getenvironment(self):
|
| 324 |
+
return self._env
|
| 325 |
+
|
| 326 |
+
def addfilter_simple(self, f, *args, **kwargs):
|
| 327 |
+
fp = SimpleFilterWrapper(self, f, *args, **kwargs)
|
| 328 |
+
self._filters.append((fp, fp.args, fp.kwargs))
|
| 329 |
+
|
| 330 |
+
def addfilter(self, p, *args, **kwargs):
|
| 331 |
+
if inspect.isclass(p):
|
| 332 |
+
pobj = p(self, *args, **kwargs)
|
| 333 |
+
self._filters.append((pobj, [], {}))
|
| 334 |
+
|
| 335 |
+
if hasattr(pobj, 'last'):
|
| 336 |
+
self._ffilters.append((pobj, [], {}))
|
| 337 |
+
|
| 338 |
+
else:
|
| 339 |
+
self._filters.append((p, args, kwargs))
|
| 340 |
+
|
| 341 |
+
def compensate(self, other):
|
| 342 |
+
'''Call it to let the broker know that actions on this asset will
|
| 343 |
+
compensate open positions in another'''
|
| 344 |
+
|
| 345 |
+
self._compensate = other
|
| 346 |
+
|
| 347 |
+
def _tick_nullify(self):
|
| 348 |
+
# These are the updating prices in case the new bar is "updated"
|
| 349 |
+
# and the length doesn't change like if a replay is happening or
|
| 350 |
+
# a real-time data feed is in use and 1 minutes bars are being
|
| 351 |
+
# constructed with 5 seconds updates
|
| 352 |
+
for lalias in self.getlinealiases():
|
| 353 |
+
if lalias != 'datetime':
|
| 354 |
+
setattr(self, 'tick_' + lalias, None)
|
| 355 |
+
|
| 356 |
+
self.tick_last = None
|
| 357 |
+
|
| 358 |
+
def _tick_fill(self, force=False):
|
| 359 |
+
# If nothing filled the tick_xxx attributes, the bar is the tick
|
| 360 |
+
alias0 = self._getlinealias(0)
|
| 361 |
+
if force or getattr(self, 'tick_' + alias0, None) is None:
|
| 362 |
+
for lalias in self.getlinealiases():
|
| 363 |
+
if lalias != 'datetime':
|
| 364 |
+
setattr(self, 'tick_' + lalias,
|
| 365 |
+
getattr(self.lines, lalias)[0])
|
| 366 |
+
|
| 367 |
+
self.tick_last = getattr(self.lines, alias0)[0]
|
| 368 |
+
|
| 369 |
+
def advance_peek(self):
|
| 370 |
+
if len(self) < self.buflen():
|
| 371 |
+
return self.lines.datetime[1] # return the future
|
| 372 |
+
|
| 373 |
+
return float('inf') # max date else
|
| 374 |
+
|
| 375 |
+
def advance(self, size=1, datamaster=None, ticks=True):
|
| 376 |
+
if ticks:
|
| 377 |
+
self._tick_nullify()
|
| 378 |
+
|
| 379 |
+
# Need intercepting this call to support datas with
|
| 380 |
+
# different lengths (timeframes)
|
| 381 |
+
self.lines.advance(size)
|
| 382 |
+
|
| 383 |
+
if datamaster is not None:
|
| 384 |
+
if len(self) > self.buflen():
|
| 385 |
+
# if no bar can be delivered, fill with an empty bar
|
| 386 |
+
self.rewind()
|
| 387 |
+
self.lines.forward()
|
| 388 |
+
return
|
| 389 |
+
|
| 390 |
+
if self.lines.datetime[0] > datamaster.lines.datetime[0]:
|
| 391 |
+
self.lines.rewind()
|
| 392 |
+
else:
|
| 393 |
+
if ticks:
|
| 394 |
+
self._tick_fill()
|
| 395 |
+
elif len(self) < self.buflen():
|
| 396 |
+
# a resampler may have advance us past the last point
|
| 397 |
+
if ticks:
|
| 398 |
+
self._tick_fill()
|
| 399 |
+
|
| 400 |
+
def next(self, datamaster=None, ticks=True):
|
| 401 |
+
|
| 402 |
+
if len(self) >= self.buflen():
|
| 403 |
+
if ticks:
|
| 404 |
+
self._tick_nullify()
|
| 405 |
+
|
| 406 |
+
# not preloaded - request next bar
|
| 407 |
+
ret = self.load()
|
| 408 |
+
if not ret:
|
| 409 |
+
# if load cannot produce bars - forward the result
|
| 410 |
+
return ret
|
| 411 |
+
|
| 412 |
+
if datamaster is None:
|
| 413 |
+
# bar is there and no master ... return load's result
|
| 414 |
+
if ticks:
|
| 415 |
+
self._tick_fill()
|
| 416 |
+
return ret
|
| 417 |
+
else:
|
| 418 |
+
self.advance(ticks=ticks)
|
| 419 |
+
|
| 420 |
+
# a bar is "loaded" or was preloaded - index has been moved to it
|
| 421 |
+
if datamaster is not None:
|
| 422 |
+
# there is a time reference to check against
|
| 423 |
+
if self.lines.datetime[0] > datamaster.lines.datetime[0]:
|
| 424 |
+
# can't deliver new bar, too early, go back
|
| 425 |
+
self.rewind()
|
| 426 |
+
return False
|
| 427 |
+
else:
|
| 428 |
+
if ticks:
|
| 429 |
+
self._tick_fill()
|
| 430 |
+
|
| 431 |
+
else:
|
| 432 |
+
if ticks:
|
| 433 |
+
self._tick_fill()
|
| 434 |
+
|
| 435 |
+
# tell the world there is a bar (either the new or the previous
|
| 436 |
+
return True
|
| 437 |
+
|
| 438 |
+
def preload(self):
|
| 439 |
+
while self.load():
|
| 440 |
+
pass
|
| 441 |
+
|
| 442 |
+
self._last()
|
| 443 |
+
self.home()
|
| 444 |
+
|
| 445 |
+
def _last(self, datamaster=None):
|
| 446 |
+
# Last chance for filters to deliver something
|
| 447 |
+
ret = 0
|
| 448 |
+
for ff, fargs, fkwargs in self._ffilters:
|
| 449 |
+
ret += ff.last(self, *fargs, **fkwargs)
|
| 450 |
+
|
| 451 |
+
doticks = False
|
| 452 |
+
if datamaster is not None and self._barstack:
|
| 453 |
+
doticks = True
|
| 454 |
+
|
| 455 |
+
while self._fromstack(forward=True):
|
| 456 |
+
# consume bar(s) produced by "last"s - adding room
|
| 457 |
+
pass
|
| 458 |
+
|
| 459 |
+
if doticks:
|
| 460 |
+
self._tick_fill()
|
| 461 |
+
|
| 462 |
+
return bool(ret)
|
| 463 |
+
|
| 464 |
+
def _check(self, forcedata=None):
|
| 465 |
+
ret = 0
|
| 466 |
+
for ff, fargs, fkwargs in self._filters:
|
| 467 |
+
if not hasattr(ff, 'check'):
|
| 468 |
+
continue
|
| 469 |
+
ff.check(self, _forcedata=forcedata, *fargs, **fkwargs)
|
| 470 |
+
|
| 471 |
+
def load(self):
|
| 472 |
+
while True:
|
| 473 |
+
# move data pointer forward for new bar
|
| 474 |
+
self.forward()
|
| 475 |
+
|
| 476 |
+
if self._fromstack(): # bar is available
|
| 477 |
+
return True
|
| 478 |
+
|
| 479 |
+
if not self._fromstack(stash=True):
|
| 480 |
+
_loadret = self._load()
|
| 481 |
+
if not _loadret: # no bar use force to make sure in exactbars
|
| 482 |
+
# the pointer is undone this covers especially (but not
|
| 483 |
+
# uniquely) the case in which the last bar has been seen
|
| 484 |
+
# and a backwards would ruin pointer accounting in the
|
| 485 |
+
# "stop" method of the strategy
|
| 486 |
+
self.backwards(force=True) # undo data pointer
|
| 487 |
+
|
| 488 |
+
# return the actual returned value which may be None to
|
| 489 |
+
# signal no bar is available, but the data feed is not
|
| 490 |
+
# done. False means game over
|
| 491 |
+
return _loadret
|
| 492 |
+
|
| 493 |
+
# Get a reference to current loaded time
|
| 494 |
+
dt = self.lines.datetime[0]
|
| 495 |
+
|
| 496 |
+
# A bar has been loaded, adapt the time
|
| 497 |
+
if self._tzinput:
|
| 498 |
+
# Input has been converted at face value but it's not UTC in
|
| 499 |
+
# the input stream
|
| 500 |
+
dtime = num2date(dt) # get it in a naive datetime
|
| 501 |
+
# localize it
|
| 502 |
+
dtime = self._tzinput.localize(dtime) # pytz compatible-ized
|
| 503 |
+
self.lines.datetime[0] = dt = date2num(dtime) # keep UTC val
|
| 504 |
+
|
| 505 |
+
# Check standard date from/to filters
|
| 506 |
+
if dt < self.fromdate:
|
| 507 |
+
# discard loaded bar and carry on
|
| 508 |
+
self.backwards()
|
| 509 |
+
continue
|
| 510 |
+
if dt > self.todate:
|
| 511 |
+
# discard loaded bar and break out
|
| 512 |
+
self.backwards(force=True)
|
| 513 |
+
break
|
| 514 |
+
|
| 515 |
+
# Pass through filters
|
| 516 |
+
retff = False
|
| 517 |
+
for ff, fargs, fkwargs in self._filters:
|
| 518 |
+
# previous filter may have put things onto the stack
|
| 519 |
+
if self._barstack:
|
| 520 |
+
for i in range(len(self._barstack)):
|
| 521 |
+
self._fromstack(forward=True)
|
| 522 |
+
retff = ff(self, *fargs, **fkwargs)
|
| 523 |
+
else:
|
| 524 |
+
retff = ff(self, *fargs, **fkwargs)
|
| 525 |
+
|
| 526 |
+
if retff: # bar removed from systemn
|
| 527 |
+
break # out of the inner loop
|
| 528 |
+
|
| 529 |
+
if retff: # bar removed from system - loop to get new bar
|
| 530 |
+
continue # in the greater loop
|
| 531 |
+
|
| 532 |
+
# Checks let the bar through ... notify it
|
| 533 |
+
return True
|
| 534 |
+
|
| 535 |
+
# Out of the loop ... no more bars or past todate
|
| 536 |
+
return False
|
| 537 |
+
|
| 538 |
+
def _load(self):
|
| 539 |
+
return False
|
| 540 |
+
|
| 541 |
+
def _add2stack(self, bar, stash=False):
|
| 542 |
+
'''Saves given bar (list of values) to the stack for later retrieval'''
|
| 543 |
+
if not stash:
|
| 544 |
+
self._barstack.append(bar)
|
| 545 |
+
else:
|
| 546 |
+
self._barstash.append(bar)
|
| 547 |
+
|
| 548 |
+
def _save2stack(self, erase=False, force=False, stash=False):
|
| 549 |
+
'''Saves current bar to the bar stack for later retrieval
|
| 550 |
+
|
| 551 |
+
Parameter ``erase`` determines removal from the data stream
|
| 552 |
+
'''
|
| 553 |
+
bar = [line[0] for line in self.itersize()]
|
| 554 |
+
if not stash:
|
| 555 |
+
self._barstack.append(bar)
|
| 556 |
+
else:
|
| 557 |
+
self._barstash.append(bar)
|
| 558 |
+
|
| 559 |
+
if erase: # remove bar if requested
|
| 560 |
+
self.backwards(force=force)
|
| 561 |
+
|
| 562 |
+
def _updatebar(self, bar, forward=False, ago=0):
|
| 563 |
+
'''Load a value from the stack onto the lines to form the new bar
|
| 564 |
+
|
| 565 |
+
Returns True if values are present, False otherwise
|
| 566 |
+
'''
|
| 567 |
+
if forward:
|
| 568 |
+
self.forward()
|
| 569 |
+
|
| 570 |
+
for line, val in zip(self.itersize(), bar):
|
| 571 |
+
line[0 + ago] = val
|
| 572 |
+
|
| 573 |
+
def _fromstack(self, forward=False, stash=False):
|
| 574 |
+
'''Load a value from the stack onto the lines to form the new bar
|
| 575 |
+
|
| 576 |
+
Returns True if values are present, False otherwise
|
| 577 |
+
'''
|
| 578 |
+
|
| 579 |
+
coll = self._barstack if not stash else self._barstash
|
| 580 |
+
|
| 581 |
+
if coll:
|
| 582 |
+
if forward:
|
| 583 |
+
self.forward()
|
| 584 |
+
|
| 585 |
+
for line, val in zip(self.itersize(), coll.popleft()):
|
| 586 |
+
line[0] = val
|
| 587 |
+
|
| 588 |
+
return True
|
| 589 |
+
|
| 590 |
+
return False
|
| 591 |
+
|
| 592 |
+
def resample(self, **kwargs):
|
| 593 |
+
self.addfilter(Resampler, **kwargs)
|
| 594 |
+
|
| 595 |
+
def replay(self, **kwargs):
|
| 596 |
+
self.addfilter(Replayer, **kwargs)
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
class DataBase(AbstractDataBase):
|
| 600 |
+
pass
|
| 601 |
+
|
| 602 |
+
|
| 603 |
+
class FeedBase(with_metaclass(metabase.MetaParams, object)):
|
| 604 |
+
params = () + DataBase.params._gettuple()
|
| 605 |
+
|
| 606 |
+
def __init__(self):
|
| 607 |
+
self.datas = list()
|
| 608 |
+
|
| 609 |
+
def start(self):
|
| 610 |
+
for data in self.datas:
|
| 611 |
+
data.start()
|
| 612 |
+
|
| 613 |
+
def stop(self):
|
| 614 |
+
for data in self.datas:
|
| 615 |
+
data.stop()
|
| 616 |
+
|
| 617 |
+
def getdata(self, dataname, name=None, **kwargs):
|
| 618 |
+
for pname, pvalue in self.p._getitems():
|
| 619 |
+
kwargs.setdefault(pname, getattr(self.p, pname))
|
| 620 |
+
|
| 621 |
+
kwargs['dataname'] = dataname
|
| 622 |
+
data = self._getdata(**kwargs)
|
| 623 |
+
|
| 624 |
+
data._name = name
|
| 625 |
+
|
| 626 |
+
self.datas.append(data)
|
| 627 |
+
return data
|
| 628 |
+
|
| 629 |
+
def _getdata(self, dataname, **kwargs):
|
| 630 |
+
for pname, pvalue in self.p._getitems():
|
| 631 |
+
kwargs.setdefault(pname, getattr(self.p, pname))
|
| 632 |
+
|
| 633 |
+
kwargs['dataname'] = dataname
|
| 634 |
+
return self.DataCls(**kwargs)
|
| 635 |
+
|
| 636 |
+
|
| 637 |
+
class MetaCSVDataBase(DataBase.__class__):
|
| 638 |
+
def dopostinit(cls, _obj, *args, **kwargs):
|
| 639 |
+
# Before going to the base class to make sure it overrides the default
|
| 640 |
+
if not _obj.p.name and not _obj._name:
|
| 641 |
+
_obj._name, _ = os.path.splitext(os.path.basename(_obj.p.dataname))
|
| 642 |
+
|
| 643 |
+
_obj, args, kwargs = \
|
| 644 |
+
super(MetaCSVDataBase, cls).dopostinit(_obj, *args, **kwargs)
|
| 645 |
+
|
| 646 |
+
return _obj, args, kwargs
|
| 647 |
+
|
| 648 |
+
|
| 649 |
+
class CSVDataBase(with_metaclass(MetaCSVDataBase, DataBase)):
|
| 650 |
+
'''
|
| 651 |
+
Base class for classes implementing CSV DataFeeds
|
| 652 |
+
|
| 653 |
+
The class takes care of opening the file, reading the lines and
|
| 654 |
+
tokenizing them.
|
| 655 |
+
|
| 656 |
+
Subclasses do only need to override:
|
| 657 |
+
|
| 658 |
+
- _loadline(tokens)
|
| 659 |
+
|
| 660 |
+
The return value of ``_loadline`` (True/False) will be the return value
|
| 661 |
+
of ``_load`` which has been overriden by this base class
|
| 662 |
+
'''
|
| 663 |
+
|
| 664 |
+
f = None
|
| 665 |
+
params = (('headers', True), ('separator', ','),)
|
| 666 |
+
|
| 667 |
+
def start(self):
|
| 668 |
+
super(CSVDataBase, self).start()
|
| 669 |
+
|
| 670 |
+
if self.f is None:
|
| 671 |
+
if hasattr(self.p.dataname, 'readline'):
|
| 672 |
+
self.f = self.p.dataname
|
| 673 |
+
else:
|
| 674 |
+
# Let an exception propagate to let the caller know
|
| 675 |
+
self.f = io.open(self.p.dataname, 'r')
|
| 676 |
+
|
| 677 |
+
if self.p.headers:
|
| 678 |
+
self.f.readline() # skip the headers
|
| 679 |
+
|
| 680 |
+
self.separator = self.p.separator
|
| 681 |
+
|
| 682 |
+
def stop(self):
|
| 683 |
+
super(CSVDataBase, self).stop()
|
| 684 |
+
if self.f is not None:
|
| 685 |
+
self.f.close()
|
| 686 |
+
self.f = None
|
| 687 |
+
|
| 688 |
+
def preload(self):
|
| 689 |
+
while self.load():
|
| 690 |
+
pass
|
| 691 |
+
|
| 692 |
+
self._last()
|
| 693 |
+
self.home()
|
| 694 |
+
|
| 695 |
+
# preloaded - no need to keep the object around - breaks multip in 3.x
|
| 696 |
+
self.f.close()
|
| 697 |
+
self.f = None
|
| 698 |
+
|
| 699 |
+
def _load(self):
|
| 700 |
+
if self.f is None:
|
| 701 |
+
return False
|
| 702 |
+
|
| 703 |
+
# Let an exception propagate to let the caller know
|
| 704 |
+
line = self.f.readline()
|
| 705 |
+
|
| 706 |
+
if not line:
|
| 707 |
+
return False
|
| 708 |
+
|
| 709 |
+
line = line.rstrip('\n')
|
| 710 |
+
linetokens = line.split(self.separator)
|
| 711 |
+
return self._loadline(linetokens)
|
| 712 |
+
|
| 713 |
+
def _getnextline(self):
|
| 714 |
+
if self.f is None:
|
| 715 |
+
return None
|
| 716 |
+
|
| 717 |
+
# Let an exception propagate to let the caller know
|
| 718 |
+
line = self.f.readline()
|
| 719 |
+
|
| 720 |
+
if not line:
|
| 721 |
+
return None
|
| 722 |
+
|
| 723 |
+
line = line.rstrip('\n')
|
| 724 |
+
linetokens = line.split(self.separator)
|
| 725 |
+
return linetokens
|
| 726 |
+
|
| 727 |
+
|
| 728 |
+
class CSVFeedBase(FeedBase):
|
| 729 |
+
params = (('basepath', ''),) + CSVDataBase.params._gettuple()
|
| 730 |
+
|
| 731 |
+
def _getdata(self, dataname, **kwargs):
|
| 732 |
+
return self.DataCls(dataname=self.p.basepath + dataname,
|
| 733 |
+
**self.p._getkwargs())
|
| 734 |
+
|
| 735 |
+
|
| 736 |
+
class DataClone(AbstractDataBase):
|
| 737 |
+
_clone = True
|
| 738 |
+
|
| 739 |
+
def __init__(self):
|
| 740 |
+
self.data = self.p.dataname
|
| 741 |
+
self._dataname = self.data._dataname
|
| 742 |
+
|
| 743 |
+
# Copy date/session parameters
|
| 744 |
+
self.p.fromdate = self.p.fromdate
|
| 745 |
+
self.p.todate = self.p.todate
|
| 746 |
+
self.p.sessionstart = self.data.p.sessionstart
|
| 747 |
+
self.p.sessionend = self.data.p.sessionend
|
| 748 |
+
|
| 749 |
+
self.p.timeframe = self.data.p.timeframe
|
| 750 |
+
self.p.compression = self.data.p.compression
|
| 751 |
+
|
| 752 |
+
def _start(self):
|
| 753 |
+
# redefine to copy data bits from guest data
|
| 754 |
+
self.start()
|
| 755 |
+
|
| 756 |
+
# Copy tz infos
|
| 757 |
+
self._tz = self.data._tz
|
| 758 |
+
self.lines.datetime._settz(self._tz)
|
| 759 |
+
|
| 760 |
+
self._calendar = self.data._calendar
|
| 761 |
+
|
| 762 |
+
# input has already been converted by guest data
|
| 763 |
+
self._tzinput = None # no need to further converr
|
| 764 |
+
|
| 765 |
+
# Copy dates/session infos
|
| 766 |
+
self.fromdate = self.data.fromdate
|
| 767 |
+
self.todate = self.data.todate
|
| 768 |
+
|
| 769 |
+
# FIXME: if removed from guest, remove here too
|
| 770 |
+
self.sessionstart = self.data.sessionstart
|
| 771 |
+
self.sessionend = self.data.sessionend
|
| 772 |
+
|
| 773 |
+
def start(self):
|
| 774 |
+
super(DataClone, self).start()
|
| 775 |
+
self._dlen = 0
|
| 776 |
+
self._preloading = False
|
| 777 |
+
|
| 778 |
+
def preload(self):
|
| 779 |
+
self._preloading = True
|
| 780 |
+
super(DataClone, self).preload()
|
| 781 |
+
self.data.home() # preloading data was pushed forward
|
| 782 |
+
self._preloading = False
|
| 783 |
+
|
| 784 |
+
def _load(self):
|
| 785 |
+
# assumption: the data is in the system
|
| 786 |
+
# simply copy the lines
|
| 787 |
+
if self._preloading:
|
| 788 |
+
# data is preloaded, we are preloading too, can move
|
| 789 |
+
# forward until have full bar or data source is exhausted
|
| 790 |
+
self.data.advance()
|
| 791 |
+
if len(self.data) > self.data.buflen():
|
| 792 |
+
return False
|
| 793 |
+
|
| 794 |
+
for line, dline in zip(self.lines, self.data.lines):
|
| 795 |
+
line[0] = dline[0]
|
| 796 |
+
|
| 797 |
+
return True
|
| 798 |
+
|
| 799 |
+
# Not preloading
|
| 800 |
+
if not (len(self.data) > self._dlen):
|
| 801 |
+
# Data not beyond last seen bar
|
| 802 |
+
return False
|
| 803 |
+
|
| 804 |
+
self._dlen += 1
|
| 805 |
+
|
| 806 |
+
for line, dline in zip(self.lines, self.data.lines):
|
| 807 |
+
line[0] = dline[0]
|
| 808 |
+
|
| 809 |
+
return True
|
| 810 |
+
|
| 811 |
+
def advance(self, size=1, datamaster=None, ticks=True):
|
| 812 |
+
self._dlen += size
|
| 813 |
+
super(DataClone, self).advance(size, datamaster, ticks=ticks)
|
backtrader/source/backtrader/feeds/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
# -*- coding: utf-8; py-indent-offset:4 -*-
|
| 3 |
+
###############################################################################
|
| 4 |
+
#
|
| 5 |
+
# Copyright (C) 2015-2023 Daniel Rodriguez
|
| 6 |
+
#
|
| 7 |
+
# This program is free software: you can redistribute it and/or modify
|
| 8 |
+
# it under the terms of the GNU General Public License as published by
|
| 9 |
+
# the Free Software Foundation, either version 3 of the License, or
|
| 10 |
+
# (at your option) any later version.
|
| 11 |
+
#
|
| 12 |
+
# This program is distributed in the hope that it will be useful,
|
| 13 |
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 14 |
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 15 |
+
# GNU General Public License for more details.
|
| 16 |
+
#
|
| 17 |
+
# You should have received a copy of the GNU General Public License
|
| 18 |
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
| 19 |
+
#
|
| 20 |
+
###############################################################################
|
| 21 |
+
from __future__ import (absolute_import, division, print_function,
|
| 22 |
+
unicode_literals)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
from .csvgeneric import *
|
| 26 |
+
from .btcsv import *
|
| 27 |
+
from .vchartcsv import *
|
| 28 |
+
from .vchart import *
|
| 29 |
+
from .yahoo import *
|
| 30 |
+
from .quandl import *
|
| 31 |
+
from .sierrachart import *
|
| 32 |
+
from .mt4csv import *
|
| 33 |
+
from .pandafeed import *
|
| 34 |
+
from .influxfeed import *
|
| 35 |
+
try:
|
| 36 |
+
from .ibdata import *
|
| 37 |
+
except ImportError:
|
| 38 |
+
pass # The user may not have ibpy installed
|
| 39 |
+
|
| 40 |
+
try:
|
| 41 |
+
from .vcdata import *
|
| 42 |
+
except ImportError:
|
| 43 |
+
pass # The user may not have something installed
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
from .oanda import OandaData
|
| 47 |
+
except ImportError:
|
| 48 |
+
pass # The user may not have something installed
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
from .vchartfile import VChartFile
|
| 52 |
+
|
| 53 |
+
from .rollover import RollOver
|
| 54 |
+
from .chainer import Chainer
|