diff --git a/MANIFEST.in b/MANIFEST.in index ef776087e..63508c05d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/freqtrade/__init__.py b/freqtrade/__init__.py index 44a21be08..916143251 100644 --- a/freqtrade/__init__.py +++ b/freqtrade/__init__.py @@ -1,3 +1,3 @@ -__version__ = '0.12.0' +__version__ = '0.13.0' from . import main diff --git a/freqtrade/analyze.py b/freqtrade/analyze.py index 310754754..b99a67809 100644 --- a/freqtrade/analyze.py +++ b/freqtrade/analyze.py @@ -4,17 +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 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 @@ -22,8 +23,9 @@ 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') + .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 @@ -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 @@ -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 @@ -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) diff --git a/freqtrade/exchange/__init__.py b/freqtrade/exchange/__init__.py index 77a2d4b84..b424660bf 100644 --- a/freqtrade/exchange/__init__.py +++ b/freqtrade/exchange/__init__.py @@ -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) diff --git a/freqtrade/exchange/bittrex.py b/freqtrade/exchange/bittrex.py index cb85aaf87..0b70e7a3b 100644 --- a/freqtrade/exchange/bittrex.py +++ b/freqtrade/exchange/bittrex.py @@ -54,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']: diff --git a/freqtrade/exchange/interface.py b/freqtrade/exchange/interface.py index 114ac9a6f..7c6f30be2 100644 --- a/freqtrade/exchange/interface.py +++ b/freqtrade/exchange/interface.py @@ -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: """ diff --git a/freqtrade/main.py b/freqtrade/main.py index ce933fe44..68277adaa 100755 --- a/freqtrade/main.py +++ b/freqtrade/main.py @@ -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(): diff --git a/freqtrade/persistence.py b/freqtrade/persistence.py index 7f8bfbc69..dc9078d5f 100644 --- a/freqtrade/persistence.py +++ b/freqtrade/persistence.py @@ -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' diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py index 4b320eb85..fef8c46b5 100644 --- a/freqtrade/rpc/telegram.py +++ b/freqtrade/rpc/telegram.py @@ -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,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), @@ -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 @@ -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: """ @@ -318,6 +348,7 @@ def _help(bot: Bot, update: Update) -> None: */profit:* `Lists cumulative profit from all finished trades` */forcesell :* `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) diff --git a/freqtrade/tests/test_analyze.py b/freqtrade/tests/test_analyze.py index e716eb3ad..18d232eef 100644 --- a/freqtrade/tests/test_analyze.py +++ b/freqtrade/tests/test_analyze.py @@ -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') diff --git a/freqtrade/tests/test_backtesting.py b/freqtrade/tests/test_backtesting.py index d008c4ae6..4bd04e721 100644 --- a/freqtrade/tests/test_backtesting.py +++ b/freqtrade/tests/test_backtesting.py @@ -13,19 +13,27 @@ 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 - )) + ) + + +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 { @@ -39,39 +47,36 @@ def conf(): } -@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)) diff --git a/freqtrade/tests/test_hyperopt.py b/freqtrade/tests/test_hyperopt.py index 167e0d9ab..5fedff519 100644 --- a/freqtrade/tests/test_hyperopt.py +++ b/freqtrade/tests/test_hyperopt.py @@ -1,34 +1,29 @@ # pragma pylint: disable=missing-docstring -import json import logging import os from functools import reduce +from math import exp +from operator import itemgetter import pytest -import arrow +from hyperopt import fmin, tpe, hp, Trials, STATUS_OK from pandas import DataFrame -from hyperopt import fmin, tpe, hp - -from freqtrade.analyze import analyze_ticker -from freqtrade.main import should_sell -from freqtrade.persistence import Trade +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 -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 - )) +# 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 { @@ -42,52 +37,14 @@ def conf(): } -def backtest(conf, pairs, mocker, buy_strategy): - 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')) - mocker.patch('freqtrade.analyze.populate_buy_trend', side_effect=buy_strategy) - 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_results(results) - - # set the value below to suit your number concurrent trades so its realistic to 20days of data - TARGET_TRADES = 1200 - if results.profit.sum() == 0 or results.profit.mean() == 0: - return 49999999999 # avoid division by zero, return huge value to discard result - return abs(len(results.index) - 1200.1) / (results.profit.sum() ** 2) * results.duration.mean() # the smaller the better - def buy_strategy_generator(params): print(params) + def populate_buy_trend(dataframe: DataFrame) -> DataFrame: conditions = [] # GUARDS AND TRENDS - if params['below_sma']['enabled']: - conditions.append(dataframe['close'] < dataframe['sma']) - if params['over_sma']['enabled']: - conditions.append(dataframe['close'] > dataframe['sma']) + 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']: @@ -96,6 +53,8 @@ def buy_strategy_generator(params): 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']: @@ -107,6 +66,9 @@ def buy_strategy_generator(params): 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'])) @@ -118,34 +80,53 @@ def buy_strategy_generator(params): 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): - return backtest(conf, pairs, mocker, buy_strategy_generator(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', 2, 40)} + {'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)} ]), 'fastd': hp.choice('fastd', [ {'enabled': False}, - {'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)} + {'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)} ]), 'adx': hp.choice('adx', [ {'enabled': False}, - {'enabled': True, 'value': hp.uniform('adx-value', 2, 40)} + {'enabled': True, 'value': hp.uniform('adx-value', 10, 30)} ]), 'cci': hp.choice('cci', [ {'enabled': False}, - {'enabled': True, 'value': hp.uniform('cci-value', -200, -100)} + {'enabled': True, 'value': hp.uniform('cci-value', -150, -100)} ]), - 'below_sma': hp.choice('below_sma', [ + 'rsi': hp.choice('rsi', [ {'enabled': False}, - {'enabled': True} + {'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)} ]), - 'over_sma': hp.choice('over_sma', [ + 'uptrend_long_ema': hp.choice('uptrend_long_ema', [ {'enabled': False}, {'enabled': True} ]), @@ -159,8 +140,15 @@ def test_hyperopt(conf, pairs, mocker): ]), 'trigger': hp.choice('trigger', [ {'type': 'lower_bb'}, - {'type': 'faststoch10'} + {'type': 'faststoch10'}, + {'type': 'ao_cross_zero'}, + {'type': 'ema5_cross_ema10'}, + {'type': 'macd_cross_signal'}, ]), } - - print('Best parameters {}'.format(fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40))) + 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'])) diff --git a/freqtrade/tests/test_main.py b/freqtrade/tests/test_main.py index 050d21ad4..67e66bd23 100644 --- a/freqtrade/tests/test_main.py +++ b/freqtrade/tests/test_main.py @@ -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 diff --git a/freqtrade/tests/test_persistence.py b/freqtrade/tests/test_persistence.py index 8cf280130..6a74ad715 100644 --- a/freqtrade/tests/test_persistence.py +++ b/freqtrade/tests/test_persistence.py @@ -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( diff --git a/freqtrade/tests/test_telegram.py b/freqtrade/tests/test_telegram.py index fb9a618a0..354abb086 100644 --- a/freqtrade/tests/test_telegram.py +++ b/freqtrade/tests/test_telegram.py @@ -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] diff --git a/freqtrade/tests/testdata/download_backtest_data.py b/freqtrade/tests/testdata/download_backtest_data.py index 1119aac42..d6212cfea 100644 --- a/freqtrade/tests/testdata/download_backtest_data.py +++ b/freqtrade/tests/testdata/download_backtest_data.py @@ -5,6 +5,7 @@ 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-' @@ -12,5 +13,6 @@ for cur in CURRENCIES: x = urlopen(url) json_data = x.read() json_str = str(json_data, 'utf-8') - with open('btc-'+cur+'.json', 'w') as file: + output = OUTPUT_DIR + 'btc-'+cur+'.json' + with open(output, 'w') as file: file.write(json_str) diff --git a/freqtrade/vendor/__init__.py b/freqtrade/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/vendor/qtpylib/__init__.py b/freqtrade/vendor/qtpylib/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/freqtrade/vendor/qtpylib/indicators.py b/freqtrade/vendor/qtpylib/indicators.py new file mode 100644 index 000000000..81e765853 --- /dev/null +++ b/freqtrade/vendor/qtpylib/indicators.py @@ -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