Merge branch 'wohlgemuth' into nullartHFT

This commit is contained in:
Gert Wohlgemuth 2018-06-13 21:50:01 -07:00 committed by GitHub
commit 22743be173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2202 additions and 769 deletions

View File

@ -6,10 +6,12 @@ If it hasn't been reported, please create a new issue.
## Step 2: Describe your environment ## Step 2: Describe your environment
* Python Version: _____ (`python -V`) * Python Version: _____ (`python -V`)
* CCXT version: _____ (`pip freeze | grep ccxt`)
* Branch: Master | Develop * Branch: Master | Develop
* Last Commit ID: _____ (`git log --format="%H" -n 1`) * Last Commit ID: _____ (`git log --format="%H" -n 1`)
## Step 3: Describe the problem: ## Step 3: Describe the problem:
*Explain the problem you have encountered* *Explain the problem you have encountered*
### Steps to reproduce: ### Steps to reproduce:

5
.gitignore vendored
View File

@ -6,7 +6,6 @@ config*.json
.hyperopt .hyperopt
logfile.txt logfile.txt
hyperopt_trials.pickle hyperopt_trials.pickle
user_data/
freqtrade-plot.html freqtrade-plot.html
freqtrade-profit-plot.html freqtrade-profit-plot.html
@ -27,8 +26,8 @@ dist/
downloads/ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib/ #lib/
lib64/ #lib64/
parts/ parts/
sdist/ sdist/
var/ var/

View File

