Compare commits

..

162 Commits

Author SHA1 Message Date
gcarq
e2eceaa904 Merge branch 'release/0.13.0' 2017-11-01 01:15:31 +01:00
gcarq
f34af0ad67 version bump 2017-11-01 01:15:06 +01:00
gcarq
e07904d436 PEP8 linting 2017-10-31 00:36:35 +01:00
gcarq
26468bef83 balance: filter currencies with 0.0 balances 2017-10-31 00:29:22 +01:00
Michael Egger
ea1b1e11ea Merge pull request #88 from gcarq/reduce_memory_use
Reduce memory use in backtesting
2017-10-31 00:28:38 +01:00
Janne Sinivirta
e68e6c0a1a reuse mock in hyperopt also 2017-10-30 22:31:28 +02:00
Janne Sinivirta
7190226c84 reuse same mock for get_ticker_history, just change return_value 2017-10-30 22:06:09 +02:00
gcarq
6f2915e25e move qtpylib to vendor folder
This is necessary to distribute qtpylib with
freqtrade when you install it globally.
2017-10-30 20:41:36 +01:00
gcarq
6f7ac0720b add qtpylib to manifest 2017-10-30 20:24:58 +01:00
gcarq
b76554a487 add __init__ file for qtpylib 2017-10-30 20:23:19 +01:00
Janne Sinivirta
8da55c3742 move patching of arrow.utcnow to remove 500 unnecessary mock objects 2017-10-30 19:56:53 +02:00
Michael Egger
05111edd04 Merge pull request #87 from gcarq/more_triggers
More triggers and guards to hyperopt
2017-10-30 14:43:18 +01:00
Michael Egger
4c2dea83c5 Merge pull request #84 from gcarq/telegram/show-balance
Telegram command: /show balance
2017-10-29 22:05:10 +01:00
Janne Sinivirta
d066817d0b removed below_sma and over_sma to wait for better implementation 2017-10-29 21:33:57 +02:00
Janne Sinivirta
a632121368 add macd cross signal trigger to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
473d09b5cd add ema50 and ema100. add long uptrend ema guard to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
893738d6f0 add MACD to analyze 2017-10-29 21:33:57 +02:00
Janne Sinivirta
22cfef7d36 add ema5 cross ema10 trigger to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
e1bbe1d9a9 adjust indicator ranges in hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
ec981b415a add RSI to hyperopt 2017-10-29 21:33:57 +02:00
Janne Sinivirta
57a17697a0 add RSI, MOM, EMA5 and EMA10 to analyze 2017-10-29 21:33:57 +02:00
Samuel Husso
f4fe09ffbf added get_balances as a abstract method to the interface baseclass 2017-10-29 17:57:57 +02:00
Michael Egger
871b5e17ee Merge pull request #85 from gcarq/datetime_fixes
Performance improvements for backtesting
2017-10-29 15:56:20 +01:00
Janne Sinivirta
9b00fc3474 use .ix instead of .loc for small perf boost 2017-10-29 16:28:55 +02:00
Janne Sinivirta
3b1dc36d8a switch to using itertuples instead of iterrows as it's a lot faster 2017-10-29 16:28:55 +02:00
Janne Sinivirta
4edf8f2079 copy only needed columns before iterating over them 2017-10-29 16:28:55 +02:00
Janne Sinivirta
54987fd9ca do date parsing while loading json, not later 2017-10-29 16:28:55 +02:00
Janne Sinivirta
da9c3e7d7d remove leftover dates from removing date filtering 2017-10-29 16:28:55 +02:00
Michael Egger
a948142ef5 Merge pull request #83 from gcarq/better-hyperopt-objective
Better hyperopt objective
2017-10-29 14:13:44 +01:00
Samuel Husso
4f6c3f94e0 added tests to /balance, minor cleanup 2017-10-29 10:10:00 +02:00
Janne Sinivirta
25d6d6bbe5 remove unused imports from test_hyperopt 2017-10-28 15:32:29 +03:00
Janne Sinivirta
649781d823 store result strings, display best result in summary. switch to a lot better objective algo 2017-10-28 15:26:22 +03:00
Janne Sinivirta
08ca7a8166 change print to format so result can be used in hyperopt Trials 2017-10-28 15:26:22 +03:00
Samuel Husso
dd78c62c3d added new command to return balance across all currencies 2017-10-28 08:59:43 +03:00
Samuel Husso
29de1645fe Merge pull request #82 from gcarq/feature/handle-process-signals
handle SIGINT, SIGTERM and SIGABRT process signals
2017-10-28 08:49:42 +03:00
gcarq
4139b0b0c7 add signal handler for SIGINT, SIGTERM and SIGABRT 2017-10-27 15:52:14 +02:00
Samuel Husso
0c33e917d5 Merge pull request #79 from gcarq/qtpylib
Include new indicators from qtpylib
2017-10-27 12:11:04 +03:00
Janne Sinivirta
e401a016f5 change analyze tests to use full json dump from bittrex 2017-10-26 16:50:31 +03:00
Janne Sinivirta
e0fde8665c Merge pull request #80 from gcarq/fix-testdate-dl-path
download testdata to correct folder when running from project root
2017-10-26 10:37:38 +03:00
Samuel Husso
752520c823 When running from project root download the files to the testdata folder instead of cwd 2017-10-26 10:24:22 +03:00
Janne Sinivirta
6ba2492360 add Awesome Oscillator and try it in hyperopt 2017-10-25 18:37:20 +03:00
Janne Sinivirta
d5d798f6fa pull in new indicators from QTPYLib 2017-10-25 18:37:20 +03:00
Janne Sinivirta
9c9cf76a0d Merge pull request #78 from gcarq/refactor-backtest
Refactor backtest functionality
2017-10-25 18:19:44 +03:00
Samuel Husso
041e201713 remove duplicated backtesting from hyperopt 2017-10-25 08:17:17 +03:00
gcarq
e09505b22d Merge tag '0.12.0' into develop
0.12.0
2017-10-24 18:14:41 +02:00
gcarq
6b15cb9b10 Merge branch 'release/0.12.0' 2017-10-24 18:14:37 +02:00
gcarq
ff4fcdc760 version bump 2017-10-24 18:14:31 +02:00
Samuel Husso
f43ba44b15 refactor backtesting to its own method as we use it also in hyperopt 2017-10-24 07:58:42 +03:00
Michael Egger
79c3e0583d Merge pull request #76 from gcarq/hyperopt
Use hyperopt to find optimal parameters for buy strategy
2017-10-23 09:40:13 +02:00
Janne Sinivirta
f6ef8383bb remove filtering from analyze that is super slow and not really needed 2017-10-22 21:50:07 +03:00
Janne Sinivirta
6f5307fda7 use more aggressive stop loss for hyperopt 2017-10-22 17:15:57 +03:00
Janne Sinivirta
37004e331a remove unused import and commented out code 2017-10-22 17:14:55 +03:00
Janne Sinivirta
57acf85b42 try a different objective function 2017-10-22 17:11:01 +03:00
Michael Egger
96790d50e5 Merge pull request #77 from gcarq/help-command
Help command to Telegram bot
2017-10-21 13:51:08 +02:00
Janne Sinivirta
d32ff3410c add help command to telegram bot 2017-10-21 11:08:08 +03:00
Janne Sinivirta
35838f5e64 upgrade to latest telegram lib 2017-10-21 11:07:29 +03:00
Janne Sinivirta
913488910c bump minimum evaluations to 40 rounds 2017-10-21 10:29:05 +03:00
Janne Sinivirta
17b984a7cd adjust objective function to emphasize trade lenghts more 2017-10-21 10:28:43 +03:00
Janne Sinivirta
f79b44eefe adjust ROI map for shorter trades 2017-10-21 10:28:02 +03:00
Janne Sinivirta
146c254c0f start adding other triggers than just the lower BBands 2017-10-21 10:26:38 +03:00
Janne Sinivirta
ce2966dd7f add uptrend_sma to hyperopt 2017-10-20 18:29:38 +03:00
Janne Sinivirta
0fbca8b8ef add CCI to hyperopt 2017-10-20 13:14:28 +03:00
Janne Sinivirta
3f7a583de6 add SAR to hyperopt. add over/under sma options to hyperopt 2017-10-20 12:56:44 +03:00
Janne Sinivirta
1196983d5f change objective to emphasize shorter trades and include average profit 2017-10-20 10:39:36 +03:00
Janne Sinivirta
bbb2c7cf62 more parametrizing. adjust ranges for previous parameters 2017-10-20 10:39:04 +03:00
Janne Sinivirta
ff100bdac4 the optimizer expects values in the order of smaller is better 2017-10-19 18:29:57 +03:00
Janne Sinivirta
4feb038d0a add hyperopt dependencies 2017-10-19 17:46:41 +03:00
Janne Sinivirta
1792e0fb9b use hyperopt to find optimal parameter values for indicators 2017-10-19 17:12:49 +03:00
Janne Sinivirta
d4f8b3ebbc remove setup.cfg as it's not used but it messes with running a single test 2017-10-19 17:12:08 +03:00
Michael Egger
aeef9bac33 Merge pull request #70 from dertione/patch-2
Download automatically altcoin datas
2017-10-17 13:36:33 +02:00
Michael Egger
eff361a104 Merge pull request #73 from gcarq/small_tweaks_to_strategy
Small tweaks to strategy
2017-10-15 18:08:18 +02:00
dertione
389f9b45bb update pylint 10/10 2017-10-15 17:24:49 +02:00
Janne Sinivirta
c9741cb291 adjust roi settings for faster trades 2017-10-15 17:32:07 +03:00
Janne Sinivirta
bf6f563df2 small tweaks to buy strategy and it's visualization 2017-10-15 17:32:07 +03:00
Michael Egger
58f34d4f4b Merge pull request #71 from steerio/develop
More efficient and flexible Docker builds
2017-10-15 15:46:39 +02:00
Janne Sinivirta
2c4d0144ba Add note about binding sqlite with dry_run enabled 2017-10-15 14:40:02 +03:00
dertione
afd1a0bf9b update for pylint 2017-10-14 14:40:26 +02:00
dertione
37f6c213f6 fork test 2017-10-13 15:50:50 +02:00
Roland Venesz
76736902c6 Merge branch 'master' into develop 2017-10-13 15:48:25 +02:00
Roland Venesz
d266171ed8 Docker improvements (faster and more secure builds) 2017-10-13 15:47:13 +02:00
Michael Egger
e7df373544 Merge pull request #67 from gcarq/upgrade-deps
Upgrade dependencies
2017-10-12 09:49:45 +02:00
Michael Egger
aa4b64d0bb Merge pull request #65 from xsmile/patch-4
set exchange in analyze.__main__ to fix plotting
2017-10-12 09:42:20 +02:00
Michael Egger
4559ddd74f Merge pull request #64 from xsmile/patch-1
Bittrex provider
2017-10-12 09:37:15 +02:00
xsmile
eecc45f8ba set exchange in analyze.__main__ to fix plotting
requires #64
2017-10-11 20:04:31 +02:00
xsmile
d76476040a Bittrex provider
remove redundant 'name' property and pair validation call
2017-10-11 19:51:37 +02:00
Janne Sinivirta
0c8c149b86 Fix the command for running backtesting in README.md 2017-10-11 13:09:57 +03:00
Janne Sinivirta
60a7f8614c upgrade dependencies 2017-10-10 19:04:05 +03:00
gcarq
c31b67bf7a Merge tag '0.11.0' into develop
0.11.0
2017-10-10 17:55:10 +02:00
gcarq
604a888791 Merge branch 'release/0.11.0' 2017-10-10 17:55:05 +02:00
gcarq
bfac1936d9 version bump 2017-10-10 17:54:42 +02:00
Janne Sinivirta
b1de0de5a5 Merge pull request #61 from xsmile/patch-2
add exchange package to manifest
2017-10-09 10:30:41 +03:00
xsmile
75ea2c4e1a add exchange package to manifest 2017-10-08 23:01:36 +02:00
Michael Egger
5e0f143a38 Merge pull request #58 from xsmile/exchange-interface
Exchange refactoring
2017-10-08 15:56:50 +02:00
gcarq
2d983db2e0 Merge branch 'master' into develop 2017-10-08 15:15:44 +02:00
gcarq
d9b01eee15 adapt install section 2017-10-08 15:15:11 +02:00
xsmile
2e368ef7aa docstring fix 2017-10-07 18:10:45 +02:00
xsmile
34c774c067 move exchange module content to exchange package and the interface to a new module 2017-10-07 18:07:29 +02:00
xsmile
ac32850034 simplify exchange initialization 2017-10-07 17:38:33 +02:00
xsmile
95e5c2e6c1 remove 'enabled' property in exchange config 2017-10-07 17:36:48 +02:00
Janne Sinivirta
aef42336e6 fixes to README.md
- Fix the command for running unit tests
- Add command to run backtest unit tests
2017-10-06 14:12:23 +03:00
Janne Sinivirta
f78427d236 Merge pull request #57 from shusso/fix-backtest-path
fix incorrect backtest testdata path
2017-10-06 14:04:01 +03:00
xsmile
b9eb266236 Exchange refactoring 2017-10-06 12:22:04 +02:00
Samuel Husso
e0896fdd7b fix incorrect backtest testdata path 2017-10-06 10:54:04 +03:00
Michael Egger
11f97ccf87 Merge pull request #54 from gcarq/fix-coverage
Fix coverage
2017-10-02 19:29:33 +02:00
Janne Sinivirta
3506e3ceec try directly invoking pytest for fixing coveralls issue 2017-10-02 20:17:14 +03:00
Janne Sinivirta
27b2624a67 let pytest do coverage 2017-10-02 19:48:47 +03:00
Janne Sinivirta
8500032bff add coverage config file to omit test files from coverage report 2017-10-02 19:27:18 +03:00
Janne Sinivirta
b2522b8dbc add pytest-cov dependency 2017-10-02 19:17:54 +03:00
Janne Sinivirta
0f3ceebcd4 Merge pull request #53 from gcarq/feature/patch-missing-calls
patch missing calls
2017-10-02 10:38:01 +03:00
gcarq
f44ab2f44b patch missing http calls 2017-10-01 23:28:09 +02:00
Janne Sinivirta
3fe5302db3 Merge pull request #52 from gcarq/convert-to-pytest
Switch to using Pytest
2017-10-01 17:28:23 +03:00
Janne Sinivirta
ea62c49c3a fix passing parameters to pytest 2017-10-01 17:19:14 +03:00
Janne Sinivirta
02673b94dd use explicit package name for pytest running 2017-10-01 17:04:38 +03:00
Janne Sinivirta
17e8bbacc3 add pytest-mock to setup.py 2017-10-01 16:17:27 +03:00
Janne Sinivirta
463123adc5 Merge branch 'develop' into convert-to-pytest 2017-10-01 16:14:50 +03:00
Janne Sinivirta
5537f0bf5b simplify unnecessary == True and == False assertions 2017-10-01 15:45:31 +03:00
Janne Sinivirta
5551c9ec3b add pragmas to disable pylint warnings for missing docstrings in test files 2017-10-01 15:40:40 +03:00
Janne Sinivirta
ff145b6306 use mocker for mocking to get rid of deep nesting 2017-10-01 15:40:12 +03:00
Janne Sinivirta
add6c875d6 add pytest-mock to requirements.txt 2017-10-01 15:24:27 +03:00
gcarq
378b5a3b14 fix indentation 2017-10-01 14:07:09 +02:00
gcarq
e14cc2e4f6 add setup.cfg to force pytest 2017-10-01 14:06:18 +02:00
Janne Sinivirta
616d5b61cc remove numbers from test method names 2017-10-01 11:11:20 +03:00
Janne Sinivirta
9cca42e371 add pytest to requirements.txt 2017-10-01 11:06:40 +03:00
Janne Sinivirta
06ad311aa3 remove Test classes and use pytest fixtures 2017-10-01 11:02:47 +03:00
gcarq
e42edd9de7 add required folders to MANIFEST 2017-09-30 21:01:23 +02:00
gcarq
3456ead839 add numpy as required dep 2017-09-30 21:00:53 +02:00
gcarq
a4a1f7961a set executable bit 2017-09-30 21:00:42 +02:00
gcarq
8057333501 adapt Dockerfile for new project structure 2017-09-30 21:00:14 +02:00
gcarq
1eee0c91bf adapt README 2017-09-30 20:59:54 +02:00
Janne Sinivirta
53b4c3722e convert asserts to pytest style 2017-09-30 20:38:19 +03:00
gcarq
3f6f502e66 add code coverage badge 2017-09-30 19:05:37 +02:00
Janne Sinivirta
d73c656514 Merge pull request #50 from gcarq/feature/fix-whitelist-vanishing
fix whitelist vanishing
2017-09-30 20:00:38 +03:00
gcarq
f409bdbba8 add coveralls.io to measure code quality 2017-09-30 18:55:48 +02:00
gcarq
4b42e1af4b use deepcopy 2017-09-30 18:23:11 +02:00
gcarq
898ab5a370 implement test to reproduce it 2017-09-30 18:22:05 +02:00
Janne Sinivirta
6389778d49 Merge pull request #47 from gcarq/feature/project-structure
Refactor project structure (closes #34)
2017-09-30 19:19:00 +03:00
gcarq
b85b913657 revert coveralls 2017-09-30 17:13:08 +02:00
gcarq
bbef0edcd1 add coveralls.io to measure code quality 2017-09-30 17:06:15 +02:00
gcarq
8d3a6279b2 use pytest 2017-09-30 15:58:31 +02:00
gcarq
df4da75535 add pypi classifiers 2017-09-29 20:15:54 +02:00
gcarq
04bba626a8 define install_requires for package distribution 2017-09-29 20:07:50 +02:00
gcarq
9c6c21637d fix testsuite 2017-09-29 19:28:32 +02:00
gcarq
09b27d2094 add manifest file 2017-09-29 19:28:32 +02:00
gcarq
998a887736 add command line script 2017-09-29 19:28:32 +02:00
gcarq
00499fa0d7 add setup.py 2017-09-29 19:28:32 +02:00
gcarq
0c517ee3b6 move project into freqtrade/ 2017-09-29 19:28:32 +02:00
gcarq
b225b0cb90 remove python nightly interpreter 2017-09-29 19:07:25 +02:00
Janne Sinivirta
d045116297 upgraded to latest telegram library (8.0) 2017-09-29 18:59:05 +03:00
Janne Sinivirta
8b859ad358 Merge pull request #44 from gcarq/another-better-strategy
Better buy strategy and sell criteria
2017-09-29 13:32:00 +03:00
Janne Sinivirta
0085db825d Merge branch 'develop' into another-better-strategy 2017-09-29 13:13:44 +03:00
Janne Sinivirta
1f1e64560a adjust roi and stop loss in config.json.example 2017-09-29 09:58:00 +03:00
Janne Sinivirta
c9226a329c adjust roi and stop loss for backtesting 2017-09-29 09:56:52 +03:00
Janne Sinivirta
44cdf3e0c2 improved buy signal strategy 2017-09-29 09:55:11 +03:00
Janne Sinivirta
b97f0f0705 use btc-eth as default pair for analyze graph 2017-09-29 09:49:19 +03:00
Janne Sinivirta
b2f4778352 show last 24hours in analyze graph 2017-09-29 09:48:59 +03:00
Janne Sinivirta
272abed807 show two decimals in average profit in backtesting results 2017-09-29 09:46:45 +03:00
Michael Egger
0437546e4b Merge pull request #41 from xsmile/optional-telegram
Telegram: Fix being optional
2017-09-29 00:37:46 +02:00
xsmile
2df2041d53 Telegram: Fix being optional 2017-09-29 00:15:38 +02:00
Michael Egger
3b9d354a62 Merge pull request #30 from freshfunkee/feature/handle-empty-dataframe
dataframe empty check
2017-09-28 21:49:06 +02:00
Eoin
a45073997d review comments: change log to warning 2017-09-28 20:07:33 +01:00
gcarq
d102a8f8a1 Merge tag '0.10.0' into develop
0.10.0
2017-09-28 19:17:08 +02:00
Eoin
0e5edd08e5 add dataframe empty check 2017-09-27 23:43:32 +01:00
47 changed files with 2027 additions and 776 deletions

2
.coveragerc Normal file
View File

@@ -0,0 +1,2 @@
[run]
omit = freqtrade/tests/*

6
.dockerignore Normal file
View File

@@ -0,0 +1,6 @@
.git
.gitignore
Dockerfile
.dockerignore
config.json*
*.sqlite

View File

@@ -4,10 +4,6 @@ os:
language: python
python:
- 3.6
- nightly
matrix:
allow_failures:
- python: nightly
addons:
apt:
packages:
@@ -19,9 +15,12 @@ install:
- tar zxvf ta-lib-0.4.0-src.tar.gz
- cd ta-lib && ./configure && sudo make && sudo make install && cd ..
- export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH
- pip install coveralls
- pip install -r requirements.txt
script:
- python -m unittest
- pytest --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/
after_success:
- coveralls
notifications:
slack:
secure: bKLXmOrx8e2aPZl7W8DA5BdPAXWGpI5UzST33oc1G/thegXcDVmHBTJrBs4sZak6bgAclQQrdZIsRd2eFYzHLalJEaw6pk7hoAw8SvLnZO0ZurWboz7qg2+aZZXfK4eKl/VUe4sM9M4e/qxjkK+yWG7Marg69c4v1ypF7ezUi1fPYILYw8u0paaiX0N5UX8XNlXy+PBlga2MxDjUY70MuajSZhPsY2pDUvYnMY1D/7XN3cFW0g+3O8zXjF0IF4q1Z/1ASQe+eYjKwPQacE+O8KDD+ZJYoTOFBAPllrtpO1jnOPFjNGf3JIbVMZw4bFjIL0mSQaiSUaUErbU3sFZ5Or79rF93XZ81V7uEZ55vD8KMfR2CB1cQJcZcj0v50BxLo0InkFqa0Y8Nra3sbpV4fV5Oe8pDmomPJrNFJnX6ULQhQ1gTCe0M5beKgVms5SITEpt4/Y0CmLUr6iHDT0CUiyMIRWAXdIgbGh1jfaWOMksybeRevlgDsIsNBjXmYI1Sw2ZZR2Eo2u4R6zyfyjOMLwYJ3vgq9IrACv2w5nmf0+oguMWHf6iWi2hiOqhlAN1W74+3HsYQcqnuM3LGOmuCnPprV1oGBqkPXjIFGpy21gNx4vHfO1noLUyJnMnlu2L7SSuN1CdLsnjJ1hVjpJjPfqB4nn8g12x87TqM1bOm+3Q=

View File

@@ -1,17 +1,23 @@
FROM python:3.6.2
FROM python:3.6.2
RUN pip install numpy
RUN apt-get update
RUN apt-get -y install build-essential
RUN wget http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz
RUN tar zxvf ta-lib-0.4.0-src.tar.gz
RUN cd ta-lib && ./configure && make && make install
# Install TA-lib
RUN apt-get update && apt-get -y install build-essential && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \
cd ta-lib && \
./configure && make && make install && \
cd .. && rm -rf ta-lib
ENV LD_LIBRARY_PATH /usr/local/lib
RUN mkdir -p /freqtrade
# Prepare environment
RUN mkdir /freqtrade
WORKDIR /freqtrade
ADD ./requirements.txt /freqtrade/requirements.txt
RUN pip install -r requirements.txt
ADD . /freqtrade
CMD python main.py
# Install dependencies
COPY requirements.txt /freqtrade/
RUN pip install -r requirements.txt
# Install and execute
COPY . /freqtrade/
RUN pip install -e .
CMD ["freqtrade"]

5
MANIFEST.in Normal file
View File

@@ -0,0 +1,5 @@
include LICENSE
include README.md
include config.json.example
recursive-include freqtrade *.py
include freqtrade/tests/testdata/*.json

View File

@@ -1,6 +1,8 @@
# freqtrade
[![Build Status](https://travis-ci.org/gcarq/freqtrade.svg?branch=develop)](https://travis-ci.org/gcarq/freqtrade)
[![Coverage Status](https://coveralls.io/repos/github/gcarq/freqtrade/badge.svg?branch=develop)](https://coveralls.io/github/gcarq/freqtrade?branch=develop)
Simple High frequency trading bot for crypto currencies.
Currently supports trading on Bittrex exchange.
@@ -28,15 +30,14 @@ in minutes and the value is the minimum ROI in percent.
See the example below:
```
"minimal_roi": {
"2880": 0.005, # Sell after 48 hours if there is at least 0.5% profit
"1440": 0.01, # Sell after 24 hours if there is at least 1% profit
"720": 0.02, # Sell after 12 hours if there is at least 2% profit
"360": 0.02, # Sell after 6 hours if there is at least 2% profit
"0": 0.025 # Sell immediately if there is at least 2.5% profit
"50": 0.0, # Sell after 30 minutes if the profit is not negative
"40": 0.01, # Sell after 25 minutes if there is at least 1% profit
"30": 0.02, # Sell after 15 minutes if there is at least 2% profit
"0": 0.045 # Sell immediately if there is at least 4.5% profit
},
```
`stoploss` is loss in percentage that should trigger a sale.
`stoploss` is loss in percentage that should trigger a sale.
For example value `-0.10` will cause immediate sell if the
profit dips below -10% for a given trade. This parameter is optional.
@@ -45,7 +46,9 @@ Possible values are `running` or `stopped`. (default=`running`)
If the value is `stopped` the bot has to be started with `/start` first.
`ask_last_balance` sets the bidding price. Value `0.0` will use `ask` price, `1.0` will
use the `last` price and values between those interpolate between ask and last price. Using `ask` price will guarantee quick success in bid, but bot will also end up paying more then would probably have been necessary.
use the `last` price and values between those interpolate between ask and last
price. Using `ask` price will guarantee quick success in bid, but bot will also
end up paying more then would probably have been necessary.
The other values should be self-explanatory,
if not feel free to raise a github issue.
@@ -56,6 +59,11 @@ if not feel free to raise a github issue.
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
#### Install
`master` branch contains the latest stable release.
`develop` branch has often new features, but might also cause breaking changes. To use it, you are encouraged to join our [slack channel](https://join.slack.com/t/highfrequencybot/shared_invite/enQtMjQ5NTM0OTYzMzY3LWMxYzE3M2MxNDdjMGM3ZTYwNzFjMGIwZGRjNTc3ZGU3MGE3NzdmZGMwNmU3NDM5ZTNmM2Y3NjRiNzk4NmM4OGE).
```
$ cd freqtrade/
# copy example config. Dont forget to insert your api keys
@@ -63,7 +71,8 @@ $ cp config.json.example config.json
$ python -m venv .env
$ source .env/bin/activate
$ pip install -r requirements.txt
$ ./main.py
$ pip install -e .
$ ./freqtrade/main.py
```
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).
@@ -71,16 +80,62 @@ There is also an [article](https://www.sales4k.com/blockchain/high-frequency-tra
#### Execute tests
```
$ python -m unittest
$ pytest
```
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
```
#### Docker
Building the image:
```
$ cd freqtrade
$ docker build -t freqtrade .
$ docker run --rm -it freqtrade
```
For security reasons, your configuration file will not be included in the
image, you will need to bind mount it. It is also advised to bind mount
a SQLite database file (see second example) to keep it between updates.
You can run a one-off container that is immediately deleted upon exiting with
the following command (config.json must be in the current working directory):
```
$ docker run --rm -v `pwd`/config.json:/freqtrade/config.json -it freqtrade
```
To run a restartable instance in the background (feel free to place your
configuration and database files wherever it feels comfortable on your
filesystem):
```
$ cd ~/.freq
$ touch tradesv2.sqlite
$ docker run -d \
--name freqtrade \
-v ~/.freq/config.json:/freqtrade/config.json \
-v ~/.freq/tradesv2.sqlite:/freqtrade/tradesv2.sqlite \
freqtrade
```
If you are using `dry_run=True` you need to bind `tradesv2.dry_run.sqlite` instead of `tradesv2.sqlite`.
You can then use the following commands to monitor and manage your container:
```
$ docker logs freqtrade
$ docker logs -f freqtrade
$ docker restart freqtrade
$ docker stop freqtrade
$ docker start freqtrade
```
You do not need to rebuild the image for configuration
changes, it will suffice to edit `config.json` and restart the container.
#### Contributing
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:

4
bin/freqtrade Executable file
View File

@@ -0,0 +1,4 @@
#!/usr/bin/env python
from freqtrade.main import main
main()

View File

@@ -4,16 +4,17 @@
"stake_amount": 0.05,
"dry_run": false,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
"50": 0.0,
"40": 0.01,
"30": 0.02,
"0": 0.045
},
"stoploss": -0.10,
"stoploss": -0.40,
"bid_strategy": {
"ask_last_balance": 0.0
},
"bittrex": {
"enabled": true,
"exchange": {
"name": "bittrex",
"key": "key",
"secret": "secret",
"pair_whitelist": [

View File

@@ -1,170 +0,0 @@
import enum
import logging
from typing import List
from bittrex.bittrex import Bittrex
logger = logging.getLogger(__name__)
# Current selected exchange
EXCHANGE = None
_API = None
_CONF = {}
class Exchange(enum.Enum):
BITTREX = 1
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _API, EXCHANGE
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
use_bittrex = config.get('bittrex', {}).get('enabled', False)
if use_bittrex:
EXCHANGE = Exchange.BITTREX
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
else:
raise RuntimeError('No exchange specified. Aborting!')
# Check if all pairs are available
markets = get_markets()
exchange_name = EXCHANGE.name.lower()
for pair in config[exchange_name]['pair_whitelist']:
if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
def buy(pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.BITTREX:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def sell(pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: None
"""
if _CONF['dry_run']:
return 'dry_run'
elif EXCHANGE == Exchange.BITTREX:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data['result']['uuid']
def get_balance(currency: str) -> float:
"""
Get account balance.
:param currency: currency as str, format: BTC
:return: float
"""
if _CONF['dry_run']:
return 999.9
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_balance(currency)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return float(data['result']['Balance'] or 0.0)
def get_ticker(pair: str) -> dict:
"""
Get Ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: dict
"""
if EXCHANGE == Exchange.BITTREX:
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
def cancel_order(order_id: str) -> None:
"""
Cancel order for given order_id
:param order_id: id as str
:return: None
"""
if _CONF['dry_run']:
pass
elif EXCHANGE == Exchange.BITTREX:
data = _API.cancel(order_id)
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
def get_open_orders(pair: str) -> List[dict]:
"""
Get all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: list of dicts
"""
if _CONF['dry_run']:
return []
elif EXCHANGE == Exchange.BITTREX:
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [{
'id': entry['OrderUuid'],
'type': entry['OrderType'],
'opened': entry['Opened'],
'rate': entry['PricePerUnit'],
'amount': entry['Quantity'],
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(pair: str) -> str:
"""
Returns the market detail url for the given pair
:param pair: pair as str, format: BTC_ANT
:return: url as str
"""
if EXCHANGE == Exchange.BITTREX:
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
def get_markets() -> List[str]:
"""
Returns all available markets
:return: list of all available pairs
"""
if EXCHANGE == Exchange. BITTREX:
data = _API.get_markets()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]

3
freqtrade/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
__version__ = '0.13.0'
from . import main

View File

@@ -1,37 +1,21 @@
import logging
import time
from datetime import timedelta
import logging
import arrow
import requests
from pandas import DataFrame
import talib.abstract as ta
import arrow
import talib.abstract as ta
from pandas import DataFrame, to_datetime
from freqtrade import exchange
from freqtrade.exchange import Bittrex, get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
"""
Request ticker data from Bittrex for a given currency pair
"""
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
}
params = {
'marketName': pair.replace('_', '-'),
'tickInterval': 'fiveMin',
'_': minimum_date.timestamp * 1000
}
data = requests.get(url, params=params, headers=headers).json()
if not data['success']:
raise RuntimeError('BITTREX: {}'.format(data['message']))
return data
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
def parse_ticker_dataframe(ticker: list) -> DataFrame:
"""
Analyses the trend for the given pair
:param pair: pair as str in format BTC_ETH or BTC-ETH
@@ -39,19 +23,37 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame
"""
df = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
.sort_values('date')
return df[df['date'].map(arrow.get) > minimum_date]
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'})
df['date'] = to_datetime(df['date'], utc=True, infer_datetime_format=True)
df.sort_values('date', inplace=True)
return df
def populate_indicators(dataframe: DataFrame) -> DataFrame:
"""
Adds several different TA indicators to the given DataFrame
"""
dataframe['ema'] = ta.EMA(dataframe, timeperiod=33)
dataframe['sar'] = ta.SAR(dataframe, 0.02, 0.22)
dataframe['sar'] = ta.SAR(dataframe)
dataframe['adx'] = ta.ADX(dataframe)
stoch = ta.STOCHF(dataframe)
dataframe['fastd'] = stoch['fastd']
dataframe['fastk'] = stoch['fastk']
dataframe['blower'] = ta.BBANDS(dataframe, nbdevup=2, nbdevdn=2)['lowerband']
dataframe['sma'] = ta.SMA(dataframe, timeperiod=40)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['mfi'] = ta.MFI(dataframe)
dataframe['cci'] = ta.CCI(dataframe)
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['mom'] = ta.MOM(dataframe)
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)
dataframe['ao'] = awesome_oscillator(dataframe)
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
return dataframe
@@ -61,28 +63,14 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
prev_sar = dataframe['sar'].shift(1)
prev_close = dataframe['close'].shift(1)
prev_sar2 = dataframe['sar'].shift(2)
prev_close2 = dataframe['close'].shift(2)
# wait for stable turn from bearish to bullish market
dataframe.loc[
(dataframe['close'] > dataframe['sar']) &
(prev_close > prev_sar) &
(prev_close2 < prev_sar2),
'swap'
] = 1
# consider prices above ema to be in upswing
dataframe.loc[dataframe['ema'] <= dataframe['close'], 'upswing'] = 1
dataframe.loc[
(dataframe['upswing'] == 1) &
(dataframe['swap'] == 1) &
(dataframe['adx'] > 25), # adx over 25 tells there's enough momentum
dataframe.ix[
(dataframe['close'] < dataframe['sma']) &
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 25) &
(dataframe['fastd'] < 25) &
(dataframe['adx'] > 30),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
@@ -93,13 +81,19 @@ def analyze_ticker(pair: str) -> DataFrame:
add several TA indicators and buy signal to it
:return DataFrame with ticker data and indicator data
"""
minimum_date = arrow.utcnow().shift(hours=-6)
data = get_ticker(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
minimum_date = arrow.utcnow().shift(hours=-24)
data = get_ticker_history(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'])
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
return dataframe
dataframe = populate_indicators(dataframe)
dataframe = populate_buy_trend(dataframe)
return dataframe
def get_buy_signal(pair: str) -> bool:
"""
Calculates a buy signal based several technical analysis indicators
@@ -107,6 +101,10 @@ def get_buy_signal(pair: str) -> bool:
:return: True if pair is good for buying, False otherwise
"""
dataframe = analyze_ticker(pair)
if dataframe.empty:
return False
latest = dataframe.iloc[-1]
# Check if dataframe is out of date
@@ -133,19 +131,26 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
import matplotlib.pyplot as plt
# Two subplots sharing x axis
fig, (ax1, ax2) = plt.subplots(2, sharex=True)
fig, (ax1, ax2, ax3) = plt.subplots(3, sharex=True)
fig.suptitle(pair, fontsize=14, fontweight='bold')
ax1.plot(dataframe.index.values, dataframe['sar'], 'g_', label='pSAR')
ax1.plot(dataframe.index.values, dataframe['close'], label='close')
# ax1.plot(dataframe.index.values, dataframe['sell'], 'ro', label='sell')
ax1.plot(dataframe.index.values, dataframe['ema'], '--', label='EMA(20)')
ax1.plot(dataframe.index.values, dataframe['buy'], 'bo', label='buy')
ax1.plot(dataframe.index.values, dataframe['sma'], '--', label='SMA')
ax1.plot(dataframe.index.values, dataframe['tema'], ':', label='TEMA')
ax1.plot(dataframe.index.values, dataframe['blower'], '-.', label='BB low')
ax1.plot(dataframe.index.values, dataframe['buy_price'], 'bo', label='buy')
ax1.legend()
ax2.plot(dataframe.index.values, dataframe['adx'], label='ADX')
ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
ax2.legend()
ax3.plot(dataframe.index.values, dataframe['fastk'], label='k')
ax3.plot(dataframe.index.values, dataframe['fastd'], label='d')
ax3.plot(dataframe.index.values, [20] * len(dataframe.index.values))
ax3.legend()
# Fine-tune figure; make subplots close to each other and hide x ticks for
# all but bottom plot.
fig.subplots_adjust(hspace=0)
@@ -156,8 +161,9 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
if __name__ == '__main__':
# Install PYQT5==5.9 manually if you want to test this helper function
while True:
test_pair = 'BTC_ANT'
#for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''})
test_pair = 'BTC_ETH'
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)
plot_dataframe(analyze_ticker(test_pair), test_pair)
time.sleep(60)

View File

@@ -0,0 +1,119 @@
import enum
import logging
from typing import List
import arrow
from freqtrade.exchange.bittrex import Bittrex
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
# Current selected exchange
EXCHANGE: Exchange = None
_CONF: dict = {}
class Exchanges(enum.Enum):
"""
Maps supported exchange names to correspondent classes.
"""
BITTREX = Bittrex
def init(config: dict) -> None:
"""
Initializes this module with the given config,
it does basic validation whether the specified
exchange and pairs are valid.
:param config: config to use
:return: None
"""
global _CONF, EXCHANGE
_CONF.update(config)
if config['dry_run']:
logger.info('Instance is running with dry_run enabled')
exchange_config = config['exchange']
# Find matching class for the given exchange name
name = exchange_config['name']
try:
exchange_class = Exchanges[name.upper()].value
except KeyError:
raise RuntimeError('Exchange {} is not supported'.format(name))
EXCHANGE = exchange_class(exchange_config)
# Check if all pairs are available
validate_pairs(config['exchange']['pair_whitelist'])
def validate_pairs(pairs: List[str]) -> None:
"""
Checks if all given pairs are tradable on the current exchange.
Raises RuntimeError if one pair is not available.
:param pairs: list of pairs
:return: None
"""
markets = EXCHANGE.get_markets()
for pair in pairs:
if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
def buy(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
return 'dry_run'
return EXCHANGE.buy(pair, rate, amount)
def sell(pair: str, rate: float, amount: float) -> str:
if _CONF['dry_run']:
return 'dry_run'
return EXCHANGE.sell(pair, rate, amount)
def get_balance(currency: str) -> float:
if _CONF['dry_run']:
return 999.9
return EXCHANGE.get_balance(currency)
def get_balances():
return EXCHANGE.get_balances()
def get_ticker(pair: str) -> dict:
return EXCHANGE.get_ticker(pair)
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
return EXCHANGE.get_ticker_history(pair, minimum_date)
def cancel_order(order_id: str) -> None:
if _CONF['dry_run']:
return
return EXCHANGE.cancel_order(order_id)
def get_open_orders(pair: str) -> List[dict]:
if _CONF['dry_run']:
return []
return EXCHANGE.get_open_orders(pair)
def get_pair_detail_url(pair: str) -> str:
return EXCHANGE.get_pair_detail_url(pair)
def get_markets() -> List[str]:
return EXCHANGE.get_markets()

View File

@@ -0,0 +1,115 @@
import logging
from typing import List, Optional
import arrow
import requests
from bittrex.bittrex import Bittrex as _Bittrex
from freqtrade.exchange.interface import Exchange
logger = logging.getLogger(__name__)
_API: _Bittrex = None
_EXCHANGE_CONF: dict = {}
class Bittrex(Exchange):
"""
Bittrex API wrapper.
"""
# Base URL and API endpoints
BASE_URL: str = 'https://www.bittrex.com'
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
# Ticker inveral
TICKER_INTERVAL: str = 'fiveMin'
# Sleep time to avoid rate limits, used in the main loop
SLEEP_TIME: float = 25
@property
def sleep_time(self) -> float:
return self.SLEEP_TIME
def __init__(self, config: dict) -> None:
global _API, _EXCHANGE_CONF
_EXCHANGE_CONF.update(config)
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data['result']['uuid']
def sell(self, pair: str, rate: float, amount: float) -> str:
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data['result']['uuid']
def get_balance(self, currency: str) -> float:
data = _API.get_balance(currency)
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return float(data['result']['Balance'] or 0.0)
def get_balances(self):
data = _API.get_balances()
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data['result']
def get_ticker(self, pair: str) -> dict:
data = _API.get_ticker(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return {
'bid': float(data['result']['Bid']),
'ask': float(data['result']['Ask']),
'last': float(data['result']['Last']),
}
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
url = self.TICKER_METHOD
headers = {
# TODO: Set as global setting
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
}
params = {
'marketName': pair.replace('_', '-'),
'tickInterval': self.TICKER_INTERVAL,
# TODO: Timestamp has no effect on API response
'_': minimum_date.timestamp * 1000
}
data = requests.get(url, params=params, headers=headers).json()
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return data
def cancel_order(self, order_id: str) -> None:
data = _API.cancel(order_id)
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
def get_open_orders(self, pair: str) -> List[dict]:
data = _API.get_open_orders(pair.replace('_', '-'))
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return [{
'id': entry['OrderUuid'],
'type': entry['OrderType'],
'opened': entry['Opened'],
'rate': entry['PricePerUnit'],
'amount': entry['Quantity'],
'remaining': entry['QuantityRemaining'],
} for entry in data['result']]
def get_pair_detail_url(self, pair: str) -> str:
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
def get_markets(self) -> List[str]:
data = _API.get_markets()
if not data['success']:
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
return [m['MarketName'].replace('-', '_') for m in data['result']]

View File

@@ -0,0 +1,142 @@
from abc import ABC, abstractmethod
from typing import List, Optional
import arrow
class Exchange(ABC):
@property
def name(self) -> str:
"""
Name of the exchange.
:return: str representation of the class name
"""
return self.__class__.__name__
@property
@abstractmethod
def sleep_time(self) -> float:
"""
Sleep time in seconds for the main loop to avoid API rate limits.
:return: float
"""
@abstractmethod
def buy(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit buy order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to purchase
:return: order_id of the placed buy order
"""
@abstractmethod
def sell(self, pair: str, rate: float, amount: float) -> str:
"""
Places a limit sell order.
:param pair: Pair as str, format: BTC_ETH
:param rate: Rate limit for order
:param amount: The amount to sell
:return: order_id of the placed sell order
"""
@abstractmethod
def get_balance(self, currency: str) -> float:
"""
Gets account balance.
:param currency: Currency as str, format: BTC
:return: float
"""
@abstractmethod
def get_balances(self) -> List[dict]:
"""
Gets account balances across currencies
:return: List of dicts, format: [
{
'Currency': str,
'Balance': float,
'Available': float,
'Pending': float,
}
...
]
"""
@abstractmethod
def get_ticker(self, pair: str) -> dict:
"""
Gets ticker for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: dict, format: {
'bid': float,
'ask': float,
'last': float
}
"""
@abstractmethod
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict:
"""
Gets ticker history for given pair.
:param pair: Pair as str, format: BTC_ETC
:param minimum_date: Minimum date (optional)
:return: dict, format: {
'success': bool,
'message': str,
'result': [
{
'O': float, (Open)
'H': float, (High)
'L': float, (Low)
'C': float, (Close)
'V': float, (Volume)
'T': datetime, (Time)
'BV': float, (Base Volume)
},
...
]
}
"""
@abstractmethod
def cancel_order(self, order_id: str) -> None:
"""
Cancels order for given order_id.
:param order_id: ID as str
:return: None
"""
@abstractmethod
def get_open_orders(self, pair: str) -> List[dict]:
"""
Gets all open orders for given pair.
:param pair: Pair as str, format: BTC_ETC
:return: List of dicts, format: [
{
'id': str,
'type': str,
'opened': datetime,
'rate': float,
'amount': float,
'remaining': int,
},
...
]
"""
@abstractmethod
def get_pair_detail_url(self, pair: str) -> str:
"""
Returns the market detail url for the given pair.
:param pair: Pair as str, format: BTC_ETC
:return: URL as str
"""
@abstractmethod
def get_markets(self) -> List[str]:
"""
Returns all available markets.
:return: List of all available pairs
"""

View File

@@ -1,29 +1,25 @@
#!/usr/bin/env python
import copy
import json
import logging
import time
import traceback
from datetime import datetime
from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM
from jsonschema import validate
import exchange
import persistence
from persistence import Trade
from analyze import get_buy_signal
from misc import CONF_SCHEMA, get_state, State, update_state
from rpc import telegram
from freqtrade import __version__, exchange, persistence
from freqtrade.analyze import get_buy_signal
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
from freqtrade.persistence import Trade
from freqtrade.rpc import telegram
logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
__author__ = "gcarq"
__copyright__ = "gcarq 2017"
__license__ = "GPLv3"
__version__ = "0.10.0"
_CONF = {}
@@ -39,7 +35,7 @@ def _process() -> None:
if len(trades) < _CONF['max_open_trades']:
try:
# Create entity and execute trade
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
trade = create_trade(float(_CONF['stake_amount']))
if trade:
Trade.session.add(trade)
else:
@@ -94,12 +90,9 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
# Get available balance
currency = trade.pair.split('_')[1]
balance = exchange.get_balance(currency)
whitelist = _CONF[trade.exchange.name.lower()]['pair_whitelist']
profit = trade.exec_sell_order(current_rate, balance)
whitelist.append(trade.pair)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
@@ -150,6 +143,7 @@ def handle_trade(trade: Trade) -> None:
except ValueError:
logger.exception('Unable to handle open order')
def get_target_bid(ticker: Dict[str, float]) -> float:
""" Calculates bid target between current ask price and last price """
if ticker['ask'] < ticker['last']:
@@ -158,15 +152,14 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
def create_trade(stake_amount: float) -> Optional[Trade]:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param _exchange: exchange to use
"""
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
whitelist = _CONF[_exchange.name.lower()]['pair_whitelist']
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
raise ValueError(
@@ -174,11 +167,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
)
# Remove currently opened and latest pairs from whitelist
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
latest_trade = Trade.query.filter(Trade.is_open.is_(False)).order_by(Trade.id.desc()).first()
if latest_trade:
trades.append(latest_trade)
for trade in trades:
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
if trade.pair in whitelist:
whitelist.remove(trade.pair)
logger.debug('Ignoring %s in pair whitelist', trade.pair)
@@ -199,7 +188,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
# Create trade entity and return
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
_exchange.name,
exchange.EXCHANGE.name.upper(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
open_rate
@@ -211,7 +200,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
open_rate=open_rate,
open_date=datetime.utcnow(),
amount=amount,
exchange=_exchange,
exchange=exchange.EXCHANGE.name.upper(),
open_order_id=order_id,
is_open=True)
@@ -235,10 +224,27 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
else:
update_state(State.STOPPED)
# Register signal handlers
for sig in (SIGINT, SIGTERM, SIGABRT):
signal(sig, cleanup)
def cleanup(*args, **kwargs) -> None:
"""
Cleanup the application state und finish all pending tasks
:return: None
"""
telegram.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
update_state(State.STOPPED)
persistence.cleanup()
telegram.cleanup()
exit(0)
def app(config: dict) -> None:
"""
Main function which handles the application state
Main loop which handles the application state
:param config: config as dict
:return: None
"""
@@ -260,17 +266,26 @@ def app(config: dict) -> None:
elif new_state == State.RUNNING:
_process()
# We need to sleep here because otherwise we would run into bittrex rate limit
time.sleep(25)
time.sleep(exchange.EXCHANGE.sleep_time)
old_state = new_state
except RuntimeError:
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
telegram.send_msg(
'*Status:* Got RuntimeError:\n```\n{}\n```'.format(traceback.format_exc())
)
logger.exception('RuntimeError. Trader stopped!')
finally:
telegram.send_msg('*Status:* `Trader has stopped`')
if __name__ == '__main__':
def main():
"""
Loads and validates the config and starts the main loop
:return: None
"""
global _CONF
with open('config.json') as file:
_CONF = json.load(file)
validate(_CONF, CONF_SCHEMA)
app(_CONF)
if __name__ == '__main__':
main()

View File

@@ -60,7 +60,7 @@ CONF_SCHEMA = {
},
'required': ['ask_last_balance']
},
'bittrex': {'$ref': '#/definitions/exchange'},
'exchange': {'$ref': '#/definitions/exchange'},
'telegram': {
'type': 'object',
'properties': {
@@ -76,7 +76,7 @@ CONF_SCHEMA = {
'exchange': {
'type': 'object',
'properties': {
'enabled': {'type': 'boolean'},
'name': {'type': 'string'},
'key': {'type': 'string'},
'secret': {'type': 'string'},
'pair_whitelist': {
@@ -85,11 +85,11 @@ CONF_SCHEMA = {
'uniqueItems': True
}
},
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
'required': ['name', 'key', 'secret', 'pair_whitelist']
}
},
'anyOf': [
{'required': ['bittrex']}
{'required': ['exchange']}
],
'required': [
'max_open_trades',

View File

@@ -6,10 +6,7 @@ from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.types import Enum
import exchange
from freqtrade import exchange
_CONF = {}
@@ -39,11 +36,19 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
Base.metadata.create_all(engine)
def cleanup() -> None:
"""
Flushes all pending operations to disk.
:return: None
"""
Trade.session.flush()
class Trade(Base):
__tablename__ = 'trades'
id = Column(Integer, primary_key=True)
exchange = Column(Enum(exchange.Exchange), nullable=False)
exchange = Column(String, nullable=False)
pair = Column(String, nullable=False)
is_open = Column(Boolean, nullable=False, default=True)
open_rate = Column(Float, nullable=False)

View File

@@ -4,21 +4,20 @@ from typing import Callable, Any
import arrow
from sqlalchemy import and_, func, text
from telegram import ParseMode, Bot, Update
from telegram.error import NetworkError
from telegram.ext import CommandHandler, Updater
from telegram import ParseMode, Bot, Update
from misc import get_state, State, update_state
from persistence import Trade
import exchange
from freqtrade import exchange
from freqtrade.misc import get_state, State, update_state
from freqtrade.persistence import Trade
# Remove noisy log messages
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_updater = None
_updater: Updater = None
_CONF = {}
@@ -31,18 +30,23 @@ def init(config: dict) -> None:
:return: None
"""
global _updater
_updater = Updater(token=config['telegram']['token'], workers=0)
_CONF.update(config)
if not _CONF['telegram']['enabled']:
return
_updater = Updater(token=config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
CommandHandler('performance', _performance),
CommandHandler('help', _help),
]
for handle in handles:
_updater.dispatcher.add_handler(handle)
@@ -58,6 +62,14 @@ def init(config: dict) -> None:
)
def cleanup() -> None:
"""
Stops all running telegram threads.
:return: None
"""
_updater.stop()
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
@@ -191,6 +203,27 @@ def _profit(bot: Bot, update: Update) -> None:
send_msg(markdown_msg, bot=bot)
@authorized_only
def _balance(bot: Bot, update: Update) -> None:
"""
Handler for /balance
Returns current account balance per crypto
"""
output = ""
balances = exchange.get_balances()
for currency in balances:
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
continue
output += """*Currency*: {Currency}
*Available*: {Available}
*Balance*: {Balance}
*Pending*: {Pending}
""".format(**currency)
send_msg(output)
@authorized_only
def _start(bot: Bot, update: Update) -> None:
"""
@@ -255,7 +288,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
# Execute sell
profit = trade.exec_sell_order(current_rate, balance)
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
trade.exchange.name,
trade.exchange,
trade.pair.replace('_', '/'),
exchange.get_pair_detail_url(trade.pair),
trade.close_rate,
@@ -299,6 +332,28 @@ def _performance(bot: Bot, update: Update) -> None:
send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only
def _help(bot: Bot, update: Update) -> None:
"""
Handler for /help.
Show commands of the bot
:param bot: telegram bot
:param update: message update
:return: None
"""
message = """
*/start:* `Starts the trader`
*/stop:* `Stops the trader`
*/status:* `Lists all open trades`
*/profit:* `Lists cumulative profit from all finished trades`
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
*/performance:* `Show performance of each finished trade grouped by pair`
*/balance:* `Show account balance per currency`
*/help:* `This help message`
"""
send_msg(message, bot=bot)
def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
"""
Send given markdown message

View File

@@ -0,0 +1,41 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
import json
import pytest
from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
get_buy_signal
@pytest.fixture
def result():
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
data = json.load(data_file)
return parse_ticker_dataframe(data['result'])
def test_dataframe_has_correct_columns(result):
assert result.columns.tolist() == \
['close', 'high', 'low', 'open', 'date', 'volume']
def test_dataframe_has_correct_length(result):
assert len(result.index) == 5751
def test_populates_buy_trend(result):
dataframe = populate_buy_trend(populate_indicators(result))
assert 'buy' in dataframe.columns
assert 'buy_price' in dataframe.columns
def test_returns_latest_buy_signal(mocker):
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert get_buy_signal('BTC-ETH')
buydf = DataFrame([{'buy': 0, 'date': datetime.today()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert not get_buy_signal('BTC-ETH')

View File

@@ -0,0 +1,82 @@
# pragma pylint: disable=missing-docstring
import json
import logging
import os
import pytest
import arrow
from pandas import DataFrame
from freqtrade.analyze import analyze_ticker
from freqtrade.main import should_sell
from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
def format_results(results):
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean() * 5
)
def print_pair_results(pair, results):
print('For currency {}:'.format(pair))
print(format_results(results[results.currency == pair]))
@pytest.fixture
def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture
def conf():
return {
"minimal_roi": {
"50": 0.0,
"40": 0.01,
"30": 0.02,
"0": 0.045
},
"stoploss": -0.40
}
def backtest(conf, pairs, mocker):
trades = []
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
for pair in pairs:
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
data = json.load(data_file)
mocked_history.return_value = data
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
# for each buy point
for row in ticker[ticker.buy == 1].itertuples(index=True):
trade = Trade(open_rate=row.close, open_date=row.date, amount=1)
# calculate win/lose forwards from buy point
for row2 in ticker[row.Index:].itertuples(index=True):
if should_sell(trade, row2.close, row2.date):
current_profit = (row2.close - trade.open_rate) / trade.open_rate
trades.append((pair, current_profit, row2.Index - row.Index))
break
labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels)
return results
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(conf, pairs, mocker, report=True):
results = backtest(conf, pairs, mocker)
print('====================== BACKTESTING REPORT ================================')
[print_pair_results(pair, results) for pair in pairs]
print('TOTAL OVER ALL TRADES:')
print(format_results(results))

View File

@@ -0,0 +1,154 @@
# pragma pylint: disable=missing-docstring
import logging
import os
from functools import reduce
from math import exp
from operator import itemgetter
import pytest
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
from pandas import DataFrame
from freqtrade.tests.test_backtesting import backtest, format_results
from freqtrade.vendor.qtpylib.indicators import crossed_above
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
TARGET_TRADES = 1200
@pytest.fixture
def pairs():
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
@pytest.fixture
def conf():
return {
"minimal_roi": {
"40": 0.0,
"30": 0.01,
"20": 0.02,
"0": 0.04
},
"stoploss": -0.05
}
def buy_strategy_generator(params):
print(params)
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if params['uptrend_long_ema']['enabled']:
conditions.append(dataframe['ema50'] > dataframe['ema100'])
if params['mfi']['enabled']:
conditions.append(dataframe['mfi'] < params['mfi']['value'])
if params['fastd']['enabled']:
conditions.append(dataframe['fastd'] < params['fastd']['value'])
if params['adx']['enabled']:
conditions.append(dataframe['adx'] > params['adx']['value'])
if params['cci']['enabled']:
conditions.append(dataframe['cci'] < params['cci']['value'])
if params['rsi']['enabled']:
conditions.append(dataframe['rsi'] < params['rsi']['value'])
if params['over_sar']['enabled']:
conditions.append(dataframe['close'] > dataframe['sar'])
if params['uptrend_sma']['enabled']:
prevsma = dataframe['sma'].shift(1)
conditions.append(dataframe['sma'] > prevsma)
prev_fastd = dataframe['fastd'].shift(1)
# TRIGGERS
triggers = {
'lower_bb': dataframe['tema'] <= dataframe['blower'],
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
}
conditions.append(triggers.get(params['trigger']['type']))
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
return dataframe
return populate_buy_trend
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_hyperopt(conf, pairs, mocker):
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
def optimizer(params):
mocked_buy_trend.side_effect = buy_strategy_generator(params)
results = backtest(conf, pairs, mocker)
result = format_results(results)
print(result)
total_profit = results.profit.sum() * 1000
trade_count = len(results.index)
trade_loss = 1 - 0.8 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5)
profit_loss = exp(-total_profit**3 / 10**11)
return {
'loss': trade_loss + profit_loss,
'status': STATUS_OK,
'result': result
}
space = {
'mfi': hp.choice('mfi', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
]),
'fastd': hp.choice('fastd', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
]),
'adx': hp.choice('adx', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
]),
'cci': hp.choice('cci', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
]),
'rsi': hp.choice('rsi', [
{'enabled': False},
{'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)}
]),
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
{'enabled': False},
{'enabled': True}
]),
'over_sar': hp.choice('over_sar', [
{'enabled': False},
{'enabled': True}
]),
'uptrend_sma': hp.choice('uptrend_sma', [
{'enabled': False},
{'enabled': True}
]),
'trigger': hp.choice('trigger', [
{'type': 'lower_bb'},
{'type': 'faststoch10'},
{'type': 'ao_cross_zero'},
{'type': 'ema5_cross_ema10'},
{'type': 'macd_cross_signal'},
]),
}
trials = Trials()
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40, trials=trials)
print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================')
print('Best parameters {}'.format(best))
newlist = sorted(trials.results, key=itemgetter('loss'))
print('Result: {}'.format(newlist[0]['result']))

