Merge branch 'develop' into feature/advanced-status-command
This commit is contained in:
commit
3884cfb809
@ -1,2 +1,4 @@
|
|||||||
[run]
|
[run]
|
||||||
omit = freqtrade/tests/*
|
omit =
|
||||||
|
freqtrade/tests/*
|
||||||
|
freqtrade/vendor/*
|
@ -1,2 +1,3 @@
|
|||||||
[BASIC]
|
[BASIC]
|
||||||
good-names=logger
|
good-names=logger
|
||||||
|
ignore=vendor
|
@ -4,6 +4,9 @@ os:
|
|||||||
language: python
|
language: python
|
||||||
python:
|
python:
|
||||||
- 3.6
|
- 3.6
|
||||||
|
env:
|
||||||
|
- BACKTEST=
|
||||||
|
- BACKTEST=true
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
packages:
|
packages:
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
include LICENSE
|
include LICENSE
|
||||||
include README.md
|
include README.md
|
||||||
include config.json.example
|
include config.json.example
|
||||||
include freqtrade/exchange/*.py
|
recursive-include freqtrade *.py
|
||||||
include freqtrade/rpc/*.py
|
|
||||||
include freqtrade/tests/*.py
|
|
||||||
include freqtrade/tests/testdata/*.json
|
include freqtrade/tests/testdata/*.json
|
||||||
|
40
README.md
40
README.md
@ -16,7 +16,7 @@ and enter the telegram `token` and your `chat_id` in `config.json`
|
|||||||
|
|
||||||
Persistence is achieved through sqlite.
|
Persistence is achieved through sqlite.
|
||||||
|
|
||||||
#### Telegram RPC commands:
|
### Telegram RPC commands:
|
||||||
* /start: Starts the trader
|
* /start: Starts the trader
|
||||||
* /stop: Stops the trader
|
* /stop: Stops the trader
|
||||||
* /status [table]: Lists all open trades
|
* /status [table]: Lists all open trades
|
||||||
@ -25,7 +25,7 @@ Persistence is achieved through sqlite.
|
|||||||
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
|
* /forcesell <trade_id>: Instantly sells the given trade (Ignoring `minimum_roi`).
|
||||||
* /performance: Show performance of each finished trade grouped by pair
|
* /performance: Show performance of each finished trade grouped by pair
|
||||||
|
|
||||||
#### Config
|
### Config
|
||||||
`minimal_roi` is a JSON object where the key is a duration
|
`minimal_roi` is a JSON object where the key is a duration
|
||||||
in minutes and the value is the minimum ROI in percent.
|
in minutes and the value is the minimum ROI in percent.
|
||||||
See the example below:
|
See the example below:
|
||||||
@ -54,12 +54,18 @@ end up paying more then would probably have been necessary.
|
|||||||
The other values should be self-explanatory,
|
The other values should be self-explanatory,
|
||||||
if not feel free to raise a github issue.
|
if not feel free to raise a github issue.
|
||||||
|
|
||||||
#### Prerequisites
|
### Prerequisites
|
||||||
* python3.6
|
* python3.6
|
||||||
* sqlite
|
* sqlite
|
||||||
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
* [TA-lib](https://github.com/mrjbq7/ta-lib#dependencies) binaries
|
||||||
|
|
||||||
#### Install
|
### Install
|
||||||
|
|
||||||
|
#### Arch Linux
|
||||||
|
|
||||||
|
Use your favorite AUR helper and install `python-freqtrade-git`.
|
||||||
|
|
||||||
|
#### Manually
|
||||||
|
|
||||||
`master` branch contains the latest stable release.
|
`master` branch contains the latest stable release.
|
||||||
|
|
||||||
@ -76,18 +82,9 @@ $ pip install -e .
|
|||||||
$ ./freqtrade/main.py
|
$ ./freqtrade/main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).
|
There is also an [article](https://www.sales4k.com/blockchain/high-frequency-trading-bot-tutorial/) about how to setup the bot (thanks [@gurghet](https://github.com/gurghet)).*
|
||||||
|
|
||||||
#### Execute tests
|
\* *Note:* that article was written for an earlier version, so it may be outdated
|
||||||
|
|
||||||
```
|
|
||||||
$ pytest
|
|
||||||
```
|
|
||||||
This will by default skip the slow running backtest set. To run backtest set:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Docker
|
#### Docker
|
||||||
|
|
||||||
@ -137,7 +134,18 @@ $ docker start freqtrade
|
|||||||
You do not need to rebuild the image for configuration
|
You do not need to rebuild the image for configuration
|
||||||
changes, it will suffice to edit `config.json` and restart the container.
|
changes, it will suffice to edit `config.json` and restart the container.
|
||||||
|
|
||||||
#### Contributing
|
### Execute tests
|
||||||
|
|
||||||
|
```
|
||||||
|
$ pytest
|
||||||
|
```
|
||||||
|
This will by default skip the slow running backtest set. To run backtest set:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ BACKTEST=true pytest -s freqtrade/tests/test_backtesting.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
Feel like our bot is missing a feature? We welcome your pull requests! Few pointers for contributions:
|
||||||
|
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
__version__ = '0.12.0'
|
__version__ = '0.13.0'
|
||||||
|
|
||||||
from . import main
|
from . import main
|
||||||
|
@ -5,10 +5,10 @@ from datetime import timedelta
|
|||||||
import arrow
|
import arrow
|
||||||
import talib.abstract as ta
|
import talib.abstract as ta
|
||||||
from pandas import DataFrame, to_datetime
|
from pandas import DataFrame, to_datetime
|
||||||
from qtpylib.indicators import awesome_oscillator, crossed_above
|
|
||||||
|
|
||||||
from freqtrade import exchange
|
from freqtrade import exchange
|
||||||
from freqtrade.exchange import Bittrex, get_ticker_history
|
from freqtrade.exchange import Bittrex, get_ticker_history
|
||||||
|
from freqtrade.vendor.qtpylib.indicators import awesome_oscillator
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG,
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
@ -17,8 +17,8 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
def parse_ticker_dataframe(ticker: list) -> DataFrame:
|
||||||
"""
|
"""
|
||||||
Analyses the trend for the given pair
|
Analyses the trend for the given ticker history
|
||||||
:param pair: pair as str in format BTC_ETH or BTC-ETH
|
:param ticker: See exchange.get_ticker_history
|
||||||
:return: DataFrame
|
:return: DataFrame
|
||||||
"""
|
"""
|
||||||
df = DataFrame(ticker) \
|
df = DataFrame(ticker) \
|
||||||
@ -43,8 +43,17 @@ def populate_indicators(dataframe: DataFrame) -> DataFrame:
|
|||||||
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
|
||||||
dataframe['mfi'] = ta.MFI(dataframe)
|
dataframe['mfi'] = ta.MFI(dataframe)
|
||||||
dataframe['cci'] = ta.CCI(dataframe)
|
dataframe['cci'] = ta.CCI(dataframe)
|
||||||
|
dataframe['rsi'] = ta.RSI(dataframe)
|
||||||
|
dataframe['mom'] = ta.MOM(dataframe)
|
||||||
|
dataframe['ema5'] = ta.EMA(dataframe, timeperiod=5)
|
||||||
|
dataframe['ema10'] = ta.EMA(dataframe, timeperiod=10)
|
||||||
|
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
|
||||||
|
dataframe['ema100'] = ta.EMA(dataframe, timeperiod=100)
|
||||||
dataframe['ao'] = awesome_oscillator(dataframe)
|
dataframe['ao'] = awesome_oscillator(dataframe)
|
||||||
|
macd = ta.MACD(dataframe)
|
||||||
|
dataframe['macd'] = macd['macd']
|
||||||
|
dataframe['macdsignal'] = macd['macdsignal']
|
||||||
|
dataframe['macdhist'] = macd['macdhist']
|
||||||
return dataframe
|
return dataframe
|
||||||
|
|
||||||
|
|
||||||
@ -152,7 +161,7 @@ def plot_dataframe(dataframe: DataFrame, pair: str) -> None:
|
|||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# Install PYQT5==5.9 manually if you want to test this helper function
|
# Install PYQT5==5.9 manually if you want to test this helper function
|
||||||
while True:
|
while True:
|
||||||
exchange.EXCHANGE = Bittrex({'key': '', 'secret': ''})
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
test_pair = 'BTC_ETH'
|
test_pair = 'BTC_ETH'
|
||||||
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
# for pair in ['BTC_ANT', 'BTC_ETH', 'BTC_GNT', 'BTC_ETC']:
|
||||||
# get_buy_signal(pair)
|
# get_buy_signal(pair)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import enum
|
import enum
|
||||||
import logging
|
import logging
|
||||||
from typing import List
|
from typing import List, Dict
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
@ -10,7 +10,7 @@ from freqtrade.exchange.interface import Exchange
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
# Current selected exchange
|
# Current selected exchange
|
||||||
EXCHANGE: Exchange = None
|
_API: Exchange = None
|
||||||
_CONF: dict = {}
|
_CONF: dict = {}
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +29,7 @@ def init(config: dict) -> None:
|
|||||||
:param config: config to use
|
:param config: config to use
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
global _CONF, EXCHANGE
|
global _CONF, _API
|
||||||
|
|
||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ def init(config: dict) -> None:
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
raise RuntimeError('Exchange {} is not supported'.format(name))
|
raise RuntimeError('Exchange {} is not supported'.format(name))
|
||||||
|
|
||||||
EXCHANGE = exchange_class(exchange_config)
|
_API = exchange_class(exchange_config)
|
||||||
|
|
||||||
# Check if all pairs are available
|
# Check if all pairs are available
|
||||||
validate_pairs(config['exchange']['pair_whitelist'])
|
validate_pairs(config['exchange']['pair_whitelist'])
|
||||||
@ -58,58 +58,86 @@ def validate_pairs(pairs: List[str]) -> None:
|
|||||||
:param pairs: list of pairs
|
:param pairs: list of pairs
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
markets = EXCHANGE.get_markets()
|
markets = _API.get_markets()
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
raise RuntimeError('Pair {} is not available at {}'.format(pair, EXCHANGE.name.lower()))
|
raise RuntimeError('Pair {} is not available at {}'.format(pair, _API.name.lower()))
|
||||||
|
|
||||||
|
|
||||||
def buy(pair: str, rate: float, amount: float) -> str:
|
def buy(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 'dry_run'
|
return 'dry_run_buy'
|
||||||
|
|
||||||
return EXCHANGE.buy(pair, rate, amount)
|
return _API.buy(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
def sell(pair: str, rate: float, amount: float) -> str:
|
def sell(pair: str, rate: float, amount: float) -> str:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 'dry_run'
|
return 'dry_run_sell'
|
||||||
|
|
||||||
return EXCHANGE.sell(pair, rate, amount)
|
return _API.sell(pair, rate, amount)
|
||||||
|
|
||||||
|
|
||||||
def get_balance(currency: str) -> float:
|
def get_balance(currency: str) -> float:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return 999.9
|
return 999.9
|
||||||
|
|
||||||
return EXCHANGE.get_balance(currency)
|
return _API.get_balance(currency)
|
||||||
|
|
||||||
|
|
||||||
|
def get_balances():
|
||||||
|
if _CONF['dry_run']:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return _API.get_balances()
|
||||||
|
|
||||||
|
|
||||||
def get_ticker(pair: str) -> dict:
|
def get_ticker(pair: str) -> dict:
|
||||||
return EXCHANGE.get_ticker(pair)
|
return _API.get_ticker(pair)
|
||||||
|
|
||||||
|
|
||||||
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
|
def get_ticker_history(pair: str, minimum_date: arrow.Arrow):
|
||||||
return EXCHANGE.get_ticker_history(pair, minimum_date)
|
return _API.get_ticker_history(pair, minimum_date)
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_id: str) -> None:
|
def cancel_order(order_id: str) -> None:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return
|
return
|
||||||
|
|
||||||
return EXCHANGE.cancel_order(order_id)
|
return _API.cancel_order(order_id)
|
||||||
|
|
||||||
|
|
||||||
def get_open_orders(pair: str) -> List[dict]:
|
def get_order(order_id: str) -> Dict:
|
||||||
if _CONF['dry_run']:
|
if _CONF['dry_run']:
|
||||||
return []
|
return {
|
||||||
|
'id': 'dry_run_sell',
|
||||||
|
'type': 'LIMIT_SELL',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': arrow.utcnow().datetime,
|
||||||
|
'rate': 0.07256060,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': arrow.utcnow().datetime,
|
||||||
|
}
|
||||||
|
|
||||||
return EXCHANGE.get_open_orders(pair)
|
return _API.get_order(order_id)
|
||||||
|
|
||||||
|
|
||||||
def get_pair_detail_url(pair: str) -> str:
|
def get_pair_detail_url(pair: str) -> str:
|
||||||
return EXCHANGE.get_pair_detail_url(pair)
|
return _API.get_pair_detail_url(pair)
|
||||||
|
|
||||||
|
|
||||||
def get_markets() -> List[str]:
|
def get_markets() -> List[str]:
|
||||||
return EXCHANGE.get_markets()
|
return _API.get_markets()
|
||||||
|
|
||||||
|
|
||||||
|
def get_name() -> str:
|
||||||
|
return _API.name
|
||||||
|
|
||||||
|
|
||||||
|
def get_sleep_time() -> float:
|
||||||
|
return _API.sleep_time
|
||||||
|
|
||||||
|
|
||||||
|
def get_fee() -> float:
|
||||||
|
return _API.fee
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
import requests
|
||||||
@ -36,6 +36,11 @@ class Bittrex(Exchange):
|
|||||||
_EXCHANGE_CONF.update(config)
|
_EXCHANGE_CONF.update(config)
|
||||||
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
|
_API = _Bittrex(api_key=_EXCHANGE_CONF['key'], api_secret=_EXCHANGE_CONF['secret'])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fee(self) -> float:
|
||||||
|
# See https://bittrex.com/fees
|
||||||
|
return 0.0025
|
||||||
|
|
||||||
def buy(self, pair: str, rate: float, amount: float) -> str:
|
def buy(self, pair: str, rate: float, amount: float) -> str:
|
||||||
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
data = _API.buy_limit(pair.replace('_', '-'), amount, rate)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
@ -54,6 +59,12 @@ class Bittrex(Exchange):
|
|||||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
return float(data['result']['Balance'] or 0.0)
|
return float(data['result']['Balance'] or 0.0)
|
||||||
|
|
||||||
|
def get_balances(self):
|
||||||
|
data = _API.get_balances()
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
return data['result']
|
||||||
|
|
||||||
def get_ticker(self, pair: str) -> dict:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
data = _API.get_ticker(pair.replace('_', '-'))
|
data = _API.get_ticker(pair.replace('_', '-'))
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
@ -81,24 +92,27 @@ class Bittrex(Exchange):
|
|||||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def get_order(self, order_id: str) -> Dict:
|
||||||
|
data = _API.get_order(order_id)
|
||||||
|
if not data['success']:
|
||||||
|
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
||||||
|
data = data['result']
|
||||||
|
return {
|
||||||
|
'id': data['OrderUuid'],
|
||||||
|
'type': data['Type'],
|
||||||
|
'pair': data['Exchange'].replace('-', '_'),
|
||||||
|
'opened': data['Opened'],
|
||||||
|
'rate': data['PricePerUnit'],
|
||||||
|
'amount': data['Quantity'],
|
||||||
|
'remaining': data['QuantityRemaining'],
|
||||||
|
'closed': data['Closed'],
|
||||||
|
}
|
||||||
|
|
||||||
def cancel_order(self, order_id: str) -> None:
|
def cancel_order(self, order_id: str) -> None:
|
||||||
data = _API.cancel(order_id)
|
data = _API.cancel(order_id)
|
||||||
if not data['success']:
|
if not data['success']:
|
||||||
raise RuntimeError('{}: {}'.format(self.name.upper(), data['message']))
|
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:
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
return self.PAIR_DETAIL_METHOD + '?MarketName={}'.format(pair.replace('_', '-'))
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Dict
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
|
|
||||||
@ -13,6 +13,14 @@ class Exchange(ABC):
|
|||||||
"""
|
"""
|
||||||
return self.__class__.__name__
|
return self.__class__.__name__
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fee(self) -> float:
|
||||||
|
"""
|
||||||
|
Fee for placing an order
|
||||||
|
:return: percentage in float
|
||||||
|
"""
|
||||||
|
return 0.0
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def sleep_time(self) -> float:
|
def sleep_time(self) -> float:
|
||||||
@ -49,6 +57,21 @@ class Exchange(ABC):
|
|||||||
:return: float
|
:return: float
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_balances(self) -> List[dict]:
|
||||||
|
"""
|
||||||
|
Gets account balances across currencies
|
||||||
|
:return: List of dicts, format: [
|
||||||
|
{
|
||||||
|
'Currency': str,
|
||||||
|
'Balance': float,
|
||||||
|
'Available': float,
|
||||||
|
'Pending': float,
|
||||||
|
}
|
||||||
|
...
|
||||||
|
]
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_ticker(self, pair: str) -> dict:
|
def get_ticker(self, pair: str) -> dict:
|
||||||
"""
|
"""
|
||||||
@ -85,6 +108,22 @@ class Exchange(ABC):
|
|||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def get_order(self, order_id: str) -> Dict:
|
||||||
|
"""
|
||||||
|
Get order details for the given order_id.
|
||||||
|
:param order_id: ID as str
|
||||||
|
:return: dict, format: {
|
||||||
|
'id': str,
|
||||||
|
'type': str,
|
||||||
|
'pair': str,
|
||||||
|
'opened': str ISO 8601 datetime,
|
||||||
|
'closed': str ISO 8601 datetime,
|
||||||
|
'rate': float,
|
||||||
|
'amount': float,
|
||||||
|
'remaining': int
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancel_order(self, order_id: str) -> None:
|
def cancel_order(self, order_id: str) -> None:
|
||||||
"""
|
"""
|
||||||
@ -93,24 +132,6 @@ class Exchange(ABC):
|
|||||||
:return: None
|
: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
|
@abstractmethod
|
||||||
def get_pair_detail_url(self, pair: str) -> str:
|
def get_pair_detail_url(self, pair: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
@ -8,6 +8,7 @@ from datetime import datetime
|
|||||||
from typing import Dict, Optional
|
from typing import Dict, Optional
|
||||||
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
from signal import signal, SIGINT, SIGABRT, SIGTERM
|
||||||
|
|
||||||
|
import requests
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
|
|
||||||
from freqtrade import __version__, exchange, persistence
|
from freqtrade import __version__, exchange, persistence
|
||||||
@ -44,22 +45,21 @@ def _process() -> None:
|
|||||||
logger.exception('Unable to create trade')
|
logger.exception('Unable to create trade')
|
||||||
|
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
# Check if there is already an open order for this trade
|
# Get order details for actual price per unit
|
||||||
orders = exchange.get_open_orders(trade.pair)
|
if trade.open_order_id:
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
# Update trade with order values
|
||||||
if orders:
|
logger.info('Got open order for %s', trade)
|
||||||
logger.info('There is an open order for: %s', orders[0])
|
trade.update(exchange.get_order(trade.open_order_id))
|
||||||
else:
|
|
||||||
# Update state
|
if not close_trade_if_fulfilled(trade):
|
||||||
trade.open_order_id = None
|
# Check if we can sell our current pair
|
||||||
# Check if this trade can be closed
|
handle_trade(trade)
|
||||||
if not close_trade_if_fulfilled(trade):
|
|
||||||
# Check if we can sell our current pair
|
Trade.session.flush()
|
||||||
handle_trade(trade)
|
except (requests.exceptions.ConnectionError, json.JSONDecodeError) as error:
|
||||||
Trade.session.flush()
|
msg = 'Got {} in _process(), retrying in 30 seconds...'.format(error.__class__.__name__)
|
||||||
except (ConnectionError, json.JSONDecodeError) as error:
|
|
||||||
msg = 'Got {} in _process()'.format(error.__class__.__name__)
|
|
||||||
logger.exception(msg)
|
logger.exception(msg)
|
||||||
|
time.sleep(30)
|
||||||
|
|
||||||
|
|
||||||
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
def close_trade_if_fulfilled(trade: Trade) -> bool:
|
||||||
@ -80,23 +80,25 @@ def close_trade_if_fulfilled(trade: Trade) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def execute_sell(trade: Trade, current_rate: float) -> None:
|
def execute_sell(trade: Trade, limit: float) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a sell for the given trade and current rate
|
Executes a limit sell for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
:param current_rate: current rate
|
:param limit: limit rate for the sell order
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
# Get available balance
|
# Execute sell and update trade record
|
||||||
currency = trade.pair.split('_')[1]
|
order_id = exchange.sell(str(trade.pair), limit, trade.amount)
|
||||||
balance = exchange.get_balance(currency)
|
trade.open_order_id = order_id
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
trade.close_date = datetime.utcnow()
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
|
||||||
|
fmt_exp_profit = round(trade.calc_profit(limit) * 100, 2)
|
||||||
|
message = '*{}:* Selling [{}]({}) with limit `{:f} (profit: ~{}%)`'.format(
|
||||||
trade.exchange,
|
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,
|
limit,
|
||||||
round(profit, 2)
|
fmt_exp_profit
|
||||||
)
|
)
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
telegram.send_msg(message)
|
telegram.send_msg(message)
|
||||||
@ -107,17 +109,15 @@ def should_sell(trade: Trade, current_rate: float, current_time: datetime) -> bo
|
|||||||
Based an earlier trade and current price and configuration, decides whether bot should sell
|
Based an earlier trade and current price and configuration, decides whether bot should sell
|
||||||
:return True if bot should sell at current rate
|
:return True if bot should sell at current rate
|
||||||
"""
|
"""
|
||||||
current_profit = (current_rate - trade.open_rate) / trade.open_rate
|
current_profit = trade.calc_profit(current_rate)
|
||||||
|
|
||||||
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
if 'stoploss' in _CONF and current_profit < float(_CONF['stoploss']):
|
||||||
logger.debug('Stop loss hit.')
|
logger.debug('Stop loss hit.')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
for duration, threshold in sorted(_CONF['minimal_roi'].items()):
|
||||||
duration, threshold = float(duration), float(threshold)
|
|
||||||
# Check if time matches and current rate is above threshold
|
# Check if time matches and current rate is above threshold
|
||||||
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
time_diff = (current_time - trade.open_date).total_seconds() / 60
|
||||||
if time_diff > duration and current_profit > threshold:
|
if time_diff > float(duration) and current_profit > threshold:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
|
logger.debug('Threshold not reached. (cur_profit: %1.2f%%)', current_profit * 100.0)
|
||||||
@ -133,7 +133,7 @@ def handle_trade(trade: Trade) -> None:
|
|||||||
if not trade.is_open:
|
if not trade.is_open:
|
||||||
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
raise ValueError('attempt to handle closed trade: {}'.format(trade))
|
||||||
|
|
||||||
logger.debug('Handling open trade %s ...', trade)
|
logger.debug('Handling %s ...', trade)
|
||||||
|
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
if should_sell(trade, current_rate, datetime.utcnow()):
|
if should_sell(trade, current_rate, datetime.utcnow()):
|
||||||
@ -163,7 +163,7 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
# 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(
|
||||||
'stake amount is not fulfilled (currency={}'.format(_CONF['stake_currency'])
|
'stake amount is not fulfilled (currency={})'.format(_CONF['stake_currency'])
|
||||||
)
|
)
|
||||||
|
|
||||||
# Remove currently opened and latest pairs from whitelist
|
# Remove currently opened and latest pairs from whitelist
|
||||||
@ -182,25 +182,29 @@ def create_trade(stake_amount: float) -> Optional[Trade]:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
open_rate = get_target_bid(exchange.get_ticker(pair))
|
# Calculate amount and subtract fee
|
||||||
amount = stake_amount / open_rate
|
fee = exchange.get_fee()
|
||||||
order_id = exchange.buy(pair, open_rate, amount)
|
buy_limit = get_target_bid(exchange.get_ticker(pair))
|
||||||
|
amount = (1 - fee) * stake_amount / buy_limit
|
||||||
|
|
||||||
|
order_id = exchange.buy(pair, buy_limit, amount)
|
||||||
# Create trade entity and return
|
# Create trade entity and return
|
||||||
message = '*{}:* Buying [{}]({}) at rate `{:f}`'.format(
|
message = '*{}:* Buying [{}]({}) with limit `{:f}`'.format(
|
||||||
exchange.EXCHANGE.name.upper(),
|
exchange.get_name().upper(),
|
||||||
pair.replace('_', '/'),
|
pair.replace('_', '/'),
|
||||||
exchange.get_pair_detail_url(pair),
|
exchange.get_pair_detail_url(pair),
|
||||||
open_rate
|
buy_limit
|
||||||
)
|
)
|
||||||
logger.info(message)
|
logger.info(message)
|
||||||
telegram.send_msg(message)
|
telegram.send_msg(message)
|
||||||
|
# Fee is applied twice because we make a LIMIT_BUY and LIMIT_SELL
|
||||||
return Trade(pair=pair,
|
return Trade(pair=pair,
|
||||||
stake_amount=stake_amount,
|
stake_amount=stake_amount,
|
||||||
open_rate=open_rate,
|
|
||||||
open_date=datetime.utcnow(),
|
|
||||||
amount=amount,
|
amount=amount,
|
||||||
exchange=exchange.EXCHANGE.name.upper(),
|
fee=fee * 2,
|
||||||
|
open_rate=buy_limit,
|
||||||
|
open_date=datetime.utcnow(),
|
||||||
|
exchange=exchange.get_name().upper(),
|
||||||
open_order_id=order_id,
|
open_order_id=order_id,
|
||||||
is_open=True)
|
is_open=True)
|
||||||
|
|
||||||
@ -266,7 +270,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(exchange.EXCHANGE.sleep_time)
|
time.sleep(exchange.get_sleep_time())
|
||||||
old_state = new_state
|
old_state = new_state
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
telegram.send_msg(
|
telegram.send_msg(
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from decimal import Decimal, getcontext
|
||||||
|
from typing import Optional, Dict
|
||||||
|
|
||||||
|
import arrow
|
||||||
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
from sqlalchemy import Boolean, Column, DateTime, Float, Integer, String, create_engine
|
||||||
from sqlalchemy.ext.declarative import declarative_base
|
from sqlalchemy.ext.declarative import declarative_base
|
||||||
from sqlalchemy.orm.scoping import scoped_session
|
from sqlalchemy.orm.scoping import scoped_session
|
||||||
from sqlalchemy.orm.session import sessionmaker
|
from sqlalchemy.orm.session import sessionmaker
|
||||||
from sqlalchemy.types import Enum
|
|
||||||
|
|
||||||
from freqtrade import exchange
|
logging.basicConfig(level=logging.DEBUG,
|
||||||
|
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
_CONF = {}
|
_CONF = {}
|
||||||
|
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
|
|
||||||
@ -26,9 +29,9 @@ def init(config: dict, db_url: Optional[str] = None) -> None:
|
|||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
if not db_url:
|
if not db_url:
|
||||||
if _CONF.get('dry_run', False):
|
if _CONF.get('dry_run', False):
|
||||||
db_url = 'sqlite:///tradesv2.dry_run.sqlite'
|
db_url = 'sqlite:///tradesv3.dry_run.sqlite'
|
||||||
else:
|
else:
|
||||||
db_url = 'sqlite:///tradesv2.sqlite'
|
db_url = 'sqlite:///tradesv3.sqlite'
|
||||||
|
|
||||||
engine = create_engine(db_url, echo=False)
|
engine = create_engine(db_url, echo=False)
|
||||||
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
session = scoped_session(sessionmaker(bind=engine, autoflush=True, autocommit=True))
|
||||||
@ -52,44 +55,55 @@ class Trade(Base):
|
|||||||
exchange = Column(String, 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)
|
fee = Column(Float, nullable=False, default=0.0)
|
||||||
|
open_rate = Column(Float)
|
||||||
close_rate = Column(Float)
|
close_rate = Column(Float)
|
||||||
close_profit = Column(Float)
|
close_profit = Column(Float)
|
||||||
stake_amount = Column(Float, name='btc_amount', nullable=False)
|
stake_amount = Column(Float, nullable=False)
|
||||||
amount = Column(Float, nullable=False)
|
amount = Column(Float)
|
||||||
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
open_date = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||||
close_date = Column(DateTime)
|
close_date = Column(DateTime)
|
||||||
open_order_id = Column(String)
|
open_order_id = Column(String)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
if self.is_open:
|
|
||||||
open_since = 'closed'
|
|
||||||
else:
|
|
||||||
open_since = round((datetime.utcnow() - self.open_date).total_seconds() / 60, 2)
|
|
||||||
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
return 'Trade(id={}, pair={}, amount={}, open_rate={}, open_since={})'.format(
|
||||||
self.id,
|
self.id,
|
||||||
self.pair,
|
self.pair,
|
||||||
self.amount,
|
self.amount,
|
||||||
self.open_rate,
|
self.open_rate,
|
||||||
open_since
|
arrow.get(self.open_date).humanize() if self.is_open else 'closed'
|
||||||
)
|
)
|
||||||
|
|
||||||
def exec_sell_order(self, rate: float, amount: float) -> float:
|
def update(self, order: Dict) -> None:
|
||||||
"""
|
"""
|
||||||
Executes a sell for the given trade and updated the entity.
|
Updates this entity with amount and actual open/close rates.
|
||||||
:param rate: rate to sell for
|
:param order: order retrieved by exchange.get_order()
|
||||||
:param amount: amount to sell
|
:return: None
|
||||||
:return: current profit as percentage
|
|
||||||
"""
|
"""
|
||||||
profit = 100 * ((rate - self.open_rate) / self.open_rate)
|
if not order['closed']:
|
||||||
|
return
|
||||||
|
|
||||||
# Execute sell and update trade record
|
logger.debug('Updating trade (id=%d) ...', self.id)
|
||||||
order_id = exchange.sell(str(self.pair), rate, amount)
|
if order['type'] == 'LIMIT_BUY':
|
||||||
self.close_rate = rate
|
# Update open rate and actual amount
|
||||||
self.close_profit = profit
|
self.open_rate = order['rate']
|
||||||
self.close_date = datetime.utcnow()
|
self.amount = order['amount']
|
||||||
self.open_order_id = order_id
|
elif order['type'] == 'LIMIT_SELL':
|
||||||
|
# Set close rate and set actual profit
|
||||||
|
self.close_rate = order['rate']
|
||||||
|
self.close_profit = self.calc_profit()
|
||||||
|
else:
|
||||||
|
raise ValueError('Unknown order type: {}'.format(order['type']))
|
||||||
|
|
||||||
# Flush changes
|
self.open_order_id = None
|
||||||
Trade.session.flush()
|
|
||||||
return profit
|
def calc_profit(self, rate: Optional[float] = None) -> float:
|
||||||
|
"""
|
||||||
|
Calculates the profit in percentage (including fee).
|
||||||
|
:param rate: rate to compare with (optional).
|
||||||
|
If rate is not set self.close_rate will be used
|
||||||
|
:return: profit in percentage as float
|
||||||
|
"""
|
||||||
|
getcontext().prec = 8
|
||||||
|
return float((Decimal(rate or self.close_rate) - Decimal(self.open_rate))
|
||||||
|
/ Decimal(self.open_rate) - Decimal(self.fee))
|
||||||
|
@ -35,7 +35,7 @@ def init(config: dict) -> None:
|
|||||||
global _updater
|
global _updater
|
||||||
|
|
||||||
_CONF.update(config)
|
_CONF.update(config)
|
||||||
if not _CONF['telegram']['enabled']:
|
if not is_enabled():
|
||||||
return
|
return
|
||||||
|
|
||||||
_updater = Updater(token=config['telegram']['token'], workers=0)
|
_updater = Updater(token=config['telegram']['token'], workers=0)
|
||||||
@ -44,6 +44,7 @@ def init(config: dict) -> None:
|
|||||||
handles = [
|
handles = [
|
||||||
CommandHandler('status', _status),
|
CommandHandler('status', _status),
|
||||||
CommandHandler('profit', _profit),
|
CommandHandler('profit', _profit),
|
||||||
|
CommandHandler('balance', _balance),
|
||||||
CommandHandler('start', _start),
|
CommandHandler('start', _start),
|
||||||
CommandHandler('stop', _stop),
|
CommandHandler('stop', _stop),
|
||||||
CommandHandler('forcesell', _forcesell),
|
CommandHandler('forcesell', _forcesell),
|
||||||
@ -70,9 +71,18 @@ def cleanup() -> None:
|
|||||||
Stops all running telegram threads.
|
Stops all running telegram threads.
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
|
if not is_enabled():
|
||||||
|
return
|
||||||
_updater.stop()
|
_updater.stop()
|
||||||
|
|
||||||
|
|
||||||
|
def is_enabled() -> bool:
|
||||||
|
"""
|
||||||
|
Returns True if the telegram module is activated, False otherwise
|
||||||
|
"""
|
||||||
|
return bool(_CONF['telegram'].get('enabled', False))
|
||||||
|
|
||||||
|
|
||||||
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
def authorized_only(command_handler: Callable[[Bot, Update], None]) -> Callable[..., Any]:
|
||||||
"""
|
"""
|
||||||
Decorator to check if the message comes from the correct chat_id
|
Decorator to check if the message comes from the correct chat_id
|
||||||
@ -116,18 +126,15 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
if get_state() != State.RUNNING:
|
if get_state() != State.RUNNING:
|
||||||
send_msg('*Status:* `trader is not running`', bot=bot)
|
send_msg('*Status:* `trader is not running`', bot=bot)
|
||||||
elif not trades:
|
elif not trades:
|
||||||
send_msg('*Status:* `no active order`', bot=bot)
|
send_msg('*Status:* `no active trade`', bot=bot)
|
||||||
else:
|
else:
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
order = exchange.get_order(trade.open_order_id)
|
||||||
# calculate profit and send message to user
|
# calculate profit and send message to user
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
current_profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
current_profit = trade.calc_profit(current_rate)
|
||||||
orders = exchange.get_open_orders(trade.pair)
|
|
||||||
orders = [o for o in orders if o['id'] == trade.open_order_id]
|
|
||||||
order = orders[0] if orders else None
|
|
||||||
|
|
||||||
fmt_close_profit = '{:.2f}%'.format(
|
fmt_close_profit = '{:.2f}%'.format(
|
||||||
round(trade.close_profit, 2)
|
round(trade.close_profit * 100, 2)
|
||||||
) if trade.close_profit else None
|
) if trade.close_profit else None
|
||||||
message = """
|
message = """
|
||||||
*Trade ID:* `{trade_id}`
|
*Trade ID:* `{trade_id}`
|
||||||
@ -150,8 +157,10 @@ def _status(bot: Bot, update: Update) -> None:
|
|||||||
current_rate=current_rate,
|
current_rate=current_rate,
|
||||||
amount=round(trade.amount, 8),
|
amount=round(trade.amount, 8),
|
||||||
close_profit=fmt_close_profit,
|
close_profit=fmt_close_profit,
|
||||||
current_profit=round(current_profit, 2),
|
current_profit=round(current_profit * 100, 2),
|
||||||
open_order='{} ({})'.format(order['remaining'], order['type']) if order else None,
|
open_order='{} ({})'.format(
|
||||||
|
order['remaining'], order['type']
|
||||||
|
) if order else None,
|
||||||
)
|
)
|
||||||
send_msg(message, bot=bot)
|
send_msg(message, bot=bot)
|
||||||
|
|
||||||
@ -214,6 +223,8 @@ def _profit(bot: Bot, update: Update) -> None:
|
|||||||
profits = []
|
profits = []
|
||||||
durations = []
|
durations = []
|
||||||
for trade in trades:
|
for trade in trades:
|
||||||
|
if not trade.open_rate:
|
||||||
|
continue
|
||||||
if trade.close_date:
|
if trade.close_date:
|
||||||
durations.append((trade.close_date - trade.open_date).total_seconds())
|
durations.append((trade.close_date - trade.open_date).total_seconds())
|
||||||
if trade.close_profit:
|
if trade.close_profit:
|
||||||
@ -221,9 +232,9 @@ def _profit(bot: Bot, update: Update) -> None:
|
|||||||
else:
|
else:
|
||||||
# Get current rate
|
# Get current rate
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
profit = 100 * ((current_rate - trade.open_rate) / trade.open_rate)
|
profit = trade.calc_profit(current_rate)
|
||||||
|
|
||||||
profit_amounts.append((profit / 100) * trade.stake_amount)
|
profit_amounts.append(profit * trade.stake_amount)
|
||||||
profits.append(profit)
|
profits.append(profit)
|
||||||
|
|
||||||
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
best_pair = Trade.session.query(Trade.pair, func.sum(Trade.close_profit).label('profit_sum')) \
|
||||||
@ -238,25 +249,49 @@ def _profit(bot: Bot, update: Update) -> None:
|
|||||||
|
|
||||||
bp_pair, bp_rate = best_pair
|
bp_pair, bp_rate = best_pair
|
||||||
markdown_msg = """
|
markdown_msg = """
|
||||||
*ROI:* `{profit_btc:.2f} ({profit:.2f}%)`
|
*ROI:* `{profit_btc:.6f} ({profit:.2f}%)`
|
||||||
*Trade Count:* `{trade_count}`
|
*Trade Count:* `{trade_count}`
|
||||||
*First Trade opened:* `{first_trade_date}`
|
*First Trade opened:* `{first_trade_date}`
|
||||||
*Latest Trade opened:* `{latest_trade_date}`
|
*Latest Trade opened:* `{latest_trade_date}`
|
||||||
*Avg. Duration:* `{avg_duration}`
|
*Avg. Duration:* `{avg_duration}`
|
||||||
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
*Best Performing:* `{best_pair}: {best_rate:.2f}%`
|
||||||
|
{dry_run_info}
|
||||||
""".format(
|
""".format(
|
||||||
profit_btc=round(sum(profit_amounts), 8),
|
profit_btc=round(sum(profit_amounts), 8),
|
||||||
profit=round(sum(profits), 2),
|
profit=round(sum(profits) * 100, 2),
|
||||||
trade_count=len(trades),
|
trade_count=len(trades),
|
||||||
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
first_trade_date=arrow.get(trades[0].open_date).humanize(),
|
||||||
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
latest_trade_date=arrow.get(trades[-1].open_date).humanize(),
|
||||||
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
avg_duration=str(timedelta(seconds=sum(durations) / float(len(durations)))).split('.')[0],
|
||||||
best_pair=bp_pair,
|
best_pair=bp_pair,
|
||||||
best_rate=round(bp_rate, 2),
|
best_rate=round(bp_rate * 100, 2),
|
||||||
|
dry_run_info='\n*NOTE:* These values are mocked because *dry_run* is enabled!'
|
||||||
|
if _CONF['dry_run'] else ''
|
||||||
)
|
)
|
||||||
send_msg(markdown_msg, bot=bot)
|
send_msg(markdown_msg, bot=bot)
|
||||||
|
|
||||||
|
|
||||||
|
@authorized_only
|
||||||
|
def _balance(bot: Bot, update: Update) -> None:
|
||||||
|
"""
|
||||||
|
Handler for /balance
|
||||||
|
Returns current account balance per crypto
|
||||||
|
"""
|
||||||
|
output = ""
|
||||||
|
balances = exchange.get_balances()
|
||||||
|
for currency in balances:
|
||||||
|
if not currency['Balance'] and not currency['Available'] and not currency['Pending']:
|
||||||
|
continue
|
||||||
|
output += """*Currency*: {Currency}
|
||||||
|
*Available*: {Available}
|
||||||
|
*Balance*: {Balance}
|
||||||
|
*Pending*: {Pending}
|
||||||
|
|
||||||
|
""".format(**currency)
|
||||||
|
|
||||||
|
send_msg(output)
|
||||||
|
|
||||||
|
|
||||||
@authorized_only
|
@authorized_only
|
||||||
def _start(bot: Bot, update: Update) -> None:
|
def _start(bot: Bot, update: Update) -> None:
|
||||||
"""
|
"""
|
||||||
@ -315,20 +350,8 @@ def _forcesell(bot: Bot, update: Update) -> None:
|
|||||||
return
|
return
|
||||||
# Get current rate
|
# Get current rate
|
||||||
current_rate = exchange.get_ticker(trade.pair)['bid']
|
current_rate = exchange.get_ticker(trade.pair)['bid']
|
||||||
# Get available balance
|
from freqtrade.main import execute_sell
|
||||||
currency = trade.pair.split('_')[1]
|
execute_sell(trade, current_rate)
|
||||||
balance = exchange.get_balance(currency)
|
|
||||||
# Execute sell
|
|
||||||
profit = trade.exec_sell_order(current_rate, balance)
|
|
||||||
message = '*{}:* Selling [{}]({}) at rate `{:f} (profit: {}%)`'.format(
|
|
||||||
trade.exchange,
|
|
||||||
trade.pair.replace('_', '/'),
|
|
||||||
exchange.get_pair_detail_url(trade.pair),
|
|
||||||
trade.close_rate,
|
|
||||||
round(profit, 2)
|
|
||||||
)
|
|
||||||
logger.info(message)
|
|
||||||
send_msg(message)
|
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
send_msg('Invalid argument. Usage: `/forcesell <trade_id>`')
|
||||||
@ -357,10 +380,14 @@ def _performance(bot: Bot, update: Update) -> None:
|
|||||||
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
|
stats = '\n'.join('{index}. <code>{pair}\t{profit:.2f}%</code>'.format(
|
||||||
index=i + 1,
|
index=i + 1,
|
||||||
pair=pair,
|
pair=pair,
|
||||||
profit=round(rate, 2)
|
profit=round(rate * 100, 2)
|
||||||
) for i, (pair, rate) in enumerate(pair_rates))
|
) for i, (pair, rate) in enumerate(pair_rates))
|
||||||
|
|
||||||
message = '<b>Performance:</b>\n{}\n'.format(stats)
|
message = '<b>Performance:</b>\n{}\n{}'.format(
|
||||||
|
stats,
|
||||||
|
'<b>NOTE:</b> These values are mocked because <b>dry_run</b> is enabled.'
|
||||||
|
if _CONF['dry_run'] else ''
|
||||||
|
)
|
||||||
logger.debug(message)
|
logger.debug(message)
|
||||||
send_msg(message, parse_mode=ParseMode.HTML)
|
send_msg(message, parse_mode=ParseMode.HTML)
|
||||||
|
|
||||||
@ -403,6 +430,7 @@ def _help(bot: Bot, update: Update) -> None:
|
|||||||
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
*/forcesell <trade_id>:* `Instantly sells the given trade, regardless of profit`
|
||||||
*/performance:* `Show performance of each finished trade grouped by pair`
|
*/performance:* `Show performance of each finished trade grouped by pair`
|
||||||
*/count:* `Show number of trades running compared to allowed number of trades`
|
*/count:* `Show number of trades running compared to allowed number of trades`
|
||||||
|
*/balance:* `Show account balance per currency`
|
||||||
*/help:* `This help message`
|
*/help:* `This help message`
|
||||||
"""
|
"""
|
||||||
send_msg(message, bot=bot)
|
send_msg(message, bot=bot)
|
||||||
@ -428,18 +456,19 @@ def send_msg(msg: str, bot: Bot = None, parse_mode: ParseMode = ParseMode.MARKDO
|
|||||||
:param parse_mode: telegram parse mode
|
:param parse_mode: telegram parse mode
|
||||||
:return: None
|
:return: None
|
||||||
"""
|
"""
|
||||||
if _CONF['telegram'].get('enabled', False):
|
if not is_enabled():
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
bot = bot or _updater.bot
|
||||||
try:
|
try:
|
||||||
bot = bot or _updater.bot
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
try:
|
except NetworkError as error:
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
# Sometimes the telegram server resets the current connection,
|
||||||
except NetworkError as error:
|
# if this is the case we send the message again.
|
||||||
# Sometimes the telegram server resets the current connection,
|
logger.warning(
|
||||||
# if this is the case we send the message again.
|
'Got Telegram NetworkError: %s! Trying one more time.',
|
||||||
logger.warning(
|
error.message
|
||||||
'Got Telegram NetworkError: %s! Trying one more time.',
|
)
|
||||||
error.message
|
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
||||||
)
|
except Exception:
|
||||||
bot.send_message(_CONF['telegram']['chat_id'], msg, parse_mode=parse_mode)
|
logger.exception('Exception occurred within Telegram API')
|
||||||
except Exception:
|
|
||||||
logger.exception('Exception occurred within Telegram API')
|
|
||||||
|
@ -7,6 +7,7 @@ from pandas import DataFrame
|
|||||||
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
from freqtrade.analyze import parse_ticker_dataframe, populate_buy_trend, populate_indicators, \
|
||||||
get_buy_signal
|
get_buy_signal
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def result():
|
def result():
|
||||||
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
with open('freqtrade/tests/testdata/btc-eth.json') as data_file:
|
||||||
@ -14,18 +15,22 @@ def result():
|
|||||||
|
|
||||||
return parse_ticker_dataframe(data['result'])
|
return parse_ticker_dataframe(data['result'])
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_has_correct_columns(result):
|
def test_dataframe_has_correct_columns(result):
|
||||||
assert result.columns.tolist() == \
|
assert result.columns.tolist() == \
|
||||||
['close', 'high', 'low', 'open', 'date', 'volume']
|
['close', 'high', 'low', 'open', 'date', 'volume']
|
||||||
|
|
||||||
|
|
||||||
def test_dataframe_has_correct_length(result):
|
def test_dataframe_has_correct_length(result):
|
||||||
assert len(result.index) == 5751
|
assert len(result.index) == 5751
|
||||||
|
|
||||||
|
|
||||||
def test_populates_buy_trend(result):
|
def test_populates_buy_trend(result):
|
||||||
dataframe = populate_buy_trend(populate_indicators(result))
|
dataframe = populate_buy_trend(populate_indicators(result))
|
||||||
assert 'buy' in dataframe.columns
|
assert 'buy' in dataframe.columns
|
||||||
assert 'buy_price' in dataframe.columns
|
assert 'buy_price' in dataframe.columns
|
||||||
|
|
||||||
|
|
||||||
def test_returns_latest_buy_signal(mocker):
|
def test_returns_latest_buy_signal(mocker):
|
||||||
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
|
buydf = DataFrame([{'buy': 1, 'date': datetime.today()}])
|
||||||
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
mocker.patch('freqtrade.analyze.analyze_ticker', return_value=buydf)
|
||||||
|
@ -7,11 +7,14 @@ import pytest
|
|||||||
import arrow
|
import arrow
|
||||||
from pandas import DataFrame
|
from pandas import DataFrame
|
||||||
|
|
||||||
|
from freqtrade import exchange
|
||||||
from freqtrade.analyze import analyze_ticker
|
from freqtrade.analyze import analyze_ticker
|
||||||
|
from freqtrade.exchange import Bittrex
|
||||||
from freqtrade.main import should_sell
|
from freqtrade.main import should_sell
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
|
|
||||||
def format_results(results):
|
def format_results(results):
|
||||||
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
return 'Made {} buys. Average profit {:.2f}%. Total profit was {:.3f}. Average duration {:.1f} mins.'.format(
|
||||||
@ -21,15 +24,18 @@ def format_results(results):
|
|||||||
results.duration.mean() * 5
|
results.duration.mean() * 5
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def print_pair_results(pair, results):
|
def print_pair_results(pair, results):
|
||||||
print('For currency {}:'.format(pair))
|
print('For currency {}:'.format(pair))
|
||||||
print(format_results(results[results.currency == pair]))
|
print(format_results(results[results.currency == pair]))
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def pairs():
|
def pairs():
|
||||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def conf():
|
def conf():
|
||||||
return {
|
return {
|
||||||
@ -42,23 +48,29 @@ def conf():
|
|||||||
"stoploss": -0.40
|
"stoploss": -0.40
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def backtest(conf, pairs, mocker):
|
def backtest(conf, pairs, mocker):
|
||||||
trades = []
|
trades = []
|
||||||
|
exchange._API = Bittrex({'key': '', 'secret': ''})
|
||||||
|
mocked_history = mocker.patch('freqtrade.analyze.get_ticker_history')
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
||||||
for pair in pairs:
|
for pair in pairs:
|
||||||
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
with open('freqtrade/tests/testdata/'+pair+'.json') as data_file:
|
||||||
data = json.load(data_file)
|
mocked_history.return_value = json.load(data_file)
|
||||||
|
|
||||||
mocker.patch('freqtrade.analyze.get_ticker_history', return_value=data)
|
|
||||||
mocker.patch('arrow.utcnow', return_value=arrow.get('2017-08-20T14:50:00'))
|
|
||||||
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
ticker = analyze_ticker(pair)[['close', 'date', 'buy']].copy()
|
||||||
# for each buy point
|
# for each buy point
|
||||||
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
for row in ticker[ticker.buy == 1].itertuples(index=True):
|
||||||
trade = Trade(open_rate=row.close, open_date=row.date, amount=1)
|
trade = Trade(
|
||||||
|
open_rate=row.close,
|
||||||
|
open_date=row.date,
|
||||||
|
amount=1,
|
||||||
|
fee=exchange.get_fee()*2
|
||||||
|
)
|
||||||
# calculate win/lose forwards from buy point
|
# calculate win/lose forwards from buy point
|
||||||
for row2 in ticker[row.Index:].itertuples(index=True):
|
for row2 in ticker[row.Index:].itertuples(index=True):
|
||||||
if should_sell(trade, row2.close, row2.date):
|
if should_sell(trade, row2.close, row2.date):
|
||||||
current_profit = (row2.close - trade.open_rate) / trade.open_rate
|
current_profit = trade.calc_profit(row2.close)
|
||||||
|
|
||||||
trades.append((pair, current_profit, row2.Index - row.Index))
|
trades.append((pair, current_profit, row2.Index - row.Index))
|
||||||
break
|
break
|
||||||
@ -66,11 +78,13 @@ def backtest(conf, pairs, mocker):
|
|||||||
results = DataFrame.from_records(trades, columns=labels)
|
results = DataFrame.from_records(trades, columns=labels)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_backtest(conf, pairs, mocker, report=True):
|
def test_backtest(conf, pairs, mocker, report=True):
|
||||||
results = backtest(conf, pairs, mocker)
|
results = backtest(conf, pairs, mocker)
|
||||||
|
|
||||||
print('====================== BACKTESTING REPORT ================================')
|
print('====================== BACKTESTING REPORT ================================')
|
||||||
[print_pair_results(pair, results) for pair in pairs]
|
for pair in pairs:
|
||||||
|
print_pair_results(pair, results)
|
||||||
print('TOTAL OVER ALL TRADES:')
|
print('TOTAL OVER ALL TRADES:')
|
||||||
print(format_results(results))
|
print(format_results(results))
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
from operator import itemgetter
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
from math import exp
|
from math import exp
|
||||||
import pytest
|
from operator import itemgetter
|
||||||
from pandas import DataFrame
|
|
||||||
from qtpylib.indicators import crossed_above
|
|
||||||
|
|
||||||
|
import pytest
|
||||||
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
from hyperopt import fmin, tpe, hp, Trials, STATUS_OK
|
||||||
|
from pandas import DataFrame
|
||||||
|
|
||||||
from freqtrade.tests.test_backtesting import backtest, format_results
|
from freqtrade.tests.test_backtesting import backtest, format_results
|
||||||
|
from freqtrade.vendor.qtpylib.indicators import crossed_above
|
||||||
|
|
||||||
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
logging.disable(logging.DEBUG) # disable debug logs that slow backtesting a lot
|
||||||
|
|
||||||
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
# set TARGET_TRADES to suit your number concurrent trades so its realistic to 20days of data
|
||||||
TARGET_TRADES = 1200
|
TARGET_TRADES = 1200
|
||||||
@ -23,6 +23,7 @@ def pairs():
|
|||||||
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
return ['btc-neo', 'btc-eth', 'btc-omg', 'btc-edg', 'btc-pay',
|
||||||
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
'btc-pivx', 'btc-qtum', 'btc-mtl', 'btc-etc', 'btc-ltc']
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def conf():
|
def conf():
|
||||||
return {
|
return {
|
||||||
@ -35,15 +36,15 @@ def conf():
|
|||||||
"stoploss": -0.05
|
"stoploss": -0.05
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def buy_strategy_generator(params):
|
def buy_strategy_generator(params):
|
||||||
print(params)
|
print(params)
|
||||||
|
|
||||||
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
def populate_buy_trend(dataframe: DataFrame) -> DataFrame:
|
||||||
conditions = []
|
conditions = []
|
||||||
# GUARDS AND TRENDS
|
# GUARDS AND TRENDS
|
||||||
if params['below_sma']['enabled']:
|
if params['uptrend_long_ema']['enabled']:
|
||||||
conditions.append(dataframe['close'] < dataframe['sma'])
|
conditions.append(dataframe['ema50'] > dataframe['ema100'])
|
||||||
if params['over_sma']['enabled']:
|
|
||||||
conditions.append(dataframe['close'] > dataframe['sma'])
|
|
||||||
if params['mfi']['enabled']:
|
if params['mfi']['enabled']:
|
||||||
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
conditions.append(dataframe['mfi'] < params['mfi']['value'])
|
||||||
if params['fastd']['enabled']:
|
if params['fastd']['enabled']:
|
||||||
@ -52,6 +53,8 @@ def buy_strategy_generator(params):
|
|||||||
conditions.append(dataframe['adx'] > params['adx']['value'])
|
conditions.append(dataframe['adx'] > params['adx']['value'])
|
||||||
if params['cci']['enabled']:
|
if params['cci']['enabled']:
|
||||||
conditions.append(dataframe['cci'] < params['cci']['value'])
|
conditions.append(dataframe['cci'] < params['cci']['value'])
|
||||||
|
if params['rsi']['enabled']:
|
||||||
|
conditions.append(dataframe['rsi'] < params['rsi']['value'])
|
||||||
if params['over_sar']['enabled']:
|
if params['over_sar']['enabled']:
|
||||||
conditions.append(dataframe['close'] > dataframe['sar'])
|
conditions.append(dataframe['close'] > dataframe['sar'])
|
||||||
if params['uptrend_sma']['enabled']:
|
if params['uptrend_sma']['enabled']:
|
||||||
@ -64,6 +67,8 @@ def buy_strategy_generator(params):
|
|||||||
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
'lower_bb': dataframe['tema'] <= dataframe['blower'],
|
||||||
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
|
'faststoch10': (dataframe['fastd'] >= 10) & (prev_fastd < 10),
|
||||||
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
'ao_cross_zero': (crossed_above(dataframe['ao'], 0.0)),
|
||||||
|
'ema5_cross_ema10': (crossed_above(dataframe['ema5'], dataframe['ema10'])),
|
||||||
|
'macd_cross_signal': (crossed_above(dataframe['macd'], dataframe['macdsignal'])),
|
||||||
}
|
}
|
||||||
conditions.append(triggers.get(params['trigger']['type']))
|
conditions.append(triggers.get(params['trigger']['type']))
|
||||||
|
|
||||||
@ -75,11 +80,14 @@ def buy_strategy_generator(params):
|
|||||||
return dataframe
|
return dataframe
|
||||||
return populate_buy_trend
|
return populate_buy_trend
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
@pytest.mark.skipif(not os.environ.get('BACKTEST', False), reason="BACKTEST not set")
|
||||||
def test_hyperopt(conf, pairs, mocker):
|
def test_hyperopt(conf, pairs, mocker):
|
||||||
|
mocked_buy_trend = mocker.patch('freqtrade.analyze.populate_buy_trend')
|
||||||
|
|
||||||
def optimizer(params):
|
def optimizer(params):
|
||||||
buy_strategy = buy_strategy_generator(params)
|
mocked_buy_trend.side_effect = buy_strategy_generator(params)
|
||||||
mocker.patch('freqtrade.analyze.populate_buy_trend', side_effect=buy_strategy)
|
|
||||||
results = backtest(conf, pairs, mocker)
|
results = backtest(conf, pairs, mocker)
|
||||||
|
|
||||||
result = format_results(results)
|
result = format_results(results)
|
||||||
@ -100,25 +108,25 @@ def test_hyperopt(conf, pairs, mocker):
|
|||||||
space = {
|
space = {
|
||||||
'mfi': hp.choice('mfi', [
|
'mfi': hp.choice('mfi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('mfi-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('mfi-value', 5, 15)}
|
||||||
]),
|
]),
|
||||||
'fastd': hp.choice('fastd', [
|
'fastd': hp.choice('fastd', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('fastd-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('fastd-value', 5, 40)}
|
||||||
]),
|
]),
|
||||||
'adx': hp.choice('adx', [
|
'adx': hp.choice('adx', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('adx-value', 2, 40)}
|
{'enabled': True, 'value': hp.uniform('adx-value', 10, 30)}
|
||||||
]),
|
]),
|
||||||
'cci': hp.choice('cci', [
|
'cci': hp.choice('cci', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True, 'value': hp.uniform('cci-value', -200, -100)}
|
{'enabled': True, 'value': hp.uniform('cci-value', -150, -100)}
|
||||||
]),
|
]),
|
||||||
'below_sma': hp.choice('below_sma', [
|
'rsi': hp.choice('rsi', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True}
|
{'enabled': True, 'value': hp.uniform('rsi-value', 20, 30)}
|
||||||
]),
|
]),
|
||||||
'over_sma': hp.choice('over_sma', [
|
'uptrend_long_ema': hp.choice('uptrend_long_ema', [
|
||||||
{'enabled': False},
|
{'enabled': False},
|
||||||
{'enabled': True}
|
{'enabled': True}
|
||||||
]),
|
]),
|
||||||
@ -133,11 +141,13 @@ def test_hyperopt(conf, pairs, mocker):
|
|||||||
'trigger': hp.choice('trigger', [
|
'trigger': hp.choice('trigger', [
|
||||||
{'type': 'lower_bb'},
|
{'type': 'lower_bb'},
|
||||||
{'type': 'faststoch10'},
|
{'type': 'faststoch10'},
|
||||||
{'type': 'ao_cross_zero'}
|
{'type': 'ao_cross_zero'},
|
||||||
|
{'type': 'ema5_cross_ema10'},
|
||||||
|
{'type': 'macd_cross_signal'},
|
||||||
]),
|
]),
|
||||||
}
|
}
|
||||||
trials = Trials()
|
trials = Trials()
|
||||||
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=40, trials=trials)
|
best = fmin(fn=optimizer, space=space, algo=tpe.suggest, max_evals=4, trials=trials)
|
||||||
print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================')
|
print('\n\n\n\n====================== HYPEROPT BACKTESTING REPORT ================================')
|
||||||
print('Best parameters {}'.format(best))
|
print('Best parameters {}'.format(best))
|
||||||
newlist = sorted(trials.results, key=itemgetter('loss'))
|
newlist = sorted(trials.results, key=itemgetter('loss'))
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
# pragma pylint: disable=missing-docstring
|
||||||
import copy
|
import copy
|
||||||
|
from datetime import datetime
|
||||||
from unittest.mock import MagicMock, call
|
from unittest.mock import MagicMock, call
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@ -48,6 +49,7 @@ def conf():
|
|||||||
validate(configuration, CONF_SCHEMA)
|
validate(configuration, CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
def test_create_trade(conf, mocker):
|
def test_create_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
buy_signal = mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
@ -59,29 +61,43 @@ def test_create_trade(conf, mocker):
|
|||||||
'ask': 0.072661,
|
'ask': 0.072661,
|
||||||
'last': 0.07256061
|
'last': 0.07256061
|
||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value='mocked_order_id'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
# Save state of current whitelist
|
# Save state of current whitelist
|
||||||
whitelist = copy.deepcopy(conf['exchange']['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 _ in ['BTC_ETH', 'BTC_TKN', 'BTC_TRST', 'BTC_SWT']:
|
||||||
trade = create_trade(15.0)
|
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.pair == pair
|
|
||||||
assert trade.exchange == Exchanges.BITTREX.name
|
|
||||||
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 trade.exchange == Exchanges.BITTREX.name
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_buy',
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.072661,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert trade.open_rate == 0.072661
|
||||||
|
assert trade.amount == 206.43811673387373
|
||||||
|
|
||||||
assert whitelist == conf['exchange']['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')]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_handle_trade(conf, mocker):
|
def test_handle_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
mocker.patch.multiple('freqtrade.main.telegram', init=MagicMock(), send_msg=MagicMock())
|
||||||
@ -92,14 +108,29 @@ def test_handle_trade(conf, mocker):
|
|||||||
'ask': 0.172661,
|
'ask': 0.172661,
|
||||||
'last': 0.17256061
|
'last': 0.17256061
|
||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value='mocked_order_id'))
|
sell=MagicMock(return_value='mocked_limit_sell'))
|
||||||
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
trade = Trade.query.filter(Trade.is_open.is_(True)).first()
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
handle_trade(trade)
|
handle_trade(trade)
|
||||||
|
assert trade.open_order_id == 'mocked_limit_sell'
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_sell_limit',
|
||||||
|
'type': 'LIMIT_SELL',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.17256061,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
assert trade.close_rate == 0.17256061
|
assert trade.close_rate == 0.17256061
|
||||||
assert trade.close_profit == 137.4872490056564
|
assert trade.close_profit == 1.3698725
|
||||||
assert trade.close_date is not None
|
assert trade.close_date is not None
|
||||||
assert trade.open_order_id == 'dry_run'
|
|
||||||
|
|
||||||
def test_close_trade(conf, mocker):
|
def test_close_trade(conf, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
@ -113,14 +144,17 @@ def test_close_trade(conf, mocker):
|
|||||||
assert closed
|
assert closed
|
||||||
assert not trade.is_open
|
assert not trade.is_open
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_ask_side(mocker):
|
def test_balance_fully_ask_side(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 0.0}})
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
assert get_target_bid({'ask': 20, 'last': 10}) == 20
|
||||||
|
|
||||||
|
|
||||||
def test_balance_fully_last_side(mocker):
|
def test_balance_fully_last_side(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
assert get_target_bid({'ask': 20, 'last': 10}) == 10
|
||||||
|
|
||||||
|
|
||||||
def test_balance_when_last_bigger_than_ask(mocker):
|
def test_balance_when_last_bigger_than_ask(mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
mocker.patch.dict('freqtrade.main._CONF', {'bid_strategy': {'ask_last_balance': 1.0}})
|
||||||
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
assert get_target_bid({'ask': 5, 'last': 10}) == 5
|
||||||
|
@ -1,20 +0,0 @@
|
|||||||
# pragma pylint: disable=missing-docstring
|
|
||||||
from freqtrade.exchange import Exchanges
|
|
||||||
from freqtrade.persistence import Trade
|
|
||||||
|
|
||||||
def test_exec_sell_order(mocker):
|
|
||||||
api_mock = mocker.patch('freqtrade.main.exchange.sell', side_effect='mocked_order_id')
|
|
||||||
trade = Trade(
|
|
||||||
pair='BTC_ETH',
|
|
||||||
stake_amount=1.00,
|
|
||||||
open_rate=0.50,
|
|
||||||
amount=10.00,
|
|
||||||
exchange=Exchanges.BITTREX,
|
|
||||||
open_order_id='mocked'
|
|
||||||
)
|
|
||||||
profit = trade.exec_sell_order(1.00, 10.00)
|
|
||||||
api_mock.assert_called_once_with('BTC_ETH', 1.0, 10.0)
|
|
||||||
assert profit == 100.0
|
|
||||||
assert trade.close_rate == 1.0
|
|
||||||
assert trade.close_profit == profit
|
|
||||||
assert trade.close_date is not None
|
|
@ -11,12 +11,9 @@ from telegram import Bot, Update, Message, Chat
|
|||||||
from freqtrade.main import init, create_trade
|
from freqtrade.main import init, create_trade
|
||||||
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
from freqtrade.misc import update_state, State, get_state, CONF_SCHEMA
|
||||||
from freqtrade.persistence import Trade
|
from freqtrade.persistence import Trade
|
||||||
from freqtrade.rpc.telegram import _status, _status_table, _profit, _forcesell, _performance, \
|
from freqtrade.rpc.telegram import (
|
||||||
_count, _start, _stop
|
_status, _status_table, _profit, _forcesell, _performance, _count, _start, _stop, _balance
|
||||||
|
)
|
||||||
logging.getLogger('requests.packages.urllib3').setLevel(logging.INFO)
|
|
||||||
logging.getLogger('telegram').setLevel(logging.INFO)
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@ -54,6 +51,7 @@ def conf():
|
|||||||
validate(configuration, CONF_SCHEMA)
|
validate(configuration, CONF_SCHEMA)
|
||||||
return configuration
|
return configuration
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def update():
|
def update():
|
||||||
_update = Update(0)
|
_update = Update(0)
|
||||||
@ -69,7 +67,10 @@ def test_status_handle(conf, update, mocker):
|
|||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -86,8 +87,26 @@ def test_status_handle(conf, update, mocker):
|
|||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
|
# Trigger status while we don't know the open_rate yet
|
||||||
_status(bot=MagicBot(), update=update)
|
_status(bot=MagicBot(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_buy',
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.07256060,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
Trade.session.flush()
|
||||||
|
|
||||||
|
# Trigger status while we have a fulfilled order for the open trade
|
||||||
|
_status(bot=MagicBot(), update=update)
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 3
|
||||||
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert '[BTC_ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
@ -127,7 +146,10 @@ def test_profit_handle(conf, update, mocker):
|
|||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -135,14 +157,36 @@ def test_profit_handle(conf, update, mocker):
|
|||||||
'ask': 0.072661,
|
'ask': 0.072661,
|
||||||
'last': 0.07256061
|
'last': 0.07256061
|
||||||
}),
|
}),
|
||||||
buy=MagicMock(return_value='mocked_order_id'))
|
buy=MagicMock(return_value='mocked_limit_buy'))
|
||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
trade.close_rate = 0.07256061
|
|
||||||
trade.close_profit = 100.00
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_buy',
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.07256061,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_sell',
|
||||||
|
'type': 'LIMIT_SELL',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.0802134,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.open_order_id = None
|
trade.open_order_id = None
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
@ -151,13 +195,18 @@ def test_profit_handle(conf, update, mocker):
|
|||||||
|
|
||||||
_profit(bot=MagicBot(), update=update)
|
_profit(bot=MagicBot(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
assert '(100.00%)' in msg_mock.call_args_list[-1][0][0]
|
assert '*ROI:* `1.507013 (10.05%)`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
assert 'Best Performing:* `BTC_ETH: 10.05%`' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_forcesell_handle(conf, update, mocker):
|
def test_forcesell_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -171,6 +220,19 @@ def test_forcesell_handle(conf, update, mocker):
|
|||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_buy',
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.07256060,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
|
|
||||||
@ -179,13 +241,17 @@ def test_forcesell_handle(conf, update, mocker):
|
|||||||
|
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
assert 'Selling [BTC/ETH]' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert '0.072561' in msg_mock.call_args_list[-1][0][0]
|
assert '0.072561 (profit: ~-0.5%)' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(conf, update, mocker):
|
def test_performance_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
mocker.patch('freqtrade.main.get_buy_signal', side_effect=lambda _: True)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
mocker.patch.multiple('freqtrade.main.exchange',
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
validate_pairs=MagicMock(),
|
validate_pairs=MagicMock(),
|
||||||
get_ticker=MagicMock(return_value={
|
get_ticker=MagicMock(return_value={
|
||||||
@ -199,10 +265,32 @@ def test_performance_handle(conf, update, mocker):
|
|||||||
# Create some test data
|
# Create some test data
|
||||||
trade = create_trade(15.0)
|
trade = create_trade(15.0)
|
||||||
assert trade
|
assert trade
|
||||||
trade.close_rate = 0.07256061
|
|
||||||
trade.close_profit = 100.00
|
# Simulate fulfilled LIMIT_BUY order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_buy',
|
||||||
|
'type': 'LIMIT_BUY',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.07256061,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
|
# Simulate fulfilled LIMIT_SELL order for trade
|
||||||
|
trade.update({
|
||||||
|
'id': 'mocked_limit_sell',
|
||||||
|
'type': 'LIMIT_SELL',
|
||||||
|
'pair': 'mocked',
|
||||||
|
'opened': datetime.utcnow(),
|
||||||
|
'rate': 0.0802134,
|
||||||
|
'amount': 206.43811673387373,
|
||||||
|
'remaining': 0.0,
|
||||||
|
'closed': datetime.utcnow(),
|
||||||
|
})
|
||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.open_order_id = None
|
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
Trade.session.add(trade)
|
Trade.session.add(trade)
|
||||||
Trade.session.flush()
|
Trade.session.flush()
|
||||||
@ -210,7 +298,8 @@ def test_performance_handle(conf, update, mocker):
|
|||||||
_performance(bot=MagicBot(), update=update)
|
_performance(bot=MagicBot(), update=update)
|
||||||
assert msg_mock.call_count == 2
|
assert msg_mock.call_count == 2
|
||||||
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
assert 'Performance' in msg_mock.call_args_list[-1][0][0]
|
||||||
assert 'BTC_ETH 100.00%' in msg_mock.call_args_list[-1][0][0]
|
assert '<code>BTC_ETH\t10.05%</code>' in msg_mock.call_args_list[-1][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(conf, update, mocker):
|
def test_count_handle(conf, update, mocker):
|
||||||
@ -245,8 +334,13 @@ def test_count_handle(conf, update, mocker):
|
|||||||
def test_start_handle(conf, update, mocker):
|
def test_start_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock())
|
||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
update_state(State.STOPPED)
|
update_state(State.STOPPED)
|
||||||
@ -255,11 +349,17 @@ def test_start_handle(conf, update, mocker):
|
|||||||
assert get_state() == State.RUNNING
|
assert get_state() == State.RUNNING
|
||||||
assert msg_mock.call_count == 0
|
assert msg_mock.call_count == 0
|
||||||
|
|
||||||
|
|
||||||
def test_stop_handle(conf, update, mocker):
|
def test_stop_handle(conf, update, mocker):
|
||||||
mocker.patch.dict('freqtrade.main._CONF', conf)
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
msg_mock = MagicMock()
|
msg_mock = MagicMock()
|
||||||
mocker.patch.multiple('freqtrade.main.telegram', _CONF=conf, init=MagicMock(), send_msg=msg_mock)
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
mocker.patch.multiple('freqtrade.main.exchange', _CONF=conf, init=MagicMock())
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock())
|
||||||
init(conf, 'sqlite://')
|
init(conf, 'sqlite://')
|
||||||
|
|
||||||
update_state(State.RUNNING)
|
update_state(State.RUNNING)
|
||||||
@ -268,3 +368,25 @@ def test_stop_handle(conf, update, mocker):
|
|||||||
assert get_state() == State.STOPPED
|
assert get_state() == State.STOPPED
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
assert 'Stopping trader' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_balance_handle(conf, update, mocker):
|
||||||
|
mock_balance = [{
|
||||||
|
'Currency': 'BTC',
|
||||||
|
'Balance': 10.0,
|
||||||
|
'Available': 12.0,
|
||||||
|
'Pending': 0.0,
|
||||||
|
'CryptoAddress': 'XXXX'}]
|
||||||
|
mocker.patch.dict('freqtrade.main._CONF', conf)
|
||||||
|
msg_mock = MagicMock()
|
||||||
|
mocker.patch.multiple('freqtrade.main.telegram',
|
||||||
|
_CONF=conf,
|
||||||
|
init=MagicMock(),
|
||||||
|
send_msg=msg_mock)
|
||||||
|
mocker.patch.multiple('freqtrade.main.exchange',
|
||||||
|
get_balances=MagicMock(return_value=mock_balance))
|
||||||
|
|
||||||
|
_balance(bot=MagicBot(), update=update)
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert '*Currency*: BTC' in msg_mock.call_args_list[0][0][0]
|
||||||
|
assert 'Balance' in msg_mock.call_args_list[0][0][0]
|
||||||
|
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
0
freqtrade/vendor/qtpylib/__init__.py
vendored
Normal file
Loading…
Reference in New Issue
Block a user