@ -42,6 +42,11 @@ pip3.6 install flake8 coveralls
flake8 freqtrade flake8 freqtrade
``` ```
We receive a lot of code that fails the `flake8` checks.
To help with that, we encourage you to install the git pre-commit
hook that will warn you when you try to commit code that fails these checks.
Guide for installing them is [here](http://flake8.pycqa.org/en/latest/user/using-hooks.html).
## 3. Test if all type-hints are correct ## 3. Test if all type-hints are correct
**Install packages** (If not already installed) **Install packages** (If not already installed)

View File

@ -1,7 +1,7 @@
FROM python:3.6.5-slim-stretch FROM python:3.6.5-slim-stretch
# Install TA-lib # Install TA-lib
RUN apt-get update && apt-get -y install curl build-essential && apt-get clean RUN apt-get update && apt-get -y install curl build-essential git && apt-get clean
RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \ RUN curl -L http://prdownloads.sourceforge.net/ta-lib/ta-lib-0.4.0-src.tar.gz | \
tar xzvf - && \ tar xzvf - && \
cd ta-lib && \ cd ta-lib && \

View File

@ -4,6 +4,15 @@
[![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop) [![Coverage Status](https://coveralls.io/repos/github/freqtrade/freqtrade/badge.svg?branch=develop&service=github)](https://coveralls.io/github/freqtrade/freqtrade?branch=develop)
[![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability) [![Maintainability](https://api.codeclimate.com/v1/badges/5737e6d668200b7518ff/maintainability)](https://codeclimate.com/github/freqtrade/freqtrade/maintainability)
## First of all, this is a fork!
Basically I required a lot more features than the awesome default freqtrade version has to offer and since pull requests always take longer than exspected or the standard disagreements. I decided to maintain on main branch for my changes, called wohlgemuth, which is incidentally my last name and have a ton of little branches, with added features.
This basically allows people to use my version, or to easily merge changes into their forks or make PR's against the main repo, which is the best of both works.
This reminds of the Torvalds kernel vs the Cox kernel...
## Back to what this is actually about
Simple High frequency trading bot for crypto currencies designed to Simple High frequency trading bot for crypto currencies designed to
support multi exchanges and be controlled via Telegram. support multi exchanges and be controlled via Telegram.
@ -44,11 +53,8 @@ hesitate to read the source code and understand the mechanism of this bot.
- [Software requirements](#software-requirements) - [Software requirements](#software-requirements)
## Branches ## Branches
The project is currently setup in two main branches:
- `develop` - This branch has often new features, but might also cause if you like to use this fork, I highly recommend to utilize the 'wohlgemuth' branch, since this is the most stable. It will be synced against the original development branch and be enriched with all my changes.
breaking changes.
- `master` - This branch contains the latest stable release. The bot
'should' be stable on this branch, and is generally well tested.
## Features ## Features
- [x] **Based on Python 3.6+**: For botting on any operating system - - [x] **Based on Python 3.6+**: For botting on any operating system -
@ -65,6 +71,26 @@ strategy parameters with real exchange data.
- [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss. - [x] **Daily summary of profit/loss**: Provide a daily summary of your profit/loss.
- [x] **Performance status report**: Provide a performance status of your current trades. - [x] **Performance status report**: Provide a performance status of your current trades.
### Additional features in this branch
#### Strategy:
- [x] loading strategies from Base64 encoded data in the config file
- [x] loading strategies from urls
- [x] trailing stop loss
#### Others:
- [x] more indicators
- [x] more telegram features
- [x] advanced plotting
### Drawbacks
- [x] not as good documentation
- [x] maybe a bug here or there I haven't fixed yet
### Exchange marketplaces supported ### Exchange marketplaces supported
- [X] [Bittrex](https://bittrex.com/) - [X] [Bittrex](https://bittrex.com/)
- [X] [Binance](https://www.binance.com/) - [X] [Binance](https://www.binance.com/)

View File

@ -10,6 +10,10 @@
"buy":10, "buy":10,
"sell":30 "sell":30
} }
"trailing_stop": {
"positive" : 0.005
},
"unfilledtimeout": 600,
"bid_strategy": { "bid_strategy": {
"ask_last_balance": 0.0, "ask_last_balance": 0.0,
"use_book_order": true, "use_book_order": true,

View File

@ -6,6 +6,7 @@
"dry_run": false, "dry_run": false,
"disable_buy" : true, "disable_buy" : true,
"ticker_interval": "5m", "ticker_interval": "5m",
"trailing_stop": true,
"minimal_roi": { "minimal_roi": {
"40": 0.0, "40": 0.0,
"30": 0.01, "30": 0.01,

50
docs/stoploss.md Normal file
View File

@ -0,0 +1,50 @@
# Stop Loss support
at this stage the bot contains the following stoploss support modes:
1. static stop loss, defined in either the strategy or configuration
2. trailing stop loss, defined in the configuration
3. trailing stop loss, custom positive loss, defined in configuration
## Static Stop Loss
This is very simple, basically you define a stop loss of x in your strategy file or alternative in the configuration, which
will overwrite the strategy definition. This will basically try to sell your asset, the second the loss exceeds the defined loss.
## Trail Stop Loss
The initial value for this stop loss, is defined in your strategy or configuration. Just as you would define your Stop Loss normally.
To enable this Feauture all you have to do, is to define the configuration element:
```
"trailing_stop" : True
```
This will now actiave an algorithm, whihch automatically moves up your stop loss, every time the price of your asset increases.
For example, simplified math,
* you buy an asset at a price of 100$
* your stop loss is defined at 2%
* which means your stop loss, gets triggered once your asset dropped below 98$
* assuming your asset now increases in proce to 102$
* your stop loss, will now be 2% of 102$ or 99.96$
* now your asset drops in value to 101$, your stop loss, will still be 99.96$
basically what this means, is that your stop loss will be adjusted to be always be 2% of the highest observed price
### Custom positive loss
due to demand, it is possible to have a default stop loss, when you are in the red with your buy, but once your buy turns positive,
the system will utilize a new stop loss, which can be a different value. For example your default stop loss is 5%, but once you are in the
black, it will be changed to be only a 1% stop loss
this can be configured in the main confiuration file, the following way:
```
"trailing_stop": {
"positive" : 0.01
},
```
The 0.01 would translate to a 1% stop loss, once you hit profit.

View File

@ -16,6 +16,7 @@ official commands. You can ask at any moment for help with `/help`.
|----------|---------|-------------| |----------|---------|-------------|
| `/start` | | Starts the trader | `/start` | | Starts the trader
| `/stop` | | Stops the trader | `/stop` | | Stops the trader
| `/reload_conf` | | Reloads the configuration file
| `/status` | | Lists all open trades | `/status` | | Lists all open trades
| `/status table` | | List all open trades in a table format | `/status table` | | List all open trades in a table format
| `/count` | | Displays number of trades used and available | `/count` | | Displays number of trades used and available

View File

@ -14,7 +14,6 @@ from freqtrade.exchange import get_ticker_history
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.strategy.resolver import StrategyResolver, IStrategy from freqtrade.strategy.resolver import StrategyResolver, IStrategy
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -31,6 +30,7 @@ class Analyze(object):
Analyze class contains everything the bot need to determine if the situation is good for Analyze class contains everything the bot need to determine if the situation is good for
buying or selling. buying or selling.
""" """
def __init__(self, config: dict) -> None: def __init__(self, config: dict) -> None:
""" """
Init Analyze Init Analyze
@ -196,10 +196,41 @@ class Analyze(object):
:return True if bot should sell at current rate :return True if bot should sell at current rate
""" """
current_profit = trade.calc_profit_percent(current_rate) current_profit = trade.calc_profit_percent(current_rate)
if self.strategy.stoploss is not None and current_profit < self.strategy.stoploss:
if trade.stop_loss is None:
# initially adjust the stop loss to the base value
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss)
# evaluate if the stoploss was hit
if self.strategy.stoploss is not None and trade.stop_loss >= current_rate:
if 'trailing_stop' in self.config and self.config['trailing_stop']:
logger.debug(
"HIT STOP: current price at {:.6f}, stop loss is {:.6f}, "
"initial stop loss was at {:.6f}, trade opened at {:.6f}".format(
current_rate, trade.stop_loss, trade.initial_stop_loss, trade.open_rate))
logger.debug("trailing stop saved us: {:.6f}"
.format(trade.stop_loss - trade.initial_stop_loss))
logger.debug('Stop loss hit.') logger.debug('Stop loss hit.')
return True return True
# update the stop loss afterwards, after all by definition it's supposed to be hanging
if 'trailing_stop' in self.config and self.config['trailing_stop']:
# check if we have a special stop loss for positive condition
# and if profit is positive
stop_loss_value = self.strategy.stoploss
if isinstance(self.config['trailing_stop'], dict) and \
'positive' in self.config['trailing_stop'] and \
current_profit > 0:
logger.debug("using positive stop loss mode: {} since we have profit {}".format(
self.config['trailing_stop']['positive'], current_profit))
stop_loss_value = self.config['trailing_stop']['positive']
trade.adjust_stop_loss(current_rate, stop_loss_value)
# Check if time matches and current rate is above threshold # Check if time matches and current rate is above threshold
time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60 time_diff = (current_time.timestamp() - trade.open_date.timestamp()) / 60
for duration, threshold in self.strategy.minimal_roi.items(): for duration, threshold in self.strategy.minimal_roi.items():

View File

@ -224,7 +224,7 @@ class Arguments(object):
Builds and attaches all subcommands Builds and attaches all subcommands
:return: None :return: None
""" """
from freqtrade.optimize import backtesting, hyperopt from freqtrade.optimize import backtesting
subparsers = self.parser.add_subparsers(dest='subparser') subparsers = self.parser.add_subparsers(dest='subparser')
@ -235,10 +235,14 @@ class Arguments(object):
self.backtesting_options(backtesting_cmd) self.backtesting_options(backtesting_cmd)
# Add hyperopt subcommand # Add hyperopt subcommand
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module') try:
hyperopt_cmd.set_defaults(func=hyperopt.start) from freqtrade.optimize import hyperopt
self.optimizer_shared_options(hyperopt_cmd) hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
self.hyperopt_options(hyperopt_cmd) hyperopt_cmd.set_defaults(func=hyperopt.start)
self.optimizer_shared_options(hyperopt_cmd)
self.hyperopt_options(hyperopt_cmd)
except ImportError as e:
logging.warn("no hyper opt found - skipping support for it")
@staticmethod @staticmethod
def parse_timerange(text: Optional[str]) -> TimeRange: def parse_timerange(text: Optional[str]) -> TimeRange:
@ -295,6 +299,93 @@ class Arguments(object):
default=None default=None
) )
self.parser.add_argument(
'--stop-loss',
help='Renders stop/loss information in the main chart',
dest='stoplossdisplay',
action='store_true',
default=False
)
self.parser.add_argument(
'--plot-rsi',
help='Renders a rsi chart of the given RSI dataframe name, for example --plot-rsi rsi',
dest='plotrsi',
nargs='+',
default=None
)
self.parser.add_argument(
'--plot-cci',
help='Renders a cci chart of the given CCI dataframe name, for example --plot-cci cci',
dest='plotcci',
nargs='+',
default=None
)
self.parser.add_argument(
'--plot-osc',
help='Renders a osc chart of the given osc dataframe name, for example --plot-osc osc',
dest='plotosc',
nargs='+',
default=None
)
self.parser.add_argument(
'--plot-cmf',
help='Renders a cmf chart of the given cmf dataframe name, for example --plot-cmf cmf',
dest='plotcmf',
nargs='+',
default=None
)
self.parser.add_argument(
'--plot-macd',
help='Renders a macd chart of the given '
'dataframe name, for example --plot-macd macd',
dest='plotmacd',
default=None
)
self.parser.add_argument(
'--plot-dataframe',
help='Renders the specified dataframes',
dest='plotdataframe',
default=None,
nargs='+',
type=str
)
self.parser.add_argument(
'--plot-dataframe-marker',
help='Renders the specified dataframes as markers. '
'Accepted values for a marker are either 100 or -100',
dest='plotdataframemarker',
default=None,
nargs='+',
type=str
)
self.parser.add_argument(
'--plot-volume',
help='plots the volume as a sub plot',
dest='plotvolume',
action='store_true'
)
self.parser.add_argument(
'--plot-max-ticks',
help='specify an upper limit of how many ticks we can display',
dest='plotticks',
default=750,
type=int
)
def testdata_dl_options(self) -> None: def testdata_dl_options(self) -> None:
""" """
Parses given arguments for testdata download Parses given arguments for testdata download

View File

@ -33,7 +33,7 @@ class FreqtradeBot(object):
This is from here the bot start its logic. This is from here the bot start its logic.
""" """
def __init__(self, config: Dict[str, Any])-> None: def __init__(self, config: Dict[str, Any]) -> None:
""" """
Init all variables and object the bot need to work Init all variables and object the bot need to work
:param config: configuration dict, you can use the Configuration.get_config() :param config: configuration dict, you can use the Configuration.get_config()
@ -76,17 +76,14 @@ class FreqtradeBot(object):
else: else:
self.state = State.STOPPED self.state = State.STOPPED
def clean(self) -> bool: def cleanup(self) -> None:
""" """
Cleanup the application state und finish all pending tasks Cleanup pending resources on an already stopped bot
:return: None :return: None
""" """
self.rpc.send_msg('*Status:* `Stopping trader...`') logger.info('Cleaning up modules ...')
logger.info('Stopping trader and cleaning up modules...')
self.state = State.STOPPED
self.rpc.cleanup() self.rpc.cleanup()
persistence.cleanup() persistence.cleanup()
return True
def worker(self, old_state: State = None) -> State: def worker(self, old_state: State = None) -> State:
""" """
@ -459,6 +456,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
# use orderbook, otherwise just use sell rate # use orderbook, otherwise just use sell rate
if (sell_rate < orderBook_rate): if (sell_rate < orderBook_rate):
sell_rate = orderBook_rate sell_rate = orderBook_rate
if self.check_sell(trade, sell_rate, buy, sell): if self.check_sell(trade, sell_rate, buy, sell):
return True return True
break break
@ -503,6 +501,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
continue continue
ordertime = arrow.get(order['datetime']).datetime ordertime = arrow.get(order['datetime']).datetime
print(order)
# Check if trade is still actually open # Check if trade is still actually open
if (int(order['filled']) == 0) and (order['status'] == 'open'): if (int(order['filled']) == 0) and (order['status'] == 'open'):
if order['side'] == 'buy' and ordertime < buy_timeoutthreashold: if order['side'] == 'buy' and ordertime < buy_timeoutthreashold:
@ -599,7 +598,7 @@ with limit `{buy_limit:.8f} ({stake_amount:.6f} \
fiat fiat
) )
message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \ message += f'` ({gain}: {fmt_exp_profit:.2f}%, {profit_trade:.8f} {stake}`' \
f'` / {profit_fiat:.3f} {fiat})`'\ f'` / {profit_fiat:.3f} {fiat})`' \
'' ''
# Because telegram._forcesell does not have the configuration # Because telegram._forcesell does not have the configuration
# Ignore the FIAT value and does not show the stake_currency as well # Ignore the FIAT value and does not show the stake_currency as well

View File

@ -5,12 +5,14 @@ Read the documentation to know what cli arguments you need.
""" """
import logging import logging
import sys import sys
from argparse import Namespace
from typing import List from typing import List
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.configuration import Configuration from freqtrade.configuration import Configuration
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.state import State
logger = logging.getLogger('freqtrade') logger = logging.getLogger('freqtrade')
@ -44,6 +46,8 @@ def main(sysargv: List[str]) -> None:
state = None state = None
while 1: while 1:
state = freqtrade.worker(old_state=state) state = freqtrade.worker(old_state=state)
if state == State.RELOAD_CONF:
freqtrade = reconfigure(freqtrade, args)
except KeyboardInterrupt: except KeyboardInterrupt:
logger.info('SIGINT received, aborting ...') logger.info('SIGINT received, aborting ...')
@ -55,10 +59,28 @@ def main(sysargv: List[str]) -> None:
logger.exception('Fatal exception!') logger.exception('Fatal exception!')
finally: finally:
if freqtrade: if freqtrade:
freqtrade.clean() freqtrade.rpc.send_msg('*Status:* `Process died ...`')
freqtrade.cleanup()
sys.exit(return_code) sys.exit(return_code)
def reconfigure(freqtrade: FreqtradeBot, args: Namespace) -> FreqtradeBot:
"""
Cleans up current instance, reloads the configuration and returns the new instance
"""
# Clean up current modules
freqtrade.cleanup()
# Create new instance
freqtrade = FreqtradeBot(Configuration(args).get_config())
freqtrade.rpc.send_msg(
'*Status:* `Config reloaded ...`'.format(
freqtrade.state.name.lower()
)
)
return freqtrade
def set_loggers() -> None: def set_loggers() -> None:
""" """
Set the logger level for Third party libs Set the logger level for Third party libs

View File

@ -31,6 +31,7 @@ class Backtesting(object):
backtesting = Backtesting(config) backtesting = Backtesting(config)
backtesting.start() backtesting.start()
""" """
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: Dict[str, Any]) -> None:
self.config = config self.config = config
self.analyze = Analyze(self.config) self.analyze = Analyze(self.config)
@ -59,42 +60,48 @@ class Backtesting(object):
for frame in data.values() for frame in data.values()
] ]
return min(timeframe, key=operator.itemgetter(0))[0], \ return min(timeframe, key=operator.itemgetter(0))[0], \
max(timeframe, key=operator.itemgetter(1))[1] max(timeframe, key=operator.itemgetter(1))[1]
def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str: def _generate_text_table(self, data: Dict[str, Dict], results: DataFrame) -> str:
""" """
Generates and returns a text table for the given backtest data and the results dataframe Generates and returns a text table for the given backtest data and the results dataframe
:return: pretty printed table with tabulate as str :return: pretty printed table with tabulate as str
""" """
stake_currency = str(self.config.get('stake_currency'))
floatfmt = ('s', 'd', '.2f', '.8f', '.1f') floatfmt, headers, tabular_data = self.aggregate(data, results)
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe")
def aggregate(self, data, results):
stake_currency = self.config.get('stake_currency')
floatfmt = ('s', 'd', '.2f', '.2f', '.8f', '.1f')
tabular_data = [] tabular_data = []
headers = ['pair', 'buy count', 'avg profit %', headers = ['pair', 'buy count', 'avg profit %', 'cum profit %',
'total profit ' + stake_currency, 'avg duration', 'profit', 'loss'] 'total profit ' + stake_currency, 'avg duration', 'profit', 'loss']
for pair in data: for pair in data:
result = results[results.currency == pair] result = results[results.currency == pair]
print(results)
tabular_data.append([ tabular_data.append([
pair, pair,
len(result.index), len(result.index),
result.profit_percent.mean() * 100.0, result.profit_percent.mean() * 100.0,
result.profit_percent.sum() * 100.0,
result.profit_BTC.sum(), result.profit_BTC.sum(),
result.duration.mean(), result.duration.mean(),
len(result[result.profit_BTC > 0]), len(result[result.profit_BTC > 0]),
len(result[result.profit_BTC < 0]) len(result[result.profit_BTC < 0])
]) ])
# Append Total # Append Total
tabular_data.append([ tabular_data.append([
'TOTAL', 'TOTAL',
len(results.index), len(results.index),
results.profit_percent.mean() * 100.0, results.profit_percent.mean() * 100.0,
results.profit_percent.sum() * 100.0,
results.profit_BTC.sum(), results.profit_BTC.sum(),
results.duration.mean(), results.duration.mean(),
len(results[results.profit_BTC > 0]), len(results[results.profit_BTC > 0]),
len(results[results.profit_BTC < 0]) len(results[results.profit_BTC < 0])
]) ])
return tabulate(tabular_data, headers=headers, floatfmt=floatfmt, tablefmt="pipe") return floatfmt, headers, tabular_data
def _get_sell_trade_entry( def _get_sell_trade_entry(
self, pair: str, buy_row: DataFrame, self, pair: str, buy_row: DataFrame,
@ -127,7 +134,9 @@ class Backtesting(object):
pair, pair,
trade.calc_profit_percent(rate=sell_row.close), trade.calc_profit_percent(rate=sell_row.close),
trade.calc_profit(rate=sell_row.close), trade.calc_profit(rate=sell_row.close),
(sell_row.date - buy_row.date).seconds // 60 (sell_row.date - buy_row.date).seconds // 60,
buy_row.date,
sell_row.date
), \ ), \
sell_row.date sell_row.date
return None return None
@ -193,6 +202,7 @@ class Backtesting(object):
if ret: if ret:
row2, trade_entry, next_date = ret row2, trade_entry, next_date = ret
lock_pair_until = next_date lock_pair_until = next_date
trades.append(trade_entry) trades.append(trade_entry)
if record: if record:
# Note, need to be json.dump friendly # Note, need to be json.dump friendly
@ -207,10 +217,12 @@ class Backtesting(object):
if record and record.find('trades') >= 0: if record and record.find('trades') >= 0:
logger.info('Dumping backtest results to %s', recordfilename) logger.info('Dumping backtest results to %s', recordfilename)
file_dump_json(recordfilename, records) file_dump_json(recordfilename, records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration'] file_dump_json('backtest-result.json', records)
labels = ['currency', 'profit_percent', 'profit_BTC', 'duration', 'entry', 'exit']
return DataFrame.from_records(trades, columns=labels) return DataFrame.from_records(trades, columns=labels)
def start(self) -> None: def start(self):
""" """
Run a backtesting end-to-end Run a backtesting end-to-end
:return: None :return: None
@ -237,6 +249,9 @@ class Backtesting(object):
timerange=timerange timerange=timerange
) )
if not data:
logger.critical("No data found. Terminating.")
return
# Ignore max_open_trades in backtesting, except realistic flag was passed # Ignore max_open_trades in backtesting, except realistic flag was passed
if self.config.get('realistic_simulation', False): if self.config.get('realistic_simulation', False):
max_open_trades = self.config['max_open_trades'] max_open_trades = self.config['max_open_trades']
@ -281,6 +296,10 @@ class Backtesting(object):
) )
) )
# return date for data storage
table = self.aggregate(data, results)
return (results, table)
def setup_configuration(args: Namespace) -> Dict[str, Any]: def setup_configuration(args: Namespace) -> Dict[str, Any]:
""" """

View File

@ -154,6 +154,12 @@ class Trade(_DECL_BASE):
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)
# absolute value of the stop loss
stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the initial stop loss
initial_stop_loss = Column(Float, nullable=True, default=0.0)
# absolute value of the highest reached price
max_rate = Column(Float, nullable=True, default=0.0)
def __repr__(self): def __repr__(self):
return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format( return 'Trade(id={}, pair={}, amount={:.8f}, open_rate={:.8f}, open_since={})'.format(
@ -164,6 +170,50 @@ class Trade(_DECL_BASE):
arrow.get(self.open_date).humanize() if self.is_open else 'closed' arrow.get(self.open_date).humanize() if self.is_open else 'closed'
) )
def adjust_stop_loss(self, current_price, stoploss):
"""
this adjusts the stop loss to it's most recently observed
setting
:param current_price:
:param stoploss:
:return:
"""
new_loss = Decimal(current_price * (1 - abs(stoploss)))
# keeping track of the highest observed rate for this trade
if self.max_rate is None:
self.max_rate = current_price
else:
if current_price > self.max_rate:
self.max_rate = current_price
# no stop loss assigned yet
if self.stop_loss is None or self.stop_loss == 0:
logger.debug("assigning new stop loss")
self.stop_loss = new_loss
self.initial_stop_loss = new_loss
# evaluate if the stop loss needs to be updated
else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
self.stop_loss = new_loss
logger.debug("adjusted stop loss")
else:
logger.debug("keeping current stop loss")
logger.debug(
"{} - current price {:.8f}, bought at {:.8f} and calculated "
"stop loss is at: {:.8f} initial stop at {:.8f}. trailing stop loss saved us: {:.8f} "
"and max observed rate was {:.8f}".format(
self.pair, current_price, self.open_rate,
self.initial_stop_loss,
self.stop_loss, float(self.stop_loss) - float(self.initial_stop_loss),
self.max_rate
))
def update(self, order: Dict) -> None: def update(self, order: Dict) -> None:
""" """
Updates this entity with amount and actual open/close rates. Updates this entity with amount and actual open/close rates.

View File

@ -2,24 +2,34 @@
This module contains class to define a RPC communications This module contains class to define a RPC communications
""" """
import logging import logging
from abc import abstractmethod
from datetime import datetime, timedelta, date from datetime import datetime, timedelta, date
from decimal import Decimal from decimal import Decimal
from typing import Dict, Tuple, Any from typing import Dict, Tuple, Any, List
import arrow import arrow
import sqlalchemy as sql import sqlalchemy as sql
from pandas import DataFrame
from numpy import mean, nan_to_num from numpy import mean, nan_to_num
from pandas import DataFrame
from freqtrade import exchange from freqtrade import exchange
from freqtrade.misc import shorten_date from freqtrade.misc import shorten_date
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.state import State from freqtrade.state import State
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class RPCException(Exception):
"""
Should be raised with a rpc-formatted message in an _rpc_* method
if the required state is wrong, i.e.:
raise RPCException('*Status:* `no active trade`')
"""
pass
class RPC(object): class RPC(object):
""" """
RPC class can be used to have extra feature, like bot data, and access to DB data RPC class can be used to have extra feature, like bot data, and access to DB data
@ -30,20 +40,32 @@ class RPC(object):
:param freqtrade: Instance of a freqtrade bot :param freqtrade: Instance of a freqtrade bot
:return: None :return: None
""" """
self.freqtrade = freqtrade self._freqtrade = freqtrade
def rpc_trade_status(self) -> Tuple[bool, Any]: @abstractmethod
def cleanup(self) -> None:
""" Cleanup pending module resources """
@property
@abstractmethod
def name(self) -> str:
""" Returns the lowercase name of this module """
@abstractmethod
def send_msg(self, msg: str) -> None:
""" Sends a message to all registered rpc modules """
def _rpc_trade_status(self) -> List[str]:
""" """
Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is Below follows the RPC backend it is prefixed with rpc_ to raise awareness that it is
a remotely exposed function a remotely exposed function
:return:
""" """
# Fetch open trade # Fetch open trade
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active trade`' raise RPCException('*Status:* `no active trade`')
else: else:
result = [] result = []
for trade in trades: for trade in trades:
@ -64,6 +86,7 @@ class RPC(object):
"*Close Rate:* `{close_rate}`\n" \ "*Close Rate:* `{close_rate}`\n" \
"*Current Rate:* `{current_rate:.8f}`\n" \ "*Current Rate:* `{current_rate:.8f}`\n" \
"*Close Profit:* `{close_profit}`\n" \ "*Close Profit:* `{close_profit}`\n" \
"*Stake Value:* `{stake_value}`\n" \
"*Current Profit:* `{current_profit:.2f}%`\n" \ "*Current Profit:* `{current_profit:.2f}%`\n" \
"*Open Order:* `{open_order}`"\ "*Open Order:* `{open_order}`"\
.format( .format(
@ -76,20 +99,21 @@ class RPC(object):
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,
stake_value=round(current_rate * trade.amount, 8),
current_profit=round(current_profit * 100, 2), current_profit=round(current_profit * 100, 2),
open_order='({} {} rem={:.8f})'.format( open_order='({} {} rem={:.8f})'.format(
order['type'], order['side'], order['remaining'] order['type'], order['side'], order['remaining']
) if order else None, ) if order else None,
) )
result.append(message) result.append(message)
return False, result return result
def rpc_status_table(self) -> Tuple[bool, Any]: def _rpc_status_table(self) -> DataFrame:
trades = Trade.query.filter(Trade.is_open.is_(True)).all() trades = Trade.query.filter(Trade.is_open.is_(True)).all()
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '*Status:* `trader is not running`' raise RPCException('*Status:* `trader is not running`')
elif not trades: elif not trades:
return True, '*Status:* `no active order`' raise RPCException('*Status:* `no active order`')
else: else:
trades_list = [] trades_list = []
for trade in trades: for trade in trades:
@ -99,28 +123,25 @@ class RPC(object):
trade.id, trade.id,
trade.pair, trade.pair,
shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)), shorten_date(arrow.get(trade.open_date).humanize(only_distance=True)),
'{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)) '{:.2f}%'.format(100 * trade.calc_profit_percent(current_rate)),
'{:.8f}'.format(trade.amount * current_rate)
]) ])
columns = ['ID', 'Pair', 'Since', 'Profit'] columns = ['ID', 'Pair', 'Since', 'Profit', 'Value']
df_statuses = DataFrame.from_records(trades_list, columns=columns) df_statuses = DataFrame.from_records(trades_list, columns=columns)
df_statuses = df_statuses.set_index(columns[0]) df_statuses = df_statuses.set_index(columns[0])
# The style used throughout is to return a tuple return df_statuses
# consisting of (error_occured?, result)
# Another approach would be to just return the
# result, or raise error
return False, df_statuses
def rpc_daily_profit( def _rpc_daily_profit(
self, timescale: int, self, timescale: int,
stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: stake_currency: str, fiat_display_currency: str) -> List[List[Any]]:
today = datetime.utcnow().date() today = datetime.utcnow().date()
profit_days: Dict[date, Dict] = {} profit_days: Dict[date, Dict] = {}
if not (isinstance(timescale, int) and timescale > 0): if not (isinstance(timescale, int) and timescale > 0):
return True, '*Daily [n]:* `must be an integer greater than 0`' raise RPCException('*Daily [n]:* `must be an integer greater than 0`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
for day in range(0, timescale): for day in range(0, timescale):
profitday = today - timedelta(days=day) profitday = today - timedelta(days=day)
trades = Trade.query \ trades = Trade.query \
@ -135,7 +156,7 @@ class RPC(object):
'trades': len(trades) 'trades': len(trades)
} }
stats = [ return [
[ [
key, key,
'{value:.8f} {symbol}'.format( '{value:.8f} {symbol}'.format(
@ -157,13 +178,10 @@ class RPC(object):
] ]
for key, value in profit_days.items() for key, value in profit_days.items()
] ]
return False, stats
def rpc_trade_statistics( def _rpc_trade_statistics(
self, stake_currency: str, fiat_display_currency: str) -> Tuple[bool, Any]: self, stake_currency: str, fiat_display_currency: str) -> Dict[str, Any]:
""" """ Returns cumulative profit statistics """
:return: cumulative profit statistics.
"""
trades = Trade.query.order_by(Trade.id).all() trades = Trade.query.order_by(Trade.id).all()
profit_all_coin = [] profit_all_coin = []
@ -201,13 +219,13 @@ class RPC(object):
.order_by(sql.text('profit_sum DESC')).first() .order_by(sql.text('profit_sum DESC')).first()
if not best_pair: if not best_pair:
return True, '*Status:* `no closed trade`' raise RPCException('*Status:* `no closed trade`')
bp_pair, bp_rate = best_pair bp_pair, bp_rate = best_pair
# FIX: we want to keep fiatconverter in a state/environment, # FIX: we want to keep fiatconverter in a state/environment,
# doing this will utilize its caching functionallity, instead we reinitialize it here # doing this will utilize its caching functionallity, instead we reinitialize it here
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
# Prepare data to display # Prepare data to display
profit_closed_coin = round(sum(profit_closed_coin), 8) profit_closed_coin = round(sum(profit_closed_coin), 8)
profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2) profit_closed_percent = round(nan_to_num(mean(profit_closed_percent)) * 100, 2)
@ -224,35 +242,29 @@ class RPC(object):
fiat_display_currency fiat_display_currency
) )
num = float(len(durations) or 1) num = float(len(durations) or 1)
return ( return {
False, 'profit_closed_coin': profit_closed_coin,
{ 'profit_closed_percent': profit_closed_percent,
'profit_closed_coin': profit_closed_coin, 'profit_closed_fiat': profit_closed_fiat,
'profit_closed_percent': profit_closed_percent, 'profit_all_coin': profit_all_coin,
'profit_closed_fiat': profit_closed_fiat, 'profit_all_percent': profit_all_percent,
'profit_all_coin': profit_all_coin, 'profit_all_fiat': profit_all_fiat,
'profit_all_percent': profit_all_percent, 'trade_count': len(trades),
'profit_all_fiat': profit_all_fiat, 'first_trade_date': arrow.get(trades[0].open_date).humanize(),
'trade_count': len(trades), 'latest_trade_date': arrow.get(trades[-1].open_date).humanize(),
'first_trade_date': arrow.get(trades[0].open_date).humanize(), 'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0],
'latest_trade_date': arrow.get(trades[-1].open_date).humanize(), 'best_pair': bp_pair,
'avg_duration': str(timedelta(seconds=sum(durations) / num)).split('.')[0], 'best_rate': round(bp_rate * 100, 2),
'best_pair': bp_pair, }
'best_rate': round(bp_rate * 100, 2)
}
)
def rpc_balance(self, fiat_display_currency: str) -> Tuple[bool, Any]: def _rpc_balance(self, fiat_display_currency: str) -> Tuple[List[Dict], float, str, float]:
""" """ Returns current account balance per crypto """
:return: current account balance per crypto
"""
output = [] output = []
total = 0.0 total = 0.0
for coin, balance in exchange.get_balances().items(): for coin, balance in exchange.get_balances().items():
if not balance['total']: if not balance['total']:
continue continue
rate = None
if coin == 'BTC': if coin == 'BTC':
rate = 1.0 rate = 1.0
else: else:
@ -272,39 +284,39 @@ class RPC(object):
} }
) )
if total == 0.0: if total == 0.0:
return True, '`All balances are zero.`' raise RPCException('`All balances are zero.`')
fiat = self.freqtrade.fiat_converter fiat = self._freqtrade.fiat_converter
symbol = fiat_display_currency symbol = fiat_display_currency
value = fiat.convert_amount(total, 'BTC', symbol) value = fiat.convert_amount(total, 'BTC', symbol)
return False, (output, total, symbol, value) return output, total, symbol, value
def rpc_start(self) -> Tuple[bool, str]: def _rpc_start(self) -> str:
""" """ Handler for start """
Handler for start. if self._freqtrade.state == State.RUNNING:
""" return '*Status:* `already running`'
if self.freqtrade.state == State.RUNNING:
return True, '*Status:* `already running`'
self.freqtrade.state = State.RUNNING self._freqtrade.state = State.RUNNING
return False, '`Starting trader ...`' return '`Starting trader ...`'
def rpc_stop(self) -> Tuple[bool, str]: def _rpc_stop(self) -> str:
""" """ Handler for stop """
Handler for stop. if self._freqtrade.state == State.RUNNING:
""" self._freqtrade.state = State.STOPPED
if self.freqtrade.state == State.RUNNING: return '`Stopping trader ...`'
self.freqtrade.state = State.STOPPED
return False, '`Stopping trader ...`'
return True, '*Status:* `already stopped`' return '*Status:* `already stopped`'
def _rpc_reload_conf(self) -> str:
""" Handler for reload_conf. """
self._freqtrade.state = State.RELOAD_CONF
return '*Status:* `Reloading config ...`'
# FIX: no test for this!!!! # FIX: no test for this!!!!
def rpc_forcesell(self, trade_id) -> Tuple[bool, Any]: def _rpc_forcesell(self, trade_id) -> None:
""" """
Handler for forcesell <id>. Handler for forcesell <id>.
Sells the given trade at current price Sells the given trade at current price
:return: error or None
""" """
def _exec_forcesell(trade: Trade) -> None: def _exec_forcesell(trade: Trade) -> None:
# Check if there is there is an open order # Check if there is there is an open order
@ -330,17 +342,17 @@ class RPC(object):
# Get current rate and execute sell # Get current rate and execute sell
current_rate = exchange.get_ticker(trade.pair, False)['bid'] current_rate = exchange.get_ticker(trade.pair, False)['bid']
self.freqtrade.execute_sell(trade, current_rate) self._freqtrade.execute_sell(trade, current_rate)
# ---- EOF def _exec_forcesell ---- # ---- EOF def _exec_forcesell ----
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
if trade_id == 'all': if trade_id == 'all':
# Execute sell for all open orders # Execute sell for all open orders
for trade in Trade.query.filter(Trade.is_open.is_(True)).all(): for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
_exec_forcesell(trade) _exec_forcesell(trade)
return False, '' return
# Query for trade # Query for trade
trade = Trade.query.filter( trade = Trade.query.filter(
@ -351,19 +363,18 @@ class RPC(object):
).first() ).first()
if not trade: if not trade:
logger.warning('forcesell: Invalid argument received') logger.warning('forcesell: Invalid argument received')
return True, 'Invalid argument.' raise RPCException('Invalid argument.')
_exec_forcesell(trade) _exec_forcesell(trade)
Trade.session.flush() Trade.session.flush()
return False, ''
def rpc_performance(self) -> Tuple[bool, Any]: def _rpc_performance(self) -> List[Dict]:
""" """
Handler for performance. Handler for performance.
Shows a performance statistic from finished trades Shows a performance statistic from finished trades
""" """
if self.freqtrade.state != State.RUNNING: if self._freqtrade.state != State.RUNNING:
return True, '`trader is not running`' raise RPCException('`trader is not running`')
pair_rates = Trade.session.query(Trade.pair, pair_rates = Trade.session.query(Trade.pair,
sql.func.sum(Trade.close_profit).label('profit_sum'), sql.func.sum(Trade.close_profit).label('profit_sum'),
@ -372,19 +383,14 @@ class RPC(object):
.group_by(Trade.pair) \ .group_by(Trade.pair) \
.order_by(sql.text('profit_sum DESC')) \ .order_by(sql.text('profit_sum DESC')) \
.all() .all()
trades = [] return [
for (pair, rate, count) in pair_rates: {'pair': pair, 'profit': round(rate * 100, 2), 'count': count}
trades.append({'pair': pair, 'profit': round(rate * 100, 2), 'count': count}) for pair, rate, count in pair_rates
]
return False, trades def _rpc_count(self) -> List[Trade]:
""" Returns the number of trades running """
if self._freqtrade.state != State.RUNNING:
raise RPCException('`trader is not running`')
def rpc_count(self) -> Tuple[bool, Any]: return Trade.query.filter(Trade.is_open.is_(True)).all()
"""
Returns the number of trades running
:return: None
"""
if self.freqtrade.state != State.RUNNING:
return True, '`trader is not running`'
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
return False, trades

View File

@ -1,11 +1,10 @@
""" """
This module contains class to manage RPC communications (Telegram, Slack, ...) This module contains class to manage RPC communications (Telegram, Slack, ...)
""" """
from typing import Any, List
import logging import logging
from typing import List
from freqtrade.rpc.telegram import Telegram from freqtrade.rpc.rpc import RPC
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -15,36 +14,23 @@ class RPCManager(object):
Class to manage RPC objects (Telegram, Slack, ...) Class to manage RPC objects (Telegram, Slack, ...)
""" """
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """ Initializes all enabled rpc modules """
Initializes all enabled rpc modules self.registered_modules: List[RPC] = []
:param config: config to use
:return: None
"""
self.freqtrade = freqtrade
self.registered_modules: List[str] = [] # Enable telegram
self.telegram: Any = None if freqtrade.config['telegram'].get('enabled', False):
self._init()
def _init(self) -> None:
"""
Init RPC modules
:return:
"""
if self.freqtrade.config['telegram'].get('enabled', False):
logger.info('Enabling rpc.telegram ...') logger.info('Enabling rpc.telegram ...')
self.registered_modules.append('telegram') from freqtrade.rpc.telegram import Telegram
self.telegram = Telegram(self.freqtrade) self.registered_modules.append(Telegram(freqtrade))
def cleanup(self) -> None: def cleanup(self) -> None:
""" """ Stops all enabled rpc modules """
Stops all enabled rpc modules logger.info('Cleaning up rpc modules ...')
:return: None while self.registered_modules:
""" mod = self.registered_modules.pop()
if 'telegram' in self.registered_modules: logger.debug('Cleaning up rpc.%s ...', mod.name)
logger.info('Cleaning up rpc.telegram ...') mod.cleanup()
self.registered_modules.remove('telegram') del mod
self.telegram.cleanup()
def send_msg(self, msg: str) -> None: def send_msg(self, msg: str) -> None:
""" """
@ -52,6 +38,7 @@ class RPCManager(object):
:param msg: message :param msg: message
:return: None :return: None
""" """
logger.info(msg) logger.info('Sending rpc message: %s', msg)
if 'telegram' in self.registered_modules: for mod in self.registered_modules:
self.telegram.send_msg(msg) logger.debug('Forwarding message to rpc.%s', mod.name)
mod.send_msg(msg)

View File

@ -12,11 +12,12 @@ from telegram.error import NetworkError, TelegramError
from telegram.ext import CommandHandler, Updater from telegram.ext import CommandHandler, Updater
from freqtrade.__init__ import __version__ from freqtrade.__init__ import __version__
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.debug('Included module rpc.telegram ...')
def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]: def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Callable[..., Any]:
""" """
@ -25,9 +26,7 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
:return: decorated function :return: decorated function
""" """
def wrapper(self, *args, **kwargs): def wrapper(self, *args, **kwargs):
""" """ Decorator logic """
Decorator logic
"""
update = kwargs.get('update') or args[1] update = kwargs.get('update') or args[1]
# Reject unauthorized messages # Reject unauthorized messages
@ -54,9 +53,12 @@ def authorized_only(command_handler: Callable[[Any, Bot, Update], None]) -> Call
class Telegram(RPC): class Telegram(RPC):
""" """ This class handles all telegram communication """
Telegram, this class send messages to Telegram
""" @property
def name(self) -> str:
return "telegram"
def __init__(self, freqtrade) -> None: def __init__(self, freqtrade) -> None:
""" """
Init the Telegram call, and init the super class RPC Init the Telegram call, and init the super class RPC
@ -74,12 +76,7 @@ class Telegram(RPC):
Initializes this module with the given config, Initializes this module with the given config,
registers all known command handlers registers all known command handlers
and starts polling for message updates and starts polling for message updates
:param config: config to use
:return: None
""" """
if not self.is_enabled():
return
self._updater = Updater(token=self._config['telegram']['token'], workers=0) self._updater = Updater(token=self._config['telegram']['token'], workers=0)
# Register command handler and start telegram message polling # Register command handler and start telegram message polling
@ -93,6 +90,7 @@ class Telegram(RPC):
CommandHandler('performance', self._performance), CommandHandler('performance', self._performance),
CommandHandler('daily', self._daily), CommandHandler('daily', self._daily),
CommandHandler('count', self._count), CommandHandler('count', self._count),
CommandHandler('reload_conf', self._reload_conf),
CommandHandler('help', self._help), CommandHandler('help', self._help),
CommandHandler('version', self._version), CommandHandler('version', self._version),
] ]
@ -114,16 +112,11 @@ class Telegram(RPC):
Stops all running telegram threads. Stops all running telegram threads.
:return: None :return: None
""" """
if not self.is_enabled():
return
self._updater.stop() self._updater.stop()
def is_enabled(self) -> bool: def send_msg(self, msg: str) -> None:
""" """ Send a message to telegram channel """
Returns True if the telegram module is activated, False otherwise self._send_msg(msg)
"""
return bool(self._config.get('telegram', {}).get('enabled', False))
@authorized_only @authorized_only
def _status(self, bot: Bot, update: Update) -> None: def _status(self, bot: Bot, update: Update) -> None:
@ -142,13 +135,11 @@ class Telegram(RPC):
self._status_table(bot, update) self._status_table(bot, update)
return return
# Fetch open trade try:
(error, trades) = self.rpc_trade_status() for trade_msg in self._rpc_trade_status():
if error: self._send_msg(trade_msg, bot=bot)
self.send_msg(trades, bot=bot) except RPCException as e:
else: self._send_msg(str(e), bot=bot)
for trademsg in trades:
self.send_msg(trademsg, bot=bot)
@authorized_only @authorized_only
def _status_table(self, bot: Bot, update: Update) -> None: def _status_table(self, bot: Bot, update: Update) -> None:
@ -159,15 +150,12 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
# Fetch open trade try:
(err, df_statuses) = self.rpc_status_table() df_statuses = self._rpc_status_table()
if err:
self.send_msg(df_statuses, bot=bot)
else:
message = tabulate(df_statuses, headers='keys', tablefmt='simple') message = tabulate(df_statuses, headers='keys', tablefmt='simple')
message = "<pre>{}</pre>".format(message) self._send_msg("<pre>{}</pre>".format(message), parse_mode=ParseMode.HTML)
except RPCException as e:
self.send_msg(message, parse_mode=ParseMode.HTML) self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _daily(self, bot: Bot, update: Update) -> None: def _daily(self, bot: Bot, update: Update) -> None:
@ -182,14 +170,12 @@ class Telegram(RPC):
timescale = int(update.message.text.replace('/daily', '').strip()) timescale = int(update.message.text.replace('/daily', '').strip())
except (TypeError, ValueError): except (TypeError, ValueError):
timescale = 7 timescale = 7
(error, stats) = self.rpc_daily_profit( try:
timescale, stats = self._rpc_daily_profit(
self._config['stake_currency'], timescale,
self._config['fiat_display_currency'] self._config['stake_currency'],
) self._config['fiat_display_currency']
if error: )
self.send_msg(stats, bot=bot)
else:
stats = tabulate(stats, stats = tabulate(stats,
headers=[ headers=[
'Day', 'Day',
@ -198,11 +184,10 @@ class Telegram(RPC):
], ],
tablefmt='simple') tablefmt='simple')
message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\ message = '<b>Daily Profit over the last {} days</b>:\n<pre>{}</pre>'\
.format( .format(timescale, stats)
timescale, self._send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
stats except RPCException as e:
) self._send_msg(str(e), bot=bot)
self.send_msg(message, bot=bot, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _profit(self, bot: Bot, update: Update) -> None: def _profit(self, bot: Bot, update: Update) -> None:
@ -213,67 +198,63 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, stats) = self.rpc_trade_statistics( try:
self._config['stake_currency'], stats = self._rpc_trade_statistics(
self._config['fiat_display_currency'] self._config['stake_currency'],
) self._config['fiat_display_currency'])
if error:
self.send_msg(stats, bot=bot)
return
# Message to display # Message to display
markdown_msg = "*ROI:* Close trades\n" \ markdown_msg = "*ROI:* Close trades\n" \
"∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \ "∙ `{profit_closed_coin:.8f} {coin} ({profit_closed_percent:.2f}%)`\n" \
"∙ `{profit_closed_fiat:.3f} {fiat}`\n" \ "∙ `{profit_closed_fiat:.3f} {fiat}`\n" \
"*ROI:* All trades\n" \ "*ROI:* All trades\n" \
"∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \ "∙ `{profit_all_coin:.8f} {coin} ({profit_all_percent:.2f}%)`\n" \
"∙ `{profit_all_fiat:.3f} {fiat}`\n" \ "∙ `{profit_all_fiat:.3f} {fiat}`\n" \
"*Total Trade Count:* `{trade_count}`\n" \ "*Total Trade Count:* `{trade_count}`\n" \
"*First Trade opened:* `{first_trade_date}`\n" \ "*First Trade opened:* `{first_trade_date}`\n" \
"*Latest Trade opened:* `{latest_trade_date}`\n" \ "*Latest Trade opened:* `{latest_trade_date}`\n" \
"*Avg. Duration:* `{avg_duration}`\n" \ "*Avg. Duration:* `{avg_duration}`\n" \
"*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\ "*Best Performing:* `{best_pair}: {best_rate:.2f}%`"\
.format( .format(
coin=self._config['stake_currency'], coin=self._config['stake_currency'],
fiat=self._config['fiat_display_currency'], fiat=self._config['fiat_display_currency'],
profit_closed_coin=stats['profit_closed_coin'], profit_closed_coin=stats['profit_closed_coin'],
profit_closed_percent=stats['profit_closed_percent'], profit_closed_percent=stats['profit_closed_percent'],
profit_closed_fiat=stats['profit_closed_fiat'], profit_closed_fiat=stats['profit_closed_fiat'],
profit_all_coin=stats['profit_all_coin'], profit_all_coin=stats['profit_all_coin'],
profit_all_percent=stats['profit_all_percent'], profit_all_percent=stats['profit_all_percent'],
profit_all_fiat=stats['profit_all_fiat'], profit_all_fiat=stats['profit_all_fiat'],
trade_count=stats['trade_count'], trade_count=stats['trade_count'],
first_trade_date=stats['first_trade_date'], first_trade_date=stats['first_trade_date'],
latest_trade_date=stats['latest_trade_date'], latest_trade_date=stats['latest_trade_date'],
avg_duration=stats['avg_duration'], avg_duration=stats['avg_duration'],
best_pair=stats['best_pair'], best_pair=stats['best_pair'],
best_rate=stats['best_rate'] best_rate=stats['best_rate']
) )
self.send_msg(markdown_msg, bot=bot) self._send_msg(markdown_msg, bot=bot)
except RPCException as e:
self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _balance(self, bot: Bot, update: Update) -> None: def _balance(self, bot: Bot, update: Update) -> None:
""" """ Handler for /balance """
Handler for /balance try:
""" currencys, total, symbol, value = \
(error, result) = self.rpc_balance(self._config['fiat_display_currency']) self._rpc_balance(self._config['fiat_display_currency'])
if error: output = ''
self.send_msg('`All balances are zero.`') for currency in currencys:
return output += "*{currency}:*\n" \
"\t`Available: {available: .8f}`\n" \
"\t`Balance: {balance: .8f}`\n" \
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
(currencys, total, symbol, value) = result output += "\n*Estimated Value*:\n" \
output = '' "\t`BTC: {0: .8f}`\n" \
for currency in currencys: "\t`{1}: {2: .2f}`\n".format(total, symbol, value)
output += "*{currency}:*\n" \ self._send_msg(output, bot=bot)
"\t`Available: {available: .8f}`\n" \ except RPCException as e:
"\t`Balance: {balance: .8f}`\n" \ self._send_msg(str(e), bot=bot)
"\t`Pending: {pending: .8f}`\n" \
"\t`Est. BTC: {est_btc: .8f}`\n".format(**currency)
output += "\n*Estimated Value*:\n" \
"\t`BTC: {0: .8f}`\n" \
"\t`{1}: {2: .2f}`\n".format(total, symbol, value)
self.send_msg(output)
@authorized_only @authorized_only
def _start(self, bot: Bot, update: Update) -> None: def _start(self, bot: Bot, update: Update) -> None:
@ -284,9 +265,8 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_start() msg = self._rpc_start()
if error: self._send_msg(msg, bot=bot)
self.send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _stop(self, bot: Bot, update: Update) -> None: def _stop(self, bot: Bot, update: Update) -> None:
@ -297,8 +277,20 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, msg) = self.rpc_stop() msg = self._rpc_stop()
self.send_msg(msg, bot=bot) self._send_msg(msg, bot=bot)
@authorized_only
def _reload_conf(self, bot: Bot, update: Update) -> None:
"""
Handler for /reload_conf.
Triggers a config file reload
:param bot: telegram bot
:param update: message update
:return: None
"""
msg = self._rpc_reload_conf()
self._send_msg(msg, bot=bot)
@authorized_only @authorized_only
def _forcesell(self, bot: Bot, update: Update) -> None: def _forcesell(self, bot: Bot, update: Update) -> None:
@ -311,10 +303,10 @@ class Telegram(RPC):
""" """
trade_id = update.message.text.replace('/forcesell', '').strip() trade_id = update.message.text.replace('/forcesell', '').strip()
(error, message) = self.rpc_forcesell(trade_id) try:
if error: self._rpc_forcesell(trade_id)
self.send_msg(message, bot=bot) except RPCException as e:
return self._send_msg(str(e), bot=bot)
@authorized_only @authorized_only
def _performance(self, bot: Bot, update: Update) -> None: def _performance(self, bot: Bot, update: Update) -> None:
@ -325,19 +317,18 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_performance() try:
if error: trades = self._rpc_performance()
self.send_msg(trades, bot=bot) stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format(
return index=i + 1,
pair=trade['pair'],
stats = '\n'.join('{index}.\t<code>{pair}\t{profit:.2f}% ({count})</code>'.format( profit=trade['profit'],
index=i + 1, count=trade['count']
pair=trade['pair'], ) for i, trade in enumerate(trades))
profit=trade['profit'], message = '<b>Performance:</b>\n{}'.format(stats)
count=trade['count'] self._send_msg(message, parse_mode=ParseMode.HTML)
) for i, trade in enumerate(trades)) except RPCException as e:
message = '<b>Performance:</b>\n{}'.format(stats) self._send_msg(str(e), bot=bot)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _count(self, bot: Bot, update: Update) -> None: def _count(self, bot: Bot, update: Update) -> None:
@ -348,19 +339,18 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
(error, trades) = self.rpc_count() try:
if error: trades = self._rpc_count()
self.send_msg(trades, bot=bot) message = tabulate({
return 'current': [len(trades)],
'max': [self._config['max_open_trades']],
message = tabulate({ 'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)]
'current': [len(trades)], }, headers=['current', 'max', 'total stake'], tablefmt='simple')
'max': [self._config['max_open_trades']], message = "<pre>{}</pre>".format(message)
'total stake': [sum((trade.open_rate * trade.amount) for trade in trades)] logger.debug(message)
}, headers=['current', 'max', 'total stake'], tablefmt='simple') self._send_msg(message, parse_mode=ParseMode.HTML)
message = "<pre>{}</pre>".format(message) except RPCException as e:
logger.debug(message) self._send_msg(str(e), bot=bot)
self.send_msg(message, parse_mode=ParseMode.HTML)
@authorized_only @authorized_only
def _help(self, bot: Bot, update: Update) -> None: def _help(self, bot: Bot, update: Update) -> None:
@ -386,7 +376,7 @@ class Telegram(RPC):
"*/help:* `This help message`\n" \ "*/help:* `This help message`\n" \
"*/version:* `Show version`" "*/version:* `Show version`"
self.send_msg(message, bot=bot) self._send_msg(message, bot=bot)
@authorized_only @authorized_only
def _version(self, bot: Bot, update: Update) -> None: def _version(self, bot: Bot, update: Update) -> None:
@ -397,10 +387,10 @@ class Telegram(RPC):
:param update: message update :param update: message update
:return: None :return: None
""" """
self.send_msg('*Version:* `{}`'.format(__version__), bot=bot) self._send_msg('*Version:* `{}`'.format(__version__), bot=bot)
def send_msg(self, msg: str, bot: Bot = None, def _send_msg(self, msg: str, bot: Bot = None,
parse_mode: ParseMode = ParseMode.MARKDOWN) -> None: parse_mode: ParseMode = ParseMode.MARKDOWN) -> None:
""" """
Send given markdown message Send given markdown message
:param msg: message :param msg: message
@ -408,9 +398,6 @@ class Telegram(RPC):
:param parse_mode: telegram parse mode :param parse_mode: telegram parse mode
:return: None :return: None
""" """
if not self.is_enabled():
return
bot = bot or self._updater.bot bot = bot or self._updater.bot
keyboard = [['/daily', '/profit', '/balance'], keyboard = [['/daily', '/profit', '/balance'],

View File

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

View File

@ -6,13 +6,17 @@ This module load custom strategies
import importlib.util import importlib.util
import inspect import inspect
import logging import logging
import os from base64 import urlsafe_b64decode
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, Dict, Type from typing import Optional, Dict, Type
from freqtrade import constants from freqtrade import constants
from freqtrade.strategy.interface import IStrategy from freqtrade.strategy.interface import IStrategy
import tempfile
from urllib.parse import urlparse
import os
import requests
from pathlib import Path
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -61,6 +65,13 @@ class StrategyResolver(object):
key=lambda t: t[0])) key=lambda t: t[0]))
self.strategy.stoploss = float(self.strategy.stoploss) self.strategy.stoploss = float(self.strategy.stoploss)
def compile(self, strategy_name: str, strategy_content: str) -> Optional[IStrategy]:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
temp.joinpath(strategy_name + ".py").write_text(strategy_content)
temp.joinpath("__init__.py").touch()
return self._load_strategy(strategy_name, temp.absolute())
def _load_strategy( def _load_strategy(
self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy: self, strategy_name: str, extra_dir: Optional[str] = None) -> IStrategy:
""" """
@ -79,6 +90,48 @@ class StrategyResolver(object):
# Add extra strategy directory on top of search paths # Add extra strategy directory on top of search paths
abs_paths.insert(0, extra_dir) abs_paths.insert(0, extra_dir)
# check if the given strategy is provided as name, value pair
# where the value is the strategy encoded in base 64
if ":" in strategy_name and "http" not in strategy_name:
strat = strategy_name.split(":")
if len(strat) == 2:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
name = strat[0] + ".py"
temp.joinpath(name).write_text(urlsafe_b64decode(strat[1]).decode('utf-8'))
temp.joinpath("__init__.py").touch()
strategy_name = os.path.splitext(name)[0]
# register temp path with the bot
abs_paths.insert(0, temp.absolute())
# check if given strategy matches an url
else:
try:
logger.debug("requesting remote strategy from {}".format(strategy_name))
resp = requests.get(strategy_name, stream=True)
if resp.status_code == 200:
temp = Path(tempfile.mkdtemp("freq", "strategy"))
if strategy_name.endswith("/code"):
strategy_name = strategy_name.replace("/code", "")
name = os.path.basename(urlparse(strategy_name).path)
temp.joinpath("{}.py".format(name)).write_text(resp.text)
temp.joinpath("__init__.py").touch()
strategy_name = os.path.splitext(name)[0]
print("stored downloaded stat at: {}".format(temp))
# register temp path with the bot
abs_paths.insert(0, temp.absolute())
except requests.RequestException:
logger.debug("received error trying to fetch strategy remotely, carry on!")
for path in abs_paths: for path in abs_paths:
strategy = self._search_strategy(path, strategy_name) strategy = self._search_strategy(path, strategy_name)
if strategy: if strategy:

View File

@ -15,6 +15,10 @@ from freqtrade.analyze import Analyze
from freqtrade import constants from freqtrade import constants
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
import moto
import boto3
import os
logging.getLogger('').setLevel(logging.INFO) logging.getLogger('').setLevel(logging.INFO)
@ -531,6 +535,7 @@ def result():
with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file: with open('freqtrade/tests/testdata/UNITTEST_BTC-1m.json') as data_file:
return Analyze.parse_ticker_dataframe(json.load(data_file)) return Analyze.parse_ticker_dataframe(json.load(data_file))
# FIX: # FIX:
# Create an fixture/function # Create an fixture/function
# that inserts a trade of some type and open-status # that inserts a trade of some type and open-status

View File

@ -84,6 +84,7 @@ def load_data_test(what):
def simple_backtest(config, contour, num_results, mocker) -> None: def simple_backtest(config, contour, num_results, mocker) -> None:
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True)) mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
backtesting = Backtesting(config) backtesting = Backtesting(config)
data = load_data_test(contour) data = load_data_test(contour)
@ -97,6 +98,7 @@ def simple_backtest(config, contour, num_results, mocker) -> None:
'realistic': True 'realistic': True
} }
) )
# results :: <class 'pandas.core.frame.DataFrame'> # results :: <class 'pandas.core.frame.DataFrame'>
assert len(results) == num_results assert len(results) == num_results
@ -363,14 +365,10 @@ def test_generate_text_table(default_conf, mocker):
) )
result_str = ( result_str = (
'| pair | buy count | avg profit % | ' """| pair | buy count | avg profit % | cum profit % | total profit BTC | avg duration | profit | loss |
'total profit BTC | avg duration | profit | loss |\n' |:--------|------------:|---------------:|---------------:|-------------------:|---------------:|---------:|-------:|
'|:--------|------------:|---------------:|' | ETH/BTC | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |
'-------------------:|---------------:|---------:|-------:|\n' | TOTAL | 2 | 15.00 | 30.00 | 0.60000000 | 20.0 | 2 | 0 |"""
'| ETH/BTC | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |\n'
'| TOTAL | 2 | 15.00 | '
'0.60000000 | 20.0 | 2 | 0 |'
) )
assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str assert backtesting._generate_text_table(data={'ETH/BTC': {}}, results=results) == result_str
@ -416,6 +414,40 @@ def test_backtesting_start(default_conf, mocker, caplog) -> None:
assert log_has(line, caplog.record_tuples) assert log_has(line, caplog.record_tuples)
def test_backtesting_start_no_data(default_conf, mocker, caplog) -> None:
"""
Test Backtesting.start() method if no data is found
"""
def get_timeframe(input1, input2):
return Arrow(2017, 11, 14, 21, 17), Arrow(2017, 11, 14, 22, 59)
mocker.patch('freqtrade.freqtradebot.Analyze', MagicMock())
mocker.patch('freqtrade.optimize.load_data', MagicMock(return_value={}))
mocker.patch('freqtrade.exchange.get_ticker_history')
mocker.patch('freqtrade.exchange.validate_pairs', MagicMock(return_value=True))
mocker.patch.multiple(
'freqtrade.optimize.backtesting.Backtesting',
backtest=MagicMock(),
_generate_text_table=MagicMock(return_value='1'),
get_timeframe=get_timeframe,
)
conf = deepcopy(default_conf)
conf['exchange']['pair_whitelist'] = ['UNITTEST/BTC']
conf['ticker_interval'] = "1m"
conf['live'] = False
conf['datadir'] = None
conf['export'] = None
conf['timerange'] = '20180101-20180102'
backtesting = Backtesting(conf)
backtesting.start()
# check the logs, that will contain the backtest result
assert log_has('No data found. Terminating.', caplog.record_tuples)
def test_backtest(default_conf, fee, mocker) -> None: def test_backtest(default_conf, fee, mocker) -> None:
""" """
Test Backtesting.backtest() method Test Backtesting.backtest() method
@ -562,6 +594,7 @@ def test_backtest_record(default_conf, fee, mocker):
results = backtesting.backtest(backtest_conf) results = backtesting.backtest(backtest_conf)
assert len(results) == 3 assert len(results) == 3
# Assert file_dump_json was only called once # Assert file_dump_json was only called once
print(names)
assert names == ['backtest-result.json'] assert names == ['backtest-result.json']
records = records[0] records = records[0]
# Ensure records are of correct type # Ensure records are of correct type

View File

@ -7,9 +7,11 @@ Unit test file for rpc/rpc.py
from datetime import datetime from datetime import datetime
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest
from freqtrade.freqtradebot import FreqtradeBot from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.rpc.rpc import RPC from freqtrade.rpc.rpc import RPC, RPCException
from freqtrade.state import State from freqtrade.state import State
from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap from freqtrade.tests.test_freqtradebot import patch_get_signal, patch_coinmarketcap
@ -29,7 +31,7 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -41,19 +43,16 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*trader is not running*'):
assert error rpc._rpc_trade_status()
assert 'trader is not running' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_trade_status() with pytest.raises(RPCException, match=r'.*no active trade*'):
assert error rpc._rpc_trade_status()
assert 'no active trade' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_trade_status() trades = rpc._rpc_trade_status()
assert not error trade = trades[0]
trade = result[0]
result_message = [ result_message = [
'*Trade ID:* `1`\n' '*Trade ID:* `1`\n'
@ -65,10 +64,11 @@ def test_rpc_trade_status(default_conf, ticker, fee, mocker) -> None:
'*Close Rate:* `None`\n' '*Close Rate:* `None`\n'
'*Current Rate:* `0.00001098`\n' '*Current Rate:* `0.00001098`\n'
'*Close Profit:* `None`\n' '*Close Profit:* `None`\n'
'*Stake Value:* `0.00099909`\n'
'*Current Profit:* `-0.59%`\n' '*Current Profit:* `-0.59%`\n'
'*Open Order:* `(limit buy rem=0.00000000)`' '*Open Order:* `(limit buy rem=0.00000000)`'
] ]
assert result == result_message assert trades == result_message
assert trade.find('[ETH/BTC]') >= 0 assert trade.find('[ETH/BTC]') >= 0
@ -78,7 +78,7 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -90,20 +90,19 @@ def test_rpc_status_table(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `trader is not running``*'):
assert error rpc._rpc_status_table()
assert '*Status:* `trader is not running`' in result
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_status_table() with pytest.raises(RPCException, match=r'.*\*Status:\* `no active order`*'):
assert error rpc._rpc_status_table()
assert '*Status:* `no active order`' in result
freqtradebot.create_trade() freqtradebot.create_trade()
(error, result) = rpc.rpc_status_table() result = rpc._rpc_status_table()
assert 'just now' in result['Since'].all() assert 'just now' in result['Since'].all()
assert 'ETH/BTC' in result['Pair'].all() assert 'ETH/BTC' in result['Pair'].all()
assert '-0.59%' in result['Profit'].all() assert '-0.59%' in result['Profit'].all()
assert 'Value' in result
def test_rpc_daily_profit(default_conf, update, ticker, fee, def test_rpc_daily_profit(default_conf, update, ticker, fee,
@ -113,7 +112,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker, value={'price_usd': 15000.0}) patch_coinmarketcap(mocker, value={'price_usd': 15000.0})
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -140,8 +139,7 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
# Try valid data # Try valid data
update.message.text = '/daily 2' update.message.text = '/daily 2'
(error, days) = rpc.rpc_daily_profit(7, stake_currency, fiat_display_currency) days = rpc._rpc_daily_profit(7, stake_currency, fiat_display_currency)
assert not error
assert len(days) == 7 assert len(days) == 7
for day in days: for day in days:
# [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD'] # [datetime.date(2018, 1, 11), '0.00000000 BTC', '0.000 USD']
@ -154,9 +152,8 @@ def test_rpc_daily_profit(default_conf, update, ticker, fee,
assert str(days[0][0]) == str(datetime.utcnow().date()) assert str(days[0][0]) == str(datetime.utcnow().date())
# Try invalid data # Try invalid data
(error, days) = rpc.rpc_daily_profit(0, stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*must be an integer greater than 0*'):
assert error rpc._rpc_daily_profit(0, stake_currency, fiat_display_currency)
assert days.find('must be an integer greater than 0') >= 0
def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee, def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
@ -170,7 +167,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -184,9 +181,8 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) with pytest.raises(RPCException, match=r'.*no closed trade*'):
assert error rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert stats.find('no closed trade') >= 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
@ -219,8 +215,7 @@ def test_rpc_trade_statistics(default_conf, ticker, ticker_sell_up, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05) assert prec_satoshi(stats['profit_closed_coin'], 6.217e-05)
assert prec_satoshi(stats['profit_closed_percent'], 6.2) assert prec_satoshi(stats['profit_closed_percent'], 6.2)
assert prec_satoshi(stats['profit_closed_fiat'], 0.93255) assert prec_satoshi(stats['profit_closed_fiat'], 0.93255)
@ -248,7 +243,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -281,8 +276,7 @@ def test_rpc_trade_statistics_closed(mocker, default_conf, ticker, fee,
for trade in Trade.query.order_by(Trade.id).all(): for trade in Trade.query.order_by(Trade.id).all():
trade.open_rate = None trade.open_rate = None
(error, stats) = rpc.rpc_trade_statistics(stake_currency, fiat_display_currency) stats = rpc._rpc_trade_statistics(stake_currency, fiat_display_currency)
assert not error
assert prec_satoshi(stats['profit_closed_coin'], 0) assert prec_satoshi(stats['profit_closed_coin'], 0)
assert prec_satoshi(stats['profit_closed_percent'], 0) assert prec_satoshi(stats['profit_closed_percent'], 0)
assert prec_satoshi(stats['profit_closed_fiat'], 0) assert prec_satoshi(stats['profit_closed_fiat'], 0)
@ -320,7 +314,7 @@ def test_rpc_balance_handle(default_conf, mocker):
ticker=MagicMock(return_value={'price_usd': 15000.0}), ticker=MagicMock(return_value={'price_usd': 15000.0}),
) )
mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0) mocker.patch('freqtrade.fiat_convert.CryptoToFiatConverter._find_price', return_value=15000.0)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -330,18 +324,16 @@ def test_rpc_balance_handle(default_conf, mocker):
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, res) = rpc.rpc_balance(default_conf['fiat_display_currency']) output, total, symbol, value = rpc._rpc_balance(default_conf['fiat_display_currency'])
assert not error assert prec_satoshi(total, 12)
(trade, x, y, z) = res assert prec_satoshi(value, 180000)
assert prec_satoshi(x, 12) assert 'USD' in symbol
assert prec_satoshi(z, 180000) assert len(output) == 1
assert 'USD' in y assert 'BTC' in output[0]['currency']
assert len(trade) == 1 assert prec_satoshi(output[0]['available'], 10)
assert 'BTC' in trade[0]['currency'] assert prec_satoshi(output[0]['balance'], 12)
assert prec_satoshi(trade[0]['available'], 10) assert prec_satoshi(output[0]['pending'], 2)
assert prec_satoshi(trade[0]['balance'], 12) assert prec_satoshi(output[0]['est_btc'], 12)
assert prec_satoshi(trade[0]['pending'], 2)
assert prec_satoshi(trade[0]['est_btc'], 12)
def test_rpc_start(mocker, default_conf) -> None: def test_rpc_start(mocker, default_conf) -> None:
@ -350,7 +342,7 @@ def test_rpc_start(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -361,13 +353,11 @@ def test_rpc_start(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert not error
assert '`Starting trader ...`' in result assert '`Starting trader ...`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
(error, result) = rpc.rpc_start() result = rpc._rpc_start()
assert error
assert '*Status:* `already running`' in result assert '*Status:* `already running`' in result
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
@ -378,7 +368,7 @@ def test_rpc_stop(mocker, default_conf) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -389,13 +379,11 @@ def test_rpc_stop(mocker, default_conf) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert not error
assert '`Stopping trader ...`' in result assert '`Stopping trader ...`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
(error, result) = rpc.rpc_stop() result = rpc._rpc_stop()
assert error
assert '*Status:* `already stopped`' in result assert '*Status:* `already stopped`' in result
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
@ -406,7 +394,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
cancel_order_mock = MagicMock() cancel_order_mock = MagicMock()
mocker.patch.multiple( mocker.patch.multiple(
@ -428,36 +416,26 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*Invalid argument.*'):
assert error rpc._rpc_forcesell(None)
assert res == 'Invalid argument.'
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
freqtradebot.create_trade() freqtradebot.create_trade()
(error, res) = rpc.rpc_forcesell('all') rpc._rpc_forcesell('all')
assert not error
assert res == ''
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
freqtradebot.state = State.STOPPED freqtradebot.state = State.STOPPED
(error, res) = rpc.rpc_forcesell(None) with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell(None)
assert res == '`trader is not running`'
(error, res) = rpc.rpc_forcesell('all') with pytest.raises(RPCException, match=r'.*`trader is not running`*'):
assert error rpc._rpc_forcesell('all')
assert res == '`trader is not running`'
freqtradebot.state = State.RUNNING freqtradebot.state = State.RUNNING
assert cancel_order_mock.call_count == 0 assert cancel_order_mock.call_count == 0
@ -475,9 +453,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
# and trade amount is updated # and trade amount is updated
(error, res) = rpc.rpc_forcesell('1') rpc._rpc_forcesell('1')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 1 assert cancel_order_mock.call_count == 1
assert trade.amount == filled_amount assert trade.amount == filled_amount
@ -495,9 +471,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
} }
) )
# check that the trade is called, which is done by ensuring exchange.cancel_order is called # check that the trade is called, which is done by ensuring exchange.cancel_order is called
(error, res) = rpc.rpc_forcesell('2') rpc._rpc_forcesell('2')
assert not error
assert res == ''
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
assert trade.amount == amount assert trade.amount == amount
@ -511,9 +485,7 @@ def test_rpc_forcesell(default_conf, ticker, fee, mocker) -> None:
'side': 'sell' 'side': 'sell'
} }
) )
(error, res) = rpc.rpc_forcesell('3') rpc._rpc_forcesell('3')
assert not error
assert res == ''
# status quo, no exchange calls # status quo, no exchange calls
assert cancel_order_mock.call_count == 2 assert cancel_order_mock.call_count == 2
@ -525,7 +497,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -550,8 +522,7 @@ def test_performance_handle(default_conf, ticker, limit_buy_order, fee,
trade.close_date = datetime.utcnow() trade.close_date = datetime.utcnow()
trade.is_open = False trade.is_open = False
(error, res) = rpc.rpc_performance() res = rpc._rpc_performance()
assert not error
assert len(res) == 1 assert len(res) == 1
assert res[0]['pair'] == 'ETH/BTC' assert res[0]['pair'] == 'ETH/BTC'
assert res[0]['count'] == 1 assert res[0]['count'] == 1
@ -564,7 +535,7 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
""" """
patch_get_signal(mocker, (True, False)) patch_get_signal(mocker, (True, False))
patch_coinmarketcap(mocker) patch_coinmarketcap(mocker)
mocker.patch('freqtrade.rpc.rpc_manager.Telegram', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
validate_pairs=MagicMock(), validate_pairs=MagicMock(),
@ -576,14 +547,12 @@ def test_rpc_count(mocker, default_conf, ticker, fee) -> None:
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
rpc = RPC(freqtradebot) rpc = RPC(freqtradebot)
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 0 assert nb_trades == 0
# Create some test data # Create some test data
freqtradebot.create_trade() freqtradebot.create_trade()
(error, trades) = rpc.rpc_count() trades = rpc._rpc_count()
nb_trades = len(trades) nb_trades = len(trades)
assert not error
assert nb_trades == 1 assert nb_trades == 1

View File

@ -7,49 +7,35 @@ from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
from freqtrade.rpc.rpc_manager import RPCManager from freqtrade.rpc.rpc_manager import RPCManager
from freqtrade.rpc.telegram import Telegram
from freqtrade.tests.conftest import log_has, get_patched_freqtradebot from freqtrade.tests.conftest import log_has, get_patched_freqtradebot
def test_rpc_manager_object() -> None: def test_rpc_manager_object() -> None:
""" """ Test the Arguments object has the mandatory methods """
Test the Arguments object has the mandatory methods
:return: None
"""
assert hasattr(RPCManager, '_init')
assert hasattr(RPCManager, 'send_msg') assert hasattr(RPCManager, 'send_msg')
assert hasattr(RPCManager, 'cleanup') assert hasattr(RPCManager, 'cleanup')
def test__init__(mocker, default_conf) -> None: def test__init__(mocker, default_conf) -> None:
""" """ Test __init__() method """
Test __init__() method conf = deepcopy(default_conf)
""" conf['telegram']['enabled'] = False
init_mock = mocker.patch('freqtrade.rpc.rpc_manager.RPCManager._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf)
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
assert rpc_manager.freqtrade == freqtradebot
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
assert init_mock.call_count == 1
def test_init_telegram_disabled(mocker, default_conf, caplog) -> None: def test_init_telegram_disabled(mocker, default_conf, caplog) -> None:
""" """ Test _init() method with Telegram disabled """
Test _init() method with Telegram disabled
"""
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
conf = deepcopy(default_conf) conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False conf['telegram']['enabled'] = False
freqtradebot = get_patched_freqtradebot(mocker, conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, conf))
rpc_manager = RPCManager(freqtradebot)
assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert not log_has('Enabling rpc.telegram ...', caplog.record_tuples)
assert rpc_manager.registered_modules == [] assert rpc_manager.registered_modules == []
assert rpc_manager.telegram is None
def test_init_telegram_enabled(mocker, default_conf, caplog) -> None: def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
@ -59,14 +45,12 @@ def test_init_telegram_enabled(mocker, default_conf, caplog) -> None:
caplog.set_level(logging.DEBUG) caplog.set_level(logging.DEBUG)
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
freqtradebot = get_patched_freqtradebot(mocker, default_conf) rpc_manager = RPCManager(get_patched_freqtradebot(mocker, default_conf))
rpc_manager = RPCManager(freqtradebot)
assert log_has('Enabling rpc.telegram ...', caplog.record_tuples) assert log_has('Enabling rpc.telegram ...', caplog.record_tuples)
len_modules = len(rpc_manager.registered_modules) len_modules = len(rpc_manager.registered_modules)
assert len_modules == 1 assert len_modules == 1
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
assert isinstance(rpc_manager.telegram, Telegram)
def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None: def test_cleanup_telegram_disabled(mocker, default_conf, caplog) -> None:
@ -99,11 +83,11 @@ def test_cleanup_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
# Check we have Telegram as a registered modules # Check we have Telegram as a registered modules
assert 'telegram' in rpc_manager.registered_modules assert 'telegram' in [mod.name for mod in rpc_manager.registered_modules]
rpc_manager.cleanup() rpc_manager.cleanup()
assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples) assert log_has('Cleaning up rpc.telegram ...', caplog.record_tuples)
assert 'telegram' not in rpc_manager.registered_modules assert 'telegram' not in [mod.name for mod in rpc_manager.registered_modules]
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1
@ -120,7 +104,7 @@ def test_send_msg_telegram_disabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 0 assert telegram_mock.call_count == 0
@ -135,5 +119,5 @@ def test_send_msg_telegram_enabled(mocker, default_conf, caplog) -> None:
rpc_manager = RPCManager(freqtradebot) rpc_manager = RPCManager(freqtradebot)
rpc_manager.send_msg('test') rpc_manager.send_msg('test')
assert log_has('test', caplog.record_tuples) assert log_has('Sending rpc message: test', caplog.record_tuples)
assert telegram_mock.call_count == 1 assert telegram_mock.call_count == 1

View File

@ -32,6 +32,9 @@ class DummyCls(Telegram):
super().__init__(freqtrade) super().__init__(freqtrade)
self.state = {'called': False} self.state = {'called': False}
def _init(self):
pass
@authorized_only @authorized_only
def dummy_handler(self, *args, **kwargs) -> None: def dummy_handler(self, *args, **kwargs) -> None:
""" """
@ -60,9 +63,7 @@ def test__init__(default_conf, mocker) -> None:
def test_init(default_conf, mocker, caplog) -> None: def test_init(default_conf, mocker, caplog) -> None:
""" """ Test _init() method """
Test _init() method
"""
start_polling = MagicMock() start_polling = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling)) mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock(return_value=start_polling))
@ -70,31 +71,16 @@ def test_init(default_conf, mocker, caplog) -> None:
assert start_polling.call_count == 0 assert start_polling.call_count == 0
# number of handles registered # number of handles registered
assert start_polling.dispatcher.add_handler.call_count == 11 assert start_polling.dispatcher.add_handler.call_count > 0
assert start_polling.start_polling.call_count == 1 assert start_polling.start_polling.call_count == 1
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \ message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \ "['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]" "['count'], ['reload_conf'], ['help'], ['version']]"
assert log_has(message_str, caplog.record_tuples) assert log_has(message_str, caplog.record_tuples)
def test_init_disabled(default_conf, mocker, caplog) -> None:
"""
Test _init() method when Telegram is disabled
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
Telegram(get_patched_freqtradebot(mocker, conf))
message_str = "rpc.telegram is listening for following commands: [['status'], ['profit'], " \
"['balance'], ['start'], ['stop'], ['forcesell'], ['performance'], ['daily'], " \
"['count'], ['help'], ['version']]"
assert not log_has(message_str, caplog.record_tuples)
def test_cleanup(default_conf, mocker) -> None: def test_cleanup(default_conf, mocker) -> None:
""" """
Test cleanup() method Test cleanup() method
@ -103,44 +89,11 @@ def test_cleanup(default_conf, mocker) -> None:
updater_mock.stop = MagicMock() updater_mock.stop = MagicMock()
mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock) mocker.patch('freqtrade.rpc.telegram.Updater', updater_mock)
# not enabled telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup()
assert telegram._updater is None
assert updater_mock.call_count == 0
assert not hasattr(telegram._updater, 'stop')
assert updater_mock.stop.call_count == 0
# enabled
conf['telegram']['enabled'] = True
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
telegram.cleanup() telegram.cleanup()
assert telegram._updater.stop.call_count == 1 assert telegram._updater.stop.call_count == 1
def test_is_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
mocker.patch('freqtrade.rpc.telegram.Updater', MagicMock())
telegram = Telegram(get_patched_freqtradebot(mocker, default_conf))
assert telegram.is_enabled()
def test_is_not_enabled(default_conf, mocker) -> None:
"""
Test is_enabled() method
"""
conf = deepcopy(default_conf)
conf['telegram']['enabled'] = False
telegram = Telegram(get_patched_freqtradebot(mocker, conf))
assert not telegram.is_enabled()
def test_authorized_only(default_conf, mocker, caplog) -> None: def test_authorized_only(default_conf, mocker, caplog) -> None:
""" """
Test authorized_only() method when we are authorized Test authorized_only() method when we are authorized
@ -256,9 +209,9 @@ def test_status(default_conf, update, mocker, fee, ticker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
rpc_trade_status=MagicMock(return_value=(False, [1, 2, 3])), _rpc_trade_status=MagicMock(return_value=[1, 2, 3]),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -296,7 +249,7 @@ def test_status_handle(default_conf, update, ticker, fee, mocker) -> None:
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
_status_table=status_table, _status_table=status_table,
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -341,7 +294,7 @@ def test_status_table_handle(default_conf, update, ticker, fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -397,7 +350,7 @@ def test_daily_handle(default_conf, update, ticker, limit_buy_order, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -465,7 +418,7 @@ def test_daily_wrong_input(default_conf, update, ticker, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -506,7 +459,7 @@ def test_profit_handle(default_conf, update, ticker, ticker_sell_up, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -604,7 +557,7 @@ def test_telegram_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -634,7 +587,7 @@ def test_zero_balance_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -656,7 +609,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -667,7 +620,7 @@ def test_start_handle(default_conf, update, mocker) -> None:
assert freqtradebot.state == State.STOPPED assert freqtradebot.state == State.STOPPED
telegram._start(bot=MagicMock(), update=update) telegram._start(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RUNNING assert freqtradebot.state == State.RUNNING
assert msg_mock.call_count == 0 assert msg_mock.call_count == 1
def test_start_handle_already_running(default_conf, update, mocker) -> None: def test_start_handle_already_running(default_conf, update, mocker) -> None:
@ -680,7 +633,7 @@ def test_start_handle_already_running(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -705,7 +658,7 @@ def test_stop_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -730,7 +683,7 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock()) mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
@ -745,6 +698,29 @@ def test_stop_handle_already_stopped(default_conf, update, mocker) -> None:
assert 'already stopped' in msg_mock.call_args_list[0][0][0] assert 'already stopped' in msg_mock.call_args_list[0][0][0]
def test_reload_conf_handle(default_conf, update, mocker) -> None:
""" Test _reload_conf() method """
patch_coinmarketcap(mocker)
mocker.patch('freqtrade.freqtradebot.exchange.init', MagicMock())
msg_mock = MagicMock()
mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(),
_send_msg=msg_mock
)
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot)
freqtradebot.state = State.RUNNING
assert freqtradebot.state == State.RUNNING
telegram._reload_conf(bot=MagicMock(), update=update)
assert freqtradebot.state == State.RELOAD_CONF
assert msg_mock.call_count == 1
assert 'Reloading config' in msg_mock.call_args_list[0][0][0]
def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None: def test_forcesell_handle(default_conf, update, ticker, fee, ticker_sell_up, mocker) -> None:
""" """
Test _forcesell() method Test _forcesell() method
@ -875,7 +851,7 @@ def test_forcesell_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
@ -917,7 +893,7 @@ def test_performance_handle(default_conf, update, ticker, fee,
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -958,7 +934,7 @@ def test_performance_handle_invalid(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock()) mocker.patch('freqtrade.freqtradebot.exchange.validate_pairs', MagicMock())
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
@ -981,7 +957,7 @@ def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.freqtradebot.exchange', 'freqtrade.freqtradebot.exchange',
@ -1024,7 +1000,7 @@ def test_help_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1044,7 +1020,7 @@ def test_version_handle(default_conf, update, mocker) -> None:
mocker.patch.multiple( mocker.patch.multiple(
'freqtrade.rpc.telegram.Telegram', 'freqtrade.rpc.telegram.Telegram',
_init=MagicMock(), _init=MagicMock(),
send_msg=msg_mock _send_msg=msg_mock
) )
freqtradebot = FreqtradeBot(default_conf) freqtradebot = FreqtradeBot(default_conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
@ -1066,13 +1042,8 @@ def test_send_msg(default_conf, mocker) -> None:
freqtradebot = FreqtradeBot(conf) freqtradebot = FreqtradeBot(conf)
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = False
telegram.send_msg('test', bot)
assert not bot.method_calls
bot.reset_mock()
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
assert len(bot.method_calls) == 1 assert len(bot.method_calls) == 1
@ -1090,7 +1061,7 @@ def test_send_msg_network_error(default_conf, mocker, caplog) -> None:
telegram = Telegram(freqtradebot) telegram = Telegram(freqtradebot)
telegram._config['telegram']['enabled'] = True telegram._config['telegram']['enabled'] = True
telegram.send_msg('test', bot) telegram._send_msg('test', bot)
# Bot should've tried to send it twice # Bot should've tried to send it twice
assert len(bot.method_calls) == 2 assert len(bot.method_calls) == 2

View File

@ -26,13 +26,30 @@ def test_load_strategy(result):
assert 'adx' in resolver.strategy.populate_indicators(result) assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_from_url(result):
resolver = StrategyResolver()
resolver._load_strategy('https://freq.isaac.international/'
'dev/strategies/GBPAQEFGGWCMWVFU34P'
'MVGS4P2NJR4IDFNVI4LTCZAKJAD3JCXUMBI4J/AverageStrategy/code')
assert hasattr(resolver.strategy, 'minimal_roi')
assert 'adx' in resolver.strategy.populate_indicators(result)
def test_load_strategy_custom_directory(result): def test_load_strategy_custom_directory(result):
resolver = StrategyResolver() resolver = StrategyResolver()
extra_dir = os.path.join('some', 'path') extra_dir = os.path.join('some', 'path')
with pytest.raises(
FileNotFoundError, if os.name == 'nt':
match=r".*No such file or directory: '{}'".format(extra_dir)): with pytest.raises(
resolver._load_strategy('TestStrategy', extra_dir) FileNotFoundError,
match="FileNotFoundError: [WinError 3] The system cannot find the "
"path specified: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
else:
with pytest.raises(
FileNotFoundError,
match=r".*No such file or directory: '{}'".format(extra_dir)):
resolver._load_strategy('TestStrategy', extra_dir)
assert hasattr(resolver.strategy, 'populate_indicators') assert hasattr(resolver.strategy, 'populate_indicators')
assert 'adx' in resolver.strategy.populate_indicators(result) assert 'adx' in resolver.strategy.populate_indicators(result)

View File

@ -63,6 +63,7 @@ def test_scripts_options() -> None:
arguments = Arguments(['-p', 'ETH/BTC'], '') arguments = Arguments(['-p', 'ETH/BTC'], '')
arguments.scripts_options() arguments.scripts_options()
args = arguments.get_parsed_arg() args = arguments.get_parsed_arg()
print(args.pair)
assert args.pair == 'ETH/BTC' assert args.pair == 'ETH/BTC'

View File

@ -57,7 +57,7 @@ def patch_RPCManager(mocker) -> MagicMock:
:param mocker: mocker to patch RPCManager class :param mocker: mocker to patch RPCManager class
:return: RPCManager.send_msg MagicMock to track if this method is called :return: RPCManager.send_msg MagicMock to track if this method is called
""" """
mocker.patch('freqtrade.freqtradebot.RPCManager._init', MagicMock()) mocker.patch('freqtrade.rpc.telegram.Telegram', MagicMock())
rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock()) rpc_mock = mocker.patch('freqtrade.freqtradebot.RPCManager.send_msg', MagicMock())
return rpc_mock return rpc_mock
@ -68,7 +68,7 @@ def test_freqtradebot_object() -> None:
Test the FreqtradeBot object has the mandatory public methods Test the FreqtradeBot object has the mandatory public methods
""" """
assert hasattr(FreqtradeBot, 'worker') assert hasattr(FreqtradeBot, 'worker')
assert hasattr(FreqtradeBot, 'clean') assert hasattr(FreqtradeBot, 'cleanup')
assert hasattr(FreqtradeBot, 'create_trade') assert hasattr(FreqtradeBot, 'create_trade')
assert hasattr(FreqtradeBot, 'get_target_bid') assert hasattr(FreqtradeBot, 'get_target_bid')
assert hasattr(FreqtradeBot, 'process_maybe_execute_buy') assert hasattr(FreqtradeBot, 'process_maybe_execute_buy')
@ -93,7 +93,7 @@ def test_freqtradebot(mocker, default_conf) -> None:
assert freqtrade.state is State.STOPPED assert freqtrade.state is State.STOPPED
def test_clean(mocker, default_conf, caplog) -> None: def test_cleanup(mocker, default_conf, caplog) -> None:
""" """
Test clean() method Test clean() method
""" """
@ -101,11 +101,8 @@ def test_clean(mocker, default_conf, caplog) -> None:
mocker.patch('freqtrade.persistence.cleanup', mock_cleanup) mocker.patch('freqtrade.persistence.cleanup', mock_cleanup)
freqtrade = get_patched_freqtradebot(mocker, default_conf) freqtrade = get_patched_freqtradebot(mocker, default_conf)
assert freqtrade.state == State.RUNNING freqtrade.cleanup()
assert log_has('Cleaning up modules ...', caplog.record_tuples)
assert freqtrade.clean()
assert freqtrade.state == State.STOPPED
assert log_has('Stopping trader and cleaning up modules...', caplog.record_tuples)
assert mock_cleanup.call_count == 1 assert mock_cleanup.call_count == 1

View File

@ -3,12 +3,16 @@ Unit test file for main.py
""" """
import logging import logging
from copy import deepcopy
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from freqtrade import OperationalException from freqtrade import OperationalException
from freqtrade.main import main, set_loggers from freqtrade.arguments import Arguments
from freqtrade.freqtradebot import FreqtradeBot
from freqtrade.main import main, set_loggers, reconfigure
from freqtrade.state import State
from freqtrade.tests.conftest import log_has from freqtrade.tests.conftest import log_has
@ -70,7 +74,7 @@ def test_main_fatal_exception(mocker, default_conf, caplog) -> None:
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(), _init_modules=MagicMock(),
worker=MagicMock(side_effect=Exception), worker=MagicMock(side_effect=Exception),
clean=MagicMock(), cleanup=MagicMock(),
) )
mocker.patch( mocker.patch(
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
@ -97,7 +101,7 @@ def test_main_keyboard_interrupt(mocker, default_conf, caplog) -> None:
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(), _init_modules=MagicMock(),
worker=MagicMock(side_effect=KeyboardInterrupt), worker=MagicMock(side_effect=KeyboardInterrupt),
clean=MagicMock(), cleanup=MagicMock(),
) )
mocker.patch( mocker.patch(
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
@ -124,7 +128,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
'freqtrade.freqtradebot.FreqtradeBot', 'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(), _init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')), worker=MagicMock(side_effect=OperationalException('Oh snap!')),
clean=MagicMock(), cleanup=MagicMock(),
) )
mocker.patch( mocker.patch(
'freqtrade.configuration.Configuration._load_config_file', 'freqtrade.configuration.Configuration._load_config_file',
@ -140,3 +144,69 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
main(args) main(args)
assert log_has('Using config: config.json.example ...', caplog.record_tuples) assert log_has('Using config: config.json.example ...', caplog.record_tuples)
assert log_has('Oh snap!', caplog.record_tuples) assert log_has('Oh snap!', caplog.record_tuples)
def test_main_reload_conf(mocker, default_conf, caplog) -> None:
"""
Test main() function
In this test we are skipping the while True loop by throwing an exception.
"""
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(return_value=State.RELOAD_CONF),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
# Raise exception as side effect to avoid endless loop
reconfigure_mock = mocker.patch(
'freqtrade.main.reconfigure', MagicMock(side_effect=Exception)
)
with pytest.raises(SystemExit):
main(['-c', 'config.json.example'])
assert reconfigure_mock.call_count == 1
assert log_has('Using config: config.json.example ...', caplog.record_tuples)
def test_reconfigure(mocker, default_conf) -> None:
""" Test recreate() function """
mocker.patch.multiple(
'freqtrade.freqtradebot.FreqtradeBot',
_init_modules=MagicMock(),
worker=MagicMock(side_effect=OperationalException('Oh snap!')),
cleanup=MagicMock(),
)
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: default_conf
)
mocker.patch('freqtrade.freqtradebot.CryptoToFiatConverter', MagicMock())
mocker.patch('freqtrade.freqtradebot.RPCManager', MagicMock())
freqtrade = FreqtradeBot(default_conf)
# Renew mock to return modified data
conf = deepcopy(default_conf)
conf['stake_amount'] += 1
mocker.patch(
'freqtrade.configuration.Configuration._load_config_file',
lambda *args, **kwargs: conf
)
# reconfigure should return a new instance
freqtrade2 = reconfigure(
freqtrade,
Arguments(['-c', 'config.json.example'], '').get_parsed_arg()
)
# Verify we have a new instance with the new config
assert freqtrade is not freqtrade2
assert freqtrade.config['stake_amount'] + 1 == freqtrade2.config['stake_amount']

View File

@ -425,6 +425,8 @@ def test_migrate_new(mocker, default_conf, fee):
close_profit FLOAT, close_profit FLOAT,
stake_amount FLOAT NOT NULL, stake_amount FLOAT NOT NULL,
amount FLOAT, amount FLOAT,
initial_stop_loss FLOAT,
max_rate FLOAT,
open_date DATETIME NOT NULL, open_date DATETIME NOT NULL,
close_date DATETIME, close_date DATETIME,
open_order_id VARCHAR, open_order_id VARCHAR,

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -110,10 +110,13 @@ def heikinashi(bars):
bars = bars.copy() bars = bars.copy()
bars['ha_close'] = (bars['open'] + bars['high'] + bars['ha_close'] = (bars['open'] + bars['high'] +
bars['low'] + bars['close']) / 4 bars['low'] + bars['close']) / 4
bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2 bars['ha_open'] = (bars['open'].shift(1) + bars['close'].shift(1)) / 2
bars.loc[:1, 'ha_open'] = bars['open'].values[0] bars.loc[:1, 'ha_open'] = bars['open'].values[0]
bars.loc[1:, 'ha_open'] = ( for x in range(2):
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:] bars.loc[1:, 'ha_open'] = (
(bars['ha_open'].shift(1) + bars['ha_close'].shift(1)) / 2)[1:]
bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1) bars['ha_high'] = bars.loc[:, ['high', 'ha_open', 'ha_close']].max(axis=1)
bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1) bars['ha_low'] = bars.loc[:, ['low', 'ha_open', 'ha_close']].min(axis=1)
@ -248,45 +251,36 @@ def crossed_below(series1, series2):
def rolling_std(series, window=200, min_periods=None): def rolling_std(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: if min_periods == window and len(series) > window:
if min_periods == window: return numpy_rolling_std(series, window, True)
return numpy_rolling_std(series, window, True) else:
else: try:
try: return series.rolling(window=window, min_periods=min_periods).std()
return series.rolling(window=window, min_periods=min_periods).std() except BaseException:
except BaseException: return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
return pd.Series(series).rolling(window=window, min_periods=min_periods).std()
except BaseException:
return pd.rolling_std(series, window=window, min_periods=min_periods)
# --------------------------------------------- # ---------------------------------------------
def rolling_mean(series, window=200, min_periods=None): def rolling_mean(series, window=200, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: if min_periods == window and len(series) > window:
if min_periods == window: return numpy_rolling_mean(series, window, True)
return numpy_rolling_mean(series, window, True) else:
else: try:
try: return series.rolling(window=window, min_periods=min_periods).mean()
return series.rolling(window=window, min_periods=min_periods).mean() except BaseException:
except BaseException: return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
return pd.Series(series).rolling(window=window, min_periods=min_periods).mean()
except BaseException:
return pd.rolling_mean(series, window=window, min_periods=min_periods)
# --------------------------------------------- # ---------------------------------------------
def rolling_min(series, window=14, min_periods=None): def rolling_min(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: try:
try: return series.rolling(window=window, min_periods=min_periods).min()
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
# --------------------------------------------- # ---------------------------------------------
@ -294,12 +288,9 @@ def rolling_min(series, window=14, min_periods=None):
def rolling_max(series, window=14, min_periods=None): def rolling_max(series, window=14, min_periods=None):
min_periods = window if min_periods is None else min_periods min_periods = window if min_periods is None else min_periods
try: try:
try: return series.rolling(window=window, min_periods=min_periods).min()
return series.rolling(window=window, min_periods=min_periods).min()
except BaseException:
return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
except BaseException: except BaseException:
return pd.rolling_min(series, window=window, min_periods=min_periods) return pd.Series(series).rolling(window=window, min_periods=min_periods).min()
# --------------------------------------------- # ---------------------------------------------
@ -566,9 +557,9 @@ def stoch(df, window=14, d=3, k=3, fast=False):
return pd.DataFrame(index=df.index, data=data) return pd.DataFrame(index=df.index, data=data)
# --------------------------------------------- # ---------------------------------------------
def zscore(bars, window=20, stds=1, col='close'): def zscore(bars, window=20, stds=1, col='close'):
""" get zscore of price """ """ get zscore of price """
std = numpy_rolling_std(bars[col], window) std = numpy_rolling_std(bars[col], window)

BIN
lib/libta_lib.a Normal file

Binary file not shown.

35
lib/libta_lib.la Executable file
View File

@ -0,0 +1,35 @@
# libta_lib.la - a libtool library file
# Generated by ltmain.sh - GNU libtool 1.5.22 Debian 1.5.22-4 (1.1220.2.365 2005/12/18 22:14:06)
#
# Please DO NOT delete this file!
# It is necessary for linking the library.
# The name that we can dlopen(3).
dlname='libta_lib.so.0'
# Names of this library.
library_names='libta_lib.so.0.0.0 libta_lib.so.0 libta_lib.so'
# The name of the static archive.
old_library='libta_lib.a'
# Libraries that this one depends upon.
dependency_libs=' -lpthread -ldl'
# Version information for libta_lib.
current=0
age=0
revision=0
# Is this an already installed library?
installed=yes
# Should we warn about portability when linking against -modules?
shouldnotlink=no
# Files to dlopen/dlpreopen
dlopen=''
dlpreopen=''
# Directory that this library needs to be installed in:
libdir='/usr/local/lib'

1
lib/libta_lib.so.0 Symbolic link
View File

@ -0,0 +1 @@
libta_lib.so.0.0.0

BIN
lib/libta_lib.so.0.0.0 Executable file

Binary file not shown.

18
requirements-aws.txt Normal file
View File

@ -0,0 +1,18 @@
ccxt==1.14.24
SQLAlchemy==1.2.7
arrow==0.12.1
cachetools==2.1.0
requests==2.18.4
urllib3==1.22
wrapt==1.10.11
pandas==0.23.0
scikit-learn==0.19.1
scipy==1.1.0
jsonschema==2.6.0
numpy==1.14.3
TA-Lib==0.4.17
git+git://github.com/berlinguyinca/networkx@v1.11
tabulate==0.8.2
coinmarketcap==5.0.3
simplejson==3.15.0
boto3

View File

@ -1,25 +1,27 @@
ccxt==1.14.169 ccxt==1.14.186
SQLAlchemy==1.2.8 SQLAlchemy==1.2.8
python-telegram-bot==10.1.0 python-telegram-bot==10.1.0
arrow==0.12.1 arrow==0.12.1
cachetools==2.1.0 cachetools==2.1.0
requests==2.18.4 requests==2.19.0
urllib3==1.22 urllib3==1.22
wrapt==1.10.11 wrapt==1.10.11
pandas==0.23.0 pandas==0.23.1
scikit-learn==0.19.1 scikit-learn==0.19.1
scipy==1.1.0 scipy==1.1.0
jsonschema==2.6.0 jsonschema==2.6.0
numpy==1.14.4 numpy==1.14.5
TA-Lib==0.4.17 TA-Lib==0.4.17
pytest==3.6.1 pytest==3.6.1
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest-cov==2.5.1 pytest-cov==2.5.1
hyperopt==0.1 hyperopt==0.1
# do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325 # do not upgrade networkx before this is fixed https://github.com/hyperopt/hyperopt/issues/325
networkx==1.11 # pyup: ignore networkx==1.11
#git+git://github.com/berlinguyinca/networkx@v1.11
git+git://github.com/berlinguyinca/technical
tabulate==0.8.2 tabulate==0.8.2
coinmarketcap==5.0.3 coinmarketcap==5.0.3
simplejson==3.15.0
# Required for plotting data # Required for plotting data
#plotly==2.3.0 #plotly==2.3.0

View File

@ -30,20 +30,27 @@ if not os.path.isfile(pairs_file):
with open(pairs_file) as file: with open(pairs_file) as file:
PAIRS = list(set(json.load(file))) PAIRS = list(set(json.load(file)))
PAIRS.sort()
since_time = None since_time = None
if args.days: if args.days:
since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000 since_time = arrow.utcnow().shift(days=-args.days).timestamp * 1000
print(f'About to download pairs: {PAIRS} to {dl_path}') print(f'About to download pairs: {PAIRS} to {dl_path}')
# Init exchange # Init exchange
exchange._API = exchange.init_ccxt({'key': '', exchange._API = exchange.init_ccxt({'key': '',
'secret': '', 'secret': '',
'name': args.exchange}) 'name': args.exchange})
pairs_not_available = []
# Make sure API markets is initialized
exchange._API.load_markets()
for pair in PAIRS: for pair in PAIRS:
if pair not in exchange._API.markets:
pairs_not_available.append(pair)
print(f"skipping pair {pair}")
continue
for tick_interval in timeframes: for tick_interval in timeframes:
print(f'downloading pair {pair}, interval {tick_interval}') print(f'downloading pair {pair}, interval {tick_interval}')
@ -60,3 +67,7 @@ for pair in PAIRS:
pair_print = pair.replace('/', '_') pair_print = pair.replace('/', '_')
filename = f'{pair_print}-{tick_interval}.json' filename = f'{pair_print}-{tick_interval}.json'
misc.file_dump_json(os.path.join(dl_path, filename), data) misc.file_dump_json(os.path.join(dl_path, filename), data)
if pairs_not_available:
print(f"Pairs [{','.join(pairs_not_available)}] not availble.")

View File

@ -5,45 +5,386 @@ Script to display when the bot will buy a specific pair
Mandatory Cli parameters: Mandatory Cli parameters:
-p / --pair: pair to examine -p / --pair: pair to examine
Option but recommended
-s / --strategy: strategy to use
Optional Cli parameters Optional Cli parameters
-s / --strategy: strategy to use
-d / --datadir: path to pair backtest data -d / --datadir: path to pair backtest data
--timerange: specify what timerange of data to use. --timerange: specify what timerange of data to use.
-l / --live: Live, to download the latest ticker for the pair -l / --live: Live, to download the latest ticker for the pair
-db / --db-url: Show trades stored in database -db / --db-url: Show trades stored in database
--plot-max-ticks N: plot N data points and overwrite the internal 750 cut of
Indicators recommended
Row 1: sma, ema3, ema5, ema10, ema50
Row 3: macd, rsi, fisher_rsi, mfi, slowd, slowk, fastd, fastk
Example of usage: Plotting Subplots, require the name of the dataframe column.
> python3 scripts/plot_dataframe.py --pair BTC/EUR -d user_data/data/ --indicators1 sma,ema3
--indicators2 fastk,fastd Each plot will be displayed as usual on exchanges
--plot-rsi <RSI>
--plot-cci <CCI>
--plot-osc <CCI>
--plot-macd <MACD>
--plot-cmf <CMF>
--
""" """
import datetime
import logging import logging
import os
import sys import sys
from argparse import Namespace from argparse import Namespace
from typing import Dict, List, Any from typing import List
import plotly.graph_objs as go import plotly.graph_objs as go
from plotly import tools from plotly import tools
from plotly.offline import plot from plotly.offline import plot
from typing import Dict, List, Any
from sqlalchemy import create_engine
import freqtrade.optimize as optimize import freqtrade.optimize as optimize
from freqtrade import exchange from freqtrade import exchange
from freqtrade import persistence
from freqtrade.analyze import Analyze from freqtrade.analyze import Analyze
from freqtrade.arguments import Arguments from freqtrade.arguments import Arguments
from freqtrade.optimize.backtesting import setup_configuration from freqtrade.analyze import Analyze
from freqtrade import exchange
import freqtrade.optimize as optimize
from freqtrade import persistence
from freqtrade.persistence import Trade from freqtrade.persistence import Trade
from freqtrade.configuration import Configuration
from pandas import DataFrame
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
_CONF: Dict[str, Any] = {} _CONF: Dict[str, Any] = {}
logger = logging.getLogger('freqtrade')
def plot_dataframes_markers(data, fig, args):
"""
plots additional dataframe markers in the main plot
:param data:
:param fig:
:param args:
:return:
"""
if args.plotdataframemarker:
for x in args.plotdataframemarker:
filter = data[(data[x] == 100) | (data[x] == -100)]
marker = go.Scatter(
x=filter.date,
y=filter.low * 0.99,
mode='markers',
name=x,
marker=dict(
symbol='diamond-tall-open',
size=10,
line=dict(width=1)
)
)
fig.append_trace(marker, 1, 1)
def plot_dataframes(data, fig, args):
"""
plots additional dataframes in the main plot
:param data:
:param fig:
:param args:
:return:
"""
if args.plotdataframe:
for x in args.plotdataframe:
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
fig.append_trace(chart, 1, 1)
def plot_volume_dataframe(data, fig, args, plotnumber):
"""
adds the plotting of the volume
:param data:
:param fig:
:param args:
:return:
"""
volume = go.Bar(x=data['date'], y=data['volume'], name='Volume')
fig.append_trace(volume, plotnumber, 1)
def plot_macd_dataframe(data, fig, args, plotnumber):
"""
adds the plotting of the MACD if specified
:param data:
:param fig:
:param args:
:return:
"""
macd = go.Scattergl(x=data['date'], y=data[args.plotmacd], name='MACD')
macdsignal = go.Scattergl(x=data['date'], y=data[args.plotmacd + 'signal'], name='MACD signal')
fig.append_trace(macd, plotnumber, 1)
fig.append_trace(macdsignal, plotnumber, 1)
def plot_rsi_dataframe(data, fig, args, plotnumber):
"""
this function plots an additional RSI chart under the exiting charts
:param data:
:param fig:
:param args:
:return:
"""
if args.plotrsi:
for x in args.plotrsi:
rsi = go.Scattergl(x=data['date'], y=data[x], name=x)
fig.append_trace(rsi, plotnumber, 1)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'fillcolor': 'red',
'opacity': 0.1,
'type': 'rect',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 70,
'y1': 100,
'line': {'color': 'gray'}
}
)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'fillcolor': 'green',
'opacity': 0.1,
'type': 'rect',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 0,
'y1': 30,
'line': {'color': 'gray'}
}
)
def plot_osc_dataframe(data, fig, args, plotnumber):
"""
this function plots an additional cci chart under the exiting charts
:param data:
:param fig:
:param args:
:return:
"""
if args.plotosc:
for x in args.plotosc:
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
fig.append_trace(chart, plotnumber, 1)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'fillcolor': 'gray',
'opacity': 0.1,
'type': 'rect',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 0.3,
'y1': 0.7,
'line': {'color': 'gray'}
}
)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'type': 'line',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 0.6,
'y1': 0.6,
'line': {'color': 'red','width': 1}
}
)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'type': 'line',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 0.4,
'y1': 0.4,
'line': {'color': 'green','width':1}
}
)
def plot_cmf_dataframe(data, fig, args, plotnumber):
"""
this function plots an additional cci chart under the exiting charts
:param data:
:param fig:
:param args:
:return:
"""
minValue = 0;
maxValue = 0;
if args.plotcmf:
for x in args.plotcmf:
chart = go.Bar(x=data['date'], y=data[x], name=x)
fig.append_trace(chart, plotnumber, 1)
def plot_cci_dataframe(data, fig, args, plotnumber):
"""
this function plots an additional cci chart under the exiting charts
:param data:
:param fig:
:param args:
:return:
"""
minValue = 0;
maxValue = 0;
if args.plotcci:
for x in args.plotcci:
if minValue > min(data[x]):
minValue = min(data[x])
if maxValue < max(data[x]):
maxValue = max(data[x])
chart = go.Scattergl(x=data['date'], y=data[x], name=x)
fig.append_trace(chart, plotnumber, 1)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'fillcolor': 'red',
'opacity': 0.1,
'type': 'rect',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': 100,
'y1': maxValue,
'line': {'color': 'gray'}
}
)
fig['layout']['shapes'].append(
{
'yref': 'y' + str(plotnumber),
'fillcolor': 'green',
'opacity': 0.1,
'type': 'rect',
'x0': DataFrame.min(data['date']),
'x1': DataFrame.max(data['date']),
'y0': -100,
'y1': minValue,
'line': {'color': 'gray'}
}
)
def plot_stop_loss_trade(df_sell, fig, analyze, args):
"""
plots the stop loss for the associated trades and buys
as well as the estimated profit ranges.
will be enabled if --stop-loss is provided
as argument
:param data:
:param trades:
:return:
"""
if args.stoplossdisplay is False:
return
if 'associated_buy_price' not in df_sell:
return
stoploss = analyze.strategy.stoploss
for index, x in df_sell.iterrows():
if x['associated_buy_price'] > 0:
# draw stop loss
fig['layout']['shapes'].append(
{
'fillcolor': 'red',
'opacity': 0.1,
'type': 'rect',
'x0': x['associated_buy_date'],
'x1': x['date'],
'y0': x['associated_buy_price'],
'y1': (x['associated_buy_price'] - abs(stoploss) * x['associated_buy_price']),
'line': {'color': 'red'}
}
)
totalTime = 0
for time in analyze.strategy.minimal_roi:
t = int(time)
totalTime = t + totalTime
enddate = x['date']
date = x['associated_buy_date'] + datetime.timedelta(minutes=totalTime)
# draw profit range
fig['layout']['shapes'].append(
{
'fillcolor': 'green',
'opacity': 0.1,
'type': 'rect',
'x0': date,
'x1': enddate,
'y0': x['associated_buy_price'],
'y1': x['associated_buy_price'] + x['associated_buy_price'] * analyze.strategy.minimal_roi[
time],
'line': {'color': 'green'}
}
)
def find_profits(data):
"""
finds the profits between sells and the associated buys. This does not take in account
ROI!
:param data:
:return:
"""
# go over all the sells
# find all previous buys
df_sell = data[data['sell'] == 1]
df_sell['profit'] = 0
df_buys = data[data['buy'] == 1]
lastDate = data['date'].iloc[0]
for index, row in df_sell.iterrows():
buys = df_buys[(df_buys['date'] < row['date']) & (df_buys['date'] > lastDate)]
profit = None
if buys['date'].count() > 0:
buys = buys.tail()
profit = round(row['close'] / buys['close'].values[0] * 100 - 100, 2)
lastDate = row['date']
df_sell.loc[index, 'associated_buy_date'] = buys['date'].values[0]
df_sell.loc[index, 'associated_buy_price'] = buys['close'].values[0]
df_sell.loc[index, 'profit'] = profit
return df_sell
def plot_analyzed_dataframe(args: Namespace) -> None: def plot_analyzed_dataframe(args: Namespace) -> None:
@ -51,29 +392,14 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
Calls analyze() and plots the returned dataframe Calls analyze() and plots the returned dataframe
:return: None :return: None
""" """
global _CONF pair = args.pair.replace('-', '_')
# Load the configuration
_CONF.update(setup_configuration(args))
# Set the pair to audit
pair = args.pair
if pair is None:
logger.critical('Parameter --pair mandatory;. E.g --pair ETH/BTC')
exit()
if '/' not in pair:
logger.critical('--pair format must be XXX/YYY')
exit()
# Set timerange to use
timerange = Arguments.parse_timerange(args.timerange) timerange = Arguments.parse_timerange(args.timerange)
# Load the strategy # Init strategy
try: try:
analyze = Analyze(_CONF) config = Configuration(args)
exchange.init(_CONF)
analyze = Analyze(config.get_config())
except AttributeError: except AttributeError:
logger.critical( logger.critical(
'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"', 'Impossible to load the strategy. Please check the file "user_data/strategies/%s.py"',
@ -81,75 +407,40 @@ def plot_analyzed_dataframe(args: Namespace) -> None:
) )
exit() exit()
# Set the ticker to use tick_interval = analyze.strategy.ticker_interval
tick_interval = analyze.get_ticker_interval()
# Load pair tickers
tickers = {} tickers = {}
if args.live: if args.live:
logger.info('Downloading pair.') logger.info('Downloading pair.')
# Init Bittrex to use public API
exchange.init({'key': '', 'secret': ''})
tickers[pair] = exchange.get_ticker_history(pair, tick_interval) tickers[pair] = exchange.get_ticker_history(pair, tick_interval)
else: else:
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=args.datadir, datadir=_CONF.get("datadir"),
pairs=[pair], pairs=[pair],
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=_CONF.get('refresh_pairs', False), refresh_pairs=False,
timerange=timerange timerange=timerange
) )
# No ticker found, or impossible to download
if tickers == {}:
exit()
# Get trades already made from the DB
trades: List[Trade] = []
if args.db_url:
persistence.init(_CONF)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
dataframes = analyze.tickerdata_to_dataframe(tickers) dataframes = analyze.tickerdata_to_dataframe(tickers)
dataframe = dataframes[pair] dataframe = dataframes[pair]
dataframe = analyze.populate_buy_trend(dataframe) dataframe = analyze.populate_buy_trend(dataframe)
dataframe = analyze.populate_sell_trend(dataframe) dataframe = analyze.populate_sell_trend(dataframe)
if len(dataframe.index) > args.plotticks:
logger.warning('Ticker contained more than {} candles, clipping.'.format(args.plotticks))
data = dataframe.tail(args.plotticks)
trades = []
if args.db_url:
engine = create_engine('sqlite:///' + args.db_url)
persistence.init(_CONF, engine)
trades = Trade.query.filter(Trade.pair.is_(pair)).all()
if len(dataframe.index) > 750: if len(dataframe.index) > 750:
logger.warning('Ticker contained more than 750 candles, clipping.') logger.warning('Ticker contained more than 750 candles, clipping.')
data = dataframe.tail(750)
fig = generate_graph(
pair=pair,
trades=trades,
data=dataframe.tail(750),
args=args
)
plot(fig, filename=os.path.join('user_data', 'freqtrade-plot.html'))
def generate_graph(pair, trades, data, args) -> tools.make_subplots:
"""
Generate the graph from the data generated by Backtesting or from DB
:param pair: Pair to Display on the graph
:param trades: All trades created
:param data: Dataframe
:param args: sys.argv that contrains the two params indicators1, and indicators2
:return: None
"""
# Define the graph
fig = tools.make_subplots(
rows=3,
cols=1,
shared_xaxes=True,
row_width=[1, 1, 4],
vertical_spacing=0.0001,
)
fig['layout'].update(title=pair)
fig['layout']['yaxis1'].update(title='Price')
fig['layout']['yaxis2'].update(title='Volume')
fig['layout']['yaxis3'].update(title='Other')
# Common information
candles = go.Candlestick( candles = go.Candlestick(
x=data.date, x=data.date,
open=data.open, open=data.open,
@ -160,6 +451,7 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
df_buy = data[data['buy'] == 1] df_buy = data[data['buy'] == 1]
buys = go.Scattergl( buys = go.Scattergl(
x=df_buy.date, x=df_buy.date,
y=df_buy.close, y=df_buy.close,
@ -167,23 +459,27 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
name='buy', name='buy',
marker=dict( marker=dict(
symbol='triangle-up-dot', symbol='triangle-up-dot',
size=9, size=15,
line=dict(width=1), line=dict(width=1),
color='green', color='green',
) )
) )
df_sell = data[data['sell'] == 1] df_sell = find_profits(data)
sells = go.Scattergl(
sells = go.Scatter(
x=df_sell.date, x=df_sell.date,
y=df_sell.close, y=df_sell.close,
mode='markers', mode='markers+text',
name='sell', name='sell',
text=df_sell.profit,
textposition='top right',
marker=dict( marker=dict(
symbol='triangle-down-dot', symbol='triangle-down-dot',
size=9, size=15,
line=dict(width=1), line=dict(width=1),
color='red', color='red',
) )
) )
trade_buys = go.Scattergl( trade_buys = go.Scattergl(
@ -211,67 +507,107 @@ def generate_graph(pair, trades, data, args) -> tools.make_subplots:
) )
) )
# Row 1 bb_lower = go.Scatter(
x=data.date,
y=data.bb_lowerband,
name='BB lower',
line={'color': "transparent"},
)
bb_upper = go.Scatter(
x=data.date,
y=data.bb_upperband,
name='BB upper',
fill="tonexty",
fillcolor="rgba(0,176,246,0.2)",
line={'color': "transparent"},
)
bb_middle = go.Scatter(
x=data.date,
y=data.bb_middleband,
name='BB middle',
fill="tonexty",
fillcolor="rgba(0,176,246,0.2)",
line={'color': "red"},
)
# ugly hack for now
rowWidth = [1]
if args.plotvolume:
rowWidth.append(1)
if args.plotmacd:
rowWidth.append(1)
if args.plotrsi:
rowWidth.append(1)
if args.plotcci:
rowWidth.append(1)
if args.plotcmf:
rowWidth.append(1)
if args.plotosc:
rowWidth.append(1)
# standard layout signal + volume
fig = tools.make_subplots(
rows=len(rowWidth),
cols=1,
shared_xaxes=True,
row_width=rowWidth,
vertical_spacing=0.0001,
)
# todo should be optional
fig.append_trace(candles, 1, 1) fig.append_trace(candles, 1, 1)
fig.append_trace(bb_lower, 1, 1)
fig.append_trace(bb_middle, 1, 1)
fig.append_trace(bb_upper, 1, 1)
if 'bb_lowerband' in data and 'bb_upperband' in data:
bb_lower = go.Scatter(
x=data.date,
y=data.bb_lowerband,
name='BB lower',
line={'color': "transparent"},
)
bb_upper = go.Scatter(
x=data.date,
y=data.bb_upperband,
name='BB upper',
fill="tonexty",
fillcolor="rgba(0,176,246,0.2)",
line={'color': "transparent"},
)
fig.append_trace(bb_lower, 1, 1)
fig.append_trace(bb_upper, 1, 1)
fig = generate_row(fig=fig, row=1, raw_indicators=args.indicators1, data=data)
fig.append_trace(buys, 1, 1) fig.append_trace(buys, 1, 1)
fig.append_trace(sells, 1, 1) fig.append_trace(sells, 1, 1)
fig.append_trace(trade_buys, 1, 1)
fig.append_trace(trade_sells, 1, 1)
# Row 2 # append stop loss/profit
volume = go.Bar( plot_stop_loss_trade(df_sell, fig, analyze, args)
x=data['date'],
y=data['volume'],
name='Volume'
)
fig.append_trace(volume, 2, 1)
# Row 3 # plot other dataframes
fig = generate_row(fig=fig, row=3, raw_indicators=args.indicators2, data=data) plot_dataframes(data, fig, args)
plot_dataframes_markers(data, fig, args)
return fig fig['layout'].update(title=args.pair)
fig['layout']['yaxis1'].update(title='Price')
subplots = 1
def generate_row(fig, row, raw_indicators, data) -> tools.make_subplots: if args.plotvolume:
""" subplots = subplots + 1
Generator all the indicator selected by the user for a specific row plot_volume_dataframe(data, fig, args, subplots)
""" fig['layout']['yaxis' + str(subplots)].update(title='Volume')
for indicator in raw_indicators.split(','):
if indicator in data:
scattergl = go.Scattergl(
x=data['date'],
y=data[indicator],
name=indicator
)
fig.append_trace(scattergl, row, 1)
else:
logger.info(
'Indicator "%s" ignored. Reason: This indicator is not found '
'in your strategy.',
indicator
)
return fig if args.plotmacd:
subplots = subplots + 1
plot_macd_dataframe(data, fig, args, subplots)
fig['layout']['yaxis' + str(subplots)].update(title='MACD')
if args.plotrsi:
subplots = subplots + 1
plot_rsi_dataframe(data, fig, args, subplots)
fig['layout']['yaxis' + str(subplots)].update(title='RSI', range=[0, 100])
if args.plotcci:
subplots = subplots + 1
plot_cci_dataframe(data, fig, args, subplots)
fig['layout']['yaxis' + str(subplots)].update(title='CCI')
if args.plotosc:
subplots = subplots + 1
plot_osc_dataframe(data, fig, args, subplots)
fig['layout']['yaxis' + str(subplots)].update(title='OSC')
if args.plotcmf:
subplots = subplots + 1
plot_cmf_dataframe(data, fig, args, subplots)
fig['layout']['yaxis' + str(subplots)].update(title='CMF')
# updated all the
plot(fig, filename='freqtrade-plot.html')
def plot_parse_args(args: List[str]) -> Namespace: def plot_parse_args(args: List[str]) -> Namespace:
@ -282,24 +618,6 @@ def plot_parse_args(args: List[str]) -> Namespace:
""" """
arguments = Arguments(args, 'Graph dataframe') arguments = Arguments(args, 'Graph dataframe')
arguments.scripts_options() arguments.scripts_options()
arguments.parser.add_argument(
'--indicators1',
help='Set indicators from your strategy you want in the first row of the graph. Separate '
'them with a coma. E.g: ema3,ema5 (default: %(default)s)',
type=str,
default='sma,ema3,ema5',
dest='indicators1',
)
arguments.parser.add_argument(
'--indicators2',
help='Set indicators from your strategy you want in the third row of the graph. Separate '
'them with a coma. E.g: fastd,fastk (default: %(default)s)',
type=str,
default='macd',
dest='indicators2',
)
arguments.common_args_parser() arguments.common_args_parser()
arguments.optimizer_shared_options(arguments.parser) arguments.optimizer_shared_options(arguments.parser)
arguments.backtesting_options(arguments.parser) arguments.backtesting_options(arguments.parser)

