fixed most tests and moved AWS related stuff out

This commit is contained in:
Gert Wohlgemuth
2018-06-13 15:09:06 -07:00
86 changed files with 2154 additions and 2785 deletions

View File

@@ -12,7 +12,8 @@ class DependencyException(BaseException):
class OperationalException(BaseException):
"""
Requires manual intervention.
This happens when an exchange returns an unexpected error during runtime.
This happens when an exchange returns an unexpected error during runtime
or given configuration is invalid.
"""

15
freqtrade/__main__.py Normal file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env python3
"""
__main__.py for Freqtrade
To launch Freqtrade as a module
> python -m freqtrade (with Python >= 3.6)
"""
import sys
from freqtrade import main
if __name__ == '__main__':
main.set_loggers()
main.main(sys.argv[1:])

View File

@@ -12,7 +12,7 @@ from pandas import DataFrame, to_datetime
from freqtrade import constants
from freqtrade.exchange import get_ticker_history
from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver
from freqtrade.strategy.resolver import StrategyResolver, IStrategy
logger = logging.getLogger(__name__)
@@ -37,7 +37,7 @@ class Analyze(object):
:param config: Bot configuration (use the one from Configuration())
"""
self.config = config
self.strategy = StrategyResolver(self.config).strategy
self.strategy: IStrategy = StrategyResolver(self.config).strategy
@staticmethod
def parse_ticker_dataframe(ticker: list) -> DataFrame:
@@ -62,6 +62,7 @@ class Analyze(object):
'close': 'last',
'volume': 'max',
})
frame.drop(frame.tail(1).index, inplace=True) # eliminate partial candle
return frame
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:

View File

@@ -2,24 +2,36 @@
This module contains the argument manager class
"""
import os
import argparse
import logging
import os
import re
import arrow
from typing import List, Tuple, Optional
from typing import List, Optional, NamedTuple
from freqtrade import __version__, constants
class TimeRange(NamedTuple):
"""
NamedTuple Defining timerange inputs.
[start/stop]type defines if [start/stop]ts shall be used.
if *type is none, don't use corresponding startvalue.
"""
starttype: Optional[str] = None
stoptype: Optional[str] = None
startts: int = 0
stopts: int = 0
class Arguments(object):
"""
Arguments Class. Manage the arguments received by the cli
"""
def __init__(self, args: List[str], description: str):
def __init__(self, args: List[str], description: str) -> None:
self.args = args
self.parsed_arg = None
self.parsed_arg: Optional[argparse.Namespace] = None
self.parser = argparse.ArgumentParser(description=description)
def _load_args(self) -> None:
@@ -60,7 +72,7 @@ class Arguments(object):
self.parser.add_argument(
'--version',
action='version',
version='%(prog)s {}'.format(__version__),
version=f'%(prog)s {__version__}'
)
self.parser.add_argument(
'-c', '--config',
@@ -72,9 +84,9 @@ class Arguments(object):
)
self.parser.add_argument(
'-d', '--datadir',
help='path to backtest data (default: %(default)s',
help='path to backtest data',
dest='datadir',
default=os.path.join('freqtrade', 'tests', 'testdata'),
default=None,
type=str,
metavar='PATH',
)
@@ -95,8 +107,8 @@ class Arguments(object):
)
self.parser.add_argument(
'--dynamic-whitelist',
help='dynamically generate and update whitelist \
based on 24h BaseVolume (Default 20 currencies)', # noqa
help='dynamically generate and update whitelist'
' based on 24h BaseVolume (default: %(const)s)',
dest='dynamic_whitelist',
const=constants.DYNAMIC_WHITELIST,
type=int,
@@ -104,11 +116,13 @@ class Arguments(object):
nargs='?',
)
self.parser.add_argument(
'--dry-run-db',
help='Force dry run to use a local DB "tradesv3.dry_run.sqlite" \
instead of memory DB. Work only if dry_run is enabled.',
action='store_true',
dest='dry_run_db',
'--db-url',
help='Override trades database URL, this is useful if dry_run is enabled'
' or in custom deployments (default: %(default)s)',
dest='db_url',
default=constants.DEFAULT_DB_PROD_URL,
type=str,
metavar='PATH',
)
@staticmethod
@@ -124,8 +138,8 @@ class Arguments(object):
)
parser.add_argument(
'-r', '--refresh-pairs-cached',
help='refresh the pairs files in tests/testdata with the latest data from the exchange. \
Use it if you want to run your backtesting with up-to-date data.',
help='refresh the pairs files in tests/testdata with the latest data from the '
'exchange. Use it if you want to run your backtesting with up-to-date data.',
action='store_true',
dest='refresh_pairs',
)
@@ -137,9 +151,25 @@ class Arguments(object):
default=None,
dest='export',
)
parser.add_argument(
'--export-filename',
help='Save backtest results to this filename \
requires --export to be set as well\
Example --export-filename=user_data/backtest_data/backtest_today.json\
(default: %(default)s)',
type=str,
default=os.path.join('user_data', 'backtest_data', 'backtest-result.json'),
dest='exportfilename',
metavar='PATH',
)
@staticmethod
def optimizer_shared_options(parser: argparse.ArgumentParser) -> None:
"""
Parses given common arguments for Backtesting and Hyperopt scripts.
:param parser:
:return:
"""
parser.add_argument(
'-i', '--ticker-interval',
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
@@ -215,17 +245,20 @@ class Arguments(object):
logging.warn("no hyper opt found - skipping support for it")
@staticmethod
def parse_timerange(text: str) -> Optional[Tuple[List, int, int]]:
def parse_timerange(text: Optional[str]) -> TimeRange:
"""
Parse the value of the argument --timerange to determine what is the range desired
:param text: value from --timerange
:return: Start and End range period
"""
if text is None:
return None
return TimeRange(None, None, 0, 0)
syntax = [(r'^-(\d{8})$', (None, 'date')),
(r'^(\d{8})-$', ('date', None)),
(r'^(\d{8})-(\d{8})$', ('date', 'date')),
(r'^-(\d{10})$', (None, 'date')),
(r'^(\d{10})-$', ('date', None)),
(r'^(\d{10})-(\d{10})$', ('date', 'date')),
(r'^(-\d+)$', (None, 'line')),
(r'^(\d+)-$', ('line', None)),
(r'^(\d+)-(\d+)$', ('index', 'index'))]
@@ -235,22 +268,24 @@ class Arguments(object):
if match: # Regex has matched
rvals = match.groups()
index = 0
start = None
stop = None
start: int = 0
stop: int = 0
if stype[0]:
start = rvals[index]
starts = rvals[index]
if stype[0] == 'date':
start = arrow.get(start, 'YYYYMMDD').timestamp
start = int(starts) if len(starts) == 10 \
else arrow.get(starts, 'YYYYMMDD').timestamp
else:
start = int(start)
start = int(starts)
index += 1
if stype[1]:
stop = rvals[index]
stops = rvals[index]
if stype[1] == 'date':
stop = arrow.get(stop, 'YYYYMMDD').timestamp
stop = int(stops) if len(stops) == 10 \
else arrow.get(stops, 'YYYYMMDD').timestamp
else:
stop = int(stop)
return stype, start, stop
stop = int(stops)
return TimeRange(stype[0], stype[1], start, stop)
raise Exception('Incorrect syntax for timerange "%s"' % text)
def scripts_options(self) -> None:
@@ -351,13 +386,6 @@ class Arguments(object):
type=int
)
self.parser.add_argument(
'-db', '--db-url',
help='Show trades stored in database.',
dest='db_url',
default=None
)
def testdata_dl_options(self) -> None:
"""
Parses given arguments for testdata download
@@ -366,25 +394,42 @@ class Arguments(object):
'--pairs-file',
help='File containing a list of pairs to download',
dest='pairs_file',
default=None
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--export',
help='Export files to given dir',
dest='export',
default=None)
default=None,
metavar='PATH',
)
self.parser.add_argument(
'--days',
help='Download data for number of days',
dest='days',
type=int,
default=None)
metavar='INT',
default=None
)
self.parser.add_argument(
'--exchange',
help='Exchange name',
help='Exchange name (default: %(default)s)',
dest='exchange',
type=str,
default='bittrex')
default='bittrex'
)
self.parser.add_argument(
'-t', '--timeframes',
help='Specify which tickers to download. Space separated list. \
Default: %(default)s',
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
'6h', '8h', '12h', '1d', '3d', '1w'],
default=['1m', '5m'],
nargs='+',
dest='timeframes',
)

View File

@@ -1,413 +0,0 @@
import datetime
import logging
import os
import tempfile
from base64 import urlsafe_b64encode
import requests
import simplejson as json
from requests import post
from freqtrade.optimize.backtesting import Backtesting
def backtest(event, context):
"""
this method is running on the AWS server
and back tests this application for us
and stores the back testing results in a local database
this event can be given as:
:param event:
{
'strategy' : 'url handle where we can find the strategy'
'stake_currency' : 'our desired stake currency'
'asset' : '[] asset we are interested in.
'username' : user who's strategy should be evaluated
'name' : name of the strategy we want to evaluate
'exchange' : name of the exchange we should be using
}
it should be invoked by SNS only to avoid abuse of the system!
:param context:
standard AWS context, so pleaes ignore for now!
:return:
no return
"""
if 'Records' in event:
for x in event['Records']:
if 'Sns' in x and 'Message' in x['Sns']:
event['body'] = json.loads(x['Sns']['Message'])
name = event['body']['name']
user = event['body']['user']
days = [90]
if 'days' in event['body']:
days = event['body']['days']
# by default we refresh data
refresh = True
if 'refresh' in event['body']:
refresh = event['body']['refresh']
try:
if "ticker" in event['body']:
ticker = event['body']['ticker']
else:
ticker = ['5m']
if "local" in event['body'] and event['body']['local']:
print("running in local mode")
for x in days:
for y in ticker:
till = datetime.datetime.today()
fromDate = till - datetime.timedelta(days=x)
configuration = generate_configuration(fromDate, till, name, refresh, user, False)
run_backtest(configuration, name, user, y, fromDate, till)
else:
print("running in remote mode")
_submit_job(name, user, ticker, days)
return {
"statusCode": 200
}
except ImportError as e:
return {
"statusCode": 500,
"body": json.dumps({"error": e})
}
else:
raise Exception("not a valid event: {}".format(event))
def _submit_job(name, user, ticker, days):
"""
submits a new task to the cluster
:param configuration:
:param user:
:return:
"""
import boto3
overrides = {"containerOverrides": [{
"name": "freqtrade-backtest",
"environment": [
{
"name": "FREQ_USER",
"value": "{}".format(user)
},
{
"name": "FREQ_TICKER",
"value": "{}".format(json.dumps(ticker))
},
{
"name": "FREQ_DAYS",
"value": "{}".format(json.dumps(days, use_decimal=False))
},
{
"name": "FREQ_STRATEGY",
"value": "{}".format(name)
}
]
}]}
print(overrides)
# fire AWS fargate instance now
# run_backtest(configuration, name, user)
# kinda ugly right now and needs more customization
client = boto3.client('ecs')
response = client.run_task(
cluster=os.environ.get('FREQ_CLUSTER_NAME', 'fargate'), # name of the cluster
launchType='FARGATE',
taskDefinition=os.environ.get('FREQ_TASK_NAME', 'freqtrade-backtesting:2'),
count=1,
platformVersion='LATEST',
networkConfiguration={
'awsvpcConfiguration': {
'subnets': [
# we need at least 2, to insure network stability
os.environ.get('FREQ_SUBNET_1', 'subnet-c35bdcab'),
os.environ.get('FREQ_SUBNET_2', 'subnet-be46b9c4'),
os.environ.get('FREQ_SUBNET_3', 'subnet-234ab559'),
os.environ.get('FREQ_SUBNET_4', 'subnet-234ab559')],
'assignPublicIp': 'ENABLED'
}
},
overrides=overrides,
)
return response
def run_backtest(configuration, name, user, interval, fromDate, till):
"""
this backtests the specified evaluation
:param configuration:
:param name:
:param user:
:param interval:
:param timerange:
:return:
"""
timerange = (till - fromDate).days
configuration['ticker_interval'] = interval
backtesting = Backtesting(configuration)
result = backtesting.start()
# store individual trades - not really needed
# _store_trade_data(interval, name, result, timerange, user)
# store aggregated values
_store_aggregated_data(interval, name, result, timerange, user)
return result
def _store_aggregated_data(interval, name, result, timerange, user):
"""
stores aggregated data for ease of access, yay for dynamodb data duplication...
:param interval:
:param name:
:param result:
:param timerange:
:param user:
:return:
"""
submit_data = []
for row in result[1][2]:
if row[1] > 0:
data = {
"pair": row[0],
"trades": row[1],
"losses": row[7],
"wins": row[6],
"duration": row[5],
"profit_mean_percent": row[2],
"profit_cum_percent": row[3],
"daily_return": row[3] / timerange,
"strategy": name,
"user": user,
"ticker": interval,
"days": timerange
}
# aggregate by pair + interval + time range for each strategy
data['id'] = "aggregate:{}:{}:{}:test".format(row[0].upper(), interval, timerange)
data['trade'] = "{}.{}".format(user, name)
submit_data.append(data.copy())
# id: aggregate by strategy + user + range + pair
# range: ticker
# allows us to easily see on which ticker the strategy works best
data['id'] = "aggregate:ticker:{}:{}:{}:{}:test".format(user, name, row[0].upper(), timerange)
data['trade'] = "{}".format(interval)
submit_data.append(data.copy())
# id: aggregate by strategy + user + ticker + pair
# range: timerange
# allows us to easily see on which time range the strategy works best
data['id'] = "aggregate:timerange:{}:{}:{}:{}:test".format(user, name, row[0].upper(), interval)
data['trade'] = "{}".format(timerange)
submit_data.append(data.copy())
_submit_to_remote(submit_data)
def _submit_to_remote(data):
"""
submits data to the backend to be persisted in the database
:param data:
:return:
"""
try:
print("submitting data: {}".format(data))
print(
post("{}/trade".format(os.environ.get('BASE_URL', 'https://freq.isaac.international/dev')),
json=data))
except Exception as e:
print("submission ignored: {}".format(e))
def _store_trade_data(interval, name, result, timerange, user):
"""
stores individual trades on the remote system
:param interval:
:param name:
:param result:
:param timerange:
:param user:
:return:
"""
submit_data = []
for index, row in result[0].iterrows():
submit_data.append({
"id": "{}.{}:{}:{}:{}:test".format(user, name, interval, timerange, row['currency'].upper()),
"trade": "{} to {}".format(row['entry'].strftime('%Y-%m-%d %H:%M:%S'),
row['exit'].strftime('%Y-%m-%d %H:%M:%S')),
"pair": row['currency'],
"duration": row['duration'],
"profit_percent": row['profit_percent'],
"profit_stake": row['profit_BTC'],
"entry_date": row['entry'].strftime('%Y-%m-%d %H:%M:%S'),
"exit_date": row['exit'].strftime('%Y-%m-%d %H:%M:%S'),
"strategy": name,
"user": user
})
_submit_to_remote(submit_data)
def generate_configuration(fromDate, till, name, refresh, user, remote=True):
"""
generates the configuration for us on the fly for a given
strategy. This is loaded from a remote url if specfied or
the internal dynamodb
:param event:
:param fromDate:
:param name:
:param response:
:param till:
:return:
"""
response = {}
if remote:
print("using remote mode to query strategy details")
response = requests.get(
"{}/strategies/{}/{}".format(os.environ.get('BASE_URL', "https://freq.isaac.international/dev"), user,
name)).json()
# load associated content right now this only works for public strategies obviously TODO
content = requests.get(
"{}/strategies/{}/{}/code".format(os.environ.get('BASE_URL', "https://freq.isaac.international/dev"), user,
name)).content
response['content'] = urlsafe_b64encode(content).decode()
print(content)
else:
print("using local mode to query strategy details")
from boto3.dynamodb.conditions import Key
from freqtrade.aws.tables import get_strategy_table
table = get_strategy_table()
response = table.query(
KeyConditionExpression=Key('user').eq(user) &
Key('name').eq(name)
)['Items'][0]
print(response)
content = response['content']
configuration = {
"max_open_trades": 1,
"stake_currency": response['stake_currency'].upper(),
"stake_amount": 1,
"fiat_display_currency": "USD",
"unfilledtimeout": 600,
"trailing_stop": response['trailing_stop'],
"bid_strategy": {
"ask_last_balance": 0.0
},
"exchange": {
"name": response['exchange'],
"enabled": True,
"key": "key",
"secret": "secret",
"pair_whitelist": list(
map(lambda x: "{}/{}".format(x, response['stake_currency']).upper(),
response['assets']))
},
"telegram": {
"enabled": False,
"token": "token",
"chat_id": "0"
},
"initial_state": "running",
"datadir": tempfile.gettempdir(),
"experimental": {
"use_sell_signal": response['use_sell'],
"sell_profit_only": True
},
"internals": {
"process_throttle_secs": 5
},
'realistic_simulation': True,
"loglevel": logging.INFO,
"strategy": "{}:{}".format(name, content),
"timerange": "{}-{}".format(fromDate.strftime('%Y%m%d'), till.strftime('%Y%m%d')),
"refresh_pairs": refresh
}
return configuration
def cron(event, context):
"""
this functions submits all strategies to the backtesting queue
:param event:
:param context:
:return:
"""
import boto3
# if topic exists, we just reuse it
client = boto3.client('sns')
topic_arn = client.create_topic(Name=os.environ['topic'])['TopicArn']
message = {
"local": False,
"refresh": True,
"ticker": ['5m', '15m', '30m', '1h', '2h', '4h', '6h', '12h', '1d'],
"days": [1, 2, 3, 4, 5, 6, 7, 14, 30, 90]
}
print("submitting: {}".format(message))
serialized = json.dumps(message, use_decimal=True)
# submit item to queue for routing to the correct persistence
result = client.publish(
TopicArn=topic_arn,
Message=json.dumps({'default': serialized}),
Subject="schedule",
MessageStructure='json'
)
print(result)
return {
"statusCode": 200
}
if __name__ == "__main__":
till = datetime.datetime.today()
fromDate = till - datetime.timedelta(days=90)
print(_submit_job(
"BinHV45",
"GBPAQEFGGWCMWVFU34PMVGS4P2NJR4IDFNVI4LTCZAKJAD3JCXUMBI4J",
"5m",
fromDate,
till
))

View File

@@ -1,14 +0,0 @@
def fetch_pairs (events, context):
"""
fetches the pairs for the given exchange name and currency
requires:
name: name of the exchange
stake_currency: name of the stake currency
:param events:
:param context:
:return:
"""

View File

@@ -1,6 +0,0 @@
# Defined the default HTTP headers for function responses
__HTTP_HEADERS__ = {
'Access-Control-Allow-Origin' : '*',
'Access-Control-Allow-Credentials' : True
}

View File

@@ -1,91 +0,0 @@
# defines the schema to submit a new strategy to the system
__SUBMIT_STRATEGY_SCHEMA__ = {
"$id": "http://example.com/example.json",
"type": "object",
"definitions": {},
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"user": {
"$id": "/properties/user",
"type": "string",
"title": "The User Schema ",
"default": "",
"examples": [
"GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG"
]
},
"description": {
"$id": "/properties/description",
"type": "string",
"title": "The Description Schema ",
"default": "",
"examples": [
"simple test strategy"
]
},
"exchange": {
"$id": "/properties/exchange",
"type": "object",
"properties": {
"name": {
"$id": "/properties/exchange/properties/name",
"type": "string",
"title": "The Name Schema ",
"default": "",
"examples": [
"binance"
]
},
"stake": {
"$id": "/properties/exchange/properties/stake",
"type": "string",
"title": "The Stake Schema ",
"default": "",
"examples": [
"usdt"
]
},
"pairs": {
"$id": "/properties/exchange/properties/pairs",
"type": "array",
"items": {
"$id": "/properties/exchange/properties/pairs/items",
"type": "string",
"title": "The 0th Schema ",
"default": "",
"examples": [
"btc/usdt"
]
}
}
}
},
"name": {
"$id": "/properties/name",
"type": "string",
"title": "The Name Schema ",
"default": "",
"examples": [
"MyFancyTestStrategy"
]
},
"content": {
"$id": "/properties/content",
"type": "string",
"title": "The Content Schema ",
"default": "",
"examples": [
"IyAtLS0gRG8gbm90IHJlbW92ZSB0aGVzZSBsaWJzIC0tLQpmcm9tIGZyZXF0cmFkZS5zdHJhdGVneS5pbnRlcmZhY2UgaW1wb3J0IElTdHJhdGVneQpmcm9tIHR5cGluZyBpbXBvcnQgRGljdCwgTGlzdApmcm9tIGh5cGVyb3B0IGltcG9ydCBocApmcm9tIGZ1bmN0b29scyBpbXBvcnQgcmVkdWNlCmZyb20gcGFuZGFzIGltcG9ydCBEYXRhRnJhbWUKIyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQoKaW1wb3J0IHRhbGliLmFic3RyYWN0IGFzIHRhCmltcG9ydCBmcmVxdHJhZGUudmVuZG9yLnF0cHlsaWIuaW5kaWNhdG9ycyBhcyBxdHB5bGliCgpjbGFzcyBNeUZhbmN5VGVzdFN0cmF0ZWd5KElTdHJhdGVneSk6CiAgICBtaW5pbWFsX3JvaSA9IHsKICAgICAgICAiMCI6IDAuNQogICAgfQogICAgc3RvcGxvc3MgPSAtMC4yCiAgICB0aWNrZXJfaW50ZXJ2YWwgPSAnNW0nCgogICAgZGVmIHBvcHVsYXRlX2luZGljYXRvcnMoc2VsZiwgZGF0YWZyYW1lOiBEYXRhRnJhbWUpIC0-IERhdGFGcmFtZToKICAgICAgICBtYWNkID0gdGEuTUFDRChkYXRhZnJhbWUpCiAgICAgICAgZGF0YWZyYW1lWydtYVNob3J0J10gPSB0YS5FTUEoZGF0YWZyYW1lLCB0aW1lcGVyaW9kPTgpCiAgICAgICAgZGF0YWZyYW1lWydtYU1lZGl1bSddID0gdGEuRU1BKGRhdGFmcmFtZSwgdGltZXBlcmlvZD0yMSkKICAgICAgICByZXR1cm4gZGF0YWZyYW1lCgogICAgZGVmIHBvcHVsYXRlX2J1eV90cmVuZChzZWxmLCBkYXRhZnJhbWU6IERhdGFGcmFtZSkgLT4gRGF0YUZyYW1lOgogICAgICAgIGRhdGFmcmFtZS5sb2NbCiAgICAgICAgICAgICgKICAgICAgICAgICAgICAgIHF0cHlsaWIuY3Jvc3NlZF9hYm92ZShkYXRhZnJhbWVbJ21hU2hvcnQnXSwgZGF0YWZyYW1lWydtYU1lZGl1bSddKQogICAgICAgICAgICApLAogICAgICAgICAgICAnYnV5J10gPSAxCgogICAgICAgIHJldHVybiBkYXRhZnJhbWUKCiAgICBkZWYgcG9wdWxhdGVfc2VsbF90cmVuZChzZWxmLCBkYXRhZnJhbWU6IERhdGFGcmFtZSkgLT4gRGF0YUZyYW1lOgogICAgICAgIGRhdGFmcmFtZS5sb2NbCiAgICAgICAgICAgICgKICAgICAgICAgICAgICAgIHF0cHlsaWIuY3Jvc3NlZF9hYm92ZShkYXRhZnJhbWVbJ21hTWVkaXVtJ10sIGRhdGFmcmFtZVsnbWFTaG9ydCddKQogICAgICAgICAgICApLAogICAgICAgICAgICAnc2VsbCddID0gMQogICAgICAgIHJldHVybiBkYXRhZnJhbWUKCgogICAgICAgIA=="
]
},
"public": {
"$id": "/properties/public",
"type": "boolean",
"title": "The Public Schema ",
"default": False,
"examples": [
False
]
}
}
}

View File

@@ -1,278 +0,0 @@
import os
import time
from base64 import urlsafe_b64decode, urlsafe_b64encode
import boto3
import simplejson as json
from boto3.dynamodb.conditions import Key, Attr
from jsonschema import validate
from freqtrade.aws.schemas import __SUBMIT_STRATEGY_SCHEMA__
from freqtrade.aws.tables import get_strategy_table, get_trade_table
from freqtrade.strategy.resolver import StrategyResolver
import requests
db = boto3.resource('dynamodb')
from freqtrade.aws.headers import __HTTP_HEADERS__
def names(event, context):
"""
returns the names of all registered strategies, both public and private
:param event:
:param context:
:return:
"""
table = get_strategy_table()
response = table.scan()
result = response['Items']
# no pagination here
while 'LastEvaluatedKey' in response:
for i in response['Items']:
result.append(i)
response = table.scan(
ExclusiveStartKey=response['LastEvaluatedKey']
)
# map results and hide informations
data = list(map(lambda x: {'name': x['name'], 'public': x['public'], 'user': x['user']}, result))
return {
"headers": __HTTP_HEADERS__,
"statusCode": 200,
"body": json.dumps(data)
}
def get(event, context):
"""
returns the code of the requested strategy, if it's public
:param event:
:param context:
:return:
"""
assert 'pathParameters' in event
assert 'user' in event['pathParameters']
assert 'name' in event['pathParameters']
table = get_strategy_table()
response = table.query(
KeyConditionExpression=Key('user').eq(event['pathParameters']['user']) &
Key('name').eq(event['pathParameters']['name'])
)
if "Items" in response and len(response['Items']) > 0:
item = response['Items'][0]
# content is private...
item.pop('content')
return {
"headers": __HTTP_HEADERS__,
"statusCode": response['ResponseMetadata']['HTTPStatusCode'],
"body": json.dumps(item)
}
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": 404,
"body": json.dumps(response)
}
def code(event, context):
"""
returns the code of the requested strategy, if it's public
:param event:
:param context:
:return:
"""
user = ""
name = ""
# proxy based handling
if 'pathParameters' in event:
assert 'user' in event['pathParameters']
assert 'name' in event['pathParameters']
user = event['pathParameters']['user']
name = event['pathParameters']['name']
# plain lambda handling
elif 'path' in event:
assert 'user' in event['path']
assert 'name' in event['path']
user = event['path']['user']
name = event['path']['name']
table = get_strategy_table()
response = table.query(
KeyConditionExpression=Key('user').eq(user) &
Key('name').eq(name)
)
if "Items" in response and len(response['Items']) > 0:
if response['Items'][0]["public"]:
content = urlsafe_b64decode(response['Items'][0]['content']).decode('utf-8')
content["headers"]: __HTTP_HEADERS__
return content
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": 403,
"body": json.dumps({"success": False, "reason": "Denied"})
}
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": response['ResponseMetadata']['HTTPStatusCode'],
"body": json.dumps(response)
}
def submit(event, context):
"""
compiles the given strategy and stores it in the internal database
:param event:
:param context:
:return:
"""
# print(event)
# get data
data = json.loads(event['body'])
# print("received data")
# validate against schema
result = validate(data, __SUBMIT_STRATEGY_SCHEMA__)
# print("data are validated");
# print(result)
# validate that the user is an Isaac User
# ToDo
result = __evaluate(data)
return {
"headers": __HTTP_HEADERS__,
"statusCode": result['ResponseMetadata']['HTTPStatusCode'],
"body": json.dumps(result)
}
def __evaluate(data):
"""
evaluates the given data object and submits it to the system
for persistence
0
:param data:
:return:
"""
strategy = urlsafe_b64decode(data['content']).decode('utf-8')
# comment out hyper opt references, they are no supported here
# due to lambda size limitations
strategy = "\n".join(
list(
map(
lambda x: "#{} # this version does not support hyperopt!".format(x) if "hyperopt" in x else x,
strategy.split("\n"))))
print("loaded strategy")
print(strategy)
# try to load the strategy
strat = StrategyResolver().compile(data['name'], strategy)
data['time'] = int(time.time() * 1000)
data['type'] = "strategy"
data['roi'] = strat.minimal_roi
data['stoploss'] = strat.stoploss
# ensure that the modified file is saved
data['content'] = urlsafe_b64encode(strategy.encode('utf-8'))
# default variables if not provided
if 'trailing_stop' not in data:
data['trailing_stop'] = False
if 'stake_currency' not in data:
data['stake_currency'] = "USDT"
if 'use_sell' not in data:
data['use_sell'] = True
if 'exchange' not in data:
data['exchange'] = 'binance'
if 'assets' not in data:
data['assets'] = ["BTC", "ETH", "LTC"]
# force serialization to deal with decimal number
data = json.dumps(data, use_decimal=True)
data = json.loads(data, use_decimal=True)
table = get_strategy_table()
result = table.put_item(Item=data)
return result
def submit_github(event, context):
"""
there has been a push to our github repository, so let's
update all the strategies.
The user account will be the provided secret
:param event:
:param context:
:return:
"""
print("download all strategies and updating the system")
result = requests.get(
"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/git/trees/master?recursive=1").json()
if 'tree' in result:
strategies = 0
for x in result['tree']:
if x['path'].endswith(".py") and x['type'] == 'blob':
file = requests.get(x['url']).json()
if "content" in file:
# assemble submit object
# generate simple id
# submit it - we should be able to support multiple repositories
# maybe another database table, where we can map these?
try:
__evaluate({
"name": x['path'].split("/")[-1].split(".py")[0],
"content": file['content'],
"user": "GBPAQEFGGWCMWVFU34PMVGS4P2NJR4IDFNVI4LTCZAKJAD3JCXUMBI4J",
"public": True,
"description": "imported from github repository: berlinguyinca/freqtrade-trading-strategies"
})
strategies = strategies + 1
except ImportError as e:
print("error: {}".format(e))
print("imported/updated: {} strategies".format(strategies))
return {
"headers": __HTTP_HEADERS__,
"statusCode": 200,
"body": json.dumps({"imported": strategies})
}
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": 404,
"body": json.dumps({"error": result})
}

View File

@@ -1,98 +0,0 @@
import os
import boto3
db = boto3.resource('dynamodb')
def get_trade_table():
"""
provides access to the trade table and if it doesn't exists
creates it for us
:return:
"""
if 'tradeTable' not in os.environ:
os.environ['tradeTable'] = "FreqTradeTable"
table_name = os.environ['tradeTable']
existing_tables = boto3.client('dynamodb').list_tables()['TableNames']
if table_name not in existing_tables:
try:
db.create_table(
TableName=table_name,
KeySchema=[
{
'AttributeName': 'id',
'KeyType': 'HASH'
},
{
'AttributeName': 'trade',
'KeyType': 'RANGE'
}
],
AttributeDefinitions=[
{
'AttributeName': 'id',
'AttributeType': 'S'
}, {
'AttributeName': 'trade',
'AttributeType': 'S'
}
],
ProvisionedThroughput={
'ReadCapacityUnits': 1,
'WriteCapacityUnits': 1
}
)
except Exception as e:
print("table already exist {}".format(e))
return db.Table(table_name)
def get_strategy_table():
"""
provides us access to the strategy table and if it doesn't exists creates it for us
:return:
"""
if 'strategyTable' not in os.environ:
os.environ['strategyTable'] = "FreqStrategyTable"
table_name = os.environ['strategyTable']
existing_tables = boto3.client('dynamodb').list_tables()['TableNames']
existing_tables = boto3.client('dynamodb').list_tables()['TableNames']
if table_name not in existing_tables:
try:
db.create_table(
TableName=table_name,
KeySchema=[
{
'AttributeName': 'user',
'KeyType': 'HASH'
},
{
'AttributeName': 'name',
'KeyType': 'RANGE'
}
],
AttributeDefinitions=[
{
'AttributeName': 'user',
'AttributeType': 'S'
}, {
'AttributeName': 'name',
'AttributeType': 'S'
}
],
ProvisionedThroughput={
'ReadCapacityUnits': 1,
'WriteCapacityUnits': 1
}
)
except Exception as e:
print("table already exist {}".format(e))
return db.Table(table_name)

View File

@@ -1,164 +0,0 @@
import datetime
from time import sleep
import boto3
import simplejson as json
import os
from freqtrade.aws.tables import get_trade_table, get_strategy_table
from boto3.dynamodb.conditions import Key, Attr
from freqtrade.aws.headers import __HTTP_HEADERS__
def store(event, context):
"""
stores the received data in the internal database
:param data:
:return:
"""
if 'Records' in event:
for x in event['Records']:
if 'Sns' in x and 'Message' in x['Sns']:
data = json.loads(x['Sns']['Message'], use_decimal=True)
print("storing {} data trade results".format(len(x)))
for x in data:
x['ttl'] = int((datetime.datetime.today() + datetime.timedelta(days=1)).timestamp())
print("storing data: {}".format(x))
sleep(0.5) # throttle to not overwhelm the DB, lambda is cheaper than dynamo
get_trade_table().put_item(Item=x)
def submit(event, context):
"""
submits a new trade to be registered in the internal queue system
:param event:
:param context:
:return:
"""
print(event)
data = json.loads(event['body'])
client = boto3.client('sns')
topic_arn = client.create_topic(Name=os.environ['tradeTopic'])['TopicArn']
result = client.publish(
TopicArn=topic_arn,
Message=json.dumps({'default': json.dumps(data, use_decimal=True)}),
Subject="persist data",
MessageStructure='json'
)
return {
"headers": __HTTP_HEADERS__,
"statusCode": 200,
"body": json.dumps(result)
}
def get_aggregated_trades(event, context):
"""
returns the aggregated trades for the given key combination
:param event:
:param context:
:return:
"""
assert 'pathParameters' in event
assert 'ticker' in event['pathParameters']
assert 'days' in event['pathParameters']
table = get_trade_table()
response = table.query(
KeyConditionExpression=Key('id').eq(
"aggregate:{}:{}:{}:test".format(
"TOTAL",
event['pathParameters']['ticker'],
event['pathParameters']['days']
)
)
)
if "Items" in response and len(response['Items']) > 0:
# preparation for pagination
# TODO include in parameters an optional
# start key ExclusiveStartKey=response['LastEvaluatedKey']
data = {
"headers": __HTTP_HEADERS__,
"result": response['Items'],
"paginationKey": response.get('LastEvaluatedKey')
}
return {
"headers": __HTTP_HEADERS__,
"statusCode": response['ResponseMetadata']['HTTPStatusCode'],
"body": json.dumps(data)
}
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": 404,
"body": json.dumps({
"error": "sorry this query did not produce any results",
"event": event
})
}
def get_trades(event, context):
"""
this function returns all the known trades for a user, strategy and pair
:param event:
:param context:
:return:
"""
assert 'pathParameters' in event
assert 'user' in event['pathParameters']
assert 'name' in event['pathParameters']
assert 'stake' in event['pathParameters']
assert 'asset' in event['pathParameters']
table = get_trade_table()
response = table.query(
KeyConditionExpression=Key('id').eq(
"{}.{}:{}/{}".format(
event['pathParameters']['user'],
event['pathParameters']['name'],
event['pathParameters']['asset'].upper(),
event['pathParameters']['stake'].upper()
)
)
)
if "Items" in response and len(response['Items']) > 0:
# preparation for pagination
# TODO include in parameters an optional
# start key ExclusiveStartKey=response['LastEvaluatedKey']
#
# data = {
# "result": response['Items'],
# "paginationKey": response.get('LastEvaluatedKey')
# }
return {
"headers": __HTTP_HEADERS__,
"statusCode": response['ResponseMetadata']['HTTPStatusCode'],
"body": response['Items']
}
else:
return {
"headers": __HTTP_HEADERS__,
"statusCode": 404,
"body": json.dumps({
"error": "sorry this query did not produce any results",
"event": event
})
}

View File

@@ -1,11 +1,11 @@
"""
This module contains the configuration class
"""
import os
import json
import logging
from argparse import Namespace
from typing import Dict, Any
from typing import Optional, Dict, Any
from jsonschema import Draft4Validator, validate
from jsonschema.exceptions import ValidationError, best_match
import ccxt
@@ -23,7 +23,7 @@ class Configuration(object):
"""
def __init__(self, args: Namespace) -> None:
self.args = args
self.config = None
self.config: Optional[Dict[str, Any]] = None
def load_config(self) -> Dict[str, Any]:
"""
@@ -61,11 +61,9 @@ class Configuration(object):
with open(path) as file:
conf = json.load(file)
except FileNotFoundError:
logger.critical(
'Config file "%s" not found. Please create your config file',
path
)
exit(0)
raise OperationalException(
'Config file "{}" not found!'
' Please create a config file or check whether it exists.'.format(path))
if 'internals' not in conf:
conf['internals'] = {}
@@ -97,22 +95,35 @@ class Configuration(object):
'(not applicable with Backtesting and Hyperopt)'
)
# Add dry_run_db if found and the bot in dry run
if self.args.dry_run_db and config.get('dry_run', False):
config.update({'dry_run_db': True})
logger.info('Parameter --dry-run-db detected ...')
if self.args.db_url != constants.DEFAULT_DB_PROD_URL:
config.update({'db_url': self.args.db_url})
logger.info('Parameter --db-url detected ...')
if config.get('dry_run_db', False):
if config.get('dry_run', False):
logger.info('Dry_run will use the DB file: "tradesv3.dry_run.sqlite"')
else:
logger.info('Dry run is disabled. (--dry_run_db ignored)')
if config.get('dry_run', False):
logger.info('Dry run is enabled')
if config.get('db_url') in [None, constants.DEFAULT_DB_PROD_URL]:
# Default to in-memory db for dry_run if not specified
config['db_url'] = constants.DEFAULT_DB_DRYRUN_URL
else:
if not config.get('db_url', None):
config['db_url'] = constants.DEFAULT_DB_PROD_URL
logger.info('Dry run is disabled')
logger.info('Using DB: "{}"'.format(config['db_url']))
# Check if the exchange set by the user is supported
self.check_exchange(config)
return config
def _create_default_datadir(self, config: Dict[str, Any]) -> str:
exchange_name = config.get('exchange', {}).get('name').lower()
default_path = os.path.join('user_data', 'data', exchange_name)
if not os.path.isdir(default_path):
os.makedirs(default_path)
logger.info(f'Created data directory: {default_path}')
return default_path
def _load_backtesting_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
"""
Extract information for sys.argv and load Backtesting configuration
@@ -145,7 +156,9 @@ class Configuration(object):
# If --datadir is used we add it to the configuration
if 'datadir' in self.args and self.args.datadir:
config.update({'datadir': self.args.datadir})
logger.info('Parameter --datadir detected: %s ...', self.args.datadir)
else:
config.update({'datadir': self._create_default_datadir(config)})
logger.info('Using data folder: %s ...', config.get('datadir'))
# If -r/--refresh-pairs-cached is used we add it to the configuration
if 'refresh_pairs' in self.args and self.args.refresh_pairs:
@@ -157,6 +170,11 @@ class Configuration(object):
config.update({'export': self.args.export})
logger.info('Parameter --export detected: %s ...', self.args.export)
# If --export-filename is used we add it to the configuration
if 'export' in config and 'exportfilename' in self.args and self.args.exportfilename:
config.update({'exportfilename': self.args.exportfilename})
logger.info('Storing backtest results to %s ...', self.args.exportfilename)
return config
def _load_hyperopt_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
@@ -192,7 +210,7 @@ class Configuration(object):
validate(conf, constants.CONF_SCHEMA)
return conf
except ValidationError as exception:
logger.fatal(
logger.critical(
'Invalid configuration. See config.json.example. Reason: %s',
exception
)
@@ -218,9 +236,8 @@ class Configuration(object):
exchange = config.get('exchange', {}).get('name').lower()
if exchange not in ccxt.exchanges:
exception_msg = 'Exchange "{}" not supported.\n' \
'The following exchanges are supported: {}'\
.format(exchange, ', '.join(ccxt.exchanges))
exception_msg = f'Exchange "{exchange}" not supported.\n' \
f'The following exchanges are supported: {", ".join(ccxt.exchanges)}'
logger.critical(exception_msg)
raise OperationalException(

View File

@@ -9,9 +9,12 @@ TICKER_INTERVAL = 5 # min
HYPEROPT_EPOCH = 100 # epochs
RETRY_TIMEOUT = 30 # sec
DEFAULT_STRATEGY = 'DefaultStrategy'
DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite://'
TICKER_INTERVAL_MINUTES = {
'1m': 1,
'3m': 3,
'5m': 5,
'15m': 15,
'30m': 30,
@@ -19,11 +22,20 @@ TICKER_INTERVAL_MINUTES = {
'2h': 120,
'4h': 240,
'6h': 360,
'8h': 480,
'12h': 720,
'1d': 1440,
'3d': 4320,
'1w': 10080,
}
SUPPORTED_FIAT = [
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD",
"BTC", "ETH", "XRP", "LTC", "BCH", "USDT"
]
# Required json-schema for user specified config
CONF_SCHEMA = {
@@ -31,16 +43,9 @@ CONF_SCHEMA = {
'properties': {
'max_open_trades': {'type': 'integer', 'minimum': 0},
'ticker_interval': {'type': 'string', 'enum': list(TICKER_INTERVAL_MINUTES.keys())},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT']},
'stake_currency': {'type': 'string', 'enum': ['BTC', 'ETH', 'USDT', 'EUR', 'USD']},
'stake_amount': {'type': 'number', 'minimum': 0.0005},
'fiat_display_currency': {'type': 'string', 'enum': ['AUD', 'BRL', 'CAD', 'CHF',
'CLP', 'CNY', 'CZK', 'DKK',
'EUR', 'GBP', 'HKD', 'HUF',
'IDR', 'ILS', 'INR', 'JPY',
'KRW', 'MXN', 'MYR', 'NOK',
'NZD', 'PHP', 'PKR', 'PLN',
'RUB', 'SEK', 'SGD', 'THB',
'TRY', 'TWD', 'ZAR', 'USD']},
'fiat_display_currency': {'type': 'string', 'enum': SUPPORTED_FIAT},
'dry_run': {'type': 'boolean'},
'minimal_roi': {
'type': 'object',
@@ -60,9 +65,19 @@ CONF_SCHEMA = {
'maximum': 1,
'exclusiveMaximum': False
},
'use_book_order': {'type': 'boolean'},
'book_order_top': {'type': 'number', 'maximum':20,'minimum':1}
},
'required': ['ask_last_balance']
},
'ask_strategy': {
'type': 'object',
'properties': {
'use_book_order': {'type': 'boolean'},
'book_order_min': {'type': 'number', 'minimum':1},
'book_order_max': {'type': 'number', 'minimum':1}
},
},
'exchange': {'$ref': '#/definitions/exchange'},
'experimental': {
'type': 'object',
@@ -80,6 +95,7 @@ CONF_SCHEMA = {
},
'required': ['enabled', 'token', 'chat_id']
},
'db_url': {'type': 'string'},
'initial_state': {'type': 'string', 'enum': ['running', 'stopped']},
'internals': {
'type': 'object',

View File

@@ -18,6 +18,8 @@ _API: ccxt.Exchange = None
_CONF: Dict = {}
API_RETRY_COUNT = 4
_CACHED_TICKER: Dict[str, Any] = {}
# Holds all open sell orders for dry_run
_DRY_RUN_OPEN_ORDERS: Dict[str, Any] = {}
@@ -57,7 +59,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
name = exchange_config['name']
if name not in ccxt.exchanges:
raise OperationalException('Exchange {} is not supported'.format(name))
raise OperationalException(f'Exchange {name} is not supported')
try:
api = getattr(ccxt, name.lower())({
'apiKey': exchange_config.get('key'),
@@ -67,7 +69,7 @@ def init_ccxt(exchange_config: dict) -> ccxt.Exchange:
'enableRateLimit': True,
})
except (KeyError, AttributeError):
raise OperationalException('Exchange {} is not supported'.format(name))
raise OperationalException(f'Exchange {name} is not supported')
return api
@@ -116,11 +118,10 @@ def validate_pairs(pairs: List[str]) -> None:
# TODO: add a support for having coins in BTC/USDT format
if not pair.endswith(stake_cur):
raise OperationalException(
'Pair {} not compatible with stake_currency: {}'.format(pair, stake_cur)
)
f'Pair {pair} not compatible with stake_currency: {stake_cur}')
if pair not in markets:
raise OperationalException(
'Pair {} is not available at {}'.format(pair, get_name()))
f'Pair {pair} is not available at {get_name()}')
def exchange_has(endpoint: str) -> bool:
@@ -136,7 +137,7 @@ def exchange_has(endpoint: str) -> bool:
def buy(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_buy_{}'.format(randint(0, 10**6))
order_id = f'dry_run_buy_{randint(0, 10**6)}'
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
@@ -154,20 +155,17 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
return _API.create_limit_buy_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
'Insufficient funds to create limit buy order on market {}.'
'Tried to buy amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
f'Insufficient funds to create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not create limit buy order on market {}.'
'Tried to buy amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
f'Could not create limit buy order on market {pair}.'
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not place buy order due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -175,7 +173,7 @@ def buy(pair: str, rate: float, amount: float) -> Dict:
def sell(pair: str, rate: float, amount: float) -> Dict:
if _CONF['dry_run']:
global _DRY_RUN_OPEN_ORDERS
order_id = 'dry_run_sell_{}'.format(randint(0, 10**6))
order_id = f'dry_run_sell_{randint(0, 10**6)}'
_DRY_RUN_OPEN_ORDERS[order_id] = {
'pair': pair,
'price': rate,
@@ -192,20 +190,17 @@ def sell(pair: str, rate: float, amount: float) -> Dict:
return _API.create_limit_sell_order(pair, amount, rate)
except ccxt.InsufficientFunds as e:
raise DependencyException(
'Insufficient funds to create limit sell order on market {}.'
'Tried to sell amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
f'Insufficient funds to create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not create limit sell order on market {}.'
'Tried to sell amount {} at rate {} (total {}).'
'Message: {}'.format(pair, amount, rate, rate*amount, e)
)
f'Could not create limit sell order on market {pair}.'
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not place sell order due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -220,8 +215,7 @@ def get_balance(currency: str) -> float:
balance = balances.get(currency)
if balance is None:
raise TemporaryError(
'Could not get {} balance due to malformed exchange response: {}'.format(
currency, balances))
f'Could not get {currency} balance due to malformed exchange response: {balances}')
return balance['free']
@@ -241,11 +235,23 @@ def get_balances() -> dict:
return balances
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get balance due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not get balance due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_order_book(pair: str, refresh: Optional[bool] = True) -> dict:
try:
return _API.fetch_order_book(pair)
except ccxt.NotSupported as e:
raise OperationalException(
f'Exchange {_API.name} does not support fetching order book.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load order book due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@retrier
def get_tickers() -> Dict:
@@ -253,28 +259,37 @@ def get_tickers() -> Dict:
return _API.fetch_tickers()
except ccxt.NotSupported as e:
raise OperationalException(
'Exchange {} does not support fetching tickers in batch.'
'Message: {}'.format(_API.name, e)
)
f'Exchange {_API.name} does not support fetching tickers in batch.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load tickers due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not load tickers due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
# TODO: remove refresh argument, keeping it to keep track of where it was intended to be used
@retrier
def get_ticker(pair: str, refresh: Optional[bool] = True) -> dict:
try:
return _API.fetch_ticker(pair)
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load ticker history due to {}. Message: {}'.format(
e.__class__.__name__, e))
except ccxt.BaseError as e:
raise OperationalException(e)
global _CACHED_TICKER
if refresh or pair not in _CACHED_TICKER.keys():
try:
data = _API.fetch_ticker(pair)
try:
_CACHED_TICKER[pair] = {
'bid': float(data['bid']),
'ask': float(data['ask']),
}
except KeyError:
logger.debug("Could not cache ticker data for %s", pair)
return data
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
else:
logger.info("returning cached ticker-data for %s", pair)
return _CACHED_TICKER[pair]
@retrier
@@ -290,10 +305,15 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
# chached data was already downloaded
till_time_ms = min(till_time_ms, arrow.utcnow().shift(minutes=-10).timestamp * 1000)
data = []
data: List[Dict[Any, Any]] = []
while not since_ms or since_ms < till_time_ms:
data_part = _API.fetch_ohlcv(pair, timeframe=tick_interval, since=since_ms)
# Because some exchange sort Tickers ASC and other DESC.
# Ex: Bittrex returns a list of tickers ASC (oldest first, newest last)
# when GDAX returns a list of tickers DESC (newest first, oldest last)
data_part = sorted(data_part, key=lambda x: x[0])
if not data_part:
break
@@ -308,15 +328,13 @@ def get_ticker_history(pair: str, tick_interval: str, since_ms: Optional[int] =
return data
except ccxt.NotSupported as e:
raise OperationalException(
'Exchange {} does not support fetching historical candlestick data.'
'Message: {}'.format(_API.name, e)
)
f'Exchange {_API.name} does not support fetching historical candlestick data.'
f'Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load ticker history due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not load ticker history due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException('Could not fetch ticker data. Msg: {}'.format(e))
raise OperationalException(f'Could not fetch ticker data. Msg: {e}')
@retrier
@@ -328,12 +346,10 @@ def cancel_order(order_id: str, pair: str) -> None:
return _API.cancel_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not cancel order. Message: {}'.format(e)
)
f'Could not cancel order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not cancel order due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not cancel order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -350,12 +366,10 @@ def get_order(order_id: str, pair: str) -> Dict:
return _API.fetch_order(order_id, pair)
except ccxt.InvalidOrder as e:
raise DependencyException(
'Could not get order. Message: {}'.format(e)
)
f'Could not get order. Message: {e}')
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get order due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not get order due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -374,8 +388,7 @@ def get_trades_for_order(order_id: str, pair: str, since: datetime) -> List:
except ccxt.NetworkError as e:
raise TemporaryError(
'Could not get trades due to networking error. Message: {}'.format(e)
)
f'Could not get trades due to networking error. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -397,8 +410,7 @@ def get_markets() -> List[dict]:
return _API.fetch_markets()
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not load markets due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not load markets due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)
@@ -423,8 +435,7 @@ def get_fee(symbol='ETH/BTC', type='', side='', amount=1,
price=price, takerOrMaker=taker_or_maker)['rate']
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
'Could not get fee info due to {}. Message: {}'.format(
e.__class__.__name__, e))
f'Could not get fee info due to {e.__class__.__name__}. Message: {e}')
except ccxt.BaseError as e:
raise OperationalException(e)

