Compare commits

..

88 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
26 changed files with 1102 additions and 123 deletions

6
.dockerignore Normal file
View File

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

View File

@@ -1,20 +1,23 @@
FROM python:3.6.2
RUN apt-get update
RUN apt-get -y install build-essential
FROM python:3.6.2
# Install TA-lib
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
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
# Prepare environment
RUN mkdir /freqtrade
COPY . /freqtrade/
WORKDIR /freqtrade
# Install dependencies and execute
# Install dependencies
COPY requirements.txt /freqtrade/
RUN pip install -r requirements.txt
# Install and execute
COPY . /freqtrade/
RUN pip install -e .
CMD ["freqtrade"]

View File

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

View File

@@ -30,11 +30,10 @@ 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
},
```
@@ -47,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.
@@ -84,16 +85,57 @@ $ pytest
This will by default skip the slow running backtest set. To run backtest set:
```
$ BACKTEST=true pytest
$ 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:

View File

@@ -4,10 +4,10 @@
"stake_amount": 0.05,
"dry_run": false,
"minimal_roi": {
"60": 0.0,
"40": 0.01,
"20": 0.02,
"0": 0.03
"50": 0.0,
"40": 0.01,
"30": 0.02,
"0": 0.045
},
"stoploss": -0.40,
"bid_strategy": {

View File

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

View File

@@ -4,16 +4,18 @@ from datetime import timedelta
import arrow
import talib.abstract as ta
from pandas import DataFrame
from pandas import DataFrame, to_datetime
from freqtrade.exchange import get_ticker_history
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 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
@@ -21,25 +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['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['cci'] = ta.CCI(dataframe, timeperiod=5)
dataframe['sma'] = ta.SMA(dataframe, timeperiod=100)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=4)
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
@@ -49,16 +63,14 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
dataframe.ix[
(dataframe['close'] < dataframe['sma']) &
(dataframe['cci'] < -100) &
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 30) &
(dataframe['fastd'] < 20) &
(dataframe['adx'] > 20),
(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
@@ -71,7 +83,7 @@ def analyze_ticker(pair: str) -> DataFrame:
"""
minimum_date = arrow.utcnow().shift(hours=-24)
data = get_ticker_history(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
dataframe = parse_ticker_dataframe(data['result'])
if dataframe.empty:
logger.warning('Empty dataframe for pair %s', pair)
@@ -119,20 +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['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, dataframe['adx'], label='ADX')
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)
@@ -143,6 +161,7 @@ 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:
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''})
test_pair = 'BTC_ETH'
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
# get_buy_signal(pair)

View File

@@ -85,6 +85,10 @@ def get_balance(currency: str) -> float:
return EXCHANGE.get_balance(currency)
def get_balances():
return EXCHANGE.get_balances()
def get_ticker(pair: str) -> dict:
return EXCHANGE.get_ticker(pair)

View File

@@ -26,10 +26,6 @@ class Bittrex(Exchange):
# Sleep time to avoid rate limits, used in the main loop
SLEEP_TIME: float = 25
@property
def name(self) -> str:
return self.__class__.__name__
@property
def sleep_time(self) -> float:
return self.SLEEP_TIME
@@ -40,13 +36,6 @@ class Bittrex(Exchange):
_EXCHANGE_CONF.update(config)
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
# Check if all pairs are available
markets = self.get_markets()
exchange_name = self.name
for pair in _EXCHANGE_CONF['pair_whitelist']:
if pair not in markets:
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
def buy(self, pair: str, rate: float, amount: float) -> str:
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
if not data['success']:
@@ -65,6 +54,12 @@ class Bittrex(Exchange):
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']:

View File

@@ -49,6 +49,21 @@ class Exchange(ABC):
: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:
"""

View File

@@ -6,6 +6,7 @@ import time
import traceback
from datetime import datetime
from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM
from jsonschema import validate
@@ -223,6 +224,23 @@ 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:
"""
@@ -251,10 +269,10 @@ def app(config: dict) -> None:
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`')
def main():

View File

@@ -5,7 +5,6 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create
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
from freqtrade import exchange
@@ -37,6 +36,14 @@ 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'

View File

@@ -17,7 +17,7 @@ logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)
_updater = None
_updater: Updater = None
_CONF = {}
@@ -41,10 +41,12 @@ def init(config: dict) -> None:
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)
@@ -60,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
@@ -193,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:
"""
@@ -301,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

@@ -1,47 +1,41 @@
# pragma pylint: disable=missing-docstring
from datetime import datetime
import json
import pytest
import arrow
from pandas import DataFrame
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
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},
]
}
@pytest.fixture
def result():
return parse_ticker_dataframe(RESULT_BITTREX['result'], arrow.get('2017-08-30T10:00:00'))
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_orders_by_date(result):
assert 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_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': arrow.utcnow()}])
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': arrow.utcnow()}])
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

