├── .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 | <!DOCTYPE html> 21 | <html lang="en"> 22 | <head> 23 | <meta charset="utf-8"> 24 | <title>{{ 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 | --------------------------------------------------------------------------------