├── .gitignore ├── README.md ├── alert_tests.py ├── example_strategy.py ├── examples ├── bitfinex.py ├── bitfinex_leverage.py └── kraken.py ├── fire_alert.py ├── setup.py └── tv2bt ├── __init__.py ├── config.py ├── server.py └── tv_feed.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | 47 | # Translations 48 | *.mo 49 | *.pot 50 | 51 | # Django stuff: 52 | *.log 53 | 54 | # Sphinx documentation 55 | docs/_build/ 56 | 57 | # PyBuilder 58 | target/ 59 | 60 | # My Stuff 61 | .idea/ 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tv2bt 2 | A python package that bridges Tradingview alerts to backtrader. 3 | 4 | A data feed for Backtrader which will allow you to receive both trade signals 5 | and/orOHLCV data from Tradingview. You can send trading signals from the 6 | plethora of open source strategies and indicators on the platform. Or perhaps 7 | you are struggling to find some good, reliable intra-day data. Tradingview 8 | has plenty we can now access in Backtrader. 9 | 10 | ## Introduction 11 | for a full overview and gentle introduction please see: https://backtest-rookies.com/2019/11/22/tv2bt-tradingview-to-backtrader-module/ 12 | 13 | ## Installation 14 | Note: Developed for Python 3.x only. 15 | 16 | Download and run `python setup.py install develop` or `python3 setup.py install develop` 17 | 18 | A simple Flask server is used to receive the webhooks from Tradingview. This means, that you will need to configure some port forwarding on your router to the machine running the datafeed. Redirect port 80(HTTP) to port 8123. If you don't do this, no alerts will be received. 19 | 20 | ## Testing 21 | If you are having trouble receiving alerts, a special test script has been provided to push alerts to the system in the same manner that Tradingview would. First, fire up the `example_strategy.py` and then run `alert_tests.py`. You should see some test data pushed to the system that looks like this: 22 | 23 | If you are able to see these alerts but you are not seeing Tradingview alerts, check your alert settings at Tradingview side or your port forward settings. If you don't see the alerts when running this script, ensure you have not made any changes to `example_strategy.py` 24 | 25 | ## Donations Welcome, but not required! 26 | 27 | Download The Brave Browser. A privacy focused browser that supports content 28 | creators: https://brave.com/dav470 29 | 30 | Tips are are lovely too. 31 | 32 | BTC: 3HxNVyh5729ieTfPnTybrtK7J7QxJD9gjD 33 | 34 | ETH: 0x9a2f88198224d59e5749bacfc23d79507da3d431 35 | 36 | XMR: 42nzsthr1C79nr6TYP2eaW7XMAdS5Rz1Ad9KVSCnCrn9RA9dthWYrHLTyfnULnCmXkA5mL3iF1EX9H7hK4XxUszyAoQjjBa 37 | -------------------------------------------------------------------------------- /alert_tests.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | import requests 28 | import json 29 | import unittest 30 | from datetime import datetime 31 | 32 | 33 | class OHLC_Test(unittest.TestCase): 34 | ''' 35 | Checks for the app route: /tv 36 | ''' 37 | 38 | def test_post_ohlc(self): 39 | dt = datetime.now().strftime('%Y-%m-%dT%H:%M:%SZ') 40 | data = { 41 | 'symbol':'XBT/USD', 'DT':dt, 42 | 'O':10, 'H':11, 'L':9, 'C':10.5, 'V':100, 'action': 0, 43 | } 44 | 45 | resp = requests.post('http://0.0.0.0:8123/tv', json=data) 46 | print('Status Code: {}'.format(resp.status_code)) 47 | print('Response: {}'.format(resp.text)) 48 | 49 | self.assertEqual(int(resp.status_code), 200) 50 | 51 | 52 | class Signal_Test(unittest.TestCase): 53 | ''' 54 | Checks for the app route: /tv 55 | ''' 56 | 57 | def test_post_long(self): 58 | 59 | data = { 60 | 'symbol':'ETH/USD', 'action':1 61 | } 62 | 63 | resp = requests.post('http://0.0.0.0:8123/tv', json=data) 64 | print('Status Code: {}'.format(resp.status_code)) 65 | print('Response: {}'.format(resp.text)) 66 | 67 | self.assertEqual(int(resp.status_code), 200) 68 | 69 | def test_post_short(self): 70 | 71 | data = { 72 | 'symbol':'ETH/USD', 'action':-1 73 | } 74 | 75 | resp = requests.post('http://0.0.0.0:8123/tv', json=data) 76 | print('Status Code: {}'.format(resp.status_code)) 77 | print('Response: {}'.format(resp.text)) 78 | 79 | self.assertEqual(int(resp.status_code), 200) 80 | 81 | def test_post_flat(self): 82 | 83 | data = { 84 | 'symbol':'ETH/USD', 'action':0 85 | } 86 | 87 | resp = requests.post('http://0.0.0.0:8123/tv', json=data) 88 | print('Status Code: {}'.format(resp.status_code)) 89 | print('Response: {}'.format(resp.text)) 90 | 91 | self.assertEqual(int(resp.status_code), 200) 92 | 93 | if __name__ == '__main__': 94 | unittest.main() 95 | -------------------------------------------------------------------------------- /example_strategy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | import tv2bt.config 27 | tv2bt.config.PORT = 8123 28 | import backtrader as bt 29 | from datetime import datetime 30 | from tv2bt import TVFeed 31 | from ccxtbt import CCXTStore 32 | 33 | class TVTest(bt.Strategy): 34 | ''' 35 | Simple strat to test TV Feed. 36 | ''' 37 | 38 | def __init__(self): 39 | 40 | self.data_info = dict() 41 | 42 | for i, d in enumerate(self.datas): 43 | 44 | self.data_info[d._name] = dict() 45 | self.data_info[d._name]['last bar'] = 0 46 | 47 | def next(self): 48 | 49 | print('='*80) 50 | print(' '*36,'NEXT') 51 | print('='*80) 52 | 53 | for i, d in enumerate(self.datas): 54 | 55 | dn = d._name 56 | dt = d.datetime.datetime() 57 | bar = len(d) 58 | 59 | # Check we have a new bar and are not repeating an old one. 60 | if bar > self.data_info[d._name]['last bar']: 61 | print('DATE : {}'.format(dt)) 62 | print('ASSET : {}'.format(d._name)) 63 | print('BAR : {}'.format(bar)) 64 | print('Open : {}'.format(d.open[0])) 65 | print('High : {}'.format(d.high[0])) 66 | print('Low : {}'.format(d.low[0])) 67 | print('Close : {}'.format(d.close[0])) 68 | print('Signal : {}'.format(d.signal[0])) 69 | print('-'*80) 70 | 71 | # Save the last bar processed 72 | self.data_info[d._name]['last bar'] = bar 73 | 74 | def notify_data(self, data, status, *args, **kwargs): 75 | print('DATA NOTIF: {}: {}'.format(data._getstatusname(status), ','.join(args))) 76 | 77 | # Example Alerts 78 | # -------------- 79 | # 1. DATA/OHLC 80 | # The ticker should be the same as you use for your broker to 81 | # make life easier. Don't blindly copy the Tradingview Ticker. 82 | # The rest of the string below should be copied in as it. Tradingview 83 | # will replace the values inside {{}} with the actual values. 84 | ''' 85 | {'symbol':'[INSERT TICKER]', 'DT':'{{time}}', 'O':{{open}}, 'H':{{high}}, 'L':{{low}}, 'C':{{close}}, 'V':{{volume}}, 'action':0} 86 | ''' 87 | # 2. SIGNAL OPNLY 88 | # Again use the same ticker as your broker unless you are adding more than one 89 | # Signal or data feed for a symbol. If so, you will need to create your own tickers 90 | # and process them accordingly in the strategy. I.e loop through all datas, 91 | # decide what to do and then create your order using the data feed that has the 92 | # correct ticker that the broker is expecting. 93 | # OHLC can be ommitted and will just appear as NaN 94 | ''' 95 | {'symbol':'[INSERT TICKER]', 'action':1} 96 | ''' 97 | 98 | print('='*80) 99 | print('Starting Example Strategy') 100 | print('All data feeds must have one bar of data before before you will see any output \n' 101 | 'on the console. Please be patient...') 102 | print('For instructions how to use, see:') 103 | print('='*80) 104 | 105 | debug = False 106 | 107 | # Create an instance of cerebro 108 | cerebro = bt.Cerebro() 109 | 110 | # Get Data 111 | data = TVFeed(dataname='XBT/USD', debug=debug) 112 | data2 = TVFeed(dataname='ETH/USD', debug=debug, kickstart=True) 113 | 114 | # Add the data feeds 115 | cerebro.adddata(data) 116 | cerebro.adddata(data2) 117 | #cerebro.adddata(data3) 118 | 119 | # Add our strategy 120 | cerebro.addstrategy(TVTest) 121 | 122 | # Run the strategy 123 | cerebro.run() 124 | -------------------------------------------------------------------------------- /examples/bitfinex.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | import backtrader as bt 28 | from datetime import datetime 29 | from tv2bt import TVFeed 30 | from ccxtbt import CCXTStore 31 | 32 | apikey = 'INSERT YOUR API KEY' 33 | secret = 'INSERT YOUR SECRET' 34 | 35 | class TVTest(bt.Strategy): 36 | ''' 37 | Simple strat to test TV Feed. 38 | ''' 39 | 40 | params = ( 41 | ('perc_size', 0.7), # 10% 42 | ('fixed_qty', 12) 43 | ) 44 | 45 | def __init__(self): 46 | 47 | self.data_info = dict() 48 | 49 | for i, d in enumerate(self.datas): 50 | 51 | # Tracking the last bar is useful when we have data coming in 52 | # at different times. 53 | self.data_info[d._name] = dict() 54 | self.data_info[d._name]['last bar'] = 0 55 | 56 | # When we are not using leverage, there is no concept of a 57 | # position. So we create a dict to store holdings of both 58 | # the base and quote currencies. 59 | ticker_components = d._name.split('/') 60 | self.data_info[d._name]['base'] = ticker_components[0] 61 | self.data_info[d._name]['counter'] = ticker_components[1] 62 | 63 | for comp in ticker_components: 64 | self.data_info[d._name][comp] = dict() 65 | self.data_info[d._name][comp]['cash'] = 0 66 | self.data_info[d._name][comp]['value'] = 0 67 | 68 | 69 | def next(self): 70 | 71 | print('='*80) 72 | print(' '*36,'NEXT') 73 | print('='*80) 74 | 75 | 76 | for i, d in enumerate(self.datas): 77 | 78 | dn = d._name 79 | dt = d.datetime.datetime() 80 | bar = len(d) 81 | pos = self.getposition(d).size 82 | base = self.data_info[dn]['base'] 83 | counter = self.data_info[dn]['counter'] 84 | 85 | print('Position : {}'.format(pos)) 86 | 87 | # Check we have a new bar and are not repeating an old one. 88 | if bar > self.data_info[dn]['last bar']: 89 | print('DATE : {}'.format(dt)) 90 | print('ASSET : {}'.format(dn)) 91 | print('BAR : {}'.format(bar)) 92 | print('Open : {}'.format(d.open[0])) 93 | print('High : {}'.format(d.high[0])) 94 | print('Low : {}'.format(d.low[0])) 95 | print('Close : {}'.format(d.close[0])) 96 | print('Signal : {}'.format(d.signal[0])) 97 | print('-'*80) 98 | 99 | # Save the last bar processed 100 | self.data_info[dn]['last bar'] = bar 101 | 102 | # Get our balances only if we want to go buy/sell. 103 | # Otherwise, the request slows things down. 104 | if d.signal in [1,-1]: 105 | ticker_components = dn.split('/') 106 | 107 | for comp in ticker_components: 108 | # Get our cash and value to enter a position 109 | cash, value = self.broker.get_wallet_balance(comp) 110 | print('{} : {} in Cash'.format(comp, cash)) 111 | print('{} : {} in Value'.format(comp, value)) 112 | self.data_info[dn][comp]['cash'] = cash 113 | self.data_info[dn][comp]['value'] = value 114 | 115 | # Buy the base currency! 116 | if d.signal == 1: 117 | 118 | qty = (self.data_info[dn][counter]['value'] * self.p.perc_size) / d.close[0] 119 | print('Action : Buy | Qty: {}'.format(qty)) 120 | self.buy(d, size=qty) 121 | 122 | # Sell the base currency! 123 | if d.signal == -1: 124 | 125 | if cash > 0: 126 | print('Action : Sell | Qty: {}'.format(pos)) 127 | self.sell(d, size=pos) 128 | 129 | 130 | def notify_data(self, data, status, *args, **kwargs): 131 | print('DATA NOTIF: {}: {}'.format(data._getstatusname(status), ','.join(args))) 132 | 133 | 134 | def notify_order(self, order): 135 | dt = order.data.datetime.datetime() 136 | dn = order.data._name 137 | 138 | print('='*33, 'NOTIFY ORDER', '='*33) 139 | 140 | if order.status == order.Submitted: 141 | print('Date : {}'.format(dt)) 142 | print('Ticker : {}'.format(dn)) 143 | print('Notify : Order Submitted') 144 | 145 | if order.status == order.Accepted: 146 | print('Date : {}'.format(dt)) 147 | print('Ticker : {}'.format(dn)) 148 | print('Notify : Order Accepted') 149 | 150 | if order.status == order.Completed: 151 | print('Date : {}'.format(dt)) 152 | print('Ticker : {}'.format(dn)) 153 | print('Notify : Order Completed') 154 | 155 | if order.status == order.Canceled: 156 | print('Date : {}'.format(dt)) 157 | print('Ticker : {}'.format(dn)) 158 | print('Notify : Order Canceled') 159 | 160 | if order.status == order.Rejected: 161 | print('Date : {}'.format(dt)) 162 | print('Ticker : {}'.format(dn)) 163 | print('Notify : Order Rejected') 164 | 165 | # Example Alerts 166 | # -------------- 167 | # 1. DATA/OHLC 168 | # The ticker should be the same as you use for your broker to 169 | # make life easier. Don't blindly copy the Tradingview Ticker. 170 | # The rest of the string below should be copied in as it. Tradingview 171 | # will replace the values inside {{}} with the actual values. 172 | ''' 173 | {'symbol':'[INSERT TICKER]', 'DT':'{{time}}', 'O':{{open}}, 'H':{{high}}, 'L':{{low}}, 'C':{{close}}, 'V':{{volume}}, 'action':0} 174 | ''' 175 | # 2. SIGNALS 176 | # 3 Types of signal are currently supported. 'long', 'short' and 'flat' 177 | # It is expected that you handle them appropriately and according to your 178 | # taste in backtrader 179 | ''' 180 | {'symbol':'[INSERT TICKER]', 'action':1} 181 | ''' 182 | 183 | print('='*80) 184 | print('Starting Example Strategy') 185 | print('All data feeds must have one bar of data before before you will see any output \n' 186 | 'on the console. Please be patient...') 187 | print('For instructions how to use, see:') 188 | print('='*80) 189 | 190 | debug = False 191 | 192 | # Create an instance of cerebro 193 | cerebro = bt.Cerebro() 194 | 195 | # Get Data 196 | data = TVFeed(dataname='BTC/USD', debug=debug) 197 | 198 | # Add the data feeds 199 | cerebro.adddata(data) 200 | #cerebro.adddata(data2) 201 | 202 | # Set Config 203 | config = {'apiKey': apikey, 204 | 'secret': secret, 205 | 'enableRateLimit': True, 206 | 'rateLimit': 3000 207 | } 208 | 209 | # Add our strategy 210 | cerebro.addstrategy(TVTest) 211 | 212 | print('Getting Store') 213 | # Create data feeds 214 | store = CCXTStore(exchange='bitfinex', currency='USD', config=config, retries=5, debug=False) 215 | 216 | print('Getting Broker') 217 | broker = store.getbroker() 218 | cerebro.setbroker(broker) 219 | 220 | # Run the strategy 221 | cerebro.run() 222 | -------------------------------------------------------------------------------- /examples/bitfinex_leverage.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | import backtrader as bt 27 | from datetime import datetime 28 | from tv2bt import TVFeed 29 | from ccxtbt import CCXTStore 30 | 31 | apikey = 'INSERT YOUR API KEY' 32 | secret = 'INSERT YOUR SECRET' 33 | 34 | class TVTest(bt.Strategy): 35 | ''' 36 | Simple strat to test TV Feed. 37 | ''' 38 | 39 | params = ( 40 | ('perc_size', 0.7), 41 | ('leverage', 1), 42 | ('fixed_qty', 12) 43 | ) 44 | 45 | def __init__(self): 46 | 47 | self.data_info = dict() 48 | 49 | for i, d in enumerate(self.datas): 50 | 51 | # Tracking the last bar is useful when we have data coming in 52 | # at different times. 53 | self.data_info[d._name] = dict() 54 | self.data_info[d._name]['last bar'] = 0 55 | 56 | # When we are not using leverage, there is no concept of a 57 | # position. So we create a dict to store holdings of both 58 | # the base and quote currencies. 59 | ticker_components = d._name.split('/') 60 | self.data_info[d._name]['base'] = ticker_components[0] 61 | self.data_info[d._name]['counter'] = ticker_components[1] 62 | 63 | for comp in ticker_components: 64 | self.data_info[d._name][comp] = dict() 65 | self.data_info[d._name][comp]['cash'] = 0 66 | self.data_info[d._name][comp]['value'] = 0 67 | 68 | 69 | def next(self): 70 | 71 | print('='*80) 72 | print(' '*36,'NEXT') 73 | print('='*80) 74 | 75 | 76 | for i, d in enumerate(self.datas): 77 | 78 | dn = d._name 79 | dt = d.datetime.datetime() 80 | bar = len(d) 81 | pos = self.getposition(d).size 82 | base = self.data_info[dn]['base'] 83 | counter = self.data_info[dn]['counter'] 84 | 85 | print('Position : {}'.format(pos)) 86 | 87 | # Check we have a new bar and are not repeating an old one. 88 | if bar > self.data_info[dn]['last bar']: 89 | print('DATE : {}'.format(dt)) 90 | print('ASSET : {}'.format(dn)) 91 | print('BAR : {}'.format(bar)) 92 | print('Open : {}'.format(d.open[0])) 93 | print('High : {}'.format(d.high[0])) 94 | print('Low : {}'.format(d.low[0])) 95 | print('Close : {}'.format(d.close[0])) 96 | print('Signal : {}'.format(d.signal[0])) 97 | print('-'*80) 98 | 99 | # Save the last bar processed 100 | self.data_info[dn]['last bar'] = bar 101 | 102 | # Get our balances only if we want to go buy/sell. 103 | # Otherwise, the request slows things down. 104 | if d.signal in [1,-1, 10]: 105 | 106 | # Get our cash and value to enter a position 107 | params = {'type': 'trading'} 108 | cash, value = self.broker.get_wallet_balance('USD', params=params) 109 | print('CASH : {}'.format(cash)) 110 | print('VALUE : {}'.format(value)) 111 | 112 | # Enter Long 113 | if d.signal == 1: 114 | 115 | # Note: Important Differences for the Type parameter! 116 | # https://docs.bitfinex.com/v1/reference#rest-auth-new-order 117 | # Also see: 118 | # https://github.com/ccxt/ccxt/issues/1207 119 | params = {'type': 'market', 'leverage':self.p.leverage} 120 | 121 | # Specify leverage! 122 | qty = ((cash * self.p.perc_size) / self.data.close[0]) * self.p.leverage # 2x leverage using 95% cash 123 | 124 | # Round to the nearest 8 places otherwise, we can end up with a bit left over when trying to close. 125 | qty = round(qty, 8) 126 | 127 | if pos < 0: 128 | qty = qty + -pos 129 | 130 | print('Action : Buy | Qty: {}'.format(qty)) 131 | self.buy(d, size=qty, params=params) 132 | 133 | # Enter Short 134 | if d.signal == -1: 135 | 136 | # Note: Important Differences for the Type parameter! 137 | # https://docs.bitfinex.com/v1/reference#rest-auth-new-order 138 | # Also see: 139 | # https://github.com/ccxt/ccxt/issues/1207 140 | params = {'type': 'market', 'leverage':self.p.leverage} 141 | 142 | # Specify leverage! 143 | qty = ((cash * self.p.perc_size) / self.data.close[0]) * self.p.leverage # 2x leverage using 95% cash 144 | 145 | # Round to the nearest 8 places otherwise, we can end up with a bit left over when trying to close. 146 | qty = round(qty, 8) 147 | 148 | if pos > 0: 149 | qty = qty + pos 150 | 151 | print('Action : Sell | Qty: {}'.format(qty)) 152 | self.sell(d, size=qty, params=params) 153 | 154 | if d.signal == 0: 155 | 156 | # Close the position 157 | # Note: Important Differences for the Type parameter! 158 | # https://docs.bitfinex.com/v1/reference#rest-auth-new-order 159 | # Also see: 160 | # https://github.com/ccxt/ccxt/issues/1207 161 | params = {'type': 'market'} 162 | print('Action: | Closing Position') 163 | self.close(params=params) 164 | 165 | 166 | 167 | 168 | def notify_data(self, data, status, *args, **kwargs): 169 | print('DATA NOTIF: {}: {}'.format(data._getstatusname(status), ','.join(args))) 170 | 171 | 172 | def notify_order(self, order): 173 | dt = order.data.datetime.datetime() 174 | dn = order.data._name 175 | 176 | print('='*33, 'NOTIFY ORDER', '='*33) 177 | 178 | if order.status == order.Submitted: 179 | print('Date : {}'.format(dt)) 180 | print('Ticker : {}'.format(dn)) 181 | print('Notify : Order Submitted') 182 | 183 | if order.status == order.Accepted: 184 | print('Date : {}'.format(dt)) 185 | print('Ticker : {}'.format(dn)) 186 | print('Notify : Order Accepted') 187 | 188 | if order.status == order.Completed: 189 | print('Date : {}'.format(dt)) 190 | print('Ticker : {}'.format(dn)) 191 | print('Notify : Order Completed') 192 | 193 | if order.status == order.Canceled: 194 | print('Date : {}'.format(dt)) 195 | print('Ticker : {}'.format(dn)) 196 | print('Notify : Order Canceled') 197 | 198 | if order.status == order.Rejected: 199 | print('Date : {}'.format(dt)) 200 | print('Ticker : {}'.format(dn)) 201 | print('Notify : Order Rejected') 202 | 203 | # Example Alerts 204 | # -------------- 205 | # 1. DATA/OHLC 206 | # The ticker should be the same as you use for your broker to 207 | # make life easier. Don't blindly copy the Tradingview Ticker. 208 | # The rest of the string below should be copied in as it. Tradingview 209 | # will replace the values inside {{}} with the actual values. 210 | ''' 211 | {'symbol':'[INSERT TICKER]', 'DT':'{{time}}', 'O':{{open}}, 'H':{{high}}, 'L':{{low}}, 'C':{{close}}, 'V':{{volume}}, 'action':0} 212 | ''' 213 | # 2. SIGNALS 214 | # 3 Types of signal are currently supported. 'long', 'short' and 'flat' 215 | # It is expected that you handle them appropriately and according to your 216 | # taste in backtrader 217 | ''' 218 | {'symbol':'[INSERT TICKER]', 'action':1} 219 | ''' 220 | 221 | print('='*80) 222 | print('Starting Example Strategy') 223 | print('All data feeds must have one bar of data before before you will see any output \n' 224 | 'on the console. Please be patient...') 225 | print('For instructions how to use, see:') 226 | print('='*80) 227 | 228 | debug = False 229 | 230 | # Create an instance of cerebro 231 | cerebro = bt.Cerebro() 232 | 233 | # Get Data 234 | data = TVFeed(dataname='BTC/USD', debug=debug) 235 | 236 | # Add the data feeds 237 | cerebro.adddata(data) 238 | #cerebro.adddata(data2) 239 | 240 | # Set Config 241 | config = {'apiKey': apikey, 242 | 'secret': secret, 243 | 'enableRateLimit': True, 244 | 'rateLimit': 3000 245 | } 246 | 247 | # Add our strategy 248 | cerebro.addstrategy(TVTest) 249 | 250 | 251 | print('Getting Store') 252 | # Create data feeds 253 | store = CCXTStore(exchange='bitfinex', currency='USD', config=config, retries=5, debug=False) 254 | 255 | print('Getting Broker') 256 | broker = store.getbroker() 257 | cerebro.setbroker(broker) 258 | 259 | # Run the strategy 260 | cerebro.run() 261 | -------------------------------------------------------------------------------- /examples/kraken.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | import tv2bt.config 28 | tv2bt.config.PORT = 8124 29 | from tv2bt import TVFeed 30 | import backtrader as bt 31 | from ccxtbt import CCXTStore 32 | 33 | apikey = 'INSERT YOUR API KEY' 34 | secret = 'INSERT YOUR SECRET' 35 | 36 | class Kraken(bt.Strategy): 37 | ''' 38 | Simple strat to test TV Feed. 39 | ''' 40 | 41 | params = ( 42 | ('perc_size', 0.7), 43 | ('fixed_qty', 0.002) 44 | ) 45 | 46 | def __init__(self): 47 | 48 | super(Kraken, self).__init__() 49 | 50 | self.data_info = dict() 51 | 52 | for i, d in enumerate(self.datas): 53 | 54 | # Tracking the last bar is useful when we have data coming in 55 | # at different times. 56 | self.data_info[d._name] = dict() 57 | self.data_info[d._name]['last bar'] = 0 58 | 59 | # When we are not using leverage, there is no concept of a 60 | # position. So we create a dict to store holdings of both 61 | # the base and quote currencies. 62 | ticker_components = d._name.split('/') 63 | self.data_info[d._name]['base'] = ticker_components[0] 64 | self.data_info[d._name]['counter'] = ticker_components[1] 65 | 66 | for comp in ticker_components: 67 | self.data_info[d._name][comp] = dict() 68 | self.data_info[d._name][comp]['cash'] = 0 69 | self.data_info[d._name][comp]['value'] = 0 70 | 71 | 72 | def next(self): 73 | 74 | print('='*80) 75 | print(' '*36,'NEXT') 76 | print('='*80) 77 | 78 | for i, d in enumerate(self.datas): 79 | 80 | dn = d._name 81 | dt = d.datetime.datetime() 82 | bar = len(d) 83 | o = d.open[0] 84 | h = d.high[0] 85 | l = d.low[0] 86 | c = d.close[0] 87 | v = d.volume[0] 88 | sig = d.signal[0] 89 | pos = self.getposition(d).size 90 | base = self.data_info[dn]['base'] 91 | counter = self.data_info[dn]['counter'] 92 | 93 | # Log Position Size 94 | print('Position : {}'.format(pos)) 95 | 96 | # Check we have a new bar and are not repeating an old one. 97 | if bar > self.data_info[dn]['last bar']: 98 | print('DATE : {}'.format(dt)) 99 | print('ASSET : {}'.format(dn)) 100 | print('BAR : {}'.format(bar)) 101 | print('Open : {}'.format(d.open[0])) 102 | print('High : {}'.format(d.high[0])) 103 | print('Low : {}'.format(d.low[0])) 104 | print('Close : {}'.format(d.close[0])) 105 | print('Signal : {}'.format(d.signal[0])) 106 | print('-' * 80) 107 | 108 | 109 | # Save the last bar processed 110 | self.data_info[dn]['last bar'] = bar 111 | 112 | 113 | # Get our balances only if we want to go buy/sell. 114 | # Otherwise, the request slows things down. 115 | # 100 for testing the signal 116 | if d.signal in [1,-1, 100]: 117 | 118 | ticker_components = dn.split('/') 119 | 120 | for comp in ticker_components: 121 | # Get our cash and value to enter a position 122 | cash, value = self.broker.get_wallet_balance(comp) 123 | print('{} : {} in Cash'.format(comp, cash)) 124 | print('{} : {} in Value'.format(comp, value)) 125 | self.data_info[dn][comp]['cash'] = cash 126 | self.data_info[dn][comp]['value'] = value 127 | 128 | # Buy the base currency! 129 | if sig == 1: 130 | 131 | # Kraken reports your balance as value and not cash 132 | qty = (self.data_info[dn][counter]['value'] * self.p.perc_size) / d.close[0] 133 | 134 | # Round to the nearest 8 places otherwise, we can end up with a bit left over when trying to close. 135 | qty = round(qty, 8) 136 | 137 | cat = 'BUY' 138 | print('Action : Buy | Qty: {}'.format(qty)) 139 | self.buy(d, size=qty) 140 | 141 | # Sell the base currency! 142 | if sig == -1: 143 | 144 | # Kraken reports your balance as value and not cash 145 | if self.data_info[dn][base]['value'] > 0: 146 | cat = 'SELL' 147 | print('Action : Sell | Qty: {}'.format(pos)) 148 | self.sell(d, size=pos) 149 | 150 | 151 | def notify_data(self, data, status, *args, **kwargs): 152 | print('DATA NOTIF: {}: {}'.format(data._getstatusname(status), ','.join(args))) 153 | 154 | def notify_order(self, order): 155 | dt = order.data.datetime.datetime() 156 | dn = order.data._name 157 | 158 | print('=' * 33, 'NOTIFY ORDER', '=' * 33) 159 | 160 | if order.status == order.Submitted: 161 | print('Date : {}'.format(dt)) 162 | print('Ticker : {}'.format(dn)) 163 | print('Notify : Order Submitted') 164 | 165 | if order.status == order.Accepted: 166 | print('Date : {}'.format(dt)) 167 | print('Ticker : {}'.format(dn)) 168 | print('Notify : Order Accepted') 169 | 170 | if order.status == order.Completed: 171 | print('Date : {}'.format(dt)) 172 | print('Ticker : {}'.format(dn)) 173 | print('Notify : Order Completed') 174 | 175 | if order.status == order.Canceled: 176 | print('Date : {}'.format(dt)) 177 | print('Ticker : {}'.format(dn)) 178 | print('Notify : Order Canceled') 179 | 180 | if order.status == order.Rejected: 181 | print('Date : {}'.format(dt)) 182 | print('Ticker : {}'.format(dn)) 183 | print('Notify : Order Rejected') 184 | 185 | # Example Alerts 186 | # -------------- 187 | # 1. DATA/OHLC 188 | # The ticker should be the same as you use for your broker to 189 | # make life easier. Don't blindly copy the Tradingview Ticker. 190 | # The rest of the string below should be copied in as it. Tradingview 191 | # will replace the values inside {{}} with the actual values. 192 | ''' 193 | {'symbol':'[INSERT TICKER]', 'DT':'{{time}}', 'O':{{open}}, 'H':{{high}}, 'L':{{low}}, 'C':{{close}}, 'V':{{volume}}, 'action':0} 194 | ''' 195 | # 2. SIGNALS 196 | # 3 Types of signal are currently supported. 'long', 'short' and 'flat' 197 | # It is expected that you handle them appropriately and according to your 198 | # taste in backtrader 199 | ''' 200 | {'symbol':'[INSERT TICKER]', 'action':1} 201 | ''' 202 | 203 | print('='*80) 204 | print('Starting Example Strategy') 205 | print('All data feeds must have one bar of data before before you will see any output \n' 206 | 'on the console. Please be patient...') 207 | print('For instructions how to use, see:') 208 | print('='*80) 209 | 210 | debug = False 211 | 212 | # Create an instance of cerebro 213 | cerebro = bt.Cerebro(quicknotify=True) 214 | 215 | # Get Data 216 | data = TVFeed(dataname='BTC/USD', debug=debug) 217 | 218 | # Add the data feeds 219 | cerebro.adddata(data) 220 | #cerebro.adddata(data2) 221 | 222 | # Set Config 223 | config = {'apiKey': apikey, 224 | 'secret': secret, 225 | 'enableRateLimit': True 226 | } 227 | 228 | 229 | 230 | # Add our strategy 231 | cerebro.addstrategy(Kraken) 232 | 233 | 234 | print('Getting Store') 235 | # Create data feeds 236 | store = CCXTStore(exchange='kraken', currency='USD', config=config, retries=5, debug=False) 237 | 238 | print('Getting Broker') 239 | broker_mapping = { 240 | 'order_types': { 241 | bt.Order.Market: 'market', 242 | bt.Order.Limit: 'limit', 243 | bt.Order.Stop: 'stop-loss', #stop-loss for kraken, stop for bitmex 244 | bt.Order.StopLimit: 'stop limit' 245 | }, 246 | 'mappings':{ 247 | 'closed_order':{ 248 | 'key': 'status', 249 | 'value':'closed' 250 | }, 251 | 'canceled_order':{ 252 | 'key': 'result', 253 | 'value':1} 254 | } 255 | } 256 | broker = store.getbroker(broker_mapping=broker_mapping) 257 | cerebro.setbroker(broker) 258 | 259 | # Run the strategy 260 | cerebro.run() 261 | -------------------------------------------------------------------------------- /fire_alert.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import argparse 3 | 4 | def parse_args(): 5 | parser = argparse.ArgumentParser(description='Test Program to trigger alerts') 6 | 7 | parser.add_argument('--port', 8 | default=8123, 9 | help='Port of the server') 10 | 11 | parser.add_argument('--endpoint', 12 | default='tv', 13 | help='endpoint to target') 14 | 15 | return parser.parse_args() 16 | 17 | args = parse_args() 18 | 19 | while True: 20 | ticker = input('Ticker: ') 21 | ohlcv = input('Send OHLCV? (y/n): ') 22 | action = input('Action: ') 23 | 24 | if action in ['q', 'quit']: 25 | break 26 | 27 | if ohlcv.lower() == 'y': 28 | o = int(input('Open: ')) 29 | h = int(input('High: ')) 30 | l = int(input('Low: ')) 31 | c = int(input('Close: ')) 32 | v = int(input('Volume: ')) 33 | 34 | data = { 35 | 'symbol': ticker, 'O': o, 'H': h, 'L': l, 'C': c, 'V': v, 'action': int(action) 36 | } 37 | 38 | elif ohlcv.lower() == 'n': 39 | 40 | data = { 41 | 'symbol': ticker, 'action': int(action) 42 | } 43 | 44 | else: 45 | print('OHLCV input not recognized. Try again') 46 | 47 | # Send the request! 48 | resp = requests.post('http://0.0.0.0:{}/{}'.format(args.port, args.endpoint), json=data) 49 | print('Status Code: {}'.format(resp.status_code)) 50 | print('Response: {}'.format(resp.text)) 51 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup( 4 | name='tv2bt', 5 | version='1.0', 6 | description='A bridge between Tradingview alerts and Backtrader', 7 | url='https://github.com/Dave-Vallance/tv2bt', 8 | author='Dave Vallance', 9 | author_email='dave@backtest-rookies.com', 10 | license='MIT', 11 | packages=['tv2bt'], 12 | install_requires=['backtrader','flask'], 13 | ) 14 | -------------------------------------------------------------------------------- /tv2bt/__init__.py: -------------------------------------------------------------------------------- 1 | from .tv_feed import * 2 | -------------------------------------------------------------------------------- /tv2bt/config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | PORT = 8123 28 | -------------------------------------------------------------------------------- /tv2bt/server.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | from flask import Flask, request, render_template 28 | from threading import Thread 29 | import queue 30 | import ast 31 | from .config import * 32 | import logging 33 | 34 | 35 | # Stop Logging Loads of informational data 36 | log = logging.getLogger('werkzeug') 37 | log.setLevel(logging.ERROR) 38 | # ------------------------------------------------------------------------------ 39 | # Create Server 40 | # ------------------------------------------------------------------------------ 41 | data_queue = dict() 42 | server = Flask(__name__) 43 | 44 | @server.route("/tv", methods=['POST']) 45 | def alert(): 46 | 47 | data = request.get_data(as_text=True) 48 | 49 | data = ast.literal_eval(data) 50 | if not isinstance(data, dict): 51 | print('Warning Invalid Signal Received') 52 | 53 | return 'Bad Request', 400 54 | 55 | else: 56 | # Check if the key has a queue, if not, make the key. 57 | if data['symbol'] not in data_queue.keys(): 58 | print("WARNING: Symbol not found for alert receieved: {}".format(data['symbol'])) 59 | return 'Bad Request', 400 60 | 61 | try: 62 | 63 | data_queue[data['symbol']].put(data) 64 | 65 | except KeyError as e: 66 | print("WARNING: Data Received not in the correct format. Missing Key: {}".format(e)) 67 | return 'Bad Request', 400 68 | 69 | 70 | return 'OK', 200 71 | 72 | server_thread = Thread(target=server.run, kwargs={'host':'0.0.0.0', 'port':PORT, 'debug': False}) 73 | server_thread.start() 74 | -------------------------------------------------------------------------------- /tv2bt/tv_feed.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: www.backtest-rookies.com 3 | 4 | MIT License 5 | 6 | Copyright (c) 2019 backtest-rookies.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | ''' 26 | 27 | from collections import deque 28 | from datetime import datetime 29 | import backtrader as bt 30 | from backtrader.feed import DataBase 31 | import queue 32 | 33 | # ------------------------------------------------------------------------------ 34 | 35 | class TVFeed(DataBase): 36 | """ 37 | TradingView Data Feed for Backtrader 38 | 39 | Params: 40 | - debug: __bool__, enable debug printing. 41 | """ 42 | 43 | lines = ('signal',) 44 | 45 | params = ( 46 | ('debug', False), 47 | ('kickstart', False) 48 | ) 49 | 50 | # States for the Finite State Machine in _load 51 | _ST_START, _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(4) 52 | 53 | def __init__(self, config={}, retries=5): 54 | 55 | global data_queue 56 | from .server import data_queue 57 | 58 | self._data = deque() # data queue for price data 59 | 60 | data_queue[self.p.dataname] = queue.Queue() 61 | 62 | if self.p.kickstart: 63 | # kickstart the queue with some data 64 | # Next does not seem to be called until all datas have 65 | # at least one bar of data. We can't afford to wait for 66 | # signals 67 | data = {'symbol':''.format(self.p.dataname), 'action':0} 68 | data_queue[self.p.dataname].put(data) 69 | 70 | def start(self): 71 | DataBase.start(self) 72 | 73 | self._state = self._ST_LIVE 74 | 75 | def _load(self): 76 | 77 | if self._state == self._ST_OVER: 78 | return False 79 | 80 | while True: 81 | 82 | # We won't have data for a while when starting so don't raise 83 | # an error if we can't find the key. 84 | try: 85 | 86 | if data_queue[self.p.dataname].empty(): 87 | return None 88 | 89 | data = data_queue[self.p.dataname].get() 90 | 91 | if self.p.debug: 92 | print('{} Data Receieved'.format(self.p.dataname)) 93 | 94 | except KeyError as e: 95 | return None 96 | 97 | # Now we have data, process it. 98 | try: 99 | if 'DT' in data.keys(): 100 | dtime = datetime.strptime(data['DT'], '%Y-%m-%dT%H:%M:%SZ') 101 | else: 102 | dtime = datetime.now() 103 | self.lines.datetime[0] = bt.date2num(dtime) 104 | 105 | if 'O' in data.keys(): 106 | self.lines.open[0] = data['O'] 107 | 108 | if 'H' in data.keys(): 109 | self.lines.high[0] = data['H'] 110 | 111 | if 'L' in data.keys(): 112 | self.lines.low[0] = data['L'] 113 | 114 | if 'C' in data.keys(): 115 | self.lines.close[0] = data['C'] 116 | 117 | if 'V' in data.keys(): 118 | self.lines.volume[0] = data['V'] 119 | 120 | if 'action' in data.keys(): 121 | self.lines.signal[0] = data['action'] 122 | 123 | except (KeyError, TypeError) as e: 124 | print('Bad Syntax in alert. Please check') 125 | print('{}'.format(e)) 126 | print('Data Supplied: {}'.format(data)) 127 | return False 128 | 129 | if self.p.debug: 130 | print('{} Loaded OHLC Data'.format(self.p.dataname)) 131 | 132 | return True 133 | 134 | return None 135 | 136 | 137 | def haslivedata(self): 138 | return self._state == self._ST_LIVE and self._data 139 | 140 | def islive(self): 141 | return True 142 | --------------------------------------------------------------------------------