├── .gitignore ├── LICENSE ├── README.md ├── _config.yml ├── doc ├── .gitkeep ├── astibot_architecture.png ├── astibot_architecture.pptx └── astibot_overview.png ├── res ├── .gitkeep ├── AstibotIcon.png ├── AstibotLogo.png ├── AstibotSplash.png ├── chart_blue.png ├── chart_orange.png ├── chart_red.png ├── chart_symbols.png ├── chart_white.png └── chart_yellow.png └── src ├── .gitkeep ├── AppState.py ├── Astibot.py ├── GDAXControler.py ├── GDAXCurrencies.py ├── InputDataHandler.py ├── MarketData.py ├── Notifier.py ├── Settings.py ├── Trader.py ├── TradingBotConfig.py ├── TransactionManager.py ├── UIDonation.py ├── UIGraph.py ├── UIInfo.py ├── UISettings.py └── UIWidgets.py /.gitignore: -------------------------------------------------------------------------------- 1 | src/__pycache__/ 2 | astibot.settings 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Florian Delaunay 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 | # Astibot 2 | **Astibot is a simple, visual and automated trading software for Coinbase Pro cryptocurrencies** 3 | 4 | Astibot is a trading bot that operates on the Coinbase Pro trading platform through a set of API keys. Its trading strategy is basic, but it provides a powerful and interactive simulation tool to backtest your settings. 5 | 6 | Astibot bases its decisions on 2 real-time indicators: 7 | * **a MACD-like indicator:** it provides buy and sell signals based on 2 moving averages: one fast, one slow. These averages can be tuned to be short-term focused (very sensitive, ~5 min chart) or more robust to price noise (less sensitive, ~2h chart). They are not computed in a traditional way, but with signal processing algorithms (recursive low pass filters). 8 | * **a risk indicator:** the purpose of this risk line is to avoid opening a trade too high that could hardly be sold with a profit. The user can set his own risk level thanks to a dedicated cursor. This line evolves automatically to match the average market level (based on the last few hours), but its value is weighted by the risk level the user has set. 9 | 10 | ![Alt text](/doc/astibot_overview.png?raw=true "Astibot overview") 11 | 12 | ## Main features 13 | * Real-time graph update 14 | * On-graph trades display (buy and sell markers) 15 | * Live trading mode (real-time trading) 16 | * Simulation mode (to backtest the settings) 17 | * Customizable MACD-like decision indicator (to detect buy and sell opportunities) 18 | * Supported Coinbase pro trading pairs must be defined in the `src/GDAXCurrencies.py` file within the `get_all_pairs` method. 19 | 20 | ## Advanced features 21 | * Risk line: a customizable, real-time updated limit price limit above which Astibot will never buy 22 | * Stop loss: crypto is automatically sold if price is below a customizable percentage of the buy price 23 | * Sell trigger: a fixed percentage above the buy price to sell, for scalping. After a buy, Astibot places a limit order at this percentage of the buy price. If this parameter is set to zero this feature is disabled and Astibot will rely on its MACD-like indicator to decide when to sell. 24 | * Limit and Market orders management: when Astibot detects a buy (or a sell) opportunity, it first tries to buy (or sell) the asset through a limit order to benefit from the fee reduction (limit orders are less expensive and on the right side of the spread). If the order cannot be filled, Astibot decides to perform a market order (immediate effect, more expensive) or to cancel the buy if the buy opportunity strength has decreased too much. 25 | 26 | 27 | ## How to use Astibot ? 28 | 29 | Astibot can run on any computer capable of runnning Python 3, including Raspberry Pi (very convenient for 24/7 trading). 30 | 31 | #### Install required dependencies 32 | 33 | pip3 install pyqt5 pyqtgraph tzlocal cbpro twilio scipy ipdb 34 | 35 | #### Start-up 36 | 37 | 1. python Astibot.py 38 | 2. At first start-up, enter your Coinbase Pro API keys (view and trade permissions are required) 39 | 40 | ## Results 41 | 42 | Let's talk about the key topic! I have run Astibot serveral weeks on my Raspberry pi. 43 | Here are my conclusions: 44 | * Astibot needs volatility to make profit: a 0.8% - 1% price amplitude on the short term chart is a minimum. These variations are required to detect dips and tops properly with the smoothing indicators, and to cover the buy and sell fees. 45 | * Astibot runs well during sideways periods. If volume and volatility are good, Astibot can outperform the chart. 46 | * Astibot is not very interesting during a bull market. Price dips are harder to find, and because of the risk line, Astibot never buys when price is too high. 47 | * Astibot is not profitable during a bear market: it will detect a lot of dips, buy these dips and it will not be able to close a trade with profit because price will have decreased. 48 | 49 | To sum up, the mose difficult part is to know **when** it is interesting to run Astibot for the next hours or days. 50 | But, there's no rule. Use the Simulation mode and tune the cursors to try :) 51 | 52 | 53 | 54 | ## Development 55 | 56 | I think current Astibot version could be a good starting point to implement more sophisticated strategies. 57 | To understand the general software breakdown, a diagram is worth thousand words. Top modules call services from the modules below. 58 | ![Alt text](/doc/astibot_architecture.png?raw=true "Astibot software architecture") 59 | 60 | ## Known limitations 61 | 62 | * Astibot is designed to prioritize the execution of limit orders over market orders. However limit orders placing, monitoring and replacing on top of the order book in real-time when a buy/sell signal is raised is tricky to implement and I don't think it works perfectly. To avoid problems with this limit order mode feature, I configured Astibot to use market orders only by default (see TradingBotConfig file). 63 | * Astibot only implements the Coinbase Pro API . It would not be that hard to create a "BinanceControler", "BitfinexControler" ... to add multiplatform support. These specific controlers could herit from a more generic and abstract controler seen from higher level modules (polymorphism). 64 | 65 | ## Development and design improvements 66 | 67 | * Some modules are too big and could be split into more micro modules (UIGraph for example) 68 | * Astibot was originally designed to trade fiat-crypto pairs. Recently, I added the support for BTC based pairs but I didn't have time to rename all the variable labelled "fiatXXX" that were orginally are designed to contain data about the fiat currency. So for example, variables fiatAccountBalance and cryptoAccountBalance should have more generic names like account1Balance, account2Balance. 69 | 70 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /doc/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /doc/astibot_architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/doc/astibot_architecture.png -------------------------------------------------------------------------------- /doc/astibot_architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/doc/astibot_architecture.pptx -------------------------------------------------------------------------------- /doc/astibot_overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/doc/astibot_overview.png -------------------------------------------------------------------------------- /res/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /res/AstibotIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/AstibotIcon.png -------------------------------------------------------------------------------- /res/AstibotLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/AstibotLogo.png -------------------------------------------------------------------------------- /res/AstibotSplash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/AstibotSplash.png -------------------------------------------------------------------------------- /res/chart_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_blue.png -------------------------------------------------------------------------------- /res/chart_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_orange.png -------------------------------------------------------------------------------- /res/chart_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_red.png -------------------------------------------------------------------------------- /res/chart_symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_symbols.png -------------------------------------------------------------------------------- /res/chart_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_white.png -------------------------------------------------------------------------------- /res/chart_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LasticotSoftware/Astibot/28f8cb790d410de3c79adfbd8b2be1db82fbbca4/res/chart_yellow.png -------------------------------------------------------------------------------- /src/.gitkeep: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/AppState.py: -------------------------------------------------------------------------------- 1 | from MarketData import MarketData 2 | from GDAXControler import GDAXControler 3 | from TransactionManager import TransactionManager 4 | from Trader import Trader 5 | from UIGraph import UIGraph 6 | import TradingBotConfig as theConfig 7 | 8 | 9 | class AppState(object): 10 | 11 | currentAppState = "STATE_INITIALIZATION" 12 | nextAppState = "STATE_INITIALIZATION" 13 | 14 | def __init__(self, UIGraph, Trader, GDAXControler, InputDataHandler, MarketData, Settings): 15 | self.theUIGraph = UIGraph 16 | self.theTrader = Trader 17 | self.theGDAXControler = GDAXControler 18 | self.theInputDataHandler = InputDataHandler 19 | self.theMarketData = MarketData 20 | # Application settings data instance 21 | self.theSettings = Settings 22 | 23 | self.previousModeWasRealMarket = theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET 24 | 25 | # Entry actions for Initialization state 26 | self.PerformInitializationStateEntryActions() 27 | 28 | self.generalPurposeDecreasingCounter = 0 29 | 30 | def APP_Execute(self): 31 | self.nextAppState = self.currentAppState 32 | #print(self.currentAppState) 33 | 34 | if (self.currentAppState == 'STATE_INITIALIZATION'): 35 | self.ManageInitializationState() 36 | elif (self.currentAppState == 'STATE_IDLE'): 37 | self.ManageIdleState() 38 | elif (self.currentAppState == 'STATE_SIMULATION_LOADING'): 39 | self.ManageSimulationLoadingState() 40 | elif (self.currentAppState == 'STATE_SIMULATION'): 41 | self.ManageSimulationState() 42 | elif (self.currentAppState == 'STATE_SIMULATION_STOPPING'): 43 | self.ManageSimulationStoppingState() 44 | elif (self.currentAppState == 'STATE_TRADING_LOADING'): 45 | self.ManageTradingLoadingState() 46 | elif (self.currentAppState == 'STATE_TRADING'): 47 | self.ManageTradingState() 48 | elif (self.currentAppState == 'STATE_FAILURE'): 49 | self.ManageFailureState() 50 | else: 51 | self.ManageIdleState() # Error case 52 | 53 | # If transition requested 54 | if (self.nextAppState != self.currentAppState): 55 | self.currentAppState = self.nextAppState 56 | self.theUIGraph.UIGR_SetCurrentAppState(self.currentAppState) 57 | 58 | if (self.generalPurposeDecreasingCounter > 0): 59 | self.generalPurposeDecreasingCounter = self.generalPurposeDecreasingCounter - 1 60 | #print(self.generalPurposeDecreasingCounter) 61 | 62 | def PerformInitializationStateEntryActions(self): 63 | self.nextAppState = "STATE_INITIALIZATION" 64 | self.theUIGraph.UIGR_updateCurrentState("Initializing...", False, True) 65 | 66 | # Init HMI functional features 67 | self.theUIGraph.UIGR_SetStartButtonEnabled(False) 68 | self.theUIGraph.UIGR_SetStartButtonAspect("START_DISABLED") 69 | self.theUIGraph.UIGR_SetPauseButtonEnabled(False) 70 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE_DISABLED") 71 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(False) 72 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(False) 73 | 74 | self.theGDAXControler.GDAX_InitializeGDAXConnection() 75 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(False) 76 | self.theInputDataHandler.INDH_PrepareHistoricDataSinceGivenHours(True, theConfig.NB_HISTORIC_DATA_HOURS_TO_PRELOAD_FOR_TRADING) 77 | 78 | 79 | def ManageInitializationState(self): 80 | self.theUIGraph.UIGR_updateCurrentState("Initializing...", False, True) 81 | 82 | if (self.theGDAXControler.GDAX_IsConnectedAndOperational() == "True"): 83 | if (self.theInputDataHandler.INDH_GetPreloadHistoricDataStatus() == "Ended"): 84 | # Initialization to Idle state transition actions 85 | self.nextAppState = 'STATE_IDLE' 86 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 87 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 88 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 89 | self.theTrader.TRAD_InitiateNewTradingSession(False) # Force accounts balances display refresh 90 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(True) 91 | print("APPL - Init: go to idle") 92 | elif (self.theGDAXControler.GDAX_IsConnectedAndOperational() == "False"): 93 | self.nextAppState = 'STATE_FAILURE' 94 | self.theUIGraph.UIGR_SetStartButtonEnabled(False) 95 | self.theUIGraph.UIGR_SetStartButtonAspect("START_DISABLED") 96 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 97 | print("APPL: Init: go to failure") 98 | else: 99 | # Ongoing 100 | print("APPL - Initialization State - Ongoing init") 101 | pass 102 | 103 | def ManageIdleState(self): 104 | self.theUIGraph.UIGR_updateCurrentState("Idle", False, False) 105 | 106 | self.CheckImpactingSettingsChanges() 107 | 108 | # User actions analysis =================================================================== 109 | # If user clicks on "Start" 110 | if (self.theUIGraph.UIGR_IsStartButtonClicked() == True): 111 | if (self.theUIGraph.UIGR_GetSelectedRadioMode() == "Simulation"): 112 | print("APPL - ManageIdleState - StartButtonClicked, going to Simulaltion") 113 | # Transition to STATE_SIMULATION_LOADING 114 | self.theInputDataHandler.INDH_PrepareHistoricDataSinceGivenHours(False, float(self.theSettings.SETT_GetSettings()["simulationTimeRange"]) + 3.0) 115 | self.theUIGraph.UIGR_SetStartButtonEnabled(False) 116 | self.theUIGraph.UIGR_SetStartButtonAspect("LOADING") 117 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(False) 118 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(False) 119 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(False) 120 | self.nextAppState = 'STATE_SIMULATION_LOADING' 121 | else: 122 | # If Fiat balance is OK 123 | if (self.theGDAXControler.GDAX_GetFiatAccountBalance() > theConfig.CONFIG_MIN_INITIAL_FIAT_BALANCE_TO_TRADE): 124 | print("APPL - ManageIdleState - StartButtonClicked, fiat balance OK, going to Trading") 125 | # Transition to STATE_TRADING_LOADING 126 | self.theInputDataHandler.INDH_PrepareHistoricDataSinceGivenHours(True, theConfig.NB_HISTORIC_DATA_HOURS_TO_PRELOAD_FOR_TRADING) 127 | self.theUIGraph.UIGR_SetStartButtonEnabled(False) 128 | self.theUIGraph.UIGR_SetStartButtonAspect("LOADING") 129 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(False) 130 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(False) 131 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(False) 132 | self.nextAppState = 'STATE_TRADING_LOADING' 133 | else: 134 | # Fiat balance too small, stay in Idle 135 | self.theUIGraph.UIGR_updateInfoText("Your balance is too low to start Trading. A minimum of %s %s is required to ensure trades comply with Coinbase Pro minimum orders sizes." % (theConfig.CONFIG_MIN_INITIAL_FIAT_BALANCE_TO_TRADE, self.theSettings.SETT_GetSettings()["strFiatType"]), True) 136 | 137 | def ManageSimulationLoadingState(self): 138 | self.theUIGraph.UIGR_updateCurrentState("Downloading and analyzing historic data...", False, True) 139 | 140 | if (self.theInputDataHandler.INDH_GetPreloadHistoricDataStatus() == "Ended"): 141 | # Transition to STATE_SIMULATION 142 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 143 | self.theUIGraph.UIGR_SetStartButtonAspect("STOP") 144 | # 5 additional hours are needed aproximately to let indicators to settle 145 | if (self.theInputDataHandler.INDH_PerformSimulation(float(self.theSettings.SETT_GetSettings()["simulationTimeRange"]) + 5) == "Ongoing"): 146 | self.nextAppState = 'STATE_SIMULATION' 147 | # Set new state in anticipation to UIGR so that it will prepare the right captions 148 | self.theUIGraph.UIGR_SetCurrentAppState(self.nextAppState) 149 | self.theTrader.TRAD_InitiateNewTradingSession(True) 150 | self.theUIGraph.UIGR_SetPauseButtonEnabled(True) 151 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE") 152 | else: 153 | # Error 154 | print("APPL - Simulation loading state > error launching simulation, going to Idle state") 155 | self.nextAppState = 'STATE_IDLE' 156 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(True) 157 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 158 | else: 159 | # Wait 160 | pass 161 | 162 | def ManageSimulationState(self): 163 | self.theUIGraph.UIGR_updateCurrentState("Ongoing simulation", False, False) 164 | 165 | # If user clicked on PAUSE button 166 | if (self.theUIGraph.UIGR_IsPauseButtonClicked() == True): 167 | self.theInputDataHandler.INDH_PauseResumeSimulation() 168 | 169 | # If user clicked on STOP button 170 | if (self.theUIGraph.UIGR_IsStartButtonClicked() == True): 171 | print("APPL - Simulation state > go to Simulation Stopping because of StartButton clicked") 172 | self.theInputDataHandler.INDH_StopSimulation() 173 | # Request graph refresh timer stop from same thread as it was launched (Main / UI Thread) 174 | self.theUIGraph.UIGR_StopContinuousGraphRefresh() 175 | # Short pass in Simulation stopping state is necessary in order to let simulation thread to end by itself 176 | # so that if user clicks quickly on Start, it will be ready to be started again 177 | self.nextAppState = 'STATE_SIMULATION_STOPPING' 178 | self.generalPurposeDecreasingCounter = 3 179 | self.theUIGraph.UIGR_SetStartButtonEnabled(False) 180 | self.theUIGraph.UIGR_SetStartButtonAspect("START_DISABLED") 181 | self.theUIGraph.UIGR_SetPauseButtonEnabled(False) 182 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE_DISABLED") 183 | 184 | # If simulation is ended by itself (end of data buffer) 185 | if (self.theInputDataHandler.INDH_GetOperationalStatus() == "Ended"): 186 | print("APPL - Simulation state > go to Idle because of buffer ended") 187 | # Request graph refresh timer stop from same thread as it was launched (Main / UI Thread) 188 | self.theUIGraph.UIGR_StopContinuousGraphRefresh() 189 | self.nextAppState = 'STATE_IDLE' 190 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 191 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 192 | self.theUIGraph.UIGR_SetPauseButtonEnabled(False) 193 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE_DISABLED") 194 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(True) 195 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 196 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(True) 197 | # Set new state in anticipation to UIGR so that it will prepare the right captions 198 | self.theUIGraph.UIGR_SetCurrentAppState(self.nextAppState) 199 | self.theTrader.TRAD_TerminateTradingSession() 200 | 201 | def ManageSimulationStoppingState(self): 202 | print("APPL - Simulation Stopping state") 203 | if (self.generalPurposeDecreasingCounter == 0): 204 | self.nextAppState = 'STATE_IDLE' 205 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 206 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 207 | self.theUIGraph.UIGR_SetPauseButtonEnabled(False) 208 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE_DISABLED") 209 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(True) 210 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 211 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(True) 212 | # Set new state in anticipation to UIGR so that it will prepare the right captions 213 | self.theUIGraph.UIGR_SetCurrentAppState(self.nextAppState) 214 | self.theTrader.TRAD_TerminateTradingSession() 215 | 216 | 217 | def ManageTradingLoadingState(self): 218 | self.theUIGraph.UIGR_updateCurrentState("Downloading and analyzing historic data to prepare trading indicators...", False, True) 219 | 220 | if (self.theInputDataHandler.INDH_GetPreloadHistoricDataStatus() == "Ended"): 221 | print("APPL - ManageTradingLoadingState - PreloadHistoricDataStatus is ended - Going to Trading state") 222 | # Transition to STATE_TRADING 223 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 224 | self.theUIGraph.UIGR_SetStartButtonAspect("STOP") 225 | if (self.theInputDataHandler.INDH_PerformLiveTradingOperation(theConfig.NB_HISTORIC_DATA_HOURS_TO_PRELOAD_FOR_TRADING) == "Ongoing"): 226 | self.nextAppState = 'STATE_TRADING' 227 | # Set new state in anticipation to UIGR so that it will prepare the right captions 228 | self.theUIGraph.UIGR_SetCurrentAppState(self.nextAppState) 229 | self.theTrader.TRAD_InitiateNewTradingSession(True) 230 | # DEBUG 231 | #♠self.generalPurposeDecreasingCounter = 600 232 | else: 233 | # Error 234 | print("APPL - Trading loading state > error launching trading, going to Idle state") 235 | self.nextAppState = 'STATE_IDLE' 236 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(True) 237 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 238 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 239 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(True) 240 | else: 241 | # Wait 242 | print("APPL - ManageTradingLoadingState - Loading ongoing") 243 | pass 244 | 245 | def ManageTradingState(self): 246 | self.theUIGraph.UIGR_updateCurrentState("Live trading", True, True) 247 | 248 | # If user clicked on STOP button 249 | if (self.theUIGraph.UIGR_IsStartButtonClicked() == True): 250 | print("APPL - Trading state > go to Idle because of StartButton clicked") 251 | self.theInputDataHandler.INDH_StopLiveTrading() 252 | # Request graph refresh timer stop from same thread as it was launched (Main / UI Thread) 253 | self.theUIGraph.UIGR_StopContinuousGraphRefresh() 254 | self.nextAppState = 'STATE_IDLE' 255 | self.theUIGraph.UIGR_SetStartButtonEnabled(True) 256 | self.theUIGraph.UIGR_SetStartButtonAspect("START") 257 | self.theUIGraph.UIGR_SetPauseButtonEnabled(False) 258 | self.theUIGraph.UIGR_SetRadioButtonsEnabled(True) 259 | self.theUIGraph.UIGR_SetSettingsButtonsEnabled(True) 260 | self.theUIGraph.UIGR_SetDonationButtonsEnabled(True) 261 | # Set new state in anticipation to UIGR so that it will prepare the right captions 262 | self.theUIGraph.UIGR_SetCurrentAppState(self.nextAppState) 263 | self.theTrader.TRAD_TerminateTradingSession() 264 | 265 | def ManageFailureState(self): 266 | self.theUIGraph.UIGR_updateCurrentState("", False, False) # Don't display error to the user 267 | 268 | self.CheckImpactingSettingsChanges() 269 | 270 | def isFailureStateRequired(self): 271 | return False 272 | 273 | def CheckImpactingSettingsChanges(self): 274 | # Settings change analysis =================================================================== 275 | bTradingPairHasChanged = False 276 | bAPIDataHasChanged = False 277 | 278 | # If trading pair or API IDs have changed, Notify stakeholders and perform Initialization state entry actions 279 | # Heavy code because SETT_hasXXXChanged APIs are "read-once" 280 | if (self.theSettings.SETT_hasTradingPairChanged() == True): 281 | bTradingPairHasChanged = True 282 | 283 | if (self.theSettings.SETT_hasAPIDataChanged() == True): 284 | bAPIDataHasChanged = True 285 | 286 | if ((bTradingPairHasChanged == True) or (bAPIDataHasChanged == True)): 287 | if (bTradingPairHasChanged == True): 288 | print("APPL - Trading pair has changed") 289 | self.theUIGraph.UIGR_NotifyThatTradingPairHasChanged() 290 | self.theGDAXControler.GDAX_NotifyThatTradingPairHasChanged() 291 | 292 | if (bAPIDataHasChanged == True): 293 | print("APPL - API data has changed") 294 | pass # Nothing specific to do as GDAX will be asked to perform a new connection 295 | 296 | # Entry actions for Initialization state 297 | self.PerformInitializationStateEntryActions() 298 | 299 | 300 | # Mode change analysis (Live trading or simulation) 301 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET != self.previousModeWasRealMarket): 302 | self.previousModeWasRealMarket = theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET 303 | # Accounts balances display differs depending on the mode (simulation or trading), we need to refresh it 304 | self.theTrader.TRAD_InitiateNewTradingSession(False) 305 | -------------------------------------------------------------------------------- /src/Astibot.py: -------------------------------------------------------------------------------- 1 | #!. 2 | 3 | 4 | import threading 5 | import time 6 | import sys 7 | 8 | import ipdb # To be able to see error stack messages occuring in the Qt MainLoop 9 | 10 | import os 11 | 12 | from Settings import Settings 13 | from MarketData import MarketData 14 | from InputDataHandler import InputDataHandler 15 | from GDAXControler import GDAXControler 16 | from TransactionManager import TransactionManager 17 | from Trader import Trader 18 | from UIGraph import UIGraph 19 | from AppState import AppState 20 | import TradingBotConfig as theConfig 21 | import pyqtgraph as pg 22 | 23 | from pyqtgraph.Qt import QtCore, QtGui # Only useful for splash screen 24 | 25 | 26 | 27 | class TradingBot(object): 28 | 29 | 30 | def __init__(self): 31 | 32 | 33 | cwd = os.getcwd() 34 | print("Running Astibot in: %s" % cwd) 35 | 36 | self.isInitializing = True 37 | self.iterationCounter = 0 38 | self.historicPriceIterationCounter = 0 39 | 40 | self.app = pg.QtGui.QApplication(['Astibot']) 41 | 42 | # Show Splash Screen 43 | splash_pix = QtGui.QPixmap('AstibotSplash.png') 44 | splash = QtGui.QSplashScreen(splash_pix, QtCore.Qt.WindowStaysOnTopHint) 45 | splash.show() 46 | 47 | 48 | # Instanciate objects 49 | self.theSettings = Settings() 50 | self.theUIGraph = UIGraph(self.app, self.theSettings) 51 | self.theGDAXControler = GDAXControler(self.theUIGraph, self.theSettings) 52 | self.theMarketData = MarketData(self.theGDAXControler, self.theUIGraph) 53 | self.theTransactionManager = TransactionManager(self.theGDAXControler, self.theUIGraph, self.theMarketData, self.theSettings) 54 | self.theUIGraph.UIGR_SetTransactionManager(self.theTransactionManager) 55 | self.theTrader = Trader(self.theTransactionManager, self.theMarketData, self.theUIGraph, self.theSettings) 56 | self.theInputDataHandler = InputDataHandler(self.theGDAXControler, self.theUIGraph, self.theMarketData, self.theTrader, self.theSettings) 57 | self.theApp = AppState(self.theUIGraph, self.theTrader, self.theGDAXControler, self.theInputDataHandler, self.theMarketData, self.theSettings) 58 | 59 | # Setup Main Tick Timer 60 | self.mainTimer = pg.QtCore.QTimer() 61 | self.mainTimer.timeout.connect(self.MainTimerHandler) 62 | self.mainTimer.start(100) 63 | 64 | # Hide splash screen 65 | splash.close() 66 | 67 | # Endless call 68 | self.app.exec_() 69 | 70 | # App closing 71 | self.theGDAXControler.GDAX_closeBackgroundOperations() 72 | self.theInputDataHandler.INDH_closeBackgroundOperations() 73 | self.theUIGraph.UIGR_closeBackgroundOperations() 74 | 75 | 76 | def MainTimerHandler(self): 77 | self.theApp.APP_Execute() 78 | 79 | 80 | if __name__ == '__main__': 81 | theTradingBot = TradingBot() 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/GDAXControler.py: -------------------------------------------------------------------------------- 1 | import cbpro 2 | from cbpro.public_client import PublicClient 3 | import time 4 | import threading 5 | from json import dumps, loads 6 | from cbpro.websocket_client import WebsocketClient 7 | import TradingBotConfig as theConfig 8 | from datetime import datetime 9 | import pytz 10 | from tzlocal import get_localzone 11 | from requests.exceptions import ConnectionError 12 | from GDAXCurrencies import GDAXCurrencies 13 | import math # truncate 14 | 15 | # This module is actually a Coinbae Pro handler 16 | # TODO : update all GDAX references to CbPro 17 | class GDAXControler(cbpro.OrderBook): 18 | ''' 19 | classdocs 20 | ''' 21 | GDAX_MAX_HISTORIC_PRICES_ELEMENTS = 300 22 | GDAX_HISTORIC_DATA_MIN_GRANULARITY_IN_SEC = 60 23 | GDAX_HISTORIC_DATA_SUBSCHEDULING_FACTOR = GDAX_HISTORIC_DATA_MIN_GRANULARITY_IN_SEC / (theConfig.CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS / 1000) 24 | 25 | 26 | def __init__(self, UIGraph, Settings): 27 | 28 | first_currency = GDAXCurrencies.get_all_pairs()[0] 29 | super(GDAXControler, self).__init__(product_id=first_currency, log_to=False) 30 | 31 | self.theUIGraph = UIGraph 32 | # Application settings data instance 33 | self.theSettings = Settings 34 | 35 | self.webSocketIsOpened = False 36 | self.isRunning = True 37 | self.requestAccountsBalanceUpdate = True 38 | self.backgroundOperationsCounter = 0 39 | 40 | self.tickBestBidPrice = 0 41 | self.tickBestAskPrice = 0 42 | self.liveBestBidPrice = 0 43 | self.liveBestAskPrice = 0 44 | self.midMarketPrice = 0 45 | self.currentOrderId = 0 46 | self.currentOrderState = "NONE" # SUBMITTED / OPENED / FILLED / NONE 47 | self.currentOrderInitialSizeInCrypto = 0 48 | self.currentOrderFilledSizeInCrypto = 0 49 | self.currentOrderAverageFilledPriceInFiat = 0 50 | 51 | self.productStr = self.theSettings.SETT_GetSettings()["strTradingPair"] 52 | self.productFiatStr = self.theSettings.SETT_GetSettings()["strFiatType"] 53 | self.productCryptoStr = self.theSettings.SETT_GetSettings()["strCryptoType"] 54 | self.bFiatAccountExists = False 55 | self.bCryptoAccountExists = False 56 | 57 | self.HistoricData = [] 58 | self.HistoricDataReadIndex = 0 59 | self.HistoricDataSubSchedulingIndex = 0 60 | 61 | self.IsConnectedAndOperational = "False" 62 | 63 | self.clientPublic = cbpro.PublicClient() 64 | 65 | # Start background thread 66 | threadRefreshPrice = threading.Timer(1, self.updateRealTimePriceInBackground) 67 | threadRefreshPrice.start() 68 | 69 | # WebSocket thread 70 | # Websocket thread is launched by parent classes 71 | self.webSocketLock = threading.Lock() 72 | 73 | print("GDAX - GDAX Controler Initialization"); 74 | 75 | def GDAX_IsConnectedAndOperational(self): 76 | return self.IsConnectedAndOperational 77 | 78 | # Fonction asynchrone 79 | def GDAX_InitializeGDAXConnection(self): 80 | self.theUIGraph.UIGR_updateInfoText("Trying to connect...", False) 81 | self.IsConnectedAndOperational = "Requested" 82 | print("GDAX - Connection requested") 83 | 84 | if (self.webSocketIsOpened == True): 85 | print("GDAX - Closing Websocket...") 86 | self.close() 87 | print("GDAX - Reseting Order book...") 88 | self.reset_book() 89 | # Orderbook class does not reset sequence number when changing product : set it to -1 will force orderbook to refresh 90 | # the sequence number and retrieve the last full order book 91 | self._sequence = -1 92 | 93 | self.liveBestBidPrice = 0 94 | self.liveBestAskPrice = 0 95 | 96 | def startWebSocketFeed(self): 97 | self.channels = ['full', 'user'] 98 | self._log_to = False 99 | self.auth=True 100 | self.products = [self.productStr] 101 | self.start() 102 | 103 | def PerformConnectionInitializationAttempt(self): 104 | print("GDAX - Performing connection initialization attempt...") 105 | 106 | bGDAXConnectionIsOK = True 107 | bInternetLinkIsOK = True 108 | self.bFiatAccountExists = False 109 | self.bCryptoAccountExists = False 110 | 111 | # Real Market keys ========================================= 112 | self.api_key = self.theSettings.SETT_GetSettings()["strAPIKey"] 113 | self.api_secret = self.theSettings.SETT_GetSettings()["strSecretKey"] 114 | self.api_passphrase = self.theSettings.SETT_GetSettings()["strPassphrase"] 115 | 116 | # Use the sandbox API : https://api-public.sandbox.cbpro.com (requires a different set of API access credentials) 117 | # Use the true API : https://api.cbpro.com 118 | try: 119 | self.clientAuth = cbpro.AuthenticatedClient(self.api_key, self.api_secret, self.api_passphrase, api_url="https://api.pro.coinbase.com") 120 | except ConnectionError as e: 121 | print("GDAX - Internet connection error") 122 | except BaseException as e: 123 | bGDAXConnectionIsOK = False 124 | bInternetLinkIsOK = False 125 | print("GDAX - Authentication error") 126 | print("GDAX - Exception : " + str(e)) 127 | 128 | # Refresh account in order to see if auth was successful 129 | try: 130 | self.accounts = self.clientAuth.get_accounts() 131 | time.sleep(0.05) 132 | print("GDAX - Init, Accounts retrieving: %s" % self.accounts) 133 | if ('id' in self.accounts[0]): 134 | print("GDAX - Successful accounts retrieving") 135 | else: 136 | bGDAXConnectionIsOK = False 137 | print("GDAX - Accounts retrieving not successful: no relevant data") 138 | except ConnectionError as e: 139 | print("GDAX - Internet connection error") 140 | bGDAXConnectionIsOK = False 141 | bInternetLinkIsOK = False 142 | except BaseException as e: 143 | bGDAXConnectionIsOK = False 144 | print("GDAX - Authentication error: not possible to get accounts data") 145 | print("GDAX - Exception : " + str(e)) 146 | print("GDAX - clientAuth is: %s" % str(self.clientAuth)) 147 | 148 | # If all steps before were successful, GDAX connection is working 149 | if (bInternetLinkIsOK == True): 150 | if (bGDAXConnectionIsOK == True): 151 | # Check existence of right accounts 152 | for currentAccount in self.accounts: 153 | if currentAccount['currency'] == self.productCryptoStr: 154 | self.CryptoAccount = currentAccount 155 | self.bCryptoAccountExists = True 156 | print("GDAX - %s account has been found" % self.productCryptoStr) 157 | if currentAccount['currency'] == self.productFiatStr: 158 | self.FiatAccount = currentAccount 159 | self.bFiatAccountExists = True 160 | print("GDAX - %s account has been found" % self.productFiatStr) 161 | 162 | # If both accounts corresponding to the trading pair exist, init is successful 163 | if ((self.bFiatAccountExists == True) and (self.bCryptoAccountExists == True)): 164 | print("GDAX - Initialization of GDAX connection successful") 165 | 166 | # Start Websocket feed 167 | self.startWebSocketFeed() 168 | 169 | self.IsConnectedAndOperational = "True" 170 | self.theUIGraph.UIGR_updateInfoText("Authentication successful", False) 171 | else: 172 | print("GDAX - Accounts corresponding to the trading pairs do not exist") 173 | self.IsConnectedAndOperational = "False" 174 | # Display error message 175 | if (self.bFiatAccountExists == False): 176 | self.theUIGraph.UIGR_updateInfoText("Error: No %s account found on your Coinbase Pro profile. Make sure you chose a trading pair that is available in your country" % self.productFiatStr, True) 177 | else: 178 | self.theUIGraph.UIGR_updateInfoText("Error: No %s account found on your Coinbase Pro profile. Make sure you chose a trading pair that you are authorized to trade" % self.productCryptoStr, True) 179 | 180 | self.refreshAccounts() 181 | else: 182 | print("GDAX - Initialization of GDAX connection failed") 183 | self.IsConnectedAndOperational = "False" 184 | # If first connection, display explanation message 185 | if (self.theSettings.SETT_IsSettingsFilePresent() == False): 186 | # Else, display error message 187 | self.theUIGraph.UIGR_updateInfoText("Welcome on Astibot! Your Coinbase Pro API keys are required for trading. Click here to set it up", False) 188 | else: 189 | # Else, display error message 190 | self.theUIGraph.UIGR_updateInfoText("Coinbase Pro Authentication error: check your API credentials", True) 191 | else: 192 | print("GDAX - Initialization of GDAX connection failed") 193 | self.IsConnectedAndOperational = "False" 194 | # Display error message 195 | self.theUIGraph.UIGR_updateInfoText("Connection to Coinbase Pro server failed. Check your internet connection.", True) 196 | 197 | def GDAX_NotifyThatTradingPairHasChanged(self): 198 | self.productStr = self.theSettings.SETT_GetSettings()["strTradingPair"] 199 | self.productFiatStr = self.theSettings.SETT_GetSettings()["strFiatType"] 200 | self.productCryptoStr = self.theSettings.SETT_GetSettings()["strCryptoType"] 201 | self.HistoricData = [] 202 | self.HistoricDataReadIndex = 0 203 | 204 | # Returns the Available fiat balance (ie. money that can be used and that is not held for any pending order) 205 | def GDAX_GetFiatAccountBalance(self): 206 | #print("GDAX - GetFiatAccountBalance") 207 | if (self.bFiatAccountExists == True): 208 | #print("GDAX - Exists") 209 | try: 210 | balanceToReturn = (round(float(self.FiatAccount['available']), 8)) 211 | return balanceToReturn 212 | except BaseException as e: 213 | print("GDAX - Error retrieving fiat account balance. Inconsistent data in fiat account object.") 214 | return 0 215 | else: 216 | print("GDAX - Does not exist") 217 | return 0 218 | 219 | def GDAX_GetFiatAccountBalanceHeld(self): 220 | #print("GDAX - GetFiatAccountBalance") 221 | if (self.bFiatAccountExists == True): 222 | #print("GDAX - Exists") 223 | try: 224 | balanceToReturn = (round(float(self.FiatAccount['hold']), 8)) 225 | return balanceToReturn 226 | except BaseException as e: 227 | print("GDAX - Error retrieving fiat hold account balance. Inconsistent data in fiat account object.") 228 | return 0 229 | else: 230 | print("GDAX - Does not exist") 231 | return 0 232 | 233 | # Returns the Available crypto balance (ie. money that can be used and that is not held for any pending order) 234 | def GDAX_GetCryptoAccountBalance(self): 235 | if (self.bCryptoAccountExists == True): 236 | try: 237 | balanceToReturn = (round(float(self.CryptoAccount['available']), 8)) 238 | return balanceToReturn 239 | except BaseException as e: 240 | print("GDAX - Error retrieving crypto account balance. Inconsistent data in crypto account object.") 241 | return 0 242 | else: 243 | print("GDAX - Error retrieving crypto account balance. Crypto account does not exist") 244 | return 0 245 | 246 | def GDAX_GetCryptoAccountBalanceHeld(self): 247 | if (self.bCryptoAccountExists == True): 248 | try: 249 | balanceToReturn = (round(float(self.CryptoAccount['hold']), 8)) 250 | print("GDAX - Returned held balance %s for %s" % (balanceToReturn, self.productCryptoStr)) 251 | return balanceToReturn 252 | except BaseException as e: 253 | print("GDAX - Error retrieving crypto hold account balance. Inconsistent data in crypto account object.") 254 | return 0 255 | else: 256 | print("GDAX - Error retrieving crypto account balance. Crypto account does not exist") 257 | return 0 258 | 259 | # Returns the Available BTC balance (ie. money that can be used and that is not held for any pending order) 260 | # Useful for payment system 261 | def GDAX_GetBTCAccountBalance(self): 262 | try: 263 | for currentAccount in self.accounts: 264 | if (currentAccount['currency'] == 'BTC'): 265 | balanceToReturn = round(float(currentAccount['available']), 7) 266 | return balanceToReturn 267 | return 0 268 | except BaseException as e: 269 | print("GDAX - Error retrieving crypto account balance. Inconsistent data in crypto account object.") 270 | return 0 271 | 272 | def refreshAccounts(self): 273 | try: 274 | self.accounts = self.clientAuth.get_accounts() 275 | # Refresh individual accounts 276 | for currentAccount in self.accounts: 277 | if currentAccount['currency'] == self.productCryptoStr: 278 | self.CryptoAccount = currentAccount 279 | #print("CRYPTO ACCOUNT") 280 | #print(self.CryptoAccount) 281 | #print(self.CryptoAccount['balance']) 282 | #print(self.CryptoAccount['available']) 283 | if currentAccount['currency'] == self.productFiatStr: 284 | self.FiatAccount = currentAccount 285 | #print("FIAT ACCOUNT") 286 | #print(self.FiatAccount) 287 | #print(self.FiatAccount['balance']) 288 | #print(self.FiatAccount['available']) 289 | 290 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 291 | self.theUIGraph.UIGR_updateAccountsBalance(self.GDAX_GetFiatAccountBalance(), self.GDAX_GetCryptoAccountBalance()) 292 | else: 293 | pass # In simulated market, accounts are refreshed by the Simulation manager 294 | except: 295 | print("GDAX - Error in refreshAccounts") 296 | 297 | 298 | def GDAX_RefreshAccountsDisplayOnly(self): 299 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 300 | self.theUIGraph.UIGR_updateAccountsBalance(self.GDAX_GetFiatAccountBalance(), self.GDAX_GetCryptoAccountBalance()) 301 | else: 302 | pass # TRNM takes care of the price update 303 | 304 | # WebSocket callback - On connection opening 305 | def on_open(self): 306 | print("GDAX - WebSocket connection opened (callback) on %s" % self.productStr) 307 | #self.url = "wss://ws-feed.pro.coinbase.com/" 308 | self.products = [self.productStr] 309 | self.webSocketIsOpened = True 310 | self.count = 0 311 | self.matchOrderProcessedSequenceId = 0 312 | 313 | 314 | def on_message(self, message): 315 | super(GDAXControler, self).on_message(message) 316 | 317 | self.webSocketLock.acquire() 318 | 319 | # Listen for user orders 320 | if ('order_id' in message): 321 | if (message['order_id'] == self.currentOrderId): 322 | print("GDAX - Current order msg: %s" % message) 323 | order_type = message['type'] 324 | if (order_type == 'open'): 325 | self.currentOrderState = "OPENED" 326 | print("GDAX - on_message: current order state updated to OPENED") 327 | elif (order_type == 'done'): 328 | if (message['reason'] == 'canceled'): 329 | self.currentOrderId = 0 330 | self.currentOrderState = "NONE" 331 | self.currentOrderInitialSizeInCrypto = 0 332 | self.currentOrderFilledSizeInCrypto = 0 333 | self.currentOrderAverageFilledPriceInFiat = 0 334 | print("GDAX - on_message: current order canceled") 335 | elif (float(message['remaining_size']) < theConfig.CONFIG_CRYPTO_PRICE_QUANTUM): 336 | self.currentOrderState = "FILLED" 337 | print("GDAX - on_message: current order totally filled (to check). Refresh accounts now") 338 | self.refreshAccounts() 339 | 340 | # Match messages do not have an "order_id" field but a maker/taker_order_id field 341 | if ('maker_order_id' in message): 342 | if (message['maker_order_id'] == self.currentOrderId): 343 | print("GDAX - Current order msg: %s" % message) 344 | if ((message['type'] == 'match') and ('size' in message)): 345 | # To preserve buy price calculation integrity, matched order must be processed once (but it appears both in user and full channels) 346 | # If this matched message is not processed yet 347 | if (self.matchOrderProcessedSequenceId != message['sequence']): 348 | print("GDAX - on_message: current order has been matched") 349 | newFillAverageInFiat = (self.currentOrderAverageFilledPriceInFiat*self.currentOrderFilledSizeInCrypto + float(message['size']) * float(message['price'])) / (self.currentOrderFilledSizeInCrypto + float(message['size'])) 350 | self.currentOrderFilledSizeInCrypto += float(message['size']) 351 | print("GDAX - on_message: average order fill price updated from %s to %s" % (self.currentOrderAverageFilledPriceInFiat, newFillAverageInFiat)) 352 | print("GDAX - on_message: current order total fill quantity updated to %s" % self.currentOrderFilledSizeInCrypto) 353 | self.currentOrderAverageFilledPriceInFiat = newFillAverageInFiat 354 | self.matchOrderProcessedSequenceId = message['sequence'] 355 | self.currentOrderState = "MATCHED" 356 | 357 | # Order book has been updated, retrieve best bid and ask 358 | self.liveBestBidPrice = self.get_bid() 359 | #print("Bid %s" % self.liveBestBidPrice) 360 | self.liveBestAskPrice = self.get_ask() 361 | #print("Ask %s" % self.liveBestAskPrice) 362 | 363 | self.webSocketLock.release() 364 | 365 | 366 | def on_close(self): 367 | print("GDAX - WebSocket connection closed (callback)") 368 | self.webSocketIsOpened = False 369 | 370 | if (self.isRunning == True): # If we are not exiting app 371 | if (self.IsConnectedAndOperational != "Requested" and self.IsConnectedAndOperational != "Ongoing"): # If we are not re-initializing connection (like settings apply) 372 | print("GDAX - Unexpected close of websocket. Trying to restart.") 373 | while (self.isRunning == True and self.webSocketIsOpened == False): 374 | print("GDAX - Restarting Websocket in 10 seconds...") 375 | time.sleep(10) 376 | self.startWebSocketFeed() 377 | print("GDAX - End of on_close()") 378 | 379 | def GDAX_GetLiveBestBidPrice(self): 380 | self.webSocketLock.acquire() 381 | liveBestBidPriceToReturn = self.liveBestBidPrice 382 | self.webSocketLock.release() 383 | 384 | return liveBestBidPriceToReturn 385 | 386 | def GDAX_GetLiveBestAskPrice(self): 387 | self.webSocketLock.acquire() 388 | liveBestAskPriceToReturn = self.liveBestAskPrice 389 | self.webSocketLock.release() 390 | 391 | return liveBestAskPriceToReturn 392 | 393 | def updateRealTimePriceInBackground(self): 394 | 395 | while (self.isRunning == True): 396 | result = "" 397 | # Attempt a GDAX Initialization if requested 398 | if (self.IsConnectedAndOperational == "Requested"): 399 | self.IsConnectedAndOperational = "Ongoing" 400 | self.PerformConnectionInitializationAttempt() 401 | time.sleep(1) # Don't poll GDAX API too much 402 | 403 | self.backgroundOperationsCounter = self.backgroundOperationsCounter + 1 404 | 405 | # Get Middle Market Price ========================================================== 406 | # Order book level 1 : Just the highest bid and lowest sell proposal 407 | try: 408 | result = self.clientPublic.get_product_order_book(self.productStr, 1) 409 | self.tickBestBidPrice = float(result['bids'][0][0]) 410 | self.tickBestAskPrice = float(result['asks'][0][0]) 411 | self.midMarketPrice = (self.tickBestBidPrice + self.tickBestAskPrice) / 2 412 | 413 | # DEBUG 414 | # print("GDAX - Highest Bid: %s" % self.tickBestBidPrice) 415 | # print("GDAX - Lowest Ask: %s" % self.tickBestAskPrice) 416 | 417 | self.PriceSpread = self.tickBestBidPrice - self.tickBestAskPrice 418 | #print("GDAX - MiddleMarket price: %s" % self.tickBestBidPrice) 419 | 420 | self.theUIGraph.UIGR_updateConnectionText("Price data received from Coinbase Pro server") 421 | 422 | # Refresh account balances 423 | # Only do it if GDAX controler is OK in authenticated mode 424 | if (self.IsConnectedAndOperational == "True"): 425 | if ((self.backgroundOperationsCounter % 20 == 0) or (self.requestAccountsBalanceUpdate == True)): 426 | self.requestAccountsBalanceUpdate = False 427 | if (self.IsConnectedAndOperational == "True"): 428 | self.refreshAccounts() 429 | 430 | except BaseException as e: 431 | print("GDAX - Error retrieving level 1 order book or account data") 432 | print("GDAX - Exception : " + str(e)) 433 | print(result) 434 | self.requestAccountsBalanceUpdate = False 435 | 436 | # Get current Orders =============================================================== 437 | 438 | for x in range(0, 5): 439 | if (self.requestAccountsBalanceUpdate == False): 440 | time.sleep(0.1) 441 | 442 | self.theUIGraph.UIGR_resetConnectionText() 443 | 444 | for x in range(0, 15): 445 | if (self.requestAccountsBalanceUpdate == False): 446 | time.sleep(0.1) 447 | 448 | 449 | def GDAX_closeBackgroundOperations(self): 450 | 451 | self.isRunning = False 452 | 453 | if (self.webSocketIsOpened == True): 454 | print("GDAX - Closing Websocket...") 455 | self.close() 456 | 457 | 458 | def GDAX_GetRealTimePriceInEUR(self): 459 | return self.midMarketPrice 460 | 461 | def GDAX_GetCurrentLimitOrderState(self): 462 | self.webSocketLock.acquire() 463 | currentState = self.currentOrderState 464 | 465 | if (currentState == "FILLED"): 466 | self.currentOrderState = "NONE" 467 | 468 | self.webSocketLock.release() 469 | 470 | return currentState 471 | 472 | def GDAX_GetAveragePriceInFiatAndSizeFilledInCrypto(self): 473 | print("GDAX - GDAX_GetAveragePriceInFiatAndSizeFilledInCrypto : AverageFilledPrice = %s, currentOrderFilledSizeInCrypo = %s" % (self.currentOrderAverageFilledPriceInFiat, self.currentOrderFilledSizeInCrypto)) 474 | return [self.currentOrderAverageFilledPriceInFiat, self.currentOrderFilledSizeInCrypto] 475 | 476 | def GDAX_PlaceLimitBuyOrder(self, amountToBuyInCrypto, buyPriceInFiat): 477 | 478 | self.webSocketLock.acquire() 479 | 480 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 481 | 482 | print("GDAX - GDAX_PlaceLimitBuyOrder") 483 | 484 | # First, cancel ongoing order if any 485 | if (self.currentOrderState != "NONE"): 486 | self.INTERNAL_CancelOngoingLimitOrder() 487 | 488 | 489 | # Send Limit order 490 | amountToBuyInCrypto = round(amountToBuyInCrypto, 8) 491 | 492 | # Don't use round because order could be placed on the other side of the spread -> rejected 493 | # Prix exprimé en BTC, arrondi variable 494 | if (self.productFiatStr == "BTC"): 495 | if (self.productCryptoStr == "LTC"): 496 | buyPriceInFiat = math.floor(buyPriceInFiat*1000000)/1000000 # Floor à 0.000001 497 | else: 498 | buyPriceInFiat = math.floor(buyPriceInFiat*100000)/100000 # Floor à 0.00001 499 | else: # Prix exprimé en Fiat, arrondi à 0.01 500 | buyPriceInFiat = math.floor(buyPriceInFiat*100)/100 501 | 502 | 503 | buyRequestReturn = self.clientAuth.buy(price=str(buyPriceInFiat), size=str(amountToBuyInCrypto), product_id=self.productStr, order_type='limit', post_only=True) # with Post Only 504 | print("GDAX - Actual buy sent with LIMIT order set to %s. Amount is %s Crypto" % (buyPriceInFiat, amountToBuyInCrypto)) 505 | print("GDAX - Limit order placing sent. Request return is: %s" % buyRequestReturn) 506 | if ('id' in buyRequestReturn): 507 | if (not 'reject_reason' in buyRequestReturn): 508 | self.currentOrderId = buyRequestReturn['id'] 509 | self.currentOrderState = "SUBMITTED" 510 | self.currentOrderInitialSizeInCrypto = amountToBuyInCrypto 511 | self.currentOrderFilledSizeInCrypto = 0 512 | self.currentOrderAverageFilledPriceInFiat = 0 513 | print("GDAX - Limit order state set to SUBMITTED") 514 | 515 | self.webSocketLock.release() 516 | return True 517 | else: 518 | print("GDAX - Buy limit order has been interpreted as rejected. Reason: %s" % buyRequestReturn['reject_reason']) 519 | 520 | self.webSocketLock.release() 521 | return False 522 | else: 523 | print("GDAX - Buy limit order has been interpreted as rejected") 524 | 525 | self.webSocketLock.release() 526 | return False 527 | else: 528 | # Simulation mode: simulate immediate order fill 529 | self.currentOrderId = -1 530 | self.currentOrderFilledSizeInCrypto = float(amountToBuyInCrypto) 531 | self.currentOrderAverageFilledPriceInFiat = float(buyPriceInFiat) 532 | print("GDAX - Limit buy simulated, buy price: %s, amountToBuyInCrypto: %s" % (round(float(buyPriceInFiat), 2), float(amountToBuyInCrypto))) 533 | self.currentOrderState = "FILLED" 534 | 535 | self.webSocketLock.release() 536 | return True 537 | 538 | 539 | def GDAX_PlaceLimitSellOrder(self, amountToSellInCrypto, sellPriceInFiat): 540 | 541 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 542 | 543 | self.webSocketLock.acquire() 544 | 545 | # First, cancel ongoing order if any 546 | if (self.currentOrderState != "NONE"): 547 | self.INTERNAL_CancelOngoingLimitOrder() 548 | 549 | # Send Limit order 550 | amountToSellInCrypto = round(amountToSellInCrypto, 8) 551 | 552 | # Don't use round because order could be placed on the other side of the spread -> rejected 553 | # Prix exprimé en BTC, arrondi variable 554 | if (self.productFiatStr == "BTC"): 555 | if (self.productCryptoStr == "LTC"): 556 | sellPriceInFiat = math.floor(sellPriceInFiat*1000000)/1000000 # Floor à 0.000001 557 | else: 558 | sellPriceInFiat = math.floor(sellPriceInFiat*100000)/100000 # Floor à 0.00001 559 | else: # Prix exprimé en Fiat, arrondi à 0.01 560 | sellPriceInFiat = math.floor(sellPriceInFiat*100)/100 561 | 562 | sellRequestReturn = self.clientAuth.sell(price=str(sellPriceInFiat), size=str(amountToSellInCrypto), product_id=self.productStr, order_type='limit', post_only=True) # with Post Only 563 | print("GDAX - Actual sell sent with LIMIT order set to %s. Amount is %s Crypto" % (sellPriceInFiat, amountToSellInCrypto)) 564 | print("GDAX - Limit order placing sent. Request return is: %s" % sellRequestReturn) 565 | if ('id' in sellRequestReturn): 566 | self.currentOrderId = sellRequestReturn['id'] 567 | self.currentOrderState = "SUBMITTED" 568 | self.currentOrderInitialSizeInCrypto = amountToSellInCrypto 569 | self.currentOrderFilledSizeInCrypto = 0 570 | self.currentOrderAverageFilledPriceInFiat = 0 571 | 572 | self.webSocketLock.release() 573 | return True 574 | else: 575 | print("GDAX - Sell limit order has been interpreted as rejected") 576 | 577 | self.webSocketLock.release() 578 | return False 579 | else: 580 | # Simulation mode: simulate immediate order fill 581 | self.currentOrderFilledSizeInCrypto = amountToSellInCrypto 582 | self.currentOrderAverageFilledPriceInFiat = sellPriceInFiat 583 | self.currentOrderState = "FILLED" 584 | 585 | self.webSocketLock.release() 586 | return True 587 | 588 | 589 | # Include thread safe protection: shall be called from outside 590 | def GDAX_CancelOngoingLimitOrder(self): 591 | self.webSocketLock.acquire() 592 | if (self.currentOrderId != 0): 593 | self.currentOrderId = 0 # So that websocket won't get the cancel notification 594 | self.currentOrderState = "NONE" 595 | self.currentOrderInitialSizeInCrypto = 0 596 | self.currentOrderFilledSizeInCrypto = 0 597 | self.currentOrderAverageFilledPriceInFiat = 0 598 | cancelAllReturn = self.clientAuth.cancel_all(self.productStr) 599 | print("GDAX - GDAX_CancelOngoingLimitOrder: Ongoing order canceled. Request return is: %s" % cancelAllReturn) 600 | else: 601 | print("GDAX - GDAX_CancelOngoingLimitOrder: No order to cancel! Just filled?") 602 | self.webSocketLock.release() 603 | 604 | # Does not include thread safe protection: shall not be called from outside 605 | def INTERNAL_CancelOngoingLimitOrder(self): 606 | 607 | if (self.currentOrderId != 0): 608 | self.currentOrderId = 0 # So that websocket won't get the cancel notification 609 | self.currentOrderState = "NONE" 610 | self.currentOrderInitialSizeInCrypto = 0 611 | self.currentOrderFilledSizeInCrypto = 0 612 | self.currentOrderAverageFilledPriceInFiat = 0 613 | cancelAllReturn = self.clientAuth.cancel_all(self.productStr) 614 | print("GDAX - INTERNAL_CancelOngoingLimitOrder: Ongoing order canceled. Request return is: %s" % cancelAllReturn) 615 | else: 616 | print("GDAX - INTERNAL_CancelOngoingLimitOrder: No order to cancel! Just filled?") 617 | 618 | 619 | def GDAX_SendBuyOrder(self, amountToBuyInBTC): 620 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 621 | if (theConfig.CONFIG_ENABLE_REAL_TRANSACTIONS == True): 622 | # Prepare the right amount to buy precision. Smallest GDAX unit is 0.00000001 623 | amountToBuyInBTC = round(amountToBuyInBTC, 8) 624 | 625 | # Send Market order 626 | buyRequestReturn = self.clientAuth.buy(size=amountToBuyInBTC, product_id=self.productStr, order_type='market') 627 | print("GDAX - Actual buy sent with MARKET order. Amount is %s BTC" % amountToBuyInBTC) 628 | 629 | print("GDAX - Buy Request return is : \n %s \nGDAX - End of Request Return" % buyRequestReturn) 630 | 631 | self.requestAccountsBalanceUpdate = True 632 | 633 | # Check if order was successful or not depending on existence of an order ID in the request response 634 | if 'id' in buyRequestReturn: 635 | print("GDAX - Buy order has been interpreted as successful") 636 | return True 637 | else: 638 | print("GDAX - Buy order has been interpreted as failed") 639 | return False 640 | else: 641 | return False 642 | else: 643 | return False 644 | 645 | 646 | def GDAX_SendSellOrder(self, amountToSellInBTC): 647 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 648 | if (theConfig.CONFIG_ENABLE_REAL_TRANSACTIONS == True): 649 | # Prepare the right amount to sell precision. Smallest GDAX unit is 0.00000001 650 | amountToSellInBTC = round(amountToSellInBTC, 8) 651 | 652 | # Send Market order 653 | sellRequestReturn = self.clientAuth.sell(size=amountToSellInBTC, product_id=self.productStr, order_type='market') 654 | print("Actual sell sent with MARKET order. Amount is %s" % amountToSellInBTC) 655 | 656 | print("GDAX - Sell Request return is : \n %s \nGDAX - End of Request Return" % sellRequestReturn) 657 | time.sleep(0.1) 658 | self.refreshAccounts() 659 | time.sleep(0.1) 660 | self.requestAccountsBalanceUpdate = True 661 | 662 | # Check if order was successful or not depending on existence of an order ID in the request response 663 | if 'id' in sellRequestReturn: 664 | print("GDAX - Sell order has been interpreted as successful") 665 | return True 666 | else: 667 | print("GDAX - Sell order has been interpreted as failed") 668 | return False 669 | else: 670 | return False 671 | else: 672 | return False 673 | 674 | def GDAX_IsAmountToBuyAboveMinimum(self, amountOfCryptoToBuy): 675 | if (self.theSettings.SETT_GetSettings()["strCryptoType"] == "BTC"): 676 | if (amountOfCryptoToBuy > 0.001): 677 | return True 678 | else: 679 | return False 680 | 681 | if (self.theSettings.SETT_GetSettings()["strCryptoType"] == "BCH"): 682 | if (amountOfCryptoToBuy > 0.01): 683 | return True 684 | else: 685 | return False 686 | 687 | if (self.theSettings.SETT_GetSettings()["strCryptoType"] == "LTC"): 688 | if (amountOfCryptoToBuy > 0.1): 689 | return True 690 | else: 691 | return False 692 | 693 | if (self.theSettings.SETT_GetSettings()["strCryptoType"] == "ETH"): 694 | if (amountOfCryptoToBuy > 0.01): 695 | return True 696 | else: 697 | return False 698 | 699 | if (self.theSettings.SETT_GetSettings()["strCryptoType"] == "ETC"): 700 | if (amountOfCryptoToBuy > 0.1): 701 | return True 702 | else: 703 | return False 704 | 705 | return True 706 | 707 | def GDAX_WithdrawBTC(self, destinationAddress, amountToWithdrawInBTC): 708 | print("GDAX - Withdraw BTC") 709 | 710 | if (theConfig.CONFIG_DEBUG_ENABLE_DUMMY_WITHDRAWALS == False): 711 | withdrawRequestReturn = self.clientAuth.crypto_withdraw(amountToWithdrawInBTC, 'BTC', destinationAddress) 712 | 713 | print("GDAX - Withdraw request return: %s" % withdrawRequestReturn) 714 | # Check if withdraw was successful or not depending on existence of an order ID in the request response 715 | if 'id' in withdrawRequestReturn: 716 | print("GDAX - Withdraw has been interpreted as successful") 717 | return withdrawRequestReturn['id'] 718 | else: 719 | print("GDAX - Withdraw has failed") 720 | return "Error" 721 | else: 722 | return "Dummy Withdraw" 723 | 724 | def GDAX_RequestAccountsBalancesUpdate(self): 725 | self.requestAccountsBalanceUpdate = True 726 | 727 | def GDAX_LoadHistoricData(self, startTimestamp, stopTimestamp): 728 | 729 | print("Init to retrieve Historic Data from %s to %s" % (datetime.fromtimestamp(startTimestamp).isoformat(), datetime.fromtimestamp(stopTimestamp).isoformat())) 730 | print("---------") 731 | # Reset read index are we will overwrite the buffer 732 | self.HistoricDataReadIndex = 0 733 | 734 | local_tz = get_localzone() 735 | print("GDAX - Local timezone found: %s" % local_tz) 736 | tz = pytz.timezone(str(local_tz)) 737 | 738 | stopSlice = 0 739 | startSlice = startTimestamp 740 | self.HistoricDataRaw = [] 741 | self.HistoricData = [] 742 | 743 | # Progression measurement 744 | granularityInSec = round(self.GDAX_HISTORIC_DATA_MIN_GRANULARITY_IN_SEC) 745 | nbIterationsToRetrieveEverything = ((stopTimestamp - startTimestamp) / (round(self.GDAX_HISTORIC_DATA_MIN_GRANULARITY_IN_SEC))) / round(self.GDAX_MAX_HISTORIC_PRICES_ELEMENTS) 746 | print("GDAX - Nb Max iterations to retrieve everything: %s" % nbIterationsToRetrieveEverything) 747 | nbLoopIterations = 0 748 | 749 | while (stopSlice < stopTimestamp): 750 | 751 | stopSlice = startSlice + self.GDAX_MAX_HISTORIC_PRICES_ELEMENTS * granularityInSec 752 | if (stopSlice > stopTimestamp): 753 | stopSlice = stopTimestamp 754 | print("GDAX - Start TS : %s stop TS : %s" % (startSlice, stopSlice)) 755 | 756 | startTimestampSliceInISO = datetime.fromtimestamp(startSlice, tz).isoformat() 757 | stopTimestampSliceInISO = datetime.fromtimestamp(stopSlice, tz).isoformat() 758 | print("GDAX - Retrieving Historic Data from %s to %s" % (startTimestampSliceInISO, stopTimestampSliceInISO)) 759 | if (self.IsConnectedAndOperational == "True"): 760 | print("GDAX - Using public client to retrieve historic prices") 761 | HistoricDataSlice = self.clientAuth.get_product_historic_rates(self.productStr, granularity=granularityInSec, start=startTimestampSliceInISO, end=stopTimestampSliceInISO) 762 | # Only sleep if reloop condition is met 763 | if (stopSlice < stopTimestamp): 764 | time.sleep(0.350) 765 | print("GDAX - Using private client to retrieve historic prices") 766 | else: 767 | HistoricDataSlice = self.clientPublic.get_product_historic_rates(self.productStr, granularity=granularityInSec, start=startTimestampSliceInISO, end=stopTimestampSliceInISO) 768 | # Only sleep if reloop condition is met 769 | if (stopSlice < stopTimestamp): 770 | time.sleep(0.250) 771 | print("GDAX - Using public client to retrieve historic prices") 772 | 773 | print("GDAX - Size of HistoricDataSlice: %s" % len(HistoricDataSlice)) 774 | 775 | try: # parfois le reversed crash. Pas de data dans la slice ? 776 | for slice in reversed(HistoricDataSlice): 777 | self.HistoricDataRaw.append(slice) 778 | except BaseException as e: 779 | print("GDAX - Exception when reversing historic data slice") 780 | #print("Historic : %s " % HistoricDataSlice) 781 | 782 | startSlice = stopSlice # Prepare next iteration 783 | 784 | # Progress report 785 | nbLoopIterations = nbLoopIterations + 1 786 | percentage = round(nbLoopIterations * 100 / nbIterationsToRetrieveEverything) 787 | if (percentage > 100): 788 | percentage = 100 789 | self.theUIGraph.UIGR_updateLoadingDataProgress(str(percentage)) 790 | 791 | # Clean buffer so that only data in the chronological order remains 792 | print("GDAX - LoadHistoricData - Cleaning buffer. Nb elements before cleaning : %s" % len(self.HistoricDataRaw)) 793 | tempIterationIndex = 0 794 | currentBrowsedTimestamp = 0 795 | while (tempIterationIndex < len(self.HistoricDataRaw)): 796 | if (self.HistoricDataRaw[tempIterationIndex][0] <= currentBrowsedTimestamp + 1): 797 | # Useless data : do not copy into final buffer 798 | pass 799 | else: 800 | currentBrowsedTimestamp = self.HistoricDataRaw[tempIterationIndex][0] 801 | self.HistoricData.append(self.HistoricDataRaw[tempIterationIndex]) 802 | 803 | #print(self.HistoricData[tempIterationIndex][0]) 804 | tempIterationIndex = tempIterationIndex + 1 805 | 806 | # DEBUG 807 | # tempIterationIndex = 0 808 | # while (tempIterationIndex < len(self.HistoricData)): 809 | # print(self.HistoricData[tempIterationIndex][0]) 810 | # tempIterationIndex = tempIterationIndex + 1 811 | # 812 | print ("GDAX - %s Historical samples have been retrieved (after cleaning)" % len(self.HistoricData)) 813 | 814 | # Returns a price data sample CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS seconds after the last call 815 | # even if GDAX historic sample period is longer 816 | def GDAX_GetNextHistoricDataSample(self): 817 | #print("HistoricData : %s " % self.HistoricData) 818 | #print("GDAX - Full Historic data list length is %s" % len(self.HistoricData)) 819 | 820 | endOfList = False 821 | self.HistoricDataReadIndex = self.HistoricDataReadIndex + 1 822 | if (self.HistoricDataReadIndex + 1 >= len(self.HistoricData)): # We've read as many samples as they are in the list 823 | endOfList = True 824 | print("GDAX - Historic Data - End of list reached") 825 | # print ("Time retrieved %s" % self.HistoricData[self.HistoricDataReadIndex][0]) 826 | # print ("Price retrieved %s" % self.HistoricData[self.HistoricDataReadIndex][4]) 827 | # print ("Len list %s, Index : %s" % (len(self.HistoricData), self.HistoricDataReadIndex)) 828 | 829 | # Fifth element (index 4) is the closure price 830 | return [self.HistoricData[self.HistoricDataReadIndex][0], self.HistoricData[self.HistoricDataReadIndex][4], endOfList] 831 | 832 | def GDAX_SetReadIndexFromPos(self, positionTimeStamp): 833 | tempIterationIndex = 0 834 | bReadIndexFound = False 835 | print ("GDAX - SetReadIndexFromPos : %d" % positionTimeStamp) 836 | print ("GDAX - Historic data length is %s" % len(self.HistoricData)) 837 | 838 | while ((tempIterationIndex < len(self.HistoricData)) and (bReadIndexFound == False)): 839 | if (self.HistoricData[tempIterationIndex][0] > positionTimeStamp): 840 | self.HistoricDataReadIndex = tempIterationIndex 841 | bReadIndexFound = True 842 | tempIterationIndex = tempIterationIndex + 1 843 | 844 | if (bReadIndexFound == True): 845 | print ("GDAX - SetReadIndexFromPos : index found: %s" % self.HistoricDataReadIndex) 846 | return True 847 | else: 848 | print ("GDAX - SetReadIndexFromPos : index not found") 849 | return False 850 | 851 | # Return the number of samples that can be read starting from the current readIndex position, until the end of the buffer 852 | def GDAX_GetNumberOfSamplesLeftToRead(self): 853 | nbOfSamplesLeftToRead = len(self.HistoricData) - self.HistoricDataReadIndex 854 | print("GDAX - Number of samples left to read is %s" % nbOfSamplesLeftToRead) 855 | return nbOfSamplesLeftToRead 856 | 857 | def GDAX_GetHistoricDataSubSchedulingFactor(self): 858 | return self.GDAX_HISTORIC_DATA_SUBSCHEDULING_FACTOR 859 | 860 | def GDAX_GetLoadedDataStartTimeStamp(self): 861 | if (len(self.HistoricData) > 2): 862 | return self.HistoricData[0][0] 863 | else: 864 | return 99999999999 865 | 866 | def GDAX_GetLoadedDataStopTimeStamp(self): 867 | if (len(self.HistoricData) > 2): 868 | return self.HistoricData[-1][0] 869 | else: 870 | return 0 871 | 872 | def GDAX_ListAccountWithdrawals(self): 873 | print(self.clientAuth.get_account_history(self.CryptoAccount['id'])) 874 | 875 | -------------------------------------------------------------------------------- /src/GDAXCurrencies.py: -------------------------------------------------------------------------------- 1 | import cbpro 2 | from cbpro.public_client import PublicClient 3 | 4 | class GDAXCurrencies: 5 | 6 | @staticmethod 7 | def get_all_pairs(): 8 | clientPublic = cbpro.PublicClient() 9 | products = clientPublic.get_products() 10 | return sorted(list(map(lambda x: x["id"], products))) 11 | 12 | @staticmethod 13 | def get_currencies_list(): 14 | pairs = GDAXCurrencies.get_all_pairs() 15 | product_map = [] 16 | 17 | for pair in pairs: 18 | pieces = pair.split('-') 19 | product_map.append({ 20 | "full": pair, 21 | "coin": pieces[0], 22 | "fiat": pieces[1] 23 | }) 24 | 25 | return product_map 26 | 27 | @staticmethod 28 | def get_index_for_currency_pair(pair): 29 | return GDAXCurrencies.get_all_pairs().index(pair) 30 | -------------------------------------------------------------------------------- /src/InputDataHandler.py: -------------------------------------------------------------------------------- 1 | import time 2 | import threading 3 | 4 | import TradingBotConfig as theConfig 5 | from MarketData import MarketData 6 | from Trader import Trader 7 | 8 | 9 | class InputDataHandler(object): 10 | 11 | 12 | def __init__(self, GDAXControler, UIGraph, MarketData, Trader, Settings): 13 | 14 | self.theGDAXControler = GDAXControler 15 | self.theUIGraph = UIGraph 16 | self.theMarketData = MarketData 17 | self.theTrader = Trader 18 | self.theSettings = Settings 19 | self.currentSubSchedulingFactor = 1 20 | self.nbIterations = 0 21 | self.PreloadHistoricDataStatus = "Idle" 22 | self.operationalStatus = "Idle" 23 | self.simulationPauseIsRequested = False 24 | self.simulationStopIsRequested = False 25 | self.liveTradingStopIsRequested = False 26 | self.abortOperations = False 27 | 28 | self.currentSubSchedulingFactor = self.theGDAXControler.GDAX_GetHistoricDataSubSchedulingFactor() 29 | 30 | def INDH_GetPreloadHistoricDataStatus(self): 31 | # If read once at Ended state, go back to Idle state 32 | if (self.PreloadHistoricDataStatus == "Ended"): 33 | self.PreloadHistoricDataStatus = "Idle" 34 | return "Ended" 35 | else: 36 | return self.PreloadHistoricDataStatus 37 | 38 | 39 | def PreloadHistoricData(self, displayWholeBufferAtTheEnd, nbHoursToPreload): 40 | self.nbHoursToPreload = nbHoursToPreload 41 | 42 | # First call in a sequence, launch loading thread 43 | if (self.PreloadHistoricDataStatus != "Ongoing"): 44 | self.PreloadHistoricDataStatus = "Ongoing" 45 | self.threadLoadHistoricData = threading.Timer(0, self.LoadHistoricData, [displayWholeBufferAtTheEnd]) 46 | self.threadLoadHistoricData.start() 47 | else: 48 | # Fetching is ongoing, do nothing 49 | pass 50 | 51 | def INDH_PrepareHistoricDataSinceGivenHours(self, displayWholeBufferAtTheEnd, nbHoursToPreload): 52 | 53 | if ((self.PreloadHistoricDataStatus == "Idle") or (self.PreloadHistoricDataStatus == "Ended")): 54 | self.PreloadHistoricData(displayWholeBufferAtTheEnd, nbHoursToPreload) 55 | else: 56 | # Ongoing, do nothing but poll INDH_GetPreloadHistoricDataStatus 57 | pass 58 | 59 | def GetLoadedDataStartTimestamp(self): 60 | return self.theGDAXControler.GDAX_GetLoadedDataStartTimeStamp() 61 | 62 | def GetLoadedDataEndTimestamp(self): 63 | return self.theGDAXControler.GDAX_GetLoadedDataStopTimeStamp() 64 | 65 | # Blocking function that allows GDAXControler to load historic data. Shall be called from a background thread. 66 | def LoadHistoricData(self, displayWholeBufferAtTheEndArray): 67 | print("INDH - Thread Load Historic Data: Started") 68 | 69 | self.initialTimeStampInUserTime = time.time() 70 | 71 | print("INDH - nbHoursToPreload: %s" % str(self.nbHoursToPreload)) 72 | print("INDH - initialTimeStampInUserTime: %s" % str(self.initialTimeStampInUserTime)) 73 | 74 | desiredStartTimeStamp = time.time() - (self.nbHoursToPreload * 3600) 75 | desiredEndTimeStamp = time.time() 76 | 77 | print("INDH - INDH_PrepareHistoricDataSinceGivenHours : desired start = %s, desired end = %s" % (desiredStartTimeStamp, desiredEndTimeStamp)) 78 | 79 | # If preloaded data start time is after desired start time with a delta, reloading is necessary OR 80 | # if preloaded end time is before current time with a delta, reloading is necessary 81 | if (self.GetLoadedDataStartTimestamp() > (desiredStartTimeStamp + theConfig.NB_SECONDS_THRESHOLD_FROM_NOW_FOR_RELOADING_DATA)) or ((self.GetLoadedDataEndTimestamp() + theConfig.NB_SECONDS_THRESHOLD_FROM_NOW_FOR_RELOADING_DATA) < desiredEndTimeStamp): 82 | print("INDH - INDH_PrepareHistoricDataSinceGivenHours : Desired data not present. Loaded start is %s, end is %s. Loading in background thread..." % (self.GetLoadedDataStartTimestamp(), self.GetLoadedDataEndTimestamp())) 83 | startTimeStampToLoadData = self.initialTimeStampInUserTime - (self.nbHoursToPreload * 3600) 84 | stopTimeStampToLoadData = self.initialTimeStampInUserTime 85 | print("INDH - Will retrieve Historic price data from %s to %s" % (startTimeStampToLoadData, stopTimeStampToLoadData)) 86 | self.theGDAXControler.GDAX_LoadHistoricData(startTimeStampToLoadData, stopTimeStampToLoadData) 87 | print("INDH - Historic Data loading ended") 88 | print("INDH - Display everything in one shot: %s" % displayWholeBufferAtTheEndArray) 89 | else: 90 | print("INDH - No need to preload historic data") 91 | 92 | # Only true for trading where we want to see the historic price d'un coup 93 | if (displayWholeBufferAtTheEndArray == True): 94 | print("INDH - Thread Load Historic Data: batch MarketData and Graph update. Subscheduling factor is %s" % self.currentSubSchedulingFactor) 95 | 96 | self.theMarketData.MRKT_ResetAllData(1) 97 | 98 | startTimeStampRequested = time.time() - (theConfig.NB_HISTORIC_DATA_HOURS_TO_PRELOAD_FOR_TRADING * 3600) 99 | self.theGDAXControler.GDAX_SetReadIndexFromPos(startTimeStampRequested) 100 | 101 | nbOfSamplesToDisplayOnGraph = theConfig.CONFIG_NB_POINTS_LIVE_TRADING_GRAPH 102 | print("INDH - Choosen to display %s points on graph" % nbOfSamplesToDisplayOnGraph) 103 | self.theUIGraph.UIGR_ResetAllGraphData(False, -1, int(nbOfSamplesToDisplayOnGraph)) 104 | 105 | [self.retrievedTime, self.retrievedPrice, endOfList] = self.theGDAXControler.GDAX_GetNextHistoricDataSample() 106 | currentTimeStamp = self.retrievedTime 107 | endOfList = False 108 | timeStep = theConfig.CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS / 1000 109 | 110 | while ((endOfList == False) and (self.abortOperations == False)): 111 | #print("currentTimestamp %s" % currentTimeStamp) 112 | if (currentTimeStamp >= self.retrievedTime): 113 | # Update market data with this original (non artificial) sample 114 | self.theMarketData.MRKT_updateMarketData(self.retrievedTime, self.retrievedPrice) 115 | currentTimeStamp = self.retrievedTime 116 | # Get next sample in memory 117 | [self.retrievedTime, self.retrievedPrice, endOfList] = self.theGDAXControler.GDAX_GetNextHistoricDataSample() 118 | else: 119 | # Interpolate with previous sample value 120 | currentTimeStamp = currentTimeStamp + timeStep 121 | self.theMarketData.MRKT_updateMarketData(currentTimeStamp, self.retrievedPrice) 122 | 123 | self.theUIGraph.UIGR_updateGraphs() 124 | self.theUIGraph.UIGR_performManualYRangeRefresh() 125 | 126 | self.PreloadHistoricDataStatus = "Ended" 127 | 128 | # Initiates the simulation thread. 129 | def INDH_PerformSimulation(self, nbHoursFromNow): 130 | # Security : if historic data is still loading, return Ended 131 | if (self.PreloadHistoricDataStatus == "Ongoing"): 132 | return "Ended" 133 | 134 | # Open background thread to perform simulation 135 | if (self.operationalStatus != "Ongoing"): 136 | self.operationalStatus = "Ongoing" 137 | self.simulationPauseIsRequested = False 138 | self.simulationStopIsRequested = False 139 | 140 | # Set read index at right pos 141 | startTimeStampRequested = time.time() - (nbHoursFromNow * 3600) 142 | setReadPosResult = self.theGDAXControler.GDAX_SetReadIndexFromPos(startTimeStampRequested) 143 | 144 | if (setReadPosResult == True): 145 | # Clear graph data and enable continuous graph update 146 | self.theMarketData.MRKT_ResetAllData(self.theGDAXControler.GDAX_GetHistoricDataSubSchedulingFactor()) 147 | self.theUIGraph.UIGR_ResetAllGraphData(True, startTimeStampRequested, theConfig.CONFIG_NB_POINTS_SIMU_GRAPH) # Last arg is the nb of points on simulation graph 148 | self.theUIGraph.UIGR_StartContinuousGraphRefresh(25) 149 | # Prepare trader 150 | self.theTrader.TRAD_ResetTradingParameters() 151 | # Launch simulation thread 152 | self.threadPerformSimulation = threading.Timer(0, self.PerformSimulationThread) 153 | self.threadPerformSimulation.start() 154 | return "Ongoing" 155 | else: 156 | # Read position not found, return Error 157 | return "Error" 158 | else: 159 | # Already initiated, should not happen 160 | return "Error" 161 | 162 | def PerformSimulationThread(self): 163 | 164 | batchSamplesSizeForSpeed = self.theSettings.SETT_GetSettings()["simulationSpeed"] + 2 165 | batchSamplesInitGraph = theConfig.CONFIG_NB_POINTS_INIT_SIMU_GRAPH * self.getCurrentSubSchedulingFactor() 166 | nbHistoricSamplesRetrieved = 0 167 | [self.retrievedTime, self.retrievedPrice, endOfList] = self.theGDAXControler.GDAX_GetNextHistoricDataSample() 168 | currentTimeStamp = self.retrievedTime 169 | endOfList = False 170 | timeStep = theConfig.CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS / 1000 171 | 172 | print("INDH - Starting Simulation Thread. Batch size for simulation speed is %s" % batchSamplesSizeForSpeed) 173 | 174 | while ((endOfList == False) and (self.simulationStopIsRequested == False)): 175 | #print("currentTimestamp %s" % currentTimeStamp) 176 | if (currentTimeStamp >= self.retrievedTime): 177 | # Update market data with this original (non artificial) sample 178 | self.theMarketData.MRKT_updateMarketData(self.retrievedTime, self.retrievedPrice) 179 | currentTimeStamp = self.retrievedTime 180 | # Get next sample in memory 181 | [self.retrievedTime, self.retrievedPrice, endOfList] = self.theGDAXControler.GDAX_GetNextHistoricDataSample() 182 | else: 183 | # Interpolate with previous sample value 184 | currentTimeStamp = currentTimeStamp + timeStep 185 | self.theMarketData.MRKT_updateMarketData(currentTimeStamp, self.retrievedPrice) 186 | 187 | #start = time.clock() 188 | #self.theMarketData.MRKT_updateMarketData(self.retrievedTime, self.retrievedPrice) 189 | #print("MRKT_updateMarketData : %s" % (time.clock() - start)) 190 | 191 | nbHistoricSamplesRetrieved = nbHistoricSamplesRetrieved + 1 192 | self.theTrader.TRAD_ProcessDecision() 193 | 194 | # Pause if UIGR is busy 195 | if (nbHistoricSamplesRetrieved > batchSamplesInitGraph): # Init graph phase passed 196 | if (nbHistoricSamplesRetrieved % batchSamplesSizeForSpeed == 0): 197 | #print("UIGR_updateGraphs : %s" % (time.clock() - start)) 198 | #print("E") 199 | while ((self.theUIGraph.UIGR_AreNewSamplesRequested() == False) and (self.simulationStopIsRequested == False)): 200 | #print("pause") 201 | time.sleep(0.002) 202 | 203 | if (self.simulationPauseIsRequested == True): 204 | while ((self.simulationPauseIsRequested == True) and (self.simulationStopIsRequested == False)): 205 | # Simulation is on pause, wait 206 | time.sleep(0.05) 207 | 208 | # End of simulation 209 | print("INDH - Simulation Thread has ended : Stop ? %s End of buffer ? %s" % (self.simulationStopIsRequested, endOfList)) 210 | self.operationalStatus = "Ended" 211 | 212 | def INDH_GetOperationalStatus(self): 213 | return self.operationalStatus 214 | 215 | def INDH_PauseResumeSimulation(self): 216 | if (self.simulationPauseIsRequested == True): 217 | self.simulationPauseIsRequested = False 218 | self.theUIGraph.UIGR_SetPauseButtonAspect("PAUSE") 219 | else: 220 | self.theUIGraph.UIGR_SetPauseButtonAspect("RESUME") 221 | self.simulationPauseIsRequested = True 222 | 223 | def INDH_StopSimulation(self): 224 | if (self.simulationStopIsRequested == True): 225 | self.simulationStopIsRequested = False 226 | else: 227 | self.simulationStopIsRequested = True 228 | print("INDH - StopSimulation: setting simulationStopIsRequested to true") 229 | 230 | def INDH_StopLiveTrading(self): 231 | if (self.liveTradingStopIsRequested == True): 232 | self.liveTradingStopIsRequested = False 233 | else: 234 | self.liveTradingStopIsRequested = True 235 | 236 | def INDH_PerformLiveTradingOperation(self, nbHoursFromNow): 237 | # Security : if historic data is still loading, return Ended 238 | if (self.PreloadHistoricDataStatus == "Ongoing"): 239 | return "Ended" 240 | 241 | # Open background thread to perform simulation 242 | if (self.operationalStatus != "Ongoing"): 243 | self.operationalStatus = "Ongoing" 244 | self.liveTradingStopIsRequested = False 245 | 246 | # Prepare trader 247 | self.theTrader.TRAD_ResetTradingParameters() 248 | 249 | # Prepare UIGR graph updater 250 | self.theUIGraph.UIGR_StartContinuousGraphRefresh(200) 251 | 252 | # Launch simulation thread 253 | self.threadPerformLiveTrading = threading.Timer(0, self.PerformLiveTradingThread) 254 | self.threadPerformLiveTrading.start() 255 | 256 | return "Ongoing" 257 | else: 258 | # Already initiated, should not happen 259 | return "Error" 260 | 261 | 262 | def PerformLiveTradingThread(self): 263 | nbLiveSamplesRetrieved = 0 264 | waitingCounter = 0 265 | timeToWaitInSecGranulaity = 0.1 266 | nbIterationToPassToWaitPeriodTime = (theConfig.CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS / 1000) / timeToWaitInSecGranulaity 267 | 268 | # While user does not want to stop trading 269 | while (self.liveTradingStopIsRequested == False): 270 | 271 | # Retrieve next live sample 272 | self.retrievedPrice = self.theGDAXControler.GDAX_GetRealTimePriceInEUR() 273 | self.retrievedTime = time.time() 274 | 275 | self.theMarketData.MRKT_updateMarketData(self.retrievedTime, self.retrievedPrice) 276 | self.theTrader.TRAD_ProcessDecision() 277 | 278 | nbLiveSamplesRetrieved = nbLiveSamplesRetrieved + 1 279 | 280 | 281 | while ((self.liveTradingStopIsRequested == False) and (waitingCounter < nbIterationToPassToWaitPeriodTime)): 282 | time.sleep(timeToWaitInSecGranulaity) 283 | waitingCounter = waitingCounter + 1 284 | 285 | waitingCounter = 0 286 | 287 | # End of Live Trading 288 | print("INDH - Live Trading Thread has ended : Stop ? %s" % (self.simulationStopIsRequested)) 289 | self.operationalStatus = "Ended" 290 | 291 | def INDH_GetCurrentState(self): 292 | return self.marketPhase 293 | 294 | def getCurrentSpotPrice(self): 295 | return self.theGDAXControler.GDAX_GetRealTimePriceInEUR() 296 | 297 | def getCurrentSubSchedulingFactor(self): 298 | return self.currentSubSchedulingFactor 299 | 300 | def INDH_closeBackgroundOperations(self): 301 | print("INDH - Close background operations requested") 302 | self.abortOperations = True 303 | self.INDH_StopSimulation() 304 | self.INDH_StopLiveTrading() -------------------------------------------------------------------------------- /src/MarketData.py: -------------------------------------------------------------------------------- 1 | from scipy import signal 2 | import numpy as np 3 | import time 4 | import math 5 | 6 | import TradingBotConfig as theConfig 7 | 8 | class MarketData(): 9 | 10 | 11 | MAX_HISTORIC_SAMPLES = 20000 12 | NB_POINTS_FOR_FAST_SMOOTH_FILTER = 600 13 | NB_POINTS_FOR_SLOW_SMOOTH_FILTER = 1200 14 | NB_POINTS_DELAY_FOR_RISK_LINE_COMPUTATION = 220 15 | NB_POINTS_FOR_RISK_LINE_COMPUTATION = 1200 16 | RISK_LINE_START_INDEX = - (NB_POINTS_FOR_RISK_LINE_COMPUTATION + NB_POINTS_DELAY_FOR_RISK_LINE_COMPUTATION) 17 | RISK_LINE_END_INDEX = - (NB_POINTS_DELAY_FOR_RISK_LINE_COMPUTATION) 18 | 19 | maxMACDValuePricePercentageForNormalization = 60 20 | 21 | NB_POINTS_MIN_FOR_ESTABLISHMENT = NB_POINTS_FOR_SLOW_SMOOTH_FILTER 22 | 23 | 24 | def __init__(self, GDAXControler, UIGraph): 25 | 26 | self.theGDAXControler = GDAXControler 27 | self.theUIGraph = UIGraph 28 | 29 | # Init model data 30 | self.MRKT_ResetAllData(1) 31 | self.RefreshSmoothFiltersCoefficients() 32 | 33 | def MRKT_ResetAllData(self, UIGraphSubScheduling): 34 | print("MRKT - Reset all data") 35 | 36 | self.totalNbIterations = 0 37 | self.dataRefTime = [] 38 | self.dataRefCryptoPriceInEUR = [] 39 | self.dataRefSmoothAverageFast = [] 40 | self.dataRefSmoothAverageSlow = [] 41 | self.dataRefRiskLine = [] 42 | self.dataRefMACD = [] 43 | self.UIGraphSubScheduling = UIGraphSubScheduling 44 | 45 | def RefreshSmoothFiltersCoefficients(self): 46 | newSensitvityValue = self.theUIGraph.UIGR_getSensitivityLevelValue() 47 | print("MRKT - Applied coefficients : %s" % newSensitvityValue) 48 | 49 | if (newSensitvityValue == 6): 50 | N = 1 51 | WnFast=float(0.0333) # 1/30 52 | WnSlow=float(0.01) # 1/100 53 | self.maxMACDValuePricePercentageForNormalization = 0.006 54 | elif (newSensitvityValue == 5): 55 | N = 1 56 | WnFast=float(0.01666) # 1/60 57 | WnSlow=float(0.005882) # 1/170 58 | self.maxMACDValuePricePercentageForNormalization = 0.007 59 | elif (newSensitvityValue == 4): 60 | N = 1 61 | WnFast=float(0.010) # 1/80 62 | WnSlow=float(0.0040) # 1/230 63 | self.maxMACDValuePricePercentageForNormalization = 0.008 64 | elif (newSensitvityValue == 3): 65 | N = 1 66 | WnFast=float(0.008) # 1/110 67 | WnSlow=float(0.003) # 1/250 68 | self.maxMACDValuePricePercentageForNormalization = 0.01 69 | elif (newSensitvityValue == 2): 70 | N = 1 71 | WnFast=float(0.0040) # 1/ 72 | WnSlow=float(0.0018) # 1/ 73 | self.maxMACDValuePricePercentageForNormalization = 0.012 74 | elif (newSensitvityValue == 1): 75 | N = 2 76 | WnFast=float(0.01111) # 1/90 77 | WnSlow=float(0.0041667) # 1/240 78 | self.maxMACDValuePricePercentageForNormalization = 0.012 79 | else: # Should not happen 80 | N = 1 81 | WnFast=float(0.0125) # 1/80 82 | WnSlow=float(0.004347) # 1/230 83 | self.maxMACDValuePricePercentageForNormalization = 0.012 84 | 85 | if (self.totalNbIterations > 1): 86 | self.maxMACDForNormalization = self.dataRefCryptoPriceInEUR[1] * self.maxMACDValuePricePercentageForNormalization 87 | else: 88 | self.maxMACDForNormalization = 10000 * self.maxMACDValuePricePercentageForNormalization 89 | 90 | print("MRKT - Coefficients updated. New self.maxMACDForNormalization is %s, WnFast = %s, WnSlow = %s" % (self.maxMACDForNormalization, WnFast, WnSlow)) 91 | 92 | self.bFast, self.aFast = signal.butter(N, float(WnFast), 'low') # One gotcha is that Wn is a fraction of the Nyquist frequency (half the sampling frequency). 93 | self.bSlow, self.aSlow = signal.butter(N, float(WnSlow), 'low') # One gotcha is that Wn is a fraction of the Nyquist frequency (half the sampling frequency). 94 | 95 | def MRKT_AreIndicatorsEstablished(self): 96 | #print("MRKT_AreIndicatorsEstablished - nb it %s minRequested %s" % (self.totalNbIterations,self.MRKT_GetMinNumberOfRequiredSamplesForEstablishment())) 97 | if (self.totalNbIterations > self.NB_POINTS_MIN_FOR_ESTABLISHMENT): 98 | return True 99 | else: 100 | return False 101 | 102 | def MRKT_GetLastRiskLineValue(self): 103 | return self.dataRefRiskLine[-1] 104 | 105 | def MRKT_GetLastMACDValue(self): 106 | return self.dataRefMACD[-1] 107 | 108 | # Used in SImulation mode in order to get the price at which we buy or sell 109 | def MRKT_GetLastRefPrice(self): 110 | return self.dataRefCryptoPriceInEUR[-1] 111 | 112 | def MRKT_GetLastFastSmoothedPrice(self): 113 | return self.dataRefSmoothAverageFast[-1] 114 | 115 | # Needs one sample every 10 sec 116 | def MRKT_updateMarketData(self, newSampleTime, newSamplePrice): 117 | 118 | if (newSampleTime is not None): 119 | if (newSamplePrice is not None): 120 | # Drop old samples (buffers shifts) 121 | self.dropOldData() 122 | 123 | # Add new sample 124 | self.updateMarketPriceAndTime(newSampleTime, newSamplePrice) 125 | 126 | # Update indicators 127 | self.updateFastSmoothAverage() 128 | self.updateSlowSmoothAverage() 129 | self.updatePriceMACD() 130 | self.updateRiskLine() 131 | 132 | # UI Data Update 133 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 134 | self.theUIGraph.UIGR_updateNextIterationData(self.dataRefTime[-1], self.dataRefCryptoPriceInEUR[-1], self.dataRefSmoothAverageFast[-1], self.dataRefSmoothAverageSlow[-1], self.dataRefRiskLine[-1], self.dataRefMACD[-1]) 135 | 136 | if (self.totalNbIterations % 20 == 0): 137 | # Update Smooth filters coefficients if needed. Check value changed in subscheduled part to save CPU 138 | # Last condition is made for calibration of MACD normalization indicator with price data 139 | if ((self.theUIGraph.UIGR_hasSensitivityLevelValueChanged() == True) or (self.totalNbIterations == 20)): 140 | self.RefreshSmoothFiltersCoefficients() 141 | 142 | self.totalNbIterations = self.totalNbIterations + 1 143 | else: 144 | print("MRKT - None Sampleprice detected") 145 | else: 146 | print("MRKT - None Sampletime detected") 147 | 148 | def dropOldData(self): 149 | if (self.totalNbIterations > self.MAX_HISTORIC_SAMPLES): 150 | self.dataRefTime.pop(0) 151 | self.dataRefCryptoPriceInEUR.pop(0) 152 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 153 | self.dataRefSmoothAverageFast.pop(0) 154 | self.dataRefSmoothAverageSlow.pop(0) 155 | self.dataRefRiskLine.pop(0) 156 | self.dataRefMACD.pop(0) 157 | 158 | def updateMarketPriceAndTime(self, newSampleTime, newSamplePrice): 159 | 160 | self.dataRefCryptoPriceInEUR.append(newSamplePrice) 161 | self.dataRefTime.append(newSampleTime) 162 | 163 | # Update price on the UI 164 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 165 | self.theUIGraph.UIGR_updatePriceLbl(round(self.dataRefCryptoPriceInEUR[-1], 5)) 166 | 167 | def updateFastSmoothAverage(self): 168 | if (self.totalNbIterations > self.NB_POINTS_FOR_FAST_SMOOTH_FILTER + 1): 169 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 170 | self.dataRefSmoothAverageFast.append((signal.lfilter(self.bFast, self.aFast, self.dataRefCryptoPriceInEUR[-self.NB_POINTS_FOR_FAST_SMOOTH_FILTER:]))[-1]) 171 | else: 172 | self.dataRefSmoothAverageFast.append(self.dataRefCryptoPriceInEUR[-1]*0.999) 173 | 174 | def updateSlowSmoothAverage(self): 175 | if (self.totalNbIterations > self.NB_POINTS_FOR_SLOW_SMOOTH_FILTER + 1): 176 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 177 | self.dataRefSmoothAverageSlow.append((signal.lfilter(self.bSlow, self.aSlow, self.dataRefCryptoPriceInEUR[-self.NB_POINTS_FOR_SLOW_SMOOTH_FILTER:]))[-1]) 178 | else: 179 | self.dataRefSmoothAverageSlow.append(self.dataRefCryptoPriceInEUR[-1]*0.999) 180 | 181 | def updateRiskLine(self): 182 | if (self.totalNbIterations > self.NB_POINTS_FOR_RISK_LINE_COMPUTATION + 1): 183 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 184 | average = (np.sum(self.dataRefCryptoPriceInEUR[self.RISK_LINE_START_INDEX:self.RISK_LINE_END_INDEX])) / self.NB_POINTS_FOR_RISK_LINE_COMPUTATION 185 | 186 | self.dataRefRiskLine.append(average) 187 | else: 188 | pass # Keep last value 189 | else: 190 | self.dataRefRiskLine.append(0) 191 | 192 | def updatePriceMACD(self): 193 | # Derivate is computed over smooth price data so wait until this one is established 194 | if (self.totalNbIterations > self.NB_POINTS_FOR_SLOW_SMOOTH_FILTER + 2): 195 | if (self.totalNbIterations % self.UIGraphSubScheduling == 0): 196 | localMACD = (self.dataRefSmoothAverageFast[-1] - self.dataRefSmoothAverageSlow[-1]) 197 | self.dataRefMACD.append(localMACD * 100 / (self.maxMACDForNormalization)) 198 | else: 199 | self.dataRefMACD.append(0) 200 | 201 | -------------------------------------------------------------------------------- /src/Notifier.py: -------------------------------------------------------------------------------- 1 | #from twilio.rest import Client 2 | import TradingBotConfig as theConfig 3 | 4 | def SendWhatsappMessage(messageToSend): 5 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 6 | pass 7 | # Your Account Sid and Auth Token from twilio.com/console 8 | # account_sid = '' 9 | # auth_token = '' 10 | # client = Client(account_sid, auth_token) 11 | # 12 | # 13 | # message = client.messages.create( 14 | # body=messageToSend, 15 | # from_='whatsapp:+', 16 | # to='whatsapp:+' 17 | # ) 18 | # 19 | # print("NOTI - Sent message, %s" % message) 20 | -------------------------------------------------------------------------------- /src/Settings.py: -------------------------------------------------------------------------------- 1 | import pickle 2 | import ctypes # Message box popup 3 | 4 | class Settings(object): 5 | ''' 6 | classdocs 7 | ''' 8 | 9 | 10 | def __init__(self): 11 | print("SETT - Constructor") 12 | 13 | # Default settings if no settings file has been saved 14 | self.settings = {"strAPIKey": "", 15 | "strSecretKey": "", 16 | "strPassphrase": "", 17 | "bHasAcceptedConditions": False, 18 | "strTradingPair": "BTC-EUR", 19 | "strFiatType": "EUR", 20 | "strCryptoType": "BTC", 21 | "investPercentage": 90, 22 | "platformTakerFee": 0.5, 23 | "sellTrigger" : 0.0, 24 | "autoSellThreshold": 0.0, 25 | "simulatedFiatBalance": 1000, 26 | "simulationSpeed": 20, 27 | "simulationTimeRange": 24, 28 | } 29 | 30 | self.tradingPairHasChanged = False 31 | self.APIDataHasChanged = False 32 | self.isSettingsFilePresent = False 33 | 34 | self.SETT_LoadSettings() 35 | 36 | def SETT_SaveSettings(self): 37 | print("SETT - Saving settings") 38 | try: 39 | pickle.dump(self.settings, open("astibot.settings", "wb")) 40 | except BaseException as e: 41 | self.MessageBoxPopup("Error during write operation of Astibot settings file. Check that you are running Astibot from a writable directory.", 0) 42 | 43 | self.SETT_DisplayCurrentSettings() 44 | 45 | def SETT_LoadSettings(self): 46 | print("SETT - Loading settings") 47 | try: 48 | self.settings = pickle.load(open("astibot.settings", "rb")) 49 | self.isSettingsFilePresent = True 50 | except BaseException as e: 51 | print("SETT - Exception : " + str(e)) 52 | self.isSettingsFilePresent = False 53 | 54 | 55 | self.SETT_DisplayCurrentSettings() 56 | 57 | def SETT_IsSettingsFilePresent(self): 58 | return self.isSettingsFilePresent 59 | 60 | def SETT_GetSettings(self): 61 | return self.settings 62 | 63 | def SETT_DisplayCurrentSettings(self): 64 | for key, value in self.settings.items(): 65 | print("SETT - %s: %s" % (key, value)) 66 | 67 | def SETT_NotifyTradingPairHasChanged(self): 68 | self.tradingPairHasChanged = True 69 | 70 | def SETT_hasTradingPairChanged(self): 71 | if (self.tradingPairHasChanged == True): 72 | self.tradingPairHasChanged = False 73 | return True 74 | else: 75 | return False 76 | 77 | def SETT_NotifyAPIDataHasChanged(self): 78 | self.APIDataHasChanged = True 79 | 80 | def SETT_hasAPIDataChanged(self): 81 | if (self.APIDataHasChanged == True): 82 | self.APIDataHasChanged = False 83 | print("SETT - API Data has Changed - returning info") 84 | return True 85 | else: 86 | return False 87 | 88 | ## Styles: 89 | ## 0 : OK 90 | ## 1 : OK | Cancel 91 | ## 2 : Abort | Retry | Ignore 92 | ## 3 : Yes | No | Cancel 93 | ## 4 : Yes | No 94 | ## 5 : Retry | No 95 | ## 6 : Cancel | Try Again | Continue 96 | def MessageBoxPopup(self, text, style): 97 | title = "Astibot Settings" 98 | return ctypes.windll.user32.MessageBoxW(0, text, title, style) -------------------------------------------------------------------------------- /src/Trader.py: -------------------------------------------------------------------------------- 1 | import TradingBotConfig as theConfig 2 | import Notifier as theNotifier 3 | import time 4 | 5 | class Trader(object): 6 | 7 | def __init__(self, transactionManager, marketData, UIGraph, Settings): 8 | self.theTransactionManager = transactionManager 9 | self.theMarketData = marketData 10 | self.theUIGraph = UIGraph 11 | # Application settings data instance 12 | self.theSettings = Settings 13 | 14 | self.TRAD_ResetTradingParameters() 15 | 16 | def TRAD_ResetTradingParameters(self): 17 | self.currentState = 'IDLE' 18 | self.nextState = 'IDLE' 19 | 20 | self.currentPriceValue = 0 21 | self.previousMACDValue = 0 22 | self.currentMACDValue = 0 23 | self.currentBuyPriceInFiat = 0 24 | #self.MACDConfirmationCounter = 0 25 | #self.MACDStrength = 0 26 | self.bought = False 27 | self.autoSellSamplesCounter = 0 28 | self.sellTriggerInPercent = self.theSettings.SETT_GetSettings()["sellTrigger"] 29 | self.ongoingBuyOrderWasFree = False 30 | 31 | def TRAD_InitiateNewTradingSession(self, startSession): 32 | self.TRAD_ResetTradingParameters() 33 | self.theTransactionManager.TRNM_InitiateNewTradingSession(startSession) 34 | if (startSession == True): 35 | theNotifier.SendWhatsappMessage("*Astibot: New trading session* started on the %s trading pair!" % self.theSettings.SETT_GetSettings()["strTradingPair"]) 36 | 37 | def TRAD_TerminateTradingSession(self): 38 | self.theTransactionManager.TRNM_TerminateCurrentTradingSession() 39 | 40 | def TRAD_ProcessDecision(self): 41 | 42 | #print("TRAD - Process decision") 43 | 44 | self.nextState = self.currentState 45 | self.updateIndicatorsTransitions() 46 | 47 | if (self.currentState == 'IDLE'): 48 | self.ManageIdleState() 49 | elif (self.currentState == 'WAITING_TO_BUY'): 50 | self.ManageWaitingToBuyState() 51 | elif (self.currentState == 'BUYING'): 52 | self.ManageBuyingState() 53 | elif (self.currentState == 'WAITING_TO_SELL'): 54 | self.ManageWaitingToSellState() 55 | elif (self.currentState == 'SELLING'): 56 | self.ManageSellingState() 57 | else: 58 | self.ManageIdleState() # Error case 59 | 60 | if (self.nextState != self.currentState): 61 | self.currentState = self.nextState 62 | 63 | def TRAD_DEBUG_ForceSell(self): 64 | print("Auto DEBUG SELL >>>>>>>>>>>>>>>>>>>") 65 | #self.theTransactionManager.TRNM_BuyNow() 66 | self.theTransactionManager.TRNM_SellNow(False) 67 | 68 | def TRAD_DEBUG_ForceBuy(self): 69 | print("Auto DEBUG BUY >>>>>>>>>>>>>>>>>>>") 70 | self.theTransactionManager.TRNM_BuyNow() 71 | #self.theTransactionManager.TRNM_SellNow(False) 72 | 73 | def ManageIdleState(self): 74 | # Check transition to WAITING_TO_BUY or WAITING_TO_SELL state 75 | if (self.theMarketData.MRKT_AreIndicatorsEstablished() == True): 76 | # By default, go to WAITING_TO_BUY state 77 | self.nextState = 'WAITING_TO_BUY' 78 | 79 | def updateIndicatorsTransitions(self): 80 | self.currentPriceValue = self.theMarketData.MRKT_GetLastRefPrice() 81 | self.previousMACDValue = self.currentMACDValue 82 | self.currentMACDValue = self.theMarketData.MRKT_GetLastMACDValue() 83 | 84 | 85 | # When MACD indicator is < BUY1 THRESHOLD : No buy signal, do nothing 86 | # When MACD indicator is > BUY1 THRESHOLD and < BUY1 THRESHOLD : Try to place a buy limit order on top of the order book 87 | # When MACD indicator is > BUY2 THRESHOLD : Do a market buy order 88 | # => The limit order mode (betwen B1 and B2 threshold) has not been fully tested. So I recommend to only use market orders. 89 | # For that, set BUY1 THRESHOLD to a value greater than BUY2 THRESHOLD in Config file so that only MACD > B2 THRESHOLD will occur. 90 | def ManageWaitingToBuyState(self): 91 | 92 | #print("ManageWaitingToBuyState") 93 | 94 | isBUY1ThresholdReached = (self.previousMACDValue < theConfig.CONFIG_MACD_BUY_1_THRESHOLD) and (self.currentMACDValue >= theConfig.CONFIG_MACD_BUY_1_THRESHOLD) 95 | isB2ThresholdReached = (self.previousMACDValue < theConfig.CONFIG_MACD_BUY_2_THRESHOLD) and (self.currentMACDValue >= theConfig.CONFIG_MACD_BUY_2_THRESHOLD) 96 | 97 | # Check MACD buy signal 98 | if (isBUY1ThresholdReached): 99 | # Check current price towards risk line 100 | riskLineThresholdInFiat = self.theMarketData.MRKT_GetLastRiskLineValue() * theConfig.CONFIG_RiskLinePercentsAboveThresholdToBuy 101 | if (self.currentPriceValue < riskLineThresholdInFiat): 102 | # Check current price toward maximum allowed buy price 103 | if (self.currentPriceValue < theConfig.CONFIG_MAX_BUY_PRICE): 104 | print("TRAD - ManageWaitingToBuyState: B1 reached, limit buy ordered and successful: Going to Buying state ============================================") 105 | self.theTransactionManager.TRNM_StartBuyOrSellAttempt("BUY", riskLineThresholdInFiat) 106 | self.nextState = 'BUYING' 107 | else: 108 | print("TRAD - ManageWaitingToBuyState: Buy not performed : Price is above max allowed limit - price: " + str(self.currentPriceValue)) 109 | else: 110 | print("TRAD - ManageWaitingToBuyState: Buy not performed : Price is above the risk line") 111 | elif (isB2ThresholdReached): 112 | self.ManageBuyingState() 113 | 114 | 115 | def ManageBuyingState(self): 116 | 117 | #print("ManageBuyingState") 118 | 119 | isB2ThresholdReached = (self.previousMACDValue < theConfig.CONFIG_MACD_BUY_2_THRESHOLD) and (self.currentMACDValue >= theConfig.CONFIG_MACD_BUY_2_THRESHOLD) 120 | 121 | # Is ongoing limit order filled? 122 | currentBuyOrderState = self.theTransactionManager.TRNM_GetOngoingLimitOrderState() 123 | 124 | #print(currentBuyOrderState) 125 | 126 | # if (currentBuyOrderState == "NONE"): 127 | # print("TRAD - Buy limit order canceled: go back to WAITING TO BUY state ============================================") 128 | # self.nextState = 'WAITING_TO_BUY' 129 | # self.theUIGraph.UIGR_updateInfoText("Buy order canceled. Waiting for next buy opportunity", False) 130 | # elif (currentBuyOrderState == "FILLED"): 131 | if (currentBuyOrderState == "FILLED"): 132 | self.ongoingBuyOrderWasFree = True 133 | self.currentBuyPriceInFiat = self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 134 | if (self.sellTriggerInPercent > 0.0): 135 | print("TRAD - ManageBuyingState: Buy Order filled and sellTrigger is set, place sell order and go to SELLING ============================================") 136 | 137 | # In real market conditions, wait for GDAX accounts to be refreshed 138 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 139 | time.sleep(10) 140 | 141 | self.theTransactionManager.TRNM_StartBuyOrSellAttempt("SELL", self.currentBuyPriceInFiat * (1 + (self.sellTriggerInPercent / 100))) 142 | self.nextState = 'SELLING' 143 | else: 144 | print("TRAD - ManageBuyingState: Buy Order filled and sellTrigger is not set, go to WAITING TO SELL") 145 | self.nextState = 'WAITING_TO_SELL' 146 | elif (isB2ThresholdReached): 147 | # MACD crossed the B2 threshold 148 | if (currentBuyOrderState == "MATCHED"): 149 | print("TRAD - ManageBuyingState: B2 reached, canceling ongoing buy order to go to Selling / Waiting to sell state") 150 | # Cancel ongoing order: too risky to maintain it 151 | self.theTransactionManager.TRNM_CancelOngoingOrder() 152 | self.currentBuyPriceInFiat = self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 153 | self.ongoingBuyOrderWasFree = True 154 | if (self.sellTriggerInPercent > 0.0): 155 | print("TRAD - ManageBuyingState: B2 reached, buy Order matched (not filled) and sellTrigger is set, place sell order and go to SELLING ============================================") 156 | self.nextState = 'SELLING' 157 | else: 158 | print("TRAD - ManageBuyingState: B2 reached, buy Order matched (not filled) and sellTrigger is not set, go to WAITING TO SELL ============================================") 159 | self.nextState = 'WAITING_TO_SELL' 160 | else: # Order still ongoing or no limit order performed 161 | # Market buy if enabled and if price is below risk line 162 | # Check current price towards risk line 163 | riskLineThresholdInFiat = self.theMarketData.MRKT_GetLastRiskLineValue() * theConfig.CONFIG_RiskLinePercentsAboveThresholdToBuy 164 | if ((self.currentPriceValue < riskLineThresholdInFiat) and (theConfig.CONFIG_ENABLE_MARKET_ORDERS == True)): 165 | print("TRAD - ManageBuyingState: B2 reached, price below risk, canceling ongoing buy order before sending Market order") 166 | # Cancel ongoing order 167 | self.theTransactionManager.TRNM_CancelOngoingOrder() 168 | # In real market conditions, wait for GDAX accounts to be refreshed 169 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 170 | time.sleep(0.8) 171 | 172 | if (self.theTransactionManager.TRNM_BuyNow() == True): 173 | self.nextState = 'WAITING_TO_SELL' 174 | self.ongoingBuyOrderWasFree = False 175 | self.currentBuyPriceInFiat = self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 176 | 177 | if (self.sellTriggerInPercent > 0.0): 178 | print("TRAD - ManageBuyingState: B2 reached, price below risk, Market Buy ordered and successful, sellTrigger is set, place sell order and go to SELLING ============================================") 179 | 180 | # In real market conditions, wait for GDAX accounts to be refreshed 181 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 182 | time.sleep(10) 183 | 184 | self.theTransactionManager.TRNM_StartBuyOrSellAttempt("SELL", self.currentBuyPriceInFiat * (1 + (self.sellTriggerInPercent / 100))) 185 | self.nextState = 'SELLING' 186 | else: 187 | print("TRAD - ManageBuyingState: B2 reached, price below risk, Market Buy ordered and successful: Going to WAITING_TO_SELL ============================================") 188 | else: 189 | print("TRAD - ManageBuyingState: B2 reached, price below risk, Market Buy ordered and failed: Going to WAITING_TO_BUY ============================================") 190 | self.nextState = 'WAITING_TO_BUY' 191 | else: 192 | # Cancel ongoing order 193 | self.theTransactionManager.TRNM_CancelOngoingOrder() 194 | print("TRAD - ManageBuyingState: B2 reached, no market buy allowed or high risk: don't continue buying, go to WAITING_TO_BUY ============================================") 195 | else: 196 | # B1 < MACD < B2 197 | # If MACD < B1, order shall have been filled 198 | pass 199 | 200 | 201 | 202 | # When MACD indicator is > SELL1 THRESHOLD : No sell signal, do nothing 203 | # When MACD indicator is < SELL1 THRESHOLD and > SELL2 THRESHOLD : Try to place a sell limit order on top of the order book 204 | # When MACD indicator is < SELL2 THRESHOLD : Do a market sell order 205 | # => The limit order mode (betwen S1 and S2 threshold) has not been fully tested. So I recommend to only use market orders. 206 | # To do that, set SELL1 THRESHOLD to a value greater than SELL2 THRESHOLD in TradingBotConfig file so that only MACD < S2 THRESHOLD will occur. 207 | def ManageWaitingToSellState(self): 208 | 209 | #print("ManageWaitingToSellState") 210 | 211 | currentMidMarketPrice = self.theMarketData.MRKT_GetLastRefPrice() 212 | risingRatio = float(currentMidMarketPrice) / self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 213 | isS1ThresholdReached = (self.previousMACDValue > theConfig.CONFIG_MACD_SELL_1_THRESHOLD) and (self.currentMACDValue <= theConfig.CONFIG_MACD_SELL_1_THRESHOLD) 214 | isAutoSellThresholdReached = risingRatio < (1 - (self.theSettings.SETT_GetSettings()["autoSellThreshold"]/100)) 215 | isS2ThresholdReached = (self.previousMACDValue > theConfig.CONFIG_MACD_SELL_2_THRESHOLD) and (self.currentMACDValue <= theConfig.CONFIG_MACD_SELL_2_THRESHOLD) 216 | 217 | 218 | #isS1ThresholdReached = True 219 | 220 | # Compute min ratio to sell without loss 221 | if (self.ongoingBuyOrderWasFree == True): 222 | minProfitRatio = theConfig.CONFIG_MIN_PRICE_ELEVATION_RATIO_TO_SELL + 0*float(self.theSettings.SETT_GetSettings()["platformTakerFee"])*0.01 223 | else: 224 | minProfitRatio = theConfig.CONFIG_MIN_PRICE_ELEVATION_RATIO_TO_SELL + 1*float(self.theSettings.SETT_GetSettings()["platformTakerFee"])*0.01 225 | 226 | # If price is high enough to sell with no loss (covers tax fees + minimum profit ratio) 227 | if (risingRatio > minProfitRatio): 228 | # Check MACD sell signal 229 | if (isS1ThresholdReached): 230 | print("TRAD - ManageWaitingToSellState: S1 crossed and rising ratio is OK to sell. Placing limit sell order.") 231 | self.theTransactionManager.TRNM_StartBuyOrSellAttempt("SELL", minProfitRatio * float(currentMidMarketPrice)) 232 | self.nextState = 'SELLING' 233 | elif (isS2ThresholdReached): # Market sell 234 | self.ManageSellingState() 235 | else: 236 | pass 237 | # Price high enough to sell but no MACD cross 238 | # If auto-sell threshold is reached, market sell now 239 | elif ((isAutoSellThresholdReached) and (self.theSettings.SETT_GetSettings()["autoSellThreshold"] != 0.0)): 240 | if (self.theTransactionManager.TRNM_SellNow(True) == True): 241 | self.nextState = 'WAITING_TO_BUY' 242 | self.ongoingBuyOrderWasFree = False 243 | print("TRAD - ManageWaitingToSellState: Auto-sell Threshold reached, Market Sell ordered and successful: Going to WAITING_TO_BUY ============================================") 244 | else: 245 | print("TRAD - ManageWaitingToSellState: Auto-sell Threshold reached, Market Sell order FAILED, staying in WAITING_TO_SELL state ============================================") 246 | 247 | 248 | 249 | def ManageSellingState(self): 250 | 251 | #print("ManageSellingState") 252 | 253 | # Is ongoing limit order filled? 254 | currentSellOrderState = self.theTransactionManager.TRNM_GetOngoingLimitOrderState() 255 | isS2ThresholdReached = (self.previousMACDValue > theConfig.CONFIG_MACD_SELL_2_THRESHOLD) and (self.currentMACDValue <= theConfig.CONFIG_MACD_SELL_2_THRESHOLD) 256 | # Is auto-sell threshold reached? 257 | currentMidMarketPrice = self.theMarketData.MRKT_GetLastRefPrice() 258 | 259 | # Buy ongoing 260 | if (self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() > 0): 261 | risingRatio = float(currentMidMarketPrice) / self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 262 | isAutoSellThresholdReached = risingRatio < (1 - (self.theSettings.SETT_GetSettings()["autoSellThreshold"]/100)) 263 | else: 264 | # No buy ongoing, for example a sell trigger limit order has filled so currentBuyInitialPrice has been reset 265 | isAutoSellThresholdReached = False 266 | 267 | #if (currentSellOrderState == "NONE"): 268 | # print("TRAD - ManageSellingState: Sell limit order canceled: go back to WAITING TO SELL state ============================================") 269 | # self.nextState = 'WAITING_TO_SELL' 270 | # self.theUIGraph.UIGR_updateInfoText("Sell order canceled. Waiting for next sell opportunity", False) 271 | #elif (currentSellOrderState == "FILLED"): 272 | if (currentSellOrderState == "FILLED"): 273 | print("TRAD - ManageSellingState: Ongoing sell limit order filled: going to WAITING TO BUY ============================================") 274 | self.nextState = 'WAITING_TO_BUY' 275 | elif (isS2ThresholdReached): 276 | if (currentSellOrderState == "MATCHED"): 277 | print("TRAD - ManageSellingState: Ongoing sell limit order matched: waiting to complete fill") 278 | pass # Do nothing, wait for complete sell 279 | else: # Order is still ongoing 280 | if (theConfig.CONFIG_ENABLE_MARKET_ORDERS == True): 281 | currentMidMarketPrice = self.theMarketData.MRKT_GetLastRefPrice() 282 | risingRatio = float(currentMidMarketPrice) / self.theTransactionManager.TRNM_GetCurrentBuyInitialPrice() 283 | 284 | # Compute min ratio to sell without loss 285 | if (self.ongoingBuyOrderWasFree == True): 286 | # Count sell order price only, buy was free 287 | minProfitRatio = theConfig.CONFIG_MIN_PRICE_ELEVATION_RATIO_TO_SELL + 1*float(self.theSettings.SETT_GetSettings()["platformTakerFee"])*0.01 288 | else: 289 | minProfitRatio = theConfig.CONFIG_MIN_PRICE_ELEVATION_RATIO_TO_SELL + 2*float(self.theSettings.SETT_GetSettings()["platformTakerFee"])*0.01 290 | 291 | # If profit is positive, market sell 292 | if (risingRatio > minProfitRatio): 293 | print("TRAD - ManageSellingState: S2 reached, price below risk, canceling ongoing sell order before sending Market order") 294 | # Cancel ongoing order: too risky to maintain it 295 | self.theTransactionManager.TRNM_CancelOngoingOrder() 296 | # In real market conditions, wait for GDAX accounts to be refreshed 297 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 298 | time.sleep(0.3) 299 | 300 | if (self.theTransactionManager.TRNM_SellNow(False) == True): 301 | self.nextState = 'WAITING_TO_BUY' 302 | self.ongoingBuyOrderWasFree = False 303 | print("TRAD - ManageSellingState: S2 reached, rising ratio profitable, Market Sell ordered and successful: Going to WAITING_TO_BUY ============================================") 304 | else: 305 | print("TRAD - ManageSellingState: S2 reached, rising ratio profitable, sell market order FAILED, staying in SELLING state ============================================") 306 | else: 307 | pass 308 | # Price is not high enough to market sell 309 | else: 310 | pass 311 | # Market orders not allowed. Only try to sell with limit orders 312 | elif ((isAutoSellThresholdReached) and (self.theSettings.SETT_GetSettings()["autoSellThreshold"] != 0.0)): 313 | print("TRAD - ManageSellingState: Auto-sell Threshold reached, canceling ongoing limit order and performing market sell") 314 | 315 | self.theTransactionManager.TRNM_CancelOngoingOrder() 316 | 317 | # In real market conditions, wait for GDAX accounts to be refreshed 318 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 319 | time.sleep(0.4) 320 | 321 | if (self.theTransactionManager.TRNM_SellNow(True) == True): 322 | self.nextState = 'WAITING_TO_BUY' 323 | self.ongoingBuyOrderWasFree = False 324 | print("TRAD - ManageSellingState: Auto-sell Threshold reached, Market Sell ordered and successful: Going to WAITING_TO_BUY ============================================") 325 | else: 326 | print("TRAD - ManageSellingState: Auto-sell Threshold reached, Market Sell order FAILED, staying in SELLING state ============================================") 327 | 328 | 329 | -------------------------------------------------------------------------------- /src/TradingBotConfig.py: -------------------------------------------------------------------------------- 1 | CONFIG_VERSION = "1.3.1" 2 | 3 | ##################################################################################################################### 4 | ######## Operational Parameters 5 | ##################################################################################################################### 6 | 7 | # False: CSV file input. True: Real-time market price 8 | CONFIG_INPUT_MODE_IS_REAL_MARKET = True 9 | 10 | # Main ticker : Retrieves the next samples and processes them 11 | CONFIG_MAIN_TICK_DURATION_IN_MS = 200 12 | 13 | # Terrestrial time between two retrieved sample. 14 | #Should be equal to CONFIG_MAIN_TICK_DURATION_IN_MS in live mode, custom value in simulation mode that 15 | # depends on the csv file sampling time 16 | CONFIG_TIME_BETWEEN_RETRIEVED_SAMPLES_IN_MS = 10000 17 | 18 | # UI Graph refresh per call to the main ticker 19 | CONFIG_UI_GRAPH_UPDATE_SUBSCHEDULING = 1 20 | 21 | # True to record price in output csv file. For the live mode only 22 | CONFIG_RECORD_PRICE_DATA_TO_CSV_FILE = False 23 | 24 | # True to enable real buy and sell transaction to the market 25 | CONFIG_ENABLE_REAL_TRANSACTIONS = True 26 | 27 | # Number of hours of historical samples to retrieve 28 | NB_HISTORIC_DATA_HOURS_TO_PRELOAD_FOR_TRADING = 10 29 | 30 | NB_SECONDS_THRESHOLD_FROM_NOW_FOR_RELOADING_DATA = 1000 31 | 32 | CONFIG_NB_POINTS_LIVE_TRADING_GRAPH = 2500 33 | CONFIG_NB_POINTS_SIMU_GRAPH = 620 34 | CONFIG_NB_POINTS_INIT_SIMU_GRAPH = CONFIG_NB_POINTS_SIMU_GRAPH 35 | 36 | # Quantum = 0.05 (%) 37 | CONFIG_PLATFORM_TAKER_FEE_QUANTUM = 0.05 # 0.05 % 38 | CONFIG_PLATFORM_TAKER_FEE_DEFAULT_VALUE = 5 # 0.25 % 39 | CONFIG_PLATFORM_TAKER_FEE_MIN_ON_SLIDER = 0 # 0 % 40 | CONFIG_PLATFORM_TAKER_FEE_MAX_ON_SLIDER = 40 # 2 % 41 | 42 | # Quantum = 0.05 (%) 43 | CONFIG_SELL_TRIGGER_PERCENTAGE_QUANTUM = 0.05 # 0.05 % 44 | CONFIG_SELL_TRIGGER_PERCENTAGE_DEFAULT_VALUE = 0 # 0.0 % 45 | CONFIG_SELL_TRIGGER_PERCENTAGE_MIN_ON_SLIDER = 0 # 0 % 46 | CONFIG_SELL_TRIGGER_PERCENTAGE_MAX_ON_SLIDER = 40 # 2 % 47 | 48 | # Quantum = 0.25 (%) 49 | CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_QUANTUM = 0.25 # 0.25 % 50 | CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_DEFAULT_VALUE = 0 # 0 % 51 | CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_MIN_ON_SLIDER = 0 # 0 % 52 | CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_MAX_ON_SLIDER = 40 # 10 % 53 | 54 | CONFIG_SIMU_INITIAL_BALANCE_MIN = 0.001 55 | CONFIG_SIMU_INITIAL_BALANCE_MAX = 20000 56 | 57 | CONFIG_MIN_INITIAL_FIAT_BALANCE_TO_TRADE = 0.0001 58 | 59 | CONFIG_DONATION_DEFAULT_AMOUNT_IN_BTC = 0.0002 60 | CONFIG_BTC_DESTINATION_ADDRESS = "136wzpD2fYFRAAHLU5yVxiMNcARQtktoDo" 61 | 62 | 63 | ##################################################################################################################### 64 | ######## Trading Parameters 65 | ##################################################################################################################### 66 | CONFIG_RISK_LINE_PERCENTS_ABOVE_THRESHOLD_TO_BUY_MIN = 0.97 67 | CONFIG_RISK_LINE_PERCENTS_ABOVE_THRESHOLD_TO_BUY_MAX = 1.02 68 | CONFIG_RiskLinePercentsAboveThresholdToBuy = 0.994 69 | 70 | CONFIG_MAX_BUY_PRICE = 100000 71 | 72 | 73 | # Buy policy: 74 | # When MACD indicator is < BUY1 THRESHOLD : No buy signal, do nothing 75 | # When MACD indicator is > BUY1 THRESHOLD and < BUY1 THRESHOLD : Try to place a buy limit order on top of the order book 76 | # When MACD indicator is > BUY2 THRESHOLD : Do a market buy order 77 | # => The limit order mode (betwen B1 and B2 threshold) has not been fully tested. So I recommend to only use market orders. 78 | # For that, set BUY1 THRESHOLD to a value greater than BUY2 THRESHOLD in Config file so that only MACD > B2 THRESHOLD will occur. 79 | CONFIG_MACD_BUY_1_THRESHOLD = 999 80 | CONFIG_MACD_BUY_2_THRESHOLD = 0 81 | 82 | 83 | # Sell policy: 84 | # When MACD indicator is > SELL1 THRESHOLD : No sell signal, do nothing 85 | # When MACD indicator is < SELL1 THRESHOLD and > SELL2 THRESHOLD : Try to place a sell limit order on top of the order book 86 | # When MACD indicator is < SELL2 THRESHOLD : Do a market sell order 87 | # => The limit order mode (betwen S1 and S2 threshold) has not been fully tested. So I recommend to only use market orders. 88 | # To do that, set SELL1 THRESHOLD to a value greater than SELL2 THRESHOLD in TradingBotConfig file so that only MACD < S2 THRESHOLD will occur. 89 | CONFIG_MACD_SELL_1_THRESHOLD = -999 90 | CONFIG_MACD_SELL_2_THRESHOLD = 0 91 | 92 | # A bit too approximative 93 | MIN_CRYPTO_AMOUNT_REQUESTED_TO_SELL = 0.0005 94 | 95 | # Minimum percentage ratio to sell with no loss : shall not include fees 96 | CONFIG_MIN_PRICE_ELEVATION_RATIO_TO_SELL = 1.0005 97 | 98 | # Orders policy : 'MAKER' or 'TAKER' 99 | CONFIG_ENABLE_MARKET_ORDERS = True 100 | 101 | # Percentage of the highest ask price to set buy price 102 | CONFIG_LIMIT_BUY_PRICE_RADIO_TO_HIGHEST_ASK = 0.999 103 | 104 | # Percentage of the highest ask price to set buy price 105 | CONFIG_LIMIT_BUY_PRICE_RATIO_TO_HIGHEST_ASK = 1.0000 106 | 107 | # Percentage of the lowest ask price to set buy price 108 | CONFIG_LIMIT_SELL_PRICE_RATIO_TO_HIGHEST_BID = 1.0000 109 | 110 | # Crypto price quantum. Useful for rounds 111 | CONFIG_CRYPTO_PRICE_QUANTUM = 0.0000001 112 | 113 | # Number of confirmed samples below auto-sell threshold to actually perform auto sell 114 | CONFIG_AUTO_SELL_NB_CONFIRMATION_SAMPLES = 10 115 | 116 | 117 | ##################################################################################################################### 118 | ######## Debug Parameters 119 | ##################################################################################################################### 120 | CONFIG_DEBUG_ENABLE_DUMMY_WITHDRAWALS = False -------------------------------------------------------------------------------- /src/UIDonation.py: -------------------------------------------------------------------------------- 1 | import math 2 | from pyqtgraph.Qt import QtCore, QtGui 3 | from PyQt5.QtWidgets import QFrame 4 | from PyQt5.Qt import QIntValidator 5 | from PyQt5.Qt import QDoubleValidator 6 | import ctypes # Message box popup 7 | 8 | import TradingBotConfig as theConfig 9 | 10 | class UIDonation(QtGui.QWidget): 11 | 12 | STR_CHECKBOX_AUTHORIZATION_TEXT = "I accept to give the present software a full control of my account through the Application Programming Interface. It includes algorithm-based buying or selling of fiat or cryptocurrency money / assets. I understand the risks related to software-based trading and, by entering here my personal API keys access, I am the only responsible for the totality of every action that is performed by this software through the API system even in case of bug, undesired software behavior, unfavorable market, inappropriate buy or sell decision. I have trained myself in Simulation mode to understand the Software trading strategy and, by entering my API keys, I only give control to money / assets that I can afford to loose." 13 | 14 | STR_BORDER_BLOCK_STYLESHEET = "QWidget {background-color : #151f2b;}" 15 | STR_QLABEL_STYLESHEET = "QLabel { background-color : #203044; color : white; font: 13px;}" 16 | STR_QLABEL_GREEN_STYLESHEET = "QLabel { background-color : #203044; color : #24b62e; font: bold 14px;}" 17 | STR_QLABEL_RED_STYLESHEET = "QLabel { background-color : #203044; color : #ff2e2e; font: bold 14px;}" 18 | STR_QLABEL_SMALL_STYLESHEET = "QLabel { background-color : #203044; color : #C2C2C2; font: 11px;}" 19 | STR_QCHECKBOX_STYLESHEET = "QCheckBox { background-color : #203044; color : white; font: 10px;}" 20 | STR_QCHECKBOX_LABEL_STYLESHEET = "QLabel { background-color : #203044; color : #C2C2C2; font: 10px;}" 21 | STR_QLABEL_TITLE_STYLESHEET = "QLabel { background-color : #203044; color : #81C6FE; font: bold 16px;}" 22 | STR_QFRAME_SEPARATOR_STYLESHEET = "background-color: rgb(20, 41, 58);" 23 | STR_QBUTTON_CLOSE_STYLESHEET = "QPushButton {background-color: #01599e; border-width: 2px; border-radius: 10px; border-color: white; font: bold 15px; color:white} QPushButton:pressed { background-color: #1d8d24 } QPushButton:hover { background-color: #002c4f }" 24 | STR_QBUTTON_WITHDRAW_ENABLED_STYLESHEET = "QPushButton {background-color: #23b42c; border-width: 2px; border-radius: 10px; border-color: white; font: bold 13px; color:white} QPushButton:pressed { background-color: #1d8d24 } QPushButton:hover { background-color: #1a821f }" 25 | STR_QBUTTON_WITHDRAW_DISABLED_STYLESHEET = "QPushButton {background-color: #9f9f9f; border-width: 2px; border-radius: 10px; border-color: white; font: bold 13px; color:white}" 26 | STR_QTEXTEDIT_STYLESHEET = "QLineEdit { background-color : #203044; color : white; font: bold 13px; border: 1px solid white; border-radius: 4px;} QLineEdit:focus {border: 2px solid #007ad9;}" 27 | 28 | RIGHT_LABELS_WIDTH_IN_PX = 75 29 | 30 | def __init__(self, settings): 31 | # Here, you should call the inherited class' init, which is QDialog 32 | QtGui.QWidget.__init__(self) 33 | 34 | print("UIDO - UI Donating constructor") 35 | 36 | # Application settings data instance 37 | self.theSettings = settings 38 | 39 | # Functional 40 | self.BTCBalance = -1.0 41 | self.windowIsShown = False 42 | self.timerRefreshBTCBalance = QtCore.QTimer() 43 | self.timerRefreshBTCBalance.timeout.connect(self.TimerRaisedRefreshBTCBalance) 44 | self.timerRefreshBTCBalance.start(200) 45 | self.withdrawHasBeenPerformed = False 46 | 47 | # Window settings 48 | self.setWindowModality(QtCore.Qt.ApplicationModal) 49 | self.setWindowTitle('Astibot') 50 | self.setStyleSheet("background-color:#203044;") 51 | self.setWindowIcon(QtGui.QIcon("AstibotIcon.png")) 52 | self.setAutoFillBackground(True); 53 | self.setFixedSize(450, 350) 54 | 55 | # Build layout 56 | self.BuildWindowLayout() 57 | 58 | def EventWithdrawButtonClick(self): 59 | print("UIDO - Withdraw Click") 60 | 61 | # Set to True to keep Withdraw button disabled during transaction 62 | self.withdrawHasBeenPerformed = True 63 | self.SetWithdrawEnabled(False) 64 | self.btnWithdrawForDonating.setText("Withdrawing...") 65 | QtGui.QApplication.processEvents() # Force UI to update previous lines, because we will block main UI loop 66 | 67 | # Perform withdraw 68 | withdrawRequestReturn = self.theTransactionManager.TRNM_WithdrawBTC(theConfig.CONFIG_BTC_DESTINATION_ADDRESS, float(self.txtDonationAmountEntry.text())) 69 | 70 | if (withdrawRequestReturn != "Error"): 71 | self.btnWithdrawForDonating.setText("Withdraw successful!") 72 | self.MessageBoxPopup("Your donation has been successfully sent: Thank you! Coinbase Pro Transfer ID is %s" % withdrawRequestReturn, 0) 73 | else: 74 | self.MessageBoxPopup("The withdraw failed, you will not be charged. Make sure you authorized the transfer feature when creating your API key.", 0) 75 | self.btnWithdrawForDonating.setText("Withdraw failed") 76 | 77 | 78 | def EventCloseButtonClick(self): 79 | print("UIDO - Close Click") 80 | self.HideWindow() 81 | 82 | def TimerRaisedRefreshBTCBalance(self): 83 | if (self.windowIsShown == True): 84 | # Retrieve balance data 85 | self.BTCBalance = self.theTransactionManager.TRNM_getBTCBalance() 86 | # Fast account refresh required in case the user would currently be withdrawing money, he would like to quickly see the update on the UI 87 | self.theTransactionManager.TRNM_ForceAccountsUpdate() 88 | 89 | try: 90 | if (float(self.BTCBalance) >= float(self.txtDonationAmountEntry.text()) and (float(self.txtDonationAmountEntry.text()) >= theConfig.MIN_CRYPTO_AMOUNT_REQUESTED_TO_SELL)): 91 | # If donation has just been performed, do not enable Withdraw button again 92 | if (self.withdrawHasBeenPerformed == False): 93 | self.SetWithdrawEnabled(True) 94 | self.lblAvailableBTCBalance.setText("%s BTC" % str(round(float(self.BTCBalance), 7))) 95 | else: 96 | self.SetWithdrawEnabled(False) 97 | self.lblAvailableBTCBalance.setText("%s BTC" % str(round(float(self.BTCBalance), 7))) 98 | self.btnWithdrawForDonating.setText("Donate %s BTC" % self.txtDonationAmountEntry.text()) 99 | except ValueError: 100 | self.SetWithdrawEnabled(False) 101 | 102 | 103 | def SetWithdrawEnabled(self, bEnable): 104 | if (bEnable == True): 105 | self.btnWithdrawForDonating.setStyleSheet(self.STR_QBUTTON_WITHDRAW_ENABLED_STYLESHEET) 106 | else: 107 | self.btnWithdrawForDonating.setStyleSheet(self.STR_QBUTTON_WITHDRAW_DISABLED_STYLESHEET) 108 | 109 | self.btnWithdrawForDonating.setEnabled(bEnable) 110 | 111 | ## Styles: 112 | ## 0 : OK 113 | ## 1 : OK | Cancel 114 | ## 2 : Abort | Retry | Ignore 115 | ## 3 : Yes | No | Cancel 116 | ## 4 : Yes | No 117 | ## 5 : Retry | No 118 | ## 6 : Cancel | Try Again | Continue 119 | def MessageBoxPopup(self, text, style): 120 | title = "Astibot Donating" 121 | return ctypes.windll.user32.MessageBoxW(0, text, title, style) 122 | 123 | def BuildWindowLayout(self): 124 | self.rootGridLayout = QtGui.QGridLayout() 125 | self.rootGridLayout.setContentsMargins(0, 0, 0, 0) 126 | self.mainGridLayout = QtGui.QGridLayout() 127 | self.mainGridLayout.setContentsMargins(0, 0, 0, 0) 128 | self.setLayout(self.rootGridLayout) 129 | self.rootGridLayout.addLayout(self.mainGridLayout, 1, 1) 130 | rowNumber = 0 131 | 132 | # Root left and right 133 | self.rootLeftBlock = QtGui.QWidget() 134 | self.rootLeftBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 135 | self.rootLeftBlock.setFixedWidth(20) 136 | self.rootRightBlock = QtGui.QWidget() 137 | self.rootRightBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 138 | self.rootRightBlock.setFixedWidth(20) 139 | self.rootGridLayout.addWidget(self.rootLeftBlock, 0, 0, 3, 1) 140 | self.rootGridLayout.addWidget(self.rootRightBlock, 0, 2, 3, 1) 141 | 142 | # Root top and bottom 143 | self.rootTopBlock = QtGui.QWidget() 144 | self.rootTopBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 145 | self.rootTopBlock.setFixedHeight(20) 146 | self.rootBottomBlock = QtGui.QWidget() 147 | self.rootBottomBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 148 | self.rootBottomBlock.setFixedHeight(60) 149 | self.rootGridLayout.addWidget(self.rootTopBlock, 0, 0, 1, 3) 150 | self.rootGridLayout.addWidget(self.rootBottomBlock, 2, 0, 1, 3) 151 | 152 | # Body layout =========================================================== 153 | 154 | self.lblTitleDonating = QtGui.QLabel("Donate & Contribute to Astibot project") 155 | 156 | 157 | self.lblTitleDonating.setStyleSheet(self.STR_QLABEL_TITLE_STYLESHEET); 158 | self.mainGridLayout.addWidget(self.lblTitleDonating, rowNumber, 0, 1, 2) 159 | rowNumber = rowNumber + 1 160 | 161 | self.lblSubTitleDonating = QtGui.QLabel("If you like this project or if you make money with it: please donate to help me make this software better!") 162 | self.lblSubTitleDonating.setStyleSheet(self.STR_QLABEL_STYLESHEET); 163 | self.lblSubTitleDonating.setWordWrap(True) 164 | self.mainGridLayout.addWidget(self.lblSubTitleDonating, rowNumber, 0, 1, 2) 165 | rowNumber = rowNumber + 1 166 | 167 | # Available BTC Balance 168 | self.lblAvailableBTCBalanceText = QtGui.QLabel("Available BTC Balance:") 169 | self.lblAvailableBTCBalanceText.setStyleSheet(self.STR_QLABEL_STYLESHEET); 170 | self.lblAvailableBTCBalanceText.setFixedHeight(28) 171 | if (self.BTCBalance >= 0): 172 | if (self.BTCBalance >= theConfig.CONFIG_DONATION_DEFAULT_AMOUNT_IN_BTC): 173 | self.lblAvailableBTCBalance = QtGui.QLabel("%s BTC" % str(round(float(self.BTCBalance)))) 174 | else: 175 | self.lblAvailableBTCBalance = QtGui.QLabel("%s BTC (insufficient funds)" % str(round(float(self.BTCBalance)))) 176 | else: 177 | self.lblAvailableBTCBalance = QtGui.QLabel("-- BTC") 178 | self.lblAvailableBTCBalance.setStyleSheet(self.STR_QLABEL_STYLESHEET); 179 | self.mainGridLayout.addWidget(self.lblAvailableBTCBalanceText, rowNumber, 0) 180 | self.mainGridLayout.addWidget(self.lblAvailableBTCBalance, rowNumber, 1) 181 | rowNumber = rowNumber + 1 182 | 183 | # Donation amount entry 184 | self.lblYourDonation = QtGui.QLabel("Your donation (BTC):") 185 | self.lblYourDonation.setStyleSheet(self.STR_QLABEL_STYLESHEET); 186 | self.lblYourDonation.setFixedHeight(28) 187 | 188 | self.txtDonationAmountEntry = QtGui.QLineEdit() 189 | self.txtDonationAmountEntry.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 190 | self.txtDonationAmountEntry.setFixedWidth(80) 191 | self.txtDonationAmountEntry.setText(str(theConfig.CONFIG_DONATION_DEFAULT_AMOUNT_IN_BTC)) 192 | #self.txtDonationAmountEntry.changeEvent.connect(self.EventDonationAmountEntryChanged) 193 | 194 | self.mainGridLayout.addWidget(self.lblYourDonation, rowNumber, 0) 195 | self.mainGridLayout.addWidget(self.txtDonationAmountEntry, rowNumber, 1) 196 | rowNumber = rowNumber + 1 197 | 198 | 199 | # Withdraw button 200 | self.btnWithdrawForDonating = QtGui.QPushButton("Donate %s BTC" % theConfig.CONFIG_DONATION_DEFAULT_AMOUNT_IN_BTC) 201 | self.btnWithdrawForDonating.setStyleSheet(self.STR_QBUTTON_WITHDRAW_DISABLED_STYLESHEET) 202 | self.btnWithdrawForDonating.setFixedHeight(35) 203 | self.btnWithdrawForDonating.setFixedWidth(240) 204 | self.btnWithdrawForDonating.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 205 | self.btnWithdrawForDonating.clicked.connect(self.EventWithdrawButtonClick) 206 | self.mainGridLayout.addWidget(self.btnWithdrawForDonating, rowNumber, 0, 1, 2, QtCore.Qt.AlignCenter) 207 | rowNumber = rowNumber + 1 208 | 209 | # Bottom buttons 210 | self.btnClose = QtGui.QPushButton("Close") 211 | self.btnClose.setStyleSheet(self.STR_QBUTTON_CLOSE_STYLESHEET) 212 | self.btnClose.setFixedWidth(120) 213 | self.btnClose.setFixedHeight(38) 214 | self.btnClose.clicked.connect(self.EventCloseButtonClick) 215 | self.btnClose.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 216 | self.hBoxBottomButtons = QtGui.QHBoxLayout() 217 | self.hBoxBottomButtons.addWidget(self.btnClose, QtCore.Qt.AlignRight) 218 | self.rootBottomBlock.setLayout(self.hBoxBottomButtons) 219 | rowNumber = rowNumber + 1 220 | 221 | 222 | def UIDO_ShowWindow(self): 223 | print("UIDO - Show") 224 | self.windowIsShown = True 225 | self.withdrawHasBeenPerformed = False 226 | 227 | # Force refresh 228 | self.TimerRaisedRefreshBTCBalance() 229 | 230 | self.show() 231 | 232 | def HideWindow(self): 233 | self.windowIsShown = False 234 | self.hide() 235 | 236 | def UIDO_SetTransactionManager(self, transactionManager): 237 | self.theTransactionManager = transactionManager 238 | -------------------------------------------------------------------------------- /src/UIInfo.py: -------------------------------------------------------------------------------- 1 | import math 2 | from pyqtgraph.Qt import QtCore, QtGui 3 | from PyQt5.QtWidgets import QFrame 4 | from PyQt5.Qt import QIntValidator 5 | from PyQt5.Qt import QDoubleValidator 6 | import ctypes # Message box popup 7 | 8 | import TradingBotConfig as theConfig 9 | 10 | class UIInfo(QtGui.QWidget): 11 | 12 | STR_CHECKBOX_AUTHORIZATION_TEXT = "I accept to give the present software a full control of my account through the Application Programming Interface. It includes algorithm-based buying or selling of fiat or cryptocurrency money / assets. I understand the risks related to software-based trading and, by entering here my personal API keys access, I am the only responsible for the totality of every action that is performed by this software through the API system even in case of bug, undesired software behavior, unfavorable market, inappropriate buy or sell decision. I have trained myself in Simulation mode to understand the Software trading strategy and, by entering my API keys, I only give control to money / assets that I can afford to loose." 13 | 14 | STR_BORDER_BLOCK_STYLESHEET = "QWidget {background-color : #151f2b;}" 15 | STR_QLABEL_STYLESHEET = "QLabel { background-color : #203044; color : white; font: 13px;}" 16 | STR_QLABEL_GREEN_STYLESHEET = "QLabel { background-color : #203044; color : #24b62e; font: bold 14px;}" 17 | STR_QLABEL_RED_STYLESHEET = "QLabel { background-color : #203044; color : #ff2e2e; font: bold 14px;}" 18 | STR_QLABEL_SMALL_STYLESHEET = "QLabel { background-color : #203044; color : #C2C2C2; font: 11px;}" 19 | STR_QCHECKBOX_STYLESHEET = "QCheckBox { background-color : #203044; color : white; font: 10px;}" 20 | STR_QCHECKBOX_LABEL_STYLESHEET = "QLabel { background-color : #203044; color : #C2C2C2; font: 10px;}" 21 | STR_QLABEL_TITLE_STYLESHEET = "QLabel { background-color : #203044; color : #81C6FE; font: bold 16px;}" 22 | STR_QFRAME_SEPARATOR_STYLESHEET = "background-color: rgb(20, 41, 58);" 23 | STR_QBUTTON_CLOSE_STYLESHEET = "QPushButton {background-color: #01599e; border-width: 2px; border-radius: 10px; border-color: white; font: bold 15px; color:white} QPushButton:pressed { background-color: #1d8d24 } QPushButton:hover { background-color: #002c4f }" 24 | STR_QBUTTON_WITHDRAW_ENABLED_STYLESHEET = "QPushButton {background-color: #23b42c; border-width: 2px; border-radius: 10px; border-color: white; font: bold 13px; color:white} QPushButton:pressed { background-color: #1d8d24 } QPushButton:hover { background-color: #1a821f }" 25 | STR_QBUTTON_WITHDRAW_DISABLED_STYLESHEET = "QPushButton {background-color: #9f9f9f; border-width: 2px; border-radius: 10px; border-color: white; font: bold 13px; color:white}" 26 | 27 | RIGHT_LABELS_WIDTH_IN_PX = 75 28 | 29 | def __init__(self): 30 | # Here, you should call the inherited class' init, which is QDialog 31 | QtGui.QWidget.__init__(self) 32 | 33 | print("UIFO - UI Info constructor") 34 | 35 | # Window settings 36 | #self.setWindowModality(QtCore.Qt.ApplicationModal) 37 | self.setWindowTitle('Astibot Information') 38 | self.setStyleSheet("background-color:#203044;") 39 | self.setWindowIcon(QtGui.QIcon("AstibotIcon.png")) 40 | self.setAutoFillBackground(True); 41 | self.setFixedSize(1060, 750) 42 | 43 | # Build layout 44 | self.BuildWindowLayout() 45 | 46 | 47 | def BuildWindowLayout(self): 48 | self.rootGridLayout = QtGui.QGridLayout() 49 | self.rootGridLayout.setContentsMargins(0, 0, 0, 0) 50 | self.mainGridLayout = QtGui.QGridLayout() 51 | self.mainGridLayout.setContentsMargins(0, 0, 0, 0) 52 | self.setLayout(self.rootGridLayout) 53 | self.rootGridLayout.addLayout(self.mainGridLayout, 1, 1) 54 | rowNumber = 0 55 | 56 | # Root left and right 57 | self.rootLeftBlock = QtGui.QWidget() 58 | self.rootLeftBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 59 | self.rootLeftBlock.setFixedWidth(20) 60 | self.rootRightBlock = QtGui.QWidget() 61 | self.rootRightBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 62 | self.rootRightBlock.setFixedWidth(20) 63 | self.rootGridLayout.addWidget(self.rootLeftBlock, 0, 0, 3, 1) 64 | self.rootGridLayout.addWidget(self.rootRightBlock, 0, 2, 3, 1) 65 | 66 | # Root top and bottom 67 | self.rootTopBlock = QtGui.QWidget() 68 | self.rootTopBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 69 | self.rootTopBlock.setFixedHeight(20) 70 | self.rootBottomBlock = QtGui.QWidget() 71 | self.rootBottomBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 72 | self.rootBottomBlock.setFixedHeight(60) 73 | self.rootGridLayout.addWidget(self.rootTopBlock, 0, 0, 1, 3) 74 | self.rootGridLayout.addWidget(self.rootBottomBlock, 2, 0, 1, 3) 75 | 76 | # Body layout =========================================================== 77 | self.mainGridLayout.setColumnStretch(0, 1) 78 | self.mainGridLayout.setColumnStretch(1, 10) 79 | 80 | # Section 1 ================================================================================== 81 | self.lblTitleSection1 = QtGui.QLabel("How does Astibot trading strategy work?") 82 | self.lblTitleSection1.setStyleSheet(self.STR_QLABEL_TITLE_STYLESHEET); 83 | self.mainGridLayout.addWidget(self.lblTitleSection1, rowNumber, 0, 1, 2) 84 | rowNumber = rowNumber + 1 85 | 86 | 87 | self.lblTxtSubtitle1 = QtGui.QLabel() 88 | self.lblTxtSubtitle1.setWordWrap(True) 89 | self.lblTxtSubtitle1.setStyleSheet(self.STR_QLABEL_STYLESHEET); 90 | self.lblTxtSubtitle1.setText("