View File

@@ -0,0 +1,132 @@
# pragma pylint: disable=missing-docstring
import copy
from unittest.mock import MagicMock, call
import pytest
from jsonschema import validate
from freqtrade.exchange import Exchanges
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
get_target_bid
from freqtrade.misc import CONF_SCHEMA
from freqtrade.persistence import Trade
@pytest.fixture
def conf():
configuration = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH",
"BTC_TKN",
"BTC_TRST",
"BTC_SWT",
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "chat_id"
}
}
validate(configuration, CONF_SCHEMA)
return configuration
def test_create_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
# Save state of current whitelist
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
init(conf, 'sqlite://')
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
trade = create_trade(15.0)
Trade.session.add(trade)
Trade.session.flush()
assert trade is not None
assert trade.open_rate == 0.072661
assert trade.pair == pair
assert trade.exchange == Exchanges.BITTREX.name
assert trade.amount == 206.43811673387373
assert trade.stake_amount == 15.0
assert trade.is_open
assert trade.open_date is not None
assert whitelist == conf['exchange']['pair_whitelist']
buy_signal.assert_has_calls(
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
)
def test_handle_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.17256061,
'ask': 0.172661,
'last': 0.17256061
}),
buy=MagicMock(return_value='mocked_order_id'))
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade
handle_trade(trade)
assert trade.close_rate == 0.17256061
assert trade.close_profit == 137.4872490056564
assert trade.close_date is not None
assert trade.open_order_id == 'dry_run'
def test_close_trade(conf, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
assert trade
# Simulate that there is no open order
trade.open_order_id = None
closed = close_trade_if_fulfilled(trade)
assert closed
assert not trade.is_open
def test_balance_fully_ask_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 20
def test_balance_fully_last_side(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 20, 'last': 10}) == 10
def test_balance_when_last_bigger_than_ask(mocker):
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
assert get_target_bid({'ask': 5, 'last': 10}) == 5

