Compare commits

..

619 Commits

Author SHA1 Message Date
Matthias
1593847203 Merge pull request #2453 from freqtrade/rel/2019-10
Release 2019-10
2019-11-01 19:52:07 +01:00
Matthias
7204227022 Replace coins in whitelist with existing ones 2019-10-31 06:29:09 +01:00
Matthias
dab4ab78fc Fix create_cum_profit to work with trades that don't open on candle
opens
2019-10-31 06:27:27 +01:00
Matthias
a74b941b72 Add test to verify this is correct 2019-10-31 06:27:22 +01:00
Matthias
89bba6f776 Version bump 2019-10-31 06:25:30 +01:00
Matthias
82f86569ee Merge branch 'master' into rel/2019-11 2019-10-31 06:25:02 +01:00
hroff-1902
2a95d6855b Merge pull request #2419 from freqtrade/silence_bot
Change loglevel of repeated message to debug
2019-10-26 10:31:59 +03:00
Matthias
3929ad4e1f Fix typo 2019-10-26 09:21:51 +02:00
Matthias
2f1d9696cd Change keepalive to heartbeat 2019-10-25 20:00:08 +02:00
Matthias
0773a65333 Add I Am Alive Message 2019-10-25 15:01:35 +02:00
Matthias
8201f70a80 Change loglevel of repeated message to debug 2019-10-25 14:19:02 +02:00
Matthias
74b2f11d4f Merge pull request #2416 from hroff-1902/cleanup-scripts
Cleanup in scripts
2019-10-25 06:31:47 +02:00
Matthias
514e073c57 Merge pull request #2415 from hroff-1902/rpc-race
minor: Fix potential race conditions between RPC and Freqtradebot
2019-10-25 06:14:59 +02:00
hroff-1902
59e881c59d Remove obsolete scripts 2019-10-24 23:11:07 +03:00
hroff-1902
2e1e080022 Fix potential race conditions between RPC and Freqtradebot during initialization 2019-10-24 22:33:44 +03:00
hroff-1902
470efd6f40 Merge pull request #2412 from freqtrade/align_utils
[minor] Use exchange.name instead of config['exchange']['name']
2019-10-24 00:16:32 +03:00
hroff-1902
6640f4a1b2 Make flake happy 2019-10-23 23:57:17 +03:00
hroff-1902
e408274fb3 Merge branch 'develop' into align_utils 2019-10-23 23:45:33 +03:00
hroff-1902
a135eaa993 Merge pull request #2370 from hroff-1902/list-pairs2
Add list-pairs and list-markets subcommands
2019-10-23 22:42:13 +03:00
Matthias
87ff7be550 Use exchange.name instead of config['exchange']['name'] 2019-10-23 07:08:49 +02:00
hroff-1902
7441300270 Merge remote-tracking branch 'origin/develop' into list-pairs2 2019-10-22 20:19:03 +03:00
hroff-1902
b4f4fae0ca Merge pull request #2411 from freqtrade/fix/2369
Correctly pass validate flag to fallback exchange too
2019-10-22 20:14:44 +03:00
Matthias
336808ec54 Correctly pass validate flag to fallback exchange too 2019-10-22 14:02:47 +02:00
hroff-1902
b26faa13bd Call validate_timeframe only when validate is True 2019-10-22 13:51:36 +03:00
hroff-1902
562e4e63de Set validate=False for exchangÑe in start_list_markets 2019-10-22 13:48:54 +03:00
hroff-1902
ad5f7e1581 Merge remote-tracking branch 'origin/develop' into list-pairs2 2019-10-22 12:30:39 +03:00
hroff-1902
3cf95f9f6c Merge pull request #2369 from freqtrade/disable_exchangevalidate
Allow skipping of exchange validation
2019-10-22 12:22:48 +03:00
hroff-1902
f0710cafd0 Merge pull request #2403 from freqtrade/dependabot/pip/develop/mypy-0.740
Bump mypy from 0.730 to 0.740
2019-10-22 00:11:13 +03:00
hroff-1902
73fa5bae96 minor: Fix wording in a docstring 2019-10-22 00:03:11 +03:00
Matthias
a43d436f98 Move decorators out of API Class 2019-10-21 19:47:09 +02:00
hroff-1902
ff5ba64385 Improve docs 2019-10-21 14:06:46 +03:00
Matthias
7e38a07490 Merge pull request #2406 from freqtrade/dependabot/pip/develop/plotly-4.2.1
Bump plotly from 4.1.1 to 4.2.1
2019-10-21 12:30:04 +02:00
dependabot-preview[bot]
8872158a6a Bump mypy from 0.730 to 0.740
Bumps [mypy](https://github.com/python/mypy) from 0.730 to 0.740.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.730...v0.740)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 10:29:50 +00:00
Matthias
db3e789294 Merge pull request #2405 from freqtrade/dependabot/pip/develop/flake8-tidy-imports-3.0.0
Bump flake8-tidy-imports from 2.0.0 to 3.0.0
2019-10-21 12:28:31 +02:00
Matthias
4813ff338b Merge pull request #2401 from freqtrade/dependabot/pip/develop/python-telegram-bot-12.2.0
Bump python-telegram-bot from 12.1.1 to 12.2.0
2019-10-21 12:24:20 +02:00
Matthias
fbd7dc1d6c Merge pull request #2404 from freqtrade/dependabot/pip/develop/pandas-0.25.2
Bump pandas from 0.25.1 to 0.25.2
2019-10-21 12:07:15 +02:00
dependabot-preview[bot]
f07b26f245 Bump pandas from 0.25.1 to 0.25.2
Bumps [pandas](https://github.com/pandas-dev/pandas) from 0.25.1 to 0.25.2.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md)
- [Commits](https://github.com/pandas-dev/pandas/compare/v0.25.1...v0.25.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 08:33:48 +00:00
Matthias
3acc10dd48 Merge pull request #2400 from freqtrade/dependabot/pip/develop/numpy-1.17.3
Bump numpy from 1.17.2 to 1.17.3
2019-10-21 10:32:29 +02:00
dependabot-preview[bot]
e5f06c201f Bump python-telegram-bot from 12.1.1 to 12.2.0
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.1.1 to 12.2.0.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.1.1...v12.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 08:29:57 +00:00
Matthias
bf20cde83d Merge pull request #2402 from freqtrade/dependabot/pip/develop/ccxt-1.18.1306
Bump ccxt from 1.18.1260 to 1.18.1306
2019-10-21 10:28:23 +02:00
dependabot-preview[bot]
364859394b Bump plotly from 4.1.1 to 4.2.1
Bumps [plotly](https://github.com/plotly/plotly.py) from 4.1.1 to 4.2.1.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v4.1.1...v4.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 07:50:16 +00:00
dependabot-preview[bot]
657f1b6c45 Bump flake8-tidy-imports from 2.0.0 to 3.0.0
Bumps [flake8-tidy-imports](https://github.com/adamchainz/flake8-tidy-imports) from 2.0.0 to 3.0.0.
- [Release notes](https://github.com/adamchainz/flake8-tidy-imports/releases)
- [Changelog](https://github.com/adamchainz/flake8-tidy-imports/blob/master/HISTORY.rst)
- [Commits](https://github.com/adamchainz/flake8-tidy-imports/compare/2.0.0...3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 07:49:35 +00:00
dependabot-preview[bot]
e350bcc2ef Bump ccxt from 1.18.1260 to 1.18.1306
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1260 to 1.18.1306.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1260...1.18.1306)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 07:47:53 +00:00
dependabot-preview[bot]
c2566f2436 Bump numpy from 1.17.2 to 1.17.3
Bumps [numpy](https://github.com/numpy/numpy) from 1.17.2 to 1.17.3.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.17.2...v1.17.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-21 07:46:32 +00:00
hroff-1902
cac314d4b3 Merge pull request #2396 from freqtrade/improve_strat_docs
[doc] Clearly highlight potential problems with looking into the future
2019-10-21 08:37:49 +03:00
Matthias
b116cc75c4 Fix failing test 2019-10-21 07:07:25 +02:00
Matthias
bedbd964fc slightly rephrase strategy docs 2019-10-21 06:51:48 +02:00
hroff-1902
ca4d0067e4 Uncomment tests with --exchange 2019-10-21 02:15:37 +03:00
hroff-1902
8a0d90136c Improve unclear sentence in the docs 2019-10-20 23:31:44 +03:00
hroff-1902
5b680f2ece minor: Condense paragraphs in the docs 2019-10-20 23:26:25 +03:00
hroff-1902
1bc63288a3 Merge branch 'develop' into list-pairs2 2019-10-20 23:22:45 +03:00
hroff-1902
45b2d24b79 Improve docs 2019-10-20 23:00:17 +03:00
hroff-1902
10ca249293 Fix fluky test 2019-10-20 22:43:00 +03:00
hroff-1902
d6b6ded8bd Print empty line separator in case of human-readable formats (list and tabular) 2019-10-20 22:30:15 +03:00
hroff-1902
a52366c45d Merge pull request #2395 from freqtrade/reenable_docker_hyperopt
Update python base to 3.7.5 and install hyperopt dependencies in docker image
2019-10-20 20:17:27 +03:00
hroff-1902
88c91a8a54 Merge pull request #2392 from freqtrade/backtest-doc
[minor][doc]Fix backtesting format since sublist does not render correctly
2019-10-20 20:13:50 +03:00
hroff-1902
14755779de Merge pull request #2391 from freqtrade/plot_trades_in_min
[minor][plot] Plotting trades from database should show correct duration
2019-10-20 20:11:01 +03:00
Matthias
20dd3f2d67 Clearly highlight potential problems with looking into the future 2019-10-20 16:22:11 +02:00
Matthias
8a31b4c646 Update python base to 3.7.5 and install hyperopt dependencies 2019-10-20 10:47:04 +02:00
Matthias
78cd75dfef Add requirement 2019-10-20 10:35:36 +02:00
Matthias
b805e4e150 Try list extension 2019-10-20 10:34:04 +02:00
hroff-1902
6e938b59c8 Merge pull request #2390 from freqtrade/remove_hardcoded_default
exportfilename should respect configured user_data_dir
2019-10-19 22:18:08 +03:00
hroff-1902
4f17511fdc Merge pull request #2393 from freqtrade/remove_timeframe
Remove non-date based timeframe selection
2019-10-19 22:05:44 +03:00
Matthias
d8630ef847 Add one-sided ms timerange 2019-10-19 19:38:16 +02:00
hroff-1902
47fabca1d9 Merge pull request #2372 from xmatthias/kraken_ohlcv_emulate
download tick-based data to emulate candles
2019-10-19 19:32:37 +03:00
Matthias
c48876b196 Trades should use timestamps or dates, not indexes 2019-10-19 15:21:47 +02:00
Matthias
16e10d99b9 Remove timeframe logic for non-date entries 2019-10-19 15:10:48 +02:00
Matthias
0adcee9233 Fix backtesting format since sublist does not render correctly 2019-10-19 14:34:55 +02:00
Matthias
f41c659cb2 Plotting trades from database should show correct duration 2019-10-19 13:18:52 +02:00
Matthias
4c977b2e01 Merge pull request #2388 from hroff-1902/no-hyperopts
Minor: No more hyperoptS
2019-10-19 11:15:24 +02:00
Matthias
b152585d9b exportfilename should respect configured user_data_dir 2019-10-19 11:13:10 +02:00
Matthias
fd22c87295 Some minor cleanups to trades download methods and docs 2019-10-19 10:05:30 +02:00
Matthias
93b213ae0b Merge pull request #2389 from hroff-1902/minor-freqtrade
Minor freqtradebot cleanup
2019-10-19 09:52:48 +02:00
hroff-1902
30eb23e1aa Minor freqtrade cleanup 2019-10-18 23:41:07 +03:00
hroff-1902
4ec83a2c24 DefaultHyperOpts --> DefaultHyperOpt; hyperopts --> hyperopt where it's not correct 2019-10-18 23:29:19 +03:00
hroff-1902
9e23ca14d1 Merge pull request #2384 from freqtrade/improve_buy_timeout_handling
Improve buy timeout handling
2019-10-18 22:30:41 +03:00
hroff-1902
ebf5738a6e Merge pull request #2387 from freqtrade/fix/testswindows
[minor] Fix/tests windows
2019-10-18 20:43:44 +03:00
Matthias
c649f9844e Compare >= instead of = 2019-10-18 19:36:04 +02:00
Matthias
3208f30c30 Fix base64 test on windows 2019-10-18 14:19:17 +02:00
hroff-1902
5e731ec278 Add more tests 2019-10-18 14:55:59 +03:00
Matthias
e55b2a1a1c Allow test to pass on fast computers by setting the offset to -1 2019-10-18 12:36:45 +02:00
Matthias
ed8d805797 Make paths os independent to have tests pass on windows 2019-10-18 11:31:43 +02:00
hroff-1902
0ebf2e44be Merge pull request #2386 from freqtrade/backtesting_doc
[minor] Improve assumptions
2019-10-18 11:34:50 +03:00
Matthias
00a95945e1 Improve assumptions 2019-10-18 10:00:43 +02:00
Matthias
9d739f98ac use requested - remaining amount - not the requested amount! 2019-10-18 09:04:05 +02:00
Matthias
2588990f4b Require unfilledtimeout - don't require telegram in config 2019-10-18 07:10:02 +02:00
Matthias
271846dfb6 Simplify cancel timedout 2019-10-18 07:01:05 +02:00
Matthias
c181fac6c7 fix #2383 2019-10-18 06:48:39 +02:00
Matthias
0ac46eddca Add tests for new scenario 2019-10-18 06:48:39 +02:00
Matthias
c735d35265 Extract open_trade generation from freqtradebot 2019-10-18 06:48:33 +02:00
hroff-1902
e957894852 Rename start_list_pairs() -> start_list_markets() 2019-10-18 01:26:05 +03:00
hroff-1902
369335b80c Add tests for start_list_pairs() 2019-10-18 01:07:52 +03:00
hroff-1902
2ebddcf45c Make flake happy again 2019-10-17 23:40:29 +03:00
hroff-1902
8564affdf0 Add tests for Exchange.get_markets() 2019-10-17 22:45:20 +03:00
Matthias
a39d51d7d0 Update test to use limit_buy_order 2019-10-17 19:36:57 +02:00
hroff-1902
750dc8bf56 Add tests for market_is_active() 2019-10-17 19:24:39 +03:00
hroff-1902
033742b708 Fix pairlists to use market_is_active() instead of custom check 2019-10-17 19:06:58 +03:00
hroff-1902
84ba431d10 Introduce a market with no 'active' field in conftest 2019-10-17 19:05:50 +03:00
hroff-1902
b6e26c82ea Replace market_is_pair() by symbol_is_pair() 2019-10-17 18:44:25 +03:00
hroff-1902
e8eb968a6f Add tests for market_is_pair() 2019-10-17 18:19:50 +03:00
hroff-1902
66605a1909 Add tests for plural(), taken from #1989 2019-10-17 17:52:33 +03:00
hroff-1902
1e61263a28 More sofisticated market_is_pair(), taken from #1989 2019-10-17 17:49:04 +03:00
hroff-1902
bd08874f1f Fix options metavars shown in the helpstring 2019-10-17 17:31:49 +03:00
hroff-1902
ff6a3465a7 Docs added 2019-10-17 17:22:33 +03:00
Matthias
5b58141f6b iFix grammar issue 2019-10-17 06:11:10 +02:00
hroff-1902
bf4e9a5dbb Code cleanup 2019-10-17 04:34:05 +03:00
hroff-1902
837d4d82b4 Sort tabular and csv data by symbol as well 2019-10-17 03:06:51 +03:00
hroff-1902
a8ffd29e18 Remove --active-only, introduce -a/--all instead 2019-10-17 02:42:07 +03:00
hroff-1902
92fda0f76c Allow --base and --quote be lists of currencies 2019-10-17 02:09:19 +03:00
hroff-1902
df62dd65d3 Merge pull request #2382 from freqtrade/iresolver_small_improvements
[minor] Iresolver small improvements
2019-10-16 23:17:52 +03:00
hroff-1902
d72d388726 Make flake happy 2019-10-16 10:55:09 +03:00
Matthias
fda71085e0 Refactor load-path building to parent class 2019-10-16 08:12:24 +02:00
Matthias
1a765f1a17 Return generator instead of Object from _get_valid_object 2019-10-16 08:11:42 +02:00
Matthias
06ab51b53d Merge pull request #2381 from hroff-1902/fix/1364-2
Minor: Fix double comments for ADX
2019-10-16 06:45:31 +02:00
hroff-1902
7de1631045 Print summary in the log for machine-readable formats 2019-10-16 03:55:04 +03:00
hroff-1902
4c8411e835 Cleanup in print tabular and print-csv parts 2019-10-16 03:02:58 +03:00
hroff-1902
f348956e4c --print-csv added 2019-10-16 02:22:27 +03:00
hroff-1902
a4dfd77d23 Fix double comments for ADX 2019-10-15 22:35:14 +03:00
hroff-1902
89e0c76a3f Add --print-json and -1/--one-column options 2019-10-15 22:31:23 +03:00
Matthias
abc504412a Merge pull request #2378 from freqtrade/fix/1364
Use crossed() in sample strategy
2019-10-15 21:22:19 +02:00
hroff-1902
36d5bb6f99 Adjust ADX placement in the comments 2019-10-15 21:11:41 +03:00
hroff-1902
ad89d19955 Print list in the human-readable format 2019-10-15 21:07:01 +03:00
Matthias
e6e35c2584 Switch samplestrategy from ADX to RSI 2019-10-15 19:45:01 +02:00
Matthias
ace70510f3 Wording fixes 2019-10-15 14:50:51 +02:00
Matthias
a320d4ccba Don't sell with 0 profit in samplestrategy 2019-10-14 20:42:08 +02:00
Matthias
790e6146f5 Use crossed() in sample strategy 2019-10-14 20:17:40 +02:00
Matthias
96bd5a6dc1 Merge pull request #2375 from freqtrade/dependabot/pip/develop/sqlalchemy-1.3.10
Bump sqlalchemy from 1.3.9 to 1.3.10
2019-10-14 19:30:51 +02:00
dependabot-preview[bot]
f5d8741832 Bump sqlalchemy from 1.3.9 to 1.3.10
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.9 to 1.3.10.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-14 15:19:50 +00:00
Matthias
effcc988c1 Merge pull request #2376 from freqtrade/dependabot/pip/develop/jsonschema-3.1.1
Bump jsonschema from 3.0.2 to 3.1.1
2019-10-14 17:18:41 +02:00
Matthias
7ac3fbfdc9 Merge pull request #2374 from freqtrade/dependabot/pip/develop/ccxt-1.18.1260
Bump ccxt from 1.18.1225 to 1.18.1260
2019-10-14 17:18:28 +02:00
dependabot-preview[bot]
4c4134a272 Bump jsonschema from 3.0.2 to 3.1.1
Bumps [jsonschema](https://github.com/Julian/jsonschema) from 3.0.2 to 3.1.1.
- [Release notes](https://github.com/Julian/jsonschema/releases)
- [Changelog](https://github.com/Julian/jsonschema/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/Julian/jsonschema/compare/v3.0.2...v3.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-14 14:41:00 +00:00
dependabot-preview[bot]
b2682bcbf5 Bump ccxt from 1.18.1225 to 1.18.1260
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1225 to 1.18.1260.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1225...1.18.1260)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-14 14:40:20 +00:00
hroff-1902
4111734637 Add 'Is pair' in the list-markets tabular output 2019-10-14 13:48:33 +03:00
hroff-1902
6e27c47dee Handle properly exchanges with no active flag set for markets 2019-10-14 13:32:39 +03:00
hroff-1902
18cc3539ca Merge pull request #2373 from freqtrade/binanceus_support
Load correct exchange class for binanceus
2019-10-14 12:47:07 +03:00
Matthias
76ad5bea0e Load correct exchange class
closes #2371
2019-10-14 11:36:42 +02:00
Matthias
13e80e449c cleanup and better docstring 2019-10-14 06:22:10 +02:00
Matthias
023eb19615 Add documentation for --dl-trades 2019-10-13 19:39:07 +02:00
Matthias
3e4617be37 add pandas-based converter-functions 2019-10-13 19:25:16 +02:00
Matthias
ed9ec402fd Add test for trades_ohlcv 2019-10-13 16:04:40 +02:00
Matthias
56de81a1f9 Add some test data 2019-10-13 16:03:35 +02:00
Matthias
ccb41d1ef9 Add tests for test_download_trades_history 2019-10-13 13:15:22 +02:00
Matthias
2374cda8d0 Cleanup and tests for refresh_backtest_trades 2019-10-13 13:15:22 +02:00
Matthias
1b7a09c184 Add test for utils --dl-trades 2019-10-13 13:15:22 +02:00
Matthias
37925e7f6c Add --dl-trades cli flag 2019-10-13 13:15:22 +02:00
Matthias
762ae3a598 Extend tests 2019-10-13 13:15:22 +02:00
Matthias
9f8a2acf46 Extend test-cases to 5 trades 2019-10-13 13:15:22 +02:00
Matthias
4fdec9d6e5 Test id-based pagination 2019-10-13 13:15:22 +02:00
Matthias
640d58eb13 Remove unneeded checks 2019-10-13 13:15:22 +02:00
Matthias
fa8c61382b Remove unneeded exception handlers 2019-10-13 13:15:22 +02:00
Matthias
b6ac898f8f Add test for exception handler 2019-10-13 13:15:22 +02:00
Matthias
57bcff1964 Test get_historic_trades 2019-10-13 13:15:22 +02:00
Matthias
939a87ed2e Add test for fetch_trades 2019-10-13 13:15:22 +02:00
Matthias
16d6914b15 Add test to cover missing line 2019-10-13 13:15:22 +02:00
Matthias
05e473642b Small adjustments to get_trade_history 2019-10-13 13:15:22 +02:00
Matthias
0d592f6c55 Refactor trade downloading to handle exceptions only once 2019-10-13 13:15:22 +02:00
Matthias
476adf872a Add conversion from trades to ohlcv at different intervals 2019-10-13 13:15:22 +02:00
Matthias
9584629f50 Rename argument from dl_path to datadir 2019-10-13 13:15:22 +02:00
Matthias
c1c49183b5 Call new method based on condition 2019-10-13 13:15:22 +02:00
Matthias
8069cd6689 add refresh_trades_ method 2019-10-13 13:15:22 +02:00
Matthias
1f79ca9539 Remove duplicate check 2019-10-13 13:15:22 +02:00
Matthias
1d8fc97053 Fix duplicate trade error, rename some methods 2019-10-13 13:15:22 +02:00
Matthias
19f3669fbd add docstring 2019-10-13 13:15:22 +02:00
Matthias
06024b0ab0 Fix zipfile handling 2019-10-13 13:15:22 +02:00
Matthias
6e952a0aa8 Capture downloaded data 2019-10-13 13:15:22 +02:00
Matthias
57dee794d1 Fix end-reached for id-based trade-download 2019-10-13 13:15:22 +02:00
Matthias
2c0bb71a6e Add download_trades_history() 2019-10-13 13:15:22 +02:00
Matthias
ab8f638e44 Move id/time detection to get_historic_trades method 2019-10-13 13:15:22 +02:00
Matthias
d250b67f33 Add load/store trades data 2019-10-13 13:15:22 +02:00
Matthias
42b8241541 use gz to save / load trades data 2019-10-13 13:15:22 +02:00
Matthias
6cc98c1ea9 Fix tests 2019-10-13 13:15:22 +02:00
Matthias
77c367ad1d First draft of async get_trade methods 2019-10-13 13:15:22 +02:00
Matthias
26b3148904 Add build_ohlcv wrapper 2019-10-13 13:15:22 +02:00
Matthias
27dc9ca799 Add trades_pagination attributes 2019-10-13 13:15:22 +02:00
Matthias
63e87ef85b Add pair_trades_filename 2019-10-13 13:15:22 +02:00
Matthias
6697b677dc Add test for test_data_filename 2019-10-13 13:15:22 +02:00
Matthias
baad1a5166 Explain _params element 2019-10-13 13:15:22 +02:00
hroff-1902
7cf7982565 Add list-pairs and list-markets subcommands 2019-10-13 13:12:20 +03:00
Matthias
f3f6e9d365 Allow skipping of exchange validation 2019-10-13 10:33:22 +02:00
hroff-1902
4228137dff Merge pull request #2366 from freqtrade/interface_noconf
Interface options should not use config
2019-10-13 11:04:51 +03:00
Matthias
3c8d27d098 remove correct comment ... 2019-10-13 09:54:03 +02:00
hroff-1902
4c1705fb1e No specific handling for trailing_stop_positive 2019-10-11 22:59:13 +03:00
hroff-1902
31389b38f1 Merge pull request #2361 from freqtrade/dataprovider_tests
Add tests for orderbook and market in dataprovider
2019-10-11 22:15:21 +03:00
Matthias
ff7a3cc885 remove last occurance of config. from stop_loss_reached 2019-10-11 09:05:21 +02:00
Matthias
4d1488498c stoploss_reached should not use config 2019-10-11 08:55:31 +02:00
Matthias
10a22e7872 Merge pull request #2365 from GrilledChickenThighs/develop
Updated Rest API Docs
2019-10-11 06:42:41 +02:00
Paul D. Mendes
e72b6a440b Updated Rest API Docs 2019-10-10 20:37:25 +00:00
Matthias
b5ca4b7f35 Merge pull request #2364 from hroff-1902/list-diff-helpstrings-2
Fix helpstring which shared between the list-exchanges and list-timeframes subcommands
2019-10-10 19:57:19 +02:00
hroff-1902
c49f4b73dd Fix helpstring 2019-10-10 20:44:24 +03:00
Matthias
80cbf08a58 Merge pull request #2219 from ahonnecke/docker-compose
Make local development start up faster and easier by leveraging docker-compose
2019-10-10 19:38:40 +02:00
Matthias
5e23cc719d Add tests for orderbook and market in dataprovider 2019-10-10 19:38:01 +02:00
Matthias
0680fe2a1a fix path to tests 2019-10-10 19:28:11 +02:00
Matthias
bba5f54722 Merge pull request #2335 from hroff-1902/dataprovider-market
Allow to use market data in the strategies
2019-10-10 16:54:04 +02:00
Matthias
85c4546333 Merge pull request #2343 from hroff-1902/move-experimental
Move experimental settings to ask_strategy
2019-10-10 16:08:11 +02:00
hroff-1902
23b5c0e833 Improve tests for handling deprecated settings 2019-10-09 18:25:57 +03:00
hroff-1902
cdd1bc425b Fix typo 2019-10-09 03:12:30 +03:00
hroff-1902
2a9c06c40f Test added 2019-10-09 02:44:04 +03:00
hroff-1902
434e0234c5 Add handling deprecated settings 2019-10-09 02:43:06 +03:00
hroff-1902
caf415dc97 Merge pull request #2355 from freqtrade/hroff-1902-patch-1
Fix minor typos in the docs
2019-10-08 22:32:39 +03:00
hroff-1902
e9337bf56e Merge pull request #2356 from freqtrade/tests_history_pathlib
[minor] Don't use os.path in test_history
2019-10-08 22:31:43 +03:00
Matthias
bcd02a871f Fix beeing again ... 2019-10-08 21:16:35 +02:00
Matthias
e1c14bc86c Don't use os.path in test_history 2019-10-08 21:10:43 +02:00
hroff-1902
69c23c00e0 Fix minor typos in the docs 2019-10-08 22:07:38 +03:00
hroff-1902
1e19d7e463 Merge pull request #2354 from freqtrade/remove_underline
remove underline from docs style
2019-10-08 21:54:03 +03:00
Matthias
5e0391aa2b Merge pull request #2332 from hroff-1902/freqtradebot-refactor
Freqtradebot refactoring
2019-10-08 19:44:08 +02:00
Matthias
2e91ee3849 remove underline from docs style 2019-10-08 19:41:18 +02:00
Matthias
9b32d617db Merge pull request #2353 from freqtrade/docs_versions
Add versions to doc theme
2019-10-08 19:39:31 +02:00
hroff-1902
2ec8376af9 Merge pull request #2342 from freqtrade/fix/negativeroi
Don't have backtest sells outside of a candle
2019-10-08 11:19:34 +03:00
Matthias
86ef32318c Add versions to doc theme 2019-10-08 06:21:05 +02:00
hroff-1902
4d062d41cb Improve comments in the SampleStrategy; set use_sell_signal = True 2019-10-08 01:07:22 +03:00
hroff-1902
e78e42339d Improve docs wordings 2019-10-08 00:58:25 +03:00
hroff-1902
057ab1b7a6 Remove unnecessary comments 2019-10-08 00:50:47 +03:00
hroff-1902
613300c61d Add short description of the market() method into docs 2019-10-08 00:38:20 +03:00
Matthias
7d1f66ccf8 Merge pull request #2341 from hroff-1902/indicator-helper
Remove indicator_helpers.py and test
2019-10-07 19:36:09 +02:00
Matthias
a7418449f9 Merge pull request #2346 from freqtrade/dependabot/pip/develop/pytest-cov-2.8.1
Bump pytest-cov from 2.7.1 to 2.8.1
2019-10-07 19:31:21 +02:00
Matthias
d68e6f8362 Merge pull request #2347 from freqtrade/dependabot/pip/develop/joblib-0.14.0
Bump joblib from 0.13.2 to 0.14.0
2019-10-07 19:18:53 +02:00
dependabot-preview[bot]
c34ce15b14 Bump pytest-cov from 2.7.1 to 2.8.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 2.7.1 to 2.8.1.
- [Release notes](https://github.com/pytest-dev/pytest-cov/releases)
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v2.7.1...v2.8.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 15:16:39 +00:00
Matthias
1350d2cd1a Merge pull request #2349 from freqtrade/dependabot/pip/develop/sqlalchemy-1.3.9
Bump sqlalchemy from 1.3.8 to 1.3.9
2019-10-07 17:14:56 +02:00
Matthias
9db2aca791 Merge pull request #2348 from freqtrade/dependabot/pip/develop/pytest-mock-1.11.1
Bump pytest-mock from 1.11.0 to 1.11.1
2019-10-07 17:14:43 +02:00
dependabot-preview[bot]
80d58b7930 Bump pytest-mock from 1.11.0 to 1.11.1
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/v1.11.0...v1.11.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 13:07:56 +00:00
dependabot-preview[bot]
568ecc201a Bump sqlalchemy from 1.3.8 to 1.3.9
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.8 to 1.3.9.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 13:06:59 +00:00
Matthias
cad7ed5570 Merge pull request #2350 from freqtrade/dependabot/pip/develop/pytest-5.2.1
Bump pytest from 5.2.0 to 5.2.1
2019-10-07 15:06:52 +02:00
Matthias
57bb9281f6 Merge pull request #2351 from freqtrade/dependabot/pip/develop/mkdocs-material-4.4.3
Bump mkdocs-material from 4.4.2 to 4.4.3
2019-10-07 15:05:48 +02:00
Matthias
5e53e9bcaa Merge pull request #2352 from freqtrade/dependabot/pip/develop/ccxt-1.18.1225
Bump ccxt from 1.18.1208 to 1.18.1225
2019-10-07 15:05:34 +02:00
hroff-1902
edfbb56749 Merge pull request #2344 from freqtrade/backtest_nofees
Backtest no fees / custom fees
2019-10-07 13:30:20 +03:00
dependabot-preview[bot]
652a04ac70 Bump ccxt from 1.18.1208 to 1.18.1225
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1208 to 1.18.1225.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1208...1.18.1225)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 10:24:53 +00:00
dependabot-preview[bot]
5e9ab3e261 Bump mkdocs-material from 4.4.2 to 4.4.3
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.4.2 to 4.4.3.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.4.2...4.4.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 10:24:29 +00:00
dependabot-preview[bot]
be6fd3af9a Bump pytest from 5.2.0 to 5.2.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.2.0 to 5.2.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.2.0...5.2.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 10:24:06 +00:00
dependabot-preview[bot]
e272cd485c Bump joblib from 0.13.2 to 0.14.0
Bumps [joblib](https://github.com/joblib/joblib) from 0.13.2 to 0.14.0.
- [Release notes](https://github.com/joblib/joblib/releases)
- [Changelog](https://github.com/joblib/joblib/blob/master/CHANGES.rst)
- [Commits](https://github.com/joblib/joblib/compare/0.13.2...0.14.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-07 10:22:59 +00:00
Matthias
f27528538d Merge pull request #2345 from hroff-1902/minor-data-history
Cleanup in data.history
2019-10-07 07:05:24 +02:00
Matthias
ad35a3d7ab Small wording improvements 2019-10-07 07:02:43 +02:00
hroff-1902
211b9cbe04 Cleanup in data.history 2019-10-06 18:35:09 +03:00
hroff-1902
946b8c29d7 Merge pull request #2317 from hroff-1902/list-timeframes
Add list-timeframes subcommand
2019-10-06 16:28:15 +03:00
Matthias
33940ae66b Use different keys and values 2019-10-06 14:33:23 +02:00
Matthias
d2589c4415 Make test exchange-independent 2019-10-06 10:32:19 +02:00
Matthias
22733e44bf Add tests for --fee 2019-10-05 15:34:31 +02:00
Matthias
82d4051a39 Add --fee to documentation 2019-10-05 15:33:44 +02:00
Matthias
0664a8c0e6 add --fee to change fees to other values 2019-10-05 15:29:00 +02:00
hroff-1902
9b23376415 Move experimental settings to ask_strategy 2019-10-05 13:29:59 +03:00
Matthias
553a1b90ba Merge pull request #2297 from jraviotta/scattergl
Enhancements to BB plotting
2019-10-05 11:01:10 +02:00
Matthias
7ea9da9605 Fix #2277 2019-10-05 10:54:28 +02:00
Matthias
9b98e608e6 Add testcase for negative ROI after certain period 2019-10-05 10:52:57 +02:00
Matthias
885edc9768 Allow multiple ROI in detail-backtest tests 2019-10-05 10:52:49 +02:00
hroff-1902
e1b8485b51 Remove indicator_helpers.py and test 2019-10-05 11:51:27 +03:00
Matthias
764a35d035 Remove scattergl and fix tests 2019-10-05 10:32:42 +02:00
hroff-1902
e93bbd3831 Merge pull request #2340 from freqtrade/cleanup_legacystrategy
[minor] Cleanup legacy strategy
2019-10-05 11:12:36 +03:00
hroff-1902
8938c95e00 Merge pull request #2339 from freqtrade/fix_doc
fix documentation error
2019-10-05 11:04:49 +03:00
Matthias
00ab6f572a Cleanup legacy strategy
it's just a test and does not need the commented elements
2019-10-05 10:01:38 +02:00
Matthias
73e9cbdea1 Fix #2338 2019-10-05 09:56:01 +02:00
Matthias
78381e9e7b Improve test to test full sell cycle 2019-10-04 14:47:37 +02:00
hroff-1902
75252b6251 Docstrings improved 2019-10-04 02:32:48 +03:00
hroff-1902
f95b0ccdab Tests added 2019-10-04 02:01:44 +03:00
Matthias
38f184e50d Update test to not mock stoploss_on_exchange 2019-10-03 06:54:15 +02:00
Matthias
1f4e5b17b7 Add basic test for execute sells_multiple logic 2019-10-03 06:37:25 +02:00
Matthias
9ee7e28ef8 Clean up some mocks 2019-10-03 06:23:58 +02:00
hroff-1902
3ac5b91899 Add market() method to dataprovider 2019-10-03 02:58:45 +03:00
hroff-1902
4b29c4cdbf Test for handling closed trade adjusted 2019-10-02 19:08:49 +03:00
hroff-1902
89729aefe8 Fix and improve process_maybe_execute_sells() 2019-10-02 18:47:45 +03:00
hroff-1902
15aae8a58c Tests adjusted 2019-10-02 13:51:32 +03:00
hroff-1902
096c69dc4f Refactor Freqtradebot 2019-10-02 13:51:32 +03:00
hroff-1902
2c0d2c1532 Merge pull request #2331 from freqtrade/testdata_cleanup
Remove unused test-data
2019-10-02 13:25:21 +03:00
Matthias
eca8ddabe9 Remove unused test-data 2019-10-02 11:05:08 +02:00
hroff-1902
c57d5ef1cd Added short descriptions and examples in utils.md 2019-10-01 21:12:52 +03:00
Matthias
6bbc0eefed Merge pull request #2329 from freqtrade/release_2019.9
Release 2019.9
2019-10-01 20:04:18 +02:00
Matthias
8c5b299449 Merge pull request #2282 from freqtrade/jupyter_nbconvert
add plotting documentation to jupyter notebook
2019-10-01 19:58:20 +02:00
Matthias
9806699592 version bump 2019.9 2019-10-01 19:35:47 +02:00
hroff-1902
f2e878d9ec Update helpstring for list-exchanges 2019-10-01 16:57:35 +03:00
Matthias
628c4c996a Merge pull request #2327 from hroff-1902/enhance-list-exchanges2
Add --all option to list-exchanges
2019-10-01 06:52:27 +02:00
Matthias
642d20b2f7 Merge pull request #2324 from freqtrade/dependabot/pip/develop/mypy-0.730
Bump mypy from 0.720 to 0.730
2019-10-01 06:49:11 +02:00
hroff-1902
f6a88c6e9b Tests adjusted 2019-10-01 00:33:54 +03:00
hroff-1902
d1fa5f307b Add --all option to list-exchanges 2019-10-01 00:33:33 +03:00
hroff-1902
cd0e813a85 Docs adjusted, utils.md added 2019-09-30 21:36:52 +03:00
Matthias
dc47a391da Move ignore to corrct line for mypy 730 2019-09-30 19:32:46 +02:00
Matthias
9f94678478 Merge pull request #2319 from hroff-1902/bad-exchanges
Add exchanges to the list of bad exchanges
2019-09-30 19:29:06 +02:00
dependabot-preview[bot]
04fea69a28 Bump mypy from 0.720 to 0.730
Bumps [mypy](https://github.com/python/mypy) from 0.720 to 0.730.
- [Release notes](https://github.com/python/mypy/releases)
- [Commits](https://github.com/python/mypy/compare/v0.720...v0.730)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 17:26:03 +00:00
Matthias
d6a8821596 Merge pull request #2322 from freqtrade/dependabot/pip/develop/pytest-5.2.0
Bump pytest from 5.1.3 to 5.2.0
2019-09-30 19:24:45 +02:00
hroff-1902
7617dd5029 Add separate message for hitbtc exchange 2019-09-30 20:01:55 +03:00
hroff-1902
e9d9df3473 Merge branch 'develop' into list-timeframes 2019-09-30 18:58:25 +03:00
hroff-1902
b6ee3d99b1 Merge pull request #2320 from freqtrade/config_no_allowed
args - Add config no allowed list to skip loading config.json
2019-09-30 18:32:29 +03:00
Matthias
9a2bd83827 Merge pull request #2323 from freqtrade/dependabot/pip/develop/tabulate-0.8.5
Bump tabulate from 0.8.3 to 0.8.5
2019-09-30 17:20:16 +02:00
dependabot-preview[bot]
f359f869ab Bump pytest from 5.1.3 to 5.2.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.3 to 5.2.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.1.3...5.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 15:20:07 +00:00
Matthias
d6fba7c2f3 Merge pull request #2321 from freqtrade/dependabot/pip/develop/urllib3-1.25.6
Bump urllib3 from 1.25.5 to 1.25.6
2019-09-30 17:19:44 +02:00
Matthias
498bcf213f Merge pull request #2325 from freqtrade/dependabot/pip/develop/pytest-mock-1.11.0
Bump pytest-mock from 1.10.4 to 1.11.0
2019-09-30 17:18:49 +02:00
Matthias
33efb7ace6 Merge pull request #2326 from freqtrade/dependabot/pip/develop/ccxt-1.18.1208
Bump ccxt from 1.18.1180 to 1.18.1208
2019-09-30 17:18:36 +02:00
dependabot-preview[bot]
d74ca78bd8 Bump ccxt from 1.18.1180 to 1.18.1208
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1180 to 1.18.1208.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1180...1.18.1208)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 10:21:37 +00:00
dependabot-preview[bot]
3c91ba134f Bump pytest-mock from 1.10.4 to 1.11.0
Bumps [pytest-mock](https://github.com/pytest-dev/pytest-mock) from 1.10.4 to 1.11.0.
- [Release notes](https://github.com/pytest-dev/pytest-mock/releases)
- [Changelog](https://github.com/pytest-dev/pytest-mock/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-mock/compare/1.10.4...v1.11.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 10:20:47 +00:00
dependabot-preview[bot]
9a83d84109 Bump tabulate from 0.8.3 to 0.8.5
Bumps [tabulate](https://github.com/astanin/python-tabulate) from 0.8.3 to 0.8.5.
- [Release notes](https://github.com/astanin/python-tabulate/releases)
- [Changelog](https://github.com/astanin/python-tabulate/blob/master/CHANGELOG)
- [Commits](https://github.com/astanin/python-tabulate/commits/v0.8.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 10:20:05 +00:00
dependabot-preview[bot]
8ae4018e4d Bump urllib3 from 1.25.5 to 1.25.6
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.5 to 1.25.6.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.5...1.25.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-30 10:19:37 +00:00
Matthias
739901b606 Add test for this behaviour 2019-09-30 09:48:00 +02:00
Matthias
03b5be91f7 some commands should not have config at all 2019-09-30 09:47:52 +02:00
hroff-1902
272c977d08 Add exchanges to the list of bad exchanges 2019-09-30 03:55:55 +03:00
hroff-1902
75446d8195 Refactor list-timeframes command with the use of the Exchange class methods 2019-09-29 23:18:04 +03:00
hroff-1902
448b09d7b6 Add list-timeframes subcommand 2019-09-29 11:54:20 +03:00
hroff-1902
704fea616b Merge pull request #2316 from freqtrade/no_main_py
Don't use main.py as entry point in documentation
2019-09-29 11:33:40 +03:00
Matthias
23665c7731 Don't use main.py as entry point in documentation 2019-09-29 10:25:47 +02:00
Matthias
4025ec9900 Merge pull request #2315 from freqtrade/hroff-1902-patch-1
Minor: Change the default stoploss space
2019-09-29 10:17:07 +02:00
hroff-1902
6a397f579e Add description of usage 2019-09-29 00:43:27 +03:00
hroff-1902
c31f118d0c Merge pull request #2307 from freqtrade/rounding
Don't compare floats when updating fees
2019-09-28 12:39:27 +03:00
hroff-1902
2f005d6be9 Align example of ROI in the docs 2019-09-28 11:56:19 +03:00
hroff-1902
45f5394d79 Align example in the docs 2019-09-28 11:54:26 +03:00
hroff-1902
7e214d8e4c minor: change default stoploss space 2019-09-28 11:50:15 +03:00
Matthias
ed10048394 Merge pull request #2308 from hroff-1902/hyperopt-config
Allow use of config in custom hyperopt methods
2019-09-28 10:36:46 +02:00
Matthias
43f2ef226c Change rel_tol to abs_tol to avoid surprises with high priced pairs 2019-09-28 10:30:12 +02:00
Matthias
42b5a0977e fix failing test 2019-09-28 10:14:38 +02:00
Matthias
3b1252207d Merge pull request #2314 from hroff-1902/fix-2259
Shorten the default hyperopt stoploss space
2019-09-28 10:09:08 +02:00
hroff-1902
4ac53f1549 Shorten the default hyperopt stoploss space 2019-09-28 04:13:53 +03:00
hroff-1902
21b807aa85 Merge pull request #2310 from freqtrade/slack_link
Update slack link
2019-09-26 21:12:40 +03:00
Matthias
28e0398c68 Merge pull request #2280 from freqtrade/backtest_docs
Improve backtesting documentation
2019-09-26 19:37:34 +02:00
Matthias
637ec60644 Update slack link 2019-09-26 19:36:09 +02:00
Ashton Honnecke
11bb7e520c use .develop dockerfile, move docs to develop.md, add freqtrade_bash 2019-09-26 09:22:49 -06:00
Matthias
6ba9316e15 Merge branch 'develop' into backtest_docs 2019-09-26 11:04:01 +02:00
Matthias
60e3e626e4 Improve timerange section of the docs 2019-09-26 11:00:26 +02:00
hroff-1902
9db915853a Allow use of config in custom hyperopt methods 2019-09-26 11:59:21 +03:00
Matthias
5237723f22 Merge pull request #2303 from freqtrade/feat/hyperopt_optional_install
Optional hyperopt dependency installation
2019-09-26 09:42:16 +02:00
Matthias
eb07f1fee9 Fix typo 2019-09-26 09:31:31 +02:00
Matthias
8d92f8b362 Compare floats via isclose instead of == 2019-09-26 07:18:00 +02:00
Matthias
49f0a72121 Add test for rounding error on fload aggregation 2019-09-26 07:17:54 +02:00
Matthias
5978b7bb93 Add explicit test for halfbought fee adjustment 2019-09-26 07:17:49 +02:00
Matthias
e09408f9b7 Merge pull request #2306 from freqtrade/hroff-1902-patch-1
Minor: fix typo in comment
2019-09-26 06:15:40 +02:00
Jonathan Raviotta
83e596c06f chart styling 2019-09-25 23:09:50 -04:00
hroff-1902
0268bfdbd4 Minor: fix typo in comment
Minor cosmetics. typo caught.
2019-09-26 02:04:48 +03:00
Matthias
b994f5c273 Merge pull request #2294 from hroff-1902/fix-skopt-memory3
Fix skopt memory exhaustion
2019-09-25 19:55:27 +02:00
Matthias
e9de088209 Add import-fails code as a fixture 2019-09-25 11:55:24 +02:00
Matthias
d05db077e2 Update PI install documentation and dockerfile 2019-09-25 11:40:34 +02:00
Matthias
d2f2473070 install hyperopt seperately ([hyperopt]) 2019-09-25 11:40:34 +02:00
Matthias
47b6b56566 Reorg dependencies to have hyperopt seperated 2019-09-25 11:40:34 +02:00
Matthias
27cc73f47e Dynamically import hyperopt modules 2019-09-25 11:40:34 +02:00
Matthias
cc91ccad3e Improve documentation wording 2019-09-25 06:26:28 +02:00
Matthias
0102413f58 Merge pull request #2301 from hroff-1902/fix-hyperopt-position-stacking
Fix hyperopt position stacking
2019-09-25 06:22:39 +02:00
hroff-1902
665e0570ae Fix hyperopt position stacking 2019-09-25 03:41:22 +03:00
Jonathan Raviotta
9391c27b80 Enhancements to BB plotting 2019-09-24 20:07:54 -04:00
hroff-1902
a75fb3d4be Merge pull request #2197 from freqtrade/implement_version_dev
Apply dynamic versioning to develop
2019-09-24 21:20:49 +03:00
hroff-1902
2d86510acf Merge pull request #2298 from freqtrade/fix/failing_tests
[minor] Fix tests that fail when config.json is present
2019-09-24 21:19:45 +03:00
hroff-1902
5c3b14069e Merge pull request #2285 from freqtrade/doc/configuration
Improve configuration documentation
2019-09-24 21:18:30 +03:00
Ashton Honnecke
fe483ad011 Don't use the develop dockerfile for local 2019-09-24 09:03:44 -06:00
Ashton Honnecke
0ce070acac Merge branch 'develop' of https://github.com/freqtrade/freqtrade into docker-compose 2019-09-24 08:59:22 -06:00
Matthias
6c0a1fc42c Fix tests that fail when config.json is present 2019-09-24 11:07:12 +02:00
hroff-1902
d066ab2620 Merge pull request #2278 from freqtrade/remove_refresh
Remove refresh-pairs-cached
2019-09-24 09:07:25 +03:00
hroff-1902
3a5bd4c03e Merge pull request #2284 from freqtrade/fix/download_errors
Gracefully handle download-data startup errors
2019-09-24 09:06:00 +03:00
Matthias
93b2621651 Add format description for pairs.json file 2019-09-24 07:07:06 +02:00
Matthias
6aa1ec2a4c Some small restructuring 2019-09-24 07:05:30 +02:00
Matthias
cc9fc41318 Rename section to data-downloading, implement some feedback 2019-09-24 06:56:31 +02:00
Matthias
fe40636ae1 Improve wordings 2019-09-24 06:42:44 +02:00
Matthias
577b1fd965 Improve documentation wording 2019-09-24 06:39:00 +02:00
Matthias
0f97a999fb Improve wording 2019-09-24 06:35:41 +02:00
Ashton Honnecke
cb6e136893 how to exec 2019-09-23 13:35:18 -06:00
Ashton Honnecke
5c3fb4d5b3 docs for running docker-compose locally 2019-09-23 13:30:55 -06:00
Ashton Honnecke
7c6921c743 pr feedback regarding docker-compose naming 2019-09-23 13:17:20 -06:00
Matthias
22af7f7881 Merge pull request #2291 from freqtrade/dependabot/pip/develop/ccxt-1.18.1180
Bump ccxt from 1.18.1159 to 1.18.1180
2019-09-23 19:50:55 +02:00
hroff-1902
6ffb8b7a70 Fix wordings in comment 2019-09-23 13:25:31 +03:00
dependabot-preview[bot]
95e725c2b6 Bump ccxt from 1.18.1159 to 1.18.1180
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1159 to 1.18.1180.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1159...1.18.1180)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-23 10:15:28 +00:00
Matthias
b18ad7a834 Merge pull request #2290 from freqtrade/dependabot/pip/develop/urllib3-1.25.5
Bump urllib3 from 1.25.3 to 1.25.5
2019-09-23 12:14:50 +02:00
Matthias
cfa76fa600 Merge pull request #2292 from freqtrade/dependabot/pip/develop/pytest-5.1.3
Bump pytest from 5.1.2 to 5.1.3
2019-09-23 12:14:32 +02:00
Matthias
d226fff111 Merge pull request #2293 from freqtrade/dependabot/pip/develop/python-telegram-bot-12.1.1
Bump python-telegram-bot from 12.1.0 to 12.1.1
2019-09-23 12:14:14 +02:00
hroff-1902
0c6164df7e Fix memory exhaustion in skopt models list 2019-09-23 13:03:43 +03:00
dependabot-preview[bot]
d8bc350445 Bump python-telegram-bot from 12.1.0 to 12.1.1
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.1.0 to 12.1.1.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.1.0...v12.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-23 08:55:31 +00:00
dependabot-preview[bot]
242ff26e21 Bump pytest from 5.1.2 to 5.1.3
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.2 to 5.1.3.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.1.2...5.1.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-23 08:54:57 +00:00
dependabot-preview[bot]
ab0adabd39 Bump urllib3 from 1.25.3 to 1.25.5
Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.25.3 to 1.25.5.
- [Release notes](https://github.com/urllib3/urllib3/releases)
- [Changelog](https://github.com/urllib3/urllib3/blob/master/CHANGES.rst)
- [Commits](https://github.com/urllib3/urllib3/compare/1.25.3...1.25.5)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-23 08:54:11 +00:00
Matthias
ba4db0da49 Improve configuration documentation 2019-09-21 13:16:53 +02:00
Matthias
7aa42f8868 Fail download-data gracefully if no pairs-file exists 2019-09-21 12:53:47 +02:00
Matthias
3245ebccd4 Fix problme when no exchange is given to download-data 2019-09-21 11:25:27 +02:00
Matthias
359b0ba088 Add samples for plotting to jupyter documentation 2019-09-21 10:57:34 +02:00
Matthias
5234f8bf28 Update jupyter notebook slightly 2019-09-21 10:42:51 +02:00
Matthias
9a3bad291a Automatically generate documentation from jupyter notebook 2019-09-21 10:27:43 +02:00
Matthias
b1a3e213ae Improve backtesting docs 2019-09-21 10:13:00 +02:00
Matthias
2fcddfc866 Clarify updating existing data 2019-09-20 20:29:26 +02:00
Matthias
313091eb1c some more refresh_pairs cleanups 2019-09-20 20:22:51 +02:00
Matthias
508a35fc20 Update comment as to why certain points have not been removed 2019-09-20 20:20:16 +02:00
Matthias
9cedbc1345 Cleanup history.py and update documentation 2019-09-20 20:16:49 +02:00
Matthias
e66fa1cec6 Adjust tests to not use --refresh-pairs 2019-09-20 20:16:12 +02:00
Matthias
1cd8ed0c1a Remove --refresh-pairs 2019-09-20 20:02:07 +02:00
hroff-1902
74a0f44230 Merge pull request #2276 from freqtrade/keep_original_config
Allow easy printing of loaded configuration
2019-09-20 20:59:33 +03:00
hroff-1902
dc825c249c Make flake happy 2019-09-20 20:51:31 +03:00
Matthias
15a4df4c49 Mock create_datadir to make sure no folders are left behind 2019-09-20 08:34:18 +02:00
Matthias
f0cf8d6a81 Allow easy printing of loaded configuration
(beforechanging types and applying defaults)
2019-09-20 07:23:32 +02:00
Matthias
7fff1f5ce1 Merge pull request #2274 from freqtrade/hroff-1902-patch-1
Manual bump to ccxt 1.18.1159
2019-09-19 12:38:15 +02:00
hroff-1902
c625058f41 Merge pull request #2275 from hroff-1902/backtest-cleanup3
minor: Cleanup in backtesting
2019-09-19 11:59:07 +03:00
hroff-1902
50b4563912 Tests adjusted 2019-09-18 22:57:37 +03:00
hroff-1902
69f29e8907 minor: Cleanup for backtesting 2019-09-18 22:57:17 +03:00
hroff-1902
ee6ad51a44 Manual bump to ccxt 1.18.1159
(support for binance.us)
2019-09-18 22:41:25 +03:00
hroff-1902
e8657d2444 Merge pull request #2272 from freqtrade/setup_names
Change package author to "freqtrade team"
2019-09-18 13:11:21 +03:00
Matthias
a42000e1dd Change package author to "freqtrade team" 2019-09-18 11:36:16 +02:00
hroff-1902
c3e19507bf Merge pull request #2268 from gaugau3000/hyperopt_test_use_case
Hyperopt test use case
2019-09-18 01:01:41 +03:00
Matthias
27238d97d5 Merge pull request #2269 from hroff-1902/hyperopt-cleanup4
minor: Cleanup in hyperopt
2019-09-17 06:45:46 +02:00
hroff-1902
e9a75e57b8 test adjusted 2019-09-16 21:53:19 +03:00
hroff-1902
5cbc073dd1 minor: Cleanup hyperopt 2019-09-16 21:46:15 +03:00
Pialat
b7da02aab4 realistic fixture datas 2019-09-16 14:05:39 +02:00
Pialat
f3e3a8fcbe unused in tests 2019-09-16 14:04:10 +02:00
Matthias
44fe0478ea Merge pull request #2267 from freqtrade/dependabot/pip/develop/python-telegram-bot-12.1.0
Bump python-telegram-bot from 12.0.0 to 12.1.0
2019-09-16 11:57:13 +02:00
Matthias
9dc9bc2346 Merge pull request #2266 from freqtrade/dependabot/pip/develop/ccxt-1.18.1149
Bump ccxt from 1.18.1124 to 1.18.1149
2019-09-16 11:56:18 +02:00
dependabot-preview[bot]
9c1cce6fe2 Bump ccxt from 1.18.1124 to 1.18.1149
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1124 to 1.18.1149.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1124...1.18.1149)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-16 09:31:14 +00:00
dependabot-preview[bot]
cab394a058 Bump python-telegram-bot from 12.0.0 to 12.1.0
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 12.0.0 to 12.1.0.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v12.0.0...v12.1.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-16 09:31:10 +00:00
Matthias
9d9ace2b22 Merge pull request #2265 from freqtrade/dependabot/pip/develop/arrow-0.15.2
Bump arrow from 0.15.0 to 0.15.2
2019-09-16 11:29:55 +02:00
dependabot-preview[bot]
c2462ee87b Bump arrow from 0.15.0 to 0.15.2
Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.15.0 to 0.15.2.
- [Release notes](https://github.com/crsmithdev/arrow/releases)
- [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/crsmithdev/arrow/compare/0.15.0...0.15.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-16 08:49:41 +00:00
hroff-1902
39f41def54 Merge pull request #2261 from freqtrade/test_speedup
[minor] Test speedup
2019-09-14 11:25:00 +03:00
hroff-1902
76e45883bd Merge pull request #2253 from hroff-1902/backtesting-improve-logs
Improve logs for backtesting
2019-09-14 11:23:46 +03:00
Matthias
19ce7180be Merge pull request #2260 from freqtrade/args_vars
Configuration/Arguments refactoing (don't pass Namespace around).
2019-09-14 10:11:02 +02:00
Matthias
b00467c8ef Fix test failure 2019-09-14 10:07:23 +02:00
Matthias
2cf045c53e Remove commented indicators from DefaultStrategy 2019-09-14 10:00:59 +02:00
Matthias
e2a100c925 Directory / folder 2019-09-14 09:54:40 +02:00
hroff-1902
eda1ec652f Revert back condition for open_since in Trade.__repr__ 2019-09-13 23:00:09 +03:00
Matthias
0135784589 remove unused indicators from default_strategy 2019-09-13 19:56:58 +02:00
Matthias
5e654620b7 Use available indicators in tests where possible 2019-09-13 19:56:06 +02:00
Matthias
16b4ae8396 Document this new behaviour 2019-09-13 07:08:42 +02:00
Matthias
a5f3b68bff Allow loading of fully initialized config from jupyter notbooks 2019-09-13 07:08:22 +02:00
Matthias
f163240710 Simplify configuration init where possible 2019-09-13 07:02:36 +02:00
hroff-1902
c5f455d660 Merge pull request #2256 from freqtrade/kraken_balance
fix Kraken balance calculation
2019-09-12 23:12:55 +03:00
hroff-1902
c8d191a5c9 Adjust test 2019-09-12 22:53:54 +03:00
hroff-1902
e6ec8f9f30 Fix tests: Change condition for printing 'close' 2019-09-12 21:28:51 +03:00
Matthias
4d566e8bad Update tests to not use Namespace 2019-09-12 20:28:37 +02:00
Matthias
e6ccc1427c have Arguments return a dict instead of Namespace 2019-09-12 20:16:39 +02:00
Matthias
52b186eabe Create-userdir does not need a configuration 2019-09-12 20:14:58 +02:00
hroff-1902
67ff48ce3e Comment out noisy log messages 2019-09-12 21:01:14 +03:00
hroff-1902
045ca8739d Do not print humanized datetime in the log message 2019-09-12 20:56:00 +03:00
Matthias
64b404068f Merge pull request #2258 from hroff-1902/dont-inherit-from-object
Minor: class cosmetics
2019-09-12 15:18:42 +02:00
hroff-1902
dda513c923 Minor class cosmetics 2019-09-12 12:13:20 +03:00
Matthias
6c5eff4a7c Use List of Tuples, remove unused columns 2019-09-12 07:03:52 +02:00
Matthias
6884ad2211 Merge pull request #2254 from hroff-1902/pytest-asyncio-warnings
Eliminate asyncio warnings in tests
2019-09-12 06:34:01 +02:00
Matthias
9e4effaa14 Merge pull request #2257 from hroff-1902/dont-inherit-from-object
Cleanup: Don't inherit from object
2019-09-12 06:22:07 +02:00
hroff-1902
849d694c27 Don't inherit from object 2019-09-12 04:39:52 +03:00
hroff-1902
1d781ea9e0 Refine 'stoploss adjusted' log message 2019-09-12 02:29:47 +03:00
hroff-1902
acf3b751f0 Log sell_flag, do not log sell_type=SellType.NONE 2019-09-12 01:21:14 +03:00
hroff-1902
9bdfaf3803 Remove quotes around the pairs 2019-09-11 23:32:08 +03:00
Matthias
f8eb1cd58a Add tests for kraken balance implementation 2019-09-11 20:53:23 +02:00
Matthias
3b4bbe7a18 Implement get_balances which uses open_orders 2019-09-11 19:43:16 +02:00
hroff-1902
2bd59de002 Cleanup log_has_re regexp string 2019-09-11 10:56:02 +03:00
hroff-1902
ac413c65dc Clean up the use of patch_exchange 2019-09-11 09:52:09 +03:00
Matthias
c01953daf2 Remove kraken block 2019-09-11 06:57:58 +02:00
hroff-1902
a9ecdc7764 Use patched exchange instead 2019-09-11 00:53:35 +03:00
hroff-1902
869a5b4901 Eliminate asyncio warnings in tests 2019-09-10 13:45:30 +03:00
hroff-1902
2081d7552f Make flake happy 2019-09-10 12:37:15 +03:00
hroff-1902
e298e77319 Adjust tests 2019-09-10 10:43:15 +03:00
hroff-1902
35580b135a Improve backtesting logs 2019-09-10 10:42:45 +03:00
hroff-1902
f987e6e0f9 Merge pull request #2251 from freqtrade/telegram_fiatconvert
Telegram fiatconvert with identical currencies
2019-09-09 21:49:00 +03:00
Matthias
85f1291597 use git log to print version 2019-09-09 20:20:38 +02:00
Matthias
5ea739f943 Merge pull request #2247 from freqtrade/dependabot/pip/develop/ccxt-1.18.1124
Bump ccxt from 1.18.1115 to 1.18.1124
2019-09-09 20:02:27 +02:00
Matthias
94d2790ab5 Fix #2239 -
return float even if fiat/crypto are identical
2019-09-09 20:00:13 +02:00
Matthias
9aa7db103d Add test for failing case 2019-09-09 19:59:41 +02:00
dependabot-preview[bot]
3398f31b87 Bump ccxt from 1.18.1115 to 1.18.1124
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1115 to 1.18.1124.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1115...1.18.1124)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-09 17:40:31 +00:00
Matthias
0e39cc1187 Merge pull request #2249 from freqtrade/dependabot/pip/develop/arrow-0.15.0
Bump arrow from 0.14.6 to 0.15.0
2019-09-09 19:39:18 +02:00
Ashton Honnecke
e8e05b6876 split docker composes 2019-09-09 09:24:40 -06:00
Matthias
a218946f52 Merge pull request #2250 from freqtrade/dependabot/pip/develop/plotly-4.1.1
Bump plotly from 4.1.0 to 4.1.1
2019-09-09 11:45:07 +02:00
Matthias
2a79c1eed2 Merge pull request #2248 from freqtrade/dependabot/pip/develop/numpy-1.17.2
Bump numpy from 1.17.1 to 1.17.2
2019-09-09 11:44:23 +02:00
dependabot-preview[bot]
7dc3e67bba Bump plotly from 4.1.0 to 4.1.1
Bumps [plotly](https://github.com/plotly/plotly.py) from 4.1.0 to 4.1.1.
- [Release notes](https://github.com/plotly/plotly.py/releases)
- [Changelog](https://github.com/plotly/plotly.py/blob/master/CHANGELOG.md)
- [Commits](https://github.com/plotly/plotly.py/compare/v4.1.0...v4.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-09 08:21:55 +00:00
dependabot-preview[bot]
3c869a8032 Bump arrow from 0.14.6 to 0.15.0
Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.14.6 to 0.15.0.
- [Release notes](https://github.com/crsmithdev/arrow/releases)
- [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crsmithdev/arrow/compare/0.14.6...0.15.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-09 08:21:33 +00:00
dependabot-preview[bot]
edba5a0014 Bump numpy from 1.17.1 to 1.17.2
Bumps [numpy](https://github.com/numpy/numpy) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.17.1...v1.17.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-09 08:21:22 +00:00
Matthias
b4a0591429 Merge pull request #2221 from jraviotta/notebook
Notebook
2019-09-09 08:01:09 +02:00
Jonathan Raviotta
adbc0159ae changed more occuranes of function 2019-09-09 07:00:25 +02:00
Jonathan Raviotta
a5510d14e9 de-mangling 2019-09-09 06:58:41 +02:00
hroff-1902
9f5d4a5252 Merge pull request #2240 from freqtrade/fix/docker_release_backtest
Pass test-data to dockerized backtest
2019-09-08 21:43:59 +03:00
hroff-1902
42d2ecba68 Merge pull request #2244 from freqtrade/minor/cleanup
[minor] cleanup tests and fix excluded samplestrategy
2019-09-08 21:19:53 +03:00
hroff-1902
ceb1f91d9d Merge pull request #2243 from freqtrade/fix/plotting_failure
Fix random failure if config.json exists
2019-09-08 21:18:37 +03:00
Matthias
3430850421 don't print in tests 2019-09-08 19:47:16 +02:00
Matthias
c5726e88e8 Don't gitignore sample_strategy 2019-09-08 19:45:50 +02:00
Matthias
867a3273ce Fix random failure if config.json exists 2019-09-08 19:38:16 +02:00
Matthias
2a236db18f Pass test-data to dockerized backtest 2019-09-08 19:27:42 +02:00
Matthias
242ac4d8f4 Merge pull request #2236 from freqtrade/move_tests
Move tests to top level
2019-09-08 19:18:44 +02:00
Matthias
3e0edc7ee2 Update backtesting section about correct data used 2019-09-08 19:05:51 +02:00
Matthias
0bb1127cb6 update .gitignore
things we no longer use should not be excluded
2019-09-08 19:05:51 +02:00
Matthias
9d2c6c8de2 Fix paths in setup and travis 2019-09-08 19:05:51 +02:00
Matthias
9513115ce0 Fix paths in tests 2019-09-08 19:05:23 +02:00
Matthias
f2cbc5fb8f Fix documentation references to tests folder 2019-09-08 19:05:23 +02:00
Matthias
26d76cdb19 Adjust imports in tests to new path 2019-09-08 19:05:23 +02:00
Matthias
65a516e229 Move tests out of freqtrade module 2019-09-08 19:05:22 +02:00
hroff-1902
edda122ed0 Merge pull request #2238 from freqtrade/fix/strategyloading
[minor, important] Fix random test failures
2019-09-08 19:37:20 +03:00
hroff-1902
3044b861bd Merge pull request #2237 from freqtrade/setup_long_desc
Enhance setup.py to include long_description
2019-09-08 19:36:07 +03:00
Matthias
13932f55f5 Fix random test failures 2019-09-08 14:02:32 +02:00
hroff-1902
3d028f512e Merge pull request #2235 from hroff-1902/eliminate_import_strategy
Allow --strategy for hyperopt
2019-09-08 12:23:48 +03:00
Matthias
bb2d8fefd7 Enhance setup.py 2019-09-08 11:04:39 +02:00
hroff-1902
623e8f6984 Merge pull request #2234 from freqtrade/test_datadir_no_default
datadir should not default to freqtrade/tests/testdata
2019-09-08 04:06:12 +03:00
hroff-1902
865e0d3af9 Adjust tests: removed tests for/with import_strategy() 2019-09-08 03:30:15 +03:00
hroff-1902
45cfdbbda7 Make flake happy 2019-09-08 03:10:01 +03:00
hroff-1902
2b00a5d90a Get rid of import_strategy() 2019-09-08 02:43:02 +03:00
Matthias
bd2ecf8ce3 Add testdatadir to missed test 2019-09-07 21:13:05 +02:00
Matthias
972b8a1726 Remove defaulting to test_data folder when no datadir is present 2019-09-07 21:06:20 +02:00
Matthias
fe631ffd04 Use fixture to determine test_data_dir 2019-09-07 20:56:03 +02:00
Matthias
bde82e9654 Move make_testdata_path to conftest 2019-09-07 20:34:25 +02:00
hroff-1902
df481eb642 Merge pull request #2227 from freqtrade/fix/balance_failure
Fix RPC /balance failure
2019-09-07 00:27:20 +03:00
Matthias
6ff83abb61 Merge pull request #2220 from hroff-1902/hyperopt-simplified-interface
Allow simplified hyperopt interface
2019-09-06 19:41:48 +02:00
hroff-1902
4fdf8a75cd Adjust hyperopt tests after the merge with develop 2019-09-06 16:46:44 +03:00
hroff-1902
2e49125e87 Merge branch 'develop' into hyperopt-simplified-interface 2019-09-06 15:11:06 +03:00
hroff-1902
7e56704767 Parametrize tests for hyperopt simplified failed 2019-09-06 15:08:44 +03:00
Matthias
95b89e059a Merge pull request #2230 from freqtrade/hroff-1902-patch-1
docs: Fix table with ROI limits in hyperopt.md
2019-09-06 11:28:36 +02:00
hroff-1902
ef8386c065 Fix table with ROI limits 2019-09-06 11:55:07 +03:00
Matthias
7af445adf3 Merge pull request #2137 from hroff-1902/hyperopt-adaptive-roi-space
Hyperopt: adaptive roi_space
2019-09-06 06:26:52 +02:00
hroff-1902
ee68f743c7 Merge pull request #2217 from freqtrade/fix/plot_config
Always use config.json if it's available
2019-09-06 01:12:03 +03:00
hroff-1902
e39d911177 Improve wordings in hyperopt.md 2019-09-05 23:31:07 +03:00
Matthias
48ac37a1b8 BLock kraken trading - it's not working at the moment 2019-09-05 20:16:09 +02:00
Matthias
e8f37666ea Fix Problem when ccxt reports None as values 2019-09-05 20:02:18 +02:00
Matthias
e2e0015119 Don't rename dict ... we can use it as is 2019-09-05 20:02:01 +02:00
hroff-1902
3343b34725 Add tests for simplified hyperopt interface 2019-09-05 00:38:15 +03:00
Matthias
e107290230 Validate plot arguments 2019-09-04 19:21:58 +02:00
Matthias
1b66f01ec0 Always use config.json if it's available 2019-09-04 19:21:58 +02:00
Matthias
f9c7a2cacb Merge pull request #2224 from freqtrade/args/aftersubcommand
Arguments - remove unused arguments
2019-09-04 19:19:31 +02:00
Matthias
5ce63cd54a Remove no_config_ argument from Arguments 2019-09-04 16:39:23 +02:00
Matthias
03f3d0dc8b Remove desc from Arguments header 2019-09-04 16:38:33 +02:00
hroff-1902
74578b8752 Merge pull request #2211 from freqtrade/dependabot/pip/develop/python-telegram-bot-12.0.0
Bump python-telegram-bot from 11.1.0 to 12.0.0
2019-09-04 10:44:11 +03:00
hroff-1902
caec5ac941 Merge pull request #2206 from freqtrade/sloe_handling
Improve stoploss on exchange handling
2019-09-04 10:00:53 +03:00
Matthias
88f823f899 Improvements to documentation 2019-09-04 06:56:25 +02:00
hroff-1902
9a6a89c238 allow simplified hyperopt interface 2019-09-03 19:54:28 +03:00
hroff-1902
e8614abc5d update table md formatting, enhance description 2019-09-03 16:52:55 +03:00
hroff-1902
87ae2430df ranges for ROI tables for different ticker_intervals in docs 2019-09-03 11:32:18 +03:00
Matthias
dc9fda76f3 Fix tests to adapt to new telegram-bot interface 2019-09-02 20:42:39 +02:00
Matthias
3b15cce07a Handle arguments uniformly (by using context.args) 2019-09-02 20:17:47 +02:00
Matthias
8cad90f9e6 Adapt to new api 2019-09-02 20:17:23 +02:00
Matthias
9c60ab796d Adapt telegram api to new interface of telegram-bot-12.0.0 2019-09-02 20:14:41 +02:00
dependabot-preview[bot]
05789c4b92 Bump python-telegram-bot from 11.1.0 to 12.0.0
Bumps [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot) from 11.1.0 to 12.0.0.
- [Release notes](https://github.com/python-telegram-bot/python-telegram-bot/releases)
- [Changelog](https://github.com/python-telegram-bot/python-telegram-bot/blob/master/CHANGES.rst)
- [Commits](https://github.com/python-telegram-bot/python-telegram-bot/compare/v11.1.0...v12.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 13:44:52 +00:00
Matthias
962d487edb Merge pull request #2210 from freqtrade/dependabot/pip/develop/ccxt-1.18.1115
Bump ccxt from 1.18.1085 to 1.18.1115
2019-09-02 15:43:37 +02:00
dependabot-preview[bot]
04335ddd89 Bump ccxt from 1.18.1085 to 1.18.1115
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1085 to 1.18.1115.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1085...1.18.1115)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 12:42:07 +00:00
Matthias
8b6010255f Merge pull request #2209 from freqtrade/dependabot/pip/develop/mkdocs-material-4.4.2
Bump mkdocs-material from 4.4.1 to 4.4.2
2019-09-02 14:40:49 +02:00
Matthias
ccdc0ed26d Merge pull request #2208 from freqtrade/dependabot/pip/develop/arrow-0.14.6
Bump arrow from 0.14.5 to 0.14.6
2019-09-02 14:40:36 +02:00
dependabot-preview[bot]
3f6c0ba6d6 Bump arrow from 0.14.5 to 0.14.6
Bumps [arrow](https://github.com/crsmithdev/arrow) from 0.14.5 to 0.14.6.
- [Release notes](https://github.com/crsmithdev/arrow/releases)
- [Changelog](https://github.com/crsmithdev/arrow/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crsmithdev/arrow/compare/0.14.5...0.14.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 11:56:28 +00:00
Matthias
fe9d7b033e Merge pull request #2214 from freqtrade/dependabot/pip/develop/sqlalchemy-1.3.8
Bump sqlalchemy from 1.3.7 to 1.3.8
2019-09-02 13:55:21 +02:00
Matthias
12e893ff2d Merge pull request #2213 from freqtrade/dependabot/pip/develop/pytest-5.1.2
Bump pytest from 5.1.1 to 5.1.2
2019-09-02 13:55:03 +02:00
Matthias
cda2a2586b Merge pull request #2212 from freqtrade/dependabot/pip/develop/numpy-1.17.1
Bump numpy from 1.17.0 to 1.17.1
2019-09-02 13:54:43 +02:00
dependabot-preview[bot]
bf4e3f55f4 Bump sqlalchemy from 1.3.7 to 1.3.8
Bumps [sqlalchemy](https://github.com/sqlalchemy/sqlalchemy) from 1.3.7 to 1.3.8.
- [Release notes](https://github.com/sqlalchemy/sqlalchemy/releases)
- [Changelog](https://github.com/sqlalchemy/sqlalchemy/blob/master/CHANGES)
- [Commits](https://github.com/sqlalchemy/sqlalchemy/commits)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 08:29:26 +00:00
dependabot-preview[bot]
51ad05efdb Bump pytest from 5.1.1 to 5.1.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.1 to 5.1.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.1.1...5.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 08:29:07 +00:00
dependabot-preview[bot]
89f5cf8291 Bump numpy from 1.17.0 to 1.17.1
Bumps [numpy](https://github.com/numpy/numpy) from 1.17.0 to 1.17.1.
- [Release notes](https://github.com/numpy/numpy/releases)
- [Changelog](https://github.com/numpy/numpy/blob/master/doc/HOWTO_RELEASE.rst.txt)
- [Commits](https://github.com/numpy/numpy/compare/v1.17.0...v1.17.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 08:28:47 +00:00
dependabot-preview[bot]
949ab2a17c Bump mkdocs-material from 4.4.1 to 4.4.2
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.4.1 to 4.4.2.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.4.1...4.4.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-09-02 08:27:37 +00:00
hroff-1902
08b090c707 Merge pull request #2176 from freqtrade/plot_commands
Move Plot scripts to freqtrade subcommands
2019-09-02 08:08:51 +03:00
Matthias
c64beb3f76 Merge pull request #2207 from freqtrade/fix/dryrun_stoploss_on_exchange
Reenable stoploss_on_exchange for dry-run
2019-09-02 06:16:44 +02:00
Matthias
aae9c3194f Reenable stoploss_on_exchange for dry-run 2019-09-01 17:48:06 +02:00
Matthias
20c9c93b3e Improve docstring 2019-09-01 10:25:05 +02:00
Matthias
771519e311 Don't show stacktrace in case of invalidorder Error
This is handled gracefully by emergency-selling
2019-09-01 10:19:18 +02:00
Matthias
f91557f549 Add space to exception message 2019-09-01 10:17:17 +02:00
Matthias
514860ac3b Improve documentation 2019-09-01 10:17:02 +02:00
Matthias
9d7ebc65e7 Move return statement to correct intend 2019-09-01 09:21:45 +02:00
Matthias
6aab3fe25a Add test for stoploss order handling behaviour 2019-09-01 09:18:15 +02:00
Matthias
7c0a49a6f9 _notify_sell needs ordertype seperately 2019-09-01 09:17:58 +02:00
Matthias
292df115e8 Support selling via emergencysell 2019-09-01 09:09:07 +02:00
Matthias
9f53e9f5dd Raise InvalidOrder error when stoploss-creation fails 2019-09-01 09:08:35 +02:00
Matthias
ee808abfea Add emergency_sell as sell reason 2019-09-01 09:07:09 +02:00
Matthias
7fc156648a simplify stoploss_oe code 2019-08-31 16:15:39 +02:00
Matthias
f0c0f5618b Abstract creating stoploss-orders from stoploss-logic 2019-08-31 16:11:04 +02:00
Matthias
2886fa288a fix documentation 2019-08-31 15:31:47 +02:00
Matthias
736deaae32 Add test with plot command without configuration 2019-08-31 15:26:34 +02:00
Matthias
c9e15c2f86 Add test for new check_exchange branch 2019-08-31 15:19:59 +02:00
Matthias
d48f03c32e check_exchange is not required for plotting 2019-08-31 15:19:53 +02:00
Matthias
1760a8dfbc Use subparser-name to exclude from config requires 2019-08-31 15:15:10 +02:00
Matthias
f278fcfc3f Use plot-runmode for plot scripts 2019-08-31 15:14:57 +02:00
Matthias
816d942ded Merge branch 'develop' into plot_commands 2019-08-30 20:42:58 +02:00
Matthias
423805c9ca Small documentation improvements 2019-08-30 20:42:14 +02:00
hroff-1902
a7e45c5a73 Merge pull request #2201 from freqtrade/fix/webhook
sending rpc messages should not stop the bot
2019-08-30 13:33:19 +03:00
Matthias
d060d27745 Add test for all messagetypes 2019-08-30 07:05:22 +02:00
Matthias
75dc174c76 support all messagetypes in webhook 2019-08-30 07:02:57 +02:00
Matthias
d977695d48 Catch NotImplementedError when sending messages
(RPC should not crash your bot!)
2019-08-30 07:02:26 +02:00
Matthias
b6b7dcd61c Test NotImplemented is cought correctly 2019-08-30 07:00:29 +02:00
hroff-1902
43ef831bf7 Merge pull request #2199 from freqtrade/fix_datadir_init
Fix datadir init to always include exchange
2019-08-30 01:54:16 +03:00
Matthias
cabe291006 Fix test-leakage by not copying config correctly 2019-08-29 06:54:28 +02:00
Matthias
6b3d25b54b Fix datadir init when used wiht --exchange 2019-08-29 06:45:20 +02:00
Matthias
68adfc6607 Init exchange before datadir ... 2019-08-29 06:42:56 +02:00
Matthias
ba0d7aa09c Merge pull request #2190 from freqtrade/strategy_version
Introduce strategy_version
2019-08-28 19:47:33 +02:00
Matthias
50b572a657 Merge branch 'develop' into strategy_version 2019-08-28 19:29:53 +02:00
Matthias
c38f3a2b9a Apply dynamic versioning to develop 2019-08-28 07:05:48 +02:00
Matthias
c6bb68bd30 Merge pull request #2192 from freqtrade/rename_teststrat
Rename testStrategy to sample_strategy
2019-08-28 06:28:19 +02:00
Matthias
8923c02222 docstring wording 2019-08-28 06:07:18 +02:00
hroff-1902
b4685151ce Merge pull request #2191 from hroff-1902/docs-minor-fixes3
minor: improvements in confuguration.md
2019-08-28 00:33:44 +03:00
hroff-1902
756f44fcbd highlight really important notifications 2019-08-28 00:20:32 +03:00
Matthias
51fbeed71f Rename TestStrategy to SampleStrategy 2019-08-27 06:42:10 +02:00
Matthias
d66fb86449 Add documentation for interface_version 2019-08-27 06:32:01 +02:00
Matthias
40df303122 Merge pull request #2184 from hroff-1902/backtesting-minor-cleanup2
minor: Backtesting cleanup
2019-08-27 06:14:02 +02:00
hroff-1902
a504abf00c minor: improvements in confuguration.md 2019-08-27 04:12:00 +03:00
hroff-1902
d9c2b7d460 fix fetching ticker_interval from strategy 2019-08-26 22:31:24 +03:00
Matthias
0e62b8bd85 Update strategy_version to INTERFACE_VERSION 2019-08-26 20:16:03 +02:00
Matthias
6d1c54ed92 Merge pull request #2179 from freqtrade/timeframe_use_ccxt
[minor] Use ccxt methods to round timeframe
2019-08-26 20:01:24 +02:00
Matthias
b5789203f2 Merge branch 'develop' into timeframe_use_ccxt 2019-08-26 19:48:58 +02:00
Matthias
92011f8294 Introduce strategy_version variable 2019-08-26 19:44:33 +02:00
Matthias
b3db1ec1a7 Merge pull request #2186 from freqtrade/dependabot/pip/develop/pytest-5.1.1
Bump pytest from 5.1.0 to 5.1.1
2019-08-26 15:35:10 +02:00
Matthias
5f60fcb602 Merge pull request #2185 from freqtrade/dependabot/pip/develop/mkdocs-material-4.4.1
Bump mkdocs-material from 4.4.0 to 4.4.1
2019-08-26 14:22:13 +02:00
Matthias
30a7be457c Merge pull request #2188 from freqtrade/dependabot/pip/develop/pandas-0.25.1
Bump pandas from 0.25.0 to 0.25.1
2019-08-26 14:04:54 +02:00
Matthias
93198ed28b Merge pull request #2187 from freqtrade/dependabot/pip/develop/ccxt-1.18.1085
Bump ccxt from 1.18.1068 to 1.18.1085
2019-08-26 14:03:41 +02:00
dependabot-preview[bot]
6af5135802 Bump pandas from 0.25.0 to 0.25.1
Bumps [pandas](https://github.com/pandas-dev/pandas) from 0.25.0 to 0.25.1.
- [Release notes](https://github.com/pandas-dev/pandas/releases)
- [Changelog](https://github.com/pandas-dev/pandas/blob/master/RELEASE.md)
- [Commits](https://github.com/pandas-dev/pandas/compare/v0.25.0...v0.25.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-26 08:07:30 +00:00
dependabot-preview[bot]
6b233eb862 Bump ccxt from 1.18.1068 to 1.18.1085
Bumps [ccxt](https://github.com/ccxt/ccxt) from 1.18.1068 to 1.18.1085.
- [Release notes](https://github.com/ccxt/ccxt/releases)
- [Changelog](https://github.com/ccxt/ccxt/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ccxt/ccxt/compare/1.18.1068...1.18.1085)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-26 08:07:24 +00:00
dependabot-preview[bot]
75e3d22043 Bump pytest from 5.1.0 to 5.1.1
Bumps [pytest](https://github.com/pytest-dev/pytest) from 5.1.0 to 5.1.1.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/5.1.0...5.1.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-26 08:07:07 +00:00
dependabot-preview[bot]
e5da5f7fe7 Bump mkdocs-material from 4.4.0 to 4.4.1
Bumps [mkdocs-material](https://github.com/squidfunk/mkdocs-material) from 4.4.0 to 4.4.1.
- [Release notes](https://github.com/squidfunk/mkdocs-material/releases)
- [Changelog](https://github.com/squidfunk/mkdocs-material/blob/master/CHANGELOG)
- [Commits](https://github.com/squidfunk/mkdocs-material/compare/4.4.0...4.4.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-08-26 08:07:03 +00:00
Matthias
4fcfb1eaca Merge pull request #2180 from freqtrade/refactor_download
[Refactor] Logic for download-data to history
2019-08-26 06:13:19 +02:00
hroff-1902
bfc68ec792 minor cleanup in Backtesting 2019-08-25 23:36:42 +03:00
Matthias
ae8c5eb75f Merge pull request #2182 from freqtrade/fix/stringescaping
[minor] Don't escape ticks where it's not needed
2019-08-25 22:09:57 +02:00
Matthias
513e84880e Don't escape ticks where it's not needed 2019-08-25 20:38:51 +02:00
Matthias
626b9bbf64 Merge pull request #2178 from freqtrade/refactor/stoploss_on_e_to_binance
[refactor] Move stoploss on exchange implementation to binance
2019-08-25 19:16:05 +02:00
Matthias
da7da2ce52 Change tests to split function 2019-08-25 15:06:47 +02:00
Matthias
3232251fea Refactor downloading ohlcv from utils to history 2019-08-25 15:01:27 +02:00
Matthias
e603cca7a5 Testing with now() should not pass in date/time 2019-08-25 10:53:56 +02:00
Matthias
8f8acf5b06 Update ccxt to have this implemented 2019-08-25 10:43:37 +02:00
Matthias
565a543b7b Use ccxt base methods to round timeframe 2019-08-25 10:34:56 +02:00
Matthias
5e12b05424 Improve test coverage 2019-08-25 10:18:55 +02:00
Matthias
a4c8b5bf5d Move binance-specific test to test_binance.py 2019-08-25 10:08:06 +02:00
Matthias
cbf09b5ad9 Improve docstring for Exception 2019-08-25 10:07:47 +02:00
Matthias
2c66b33fd1 Adapt some tests to use Binance subclass for stoplosslimit 2019-08-25 09:57:21 +02:00
Matthias
067c122bf3 Adapt test to use Binance class 2019-08-25 09:52:21 +02:00
Matthias
defa1c027d Move stoploss_limit to binance subclass 2019-08-25 09:50:37 +02:00
Matthias
ea179a8e38 stoploss_limit shall not use create_order()
It needs to handle exceptions differently
2019-08-25 09:43:10 +02:00
Matthias
8a17615b5a move exceptionhandling from create_order() to calling functions 2019-08-25 09:42:02 +02:00
Matthias
3121206afe correct wrongly named test 2019-08-24 15:35:43 +02:00
Matthias
240936eb19 Small fixes 2019-08-24 15:26:42 +02:00
Matthias
1336781f8f Reorder points in documentation to group analysis points 2019-08-24 15:11:31 +02:00
Matthias
661cd65bdd Improvements to plot documentation 2019-08-24 15:11:31 +02:00
Matthias
fb498795ad Improve profit-plot styling 2019-08-24 15:11:31 +02:00
Matthias
2ae398913d Fix bug in bt-analysis when multiple trades sell at the same time 2019-08-24 15:11:31 +02:00
Matthias
d711b8c0e9 Plot-profit should have subtitles per subplot 2019-08-24 15:11:31 +02:00
Matthias
395414ccde Refactor init_plotscript a bit (strategy is not needed for plot_profit) 2019-08-24 15:11:31 +02:00
Matthias
9f29ad77bd fix test after plot_dataframe change 2019-08-24 15:11:31 +02:00
Matthias
545e5c5bc6 simplify load_trades call 2019-08-24 15:11:31 +02:00
Matthias
1b374fcf7e Improve plotting documentation 2019-08-24 15:11:31 +02:00
Matthias
518d7dfde8 Replace plot-scripts with pointers to the new commands 2019-08-24 15:11:31 +02:00
Matthias
f8ddb10607 switch indicators to nargs argument type 2019-08-24 15:11:31 +02:00
Matthias
0ef13be577 Test plot_profit 2019-08-24 15:11:31 +02:00
Matthias
c559f95703 Add test for plot-profit 2019-08-24 15:11:31 +02:00
Matthias
f7cb75ff93 Add plot-profit command 2019-08-24 15:11:31 +02:00
Matthias
29076acc69 Add test for analyse_and_plot 2019-08-24 15:11:31 +02:00
Matthias
99b2be90fd Cleanup plotting (if you have backtest results, no need to download
data!)
2019-08-24 15:11:31 +02:00
Matthias
f8c72feea8 Add some initial tests for plot_dataframe 2019-08-24 15:11:31 +02:00
Matthias
69c2b12879 Move plot_dataframe as freqtrade submodule 2019-08-24 15:11:31 +02:00
hroff-1902
fcb0ff1b60 do not round values in the debug message 2019-08-20 23:42:44 +03:00
hroff-1902
31669fde03 test adjusted 2019-08-20 23:28:16 +03:00
hroff-1902
17b3f01b28 Merge branch 'develop' into hyperopt-adaptive-roi-space 2019-08-20 23:00:23 +03:00
hroff-1902
cadf573170 round printed stoploss value as well 2019-08-20 22:24:59 +03:00
hroff-1902
a12876da92 fine printing for floats in the roi tables (round to 5 digits after the decimal point) 2019-08-20 22:17:21 +03:00
hroff-1902
5b9711c002 adaptive roi_space 2019-08-14 13:25:49 +03:00
183 changed files with 6556 additions and 3605 deletions

View File

@@ -1,6 +1,6 @@
[run] [run]
omit = omit =
scripts/* scripts/*
freqtrade/tests/*
freqtrade/vendor/* freqtrade/vendor/*
freqtrade/__main__.py freqtrade/__main__.py
tests/*

5
.gitignore vendored
View File

@@ -1,12 +1,9 @@
# Freqtrade rules # Freqtrade rules
freqtrade/tests/testdata/*.json
hyperopt_conf.py
config*.json config*.json
*.sqlite *.sqlite
.hyperopt
logfile.txt logfile.txt
hyperopt_trials.pickle
user_data/* user_data/*
!user_data/strategy/sample_strategy.py
!user_data/notebooks !user_data/notebooks
user_data/notebooks/* user_data/notebooks/*
!user_data/notebooks/*example.ipynb !user_data/notebooks/*example.ipynb

View File

@@ -22,19 +22,19 @@ jobs:
include: include:
- stage: tests - stage: tests
script: script:
- pytest --random-order --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/ - pytest --random-order --cov=freqtrade --cov-config=.coveragerc
# Allow failure for coveralls # Allow failure for coveralls
- coveralls || true - coveralls || true
name: pytest name: pytest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir freqtrade/tests/testdata backtesting - freqtrade --datadir tests/testdata backtesting
name: backtest name: backtest
- script: - script:
- cp config.json.example config.json - cp config.json.example config.json
- freqtrade --datadir freqtrade/tests/testdata hyperopt -e 5 - freqtrade --datadir tests/testdata hyperopt -e 5
name: hyperopt name: hyperopt
- script: flake8 freqtrade scripts - script: flake8
name: flake8 name: flake8
- script: - script:
# Test Documentation boxes - # Test Documentation boxes -

View File

@@ -11,7 +11,7 @@ Few pointers for contributions:
- Create your PR against the `develop` branch, not `master`. - Create your PR against the `develop` branch, not `master`.
- New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100). - New features need to contain unit tests and must be PEP8 conformant (max-line-length = 100).
If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) If you are unsure, discuss the feature on our [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE)
or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR. or in a [issue](https://github.com/freqtrade/freqtrade/issues) before a PR.
## Getting started ## Getting started
@@ -28,19 +28,19 @@ make it pass. It means you have introduced a regression.
#### Test the whole project #### Test the whole project
```bash ```bash
pytest freqtrade pytest
``` ```
#### Test only one file #### Test only one file
```bash ```bash
pytest freqtrade/tests/test_<file_name>.py pytest tests/test_<file_name>.py
``` ```
#### Test only one method from one file #### Test only one method from one file
```bash ```bash
pytest freqtrade/tests/test_<file_name>.py::test_<method_name> pytest tests/test_<file_name>.py::test_<method_name>
``` ```
### 2. Test if your code is PEP8 compliant ### 2. Test if your code is PEP8 compliant
@@ -114,6 +114,6 @@ Contributors may be given commit privileges. Preference will be given to those w
1. Access to resources for cross-platform development and testing. 1. Access to resources for cross-platform development and testing.
1. Time to devote to the project regularly. 1. Time to devote to the project regularly.
Beeing a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust FreqTrade with their Exchange API keys). Being a Committer does not grant write permission on `develop` or `master` for security reasons (Users trust FreqTrade with their Exchange API keys).
After beeing Committer for some time, a Committer may be named Core Committer and given full repository access. After being Committer for some time, a Committer may be named Core Committer and given full repository access.

View File

@@ -1,4 +1,4 @@
FROM python:3.7.4-slim-stretch FROM python:3.7.5-slim-stretch
RUN apt-get update \ RUN apt-get update \
&& apt-get -y install curl build-essential libssl-dev \ && apt-get -y install curl build-essential libssl-dev \
@@ -16,9 +16,9 @@ RUN cd /tmp && /tmp/install_ta-lib.sh && rm -r /tmp/*ta-lib*
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib
# Install dependencies # Install dependencies
COPY requirements.txt requirements-common.txt /freqtrade/ COPY requirements.txt requirements-common.txt requirements-hyperopt.txt /freqtrade/
RUN pip install numpy --no-cache-dir \ RUN pip install numpy --no-cache-dir \
&& pip install -r requirements.txt --no-cache-dir && pip install -r requirements-hyperopt.txt --no-cache-dir
# Install and execute # Install and execute
COPY . /freqtrade/ COPY . /freqtrade/

View File

@@ -22,13 +22,13 @@ RUN tar -xzf /freqtrade/ta-lib-0.4.0-src.tar.gz \
ENV LD_LIBRARY_PATH /usr/local/lib ENV LD_LIBRARY_PATH /usr/local/lib
# Install berryconda # Install berryconda
RUN wget https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \ RUN wget -q https://github.com/jjhelmus/berryconda/releases/download/v2.0.0/Berryconda3-2.0.0-Linux-armv7l.sh \
&& bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \ && bash ./Berryconda3-2.0.0-Linux-armv7l.sh -b \
&& rm Berryconda3-2.0.0-Linux-armv7l.sh && rm Berryconda3-2.0.0-Linux-armv7l.sh
# Install dependencies # Install dependencies
COPY requirements-common.txt /freqtrade/ COPY requirements-common.txt /freqtrade/
RUN ~/berryconda3/bin/conda install -y numpy pandas scipy \ RUN ~/berryconda3/bin/conda install -y numpy pandas \
&& ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir && ~/berryconda3/bin/pip install -r requirements-common.txt --no-cache-dir
# Install and execute # Install and execute

View File

@@ -2,4 +2,3 @@ include LICENSE
include README.md include README.md
include config.json.example include config.json.example
recursive-include freqtrade *.py recursive-include freqtrade *.py
include freqtrade/tests/testdata/*.json

View File

@@ -141,7 +141,7 @@ Accounts having BNB accounts use this to pay for fees - if your first trade happ
For any questions not covered by the documentation or for further For any questions not covered by the documentation or for further
information about the bot, we encourage you to join our slack channel. information about the bot, we encourage you to join our slack channel.
- [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). - [Click here to join Slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE).
### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue) ### [Bugs / Issues](https://github.com/freqtrade/freqtrade/issues?q=is%3Aissue)
@@ -172,7 +172,7 @@ to understand the requirements before sending your pull-requests.
Coding is not a neccessity to contribute - maybe start with improving our documentation? Coding is not a neccessity to contribute - maybe start with improving our documentation?
Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase. Issues labeled [good first issue](https://github.com/freqtrade/freqtrade/labels/good%20first%20issue) can be good first contributions, and will help get you familiar with the codebase.
**Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it. **Note** before starting any major new feature work, *please open an issue describing what you are planning to do* or talk to us on [Slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE). This will ensure that interested parties can give valuable feedback on the feature, and let others know that you are working on it.
**Important:** Always create your PR against the `develop` branch, not `master`. **Important:** Always create your PR against the `develop` branch, not `master`.

View File

@@ -23,7 +23,7 @@ if [ $? -ne 0 ]; then
fi fi
# Run backtest # Run backtest
docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro freqtrade:${TAG} --datadir freqtrade/tests/testdata backtesting docker run --rm -it -v $(pwd)/config.json.example:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} --datadir /tests/testdata backtesting
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "failed running backtest" echo "failed running backtest"

View File

@@ -22,7 +22,10 @@
"ask_strategy":{ "ask_strategy":{
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
}, },
"exchange": { "exchange": {
"name": "bittrex", "name": "bittrex",
@@ -41,7 +44,7 @@
"ZEC/BTC", "ZEC/BTC",
"XLM/BTC", "XLM/BTC",
"NXT/BTC", "NXT/BTC",
"POWR/BTC", "TRX/BTC",
"ADA/BTC", "ADA/BTC",
"XMR/BTC" "XMR/BTC"
], ],
@@ -49,11 +52,6 @@
"DOGE/BTC" "DOGE/BTC"
] ]
}, },
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

View File

@@ -22,7 +22,10 @@
"ask_strategy":{ "ask_strategy":{
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
}, },
"exchange": { "exchange": {
"name": "binance", "name": "binance",
@@ -51,11 +54,6 @@
"BNB/BTC" "BNB/BTC"
] ]
}, },
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"edge": { "edge": {
"enabled": false, "enabled": false,
"process_throttle_secs": 3600, "process_throttle_secs": 3600,

View File

@@ -33,11 +33,15 @@
"ask_strategy":{ "ask_strategy":{
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
}, },
"order_types": { "order_types": {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@@ -74,7 +78,7 @@
"ZEC/BTC", "ZEC/BTC",
"XLM/BTC", "XLM/BTC",
"NXT/BTC", "NXT/BTC",
"POWR/BTC", "TRX/BTC",
"ADA/BTC", "ADA/BTC",
"XMR/BTC" "XMR/BTC"
], ],
@@ -99,11 +103,6 @@
"max_trade_duration_minute": 1440, "max_trade_duration_minute": 1440,
"remove_pumps": false "remove_pumps": false
}, },
"experimental": {
"use_sell_signal": false,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
},
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "your_telegram_token", "token": "your_telegram_token",
@@ -120,7 +119,8 @@
"initial_state": "running", "initial_state": "running",
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5,
"heartbeat_interval": 60
}, },
"strategy": "DefaultStrategy", "strategy": "DefaultStrategy",
"strategy_path": "user_data/strategies/" "strategy_path": "user_data/strategies/"

View File

@@ -22,7 +22,11 @@
"ask_strategy":{ "ask_strategy":{
"use_order_book": false, "use_order_book": false,
"order_book_min": 1, "order_book_min": 1,
"order_book_max": 9 "order_book_max": 9,
"use_sell_signal": true,
"sell_profit_only": false,
"ignore_roi_if_buy_signal": false
}, },
"exchange": { "exchange": {
"name": "kraken", "name": "kraken",
@@ -66,5 +70,6 @@
"forcebuy_enable": false, "forcebuy_enable": false,
"internals": { "internals": {
"process_throttle_secs": 5 "process_throttle_secs": 5
} },
"download_trades": true
} }

View File

@@ -0,0 +1,20 @@
---
version: '3'
services:
freqtrade_develop:
build:
context: .
dockerfile: "./Dockerfile.develop"
volumes:
- ".:/freqtrade"
entrypoint:
- "freqtrade"
freqtrade_bash:
build:
context: .
dockerfile: "./Dockerfile.develop"
volumes:
- ".:/freqtrade"
entrypoint:
- "/bin/bash"

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
---
version: '3'
services:
freqtrade:
image: freqtradeorg/freqtrade:master
volumes:
- "./user_data:/freqtrade/user_data"
- "./config.json:/freqtrade/config.json"

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

BIN
docs/assets/plot-profit.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

View File

@@ -1,41 +1,9 @@
# Backtesting # Backtesting
This page explains how to validate your strategy performance by using This page explains how to validate your strategy performance by using Backtesting.
Backtesting.
## Getting data for backtesting and hyperopt Backtesting requires historic data to be available.
To learn how to get data for the pairs and exchange you're interested in, head over to the [Data Downloading](data-download.md) section of the documentation.
To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command.
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes.
Exchange and pairs will come from `config.json` (if specified using `-c/--config`). Otherwise `--exchange` becomes mandatory.
Alternatively, a `pairs.json` file can be used.
If you are using Binance for example:
- create a directory `user_data/data/binance` and copy `pairs.json` in that directory.
- update the `pairs.json` to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
Then run:
```bash
freqtrade download-data --exchange binance
```
This will download ticker data for all the currency pairs you defined in `pairs.json`.
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days).
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
## Test your strategy with Backtesting ## Test your strategy with Backtesting
@@ -43,18 +11,16 @@ Now you have good Buy and Sell strategies and some historic data, you want to te
real data. This is what we call real data. This is what we call
[backtesting](https://en.wikipedia.org/wiki/Backtesting). [backtesting](https://en.wikipedia.org/wiki/Backtesting).
Backtesting will use the crypto-currencies (pair) from your config file Backtesting will use the crypto-currencies (pairs) from your config file
and load static tickers located in and load ticker data from `user_data/data/<exchange>` by default.
[/freqtrade/tests/testdata](https://github.com/freqtrade/freqtrade/tree/develop/freqtrade/tests/testdata). If no data is available for the exchange / pair / ticker interval combination, backtesting will
If the 5 min and 1 min ticker for the crypto-currencies to test is not ask you to download them first using `freqtrade download-data`.
already in the `testdata` directory, backtesting will download them For details on downloading, please refer to the [Data Downloading](data-download.md) section in the documentation.
automatically. Testdata files will not be updated until you specify it.
The result of backtesting will confirm you if your bot has better odds of making a profit than a loss. The result of backtesting will confirm if your bot has better odds of making a profit than a loss.
The backtesting is very easy with freqtrade.
### Run a backtesting against the currencies listed in your config file ### Run a backtesting against the currencies listed in your config file
#### With 5 min tickers (Per default) #### With 5 min tickers (Per default)
```bash ```bash
@@ -73,24 +39,24 @@ Assume you downloaded the history data from the Bittrex exchange and kept it in
You can then use this data for backtesting as follows: You can then use this data for backtesting as follows:
```bash ```bash
freqtrade backtesting --datadir user_data/data/bittrex-20180101 freqtrade --datadir user_data/data/bittrex-20180101 backtesting
``` ```
#### With a (custom) strategy file #### With a (custom) strategy file
```bash ```bash
freqtrade -s TestStrategy backtesting freqtrade -s SampleStrategy backtesting
``` ```
Where `-s TestStrategy` refers to the class name within the strategy file `test_strategy.py` found in the `freqtrade/user_data/strategies` directory. Where `-s SampleStrategy` refers to the class name within the strategy file `sample_strategy.py` found in the `freqtrade/user_data/strategies` directory.
#### Comparing multiple Strategies #### Comparing multiple Strategies
```bash ```bash
freqtrade backtesting --strategy-list TestStrategy1 AwesomeStrategy --ticker-interval 5m freqtrade backtesting --strategy-list SampleStrategy1 AwesomeStrategy --ticker-interval 5m
``` ```
Where `TestStrategy1` and `AwesomeStrategy` refer to class names of strategies. Where `SampleStrategy1` and `AwesomeStrategy` refer to class names of strategies.
#### Exporting trades to file #### Exporting trades to file
@@ -103,31 +69,35 @@ The exported trades can be used for [further analysis](#further-backtest-result-
#### Exporting trades to file specifying a custom filename #### Exporting trades to file specifying a custom filename
```bash ```bash
freqtrade backtesting --export trades --export-filename=backtest_teststrategy.json freqtrade backtesting --export trades --export-filename=backtest_samplestrategy.json
``` ```
#### Running backtest with smaller testset #### Supplying custom fee value
Use the `--timerange` argument to change how much of the testset Sometimes your account has certain fee rebates (fee reductions starting with a certain account size or monthly volume), which are not visible to ccxt.
you want to use. The last N ticks/timeframes will be used. To account for this in backtesting, you can use `--fee 0.001` to supply this value to backtesting.
This fee must be a percentage, and will be applied twice (once for trade entry, and once for trade exit).
Example:
```bash ```bash
freqtrade backtesting --timerange=-200 freqtrade backtesting --fee 0.001
``` ```
#### Advanced use of timerange
Doing `--timerange=-200` will get the last 200 timeframes #### Running backtest with smaller testset by using timerange
from your inputdata. You can also specify specific dates,
or a range span indexed by start and stop. Use the `--timerange` argument to change how much of the testset you want to use.
For example, running backtesting with the `--timerange=20190501-` option will use all available data starting with May 1st, 2019 from your inputdata.
```bash
freqtrade backtesting --timerange=20190501-
```
You can also specify particular dates or a range span indexed by start and stop.
The full timerange specification: The full timerange specification:
- Use last 123 tickframes of data: `--timerange=-123`
- Use first 123 tickframes of data: `--timerange=123-`
- Use tickframes from line 123 through 456: `--timerange=123-456`
- Use tickframes till 2018/01/31: `--timerange=-20180131` - Use tickframes till 2018/01/31: `--timerange=-20180131`
- Use tickframes since 2018/01/31: `--timerange=20180131-` - Use tickframes since 2018/01/31: `--timerange=20180131-`
- Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` - Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
@@ -178,11 +148,12 @@ A backtesting result will look like that:
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 | | TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 | 0 |
``` ```
The 1st table will contain all trades the bot made. The 1st table contains all trades the bot made, including "left open trades".
The 2nd table will contain a recap of sell reasons. The 2nd table contains a recap of sell reasons.
The 3rd table will contain all trades the bot had to `forcesell` at the end of the backtest period to present a full picture. The 3rd table contains all trades the bot had to `forcesell` at the end of the backtest period to present a full picture.
This is necessary to simulate realistic behaviour, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
These trades are also included in the first table, but are extracted separately for clarity. These trades are also included in the first table, but are extracted separately for clarity.
The last line will give you the overall performance of your strategy, The last line will give you the overall performance of your strategy,
@@ -192,22 +163,16 @@ here:
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 | | TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 | 243 |
``` ```
We understand the bot has made `429` trades for an average duration of The bot has made `429` trades for an average duration of `4:12:00`, with a performance of `76.20%` (profit), that means it has
`4:12:00`, with a performance of `76.20%` (profit), that means it has
earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC. earned a total of `0.00762792 BTC` starting with a capital of 0.01 BTC.
The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums all the profits/losses. The column `avg profit %` shows the average profit for all trades made while the column `cum profit %` sums up all the profits/losses.
The column `tot profit %` shows instead the total profit % in relation to allocated capital The column `tot profit %` shows instead the total profit % in relation to allocated capital (`max_open_trades * stake_amount`).
(`max_open_trades * stake_amount`). In the above results we have `max_open_trades=2 stake_amount=0.005` in config In the above results we have `max_open_trades=2` and `stake_amount=0.005` in config so `tot_profit %` will be `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
so `(76.20/100) * (0.005 * 2) =~ 0.00762792 BTC`.
As you will see your strategy performance will be influenced by your buy Your strategy performance is influenced by your buy strategy, your sell strategy, and also by the `minimal_roi` and `stop_loss` you have set.
strategy, your sell strategy, and also by the `minimal_roi` and
`stop_loss` you have set.
As for an example if your minimal_roi is only `"0": 0.01`. You cannot For example, if your `minimal_roi` is only `"0": 0.01` you cannot expect the bot to make more profit than 1% (because it will sell every time a trade reaches 1%).
expect the bot to make more profit than 1% (because it will sell every
time a trade will reach 1%).
```json ```json
"minimal_roi": { "minimal_roi": {
@@ -216,22 +181,39 @@ time a trade will reach 1%).
``` ```
On the other hand, if you set a too high `minimal_roi` like `"0": 0.55` On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
(55%), there is a lot of chance that the bot will never reach this (55%), there is almost no chance that the bot will ever reach this profit.
profit. Hence, keep in mind that your performance is a mix of your Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
strategies, your configuration, and the crypto-currency you have set up.
### Assumptions made by backtesting
Since backtesting lacks some detailed information about what happens within a candle, it needs to take a few assumptions:
- Buys happen at open-price
- Sell signal sells happen at open-price of the following candle
- Low happens before high for stoploss, protecting capital first.
- ROI sells are compared to high - but the ROI value is used (e.g. ROI = 2%, high=5% - so the sell will be at 2%)
- Stoploss sells happen exactly at stoploss price, even if low was lower
- Trailing stoploss
- High happens first - adjusting stoploss
- Low uses the adjusted stoploss (so sells with large high-low difference are backtested correctly)
- Sell-reason does not explain if a trade was positive or negative, just what triggered the sell (this can look odd if negative ROI values are used)
Taking these assumptions, backtesting tries to mirror real trading as closely as possible. However, backtesting will **never** replace running a strategy in dry-run mode.
Also, keep in mind that past results don't guarantee future success.
In addition to the above assumptions, strategy authors should carefully read the [Common Mistakes](strategy-customization.md#common-mistakes-when-developing-strategies) section, to avoid using data in backtesting which is not available in real market conditions.
### Further backtest-result analysis ### Further backtest-result analysis
To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file). To further analyze your backtest results, you can [export the trades](#exporting-trades-to-file).
You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section. You can then load the trades to perform further analysis as shown in our [data analysis](data-analysis.md#backtesting) backtesting section.
## Backtesting multiple strategies ## Backtesting multiple strategies
To backtest multiple strategies, a list of Strategies can be provided. To compare multiple strategies, a list of Strategies can be provided to backtesting.
This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple This is limited to 1 ticker-interval per run, however, data is only loaded once from disk so if you have multiple
strategies you'd like to compare, this should give a nice runtime boost. strategies you'd like to compare, this will give a nice runtime boost.
All listed Strategies need to be in the same directory. All listed Strategies need to be in the same directory.
@@ -241,7 +223,7 @@ freqtrade backtesting --timerange 20180401-20180410 --ticker-interval 5m --strat
This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename. This will save the results to `user_data/backtest_results/backtest-result-<strategy>.json`, injecting the strategy-name into the target filename.
There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table). There will be an additional table comparing win/losses of the different strategies (identical to the "Total" row in the first table).
Detailed output for all strategies one after the other will be available, so make sure to scroll up. Detailed output for all strategies one after the other will be available, so make sure to scroll up to see the details per strategy.
``` ```
=========================================================== Strategy Summary =========================================================== =========================================================== Strategy Summary ===========================================================

View File

@@ -12,17 +12,23 @@ This page explains the different parameters of the bot and how to run it.
usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH] usage: freqtrade [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [-s NAME] [--strategy-path PATH] [--userdir PATH] [-s NAME] [--strategy-path PATH]
[--db-url PATH] [--sd-notify] [--db-url PATH] [--sd-notify]
{backtesting,edge,hyperopt,create-userdir,list-exchanges} ... {backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
...
Free, open source crypto trading bot Free, open source crypto trading bot
positional arguments: positional arguments:
{backtesting,edge,hyperopt,create-userdir,list-exchanges} {backtesting,edge,hyperopt,create-userdir,list-exchanges,list-timeframes,download-data,plot-dataframe,plot-profit}
backtesting Backtesting module. backtesting Backtesting module.
edge Edge module. edge Edge module.
hyperopt Hyperopt module. hyperopt Hyperopt module.
create-userdir Create user-data directory. create-userdir Create user-data directory.
list-exchanges Print available exchanges. list-exchanges Print available exchanges.
list-timeframes Print available ticker intervals (timeframes) for the
exchange.
download-data Download backtesting data.
plot-dataframe Plot candles with indicators.
plot-profit Generate plot showing profits.
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@@ -100,7 +106,7 @@ user_data/
├── backtest_results ├── backtest_results
├── data ├── data
├── hyperopts ├── hyperopts
├── hyperopts_results ├── hyperopt_results
├── plot ├── plot
└── strategies └── strategies
``` ```
@@ -168,26 +174,25 @@ Backtesting also uses the config specified via `-c/--config`.
``` ```
usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade backtesting [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades MAX_OPEN_TRADES] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [-r] [--eps] [--dmmp] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[-l] [--eps] [--dmmp]
[--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]] [--strategy-list STRATEGY_LIST [STRATEGY_LIST ...]]
[--export EXPORT] [--export-filename PATH] [--export EXPORT] [--export-filename PATH]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (1m, 5m, 30m, 1h, 1d). Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades MAX_OPEN_TRADES --max_open_trades INT
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
-r, --refresh-pairs-cached --fee FLOAT Specify fee ratio. Will be applied twice (on trade
Refresh the pairs files in tests/testdata with the entry and exit).
latest data from the exchange. Use it if you want to
run your optimization commands with up-to-date data.
--eps, --enable-position-stacking --eps, --enable-position-stacking
Allow buying the same pair multiple times (position Allow buying the same pair multiple times (position
stacking). stacking).
@@ -197,19 +202,21 @@ optional arguments:
number). number).
--strategy-list STRATEGY_LIST [STRATEGY_LIST ...] --strategy-list STRATEGY_LIST [STRATEGY_LIST ...]
Provide a space-separated list of strategies to Provide a space-separated list of strategies to
backtest Please note that ticker-interval needs to be backtest. Please note that ticker-interval needs to be
set either in config or via command line. When using set either in config or via command line. When using
this together with --export trades, the strategy-name this together with `--export trades`, the strategy-
is injected into the filename (so backtest-data.json name is injected into the filename (so `backtest-
becomes backtest-data-DefaultStrategy.json data.json` becomes `backtest-data-
--export EXPORT Export backtest results, argument are: trades. Example DefaultStrategy.json`
--export=trades --export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH --export-filename PATH
Save backtest results to this filename requires Save backtest results to the file with this filename
--export to be set as well Example --export- (default: `user_data/backtest_results/backtest-
filename=user_data/backtest_results/backtest_today.json result.json`). Requires `--export` to be set as well.
(default: user_data/backtest_results/backtest- Example: `--export-filename=user_data/backtest_results
result.json) /backtest_today.json`
``` ```
### Getting historic data for backtesting ### Getting historic data for backtesting
@@ -226,13 +233,13 @@ to find optimal parameter values for your stategy.
``` ```
usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade hyperopt [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades INT] [--max_open_trades INT]
[--stake_amount STAKE_AMOUNT] [-r] [--stake_amount STAKE_AMOUNT] [--fee FLOAT]
[--customhyperopt NAME] [--hyperopt-path PATH] [--customhyperopt NAME] [--hyperopt-path PATH]
[--eps] [-e INT] [--eps] [-e INT]
[-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]] [-s {all,buy,sell,roi,stoploss} [{all,buy,sell,roi,stoploss} ...]]
[--dmmp] [--print-all] [--no-color] [-j JOBS] [--dmmp] [--print-all] [--no-color] [--print-json]
[--random-state INT] [--min-trades INT] [--continue] [-j JOBS] [--random-state INT] [--min-trades INT]
[--hyperopt-loss NAME] [--continue] [--hyperopt-loss NAME]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
@@ -245,13 +252,11 @@ optional arguments:
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
-r, --refresh-pairs-cached --fee FLOAT Specify fee ratio. Will be applied twice (on trade
Refresh the pairs files in tests/testdata with the entry and exit).
latest data from the exchange. Use it if you want to
run your optimization commands with up-to-date data.
--customhyperopt NAME --customhyperopt NAME
Specify hyperopt class name (default: Specify hyperopt class name (default:
`DefaultHyperOpts`). `DefaultHyperOpt`).
--hyperopt-path PATH Specify additional lookup path for Hyperopts and --hyperopt-path PATH Specify additional lookup path for Hyperopts and
Hyperopt Loss functions. Hyperopt Loss functions.
--eps, --enable-position-stacking --eps, --enable-position-stacking
@@ -268,6 +273,7 @@ optional arguments:
--print-all Print all results, not only the best ones. --print-all Print all results, not only the best ones.
--no-color Disable colorization of hyperopt results. May be --no-color Disable colorization of hyperopt results. May be
useful if you are redirecting output to a file. useful if you are redirecting output to a file.
--print-json Print best result detailization in JSON format.
-j JOBS, --job-workers JOBS -j JOBS, --job-workers JOBS
The number of concurrently running jobs for The number of concurrently running jobs for
hyperoptimization (hyperopt worker processes). If -1 hyperoptimization (hyperopt worker processes). If -1
@@ -286,8 +292,8 @@ optional arguments:
generate completely different results, since the generate completely different results, since the
target for optimization is different. Built-in target for optimization is different. Built-in
Hyperopt-loss-functions are: DefaultHyperOptLoss, Hyperopt-loss-functions are: DefaultHyperOptLoss,
OnlyProfitHyperOptLoss, SharpeHyperOptLoss. OnlyProfitHyperOptLoss, SharpeHyperOptLoss.(default:
(default: `DefaultHyperOptLoss`). `DefaultHyperOptLoss`).
``` ```
## Edge commands ## Edge commands
@@ -296,29 +302,28 @@ To know your trade expectancy and winrate against historical data, you can use E
``` ```
usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE] usage: freqtrade edge [-h] [-i TICKER_INTERVAL] [--timerange TIMERANGE]
[--max_open_trades MAX_OPEN_TRADES] [--max_open_trades INT] [--stake_amount STAKE_AMOUNT]
[--stake_amount STAKE_AMOUNT] [-r] [--fee FLOAT] [--stoplosses STOPLOSS_RANGE]
[--stoplosses STOPLOSS_RANGE]
optional arguments: optional arguments:
-h, --help show this help message and exit -h, --help show this help message and exit
-i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL -i TICKER_INTERVAL, --ticker-interval TICKER_INTERVAL
Specify ticker interval (1m, 5m, 30m, 1h, 1d). Specify ticker interval (`1m`, `5m`, `30m`, `1h`,
`1d`).
--timerange TIMERANGE --timerange TIMERANGE
Specify what timerange of data to use. Specify what timerange of data to use.
--max_open_trades MAX_OPEN_TRADES --max_open_trades INT
Specify max_open_trades to use. Specify max_open_trades to use.
--stake_amount STAKE_AMOUNT --stake_amount STAKE_AMOUNT
Specify stake_amount. Specify stake_amount.
-r, --refresh-pairs-cached --fee FLOAT Specify fee ratio. Will be applied twice (on trade
Refresh the pairs files in tests/testdata with the entry and exit).
latest data from the exchange. Use it if you want to
run your optimization commands with up-to-date data.
--stoplosses STOPLOSS_RANGE --stoplosses STOPLOSS_RANGE
Defines a range of stoploss against which edge will Defines a range of stoploss values against which edge
assess the strategy the format is "min,max,step" will assess the strategy. The format is "min,max,step"
(without any space).example: (without any space). Example:
--stoplosses=-0.01,-0.1,-0.001 `--stoplosses=-0.01,-0.1,-0.001`
``` ```
To understand edge and how to read the results, please read the [edge documentation](edge.md). To understand edge and how to read the results, please read the [edge documentation](edge.md).

View File

@@ -1,14 +1,15 @@
# Configure the bot # Configure the bot
This page explains how to configure the bot. Freqtrade has many configurable features and possibilities.
By default, these settings are configured via the configuration file (see below).
## The Freqtrade configuration file ## The Freqtrade configuration file
The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file). The bot uses a set of configuration parameters during its operation that all together conform the bot configuration. It normally reads its configuration from a file (Freqtrade configuration file).
Per default, the bot loads configuration from the `config.json` file located in the current working directory. Per default, the bot loads the configuration from the `config.json` file, located in the current working directory.
You can change the name of the configuration file used by the bot with the `-c/--config` command line option. You can specify a different configuration file used by the bot with the `-c/--config` command line option.
In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream. In some advanced use cases, multiple configuration files can be specified and used by the bot or the bot can read its configuration parameters from the process standard input stream.
@@ -22,19 +23,26 @@ The Freqtrade configuration file is to be written in the JSON format.
Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters. Additionally to the standard JSON syntax, you may use one-line `// ...` and multi-line `/* ... */` comments in your configuration files and trailing commas in the lists of parameters.
Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it. Do not worry if you are not familiar with JSON format -- simply open the configuration file with an editor of your choice, make some changes to the parameters you need, save your changes and, finally, restart the bot or, if it was previously stopped, run it again with the changes you made to the configuration. The bot validates syntax of the configuration file at startup and will warn you if you made any errors editing it, pointing out problematic lines.
## Configuration parameters ## Configuration parameters
The table below will list all configuration parameters available. The table below will list all configuration parameters available.
Mandatory parameters are marked as **Required**. Freqtrade can also load many options via command line (CLI) arguments (check out the commands `--help` output for details).
The prevelance for all Options is as follows:
- CLI arguments override any other option
- Configuration files are used in sequence (last file wins), and override Strategy configurations.
- Strategy configurations are only used if they are not set via configuration or via command line arguments. These options are market with [Strategy Override](#parameters-in-the-strategy) in the below table.
Mandatory parameters are marked as **Required**, which means that they are required to be set in one of the possible ways.
| Command | Default | Description | | Command | Default | Description |
|----------|---------|-------------| |----------|---------|-------------|
| `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades) | `max_open_trades` | 3 | **Required.** Number of trades open your bot will have. If -1 then it is ignored (i.e. potentially unlimited open trades)
| `stake_currency` | BTC | **Required.** Crypto-currency used for trading. [Strategy Override](#parameters-in-the-strategy). | `stake_currency` | BTC | **Required.** Crypto-currency used for trading.
| `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance. [Strategy Override](#parameters-in-the-strategy). | `stake_amount` | 0.05 | **Required.** Amount of crypto-currency your bot will use for each trade. Per default, the bot will use (0.05 BTC x 3) = 0.15 BTC in total will be always engaged. Set it to `"unlimited"` to allow the bot to use all available balance.
| `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals. | `amount_reserve_percent` | 0.05 | Reserve some amount in min pair stake amount. Default is 5%. The bot will reserve `amount_reserve_percent` + stop-loss value when calculating min pair stake amount in order to avoid possible trade refusals.
| `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy). | `ticker_interval` | [1m, 5m, 15m, 30m, 1h, 1d, ...] | The ticker interval to use (1min, 5 min, 15 min, 30 min, 1 hour or 1 day). Default is 5 minutes. [Strategy Override](#parameters-in-the-strategy).
| `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below. | `fiat_display_currency` | USD | **Required.** Fiat currency used to show your profits. More information below.
@@ -51,33 +59,34 @@ Mandatory parameters are marked as **Required**.
| `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled. | `unfilledtimeout.sell` | 10 | **Required.** How long (in minutes) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled.
| `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance). | `bid_strategy.ask_last_balance` | 0.0 | **Required.** Set the bidding price. More information [below](#understand-ask_last_balance).
| `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids. | `bid_strategy.use_order_book` | false | Allows buying of pair using the rates in Order Book Bids.
| `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. Ie. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids. | `bid_strategy.order_book_top` | 0 | Bot will use the top N rate in Order Book Bids. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in Order Book Bids.
| `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book. | `bid_strategy. check_depth_of_market.enabled` | false | Does not buy if the % difference of buy orders and sell orders is met in Order Book.
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher. | `bid_strategy. check_depth_of_market.bids_to_ask_delta` | 0 | The % difference of buy orders and sell orders found in Order Book. A value lesser than 1 means sell orders is greater, while value greater than 1 means buy orders is higher.
| `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks. | `ask_strategy.use_order_book` | false | Allows selling of open traded pair using the rates in Order Book Asks.
| `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_min` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate. | `ask_strategy.order_book_max` | 0 | Bot will scan from the top min to max Order Book Asks searching for a profitable rate.
| `ask_strategy.use_sell_signal` | true | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
| `ask_strategy.sell_profit_only` | false | Wait until the bot makes a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
| `ask_strategy.ignore_roi_if_buy_signal` | false | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
| `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy). | `order_types` | None | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).
| `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). | `order_time_in_force` | None | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy).
| `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). | `exchange.name` | | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename).
| `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details. | `exchange.sandbox` | false | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.
| `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. | `exchange.key` | '' | API key to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
| `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. | `exchange.secret` | '' | API secret to use for the exchange. Only required when you are in production mode. ***Keep it in secrete, do not disclose publicly.***
| `exchange.password` | '' | API password to use for the exchange. Only required when you are in production mode and for exchanges that use password for API requests. ***Keep it in secrete, do not disclose publicly.***
| `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_whitelist` | [] | List of pairs to use by the bot for trading and to check for potential trades during backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)). | `exchange.pair_blacklist` | [] | List of pairs the bot must absolutely avoid for trading and backtesting. Can be overriden by dynamic pairlists (see [below](#dynamic-pairlists)).
| `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_config` | None | Additional CCXT parameters passed to the regular ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
| `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation) | `exchange.ccxt_async_config` | None | Additional CCXT parameters passed to the async ccxt instance. Parameters may differ from exchange to exchange and are documented in the [ccxt documentation](https://ccxt.readthedocs.io/en/latest/manual.html#instantiation)
| `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded. | `exchange.markets_refresh_interval` | 60 | The interval in minutes in which markets are reloaded.
| `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation. | `edge` | false | Please refer to [edge configuration document](edge.md) for detailed explanation.
| `experimental.use_sell_signal` | false | Use your sell strategy in addition of the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy).
| `experimental.sell_profit_only` | false | Waits until you have made a positive profit before taking a sell decision. [Strategy Override](#parameters-in-the-strategy).
| `experimental.ignore_roi_if_buy_signal` | false | Does not sell if the buy-signal is still active. Takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy).
| `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now. | `experimental.block_bad_exchanges` | true | Block exchanges known to not work with freqtrade. Leave on default unless you want to test if that exchange works now.
| `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists). | `pairlist.method` | StaticPairList | Use static or dynamic volume-based pairlist. [More information below](#dynamic-pairlists).
| `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists). | `pairlist.config` | None | Additional configuration for dynamic pairlists. [More information below](#dynamic-pairlists).
| `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram. | `telegram.enabled` | true | **Required.** Enable or not the usage of Telegram.
| `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. | `telegram.token` | token | Your Telegram bot token. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
| `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. | `telegram.chat_id` | chat_id | Your personal Telegram account id. Only required if `telegram.enabled` is `true`. ***Keep it in secrete, do not disclose publicly.***
| `webhook.enabled` | false | Enable usage of Webhook notifications | `webhook.enabled` | false | Enable usage of Webhook notifications
| `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details. | `webhook.url` | false | URL for the webhook. Only required if `webhook.enabled` is `true`. See the [webhook documentation](webhook-config.md) for more details.
| `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details. | `webhook.webhookbuy` | false | Payload to send on buy. Only required if `webhook.enabled` is `true`. See the [webhook documentationV](webhook-config.md) for more details.
@@ -89,6 +98,7 @@ Mandatory parameters are marked as **Required**.
| `strategy` | DefaultStrategy | Defines Strategy class to use. | `strategy` | DefaultStrategy | Defines Strategy class to use.
| `strategy_path` | null | Adds an additional strategy lookup path (must be a directory). | `strategy_path` | null | Adds an additional strategy lookup path (must be a directory).
| `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second. | `internals.process_throttle_secs` | 5 | **Required.** Set the process throttle. Value in second.
| `internals.heartbeat_interval` | 60 | Print heartbeat message every X seconds. Set to 0 to disable heartbeat messages.
| `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details. | `internals.sd_notify` | false | Enables use of the sd_notify protocol to tell systemd service manager about changes in the bot state and issue keep-alive pings. See [here](installation.md#7-optional-configure-freqtrade-as-a-systemd-service) for more details.
| `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file. | `logfile` | | Specify Logfile. Uses a rolling strategy of 10 files, with 1Mb per file.
| `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`. | `user_data_dir` | cwd()/user_data | Directory containing user data. Defaults to `./user_data/`.
@@ -98,8 +108,6 @@ Mandatory parameters are marked as **Required**.
The following parameters can be set in either configuration file or strategy. The following parameters can be set in either configuration file or strategy.
Values set in the configuration file always overwrite values set in the strategy. Values set in the configuration file always overwrite values set in the strategy.
* `stake_currency`
* `stake_amount`
* `ticker_interval` * `ticker_interval`
* `minimal_roi` * `minimal_roi`
* `stoploss` * `stoploss`
@@ -109,9 +117,9 @@ Values set in the configuration file always overwrite values set in the strategy
* `process_only_new_candles` * `process_only_new_candles`
* `order_types` * `order_types`
* `order_time_in_force` * `order_time_in_force`
* `use_sell_signal` (experimental) * `use_sell_signal` (ask_strategy)
* `sell_profit_only` (experimental) * `sell_profit_only` (ask_strategy)
* `ignore_roi_if_buy_signal` (experimental) * `ignore_roi_if_buy_signal` (ask_strategy)
### Understand stake_amount ### Understand stake_amount
@@ -191,19 +199,20 @@ end up paying more then would probably have been necessary.
### Understand order_types ### Understand order_types
The `order_types` configuration parameter contains a dict mapping order-types to The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
market-types as well as stoploss on or off exchange type and stoploss on exchange
update interval in seconds. This allows to buy using limit orders, sell using
limit-orders, and create stoploss orders using market. It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once
the buy order is fulfilled. In case stoploss on exchange and `trailing_stop` are
both set, then the bot will use `stoploss_on_exchange_interval` to check it periodically
and update it if necessary (e.x. in case of trailing stoploss).
This can be set in the configuration file or in the strategy.
Values set in the configuration file overwrites values set in the strategy.
If this is configured, all 4 values (`buy`, `sell`, `stoploss` and This allows to buy using limit orders, sell using
`stoploss_on_exchange`) need to be present, otherwise the bot will warn about it and fail to start. limit-orders, and create stoplosses using using market orders. It also allows to set the
stoploss "on exchange" which means stoploss order would be placed immediately once
the buy order is fulfilled.
If `stoploss_on_exchange` and `trailing_stop` are both set, then the bot will use `stoploss_on_exchange_interval` to check and update the stoploss on exchange periodically.
`order_types` can be set in the configuration file or in the strategy.
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise the bot will fail to start.
`emergencysell` is an optional value, which defaults to `market` and is used when creating stoploss on exchange orders fails.
The below is the default which is used if this is not configured in either strategy or configuration file. The below is the default which is used if this is not configured in either strategy or configuration file.
Syntax for Strategy: Syntax for Strategy:
@@ -212,6 +221,7 @@ Syntax for Strategy:
order_types = { order_types = {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": False, "stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@@ -224,6 +234,7 @@ Configuration:
"order_types": { "order_types": {
"buy": "limit", "buy": "limit",
"sell": "limit", "sell": "limit",
"emergencysell": "market",
"stoploss": "market", "stoploss": "market",
"stoploss_on_exchange": false, "stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60 "stoploss_on_exchange_interval": 60
@@ -238,11 +249,13 @@ Configuration:
!!! Note !!! Note
Stoploss on exchange interval is not mandatory. Do not change its value if you are Stoploss on exchange interval is not mandatory. Do not change its value if you are
unsure of what you are doing. For more information about how stoploss works please unsure of what you are doing. For more information about how stoploss works please
read [the stoploss documentation](stoploss.md). refer to [the stoploss documentation](stoploss.md).
!!! Note !!! Note
In case of stoploss on exchange if the stoploss is cancelled manually then If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new order.
the bot would recreate one.
!!! Warning stoploss_on_exchange failures
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however this is not advised.
### Understand order_time_in_force ### Understand order_time_in_force

View File

@@ -61,37 +61,10 @@ except:
print(Path.cwd()) print(Path.cwd())
``` ```
## Load existing objects into a Jupyter notebook
These examples assume that you have already generated data using the cli. They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload.
### Load backtest results into a pandas dataframe
```python
from freqtrade.data.btanalysis import load_backtest_data
# Load backtest results
df = load_backtest_data("user_data/backtest_results/backtest-result.json")
# Show value-counts per pair
df.groupby("pair")["sell_reason"].value_counts()
```
### Load live trading results into a pandas dataframe
``` python
from freqtrade.data.btanalysis import load_trades_from_db
# Fetch trades from database
df = load_trades_from_db("sqlite:///tradesv3.sqlite")
# Display results
df.groupby("pair")["sell_reason"].value_counts()
```
### Load multiple configuration files ### Load multiple configuration files
This option can be useful to inspect the results of passing in multiple configs This option can be useful to inspect the results of passing in multiple configs.
This will also run through the whole Configuration initialization, so the configuration is completely initialized to be passed to other methods.
``` python ``` python
import json import json
@@ -101,102 +74,21 @@ from freqtrade.configuration import Configuration
config = Configuration.from_files(["config1.json", "config2.json"]) config = Configuration.from_files(["config1.json", "config2.json"])
# Show the config in memory # Show the config in memory
print(json.dumps(config, indent=1)) print(json.dumps(config['original_config'], indent=2))
``` ```
### Load exchange data to a pandas dataframe For Interactive environments, have an additional configuration specifying `user_data_dir` and pass this in last, so you don't have to change directories while running the bot.
Best avoid relative paths, since this starts at the storage location of the jupyter notebook, unless the directory is changed.
This loads candle data to a dataframe ``` json
{
```python "user_data_dir": "~/.freqtrade/"
from pathlib import Path }
from freqtrade.data.history import load_pair_history
# Load data using values passed to function
ticker_interval = "5m"
data_location = Path('user_data', 'data', 'bitrex')
pair = "BTC_USDT"
candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval,
pair=pair)
# Confirm success
print(f"Loaded len(candles) rows of data for {pair} from {data_location}")
candles.head()
``` ```
## Strategy debugging example ### Further Data analysis documentation
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data. * [Strategy debugging](strategy_analysis_example.md) - also available as Jupyter notebook (`user_data/notebooks/strategy_analysis_example.ipynb`)
* [Plotting](plotting.md)
### Define variables used in analyses
You can override strategy settings as demonstrated below.
```python
# Customize these according to your needs.
# Define some constants
ticker_interval = "5m"
# Name of the strategy class
strategy_name = 'TestStrategy'
# Path to user data
user_data_dir = 'user_data'
# Location of the strategy
strategy_location = Path(user_data_dir, 'strategies')
# Location of the data
data_location = Path(user_data_dir, 'data', 'binance')
# Pair to analyze - Only use one pair here
pair = "BTC_USDT"
```
### Load exchange data
```python
from pathlib import Path
from freqtrade.data.history import load_pair_history
# Load data using values set above
candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval,
pair=pair)
# Confirm success
print(f"Loaded {len(candles)} rows of data for {pair} from {data_location}")
candles.head()
```
### Load and run strategy
* Rerun each time the strategy file is changed
```python
from freqtrade.resolvers import StrategyResolver
# Load strategy using values set above
strategy = StrategyResolver({'strategy': strategy_name,
'user_data_dir': user_data_dir,
'strategy_path': strategy_location}).strategy
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})
```
### Display the trade details
* Note that using `data.tail()` is preferable to `data.head()` as most indicators have some "startup" data at the top of the dataframe.
* Some possible problems
* Columns with NaN values at the end of the dataframe
* Columns used in `crossed*()` functions with completely different units
* Comparison with full backtest
* having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.
* Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available.
```python
# Report results
print(f"Generated {df['buy'].sum()} buy signals")
data = df.set_index('date', drop=True)
data.tail()
```
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data. Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.

88
docs/data-download.md Normal file
View File

@@ -0,0 +1,88 @@
# Data Downloading
## Getting data for backtesting and hyperopt
To download data (candles / OHLCV) needed for backtesting and hyperoptimization use the `freqtrade download-data` command.
If no additional parameter is specified, freqtrade will download data for `"1m"` and `"5m"` timeframes for the last 30 days.
Exchange and pairs will come from `config.json` (if specified using `-c/--config`).
Otherwise `--exchange` becomes mandatory.
!!! Tip Updating existing data
If you already have backtesting data available in your data-directory and would like to refresh this data up to today, use `--days xx` with a number slightly higher than the missing number of days. Freqtrade will keep the available data and only download the missing data.
Be carefull though: If the number is too small (which would result in a few missing days), the whole dataset will be removed and only xx days will be downloaded.
### Pairs file
In alternative to the whitelist from `config.json`, a `pairs.json` file can be used.
If you are using Binance for example:
- create a directory `user_data/data/binance` and copy or create the `pairs.json` file in that directory.
- update the `pairs.json` file to contain the currency pairs you are interested in.
```bash
mkdir -p user_data/data/binance
cp freqtrade/tests/testdata/pairs.json user_data/data/binance
```
The format of the `pairs.json` file is a simple json list.
Mixing different stake-currencies is allowed for this file, since it's only used for downloading.
``` json
[
"ETH/BTC",
"ETH/USDT",
"BTC/USDT",
"XRP/ETH"
]
```
### Start download
Then run:
```bash
freqtrade download-data --exchange binance
```
This will download ticker data for all the currency pairs you defined in `pairs.json`.
### Other Notes
- To use a different directory than the exchange specific default, use `--datadir user_data/data/some_directory`.
- To change the exchange used to download the tickers, please use a different configuration file (you'll probably need to adjust ratelimits etc.)
- To use `pairs.json` from some other directory, use `--pairs-file some_other_dir/pairs.json`.
- To download ticker data for only 10 days, use `--days 10` (defaults to 30 days).
- Use `--timeframes` to specify which tickers to download. Default is `--timeframes 1m 5m` which will download 1-minute and 5-minute tickers.
- To use exchange, timeframe and list of pairs as defined in your configuration file, use the `-c/--config` option. With this, the script uses the whitelist defined in the config as the list of currency pairs to download data for and does not require the pairs.json file. You can combine `-c/--config` with most other options.
### Trades (tick) data
By default, `download-data` subcommand downloads Candles (OHLCV) data. Some exchanges also provide historic trade-data via their API.
This data can be useful if you need many different timeframes, since it is only downloaded once, and then resampled locally to the desired timeframes.
Since this data is large by default, the files use gzip by default. They are stored in your data-directory with the naming convention of `<pair>-trades.json.gz` (`ETH_BTC-trades.json.gz`). Incremental mode is also supported, as for historic OHLCV data, so downloading the data once per week with `--days 8` will create an incremental data-repository.
To use this mode, simply add `--dl-trades` to your call. This will swap the download method to download trades, and resamples the data locally.
Example call:
```bash
freqtrade download-data --exchange binance --pairs XRP/ETH ETH/BTC --days 20 --dl-trades
```
!!! Note
While this method uses async calls, it will be slow, since it requires the result of the previous call to generate the next request to the exchange.
!!! Warning
The historic trades are not available during Freqtrade dry-run and live trade modes because all exchanges tested provide this data with a delay of few 100 candles, so it's not suitable for real-time trading.
### Historic Kraken data
The Kraken API does only provide 720 historic candles, which is sufficient for FreqTrade dry-run and live trade modes, but is a problem for backtesting.
To download data for the Kraken exchange, using `--dl-trades` is mandatory, otherwise the bot will download the same 720 candles over and over, and you'll not have enough backtest data.
## Next step
Great, you now have backtest data downloaded, so you can now start [backtesting](backtesting.md) your strategy.

View File

@@ -4,7 +4,7 @@ This page contains description of the command line arguments, configuration para
and the bot features that were declared as DEPRECATED by the bot development team and the bot features that were declared as DEPRECATED by the bot development team
and are no longer supported. Please avoid their usage in your configuration. and are no longer supported. Please avoid their usage in your configuration.
## Deprecated ## Removed features
### the `--refresh-pairs-cached` command line option ### the `--refresh-pairs-cached` command line option
@@ -12,9 +12,7 @@ and are no longer supported. Please avoid their usage in your configuration.
Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out Since this leads to much confusion, and slows down backtesting (while not being part of backtesting) this has been singled out
as a seperate freqtrade subcommand `freqtrade download-data`. as a seperate freqtrade subcommand `freqtrade download-data`.
This command line option was deprecated in `2019.7-dev` and will be removed after the next release. This command line option was deprecated in 2019.7-dev (develop branch) and removed in 2019.9 (master branch).
## Removed features
### The **--dynamic-whitelist** command line option ### The **--dynamic-whitelist** command line option

View File

@@ -2,7 +2,7 @@
This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running. This page is intended for developers of FreqTrade, people who want to contribute to the FreqTrade codebase or documentation, or people who want to understand the source code of the application they're running.
All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) where you can ask questions. All contributions, bug reports, bug fixes, documentation improvements, enhancements and ideas are welcome. We [track issues](https://github.com/freqtrade/freqtrade/issues) on [GitHub](https://github.com) and also have a dev channel in [slack](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) where you can ask questions.
## Documentation ## Documentation
@@ -30,7 +30,7 @@ These are available from `conftest.py` and can be imported in any test module.
A sample check looks as follows: A sample check looks as follows:
``` python ``` python
from freqtrade.tests.conftest import log_has, log_has_re from tests.conftest import log_has, log_has_re
def test_method_to_test(caplog): def test_method_to_test(caplog):
method_to_test() method_to_test()
@@ -38,8 +38,48 @@ def test_method_to_test(caplog):
assert log_has("This event happened", caplog) assert log_has("This event happened", caplog)
# Check regex with trailing number ... # Check regex with trailing number ...
assert log_has_re(r"This dynamic event happened and produced \d+", caplog) assert log_has_re(r"This dynamic event happened and produced \d+", caplog)
``` ```
### Local docker usage
The fastest and easiest way to start up is to use docker-compose.develop which gives developers the ability to start the bot up with all the required dependencies, *without* needing to install any freqtrade specific dependencies on your local machine.
#### Install
* [git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [docker](https://docs.docker.com/install/)
* [docker-compose](https://docs.docker.com/compose/install/)
#### Starting the bot
##### Use the develop dockerfile
``` bash
rm docker-compose.yml && mv docker-compose.develop.yml docker-compose.yml
```
#### Docker Compose
##### Starting
``` bash
docker-compose up
```
![Docker compose up](https://user-images.githubusercontent.com/419355/65456322-47f63a80-de06-11e9-90c6-3c74d1bad0b8.png)
##### Rebuilding
``` bash
docker-compose build
```
##### Execing (effectively SSH into the container)
The `exec` command requires that the container already be running, if you want to start it
that can be effected by `docker-compose up` or `docker-compose run freqtrade_develop`
``` bash
docker-compose exec freqtrade_develop /bin/bash
```
![image](https://user-images.githubusercontent.com/419355/65456522-ba671a80-de06-11e9-9598-df9ca0d8dcac.png)
## Modules ## Modules
### Dynamic Pairlist ### Dynamic Pairlist
@@ -149,6 +189,15 @@ print(datetime.utcnow())
The output will show the last entry from the Exchange as well as the current UTC date. The output will show the last entry from the Exchange as well as the current UTC date.
If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above). If the day shows the same day, then the last candle can be assumed as incomplete and should be dropped (leave the setting `"ohlcv_partial_candle"` from the exchange-class untouched / True). Otherwise, set `"ohlcv_partial_candle"` to `False` to not drop Candles (shown in the example above).
## Updating example notebooks
To keep the jupyter notebooks aligned with the documentation, the following should be ran after updating a example notebook.
``` bash
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --inplace user_data/notebooks/strategy_analysis_example.ipynb
jupyter nbconvert --ClearOutputPreprocessor.enabled=True --to markdown user_data/notebooks/strategy_analysis_example.ipynb --stdout > docs/strategy_analysis_example.md
```
## Creating a release ## Creating a release
This part of the documentation is aimed at maintainers, and shows how to create a release. This part of the documentation is aimed at maintainers, and shows how to create a release.
@@ -182,7 +231,7 @@ git log --oneline --no-decorate --no-merges master..develop
Once the PR against master is merged (best right after merging): Once the PR against master is merged (best right after merging):
* Use the button "Draft a new release" in the Github UI (subsection releases) * Use the button "Draft a new release" in the Github UI (subsection releases)
* Use the version-number specified as tag. * Use the version-number specified as tag.
* Use "master" as reference (this step comes after the above PR is merged). * Use "master" as reference (this step comes after the above PR is merged).
* Use the above changelog as release comment (as codeblock) * Use the above changelog as release comment (as codeblock)

View File

@@ -249,13 +249,10 @@ freqtrade edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
freqtrade edge --timerange=20181110-20181113 freqtrade edge --timerange=20181110-20181113
``` ```
Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop. Doing `--timerange=-20190901` will get all available data until September 1st (excluding September 1st 2019).
The full timerange specification: The full timerange specification:
* Use last 123 tickframes of data: `--timerange=-123`
* Use first 123 tickframes of data: `--timerange=123-`
* Use tickframes from line 123 through 456: `--timerange=123-456`
* Use tickframes till 2018/01/31: `--timerange=-20180131` * Use tickframes till 2018/01/31: `--timerange=-20180131`
* Use tickframes since 2018/01/31: `--timerange=20180131-` * Use tickframes since 2018/01/31: `--timerange=20180131-`
* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301` * Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`

View File

@@ -38,7 +38,7 @@ like pauses. You can stop your bot, adjust settings and start it again.
### I want to improve the bot with a new strategy ### I want to improve the bot with a new strategy
That's great. We have a nice backtesting and hyperoptimizing setup. See That's great. We have a nice backtesting and hyperoptimization setup. See
the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands). the tutorial [here|Testing-new-strategies-with-Hyperopt](bot-usage.md#hyperopt-commands).
### Is there a setting to only SELL the coins being held and not perform anymore BUYS? ### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
@@ -59,7 +59,7 @@ If you're a US customer, the bot will fail to create orders for these pairs, and
### How many epoch do I need to get a good Hyperopt result? ### How many epoch do I need to get a good Hyperopt result?
Per default Hyperopts without `-e` or `--epochs` parameter will only Per default Hyperopt called without the `-e`/`--epochs` command line option will only
run 100 epochs, means 100 evals of your triggers, guards, ... Too few run 100 epochs, means 100 evals of your triggers, guards, ... Too few
to find a great result (unless if you are very lucky), so you probably to find a great result (unless if you are very lucky), so you probably
have to run it for 10.000 or more. But it will take an eternity to have to run it for 10.000 or more. But it will take an eternity to

View File

@@ -6,13 +6,16 @@ algorithms included in the `scikit-optimize` package to accomplish this. The
search will burn all your CPU cores, make your laptop sound like a fighter jet search will burn all your CPU cores, make your laptop sound like a fighter jet
and still take a long time. and still take a long time.
Hyperopt requires historic data to be available, just as backtesting does.
To learn how to get data for the pairs and exchange you're interrested in, head over to the [Data Downloading](data-download.md) section of the documentation.
!!! Bug !!! Bug
Hyperopt will crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133) Hyperopt can crash when used with only 1 CPU Core as found out in [Issue #1133](https://github.com/freqtrade/freqtrade/issues/1133)
## Prepare Hyperopting ## Prepare Hyperopting
Before we start digging into Hyperopt, we recommend you to take a look at Before we start digging into Hyperopt, we recommend you to take a look at
an example hyperopt file located into [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py) the sample hyperopt file located in [user_data/hyperopts/](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt.py).
Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy. Configuring hyperopt is similar to writing your own strategy, and many tasks will be similar and a lot of code can be copied across from the strategy.
@@ -61,9 +64,9 @@ multiple guards. The constructed strategy will be something like
"*buy exactly when close price touches lower bollinger band, BUT only if "*buy exactly when close price touches lower bollinger band, BUT only if
ADX > 10*". ADX > 10*".
If you have updated the buy strategy, ie. changed the contents of If you have updated the buy strategy, i.e. changed the contents of
`populate_buy_trend()` method you have to update the `guards` and `populate_buy_trend()` method, you have to update the `guards` and
`triggers` hyperopts must use. `triggers` your hyperopt must use correspondingly.
#### Sell optimization #### Sell optimization
@@ -79,7 +82,7 @@ To avoid naming collisions in the search-space, please prefix all sell-spaces wi
#### Using ticker-interval as part of the Strategy #### Using ticker-interval as part of the Strategy
The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`. The Strategy exposes the ticker-interval as `self.ticker_interval`. The same value is available as class-attribute `HyperoptName.ticker_interval`.
In the case of the linked sample-value this would be `SampleHyperOpts.ticker_interval`. In the case of the linked sample-value this would be `SampleHyperOpt.ticker_interval`.
## Solving a Mystery ## Solving a Mystery
@@ -372,35 +375,42 @@ Buy hyperspace params:
'rsi-enabled': True, 'rsi-enabled': True,
'trigger': 'bb_lower'} 'trigger': 'bb_lower'}
ROI table: ROI table:
{ 0: 0.10674752302642071, { 0: 0.10674,
21: 0.09158372701087236, 21: 0.09158,
78: 0.03634636907306948, 78: 0.03634,
118: 0} 118: 0}
``` ```
This would translate to the following ROI table: In order to use this best ROI table found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `minimal_roi` attribute of your custom strategy:
``` python ```
minimal_roi = { # Minimal ROI designed for the strategy.
"118": 0, # This attribute will be overridden if the config file contains "minimal_roi"
"78": 0.0363, minimal_roi = {
"21": 0.0915, 0: 0.10674,
"0": 0.106 21: 0.09158,
78: 0.03634,
118: 0
} }
``` ```
As stated in the comment, you can also use it as the value of the `minimal_roi` setting in the configuration file.
If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps) with the values that can vary in the following ranges: #### Default ROI Search Space
| # | minutes | ROI percentage | If you are optimizing ROI, Freqtrade creates the 'roi' optimization hyperspace for you -- it's the hyperspace of components for the ROI tables. By default, each ROI table generated by the Freqtrade consists of 4 rows (steps). Hyperopt implements adaptive ranges for ROI tables with ranges for values in the ROI steps that depend on the ticker_interval used. By default the values can vary in the following ranges (for some of the most used ticker intervals, values are rounded to 5 digits after the decimal point):
|---|---|---|
| 1 | always 0 | 0.03...0.31 |
| 2 | 10...40 | 0.02...0.11 |
| 3 | 20...100 | 0.01...0.04 |
| 4 | 30...220 | always 0 |
This structure of the ROI table is sufficient in most cases. Override the `roi_space()` method defining the ranges desired if you need components of the ROI tables to vary in other ranges. | # step | 1m | | 5m | | 1h | | 1d | |
|---|---|---|---|---|---|---|---|---|
| 1 | 0 | 0.01161...0.11992 | 0 | 0.03...0.31 | 0 | 0.06883...0.71124 | 0 | 0.12178...1.25835 |
| 2 | 2...8 | 0.00774...0.04255 | 10...40 | 0.02...0.11 | 120...480 | 0.04589...0.25238 | 2880...11520 | 0.08118...0.44651 |
| 3 | 4...20 | 0.00387...0.01547 | 20...100 | 0.01...0.04 | 240...1200 | 0.02294...0.09177 | 5760...28800 | 0.04059...0.16237 |
| 4 | 6...44 | 0.0 | 30...220 | 0.0 | 360...2640 | 0.0 | 8640...63360 | 0.0 |
Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization in these methods if you need a different structure of the ROI table or other amount of rows (steps) in the ROI tables. These ranges should be sufficient in most cases. The minutes in the steps (ROI dict keys) are scaled linearly depending on the ticker interval used. The ROI values in the steps (ROI dict values) are scaled logarithmically depending on the ticker interval used.
If you have the `generate_roi_table()` and `roi_space()` methods in your custom hyperopt file, remove them in order to utilize these adaptive ROI tables and the ROI hyperoptimization space generated by Freqtrade by default.
Override the `roi_space()` method if you need components of the ROI tables to vary in other ranges. Override the `generate_roi_table()` and `roi_space()` methods and implement your own custom approach for generation of the ROI tables during hyperoptimization if you need a different structure of the ROI tables or other amount of rows (steps). A sample for these methods can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
### Understand Hyperopt Stoploss results ### Understand Hyperopt Stoploss results
@@ -417,12 +427,25 @@ Buy hyperspace params:
'adx-enabled': False, 'adx-enabled': False,
'rsi-enabled': True, 'rsi-enabled': True,
'trigger': 'bb_lower'} 'trigger': 'bb_lower'}
Stoploss: -0.37996664668703606 Stoploss: -0.27996
``` ```
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.5...-0.02, which is sufficient in most cases. In order to use this best stoploss value found by Hyperopt in backtesting and for live trades/dry-run, copy-paste it as the value of the `stoploss` attribute of your custom strategy:
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. ```
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.27996
```
As stated in the comment, you can also use it as the value of the `stoploss` setting in the configuration file.
#### Default Stoploss Search Space
If you are optimizing stoploss values, Freqtrade creates the 'stoploss' optimization hyperspace for you. By default, the stoploss values in that hyperspace can vary in the range -0.35...-0.02, which is sufficient in most cases.
If you have the `stoploss_space()` method in your custom hyperopt file, remove it in order to utilize Stoploss hyperoptimization space generated by Freqtrade by default.
Override the `stoploss_space()` method and define the desired range in it if you need stoploss values to vary in other range during hyperoptimization. A sample for this method can be found in [user_data/hyperopts/sample_hyperopt_advanced.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/hyperopts/sample_hyperopt_advanced.py).
### Validate backtesting results ### Validate backtesting results

View File

@@ -64,7 +64,7 @@ To run this bot we recommend you a cloud instance with a minimum of:
Help / Slack Help / Slack
For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel. For any questions not covered by the documentation or for further information about the bot, we encourage you to join our Slack channel.
Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) to join Slack channel. Click [here](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) to join Slack channel.
## Ready to try? ## Ready to try?

View File

@@ -99,8 +99,8 @@ sudo apt-get install build-essential git
Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/). Before installing FreqTrade on a Raspberry Pi running the official Raspbian Image, make sure you have at least Python 3.6 installed. The default image only provides Python 3.5. Probably the easiest way to get a recent version of python is [miniconda](https://repo.continuum.io/miniconda/).
The following assumes that miniconda3 is installed and available in your environment. Last miniconda3 installation file use python 3.4, we will update to python 3.6 on this installation. The following assumes that miniconda3 is installed and available in your environment. Since the last miniconda3 installation file uses python 3.4, we will update to python 3.6 on this installation.
It's recommended to use (mini)conda for this as installation/compilation of `numpy`, `scipy` and `pandas` takes a long time. It's recommended to use (mini)conda for this as installation/compilation of `numpy` and `pandas` takes a long time.
Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot). Additional package to install on your Raspbian, `libffi-dev` required by cryptography (from python-telegram-bot).
@@ -109,13 +109,17 @@ conda config --add channels rpi
conda install python=3.6 conda install python=3.6
conda create -n freqtrade python=3.6 conda create -n freqtrade python=3.6
conda activate freqtrade conda activate freqtrade
conda install scipy pandas numpy conda install pandas numpy
sudo apt install libffi-dev sudo apt install libffi-dev
python3 -m pip install -r requirements-common.txt python3 -m pip install -r requirements-common.txt
python3 -m pip install -e . python3 -m pip install -e .
``` ```
!!! Note
This does not install hyperopt dependencies. To install these, please use `python3 -m pip install -e .[hyperopt]`.
We do not advise to run hyperopt on a Raspberry Pi, since this is a very resource-heavy operation, which should be done on powerful machine.
### Common ### Common
#### 1. Install TA-Lib #### 1. Install TA-Lib
@@ -175,7 +179,6 @@ cp config.json.example config.json
``` bash ``` bash
python3 -m pip install --upgrade pip python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
python3 -m pip install -e . python3 -m pip install -e .
``` ```
@@ -254,14 +257,12 @@ As compiling from source on windows has heavy dependencies (requires a partial v
```cmd ```cmd
>cd \path\freqtrade-develop >cd \path\freqtrade-develop
>python -m venv .env >python -m venv .env
>cd .env\Scripts >.env\Scripts\activate.bat
>activate.bat
>cd \path\freqtrade-develop
REM optionally install ta-lib from wheel REM optionally install ta-lib from wheel
REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl REM >pip install TA_Lib0.4.17cp36cp36mwin32.whl
>pip install -r requirements.txt >pip install -r requirements.txt
>pip install -e . >pip install -e .
>python freqtrade\main.py >freqtrade
``` ```
> Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222) > Thanks [Owdr](https://github.com/Owdr) for the commands. Source: [Issue #222](https://github.com/freqtrade/freqtrade/issues/222)

View File

@@ -49,4 +49,6 @@
</nav> </nav>
<!-- Place this tag in your head or just before your close body tag. --> <!-- Place this tag in your head or just before your close body tag. -->
<script async defer src="https://buttons.github.io/buttons.js"></script> <script async defer src="https://buttons.github.io/buttons.js"></script>
</header> <script src="https://code.jquery.com/jquery-3.4.1.min.js"
integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>
</header>

View File

@@ -2,9 +2,9 @@
This page explains how to plot prices, indicators and profits. This page explains how to plot prices, indicators and profits.
## Installation ## Installation / Setup
Plotting scripts use Plotly library. Install/upgrade it with: Plotting modules use the Plotly library. You can install / upgrade this by running the following command:
``` bash ``` bash
pip install -U -r requirements-plot.txt pip install -U -r requirements-plot.txt
@@ -12,90 +12,172 @@ pip install -U -r requirements-plot.txt
## Plot price and indicators ## Plot price and indicators
Usage for the price plotter: The `freqtrade plot-dataframe` subcommand shows an interactive graph with three subplots:
* Main plot with candlestics and indicators following price (sma/ema)
* Volume bars
* Additional indicators as specified by `--indicators2`
![plot-dataframe](assets/plot-dataframe.png)
Possible arguments:
```
usage: freqtrade plot-dataframe [-h] [-p PAIRS [PAIRS ...]]
[--indicators1 INDICATORS1 [INDICATORS1 ...]]
[--indicators2 INDICATORS2 [INDICATORS2 ...]]
[--plot-limit INT] [--db-url PATH]
[--trade-source {DB,file}] [--export EXPORT]
[--export-filename PATH]
[--timerange TIMERANGE]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
separated.
--indicators1 INDICATORS1 [INDICATORS1 ...]
Set indicators from your strategy you want in the
first row of the graph. Space-separated list. Example:
`ema3 ema5`. Default: `['sma', 'ema3', 'ema5']`.
--indicators2 INDICATORS2 [INDICATORS2 ...]
Set indicators from your strategy you want in the
third row of the graph. Space-separated list. Example:
`fastd fastk`. Default: `['macd', 'macdsignal']`.
--plot-limit INT Specify tick limit for plotting. Notice: too high
values cause huge files. Default: 750.
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--trade-source {DB,file}
Specify the source for trades (Can be DB or file
(backtest file)) Default: file
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH
Save backtest results to the file with this filename
(default: `user_data/backtest_results/backtest-
result.json`). Requires `--export` to be set as well.
Example: `--export-filename=user_data/backtest_results
/backtest_today.json`
--timerange TIMERANGE
Specify what timerange of data to use.
``` bash
python3 script/plot_dataframe.py [-h] [-p pairs]
``` ```
Example Example:
``` bash ``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH freqtrade plot-dataframe -p BTC/ETH
``` ```
The `-p` pairs argument can be used to specify pairs you would like to plot. The `-p/--pairs` argument can be used to specify pairs you would like to plot.
!!! Note
The `freqtrade plot-dataframe` subcommand generates one plot-file per pair.
Specify custom indicators. Specify custom indicators.
Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices). Use `--indicators1` for the main plot and `--indicators2` for the subplot below (if values are in a different range than prices).
!!! tip
You will almost certainly want to specify a custom strategy! This can be done by adding `-s Classname` / `--strategy ClassName` to the command.
``` bash ``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH --indicators1 sma,ema --indicators2 macd freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --indicators1 sma ema --indicators2 macd
``` ```
### Advanced use ### Further usage examples
To plot multiple pairs, separate them with a comma: To plot multiple pairs, separate them with a space:
``` bash ``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH,XRP/ETH freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH XRP/ETH
``` ```
To plot a timerange (to zoom in): To plot a timerange (to zoom in)
``` bash ``` bash
python3 scripts/plot_dataframe.py -p BTC/ETH --timerange=20180801-20180805 freqtrade --strategy AwesomeStrategy plot-dataframe -p BTC/ETH --timerange=20180801-20180805
``` ```
To plot trades stored in a database use `--db-url` argument: To plot trades stored in a database use `--db-url` in combination with `--trade-source DB`:
``` bash ``` bash
python3 scripts/plot_dataframe.py --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB freqtrade --strategy AwesomeStrategy plot-dataframe --db-url sqlite:///tradesv3.dry_run.sqlite -p BTC/ETH --trade-source DB
``` ```
To plot trades from a backtesting result, use `--export-filename <filename>` To plot trades from a backtesting result, use `--export-filename <filename>`
``` bash ``` bash
python3 scripts/plot_dataframe.py --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH freqtrade --strategy AwesomeStrategy plot-dataframe --export-filename user_data/backtest_results/backtest-result.json -p BTC/ETH
```
To plot a custom strategy the strategy should have first be backtested.
The results may then be plotted with the -s argument:
``` bash
python3 scripts/plot_dataframe.py -s Strategy_Name -p BTC/ETH --datadir user_data/data/<exchange_name>/
``` ```
## Plot profit ## Plot profit
The profit plotter shows a picture with three plots: ![plot-profit](assets/plot-profit.png)
The `freqtrade plot-profit` subcommand shows an interactive graph with three plots:
1) Average closing price for all pairs 1) Average closing price for all pairs
2) The summarized profit made by backtesting. 2) The summarized profit made by backtesting.
Note that this is not the real-world profit, but Note that this is not the real-world profit, but more of an estimate.
more of an estimate. 3) Profit for each individual pair
3) Each pair individually profit
The first graph is good to get a grip of how the overall market The first graph is good to get a grip of how the overall market progresses.
progresses.
The second graph will show how your algorithm works or doesn't. The second graph will show if your algorithm works or doesn't.
Perhaps you want an algorithm that steadily makes small profits, Perhaps you want an algorithm that steadily makes small profits, or one that acts less often, but makes big swings.
or one that acts less seldom, but makes big swings.
The third graph can be useful to spot outliers, events in pairs The third graph can be useful to spot outliers, events in pairs that cause profit spikes.
that makes profit spikes.
Usage for the profit plotter: Possible options for the `freqtrade plot-profit` subcommand:
```
usage: freqtrade plot-profit [-h] [-p PAIRS [PAIRS ...]]
[--timerange TIMERANGE] [--export EXPORT]
[--export-filename PATH] [--db-url PATH]
[--trade-source {DB,file}]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
separated.
--timerange TIMERANGE
Specify what timerange of data to use.
--export EXPORT Export backtest results, argument are: trades.
Example: `--export=trades`
--export-filename PATH
Save backtest results to the file with this filename
(default: `user_data/backtest_results/backtest-
result.json`). Requires `--export` to be set as well.
Example: `--export-filename=user_data/backtest_results
/backtest_today.json`
--db-url PATH Override trades database URL, this is useful in custom
deployments (default: `sqlite:///tradesv3.sqlite` for
Live Run mode, `sqlite://` for Dry Run).
--trade-source {DB,file}
Specify the source for trades (Can be DB or file
(backtest file)) Default: file
``` bash
python3 script/plot_profit.py [-h] [-p pair] [--datadir directory] [--ticker_interval num]
``` ```
The `-p` pair argument, can be used to plot a single pair The `-p/--pairs` argument, can be used to limit the pairs that are considered for this calculation.
Example Examples:
Use custom backtest-export file
``` bash ``` bash
python3 scripts/plot_profit.py --datadir ../freqtrade/freqtrade/tests/testdata-20171221/ -p LTC/BTC freqtrade plot-profit -p LTC/BTC --export-filename user_data/backtest_results/backtest-result-Strategy005.json
```
Use custom database
``` bash
freqtrade plot-profit -p LTC/BTC --db-url sqlite:///tradesv3.sqlite --trade-source DB
```
``` bash
freqtrade --datadir user_data/data/binance_save/ plot-profit -p LTC/BTC
``` ```

View File

@@ -1 +1,2 @@
mkdocs-material==4.4.0 mkdocs-material==4.4.3
mdx_truly_sane_lists==1.2

View File

@@ -100,7 +100,6 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules. | `stopbuy` | | Stops the trader from opening new trades. Gracefully closes open trades according to their rules.
| `reload_conf` | | Reloads the configuration file | `reload_conf` | | Reloads the configuration file
| `status` | | Lists all open trades | `status` | | Lists all open trades
| `status table` | | List all open trades in a table format
| `count` | | Displays number of trades used and available | `count` | | Displays number of trades used and available
| `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance | `profit` | | Display a summary of your profit/loss from close trades and some stats about your performance
| `forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`). | `forcesell <trade_id>` | | Instantly sells the given trade (Ignoring `minimum_roi`).

View File

@@ -24,7 +24,7 @@ strategy file will be updated on Github. Put your custom strategy file
into the directory `user_data/strategies`. into the directory `user_data/strategies`.
Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes. Best copy the test-strategy and modify this copy to avoid having bot-updates override your changes.
`cp user_data/strategies/test_strategy.py user_data/strategies/awesome-strategy.py` `cp user_data/strategies/sample_strategy.py user_data/strategies/awesome-strategy.py`
### Anatomy of a strategy ### Anatomy of a strategy
@@ -36,14 +36,19 @@ A strategy file contains all the information needed to build a good strategy:
- Minimal ROI recommended - Minimal ROI recommended
- Stoploss strongly recommended - Stoploss strongly recommended
The bot also include a sample strategy called `TestStrategy` you can update: `user_data/strategies/test_strategy.py`. The bot also include a sample strategy called `SampleStrategy` you can update: `user_data/strategies/sample_strategy.py`.
You can test it with the parameter: `--strategy TestStrategy` You can test it with the parameter: `--strategy SampleStrategy`
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
The current version is 2 - which is also the default when it's not set explicitly in the strategy.
Future versions will require this to be set.
```bash ```bash
freqtrade --strategy AwesomeStrategy freqtrade --strategy AwesomeStrategy
``` ```
**For the following section we will use the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py) **For the following section we will use the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py)
file as reference.** file as reference.**
!!! Note Strategies and Backtesting !!! Note Strategies and Backtesting
@@ -55,8 +60,7 @@ file as reference.**
!!! Warning Using future data !!! Warning Using future data
Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author Since backtesting passes the full time interval to the `populate_*()` methods, the strategy author
needs to take care to avoid having the strategy utilize data from the future. needs to take care to avoid having the strategy utilize data from the future.
Samples for usage of future data are `dataframe.shift(-1)`, `dataframe.resample("1h")` (this uses the left border of the interval, so moves data from an hour to the start of the hour). Some common patterns for this are listed in the [Common Mistakes](#common-mistakes-when-developing-strategies) section of this document.
They all use data which is not available during regular operations, so these strategies will perform well during backtesting, but will fail / perform badly in dry-runs.
### Customize Indicators ### Customize Indicators
@@ -109,9 +113,8 @@ def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame
return dataframe return dataframe
``` ```
!!! Note "Want more indicator examples?" !!! Note "Want more indicator examples?"
Look into the [user_data/strategies/test_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/test_strategy.py).<br/> Look into the [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py).
Then uncomment indicators you need. Then uncomment indicators you need.
### Buy signal rules ### Buy signal rules
@@ -122,7 +125,7 @@ It's important to always return the dataframe without removing/modifying the col
This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action". This will method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/sample_strategy.py`:
```python ```python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -134,15 +137,19 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 30) & (qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['tema'] <= dataframe['bb_middleband']) & (dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
(dataframe['tema'] > dataframe['tema'].shift(1)) (dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'buy'] = 1 'buy'] = 1
return dataframe return dataframe
``` ```
!!! Note
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
### Sell signal rules ### Sell signal rules
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy. Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
@@ -152,7 +159,7 @@ It's important to always return the dataframe without removing/modifying the col
This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action". This will method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
Sample from `user_data/strategies/test_strategy.py`: Sample from `user_data/strategies/sample_strategy.py`:
```python ```python
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -164,9 +171,10 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
""" """
dataframe.loc[ dataframe.loc[
( (
(dataframe['adx'] > 70) & (qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
(dataframe['tema'] > dataframe['bb_middleband']) & (dataframe['tema'] > dataframe['bb_middleband']) & # Guard
(dataframe['tema'] < dataframe['tema'].shift(1)) (dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
), ),
'sell'] = 1 'sell'] = 1
return dataframe return dataframe
@@ -220,7 +228,7 @@ This would signify a stoploss of -10%.
For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md). For the full documentation on stoploss features, look at the dedicated [stoploss page](stoploss.md).
If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order dict, so your stoploss is on the exchange and cannot be missed for network-problems (or other problems). If your exchange supports it, it's recommended to also set `"stoploss_on_exchange"` in the order_types dictionary, so your stoploss is on the exchange and cannot be missed due to network problems, high load or other reasons.
For more information on order_types please look [here](configuration.md#understand-order_types). For more information on order_types please look [here](configuration.md#understand-order_types).
@@ -242,9 +250,9 @@ Instead, have a look at the section [Storing information](#Storing-information)
### Storing information ### Storing information
Storing information can be accomplished by crating a new dictionary within the strategy class. Storing information can be accomplished by creating a new dictionary within the strategy class.
The name of the variable can be choosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables. The name of the variable can be chosen at will, but should be prefixed with `cust_` to avoid naming collisions with predefined strategy variables.
```python ```python
class Awesomestrategy(IStrategy): class Awesomestrategy(IStrategy):
@@ -278,6 +286,8 @@ Please always check the mode of operation to select the correct method to get da
- `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame. - `ohlcv(pair, ticker_interval)` - Currently cached ticker data for the pair, returns DataFrame or empty DataFrame.
- `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk. - `historic_ohlcv(pair, ticker_interval)` - Returns historical data stored on disk.
- `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes). - `get_pair_dataframe(pair, ticker_interval)` - This is a universal method, which returns either historical data (for backtesting) or cached live data (for the Dry-Run and Live-Run modes).
- `orderbook(pair, maximum)` - Returns latest orderbook data for the pair, a dict with bids/asks with a total of `maximum` entries.
- `market(pair)` - Returns market data for the pair: fees, limits, precisions, activity flag, etc. See [ccxt documentation](https://github.com/ccxt/ccxt/wiki/Manual#markets) for more details on Market data structure.
- `runmode` - Property containing the current runmode. - `runmode` - Property containing the current runmode.
#### Example: fetch live ohlcv / historic data for the first informative pair #### Example: fetch live ohlcv / historic data for the first informative pair
@@ -340,9 +350,9 @@ def informative_pairs(self):
As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short. As these pairs will be refreshed as part of the regular whitelist refresh, it's best to keep this list short.
All intervals and all pairs can be specified as long as they are available (and active) on the used exchange. All intervals and all pairs can be specified as long as they are available (and active) on the used exchange.
It is however better to use resampling to longer time-intervals when possible It is however better to use resampling to longer time-intervals when possible
to avoid hammering the exchange with too many requests and risk beeing blocked. to avoid hammering the exchange with too many requests and risk being blocked.
### Additional data - Wallets ### Additional data (Wallets)
The strategy provides access to the `Wallets` object. This contains the current balances on the exchange. The strategy provides access to the `Wallets` object. This contains the current balances on the exchange.
@@ -388,10 +398,10 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds). Printing more than a few rows is also possible (simply use `print(dataframe)` instead of `print(dataframe.tail())`), however not recommended, as that will be very verbose (~500 lines per pair every 5 seconds).
### Where is the default strategy? ### Where can i find a strategy template?
The default buy strategy is located in the file The strategy template is located in the file
[freqtrade/default_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/freqtrade/strategy/default_strategy.py). [user_data/strategies/sample_strategy.py](https://github.com/freqtrade/freqtrade/blob/develop/user_data/strategies/sample_strategy.py).
### Specify custom strategy location ### Specify custom strategy location
@@ -401,13 +411,25 @@ If you want to use a strategy from a different directory you can pass `--strateg
freqtrade --strategy AwesomeStrategy --strategy-path /some/directory freqtrade --strategy AwesomeStrategy --strategy-path /some/directory
``` ```
### Common mistakes when developing strategies
Backtesting analyzes the whole time-range at once for performance reasons. Because of this, strategy authors need to make sure that strategies do not look-ahead into the future.
This is a common pain-point, which can cause huge differences between backtesting and dry/live run methods, since they all use data which is not available during dry/live runs, so these strategies will perform well during backtesting, but will fail / perform badly in real conditions.
The following lists some common patterns which should be avoided to prevent frustration:
- don't use `shift(-1)`. This uses data from the future, which is not available.
- don't use `.iloc[-1]` or any other absolute position in the dataframe, this will be different between dry-run and backtesting.
- don't use `dataframe['volume'].mean()`. This uses the full DataFrame for backtesting, including data from the future. Use `dataframe['volume'].rolling(<window>).mean()` instead
- don't use `.resample('1h')`. This uses the left border of the interval, so moves data from an hour to the start of the hour. Use `.resample('1h', label='right')` instead.
### Further strategy ideas ### Further strategy ideas
To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk. To get additional Ideas for strategies, head over to our [strategy repository](https://github.com/freqtrade/freqtrade-strategies). Feel free to use them as they are - but results will depend on the current market situation, pairs used etc. - therefore please backtest the strategy for your exchange/desired pairs first, evaluate carefully, use at your own risk.
Feel free to use any of them as inspiration for your own strategies. Feel free to use any of them as inspiration for your own strategies.
We're happy to accept Pull Requests containing new Strategies to that repo. We're happy to accept Pull Requests containing new Strategies to that repo.
We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LWEyODBiNzkzNzcyNzU0MWYyYzE5NjIyOTQxMzBmMGUxOTIzM2YyN2Y4NWY1YTEwZDgwYTRmMzE2NmM5ZmY2MTg) which is a great place to get and/or share ideas. We also got a *strategy-sharing* channel in our [Slack community](https://join.slack.com/t/highfrequencybot/shared_invite/enQtNjU5ODcwNjI1MDU3LTU1MTgxMjkzNmYxNWE1MDEzYzQ3YmU4N2MwZjUyNjJjODRkMDVkNjg4YTAyZGYzYzlhOTZiMTE4ZjQ4YzM0OGE) which is a great place to get and/or share ideas.
## Next step ## Next step

View File

@@ -0,0 +1,142 @@
# Strategy analysis example
Debugging a strategy can be time-consuming. FreqTrade offers helper functions to visualize raw data.
## Setup
```python
from pathlib import Path
# Customize these according to your needs.
# Define some constants
ticker_interval = "5m"
# Name of the strategy class
strategy_name = 'SampleStrategy'
# Path to user data
user_data_dir = Path('user_data')
# Location of the strategy
strategy_location = user_data_dir / 'strategies'
# Location of the data
data_location = Path(user_data_dir, 'data', 'binance')
# Pair to analyze - Only use one pair here
pair = "BTC_USDT"
```
```python
# Load data using values set above
from freqtrade.data.history import load_pair_history
candles = load_pair_history(datadir=data_location,
ticker_interval=ticker_interval,
pair=pair)
# Confirm success
print("Loaded " + str(len(candles)) + f" rows of data for {pair} from {data_location}")
candles.head()
```
## Load and run strategy
* Rerun each time the strategy file is changed
```python
# Load strategy using values set above
from freqtrade.resolvers import StrategyResolver
strategy = StrategyResolver({'strategy': strategy_name,
'user_data_dir': user_data_dir,
'strategy_path': strategy_location}).strategy
# Generate buy/sell signals using strategy
df = strategy.analyze_ticker(candles, {'pair': pair})
df.tail()
```
### Display the trade details
* Note that using `data.head()` would also work, however most indicators have some "startup" data at the top of the dataframe.
* Some possible problems
* Columns with NaN values at the end of the dataframe
* Columns used in `crossed*()` functions with completely different units
* Comparison with full backtest
* having 200 buy signals as output for one pair from `analyze_ticker()` does not necessarily mean that 200 trades will be made during backtesting.
* Assuming you use only one condition such as, `df['rsi'] < 30` as buy condition, this will generate multiple "buy" signals for each pair in sequence (until rsi returns > 29). The bot will only buy on the first of these signals (and also only if a trade-slot ("max_open_trades") is still available), or on one of the middle signals, as soon as a "slot" becomes available.
```python
# Report results
print(f"Generated {df['buy'].sum()} buy signals")
data = df.set_index('date', drop=False)
data.tail()
```
## Load existing objects into a Jupyter notebook
The following cells assume that you have already generated data using the cli.
They will allow you to drill deeper into your results, and perform analysis which otherwise would make the output very difficult to digest due to information overload.
### Load backtest results to pandas dataframe
Analyze a trades dataframe (also used below for plotting)
```python
from freqtrade.data.btanalysis import load_backtest_data
# Load backtest results
trades = load_backtest_data(user_data_dir / "backtest_results/backtest-result.json")
# Show value-counts per pair
trades.groupby("pair")["sell_reason"].value_counts()
```
### Load live trading results into a pandas dataframe
In case you did already some trading and want to analyze your performance
```python
from freqtrade.data.btanalysis import load_trades_from_db
# Fetch trades from database
trades = load_trades_from_db("sqlite:///tradesv3.sqlite")
# Display results
trades.groupby("pair")["sell_reason"].value_counts()
```
## Plot results
Freqtrade offers interactive plotting capabilities based on plotly.
```python
from freqtrade.plot.plotting import generate_candlestick_graph
# Limit graph period to keep plotly quick and reactive
data_red = data['2019-06-01':'2019-06-10']
# Generate candlestick graph
graph = generate_candlestick_graph(pair=pair,
data=data_red,
trades=trades,
indicators1=['sma20', 'ema50', 'ema55'],
indicators2=['rsi', 'macd', 'macdsignal', 'macdhist']
)
```
```python
# Show graph inline
# graph.show()
# Render graph in a seperate window
graph.show(renderer="browser")
```
Feel free to submit an issue or Pull Request enhancing this document if you would like to share ideas on how to best analyze the data.

View File

@@ -0,0 +1,13 @@
.rst-versions {
font-size: .7rem;
color: white;
}
.rst-versions.rst-badge .rst-current-version {
font-size: .7rem;
color: white;
}
.rst-versions .rst-other-versions {
color: white;
}

126
docs/utils.md Normal file
View File

@@ -0,0 +1,126 @@
# Utility Subcommands
Besides the Live-Trade and Dry-Run run modes, the `backtesting`, `edge` and `hyperopt` optimization subcommands, and the `download-data` subcommand which prepares historical data, the bot contains a number of utility subcommands. They are described in this section.
## List Exchanges
Use the `list-exchanges` subcommand to see the exchanges available for the bot.
```
usage: freqtrade list-exchanges [-h] [-1] [-a]
optional arguments:
-h, --help show this help message and exit
-1, --one-column Print output in one column.
-a, --all Print all exchanges known to the ccxt library.
```
* Example: see exchanges available for the bot:
```
$ freqtrade list-exchanges
Exchanges available for Freqtrade: _1btcxe, acx, allcoin, bequant, bibox, binance, binanceje, binanceus, bitbank, bitfinex, bitfinex2, bitkk, bitlish, bitmart, bittrex, bitz, bleutrade, btcalpha, btcmarkets, btcturk, buda, cex, cobinhood, coinbaseprime, coinbasepro, coinex, cointiger, coss, crex24, digifinex, dsx, dx, ethfinex, fcoin, fcoinjp, gateio, gdax, gemini, hitbtc2, huobipro, huobiru, idex, kkex, kraken, kucoin, kucoin2, kuna, lbank, mandala, mercado, oceanex, okcoincny, okcoinusd, okex, okex3, poloniex, rightbtc, theocean, tidebit, upbit, zb
```
* Example: see all exchanges supported by the ccxt library (including 'bad' ones, i.e. those that are known to not work with Freqtrade):
```
$ freqtrade list-exchanges -a
All exchanges supported by the ccxt library: _1btcxe, acx, adara, allcoin, anxpro, bcex, bequant, bibox, bigone, binance, binanceje, binanceus, bit2c, bitbank, bitbay, bitfinex, bitfinex2, bitflyer, bitforex, bithumb, bitkk, bitlish, bitmart, bitmex, bitso, bitstamp, bitstamp1, bittrex, bitz, bl3p, bleutrade, braziliex, btcalpha, btcbox, btcchina, btcmarkets, btctradeim, btctradeua, btcturk, buda, bxinth, cex, chilebit, cobinhood, coinbase, coinbaseprime, coinbasepro, coincheck, coinegg, coinex, coinexchange, coinfalcon, coinfloor, coingi, coinmarketcap, coinmate, coinone, coinspot, cointiger, coolcoin, coss, crex24, crypton, deribit, digifinex, dsx, dx, ethfinex, exmo, exx, fcoin, fcoinjp, flowbtc, foxbit, fybse, gateio, gdax, gemini, hitbtc, hitbtc2, huobipro, huobiru, ice3x, idex, independentreserve, indodax, itbit, kkex, kraken, kucoin, kucoin2, kuna, lakebtc, latoken, lbank, liquid, livecoin, luno, lykke, mandala, mercado, mixcoins, negociecoins, nova, oceanex, okcoincny, okcoinusd, okex, okex3, paymium, poloniex, rightbtc, southxchange, stronghold, surbitcoin, theocean, therock, tidebit, tidex, upbit, vaultoro, vbtc, virwox, xbtce, yobit, zaif, zb
```
## List Timeframes
Use the `list-timeframes` subcommand to see the list of ticker intervals (timeframes) available for the exchange.
```
usage: freqtrade list-timeframes [-h] [--exchange EXCHANGE] [-1]
optional arguments:
-h, --help show this help message and exit
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
config is provided.
-1, --one-column Print output in one column.
```
* Example: see the timeframes for the 'binance' exchange, set in the configuration file:
```
$ freqtrade -c config_binance.json list-timeframes
...
Timeframes available for the exchange `binance`: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w, 1M
```
* Example: enumerate exchanges available for Freqtrade and print timeframes supported by each of them:
```
$ for i in `freqtrade list-exchanges -1`; do freqtrade list-timeframes --exchange $i; done
```
## List pairs/list markets
The `list-pairs` and `list-markets` subcommands allow to see the pairs/markets available on exchange.
Pairs are markets with the '/' character between the base currency part and the quote currency part in the market symbol.
For example, in the 'ETH/BTC' pair 'ETH' is the base currency, while 'BTC' is the quote currency.
For pairs traded by Freqtrade the pair quote currency is defined by the value of the `stake_currency` configuration setting.
You can print info about any pair/market with these subcommands - and you can filter output by quote-currency using `--quote BTC`, or by base-currency using `--base ETH` options correspondingly.
These subcommands have same usage and same set of available options:
```
usage: freqtrade list-markets [-h] [--exchange EXCHANGE] [--print-list]
[--print-json] [-1] [--print-csv]
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
[-a]
usage: freqtrade list-pairs [-h] [--exchange EXCHANGE] [--print-list]
[--print-json] [-1] [--print-csv]
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
optional arguments:
-h, --help show this help message and exit
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
config is provided.
--print-list Print list of pairs or market symbols. By default data
is printed in the tabular format.
--print-json Print list of pairs or market symbols in JSON format.
-1, --one-column Print output in one column.
--print-csv Print exchange pair or market data in the csv format.
--base BASE_CURRENCY [BASE_CURRENCY ...]
Specify base currency(-ies). Space-separated list.
--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]
Specify quote currency(-ies). Space-separated list.
-a, --all Print all pairs or market symbols. By default only
active ones are shown.
```
By default, only active pairs/markets are shown. Active pairs/markets are those that can currently be traded
on the exchange. The see the list of all pairs/markets (not only the active ones), use the `-a`/`-all` option.
Pairs/markets are sorted by its symbol string in the printed output.
### Examples
* Print the list of active pairs with quote currency USD on exchange, specified in the default
configuration file (i.e. pairs on the "Bittrex" exchange) in JSON format:
```
$ freqtrade list-pairs --quote USD --print-json
```
* Print the list of all pairs on the exchange, specified in the `config_binance.json` configuration file
(i.e. on the "Binance" exchange) with base currencies BTC or ETH and quote currencies USDT or USD, as the
human-readable list with summary:
```
$ freqtrade -c config_binance.json list-pairs --all --base BTC ETH --quote USDT USD --print-list
```
* Print all markets on exchange "Kraken", in the tabular format:
```
$ freqtrade list-markets --exchange kraken --all
```

View File

@@ -9,25 +9,26 @@ dependencies:
- wheel - wheel
- numpy - numpy
- pandas - pandas
- scipy
- SQLAlchemy - SQLAlchemy
- scikit-learn
- arrow - arrow
- requests - requests
- urllib3 - urllib3
- wrapt - wrapt
- joblib
- jsonschema - jsonschema
- tabulate - tabulate
- python-rapidjson - python-rapidjson
- filelock
- flask - flask
- python-dotenv - python-dotenv
- cachetools - cachetools
- scikit-optimize
- python-telegram-bot - python-telegram-bot
# Optional for plotting # Optional for plotting
- plotly - plotly
# Optional for hyperopt
- scipy
- scikit-optimize
- scikit-learn
- filelock
- joblib
# Optional for development # Optional for development
- flake8 - flake8
- pytest - pytest

View File

@@ -1,5 +1,16 @@
""" FreqTrade bot """ """ FreqTrade bot """
__version__ = '2019.8-1' __version__ = '2019.10'
if __version__ == 'develop':
try:
import subprocess
__version__ = 'develop-' + subprocess.check_output(
['git', 'log', '--format="%h"', '-n 1'],
stderr=subprocess.DEVNULL).decode("utf-8").rstrip().strip('"')
except Exception:
# git not available, ignore
pass
class DependencyException(Exception): class DependencyException(Exception):
@@ -11,7 +22,7 @@ class DependencyException(Exception):
class OperationalException(Exception): class OperationalException(Exception):
""" """
Requires manual intervention. Requires manual intervention and will usually stop the bot.
This happens when an exchange returns an unexpected error during runtime This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid. or given configuration is invalid.
""" """

View File

@@ -2,10 +2,12 @@
This module contains the argument manager class This module contains the argument manager class
""" """
import argparse import argparse
from typing import List, Optional from functools import partial
from pathlib import Path
from typing import Any, Dict, List, Optional
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
from freqtrade import constants from freqtrade import constants
from freqtrade.configuration.cli_options import AVAILABLE_CLI_OPTIONS
ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"] ARGS_COMMON = ["verbosity", "logfile", "version", "config", "datadir", "user_data_dir"]
@@ -14,7 +16,7 @@ ARGS_STRATEGY = ["strategy", "strategy_path"]
ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"] ARGS_MAIN = ARGS_COMMON + ARGS_STRATEGY + ["db_url", "sd_notify"]
ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange", ARGS_COMMON_OPTIMIZE = ["ticker_interval", "timerange",
"max_open_trades", "stake_amount", "refresh_pairs"] "max_open_trades", "stake_amount", "fee"]
ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions", ARGS_BACKTEST = ARGS_COMMON_OPTIMIZE + ["position_stacking", "use_max_market_positions",
"strategy_list", "export", "exportfilename"] "strategy_list", "export", "exportfilename"]
@@ -28,39 +30,44 @@ ARGS_HYPEROPT = ARGS_COMMON_OPTIMIZE + ["hyperopt", "hyperopt_path",
ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"] ARGS_EDGE = ARGS_COMMON_OPTIMIZE + ["stoploss_range"]
ARGS_LIST_EXCHANGES = ["print_one_column"] ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
ARGS_CREATE_USERDIR = ["user_data_dir"] ARGS_CREATE_USERDIR = ["user_data_dir"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "exchange", "timeframes", "erase"] ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "download_trades", "exchange",
"timeframes", "erase"]
ARGS_PLOT_DATAFRAME = (ARGS_COMMON + ARGS_STRATEGY + ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit", "db_url",
["pairs", "indicators1", "indicators2", "plot_limit", "db_url", "trade_source", "export", "exportfilename", "timerange", "ticker_interval"]
"trade_source", "export", "exportfilename", "timerange",
"refresh_pairs"])
ARGS_PLOT_PROFIT = (ARGS_COMMON + ARGS_STRATEGY + ARGS_PLOT_PROFIT = ["pairs", "timerange", "export", "exportfilename", "db_url",
["pairs", "timerange", "export", "exportfilename", "db_url", "trade_source"]) "trade_source", "ticker_interval"]
NO_CONF_REQURIED = ["start_download_data"] NO_CONF_REQURIED = ["download-data", "list-timeframes", "list-markets", "list-pairs",
"plot-dataframe", "plot-profit"]
NO_CONF_ALLOWED = ["create-userdir", "list-exchanges"]
class Arguments(object): class Arguments:
""" """
Arguments Class. Manage the arguments received by the cli Arguments Class. Manage the arguments received by the cli
""" """
def __init__(self, args: Optional[List[str]], description: str, def __init__(self, args: Optional[List[str]]) -> None:
no_default_config: bool = False) -> None:
self.args = args self.args = args
self._parsed_arg: Optional[argparse.Namespace] = None self._parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description) self.parser = argparse.ArgumentParser(description='Free, open source crypto trading bot')
self._no_default_config = no_default_config
def _load_args(self) -> None: def _load_args(self) -> None:
self._build_args(optionlist=ARGS_MAIN) self._build_args(optionlist=ARGS_MAIN)
self._build_subcommands() self._build_subcommands()
def get_parsed_arg(self) -> argparse.Namespace: def get_parsed_arg(self) -> Dict[str, Any]:
""" """
Return the list of arguments Return the list of arguments
:return: List[str] List of arguments :return: List[str] List of arguments
@@ -69,7 +76,7 @@ class Arguments(object):
self._load_args() self._load_args()
self._parsed_arg = self._parse_args() self._parsed_arg = self._parse_args()
return self._parsed_arg return vars(self._parsed_arg)
def _parse_args(self) -> argparse.Namespace: def _parse_args(self) -> argparse.Namespace:
""" """
@@ -77,12 +84,16 @@ class Arguments(object):
""" """
parsed_arg = self.parser.parse_args(self.args) parsed_arg = self.parser.parse_args(self.args)
# When no config is provided, but a config exists, use that configuration!
subparser = parsed_arg.subparser if 'subparser' in parsed_arg else None
# Workaround issue in argparse with action='append' and default value # Workaround issue in argparse with action='append' and default value
# (see https://bugs.python.org/issue16399) # (see https://bugs.python.org/issue16399)
# Allow no-config for certain commands (like downloading / plotting) # Allow no-config for certain commands (like downloading / plotting)
if (not self._no_default_config and parsed_arg.config is None if (parsed_arg.config is None
and not (hasattr(parsed_arg, 'func') and subparser not in NO_CONF_ALLOWED
and parsed_arg.func.__name__ in NO_CONF_REQURIED)): and ((Path.cwd() / constants.DEFAULT_CONFIG).is_file()
or (subparser not in NO_CONF_REQURIED))):
parsed_arg.config = [constants.DEFAULT_CONFIG] parsed_arg.config = [constants.DEFAULT_CONFIG]
return parsed_arg return parsed_arg
@@ -100,7 +111,9 @@ class Arguments(object):
:return: None :return: None
""" """
from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge from freqtrade.optimize import start_backtesting, start_hyperopt, start_edge
from freqtrade.utils import start_create_userdir, start_download_data, start_list_exchanges from freqtrade.utils import (start_create_userdir, start_download_data,
start_list_exchanges, start_list_timeframes,
start_list_markets)
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='subparser')
@@ -119,6 +132,7 @@ class Arguments(object):
hyperopt_cmd.set_defaults(func=start_hyperopt) hyperopt_cmd.set_defaults(func=start_hyperopt)
self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd) self._build_args(optionlist=ARGS_HYPEROPT, parser=hyperopt_cmd)
# add create-userdir subcommand
create_userdir_cmd = subparsers.add_parser('create-userdir', create_userdir_cmd = subparsers.add_parser('create-userdir',
help="Create user-data directory.") help="Create user-data directory.")
create_userdir_cmd.set_defaults(func=start_create_userdir) create_userdir_cmd.set_defaults(func=start_create_userdir)
@@ -132,6 +146,30 @@ class Arguments(object):
list_exchanges_cmd.set_defaults(func=start_list_exchanges) list_exchanges_cmd.set_defaults(func=start_list_exchanges)
self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd) self._build_args(optionlist=ARGS_LIST_EXCHANGES, parser=list_exchanges_cmd)
# Add list-timeframes subcommand
list_timeframes_cmd = subparsers.add_parser(
'list-timeframes',
help='Print available ticker intervals (timeframes) for the exchange.'
)
list_timeframes_cmd.set_defaults(func=start_list_timeframes)
self._build_args(optionlist=ARGS_LIST_TIMEFRAMES, parser=list_timeframes_cmd)
# Add list-markets subcommand
list_markets_cmd = subparsers.add_parser(
'list-markets',
help='Print markets on exchange.'
)
list_markets_cmd.set_defaults(func=partial(start_list_markets, pairs_only=False))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_markets_cmd)
# Add list-pairs subcommand
list_pairs_cmd = subparsers.add_parser(
'list-pairs',
help='Print pairs on exchange.'
)
list_pairs_cmd.set_defaults(func=partial(start_list_markets, pairs_only=True))
self._build_args(optionlist=ARGS_LIST_PAIRS, parser=list_pairs_cmd)
# Add download-data subcommand # Add download-data subcommand
download_data_cmd = subparsers.add_parser( download_data_cmd = subparsers.add_parser(
'download-data', 'download-data',
@@ -139,3 +177,20 @@ class Arguments(object):
) )
download_data_cmd.set_defaults(func=start_download_data) download_data_cmd.set_defaults(func=start_download_data)
self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd) self._build_args(optionlist=ARGS_DOWNLOAD_DATA, parser=download_data_cmd)
# Add Plotting subcommand
from freqtrade.plot.plot_utils import start_plot_dataframe, start_plot_profit
plot_dataframe_cmd = subparsers.add_parser(
'plot-dataframe',
help='Plot candles with indicators.'
)
plot_dataframe_cmd.set_defaults(func=start_plot_dataframe)
self._build_args(optionlist=ARGS_PLOT_DATAFRAME, parser=plot_dataframe_cmd)
# Plot profit
plot_profit_cmd = subparsers.add_parser(
'plot-profit',
help='Generate plot showing profits.'
)
plot_profit_cmd.set_defaults(func=start_plot_profit)
self._build_args(optionlist=ARGS_PLOT_PROFIT, parser=plot_profit_cmd)

View File

@@ -3,8 +3,9 @@ from typing import Any, Dict
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason, from freqtrade.exchange import (available_exchanges, get_exchange_bad_reason,
is_exchange_available, is_exchange_bad, is_exchange_known_ccxt, is_exchange_bad,
is_exchange_officially_supported) is_exchange_officially_supported)
from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,14 +20,26 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
raises an exception if the exchange if not supported by ccxt raises an exception if the exchange if not supported by ccxt
and thus is not known for the Freqtrade at all. and thus is not known for the Freqtrade at all.
""" """
if config['runmode'] in [RunMode.PLOT] and not config.get('exchange', {}).get('name'):
# Skip checking exchange in plot mode, since it requires no exchange
return True
logger.info("Checking exchange...") logger.info("Checking exchange...")
exchange = config.get('exchange', {}).get('name').lower() exchange = config.get('exchange', {}).get('name').lower()
if not is_exchange_available(exchange): if not exchange:
raise OperationalException( raise OperationalException(
f'Exchange "{exchange}" is not supported by ccxt ' f'This command requires a configured exchange. You should either use '
f'`--exchange <exchange_name>` or specify a configuration file via `--config`.\n'
f'The following exchanges are available for Freqtrade: '
f'{", ".join(available_exchanges())}'
)
if not is_exchange_known_ccxt(exchange):
raise OperationalException(
f'Exchange "{exchange}" is not known to the ccxt library '
f'and therefore not available for the bot.\n' f'and therefore not available for the bot.\n'
f'The following exchanges are supported by ccxt: ' f'The following exchanges are available for Freqtrade: '
f'{", ".join(available_exchanges())}' f'{", ".join(available_exchanges())}'
) )
@@ -38,8 +51,8 @@ def check_exchange(config: Dict[str, Any], check_for_bad: bool = True) -> bool:
logger.info(f'Exchange "{exchange}" is officially supported ' logger.info(f'Exchange "{exchange}" is officially supported '
f'by the Freqtrade development team.') f'by the Freqtrade development team.')
else: else:
logger.warning(f'Exchange "{exchange}" is supported by ccxt ' logger.warning(f'Exchange "{exchange}" is known to the the ccxt library, '
f'and therefore available for the bot but not officially supported ' f'available for the bot, but not officially supported '
f'by the Freqtrade development team. ' f'by the Freqtrade development team. '
f'It may work flawlessly (please report back) or have serious issues. ' f'It may work flawlessly (please report back) or have serious issues. '
f'Use it at your own discretion.') f'Use it at your own discretion.')

View File

@@ -2,7 +2,6 @@
Definition of cli arguments used in arguments.py Definition of cli arguments used in arguments.py
""" """
import argparse import argparse
import os
from freqtrade import __version__, constants from freqtrade import __version__, constants
@@ -107,13 +106,6 @@ AVAILABLE_CLI_OPTIONS = {
help='Specify stake_amount.', help='Specify stake_amount.',
type=float, type=float,
), ),
"refresh_pairs": Arg(
'-r', '--refresh-pairs-cached',
help='Refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your optimization commands with '
'up-to-date data.',
action='store_true',
),
# Backtesting # Backtesting
"position_stacking": Arg( "position_stacking": Arg(
'--eps', '--enable-position-stacking', '--eps', '--enable-position-stacking',
@@ -148,8 +140,12 @@ AVAILABLE_CLI_OPTIONS = {
'Requires `--export` to be set as well. ' 'Requires `--export` to be set as well. '
'Example: `--export-filename=user_data/backtest_results/backtest_today.json`', 'Example: `--export-filename=user_data/backtest_results/backtest_today.json`',
metavar='PATH', metavar='PATH',
default=os.path.join('user_data', 'backtest_results', ),
'backtest-result.json'), "fee": Arg(
'--fee',
help='Specify fee ratio. Will be applied twice (on trade entry and exit).',
type=float,
metavar='FLOAT',
), ),
# Edge # Edge
"stoploss_range": Arg( "stoploss_range": Arg(
@@ -248,9 +244,50 @@ AVAILABLE_CLI_OPTIONS = {
# List exchanges # List exchanges
"print_one_column": Arg( "print_one_column": Arg(
'-1', '--one-column', '-1', '--one-column',
help='Print exchanges in one column.', help='Print output in one column.',
action='store_true', action='store_true',
), ),
"list_exchanges_all": Arg(
'-a', '--all',
help='Print all exchanges known to the ccxt library.',
action='store_true',
),
# List pairs / markets
"list_pairs_all": Arg(
'-a', '--all',
help='Print all pairs or market symbols. By default only active '
'ones are shown.',
action='store_true',
),
"print_list": Arg(
'--print-list',
help='Print list of pairs or market symbols. By default data is '
'printed in the tabular format.',
action='store_true',
),
"list_pairs_print_json": Arg(
'--print-json',
help='Print list of pairs or market symbols in JSON format.',
action='store_true',
default=False,
),
"print_csv": Arg(
'--print-csv',
help='Print exchange pair or market data in the csv format.',
action='store_true',
),
"quote_currencies": Arg(
'--quote',
help='Specify quote currency(-ies). Space-separated list.',
nargs='+',
metavar='QUOTE_CURRENCY',
),
"base_currencies": Arg(
'--base',
help='Specify base currency(-ies). Space-separated list.',
nargs='+',
metavar='BASE_CURRENCY',
),
# Script options # Script options
"pairs": Arg( "pairs": Arg(
'-p', '--pairs', '-p', '--pairs',
@@ -269,6 +306,12 @@ AVAILABLE_CLI_OPTIONS = {
type=check_int_positive, type=check_int_positive,
metavar='INT', metavar='INT',
), ),
"download_trades": Arg(
'--dl-trades',
help='Download trades instead of OHLCV data. The bot will resample trades to the '
'desired timeframe as specified as --timeframes/-t.',
action='store_true',
),
"exchange": Arg( "exchange": Arg(
'--exchange', '--exchange',
help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). ' help=f'Exchange name (default: `{constants.DEFAULT_EXCHANGE}`). '
@@ -292,14 +335,16 @@ AVAILABLE_CLI_OPTIONS = {
"indicators1": Arg( "indicators1": Arg(
'--indicators1', '--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. ' help='Set indicators from your strategy you want in the first row of the graph. '
'Comma-separated list. Example: `ema3,ema5`. Default: `%(default)s`.', 'Space-separated list. Example: `ema3 ema5`. Default: `%(default)s`.',
default='sma,ema3,ema5', default=['sma', 'ema3', 'ema5'],
nargs='+',
), ),
"indicators2": Arg( "indicators2": Arg(
'--indicators2', '--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. ' help='Set indicators from your strategy you want in the third row of the graph. '
'Comma-separated list. Example: `fastd,fastk`. Default: `%(default)s`.', 'Space-separated list. Example: `fastd fastk`. Default: `%(default)s`.',
default='macd,macdsignal', default=['macd', 'macdsignal'],
nargs='+',
), ),
"plot_limit": Arg( "plot_limit": Arg(
'--plot-limit', '--plot-limit',

View File

@@ -3,15 +3,15 @@ This module contains the configuration class
""" """
import logging import logging
import warnings import warnings
from argparse import Namespace
from copy import deepcopy from copy import deepcopy
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Dict, List, Optional from typing import Any, Callable, Dict, List, Optional
from freqtrade import OperationalException, constants from freqtrade import OperationalException, constants
from freqtrade.configuration.check_exchange import check_exchange from freqtrade.configuration.check_exchange import check_exchange
from freqtrade.configuration.config_validation import ( from freqtrade.configuration.config_validation import (validate_config_consistency,
validate_config_consistency, validate_config_schema) validate_config_schema)
from freqtrade.configuration.deprecated_settings import process_temporary_deprecated_settings
from freqtrade.configuration.directory_operations import (create_datadir, from freqtrade.configuration.directory_operations import (create_datadir,
create_userdata_dir) create_userdata_dir)
from freqtrade.configuration.load_config import load_config_file from freqtrade.configuration.load_config import load_config_file
@@ -22,13 +22,13 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class Configuration(object): class Configuration:
""" """
Class to read and init the bot configuration Class to read and init the bot configuration
Reuse this class for the bot, backtesting, hyperopt and every script that required configuration Reuse this class for the bot, backtesting, hyperopt and every script that required configuration
""" """
def __init__(self, args: Namespace, runmode: RunMode = None) -> None: def __init__(self, args: Dict[str, Any], runmode: RunMode = None) -> None:
self.args = args self.args = args
self.config: Optional[Dict[str, Any]] = None self.config: Optional[Dict[str, Any]] = None
self.runmode = runmode self.runmode = runmode
@@ -50,9 +50,16 @@ class Configuration(object):
and merging their contents. and merging their contents.
Files are loaded in sequence, parameters in later configuration files Files are loaded in sequence, parameters in later configuration files
override the same parameter from an earlier file (last definition wins). override the same parameter from an earlier file (last definition wins).
Runs through the whole Configuration initialization, so all expected config entries
are available to interactive environments.
:param files: List of file paths :param files: List of file paths
:return: configuration dictionary :return: configuration dictionary
""" """
c = Configuration({"config": files}, RunMode.OTHER)
return c.get_config()
def load_from_files(self, files: List[str]) -> Dict[str, Any]:
# Keep this method as staticmethod, so it can be used from interactive environments # Keep this method as staticmethod, so it can be used from interactive environments
config: Dict[str, Any] = {} config: Dict[str, Any] = {}
@@ -69,6 +76,10 @@ class Configuration(object):
# Normalize config # Normalize config
if 'internals' not in config: if 'internals' not in config:
config['internals'] = {} config['internals'] = {}
# TODO: This can be deleted along with removal of deprecated
# experimental settings
if 'ask_strategy' not in config:
config['ask_strategy'] = {}
# validate configuration before returning # validate configuration before returning
logger.info('Validating configuration ...') logger.info('Validating configuration ...')
@@ -82,7 +93,10 @@ class Configuration(object):
:return: Configuration dictionary :return: Configuration dictionary
""" """
# Load all configs # Load all configs
config: Dict[str, Any] = Configuration.from_files(self.args.config) config: Dict[str, Any] = self.load_from_files(self.args["config"])
# Keep a copy of the original configuration file
config['original_config'] = deepcopy(config)
self._process_common_options(config) self._process_common_options(config)
@@ -97,6 +111,8 @@ class Configuration(object):
self._resolve_pairs_list(config) self._resolve_pairs_list(config)
process_temporary_deprecated_settings(config)
validate_config_consistency(config) validate_config_consistency(config)
return config return config
@@ -107,13 +123,10 @@ class Configuration(object):
the -v/--verbose, --logfile options the -v/--verbose, --logfile options
""" """
# Log level # Log level
if 'verbosity' in self.args and self.args.verbosity: config.update({'verbosity': self.args.get("verbosity", 0)})
config.update({'verbosity': self.args.verbosity})
else:
config.update({'verbosity': 0})
if 'logfile' in self.args and self.args.logfile: if 'logfile' in self.args and self.args["logfile"]:
config.update({'logfile': self.args.logfile}) config.update({'logfile': self.args["logfile"]})
setup_logging(config) setup_logging(config)
@@ -122,15 +135,15 @@ class Configuration(object):
self._process_logging_options(config) self._process_logging_options(config)
# Set strategy if not specified in config and or if it's non default # Set strategy if not specified in config and or if it's non default
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'): if self.args.get("strategy") != constants.DEFAULT_STRATEGY or not config.get('strategy'):
config.update({'strategy': self.args.strategy}) config.update({'strategy': self.args.get("strategy")})
self._args_to_config(config, argname='strategy_path', self._args_to_config(config, argname='strategy_path',
logstring='Using additional Strategy lookup path: {}') logstring='Using additional Strategy lookup path: {}')
if ('db_url' in self.args and self.args.db_url and if ('db_url' in self.args and self.args["db_url"] and
self.args.db_url != constants.DEFAULT_DB_PROD_URL): self.args["db_url"] != constants.DEFAULT_DB_PROD_URL):
config.update({'db_url': self.args.db_url}) config.update({'db_url': self.args["db_url"]})
logger.info('Parameter --db-url detected ...') logger.info('Parameter --db-url detected ...')
if config.get('dry_run', False): if config.get('dry_run', False):
@@ -153,7 +166,7 @@ class Configuration(object):
config['max_open_trades'] = float('inf') config['max_open_trades'] = float('inf')
# Support for sd_notify # Support for sd_notify
if 'sd_notify' in self.args and self.args.sd_notify: if 'sd_notify' in self.args and self.args["sd_notify"]:
config['internals'].update({'sd_notify': True}) config['internals'].update({'sd_notify': True})
def _process_datadir_options(self, config: Dict[str, Any]) -> None: def _process_datadir_options(self, config: Dict[str, Any]) -> None:
@@ -162,12 +175,12 @@ class Configuration(object):
--user-data, --datadir --user-data, --datadir
""" """
# Check exchange parameter here - otherwise `datadir` might be wrong. # Check exchange parameter here - otherwise `datadir` might be wrong.
if "exchange" in self.args and self.args.exchange: if "exchange" in self.args and self.args["exchange"]:
config['exchange']['name'] = self.args.exchange config['exchange']['name'] = self.args["exchange"]
logger.info(f"Using exchange {config['exchange']['name']}") logger.info(f"Using exchange {config['exchange']['name']}")
if 'user_data_dir' in self.args and self.args.user_data_dir: if 'user_data_dir' in self.args and self.args["user_data_dir"]:
config.update({'user_data_dir': self.args.user_data_dir}) config.update({'user_data_dir': self.args["user_data_dir"]})
elif 'user_data_dir' not in config: elif 'user_data_dir' not in config:
# Default to cwd/user_data (legacy option ...) # Default to cwd/user_data (legacy option ...)
config.update({'user_data_dir': str(Path.cwd() / "user_data")}) config.update({'user_data_dir': str(Path.cwd() / "user_data")})
@@ -176,12 +189,16 @@ class Configuration(object):
config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False) config['user_data_dir'] = create_userdata_dir(config['user_data_dir'], create_dir=False)
logger.info('Using user-data directory: %s ...', config['user_data_dir']) logger.info('Using user-data directory: %s ...', config['user_data_dir'])
if 'datadir' in self.args and self.args.datadir: config.update({'datadir': create_datadir(config, self.args.get("datadir", None))})
config.update({'datadir': create_datadir(config, self.args.datadir)})
else:
config.update({'datadir': create_datadir(config, None)})
logger.info('Using data directory: %s ...', config.get('datadir')) logger.info('Using data directory: %s ...', config.get('datadir'))
if self.args.get('exportfilename'):
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
else:
config['exportfilename'] = (config['user_data_dir']
/ 'backtest_results/backtest-result.json')
def _process_optimize_options(self, config: Dict[str, Any]) -> None: def _process_optimize_options(self, config: Dict[str, Any]) -> None:
# This will override the strategy configuration # This will override the strategy configuration
@@ -192,12 +209,12 @@ class Configuration(object):
self._args_to_config(config, argname='position_stacking', self._args_to_config(config, argname='position_stacking',
logstring='Parameter --enable-position-stacking detected ...') logstring='Parameter --enable-position-stacking detected ...')
if 'use_max_market_positions' in self.args and not self.args.use_max_market_positions: if 'use_max_market_positions' in self.args and not self.args["use_max_market_positions"]:
config.update({'use_max_market_positions': False}) config.update({'use_max_market_positions': False})
logger.info('Parameter --disable-max-market-positions detected ...') logger.info('Parameter --disable-max-market-positions detected ...')
logger.info('max_open_trades set to unlimited ...') logger.info('max_open_trades set to unlimited ...')
elif 'max_open_trades' in self.args and self.args.max_open_trades: elif 'max_open_trades' in self.args and self.args["max_open_trades"]:
config.update({'max_open_trades': self.args.max_open_trades}) config.update({'max_open_trades': self.args["max_open_trades"]})
logger.info('Parameter --max_open_trades detected, ' logger.info('Parameter --max_open_trades detected, '
'overriding max_open_trades to: %s ...', config.get('max_open_trades')) 'overriding max_open_trades to: %s ...', config.get('max_open_trades'))
else: else:
@@ -207,17 +224,17 @@ class Configuration(object):
logstring='Parameter --stake_amount detected, ' logstring='Parameter --stake_amount detected, '
'overriding stake_amount to: {} ...') 'overriding stake_amount to: {} ...')
self._args_to_config(config, argname='fee',
logstring='Parameter --fee detected, '
'setting fee to: {} ...')
self._args_to_config(config, argname='timerange', self._args_to_config(config, argname='timerange',
logstring='Parameter --timerange detected: {} ...') logstring='Parameter --timerange detected: {} ...')
self._process_datadir_options(config) self._process_datadir_options(config)
self._args_to_config(config, argname='refresh_pairs',
logstring='Parameter -r/--refresh-pairs-cached detected ...',
deprecated_msg='-r/--refresh-pairs-cached will be removed soon.')
self._args_to_config(config, argname='strategy_list', self._args_to_config(config, argname='strategy_list',
logstring='Using strategy list of {} Strategies', logfun=len) logstring='Using strategy list of {} strategies', logfun=len)
self._args_to_config(config, argname='ticker_interval', self._args_to_config(config, argname='ticker_interval',
logstring='Overriding ticker interval with Command line argument') logstring='Overriding ticker interval with Command line argument')
@@ -225,20 +242,17 @@ class Configuration(object):
self._args_to_config(config, argname='export', self._args_to_config(config, argname='export',
logstring='Parameter --export detected: {} ...') logstring='Parameter --export detected: {} ...')
self._args_to_config(config, argname='exportfilename',
logstring='Storing backtest results to {} ...')
# Edge section: # Edge section:
if 'stoploss_range' in self.args and self.args.stoploss_range: if 'stoploss_range' in self.args and self.args["stoploss_range"]:
txt_range = eval(self.args.stoploss_range) txt_range = eval(self.args["stoploss_range"])
config['edge'].update({'stoploss_range_min': txt_range[0]}) config['edge'].update({'stoploss_range_min': txt_range[0]})
config['edge'].update({'stoploss_range_max': txt_range[1]}) config['edge'].update({'stoploss_range_max': txt_range[1]})
config['edge'].update({'stoploss_range_step': txt_range[2]}) config['edge'].update({'stoploss_range_step': txt_range[2]})
logger.info('Parameter --stoplosses detected: %s ...', self.args.stoploss_range) logger.info('Parameter --stoplosses detected: %s ...', self.args["stoploss_range"])
# Hyperopt section # Hyperopt section
self._args_to_config(config, argname='hyperopt', self._args_to_config(config, argname='hyperopt',
logstring='Using Hyperopt file {}') logstring='Using Hyperopt class name: {}')
self._args_to_config(config, argname='hyperopt_path', self._args_to_config(config, argname='hyperopt_path',
logstring='Using additional Hyperopt lookup path: {}') logstring='Using additional Hyperopt lookup path: {}')
@@ -254,7 +268,7 @@ class Configuration(object):
self._args_to_config(config, argname='print_all', self._args_to_config(config, argname='print_all',
logstring='Parameter --print-all detected ...') logstring='Parameter --print-all detected ...')
if 'print_colorized' in self.args and not self.args.print_colorized: if 'print_colorized' in self.args and not self.args["print_colorized"]:
logger.info('Parameter --no-color detected ...') logger.info('Parameter --no-color detected ...')
config.update({'print_colorized': False}) config.update({'print_colorized': False})
else: else:
@@ -276,7 +290,7 @@ class Configuration(object):
logstring='Hyperopt continue: {}') logstring='Hyperopt continue: {}')
self._args_to_config(config, argname='hyperopt_loss', self._args_to_config(config, argname='hyperopt_loss',
logstring='Using loss function: {}') logstring='Using Hyperopt loss class name: {}')
def _process_plot_options(self, config: Dict[str, Any]) -> None: def _process_plot_options(self, config: Dict[str, Any]) -> None:
@@ -302,6 +316,8 @@ class Configuration(object):
self._args_to_config(config, argname='days', self._args_to_config(config, argname='days',
logstring='Detected --days: {}') logstring='Detected --days: {}')
self._args_to_config(config, argname='download_trades',
logstring='Detected --dl-trades: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None: def _process_runmode(self, config: Dict[str, Any]) -> None:
@@ -324,9 +340,10 @@ class Configuration(object):
sample: logfun=len (prints the length of the found sample: logfun=len (prints the length of the found
configuration instead of the content) configuration instead of the content)
""" """
if argname in self.args and getattr(self.args, argname): if (argname in self.args and self.args[argname] is not None
and self.args[argname] is not False):
config.update({argname: getattr(self.args, argname)}) config.update({argname: self.args[argname]})
if logfun: if logfun:
logger.info(logstring.format(logfun(config[argname]))) logger.info(logstring.format(logfun(config[argname])))
else: else:
@@ -346,8 +363,8 @@ class Configuration(object):
if "pairs" in config: if "pairs" in config:
return return
if "pairs_file" in self.args and self.args.pairs_file: if "pairs_file" in self.args and self.args["pairs_file"]:
pairs_file = Path(self.args.pairs_file) pairs_file = Path(self.args["pairs_file"])
logger.info(f'Reading pairs file "{pairs_file}".') logger.info(f'Reading pairs file "{pairs_file}".')
# Download pairs from the pairs file if no config is specified # Download pairs from the pairs file if no config is specified
# or if pairs file is specified explicitely # or if pairs file is specified explicitely
@@ -358,7 +375,7 @@ class Configuration(object):
config['pairs'].sort() config['pairs'].sort()
return return
if "config" in self.args and self.args.config: if "config" in self.args and self.args["config"]:
logger.info("Using pairlist from configuration.") logger.info("Using pairlist from configuration.")
config['pairs'] = config.get('exchange', {}).get('pair_whitelist') config['pairs'] = config.get('exchange', {}).get('pair_whitelist')
else: else:

View File

@@ -0,0 +1,59 @@
"""
Functions to handle deprecated settings
"""
import logging
from typing import Any, Dict
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
def check_conflicting_settings(config: Dict[str, Any],
section1: str, name1: str,
section2: str, name2: str):
section1_config = config.get(section1, {})
section2_config = config.get(section2, {})
if name1 in section1_config and name2 in section2_config:
raise OperationalException(
f"Conflicting settings `{section1}.{name1}` and `{section2}.{name2}` "
"(DEPRECATED) detected in the configuration file. "
"This deprecated setting will be removed in the next versions of Freqtrade. "
f"Please delete it from your configuration and use the `{section1}.{name1}` "
"setting instead."
)
def process_deprecated_setting(config: Dict[str, Any],
section1: str, name1: str,
section2: str, name2: str):
section2_config = config.get(section2, {})
if name2 in section2_config:
logger.warning(
"DEPRECATED: "
f"The `{section2}.{name2}` setting is deprecated and "
"will be removed in the next versions of Freqtrade. "
f"Please use the `{section1}.{name1}` setting in your configuration instead."
)
section1_config = config.get(section1, {})
section1_config[name1] = section2_config[name2]
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:
check_conflicting_settings(config, 'ask_strategy', 'use_sell_signal',
'experimental', 'use_sell_signal')
check_conflicting_settings(config, 'ask_strategy', 'sell_profit_only',
'experimental', 'sell_profit_only')
check_conflicting_settings(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
'experimental', 'ignore_roi_if_buy_signal')
process_deprecated_setting(config, 'ask_strategy', 'use_sell_signal',
'experimental', 'use_sell_signal')
process_deprecated_setting(config, 'ask_strategy', 'sell_profit_only',
'experimental', 'sell_profit_only')
process_deprecated_setting(config, 'ask_strategy', 'ignore_roi_if_buy_signal',
'experimental', 'ignore_roi_if_buy_signal')

View File

@@ -7,7 +7,7 @@ from typing import Optional
import arrow import arrow
class TimeRange(): class TimeRange:
""" """
object defining timerange inputs. object defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used. [start/stop]type defines if [start/stop]ts shall be used.
@@ -42,9 +42,10 @@ class TimeRange():
(r'^-(\d{10})$', (None, 'date')), (r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)), (r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')), (r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')), (r'^-(\d{13})$', (None, 'date')),
(r'^(\d+)-$', ('line', None)), (r'^(\d{13})-$', ('date', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))] (r'^(\d{13})-(\d{13})$', ('date', 'date')),
]
for rex, stype in syntax: for rex, stype in syntax:
# Apply the regular expression to text # Apply the regular expression to text
match = re.match(rex, text) match = re.match(rex, text)
@@ -57,6 +58,8 @@ class TimeRange():
starts = rvals[index] starts = rvals[index]
if stype[0] == 'date' and len(starts) == 8: if stype[0] == 'date' and len(starts) == 8:
start = arrow.get(starts, 'YYYYMMDD').timestamp start = arrow.get(starts, 'YYYYMMDD').timestamp
elif len(starts) == 13:
start = int(starts) // 1000
else: else:
start = int(starts) start = int(starts)
index += 1 index += 1
@@ -64,6 +67,8 @@ class TimeRange():
stops = rvals[index] stops = rvals[index]
if stype[1] == 'date' and len(stops) == 8: if stype[1] == 'date' and len(stops) == 8:
stop = arrow.get(stops, 'YYYYMMDD').timestamp stop = arrow.get(stops, 'YYYYMMDD').timestamp
elif len(stops) == 13:
stop = int(stops) // 1000
else: else:
stop = int(stops) stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop) return TimeRange(stype[0], stype[1], start, stop)

View File

@@ -10,7 +10,7 @@ DEFAULT_TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy' DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_HYPEROPT = 'DefaultHyperOpts' DEFAULT_HYPEROPT = 'DefaultHyperOpt'
DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss' DEFAULT_HYPEROPT_LOSS = 'DefaultHyperOptLoss'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite' DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://' DEFAULT_DB_DRYRUN_URL = 'sqlite://'
@@ -22,6 +22,7 @@ ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc'] ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList'] AVAILABLE_PAIRLISTS = ['StaticPairList', 'VolumePairList']
DRY_RUN_WALLET = 999.9 DRY_RUN_WALLET = 999.9
MATH_CLOSE_PREC = 1e-14 # Precision used for float comparisons
TICKER_INTERVALS = [ TICKER_INTERVALS = [
'1m', '3m', '5m', '15m', '30m', '1m', '3m', '5m', '15m', '30m',
@@ -113,7 +114,10 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'use_order_book': {'type': 'boolean'}, 'use_order_book': {'type': 'boolean'},
'order_book_min': {'type': 'number', 'minimum': 1}, 'order_book_min': {'type': 'number', 'minimum': 1},
'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50} 'order_book_max': {'type': 'number', 'minimum': 1, 'maximum': 50},
'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'},
'ignore_roi_if_buy_signal': {'type': 'boolean'}
} }
}, },
'order_types': { 'order_types': {
@@ -121,6 +125,7 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES}, 'stoploss': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'stoploss_on_exchange': {'type': 'boolean'}, 'stoploss_on_exchange': {'type': 'boolean'},
'stoploss_on_exchange_interval': {'type': 'number'} 'stoploss_on_exchange_interval': {'type': 'number'}
@@ -142,7 +147,8 @@ CONF_SCHEMA = {
'properties': { 'properties': {
'use_sell_signal': {'type': 'boolean'}, 'use_sell_signal': {'type': 'boolean'},
'sell_profit_only': {'type': 'boolean'}, 'sell_profit_only': {'type': 'boolean'},
'ignore_roi_if_buy_signal_true': {'type': 'boolean'} 'ignore_roi_if_buy_signal': {'type': 'boolean'},
'block_bad_exchanges': {'type': 'boolean'}
} }
}, },
'pairlist': { 'pairlist': {
@@ -260,6 +266,6 @@ CONF_SCHEMA = {
'stake_amount', 'stake_amount',
'dry_run', 'dry_run',
'bid_strategy', 'bid_strategy',
'telegram' 'unfilledtimeout',
] ]
} }

View File

@@ -2,7 +2,7 @@
Module to handle data operations for freqtrade Module to handle data operations for freqtrade
""" """
# limit what's imported when using `from freqtrad.data import *`` # limit what's imported when using `from freqtrade.data import *`
__all__ = [ __all__ = [
'converter' 'converter'
] ]

View File

@@ -93,7 +93,7 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None, t.close_date.replace(tzinfo=pytz.UTC) if t.close_date else None,
t.calc_profit(), t.calc_profit_percent(), t.calc_profit(), t.calc_profit_percent(),
t.open_rate, t.close_rate, t.amount, t.open_rate, t.close_rate, t.amount,
(t.close_date.timestamp() - t.open_date.timestamp() (round((t.close_date.timestamp() - t.open_date.timestamp()) / 60, 2)
if t.close_date else None), if t.close_date else None),
t.sell_reason, t.sell_reason,
t.fee_open, t.fee_close, t.fee_open, t.fee_close,
@@ -112,16 +112,16 @@ def load_trades_from_db(db_url: str) -> pd.DataFrame:
return trades return trades
def load_trades(config) -> pd.DataFrame: def load_trades(source: str, db_url: str, exportfilename: str) -> pd.DataFrame:
""" """
Based on configuration option "trade_source": Based on configuration option "trade_source":
* loads data from DB (using `db_url`) * loads data from DB (using `db_url`)
* loads data from backtestfile (using `exportfilename`) * loads data from backtestfile (using `exportfilename`)
""" """
if config["trade_source"] == "DB": if source == "DB":
return load_trades_from_db(config["db_url"]) return load_trades_from_db(db_url)
elif config["trade_source"] == "file": elif source == "file":
return load_backtest_data(Path(config["exportfilename"])) return load_backtest_data(Path(exportfilename))
def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame: def extract_trades_of_period(dataframe: pd.DataFrame, trades: pd.DataFrame) -> pd.DataFrame:
@@ -150,14 +150,21 @@ def combine_tickers_with_mean(tickers: Dict[str, pd.DataFrame], column: str = "c
return df_comb return df_comb
def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str) -> pd.DataFrame: def create_cum_profit(df: pd.DataFrame, trades: pd.DataFrame, col_name: str,
timeframe: str) -> pd.DataFrame:
""" """
Adds a column `col_name` with the cumulative profit for the given trades array. Adds a column `col_name` with the cumulative profit for the given trades array.
:param df: DataFrame with date index :param df: DataFrame with date index
:param trades: DataFrame containing trades (requires columns close_time and profitperc) :param trades: DataFrame containing trades (requires columns close_time and profitperc)
:param col_name: Column name that will be assigned the results
:param timeframe: Timeframe used during the operations
:return: Returns df with one additional column, col_name, containing the cumulative profit. :return: Returns df with one additional column, col_name, containing the cumulative profit.
""" """
df[col_name] = trades.set_index('close_time')['profitperc'].cumsum() from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe)
# Resample to ticker_interval to make sure trades match candles
_trades_sum = trades.resample(f'{ticker_minutes}min', on='close_time')[['profitperc']].sum()
df.loc[:, col_name] = _trades_sum.cumsum()
# Set first value to 0 # Set first value to 0
df.loc[df.iloc[0].name, col_name] = 0 df.loc[df.iloc[0].name, col_name] = 0
# FFill to get continuous # FFill to get continuous

View File

@@ -114,3 +114,25 @@ def order_book_to_dataframe(bids: list, asks: list) -> DataFrame:
keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum']) keys=['b_sum', 'b_size', 'bids', 'asks', 'a_size', 'a_sum'])
# logger.info('order book %s', frame ) # logger.info('order book %s', frame )
return frame return frame
def trades_to_ohlcv(trades: list, timeframe: str) -> list:
"""
Converts trades list to ohlcv list
:param trades: List of trades, as returned by ccxt.fetch_trades.
:param timeframe: Ticker timeframe to resample data to
:return: ohlcv timeframe as list (as returned by ccxt.fetch_ohlcv)
"""
from freqtrade.exchange import timeframe_to_minutes
ticker_minutes = timeframe_to_minutes(timeframe)
df = pd.DataFrame(trades)
df['datetime'] = pd.to_datetime(df['datetime'])
df = df.set_index('datetime')
df_new = df['price'].resample(f'{ticker_minutes}min').ohlc()
df_new['volume'] = df['amount'].resample(f'{ticker_minutes}min').sum()
df_new['date'] = df_new.index.astype("int64") // 10 ** 6
# Drop 0 volume rows
df_new = df_new.dropna()
columns = ["date", "open", "high", "low", "close", "volume"]
return list(zip(*[df_new[x].values.tolist() for x in columns]))

View File

@@ -6,7 +6,7 @@ Common Interface for bot and strategy to access data.
""" """
import logging import logging
from pathlib import Path from pathlib import Path
from typing import List, Tuple from typing import Any, Dict, List, Optional, Tuple
from pandas import DataFrame from pandas import DataFrame
@@ -17,7 +17,7 @@ from freqtrade.state import RunMode
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class DataProvider(): class DataProvider:
def __init__(self, config: dict, exchange: Exchange) -> None: def __init__(self, config: dict, exchange: Exchange) -> None:
self._config = config self._config = config
@@ -65,9 +65,7 @@ class DataProvider():
""" """
return load_pair_history(pair=pair, return load_pair_history(pair=pair,
ticker_interval=ticker_interval or self._config['ticker_interval'], ticker_interval=ticker_interval or self._config['ticker_interval'],
refresh_pairs=False, datadir=Path(self._config['datadir'])
datadir=Path(self._config['datadir']) if self._config.get(
'datadir') else None
) )
def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame: def get_pair_dataframe(self, pair: str, ticker_interval: str = None) -> DataFrame:
@@ -87,6 +85,14 @@ class DataProvider():
logger.warning(f"No data found for ({pair}, {ticker_interval}).") logger.warning(f"No data found for ({pair}, {ticker_interval}).")
return data return data
def market(self, pair: str) -> Optional[Dict[str, Any]]:
"""
Return market data for the pair
:param pair: Pair to get the data for
:return: Market data dict from ccxt or None if market info is not available for the pair
"""
return self._exchange.markets.get(pair)
def ticker(self, pair: str): def ticker(self, pair: str):
""" """
Return last ticker data Return last ticker data
@@ -94,9 +100,9 @@ class DataProvider():
# TODO: Implement me # TODO: Implement me
pass pass
def orderbook(self, pair: str, maximum: int): def orderbook(self, pair: str, maximum: int) -> Dict[str, List]:
""" """
return latest orderbook data fetch latest orderbook data
:param pair: pair to get the data for :param pair: pair to get the data for
:param maximum: Maximum number of orderbook entries to query :param maximum: Maximum number of orderbook entries to query
:return: dict including bids/asks with a total of `maximum` entries. :return: dict including bids/asks with a total of `maximum` entries.

View File

@@ -17,7 +17,7 @@ from pandas import DataFrame
from freqtrade import OperationalException, misc from freqtrade import OperationalException, misc
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe, trades_to_ohlcv
from freqtrade.exchange import Exchange, timeframe_to_minutes from freqtrade.exchange import Exchange, timeframe_to_minutes
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -33,20 +33,12 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
start_index = 0 start_index = 0
stop_index = len(tickerlist) stop_index = len(tickerlist)
if timerange.starttype == 'line': if timerange.starttype == 'date':
stop_index = timerange.startts
if timerange.starttype == 'index':
start_index = timerange.startts
elif timerange.starttype == 'date':
while (start_index < len(tickerlist) and while (start_index < len(tickerlist) and
tickerlist[start_index][0] < timerange.startts * 1000): tickerlist[start_index][0] < timerange.startts * 1000):
start_index += 1 start_index += 1
if timerange.stoptype == 'line': if timerange.stoptype == 'date':
start_index = max(len(tickerlist) + timerange.stopts, 0)
if timerange.stoptype == 'index':
stop_index = timerange.stopts
elif timerange.stoptype == 'date':
while (stop_index > 0 and while (stop_index > 0 and
tickerlist[stop_index-1][0] > timerange.stopts * 1000): tickerlist[stop_index-1][0] > timerange.stopts * 1000):
stop_index -= 1 stop_index -= 1
@@ -57,11 +49,11 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
return tickerlist[start_index:stop_index] return tickerlist[start_index:stop_index]
def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: str, def load_tickerdata_file(datadir: Path, pair: str, ticker_interval: str,
timerange: Optional[TimeRange] = None) -> Optional[list]: timerange: Optional[TimeRange] = None) -> Optional[list]:
""" """
Load a pair from file, either .json.gz or .json Load a pair from file, either .json.gz or .json
:return: tickerlist or None if unsuccesful :return: tickerlist or None if unsuccessful
""" """
filename = pair_data_filename(datadir, pair, ticker_interval) filename = pair_data_filename(datadir, pair, ticker_interval)
pairdata = misc.file_load_json(filename) pairdata = misc.file_load_json(filename)
@@ -73,7 +65,7 @@ def load_tickerdata_file(datadir: Optional[Path], pair: str, ticker_interval: st
return pairdata return pairdata
def store_tickerdata_file(datadir: Optional[Path], pair: str, def store_tickerdata_file(datadir: Path, pair: str,
ticker_interval: str, data: list, is_zip: bool = False): ticker_interval: str, data: list, is_zip: bool = False):
""" """
Stores tickerdata to file Stores tickerdata to file
@@ -82,10 +74,42 @@ def store_tickerdata_file(datadir: Optional[Path], pair: str,
misc.file_dump_json(filename, data, is_zip=is_zip) misc.file_dump_json(filename, data, is_zip=is_zip)
def load_trades_file(datadir: Path, pair: str,
timerange: Optional[TimeRange] = None) -> List[Dict]:
"""
Load a pair from file, either .json.gz or .json
:return: tradelist or empty list if unsuccesful
"""
filename = pair_trades_filename(datadir, pair)
tradesdata = misc.file_load_json(filename)
if not tradesdata:
return []
return tradesdata
def store_trades_file(datadir: Path, pair: str,
data: list, is_zip: bool = True):
"""
Stores tickerdata to file
"""
filename = pair_trades_filename(datadir, pair)
misc.file_dump_json(filename, data, is_zip=is_zip)
def _validate_pairdata(pair, pairdata, timerange: TimeRange):
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000:
logger.warning('Missing data at start for pair %s, data starts at %s',
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000:
logger.warning('Missing data at end for pair %s, data ends at %s',
pair, arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
def load_pair_history(pair: str, def load_pair_history(pair: str,
ticker_interval: str, ticker_interval: str,
datadir: Optional[Path], datadir: Path,
timerange: TimeRange = TimeRange(None, None, 0, 0), timerange: Optional[TimeRange] = None,
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
@@ -116,81 +140,68 @@ def load_pair_history(pair: str,
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange) pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if pairdata: if pairdata:
if timerange.starttype == 'date' and pairdata[0][0] > timerange.startts * 1000: if timerange:
logger.warning('Missing data at start for pair %s, data starts at %s', _validate_pairdata(pair, pairdata, timerange)
pair, arrow.get(pairdata[0][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
if timerange.stoptype == 'date' and pairdata[-1][0] < timerange.stopts * 1000:
logger.warning('Missing data at end for pair %s, data ends at %s',
pair,
arrow.get(pairdata[-1][0] // 1000).strftime('%Y-%m-%d %H:%M:%S'))
return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair, return parse_ticker_dataframe(pairdata, ticker_interval, pair=pair,
fill_missing=fill_up_missing, fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete) drop_incomplete=drop_incomplete)
else: else:
logger.warning( logger.warning(
f'No history data for pair: "{pair}", interval: {ticker_interval}. ' f'No history data for pair: "{pair}", interval: {ticker_interval}. '
'Use --refresh-pairs-cached option or `freqtrade download-data` ' 'Use `freqtrade download-data` to download the data'
'script to download the data'
) )
return None return None
def load_data(datadir: Optional[Path], def load_data(datadir: Path,
ticker_interval: str, ticker_interval: str,
pairs: List[str], pairs: List[str],
refresh_pairs: bool = False, refresh_pairs: bool = False,
exchange: Optional[Exchange] = None, exchange: Optional[Exchange] = None,
timerange: TimeRange = TimeRange(None, None, 0, 0), timerange: Optional[TimeRange] = None,
fill_up_missing: bool = True, fill_up_missing: bool = True,
live: bool = False
) -> Dict[str, DataFrame]: ) -> Dict[str, DataFrame]:
""" """
Loads ticker history data for a list of pairs the given parameters Loads ticker history data for a list of pairs
:return: dict(<pair>:<tickerlist>) :return: dict(<pair>:<tickerlist>)
TODO: refresh_pairs is still used by edge to keep the data uptodate.
This should be replaced in the future. Instead, writing the current candles to disk
from dataprovider should be implemented, as this would avoid loading ohlcv data twice.
exchange and refresh_pairs are then not needed here nor in load_pair_history.
""" """
result: Dict[str, DataFrame] = {} result: Dict[str, DataFrame] = {}
if live:
if exchange:
logger.info('Live: Downloading data for all defined pairs ...')
exchange.refresh_latest_ohlcv([(pair, ticker_interval) for pair in pairs])
result = {key[0]: value for key, value in exchange._klines.items() if value is not None}
else:
raise OperationalException(
"Exchange needs to be initialized when using live data."
)
else:
logger.info('Using local backtesting data ...')
for pair in pairs: for pair in pairs:
hist = load_pair_history(pair=pair, ticker_interval=ticker_interval, hist = load_pair_history(pair=pair, ticker_interval=ticker_interval,
datadir=datadir, timerange=timerange, datadir=datadir, timerange=timerange,
refresh_pairs=refresh_pairs, refresh_pairs=refresh_pairs,
exchange=exchange, exchange=exchange,
fill_up_missing=fill_up_missing) fill_up_missing=fill_up_missing)
if hist is not None: if hist is not None:
result[pair] = hist result[pair] = hist
return result return result
def make_testdata_path(datadir: Optional[Path]) -> Path: def pair_data_filename(datadir: Path, pair: str, ticker_interval: str) -> Path:
"""Return the path where testdata files are stored"""
return datadir or (Path(__file__).parent.parent / "tests" / "testdata").resolve()
def pair_data_filename(datadir: Optional[Path], pair: str, ticker_interval: str) -> Path:
path = make_testdata_path(datadir)
pair_s = pair.replace("/", "_") pair_s = pair.replace("/", "_")
filename = path.joinpath(f'{pair_s}-{ticker_interval}.json') filename = datadir.joinpath(f'{pair_s}-{ticker_interval}.json')
return filename return filename
def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_interval: str, def pair_trades_filename(datadir: Path, pair: str) -> Path:
timerange: Optional[TimeRange]) -> Tuple[List[Any], pair_s = pair.replace("/", "_")
Optional[int]]: filename = datadir.joinpath(f'{pair_s}-trades.json.gz')
return filename
def _load_cached_data_for_updating(datadir: Path, pair: str, ticker_interval: str,
timerange: Optional[TimeRange]) -> Tuple[List[Any],
Optional[int]]:
""" """
Load cached data to download more data. Load cached data to download more data.
If timerange is passed in, checks wether data from an before the stored data will be downloaded. If timerange is passed in, checks whether data from an before the stored data will be
If that's the case than what's available should be completely overwritten. downloaded.
If that's the case then what's available should be completely overwritten.
Only used by download_pair_history(). Only used by download_pair_history().
""" """
@@ -224,7 +235,7 @@ def load_cached_data_for_updating(datadir: Optional[Path], pair: str, ticker_int
return (data, since_ms) return (data, since_ms)
def download_pair_history(datadir: Optional[Path], def download_pair_history(datadir: Path,
exchange: Optional[Exchange], exchange: Optional[Exchange],
pair: str, pair: str,
ticker_interval: str = '5m', ticker_interval: str = '5m',
@@ -253,7 +264,7 @@ def download_pair_history(datadir: Optional[Path],
f'and store in {datadir}.' f'and store in {datadir}.'
) )
data, since_ms = load_cached_data_for_updating(datadir, pair, ticker_interval, timerange) data, since_ms = _load_cached_data_for_updating(datadir, pair, ticker_interval, timerange)
logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None') logger.debug("Current Start: %s", misc.format_ms_time(data[1][0]) if data else 'None')
logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None') logger.debug("Current End: %s", misc.format_ms_time(data[-1][0]) if data else 'None')
@@ -280,6 +291,121 @@ def download_pair_history(datadir: Optional[Path],
return False return False
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
dl_path: Path, timerange: Optional[TimeRange] = None,
erase=False) -> List[str]:
"""
Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data
:return: Pairs not available
"""
pairs_not_available = []
for pair in pairs:
if pair not in exchange.markets:
pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...")
continue
for ticker_interval in timeframes:
dl_file = pair_data_filename(dl_path, pair, ticker_interval)
if erase and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}, interval {ticker_interval}.')
dl_file.unlink()
logger.info(f'Downloading pair {pair}, interval {ticker_interval}.')
download_pair_history(datadir=dl_path, exchange=exchange,
pair=pair, ticker_interval=str(ticker_interval),
timerange=timerange)
return pairs_not_available
def download_trades_history(datadir: Path,
exchange: Exchange,
pair: str,
timerange: Optional[TimeRange] = None) -> bool:
"""
Download trade history from the exchange.
Appends to previously downloaded trades data.
"""
try:
since = timerange.startts * 1000 if timerange and timerange.starttype == 'date' else None
trades = load_trades_file(datadir, pair)
from_id = trades[-1]['id'] if trades else None
logger.debug("Current Start: %s", trades[0]['datetime'] if trades else 'None')
logger.debug("Current End: %s", trades[-1]['datetime'] if trades else 'None')
new_trades = exchange.get_historic_trades(pair=pair,
since=since if since else
int(arrow.utcnow().shift(
days=-30).float_timestamp) * 1000,
# until=xxx,
from_id=from_id,
)
trades.extend(new_trades[1])
store_trades_file(datadir, pair, trades)
logger.debug("New Start: %s", trades[0]['datetime'])
logger.debug("New End: %s", trades[-1]['datetime'])
logger.info(f"New Amount of trades: {len(trades)}")
return True
except Exception as e:
logger.error(
f'Failed to download historic trades for pair: "{pair}". '
f'Error: {e}'
)
return False
def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir: Path,
timerange: TimeRange, erase=False) -> List[str]:
"""
Refresh stored trades data.
Used by freqtrade download-data
:return: Pairs not available
"""
pairs_not_available = []
for pair in pairs:
if pair not in exchange.markets:
pairs_not_available.append(pair)
logger.info(f"Skipping pair {pair}...")
continue
dl_file = pair_trades_filename(datadir, pair)
if erase and dl_file.exists():
logger.info(
f'Deleting existing data for pair {pair}.')
dl_file.unlink()
logger.info(f'Downloading trades for pair {pair}.')
download_trades_history(datadir=datadir, exchange=exchange,
pair=pair,
timerange=timerange)
return pairs_not_available
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
datadir: Path, timerange: TimeRange, erase=False) -> None:
"""
Convert stored trades data to ohlcv data
"""
for pair in pairs:
trades = load_trades_file(datadir, pair)
for timeframe in timeframes:
ohlcv_file = pair_data_filename(datadir, pair, timeframe)
if erase and ohlcv_file.exists():
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
ohlcv_file.unlink()
ohlcv = trades_to_ohlcv(trades, timeframe)
# Store ohlcv
store_tickerdata_file(datadir, pair, timeframe, data=ohlcv)
def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]: def get_timeframe(data: Dict[str, DataFrame]) -> Tuple[arrow.Arrow, arrow.Arrow]:
""" """
Get the maximum timeframe for the given backtest data Get the maximum timeframe for the given backtest data

View File

@@ -28,7 +28,7 @@ class PairInfo(NamedTuple):
avg_trade_duration: float avg_trade_duration: float
class Edge(): class Edge:
""" """
Calculates Win Rate, Risk Reward Ratio, Expectancy Calculates Win Rate, Risk Reward Ratio, Expectancy
against historical data for a give set of markets and a strategy against historical data for a give set of markets and a strategy
@@ -77,8 +77,10 @@ class Edge():
self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift( self._timerange: TimeRange = TimeRange.parse_timerange("%s-" % arrow.now().shift(
days=-1 * self._since_number_of_days).format('YYYYMMDD')) days=-1 * self._since_number_of_days).format('YYYYMMDD'))
if config.get('fee'):
self.fee = self.exchange.get_fee() self.fee = config['fee']
else:
self.fee = self.exchange.get_fee()
def calculate(self) -> bool: def calculate(self) -> bool:
pairs = self.config['exchange']['pair_whitelist'] pairs = self.config['exchange']['pair_whitelist']
@@ -93,7 +95,7 @@ class Edge():
logger.info('Using local backtesting data (using whitelist in given config) ...') logger.info('Using local backtesting data (using whitelist in given config) ...')
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']),
pairs=pairs, pairs=pairs,
ticker_interval=self.strategy.ticker_interval, ticker_interval=self.strategy.ticker_interval,
refresh_pairs=self._refresh_pairs, refresh_pairs=self._refresh_pairs,

View File

@@ -1,13 +1,16 @@
from freqtrade.exchange.exchange import Exchange # noqa: F401 from freqtrade.exchange.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS # noqa: F401
from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401 from freqtrade.exchange.exchange import (get_exchange_bad_reason, # noqa: F401
is_exchange_bad, is_exchange_bad,
is_exchange_available, is_exchange_known_ccxt,
is_exchange_officially_supported, is_exchange_officially_supported,
ccxt_exchanges,
available_exchanges) available_exchanges)
from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401 from freqtrade.exchange.exchange import (timeframe_to_seconds, # noqa: F401
timeframe_to_minutes, timeframe_to_minutes,
timeframe_to_msecs, timeframe_to_msecs,
timeframe_to_next_date, timeframe_to_next_date,
timeframe_to_prev_date) timeframe_to_prev_date)
from freqtrade.exchange.exchange import (market_is_active, # noqa: F401
symbol_is_pair)
from freqtrade.exchange.kraken import Kraken # noqa: F401 from freqtrade.exchange.kraken import Kraken # noqa: F401
from freqtrade.exchange.binance import Binance # noqa: F401 from freqtrade.exchange.binance import Binance # noqa: F401

View File

@@ -2,6 +2,10 @@
import logging import logging
from typing import Dict from typing import Dict
import ccxt
from freqtrade import (DependencyException, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -12,6 +16,8 @@ class Binance(Exchange):
_ft_has: Dict = { _ft_has: Dict = {
"stoploss_on_exchange": True, "stoploss_on_exchange": True,
"order_time_in_force": ['gtc', 'fok', 'ioc'], "order_time_in_force": ['gtc', 'fok', 'ioc'],
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
} }
def get_order_book(self, pair: str, limit: int = 100) -> dict: def get_order_book(self, pair: str, limit: int = 100) -> dict:
@@ -25,3 +31,55 @@ class Binance(Exchange):
limit = min(list(filter(lambda x: limit <= x, limit_range))) limit = min(list(filter(lambda x: limit <= x, limit_range)))
return super().get_order_book(pair, limit) return super().get_order_book(pair, limit)
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
"""
creates a stoploss limit order.
this stoploss-limit is binance-specific.
It may work with a limited number of other exchanges, but this has not been tested yet.
"""
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price)
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
try:
params = self._params.copy()
params.update({'stopPrice': stop_price})
amount = self.symbol_amount_prec(pair, amount)
rate = self.symbol_price_prec(pair, rate)
order = self._api.create_order(pair, ordertype, 'sell',
amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
except ccxt.InsufficientFunds as e:
raise DependencyException(
f'Insufficient funds to create {ordertype} sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
# Errors:
# `binance Order would trigger immediately.`
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Tried to sell amount {amount} at rate {rate}. '
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@@ -14,6 +14,7 @@ from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
import ccxt import ccxt
import ccxt.async_support as ccxt_async import ccxt.async_support as ccxt_async
from ccxt.base.decimal_to_precision import ROUND_UP, ROUND_DOWN
from pandas import DataFrame from pandas import DataFrame
from freqtrade import (DependencyException, InvalidOrderException, from freqtrade import (DependencyException, InvalidOrderException,
@@ -21,16 +22,92 @@ from freqtrade import (DependencyException, InvalidOrderException,
from freqtrade.data.converter import parse_ticker_dataframe from freqtrade.data.converter import parse_ticker_dataframe
from freqtrade.misc import deep_merge_dicts from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
API_RETRY_COUNT = 4 API_RETRY_COUNT = 4
BAD_EXCHANGES = { BAD_EXCHANGES = {
"bitmex": "Various reasons", "bitmex": "Various reasons.",
"bitstamp": "Does not provide history. " "bitstamp": "Does not provide history. "
"Details in https://github.com/freqtrade/freqtrade/issues/1983", "Details in https://github.com/freqtrade/freqtrade/issues/1983",
"hitbtc": "This API cannot be used with Freqtrade. "
"Use `hitbtc2` exchange id to access this exchange.",
**dict.fromkeys([
'adara',
'anxpro',
'bigone',
'coinbase',
'coinexchange',
'coinmarketcap',
'lykke',
'xbtce',
], "Does not provide timeframes. ccxt fetchOHLCV: False"),
**dict.fromkeys([
'bcex',
'bit2c',
'bitbay',
'bitflyer',
'bitforex',
'bithumb',
'bitso',
'bitstamp1',
'bl3p',
'braziliex',
'btcbox',
'btcchina',
'btctradeim',
'btctradeua',
'bxinth',
'chilebit',
'coincheck',
'coinegg',
'coinfalcon',
'coinfloor',
'coingi',
'coinmate',
'coinone',
'coinspot',
'coolcoin',
'crypton',
'deribit',
'exmo',
'exx',
'flowbtc',
'foxbit',
'fybse',
# 'hitbtc',
'ice3x',
'independentreserve',
'indodax',
'itbit',
'lakebtc',
'latoken',
'liquid',
'livecoin',
'luno',
'mixcoins',
'negociecoins',
'nova',
'paymium',
'southxchange',
'stronghold',
'surbitcoin',
'therock',
'tidex',
'vaultoro',
'vbtc',
'virwox',
'yobit',
'zaif',
], "Does not provide timeframes. ccxt fetchOHLCV: emulated"),
} }
MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance',
'binanceje': 'binance',
}
def retrier_async(f): def retrier_async(f):
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
@@ -68,9 +145,11 @@ def retrier(f):
return wrapper return wrapper
class Exchange(object): class Exchange:
_config: Dict = {} _config: Dict = {}
# Parameters to add directly to buy/sell calls (like agreeing to trading agreement)
_params: Dict = {} _params: Dict = {}
# Dict to specify which options each exchange implements # Dict to specify which options each exchange implements
@@ -81,10 +160,13 @@ class Exchange(object):
"order_time_in_force": ["gtc"], "order_time_in_force": ["gtc"],
"ohlcv_candle_limit": 500, "ohlcv_candle_limit": 500,
"ohlcv_partial_candle": True, "ohlcv_partial_candle": True,
"trades_pagination": "time", # Possible are "time" or "id"
"trades_pagination_arg": "since",
} }
_ft_has: Dict = {} _ft_has: Dict = {}
def __init__(self, config: dict) -> None: def __init__(self, config: dict, validate: bool = True) -> None:
""" """
Initializes this module with the given config, Initializes this module with the given config,
it does basic validation whether the specified exchange and pairs are valid. it does basic validation whether the specified exchange and pairs are valid.
@@ -124,6 +206,9 @@ class Exchange(object):
self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit'] self._ohlcv_candle_limit = self._ft_has['ohlcv_candle_limit']
self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle'] self._ohlcv_partial_candle = self._ft_has['ohlcv_partial_candle']
self._trades_pagination = self._ft_has['trades_pagination']
self._trades_pagination_arg = self._ft_has['trades_pagination_arg']
# Initialize ccxt objects # Initialize ccxt objects
self._api = self._init_ccxt( self._api = self._init_ccxt(
exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config')) exchange_config, ccxt_kwargs=exchange_config.get('ccxt_config'))
@@ -132,20 +217,21 @@ class Exchange(object):
logger.info('Using Exchange "%s"', self.name) logger.info('Using Exchange "%s"', self.name)
if validate:
# Check if timeframe is available
self.validate_timeframes(config.get('ticker_interval'))
# Initial markets load
self._load_markets()
# Check if all pairs are available
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
# Converts the interval provided in minutes in config to seconds # Converts the interval provided in minutes in config to seconds
self.markets_refresh_interval: int = exchange_config.get( self.markets_refresh_interval: int = exchange_config.get(
"markets_refresh_interval", 60) * 60 "markets_refresh_interval", 60) * 60
# Initial markets load
self._load_markets()
# Check if all pairs are available
self.validate_pairs(config['exchange']['pair_whitelist'])
self.validate_ordertypes(config.get('order_types', {}))
self.validate_order_time_in_force(config.get('order_time_in_force', {}))
if config.get('ticker_interval'):
# Check if timeframe is available
self.validate_timeframes(config['ticker_interval'])
def __del__(self): def __del__(self):
""" """
@@ -164,7 +250,7 @@ class Exchange(object):
# Find matching class for the given exchange name # Find matching class for the given exchange name
name = exchange_config['name'] name = exchange_config['name']
if not is_exchange_available(name, ccxt_module): if not is_exchange_known_ccxt(name, ccxt_module):
raise OperationalException(f'Exchange {name} is not supported by ccxt') raise OperationalException(f'Exchange {name} is not supported by ccxt')
ex_config = { ex_config = {
@@ -198,6 +284,10 @@ class Exchange(object):
"""exchange ccxt id""" """exchange ccxt id"""
return self._api.id return self._api.id
@property
def timeframes(self) -> List[str]:
return list((self._api.timeframes or {}).keys())
@property @property
def markets(self) -> Dict: def markets(self) -> Dict:
"""exchange ccxt markets""" """exchange ccxt markets"""
@@ -206,6 +296,28 @@ class Exchange(object):
self._load_markets() self._load_markets()
return self._api.markets return self._api.markets
def get_markets(self, base_currencies: List[str] = None, quote_currencies: List[str] = None,
pairs_only: bool = False, active_only: bool = False) -> Dict:
"""
Return exchange ccxt markets, filtered out by base currency and quote currency
if this was requested in parameters.
TODO: consider moving it to the Dataprovider
"""
markets = self.markets
if not markets:
raise OperationalException("Markets were not loaded.")
if base_currencies:
markets = {k: v for k, v in markets.items() if v['base'] in base_currencies}
if quote_currencies:
markets = {k: v for k, v in markets.items() if v['quote'] in quote_currencies}
if pairs_only:
markets = {k: v for k, v in markets.items() if symbol_is_pair(v['symbol'])}
if active_only:
markets = {k: v for k, v in markets.items() if market_is_active(v)}
return markets
def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame: def klines(self, pair_interval: Tuple[str, str], copy=True) -> DataFrame:
if pair_interval in self._klines: if pair_interval in self._klines:
return self._klines[pair_interval].copy() if copy else self._klines[pair_interval] return self._klines[pair_interval].copy() if copy else self._klines[pair_interval]
@@ -290,7 +402,7 @@ class Exchange(object):
return pair return pair
raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.") raise DependencyException(f"Could not combine {curr_1} and {curr_2} to get a valid pair.")
def validate_timeframes(self, timeframe: List[str]) -> None: def validate_timeframes(self, timeframe: Optional[str]) -> None:
""" """
Checks if ticker interval from config is a supported timeframe on the exchange Checks if ticker interval from config is a supported timeframe on the exchange
""" """
@@ -303,10 +415,9 @@ class Exchange(object):
f"for the exchange \"{self.name}\" and this exchange " f"for the exchange \"{self.name}\" and this exchange "
f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}") f"is therefore not supported. ccxt fetchOHLCV: {self.exchange_has('fetchOHLCV')}")
timeframes = self._api.timeframes if timeframe and (timeframe not in self.timeframes):
if timeframe not in timeframes:
raise OperationalException( raise OperationalException(
f'Invalid ticker {timeframe}, this Exchange supports {timeframes}') f"Invalid ticker interval '{timeframe}'. This exchange supports: {self.timeframes}")
def validate_ordertypes(self, order_types: Dict) -> None: def validate_ordertypes(self, order_types: Dict) -> None:
""" """
@@ -320,7 +431,7 @@ class Exchange(object):
if (order_types.get("stoploss_on_exchange") if (order_types.get("stoploss_on_exchange")
and not self._ft_has.get("stoploss_on_exchange", False)): and not self._ft_has.get("stoploss_on_exchange", False)):
raise OperationalException( raise OperationalException(
'On exchange stoploss is not supported for %s.' % self.name f'On exchange stoploss is not supported for {self.name}.'
) )
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None: def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
@@ -366,7 +477,7 @@ class Exchange(object):
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float, def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
rate: float, params: Dict = {}) -> Dict[str, Any]: rate: float, params: Dict = {}) -> Dict[str, Any]:
order_id = f'dry_run_{side}_{randint(0, 10**6)}' order_id = f'dry_run_{side}_{randint(0, 10**6)}'
dry_order = { # TODO: additional entry should be added for stoploss limit dry_order = {
"id": order_id, "id": order_id,
'pair': pair, 'pair': pair,
'price': rate, 'price': rate,
@@ -381,6 +492,7 @@ class Exchange(object):
"info": {} "info": {}
} }
self._store_dry_order(dry_order) self._store_dry_order(dry_order)
# Copy order and close it - so the returned order is open unless it's a market order
return dry_order return dry_order
def _store_dry_order(self, dry_order: Dict) -> None: def _store_dry_order(self, dry_order: Dict) -> None:
@@ -391,6 +503,8 @@ class Exchange(object):
"filled": closed_order["amount"], "filled": closed_order["amount"],
"remaining": 0 "remaining": 0
}) })
if closed_order["type"] in ["stop_loss_limit"]:
closed_order["info"].update({"stopPrice": closed_order["price"]})
self._dry_run_open_orders[closed_order["id"]] = closed_order self._dry_run_open_orders[closed_order["id"]] = closed_order
def create_order(self, pair: str, ordertype: str, side: str, amount: float, def create_order(self, pair: str, ordertype: str, side: str, amount: float,
@@ -450,30 +564,14 @@ class Exchange(object):
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict: def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
""" """
creates a stoploss limit order. creates a stoploss limit order.
NOTICE: it is not supported by all exchanges. only binance is tested for now. Since ccxt does not unify stoploss-limit orders yet, this needs to be implemented in each
TODO: implementation maybe needs to be moved to the binance subclass exchange's subclass.
The exception below should never raise, since we disallow
starting the bot in validate_ordertypes()
Note: Changes to this interface need to be applied to all sub-classes too.
""" """
ordertype = "stop_loss_limit"
stop_price = self.symbol_price_prec(pair, stop_price) raise OperationalException(f"stoploss_limit is not implemented for {self.name}.")
# Ensure rate is less than stop price
if stop_price <= rate:
raise OperationalException(
'In stoploss limit order, stop price should be more than limit price')
if self._config['dry_run']:
dry_order = self.dry_run_order(
pair, ordertype, "sell", amount, stop_price)
return dry_order
params = self._params.copy()
params.update({'stopPrice': stop_price})
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
logger.info('stoploss limit order added for %s. '
'stop price: %s. limit: %s', pair, stop_price, rate)
return order
@retrier @retrier
def get_balance(self, currency: str) -> float: def get_balance(self, currency: str) -> float:
@@ -677,6 +775,154 @@ class Exchange(object):
except ccxt.BaseError as e: except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e raise OperationalException(f'Could not fetch ticker data. Msg: {e}') from e
@retrier_async
async def _async_fetch_trades(self, pair: str,
since: Optional[int] = None,
params: Optional[dict] = None) -> List[Dict]:
"""
Asyncronously gets trade history using fetch_trades.
Handles exchange errors, does one call to the exchange.
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
returns: List of dicts containing trades
"""
try:
# fetch trades asynchronously
if params:
logger.debug("Fetching trades for pair %s, params: %s ", pair, params)
trades = await self._api_async.fetch_trades(pair, params=params, limit=1000)
else:
logger.debug(
"Fetching trades for pair %s, since %s %s...",
pair, since,
'(' + arrow.get(since // 1000).isoformat() + ') ' if since is not None else ''
)
trades = await self._api_async.fetch_trades(pair, since=since, limit=1000)
return trades
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {self._api.name} does not support fetching historical trade data.'
f'Message: {e}') from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not load trade history due to {e.__class__.__name__}. '
f'Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(f'Could not fetch trade data. Msg: {e}') from e
async def _async_get_trade_history_id(self, pair: str,
until: int,
since: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
"""
Asyncronously gets trade history using fetch_trades
use this when exchange uses id-based iteration (check `self._trades_pagination`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
:param from_id: Download data starting with ID (if id is known). Ignores "since" if set.
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
if not from_id:
# Fetch first elements using timebased method to get an ID to paginate on
# Depending on the Exchange, this can introduce a drift at the start of the interval
# of up to an hour.
# e.g. Binance returns the "last 1000" candles within a 1h time interval
# - so we will miss the first trades.
t = await self._async_fetch_trades(pair, since=since)
from_id = t[-1]['id']
trades.extend(t[:-1])
while True:
t = await self._async_fetch_trades(pair,
params={self._trades_pagination_arg: from_id})
if len(t):
# Skip last id since its the key for the next call
trades.extend(t[:-1])
if from_id == t[-1]['id'] or t[-1]['timestamp'] > until:
logger.debug(f"Stopping because from_id did not change. "
f"Reached {t[-1]['timestamp']} > {until}")
# Reached the end of the defined-download period - add last trade as well.
trades.extend(t[-1:])
break
from_id = t[-1]['id']
else:
break
return (pair, trades)
async def _async_get_trade_history_time(self, pair: str, until: int,
since: Optional[int] = None) -> Tuple[str, List]:
"""
Asyncronously gets trade history using fetch_trades,
when the exchange uses time-based iteration (check `self._trades_pagination`)
:param pair: Pair to fetch trade data for
:param since: Since as integer timestamp in milliseconds
:param until: Until as integer timestamp in milliseconds
returns tuple: (pair, trades-list)
"""
trades: List[Dict] = []
while True:
t = await self._async_fetch_trades(pair, since=since)
if len(t):
since = t[-1]['timestamp']
trades.extend(t)
# Reached the end of the defined-download period
if until and t[-1]['timestamp'] > until:
logger.debug(
f"Stopping because until was reached. {t[-1]['timestamp']} > {until}")
break
else:
break
return (pair, trades)
async def _async_get_trade_history(self, pair: str,
since: Optional[int] = None,
until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List[Dict]]:
"""
Async wrapper handling downloading trades using either time or id based methods.
"""
if self._trades_pagination == 'time':
return await self._async_get_trade_history_time(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds())
elif self._trades_pagination == 'id':
return await self._async_get_trade_history_id(
pair=pair, since=since,
until=until or ccxt.Exchange.milliseconds(), from_id=from_id
)
else:
raise OperationalException(f"Exchange {self.name} does use neither time, "
f"nor id based pagination")
def get_historic_trades(self, pair: str,
since: Optional[int] = None,
until: Optional[int] = None,
from_id: Optional[str] = None) -> Tuple[str, List]:
"""
Gets candle history using asyncio and returns the list of candles.
Handles all async doing.
Async over one pair, assuming we get `_ohlcv_candle_limit` candles per call.
:param pair: Pair to download
:param ticker_interval: Interval to get
:param since: Timestamp in milliseconds to get history from
:param until: Timestamp in milliseconds. Defaults to current timestamp if not defined.
:param from_id: Download data starting with ID (if id is known)
:returns List of tickers
"""
if not self.exchange_has("fetchTrades"):
raise OperationalException("This exchange does not suport downloading Trades.")
return asyncio.get_event_loop().run_until_complete(
self._async_get_trade_history(pair=pair, since=since,
until=until, from_id=from_id))
@retrier @retrier
def cancel_order(self, order_id: str, pair: str) -> None: def cancel_order(self, order_id: str, pair: str) -> None:
if self._config['dry_run']: if self._config['dry_run']:
@@ -780,18 +1026,29 @@ def get_exchange_bad_reason(exchange_name: str) -> str:
return BAD_EXCHANGES.get(exchange_name, "") return BAD_EXCHANGES.get(exchange_name, "")
def is_exchange_available(exchange_name: str, ccxt_module=None) -> bool: def is_exchange_known_ccxt(exchange_name: str, ccxt_module=None) -> bool:
return exchange_name in available_exchanges(ccxt_module) return exchange_name in ccxt_exchanges(ccxt_module)
def is_exchange_officially_supported(exchange_name: str) -> bool: def is_exchange_officially_supported(exchange_name: str) -> bool:
return exchange_name in ['bittrex', 'binance'] return exchange_name in ['bittrex', 'binance']
def available_exchanges(ccxt_module=None) -> List[str]: def ccxt_exchanges(ccxt_module=None) -> List[str]:
"""
Return the list of all exchanges known to ccxt
"""
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
def available_exchanges(ccxt_module=None) -> List[str]:
"""
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
"""
exchanges = ccxt_exchanges(ccxt_module)
return [x for x in exchanges if not is_exchange_bad(x)]
def timeframe_to_seconds(ticker_interval: str) -> int: def timeframe_to_seconds(ticker_interval: str) -> int:
""" """
Translates the timeframe interval value written in the human readable Translates the timeframe interval value written in the human readable
@@ -824,11 +1081,9 @@ def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
""" """
if not date: if not date:
date = datetime.now(timezone.utc) date = datetime.now(timezone.utc)
timeframe_secs = timeframe_to_seconds(timeframe)
# Get offset based on timerame_secs new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
offset = date.timestamp() % timeframe_secs ROUND_DOWN) // 1000
# Subtract seconds passed since last offset
new_timestamp = date.timestamp() - offset
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
@@ -839,9 +1094,32 @@ def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
:param date: date to use. Defaults to utcnow() :param date: date to use. Defaults to utcnow()
:returns: date of next candle (with utc timezone) :returns: date of next candle (with utc timezone)
""" """
prevdate = timeframe_to_prev_date(timeframe, date) if not date:
timeframe_secs = timeframe_to_seconds(timeframe) date = datetime.now(timezone.utc)
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
# Add one interval to previous candle ROUND_UP) // 1000
new_timestamp = prevdate.timestamp() + timeframe_secs
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc) return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
def symbol_is_pair(market_symbol: str, base_currency: str = None, quote_currency: str = None):
"""
Check if the market symbol is a pair, i.e. that its symbol consists of the base currency and the
quote currency separated by '/' character. If base_currency and/or quote_currency is passed,
it also checks that the symbol contains appropriate base and/or quote currency part before
and after the separating character correspondingly.
"""
symbol_parts = market_symbol.split('/')
return (len(symbol_parts) == 2 and
(symbol_parts[0] == base_currency if base_currency else len(symbol_parts[0]) > 0) and
(symbol_parts[1] == quote_currency if quote_currency else len(symbol_parts[1]) > 0))
def market_is_active(market):
"""
Return True if the market is active.
"""
# "It's active, if the active flag isn't explicitly set to false. If it's missing or
# true then it's true. If it's undefined, then it's most likely true, but not 100% )"
# See https://github.com/ccxt/ccxt/issues/4874,
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
return market.get('active', True) is not False

View File

@@ -2,7 +2,11 @@
import logging import logging
from typing import Dict from typing import Dict
import ccxt
from freqtrade import OperationalException, TemporaryError
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange
from freqtrade.exchange.exchange import retrier
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -10,3 +14,37 @@ logger = logging.getLogger(__name__)
class Kraken(Exchange): class Kraken(Exchange):
_params: Dict = {"trading_agreement": "agree"} _params: Dict = {"trading_agreement": "agree"}
_ft_has: Dict = {
"trades_pagination": "id",
"trades_pagination_arg": "since",
}
@retrier
def get_balances(self) -> dict:
if self._config['dry_run']:
return {}
try:
balances = self._api.fetch_balance()
# Remove additional info from ccxt results
balances.pop("info", None)
balances.pop("free", None)
balances.pop("total", None)
balances.pop("used", None)
orders = self._api.fetch_open_orders()
order_list = [(x["symbol"].split("/")[0 if x["side"] == "sell" else 1],
x["remaining"],
# Don't remove the below comment, this can be important for debuggung
# x["side"], x["amount"],
) for x in orders]
for bal in balances:
balances[bal]['used'] = sum(order[1] for order in order_list if order[0] == bal)
balances[bal]['free'] = balances[bal]['total'] - balances[bal]['used']
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not get balance due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@@ -1,35 +1,36 @@
""" """
Freqtrade is the main module of this bot. It contains the class Freqtrade() Freqtrade is the main module of this bot. It contains the class Freqtrade()
""" """
import copy import copy
import logging import logging
import traceback import traceback
from datetime import datetime from datetime import datetime
from math import isclose
from os import getpid
from typing import Any, Dict, List, Optional, Tuple from typing import Any, Dict, List, Optional, Tuple
import arrow import arrow
from requests.exceptions import RequestException from requests.exceptions import RequestException
from freqtrade import (DependencyException, OperationalException, InvalidOrderException, from freqtrade import (DependencyException, InvalidOrderException, __version__,
__version__, constants, persistence) constants, persistence)
from freqtrade.configuration import validate_config_consistency
from freqtrade.data.converter import order_book_to_dataframe from freqtrade.data.converter import order_book_to_dataframe
from freqtrade.data.dataprovider import DataProvider from freqtrade.data.dataprovider import DataProvider
from freqtrade.edge import Edge from freqtrade.edge import Edge
from freqtrade.configuration import validate_config_consistency
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date from freqtrade.exchange import timeframe_to_minutes, timeframe_to_next_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.resolvers import (ExchangeResolver, PairListResolver,
StrategyResolver)
from freqtrade.rpc import RPCManager, RPCMessageType from freqtrade.rpc import RPCManager, RPCMessageType
from freqtrade.resolvers import ExchangeResolver, StrategyResolver, PairListResolver from freqtrade.state import State
from freqtrade.state import State, RunMode from freqtrade.strategy.interface import IStrategy, SellType
from freqtrade.strategy.interface import SellType, IStrategy
from freqtrade.wallets import Wallets from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FreqtradeBot(object): class FreqtradeBot:
""" """
Freqtrade is the main class of the bot. Freqtrade is the main class of the bot.
This is from here the bot start its logic. This is from here the bot start its logic.
@@ -50,13 +51,15 @@ class FreqtradeBot(object):
# Init objects # Init objects
self.config = config self.config = config
self._heartbeat_msg = 0
self.heartbeat_interval = self.config.get('internals', {}).get('heartbeat_interval', 60)
self.strategy: IStrategy = StrategyResolver(self.config).strategy self.strategy: IStrategy = StrategyResolver(self.config).strategy
# Check config consistency here since strategies can set certain options # Check config consistency here since strategies can set certain options
validate_config_consistency(config) validate_config_consistency(config)
self.rpc: RPCManager = RPCManager(self)
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.wallets = Wallets(self.config, self.exchange) self.wallets = Wallets(self.config, self.exchange)
@@ -79,16 +82,17 @@ class FreqtradeBot(object):
persistence.init(self.config.get('db_url', None), persistence.init(self.config.get('db_url', None),
clean_open_orders=self.config.get('dry_run', False)) clean_open_orders=self.config.get('dry_run', False))
# Stoploss on exchange does not make sense, therefore we need to disable that.
if (self.dataprovider.runmode == RunMode.DRY_RUN and
self.strategy.order_types.get('stoploss_on_exchange', False)):
logger.info("Disabling stoploss_on_exchange during dry-run.")
self.strategy.order_types['stoploss_on_exchange'] = False
config['order_types']['stoploss_on_exchange'] = False
# Set initial bot state from config # Set initial bot state from config
initial_state = self.config.get('initial_state') initial_state = self.config.get('initial_state')
self.state = State[initial_state.upper()] if initial_state else State.STOPPED self.state = State[initial_state.upper()] if initial_state else State.STOPPED
# RPC runs in separate threads, can start handling external commands just after
# initialization, even before Freqtradebot has a chance to start its throttling,
# so anything in the Freqtradebot instance should be ready (initialized), including
# the initial state of the bot.
# Keep this at the end of this initialization method.
self.rpc: RPCManager = RPCManager(self)
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Cleanup pending resources on an already stopped bot Cleanup pending resources on an already stopped bot
@@ -140,18 +144,22 @@ class FreqtradeBot(object):
self.strategy.informative_pairs()) self.strategy.informative_pairs())
# First process current opened trades # First process current opened trades
for trade in trades: self.process_maybe_execute_sells(trades)
self.process_maybe_execute_sell(trade)
# Then looking for buy opportunities # Then looking for buy opportunities
if len(trades) < self.config['max_open_trades']: if len(trades) < self.config['max_open_trades']:
self.process_maybe_execute_buy() self.process_maybe_execute_buys()
if 'unfilledtimeout' in self.config: if 'unfilledtimeout' in self.config:
# Check and handle any timed out open orders # Check and handle any timed out open orders
self.check_handle_timedout() self.check_handle_timedout()
Trade.session.flush() Trade.session.flush()
if (self.heartbeat_interval
and (arrow.utcnow().timestamp - self._heartbeat_msg > self.heartbeat_interval)):
logger.info(f"Bot heartbeat. PID={getpid()}")
self._heartbeat_msg = arrow.utcnow().timestamp
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]): def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
""" """
Extend whitelist with pairs from open trades Extend whitelist with pairs from open trades
@@ -216,7 +224,7 @@ class FreqtradeBot(object):
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT: if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
open_trades = len(Trade.get_open_trades()) open_trades = len(Trade.get_open_trades())
if open_trades >= self.config['max_open_trades']: if open_trades >= self.config['max_open_trades']:
logger.warning('Can\'t open a new trade: max number of trades is reached') logger.warning("Can't open a new trade: max number of trades is reached")
return None return None
return available_amount / (self.config['max_open_trades'] - open_trades) return available_amount / (self.config['max_open_trades'] - open_trades)
@@ -267,11 +275,10 @@ class FreqtradeBot(object):
Checks pairs as long as the open trade count is below `max_open_trades`. Checks pairs as long as the open trade count is below `max_open_trades`.
:return: True if at least one trade has been created. :return: True if at least one trade has been created.
""" """
interval = self.strategy.ticker_interval
whitelist = copy.deepcopy(self.active_pair_whitelist) whitelist = copy.deepcopy(self.active_pair_whitelist)
if not whitelist: if not whitelist:
logger.warning("Whitelist is empty.") logger.info("Active pair whitelist is empty.")
return False return False
# Remove currently opened and latest pairs from whitelist # Remove currently opened and latest pairs from whitelist
@@ -281,7 +288,8 @@ class FreqtradeBot(object):
logger.debug('Ignoring %s in pair whitelist', trade.pair) logger.debug('Ignoring %s in pair whitelist', trade.pair)
if not whitelist: if not whitelist:
logger.info("No currency pair in whitelist, but checking to sell open trades.") logger.info("No currency pair in active pair whitelist, "
"but checking to sell open trades.")
return False return False
buycount = 0 buycount = 0
@@ -290,8 +298,10 @@ class FreqtradeBot(object):
if self.strategy.is_pair_locked(_pair): if self.strategy.is_pair_locked(_pair):
logger.info(f"Pair {_pair} is currently locked.") logger.info(f"Pair {_pair} is currently locked.")
continue continue
(buy, sell) = self.strategy.get_signal( (buy, sell) = self.strategy.get_signal(
_pair, interval, self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval)) _pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(_pair, self.strategy.ticker_interval))
if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']: if buy and not sell and len(Trade.get_open_trades()) < self.config['max_open_trades']:
stake_amount = self._get_trade_stake_amount(_pair) stake_amount = self._get_trade_stake_amount(_pair)
@@ -351,8 +361,8 @@ class FreqtradeBot(object):
min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested) min_stake_amount = self._get_min_pair_stake_amount(pair_s, buy_limit_requested)
if min_stake_amount is not None and min_stake_amount > stake_amount: if min_stake_amount is not None and min_stake_amount > stake_amount:
logger.warning( logger.warning(
f'Can\'t open a new trade for {pair_s}: stake amount ' f"Can't open a new trade for {pair_s}: stake amount "
f'is too small ({stake_amount} < {min_stake_amount})' f"is too small ({stake_amount} < {min_stake_amount})"
) )
return False return False
@@ -436,52 +446,48 @@ class FreqtradeBot(object):
return True return True
def process_maybe_execute_buy(self) -> None: def process_maybe_execute_buys(self) -> None:
""" """
Tries to execute a buy trade in a safe way Tries to execute buy orders for trades in a safe way
:return: True if executed
""" """
try: try:
# Create entity and execute trade # Create entity and execute trade
if not self.create_trades(): if not self.create_trades():
logger.info('Found no buy signals for whitelisted currencies. Trying again...') logger.debug('Found no buy signals for whitelisted currencies. Trying again...')
except DependencyException as exception: except DependencyException as exception:
logger.warning('Unable to create trade: %s', exception) logger.warning('Unable to create trade: %s', exception)
def process_maybe_execute_sell(self, trade: Trade) -> bool: def process_maybe_execute_sells(self, trades: List[Any]) -> None:
""" """
Tries to execute a sell trade Tries to execute sell orders for trades in a safe way
:return: True if executed
""" """
try: result = False
self.update_trade_state(trade) for trade in trades:
try:
self.update_trade_state(trade)
if self.strategy.order_types.get('stoploss_on_exchange') and trade.is_open: if (self.strategy.order_types.get('stoploss_on_exchange') and
result = self.handle_stoploss_on_exchange(trade) self.handle_stoploss_on_exchange(trade)):
if result: result = True
self.wallets.update() continue
return result
if trade.is_open and trade.open_order_id is None:
# Check if we can sell our current pair # Check if we can sell our current pair
result = self.handle_trade(trade) if trade.open_order_id is None and self.handle_trade(trade):
result = True
# Updating wallets if any trade occured except DependencyException as exception:
if result: logger.warning('Unable to sell trade: %s', exception)
self.wallets.update()
return result # Updating wallets if any trade occured
if result:
self.wallets.update()
except DependencyException as exception: def get_real_amount(self, trade: Trade, order: Dict, order_amount: float = None) -> float:
logger.warning('Unable to sell trade: %s', exception)
return False
def get_real_amount(self, trade: Trade, order: Dict) -> float:
""" """
Get real amount for the trade Get real amount for the trade
Necessary for exchanges which charge fees in base currency (e.g. binance) Necessary for exchanges which charge fees in base currency (e.g. binance)
""" """
order_amount = order['amount'] if order_amount is None:
order_amount = order['amount']
# Only run for closed orders # Only run for closed orders
if trade.fee_open == 0 or order['status'] == 'open': if trade.fee_open == 0 or order['status'] == 'open':
return order_amount return order_amount
@@ -516,9 +522,9 @@ class FreqtradeBot(object):
trade.pair.startswith(exectrade['fee']['currency'])): trade.pair.startswith(exectrade['fee']['currency'])):
fee_abs += exectrade['fee']['cost'] fee_abs += exectrade['fee']['cost']
if amount != order_amount: if not isclose(amount, order_amount, abs_tol=constants.MATH_CLOSE_PREC):
logger.warning(f"Amount {amount} does not match amount {trade.amount}") logger.warning(f"Amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match") raise DependencyException("Half bought? Amounts don't match")
real_amount = amount - fee_abs real_amount = amount - fee_abs
if fee_abs != 0: if fee_abs != 0:
logger.info(f"Applying fee on amount for {trade} " logger.info(f"Applying fee on amount for {trade} "
@@ -541,12 +547,12 @@ class FreqtradeBot(object):
# Try update amount (binance-fix) # Try update amount (binance-fix)
try: try:
new_amount = self.get_real_amount(trade, order) new_amount = self.get_real_amount(trade, order)
if order['amount'] != new_amount: if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
order['amount'] = new_amount order['amount'] = new_amount
# Fee was applied, so set to 0 # Fee was applied, so set to 0
trade.fee_open = 0 trade.fee_open = 0
except OperationalException as exception: except DependencyException as exception:
logger.warning("Could not update trade amount: %s", exception) logger.warning("Could not update trade amount: %s", exception)
trade.update(order) trade.update(order)
@@ -580,18 +586,20 @@ class FreqtradeBot(object):
:return: True if trade has been sold, False otherwise :return: True if trade has been sold, False otherwise
""" """
if not trade.is_open: if not trade.is_open:
raise ValueError(f'Attempt to handle closed trade: {trade}') raise DependencyException(f'Attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade) logger.debug('Handling %s ...', trade)
(buy, sell) = (False, False) (buy, sell) = (False, False)
experimental = self.config.get('experimental', {})
if experimental.get('use_sell_signal') or experimental.get('ignore_roi_if_buy_signal'): config_ask_strategy = self.config.get('ask_strategy', {})
if (config_ask_strategy.get('use_sell_signal', True) or
config_ask_strategy.get('ignore_roi_if_buy_signal')):
(buy, sell) = self.strategy.get_signal( (buy, sell) = self.strategy.get_signal(
trade.pair, self.strategy.ticker_interval, trade.pair, self.strategy.ticker_interval,
self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval)) self.dataprovider.ohlcv(trade.pair, self.strategy.ticker_interval))
config_ask_strategy = self.config.get('ask_strategy', {})
if config_ask_strategy.get('use_order_book', False): if config_ask_strategy.get('use_order_book', False):
logger.info('Using order book for selling...') logger.info('Using order book for selling...')
# logger.debug('Order book %s',orderBook) # logger.debug('Order book %s',orderBook)
@@ -617,6 +625,33 @@ class FreqtradeBot(object):
logger.debug('Found no sell signal for %s.', trade) logger.debug('Found no sell signal for %s.', trade)
return False return False
def create_stoploss_order(self, trade: Trade, stop_price: float, rate: float) -> bool:
"""
Abstracts creating stoploss orders from the logic.
Handles errors and updates the trade database object.
Force-sells the pair (using EmergencySell reason) in case of Problems creating the order.
:return: True if the order succeeded, and False in case of problems.
"""
# Limit price threshold: As limit price should always be below price
LIMIT_PRICE_PCT = 0.99
try:
stoploss_order = self.exchange.stoploss_limit(pair=trade.pair, amount=trade.amount,
stop_price=stop_price,
rate=rate * LIMIT_PRICE_PCT)
trade.stoploss_order_id = str(stoploss_order['id'])
return True
except InvalidOrderException as e:
trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Selling the trade forcefully')
self.execute_sell(trade, trade.stop_loss, sell_reason=SellType.EMERGENCY_SELL)
except DependencyException:
trade.stoploss_order_id = None
logger.exception('Unable to place a stoploss order on exchange.')
return False
def handle_stoploss_on_exchange(self, trade: Trade) -> bool: def handle_stoploss_on_exchange(self, trade: Trade) -> bool:
""" """
Check if trade is fulfilled in which case the stoploss Check if trade is fulfilled in which case the stoploss
@@ -635,49 +670,25 @@ class FreqtradeBot(object):
except InvalidOrderException as exception: except InvalidOrderException as exception:
logger.warning('Unable to fetch stoploss order: %s', exception) logger.warning('Unable to fetch stoploss order: %s', exception)
# If trade open order id does not exist: buy order is fulfilled
buy_order_fulfilled = not trade.open_order_id
# Limit price threshold: As limit price should always be below price
limit_price_pct = 0.99
# If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange # If buy order is fulfilled but there is no stoploss, we add a stoploss on exchange
if (buy_order_fulfilled and not stoploss_order): if (not trade.open_order_id and not stoploss_order):
if self.edge:
stoploss = self.edge.stoploss(pair=trade.pair) stoploss = self.edge.stoploss(pair=trade.pair) if self.edge else self.strategy.stoploss
else:
stoploss = self.strategy.stoploss
stop_price = trade.open_rate * (1 + stoploss) stop_price = trade.open_rate * (1 + stoploss)
# limit price should be less than stop price. if self.create_stoploss_order(trade=trade, stop_price=stop_price, rate=stop_price):
limit_price = stop_price * limit_price_pct
try:
stoploss_order_id = self.exchange.stoploss_limit(
pair=trade.pair, amount=trade.amount, stop_price=stop_price, rate=limit_price
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
trade.stoploss_last_update = datetime.now() trade.stoploss_last_update = datetime.now()
return False return False
except DependencyException as exception:
trade.stoploss_order_id = None
logger.warning('Unable to place a stoploss order on exchange: %s', exception)
# If stoploss order is canceled for some reason we add it # If stoploss order is canceled for some reason we add it
if stoploss_order and stoploss_order['status'] == 'canceled': if stoploss_order and stoploss_order['status'] == 'canceled':
try: if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
stoploss_order_id = self.exchange.stoploss_limit( rate=trade.stop_loss):
pair=trade.pair, amount=trade.amount,
stop_price=trade.stop_loss, rate=trade.stop_loss * limit_price_pct
)['id']
trade.stoploss_order_id = str(stoploss_order_id)
return False return False
except DependencyException as exception: else:
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.warning('Stoploss order was cancelled, ' logger.warning('Stoploss order was cancelled, but unable to recreate one.')
'but unable to recreate one: %s', exception)
# We check if stoploss order is fulfilled # We check if stoploss order is fulfilled
if stoploss_order and stoploss_order['status'] == 'closed': if stoploss_order and stoploss_order['status'] == 'closed':
@@ -686,7 +697,7 @@ class FreqtradeBot(object):
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, self.strategy.lock_pair(trade.pair,
timeframe_to_next_date(self.config['ticker_interval'])) timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade) self._notify_sell(trade, "stoploss")
return True return True
# Finally we check if stoploss on exchange should be moved up because of trailing. # Finally we check if stoploss on exchange should be moved up because of trailing.
@@ -710,7 +721,7 @@ class FreqtradeBot(object):
if trade.stop_loss > float(order['info']['stopPrice']): if trade.stop_loss > float(order['info']['stopPrice']):
# we check if the update is neccesary # we check if the update is neccesary
update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60) update_beat = self.strategy.order_types.get('stoploss_on_exchange_interval', 60)
if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() > update_beat: if (datetime.utcnow() - trade.stoploss_last_update).total_seconds() >= update_beat:
# cancelling the current stoploss on exchange first # cancelling the current stoploss on exchange first
logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})' logger.info('Trailing stoploss: cancelling current stoploss on exchange (id:{%s})'
'in order to add another one ...', order['id']) 'in order to add another one ...', order['id'])
@@ -720,17 +731,13 @@ class FreqtradeBot(object):
logger.exception(f"Could not cancel stoploss order {order['id']} " logger.exception(f"Could not cancel stoploss order {order['id']} "
f"for pair {trade.pair}") f"for pair {trade.pair}")
try: # Create new stoploss order
# creating the new one if self.create_stoploss_order(trade=trade, stop_price=trade.stop_loss,
stoploss_order_id = self.exchange.stoploss_limit( rate=trade.stop_loss):
pair=trade.pair, amount=trade.amount, return False
stop_price=trade.stop_loss, rate=trade.stop_loss * 0.99 else:
)['id'] logger.warning(f"Could not create trailing stoploss order "
trade.stoploss_order_id = str(stoploss_order_id) f"for pair {trade.pair}.")
except DependencyException:
trade.stoploss_order_id = None
logger.exception(f"Could not create trailing stoploss order "
f"for pair {trade.pair}.")
def _check_and_execute_sell(self, trade: Trade, sell_rate: float, def _check_and_execute_sell(self, trade: Trade, sell_rate: float,
buy: bool, sell: bool) -> bool: buy: bool, sell: bool) -> bool:
@@ -756,8 +763,8 @@ class FreqtradeBot(object):
""" """
buy_timeout = self.config['unfilledtimeout']['buy'] buy_timeout = self.config['unfilledtimeout']['buy']
sell_timeout = self.config['unfilledtimeout']['sell'] sell_timeout = self.config['unfilledtimeout']['sell']
buy_timeoutthreashold = arrow.utcnow().shift(minutes=-buy_timeout).datetime buy_timeout_threshold = arrow.utcnow().shift(minutes=-buy_timeout).datetime
sell_timeoutthreashold = arrow.utcnow().shift(minutes=-sell_timeout).datetime sell_timeout_threshold = arrow.utcnow().shift(minutes=-sell_timeout).datetime
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all(): for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try: try:
@@ -781,21 +788,18 @@ class FreqtradeBot(object):
self.wallets.update() self.wallets.update()
continue continue
# Handle cancelled on exchange if ((order['side'] == 'buy' and order['status'] == 'canceled')
if order['status'] == 'canceled': or (order['status'] == 'open'
if order['side'] == 'buy': and order['side'] == 'buy' and ordertime < buy_timeout_threshold)):
self.handle_buy_order_full_cancel(trade, "canceled on Exchange")
elif order['side'] == 'sell': self.handle_timedout_limit_buy(trade, order)
self.handle_timedout_limit_sell(trade, order) self.wallets.update()
self.wallets.update()
# Check if order is still actually open elif ((order['side'] == 'sell' and order['status'] == 'canceled')
elif order['status'] == 'open': or (order['status'] == 'open'
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: and order['side'] == 'sell' and ordertime < sell_timeout_threshold)):
self.handle_timedout_limit_buy(trade, order) self.handle_timedout_limit_sell(trade, order)
self.wallets.update() self.wallets.update()
elif order['side'] == 'sell' and ordertime < sell_timeoutthreashold:
self.handle_timedout_limit_sell(trade, order)
self.wallets.update()
def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None: def handle_buy_order_full_cancel(self, trade: Trade, reason: str) -> None:
"""Close trade in database and send message""" """Close trade in database and send message"""
@@ -811,16 +815,33 @@ class FreqtradeBot(object):
"""Buy timeout - cancel order """Buy timeout - cancel order
:return: True if order was fully cancelled :return: True if order was fully cancelled
""" """
self.exchange.cancel_order(trade.open_order_id, trade.pair) reason = "cancelled due to timeout"
if order['remaining'] == order['amount']: if order['status'] != 'canceled':
corder = self.exchange.cancel_order(trade.open_order_id, trade.pair)
else:
# Order was cancelled already, so we can reuse the existing dict
corder = order
reason = "canceled on Exchange"
if corder['remaining'] == corder['amount']:
# if trade is not partially completed, just delete the trade # if trade is not partially completed, just delete the trade
self.handle_buy_order_full_cancel(trade, "cancelled due to timeout") self.handle_buy_order_full_cancel(trade, reason)
return True return True
# if trade is partially complete, edit the stake details for the trade # if trade is partially complete, edit the stake details for the trade
# and close the order # and close the order
trade.amount = order['amount'] - order['remaining'] trade.amount = corder['amount'] - corder['remaining']
trade.stake_amount = trade.amount * trade.open_rate trade.stake_amount = trade.amount * trade.open_rate
# verify if fees were taken from amount to avoid problems during selling
try:
new_amount = self.get_real_amount(trade, corder, trade.amount)
if not isclose(order['amount'], new_amount, abs_tol=constants.MATH_CLOSE_PREC):
trade.amount = new_amount
# Fee was applied, so set to 0
trade.fee_open = 0
except DependencyException as e:
logger.warning("Could not update trade amount: %s", e)
trade.open_order_id = None trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade) logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg({ self.rpc.send_msg({
@@ -883,9 +904,14 @@ class FreqtradeBot(object):
except InvalidOrderException: except InvalidOrderException:
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}") logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
ordertype = self.strategy.order_types[sell_type]
if sell_reason == SellType.EMERGENCY_SELL:
# Emergencysells (default to market!)
ordertype = self.strategy.order_types.get("emergencysell", "market")
# Execute sell and update trade record # Execute sell and update trade record
order = self.exchange.sell(pair=str(trade.pair), order = self.exchange.sell(pair=str(trade.pair),
ordertype=self.strategy.order_types[sell_type], ordertype=ordertype,
amount=trade.amount, rate=limit, amount=trade.amount, rate=limit,
time_in_force=self.strategy.order_time_in_force['sell'] time_in_force=self.strategy.order_time_in_force['sell']
) )
@@ -901,9 +927,9 @@ class FreqtradeBot(object):
# Lock pair for one candle to prevent immediate rebuys # Lock pair for one candle to prevent immediate rebuys
self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval'])) self.strategy.lock_pair(trade.pair, timeframe_to_next_date(self.config['ticker_interval']))
self._notify_sell(trade) self._notify_sell(trade, ordertype)
def _notify_sell(self, trade: Trade): def _notify_sell(self, trade: Trade, order_type: str):
""" """
Sends rpc notification when a sell occured. Sends rpc notification when a sell occured.
""" """
@@ -920,7 +946,7 @@ class FreqtradeBot(object):
'pair': trade.pair, 'pair': trade.pair,
'gain': gain, 'gain': gain,
'limit': trade.close_rate_requested, 'limit': trade.close_rate_requested,
'order_type': self.strategy.order_types['sell'], 'order_type': order_type,
'amount': trade.amount, 'amount': trade.amount,
'open_rate': trade.open_rate, 'open_rate': trade.open_rate,
'current_rate': current_rate, 'current_rate': current_rate,

View File

@@ -1,40 +0,0 @@
from math import cos, exp, pi, sqrt
import numpy as np
import talib as ta
from pandas import Series
def went_up(series: Series) -> bool:
return series > series.shift(1)
def went_down(series: Series) -> bool:
return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
magic = pi * sqrt(2) / smoothing
a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic)
coeff3 = -a1 * a1
coeff1 = (1 - coeff2 - coeff3) / 2
filtered = series.copy()
for i in range(2, len(series)):
filtered.iloc[i] = coeff1 * (series.iloc[i] + series.iloc[i-1]) + \
coeff2 * filtered.iloc[i-1] + coeff3 * filtered.iloc[i-2]
return filtered
def fishers_inverse(series: Series, smoothing: float = 0) -> np.ndarray:
""" Does a smoothed fishers inverse transformation.
Can be used with any oscillator that goes from 0 to 100 like RSI or MFI """
v1 = 0.1 * (series - 50)
if smoothing > 0:
v2 = ta.WMA(v1.values, timeperiod=smoothing)
else:
v2 = v1
return (np.exp(2 * v2)-1) / (np.exp(2 * v2) + 1)

View File

@@ -11,7 +11,6 @@ if sys.version_info < (3, 6):
# flake8: noqa E402 # flake8: noqa E402
import logging import logging
from argparse import Namespace
from typing import Any, List from typing import Any, List
from freqtrade import OperationalException from freqtrade import OperationalException
@@ -31,16 +30,13 @@ def main(sysargv: List[str] = None) -> None:
return_code: Any = 1 return_code: Any = 1
worker = None worker = None
try: try:
arguments = Arguments( arguments = Arguments(sysargv)
sysargv, args = arguments.get_parsed_arg()
'Free, open source crypto trading bot'
)
args: Namespace = arguments.get_parsed_arg()
# A subcommand has been issued. # A subcommand has been issued.
# Means if Backtesting or Hyperopt have been called we exit the bot # Means if Backtesting or Hyperopt have been called we exit the bot
if hasattr(args, 'func'): if 'func' in args:
args.func(args) args['func'](args)
# TODO: fetch return_code as returned by the command function here # TODO: fetch return_code as returned by the command function here
return_code = 0 return_code = 0
else: else:

View File

@@ -72,8 +72,10 @@ def json_load(datafile: IO):
def file_load_json(file): def file_load_json(file):
gzipfile = file.with_suffix(file.suffix + '.gz') if file.suffix != ".gz":
gzipfile = file.with_suffix(file.suffix + '.gz')
else:
gzipfile = file
# Try gzip file first, otherwise regular json file. # Try gzip file first, otherwise regular json file.
if gzipfile.is_file(): if gzipfile.is_file():
logger.debug('Loading ticker data from file %s', gzipfile) logger.debug('Loading ticker data from file %s', gzipfile)
@@ -114,3 +116,14 @@ def deep_merge_dicts(source, destination):
destination[key] = value destination[key] = value
return destination return destination
def round_dict(d, n):
"""
Rounds float values in the dict to n digits after the decimal point.
"""
return {k: (round(v, n) if isinstance(v, float) else v) for k, v in d.items()}
def plural(num, singular: str, plural: str = None) -> str:
return singular if (num == 1 or num == -1) else plural or singular + 's'

View File

@@ -1,10 +1,7 @@
import logging import logging
from argparse import Namespace
from typing import Any, Dict from typing import Any, Dict
from filelock import FileLock, Timeout from freqtrade import DependencyException, constants, OperationalException
from freqtrade import DependencyException, constants
from freqtrade.state import RunMode from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration from freqtrade.utils import setup_utils_configuration
@@ -12,7 +9,7 @@ from freqtrade.utils import setup_utils_configuration
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]: def setup_configuration(args: Dict[str, Any], method: RunMode) -> Dict[str, Any]:
""" """
Prepare the configuration for the Hyperopt module Prepare the configuration for the Hyperopt module
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
@@ -25,20 +22,10 @@ def setup_configuration(args: Namespace, method: RunMode) -> Dict[str, Any]:
raise DependencyException('stake amount could not be "%s" for backtesting' % raise DependencyException('stake amount could not be "%s" for backtesting' %
constants.UNLIMITED_STAKE_AMOUNT) constants.UNLIMITED_STAKE_AMOUNT)
if method == RunMode.HYPEROPT:
# Special cases for Hyperopt
if config.get('strategy') and config.get('strategy') != 'DefaultStrategy':
logger.error("Please don't use --strategy for hyperopt.")
logger.error(
"Read the documentation at "
"https://github.com/freqtrade/freqtrade/blob/develop/docs/hyperopt.md "
"to understand how to configure hyperopt.")
raise DependencyException("--strategy configured but not supported for hyperopt")
return config return config
def start_backtesting(args: Namespace) -> None: def start_backtesting(args: Dict[str, Any]) -> None:
""" """
Start Backtesting script Start Backtesting script
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
@@ -57,15 +44,19 @@ def start_backtesting(args: Namespace) -> None:
backtesting.start() backtesting.start()
def start_hyperopt(args: Namespace) -> None: def start_hyperopt(args: Dict[str, Any]) -> None:
""" """
Start hyperopt script Start hyperopt script
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: None :return: None
""" """
# Import here to avoid loading hyperopt module when it's not used # Import here to avoid loading hyperopt module when it's not used
from freqtrade.optimize.hyperopt import Hyperopt try:
from filelock import FileLock, Timeout
from freqtrade.optimize.hyperopt import Hyperopt
except ImportError as e:
raise OperationalException(
f"{e}. Please ensure that the hyperopt dependencies are installed.") from e
# Initialize configuration # Initialize configuration
config = setup_configuration(args, RunMode.HYPEROPT) config = setup_configuration(args, RunMode.HYPEROPT)
@@ -95,7 +86,7 @@ def start_hyperopt(args: Namespace) -> None:
# Same in Edge and Backtesting start() functions. # Same in Edge and Backtesting start() functions.
def start_edge(args: Namespace) -> None: def start_edge(args: Dict[str, Any]) -> None:
""" """
Start Edge script Start Edge script
:param args: Cli args from Arguments() :param args: Cli args from Arguments()

View File

@@ -44,7 +44,7 @@ class BacktestResult(NamedTuple):
sell_reason: SellType sell_reason: SellType
class Backtesting(object): class Backtesting:
""" """
Backtesting class, this class contains all the logic to run a backtest Backtesting class, this class contains all the logic to run a backtest
@@ -63,9 +63,12 @@ class Backtesting(object):
self.config['exchange']['uid'] = '' self.config['exchange']['uid'] = ''
self.config['dry_run'] = True self.config['dry_run'] = True
self.strategylist: List[IStrategy] = [] self.strategylist: List[IStrategy] = []
self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange self.exchange = ExchangeResolver(self.config['exchange']['name'], self.config).exchange
self.fee = self.exchange.get_fee()
if config.get('fee'):
self.fee = config['fee']
else:
self.fee = self.exchange.get_fee()
if self.config.get('runmode') != RunMode.HYPEROPT: if self.config.get('runmode') != RunMode.HYPEROPT:
self.dataprovider = DataProvider(self.config, self.exchange) self.dataprovider = DataProvider(self.config, self.exchange)
@@ -81,6 +84,12 @@ class Backtesting(object):
# No strategy list specified, only one strategy # No strategy list specified, only one strategy
self.strategylist.append(StrategyResolver(self.config).strategy) self.strategylist.append(StrategyResolver(self.config).strategy)
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
self.ticker_interval = str(self.config.get('ticker_interval'))
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
# Load one (first) strategy # Load one (first) strategy
self._set_strategy(self.strategylist[0]) self._set_strategy(self.strategylist[0])
@@ -89,14 +98,6 @@ class Backtesting(object):
Load strategy into backtesting Load strategy into backtesting
""" """
self.strategy = strategy self.strategy = strategy
if "ticker_interval" not in self.config:
raise OperationalException("Ticker-interval needs to be set in either configuration "
"or as cli argument `--ticker-interval 5m`")
self.ticker_interval = self.config.get('ticker_interval')
self.ticker_interval_mins = timeframe_to_minutes(self.ticker_interval)
self.advise_buy = strategy.advise_buy
self.advise_sell = strategy.advise_sell
# Set stoploss_on_exchange to false for backtesting, # Set stoploss_on_exchange to false for backtesting,
# since a "perfect" stoploss-sell is assumed anyway # since a "perfect" stoploss-sell is assumed anyway
# And the regular "stoploss" function would not apply to that case # And the regular "stoploss" function would not apply to that case
@@ -148,8 +149,8 @@ class Backtesting(object):
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") floatfmt=floatfmt, tablefmt="pipe") # type: ignore
def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str: def _generate_text_table_sell_reason(self, data: Dict[str, Dict], results: DataFrame) -> str:
""" """
@@ -187,8 +188,8 @@ class Backtesting(object):
len(results[results.profit_abs < 0]) len(results[results.profit_abs < 0])
]) ])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") floatfmt=floatfmt, tablefmt="pipe") # type: ignore
def _store_backtest_result(self, recordfilename: Path, results: DataFrame, def _store_backtest_result(self, recordfilename: Path, results: DataFrame,
strategyname: Optional[str] = None) -> None: strategyname: Optional[str] = None) -> None:
@@ -219,8 +220,8 @@ class Backtesting(object):
for pair, pair_data in processed.items(): for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.advise_sell( ticker_data = self.strategy.advise_sell(
self.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy() self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle # to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1) ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
@@ -239,14 +240,16 @@ class Backtesting(object):
stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]: stake_amount: float, max_open_trades: int) -> Optional[BacktestResult]:
trade = Trade( trade = Trade(
pair=pair,
open_rate=buy_row.open, open_rate=buy_row.open,
open_date=buy_row.date, open_date=buy_row.date,
stake_amount=stake_amount, stake_amount=stake_amount,
amount=stake_amount / buy_row.open, amount=stake_amount / buy_row.open,
fee_open=self.fee, fee_open=self.fee,
fee_close=self.fee fee_close=self.fee,
is_open=True,
) )
logger.debug(f"{pair} - Backtesting emulates creation of new trade: {trade}.")
# calculate win/lose forwards from buy point # calculate win/lose forwards from buy point
for sell_row in partial_ticker: for sell_row in partial_ticker:
if max_open_trades > 0: if max_open_trades > 0:
@@ -267,6 +270,11 @@ class Backtesting(object):
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1) # - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
closerate = - (trade.open_rate * roi + trade.open_rate * closerate = - (trade.open_rate * roi + trade.open_rate *
(1 + trade.fee_open)) / (trade.fee_close - 1) (1 + trade.fee_open)) / (trade.fee_close - 1)
# Use the maximum between closerate and low as we
# cannot sell outside of a candle.
# Applies when using {"xx": -1} as roi to force sells after xx minutes
closerate = max(closerate, sell_row.low)
else: else:
# This should not be reached... # This should not be reached...
closerate = sell_row.open closerate = sell_row.open
@@ -289,23 +297,25 @@ class Backtesting(object):
if partial_ticker: if partial_ticker:
# no sell condition found - trade stil open at end of backtest period # no sell condition found - trade stil open at end of backtest period
sell_row = partial_ticker[-1] sell_row = partial_ticker[-1]
btr = BacktestResult(pair=pair, bt_res = BacktestResult(pair=pair,
profit_percent=trade.calc_profit_percent(rate=sell_row.open), profit_percent=trade.calc_profit_percent(rate=sell_row.open),
profit_abs=trade.calc_profit(rate=sell_row.open), profit_abs=trade.calc_profit(rate=sell_row.open),
open_time=buy_row.date, open_time=buy_row.date,
close_time=sell_row.date, close_time=sell_row.date,
trade_duration=int(( trade_duration=int((
sell_row.date - buy_row.date).total_seconds() // 60), sell_row.date - buy_row.date).total_seconds() // 60),
open_index=buy_row.Index, open_index=buy_row.Index,
close_index=sell_row.Index, close_index=sell_row.Index,
open_at_end=True, open_at_end=True,
open_rate=buy_row.open, open_rate=buy_row.open,
close_rate=sell_row.open, close_rate=sell_row.open,
sell_reason=SellType.FORCE_SELL sell_reason=SellType.FORCE_SELL
) )
logger.debug('Force_selling still open trade %s with %s perc - %s', btr.pair, logger.debug(f"{pair} - Force selling still open trade, "
btr.profit_percent, btr.profit_abs) f"profit percent: {bt_res.profit_percent}, "
return btr f"profit abs: {bt_res.profit_abs}")
return bt_res
return None return None
def backtest(self, args: Dict) -> DataFrame: def backtest(self, args: Dict) -> DataFrame:
@@ -384,6 +394,8 @@ class Backtesting(object):
max_open_trades) max_open_trades)
if trade_entry: if trade_entry:
logger.debug(f"{pair} - Locking pair till "
f"close_time={trade_entry.close_time}")
lock_pair_until[pair] = trade_entry.close_time lock_pair_until[pair] = trade_entry.close_time
trades.append(trade_entry) trades.append(trade_entry)
else: else:
@@ -407,11 +419,9 @@ class Backtesting(object):
timerange = TimeRange.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
data = history.load_data( data = history.load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']),
pairs=pairs, pairs=pairs,
ticker_interval=self.ticker_interval, ticker_interval=self.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.exchange,
timerange=timerange, timerange=timerange,
) )

View File

@@ -11,7 +11,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.optimize.hyperopt_interface import IHyperOpt from freqtrade.optimize.hyperopt_interface import IHyperOpt
class DefaultHyperOpts(IHyperOpt): class DefaultHyperOpt(IHyperOpt):
""" """
Default hyperopt provided by the Freqtrade bot. Default hyperopt provided by the Freqtrade bot.
You can override it with your own Hyperopt You can override it with your own Hyperopt

View File

@@ -16,7 +16,7 @@ from freqtrade.resolvers import StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class EdgeCli(object): class EdgeCli:
""" """
EdgeCli class, this class contains all the logic to run edge backtesting EdgeCli class, this class contains all the logic to run edge backtesting
@@ -39,7 +39,8 @@ class EdgeCli(object):
self.strategy = StrategyResolver(self.config).strategy self.strategy = StrategyResolver(self.config).strategy
self.edge = Edge(config, self.exchange, self.strategy) self.edge = Edge(config, self.exchange, self.strategy)
self.edge._refresh_pairs = self.config.get('refresh_pairs', False) # Set refresh_pairs to false for edge-cli (it must be true for edge)
self.edge._refresh_pairs = False
self.timerange = TimeRange.parse_timerange(None if self.config.get( self.timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
@@ -68,8 +69,8 @@ class EdgeCli(object):
]) ])
# Ignore type as floatfmt does allow tuples but mypy does not know that # Ignore type as floatfmt does allow tuples but mypy does not know that
return tabulate(tabular_data, headers=headers, # type: ignore return tabulate(tabular_data, headers=headers,
floatfmt=floatfmt, tablefmt="pipe") floatfmt=floatfmt, tablefmt="pipe") # type: ignore
def start(self) -> None: def start(self) -> None:
result = self.edge.calculate() result = self.edge.calculate()

View File

@@ -24,8 +24,10 @@ from skopt.space import Dimension
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data.history import load_data, get_timeframe from freqtrade.data.history import load_data, get_timeframe
from freqtrade.misc import round_dict
from freqtrade.optimize.backtesting import Backtesting from freqtrade.optimize.backtesting import Backtesting
# Import IHyperOptLoss to allow users import from this file # Import IHyperOpt and IHyperOptLoss to allow unpickling classes from these modules
from freqtrade.optimize.hyperopt_interface import IHyperOpt # noqa: F4
from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4 from freqtrade.optimize.hyperopt_loss_interface import IHyperOptLoss # noqa: F4
from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver from freqtrade.resolvers.hyperopt_resolver import HyperOptResolver, HyperOptLossResolver
@@ -34,6 +36,11 @@ logger = logging.getLogger(__name__)
INITIAL_POINTS = 30 INITIAL_POINTS = 30
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models
# in the skopt models list
SKOPT_MODELS_MAX_NUM = 10
MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization MAX_LOSS = 100000 # just a big enough number to be bad result in loss optimization
@@ -47,10 +54,11 @@ class Hyperopt:
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.backtesting = Backtesting(self.config)
self.custom_hyperopt = HyperOptResolver(self.config).hyperopt self.custom_hyperopt = HyperOptResolver(self.config).hyperopt
self.backtesting = Backtesting(self.config)
self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss self.custom_hyperoptloss = HyperOptLossResolver(self.config).hyperoptloss
self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function self.calculate_loss = self.custom_hyperoptloss.hyperopt_loss_function
@@ -71,11 +79,15 @@ class Hyperopt:
self.trials: List = [] self.trials: List = []
# Populate functions here (hasattr is slow so should not be run during "regular" operations) # Populate functions here (hasattr is slow so should not be run during "regular" operations)
if hasattr(self.custom_hyperopt, 'populate_indicators'):
self.backtesting.strategy.advise_indicators = \
self.custom_hyperopt.populate_indicators # type: ignore
if hasattr(self.custom_hyperopt, 'populate_buy_trend'): if hasattr(self.custom_hyperopt, 'populate_buy_trend'):
self.backtesting.advise_buy = self.custom_hyperopt.populate_buy_trend # type: ignore self.backtesting.strategy.advise_buy = \
self.custom_hyperopt.populate_buy_trend # type: ignore
if hasattr(self.custom_hyperopt, 'populate_sell_trend'): if hasattr(self.custom_hyperopt, 'populate_sell_trend'):
self.backtesting.advise_sell = self.custom_hyperopt.populate_sell_trend # type: ignore self.backtesting.strategy.advise_sell = \
self.custom_hyperopt.populate_sell_trend # type: ignore
# Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set # Use max_open_trades for hyperopt as well, except --disable-max-market-positions is set
if self.config.get('use_max_market_positions', True): if self.config.get('use_max_market_positions', True):
@@ -83,13 +95,13 @@ class Hyperopt:
else: else:
logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...') logger.debug('Ignoring max_open_trades (--disable-max-market-positions was used) ...')
self.max_open_trades = 0 self.max_open_trades = 0
self.position_stacking = self.config.get('position_stacking', False), self.position_stacking = self.config.get('position_stacking', False)
if self.has_space('sell'): if self.has_space('sell'):
# Make sure experimental is enabled # Make sure use_sell_signal is enabled
if 'experimental' not in self.config: if 'ask_strategy' not in self.config:
self.config['experimental'] = {} self.config['ask_strategy'] = {}
self.config['experimental']['use_sell_signal'] = True self.config['ask_strategy']['use_sell_signal'] = True
@staticmethod @staticmethod
def get_lock_filename(config) -> str: def get_lock_filename(config) -> str:
@@ -107,7 +119,9 @@ class Hyperopt:
p.unlink() p.unlink()
def get_args(self, params): def get_args(self, params):
dimensions = self.hyperopt_space()
dimensions = self.dimensions
# Ensure the number of dimensions match # Ensure the number of dimensions match
# the number of parameters in the list x. # the number of parameters in the list x.
if len(params) != len(dimensions): if len(params) != len(dimensions):
@@ -124,14 +138,14 @@ class Hyperopt:
Save hyperopt trials to file Save hyperopt trials to file
""" """
if self.trials: if self.trials:
logger.info('Saving %d evaluations to \'%s\'', len(self.trials), self.trials_file) logger.info("Saving %d evaluations to '%s'", len(self.trials), self.trials_file)
dump(self.trials, self.trials_file) dump(self.trials, self.trials_file)
def read_trials(self) -> List: def read_trials(self) -> List:
""" """
Read hyperopt trials file Read hyperopt trials file
""" """
logger.info('Reading Trials from \'%s\'', self.trials_file) logger.info("Reading Trials from '%s'", self.trials_file)
trials = load(self.trials_file) trials = load(self.trials_file)
self.trials_file.unlink() self.trials_file.unlink()
return trials return trials
@@ -178,9 +192,11 @@ class Hyperopt:
indent=4) indent=4)
if self.has_space('roi'): if self.has_space('roi'):
print("ROI table:") print("ROI table:")
pprint(self.custom_hyperopt.generate_roi_table(params), indent=4) # Round printed values to 5 digits after the decimal point
pprint(round_dict(self.custom_hyperopt.generate_roi_table(params), 5), indent=4)
if self.has_space('stoploss'): if self.has_space('stoploss'):
print(f"Stoploss: {params.get('stoploss')}") # Also round to 5 digits after the decimal point
print(f"Stoploss: {round(params.get('stoploss'), 5)}")
def log_results(self, results) -> None: def log_results(self, results) -> None:
""" """
@@ -244,20 +260,24 @@ class Hyperopt:
spaces += self.custom_hyperopt.stoploss_space() spaces += self.custom_hyperopt.stoploss_space()
return spaces return spaces
def generate_optimizer(self, _params: Dict) -> Dict: def generate_optimizer(self, _params: Dict, iteration=None) -> Dict:
""" """
Used Optimize function. Called once per epoch to optimize whatever is configured. Used Optimize function. Called once per epoch to optimize whatever is configured.
Keep this function as optimized as possible! Keep this function as optimized as possible!
""" """
params = self.get_args(_params) params = self.get_args(_params)
if self.has_space('roi'): if self.has_space('roi'):
self.backtesting.strategy.minimal_roi = self.custom_hyperopt.generate_roi_table(params) self.backtesting.strategy.minimal_roi = \
self.custom_hyperopt.generate_roi_table(params)
if self.has_space('buy'): if self.has_space('buy'):
self.backtesting.advise_buy = self.custom_hyperopt.buy_strategy_generator(params) self.backtesting.strategy.advise_buy = \
self.custom_hyperopt.buy_strategy_generator(params)
if self.has_space('sell'): if self.has_space('sell'):
self.backtesting.advise_sell = self.custom_hyperopt.sell_strategy_generator(params) self.backtesting.strategy.advise_sell = \
self.custom_hyperopt.sell_strategy_generator(params)
if self.has_space('stoploss'): if self.has_space('stoploss'):
self.backtesting.strategy.stoploss = params['stoploss'] self.backtesting.strategy.stoploss = params['stoploss']
@@ -318,9 +338,9 @@ class Hyperopt:
f'Total profit {total_profit: 11.8f} {stake_cur} ' f'Total profit {total_profit: 11.8f} {stake_cur} '
f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.') f'({profit: 7.2f}Σ%). Avg duration {duration:5.1f} mins.')
def get_optimizer(self, cpu_count) -> Optimizer: def get_optimizer(self, dimensions, cpu_count) -> Optimizer:
return Optimizer( return Optimizer(
self.hyperopt_space(), dimensions,
base_estimator="ET", base_estimator="ET",
acq_optimizer="auto", acq_optimizer="auto",
n_initial_points=INITIAL_POINTS, n_initial_points=INITIAL_POINTS,
@@ -328,9 +348,26 @@ class Hyperopt:
random_state=self.config.get('hyperopt_random_state', None) random_state=self.config.get('hyperopt_random_state', None)
) )
def run_optimizer_parallel(self, parallel, asked) -> List: def fix_optimizer_models_list(self):
"""
WORKAROUND: Since skopt is not actively supported, this resolves problems with skopt
memory usage, see also: https://github.com/scikit-optimize/scikit-optimize/pull/746
This may cease working when skopt updates if implementation of this intrinsic
part changes.
"""
n = len(self.opt.models) - SKOPT_MODELS_MAX_NUM
# Keep no more than 2*SKOPT_MODELS_MAX_NUM models in the skopt models list,
# remove the old ones. These are actually of no use, the current model
# from the estimator is the only one used in the skopt optimizer.
# Freqtrade code also does not inspect details of the models.
if n >= SKOPT_MODELS_MAX_NUM:
logger.debug(f"Fixing skopt models list, removing {n} old items...")
del self.opt.models[0:n]
def run_optimizer_parallel(self, parallel, asked, i) -> List:
return parallel(delayed( return parallel(delayed(
wrap_non_picklable_objects(self.generate_optimizer))(v) for v in asked) wrap_non_picklable_objects(self.generate_optimizer))(v, i) for v in asked)
def load_previous_results(self): def load_previous_results(self):
""" read trials file if we have one """ """ read trials file if we have one """
@@ -345,11 +382,9 @@ class Hyperopt:
timerange = TimeRange.parse_timerange(None if self.config.get( timerange = TimeRange.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange'))) 'timerange') is None else str(self.config.get('timerange')))
data = load_data( data = load_data(
datadir=Path(self.config['datadir']) if self.config.get('datadir') else None, datadir=Path(self.config['datadir']),
pairs=self.config['exchange']['pair_whitelist'], pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.backtesting.ticker_interval, ticker_interval=self.backtesting.ticker_interval,
refresh_pairs=self.config.get('refresh_pairs', False),
exchange=self.backtesting.exchange,
timerange=timerange timerange=timerange
) )
@@ -366,9 +401,6 @@ class Hyperopt:
(max_date - min_date).days (max_date - min_date).days
) )
self.backtesting.strategy.advise_indicators = \
self.custom_hyperopt.populate_indicators # type: ignore
preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data) preprocessed = self.backtesting.strategy.tickerdata_to_dataframe(data)
dump(preprocessed, self.tickerdata_pickle) dump(preprocessed, self.tickerdata_pickle)
@@ -379,11 +411,12 @@ class Hyperopt:
self.load_previous_results() self.load_previous_results()
cpus = cpu_count() cpus = cpu_count()
logger.info(f'Found {cpus} CPU cores. Let\'s make them scream!') logger.info(f"Found {cpus} CPU cores. Let's make them scream!")
config_jobs = self.config.get('hyperopt_jobs', -1) config_jobs = self.config.get('hyperopt_jobs', -1)
logger.info(f'Number of parallel jobs set as: {config_jobs}') logger.info(f'Number of parallel jobs set as: {config_jobs}')
opt = self.get_optimizer(config_jobs) self.dimensions = self.hyperopt_space()
self.opt = self.get_optimizer(self.dimensions, config_jobs)
if self.config.get('print_colorized', False): if self.config.get('print_colorized', False):
colorama_init(autoreset=True) colorama_init(autoreset=True)
@@ -394,9 +427,10 @@ class Hyperopt:
logger.info(f'Effective number of parallel workers used: {jobs}') logger.info(f'Effective number of parallel workers used: {jobs}')
EVALS = max(self.total_epochs // jobs, 1) EVALS = max(self.total_epochs // jobs, 1)
for i in range(EVALS): for i in range(EVALS):
asked = opt.ask(n_points=jobs) asked = self.opt.ask(n_points=jobs)
f_val = self.run_optimizer_parallel(parallel, asked) f_val = self.run_optimizer_parallel(parallel, asked, i)
opt.tell(asked, [v['loss'] for v in f_val]) self.opt.tell(asked, [v['loss'] for v in f_val])
self.fix_optimizer_models_list()
for j in range(jobs): for j in range(jobs):
current = i * jobs + j current = i * jobs + j
val = f_val[j] val = f_val[j]

View File

@@ -2,6 +2,8 @@
IHyperOpt interface IHyperOpt interface
This module defines the interface to apply for hyperopts This module defines the interface to apply for hyperopts
""" """
import logging
import math
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Any, Callable, List from typing import Dict, Any, Callable, List
@@ -9,19 +11,37 @@ from typing import Dict, Any, Callable, List
from pandas import DataFrame from pandas import DataFrame
from skopt.space import Dimension, Integer, Real from skopt.space import Dimension, Integer, Real
from freqtrade import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import round_dict
logger = logging.getLogger(__name__)
def _format_exception_message(method: str, space: str) -> str:
return (f"The '{space}' space is included into the hyperoptimization "
f"but {method}() method is not found in your "
f"custom Hyperopt class. You should either implement this "
f"method or remove the '{space}' space from hyperoptimization.")
class IHyperOpt(ABC): class IHyperOpt(ABC):
""" """
Interface for freqtrade hyperopts Interface for freqtrade hyperopts
Defines the mandatory structure must follow any custom strategies Defines the mandatory structure must follow any custom hyperopts
Attributes you can use: Class attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> int: value of the ticker interval to use for the strategy ticker_interval -> int: value of the ticker interval to use for the strategy
""" """
ticker_interval: str ticker_interval: str
def __init__(self, config: dict) -> None:
self.config = config
# Assign ticker_interval to be used in hyperopt
IHyperOpt.ticker_interval = str(config['ticker_interval'])
@staticmethod @staticmethod
@abstractmethod @abstractmethod
def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_indicators(dataframe: DataFrame, metadata: dict) -> DataFrame:
@@ -32,32 +52,32 @@ class IHyperOpt(ABC):
""" """
@staticmethod @staticmethod
@abstractmethod
def buy_strategy_generator(params: Dict[str, Any]) -> Callable: def buy_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Create a buy strategy generator. Create a buy strategy generator.
""" """
raise OperationalException(_format_exception_message('buy_strategy_generator', 'buy'))
@staticmethod @staticmethod
@abstractmethod
def sell_strategy_generator(params: Dict[str, Any]) -> Callable: def sell_strategy_generator(params: Dict[str, Any]) -> Callable:
""" """
Create a sell strategy generator. Create a sell strategy generator.
""" """
raise OperationalException(_format_exception_message('sell_strategy_generator', 'sell'))
@staticmethod @staticmethod
@abstractmethod
def indicator_space() -> List[Dimension]: def indicator_space() -> List[Dimension]:
""" """
Create an indicator space. Create an indicator space.
""" """
raise OperationalException(_format_exception_message('indicator_space', 'buy'))
@staticmethod @staticmethod
@abstractmethod
def sell_indicator_space() -> List[Dimension]: def sell_indicator_space() -> List[Dimension]:
""" """
Create a sell indicator space. Create a sell indicator space.
""" """
raise OperationalException(_format_exception_message('sell_indicator_space', 'sell'))
@staticmethod @staticmethod
def generate_roi_table(params: Dict) -> Dict[int, float]: def generate_roi_table(params: Dict) -> Dict[int, float]:
@@ -75,6 +95,83 @@ class IHyperOpt(ABC):
return roi_table return roi_table
@staticmethod
def roi_space() -> List[Dimension]:
"""
Create a ROI space.
Defines values to search for each ROI steps.
This method implements adaptive roi hyperspace with varied
ranges for parameters which automatically adapts to the
ticker interval used.
It's used by Freqtrade by default, if no custom roi_space method is defined.
"""
# Default scaling coefficients for the roi hyperspace. Can be changed
# to adjust resulting ranges of the ROI tables.
# Increase if you need wider ranges in the roi hyperspace, decrease if shorter
# ranges are needed.
roi_t_alpha = 1.0
roi_p_alpha = 1.0
ticker_interval_mins = timeframe_to_minutes(IHyperOpt.ticker_interval)
# We define here limits for the ROI space parameters automagically adapted to the
# ticker_interval used by the bot:
#
# * 'roi_t' (limits for the time intervals in the ROI tables) components
# are scaled linearly.
# * 'roi_p' (limits for the ROI value steps) components are scaled logarithmically.
#
# The scaling is designed so that it maps exactly to the legacy Freqtrade roi_space()
# method for the 5m ticker interval.
roi_t_scale = ticker_interval_mins / 5
roi_p_scale = math.log1p(ticker_interval_mins) / math.log1p(5)
roi_limits = {
'roi_t1_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t1_max': int(120 * roi_t_scale * roi_t_alpha),
'roi_t2_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t2_max': int(60 * roi_t_scale * roi_t_alpha),
'roi_t3_min': int(10 * roi_t_scale * roi_t_alpha),
'roi_t3_max': int(40 * roi_t_scale * roi_t_alpha),
'roi_p1_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p1_max': 0.04 * roi_p_scale * roi_p_alpha,
'roi_p2_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p2_max': 0.07 * roi_p_scale * roi_p_alpha,
'roi_p3_min': 0.01 * roi_p_scale * roi_p_alpha,
'roi_p3_max': 0.20 * roi_p_scale * roi_p_alpha,
}
logger.debug(f"Using roi space limits: {roi_limits}")
p = {
'roi_t1': roi_limits['roi_t1_min'],
'roi_t2': roi_limits['roi_t2_min'],
'roi_t3': roi_limits['roi_t3_min'],
'roi_p1': roi_limits['roi_p1_min'],
'roi_p2': roi_limits['roi_p2_min'],
'roi_p3': roi_limits['roi_p3_min'],
}
logger.info(f"Min roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
p = {
'roi_t1': roi_limits['roi_t1_max'],
'roi_t2': roi_limits['roi_t2_max'],
'roi_t3': roi_limits['roi_t3_max'],
'roi_p1': roi_limits['roi_p1_max'],
'roi_p2': roi_limits['roi_p2_max'],
'roi_p3': roi_limits['roi_p3_max'],
}
logger.info(f"Max roi table: {round_dict(IHyperOpt.generate_roi_table(p), 5)}")
return [
Integer(roi_limits['roi_t1_min'], roi_limits['roi_t1_max'], name='roi_t1'),
Integer(roi_limits['roi_t2_min'], roi_limits['roi_t2_max'], name='roi_t2'),
Integer(roi_limits['roi_t3_min'], roi_limits['roi_t3_max'], name='roi_t3'),
Real(roi_limits['roi_p1_min'], roi_limits['roi_p1_max'], name='roi_p1'),
Real(roi_limits['roi_p2_min'], roi_limits['roi_p2_max'], name='roi_p2'),
Real(roi_limits['roi_p3_min'], roi_limits['roi_p3_max'], name='roi_p3'),
]
@staticmethod @staticmethod
def stoploss_space() -> List[Dimension]: def stoploss_space() -> List[Dimension]:
""" """
@@ -84,22 +181,17 @@ class IHyperOpt(ABC):
You may override it in your custom Hyperopt class. You may override it in your custom Hyperopt class.
""" """
return [ return [
Real(-0.5, -0.02, name='stoploss'), Real(-0.35, -0.02, name='stoploss'),
] ]
@staticmethod # This is needed for proper unpickling the class attribute ticker_interval
def roi_space() -> List[Dimension]: # which is set to the actual value by the resolver.
""" # Why do I still need such shamanic mantras in modern python?
Create a ROI space. def __getstate__(self):
state = self.__dict__.copy()
state['ticker_interval'] = self.ticker_interval
return state
Defines values to search for each ROI steps. def __setstate__(self, state):
You may override it in your custom Hyperopt class. self.__dict__.update(state)
""" IHyperOpt.ticker_interval = state['ticker_interval']
return [
Integer(10, 120, name='roi_t1'),
Integer(10, 60, name='roi_t2'),
Integer(10, 40, name='roi_t3'),
Real(0.01, 0.04, name='roi_p1'),
Real(0.01, 0.07, name='roi_p2'),
Real(0.01, 0.20, name='roi_p3'),
]

View File

@@ -8,6 +8,9 @@ import logging
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List from typing import List
from freqtrade.exchange import market_is_active
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -77,7 +80,7 @@ class IPairList(ABC):
continue continue
# Check if market is active # Check if market is active
market = markets[pair] market = markets[pair]
if not market['active']: if not market_is_active(market):
logger.info(f"Ignoring {pair} from whitelist. Market is not active.") logger.info(f"Ignoring {pair} from whitelist. Market is not active.")
continue continue
sanitized_whitelist.add(pair) sanitized_whitelist.add(pair)

View File

@@ -1,7 +1,6 @@
""" """
This module contains the class to persist trades into SQLite This module contains the class to persist trades into SQLite
""" """
import logging import logging
from datetime import datetime from datetime import datetime
from decimal import Decimal from decimal import Decimal
@@ -19,8 +18,10 @@ from sqlalchemy.pool import StaticPool
from freqtrade import OperationalException from freqtrade import OperationalException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_DECL_BASE: Any = declarative_base() _DECL_BASE: Any = declarative_base()
_SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls' _SQL_DOCS_URL = 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
@@ -48,8 +49,8 @@ def init(db_url: str, clean_open_orders: bool = False) -> None:
try: try:
engine = create_engine(db_url, **kwargs) engine = create_engine(db_url, **kwargs)
except NoSuchModuleError: except NoSuchModuleError:
raise OperationalException(f'Given value for db_url: \'{db_url}\' ' raise OperationalException(f"Given value for db_url: '{db_url}' "
f'is no valid database URL! (See {_SQL_DOCS_URL})') f"is no valid database URL! (See {_SQL_DOCS_URL})")
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True)) session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session() Trade.session = session()
@@ -209,7 +210,7 @@ class Trade(_DECL_BASE):
ticker_interval = Column(Integer, nullable=True) ticker_interval = Column(Integer, nullable=True)
def __repr__(self): def __repr__(self):
open_since = arrow.get(self.open_date).humanize() if self.is_open else 'closed' open_since = self.open_date.strftime('%Y-%m-%d %H:%M:%S') if self.is_open else 'closed'
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, ' return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})') f'open_rate={self.open_rate:.8f}, open_since={open_since})')
@@ -250,7 +251,6 @@ class Trade(_DECL_BASE):
:param initial: Called to initiate stop_loss. :param initial: Called to initiate stop_loss.
Skips everything if self.stop_loss is already set. Skips everything if self.stop_loss is already set.
""" """
if initial and not (self.stop_loss is None or self.stop_loss == 0): if initial and not (self.stop_loss is None or self.stop_loss == 0):
# Don't modify if called with initial and nothing to do # Don't modify if called with initial and nothing to do
return return
@@ -259,7 +259,7 @@ class Trade(_DECL_BASE):
# no stop loss assigned yet # no stop loss assigned yet
if not self.stop_loss: if not self.stop_loss:
logger.debug("assigning new stop loss") logger.debug(f"{self.pair} - Assigning new stoploss...")
self.stop_loss = new_loss self.stop_loss = new_loss
self.stop_loss_pct = -1 * abs(stoploss) self.stop_loss_pct = -1 * abs(stoploss)
self.initial_stop_loss = new_loss self.initial_stop_loss = new_loss
@@ -269,21 +269,20 @@ class Trade(_DECL_BASE):
# evaluate if the stop loss needs to be updated # evaluate if the stop loss needs to be updated
else: else:
if new_loss > self.stop_loss: # stop losses only walk up, never down! if new_loss > self.stop_loss: # stop losses only walk up, never down!
logger.debug(f"{self.pair} - Adjusting stoploss...")
self.stop_loss = new_loss self.stop_loss = new_loss
self.stop_loss_pct = -1 * abs(stoploss) self.stop_loss_pct = -1 * abs(stoploss)
self.stoploss_last_update = datetime.utcnow() self.stoploss_last_update = datetime.utcnow()
logger.debug("adjusted stop loss")
else: else:
logger.debug("keeping current stop loss") logger.debug(f"{self.pair} - Keeping current stoploss...")
logger.debug( logger.debug(
f"{self.pair} - current price {current_price:.8f}, " f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
f"bought at {self.open_rate:.8f} and calculated " f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate:.8f}, "
f"stop loss is at: {self.initial_stop_loss:.8f} initial " f"initial_stop_loss={self.initial_stop_loss:.8f}, "
f"stop at {self.stop_loss:.8f}. " f"stop_loss={self.stop_loss:.8f}. "
f"trailing stop loss saved us: " f"Trailing stoploss saved us: "
f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f} " f"{float(self.stop_loss) - float(self.initial_stop_loss):.8f}.")
f"and max observed rate was {self.max_rate:.8f}")
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
@@ -331,24 +330,19 @@ class Trade(_DECL_BASE):
self self
) )
def calc_open_trade_price( def calc_open_trade_price(self, fee: Optional[float] = None) -> float:
self,
fee: Optional[float] = None) -> float:
""" """
Calculate the open_rate including fee. Calculate the open_rate including fee.
:param fee: fee to use on the open rate (optional). :param fee: fee to use on the open rate (optional).
If rate is not set self.fee will be used If rate is not set self.fee will be used
:return: Price in of the open trade incl. Fees :return: Price in of the open trade incl. Fees
""" """
buy_trade = (Decimal(self.amount) * Decimal(self.open_rate)) buy_trade = (Decimal(self.amount) * Decimal(self.open_rate))
fees = buy_trade * Decimal(fee or self.fee_open) fees = buy_trade * Decimal(fee or self.fee_open)
return float(buy_trade + fees) return float(buy_trade + fees)
def calc_close_trade_price( def calc_close_trade_price(self, rate: Optional[float] = None,
self, fee: Optional[float] = None) -> float:
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
""" """
Calculate the close_rate including fee Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
@@ -357,7 +351,6 @@ class Trade(_DECL_BASE):
If rate is not set self.close_rate will be used If rate is not set self.close_rate will be used
:return: Price in BTC of the open trade :return: Price in BTC of the open trade
""" """
if rate is None and not self.close_rate: if rate is None and not self.close_rate:
return 0.0 return 0.0
@@ -365,10 +358,8 @@ class Trade(_DECL_BASE):
fees = sell_trade * Decimal(fee or self.fee_close) fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees) return float(sell_trade - fees)
def calc_profit( def calc_profit(self, rate: Optional[float] = None,
self, fee: Optional[float] = None) -> float:
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
""" """
Calculate the absolute profit in stake currency between Close and Open trade Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
@@ -385,10 +376,8 @@ class Trade(_DECL_BASE):
profit = close_trade_price - open_trade_price profit = close_trade_price - open_trade_price
return float(f"{profit:.8f}") return float(f"{profit:.8f}")
def calc_profit_percent( def calc_profit_percent(self, rate: Optional[float] = None,
self, fee: Optional[float] = None) -> float:
rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
""" """
Calculates the profit in percentage (including fee). Calculates the profit in percentage (including fee).
:param rate: rate to compare with (optional). :param rate: rate to compare with (optional).
@@ -396,7 +385,6 @@ class Trade(_DECL_BASE):
:param fee: fee to use on the close rate (optional). :param fee: fee to use on the close rate (optional).
:return: profit in percentage as float :return: profit in percentage as float
""" """
open_trade_price = self.calc_open_trade_price() open_trade_price = self.calc_open_trade_price()
close_trade_price = self.calc_close_trade_price( close_trade_price = self.calc_close_trade_price(
rate=(rate or self.close_rate), rate=(rate or self.close_rate),
@@ -436,8 +424,8 @@ class Trade(_DECL_BASE):
and trade.initial_stop_loss_pct != desired_stoploss): and trade.initial_stop_loss_pct != desired_stoploss):
# Stoploss value got changed # Stoploss value got changed
logger.info(f"Stoploss for {trade} needs adjustment.") logger.info(f"Stoploss for {trade} needs adjustment...")
# Force reset of stoploss # Force reset of stoploss
trade.stop_loss = None trade.stop_loss = None
trade.adjust_stop_loss(trade.open_rate, desired_stoploss) trade.adjust_stop_loss(trade.open_rate, desired_stoploss)
logger.info(f"new stoploss: {trade.stop_loss}, ") logger.info(f"New stoploss: {trade.stop_loss}.")

View File

@@ -0,0 +1,36 @@
from typing import Any, Dict
from freqtrade import OperationalException
from freqtrade.state import RunMode
from freqtrade.utils import setup_utils_configuration
def validate_plot_args(args: Dict[str, Any]):
if not args.get('datadir') and not args.get('config'):
raise OperationalException(
"You need to specify either `--datadir` or `--config` "
"for plot-profit and plot-dataframe.")
def start_plot_dataframe(args: Dict[str, Any]) -> None:
"""
Entrypoint for dataframe plotting
"""
# Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import load_and_plot_trades
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT)
load_and_plot_trades(config)
def start_plot_profit(args: Dict[str, Any]) -> None:
"""
Entrypoint for plot_profit
"""
# Import here to avoid errors if plot-dependencies are not installed.
from freqtrade.plot.plotting import plot_profit
validate_plot_args(args)
config = setup_utils_configuration(args, RunMode.PLOT)
plot_profit(config)

View File

@@ -1,15 +1,14 @@
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional from typing import Any, Dict, List
import pandas as pd import pandas as pd
from freqtrade.configuration import TimeRange from freqtrade.configuration import TimeRange
from freqtrade.data import history from freqtrade.data import history
from freqtrade.data.btanalysis import (combine_tickers_with_mean, from freqtrade.data.btanalysis import (combine_tickers_with_mean,
create_cum_profit, load_trades) create_cum_profit,
from freqtrade.exchange import Exchange extract_trades_of_period, load_trades)
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import StrategyResolver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -19,23 +18,16 @@ try:
from plotly.offline import plot from plotly.offline import plot
import plotly.graph_objects as go import plotly.graph_objects as go
except ImportError: except ImportError:
logger.exception("Module plotly not found \n Please install using `pip install plotly`") logger.exception("Module plotly not found \n Please install using `pip3 install plotly`")
exit(1) exit(1)
def init_plotscript(config): def init_plotscript(config):
""" """
Initialize objects needed for plotting Initialize objects needed for plotting
:return: Dict with tickers, trades, pairs and strategy :return: Dict with tickers, trades and pairs
""" """
exchange: Optional[Exchange] = None
# Exchange is only needed when downloading data!
if config.get("refresh_pairs", False):
exchange = ExchangeResolver(config.get('exchange', {}).get('name'),
config).exchange
strategy = StrategyResolver(config).strategy
if "pairs" in config: if "pairs" in config:
pairs = config["pairs"] pairs = config["pairs"]
else: else:
@@ -47,17 +39,18 @@ def init_plotscript(config):
tickers = history.load_data( tickers = history.load_data(
datadir=Path(str(config.get("datadir"))), datadir=Path(str(config.get("datadir"))),
pairs=pairs, pairs=pairs,
ticker_interval=config['ticker_interval'], ticker_interval=config.get('ticker_interval', '5m'),
refresh_pairs=config.get('refresh_pairs', False),
timerange=timerange, timerange=timerange,
exchange=exchange,
) )
trades = load_trades(config) trades = load_trades(config['trade_source'],
db_url=config.get('db_url'),
exportfilename=config.get('exportfilename'),
)
return {"tickers": tickers, return {"tickers": tickers,
"trades": trades, "trades": trades,
"pairs": pairs, "pairs": pairs,
"strategy": strategy,
} }
@@ -71,14 +64,13 @@ def add_indicators(fig, row, indicators: List[str], data: pd.DataFrame) -> make_
""" """
for indicator in indicators: for indicator in indicators:
if indicator in data: if indicator in data:
# TODO: Figure out why scattergl causes problems scatter = go.Scatter(
scattergl = go.Scatter(
x=data['date'], x=data['date'],
y=data[indicator].values, y=data[indicator].values,
mode='lines', mode='lines',
name=indicator name=indicator
) )
fig.add_trace(scattergl, row, 1) fig.add_trace(scatter, row, 1)
else: else:
logger.info( logger.info(
'Indicator "%s" ignored. Reason: This indicator is not found ' 'Indicator "%s" ignored. Reason: This indicator is not found '
@@ -99,7 +91,7 @@ def add_profit(fig, row, data: pd.DataFrame, column: str, name: str) -> make_sub
:param name: Name to use :param name: Name to use
:return: fig with added profit plot :return: fig with added profit plot
""" """
profit = go.Scattergl( profit = go.Scatter(
x=data.index, x=data.index,
y=data[column], y=data[column],
name=name, name=name,
@@ -228,23 +220,27 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
else: else:
logger.warning("No sell-signals found.") logger.warning("No sell-signals found.")
# TODO: Figure out why scattergl causes problems plotly/plotly.js#2284
if 'bb_lowerband' in data and 'bb_upperband' in data: if 'bb_lowerband' in data and 'bb_upperband' in data:
bb_lower = go.Scattergl( bb_lower = go.Scatter(
x=data.date, x=data.date,
y=data.bb_lowerband, y=data.bb_lowerband,
name='BB lower', showlegend=False,
line={'color': 'rgba(255,255,255,0)'}, line={'color': 'rgba(255,255,255,0)'},
) )
bb_upper = go.Scattergl( bb_upper = go.Scatter(
x=data.date, x=data.date,
y=data.bb_upperband, y=data.bb_upperband,
name='BB upper', name='Bollinger Band',
fill="tonexty", fill="tonexty",
fillcolor="rgba(0,176,246,0.2)", fillcolor="rgba(0,176,246,0.2)",
line={'color': 'rgba(255,255,255,0)'}, line={'color': 'rgba(255,255,255,0)'},
) )
fig.add_trace(bb_lower, 1, 1) fig.add_trace(bb_lower, 1, 1)
fig.add_trace(bb_upper, 1, 1) fig.add_trace(bb_upper, 1, 1)
if 'bb_upperband' in indicators1 and 'bb_lowerband' in indicators1:
indicators1.remove('bb_upperband')
indicators1.remove('bb_lowerband')
# Add indicators to main plot # Add indicators to main plot
fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data) fig = add_indicators(fig=fig, row=1, indicators=indicators1, data=data)
@@ -255,40 +251,49 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
volume = go.Bar( volume = go.Bar(
x=data['date'], x=data['date'],
y=data['volume'], y=data['volume'],
name='Volume' name='Volume',
) marker_color='DarkSlateGrey',
marker_line_color='DarkSlateGrey'
)
fig.add_trace(volume, 2, 1) fig.add_trace(volume, 2, 1)
# Add indicators to seperate row # Add indicators to separate row
fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data) fig = add_indicators(fig=fig, row=3, indicators=indicators2, data=data)
return fig return fig
def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame], def generate_profit_graph(pairs: str, tickers: Dict[str, pd.DataFrame],
trades: pd.DataFrame) -> go.Figure: trades: pd.DataFrame, timeframe: str) -> go.Figure:
# Combine close-values for all pairs, rename columns to "pair" # Combine close-values for all pairs, rename columns to "pair"
df_comb = combine_tickers_with_mean(tickers, "close") df_comb = combine_tickers_with_mean(tickers, "close")
# Add combined cumulative profit # Add combined cumulative profit
df_comb = create_cum_profit(df_comb, trades, 'cum_profit') df_comb = create_cum_profit(df_comb, trades, 'cum_profit', timeframe)
# Plot the pairs average close prices, and total profit growth # Plot the pairs average close prices, and total profit growth
avgclose = go.Scattergl( avgclose = go.Scatter(
x=df_comb.index, x=df_comb.index,
y=df_comb['mean'], y=df_comb['mean'],
name='Avg close price', name='Avg close price',
) )
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_width=[1, 1, 1]) fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
fig['layout'].update(title="Profit plot") row_width=[1, 1, 1],
vertical_spacing=0.05,
subplot_titles=["AVG Close Price", "Combined Profit", "Profit per pair"])
fig['layout'].update(title="Freqtrade Profit plot")
fig['layout']['yaxis1'].update(title='Price')
fig['layout']['yaxis2'].update(title='Profit')
fig['layout']['yaxis3'].update(title='Profit')
fig['layout']['xaxis']['rangeslider'].update(visible=False)
fig.add_trace(avgclose, 1, 1) fig.add_trace(avgclose, 1, 1)
fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit') fig = add_profit(fig, 2, df_comb, 'cum_profit', 'Profit')
for pair in pairs: for pair in pairs:
profit_col = f'cum_profit_{pair}' profit_col = f'cum_profit_{pair}'
df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col) df_comb = create_cum_profit(df_comb, trades[trades['pair'] == pair], profit_col, timeframe)
fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}") fig = add_profit(fig, 3, df_comb, profit_col, f"Profit {pair}")
@@ -321,3 +326,65 @@ def store_plot_file(fig, filename: str, directory: Path, auto_open: bool = False
plot(fig, filename=str(_filename), plot(fig, filename=str(_filename),
auto_open=auto_open) auto_open=auto_open)
logger.info(f"Stored plot as {_filename}") logger.info(f"Stored plot as {_filename}")
def load_and_plot_trades(config: Dict[str, Any]):
"""
From configuration provided
- Initializes plot-script
- Get tickers data
- Generate Dafaframes populated with indicators and signals based on configured strategy
- Load trades excecuted during the selected period
- Generate Plotly plot objects
- Generate plot files
:return: None
"""
strategy = StrategyResolver(config).strategy
plot_elements = init_plotscript(config)
trades = plot_elements['trades']
pair_counter = 0
for pair, data in plot_elements["tickers"].items():
pair_counter += 1
logger.info("analyse pair %s", pair)
tickers = {}
tickers[pair] = data
dataframe = strategy.analyze_ticker(tickers[pair], {'pair': pair})
trades_pair = trades.loc[trades['pair'] == pair]
trades_pair = extract_trades_of_period(dataframe, trades_pair)
fig = generate_candlestick_graph(
pair=pair,
data=dataframe,
trades=trades_pair,
indicators1=config["indicators1"],
indicators2=config["indicators2"],
)
store_plot_file(fig, filename=generate_plot_filename(pair, config['ticker_interval']),
directory=config['user_data_dir'] / "plot")
logger.info('End of plotting process. %s plots generated', pair_counter)
def plot_profit(config: Dict[str, Any]) -> None:
"""
Plots the total profit for all pairs.
Note, the profit calculation isn't realistic.
But should be somewhat proportional, and therefor useful
in helping out to find a good algorithm.
"""
plot_elements = init_plotscript(config)
trades = load_trades(config['trade_source'],
db_url=str(config.get('db_url')),
exportfilename=str(config.get('exportfilename')),
)
# Filter trades to relevant pairs
trades = trades[trades['pair'].isin(plot_elements["pairs"])]
# Create an average close price of all the pairs that were involved.
# this could be useful to gauge the overall market trend
fig = generate_profit_graph(plot_elements["pairs"], plot_elements["tickers"],
trades, config.get('ticker_interval', '5m'))
store_plot_file(fig, filename='freqtrade-profit-plot.html',
directory=config['user_data_dir'] / "plot", auto_open=True)

View File

@@ -3,7 +3,7 @@ This module loads custom exchanges
""" """
import logging import logging
from freqtrade.exchange import Exchange from freqtrade.exchange import Exchange, MAP_EXCHANGE_CHILDCLASS
import freqtrade.exchange as exchanges import freqtrade.exchange as exchanges
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
@@ -17,19 +17,22 @@ class ExchangeResolver(IResolver):
__slots__ = ['exchange'] __slots__ = ['exchange']
def __init__(self, exchange_name: str, config: dict) -> None: def __init__(self, exchange_name: str, config: dict, validate: bool = True) -> None:
""" """
Load the custom class from config parameter Load the custom class from config parameter
:param config: configuration dictionary :param config: configuration dictionary
""" """
# Map exchange name to avoid duplicate classes for identical exchanges
exchange_name = MAP_EXCHANGE_CHILDCLASS.get(exchange_name, exchange_name)
exchange_name = exchange_name.title() exchange_name = exchange_name.title()
try: try:
self.exchange = self._load_exchange(exchange_name, kwargs={'config': config}) self.exchange = self._load_exchange(exchange_name, kwargs={'config': config,
'validate': validate})
except ImportError: except ImportError:
logger.info( logger.info(
f"No {exchange_name} specific subclass found. Using the generic class instead.") f"No {exchange_name} specific subclass found. Using the generic class instead.")
if not hasattr(self, "exchange"): if not hasattr(self, "exchange"):
self.exchange = Exchange(config) self.exchange = Exchange(config, validate=validate)
def _load_exchange( def _load_exchange(
self, exchange_name: str, kwargs: dict) -> Exchange: self, exchange_name: str, kwargs: dict) -> Exchange:
@@ -43,7 +46,7 @@ class ExchangeResolver(IResolver):
try: try:
ex_class = getattr(exchanges, exchange_name) ex_class = getattr(exchanges, exchange_name)
exchange = ex_class(kwargs['config']) exchange = ex_class(**kwargs)
if exchange: if exchange:
logger.info(f"Using resolved exchange '{exchange_name}'...") logger.info(f"Using resolved exchange '{exchange_name}'...")
return exchange return exchange

View File

@@ -34,15 +34,12 @@ class HyperOptResolver(IResolver):
self.hyperopt = self._load_hyperopt(hyperopt_name, config, self.hyperopt = self._load_hyperopt(hyperopt_name, config,
extra_dir=config.get('hyperopt_path')) extra_dir=config.get('hyperopt_path'))
# Assign ticker_interval to be used in hyperopt
self.hyperopt.__class__.ticker_interval = str(config['ticker_interval'])
if not hasattr(self.hyperopt, 'populate_buy_trend'): if not hasattr(self.hyperopt, 'populate_buy_trend'):
logger.warning("Custom Hyperopt does not provide populate_buy_trend. " logger.warning("Hyperopt class does not provide populate_buy_trend() method. "
"Using populate_buy_trend from DefaultStrategy.") "Using populate_buy_trend from the strategy.")
if not hasattr(self.hyperopt, 'populate_sell_trend'): if not hasattr(self.hyperopt, 'populate_sell_trend'):
logger.warning("Custom Hyperopt does not provide populate_sell_trend. " logger.warning("Hyperopt class does not provide populate_sell_trend() method. "
"Using populate_sell_trend from DefaultStrategy.") "Using populate_sell_trend from the strategy.")
def _load_hyperopt( def _load_hyperopt(
self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt: self, hyperopt_name: str, config: Dict, extra_dir: Optional[str] = None) -> IHyperOpt:
@@ -55,17 +52,11 @@ class HyperOptResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [ abs_paths = self.build_search_paths(config, current_path=current_path,
config['user_data_dir'].joinpath('hyperopts'), user_subdir='hyperopts', extra_dir=extra_dir)
current_path,
]
if extra_dir:
# Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt, hyperopt = self._load_object(paths=abs_paths, object_type=IHyperOpt,
object_name=hyperopt_name) object_name=hyperopt_name, kwargs={'config': config})
if hyperopt: if hyperopt:
return hyperopt return hyperopt
raise OperationalException( raise OperationalException(
@@ -112,14 +103,8 @@ class HyperOptLossResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('optimize').resolve() current_path = Path(__file__).parent.parent.joinpath('optimize').resolve()
abs_paths = [ abs_paths = self.build_search_paths(config, current_path=current_path,
config['user_data_dir'].joinpath('hyperopts'), user_subdir='hyperopts', extra_dir=extra_dir)
current_path,
]
if extra_dir:
# Add extra hyperopt directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss, hyperoptloss = self._load_object(paths=abs_paths, object_type=IHyperOptLoss,
object_name=hyper_loss_name) object_name=hyper_loss_name)

View File

@@ -7,25 +7,39 @@ import importlib.util
import inspect import inspect
import logging import logging
from pathlib import Path from pathlib import Path
from typing import Any, List, Optional, Tuple, Type, Union from typing import Any, List, Optional, Tuple, Union, Generator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class IResolver(object): class IResolver:
""" """
This class contains all the logic to load custom classes This class contains all the logic to load custom classes
""" """
def build_search_paths(self, config, current_path: Path, user_subdir: str,
extra_dir: Optional[str] = None) -> List[Path]:
abs_paths = [
config['user_data_dir'].joinpath(user_subdir),
current_path,
]
if extra_dir:
# Add extra directory to the top of the search paths
abs_paths.insert(0, Path(extra_dir).resolve())
return abs_paths
@staticmethod @staticmethod
def _get_valid_object(object_type, module_path: Path, def _get_valid_object(object_type, module_path: Path,
object_name: str) -> Optional[Type[Any]]: object_name: str) -> Generator[Any, None, None]:
""" """
Returns the first object with matching object_type and object_name in the path given. Generator returning objects with matching object_type and object_name in the path given.
:param object_type: object_type (class) :param object_type: object_type (class)
:param module_path: absolute path to the module :param module_path: absolute path to the module
:param object_name: Class name of the object :param object_name: Class name of the object
:return: class or None :return: generator containing matching objects
""" """
# Generate spec based on absolute path # Generate spec based on absolute path
@@ -42,7 +56,7 @@ class IResolver(object):
obj for name, obj in inspect.getmembers(module, inspect.isclass) obj for name, obj in inspect.getmembers(module, inspect.isclass)
if object_name == name and object_type in obj.__bases__ if object_name == name and object_type in obj.__bases__
) )
return next(valid_objects_gen, None) return valid_objects_gen
@staticmethod @staticmethod
def _search_object(directory: Path, object_type, object_name: str, def _search_object(directory: Path, object_type, object_name: str,
@@ -59,9 +73,9 @@ class IResolver(object):
logger.debug('Ignoring %s', entry) logger.debug('Ignoring %s', entry)
continue continue
module_path = entry.resolve() module_path = entry.resolve()
obj = IResolver._get_valid_object(
object_type, module_path, object_name obj = next(IResolver._get_valid_object(object_type, module_path, object_name), None)
)
if obj: if obj:
return (obj(**kwargs), module_path) return (obj(**kwargs), module_path)
return (None, None) return (None, None)

View File

@@ -1,7 +1,7 @@
# pragma pylint: disable=attribute-defined-outside-init # pragma pylint: disable=attribute-defined-outside-init
""" """
This module load custom hyperopts This module load custom pairlists
""" """
import logging import logging
from pathlib import Path from pathlib import Path
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
class PairListResolver(IResolver): class PairListResolver(IResolver):
""" """
This class contains all the logic to load custom hyperopt class This class contains all the logic to load custom PairList class
""" """
__slots__ = ['pairlist'] __slots__ = ['pairlist']
@@ -39,10 +39,8 @@ class PairListResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve() current_path = Path(__file__).parent.parent.joinpath('pairlist').resolve()
abs_paths = [ abs_paths = self.build_search_paths(config, current_path=current_path,
config['user_data_dir'].joinpath('pairlist'), user_subdir='pairlist', extra_dir=None)
current_path,
]
pairlist = self._load_object(paths=abs_paths, object_type=IPairList, pairlist = self._load_object(paths=abs_paths, object_type=IPairList,
object_name=pairlist_name, kwargs=kwargs) object_name=pairlist_name, kwargs=kwargs)

View File

@@ -13,7 +13,6 @@ from typing import Dict, Optional
from freqtrade import constants, OperationalException from freqtrade import constants, OperationalException
from freqtrade.resolvers import IResolver from freqtrade.resolvers import IResolver
from freqtrade.strategy import import_strategy
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -39,13 +38,13 @@ class StrategyResolver(IResolver):
config=config, config=config,
extra_dir=config.get('strategy_path')) extra_dir=config.get('strategy_path'))
# make sure experimental dict is available # make sure ask_strategy dict is available
if 'experimental' not in config: if 'ask_strategy' not in config:
config['experimental'] = {} config['ask_strategy'] = {}
# Set attributes # Set attributes
# Check if we need to override configuration # Check if we need to override configuration
# (Attribute name, default, experimental) # (Attribute name, default, ask_strategy)
attributes = [("minimal_roi", {"0": 10.0}, False), attributes = [("minimal_roi", {"0": 10.0}, False),
("ticker_interval", None, False), ("ticker_interval", None, False),
("stoploss", None, False), ("stoploss", None, False),
@@ -58,20 +57,20 @@ class StrategyResolver(IResolver):
("order_time_in_force", None, False), ("order_time_in_force", None, False),
("stake_currency", None, False), ("stake_currency", None, False),
("stake_amount", None, False), ("stake_amount", None, False),
("use_sell_signal", False, True), ("use_sell_signal", True, True),
("sell_profit_only", False, True), ("sell_profit_only", False, True),
("ignore_roi_if_buy_signal", False, True), ("ignore_roi_if_buy_signal", False, True),
] ]
for attribute, default, experimental in attributes: for attribute, default, ask_strategy in attributes:
if experimental: if ask_strategy:
self._override_attribute_helper(config['experimental'], attribute, default) self._override_attribute_helper(config['ask_strategy'], attribute, default)
else: else:
self._override_attribute_helper(config, attribute, default) self._override_attribute_helper(config, attribute, default)
# Loop this list again to have output combined # Loop this list again to have output combined
for attribute, _, exp in attributes: for attribute, _, exp in attributes:
if exp and attribute in config['experimental']: if exp and attribute in config['ask_strategy']:
logger.info("Strategy using %s: %s", attribute, config['experimental'][attribute]) logger.info("Strategy using %s: %s", attribute, config['ask_strategy'][attribute])
elif attribute in config: elif attribute in config:
logger.info("Strategy using %s: %s", attribute, config[attribute]) logger.info("Strategy using %s: %s", attribute, config[attribute])
@@ -96,7 +95,10 @@ class StrategyResolver(IResolver):
logger.info("Override strategy '%s' with value in config file: %s.", logger.info("Override strategy '%s' with value in config file: %s.",
attribute, config[attribute]) attribute, config[attribute])
elif hasattr(self.strategy, attribute): elif hasattr(self.strategy, attribute):
config[attribute] = getattr(self.strategy, attribute) val = getattr(self.strategy, attribute)
# None's cannot exist in the config, so do not copy them
if val is not None:
config[attribute] = val
# Explicitly check for None here as other "falsy" values are possible # Explicitly check for None here as other "falsy" values are possible
elif default is not None: elif default is not None:
setattr(self.strategy, attribute, default) setattr(self.strategy, attribute, default)
@@ -122,14 +124,8 @@ class StrategyResolver(IResolver):
""" """
current_path = Path(__file__).parent.parent.joinpath('strategy').resolve() current_path = Path(__file__).parent.parent.joinpath('strategy').resolve()
abs_paths = [ abs_paths = self.build_search_paths(config, current_path=current_path,
config['user_data_dir'].joinpath('strategies'), user_subdir='strategies', extra_dir=extra_dir)
current_path,
]
if extra_dir:
# Add extra strategy directory on top of search paths
abs_paths.insert(0, Path(extra_dir).resolve())
if ":" in strategy_name: if ":" in strategy_name:
logger.info("loading base64 encoded strategy") logger.info("loading base64 encoded strategy")
@@ -153,13 +149,12 @@ class StrategyResolver(IResolver):
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args) strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args) strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args) strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any([x == 2 for x in [strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len]]):
strategy.INTERFACE_VERSION = 1
try: return strategy
return import_strategy(strategy, config=config)
except TypeError as e:
logger.warning(
f"Impossible to load strategy '{strategy_name}'. "
f"Error: {e}")
raise OperationalException( raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist " f"Impossible to load Strategy '{strategy_name}'. This class does not exist "

View File

@@ -2,7 +2,7 @@ import logging
import threading import threading
from datetime import date, datetime from datetime import date, datetime
from ipaddress import IPv4Address from ipaddress import IPv4Address
from typing import Dict from typing import Dict, Callable, Any
from arrow import Arrow from arrow import Arrow
from flask import Flask, jsonify, request from flask import Flask, jsonify, request
@@ -34,41 +34,45 @@ class ArrowJSONEncoder(JSONEncoder):
return JSONEncoder.default(self, obj) return JSONEncoder.default(self, obj)
# Type should really be Callable[[ApiServer, Any], Any], but that will create a circular dependency
def require_login(func: Callable[[Any, Any], Any]):
def func_wrapper(obj, *args, **kwargs):
auth = request.authorization
if auth and obj.check_auth(auth.username, auth.password):
return func(obj, *args, **kwargs)
else:
return jsonify({"error": "Unauthorized"}), 401
return func_wrapper
# Type should really be Callable[[ApiServer], Any], but that will create a circular dependency
def rpc_catch_errors(func: Callable[[Any], Any]):
def func_wrapper(obj, *args, **kwargs):
try:
return func(obj, *args, **kwargs)
except RPCException as e:
logger.exception("API Error calling %s: %s", func.__name__, e)
return obj.rest_error(f"Error querying {func.__name__}: {e}")
return func_wrapper
class ApiServer(RPC): class ApiServer(RPC):
""" """
This class runs api server and provides rpc.rpc functionality to it This class runs api server and provides rpc.rpc functionality to it
This class starts a none blocking thread the api server runs within This class starts a non-blocking thread the api server runs within
""" """
def rpc_catch_errors(func):
def func_wrapper(self, *args, **kwargs):
try:
return func(self, *args, **kwargs)
except RPCException as e:
logger.exception("API Error calling %s: %s", func.__name__, e)
return self.rest_error(f"Error querying {func.__name__}: {e}")
return func_wrapper
def check_auth(self, username, password): def check_auth(self, username, password):
return (username == self._config['api_server'].get('username') and return (username == self._config['api_server'].get('username') and
password == self._config['api_server'].get('password')) password == self._config['api_server'].get('password'))
def require_login(func):
def func_wrapper(self, *args, **kwargs):
auth = request.authorization
if auth and self.check_auth(auth.username, auth.password):
return func(self, *args, **kwargs)
else:
return jsonify({"error": "Unauthorized"}), 401
return func_wrapper
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the api server, and init the super class RPC Init the api server, and init the super class RPC

View File

@@ -15,7 +15,7 @@ from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CryptoFiat(object): class CryptoFiat:
""" """
Object to describe what is the price of Crypto-currency in a FIAT Object to describe what is the price of Crypto-currency in a FIAT
""" """
@@ -60,7 +60,7 @@ class CryptoFiat(object):
return self._expiration - time.time() <= 0 return self._expiration - time.time() <= 0
class CryptoToFiatConverter(object): class CryptoToFiatConverter:
""" """
Main class to initiate Crypto to FIAT. Main class to initiate Crypto to FIAT.
This object contains a list of pair Crypto, FIAT This object contains a list of pair Crypto, FIAT
@@ -104,7 +104,7 @@ class CryptoToFiatConverter(object):
:return: float, value in fiat of the crypto-currency amount :return: float, value in fiat of the crypto-currency amount
""" """
if crypto_symbol == fiat_symbol: if crypto_symbol == fiat_symbol:
return crypto_amount return float(crypto_amount)
price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol) price = self.get_price(crypto_symbol=crypto_symbol, fiat_symbol=fiat_symbol)
return float(crypto_amount) * float(price) return float(crypto_amount) * float(price)

View File

@@ -54,7 +54,7 @@ class RPCException(Exception):
} }
class RPC(object): class RPC:
""" """
RPC class can be used to have extra feature, like bot data, and access to DB data RPC class can be used to have extra feature, like bot data, and access to DB data
""" """
@@ -294,9 +294,9 @@ class RPC(object):
total = total + est_btc total = total + est_btc
output.append({ output.append({
'currency': coin, 'currency': coin,
'available': balance['free'], 'free': balance['free'] if balance['free'] is not None else 0,
'balance': balance['total'], 'balance': balance['total'] if balance['total'] is not None else 0,
'pending': balance['used'], 'used': balance['used'] if balance['used'] is not None else 0,
'est_btc': est_btc, 'est_btc': est_btc,
}) })
if total == 0.0: if total == 0.0:

View File

@@ -9,7 +9,7 @@ from freqtrade.rpc import RPC, RPCMessageType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCManager(object): class RPCManager:
""" """
Class to manage RPC objects (Telegram, Slack, ...) Class to manage RPC objects (Telegram, Slack, ...)
""" """
@@ -18,7 +18,7 @@ class RPCManager(object):
self.registered_modules: List[RPC] = [] self.registered_modules: List[RPC] = []
# Enable telegram # Enable telegram
if freqtrade.config['telegram'].get('enabled', False): if freqtrade.config.get('telegram', {}).get('enabled', False):
logger.info('Enabling rpc.telegram ...') logger.info('Enabling rpc.telegram ...')
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.telegram import Telegram
self.registered_modules.append(Telegram(freqtrade)) self.registered_modules.append(Telegram(freqtrade))
@@ -56,7 +56,10 @@ class RPCManager(object):
logger.info('Sending rpc message: %s', msg) logger.info('Sending rpc message: %s', msg)
for mod in self.registered_modules: for mod in self.registered_modules:
logger.debug('Forwarding message to rpc.%s', mod.name) logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg) try:
mod.send_msg(msg)
except NotImplementedError:
logger.error(f"Message type {msg['type']} not implemented by handler {mod.name}.")
def startup_messages(self, config, pairlist) -> None: def startup_messages(self, config, pairlist) -> None:
if config.get('dry_run', False): if config.get('dry_run', False):

View File

@@ -4,12 +4,12 @@
This module manage Telegram communication This module manage Telegram communication
""" """
import logging import logging
from typing import Any, Callable, Dict, List from typing import Any, Callable, Dict
from tabulate import tabulate from tabulate import tabulate
from telegram import Bot, ParseMode, ReplyKeyboardMarkup, Update from telegram import ParseMode, ReplyKeyboardMarkup, Update
from telegram.error import NetworkError, TelegramError from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater, CallbackContext
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc import RPC, RPCException, RPCMessageType from freqtrade.rpc import RPC, RPCException, RPCMessageType
@@ -31,7 +31,7 @@ def authorized_only(command_handler: Callable[..., None]) -> Callable[..., Any]:
""" """
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
""" Decorator logic """ """ Decorator logic """
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[0]
# Reject unauthorized messages # Reject unauthorized messages
chat_id = int(self._config['telegram']['chat_id']) chat_id = int(self._config['telegram']['chat_id'])
@@ -79,7 +79,8 @@ class Telegram(RPC):
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
""" """
self._updater = Updater(token=self._config['telegram']['token'], workers=0) self._updater = Updater(token=self._config['telegram']['token'], workers=0,
use_context=True)
# Register command handler and start telegram message polling # Register command handler and start telegram message polling
handles = [ handles = [
@@ -96,7 +97,7 @@ class Telegram(RPC):
CommandHandler('reload_conf', self._reload_conf), CommandHandler('reload_conf', self._reload_conf),
CommandHandler('stopbuy', self._stopbuy), CommandHandler('stopbuy', self._stopbuy),
CommandHandler('whitelist', self._whitelist), CommandHandler('whitelist', self._whitelist),
CommandHandler('blacklist', self._blacklist, pass_args=True), CommandHandler('blacklist', self._blacklist),
CommandHandler('edge', self._edge), CommandHandler('edge', self._edge),
CommandHandler('help', self._help), CommandHandler('help', self._help),
CommandHandler('version', self._version), CommandHandler('version', self._version),
@@ -175,7 +176,7 @@ class Telegram(RPC):
self._send_msg(message) self._send_msg(message)
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /status. Handler for /status.
Returns the current TradeThread status Returns the current TradeThread status
@@ -184,11 +185,8 @@ class Telegram(RPC):
:return: None :return: None
""" """
# Check if additional parameters are passed if 'table' in context.args:
params = update.message.text.replace('/status', '').split(' ') \ self._status_table(update, context)
if update.message.text else []
if 'table' in params:
self._status_table(bot, update)
return return
try: try:
@@ -221,13 +219,13 @@ class Telegram(RPC):
messages.append("\n".join([l for l in lines if l]).format(**r)) messages.append("\n".join([l for l in lines if l]).format(**r))
for msg in messages: for msg in messages:
self._send_msg(msg, bot=bot) self._send_msg(msg)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _status_table(self, bot: Bot, update: Update) -> None: def _status_table(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /status table. Handler for /status table.
Returns the current TradeThread status in table format Returns the current TradeThread status in table format
@@ -240,10 +238,10 @@ class Telegram(RPC):
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML) self._send_msg(f"<pre>{message}</pre>", parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _daily(self, bot: Bot, update: Update) -> None: def _daily(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /daily <n> Handler for /daily <n>
Returns a daily profit (in BTC) over the last n days. Returns a daily profit (in BTC) over the last n days.
@@ -254,8 +252,8 @@ class Telegram(RPC):
stake_cur = self._config['stake_currency'] stake_cur = self._config['stake_currency']
fiat_disp_cur = self._config.get('fiat_display_currency', '') fiat_disp_cur = self._config.get('fiat_display_currency', '')
try: try:
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(context.args[0])
except (TypeError, ValueError): except (TypeError, ValueError, IndexError):
timescale = 7 timescale = 7
try: try:
stats = self._rpc_daily_profit( stats = self._rpc_daily_profit(
@@ -272,12 +270,12 @@ class Telegram(RPC):
], ],
tablefmt='simple') tablefmt='simple')
message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>' message = f'<b>Daily Profit over the last {timescale} days</b>:\n<pre>{stats_tab}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _profit(self, bot: Bot, update: Update) -> None: def _profit(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /profit. Handler for /profit.
Returns a cumulative profit statistics. Returns a cumulative profit statistics.
@@ -317,12 +315,12 @@ class Telegram(RPC):
f"*Latest Trade opened:* `{latest_trade_date}`\n" \ f"*Latest Trade opened:* `{latest_trade_date}`\n" \
f"*Avg. Duration:* `{avg_duration}`\n" \ f"*Avg. Duration:* `{avg_duration}`\n" \
f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`" f"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"
self._send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, update: Update, context: CallbackContext) -> None:
""" Handler for /balance """ """ Handler for /balance """
try: try:
result = self._rpc_balance(self._config.get('fiat_display_currency', '')) result = self._rpc_balance(self._config.get('fiat_display_currency', ''))
@@ -330,16 +328,16 @@ class Telegram(RPC):
for currency in result['currencies']: for currency in result['currencies']:
if currency['est_btc'] > 0.0001: if currency['est_btc'] > 0.0001:
curr_output = "*{currency}:*\n" \ curr_output = "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \ "\t`Available: {free: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \ "\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \ "\t`Pending: {used: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency) "\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
else: else:
curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency) curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
# Handle overflowing messsage length # Handle overflowing messsage length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH: if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
self._send_msg(output, bot=bot) self._send_msg(output)
output = curr_output output = curr_output
else: else:
output += curr_output output += curr_output
@@ -347,12 +345,12 @@ class Telegram(RPC):
output += "\n*Estimated Value*:\n" \ output += "\n*Estimated Value*:\n" \
"\t`BTC: {total: .8f}`\n" \ "\t`BTC: {total: .8f}`\n" \
"\t`{symbol}: {value: .2f}`\n".format(**result) "\t`{symbol}: {value: .2f}`\n".format(**result)
self._send_msg(output, bot=bot) self._send_msg(output)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _start(self, bot: Bot, update: Update) -> None: def _start(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /start. Handler for /start.
Starts TradeThread Starts TradeThread
@@ -361,10 +359,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_start() msg = self._rpc_start()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /stop. Handler for /stop.
Stops TradeThread Stops TradeThread
@@ -373,10 +371,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_stop() msg = self._rpc_stop()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None: def _reload_conf(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /reload_conf. Handler for /reload_conf.
Triggers a config file reload Triggers a config file reload
@@ -385,10 +383,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_reload_conf() msg = self._rpc_reload_conf()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _stopbuy(self, bot: Bot, update: Update) -> None: def _stopbuy(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /stop_buy. Handler for /stop_buy.
Sets max_open_trades to 0 and gracefully sells all open trades Sets max_open_trades to 0 and gracefully sells all open trades
@@ -397,10 +395,10 @@ class Telegram(RPC):
:return: None :return: None
""" """
msg = self._rpc_stopbuy() msg = self._rpc_stopbuy()
self._send_msg('Status: `{status}`'.format(**msg), bot=bot) self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /forcesell <id>. Handler for /forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
@@ -409,16 +407,16 @@ class Telegram(RPC):
:return: None :return: None
""" """
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = context.args[0] if len(context.args) > 0 else None
try: try:
msg = self._rpc_forcesell(trade_id) msg = self._rpc_forcesell(trade_id)
self._send_msg('Forcesell Result: `{result}`'.format(**msg), bot=bot) self._send_msg('Forcesell Result: `{result}`'.format(**msg))
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _forcebuy(self, bot: Bot, update: Update) -> None: def _forcebuy(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /forcebuy <asset> <price>. Handler for /forcebuy <asset> <price>.
Buys a pair trade at the given or current price Buys a pair trade at the given or current price
@@ -427,16 +425,15 @@ class Telegram(RPC):
:return: None :return: None
""" """
message = update.message.text.replace('/forcebuy', '').strip().split() pair = context.args[0]
pair = message[0] price = float(context.args[1]) if len(context.args) > 1 else None
price = float(message[1]) if len(message) > 1 else None
try: try:
self._rpc_forcebuy(pair, price) self._rpc_forcebuy(pair, price)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /performance. Handler for /performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
@@ -455,10 +452,10 @@ class Telegram(RPC):
message = '<b>Performance:</b>\n{}'.format(stats) message = '<b>Performance:</b>\n{}'.format(stats)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _count(self, bot: Bot, update: Update) -> None: def _count(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /count. Handler for /count.
Returns the number of trades running Returns the number of trades running
@@ -475,10 +472,10 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _whitelist(self, bot: Bot, update: Update) -> None: def _whitelist(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /whitelist Handler for /whitelist
Shows the currently active whitelist Shows the currently active whitelist
@@ -492,17 +489,17 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message) self._send_msg(message)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _blacklist(self, bot: Bot, update: Update, args: List[str]) -> None: def _blacklist(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /blacklist Handler for /blacklist
Shows the currently active blacklist Shows the currently active blacklist
""" """
try: try:
blacklist = self._rpc_blacklist(args) blacklist = self._rpc_blacklist(context.args)
message = f"Blacklist contains {blacklist['length']} pairs\n" message = f"Blacklist contains {blacklist['length']} pairs\n"
message += f"`{', '.join(blacklist['blacklist'])}`" message += f"`{', '.join(blacklist['blacklist'])}`"
@@ -510,10 +507,10 @@ class Telegram(RPC):
logger.debug(message) logger.debug(message)
self._send_msg(message) self._send_msg(message)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _edge(self, bot: Bot, update: Update) -> None: def _edge(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /edge Handler for /edge
Shows information related to Edge Shows information related to Edge
@@ -522,12 +519,12 @@ class Telegram(RPC):
edge_pairs = self._rpc_edge() edge_pairs = self._rpc_edge()
edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple') edge_pairs_tab = tabulate(edge_pairs, headers='keys', tablefmt='simple')
message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>' message = f'<b>Edge only validated following pairs:</b>\n<pre>{edge_pairs_tab}</pre>'
self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML) self._send_msg(message, parse_mode=ParseMode.HTML)
except RPCException as e: except RPCException as e:
self._send_msg(str(e), bot=bot) self._send_msg(str(e))
@authorized_only @authorized_only
def _help(self, bot: Bot, update: Update) -> None: def _help(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /help. Handler for /help.
Show commands of the bot Show commands of the bot
@@ -559,10 +556,10 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
self._send_msg(message, bot=bot) self._send_msg(message)
@authorized_only @authorized_only
def _version(self, bot: Bot, update: Update) -> None: def _version(self, update: Update, context: CallbackContext) -> None:
""" """
Handler for /version. Handler for /version.
Show version information Show version information
@@ -570,10 +567,9 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
self._send_msg('*Version:* `{}`'.format(__version__), bot=bot) self._send_msg('*Version:* `{}`'.format(__version__))
def _send_msg(self, msg: str, bot: Bot = None, def _send_msg(self, msg: str, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@@ -581,7 +577,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'], keyboard = [['/daily', '/profit', '/balance'],
['/status', '/status table', '/performance'], ['/status', '/status table', '/performance'],
@@ -591,7 +586,7 @@ class Telegram(RPC):
try: try:
try: try:
bot.send_message( self._updater.bot.send_message(
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,
@@ -604,7 +599,7 @@ class Telegram(RPC):
'Telegram NetworkError: %s! Trying one more time.', 'Telegram NetworkError: %s! Trying one more time.',
network_err.message network_err.message
) )
bot.send_message( self._updater.bot.send_message(
self._config['telegram']['chat_id'], self._config['telegram']['chat_id'],
text=msg, text=msg,
parse_mode=parse_mode, parse_mode=parse_mode,

View File

@@ -43,7 +43,9 @@ class Webhook(RPC):
valuedict = self._config['webhook'].get('webhookbuy', None) valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.SELL_NOTIFICATION: elif msg['type'] == RPCMessageType.SELL_NOTIFICATION:
valuedict = self._config['webhook'].get('webhooksell', None) valuedict = self._config['webhook'].get('webhooksell', None)
elif msg['type'] == RPCMessageType.STATUS_NOTIFICATION: elif msg['type'] in(RPCMessageType.STATUS_NOTIFICATION,
RPCMessageType.CUSTOM_NOTIFICATION,
RPCMessageType.WARNING_NOTIFICATION):
valuedict = self._config['webhook'].get('webhookstatus', None) valuedict = self._config['webhook'].get('webhookstatus', None)
else: else:
raise NotImplementedError('Unknown message type: {}'.format(msg['type'])) raise NotImplementedError('Unknown message type: {}'.format(msg['type']))

View File

@@ -25,4 +25,5 @@ class RunMode(Enum):
BACKTEST = "backtest" BACKTEST = "backtest"
EDGE = "edge" EDGE = "edge"
HYPEROPT = "hyperopt" HYPEROPT = "hyperopt"
PLOT = "plot"
OTHER = "other" # Used for plotting scripts and test OTHER = "other" # Used for plotting scripts and test

View File

@@ -1,45 +1 @@
import logging from freqtrade.strategy.interface import IStrategy # noqa: F401
import sys
from copy import deepcopy
from freqtrade.strategy.interface import IStrategy
# Import Default-Strategy to have hyperopt correctly resolve
from freqtrade.strategy.default_strategy import DefaultStrategy # noqa: F401
logger = logging.getLogger(__name__)
def import_strategy(strategy: IStrategy, config: dict) -> IStrategy:
"""
Imports given Strategy instance to global scope
of freqtrade.strategy and returns an instance of it
"""
# Copy all attributes from base class and class
comb = {**strategy.__class__.__dict__, **strategy.__dict__}
# Delete '_abc_impl' from dict as deepcopy fails on 3.7 with
# `TypeError: can't pickle _abc_data objects``
# This will only apply to python 3.7
if sys.version_info.major == 3 and sys.version_info.minor == 7 and '_abc_impl' in comb:
del comb['_abc_impl']
attr = deepcopy(comb)
# Adjust module name
attr['__module__'] = 'freqtrade.strategy'
name = strategy.__class__.__name__
clazz = type(name, (IStrategy,), attr)
logger.debug(
'Imported strategy %s.%s as %s.%s',
strategy.__module__, strategy.__class__.__name__,
clazz.__module__, strategy.__class__.__name__,
)
# Modify global scope to declare class
globals()[name] = clazz
return clazz(config)

View File

@@ -4,15 +4,18 @@ import talib.abstract as ta
from pandas import DataFrame from pandas import DataFrame
import freqtrade.vendor.qtpylib.indicators as qtpylib import freqtrade.vendor.qtpylib.indicators as qtpylib
from freqtrade.indicator_helpers import fishers_inverse
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
class DefaultStrategy(IStrategy): class DefaultStrategy(IStrategy):
""" """
Default Strategy provided by freqtrade bot. Default Strategy provided by freqtrade bot.
You can override it with your own strategy Please do not modify this strategy, it's intended for internal use only.
Please look at the SampleStrategy in the user_data/strategy directory
or strategy repository https://github.com/freqtrade/freqtrade-strategies
for samples and inspiration.
""" """
INTERFACE_VERSION = 2
# Minimal ROI designed for the strategy # Minimal ROI designed for the strategy
minimal_roi = { minimal_roi = {
@@ -73,67 +76,25 @@ class DefaultStrategy(IStrategy):
# ADX # ADX
dataframe['adx'] = ta.ADX(dataframe) dataframe['adx'] = ta.ADX(dataframe)
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
"""
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
"""
# MACD # MACD
macd = ta.MACD(dataframe) macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd'] dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal'] dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist'] dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement # Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe) dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement # Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe) dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
"""
# ROC
dataframe['roc'] = ta.ROC(dataframe)
"""
# RSI # RSI
dataframe['rsi'] = ta.RSI(dataframe) dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi'] = fishers_inverse(dataframe['rsi'])
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast # Stoch fast
stoch_fast = ta.STOCHF(dataframe) stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd'] dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk'] dataframe['fastk'] = stoch_fast['fastk']
"""
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Previous Bollinger bands
# Because ta.BBANDS implementation is broken with small numbers, it actually
# returns middle band for all the three bands. Switch to qtpylib.bollinger_bands
# and use middle band instead.
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
# Bollinger bands # Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2) bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
@@ -142,88 +103,11 @@ class DefaultStrategy(IStrategy):
dataframe['bb_upperband'] = bollinger['upper'] dataframe['bb_upperband'] = bollinger['upper']
# EMA - Exponential Moving Average # EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10) dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average # SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40) dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
return dataframe return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

View File

@@ -39,6 +39,7 @@ class SellType(Enum):
TRAILING_STOP_LOSS = "trailing_stop_loss" TRAILING_STOP_LOSS = "trailing_stop_loss"
SELL_SIGNAL = "sell_signal" SELL_SIGNAL = "sell_signal"
FORCE_SELL = "force_sell" FORCE_SELL = "force_sell"
EMERGENCY_SELL = "emergency_sell"
NONE = "" NONE = ""
@@ -60,6 +61,11 @@ class IStrategy(ABC):
stoploss -> float: optimal stoploss designed for the strategy stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy ticker_interval -> str: value of the ticker interval to use for the strategy
""" """
# Strategy interface version
# Default to version 2
# Version 1 is the initial interface without metadata dict
# Version 2 populate_* include metadata dict
INTERFACE_VERSION: int = 2
_populate_fun_len: int = 0 _populate_fun_len: int = 0
_buy_fun_len: int = 0 _buy_fun_len: int = 0
@@ -72,8 +78,8 @@ class IStrategy(ABC):
# trailing stoploss # trailing stoploss
trailing_stop: bool = False trailing_stop: bool = False
trailing_stop_positive: float trailing_stop_positive: Optional[float] = None
trailing_stop_positive_offset: float trailing_stop_positive_offset: float = 0.0
trailing_only_offset_is_reached = False trailing_only_offset_is_reached = False
# associated ticker interval # associated ticker interval
@@ -196,7 +202,6 @@ class IStrategy(ABC):
:param metadata: Metadata dictionary with additional data (e.g. 'pair') :param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame with ticker data and indicator data :return: DataFrame with ticker data and indicator data
""" """
pair = str(metadata.get('pair')) pair = str(metadata.get('pair'))
# Test if seen this pair and last candle before. # Test if seen this pair and last candle before.
@@ -286,7 +291,6 @@ class IStrategy(ABC):
:param force_stoploss: Externally provided stoploss :param force_stoploss: Externally provided stoploss
:return: True if trade should be sold, False otherwise :return: True if trade should be sold, False otherwise
""" """
# Set current rate to low for backtesting sell # Set current rate to low for backtesting sell
current_rate = low or rate current_rate = low or rate
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
@@ -298,30 +302,41 @@ class IStrategy(ABC):
force_stoploss=force_stoploss, high=high) force_stoploss=force_stoploss, high=high)
if stoplossflag.sell_flag: if stoplossflag.sell_flag:
logger.debug(f"{trade.pair} - Stoploss hit. sell_flag=True, "
f"sell_type={stoplossflag.sell_type}")
return stoplossflag return stoplossflag
# Set current rate to high for backtesting sell # Set current rate to high for backtesting sell
current_rate = high or rate current_rate = high or rate
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
experimental = self.config.get('experimental', {}) config_ask_strategy = self.config.get('ask_strategy', {})
if buy and experimental.get('ignore_roi_if_buy_signal', False): if buy and config_ask_strategy.get('ignore_roi_if_buy_signal', False):
logger.debug('Buy signal still active - not selling.') # This one is noisy, commented out
# logger.debug(f"{trade.pair} - Buy signal still active. sell_flag=False")
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
# Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee) # Check if minimal roi has been reached and no longer in buy conditions (avoiding a fee)
if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date): if self.min_roi_reached(trade=trade, current_profit=current_profit, current_time=date):
logger.debug('Required profit reached. Selling..') logger.debug(f"{trade.pair} - Required profit reached. sell_flag=True, "
f"sell_type=SellType.ROI")
return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI) return SellCheckTuple(sell_flag=True, sell_type=SellType.ROI)
if experimental.get('sell_profit_only', False): if config_ask_strategy.get('sell_profit_only', False):
logger.debug('Checking if trade is profitable..') # This one is noisy, commented out
# logger.debug(f"{trade.pair} - Checking if trade is profitable...")
if trade.calc_profit(rate=rate) <= 0: if trade.calc_profit(rate=rate) <= 0:
# This one is noisy, commented out
# logger.debug(f"{trade.pair} - Trade is not profitable. sell_flag=False")
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
if sell and not buy and experimental.get('use_sell_signal', False):
logger.debug('Sell signal received. Selling..') if sell and not buy and config_ask_strategy.get('use_sell_signal', True):
logger.debug(f"{trade.pair} - Sell signal received. sell_flag=True, "
f"sell_type=SellType.SELL_SIGNAL")
return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL) return SellCheckTuple(sell_flag=True, sell_type=SellType.SELL_SIGNAL)
# This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No sell signal. sell_flag=False")
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade, def stop_loss_reached(self, current_rate: float, trade: Trade,
@@ -332,28 +347,24 @@ class IStrategy(ABC):
decides to sell or not decides to sell or not
:param current_profit: current profit in percent :param current_profit: current profit in percent
""" """
trailing_stop = self.config.get('trailing_stop', False)
stop_loss_value = force_stoploss if force_stoploss else self.stoploss stop_loss_value = force_stoploss if force_stoploss else self.stoploss
# Initiate stoploss with open_rate. Does nothing if stoploss is already set. # Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True) trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if trailing_stop: if self.trailing_stop:
# trailing stoploss handling # trailing stoploss handling
sl_offset = self.config.get('trailing_stop_positive_offset') or 0.0 sl_offset = self.trailing_stop_positive_offset
tsl_only_offset = self.config.get('trailing_only_offset_is_reached', False)
# Make sure current_profit is calculated using high for backtesting. # Make sure current_profit is calculated using high for backtesting.
high_profit = current_profit if not high else trade.calc_profit_percent(high) high_profit = current_profit if not high else trade.calc_profit_percent(high)
# Don't update stoploss if trailing_only_offset_is_reached is true. # Don't update stoploss if trailing_only_offset_is_reached is true.
if not (tsl_only_offset and high_profit < sl_offset): if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
# Specific handling for trailing_stop_positive # Specific handling for trailing_stop_positive
if 'trailing_stop_positive' in self.config and high_profit > sl_offset: if self.trailing_stop_positive is not None and high_profit > sl_offset:
# Ignore mypy error check in configuration that this is a float stop_loss_value = self.trailing_stop_positive
stop_loss_value = self.config.get('trailing_stop_positive') # type: ignore logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
logger.debug(f"using positive stop loss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%") f"offset: {sl_offset:.4g} profit: {current_profit:.4f}%")
trade.adjust_stop_loss(high or current_rate, stop_loss_value) trade.adjust_stop_loss(high or current_rate, stop_loss_value)
@@ -363,20 +374,20 @@ class IStrategy(ABC):
(trade.stop_loss >= current_rate) and (trade.stop_loss >= current_rate) and
(not self.order_types.get('stoploss_on_exchange'))): (not self.order_types.get('stoploss_on_exchange'))):
selltype = SellType.STOP_LOSS sell_type = SellType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing. # If initial stoploss is not the same as current one then it is trailing.
if trade.initial_stop_loss != trade.stop_loss: if trade.initial_stop_loss != trade.stop_loss:
selltype = SellType.TRAILING_STOP_LOSS sell_type = SellType.TRAILING_STOP_LOSS
logger.debug( logger.debug(
f"HIT STOP: current price at {current_rate:.6f}, " f"{trade.pair} - HIT STOP: current price at {current_rate:.6f}, "
f"stop loss is {trade.stop_loss:.6f}, " f"stoploss is {trade.stop_loss:.6f}, "
f"initial stop loss was at {trade.initial_stop_loss:.6f}, " f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}") f"trade opened at {trade.open_rate:.6f}")
logger.debug(f"trailing stop saved {trade.stop_loss - trade.initial_stop_loss:.6f}") logger.debug(f"{trade.pair} - Trailing stop saved "
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
logger.debug('Stop loss hit.') return SellCheckTuple(sell_flag=True, sell_type=sell_type)
return SellCheckTuple(sell_flag=True, sell_type=selltype)
return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE) return SellCheckTuple(sell_flag=False, sell_type=SellType.NONE)

View File

@@ -1,69 +0,0 @@
# pragma pylint: disable=missing-docstring, C0103, bad-continuation, global-statement
# pragma pylint: disable=protected-access
from random import randint
from unittest.mock import MagicMock
from freqtrade.tests.conftest import get_patched_exchange
def test_buy_kraken_trading_agreement(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
order_type = 'limit'
time_in_force = 'ioc'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
amount=1, rate=200, time_in_force=time_in_force)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'buy'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] == 200
assert api_mock.create_order.call_args[0][5] == {'timeInForce': 'ioc',
'trading_agreement': 'agree'}
def test_sell_kraken_trading_agreement(default_conf, mocker):
api_mock = MagicMock()
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
order_type = 'market'
api_mock.options = {}
api_mock.create_order = MagicMock(return_value={
'id': order_id,
'info': {
'foo': 'bar'
}
})
default_conf['dry_run'] = False
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
exchange = get_patched_exchange(mocker, default_conf, api_mock, id="kraken")
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
assert 'id' in order
assert 'info' in order
assert order['id'] == order_id
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
assert api_mock.create_order.call_args[0][1] == order_type
assert api_mock.create_order.call_args[0][2] == 'sell'
assert api_mock.create_order.call_args[0][3] == 1
assert api_mock.create_order.call_args[0][4] is None
assert api_mock.create_order.call_args[0][5] == {'trading_agreement': 'agree'}

View File

@@ -1,235 +0,0 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
# --------------------------------
# Add your lib to import here
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
# This class is a sample. Feel free to customize it.
class TestStrategyLegacy(IStrategy):
"""
This is a test strategy using the legacy function headers, which will be
removed in a future update.
Please do not use this as a template, but refer to user_data/strategy/TestStrategy.py
for a uptodate version of this template.
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.10
# Optimal ticker interval for the strategy
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
Performance Note: For the best performance be frugal on the number of indicators
you are using. Let uncomment only the indicator you are using in your strategies
or your hyperopt configuration, otherwise you will waste your memory and CPU usage.
"""
# Momentum Indicator
# ------------------------------------
# ADX
dataframe['adx'] = ta.ADX(dataframe)
"""
# Awesome oscillator
dataframe['ao'] = qtpylib.awesome_oscillator(dataframe)
# Commodity Channel Index: values Oversold:<-100, Overbought:>100
dataframe['cci'] = ta.CCI(dataframe)
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# MFI
dataframe['mfi'] = ta.MFI(dataframe)
# Minus Directional Indicator / Movement
dataframe['minus_dm'] = ta.MINUS_DM(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# Plus Directional Indicator / Movement
dataframe['plus_dm'] = ta.PLUS_DM(dataframe)
dataframe['plus_di'] = ta.PLUS_DI(dataframe)
dataframe['minus_di'] = ta.MINUS_DI(dataframe)
# ROC
dataframe['roc'] = ta.ROC(dataframe)
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# Inverse Fisher transform on RSI normalized, value [0.0, 100.0] (https://goo.gl/2JGGoy)
dataframe['fisher_rsi_norma'] = 50 * (dataframe['fisher_rsi'] + 1)
# Stoch
stoch = ta.STOCH(dataframe)
dataframe['slowd'] = stoch['slowd']
dataframe['slowk'] = stoch['slowk']
# Stoch fast
stoch_fast = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch_fast['fastd']
dataframe['fastk'] = stoch_fast['fastk']
# Stoch RSI
stoch_rsi = ta.STOCHRSI(dataframe)
dataframe['fastd_rsi'] = stoch_rsi['fastd']
dataframe['fastk_rsi'] = stoch_rsi['fastk']
"""
# Overlap Studies
# ------------------------------------
# Bollinger bands
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
"""
# EMA - Exponential Moving Average
dataframe['ema3'] = ta.EMA(dataframe, timeperiod=3)
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
# SMA - Simple Moving Average
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
"""
# TEMA - Triple Exponential Moving Average
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
# Cycle Indicator
# ------------------------------------
# Hilbert Transform Indicator - SineWave
hilbert = ta.HT_SINE(dataframe)
dataframe['htsine'] = hilbert['sine']
dataframe['htleadsine'] = hilbert['leadsine']
# Pattern Recognition - Bullish candlestick patterns
# ------------------------------------
"""
# Hammer: values [0, 100]
dataframe['CDLHAMMER'] = ta.CDLHAMMER(dataframe)
# Inverted Hammer: values [0, 100]
dataframe['CDLINVERTEDHAMMER'] = ta.CDLINVERTEDHAMMER(dataframe)
# Dragonfly Doji: values [0, 100]
dataframe['CDLDRAGONFLYDOJI'] = ta.CDLDRAGONFLYDOJI(dataframe)
# Piercing Line: values [0, 100]
dataframe['CDLPIERCING'] = ta.CDLPIERCING(dataframe) # values [0, 100]
# Morningstar: values [0, 100]
dataframe['CDLMORNINGSTAR'] = ta.CDLMORNINGSTAR(dataframe) # values [0, 100]
# Three White Soldiers: values [0, 100]
dataframe['CDL3WHITESOLDIERS'] = ta.CDL3WHITESOLDIERS(dataframe) # values [0, 100]
"""
# Pattern Recognition - Bearish candlestick patterns
# ------------------------------------
"""
# Hanging Man: values [0, 100]
dataframe['CDLHANGINGMAN'] = ta.CDLHANGINGMAN(dataframe)
# Shooting Star: values [0, 100]
dataframe['CDLSHOOTINGSTAR'] = ta.CDLSHOOTINGSTAR(dataframe)
# Gravestone Doji: values [0, 100]
dataframe['CDLGRAVESTONEDOJI'] = ta.CDLGRAVESTONEDOJI(dataframe)
# Dark Cloud Cover: values [0, 100]
dataframe['CDLDARKCLOUDCOVER'] = ta.CDLDARKCLOUDCOVER(dataframe)
# Evening Doji Star: values [0, 100]
dataframe['CDLEVENINGDOJISTAR'] = ta.CDLEVENINGDOJISTAR(dataframe)
# Evening Star: values [0, 100]
dataframe['CDLEVENINGSTAR'] = ta.CDLEVENINGSTAR(dataframe)
"""
# Pattern Recognition - Bullish/Bearish candlestick patterns
# ------------------------------------
"""
# Three Line Strike: values [0, -100, 100]
dataframe['CDL3LINESTRIKE'] = ta.CDL3LINESTRIKE(dataframe)
# Spinning Top: values [0, -100, 100]
dataframe['CDLSPINNINGTOP'] = ta.CDLSPINNINGTOP(dataframe) # values [0, -100, 100]
# Engulfing: values [0, -100, 100]
dataframe['CDLENGULFING'] = ta.CDLENGULFING(dataframe) # values [0, -100, 100]
# Harami: values [0, -100, 100]
dataframe['CDLHARAMI'] = ta.CDLHARAMI(dataframe) # values [0, -100, 100]
# Three Outside Up/Down: values [0, -100, 100]
dataframe['CDL3OUTSIDE'] = ta.CDL3OUTSIDE(dataframe) # values [0, -100, 100]
# Three Inside Up/Down: values [0, -100, 100]
dataframe['CDL3INSIDE'] = ta.CDL3INSIDE(dataframe) # values [0, -100, 100]
"""
# Chart type
# ------------------------------------
"""
# Heikinashi stategy
heikinashi = qtpylib.heikinashi(dataframe)
dataframe['ha_open'] = heikinashi['open']
dataframe['ha_close'] = heikinashi['close']
dataframe['ha_high'] = heikinashi['high']
dataframe['ha_low'] = heikinashi['low']
"""
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 30) &
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1))
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
),
'sell'] = 1
return dataframe

View File

@@ -1,181 +0,0 @@
# pragma pylint: disable=missing-docstring, C0103
import argparse
import pytest
from freqtrade.configuration import Arguments
from freqtrade.configuration.arguments import ARGS_PLOT_DATAFRAME
from freqtrade.configuration.cli_options import check_int_positive
# Parse common command-line-arguments. Used for all tools
def test_parse_args_none() -> None:
arguments = Arguments([], '')
assert isinstance(arguments, Arguments)
assert isinstance(arguments.parser, argparse.ArgumentParser)
def test_parse_args_defaults() -> None:
args = Arguments([], '').get_parsed_arg()
assert args.config == ['config.json']
assert args.strategy_path is None
assert args.datadir is None
assert args.verbosity == 0
def test_parse_args_config() -> None:
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
assert args.config == ['/dev/null']
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
assert args.config == ['/dev/null']
args = Arguments(['--config', '/dev/null',
'--config', '/dev/zero'],
'').get_parsed_arg()
assert args.config == ['/dev/null', '/dev/zero']
def test_parse_args_db_url() -> None:
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
assert args.db_url == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg()
assert args.verbosity == 1
args = Arguments(['--verbose'], '').get_parsed_arg()
assert args.verbosity == 1
def test_common_scripts_options() -> None:
args = Arguments(['download-data', '-p', 'ETH/BTC', 'XRP/BTC'], '').get_parsed_arg()
assert args.pairs == ['ETH/BTC', 'XRP/BTC']
assert hasattr(args, "func")
def test_parse_args_version() -> None:
with pytest.raises(SystemExit, match=r'0'):
Arguments(['--version'], '').get_parsed_arg()
def test_parse_args_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['-c'], '').get_parsed_arg()
def test_parse_args_strategy() -> None:
args = Arguments(['--strategy', 'SomeStrategy'], '').get_parsed_arg()
assert args.strategy == 'SomeStrategy'
def test_parse_args_strategy_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy'], '').get_parsed_arg()
def test_parse_args_strategy_path() -> None:
args = Arguments(['--strategy-path', '/some/path'], '').get_parsed_arg()
assert args.strategy_path == '/some/path'
def test_parse_args_strategy_path_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['--strategy-path'], '').get_parsed_arg()
def test_parse_args_backtesting_invalid() -> None:
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval'], '').get_parsed_arg()
with pytest.raises(SystemExit, match=r'2'):
Arguments(['backtesting --ticker-interval', 'abc'], '').get_parsed_arg()
def test_parse_args_backtesting_custom() -> None:
args = [
'-c', 'test_conf.json',
'backtesting',
'--ticker-interval', '1m',
'--refresh-pairs-cached',
'--strategy-list',
'DefaultStrategy',
'TestStrategy'
]
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json']
assert call_args.verbosity == 0
assert call_args.subparser == 'backtesting'
assert call_args.func is not None
assert call_args.ticker_interval == '1m'
assert call_args.refresh_pairs is True
assert type(call_args.strategy_list) is list
assert len(call_args.strategy_list) == 2
def test_parse_args_hyperopt_custom() -> None:
args = [
'-c', 'test_conf.json',
'hyperopt',
'--epochs', '20',
'--spaces', 'buy'
]
call_args = Arguments(args, '').get_parsed_arg()
assert call_args.config == ['test_conf.json']
assert call_args.epochs == 20
assert call_args.verbosity == 0
assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy']
assert call_args.func is not None
def test_download_data_options() -> None:
args = [
'--datadir', 'datadir/directory',
'download-data',
'--pairs-file', 'file_with_pairs',
'--days', '30',
'--exchange', 'binance'
]
args = Arguments(args, '').get_parsed_arg()
assert args.pairs_file == 'file_with_pairs'
assert args.datadir == 'datadir/directory'
assert args.days == 30
assert args.exchange == 'binance'
def test_plot_dataframe_options() -> None:
args = [
'--indicators1', 'sma10,sma100',
'--indicators2', 'macd,fastd,fastk',
'--plot-limit', '30',
'-p', 'UNITTEST/BTC',
]
arguments = Arguments(args, '')
arguments._build_args(ARGS_PLOT_DATAFRAME)
pargs = arguments._parse_args()
assert pargs.indicators1 == "sma10,sma100"
assert pargs.indicators2 == "macd,fastd,fastk"
assert pargs.plot_limit == 30
assert pargs.pairs == ["UNITTEST/BTC"]
def test_check_int_positive() -> None:
assert check_int_positive("3") == 3
assert check_int_positive("1") == 1
assert check_int_positive("100") == 100
with pytest.raises(argparse.ArgumentTypeError):
check_int_positive("-2")
with pytest.raises(argparse.ArgumentTypeError):
check_int_positive("0")
with pytest.raises(argparse.ArgumentTypeError):
check_int_positive("3.5")
with pytest.raises(argparse.ArgumentTypeError):
check_int_positive("DeadBeef")

View File

@@ -1,15 +0,0 @@
# pragma pylint: disable=missing-docstring
import pandas as pd
from freqtrade.indicator_helpers import went_down, went_up
def test_went_up():
series = pd.Series([1, 2, 3, 1])
assert went_up(series).equals(pd.Series([False, True, True, False]))
def test_went_down():
series = pd.Series([1, 2, 3, 1])
assert went_down(series).equals(pd.Series([False, False, False, True]))

View File

@@ -1,154 +0,0 @@
import re
from pathlib import Path
from unittest.mock import MagicMock, PropertyMock
import pytest
from freqtrade.state import RunMode
from freqtrade.tests.conftest import get_args, log_has, patch_exchange
from freqtrade.utils import (setup_utils_configuration, start_create_userdir,
start_download_data, start_list_exchanges)
def test_setup_utils_configuration():
args = [
'--config', 'config.json.example',
]
config = setup_utils_configuration(get_args(args), RunMode.OTHER)
assert "exchange" in config
assert config['exchange']['dry_run'] is True
assert config['exchange']['key'] == ''
assert config['exchange']['secret'] == ''
def test_list_exchanges(capsys):
args = [
"list-exchanges",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert re.match(r"Exchanges supported by ccxt and available.*", captured.out)
assert re.match(r".*binance,.*", captured.out)
assert re.match(r".*bittrex,.*", captured.out)
# Test with --one-column
args = [
"list-exchanges",
"--one-column",
]
start_list_exchanges(get_args(args))
captured = capsys.readouterr()
assert not re.match(r"Exchanges supported by ccxt and available.*", captured.out)
assert re.search(r"^binance$", captured.out, re.MULTILINE)
assert re.search(r"^bittrex$", captured.out, re.MULTILINE)
def test_create_datadir_failed(caplog):
args = [
"create-userdir",
]
with pytest.raises(SystemExit):
start_create_userdir(get_args(args))
assert log_has("`create-userdir` requires --userdir to be set.", caplog)
def test_create_datadir(caplog, mocker):
cud = mocker.patch("freqtrade.utils.create_userdata_dir", MagicMock())
args = [
"create-userdir",
"--userdir",
"/temp/freqtrade/test"
]
start_create_userdir(get_args(args))
assert cud.call_count == 1
assert len(caplog.record_tuples) == 0
def test_download_data(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--erase",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype is None
assert dl_mock.call_args[1]['timerange'].stoptype is None
assert log_has("Deleting existing data for pair ETH/BTC, interval 1m.", caplog)
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_days(mocker, markets, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
mocker.patch.object(Path, "exists", MagicMock(return_value=True))
mocker.patch.object(Path, "unlink", MagicMock())
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
"--days", "20",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 4
assert dl_mock.call_args[1]['timerange'].starttype == 'date'
assert log_has("Downloading pair ETH/BTC, interval 1m.", caplog)
def test_download_data_no_markets(mocker, caplog):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history', MagicMock())
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value={})
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
]
start_download_data(get_args(args))
assert dl_mock.call_count == 0
assert log_has("Skipping pair ETH/BTC...", caplog)
assert log_has("Pairs [ETH/BTC,XRP/BTC] not available on exchange binance.", caplog)
def test_download_data_keyboardInterrupt(mocker, caplog, markets):
dl_mock = mocker.patch('freqtrade.utils.download_pair_history',
MagicMock(side_effect=KeyboardInterrupt))
patch_exchange(mocker)
mocker.patch(
'freqtrade.exchange.Exchange.markets', PropertyMock(return_value=markets)
)
args = [
"download-data",
"--exchange", "binance",
"--pairs", "ETH/BTC", "XRP/BTC",
]
with pytest.raises(SystemExit):
start_download_data(get_args(args))
assert dl_mock.call_count == 1

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More