- In live trading mode:

") 91 | self.mainGridLayout.addWidget(self.lblTxtSubtitle1, rowNumber, 0, 1, 2) 92 | rowNumber = rowNumber + 1 93 | 94 | self.lblTxtSection11 = QtGui.QLabel() 95 | self.lblTxtSection11.setWordWrap(True) 96 | self.lblTxtSection11.setStyleSheet(self.STR_QLABEL_STYLESHEET); 97 | self.lblTxtSection11.setText("

When running in live trading mode, Astibot updates the price chart every 10 seconds with the most recent middle-market price from Coinbase Pro exchange. It also computes a MACD-like indicator (bottom yellow graph) that helps finding buy and sell opportunities. Astibot strategy is simple:

1. Wait the dip. First, Astibot is waiting for a buy opportunity. Ideally buy oppotunities are detected at the end of a dip.

2. Buy the dip. If a buy opportunity is detected AND if the current price is below the red dashed line (the « risk line »), Astibot sends a market buy order to Coinbase Pro in order to buy the crypto asset. The amount of fiat money that is invested can be adjusted in the Settings.

3. Wait the top. Astibot waits for the next sell oppotunity that would generate a positive profit, in other words which will at least cover the 2 market order fees (buy + sell fees).

4. Sell the top. If a sell oppotunity meets the conditions explained at step 3, the entirety of your crypto asset balance is sold into fiat, and you funds should have increased. Then Astibot goes back to step 1 for another trade
") 98 | self.mainGridLayout.addWidget(self.lblTxtSection11, rowNumber, 0, 1, 2) 99 | rowNumber = rowNumber + 1 100 | 101 | self.lblTxtSubtitle1 = QtGui.QLabel() 102 | self.lblTxtSubtitle1.setWordWrap(True) 103 | self.lblTxtSubtitle1.setStyleSheet(self.STR_QLABEL_STYLESHEET); 104 | self.lblTxtSubtitle1.setText("