View File

@@ -0,0 +1,21 @@
# pragma pylint: disable=missing-docstring
from freqtrade.exchange import Exchanges
from freqtrade.persistence import Trade
def test_exec_sell_order(mocker):
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
open_rate=0.50,
amount=10.00,
exchange=Exchanges.BITTREX,
open_order_id='mocked'
)
profit = trade.exec_sell_order(1.00, 10.00)
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
assert profit == 100.0
assert trade.close_rate == 1.0
assert trade.close_profit == profit
assert trade.close_date is not None

View File

@@ -0,0 +1,223 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
from unittest.mock import MagicMock
import pytest
from jsonschema import validate
from telegram import Bot, Update, Message, Chat
from freqtrade.main import init, create_trade
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
from freqtrade.persistence import Trade
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop, _balance
@pytest.fixture
def conf():
configuration = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": "bittrex",
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
validate(configuration, CONF_SCHEMA)
return configuration
@pytest.fixture
def update():
_update = Update(0)
_update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
return _update
class MagicBot(MagicMock, Bot):
pass
def test_status_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
_status(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
def test_profit_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0)
assert trade
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_profit(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
def test_forcesell_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0)
assert trade
Trade.session.add(trade)
Trade.session.flush()
update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
assert '0.072561' in msg_mock.call_args_list[-1][0][0]
def test_performance_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
validate_pairs=MagicMock(),
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id'))
init(conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0)
assert trade
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_performance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 2
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0]
def test_start_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
init(conf, 'sqlite://')
update_state(State.STOPPED)
assert get_state() == State.STOPPED
_start(bot=MagicBot(), update=update)
assert get_state() == State.RUNNING
assert msg_mock.call_count == 0
def test_stop_handle(conf, update, mocker):
mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
init(conf, 'sqlite://')
update_state(State.RUNNING)
assert get_state() == State.RUNNING
_stop(bot=MagicBot(), update=update)
assert get_state() == State.STOPPED
assert msg_mock.call_count == 1
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
def test_balance_handle(conf, update, mocker):
mock_balance = [{
'Currency': 'BTC',
'Balance': 10.0,
'Available': 12.0,
'Pending': 0.0,
'CryptoAddress': 'XXXX'}]
mocker.patch.dict('freqtrade.main._CONF', conf)
msg_mock = MagicMock()
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
mocker.patch.multiple('freqtrade.main.exchange',
get_balances=MagicMock(return_value=mock_balance))
_balance(bot=MagicBot(), update=update)
assert msg_mock.call_count == 1
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
assert 'Balance' in msg_mock.call_args_list[0][0][0]

