Skip to content

Commit

Permalink
Merge branch 'release/0.13.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
gcarq committed Nov 1, 2017
2 parents 6b15cb9 + f34af0a commit e2eceaa
Show file tree
Hide file tree
Showing 19 changed files with 860 additions and 129 deletions.
4 changes: 1 addition & 3 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion freqtrade/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__version__ = '0.12.0'
__version__ = '0.13.0'

from . import main
27 changes: 20 additions & 7 deletions freqtrade/analyze.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,28 @@

import arrow
import talib.abstract as ta
from pandas import DataFrame
from pandas import DataFrame, to_datetime

from freqtrade import exchange
from freqtrade.exchange import Bittrex, get_ticker_history
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator

logging.basicConfig(level=logging.DEBUG,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)


def 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
:return: DataFrame
"""
df = DataFrame(ticker) \
.drop('BV', 1) \
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
.sort_values('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


Expand All @@ -41,6 +43,17 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
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


Expand All @@ -50,14 +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['tema'] <= dataframe['blower']) &
(dataframe['mfi'] < 25) &
(dataframe['fastd'] < 25) &
(dataframe['adx'] > 30),
'buy'] = 1
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']

return dataframe

Expand All @@ -70,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)
Expand Down
4 changes: 4 additions & 0 deletions freqtrade/exchange/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
6 changes: 6 additions & 0 deletions freqtrade/exchange/bittrex.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@ def get_balance(self, currency: str) -> float:
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']:
Expand Down
15 changes: 15 additions & 0 deletions freqtrade/exchange/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,21 @@ def get_balance(self, currency: str) -> float:
: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:
"""
Expand Down
24 changes: 21 additions & 3 deletions freqtrade/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import traceback
from datetime import datetime
from typing import Dict, Optional
from signal import signal, SIGINT, SIGABRT, SIGTERM

from jsonschema import validate

Expand Down Expand Up @@ -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:
"""
Expand Down Expand Up @@ -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():
Expand Down
9 changes: 8 additions & 1 deletion freqtrade/persistence.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
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

Expand Down Expand Up @@ -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'

Expand Down
33 changes: 32 additions & 1 deletion freqtrade/rpc/telegram.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
logging.getLogger('telegram').setLevel(logging.INFO)
logger = logging.getLogger(__name__)

_updater = None
_updater: Updater = None
_CONF = {}


Expand All @@ -41,6 +41,7 @@ def init(config: dict) -> None:
handles = [
CommandHandler('status', _status),
CommandHandler('profit', _profit),
CommandHandler('balance', _balance),
CommandHandler('start', _start),
CommandHandler('stop', _stop),
CommandHandler('forcesell', _forcesell),
Expand All @@ -61,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
Expand Down Expand Up @@ -194,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:
"""
Expand Down Expand Up @@ -318,6 +348,7 @@ def _help(bot: Bot, update: Update) -> None:
*/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)
Expand Down
34 changes: 14 additions & 20 deletions freqtrade/tests/test_analyze.py
Original file line number Diff line number Diff line change
@@ -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')

0 comments on commit e2eceaa

Please sign in to comment.