Merge branch 'release/0.13.0'
This commit is contained in:
commit
e2eceaa904
@ -1,7 +1,5 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include README.md
|
include README.md
|
||||||
include config.json.example
|
include config.json.example
|
||||||
include freqtrade/exchange/*.py
|
recursive-include freqtrade *.py
|
||||||
include freqtrade/rpc/*.py
|
|
||||||
include freqtrade/tests/*.py
|
|
||||||
include freqtrade/tests/testdata/*.json
|
include freqtrade/tests/testdata/*.json
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
__version__ = '0.12.0'
|
__version__ = '0.13.0'
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
@ -4,17 +4,18 @@ from datetime import timedelta
|
|||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame, to_datetime
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex, get_ticker_history
|
from freqtrade.exchange import Bittrex, get_ticker_history
|
||||||
|
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
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
|
Analyses the trend for the given pair
|
||||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
: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) \
|
df = DataFrame(ticker) \
|
||||||
.drop('BV', 1) \
|
.drop('BV', 1) \
|
||||||
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'}) \
|
.rename(columns={'C':'close', 'V':'volume', 'O':'open', 'H':'high', 'L':'low', 'T':'date'})
|
||||||
.sort_values('date')
|
df['date'] = to_datetime(df['date'], utc=True, infer_datetime_format=True)
|
||||||
|
df.sort_values('date', inplace=True)
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +43,17 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
dataframe['cci'] = ta.CCI(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
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@ -50,14 +63,14 @@ def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
|||||||
:param dataframe: DataFrame
|
:param dataframe: DataFrame
|
||||||
:return: DataFrame with buy column
|
:return: DataFrame with buy column
|
||||||
"""
|
"""
|
||||||
dataframe.loc[
|
dataframe.ix[
|
||||||
(dataframe['close'] < dataframe['sma']) &
|
(dataframe['close'] < dataframe['sma']) &
|
||||||
(dataframe['tema'] <= dataframe['blower']) &
|
(dataframe['tema'] <= dataframe['blower']) &
|
||||||
(dataframe['mfi'] < 25) &
|
(dataframe['mfi'] < 25) &
|
||||||
(dataframe['fastd'] < 25) &
|
(dataframe['fastd'] < 25) &
|
||||||
(dataframe['adx'] > 30),
|
(dataframe['adx'] > 30),
|
||||||
'buy'] = 1
|
'buy'] = 1
|
||||||
dataframe.loc[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
dataframe.ix[dataframe['buy'] == 1, 'buy_price'] = dataframe['close']
|
||||||
|
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
@ -70,7 +83,7 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
"""
|
"""
|
||||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||||
data = get_ticker_history(pair, minimum_date)
|
data = get_ticker_history(pair, minimum_date)
|
||||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
dataframe = parse_ticker_dataframe(data['result'])
|
||||||
|
|
||||||
if dataframe.empty:
|
if dataframe.empty:
|
||||||
logger.warning('Empty dataframe for pair %s', pair)
|
logger.warning('Empty dataframe for pair %s', pair)
|
||||||
|
@ -85,6 +85,10 @@ def get_balance(currency: str) -> float:
|
|||||||
return EXCHANGE.get_balance(currency)
|
return EXCHANGE.get_balance(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_balances():
|
||||||
|
return EXCHANGE.get_balances()
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str) -> dict:
|
def get_ticker(pair: str) -> dict:
|
||||||
return EXCHANGE.get_ticker(pair)
|
return EXCHANGE.get_ticker(pair)
|
||||||
|
|
||||||
|
@ -54,6 +54,12 @@ class Bittrex(Exchange):
|
|||||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
return float(data['result']['Balance'] or 0.0)
|
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:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
|
@ -49,6 +49,21 @@ class Exchange(ABC):
|
|||||||
:return: 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
|
@abstractmethod
|
||||||
def get_ticker(self, pair: str) -> dict:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
"""
|
"""
|
||||||
|
@ -6,6 +6,7 @@ import time
|
|||||||
import traceback
|
import traceback
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||||
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
@ -223,6 +224,23 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
else:
|
else:
|
||||||
update_state(State.STOPPED)
|
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:
|
def app(config: dict) -> None:
|
||||||
"""
|
"""
|
||||||
@ -251,10 +269,10 @@ def app(config: dict) -> None:
|
|||||||
time.sleep(exchange.EXCHANGE.sleep_time)
|
time.sleep(exchange.EXCHANGE.sleep_time)
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
except RuntimeError:
|
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!')
|
logger.exception('RuntimeError. Trader stopped!')
|
||||||
finally:
|
|
||||||
telegram.send_msg('*Status:* `Trader has stopped`')
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -5,7 +5,6 @@ from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create
|
|||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy.types import Enum
|
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
|
|
||||||
@ -37,6 +36,14 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
Base.metadata.create_all(engine)
|
Base.metadata.create_all(engine)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup() -> None:
|
||||||
|
"""
|
||||||
|
Flushes all pending operations to disk.
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
|
||||||
class Trade(Base):
|
class Trade(Base):
|
||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
|||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
logging.getLogger('telegram').setLevel(logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_updater = None
|
_updater: Updater = None
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +41,7 @@ def init(config: dict) -> None:
|
|||||||
handles = [
|
handles = [
|
||||||
CommandHandler('status', _status),
|
CommandHandler('status', _status),
|
||||||
CommandHandler('profit', _profit),
|
CommandHandler('profit', _profit),
|
||||||
|
CommandHandler('balance', _balance),
|
||||||
CommandHandler('start', _start),
|
CommandHandler('start', _start),
|
||||||
CommandHandler('stop', _stop),
|
CommandHandler('stop', _stop),
|
||||||
CommandHandler('forcesell', _forcesell),
|
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]:
|
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
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)
|
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
|
@authorized_only
|
||||||
def _start(bot: Bot, update: Update) -> None:
|
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`
|
*/profit:* `Lists cumulative profit from all finished trades`
|
||||||
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
||||||
*/performance:* `Show performance of each finished trade grouped by pair`
|
*/performance:* `Show performance of each finished trade grouped by pair`
|
||||||
|
*/balance:* `Show account balance per currency`
|
||||||
*/help:* `This help message`
|
*/help:* `This help message`
|
||||||
"""
|
"""
|
||||||
send_msg(message, bot=bot)
|
send_msg(message, bot=bot)
|
||||||
|
@ -1,47 +1,41 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
import pytest
|
import pytest
|
||||||
import arrow
|
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
get_buy_signal
|
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
|
@pytest.fixture
|
||||||
def result():
|
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):
|
def test_dataframe_has_correct_columns(result):
|
||||||
assert result.columns.tolist() == \
|
assert result.columns.tolist() == \
|
||||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||||
|
|
||||||
def test_orders_by_date(result):
|
|
||||||
assert result['date'].tolist() == \
|
def test_dataframe_has_correct_length(result):
|
||||||
['2017-08-30T10:34:00',
|
assert len(result.index) == 5751
|
||||||
'2017-08-30T10:37:00',
|
|
||||||
'2017-08-30T10:40:00',
|
|
||||||
'2017-08-30T10:42:00']
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
assert 'buy_price' in dataframe.columns
|
assert 'buy_price' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
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)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert get_buy_signal('BTC-ETH')
|
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)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
assert not get_buy_signal('BTC-ETH')
|
assert not get_buy_signal('BTC-ETH')
|
||||||
|
@ -13,19 +13,27 @@ from freqtrade.persistence import Trade
|
|||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
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),
|
len(results.index),
|
||||||
results.profit.mean() * 100.0,
|
results.profit.mean() * 100.0,
|
||||||
results.profit.sum(),
|
results.profit.sum(),
|
||||||
results.duration.mean() * 5
|
results.duration.mean() * 5
|
||||||
))
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def print_pair_results(pair, results):
|
||||||
|
print('For currency {}:'.format(pair))
|
||||||
|
print(format_results(results[results.currency == pair]))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pairs():
|
def pairs():
|
||||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def conf():
|
def conf():
|
||||||
return {
|
return {
|
||||||
@ -39,39 +47,36 @@ def conf():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
def backtest(conf, pairs, mocker):
|
||||||
def test_backtest(conf, pairs, mocker):
|
|
||||||
trades = []
|
trades = []
|
||||||
|
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||||
data = json.load(data_file)
|
data = json.load(data_file)
|
||||||
|
mocked_history.return_value = data
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
|
||||||
ticker = analyze_ticker(pair)
|
|
||||||
# for each buy point
|
# for each buy point
|
||||||
for index, row in ticker[ticker.buy == 1].iterrows():
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||||
trade = Trade(
|
trade = Trade(open_rate=row.close, open_date=row.date, amount=1)
|
||||||
open_rate=row['close'],
|
|
||||||
open_date=arrow.get(row['date']).datetime,
|
|
||||||
amount=1,
|
|
||||||
)
|
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
for index2, row2 in ticker[index:].iterrows():
|
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||||
if should_sell(trade, row2['close'], arrow.get(row2['date']).datetime):
|
if should_sell(trade, row2.close, row2.date):
|
||||||
current_profit = (row2['close'] - trade.open_rate) / trade.open_rate
|
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
|
break
|
||||||
|
|
||||||
labels = ['currency', 'profit', 'duration']
|
labels = ['currency', 'profit', 'duration']
|
||||||
results = DataFrame.from_records(trades, columns=labels)
|
results = DataFrame.from_records(trades, columns=labels)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
|
def test_backtest(conf, pairs, mocker, report=True):
|
||||||
|
results = backtest(conf, pairs, mocker)
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
print('====================== BACKTESTING REPORT ================================')
|
||||||
|
[print_pair_results(pair, results) for pair in pairs]
|
||||||
for pair in pairs:
|
|
||||||
print('For currency {}:'.format(pair))
|
|
||||||
print_results(results[results.currency == pair])
|
|
||||||
print('TOTAL OVER ALL TRADES:')
|
print('TOTAL OVER ALL TRADES:')
|
||||||
print_results(results)
|
print(format_results(results))
|
||||||
|
@ -1,34 +1,29 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from math import exp
|
||||||
|
from operator import itemgetter
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import arrow
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
from hyperopt import fmin, tpe, hp
|
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||||
|
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||||
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
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
def print_results(results):
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||||
print('Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
TARGET_TRADES = 1200
|
||||||
len(results.index),
|
|
||||||
results.profit.mean() * 100.0,
|
|
||||||
results.profit.sum(),
|
|
||||||
results.duration.mean() * 5
|
|
||||||
))
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pairs():
|
def pairs():
|
||||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def conf():
|
def conf():
|
||||||
return {
|
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):
|
def buy_strategy_generator(params):
|
||||||
print(params)
|
print(params)
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if params['below_sma']['enabled']:
|
if params['uptrend_long_ema']['enabled']:
|
||||||
conditions.append(dataframe['close'] < dataframe['sma'])
|
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||||
if params['over_sma']['enabled']:
|
|
||||||
conditions.append(dataframe['close'] > dataframe['sma'])
|
|
||||||
if params['mfi']['enabled']:
|
if params['mfi']['enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||||
if params['fastd']['enabled']:
|
if params['fastd']['enabled']:
|
||||||
@ -96,6 +53,8 @@ def buy_strategy_generator(params):
|
|||||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||||
if params['cci']['enabled']:
|
if params['cci']['enabled']:
|
||||||
conditions.append(dataframe['cci'] < params['cci']['value'])
|
conditions.append(dataframe['cci'] < params['cci']['value'])
|
||||||
|
if params['rsi']['enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||||
if params['over_sar']['enabled']:
|
if params['over_sar']['enabled']:
|
||||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||||
if params['uptrend_sma']['enabled']:
|
if params['uptrend_sma']['enabled']:
|
||||||
@ -107,6 +66,9 @@ def buy_strategy_generator(params):
|
|||||||
triggers = {
|
triggers = {
|
||||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||||
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
|
'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']))
|
conditions.append(triggers.get(params['trigger']['type']))
|
||||||
|
|
||||||
@ -118,34 +80,53 @@ def buy_strategy_generator(params):
|
|||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_hyperopt(conf, pairs, mocker):
|
def test_hyperopt(conf, pairs, mocker):
|
||||||
|
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
||||||
|
|
||||||
def optimizer(params):
|
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 = {
|
space = {
|
||||||
'mfi': hp.choice('mfi', [
|
'mfi': hp.choice('mfi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('mfi-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
|
||||||
]),
|
]),
|
||||||
'fastd': hp.choice('fastd', [
|
'fastd': hp.choice('fastd', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
|
||||||
]),
|
]),
|
||||||
'adx': hp.choice('adx', [
|
'adx': hp.choice('adx', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('adx-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
|
||||||
]),
|
]),
|
||||||
'cci': hp.choice('cci', [
|
'cci': hp.choice('cci', [
|
||||||
{'enabled': False},
|
{'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': 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': False},
|
||||||
{'enabled': True}
|
{'enabled': True}
|
||||||
]),
|
]),
|
||||||
@ -159,8 +140,15 @@ def test_hyperopt(conf, pairs, mocker):
|
|||||||
]),
|
]),
|
||||||
'trigger': hp.choice('trigger', [
|
'trigger': hp.choice('trigger', [
|
||||||
{'type': 'lower_bb'},
|
{'type': 'lower_bb'},
|
||||||
{'type': 'faststoch10'}
|
{'type': 'faststoch10'},
|
||||||
|
{'type': 'ao_cross_zero'},
|
||||||
|
{'type': 'ema5_cross_ema10'},
|
||||||
|
{'type': 'macd_cross_signal'},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
|
trials = Trials()
|
||||||
print('Best parameters {}'.format(fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40)))
|
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']))
|
||||||
|
@ -48,6 +48,7 @@ def conf():
|
|||||||
validate(configuration, CONF_SCHEMA)
|
validate(configuration, CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade(conf, mocker):
|
def test_create_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
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')]
|
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(conf, mocker):
|
def test_handle_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
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.close_date is not None
|
||||||
assert trade.open_order_id == 'dry_run'
|
assert trade.open_order_id == 'dry_run'
|
||||||
|
|
||||||
|
|
||||||
def test_close_trade(conf, mocker):
|
def test_close_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
@ -113,14 +116,17 @@ def test_close_trade(conf, mocker):
|
|||||||
assert closed
|
assert closed
|
||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker):
|
def test_balance_fully_ask_side(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker):
|
def test_balance_fully_last_side(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
||||||
|
|
||||||
|
|
||||||
def test_balance_when_last_bigger_than_ask(mocker):
|
def test_balance_when_last_bigger_than_ask(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
from freqtrade.exchange import Exchanges
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
|
|
||||||
def test_exec_sell_order(mocker):
|
def test_exec_sell_order(mocker):
|
||||||
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
||||||
trade = Trade(
|
trade = Trade(
|
||||||
|
@ -9,7 +9,7 @@ from telegram import Bot, Update, Message, Chat
|
|||||||
from freqtrade.main import init, create_trade
|
from freqtrade.main import init, create_trade
|
||||||
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||||
from freqtrade.persistence import Trade
|
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
|
@pytest.fixture
|
||||||
@ -82,6 +82,7 @@ def test_status_handle(conf, update, mocker):
|
|||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_profit_handle(conf, update, mocker):
|
def test_profit_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
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 msg_mock.call_count == 2
|
||||||
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(conf, update, mocker):
|
def test_forcesell_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
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 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '0.072561' 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):
|
def test_performance_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
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 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert 'BTC_ETH 100.00%' 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):
|
def test_start_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -184,6 +188,7 @@ def test_start_handle(conf, update, mocker):
|
|||||||
assert get_state() == State.RUNNING
|
assert get_state() == State.RUNNING
|
||||||
assert msg_mock.call_count == 0
|
assert msg_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_stop_handle(conf, update, mocker):
|
def test_stop_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
@ -197,3 +202,22 @@ def test_stop_handle(conf, update, mocker):
|
|||||||
assert get_state() == State.STOPPED
|
assert get_state() == State.STOPPED
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
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]
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
|
||||||
CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"]
|
CURRENCIES = ["ok", "neo", "dash", "etc", "eth", "snt"]
|
||||||
|
OUTPUT_DIR = 'freqtrade/tests/testdata/'
|
||||||
|
|
||||||
for cur in CURRENCIES:
|
for cur in CURRENCIES:
|
||||||
url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-'
|
url1 = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks?marketName=BTC-'
|
||||||
@ -12,5 +13,6 @@ for cur in CURRENCIES:
|
|||||||
x = urlopen(url)
|
x = urlopen(url)
|
||||||
json_data = x.read()
|
json_data = x.read()
|
||||||
json_str = str(json_data, 'utf-8')
|
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)
|
file.write(json_str)
|
||||||
|
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
619
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal file
619
freqtrade/vendor/qtpylib/indicators.py
vendored
Normal file
@ -0,0 +1,619 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# QTPyLib: Quantitative Trading Python Library
|
||||||
|
# https://github.com/ranaroussi/qtpylib
|
||||||
|
#
|
||||||
|
# Copyright 2016 Ran Aroussi
|
||||||
|
#
|
||||||
|
# Licensed under the GNU Lesser General Public License, v3.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.gnu.org/licenses/lgpl-3.0.en.html
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
import warnings
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pandas.core.base import PandasObject
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
# check min, python version
|
||||||
|
if sys.version_info < (3, 4):
|
||||||
|
raise SystemError("QTPyLib requires Python version >= 3.4")
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
warnings.simplefilter(action="ignore", category=RuntimeWarning)
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
|
||||||
|
def numpy_rolling_window(data, window):
|
||||||
|
shape = data.shape[:-1] + (data.shape[-1] - window + 1, window)
|
||||||
|
strides = data.strides + (data.strides[-1],)
|
||||||
|
return np.lib.stride_tricks.as_strided(data, shape=shape, strides=strides)
|
||||||
|
|
||||||
|
|
||||||
|
def numpy_rolling_series(func):
|
||||||
|
def func_wrapper(data, window, as_source=False):
|
||||||
|
series = data.values if isinstance(data, pd.Series) else data
|
||||||
|
|
||||||
|
new_series = np.empty(len(series)) * np.nan
|
||||||
|
calculated = func(series, window)
|
||||||
|
new_series[-len(calculated):] = calculated
|
||||||
|
|
||||||
|
if as_source and isinstance(data, pd.Series):
|
||||||
|
return pd.Series(index=data.index, data=new_series)
|
||||||
|
|
||||||
|
return new_series
|
||||||
|
|
||||||
|
return func_wrapper
|
||||||
|
|
||||||
|
|
||||||
|
@numpy_rolling_series
|
||||||
|
def numpy_rolling_mean(data, window, as_source=False):
|
||||||
|
return np.mean(numpy_rolling_window(data, window), -1)
|
||||||
|
|
||||||
|
|
||||||
|
@numpy_rolling_series
|
||||||
|
def numpy_rolling_std(data, window, as_source=False):
|
||||||
|
return np.std(numpy_rolling_window(data, window), -1)
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def session(df, start='17:00', end='16:00'):
|
||||||
|
""" remove previous globex day from df """
|
||||||
|
if len(df) == 0:
|
||||||
|
return df
|
||||||
|
|
||||||
|
# get start/end/now as decimals
|
||||||
|
int_start = list(map(int, start.split(':')))
|
||||||
|
int_start = (int_start[0] + int_start[1] - 1 / 100) - 0.0001
|
||||||
|
int_end = list(map(int, end.split(':')))
|
||||||
|
int_end = int_end[0] + int_end[1] / 100
|
||||||
|
int_now = (df[-1:].index.hour[0] + (df[:1].index.minute[0]) / 100)
|
||||||
|
|
||||||
|
# same-dat session?
|
||||||
|
is_same_day = int_end > int_start
|
||||||
|
|
||||||
|
# set pointers
|
||||||
|
curr = prev = df[-1:].index[0].strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# globex/forex session
|
||||||
|
if is_same_day == False:
|
||||||
|
prev = (datetime.strptime(curr, '%Y-%m-%d') -
|
||||||
|
timedelta(1)).strftime('%Y-%m-%d')
|
||||||
|
|
||||||
|
# slice
|
||||||
|
if int_now >= int_start:
|
||||||
|
df = df[df.index >= curr + ' ' + start]
|
||||||
|
else:
|
||||||
|
df = df[df.index >= prev + ' ' + start]
|
||||||
|
|
||||||
|
return df.copy()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def heikinashi(bars):
|
||||||
|
bars = bars.copy()
|
||||||
|
bars['ha_close'] = (bars['open'] + bars['high'] +
|
||||||
|
bars['low'] + bars['close']) / 4
|
||||||
|
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
|
||||||
|
bars.loc[:1, 'ha_open'] = bars['open'].values[0]
|
||||||
|
bars.loc[1:, 'ha_open'] = (
|
||||||
|
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
|
||||||
|
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
|
||||||
|
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
|
||||||
|
|
||||||
|
return pd.DataFrame(index=bars.index, data={'open': bars['ha_open'],
|
||||||
|
'high': bars['ha_high'], 'low': bars['ha_low'], 'close': bars['ha_close']})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def tdi(series, rsi_len=13, bollinger_len=34, rsi_smoothing=2, rsi_signal_len=7, bollinger_std=1.6185):
|
||||||
|
rsi_series = rsi(series, rsi_len)
|
||||||
|
bb_series = bollinger_bands(rsi_series, bollinger_len, bollinger_std)
|
||||||
|
signal = sma(rsi_series, rsi_signal_len)
|
||||||
|
rsi_series = sma(rsi_series, rsi_smoothing)
|
||||||
|
|
||||||
|
return pd.DataFrame(index=series.index, data={
|
||||||
|
"rsi": rsi_series,
|
||||||
|
"signal": signal,
|
||||||
|
"bbupper": bb_series['upper'],
|
||||||
|
"bblower": bb_series['lower'],
|
||||||
|
"bbmid": bb_series['mid']
|
||||||
|
})
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def awesome_oscillator(df, weighted=False, fast=5, slow=34):
|
||||||
|
midprice = (df['high'] + df['low']) / 2
|
||||||
|
|
||||||
|
if weighted:
|
||||||
|
ao = (midprice.ewm(fast).mean() - midprice.ewm(slow).mean()).values
|
||||||
|
else:
|
||||||
|
ao = numpy_rolling_mean(midprice, fast) - \
|
||||||
|
numpy_rolling_mean(midprice, slow)
|
||||||
|
|
||||||
|
return pd.Series(index=df.index, data=ao)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def nans(len=1):
|
||||||
|
mtx = np.empty(len)
|
||||||
|
mtx[:] = np.nan
|
||||||
|
return mtx
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def typical_price(bars):
|
||||||
|
res = (bars['high'] + bars['low'] + bars['close']) / 3.
|
||||||
|
return pd.Series(index=bars.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def mid_price(bars):
|
||||||
|
res = (bars['high'] + bars['low']) / 2.
|
||||||
|
return pd.Series(index=bars.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def ibs(bars):
|
||||||
|
""" Internal bar strength """
|
||||||
|
res = np.round((bars['close'] - bars['low']) /
|
||||||
|
(bars['high'] - bars['low']), 2)
|
||||||
|
return pd.Series(index=bars.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def true_range(bars):
|
||||||
|
return pd.DataFrame({
|
||||||
|
"hl": bars['high'] - bars['low'],
|
||||||
|
"hc": abs(bars['high'] - bars['close'].shift(1)),
|
||||||
|
"lc": abs(bars['low'] - bars['close'].shift(1))
|
||||||
|
}).max(axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def atr(bars, window=14, exp=False):
|
||||||
|
tr = true_range(bars)
|
||||||
|
|
||||||
|
if exp:
|
||||||
|
res = rolling_weighted_mean(tr, window)
|
||||||
|
else:
|
||||||
|
res = rolling_mean(tr, window)
|
||||||
|
|
||||||
|
res = pd.Series(res)
|
||||||
|
return (res.shift(1) * (window - 1) + res) / window
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def crossed(series1, series2, direction=None):
|
||||||
|
if isinstance(series1, np.ndarray):
|
||||||
|
series1 = pd.Series(series1)
|
||||||
|
|
||||||
|
if isinstance(series2, int) or isinstance(series2, float) or isinstance(series2, np.ndarray):
|
||||||
|
series2 = pd.Series(index=series1.index, data=series2)
|
||||||
|
|
||||||
|
if direction is None or direction == "above":
|
||||||
|
above = pd.Series((series1 > series2) & (
|
||||||
|
series1.shift(1) <= series2.shift(1)))
|
||||||
|
|
||||||
|
if direction is None or direction == "below":
|
||||||
|
below = pd.Series((series1 < series2) & (
|
||||||
|
series1.shift(1) >= series2.shift(1)))
|
||||||
|
|
||||||
|
if direction is None:
|
||||||
|
return above or below
|
||||||
|
|
||||||
|
return above if direction is "above" else below
|
||||||
|
|
||||||
|
|
||||||
|
def crossed_above(series1, series2):
|
||||||
|
return crossed(series1, series2, "above")
|
||||||
|
|
||||||
|
|
||||||
|
def crossed_below(series1, series2):
|
||||||
|
return crossed(series1, series2, "below")
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def rolling_std(series, window=200, min_periods=None):
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
try:
|
||||||
|
if min_periods == window:
|
||||||
|
return numpy_rolling_std(series, window, True)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return series.rolling(window=window, min_periods=min_periods).std()
|
||||||
|
except:
|
||||||
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
|
||||||
|
except:
|
||||||
|
return pd.rolling_std(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rolling_mean(series, window=200, min_periods=None):
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
try:
|
||||||
|
if min_periods == window:
|
||||||
|
return numpy_rolling_mean(series, window, True)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
return series.rolling(window=window, min_periods=min_periods).mean()
|
||||||
|
except:
|
||||||
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
|
||||||
|
except:
|
||||||
|
return pd.rolling_mean(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rolling_min(series, window=14, min_periods=None):
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
|
except:
|
||||||
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
except:
|
||||||
|
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rolling_max(series, window=14, min_periods=None):
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
return series.rolling(window=window, min_periods=min_periods).min()
|
||||||
|
except:
|
||||||
|
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
|
||||||
|
except:
|
||||||
|
return pd.rolling_min(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rolling_weighted_mean(series, window=200, min_periods=None):
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
try:
|
||||||
|
return series.ewm(span=window, min_periods=min_periods).mean()
|
||||||
|
except:
|
||||||
|
return pd.ewma(series, span=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def hull_moving_average(series, window=200):
|
||||||
|
wma = (2 * rolling_weighted_mean(series, window=window / 2)) - \
|
||||||
|
rolling_weighted_mean(series, window=window)
|
||||||
|
return rolling_weighted_mean(wma, window=np.sqrt(window))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def sma(series, window=200, min_periods=None):
|
||||||
|
return rolling_mean(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def wma(series, window=200, min_periods=None):
|
||||||
|
return rolling_weighted_mean(series, window=window, min_periods=min_periods)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def hma(series, window=200):
|
||||||
|
return hull_moving_average(series, window=window)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def vwap(bars):
|
||||||
|
"""
|
||||||
|
calculate vwap of entire time series
|
||||||
|
(input can be pandas series or numpy array)
|
||||||
|
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
||||||
|
"""
|
||||||
|
typical = ((bars['high'] + bars['low'] + bars['close']) / 3).values
|
||||||
|
volume = bars['volume'].values
|
||||||
|
|
||||||
|
return pd.Series(index=bars.index,
|
||||||
|
data=np.cumsum(volume * typical) / np.cumsum(volume))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rolling_vwap(bars, window=200, min_periods=None):
|
||||||
|
"""
|
||||||
|
calculate vwap using moving window
|
||||||
|
(input can be pandas series or numpy array)
|
||||||
|
bars are usually mid [ (h+l)/2 ] or typical [ (h+l+c)/3 ]
|
||||||
|
"""
|
||||||
|
min_periods = window if min_periods is None else min_periods
|
||||||
|
|
||||||
|
typical = ((bars['high'] + bars['low'] + bars['close']) / 3)
|
||||||
|
volume = bars['volume']
|
||||||
|
|
||||||
|
left = (volume * typical).rolling(window=window,
|
||||||
|
min_periods=min_periods).sum()
|
||||||
|
right = volume.rolling(window=window, min_periods=min_periods).sum()
|
||||||
|
|
||||||
|
return pd.Series(index=bars.index, data=(left / right))
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def rsi(series, window=14):
|
||||||
|
"""
|
||||||
|
compute the n period relative strength indicator
|
||||||
|
"""
|
||||||
|
# 100-(100/relative_strength)
|
||||||
|
deltas = np.diff(series)
|
||||||
|
seed = deltas[:window + 1]
|
||||||
|
|
||||||
|
# default values
|
||||||
|
ups = seed[seed > 0].sum() / window
|
||||||
|
downs = -seed[seed < 0].sum() / window
|
||||||
|
rsival = np.zeros_like(series)
|
||||||
|
rsival[:window] = 100. - 100. / (1. + ups / downs)
|
||||||
|
|
||||||
|
# period values
|
||||||
|
for i in range(window, len(series)):
|
||||||
|
delta = deltas[i - 1]
|
||||||
|
if delta > 0:
|
||||||
|
upval = delta
|
||||||
|
downval = 0
|
||||||
|
else:
|
||||||
|
upval = 0
|
||||||
|
downval = -delta
|
||||||
|
|
||||||
|
ups = (ups * (window - 1) + upval) / window
|
||||||
|
downs = (downs * (window - 1.) + downval) / window
|
||||||
|
rsival[i] = 100. - 100. / (1. + ups / downs)
|
||||||
|
|
||||||
|
# return rsival
|
||||||
|
return pd.Series(index=series.index, data=rsival)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def macd(series, fast=3, slow=10, smooth=16):
|
||||||
|
"""
|
||||||
|
compute the MACD (Moving Average Convergence/Divergence)
|
||||||
|
using a fast and slow exponential moving avg'
|
||||||
|
return value is emaslow, emafast, macd which are len(x) arrays
|
||||||
|
"""
|
||||||
|
macd = rolling_weighted_mean(series, window=fast) - \
|
||||||
|
rolling_weighted_mean(series, window=slow)
|
||||||
|
signal = rolling_weighted_mean(macd, window=smooth)
|
||||||
|
histogram = macd - signal
|
||||||
|
# return macd, signal, histogram
|
||||||
|
return pd.DataFrame(index=series.index, data={
|
||||||
|
'macd': macd.values,
|
||||||
|
'signal': signal.values,
|
||||||
|
'histogram': histogram.values
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def bollinger_bands(series, window=20, stds=2):
|
||||||
|
sma = rolling_mean(series, window=window)
|
||||||
|
std = rolling_std(series, window=window)
|
||||||
|
upper = sma + std * stds
|
||||||
|
lower = sma - std * stds
|
||||||
|
|
||||||
|
return pd.DataFrame(index=series.index, data={
|
||||||
|
'upper': upper,
|
||||||
|
'mid': sma,
|
||||||
|
'lower': lower
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def weighted_bollinger_bands(series, window=20, stds=2):
|
||||||
|
ema = rolling_weighted_mean(series, window=window)
|
||||||
|
std = rolling_std(series, window=window)
|
||||||
|
upper = ema + std * stds
|
||||||
|
lower = ema - std * stds
|
||||||
|
|
||||||
|
return pd.DataFrame(index=series.index, data={
|
||||||
|
'upper': upper.values,
|
||||||
|
'mid': ema.values,
|
||||||
|
'lower': lower.values
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def returns(series):
|
||||||
|
try:
|
||||||
|
res = (series / series.shift(1) -
|
||||||
|
1).replace([np.inf, -np.inf], float('NaN'))
|
||||||
|
except:
|
||||||
|
res = nans(len(series))
|
||||||
|
|
||||||
|
return pd.Series(index=series.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def log_returns(series):
|
||||||
|
try:
|
||||||
|
res = np.log(series / series.shift(1)
|
||||||
|
).replace([np.inf, -np.inf], float('NaN'))
|
||||||
|
except:
|
||||||
|
res = nans(len(series))
|
||||||
|
|
||||||
|
return pd.Series(index=series.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def implied_volatility(series, window=252):
|
||||||
|
try:
|
||||||
|
logret = np.log(series / series.shift(1)
|
||||||
|
).replace([np.inf, -np.inf], float('NaN'))
|
||||||
|
res = numpy_rolling_std(logret, window) * np.sqrt(window)
|
||||||
|
except:
|
||||||
|
res = nans(len(series))
|
||||||
|
|
||||||
|
return pd.Series(index=series.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def keltner_channel(bars, window=14, atrs=2):
|
||||||
|
typical_mean = rolling_mean(typical_price(bars), window)
|
||||||
|
atrval = atr(bars, window) * atrs
|
||||||
|
|
||||||
|
upper = typical_mean + atrval
|
||||||
|
lower = typical_mean - atrval
|
||||||
|
|
||||||
|
return pd.DataFrame(index=bars.index, data={
|
||||||
|
'upper': upper.values,
|
||||||
|
'mid': typical_mean.values,
|
||||||
|
'lower': lower.values
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def roc(series, window=14):
|
||||||
|
"""
|
||||||
|
compute rate of change
|
||||||
|
"""
|
||||||
|
res = (series - series.shift(window)) / series.shift(window)
|
||||||
|
return pd.Series(index=series.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def cci(series, window=14):
|
||||||
|
"""
|
||||||
|
compute commodity channel index
|
||||||
|
"""
|
||||||
|
price = typical_price(series)
|
||||||
|
typical_mean = rolling_mean(price, window)
|
||||||
|
res = (price - typical_mean) / (.015 * np.std(typical_mean))
|
||||||
|
return pd.Series(index=series.index, data=res)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def stoch(df, window=14, d=3, k=3, fast=False):
|
||||||
|
"""
|
||||||
|
compute the n period relative strength indicator
|
||||||
|
http://excelta.blogspot.co.il/2013/09/stochastic-oscillator-technical.html
|
||||||
|
"""
|
||||||
|
highs_ma = pd.concat([df['high'].shift(i)
|
||||||
|
for i in np.arange(window)], 1).apply(list, 1)
|
||||||
|
highs_ma = highs_ma.T.max().T
|
||||||
|
|
||||||
|
lows_ma = pd.concat([df['low'].shift(i)
|
||||||
|
for i in np.arange(window)], 1).apply(list, 1)
|
||||||
|
lows_ma = lows_ma.T.min().T
|
||||||
|
|
||||||
|
fast_k = ((df['close'] - lows_ma) / (highs_ma - lows_ma)) * 100
|
||||||
|
fast_d = numpy_rolling_mean(fast_k, d)
|
||||||
|
|
||||||
|
if fast:
|
||||||
|
data = {
|
||||||
|
'k': fast_k,
|
||||||
|
'd': fast_d
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
slow_k = numpy_rolling_mean(fast_k, k)
|
||||||
|
slow_d = numpy_rolling_mean(slow_k, d)
|
||||||
|
data = {
|
||||||
|
'k': slow_k,
|
||||||
|
'd': slow_d
|
||||||
|
}
|
||||||
|
|
||||||
|
return pd.DataFrame(index=df.index, data=data)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
def zscore(bars, window=20, stds=1, col='close'):
|
||||||
|
""" get zscore of price """
|
||||||
|
std = numpy_rolling_std(bars[col], window)
|
||||||
|
mean = numpy_rolling_mean(bars[col], window)
|
||||||
|
return (bars[col] - mean) / (std * stds)
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def pvt(bars):
|
||||||
|
""" Price Volume Trend """
|
||||||
|
pvt = ((bars['close'] - bars['close'].shift(1)) /
|
||||||
|
bars['close'].shift(1)) * bars['volume']
|
||||||
|
return pvt.cumsum()
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================
|
||||||
|
|
||||||
|
PandasObject.session = session
|
||||||
|
PandasObject.atr = atr
|
||||||
|
PandasObject.bollinger_bands = bollinger_bands
|
||||||
|
PandasObject.cci = cci
|
||||||
|
PandasObject.crossed = crossed
|
||||||
|
PandasObject.crossed_above = crossed_above
|
||||||
|
PandasObject.crossed_below = crossed_below
|
||||||
|
PandasObject.heikinashi = heikinashi
|
||||||
|
PandasObject.hull_moving_average = hull_moving_average
|
||||||
|
PandasObject.ibs = ibs
|
||||||
|
PandasObject.implied_volatility = implied_volatility
|
||||||
|
PandasObject.keltner_channel = keltner_channel
|
||||||
|
PandasObject.log_returns = log_returns
|
||||||
|
PandasObject.macd = macd
|
||||||
|
PandasObject.returns = returns
|
||||||
|
PandasObject.roc = roc
|
||||||
|
PandasObject.rolling_max = rolling_max
|
||||||
|
PandasObject.rolling_min = rolling_min
|
||||||
|
PandasObject.rolling_mean = rolling_mean
|
||||||
|
PandasObject.rolling_std = rolling_std
|
||||||
|
PandasObject.rsi = rsi
|
||||||
|
PandasObject.stoch = stoch
|
||||||
|
PandasObject.zscore = zscore
|
||||||
|
PandasObject.pvt = pvt
|
||||||
|
PandasObject.tdi = tdi
|
||||||
|
PandasObject.true_range = true_range
|
||||||
|
PandasObject.mid_price = mid_price
|
||||||
|
PandasObject.typical_price = typical_price
|
||||||
|
PandasObject.vwap = vwap
|
||||||
|
PandasObject.rolling_vwap = rolling_vwap
|
||||||
|
PandasObject.weighted_bollinger_bands = weighted_bollinger_bands
|
||||||
|
PandasObject.rolling_weighted_mean = rolling_weighted_mean
|
||||||
|
|
||||||
|
PandasObject.sma = sma
|
||||||
|
PandasObject.wma = wma
|
||||||
|
PandasObject.hma = hma
|
Loading…
Reference in New Issue
Block a user