View File

@@ -0,0 +1,18 @@
#!/usr/bin/env python3
"""This script generate json data from bittrex"""
from urllib.request import urlopen
CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"]
OUTPUT_DIR = 'freqtrade/tests/testdata/'
for cur in CURRENCIES:
url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-'
url = url1+cur+'&tickInterval=fiveMin'
x = urlopen(url)
json_data = x.read()
json_str = str(json_data, 'utf-8')
output = OUTPUT_DIR + 'btc-'+cur+'.json'
with open(output, 'w') as file:
file.write(json_str)

0
freqtrade/vendor/__init__.py vendored Normal file
View File

0
freqtrade/vendor/qtpylib/__init__.py vendored Normal file
View File

619
freqtrade/vendor/qtpylib/indicators.py vendored Normal file
View File

@@ -0,0 +1,619 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# QTPyLib: Quantitative Trading Python Library
# https://github.com/ranaroussi/qtpylib
#
# Copyright 2016 Ran Aroussi
#
# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.gnu.org/licenses/lgpl-3.0.en.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import numpy as np
import pandas as pd
import warnings
import sys
from datetime import datetime, timedelta
from pandas.core.base import PandasObject
# =============================================
# check min, python version
if sys.version_info < (3, 4):
raise SystemError("QTPyLib requires Python version >= 3.4")
# =============================================
warnings.simplefilter(action="ignore", category=RuntimeWarning)
# =============================================
def numpy_rolling_window(data, window):
shape = data.shape[:-1] + (data.shape[-1] - window + 1, window)
strides = data.strides + (data.strides[-1],)
return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)
def numpy_rolling_series(func):
def func_wrapper(data, window, as_source=False):
series = data.values if isinstance(data, pd.Series) else data
new_series = np.empty(len(series)) * np.nan
calculated = func(series, window)
new_series[-len(calculated):] = calculated
if as_source and isinstance(data, pd.Series):
return pd.Series(index=data.index, data=new_series)
return new_series
return func_wrapper
@numpy_rolling_series
def numpy_rolling_mean(data, window, as_source=False):
return np.mean(numpy_rolling_window(data, window), -1)
@numpy_rolling_series
def numpy_rolling_std(data, window, as_source=False):
return np.std(numpy_rolling_window(data, window), -1)
# ---------------------------------------------
def session(df, start='17:00', end='16:00'):
""" remove previous globex day from df """
if len(df) == 0:
return df
# get start/end/now as decimals
int_start = list(map(int, start.split(':')))
int_start = (int_start[0] + int_start[1] - 1 / 100) - 0.0001
int_end = list(map(int, end.split(':')))
int_end = int_end[0] + int_end[1] / 100
int_now = (df[-1:].index.hour[0] + (df[:1].index.minute[0]) / 100)
# same-dat session?
is_same_day = int_end > int_start
# set pointers
curr = prev = df[-1:].index[0].strftime('%Y-%m-%d')
# globex/forex session
if is_same_day == False:
prev = (datetime.strptime(curr, '%Y-%m-%d') -
timedelta(1)).strftime('%Y-%m-%d')
# slice
if int_now >= int_start:
df = df[df.index >= curr + ' ' + start]
else:
df = df[df.index >= prev + ' ' + start]
return df.copy()
# ---------------------------------------------
def heikinashi(bars):
bars = bars.copy()
bars['ha_close'] = (bars['open'] + bars['high'] +
bars['low'] + bars['close']) / 4
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
bars.loc[1:, 'ha_open'] = (
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
return pd.DataFrame(index=bars.index, data={'open': bars['ha_open'],
'high': bars['ha_high'], 'low': bars['ha_low'], 'close': bars['ha_close']})
# ---------------------------------------------
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2, rsi_signal_len=7, bollinger_std=1.6185):
rsi_series = rsi(series, rsi_len)
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
signal = sma(rsi_series, rsi_signal_len)
rsi_series = sma(rsi_series, rsi_smoothing)
return pd.DataFrame(index=series.index, data={
"rsi": rsi_series,
"signal": signal,
"bbupper": bb_series['upper'],
"bblower": bb_series['lower'],
"bbmid": bb_series['mid']
})
# ---------------------------------------------
def awesome_oscillator(df, weighted=False, fast=5, slow=34):
midprice = (df['high'] + df['low']) / 2
if weighted:
ao = (midprice.ewm(fast).mean() - midprice.ewm(slow).mean()).values
else:
ao = numpy_rolling_mean(midprice, fast) - \
numpy_rolling_mean(midprice, slow)
return pd.Series(index=df.index, data=ao)
# ---------------------------------------------
def nans(len=1):
mtx = np.empty(len)
mtx[:] = np.nan
return mtx
# ---------------------------------------------
def typical_price(bars):
res = (bars['high'] + bars['low'] + bars['close']) / 3.
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def mid_price(bars):
res = (bars['high'] + bars['low']) / 2.
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def ibs(bars):
""" Internal bar strength """
res = np.round((bars['close'] - bars['low']) /
(bars['high'] - bars['low']), 2)
return pd.Series(index=bars.index, data=res)
# ---------------------------------------------
def true_range(bars):
return pd.DataFrame({
"hl": bars['high'] - bars['low'],
"hc": abs(bars['high'] - bars['close'].shift(1)),
"lc": abs(bars['low'] - bars['close'].shift(1))
}).max(axis=1)
# ---------------------------------------------
def atr(bars, window=14, exp=False):
tr = true_range(bars)
if exp:
res = rolling_weighted_mean(tr, window)
else:
res = rolling_mean(tr, window)
res = pd.Series(res)
return (res.shift(1) * (window - 1) + res) / window
# ---------------------------------------------
def crossed(series1, series2, direction=None):
if isinstance(series1, np.ndarray):
series1 = pd.Series(series1)
if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
series2 = pd.Series(index=series1.index, data=series2)
if direction is None or direction == "above":
above = pd.Series((series1 > series2) & (
series1.shift(1) <= series2.shift(1)))
if direction is None or direction == "below":
below = pd.Series((series1 < series2) & (
series1.shift(1) >= series2.shift(1)))
if direction is None:
return above or below
return above if direction is "above" else below
def crossed_above(series1, series2):
return crossed(series1, series2, "above")
def crossed_below(series1, series2):
return crossed(series1, series2, "below")
# ---------------------------------------------
def rolling_std(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
if min_periods == window:
return numpy_rolling_std(series, window, True)
else:
try:
return series.rolling(window=window, min_periods=min_periods).std()
except:
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
except:
return pd.rolling_std(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
if min_periods == window:
return numpy_rolling_mean(series, window, True)
else:
try:
return series.rolling(window=window, min_periods=min_periods).mean()
except:
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
except:
return pd.rolling_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_min(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
try:
return series.rolling(window=window, min_periods=min_periods).min()
except:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except:
return pd.rolling_min(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_max(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
try:
return series.rolling(window=window, min_periods=min_periods).min()
except:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except:
return pd.rolling_min(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def rolling_weighted_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods
try:
return series.ewm(span=window, min_periods=min_periods).mean()
except:
return pd.ewma(series, span=window, min_periods=min_periods)
# ---------------------------------------------
def hull_moving_average(series, window=200):
wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
rolling_weighted_mean(series, window=window)
return rolling_weighted_mean(wma, window=np.sqrt(window))
# ---------------------------------------------
def sma(series, window=200, min_periods=None):
return rolling_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def wma(series, window=200, min_periods=None):
return rolling_weighted_mean(series, window=window, min_periods=min_periods)
# ---------------------------------------------
def hma(series, window=200):
return hull_moving_average(series, window=window)
# ---------------------------------------------
def vwap(bars):
"""
calculate vwap of entire time series
(input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
"""
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
volume = bars['volume'].values
return pd.Series(index=bars.index,
data=np.cumsum(volume * typical) / np.cumsum(volume))
# ---------------------------------------------
def rolling_vwap(bars, window=200, min_periods=None):
"""
calculate vwap using moving window
(input can be pandas series or numpy array)
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
"""
min_periods = window if min_periods is None else min_periods
typical = ((bars['high'] + bars['low'] + bars['close']) / 3)
volume = bars['volume']
left = (volume * typical).rolling(window=window,
min_periods=min_periods).sum()
right = volume.rolling(window=window, min_periods=min_periods).sum()
return pd.Series(index=bars.index, data=(left / right))
# ---------------------------------------------
def rsi(series, window=14):
"""
compute the n period relative strength indicator
"""
# 100-(100/relative_strength)
deltas = np.diff(series)
seed = deltas[:window + 1]
# default values
ups = seed[seed > 0].sum() / window
downs = -seed[seed < 0].sum() / window
rsival = np.zeros_like(series)
rsival[:window] = 100. - 100. / (1. + ups / downs)
# period values
for i in range(window, len(series)):
delta = deltas[i - 1]
if delta > 0:
upval = delta
downval = 0
else:
upval = 0
downval = -delta
ups = (ups * (window - 1) + upval) / window
downs = (downs * (window - 1.) + downval) / window
rsival[i] = 100. - 100. / (1. + ups / downs)
# return rsival
return pd.Series(index=series.index, data=rsival)
# ---------------------------------------------
def macd(series, fast=3, slow=10, smooth=16):
"""
compute the MACD (Moving Average Convergence/Divergence)
using a fast and slow exponential moving avg'
return value is emaslow, emafast, macd which are len(x) arrays
"""
macd = rolling_weighted_mean(series, window=fast) - \
rolling_weighted_mean(series, window=slow)
signal = rolling_weighted_mean(macd, window=smooth)
histogram = macd - signal
# return macd, signal, histogram
return pd.DataFrame(index=series.index, data={
'macd': macd.values,
'signal': signal.values,
'histogram': histogram.values
})
# ---------------------------------------------
def bollinger_bands(series, window=20, stds=2):
sma = rolling_mean(series, window=window)
std = rolling_std(series, window=window)
upper = sma + std * stds
lower = sma - std * stds
return pd.DataFrame(index=series.index, data={
'upper': upper,
'mid': sma,
'lower': lower
})
# ---------------------------------------------
def weighted_bollinger_bands(series, window=20, stds=2):
ema = rolling_weighted_mean(series, window=window)
std = rolling_std(series, window=window)
upper = ema + std * stds
lower = ema - std * stds
return pd.DataFrame(index=series.index, data={
'upper': upper.values,
'mid': ema.values,
'lower': lower.values
})
# ---------------------------------------------
def returns(series):
try:
res = (series / series.shift(1) -
1).replace([np.inf, -np.inf], float('NaN'))
except:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def log_returns(series):
try:
res = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN'))
except:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def implied_volatility(series, window=252):
try:
logret = np.log(series / series.shift(1)
).replace([np.inf, -np.inf], float('NaN'))
res = numpy_rolling_std(logret, window) * np.sqrt(window)
except:
res = nans(len(series))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def keltner_channel(bars, window=14, atrs=2):
typical_mean = rolling_mean(typical_price(bars), window)
atrval = atr(bars, window) * atrs
upper = typical_mean + atrval
lower = typical_mean - atrval
return pd.DataFrame(index=bars.index, data={
'upper': upper.values,
'mid': typical_mean.values,
'lower': lower.values
})
# ---------------------------------------------
def roc(series, window=14):
"""
compute rate of change
"""
res = (series - series.shift(window)) / series.shift(window)
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def cci(series, window=14):
"""
compute commodity channel index
"""
price = typical_price(series)
typical_mean = rolling_mean(price, window)
res = (price - typical_mean) / (.015 * np.std(typical_mean))
return pd.Series(index=series.index, data=res)
# ---------------------------------------------
def stoch(df, window=14, d=3, k=3, fast=False):
"""
compute the n period relative strength indicator
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
"""
highs_ma = pd.concat([df['high'].shift(i)
for i in np.arange(window)], 1).apply(list, 1)
highs_ma = highs_ma.T.max().T
lows_ma = pd.concat([df['low'].shift(i)
for i in np.arange(window)], 1).apply(list, 1)
lows_ma = lows_ma.T.min().T
fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
fast_d = numpy_rolling_mean(fast_k, d)
if fast:
data = {
'k': fast_k,
'd': fast_d
}
else:
slow_k = numpy_rolling_mean(fast_k, k)
slow_d = numpy_rolling_mean(slow_k, d)
data = {
'k': slow_k,
'd': slow_d
}
return pd.DataFrame(index=df.index, data=data)
# ---------------------------------------------
def zscore(bars, window=20, stds=1, col='close'):
""" get zscore of price """
std = numpy_rolling_std(bars[col], window)
mean = numpy_rolling_mean(bars[col], window)
return (bars[col] - mean) / (std * stds)
# ---------------------------------------------
def pvt(bars):
""" Price Volume Trend """
pvt = ((bars['close'] - bars['close'].shift(1)) /
bars['close'].shift(1)) * bars['volume']
return pvt.cumsum()
# =============================================
PandasObject.session = session
PandasObject.atr = atr
PandasObject.bollinger_bands = bollinger_bands
PandasObject.cci = cci
PandasObject.crossed = crossed
PandasObject.crossed_above = crossed_above
PandasObject.crossed_below = crossed_below
PandasObject.heikinashi = heikinashi
PandasObject.hull_moving_average = hull_moving_average
PandasObject.ibs = ibs
PandasObject.implied_volatility = implied_volatility
PandasObject.keltner_channel = keltner_channel
PandasObject.log_returns = log_returns
PandasObject.macd = macd
PandasObject.returns = returns
PandasObject.roc = roc
PandasObject.rolling_max = rolling_max
PandasObject.rolling_min = rolling_min
PandasObject.rolling_mean = rolling_mean
PandasObject.rolling_std = rolling_std
PandasObject.rsi = rsi
PandasObject.stoch = stoch
PandasObject.zscore = zscore
PandasObject.pvt = pvt
PandasObject.tdi = tdi
PandasObject.true_range = true_range
PandasObject.mid_price = mid_price
PandasObject.typical_price = typical_price
PandasObject.vwap = vwap
PandasObject.rolling_vwap = rolling_vwap
PandasObject.weighted_bollinger_bands = weighted_bollinger_bands
PandasObject.rolling_weighted_mean = rolling_weighted_mean
PandasObject.sma = sma
PandasObject.wma = wma
PandasObject.hma = hma

View File

@@ -1,14 +1,23 @@
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
SQLAlchemy==1.1.13
python-telegram-bot==7.0.1
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
SQLAlchemy==1.1.14
python-telegram-bot==8.1.1
arrow==0.10.0
requests==2.18.4
urllib3==1.22
wrapt==1.10.11
pandas==0.20.3
matplotlib==2.0.2
scikit-learn==0.19.0
scipy==0.19.1
jsonschema==2.6.0
numpy==1.13.3
TA-Lib==0.4.10
pytest==3.2.3
pytest-mock==1.6.3
pytest-cov==2.5.1
hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11
# Required for plotting data
#matplotlib==2.1.0
#PYQT5==5.9

41
setup.py Normal file
View File

@@ -0,0 +1,41 @@
from setuptools import setup
from freqtrade import __version__
setup(name='freqtrade',
version=__version__,
description='Simple High Frequency Trading Bot for crypto currencies',
url='https://github.com/gcarq/freqtrade',
author='gcarq and contributors',
author_email='michael.egger@tsn.at',
license='GPLv3',
packages=['freqtrade'],
scripts=['bin/freqtrade'],
setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'],
install_requires=[
'python-bittrex==0.1.3',
'SQLAlchemy==1.1.13',
'python-telegram-bot==8.1.1',
'arrow==0.10.0',
'requests==2.18.4',
'urllib3==1.22',
'wrapt==1.10.11',
'pandas==0.20.3',
'scikit-learn==0.19.0',
'scipy==0.19.1',
'jsonschema==2.6.0',
'TA-Lib==0.4.10',
],
dependency_links=[
"git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex-0.1.3"
],
include_package_data=True,
zip_safe=False,
classifiers=[
'Programming Language :: Python :: 3.6',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
'Topic :: Office/Business :: Financial :: Investment',
'Intended Audience :: Science/Research',
])

View File

@@ -1,49 +0,0 @@
# pragma pylint: disable=missing-docstring
import unittest
from unittest.mock import patch
from pandas import DataFrame
import arrow
from analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, analyze_ticker, get_buy_signal
RESULT_BITTREX = {
'success': True,
'message': '',
'result': [
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 22.17210568, 'T': '2017-08-30T10:40:00', 'BV': 0.01448082},
{'O': 0.00066194, 'H': 0.00066195, 'L': 0.00066194, 'C': 0.00066195, 'V': 33.4727437, 'T': '2017-08-30T10:34:00', 'BV': 0.02215696},
{'O': 0.00065311, 'H': 0.00065311, 'L': 0.00065311, 'C': 0.00065311, 'V': 53.85127609, 'T': '2017-08-30T10:37:00', 'BV': 0.0351708},
{'O': 0.00066194, 'H': 0.00066194, 'L': 0.00065311, 'C': 0.00065311, 'V': 46.29210665, 'T': '2017-08-30T10:42:00', 'BV': 0.03063118},
]
}
class TestAnalyze(unittest.TestCase):
def setUp(self):
self.result = parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
def test_1_dataframe_has_correct_columns(self):
self.assertEqual(self.result.columns.tolist(),
['close', 'high', 'low', 'open', 'date', 'volume'])
def test_2_orders_by_date(self):
self.assertEqual(self.result['date'].tolist(),
['2017-08-30T10:34:00',
'2017-08-30T10:37:00',
'2017-08-30T10:40:00',
'2017-08-30T10:42:00'])
def test_3_populates_buy_trend(self):
dataframe = populate_buy_trend(populate_indicators(self.result))
self.assertTrue('buy' in dataframe.columns)
self.assertTrue('buy_price' in dataframe.columns)
def test_4_returns_latest_buy_signal(self):
buydf = DataFrame([{'buy': 1, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), True)
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
with patch('analyze.analyze_ticker', return_value=buydf):
self.assertEqual(get_buy_signal('BTC-ETH'), False)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,71 +0,0 @@
# pragma pylint: disable=missing-docstring
import unittest
from unittest.mock import patch
import os
import json
import logging
import arrow
from pandas import DataFrame
from analyze import analyze_ticker
from persistence import Trade
from main import should_sell
def print_results(results):
print('Made {} buys. Average profit {:.1f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
len(results.index),
results.profit.mean() * 100.0,
results.profit.sum(),
results.duration.mean()*5
))
class TestMain(unittest.TestCase):
pairs = ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay', 'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
conf = {
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"stoploss": -0.10
}
@classmethod
def setUpClass(cls):
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
@unittest.skipIf(not os.environ.get('BACKTEST', False), "slow, should be run manually")
def test_backtest(self):
trades = []
with patch.dict('main._CONF', self.conf):
for pair in self.pairs:
with open('test/testdata/'+pair+'.json') as data_file:
data = json.load(data_file)
with patch('analyze.get_ticker', return_value=data):
with patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00')):
ticker = analyze_ticker(pair)
# for each buy point
for index, row in ticker[ticker.buy == 1].iterrows():
trade = Trade(
open_rate=row['close'],
open_date=arrow.get(row['date']).datetime,
amount=1,
)
# calculate win/lose forwards from buy point
for index2, row2 in ticker[index:].iterrows():
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
trades.append((pair, current_profit, index2 - index))
break
labels = ['currency', 'profit', 'duration']
results = DataFrame.from_records(trades, columns=labels)
print('====================== BACKTESTING REPORT ================================')
for pair in self.pairs:
print('For currency {}:'.format(pair))
print_results(results[results.currency == pair])
print('TOTAL OVER ALL TRADES:')
print_results(results)

View File

@@ -1,114 +0,0 @@
import unittest
from unittest.mock import patch, MagicMock
from jsonschema import validate
import exchange
from main import create_trade, handle_trade, close_trade_if_fulfilled, init, get_target_bid
from misc import CONF_SCHEMA
from persistence import Trade
class TestMain(unittest.TestCase):
conf = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"bittrex": {
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "chat_id"
}
}
def test_1_create_trade(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True) as buy_signal:
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
trade = create_trade(15.0, exchange.Exchange.BITTREX)
Trade.session.add(trade)
Trade.session.flush()
self.assertIsNotNone(trade)
self.assertEqual(trade.open_rate, 0.072661)
self.assertEqual(trade.pair, 'BTC_ETH')
self.assertEqual(trade.exchange, exchange.Exchange.BITTREX)
self.assertEqual(trade.amount, 206.43811673387373)
self.assertEqual(trade.stake_amount, 15.0)
self.assertEqual(trade.is_open, True)
self.assertIsNotNone(trade.open_date)
buy_signal.assert_called_once_with('BTC_ETH')
def test_2_handle_trade(self):
with patch.dict('main._CONF', self.conf):
with patch.multiple('main.telegram', init=MagicMock(), send_msg=MagicMock()):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.17256061,
'ask': 0.172661,
'last': 0.17256061
}),
buy=MagicMock(return_value='mocked_order_id')):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
handle_trade(trade)
self.assertEqual(trade.close_rate, 0.17256061)
self.assertEqual(trade.close_profit, 137.4872490056564)
self.assertIsNotNone(trade.close_date)
self.assertEqual(trade.open_order_id, 'dry_run')
def test_3_close_trade(self):
with patch.dict('main._CONF', self.conf):
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
self.assertTrue(trade)
# Simulate that there is no open order
trade.open_order_id = None
closed = close_trade_if_fulfilled(trade)
self.assertTrue(closed)
self.assertEqual(trade.is_open, False)
def test_balance_fully_ask_side(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}}):
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 20)
def test_balance_fully_last_side(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
self.assertEqual(get_target_bid({'ask': 20, 'last': 10}), 10)
def test_balance_when_last_bigger_than_ask(self):
with patch.dict('main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}}):
self.assertEqual(get_target_bid({'ask': 5, 'last': 10}), 5)
@classmethod
def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,28 +0,0 @@
import unittest
from unittest.mock import patch
from exchange import Exchange
from persistence import Trade
class TestTrade(unittest.TestCase):
def test_1_exec_sell_order(self):
with patch('main.exchange.sell', side_effect='mocked_order_id') as api_mock:
trade = Trade(
pair='BTC_ETH',
stake_amount=1.00,
open_rate=0.50,
amount=10.00,
exchange=Exchange.BITTREX,
open_order_id='mocked'
)
profit = trade.exec_sell_order(1.00, 10.00)
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
self.assertEqual(profit, 100.0)
self.assertEqual(trade.close_rate, 1.0)
self.assertEqual(trade.close_profit, profit)
self.assertIsNotNone(trade.close_date)
if __name__ == '__main__':
unittest.main()

View File

@@ -1,195 +0,0 @@
import unittest
from unittest.mock import patch, MagicMock
from datetime import datetime
from jsonschema import validate
from telegram import Bot, Update, Message, Chat
import exchange
from main import init, create_trade
from misc import CONF_SCHEMA, update_state, State, get_state
from persistence import Trade
from rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop
class MagicBot(MagicMock, Bot):
pass
class TestTelegram(unittest.TestCase):
conf = {
"max_open_trades": 3,
"stake_currency": "BTC",
"stake_amount": 0.05,
"dry_run": True,
"minimal_roi": {
"2880": 0.005,
"720": 0.01,
"0": 0.02
},
"bid_strategy": {
"ask_last_balance": 0.0
},
"bittrex": {
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": [
"BTC_ETH"
]
},
"telegram": {
"enabled": True,
"token": "token",
"chat_id": "0"
},
"initial_state": "running"
}
def test_1_status_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
Trade.session.add(trade)
Trade.session.flush()
_status(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('[BTC_ETH]', msg_mock.call_args_list[-1][0][0])
def test_2_profit_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_profit(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('(100.00%)', msg_mock.call_args_list[-1][0][0])
def test_3_forcesell_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
Trade.session.add(trade)
Trade.session.flush()
self.update.message.text = '/forcesell 1'
_forcesell(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('Selling [BTC/ETH]', msg_mock.call_args_list[-1][0][0])
self.assertIn('0.072561', msg_mock.call_args_list[-1][0][0])
def test_4_performance_handle(self):
with patch.dict('main._CONF', self.conf):
with patch('main.get_buy_signal', side_effect=lambda _: True):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
with patch.multiple('main.exchange',
get_ticker=MagicMock(return_value={
'bid': 0.07256061,
'ask': 0.072661,
'last': 0.07256061
}),
buy=MagicMock(return_value='mocked_order_id')):
init(self.conf, 'sqlite://')
# Create some test data
trade = create_trade(15.0, exchange.Exchange.BITTREX)
self.assertTrue(trade)
trade.close_rate = 0.07256061
trade.close_profit = 100.00
trade.close_date = datetime.utcnow()
trade.open_order_id = None
trade.is_open = False
Trade.session.add(trade)
Trade.session.flush()
_performance(bot=MagicBot(), update=self.update)
self.assertEqual(msg_mock.call_count, 2)
self.assertIn('Performance', msg_mock.call_args_list[-1][0][0])
self.assertIn('BTC_ETH 100.00%', msg_mock.call_args_list[-1][0][0])
def test_5_start_handle(self):
with patch.dict('main._CONF', self.conf):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
init(self.conf, 'sqlite://')
update_state(State.STOPPED)
self.assertEqual(get_state(), State.STOPPED)
_start(bot=MagicBot(), update=self.update)
self.assertEqual(get_state(), State.RUNNING)
self.assertEqual(msg_mock.call_count, 0)
def test_6_stop_handle(self):
with patch.dict('main._CONF', self.conf):
msg_mock = MagicMock()
with patch.multiple('main.telegram', _CONF=self.conf, init=MagicMock(), send_msg=msg_mock):
init(self.conf, 'sqlite://')
update_state(State.RUNNING)
self.assertEqual(get_state(), State.RUNNING)
_stop(bot=MagicBot(), update=self.update)
self.assertEqual(get_state(), State.STOPPED)
self.assertEqual(msg_mock.call_count, 1)
self.assertIn('Stopping trader', msg_mock.call_args_list[0][0][0])
def setUp(self):
self.update = Update(0)
self.update.message = Message(0, 0, datetime.utcnow(), Chat(0, 0))
@classmethod
def setUpClass(cls):
validate(cls.conf, CONF_SCHEMA)
if __name__ == '__main__':
unittest.main()