├── .gitignore ├── README.md ├── bin └── manager ├── bot ├── composer.json ├── composer.lock ├── docker ├── freqtrade-ui │ └── Dockerfile ├── freqtrade │ └── Dockerfile └── manager │ └── Dockerfile ├── install ├── resources └── scripts │ ├── binance-scrapper.js │ ├── generate-random-available-port.sh │ └── ui-instance-entrypoint.sh ├── screenshots ├── manager-status.jpg └── manager-trade.jpg └── src └── Manager ├── App ├── Behaviour │ ├── AbstractBehaviour.php │ ├── BinanceVolumePercent24hPairlistBehaviour.php.disabled │ └── TradingViewScanBehaviour.php ├── InstanceHandler.php └── Manager.php ├── Domain ├── BehaviourInterface.php ├── Exception │ ├── InstanceNotFoundConfigFileException.php │ ├── InstanceNotFoundException.php │ └── StrategyNotFoundException.php └── Instance.php ├── Infra ├── Filesystem │ ├── InstanceFilesystem.php │ └── ManagerFilesystem.php └── Process │ ├── InstanceProcess.php │ ├── InstanceUIProcess.php │ ├── ManagerProcess.php │ └── Process.php └── UI └── Console ├── BackTestCommand.php ├── BaseCommand.php ├── CronCommand.php ├── InstanceStopCommand.php ├── InstancesResetDataCommand.php ├── InstancesStatusCommand.php └── TradeCommand.php /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptocurrencies Trading Bot - Freqtrade Manager 2 | 3 | This automated Trading Bot is based on the amazing [Freqtrade](https://www.freqtrade.io/en/latest/) one. 4 | It allows you to manage many Freqtrade fully Dockerized instances with ease. 5 | Each generated instance is accompagnated by its Freqtrade UI, automatically plugged to. 6 | 7 | ## Features 8 | 9 | * **Fast & easy deploy** 🚀 10 | * 1-line installation 11 | * Unlimited instances configurations from 1 only YAML file 12 | * API endpoint and Freqtrade UI ports auto-generation and management 13 | * Many more is coming! 14 | 15 | ### Integrated Behaviours 16 | 17 | * Regular automatic Pairlist (StaticPairlist) generation from [TradingView Screener Crypto](https://fr.tradingview.com/crypto-screener) 18 | 19 | ## Requirements 20 | 21 | * [Docker](https://www.docker.com/) #CaptainObvious 22 | 23 | ## Installation 24 | 25 | ### Look how it's easy to install! 🤩 26 | 27 | [![asciicast](https://asciinema.org/a/74Fg9hEfNvjZR4DIc1VsgwWPi.svg)](https://asciinema.org/a/74Fg9hEfNvjZR4DIc1VsgwWPi) 28 | 29 | ### Your turn! Install it! 🙌 30 | 31 | Just 1 line to install your Trading Bot: 32 | 33 | ``` 34 | curl -sSL https://raw.githubusercontent.com/Ph3nol/Trading-Bot/master/install | sh 35 | ``` 36 | 37 | You now can access `bot` command. 38 | 39 | Now, init a configuration, based on [demo one](https://github.com/Ph3nol/Trading-Bot-Config): 40 | 41 | ``` 42 | mkdir ~/trading-bot-config && cd ~/trading-bot-config 43 | git clone https://github.com/Ph3nol/Trading-Bot-Config . 44 | ``` 45 | 46 | Congrats! 👏 You can now configure your `manager.yaml` file and run your first `bot status` command! 🚀🔥 47 | 48 | ### Crontab entry 49 | 50 | A crontab entry is to add, in order to run periodic tasks needed by your instances and their behaviours. 51 | To obtain this line and add it to your crontabs (`crontab -e`), just run this command: 52 | 53 | ``` 54 | bot cron --crontab 55 | ``` 56 | 57 | ## Some screenshots 58 | 59 | 60 | 61 | 62 | 63 | ## Usage 64 | 65 | Just use `./bot` from your Freqtrade Manager directory. 66 | 67 | ### Commands 68 | 69 | From your config directory: 70 | 71 | ``` 72 | bot 73 | bot status 74 | bot trade 75 | bot stop 76 | bot reset 77 | 78 | bot backtest --days 10 79 | bot backtest --days= --no-download --plotting 80 | 81 | bot cron # To manually execute the cron 82 | bot cron --crontab # To show crontab line to add 83 | ``` 84 | 85 | For more options informations, add `--help` to the base commands. 86 | 87 | ## Update 88 | 89 | To update the Bot and its Docker images, just re-run install command: 90 | 91 | ``` 92 | curl -sSL https://raw.githubusercontent.com/Ph3nol/Trading-Bot/master/install | sh 93 | ``` 94 | 95 | --- 96 | 97 | ## Thanks 98 | 99 | ![Thanks](https://media.giphy.com/media/PoImMjCPa8QaiBWJd0/giphy.gif) 100 | 101 | You want to support this project? 102 | You are using this project and you want to contribute? 103 | Feeling generous? 104 | 105 | * **BTC** -> `1MksZdEXqFwqNhEiPT5sLhgWijuCH42r9c` 106 | * **ETH/USDT/..**. (or other ERC20 loving crypto) -> `0x3167ddc7a6b47a0af1ce5270e067a70b997fd313` 107 | * Register to [Binance](https://www.binance.com/fr/register?ref=69525434) following this [sponsored link](https://www.binance.com/fr/register?ref=69525434) 108 | 109 | --- 110 | 111 | ## Development 112 | 113 | ![Development](https://media.giphy.com/media/fQZX2aoRC1Tqw/giphy.gif) 114 | 115 | ### Execute as a PHP project 116 | 117 | ``` 118 | mkdir ~/trading-bot-dev 119 | cd ~/trading-bot-dev && git clone https://github.com/Ph3nol/trading-bot.git . 120 | ln -s $PWD/bot /usr/local/bin/trading-bot-dev 121 | ``` 122 | 123 | You can now go to your config directory, and use `trading-bot-dev` command instead of the production `bot` one. 124 | 125 | ### Build reference Docker images 126 | 127 | ``` 128 | docker pull freqtradeorg/freqtrade:stable && \ 129 | docker build --file ./docker/freqtrade/Dockerfile --tag ph3nol/freqtrade:latest --no-cache . 130 | docker build --file ./docker/freqtrade-ui/Dockerfile --tag ph3nol/freqtrade-ui:latest --no-cache . 131 | ``` 132 | -------------------------------------------------------------------------------- /bin/manager: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | add(new InstancesStatusCommand()); 32 | $application->add(new InstanceStopCommand()); 33 | $application->add(new InstancesResetDataCommand()); 34 | $application->add(new TradeCommand()); 35 | $application->add(new CronCommand()); 36 | $application->add(new BackTestCommand()); 37 | 38 | $application->run(); 39 | -------------------------------------------------------------------------------- /bot: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | e_error() { 6 | printf "$(tput setaf 1)✖ %s$(tput sgr0)\n" "$@" 7 | } 8 | 9 | FULL_PATH=$(realpath $0) 10 | SCRIPT_DIRECTORY=$(dirname $FULL_PATH) 11 | 12 | MANAGER_TMP_DIRECTORY="/tmp/freqtrade-manager" 13 | 14 | if [ ! -d "${MANAGER_TMP_DIRECTORY}" ]; then 15 | mkdir ${MANAGER_TMP_DIRECTORY} 16 | fi 17 | 18 | if [ -z "${BOT_CONFIG_DIRECTORY}" ]; then 19 | BOT_CONFIG_DIRECTORY=${PWD} 20 | fi 21 | 22 | if [ ! -f "${BOT_CONFIG_DIRECTORY}/manager.yaml" ]; then 23 | e_error "You seem not to be into a Freqtrade Manager working directory. Please take a look at the documentation." 24 | exit 25 | fi 26 | 27 | if [ -t 1 ]; then 28 | DOCKER_TTY_ARGS="-it" 29 | else 30 | DOCKER_TTY_ARGS="-i" 31 | fi 32 | 33 | # Development (from PHP app sources) 34 | if [ -f "${SCRIPT_DIRECTORY}/composer.json" ]; then 35 | if [ -d ${MANAGER_TMP_DIRECTORY}/resources ]; then 36 | rm -rf ${MANAGER_TMP_DIRECTORY}/resources 37 | fi 38 | 39 | if [[ -f ${MANAGER_TMP_DIRECTORY}/resources || -L ${MANAGER_TMP_DIRECTORY}/resources ]]; then 40 | rm ${MANAGER_TMP_DIRECTORY}/resources 41 | fi 42 | 43 | if [ -L ${MANAGER_TMP_DIRECTORY}/resources ]; then 44 | ${MANAGER_TMP_DIRECTORY}/resources 45 | fi 46 | 47 | ln -s ${SCRIPT_DIRECTORY}/resources ${MANAGER_TMP_DIRECTORY}/resources 48 | 49 | docker run ${DOCKER_TTY_ARGS} --rm --name "freqtrade-manager-$(uuidgen)" --privileged=true \ 50 | -e HOST_CONFIGURATION_DIRECTORY=${BOT_CONFIG_DIRECTORY} \ 51 | -e HOST_BOT_SCRIPT_PATH=${FULL_PATH} \ 52 | -v /var/run/docker.sock:/var/run/docker.sock \ 53 | -v ${BOT_CONFIG_DIRECTORY}:/config:rw \ 54 | -v ${MANAGER_TMP_DIRECTORY}/:/tmp/manager/:rw \ 55 | -v ${SCRIPT_DIRECTORY}:/app \ 56 | ph3nol/freqtrade-manager $* 57 | # Staging (from Docker image) 58 | else 59 | docker run ${DOCKER_TTY_ARGS} --rm --name "freqtrade-manager-$(uuidgen)" --privileged=true \ 60 | -e HOST_CONFIGURATION_DIRECTORY=${BOT_CONFIG_DIRECTORY} \ 61 | -e HOST_BOT_SCRIPT_PATH=${FULL_PATH} \ 62 | -v /var/run/docker.sock:/var/run/docker.sock \ 63 | -v ${BOT_CONFIG_DIRECTORY}:/config:rw \ 64 | -v ${MANAGER_TMP_DIRECTORY}/:/tmp/manager/:rw \ 65 | ph3nol/freqtrade-manager $* 66 | fi 67 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ph3nol/freqtrade-manager", 3 | "type": "project", 4 | "authors": [ 5 | { 6 | "name": "Cédric Dugat", 7 | "email": "cedric@dugat.me" 8 | } 9 | ], 10 | "autoload": { 11 | "psr-4": { 12 | "": "src/" 13 | } 14 | }, 15 | "minimum-stability": "stable", 16 | "require": { 17 | "symfony/console": "^5.2", 18 | "symfony/yaml": "^5.2", 19 | "ramsey/uuid": "^4.1", 20 | "symfony/filesystem": "^5.2", 21 | "symfony/process": "^5.2", 22 | "symfony/finder": "^5.2", 23 | "guzzlehttp/guzzle": "^7.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "6ee37245390e1922aed8e2eec621672c", 8 | "packages": [ 9 | { 10 | "name": "brick/math", 11 | "version": "0.9.2", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/brick/math.git", 15 | "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/brick/math/zipball/dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", 20 | "reference": "dff976c2f3487d42c1db75a3b180e2b9f0e72ce0", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-json": "*", 25 | "php": "^7.1 || ^8.0" 26 | }, 27 | "require-dev": { 28 | "php-coveralls/php-coveralls": "^2.2", 29 | "phpunit/phpunit": "^7.5.15 || ^8.5 || ^9.0", 30 | "vimeo/psalm": "4.3.2" 31 | }, 32 | "type": "library", 33 | "autoload": { 34 | "psr-4": { 35 | "Brick\\Math\\": "src/" 36 | } 37 | }, 38 | "notification-url": "https://packagist.org/downloads/", 39 | "license": [ 40 | "MIT" 41 | ], 42 | "description": "Arbitrary-precision arithmetic library", 43 | "keywords": [ 44 | "Arbitrary-precision", 45 | "BigInteger", 46 | "BigRational", 47 | "arithmetic", 48 | "bigdecimal", 49 | "bignum", 50 | "brick", 51 | "math" 52 | ], 53 | "funding": [ 54 | { 55 | "url": "https://tidelift.com/funding/github/packagist/brick/math", 56 | "type": "tidelift" 57 | } 58 | ], 59 | "time": "2021-01-20T22:51:39+00:00" 60 | }, 61 | { 62 | "name": "guzzlehttp/guzzle", 63 | "version": "7.2.0", 64 | "source": { 65 | "type": "git", 66 | "url": "https://github.com/guzzle/guzzle.git", 67 | "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79" 68 | }, 69 | "dist": { 70 | "type": "zip", 71 | "url": "https://api.github.com/repos/guzzle/guzzle/zipball/0aa74dfb41ae110835923ef10a9d803a22d50e79", 72 | "reference": "0aa74dfb41ae110835923ef10a9d803a22d50e79", 73 | "shasum": "" 74 | }, 75 | "require": { 76 | "ext-json": "*", 77 | "guzzlehttp/promises": "^1.4", 78 | "guzzlehttp/psr7": "^1.7", 79 | "php": "^7.2.5 || ^8.0", 80 | "psr/http-client": "^1.0" 81 | }, 82 | "provide": { 83 | "psr/http-client-implementation": "1.0" 84 | }, 85 | "require-dev": { 86 | "ext-curl": "*", 87 | "php-http/client-integration-tests": "^3.0", 88 | "phpunit/phpunit": "^8.5.5 || ^9.3.5", 89 | "psr/log": "^1.1" 90 | }, 91 | "suggest": { 92 | "ext-curl": "Required for CURL handler support", 93 | "ext-intl": "Required for Internationalized Domain Name (IDN) support", 94 | "psr/log": "Required for using the Log middleware" 95 | }, 96 | "type": "library", 97 | "extra": { 98 | "branch-alias": { 99 | "dev-master": "7.1-dev" 100 | } 101 | }, 102 | "autoload": { 103 | "psr-4": { 104 | "GuzzleHttp\\": "src/" 105 | }, 106 | "files": [ 107 | "src/functions_include.php" 108 | ] 109 | }, 110 | "notification-url": "https://packagist.org/downloads/", 111 | "license": [ 112 | "MIT" 113 | ], 114 | "authors": [ 115 | { 116 | "name": "Michael Dowling", 117 | "email": "mtdowling@gmail.com", 118 | "homepage": "https://github.com/mtdowling" 119 | }, 120 | { 121 | "name": "Márk Sági-Kazár", 122 | "email": "mark.sagikazar@gmail.com", 123 | "homepage": "https://sagikazarmark.hu" 124 | } 125 | ], 126 | "description": "Guzzle is a PHP HTTP client library", 127 | "homepage": "http://guzzlephp.org/", 128 | "keywords": [ 129 | "client", 130 | "curl", 131 | "framework", 132 | "http", 133 | "http client", 134 | "psr-18", 135 | "psr-7", 136 | "rest", 137 | "web service" 138 | ], 139 | "funding": [ 140 | { 141 | "url": "https://github.com/GrahamCampbell", 142 | "type": "github" 143 | }, 144 | { 145 | "url": "https://github.com/Nyholm", 146 | "type": "github" 147 | }, 148 | { 149 | "url": "https://github.com/alexeyshockov", 150 | "type": "github" 151 | }, 152 | { 153 | "url": "https://github.com/gmponos", 154 | "type": "github" 155 | } 156 | ], 157 | "time": "2020-10-10T11:47:56+00:00" 158 | }, 159 | { 160 | "name": "guzzlehttp/promises", 161 | "version": "1.4.1", 162 | "source": { 163 | "type": "git", 164 | "url": "https://github.com/guzzle/promises.git", 165 | "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d" 166 | }, 167 | "dist": { 168 | "type": "zip", 169 | "url": "https://api.github.com/repos/guzzle/promises/zipball/8e7d04f1f6450fef59366c399cfad4b9383aa30d", 170 | "reference": "8e7d04f1f6450fef59366c399cfad4b9383aa30d", 171 | "shasum": "" 172 | }, 173 | "require": { 174 | "php": ">=5.5" 175 | }, 176 | "require-dev": { 177 | "symfony/phpunit-bridge": "^4.4 || ^5.1" 178 | }, 179 | "type": "library", 180 | "extra": { 181 | "branch-alias": { 182 | "dev-master": "1.4-dev" 183 | } 184 | }, 185 | "autoload": { 186 | "psr-4": { 187 | "GuzzleHttp\\Promise\\": "src/" 188 | }, 189 | "files": [ 190 | "src/functions_include.php" 191 | ] 192 | }, 193 | "notification-url": "https://packagist.org/downloads/", 194 | "license": [ 195 | "MIT" 196 | ], 197 | "authors": [ 198 | { 199 | "name": "Michael Dowling", 200 | "email": "mtdowling@gmail.com", 201 | "homepage": "https://github.com/mtdowling" 202 | } 203 | ], 204 | "description": "Guzzle promises library", 205 | "keywords": [ 206 | "promise" 207 | ], 208 | "time": "2021-03-07T09:25:29+00:00" 209 | }, 210 | { 211 | "name": "guzzlehttp/psr7", 212 | "version": "1.7.0", 213 | "source": { 214 | "type": "git", 215 | "url": "https://github.com/guzzle/psr7.git", 216 | "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3" 217 | }, 218 | "dist": { 219 | "type": "zip", 220 | "url": "https://api.github.com/repos/guzzle/psr7/zipball/53330f47520498c0ae1f61f7e2c90f55690c06a3", 221 | "reference": "53330f47520498c0ae1f61f7e2c90f55690c06a3", 222 | "shasum": "" 223 | }, 224 | "require": { 225 | "php": ">=5.4.0", 226 | "psr/http-message": "~1.0", 227 | "ralouphie/getallheaders": "^2.0.5 || ^3.0.0" 228 | }, 229 | "provide": { 230 | "psr/http-message-implementation": "1.0" 231 | }, 232 | "require-dev": { 233 | "ext-zlib": "*", 234 | "phpunit/phpunit": "~4.8.36 || ^5.7.27 || ^6.5.14 || ^7.5.20 || ^8.5.8 || ^9.3.10" 235 | }, 236 | "suggest": { 237 | "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses" 238 | }, 239 | "type": "library", 240 | "extra": { 241 | "branch-alias": { 242 | "dev-master": "1.7-dev" 243 | } 244 | }, 245 | "autoload": { 246 | "psr-4": { 247 | "GuzzleHttp\\Psr7\\": "src/" 248 | }, 249 | "files": [ 250 | "src/functions_include.php" 251 | ] 252 | }, 253 | "notification-url": "https://packagist.org/downloads/", 254 | "license": [ 255 | "MIT" 256 | ], 257 | "authors": [ 258 | { 259 | "name": "Michael Dowling", 260 | "email": "mtdowling@gmail.com", 261 | "homepage": "https://github.com/mtdowling" 262 | }, 263 | { 264 | "name": "Tobias Schultze", 265 | "homepage": "https://github.com/Tobion" 266 | } 267 | ], 268 | "description": "PSR-7 message implementation that also provides common utility methods", 269 | "keywords": [ 270 | "http", 271 | "message", 272 | "psr-7", 273 | "request", 274 | "response", 275 | "stream", 276 | "uri", 277 | "url" 278 | ], 279 | "time": "2020-09-30T07:37:11+00:00" 280 | }, 281 | { 282 | "name": "psr/container", 283 | "version": "1.0.0", 284 | "source": { 285 | "type": "git", 286 | "url": "https://github.com/php-fig/container.git", 287 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" 288 | }, 289 | "dist": { 290 | "type": "zip", 291 | "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 292 | "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", 293 | "shasum": "" 294 | }, 295 | "require": { 296 | "php": ">=5.3.0" 297 | }, 298 | "type": "library", 299 | "extra": { 300 | "branch-alias": { 301 | "dev-master": "1.0.x-dev" 302 | } 303 | }, 304 | "autoload": { 305 | "psr-4": { 306 | "Psr\\Container\\": "src/" 307 | } 308 | }, 309 | "notification-url": "https://packagist.org/downloads/", 310 | "license": [ 311 | "MIT" 312 | ], 313 | "authors": [ 314 | { 315 | "name": "PHP-FIG", 316 | "homepage": "http://www.php-fig.org/" 317 | } 318 | ], 319 | "description": "Common Container Interface (PHP FIG PSR-11)", 320 | "homepage": "https://github.com/php-fig/container", 321 | "keywords": [ 322 | "PSR-11", 323 | "container", 324 | "container-interface", 325 | "container-interop", 326 | "psr" 327 | ], 328 | "support": { 329 | "issues": "https://github.com/php-fig/container/issues", 330 | "source": "https://github.com/php-fig/container/tree/master" 331 | }, 332 | "time": "2017-02-14T16:28:37+00:00" 333 | }, 334 | { 335 | "name": "psr/http-client", 336 | "version": "1.0.1", 337 | "source": { 338 | "type": "git", 339 | "url": "https://github.com/php-fig/http-client.git", 340 | "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621" 341 | }, 342 | "dist": { 343 | "type": "zip", 344 | "url": "https://api.github.com/repos/php-fig/http-client/zipball/2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", 345 | "reference": "2dfb5f6c5eff0e91e20e913f8c5452ed95b86621", 346 | "shasum": "" 347 | }, 348 | "require": { 349 | "php": "^7.0 || ^8.0", 350 | "psr/http-message": "^1.0" 351 | }, 352 | "type": "library", 353 | "extra": { 354 | "branch-alias": { 355 | "dev-master": "1.0.x-dev" 356 | } 357 | }, 358 | "autoload": { 359 | "psr-4": { 360 | "Psr\\Http\\Client\\": "src/" 361 | } 362 | }, 363 | "notification-url": "https://packagist.org/downloads/", 364 | "license": [ 365 | "MIT" 366 | ], 367 | "authors": [ 368 | { 369 | "name": "PHP-FIG", 370 | "homepage": "http://www.php-fig.org/" 371 | } 372 | ], 373 | "description": "Common interface for HTTP clients", 374 | "homepage": "https://github.com/php-fig/http-client", 375 | "keywords": [ 376 | "http", 377 | "http-client", 378 | "psr", 379 | "psr-18" 380 | ], 381 | "time": "2020-06-29T06:28:15+00:00" 382 | }, 383 | { 384 | "name": "psr/http-message", 385 | "version": "1.0.1", 386 | "source": { 387 | "type": "git", 388 | "url": "https://github.com/php-fig/http-message.git", 389 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" 390 | }, 391 | "dist": { 392 | "type": "zip", 393 | "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", 394 | "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", 395 | "shasum": "" 396 | }, 397 | "require": { 398 | "php": ">=5.3.0" 399 | }, 400 | "type": "library", 401 | "extra": { 402 | "branch-alias": { 403 | "dev-master": "1.0.x-dev" 404 | } 405 | }, 406 | "autoload": { 407 | "psr-4": { 408 | "Psr\\Http\\Message\\": "src/" 409 | } 410 | }, 411 | "notification-url": "https://packagist.org/downloads/", 412 | "license": [ 413 | "MIT" 414 | ], 415 | "authors": [ 416 | { 417 | "name": "PHP-FIG", 418 | "homepage": "http://www.php-fig.org/" 419 | } 420 | ], 421 | "description": "Common interface for HTTP messages", 422 | "homepage": "https://github.com/php-fig/http-message", 423 | "keywords": [ 424 | "http", 425 | "http-message", 426 | "psr", 427 | "psr-7", 428 | "request", 429 | "response" 430 | ], 431 | "time": "2016-08-06T14:39:51+00:00" 432 | }, 433 | { 434 | "name": "ralouphie/getallheaders", 435 | "version": "3.0.3", 436 | "source": { 437 | "type": "git", 438 | "url": "https://github.com/ralouphie/getallheaders.git", 439 | "reference": "120b605dfeb996808c31b6477290a714d356e822" 440 | }, 441 | "dist": { 442 | "type": "zip", 443 | "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822", 444 | "reference": "120b605dfeb996808c31b6477290a714d356e822", 445 | "shasum": "" 446 | }, 447 | "require": { 448 | "php": ">=5.6" 449 | }, 450 | "require-dev": { 451 | "php-coveralls/php-coveralls": "^2.1", 452 | "phpunit/phpunit": "^5 || ^6.5" 453 | }, 454 | "type": "library", 455 | "autoload": { 456 | "files": [ 457 | "src/getallheaders.php" 458 | ] 459 | }, 460 | "notification-url": "https://packagist.org/downloads/", 461 | "license": [ 462 | "MIT" 463 | ], 464 | "authors": [ 465 | { 466 | "name": "Ralph Khattar", 467 | "email": "ralph.khattar@gmail.com" 468 | } 469 | ], 470 | "description": "A polyfill for getallheaders.", 471 | "time": "2019-03-08T08:55:37+00:00" 472 | }, 473 | { 474 | "name": "ramsey/collection", 475 | "version": "1.1.3", 476 | "source": { 477 | "type": "git", 478 | "url": "https://github.com/ramsey/collection.git", 479 | "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1" 480 | }, 481 | "dist": { 482 | "type": "zip", 483 | "url": "https://api.github.com/repos/ramsey/collection/zipball/28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", 484 | "reference": "28a5c4ab2f5111db6a60b2b4ec84057e0f43b9c1", 485 | "shasum": "" 486 | }, 487 | "require": { 488 | "php": "^7.2 || ^8" 489 | }, 490 | "require-dev": { 491 | "captainhook/captainhook": "^5.3", 492 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", 493 | "ergebnis/composer-normalize": "^2.6", 494 | "fakerphp/faker": "^1.5", 495 | "hamcrest/hamcrest-php": "^2", 496 | "jangregor/phpstan-prophecy": "^0.8", 497 | "mockery/mockery": "^1.3", 498 | "phpstan/extension-installer": "^1", 499 | "phpstan/phpstan": "^0.12.32", 500 | "phpstan/phpstan-mockery": "^0.12.5", 501 | "phpstan/phpstan-phpunit": "^0.12.11", 502 | "phpunit/phpunit": "^8.5 || ^9", 503 | "psy/psysh": "^0.10.4", 504 | "slevomat/coding-standard": "^6.3", 505 | "squizlabs/php_codesniffer": "^3.5", 506 | "vimeo/psalm": "^4.4" 507 | }, 508 | "type": "library", 509 | "autoload": { 510 | "psr-4": { 511 | "Ramsey\\Collection\\": "src/" 512 | } 513 | }, 514 | "notification-url": "https://packagist.org/downloads/", 515 | "license": [ 516 | "MIT" 517 | ], 518 | "authors": [ 519 | { 520 | "name": "Ben Ramsey", 521 | "email": "ben@benramsey.com", 522 | "homepage": "https://benramsey.com" 523 | } 524 | ], 525 | "description": "A PHP 7.2+ library for representing and manipulating collections.", 526 | "keywords": [ 527 | "array", 528 | "collection", 529 | "hash", 530 | "map", 531 | "queue", 532 | "set" 533 | ], 534 | "funding": [ 535 | { 536 | "url": "https://github.com/ramsey", 537 | "type": "github" 538 | }, 539 | { 540 | "url": "https://tidelift.com/funding/github/packagist/ramsey/collection", 541 | "type": "tidelift" 542 | } 543 | ], 544 | "time": "2021-01-21T17:40:04+00:00" 545 | }, 546 | { 547 | "name": "ramsey/uuid", 548 | "version": "4.1.1", 549 | "source": { 550 | "type": "git", 551 | "url": "https://github.com/ramsey/uuid.git", 552 | "reference": "cd4032040a750077205918c86049aa0f43d22947" 553 | }, 554 | "dist": { 555 | "type": "zip", 556 | "url": "https://api.github.com/repos/ramsey/uuid/zipball/cd4032040a750077205918c86049aa0f43d22947", 557 | "reference": "cd4032040a750077205918c86049aa0f43d22947", 558 | "shasum": "" 559 | }, 560 | "require": { 561 | "brick/math": "^0.8 || ^0.9", 562 | "ext-json": "*", 563 | "php": "^7.2 || ^8", 564 | "ramsey/collection": "^1.0", 565 | "symfony/polyfill-ctype": "^1.8" 566 | }, 567 | "replace": { 568 | "rhumsaa/uuid": "self.version" 569 | }, 570 | "require-dev": { 571 | "codeception/aspect-mock": "^3", 572 | "dealerdirect/phpcodesniffer-composer-installer": "^0.6.2 || ^0.7.0", 573 | "doctrine/annotations": "^1.8", 574 | "goaop/framework": "^2", 575 | "mockery/mockery": "^1.3", 576 | "moontoast/math": "^1.1", 577 | "paragonie/random-lib": "^2", 578 | "php-mock/php-mock-mockery": "^1.3", 579 | "php-mock/php-mock-phpunit": "^2.5", 580 | "php-parallel-lint/php-parallel-lint": "^1.1", 581 | "phpbench/phpbench": "^0.17.1", 582 | "phpstan/extension-installer": "^1.0", 583 | "phpstan/phpstan": "^0.12", 584 | "phpstan/phpstan-mockery": "^0.12", 585 | "phpstan/phpstan-phpunit": "^0.12", 586 | "phpunit/phpunit": "^8.5", 587 | "psy/psysh": "^0.10.0", 588 | "slevomat/coding-standard": "^6.0", 589 | "squizlabs/php_codesniffer": "^3.5", 590 | "vimeo/psalm": "3.9.4" 591 | }, 592 | "suggest": { 593 | "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.", 594 | "ext-ctype": "Enables faster processing of character classification using ctype functions.", 595 | "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.", 596 | "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.", 597 | "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter", 598 | "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type." 599 | }, 600 | "type": "library", 601 | "extra": { 602 | "branch-alias": { 603 | "dev-master": "4.x-dev" 604 | } 605 | }, 606 | "autoload": { 607 | "psr-4": { 608 | "Ramsey\\Uuid\\": "src/" 609 | }, 610 | "files": [ 611 | "src/functions.php" 612 | ] 613 | }, 614 | "notification-url": "https://packagist.org/downloads/", 615 | "license": [ 616 | "MIT" 617 | ], 618 | "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).", 619 | "homepage": "https://github.com/ramsey/uuid", 620 | "keywords": [ 621 | "guid", 622 | "identifier", 623 | "uuid" 624 | ], 625 | "funding": [ 626 | { 627 | "url": "https://github.com/ramsey", 628 | "type": "github" 629 | } 630 | ], 631 | "time": "2020-08-18T17:17:46+00:00" 632 | }, 633 | { 634 | "name": "symfony/console", 635 | "version": "v5.2.2", 636 | "source": { 637 | "type": "git", 638 | "url": "https://github.com/symfony/console.git", 639 | "reference": "d62ec79478b55036f65e2602e282822b8eaaff0a" 640 | }, 641 | "dist": { 642 | "type": "zip", 643 | "url": "https://api.github.com/repos/symfony/console/zipball/d62ec79478b55036f65e2602e282822b8eaaff0a", 644 | "reference": "d62ec79478b55036f65e2602e282822b8eaaff0a", 645 | "shasum": "" 646 | }, 647 | "require": { 648 | "php": ">=7.2.5", 649 | "symfony/polyfill-mbstring": "~1.0", 650 | "symfony/polyfill-php73": "^1.8", 651 | "symfony/polyfill-php80": "^1.15", 652 | "symfony/service-contracts": "^1.1|^2", 653 | "symfony/string": "^5.1" 654 | }, 655 | "conflict": { 656 | "symfony/dependency-injection": "<4.4", 657 | "symfony/dotenv": "<5.1", 658 | "symfony/event-dispatcher": "<4.4", 659 | "symfony/lock": "<4.4", 660 | "symfony/process": "<4.4" 661 | }, 662 | "provide": { 663 | "psr/log-implementation": "1.0" 664 | }, 665 | "require-dev": { 666 | "psr/log": "~1.0", 667 | "symfony/config": "^4.4|^5.0", 668 | "symfony/dependency-injection": "^4.4|^5.0", 669 | "symfony/event-dispatcher": "^4.4|^5.0", 670 | "symfony/lock": "^4.4|^5.0", 671 | "symfony/process": "^4.4|^5.0", 672 | "symfony/var-dumper": "^4.4|^5.0" 673 | }, 674 | "suggest": { 675 | "psr/log": "For using the console logger", 676 | "symfony/event-dispatcher": "", 677 | "symfony/lock": "", 678 | "symfony/process": "" 679 | }, 680 | "type": "library", 681 | "autoload": { 682 | "psr-4": { 683 | "Symfony\\Component\\Console\\": "" 684 | }, 685 | "exclude-from-classmap": [ 686 | "/Tests/" 687 | ] 688 | }, 689 | "notification-url": "https://packagist.org/downloads/", 690 | "license": [ 691 | "MIT" 692 | ], 693 | "authors": [ 694 | { 695 | "name": "Fabien Potencier", 696 | "email": "fabien@symfony.com" 697 | }, 698 | { 699 | "name": "Symfony Community", 700 | "homepage": "https://symfony.com/contributors" 701 | } 702 | ], 703 | "description": "Eases the creation of beautiful and testable command line interfaces", 704 | "homepage": "https://symfony.com", 705 | "keywords": [ 706 | "cli", 707 | "command line", 708 | "console", 709 | "terminal" 710 | ], 711 | "support": { 712 | "source": "https://github.com/symfony/console/tree/v5.2.2" 713 | }, 714 | "funding": [ 715 | { 716 | "url": "https://symfony.com/sponsor", 717 | "type": "custom" 718 | }, 719 | { 720 | "url": "https://github.com/fabpot", 721 | "type": "github" 722 | }, 723 | { 724 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 725 | "type": "tidelift" 726 | } 727 | ], 728 | "time": "2021-01-27T10:15:41+00:00" 729 | }, 730 | { 731 | "name": "symfony/deprecation-contracts", 732 | "version": "v2.2.0", 733 | "source": { 734 | "type": "git", 735 | "url": "https://github.com/symfony/deprecation-contracts.git", 736 | "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" 737 | }, 738 | "dist": { 739 | "type": "zip", 740 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", 741 | "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", 742 | "shasum": "" 743 | }, 744 | "require": { 745 | "php": ">=7.1" 746 | }, 747 | "type": "library", 748 | "extra": { 749 | "branch-alias": { 750 | "dev-master": "2.2-dev" 751 | }, 752 | "thanks": { 753 | "name": "symfony/contracts", 754 | "url": "https://github.com/symfony/contracts" 755 | } 756 | }, 757 | "autoload": { 758 | "files": [ 759 | "function.php" 760 | ] 761 | }, 762 | "notification-url": "https://packagist.org/downloads/", 763 | "license": [ 764 | "MIT" 765 | ], 766 | "authors": [ 767 | { 768 | "name": "Nicolas Grekas", 769 | "email": "p@tchwork.com" 770 | }, 771 | { 772 | "name": "Symfony Community", 773 | "homepage": "https://symfony.com/contributors" 774 | } 775 | ], 776 | "description": "A generic function and convention to trigger deprecation notices", 777 | "homepage": "https://symfony.com", 778 | "funding": [ 779 | { 780 | "url": "https://symfony.com/sponsor", 781 | "type": "custom" 782 | }, 783 | { 784 | "url": "https://github.com/fabpot", 785 | "type": "github" 786 | }, 787 | { 788 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 789 | "type": "tidelift" 790 | } 791 | ], 792 | "time": "2020-09-07T11:33:47+00:00" 793 | }, 794 | { 795 | "name": "symfony/filesystem", 796 | "version": "v5.2.4", 797 | "source": { 798 | "type": "git", 799 | "url": "https://github.com/symfony/filesystem.git", 800 | "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108" 801 | }, 802 | "dist": { 803 | "type": "zip", 804 | "url": "https://api.github.com/repos/symfony/filesystem/zipball/710d364200997a5afde34d9fe57bd52f3cc1e108", 805 | "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108", 806 | "shasum": "" 807 | }, 808 | "require": { 809 | "php": ">=7.2.5", 810 | "symfony/polyfill-ctype": "~1.8" 811 | }, 812 | "type": "library", 813 | "autoload": { 814 | "psr-4": { 815 | "Symfony\\Component\\Filesystem\\": "" 816 | }, 817 | "exclude-from-classmap": [ 818 | "/Tests/" 819 | ] 820 | }, 821 | "notification-url": "https://packagist.org/downloads/", 822 | "license": [ 823 | "MIT" 824 | ], 825 | "authors": [ 826 | { 827 | "name": "Fabien Potencier", 828 | "email": "fabien@symfony.com" 829 | }, 830 | { 831 | "name": "Symfony Community", 832 | "homepage": "https://symfony.com/contributors" 833 | } 834 | ], 835 | "description": "Provides basic utilities for the filesystem", 836 | "homepage": "https://symfony.com", 837 | "funding": [ 838 | { 839 | "url": "https://symfony.com/sponsor", 840 | "type": "custom" 841 | }, 842 | { 843 | "url": "https://github.com/fabpot", 844 | "type": "github" 845 | }, 846 | { 847 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 848 | "type": "tidelift" 849 | } 850 | ], 851 | "time": "2021-02-12T10:38:38+00:00" 852 | }, 853 | { 854 | "name": "symfony/finder", 855 | "version": "v5.2.4", 856 | "source": { 857 | "type": "git", 858 | "url": "https://github.com/symfony/finder.git", 859 | "reference": "0d639a0943822626290d169965804f79400e6a04" 860 | }, 861 | "dist": { 862 | "type": "zip", 863 | "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", 864 | "reference": "0d639a0943822626290d169965804f79400e6a04", 865 | "shasum": "" 866 | }, 867 | "require": { 868 | "php": ">=7.2.5" 869 | }, 870 | "type": "library", 871 | "autoload": { 872 | "psr-4": { 873 | "Symfony\\Component\\Finder\\": "" 874 | }, 875 | "exclude-from-classmap": [ 876 | "/Tests/" 877 | ] 878 | }, 879 | "notification-url": "https://packagist.org/downloads/", 880 | "license": [ 881 | "MIT" 882 | ], 883 | "authors": [ 884 | { 885 | "name": "Fabien Potencier", 886 | "email": "fabien@symfony.com" 887 | }, 888 | { 889 | "name": "Symfony Community", 890 | "homepage": "https://symfony.com/contributors" 891 | } 892 | ], 893 | "description": "Finds files and directories via an intuitive fluent interface", 894 | "homepage": "https://symfony.com", 895 | "funding": [ 896 | { 897 | "url": "https://symfony.com/sponsor", 898 | "type": "custom" 899 | }, 900 | { 901 | "url": "https://github.com/fabpot", 902 | "type": "github" 903 | }, 904 | { 905 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 906 | "type": "tidelift" 907 | } 908 | ], 909 | "time": "2021-02-15T18:55:04+00:00" 910 | }, 911 | { 912 | "name": "symfony/polyfill-ctype", 913 | "version": "v1.22.0", 914 | "source": { 915 | "type": "git", 916 | "url": "https://github.com/symfony/polyfill-ctype.git", 917 | "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" 918 | }, 919 | "dist": { 920 | "type": "zip", 921 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", 922 | "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", 923 | "shasum": "" 924 | }, 925 | "require": { 926 | "php": ">=7.1" 927 | }, 928 | "suggest": { 929 | "ext-ctype": "For best performance" 930 | }, 931 | "type": "library", 932 | "extra": { 933 | "branch-alias": { 934 | "dev-main": "1.22-dev" 935 | }, 936 | "thanks": { 937 | "name": "symfony/polyfill", 938 | "url": "https://github.com/symfony/polyfill" 939 | } 940 | }, 941 | "autoload": { 942 | "psr-4": { 943 | "Symfony\\Polyfill\\Ctype\\": "" 944 | }, 945 | "files": [ 946 | "bootstrap.php" 947 | ] 948 | }, 949 | "notification-url": "https://packagist.org/downloads/", 950 | "license": [ 951 | "MIT" 952 | ], 953 | "authors": [ 954 | { 955 | "name": "Gert de Pagter", 956 | "email": "BackEndTea@gmail.com" 957 | }, 958 | { 959 | "name": "Symfony Community", 960 | "homepage": "https://symfony.com/contributors" 961 | } 962 | ], 963 | "description": "Symfony polyfill for ctype functions", 964 | "homepage": "https://symfony.com", 965 | "keywords": [ 966 | "compatibility", 967 | "ctype", 968 | "polyfill", 969 | "portable" 970 | ], 971 | "support": { 972 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.0" 973 | }, 974 | "funding": [ 975 | { 976 | "url": "https://symfony.com/sponsor", 977 | "type": "custom" 978 | }, 979 | { 980 | "url": "https://github.com/fabpot", 981 | "type": "github" 982 | }, 983 | { 984 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 985 | "type": "tidelift" 986 | } 987 | ], 988 | "time": "2021-01-07T16:49:33+00:00" 989 | }, 990 | { 991 | "name": "symfony/polyfill-intl-grapheme", 992 | "version": "v1.22.0", 993 | "source": { 994 | "type": "git", 995 | "url": "https://github.com/symfony/polyfill-intl-grapheme.git", 996 | "reference": "267a9adeb8ecb8071040a740930e077cdfb987af" 997 | }, 998 | "dist": { 999 | "type": "zip", 1000 | "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af", 1001 | "reference": "267a9adeb8ecb8071040a740930e077cdfb987af", 1002 | "shasum": "" 1003 | }, 1004 | "require": { 1005 | "php": ">=7.1" 1006 | }, 1007 | "suggest": { 1008 | "ext-intl": "For best performance" 1009 | }, 1010 | "type": "library", 1011 | "extra": { 1012 | "branch-alias": { 1013 | "dev-main": "1.22-dev" 1014 | }, 1015 | "thanks": { 1016 | "name": "symfony/polyfill", 1017 | "url": "https://github.com/symfony/polyfill" 1018 | } 1019 | }, 1020 | "autoload": { 1021 | "psr-4": { 1022 | "Symfony\\Polyfill\\Intl\\Grapheme\\": "" 1023 | }, 1024 | "files": [ 1025 | "bootstrap.php" 1026 | ] 1027 | }, 1028 | "notification-url": "https://packagist.org/downloads/", 1029 | "license": [ 1030 | "MIT" 1031 | ], 1032 | "authors": [ 1033 | { 1034 | "name": "Nicolas Grekas", 1035 | "email": "p@tchwork.com" 1036 | }, 1037 | { 1038 | "name": "Symfony Community", 1039 | "homepage": "https://symfony.com/contributors" 1040 | } 1041 | ], 1042 | "description": "Symfony polyfill for intl's grapheme_* functions", 1043 | "homepage": "https://symfony.com", 1044 | "keywords": [ 1045 | "compatibility", 1046 | "grapheme", 1047 | "intl", 1048 | "polyfill", 1049 | "portable", 1050 | "shim" 1051 | ], 1052 | "support": { 1053 | "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.0" 1054 | }, 1055 | "funding": [ 1056 | { 1057 | "url": "https://symfony.com/sponsor", 1058 | "type": "custom" 1059 | }, 1060 | { 1061 | "url": "https://github.com/fabpot", 1062 | "type": "github" 1063 | }, 1064 | { 1065 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1066 | "type": "tidelift" 1067 | } 1068 | ], 1069 | "time": "2021-01-07T16:49:33+00:00" 1070 | }, 1071 | { 1072 | "name": "symfony/polyfill-intl-normalizer", 1073 | "version": "v1.22.0", 1074 | "source": { 1075 | "type": "git", 1076 | "url": "https://github.com/symfony/polyfill-intl-normalizer.git", 1077 | "reference": "6e971c891537eb617a00bb07a43d182a6915faba" 1078 | }, 1079 | "dist": { 1080 | "type": "zip", 1081 | "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", 1082 | "reference": "6e971c891537eb617a00bb07a43d182a6915faba", 1083 | "shasum": "" 1084 | }, 1085 | "require": { 1086 | "php": ">=7.1" 1087 | }, 1088 | "suggest": { 1089 | "ext-intl": "For best performance" 1090 | }, 1091 | "type": "library", 1092 | "extra": { 1093 | "branch-alias": { 1094 | "dev-main": "1.22-dev" 1095 | }, 1096 | "thanks": { 1097 | "name": "symfony/polyfill", 1098 | "url": "https://github.com/symfony/polyfill" 1099 | } 1100 | }, 1101 | "autoload": { 1102 | "psr-4": { 1103 | "Symfony\\Polyfill\\Intl\\Normalizer\\": "" 1104 | }, 1105 | "files": [ 1106 | "bootstrap.php" 1107 | ], 1108 | "classmap": [ 1109 | "Resources/stubs" 1110 | ] 1111 | }, 1112 | "notification-url": "https://packagist.org/downloads/", 1113 | "license": [ 1114 | "MIT" 1115 | ], 1116 | "authors": [ 1117 | { 1118 | "name": "Nicolas Grekas", 1119 | "email": "p@tchwork.com" 1120 | }, 1121 | { 1122 | "name": "Symfony Community", 1123 | "homepage": "https://symfony.com/contributors" 1124 | } 1125 | ], 1126 | "description": "Symfony polyfill for intl's Normalizer class and related functions", 1127 | "homepage": "https://symfony.com", 1128 | "keywords": [ 1129 | "compatibility", 1130 | "intl", 1131 | "normalizer", 1132 | "polyfill", 1133 | "portable", 1134 | "shim" 1135 | ], 1136 | "support": { 1137 | "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.0" 1138 | }, 1139 | "funding": [ 1140 | { 1141 | "url": "https://symfony.com/sponsor", 1142 | "type": "custom" 1143 | }, 1144 | { 1145 | "url": "https://github.com/fabpot", 1146 | "type": "github" 1147 | }, 1148 | { 1149 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1150 | "type": "tidelift" 1151 | } 1152 | ], 1153 | "time": "2021-01-07T17:09:11+00:00" 1154 | }, 1155 | { 1156 | "name": "symfony/polyfill-mbstring", 1157 | "version": "v1.22.0", 1158 | "source": { 1159 | "type": "git", 1160 | "url": "https://github.com/symfony/polyfill-mbstring.git", 1161 | "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" 1162 | }, 1163 | "dist": { 1164 | "type": "zip", 1165 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", 1166 | "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", 1167 | "shasum": "" 1168 | }, 1169 | "require": { 1170 | "php": ">=7.1" 1171 | }, 1172 | "suggest": { 1173 | "ext-mbstring": "For best performance" 1174 | }, 1175 | "type": "library", 1176 | "extra": { 1177 | "branch-alias": { 1178 | "dev-main": "1.22-dev" 1179 | }, 1180 | "thanks": { 1181 | "name": "symfony/polyfill", 1182 | "url": "https://github.com/symfony/polyfill" 1183 | } 1184 | }, 1185 | "autoload": { 1186 | "psr-4": { 1187 | "Symfony\\Polyfill\\Mbstring\\": "" 1188 | }, 1189 | "files": [ 1190 | "bootstrap.php" 1191 | ] 1192 | }, 1193 | "notification-url": "https://packagist.org/downloads/", 1194 | "license": [ 1195 | "MIT" 1196 | ], 1197 | "authors": [ 1198 | { 1199 | "name": "Nicolas Grekas", 1200 | "email": "p@tchwork.com" 1201 | }, 1202 | { 1203 | "name": "Symfony Community", 1204 | "homepage": "https://symfony.com/contributors" 1205 | } 1206 | ], 1207 | "description": "Symfony polyfill for the Mbstring extension", 1208 | "homepage": "https://symfony.com", 1209 | "keywords": [ 1210 | "compatibility", 1211 | "mbstring", 1212 | "polyfill", 1213 | "portable", 1214 | "shim" 1215 | ], 1216 | "support": { 1217 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.0" 1218 | }, 1219 | "funding": [ 1220 | { 1221 | "url": "https://symfony.com/sponsor", 1222 | "type": "custom" 1223 | }, 1224 | { 1225 | "url": "https://github.com/fabpot", 1226 | "type": "github" 1227 | }, 1228 | { 1229 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1230 | "type": "tidelift" 1231 | } 1232 | ], 1233 | "time": "2021-01-07T16:49:33+00:00" 1234 | }, 1235 | { 1236 | "name": "symfony/polyfill-php73", 1237 | "version": "v1.22.0", 1238 | "source": { 1239 | "type": "git", 1240 | "url": "https://github.com/symfony/polyfill-php73.git", 1241 | "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" 1242 | }, 1243 | "dist": { 1244 | "type": "zip", 1245 | "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", 1246 | "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", 1247 | "shasum": "" 1248 | }, 1249 | "require": { 1250 | "php": ">=7.1" 1251 | }, 1252 | "type": "library", 1253 | "extra": { 1254 | "branch-alias": { 1255 | "dev-main": "1.22-dev" 1256 | }, 1257 | "thanks": { 1258 | "name": "symfony/polyfill", 1259 | "url": "https://github.com/symfony/polyfill" 1260 | } 1261 | }, 1262 | "autoload": { 1263 | "psr-4": { 1264 | "Symfony\\Polyfill\\Php73\\": "" 1265 | }, 1266 | "files": [ 1267 | "bootstrap.php" 1268 | ], 1269 | "classmap": [ 1270 | "Resources/stubs" 1271 | ] 1272 | }, 1273 | "notification-url": "https://packagist.org/downloads/", 1274 | "license": [ 1275 | "MIT" 1276 | ], 1277 | "authors": [ 1278 | { 1279 | "name": "Nicolas Grekas", 1280 | "email": "p@tchwork.com" 1281 | }, 1282 | { 1283 | "name": "Symfony Community", 1284 | "homepage": "https://symfony.com/contributors" 1285 | } 1286 | ], 1287 | "description": "Symfony polyfill backporting some PHP 7.3+ features to lower PHP versions", 1288 | "homepage": "https://symfony.com", 1289 | "keywords": [ 1290 | "compatibility", 1291 | "polyfill", 1292 | "portable", 1293 | "shim" 1294 | ], 1295 | "support": { 1296 | "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.0" 1297 | }, 1298 | "funding": [ 1299 | { 1300 | "url": "https://symfony.com/sponsor", 1301 | "type": "custom" 1302 | }, 1303 | { 1304 | "url": "https://github.com/fabpot", 1305 | "type": "github" 1306 | }, 1307 | { 1308 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1309 | "type": "tidelift" 1310 | } 1311 | ], 1312 | "time": "2021-01-07T16:49:33+00:00" 1313 | }, 1314 | { 1315 | "name": "symfony/polyfill-php80", 1316 | "version": "v1.22.0", 1317 | "source": { 1318 | "type": "git", 1319 | "url": "https://github.com/symfony/polyfill-php80.git", 1320 | "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" 1321 | }, 1322 | "dist": { 1323 | "type": "zip", 1324 | "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", 1325 | "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", 1326 | "shasum": "" 1327 | }, 1328 | "require": { 1329 | "php": ">=7.1" 1330 | }, 1331 | "type": "library", 1332 | "extra": { 1333 | "branch-alias": { 1334 | "dev-main": "1.22-dev" 1335 | }, 1336 | "thanks": { 1337 | "name": "symfony/polyfill", 1338 | "url": "https://github.com/symfony/polyfill" 1339 | } 1340 | }, 1341 | "autoload": { 1342 | "psr-4": { 1343 | "Symfony\\Polyfill\\Php80\\": "" 1344 | }, 1345 | "files": [ 1346 | "bootstrap.php" 1347 | ], 1348 | "classmap": [ 1349 | "Resources/stubs" 1350 | ] 1351 | }, 1352 | "notification-url": "https://packagist.org/downloads/", 1353 | "license": [ 1354 | "MIT" 1355 | ], 1356 | "authors": [ 1357 | { 1358 | "name": "Ion Bazan", 1359 | "email": "ion.bazan@gmail.com" 1360 | }, 1361 | { 1362 | "name": "Nicolas Grekas", 1363 | "email": "p@tchwork.com" 1364 | }, 1365 | { 1366 | "name": "Symfony Community", 1367 | "homepage": "https://symfony.com/contributors" 1368 | } 1369 | ], 1370 | "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions", 1371 | "homepage": "https://symfony.com", 1372 | "keywords": [ 1373 | "compatibility", 1374 | "polyfill", 1375 | "portable", 1376 | "shim" 1377 | ], 1378 | "support": { 1379 | "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.0" 1380 | }, 1381 | "funding": [ 1382 | { 1383 | "url": "https://symfony.com/sponsor", 1384 | "type": "custom" 1385 | }, 1386 | { 1387 | "url": "https://github.com/fabpot", 1388 | "type": "github" 1389 | }, 1390 | { 1391 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1392 | "type": "tidelift" 1393 | } 1394 | ], 1395 | "time": "2021-01-07T16:49:33+00:00" 1396 | }, 1397 | { 1398 | "name": "symfony/process", 1399 | "version": "v5.2.4", 1400 | "source": { 1401 | "type": "git", 1402 | "url": "https://github.com/symfony/process.git", 1403 | "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f" 1404 | }, 1405 | "dist": { 1406 | "type": "zip", 1407 | "url": "https://api.github.com/repos/symfony/process/zipball/313a38f09c77fbcdc1d223e57d368cea76a2fd2f", 1408 | "reference": "313a38f09c77fbcdc1d223e57d368cea76a2fd2f", 1409 | "shasum": "" 1410 | }, 1411 | "require": { 1412 | "php": ">=7.2.5", 1413 | "symfony/polyfill-php80": "^1.15" 1414 | }, 1415 | "type": "library", 1416 | "autoload": { 1417 | "psr-4": { 1418 | "Symfony\\Component\\Process\\": "" 1419 | }, 1420 | "exclude-from-classmap": [ 1421 | "/Tests/" 1422 | ] 1423 | }, 1424 | "notification-url": "https://packagist.org/downloads/", 1425 | "license": [ 1426 | "MIT" 1427 | ], 1428 | "authors": [ 1429 | { 1430 | "name": "Fabien Potencier", 1431 | "email": "fabien@symfony.com" 1432 | }, 1433 | { 1434 | "name": "Symfony Community", 1435 | "homepage": "https://symfony.com/contributors" 1436 | } 1437 | ], 1438 | "description": "Executes commands in sub-processes", 1439 | "homepage": "https://symfony.com", 1440 | "funding": [ 1441 | { 1442 | "url": "https://symfony.com/sponsor", 1443 | "type": "custom" 1444 | }, 1445 | { 1446 | "url": "https://github.com/fabpot", 1447 | "type": "github" 1448 | }, 1449 | { 1450 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1451 | "type": "tidelift" 1452 | } 1453 | ], 1454 | "time": "2021-01-27T10:15:41+00:00" 1455 | }, 1456 | { 1457 | "name": "symfony/service-contracts", 1458 | "version": "v2.2.0", 1459 | "source": { 1460 | "type": "git", 1461 | "url": "https://github.com/symfony/service-contracts.git", 1462 | "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" 1463 | }, 1464 | "dist": { 1465 | "type": "zip", 1466 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", 1467 | "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", 1468 | "shasum": "" 1469 | }, 1470 | "require": { 1471 | "php": ">=7.2.5", 1472 | "psr/container": "^1.0" 1473 | }, 1474 | "suggest": { 1475 | "symfony/service-implementation": "" 1476 | }, 1477 | "type": "library", 1478 | "extra": { 1479 | "branch-alias": { 1480 | "dev-master": "2.2-dev" 1481 | }, 1482 | "thanks": { 1483 | "name": "symfony/contracts", 1484 | "url": "https://github.com/symfony/contracts" 1485 | } 1486 | }, 1487 | "autoload": { 1488 | "psr-4": { 1489 | "Symfony\\Contracts\\Service\\": "" 1490 | } 1491 | }, 1492 | "notification-url": "https://packagist.org/downloads/", 1493 | "license": [ 1494 | "MIT" 1495 | ], 1496 | "authors": [ 1497 | { 1498 | "name": "Nicolas Grekas", 1499 | "email": "p@tchwork.com" 1500 | }, 1501 | { 1502 | "name": "Symfony Community", 1503 | "homepage": "https://symfony.com/contributors" 1504 | } 1505 | ], 1506 | "description": "Generic abstractions related to writing services", 1507 | "homepage": "https://symfony.com", 1508 | "keywords": [ 1509 | "abstractions", 1510 | "contracts", 1511 | "decoupling", 1512 | "interfaces", 1513 | "interoperability", 1514 | "standards" 1515 | ], 1516 | "support": { 1517 | "source": "https://github.com/symfony/service-contracts/tree/master" 1518 | }, 1519 | "funding": [ 1520 | { 1521 | "url": "https://symfony.com/sponsor", 1522 | "type": "custom" 1523 | }, 1524 | { 1525 | "url": "https://github.com/fabpot", 1526 | "type": "github" 1527 | }, 1528 | { 1529 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1530 | "type": "tidelift" 1531 | } 1532 | ], 1533 | "time": "2020-09-07T11:33:47+00:00" 1534 | }, 1535 | { 1536 | "name": "symfony/string", 1537 | "version": "v5.2.2", 1538 | "source": { 1539 | "type": "git", 1540 | "url": "https://github.com/symfony/string.git", 1541 | "reference": "c95468897f408dd0aca2ff582074423dd0455122" 1542 | }, 1543 | "dist": { 1544 | "type": "zip", 1545 | "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", 1546 | "reference": "c95468897f408dd0aca2ff582074423dd0455122", 1547 | "shasum": "" 1548 | }, 1549 | "require": { 1550 | "php": ">=7.2.5", 1551 | "symfony/polyfill-ctype": "~1.8", 1552 | "symfony/polyfill-intl-grapheme": "~1.0", 1553 | "symfony/polyfill-intl-normalizer": "~1.0", 1554 | "symfony/polyfill-mbstring": "~1.0", 1555 | "symfony/polyfill-php80": "~1.15" 1556 | }, 1557 | "require-dev": { 1558 | "symfony/error-handler": "^4.4|^5.0", 1559 | "symfony/http-client": "^4.4|^5.0", 1560 | "symfony/translation-contracts": "^1.1|^2", 1561 | "symfony/var-exporter": "^4.4|^5.0" 1562 | }, 1563 | "type": "library", 1564 | "autoload": { 1565 | "psr-4": { 1566 | "Symfony\\Component\\String\\": "" 1567 | }, 1568 | "files": [ 1569 | "Resources/functions.php" 1570 | ], 1571 | "exclude-from-classmap": [ 1572 | "/Tests/" 1573 | ] 1574 | }, 1575 | "notification-url": "https://packagist.org/downloads/", 1576 | "license": [ 1577 | "MIT" 1578 | ], 1579 | "authors": [ 1580 | { 1581 | "name": "Nicolas Grekas", 1582 | "email": "p@tchwork.com" 1583 | }, 1584 | { 1585 | "name": "Symfony Community", 1586 | "homepage": "https://symfony.com/contributors" 1587 | } 1588 | ], 1589 | "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", 1590 | "homepage": "https://symfony.com", 1591 | "keywords": [ 1592 | "grapheme", 1593 | "i18n", 1594 | "string", 1595 | "unicode", 1596 | "utf-8", 1597 | "utf8" 1598 | ], 1599 | "support": { 1600 | "source": "https://github.com/symfony/string/tree/v5.2.2" 1601 | }, 1602 | "funding": [ 1603 | { 1604 | "url": "https://symfony.com/sponsor", 1605 | "type": "custom" 1606 | }, 1607 | { 1608 | "url": "https://github.com/fabpot", 1609 | "type": "github" 1610 | }, 1611 | { 1612 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1613 | "type": "tidelift" 1614 | } 1615 | ], 1616 | "time": "2021-01-25T15:14:59+00:00" 1617 | }, 1618 | { 1619 | "name": "symfony/yaml", 1620 | "version": "v5.2.4", 1621 | "source": { 1622 | "type": "git", 1623 | "url": "https://github.com/symfony/yaml.git", 1624 | "reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277" 1625 | }, 1626 | "dist": { 1627 | "type": "zip", 1628 | "url": "https://api.github.com/repos/symfony/yaml/zipball/7d6ae0cce3c33965af681a4355f1c4de326ed277", 1629 | "reference": "7d6ae0cce3c33965af681a4355f1c4de326ed277", 1630 | "shasum": "" 1631 | }, 1632 | "require": { 1633 | "php": ">=7.2.5", 1634 | "symfony/deprecation-contracts": "^2.1", 1635 | "symfony/polyfill-ctype": "~1.8" 1636 | }, 1637 | "conflict": { 1638 | "symfony/console": "<4.4" 1639 | }, 1640 | "require-dev": { 1641 | "symfony/console": "^4.4|^5.0" 1642 | }, 1643 | "suggest": { 1644 | "symfony/console": "For validating YAML files using the lint command" 1645 | }, 1646 | "bin": [ 1647 | "Resources/bin/yaml-lint" 1648 | ], 1649 | "type": "library", 1650 | "autoload": { 1651 | "psr-4": { 1652 | "Symfony\\Component\\Yaml\\": "" 1653 | }, 1654 | "exclude-from-classmap": [ 1655 | "/Tests/" 1656 | ] 1657 | }, 1658 | "notification-url": "https://packagist.org/downloads/", 1659 | "license": [ 1660 | "MIT" 1661 | ], 1662 | "authors": [ 1663 | { 1664 | "name": "Fabien Potencier", 1665 | "email": "fabien@symfony.com" 1666 | }, 1667 | { 1668 | "name": "Symfony Community", 1669 | "homepage": "https://symfony.com/contributors" 1670 | } 1671 | ], 1672 | "description": "Loads and dumps YAML files", 1673 | "homepage": "https://symfony.com", 1674 | "funding": [ 1675 | { 1676 | "url": "https://symfony.com/sponsor", 1677 | "type": "custom" 1678 | }, 1679 | { 1680 | "url": "https://github.com/fabpot", 1681 | "type": "github" 1682 | }, 1683 | { 1684 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1685 | "type": "tidelift" 1686 | } 1687 | ], 1688 | "time": "2021-02-22T15:48:39+00:00" 1689 | } 1690 | ], 1691 | "packages-dev": [], 1692 | "aliases": [], 1693 | "minimum-stability": "stable", 1694 | "stability-flags": [], 1695 | "prefer-stable": false, 1696 | "prefer-lowest": false, 1697 | "platform": [], 1698 | "platform-dev": [], 1699 | "plugin-api-version": "1.1.0" 1700 | } 1701 | -------------------------------------------------------------------------------- /docker/freqtrade-ui/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:15.8.0-alpine as ui-builder 2 | 3 | RUN mkdir /app 4 | 5 | RUN wget --quiet https://github.com/freqtrade/frequi/archive/0.0.6.tar.gz -O /tmp/ui.tar.gz \ 6 | && tar xf /tmp/ui.tar.gz -C /app --strip 1 \ 7 | && rm /tmp/ui.tar.gz 8 | 9 | WORKDIR /app 10 | 11 | RUN yarn 12 | RUN yarn global add @vue/cli 13 | 14 | COPY . /app 15 | RUN yarn build 16 | 17 | FROM nginx:1.19.6-alpine 18 | COPY --from=ui-builder /app/dist /etc/nginx/html 19 | COPY --from=ui-builder /app/nginx.conf /etc/nginx/nginx.conf 20 | EXPOSE 80 21 | CMD ["nginx"] 22 | -------------------------------------------------------------------------------- /docker/freqtrade/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM freqtradeorg/freqtrade:stable 2 | 3 | RUN pip install --upgrade pip 4 | 5 | RUN pip install -U -r requirements-plot.txt 6 | RUN pip install technical 7 | RUN pip install ta 8 | RUN pip install finta 9 | -------------------------------------------------------------------------------- /docker/manager/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-cli-alpine 2 | 3 | RUN apk update \ 4 | && apk upgrade --available \ 5 | && apk add --virtual build-deps \ 6 | docker 7 | 8 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 9 | 10 | COPY bin /app/bin 11 | COPY src /app/src 12 | COPY resources /app/resources 13 | COPY composer.json /app/composer.json 14 | COPY composer.lock /app/composer.lock 15 | 16 | WORKDIR /app 17 | 18 | RUN composer install --prefer-dist --no-dev --no-scripts --no-progress --no-suggest; \ 19 | composer clear-cache 20 | 21 | RUN set -eux; \ 22 | composer dump-autoload --classmap-authoritative --no-dev; \ 23 | chmod +x bin/manager 24 | 25 | RUN mkdir /tmp/manager 26 | 27 | ENTRYPOINT ["/app/bin/manager"] 28 | -------------------------------------------------------------------------------- /install: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | set -e 4 | 5 | echo "▶️ Preparing..." 6 | [ -d /tmp/freqtrade-manager ] || mkdir /tmp/freqtrade-manager 7 | [ -d /tmp/freqtrade-manager/install ] || mkdir /tmp/freqtrade-manager/install 8 | [ -d /tmp/freqtrade-manager/install/trading-bot ] || mkdir /tmp/freqtrade-manager/install/trading-bot 9 | 10 | echo "▶️ Downloading some required Docker images (wait some seconds/minutes while downloading)..." 11 | docker pull --quiet ph3nol/freqtrade:latest 12 | docker pull --quiet ph3nol/freqtrade-ui:latest 13 | docker pull --quiet ph3nol/freqtrade-manager:latest 14 | 15 | echo "▶️ Downloading/Installing Trading Bot..." 16 | if [ -d "/usr/local/bin" ]; then 17 | BOT_SCRIPT_DESTINATION_PATH="/usr/local/bin/bot" 18 | elif [ -d "/usr/bin" ]; then 19 | BOT_SCRIPT_DESTINATION_PATH="/usr/bin/bot" 20 | else 21 | mkdir -p /usr/local/bin 22 | BOT_SCRIPT_DESTINATION_PATH="/usr/local/bin/bot" 23 | fi 24 | curl -sSL -o ${BOT_SCRIPT_DESTINATION_PATH} https://raw.githubusercontent.com/Ph3nol/Trading-Bot/master/bot && chmod +x ${BOT_SCRIPT_DESTINATION_PATH} 25 | 26 | echo "▶️ Downloading needed Trading Bot scripts..." 27 | GITHUB_LAST_RELEASE_ENDPOINT="https://api.github.com/repos/Ph3nol/Trading-Bot/releases/latest" 28 | GITHUB_TAR_URL=$(curl -s "$GITHUB_LAST_RELEASE_ENDPOINT" | grep tarball_url | cut -d '"' -f 4) 29 | wget -qO /tmp/freqtrade-manager/install/trading-bot.tar.gz ${GITHUB_TAR_URL} 30 | tar -zxf /tmp/freqtrade-manager/install/trading-bot.tar.gz --directory /tmp/freqtrade-manager/install/trading-bot --strip 1 31 | rm -rf /tmp/freqtrade-manager/resources && mv /tmp/freqtrade-manager/install/trading-bot/resources /tmp/freqtrade-manager/resources 32 | rm -rf /tmp/freqtrade-manager/install 33 | 34 | echo "" 35 | echo "✅ Your Trading Bot is now ready! 🚀🔥" 36 | -------------------------------------------------------------------------------- /resources/scripts/binance-scrapper.js: -------------------------------------------------------------------------------- 1 | const puppeteer = require('puppeteer'); 2 | 3 | if (!process.env.BINANCE_SCRAPPER_TYPE) { 4 | console.log([]) 5 | 6 | return 7 | } 8 | 9 | const volumePercent24hCases = { 10 | USDT: { 11 | fiatClick: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[1]/div[1]/div/button[4]', 12 | pairButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div/div[3]', 13 | variation24hFilterButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[3]/div/div[1]/div[4]/div/div', 14 | pairListColumn: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[3]/div/div[2]/div/div/div/self::div/div/div[2]/div[1]' 15 | }, 16 | EUR: { 17 | fiatClick: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[1]/div[1]/div/button[4]', 18 | pairButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div/div[9]', 19 | variation24hFilterButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[3]/div/div[1]/div[4]', 20 | pairListColumn: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[3]/div/div[2]/div/div/div/self::div/div/div[2]/div[1]' 21 | }, 22 | BTC: { 23 | fiatClick: false, 24 | pairButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[1]/div[1]/div/button[2]', 25 | variation24hFilterButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div[1]/div[4]', 26 | pairListColumn: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div[2]/div/div/div/self::div/div/div[2]/div[1]' 27 | }, 28 | BNB: { 29 | fiatClick: false, 30 | pairButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[1]/div[1]/div/button[1]', 31 | variation24hFilterButton: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div[1]/div[4]', 32 | pairListColumn: '//*[@id="__APP"]/div[1]/main/div/div[2]/div/div/div[2]/div[2]/div/div[2]/div/div/div/self::div/div/div[2]/div[1]' 33 | } 34 | }; 35 | 36 | const getScrappedVolumePercent24hPairlist = async function (page) { 37 | let volumePercent24hResults = {}; 38 | 39 | for (const currency in volumePercent24hCases) { 40 | // await page.goto('https://www.binance.com/fr/markets', { waitUntil: 'networkidle2' }) 41 | await page.goto('https://www.binance.com/fr/markets') 42 | // await page.screenshot({ path: '/screens/0_markets_home.jpg' }) 43 | 44 | const scrapPayloadXPaths = volumePercent24hCases[currency] 45 | 46 | if (scrapPayloadXPaths.fiatClick) { 47 | // console.debug('Fiats button click...') 48 | await page.waitForXPath(scrapPayloadXPaths.fiatClick) 49 | fiatsButton = await page.$x(scrapPayloadXPaths.fiatClick) 50 | await fiatsButton[0].click() 51 | // await page.screenshot({ path: '/screens/1_markets_fiat_click.jpg' }) 52 | } 53 | 54 | if (scrapPayloadXPaths.pairButton) { 55 | // console.debug('Pair filter button click...') 56 | await page.waitForXPath(scrapPayloadXPaths.pairButton) 57 | pairButton = await page.$x(scrapPayloadXPaths.pairButton) 58 | await pairButton[0].click() 59 | // await page.screenshot({ path: '/screens/2_markets_pairs.jpg' }) 60 | } 61 | 62 | // console.debug('24h variation % filter click...') 63 | await page.waitForXPath(scrapPayloadXPaths.variation24hFilterButton) 64 | variation24hButton = await page.$x(scrapPayloadXPaths.variation24hFilterButton) 65 | await variation24hButton[0].click() 66 | await variation24hButton[0].click() 67 | // await page.screenshot({ path: '/screens/3_variation_24h.jpg' }) 68 | 69 | // console.debug('Scraping pairs...') 70 | let pairsList = [] 71 | await page.waitForXPath(scrapPayloadXPaths.pairListColumn) 72 | // await page.screenshot({ path: '/screens/4_scrap.jpg' }) 73 | let pairsRows = await page.$x(scrapPayloadXPaths.pairListColumn) 74 | for (let pairIndex in pairsRows) { 75 | pairsList.push( 76 | await page.evaluate(element => element.textContent, pairsRows[pairIndex]) 77 | ) 78 | } 79 | 80 | volumePercent24hResults[currency] = pairsList 81 | } 82 | 83 | return volumePercent24hResults 84 | }; 85 | 86 | (async () => { 87 | const browser = await puppeteer.launch({ 88 | executablePath: process.env.CHROME_BIN || null, 89 | args: ['--no-sandbox', '--headless', '--disable-gpu'] 90 | }) 91 | 92 | // console.debug('Binance markets access...') 93 | const page = await browser.newPage() 94 | await page.setViewport({ width: 1366, height: 3000 }) 95 | await page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36') 96 | 97 | switch (process.env.BINANCE_SCRAPPER_TYPE) { 98 | case 'binanceVolumePercent24hPairlist': 99 | try { 100 | console.log(JSON.stringify( 101 | await getScrappedVolumePercent24hPairlist(page) 102 | )) 103 | } catch (e) { 104 | console.log([]) 105 | } 106 | break 107 | default: 108 | console.log([]) 109 | break 110 | } 111 | 112 | await browser.close() 113 | })(); 114 | -------------------------------------------------------------------------------- /resources/scripts/generate-random-available-port.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | [[ -t 1 ]] && piped=0 || piped=1 5 | 6 | while 7 | RANDOM_PORT=$(shuf -n 1 -i 49152-65535) 8 | netstat -atun | grep -q "$RANDOM_PORT" 9 | do 10 | continue 11 | done 12 | 13 | echo $RANDOM_PORT 14 | -------------------------------------------------------------------------------- /resources/scripts/ui-instance-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | JS_FILE=$(grep -nril "window.location.origin||\"http://localhost:8080\"" /etc/nginx/html/ || echo "0") 5 | if [ "0" != "${JS_FILE}" ]; then 6 | sed -i 's/window.location.origin||"http:\/\/localhost:8080"/"http:\/\/'${TRADING_BOT_API_HOST}':'${TRADING_BOT_API_PORT}'"/g' $JS_FILE 7 | fi 8 | 9 | exec "$@" 10 | -------------------------------------------------------------------------------- /screenshots/manager-status.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ph3nol/Trading-Bot/9fe19a13901e93769e1aefd891b7725eaa598468/screenshots/manager-status.jpg -------------------------------------------------------------------------------- /screenshots/manager-trade.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ph3nol/Trading-Bot/9fe19a13901e93769e1aefd891b7725eaa598468/screenshots/manager-trade.jpg -------------------------------------------------------------------------------- /src/Manager/App/Behaviour/AbstractBehaviour.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | abstract class AbstractBehaviour implements BehaviourInterface 13 | { 14 | public array $data = []; 15 | 16 | /** 17 | * TTLs are minutes. 18 | * 19 | * `false` to disable update. 20 | * `true` to always update (each cron exec). 21 | */ 22 | public $cronTtl = false; 23 | public $instanceTtl = false; 24 | 25 | public function __construct() 26 | { 27 | $this->data = ManagerFilesystem::getBehaviourData($this); 28 | } 29 | 30 | public function updateCron(): void 31 | { 32 | $this->data['cron_last_update'] = (new \DateTimeImmutable())->format('c'); 33 | } 34 | 35 | 36 | public function updateInstance(Instance $instance): Instance 37 | { 38 | return $instance; 39 | } 40 | 41 | public function updateInstanceFromCron(Instance $instance): Instance 42 | { 43 | $this->data['instances_last_updates'][$instance->slug] = (new \DateTimeImmutable())->format('c'); 44 | 45 | return $this->updateInstance($instance); 46 | } 47 | 48 | public function resetInstance(Instance $instance): Instance 49 | { 50 | unset($this->data['instances_last_updates'][$instance->slug]); 51 | 52 | return $instance; 53 | } 54 | 55 | public function write(): void 56 | { 57 | ManagerFilesystem::writeBehaviourData($this); 58 | } 59 | 60 | public function needsCronUpdate(): bool 61 | { 62 | return $this->needsUpdate($this->data['cron_last_update'] ?? null, $this->cronTtl); 63 | } 64 | 65 | public function needsInstanceUpdate(Instance $instance): bool 66 | { 67 | return $this->needsUpdate($this->data['instances_last_updates'][$instance->slug] ?? null, $this->instanceTtl); 68 | } 69 | 70 | private function needsUpdate(string $lastUpdate = null, $ttl): bool 71 | { 72 | if (is_bool($ttl)) { 73 | return $ttl; 74 | } 75 | 76 | if (null === $lastUpdate) { 77 | return true; 78 | } 79 | 80 | $lastUpdate = new \DateTimeImmutable($lastUpdate); 81 | $limitTtl = (new \DateTime)->sub(new \DateInterval(sprintf('PT%dM', $ttl))); 82 | 83 | return $lastUpdate < $limitTtl; 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Manager/App/Behaviour/BinanceVolumePercent24hPairlistBehaviour.php.disabled: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class BinanceVolumePercent24hPairlistBehaviour extends AbstractBehaviour 13 | { 14 | public $cronTtl = 10; 15 | public $instanceTtl = 30; 16 | 17 | public function getSlug(): string 18 | { 19 | return 'binanceVolumePercent24hPairlist'; 20 | } 21 | 22 | public function updateCron(): void 23 | { 24 | parent::updateCron(); 25 | 26 | $this->data = array_merge($this->data, [ 27 | 'pairLists' => $this->scrapDataFromBinance(), 28 | ]); 29 | } 30 | 31 | public function updateInstance(Instance $instance): Instance 32 | { 33 | parent::updateInstance($instance); 34 | 35 | $instanceBehaviourConfig = $instance->getBehaviourConfig($this); 36 | $pairsCount = $instanceBehaviourConfig['pairsCount'] ?? 40; 37 | 38 | $pairList = $this->data['pairLists'][$instance->config['stake_currency']] ?? []; 39 | if ($pairList) { 40 | $instance->config['exchange']['pair_whitelist'] = array_slice(array_unique($pairList), 0, $pairsCount + 1); 41 | 42 | foreach ($instance->config['pairlists'] ?? [] as $k => $pairlistEntry) { 43 | if (in_array($pairlistEntry['method'], ['StaticPairList', 'VolumePairList'])) { 44 | $instance->config['pairlists'][$k] = [ 45 | 'method' => 'StaticPairList', 46 | ]; 47 | } 48 | } 49 | } 50 | 51 | return $instance; 52 | } 53 | 54 | private function scrapDataFromBinance(): array 55 | { 56 | $processCommand = [ 57 | sprintf('docker run --rm --name trading-bot-behaviour-%s-binance-scrapper', $this->getSlug()), 58 | sprintf('-e BINANCE_SCRAPPER_TYPE=%s', $this->getSlug()), 59 | sprintf('-v %s:/app/index.js', '/tmp/freqtrade-manager/resources/scripts/binance-scrapper.js'), 60 | 'alekzonder/puppeteer:latest', 61 | ]; 62 | 63 | $process = Process::processCommandLine(implode(' ', $processCommand), false); 64 | if (null === $process) { 65 | return []; 66 | } 67 | 68 | $pairLists = json_decode($process, true) ?? []; 69 | $pairLists = array_map(function (array $pairList): array { 70 | return array_slice(array_unique($pairList ?: []), 0, 100); 71 | }, $pairLists); 72 | 73 | return $pairLists; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Manager/App/Behaviour/TradingViewScanBehaviour.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class TradingViewScanBehaviour extends AbstractBehaviour 13 | { 14 | public $cronTtl = 1; 15 | public $instanceTtl = 15; 16 | 17 | private static $allowedPairs = ['USDT', 'BTC', 'ETH', 'USD', 'EUR', 'BNB', 'USDC', 'BUSD']; 18 | 19 | public function getSlug(): string 20 | { 21 | return 'tradingViewScan'; 22 | } 23 | 24 | public function updateCron(): void 25 | { 26 | parent::updateCron(); 27 | 28 | $pairLists = []; 29 | $searchTypes = $this->getSortTypesPayloads(); 30 | foreach ($searchTypes as $type => $requestPayload) { 31 | $requestPayload = json_encode(json_decode(trim($requestPayload), true)); 32 | $pairLists[$type] = $this->scrapPairlistsFromTW($requestPayload); 33 | } 34 | 35 | $this->data = array_merge($this->data, [ 36 | 'pairLists' => $pairLists, 37 | ]); 38 | } 39 | 40 | public function updateInstance(Instance $instance): Instance 41 | { 42 | parent::updateInstance($instance); 43 | 44 | $instanceBehaviourConfig = $instance->getBehaviourConfig($this); 45 | $searchType = $instanceBehaviourConfig['searchType'] ?? '5mChangePercent'; 46 | $pairsCount = $instanceBehaviourConfig['pairsCount'] ?? 40; 47 | 48 | $exchangeKey = strtoupper($instance->config['exchange']['name']); 49 | $pairList = $this->data['pairLists'][$searchType][$exchangeKey][$instance->config['stake_currency']] ?? []; 50 | if ($pairList) { 51 | $instance->updateStaticPairList( 52 | array_slice(array_unique($pairList), 0, $pairsCount + 1) 53 | ); 54 | } 55 | 56 | return $instance; 57 | } 58 | 59 | public function scrapPairlistsFromTW(string $requestPayload): array 60 | { 61 | /** 62 | * https://fr.tradingview.com/crypto-screener/ 63 | */ 64 | $client = new \GuzzleHttp\Client( 65 | [ 66 | 'base_uri' => 'https://scanner.tradingview.com', 67 | ] 68 | ); 69 | $response = $client->request('POST', '/crypto/scan', [ 70 | 'body' => $requestPayload, 71 | 'headers' => [ 72 | 'authority' => 'scanner.tradingview.com', 73 | 'origin' => 'https://fr.tradingview.com', 74 | 'content-type' => 'application/x-www-form-urlencoded; charset=UTF-8', 75 | 'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36', 76 | ], 77 | ]); 78 | 79 | $pairList = []; 80 | $scanData = json_decode((string) $response->getBody(), true); 81 | foreach ($scanData['data'] as $data) { 82 | if (false !== strpos($data['d'][0], '_PREMIUM')) { 83 | continue; 84 | } 85 | 86 | $pair = $data['d'][0]; 87 | $exchange = $data['d'][1]; 88 | foreach (self::$allowedPairs as $allowedPair) { 89 | if ($allowedPair === substr($pair, -strlen($allowedPair))) { 90 | $pairList[$exchange][$allowedPair][] = str_replace($allowedPair, '/'.$allowedPair, $pair); 91 | continue 2; 92 | } 93 | } 94 | } 95 | 96 | return $pairList; 97 | } 98 | 99 | private function getSortTypesPayloads(): array 100 | { 101 | return [ 102 | '1mChangePercent' => << <<instance = $instance; 18 | } 19 | 20 | public function getInstance(): Instance 21 | { 22 | return $this->instance; 23 | } 24 | 25 | public static function init(Instance $instance) 26 | { 27 | $strategyFilePath = sprintf('%s/strategies/%s.py', MANAGER_DIRECTORY, $instance->strategy); 28 | if (false === file_exists($strategyFilePath)) { 29 | throw new StrategyNotFoundException($instance->strategy); 30 | } 31 | 32 | $data = InstanceFilesystem::initInstance($instance); 33 | $instance->mergeParameters($data['parameters']); 34 | 35 | $handler = new static($instance); 36 | $handler->updateConfigApiServiceCors(); 37 | 38 | return $handler; 39 | } 40 | 41 | public function updateConfigApiServiceCors(): self 42 | { 43 | $managerConfig = MANAGER_CONFIGURATION; 44 | 45 | $corsEntries = []; 46 | $managerConfig['cors_domains'][] = $managerConfig['hosts']['ui']; 47 | foreach ($managerConfig['cors_domains'] as $corsDomain) { 48 | $corsEntries[] = sprintf( 49 | 'http://%s:%d', 50 | $corsDomain, 51 | $this->instance->parameters['ports']['ui'] 52 | ); 53 | } 54 | 55 | $this->instance->config['api_server']['CORS_origins'] = array_unique($corsEntries); 56 | InstanceFilesystem::writeInstanceConfig($this->instance); 57 | 58 | return $this; 59 | } 60 | 61 | public function trade(bool $withUI = true): array 62 | { 63 | $dockerIds = [ 64 | 'core' => InstanceProcess::runInstanceTrading($this->instance), 65 | ]; 66 | $this->instance->declareAsRunning(); 67 | 68 | if (true === $withUI && false === $this->instance->isUIRunning()) { 69 | $dockerIds['ui'] = InstanceUIProcess::run($this->instance); 70 | $this->instance->declareUIAsRunning(); 71 | } 72 | 73 | if (false === $withUI && true === $this->instance->isUIRunning()) { 74 | InstanceUIProcess::stop($this->instance); 75 | $this->instance->declareUIAsStopped(); 76 | } 77 | 78 | return $dockerIds; 79 | } 80 | 81 | public function stop(bool $withUI = true): void 82 | { 83 | InstanceProcess::stopInstance($this->instance); 84 | $this->instance->declareAsStopped(); 85 | 86 | if ($withUI && true === $this->instance->isUIRunning()) { 87 | InstanceUIProcess::stop($this->instance); 88 | $this->instance->declareUIAsStopped(); 89 | } 90 | } 91 | 92 | public function restart(bool $withUI = true): array 93 | { 94 | $dockerIds = [ 95 | 'core' => InstanceProcess::restartInstance($this->instance), 96 | ]; 97 | $this->instance->declareAsRunning(); 98 | 99 | if (true === $withUI && true === $this->instance->isUIRunning()) { 100 | $dockerIds['ui'] = InstanceUIProcess::restart($this->instance); 101 | $this->instance->declareUIAsRunning(); 102 | } 103 | 104 | return $dockerIds; 105 | } 106 | 107 | public function reset(): void 108 | { 109 | InstanceFilesystem::resetInstanceData($this->instance); 110 | } 111 | 112 | public function backtestDownloadData(int $daysCount = 5): void 113 | { 114 | $timeframes = ['5m']; 115 | $timeframes[] = $this->instance->config['timeframe'] ?? null; 116 | $timeframes = array_merge($timeframes, $this->extractTimeframesFromInstanceStrategy()); 117 | $timeframes = array_unique(array_filter($timeframes)); 118 | 119 | InstanceProcess::backtestDownloadDataForInstance($this->instance, $daysCount, $timeframes); 120 | } 121 | 122 | public function removeBacktestData(): void 123 | { 124 | InstanceFilesystem::removeInstanceBacktestData($this->instance); 125 | } 126 | 127 | public function backtest(float $fee = 0.001): string 128 | { 129 | return InstanceProcess::backtestInstance($this->instance, $fee); 130 | } 131 | 132 | public function removePlottingData(): void 133 | { 134 | InstanceFilesystem::removeInstancePlottingData($this->instance); 135 | } 136 | 137 | public function plot(array $pairs = []): void 138 | { 139 | InstanceProcess::plotInstance($this->instance, $pairs); 140 | } 141 | 142 | public function getPairsList(): array 143 | { 144 | $pairsListOutput = InstanceProcess::getPairsList($this->instance); 145 | if (null === $pairsListOutput) { 146 | return []; 147 | } 148 | 149 | $pairsListOutput = explode("\n", $pairsListOutput); 150 | array_shift($pairsListOutput); 151 | $pairListOutput = str_replace('\'', '"', $pairsListOutput[0]); 152 | $pairsList = json_decode($pairListOutput, true); 153 | 154 | return $pairsList; 155 | } 156 | 157 | private function extractTimeframesFromInstanceStrategy(): array 158 | { 159 | $strategyContent = InstanceFilesystem::getInstanceStrategyFileContent($this->instance); 160 | $timeframesRegex = '/(timeframe|backtest_timeframe|informative_timeframe)?[ ]=?[ ][\'|"](1m|3m|5m|15m|30m|1h|2h|4h|6h|8h|12h|1d|3d|1w|2w|1M|1y)[\'|"]/im'; 161 | preg_match_all($timeframesRegex, $strategyContent, $matches); 162 | 163 | return array_filter(array_unique($matches[2] ?? [])); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/Manager/App/Manager.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class Manager 18 | { 19 | const COMMON_INSTANCE_KEY = '_all'; 20 | 21 | private $instances = []; 22 | private $parameters = []; 23 | private $behaviours = []; 24 | private $dockerStatus = []; 25 | 26 | public function __construct(array $managerData) 27 | { 28 | ManagerFilesystem::init(); 29 | $this->parameters = $managerData['parameters']; 30 | define('MANAGER_CONFIGURATION', $this->parameters); 31 | $this->initDockerStatus(); 32 | $this->initBehaviours(); 33 | 34 | $this->populateInstances($managerData['instances'] ?? []); 35 | } 36 | 37 | public static function fromFile(string $filePath): self 38 | { 39 | $managerData = Yaml::parseFile($filePath); 40 | 41 | return new static($managerData); 42 | } 43 | 44 | public function setInstances(array $instances): self 45 | { 46 | $this->instances = $instances; 47 | 48 | return $this; 49 | } 50 | 51 | public function getInstances(): array 52 | { 53 | return $this->instances; 54 | } 55 | 56 | public function getParameters(): array 57 | { 58 | return $this->parameters; 59 | } 60 | 61 | public function getBehaviours(): array 62 | { 63 | return $this->behaviours; 64 | } 65 | 66 | public function findRequiredInstanceFromSlug(string $slug): Instance 67 | { 68 | foreach ($this->getInstances() as $instance) { 69 | if ($slug === $instance->slug) { 70 | return $instance; 71 | } 72 | } 73 | 74 | throw new InstanceNotFoundException($slug); 75 | } 76 | 77 | private function populateInstances(array $instancesPayloads): self 78 | { 79 | $commonPayload = []; 80 | if (array_key_exists(self::COMMON_INSTANCE_KEY, $instancesPayloads)) { 81 | $commonPayload = $instancesPayloads[self::COMMON_INSTANCE_KEY]; 82 | unset($instancesPayloads[self::COMMON_INSTANCE_KEY]); 83 | } 84 | 85 | foreach ($instancesPayloads as $instanceSlug => $instancePayload) { 86 | $instanceConfig = array_replace_recursive( 87 | $this->getBaseConfigurationDataFromInstancePayload($instancePayload), 88 | $commonPayload['config'] ?? [], 89 | $instancePayload['config'] ?? [] 90 | ); 91 | $instanceParameters = array_replace_recursive( 92 | $this->getDefaultParametersFromInstance(), 93 | $commonPayload['parameters'] ?? [], 94 | $instancePayload['parameters'] ?? [] 95 | ); 96 | 97 | $instanceBehaviours = $instancePayload['behaviours'] ?? []; 98 | $instanceBehaviours = array_map(function ($behaviourData): array { 99 | return $behaviourData ?? []; 100 | }, $instanceBehaviours); 101 | 102 | $instance = Instance::create( 103 | $instanceSlug, 104 | $instancePayload['strategy'], 105 | $instanceConfig, 106 | $instanceBehaviours, 107 | $instanceParameters 108 | ); 109 | 110 | $instance->config['bot_name'] = sprintf('TB.%s', (string) $instance); 111 | 112 | $this->applyDockerStatusToInstance($instance); 113 | $this->applyBehavioursToInstance($instance); 114 | 115 | InstanceFilesystem::writeInstanceConfig($instance); 116 | 117 | $this->instances[$instance->slug] = $instance; 118 | } 119 | 120 | return $this; 121 | } 122 | 123 | private function getBaseConfigurationDataFromInstancePayload(array $instancePayload): array 124 | { 125 | $configFilePath = MANAGER_CONFIGS_DIRECTORY . '/' . $instancePayload['config_file']; 126 | if (false === file_exists($configFilePath)) { 127 | throw new InstanceNotFoundConfigFileException($instancePayload['config_file']); 128 | } 129 | 130 | $configContent = file_get_contents($configFilePath); 131 | 132 | return json_decode($configContent, true); 133 | } 134 | 135 | private function getDefaultParametersFromInstance(): array 136 | { 137 | return [ 138 | 'tradingHours' => [], 139 | ]; 140 | } 141 | 142 | private function initBehaviours(): self 143 | { 144 | $finder = new Finder(); 145 | $finder->files() 146 | ->in(MANAGER_PROJECT_DIRECTORY . '/src/Manager/App/Behaviour') 147 | ->name('*.php') 148 | ->notName('AbstractBehaviour.php'); 149 | 150 | if (false === $finder->hasResults()) { 151 | return $this; 152 | } 153 | 154 | foreach ($finder as $file) { 155 | $behaviourFqcn = sprintf( 156 | 'Manager\\App\\Behaviour\\%s', 157 | pathinfo($file->getRelativePathname(), PATHINFO_FILENAME) 158 | ); 159 | 160 | $behaviour = new $behaviourFqcn; 161 | $this->behaviours[$behaviour->getSlug()] = $behaviour; 162 | } 163 | 164 | return $this; 165 | } 166 | 167 | private function applyBehavioursToInstance(Instance $instance): self 168 | { 169 | foreach ($instance->behaviours as $behaviourSlug => $behaviourConfig) { 170 | if (false === array_key_exists($behaviourSlug, $this->behaviours)) { 171 | continue; 172 | } 173 | 174 | $behaviour = $this->behaviours[$behaviourSlug]; 175 | $behaviour->updateInstance($instance); 176 | $behaviour->write(); 177 | } 178 | 179 | return $this; 180 | } 181 | 182 | private function initDockerStatus(): self 183 | { 184 | $this->dockerStatus = []; 185 | $dockerStatus = ManagerProcess::getDockerStatus(); 186 | 187 | if ((bool) $dockerStatus) { 188 | $dockerStatusEntries = explode("\n", $dockerStatus); 189 | $dockerStatusEntries = array_map(function (string $statusEntry): array { 190 | $data = explode(';;;', $statusEntry); 191 | $isRunning = 0 === strpos($data[3], 'Up'); 192 | $uptime = $isRunning ? $data[3] : null; 193 | if ($uptime) { 194 | $uptime = strtolower($uptime); 195 | $uptime = str_replace( 196 | ['up', 'less than', 'about', 'an hour', 'a minute', 'a day', 'hours', ' minutes', ' seconds', ' days', ' hour', ' minute', ' second', ' day'], 197 | ['', '-', '~', '1h', '1m', '1d', 'h', 'm', 's', 'd','h', 'm', 's', 'd'], 198 | $uptime 199 | ); 200 | $uptime = trim($uptime); 201 | } 202 | 203 | return [ 204 | 'id' => $data[0], 205 | 'name' => $data[1], 206 | 'image' => $data[2], 207 | 'is_running' => $isRunning, 208 | 'uptime' => $uptime, 209 | ]; 210 | }, $dockerStatusEntries); 211 | 212 | foreach ($dockerStatusEntries as $statusEntry) { 213 | $this->dockerStatus[$statusEntry['name']] = $statusEntry; 214 | } 215 | } 216 | 217 | return $this; 218 | } 219 | 220 | private function applyDockerStatusToInstance(Instance $instance): self 221 | { 222 | if (true === $this->dockerStatus[$instance->getDockerCoreInstanceName()]['is_running'] ?? false) { 223 | $instance->declareAsRunning( 224 | $this->dockerStatus[$instance->getDockerCoreInstanceName()]['uptime'] 225 | ); 226 | } 227 | 228 | if (true === $this->dockerStatus[$instance->getDockerUIInstanceName()]['is_running'] ?? false) { 229 | $instance->declareUIAsRunning( 230 | $this->dockerStatus[$instance->getDockerUIInstanceName()]['uptime'] 231 | ); 232 | } 233 | 234 | return $this; 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/Manager/Domain/BehaviourInterface.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | interface BehaviourInterface 11 | { 12 | public function getSlug(): string; 13 | 14 | public function updateCron(): void; 15 | 16 | public function updateInstance(Instance $instance): Instance; 17 | 18 | public function resetInstance(Instance $instance): Instance; 19 | } 20 | -------------------------------------------------------------------------------- /src/Manager/Domain/Exception/InstanceNotFoundConfigFileException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class InstanceNotFoundConfigFileException extends \Exception 9 | { 10 | public function __construct(string $fileName) 11 | { 12 | parent::__construct(sprintf('Instance configuration file `%s` is not found', $fileName)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Manager/Domain/Exception/InstanceNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class InstanceNotFoundException extends \Exception 9 | { 10 | public function __construct(string $slug) 11 | { 12 | parent::__construct(sprintf('Instance `%s` is not found', $slug)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Manager/Domain/Exception/StrategyNotFoundException.php: -------------------------------------------------------------------------------- 1 | 7 | */ 8 | class StrategyNotFoundException extends \Exception 9 | { 10 | public function __construct(string $strategyName) 11 | { 12 | parent::__construct(sprintf('Strategy `%s` is not found into strategies folder', $strategyName)); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Manager/Domain/Instance.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class Instance 13 | { 14 | const STATUS_STOPPED = 'stopped'; 15 | const STATUS_RUNNING = 'running'; 16 | 17 | public UuidInterface $uuid; 18 | public string $status = self::STATUS_STOPPED; 19 | public string $uiStatus = self::STATUS_STOPPED; 20 | public ?string $uptime; 21 | public ?string $uiUptime; 22 | public string $slug; 23 | public string $label; 24 | public string $strategy; 25 | public array $config; 26 | public array $behaviours; 27 | public array $parameters = []; 28 | 29 | public array $directories = []; 30 | public array $files = []; 31 | 32 | public function __construct( 33 | UuidInterface $uuid, 34 | string $slug, 35 | string $label, 36 | string $strategy, 37 | array $config, 38 | array $behaviours = [], 39 | array $parameters = [] 40 | ) { 41 | $this->uuid = $uuid; 42 | $this->slug = $slug; 43 | $this->label = $label; 44 | $this->strategy = $strategy; 45 | $this->config = $config; 46 | $this->behaviours = $behaviours; 47 | $this->parameters = $parameters; 48 | 49 | $this->initDirectoriesAndFiles(); 50 | } 51 | 52 | public static function create( 53 | string $slug = null, 54 | string $strategy, 55 | array $config, 56 | array $behaviours = [], 57 | array $parameters = [] 58 | ): self { 59 | $uuid = Uuid::uuid4(); 60 | $label = $slug ? strtoupper($slug) : (string) $uuid; 61 | 62 | return new static($uuid, $slug, $label, $strategy, $config, $behaviours, $parameters); 63 | } 64 | 65 | public function __toString(): string 66 | { 67 | return $this->label; 68 | } 69 | 70 | public function isProduction(): bool 71 | { 72 | return ! (($this->config['dry_run'] ?? false) === true); 73 | } 74 | 75 | public function isApiEnabled(): bool 76 | { 77 | return ($this->config['api_server']['enabled'] ?? false) === true; 78 | } 79 | 80 | public function isEdgeEnabled(): bool 81 | { 82 | return ($this->config['edge']['enabled'] ?? false) === true; 83 | } 84 | 85 | public function isTelegramEnabled(): bool 86 | { 87 | return ($this->config['telegram']['enabled'] ?? false) === true; 88 | } 89 | 90 | public function isForceBuyEnabled(): bool 91 | { 92 | return ($this->config['forcebuy_enable'] ?? false) === true; 93 | } 94 | 95 | public function declareAsRunning(string $uptime = null): self 96 | { 97 | $this->status = self::STATUS_RUNNING; 98 | $this->uptime = $uptime; 99 | 100 | return $this; 101 | } 102 | 103 | public function declareAsStopped(): self 104 | { 105 | $this->status = self::STATUS_STOPPED; 106 | $this->uptime = null; 107 | 108 | return $this; 109 | } 110 | 111 | public function getStatus(): string 112 | { 113 | return $this->status; 114 | } 115 | 116 | public function isRunning(): bool 117 | { 118 | return self::STATUS_RUNNING === $this->getStatus(); 119 | } 120 | 121 | public function declareUIAsRunning(string $uptime = null): self 122 | { 123 | $this->uiStatus = self::STATUS_RUNNING; 124 | $this->uiUptime = $uptime; 125 | 126 | return $this; 127 | } 128 | 129 | public function declareUIAsStopped(): self 130 | { 131 | $this->uiStatus = self::STATUS_STOPPED; 132 | $this->uiUptime = null; 133 | 134 | return $this; 135 | } 136 | 137 | public function getUIStatus(): string 138 | { 139 | return $this->uiStatus; 140 | } 141 | 142 | public function isUIRunning(): bool 143 | { 144 | return self::STATUS_RUNNING === $this->getUIStatus(); 145 | } 146 | 147 | public function mergeParameters(array $parameters): self 148 | { 149 | $this->parameters = array_replace_recursive($this->parameters, $parameters); 150 | 151 | return $this; 152 | } 153 | 154 | public function getDockerCoreInstanceName(): string 155 | { 156 | return sprintf('trading-bot-%s-core', $this->slug); 157 | } 158 | 159 | public function getDockerUIInstanceName(): string 160 | { 161 | return sprintf('trading-bot-%s-ui', $this->slug); 162 | } 163 | 164 | public function hasBehaviour(BehaviourInterface $behaviour): bool 165 | { 166 | return array_key_exists($behaviour->getSlug(), $this->behaviours); 167 | } 168 | 169 | public function getBehaviourConfig(BehaviourInterface $behaviour): array 170 | { 171 | return $this->hasBehaviour($behaviour) ? $this->behaviours[$behaviour->getSlug()] : []; 172 | } 173 | 174 | public function isOutOfTradingHours(): bool 175 | { 176 | if (!$tradingHours = $this->parameters['tradingHours']) { 177 | return false; 178 | } 179 | 180 | $outOfHours = true; 181 | $nowDateTime = new \DateTime; 182 | foreach ($tradingHours as $tradingHour) { 183 | list($from, $to) = explode('-', $tradingHour); 184 | $fromString = sprintf('%s %s:00', (new \DateTime)->format('Y-m-d'), $from); 185 | $toString = sprintf('%s %s:00', (new \DateTime)->format('Y-m-d'), $to); 186 | $fromDateTime = new \DateTime($fromString); 187 | $toDateTime = new \DateTime($toString); 188 | 189 | if ($nowDateTime >= $fromDateTime && $nowDateTime <= $toDateTime) { 190 | $outOfHours = false; 191 | break; 192 | } 193 | } 194 | 195 | return $outOfHours; 196 | } 197 | 198 | public function updateStaticPairList(array $pairList): self 199 | { 200 | $this->config['exchange']['pair_whitelist'] = $pairList; 201 | 202 | foreach ($this->config['pairlists'] ?? [] as $k => $pairlistEntry) { 203 | if (in_array($pairlistEntry['method'], ['StaticPairList', 'VolumePairList'])) { 204 | $this->config['pairlists'][$k] = [ 205 | 'method' => 'StaticPairList', 206 | ]; 207 | } 208 | } 209 | 210 | $this->config['pairlists'] = array_values($this->config['pairlists']); 211 | 212 | return $this; 213 | } 214 | 215 | public function removeUnbacktestablePairlistsFilters(): self 216 | { 217 | foreach ($this->config['pairlists'] ?? [] as $k => $pairlistEntry) { 218 | if (in_array($pairlistEntry['method'], ['PerformanceFilter'])) { 219 | unset($this->config['pairlists'][$k]); 220 | } 221 | } 222 | 223 | $this->config['pairlists'] = array_values($this->config['pairlists']); 224 | 225 | return $this; 226 | } 227 | 228 | private function initDirectoriesAndFiles(): void 229 | { 230 | $baseDirectory = MANAGER_INSTANCES_DIRECTORY . '/' . $this->slug; 231 | $hostBaseDirectory = HOST_MANAGER_INSTANCES_DIRECTORY . '/' . $this->slug; 232 | 233 | $this->directories = [ 234 | 'container' => [ 235 | '_base' => $baseDirectory, 236 | 'db' => $baseDirectory . '/db', 237 | 'data' => $baseDirectory . '/data', 238 | ], 239 | 'host' => [ 240 | '_base' => $hostBaseDirectory, 241 | 'db' => $hostBaseDirectory . '/db', 242 | 'data' => $hostBaseDirectory . '/data', 243 | ], 244 | ]; 245 | 246 | $this->files = [ 247 | 'container' => [ 248 | 'parameters' => $this->directories['container']['_base'] . '/parameters.json', 249 | 'config' => $this->directories['container']['_base'] . '/config.ready.json', 250 | 'config_backtest' => $this->directories['container']['_base'] . '/config.backtest.json', 251 | 'logs' => $this->directories['container']['_base'] . '/instance.log', 252 | 'db_dry_run' => $this->directories['container']['db'] . '/tradesv3.dryrun.sqlite', 253 | 'db_production' => $this->directories['container']['db'] . '/tradesv3.sqlite', 254 | ], 255 | 'host' => [ 256 | 'parameters' => $this->directories['host']['_base'] . '/parameters.json', 257 | 'config' => $this->directories['host']['_base'] . '/config.ready.json', 258 | 'config_backtest' => $this->directories['host']['_base'] . '/config.backtest.json', 259 | 'logs' => $this->directories['host']['_base'] . '/instance.log', 260 | 'db_dry_run' => $this->directories['host']['db'] . '/tradesv3.dryrun.sqlite', 261 | 'db_production' => $this->directories['host']['db'] . '/tradesv3.sqlite', 262 | ], 263 | ]; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/Manager/Infra/Filesystem/InstanceFilesystem.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class InstanceFilesystem 14 | { 15 | public static function initInstance(Instance $instance): array 16 | { 17 | $filesystemData = []; 18 | $filesystem = new Filesystem(); 19 | 20 | if (false === $filesystem->exists($instance->files['container']['parameters'])) { 21 | $filesystem->touch($instance->files['container']['parameters']); 22 | } 23 | $parametersContent = file_get_contents($instance->files['container']['parameters']); 24 | $parameters = json_decode($parametersContent, true) ?? []; 25 | $parameters = array_replace_recursive(self::getDefaultParameters(), $parameters); 26 | if (null === $parameters['ports']['api']) { 27 | $parameters['ports']['api'] = InstanceProcess::generateHostRandomAvailablePort(); 28 | } 29 | if (null === $parameters['ports']['ui']) { 30 | $parameters['ports']['ui'] = InstanceProcess::generateHostRandomAvailablePort(); 31 | } 32 | $parametersContent = json_encode($parameters, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 33 | $filesystem->dumpFile($instance->files['container']['parameters'], $parametersContent); 34 | $filesystemData['parameters'] = $parameters; 35 | 36 | $filesystem->touch($instance->files['container']['config']); 37 | $filesystem->touch($instance->files['container']['config_backtest']); 38 | 39 | try { 40 | $filesystem->mkdir($instance->directories['container']['_base']); 41 | } catch (IOExceptionInterface $exception) { 42 | // Already exists. 43 | } 44 | self::writeInstanceConfig($instance); 45 | $filesystem->touch($instance->files['container']['logs']); 46 | 47 | try { 48 | $filesystem->mkdir($instance->directories['container']['db']); 49 | } catch (IOExceptionInterface $exception) { 50 | // Already exists. 51 | } 52 | $filesystem->touch($instance->files['container']['db_dry_run']); 53 | $filesystem->touch($instance->files['container']['db_production']); 54 | 55 | try { 56 | $filesystem->mkdir($instance->directories['container']['data']); 57 | } catch (IOExceptionInterface $exception) { 58 | // Already exists. 59 | } 60 | 61 | // Temp: in order to by-pass new Freqtrade 2021.4 release Docker user (`ftuser`). 62 | $filesystem->chmod($instance->directories['container']['_base'], 0777, 0000, true); 63 | 64 | return $filesystemData; 65 | } 66 | 67 | public static function writeInstanceConfig(Instance $instance): void 68 | { 69 | $filesystem = new Filesystem(); 70 | $configContent = json_encode($instance->config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 71 | $filesystem->dumpFile($instance->files['container']['config'], $configContent); 72 | } 73 | 74 | public static function writeInstanceConfigBacktest(Instance $instance): void 75 | { 76 | $filesystem = new Filesystem(); 77 | $configContent = json_encode($instance->config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 78 | $filesystem->dumpFile($instance->files['container']['config_backtest'], $configContent); 79 | } 80 | 81 | public static function resetInstanceData(Instance $instance): void 82 | { 83 | $filesystem = new Filesystem(); 84 | $filesystem->remove($instance->directories['container']['_base']); 85 | } 86 | 87 | public static function removeInstanceBacktestData(Instance $instance): void 88 | { 89 | $filesystem = new Filesystem(); 90 | $filesystem->remove(sprintf('%s/backtest_results', $instance->directories['container']['data'])); 91 | } 92 | 93 | public static function removeInstancePlottingData(Instance $instance): void 94 | { 95 | $filesystem = new Filesystem(); 96 | $filesystem->remove(sprintf('%s/plot', $instance->directories['container']['data'])); 97 | } 98 | 99 | public static function getInstanceStrategyFileContent(Instance $instance): string 100 | { 101 | return file_get_contents( 102 | sprintf('%s/%s.py', MANAGER_STRATEGIES_DIRECTORY, $instance->strategy) 103 | ); 104 | } 105 | 106 | public static function getDefaultParameters(): array 107 | { 108 | return [ 109 | 'ports' => [ 110 | 'api' => null, 111 | 'ui' => null, 112 | ], 113 | ]; 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Manager/Infra/Filesystem/ManagerFilesystem.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ManagerFilesystem 14 | { 15 | public static function init() 16 | { 17 | $filesystem = new Filesystem(); 18 | 19 | try { 20 | $filesystem->mkdir(MANAGER_DATA_DIRECTORY); 21 | } catch (IOExceptionInterface $exception) { 22 | // Already exists. 23 | } 24 | 25 | try { 26 | $filesystem->mkdir(MANAGER_DATA_DIRECTORY . '/behaviours'); 27 | } catch (IOExceptionInterface $exception) { 28 | // Already exists. 29 | } 30 | } 31 | 32 | public static function writeBehaviourData(BehaviourInterface $behaviour): void 33 | { 34 | $filesystem = new Filesystem(); 35 | 36 | $dataContent = json_encode($behaviour->data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); 37 | $filesystem->dumpFile(self::getBehaviourDataFilePath($behaviour), $dataContent); 38 | } 39 | 40 | public static function getBehaviourData(BehaviourInterface $behaviour): array 41 | { 42 | $filesystem = new Filesystem(); 43 | 44 | $filePath = self::getBehaviourDataFilePath($behaviour); 45 | if (false === file_exists($filePath)) { 46 | return []; 47 | } 48 | 49 | $dataContent = file_get_contents($filePath); 50 | 51 | return json_decode($dataContent, true); 52 | } 53 | 54 | private static function getBehaviourDataFilePath(BehaviourInterface $behaviour): string 55 | { 56 | return sprintf('%s/behaviours/%s.json', MANAGER_DATA_DIRECTORY, $behaviour->getSlug()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Manager/Infra/Process/InstanceProcess.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InstanceProcess 11 | { 12 | public static function generateHostRandomAvailablePort(): int 13 | { 14 | return (int) Process::processCommandLine( 15 | sprintf('sh %s/resources/scripts/generate-random-available-port.sh', MANAGER_PROJECT_DIRECTORY) 16 | ); 17 | } 18 | 19 | public static function runInstanceTrading(Instance $instance) 20 | { 21 | $processCommand = [ 22 | sprintf('docker run --name %s --detach --restart=always', $instance->getDockerCoreInstanceName()), 23 | '--volume /etc/localtime:/etc/localtime:ro', 24 | sprintf('--volume %s:/freqtrade/config.json:ro', $instance->files['host']['config']), 25 | sprintf('--volume %s/strategies/%s.py:/freqtrade/strategy.py:ro', HOST_MANAGER_DIRECTORY, $instance->strategy), 26 | sprintf('--volume %s:/freqtrade/freqtrade.log:rw', $instance->files['host']['logs']), 27 | sprintf('--volume %s:/freqtrade/user_data:rw', $instance->directories['host']['data']), 28 | sprintf('--volume %s:/freqtrade/tradesv3.dryrun.sqlite:rw', $instance->files['host']['db_dry_run']), 29 | sprintf('--volume %s:/freqtrade/tradesv3.sqlite:rw', $instance->files['host']['db_production']), 30 | sprintf('--publish %d:8080/tcp', $instance->parameters['ports']['api']), 31 | 'ph3nol/freqtrade:latest', 32 | 'trade --config /freqtrade/config.json', 33 | '--logfile /freqtrade/freqtrade.log', 34 | '--strategy-path /freqtrade', 35 | sprintf('--strategy %s', $instance->strategy), 36 | ]; 37 | 38 | return trim(Process::processCommandLine(implode(' ', $processCommand))); 39 | } 40 | 41 | public static function restartInstance(Instance $instance) 42 | { 43 | return trim(Process::processCommandLine( 44 | sprintf('docker restart %s', $instance->getDockerCoreInstanceName()) 45 | )); 46 | } 47 | 48 | public static function stopInstance(Instance $instance): void 49 | { 50 | $processCommand = [ 51 | sprintf('docker kill %s', $instance->getDockerCoreInstanceName()), 52 | sprintf('docker rm %s', $instance->getDockerCoreInstanceName()), 53 | ]; 54 | 55 | Process::processCommandLine(implode('; ', $processCommand), false); 56 | } 57 | 58 | public static function isInstanceCoreRunning(Instance $instance): bool 59 | { 60 | return (bool) Process::processCommandLine( 61 | sprintf('docker ps -q -f "name=%s"', $instance->getDockerCoreInstanceName()) 62 | ); 63 | } 64 | 65 | public static function backtestDownloadDataForInstance(Instance $instance, int $daysCount = 5, array $timeframes): void 66 | { 67 | $processCommand = [ 68 | sprintf('docker run --rm --name %s-download-data', $instance->getDockerCoreInstanceName()), 69 | '--volume /etc/localtime:/etc/localtime:ro', 70 | sprintf('--volume %s:/freqtrade/config.json:ro', $instance->files['host']['config_backtest']), 71 | sprintf('--volume %s:/freqtrade/user_data:rw', $instance->directories['host']['data']), 72 | 'ph3nol/freqtrade:latest', 73 | 'download-data', 74 | sprintf('-t %s', implode(' ', $timeframes)), 75 | sprintf('--exchange %s', $instance->config['exchange']['name']), 76 | sprintf('--days=%d', $daysCount), 77 | ]; 78 | 79 | Process::processCommandLine(implode(' ', $processCommand)); 80 | } 81 | 82 | public static function backtestInstance(Instance $instance, float $fee = 0.001): string 83 | { 84 | $processCommand = [ 85 | sprintf('docker run --rm --name %s-backtest', $instance->getDockerCoreInstanceName()), 86 | '--volume /etc/localtime:/etc/localtime:ro', 87 | sprintf('--volume %s:/freqtrade/config.json:ro', $instance->files['host']['config_backtest']), 88 | sprintf('--volume %s/%s.py:/freqtrade/strategy.py:ro', HOST_MANAGER_STRATEGIES_DIRECTORY, $instance->strategy), 89 | sprintf('--volume %s:/freqtrade/freqtrade.log:rw', $instance->files['host']['logs']), 90 | sprintf('--volume %s:/freqtrade/user_data:rw', $instance->directories['host']['data']), 91 | 'ph3nol/freqtrade:latest', 92 | 'backtesting --config /freqtrade/config.json', 93 | sprintf('--fee %s', $fee), 94 | '--enable-protections', 95 | '--export trades', 96 | '--strategy-path /freqtrade', 97 | sprintf('--strategy %s', $instance->strategy), 98 | ]; 99 | 100 | return Process::processCommandLine(implode(' ', $processCommand), true, true); 101 | } 102 | 103 | public static function plotInstance(Instance $instance, array $pairs = []): void 104 | { 105 | $processCommand = [ 106 | sprintf('docker run --rm --name %s-plotting', $instance->getDockerCoreInstanceName()), 107 | '--volume /etc/localtime:/etc/localtime:ro', 108 | sprintf('--volume %s:/freqtrade/config.json:ro', $instance->files['host']['config_backtest']), 109 | sprintf('--volume %s/strategies/%s.py:/freqtrade/strategy.py:ro', HOST_MANAGER_DIRECTORY, $instance->strategy), 110 | sprintf('--volume %s:/freqtrade/freqtrade.log:rw', $instance->files['host']['logs']), 111 | sprintf('--volume %s:/freqtrade/user_data:rw', $instance->directories['host']['data']), 112 | 'ph3nol/freqtrade:latest', 113 | 'plot-dataframe --config /freqtrade/config.json', 114 | '--strategy-path /freqtrade', 115 | sprintf('--strategy %s', $instance->strategy), 116 | ]; 117 | 118 | if ($pairs) { 119 | $processCommand[] = sprintf('-p %s', implode(' ', $pairs)); 120 | } 121 | 122 | Process::processCommandLine(implode(' ', $processCommand), true, true); 123 | } 124 | 125 | public static function getPairsList(Instance $instance): ?string 126 | { 127 | $processCommand = [ 128 | sprintf('docker run --rm --name %s-test-pairlist', $instance->getDockerCoreInstanceName()), 129 | '--volume /etc/localtime:/etc/localtime:ro', 130 | sprintf('--volume %s:/freqtrade/config.json:ro', $instance->files['host']['config_backtest']), 131 | sprintf('--volume %s:/freqtrade/user_data:rw', $instance->directories['host']['data']), 132 | 'ph3nol/freqtrade:latest', 133 | 'test-pairlist', 134 | ]; 135 | 136 | return Process::processCommandLine(implode(' ', $processCommand), false) ? : null; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Manager/Infra/Process/InstanceUIProcess.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InstanceUIProcess 11 | { 12 | public static function run(Instance $instance): ?string 13 | { 14 | $managerConfig = MANAGER_CONFIGURATION; 15 | 16 | $processCommand = [ 17 | sprintf('docker run --name %s --detach --restart=always', $instance->getDockerUIInstanceName()), 18 | sprintf('-e TRADING_BOT_API_PORT=%d', $instance->parameters['ports']['api']), 19 | sprintf('-e TRADING_BOT_API_HOST=%s', $managerConfig['hosts']['api']), 20 | '--volume /etc/localtime:/etc/localtime:ro', 21 | sprintf('-v /tmp/freqtrade-manager/resources/scripts/ui-instance-entrypoint.sh:/docker-entrypoint.d/100-ui-instance-entrypoint.sh:ro'), 22 | sprintf('--publish %d:80/tcp', $instance->parameters['ports']['ui']), 23 | 'ph3nol/freqtrade-ui:latest', 24 | ]; 25 | 26 | return trim(Process::processCommandLine(implode(' ', $processCommand))); 27 | } 28 | 29 | public static function restartInstance(Instance $instance) 30 | { 31 | return trim(Process::processCommandLine( 32 | sprintf('docker restart %s', $instance->getDockerUIInstanceName()) 33 | )); 34 | } 35 | 36 | public static function stop(Instance $instance): void 37 | { 38 | $processCommand = [ 39 | sprintf('docker kill %s', $instance->getDockerUIInstanceName()), 40 | sprintf('docker rm %s', $instance->getDockerUIInstanceName()), 41 | ]; 42 | 43 | Process::processCommandLine(implode('; ', $processCommand), false); 44 | } 45 | 46 | public static function isRunning(Instance $instance): bool 47 | { 48 | return (bool) Process::processCommandLine( 49 | sprintf('docker ps -q -f "name=%s"', $instance->getDockerUIInstanceName()) 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Manager/Infra/Process/ManagerProcess.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ManagerProcess 11 | { 12 | public static function getDockerStatus(): string 13 | { 14 | $commandLine = 'docker ps --format \'{{ .ID }};;;{{ .Names }};;;{{.Image}};;;{{ .Status }}\''; 15 | $dockerStatusOutput = trim(Process::processCommandLine($commandLine, false)); 16 | 17 | return $dockerStatusOutput; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Manager/Infra/Process/Process.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | final class Process 13 | { 14 | public static function processCommandLine(string $commandLine, bool $withException = true, bool $unlimited = false): ?string 15 | { 16 | $process = BaseProcess::fromShellCommandline($commandLine); 17 | 18 | if ($unlimited) { 19 | $process->setTimeout(0); 20 | } 21 | 22 | try { 23 | $process->run(); 24 | } catch (ProcessTimedOutException $e) { 25 | return null; 26 | } 27 | 28 | if (!$process->isSuccessful() && $withException) { 29 | throw new ProcessFailedException($process); 30 | } 31 | 32 | if (!$process->isSuccessful()) { 33 | return null; 34 | } 35 | 36 | return $process->getOutput(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/BackTestCommand.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class BackTestCommand extends BaseCommand 18 | { 19 | protected static $defaultName = 'backtest'; 20 | 21 | protected function configure() 22 | { 23 | parent::configure(); 24 | 25 | $this 26 | ->addOption('--no-download', null, InputOption::VALUE_OPTIONAL, 'Disable data download and use already grabbed one', false) 27 | ->addOption('--days', null, InputOption::VALUE_OPTIONAL, 'Days count', 5) 28 | ->addOption('--fee', null, InputOption::VALUE_OPTIONAL, 'Fee', 0.001) 29 | ->addOption('--plotting', null, InputOption::VALUE_OPTIONAL, 'Plotting', false) 30 | ; 31 | } 32 | 33 | protected function execute(InputInterface $input, OutputInterface $output) 34 | { 35 | parent::execute($input, $output); 36 | 37 | $instance = $this->askForInstance($input, $output); 38 | if (null === $instance) { 39 | return Command::SUCCESS; 40 | } 41 | $handler = InstanceHandler::init($instance); 42 | InstanceFilesystem::writeInstanceConfigBacktest($instance); 43 | 44 | $daysCount = (int) $input->getOption('days'); 45 | $output->writeln(sprintf('⚙️ Getting instance pairs, period of %d day(s)...', $daysCount)); 46 | $this->generatePairsAndUpdateInstance($handler, $daysCount); 47 | 48 | if (false === $input->getOption('no-download')) { 49 | $output->writeln('⚙️ Downloading backtest data...'); 50 | $handler->backtestDownloadData((int) $input->getOption('days')); 51 | } 52 | 53 | $output->writeln('⚙️ Backtesting...'); 54 | $backtestOutput = $handler->backtest((float) $input->getOption('fee')); 55 | $output->writeln($backtestOutput); 56 | 57 | $plottingCount = $input->getOption('plotting'); 58 | if (false !== $plottingCount) { 59 | $output->writeln('⚙️ Plotting...'); 60 | 61 | $plottingPairs = []; 62 | if (null !== $plottingCount) { 63 | $plottingPairs = $instance->config['exchange']['pair_whitelist']; 64 | shuffle($plottingPairs); 65 | $plottingPairs = array_slice($plottingPairs, 0, $plottingCount); 66 | } 67 | 68 | $handler->removePlottingData(); 69 | $handler->plot($plottingPairs); 70 | } 71 | 72 | $output->writeln(''); 73 | $output->writeln('🎉 Done!'); 74 | 75 | return Command::SUCCESS; 76 | } 77 | 78 | private function generatePairsAndUpdateInstance(InstanceHandler $instanceHandler, int $daysCount): void 79 | { 80 | $instance = $instanceHandler->getInstance(); 81 | // $pairList = $this->fetchPairsFromTradingViewBehaviour($instance, $daysCount); 82 | $pairList = $this->fetchPairsFromConfig($instanceHandler, $daysCount); 83 | 84 | $instance->updateStaticPairList(array_unique($pairList)); 85 | $instance->removeUnbacktestablePairlistsFilters(); 86 | 87 | InstanceFilesystem::writeInstanceConfigBacktest($instance); 88 | } 89 | 90 | private function fetchPairsFromConfig(InstanceHandler $instanceHandler, int $daysCount): array 91 | { 92 | return $instanceHandler->getPairsList(); 93 | } 94 | 95 | private function fetchPairsFromTradingViewBehaviour(InstanceHandler $instanceHandler, int $daysCount): array 96 | { 97 | $instance = $instanceHandler->getInstance(); 98 | 99 | $pairsBehaviour = new TradingViewScanBehaviour(); 100 | $requestPayload = <<scrapPairlistsFromTW( 135 | sprintf($requestPayload, $daysCount) 136 | ); 137 | 138 | $exchangeKey = strtoupper($instance->config['exchange']['name']); 139 | 140 | return $pairs[$exchangeKey][$instance->config['stake_currency']] ?? []; 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/BaseCommand.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class BaseCommand extends Command 25 | { 26 | const CONFIGURATION_FILE_PATH_DEFAUT = '/manager/config.yaml'; 27 | const INSTANCE_FOLDER_PATH_DEFAUT = '/manager/instances'; 28 | 29 | protected function configure() 30 | { 31 | $this 32 | ->addArgument('instance', InputArgument::OPTIONAL, 'Instance name') 33 | ; 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output) 37 | { 38 | $outputStyle = new OutputFormatterStyle('white', 'red', ['blink']); 39 | $output->getFormatter()->setStyle('warning', $outputStyle); 40 | 41 | $outputStyle = new OutputFormatterStyle('red', null); 42 | $output->getFormatter()->setStyle('danger', $outputStyle); 43 | 44 | $outputStyle = new OutputFormatterStyle('#888'); 45 | $output->getFormatter()->setStyle('muted', $outputStyle); 46 | 47 | $outputStyle = new OutputFormatterStyle(null, null, ['reverse']); 48 | $output->getFormatter()->setStyle('reverse', $outputStyle); 49 | 50 | ProgressBar::setFormatDefinition('withDescription', '⚙️ %current%/%max% --- %message%'); 51 | } 52 | 53 | protected function renderInstancesTable(array $instances, OutputInterface $output) 54 | { 55 | $statusData = []; 56 | $instanceIndex = 1; 57 | foreach ($instances as $instance) { 58 | $components = $this->getInstanceComponents($instance); 59 | $containers = $this->getInstanceContainers($instance); 60 | $informations = $this->getInstanceInformations($instance); 61 | 62 | $statusData[] = [ 63 | sprintf( 64 | "%s\n%s", 65 | (string) $instance, 66 | $instance->isProduction() ? 'PRODUCTION' : 'DRY-RUN' 67 | ), 68 | implode("\n", $informations), 69 | implode("\n", $containers), 70 | implode("\n", $components), 71 | ]; 72 | 73 | if ($instanceIndex < count($instances)) { 74 | $statusData[] = new TableSeparator(); 75 | } 76 | 77 | $instanceIndex++; 78 | } 79 | 80 | $table = new Table($output); 81 | $table 82 | ->setHeaders([ 83 | 'Instance', 84 | 'Configuration', 85 | new TableCell('Components', ['colspan' => 2]) 86 | ]) 87 | ->setRows($statusData); 88 | $table->setStyle('box-double'); 89 | $table->render(); 90 | } 91 | 92 | protected function askForInstance(InputInterface $input, OutputInterface $output): ?Instance 93 | { 94 | $manager = Manager::fromFile(MANAGER_DIRECTORY . '/manager.yaml'); 95 | 96 | if ($instanceSlug = $input->getArgument('instance')) { 97 | return $manager->findRequiredInstanceFromSlug($instanceSlug); 98 | } 99 | 100 | $instancesData = []; 101 | $instances = array_values($manager->getInstances()); 102 | foreach ($instances as $k => $instance) { 103 | InstanceHandler::init($instance); 104 | 105 | $instancesData[] = [ 106 | sprintf('[%d]', $k +1), 107 | (string) $instance, 108 | $instance->isRunning() ? '' : '' 109 | ]; 110 | } 111 | 112 | $table = new Table($output); 113 | $table 114 | ->setHeaders(['', 'Instance', 'Status']) 115 | ->setRows($instancesData); 116 | $table->setStyle('box-double'); 117 | $table->render(); 118 | 119 | $helper = $this->getHelper('question'); 120 | $question = new Question('Which instance is concerned? (0 to CANCEL) --> '); 121 | $choiceNumber = $helper->ask($input, $output, $question); 122 | if ('0' === $choiceNumber || 'cancel' === strtolower($choiceNumber)) { 123 | return null; 124 | } 125 | 126 | $instance = $instances[(int) $choiceNumber - 1] ?? null; 127 | if (null === $instance) { 128 | $output->writeln('Invalid choice'); 129 | 130 | return null; 131 | } 132 | 133 | return $instance; 134 | } 135 | 136 | private function getInstanceInformations(Instance $instance): array 137 | { 138 | return [ 139 | sprintf('%s', $instance->strategy), 140 | sprintf( 141 | '-> Stakes: %s %f %s', 142 | (-1 === $instance->config['max_open_trades']) ? 'Unlimited' : $instance->config['max_open_trades'] ?? 0 . ' x', 143 | $instance->config['stake_amount'] ?? 0, 144 | $instance->config['stake_currency'] ?? 'BTC' 145 | ), 146 | sprintf('-> DRW: %f %s', $instance->config['dry_run_wallet'] ?? 0, $instance->config['stake_currency'] ?? 'BTC'), 147 | sprintf('-> Behaviours: %s', $instance->behaviours ? implode(', ', array_keys($instance->behaviours)) : '∅'), 148 | ]; 149 | } 150 | 151 | private function getInstanceContainers(Instance $instance): array 152 | { 153 | $managerConfig = MANAGER_CONFIGURATION; 154 | 155 | $containers = []; 156 | $containers[] = sprintf( 157 | '%s Core%s %s', 158 | $instance->isRunning() ? '' : '', 159 | ($instance->isRunning() && $instance->uptime) ? sprintf(' (%s)', $instance->uptime) : '', 160 | $instance->isOutOfTradingHours() ? '⌚︎' : '' 161 | ); 162 | $containers[] = sprintf( 163 | '%s UI%s', 164 | $instance->isUIRunning() ? '' : '', 165 | $managerConfig['hosts']['ui'], 166 | $instance->parameters['ports']['ui'], 167 | ($instance->isUIRunning() && $instance->uiUptime) ? sprintf(' (%s)', $instance->uiUptime) : '' 168 | ); 169 | 170 | return $containers; 171 | } 172 | 173 | private function getInstanceComponents(Instance $instance): array 174 | { 175 | $managerConfig = MANAGER_CONFIGURATION; 176 | 177 | $components = []; 178 | $components[] = sprintf( 179 | '%s API Server', 180 | $instance->isApiEnabled() ? '' : '-', 181 | $managerConfig['hosts']['api'], 182 | $instance->parameters['ports']['api'] 183 | ); 184 | $components[] = sprintf('%s Edge', $instance->isEdgeEnabled() ? '' : '-'); 185 | $components[] = sprintf('%s Telegram', $instance->isTelegramEnabled() ? '' : '-'); 186 | $components[] = sprintf('%s Force-Buy', $instance->isForceBuyEnabled() ? '' : '-'); 187 | 188 | return $components; 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/CronCommand.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class CronCommand extends BaseCommand 18 | { 19 | private const CRONTAB_LINE = '*/1 * * * * BOT_CONFIG_DIRECTORY=%s %s cron >> /tmp/trading-bot-manager-cron.log'; 20 | 21 | protected static $defaultName = 'cron'; 22 | 23 | protected function configure() 24 | { 25 | parent::configure(); 26 | 27 | $this 28 | ->addOption('--crontab', null, InputOption::VALUE_OPTIONAL, 'Output the Crontab line', false) 29 | ->addOption('--only-instances', null, InputOption::VALUE_OPTIONAL, 'Instance updates only', false) 30 | ->addOption('--force', null, InputOption::VALUE_OPTIONAL, 'Force updates', false) 31 | ; 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output) 35 | { 36 | parent::execute($input, $output); 37 | 38 | $forceUpdates = false !== $input->getOption('force'); 39 | $onlyInstances = false !== $input->getOption('only-instances'); 40 | 41 | $manager = Manager::fromFile(MANAGER_DIRECTORY . '/manager.yaml'); 42 | 43 | if (false !== $input->getOption('crontab')) { 44 | $output->writeln('This line is to add to your crontabs, in order to run periodic tasks needed by your instances and their behaviours.'); 45 | $output->writeln(''); 46 | $output->writeln(sprintf( 47 | '' . self::CRONTAB_LINE . '', 48 | HOST_MANAGER_DIRECTORY, 49 | HOST_BOT_SCRIPT_PATH 50 | )); 51 | 52 | return Command::SUCCESS; 53 | } 54 | 55 | $output->writeln('⚙️ Applying Instances trading hours limitations...'); 56 | $this->applyTradingHours($manager, $output); 57 | 58 | $output->writeln(''); 59 | $output->writeln('⚙️ Updating behaviours...'); 60 | $updatedInstances = $this->updateBehaviours($manager, $output, $forceUpdates, $onlyInstances); 61 | 62 | $this->restartInstances($updatedInstances, $output); 63 | 64 | $output->writeln(''); 65 | $output->writeln('🎉 Done!'); 66 | 67 | return Command::SUCCESS; 68 | } 69 | 70 | private function applyTradingHours(Manager $manager, OutputInterface $output): void 71 | { 72 | foreach ($manager->getInstances() as $instance) { 73 | $handler = InstanceHandler::init($instance); 74 | 75 | if ($instance->isRunning() && true === $instance->isOutOfTradingHours()) { 76 | $output->write(sprintf(' [%s] Out of trading hours -> Stopping... ', (string) $instance)); 77 | $handler->stop(); 78 | $output->writeln('✅'); 79 | } else { 80 | $output->writeln(sprintf(' [%s] ⏺', (string) $instance)); 81 | } 82 | } 83 | } 84 | 85 | private function updateBehaviours( 86 | Manager $manager, 87 | OutputInterface $output, 88 | bool $forceUpdates = false, 89 | bool $onlyInstances = false 90 | ): array { 91 | $updatedInstances = []; 92 | 93 | foreach ($manager->getBehaviours() as $behaviour) { 94 | $behaviourName = ucfirst($behaviour->getSlug()); 95 | 96 | if ($forceUpdates || false === $onlyInstances) { 97 | $output->write(sprintf(' [%s] Main update... ', $behaviourName)); 98 | if ($forceUpdates || $behaviour->needsCronUpdate()) { 99 | $behaviour->updateCron(); 100 | $output->writeln('✅'); 101 | } else { 102 | $output->writeln('⏺'); 103 | } 104 | } 105 | 106 | foreach ($manager->getInstances() as $instance) { 107 | $handler = InstanceHandler::init($instance); 108 | 109 | if (false === $instance->hasBehaviour($behaviour)) { 110 | continue; 111 | } 112 | 113 | $output->write(sprintf( 114 | ' [%s] @ [%s] Updating... ', 115 | $behaviourName, 116 | (string) $instance 117 | )); 118 | 119 | if ($forceUpdates || $behaviour->needsInstanceUpdate($instance)) { 120 | $behaviour->updateInstanceFromCron($instance); 121 | InstanceFilesystem::writeInstanceConfig($instance); 122 | $updatedInstances[] = $instance; 123 | $output->writeln('✅'); 124 | } else { 125 | $output->writeln('⏺'); 126 | } 127 | } 128 | 129 | $behaviour->write(); 130 | } 131 | 132 | return $updatedInstances; 133 | } 134 | 135 | private function restartInstances(array $updatedInstances, OutputInterface $output): void 136 | { 137 | $instancesToRestart = array_filter($updatedInstances, function (Instance $instance): bool { 138 | return $instance->isRunning(); 139 | }); 140 | 141 | if (!$instancesToRestart) { 142 | return ; 143 | } 144 | 145 | $output->writeln(''); 146 | $output->writeln('⚙️ Restarting updated running instances...'); 147 | foreach ($instancesToRestart as $instance) { 148 | $handler = InstanceHandler::init($instance); 149 | 150 | if ($instance->isRunning()) { 151 | $output->write(sprintf( 152 | ' [%s] Restarting... ', 153 | (string) $instance 154 | )); 155 | $handler->restart(false); 156 | $output->writeln('✅'); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/InstanceStopCommand.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class InstanceStopCommand extends BaseCommand 16 | { 17 | protected static $defaultName = 'stop'; 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output) 20 | { 21 | parent::execute($input, $output); 22 | 23 | $manager = Manager::fromFile(MANAGER_DIRECTORY . '/manager.yaml'); 24 | 25 | $instance = $this->askForInstance($input, $output); 26 | if (null === $instance) { 27 | return Command::SUCCESS; 28 | } 29 | $handler = InstanceHandler::init($instance); 30 | 31 | $output->writeln('Stopping the instance...'); 32 | $handler->stop(); 33 | $output->writeln('🎉 Instance has been stopped!'); 34 | 35 | return Command::SUCCESS; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/InstancesResetDataCommand.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class InstancesResetDataCommand extends BaseCommand 17 | { 18 | protected static $defaultName = 'reset'; 19 | 20 | protected function execute(InputInterface $input, OutputInterface $output) 21 | { 22 | parent::execute($input, $output); 23 | 24 | $manager = Manager::fromFile(MANAGER_DIRECTORY . '/manager.yaml'); 25 | 26 | $instance = $this->askForInstance($input, $output); 27 | if (null === $instance) { 28 | return Command::SUCCESS; 29 | } 30 | $handler = InstanceHandler::init($instance); 31 | 32 | if (false === $input->getOption('no-interaction')) { 33 | $this->renderInstancesTable([$instance], $output); 34 | 35 | $output->writeln('You gonna reset this instance data, which will stop it (if running) and definitely erase its data (irrevocable).'); 36 | 37 | $helper = $this->getHelper('question'); 38 | $question = new ConfirmationQuestion('Do you confirm? [y/N] ', false); 39 | 40 | if (!$helper->ask($input, $output, $question)) { 41 | return Command::SUCCESS; 42 | } 43 | 44 | $output->writeln(''); 45 | } 46 | 47 | if ($instance->isRunning()) { 48 | $handler->stop(); 49 | } 50 | 51 | $output->writeln('Resetting instance data...'); 52 | 53 | $output->write(' Main instance data... '); 54 | $handler = InstanceHandler::init($instance); 55 | $handler->reset(); 56 | $output->writeln('✅'); 57 | 58 | $behaviours = $manager->getBehaviours(); 59 | if ($behaviours) { 60 | $output->write(' Behaviours instance data... '); 61 | 62 | foreach ($behaviours as $behaviour) { 63 | $behaviourName = ucfirst($behaviour->getSlug()); 64 | $behaviour->resetInstance($instance); 65 | $behaviour->write(); 66 | } 67 | 68 | $output->writeln('✅'); 69 | } 70 | $output->writeln('🎉 Instance data has been resetted!'); 71 | 72 | return Command::SUCCESS; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/InstancesStatusCommand.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class InstancesStatusCommand extends BaseCommand 16 | { 17 | protected static $defaultName = 'status'; 18 | 19 | protected function execute(InputInterface $input, OutputInterface $output) 20 | { 21 | parent::execute($input, $output); 22 | 23 | $manager = Manager::fromFile(MANAGER_DIRECTORY . '/manager.yaml'); 24 | 25 | foreach ($manager->getInstances() as $instance) { 26 | InstanceHandler::init($instance); 27 | } 28 | 29 | $this->renderManagerParametersTable($manager, $output); 30 | $output->writeln(''); 31 | $this->renderInstancesTable($manager->getInstances(), $output); 32 | 33 | return Command::SUCCESS; 34 | } 35 | 36 | private function renderManagerParametersTable(Manager $manager, OutputInterface $output): void 37 | { 38 | $managerParameters = $manager->getParameters(); 39 | 40 | $paramsData = [ 41 | [ 42 | 'Base API host', 43 | $managerParameters['hosts']['api'], 44 | ], 45 | [ 46 | 'Base UI host', 47 | $managerParameters['hosts']['ui'], 48 | ], 49 | [ 50 | 'API CORS domains', 51 | implode(', ', $managerParameters['cors_domains'] ?? []), 52 | ], 53 | ]; 54 | 55 | $table = new Table($output); 56 | $table 57 | ->setHeaders([ 58 | 'Parameter', 59 | 'Value' 60 | ]) 61 | ->setRows($paramsData); 62 | $table->setStyle('borderless'); 63 | $table->render(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Manager/UI/Console/TradeCommand.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | class TradeCommand extends BaseCommand 17 | { 18 | protected static $defaultName = 'trade'; 19 | 20 | protected function configure() 21 | { 22 | parent::configure(); 23 | 24 | $this 25 | ->addOption('no-ui', null, InputOption::VALUE_OPTIONAL, 'To disable UI and avoid its Docker container creation', false) 26 | ; 27 | } 28 | 29 | protected function execute(InputInterface $input, OutputInterface $output) 30 | { 31 | parent::execute($input, $output); 32 | 33 | $instance = $this->askForInstance($input, $output); 34 | if (null === $instance) { 35 | return Command::SUCCESS; 36 | } 37 | $handler = InstanceHandler::init($instance); 38 | 39 | if (false === $input->getOption('no-interaction')) { 40 | $this->renderInstancesTable([$instance], $output); 41 | 42 | $helper = $this->getHelper('question'); 43 | $question = new ConfirmationQuestion('You gonna trade with this instance, do you confirm? [y/N] ', false); 44 | 45 | if (!$helper->ask($input, $output, $question)) { 46 | return Command::SUCCESS; 47 | } 48 | 49 | $output->writeln(''); 50 | } 51 | 52 | $managerConfig = MANAGER_CONFIGURATION; 53 | $stepsCount = 1; 54 | if ($instance->isRunning()) { 55 | $stepsCount++; 56 | } 57 | 58 | $progressBar = new ProgressBar($output, $stepsCount); 59 | $progressBar->setFormat('withDescription'); 60 | $progressBar->setMessage('Preparing...'); 61 | $progressBar->start(); 62 | 63 | if ($instance->isRunning()) { 64 | $progressBar->setMessage('Stopping running Docker instance...'); 65 | $progressBar->advance(); 66 | $handler->stop(); 67 | } 68 | 69 | $withUI = (false === $input->getOption('no-ui')); 70 | $progressBar->setMessage(sprintf('Launching instance from Docker...%s', $withUI ? ' (+ UI instance)': '')); 71 | $progressBar->advance(); 72 | $dockerIds = $handler->trade($withUI); 73 | 74 | $progressBar->finish(); 75 | 76 | $output->writeln(''); 77 | $output->writeln('🎉 Instance is launched!'); 78 | if ($dockerIds['core']) { 79 | $output->writeln(sprintf('> Core Docker container ID: %s', $dockerIds['core'])); 80 | } 81 | if ($dockerIds['ui']) { 82 | $output->writeln(sprintf('> UI Docker container ID: %s', $dockerIds['ui'])); 83 | } 84 | 85 | $output->writeln(''); 86 | $this->renderInstancesTable([$instance], $output); 87 | 88 | return Command::SUCCESS; 89 | } 90 | } 91 | --------------------------------------------------------------------------------