View File

@@ -5,9 +5,11 @@ e.g BTC to USD
import logging
import time
from typing import Dict
from typing import Dict, List
from coinmarketcap import Market
from requests.exceptions import RequestException
from freqtrade.constants import SUPPORTED_FIAT
logger = logging.getLogger(__name__)
@@ -33,7 +35,7 @@ class CryptoFiat(object):
self.price = 0.0
# Private attributes
self._expiration = 0
self._expiration = 0.0
self.crypto_symbol = crypto_symbol.upper()
self.fiat_symbol = fiat_symbol.upper()
@@ -64,15 +66,7 @@ class CryptoToFiatConverter(object):
This object is also a Singleton
"""
__instance = None
_coinmarketcap = None
# Constants
SUPPORTED_FIAT = [
"AUD", "BRL", "CAD", "CHF", "CLP", "CNY", "CZK", "DKK",
"EUR", "GBP", "HKD", "HUF", "IDR", "ILS", "INR", "JPY",
"KRW", "MXN", "MYR", "NOK", "NZD", "PHP", "PKR", "PLN",
"RUB", "SEK", "SGD", "THB", "TRY", "TWD", "ZAR", "USD"
]
_coinmarketcap: Market = None
_cryptomap: Dict = {}
@@ -86,7 +80,7 @@ class CryptoToFiatConverter(object):
return CryptoToFiatConverter.__instance
def __init__(self) -> None:
self._pairs = []
self._pairs: List[CryptoFiat] = []
self._load_cryptomap()
def _load_cryptomap(self) -> None:
@@ -94,8 +88,11 @@ class CryptoToFiatConverter(object):
coinlistings = self._coinmarketcap.listings()
self._cryptomap = dict(map(lambda coin: (coin["symbol"], str(coin["id"])),
coinlistings["data"]))
except ValueError:
logger.error("Could not load FIAT Cryptocurrency map")
except (ValueError, RequestException) as exception:
logger.error(
"Could not load FIAT Cryptocurrency map for the following problem: %s",
exception
)
def convert_amount(self, crypto_amount: float, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@@ -122,7 +119,7 @@ class CryptoToFiatConverter(object):
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# Get the pair that interest us and return the price in fiat
for pair in self._pairs:
@@ -174,7 +171,7 @@ class CryptoToFiatConverter(object):
fiat = fiat.upper()
return fiat in self.SUPPORTED_FIAT
return fiat in SUPPORTED_FIAT
def _find_price(self, crypto_symbol: str, fiat_symbol: str) -> float:
"""
@@ -185,12 +182,17 @@ class CryptoToFiatConverter(object):
"""
# Check if the fiat convertion you want is supported
if not self._is_supported_fiat(fiat=fiat_symbol):
raise ValueError('The fiat {} is not supported.'.format(fiat_symbol))
raise ValueError(f'The fiat {fiat_symbol} is not supported.')
# No need to convert if both crypto and fiat are the same
if crypto_symbol == fiat_symbol:
return 1.0
if crypto_symbol not in self._cryptomap:
# return 0 for unsupported stake currencies (fiat-convert should not break the bot)
logger.warning("unsupported crypto-symbol %s - returning 0.0", crypto_symbol)
return 0.0
try:
return float(
self._coinmarketcap.ticker(
@@ -198,6 +200,6 @@ class CryptoToFiatConverter(object):
convert=fiat_symbol
)['data']['quotes'][fiat_symbol.upper()]['price']
)
except BaseException as ex:
logger.error("Error in _find_price: %s", ex)
except BaseException as exception:
logger.error("Error in _find_price: %s", exception)
return 0.0

View File

@@ -33,12 +33,11 @@ class FreqtradeBot(object):
This is from here the bot start its logic.
"""
def __init__(self, config: Dict[str, Any], db_url: Optional[str] = None):
def __init__(self, config: Dict[str, Any]) -> None:
"""
Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config()
method to get the config dict.
:param db_url: database connector string for sqlalchemy (Optional)
"""
logger.info(
@@ -51,26 +50,22 @@ class FreqtradeBot(object):
# Init objects
self.config = config
self.analyze = None
self.fiat_converter = None
self.rpc = None
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc: RPCManager = RPCManager(self)
self.persistence = None
self.exchange = None
self._init_modules(db_url=db_url)
self._init_modules()
def _init_modules(self, db_url: Optional[str] = None) -> None:
def _init_modules(self) -> None:
"""
Initializes all modules and updates the config
:param db_url: database connector string for sqlalchemy (Optional)
:return: None
"""
# Initialize all modules
self.analyze = Analyze(self.config)
self.fiat_converter = CryptoToFiatConverter()
self.rpc = RPCManager(self)
persistence.init(self.config, db_url)
persistence.init(self.config)
exchange.init(self.config)
# Set initial application state
@@ -81,19 +76,16 @@ class FreqtradeBot(object):
else:
self.state = State.STOPPED
def clean(self) -> bool:
def cleanup(self) -> None:
"""
Cleanup the application state und finish all pending tasks
Cleanup pending resources on an already stopped bot
:return: None
"""
self.rpc.send_msg('*Status:* `Stopping trader...`')
logger.info('Stopping trader and cleaning up modules...')
self.state = State.STOPPED
logger.info('Cleaning up modules ...')
self.rpc.cleanup()
persistence.cleanup()
return True
def worker(self, old_state: None) -> State:
def worker(self, old_state: State = None) -> State:
"""
Trading routine that must be run at each loop
:param old_state: the previous service state from the previous call
@@ -102,7 +94,7 @@ class FreqtradeBot(object):
# Log state transition
state = self.state
if state != old_state:
self.rpc.send_msg('*Status:* `{}`'.format(state.name.lower()))
self.rpc.send_msg(f'*Status:* `{state.name.lower()}`')
logger.info('Changing state to: %s', state.name)
if state == State.STOPPED:
@@ -176,12 +168,10 @@ class FreqtradeBot(object):
logger.warning('%s, retrying in 30 seconds...', error)
time.sleep(constants.RETRY_TIMEOUT)
except OperationalException:
tb = traceback.format_exc()
hint = 'Issue `/start` if you think it is safe to restart.'
self.rpc.send_msg(
'*Status:* OperationalException:\n```\n{traceback}```{hint}'
.format(
traceback=traceback.format_exc(),
hint='Issue `/start` if you think it is safe to restart.'
)
f'*Status:* OperationalException:\n```\n{tb}```{hint}'
)
logger.exception('OperationalException. Stopping trader ...')
self.state = State.STOPPED
@@ -244,27 +234,36 @@ class FreqtradeBot(object):
return final_list
def get_target_bid(self, ticker: Dict[str, float]) -> float:
def get_target_bid(self, pair: str) -> float:
"""
Calculates bid target between current ask price and last price
:param ticker: Ticker to use for getting Ask and Last Price
:return: float: Price
"""
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
if self.config['bid_strategy']['use_book_order']:
logger.info('Getting price from Order Book')
orderBook = exchange.get_order_book(pair)
return orderBook['bids'][self.config['bid_strategy']['book_order_top']][0]
else:
logger.info('Using Ask / Last Price')
ticker = exchange.get_ticker(pair);
if ticker['ask'] < ticker['last']:
return ticker['ask']
balance = self.config['bid_strategy']['ask_last_balance']
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
def create_trade(self) -> bool:
"""
Checks the implemented trading indicator(s) for a randomly picked pair,
if one pair triggers the buy_signal a new trade record gets created
:param stake_amount: amount of btc to spend
:param interval: Ticker interval used for Analyze
:return: True if a trade object has been created and persisted, False otherwise
"""
stake_amount = self.config['stake_amount']
interval = self.analyze.get_ticker_interval()
stake_currency = self.config['stake_currency']
fiat_currency = self.config['fiat_display_currency']
exc_name = exchange.get_name()
logger.info(
'Checking buy signals to create a new trade with stake_amount: %f ...',
@@ -272,10 +271,9 @@ class FreqtradeBot(object):
)
whitelist = copy.deepcopy(self.config['exchange']['pair_whitelist'])
# Check if stake_amount is fulfilled
if exchange.get_balance(self.config['stake_currency']) < stake_amount:
if exchange.get_balance(stake_currency) < stake_amount:
raise DependencyException(
'stake amount is not fulfilled (currency={})'.format(self.config['stake_currency'])
)
f'stake amount is not fulfilled (currency={stake_currency})')
# Remove currently opened and latest pairs from whitelist
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
@@ -294,32 +292,25 @@ class FreqtradeBot(object):
break
else:
return False
pair_s = pair.replace('_', '/')
pair_url = exchange.get_pair_detail_url(pair)
# Calculate amount
buy_limit = self.get_target_bid(exchange.get_ticker(pair))
buy_limit = self.get_target_bid(pair)
amount = stake_amount / buy_limit
order_id = exchange.buy(pair, buy_limit, amount)['id']
stake_amount_fiat = self.fiat_converter.convert_amount(
stake_amount,
self.config['stake_currency'],
self.config['fiat_display_currency']
stake_currency,
fiat_currency
)
# Create trade entity and return
self.rpc.send_msg(
'*{}:* Buying [{}]({}) with limit `{:.8f} ({:.6f} {}, {:.3f} {})` '
.format(
exchange.get_name(),
pair.replace('_', '/'),
exchange.get_pair_detail_url(pair),
buy_limit,
stake_amount,
self.config['stake_currency'],
stake_amount_fiat,
self.config['fiat_display_currency']
)
f"""*{exc_name}:* Buying [{pair_s}]({pair_url}) \
with limit `{buy_limit:.8f} ({stake_amount:.6f} \
{stake_currency}, {stake_amount_fiat:.3f} {fiat_currency})`"""
)
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
fee = exchange.get_fee(symbol=pair, taker_or_maker='maker')
@@ -420,12 +411,12 @@ class FreqtradeBot(object):
fee_abs += exectrade['fee']['cost']
if amount != order_amount:
logger.warning("amount {} does not match amount {}".format(amount, trade.amount))
logger.warning(f"amount {amount} does not match amount {trade.amount}")
raise OperationalException("Half bought? Amounts don't match")
real_amount = amount - fee_abs
if fee_abs != 0:
logger.info("Applying fee on amount for {} (from {} to {}) from Trades".format(
trade, order['amount'], real_amount))
logger.info(f"""Applying fee on amount for {trade} \
(from {order_amount} to {real_amount}) from Trades""")
return real_amount
def handle_trade(self, trade: Trade) -> bool:
@@ -434,22 +425,39 @@ class FreqtradeBot(object):
:return: True if trade has been sold, False otherwise
"""
if not trade.is_open:
raise ValueError('attempt to handle closed trade: {}'.format(trade))
raise ValueError(f'attempt to handle closed trade: {trade}')
logger.debug('Handling %s ...', trade)
current_rate = exchange.get_ticker(trade.pair)['bid']
sell_rate = exchange.get_ticker(trade.pair)['bid']
(buy, sell) = (False, False)
if self.config.get('experimental', {}).get('use_sell_signal'):
(buy, sell) = self.analyze.get_signal(trade.pair, self.analyze.get_ticker_interval())
if self.analyze.should_sell(trade, current_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, current_rate)
return True
if self.config['ask_strategy']['use_book_order']:
logger.info('Using order book for selling...')
orderBook = exchange.get_order_book(trade.pair)
# logger.debug('Order book %s',orderBook)
for i in range(self.config['ask_strategy']['book_order_min'],
self.config['ask_strategy']['book_order_max'] + 1):
sell_rate = orderBook['asks'][i - 1][0]
if self.check_sell(trade, sell_rate, buy, sell):
return True
break
else:
if self.check_sell(trade, sell_rate, buy, sell):
return True
logger.info('Found no sell signals for whitelisted currencies. Trying again..')
return False
def check_sell(self, trade: Trade, sell_rate: float, buy: bool, sell: bool) -> bool:
if self.analyze.should_sell(trade, sell_rate, datetime.utcnow(), buy, sell):
self.execute_sell(trade, sell_rate)
return True
return False
def check_handle_timedout(self, timeoutvalue: int) -> None:
"""
Check if any orders are timed out and cancel if neccessary
@@ -460,6 +468,12 @@ class FreqtradeBot(object):
for trade in Trade.query.filter(Trade.open_order_id.isnot(None)).all():
try:
# FIXME: Somehow the query above returns results
# where the open_order_id is in fact None.
# This is probably because the record got
# updated via /forcesell in a different thread.
if not trade.open_order_id:
continue
order = exchange.get_order(trade.open_order_id, trade.pair)
except requests.exceptions.RequestException:
logger.info(
@@ -469,10 +483,11 @@ class FreqtradeBot(object):
continue
ordertime = arrow.get(order['datetime']).datetime
print(order)
# Check if trade is still actually open
if int(order['remaining']) == 0:
continue
# this makes no real sense and causes errors!
#
# if (filled(int(order['filled']) == 0) and (order['status'] == 'open'):
if order['side'] == 'buy' and ordertime < timeoutthreashold:
self.handle_timedout_limit_buy(trade, order)
elif order['side'] == 'sell' and ordertime < timeoutthreashold:
@@ -485,16 +500,14 @@ class FreqtradeBot(object):
"""Buy timeout - cancel order
:return: True if order was fully cancelled
"""
pair_s = trade.pair.replace('_', '/')
exchange.cancel_order(trade.open_order_id, trade.pair)
if order['remaining'] == order['amount']:
# if trade is not partially completed, just delete the trade
Trade.session.delete(trade)
# FIX? do we really need to flush, caller of
# check_handle_timedout will flush afterwards
Trade.session.flush()
logger.info('Buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Unfilled buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Unfilled buy order for {pair_s} cancelled')
return True
# if trade is partially complete, edit the stake details for the trade
@@ -503,8 +516,7 @@ class FreqtradeBot(object):
trade.stake_amount = trade.amount * trade.open_rate
trade.open_order_id = None
logger.info('Partial buy order timeout for %s.', trade)
self.rpc.send_msg('*Timeout:* Remaining buy order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Remaining buy order for {pair_s} cancelled')
return False
# FIX: 20180110, should cancel_order() be cond. or unconditionally called?
@@ -513,6 +525,7 @@ class FreqtradeBot(object):
Sell timeout - cancel order and update trade
:return: True if order was fully cancelled
"""
pair_s = trade.pair.replace('_', '/')
if order['remaining'] == order['amount']:
# if trade is not partially completed, just cancel the trade
exchange.cancel_order(trade.open_order_id, trade.pair)
@@ -521,8 +534,7 @@ class FreqtradeBot(object):
trade.close_date = None
trade.is_open = True
trade.open_order_id = None
self.rpc.send_msg('*Timeout:* Unfilled sell order for {} cancelled'.format(
trade.pair.replace('_', '/')))
self.rpc.send_msg(f'*Timeout:* Unfilled sell order for {pair_s} cancelled')
logger.info('Sell order timeout for %s.', trade)
return True
@@ -536,6 +548,8 @@ class FreqtradeBot(object):
:param limit: limit rate for the sell order
:return: None
"""
exc = trade.exchange
pair = trade.pair
# Execute sell and update trade record
order_id = exchange.sell(str(trade.pair), limit, trade.amount)['id']
trade.open_order_id = order_id
@@ -545,43 +559,31 @@ class FreqtradeBot(object):
profit_trade = trade.calc_profit(rate=limit)
current_rate = exchange.get_ticker(trade.pair)['bid']
profit = trade.calc_profit_percent(limit)
pair_url = exchange.get_pair_detail_url(trade.pair)
gain = "profit" if fmt_exp_profit > 0 else "loss"
message = "*{exchange}:* Selling\n" \
"*Current Pair:* [{pair}]({pair_url})\n" \
"*Limit:* `{limit}`\n" \
"*Amount:* `{amount}`\n" \
"*Open Rate:* `{open_rate:.8f}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \
"*Profit:* `{profit:.2f}%`" \
"".format(
exchange=trade.exchange,
pair=trade.pair,
pair_url=exchange.get_pair_detail_url(trade.pair),
limit=limit,
open_rate=trade.open_rate,
current_rate=current_rate,
amount=round(trade.amount, 8),
profit=round(profit * 100, 2),
)
message = f"*{exc}:* Selling\n" \
f"*Current Pair:* [{pair}]({pair_url})\n" \
f"*Limit:* `{limit}`\n" \
f"*Amount:* `{round(trade.amount, 8)}`\n" \
f"*Open Rate:* `{trade.open_rate:.8f}`\n" \
f"*Current Rate:* `{current_rate:.8f}`\n" \
f"*Profit:* `{round(profit * 100, 2):.2f}%`" \
""
# For regular case, when the configuration exists
if 'stake_currency' in self.config and 'fiat_display_currency' in self.config:
stake = self.config['stake_currency']
fiat = self.config['fiat_display_currency']
fiat_converter = CryptoToFiatConverter()
profit_fiat = fiat_converter.convert_amount(
profit_trade,
self.config['stake_currency'],
self.config['fiat_display_currency']
stake,
fiat
)
message += '` ({gain}: {profit_percent:.2f}%, {profit_coin:.8f} {coin}`' \
'` / {profit_fiat:.3f} {fiat})`' \
''.format(
gain="profit" if fmt_exp_profit > 0 else "loss",
profit_percent=fmt_exp_profit,
profit_coin=profit_trade,
coin=self.config['stake_currency'],
profit_fiat=profit_fiat,
fiat=self.config['fiat_display_currency'],
)
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
f'` / {profit_fiat:.3f} {fiat})`' \
''
# Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well
else:

View File

@@ -13,7 +13,7 @@ def went_down(series: Series) -> bool:
return series < series.shift(1)
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> type(Series):
def ehlers_super_smoother(series: Series, smoothing: float = 6) -> Series:
magic = pi * sqrt(2) / smoothing
a1 = exp(-magic)
coeff2 = 2 * a1 * cos(magic)

View File

@@ -5,11 +5,14 @@ Read the documentation to know what cli arguments you need.
"""
import logging
import sys
from argparse import Namespace
from typing import List
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State
logger = logging.getLogger('freqtrade')
@@ -43,24 +46,48 @@ def main(sysargv: List[str]) -> None:
state = None
while 1:
state = freqtrade.worker(old_state=state)
if state == State.RELOAD_CONF:
freqtrade = reconfigure(freqtrade, args)
except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...')
return_code = 0
except OperationalException as e:
logger.error(str(e))
return_code = 2
except BaseException:
logger.exception('Fatal exception!')
finally:
if freqtrade:
freqtrade.clean()
freqtrade.rpc.send_msg('*Status:* `Process died ...`')
freqtrade.cleanup()
sys.exit(return_code)
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
"""
Cleans up current instance, reloads the configuration and returns the new instance
"""
# Clean up current modules
freqtrade.cleanup()
# Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg(
'*Status:* `Config reloaded ...`'.format(
freqtrade.state.name.lower()
)
)
return freqtrade
def set_loggers() -> None:
"""
Set the logger level for Third party libs
:return: None
"""
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
logging.getLogger('ccxt.base.exchange').setLevel(logging.INFO)
logging.getLogger('telegram').setLevel(logging.INFO)

