├── .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 | [](https://discord.gg/56ERy324)
2 |
3 | # Skinok backtrader UI (PyQt and finplot)
4 |
5 | 
6 |
7 | 
8 |
9 | 
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)
--------------------------------------------------------------------------------