- In Simulation mode:

") 105 | self.mainGridLayout.addWidget(self.lblTxtSubtitle1, rowNumber, 0, 1, 2) 106 | rowNumber = rowNumber + 1 107 | 108 | self.lblTxtSection12 = QtGui.QLabel() 109 | self.lblTxtSection12.setWordWrap(True) 110 | self.lblTxtSection12.setStyleSheet(self.STR_QLABEL_STYLESHEET); 111 | self.lblTxtSection12.setText("

In simulation mode, Trading strategy is the same as in Live trading mode, excepted that it is performed on historic samples that quickly scroll allowing you to test different tunnings or trading pairs. No orders are sent to Coinbase Pro in simulation mode, trades are entirely simulated.
Simulation mode will familiarise you with Astibot trading strategy and how to tune it.

") 112 | self.mainGridLayout.addWidget(self.lblTxtSection12, rowNumber, 0, 1, 2) 113 | rowNumber = rowNumber + 1 114 | 115 | # Section 2 ================================================================================== 116 | self.lblTitleSection2 = QtGui.QLabel("What is displayed on the graphs ?") 117 | self.lblTitleSection2.setStyleSheet(self.STR_QLABEL_TITLE_STYLESHEET); 118 | self.mainGridLayout.addWidget(self.lblTitleSection2, rowNumber, 0, 1, 2) 119 | rowNumber = rowNumber + 1 120 | 121 | self.lblTxtSubtitle21 = QtGui.QLabel() 122 | self.lblTxtSubtitle21.setWordWrap(True) 123 | self.lblTxtSubtitle21.setStyleSheet(self.STR_QLABEL_STYLESHEET); 124 | self.lblTxtSubtitle21.setText("

