├── .github
└── FUNDING.yml
├── .gitignore
├── .travis.yml
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── btplotting
├── __init__.py
├── analyzer_tables
│ ├── __init__.py
│ ├── annualreturn.py
│ ├── calmar.py
│ ├── drawdown.py
│ ├── leverage.py
│ ├── periodstats.py
│ ├── sharperatio.py
│ ├── sqn.py
│ ├── timereturn.py
│ ├── tradeanalyzers.py
│ ├── transactions.py
│ └── vwr.py
├── analyzers
│ ├── __init__.py
│ ├── plot.py
│ └── recorder.py
├── app.py
├── cds.py
├── clock.py
├── feeds
│ ├── __init__.py
│ └── fakefeed.py
├── figure.py
├── helper
│ ├── __init__.py
│ ├── bokeh.py
│ ├── cds_ops.py
│ ├── datatable.py
│ ├── label.py
│ ├── marker.py
│ ├── params.py
│ └── plot.py
├── live
│ ├── __init__.py
│ ├── client.py
│ └── datahandler.py
├── optbrowser.py
├── schemes
│ ├── __init__.py
│ ├── blackly.py
│ ├── scheme.py
│ └── tradimo.py
├── tab.py
├── tabs
│ ├── __init__.py
│ ├── analyzer.py
│ ├── config.py
│ ├── log.py
│ ├── metadata.py
│ └── source.py
├── templates
│ ├── basic.css.j2
│ ├── basic.html.j2
│ ├── bokeh.css.j2
│ └── js
│ │ └── tick_formatter.js
├── utils.py
├── version.py
└── webapp.py
├── demos
├── blackly_single.py
├── blackly_tabs.py
├── data_live.py
├── data_logging.py
├── data_multi_live.py
├── data_replay.py
├── datas
│ ├── 2006-day-001.txt
│ └── orcl-1995-2014.txt
├── different_line_types.py
├── multiple_datas_on_one.py
├── optimization.py
├── optimization_columns.py
├── ordered_optimization.py
├── plot-same-axis.py
├── same-axis.py
├── tabs.py
└── tradimo_single.py
├── requirements-test.txt
├── requirements.txt
├── setup.py
├── tests
├── asserts
│ ├── __init__.py
│ └── asserts.py
├── datas
│ ├── 20170319-20200319-0388.HK.csv
│ ├── NQ.csv
│ ├── nvda-1999-2014.txt
│ └── orcl-1995-2014.txt
├── strategies
│ ├── __init__.py
│ └── togglestrategy.py
├── test_backtest.py
├── test_issue10.py
├── test_issue30.py
├── test_issue36.py
├── test_issue37.py
├── test_issue44.py
├── test_lineactions.py
└── testcommon.py
└── tox.ini
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | liberapay: happydasch
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | __pycache__
2 | backtrader_plotting.egg-info
3 | /build
4 | /dist
5 | /tests/.pytest_cache
6 | /.idea
7 | /.pytest_cache
8 | /.tox
9 | /venv
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - "3.6"
4 | - "3.7"
5 | - "3.8"
6 | - "3.8-dev"
7 | # - "nightly"
8 |
9 | install: pip install tox-travis
10 |
11 | script: tox
12 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | ## Creating an Issue
3 | When creating an issue please follow this guideline:
4 | * Make sure you are using the latest version
5 | * Include the exact commit hash you are using
6 | * Provide accurate instructions of how to reproduce the problem. Please also add a minimal source code example which shows the problem.
7 |
8 | Thanks alot, reporting bugs is highly appreciated!
9 |
10 | ## Pull Requests
11 | Development should take place in the branch `develop`. The branch `master` should always point to the latest release version. Please create pull requests only against the branch `develop`.
12 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # btplotting
2 |
3 | Library to add extended plotting capabilities to `backtrader` () using bokeh.
4 |
5 | btplotting is based on the awesome `backtrader_plotting` ()
6 |
7 | `btplotting` is a complete rework of `backtrader_plotting` with the live client in focus. Besides this, a lot of
8 | issues are fixed and new functionality is added. See the list below for differences.
9 |
10 | **What is different:**
11 |
12 | Basic:
13 |
14 | * No need for custom backtrader
15 | * Different naming / structure
16 | * Data alignment which allows to generate data for different data sources.
17 | This is useful when replaying or resampling data, for example to remove gaps.
18 | * Support for replay data
19 | * Different filtering of plot objects
20 | * Every figure has its own ColumnDataSource, so the live client can patch without
21 | having issues with nan values, every figure is updated individually
22 | * Plotting looks very similar to backtraders own plotting (order, heights, etc.)
23 | * Allows to generate custom columns, which don't have to be hardcoded.
24 | * This is being used to generate color for candles, varea values, etc.
25 | * Save images of strategy or a single data (for example save an image of data when
26 | a trade is happening)
27 |
28 | Plotting:
29 |
30 | * Datas, Indicators, Observer and Volume have own aspect ratios, which can be configured in live client
31 | or scheme
32 | * Only one axis for volume will be added when using multiple data sources on one figure
33 | * Volume axis position is configureable in scheme, by default it is being plotted on the right side
34 | * Linked Crosshair across all figures
35 | * _skipnan, fill_gt, fill_lt, fill support
36 | * Plot objects can be filtered by one or more datanames or by plot group
37 | * Custom plot group, which can be configured in app or in live client by providing all
38 | plotids in a comma-separated list or by selecting the parts of the plot to display
39 |
40 | Tabs:
41 |
42 | * Default tabs can be completely removed
43 | * New log panel to also include logging information
44 | * Can be extended with custom tabs (for example order execution with live client, custom analysis, etc.)
45 |
46 | Live plotting:
47 |
48 | **(Live plotting is broken at the moment)**
49 |
50 | * Navigation in live client (Pause, Backward, Forward)
51 | * Live plotting is done using an analyzer, so there is no need to use custom backtrader
52 | * Live plotting data update works in a single thread and is done by a DataHandler
53 | * Data update is being done every n seconds, which is configureable
54 |
55 | ## Features
56 |
57 | * Interactive plots
58 | * Interactive `backtrader` optimization result browser (only supported for single-strategy runs)
59 | * Highly configurable
60 | * Different skinnable themes
61 | * Easy to use
62 |
63 | Python >= 3.6 is required.
64 |
65 | ## How to use
66 |
67 | * Add to cerebro as an analyzer:
68 |
69 | ```python
70 | from btplotting import BacktraderPlottingLive
71 | ...
72 | ...
73 |
74 | cerebro = bt.Cerebro()
75 | cerebro.addstrategy(MyStrategy)
76 | cerebro.adddata(LiveDataStream())
77 | cerebro.addanalyzer(BacktraderPlottingLive)
78 | cerebro.run()
79 | cerebro.plot()
80 | ```
81 |
82 | * If you need to change the default port or share the plotting to public:
83 |
84 | ```python
85 | cerebro.addanalyzer(BacktraderPlottingLive, address="*", port=8889)
86 | ```
87 |
88 | ## Jupyter
89 |
90 | In Jupyter you can plot to a single browser tab with iplot=False:
91 |
92 | ```python
93 | plot = btplotting.BacktraderPlotting()
94 | cerebro.plot(plot, iplot=False)
95 | ```
96 |
97 | You may encounters TypeError: `` is a built-in class error.
98 |
99 | To remove the source code tab use:
100 |
101 | ```python
102 | plot = btplotting.BacktraderPlotting()
103 | plot.tabs.remove(btplotting.tabs.SourceTab)
104 | cerebro.plot(plot, iplot=False)
105 | ```
106 |
107 | ## Demos
108 |
109 |
110 |
111 | ## Installation
112 |
113 | `pip install git+https://github.com/happydasch/btplotting`
114 |
115 | ## Sponsoring
116 |
117 | If you want to support the development of btplotting, consider to support this project.
118 |
119 | * BTC: 39BJtPgUv6UMjQvjguphN7kkjQF65rgMMF
120 | * ETH: 0x06d6f3134CD679d05AAfeA6e426f55805f9B395D
121 | *
122 |
--------------------------------------------------------------------------------
/btplotting/__init__.py:
--------------------------------------------------------------------------------
1 | import sys
2 | from .app import BacktraderPlotting # noqa: F401
3 | from .analyzers import LivePlotAnalyzer as BacktraderPlottingLive # noqa: F401
4 | from .optbrowser import OptBrowser as BacktraderPlottingOptBrowser # noqa: F401, E501
5 |
6 | # initialize analyzer tables
7 | from .analyzer_tables import inject_datatables
8 | inject_datatables()
9 |
10 | if 'ipykernel' in sys.modules:
11 | from bokeh.io import output_notebook
12 | output_notebook()
13 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/__init__.py:
--------------------------------------------------------------------------------
1 | import backtrader
2 | import logging
3 | from .drawdown import datatable as drawdown
4 | from .sharperatio import datatable as sharperatio
5 | from .tradeanalyzers import datatable as tradeanalyzer
6 | from .transactions import datatable as transactions
7 | from .calmar import datatable as calmar
8 | from .annualreturn import datatable as annualreturn
9 | from .leverage import datatable as leverage
10 | from .vwr import datatable as vwr
11 | from .timereturn import datatable as timereturn
12 | from .sqn import datatable as sqn
13 |
14 | _logger = logging.getLogger(__name__)
15 | _DATATABLE_FNC_NAME = 'get_analysis_table'
16 |
17 |
18 | def inject_datatables():
19 | '''Injects function 'get_analysis_table' to some well-known Analyzer classes.'''
20 | _atables = {
21 | backtrader.analyzers.sharpe.SharpeRatio: sharperatio,
22 | backtrader.analyzers.DrawDown: drawdown,
23 | backtrader.analyzers.TradeAnalyzer: tradeanalyzer,
24 | backtrader.analyzers.Transactions: transactions,
25 | backtrader.analyzers.Calmar: calmar,
26 | backtrader.analyzers.AnnualReturn: annualreturn,
27 | backtrader.analyzers.GrossLeverage: leverage,
28 | backtrader.analyzers.VariabilityWeightedReturn: vwr,
29 | backtrader.analyzers.TimeReturn: timereturn,
30 | backtrader.analyzers.SQN: sqn,
31 | }
32 |
33 | for cls, labdict in _atables.items():
34 | curlab = getattr(cls, _DATATABLE_FNC_NAME, None)
35 | if curlab is not None:
36 | _logger.warning(f"Analyzer class '{cls.__name__}' already contains a function 'get_rets_table'. Not overriding.")
37 | continue
38 | setattr(cls, _DATATABLE_FNC_NAME, labdict)
39 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/annualreturn.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['Period', ColummDataType.STRING], ['Return', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 |
9 | for k, v in a.items():
10 | cols1[0].append(k)
11 | cols1[1].append(v)
12 |
13 | return 'Annual Return', [cols1]
14 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/calmar.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['DateTime', ColummDataType.DATETIME], ['Value', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 |
9 | for k, v in a.items():
10 | cols1[0].append(k)
11 | cols1[1].append(v)
12 |
13 | return 'Calmar', [cols1]
14 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/drawdown.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['Feature', ColummDataType.STRING], ['Value', ColummDataType.FLOAT], ['Maximum', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 | cols1[0].append('Length')
9 | cols1[1].append(a['len'])
10 | cols1[2].append(a['max']['len'])
11 |
12 | cols1[0].append('Moneydown')
13 | cols1[1].append(a['moneydown'])
14 | cols1[2].append(a['max']['moneydown'])
15 |
16 | cols1[0].append('Drawdown')
17 | cols1[1].append(a['drawdown'])
18 | cols1[2].append(a['max']['drawdown'])
19 |
20 | return 'Drawdown', [cols1]
21 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/leverage.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['DateTime', ColummDataType.DATETIME], ['Leverage', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 |
9 | for k, v in a.items():
10 | cols1[0].append(k)
11 | cols1[1].append(v)
12 |
13 | return 'Gross Leverage', [cols1]
14 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/periodstats.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | table1 = [['Name', ColummDataType.STRING], ['Value', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 |
9 | table1[0].append('Average')
10 | table1[1].append(a.average)
11 |
12 | table1[0].append('Standard Deviation')
13 | table1[1].append(a.stddev)
14 |
15 | table1[0].append('Positive #')
16 | table1[1].append(a.positive)
17 |
18 | table1[0].append('Negative #')
19 | table1[1].append(a.negative)
20 |
21 | table1[0].append('Neutral #')
22 | table1[1].append(a.nochange)
23 |
24 | table1[0].append('Best')
25 | table1[1].append(a.best)
26 |
27 | table1[0].append('Worst')
28 | table1[1].append(a.worst)
29 |
30 | return 'Period Stats', [table1]
31 |
32 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/sharperatio.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols = [['', ColummDataType.STRING], ['Value', ColummDataType.FLOAT]]
6 | cols[0].append('Sharpe-Ratio')
7 |
8 | a = self.get_analysis()
9 | if len(a):
10 | cols[1].append(a['sharperatio'])
11 | else:
12 | cols[1].append('')
13 | return 'Sharpe-Ratio', [cols]
14 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/sqn.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['', ColummDataType.STRING], ['Value', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 | cols1[0].append('SystemQualityNumber')
9 | cols1[1].append(a['sqn'])
10 |
11 | cols1[0].append('Trades')
12 | cols1[1].append(a['trades'])
13 |
14 | return 'SystemQualityNumber', [cols1]
15 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/timereturn.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['DateTime', ColummDataType.DATETIME], ['Return', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 |
9 | for k, v in a.items():
10 | cols1[0].append(k)
11 | cols1[1].append(v)
12 |
13 | return 'Time Return', [cols1]
14 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/tradeanalyzers.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | def gdef(obj, attr, d):
6 | return obj[attr] if attr in obj else d
7 |
8 | a = self.get_analysis()
9 | tables = []
10 |
11 | tab1 = [['', ColummDataType.STRING], ['Total', ColummDataType.INT], ['Open', ColummDataType.INT], ['Closed', ColummDataType.INT]]
12 | tab1[0].append('Number of Trades')
13 | tab1[1].append(a['total']['total'])
14 | if 'open' in a['total']:
15 | tab1[2].append(a['total']['open'])
16 | else:
17 | tab1[2].append('-')
18 | tab1[3].append(gdef(a['total'], 'closed', 0))
19 | tables.append(tab1)
20 |
21 | if 'streak' in a and 'pnl' in a:
22 | tab2 = [['Streak', ColummDataType.STRING], ['Current', ColummDataType.INT], ['Longest', ColummDataType.INT]]
23 | tab2[0].append('Won')
24 | tab2[1].append(a['streak']['won']['current'])
25 | tab2[2].append(a['streak']['won']['longest'])
26 |
27 | tab2[0].append('Lost')
28 | tab2[1].append(a['streak']['lost']['current'])
29 | tab2[2].append(a['streak']['lost']['longest'])
30 |
31 | tables.append(tab2)
32 |
33 | tab3 = [['Profit & Loss', ColummDataType.STRING], ['Total', ColummDataType.FLOAT], ['Average', ColummDataType.FLOAT]]
34 | tab3[0].append('Gross Profit')
35 | tab3[1].append(a['pnl']['gross']['total'])
36 | tab3[2].append(a['pnl']['gross']['average'])
37 |
38 | tab3[0].append('Net Profit (w/ Commissions)')
39 | tab3[1].append(a['pnl']['net']['total'])
40 | tab3[2].append(a['pnl']['net']['average'])
41 |
42 | tab3[0].append('Short')
43 | tab3[1].append(a['short']['pnl']['total'])
44 | tab3[2].append(a['short']['pnl']['average'])
45 |
46 | tab3[0].append('Long')
47 | tab3[1].append(a['long']['pnl']['total'])
48 | tab3[2].append(a['long']['pnl']['average'])
49 |
50 | tab3[0].append('Won / Short')
51 | tab3[1].append(a['short']['pnl']['won']['total'])
52 | tab3[2].append(a['short']['pnl']['won']['average'])
53 |
54 | tab3[0].append('Lost / Short')
55 | tab3[1].append(a['short']['pnl']['lost']['total'])
56 | tab3[2].append(a['short']['pnl']['lost']['average'])
57 |
58 | tab3[0].append('Won / Long')
59 | tab3[1].append(a['long']['pnl']['won']['total'])
60 | tab3[2].append(a['long']['pnl']['won']['average'])
61 |
62 | tab3[0].append('Lost / Long')
63 | tab3[1].append(a['long']['pnl']['lost']['total'])
64 | tab3[2].append(a['long']['pnl']['lost']['average'])
65 |
66 | tables.append(tab3)
67 |
68 | tab4 = [['Long', ColummDataType.STRING], ['Gross', ColummDataType.FLOAT], ['Net', ColummDataType.FLOAT]]
69 | tab4[0].append('Longest')
70 | tab4[1].append(a['streak']['won']['longest'])
71 | tab4[2].append(a['streak']['lost']['longest'])
72 | tables.append(tab4)
73 |
74 | tab5 = [['Trades', ColummDataType.STRING], ['Total', ColummDataType.INT], ['Won', ColummDataType.INT], ['Lost', ColummDataType.INT]]
75 | tab5[0].append('Long')
76 | tab5[1].append(a['long']['total'])
77 | tab5[2].append(a['long']['won'])
78 | tab5[3].append(a['long']['lost'])
79 |
80 | tab5[0].append('Short')
81 | tab5[1].append(a['short']['total'])
82 | tab5[2].append(a['short']['won'])
83 | tab5[3].append(a['short']['lost'])
84 |
85 | tab5[0].append('All')
86 | tab5[1].append(a['won']['total'] + a['lost']['total'])
87 | tab5[2].append(a['won']['total'])
88 | tab5[3].append(a['lost']['total'])
89 |
90 | tables.append(tab5)
91 |
92 | tab_len = [['Trade Length', ColummDataType.STRING], ['Total', ColummDataType.INT], ['Min', ColummDataType.INT], ['Max', ColummDataType.INT], ['Average', ColummDataType.FLOAT]]
93 | tab_len[0].append('Won')
94 | tab_len[1].append(a['len']['won']['total'])
95 | if 'min' in a['len']['won']:
96 | tab_len[2].append(a['len']['won']['min'])
97 | else:
98 | tab_len[2].append(float('nan'))
99 | tab_len[3].append(a['len']['won']['max'])
100 | tab_len[4].append(a['len']['won']['average'])
101 |
102 | tab_len[0].append('Lost')
103 | tab_len[1].append(a['len']['lost']['total'])
104 | if 'min' in a['len']['lost']:
105 | tab_len[2].append(a['len']['lost']['min'])
106 | else:
107 | tab_len[2].append(float('nan'))
108 | tab_len[3].append(a['len']['lost']['max'])
109 | tab_len[4].append(a['len']['lost']['average'])
110 |
111 | tab_len[0].append('Long')
112 | tab_len[1].append(a['len']['long']['total'])
113 | tab_len[2].append(a['len']['long']['min'])
114 | tab_len[3].append(a['len']['long']['max'])
115 | tab_len[4].append(a['len']['long']['average'])
116 |
117 | tab_len[0].append('Short')
118 | tab_len[1].append(a['len']['short']['total'])
119 | tab_len[2].append(a['len']['short']['min'])
120 | tab_len[3].append(a['len']['short']['max'])
121 | tab_len[4].append(a['len']['short']['average'])
122 |
123 | tab_len[0].append('Won / Long')
124 | tab_len[1].append(a['len']['long']['won']['total'])
125 | tab_len[2].append(a['len']['long']['won']['min'])
126 | tab_len[3].append(a['len']['long']['won']['max'])
127 | tab_len[4].append(a['len']['long']['won']['average'])
128 |
129 | tab_len[0].append('Won / Short')
130 | tab_len[1].append(a['len']['short']['won']['total'])
131 | tab_len[2].append(a['len']['short']['won']['min'])
132 | tab_len[3].append(a['len']['short']['won']['max'])
133 | tab_len[4].append(a['len']['short']['won']['average'])
134 |
135 | tab_len[0].append('Lost / Long')
136 | tab_len[1].append(a['len']['long']['lost']['total'])
137 | tab_len[2].append(a['len']['long']['lost']['min'])
138 | tab_len[3].append(a['len']['long']['lost']['max'])
139 | tab_len[4].append(a['len']['long']['lost']['average'])
140 |
141 | tab_len[0].append('Lost / Short')
142 | tab_len[1].append(a['len']['short']['lost']['total'])
143 | tab_len[2].append(a['len']['short']['lost']['min'])
144 | tab_len[3].append(a['len']['short']['lost']['max'])
145 | tab_len[4].append(a['len']['short']['lost']['average'])
146 |
147 | tables.append(tab_len)
148 |
149 | return 'Transaction Analyzer', tables
150 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/transactions.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols = [['Time', ColummDataType.DATETIME],
6 | # ['Data ID', ColummDataType.INT],
7 | ['Size', ColummDataType.INT],
8 | ['Price', ColummDataType.FLOAT],
9 | ['Instrument', ColummDataType.STRING],
10 | ['Total Price', ColummDataType.FLOAT]
11 | ]
12 |
13 | # size, price, i, dname, -size * price
14 | for k, v in self.get_analysis().items():
15 | cols[0].append(k)
16 | # cols[1].append(v[0][2])
17 | cols[1].append(v[0][0])
18 | cols[2].append(v[0][1])
19 | cols[3].append(v[0][3])
20 | cols[4].append(v[0][4])
21 |
22 | return 'Transactions', [cols]
23 |
--------------------------------------------------------------------------------
/btplotting/analyzer_tables/vwr.py:
--------------------------------------------------------------------------------
1 | from ..helper.datatable import ColummDataType
2 |
3 |
4 | def datatable(self):
5 | cols1 = [['Name', ColummDataType.STRING], ['Value', ColummDataType.FLOAT]]
6 |
7 | a = self.get_analysis()
8 | cols1[0].append('VWR')
9 | cols1[1].append(a['vwr'])
10 |
11 | return 'Variability-Weighted Return', [cols1]
12 |
--------------------------------------------------------------------------------
/btplotting/analyzers/__init__.py:
--------------------------------------------------------------------------------
1 | from .plot import LivePlotAnalyzer
2 | from .recorder import RecorderAnalyzer
3 |
--------------------------------------------------------------------------------
/btplotting/analyzers/plot.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | from threading import Thread, Lock
4 |
5 | import backtrader as bt
6 |
7 | import tornado.ioloop
8 |
9 | from ..app import BacktraderPlotting
10 | from ..webapp import Webapp
11 | from ..schemes import Blackly
12 | from ..live.client import LiveClient
13 |
14 | _logger = logging.getLogger(__name__)
15 |
16 |
17 | class LivePlotAnalyzer(bt.Analyzer):
18 |
19 | params = (
20 | ('scheme', Blackly()),
21 | ('style', 'bar'),
22 | ('lookback', 23),
23 | ('address', 'localhost'),
24 | ('port', 80),
25 | ('title', None),
26 | ('interval', 0.2),
27 | ('paused_at_beginning', False),
28 | )
29 |
30 | def __init__(self, iplot=True, autostart=False, **kwargs):
31 | title = self.p.title
32 | if title is None:
33 | title = 'Live %s' % type(self.strategy).__name__
34 | self._title = title
35 | self._webapp = Webapp(
36 | self._title,
37 | 'basic.html.j2',
38 | self.p.scheme,
39 | self._app_cb_build_root_model,
40 | on_session_destroyed=self._on_session_destroyed,
41 | address=self.p.address,
42 | port=self.p.port,
43 | autostart=autostart,
44 | iplot=iplot)
45 | self._lock = Lock()
46 | self._clients = {}
47 | self._app_kwargs = kwargs
48 |
49 | def _create_app(self):
50 | return BacktraderPlotting(
51 | style=self.p.style,
52 | scheme=self.p.scheme,
53 | **self._app_kwargs)
54 |
55 | def _on_session_destroyed(self, session_context):
56 | with self._lock:
57 | self._clients[session_context.id].stop()
58 | del self._clients[session_context.id]
59 |
60 | def _t_server(self):
61 | asyncio.set_event_loop(asyncio.new_event_loop())
62 | loop = tornado.ioloop.IOLoop.current()
63 | self._webapp.start(loop)
64 |
65 | def _app_cb_build_root_model(self, doc):
66 | client = LiveClient(doc,
67 | self._create_app(),
68 | self.strategy,
69 | self.p.lookback,
70 | self.p.paused_at_beginning,
71 | self.p.interval)
72 | with self._lock:
73 | self._clients[doc.session_context.id] = client
74 | return client.model
75 |
76 | def start(self):
77 | '''
78 | Start from backtrader
79 | '''
80 | _logger.debug('Starting PlotListener...')
81 | t = Thread(target=self._t_server)
82 | t.daemon = True
83 | t.start()
84 |
85 | def stop(self):
86 | '''
87 | Stop from backtrader
88 | '''
89 | _logger.debug('Stopping PlotListener...')
90 | for c in list(self._clients.values()):
91 | c.stop()
92 |
93 | def next(self):
94 | '''
95 | Next from backtrader, new data arrives
96 | '''
97 | for c in list(self._clients.values()):
98 | c.next()
99 |
--------------------------------------------------------------------------------
/btplotting/analyzers/recorder.py:
--------------------------------------------------------------------------------
1 | import copy
2 | import logging
3 | import backtrader as bt
4 |
5 | _logger = logging.getLogger(__name__)
6 |
7 |
8 | class RecorderAnalyzer(bt.Analyzer):
9 | def __init__(self):
10 | self.nexts = []
11 |
12 | @staticmethod
13 | def print_line_snapshot(name, snapshot):
14 | line = snapshot['array']
15 | if name == 'datetime':
16 | line = [bt.num2date(x) for x in line]
17 | _logger.debug(f"Line '{name:20}' idx: {snapshot['idx']} - lencount: {snapshot['lencount']} - {list(reversed(line))}")
18 |
19 | @staticmethod
20 | def print_next(idx, next):
21 | _logger.debug(f'--- Next: {next["prenext"]} - #{idx}')
22 | __class__.print_line_snapshot('datetime', next['strategy']['datetime'])
23 |
24 | for di, data in enumerate(next['datas']):
25 | _logger.debug(f'\t--- Data {di}')
26 | for k, v in data[1].items():
27 | __class__.print_line_snapshot(k, v)
28 |
29 | for oi, obs in enumerate(next['observers']):
30 | _logger.debug(f'\t--- Obvserver {oi}')
31 | for k, v in obs[1].items():
32 | __class__.print_line_snapshot(k, v)
33 |
34 | @staticmethod
35 | def print_nexts(nexts):
36 | for i, n in enumerate(nexts):
37 | __class__.print_next(i, n)
38 |
39 | @staticmethod
40 | def _copy_lines(data):
41 | lines = {}
42 |
43 | for lineidx in range(data.lines.size()):
44 | line = data.lines[lineidx]
45 | linealias = data.lines._getlinealias(lineidx)
46 | lines[linealias] = {'idx': line.idx, 'lencount': line.lencount, 'array': copy.deepcopy(line.array)}
47 |
48 | return lines
49 |
50 | def _record_data(self, strat, is_prenext=False):
51 | curbars = []
52 | for i, d in enumerate(strat.datas):
53 | curbars.append((d._name, self._copy_lines(d)))
54 |
55 | oblines = []
56 | for obs in strat.getobservers():
57 | oblines.append((obs.__class__, self._copy_lines(obs)))
58 |
59 | self.nexts.append({'prenext': is_prenext, 'strategy': self._copy_lines(strat), 'datas': curbars, 'observers': oblines})
60 |
61 | _logger.debug(f"------------------- next")
62 | self.print_next(len(strat), self.nexts[-1])
63 | _logger.debug(f"------------------- next-end")
64 |
65 | def next(self):
66 | for s in self.strategy.env.runningstrats:
67 | minper = s._getminperstatus()
68 | if minper > 0:
69 | continue
70 | self._record_data(s)
71 |
--------------------------------------------------------------------------------
/btplotting/cds.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 |
3 | import numpy as np
4 | import pandas as pd
5 |
6 | from bokeh.models import ColumnDataSource
7 |
8 |
9 | class CDSObject:
10 | """
11 | Base class for FigurePage and Figure with ColumnDataSource support
12 | also alows to create custom columns which are not available in
13 | provided data.
14 | It will create data for stream, patch and set up the columns
15 | in ColumnDataSource
16 |
17 | It is using index and datetime columns as special cases:
18 |
19 | -index is added, so stream has also the real index of the row
20 | without it, the index would be resetted in ColumnDataSource
21 | -datetime is added only if there are any rows to prevent gaps
22 | in data, so this column should only be set in cds_cols if all
23 | values needs to be added
24 |
25 | This special cases will be available in every row
26 | """
27 |
28 | def __init__(self, cols=[]):
29 | self._cds_cols = []
30 | self._cds_cols_default = cols
31 | self._cds = ColumnDataSource()
32 | self.set_cds_col(cols)
33 |
34 | @property
35 | def cds(self):
36 | """
37 | Property for ColumnDataSource
38 | """
39 | return self._cds
40 |
41 | @property
42 | def cds_cols(self):
43 | """
44 | Property for Columns in ColumnDataSource
45 | """
46 | return self._cds_cols
47 |
48 | def _get_cds_cols(self):
49 | """
50 | Returns all set columns
51 | 2 lists will be returned:
52 | - columns: columns from data source
53 | - additional: additional data sources which should be
54 | created from data source
55 | """
56 | columns = []
57 | additional = []
58 | for c in self._cds_cols:
59 | if isinstance(c, str):
60 | columns.append(c)
61 | else:
62 | additional.append(c)
63 | return columns, additional
64 |
65 | def _create_cds_col_from_df(self, op, df):
66 | """
67 | Creates a column from DataFrame
68 | op - tuple: [0] - name of column
69 | [1] - source column
70 | [2] - other column or value
71 | [3] - op method (callable with 2 params: a, b)
72 | """
73 | a = np.array(df[op[1]])
74 | if isinstance(op[2], str):
75 | b = np.array(df[op[2]])
76 | else:
77 | b = np.full(df.shape[0], op[2])
78 | arr = op[3](a, b)
79 | return arr
80 |
81 | def _create_cds_col_from_series(self, op, series):
82 | """
83 | Creates a column from Series
84 | """
85 | arr = self._create_cds_col_from_df(op, pd.DataFrame([series]))
86 | return arr[0]
87 |
88 | def set_cds_col(self, col):
89 | """
90 | Sets ColumnDataSource columns to use
91 | allowed column types are string, tuple
92 | col can contain multiple columns in a list
93 | tuples will be used to create a new column from
94 | existing columns
95 | """
96 | if not isinstance(col, list):
97 | col = [col]
98 | for c in col:
99 | if isinstance(c, str):
100 | if c not in self._cds_cols:
101 | self._cds_cols.append(c)
102 | elif isinstance(c, tuple) and len(c) == 4:
103 | self._cds_cols.append(c)
104 | else:
105 | raise Exception("Unsupported col provided")
106 |
107 | def set_cds_columns_from_df(self, df):
108 | """
109 | Sets the ColumnDataSource columns based on the given DataFrame using
110 | the given columns. Only the given columns will be added, all will be
111 | added if columns=None
112 | """
113 | columns, additional = self._get_cds_cols()
114 | if not len(columns) > 0:
115 | columns = list(df.columns)
116 | columns = ["index", "datetime"] + [
117 | x for x in columns if x not in ["index", "datetime"]
118 | ]
119 | try:
120 | c_df = df.loc[:, columns]
121 | except Exception:
122 | return None
123 |
124 | # add additional columns
125 | for a in additional:
126 | col = self._create_cds_col_from_df(a, c_df)
127 | c_df[a[0]] = col
128 |
129 | # set cds
130 | for c in c_df.columns:
131 | if c in self._cds.column_names:
132 | self._cds.remove(c)
133 | self._cds.add(np.array(c_df[c]), c)
134 |
135 | def get_cds_streamdata_from_df(self, df):
136 | """
137 | Creates stream data from a pandas DataFrame
138 | """
139 | columns, additional = self._get_cds_cols()
140 | if not len(columns):
141 | columns = list(df.columns)
142 | columns = ["index", "datetime"] + [
143 | x for x in columns if x not in ["index", "datetime"]
144 | ]
145 | try:
146 | c_df = df.loc[:, columns]
147 | c_df.fillna("NaN")
148 | except Exception:
149 | return {}
150 |
151 | # add additional columns
152 | for a in additional:
153 | col = self._create_cds_col_from_df(a, c_df)
154 | c_df[a[0]] = col
155 |
156 | res = ColumnDataSource.from_df(c_df)
157 | del res["level_0"]
158 | return res
159 |
160 | def get_cds_patchdata_from_series(self, idx, series, fillnan=[]):
161 | """
162 | Creates patch data from a pandas Series
163 | """
164 | p_data = defaultdict(list)
165 | s_data = defaultdict(list)
166 | columns, additional = self._get_cds_cols()
167 |
168 | idx_map = {d: idx for idx, d in enumerate(self._cds.data["index"])}
169 | # get the index in cds for series index
170 | if idx in idx_map:
171 | idx = idx_map[idx]
172 | else:
173 | idx = False
174 |
175 | # create patch or stream data based on given series
176 | if idx is not False:
177 | # ensure datetime is checked for changes
178 | if "datetime" not in columns:
179 | columns.append("datetime")
180 |
181 | cds_val = None
182 | for c in columns:
183 | val = series[c]
184 | if c == "datetime":
185 | val = val.to_numpy()
186 | cds_val = self._cds.data[c][idx]
187 | if c in fillnan or cds_val != val:
188 | if val != val:
189 | val = "NaN"
190 | p_data[c].append((idx, val))
191 | for a in additional:
192 | c = a[0]
193 | val = self._create_cds_col_from_series(a, series)
194 | if c in fillnan or cds_val != val:
195 | if val != val:
196 | val = "NaN"
197 | p_data[c].append((idx, val))
198 | else:
199 | # add all columns to stream result. This may be needed if a value
200 | # was nan and therefore not added before
201 | df = pd.DataFrame([series])
202 | s_data = self.get_cds_streamdata_from_df(df)
203 | return p_data, s_data
204 |
205 | def cds_reset(self):
206 | """
207 | Resets the ColumnDataSource and other config to default
208 | """
209 | self._cds = ColumnDataSource()
210 | self._cds_cols = []
211 | self.set_cds_col(self._cds_cols_default)
212 |
--------------------------------------------------------------------------------
/btplotting/clock.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import backtrader as bt
3 |
4 | from bisect import bisect_left
5 | from datetime import timedelta
6 |
7 | from .utils import get_dataname, get_source_id
8 |
9 |
10 | class DataClockHandler:
11 | """
12 | Wraps around a data source for clock generation
13 |
14 | a clock is a index based on a data source on which
15 | other data sources with a different periods can be
16 | aligned to.
17 |
18 | If datas period is smaller than the clock period,
19 | the last data entry in the period of the clock entry
20 | will be used. All other entries will be discarded.
21 |
22 | the length of the returned data will always be the same
23 | as the length of the clock. the resulting gaps will be
24 | filled with nan or the last seen entry.
25 |
26 | the index is 0 based to len(clock) - 1
27 | """
28 |
29 | def __init__(self, strategy, dataname=False):
30 | clk, tz = self._get_clk_details(strategy, dataname)
31 | self._strategy = strategy
32 | self._dataname = dataname
33 | self._clk = clk
34 | self._tz = tz
35 | if not dataname:
36 | data = strategy.data
37 | else:
38 | data = strategy.getdatabyname(dataname)
39 | self._rightedge = data.p._get("rightedge", True)
40 |
41 | self._clk_cache = None
42 | self.last_endidx = -1
43 |
44 | def __len__(self):
45 | """
46 | Length of the clock
47 | """
48 | if not self._clk_cache:
49 | return len(self._clk)
50 |
51 | offset = 0
52 |
53 | # clk = self._get_clk()
54 | clk = self._clk_cache
55 |
56 | idx = len(clk) - 1
57 | while True:
58 | if idx < 0:
59 | break
60 | val = clk[idx - offset]
61 | if val == val:
62 | break
63 | offset += 1
64 | return (idx - offset) + 1 # last valid index + 1
65 |
66 | def init_clk(self):
67 | # for live data, self._clk might change so we cache it
68 | last_index = len(self._clk)
69 | last_one = self._clk.array[-1]
70 | if last_one != last_one:
71 | last_index -= 1
72 |
73 | # self._clk_cache = sorted(set(self._clk.array[-len(self._clk) + 1: last_index]))
74 | self._clk_cache = sorted(set(self._clk.array[:last_index]))
75 |
76 | def uinit_clk(self, last_endidx):
77 | assert self._clk_cache, "init_clk should have been called"
78 | self._clk_cache = None
79 | self.last_endidx = last_endidx
80 |
81 | # def _get_clk(self):
82 | # '''
83 | # Returns the clock to use
84 | # '''
85 | # # ensure clk has unique values also use only reported len
86 | # # of data from clock
87 | # # return sorted(set(self._clk.array[-len(self._clk)+1:]))
88 | # return self.clk_cache
89 |
90 | def _get_clk_details(self, strategy, dataname):
91 | """
92 | Returns clock details (clk, tz)
93 | """
94 | if dataname is not False:
95 | data = strategy.getdatabyname(dataname)
96 | return data.datetime, data._tz or strategy.data._tz
97 | # if no dataname provided, use first data
98 | return strategy.datetime, strategy.data._tz
99 |
100 | def _align_slice(self, slicedata, startidx=None, endidx=None, rightedge=True):
101 | """
102 | Aligns a slice to the clock
103 | """
104 | res = []
105 |
106 | # loop through timestamps of curent clock as float values
107 | dtlist = self.get_dt_list(startidx, endidx, asfloat=True)
108 | # initialize last index used in slicedata
109 | l_idx = -1 if not len(slicedata["float"]) else 0
110 | maxidx = min(len(slicedata["float"]), len(slicedata["value"])) - 1
111 | for i in range(0, len(dtlist)):
112 | # set initial value for this candle
113 | t_val = float("nan") # target candle value
114 | if rightedge:
115 | t_end = dtlist[i]
116 | t_start = t_end
117 | if len(dtlist) > 1:
118 | if i == 0:
119 | t_start = t_end - (dtlist[1] - dtlist[0])
120 | else:
121 | t_start = dtlist[i - 1]
122 | else:
123 | t_start = dtlist[i]
124 | t_end = t_start
125 | if i < len(dtlist) - 1:
126 | t_end = dtlist[i + 1]
127 | elif len(dtlist) > 1:
128 | t_end = t_start + dtlist[1] - dtlist[0]
129 | # align slicedata to target clock
130 | while True:
131 | # there is no data to align, just set nan values
132 | if l_idx < 0:
133 | res.append(t_val)
134 | break
135 | # all candles from data consumed
136 | if l_idx > maxidx:
137 | break
138 | # get duration of current candle
139 | # current values from data
140 | c_val = slicedata["value"][l_idx]
141 | if rightedge:
142 | c_end = slicedata["float"][l_idx]
143 | c_start = None
144 | if maxidx > 1:
145 | if l_idx == 0:
146 | c_start = c_end - (
147 | slicedata["float"][1] - slicedata["float"][0]
148 | )
149 | else:
150 | c_start = slicedata["float"][l_idx - 1]
151 | else:
152 | c_start = slicedata["float"][l_idx]
153 | c_end = None
154 | if l_idx < maxidx - 1:
155 | c_end = slicedata["float"][l_idx + 1]
156 | elif maxidx > 1:
157 | c_end = c_start + (
158 | slicedata["float"][1] - slicedata["float"][0]
159 | )
160 | # check if value belongs to next candle, if current value
161 | # belongs to next target candle don't use this value and
162 | # stop here and use previously set value
163 | if c_start and c_start >= t_end:
164 | break
165 | # forward until start of target start is readched
166 | # move forward in source data and remember the last value
167 | # of the candle, also don't process further if last candle
168 | # and after start of target
169 | if c_end and c_end <= t_start:
170 | l_idx += 1
171 | continue
172 | # set target value
173 | if c_val == c_val:
174 | t_val = c_val
175 | # increment index in slice data if current candle consumed
176 | l_idx += 1
177 | # append the set value to aligned list with values
178 | res.append(t_val)
179 | return res
180 |
181 | def get_idx_for_dt(self, dt):
182 | clk = self._clk_cache
183 | assert clk, "wrong"
184 | return bisect_left(clk, bt.date2num(dt, tz=self._tz))
185 |
186 | def get_start_end_idx(self, startdt=None, enddt=None, back=None, obj_clk=None):
187 | """
188 | Returns the startidx and endidx for a given datetime
189 | """
190 | clk = obj_clk or self._clk_cache
191 | assert clk, "wrong"
192 | startidx = (
193 | 0
194 | if startdt is None
195 | else bisect_left(clk, bt.date2num(startdt, tz=self._tz))
196 | )
197 | if startidx is not None and startidx >= len(clk):
198 | startidx = 0
199 | endidx = (
200 | len(clk) - 1
201 | if enddt is None
202 | else bisect_left(clk, bt.date2num(enddt, tz=self._tz))
203 | )
204 | if endidx is not None and endidx >= len(clk):
205 | endidx = len(clk) - 1
206 | if back:
207 | if endidx is None:
208 | endidx = len(clk) - 1
209 | startidx = max(0, endidx - back + 1)
210 | return startidx, endidx
211 |
212 | def get_dt_at_idx(self, idx, localized=True):
213 | """
214 | Returns a datetime object for given index
215 | """
216 | clk = self._clk_cache
217 | assert clk, "wrong"
218 | return bt.num2date(clk[idx], tz=None if not localized else self._tz)
219 |
220 | def get_idx_list(self, startidx=None, endidx=None, preserveidx=True):
221 | """
222 | Returns a list with int indexes for the clock
223 | """
224 | clk = self._clk_cache
225 | assert clk, "wrong"
226 | if startidx is not None:
227 | startidx = max(0, startidx)
228 | if endidx is not None:
229 | endidx = min(len(clk), endidx)
230 | if preserveidx:
231 | assert endidx is not None, "wrong"
232 | return [int(x) for x in range(startidx, endidx + 1)]
233 | return [int(x) for x in range(endidx - startidx + 1)]
234 |
235 | def get_dt_list(self, startidx=None, endidx=None, asfloat=False, localized=True):
236 | """
237 | Returns a list with datetime/float indexes for the clock
238 | """
239 | clk = self._clk_cache
240 | assert clk, "wrong"
241 | dtlist = []
242 | for i in self.get_idx_list(startidx, endidx):
243 | val = clk[i]
244 | if not asfloat:
245 | val = bt.num2date(val, tz=None if not localized else self._tz)
246 | dtlist.append(val)
247 | return dtlist
248 |
249 | def get_slice(self, line, startdt=None, enddt=None, obj_clk=None):
250 | """
251 | Returns a slice from given line
252 |
253 | This method is used to slice something from another clock for later
254 | alignment in another clock.
255 | """
256 | clk = self._clk_cache
257 | assert clk, "wrong"
258 | res = {"float": [], "value": []}
259 | startidx, endidx = self.get_start_end_idx(startdt, enddt)
260 | for i in self.get_idx_list(startidx, endidx):
261 | res["float"].append(clk[i])
262 | if obj_clk is None:
263 | if i < len(line.array):
264 | res["value"].append(line.array[i])
265 | else:
266 | res["value"].append(float("nan"))
267 | else:
268 | idx = self.get_idx(obj_clk.array, clk[i])
269 |
270 | if obj_clk.array[idx] == clk[i]:
271 | res["value"].append(line.array[idx])
272 | else:
273 | res["value"].append(float("nan"))
274 | return res
275 |
276 | def get_idx(self, obj_clk, clk_value):
277 | idx = bisect_left(obj_clk, clk_value)
278 | return idx
279 |
280 | def get_data(
281 | self, obj, startidx=None, endidx=None, fillnan=[], skipnan=[], obj_clk=None
282 | ):
283 | """
284 | Returns data from object aligned to clock
285 | """
286 | clk = self._clk_cache
287 | assert clk, "wrong"
288 |
289 | slice_startdt = self.get_dt_at_idx(startidx)
290 | slice_enddt = self.get_dt_at_idx(endidx)
291 |
292 | # dataname = get_dataname(obj)
293 | # tmpclk = DataClockHandler(self._strategy, dataname)
294 | df = pd.DataFrame()
295 | source_id = get_source_id(obj)
296 | for lineidx, line in enumerate(obj.lines):
297 | alias = obj._getlinealias(lineidx)
298 | if isinstance(obj, bt.AbstractDataBase):
299 | if alias == "datetime":
300 | continue
301 | name = source_id + alias
302 | else:
303 | name = get_source_id(line)
304 |
305 | slicedata = self.get_slice(line, slice_startdt, slice_enddt, obj_clk)
306 |
307 | data = self._align_slice(
308 | slicedata, startidx, endidx, rightedge=self._rightedge
309 | )
310 | df[name] = data
311 | # make sure all data is filled correctly,
312 | # either skip if skipnan
313 | # or forward fill if not fillnan
314 | if name in skipnan:
315 | pass
316 | elif name not in fillnan:
317 | df[name] = df[name].ffill()
318 | return df
319 |
--------------------------------------------------------------------------------
/btplotting/feeds/__init__.py:
--------------------------------------------------------------------------------
1 | from .fakefeed import FakeFeed
2 |
--------------------------------------------------------------------------------
/btplotting/feeds/fakefeed.py:
--------------------------------------------------------------------------------
1 | import math
2 | import datetime
3 | import logging
4 |
5 | import backtrader as bt
6 | from enum import Enum
7 |
8 | _logger = logging.getLogger(__name__)
9 |
10 |
11 | class FakeFeed(bt.DataBase):
12 | class State(Enum):
13 | BACKTEST = 0,
14 | BACKFILL = 1,
15 | LIVE = 2,
16 |
17 | params = (
18 | ('starting_value', 200),
19 | ('tick_interval', datetime.timedelta(seconds=25)),
20 | ('start_delay', 0),
21 | ('run_duration', datetime.timedelta(seconds=30)), # only used when not backtest mode
22 | ('num_gen_bars', 10), # number of bars to generate in backtest or backfill mode
23 | ('live', True),
24 | )
25 |
26 | def __init__(self):
27 | super(FakeFeed, self).__init__()
28 |
29 | self._last_delivered = None
30 |
31 | self._cur_value = None
32 | self._current_comp = 0
33 | self._num_bars_delivered = 0
34 | self._compression_in_effect = None
35 | self._tmoffset = datetime.timedelta(seconds=-0.5) # configure offset cause we are sending slightly delayed ticked data (of course!)
36 | self._start_ts = None # time of the first call to _load to obey start_delay
37 |
38 | def start(self):
39 | super(FakeFeed, self).start()
40 |
41 | self._start_ts = datetime.datetime.now()
42 | self._cur_value = self.p.starting_value
43 |
44 | def islive(self):
45 | return self.p.live
46 |
47 | def _update_line(self, dt, value):
48 | _logger.debug(f"{self._name} - Updating line - Bar Time: {dt} - Value: {value}")
49 |
50 | self.lines.datetime[0] = bt.date2num(dt)
51 | self.lines.volume[0] = 0.0
52 | self.lines.openinterest[0] = 0.0
53 |
54 | # Put the prices into the bar
55 | if math.isnan(self.lines.open[0]):
56 | self.lines.open[0] = value
57 | if math.isnan(self.lines.high[0]) or value > self.lines.high[0]:
58 | self.lines.high[0] = value
59 | if math.isnan(self.lines.low[0]) or value < self.lines.low[0]:
60 | self.lines.low[0] = value
61 | self.lines.close[0] = value
62 | self.lines.volume[0] = 0.0
63 | self.lines.openinterest[0] = 0.0
64 |
65 | def _update_bar(self, dt, vopen, vlow, vhigh, vclose):
66 | _logger.debug(f"{self._name} - Updating bar - Bar Time: {dt} - Value: {vclose}")
67 |
68 | self.lines.datetime[0] = bt.date2num(dt)
69 | self.lines.volume[0] = 0.0
70 | self.lines.openinterest[0] = 0.0
71 |
72 | # Put the prices into the bar
73 | self.lines.open[0] = vopen
74 | self.lines.high[0] = vhigh
75 | self.lines.low[0] = vlow
76 | self.lines.close[0] = vclose
77 | self.lines.volume[0] = 0.0
78 | self.lines.openinterest[0] = 0.0
79 |
80 | def _load(self):
81 | now = datetime.datetime.now()
82 | if now - self._start_ts < datetime.timedelta(seconds=self.p.start_delay):
83 | return None
84 |
85 | bars_done = self._num_bars_delivered >= self.p.num_gen_bars
86 |
87 | if self.p.live:
88 | if now - self._start_ts > self.p.run_duration:
89 | return False
90 | else:
91 | if bars_done:
92 | return False
93 |
94 | if self.p.live:
95 | if bars_done:
96 | return self._load_live(now)
97 | else:
98 | return self._load_bar(now, True)
99 | else:
100 | return self._load_bar(now)
101 |
102 | def _load_bar(self, now, backfill=False):
103 | tf, comp = (self.p.timeframe, self.p.compression) if not backfill else (self._timeframe, self._compression)
104 | if tf == bt.TimeFrame.Ticks:
105 | delta = self.p.tick_interval * comp
106 | elif tf == bt.TimeFrame.Seconds:
107 | delta = datetime.timedelta(seconds=comp)
108 | elif tf == bt.TimeFrame.Minutes:
109 | delta = datetime.timedelta(minutes=comp)
110 | elif tf == bt.TimeFrame.Days:
111 | delta = datetime.timedelta(days=comp)
112 | else:
113 | raise RuntimeError(f"{self._name} - Unsupported timeframe: {self.p.timeframe}")
114 |
115 | if self._last_delivered is None:
116 | if backfill:
117 | self._last_delivered = self._time_floored(now - delta * self.p.num_gen_bars, tf, comp) # go back one bar too far since we add one instantly
118 | else:
119 | self._last_delivered = self._time_floored(now, tf)
120 |
121 | self._last_delivered += delta
122 |
123 | _logger.debug(f"{self._name} - Loading bar: {self._last_delivered}")
124 |
125 | if backfill:
126 | self._update_bar(self._last_delivered, self._cur_value, self._cur_value, self._cur_value + comp, self._cur_value + comp)
127 | self._cur_value += comp
128 | else:
129 | self._update_line(self._last_delivered, self._cur_value)
130 | self._cur_value += 1
131 |
132 | self._num_bars_delivered += 1
133 | return True
134 |
135 | @staticmethod
136 | def _time_floored(now, timeframe, comp=1):
137 | t = now
138 | if timeframe in [bt.TimeFrame.Seconds, bt.TimeFrame.Ticks]:
139 | t -= datetime.timedelta(seconds=t.second % comp,
140 | microseconds=t.microsecond)
141 | elif timeframe == bt.TimeFrame.Minutes:
142 | t -= datetime.timedelta(minutes=t.minute % comp,
143 | seconds=t.second,
144 | microseconds=t.microsecond)
145 | elif timeframe == bt.TimeFrame.Days:
146 | if comp != 1:
147 | raise Exception('For timeframe days only compression of 1 is supported.')
148 | t -= datetime.timedelta(hours=t.hour,
149 | minutes=t.minute,
150 | seconds=t.second,
151 | microseconds=t.microsecond)
152 | else:
153 | raise Exception(f'TimeFrame {timeframe} not supported')
154 | return t
155 |
156 | def _load_live(self, now):
157 | tf = self.p.timeframe
158 |
159 | comp = self.p.compression
160 |
161 | if self._last_delivered is None:
162 | # first run, fill last_delivered
163 | self._last_delivered = self._time_floored(now, tf)
164 |
165 | if tf == bt.TimeFrame.Ticks:
166 | if now - self._last_delivered < self.p.tick_interval:
167 | return None
168 | _logger.debug(f"{self._name} - Delivering - now: {now} - lastDel: {self._last_delivered}")
169 | self._last_delivered += self.p.tick_interval
170 | else:
171 | if tf == bt.TimeFrame.Minutes:
172 | if now.minute == self._last_delivered.minute:
173 | return None
174 | self._last_delivered += datetime.timedelta(minutes=1)
175 | elif tf == bt.TimeFrame.Days:
176 | if now.day == self._last_delivered.day:
177 | return None
178 | self._last_delivered += datetime.timedelta(days=1)
179 |
180 | self._current_comp += 1
181 |
182 | if self._current_comp == comp: # do not use self._compression as it is modified by resampler already
183 | self._current_comp = 0
184 |
185 | self._update_line(self._last_delivered, self._cur_value)
186 | self._cur_value += 1
187 | _logger.debug(f"{self._name} - Tick delivered: {self._last_delivered}")
188 | return True
189 | else:
190 | return None
191 |
192 |
--------------------------------------------------------------------------------
/btplotting/helper/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/happydasch/btplotting/727e8c688d3f2d8fca6b1a431499a835f63e3d73/btplotting/helper/__init__.py
--------------------------------------------------------------------------------
/btplotting/helper/bokeh.py:
--------------------------------------------------------------------------------
1 | from jinja2 import Environment, PackageLoader
2 |
3 |
4 | def generate_stylesheet(scheme, template='basic.css.j2'):
5 | '''
6 | Generates stylesheet with values from scheme
7 | '''
8 | env = Environment(loader=PackageLoader('btplotting', 'templates'))
9 | templ = env.get_template(template)
10 |
11 | css = templ.render(scheme.__dict__)
12 | return css
13 |
--------------------------------------------------------------------------------
/btplotting/helper/cds_ops.py:
--------------------------------------------------------------------------------
1 | import numpy as np
2 |
3 |
4 | def cds_op_gt(a, b):
5 | '''
6 | Operator for gt
7 | will create a new column with values
8 | from b if a > b else a
9 | '''
10 | res = np.where(a > b, b, a)
11 | return res
12 |
13 |
14 | def cds_op_lt(a, b):
15 | '''
16 | Operator for lt
17 | will create a new column with values
18 | from b if a < b else a
19 | '''
20 | res = np.where(a < b, b, a)
21 | return res
22 |
23 |
24 | def cds_op_non(a, b):
25 | '''
26 | Operator for non
27 | will return b as new column
28 | '''
29 | return b
30 |
31 |
32 | def cds_op_color(a, b, color_up, color_down):
33 | '''
34 | Operator for color generation
35 | will return a column with colors
36 | To provide color values, use functools.partial.
37 | Example:
38 | partial(cds_op_color, color_up=color_up, color_down=color_down)
39 | '''
40 | c_up = np.full(len(a), color_up)
41 | c_down = np.full(len(a), color_down)
42 | res = np.where(b >= a, c_up, c_down)
43 | return res
44 |
--------------------------------------------------------------------------------
/btplotting/helper/datatable.py:
--------------------------------------------------------------------------------
1 | from collections import OrderedDict
2 | from enum import Enum
3 |
4 | import backtrader as bt
5 |
6 | from bokeh.models import ColumnDataSource, Div, TableColumn, DataTable, \
7 | DateFormatter, NumberFormatter, StringFormatter
8 |
9 | from .params import get_params_str
10 |
11 |
12 | # the height of a single row
13 | ROW_HEIGHT = 25
14 |
15 |
16 | class ColummDataType(Enum):
17 | DATETIME = 1
18 | FLOAT = 2
19 | INT = 3
20 | PERCENTAGE = 4
21 | STRING = 5
22 |
23 |
24 | class TableGenerator:
25 |
26 | '''
27 | Table generator for key -> value tuples
28 | '''
29 |
30 | def __init__(self, stylesheet):
31 | self._stylesheet = stylesheet
32 |
33 | def get_table(self, data):
34 | table = [['Name'], ['Value']]
35 | cds = ColumnDataSource()
36 | columns = []
37 | for n, v in data.items():
38 | table[0].append(n)
39 | table[1].append(v)
40 | for i, c in enumerate(table):
41 | col_name = f'col{i}'
42 | cds.add(c[1:], col_name)
43 | columns.append(TableColumn(
44 | field=col_name,
45 | title=c[0]))
46 | column_height = len(table[0]) * ROW_HEIGHT
47 | dtable = DataTable(
48 | source=cds,
49 | columns=columns,
50 | index_position=None,
51 | height=column_height,
52 | width=0, # set width to 0 so there is no min_width
53 | sizing_mode='stretch_width',
54 | fit_columns=True,
55 | stylesheets=[self._stylesheet])
56 | return dtable
57 |
58 |
59 | class AnalysisTableGenerator:
60 |
61 | '''
62 | Table generator for analyzers
63 | '''
64 |
65 | def __init__(self, scheme, stylesheet):
66 | self._scheme = scheme
67 | self._stylesheet = stylesheet
68 |
69 | @staticmethod
70 | def _get_table_generic(analyzer):
71 | '''
72 | Returns two columns labeled '' and 'Value'
73 | '''
74 | table = [
75 | ['', ColummDataType.STRING],
76 | ['Value', ColummDataType.STRING]]
77 |
78 | def add_to_table(item, baselabel=''):
79 | if isinstance(item, dict):
80 | for ak, av in item.items():
81 | label = f'{baselabel} - {ak}' if len(baselabel) > 0 else ak
82 | if isinstance(av, (dict, bt.AutoOrderedDict, OrderedDict)):
83 | add_to_table(av, label)
84 | else:
85 | table[0].append(label)
86 | table[1].append(av)
87 |
88 | add_to_table(analyzer.get_analysis())
89 | return analyzer.__class__.__name__, [table]
90 |
91 | def _get_formatter(self, ctype):
92 | if ctype.name == ColummDataType.FLOAT.name:
93 | return NumberFormatter(format=self._scheme.number_format)
94 | elif ctype.name == ColummDataType.INT.name:
95 | return NumberFormatter()
96 | elif ctype.name == ColummDataType.DATETIME.name:
97 | return DateFormatter(format='%c')
98 | elif ctype.name == ColummDataType.STRING.name:
99 | return StringFormatter()
100 | elif ctype.name == ColummDataType.PERCENTAGE.name:
101 | return NumberFormatter(format='0.000 %')
102 | else:
103 | raise Exception(f'Unsupported ColumnDataType: "{ctype}"')
104 |
105 | def get_tables(self, analyzer):
106 | '''
107 | Return a header for this analyzer and one *or more* data tables.
108 | '''
109 | if hasattr(analyzer, 'get_analysis_table'):
110 | title, table_columns_list = analyzer.get_analysis_table()
111 | else:
112 | # Analyzer does not provide a table function. Use our generic one
113 | title, table_columns_list = __class__._get_table_generic(
114 | analyzer)
115 |
116 | # don't add empty analyzer
117 | if len(table_columns_list[0][0]) == 2:
118 | return None, None
119 |
120 | param_str = get_params_str(analyzer.params)
121 | if len(param_str) > 0:
122 | title += f' ({param_str})'
123 |
124 | elems = []
125 | for table_columns in table_columns_list:
126 | cds = ColumnDataSource()
127 | columns = []
128 | for i, c in enumerate(table_columns):
129 | col_name = f'col{i}'
130 | cds.add(c[2:], col_name)
131 | columns.append(TableColumn(
132 | field=col_name,
133 | title=c[0],
134 | formatter=self._get_formatter(c[1])))
135 | # define height of column by multiplying count of rows
136 | # with ROW_HEIGHT
137 | column_height = len(table_columns[0]) * ROW_HEIGHT
138 | elems.append(DataTable(
139 | source=cds,
140 | columns=columns,
141 | index_position=None,
142 | height=column_height,
143 | width=0, # set width to 0 so there is no min_width
144 | sizing_mode='stretch_width',
145 | fit_columns=True,
146 | stylesheets=[self._stylesheet]))
147 |
148 | table_title = Div(
149 | text=title,
150 | css_classes=['table-title'],
151 | stylesheets=[self._stylesheet])
152 | return table_title, elems
153 |
--------------------------------------------------------------------------------
/btplotting/helper/label.py:
--------------------------------------------------------------------------------
1 | import backtrader as bt
2 |
3 | from .params import get_params_str
4 | from ..utils import get_clock_obj, get_dataname
5 |
6 |
7 | def obj2label(obj, fullid=False):
8 | if isinstance(obj, bt.Strategy):
9 | return strategy2label(obj, fullid)
10 | elif isinstance(obj, bt.IndicatorBase):
11 | return indicator2label(obj, fullid)
12 | elif isinstance(obj, bt.AbstractDataBase):
13 | return data2label(obj, fullid)
14 | elif isinstance(obj, bt.ObserverBase):
15 | return observer2label(obj, fullid)
16 | elif isinstance(obj, bt.Analyzer):
17 | return obj.__class__.__name__
18 | elif isinstance(obj, bt.MultiCoupler):
19 | return obj2label(obj.datas[0], fullid)
20 | elif isinstance(obj, (bt.LinesOperation,
21 | bt.LineSingle,
22 | bt.LineSeriesStub)):
23 | return obj.__class__.__name__
24 | else:
25 | raise RuntimeError(f'Unsupported type: {obj.__class__}')
26 |
27 |
28 | def strategy2label(strategy, params=False):
29 | label = strategy.__class__.__name__
30 | if params:
31 | param_labels = get_params_str(strategy.params)
32 | if len(param_labels) > 0:
33 | label += f' [{param_labels}]'
34 | return label
35 |
36 |
37 | def data2label(data, fullid=False):
38 | if fullid:
39 | return f'{get_dataname(data)}-{data.__class__.__name__}'
40 | else:
41 | return get_dataname(data)
42 |
43 |
44 | def observer2label(obs, fullid=False):
45 | if fullid:
46 | return obs.plotlabel()
47 | else:
48 | return obs.plotinfo.plotname or obs.__class__.__name__
49 |
50 |
51 | def indicator2label(ind, fullid=False):
52 | if fullid:
53 | return ind.plotlabel()
54 | else:
55 | return ind.plotinfo.plotname or ind.__class__.__name__
56 |
57 |
58 | def obj2data(obj):
59 | '''
60 | Returns a string listing all involved data feeds. Empty string if
61 | there is only a single feed in the mix
62 | '''
63 | if isinstance(obj, bt.LineActions):
64 | return 'Line Action'
65 | elif isinstance(obj, bt.AbstractDataBase):
66 | return obj2label(obj)
67 | elif isinstance(obj, bt.IndicatorBase):
68 | names = []
69 | for x in obj.datas:
70 | if isinstance(x, bt.AbstractDataBase):
71 | return obj2label(x)
72 | elif isinstance(x, bt.IndicatorBase):
73 | names.append(indicator2label(x, False))
74 | elif isinstance(x, bt.LineSeriesStub):
75 | # indicator target is one specific line of a datafeed
76 | # add [L] at the end
77 | return obj2label(get_clock_obj(x)) + ' [L]'
78 | if len(names) > 0:
79 | return ",".join(names)
80 | else:
81 | raise RuntimeError(f'Unsupported type: {obj.__class__}')
82 | return ''
83 |
--------------------------------------------------------------------------------
/btplotting/helper/marker.py:
--------------------------------------------------------------------------------
1 | '''
2 | Marker definition used to generate markers in bokeh using matplotlib notation
3 | '''
4 |
5 | _mrk_fncs = {
6 | # '.' m00 point
7 | '.': ('dot', ['color'], {'size': 8}, {}),
8 | # ',' m01 pixel
9 | ',': ('dot', ['color'], {'size': 8}, {}),
10 | # 'o' m02 circle
11 | 'o': ('circle', ['color', 'size'], {}, {}),
12 | # 'v' m03 triangle_down
13 | 'v': ('triangle', ['color', 'size'],
14 | {'angle': 180, 'angle_units': 'deg'}, {}),
15 | # '^' m04 triangle_up
16 | '^': ('triangle', ['color', 'size'], {}, {}),
17 | # '<' m05 triangle_left
18 | '<': ('triangle', ['color', 'size'],
19 | {'angle': -90, 'angle_units': 'deg'}, {}),
20 | # '>' m06 triangle_right
21 | '>': ('triangle', ['color', 'size'],
22 | {'angle': 90, 'angle_units': 'deg'}, {}),
23 | # '1' m07 tri_down
24 | '1': ('y', ['color', 'size'], {}, {}),
25 | # '2' m08 tri_up
26 | '2': ('y', ['color', 'size'],
27 | {'angle': 180, 'angle_units': 'deg'}, {}),
28 | # '3' m09 tri_left
29 | '3': ('y', ['color', 'size'],
30 | {'angle': -90, 'angle_units': 'deg'}, {}),
31 | # '4' m10 tri_right
32 | '4': ('y', ['color', 'size'],
33 | {'angle': 90, 'angle_units': 'deg'}, {}),
34 | # '8' m11 octagon
35 | '8': ('star', ['color', 'size'], {}, {}),
36 | # 's' m12 square
37 | 's': ('square', ['color', 'size'], {}, {}),
38 | # 'p' m13 pentagon
39 | 'p': ('star_dot', ['color', 'size'], {}, {}),
40 | # 'P' m23 plus(filled)
41 | 'P': ('plus', ['color', 'size'], {}, {'size': -3}),
42 | # '*' m14 star
43 | '*': ('asterisk', ['color', 'size'], {}, {}),
44 | # 'h' m15 hexagon1
45 | 'h': ('hex', ['color', 'size'], {}, {}),
46 | # 'H' m16 hexagon2
47 | 'H': ('hex', ['color', 'size'],
48 | {'angle': 45, 'angle_units': 'deg'}, {}),
49 | # '+' m17 plus
50 | '+': ('plus', ['color', 'size'], {}, {}),
51 | # 'x' m18 x
52 | 'x': ('x', ['color', 'size'], {}, {}),
53 | # 'X' m24 x(filled)
54 | 'X': ('x', ['color', 'size'], {}, {'size': -3}),
55 | # 'D' m19 diamond
56 | 'D': ('diamond_cross', ['color', 'size'], {}, {}),
57 | # 'd' m20 thin_diamond
58 | 'd': ('diamond', ['color', 'size'], {}, {}),
59 | # '|' m21 vline
60 | '|': ('vbar', ['color'], {}, {}),
61 | # '_' m22 hline
62 | '_': ('hbar', ['color'], {}, {}),
63 | # 0 (TICKLEFT) m25 tickleft
64 | 0: ('triangle', ['color', 'size'],
65 | {'angle': -90, 'angle_units': 'deg'}, {'size': -3}),
66 | # 1 (TICKRIGHT) m26 tickright
67 | 1: ('triangle', ['color', 'size'],
68 | {'angle': 90, 'angle_units': 'deg'}, {'size': -3}),
69 | # 2 (TICKUP) m27 tickup
70 | 2: ('triangle', ['color', 'size'], {}, {'size': -3}),
71 | # 3 (TICKDOWN) m28 tickdown
72 | 3: ('triangle', ['color', 'size'],
73 | {'angle': 180, 'angle_units': 'deg'}, {'size': -3}),
74 | # 4 (CARETLEFT) m29 caretleft
75 | 4: ('triangle', ['fill_color', 'color', 'size'],
76 | {'angle': -90, 'angle_units': 'deg'}, {}),
77 | # 5 (CARETRIGHT) m30 caretright
78 | 5: ('triangle', ['fill_color', 'color', 'size'],
79 | {'angle': 90, 'angle_units': 'deg'}, {}),
80 | # 6 (CARETUP) m31 caretup
81 | 6: ('triangle', ['fill_color', 'color', 'size'], {}, {}),
82 | # 7 (CARETDOWN) m32 caretdown
83 | 7: ('triangle', ['fill_color', 'color', 'size'],
84 | {'angle': 180, 'angle_units': 'deg'}, {}),
85 | # 8 (CARETLEFTBASE) m33 caretleft(centered at base)
86 | 8: ('triangle', ['fill_color', 'color', 'size'],
87 | {'angle': -90, 'angle_units': 'deg'}, {'x': 0.25}),
88 | # 9 (CARETRIGHTBASE) m34 caretright(centered at base)
89 | 9: ('triangle', ['fill_color', 'color', 'size'],
90 | {'angle': 90, 'angle_units': 'deg'}, {'x': -0.25}),
91 | # 10 (CARETUPBASE) m35 caretup(centered at base)
92 | 10: ('triangle', ['fill_color', 'color', 'size'],
93 | {}, {'y': -0.25}),
94 | # 11 (CARETDOWNBASE) m36 caretdown(centered at base)
95 | 11: ('triangle', ['fill_color', 'color', 'size'],
96 | {'angle': 180, 'angle_units': 'deg'}, {'y': 0.25}),
97 | # 'None', ' ' or '' nothing
98 | '': ('text', ['text_color', 'text_font_size', 'text'],
99 | {}, {}),
100 | ' ': ('text', ['text_color', 'text_font_size', 'text'],
101 | {}, {}),
102 | # '$...$' text
103 | '$': ('text', ['text_color', 'text_font_size', 'text'],
104 | {}, {}),
105 | }
106 |
107 |
108 | def get_marker_info(marker):
109 | fnc_name, attrs, vals, updates = None, list(), dict(), dict()
110 | if isinstance(marker, (int, float)):
111 | fnc_name, attrs, vals, updates = _mrk_fncs[int(marker)]
112 | elif isinstance(marker, str):
113 | if not len(marker):
114 | # empty string
115 | fnc_name, attrs, vals, updates = _mrk_fncs[str(marker)]
116 | else:
117 | fnc_name, attrs, vals, updates = _mrk_fncs[str(marker)[0]]
118 | else:
119 | raise Exception(
120 | f'unsupported marker type {type(marker)} for {marker}')
121 | return fnc_name, attrs, vals, updates
122 |
--------------------------------------------------------------------------------
/btplotting/helper/params.py:
--------------------------------------------------------------------------------
1 | import backtrader as bt
2 |
3 |
4 | def paramval2str(name, value):
5 | if value is None: # catch None value early here!
6 | return str(value)
7 | elif name == "timeframe":
8 | return bt.TimeFrame.getname(value, 1)
9 | elif isinstance(value, float):
10 | return f"{value:.2f}"
11 | elif isinstance(value, (list, tuple)):
12 | vals = []
13 | for v in value:
14 | if isinstance(v, (list, tuple)):
15 | v = f'[{paramval2str(name, v)}]'
16 | vals.append(str(v))
17 | return ','.join(vals)
18 | elif isinstance(value, type):
19 | return value.__name__
20 | else:
21 | return str(value)
22 |
23 |
24 | def get_nondefault_params(params: object):
25 | return {key: params._get(key)
26 | for key in params._getkeys()
27 | if not params.isdefault(key)}
28 |
29 |
30 | def get_params(params):
31 | return {key: params._get(key) for key in params._getkeys()}
32 |
33 |
34 | def get_params_str(params):
35 | user_params = get_nondefault_params(params)
36 | plabs = [f'{x}: {paramval2str(x, y)}' for x, y in user_params.items()]
37 | plabs = '/'.join(plabs)
38 | return plabs
39 |
--------------------------------------------------------------------------------
/btplotting/helper/plot.py:
--------------------------------------------------------------------------------
1 |
2 | import matplotlib.colors
3 |
4 |
5 | def convert_color(color):
6 | '''
7 | if color is a float value then it is interpreted as a shade of grey
8 | and converted to the corresponding html color code
9 | '''
10 | try:
11 | val = round(float(color) * 255.0)
12 | hex_string = '#{0:02x}{0:02x}{0:02x}'.format(val)
13 | return hex_string
14 | except ValueError:
15 | return matplotlib.colors.to_hex(color)
16 |
17 |
18 | def sanitize_source_name(name: str):
19 | '''
20 | removes illegal characters from source name to make it
21 | compatible with Bokeh
22 | '''
23 | forbidden_chars = ' (),.-/*:'
24 | for fc in forbidden_chars:
25 | name = name.replace(fc, '_')
26 | return name
27 |
--------------------------------------------------------------------------------
/btplotting/live/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/happydasch/btplotting/727e8c688d3f2d8fca6b1a431499a835f63e3d73/btplotting/live/__init__.py
--------------------------------------------------------------------------------
/btplotting/live/client.py:
--------------------------------------------------------------------------------
1 | import time
2 | import logging
3 | from functools import partial
4 | from threading import Thread
5 |
6 | from bokeh.layouts import column, row, layout
7 | from bokeh.models import Select, Spacer, Tabs, Button, Slider
8 |
9 | from .datahandler import LiveDataHandler
10 | from ..tabs import ConfigTab
11 | from ..utils import get_datanames
12 |
13 | _logger = logging.getLogger(__name__)
14 |
15 |
16 | class LiveClient:
17 |
18 | '''
19 | LiveClient provides live plotting functionality.
20 | '''
21 |
22 | NAV_BUTTON_WIDTH = 35
23 |
24 | def __init__(self, doc, app, strategy, lookback, paused_at_beginning, interval=0.5):
25 | self._app = app
26 | self._doc = doc
27 | self._strategy = strategy
28 | self._interval = interval
29 | self._thread = Thread(target=self._t_thread, daemon=True)
30 | self._refresh_fnc = None
31 | self._datahandler = None
32 | self._figurepage = None
33 | self._running = True
34 | self._paused = paused_at_beginning
35 | self._lastlen = -1
36 | self._filterdata = ''
37 | # plotgroup for filter
38 | self.plotgroup = ''
39 | # amount of candles to plot
40 | self.lookback = lookback
41 | # model is the root model for bokeh and will be set in baseapp
42 | self.model = None
43 |
44 | # append config tab if default tabs should be added
45 | if self._app.p.use_default_tabs:
46 | self._app.tabs.append(ConfigTab)
47 | # set plotgroup from app params if provided
48 | if self._app.p.filterdata and self._app.p.filterdata['group']:
49 | self.plotgroup = self._app.p.filterdata['group']
50 | # create figurepage
51 | self._figid, self._figurepage = self._app.create_figurepage(
52 | self._strategy, filldata=False)
53 |
54 | # create model and finish initialization
55 | self.model, self._refresh_fnc = self._createmodel()
56 | self.refreshmodel()
57 | self._thread.start()
58 |
59 | def _t_thread(self):
60 | '''
61 | Thread method for updates
62 | '''
63 | if self._interval == 0:
64 | return
65 | while self._running:
66 | if not self.is_paused():
67 | if len(self._strategy) == self._lastlen:
68 | continue
69 | self._lastlen = len(self._strategy)
70 | self._datahandler.update()
71 | self.refresh()
72 | time.sleep(self._interval)
73 |
74 | def _createmodel(self):
75 |
76 | def on_select_filterdata(self, a, old, new):
77 | _logger.debug(f'Switching filterdata to {new}...')
78 | self._datahandler.stop()
79 | self._filterdata = new
80 | self.refreshmodel()
81 | _logger.debug('Switching filterdata finished')
82 |
83 | def on_click_nav_action(self):
84 | if not self._paused:
85 | self._pause()
86 | else:
87 | self._resume()
88 | update_nav_buttons(self)
89 |
90 | def on_click_nav_prev(self, steps=1):
91 | self._pause()
92 | self._set_data_by_idx(self._datahandler.get_last_idx() - steps)
93 | update_nav_buttons(self)
94 |
95 | def on_click_nav_next(self, steps=1):
96 | self._pause()
97 | self._set_data_by_idx(self._datahandler.get_last_idx() + steps)
98 | update_nav_buttons(self)
99 |
100 | def refresh(self, now=False):
101 | if now:
102 | update_nav_buttons(self)
103 | else:
104 | self._doc.add_next_tick_callback(
105 | partial(update_nav_buttons, self))
106 |
107 | def reset_nav_buttons(self):
108 | btn_nav_prev.disabled = True
109 | btn_nav_prev_big.disabled = True
110 | btn_nav_next.disabled = True
111 | btn_nav_next_big.disabled = True
112 | btn_nav_action.label = '❙❙'
113 |
114 | def update_nav_buttons(self):
115 | last_idx = self._datahandler.get_last_idx()
116 | last_avail_idx = self._app.get_last_idx(self._figid)
117 |
118 | if self._paused:
119 | btn_nav_action.label = '▶'
120 | else:
121 | btn_nav_action.label = '❙❙'
122 |
123 | if last_idx < self.lookback:
124 | btn_nav_prev.disabled = True
125 | btn_nav_prev_big.disabled = True
126 | else:
127 | btn_nav_prev.disabled = False
128 | btn_nav_prev_big.disabled = False
129 | if last_idx >= last_avail_idx:
130 | btn_nav_next.disabled = True
131 | btn_nav_next_big.disabled = True
132 | else:
133 | btn_nav_next.disabled = False
134 | btn_nav_next_big.disabled = False
135 |
136 | # filter selection
137 | datanames = get_datanames(self._strategy)
138 | options = [('', 'Strategy')]
139 | for d in datanames:
140 | options.append(('D' + d, f'Data: {d}'))
141 | options.append(('G', 'Plot Group'))
142 | self._filterdata = 'D' + datanames[0]
143 | select_filterdata = Select(
144 | value=self._filterdata,
145 | options=options)
146 | select_filterdata.on_change(
147 | 'value',
148 | partial(on_select_filterdata, self))
149 |
150 | # nav
151 | btn_nav_prev = Button(label='❮', width=self.NAV_BUTTON_WIDTH)
152 | btn_nav_prev.on_click(partial(on_click_nav_prev, self))
153 | btn_nav_prev_big = Button(label='❮❮', width=self.NAV_BUTTON_WIDTH)
154 | btn_nav_prev_big.on_click(partial(on_click_nav_prev, self, 10))
155 | btn_nav_action = Button(label='❙❙', width=self.NAV_BUTTON_WIDTH)
156 | btn_nav_action.on_click(partial(on_click_nav_action, self))
157 | btn_nav_next = Button(label='❯', width=self.NAV_BUTTON_WIDTH)
158 | btn_nav_next.on_click(partial(on_click_nav_next, self))
159 | btn_nav_next_big = Button(label='❯❯', width=self.NAV_BUTTON_WIDTH)
160 | btn_nav_next_big.on_click(partial(on_click_nav_next, self, 10))
161 |
162 | # layout
163 | controls = row(
164 | children=[select_filterdata])
165 | nav = row(
166 | children=[btn_nav_prev_big,
167 | btn_nav_prev,
168 | btn_nav_action,
169 | btn_nav_next,
170 | btn_nav_next_big])
171 | slider = Slider(
172 | title='Period for data to plot',
173 | value=self.lookback,
174 | start=1, end=200, step=1)
175 |
176 | # tabs
177 | tabs = Tabs(
178 | sizing_mode='stretch_width')
179 |
180 | # model
181 | model = layout(
182 | [
183 | # app settings, top area
184 | [column(controls, width_policy='min'),
185 | column(slider, sizing_mode='stretch_width'),
186 | column(nav, width_policy='min')],
187 | Spacer(height=15),
188 | # layout for tabs
189 | [tabs]
190 | ],
191 | sizing_mode='stretch_width')
192 |
193 | # return model and a refrash function
194 | return model, partial(refresh, self)
195 |
196 | def _get_filterdata(self):
197 | res = {}
198 | if self._filterdata.startswith('D'):
199 | res['dataname'] = self._filterdata[1:]
200 | elif self._filterdata.startswith('G'):
201 | res['group'] = self.plotgroup
202 | return res
203 |
204 | def _get_tabs(self):
205 | # return self.model.select_one({'id': 'tabs'})
206 | return self.model.select_one({'type': Tabs})
207 |
208 | def _set_data_by_idx(self, idx=None):
209 | # if a index is provided, ensure that index is within data range
210 | if idx:
211 | # don't allow idx to be smaller than lookback - 1
212 | idx = max(idx, self.lookback - 1)
213 | # don't allow idx to be bigger than max idx
214 | last_avail_idx = self._app.get_last_idx(self._figid)
215 | idx = min(idx, last_avail_idx)
216 |
217 | clk = self._figurepage.data_clock._get_clk()
218 | # create DataFrame based on last index with length of lookback
219 | end = self._figurepage.data_clock.get_dt_at_idx(clk, idx)
220 | df = self._app.get_data(
221 | end=end,
222 | figid=self._figid,
223 | back=self.lookback)
224 | self._datahandler.set_df(df)
225 |
226 | def _pause(self):
227 | self._paused = True
228 |
229 | def _resume(self):
230 | if not self._paused:
231 | return
232 | self._paused = False
233 |
234 | def get_app(self):
235 | return self._app
236 |
237 | def get_doc(self):
238 | return self._doc
239 |
240 | def get_figurepage(self):
241 | return self._figurepage
242 |
243 | def get_figid(self):
244 | return self._figid
245 |
246 | def is_paused(self):
247 | return self._paused
248 |
249 | def refresh(self):
250 | if self._refresh_fnc:
251 | self._refresh_fnc(False)
252 |
253 | def refreshmodel(self):
254 | if self._datahandler is not None:
255 | self._datahandler.stop()
256 | self._app.update_figurepage(filterdata=self._get_filterdata())
257 | self._datahandler = LiveDataHandler(self)
258 | tab_panels = self._app.generate_bokeh_model_tab_panels()
259 | for t in self._app.tabs:
260 | tab = t(self._app, self._figurepage, self)
261 | if tab.is_useable():
262 | tab_panels.append(tab.get_tab_panel())
263 | self._get_tabs().tabs = list(filter(None.__ne__, tab_panels))
264 | self.refresh()
265 |
266 | def next(self):
267 | if self._interval != 0:
268 | return
269 | if len(self._strategy) == self._lastlen:
270 | return
271 | self._lastlen = len(self._strategy)
272 | self._datahandler.update()
273 |
274 | def stop(self):
275 | self._running = False
276 | self._thread.join()
277 | self._datahandler.stop()
278 |
--------------------------------------------------------------------------------
/btplotting/live/datahandler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from tornado import gen
3 |
4 | import pandas as pd
5 |
6 | from ..clock import DataClockHandler
7 |
8 | _logger = logging.getLogger(__name__)
9 |
10 |
11 | class LiveDataHandler:
12 |
13 | '''
14 | Handler for live data
15 | '''
16 |
17 | def __init__(self, client):
18 | self._client = client
19 | self._datastore = None
20 | self._lastidx = -1
21 | self._patches = {}
22 | self._cb = None
23 | # inital fill of datastore
24 | self._fill()
25 |
26 | @gen.coroutine
27 | def _cb_push(self):
28 | '''
29 | Pushes to all ColumnDataSources
30 | '''
31 | fp = self._client.get_figurepage()
32 |
33 | # get all rows to patch
34 | patches = {}
35 | for idx in list(self._patches.keys()):
36 | try:
37 | patch = self._patches.pop(idx)
38 | patches[idx] = patch
39 | except KeyError:
40 | continue
41 |
42 | # patch figurepage
43 | for idx, patch in patches.items():
44 | p_data, s_data = fp.get_cds_patchdata_from_series(idx, patch)
45 | if len(p_data) > 0:
46 | _logger.debug(f'Sending patch for figurepage: {p_data}')
47 | fp.cds.patch(p_data)
48 | if len(s_data) > 0:
49 | _logger.debug(f'Sending stream for figurepage: {s_data}')
50 | fp.cds.stream(s_data, self._get_data_stream_length())
51 | # patch all figures
52 | for f in fp.figures:
53 | # only fill with nan if not filling gaps
54 | fillnan = f.fillnan()
55 | # get patch data
56 | p_data, s_data = f.get_cds_patchdata_from_series(
57 | idx, patch, fillnan)
58 | if len(p_data) > 0:
59 | _logger.debug(f'Sending patch for figure: {p_data}')
60 | f.cds.patch(p_data)
61 | if len(s_data) > 0:
62 | _logger.debug(f'Sending stream for figure: {s_data}')
63 | f.cds.stream(s_data, self._get_data_stream_length())
64 | self._lastidx = s_data['index'][-1]
65 |
66 | '''
67 | # take all rows from datastore that were not yet streamed
68 | update_df = self._datastore[self._datastore.index >= self._lastidx]
69 | if not update_df.shape[0]:
70 | return
71 |
72 | # store last index of streamed data
73 | self._lastidx = update_df.index[-1]
74 |
75 | # create stream data for figurepage
76 | data = fp.get_cds_streamdata_from_df(update_df)
77 | if data:
78 | _logger.debug(f'Sending stream for figurepage: {data}')
79 | fp.cds.stream(data, self._get_data_stream_length())
80 |
81 | # create stream df for every figure
82 | for f in fp.figures:
83 | data = f.get_cds_streamdata_from_df(update_df)
84 | if data:
85 | _logger.debug(f'Sending stream for figure: {data}')
86 | f.cds.stream(data, self._get_data_stream_length())
87 | self._lastidx = self._datastore.index[-1]
88 | '''
89 |
90 | def _fill(self):
91 | '''
92 | Fills datastore with latest values
93 | '''
94 | app = self._client.get_app()
95 | fp = self._client.get_figurepage()
96 | figid = self._client.get_figid()
97 | lookback = self._client.lookback
98 | df = app.get_data(figid=figid, back=lookback)
99 | self._set_data(df)
100 | # init by calling set_cds_columns_from_df
101 | # after this, all cds will already contain data
102 | fp.set_cds_columns_from_df(self._datastore)
103 |
104 | def _set_data(self, data, idx=None):
105 | '''
106 | Replaces or appends data to datastore
107 | '''
108 | if isinstance(data, pd.DataFrame):
109 | self._datastore = data
110 | self._lastidx = -1
111 | elif isinstance(data, pd.Series):
112 | if idx is None:
113 | self._datastore = self._datastore.append(data)
114 | else:
115 | self._datastore.loc[idx] = data
116 | else:
117 | raise Exception('Unsupported data provided')
118 | self._datastore = self._datastore.tail(
119 | self._get_data_stream_length())
120 |
121 | def _push(self):
122 | doc = self._client.get_doc()
123 | try:
124 | doc.remove_next_tick_callback(self._cb)
125 | except ValueError:
126 | pass
127 | self._cb = doc.add_next_tick_callback(self._cb_push)
128 |
129 | def _process_data(self, data):
130 | '''
131 | Request to update data with given data
132 | '''
133 | for idx, row in data.iterrows():
134 | if (idx in self._datastore.index):
135 | self._set_data(row, idx)
136 | self._patches[idx] = row
137 | else:
138 | self._set_data(row)
139 |
140 | # if self._datastore is not None:
141 | # self._datastore.drop_duplicates("datetime", keep='last', inplace=True)
142 |
143 | self._push()
144 |
145 | def _get_data_stream_length(self):
146 | '''
147 | Returns the length of data stream to use
148 | '''
149 | return min(self._client.lookback, self._datastore.shape[0])
150 |
151 | def get_last_idx(self):
152 | '''
153 | Returns the last index in local datastore
154 | '''
155 | if self._datastore.shape[0] > 0:
156 | return self._datastore.index[-1]
157 | return -1
158 |
159 | def set_df(self, df):
160 | '''
161 | Sets a new df and streams data
162 | '''
163 | self._set_data(df)
164 | self._push()
165 |
166 | def update(self):
167 | data = None
168 | # fp = self._client.get_figurepage()
169 | app = self._client.get_app()
170 | figid = self._client.get_figid()
171 | lookback = self._client.lookback
172 | # data_clock: DataClockHandler = fp.data_clock
173 | # clk = data_clock._get_clk()
174 | lastidx = self._lastidx
175 | lastavailidx = app.get_last_idx(figid)
176 | # if there is more new data then lookback length
177 | # don't load from last index but from end of data
178 | if (lastidx < 0 or lastavailidx - lastidx > (2 * lookback)):
179 | data = app.get_data(back=lookback)
180 | # if there is just some new data (less then lookback)
181 | # load from last index, so no data is skipped
182 | elif lastidx <= lastavailidx:
183 | startidx = max(0, lastidx - 2)
184 | # start = data_clock.get_dt_at_idx(startidx)
185 | data = app.get_data(startidx=startidx)
186 | # if any new data was loaded
187 | if data is not None:
188 | self._process_data(data)
189 |
190 | def stop(self):
191 | '''
192 | Stops the datahandler
193 | '''
194 | # ensure no pending calls are set
195 | doc = self._client.get_doc()
196 | try:
197 | doc.remove_next_tick_callback(self._cb)
198 | except ValueError:
199 | pass
200 |
--------------------------------------------------------------------------------
/btplotting/optbrowser.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 |
5 | from collections import defaultdict
6 | from functools import partial
7 |
8 | from pandas import DataFrame
9 |
10 | from bokeh.models import ColumnDataSource
11 | from bokeh.layouts import column
12 | from bokeh.models.widgets import (
13 | DataTable,
14 | TableColumn,
15 | NumberFormatter,
16 | StringFormatter,
17 | )
18 |
19 | from .webapp import Webapp
20 |
21 |
22 | class OptBrowser:
23 | def __init__(
24 | self,
25 | app,
26 | optresults,
27 | usercolumns=None,
28 | num_result_limit=None,
29 | sortcolumn=None,
30 | sortasc=True,
31 | address="localhost",
32 | port=81,
33 | autostart=False,
34 | iplot=True,
35 | ):
36 | self._usercolumns = {} if usercolumns is None else usercolumns
37 | self._num_result_limit = num_result_limit
38 | self._app = app
39 | self._sortcolumn = sortcolumn
40 | self._sortasc = sortasc
41 | self._optresults = optresults
42 | self._address = address
43 | self._port = port
44 | self._autostart = autostart
45 | self._iplot = iplot
46 |
47 | def start(self, ioloop=None):
48 | webapp = Webapp(
49 | "Backtrader Optimization Result",
50 | "basic.html.j2",
51 | self._app.params.scheme,
52 | self.build_optresult_model,
53 | address=self._address,
54 | port=self._port,
55 | autostart=self._autostart,
56 | iplot=self._iplot,
57 | )
58 | webapp.start(ioloop)
59 |
60 | def _build_optresult_selector(self, optresults):
61 | # 1. build a dict with all params and all user columns
62 | data_dict = defaultdict(list)
63 | for optres in optresults:
64 | for param_name, _ in optres[0].params._getitems():
65 | param_val = optres[0].params._get(param_name)
66 | data_dict[param_name].append(param_val)
67 |
68 | for usercol_label, usercol_fnc in self._usercolumns.items():
69 | data_dict[usercol_label].append(usercol_fnc(optres))
70 |
71 | # 2. build a pandas DataFrame
72 | df = DataFrame(data_dict)
73 |
74 | # 3. now sort and limit result
75 | if self._sortcolumn is not None:
76 | df = df.sort_values(by=[self._sortcolumn], ascending=self._sortasc)
77 |
78 | if self._num_result_limit is not None:
79 | df = df.head(self._num_result_limit)
80 |
81 | # 4. build column info for Bokeh table
82 | tab_columns = []
83 |
84 | for colname in data_dict.keys():
85 | formatter = NumberFormatter(format="0.000")
86 |
87 | if len(data_dict[colname]) > 0 and isinstance(data_dict[colname][0], int):
88 | formatter = StringFormatter()
89 |
90 | tab_columns.append(
91 | TableColumn(
92 | field=colname,
93 | title=f"{colname}",
94 | sortable=False,
95 | formatter=formatter,
96 | )
97 | )
98 |
99 | cds = ColumnDataSource(df)
100 | selector = DataTable(
101 | source=cds,
102 | columns=tab_columns,
103 | height=150, # fixed height for selector
104 | width=0, # set width to 0 so there is no min_width
105 | sizing_mode="stretch_width",
106 | fit_columns=True,
107 | )
108 | return selector, cds
109 |
110 | def build_optresult_model(self, doc):
111 | """
112 | Generates and returns an interactive model for an OptResult
113 | or an OrderedOptResult
114 | """
115 |
116 | def _get_model(selector_cds, idx: int):
117 | selector_cds.selected.indices = [idx]
118 | selected = selector_cds.data["index"][idx]
119 | return self._app.plot_optmodel(self._optresults[selected][0])
120 |
121 | def update_model_new(selector_cds, stratidx):
122 | new_model = _get_model(selector_cds, stratidx)
123 | model.children.append(new_model)
124 |
125 | def update_model(selector_cds, name, old, new):
126 | if len(new) == 0:
127 | return
128 | stratidx = new[0]
129 | del model.children[-1]
130 | doc.add_next_tick_callback(
131 | partial(update_model_new, selector_cds, stratidx)
132 | )
133 |
134 | # we have list of results, each result contains the result for
135 | # one strategy. we don't support having more than one strategy!
136 | if len(self._optresults) > 0 and len(self._optresults[0]) > 1:
137 | raise RuntimeError(
138 | "You passed on optimization result based on more than"
139 | " one strategy which is not supported!"
140 | )
141 |
142 | selector, selector_cds = self._build_optresult_selector(self._optresults)
143 |
144 | # show the first result in list as default
145 | model = column(
146 | [selector, _get_model(selector_cds, 0)], sizing_mode="stretch_width"
147 | )
148 | model.background = self._app.params.scheme.background_fill
149 |
150 | selector_cds.selected.on_change("indices", partial(update_model, selector_cds))
151 |
152 | return model
153 |
--------------------------------------------------------------------------------
/btplotting/schemes/__init__.py:
--------------------------------------------------------------------------------
1 | from .scheme import Scheme
2 | from .tradimo import Tradimo
3 | from .blackly import Blackly
4 |
--------------------------------------------------------------------------------
/btplotting/schemes/blackly.py:
--------------------------------------------------------------------------------
1 | from .scheme import Scheme
2 |
3 |
4 | class Blackly(Scheme):
5 | def _set_params(self):
6 | super()._set_params()
7 |
8 | self.legend_background_color = '#3C3F41'
9 | self.legend_text_color = 'lightgrey'
10 |
11 | self.crosshair_line_color = '#AAAAAA'
12 | self.tag_pre_background_color = '#2B2B2B'
13 | self.tag_pre_text_color = 'lightgrey'
14 |
15 | self.background_fill = '#2B2B2B'
16 | self.body_background_color = '#2B2B2B'
17 | self.plot_background_color = '#333333'
18 | self.border_fill = '#3C3F41'
19 | self.legend_click = 'hide' # or 'mute'
20 | self.axis_line_color = 'darkgrey'
21 | self.tick_line_color = self.axis_line_color
22 | self.grid_line_color = '#444444'
23 | self.axis_text_color = 'lightgrey'
24 | self.plot_title_text_color = 'darkgrey'
25 | self.axis_label_text_color = 'darkgrey'
26 |
27 | self.tab_active_background_color = '#666666'
28 | self.tab_active_color = '#BBBBBB'
29 |
30 | self.table_color_even = '#404040'
31 | self.table_color_odd = '#333333'
32 | self.table_header_color = '#707070'
33 |
34 | self.tooltip_background_color = '#4C4F51'
35 | self.tooltip_text_label_color = '#848EFF'
36 | self.tooltip_text_value_color = '#AAAAAA'
37 |
--------------------------------------------------------------------------------
/btplotting/schemes/scheme.py:
--------------------------------------------------------------------------------
1 | tableau20 = [
2 | 'steelblue', # 0
3 | 'lightsteelblue', # 1
4 | 'darkorange', # 2
5 | 'peachpuff', # 3
6 | 'green', # 4
7 | 'lightgreen', # 5
8 | 'crimson', # 6
9 | 'lightcoral', # 7
10 | 'mediumpurple', # 8
11 | 'thistle', # 9
12 | 'saddlebrown', # 10
13 | 'rosybrown', # 11
14 | 'orchid', # 12
15 | 'lightpink', # 13
16 | 'gray', # 14
17 | 'lightgray', # 15
18 | 'olive', # 16
19 | 'palegoldenrod', # 17
20 | 'mediumturquoise', # 18
21 | 'paleturquoise', # 19
22 | ]
23 |
24 | tableau10 = [
25 | 'blue', # 'steelblue', # 0
26 | 'darkorange', # 1
27 | 'green', # 2
28 | 'crimson', # 3
29 | 'mediumpurple', # 4
30 | 'saddlebrown', # 5
31 | 'orchid', # 6
32 | 'gray', # 7
33 | 'olive', # 8
34 | 'mediumturquoise', # 9
35 | ]
36 |
37 | tableau10_light = [
38 | 'lightsteelblue', # 0
39 | 'peachpuff', # 1
40 | 'lightgreen', # 2
41 | 'lightcoral', # 3
42 | 'thistle', # 4
43 | 'rosybrown', # 5
44 | 'lightpink', # 6
45 | 'lightgray', # 7
46 | 'palegoldenrod', # 8
47 | 'paleturquoise', # 9
48 | ]
49 |
50 | tab10_index = [3, 0, 2, 1, 2, 4, 5, 6, 7, 8, 9]
51 |
52 |
53 | class PlotScheme(object):
54 | def __init__(self):
55 | # to have a tight packing on the chart wether only the x axis or also
56 | # the y axis have (see matplotlib)
57 | self.ytight = False
58 |
59 | # y-margin (top/bottom) for the subcharts. This will not overrule the
60 | # option plotinfo.plotymargin
61 | self.yadjust = 0.0
62 | # Each new line is in z-order below the previous one. change it False
63 | # to have lines paint above the previous line
64 | self.zdown = True
65 | # Rotation of the date labes on the x axis
66 | self.tickrotation = 15
67 |
68 | # How many "subparts" takes a major chart (datas) in the overall chart
69 | # This is proportional to the total number of subcharts
70 | self.rowsmajor = 5
71 |
72 | # How many "subparts" takes a minor chart (indicators/observers) in the
73 | # overall chart. This is proportional to the total number of subcharts
74 | # Together with rowsmajor, this defines a proportion ratio betwen data
75 | # charts and indicators/observers charts
76 | self.rowsminor = 1
77 |
78 | # Distance in between subcharts
79 | self.plotdist = 0.0
80 |
81 | # Have a grid in the background of all charts
82 | self.grid = True
83 |
84 | # Default plotstyle for the OHLC bars which (line -> line on close)
85 | # Other options: 'bar' and 'candle'
86 | self.style = 'line'
87 |
88 | # Default color for the 'line on close' plot
89 | self.loc = 'black'
90 | # Default color for a bullish bar/candle (0.75 -> intensity of gray)
91 | self.barup = '0.75'
92 | # Default color for a bearish bar/candle
93 | self.bardown = 'red'
94 |
95 | # Wether the candlesticks have to be filled or be transparent
96 | self.barupfill = True
97 | self.bardownfill = True
98 |
99 | # Opacity for the filled candlesticks (1.0 opaque - 0.0 transparent)
100 | self.baralpha = 1.0
101 |
102 | # Alpha blending for fill areas between lines (_fill_gt and _fill_lt)
103 | self.fillalpha = 0.20
104 |
105 | # Wether to plot volume or not. Note: if the data in question has no
106 | # volume values, volume plotting will be skipped even if this is True
107 | self.volume = True
108 |
109 | # Wether to overlay the volume on the data or use a separate subchart
110 | self.voloverlay = True
111 | # Scaling of the volume to the data when plotting as overlay
112 | self.volscaling = 0.33
113 | # Pushing overlay volume up for better visibiliy. Experimentation
114 | # needed if the volume and data overlap too much
115 | self.volpushup = 0.00
116 |
117 | # Default colour for the volume of a bullish day
118 | self.volup = '#aaaaaa' # 0.66 of gray
119 | # Default colour for the volume of a bearish day
120 | self.voldown = '#cc6073' # (204, 96, 115)
121 | # Transparency to apply to the volume when overlaying
122 | self.voltrans = 0.50
123 |
124 | # Transparency for text labels (NOT USED CURRENTLY)
125 | self.subtxttrans = 0.66
126 | # Default font text size for labels on the chart
127 | self.subtxtsize = 9
128 |
129 | # Transparency for the legend
130 | self.legendtrans = 0.66
131 | # Wether indicators have a leged displaey in their charts
132 | self.legendind = True
133 | # Location of the legend for indicators (see matplotlib)
134 | self.legendindloc = 'upper left'
135 |
136 | # Location of the legend for datafeeds (see matplotlib)
137 | self.legenddataloc = 'upper left'
138 |
139 | # Plot the last value of a line after the Object name
140 | self.linevalues = True
141 |
142 | # Plot a tag at the end of each line with the last value
143 | self.valuetags = True
144 |
145 | # Default color for horizontal lines (see plotinfo.plothlines)
146 | self.hlinescolor = '0.66' # shade of gray
147 | # Default style for horizontal lines
148 | self.hlinesstyle = '--'
149 | # Default width for horizontal lines
150 | self.hlineswidth = 1.0
151 |
152 | # Default color scheme: Tableau 10
153 | self.lcolors = tableau10
154 |
155 | # strftime Format string for the display of ticks on the x axis
156 | self.fmt_x_ticks = None
157 |
158 | # strftime Format string for the display of data points values
159 | self.fmt_x_data = None
160 |
161 | def __str__(self):
162 | return self.__class__.__name__
163 |
164 | def color(self, idx):
165 | colidx = tab10_index[idx % len(tab10_index)]
166 | return self.lcolors[colidx]
167 |
168 |
169 | class Scheme(PlotScheme):
170 | def __init__(self, **kwargs):
171 | super().__init__()
172 | self._set_params()
173 | self._set_args(**kwargs)
174 |
175 | def _set_params(self):
176 | self.multiple_tabs = False
177 | self.show_headline = True
178 | self.headline = ''
179 | self.hover_tooltip_config = ''
180 |
181 | self.barup_wick = self.barup
182 | self.bardown_wick = self.bardown
183 |
184 | self.barup_outline = self.barup
185 | self.bardown_outline = self.bardown
186 |
187 | self.crosshair_line_color = '#999999'
188 |
189 | self.legend_background_color = '#3C3F41'
190 | self.legend_text_color = 'lightgrey'
191 | self.legend_location = 'top_left'
192 | self.legend_orientation = 'vertical'
193 |
194 | self.loc = 'lightgray'
195 | self.background_fill = '#222222'
196 | self.body_background_color = 'white'
197 | self.border_fill = '#3C3F41'
198 | self.legend_click = 'hide' # or 'mute'
199 | self.axis_line_color = 'darkgrey'
200 | self.tick_line_color = self.axis_line_color
201 | self.grid_line_color = '#444444'
202 | self.axis_text_color = 'lightgrey'
203 | self.plot_title_text_color = 'darkgrey'
204 | self.axis_label_text_color = 'darkgrey'
205 | self.tag_pre_background_color = 'lightgrey'
206 | self.tag_pre_text_color = 'black'
207 |
208 | self.xaxis_pos = 'all' # 'all' or 'bottom'
209 |
210 | self.table_color_even = '#404040'
211 | self.table_color_odd = '#333333'
212 | self.table_header_color = '#7a7a7a'
213 |
214 | # Plot a title above the plot figure
215 | self.plot_title = True
216 | # Number of columns on the analyzer tab
217 | self.analyzer_tab_num_cols = 1
218 | # Number of columns on the metadata tab
219 | self.metadata_tab_num_cols = 3
220 | # Sizing mode for plot figures (scale or stretch)
221 | self.plot_sizing = 'scale'
222 | self.plot_width = 1000
223 | self.plot_height = 300
224 | # Aspect ratios for different figure types
225 | self.use_aspectratio = True
226 | self.data_aspectratio = 2.5
227 | self.vol_aspectratio = 5.0
228 | self.obs_aspectratio = 5.0
229 | self.ind_aspectratio = 5.0
230 | # output backend mode ("canvas", "svg", "webgl")
231 | self.output_backend = 'canvas'
232 |
233 | self.toolbar_location = 'right'
234 |
235 | self.tooltip_background_color = '#4C4F51'
236 | self.tooltip_text_label_color = '#848EFF'
237 | self.tooltip_text_value_color = '#aaaaaa'
238 |
239 | self.tab_active_background_color = '#333333'
240 | self.tab_active_color = '#4C4F51'
241 |
242 | self.text_color = 'lightgrey'
243 |
244 | # https://docs.bokeh.org/en/latest/docs/reference/models/formatters.html#bokeh.models.formatters.DatetimeTickFormatter
245 | self.hovertool_timeformat = '%F %R'
246 |
247 | self.number_format = '0,0[.]00[000000]'
248 | self.number_format_volume = '0.00 a'
249 |
250 | # https://docs.bokeh.org/en/latest/docs/reference/models/formatters.html
251 | self.axis_tickformat_days = '%d %b %R'
252 | self.axis_tickformat_hourmin = '%H:%M:%S'
253 | self.axis_tickformat_hours = '%d %b %R'
254 | self.axis_tickformat_minsec = '%H:%M:%S'
255 | self.axis_tickformat_minutes = '%H:%M'
256 | self.axis_tickformat_months = '%d/%m/%y'
257 | self.axis_tickformat_seconds = '%H:%M:%S'
258 | self.axis_tickformat_years = '%Y %b'
259 |
260 | # used to add padding on the y-axis for all data except volume
261 | self.y_range_padding = 0.5
262 | # position of y axis for volume
263 | self.vol_axis_location = 'right'
264 |
265 | def _set_args(self, **kwargs):
266 | for k, v in kwargs.items():
267 | if not hasattr(self, k):
268 | raise Exception(f'Invalid scheme parameter "{k}"')
269 | setattr(self, k, v)
270 |
--------------------------------------------------------------------------------
/btplotting/schemes/tradimo.py:
--------------------------------------------------------------------------------
1 | from .blackly import Blackly
2 |
3 |
4 | class Tradimo(Blackly):
5 | def _set_params(self):
6 | super()._set_params()
7 |
8 | dark_text = '#333333'
9 |
10 | self.barup = '#4CAF50'
11 | self.bardown = '#FF5252'
12 |
13 | self.barup_wick = self.barup
14 | self.bardown_wick = self.bardown
15 |
16 | self.barup_outline = self.barup
17 | self.bardown_outline = self.bardown
18 |
19 | self.text_color = '#222222'
20 |
21 | self.crosshair_line_color = '#444444'
22 | self.tag_pre_background_color = '#FFFFFF'
23 | self.tag_pre_text_color = dark_text
24 |
25 | self.legend_background_color = '#F5F5F5'
26 | self.legend_text_color = dark_text
27 | self.legend_click = 'hide' # or 'mute'
28 |
29 | self.loc = '#265371'
30 | self.body_background_color = '#FFFFFF'
31 | self.background_fill = '#FDFDFD'
32 | self.border_fill = '#FFFFFF'
33 | self.axis_line_color = '#222222'
34 | self.grid_line_color = '#EEEEEE'
35 | self.tick_line_color = self.axis_line_color
36 | self.axis_text_color = dark_text
37 | self.plot_title_text_color = dark_text
38 | self.axis_label_text_color = dark_text
39 |
40 | self.table_color_even = '#F0F0F0'
41 | self.table_color_odd = '#FFFFFF'
42 | self.table_header_color = '#FFFFFF'
43 |
44 | self.tooltip_background_color = '#F5F5F5'
45 | self.tooltip_text_label_color = '#848EFF'
46 | self.tooltip_text_value_color = '#AAAAAA'
47 |
48 | self.tab_active_background_color = '#CCCCCC'
49 | self.tab_active_color = '#111111'
50 |
--------------------------------------------------------------------------------
/btplotting/tab.py:
--------------------------------------------------------------------------------
1 | from bokeh.models import TabPanel
2 |
3 |
4 | class BacktraderPlottingTab:
5 | """
6 | Abstract class for tabs
7 | This class needs to be extended from when creating custom tabs.
8 | It is required to overwrite the _is_useable and _get_tab_panel method.
9 | The _get_tab_panel method needs to return a tab panel child and a title.
10 | """
11 |
12 | def __init__(self, app, figurepage, client=None):
13 | self._app = app
14 | self._figurepage = figurepage
15 | self._client = client
16 | self._tab_panel = None
17 |
18 | def _is_useable(self):
19 | raise Exception("_is_useable needs to be implemented.")
20 |
21 | def _get_tab_panel(self):
22 | raise Exception("_get_tab_panel needs to be implemented.")
23 |
24 | def is_useable(self):
25 | """
26 | Returns if the tab is useable within the current environment
27 | """
28 | return self._is_useable()
29 |
30 | def get_tab_panel(self):
31 | """
32 | Returns the tab panel to show as a tab
33 | """
34 | child, title = self._get_tab_panel()
35 | self._tab_panel = TabPanel(child=child, title=title)
36 | return self._tab_panel
37 |
--------------------------------------------------------------------------------
/btplotting/tabs/__init__.py:
--------------------------------------------------------------------------------
1 | from .analyzer import AnalyzerTab
2 | from .metadata import MetadataTab
3 | from .config import ConfigTab
4 | from .log import LogTab
5 | from .source import SourceTab
6 |
--------------------------------------------------------------------------------
/btplotting/tabs/analyzer.py:
--------------------------------------------------------------------------------
1 | from bokeh.layouts import column, row, gridplot, layout
2 | from bokeh.models import Div, Spacer, Button
3 | from ..helper.datatable import AnalysisTableGenerator
4 | from ..tab import BacktraderPlottingTab
5 |
6 |
7 | class AnalyzerTab(BacktraderPlottingTab):
8 |
9 | def __init__(self, app, figurepage, client=None):
10 | super(AnalyzerTab, self).__init__(app, figurepage, client)
11 | self.content = None
12 |
13 | def _is_useable(self):
14 | return len(self._figurepage.analyzers) > 0
15 |
16 | def _get_analyzer_info(self):
17 | tablegen = AnalysisTableGenerator(self._app.scheme, self._app.stylesheet)
18 | acolumns = []
19 | for analyzer in self._figurepage.analyzers:
20 | table_header, elements = tablegen.get_tables(analyzer)
21 | if table_header and elements:
22 | acolumns.append(column([table_header] + elements))
23 | info = gridplot(
24 | acolumns,
25 | ncols=self._app.scheme.analyzer_tab_num_cols,
26 | sizing_mode='stretch_width',
27 | toolbar_options={'logo': None})
28 | return info
29 |
30 | def _on_update_analyzer_info(self):
31 | self.content.children[1] = self._get_analyzer_info()
32 |
33 | def _create_content(self):
34 | title_area = []
35 | title = Div(
36 | text='Available Analyzer Results',
37 | css_classes=['tab-panel-title'],
38 | stylesheets=[self._app.stylesheet])
39 | title_area.append(row([title], width_policy='min'))
40 | if self._client:
41 | btn_refresh = Button(label='Refresh', width_policy='min')
42 | btn_refresh.on_click(self._on_update_analyzer_info)
43 | title_area.append(Spacer())
44 | title_area.append(row([btn_refresh], width_policy='min'))
45 | # set content in self
46 | return layout(
47 | [
48 | title_area,
49 | # initialize with info
50 | [self._get_analyzer_info()]
51 | ],
52 | sizing_mode='stretch_width')
53 |
54 | def _get_tab_panel(self):
55 | if self.content is None:
56 | self.content = self._create_content()
57 | return self.content, 'Analyzers'
58 |
--------------------------------------------------------------------------------
/btplotting/tabs/config.py:
--------------------------------------------------------------------------------
1 | from collections import defaultdict
2 | from functools import partial
3 |
4 | from bokeh.layouts import column
5 | from bokeh.models import Slider, Button, Div, \
6 | CheckboxButtonGroup, TextInput
7 |
8 | from ..figure import FigureType
9 | from ..tab import BacktraderPlottingTab
10 | from ..utils import get_plotobjs
11 | from ..helper.label import obj2label
12 |
13 | import backtrader as bt
14 |
15 |
16 | class ConfigTab(BacktraderPlottingTab):
17 |
18 | def __init__(self, app, figurepage, client=None):
19 | super(ConfigTab, self).__init__(app, figurepage, client)
20 | self.scheme = app.scheme
21 |
22 | def _is_useable(self):
23 | return (self._client is not None)
24 |
25 | def _on_button_save_config(self):
26 | # apply config
27 | self._apply_lookback_config()
28 | self._apply_plotgroup_config()
29 | self._apply_aspectratio_config()
30 | self._client.refreshmodel()
31 |
32 | def _create_lookback_config(self):
33 | title = Div(
34 | text='Lookback period',
35 | css_classes=['config-title'])
36 | self.sld_lookback = Slider(
37 | title='Period for data to plot',
38 | value=self._client.lookback,
39 | start=1, end=200, step=1)
40 | return column([title, self.sld_lookback], sizing_mode='stretch_width')
41 |
42 | def _apply_lookback_config(self):
43 | self._client.lookback = self.sld_lookback.value
44 |
45 | def _create_plotgroup_config(self):
46 | self.plotgroup = []
47 | self.plotgroup_chk = defaultdict(list)
48 | self.plotgroup_objs = defaultdict(list)
49 | self.plotgroup_text = None
50 |
51 | def active_obj(obj, selected):
52 | if not len(selected) or obj.plotinfo.plotid in selected:
53 | return True
54 | return False
55 |
56 | title = Div(
57 | text='Plot Group',
58 | css_classes=['config-title'])
59 | options = []
60 |
61 | # get client plot group selection
62 | if self._client.plotgroup != '':
63 | selected_plot_objs = self._client.plotgroup.split(',')
64 | else:
65 | selected_plot_objs = []
66 |
67 | # get all plot objects
68 | self.plotgroup_objs = get_plotobjs(
69 | self._figurepage.strategy,
70 | order_by_plotmaster=False)
71 |
72 | # create plotgroup checkbox buttons
73 | for d in self.plotgroup_objs:
74 | # generate master chk
75 | master_chk = None
76 | if not isinstance(d, bt.Strategy):
77 | active = []
78 | if active_obj(d, selected_plot_objs):
79 | active.append(0)
80 | self._add_to_plotgroup(d)
81 | master_chk = CheckboxButtonGroup(
82 | labels=[obj2label(d)], active=active)
83 |
84 | # generate childs chk
85 | childs_chk = []
86 | objsd = self.plotgroup_objs[d]
87 | # sort child objs by type
88 | objsd.sort(key=lambda x: (FigureType.get_type(x).value))
89 | # split objs into chunks and store chk
90 | objsd = [objsd[i:i + 3] for i in range(0, len(objsd), 3)]
91 | for x in objsd:
92 | childs = []
93 | active = []
94 | for i, o in enumerate(x):
95 | childs.append(obj2label(o))
96 | if active_obj(o, selected_plot_objs):
97 | active.append(i)
98 | self._add_to_plotgroup(o)
99 | # create a chk for every chunk
100 | if len(childs):
101 | chk = CheckboxButtonGroup(
102 | labels=childs, active=active)
103 | chk.on_change(
104 | 'active',
105 | partial(
106 | self._on_update_plotgroups,
107 | chk=chk,
108 | master=d,
109 | childs=x))
110 | # if master is not active, disable childs
111 | if master_chk and not len(master_chk.active):
112 | chk.disabled = True
113 | childs_chk.append(chk)
114 | self.plotgroup_chk[d].append(x)
115 |
116 | # append title for master (this will also include strategy)
117 | if len(self.plotgroup_objs[d]):
118 | options.append(Div(text=f'{obj2label(d)}:'))
119 | # append master_chk and childs_chk to layout
120 | if master_chk:
121 | master_chk.on_change(
122 | 'active',
123 | partial(
124 | self._on_update_plotgroups,
125 | # provide all related chk to master
126 | chk=[master_chk] + childs_chk,
127 | master=d))
128 | options.append(master_chk)
129 | for c in childs_chk:
130 | options.append(c)
131 |
132 | # text input to display selection
133 | self.plotgroup_text = TextInput(
134 | value=','.join(self.plotgroup),
135 | disabled=True)
136 | options.append(Div(text='Plot Group Selection:'))
137 | options.append(self.plotgroup_text)
138 |
139 | return column([title] + options)
140 |
141 | def _add_to_plotgroup(self, obj):
142 | plotid = obj.plotinfo.plotid
143 | if plotid not in self.plotgroup:
144 | self.plotgroup.append(plotid)
145 |
146 | def _remove_from_plotgroup(self, obj):
147 | plotid = obj.plotinfo.plotid
148 | if plotid in self.plotgroup:
149 | self.plotgroup.remove(plotid)
150 |
151 | def _on_update_plotgroups(self, attr, old, new, chk=None, master=None,
152 | childs=None):
153 | '''
154 | Callback for plot group selection
155 | '''
156 | if childs is None:
157 | # master was clicked
158 | if not len(new):
159 | self._remove_from_plotgroup(master)
160 | # disable all child chk, master has i=0
161 | for i, c in enumerate(chk[1:]):
162 | c.disabled = True
163 | for o in self.plotgroup_chk[master][i]:
164 | self._remove_from_plotgroup(o)
165 | else:
166 | self._add_to_plotgroup(master)
167 | # enable all childs
168 | for i, c in enumerate(chk[1:]):
169 | c.disabled = False
170 | for j in c.active:
171 | o = self.plotgroup_chk[master][i][j]
172 | self._add_to_plotgroup(o)
173 | else:
174 | # child was clicked
175 | added_diff = [i for i in old + new if i not in old and i in new]
176 | removed_diff = [i for i in old + new if i in old and i not in new]
177 | for i in added_diff:
178 | o = childs[i]
179 | self._add_to_plotgroup(o)
180 | for i in removed_diff:
181 | o = childs[i]
182 | self._remove_from_plotgroup(o)
183 |
184 | self.plotgroup_text.value = ','.join(self.plotgroup)
185 |
186 | def _apply_plotgroup_config(self):
187 | # update scheme with new plot group
188 | self._client.plotgroup = ','.join(self.plotgroup)
189 |
190 | def _create_aspectratio_config(self):
191 | self.sld_obs_ar = None
192 | self.sld_data_ar = None
193 | self.sld_vol_ar = None
194 | self.sld_ind_ar = None
195 | title = Div(
196 | text='Aspect Ratios',
197 | css_classes=['config-title'])
198 | self.sld_obs_ar = Slider(
199 | title='Observer Aspect Ratio',
200 | value=self.scheme.obs_aspectratio,
201 | start=0.1, end=20.0, step=0.1)
202 | self.sld_data_ar = Slider(
203 | title='Data Aspect Ratio',
204 | value=self.scheme.data_aspectratio,
205 | start=0.1, end=20.0, step=0.1)
206 | self.sld_vol_ar = Slider(
207 | title='Volume Aspect Ratio',
208 | value=self.scheme.vol_aspectratio,
209 | start=0.1, end=20.0, step=0.1)
210 | self.sld_ind_ar = Slider(
211 | title='Indicator Aspect Ratio',
212 | value=self.scheme.ind_aspectratio,
213 | start=0.1, end=20.0, step=0.1)
214 | return column([title,
215 | self.sld_obs_ar,
216 | self.sld_data_ar,
217 | self.sld_vol_ar,
218 | self.sld_ind_ar])
219 |
220 | def _apply_aspectratio_config(self):
221 | # update scheme with new aspect ratios
222 | self.scheme.obs_aspectratio = self.sld_obs_ar.value
223 | self.scheme.data_aspectratio = self.sld_data_ar.value
224 | self.scheme.vol_aspectratio = self.sld_vol_ar.value
225 | self.scheme.ind_aspectratio = self.sld_ind_ar.value
226 |
227 | def _get_tab_panel(self):
228 | '''
229 | Returns the tab panel for tab
230 | '''
231 | title = Div(
232 | text='Client Configuration',
233 | css_classes=['tab-panel-title'])
234 | button = Button(
235 | label='Save',
236 | button_type='success',
237 | width_policy='min')
238 | button.on_click(self._on_button_save_config)
239 | # layout for config area
240 | config = column(
241 | [self._create_lookback_config(),
242 | self._create_plotgroup_config(),
243 | self._create_aspectratio_config()],
244 | sizing_mode='scale_width')
245 | # layout for config buttons
246 | buttons = column([button])
247 | # config layout
248 | child = column(
249 | children=[title, config, buttons],
250 | sizing_mode='scale_width')
251 |
252 | return child, 'Config'
253 |
--------------------------------------------------------------------------------
/btplotting/tabs/log.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from threading import Lock
3 | from functools import partial
4 | from tornado import gen
5 |
6 | from bokeh.io import curdoc
7 | from bokeh.models import DataTable, TableColumn, ColumnDataSource, Div
8 | from bokeh.layouts import column
9 |
10 | from ..tab import BacktraderPlottingTab
11 |
12 | handler = None
13 |
14 |
15 | def init_log_tab(names, level=logging.NOTSET):
16 | global handler
17 | if handler is None:
18 | handler = CDSHandler(level=level)
19 | for n in names:
20 | logging.getLogger(n).addHandler(handler)
21 |
22 |
23 | def is_log_tab_initialized():
24 | global handler
25 | return handler is not None
26 |
27 |
28 | class CDSHandler(logging.Handler):
29 |
30 | def __init__(self, level=logging.NOTSET):
31 | super(CDSHandler, self).__init__(level=level)
32 | self._lock = Lock()
33 | self.messages = []
34 | self.idx = {}
35 | self.cds = {}
36 | self.cb = {}
37 |
38 | def emit(self, record):
39 | message = record.msg
40 | self.messages.append(message)
41 | with self._lock:
42 | for doc in self.cds:
43 | try:
44 | doc.remove_next_tick_callback(self.cb[doc])
45 | except ValueError:
46 | pass
47 | self.cb[doc] = doc.add_next_tick_callback(
48 | partial(self._stream_to_cds, doc))
49 |
50 | def get_cds(self, doc):
51 | if doc not in self.cds:
52 | with self._lock:
53 | self.cds[doc] = ColumnDataSource(
54 | data=dict(message=self.messages.copy()))
55 | self.cb[doc] = None
56 | self.idx[doc] = len(self.messages) - 1
57 | self.cds[doc].selected.indices = [self.idx[doc]]
58 | return self.cds[doc]
59 |
60 | @gen.coroutine
61 | def _stream_to_cds(self, doc):
62 | last = len(self.messages) - 1
63 | messages = self.messages[self.idx[doc] + 1:last + 1]
64 | if not len(messages):
65 | return
66 | with self._lock:
67 | self.idx[doc] = last
68 | self.cds[doc].stream({'message': messages})
69 | # move only to last if there is a selected row
70 | # when no row is selected, then don't move to new
71 | # row
72 | if len(self.cds[doc].selected.indices) > 0:
73 | self.cds[doc].selected.indices = [self.idx[doc]]
74 |
75 |
76 | class LogTab(BacktraderPlottingTab):
77 |
78 | def _is_useable(self):
79 | return is_log_tab_initialized()
80 |
81 | def _get_tab_panel(self):
82 | global handler
83 |
84 | if handler is None:
85 | init_log_tab([])
86 | if self._client is not None:
87 | doc = self._client.get_doc()
88 | else:
89 | doc = curdoc()
90 |
91 | message = TableColumn(
92 | field='message',
93 | title='Message',
94 | sortable=False)
95 | title = Div(
96 | text='Log Messages',
97 | css_classes=['tab-panel-title'],
98 | stylesheets=[self._app.stylesheet])
99 | table = DataTable(
100 | source=handler.get_cds(doc),
101 | columns=[message],
102 | sizing_mode='stretch_width',
103 | scroll_to_selection=True,
104 | sortable=False,
105 | reorderable=False,
106 | fit_columns=True,
107 | stylesheets=[self._app.stylesheet])
108 | child = column(
109 | children=[title, table],
110 | sizing_mode='stretch_width')
111 |
112 | return child, 'Log'
113 |
--------------------------------------------------------------------------------
/btplotting/tabs/metadata.py:
--------------------------------------------------------------------------------
1 | import math
2 |
3 | import backtrader as bt
4 |
5 | from bokeh.layouts import column, row, gridplot, layout
6 | from bokeh.models import Div, Spacer, Button
7 |
8 | from ..helper.params import get_params, paramval2str
9 | from ..helper.label import obj2label, obj2data
10 | from ..helper.datatable import TableGenerator
11 | from ..tab import BacktraderPlottingTab
12 |
13 |
14 | class MetadataTab(BacktraderPlottingTab):
15 |
16 | def __init__(self, app, figurepage, client=None):
17 | super(MetadataTab, self).__init__(app, figurepage, client)
18 | self.content = None
19 |
20 | def _is_useable(self):
21 | return True
22 |
23 | def _get_title(self, title):
24 | return Div(
25 | text=title,
26 | css_classes=['table-title'],
27 | stylesheets=[self._app.stylesheet])
28 |
29 | def _get_no_params(self):
30 | return Div(text="No parameters",
31 | css_classes=['table-info'],
32 | stylesheets=[self._app.stylesheet])
33 |
34 | def _get_parameter_table(self, params):
35 | tablegen = TableGenerator(self._app.stylesheet)
36 | params = get_params(params)
37 | if len(params) == 0:
38 | return self._get_no_params()
39 | else:
40 | for k, v in params.items():
41 | params[k] = paramval2str(k, v)
42 | return tablegen.get_table(params)
43 |
44 | def _get_values_table(self, values):
45 | tablegen = TableGenerator(self._app.stylesheet)
46 | if len(values) == 0:
47 | values[''] = ''
48 | return tablegen.get_table(values)
49 |
50 | def _get_strategy(self, strategy):
51 | columns = []
52 | childs = []
53 | childs.append(self._get_title(f'Strategy: {obj2label(strategy)}'))
54 | childs.append(self._get_parameter_table(strategy.params))
55 | for o in strategy.observers:
56 | childs.append(self._get_title(f'Observer: {obj2label(o)}'))
57 | childs.append(self._get_parameter_table(o.params))
58 | for a in strategy.analyzers:
59 | childs.append(self._get_title(f'Analyzer: {obj2label(a)}{" [Analysis Table]" if hasattr(a, "get_analysis_table") else ""}'))
60 | childs.append(self._get_parameter_table(a.params))
61 | columns.append(column(childs))
62 | return columns
63 |
64 | def _get_indicators(self, strategy):
65 | columns = []
66 | childs = []
67 | inds = strategy.getindicators()
68 | for i in inds:
69 | if isinstance(i, bt.IndicatorBase):
70 | childs.append(self._get_title(
71 | f'Indicator: {obj2label(i)}@{obj2data(i)}'))
72 | childs.append(self._get_parameter_table(i.params))
73 | columns.append(column(childs))
74 | return columns
75 |
76 | def _get_datas(self, strategy):
77 | columns = []
78 | childs = []
79 | for data in strategy.datas:
80 | tabdata = {
81 | 'DataName:': str(data._dataname).replace('|', '\\|'),
82 | 'Timezone:': str(data._tz),
83 | 'Live:': f'{"Yes" if data.islive() else "No"}',
84 | 'Length:': len(data),
85 | 'Granularity:': f'{data._compression} {bt.TimeFrame.getname(data._timeframe, data._compression)}',
86 | 'Resample:': f'{"Yes" if data.resampling else "No"}',
87 | 'Replay:': f'{"Yes" if data.replaying else "No"}'
88 | }
89 | # live trading does not have valid data parameters (other datas
90 | # might also not have)
91 | if not math.isinf(data.fromdate):
92 | tabdata['Time From:'] = str(bt.num2date(data.fromdate))
93 | if not math.isinf(data.todate):
94 | tabdata['Time To:'] = str(bt.num2date(data.todate))
95 | childs.append(self._get_title(f'Data Feed: {obj2label(data, True)}'))
96 | childs.append(self._get_values_table(tabdata))
97 | columns.append(column(childs))
98 | return columns
99 |
100 | def _get_metadata_columns(self, strategy):
101 | acolumns = []
102 | acolumns.extend(self._get_strategy(strategy))
103 | acolumns.extend(self._get_indicators(strategy))
104 | acolumns.extend(self._get_datas(strategy))
105 | return acolumns
106 |
107 | def _get_metadata_info(self):
108 | acolumns = self._get_metadata_columns(self._figurepage.strategy)
109 | info = gridplot(
110 | acolumns,
111 | ncols=self._app.scheme.metadata_tab_num_cols,
112 | sizing_mode='stretch_width',
113 | toolbar_options={'logo': None})
114 | return info
115 |
116 | def _on_update_metadata_info(self):
117 | self.content.children[1] = self._get_metadata_info()
118 |
119 | def _create_content(self):
120 | title_area = []
121 | title = Div(
122 | text='Strategy Metadata Overview',
123 | css_classes=['tab-panel-title'],
124 | stylesheets=[self._app.stylesheet])
125 | title_area.append(row([title], width_policy='min'))
126 | if self._client:
127 | btn_refresh = Button(label='Refresh', width_policy='min')
128 | btn_refresh.on_click(self._on_update_metadata_info)
129 | title_area.append(Spacer())
130 | title_area.append(row([btn_refresh], width_policy='min'))
131 | # set content in self
132 | return layout(
133 | [
134 | title_area,
135 | # initialize with info
136 | [self._get_metadata_info()]
137 | ],
138 | sizing_mode='stretch_width')
139 |
140 | def _get_tab_panel(self):
141 | if self.content is None:
142 | self.content = self._create_content()
143 | return self.content, 'Metadata'
144 |
--------------------------------------------------------------------------------
/btplotting/tabs/source.py:
--------------------------------------------------------------------------------
1 | import inspect
2 |
3 | from bokeh.models import Div
4 | from bokeh.layouts import column
5 |
6 | from ..tab import BacktraderPlottingTab
7 |
8 |
9 | class SourceTab(BacktraderPlottingTab):
10 |
11 | def _is_useable(self):
12 | return not self._app.is_iplot()
13 |
14 | def _getSource(self):
15 | try:
16 | text = inspect.getsource(
17 | self._figurepage.strategy.__class__)
18 | except Exception:
19 | text = ''
20 | return text
21 |
22 | def _get_tab_panel(self):
23 | title = Div(
24 | text='Source Code',
25 | css_classes=['tab-panel-title'])
26 | child = column(
27 | [title,
28 | Div(text=self._getSource(),
29 | css_classes=['source-pre'],
30 | sizing_mode='stretch_width')],
31 | sizing_mode='stretch_width')
32 | return child, 'Source Code'
33 |
--------------------------------------------------------------------------------
/btplotting/templates/basic.css.j2:
--------------------------------------------------------------------------------
1 | body {
2 | background-color: {{body_background_color}};
3 | color: {{text_color}};
4 | font-family: Arial;
5 | }
6 |
7 | #headline {
8 | color: {{headline_color}};
9 | }
10 |
11 | #footer {
12 | margin-top: 50px;
13 | text-align: right;
14 | padding-right: 1em;
15 | font-size: 0.8em;
16 | }
17 |
--------------------------------------------------------------------------------
/btplotting/templates/basic.html.j2:
--------------------------------------------------------------------------------
1 | {#
2 | Renders Bokeh models into a basic .html file.
3 |
4 | :param title: value for ```` tags
5 | :type title: str
6 |
7 | :param plot_resources: typically the output of RESOURCES
8 | :type plot_resources: str
9 |
10 | :param plot_script: typically the output of PLOT_SCRIPT
11 | :type plot_script: str
12 |
13 | :param plot_div: typically the output of PLOT_DIV
14 | :type plot_div: str
15 |
16 | Users can customize the file output by providing their own Jinja2 template
17 | that accepts these same parameters.
18 |
19 | #}
20 |
21 |
22 |
23 |
24 | {{ title|e if title else "Bokeh Plot" }}
25 | {{ bokeh_css }}
26 | {{ bokeh_js }}
27 |
28 |
29 |
30 | {%if show_headline %}
31 | {{ headline|e if headline else "Backtest" }}
32 | {%endif%}
33 | {{ plot_div|indent(8) }}
34 | {{ plot_script|indent(8) }}
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/btplotting/templates/bokeh.css.j2:
--------------------------------------------------------------------------------
1 | :host(.config-title) {
2 | padding-top: 1em;
3 | padding-bottom: .6em;
4 | font-weight: bold;
5 | font-size: 1.1em;
6 | white-space: nowrap;
7 | overflow: hidden;
8 | text-overflow: ellipsis;
9 | }
10 |
11 | :host(.table-title) {
12 | font-weight: bold;
13 | font-size: 1.1em;
14 | line-height: 1.6em;
15 | white-space: nowrap;
16 | overflow: hidden;
17 | text-overflow: ellipsis;
18 | }
19 |
20 | :host(.tab-panel-title) {
21 | font-weight: bold;
22 | font-size: 1.5em;
23 | line-height: 2.1em;
24 | white-space: nowrap;
25 | overflow: hidden;
26 | text-overflow: ellipsis;
27 | }
28 |
29 | .slick-row.even {
30 | background-color: {{table_color_even}} !important;
31 | }
32 |
33 | .slick-row.odd {
34 | background-color: {{table_color_odd}} !important;
35 | }
36 |
37 | .slick-header-column {
38 | background-color: {{table_header_color}} !important;
39 | background-image: none !important;
40 | font-size: 130%;
41 | }
42 |
43 | .source-pre {
44 | font-family: Courier, monospace;
45 | white-space: -moz-pre-wrap; /* Mozilla, supported since 1999 */
46 | white-space: -pre-wrap; /* Opera */
47 | white-space: -o-pre-wrap; /* Opera */
48 | white-space: pre-wrap; /* CSS3 - Text module (Candidate Recommendation) http://www.w3.org/TR/css3-text/#white-space */
49 | word-wrap: break-word; /* IE 5.5+ */
50 | background-color: {{tag_pre_background_color}};
51 | color: {{tag_pre_text_color}};
52 | padding: 1.2em !important;
53 | font-size: 1.2em;
54 | }
55 |
56 | .label {
57 | font-weight: bold;
58 | line-height: 1.6em;
59 | }
60 |
61 | .table-info {
62 | padding-bottom: .6em;
63 | }
64 |
65 | .bk-root .bk-bs-nav-tabs>li.bk-bs-active>span {
66 | background-color: {{tab_active_background_color}} !important;
67 | color: {{tab_active_color}} !important;
68 | border-color: {{tab_active_background_color}} !important;
69 | }
70 |
71 | .bk-root .bk-bs-nav>li>span:hover {
72 | background-color: {{tab_active_background_color}} !important;
73 | border-color: {{tab_active_background_color}} !important;
74 | color: {{tab_active_color}} !important;
75 | }
76 |
77 | .bk-tooltip > div > div:not(:first-child) {
78 | display: none !important;
79 | }
80 |
81 | .bk-tooltip {
82 | border-radius: 3px;
83 | background-color: {{tooltip_background_color}} !important;
84 | border-color: {{tooltip_background_color}} !important;
85 | }
86 |
87 | .bk-tooltip-row-label {
88 | color: {{tooltip_text_color_label}} !important;
89 | }
90 |
91 | .bk-tooltip-row-value {
92 | color: {{tooltip_text_color_value}} !important;
93 | }
94 |
--------------------------------------------------------------------------------
/btplotting/templates/js/tick_formatter.js:
--------------------------------------------------------------------------------
1 | // args: axis, formatter, source
2 | // We override this axis' formatter's `doFormat` method
3 | // with one that maps index ticks to dates. Some of those dates
4 | // are undefined (e.g. those whose ticks fall out of defined data
5 | // range) and we must filter out and account for those, otherwise
6 | // the formatter computes invalid visible span and returns some
7 | // labels as 'ERR'.
8 | // Note, after this assignment statement, on next plot redrawing,
9 | // our override `doFormat` will be called directly
10 | // -- FunctionTickFormatter.doFormat(), i.e. _this_ code, no longer
11 | // executes.
12 |
13 | axis.formatter.doFormat = function (ticks) {
14 | const dates = ticks.map(i => source.data.datetime[source.data.index.indexOf(i)]),
15 | valid = t => t !== undefined,
16 | labels = formatter.doFormat(dates.filter(valid));
17 | let i = 0;
18 | return dates.map(t => valid(t) ? labels[i++] : '');
19 | };
20 |
21 | // we do this manually only for the first time we are called
22 | const labels = axis.formatter.doFormat(ticks);
23 | return labels[index];
24 |
--------------------------------------------------------------------------------
/btplotting/utils.py:
--------------------------------------------------------------------------------
1 | import itertools
2 | from collections import defaultdict
3 |
4 | import backtrader as bt
5 |
6 |
7 | def get_plotobjs(strategy, include_non_plotable=False, order_by_plotmaster=False):
8 | """
9 | Returns all plotable objects of a strategy
10 |
11 | By default the result will be ordered by the
12 | data the object is aligned to. If order_by_plotmaster
13 | is True, objects will be aligned to their plotmaster.
14 | """
15 | datas = strategy.datas
16 | inds = strategy.getindicators()
17 | obs = strategy.getobservers()
18 | objs = defaultdict(list)
19 | # ensure strategy is included
20 | objs[strategy] = []
21 | # first loop through datas
22 | for d in datas:
23 | if not include_non_plotable and not d.plotinfo._get("plot", True):
24 | continue
25 | objs[d] = []
26 | # next loop through all ind and obs and set them to
27 | # the corresponding data clock
28 | for obj in itertools.chain(inds, obs):
29 | # check for base classes
30 | if not isinstance(obj, (bt.IndicatorBase, bt.MultiCoupler, bt.ObserverBase)):
31 | continue
32 | # check for plotinfos
33 | if not hasattr(obj, "plotinfo"):
34 | # no plotting support cause no plotinfo attribute
35 | # available - so far LineSingle derived classes
36 | continue
37 | # should this indicator be plotted?
38 | if not include_non_plotable and (
39 | not obj.plotinfo._get("plot", True)
40 | or obj.plotinfo._get("plotskip", False)
41 | or obj.plotinfo._get("_plotskip", False)
42 | ):
43 | continue
44 | # append object to the data object
45 | pltmaster = get_plotmaster(obj)
46 | data = get_clock_obj(obj, True)
47 | if pltmaster in objs:
48 | objs[pltmaster].append(obj)
49 | elif data in objs:
50 | objs[data].append(obj)
51 |
52 | if not order_by_plotmaster:
53 | return objs
54 |
55 | # order objects by its plotmaster
56 | pobjs = defaultdict(list)
57 | for d in objs:
58 | pmaster = get_plotmaster(d)
59 | # add all datas, if a data has a plotmaster, add it to plotmaster
60 | if pmaster is d and pmaster not in pobjs:
61 | pobjs[pmaster] = []
62 | elif pmaster is not None and pmaster is not d:
63 | pobjs[pmaster].append(d)
64 | # all objects in parent
65 | for o in objs[d]:
66 | pmaster = get_plotmaster(o.plotinfo.plotmaster)
67 | subplot = o.plotinfo.subplot
68 | if subplot and pmaster is None:
69 | # ensure the plot object is set as a key in plot objects
70 | if o not in pobjs:
71 | pobjs[o] = []
72 | elif pmaster is not None:
73 | # even if subplot but has a plotmaster
74 | pobjs[pmaster].append(o)
75 | else:
76 | # get the plotmaster
77 | pmaster = get_plotmaster(get_clock_obj(o, True))
78 | if pmaster is not None and pmaster in pobjs:
79 | # only append if the plotmaster is really present in
80 | # plot objects, so no skipped plot objects will be
81 | # readded
82 | pobjs[pmaster].append(o)
83 |
84 | # return objects ordered by plotmaster
85 | return pobjs
86 |
87 |
88 | def get_plotmaster(obj):
89 | """
90 | Resolves the plotmaster of the given object
91 | """
92 | if obj is None:
93 | return None
94 |
95 | while True:
96 | pm = obj.plotinfo.plotmaster
97 | if pm is None:
98 | break
99 | else:
100 | obj = pm
101 | if isinstance(obj, bt.Strategy):
102 | return None
103 | return obj
104 |
105 |
106 | def get_last_avail_idx(strategy, dataname=False):
107 | """
108 | Returns the last available index of a data source
109 | """
110 | if dataname is not False:
111 | data = strategy.getdatabyname(dataname)
112 | else:
113 | data = strategy
114 | return len(data) - 1
115 |
116 |
117 | def filter_obj(obj, filterdata):
118 | """
119 | Returns if the given object should be filtered.
120 | False if object should not be filtered out,
121 | True if object should be filtered out.
122 | """
123 |
124 | if filterdata is None:
125 | return False
126 |
127 | dataname = get_dataname(obj)
128 | plotid = obj.plotinfo.plotid
129 |
130 | # filter by dataname
131 | if "dataname" in filterdata:
132 | if dataname is not False:
133 | if isinstance(filterdata["dataname"], str):
134 | if dataname != filterdata["dataname"]:
135 | return True
136 | elif isinstance(filterdata["dataname", list]):
137 | if dataname not in filterdata["dataname"]:
138 | return True
139 | else:
140 | return True
141 | if "group" in filterdata:
142 | if isinstance(filterdata["group"], str):
143 | if filterdata["group"] != "":
144 | plotids = filterdata["group"].split(",")
145 | if plotid not in plotids:
146 | return True
147 |
148 | return False
149 |
150 |
151 | def get_datanames(strategy, onlyplotable=True):
152 | """
153 | Returns the names of all data sources
154 | """
155 | datanames = []
156 | for d in strategy.datas:
157 | if not onlyplotable or d.plotinfo.plot is not False:
158 | datanames.append(get_dataname(d))
159 | return datanames
160 |
161 |
162 | def get_dataname(obj):
163 | """
164 | Returns the name of the data for the given object
165 | If the data for a object is a strategy then False will
166 | be returned.
167 | """
168 | data = get_clock_obj(obj, True)
169 | if isinstance(data, bt.Strategy):
170 | # strategy will have no dataname
171 | return False
172 | elif isinstance(data, bt.AbstractDataBase):
173 | # data feeds are end points
174 | # try some popular attributes that might carry a name
175 | # _name: user assigned value upon instantiation
176 | # _dataname: underlying bt dataname (is always available)
177 | # if that fails, use str
178 | for n in ["_name", "_dataname"]:
179 | val = getattr(data, n)
180 | if val is not None:
181 | break
182 | if val is None:
183 | val = str(data)
184 | return val
185 | else:
186 | raise Exception(f"Unsupported data: {obj.__class__}")
187 |
188 |
189 | def get_smallest_dataname(strategy, datanames):
190 | """
191 | Returns the smallest dataname from a list of
192 | datanames
193 | """
194 | data = False
195 | for d in datanames:
196 | if not d:
197 | continue
198 | tmp = strategy.getdatabyname(d)
199 | if (
200 | data is False
201 | or (tmp._timeframe < data._timeframe)
202 | or (
203 | tmp._timeframe == data._timeframe
204 | and tmp._compression < data._compression
205 | )
206 | ):
207 | data = tmp
208 | if data is False:
209 | return data
210 | return get_dataname(data)
211 |
212 |
213 | def get_clock_obj(obj, resolve_to_data=False):
214 | """
215 | Returns a clock object to use for building data
216 | A clock object can be either a strategy, data source,
217 | indicator or a observer.
218 | """
219 | if isinstance(obj, bt.LinesOperation):
220 | # indicators can be created to run on a line
221 | # (instead of e.g. a data object) in that case grab
222 | # the owner of that line to find the corresponding clock
223 | # also check for line actions like "macd > data[0]"
224 | return get_clock_obj(obj._clock, resolve_to_data)
225 | elif isinstance(obj, (bt.LineSingle)):
226 | # if we have a line, return its owners clock
227 | return get_clock_obj(obj._owner, resolve_to_data)
228 | elif isinstance(obj, bt.LineSeriesStub):
229 | # if its a LineSeriesStub object, take the first line
230 | # and get the clock from it
231 | return get_clock_obj(obj.lines[0], resolve_to_data)
232 | elif isinstance(obj, (bt.IndicatorBase, bt.MultiCoupler, bt.ObserverBase)):
233 | # a indicator and observer can be a clock, internally
234 | # it is obj._clock
235 | if resolve_to_data:
236 | return get_clock_obj(obj._clock, resolve_to_data)
237 | clk = obj
238 | elif isinstance(obj, bt.StrategyBase):
239 | # a strategy can be a clock, internally it is obj.data
240 | clk = obj
241 | elif isinstance(obj, bt.AbstractDataBase):
242 | clk = obj
243 | else:
244 | raise Exception(f"Unsupported clock: {obj.__class__}")
245 | return clk
246 |
247 |
248 | def get_clock_line(obj):
249 | """
250 | Find the corresponding clock for an object.
251 | A clock is a datetime line that holds timestamps
252 | for the line in question.
253 | """
254 | clk = get_clock_obj(obj)
255 | return clk.lines.datetime
256 |
257 |
258 | def get_source_id(source):
259 | """
260 | Returns a unique source id for given source.
261 | This is used for unique column names.
262 | """
263 | return str(id(source))
264 |
--------------------------------------------------------------------------------
/btplotting/version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.2.2"
2 |
--------------------------------------------------------------------------------
/btplotting/webapp.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 |
3 | from bokeh.application.handlers.function import FunctionHandler
4 | from bokeh.application import Application
5 | from bokeh.document import Document
6 | from bokeh.server.server import Server
7 | from bokeh.io import show
8 | from bokeh.util.browser import view
9 | from bokeh.server.views.ws import WSHandler
10 | from jinja2 import Environment, PackageLoader
11 |
12 | from .helper.bokeh import generate_stylesheet
13 |
14 |
15 | def check_origin_overwrite(self, origin):
16 | return True
17 |
18 |
19 | class Webapp:
20 | def __init__(
21 | self,
22 | title,
23 | html_template,
24 | scheme,
25 | model_factory_fnc,
26 | on_session_destroyed=None,
27 | address="localhost",
28 | port=81,
29 | autostart=False,
30 | iplot=True,
31 | ):
32 | self._title = title
33 | self._html_template = html_template
34 | self._scheme = scheme
35 | self._model_factory_fnc = model_factory_fnc
36 | self._on_session_destroyed = on_session_destroyed
37 | self._address = address
38 | self._port = port
39 | self._autostart = autostart
40 | self._iplot = iplot
41 |
42 | def start(self, ioloop=None):
43 | """
44 | Serves a backtrader result as a Bokeh application running on
45 | a web server
46 | """
47 |
48 | def make_document(doc: Document):
49 | if self._on_session_destroyed is not None:
50 | doc.on_session_destroyed(self._on_session_destroyed)
51 |
52 | # set document title
53 | doc.title = self._title
54 |
55 | # set document template
56 | now = datetime.now()
57 | env = Environment(loader=PackageLoader("btplotting", "templates"))
58 | templ = env.get_template(self._html_template)
59 | templ.globals["now"] = now.strftime("%Y-%m-%d %H:%M:%S")
60 | doc.template = templ
61 | doc.template_variables["stylesheet"] = generate_stylesheet(self._scheme)
62 | model = self._model_factory_fnc(doc)
63 | doc.add_root(model)
64 |
65 | self._run_server(
66 | make_document,
67 | ioloop=ioloop,
68 | address=self._address,
69 | port=self._port,
70 | autostart=self._autostart,
71 | iplot=self._iplot,
72 | )
73 |
74 | @staticmethod
75 | def _run_server(
76 | fnc_make_document,
77 | ioloop=None,
78 | address="localhost",
79 | port=81,
80 | autostart=False,
81 | iplot=True,
82 | ):
83 | """
84 | Runs a Bokeh webserver application. Documents will be created using
85 | fnc_make_document
86 | """
87 | handler = FunctionHandler(fnc_make_document)
88 | app = Application(handler)
89 |
90 | if iplot:
91 | try:
92 | # src: https://stackoverflow.com/questions/44100477/how-to-check-if-you-are-in-a-jupyter-notebook
93 | get_ipython # noqa: *
94 | # patch ws handler as a workaround for jupyter in vscode
95 | # check_origin will return allways true
96 | WSHandler.check_origin_src = WSHandler.check_origin
97 | WSHandler.check_origin = check_origin_overwrite
98 | return show(app)
99 | except NameError:
100 | pass
101 |
102 | apps = {"/": app}
103 | display_address = address if address != "*" else "localhost"
104 | origin = [f"{address}:{port}" if address != "*" else address]
105 | server = Server(apps, port=port, io_loop=ioloop, allow_websocket_origin=origin)
106 | if autostart:
107 | print("Browser is launching at" f" http://{display_address}:{port}")
108 | view(f"http://{display_address}:{port}")
109 | else:
110 | print(f"Open browser at http://{display_address}:{port}")
111 | if ioloop is None:
112 | server.run_until_shutdown()
113 | else:
114 | server.start()
115 | ioloop.start()
116 |
--------------------------------------------------------------------------------
/demos/blackly_single.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 |
7 |
8 | class MyStrategy(bt.Strategy):
9 | def __init__(self):
10 | sma1 = bt.indicators.SMA(period=11, subplot=True)
11 | bt.indicators.SMA(period=17, plotmaster=sma1)
12 | bt.indicators.RSI()
13 |
14 | def next(self):
15 | pos = len(self.data)
16 | if pos == 45 or pos == 145:
17 | self.buy(self.datas[0], size=None)
18 |
19 | if pos == 116 or pos == 215:
20 | self.sell(self.datas[0], size=None)
21 |
22 |
23 | if __name__ == '__main__':
24 | cerebro = bt.Cerebro()
25 |
26 | cerebro.addstrategy(MyStrategy)
27 |
28 | data = bt.feeds.YahooFinanceCSVData(
29 | dataname="datas/orcl-1995-2014.txt",
30 | fromdate=datetime.datetime(2000, 1, 1),
31 | todate=datetime.datetime(2001, 2, 28),
32 | reverse=False,
33 | swapcloses=True,
34 | )
35 | cerebro.adddata(data)
36 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
37 |
38 | cerebro.run()
39 |
40 | p = BacktraderPlotting(style='bar')
41 | cerebro.plot(p)
42 |
--------------------------------------------------------------------------------
/demos/blackly_tabs.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 |
7 |
8 | class MyStrategy(bt.Strategy):
9 | def __init__(self):
10 | sma1 = bt.indicators.SMA(period=11, subplot=True)
11 | bt.indicators.SMA(period=17, plotmaster=sma1)
12 | bt.indicators.RSI()
13 |
14 | def next(self):
15 | pos = len(self.data)
16 | if pos == 45 or pos == 145:
17 | self.buy(self.datas[0], size=None)
18 |
19 | if pos == 116 or pos == 215:
20 | self.sell(self.datas[0], size=None)
21 |
22 |
23 | if __name__ == '__main__':
24 | cerebro = bt.Cerebro()
25 |
26 | cerebro.addstrategy(MyStrategy)
27 |
28 | data = bt.feeds.YahooFinanceCSVData(
29 | dataname="datas/orcl-1995-2014.txt",
30 | fromdate=datetime.datetime(2000, 1, 1),
31 | todate=datetime.datetime(2001, 2, 28),
32 | reverse=False,
33 | swapcloses=True,
34 | )
35 | cerebro.adddata(data)
36 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
37 |
38 | cerebro.run()
39 |
40 | p = BacktraderPlotting(style='bar', multiple_tabs=True)
41 | cerebro.plot(p)
42 |
--------------------------------------------------------------------------------
/demos/data_live.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8; py-indent-offset:4 -*-
3 | from __future__ import (absolute_import, division, print_function,
4 | unicode_literals)
5 |
6 | import datetime
7 | import logging
8 |
9 | import backtrader as bt
10 |
11 | from btplotting import BacktraderPlottingLive
12 | from btplotting.schemes import Blackly
13 | from btplotting.analyzers import RecorderAnalyzer
14 | from btplotting.feeds import FakeFeed
15 |
16 | _logger = logging.getLogger(__name__)
17 |
18 |
19 | class LiveDemoStrategy(bt.Strategy):
20 | params = (
21 | ('modbuy', 2),
22 | ('modsell', 3),
23 | )
24 |
25 | def __init__(self):
26 | pass
27 | sma1 = bt.indicators.SMA(self.data0.close, subplot=True)
28 | sma2 = bt.indicators.SMA(self.data1.close, subplot=True)
29 | rsi = bt.ind.RSI()
30 | cross = bt.ind.CrossOver(sma1, sma2)
31 |
32 | def next(self):
33 | pos = len(self.data)
34 | if pos % self.p.modbuy == 0:
35 | if self.broker.getposition(self.datas[0]).size == 0:
36 | self.buy(self.datas[0], size=None)
37 |
38 | if pos % self.p.modsell == 0:
39 | if self.broker.getposition(self.datas[0]).size > 0:
40 | self.sell(self.datas[0], size=None)
41 |
42 |
43 | def _get_trading_calendar(open_hour, close_hour, close_minute):
44 | cal = bt.TradingCalendar(open=datetime.time(hour=open_hour), close=datetime.time(hour=close_hour, minute=close_minute))
45 | return cal
46 |
47 |
48 | def _run_resampler(data_timeframe,
49 | data_compression,
50 | resample_timeframe,
51 | resample_compression,
52 | runtime_seconds=27,
53 | starting_value=200,
54 | tick_interval=datetime.timedelta(seconds=11),
55 | num_gen_bars=None,
56 | start_delays=None,
57 | num_data=1,
58 | ) -> bt.Strategy:
59 | _logger.info("Constructing Cerebro")
60 | cerebro = bt.Cerebro()
61 | cerebro.addstrategy(LiveDemoStrategy)
62 |
63 | cerebro.addanalyzer(RecorderAnalyzer)
64 |
65 | cerebro.addanalyzer(BacktraderPlottingLive, volume=False, scheme=Blackly(
66 | hovertool_timeformat='%F %R:%S'), lookback=120)
67 |
68 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
69 |
70 | for i in range(0, num_data):
71 | start_delay = 0
72 | if start_delays is not None and i <= len(start_delays) and start_delays[i] is not None:
73 | start_delay = start_delays[i]
74 |
75 | num_gen_bar = 0
76 | if num_gen_bars is not None and i <= len(num_gen_bars) and num_gen_bars[i] is not None:
77 | num_gen_bar = num_gen_bars[i]
78 |
79 | data = FakeFeed(timeframe=data_timeframe,
80 | compression=data_compression,
81 | run_duration=datetime.timedelta(seconds=runtime_seconds),
82 | starting_value=starting_value,
83 | tick_interval=tick_interval,
84 | live=True,
85 | num_gen_bars=num_gen_bar,
86 | start_delay=start_delay,
87 | name=f'data{i}',
88 | )
89 |
90 | cerebro.resampledata(data, timeframe=resample_timeframe, compression=resample_compression)
91 |
92 | # return the recorded bars attribute from the first strategy
93 | res = cerebro.run()
94 | return cerebro, res[0]
95 |
96 |
97 | if __name__ == '__main__':
98 | logging.basicConfig(format='%(asctime)s %(name)s:%(levelname)s:%(message)s', level=logging.INFO)
99 | cerebro, strat = _run_resampler(data_timeframe=bt.TimeFrame.Ticks,
100 | data_compression=1,
101 | resample_timeframe=bt.TimeFrame.Seconds,
102 | resample_compression=10,
103 | runtime_seconds=60000,
104 | tick_interval=datetime.timedelta(seconds=1),
105 | start_delays=[None, None],
106 | num_gen_bars=[0, 10],
107 | num_data=2,
108 | )
109 |
--------------------------------------------------------------------------------
/demos/data_logging.py:
--------------------------------------------------------------------------------
1 | from btplotting.tabs.log import init_log_tab
2 | from btplotting import BacktraderPlotting
3 | import backtrader as bt
4 | import logging
5 | import datetime
6 |
7 |
8 | class MyStrategy(bt.Strategy):
9 | def next(self):
10 | print(f"close: {self.data.close[0]}")
11 | logger.debug(f"open: {self.data.open[0]}")
12 | logger.info(f"close: {self.data.close[0]}")
13 |
14 |
15 | if __name__ == '__main__':
16 | logger = logging.getLogger(__name__)
17 | logger.setLevel(logging.DEBUG)
18 | # add stream handler to log everything to console
19 | logger.addHandler(logging.StreamHandler())
20 | cerebro = bt.Cerebro()
21 |
22 | # init log tab with log level INFO
23 | init_log_tab([__name__], logging.INFO)
24 |
25 | cerebro.addstrategy(MyStrategy)
26 |
27 | data = bt.feeds.YahooFinanceCSVData(
28 | dataname="datas/orcl-1995-2014.txt",
29 | fromdate=datetime.datetime(2000, 1, 1),
30 | todate=datetime.datetime(2001, 2, 28),
31 | reverse=False,
32 | swapcloses=True,
33 | )
34 | cerebro.adddata(data)
35 |
36 | cerebro.run()
37 |
38 | p = BacktraderPlotting(style='bar')
39 | cerebro.plot(p)
40 |
--------------------------------------------------------------------------------
/demos/data_multi_live.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8; py-indent-offset:4 -*-
3 | from __future__ import (absolute_import, division, print_function,
4 | unicode_literals)
5 |
6 | import datetime
7 |
8 | import backtrader as bt
9 | import logging
10 |
11 | from btplotting import BacktraderPlottingLive
12 | from btplotting.schemes import Blackly
13 | from btplotting.analyzers import RecorderAnalyzer
14 | from btplotting.feeds import FakeFeed
15 |
16 | _logger = logging.getLogger(__name__)
17 |
18 |
19 | def _get_trading_calendar(open_hour, close_hour, close_minute):
20 | cal = bt.TradingCalendar(open=datetime.time(hour=open_hour), close=datetime.time(hour=close_hour, minute=close_minute))
21 | return cal
22 |
23 |
24 | def _run_resampler(data_timeframe,
25 | data_compression,
26 | resample_timeframe,
27 | resample_compression,
28 | runtime_seconds=27,
29 | starting_value=200,
30 | tick_interval=datetime.timedelta(seconds=11),
31 | num_gen_bars=20,
32 | ) -> bt.Strategy:
33 | _logger.info("Constructing Cerebro")
34 | cerebro = bt.Cerebro()
35 |
36 | cerebro.addanalyzer(RecorderAnalyzer)
37 | cerebro.addanalyzer(BacktraderPlottingLive, volume=False, scheme=Blackly(
38 | hovertool_timeformat='%F %R:%S'), lookback=120)
39 |
40 | data = FakeFeed(timeframe=data_timeframe,
41 | compression=data_compression,
42 | run_duration=datetime.timedelta(seconds=runtime_seconds),
43 | starting_value=starting_value,
44 | tick_interval=tick_interval,
45 | live=True,
46 | num_gen_bars=num_gen_bars,
47 | start_delay=0,
48 | name='data0',
49 | )
50 |
51 | cerebro.resampledata(data, timeframe=resample_timeframe, compression=resample_compression)
52 |
53 | data2 = FakeFeed(timeframe=data_timeframe,
54 | compression=data_compression,
55 | run_duration=datetime.timedelta(seconds=runtime_seconds),
56 | starting_value=starting_value,
57 | tick_interval=tick_interval,
58 | live=True,
59 | num_gen_bars=num_gen_bars,
60 | start_delay=80,
61 | name='data1',
62 | )
63 |
64 | cerebro.resampledata(data2, timeframe=resample_timeframe, compression=resample_compression)
65 |
66 | # return the recorded bars attribute from the first strategy
67 | res = cerebro.run()
68 | return cerebro, res[0]
69 |
70 |
71 | if __name__ == '__main__':
72 | logging.basicConfig(format='%(asctime)s %(name)s:%(levelname)s:%(message)s', level=logging.DEBUG)
73 | cerebro, strat = _run_resampler(data_timeframe=bt.TimeFrame.Ticks,
74 | data_compression=1,
75 | resample_timeframe=bt.TimeFrame.Minutes,
76 | resample_compression=1,
77 | runtime_seconds=190,
78 | )
79 |
--------------------------------------------------------------------------------
/demos/data_replay.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8; py-indent-offset:4 -*-
3 | from __future__ import (absolute_import, division, print_function,
4 | unicode_literals)
5 |
6 | import datetime
7 | import logging
8 |
9 | import backtrader as bt
10 |
11 |
12 | from btplotting import BacktraderPlottingLive
13 | from btplotting.schemes import Blackly
14 | from btplotting.analyzers import RecorderAnalyzer
15 | from btplotting.feeds import FakeFeed
16 |
17 |
18 | class LiveDemoStrategy(bt.Strategy):
19 | params = (
20 | ('modbuy', 2),
21 | ('modsell', 3),
22 | )
23 |
24 | def __init__(self):
25 | self._sma = bt.indicators.SMA(self.data0.close, subplot=True, period=3)
26 | #self._sma2 = bt.indicators.SMA(self.data1.close, subplot=True)
27 |
28 | def next(self):
29 | pos = len(self.data)
30 | if pos % self.p.modbuy == 0:
31 | if self.broker.getposition(self.datas[0]).size == 0:
32 | self.buy(self.datas[0], size=None)
33 |
34 | if pos % self.p.modsell == 0:
35 | if self.broker.getposition(self.datas[0]).size > 0:
36 | self.sell(self.datas[0], size=None)
37 |
38 |
39 | def _get_trading_calendar(open_hour, close_hour, close_minute):
40 | cal = bt.TradingCalendar(open=datetime.time(
41 | hour=open_hour), close=datetime.time(hour=close_hour, minute=close_minute))
42 | return cal
43 |
44 |
45 | def _run_resampler(data_timeframe,
46 | data_compression,
47 | resample_timeframe,
48 | resample_compression,
49 | runtime_seconds=27,
50 | starting_value=200,
51 | tick_interval=datetime.timedelta(seconds=11),
52 | num_gen_bars=None,
53 | start_delays=None,
54 | num_data=1,
55 | ) -> bt.Strategy:
56 | cerebro = bt.Cerebro()
57 | cerebro.addstrategy(LiveDemoStrategy)
58 |
59 | cerebro.addanalyzer(RecorderAnalyzer)
60 |
61 | cerebro.addanalyzer(BacktraderPlottingLive, volume=False, scheme=Blackly(
62 | hovertool_timeformat='%F %R:%S'), lookback=120)
63 |
64 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
65 |
66 | for i in range(0, num_data):
67 | start_delay = 0
68 | if start_delays is not None and i <= len(start_delays) and start_delays[i] is not None:
69 | start_delay = start_delays[i]
70 |
71 | num_gen_bar = 0
72 | if num_gen_bars is not None and i <= len(num_gen_bars) and num_gen_bars[i] is not None:
73 | num_gen_bar = num_gen_bars[i]
74 |
75 | data = FakeFeed(timeframe=data_timeframe,
76 | compression=data_compression,
77 | run_duration=datetime.timedelta(
78 | seconds=runtime_seconds),
79 | starting_value=starting_value,
80 | tick_interval=tick_interval,
81 | live=True,
82 | num_gen_bars=num_gen_bar,
83 | start_delay=start_delay,
84 | name=f'data{i}',
85 | )
86 |
87 | cerebro.replaydata(data, timeframe=resample_timeframe,
88 | compression=resample_compression)
89 |
90 | # return the recorded bars attribute from the first strategy
91 | res = cerebro.run()
92 | return cerebro, res[0]
93 |
94 |
95 | if __name__ == '__main__':
96 | logging.basicConfig(
97 | format='%(asctime)s %(name)s:%(levelname)s:%(message)s', level=logging.DEBUG)
98 | cerebro, strat = _run_resampler(data_timeframe=bt.TimeFrame.Ticks,
99 | data_compression=1,
100 | resample_timeframe=bt.TimeFrame.Seconds,
101 | resample_compression=10,
102 | runtime_seconds=60000,
103 | tick_interval=datetime.timedelta(
104 | seconds=1),
105 | start_delays=[None, None],
106 | num_gen_bars=[0, 10],
107 | num_data=1,
108 | )
109 |
--------------------------------------------------------------------------------
/demos/datas/2006-day-001.txt:
--------------------------------------------------------------------------------
1 | Date,Open,High,Low,Close,Volume,OpenInterest
2 | 2006-01-02,3578.73,3605.95,3578.73,3604.33,0,0
3 | 2006-01-03,3604.08,3638.42,3601.84,3614.34,0,0
4 | 2006-01-04,3615.23,3652.46,3615.23,3652.46,0,0
5 | 2006-01-05,3652.19,3661.65,3643.17,3650.24,0,0
6 | 2006-01-06,3650.54,3666.99,3647.66,3666.99,0,0
7 | 2006-01-09,3667.10,3685.99,3667.10,3671.78,0,0
8 | 2006-01-10,3671.23,3671.23,3638.77,3644.94,0,0
9 | 2006-01-11,3645.73,3674.31,3645.73,3668.61,0,0
10 | 2006-01-12,3667.16,3676.00,3656.99,3670.20,0,0
11 | 2006-01-13,3670.27,3670.27,3618.06,3629.25,0,0
12 | 2006-01-16,3628.73,3649.10,3621.03,3644.41,0,0
13 | 2006-01-17,3639.57,3639.57,3606.54,3610.07,0,0
14 | 2006-01-18,3609.34,3609.34,3550.16,3570.17,0,0
15 | 2006-01-19,3572.19,3597.34,3572.19,3593.22,0,0
16 | 2006-01-20,3593.16,3612.37,3550.80,3550.80,0,0
17 | 2006-01-23,3550.24,3550.24,3515.07,3544.31,0,0
18 | 2006-01-24,3544.78,3553.16,3526.37,3532.68,0,0
19 | 2006-01-25,3532.72,3578.00,3532.72,3578.00,0,0
20 | 2006-01-26,3578.92,3641.42,3577.98,3641.42,0,0
21 | 2006-01-27,3643.35,3685.48,3643.35,3685.48,0,0
22 | 2006-01-30,3684.38,3685.65,3664.45,3677.52,0,0
23 | 2006-01-31,3676.71,3707.63,3671.67,3691.41,0,0
24 | 2006-02-01,3686.16,3728.80,3674.89,3728.25,0,0
25 | 2006-02-02,3728.92,3745.14,3677.05,3677.05,0,0
26 | 2006-02-03,3677.05,3696.00,3652.76,3678.48,0,0
27 | 2006-02-06,3678.87,3704.17,3672.53,3682.32,0,0
28 | 2006-02-07,3682.97,3698.63,3656.20,3680.80,0,0
29 | 2006-02-08,3680.05,3680.05,3637.93,3671.37,0,0
30 | 2006-02-09,3672.34,3726.81,3672.34,3726.81,0,0
31 | 2006-02-10,3725.18,3735.14,3692.63,3695.63,0,0
32 | 2006-02-13,3696.09,3727.46,3684.83,3727.46,0,0
33 | 2006-02-14,3728.16,3744.66,3707.25,3734.48,0,0
34 | 2006-02-15,3733.97,3749.36,3720.41,3729.79,0,0
35 | 2006-02-16,3730.82,3756.47,3730.82,3756.47,0,0
36 | 2006-02-17,3757.34,3777.16,3749.94,3767.70,0,0
37 | 2006-02-20,3767.11,3769.16,3749.88,3766.74,0,0
38 | 2006-02-21,3767.21,3800.78,3767.21,3779.51,0,0
39 | 2006-02-22,3778.02,3818.48,3771.06,3818.48,0,0
40 | 2006-02-23,3819.56,3831.16,3796.21,3813.29,0,0
41 | 2006-02-24,3812.76,3826.00,3805.55,3826.00,0,0
42 | 2006-02-27,3828.99,3840.56,3819.65,3840.56,0,0
43 | 2006-02-28,3840.31,3840.31,3769.25,3774.51,0,0
44 | 2006-03-01,3775.23,3806.34,3772.49,3806.03,0,0
45 | 2006-03-02,3807.30,3820.55,3745.46,3763.73,0,0
46 | 2006-03-03,3763.95,3774.03,3715.35,3733.95,0,0
47 | 2006-03-06,3737.58,3766.47,3737.58,3754.07,0,0
48 | 2006-03-07,3751.30,3751.30,3719.92,3745.20,0,0
49 | 2006-03-08,3745.10,3757.16,3702.04,3727.96,0,0
50 | 2006-03-09,3736.61,3765.56,3736.61,3757.59,0,0
51 | 2006-03-10,3754.13,3798.46,3741.51,3798.46,0,0
52 | 2006-03-13,3801.03,3827.45,3801.03,3824.97,0,0
53 | 2006-03-14,3823.18,3833.48,3808.96,3833.48,0,0
54 | 2006-03-15,3834.11,3853.33,3834.11,3842.16,0,0
55 | 2006-03-16,3844.15,3847.88,3822.56,3839.71,0,0
56 | 2006-03-17,3840.20,3874.64,3820.50,3832.43,0,0
57 | 2006-03-20,3833.25,3863.95,3833.11,3842.03,0,0
58 | 2006-03-21,3842.49,3848.17,3811.02,3848.17,0,0
59 | 2006-03-22,3840.27,3872.62,3827.40,3868.48,0,0
60 | 2006-03-23,3869.22,3878.49,3850.46,3860.13,0,0
61 | 2006-03-24,3859.58,3875.01,3853.43,3870.89,0,0
62 | 2006-03-27,3872.28,3872.28,3826.49,3828.53,0,0
63 | 2006-03-28,3829.82,3846.52,3799.04,3811.45,0,0
64 | 2006-03-29,3811.85,3830.70,3799.12,3826.30,0,0
65 | 2006-03-30,3835.21,3881.69,3835.21,3874.61,0,0
66 | 2006-03-31,3872.37,3872.37,3840.64,3853.74,0,0
67 | 2006-04-03,3859.99,3881.11,3857.23,3878.64,0,0
68 | 2006-04-04,3875.08,3875.08,3843.18,3850.11,0,0
69 | 2006-04-05,3853.28,3865.82,3835.35,3863.92,0,0
70 | 2006-04-06,3866.01,3879.70,3848.73,3861.29,0,0
71 | 2006-04-07,3860.03,3874.59,3822.26,3823.11,0,0
72 | 2006-04-10,3822.35,3843.52,3813.80,3843.52,0,0
73 | 2006-04-11,3840.89,3843.62,3781.99,3788.81,0,0
74 | 2006-04-12,3786.93,3791.15,3753.47,3776.94,0,0
75 | 2006-04-13,3777.24,3787.52,3755.69,3779.94,0,0
76 | 2006-04-18,3779.23,3779.23,3749.71,3770.79,0,0
77 | 2006-04-19,3778.46,3825.18,3778.46,3820.96,0,0
78 | 2006-04-20,3820.93,3878.29,3820.93,3860.00,0,0
79 | 2006-04-21,3863.57,3892.35,3863.57,3888.46,0,0
80 | 2006-04-24,3884.57,3884.57,3858.67,3862.27,0,0
81 | 2006-04-25,3864.64,3888.65,3860.61,3871.09,0,0
82 | 2006-04-26,3873.67,3892.16,3873.06,3887.00,0,0
83 | 2006-04-27,3889.43,3889.43,3832.10,3865.42,0,0
84 | 2006-04-28,3865.91,3865.91,3833.74,3839.90,0,0
85 | 2006-05-02,3839.24,3864.19,3830.96,3862.24,0,0
86 | 2006-05-03,3865.29,3879.31,3817.60,3821.97,0,0
87 | 2006-05-04,3822.57,3843.66,3806.35,3843.08,0,0
88 | 2006-05-05,3845.32,3874.32,3836.65,3874.32,0,0
89 | 2006-05-08,3877.74,3897.40,3872.67,3877.53,0,0
90 | 2006-05-09,3879.59,3890.94,3866.35,3890.94,0,0
91 | 2006-05-10,3883.38,3889.78,3863.56,3863.56,0,0
92 | 2006-05-11,3864.02,3894.60,3836.67,3837.86,0,0
93 | 2006-05-12,3829.82,3829.82,3750.44,3750.44,0,0
94 | 2006-05-15,3746.40,3746.40,3680.95,3711.16,0,0
95 | 2006-05-16,3711.46,3750.12,3692.35,3730.36,0,0
96 | 2006-05-17,3734.32,3750.42,3605.19,3605.37,0,0
97 | 2006-05-18,3607.41,3649.54,3558.27,3606.33,0,0
98 | 2006-05-19,3608.26,3638.38,3601.68,3625.33,0,0
99 | 2006-05-22,3622.35,3622.35,3527.05,3539.77,0,0
100 | 2006-05-23,3541.56,3637.39,3541.56,3620.28,0,0
101 | 2006-05-24,3617.11,3617.11,3542.93,3574.86,0,0
102 | 2006-05-25,3579.36,3635.00,3555.18,3635.00,0,0
103 | 2006-05-26,3647.15,3699.80,3646.42,3699.80,0,0
104 | 2006-05-29,3696.48,3696.48,3677.02,3679.57,0,0
105 | 2006-05-30,3677.67,3683.30,3581.65,3590.91,0,0
106 | 2006-05-31,3581.80,3641.83,3542.41,3637.17,0,0
107 | 2006-06-01,3634.82,3652.84,3595.27,3648.33,0,0
108 | 2006-06-02,3656.43,3688.89,3622.96,3636.89,0,0
109 | 2006-06-05,3636.83,3638.59,3592.71,3604.33,0,0
110 | 2006-06-06,3598.58,3598.58,3519.86,3529.10,0,0
111 | 2006-06-07,3536.39,3575.67,3512.25,3562.36,0,0
112 | 2006-06-08,3556.87,3556.87,3462.37,3462.37,0,0
113 | 2006-06-09,3470.27,3531.70,3470.27,3520.99,0,0
114 | 2006-06-12,3519.43,3528.27,3477.06,3480.76,0,0
115 | 2006-06-13,3476.33,3476.33,3392.75,3408.02,0,0
116 | 2006-06-14,3410.79,3433.72,3379.66,3414.21,0,0
117 | 2006-06-15,3423.23,3496.64,3423.23,3493.25,0,0
118 | 2006-06-16,3508.39,3544.27,3459.56,3463.56,0,0
119 | 2006-06-19,3469.88,3520.51,3469.88,3490.24,0,0
120 | 2006-06-20,3474.60,3514.83,3453.14,3514.83,0,0
121 | 2006-06-21,3519.86,3526.86,3476.22,3526.84,0,0
122 | 2006-06-22,3542.65,3571.24,3523.72,3544.85,0,0
123 | 2006-06-23,3545.60,3564.06,3530.00,3550.15,0,0
124 | 2006-06-26,3554.07,3566.55,3528.59,3534.84,0,0
125 | 2006-06-27,3540.49,3555.94,3500.72,3506.93,0,0
126 | 2006-06-28,3503.30,3526.09,3484.71,3506.07,0,0
127 | 2006-06-29,3519.54,3583.90,3519.54,3582.61,0,0
128 | 2006-06-30,3592.01,3655.02,3592.01,3648.92,0,0
129 | 2006-07-03,3648.91,3662.92,3639.07,3662.92,0,0
130 | 2006-07-04,3664.59,3670.75,3646.04,3670.75,0,0
131 | 2006-07-05,3656.71,3656.71,3607.81,3618.64,0,0
132 | 2006-07-06,3624.02,3665.54,3624.02,3662.39,0,0
133 | 2006-07-07,3657.00,3670.45,3627.02,3651.33,0,0
134 | 2006-07-10,3645.42,3671.09,3621.34,3666.51,0,0
135 | 2006-07-11,3656.57,3656.65,3609.05,3617.78,0,0
136 | 2006-07-12,3632.02,3662.83,3622.26,3630.50,0,0
137 | 2006-07-13,3617.55,3617.55,3552.52,3562.56,0,0
138 | 2006-07-14,3545.92,3552.04,3508.25,3508.25,0,0
139 | 2006-07-17,3512.22,3518.34,3462.77,3498.62,0,0
140 | 2006-07-18,3491.81,3516.31,3475.98,3492.11,0,0
141 | 2006-07-19,3497.48,3585.65,3497.48,3585.65,0,0
142 | 2006-07-20,3593.87,3612.48,3580.86,3589.63,0,0
143 | 2006-07-21,3580.53,3590.68,3546.24,3557.08,0,0
144 | 2006-07-24,3559.34,3633.50,3559.34,3632.93,0,0
145 | 2006-07-25,3639.65,3651.74,3621.71,3631.50,0,0
146 | 2006-07-26,3635.17,3647.02,3625.07,3640.75,0,0
147 | 2006-07-27,3649.29,3681.55,3649.29,3681.55,0,0
148 | 2006-07-28,3671.71,3711.41,3659.67,3710.60,0,0
149 | 2006-07-31,3708.82,3711.52,3688.22,3691.87,0,0
150 | 2006-08-01,3687.82,3696.52,3632.51,3640.60,0,0
151 | 2006-08-02,3655.93,3696.77,3655.93,3696.35,0,0
152 | 2006-08-03,3695.86,3703.38,3647.96,3667.91,0,0
153 | 2006-08-04,3677.44,3729.29,3677.44,3718.09,0,0
154 | 2006-08-07,3707.49,3707.49,3654.09,3659.03,0,0
155 | 2006-08-08,3672.22,3684.78,3654.51,3668.10,0,0
156 | 2006-08-09,3674.04,3712.22,3651.29,3707.19,0,0
157 | 2006-08-10,3686.63,3686.63,3638.55,3675.44,0,0
158 | 2006-08-11,3682.86,3698.24,3659.10,3675.10,0,0
159 | 2006-08-14,3690.09,3720.39,3690.09,3719.11,0,0
160 | 2006-08-15,3712.47,3773.87,3706.87,3766.38,0,0
161 | 2006-08-16,3767.86,3798.63,3765.45,3790.94,0,0
162 | 2006-08-17,3792.00,3801.01,3779.32,3800.10,0,0
163 | 2006-08-18,3798.33,3807.48,3781.99,3791.40,0,0
164 | 2006-08-21,3789.99,3790.58,3765.38,3777.25,0,0
165 | 2006-08-22,3788.55,3797.51,3754.38,3792.55,0,0
166 | 2006-08-23,3793.49,3793.49,3753.04,3758.98,0,0
167 | 2006-08-24,3761.86,3796.84,3743.26,3781.87,0,0
168 | 2006-08-25,3784.01,3797.91,3766.21,3781.17,0,0
169 | 2006-08-28,3778.79,3811.84,3758.87,3808.57,0,0
170 | 2006-08-29,3810.18,3829.39,3800.05,3806.81,0,0
171 | 2006-08-30,3815.88,3829.40,3809.02,3817.86,0,0
172 | 2006-08-31,3823.70,3828.06,3802.39,3808.70,0,0
173 | 2006-09-01,3808.99,3836.22,3808.99,3820.89,0,0
174 | 2006-09-04,3824.02,3839.30,3824.02,3837.61,0,0
175 | 2006-09-05,3835.82,3835.82,3801.14,3817.76,0,0
176 | 2006-09-06,3818.12,3818.36,3765.73,3772.21,0,0
177 | 2006-09-07,3766.80,3766.80,3729.77,3739.70,0,0
178 | 2006-09-08,3745.99,3762.09,3736.31,3750.08,0,0
179 | 2006-09-11,3745.78,3745.78,3709.81,3742.06,0,0
180 | 2006-09-12,3744.91,3792.73,3729.36,3788.96,0,0
181 | 2006-09-13,3799.86,3810.07,3787.11,3805.55,0,0
182 | 2006-09-14,3809.08,3824.77,3786.70,3796.65,0,0
183 | 2006-09-15,3800.99,3825.15,3789.18,3812.11,0,0
184 | 2006-09-18,3813.73,3823.92,3790.83,3808.47,0,0
185 | 2006-09-19,3807.67,3811.25,3770.36,3780.18,0,0
186 | 2006-09-20,3782.15,3843.26,3775.48,3841.31,0,0
187 | 2006-09-21,3840.20,3867.74,3831.23,3857.14,0,0
188 | 2006-09-22,3839.51,3839.65,3800.65,3812.73,0,0
189 | 2006-09-25,3815.13,3842.67,3802.47,3822.12,0,0
190 | 2006-09-26,3838.00,3877.79,3838.00,3872.92,0,0
191 | 2006-09-27,3877.55,3899.04,3871.12,3896.18,0,0
192 | 2006-09-28,3893.86,3907.41,3885.32,3894.98,0,0
193 | 2006-09-29,3898.07,3921.15,3894.87,3899.41,0,0
194 | 2006-10-02,3902.03,3917.40,3875.76,3892.48,0,0
195 | 2006-10-03,3886.09,3886.09,3858.87,3880.14,0,0
196 | 2006-10-04,3884.39,3914.73,3883.38,3914.73,0,0
197 | 2006-10-05,3921.17,3949.47,3921.17,3939.86,0,0
198 | 2006-10-06,3939.28,3950.06,3919.88,3940.31,0,0
199 | 2006-10-09,3932.33,3942.17,3921.81,3939.48,0,0
200 | 2006-10-10,3946.55,3963.20,3943.35,3960.67,0,0
201 | 2006-10-11,3956.15,3969.72,3939.78,3967.39,0,0
202 | 2006-10-12,3966.39,4000.49,3964.44,3999.93,0,0
203 | 2006-10-13,4002.28,4008.67,3986.41,3999.07,0,0
204 | 2006-10-16,4000.30,4007.38,3987.52,4001.97,0,0
205 | 2006-10-17,3993.04,3993.33,3947.39,3949.57,0,0
206 | 2006-10-18,3958.29,4007.17,3958.29,3991.38,0,0
207 | 2006-10-19,3986.30,4000.76,3967.98,3986.82,0,0
208 | 2006-10-20,3991.86,4016.63,3981.18,3998.19,0,0
209 | 2006-10-23,4001.63,4024.75,3982.02,4019.02,0,0
210 | 2006-10-24,4018.21,4022.87,4003.96,4014.01,0,0
211 | 2006-10-25,4011.18,4025.56,4004.86,4019.14,0,0
212 | 2006-10-26,4026.47,4047.54,4019.98,4027.29,0,0
213 | 2006-10-27,4029.07,4039.77,3998.43,4017.27,0,0
214 | 2006-10-30,4007.26,4007.26,3979.81,4004.92,0,0
215 | 2006-10-31,4003.92,4019.84,3990.01,4004.80,0,0
216 | 2006-11-01,4003.80,4029.57,3999.78,4014.34,0,0
217 | 2006-11-02,4003.97,4010.72,3961.64,3974.62,0,0
218 | 2006-11-03,3979.73,4010.44,3971.83,3990.46,0,0
219 | 2006-11-06,3991.47,4045.22,3991.47,4045.22,0,0
220 | 2006-11-07,4047.63,4075.99,4045.52,4072.86,0,0
221 | 2006-11-08,4064.92,4078.99,4047.19,4073.81,0,0
222 | 2006-11-09,4071.17,4081.70,4059.21,4073.00,0,0
223 | 2006-11-10,4067.10,4072.42,4048.97,4063.84,0,0
224 | 2006-11-13,4063.01,4095.55,4059.51,4086.14,0,0
225 | 2006-11-14,4087.11,4097.05,4068.51,4084.33,0,0
226 | 2006-11-15,4089.39,4110.53,4089.39,4108.83,0,0
227 | 2006-11-16,4107.71,4116.79,4096.67,4109.71,0,0
228 | 2006-11-17,4106.78,4107.24,4066.05,4078.36,0,0
229 | 2006-11-20,4074.59,4101.04,4049.44,4096.74,0,0
230 | 2006-11-21,4095.27,4112.27,4090.91,4096.06,0,0
231 | 2006-11-22,4105.91,4118.40,4084.71,4094.97,0,0
232 | 2006-11-23,4099.96,4105.18,4070.31,4085.76,0,0
233 | 2006-11-24,4076.14,4078.44,4028.30,4048.16,0,0
234 | 2006-11-27,4045.05,4053.68,3978.25,3978.25,0,0
235 | 2006-11-28,3976.16,3990.75,3951.94,3975.11,0,0
236 | 2006-11-29,3983.51,4023.89,3983.51,4023.09,0,0
237 | 2006-11-30,4027.46,4036.72,3983.05,3987.23,0,0
238 | 2006-12-01,3993.03,4011.96,3914.46,3932.09,0,0
239 | 2006-12-04,3935.81,3965.16,3927.40,3962.93,0,0
240 | 2006-12-05,3966.61,4014.55,3961.06,4007.94,0,0
241 | 2006-12-06,4007.75,4015.80,3987.15,4002.31,0,0
242 | 2006-12-07,3997.09,4039.25,3991.84,4018.69,0,0
243 | 2006-12-08,4011.63,4028.14,3980.66,4019.89,0,0
244 | 2006-12-11,4024.14,4055.74,4024.14,4052.89,0,0
245 | 2006-12-12,4052.55,4062.20,4044.02,4059.74,0,0
246 | 2006-12-13,4063.14,4096.28,4054.64,4094.33,0,0
247 | 2006-12-14,4100.49,4122.89,4099.98,4118.84,0,0
248 | 2006-12-15,4119.08,4147.38,4119.08,4140.66,0,0
249 | 2006-12-18,4140.99,4141.46,4129.65,4130.06,0,0
250 | 2006-12-19,4121.01,4121.01,4085.18,4100.48,0,0
251 | 2006-12-20,4108.30,4130.80,4108.30,4118.54,0,0
252 | 2006-12-21,4111.85,4125.27,4104.46,4112.10,0,0
253 | 2006-12-22,4109.86,4109.86,4072.62,4073.50,0,0
254 | 2006-12-27,4079.70,4134.86,4079.70,4134.86,0,0
255 | 2006-12-28,4137.44,4142.06,4125.14,4130.66,0,0
256 | 2006-12-29,4130.12,4142.01,4119.94,4119.94,0,0
257 |
--------------------------------------------------------------------------------
/demos/different_line_types.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 |
7 |
8 | class MyStrategy(bt.Strategy):
9 | def __init__(self):
10 | sma1 = bt.ind.SMA(period=11, subplot=True)
11 | sma2 = bt.ind.SMA(period=17, plotmaster=sma1)
12 | sma3 = bt.ind.SMA(sma2, period=5)
13 | rsi = bt.ind.RSI()
14 | cross = bt.ind.CrossOver(sma1, sma2)
15 | a = bt.ind.And(sma1 > sma2, cross)
16 |
17 | def next(self):
18 | pos = len(self.data)
19 | if pos == 45 or pos == 145:
20 | self.buy(self.datas[0], size=None)
21 |
22 | if pos == 116 or pos == 215:
23 | self.sell(self.datas[0], size=None)
24 |
25 |
26 | if __name__ == '__main__':
27 | cerebro = bt.Cerebro()
28 |
29 | cerebro.addstrategy(MyStrategy)
30 |
31 | data = bt.feeds.YahooFinanceCSVData(
32 | dataname="datas/orcl-1995-2014.txt",
33 | fromdate=datetime.datetime(2000, 1, 1),
34 | todate=datetime.datetime(2001, 2, 28),
35 | reverse=False,
36 | swapcloses=True,
37 | )
38 | cerebro.adddata(data)
39 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
40 |
41 | cerebro.run()
42 |
43 | p = BacktraderPlotting(style='bar')
44 | cerebro.plot(p)
45 |
--------------------------------------------------------------------------------
/demos/multiple_datas_on_one.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 |
7 |
8 | cerebro = bt.Cerebro()
9 |
10 | data = bt.feeds.YahooFinanceCSVData(
11 | dataname="datas/orcl-1995-2014.txt",
12 | fromdate=datetime.datetime(2000, 1, 1),
13 | todate=datetime.datetime(2001, 2, 28),
14 | reverse=False,
15 | swapcloses=True,
16 | )
17 | cerebro.adddata(data)
18 | data1 = cerebro.resampledata(data, timeframe=bt.TimeFrame.Weeks, compression=1)
19 | data1.plotinfo.plotmaster = data
20 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
21 |
22 | cerebro.run()
23 |
24 | p = BacktraderPlotting(style='bar')
25 | cerebro.plot(p)
26 |
--------------------------------------------------------------------------------
/demos/optimization.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting, BacktraderPlottingOptBrowser
6 | from btplotting.schemes import Tradimo
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | params = (
11 | ('buydate', 21),
12 | ('holdtime', 20),
13 | )
14 |
15 | def __init__(self):
16 | sma1 = bt.indicators.SMA(period=11, subplot=True)
17 | bt.indicators.SMA(period=17, plotmaster=sma1)
18 | bt.indicators.RSI()
19 |
20 | def next(self):
21 | pos = len(self.data)
22 | if pos == self.p.buydate:
23 | self.buy(self.datas[0], size=None)
24 |
25 | if pos == self.p.buydate + self.p.holdtime:
26 | self.sell(self.datas[0], size=None)
27 |
28 |
29 | if __name__ == '__main__':
30 | cerebro = bt.Cerebro(maxcpus=1)
31 |
32 | data = bt.feeds.YahooFinanceCSVData(
33 | dataname="datas/orcl-1995-2014.txt",
34 | fromdate=datetime.datetime(2000, 1, 1),
35 | todate=datetime.datetime(2001, 2, 28),
36 | reverse=False,
37 | swapcloses=True,
38 | )
39 | cerebro.adddata(data)
40 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
41 |
42 | cerebro.optstrategy(MyStrategy, buydate=range(40, 180, 30))
43 |
44 | result = cerebro.run(optreturn=False)
45 |
46 | btp = BacktraderPlotting(style='bar', scheme=Tradimo())
47 | browser = BacktraderPlottingOptBrowser(btp, result)
48 | browser.start()
49 |
--------------------------------------------------------------------------------
/demos/optimization_columns.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting, BacktraderPlottingOptBrowser
6 | from btplotting.schemes import Tradimo
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | params = (
11 | ('buydate', 21),
12 | ('holdtime', 20),
13 | )
14 |
15 | def __init__(self):
16 | sma1 = bt.indicators.SMA(period=11, subplot=True)
17 | bt.indicators.SMA(period=17, plotmaster=sma1)
18 | bt.indicators.RSI()
19 |
20 | def next(self):
21 | pos = len(self.data)
22 | if pos == self.p.buydate:
23 | self.buy(self.datas[0], size=None)
24 |
25 | if pos == self.p.buydate + self.p.holdtime:
26 | self.sell(self.datas[0], size=None)
27 |
28 |
29 | if __name__ == '__main__':
30 | cerebro = bt.Cerebro(maxcpus=1)
31 |
32 | data = bt.feeds.YahooFinanceCSVData(
33 | dataname="datas/orcl-1995-2014.txt",
34 | fromdate=datetime.datetime(2000, 1, 1),
35 | todate=datetime.datetime(2001, 2, 28),
36 | reverse=False,
37 | swapcloses=True,
38 | )
39 | cerebro.adddata(data)
40 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
41 |
42 | cerebro.optstrategy(MyStrategy, buydate=range(40, 180, 30))
43 |
44 | result = cerebro.run(optreturn=False)
45 |
46 | def get_pnl_gross(strats):
47 | a = strats[0].analyzers.tradeanalyzer.get_analysis()
48 | return a.pnl.gross.total if 'pnl' in a else 0
49 |
50 | btp = BacktraderPlotting(style='bar', scheme=Tradimo())
51 | browser = BacktraderPlottingOptBrowser(btp, result, usercolumns=dict(
52 | pnl=get_pnl_gross), sortcolumn='pnl', sortasc=False)
53 | browser.start()
54 |
--------------------------------------------------------------------------------
/demos/ordered_optimization.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting, BacktraderPlottingOptBrowser
6 | from btplotting.schemes import Tradimo
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | params = (
11 | ('buydate', 21),
12 | ('holdtime', 20),
13 | )
14 |
15 | def __init__(self):
16 | sma1 = bt.indicators.SMA(period=11, subplot=True)
17 | bt.indicators.SMA(period=17, plotmaster=sma1)
18 | bt.indicators.RSI()
19 |
20 | def next(self):
21 | pos = len(self.data)
22 | if pos == self.p.buydate:
23 | self.buy(self.datas[0], size=None)
24 |
25 | if pos == self.p.buydate + self.p.holdtime:
26 | self.sell(self.datas[0], size=None)
27 |
28 |
29 | if __name__ == '__main__':
30 | cerebro = bt.Cerebro(maxcpus=1)
31 |
32 | data = bt.feeds.YahooFinanceCSVData(
33 | dataname="datas/orcl-1995-2014.txt",
34 | fromdate=datetime.datetime(2000, 1, 1),
35 | todate=datetime.datetime(2001, 2, 28),
36 | reverse=False,
37 | swapcloses=True,
38 | )
39 | cerebro.adddata(data)
40 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
41 |
42 | cerebro.optstrategy(MyStrategy, buydate=range(40, 180, 30))
43 |
44 | optres = cerebro.run(optreturn=False)
45 |
46 | def df(optresults):
47 | a = [x.analyzers.tradeanalyzer.get_analysis() for x in optresults]
48 | return sum([x.pnl.gross.total if 'pnl' in x else 0 for x in a])
49 |
50 | usercolumns = {'Profit & Loss': df}
51 |
52 | btp = BacktraderPlotting(style='bar', scheme=Tradimo())
53 | browser = BacktraderPlottingOptBrowser(
54 | btp, optres, usercolumns=usercolumns, sortcolumn='Profit & Loss', sortasc=False)
55 |
56 | browser.start()
57 |
--------------------------------------------------------------------------------
/demos/plot-same-axis.py:
--------------------------------------------------------------------------------
1 | from __future__ import (absolute_import, division, print_function,
2 | unicode_literals)
3 |
4 | import argparse
5 | import datetime
6 |
7 | # The above could be sent to an independent module
8 | import backtrader as bt
9 | import backtrader.feeds as btfeeds
10 | import backtrader.indicators as btind
11 |
12 | from btplotting import BacktraderPlotting
13 |
14 | '''
15 | https://www.backtrader.com/blog/posts/2015-09-21-plotting-same-axis/plotting-same-axis/
16 | '''
17 |
18 | class PlotStrategy(bt.Strategy):
19 | '''
20 | The strategy does nothing but create indicators for plotting purposes
21 | '''
22 | params = dict(
23 | smasubplot=False, # default for Moving averages
24 | nomacdplot=False,
25 | rsioverstoc=False,
26 | rsioversma=False,
27 | stocrsi=False,
28 | stocrsilabels=False,
29 | )
30 |
31 | def __init__(self):
32 | sma = btind.SMA(subplot=self.params.smasubplot)
33 |
34 | macd = btind.MACD()
35 | # In SMA we passed plot directly as kwarg, here the plotinfo.plot
36 | # attribute is changed - same effect
37 | macd.plotinfo.plot = not self.params.nomacdplot
38 |
39 | # Let's put rsi on stochastic/sma or the other way round
40 | stoc = btind.Stochastic()
41 | rsi = btind.RSI()
42 | if self.params.stocrsi:
43 | stoc.plotinfo.plotmaster = rsi
44 | stoc.plotinfo.plotlinelabels = self.p.stocrsilabels
45 | elif self.params.rsioverstoc:
46 | rsi.plotinfo.plotmaster = stoc
47 | elif self.params.rsioversma:
48 | rsi.plotinfo.plotmaster = sma
49 |
50 |
51 | def runstrategy():
52 | args = parse_args()
53 |
54 | # Create a cerebro
55 | cerebro = bt.Cerebro()
56 |
57 | # Get the dates from the args
58 | fromdate = datetime.datetime.strptime(args.fromdate, '%Y-%m-%d')
59 | todate = datetime.datetime.strptime(args.todate, '%Y-%m-%d')
60 |
61 | # Create the 1st data
62 | data = btfeeds.BacktraderCSVData(
63 | dataname=args.data,
64 | fromdate=fromdate,
65 | todate=todate)
66 |
67 | # Add the 1st data to cerebro
68 | cerebro.adddata(data)
69 |
70 | # Add the strategy
71 | cerebro.addstrategy(PlotStrategy,
72 | smasubplot=args.smasubplot,
73 | nomacdplot=args.nomacdplot,
74 | rsioverstoc=args.rsioverstoc,
75 | rsioversma=args.rsioversma,
76 | stocrsi=args.stocrsi,
77 | stocrsilabels=args.stocrsilabels)
78 |
79 | # And run it
80 | cerebro.run(stdstats=args.stdstats)
81 |
82 | # Plot
83 | p = BacktraderPlotting()
84 | cerebro.plot(p, volume=False)
85 |
86 |
87 | def parse_args():
88 | parser = argparse.ArgumentParser(description='Plotting Example')
89 |
90 | parser.add_argument('--data', '-d',
91 | default='datas/2006-day-001.txt',
92 | help='data to add to the system')
93 |
94 | parser.add_argument('--fromdate', '-f',
95 | default='2006-01-01',
96 | help='Starting date in YYYY-MM-DD format')
97 |
98 | parser.add_argument('--todate', '-t',
99 | default='2006-12-31',
100 | help='Starting date in YYYY-MM-DD format')
101 |
102 | parser.add_argument('--stdstats', '-st', action='store_true',
103 | help='Show standard observers')
104 |
105 | parser.add_argument('--smasubplot', '-ss', action='store_true',
106 | help='Put SMA on own subplot/axis')
107 |
108 | parser.add_argument('--nomacdplot', '-nm', action='store_true',
109 | help='Hide the indicator from the plot')
110 |
111 | group = parser.add_mutually_exclusive_group(required=False)
112 |
113 | group.add_argument('--rsioverstoc', '-ros', action='store_true',
114 | help='Plot the RSI indicator on the Stochastic axis')
115 |
116 | group.add_argument('--rsioversma', '-rom', action='store_true',
117 | help='Plot the RSI indicator on the SMA axis')
118 |
119 | group.add_argument('--stocrsi', '-strsi', action='store_true',
120 | help='Plot the Stochastic indicator on the RSI axis')
121 |
122 | parser.add_argument('--stocrsilabels', action='store_true',
123 | help='Plot line names instead of indicator name')
124 |
125 | return parser.parse_args()
126 |
127 |
128 | if __name__ == '__main__':
129 | runstrategy()
130 |
--------------------------------------------------------------------------------
/demos/same-axis.py:
--------------------------------------------------------------------------------
1 | from __future__ import (absolute_import, division, print_function,
2 | unicode_literals)
3 |
4 | import argparse
5 | import random
6 | import backtrader as bt
7 |
8 | from btplotting import BacktraderPlotting
9 |
10 | '''
11 | https://www.backtrader.com/docu/plotting/sameaxis/plot-sameaxis/
12 | '''
13 |
14 | # The filter which changes the close price
15 | def close_changer(data, *args, **kwargs):
16 | data.close[0] += 50.0 * random.randint(-1, 1)
17 | return False # length of stream is unchanged
18 |
19 |
20 | # override the standard markers
21 | class BuySellArrows(bt.observers.BuySell):
22 | plotlines = dict(buy=dict(marker='$\u21E7$', markersize=12.0),
23 | sell=dict(marker='$\u21E9$', markersize=12.0))
24 |
25 |
26 | class St(bt.Strategy):
27 | def __init__(self):
28 | bt.obs.BuySell(self.data0, barplot=True) # done here for
29 | BuySellArrows(self.data1, barplot=True) # different markers per data
30 |
31 | def next(self):
32 | if not self.position:
33 | if random.randint(0, 1):
34 | self.buy(data=self.data0)
35 | self.entered = len(self)
36 |
37 | else: # in the market
38 | if (len(self) - self.entered) >= 10:
39 | self.sell(data=self.data1)
40 |
41 |
42 | def runstrat(args=None):
43 | args = parse_args(args)
44 | cerebro = bt.Cerebro()
45 |
46 | dataname = 'datas/2006-day-001.txt' # data feed
47 |
48 | data0 = bt.feeds.BacktraderCSVData(dataname=dataname, name='data0')
49 | cerebro.adddata(data0)
50 |
51 | data1 = bt.feeds.BacktraderCSVData(dataname=dataname, name='data1')
52 | data1.addfilter(close_changer)
53 | if not args.no_comp:
54 | data1.compensate(data0)
55 | data1.plotinfo.plotmaster = data0
56 | if args.sameaxis:
57 | data1.plotinfo.sameaxis = True
58 | cerebro.adddata(data1)
59 |
60 | cerebro.addstrategy(St) # sample strategy
61 |
62 | cerebro.addobserver(bt.obs.Broker) # removed below with stdstats=False
63 | cerebro.addobserver(bt.obs.Trades) # removed below with stdstats=False
64 |
65 | cerebro.broker.set_coc(True)
66 | cerebro.run(stdstats=False) # execute
67 | p = BacktraderPlotting()
68 | cerebro.plot(p, volume=False) # and plot
69 |
70 |
71 | def parse_args(pargs=None):
72 | parser = argparse.ArgumentParser(
73 | formatter_class=argparse.ArgumentDefaultsHelpFormatter,
74 | description=('Compensation example'))
75 |
76 | parser.add_argument('--no-comp', required=False, action='store_true')
77 | parser.add_argument('--sameaxis', required=False, action='store_true')
78 | return parser.parse_args(pargs)
79 |
80 |
81 | if __name__ == '__main__':
82 | runstrat()
83 |
--------------------------------------------------------------------------------
/demos/tabs.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 | from btplotting.tabs import MetadataTab
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def __init__(self):
11 | sma1 = bt.indicators.SMA(period=11, subplot=True)
12 | bt.indicators.SMA(period=17, plotmaster=sma1)
13 | bt.indicators.RSI()
14 |
15 | def next(self):
16 | pos = len(self.data)
17 | if pos == 45 or pos == 145:
18 | self.buy(self.datas[0], size=None)
19 |
20 | if pos == 116 or pos == 215:
21 | self.sell(self.datas[0], size=None)
22 |
23 |
24 | if __name__ == '__main__':
25 | cerebro = bt.Cerebro()
26 |
27 | cerebro.addstrategy(MyStrategy)
28 |
29 | data = bt.feeds.YahooFinanceCSVData(
30 | dataname="datas/orcl-1995-2014.txt",
31 | fromdate=datetime.datetime(2000, 1, 1),
32 | todate=datetime.datetime(2001, 2, 28),
33 | reverse=False,
34 | swapcloses=True,
35 | )
36 | cerebro.adddata(data)
37 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
38 |
39 | cerebro.run()
40 |
41 | p = BacktraderPlotting(use_default_tabs=False, tabs=[MetadataTab])
42 | cerebro.plot(p)
43 |
--------------------------------------------------------------------------------
/demos/tradimo_single.py:
--------------------------------------------------------------------------------
1 | import datetime
2 |
3 | import backtrader as bt
4 |
5 | from btplotting import BacktraderPlotting
6 | from btplotting.schemes import Tradimo
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def __init__(self):
11 | sma1 = bt.indicators.SMA(period=11, subplot=True)
12 | bt.indicators.SMA(period=17, plotmaster=sma1)
13 | bt.indicators.RSI()
14 |
15 | def next(self):
16 | pos = len(self.data)
17 | if pos == 45 or pos == 145:
18 | self.buy(self.datas[0], size=None)
19 |
20 | if pos == 116 or pos == 215:
21 | self.sell(self.datas[0], size=None)
22 |
23 |
24 | if __name__ == '__main__':
25 | cerebro = bt.Cerebro()
26 |
27 | cerebro.addstrategy(MyStrategy)
28 |
29 | data = bt.feeds.YahooFinanceCSVData(
30 | dataname="datas/orcl-1995-2014.txt",
31 | fromdate=datetime.datetime(2000, 1, 1),
32 | todate=datetime.datetime(2001, 2, 28),
33 | reverse=False,
34 | swapcloses=True,
35 | )
36 | cerebro.adddata(data)
37 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
38 |
39 | cerebro.run()
40 |
41 | p = BacktraderPlotting(style='bar', scheme=Tradimo())
42 | cerebro.plot(p)
43 |
--------------------------------------------------------------------------------
/requirements-test.txt:
--------------------------------------------------------------------------------
1 | freezegun
2 | pytest
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | backtrader
2 | matplotlib
3 | pandas
4 | jinja2
5 | bokeh>=3.1.0
6 | selenium>=4.4.3
7 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import setuptools
2 | from distutils.util import convert_path
3 |
4 | with open("README.md", "r") as fh:
5 | long_description = fh.read()
6 |
7 | main_ns = {}
8 | ver_path = convert_path('btplotting/version.py')
9 | with open(ver_path) as ver_file:
10 | exec(ver_file.read(), main_ns)
11 |
12 | setuptools.setup(
13 | name='btplotting',
14 | version=main_ns['__version__'],
15 | description='Plotting package for Backtrader (Bokeh)',
16 | python_requires='>=3.6',
17 | author='happydasch',
18 | author_email='daniel@vcard24.de',
19 | long_description=long_description,
20 | long_description_content_type="text/markdown",
21 | license='GPLv3+',
22 | url="https://github.com/happydasch/btplotting",
23 | project_urls={
24 | "Bug Tracker": "https://github.com/happydasch/btplotting/issues",
25 | "Documentation": "https://github.com/happydasch/btplotting/wiki",
26 | "Source Code": "https://github.com/happydasch/btplotting",
27 | "Demos": "https://github.com/happydasch/btplotting/tree/gh-pages",
28 | },
29 |
30 | # What does your project relate to?
31 | keywords=['trading', 'development', 'plotting', 'backtrader'],
32 |
33 | packages=setuptools.find_packages(),
34 | package_data={'btplotting': ['templates/*.j2', 'templates/js/*.js']},
35 |
36 | install_requires=[
37 | 'backtrader',
38 | 'bokeh',
39 | 'jinja2',
40 | 'pandas',
41 | 'matplotlib',
42 | ],
43 | )
44 |
--------------------------------------------------------------------------------
/tests/asserts/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/happydasch/btplotting/727e8c688d3f2d8fca6b1a431499a835f63e3d73/tests/asserts/__init__.py
--------------------------------------------------------------------------------
/tests/asserts/asserts.py:
--------------------------------------------------------------------------------
1 | from backtrader_plotting.bokeh.bokeh import FigurePage
2 |
3 |
4 | def assert_num_tabs(figs, *args):
5 | for idx, num in enumerate(args):
6 | assert len(figs[idx][0].model.tabs) == num
7 |
8 |
9 | def assert_num_figures(figs, *args):
10 | for idx, num in enumerate(args):
11 | fp: FigurePage = figs[idx][0]
12 | assert len(fp.figure_envs) == num
13 |
--------------------------------------------------------------------------------
/tests/strategies/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/happydasch/btplotting/727e8c688d3f2d8fca6b1a431499a835f63e3d73/tests/strategies/__init__.py
--------------------------------------------------------------------------------
/tests/strategies/togglestrategy.py:
--------------------------------------------------------------------------------
1 | import backtrader as bt
2 |
3 |
4 | class ToggleStrategy(bt.Strategy):
5 | params = (
6 | ('modbuy', 23),
7 | ('modsell', 54),
8 | )
9 |
10 | def next(self):
11 | pos = len(self.data)
12 | if pos % self.p.modbuy == 0:
13 | self.buy(self.datas[0], size=None)
14 |
15 | if pos % self.p.modsell == 0:
16 | self.sell(self.datas[0], size=None)
17 |
--------------------------------------------------------------------------------
/tests/test_backtest.py:
--------------------------------------------------------------------------------
1 | import backtrader as bt
2 | import datetime
3 | import pytest
4 |
5 | import backtrader_plotting.bokeh.bokeh
6 | from backtrader_plotting import Bokeh, OptBrowser
7 |
8 | from strategies.togglestrategy import ToggleStrategy
9 | from asserts.asserts import assert_num_tabs, assert_num_figures
10 | from testcommon import getdatadir
11 |
12 | # set to 'show' for debugging
13 | _output_mode = 'memory'
14 |
15 |
16 | @pytest.fixture
17 | def cerebro() -> bt.Cerebro:
18 | cerebro = bt.Cerebro()
19 |
20 | datapath = getdatadir('orcl-1995-2014.txt')
21 | data = bt.feeds.YahooFinanceCSVData(
22 | dataname=datapath,
23 | fromdate=datetime.datetime(1998, 1, 1),
24 | todate=datetime.datetime(2000, 12, 31),
25 | reverse=False,
26 | swapcloses=True,
27 | )
28 | cerebro.adddata(data)
29 |
30 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
31 | cerebro.addanalyzer(bt.analyzers.SharpeRatio, compression=2)
32 | cerebro.addanalyzer(bt.analyzers.TimeDrawDown)
33 |
34 | return cerebro
35 |
36 |
37 | @pytest.fixture()
38 | def cerebro_no_optreturn() -> bt.Cerebro:
39 | cerebro = bt.Cerebro(optreturn=False)
40 |
41 | datapath = getdatadir('orcl-1995-2014.txt')
42 | data = bt.feeds.YahooFinanceCSVData(
43 | dataname=datapath,
44 | fromdate=datetime.datetime(1998, 1, 1),
45 | todate=datetime.datetime(2000, 12, 31),
46 | reverse=False,
47 | swapcloses=True,
48 | )
49 | cerebro.adddata(data)
50 |
51 | cerebro.addanalyzer(bt.analyzers.TradeAnalyzer)
52 | cerebro.addanalyzer(bt.analyzers.SharpeRatio, compression=2)
53 | cerebro.addanalyzer(bt.analyzers.TimeDrawDown)
54 |
55 | return cerebro
56 |
57 |
58 | def test_std_backtest_volume_subplot(cerebro: bt.Cerebro):
59 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
60 | cerebro.run()
61 |
62 | s = backtrader_plotting.schemes.Blackly()
63 | s.voloverlay = False
64 | b = Bokeh(style='bar', scheme=s, output_mode=_output_mode)
65 | figs = cerebro.plot(b)
66 |
67 | assert len(figs) == 1
68 | assert_num_tabs(figs, 3)
69 | assert_num_figures(figs, 5)
70 |
71 |
72 | def test_std_backtest(cerebro: bt.Cerebro):
73 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
74 | cerebro.run()
75 |
76 | s = backtrader_plotting.schemes.Blackly()
77 | b = Bokeh(style='bar', scheme=s, output_mode=_output_mode)
78 | figs = cerebro.plot(b)
79 |
80 | assert len(figs) == 1
81 | assert_num_tabs(figs, 3)
82 | assert_num_figures(figs, 4)
83 |
84 |
85 | def test_std_backtest_2datas(cerebro: bt.Cerebro):
86 | datapath = getdatadir('nvda-1999-2014.txt')
87 | data = bt.feeds.YahooFinanceCSVData(
88 | dataname=datapath,
89 | fromdate=datetime.datetime(1998, 1, 1),
90 | todate=datetime.datetime(2000, 12, 31),
91 | reverse=False,
92 | swapcloses=True,
93 | )
94 | cerebro.adddata(data)
95 |
96 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
97 | cerebro.run()
98 |
99 | s = backtrader_plotting.schemes.Blackly()
100 | b = Bokeh(style='bar', scheme=s, output_mode=_output_mode, merge_data_hovers=True)
101 | figs = cerebro.plot(b)
102 |
103 | assert len(figs) == 1
104 | assert_num_tabs(figs, 3)
105 | assert_num_figures(figs, 5)
106 |
107 |
108 | def test_std_backtest_tabs_multi(cerebro: bt.Cerebro):
109 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
110 | cerebro.run()
111 |
112 | s = backtrader_plotting.schemes.Blackly()
113 | b = Bokeh(style='bar', tabs='multi', scheme=s, output_mode=_output_mode)
114 | figs = cerebro.plot(b)
115 |
116 | assert len(figs) == 1
117 | assert_num_tabs(figs, 5)
118 | assert_num_figures(figs, 4)
119 |
120 |
121 | def test_std_backtest_ind_subplot(cerebro: bt.Cerebro):
122 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
123 | cerebro.run()
124 |
125 | plotconfig = {
126 | '#:i-0': {
127 | 'subplot': True,
128 | }
129 | }
130 |
131 | s = backtrader_plotting.schemes.Blackly()
132 | b = Bokeh(style='bar', scheme=s, output_mode=_output_mode, plotconfig=plotconfig)
133 |
134 | figs = cerebro.plot(b)
135 |
136 | assert_num_tabs(figs, 3)
137 | assert_num_figures(figs, 5)
138 |
139 |
140 | def test_std_backtest_ind_on_line(cerebro: bt.Cerebro):
141 | '''In the past it crashed when creating indicators with specific lines case LineSeriesStub was not handled correctly'''
142 | class TestStrategy(bt.Strategy):
143 | def __init__(self):
144 | self._sma = bt.indicators.SMA(self.data.close)
145 |
146 | cerebro.addstrategy(TestStrategy)
147 | cerebro.run()
148 |
149 | s = backtrader_plotting.schemes.Blackly()
150 | b = Bokeh(style='bar', scheme=s, output_mode=_output_mode)
151 | figs = cerebro.plot(b)
152 |
153 | assert len(figs) == 1
154 | assert_num_tabs(figs, 3)
155 | assert_num_figures(figs, 3)
156 |
157 |
158 | def test_backtest_2strats(cerebro: bt.Cerebro):
159 | cerebro.addstrategy(bt.strategies.MA_CrossOver)
160 | cerebro.addstrategy(ToggleStrategy)
161 | cerebro.run()
162 |
163 | b = Bokeh(style='bar', output_mode=_output_mode)
164 |
165 | figs = cerebro.plot(b)
166 |
167 | assert len(figs) == 2
168 | assert_num_tabs(figs, 3, 3)
169 | assert_num_figures(figs, 4, 3)
170 |
171 |
172 | def test_optimize(cerebro: bt.Cerebro):
173 | cerebro.optstrategy(bt.strategies.MA_CrossOver, slow=[5, 10, 20], fast=[5, 10, 20])
174 | res = cerebro.run(optreturn=True)
175 |
176 | b = Bokeh(style='bar', output_mode=_output_mode)
177 |
178 | browser = OptBrowser(b, res)
179 | model = browser.build_optresult_model()
180 | # browser.start()
181 |
182 | def count_children(obj):
183 | numo = 1
184 | if hasattr(obj, "children"):
185 | numo = count_children(obj.children)
186 | if hasattr(obj, '__len__'):
187 | numo += len(obj)
188 | return numo
189 |
190 | num = count_children(model)
191 |
192 | assert num == 3
193 |
194 |
195 | def test_optimize_2strat(cerebro: bt.Cerebro):
196 | cerebro.optstrategy(bt.strategies.MA_CrossOver, slow=[5, 10, 20], fast=[5, 10, 20])
197 | cerebro.optstrategy(ToggleStrategy, modbuy=[12, 15], modsell=[17, 19])
198 | res = cerebro.run()
199 |
200 | b = Bokeh(style='bar', output_mode=_output_mode)
201 |
202 | browser = OptBrowser(b, res)
203 |
204 | with pytest.raises(RuntimeError):
205 | browser.build_optresult_model()
206 | # browser.start()
207 |
208 |
209 | def test_optimize_no_optreturn(cerebro_no_optreturn: bt.Cerebro):
210 | cerebro_no_optreturn.optstrategy(bt.strategies.MA_CrossOver, slow=[5, 10, 20], fast=[5, 10, 20])
211 | res = cerebro_no_optreturn.run()
212 |
213 | s = backtrader_plotting.schemes.Blackly()
214 | b = Bokeh(style='bar', output_mode=_output_mode, scheme=s)
215 |
216 | browser = OptBrowser(b, res)
217 | model = browser.build_optresult_model()
218 | #browser.start()
219 |
220 | def count_children(obj):
221 | numo = 1
222 | if hasattr(obj, "children"):
223 | numo = count_children(obj.children)
224 | if hasattr(obj, '__len__'):
225 | numo += len(obj)
226 | return numo
227 |
228 | num = count_children(model)
229 |
230 | assert num == 3
231 |
232 |
233 | def test_ordered_optimize(cerebro: bt.Cerebro):
234 | cerebro.optstrategy(bt.strategies.MA_CrossOver, slow=[20], fast=[5, 10, 20])
235 | res = cerebro.run(optreturn=True)
236 |
237 | def df(optresults):
238 | a = [x.analyzers.tradeanalyzer.get_analysis() for x in optresults]
239 | return sum([x.pnl.gross.total if 'pnl' in x else 0 for x in a])
240 |
241 | usercolumns = {
242 | 'Profit & Loss': df,
243 | }
244 |
245 | b = Bokeh(style='bar', output_mode=_output_mode)
246 |
247 | browser = OptBrowser(b, res, usercolumns=usercolumns, sortcolumn='Profit & Loss')
248 | model = browser.build_optresult_model()
249 | # browser.start()
250 |
251 | def count_children(obj):
252 | numo = 1
253 | if hasattr(obj, "children"):
254 | numo = count_children(obj.children)
255 | if hasattr(obj, '__len__'):
256 | numo += len(obj)
257 | return numo
258 |
259 | num = count_children(model)
260 |
261 | assert num == 3
262 |
--------------------------------------------------------------------------------
/tests/test_issue10.py:
--------------------------------------------------------------------------------
1 | import datetime # For datetime objects
2 |
3 | # Import the backtrader platform
4 | import backtrader as bt
5 |
6 | import backtrader_plotting
7 |
8 | from testcommon import getdatadir
9 |
10 |
11 | # Create a Stratey
12 | class MACDStrategy(bt.Strategy):
13 | params = (
14 | # Standard MACD Parameters
15 | ('macd1', 12),
16 | ('macd2', 26),
17 | ('macdsig', 9),
18 | )
19 |
20 | def __init__(self):
21 | self.macd = bt.indicators.MACD(self.data,
22 | period_me1=self.p.macd1,
23 | period_me2=self.p.macd2,
24 | period_signal=self.p.macdsig)
25 | # backtrader.LinePlotterIndicator(macd, name='MACD')
26 | # Cross of macd.macd and macd.signal
27 | self.mcross = bt.indicators.CrossOver(self.macd.macd, self.macd.signal)
28 |
29 | # backtrader.LinePlotterIndicator(mcross, name='MACDCross')
30 |
31 | def start(self):
32 | self.order = None # sentinel to avoid operrations on pending order
33 |
34 | def log(self, txt, dt=None):
35 | ''' Logging function for this strategy
36 | '''
37 | dt = dt or self.datas[0].datetime.date(0)
38 | time = self.datas[0].datetime.time()
39 | print('%s,%s' % (dt.isoformat(), txt))
40 |
41 | def next(self):
42 | if self.order:
43 | return # pending order execution
44 |
45 | if not self.position: # not in the market
46 | if self.mcross[0] > 0.0 and self.macd.lines.signal[0] > 0 and self.macd.lines.macd[0] > 0:
47 | self.order = self.buy()
48 | self.log('BUY CREATED, %.2f' % self.data[0])
49 | # else:
50 | # if self.mcross[0] > 0.0 and self.macd.lines.signal[0] < 0 and self.macd.lines.macd[0] < 0:
51 | # self.order = self.buy()
52 | # self.log('BUY CREATED, %.2f' % self.data[0])
53 |
54 | else: # in the market
55 | if self.mcross[0] < 0.0 and self.macd.lines.signal[0] < 0 and self.macd.lines.macd[0] < 0:
56 | self.sell() # stop met - get out
57 | self.log('BUY CREATED, %.2f' % self.data[0])
58 | # else:
59 | # if self.mcross[0] < 0.0 and self.macd.lines.signal[0] > 0 and self.macd.lines.macd[0] > 0:
60 | # self.sell() # stop met - get out
61 | # self.log('BUY CREATED, %.2f' % self.data[0])
62 |
63 |
64 | def bokeh_plot(data):
65 | # Create a cerebro entity
66 | cerebro = bt.Cerebro()
67 |
68 | # Add a strategy
69 | cerebro.addstrategy(MACDStrategy)
70 |
71 | # Add the Data Feed to Cerebro
72 | cerebro.adddata(data)
73 |
74 | # Set our desired cash start
75 | cerebro.broker.setcash(100000.0)
76 | cerebro.broker.setcommission(commission=0.0002)
77 |
78 | '''
79 | -----------------strategy & sizer------------------------------
80 | '''
81 | cerebro.addsizer(bt.sizers.PercentSizer, percents=98)
82 |
83 | cerebro.addobserver(bt.observers.FundShares)
84 | # cerebro.addanalyzer(bt.analyzers.TimeReturn, timeframe=bt.TimeFrame.Years)
85 | # cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
86 |
87 | # Print out the starting conditions
88 | print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
89 |
90 | # Run over everything
91 | cerebro.run()
92 |
93 | print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
94 | # cerebro.plot(style='bar')
95 | b = backtrader_plotting.Bokeh(style='bar', scheme=backtrader_plotting.schemes.Tradimo(), output_mode='memory')
96 | figs = cerebro.plot(b)
97 |
98 | assert isinstance(figs[0][0], backtrader_plotting.bokeh.bokeh.FigurePage)
99 | assert len(figs[0][0].figure_envs) == 6
100 |
101 |
102 | def test_github_issue10():
103 | data = bt.feeds.YahooFinanceCSVData(
104 | dataname=getdatadir("orcl-1995-2014.txt"),
105 | fromdate=datetime.datetime(2000, 1, 1),
106 | todate=datetime.datetime(2001, 2, 28),
107 | )
108 |
109 | bokeh_plot(data)
110 |
111 |
112 | if __name__ == '__main__':
113 | test_github_issue10()
114 |
--------------------------------------------------------------------------------
/tests/test_issue30.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import backtrader as bt
3 | import backtrader_plotting
4 | from backtrader_plotting.schemes import Tradimo
5 |
6 | from testcommon import getdatadir
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def __init__(self):
11 | sma = bt.indicators.SimpleMovingAverage(period=20, subplot=True)
12 | sma2 = bt.indicators.SimpleMovingAverage(period=5, subplot=True, plotmaster=sma)
13 |
14 | def next(self):
15 | pos = len(self.data)
16 | if pos == 45 or pos == 145:
17 | self.buy(self.datas[0], size=None)
18 |
19 | if pos == 116 or pos == 215:
20 | self.sell(self.datas[0], size=None)
21 |
22 |
23 | def test_github_issue30():
24 | cerebro = bt.Cerebro()
25 |
26 | cerebro.addstrategy(MyStrategy)
27 |
28 | data = bt.feeds.YahooFinanceCSVData(
29 | dataname=getdatadir("orcl-1995-2014.txt"),
30 | fromdate=datetime.datetime(2000, 1, 1),
31 | todate=datetime.datetime(2001, 2, 28),
32 | reverse=False,
33 | )
34 | cerebro.adddata(data)
35 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
36 |
37 | cerebro.run()
38 |
39 | b = backtrader_plotting.Bokeh(filename='chart.html', style='bar', scheme=Tradimo(), output_mode='memory')
40 |
41 | figs = cerebro.plot(b)
42 |
43 | assert isinstance(figs[0][0], backtrader_plotting.bokeh.bokeh.FigurePage)
44 | assert len(figs[0][0].figure_envs) == 4
45 | assert len(figs[0][0].analyzers) == 1
46 |
47 |
48 | if __name__ == '__main__':
49 | test_github_issue30()
50 |
--------------------------------------------------------------------------------
/tests/test_issue36.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | import pandas as pd
4 | import backtrader as bt
5 | from backtrader_plotting import Bokeh
6 | from backtrader_plotting.schemes import Tradimo
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def next(self):
11 | pos = len(self.data)
12 | if pos == 45 or pos == 145:
13 | self.buy(self.datas[0], size=None)
14 |
15 | if pos == 116 or pos == 215:
16 | self.sell(self.datas[0], size=None)
17 |
18 |
19 | def test_github_issue36():
20 | cerebro = bt.Cerebro()
21 |
22 | cerebro.addstrategy(MyStrategy)
23 |
24 | filedir = os.path.dirname(os.path.abspath(__file__))
25 |
26 | df = pd.read_csv(os.path.join(filedir, "datas/NQ.csv"), index_col=0)
27 | df.index = pd.DatetimeIndex(df.index)
28 | data = bt.feeds.PandasData(dataname=df, name='NQ', timeframe=bt.TimeFrame.Minutes)
29 | cerebro.adddata(data)
30 | cerebro.resampledata(data, name='NQ_5min', timeframe=bt.TimeFrame.Minutes, compression=5)
31 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
32 | cerebro.run()
33 |
34 | b = Bokeh(filename='chart.html', style='bar', plot_mode="single", scheme=Tradimo(), output_mode='memory')
35 | cerebro.plot(b)
36 |
37 |
38 | if __name__ == "__main__":
39 | test_github_issue36()
40 |
--------------------------------------------------------------------------------
/tests/test_issue37.py:
--------------------------------------------------------------------------------
1 | from backtrader_plotting import Bokeh
2 | import datetime
3 | import backtrader as bt
4 | from backtrader_plotting.schemes import Tradimo
5 |
6 | from testcommon import getdatadir
7 |
8 |
9 | class BokehTest(bt.Strategy):
10 |
11 | def __init__(self):
12 | self.rsi = bt.indicators.RSI_SMA(self.data.close, period=14, safediv=True)
13 |
14 | # we set it manually (vanilla backtrader doesn't know about plotid so we can't set regularly in constructor)
15 | self.rsi.plotinfo.plotid = 'rsi'
16 |
17 |
18 | def test_github_issue37_plotaspectratio():
19 | cerebro = bt.Cerebro()
20 |
21 | data = bt.feeds.YahooFinanceCSVData(
22 | dataname=getdatadir("orcl-1995-2014.txt"),
23 | fromdate=datetime.datetime(2000, 1, 1),
24 | todate=datetime.datetime(2001, 2, 28),
25 | reverse=False,
26 | )
27 |
28 | cerebro.adddata(data)
29 | cerebro.broker.setcash(10000)
30 | cerebro.coc = True
31 | cerebro.broker.setcommission(commission=0.00075)
32 | cerebro.addstrategy(BokehTest)
33 | cerebro.run()
34 |
35 | plotconfig = {
36 | 'id:rsi': dict(
37 | plotaspectratio=10,
38 | ),
39 | }
40 |
41 | b = Bokeh(style='bar', plot_mode='single', scheme=Tradimo(), plotconfig=plotconfig, output_mode='memory')
42 | output = cerebro.plot(b)
43 |
44 | assert output[0][0].figure_envs[1].figure.aspect_ratio == 10
45 |
--------------------------------------------------------------------------------
/tests/test_issue44.py:
--------------------------------------------------------------------------------
1 | # -*- coding:utf-8 -*-
2 | import backtrader as bt
3 | import datetime
4 | from backtrader_plotting import Bokeh
5 |
6 | from testcommon import getdatadir
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def __init__(self):
11 | sma = bt.indicators.SimpleMovingAverage(period=20, subplot=True)
12 | sma2 = bt.indicators.SimpleMovingAverage(period=5, subplot=True, plotmaster=sma)
13 |
14 | def next(self):
15 | pos = len(self.data)
16 | if pos == 45 or pos == 145:
17 | self.buy(self.datas[0], size=None)
18 |
19 | if pos == 116 or pos == 215:
20 | self.sell(self.datas[0], size=None)
21 |
22 |
23 | if __name__ == '__main__':
24 | cerebro = bt.Cerebro()
25 | data = bt.feeds.YahooFinanceCSVData(
26 | dataname=getdatadir("20170319-20200319-0388.HK.csv"),
27 | fromdata=datetime.datetime(2020, 2, 19, 0, 0, 0,),
28 | todata=datetime.datetime(2020, 3, 19, 0, 0, 0),
29 | reverse=False
30 | )
31 |
32 | cerebro.addobserver(bt.observers.Benchmark, data=data, timeframe=bt.TimeFrame.NoTimeFrame)
33 | cerebro.addstrategy(MyStrategy)
34 | cerebro.adddata(data)
35 | cerebro.addsizer(bt.sizers.AllInSizer)
36 | cerebro.broker.setcash(100000)
37 |
38 | print('Starting Portfolio Value: %.2f' % cerebro.broker.getvalue())
39 | import json
40 | strats = cerebro.run()[0]
41 | print('Final Portfolio Value: %.2f' % cerebro.broker.getvalue())
42 |
43 | bo = Bokeh(style='bar', plot_mode='single', output_mode='memory')
44 | cerebro.plot(bo)
45 |
46 |
--------------------------------------------------------------------------------
/tests/test_lineactions.py:
--------------------------------------------------------------------------------
1 | import datetime
2 | import backtrader as bt
3 | import backtrader_plotting
4 | from backtrader_plotting.schemes import Tradimo
5 |
6 | from testcommon import getdatadir
7 |
8 |
9 | class MyStrategy(bt.Strategy):
10 | def __init__(self):
11 | self.macd = bt.indicators.MACD(
12 | period_me1=5,
13 | period_me2=8,
14 | period_signal=12
15 | )
16 |
17 | self.badf = self.macd.macd > self.data(0)
18 |
19 | self.macd_buy = bt.And(
20 | self.macd.macd(0) > self.macd.macd(-1),
21 | self.macd.signal(0) > self.macd.signal(-1),
22 | self.macd.macd(0) > self.macd.signal(0)
23 | )
24 |
25 | def next(self):
26 | pos = len(self.data)
27 | if pos == 45 or pos == 145:
28 | self.buy(self.datas[0], size=None)
29 |
30 | if pos == 116 or pos == 215:
31 | self.sell(self.datas[0], size=None)
32 |
33 |
34 | def test_lineactions():
35 | cerebro = bt.Cerebro()
36 |
37 | cerebro.addstrategy(MyStrategy)
38 |
39 | data = bt.feeds.YahooFinanceCSVData(
40 | dataname=getdatadir("orcl-1995-2014.txt"),
41 | fromdate=datetime.datetime(2000, 1, 1),
42 | todate=datetime.datetime(2001, 2, 28),
43 | reverse=False,
44 | )
45 | cerebro.adddata(data)
46 | cerebro.addanalyzer(bt.analyzers.SharpeRatio)
47 |
48 | cerebro.run()
49 |
50 | b = backtrader_plotting.Bokeh(style='bar', scheme=Tradimo(), output_mode='memory')
51 |
52 | figs = cerebro.plot(b)
53 |
54 | assert isinstance(figs[0][0], backtrader_plotting.bokeh.bokeh.FigurePage)
55 | assert len(figs[0][0].figure_envs) == 4
56 | assert len(figs[0][0].analyzers) == 1
57 |
58 |
59 | if __name__ == '__main__':
60 | test_lineactions()
61 |
--------------------------------------------------------------------------------
/tests/testcommon.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 |
4 |
5 | modpath = os.path.dirname(os.path.abspath(__file__))
6 | dataspath = 'datas'
7 |
8 |
9 | def getdatadir(filename):
10 | return os.path.join(modpath, dataspath, filename)
11 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [tox]
2 | envlist = py36,py37,p38
3 |
4 | [testenv]
5 | deps = -rrequirements-test.txt
6 | commands = pytest
7 |
--------------------------------------------------------------------------------