View File

@ -121,7 +121,7 @@ def plot_profit(args: Namespace) -> None:
logger.info('Filter, keep pairs %s' % pairs) logger.info('Filter, keep pairs %s' % pairs)
tickers = optimize.load_data( tickers = optimize.load_data(
datadir=args.datadir, datadir=config.get('datadir'),
pairs=pairs, pairs=pairs,
ticker_interval=tick_interval, ticker_interval=tick_interval,
refresh_pairs=False, refresh_pairs=False,

337
serverless.yml Normal file
View File

@ -0,0 +1,337 @@
service: freq
frameworkVersion: ">=1.1.0 <2.0.0"
plugins:
- serverless-domain-manager
- serverless-python-requirements
############################################################################################
# configure out provider and the security guide lines
############################################################################################
provider:
name: aws
runtime: python3.6
region: us-east-2
#required permissions
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:*
Resource: "*"
- Effect: Allow
Action:
- SNS:*
Resource: { "Fn::Join" : [":", ["arn:aws:sns:${self:custom.region}", "*:*" ] ] }
- Effect: "Allow"
Action:
- ecs:RunTask
Resource: "*"
- Effect: Allow
Action:
- iam:PassRole
Resource: "*"
memorySize: 128
timeout: 90
versionFunctions: false
logRetentionInDays: 3
#where to store out data, needs to be manually created!
deploymentBucket:
name: lambdas-freq
# limit the invocations a bit to avoid overloading the server
usagePlan:
throttle:
burstLimit: 100
rateLimit: 50
############################################################################################
#custom configuration settings
############################################################################################
custom:
stage: ${opt:stage, self:provider.stage}
region: ${opt:region, self:provider.region}
snsTopic: "FreqQueue-${self:custom.stage}"
snsTradeTopic: "FreqTradeQueue-${self:custom.stage}"
tradeTable: "FreqTradesTable-${self:custom.stage}"
strategyTable: "FreqStrategyTable-${self:custom.stage}"
###
# custom domain management
###
customDomain:
basePath: "${self:custom.stage}"
domainName: "freq.isaac.international"
stage: "${self:custom.stage}"
createRoute53Record: true
pythonRequirements:
slim: true
invalidateCaches: true
dockerizePip: false
fileName: requirements-aws.txt
noDeploy:
- pytest
- moto
- plotly
- boto3
- pytest-mock
- pytest-cov
- pymongo
package:
exclude:
- test/**
- node_modules/**
- doc/**
- scripts/**
- bin
- freqtrade/tests/**
############################################################################################
# this section defines all lambda function and triggers
############################################################################################
functions:
#returns all known strategy names from the server
#and if they are private or not
strategies:
memorySize: 128
handler: freqtrade/aws/strategy.names
events:
- http:
path: strategies
method: get
cors: true
environment:
strategyTable: ${self:custom.strategyTable}
reservedConcurrency: 5
#returns the source code of this given strategy
#unless it's private
code:
memorySize: 128
handler: freqtrade/aws/strategy.code
events:
- http:
path: strategies/{user}/{name}/code
method: get
cors: true
integration: lambda
request:
parameter:
paths:
user: true
name: true
response:
headers:
Content-Type: "'text/plain'"
template: $input.path('$')
environment:
strategyTable: ${self:custom.strategyTable}
reservedConcurrency: 5
# loads the details of the specific strategy
get:
memorySize: 128
handler: freqtrade/aws/strategy.get
events:
- http:
path: strategies/{user}/{name}
method: get
cors: true
request:
parameter:
paths:
user: true
name: true
environment:
strategyTable: ${self:custom.strategyTable}
reservedConcurrency: 5
# loads the aggregation report for the given strategy based on different tickers
get_aggregate_interval:
memorySize: 128
handler: freqtrade/aws/aggregate/strategy.ticker
events:
- http:
path: strategies/{user}/{name}/aggregate/ticker
method: get
cors: true
request:
parameter:
paths:
user: true
name: true
environment:
strategyTable: ${self:custom.strategyTable}
tradeTable: ${self:custom.tradeTable}
reservedConcurrency: 5
# loads the aggregation report for the given strategy based on different tickers
get_aggregate_timeframe:
memorySize: 128
handler: freqtrade/aws/aggregate/strategy.timeframe
events:
- http:
path: strategies/{user}/{name}/aggregate/timeframe
method: get
cors: true
request:
parameter:
paths:
user: true
name: true
environment:
strategyTable: ${self:custom.strategyTable}
tradeTable: ${self:custom.tradeTable}
reservedConcurrency: 5
#submits a new strategy to the system
submit:
memorySize: 128
handler: freqtrade/aws/strategy.submit
events:
- http:
path: strategies/submit
method: post
cors: true
environment:
topic: ${self:custom.snsTopic}
strategyTable: ${self:custom.strategyTable}
BASE_URL: ${self:custom.customDomain.domainName}/${self:custom.customDomain.stage}
reservedConcurrency: 5
#submits a new strategy to the system
submit_github:
memorySize: 128
handler: freqtrade/aws/strategy.submit_github
events:
- http:
path: strategies/submit/github
method: post
cors: true
environment:
topic: ${self:custom.snsTopic}
strategyTable: ${self:custom.strategyTable}
reservedConcurrency: 1
### TRADE REQUESTS
# loads all trades for a strategy and it's associated pairs
trades:
memorySize: 128
handler: freqtrade/aws/trade.get_trades
events:
- http:
path: strategies/{user}/{name}/{stake}/{asset}
method: get
cors: true
request:
parameter:
paths:
user: true
name: true
stake: true
asset: true
environment:
strategyTable: ${self:custom.strategyTable}
tradeTable: ${self:custom.tradeTable}
reservedConcurrency: 5
# submits a new trade to the system
trade:
memorySize: 128
handler: freqtrade/aws/trade.submit
events:
- http:
path: trade
method: post
cors: true
environment:
tradeTopic: ${self:custom.snsTradeTopic}
reservedConcurrency: 5
# query aggregates by day and ticker for all strategies
trade-aggregate:
memorySize: 128
handler: freqtrade/aws/trade.get_aggregated_trades
events:
- http:
path: trades/aggregate/{ticker}/{days}
method: get
cors: true
request:
parameter:
paths:
ticker: true
days: true
environment:
tradeTable: ${self:custom.tradeTable}
reservedConcurrency: 5
### SNS TRIGGERED FUNCTIONS
# stores the received message in the trade table
trade-store:
memorySize: 128
handler: freqtrade/aws/trade.store
events:
- sns: ${self:custom.snsTradeTopic}
environment:
tradeTable: ${self:custom.tradeTable}
reservedConcurrency: 1
#backtests the strategy
#should be switched to utilze aws fargate instead
#and running a container
#so that we can evaluate long running tasks
backtest:
memorySize: 128
handler: freqtrade/aws/backtesting_lambda.backtest
events:
- sns: ${self:custom.snsTopic}
environment:
topic: ${self:custom.snsTopic}
tradeTable: ${self:custom.tradeTable}
strategyTable: ${self:custom.strategyTable}
BASE_URL: https://${self:custom.customDomain.domainName}/${self:custom.customDomain.stage}
reservedConcurrency: 1
# schedules all registered strategies on a daily base
schedule:
memorySize: 128
handler: freqtrade/aws/backtesting_lambda.cron
events:
- schedule:
rate: rate(1440 minutes)
enabled: true
environment:
topic: ${self:custom.snsTopic}
tradeTable: ${self:custom.tradeTable}
strategyTable: ${self:custom.strategyTable}
reservedConcurrency: 1

View File

@ -19,7 +19,7 @@ setup(name='freqtrade',
packages=['freqtrade'], packages=['freqtrade'],
scripts=['bin/freqtrade'], scripts=['bin/freqtrade'],
setup_requires=['pytest-runner'], setup_requires=['pytest-runner'],
tests_require=['pytest', 'pytest-mock', 'pytest-cov'], tests_require=['pytest', 'pytest-mock', 'pytest-cov', 'moto'],
install_requires=[ install_requires=[
'ccxt', 'ccxt',
'SQLAlchemy', 'SQLAlchemy',
@ -36,6 +36,8 @@ setup(name='freqtrade',
'tabulate', 'tabulate',
'cachetools', 'cachetools',
'coinmarketcap', 'coinmarketcap',
'boto3'
], ],
include_package_data=True, include_package_data=True,
zip_safe=False, zip_safe=False,

View File

@ -0,0 +1,94 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
import numpy # noqa
class Long(IStrategy):
"""
author@: Gert Wohlgemuth
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"60": 0.05,
"30": 0.06,
"20": 0.07,
"0": 0.08
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.15
# Optimal ticker interval for the strategy
ticker_interval = 60
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
dataframe['cci'] = ta.CCI(dataframe)
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=50)
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
# RSI
dataframe['rsi'] = ta.RSI(dataframe)
# Inverse Fisher transform on RSI, values [-1.0, 1.0] (https://goo.gl/2JGGoy)
rsi = 0.1 * (dataframe['rsi'] - 50)
dataframe['fisher_rsi'] = (numpy.exp(2 * rsi) - 1) / (numpy.exp(2 * rsi) + 1)
# SAR Parabol
dataframe['sar'] = ta.SAR(dataframe)
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
(dataframe['macd'] > dataframe['macdsignal']) &
(dataframe['macd'] > 0) &
(dataframe['cci'] <= 0.0)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
# (dataframe['tema'] < dataframe['close'])
(dataframe['sar'] > dataframe['close']) &
(dataframe['fisher_rsi'] > 0.3)
),
'sell'] = 1
return dataframe

View File

@ -0,0 +1,75 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class Quickie(IStrategy):
"""
author@: Gert Wohlgemuth
idea:
momentum based strategie. The main idea is that it closes trades very quickly, while avoiding excessive losses. Hence a rather moderate stop loss in this case
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"60": 0.005,
"10": 0.01,
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.25
# Optimal ticker interval for the strategy
ticker_interval = 5
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
dataframe['tema'] = ta.TEMA(dataframe, timeperiod=9)
dataframe['adx'] = ta.ADX(dataframe)
dataframe['sma_200'] = ta.SMA(dataframe, timeperiod=200)
dataframe['sma_50'] = ta.SMA(dataframe, timeperiod=50)
# required for graphing
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
(
(dataframe['adx'] > 30) &
(dataframe['tema'] < dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) &
(dataframe['sma_200'] > dataframe['close'])
)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
(
(dataframe['adx'] > 70) &
(dataframe['tema'] > dataframe['bb_middleband']) &
(dataframe['tema'] < dataframe['tema'].shift(1))
)
),
'sell'] = 1
return dataframe

View File

@ -0,0 +1,76 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class Simple(IStrategy):
"""
author@: Gert Wohlgemuth
idea:
this strategy is based on the book, 'The Simple Strategy' and can be found in detail here:
https://www.amazon.com/Simple-Strategy-Powerful-Trading-Futures-ebook/dp/B00E66QPCG/ref=sr_1_1?ie=UTF8&qid=1525202675&sr=8-1&keywords=the+simple+strategy
"""
# Minimal ROI designed for the strategy.
# since this strategy is planned around 5 minutes, we assume any time we have a 5% profit we should call it a day
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"0": 0.01
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.25
# Optimal ticker interval for the strategy
ticker_interval = 5
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# RSI
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=7)
# required for graphing
bollinger = qtpylib.bollinger_bands(dataframe['close'], window=12, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['bb_middleband'] = bollinger['mid']
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
dataframe.loc[
(
(
(dataframe['macd'] > 0) # over 0
& (dataframe['macd'] > dataframe['macdsignal']) # over signal
& (dataframe['bb_upperband'] > dataframe['bb_upperband'].shift(1)) # pointed up
& (dataframe['rsi'] > 70) # optional filter, need to investigate
)
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
# different strategy used for sell points, due to be able to duplicate it to 100%
dataframe.loc[
(
(dataframe['rsi'] > 80)
),
'sell'] = 1
return dataframe

View File

@ -0,0 +1,90 @@
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from typing import Dict, List
from hyperopt import hp
from functools import reduce
from pandas import DataFrame
# --------------------------------
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib
class ZLC(IStrategy):
"""
author@: Gert Wohlgemuth
"""
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi"
minimal_roi = {
"60": 0.01,
"30": 0.03,
"20": 0.04,
"0": 0.01
}
# Optimal stoploss designed for the strategy
# This attribute will be overridden if the config file contains "stoploss"
stoploss = -0.3
# Optimal ticker interval for the strategy
ticker_interval = 5
def populate_indicators(self, dataframe: DataFrame) -> DataFrame:
dataframe['cci-slow'] = ta.CCI(dataframe, timeperiod=25)
dataframe['cci-fast'] = ta.CCI(dataframe, timeperiod=50)
dataframe['expo'] = ta.EMA(dataframe, timeperiod=35)
# required for graphing
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
return dataframe
def populate_buy_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(
#don't buy on peak tops
(dataframe['close'] < dataframe['bb_middleband'])
# this is the main concept of evaluating buys
& (dataframe['cci-fast'] > 0)
& (dataframe['cci-slow'] > 0)
& (dataframe['close'] > dataframe['expo'])
)
,
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:return: DataFrame with buy column
"""
dataframe.loc[
(dataframe['close'] >= dataframe['bb_upperband']) |
(
(dataframe['cci-fast'] < 0)
& (dataframe['cci-slow'] < 0)
& (dataframe['close'] < dataframe['expo'])
)
,
'sell'] = 0
return dataframe