Merge branch 'release/0.11.0'

This commit is contained in:
gcarq 2017-10-10 17:55:05 +02:00
commit 604a888791
42 changed files with 1014 additions and 747 deletions

2
.coveragerc Normal file
View File

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

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,20 @@
FROM python:3.6.2
RUN pip install numpy
RUN apt-get update
RUN apt-get -y install build-essential
# 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
ENV LD_LIBRARY_PATH /usr/local/lib
RUN mkdir -p /freqtrade
# Prepare environment
RUN mkdir /freqtrade
COPY . /freqtrade/
WORKDIR /freqtrade
ADD ./requirements.txt /freqtrade/requirements.txt
# Install dependencies and execute
RUN pip install -r requirements.txt
ADD . /freqtrade
CMD python main.py
RUN pip install -e .
CMD ["freqtrade"]

7
MANIFEST.in Normal file
View File

@ -0,0 +1,7 @@
include LICENSE
include README.md
include config.json.example
include freqtrade/exchange/*.py
include freqtrade/rpc/*.py
include freqtrade/tests/*.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.
@ -68,7 +70,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)).
@ -76,7 +79,12 @@ 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
```
#### Docker

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
"60": 0.0,
"40": 0.01,
"20": 0.02,
"0": 0.03
},
"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.11.0'
from . import main

View File

@ -1,36 +1,18 @@
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
from freqtrade.exchange import get_ticker_history
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:
"""
Analyses the trend for the given pair
@ -48,10 +30,16 @@ 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['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['mfi'] = ta.MFI(dataframe)
return dataframe
@ -61,26 +49,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['close'] < dataframe['sma']) &
(dataframe['cci'] < -100) &
(dataframe['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 30) &
(dataframe['fastd'] < 20) &
(dataframe['adx'] > 20),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
@ -93,13 +69,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)
minimum_date = arrow.utcnow().shift(hours=-24)
data = get_ticker_history(pair, minimum_date)
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
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 +89,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
@ -138,12 +124,13 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
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['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['adx'], label='ADX')
ax2.plot(dataframe.index.values, dataframe['mfi'], label='MFI')
# ax2.plot(dataframe.index.values, [25] * len(dataframe.index.values))
ax2.legend()
# Fine-tune figure; make subplots close to each other and hide x ticks for
@ -156,7 +143,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:
test_pair = 'BTC_ANT'
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)

View File

@ -0,0 +1,115 @@
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_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,120 @@
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 name(self) -> str:
return self.__class__.__name__
@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'])
# 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']:
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_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,127 @@
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_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,4 +1,5 @@
#!/usr/bin/env python
import copy
import json
import logging
import time
@ -8,22 +9,16 @@ from typing import Dict, Optional
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 +34,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 +89,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 +142,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 +151,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 +166,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 +187,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 +199,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)
@ -238,7 +226,7 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
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,7 +248,7 @@ 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()))
@ -269,8 +257,17 @@ def app(config: dict) -> None:
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

@ -5,11 +5,9 @@ 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
import exchange
from freqtrade import exchange
_CONF = {}
@ -43,7 +41,7 @@ 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,14 +4,13 @@ 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)
@ -31,9 +30,12 @@ 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 = [
@ -255,7 +257,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,

View File

@ -0,0 +1,47 @@
# pragma pylint: disable=missing-docstring
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'))
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_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()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert get_buy_signal('BTC-ETH')
buydf = DataFrame([{'buy': 0, 'date': arrow.utcnow()}])
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
assert not get_buy_signal('BTC-ETH')

View File

@ -0,0 +1,77 @@
# 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 print_results(results):
print('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
))
@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,
"40": 0.01,
"20": 0.02,
"0": 0.03
},
"stoploss": -0.40
}
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
def test_backtest(conf, pairs, mocker):
trades = []
mocker.patch.dict('freqtrade.main._CONF', conf)
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)
# 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 pairs:
print('For currency {}:'.format(pair))
print_results(results[results.currency == pair])
print('TOTAL OVER ALL TRADES:')
print_results(results)

View File

@ -0,0 +1,126 @@
# 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,20 @@
# 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,199 @@
# 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
@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]

View File

@ -1,14 +1,20 @@
-e git+https://github.com/ericsomdahl/python-bittrex.git#egg=python-bittrex
-e git+https://github.com/ericsomdahl/python-bittrex.git@d7033d0#egg=python-bittrex
SQLAlchemy==1.1.13
python-telegram-bot==7.0.1
python-telegram-bot==8.0
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.2
pytest-mock==1.6.3
pytest-cov==2.5.1
# Required for plotting data
#matplotlib==2.0.2
#PYQT5==5.9

5
setup.cfg Normal file
View File

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

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.0',
'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()