├── .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 | [](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 | 
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 | 
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 |
--------------------------------------------------------------------------------