├── .gitignore ├── .qt_for_python └── uic │ ├── indicatorParameters.py │ └── loadDataFiles.py ├── .vscode └── launch.json ├── CerebroEnhanced.py ├── DataFile.py ├── LICENSE ├── README.md ├── Singleton.py ├── SkinokBacktraderUI.py ├── SofienKaabar.py ├── common.py ├── connectors └── OandaV20Connector.py ├── custom_indicators ├── BollingerBandsBandwitch.py ├── __init__.py ├── ema.py ├── fin_macd.py ├── ichimoku.py ├── rsi.py ├── sma.py ├── stochastic.py └── stochasticRsi.py ├── data ├── Source 1 │ ├── EURUSD_D1.csv │ ├── EURUSD_H1.csv │ ├── EURUSD_H4.csv │ ├── EURUSD_M1.csv │ ├── EURUSD_M15.csv │ ├── EURUSD_M15_2020.csv │ ├── EURUSD_M15_2020_1kbar.csv │ ├── EURUSD_M30.csv │ └── EURUSD_M5.csv ├── Source 2 │ └── EURUSD_M15_light_2012.csv └── sh600000.csv ├── dataManager.py ├── finplotWindow.py ├── images ├── gitkeep.txt ├── overview.png ├── overview2.png └── overview3.png ├── indicatorParametersUI.py ├── loadDataFilesUI.py ├── main.py ├── metaStrategy.py ├── observers └── SkinokObserver.py ├── qt.conf ├── settings.json ├── solution.code-workspace ├── strategies ├── AiStableBaselinesModel.py ├── AiTensorFlowModel.py ├── AiTorchModel.py ├── ichimokuStrat1.py └── sma_crossover.py ├── strategyResultsUI.py ├── strategyTesterUI.py ├── stylesheets ├── Dark.qss └── defaut.qss ├── ui ├── indicatorParameters.ui ├── loadDataFiles.ui ├── loadDataFiles_ui.py ├── strategyResults.ui ├── strategyResults_ui.py ├── strategyTester.ui └── strategyTester_ui.py ├── userConfig.py ├── userData.json ├── userInterface.py ├── wallet.py └── websockets └── binance.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/*.* 2 | /__pycache__ 3 | *.pyc 4 | -------------------------------------------------------------------------------- /.qt_for_python/uic/indicatorParameters.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'c:\perso\trading\anaconda3\backtrader-ichimoku\ui\indicatorParameters.ui' 4 | # 5 | # Created by: PyQt6 UI code generator 5.15.5 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt6 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_Dialog(object): 15 | def setupUi(self, Dialog): 16 | Dialog.setObjectName("Dialog") 17 | Dialog.resize(400, 300) 18 | self.gridLayout = QtWidgets.QGridLayout(Dialog) 19 | self.gridLayout.setObjectName("gridLayout") 20 | self.title = QtWidgets.QLabel(Dialog) 21 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) 22 | sizePolicy.setHorizontalStretch(0) 23 | sizePolicy.setVerticalStretch(0) 24 | sizePolicy.setHeightForWidth(self.title.sizePolicy().hasHeightForWidth()) 25 | self.title.setSizePolicy(sizePolicy) 26 | self.title.setMinimumSize(QtCore.QSize(0, 40)) 27 | self.title.setAlignment(QtCore.Qt.AlignCenter) 28 | self.title.setObjectName("title") 29 | self.gridLayout.addWidget(self.title, 0, 0, 1, 1) 30 | self.parameterLayout = QtWidgets.QFormLayout() 31 | self.parameterLayout.setObjectName("parameterLayout") 32 | self.gridLayout.addLayout(self.parameterLayout, 1, 0, 1, 1) 33 | self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) 34 | self.buttonBox.setOrientation(QtCore.Qt.Horizontal) 35 | self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) 36 | self.buttonBox.setObjectName("buttonBox") 37 | self.gridLayout.addWidget(self.buttonBox, 2, 0, 1, 1) 38 | 39 | self.retranslateUi(Dialog) 40 | self.buttonBox.accepted.connect(Dialog.accept) 41 | self.buttonBox.rejected.connect(Dialog.reject) 42 | QtCore.QMetaObject.connectSlotsByName(Dialog) 43 | 44 | def retranslateUi(self, Dialog): 45 | _translate = QtCore.QCoreApplication.translate 46 | Dialog.setWindowTitle(_translate("Dialog", "Custom indicator configuration")) 47 | self.title.setText(_translate("Dialog", "Customize indicator")) 48 | -------------------------------------------------------------------------------- /.qt_for_python/uic/loadDataFiles.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'c:\perso\trading\anaconda3\backtrader-ichimoku\ui\loadDataFiles.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.5 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt6 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_Form(object): 15 | def setupUi(self, Form): 16 | Form.setObjectName("Form") 17 | Form.resize(488, 458) 18 | self.gridLayout_2 = QtWidgets.QGridLayout(Form) 19 | self.gridLayout_2.setObjectName("gridLayout_2") 20 | self.dataFilesListWidget = QtWidgets.QListWidget(Form) 21 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred) 22 | sizePolicy.setHorizontalStretch(0) 23 | sizePolicy.setVerticalStretch(0) 24 | sizePolicy.setHeightForWidth(self.dataFilesListWidget.sizePolicy().hasHeightForWidth()) 25 | self.dataFilesListWidget.setSizePolicy(sizePolicy) 26 | self.dataFilesListWidget.setObjectName("dataFilesListWidget") 27 | self.gridLayout_2.addWidget(self.dataFilesListWidget, 3, 0, 1, 1) 28 | self.label_4 = QtWidgets.QLabel(Form) 29 | self.label_4.setObjectName("label_4") 30 | self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) 31 | self.importPB = QtWidgets.QPushButton(Form) 32 | self.importPB.setMinimumSize(QtCore.QSize(0, 40)) 33 | self.importPB.setObjectName("importPB") 34 | self.gridLayout_2.addWidget(self.importPB, 5, 0, 1, 2) 35 | self.verticalLayout = QtWidgets.QVBoxLayout() 36 | self.verticalLayout.setObjectName("verticalLayout") 37 | self.gridLayout_2.addLayout(self.verticalLayout, 3, 1, 1, 1) 38 | self.groupBox = QtWidgets.QGroupBox(Form) 39 | self.groupBox.setObjectName("groupBox") 40 | self.gridLayout = QtWidgets.QGridLayout(self.groupBox) 41 | self.gridLayout.setObjectName("gridLayout") 42 | self.semicolonRB = QtWidgets.QRadioButton(self.groupBox) 43 | self.semicolonRB.setObjectName("semicolonRB") 44 | self.gridLayout.addWidget(self.semicolonRB, 3, 3, 1, 1) 45 | self.label = QtWidgets.QLabel(self.groupBox) 46 | self.label.setObjectName("label") 47 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 48 | self.label_3 = QtWidgets.QLabel(self.groupBox) 49 | self.label_3.setObjectName("label_3") 50 | self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) 51 | self.label_2 = QtWidgets.QLabel(self.groupBox) 52 | self.label_2.setObjectName("label_2") 53 | self.gridLayout.addWidget(self.label_2, 3, 0, 1, 1) 54 | self.tabRB = QtWidgets.QRadioButton(self.groupBox) 55 | self.tabRB.setChecked(True) 56 | self.tabRB.setObjectName("tabRB") 57 | self.gridLayout.addWidget(self.tabRB, 3, 1, 1, 1) 58 | self.commaRB = QtWidgets.QRadioButton(self.groupBox) 59 | self.commaRB.setObjectName("commaRB") 60 | self.gridLayout.addWidget(self.commaRB, 3, 2, 1, 1) 61 | self.filePathLE = QtWidgets.QLineEdit(self.groupBox) 62 | self.filePathLE.setObjectName("filePathLE") 63 | self.gridLayout.addWidget(self.filePathLE, 0, 1, 1, 3) 64 | self.openFilePB = QtWidgets.QToolButton(self.groupBox) 65 | self.openFilePB.setObjectName("openFilePB") 66 | self.gridLayout.addWidget(self.openFilePB, 0, 4, 1, 1) 67 | self.datetimeFormatLE = QtWidgets.QLineEdit(self.groupBox) 68 | self.datetimeFormatLE.setObjectName("datetimeFormatLE") 69 | self.gridLayout.addWidget(self.datetimeFormatLE, 1, 1, 1, 4) 70 | self.loadFilePB = QtWidgets.QPushButton(self.groupBox) 71 | self.loadFilePB.setMinimumSize(QtCore.QSize(0, 40)) 72 | self.loadFilePB.setObjectName("loadFilePB") 73 | self.gridLayout.addWidget(self.loadFilePB, 4, 1, 1, 4) 74 | self.errorLabel = QtWidgets.QLabel(self.groupBox) 75 | self.errorLabel.setStyleSheet("color: red") 76 | self.errorLabel.setText("") 77 | self.errorLabel.setAlignment(QtCore.Qt.AlignCenter) 78 | self.errorLabel.setObjectName("errorLabel") 79 | self.gridLayout.addWidget(self.errorLabel, 5, 0, 1, 5) 80 | self.gridLayout_2.addWidget(self.groupBox, 0, 0, 1, 2) 81 | self.label_5 = QtWidgets.QLabel(Form) 82 | self.label_5.setStyleSheet("font-style: italic") 83 | self.label_5.setScaledContents(False) 84 | self.label_5.setAlignment(QtCore.Qt.AlignCenter) 85 | self.label_5.setWordWrap(True) 86 | self.label_5.setObjectName("label_5") 87 | self.gridLayout_2.addWidget(self.label_5, 4, 0, 1, 1) 88 | 89 | self.retranslateUi(Form) 90 | QtCore.QMetaObject.connectSlotsByName(Form) 91 | 92 | def retranslateUi(self, Form): 93 | _translate = QtCore.QCoreApplication.translate 94 | Form.setWindowTitle(_translate("Form", "Import one or multiple data files")) 95 | self.label_4.setText(_translate("Form", "List of all files to import in cerebro")) 96 | self.importPB.setText(_translate("Form", "Import all data files")) 97 | self.groupBox.setTitle(_translate("Form", "Loading a new data file")) 98 | self.semicolonRB.setText(_translate("Form", "semicolon")) 99 | self.label.setText(_translate("Form", "Import a new data file")) 100 | self.label_3.setText(_translate("Form", "Date time format")) 101 | self.label_2.setText(_translate("Form", "Separator")) 102 | self.tabRB.setText(_translate("Form", "tab")) 103 | self.commaRB.setText(_translate("Form", "comma")) 104 | self.openFilePB.setText(_translate("Form", "...")) 105 | self.loadFilePB.setText(_translate("Form", "Load .CSV file")) 106 | self.label_5.setText(_translate("Form", "Files should be ordered from lower (on top) to higher timeframe (at bottom).")) 107 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "configurations": [ 4 | { 5 | "name": "Python: Current File (Integrated Terminal)", 6 | "type": "python", 7 | "request": "launch", 8 | "program": "${file}", 9 | "console": "integratedTerminal", 10 | "cwd": "C:/perso/trading/anaconda3/backtrader-ichimoku/" 11 | }] 12 | } -------------------------------------------------------------------------------- /CerebroEnhanced.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2021-2025 Skinok 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | from backtrader import Cerebro 22 | 23 | class CerebroEnhanced(Cerebro): 24 | 25 | def __init__(self): 26 | super().__init__() 27 | pass 28 | 29 | def clearStrategies(self): 30 | self.strats.clear() 31 | 32 | pass 33 | -------------------------------------------------------------------------------- /DataFile.py: -------------------------------------------------------------------------------- 1 | 2 | class DataFile(): 3 | 4 | def __init__(self): 5 | 6 | # File location 7 | self.filePath = "" 8 | self.fileName = "" 9 | 10 | # File import parameters 11 | self.timeFrame = "" 12 | self.separator = "" 13 | self.timeFormat = "" 14 | 15 | # Panda data frame 16 | self.dataFrame = None 17 | 18 | pass 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Skinok Todar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Discord](https://img.shields.io/badge/DISCORD-Join%20the%20server-blue?style=for-the-badge&logo=discord)](https://discord.gg/56ERy324) 2 | 3 | # Skinok backtrader UI (PyQt and finplot) 4 | 5 | ![Backtrader PyQt Ui](./images/overview.png "Backtrader PyQt Ui") 6 | 7 | ![Backtrader PyQt Ui 2](./images/overview2.png "Backtrader PyQt Ui") 8 | 9 | ![Backtrader PyQt Ui 3](./images/overview3.png "Backtrader PyQt Ui") 10 | 11 | # How to install ? 12 | 13 | You should have python installed on your machine (obvisously) 14 | 15 | Please run the following commands : 16 | 17 | ``` 18 | pip install git+https://github.com/backtrader2/backtrader matplotlib requests \ 19 | websocket websocket-client oandapy qdarkstyle git+https://github.com/blampe/IbPy.git \ 20 | git+https://github.com/oanda/oandapy.git git+https://github.com/Skinok/finplot.git 21 | ``` 22 | 23 | # How to use it ? 24 | 25 | * Put your CSV Data in the *data* folder 26 | * Create your strategy and put it in the *strategies* folder 27 | Your strategy *file* name should be exactly the same as the strategy *class* name 28 | You can take a look at the provided exemples 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /Singleton.py: -------------------------------------------------------------------------------- 1 | class Singleton: 2 | __instance = None 3 | 4 | def __new__(cls,*args, **kwargs): 5 | if cls.__instance is None : 6 | cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwargs) 7 | return cls.__instance -------------------------------------------------------------------------------- /SkinokBacktraderUI.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | 20 | #import sys 21 | #sys.path.append('D:/perso/trading/anaconda3/backtrader2') 22 | import backtrader as bt 23 | from CerebroEnhanced import * 24 | from PyQt6 import QtWidgets 25 | 26 | import sys, os 27 | from DataFile import DataFile 28 | 29 | from dataManager import DataManager 30 | sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/observers') 31 | sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/strategies') 32 | sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../finplot') 33 | 34 | import pandas as pd 35 | 36 | # local files 37 | import userInterface as Ui 38 | 39 | from observers.SkinokObserver import SkinokObserver 40 | from wallet import Wallet 41 | from userConfig import UserConfig 42 | 43 | from PyQt6 import QtCore 44 | 45 | class SkinokBacktraderUI: 46 | 47 | def __init__(self): 48 | 49 | # init variables 50 | self.data = None 51 | self.startingcash = 10000.0 52 | 53 | # Init attributes 54 | self.strategyParameters = {} 55 | self.dataFiles = {} 56 | 57 | # Global is here to update the Ui in observers easily, if you find a better way, don't hesistate to tell me (Skinok) 58 | global interface 59 | interface = Ui.UserInterface(self) 60 | self.interface = interface 61 | 62 | global wallet 63 | wallet = Wallet(self.startingcash ) 64 | self.wallet = wallet 65 | 66 | self.resetCerebro() 67 | 68 | # Once everything is created, initialize data 69 | self.interface.initialize() 70 | 71 | # Timeframes 72 | self.timeFrameIndex = {"M1" : 0, "M5" : 10, "M15": 20, "M30": 30, "H1":40, "H4":50, "D":60, "W":70} 73 | 74 | self.dataManager = DataManager() 75 | 76 | self.datafileName_to_dataFile={} 77 | 78 | # Restore previous session for faster tests 79 | self.loadConfig() 80 | 81 | pass 82 | 83 | def loadConfig(self): 84 | 85 | userConfig = UserConfig() 86 | userConfig.loadConfigFile() 87 | 88 | isEmpty = True 89 | 90 | # Load previous data files 91 | #for timeframe in self.timeFrameIndex.keys(): 92 | 93 | for timeFrame in userConfig.data.keys(): 94 | 95 | dataFile = DataFile() 96 | 97 | dataFile.filePath = userConfig.data[timeFrame]['filePath'] 98 | dataFile.fileName = userConfig.data[timeFrame]['fileName'] 99 | dataFile.timeFormat = userConfig.data[timeFrame]['timeFormat'] 100 | dataFile.separator = userConfig.data[timeFrame]['separator'] 101 | dataFile.timeFrame = timeFrame 102 | 103 | if not dataFile.timeFormat in self.dataFiles: 104 | dataFile.dataFrame, errorMessage = self.dataManager.loadDataFrame(dataFile) 105 | 106 | if dataFile.dataFrame is not None: 107 | 108 | isEmpty = False 109 | 110 | self.datafileName_to_dataFile[dataFile.fileName] = dataFile 111 | 112 | # REALLY UGLY : it should be a function of user interface 113 | items = self.interface.strategyTesterUI.loadDataFileUI.dataFilesListWidget.findItems(dataFile.fileName, QtCore.Qt.MatchFixedString) 114 | 115 | if len(items) == 0: 116 | self.interface.strategyTesterUI.loadDataFileUI.dataFilesListWidget.addItem(dataFile.fileName) 117 | 118 | self.dataFiles[dataFile.timeFrame] = dataFile 119 | 120 | if not isEmpty: 121 | self.importData() 122 | 123 | pass 124 | 125 | def removeTimeframe(self, timeFrame): 126 | 127 | # Delete from controler 128 | del self.dataFiles[timeFrame] 129 | 130 | # Delete from chart 131 | self.interface.deleteChartDock(timeFrame) 132 | 133 | # Delete from Cerebro ? 134 | self.resetCerebro() 135 | 136 | pass 137 | 138 | def resetCerebro(self): 139 | 140 | # create a "Cerebro" engine instance 141 | self.cerebro = CerebroEnhanced() 142 | 143 | # Then add obersers and analyzers 144 | self.cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name = "ta") 145 | 146 | ''' 147 | self.cerebro.addanalyzer(bt.analyzers.PyFolio, _name='PyFolio') 148 | self.cerebro.addanalyzer(bt.analyzers.SharpeRatio) 149 | self.cerebro.addanalyzer(bt.analyzers.Transactions) 150 | self.cerebro.addanalyzer(bt.analyzers.Returns) 151 | self.cerebro.addanalyzer(bt.analyzers.Position) 152 | 153 | self.cerebro.addobserver(bt.observers.Broker) 154 | self.cerebro.addobserver(bt.observers.Trades) 155 | self.cerebro.addobserver(bt.observers.BuySell) 156 | self.cerebro.addanalyzer(bt.analyzers.Transactions, _name='Transactions') 157 | 158 | ''' 159 | 160 | # Add an observer to watch the strat running and update the progress bar values 161 | self.cerebro.addobserver( SkinokObserver ) 162 | 163 | # Add data to cerebro 164 | if self.data is not None: 165 | self.cerebro.adddata(self.data) # Add the data feed 166 | 167 | pass 168 | 169 | 170 | def importData(self): 171 | 172 | try: 173 | 174 | timeFrames = list(self.dataFiles.keys()) 175 | 176 | # Sort data by timeframe 177 | # For cerebro, we need to add lower timeframes first 178 | timeFrames.sort( key=lambda x: self.timeFrameIndex[x] ) 179 | 180 | # Files should be loaded in the good order 181 | for timeFrame in timeFrames: 182 | 183 | df = self.dataFiles[timeFrame].dataFrame 184 | 185 | # Datetime first column : 2012-12-28 17:45:00 186 | #self.dataframe['TimeInt'] = pd.to_datetime(self.dataframe.index).astype('int64') # use finplot's internal representation, which is ns 187 | 188 | # Pass it to the backtrader datafeed and add it to the cerebro 189 | self.data = bt.feeds.PandasData(dataname=df, timeframe=bt.TimeFrame.Minutes) 190 | 191 | # Add data to cerebro : only add data when all files have been selected for multi-timeframes 192 | self.cerebro.adddata(self.data) # Add the data feed 193 | 194 | # Create the chart window for the good timeframe (if it does not already exists?) 195 | self.interface.createChartDock(timeFrame) 196 | 197 | # Draw charts based on input data 198 | self.interface.drawChart(df, timeFrame) 199 | 200 | # Enable run button 201 | self.interface.strategyTesterUI.runBacktestBtn.setEnabled(True) 202 | 203 | return True 204 | 205 | except AttributeError as e: 206 | print("AttributeError error:" + str(e) + " " + str(sys.exc_info()[0])) 207 | except KeyError as e: 208 | print("KeyError error:" + str(e) + " " + str(sys.exc_info()[0])) 209 | except: 210 | print("Unexpected error:" + str(sys.exc_info()[0])) 211 | return False 212 | pass 213 | 214 | 215 | 216 | def addStrategy(self, strategyName): 217 | 218 | #For now, only one strategy is allowed at a time 219 | self.cerebro.clearStrategies() 220 | 221 | # Reset strategy parameters 222 | self.strategyParameters = {} 223 | 224 | mod = __import__(strategyName, fromlist=[strategyName]) # first strategyName is the file name, and second (fromlist) is the class name 225 | self.strategyClass = getattr(mod, strategyName) # class name in the file 226 | 227 | # Add strategy parameters 228 | self.interface.fillStrategyParameters(self.strategyClass) 229 | 230 | pass 231 | 232 | def strategyParametersChanged(self, widget, parameterName, parameterOldValue): 233 | 234 | # todo something 235 | if len(widget.text()) > 0: 236 | 237 | param = self.strategyClass.params._get(self.strategyClass.params,parameterName) 238 | 239 | if isinstance(param, bool): 240 | self.strategyParameters[parameterName] = widget.checkState() == QtCore.Qt.CheckState.Checked 241 | elif isinstance(param, int): 242 | self.strategyParameters[parameterName] = int(widget.text()) 243 | elif isinstance(param, float): 244 | self.strategyParameters[parameterName] = float(widget.text()) 245 | else: 246 | self.strategyParameters[parameterName] = widget.text() 247 | 248 | pass 249 | 250 | def strategyParametersSave(self, parameterName, parameterValue): 251 | self.strategyParameters[parameterName] = parameterValue 252 | pass 253 | 254 | def run(self): 255 | 256 | #Reset cerebro internal variables 257 | self.resetCerebro() 258 | 259 | # UI label 260 | self.interface.strategyTesterUI.runLabel.setText("Running strategy...") 261 | 262 | self.interface.resetChart() 263 | 264 | # Add strategy here to get modified parameters 265 | params = self.strategyParameters 266 | self.strategyIndex = self.cerebro.addstrategy(self.strategyClass, params) 267 | 268 | # Wallet Management : reset between each run 269 | self.cerebro.broker.setcash(self.startingcash) 270 | self.wallet.reset(self.startingcash) 271 | 272 | # Compute strategy results 273 | results = self.cerebro.run() # run it all 274 | self.strat_results = results[0] # results of the first strategy 275 | 276 | # Display results 277 | self.displayStrategyResults() 278 | 279 | # UI label 280 | self.interface.strategyTesterUI.runLabel.setText("Strategy backtest completed.") 281 | 282 | pass 283 | 284 | def displayStrategyResults(self): 285 | # Stats on trades 286 | #portfolio_stats = self.strat_results.analyzers.getbyname('PyFolio') 287 | #self.returns, self.positions, self.transactions, self.gross_lev = portfolio_stats.get_pf_items() 288 | #self.portfolio_transactions = self.strat_results.analyzers.Transactions.get_analysis().items() 289 | #self.returns.index = self.returns.index.tz_convert(None) 290 | 291 | #self.interface.createTransactionsUI(self.portfolio_transactions) 292 | self.interface.fillSummaryUI(self.strat_results.stats.broker.cash[0], self.strat_results.stats.broker.value[0], self.strat_results.analyzers.ta.get_analysis()) 293 | self.interface.fillTradesUI(self.strat_results._trades.items()) 294 | self.interface.dock_strategyResultsUI.show() 295 | #self.interface.drawTrades(self.strat_results._trades.items()) 296 | #Orders filters 297 | self.myOrders = [] 298 | for order in self.strat_results._orders: 299 | if order.status in [order.Completed]: 300 | self.myOrders.append(order) 301 | 302 | self.interface.setOrders(self.myOrders) 303 | 304 | # Profit and Loss 305 | pnl_data = {} 306 | 307 | pnl_data['value'] = self.wallet.value_list 308 | pnl_data['equity'] = self.wallet.equity_list 309 | pnl_data['cash'] = self.wallet.cash_list 310 | 311 | # really uggly 312 | pnl_data['time'] = list(self.dataFiles.values())[0].dataFrame.index 313 | 314 | # draw charts 315 | df = pd.DataFrame(pnl_data) 316 | self.interface.displayPnL( df ) 317 | 318 | pass 319 | 320 | def displayUI(self): 321 | self.interface.show() 322 | pass 323 | 324 | def cashChanged(self, cashString): 325 | 326 | if len(cashString) > 0: 327 | self.startingcash = float(cashString) 328 | self.cerebro.broker.setcash(self.startingcash) 329 | 330 | pass 331 | 332 | -------------------------------------------------------------------------------- /SofienKaabar.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def rsi(Data, rsi_lookback, what1, what2): 4 | 5 | # From exponential to smoothed 6 | rsi_lookback = (rsi_lookback * 2) - 1 7 | 8 | # Get the difference in price from previous step 9 | delta = [] 10 | 11 | for i in range(len(Data)): 12 | try: 13 | diff = Data[i, what1] - Data[i - 1, what1] 14 | delta = np.append(delta, diff) 15 | except IndexError: 16 | pass 17 | 18 | delta = np.insert(delta, 0, 0, axis = 0) 19 | delta = delta[1:] 20 | 21 | # Make the positive gains (up) and negative gains (down) Series 22 | up, down = delta.copy(), delta.copy() 23 | up[up < 0] = 0 24 | down[down > 0] = 0 25 | 26 | up = np.array(up) 27 | down = np.array(down) 28 | 29 | roll_up = up 30 | roll_down = down 31 | 32 | roll_up = np.reshape(roll_up, (-1, 1)) 33 | roll_down = np.reshape(roll_down, (-1, 1)) 34 | 35 | roll_up = adder(roll_up, 3) 36 | roll_down = adder(roll_down, 3) 37 | 38 | roll_up = ema(roll_up, 2, rsi_lookback, what2, 1) 39 | roll_down = ema(abs(roll_down), 2, rsi_lookback, what2, 1) 40 | 41 | roll_up = roll_up[rsi_lookback:, 1:2] 42 | roll_down = roll_down[rsi_lookback:, 1:2] 43 | Data = Data[rsi_lookback + 1:,] 44 | 45 | # Calculate the RS & RSI 46 | RS = roll_up / roll_down 47 | RSI = (100.0 - (100.0 / (1.0 + RS))) 48 | RSI = np.array(RSI) 49 | RSI = np.reshape(RSI, (-1, 1)) 50 | RSI = RSI[1:,] 51 | 52 | Data = np.concatenate((Data, RSI), axis = 1) 53 | return Data 54 | 55 | def ma(Data, lookback, what, where): 56 | 57 | for i in range(len(Data)): 58 | try: 59 | Data[i, where] = (Data[i - lookback + 1:i + 1, what].mean()) 60 | except IndexError: 61 | pass 62 | 63 | return Data 64 | 65 | def ema(Data, alpha, lookback, what, where): 66 | 67 | # alpha is the smoothing factor 68 | # window is the lookback period 69 | # what is the column that needs to have its average calculated 70 | # where is where to put the exponential moving average 71 | 72 | alpha = alpha / (lookback + 1.0) 73 | beta = 1 - alpha 74 | 75 | # First value is a simple SMA 76 | Data = ma(Data, lookback, what, where) 77 | 78 | # Calculating first EMA 79 | Data[lookback + 1, where] = (Data[lookback + 1, what] * alpha) + (Data[lookback, where] * beta) 80 | # Calculating the rest of EMA 81 | for i in range(lookback + 2, len(Data)): 82 | try: 83 | Data[i, where] = (Data[i, what] * alpha) + (Data[i - 1, where] * beta) 84 | 85 | except IndexError: 86 | pass 87 | return Data 88 | 89 | # The function to add a certain number of columns 90 | def adder(Data, times): 91 | 92 | for i in range(1, times + 1): 93 | 94 | z = np.zeros((len(Data), 1), dtype = float) 95 | Data = np.append(Data, z, axis = 1) 96 | return Data 97 | # The function to deleter a certain number of columns 98 | def deleter(Data, index, times): 99 | 100 | for i in range(1, times + 1): 101 | 102 | Data = np.delete(Data, index, axis = 1) 103 | return Data 104 | # The function to delete a certain number of rows from the beginning 105 | def jump(Data, jump): 106 | 107 | Data = Data[jump:, ] 108 | 109 | return Data 110 | 111 | 112 | def stochastic(Data, lookback, what, high, low, where): 113 | 114 | for i in range(len(Data)): 115 | 116 | try: 117 | Data[i, where] = (Data[i, what] - min(Data[i - lookback + 1:i + 1, low])) / (max(Data[i - lookback + 1:i + 1, high]) - min(Data[i - lookback + 1:i + 1, low])) 118 | 119 | except ValueError: 120 | pass 121 | 122 | Data[:, where] = Data[:, where] * 100 123 | return Data 124 | 125 | # The Data variable refers to the OHLC array 126 | # The lookback variable refers to the period (5, 14, 21, etc.) 127 | # The what variable refers to the closing price 128 | # The high variable refers to the high price 129 | # The low variable refers to the low price 130 | # The where variable refers to where to put the Oscillator 131 | def stoch_rsi(Data, lookback, where): 132 | 133 | # Calculating RSI of the Closing prices 134 | Data = rsi(Data, lookback, 3, 0) 135 | 136 | # Adding two columns 137 | Data = adder(Data, 2) 138 | 139 | for i in range(len(Data)): 140 | 141 | try: 142 | Data[i, where + 1] = (Data[i, where] - min(Data[i - lookback + 1:i + 1, where])) / (max(Data[i - lookback + 1:i + 1, where]) - min(Data[i - lookback + 1:i + 1, where])) 143 | 144 | except ValueError: 145 | pass 146 | 147 | Data[:, where + 1] = Data[:, where + 1] * 100 148 | 149 | # Signal Line using a 3-period moving average 150 | Data = ma(Data, 3, where + 1, where + 2) 151 | 152 | Data = deleter(Data, where, 2) 153 | Data = jump(Data, lookback) 154 | 155 | return Data -------------------------------------------------------------------------------- /common.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import numpy as np 3 | 4 | from math import nan 5 | 6 | def calc_parabolic_sar(df, af=0.2, steps=10): 7 | up = True 8 | sars = [nan] * len(df) 9 | sar = ep_lo = df.Low.iloc[0] 10 | ep = ep_hi = df.High.iloc[0] 11 | aaf = af 12 | aaf_step = aaf / steps 13 | af = 0 14 | for i,(hi,lo) in enumerate(zip(df.High, df.Low)): 15 | # parabolic sar formula: 16 | sar = sar + af * (ep - sar) 17 | # handle new extreme points 18 | if hi > ep_hi: 19 | ep_hi = hi 20 | if up: 21 | ep = ep_hi 22 | af = min(aaf, af+aaf_step) 23 | elif lo < ep_lo: 24 | ep_lo = lo 25 | if not up: 26 | ep = ep_lo 27 | af = min(aaf, af+aaf_step) 28 | # handle switch 29 | if up: 30 | if lo < sar: 31 | up = not up 32 | sar = ep_hi 33 | ep = ep_lo = lo 34 | af = 0 35 | else: 36 | if hi > sar: 37 | up = not up 38 | sar = ep_lo 39 | ep = ep_hi = hi 40 | af = 0 41 | sars[i] = sar 42 | df['sar'] = sars 43 | return df['sar'] 44 | 45 | 46 | def calc_rsi(df, n=14): 47 | diff = df.Close.diff().values 48 | gains = diff 49 | losses = -diff 50 | gains[~(gains>0)] = 0.0 51 | losses[~(losses>0)] = 1e-10 # we don't want divide by zero/NaN 52 | m = (n-1) / n 53 | ni = 1 / n 54 | g = gains[n] = gains[:n].mean() 55 | l = losses[n] = losses[:n].mean() 56 | gains[:n] = losses[:n] = nan 57 | for i,v in enumerate(gains[n:],n): 58 | g = gains[i] = ni*v + m*g 59 | for i,v in enumerate(losses[n:],n): 60 | l = losses[i] = ni*v + m*l 61 | rs = gains / losses 62 | rsi = 100 - (100/(1+rs)) 63 | return rsi 64 | 65 | 66 | def calc_stochastic_oscillator(df, n=14, m=3, smooth=3): 67 | lo = df.Low.rolling(n).min() 68 | hi = df.High.rolling(n).max() 69 | k = 100 * (df.Close-lo) / (hi-lo) 70 | d = k.rolling(m).mean() 71 | return k, d 72 | 73 | 74 | def calc_stochasticRsi_oscillator(df, n=14, m=3, smooth=3): 75 | lo = df.Low.rolling(n).min() 76 | hi = df.High.rolling(n).max() 77 | k = 100 * (df.Close-lo) / (hi-lo) 78 | d = k.rolling(m).mean() 79 | return k, d 80 | 81 | 82 | # calculating RSI (gives the same values as TradingView) 83 | # https://stackoverflow.com/questions/20526414/relative-strength-index-in-python-pandas 84 | def RSI(series, period=14): 85 | delta = series.diff().dropna() 86 | ups = delta * 0 87 | downs = ups.copy() 88 | ups[delta > 0] = delta[delta > 0] 89 | downs[delta < 0] = -delta[delta < 0] 90 | ups[ups.index[period-1]] = np.mean( ups[:period] ) #first value is sum of avg gains 91 | ups = ups.drop(ups.index[:(period-1)]) 92 | downs[downs.index[period-1]] = np.mean( downs[:period] ) #first value is sum of avg losses 93 | downs = downs.drop(downs.index[:(period-1)]) 94 | rs = ups.ewm(com=period-1,min_periods=0,adjust=False,ignore_na=False).mean() / \ 95 | downs.ewm(com=period-1,min_periods=0,adjust=False,ignore_na=False).mean() 96 | return 100 - 100 / (1 + rs) 97 | 98 | 99 | # calculating Stoch RSI (gives the same values as TradingView) 100 | # https://www.tradingview.com/wiki/Stochastic_RSI_(STOCH_RSI) 101 | def StochRSI(series, period=14, smoothK=3, smoothD=3): 102 | # Calculate RSI 103 | delta = series.diff().dropna() 104 | ups = delta * 0 105 | downs = ups.copy() 106 | ups[delta > 0] = delta[delta > 0] 107 | downs[delta < 0] = -delta[delta < 0] 108 | ups[ups.index[period-1]] = np.mean( ups[:period] ) #first value is sum of avg gains 109 | ups = ups.drop(ups.index[:(period-1)]) 110 | downs[downs.index[period-1]] = np.mean( downs[:period] ) #first value is sum of avg losses 111 | downs = downs.drop(downs.index[:(period-1)]) 112 | rs = ups.ewm(com=period-1,min_periods=0,adjust=False,ignore_na=False).mean() / \ 113 | downs.ewm(com=period-1,min_periods=0,adjust=False,ignore_na=False).mean() 114 | rsi = 100 - 100 / (1 + rs) 115 | 116 | # Calculate StochRSI 117 | stochrsi = (rsi - rsi.rolling(period).min()) / (rsi.rolling(period).max() - rsi.rolling(period).min()) 118 | stochrsi_K = stochrsi.rolling(smoothK).mean() 119 | stochrsi_D = stochrsi_K.rolling(smoothD).mean() 120 | 121 | return stochrsi, stochrsi_K, stochrsi_D -------------------------------------------------------------------------------- /connectors/OandaV20Connector.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Après avoir importé les librairies nécessaires 4 | class OandaV20Connector(OandaV20Store): 5 | # Le constructeur de la classe 6 | def __init__(self, token, account, practice=True): 7 | # Appelle le constructeur de la classe parente 8 | super().__init__(token=token, account=account, practice=practice) 9 | # Crée un broker à partir du store 10 | self.broker = self.getbroker() 11 | # Crée un data feed à partir du store 12 | self.data = self.getdata(dataname="EUR_USD", timeframe=bt.TimeFrame.Minutes, compression=1) 13 | 14 | # Une méthode pour ajouter le data feed au Cerebro 15 | def add_data(self, cerebro): 16 | cerebro.adddata(self.data) 17 | 18 | # Une méthode pour ajouter le broker au Cerebro 19 | def add_broker(self, cerebro): 20 | cerebro.setbroker(self.broker) 21 | -------------------------------------------------------------------------------- /custom_indicators/BollingerBandsBandwitch.py: -------------------------------------------------------------------------------- 1 | from backtrader import bt 2 | 3 | class BollingerBandsBandwitch(bt.ind.BollingerBands): 4 | 5 | ''' 6 | Extends the Bollinger Bands with a Percentage line and a Bandwitch indicator 7 | ''' 8 | lines = ('bandwitch',) 9 | plotlines = dict(bandwitch=dict(_name='%Bwtch')) # display the line as %B on chart 10 | 11 | def __init__(self): 12 | super(BollingerBandsBandwitch, self).__init__() 13 | self.l.bandwitch = 100 * (self.l.top - self.l.bot) / self.l.mid 14 | pass -------------------------------------------------------------------------------- /custom_indicators/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2021-2025 Skinok 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | 22 | -------------------------------------------------------------------------------- /custom_indicators/ema.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | sys.path.append('../finplot') 5 | import finplot as fplt 6 | 7 | from common import calc_rsi 8 | 9 | class Ema(): 10 | 11 | def __init__(self, dataFrames, ema_periods=9): 12 | self.ema_df = dataFrames["Close"].ewm(span=ema_periods, adjust=False).mean() 13 | pass 14 | 15 | def draw(self, ax, ema_color = "yellow", width=1): 16 | self.ema_plot = fplt.plot(self.ema_df, ax = ax, color=ema_color, width=width ) 17 | pass 18 | 19 | def clear(self): 20 | pass 21 | -------------------------------------------------------------------------------- /custom_indicators/fin_macd.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import finplot as fplt 4 | 5 | import backtrader as bt 6 | import backtrader.indicators as btind 7 | 8 | class Macd(): 9 | 10 | ''' 11 | dataFrames should be : ['Date','Open','Close'] 12 | ax is the finplot plot previously created 13 | ''' 14 | def __init__(self, dataFrames, ax): 15 | 16 | self.macd_ax = fplt.create_plot('MACD', rows=2) 17 | 18 | # plot macd with standard colors first 19 | self.macd = dataFrames.Close.ewm(span=12).mean() - dataFrames.Close.ewm(span=26).mean() 20 | self.signal = self.macd.ewm(span=9).mean() 21 | 22 | #self.macd = bt.indicators.MACD(dataFrames, period_me1 = ) 23 | 24 | # Add MACD Diff to the data frames 25 | dataFrames['macd_diff'] = self.macd - self.signal 26 | 27 | # draw MACD in the MACD window (self.macd_ax) 28 | fplt.volume_ocv(dataFrames[['TimeInt','Open','Close','macd_diff']], ax=ax, colorfunc=fplt.strength_colorfilter) 29 | fplt.plot(self.macd, ax=ax, legend='MACD') 30 | fplt.plot(self.signal, ax=ax, legend='Signal') 31 | pass 32 | -------------------------------------------------------------------------------- /custom_indicators/ichimoku.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | 5 | from pyqtgraph.functions import Color 6 | sys.path.append('../finplot') 7 | import finplot as fplt 8 | 9 | import backtrader as bt 10 | import backtrader.indicators as btind 11 | 12 | 13 | import pandas as pd 14 | 15 | class Ichimoku(): 16 | 17 | ''' 18 | THIS CLASS TAKE A PANDA DATAFRAME AS INPUT, AND NOT A BACKTRADER DATA FEED (Use bt.Ind.Ichimoku instead) 19 | ''' 20 | 21 | 22 | ''' 23 | Developed and published in his book in 1969 by journalist Goichi Hosoda 24 | 25 | Formula: 26 | - tenkan_sen = (Highest(High, tenkan) + Lowest(Low, tenkan)) / 2.0 27 | - kijun_sen = (Highest(High, kijun) + Lowest(Low, kijun)) / 2.0 28 | 29 | The next 2 are pushed 26 bars into the future 30 | 31 | - senkou_span_a = (tenkan_sen + kijun_sen) / 2.0 32 | - senkou_span_b = ((Highest(High, senkou) + Lowest(Low, senkou)) / 2.0 33 | 34 | This is pushed 26 bars into the past 35 | 36 | - chikou = close 37 | 38 | The cloud (Kumo) is formed by the area between the senkou_spans 39 | 40 | See: 41 | - http://stockcharts.com/school/doku.php?id=chart_school:technical_indicators:ichimoku_cloud 42 | 43 | ''' 44 | 45 | 46 | ''' 47 | THIS METHOD TAKE A PANDA DATAFRAME AS INPUT, AND NOT A BACKTRADER DATA FEED (Use bt.Ind.Ichimoku instead) 48 | ''' 49 | def __init__(self, dataFrames, tenkan = 9, kijun = 26, senkou = 52, senkou_lead = 26, chikou = 26): 50 | 51 | # Tenkan-sen (Conversion Line): (9-period high + 9-period low)/2)) 52 | period9_high = dataFrames['High'].rolling(window=tenkan).max() 53 | period9_low = dataFrames['Low'].rolling(window=tenkan).min() 54 | self.tenkan_sen = (period9_high + period9_low) / 2 55 | 56 | # Kijun-sen (Base Line): (26-period high + 26-period low)/2)) 57 | period26_high = dataFrames['High'].rolling(window=kijun).max() 58 | period26_low = dataFrames['Low'].rolling(window=kijun).min() 59 | self.kijun_sen = (period26_high + period26_low) / 2 60 | 61 | # Senkou Span A (Leading Span A): (Conversion Line + Base Line)/2)) 62 | self.senkou_span_a = ((self.tenkan_sen + self.kijun_sen) / 2).shift(senkou_lead) 63 | 64 | # Senkou Span B (Leading Span B): (52-period high + 52-period low)/2)) 65 | period52_high = dataFrames['High'].rolling(window=senkou).max() 66 | period52_low = dataFrames['Low'].rolling(window=senkou).min() 67 | self.senkou_span_b = ((period52_high + period52_low) / 2).shift(senkou_lead) 68 | 69 | # The most current closing price plotted 26 time periods behind (optional) 70 | self.chikou_span = dataFrames['Close'].shift(-chikou) # 26 according to investopedia 71 | 72 | pass 73 | 74 | def draw(self, ax, tenkan_color = "magenta", kijun_color = "blue", senkou_a_color = "gray", senkou_b_color = "gray", chikou_color = "yellow"): 75 | 76 | 77 | self.tenkan_sen_plot = fplt.plot(self.tenkan_sen, ax = ax, color=tenkan_color, width=1 ) 78 | self.kijun_sen_plot = fplt.plot(self.kijun_sen, ax = ax, color=kijun_color, width=2 ) 79 | self.chikou_span_plot = fplt.plot(self.chikou_span, ax = ax, color=chikou_color, width=2 ) 80 | 81 | self.senkou_span_a_plot = fplt.plot(self.senkou_span_a, ax = ax, color=senkou_a_color ) 82 | self.senkou_span_b_plot = fplt.plot(self.senkou_span_b, ax = ax, color=senkou_b_color ) 83 | 84 | fplt.fill_between( self.senkou_span_a_plot, self.senkou_span_b_plot, color = Color("darkGray") ) 85 | 86 | pass 87 | 88 | def clear(self): 89 | 90 | pass 91 | -------------------------------------------------------------------------------- /custom_indicators/rsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | sys.path.append('../finplot') 5 | import finplot as fplt 6 | 7 | from common import calc_rsi 8 | 9 | class Rsi(): 10 | 11 | def __init__(self, dataFrames, rsi_periods=14): 12 | self.rsi_df = calc_rsi(dataFrames, rsi_periods) 13 | pass 14 | 15 | def draw(self, ax, rsi_color = "magenta"): 16 | ax.reset() 17 | self.rsi_plot = fplt.plot(self.rsi_df, ax = ax, color=rsi_color, width=1 ) 18 | pass 19 | 20 | def clear(self): 21 | pass 22 | -------------------------------------------------------------------------------- /custom_indicators/sma.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | sys.path.append('../finplot') 5 | import finplot as fplt 6 | 7 | from common import calc_rsi 8 | 9 | class Sma(): 10 | 11 | def __init__(self, dataFrames, sma_periods=14): 12 | self.sma_df = dataFrames["Close"].rolling(window=sma_periods).mean() 13 | pass 14 | 15 | def draw(self, ax, sma_color = "green", width=1): 16 | self.sma_plot = fplt.plot(self.sma_df, ax = ax, color=sma_color, width=width ) 17 | pass 18 | 19 | def clear(self): 20 | pass 21 | -------------------------------------------------------------------------------- /custom_indicators/stochastic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | sys.path.append('../finplot') 5 | import finplot as fplt 6 | 7 | from common import calc_stochastic_oscillator 8 | 9 | class Stochastic(): 10 | 11 | def __init__(self, dataFrames, stochastic_periods=14, stochastic_k_smooth=1, stochastic_d_smooth = 3): 12 | self.stochastic_k_df, self.stochastic_d_df = calc_stochastic_oscillator(dataFrames, stochastic_periods, stochastic_k_smooth, stochastic_d_smooth) 13 | pass 14 | 15 | def draw(self, ax, stochasticColor = "magenta", stochastic_quick_color="yellow"): 16 | self.stochastic_k_plot = fplt.plot(self.stochastic_k_df, ax = ax, color=stochasticColor, width=1 ) 17 | self.stochastic_d_plot = fplt.plot(self.stochastic_d_df, ax = ax, color=stochastic_quick_color, width=1 ) 18 | pass 19 | -------------------------------------------------------------------------------- /custom_indicators/stochasticRsi.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | 4 | sys.path.append('../finplot') 5 | import finplot as fplt 6 | 7 | from common import StochRSI 8 | 9 | class StochasticRsi(): 10 | 11 | def __init__(self, dataFrames, period=14, smoothK=3, smoothD = 3): 12 | self.stochrsi, self.stochrsi_K, self.stochrsi_D = StochRSI(dataFrames, period, smoothK, smoothD) 13 | pass 14 | 15 | def draw(self, ax, stochasticRsi_k_color = "red", stochasticRsi_d_color="green"): 16 | self.stochrsi_K_plot = fplt.plot(self.stochrsi_K, ax = ax, color=stochasticRsi_k_color, width=1 ) 17 | self.stochrsi_D_plot = fplt.plot(self.stochrsi_D, ax = ax, color=stochasticRsi_d_color, width=1 ) 18 | pass 19 | 20 | -------------------------------------------------------------------------------- /dataManager.py: -------------------------------------------------------------------------------- 1 | 2 | import os,sys 3 | import Singleton 4 | import pandas as pd 5 | 6 | from Singleton import Singleton 7 | 8 | class DataManager(Singleton): 9 | 10 | def DatetimeFormat(self, dataFilePath): 11 | fileparts = os.path.split(dataFilePath) 12 | datafile = fileparts[1] 13 | # print(datafile[-4:]) 14 | if datafile[-4:]=='.csv': 15 | df = pd.read_csv(dataFilePath, nrows=1) 16 | 17 | timestring = df.iloc[0,0] 18 | ncolon = timestring.count(':') 19 | if ncolon==2: 20 | return "%Y-%m-%d %H:%M:%S" 21 | elif ncolon==1: 22 | return "%Y-%m-%d %H:%M" 23 | else: 24 | nspace = timestring.count(' ') 25 | if nspace==1: 26 | return "%Y-%m-%d %H" 27 | else: 28 | return "%Y-%m-%d" 29 | 30 | return "" 31 | 32 | 33 | # Return True if loading is successfull & the error string if False 34 | # dataPath is the full file path 35 | def loadDataFrame(self, loadDataFile): 36 | 37 | # Try importing data file 38 | # We should code a widget that ask for options as : separators, date format, and so on... 39 | try: 40 | 41 | # Python contains 42 | if pd.__version__<'2.0.0': 43 | df = pd.read_csv(loadDataFile.filePath, 44 | sep=loadDataFile.separator, 45 | parse_dates=[0], 46 | date_parser=lambda x: pd.to_datetime(x, format=loadDataFile.timeFormat), 47 | skiprows=0, 48 | header=0, 49 | names=["Time", "Open", "High", "Low", "Close", "Volume"], 50 | index_col=0) 51 | else: 52 | df = pd.read_csv(loadDataFile.filePath, 53 | sep=loadDataFile.separator, 54 | parse_dates=[0], 55 | date_format=loadDataFile.timeFormat, 56 | skiprows=0, 57 | header=0, 58 | names=["Time", "Open", "High", "Low", "Close", "Volume"], 59 | index_col=0) 60 | 61 | return df, "" 62 | 63 | except ValueError as err: 64 | return None, "ValueError error:" + str(err) 65 | except AttributeError as err: 66 | return None, "AttributeError error:" + str(err) 67 | except IndexError as err: 68 | return None, "IndexError error:" + str(err) 69 | except : 70 | return None, "Unexpected error:" + str(sys.exc_info()[0]) 71 | 72 | 73 | def findTimeFrame(self, df): 74 | 75 | if len(df.index) > 2: 76 | dtDiff = df.index[1] - df.index[0] 77 | 78 | if dtDiff.total_seconds() == 60: 79 | return "M1" 80 | elif dtDiff.total_seconds() == 300: 81 | return "M5" 82 | elif dtDiff.total_seconds() == 900: 83 | return "M15" 84 | elif dtDiff.total_seconds() == 1800: 85 | return "M30" 86 | elif dtDiff.total_seconds() == 3600: 87 | return "H1" 88 | elif dtDiff.total_seconds() == 14400: 89 | return "H4" 90 | elif dtDiff.total_seconds() == 86400: 91 | return "D" 92 | elif dtDiff.total_seconds() == 604800: 93 | return "W" 94 | 95 | pass -------------------------------------------------------------------------------- /finplotWindow.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | from pyqtgraph.graphicsItems.LegendItem import LegendItem 4 | 5 | from custom_indicators import ichimoku 6 | from custom_indicators import rsi 7 | from custom_indicators import stochastic 8 | from custom_indicators import stochasticRsi 9 | from custom_indicators import sma 10 | from custom_indicators import ema 11 | 12 | sys.path.append('../finplot') 13 | import finplot as fplt 14 | 15 | import pandas as pd 16 | import numpy as np 17 | from datetime import datetime as dt 18 | import time as _time 19 | import backtrader as bt 20 | from pyqtgraph import mkColor, mkBrush 21 | 22 | def chinese_price_colorfilter(item, datasrc, df): 23 | opencol = df.columns[1] 24 | closecol = df.columns[2] 25 | is_up = df[opencol] <= df[closecol] # open lower than close = goes up 26 | yield item.rowcolors('bear') + [df.loc[is_up, :]] 27 | yield item.rowcolors('bull') + [df.loc[~is_up, :]] 28 | 29 | class FinplotWindow(): 30 | 31 | def __init__(self, dockArea, dockChart, interface): 32 | 33 | self.dockArea = dockArea 34 | self.dockChart = dockChart 35 | self.interface = interface 36 | 37 | self.IndIchimokuActivated = False 38 | self.IndRsiActivated = False 39 | self.IndStochasticActivated = False 40 | self.IndStochasticRsiActivated = False 41 | 42 | self.IndVolumesActivated = False 43 | 44 | self.last_ax_data_xtick = [] 45 | 46 | pass 47 | 48 | ######### 49 | # Prepare the plot widgets 50 | ######### 51 | def createPlotWidgets(self, timeframe): 52 | 53 | # fin plot 54 | self.ax0, self.ax_rsi, self.ax_stochasticRsi, self.ax_stochastic, self.axPnL = fplt.create_plot_widget(master=self.dockArea, rows=5, init_zoom_periods=200) 55 | self.dockArea.axs = [self.ax0, self.ax_rsi, self.ax_stochasticRsi, self.ax_stochastic, self.axPnL] # , self.ax_rsi, self.ax2, self.axPnL 56 | self.dockChart.addWidget(self.ax0.ax_widget, 1, 0, 1, 1) 57 | 58 | ''' 59 | self.dockChart.addWidget(self.ax_rsi.ax_widget, 2, 0, 1, 1) 60 | self.dockChart.addWidget(self.ax2.ax_widget, 3, 0, 1, 1) 61 | ''' 62 | 63 | self.interface.dock_rsi[timeframe].layout.addWidget(self.ax_rsi.ax_widget) 64 | self.interface.dock_stochasticRsi[timeframe].layout.addWidget(self.ax_stochasticRsi.ax_widget) 65 | self.interface.dock_stochastic[timeframe].layout.addWidget(self.ax_stochastic.ax_widget) 66 | 67 | # Ax Profit & Loss 68 | self.interface.strategyResultsUI.ResultsTabWidget.widget(1).layout().addWidget(self.axPnL.ax_widget) 69 | 70 | fplt.add_crosshair_info(self.update_crosshair_text, ax=self.ax0) 71 | pass 72 | 73 | def drawCandles(self): 74 | 75 | fplt.candlestick_ochl(self.data['Open Close High Low'.split()], ax=self.ax0 ) # colorfunc=chinese_price_colorfilter 76 | 77 | #self.hover_label = fplt.add_legend('', ax=self.ax0) 78 | #fplt.set_time_inspector(self.update_legend_text, ax=self.ax0, when='hover', data=data) 79 | 80 | # Inside plot widget controls 81 | #self.createControlPanel(self.ax0.ax_widget) 82 | pass 83 | 84 | def drawSma(self, period, color, width): 85 | self.sma_indicator = sma.Sma(self.data, period) 86 | self.sma_indicator.draw(self.ax0, color, width) 87 | pass 88 | 89 | def drawEma(self, period, color, width): 90 | self.ema_indicator = ema.Ema(self.data, period) 91 | self.ema_indicator.draw(self.ax0, color, width) 92 | pass 93 | 94 | def drawRsi(self, period, color): 95 | self.rsi_indicator = rsi.Rsi(self.data, period) 96 | self.rsi_indicator.draw(self.ax_rsi, color) 97 | pass 98 | 99 | def drawStochastic(self, period, smooth_k, smooth_d): 100 | self.stochastic_indicator = stochastic.Stochastic(self.data, period, smooth_k, smooth_d) 101 | self.stochastic_indicator.draw(self.ax_stochastic) 102 | pass 103 | 104 | def drawStochasticRsi(self, period, smooth_k, smooth_d): 105 | self.stochasticRsi_indicator = stochasticRsi.StochasticRsi(self.data, period, smooth_k, smooth_d) 106 | self.stochasticRsi_indicator.draw(self.ax_stochasticRsi) 107 | pass 108 | 109 | ######### 110 | # Draw orders on charts (with arrows) 111 | ######### 112 | def drawOrders(self, orders = None): 113 | 114 | # Orders need to be stuied to know if an order is an open or a close order, or both... 115 | # It depends on the order volume and the currently opened positions volume 116 | currentPositionSize = 0 117 | open_orders = [] 118 | 119 | if orders != None: 120 | self.orders = orders 121 | 122 | if hasattr(self,"orders"): 123 | 124 | for order in self.orders: 125 | 126 | ############## 127 | # Buy 128 | ############## 129 | if order.isbuy(): 130 | 131 | # Tracer les traites allant des ouvertures de positions vers la fermeture de position 132 | if currentPositionSize < 0: 133 | 134 | # Réduction, cloture, ou invertion de la position 135 | if order.size == abs(currentPositionSize): # it's a buy so order.size > 0 136 | 137 | # Cloture de la position 138 | last_order = open_orders.pop() 139 | posOpen = (bt.num2date(last_order.executed.dt),last_order.executed.price) 140 | posClose = (bt.num2date(order.executed.dt), order.executed.price) 141 | 142 | color = "#555555" 143 | if order.executed.pnl > 0: 144 | color = "#30FF30" 145 | elif order.executed.pnl < 0: 146 | color = "#FF3030" 147 | 148 | fplt.add_line(posOpen, posClose, color, 2, style="--", ax = self.ax0 ) 149 | 150 | elif order.size > abs(currentPositionSize): 151 | # Fermeture de la position précédente + ouverture d'une position inverse 152 | pass 153 | 154 | elif order.size < abs(currentPositionSize): 155 | # Réduction de la position courante 156 | pass 157 | 158 | elif currentPositionSize > 0: 159 | # Augmentation de la postion 160 | # on enregistre la position pour pouvoir tracer un trait de ce point vers l'ordre de cloture du trade. 161 | open_orders.append(order) 162 | 163 | else: 164 | # Ouverture d'une nouvelle position 165 | open_orders.append(order) 166 | pass 167 | 168 | ############## 169 | # Sell 170 | ############## 171 | elif order.issell(): 172 | 173 | if currentPositionSize < 0: 174 | # Augmentation de la postion 175 | 176 | # on enregistre la position pour pouvoir tracer un trait de ce point vers l'ordre de cloture du trade. 177 | open_orders.append(order) 178 | 179 | elif currentPositionSize > 0: 180 | # Réduction, cloture, ou invertion de la position 181 | 182 | if abs(order.size) == abs(currentPositionSize): # it's a buy so order.size > 0 183 | # Cloture de la position 184 | last_order = open_orders.pop() 185 | posOpen = (bt.num2date(last_order.executed.dt),last_order.executed.price) 186 | posClose = (bt.num2date(order.executed.dt), order.executed.price) 187 | 188 | color = "#555555" 189 | if order.executed.pnl > 0: 190 | color = "#30FF30" 191 | elif order.executed.pnl < 0: 192 | color = "#FF3030" 193 | 194 | fplt.add_line(posOpen, posClose, color, 2, ax=self.ax0, style="--" ) 195 | 196 | pass 197 | 198 | elif order.size > abs(currentPositionSize): 199 | # Réduction de la position courante 200 | pass 201 | 202 | elif order.size < abs(currentPositionSize): 203 | # Fermeture de la position précédente + ouverture d'une position inverse 204 | pass 205 | 206 | else: 207 | # Ouverture d'une nouvelle position 208 | open_orders.append(order) 209 | pass 210 | 211 | else: 212 | print("Unknown order") 213 | 214 | # Cumul des positions 215 | currentPositionSize += order.size 216 | 217 | # Todo: We could display the size of the order with a label on the chart 218 | fplt.add_order(bt.num2date(order.executed.dt), order.executed.price, order.isbuy(), ax=self.ax0) 219 | 220 | pass 221 | 222 | ######### 223 | # Finplot configuration functions : maybe it should be in a different file 224 | ######### 225 | def update_legend_text(self, x, y, ax, data): 226 | row = data.loc[data.TimeInt==x] 227 | 228 | # format html with the candle and set legend 229 | fmt = '%%.5f' % ('0f0' if (row.Open 10: 428 | date1 = date1 - 10 429 | 430 | date2 = x2[0] + 10 431 | 432 | ax.vb.update_y_zoom(date1,date2) 433 | 434 | pass 435 | 436 | ############# 437 | # Show finplot Window 438 | ############# 439 | def show(self): 440 | 441 | #qt_exec create a whole qt context : we dont need it here 442 | fplt.show(qt_exec=False) 443 | 444 | pass 445 | 446 | def drawPnL(self, pln_data): 447 | 448 | self.axPnL.reset() 449 | 450 | fplt.plot(pln_data['time'], pln_data['equity'], ax = self.axPnL, legend="equity") 451 | fplt.plot(pln_data['time'], pln_data['value'], ax = self.axPnL, legend="value") 452 | 453 | self.axPnL.ax_widget.show() 454 | self.axPnL.show() 455 | 456 | pass 457 | 458 | def showPnL(self): 459 | self.axPnL.show() 460 | self.axPnL.ax_widget.show() 461 | pass 462 | 463 | def hidePnL(self): 464 | self.axPnL.hide() 465 | self.axPnL.ax_widget.hide() 466 | pass 467 | -------------------------------------------------------------------------------- /images/gitkeep.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skinok/backtrader-pyqt-ui/28c4e065a79dbcc5d3b260b29fab53886a253ef3/images/gitkeep.txt -------------------------------------------------------------------------------- /images/overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skinok/backtrader-pyqt-ui/28c4e065a79dbcc5d3b260b29fab53886a253ef3/images/overview.png -------------------------------------------------------------------------------- /images/overview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skinok/backtrader-pyqt-ui/28c4e065a79dbcc5d3b260b29fab53886a253ef3/images/overview2.png -------------------------------------------------------------------------------- /images/overview3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skinok/backtrader-pyqt-ui/28c4e065a79dbcc5d3b260b29fab53886a253ef3/images/overview3.png -------------------------------------------------------------------------------- /indicatorParametersUI.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtCore, QtWidgets, QtGui, uic 2 | 3 | import os 4 | 5 | class IndicatorParametersUI(QtWidgets.QDialog): 6 | 7 | def __init__(self, parent = None): 8 | super(IndicatorParametersUI, self).__init__() 9 | 10 | #self.setParent(parent) 11 | 12 | # It does not finish by a "/" 13 | self.current_dir_path = os.path.dirname(os.path.realpath(__file__)) 14 | 15 | uic.loadUi( self.current_dir_path + "/ui/indicatorParameters.ui", self) 16 | 17 | self.title = self.findChild(QtWidgets.QLabel, "title") 18 | self.parameterLayout = self.findChild(QtWidgets.QFormLayout, "parameterLayout") 19 | 20 | pass 21 | 22 | def setTitle(self, title): 23 | self.title = title 24 | pass 25 | 26 | def addParameter(self, parameterName, defaultValue): 27 | lineEdit = QtWidgets.QLineEdit(parameterName, self) 28 | lineEdit.setObjectName(parameterName) 29 | lineEdit.setText(str(defaultValue)) 30 | self.parameterLayout.addRow(parameterName, lineEdit) 31 | pass 32 | 33 | def addParameterColor(self, parameterName, defaultValue): 34 | # Custom color picker 35 | colorButton = SelectColorButton(parameterName, self) 36 | self.parameterLayout.addRow(parameterName, colorButton) 37 | pass 38 | 39 | def getValue(self, parameterName): 40 | lineEdit = self.findChild(QtWidgets.QLineEdit, parameterName) 41 | if lineEdit is not None: 42 | try: 43 | return int(lineEdit.text()) 44 | except: 45 | try: 46 | return float(lineEdit.text()) 47 | except: 48 | try: 49 | return lineEdit.text() 50 | except: 51 | return None 52 | else: 53 | return None 54 | 55 | def getColorValue(self, parameterName): 56 | colorButton = self.findChild(SelectColorButton, parameterName) 57 | if colorButton is not None: 58 | try: 59 | return colorButton.getColor().name() 60 | except: 61 | return None 62 | else: 63 | return None 64 | 65 | 66 | class SelectColorButton(QtWidgets.QPushButton): 67 | 68 | def __init__(self, objectName, parent = None): 69 | 70 | super(SelectColorButton, self).__init__() 71 | 72 | self.setColor( QtGui.QColor("yellow") ) 73 | 74 | self.setObjectName(objectName) 75 | self.setParent(parent) 76 | self.clicked.connect(self.changeColor) 77 | 78 | def setColor(self,color): 79 | self.color = color 80 | self.updateColor() 81 | 82 | def getColor(self): 83 | return self.color 84 | 85 | def updateColor(self): 86 | self.setStyleSheet( "background-color: " + self.color.name() ) 87 | 88 | def changeColor(self): 89 | newColor = QtWidgets.QColorDialog.getColor(self.color, self.parentWidget()) 90 | if newColor != self.color: 91 | self.setColor( newColor ) -------------------------------------------------------------------------------- /loadDataFilesUI.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtCore, QtWidgets, uic 2 | import pandas as pd 3 | import os 4 | from DataFile import DataFile 5 | 6 | from userConfig import UserConfig 7 | from dataManager import DataManager 8 | 9 | class LoadDataFilesUI(QtWidgets.QWidget): 10 | 11 | def __init__(self, controller, parent = None): 12 | 13 | super(LoadDataFilesUI, self).__init__() 14 | 15 | self.controller = controller 16 | 17 | self.parent = parent 18 | #self.setParent(parent) 19 | 20 | # It does not finish by a "/" 21 | self.current_dir_path = os.path.dirname(os.path.realpath(__file__)) 22 | 23 | uic.loadUi( self.current_dir_path + "/ui/loadDataFiles.ui", self) 24 | 25 | self.filePathLE = self.findChild(QtWidgets.QLineEdit, "filePathLE") 26 | self.datetimeFormatLE = self.findChild(QtWidgets.QLineEdit, "datetimeFormatLE") 27 | 28 | self.tabRB = self.findChild(QtWidgets.QRadioButton, "tabRB") 29 | self.commaRB = self.findChild(QtWidgets.QRadioButton, "commaRB") 30 | self.semicolonRB = self.findChild(QtWidgets.QRadioButton, "semicolonRB") 31 | 32 | self.openFilePB = self.findChild(QtWidgets.QToolButton, "openFilePB") 33 | self.loadFilePB = self.findChild(QtWidgets.QPushButton, "loadFilePB") 34 | self.deletePB = self.findChild(QtWidgets.QLineEdit, "deletePB") 35 | self.importPB = self.findChild(QtWidgets.QPushButton, "importPB") 36 | 37 | self.deleteDataFilePB = self.findChild(QtWidgets.QPushButton, "deleteDataFilePB") 38 | 39 | self.errorLabel = self.findChild(QtWidgets.QLabel, "errorLabel") 40 | 41 | self.dataFilesListWidget = self.findChild(QtWidgets.QListWidget, "dataFilesListWidget") 42 | 43 | # Connect slots : open file 44 | self.openFilePB.clicked.connect( self.openFileDialog ) 45 | self.loadFilePB.clicked.connect( self.createDataFile ) 46 | self.deleteDataFilePB.clicked.connect(self.deleteFile) 47 | self.importPB.clicked.connect( self.importFiles ) 48 | 49 | self.dataManager = DataManager() 50 | self.userConfig = UserConfig() 51 | 52 | pass 53 | 54 | def openFileDialog(self): 55 | self.dataFilePath = QtWidgets.QFileDialog.getOpenFileName(self, 'Open data file', self.current_dir_path + "/data","CSV files (*.csv)")[0] 56 | self.filePathLE.setText(self.dataFilePath) 57 | 58 | self.datetimeFormatLE.setText(self.dataManager.DatetimeFormat(self.dataFilePath)) 59 | 60 | pass 61 | 62 | def createDataFile(self): 63 | 64 | dataFile = DataFile() 65 | 66 | # try loading file by controller 67 | dataFile.separator = '\t' if self.tabRB.isChecked() else ',' if self.commaRB.isChecked() else ';' 68 | dataFile.timeFormat = self.datetimeFormatLE.text() 69 | dataFile.filePath = self.dataFilePath 70 | dataFile.fileName = os.path.basename(self.dataFilePath) 71 | 72 | if not dataFile.timeFormat in self.controller.dataFiles: 73 | 74 | dataFile.dataFrame, errorMessage = self.dataManager.loadDataFrame(dataFile) 75 | 76 | # Store data file in the user config parameters to later use 77 | dataFile.timeFrame = self.dataManager.findTimeFrame(dataFile.dataFrame) 78 | 79 | if dataFile.dataFrame is not None: 80 | 81 | self.errorLabel.setStyleSheet("color:green") 82 | self.errorLabel.setText("The file has been loaded correctly.") 83 | 84 | # Add file name 85 | items = self.dataFilesListWidget.findItems(dataFile.fileName, QtCore.Qt.MatchFixedString) 86 | 87 | if len(items) == 0: 88 | self.dataFilesListWidget.addItem(os.path.basename(dataFile.filePath)) 89 | 90 | self.controller.dataFiles[dataFile.timeFrame] = dataFile; 91 | self.userConfig.saveObject(dataFile.timeFrame, dataFile) 92 | 93 | else: 94 | self.errorLabel.setStyleSheet("color:red") 95 | self.errorLabel.setText(errorMessage) 96 | else: 97 | self.errorLabel.setStyleSheet("color:red") 98 | self.errorLabel.setText("The file is already in the list") 99 | pass 100 | 101 | 102 | def loadDataFileFromConfig(self, dataPath, datetimeFormat, separator): 103 | 104 | fileName = os.path.basename(dataPath) 105 | df, errorMessage = self.dataManager.loadDataFrame(dataPath, datetimeFormat, separator) 106 | 107 | if df is not None: 108 | 109 | # Add file name 110 | items = self.dataFilesListWidget.findItems(fileName, QtCore.Qt.MatchFixedString) 111 | 112 | if len(items) == 0: 113 | self.dataFilesListWidget.addItem(os.path.basename(dataPath)) 114 | 115 | return df 116 | 117 | 118 | def deleteFile(self): 119 | 120 | listItems=self.dataFilesListWidget.selectedItems() 121 | if not listItems: return 122 | for item in listItems: 123 | itemTaken = self.dataFilesListWidget.takeItem(self.dataFilesListWidget.row(item)) 124 | 125 | # Delete from dataFrames 126 | timeFrame = self.controller.datafileName_to_dataFile[itemTaken.text()].timeFrame 127 | 128 | # Delete from controler 129 | self.controller.removeTimeframe(timeFrame) 130 | 131 | # Delete from config 132 | self.userConfig.removeParameter(timeFrame) 133 | 134 | pass 135 | 136 | def importFiles(self): 137 | 138 | # Give all ordered data path to the controller 139 | if self.controller.importData(): 140 | self.hide() 141 | 142 | pass 143 | 144 | ''' 145 | def showEvent(self, ev): 146 | # Move at the center of the window 147 | #if self.parent is not None: 148 | 149 | # x = int(self.parent.sizeHint().width() / 2 - self.sizeHint().width()) 150 | # y = int(self.parent.sizeHint().height() / 2 - self.sizeHint().height()) 151 | 152 | # self.move( x, y ) 153 | 154 | return QtWidgets.showEvent(self, ev) 155 | ''' -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | 20 | from SkinokBacktraderUI import SkinokBacktraderUI 21 | skinokTrader = SkinokBacktraderUI() 22 | skinokTrader.displayUI() -------------------------------------------------------------------------------- /metaStrategy.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | import backtrader as bt 20 | 21 | class MetaStrategy(bt.Strategy): 22 | 23 | def __init__(self, parameters = None): 24 | 25 | # Set UI modified parameters 26 | if parameters != None: 27 | for parameterName, parameterValue in parameters.items(): 28 | setattr(self.params, parameterName, parameterValue) 29 | 30 | pass -------------------------------------------------------------------------------- /observers/SkinokObserver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2021-2025 Skinok 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | from __future__ import (absolute_import, division, print_function, 22 | unicode_literals) 23 | 24 | from backtrader import Observer 25 | 26 | import SkinokBacktraderUI 27 | 28 | class SkinokObserver(Observer): 29 | 30 | lines = ('wallet_value', 'wallet_equity', 'wallet_cash') 31 | 32 | def __init__(self): 33 | 34 | # Ui following 35 | self.progressBar = SkinokBacktraderUI.interface.getProgressBar() 36 | self.progressBar.setMaximum(self.datas[0].close.buflen()) 37 | self.progressBar.setValue(0) 38 | 39 | SkinokBacktraderUI.wallet.value_list = [] 40 | SkinokBacktraderUI.wallet.equity_list = [] 41 | SkinokBacktraderUI.wallet.cash_list = [] 42 | 43 | def next(self): 44 | 45 | # Watch trades 46 | pnl = 0 47 | for trade in self._owner._tradespending: 48 | 49 | if trade.data not in self.ddatas: 50 | continue 51 | 52 | if not trade.isclosed: 53 | continue 54 | 55 | pnl += trade.pnl # trade.pnlcomm if self.p.pnlcomm else trade.pnl 56 | 57 | # Portfolio update 58 | SkinokBacktraderUI.wallet.current_value = self.wallet_value = SkinokBacktraderUI.wallet.current_value + pnl 59 | SkinokBacktraderUI.wallet.value_list.append( SkinokBacktraderUI.wallet.current_value ) 60 | 61 | SkinokBacktraderUI.wallet.current_equity = self.wallet_equity = self._owner.broker.getvalue() 62 | SkinokBacktraderUI.wallet.equity_list.append(self._owner.broker.getvalue()) 63 | 64 | SkinokBacktraderUI.wallet.current_cash = self.wallet_cash = self._owner.broker.getcash() 65 | SkinokBacktraderUI.wallet.cash_list.append(self._owner.broker.getcash()) 66 | 67 | # Progress bar update 68 | self.progressBar.setValue( self.progressBar.value() + 1 ) 69 | SkinokBacktraderUI.interface.app.processEvents() 70 | 71 | -------------------------------------------------------------------------------- /qt.conf: -------------------------------------------------------------------------------- 1 | [Platforms] 2 | WindowsArguments = dpiawareness=2 -------------------------------------------------------------------------------- /settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.pythonPath": "C:/Python39" 3 | } -------------------------------------------------------------------------------- /solution.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": {} 8 | } -------------------------------------------------------------------------------- /strategies/AiStableBaselinesModel.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | import backtrader as bt 20 | 21 | import metaStrategy as mt 22 | 23 | from stable_baselines3 import PPO, DQN 24 | 25 | import numpy as np 26 | from enum import Enum 27 | 28 | import pandas as pd 29 | import pandas_ta as ta 30 | 31 | from custom_indicators import BollingerBandsBandwitch 32 | 33 | # action list 34 | class Action(Enum): 35 | HOLD=0 36 | BUY=1 37 | SELL=2 38 | 39 | # Create a subclass of Strategy to define the indicators and logic 40 | class AiStableBaselinesModel(mt.MetaStrategy): 41 | 42 | params = ( 43 | ('model', ""), # Model name 44 | ('tradeSize', 10.0), 45 | ('use_ATR_SL', True), 46 | ('atrperiod', 14), # ATR Period (standard) 47 | ('atrdist_SL', 3.0), # ATR distance for stop price 48 | ('atrdist_TP', 5.0), # ATR distance for take profit price 49 | ('use_Fixed_SL', False), 50 | ('fixed_SL', 50.0), # Fixed distance Stop Loss 51 | ('fixed_TP', 100.0), # Fixed distance Take Profit 52 | ) 53 | 54 | def notify_order(self, order): 55 | if order.status == order.Completed: 56 | #print("Order completed") 57 | pass 58 | 59 | if not order.alive(): 60 | self.order = None # indicate no order is pending 61 | 62 | def __init__(self, *argv): 63 | 64 | # used to modify parameters 65 | super().__init__(argv[0]) 66 | 67 | # Ichi indicator 68 | self.ichimoku = bt.ind.Ichimoku(self.data) 69 | 70 | # To set the stop price 71 | self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod) 72 | 73 | self.stochastic = bt.ind.stochastic.Stochastic(self.data) 74 | self.rsi = bt.ind.RelativeStrengthIndex(self.data) 75 | self.bbands = bt.ind.BollingerBands(self.data) 76 | self.bbandsPct = bt.ind.BollingerBandsPct(self.data) 77 | self.bbandsBandwitch = BollingerBandsBandwitch.BollingerBandsBandwitch(self.data) 78 | 79 | self.sma_200 = bt.ind.MovingAverageSimple(self.data,period=200) 80 | self.sma_150 = bt.ind.MovingAverageSimple(self.data,period=150) 81 | self.sma_100 = bt.ind.MovingAverageSimple(self.data,period=100) 82 | self.sma_50 = bt.ind.MovingAverageSimple(self.data,period=50) 83 | self.sma_21 = bt.ind.MovingAverageSimple(self.data,period=21) 84 | 85 | self.ema_200 = bt.ind.ExponentialMovingAverage(self.data,period=200) 86 | self.ema_100 = bt.ind.ExponentialMovingAverage(self.data,period=100) 87 | self.ema_50 = bt.ind.ExponentialMovingAverage(self.data,period=50) 88 | self.ema_26 = bt.ind.ExponentialMovingAverage(self.data,period=26) 89 | self.ema_12 = bt.ind.ExponentialMovingAverage(self.data,period=12) 90 | self.ema_9 = bt.ind.ExponentialMovingAverage(self.data,period=9) 91 | 92 | self.macd = bt.ind.MACDHisto(self.data) 93 | 94 | self.normalization_bounds_df_min = self.loadNormalizationBoundsCsv( "C:/perso/AI/AI_Framework/prepared_data/normalization_bounds_min.csv" ).transpose() 95 | self.normalization_bounds_df_max = self.loadNormalizationBoundsCsv( "C:/perso/AI/AI_Framework/prepared_data/normalization_bounds_max.csv" ).transpose() 96 | 97 | pass 98 | 99 | def start(self): 100 | self.order = None # sentinel to avoid operrations on pending order 101 | 102 | # Load the model 103 | self.model = PPO.load(self.p.model) 104 | #self.model = DQN.load(self.p.model) 105 | pass 106 | 107 | def next(self): 108 | 109 | self.obseravation = self.next_observation() 110 | 111 | # Normalize observation 112 | #self.normalizeObservations() 113 | 114 | # Do nothing if a parameter is not valid yet (basically wait for all idicators to be loaded) 115 | 116 | if pd.isna(self.obseravation).any(): 117 | print("Waiting indicators") 118 | return 119 | 120 | # Prepare data for Model 121 | action, _states = self.model.predict(self.obseravation) # deterministic=True 122 | 123 | if not self.position: # not in the market 124 | 125 | # TP & SL calculation 126 | loss_dist = self.atr[0] * self.p.atrdist_SL if self.p.use_ATR_SL else self.p.fixed_SL 127 | profit_dist = self.atr[0] * self.p.atrdist_TP if self.p.use_ATR_SL else self.p.fixed_TP 128 | 129 | if action == Action.SELL.value: 130 | self.order = self.sell(size=self.p.tradeSize) 131 | self.lstop = self.data.close[0] + loss_dist 132 | self.take_profit = self.data.close[0] - profit_dist 133 | 134 | elif action == Action.BUY.value: 135 | self.order = self.buy(size=self.p.tradeSize) 136 | self.lstop = self.data.close[0] - loss_dist 137 | self.take_profit = self.data.close[0] + profit_dist 138 | 139 | else: # in the market 140 | pclose = self.data.close[0] 141 | 142 | if (not ((self.lstoppclose>self.take_profit))): 143 | self.close() # Close position 144 | 145 | pass 146 | 147 | # Here you have to transform self object price and indicators into a np.array input for AI Model 148 | # How you do it depend on your AI Model inputs 149 | # Strategy is in the data preparation for AI :D 150 | # https://stackoverflow.com/questions/53979199/tensorflow-keras-returning-multiple-predictions-while-expecting-one 151 | 152 | def next_observation(self): 153 | 154 | # OHLCV 155 | inputs = [ self.data.open[0],self.data.high[0],self.data.low[0],self.data.close[0],self.data.volume[0] ] 156 | 157 | # Ichimoku 158 | inputs = inputs + [ self.ichimoku.senkou_span_a[0], self.ichimoku.senkou_span_b[0], self.ichimoku.tenkan_sen[0], self.ichimoku.kijun_sen[0], self.ichimoku.chikou_span[0] ] 159 | 160 | # Rsi 161 | inputs = inputs + [self.rsi.rsi[0]] 162 | 163 | # Stochastic 164 | inputs = inputs + [self.stochastic.percK[0], self.stochastic.percD[0]] 165 | 166 | # bbands 167 | inputs = inputs + [self.bbands.bot[0]] # BBL 168 | inputs = inputs + [self.bbands.mid[0]] # BBM 169 | inputs = inputs + [self.bbands.top[0]] # BBU 170 | inputs = inputs + [self.bbandsBandwitch.bandwitch[0]] # BBB 171 | inputs = inputs + [self.bbandsPct.pctb[0]] # BBP 172 | 173 | # sma 174 | inputs = inputs + [self.sma_200.sma[0]] 175 | inputs = inputs + [self.sma_150.sma[0]] 176 | inputs = inputs + [self.sma_100.sma[0]] 177 | inputs = inputs + [self.sma_50.sma[0]] 178 | inputs = inputs + [self.sma_21.sma[0]] 179 | 180 | # ema 181 | inputs = inputs + [self.ema_200.ema[0]] 182 | inputs = inputs + [self.ema_100.ema[0]] 183 | inputs = inputs + [self.ema_50.ema[0]] 184 | inputs = inputs + [self.ema_26.ema[0]] 185 | inputs = inputs + [self.ema_12.ema[0]] 186 | inputs = inputs + [self.ema_9.ema[0]] 187 | 188 | # macd 189 | inputs = inputs + [self.macd.macd[0]] # MACD 190 | inputs = inputs + [self.macd.histo[0]] # MACD histo 191 | inputs = inputs + [self.macd.signal[0]] # MACD signal 192 | 193 | return np.array(inputs) 194 | 195 | pass 196 | 197 | 198 | def normalizeObservations(self): 199 | 200 | MIN_BITCOIN_VALUE = 10_000.0 201 | MAX_BITCOIN_VALUE = 80_000.0 202 | 203 | self.obseravation_normalized = np.empty(len(self.obseravation)) 204 | try: 205 | # Normalize data 206 | for index, value in enumerate(self.obseravation): 207 | if value < 100.0: 208 | self.obseravation_normalized[index] = (value - 0.0) / (100.0 - 0.0) 209 | else: 210 | self.obseravation_normalized[index] = (value - MIN_BITCOIN_VALUE) / (MAX_BITCOIN_VALUE - MIN_BITCOIN_VALUE) 211 | 212 | #self.obseravation_normalized = (self.obseravation-self.normalization_bounds_df_min) / (self.normalization_bounds_df_max-self.normalization_bounds_df_min) 213 | 214 | 215 | except ValueError as err: 216 | return None, "ValueError error:" + str(err) 217 | except AttributeError as err: 218 | return None, "AttributeError error:" + str(err) 219 | except IndexError as err: 220 | return None, "IndexError error:" + str(err) 221 | except: 222 | aie = 1 223 | 224 | pass 225 | 226 | def loadNormalizationBoundsCsv(self, filePath): 227 | 228 | # Try importing data file 229 | # We should code a widget that ask for options as : separators, date format, and so on... 230 | try: 231 | 232 | # Python contains 233 | if pd.__version__<'2.0.0': 234 | df = pd.read_csv(filePath, 235 | sep=";", 236 | parse_dates=None, 237 | date_parser=lambda x: pd.to_datetime(x, format=""), 238 | skiprows=0, 239 | header=None, 240 | index_col=0) 241 | else: 242 | df = pd.read_csv(filePath, 243 | sep=";", 244 | parse_dates=None, 245 | date_format="", 246 | skiprows=0, 247 | header=None, 248 | index_col=0) 249 | 250 | return df 251 | 252 | except ValueError as err: 253 | return None, "ValueError error:" + str(err) 254 | except AttributeError as err: 255 | return None, "AttributeError error:" + str(err) 256 | except IndexError as err: 257 | return None, "IndexError error:" + str(err) 258 | 259 | pass 260 | 261 | 262 | # https://stackoverflow.com/questions/53321608/extract-dataframe-from-pandas-datafeed-in-backtrader 263 | def __bt_to_pandas__(self, btdata, len): 264 | get = lambda mydata: mydata.get(ago=0, size=len) 265 | 266 | fields = { 267 | 'open': get(btdata.open), 268 | 'high': get(btdata.high), 269 | 'low': get(btdata.low), 270 | 'close': get(btdata.close), 271 | 'volume': get(btdata.volume) 272 | } 273 | time = [btdata.num2date(x) for x in get(btdata.datetime)] 274 | 275 | return pd.DataFrame(data=fields, index=time) 276 | 277 | pass 278 | 279 | -------------------------------------------------------------------------------- /strategies/AiTensorFlowModel.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | import backtrader as bt 20 | 21 | import metaStrategy as mt 22 | 23 | import tensorflow as tf 24 | import numpy as np 25 | from enum import Enum 26 | 27 | # action list 28 | class Action(Enum): 29 | HOLD=0 30 | BUY=1 31 | SELL=2 32 | 33 | # Create a subclass of Strategy to define the indicators and logic 34 | class AiTensorFlowModel(mt.MetaStrategy): 35 | 36 | params = ( 37 | ('model', ""), # Model name 38 | ('tradeSize', 2000), 39 | ('atrperiod', 14), # ATR Period (standard) 40 | ('atrdist_SL', 3), # ATR distance for stop price 41 | ('atrdist_TP', 5), # ATR distance for take profit price 42 | ('tenkan', 9), 43 | ('kijun', 26), 44 | ('senkou', 52), 45 | ('senkou_lead', 26), # forward push 46 | ('chikou', 26), # backwards push 47 | ) 48 | 49 | def notify_order(self, order): 50 | if order.status == order.Completed: 51 | #print("Order completed") 52 | pass 53 | 54 | if not order.alive(): 55 | self.order = None # indicate no order is pending 56 | 57 | def __init__(self, *argv): 58 | 59 | # used to modify parameters 60 | super().__init__(argv[0]) 61 | 62 | # Ichi indicator 63 | self.ich = bt.ind.Ichimoku() 64 | ''' 65 | self.data, 66 | tenkan=self.params.tenkan, 67 | kijun=self.params.kijun, 68 | senkou=self.params.senkou, 69 | senkou_lead=self.params.senkou_lead, 70 | chikou=self.params.chikou) 71 | ''' 72 | 73 | # To set the stop price 74 | self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod) 75 | 76 | self.stochastic = bt.ind.stochastic.Stochastic(self.data) 77 | 78 | pass 79 | 80 | def start(self): 81 | self.order = None # sentinel to avoid operrations on pending order 82 | 83 | # Load the model 84 | self.model = tf.keras.models.load_model(self.p.model) 85 | pass 86 | 87 | def next(self): 88 | 89 | self.ai_ready = self.prepareData() 90 | 91 | if self.order or not self.ai_ready: 92 | return # pending order execution 93 | 94 | # Prepare data for Model 95 | predicted_actions = self.model.predict_step([self.ai_inputs]) 96 | 97 | # Take action with the most credibility 98 | action = np.argmax( predicted_actions ) 99 | 100 | if not self.position: # not in the market 101 | 102 | if action == Action.SELL.value: 103 | self.order = self.sell(size=self.p.tradeSize) 104 | ldist = self.atr[0] * self.p.atrdist_SL 105 | self.lstop = self.data.close[0] + ldist 106 | pdist = self.atr[0] * self.p.atrdist_TP 107 | self.take_profit = self.data.close[0] - pdist 108 | 109 | elif action == Action.BUY.value: 110 | self.order = self.buy(size=self.p.tradeSize) 111 | ldist = self.atr[0] * self.p.atrdist_SL 112 | self.lstop = self.data.close[0] - ldist 113 | pdist = self.atr[0] * self.p.atrdist_TP 114 | self.take_profit = self.data.close[0] + pdist 115 | 116 | else: # in the market 117 | pclose = self.data.close[0] 118 | pstop = self.lstop # seems to be the bug 119 | 120 | if (not ((pstoppclose>self.take_profit))): 121 | self.close() # Close position 122 | 123 | pass 124 | 125 | # Here you have to transform self object price and indicators into a np.array input for AI Model 126 | # How you do it depend on your AI Model inputs 127 | # Strategy is in the data preparation for AI :D 128 | def prepareData(self): 129 | 130 | # https://stackoverflow.com/questions/53979199/tensorflow-keras-returning-multiple-predictions-while-expecting-one 131 | 132 | inputs_array = np.array([[ self.stochastic.percK[0] / 100.0, self.stochastic.percD[0] / 100.0 ]]) 133 | 134 | self.ai_inputs = inputs_array 135 | 136 | return True 137 | """ 138 | if len(self.ich.l.tenkan_sen) > 0 and len(self.ich.kijun_sen) > 0 and len(self.ich.senkou_span_a) > 0 and len(self.ich.senkou_span_b) > 0 and len(self.ich.chikou_span) > 0: 139 | 140 | tenkan_sen = self.ich.tenkan_sen[0] 141 | kijun_sen = self.ich.kijun_sen[0] 142 | senkou_span_a = self.ich.senkou_span_a[0] 143 | senkou_span_b = self.ich.senkou_span_b[0] 144 | chikou_span = self.ich.chikou_span[0] 145 | 146 | diff_tenkan = self.data.close[0] - tenkan_sen 147 | diff_kijun = self.data.close[0] - kijun_sen 148 | diff_senkou_span_a = self.data.close[0] - senkou_span_a 149 | diff_senkou_span_b = self.data.close[0] - senkou_span_b 150 | diff_chikou_span = self.data.close[0] - chikou_span 151 | 152 | inputs_array = [ diff_tenkan, 153 | diff_kijun, 154 | diff_senkou_span_a, 155 | diff_senkou_span_b, 156 | diff_chikou_span 157 | ] 158 | 159 | #NormaizeArray(inputs_array) 160 | 161 | # AI Input 162 | 163 | self.ai_inputs = np.array(inputs_array).reshape(1,5) 164 | 165 | # AI data are ready 166 | return True 167 | else: 168 | return False 169 | """ 170 | 171 | pass -------------------------------------------------------------------------------- /strategies/AiTorchModel.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | import backtrader as bt 20 | 21 | import metaStrategy as mt 22 | 23 | import numpy as np 24 | from enum import Enum 25 | 26 | import torch 27 | 28 | # action list 29 | class Action(Enum): 30 | HOLD=0 31 | BUY=1 32 | SELL=2 33 | 34 | # Create a subclass of Strategy to define the indicators and logic 35 | class AiTorchModel(mt.MetaStrategy): 36 | 37 | params = ( 38 | ('model', ""), # Model name 39 | ('tradeSize', 2000), 40 | ('atrperiod', 14), # ATR Period (standard) 41 | ('atrdist_SL', 3), # ATR distance for stop price 42 | ('atrdist_TP', 5), # ATR distance for take profit price 43 | ('tenkan', 9), 44 | ('kijun', 26), 45 | ('senkou', 52), 46 | ('senkou_lead', 26), # forward push 47 | ('chikou', 26), # backwards push 48 | ) 49 | 50 | def notify_order(self, order): 51 | if order.status == order.Completed: 52 | #print("Order completed") 53 | pass 54 | 55 | if not order.alive(): 56 | self.order = None # indicate no order is pending 57 | 58 | def __init__(self, *argv): 59 | 60 | # used to modify parameters 61 | super().__init__(argv[0]) 62 | 63 | # Ichi indicator 64 | self.ich = bt.ind.Ichimoku() 65 | ''' 66 | self.data, 67 | tenkan=self.params.tenkan, 68 | kijun=self.params.kijun, 69 | senkou=self.params.senkou, 70 | senkou_lead=self.params.senkou_lead, 71 | chikou=self.params.chikou) 72 | ''' 73 | 74 | # To set the stop price 75 | self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod) 76 | 77 | self.stochastic = bt.ind.stochastic.Stochastic(self.data) 78 | 79 | pass 80 | 81 | def start(self): 82 | self.order = None # sentinel to avoid operrations on pending order 83 | 84 | # Load the model 85 | self.model = torch.load(self.p.model) 86 | pass 87 | 88 | def next(self): 89 | 90 | self.ai_ready = self.prepareData() 91 | 92 | if self.order or not self.ai_ready: 93 | return # pending order execution 94 | 95 | # Prepare data for Model 96 | predicted_actions = self.model.predict_step([self.ai_inputs]) 97 | 98 | # Take action with the most credibility 99 | action = np.argmax( predicted_actions ) 100 | 101 | if not self.position: # not in the market 102 | 103 | if action == Action.SELL.value: 104 | self.order = self.sell(size=self.p.tradeSize) 105 | ldist = self.atr[0] * self.p.atrdist_SL 106 | self.lstop = self.data.close[0] + ldist 107 | pdist = self.atr[0] * self.p.atrdist_TP 108 | self.take_profit = self.data.close[0] - pdist 109 | 110 | elif action == Action.BUY.value: 111 | self.order = self.buy(size=self.p.tradeSize) 112 | ldist = self.atr[0] * self.p.atrdist_SL 113 | self.lstop = self.data.close[0] - ldist 114 | pdist = self.atr[0] * self.p.atrdist_TP 115 | self.take_profit = self.data.close[0] + pdist 116 | 117 | else: # in the market 118 | pclose = self.data.close[0] 119 | pstop = self.lstop # seems to be the bug 120 | 121 | if (not ((pstoppclose>self.take_profit))): 122 | self.close() # Close position 123 | 124 | pass 125 | 126 | # Here you have to transform self object price and indicators into a np.array input for AI Model 127 | # How you do it depend on your AI Model inputs 128 | # Strategy is in the data preparation for AI :D 129 | def prepareData(self): 130 | 131 | # https://stackoverflow.com/questions/53979199/tensorflow-keras-returning-multiple-predictions-while-expecting-one 132 | 133 | inputs_array = np.array([[ self.stochastic.percK[0] / 100.0, self.stochastic.percD[0] / 100.0 ]]) 134 | 135 | self.ai_inputs = inputs_array 136 | 137 | return True 138 | """ 139 | if len(self.ich.l.tenkan_sen) > 0 and len(self.ich.kijun_sen) > 0 and len(self.ich.senkou_span_a) > 0 and len(self.ich.senkou_span_b) > 0 and len(self.ich.chikou_span) > 0: 140 | 141 | tenkan_sen = self.ich.tenkan_sen[0] 142 | kijun_sen = self.ich.kijun_sen[0] 143 | senkou_span_a = self.ich.senkou_span_a[0] 144 | senkou_span_b = self.ich.senkou_span_b[0] 145 | chikou_span = self.ich.chikou_span[0] 146 | 147 | diff_tenkan = self.data.close[0] - tenkan_sen 148 | diff_kijun = self.data.close[0] - kijun_sen 149 | diff_senkou_span_a = self.data.close[0] - senkou_span_a 150 | diff_senkou_span_b = self.data.close[0] - senkou_span_b 151 | diff_chikou_span = self.data.close[0] - chikou_span 152 | 153 | inputs_array = [ diff_tenkan, 154 | diff_kijun, 155 | diff_senkou_span_a, 156 | diff_senkou_span_b, 157 | diff_chikou_span 158 | ] 159 | 160 | #NormaizeArray(inputs_array) 161 | 162 | # AI Input 163 | 164 | self.ai_inputs = np.array(inputs_array).reshape(1,5) 165 | 166 | # AI data are ready 167 | return True 168 | else: 169 | return False 170 | """ 171 | 172 | pass -------------------------------------------------------------------------------- /strategies/ichimokuStrat1.py: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # 3 | # Copyright (C) 2021 - Skinok 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU General Public License as published by 7 | # the Free Software Foundation, either version 3 of the License, or 8 | # (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program. If not, see . 17 | # 18 | ############################################################################### 19 | import sys 20 | import backtrader as bt 21 | 22 | import metaStrategy as mt 23 | 24 | # Create a subclass of Strategy to define the indicators and logic 25 | class ichimokuStrat1(mt.MetaStrategy): 26 | 27 | params = ( 28 | ('atrperiod', 14), # ATR Period (standard) 29 | ('atrdist_x', 1.5), # ATR distance for stop price 30 | ('atrdist_y', 1.35), # ATR distance for take profit price 31 | ('tenkan', 9), 32 | ('kijun', 26), 33 | ('senkou', 52), 34 | ('senkou_lead', 26), # forward push 35 | ('chikou', 26), # backwards push 36 | ) 37 | 38 | def notify_order(self, order): 39 | if order.status == order.Completed: 40 | #print("Order completed") 41 | pass 42 | 43 | if not order.alive(): 44 | self.order = None # indicate no order is pending 45 | 46 | def __init__(self, *argv): 47 | 48 | # used to modify parameters 49 | super().__init__(argv[0]) 50 | 51 | # Ichi indicator 52 | self.ichi = bt.indicators.Ichimoku(self.datas[0], 53 | tenkan=self.params.tenkan, 54 | kijun=self.params.kijun, 55 | senkou=self.params.senkou, 56 | senkou_lead=self.params.senkou_lead, 57 | chikou=self.params.chikou) 58 | 59 | # Cross of tenkan and kijun - 60 | #1.0 if the 1st data crosses the 2nd data upwards - long 61 | #-1.0 if the 1st data crosses the 2nd data downwards - short 62 | self.tkcross = bt.indicators.CrossOver(self.ichi.tenkan_sen, self.ichi.kijun_sen) 63 | 64 | # To set the stop price 65 | self.atr = bt.indicators.ATR(self.data, period=self.p.atrperiod) 66 | 67 | # Long Short ichimoku logic 68 | self.long = bt.And( (self.data.close[0] > self.ichi.senkou_span_a(0)), 69 | (self.data.close[0] > self.ichi.senkou_span_b(0)), 70 | (self.tkcross == 1)) 71 | 72 | self.short = bt.And((self.data.close[0] < self.ichi.senkou_span_a(0)), 73 | (self.data.close[0] < self.ichi.senkou_span_b(0)), 74 | (self.tkcross == -1)) 75 | 76 | def start(self): 77 | print(" Starting IchimokuStart1 strategy") 78 | self.order = None # sentinel to avoid operrations on pending order 79 | 80 | def next(self): 81 | 82 | if self.order: 83 | return # pending order execution 84 | 85 | if not self.position: # not in the market 86 | if self.short: 87 | self.order = self.sell() 88 | ldist = self.atr[0] * self.p.atrdist_x 89 | self.lstop = self.data.close[0] + ldist 90 | pdist = self.atr[0] * self.p.atrdist_y 91 | self.take_profit = self.data.close[0] - pdist 92 | if self.long: 93 | self.order = self.buy() 94 | ldist = self.atr[0] * self.p.atrdist_x 95 | self.lstop = self.data.close[0] - ldist 96 | pdist = self.atr[0] * self.p.atrdist_y 97 | self.take_profit = self.data.close[0] + pdist 98 | 99 | else: # in the market 100 | pclose = self.data.close[0] 101 | pstop = self.lstop # seems to be the bug 102 | 103 | if ((pstoppclose>self.take_profit)): 104 | self.close() # Close position 105 | 106 | -------------------------------------------------------------------------------- /strategies/sma_crossover.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2015-2020 Daniel Rodriguez 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | from __future__ import (absolute_import, division, print_function, 22 | unicode_literals) 23 | 24 | 25 | import backtrader as bt 26 | import backtrader.indicators as btind 27 | 28 | import metaStrategy as mt 29 | 30 | class sma_crossover(mt.MetaStrategy): 31 | '''This is a long-only strategy which operates on a moving average cross 32 | 33 | Note: 34 | - Although the default 35 | 36 | Buy Logic: 37 | - No position is open on the data 38 | 39 | - The ``fast`` moving averagecrosses over the ``slow`` strategy to the 40 | upside. 41 | 42 | Sell Logic: 43 | - A position exists on the data 44 | 45 | - The ``fast`` moving average crosses over the ``slow`` strategy to the 46 | downside 47 | 48 | Order Execution Type: 49 | - Market 50 | 51 | ''' 52 | alias = ('SMA_CrossOver',) 53 | 54 | params = ( 55 | # period for the fast Moving Average 56 | ('fast', 5), 57 | # period for the slow moving average 58 | ('slow', 30), 59 | # Trade size 60 | ('tradeSize', 500) 61 | ) 62 | 63 | def __init__(self, *argv): 64 | 65 | # used to modify parameters 66 | super().__init__(argv[0]) 67 | 68 | sma_fast = btind.MovAv.SMA(period=self.p.fast) 69 | sma_slow = btind.MovAv.SMA(period=self.p.slow) 70 | 71 | self.buysig = btind.CrossOver(sma_fast, sma_slow) 72 | 73 | def next(self): 74 | if self.position.size: 75 | if self.buysig < 0: 76 | self.sell(size=self.p.tradeSize) 77 | 78 | elif self.buysig > 0: 79 | self.buy(size=self.p.tradeSize) 80 | -------------------------------------------------------------------------------- /strategyResultsUI.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtCore, QtWidgets, uic 2 | 3 | import os 4 | 5 | class StrategyResultsUI(QtWidgets.QWidget): 6 | 7 | def __init__(self, controller, parent = None): 8 | super(StrategyResultsUI, self).__init__() 9 | 10 | self.controller = controller 11 | 12 | self.parent = parent 13 | 14 | # It does not finish by a "/" 15 | self.current_dir_path = os.path.dirname(os.path.realpath(__file__)) 16 | 17 | uic.loadUi( self.current_dir_path + "/ui/strategyResults.ui", self) 18 | 19 | self.summaryTableWidget= self.findChild(QtWidgets.QTableWidget, "summaryTableWidget") 20 | self.TradesGB = self.findChild(QtWidgets.QGroupBox, "TradesGB") 21 | -------------------------------------------------------------------------------- /strategyTesterUI.py: -------------------------------------------------------------------------------- 1 | from PyQt6 import QtCore, QtWidgets, uic 2 | 3 | import os 4 | import loadDataFilesUI 5 | 6 | class StrategyTesterUI(QtWidgets.QWidget): 7 | 8 | def __init__(self, controller, parentWindow): 9 | super(StrategyTesterUI, self).__init__() 10 | 11 | self.controller = controller 12 | 13 | self.parent = parentWindow 14 | 15 | # It does not finish by a "/" 16 | self.current_dir_path = os.path.dirname(os.path.realpath(__file__)) 17 | 18 | uic.loadUi( self.current_dir_path + "/ui/strategyTester.ui", self) 19 | 20 | # Data 21 | self.importDataBtn = self.findChild(QtWidgets.QPushButton, "importDataBtn") 22 | self.importDataBtn.clicked.connect( self.loadData ) 23 | 24 | # Strategy type PushButtons 25 | self.strategyTypeAITensorFlowBtn = self.findChild(QtWidgets.QPushButton, "strategyTypeAITensorFlowBtn") 26 | self.strategyTypeAiStablebaselinesBtn = self.findChild(QtWidgets.QPushButton, "strategyTypeAiStablebaselinesBtn") 27 | self.strategyTypeAlgoBtn = self.findChild(QtWidgets.QPushButton, "strategyTypeAlgoBtn") 28 | self.strategyTypeAiTorchBtn = self.findChild(QtWidgets.QPushButton, "strategyTypeAiTorchBtn") 29 | 30 | self.strategyTypeDetailsSW = self.findChild(QtWidgets.QStackedWidget, "strategyTypeDetailsSW") 31 | 32 | # Ai Algo 33 | self.AiModelPathLE = self.findChild(QtWidgets.QLineEdit, "AiModelPathLE") 34 | self.AiModelPathBtn = self.findChild(QtWidgets.QPushButton, "AiModelPathBtn") 35 | 36 | # Custom Algo 37 | self.runningStratBtn = self.findChild(QtWidgets.QProgressBar, "runningStratBtn") 38 | self.strategyNameCB = self.findChild(QtWidgets.QComboBox, "strategyNameCB") 39 | 40 | # Run button 41 | self.runBacktestBtn = self.findChild(QtWidgets.QPushButton, "runBacktestBtn") 42 | 43 | # Connect ui buttons 44 | self.strategyTypeAITensorFlowBtn.clicked.connect(self.loadTFModel) 45 | self.strategyTypeAiStablebaselinesBtn.clicked.connect(self.loadStableBaselinesModel) 46 | self.strategyTypeAiTorchBtn.clicked.connect(self.loadTorchModel) 47 | self.strategyTypeAlgoBtn.clicked.connect(self.strategyTypeAlgoActivated) 48 | 49 | self.strategyNameCB.currentIndexChanged.connect(self.strategyNameActivated) 50 | self.runBacktestBtn.clicked.connect(self.run) 51 | 52 | # Init Run button to false waiting for user inputs 53 | self.runBacktestBtn.setEnabled(False) 54 | 55 | def initialize(self): 56 | 57 | # adding list of items to combo box 58 | self.strategyNames = list(QtCore.QDir(self.current_dir_path + "/strategies").entryList(QtCore.QDir.Files)) 59 | 60 | # Remove straty .py file name 61 | self.strategyBaseName = [] 62 | for stratName in self.strategyNames: 63 | # here remove file extension 64 | if not stratName.startswith('Ai'): 65 | self.strategyBaseName.append(QtCore.QFileInfo(stratName).baseName()) 66 | 67 | self.strategyNameCB.addItems(self.strategyBaseName) 68 | self.strategyNameCB.setCurrentIndex(self.strategyNameCB.count()-1) 69 | 70 | # 71 | self.loadDataFileUI = loadDataFilesUI.LoadDataFilesUI(self.controller, self.parent) 72 | self.loadDataFileUI.hide() 73 | pass 74 | 75 | def loadData(self): 76 | self.loadDataFileUI.show() 77 | pass 78 | 79 | def run(self): 80 | self.controller.run() 81 | pass 82 | 83 | def strategyNameActivated(self): 84 | stratBaseName = self.strategyNameCB.currentText() 85 | self.controller.addStrategy(stratBaseName) 86 | pass 87 | 88 | def strategyTypeAlgoActivated(self): 89 | if self.strategyTypeAlgoBtn.isChecked(): 90 | self.strategyTypeDetailsSW.setCurrentIndex(0) 91 | pass 92 | 93 | 94 | # Load an AI Model from Tensor Flow framework 95 | def loadTFModel(self): 96 | 97 | ai_model_dir = QtWidgets.QFileDialog.getExistingDirectory(self.parent,"Open Tensorflow Model", self.current_dir_path) 98 | 99 | self.controller.addStrategy("AiTensorFlowModel") 100 | self.strategyTypeDetailsSW.setCurrentIndex(1) 101 | 102 | self.AiModelPathLE.setText(ai_model_dir) 103 | self.controller.strategyParametersSave("model", ai_model_dir) 104 | 105 | pass 106 | 107 | # Load an AI Model from Stable Baselines framework 108 | def loadStableBaselinesModel(self): 109 | 110 | ai_model_zip_file = QtWidgets.QFileDialog.getOpenFileName(self.parent,"Open Torch Model", self.current_dir_path, "*.zip")[0] 111 | 112 | self.controller.addStrategy("AiStableBaselinesModel") 113 | self.strategyTypeDetailsSW.setCurrentIndex(1) 114 | 115 | self.AiModelPathLE.setText(ai_model_zip_file) 116 | self.controller.strategyParametersSave("model", ai_model_zip_file) 117 | 118 | pass 119 | 120 | # Load an AI Model from Py Torch framework 121 | def loadTorchModel(self): 122 | 123 | ai_model_zip_file = QtWidgets.QFileDialog.getOpenFileName(self.parent,"Open Torch Model", self.current_dir_path, "*.zip")[0] 124 | 125 | self.controller.addStrategy("AiTorchModel") 126 | self.strategyTypeDetailsSW.setCurrentIndex(1) 127 | 128 | self.AiModelPathLE.setText(ai_model_zip_file) 129 | self.controller.strategyParametersSave("model", ai_model_zip_file) 130 | 131 | pass -------------------------------------------------------------------------------- /stylesheets/Dark.qss: -------------------------------------------------------------------------------- 1 | /* 2 | * Mumble Dark Theme 3 | * https://github.com/mumble-voip/mumble-theme 4 | * 5 | * Based on MetroMumble Theme by xPoke 6 | * https://github.com/xPoke 7 | * 8 | * Originally forked from Flat Mumble Theme by xPaw (xpaw.ru) 9 | * 10 | * Licensed under The Do What The Fuck You Want To Public License (WTFPL) 11 | */ 12 | /* 13 | * YOU SHOULD NOT MODIFY THIS FILE 14 | * Edit the files in the "source" folder instead. 15 | * See project README 16 | * 17 | */ 18 | ApplicationPalette { 19 | qproperty-window: #2e2e2e; 20 | qproperty-windowtext: #eee; 21 | qproperty-windowtext_disabled: #484848; 22 | qproperty-base: #191919; 23 | qproperty-alternatebase: #2d2d2d; 24 | qproperty-text: #d8d8d8; 25 | qproperty-text_disabled: #484848; 26 | qproperty-tooltipbase: #191919; 27 | qproperty-tooltiptext: #d8d8d8; 28 | qproperty-tooltiptext_disabled: #484848; 29 | qproperty-brighttext: #FFF; 30 | qproperty-brighttext_disabled: #484848; 31 | qproperty-highlight: #298ce1; 32 | qproperty-highlightedtext: #FFF; 33 | qproperty-highlightedtext_disabled: #484848; 34 | qproperty-button: #444; 35 | qproperty-buttontext: #d8d8d8; 36 | qproperty-buttontext_disabled: #484848; 37 | qproperty-link: #39a5dd; 38 | qproperty-linkvisited: #39a5dd; 39 | qproperty-light: #1c1c1c; 40 | qproperty-midlight: transparent; 41 | qproperty-mid: #1c1c1c; 42 | qproperty-dark: transparent; 43 | qproperty-shadow: #1c1c1c; 44 | } 45 | 46 | QObject, 47 | QObject::separator, 48 | QObject::handle, 49 | QObject::tab-bar, 50 | QObject::tab, 51 | QObject::section { 52 | font-family: "Segoe UI", Frutiger, "Frutiger Linotype", "Dejavu Sans", "Helvetica Neue", Arial, sans-serif; 53 | font-size: 10pt; 54 | margin: 0; 55 | padding: 0; 56 | outline: 0; 57 | border: 0; 58 | selection-background-color: #298ce1; 59 | selection-color: #FFF; 60 | alternate-background-color: transparent; 61 | color: #eee; 62 | border-radius: 2px; 63 | } 64 | 65 | QMainWindow, 66 | QDockWidget { 67 | background-color: #2e2e2e; 68 | } 69 | 70 | QDialog, 71 | QWizard *, 72 | QCalendarWidget *, 73 | #qswPages > QObject { 74 | background-color: #1D1D1D; 75 | color: #d8d8d8; 76 | } 77 | 78 | QObject:disabled, 79 | QObject::item:disabled { 80 | color: #484848; 81 | } 82 | 83 | a { 84 | color: #39a5dd; 85 | text-decoration: none; 86 | } 87 | 88 | QObject::separator { 89 | height: 4px; 90 | width: 4px; 91 | } 92 | 93 | QObject::separator:hover { 94 | background: #333; 95 | } 96 | 97 | DockTitleBar { 98 | font-size: 7pt; 99 | } 100 | 101 | QToolTip, 102 | QWhatsThis { 103 | font-size: 8pt; 104 | min-height: 1.3em; 105 | border: 1px solid #888; 106 | border-radius: 0; 107 | background-color: #191919; 108 | color: #d8d8d8; 109 | } 110 | 111 | QTextBrowser, 112 | QTextEdit { 113 | background-color: #191919; 114 | color: #d8d8d8; 115 | border: 1px solid #1c1c1c; 116 | } 117 | 118 | QToolBar { 119 | background-color: #2e2e2e; 120 | spacing: 0; 121 | padding: 2px; 122 | } 123 | 124 | QToolButton { 125 | border: 1px solid transparent; 126 | border-radius: 2px; 127 | padding: 1px; 128 | margin: 1px; 129 | } 130 | 131 | QToolButton:on { 132 | background-color: #444; 133 | border: 1px solid #444; 134 | } 135 | 136 | QToolButton:hover { 137 | background-color: #3e4f5e; 138 | border: 1px solid #3e4f5e; 139 | } 140 | 141 | QToolButton:pressed { 142 | background-color: #484848; 143 | } 144 | 145 | QToolBar::separator { 146 | background: #555; 147 | height: 1px; 148 | margin: 4px; 149 | width: 1px; 150 | } 151 | 152 | QToolBar::separator:hover { 153 | background: #555; 154 | border: 0; 155 | } 156 | 157 | QToolButton#qt_toolbar_ext_button { 158 | min-width: 8px; 159 | width: 8px; 160 | padding: 1px; 161 | qproperty-icon: url(skin:controls/toolbar_ext.svg); 162 | } 163 | 164 | QToolBar::handle:horizontal { 165 | image: url(skin:controls/handle_horizontal.svg); 166 | width: 8px; 167 | padding: 4px; 168 | } 169 | 170 | QToolBar::handle:vertical { 171 | image: url(skin:controls/handle_vertical.svg); 172 | height: 8px; 173 | padding: 4px; 174 | } 175 | 176 | QMenuBar::item { 177 | background-color: transparent; 178 | padding: 4px 12px; 179 | } 180 | 181 | QMenuBar::item:selected { 182 | background: #298ce1; 183 | color: #FFF; 184 | } 185 | 186 | QMenuBar::item:pressed { 187 | background: #1979ca; 188 | color: #FFF; 189 | } 190 | 191 | QMenu { 192 | background: #2b2b2b; 193 | border: 1px solid #1c1c1c; 194 | color: #d8d8d8; 195 | } 196 | 197 | QMenu::item { 198 | border: 1px solid transparent; 199 | color: #d8d8d8; 200 | padding: 5px 16px; 201 | padding-left: 25px; 202 | border-radius: 2px; 203 | } 204 | 205 | QMenu::item:selected { 206 | background: #3e4f5e; 207 | border: 1px solid #3e4f5e; 208 | } 209 | 210 | QMenu::item:disabled { 211 | border: 1px solid transparent; 212 | background: transparent; 213 | } 214 | 215 | QMenu::separator { 216 | background: #555; 217 | height: 1px; 218 | } 219 | 220 | QMenu::indicator { 221 | padding-top: 2px; 222 | height: 25px; 223 | width: 25px; 224 | } 225 | 226 | QPushButton { 227 | background-color: #444; 228 | border: 1px solid #444; 229 | color: #d8d8d8; 230 | font-size: 12pt; 231 | padding: 3px 20px; 232 | } 233 | 234 | QPushButton:focus { 235 | background-color: #3e4f5e; 236 | } 237 | 238 | QPushButton:hover { 239 | background-color: #595959; 240 | border-color: #555; 241 | } 242 | 243 | QPushButton:hover:focus { 244 | background-color: #485d6f; 245 | border-color: #485d6f; 246 | } 247 | 248 | QPushButton:focus { 249 | border-color: #3e4f5e; 250 | } 251 | 252 | QPushButton:pressed, 253 | QPushButton:pressed:focus { 254 | background-color: #298ce1; 255 | border-color: #298ce1; 256 | color: #FFF; 257 | } 258 | 259 | QGroupBox, 260 | #qwMacWarning, 261 | #qwInlineNotice { 262 | background-color: #2d2d2d; 263 | border: 1px solid #1c1c1c; 264 | color: #d8d8d8; 265 | font-size: 13pt; 266 | padding: 4px; 267 | padding-top: 1em; 268 | } 269 | 270 | QGroupBox::title { 271 | background-color: transparent; 272 | margin: 6px; 273 | margin-left: 8px; 274 | margin-right: 8px; 275 | } 276 | 277 | QListView { 278 | background-color: #191919; 279 | border: 1px solid #1c1c1c; 280 | } 281 | 282 | QListView::item { 283 | border-radius: 2px; 284 | border: 1px solid transparent; 285 | color: #d8d8d8; 286 | selection-color: #d8d8d8; 287 | padding: 2px 4px; 288 | } 289 | 290 | QListView::item:hover { 291 | background-color: #333; 292 | border: 1px solid #333; 293 | } 294 | 295 | QListView::item:selected { 296 | background-color: #3b3b3b; 297 | border: 1px solid #3b3b3b; 298 | } 299 | 300 | QListView::item:selected:active { 301 | background-color: #3e4f5e; 302 | border: 1px solid #3e4f5e; 303 | } 304 | 305 | QTreeView { 306 | background-color: #191919; 307 | color: #d8d8d8; 308 | selection-background-color: #191919; 309 | selection-color: #d8d8d8; 310 | border: 1px solid #1c1c1c; 311 | } 312 | 313 | QTreeView::item { 314 | min-width: 60px; 315 | border: 1px solid transparent; 316 | border-left: 0; 317 | border-right: 0; 318 | color: #d8d8d8; 319 | padding: 2px 4px; 320 | selection-color: #d8d8d8; 321 | border-radius: 0; 322 | } 323 | 324 | QTreeView::item:first, 325 | QTreeView::item:only-one { 326 | border-left: 1px solid transparent; 327 | border-top-left-radius: 2px; 328 | border-bottom-left-radius: 2px; 329 | } 330 | 331 | QTreeView::item:last, 332 | QTreeView::item:only-one { 333 | border-right: 1px solid transparent; 334 | border-top-right-radius: 2px; 335 | border-bottom-right-radius: 2px; 336 | } 337 | 338 | QTreeView::item:hover, 339 | QTreeView::item:focus { 340 | background-color: #333; 341 | border-color: #333; 342 | } 343 | 344 | QTreeView::item:selected { 345 | background-color: #3b3b3b; 346 | border: 1px solid #3b3b3b; 347 | border-right: 0; 348 | border-left: 0; 349 | } 350 | 351 | QTreeView::item:selected:first, 352 | QTreeView::item:selected:only-one { 353 | border-left: 1px solid #3b3b3b; 354 | } 355 | 356 | QTreeView::item:selected:last, 357 | QTreeView::item:selected:only-one { 358 | border-right: 1px solid #3b3b3b; 359 | } 360 | 361 | QTreeView::item:selected:active { 362 | background-color: #3e4f5e; 363 | border: 1px solid #3e4f5e; 364 | border-right: 0; 365 | border-left: 0; 366 | } 367 | 368 | QTreeView::item:selected:active:first, 369 | QTreeView::item:selected:active:only-one { 370 | border-left: 1px solid #3e4f5e; 371 | } 372 | 373 | QTreeView::item:selected:active:last, 374 | QTreeView::item:selected:active:only-one { 375 | border-right: 1px solid #3e4f5e; 376 | } 377 | 378 | QTreeView::branch { 379 | border-image: none; 380 | image: none; 381 | margin-left: 3px; 382 | margin-top: 1px; 383 | padding-left: 3px; 384 | } 385 | 386 | QTreeView::branch:has-children:closed { 387 | image: url(skin:controls/branch_closed.svg); 388 | } 389 | 390 | QTreeView::branch:has-children:open { 391 | image: url(skin:controls/branch_open.svg); 392 | } 393 | 394 | QHeaderView { 395 | border-bottom: 1px solid #1c1c1c; 396 | border-radius: 0; 397 | } 398 | 399 | QHeaderView::section { 400 | border: 0; 401 | background-color: #2d2d2d; 402 | color: #d8d8d8; 403 | padding: 4px; 404 | padding-left: 8px; 405 | padding-right: 20px; 406 | border-radius: 0; 407 | } 408 | 409 | QHeaderView::down-arrow, 410 | QHeaderView::up-arrow { 411 | margin: 1px; 412 | top: 1px; 413 | right: 5px; 414 | width: 14px; 415 | } 416 | 417 | QHeaderView::down-arrow { 418 | image: url(skin:controls/arrow_down.svg); 419 | } 420 | 421 | QHeaderView::up-arrow { 422 | image: url(skin:controls/arrow_up.svg); 423 | } 424 | 425 | QTabWidget::pane { 426 | background-color: #2d2d2d; 427 | border: 1px solid #1c1c1c; 428 | } 429 | 430 | QTabWidget::pane:top { 431 | margin-top: -1px; 432 | border-radius: 2px; 433 | border-top-left-radius: 0; 434 | } 435 | 436 | QTabWidget::pane:bottom { 437 | margin-bottom: -1px; 438 | border-radius: 2px; 439 | border-bottom-left-radius: 0; 440 | } 441 | 442 | QTabWidget::tab-bar { 443 | background-color: #1D1D1D; 444 | } 445 | 446 | QTabBar::tab { 447 | color: #ccc; 448 | background-color: #1e1e1e; 449 | padding: 6px 16px; 450 | border-radius: 0; 451 | border: 1px solid #1c1c1c; 452 | border-right: 0; 453 | } 454 | 455 | QTabBar::tab:last, 456 | QTabBar::tab:only-one { 457 | border-right: 1px solid #1c1c1c; 458 | } 459 | 460 | QTabBar::tab:hover { 461 | background-color: #3e4f5e; 462 | } 463 | 464 | QTabBar::tab:disabled { 465 | color: #484848; 466 | } 467 | 468 | QTabBar::tab:selected { 469 | color: #d8d8d8; 470 | background-color: #2d2d2d; 471 | } 472 | 473 | QTabBar::tab:top { 474 | border-bottom: 0; 475 | margin-bottom: 1px; 476 | } 477 | 478 | QTabBar::tab:bottom { 479 | border-top: 0; 480 | margin-top: 1px; 481 | } 482 | 483 | QTabBar::tab:top:selected { 484 | padding-bottom: 7px; 485 | margin-bottom: 0; 486 | } 487 | 488 | QTabBar::tab:bottom:selected { 489 | padding-top: 7px; 490 | margin-top: 0; 491 | } 492 | 493 | QTabBar::tab:top:first, 494 | QTabBar::tab:top:only-one { 495 | border-top-left-radius: 2px; 496 | } 497 | 498 | QTabBar::tab:top:last, 499 | QTabBar::tab:top:only-one { 500 | border-top-right-radius: 2px; 501 | } 502 | 503 | QTabBar::tab:bottom:first, 504 | QTabBar::tab:bottom:only-one { 505 | border-bottom-left-radius: 2px; 506 | } 507 | 508 | QTabBar::tab:bottom:last, 509 | QTabBar::tab:bottom:only-one { 510 | border-bottom-right-radius: 2px; 511 | } 512 | 513 | QScrollBar { 514 | border-radius: 0; 515 | font-size: 10pt; 516 | } 517 | 518 | QScrollBar:vertical { 519 | border-left: 1px solid #1c1c1c; 520 | width: 1em; 521 | } 522 | 523 | QScrollBar:horizontal { 524 | border-top: 1px solid #1c1c1c; 525 | height: 1em; 526 | } 527 | 528 | QScrollBar::handle { 529 | margin: -1px; 530 | background: #666; 531 | border: 1px solid #1c1c1c; 532 | } 533 | 534 | QScrollBar::handle:vertical { 535 | min-height: 10px; 536 | } 537 | 538 | QScrollBar::handle:horizontal { 539 | min-width: 10px; 540 | } 541 | 542 | QScrollBar::handle:hover { 543 | background: #888; 544 | } 545 | 546 | QScrollBar::left-arrow, 547 | QScrollBar::right-arrow, 548 | QScrollBar::up-arrow, 549 | QScrollBar::down-arrow, 550 | QScrollBar::sub-line, 551 | QScrollBar::add-line, 552 | QScrollBar::add-page, 553 | QScrollBar::sub-page { 554 | background: #2e2e2e; 555 | height: 0; 556 | width: 0; 557 | border-radius: 0; 558 | border: 0; 559 | } 560 | 561 | QAbstractScrollArea::corner { 562 | border-left: 1px solid #1c1c1c; 563 | border-top: 1px solid #1c1c1c; 564 | height: 0; 565 | width: 0; 566 | border-radius: 0; 567 | border-top: 1px solid #1c1c1c; 568 | border-left: 1px solid #1c1c1c; 569 | background: #191919; 570 | } 571 | 572 | QLineEdit, 573 | QComboBox, 574 | QSpinBox, 575 | QAbstractSpinBox { 576 | color: #d8d8d8; 577 | padding: 4px; 578 | min-height: 1em; 579 | } 580 | 581 | QComboBox, 582 | QSpinBox, 583 | QAbstractSpinBox { 584 | border: 1px solid #444; 585 | background-color: #444; 586 | } 587 | 588 | QLineEdit, 589 | QTextEdit, 590 | QPlainTextEdit, 591 | QSpinBox, 592 | QAbstractSpinBox, 593 | QComboBox:editable { 594 | border: 1px solid #2e2e2e; 595 | background-color: #191919; 596 | } 597 | 598 | QSpinBox, 599 | QAbstractSpinBox { 600 | min-width: 2.5em; 601 | padding-right: 10px; 602 | } 603 | 604 | QPushButton:disabled, 605 | QLineEdit:disabled, 606 | QTextEdit:disabled, 607 | QPlainTextEdit:disabled, 608 | QListWidget:disabled, 609 | QTreeWidget:disabled, 610 | QComboBox:disabled, 611 | QSpinBox:disabled, 612 | QAbstractSpinBox:disabled { 613 | border: 1px solid transparent; 614 | background-color: #282828; 615 | } 616 | 617 | QComboBox::drop-down, 618 | QAbstractSpinBox::drop-down, 619 | QSpinBox::drop-down, 620 | QDateTimeEdit::drop-down { 621 | background-color: #191919; 622 | border: 0; 623 | margin-left: 4px; 624 | margin-right: 12px; 625 | margin-top: 5px; 626 | } 627 | 628 | QComboBox::down-arrow, 629 | QDateTimeEdit::down-arrow { 630 | margin-top: -2px; 631 | image: url(skin:controls/arrow_down.svg); 632 | width: 14px; 633 | } 634 | 635 | QComboBox::down-arrow:disabled, 636 | QDateTimeEdit::down-arrow:disabled { 637 | image: url(skin:controls/arrow_down_disabled.svg); 638 | } 639 | 640 | QToolButton[popupMode="1"], 641 | QToolButton[popupMode="2"], 642 | QPushButton[popupMode="1"], 643 | QPushButton[popupMode="2"] { 644 | padding-right: 14px; 645 | } 646 | 647 | QToolButton::menu-arrow, 648 | QToolButton::menu-indicator, 649 | QPushButton::menu-arrow, 650 | QPushButton::menu-indicator { 651 | image: url(skin:controls/arrow_down.svg); 652 | subcontrol-origin: padding; 653 | subcontrol-position: center right; 654 | top: 2px; 655 | right: 2px; 656 | width: 14px; 657 | } 658 | 659 | QSpinBox::down-button, 660 | QAbstractSpinBox::down-button { 661 | padding-right: 4px; 662 | image: url(skin:controls/arrow_down.svg); 663 | width: 14px; 664 | padding-bottom: 1px; 665 | } 666 | 667 | QSpinBox::down-button:disabled, 668 | QAbstractSpinBox::down-button:disabled { 669 | image: url(skin:controls/arrow_down_disabled.svg); 670 | } 671 | 672 | QSpinBox::up-button, 673 | QAbstractSpinBox::up-button { 674 | padding-right: 4px; 675 | image: url(skin:controls/arrow_up.svg); 676 | width: 14px; 677 | padding-top: 1px; 678 | } 679 | 680 | QSpinBox::up-button:disabled, 681 | QAbstractSpinBox::up-button:disabled { 682 | image: url(skin:controls/arrow_up_disabled.svg); 683 | } 684 | 685 | QComboBox QAbstractItemView { 686 | background-color: #191919; 687 | border: 1px solid #1c1c1c; 688 | color: #d8d8d8; 689 | border-radius: 0; 690 | } 691 | 692 | QLabel, 693 | QCheckBox, 694 | QAbstractCheckBox, 695 | QTreeView::indicator, 696 | QRadioButton { 697 | color: #d8d8d8; 698 | background: transparent; 699 | } 700 | 701 | QCheckBox::indicator, 702 | QTreeView::indicator { 703 | background-color: #444; 704 | border: 1px solid #444; 705 | height: 13px; 706 | width: 13px; 707 | margin-top: 1px; 708 | } 709 | 710 | QMenu::indicator { 711 | width: 12px; 712 | left: 6px; 713 | } 714 | 715 | QCheckBox::indicator:checked, 716 | QMenu::indicator:checked, 717 | QTreeView::indicator:checked { 718 | image: url(skin:controls/checkbox_check_dark.svg); 719 | } 720 | 721 | QCheckBox::indicator:disabled, 722 | QTreeView::indicator:disabled { 723 | border: 1px solid #2d2d2d; 724 | background-color: #282828; 725 | } 726 | 727 | QCheckBox::indicator:checked:disabled, 728 | QTreeView::indicator:checked:disabled { 729 | border: 1px solid transparent; 730 | image: url(skin:controls/checkbox_check_disabled.svg); 731 | } 732 | 733 | QRadioButton::indicator { 734 | background: #444; 735 | border: 1px solid #444; 736 | border-radius: 7px; 737 | height: 12px; 738 | width: 12px; 739 | } 740 | 741 | QTreeView::indicator { 742 | background: #444; 743 | } 744 | 745 | QRadioButton::indicator:disabled { 746 | background-color: #282828; 747 | margin: 1px; 748 | border: 1px solid transparent; 749 | } 750 | 751 | QRadioButton::indicator:checked { 752 | image: url(skin:controls/radio_check_dark.svg); 753 | } 754 | 755 | QRadioButton::indicator:checked:disabled { 756 | image: url(skin:controls/radio_check_disabled.svg); 757 | } 758 | 759 | QSlider::groove { 760 | background: #393939; 761 | border: 1px solid #393939; 762 | border-radius: 2px; 763 | font-size: 3pt; 764 | } 765 | 766 | QSlider::groove:horizontal { 767 | height: 0.8em; 768 | } 769 | 770 | QSlider::groove:vertical { 771 | width: 0.8em; 772 | } 773 | 774 | QSlider::groove:disabled, 775 | QSlider::sub-page:disabled { 776 | background: #282828; 777 | border: 1px solid transparent; 778 | border-radius: 2px; 779 | } 780 | 781 | QSlider::sub-page { 782 | background: #486d8d; 783 | border: 1px solid #486d8d; 784 | border-radius: 2px; 785 | } 786 | 787 | QSlider::handle { 788 | background: #777; 789 | border: 1px solid #222; 790 | border-radius: 3px; 791 | font-size: 4pt; 792 | } 793 | 794 | QSlider::handle:horizontal { 795 | margin: -5px -1px; 796 | width: 4.5em; 797 | } 798 | 799 | QSlider::handle:vertical { 800 | margin: -1px -5px; 801 | height: 4.5em; 802 | } 803 | 804 | QSlider::handle:focus { 805 | background-color: #6d96ba; 806 | border-color: #226; 807 | } 808 | 809 | QSlider::handle:hover { 810 | background-color: #999; 811 | } 812 | 813 | QSlider::handle:pressed { 814 | background-color: #bbb; 815 | border-color: #222; 816 | } 817 | 818 | QSlider::handle:disabled { 819 | background-color: #282828; 820 | border: 1px solid #282828; 821 | } 822 | 823 | QCheckBox::indicator:focus, 824 | QTreeView::indicator:focus, 825 | QRadioButton::indicator:focus, 826 | QComboBox:focus { 827 | background-color: #3e4f5e; 828 | } 829 | 830 | QCheckBox::indicator:focus:hover, 831 | QTreeView::indicator:focus:hover, 832 | QRadioButton::indicator:focus:hover, 833 | QComboBox:focus:hover { 834 | background-color: #485d6f; 835 | border-color: #485d6f; 836 | } 837 | 838 | QCheckBox::indicator:hover, 839 | QTreeView::indicator:hover, 840 | QRadioButton::indicator:hover, 841 | QComboBox:hover { 842 | background-color: #595959; 843 | border-color: #555; 844 | } 845 | 846 | QLineEdit:focus, 847 | QSpinBox:focus, 848 | QAbstractSpinBox:focus, 849 | QComboBox:editable:focus { 850 | background-color: #191919; 851 | } 852 | 853 | QLineEdit:focus:hover, 854 | QSpinBox:focus:hover, 855 | QAbstractSpinBox:focus:hover, 856 | QComboBox:editable:focus:hover { 857 | border-color: #485d6f; 858 | } 859 | 860 | QLineEdit:hover, 861 | QSpinBox:hover, 862 | QAbstractSpinBox:hover, 863 | QComboBox:editable:hover { 864 | background-color: #191919; 865 | border-color: #555; 866 | } 867 | 868 | QCheckBox::indicator:focus, 869 | QTreeView::indicator:focus, 870 | QRadioButton::indicator:focus, 871 | QComboBox:focus, 872 | QLineEdit:focus, 873 | QTextEdit:focus, 874 | QPlainTextEdit:focus, 875 | QSpinBox:focus, 876 | QAbstractSpinBox:focus, 877 | QComboBox:editable:focus { 878 | border-color: #3e4f5e; 879 | } 880 | 881 | QFontDialog { 882 | min-width: 32em; 883 | min-height: 24em; 884 | } 885 | 886 | QColorDialog QColorLuminancePicker { 887 | background-color: transparent; 888 | } 889 | 890 | QMessageBox, 891 | QDialogButtonBox { 892 | dialogbuttonbox-buttons-have-icons: 0; 893 | } 894 | 895 | /* Mumble Specifics */ 896 | LogTextBrowser, 897 | #qdsChat { 898 | margin: 0 2px; 899 | min-height: 120px; 900 | min-width: 40px; 901 | border-color: #1c1c1c; 902 | } 903 | 904 | UserView { 905 | margin: 0 2px; 906 | min-height: 120px; 907 | min-width: 40px; 908 | } 909 | 910 | UserView::item { 911 | padding: 0; 912 | padding-top: -1px; 913 | } 914 | 915 | #qdwChat > QTextEdit { 916 | padding: -2px; 917 | margin: 0 2px; 918 | margin-bottom: 2px; 919 | font-size: 10pt; 920 | } 921 | 922 | #qtIconToolbar QComboBox { 923 | font-size: 8pt; 924 | } 925 | 926 | .log-time { 927 | background-color: transparent; 928 | color: #95a5a6; 929 | font-size: 9pt; 930 | } 931 | 932 | .log-server { 933 | background-color: transparent; 934 | color: #F9655D; 935 | font-weight: bold; 936 | } 937 | 938 | .log-channel { 939 | background-color: transparent; 940 | color: #e67e22; 941 | font-weight: bold; 942 | } 943 | 944 | .log-privilege { 945 | background-color: transparent; 946 | color: #c0392b; 947 | font-weight: bold; 948 | } 949 | 950 | .log-target { 951 | background-color: transparent; 952 | color: #27ae60; 953 | font-weight: bold; 954 | } 955 | 956 | .log-source { 957 | background-color: transparent; 958 | color: #27ae60; 959 | font-weight: bold; 960 | } 961 | 962 | QListView#qlwIcons { 963 | padding: 0; 964 | background-color: transparent; 965 | border: 0; 966 | font-size: 12pt; 967 | min-width: 165%; 968 | margin-left: 4px; 969 | margin-top: 12px; 970 | } 971 | 972 | QListView#qlwIcons::item { 973 | margin-bottom: 1px; 974 | padding: 5px 7px; 975 | } 976 | 977 | QListView#qlwIcons::item:hover { 978 | border-color: #333; 979 | background-color: #333; 980 | } 981 | 982 | QListView#qlwIcons::item:selected { 983 | background-color: #444; 984 | border: 1px solid #444; 985 | } 986 | 987 | QListView#qlwIcons::item:focus { 988 | background-color: #3e4f5e; 989 | border: 1px solid #3e4f5e; 990 | } 991 | 992 | QSlider { 993 | margin-left: 30px; 994 | margin-right: 30px; 995 | } 996 | 997 | #qswPages > * > * > QScrollBar { 998 | margin: 0; 999 | } 1000 | 1001 | #qswPages > * > QWidget { 1002 | margin: 2px; 1003 | } 1004 | 1005 | QListView::item QListWidgetItem, 1006 | QListView::item QLineEdit, 1007 | QTreeView::item QComboBox, 1008 | QTreeView::item QLineEdit { 1009 | background: #444; 1010 | margin: 0; 1011 | padding-top: 0; 1012 | padding-bottom: 0; 1013 | padding-left: 4px; 1014 | padding-right: 4px; 1015 | font-size: 9pt; 1016 | } 1017 | 1018 | QListView::item QListWidgetItem:hover, 1019 | QListView::item QLineEdit:hover, 1020 | QTreeView::item QComboBox:hover, 1021 | QTreeView::item QLineEdit:hover { 1022 | background: #444; 1023 | } 1024 | 1025 | AboutDialog > QTextBrowser, 1026 | AboutDialog QTextEdit { 1027 | border: 0; 1028 | } 1029 | 1030 | #qtbToolBar { 1031 | border: 1px solid transparent; 1032 | background: transparent; 1033 | } 1034 | 1035 | #BanEditor { 1036 | min-width: 600px; 1037 | } 1038 | 1039 | #GlobalShortcutTarget { 1040 | min-height: 600px; 1041 | } 1042 | 1043 | ViewCert { 1044 | min-height: 600px; 1045 | } 1046 | 1047 | TalkingUI { 1048 | background-color: #191919; 1049 | } 1050 | 1051 | TalkingUI > * { 1052 | background-color: #191919; 1053 | } 1054 | 1055 | TalkingUI [selected="false"] { 1056 | background-color: #191919; 1057 | } 1058 | 1059 | TalkingUI [selected="false"]:hover { 1060 | background-color: #333; 1061 | } 1062 | 1063 | TalkingUI [selected="true"] { 1064 | background-color: #3e4f5e; 1065 | border: 1px solid #3e4f5e; 1066 | } 1067 | 1068 | TalkingUI > QFrame { 1069 | border: 1px solid; 1070 | border-color: #1c1c1c; 1071 | border-radius: 2px; 1072 | } 1073 | 1074 | /*# sourceMappingURL=Dark.qss.map */ 1075 | -------------------------------------------------------------------------------- /stylesheets/defaut.qss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Skinok/backtrader-pyqt-ui/28c4e065a79dbcc5d3b260b29fab53886a253ef3/stylesheets/defaut.qss -------------------------------------------------------------------------------- /ui/indicatorParameters.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 300 11 | 12 | 13 | 14 | Custom indicator configuration 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | 27 | 0 28 | 40 29 | 30 | 31 | 32 | Customize indicator 33 | 34 | 35 | Qt::AlignCenter 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Qt::Horizontal 46 | 47 | 48 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | buttonBox 58 | accepted() 59 | Dialog 60 | accept() 61 | 62 | 63 | 248 64 | 254 65 | 66 | 67 | 157 68 | 274 69 | 70 | 71 | 72 | 73 | buttonBox 74 | rejected() 75 | Dialog 76 | reject() 77 | 78 | 79 | 316 80 | 260 81 | 82 | 83 | 286 84 | 274 85 | 86 | 87 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /ui/loadDataFiles.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 488 10 | 458 11 | 12 | 13 | 14 | Import one or multiple data files 15 | 16 | 17 | 18 | 19 | 20 | 21 | 0 22 | 0 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | List of data files to import 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 0 39 | 40 40 | 41 | 42 | 43 | Import all data files 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 40 54 | 16777215 55 | 56 | 57 | 58 | X 59 | 60 | 61 | 62 | 63 | 64 | 65 | Qt::Vertical 66 | 67 | 68 | 69 | 20 70 | 40 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Loading a new data file 81 | 82 | 83 | 84 | 85 | 86 | semicolon 87 | 88 | 89 | 90 | 91 | 92 | 93 | Import a new data file 94 | 95 | 96 | 97 | 98 | 99 | 100 | Date time format 101 | 102 | 103 | 104 | 105 | 106 | 107 | Separator 108 | 109 | 110 | 111 | 112 | 113 | 114 | tab 115 | 116 | 117 | true 118 | 119 | 120 | 121 | 122 | 123 | 124 | comma 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | ... 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 40 144 | 145 | 146 | 147 | Load .CSV file 148 | 149 | 150 | 151 | 152 | 153 | 154 | color: red 155 | 156 | 157 | 158 | 159 | 160 | Qt::AlignCenter 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | font-style: italic 174 | 175 | 176 | Files should be ordered from lower (on top) to higher timeframe (at bottom). 177 | 178 | 179 | false 180 | 181 | 182 | Qt::AlignCenter 183 | 184 | 185 | true 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | -------------------------------------------------------------------------------- /ui/loadDataFiles_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'c:\perso\trading\anaconda3\backtrader-ichimoku\ui\loadDataFiles.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.5.0 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(488, 458) 16 | self.gridLayout_2 = QtWidgets.QGridLayout(Form) 17 | self.gridLayout_2.setObjectName("gridLayout_2") 18 | self.dataFilesListWidget = QtWidgets.QListWidget(parent=Form) 19 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred) 20 | sizePolicy.setHorizontalStretch(0) 21 | sizePolicy.setVerticalStretch(0) 22 | sizePolicy.setHeightForWidth(self.dataFilesListWidget.sizePolicy().hasHeightForWidth()) 23 | self.dataFilesListWidget.setSizePolicy(sizePolicy) 24 | self.dataFilesListWidget.setObjectName("dataFilesListWidget") 25 | self.gridLayout_2.addWidget(self.dataFilesListWidget, 3, 0, 1, 1) 26 | self.label_4 = QtWidgets.QLabel(parent=Form) 27 | self.label_4.setObjectName("label_4") 28 | self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) 29 | self.importPB = QtWidgets.QPushButton(parent=Form) 30 | self.importPB.setMinimumSize(QtCore.QSize(0, 40)) 31 | self.importPB.setObjectName("importPB") 32 | self.gridLayout_2.addWidget(self.importPB, 5, 0, 1, 2) 33 | self.verticalLayout = QtWidgets.QVBoxLayout() 34 | self.verticalLayout.setObjectName("verticalLayout") 35 | self.deleteDataFilePB = QtWidgets.QPushButton(parent=Form) 36 | self.deleteDataFilePB.setMaximumSize(QtCore.QSize(40, 16777215)) 37 | self.deleteDataFilePB.setObjectName("deleteDataFilePB") 38 | self.verticalLayout.addWidget(self.deleteDataFilePB) 39 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) 40 | self.verticalLayout.addItem(spacerItem) 41 | self.gridLayout_2.addLayout(self.verticalLayout, 3, 1, 1, 1) 42 | self.groupBox = QtWidgets.QGroupBox(parent=Form) 43 | self.groupBox.setObjectName("groupBox") 44 | self.gridLayout = QtWidgets.QGridLayout(self.groupBox) 45 | self.gridLayout.setObjectName("gridLayout") 46 | self.semicolonRB = QtWidgets.QRadioButton(parent=self.groupBox) 47 | self.semicolonRB.setObjectName("semicolonRB") 48 | self.gridLayout.addWidget(self.semicolonRB, 3, 3, 1, 1) 49 | self.label = QtWidgets.QLabel(parent=self.groupBox) 50 | self.label.setObjectName("label") 51 | self.gridLayout.addWidget(self.label, 0, 0, 1, 1) 52 | self.label_3 = QtWidgets.QLabel(parent=self.groupBox) 53 | self.label_3.setObjectName("label_3") 54 | self.gridLayout.addWidget(self.label_3, 1, 0, 1, 1) 55 | self.label_2 = QtWidgets.QLabel(parent=self.groupBox) 56 | self.label_2.setObjectName("label_2") 57 | self.gridLayout.addWidget(self.label_2, 3, 0, 1, 1) 58 | self.tabRB = QtWidgets.QRadioButton(parent=self.groupBox) 59 | self.tabRB.setChecked(True) 60 | self.tabRB.setObjectName("tabRB") 61 | self.gridLayout.addWidget(self.tabRB, 3, 1, 1, 1) 62 | self.commaRB = QtWidgets.QRadioButton(parent=self.groupBox) 63 | self.commaRB.setObjectName("commaRB") 64 | self.gridLayout.addWidget(self.commaRB, 3, 2, 1, 1) 65 | self.filePathLE = QtWidgets.QLineEdit(parent=self.groupBox) 66 | self.filePathLE.setObjectName("filePathLE") 67 | self.gridLayout.addWidget(self.filePathLE, 0, 1, 1, 3) 68 | self.openFilePB = QtWidgets.QToolButton(parent=self.groupBox) 69 | self.openFilePB.setObjectName("openFilePB") 70 | self.gridLayout.addWidget(self.openFilePB, 0, 4, 1, 1) 71 | self.loadFilePB = QtWidgets.QPushButton(parent=self.groupBox) 72 | self.loadFilePB.setMinimumSize(QtCore.QSize(0, 40)) 73 | self.loadFilePB.setObjectName("loadFilePB") 74 | self.gridLayout.addWidget(self.loadFilePB, 4, 1, 1, 4) 75 | self.errorLabel = QtWidgets.QLabel(parent=self.groupBox) 76 | self.errorLabel.setStyleSheet("color: red") 77 | self.errorLabel.setText("") 78 | self.errorLabel.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 79 | self.errorLabel.setObjectName("errorLabel") 80 | self.gridLayout.addWidget(self.errorLabel, 5, 0, 1, 5) 81 | self.datetimeFormatLE = QtWidgets.QLineEdit(parent=self.groupBox) 82 | self.datetimeFormatLE.setObjectName("datetimeFormatLE") 83 | self.gridLayout.addWidget(self.datetimeFormatLE, 1, 1, 1, 3) 84 | self.gridLayout_2.addWidget(self.groupBox, 0, 0, 1, 2) 85 | self.label_5 = QtWidgets.QLabel(parent=Form) 86 | self.label_5.setStyleSheet("font-style: italic") 87 | self.label_5.setScaledContents(False) 88 | self.label_5.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 89 | self.label_5.setWordWrap(True) 90 | self.label_5.setObjectName("label_5") 91 | self.gridLayout_2.addWidget(self.label_5, 4, 0, 1, 1) 92 | 93 | self.retranslateUi(Form) 94 | QtCore.QMetaObject.connectSlotsByName(Form) 95 | 96 | def retranslateUi(self, Form): 97 | _translate = QtCore.QCoreApplication.translate 98 | Form.setWindowTitle(_translate("Form", "Import one or multiple data files")) 99 | self.label_4.setText(_translate("Form", "List of data files to import")) 100 | self.importPB.setText(_translate("Form", "Import all data files")) 101 | self.deleteDataFilePB.setText(_translate("Form", "X")) 102 | self.groupBox.setTitle(_translate("Form", "Loading a new data file")) 103 | self.semicolonRB.setText(_translate("Form", "semicolon")) 104 | self.label.setText(_translate("Form", "Import a new data file")) 105 | self.label_3.setText(_translate("Form", "Date time format")) 106 | self.label_2.setText(_translate("Form", "Separator")) 107 | self.tabRB.setText(_translate("Form", "tab")) 108 | self.commaRB.setText(_translate("Form", "comma")) 109 | self.openFilePB.setText(_translate("Form", "...")) 110 | self.loadFilePB.setText(_translate("Form", "Load .CSV file")) 111 | self.label_5.setText(_translate("Form", "Files should be ordered from lower (on top) to higher timeframe (at bottom).")) 112 | -------------------------------------------------------------------------------- /ui/strategyResults.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | StrategyResults 4 | 5 | 6 | 7 | 0 8 | 0 9 | 989 10 | 200 11 | 12 | 13 | 14 | 15 | 0 16 | 200 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | Trades 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | Wallet 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 0 55 | 0 56 | 57 | 58 | 59 | 60 | 16 61 | 62 | 63 | 64 | Results 65 | 66 | 67 | Qt::AlignCenter 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 100 76 | 0 77 | 78 | 79 | 80 | 81 | 210 82 | 16777215 83 | 84 | 85 | 86 | 87 | true 88 | 89 | 90 | 91 | border:0 92 | 93 | 94 | true 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /ui/strategyResults_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'c:\perso\trading\anaconda3\backtrader-ichimoku\ui\strategyResults.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.5.0 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_StrategyResults(object): 13 | def setupUi(self, StrategyResults): 14 | StrategyResults.setObjectName("StrategyResults") 15 | StrategyResults.resize(989, 200) 16 | StrategyResults.setMinimumSize(QtCore.QSize(0, 200)) 17 | self.gridLayout_4 = QtWidgets.QGridLayout(StrategyResults) 18 | self.gridLayout_4.setObjectName("gridLayout_4") 19 | self.ResultsTabWidget = QtWidgets.QTabWidget(parent=StrategyResults) 20 | self.ResultsTabWidget.setObjectName("ResultsTabWidget") 21 | self.tradeTab = QtWidgets.QWidget() 22 | self.tradeTab.setObjectName("tradeTab") 23 | self.gridLayout_3 = QtWidgets.QGridLayout(self.tradeTab) 24 | self.gridLayout_3.setObjectName("gridLayout_3") 25 | self.horizontalLayout = QtWidgets.QHBoxLayout() 26 | self.horizontalLayout.setObjectName("horizontalLayout") 27 | self.gridLayout_3.addLayout(self.horizontalLayout, 0, 0, 1, 1) 28 | self.ResultsTabWidget.addTab(self.tradeTab, "") 29 | self.walletTab = QtWidgets.QWidget() 30 | self.walletTab.setObjectName("walletTab") 31 | self.gridLayout_5 = QtWidgets.QGridLayout(self.walletTab) 32 | self.gridLayout_5.setObjectName("gridLayout_5") 33 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 34 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 35 | self.gridLayout_5.addLayout(self.horizontalLayout_2, 0, 0, 1, 1) 36 | self.ResultsTabWidget.addTab(self.walletTab, "") 37 | self.gridLayout_4.addWidget(self.ResultsTabWidget, 0, 1, 3, 1) 38 | self.label_4 = QtWidgets.QLabel(parent=StrategyResults) 39 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 40 | sizePolicy.setHorizontalStretch(0) 41 | sizePolicy.setVerticalStretch(0) 42 | sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) 43 | self.label_4.setSizePolicy(sizePolicy) 44 | font = QtGui.QFont() 45 | font.setPointSize(16) 46 | self.label_4.setFont(font) 47 | self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 48 | self.label_4.setObjectName("label_4") 49 | self.gridLayout_4.addWidget(self.label_4, 0, 0, 1, 1) 50 | self.summaryTableWidget = QtWidgets.QTableWidget(parent=StrategyResults) 51 | self.summaryTableWidget.setMinimumSize(QtCore.QSize(100, 0)) 52 | self.summaryTableWidget.setMaximumSize(QtCore.QSize(210, 16777215)) 53 | font = QtGui.QFont() 54 | font.setKerning(True) 55 | self.summaryTableWidget.setFont(font) 56 | self.summaryTableWidget.setStyleSheet("border:0") 57 | self.summaryTableWidget.setShowGrid(True) 58 | self.summaryTableWidget.setObjectName("summaryTableWidget") 59 | self.summaryTableWidget.setColumnCount(0) 60 | self.summaryTableWidget.setRowCount(0) 61 | self.gridLayout_4.addWidget(self.summaryTableWidget, 1, 0, 1, 1) 62 | 63 | self.retranslateUi(StrategyResults) 64 | self.ResultsTabWidget.setCurrentIndex(0) 65 | QtCore.QMetaObject.connectSlotsByName(StrategyResults) 66 | 67 | def retranslateUi(self, StrategyResults): 68 | _translate = QtCore.QCoreApplication.translate 69 | StrategyResults.setWindowTitle(_translate("StrategyResults", "Form")) 70 | self.ResultsTabWidget.setTabText(self.ResultsTabWidget.indexOf(self.tradeTab), _translate("StrategyResults", "Trades")) 71 | self.ResultsTabWidget.setTabText(self.ResultsTabWidget.indexOf(self.walletTab), _translate("StrategyResults", "Wallet")) 72 | self.label_4.setText(_translate("StrategyResults", "Results")) 73 | -------------------------------------------------------------------------------- /ui/strategyTester.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 270 10 | 694 11 | 12 | 13 | 14 | 15 | 270 16 | 0 17 | 18 | 19 | 20 | Form 21 | 22 | 23 | 24 | 25 | 26 | 27 | 0 28 | 0 29 | 30 | 31 | 32 | 33 | 12 34 | 50 35 | false 36 | false 37 | false 38 | 39 | 40 | 41 | Data 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | Import data 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 0 61 | 0 62 | 63 | 64 | 65 | 66 | 12 67 | 50 68 | false 69 | 70 | 71 | 72 | Strategy 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 0 83 | 0 84 | 85 | 86 | 87 | Ai 88 | TF 89 | 90 | 91 | true 92 | 93 | 94 | true 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 0 103 | 0 104 | 105 | 106 | 107 | Ai 108 | SB3 109 | 110 | 111 | true 112 | 113 | 114 | true 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 0 123 | 0 124 | 125 | 126 | 127 | Ai 128 | Torch 129 | 130 | 131 | true 132 | 133 | 134 | true 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 0 143 | 0 144 | 145 | 146 | 147 | Algorithm 148 | 149 | 150 | true 151 | 152 | 153 | true 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 0 164 | 0 165 | 166 | 167 | 168 | 169 | 0 170 | 0 171 | 172 | 173 | 174 | 1 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | Custom strategy 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | AI Model 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 0 211 | 0 212 | 213 | 214 | 215 | 216 | 12 217 | 50 218 | false 219 | 220 | 221 | 222 | Parameters 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 9 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | Starting cash 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 0 254 | 0 255 | 256 | 257 | 258 | 259 | 0 260 | 50 261 | 262 | 263 | 264 | 265 | 16777215 266 | 250 267 | 268 | 269 | 270 | true 271 | 272 | 273 | 274 | 275 | 0 276 | 0 277 | 248 278 | 69 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | Qt::Vertical 295 | 296 | 297 | 298 | 20 299 | 40 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | true 310 | 311 | 312 | 313 | 0 314 | 0 315 | 316 | 317 | 318 | 319 | 0 320 | 50 321 | 322 | 323 | 324 | Run 325 | 326 | 327 | 328 | 329 | 330 | 331 | 0 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 0 349 | 0 350 | 351 | 352 | 353 | 354 | 16 355 | 356 | 357 | 358 | BackTest 359 | 360 | 361 | Qt::AlignCenter 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | -------------------------------------------------------------------------------- /ui/strategyTester_ui.py: -------------------------------------------------------------------------------- 1 | # Form implementation generated from reading ui file 'c:\perso\trading\anaconda3\backtrader-ichimoku\ui\strategyTester.ui' 2 | # 3 | # Created by: PyQt6 UI code generator 6.5.0 4 | # 5 | # WARNING: Any manual changes made to this file will be lost when pyuic6 is 6 | # run again. Do not edit this file unless you know what you are doing. 7 | 8 | 9 | from PyQt6 import QtCore, QtGui, QtWidgets 10 | 11 | 12 | class Ui_Form(object): 13 | def setupUi(self, Form): 14 | Form.setObjectName("Form") 15 | Form.resize(270, 694) 16 | Form.setMinimumSize(QtCore.QSize(270, 0)) 17 | self.gridLayout = QtWidgets.QGridLayout(Form) 18 | self.gridLayout.setObjectName("gridLayout") 19 | self.label_7 = QtWidgets.QLabel(parent=Form) 20 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 21 | sizePolicy.setHorizontalStretch(0) 22 | sizePolicy.setVerticalStretch(0) 23 | sizePolicy.setHeightForWidth(self.label_7.sizePolicy().hasHeightForWidth()) 24 | self.label_7.setSizePolicy(sizePolicy) 25 | font = QtGui.QFont() 26 | font.setPointSize(12) 27 | font.setBold(False) 28 | font.setUnderline(False) 29 | font.setWeight(50) 30 | font.setStrikeOut(False) 31 | self.label_7.setFont(font) 32 | self.label_7.setObjectName("label_7") 33 | self.gridLayout.addWidget(self.label_7, 1, 0, 1, 1) 34 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 35 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 36 | self.importDataBtn = QtWidgets.QPushButton(parent=Form) 37 | self.importDataBtn.setObjectName("importDataBtn") 38 | self.horizontalLayout_2.addWidget(self.importDataBtn) 39 | self.gridLayout.addLayout(self.horizontalLayout_2, 2, 0, 1, 2) 40 | self.label_6 = QtWidgets.QLabel(parent=Form) 41 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 42 | sizePolicy.setHorizontalStretch(0) 43 | sizePolicy.setVerticalStretch(0) 44 | sizePolicy.setHeightForWidth(self.label_6.sizePolicy().hasHeightForWidth()) 45 | self.label_6.setSizePolicy(sizePolicy) 46 | font = QtGui.QFont() 47 | font.setPointSize(12) 48 | font.setBold(False) 49 | font.setWeight(50) 50 | self.label_6.setFont(font) 51 | self.label_6.setObjectName("label_6") 52 | self.gridLayout.addWidget(self.label_6, 3, 0, 1, 1) 53 | self.horizontalLayout = QtWidgets.QHBoxLayout() 54 | self.horizontalLayout.setObjectName("horizontalLayout") 55 | self.strategyTypeAITensorFlowBtn = QtWidgets.QPushButton(parent=Form) 56 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) 57 | sizePolicy.setHorizontalStretch(0) 58 | sizePolicy.setVerticalStretch(0) 59 | sizePolicy.setHeightForWidth(self.strategyTypeAITensorFlowBtn.sizePolicy().hasHeightForWidth()) 60 | self.strategyTypeAITensorFlowBtn.setSizePolicy(sizePolicy) 61 | self.strategyTypeAITensorFlowBtn.setCheckable(True) 62 | self.strategyTypeAITensorFlowBtn.setAutoExclusive(True) 63 | self.strategyTypeAITensorFlowBtn.setObjectName("strategyTypeAITensorFlowBtn") 64 | self.horizontalLayout.addWidget(self.strategyTypeAITensorFlowBtn) 65 | self.strategyTypeAiStablebaselinesBtn = QtWidgets.QPushButton(parent=Form) 66 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) 67 | sizePolicy.setHorizontalStretch(0) 68 | sizePolicy.setVerticalStretch(0) 69 | sizePolicy.setHeightForWidth(self.strategyTypeAiStablebaselinesBtn.sizePolicy().hasHeightForWidth()) 70 | self.strategyTypeAiStablebaselinesBtn.setSizePolicy(sizePolicy) 71 | self.strategyTypeAiStablebaselinesBtn.setCheckable(True) 72 | self.strategyTypeAiStablebaselinesBtn.setAutoExclusive(True) 73 | self.strategyTypeAiStablebaselinesBtn.setObjectName("strategyTypeAiStablebaselinesBtn") 74 | self.horizontalLayout.addWidget(self.strategyTypeAiStablebaselinesBtn) 75 | self.strategyTypeAiTorchBtn = QtWidgets.QPushButton(parent=Form) 76 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Fixed) 77 | sizePolicy.setHorizontalStretch(0) 78 | sizePolicy.setVerticalStretch(0) 79 | sizePolicy.setHeightForWidth(self.strategyTypeAiTorchBtn.sizePolicy().hasHeightForWidth()) 80 | self.strategyTypeAiTorchBtn.setSizePolicy(sizePolicy) 81 | self.strategyTypeAiTorchBtn.setCheckable(True) 82 | self.strategyTypeAiTorchBtn.setAutoExclusive(True) 83 | self.strategyTypeAiTorchBtn.setObjectName("strategyTypeAiTorchBtn") 84 | self.horizontalLayout.addWidget(self.strategyTypeAiTorchBtn) 85 | self.strategyTypeAlgoBtn = QtWidgets.QPushButton(parent=Form) 86 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) 87 | sizePolicy.setHorizontalStretch(0) 88 | sizePolicy.setVerticalStretch(0) 89 | sizePolicy.setHeightForWidth(self.strategyTypeAlgoBtn.sizePolicy().hasHeightForWidth()) 90 | self.strategyTypeAlgoBtn.setSizePolicy(sizePolicy) 91 | self.strategyTypeAlgoBtn.setCheckable(True) 92 | self.strategyTypeAlgoBtn.setAutoExclusive(True) 93 | self.strategyTypeAlgoBtn.setObjectName("strategyTypeAlgoBtn") 94 | self.horizontalLayout.addWidget(self.strategyTypeAlgoBtn) 95 | self.gridLayout.addLayout(self.horizontalLayout, 4, 0, 1, 2) 96 | self.strategyTypeDetailsSW = QtWidgets.QStackedWidget(parent=Form) 97 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 98 | sizePolicy.setHorizontalStretch(0) 99 | sizePolicy.setVerticalStretch(0) 100 | sizePolicy.setHeightForWidth(self.strategyTypeDetailsSW.sizePolicy().hasHeightForWidth()) 101 | self.strategyTypeDetailsSW.setSizePolicy(sizePolicy) 102 | self.strategyTypeDetailsSW.setBaseSize(QtCore.QSize(0, 0)) 103 | self.strategyTypeDetailsSW.setObjectName("strategyTypeDetailsSW") 104 | self.page = QtWidgets.QWidget() 105 | self.page.setObjectName("page") 106 | self.gridLayout_4 = QtWidgets.QGridLayout(self.page) 107 | self.gridLayout_4.setObjectName("gridLayout_4") 108 | self.strategyNameCB = QtWidgets.QComboBox(parent=self.page) 109 | self.strategyNameCB.setObjectName("strategyNameCB") 110 | self.gridLayout_4.addWidget(self.strategyNameCB, 1, 0, 1, 1) 111 | self.label_5 = QtWidgets.QLabel(parent=self.page) 112 | self.label_5.setObjectName("label_5") 113 | self.gridLayout_4.addWidget(self.label_5, 0, 0, 1, 1) 114 | self.strategyTypeDetailsSW.addWidget(self.page) 115 | self.page_2 = QtWidgets.QWidget() 116 | self.page_2.setObjectName("page_2") 117 | self.gridLayout_5 = QtWidgets.QGridLayout(self.page_2) 118 | self.gridLayout_5.setObjectName("gridLayout_5") 119 | self.label_2 = QtWidgets.QLabel(parent=self.page_2) 120 | self.label_2.setObjectName("label_2") 121 | self.gridLayout_5.addWidget(self.label_2, 0, 0, 1, 1) 122 | self.AiModelPathLE = QtWidgets.QLineEdit(parent=self.page_2) 123 | self.AiModelPathLE.setObjectName("AiModelPathLE") 124 | self.gridLayout_5.addWidget(self.AiModelPathLE, 1, 0, 1, 1) 125 | self.strategyTypeDetailsSW.addWidget(self.page_2) 126 | self.gridLayout.addWidget(self.strategyTypeDetailsSW, 5, 0, 1, 2) 127 | self.label_3 = QtWidgets.QLabel(parent=Form) 128 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 129 | sizePolicy.setHorizontalStretch(0) 130 | sizePolicy.setVerticalStretch(0) 131 | sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) 132 | self.label_3.setSizePolicy(sizePolicy) 133 | font = QtGui.QFont() 134 | font.setPointSize(12) 135 | font.setBold(False) 136 | font.setWeight(50) 137 | self.label_3.setFont(font) 138 | self.label_3.setObjectName("label_3") 139 | self.gridLayout.addWidget(self.label_3, 6, 0, 1, 1) 140 | self.verticalLayout_3 = QtWidgets.QVBoxLayout() 141 | self.verticalLayout_3.setObjectName("verticalLayout_3") 142 | self.gridLayout_3 = QtWidgets.QGridLayout() 143 | self.gridLayout_3.setSpacing(9) 144 | self.gridLayout_3.setObjectName("gridLayout_3") 145 | self.startingCashLE = QtWidgets.QLineEdit(parent=Form) 146 | self.startingCashLE.setInputMask("") 147 | self.startingCashLE.setObjectName("startingCashLE") 148 | self.gridLayout_3.addWidget(self.startingCashLE, 0, 2, 1, 1) 149 | self.label = QtWidgets.QLabel(parent=Form) 150 | self.label.setObjectName("label") 151 | self.gridLayout_3.addWidget(self.label, 0, 1, 1, 1) 152 | self.verticalLayout_3.addLayout(self.gridLayout_3) 153 | self.parametersScrollArea = QtWidgets.QScrollArea(parent=Form) 154 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) 155 | sizePolicy.setHorizontalStretch(0) 156 | sizePolicy.setVerticalStretch(0) 157 | sizePolicy.setHeightForWidth(self.parametersScrollArea.sizePolicy().hasHeightForWidth()) 158 | self.parametersScrollArea.setSizePolicy(sizePolicy) 159 | self.parametersScrollArea.setMinimumSize(QtCore.QSize(0, 50)) 160 | self.parametersScrollArea.setMaximumSize(QtCore.QSize(16777215, 250)) 161 | self.parametersScrollArea.setWidgetResizable(True) 162 | self.parametersScrollArea.setObjectName("parametersScrollArea") 163 | self.scrollAreaWidgetContents = QtWidgets.QWidget() 164 | self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 248, 69)) 165 | self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents") 166 | self.verticalLayout = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents) 167 | self.verticalLayout.setObjectName("verticalLayout") 168 | self.parametersLayout = QtWidgets.QFormLayout() 169 | self.parametersLayout.setObjectName("parametersLayout") 170 | self.verticalLayout.addLayout(self.parametersLayout) 171 | self.parametersScrollArea.setWidget(self.scrollAreaWidgetContents) 172 | self.verticalLayout_3.addWidget(self.parametersScrollArea) 173 | self.gridLayout.addLayout(self.verticalLayout_3, 7, 0, 1, 2) 174 | spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) 175 | self.gridLayout.addItem(spacerItem, 8, 1, 1, 1) 176 | self.verticalLayout_2 = QtWidgets.QVBoxLayout() 177 | self.verticalLayout_2.setObjectName("verticalLayout_2") 178 | self.runBacktestBtn = QtWidgets.QPushButton(parent=Form) 179 | self.runBacktestBtn.setEnabled(True) 180 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Preferred) 181 | sizePolicy.setHorizontalStretch(0) 182 | sizePolicy.setVerticalStretch(0) 183 | sizePolicy.setHeightForWidth(self.runBacktestBtn.sizePolicy().hasHeightForWidth()) 184 | self.runBacktestBtn.setSizePolicy(sizePolicy) 185 | self.runBacktestBtn.setMinimumSize(QtCore.QSize(0, 50)) 186 | self.runBacktestBtn.setObjectName("runBacktestBtn") 187 | self.verticalLayout_2.addWidget(self.runBacktestBtn) 188 | self.runningStratPB = QtWidgets.QProgressBar(parent=Form) 189 | self.runningStratPB.setProperty("value", 0) 190 | self.runningStratPB.setObjectName("runningStratPB") 191 | self.verticalLayout_2.addWidget(self.runningStratPB) 192 | self.runLabel = QtWidgets.QLabel(parent=Form) 193 | self.runLabel.setText("") 194 | self.runLabel.setObjectName("runLabel") 195 | self.verticalLayout_2.addWidget(self.runLabel) 196 | self.gridLayout.addLayout(self.verticalLayout_2, 9, 0, 1, 2) 197 | self.label_4 = QtWidgets.QLabel(parent=Form) 198 | sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) 199 | sizePolicy.setHorizontalStretch(0) 200 | sizePolicy.setVerticalStretch(0) 201 | sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) 202 | self.label_4.setSizePolicy(sizePolicy) 203 | font = QtGui.QFont() 204 | font.setPointSize(16) 205 | self.label_4.setFont(font) 206 | self.label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) 207 | self.label_4.setObjectName("label_4") 208 | self.gridLayout.addWidget(self.label_4, 0, 0, 1, 2) 209 | 210 | self.retranslateUi(Form) 211 | self.strategyTypeDetailsSW.setCurrentIndex(1) 212 | QtCore.QMetaObject.connectSlotsByName(Form) 213 | 214 | def retranslateUi(self, Form): 215 | _translate = QtCore.QCoreApplication.translate 216 | Form.setWindowTitle(_translate("Form", "Form")) 217 | self.label_7.setText(_translate("Form", "Data")) 218 | self.importDataBtn.setText(_translate("Form", "Import data")) 219 | self.label_6.setText(_translate("Form", "Strategy")) 220 | self.strategyTypeAITensorFlowBtn.setText(_translate("Form", "Ai\n" 221 | "TF")) 222 | self.strategyTypeAiStablebaselinesBtn.setText(_translate("Form", "Ai\n" 223 | " SB3")) 224 | self.strategyTypeAiTorchBtn.setText(_translate("Form", "Ai\n" 225 | "Torch")) 226 | self.strategyTypeAlgoBtn.setText(_translate("Form", "Algorithm")) 227 | self.label_5.setText(_translate("Form", "Custom strategy")) 228 | self.label_2.setText(_translate("Form", "AI Model")) 229 | self.label_3.setText(_translate("Form", "Parameters")) 230 | self.label.setText(_translate("Form", "Starting cash")) 231 | self.runBacktestBtn.setText(_translate("Form", "Run")) 232 | self.label_4.setText(_translate("Form", "BackTest")) 233 | -------------------------------------------------------------------------------- /userConfig.py: -------------------------------------------------------------------------------- 1 | import json 2 | from Singleton import Singleton 3 | 4 | class UserConfig(Singleton): 5 | 6 | def __init__(self): 7 | self.data = {} 8 | pass 9 | 10 | def loadConfigFile(self): 11 | try: 12 | with open("userData.json") as userFile: 13 | self.data = json.load(userFile) 14 | except: 15 | print(" Can't load user config file") 16 | 17 | def saveObject(self, name, obj): 18 | 19 | obj_dict = {} 20 | 21 | # Get all objects attributes and store them in the config file 22 | # https://stackoverflow.com/questions/11637293/iterate-over-object-attributes-in-python 23 | for attributeName in [a for a in dir(obj) if not a.startswith('__') and not callable(getattr(obj, a))]: 24 | 25 | # we should use decorator here instead of a hardcoded 26 | if attributeName != "dataFrame": 27 | obj_dict[attributeName] = getattr(obj, attributeName) 28 | 29 | self.data[name] = obj_dict 30 | 31 | self.saveConfig() 32 | pass 33 | 34 | def saveParameter(self, parameter, value): 35 | self.data[parameter] = value 36 | self.saveConfig() 37 | pass 38 | 39 | def removeParameter(self, parameter): 40 | if parameter in self.data: 41 | del self.data[parameter] 42 | self.saveConfig() 43 | pass 44 | 45 | def saveConfig(self): 46 | try: 47 | with open("userData.json", "w+") as userFile: 48 | json.dump(self.data, userFile, indent = 4) 49 | except: 50 | print(" Can't save user config file") 51 | 52 | 53 | -------------------------------------------------------------------------------- /userData.json: -------------------------------------------------------------------------------- 1 | { 2 | "M5": { 3 | "fileName": "BTC_USDT_USDT-5m-futures-train.csv", 4 | "filePath": "C:/perso/AI/AI_Framework/freqtrade_data/binance/futures/BTC_USDT_USDT-5m-futures-train.csv", 5 | "separator": ",", 6 | "timeFormat": "%Y-%m-%d %H:%M:%S", 7 | "timeFrame": "M5" 8 | } 9 | } -------------------------------------------------------------------------------- /wallet.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8; py-indent-offset:4 -*- 3 | ############################################################################### 4 | # 5 | # Copyright (C) 2021-2025 Skinok 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | ############################################################################### 21 | class Wallet(): 22 | 23 | def __init__(self, startingCash): 24 | 25 | self.reset(startingCash) 26 | 27 | pass 28 | 29 | def reset(self, startingCash): 30 | 31 | self.starting_cash = startingCash # todo: change it by initial cash settings 32 | self.current_value = startingCash # todo: change it by initial cash settings 33 | self.current_cash = startingCash # todo: change it by initial cash settings 34 | self.current_equity = startingCash # todo: change it by initial cash settings 35 | 36 | self.value_list = [] 37 | self.cash_list = [] 38 | self.equity_list = [] 39 | 40 | pass -------------------------------------------------------------------------------- /websockets/binance.py: -------------------------------------------------------------------------------- 1 | 2 | import sys, os 3 | sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../../finplot') 4 | import finplot as fplt 5 | 6 | import json 7 | import pandas as pd 8 | from time import sleep 9 | from threading import Thread 10 | import websocket 11 | 12 | class BinanceFutureWebsocket: 13 | def __init__(self): 14 | self.url = 'wss://fstream.binance.com/stream' 15 | self.symbol = None 16 | self.interval = None 17 | self.ws = None 18 | self.df = None 19 | 20 | def reconnect(self, symbol, interval, df): 21 | '''Connect and subscribe, if not already done so.''' 22 | self.df = df 23 | if symbol.lower() == self.symbol and self.interval == interval: 24 | return 25 | self.symbol = symbol.lower() 26 | self.interval = interval 27 | self.thread_connect = Thread(target=self._thread_connect) 28 | self.thread_connect.daemon = True 29 | self.thread_connect.start() 30 | 31 | def close(self, reset_symbol=True): 32 | if reset_symbol: 33 | self.symbol = None 34 | if self.ws: 35 | self.ws.close() 36 | self.ws = None 37 | 38 | def _thread_connect(self): 39 | self.close(reset_symbol=False) 40 | print('websocket connecting to %s...' % self.url) 41 | self.ws = websocket.WebSocketApp(self.url, on_message=self.on_message, on_error=self.on_error) 42 | self.thread_io = Thread(target=self.ws.run_forever) 43 | self.thread_io.daemon = True 44 | self.thread_io.start() 45 | for _ in range(100): 46 | if self.ws.sock and self.ws.sock.connected: 47 | break 48 | sleep(0.1) 49 | else: 50 | self.close() 51 | raise websocket.WebSocketTimeoutException('websocket connection failed') 52 | self.subscribe(self.symbol, self.interval) 53 | print('websocket connected') 54 | 55 | def subscribe(self, symbol, interval): 56 | try: 57 | data = '{"method":"SUBSCRIBE","params":["%s@kline_%s"],"id":1}' % (symbol, interval) 58 | self.ws.send(data) 59 | except Exception as e: 60 | print('websocket subscribe error:', type(e), e) 61 | raise e 62 | 63 | def on_message(self, *args, **kwargs): 64 | df = self.df 65 | if df is None: 66 | return 67 | msg = json.loads(args[-1]) 68 | if 'stream' not in msg: 69 | return 70 | stream = msg['stream'] 71 | if '@kline_' in stream: 72 | k = msg['data']['k'] 73 | t = k['t'] 74 | t0 = int(df.index[-2].timestamp()) * 1000 75 | t1 = int(df.index[-1].timestamp()) * 1000 76 | t2 = t1 + (t1-t0) 77 | if t < t2: 78 | # update last candle 79 | i = df.index[-1] 80 | df.loc[i, 'Close'] = float(k['c']) 81 | df.loc[i, 'High'] = max(df.loc[i, 'High'], float(k['h'])) 82 | df.loc[i, 'Low'] = min(df.loc[i, 'Low'], float(k['l'])) 83 | df.loc[i, 'Volume'] = float(k['v']) 84 | else: 85 | # create a new candle 86 | data = [t] + [float(k[i]) for i in ['o','c','h','l','v']] 87 | candle = pd.DataFrame([data], columns='Time Open Close High Low Volume'.split()).astype({'Time':'datetime64[ms]'}) 88 | candle.set_index('Time', inplace=True) 89 | self.df = df.append(candle) 90 | 91 | def on_error(self, error, *args, **kwargs): 92 | print('websocket error: %s' % error) --------------------------------------------------------------------------------