@@ -13,65 +13,70 @@ from freqtrade.persistence import Trade
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
def print_results(results):
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
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
))
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": {
"60": 0.0,
"50": 0.0,
"40": 0.01,
"20": 0.02,
"0": 0.03
"30": 0.02,
"0": 0.045
},
"stoploss": -0.40
}
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(conf, pairs, mocker):
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)
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
ticker = analyze_ticker(pair)
mocked_history.return_value = data
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
# 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,
)
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 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
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, index2 - index))
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 ================================')
for pair in pairs:
print('For currency {}:'.format(pair))
print_results(results[results.currency == pair])
[print_pair_results(pair, results) for pair in pairs]
print('TOTAL OVER ALL TRADES:')
print_results(results)
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

@@ -48,6 +48,7 @@ def conf():
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)
@@ -82,6 +83,7 @@ def test_create_trade(conf, mocker):
[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())
@@ -101,6 +103,7 @@ def test_handle_trade(conf, mocker):
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()
@@ -113,14 +116,17 @@ def test_close_trade(conf, mocker):
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

@@ -2,6 +2,7 @@
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(

View File

@@ -9,7 +9,7 @@ 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
from freqtrade.rpc.telegram import _status, _profit, _forcesell, _performance, _start, _stop, _balance
@pytest.fixture
@@ -82,6 +82,7 @@ def test_status_handle(conf, update, mocker):
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)
@@ -112,6 +113,7 @@ def test_profit_handle(conf, update, mocker):
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)
@@ -140,6 +142,7 @@ def test_forcesell_handle(conf, update, mocker):
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)
@@ -171,6 +174,7 @@ def test_performance_handle(conf, update, mocker):
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()
@@ -184,6 +188,7 @@ def test_start_handle(conf, update, mocker):
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()
@@ -197,3 +202,22 @@ def test_stop_handle(conf, update, mocker):
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,6 +1,6 @@
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
SQLAlchemy==1.1.13
python-telegram-bot==8.0
SQLAlchemy==1.1.14
python-telegram-bot==8.1.1
arrow==0.10.0
requests==2.18.4
urllib3==1.22
@@ -11,10 +11,13 @@ scipy==0.19.1
jsonschema==2.6.0
numpy==1.13.3
TA-Lib==0.4.10
pytest==3.2.2
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.0.2
#matplotlib==2.1.0
#PYQT5==5.9

View File

@@ -1,5 +0,0 @@
[aliases]
test=pytest
[tool:pytest]
addopts = --cov=freqtrade --cov-config=.coveragerc freqtrade/tests/

View File

@@ -17,7 +17,7 @@ setup(name='freqtrade',
install_requires=[
'python-bittrex==0.1.3',
'SQLAlchemy==1.1.13',
'python-telegram-bot==8.0',
'python-telegram-bot==8.1.1',
'arrow==0.10.0',
'requests==2.18.4',
'urllib3==1.22',