- Main Graph:

") 125 | self.mainGridLayout.addWidget(self.lblTxtSubtitle21, rowNumber, 0, 1, 2) 126 | rowNumber = rowNumber + 1 127 | 128 | # White chart ------------------------------- 129 | self.lblChartWhite = QtGui.QLabel("") 130 | pixmap = QtGui.QPixmap('chart_white.png') 131 | self.lblChartWhite.setPixmap(pixmap) 132 | self.mainGridLayout.addWidget(self.lblChartWhite, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 133 | 134 | self.lblTxtSection21 = QtGui.QLabel() 135 | self.lblTxtSection21.setWordWrap(True) 136 | self.lblTxtSection21.setStyleSheet(self.STR_QLABEL_STYLESHEET); 137 | self.lblTxtSection21.setText("Estimate of the trading pair price") 138 | self.mainGridLayout.addWidget(self.lblTxtSection21, rowNumber, 1, 1, 1) 139 | rowNumber = rowNumber + 1 140 | 141 | # Orange chart ------------------------------- 142 | self.lblChartOrange = QtGui.QLabel("") 143 | pixmap = QtGui.QPixmap('chart_orange.png') 144 | self.lblChartOrange.setPixmap(pixmap) 145 | self.mainGridLayout.addWidget(self.lblChartOrange, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 146 | 147 | self.lblTxtSection22 = QtGui.QLabel() 148 | self.lblTxtSection22.setWordWrap(True) 149 | self.lblTxtSection22.setStyleSheet(self.STR_QLABEL_STYLESHEET); 150 | self.lblTxtSection22.setText("Price slow moving average. The smoothing intensity can be set with the sensitivity cursor. This chart is used for the bottom (yellow graph) computation.") 151 | self.mainGridLayout.addWidget(self.lblTxtSection22, rowNumber, 1, 1, 1) 152 | rowNumber = rowNumber + 1 153 | 154 | # Blue chart ------------------------------- 155 | self.lblChartBlue = QtGui.QLabel("") 156 | pixmap = QtGui.QPixmap('chart_blue.png') 157 | self.lblChartBlue.setPixmap(pixmap) 158 | self.mainGridLayout.addWidget(self.lblChartBlue, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 159 | 160 | self.lblTxtSection23 = QtGui.QLabel() 161 | self.lblTxtSection23.setWordWrap(True) 162 | self.lblTxtSection23.setStyleSheet(self.STR_QLABEL_STYLESHEET); 163 | self.lblTxtSection23.setText("Price fast moving average. The smoothing intensity can be set with the sensitivity cursor. This chart is used for the bottom (yellow graph) computation.") 164 | self.mainGridLayout.addWidget(self.lblTxtSection23, rowNumber, 1, 1, 1) 165 | rowNumber = rowNumber + 1 166 | 167 | # Red line ------------------------------- 168 | self.lblChartRed = QtGui.QLabel("") 169 | pixmap = QtGui.QPixmap('chart_red.png') 170 | self.lblChartRed.setPixmap(pixmap) 171 | self.mainGridLayout.addWidget(self.lblChartRed, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 172 | 173 | self.lblTxtSection24 = QtGui.QLabel() 174 | self.lblTxtSection24.setWordWrap(True) 175 | self.lblTxtSection24.setStyleSheet(self.STR_QLABEL_STYLESHEET); 176 | self.lblTxtSection24.setText("Risk line: This is the maximum buy level. Astibot only performs buy transactions if the current price is below this line. The purpose of this line is to avoid opening a trade too high that could hardly be sold. You are free to set your own risk level thanks to the Risk level cursor. This line also evolves automatically to match the average market level (based on the last few hours), but its value is weighted by the risk level you set.") 177 | self.mainGridLayout.addWidget(self.lblTxtSection24, rowNumber, 1, 1, 1) 178 | rowNumber = rowNumber + 1 179 | 180 | # Buy Sells symbols ------------------------------- 181 | self.lblChartSymbols = QtGui.QLabel("") 182 | pixmap = QtGui.QPixmap('chart_symbols.png') 183 | self.lblChartSymbols.setPixmap(pixmap) 184 | self.mainGridLayout.addWidget(self.lblChartSymbols, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 185 | 186 | self.lblTxtSection25 = QtGui.QLabel() 187 | self.lblTxtSection25.setWordWrap(True) 188 | self.lblTxtSection25.setStyleSheet(self.STR_QLABEL_STYLESHEET); 189 | self.lblTxtSection25.setText("Approximate positions of the buy (green symbol) and sell (red symbol) transactions performed by Astibot.") 190 | self.mainGridLayout.addWidget(self.lblTxtSection25, rowNumber, 1, 1, 1) 191 | rowNumber = rowNumber + 1 192 | 193 | # Bottom chart ------------------------------- 194 | self.lblTxtSubtitle21 = QtGui.QLabel() 195 | self.lblTxtSubtitle21.setWordWrap(True) 196 | self.lblTxtSubtitle21.setStyleSheet(self.STR_QLABEL_STYLESHEET); 197 | self.lblTxtSubtitle21.setText("

