Merge pull request #58 from xsmile/exchange-interface
Exchange refactoring
This commit is contained in:
commit
5e0f143a38
@ -13,8 +13,8 @@
|
|||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"bittrex": {
|
"exchange": {
|
||||||
"enabled": true,
|
"name": "bittrex",
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
"pair_whitelist": [
|
"pair_whitelist": [
|
||||||
|
@ -1,36 +1,18 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
|
||||||
import arrow
|
|
||||||
import requests
|
|
||||||
from pandas import DataFrame
|
|
||||||
import talib.abstract as ta
|
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import talib.abstract as ta
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade.exchange import get_ticker_history
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str, minimum_date: arrow.Arrow) -> dict:
|
|
||||||
"""
|
|
||||||
Request ticker data from Bittrex for a given currency pair
|
|
||||||
"""
|
|
||||||
url = 'https://bittrex.com/Api/v2.0/pub/market/GetTicks'
|
|
||||||
headers = {
|
|
||||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
|
|
||||||
}
|
|
||||||
params = {
|
|
||||||
'marketName': pair.replace('_', '-'),
|
|
||||||
'tickInterval': 'fiveMin',
|
|
||||||
'_': minimum_date.timestamp * 1000
|
|
||||||
}
|
|
||||||
data = requests.get(url, params=params, headers=headers).json()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given pair
|
Analyses the trend for the given pair
|
||||||
@ -43,6 +25,7 @@ def parse_ticker_dataframe(ticker: list, minimum_date: arrow.Arrow) -> DataFrame
|
|||||||
.sort_values('date')
|
.sort_values('date')
|
||||||
return df[df['date'].map(arrow.get) > minimum_date]
|
return df[df['date'].map(arrow.get) > minimum_date]
|
||||||
|
|
||||||
|
|
||||||
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Adds several different TA indicators to the given DataFrame
|
Adds several different TA indicators to the given DataFrame
|
||||||
@ -87,7 +70,7 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
:return DataFrame with ticker data and indicator data
|
:return DataFrame with ticker data and indicator data
|
||||||
"""
|
"""
|
||||||
minimum_date = arrow.utcnow().shift(hours=-24)
|
minimum_date = arrow.utcnow().shift(hours=-24)
|
||||||
data = get_ticker(pair, minimum_date)
|
data = get_ticker_history(pair, minimum_date)
|
||||||
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
dataframe = parse_ticker_dataframe(data['result'], minimum_date)
|
||||||
|
|
||||||
if dataframe.empty:
|
if dataframe.empty:
|
||||||
@ -98,6 +81,7 @@ def analyze_ticker(pair: str) -> DataFrame:
|
|||||||
dataframe = populate_buy_trend(dataframe)
|
dataframe = populate_buy_trend(dataframe)
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
def get_buy_signal(pair: str) -> bool:
|
def get_buy_signal(pair: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Calculates a buy signal based several technical analysis indicators
|
Calculates a buy signal based several technical analysis indicators
|
||||||
|
@ -1,179 +0,0 @@
|
|||||||
import enum
|
|
||||||
import logging
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from bittrex.bittrex import Bittrex
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Current selected exchange
|
|
||||||
EXCHANGE = None
|
|
||||||
_API = None
|
|
||||||
_CONF = {}
|
|
||||||
|
|
||||||
|
|
||||||
class Exchange(enum.Enum):
|
|
||||||
BITTREX = 1
|
|
||||||
|
|
||||||
|
|
||||||
def init(config: dict) -> None:
|
|
||||||
"""
|
|
||||||
Initializes this module with the given config,
|
|
||||||
it does basic validation whether the specified
|
|
||||||
exchange and pairs are valid.
|
|
||||||
:param config: config to use
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
global _API, EXCHANGE
|
|
||||||
|
|
||||||
_CONF.update(config)
|
|
||||||
|
|
||||||
if config['dry_run']:
|
|
||||||
logger.info('Instance is running with dry_run enabled')
|
|
||||||
|
|
||||||
use_bittrex = config.get('bittrex', {}).get('enabled', False)
|
|
||||||
if use_bittrex:
|
|
||||||
EXCHANGE = Exchange.BITTREX
|
|
||||||
_API = Bittrex(api_key=config['bittrex']['key'], api_secret=config['bittrex']['secret'])
|
|
||||||
else:
|
|
||||||
raise RuntimeError('No exchange specified. Aborting!')
|
|
||||||
|
|
||||||
# Check if all pairs are available
|
|
||||||
validate_pairs(config[EXCHANGE.name.lower()]['pair_whitelist'])
|
|
||||||
|
|
||||||
|
|
||||||
def validate_pairs(pairs: List[str]) -> None:
|
|
||||||
"""
|
|
||||||
Checks if all given pairs are tradable on the current exchange.
|
|
||||||
Raises RuntimeError if one pair is not available.
|
|
||||||
:param pairs: list of pairs
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
markets = get_markets()
|
|
||||||
for pair in pairs:
|
|
||||||
if pair not in markets:
|
|
||||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
|
||||||
|
|
||||||
|
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit buy order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to purchase
|
|
||||||
:return: order_id of the placed buy order
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 'dry_run'
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
|
|
||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
|
||||||
"""
|
|
||||||
Places a limit sell order.
|
|
||||||
:param pair: Pair as str, format: BTC_ETH
|
|
||||||
:param rate: Rate limit for order
|
|
||||||
:param amount: The amount to sell
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 'dry_run'
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return data['result']['uuid']
|
|
||||||
|
|
||||||
|
|
||||||
def get_balance(currency: str) -> float:
|
|
||||||
"""
|
|
||||||
Get account balance.
|
|
||||||
:param currency: currency as str, format: BTC
|
|
||||||
:return: float
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return 999.9
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_balance(currency)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return float(data['result']['Balance'] or 0.0)
|
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str) -> dict:
|
|
||||||
"""
|
|
||||||
Get Ticker for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: dict
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return {
|
|
||||||
'bid': float(data['result']['Bid']),
|
|
||||||
'ask': float(data['result']['Ask']),
|
|
||||||
'last': float(data['result']['Last']),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_id: str) -> None:
|
|
||||||
"""
|
|
||||||
Cancel order for given order_id
|
|
||||||
:param order_id: id as str
|
|
||||||
:return: None
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
pass
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.cancel(order_id)
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
|
|
||||||
|
|
||||||
def get_open_orders(pair: str) -> List[dict]:
|
|
||||||
"""
|
|
||||||
Get all open orders for given pair.
|
|
||||||
:param pair: Pair as str, format: BTC_ETC
|
|
||||||
:return: list of dicts
|
|
||||||
"""
|
|
||||||
if _CONF['dry_run']:
|
|
||||||
return []
|
|
||||||
elif EXCHANGE == Exchange.BITTREX:
|
|
||||||
data = _API.get_open_orders(pair.replace('_', '-'))
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [{
|
|
||||||
'id': entry['OrderUuid'],
|
|
||||||
'type': entry['OrderType'],
|
|
||||||
'opened': entry['Opened'],
|
|
||||||
'rate': entry['PricePerUnit'],
|
|
||||||
'amount': entry['Quantity'],
|
|
||||||
'remaining': entry['QuantityRemaining'],
|
|
||||||
} for entry in data['result']]
|
|
||||||
|
|
||||||
|
|
||||||
def get_pair_detail_url(pair: str) -> str:
|
|
||||||
"""
|
|
||||||
Returns the market detail url for the given pair
|
|
||||||
:param pair: pair as str, format: BTC_ANT
|
|
||||||
:return: url as str
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange.BITTREX:
|
|
||||||
return 'https://bittrex.com/Market/Index?MarketName={}'.format(pair.replace('_', '-'))
|
|
||||||
|
|
||||||
|
|
||||||
def get_markets() -> List[str]:
|
|
||||||
"""
|
|
||||||
Returns all available markets
|
|
||||||
:return: list of all available pairs
|
|
||||||
"""
|
|
||||||
if EXCHANGE == Exchange. BITTREX:
|
|
||||||
data = _API.get_markets()
|
|
||||||
if not data['success']:
|
|
||||||
raise RuntimeError('BITTREX: {}'.format(data['message']))
|
|
||||||
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
|
115
freqtrade/exchange/__init__.py
Normal file
115
freqtrade/exchange/__init__.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
import enum
|
||||||
|
import logging
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
from freqtrade.exchange.bittrex import Bittrex
|
||||||
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Current selected exchange
|
||||||
|
EXCHANGE: Exchange = None
|
||||||
|
_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Exchanges(enum.Enum):
|
||||||
|
"""
|
||||||
|
Maps supported exchange names to correspondent classes.
|
||||||
|
"""
|
||||||
|
BITTREX = Bittrex
|
||||||
|
|
||||||
|
|
||||||
|
def init(config: dict) -> None:
|
||||||
|
"""
|
||||||
|
Initializes this module with the given config,
|
||||||
|
it does basic validation whether the specified
|
||||||
|
exchange and pairs are valid.
|
||||||
|
:param config: config to use
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
global _CONF, EXCHANGE
|
||||||
|
|
||||||
|
_CONF.update(config)
|
||||||
|
|
||||||
|
if config['dry_run']:
|
||||||
|
logger.info('Instance is running with dry_run enabled')
|
||||||
|
|
||||||
|
exchange_config = config['exchange']
|
||||||
|
|
||||||
|
# Find matching class for the given exchange name
|
||||||
|
name = exchange_config['name']
|
||||||
|
try:
|
||||||
|
exchange_class = Exchanges[name.upper()].value
|
||||||
|
except KeyError:
|
||||||
|
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||||
|
|
||||||
|
EXCHANGE = exchange_class(exchange_config)
|
||||||
|
|
||||||
|
# Check if all pairs are available
|
||||||
|
validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
|
|
||||||
|
def validate_pairs(pairs: List[str]) -> None:
|
||||||
|
"""
|
||||||
|
Checks if all given pairs are tradable on the current exchange.
|
||||||
|
Raises RuntimeError if one pair is not available.
|
||||||
|
:param pairs: list of pairs
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
markets = EXCHANGE.get_markets()
|
||||||
|
for pair in pairs:
|
||||||
|
if pair not in markets:
|
||||||
|
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
||||||
|
|
||||||
|
|
||||||
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 'dry_run'
|
||||||
|
|
||||||
|
return EXCHANGE.buy(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 'dry_run'
|
||||||
|
|
||||||
|
return EXCHANGE.sell(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
|
def get_balance(currency: str) -> float:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return 999.9
|
||||||
|
|
||||||
|
return EXCHANGE.get_balance(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ticker(pair: str) -> dict:
|
||||||
|
return EXCHANGE.get_ticker(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
|
||||||
|
return EXCHANGE.get_ticker_history(pair, minimum_date)
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_order(order_id: str) -> None:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return
|
||||||
|
|
||||||
|
return EXCHANGE.cancel_order(order_id)
|
||||||
|
|
||||||
|
|
||||||
|
def get_open_orders(pair: str) -> List[dict]:
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return EXCHANGE.get_open_orders(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_pair_detail_url(pair: str) -> str:
|
||||||
|
return EXCHANGE.get_pair_detail_url(pair)
|
||||||
|
|
||||||
|
|
||||||
|
def get_markets() -> List[str]:
|
||||||
|
return EXCHANGE.get_markets()
|
120
freqtrade/exchange/bittrex.py
Normal file
120
freqtrade/exchange/bittrex.py
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import logging
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
import requests
|
||||||
|
from bittrex.bittrex import Bittrex as _Bittrex
|
||||||
|
|
||||||
|
from freqtrade.exchange.interface import Exchange
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_API: _Bittrex = None
|
||||||
|
_EXCHANGE_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex(Exchange):
|
||||||
|
"""
|
||||||
|
Bittrex API wrapper.
|
||||||
|
"""
|
||||||
|
# Base URL and API endpoints
|
||||||
|
BASE_URL: str = 'https://www.bittrex.com'
|
||||||
|
TICKER_METHOD: str = BASE_URL + '/Api/v2.0/pub/market/GetTicks'
|
||||||
|
PAIR_DETAIL_METHOD: str = BASE_URL + '/Market/Index'
|
||||||
|
# Ticker inveral
|
||||||
|
TICKER_INTERVAL: str = 'fiveMin'
|
||||||
|
# Sleep time to avoid rate limits, used in the main loop
|
||||||
|
SLEEP_TIME: float = 25
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sleep_time(self) -> float:
|
||||||
|
return self.SLEEP_TIME
|
||||||
|
|
||||||
|
def __init__(self, config: dict) -> None:
|
||||||
|
global _API, _EXCHANGE_CONF
|
||||||
|
|
||||||
|
_EXCHANGE_CONF.update(config)
|
||||||
|
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
|
||||||
|
|
||||||
|
# Check if all pairs are available
|
||||||
|
markets = self.get_markets()
|
||||||
|
exchange_name = self.name
|
||||||
|
for pair in _EXCHANGE_CONF['pair_whitelist']:
|
||||||
|
if pair not in markets:
|
||||||
|
raise RuntimeError('Pair {} is not available at {}'.format(pair, exchange_name))
|
||||||
|
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
data = _API.sell_limit(pair.replace('_', '-'), amount, rate)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data['result']['uuid']
|
||||||
|
|
||||||
|
def get_balance(self, currency: str) -> float:
|
||||||
|
data = _API.get_balance(currency)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return float(data['result']['Balance'] or 0.0)
|
||||||
|
|
||||||
|
def get_ticker(self, pair: str) -> dict:
|
||||||
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return {
|
||||||
|
'bid': float(data['result']['Bid']),
|
||||||
|
'ask': float(data['result']['Ask']),
|
||||||
|
'last': float(data['result']['Last']),
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None):
|
||||||
|
url = self.TICKER_METHOD
|
||||||
|
headers = {
|
||||||
|
# TODO: Set as global setting
|
||||||
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3163.100 Safari/537.36'
|
||||||
|
}
|
||||||
|
params = {
|
||||||
|
'marketName': pair.replace('_', '-'),
|
||||||
|
'tickInterval': self.TICKER_INTERVAL,
|
||||||
|
# TODO: Timestamp has no effect on API response
|
||||||
|
'_': minimum_date.timestamp * 1000
|
||||||
|
}
|
||||||
|
data = requests.get(url, params=params, headers=headers).json()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data
|
||||||
|
|
||||||
|
def cancel_order(self, order_id: str) -> None:
|
||||||
|
data = _API.cancel(order_id)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
|
||||||
|
def get_open_orders(self, pair: str) -> List[dict]:
|
||||||
|
data = _API.get_open_orders(pair.replace('_', '-'))
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return [{
|
||||||
|
'id': entry['OrderUuid'],
|
||||||
|
'type': entry['OrderType'],
|
||||||
|
'opened': entry['Opened'],
|
||||||
|
'rate': entry['PricePerUnit'],
|
||||||
|
'amount': entry['Quantity'],
|
||||||
|
'remaining': entry['QuantityRemaining'],
|
||||||
|
} for entry in data['result']]
|
||||||
|
|
||||||
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
|
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
def get_markets(self) -> List[str]:
|
||||||
|
data = _API.get_markets()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return [m['MarketName'].replace('-', '_') for m in data['result']]
|
127
freqtrade/exchange/interface.py
Normal file
127
freqtrade/exchange/interface.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
|
||||||
|
|
||||||
|
class Exchange(ABC):
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""
|
||||||
|
Name of the exchange.
|
||||||
|
:return: str representation of the class name
|
||||||
|
"""
|
||||||
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
@abstractmethod
|
||||||
|
def sleep_time(self) -> float:
|
||||||
|
"""
|
||||||
|
Sleep time in seconds for the main loop to avoid API rate limits.
|
||||||
|
:return: float
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
"""
|
||||||
|
Places a limit buy order.
|
||||||
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to purchase
|
||||||
|
:return: order_id of the placed buy order
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def sell(self, pair: str, rate: float, amount: float) -> str:
|
||||||
|
"""
|
||||||
|
Places a limit sell order.
|
||||||
|
:param pair: Pair as str, format: BTC_ETH
|
||||||
|
:param rate: Rate limit for order
|
||||||
|
:param amount: The amount to sell
|
||||||
|
:return: order_id of the placed sell order
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_balance(self, currency: str) -> float:
|
||||||
|
"""
|
||||||
|
Gets account balance.
|
||||||
|
:param currency: Currency as str, format: BTC
|
||||||
|
:return: float
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_ticker(self, pair: str) -> dict:
|
||||||
|
"""
|
||||||
|
Gets ticker for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: dict, format: {
|
||||||
|
'bid': float,
|
||||||
|
'ask': float,
|
||||||
|
'last': float
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_ticker_history(self, pair: str, minimum_date: Optional[arrow.Arrow] = None) -> dict:
|
||||||
|
"""
|
||||||
|
Gets ticker history for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:param minimum_date: Minimum date (optional)
|
||||||
|
:return: dict, format: {
|
||||||
|
'success': bool,
|
||||||
|
'message': str,
|
||||||
|
'result': [
|
||||||
|
{
|
||||||
|
'O': float, (Open)
|
||||||
|
'H': float, (High)
|
||||||
|
'L': float, (Low)
|
||||||
|
'C': float, (Close)
|
||||||
|
'V': float, (Volume)
|
||||||
|
'T': datetime, (Time)
|
||||||
|
'BV': float, (Base Volume)
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def cancel_order(self, order_id: str) -> None:
|
||||||
|
"""
|
||||||
|
Cancels order for given order_id.
|
||||||
|
:param order_id: ID as str
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_open_orders(self, pair: str) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Gets all open orders for given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: List of dicts, format: [
|
||||||
|
{
|
||||||
|
'id': str,
|
||||||
|
'type': str,
|
||||||
|
'opened': datetime,
|
||||||
|
'rate': float,
|
||||||
|
'amount': float,
|
||||||
|
'remaining': int,
|
||||||
|
},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
|
"""
|
||||||
|
Returns the market detail url for the given pair.
|
||||||
|
:param pair: Pair as str, format: BTC_ETC
|
||||||
|
:return: URL as str
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_markets(self) -> List[str]:
|
||||||
|
"""
|
||||||
|
Returns all available markets.
|
||||||
|
:return: List of all available pairs
|
||||||
|
"""
|
@ -1,4 +1,5 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
import copy
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
@ -6,12 +7,11 @@ import traceback
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
|
|
||||||
import copy
|
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
from freqtrade import exchange, persistence, __version__
|
from freqtrade import __version__, exchange, persistence
|
||||||
from freqtrade.analyze import get_buy_signal
|
from freqtrade.analyze import get_buy_signal
|
||||||
from freqtrade.misc import State, update_state, get_state, CONF_SCHEMA
|
from freqtrade.misc import CONF_SCHEMA, State, get_state, update_state
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc import telegram
|
from freqtrade.rpc import telegram
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ def _process() -> None:
|
|||||||
if len(trades) < _CONF['max_open_trades']:
|
if len(trades) < _CONF['max_open_trades']:
|
||||||
try:
|
try:
|
||||||
# Create entity and execute trade
|
# Create entity and execute trade
|
||||||
trade = create_trade(float(_CONF['stake_amount']), exchange.EXCHANGE)
|
trade = create_trade(float(_CONF['stake_amount']))
|
||||||
if trade:
|
if trade:
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
else:
|
else:
|
||||||
@ -91,7 +91,7 @@ def execute_sell(trade: Trade, current_rate: float) -> None:
|
|||||||
balance = exchange.get_balance(currency)
|
balance = exchange.get_balance(currency)
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
profit = trade.exec_sell_order(current_rate, balance)
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||||
trade.exchange.name,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
trade.close_rate,
|
trade.close_rate,
|
||||||
@ -142,6 +142,7 @@ def handle_trade(trade: Trade) -> None:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
logger.exception('Unable to handle open order')
|
logger.exception('Unable to handle open order')
|
||||||
|
|
||||||
|
|
||||||
def get_target_bid(ticker: Dict[str, float]) -> float:
|
def get_target_bid(ticker: Dict[str, float]) -> float:
|
||||||
""" Calculates bid target between current ask price and last price """
|
""" Calculates bid target between current ask price and last price """
|
||||||
if ticker['ask'] < ticker['last']:
|
if ticker['ask'] < ticker['last']:
|
||||||
@ -150,15 +151,14 @@ def get_target_bid(ticker: Dict[str, float]) -> float:
|
|||||||
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
return ticker['ask'] + balance * (ticker['last'] - ticker['ask'])
|
||||||
|
|
||||||
|
|
||||||
def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[Trade]:
|
def create_trade(stake_amount: float) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Checks the implemented trading indicator(s) for a randomly picked pair,
|
Checks the implemented trading indicator(s) for a randomly picked pair,
|
||||||
if one pair triggers the buy_signal a new trade record gets created
|
if one pair triggers the buy_signal a new trade record gets created
|
||||||
:param stake_amount: amount of btc to spend
|
:param stake_amount: amount of btc to spend
|
||||||
:param _exchange: exchange to use
|
|
||||||
"""
|
"""
|
||||||
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
logger.info('Creating new trade with stake_amount: %f ...', stake_amount)
|
||||||
whitelist = copy.deepcopy(_CONF[_exchange.name.lower()]['pair_whitelist'])
|
whitelist = copy.deepcopy(_CONF['exchange']['pair_whitelist'])
|
||||||
# Check if stake_amount is fulfilled
|
# Check if stake_amount is fulfilled
|
||||||
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
if exchange.get_balance(_CONF['stake_currency']) < stake_amount:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
@ -187,7 +187,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
|
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
||||||
_exchange.name,
|
exchange.EXCHANGE.name.upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
open_rate
|
open_rate
|
||||||
@ -199,7 +199,7 @@ def create_trade(stake_amount: float, _exchange: exchange.Exchange) -> Optional[
|
|||||||
open_rate=open_rate,
|
open_rate=open_rate,
|
||||||
open_date=datetime.utcnow(),
|
open_date=datetime.utcnow(),
|
||||||
amount=amount,
|
amount=amount,
|
||||||
exchange=_exchange,
|
exchange=exchange.EXCHANGE.name.upper(),
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
is_open=True)
|
is_open=True)
|
||||||
|
|
||||||
@ -248,7 +248,7 @@ def app(config: dict) -> None:
|
|||||||
elif new_state == State.RUNNING:
|
elif new_state == State.RUNNING:
|
||||||
_process()
|
_process()
|
||||||
# We need to sleep here because otherwise we would run into bittrex rate limit
|
# We need to sleep here because otherwise we would run into bittrex rate limit
|
||||||
time.sleep(25)
|
time.sleep(exchange.EXCHANGE.sleep_time)
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
telegram.send_msg('*Status:* Got RuntimeError: ```\n{}\n```'.format(traceback.format_exc()))
|
||||||
|
@ -60,7 +60,7 @@ CONF_SCHEMA = {
|
|||||||
},
|
},
|
||||||
'required': ['ask_last_balance']
|
'required': ['ask_last_balance']
|
||||||
},
|
},
|
||||||
'bittrex': {'$ref': '#/definitions/exchange'},
|
'exchange': {'$ref': '#/definitions/exchange'},
|
||||||
'telegram': {
|
'telegram': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
@ -76,7 +76,7 @@ CONF_SCHEMA = {
|
|||||||
'exchange': {
|
'exchange': {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
'enabled': {'type': 'boolean'},
|
'name': {'type': 'string'},
|
||||||
'key': {'type': 'string'},
|
'key': {'type': 'string'},
|
||||||
'secret': {'type': 'string'},
|
'secret': {'type': 'string'},
|
||||||
'pair_whitelist': {
|
'pair_whitelist': {
|
||||||
@ -85,11 +85,11 @@ CONF_SCHEMA = {
|
|||||||
'uniqueItems': True
|
'uniqueItems': True
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'required': ['enabled', 'key', 'secret', 'pair_whitelist']
|
'required': ['name', 'key', 'secret', 'pair_whitelist']
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
'anyOf': [
|
'anyOf': [
|
||||||
{'required': ['bittrex']}
|
{'required': ['exchange']}
|
||||||
],
|
],
|
||||||
'required': [
|
'required': [
|
||||||
'max_open_trades',
|
'max_open_trades',
|
||||||
|
@ -41,7 +41,7 @@ class Trade(Base):
|
|||||||
__tablename__ = 'trades'
|
__tablename__ = 'trades'
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True)
|
id = Column(Integer, primary_key=True)
|
||||||
exchange = Column(Enum(exchange.Exchange), nullable=False)
|
exchange = Column(String, nullable=False)
|
||||||
pair = Column(String, nullable=False)
|
pair = Column(String, nullable=False)
|
||||||
is_open = Column(Boolean, nullable=False, default=True)
|
is_open = Column(Boolean, nullable=False, default=True)
|
||||||
open_rate = Column(Float, nullable=False)
|
open_rate = Column(Float, nullable=False)
|
||||||
|
@ -257,7 +257,7 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
|||||||
# Execute sell
|
# Execute sell
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
profit = trade.exec_sell_order(current_rate, balance)
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
||||||
trade.exchange.name,
|
trade.exchange,
|
||||||
trade.pair.replace('_', '/'),
|
trade.pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
exchange.get_pair_detail_url(trade.pair),
|
||||||
trade.close_rate,
|
trade.close_rate,
|
||||||
|
@ -47,7 +47,7 @@ def test_backtest(conf, pairs, mocker):
|
|||||||
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||||
data = json.load(data_file)
|
data = json.load(data_file)
|
||||||
|
|
||||||
mocker.patch('freqtrade.analyze.get_ticker', return_value=data)
|
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||||
ticker = analyze_ticker(pair)
|
ticker = analyze_ticker(pair)
|
||||||
# for each buy point
|
# for each buy point
|
||||||
|
@ -5,8 +5,7 @@ from unittest.mock import MagicMock, call
|
|||||||
import pytest
|
import pytest
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.exchange import validate_pairs
|
|
||||||
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
from freqtrade.main import create_trade, handle_trade, close_trade_if_fulfilled, init, \
|
||||||
get_target_bid
|
get_target_bid
|
||||||
from freqtrade.misc import CONF_SCHEMA
|
from freqtrade.misc import CONF_SCHEMA
|
||||||
@ -28,7 +27,8 @@ def conf():
|
|||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"bittrex": {
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
@ -61,22 +61,22 @@ def test_create_trade(conf, mocker):
|
|||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value='mocked_order_id'))
|
buy=MagicMock(return_value='mocked_order_id'))
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
whitelist = copy.deepcopy(conf['bittrex']['pair_whitelist'])
|
whitelist = copy.deepcopy(conf['exchange']['pair_whitelist'])
|
||||||
|
|
||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
for pair in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
trade = create_trade(15.0)
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
assert trade is not None
|
assert trade is not None
|
||||||
assert trade.open_rate == 0.072661
|
assert trade.open_rate == 0.072661
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.exchange == exchange.Exchange.BITTREX
|
assert trade.exchange == Exchanges.BITTREX.name
|
||||||
assert trade.amount == 206.43811673387373
|
assert trade.amount == 206.43811673387373
|
||||||
assert trade.stake_amount == 15.0
|
assert trade.stake_amount == 15.0
|
||||||
assert trade.is_open
|
assert trade.is_open
|
||||||
assert trade.open_date is not None
|
assert trade.open_date is not None
|
||||||
assert whitelist == conf['bittrex']['pair_whitelist']
|
assert whitelist == conf['exchange']['pair_whitelist']
|
||||||
|
|
||||||
buy_signal.assert_has_calls(
|
buy_signal.assert_has_calls(
|
||||||
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
[call('BTC_ETH'), call('BTC_TKN'), call('BTC_TRST'), call('BTC_SWT')]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
from freqtrade.exchange import Exchange
|
from freqtrade.exchange import Exchanges
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
def test_exec_sell_order(mocker):
|
def test_exec_sell_order(mocker):
|
||||||
@ -9,7 +9,7 @@ def test_exec_sell_order(mocker):
|
|||||||
stake_amount=1.00,
|
stake_amount=1.00,
|
||||||
open_rate=0.50,
|
open_rate=0.50,
|
||||||
amount=10.00,
|
amount=10.00,
|
||||||
exchange=Exchange.BITTREX,
|
exchange=Exchanges.BITTREX,
|
||||||
open_order_id='mocked'
|
open_order_id='mocked'
|
||||||
)
|
)
|
||||||
profit = trade.exec_sell_order(1.00, 10.00)
|
profit = trade.exec_sell_order(1.00, 10.00)
|
||||||
|
@ -6,7 +6,6 @@ import pytest
|
|||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from telegram import Bot, Update, Message, Chat
|
from telegram import Bot, Update, Message, Chat
|
||||||
|
|
||||||
from freqtrade import exchange
|
|
||||||
from freqtrade.main import init, create_trade
|
from freqtrade.main import init, create_trade
|
||||||
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
@ -28,7 +27,8 @@ def conf():
|
|||||||
"bid_strategy": {
|
"bid_strategy": {
|
||||||
"ask_last_balance": 0.0
|
"ask_last_balance": 0.0
|
||||||
},
|
},
|
||||||
"bittrex": {
|
"exchange": {
|
||||||
|
"name": "bittrex",
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"key": "key",
|
"key": "key",
|
||||||
"secret": "secret",
|
"secret": "secret",
|
||||||
@ -73,7 +73,7 @@ def test_status_handle(conf, update, mocker):
|
|||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
@ -98,7 +98,7 @@ def test_profit_handle(conf, update, mocker):
|
|||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
trade.close_rate = 0.07256061
|
trade.close_rate = 0.07256061
|
||||||
trade.close_profit = 100.00
|
trade.close_profit = 100.00
|
||||||
@ -128,7 +128,7 @@ def test_forcesell_handle(conf, update, mocker):
|
|||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
@ -156,7 +156,7 @@ def test_performance_handle(conf, update, mocker):
|
|||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0, exchange.Exchange.BITTREX)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
trade.close_rate = 0.07256061
|
trade.close_rate = 0.07256061
|
||||||
trade.close_profit = 100.00
|
trade.close_profit = 100.00
|
||||||
|
Loading…
Reference in New Issue
Block a user