View File

@@ -83,7 +83,7 @@ def file_dump_json(filename, data, is_zip=False) -> None:
json.dump(data, fp, default=str)
def format_ms_time(date: str) -> str:
def format_ms_time(date: int) -> str:
"""
convert MS date to readable format.
: epoch-string in ms

View File

@@ -4,44 +4,45 @@ import gzip
import json
import logging
import os
from typing import Optional, List, Dict, Tuple, Any
import arrow
from typing import Optional, List, Dict, Tuple
from freqtrade import misc, constants
from freqtrade.exchange import get_ticker_history
from freqtrade.arguments import TimeRange
from user_data.hyperopt_conf import hyperopt_optimize_conf
logger = logging.getLogger(__name__)
def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -> List[Dict]:
def trim_tickerlist(tickerlist: List[Dict], timerange: TimeRange) -> List[Dict]:
if not tickerlist:
return tickerlist
stype, start, stop = timerange
start_index = 0
stop_index = len(tickerlist)
if stype[0] == 'line':
stop_index = start
if stype[0] == 'index':
start_index = start
elif stype[0] == 'date':
while tickerlist[start_index][0] < start * 1000:
if timerange.starttype == 'line':
stop_index = timerange.startts
if timerange.starttype == 'index':
start_index = timerange.startts
elif timerange.starttype == 'date':
while (start_index < len(tickerlist) and
tickerlist[start_index][0] < timerange.startts * 1000):
start_index += 1
if stype[1] == 'line':
start_index = len(tickerlist) + stop
if stype[1] == 'index':
stop_index = stop
elif stype[1] == 'date':
while tickerlist[stop_index-1][0] > stop * 1000:
if timerange.stoptype == 'line':
start_index = len(tickerlist) + timerange.stopts
if timerange.stoptype == 'index':
stop_index = timerange.stopts
elif timerange.stoptype == 'date':
while (stop_index > 0 and
tickerlist[stop_index-1][0] > timerange.stopts * 1000):
stop_index -= 1
if start_index > stop_index:
raise ValueError(f'The timerange [{start},{stop}] is incorrect')
raise ValueError(f'The timerange [{timerange.startts},{timerange.stopts}] is incorrect')
return tickerlist[start_index:stop_index]
@@ -49,7 +50,7 @@ def trim_tickerlist(tickerlist: List[Dict], timerange: Tuple[Tuple, int, int]) -
def load_tickerdata_file(
datadir: str, pair: str,
ticker_interval: str,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Optional[List[Dict]]:
timerange: Optional[TimeRange] = None) -> Optional[List[Dict]]:
"""
Load a pair from file,
:return dict OR empty if unsuccesful
@@ -84,7 +85,7 @@ def load_data(datadir: str,
ticker_interval: str,
pairs: Optional[List[str]] = None,
refresh_pairs: Optional[bool] = False,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> Dict[str, List]:
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> Dict[str, List]:
"""
Loads ticker history data for the given parameters
:return: dict
@@ -100,15 +101,16 @@ def load_data(datadir: str,
for pair in _pairs:
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
if not pairdata:
# download the tickerdata from exchange
download_backtesting_testdata(datadir,
pair=pair,
tick_interval=ticker_interval,
timerange=timerange)
# and retry reading the pair
pairdata = load_tickerdata_file(datadir, pair, ticker_interval, timerange=timerange)
result[pair] = pairdata
if pairdata:
result[pair] = pairdata
else:
logger.warning(
'No data for pair: "%s", Interval: %s. '
'Use --refresh-pairs-cached to download the data',
pair,
ticker_interval
)
return result
@@ -123,7 +125,7 @@ def make_testdata_path(datadir: str) -> str:
def download_pairs(datadir, pairs: List[str],
ticker_interval: str,
timerange: Optional[Tuple[Tuple, int, int]] = None) -> bool:
timerange: TimeRange = TimeRange(None, None, 0, 0)) -> bool:
"""For each pairs passed in parameters, download the ticker intervals"""
for pair in pairs:
try:
@@ -143,7 +145,9 @@ def download_pairs(datadir, pairs: List[str],
def load_cached_data_for_updating(filename: str,
tick_interval: str,
timerange: Optional[Tuple[Tuple, int, int]]) -> Tuple[list, int]:
timerange: Optional[TimeRange]) -> Tuple[
List[Any],
Optional[int]]:
"""
Load cached data and choose what part of the data should be updated
"""
@@ -152,10 +156,10 @@ def load_cached_data_for_updating(filename: str,
# user sets timerange, so find the start time
if timerange:
if timerange[0][0] == 'date':
since_ms = timerange[1] * 1000
elif timerange[0][1] == 'line':
num_minutes = timerange[2] * constants.TICKER_INTERVAL_MINUTES[tick_interval]
if timerange.starttype == 'date':
since_ms = timerange.startts * 1000
elif timerange.stoptype == 'line':
num_minutes = timerange.stopts * constants.TICKER_INTERVAL_MINUTES[tick_interval]
since_ms = arrow.utcnow().shift(minutes=num_minutes).timestamp * 1000
# read the cached file
@@ -185,7 +189,7 @@ def load_cached_data_for_updating(filename: str,
def download_backtesting_testdata(datadir: str,
pair: str,
tick_interval: str = '5m',
timerange: Optional[Tuple[Tuple, int, int]] = None) -> None:
timerange: Optional[TimeRange] = None) -> None:
"""
Download the latest ticker intervals from the exchange for the pairs passed in parameters

View File

@@ -34,18 +34,6 @@ class Backtesting(object):
def __init__(self, config: Dict[str, Any]) -> None:
self.config = config
self.analyze = None
self.ticker_interval = None
self.tickerdata_to_dataframe = None
self.populate_buy_trend = None
self.populate_sell_trend = None
self._init()
def _init(self) -> None:
"""
Init objects required for backtesting
:return: None
"""
self.analyze = Analyze(self.config)
self.ticker_interval = self.analyze.strategy.ticker_interval
self.tickerdata_to_dataframe = self.analyze.tickerdata_to_dataframe
@@ -79,9 +67,9 @@ class Backtesting(object):
Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str
"""
floatfmt, headers, tabular_data = self.aggregate(data, results)
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt)
floatfmt, headers, tabular_data = self.aggregate(data, results)
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def aggregate(self, data, results):
stake_currency = self.config.get('stake_currency')
@@ -91,6 +79,7 @@ class Backtesting(object):
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data:
result = results[results.currency == pair]
print(results)
tabular_data.append([
pair,
len(result.index),
@@ -174,13 +163,22 @@ class Backtesting(object):
max_open_trades = args.get('max_open_trades', 0)
realistic = args.get('realistic', False)
record = args.get('record', None)
recordfilename = args.get('recordfn', 'backtest-result.json')
records = []
trades = []
trade_count_lock = {}
trade_count_lock: Dict = {}
for pair, pair_data in processed.items():
pair_data['buy'], pair_data['sell'] = 0, 0 # cleanup from previous run
ticker_data = self.populate_sell_trend(self.populate_buy_trend(pair_data))[headers]
ticker_data = self.populate_sell_trend(
self.populate_buy_trend(pair_data))[headers].copy()
# to avoid using data from future, we buy/sell with signal from previous candle
ticker_data.loc[:, 'buy'] = ticker_data['buy'].shift(1)
ticker_data.loc[:, 'sell'] = ticker_data['sell'].shift(1)
ticker_data.drop(ticker_data.head(1).index, inplace=True)
ticker = [x for x in ticker_data.itertuples()]
lock_pair_until = None
@@ -217,7 +215,8 @@ class Backtesting(object):
# For now export inside backtest(), maybe change so that backtest()
# returns a tuple like: (dataframe, records, logs, etc)
if record and record.find('trades') >= 0:
logger.info('Dumping backtest results')
logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records)
file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'entry', 'exit']
@@ -240,7 +239,8 @@ class Backtesting(object):
else:
logger.info('Using local backtesting data (using whitelist in given config) ...')
timerange = Arguments.parse_timerange(self.config.get('timerange'))
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = optimize.load_data(
self.config['datadir'],
pairs=pairs,
@@ -249,6 +249,9 @@ class Backtesting(object):
timerange=timerange
)
if not data:
logger.critical("No data found. Terminating.")
return
# Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades']
@@ -278,7 +281,8 @@ class Backtesting(object):
'realistic': self.config.get('realistic_simulation', False),
'sell_profit_only': sell_profit_only,
'use_sell_signal': use_sell_signal,
'record': self.config.get('export')
'record': self.config.get('export'),
'recordfn': self.config.get('exportfilename'),
}
)
logger.info(

View File

@@ -14,7 +14,7 @@ from argparse import Namespace
from functools import reduce
from math import exp
from operator import itemgetter
from typing import Dict, Any, Callable
from typing import Dict, Any, Callable, Optional
import numpy
import talib.abstract as ta
@@ -60,7 +60,7 @@ class Hyperopt(Backtesting):
self.expected_max_profit = 3.0
# Configuration and data used by hyperopt
self.processed = None
self.processed: Optional[Dict[str, Any]] = None
# Hyperopt Trials
self.trials_file = os.path.join('user_data', 'hyperopt_trials.pickle')
@@ -344,7 +344,7 @@ class Hyperopt(Backtesting):
"""
Return the space to use during Hyperopt
"""
spaces = {}
spaces: Dict = {}
if self.has_space('buy'):
spaces = {**spaces, **Hyperopt.indicator_space()}
if self.has_space('roi'):
@@ -455,6 +455,7 @@ class Hyperopt(Backtesting):
if trade_count == 0 or trade_duration > self.max_accepted_trade_duration:
print('.', end='')
sys.stdout.flush()
return {
'status': STATUS_FAIL,
'loss': float('inf')
@@ -479,31 +480,32 @@ class Hyperopt(Backtesting):
'result': result_explanation,
}
@staticmethod
def format_results(results: DataFrame) -> str:
def format_results(self, results: DataFrame) -> str:
"""
Return the format result in a string
"""
return ('{:6d} trades. Avg profit {: 5.2f}%. '
'Total profit {: 11.8f} BTC ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
'Total profit {: 11.8f} {} ({:.4f}Σ%). Avg duration {:5.1f} mins.').format(
len(results.index),
results.profit_percent.mean() * 100.0,
results.profit_BTC.sum(),
self.config['stake_currency'],
results.profit_percent.sum(),
results.duration.mean(),
)
def start(self) -> None:
timerange = Arguments.parse_timerange(self.config.get('timerange'))
timerange = Arguments.parse_timerange(None if self.config.get(
'timerange') is None else str(self.config.get('timerange')))
data = load_data(
datadir=self.config.get('datadir'),
datadir=str(self.config.get('datadir')),
pairs=self.config['exchange']['pair_whitelist'],
ticker_interval=self.ticker_interval,
timerange=timerange
)
if self.has_space('buy'):
self.analyze.populate_indicators = Hyperopt.populate_indicators
self.analyze.populate_indicators = Hyperopt.populate_indicators # type: ignore
self.processed = self.tickerdata_to_dataframe(data)
if self.config.get('mongodb'):

View File

@@ -5,48 +5,54 @@ This module contains the class to persist trades into SQLite
import logging
from datetime import datetime
from decimal import Decimal, getcontext
from typing import Dict, Optional
from typing import Dict, Optional, Any
import arrow
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
create_engine)
from sqlalchemy.engine import Engine
from sqlalchemy import inspect
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm.scoping import scoped_session
from sqlalchemy.orm.session import sessionmaker
from sqlalchemy.pool import StaticPool
from sqlalchemy import inspect
from freqtrade import OperationalException
logger = logging.getLogger(__name__)
_CONF = {}
_DECL_BASE = declarative_base()
_DECL_BASE: Any = declarative_base()
def init(config: dict, engine: Optional[Engine] = None) -> None:
def init(config: Dict) -> None:
"""
Initializes this module with the given config,
registers all known command handlers
and starts polling for message updates
:param config: config to use
:param engine: database engine for sqlalchemy (Optional)
:return: None
"""
_CONF.update(config)
if not engine:
if _CONF.get('dry_run', False):
# the user wants dry run to use a DB
if _CONF.get('dry_run_db', False):
engine = create_engine('sqlite:///tradesv3.dry_run.sqlite')
# Otherwise dry run will store in memory
else:
engine = create_engine('sqlite://',
connect_args={'check_same_thread': False},
poolclass=StaticPool,
echo=False)
else:
engine = create_engine('sqlite:///tradesv3.sqlite')
db_url = _CONF.get('db_url', None)
kwargs = {}
# Take care of thread ownership if in-memory db
if db_url == 'sqlite://':
kwargs.update({
'connect_args': {'check_same_thread': False},
'poolclass': StaticPool,
'echo': False,
})
try:
engine = create_engine(db_url, **kwargs)
except NoSuchModuleError:
error = 'Given value for db_url: \'{}\' is no valid database URL! (See {}).'.format(
db_url, 'http://docs.sqlalchemy.org/en/latest/core/engines.html#database-urls'
)
raise OperationalException(error)
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
Trade.session = session()
@@ -54,8 +60,8 @@ def init(config: dict, engine: Optional[Engine] = None) -> None:
_DECL_BASE.metadata.create_all(engine)
check_migrate(engine)
# Clean dry_run DB
if _CONF.get('dry_run', False) and _CONF.get('dry_run_db', False):
# Clean dry_run DB if the db is not in-memory
if _CONF.get('dry_run', False) and db_url != 'sqlite://':
clean_dry_run_db()
@@ -149,11 +155,11 @@ class Trade(_DECL_BASE):
close_date = Column(DateTime)
open_order_id = Column(String)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=False, default=0.0)
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=False, default=0.0)
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=False, default=0.0)
max_rate = Column(Float, nullable=True, default=0.0)
def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(

View File

@@ -2,13 +2,14 @@
This module contains class to define a RPC communications
"""
import logging
from datetime import datetime, timedelta
from datetime import datetime, timedelta, date
from decimal import Decimal
from typing import Tuple, Any
from typing import Dict, Tuple, Any
import arrow
import sqlalchemy as sql
from pandas import DataFrame
from numpy import mean, nan_to_num
from freqtrade import exchange
from freqtrade.misc import shorten_date
@@ -117,7 +118,7 @@ class RPC(object):
self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]:
today = datetime.utcnow().date()
profit_days = {}
profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`'
@@ -175,7 +176,7 @@ class RPC(object):
durations = []
for trade in trades:
current_rate = None
current_rate: float = 0.0
if not trade.open_rate:
continue
@@ -212,14 +213,14 @@ class RPC(object):
fiat = self.freqtrade.fiat_converter
# Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(sum(profit_closed_percent) * 100, 2)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
profit_closed_fiat = fiat.convert_amount(
profit_closed_coin,
stake_currency,
fiat_display_currency
)
profit_all_coin = round(sum(profit_all_coin), 8)
profit_all_percent = round(sum(profit_all_percent) * 100, 2)
profit_all_percent = round(nan_to_num(mean(profit_all_percent)) * 100, 2)
profit_all_fiat = fiat.convert_amount(
profit_all_coin,
stake_currency,
@@ -281,7 +282,7 @@ class RPC(object):
value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value)
def rpc_start(self) -> (bool, str):
def rpc_start(self) -> Tuple[bool, str]:
"""
Handler for start.
"""
@@ -291,7 +292,7 @@ class RPC(object):
self.freqtrade.state = State.RUNNING
return False, '`Starting trader ...`'
def rpc_stop(self) -> (bool, str):
def rpc_stop(self) -> Tuple[bool, str]:
"""
Handler for stop.
"""
@@ -301,6 +302,11 @@ class RPC(object):
return True, '*Status:* `already stopped`'
def rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """
self.freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]:
"""
@@ -319,8 +325,10 @@ class RPC(object):
and order['side'] == 'buy':
exchange.cancel_order(trade.open_order_id, trade.pair)
trade.close(order.get('price') or trade.open_rate)
# TODO: sell amount which has been bought already
return
# Do the best effort, if we don't know 'filled' amount, don't try selling
if order['filled'] is None:
return
trade.amount = order['filled']
# Ignore trades with an attached LIMIT_SELL order
if order and order['status'] == 'open' \
@@ -354,6 +362,7 @@ class RPC(object):
return True, 'Invalid argument.'
_exec_forcesell(trade)
Trade.session.flush()
return False, ''
def rpc_performance(self) -> Tuple[bool, Any]:

View File

@@ -1,6 +1,7 @@
"""
This module contains class to manage RPC communications (Telegram, Slack, ...)
"""
from typing import Any, List
import logging
from freqtrade.rpc.telegram import Telegram
@@ -21,8 +22,8 @@ class RPCManager(object):
"""
self.freqtrade = freqtrade
self.registered_modules = []
self.telegram = None
self.registered_modules: List[str] = []
self.telegram: Any = None
self._init()
def _init(self) -> None:

View File

@@ -18,7 +18,7 @@ from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__)
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
"""
Decorator to check if the message comes from the correct chat_id
:param command_handler: Telegram CommandHandler
@@ -65,7 +65,7 @@ class Telegram(RPC):
"""
super().__init__(freqtrade)
self._updater = None
self._updater: Updater = None
self._config = freqtrade.config
self._init()
@@ -93,6 +93,7 @@ class Telegram(RPC):
CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily),
CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('help', self._help),
CommandHandler('version', self._version),
]
@@ -300,6 +301,18 @@ class Telegram(RPC):
(error, msg) = self.rpc_stop()
self.send_msg(msg, bot=bot)
@authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None:
"""
Handler for /reload_conf.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self.rpc_reload_conf()
self.send_msg(msg, bot=bot)
@authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None:
"""

View File

@@ -8,7 +8,8 @@ import enum
class State(enum.Enum):
"""
Bot running states
Bot application states
"""
RUNNING = 0
STOPPED = 1
RELOAD_CONF = 2

View File

@@ -2,7 +2,7 @@
IStrategy interface
This module defines the interface to apply for strategies
"""
from typing import Dict
from abc import ABC, abstractmethod
from pandas import DataFrame
@@ -16,9 +16,13 @@ class IStrategy(ABC):
Attributes you can use:
minimal_roi -> Dict: Minimal ROI designed for the strategy
stoploss -> float: optimal stoploss designed for the strategy
ticker_interval -> int: value of the ticker interval to use for the strategy
ticker_interval -> str: value of the ticker interval to use for the strategy
"""
minimal_roi: Dict
stoploss: float
ticker_interval: str
@abstractmethod
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
"""

View File

@@ -37,7 +37,8 @@ class StrategyResolver(object):
# Verify the strategy is in the configuration, otherwise fallback to the default strategy
strategy_name = config.get('strategy') or constants.DEFAULT_STRATEGY
self.strategy = self._load_strategy(strategy_name, extra_dir=config.get('strategy_path'))
self.strategy: IStrategy = self._load_strategy(strategy_name,
extra_dir=config.get('strategy_path'))
# Set attributes
# Check if we need to override configuration
@@ -72,7 +73,7 @@ class StrategyResolver(object):
return self._load_strategy(strategy_name, temp.absolute())
def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> Optional[IStrategy]:
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
"""
Search and loads the specified strategy.
:param strategy_name: name of the module to import
@@ -91,7 +92,7 @@ class StrategyResolver(object):
# check if the given strategy is provided as name, value pair
# where the value is the strategy encoded in base 64
if ":" in strategy_name:
if ":" in strategy_name and "http" not in strategy_name:
strat = strategy_name.split(":")
if len(strat) == 2:
@@ -113,13 +114,18 @@ class StrategyResolver(object):
resp = requests.get(strategy_name, stream=True)
if resp.status_code == 200:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
if strategy_name.endswith("/code"):
strategy_name = strategy_name.replace("/code", "")
name = os.path.basename(urlparse(strategy_name).path)
temp.joinpath(name).write_text(resp.text)
temp.joinpath("{}.py".format(name)).write_text(resp.text)
temp.joinpath("__init__.py").touch()
strategy_name = os.path.splitext(name)[0]
print("stored downloaded stat at: {}".format(temp))
# register temp path with the bot
abs_paths.insert(0, temp.absolute())
@@ -149,7 +155,7 @@ class StrategyResolver(object):
# Generate spec based on absolute path
spec = importlib.util.spec_from_file_location('user_data.strategies', module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
spec.loader.exec_module(module) # type: ignore # importlib does not use typehints
valid_strategies_gen = (
obj for name, obj in inspect.getmembers(module, inspect.isclass)

View File

@@ -1,330 +0,0 @@
from base64 import urlsafe_b64encode
import os
import pytest
import simplejson as json
from datetime import datetime, timedelta
from freqtrade.aws.backtesting_lambda import backtest, cron, generate_configuration
from freqtrade.aws.strategy import submit
# @pytest.mark.skip(reason="no way of currently testing this")
def test_backtest_remote(lambda_context):
content = """# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class TestStrategy(IStrategy):
minimal_roi = {
"0": 0.5
}
stoploss = -0.2
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['maShort'] = ta.EMA(dataframe, timeperiod=8)
dataframe['maMedium'] = ta.EMA(dataframe, timeperiod=21)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maShort'], dataframe['maMedium'])
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maMedium'], dataframe['maShort'])
),
'sell'] = 1
return dataframe
"""
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"description": "simple test strategy",
"name": "TestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": False
}
# now we add an entry
submit({
"body": json.dumps(request)
}, {})
# build sns request
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"name": "MyFancyTestStrategy",
"from": "20180401",
"till": "20180501",
"stake_currency": "usdt",
"assets": ["ltc"],
"local": False
}
assert backtest({
"Records": [
{
"Sns": {
"Subject": "backtesting",
"Message": json.dumps(request)
}
}]
}, {})['statusCode'] == 200
def test_backtest_time_frame(lambda_context):
content = """# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyFancyTestStrategy(IStrategy):
minimal_roi = {
"0": 0.5
}
stoploss = -0.2
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['maShort'] = ta.EMA(dataframe, timeperiod=8)
dataframe['maMedium'] = ta.EMA(dataframe, timeperiod=21)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maShort'], dataframe['maMedium'])
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maMedium'], dataframe['maShort'])
),
'sell'] = 1
return dataframe
"""
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"description": "simple test strategy",
"name": "MyFancyTestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": False
}
# now we add an entry
submit({
"body": json.dumps(request)
}, {})
# build sns request
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"name": "MyFancyTestStrategy",
"from": "20180401",
"till": "20180501",
"stake_currency": "usdt",
"assets": ["ltc"],
"local": True
}
assert backtest({
"Records": [
{
"Sns": {
"Subject": "backtesting",
"Message": json.dumps(request)
}
}]
}, {})['statusCode'] == 200
def test_backtest(lambda_context):
content = """# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyFancyTestStrategy(IStrategy):
minimal_roi = {
"0": 0.5
}
stoploss = -0.2
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['maShort'] = ta.EMA(dataframe, timeperiod=8)
dataframe['maMedium'] = ta.EMA(dataframe, timeperiod=21)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maShort'], dataframe['maMedium'])
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maMedium'], dataframe['maShort'])
),
'sell'] = 1
return dataframe
"""
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"description": "simple test strategy",
"name": "MyFancyTestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": False
}
# now we add an entry
submit({
"body": json.dumps(request)
}, {})
# build sns request
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"name": "MyFancyTestStrategy",
"stake_currency": "usdt",
"assets": ["ltc"],
"days": 2,
"ticker": '15m',
"local": True
}
assert backtest({
"Records": [
{
"Sns": {
"Subject": "backtesting",
"Message": json.dumps(request)
}
}]
}, {})['statusCode'] == 200
def test_cron(lambda_context):
""" test the scheduling to the queue"""
content = """# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class MyFancyTestStrategy(IStrategy):
minimal_roi = {
"0": 0.5
}
stoploss = -0.2
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['maShort'] = ta.EMA(dataframe, timeperiod=8)
dataframe['maMedium'] = ta.EMA(dataframe, timeperiod=21)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maShort'], dataframe['maMedium'])
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maMedium'], dataframe['maShort'])
),
'sell'] = 1
return dataframe
"""
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"description": "simple test strategy",
"name": "MyFancyTestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": False
}
# now we add an entry
submit({
"body": json.dumps(request)
}, {})
print("evaluating cron job")
cron({}, {})
# TODO test receiving of message some how
def test_generate_configuration(lambda_context):
os.environ["BASE_URL"] = "https://freq.isaac.international/dev"
till = datetime.today()
fromDate = till - timedelta(days=90)
config = generate_configuration(fromDate, till, "TestStrategy", True,
"GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG", True)
print(config)

View File

@@ -1,211 +0,0 @@
import simplejson as json
from base64 import urlsafe_b64encode
import freqtrade.aws.strategy as aws
import responses
def test_strategy(lambda_context):
"""
very ugly long test
:param lambda_context:
:return:
"""
content = """# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class TestStrategy(IStrategy):
minimal_roi = {
"0": 0.5
}
stoploss = -0.2
ticker_interval = '5m'
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['maShort'] = ta.EMA(dataframe, timeperiod=8)
dataframe['maMedium'] = ta.EMA(dataframe, timeperiod=21)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maShort'], dataframe['maMedium'])
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
qtpylib.crossed_above(dataframe['maMedium'], dataframe['maShort'])
),
'sell'] = 1
return dataframe
"""
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG",
"description": "simple test strategy",
"name": "TestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": False
}
# now we add an entry
aws.submit({
"body": json.dumps(request)
}, {})
# now we should have items
print(json.loads(aws.names({}, {})['body']))
assert (len(json.loads(aws.names({}, {})['body'])['result']) == 1)
# able to add a second strategy with the sample name, but different user
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TH",
"description": "simple test strategy",
"name": "TestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": True
}
aws.submit({
"body": json.dumps(request)
}, {})
assert (len(json.loads(aws.names({}, {})['body'])['result']) == 2)
# able to add a duplicated strategy, which should overwrite the existing strategy
request = {
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TH",
"description": "simple test strategy",
"name": "TestStrategy",
"content": urlsafe_b64encode(content.encode('utf-8')),
"public": True
}
aws.submit({
"body": json.dumps(request)
}, {})
assert (len(json.loads(aws.names({}, {})['body'])['result']) == 2)
# we need to be able to get a strategy ( code cannot be included )
strategy = aws.get({'pathParameters': {
"name": "TestStrategy",
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TH"
}}, {})
strategy = json.loads(strategy['body'])
assert "content" not in strategy
assert "user" in strategy
assert "name" in strategy
assert "description" in strategy
assert "public" in strategy
assert "content" not in strategy
# we need to be able to get the code of the strategy
code = aws.code({'pathParameters': {
"name": "TestStrategy",
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TH"
}}, {})
print("code is")
print(code)
# code should equal our initial content
# assert code == content
# we are not allowed to load a private strategy
code = aws.code({'pathParameters': {
"name": "TestStrategy",
"user": "GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG"
}}, {})
# code should equal our initial content
assert code['statusCode'] == 403
assert json.loads(code['body']) == {"success": False, "reason": "Denied"}
def test_strategy_submit_github(lambda_context):
event = {'resource': '/strategies/submit/github', 'path': '/strategies/submit/github', 'httpMethod': 'POST',
'headers': {'Accept': '*/*', 'CloudFront-Forwarded-Proto': 'https', 'CloudFront-Is-Desktop-Viewer': 'true',
'CloudFront-Is-Mobile-Viewer': 'false', 'CloudFront-Is-SmartTV-Viewer': 'false',
'CloudFront-Is-Tablet-Viewer': 'false', 'CloudFront-Viewer-Country': 'US',
'content-type': 'application/json', 'Host': '887c8k0tui.execute-api.us-east-2.amazonaws.com',
'User-Agent': 'GitHub-Hookshot/419cd30',
'Via': '1.1 fd885dc16612d4e9d70f328fd0542052.cloudfront.net (CloudFront)',
'X-Amz-Cf-Id': 'l8qrc32exLsdGHyWDr5i1WtmlJIQZKo7cqOElKrEEDGRgOm7PPxoKA==',
'X-Amzn-Trace-Id': 'Root=1-5b035d39-de61ead01e4729f073a67480',
'X-Forwarded-For': '192.30.252.39, 54.182.230.5', 'X-Forwarded-Port': '443',
'X-Forwarded-Proto': 'https', 'X-GitHub-Delivery': 'e7baca80-5d52-11e8-86c9-f183bfa87d9b',
'X-GitHub-Event': 'ping', 'X-Hub-Signature': 'sha1=d7d4cd82a5e7e4357e0f4df8d032c474c26b6d61'},
'queryStringParameters': None, 'pathParameters': None, 'stageVariables': None,
'requestContext': {'resourceId': 'dmek8c', 'resourcePath': '/strategies/submit/github',
'httpMethod': 'POST', 'extendedRequestId': 'HQuA9EbLiYcFr3A=',
'requestTime': '21/May/2018:23:58:49 +0000', 'path': '/dev/strategies/submit/github',
'accountId': '905951628980', 'protocol': 'HTTP/1.1', 'stage': 'dev',
'requestTimeEpoch': 1526947129330, 'requestId': 'e7d99de1-5d52-11e8-a559-fb527c3a0860',
'identity': {'cognitoIdentityPoolId': None, 'accountId': None,
'cognitoIdentityId': None, 'caller': None, 'sourceIp': '192.30.252.39',
'accessKey': None, 'cognitoAuthenticationType': None,
'cognitoAuthenticationProvider': None, 'userArn': None,
'userAgent': 'GitHub-Hookshot/419cd30', 'user': None},
'apiId': '887c8k0tui'},
'body': '{"zen":"Mind your words, they are important.","hook_id":30374368,"hook":{"type":"Repository",'
'"id":30374368,"name":"web","active":true,"events":["push"],"config":{"content_type":"json",'
'"insecure_ssl":"0","secret":"********","url":"https://887c8k0tui'
'.execute-api.us-east-2.amazonaws.com/dev/strategies/submit/github"},"updated_at":"2018-05'
'-21T23:58:49Z","created_at":"2018'
'-05-21T23:58:49Z","url":"https://api.'
'github.com/repos/'
'berlinguyinca/freqtrade-trading-strategies/hooks/30374368","test_url":"https://api'
'.github.com/repos/berlinguyinca/freqtrade-trading-strategies/hooks/30374368/test","ping_url'
'":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/hooks/30374368/pings'
'","last_response":{"code":null,"status":"unused","message":null}},"repository":{"id":130613180,"'
'name":"freqtrade-trading-strategies","full_name":"berlinguyinca/freqtrade-trading-strategies",'
'"owner":{"login":"berlinguyinca","id":16364,"avatar_url":"https://avatars2.githubusercontent.com'
'/u/16364?v=4","gravatar_id":"","url":"https://api.github.com/users/berlinguyinca","html_url":"'
'https://github.com/berlinguyinca","followers_url":"https://api.github.com/users/berlinguyinca/'
'followers","following_url":"https://api.github.com/users/berlinguyinca/following{/other_user}",'
'"gists_url":"https://api.github.com/users/berlinguyinca/gists{/gist_id}","'
'starred_url":"https://api.github.com/users/berlinguyinca/starred{/owner}{/repo}","subscriptions_url'
'":"https://api.github.com/users/berlinguyinca/subscriptions","organizations_url":"'
'https://api.github.com/users/berlinguyinca/orgs","repos_url":"https://api.github.com/users'
'/berlinguyinca/repos","events_url":"https://api.github.com/users/berlinguyinca/events{/privacy}'
'","received_events_url":"https://api.github.com/users/berlinguyinca/received_events","type":"Us'
'er","site_admin":false},"private":false,"html_url":"https://github.com/berlinguyinca/freqtrade-'
'trading-strategies","description":"contains strategies for using freqtrade","fork":false,"url":"'
'https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies","forks_url":"https://a'
'pi.github.com/repos/berlinguyinca/freqtrade-trading-strategies/forks","keys_url":"https://api.gi'
'thub.com/repos/berlinguyinca/freqtrade-trading-strategies/keys{/key_id}","collaborators_url":"htt'
'ps://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/collaborators{/collaborator}'
'","teams_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/teams","ho'
'oks_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/hooks","issue_e'
'vents_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/issues/events'
'{/number}","events_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/'
'events","assignees_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/'
'assignees{/user}","branches_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-st'
'rategies/branches{/branch}","tags_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trad'
'ing-strategies/tags","blobs_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-st'
'rategies/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/berlinguyinca/freqtrade-tr'
'ading-strategies/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/berlinguyinca/freqtr'
'ade-trading-strategies/git/refs{/sha}","trees_url":"https://api.github.com/repos/berlinguyinca/fre'
'qtrade-trading-strategies/git/trees{/sha}","statuses_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/statuses/{sha}","languages_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/languages","stargazers_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/stargazers","contributors_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/contributors","subscribers_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/subscribers","subscription_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/subscription","commits_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/commits{/sha}","git_commits_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/git/commits{/sha}","comments_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/comments{/number}","issue_comment_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/issues/comments{/number}","contents_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/contents/{+path}","compare_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/compare/{base}...{head}","merges_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/merges","archive_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/downloads","issues_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/issues{/number}","pulls_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/pulls{/number}","milestones_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/milestones{/number}","notifications_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/labels{/name}","releases_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/releases{/id}","deployments_url":"https://api.github.com/repos/berlinguyinca/freqtrade-trading-strategies/deployments","created_at":"2018-04-22T22:31:25Z","updated_at":"2018-05-21T05:46:21Z","pushed_at":"2018-05-16T07:53:59Z","git_url":"git://github.com/berlinguyinca/freqtrade-trading-strategies.git","ssh_url":"git@github.com:berlinguyinca/freqtrade-trading-strategies.git","clone_url":"https://github.com/berlinguyinca/freqtrade-trading-strategies.git","svn_url":"https://github.com/berlinguyinca/freqtrade-trading-strategies","homepage":null,"size":67,"stargazers_count":11,"watchers_count":11,"language":"Python","has_issues":true,"has_projects":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":3,"mirror_url":null,"archived":false,"open_issues_count":1,"license":{"key":"mit","name":"MIT License","spdx_id":"MIT","url":"https://api.github.com/licenses/mit"},"forks":3,"open_issues":1,"watchers":11,"default_branch":"master"},"sender":{"login":"berlinguyinca","id":16364,"avatar_url":"https://avatars2.githubusercontent.com/u/16364?v=4","gravatar_id":"","url":"https://api.github.com/users/berlinguyinca","html_url":"https://github.com/berlinguyinca","followers_url":"https://api.github.com/users/berlinguyinca/followers","following_url":"https://api.github.com/users/berlinguyinca/following{/other_user}","gists_url":"https://api.github.com/users/berlinguyinca/gists{/gist_id}","starred_url":"https://api.github.com/users/berlinguyinca/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/berlinguyinca/subscriptions","organizations_url":"https://api.github.com/users/berlinguyinca/orgs","repos_url":"https://api.github.com/users/berlinguyinca/repos","events_url":"https://api.github.com/users/berlinguyinca/events{/privacy}","received_events_url":"https://api.github.com/users/berlinguyinca/received_events","type":"User","site_admin":false}}',
'isBase64Encoded': False}
aws.submit_github(event, {})

View File

@@ -1,40 +0,0 @@
from freqtrade.aws.trade import store, submit
from freqtrade.aws.tables import get_trade_table
import simplejson as json
from boto3.dynamodb.conditions import Key, Attr
def test_store(lambda_context):
store({
"Records": [
{
"Sns": {
"Subject": "trade",
"Message": json.dumps(
{
'id': 'GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG.MyFancyTestStrategy:BTC/USDT:test',
'trade': '2018-05-05 14:15:00 to 2018-05-18 00:40:00',
'pair': 'BTC/USDT',
'duration': 625,
'profit_percent': -0.20453928,
'profit_stake': -0.20514198,
'entry_date': '2018-05-05 14:15:00',
'exit_date': '2018-05-18 00:40:00'
}
)
}
}]
}
, {})
# trade table should not have 1 item in it, with our given key
table = get_trade_table()
response = table.query(
KeyConditionExpression=Key('id')
.eq('GCU4LW2XXZW3A3FM2XZJTEJHNWHTWDKY2DIJLCZJ5ULVZ4K7LZ7D23TG.MyFancyTestStrategy:BTC/USDT:test')
)
print(response)
assert 'Items' in response
assert len(response['Items']) == 1

View File

@@ -9,7 +9,6 @@ from unittest.mock import MagicMock
import arrow
import pytest
from jsonschema import validate
from sqlalchemy import create_engine
from telegram import Chat, Message, Update
from freqtrade.analyze import Analyze
@@ -49,7 +48,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
mocker.patch('freqtrade.freqtradebot.Analyze.get_signal', MagicMock())
return FreqtradeBot(config, create_engine('sqlite://'))
return FreqtradeBot(config)
def patch_coinmarketcap(mocker, value: Optional[Dict[str, float]] = None) -> None:
@@ -92,7 +91,14 @@ def default_conf():
"stoploss": -0.10,
"unfilledtimeout": 600,
"bid_strategy": {
"ask_last_balance": 0.0
"use_book_order": False,
"book_order_top": 6,
"ask_last_balance": 0.0,
},
"ask_strategy": {
"use_book_order": False,
"book_order_min": 1,
"book_order_max": 10
},
"exchange": {
"name": "bittrex",
@@ -112,7 +118,8 @@ def default_conf():
"chat_id": "0"
},
"initial_state": "running",
"loglevel": logging.DEBUG
"db_url": "sqlite://",
"loglevel": logging.DEBUG,
}
validate(configuration, constants.CONF_SCHEMA)
return configuration
@@ -613,66 +620,3 @@ def buy_order_fee():
'status': 'closed',
'fee': None
}
@pytest.fixture
def lambda_context():
# mock the different AWS features we need
sns = moto.mock_sns()
sns.start()
dynamo = moto.mock_dynamodb2()
dynamo.start()
lamb = moto.mock_lambda()
lamb.start()
ecs = moto.mock_ecs()
ecs.start()
cluster = boto3.client('ecs')
cluster.create_cluster(clusterName='fargate')
cluster.register_task_definition(
containerDefinitions=[
{
'name': 'freqtrade-backtesting',
'command': [
'sleep',
'360',
],
'cpu': 10,
'essential': True,
'image': 'busybox',
'memory': 10,
},
],
family='sleep360',
taskRoleArn='',
volumes=[
],
)
session = boto3.session.Session()
os.environ["strategyTable"] = "StrategyTable"
os.environ["tradeTable"] = "TradeTable"
os.environ["topic"] = "UnitTestTopic"
os.environ["BASE_URL"] = "http://127.0.0.1/test"
client = session.client('sns')
client.create_topic(Name=os.environ["topic"])
dynamodb = boto3.resource('dynamodb')
import responses
# do not mock requests to these urls
responses.add_passthru('https://api.github.com')
responses.add_passthru('https://bittrex.com')
responses.add_passthru('https://api.binance.com')
responses.add_passthru('https://freq.isaac.international')
yield
sns.stop()
dynamo.stop()
lamb.stop()
ecs.stop()

View File

@@ -310,9 +310,19 @@ def test_get_ticker(default_conf, mocker):
# if not fetching a new result we should get the cached ticker
ticker = get_ticker(pair='ETH/BTC')
assert api_mock.fetch_ticker.call_count == 1
assert ticker['bid'] == 0.5
assert ticker['ask'] == 1
assert 'ETH/BTC' in exchange._CACHED_TICKER
assert exchange._CACHED_TICKER['ETH/BTC']['bid'] == 0.5
assert exchange._CACHED_TICKER['ETH/BTC']['ask'] == 1
# Test caching
api_mock.fetch_ticker = MagicMock()
get_ticker(pair='ETH/BTC', refresh=False)
assert api_mock.fetch_ticker.call_count == 0
with pytest.raises(TemporaryError): # test retrier
api_mock.fetch_ticker = MagicMock(side_effect=ccxt.NetworkError)
mocker.patch('freqtrade.exchange._API', api_mock)
@@ -323,6 +333,10 @@ def test_get_ticker(default_conf, mocker):
mocker.patch('freqtrade.exchange._API', api_mock)
get_ticker(pair='ETH/BTC', refresh=True)
api_mock.fetch_ticker = MagicMock(return_value={})
mocker.patch('freqtrade.exchange._API', api_mock)
get_ticker(pair='ETH/BTC', refresh=True)
def make_fetch_ohlcv_mock(data):
def fetch_ohlcv_mock(pair, timeframe, since):
@@ -393,6 +407,78 @@ def test_get_ticker_history(default_conf, mocker):
get_ticker_history('EFGH/BTC', default_conf['ticker_interval'])
def test_get_ticker_history_sort(default_conf, mocker):
api_mock = MagicMock()
# GDAX use-case (real data from GDAX)
# This ticker history is ordered DESC (newest first, oldest last)
tick = [
[1527833100000, 0.07666, 0.07671, 0.07666, 0.07668, 16.65244264],
[1527832800000, 0.07662, 0.07666, 0.07662, 0.07666, 1.30051526],
[1527832500000, 0.07656, 0.07661, 0.07656, 0.07661, 12.034778840000001],
[1527832200000, 0.07658, 0.07658, 0.07655, 0.07656, 0.59780186],
[1527831900000, 0.07658, 0.07658, 0.07658, 0.07658, 1.76278136],
[1527831600000, 0.07658, 0.07658, 0.07658, 0.07658, 2.22646521],
[1527831300000, 0.07655, 0.07657, 0.07655, 0.07657, 1.1753],
[1527831000000, 0.07654, 0.07654, 0.07651, 0.07651, 0.8073060299999999],
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
mocker.patch('freqtrade.exchange._API', api_mock)
# Test the ticker history sort
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527830400000
assert ticks[0][1] == 0.07649
assert ticks[0][2] == 0.07651
assert ticks[0][3] == 0.07649
assert ticks[0][4] == 0.07651
assert ticks[0][5] == 2.5734867
assert ticks[9][0] == 1527833100000
assert ticks[9][1] == 0.07666
assert ticks[9][2] == 0.07671
assert ticks[9][3] == 0.07666
assert ticks[9][4] == 0.07668
assert ticks[9][5] == 16.65244264
# Bittrex use-case (real data from Bittrex)
# This ticker history is ordered ASC (oldest first, newest last)
tick = [
[1527827700000, 0.07659999, 0.0766, 0.07627, 0.07657998, 1.85216924],
[1527828000000, 0.07657995, 0.07657995, 0.0763, 0.0763, 26.04051037],
[1527828300000, 0.0763, 0.07659998, 0.0763, 0.0764, 10.36434124],
[1527828600000, 0.0764, 0.0766, 0.0764, 0.0766, 5.71044773],
[1527828900000, 0.0764, 0.07666998, 0.0764, 0.07666998, 47.48888565],
[1527829200000, 0.0765, 0.07672999, 0.0765, 0.07672999, 3.37640326],
[1527829500000, 0.0766, 0.07675, 0.0765, 0.07675, 8.36203831],
[1527829800000, 0.07675, 0.07677999, 0.07620002, 0.076695, 119.22963884],
[1527830100000, 0.076695, 0.07671, 0.07624171, 0.07671, 1.80689244],
[1527830400000, 0.07671, 0.07674399, 0.07629216, 0.07655213, 2.31452783]
]
type(api_mock).has = PropertyMock(return_value={'fetchOHLCV': True})
api_mock.fetch_ohlcv = MagicMock(side_effect=make_fetch_ohlcv_mock(tick))
mocker.patch('freqtrade.exchange._API', api_mock)
# Test the ticker history sort
ticks = get_ticker_history('ETH/BTC', default_conf['ticker_interval'])
assert ticks[0][0] == 1527827700000
assert ticks[0][1] == 0.07659999
assert ticks[0][2] == 0.0766
assert ticks[0][3] == 0.07627
assert ticks[0][4] == 0.07657998
assert ticks[0][5] == 1.85216924
assert ticks[9][0] == 1527830400000
assert ticks[9][1] == 0.07671
assert ticks[9][2] == 0.07674399
assert ticks[9][3] == 0.07629216
assert ticks[9][4] == 0.07655213
assert ticks[9][5] == 2.31452783
def test_cancel_order_dry_run(default_conf, mocker):
default_conf['dry_run'] = True
mocker.patch.dict('freqtrade.exchange._CONF', default_conf)

View File

@@ -13,7 +13,7 @@ from arrow import Arrow
from freqtrade import optimize
from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments
from freqtrade.arguments import Arguments, TimeRange
from freqtrade.optimize.backtesting import Backtesting, start, setup_configuration
from freqtrade.tests.conftest import log_has
@@ -30,7 +30,7 @@ def trim_dictlist(dict_list, num):
def load_data_test(what):
timerange = ((None, 'line'), None, -100)
timerange = TimeRange(None, 'line', 0, -101)
data = optimize.load_data(None, ticker_interval='1m',
pairs=['UNITTEST/BTC'], timerange=timerange)
pair = data['UNITTEST/BTC']
@@ -84,6 +84,7 @@ def load_data_test(what):
def simple_backtest(config, contour, num_results, mocker) -> None:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(config)
data = load_data_test(contour)
@@ -97,6 +98,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
'realistic': True
}
)
# results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results
@@ -110,14 +112,14 @@ def mocked_load_data(datadir, pairs=[], ticker_interval='0m', refresh_pairs=Fals
# use for mock freqtrade.exchange.get_ticker_history'
def _load_pair_as_ticks(pair, tickfreq):
ticks = optimize.load_data(None, ticker_interval=tickfreq, pairs=[pair])
ticks = trim_dictlist(ticks, -200)
ticks = trim_dictlist(ticks, -201)
return ticks[pair]
# FIX: fixturize this?
def _make_backtest_conf(mocker, conf=None, pair='UNITTEST/BTC', record=None):
data = optimize.load_data(None, ticker_interval='8m', pairs=[pair])
data = trim_dictlist(data, -200)
data = trim_dictlist(data, -201)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(conf)
return {
@@ -181,7 +183,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -218,7 +220,8 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--realistic-simulation',
'--refresh-pairs-cached',
'--timerange', ':100',
'--export', '/bar/foo'
'--export', '/bar/foo',
'--export-filename', 'foo_bar.json'
]
config = setup_configuration(get_args(args))
@@ -229,7 +232,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -259,6 +262,11 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'Parameter --export detected: {} ...'.format(config['export']),
caplog.record_tuples
)
assert 'exportfilename' in config
assert log_has(
'Storing backtest results to {} ...'.format(config['exportfilename']),
caplog.record_tuples
)
def test_start(mocker, fee, default_conf, caplog) -> None:
@@ -286,23 +294,6 @@ def test_start(mocker, fee, default_conf, caplog) -> None:
assert start_mock.call_count == 1
def test_backtesting__init__(mocker, default_conf) -> None:
"""
Test Backtesting.__init__() method
"""
init_mock = MagicMock()
mocker.patch('freqtrade.optimize.backtesting.Backtesting._init', init_mock)
backtesting = Backtesting(default_conf)
assert backtesting.config == default_conf
assert backtesting.analyze is None
assert backtesting.ticker_interval is None
assert backtesting.tickerdata_to_dataframe is None
assert backtesting.populate_buy_trend is None
assert backtesting.populate_sell_trend is None
assert init_mock.call_count == 1
def test_backtesting_init(mocker, default_conf) -> None:
"""
Test Backtesting._init() method
@@ -322,13 +313,13 @@ def test_tickerdata_to_dataframe(default_conf, mocker) -> None:
Test Backtesting.tickerdata_to_dataframe() method
"""
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
timerange = ((None, 'line'), None, -100)
timerange = TimeRange(None, 'line', 0, -100)
tick = optimize.load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
backtesting = Backtesting(default_conf)
data = backtesting.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 100
assert len(data['UNITTEST/BTC']) == 99
# Load Analyze to compare the result between Backtesting function and Analyze are the same
analyze = Analyze(default_conf)
@@ -352,7 +343,7 @@ def test_get_timeframe(default_conf, mocker) -> None:
)
min_date, max_date = backtesting.get_timeframe(data)
assert min_date.isoformat() == '2017-11-04T23:02:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:59:00+00:00'
assert max_date.isoformat() == '2017-11-14T22:58:00+00:00'
def test_generate_text_table(default_conf, mocker):
@@ -374,16 +365,11 @@ def test_generate_text_table(default_conf, mocker):
)
result_str = (
'pair buy count avg profit % '
'total profit BTC avg duration profit loss\n'
'------- ----------- -------------- '
'------------------ -------------- -------- ------\n'
'ETH/BTC 2 15.00 '
'0.60000000 20.0 2 0\n'
'TOTAL 2 15.00 '
'0.60000000 20.0 2 0'
"""| pair | buy count | avg profit % | cum profit % | total profit BTC | avg duration | profit | loss |
|:--------|------------:|---------------:|---------------:|-------------------:|---------------:|---------:|-------:|
| ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |
| TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |"""
)
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
@@ -428,6 +414,40 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples)
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method if no data is found
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = "1m"
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples)
def test_backtest(default_conf, fee, mocker) -> None:
"""
Test Backtesting.backtest() method
@@ -574,6 +594,7 @@ def test_backtest_record(default_conf, fee, mocker):
results = backtesting.backtest(backtest_conf)
assert len(results) == 3
# Assert file_dump_json was only called once
print(names)
assert names == ['backtest-result.json']
records = records[0]
# Ensure records are of correct type
@@ -618,10 +639,12 @@ def test_backtest_start_live(default_conf, mocker, caplog):
args = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', 'freqtrade/tests/testdata',
'backtesting',
'--ticker-interval', '1m',
'--live',
'--timerange', '-100'
'--timerange', '-100',
'--realistic-simulation'
]
args = get_args(args)
start(args)
@@ -631,13 +654,14 @@ def test_backtest_start_live(default_conf, mocker, caplog):
'Using ticker_interval: 1m ...',
'Parameter -l/--live detected ...',
'Using max_open_trades: 1 ...',
'Parameter --timerange detected: -100 ..',
'Parameter --datadir detected: freqtrade/tests/testdata ...',
'Parameter --timerange detected: -100 ...',
'Using data folder: freqtrade/tests/testdata ...',
'Using stake_currency: BTC ...',
'Using stake_amount: 0.001 ...',
'Downloading data for all pairs in whitelist ...',
'Measuring data from 2017-11-14T19:32:00+00:00 up to 2017-11-14T22:59:00+00:00 (0 days)..'
'Measuring data from 2017-11-14T19:31:00+00:00 up to 2017-11-14T22:58:00+00:00 (0 days)..',
'Parameter --realistic-simulation detected ...'
]
for line in exists:
log_has(line, caplog.record_tuples)
assert log_has(line, caplog.record_tuples)

View File

@@ -389,10 +389,12 @@ def test_start_uses_mongotrials(mocker, init_hyperopt, default_conf) -> None:
# test buy_strategy_generator def populate_buy_trend
# test optimizer if 'ro_t1' in params
def test_format_results():
def test_format_results(init_hyperopt):
"""
Test Hyperopt.format_results()
"""
# Test with BTC as stake_currency
trades = [
('ETH/BTC', 2, 2, 123),
('LTC/BTC', 1, 1, 123),
@@ -400,8 +402,21 @@ def test_format_results():
]
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration']
df = pd.DataFrame.from_records(trades, columns=labels)
x = Hyperopt.format_results(df)
assert x.find(' 66.67%')
result = _HYPEROPT.format_results(df)
assert result.find(' 66.67%')
assert result.find('Total profit 1.00000000 BTC')
assert result.find('2.0000Σ %')
# Test with EUR as stake_currency
trades = [
('ETH/EUR', 2, 2, 123),
('LTC/EUR', 1, 1, 123),
('XPR/EUR', -1, -2, -246)
]
df = pd.DataFrame.from_records(trades, columns=labels)
result = _HYPEROPT.format_results(df)
assert result.find('Total profit 1.00000000 EUR')
def test_signal_handler(mocker, init_hyperopt):

View File

@@ -11,6 +11,7 @@ from freqtrade.misc import file_dump_json
from freqtrade.optimize.__init__ import make_testdata_path, download_pairs, \
download_backtesting_testdata, load_tickerdata_file, trim_tickerlist, \
load_cached_data_for_updating
from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has
# Change this if modifying UNITTEST/BTC testdatafile
@@ -99,7 +100,21 @@ def test_load_data_with_new_pair_1min(ticker_history, mocker, caplog) -> None:
file = os.path.join(os.path.dirname(__file__), '..', 'testdata', 'MEME_BTC-1m.json')
_backup_file(file)
optimize.load_data(None, ticker_interval='1m', pairs=['MEME/BTC'])
# do not download a new pair if refresh_pairs isn't set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=False,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is False
assert log_has('No data for pair: "MEME/BTC", Interval: 1m. '
'Use --refresh-pairs-cached to download the data',
caplog.record_tuples)
# download a new pair if refresh_pairs is set
optimize.load_data(None,
ticker_interval='1m',
refresh_pairs=True,
pairs=['MEME/BTC'])
assert os.path.isfile(file) is True
assert log_has('Download the pair: "MEME/BTC", Interval: 1m', caplog.record_tuples)
_clean_test_file(file)
@@ -162,7 +177,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# timeframe starts earlier than the cached data
# should fully update data
timerange = (('date', None), test_data[0][0] / 1000 - 1, None)
timerange = TimeRange('date', None, test_data[0][0] / 1000 - 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -173,13 +188,13 @@ def test_load_cached_data_for_updating(mocker) -> None:
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 120
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
((None, 'line'), None, -num_lines))
TimeRange(None, 'line', 0, -num_lines))
assert data == []
assert start_ts < test_data[0][0] - 1
# timeframe starts in the center of the cached data
# should return the chached data w/o the last item
timerange = (('date', None), test_data[0][0] / 1000 + 1, None)
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -188,7 +203,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# same with 'line' timeframe
num_lines = (test_data[-1][0] - test_data[1][0]) / 1000 / 60 + 30
timerange = ((None, 'line'), None, -num_lines)
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -197,7 +212,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# timeframe starts after the chached data
# should return the chached data w/o the last item
timerange = (('date', None), test_data[-1][0] / 1000 + 1, None)
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 1, 0)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -206,7 +221,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# same with 'line' timeframe
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -216,7 +231,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# no timeframe is set
# should return the chached data w/o the last item
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename,
'1m',
timerange)
@@ -225,7 +240,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# no datafile exist
# should return timestamp start time
timerange = (('date', None), now_ts - 10000, None)
timerange = TimeRange('date', None, now_ts - 10000, 0)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
@@ -234,7 +249,7 @@ def test_load_cached_data_for_updating(mocker) -> None:
# same with 'line' timeframe
num_lines = 30
timerange = ((None, 'line'), None, -num_lines)
timerange = TimeRange(None, 'line', 0, -num_lines)
data, start_ts = load_cached_data_for_updating(test_filename + 'unexist',
'1m',
timerange)
@@ -329,7 +344,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(-\d+)$
# This pattern uses the latest N elements
timerange = ((None, 'line'), None, -5)
timerange = TimeRange(None, 'line', 0, -5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -339,7 +354,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(\d+)-$
# This pattern keep X element from the end
timerange = (('line', None), 5, None)
timerange = TimeRange('line', None, 5, 0)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -349,7 +364,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(\d+)-(\d+)$
# This pattern extract a window
timerange = (('index', 'index'), 5, 10)
timerange = TimeRange('index', 'index', 5, 10)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -360,7 +375,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(\d{8})-(\d{8})$
# This pattern extract a window between the dates
timerange = (('date', 'date'), ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
timerange = TimeRange('date', 'date', ticker_list[5][0] / 1000, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -371,7 +386,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^-(\d{8})$
# This pattern extracts elements from the start to the date
timerange = ((None, 'date'), None, ticker_list[10][0] / 1000 - 1)
timerange = TimeRange(None, 'date', 0, ticker_list[10][0] / 1000 - 1)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -381,7 +396,7 @@ def test_trim_tickerlist() -> None:
# Test the pattern ^(\d{8})-$
# This pattern extracts elements from the date to now
timerange = (('date', None), ticker_list[10][0] / 1000 - 1, None)
timerange = TimeRange('date', None, ticker_list[10][0] / 1000 - 1, None)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)
@@ -391,7 +406,7 @@ def test_trim_tickerlist() -> None:
# Test a wrong pattern
# This pattern must return the list unchanged
timerange = ((None, None), None, 5)
timerange = TimeRange(None, None, None, 5)
ticker = trim_tickerlist(ticker_list, timerange)
ticker_len = len(ticker)

View File

@@ -7,8 +7,6 @@ Unit test file for rpc/rpc.py
from datetime import datetime
from unittest.mock import MagicMock
from sqlalchemy import create_engine
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC
@@ -39,7 +37,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -89,7 +87,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -125,7 +123,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
@@ -182,7 +180,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
@@ -208,15 +206,30 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow()
trade.is_open = False
freqtradebot.create_trade()
trade = Trade.query.first()
# Simulate fulfilled LIMIT_BUY order for trade
trade.update(limit_buy_order)
# Update the ticker with a market going up
mocker.patch.multiple(
'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(),
get_ticker=ticker_sell_up
)
trade.update(limit_sell_order)
trade.close_date = datetime.utcnow()
trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
assert prec_satoshi(stats['profit_all_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_all_percent'], 6.2)
assert prec_satoshi(stats['profit_all_fiat'], 0.93255)
assert stats['trade_count'] == 1
assert prec_satoshi(stats['profit_all_coin'], 5.632e-05)
assert prec_satoshi(stats['profit_all_percent'], 2.81)
assert prec_satoshi(stats['profit_all_fiat'], 0.8448)
assert stats['trade_count'] == 2
assert stats['first_trade_date'] == 'just now'
assert stats['latest_trade_date'] == 'just now'
assert stats['avg_duration'] == '0:00:00'
@@ -245,7 +258,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
stake_currency = default_conf['stake_currency']
fiat_display_currency = default_conf['fiat_display_currency']
@@ -316,7 +329,7 @@ def test_rpc_balance_handle(default_conf, mocker):
get_balances=MagicMock(return_value=mock_balance)
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency'])
@@ -346,7 +359,7 @@ def test_rpc_start(mocker, default_conf) -> None:
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -374,7 +387,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
get_ticker=MagicMock()
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING
@@ -413,7 +426,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
get_fee=fee,
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -451,20 +464,44 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0
# make an limit-buy open trade
trade = Trade.query.filter(Trade.id == '1').first()
filled_amount = trade.amount / 2
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy'
'side': 'buy',
'filled': filled_amount
}
)
# check that the trade is called, which is done
# by ensuring exchange.cancel_order is called
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated
(error, res) = rpc.rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount
freqtradebot.create_trade()
trade = Trade.query.filter(Trade.id == '2').first()
amount = trade.amount
# make an limit-buy open trade, if there is no 'filled', don't sell it
mocker.patch(
'freqtrade.freqtradebot.exchange.get_order',
return_value={
'status': 'open',
'type': 'limit',
'side': 'buy',
'filled': None
}
)
# check that the trade is called, which is done by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('2')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 2
assert trade.amount == amount
freqtradebot.create_trade()
# make an limit-sell open trade
@@ -476,11 +513,11 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'side': 'sell'
}
)
(error, res) = rpc.rpc_forcesell('2')
(error, res) = rpc.rpc_forcesell('3')
assert not error
assert res == ''
# status quo, no exchange calls
assert cancel_order_mock.call_count == 1
assert cancel_order_mock.call_count == 2
def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
@@ -499,7 +536,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
# Create some test data
@@ -538,7 +575,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
get_fee=fee,
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count()

View File

@@ -11,7 +11,6 @@ from datetime import datetime
from random import randint
from unittest.mock import MagicMock
from sqlalchemy import create_engine
from telegram import Update, Message, Chat
from telegram.error import NetworkError
@@ -71,12 +70,12 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.call_count == 0
# number of handles registered
assert start_polling.dispatcher.add_handler.call_count == 11
assert start_polling.dispatcher.add_handler.call_count > 0
assert start_polling.start_polling.call_count == 1
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
"['count'], ['reload_conf'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples)
@@ -156,7 +155,7 @@ def test_authorized_only(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is True
assert log_has(
@@ -187,7 +186,7 @@ def test_authorized_only_unauthorized(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_handler(bot=MagicMock(), update=update)
assert dummy.state['called'] is False
assert not log_has(
@@ -217,7 +216,7 @@ def test_authorized_only_exception(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
dummy = DummyCls(FreqtradeBot(conf, create_engine('sqlite://')))
dummy = DummyCls(FreqtradeBot(conf))
dummy.dummy_exception(bot=MagicMock(), update=update)
assert dummy.state['called'] is False
assert not log_has(
@@ -263,7 +262,7 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -301,7 +300,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -348,7 +347,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
conf = deepcopy(default_conf)
conf['stake_amount'] = 15.0
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -402,7 +401,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -470,7 +469,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Try invalid data
@@ -511,7 +510,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._profit(bot=MagicMock(), update=update)
@@ -608,7 +607,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
@@ -638,7 +637,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._balance(bot=MagicMock(), update=update)
@@ -661,7 +660,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -685,7 +684,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
@@ -710,7 +709,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
@@ -735,7 +734,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -746,6 +745,29 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_reload_conf_handle(default_conf, update, mocker) -> None:
""" Test _reload_conf() method """
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._reload_conf(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
"""
Test _forcesell() method
@@ -762,7 +784,7 @@ def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, moc
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -802,7 +824,7 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee, ticker_sell_do
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -847,7 +869,7 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, mocker) -> None
get_fee=fee
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -880,7 +902,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Trader is not running
@@ -927,7 +949,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
get_fee=fee
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Create some test data
@@ -962,7 +984,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
# Trader is not running
@@ -991,7 +1013,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
buy=MagicMock(return_value={'id': 'mocked_order_id'})
)
mocker.patch('freqtrade.optimize.backtesting.exchange.get_fee', fee)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.STOPPED
@@ -1027,7 +1049,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
_init=MagicMock(),
send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._help(bot=MagicMock(), update=update)
@@ -1047,7 +1069,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
_init=MagicMock(),
send_msg=msg_mock
)
freqtradebot = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
telegram._version(bot=MagicMock(), update=update)
@@ -1064,7 +1086,7 @@ def test_send_msg(default_conf, mocker) -> None:
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
conf = deepcopy(default_conf)
bot = MagicMock()
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = False
@@ -1087,7 +1109,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
conf = deepcopy(default_conf)
bot = MagicMock()
bot.send_message = MagicMock(side_effect=NetworkError('Oh snap'))
freqtradebot = FreqtradeBot(conf, create_engine('sqlite://'))
freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True

View File

@@ -28,10 +28,10 @@ def test_load_strategy(result):
def test_load_strategy_from_url(result):
resolver = StrategyResolver()
resolver._load_strategy('https://raw.githubusercontent.com/berlinguyinca'
'/freqtrade-trading-strategies'
'/master/user_data/strategies/Simple.py')
assert hasattr(resolver.strategy, 'populate_indicators')
resolver._load_strategy('https://freq.isaac.international/'
'dev/strategies/GBPAQEFGGWCMWVFU34P'
'MVGS4P2NJR4IDFNVI4LTCZAKJAD3JCXUMBI4J/AverageStrategy/code')
assert hasattr(resolver.strategy, 'minimal_roi')
assert 'adx' in resolver.strategy.populate_indicators(result)
@@ -42,7 +42,8 @@ def test_load_strategy_custom_directory(result):
if os.name == 'nt':
with pytest.raises(
FileNotFoundError,
match="FileNotFoundError: [WinError 3] The system cannot find the path specified: '{}'".format(extra_dir)):
match="FileNotFoundError: [WinError 3] The system cannot find the "
"path specified: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
else:
with pytest.raises(

View File

@@ -13,6 +13,7 @@ from pandas import DataFrame
from freqtrade.analyze import Analyze, SignalType
from freqtrade.optimize.__init__ import load_tickerdata_file
from freqtrade.arguments import TimeRange
from freqtrade.tests.conftest import log_has
# Avoid to reinit the same object again and again
@@ -45,7 +46,7 @@ def test_analyze_object() -> None:
def test_dataframe_correct_length(result):
dataframe = Analyze.parse_ticker_dataframe(result)
assert len(result.index) == len(dataframe.index)
assert len(result.index) - 1 == len(dataframe.index) # last partial candle removed
def test_dataframe_correct_columns(result):
@@ -183,8 +184,8 @@ def test_tickerdata_to_dataframe(default_conf) -> None:
"""
analyze = Analyze(default_conf)
timerange = ((None, 'line'), None, -100)
timerange = TimeRange(None, 'line', 0, -100)
tick = load_tickerdata_file(None, 'UNITTEST/BTC', '1m', timerange=timerange)
tickerlist = {'UNITTEST/BTC': tick}
data = analyze.tickerdata_to_dataframe(tickerlist)
assert len(data['UNITTEST/BTC']) == 100
assert len(data['UNITTEST/BTC']) == 99 # partial candle was removed

View File

@@ -9,7 +9,7 @@ import logging
import pytest
from freqtrade.arguments import Arguments
from freqtrade.arguments import Arguments, TimeRange
def test_arguments_object() -> None:
@@ -46,6 +46,11 @@ def test_parse_args_config() -> None:
assert args.config == '/dev/null'
def test_parse_args_db_url() -> None:
args = Arguments(['--db-url', 'sqlite:///test.sqlite'], '').get_parsed_arg()
assert args.db_url == 'sqlite:///test.sqlite'
def test_parse_args_verbose() -> None:
args = Arguments(['-v'], '').get_parsed_arg()
assert args.loglevel == logging.DEBUG
@@ -58,6 +63,7 @@ def test_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options()
args = arguments.get_parsed_arg()
print(args.pair)
assert args.pair == 'ETH/BTC'
@@ -107,14 +113,24 @@ def test_parse_args_dynamic_whitelist_invalid_values() -> None:
def test_parse_timerange_incorrect() -> None:
assert ((None, 'line'), None, -200) == Arguments.parse_timerange('-200')
assert (('line', None), 200, None) == Arguments.parse_timerange('200-')
assert (('index', 'index'), 200, 500) == Arguments.parse_timerange('200-500')
assert TimeRange(None, 'line', 0, -200) == Arguments.parse_timerange('-200')
assert TimeRange('line', None, 200, 0) == Arguments.parse_timerange('200-')
assert TimeRange('index', 'index', 200, 500) == Arguments.parse_timerange('200-500')
assert (('date', None), 1274486400, None) == Arguments.parse_timerange('20100522-')
assert ((None, 'date'), None, 1274486400) == Arguments.parse_timerange('-20100522')
assert TimeRange('date', None, 1274486400, 0) == Arguments.parse_timerange('20100522-')
assert TimeRange(None, 'date', 0, 1274486400) == Arguments.parse_timerange('-20100522')
timerange = Arguments.parse_timerange('20100522-20150730')
assert timerange == (('date', 'date'), 1274486400, 1438214400)
assert timerange == TimeRange('date', 'date', 1274486400, 1438214400)
# Added test for unix timestamp - BTC genesis date
assert TimeRange('date', None, 1231006505, 0) == Arguments.parse_timerange('1231006505-')
assert TimeRange(None, 'date', 0, 1233360000) == Arguments.parse_timerange('-1233360000')
timerange = Arguments.parse_timerange('1231006505-1233360000')
assert TimeRange('date', 'date', 1231006505, 1233360000) == timerange
# TODO: Find solution for the following case (passing timestamp in ms)
timerange = Arguments.parse_timerange('1231006505000-1233360000000')
assert TimeRange('date', 'date', 1231006505, 1233360000) != timerange
with pytest.raises(Exception, match=r'Incorrect syntax.*'):
Arguments.parse_timerange('-')
@@ -159,3 +175,19 @@ def test_parse_args_hyperopt_custom() -> None:
assert call_args.subparser == 'hyperopt'
assert call_args.spaces == ['buy']
assert call_args.func is not None
def test_testdata_dl_options() -> None:
args = [
'--pairs-file', 'file_with_pairs',
'--export', 'export/folder',
'--days', '30',
'--exchange', 'binance'
]
arguments = Arguments(args, '')
arguments.testdata_dl_options()
args = arguments.parse_args()
assert args.pairs_file == 'file_with_pairs'
assert args.export == 'export/folder'
assert args.days == 30
assert args.exchange == 'binance'

View File

@@ -6,6 +6,7 @@ Unit test file for configuration.py
import json
from copy import deepcopy
from unittest.mock import MagicMock
from argparse import Namespace
import pytest
from jsonschema import ValidationError
@@ -37,7 +38,7 @@ def test_load_config_invalid_pair(default_conf) -> None:
conf['exchange']['pair_whitelist'].append('ETH-BTC')
with pytest.raises(ValidationError, match=r'.*does not match.*'):
configuration = Configuration([])
configuration = Configuration(Namespace())
configuration._validate_config(conf)
@@ -49,7 +50,7 @@ def test_load_config_missing_attributes(default_conf) -> None:
conf.pop('exchange')
with pytest.raises(ValidationError, match=r'.*\'exchange\' is a required property.*'):
configuration = Configuration([])
configuration = Configuration(Namespace())
configuration._validate_config(conf)
@@ -61,7 +62,7 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf)
))
configuration = Configuration([])
configuration = Configuration(Namespace())
validated_conf = configuration._load_config_file('somefile')
assert file_mock.call_count == 1
assert validated_conf.items() >= default_conf.items()
@@ -79,12 +80,12 @@ def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
read_data=json.dumps(conf)
))
Configuration([])._load_config_file('somefile')
Configuration(Namespace())._load_config_file('somefile')
assert file_mock.call_count == 1
assert log_has('Validating configuration ...', caplog.record_tuples)
def test_load_config_file_exception(mocker, caplog) -> None:
def test_load_config_file_exception(mocker) -> None:
"""
Test Configuration._load_config_file() method
"""
@@ -92,14 +93,10 @@ def test_load_config_file_exception(mocker, caplog) -> None:
'freqtrade.configuration.open',
MagicMock(side_effect=FileNotFoundError('File not found'))
)
configuration = Configuration([])
configuration = Configuration(Namespace())
with pytest.raises(SystemExit):
with pytest.raises(OperationalException, match=r'.*Config file "somefile" not found!*'):
configuration._load_config_file('somefile')
assert log_has(
'Config file "somefile" not found. Please create your config file',
caplog.record_tuples
)
def test_load_config(default_conf, mocker) -> None:
@@ -117,7 +114,6 @@ def test_load_config(default_conf, mocker) -> None:
assert validated_conf.get('strategy') == 'DefaultStrategy'
assert validated_conf.get('strategy_path') is None
assert 'dynamic_whitelist' not in validated_conf
assert 'dry_run_db' not in validated_conf
def test_load_config_with_params(default_conf, mocker) -> None:
@@ -128,13 +124,13 @@ def test_load_config_with_params(default_conf, mocker) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--strategy-path', '/some/path',
'--dry-run-db',
'--db-url', 'sqlite:///someurl',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
validated_conf = configuration.load_config()
@@ -142,7 +138,7 @@ def test_load_config_with_params(default_conf, mocker) -> None:
assert validated_conf.get('dynamic_whitelist') == 10
assert validated_conf.get('strategy') == 'TestStrategy'
assert validated_conf.get('strategy_path') == '/some/path'
assert validated_conf.get('dry_run_db') is True
assert validated_conf.get('db_url') == 'sqlite:///someurl'
def test_load_custom_strategy(default_conf, mocker) -> None:
@@ -174,12 +170,12 @@ def test_show_info(default_conf, mocker, caplog) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--dynamic-whitelist', '10',
'--strategy', 'TestStrategy',
'--dry-run-db'
'--db-url', 'sqlite:///tmp/testdb',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
configuration.get_config()
@@ -191,23 +187,8 @@ def test_show_info(default_conf, mocker, caplog) -> None:
caplog.record_tuples
)
assert log_has(
'Parameter --dry-run-db detected ...',
caplog.record_tuples
)
assert log_has(
'Dry_run will use the DB file: "tradesv3.dry_run.sqlite"',
caplog.record_tuples
)
# Test the Dry run condition
configuration.config.update({'dry_run': False})
configuration._load_common_config(configuration.config)
assert log_has(
'Dry run is disabled. (--dry_run_db ignored)',
caplog.record_tuples
)
assert log_has('Using DB: "sqlite:///tmp/testdb"', caplog.record_tuples)
assert log_has('Dry run is enabled', caplog.record_tuples)
def test_setup_configuration_without_arguments(mocker, default_conf, caplog) -> None:
@@ -218,13 +199,13 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'backtesting'
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -235,7 +216,7 @@ def test_setup_configuration_without_arguments(mocker, default_conf, caplog) ->
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -262,7 +243,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'--config', 'config.json',
'--strategy', 'DefaultStrategy',
'--datadir', '/foo/bar',
@@ -275,7 +256,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
'--export', '/bar/foo'
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -286,7 +267,7 @@ def test_setup_configuration_with_arguments(mocker, default_conf, caplog) -> Non
assert 'pair_whitelist' in config['exchange']
assert 'datadir' in config
assert log_has(
'Parameter --datadir detected: {} ...'.format(config['datadir']),
'Using data folder: {} ...'.format(config['datadir']),
caplog.record_tuples
)
assert 'ticker_interval' in config
@@ -326,14 +307,14 @@ def test_hyperopt_with_arguments(mocker, default_conf, caplog) -> None:
read_data=json.dumps(default_conf)
))
args = [
arglist = [
'hyperopt',
'--epochs', '10',
'--use-mongodb',
'--spaces', 'all',
]
args = Arguments(args, '').get_parsed_arg()
args = Arguments(arglist, '').get_parsed_arg()
configuration = Configuration(args)
config = configuration.get_config()
@@ -357,7 +338,7 @@ def test_check_exchange(default_conf) -> None:
Test the configuration validator with a missing attribute
"""
conf = deepcopy(default_conf)
configuration = Configuration([])
configuration = Configuration(Namespace())
# Test a valid exchange
conf.get('exchange').update({'name': 'BITTREX'})

View File

@@ -6,8 +6,10 @@ from unittest.mock import MagicMock
import pytest
from requests.exceptions import RequestException
from freqtrade.fiat_convert import CryptoFiat, CryptoToFiatConverter
from freqtrade.tests.conftest import patch_coinmarketcap
from freqtrade.tests.conftest import log_has, patch_coinmarketcap
def test_pair_convertion_object():
@@ -88,6 +90,13 @@ def test_fiat_convert_find_price(mocker):
assert fiat_convert.get_price(crypto_symbol='BTC', fiat_symbol='EUR') == 13000.2
def test_fiat_convert_unsupported_crypto(mocker, caplog):
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._cryptomap', return_value=[])
fiat_convert = CryptoToFiatConverter()
assert fiat_convert._find_price(crypto_symbol='CRYPTO_123', fiat_symbol='EUR') == 0.0
assert log_has('unsupported crypto-symbol CRYPTO_123 - returning 0.0', caplog.record_tuples)
def test_fiat_convert_get_price(mocker):
api_mock = MagicMock(return_value={
'price_usd': 28000.0,
@@ -124,6 +133,20 @@ def test_fiat_convert_get_price(mocker):
assert fiat_convert._pairs[0]._expiration is not expiration
def test_fiat_convert_same_currencies(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='USD') == 1.0
def test_fiat_convert_two_FIAT(mocker):
patch_coinmarketcap(mocker)
fiat_convert = CryptoToFiatConverter()
assert fiat_convert.get_price(crypto_symbol='USD', fiat_symbol='EUR') == 0.0
def test_loadcryptomap(mocker):
patch_coinmarketcap(mocker)
@@ -133,6 +156,22 @@ def test_loadcryptomap(mocker):
assert fiat_convert._cryptomap["BTC"] == "1"
def test_fiat_init_network_exception(mocker):
# Because CryptoToFiatConverter is a Singleton we reset the listings
listmock = MagicMock(side_effect=RequestException)
mocker.patch.multiple(
'freqtrade.fiat_convert.Market',
listings=listmock,
)
# with pytest.raises(RequestEsxception):
fiat_convert = CryptoToFiatConverter()
fiat_convert._cryptomap = {}
fiat_convert._load_cryptomap()
length_cryptomap = len(fiat_convert._cryptomap)
assert length_cryptomap == 0
def test_fiat_convert_without_network():
# Because CryptoToFiatConverter is a Singleton we reset the value of _coinmarketcap
@@ -144,3 +183,22 @@ def test_fiat_convert_without_network():
assert fiat_convert._coinmarketcap is None
assert fiat_convert._find_price(crypto_symbol='BTC', fiat_symbol='USD') == 0.0
CryptoToFiatConverter._coinmarketcap = cmc_temp
def test_convert_amount(mocker):
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter.get_price', return_value=12345.0)
fiat_convert = CryptoToFiatConverter()
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="USD"
)
assert result == 15184.35
result = fiat_convert.convert_amount(
crypto_amount=1.23,
crypto_symbol="BTC",
fiat_symbol="BTC"
)
assert result == 1.23

View File

@@ -13,7 +13,6 @@ from unittest.mock import MagicMock
import arrow
import pytest
import requests
from sqlalchemy import create_engine
from freqtrade import DependencyException, OperationalException, TemporaryError
from freqtrade.freqtradebot import FreqtradeBot
@@ -36,7 +35,7 @@ def get_patched_freqtradebot(mocker, config) -> FreqtradeBot:
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
patch_coinmarketcap(mocker)
return FreqtradeBot(config, create_engine('sqlite://'))
return FreqtradeBot(config)
def patch_get_signal(mocker, value=(True, False)) -> None:
@@ -69,7 +68,7 @@ def test_freqtradebot_object() -> None:
Test the FreqtradeBot object has the mandatory public methods
"""
assert hasattr(FreqtradeBot, 'worker')
assert hasattr(FreqtradeBot, 'clean')
assert hasattr(FreqtradeBot, 'cleanup')
assert hasattr(FreqtradeBot, 'create_trade')
assert hasattr(FreqtradeBot, 'get_target_bid')
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
@@ -94,7 +93,7 @@ def test_freqtradebot(mocker, default_conf) -> None:
assert freqtrade.state is State.STOPPED
def test_clean(mocker, default_conf, caplog) -> None:
def test_cleanup(mocker, default_conf, caplog) -> None:
"""
Test clean() method
"""
@@ -102,11 +101,8 @@ def test_clean(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.state == State.RUNNING
assert freqtrade.clean()
assert freqtrade.state == State.STOPPED
assert log_has('Stopping trader and cleaning up modules...', caplog.record_tuples)
freqtrade.cleanup()
assert log_has('Cleaning up modules ...', caplog.record_tuples)
assert mock_cleanup.call_count == 1
@@ -237,7 +233,7 @@ def test_create_trade(default_conf, ticker, limit_buy_order, fee, mocker) -> Non
# Save state of current whitelist
whitelist = deepcopy(default_conf['exchange']['pair_whitelist'])
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -274,7 +270,7 @@ def test_create_trade_minimal_amount(default_conf, ticker, limit_buy_order, fee,
conf = deepcopy(default_conf)
conf['stake_amount'] = 0.0005
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
rate, amount = buy_mock.call_args[0][1], buy_mock.call_args[0][2]
@@ -296,7 +292,7 @@ def test_create_trade_no_stake_amount(default_conf, ticker, limit_buy_order, fee
get_balance=MagicMock(return_value=default_conf['stake_amount'] * 0.5),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
with pytest.raises(DependencyException, match=r'.*stake amount.*'):
freqtrade.create_trade()
@@ -320,7 +316,7 @@ def test_create_trade_no_pairs(default_conf, ticker, limit_buy_order, fee, mocke
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
@@ -347,7 +343,7 @@ def test_create_trade_no_pairs_after_blacklist(default_conf, ticker,
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ["ETH/BTC"]
conf['exchange']['pair_blacklist'] = ["ETH/BTC"]
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
@@ -375,7 +371,7 @@ def test_create_trade_no_signal(default_conf, fee, mocker) -> None:
conf = deepcopy(default_conf)
conf['stake_amount'] = 10
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
Trade.query = MagicMock()
Trade.query.filter = MagicMock()
@@ -399,7 +395,7 @@ def test_process_trade_creation(default_conf, ticker, limit_buy_order,
get_order=MagicMock(return_value=limit_buy_order),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
@@ -440,7 +436,7 @@ def test_process_exchange_failures(default_conf, ticker, markets, mocker) -> Non
)
sleep_mock = mocker.patch('time.sleep', side_effect=lambda _: None)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
result = freqtrade._process()
assert result is False
assert sleep_mock.has_calls()
@@ -460,7 +456,7 @@ def test_process_operational_exception(default_conf, ticker, markets, mocker) ->
get_markets=markets,
buy=MagicMock(side_effect=OperationalException)
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.state == State.RUNNING
result = freqtrade._process()
@@ -486,7 +482,7 @@ def test_process_trade_handling(
get_order=MagicMock(return_value=limit_buy_order),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
assert not trades
@@ -503,27 +499,27 @@ def test_balance_fully_ask_side(mocker) -> None:
"""
Test get_target_bid() method
"""
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 0.0}})
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 0.0}})
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 20
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
def test_balance_fully_last_side(mocker) -> None:
"""
Test get_target_bid() method
"""
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
assert freqtrade.get_target_bid({'ask': 20, 'last': 10}) == 10
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
def test_balance_bigger_last_ask(mocker) -> None:
"""
Test get_target_bid() method
"""
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'ask_last_balance': 1.0}})
freqtrade = get_patched_freqtradebot(mocker, {'bid_strategy': {'use_book_order':False,'book_order_top':6,'ask_last_balance': 1.0}})
assert freqtrade.get_target_bid({'ask': 5, 'last': 10}) == 5
assert freqtrade.get_target_bid('ETH/BTC') >= 0.07
def test_process_maybe_execute_buy(mocker, default_conf) -> None:
@@ -570,8 +566,10 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
trade.open_fee = 0.001
assert not freqtrade.process_maybe_execute_sell(trade)
# Test amount not modified by fee-logic
assert not log_has('Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(
trade), caplog.record_tuples)
assert not log_has(
'Applying fee to amount for Trade {} from 90.99181073 to 90.81'.format(trade),
caplog.record_tuples
)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.get_real_amount', return_value=90.81)
# test amount modified by fee-logic
@@ -582,6 +580,38 @@ def test_process_maybe_execute_sell(mocker, default_conf, limit_buy_order, caplo
# Assert we call handle_trade() if trade is feasible for execution
assert freqtrade.process_maybe_execute_sell(trade)
regexp = re.compile('Found open order for.*')
assert filter(regexp.match, caplog.record_tuples)
def test_process_maybe_execute_sell_exception(mocker, default_conf,
limit_buy_order, caplog) -> None:
"""
Test the exceptions in process_maybe_execute_sell()
"""
freqtrade = get_patched_freqtradebot(mocker, default_conf)
mocker.patch('freqtrade.freqtradebot.exchange.get_order', return_value=limit_buy_order)
trade = MagicMock()
trade.open_order_id = '123'
trade.open_fee = 0.001
# Test raise of OperationalException exception
mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
side_effect=OperationalException()
)
freqtrade.process_maybe_execute_sell(trade)
assert log_has('could not update trade amount: ', caplog.record_tuples)
# Test raise of DependencyException exception
mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.get_real_amount',
side_effect=DependencyException()
)
freqtrade.process_maybe_execute_sell(trade)
assert log_has('Unable to sell trade: ', caplog.record_tuples)
def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mocker) -> None:
"""
@@ -603,7 +633,7 @@ def test_handle_trade(default_conf, limit_buy_order, limit_sell_order, fee, mock
)
patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
freqtrade.create_trade()
@@ -646,7 +676,7 @@ def test_handle_overlpapping_signals(default_conf, ticker, limit_buy_order, fee,
get_fee=fee,
)
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
@@ -705,7 +735,7 @@ def test_handle_trade_roi(default_conf, ticker, limit_buy_order, fee, mocker, ca
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=True)
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -742,7 +772,7 @@ def test_handle_trade_experimental(
)
mocker.patch('freqtrade.freqtradebot.Analyze.min_roi_reached', return_value=False)
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -770,7 +800,7 @@ def test_close_trade(default_conf, ticker, limit_buy_order, limit_sell_order, fe
buy=MagicMock(return_value={'id': limit_buy_order['id']}),
get_fee=fee,
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Create trade and sell it
freqtrade.create_trade()
@@ -801,7 +831,7 @@ def test_check_handle_timedout_buy(default_conf, ticker, limit_buy_order_old, fe
cancel_order=cancel_order_mock,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trade_buy = Trade(
pair='ETH/BTC',
@@ -841,7 +871,7 @@ def test_check_handle_timedout_sell(default_conf, ticker, limit_sell_order_old,
get_order=MagicMock(return_value=limit_sell_order_old),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trade_sell = Trade(
pair='ETH/BTC',
@@ -881,7 +911,7 @@ def test_check_handle_timedout_partial(default_conf, ticker, limit_buy_order_old
get_order=MagicMock(return_value=limit_buy_order_old_partial),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trade_buy = Trade(
pair='ETH/BTC',
@@ -929,7 +959,7 @@ def test_check_handle_timedout_exception(default_conf, ticker, mocker, caplog) -
get_order=MagicMock(side_effect=requests.exceptions.RequestException('Oh snap')),
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trade_buy = Trade(
pair='ETH/BTC',
@@ -968,7 +998,7 @@ def test_handle_timedout_limit_buy(mocker, default_conf) -> None:
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
Trade.session = MagicMock()
trade = MagicMock()
@@ -994,7 +1024,7 @@ def test_handle_timedout_limit_sell(mocker, default_conf) -> None:
cancel_order=cancel_order_mock
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
trade = MagicMock()
order = {'remaining': 1,
@@ -1021,7 +1051,7 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, mocker) -> N
get_fee=fee
)
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Create some test data
freqtrade.create_trade()
@@ -1062,7 +1092,7 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, mocker)
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Create some test data
freqtrade.create_trade()
@@ -1102,7 +1132,7 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Create some test data
freqtrade.create_trade()
@@ -1143,7 +1173,7 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
get_ticker=ticker,
get_fee=fee
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Create some test data
freqtrade.create_trade()
@@ -1192,7 +1222,7 @@ def test_sell_profit_only_enable_profit(default_conf, limit_buy_order, fee, mock
'use_sell_signal': True,
'sell_profit_only': True,
}
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -1225,7 +1255,7 @@ def test_sell_profit_only_disable_profit(default_conf, limit_buy_order, fee, moc
'use_sell_signal': True,
'sell_profit_only': False,
}
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -1258,7 +1288,7 @@ def test_sell_profit_only_enable_loss(default_conf, limit_buy_order, fee, mocker
'use_sell_signal': True,
'sell_profit_only': True,
}
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -1293,7 +1323,7 @@ def test_sell_profit_only_disable_loss(default_conf, limit_buy_order, fee, mocke
'sell_profit_only': False,
}
freqtrade = FreqtradeBot(conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(conf)
freqtrade.create_trade()
trade = Trade.query.first()
@@ -1321,7 +1351,7 @@ def test_get_real_amount_quote(default_conf, trades_for_order, buy_order_fee, ca
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
@@ -1348,7 +1378,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
@@ -1356,7 +1386,7 @@ def test_get_real_amount_no_trade(default_conf, buy_order_fee, caplog, mocker):
caplog.record_tuples)
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, caplog, mocker):
def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, mocker):
"""
Test get_real_amount - fees in Stake currency
"""
@@ -1375,7 +1405,7 @@ def test_get_real_amount_stake(default_conf, trades_for_order, buy_order_fee, ca
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@@ -1401,7 +1431,7 @@ def test_get_real_amount_BNB(default_conf, trades_for_order, buy_order_fee, mock
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
@@ -1424,7 +1454,7 @@ def test_get_real_amount_multi(default_conf, trades_for_order2, buy_order_fee, c
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount - (amount * 0.001)
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
@@ -1452,7 +1482,7 @@ def test_get_real_amount_fromorder(default_conf, trades_for_order, buy_order_fee
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount is reduced by "fee"
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount - 0.004
assert log_has('Applying fee on amount for Trade(id=None, pair=LTC/ETH, amount=8.00000000, '
@@ -1480,7 +1510,7 @@ def test_get_real_amount_invalid_order(default_conf, trades_for_order, buy_order
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, limit_buy_order) == amount
@@ -1505,6 +1535,31 @@ def test_get_real_amount_invalid(default_conf, trades_for_order, buy_order_fee,
open_rate=0.245441,
open_order_id="123456"
)
freqtrade = FreqtradeBot(default_conf, create_engine('sqlite://'))
freqtrade = FreqtradeBot(default_conf)
# Amount does not change
assert freqtrade.get_real_amount(trade, buy_order_fee) == amount
def test_get_real_amount_open_trade(default_conf, mocker):
"""
Test get_real_amount condition trade.fee_open == 0 or order['status'] == 'open'
"""
patch_get_signal(mocker)
patch_RPCManager(mocker)
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
amount = 12345
trade = Trade(
pair='LTC/ETH',
amount=amount,
exchange='binance',
open_rate=0.245441,
open_order_id="123456"
)
order = {
'id': 'mocked_order',
'amount': amount,
'status': 'open',
}
freqtrade = FreqtradeBot(default_conf)
assert freqtrade.get_real_amount(trade, order) == amount

View File

@@ -3,11 +3,16 @@ Unit test file for main.py
"""
import logging
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from freqtrade.main import main, set_loggers
from freqtrade import OperationalException
from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, set_loggers, reconfigure
from freqtrade.state import State
from freqtrade.tests.conftest import log_has
@@ -60,7 +65,7 @@ def test_set_loggers() -> None:
assert value2 is logging.INFO
def test_main(mocker, caplog) -> None:
def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
@@ -68,26 +73,140 @@ def test_main(mocker, caplog) -> None:
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(
side_effect=KeyboardInterrupt
),
clean=MagicMock(),
worker=MagicMock(side_effect=Exception),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit) as pytest_wrapped_e:
main(args)
log_has('Starting freqtrade', caplog.record_tuples)
log_has('Got SIGINT, aborting ...', caplog.record_tuples)
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 42
# Test the BaseException case
mocker.patch(
'freqtrade.freqtradebot.FreqtradeBot.worker',
MagicMock(side_effect=BaseException)
)
with pytest.raises(SystemExit):
main(args)
log_has('Got fatal exception!', caplog.record_tuples)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Fatal exception!', caplog.record_tuples)
def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=KeyboardInterrupt),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('SIGINT received, aborting ...', caplog.record_tuples)
def test_main_operational_exception(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
args = ['-c', 'config.json.example']
# Test Main + the KeyboardInterrupt exception
with pytest.raises(SystemExit):
main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Oh snap!', caplog.record_tuples)
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(return_value=State.RELOAD_CONF),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
# Raise exception as side effect to avoid endless loop
reconfigure_mock = mocker.patch(
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
)
with pytest.raises(SystemExit):
main(['-c', 'config.json.example'])
assert reconfigure_mock.call_count == 1
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
def test_reconfigure(mocker, default_conf) -> None:
""" Test recreate() function """
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtrade = FreqtradeBot(default_conf)
# Renew mock to return modified data
conf = deepcopy(default_conf)
conf['stake_amount'] += 1
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
# reconfigure should return a new instance
freqtrade2 = reconfigure(
freqtrade,
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
)
# Verify we have a new instance with the new config
assert freqtrade is not freqtrade2
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']

View File

@@ -39,7 +39,7 @@ def test_datesarray_to_datetimearray(ticker_history):
assert dates[0].minute == 50
date_len = len(dates)
assert date_len == 3
assert date_len == 2
def test_common_datearray(default_conf) -> None:

View File

@@ -1,9 +1,11 @@
# pragma pylint: disable=missing-docstring, C0103
import os
from copy import deepcopy
from unittest.mock import MagicMock
import pytest
from sqlalchemy import create_engine
from freqtrade import constants, OperationalException
from freqtrade.persistence import Trade, init, clean_dry_run_db
@@ -21,77 +23,54 @@ def test_init_create_session(default_conf, mocker):
assert 'Session' in type(Trade.session).__name__
def test_init_dry_run_db(default_conf, mocker):
default_conf.update({'dry_run_db': True})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_custom_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'sqlite:///tmp/freqtrade2_test.sqlite'})
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is True
# Delete the file made for this unitest and rollback to the previous
# tradesv3.dry_run.sqlite file
# 1. Delete file from the test
if os.path.isfile(dry_run_db):
os.remove(dry_run_db)
# 2. Rollback to the initial file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tmp/freqtrade2_test.sqlite'
def test_init_dry_run_without_db(default_conf, mocker):
default_conf.update({'dry_run_db': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
def test_init_invalid_db_url(default_conf, mocker):
conf = deepcopy(default_conf)
# First, protect the existing 'tradesv3.dry_run.sqlite' (Do not delete user data)
dry_run_db = 'tradesv3.dry_run.sqlite'
dry_run_db_swp = dry_run_db + '.swp'
# Update path to a value other than default, but still in-memory
conf.update({'db_url': 'unknown:///some.url'})
mocker.patch.dict('freqtrade.persistence._CONF', conf)
if os.path.isfile(dry_run_db):
os.rename(dry_run_db, dry_run_db_swp)
# Check if the new tradesv3.dry_run.sqlite was created
init(default_conf)
assert os.path.isfile(dry_run_db) is False
# Rollback to the initial 'tradesv3.dry_run.sqlite' file
if os.path.isfile(dry_run_db_swp):
os.rename(dry_run_db_swp, dry_run_db)
with pytest.raises(OperationalException, match=r'.*no valid database URL*'):
init(conf)
def test_init_prod_db(default_conf, mocker):
default_conf.update({'dry_run': False})
mocker.patch.dict('freqtrade.persistence._CONF', default_conf)
conf = deepcopy(default_conf)
conf.update({'dry_run': False})
conf.update({'db_url': constants.DEFAULT_DB_PROD_URL})
# First, protect the existing 'tradesv3.sqlite' (Do not delete user data)
prod_db = 'tradesv3.sqlite'
prod_db_swp = prod_db + '.swp'
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
if os.path.isfile(prod_db):
os.rename(prod_db, prod_db_swp)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite:///tradesv3.sqlite'
# Check if the new tradesv3.sqlite was created
init(default_conf)
assert os.path.isfile(prod_db) is True
# Delete the file made for this unitest and rollback to the previous tradesv3.sqlite file
def test_init_dryrun_db(default_conf, mocker):
conf = deepcopy(default_conf)
conf.update({'dry_run': True})
conf.update({'db_url': constants.DEFAULT_DB_DRYRUN_URL})
# 1. Delete file from the test
if os.path.isfile(prod_db):
os.remove(prod_db)
create_engine_mock = mocker.patch('freqtrade.persistence.create_engine', MagicMock())
mocker.patch.dict('freqtrade.persistence._CONF', conf)
# Rollback to the initial 'tradesv3.sqlite' file
if os.path.isfile(prod_db_swp):
os.rename(prod_db_swp, prod_db)
init(conf)
assert create_engine_mock.call_count == 1
assert create_engine_mock.mock_calls[0][1][0] == 'sqlite://'
@pytest.mark.usefixtures("init_persistence")
@@ -328,7 +307,7 @@ def test_calc_profit_percent(limit_buy_order, limit_sell_order, fee):
def test_clean_dry_run_db(default_conf, fee):
init(default_conf, create_engine('sqlite://'))
init(default_conf)
# Simulate dry_run entries
trade = Trade(
@@ -377,7 +356,7 @@ def test_clean_dry_run_db(default_conf, fee):
assert len(Trade.query.filter(Trade.open_order_id.isnot(None)).all()) == 1
def test_migrate_old(default_conf, fee):
def test_migrate_old(mocker, default_conf, fee):
"""
Test Database migration(starting with old pairformat)
"""
@@ -409,11 +388,13 @@ def test_migrate_old(default_conf, fee):
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
init(default_conf)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()
@@ -428,7 +409,7 @@ def test_migrate_old(default_conf, fee):
assert trade.exchange == "bittrex"
def test_migrate_new(default_conf, fee):
def test_migrate_new(mocker, default_conf, fee):
"""
Test Database migration (starting with new pairformat)
"""
@@ -444,6 +425,8 @@ def test_migrate_new(default_conf, fee):
close_profit FLOAT,
stake_amount FLOAT NOT NULL,
amount FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
open_date DATETIME NOT NULL,
close_date DATETIME,
open_order_id VARCHAR,
@@ -460,11 +443,13 @@ def test_migrate_new(default_conf, fee):
amount=amount
)
engine = create_engine('sqlite://')
mocker.patch('freqtrade.persistence.create_engine', lambda *args, **kwargs: engine)
# Create table using the old format
engine.execute(create_table_old)
engine.execute(insert_table_old)
# Run init to test migration
init(default_conf, engine)
init(default_conf)
assert len(Trade.query.filter(Trade.id == 1).all()) == 1
trade = Trade.query.filter(Trade.id == 1).first()