- Bottom Graph:

") 198 | self.mainGridLayout.addWidget(self.lblTxtSubtitle21, rowNumber, 0, 1, 2) 199 | rowNumber = rowNumber + 1 200 | 201 | self.lblChartYellow = QtGui.QLabel("") 202 | pixmap = QtGui.QPixmap('chart_yellow.png') 203 | self.lblChartYellow.setPixmap(pixmap) 204 | self.mainGridLayout.addWidget(self.lblChartYellow, rowNumber, 0, 1, 1, QtCore.Qt.AlignRight) 205 | 206 | self.lblTxtSection26 = QtGui.QLabel() 207 | self.lblTxtSection26.setWordWrap(True) 208 | self.lblTxtSection26.setStyleSheet(self.STR_QLABEL_STYLESHEET); 209 | self.lblTxtSection26.setText("Decision indicator chart. It is computed with the subtraction of the blue and orange smoothed prices. It is similar to the well known MACD indicator. If it goes from negative to positive, Astibot will interpret it as a buy signal. A positive to negative change is identified as a sell signal. The influence of the sensitivity setting is directly visible on this graph as if you increase the sensitivity, more buy and sell signals will appear.") 210 | self.mainGridLayout.addWidget(self.lblTxtSection26, rowNumber, 1, 1, 1) 211 | rowNumber = rowNumber + 1 212 | 213 | # Bottom buttons ================================================================================== 214 | self.btnClose = QtGui.QPushButton("Close") 215 | self.btnClose.setStyleSheet(self.STR_QBUTTON_CLOSE_STYLESHEET) 216 | self.btnClose.setFixedWidth(120) 217 | self.btnClose.setFixedHeight(38) 218 | self.btnClose.clicked.connect(self.HideWindow) 219 | self.btnClose.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 220 | self.hBoxBottomButtons = QtGui.QHBoxLayout() 221 | self.hBoxBottomButtons.addWidget(self.btnClose, QtCore.Qt.AlignRight) 222 | self.rootBottomBlock.setLayout(self.hBoxBottomButtons) 223 | rowNumber = rowNumber + 1 224 | 225 | 226 | def UIFO_ShowWindow(self): 227 | print("UIFO - Show") 228 | self.show() 229 | 230 | def HideWindow(self): 231 | self.hide() 232 | -------------------------------------------------------------------------------- /src/UISettings.py: -------------------------------------------------------------------------------- 1 | import math 2 | from pyqtgraph.Qt import QtCore, QtGui 3 | from PyQt5.QtWidgets import QFrame 4 | from PyQt5.Qt import QIntValidator 5 | from PyQt5.Qt import QDoubleValidator 6 | from GDAXCurrencies import GDAXCurrencies 7 | import ctypes # Message box popup 8 | 9 | import TradingBotConfig as theConfig 10 | 11 | class UISettings(QtGui.QWidget): 12 | 13 | STR_CHECKBOX_AUTHORIZATION_TEXT = "By entering your API keys, you accept to leave control of your Coinbase Pro account to this software through the Application Programming Interface (API). It includes algorithm-based buying or selling of fiat money or crypto-assets. You are the only responsible for the actions that are performed by this software through the API, even in case of unfavorable market, inappropriate buy or sell decision, software bug, undesired software behavior or any other undesired activity. Train yourself on the simulator before performing actual trading. Only give control to money / assets that you can afford to loose." 14 | 15 | STR_BORDER_BLOCK_STYLESHEET = "QWidget {background-color : #151f2b;}" 16 | STR_QLABEL_STYLESHEET = "QLabel { background-color : #203044; color : white; font: bold 13px;}" 17 | STR_QLABEL_NOTE_STYLESHEET = "QLabel { background-color : #203044; color : white; font: 12px;}" 18 | STR_QCHECKBOX_STYLESHEET = "QCheckBox { background-color : #203044; color : white; font: 10px;}" 19 | STR_QCHECKBOX_LABEL_STYLESHEET = "QLabel { background-color : #203044; color : #C2C2C2; font: 10px;}" 20 | STR_QLABEL_TITLE_STYLESHEET = "QLabel { background-color : #203044; color : #81C6FE; font: bold 16px;}" 21 | STR_QTEXTEDIT_STYLESHEET = "QLineEdit { background-color : #203044; color : white; font: bold 13px; border: 1px solid white; border-radius: 4px;} QLineEdit:focus {border: 2px solid #007ad9;}" 22 | STR_QTEXTEDIT_BLINK_STYLESHEET = "QLineEdit { background-color : #203044; color : white; font: bold 13px; border: 3px solid #00c11a; border-radius: 4px;} QLineEdit:focus {border: 3px solid #00c11a;}" 23 | STR_QFRAME_SEPARATOR_STYLESHEET = "background-color: rgb(28, 30, 28)" 24 | STR_COMBO_STYLESHEET = "QComboBox { background-color : #203044; color : white; font: bold bold 13px; border: 1px solid white; border-radius: 4px;} QComboBox:focus {border: 2px solid #007ad9;} QListView{color: white; font: bold 13px;} QListView:focus {border: 2px solid #007ad9;} QComboBox QAbstractItemView::item{min-height: 32px;}" 25 | STR_QSLIDER_STYLESHEET = "QSlider::handle:hover {background-color: #C6D0FF;}" 26 | STR_QBUTTON_APPLY_STYLESHEET = "QPushButton {background-color: #01599e; border-width: 2px; border-radius: 10px; border-color: white; font: bold 15px; color:white} QPushButton:pressed { background-color: #1d8d24 } QPushButton:hover { background-color: #002c4f }" 27 | STR_QBUTTON_CANCEL_STYLESHEET = "QPushButton {background-color: #7e8c98; border-width: 2px; border-radius: 10px; border-color: white; font: bold 15px; color:white} QPushButton:pressed { background-color: #bda300 } QPushButton:hover { background-color: #56616b }" 28 | 29 | RIGHT_LABELS_WIDTH_IN_PX = 75 30 | 31 | def __init__(self, settings): 32 | # Here, you should call the inherited class' init, which is QDialog 33 | QtGui.QWidget.__init__(self) 34 | 35 | print("UIST - UI Settings constructor") 36 | 37 | # Application settings data instance 38 | self.theSettings = settings 39 | 40 | # Window settings 41 | self.setWindowModality(QtCore.Qt.ApplicationModal) 42 | self.setWindowTitle('Astibot Settings') 43 | self.setStyleSheet("background-color:#203044;") 44 | self.setWindowIcon(QtGui.QIcon("AstibotIcon.png")) 45 | self.setAutoFillBackground(True); 46 | self.setFixedSize(646, 660) 47 | 48 | # Build layout 49 | self.BuildWindowLayout() 50 | 51 | # Timer to make API textboxes blink 52 | self.timerBlinkStuffs = QtCore.QTimer() 53 | self.timerBlinkStuffs.timeout.connect(self.TimerRaisedBlinkStuff) 54 | self.blinkIsOn = False 55 | self.blinkCounter = 0 56 | 57 | # Apply saved (or default) settings 58 | self.ApplySettings() 59 | 60 | 61 | def ApplySettings(self): 62 | self.txtAPIKey.setText(self.theSettings.SETT_GetSettings()["strAPIKey"]) 63 | self.strAPIKeyApplicable = self.theSettings.SETT_GetSettings()["strAPIKey"] 64 | 65 | self.txtSecretKey.setText(self.theSettings.SETT_GetSettings()["strSecretKey"]) 66 | self.strSecretKeyApplicable = self.theSettings.SETT_GetSettings()["strSecretKey"] 67 | 68 | self.txtPassPhrase.setText(self.theSettings.SETT_GetSettings()["strPassphrase"]) 69 | self.strPassPhraseApplicable = self.theSettings.SETT_GetSettings()["strPassphrase"] 70 | 71 | # if (str(self.theSettings.SETT_GetSettings()["bHasAcceptedConditions"]) == "True"): 72 | # self.checkboxAuthorization.setChecked(True) 73 | # else: 74 | # self.checkboxAuthorization.setChecked(False) 75 | 76 | self.strTradingPair = self.theSettings.SETT_GetSettings()["strTradingPair"] 77 | self.strApplicableTradingPair = self.strTradingPair 78 | self.comboTradingPair.setCurrentIndex(GDAXCurrencies.get_index_for_currency_pair(self.strTradingPair)) 79 | 80 | self.strFiatType = self.theSettings.SETT_GetSettings()["strFiatType"] 81 | self.strCryptoType = self.theSettings.SETT_GetSettings()["strCryptoType"] 82 | 83 | self.simulationTimeRange = self.theSettings.SETT_GetSettings()["simulationTimeRange"] 84 | if (self.simulationTimeRange == 24): 85 | self.comboSimulationTimeRange.setCurrentIndex(0) 86 | elif (self.simulationTimeRange == 48): 87 | self.comboSimulationTimeRange.setCurrentIndex(1) 88 | elif (self.simulationTimeRange == 72): 89 | self.comboSimulationTimeRange.setCurrentIndex(2) 90 | elif (self.simulationTimeRange == 168): 91 | self.comboSimulationTimeRange.setCurrentIndex(3) 92 | else: 93 | self.comboSimulationTimeRange.setCurrentIndex(0) 94 | 95 | self.investPercentage = self.theSettings.SETT_GetSettings()["investPercentage"] 96 | self.sliderFiatAmount.setValue(int(self.investPercentage)) 97 | self.lblFiatAmountValue.setText(str(self.sliderFiatAmount.value()) + " %") 98 | 99 | self.platformTakerFee = self.theSettings.SETT_GetSettings()["platformTakerFee"] 100 | self.sliderTakerFee.setValue(float(self.platformTakerFee) * 20.0) # 20 = 1/quantum, a cabler 101 | self.lblTakerFeePercent.setText(str(self.platformTakerFee) + " %") 102 | 103 | self.sellTrigger = self.theSettings.SETT_GetSettings()["sellTrigger"] 104 | self.sliderSellTrigger.setValue(float(self.sellTrigger) * 20.0) # 20 = 1/quantum, a cabler) 105 | self.lblSellTriggerPercent.setText(str(self.sellTrigger) + " %") 106 | 107 | self.autoSellThreshold = self.theSettings.SETT_GetSettings()["autoSellThreshold"] 108 | self.sliderAutoSellThreshold.setValue(float(self.autoSellThreshold) * 4) # 4 = 1/quantum, a cabler 109 | self.lblAutoSellPercent.setText(str(self.autoSellThreshold) + " %") 110 | 111 | self.txtSimulatedFiatBalance.setText(str(self.theSettings.SETT_GetSettings()["simulatedFiatBalance"])) 112 | self.txtSimulatedFiatBalance.text().replace('.',',') # TODO: ok for french only 113 | 114 | self.simulationSpeed = self.theSettings.SETT_GetSettings()["simulationSpeed"] 115 | self.sliderSimulationSpeed.setValue(int(self.simulationSpeed)) 116 | 117 | def EventApplylButtonClick(self): 118 | print("UIST - Apply Click") 119 | 120 | if (self.checkParametersValidity() == True): 121 | # Set settings 122 | settingsList = self.theSettings.SETT_GetSettings() 123 | 124 | settingsList["strAPIKey"] = self.txtAPIKey.text() 125 | if (str(settingsList["strAPIKey"]) != self.strAPIKeyApplicable): 126 | print("UIST - New API Key set") 127 | self.strAPIKeyApplicable = str(settingsList["strAPIKey"]) 128 | self.theSettings.SETT_NotifyAPIDataHasChanged() 129 | 130 | settingsList["strSecretKey"] = self.txtSecretKey.text() 131 | if (str(settingsList["strSecretKey"]) != self.strSecretKeyApplicable): 132 | print("UIST - New Secret Key set") 133 | self.strSecretKeyApplicable = str(settingsList["strSecretKey"]) 134 | self.theSettings.SETT_NotifyAPIDataHasChanged() 135 | 136 | settingsList["strPassphrase"] = self.txtPassPhrase.text() 137 | if (str(settingsList["strPassphrase"]) != self.strPassPhraseApplicable): 138 | print("UIST - New API Passphrase set") 139 | self.strPassPhraseApplicable = str(settingsList["strPassphrase"]) 140 | self.theSettings.SETT_NotifyAPIDataHasChanged() 141 | 142 | #settingsList["bHasAcceptedConditions"] = self.checkboxAuthorization.isChecked() 143 | settingsList["strTradingPair"] = self.strTradingPair 144 | if (self.strTradingPair != self.strApplicableTradingPair): 145 | print("UIST - New trading pair set: new %s / old %s" % (self.strTradingPair, self.strApplicableTradingPair)) 146 | self.strApplicableTradingPair = self.strTradingPair # The new applicable trading pair becomes this one 147 | self.theSettings.SETT_NotifyTradingPairHasChanged() 148 | settingsList["strFiatType"] = self.strFiatType 149 | settingsList["strCryptoType"] = self.strCryptoType 150 | settingsList["investPercentage"] = self.investPercentage 151 | settingsList["platformTakerFee"] = self.platformTakerFee 152 | settingsList["sellTrigger"] = self.sellTrigger 153 | settingsList["autoSellThreshold"] = self.autoSellThreshold 154 | settingsList["simulatedFiatBalance"] = self.txtSimulatedFiatBalance.text().replace(',','.') 155 | settingsList["simulationSpeed"] = self.simulationSpeed 156 | settingsList["simulationTimeRange"] = self.simulationTimeRange 157 | 158 | # Save settings 159 | self.theSettings.SETT_SaveSettings() 160 | 161 | # Close window 162 | self.HideWindow() 163 | 164 | 165 | def EventCancelButtonClick(self): 166 | print("UIST - Cancel Click") 167 | self.HideWindow() 168 | 169 | def EventComboTradingPairChanged(self): 170 | print("UIST - Combo Trading pair set to: %s" % str(self.comboTradingPair.currentIndex())) 171 | all_data = GDAXCurrencies.get_currencies_list() 172 | try: 173 | a_currency = all_data[self.comboTradingPair.currentIndex()] 174 | self.strTradingPair = a_currency['full'] 175 | self.strFiatType = a_currency['fiat'] 176 | self.strCryptoType = a_currency['coin'] 177 | except IndexError: 178 | pass 179 | 180 | # Refresh labels that mention the currency 181 | self.lblSimulatedFiatBalance.setText(self.strFiatType) 182 | self.lblFiatPercentageToInvest.setText("Percentage of " + self.strFiatType + " account balance to invest in trades:") 183 | 184 | def EventMovedSliderFiatAmountInvest(self): 185 | #print("Fiat amount percentage to invest : " + str(self.sliderFiatAmount.value())) 186 | self.lblFiatAmountValue.setText(str(self.sliderFiatAmount.value()) + " %") 187 | self.investPercentage = self.sliderFiatAmount.value() 188 | 189 | def EventMovedSliderTakerFee(self): 190 | #print("UIST - Slider Taker Fee value change: " + str(self.sliderTakerFee.value()*theConfig.CONFIG_PLATFORM_TAKER_FEE_QUANTUM)) 191 | self.platformTakerFee = round(float(self.sliderTakerFee.value()*theConfig.CONFIG_PLATFORM_TAKER_FEE_QUANTUM), 2) 192 | self.lblTakerFeePercent.setText(str(self.platformTakerFee) + " %") 193 | 194 | def EventMovedSliderAutoSell(self): 195 | #print("UIST - Slider Auto Sell Threshold value change: " + str(self.sliderAutoSellThreshold.value()*theConfig.CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_QUANTUM)) 196 | self.autoSellThreshold = round(float(self.sliderAutoSellThreshold.value()*theConfig.CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_QUANTUM), 2) 197 | self.lblAutoSellPercent.setText(str(self.autoSellThreshold) + " %") 198 | 199 | def EventMovedSliderSimulationSpeed(self): 200 | self.simulationSpeed = int(self.sliderSimulationSpeed.value()) 201 | 202 | def EventMovedSliderSellTrigger(self): 203 | self.sellTrigger = round(float(self.sliderSellTrigger.value()*theConfig.CONFIG_SELL_TRIGGER_PERCENTAGE_QUANTUM), 2) 204 | self.lblSellTriggerPercent.setText(str(self.sellTrigger) + " %") 205 | 206 | def EventComboSimulationTimeRange(self): 207 | print("UIST - Combo Simulation time range set to: %s" % str(self.comboSimulationTimeRange.currentIndex())) 208 | if (self.comboSimulationTimeRange.currentIndex() == 0): 209 | self.simulationTimeRange = 24 210 | elif (self.comboSimulationTimeRange.currentIndex() == 1): 211 | self.simulationTimeRange = 48 212 | elif (self.comboSimulationTimeRange.currentIndex() == 2): 213 | self.simulationTimeRange = 72 214 | elif (self.comboSimulationTimeRange.currentIndex() == 3): 215 | self.simulationTimeRange = 168 216 | else: 217 | pass 218 | 219 | def TimerRaisedBlinkStuff(self): 220 | if (self.blinkCounter < 6): 221 | self.blinkIsOn = not self.blinkIsOn 222 | self.UpdateBlinkWidgetsDisplay() 223 | self.blinkCounter = self.blinkCounter + 1 224 | else: 225 | self.blinkIsOn = False 226 | self.UpdateBlinkWidgetsDisplay() 227 | 228 | def UpdateBlinkWidgetsDisplay(self): 229 | if (self.blinkIsOn == True): 230 | self.txtAPIKey.setStyleSheet(self.STR_QTEXTEDIT_BLINK_STYLESHEET) 231 | self.txtPassPhrase.setStyleSheet(self.STR_QTEXTEDIT_BLINK_STYLESHEET) 232 | self.txtSecretKey.setStyleSheet(self.STR_QTEXTEDIT_BLINK_STYLESHEET) 233 | else: 234 | self.txtAPIKey.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 235 | self.txtPassPhrase.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 236 | self.txtSecretKey.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 237 | 238 | def checkParametersValidity(self): 239 | # Check amount of money to virtually invest in simulation mode 240 | if (self.txtSimulatedFiatBalance.text() != ""): 241 | fiatBalance = float(self.txtSimulatedFiatBalance.text().replace(',','.')) 242 | if ((fiatBalance < theConfig.CONFIG_SIMU_INITIAL_BALANCE_MIN) or (fiatBalance > theConfig.CONFIG_SIMU_INITIAL_BALANCE_MAX)): 243 | print("UIST - Input range error on Simulated fiat balance to invest") 244 | self.MessageBoxPopup("Error: Initial simulated fiat balance entry must be between " + str(theConfig.CONFIG_SIMU_INITIAL_BALANCE_MIN) + " to " + str(theConfig.CONFIG_SIMU_INITIAL_BALANCE_MAX), 0) 245 | return False 246 | else: 247 | print("UIST - No entry for initial simulated balance to invest") 248 | self.MessageBoxPopup("Error: No entry for initial simulated fiat balance (range is " + str(theConfig.CONFIG_SIMU_INITIAL_BALANCE_MIN) + " to " + str(theConfig.CONFIG_SIMU_INITIAL_BALANCE_MAX) + ")", 0) 249 | return False 250 | 251 | return True 252 | 253 | 254 | ## Styles: 255 | ## 0 : OK 256 | ## 1 : OK | Cancel 257 | ## 2 : Abort | Retry | Ignore 258 | ## 3 : Yes | No | Cancel 259 | ## 4 : Yes | No 260 | ## 5 : Retry | No 261 | ## 6 : Cancel | Try Again | Continue 262 | def MessageBoxPopup(self, text, style): 263 | title = "Astibot Settings" 264 | return ctypes.windll.user32.MessageBoxW(0, text, title, style) 265 | 266 | def BuildWindowLayout(self): 267 | self.rootGridLayout = QtGui.QGridLayout() 268 | self.rootGridLayout.setContentsMargins(0, 0, 0, 0) 269 | self.mainGridLayout1 = QtGui.QGridLayout() 270 | self.mainGridLayout1.setContentsMargins(0, 0, 0, 0) 271 | self.mainGridLayout2 = QtGui.QGridLayout() 272 | self.mainGridLayout2.setContentsMargins(0, 0, 0, 0) 273 | self.setLayout(self.rootGridLayout) 274 | self.rootGridLayout.addLayout(self.mainGridLayout1, 1, 1) 275 | self.rootGridLayout.addLayout(self.mainGridLayout2, 3, 1) 276 | self.mainGridLayout1.setColumnStretch(0, 2) 277 | self.mainGridLayout1.setColumnStretch(1, 1) 278 | self.mainGridLayout2.setColumnStretch(0, 2) 279 | self.mainGridLayout2.setColumnStretch(1, 1) 280 | rowNumber = 0 281 | 282 | # Root left and right 283 | self.rootLeftBlock = QtGui.QWidget() 284 | self.rootLeftBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 285 | self.rootLeftBlock.setFixedWidth(20) 286 | self.rootRightBlock = QtGui.QWidget() 287 | self.rootRightBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 288 | self.rootRightBlock.setFixedWidth(20) 289 | self.rootGridLayout.addWidget(self.rootLeftBlock, 0, 0, 5, 1) 290 | self.rootGridLayout.addWidget(self.rootRightBlock, 0, 3, 5, 1) 291 | 292 | # Root top and bottom 293 | self.rootTopBlock = QtGui.QWidget() 294 | self.rootTopBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 295 | self.rootTopBlock.setFixedHeight(20) 296 | self.rootBottomBlock = QtGui.QWidget() 297 | self.rootBottomBlock.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 298 | self.rootBottomBlock.setFixedHeight(60) 299 | self.rootGridLayout.addWidget(self.rootTopBlock, 0, 0, 1, 4) 300 | self.rootGridLayout.addWidget(self.rootBottomBlock, 4, 0, 1, 4) 301 | 302 | # Reuse ============================================================================ 303 | self.SeparatorLine = QtGui.QWidget() 304 | self.SeparatorLine.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 305 | self.SeparatorLine.setFixedHeight(15) 306 | self.SeparatorLine2 = QtGui.QWidget() 307 | self.SeparatorLine2.setStyleSheet(self.STR_BORDER_BLOCK_STYLESHEET) 308 | self.SeparatorLine2.setFixedHeight(15) 309 | 310 | # Trading Account layout =========================================================== 311 | self.lblTitleTradingAccount = QtGui.QLabel("Coinbase Pro Connection parameters") 312 | self.lblTitleTradingAccount.setStyleSheet(self.STR_QLABEL_TITLE_STYLESHEET); 313 | self.mainGridLayout1.addWidget(self.lblTitleTradingAccount, rowNumber, 0) 314 | rowNumber = rowNumber + 1 315 | 316 | # API Key 317 | self.lblAPIKey = QtGui.QLabel("API Key:") 318 | self.lblAPIKey.setStyleSheet(self.STR_QLABEL_STYLESHEET); 319 | self.lblAPIKey.setFixedHeight(30) 320 | self.lblAPIKey.setContentsMargins(20,0,0,0) 321 | self.mainGridLayout1.addWidget(self.lblAPIKey, rowNumber, 0) 322 | self.txtAPIKey = QtGui.QLineEdit() 323 | self.txtAPIKey.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 324 | self.mainGridLayout1.addWidget(self.txtAPIKey, rowNumber, 1) 325 | rowNumber = rowNumber + 1 326 | 327 | # Secret Key 328 | self.lblSecretKey = QtGui.QLabel("Secret Key:") 329 | self.lblSecretKey.setStyleSheet(self.STR_QLABEL_STYLESHEET); 330 | self.lblSecretKey.setFixedHeight(30) 331 | self.lblSecretKey.setContentsMargins(20,0,0,0) 332 | self.mainGridLayout1.addWidget(self.lblSecretKey, rowNumber, 0) 333 | self.txtSecretKey = QtGui.QLineEdit() 334 | self.txtSecretKey.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 335 | self.txtSecretKey.setEchoMode(QtGui.QLineEdit.Password) 336 | self.mainGridLayout1.addWidget(self.txtSecretKey, rowNumber, 1) 337 | rowNumber = rowNumber + 1 338 | 339 | # Passphrase 340 | self.lblPassPhrase = QtGui.QLabel("Passphrase:") 341 | self.lblPassPhrase.setStyleSheet(self.STR_QLABEL_STYLESHEET); 342 | self.lblPassPhrase.setFixedHeight(30) 343 | self.lblPassPhrase.setContentsMargins(20,0,0,0) 344 | self.mainGridLayout1.addWidget(self.lblPassPhrase, rowNumber, 0) 345 | self.txtPassPhrase = QtGui.QLineEdit() 346 | self.txtPassPhrase.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 347 | self.txtPassPhrase.setEchoMode(QtGui.QLineEdit.Password) 348 | self.mainGridLayout1.addWidget(self.txtPassPhrase, rowNumber, 1) 349 | rowNumber = rowNumber + 1 350 | 351 | # Authorization checkbox 352 | self.lblPermissions = QtGui.QLabel("Note: Make sure you enabled the View and Trade permissions when creating your API keys on your Coinbase Pro profile. These permissions are required for Astibot to operate.") 353 | self.lblPermissions.setWordWrap(True) 354 | self.lblPermissions.setStyleSheet(self.STR_QLABEL_NOTE_STYLESHEET); 355 | self.lblAuthorization = QtGui.QLabel(self.STR_CHECKBOX_AUTHORIZATION_TEXT) 356 | self.lblAuthorization.setStyleSheet(self.STR_QCHECKBOX_LABEL_STYLESHEET) 357 | self.lblAuthorization.setWordWrap(True) 358 | 359 | self.mainGridLayout1.addWidget(self.lblPermissions, rowNumber, 0, 1, 2) 360 | rowNumber = rowNumber + 1 361 | self.mainGridLayout1.addWidget(self.lblAuthorization, rowNumber, 0, 1, 2) 362 | rowNumber = rowNumber + 1 363 | 364 | # Separator 365 | self.rootGridLayout.addWidget(self.SeparatorLine, 2, 0, 1, 4) 366 | rowNumber = 0 367 | 368 | # Trading Parameters layout ======================================================== 369 | self.lblTitleTradingParameters = QtGui.QLabel("Trading parameters") 370 | self.lblTitleTradingParameters.setStyleSheet(self.STR_QLABEL_TITLE_STYLESHEET); 371 | self.mainGridLayout2.addWidget(self.lblTitleTradingParameters, rowNumber, 0) 372 | rowNumber = rowNumber + 1 373 | 374 | # Trading pair 375 | self.lblTradingPair = QtGui.QLabel("Trading pair:") 376 | self.lblTradingPair.setStyleSheet(self.STR_QLABEL_STYLESHEET); 377 | self.lblTradingPair.setFixedHeight(30) 378 | self.lblTradingPair.setContentsMargins(20,0,0,0) 379 | self.mainGridLayout2.addWidget(self.lblTradingPair, rowNumber, 0) 380 | self.comboTradingPair = QtGui.QComboBox() 381 | self.comboTradingPair.setView(QtGui.QListView()) # Necessary to allow height change 382 | for currency in GDAXCurrencies.get_all_pairs(): 383 | self.comboTradingPair.addItem(currency) 384 | self.comboTradingPair.currentIndexChanged.connect(self.EventComboTradingPairChanged) 385 | self.comboTradingPair.setStyleSheet(self.STR_COMBO_STYLESHEET) 386 | self.mainGridLayout2.addWidget(self.comboTradingPair, rowNumber, 1) 387 | rowNumber = rowNumber + 1 388 | 389 | # Fiat amount to invest 390 | self.lblFiatPercentageToInvest = QtGui.QLabel("Percentage of " + self.theSettings.SETT_GetSettings()["strFiatType"] + " account balance to invest in trades:") 391 | self.lblFiatPercentageToInvest.setStyleSheet(self.STR_QLABEL_STYLESHEET); 392 | self.lblFiatPercentageToInvest.setFixedHeight(30) 393 | self.lblFiatPercentageToInvest.setContentsMargins(20,0,0,0) 394 | self.mainGridLayout2.addWidget(self.lblFiatPercentageToInvest, rowNumber, 0) 395 | self.hBoxFiatAmount = QtGui.QHBoxLayout() 396 | self.sliderFiatAmount = QtGui.QSlider(QtCore.Qt.Horizontal) 397 | self.sliderFiatAmount.setMinimum(1) 398 | self.sliderFiatAmount.setMaximum(99) 399 | self.sliderFiatAmount.setValue(50) 400 | self.sliderFiatAmount.setStyleSheet(self.STR_QSLIDER_STYLESHEET) 401 | self.sliderFiatAmount.valueChanged.connect(self.EventMovedSliderFiatAmountInvest) 402 | self.lblFiatAmountValue = QtGui.QLabel("90 %") 403 | self.lblFiatAmountValue.setStyleSheet(self.STR_QLABEL_STYLESHEET); 404 | self.lblFiatAmountValue.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 405 | self.hBoxFiatAmount.addWidget(self.sliderFiatAmount) 406 | self.hBoxFiatAmount.addWidget(self.lblFiatAmountValue) 407 | self.mainGridLayout2.addLayout(self.hBoxFiatAmount, rowNumber, 1) 408 | rowNumber = rowNumber + 1 409 | 410 | # Coinbase Pro Taker fee 411 | self.lblTakerFee = QtGui.QLabel("Coinbase Pro Taker and Maker order fee:") 412 | self.lblTakerFee.setStyleSheet(self.STR_QLABEL_STYLESHEET); 413 | self.lblTakerFee.setFixedHeight(30) 414 | self.lblTakerFee.setContentsMargins(20,0,0,0) 415 | self.mainGridLayout2.addWidget(self.lblTakerFee, rowNumber, 0) 416 | self.sliderTakerFee = QtGui.QSlider(QtCore.Qt.Horizontal) 417 | self.sliderTakerFee.setMinimum(theConfig.CONFIG_PLATFORM_TAKER_FEE_MIN_ON_SLIDER) 418 | self.sliderTakerFee.setMaximum(theConfig.CONFIG_PLATFORM_TAKER_FEE_MAX_ON_SLIDER) 419 | self.sliderTakerFee.setSingleStep(1) 420 | self.sliderTakerFee.setValue(theConfig.CONFIG_PLATFORM_TAKER_FEE_DEFAULT_VALUE) 421 | self.sliderTakerFee.setStyleSheet(self.STR_QSLIDER_STYLESHEET) 422 | self.sliderTakerFee.valueChanged.connect(self.EventMovedSliderTakerFee) 423 | self.lblTakerFeePercent = QtGui.QLabel("%") 424 | self.lblTakerFeePercent.setStyleSheet(self.STR_QLABEL_STYLESHEET); 425 | self.lblTakerFeePercent.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 426 | self.hBoxTakerFee = QtGui.QHBoxLayout() 427 | self.hBoxTakerFee.addWidget(self.sliderTakerFee) 428 | self.hBoxTakerFee.addWidget(self.lblTakerFeePercent) 429 | self.mainGridLayout2.addLayout(self.hBoxTakerFee, rowNumber, 1) 430 | rowNumber = rowNumber + 1 431 | 432 | # Sell trigger 433 | self.lblSellTrigger = QtGui.QLabel("Auto-Sell trigger (% above buy price, set 0 to disable):") 434 | self.lblSellTrigger.setStyleSheet(self.STR_QLABEL_STYLESHEET); 435 | self.lblSellTrigger.setFixedHeight(30) 436 | self.lblSellTrigger.setContentsMargins(20,0,0,0) 437 | self.mainGridLayout2.addWidget(self.lblSellTrigger, rowNumber, 0) 438 | self.sliderSellTrigger = QtGui.QSlider(QtCore.Qt.Horizontal) 439 | self.sliderSellTrigger.setMinimum(theConfig.CONFIG_SELL_TRIGGER_PERCENTAGE_MIN_ON_SLIDER) 440 | self.sliderSellTrigger.setMaximum(theConfig.CONFIG_SELL_TRIGGER_PERCENTAGE_MAX_ON_SLIDER) 441 | self.sliderSellTrigger.setSingleStep(1) 442 | self.sliderSellTrigger.setValue(theConfig.CONFIG_SELL_TRIGGER_PERCENTAGE_DEFAULT_VALUE) 443 | self.sliderSellTrigger.setStyleSheet(self.STR_QSLIDER_STYLESHEET) 444 | self.sliderSellTrigger.valueChanged.connect(self.EventMovedSliderSellTrigger) 445 | self.lblSellTriggerPercent = QtGui.QLabel("%") 446 | self.lblSellTriggerPercent.setStyleSheet(self.STR_QLABEL_STYLESHEET); 447 | self.lblSellTriggerPercent.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 448 | self.hBoxSellTrigger = QtGui.QHBoxLayout() 449 | self.hBoxSellTrigger.addWidget(self.sliderSellTrigger) 450 | self.hBoxSellTrigger.addWidget(self.lblSellTriggerPercent) 451 | self.mainGridLayout2.addLayout(self.hBoxSellTrigger, rowNumber, 1) 452 | rowNumber = rowNumber + 1 453 | 454 | # Auto-sell by percentage threshold 455 | self.lblAutoSellThreshold = QtGui.QLabel("Stop-loss trigger (% below buy price, set 0 to disable):") 456 | self.lblAutoSellThreshold.setStyleSheet(self.STR_QLABEL_STYLESHEET); 457 | self.lblAutoSellThreshold.setFixedHeight(30) 458 | self.lblAutoSellThreshold.setContentsMargins(20,0,0,0) 459 | self.mainGridLayout2.addWidget(self.lblAutoSellThreshold, rowNumber, 0) 460 | self.sliderAutoSellThreshold = QtGui.QSlider(QtCore.Qt.Horizontal) 461 | self.sliderAutoSellThreshold.setMinimum(theConfig.CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_MIN_ON_SLIDER) 462 | self.sliderAutoSellThreshold.setMaximum(theConfig.CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_MAX_ON_SLIDER) 463 | self.sliderAutoSellThreshold.setSingleStep(1) 464 | self.sliderAutoSellThreshold.setValue(theConfig.CONFIG_PLATFORM_AUTO_SELL_THRESHOLD_DEFAULT_VALUE) 465 | self.sliderAutoSellThreshold.setStyleSheet(self.STR_QSLIDER_STYLESHEET) 466 | self.sliderAutoSellThreshold.valueChanged.connect(self.EventMovedSliderAutoSell) 467 | self.lblAutoSellPercent = QtGui.QLabel("%") 468 | self.lblAutoSellPercent.setStyleSheet(self.STR_QLABEL_STYLESHEET); 469 | self.lblAutoSellPercent.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 470 | self.hBoxAutoSell = QtGui.QHBoxLayout() 471 | self.hBoxAutoSell.addWidget(self.sliderAutoSellThreshold) 472 | self.hBoxAutoSell.addWidget(self.lblAutoSellPercent) 473 | self.mainGridLayout2.addLayout(self.hBoxAutoSell, rowNumber, 1) 474 | rowNumber = rowNumber + 1 475 | 476 | # Simulated fiat account balance 477 | self.lblSimulatedFiatBalance = QtGui.QLabel("Simulated fiat account balance (simulation mode only):") 478 | self.lblSimulatedFiatBalance.setStyleSheet(self.STR_QLABEL_STYLESHEET); 479 | self.lblSimulatedFiatBalance.setFixedHeight(30) 480 | self.lblSimulatedFiatBalance.setContentsMargins(20,0,0,0) 481 | self.mainGridLayout2.addWidget(self.lblSimulatedFiatBalance, rowNumber, 0) 482 | self.txtSimulatedFiatBalance = QtGui.QLineEdit() 483 | self.txtSimulatedFiatBalance.setStyleSheet(self.STR_QTEXTEDIT_STYLESHEET) 484 | self.txtSimulatedFiatBalance.setFixedWidth(80) 485 | self.txtSimulatedFiatBalance.setValidator(QDoubleValidator()) 486 | self.lblSimulatedFiatBalance = QtGui.QLabel(self.theSettings.SETT_GetSettings()["strFiatType"]) 487 | self.lblSimulatedFiatBalance.setStyleSheet(self.STR_QLABEL_STYLESHEET); 488 | self.lblSimulatedFiatBalance.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 489 | self.hBoxSimulatedFiatBalance = QtGui.QHBoxLayout() 490 | self.hBoxSimulatedFiatBalance.addWidget(self.txtSimulatedFiatBalance, QtCore.Qt.AlignLeft) 491 | self.hBoxSimulatedFiatBalance.addWidget(self.lblSimulatedFiatBalance, QtCore.Qt.AlignLeft) 492 | self.mainGridLayout2.addLayout(self.hBoxSimulatedFiatBalance, rowNumber, 1, QtCore.Qt.AlignLeft) 493 | rowNumber = rowNumber + 1 494 | 495 | # Simulation speed 496 | self.lblSimulationSpeed = QtGui.QLabel("Simulation speed (simulation mode only):") 497 | self.lblSimulationSpeed.setStyleSheet(self.STR_QLABEL_STYLESHEET); 498 | self.lblSimulationSpeed.setFixedHeight(30) 499 | self.lblSimulationSpeed.setContentsMargins(20,0,0,0) 500 | self.mainGridLayout2.addWidget(self.lblSimulationSpeed, rowNumber, 0) 501 | self.sliderSimulationSpeed = QtGui.QSlider(QtCore.Qt.Horizontal) 502 | self.sliderSimulationSpeed.setMinimum(0) 503 | self.sliderSimulationSpeed.setMaximum(100) 504 | self.sliderSimulationSpeed.setValue(50) 505 | self.sliderSimulationSpeed.setStyleSheet(self.STR_QSLIDER_STYLESHEET) 506 | self.sliderSimulationSpeed.valueChanged.connect(self.EventMovedSliderSimulationSpeed) 507 | self.lblSlow = QtGui.QLabel("Slow") 508 | self.lblSlow.setStyleSheet(self.STR_QLABEL_STYLESHEET); 509 | self.lblFast = QtGui.QLabel("Fast") 510 | self.lblFast.setStyleSheet(self.STR_QLABEL_STYLESHEET); 511 | self.lblFast.setFixedWidth(self.RIGHT_LABELS_WIDTH_IN_PX) 512 | self.hBoxSimulationSpeed = QtGui.QHBoxLayout() 513 | self.hBoxSimulationSpeed.addWidget(self.lblSlow) 514 | self.hBoxSimulationSpeed.addWidget(self.sliderSimulationSpeed) 515 | self.hBoxSimulationSpeed.addWidget(self.lblFast, QtCore.Qt.AlignLeft) 516 | self.mainGridLayout2.addLayout(self.hBoxSimulationSpeed, rowNumber, 1) 517 | rowNumber = rowNumber + 1 518 | 519 | # Simulation time range 520 | self.lblSimulationTimeRange = QtGui.QLabel("Simulation time range (simulation mode only):") 521 | self.lblSimulationTimeRange.setFixedHeight(30) 522 | self.lblSimulationTimeRange.setStyleSheet(self.STR_QLABEL_STYLESHEET); 523 | self.lblSimulationTimeRange.setContentsMargins(20,0,0,0) 524 | self.mainGridLayout2.addWidget(self.lblSimulationTimeRange, rowNumber, 0) 525 | self.comboSimulationTimeRange = QtGui.QComboBox() 526 | self.comboSimulationTimeRange.setView(QtGui.QListView()); # Necessary to allow height change 527 | self.comboSimulationTimeRange.addItem("Last 24h") 528 | self.comboSimulationTimeRange.addItem("Last 48h") 529 | self.comboSimulationTimeRange.addItem("Last 72h") 530 | self.comboSimulationTimeRange.addItem("Last Week") 531 | self.comboSimulationTimeRange.setStyleSheet(self.STR_COMBO_STYLESHEET) 532 | self.comboSimulationTimeRange.currentIndexChanged.connect(self.EventComboSimulationTimeRange) 533 | self.mainGridLayout2.addWidget(self.comboSimulationTimeRange, rowNumber, 1) 534 | rowNumber = rowNumber + 1 535 | 536 | # Bottom buttons 537 | self.btnCancel = QtGui.QPushButton("Cancel") 538 | self.btnCancel.setStyleSheet(self.STR_QBUTTON_CANCEL_STYLESHEET) 539 | self.btnCancel.setFixedWidth(120) 540 | self.btnCancel.setFixedHeight(38) 541 | self.btnCancel.clicked.connect(self.EventCancelButtonClick) 542 | self.btnCancel.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 543 | self.btnApply = QtGui.QPushButton("Apply and Close") 544 | self.btnApply.setStyleSheet(self.STR_QBUTTON_APPLY_STYLESHEET) 545 | self.btnApply.setFixedWidth(140) 546 | self.btnApply.setFixedHeight(38) 547 | self.btnApply.clicked.connect(self.EventApplylButtonClick) 548 | self.btnApply.setCursor(QtGui.QCursor(QtCore.Qt.PointingHandCursor)) 549 | self.hBoxBottomButtons = QtGui.QHBoxLayout() 550 | self.hBoxBottomButtons.addWidget(self.btnCancel, QtCore.Qt.AlignRight) 551 | self.hBoxBottomButtons.addWidget(self.btnApply, QtCore.Qt.AlignRight) 552 | self.rootBottomBlock.setLayout(self.hBoxBottomButtons) 553 | rowNumber = rowNumber + 1 554 | 555 | 556 | def UIST_ShowWindow(self): 557 | print("UIST - Show") 558 | 559 | # Apply saved settings 560 | self.ApplySettings() 561 | 562 | # Start blink timer if relevant 563 | if ((self.txtAPIKey.text() == "") or (self.txtSecretKey.text() == "") or (self.txtPassPhrase.text() == "")): 564 | self.timerBlinkStuffs.start(500) 565 | self.blinkCounter = 0 566 | 567 | self.show() 568 | 569 | # Sometimes the window is not correctly displayed (blank zone in the bottom) until for example a manual window resize. So force a refresh. 570 | QtGui.QApplication.processEvents() 571 | 572 | def HideWindow(self): 573 | self.timerBlinkStuffs.stop() 574 | self.blinkIsOn = False 575 | self.UpdateBlinkWidgetsDisplay() 576 | self.hide() -------------------------------------------------------------------------------- /src/UIWidgets.py: -------------------------------------------------------------------------------- 1 | import TradingBotConfig as theConfig 2 | from pyqtgraph.Qt import QtCore, QtGui 3 | 4 | class ButtonHoverStart(QtGui.QPushButton): 5 | 6 | def __init__(self, inLblToolTip, parent=None): 7 | super(QtGui.QPushButton, self).__init__(parent) 8 | self.lblToolTip = inLblToolTip 9 | 10 | def enterEvent(self, QEvent): 11 | if (theConfig.CONFIG_INPUT_MODE_IS_REAL_MARKET == True): 12 | self.lblToolTip.setText("Start / Stop live trading") 13 | else: 14 | self.lblToolTip.setText("Start / Stop simulated trading") 15 | 16 | def leaveEvent(self, QEvent): 17 | self.lblToolTip.setText("") 18 | 19 | class ButtonHoverPause(QtGui.QPushButton): 20 | 21 | def __init__(self, inLblToolTip, parent=None): 22 | super(QtGui.QPushButton, self).__init__(parent) 23 | self.lblToolTip = inLblToolTip 24 | 25 | def enterEvent(self, QEvent): 26 | self.lblToolTip.setText("Pause / Resume simulation") 27 | 28 | def leaveEvent(self, QEvent): 29 | self.lblToolTip.setText("") 30 | 31 | class ButtonHoverSettings(QtGui.QPushButton): 32 | 33 | def __init__(self, inLblToolTip, parent=None): 34 | super(QtGui.QPushButton, self).__init__(parent) 35 | self.lblToolTip = inLblToolTip 36 | 37 | def enterEvent(self, QEvent): 38 | self.lblToolTip.setText("Open Settings page") 39 | 40 | def leaveEvent(self, QEvent): 41 | self.lblToolTip.setText("") 42 | 43 | class ButtonHoverDonation(QtGui.QPushButton): 44 | 45 | def __init__(self, inLblToolTip, parent=None): 46 | super(QtGui.QPushButton, self).__init__(parent) 47 | self.lblToolTip = inLblToolTip 48 | 49 | def enterEvent(self, QEvent): 50 | self.lblToolTip.setText("Open Donation page") 51 | 52 | def leaveEvent(self, QEvent): 53 | self.lblToolTip.setText("") 54 | 55 | class ButtonHoverInfo(QtGui.QPushButton): 56 | 57 | def __init__(self, inLblToolTip, parent=None): 58 | super(QtGui.QPushButton, self).__init__(parent) 59 | self.lblToolTip = inLblToolTip 60 | 61 | def enterEvent(self, QEvent): 62 | self.lblToolTip.setText("Open Information page") 63 | 64 | def leaveEvent(self, QEvent): 65 | self.lblToolTip.setText("") 66 | 67 | class RadioHoverSimulation(QtGui.QRadioButton): 68 | 69 | def __init__(self, inLblToolTip, parent=None): 70 | super(QtGui.QRadioButton, self).__init__(parent) 71 | self.lblToolTip = inLblToolTip 72 | 73 | def enterEvent(self, QEvent): 74 | self.lblToolTip.setText("Simulation mode: In order to test your strategy, the bot operates on historic data and simulates the trades in order to estimate the money you could have earned. No real transaction is performed in this mode.") 75 | 76 | def leaveEvent(self, QEvent): 77 | self.lblToolTip.setText("") 78 | 79 | class RadioHoverTrading(QtGui.QRadioButton): 80 | 81 | def __init__(self, inLblToolTip, parent=None): 82 | super(QtGui.QRadioButton, self).__init__(parent) 83 | self.lblToolTip = inLblToolTip 84 | 85 | def enterEvent(self, QEvent): 86 | self.lblToolTip.setText("Trading mode: Astibot trades on live market. It will buy the dips and sell the tops on the current trading pair. Refresh is performed every 10 seconds. Depending on the market, the first trade can be initiated a few minutes or hours after the start. By using this mode, you give Astibot the control of your account balance.") 87 | 88 | def leaveEvent(self, QEvent): 89 | self.lblToolTip.setText("") 90 | 91 | 92 | class SliderHoverRiskLevel(QtGui.QSlider): 93 | 94 | def __init__(self, inLblToolTip, parent=None): 95 | super(QtGui.QSlider, self).__init__(parent) 96 | self.lblToolTip = inLblToolTip 97 | 98 | def enterEvent(self, QEvent): 99 | self.lblToolTip.setText("Adjust the level of the red dashed line (risk line): Astibot will not buy if the current price value is above this line. This line is updated with the past hours average price and it is weighted with your setting.") 100 | 101 | def leaveEvent(self, QEvent): 102 | self.lblToolTip.setText("") 103 | 104 | class SliderHoverSensitivityLevel(QtGui.QSlider): 105 | 106 | def __init__(self, inLblToolTip, parent=None): 107 | super(QtGui.QSlider, self).__init__(parent) 108 | self.lblToolTip = inLblToolTip 109 | 110 | def enterEvent(self, QEvent): 111 | self.lblToolTip.setText("Sensitivity to dips and tops detection. If you change this setting, consequences will be visible after 1 to 2 hours as it affects the price smoothing") 112 | 113 | def leaveEvent(self, QEvent): 114 | self.lblToolTip.setText("") 115 | 116 | class LabelClickable(QtGui.QLabel): 117 | 118 | def __init__(self, parent): 119 | QtGui.QLabel.__init__(self, parent) 120 | self.UIsAreSet = False 121 | 122 | def SetUIs(self, UISettings, UIDonation): 123 | self.theUISettings = UISettings 124 | self.theUIDonation = UIDonation 125 | self.UIsAreSet = True 126 | 127 | def mousePressEvent(self, event): 128 | print("QLabelMouseClick") 129 | 130 | if (self.UIsAreSet == True): 131 | if (("Welcome" in self.text()) == True): 132 | self.theUISettings.UIST_ShowWindow() 133 | elif (("here to unlock" in self.text()) == True): 134 | self.theUIDonation.UILI_ShowWindow() 135 | 136 | --------------------------------------------------------------------------------