Merge branch 'develop' into feature/volume-precision-pairlist
This commit is contained in:
commit
786244c0d3
197
docs/edge.md
197
docs/edge.md
@ -3,159 +3,198 @@
|
||||
This page explains how to use Edge Positioning module in your bot in order to enter into a trade only if the trade has a reasonable win rate and risk reward ratio, and consequently adjust your position size and stoploss.
|
||||
|
||||
!!! Warning
|
||||
Edge positioning is not compatible with dynamic whitelist. it overrides dynamic whitelist.
|
||||
Edge positioning is not compatible with dynamic whitelist. If enabled, it overrides the dynamic whitelist option.
|
||||
|
||||
!!! Note
|
||||
Edge won't consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else will be ignored in its calculation.
|
||||
Edge does not consider anything else than buy/sell/stoploss signals. So trailing stoploss, ROI, and everything else are ignored in its calculation.
|
||||
|
||||
## Introduction
|
||||
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.<br/><br/>
|
||||
But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: You give me 10$. Is it an interesting game ? no, it is quite boring, isn't it?<br/><br/>
|
||||
But let's say the probability that we have heads is 80%, and the probability that we have tails is 20%. Now it is becoming interesting ...
|
||||
That means 10$ x 80% versus 10$ x 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin.<br/><br/>
|
||||
Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% * 2$ versus 20% * 8$. It is becoming boring again because overtime you win $1.6$ (80% x 2$) and me $1.6 (20% * 8$) too.<br/><br/>
|
||||
The question is: How do you calculate that? how do you know if you wanna play?
|
||||
Trading is all about probability. No one can claim that he has a strategy working all the time. You have to assume that sometimes you lose.
|
||||
|
||||
But it doesn't mean there is no rule, it only means rules should work "most of the time". Let's play a game: we toss a coin, heads: I give you 10$, tails: you give me 10$. Is it an interesting game? No, it's quite boring, isn't it?
|
||||
|
||||
But let's say the probability that we have heads is 80% (because our coin has the displaced distribution of mass or other defect), and the probability that we have tails is 20%. Now it is becoming interesting...
|
||||
|
||||
That means 10$ X 80% versus 10$ X 20%. 8$ versus 2$. That means over time you will win 8$ risking only 2$ on each toss of coin.
|
||||
|
||||
Let's complicate it more: you win 80% of the time but only 2$, I win 20% of the time but 8$. The calculation is: 80% X 2$ versus 20% X 8$. It is becoming boring again because overtime you win $1.6$ (80% X 2$) and me $1.6 (20% X 8$) too.
|
||||
|
||||
The question is: How do you calculate that? How do you know if you wanna play?
|
||||
|
||||
The answer comes to two factors:
|
||||
- Win Rate
|
||||
- Risk Reward Ratio
|
||||
|
||||
|
||||
### Win Rate
|
||||
Means over X trades what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only If you won or not).
|
||||
Win Rate (*W*) is is the mean over some amount of trades (*N*) what is the percentage of winning trades to total number of trades (note that we don't consider how much you gained but only if you won or not).
|
||||
|
||||
W = (Number of winning trades) / (Total number of trades) = (Number of winning trades) / N
|
||||
|
||||
`W = (Number of winning trades) / (Total number of trades)`
|
||||
Complementary Loss Rate (*L*) is defined as
|
||||
|
||||
L = (Number of losing trades) / (Total number of trades) = (Number of losing trades) / N
|
||||
|
||||
or, which is the same, as
|
||||
|
||||
R = 1 – W
|
||||
|
||||
### Risk Reward Ratio
|
||||
Risk Reward Ratio is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
|
||||
Risk Reward Ratio (*R*) is a formula used to measure the expected gains of a given investment against the risk of loss. It is basically what you potentially win divided by what you potentially lose:
|
||||
|
||||
`R = Profit / Loss`
|
||||
R = Profit / Loss
|
||||
|
||||
Over time, on many trades, you can calculate your risk reward by dividing your average profit on winning trades by your average loss on losing trades:
|
||||
|
||||
`Average profit = (Sum of profits) / (Number of winning trades)`
|
||||
Average profit = (Sum of profits) / (Number of winning trades)
|
||||
|
||||
`Average loss = (Sum of losses) / (Number of losing trades)`
|
||||
Average loss = (Sum of losses) / (Number of losing trades)
|
||||
|
||||
`R = (Average profit) / (Average loss)`
|
||||
R = (Average profit) / (Average loss)
|
||||
|
||||
### Expectancy
|
||||
At this point we can combine *W* and *R* to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades and subtracting the percentage of losing trades, which is calculated as follows:
|
||||
|
||||
At this point we can combine W and R to create an expectancy ratio. This is a simple process of multiplying the risk reward ratio by the percentage of winning trades, and subtracting the percentage of losing trades, which is calculated as follows:
|
||||
|
||||
Expectancy Ratio = (Risk Reward Ratio x Win Rate) – Loss Rate
|
||||
Expectancy Ratio = (Risk Reward Ratio X Win Rate) – Loss Rate = (R X W) – L
|
||||
|
||||
So lets say your Win rate is 28% and your Risk Reward Ratio is 5:
|
||||
|
||||
`Expectancy = (5 * 0.28) - 0.72 = 0.68`
|
||||
Expectancy = (5 X 0.28) – 0.72 = 0.68
|
||||
|
||||
Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your losers. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||
Superficially, this means that on average you expect this strategy’s trades to return .68 times the size of your loses. This is important for two reasons: First, it may seem obvious, but you know right away that you have a positive return. Second, you now have a number you can compare to other candidate systems to make decisions about which ones you employ.
|
||||
|
||||
It is important to remember that any system with an expectancy greater than 0 is profitable using past data. The key is finding one that will be profitable in the future.
|
||||
|
||||
You can also use this number to evaluate the effectiveness of modifications to this system.
|
||||
You can also use this value to evaluate the effectiveness of modifications to this system.
|
||||
|
||||
**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data , there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||
**NOTICE:** It's important to keep in mind that Edge is testing your expectancy using historical data, there's no guarantee that you will have a similar edge in the future. It's still vital to do this testing in order to build confidence in your methodology, but be wary of "curve-fitting" your approach to the historical data as things are unlikely to play out the exact same way for future trades.
|
||||
|
||||
## How does it work?
|
||||
If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over X trades for each stoploss. Here is an example:
|
||||
If enabled in config, Edge will go through historical data with a range of stoplosses in order to find buy and sell/stoploss signals. It then calculates win rate and expectancy over *N* trades for each stoploss. Here is an example:
|
||||
|
||||
| Pair | Stoploss | Win Rate | Risk Reward Ratio | Expectancy |
|
||||
|----------|:-------------:|-------------:|------------------:|-----------:|
|
||||
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||
| XZC/ETH | -0.01 | 0.50 |1.176384 | 0.088 |
|
||||
| XZC/ETH | -0.02 | 0.51 |1.115941 | 0.079 |
|
||||
| XZC/ETH | -0.03 | 0.52 |1.359670 | 0.228 |
|
||||
| XZC/ETH | -0.04 | 0.51 |1.234539 | 0.117 |
|
||||
|
||||
The goal here is to find the best stoploss for the strategy in order to have the maximum expectancy. In the above example stoploss at 3% leads to the maximum expectancy according to historical data.
|
||||
|
||||
Edge then forces stoploss to your strategy dynamically.
|
||||
Edge module then forces stoploss value it evaluated to your strategy dynamically.
|
||||
|
||||
### Position size
|
||||
Edge dictates the stake amount for each trade to the bot according to the following factors:
|
||||
Edge also dictates the stake amount for each trade to the bot according to the following factors:
|
||||
|
||||
- Allowed capital at risk
|
||||
- Stoploss
|
||||
|
||||
Allowed capital at risk is calculated as follows:
|
||||
|
||||
**allowed capital at risk** = **capital_available_percentage** X **allowed risk per trade**
|
||||
Allowed capital at risk = (Capital available_percentage) X (Allowed risk per trade)
|
||||
|
||||
**Stoploss** is calculated as described above against historical data.
|
||||
Stoploss is calculated as described above against historical data.
|
||||
|
||||
Your position size then will be:
|
||||
|
||||
**position size** = **allowed capital at risk** / **stoploss**
|
||||
Position size = (Allowed capital at risk) / Stoploss
|
||||
|
||||
Example:<br/>
|
||||
Let's say the stake currency is ETH and you have 10 ETH on the exchange, your **capital_available_percentage** is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**. <br/>
|
||||
Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5ETH**.<br/>
|
||||
Bot takes a position of 2.5ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on BTC/ETH market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25ETH (call it trade 2).<br/>
|
||||
Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.<br/>
|
||||
Now you have two trades open. The Bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5ETH**. But there are already 4ETH blocked in two previous trades. So the position size for this third trade would be 1ETH.<br/>
|
||||
Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5ETH. <br/>
|
||||
So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75**.
|
||||
Example:
|
||||
|
||||
Let's say the stake currency is ETH and you have 10 ETH on the exchange, your capital available percentage is 50% and you would allow 1% of risk for each trade. thus your available capital for trading is **10 x 0.5 = 5 ETH** and allowed capital at risk would be **5 x 0.01 = 0.05 ETH**.
|
||||
|
||||
Let's assume Edge has calculated that for **XLM/ETH** market your stoploss should be at 2%. So your position size will be **0.05 / 0.02 = 2.5 ETH**.
|
||||
|
||||
Bot takes a position of 2.5 ETH on XLM/ETH (call it trade 1). Up next, you receive another buy signal while trade 1 is still open. This time on **BTC/ETH** market. Edge calculated stoploss for this market at 4%. So your position size would be 0.05 / 0.04 = 1.25 ETH (call it trade 2).
|
||||
|
||||
Note that available capital for trading didn’t change for trade 2 even if you had already trade 1. The available capital doesn’t mean the free amount on your wallet.
|
||||
|
||||
Now you have two trades open. The bot receives yet another buy signal for another market: **ADA/ETH**. This time the stoploss is calculated at 1%. So your position size is **0.05 / 0.01 = 5 ETH**. But there are already 3.75 ETH blocked in two previous trades. So the position size for this third trade would be **5 – 3.75 = 1.25 ETH**.
|
||||
|
||||
Available capital doesn’t change before a position is sold. Let’s assume that trade 1 receives a sell signal and it is sold with a profit of 1 ETH. Your total capital on exchange would be 11 ETH and the available capital for trading becomes 5.5 ETH.
|
||||
|
||||
So the Bot receives another buy signal for trade 4 with a stoploss at 2% then your position size would be **0.055 / 0.02 = 2.75 ETH**.
|
||||
|
||||
## Configurations
|
||||
Edge has following configurations:
|
||||
Edge module has following configuration options:
|
||||
|
||||
#### enabled
|
||||
If true, then Edge will run periodically.<br/>
|
||||
(default to false)
|
||||
If true, then Edge will run periodically.
|
||||
|
||||
(defaults to false)
|
||||
|
||||
#### process_throttle_secs
|
||||
How often should Edge run in seconds? <br/>
|
||||
(default to 3600 so one hour)
|
||||
How often should Edge run in seconds?
|
||||
|
||||
(defaults to 3600 so one hour)
|
||||
|
||||
#### calculate_since_number_of_days
|
||||
Number of days of data against which Edge calculates Win Rate, Risk Reward and Expectancy
|
||||
Note that it downloads historical data so increasing this number would lead to slowing down the bot.<br/>
|
||||
(default to 7)
|
||||
Note that it downloads historical data so increasing this number would lead to slowing down the bot.
|
||||
|
||||
(defaults to 7)
|
||||
|
||||
#### capital_available_percentage
|
||||
This is the percentage of the total capital on exchange in stake currency. <br/>
|
||||
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.<br/>
|
||||
(default to 0.5)
|
||||
This is the percentage of the total capital on exchange in stake currency.
|
||||
|
||||
As an example if you have 10 ETH available in your wallet on the exchange and this value is 0.5 (which is 50%), then the bot will use a maximum amount of 5 ETH for trading and considers it as available capital.
|
||||
|
||||
(defaults to 0.5)
|
||||
|
||||
#### allowed_risk
|
||||
Percentage of allowed risk per trade.<br/>
|
||||
(default to 0.01 [1%])
|
||||
Percentage of allowed risk per trade.
|
||||
|
||||
(defaults to 0.01 so 1%)
|
||||
|
||||
#### stoploss_range_min
|
||||
Minimum stoploss.<br/>
|
||||
(default to -0.01)
|
||||
Minimum stoploss.
|
||||
|
||||
(defaults to -0.01)
|
||||
|
||||
#### stoploss_range_max
|
||||
Maximum stoploss.<br/>
|
||||
(default to -0.10)
|
||||
Maximum stoploss.
|
||||
|
||||
(defaults to -0.10)
|
||||
|
||||
#### stoploss_range_step
|
||||
As an example if this is set to -0.01 then Edge will test the strategy for [-0.01, -0,02, -0,03 ..., -0.09, -0.10] ranges.
|
||||
Note than having a smaller step means having a bigger range which could lead to slow calculation. <br/>
|
||||
if you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10. <br/>
|
||||
(default to -0.01)
|
||||
As an example if this is set to -0.01 then Edge will test the strategy for \[-0.01, -0,02, -0,03 ..., -0.09, -0.10\] ranges.
|
||||
Note than having a smaller step means having a bigger range which could lead to slow calculation.
|
||||
|
||||
If you set this parameter to -0.001, you then slow down the Edge calculation by a factor of 10.
|
||||
|
||||
(defaults to -0.01)
|
||||
|
||||
#### minimum_winrate
|
||||
It filters pairs which don't have at least minimum_winrate.
|
||||
This comes handy if you want to be conservative and don't comprise win rate in favor of risk reward ratio.<br/>
|
||||
(default to 0.60)
|
||||
It filters out pairs which don't have at least minimum_winrate.
|
||||
|
||||
This comes handy if you want to be conservative and don't comprise win rate in favour of risk reward ratio.
|
||||
|
||||
(defaults to 0.60)
|
||||
|
||||
#### minimum_expectancy
|
||||
It filters paris which have an expectancy lower than this number .
|
||||
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.<br/>
|
||||
(default to 0.20)
|
||||
It filters out pairs which have the expectancy lower than this number.
|
||||
|
||||
Having an expectancy of 0.20 means if you put 10$ on a trade you expect a 12$ return.
|
||||
|
||||
(defaults to 0.20)
|
||||
|
||||
#### min_trade_number
|
||||
When calculating W and R and E (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable. Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something. <br/>
|
||||
(default to 10, it is highly recommended not to decrease this number)
|
||||
When calculating *W*, *R* and *E* (expectancy) against historical data, you always want to have a minimum number of trades. The more this number is the more Edge is reliable.
|
||||
|
||||
Having a win rate of 100% on a single trade doesn't mean anything at all. But having a win rate of 70% over past 100 trades means clearly something.
|
||||
|
||||
(defaults to 10, it is highly recommended not to decrease this number)
|
||||
|
||||
#### max_trade_duration_minute
|
||||
Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.<br/>
|
||||
**NOTICE:** While configuring this value, you should take into consideration your ticker interval. as an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. default value is set assuming your strategy interval is relatively small (1m or 5m, etc).<br/>
|
||||
(default to 1 day, 1440 = 60 * 24)
|
||||
Edge will filter out trades with long duration. If a trade is profitable after 1 month, it is hard to evaluate the strategy based on it. But if most of trades are profitable and they have maximum duration of 30 minutes, then it is clearly a good sign.
|
||||
|
||||
**NOTICE:** While configuring this value, you should take into consideration your ticker interval. As an example filtering out trades having duration less than one day for a strategy which has 4h interval does not make sense. Default value is set assuming your strategy interval is relatively small (1m or 5m, etc.).
|
||||
|
||||
(defaults to 1 day, i.e. to 60 * 24 = 1440 minutes)
|
||||
|
||||
#### remove_pumps
|
||||
Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.<br/>
|
||||
(default to false)
|
||||
Edge will remove sudden pumps in a given market while going through historical data. However, given that pumps happen very often in crypto markets, we recommend you keep this off.
|
||||
|
||||
(defaults to false)
|
||||
|
||||
|
||||
## Running Edge independently
|
||||
@ -199,14 +238,14 @@ python3 ./freqtrade/main.py edge --stoplosses=-0.01,-0.1,-0.001 #min,max,step
|
||||
python3 ./freqtrade/main.py edge --timerange=20181110-20181113
|
||||
```
|
||||
|
||||
Doing --timerange=-200 will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop.
|
||||
Doing `--timerange=-200` will get the last 200 timeframes from your inputdata. You can also specify specific dates, or a range span indexed by start and stop.
|
||||
|
||||
The full timerange specification:
|
||||
|
||||
* Use last 123 tickframes of data: --timerange=-123
|
||||
* Use first 123 tickframes of data: --timerange=123-
|
||||
* Use tickframes from line 123 through 456: --timerange=123-456
|
||||
* Use tickframes till 2018/01/31: --timerange=-20180131
|
||||
* Use tickframes since 2018/01/31: --timerange=20180131-
|
||||
* Use tickframes since 2018/01/31 till 2018/03/01 : --timerange=20180131-20180301
|
||||
* Use tickframes between POSIX timestamps 1527595200 1527618600: --timerange=1527595200-1527618600
|
||||
* Use last 123 tickframes of data: `--timerange=-123`
|
||||
* Use first 123 tickframes of data: `--timerange=123-`
|
||||
* Use tickframes from line 123 through 456: `--timerange=123-456`
|
||||
* Use tickframes till 2018/01/31: `--timerange=-20180131`
|
||||
* Use tickframes since 2018/01/31: `--timerange=20180131-`
|
||||
* Use tickframes since 2018/01/31 till 2018/03/01 : `--timerange=20180131-20180301`
|
||||
* Use tickframes between POSIX timestamps 1527595200 1527618600: `--timerange=1527595200-1527618600`
|
||||
|
@ -41,7 +41,6 @@ Possible parameters are:
|
||||
|
||||
* exchange
|
||||
* pair
|
||||
* market_url
|
||||
* limit
|
||||
* stake_amount
|
||||
* stake_amount_fiat
|
||||
@ -56,7 +55,6 @@ Possible parameters are:
|
||||
* exchange
|
||||
* pair
|
||||
* gain
|
||||
* market_url
|
||||
* limit
|
||||
* amount
|
||||
* open_rate
|
||||
|
@ -6,9 +6,7 @@ import argparse
|
||||
import os
|
||||
import re
|
||||
from typing import List, NamedTuple, Optional
|
||||
|
||||
import arrow
|
||||
|
||||
from freqtrade import __version__, constants
|
||||
|
||||
|
||||
@ -55,6 +53,11 @@ class Arguments(object):
|
||||
"""
|
||||
parsed_arg = self.parser.parse_args(self.args)
|
||||
|
||||
# Workaround issue in argparse with action='append' and default value
|
||||
# (see https://bugs.python.org/issue16399)
|
||||
if parsed_arg.config is None:
|
||||
parsed_arg.config = [constants.DEFAULT_CONFIG]
|
||||
|
||||
return parsed_arg
|
||||
|
||||
def common_args_parser(self) -> None:
|
||||
@ -63,7 +66,7 @@ class Arguments(object):
|
||||
"""
|
||||
self.parser.add_argument(
|
||||
'-v', '--verbose',
|
||||
help='verbose mode (-vv for more, -vvv to get all messages)',
|
||||
help='Verbose mode (-vv for more, -vvv to get all messages).',
|
||||
action='count',
|
||||
dest='loglevel',
|
||||
default=0,
|
||||
@ -75,15 +78,16 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-c', '--config',
|
||||
help='specify configuration file (default: %(default)s)',
|
||||
help='Specify configuration file (default: %(default)s). '
|
||||
'Multiple --config options may be used.',
|
||||
dest='config',
|
||||
default='config.json',
|
||||
action='append',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-d', '--datadir',
|
||||
help='path to backtest data',
|
||||
help='Path to backtest data.',
|
||||
dest='datadir',
|
||||
default=None,
|
||||
type=str,
|
||||
@ -91,7 +95,7 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'-s', '--strategy',
|
||||
help='specify strategy class name (default: %(default)s)',
|
||||
help='Specify strategy class name (default: %(default)s).',
|
||||
dest='strategy',
|
||||
default='DefaultStrategy',
|
||||
type=str,
|
||||
@ -99,14 +103,14 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'--strategy-path',
|
||||
help='specify additional strategy lookup path',
|
||||
help='Specify additional strategy lookup path.',
|
||||
dest='strategy_path',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'--customhyperopt',
|
||||
help='specify hyperopt class name (default: %(default)s)',
|
||||
help='Specify hyperopt class name (default: %(default)s).',
|
||||
dest='hyperopt',
|
||||
default=constants.DEFAULT_HYPEROPT,
|
||||
type=str,
|
||||
@ -114,8 +118,8 @@ class Arguments(object):
|
||||
)
|
||||
self.parser.add_argument(
|
||||
'--dynamic-whitelist',
|
||||
help='dynamically generate and update whitelist'
|
||||
' based on 24h BaseVolume (default: %(const)s)'
|
||||
help='Dynamically generate and update whitelist'
|
||||
' based on 24h BaseVolume (default: %(const)s).'
|
||||
' DEPRECATED.',
|
||||
dest='dynamic_whitelist',
|
||||
const=constants.DYNAMIC_WHITELIST,
|
||||
@ -126,7 +130,7 @@ class Arguments(object):
|
||||
self.parser.add_argument(
|
||||
'--db-url',
|
||||
help='Override trades database URL, this is useful if dry_run is enabled'
|
||||
' or in custom deployments (default: %(default)s)',
|
||||
' or in custom deployments (default: %(default)s).',
|
||||
dest='db_url',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
@ -139,7 +143,7 @@ class Arguments(object):
|
||||
"""
|
||||
parser.add_argument(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking)',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
action='store_true',
|
||||
dest='position_stacking',
|
||||
default=False
|
||||
@ -148,20 +152,20 @@ class Arguments(object):
|
||||
parser.add_argument(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number)',
|
||||
'(same as setting `max_open_trades` to a very high number).',
|
||||
action='store_false',
|
||||
dest='use_max_market_positions',
|
||||
default=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'-l', '--live',
|
||||
help='using live data',
|
||||
help='Use live data.',
|
||||
action='store_true',
|
||||
dest='live',
|
||||
)
|
||||
parser.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
||||
'exchange. Use it if you want to run your backtesting with up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
@ -178,8 +182,8 @@ class Arguments(object):
|
||||
)
|
||||
parser.add_argument(
|
||||
'--export',
|
||||
help='export backtest results, argument are: trades\
|
||||
Example --export=trades',
|
||||
help='Export backtest results, argument are: trades. '
|
||||
'Example --export=trades',
|
||||
type=str,
|
||||
default=None,
|
||||
dest='export',
|
||||
@ -203,14 +207,14 @@ class Arguments(object):
|
||||
"""
|
||||
parser.add_argument(
|
||||
'-r', '--refresh-pairs-cached',
|
||||
help='refresh the pairs files in tests/testdata with the latest data from the '
|
||||
help='Refresh the pairs files in tests/testdata with the latest data from the '
|
||||
'exchange. Use it if you want to run your edge with up-to-date data.',
|
||||
action='store_true',
|
||||
dest='refresh_pairs',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--stoplosses',
|
||||
help='defines a range of stoploss against which edge will assess the strategy '
|
||||
help='Defines a range of stoploss against which edge will assess the strategy '
|
||||
'the format is "min,max,step" (without any space).'
|
||||
'example: --stoplosses=-0.01,-0.1,-0.001',
|
||||
type=str,
|
||||
@ -226,14 +230,14 @@ class Arguments(object):
|
||||
"""
|
||||
parser.add_argument(
|
||||
'-i', '--ticker-interval',
|
||||
help='specify ticker interval (1m, 5m, 30m, 1h, 1d)',
|
||||
help='Specify ticker interval (1m, 5m, 30m, 1h, 1d).',
|
||||
dest='ticker_interval',
|
||||
type=str,
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'--timerange',
|
||||
help='specify what timerange of data to use.',
|
||||
help='Specify what timerange of data to use.',
|
||||
default=None,
|
||||
type=str,
|
||||
dest='timerange',
|
||||
@ -246,7 +250,7 @@ class Arguments(object):
|
||||
"""
|
||||
parser.add_argument(
|
||||
'--eps', '--enable-position-stacking',
|
||||
help='Allow buying the same pair multiple times (position stacking)',
|
||||
help='Allow buying the same pair multiple times (position stacking).',
|
||||
action='store_true',
|
||||
dest='position_stacking',
|
||||
default=False
|
||||
@ -255,14 +259,14 @@ class Arguments(object):
|
||||
parser.add_argument(
|
||||
'--dmmp', '--disable-max-market-positions',
|
||||
help='Disable applying `max_open_trades` during backtest '
|
||||
'(same as setting `max_open_trades` to a very high number)',
|
||||
'(same as setting `max_open_trades` to a very high number).',
|
||||
action='store_false',
|
||||
dest='use_max_market_positions',
|
||||
default=True
|
||||
)
|
||||
parser.add_argument(
|
||||
'-e', '--epochs',
|
||||
help='specify number of epochs (default: %(default)d)',
|
||||
help='Specify number of epochs (default: %(default)d).',
|
||||
dest='epochs',
|
||||
default=constants.HYPEROPT_EPOCH,
|
||||
type=int,
|
||||
@ -271,7 +275,7 @@ class Arguments(object):
|
||||
parser.add_argument(
|
||||
'-s', '--spaces',
|
||||
help='Specify which parameters to hyperopt. Space separate list. \
|
||||
Default: %(default)s',
|
||||
Default: %(default)s.',
|
||||
choices=['all', 'buy', 'sell', 'roi', 'stoploss'],
|
||||
default='all',
|
||||
nargs='+',
|
||||
@ -288,19 +292,19 @@ class Arguments(object):
|
||||
subparsers = self.parser.add_subparsers(dest='subparser')
|
||||
|
||||
# Add backtesting subcommand
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='backtesting module')
|
||||
backtesting_cmd = subparsers.add_parser('backtesting', help='Backtesting module.')
|
||||
backtesting_cmd.set_defaults(func=backtesting.start)
|
||||
self.optimizer_shared_options(backtesting_cmd)
|
||||
self.backtesting_options(backtesting_cmd)
|
||||
|
||||
# Add edge subcommand
|
||||
edge_cmd = subparsers.add_parser('edge', help='edge module')
|
||||
edge_cmd = subparsers.add_parser('edge', help='Edge module.')
|
||||
edge_cmd.set_defaults(func=edge_cli.start)
|
||||
self.optimizer_shared_options(edge_cmd)
|
||||
self.edge_options(edge_cmd)
|
||||
|
||||
# Add hyperopt subcommand
|
||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='hyperopt module')
|
||||
hyperopt_cmd = subparsers.add_parser('hyperopt', help='Hyperopt module.')
|
||||
hyperopt_cmd.set_defaults(func=hyperopt.start)
|
||||
self.optimizer_shared_options(hyperopt_cmd)
|
||||
self.hyperopt_options(hyperopt_cmd)
|
||||
@ -364,7 +368,7 @@ class Arguments(object):
|
||||
"""
|
||||
self.parser.add_argument(
|
||||
'--pairs-file',
|
||||
help='File containing a list of pairs to download',
|
||||
help='File containing a list of pairs to download.',
|
||||
dest='pairs_file',
|
||||
default=None,
|
||||
metavar='PATH',
|
||||
@ -372,7 +376,7 @@ class Arguments(object):
|
||||
|
||||
self.parser.add_argument(
|
||||
'--export',
|
||||
help='Export files to given dir',
|
||||
help='Export files to given dir.',
|
||||
dest='export',
|
||||
default=None,
|
||||
metavar='PATH',
|
||||
@ -380,16 +384,17 @@ class Arguments(object):
|
||||
|
||||
self.parser.add_argument(
|
||||
'-c', '--config',
|
||||
help='specify configuration file, used for additional exchange parameters',
|
||||
help='Specify configuration file (default: %(default)s). '
|
||||
'Multiple --config options may be used.',
|
||||
dest='config',
|
||||
default=None,
|
||||
action='append',
|
||||
type=str,
|
||||
metavar='PATH',
|
||||
)
|
||||
|
||||
self.parser.add_argument(
|
||||
'--days',
|
||||
help='Download data for number of days',
|
||||
help='Download data for given number of days.',
|
||||
dest='days',
|
||||
type=int,
|
||||
metavar='INT',
|
||||
@ -398,7 +403,7 @@ class Arguments(object):
|
||||
|
||||
self.parser.add_argument(
|
||||
'--exchange',
|
||||
help='Exchange name (default: %(default)s). Only valid if no config is provided',
|
||||
help='Exchange name (default: %(default)s). Only valid if no config is provided.',
|
||||
dest='exchange',
|
||||
type=str,
|
||||
default='bittrex'
|
||||
@ -407,7 +412,7 @@ class Arguments(object):
|
||||
self.parser.add_argument(
|
||||
'-t', '--timeframes',
|
||||
help='Specify which tickers to download. Space separated list. \
|
||||
Default: %(default)s',
|
||||
Default: %(default)s.',
|
||||
choices=['1m', '3m', '5m', '15m', '30m', '1h', '2h', '4h',
|
||||
'6h', '8h', '12h', '1d', '3d', '1w'],
|
||||
default=['1m', '5m'],
|
||||
@ -417,7 +422,7 @@ class Arguments(object):
|
||||
|
||||
self.parser.add_argument(
|
||||
'--erase',
|
||||
help='Clean all existing data for the selected exchange/pairs/timeframes',
|
||||
help='Clean all existing data for the selected exchange/pairs/timeframes.',
|
||||
dest='erase',
|
||||
action='store_true'
|
||||
)
|
||||
|
@ -13,6 +13,8 @@ from jsonschema.exceptions import ValidationError, best_match
|
||||
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.state import RunMode
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ -45,8 +47,18 @@ class Configuration(object):
|
||||
Extract information for sys.argv and load the bot configuration
|
||||
:return: Configuration dictionary
|
||||
"""
|
||||
logger.info('Using config: %s ...', self.args.config)
|
||||
config = self._load_config_file(self.args.config)
|
||||
config: Dict[str, Any] = {}
|
||||
# Now expecting a list of config filenames here, not a string
|
||||
for path in self.args.config:
|
||||
logger.info('Using config: %s ...', path)
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(self._load_config_file(path), config)
|
||||
|
||||
if 'internals' not in config:
|
||||
config['internals'] = {}
|
||||
|
||||
logger.info('Validating configuration ...')
|
||||
self._validate_config(config)
|
||||
|
||||
# Set strategy if not specified in config and or if it's non default
|
||||
if self.args.strategy != constants.DEFAULT_STRATEGY or not config.get('strategy'):
|
||||
@ -93,11 +105,7 @@ class Configuration(object):
|
||||
f'Config file "{path}" not found!'
|
||||
' Please create a config file or check whether it exists.')
|
||||
|
||||
if 'internals' not in conf:
|
||||
conf['internals'] = {}
|
||||
logger.info('Validating configuration ...')
|
||||
|
||||
return self._validate_config(conf)
|
||||
return conf
|
||||
|
||||
def _load_common_config(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
|
@ -3,6 +3,7 @@
|
||||
"""
|
||||
bot constants
|
||||
"""
|
||||
DEFAULT_CONFIG = 'config.json'
|
||||
DYNAMIC_WHITELIST = 20 # pairs
|
||||
PROCESS_THROTTLE_SECS = 5 # sec
|
||||
TICKER_INTERVAL = 5 # min
|
||||
|
@ -351,91 +351,93 @@ class Edge():
|
||||
return result
|
||||
|
||||
def _detect_next_stop_or_sell_point(self, buy_column, sell_column, date_column,
|
||||
ohlc_columns, stoploss, pair, start_point=0):
|
||||
ohlc_columns, stoploss, pair):
|
||||
"""
|
||||
Iterate through ohlc_columns recursively in order to find the next trade
|
||||
Iterate through ohlc_columns in order to find the next trade
|
||||
Next trade opens from the first buy signal noticed to
|
||||
The sell or stoploss signal after it.
|
||||
It then calls itself cutting OHLC, buy_column, sell_colum and date_column
|
||||
Cut from (the exit trade index) + 1
|
||||
It then cuts OHLC, buy_column, sell_column and date_column.
|
||||
Cut from (the exit trade index) + 1.
|
||||
|
||||
Author: https://github.com/mishaker
|
||||
"""
|
||||
|
||||
result: list = []
|
||||
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||
start_point = 0
|
||||
|
||||
# return empty if we don't find trade entry (i.e. buy==1) or
|
||||
# we find a buy but at the of array
|
||||
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||
return []
|
||||
else:
|
||||
open_trade_index += 1 # when a buy signal is seen,
|
||||
# trade opens in reality on the next candle
|
||||
while True:
|
||||
open_trade_index = utf1st.find_1st(buy_column, 1, utf1st.cmp_equal)
|
||||
|
||||
stop_price_percentage = stoploss + 1
|
||||
open_price = ohlc_columns[open_trade_index, 0]
|
||||
stop_price = (open_price * stop_price_percentage)
|
||||
# Return empty if we don't find trade entry (i.e. buy==1) or
|
||||
# we find a buy but at the end of array
|
||||
if open_trade_index == -1 or open_trade_index == len(buy_column) - 1:
|
||||
break
|
||||
else:
|
||||
# When a buy signal is seen,
|
||||
# trade opens in reality on the next candle
|
||||
open_trade_index += 1
|
||||
|
||||
# Searching for the index where stoploss is hit
|
||||
stop_index = utf1st.find_1st(
|
||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller)
|
||||
stop_price_percentage = stoploss + 1
|
||||
open_price = ohlc_columns[open_trade_index, 0]
|
||||
stop_price = (open_price * stop_price_percentage)
|
||||
|
||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||
if stop_index == -1:
|
||||
stop_index = float('inf')
|
||||
# Searching for the index where stoploss is hit
|
||||
stop_index = utf1st.find_1st(
|
||||
ohlc_columns[open_trade_index:, 2], stop_price, utf1st.cmp_smaller)
|
||||
|
||||
# Searching for the index where sell is hit
|
||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||
# If we don't find it then we assume stop_index will be far in future (infinite number)
|
||||
if stop_index == -1:
|
||||
stop_index = float('inf')
|
||||
|
||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||
if sell_index == -1:
|
||||
sell_index = float('inf')
|
||||
# Searching for the index where sell is hit
|
||||
sell_index = utf1st.find_1st(sell_column[open_trade_index:], 1, utf1st.cmp_equal)
|
||||
|
||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||
# And stop iterating there is no more entry
|
||||
if stop_index == sell_index == float('inf'):
|
||||
return []
|
||||
# If we don't find it then we assume sell_index will be far in future (infinite number)
|
||||
if sell_index == -1:
|
||||
sell_index = float('inf')
|
||||
|
||||
if stop_index <= sell_index:
|
||||
exit_index = open_trade_index + stop_index
|
||||
exit_type = SellType.STOP_LOSS
|
||||
exit_price = stop_price
|
||||
elif stop_index > sell_index:
|
||||
# if exit is SELL then we exit at the next candle
|
||||
exit_index = open_trade_index + sell_index + 1
|
||||
# Check if we don't find any stop or sell point (in that case trade remains open)
|
||||
# It is not interesting for Edge to consider it so we simply ignore the trade
|
||||
# And stop iterating there is no more entry
|
||||
if stop_index == sell_index == float('inf'):
|
||||
break
|
||||
|
||||
# check if we have the next candle
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
return []
|
||||
if stop_index <= sell_index:
|
||||
exit_index = open_trade_index + stop_index
|
||||
exit_type = SellType.STOP_LOSS
|
||||
exit_price = stop_price
|
||||
elif stop_index > sell_index:
|
||||
# If exit is SELL then we exit at the next candle
|
||||
exit_index = open_trade_index + sell_index + 1
|
||||
|
||||
exit_type = SellType.SELL_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
# Check if we have the next candle
|
||||
if len(ohlc_columns) - 1 < exit_index:
|
||||
break
|
||||
|
||||
trade = {'pair': pair,
|
||||
'stoploss': stoploss,
|
||||
'profit_percent': '',
|
||||
'profit_abs': '',
|
||||
'open_time': date_column[open_trade_index],
|
||||
'close_time': date_column[exit_index],
|
||||
'open_index': start_point + open_trade_index,
|
||||
'close_index': start_point + exit_index,
|
||||
'trade_duration': '',
|
||||
'open_rate': round(open_price, 15),
|
||||
'close_rate': round(exit_price, 15),
|
||||
'exit_type': exit_type
|
||||
}
|
||||
exit_type = SellType.SELL_SIGNAL
|
||||
exit_price = ohlc_columns[exit_index, 0]
|
||||
|
||||
result.append(trade)
|
||||
trade = {'pair': pair,
|
||||
'stoploss': stoploss,
|
||||
'profit_percent': '',
|
||||
'profit_abs': '',
|
||||
'open_time': date_column[open_trade_index],
|
||||
'close_time': date_column[exit_index],
|
||||
'open_index': start_point + open_trade_index,
|
||||
'close_index': start_point + exit_index,
|
||||
'trade_duration': '',
|
||||
'open_rate': round(open_price, 15),
|
||||
'close_rate': round(exit_price, 15),
|
||||
'exit_type': exit_type
|
||||
}
|
||||
|
||||
# Calling again the same function recursively but giving
|
||||
# it a view of exit_index till the end of array
|
||||
return result + self._detect_next_stop_or_sell_point(
|
||||
buy_column[exit_index:],
|
||||
sell_column[exit_index:],
|
||||
date_column[exit_index:],
|
||||
ohlc_columns[exit_index:],
|
||||
stoploss,
|
||||
pair,
|
||||
(start_point + exit_index)
|
||||
)
|
||||
result.append(trade)
|
||||
|
||||
# Giving a view of exit_index till the end of array
|
||||
buy_column = buy_column[exit_index:]
|
||||
sell_column = sell_column[exit_index:]
|
||||
date_column = date_column[exit_index:]
|
||||
ohlc_columns = ohlc_columns[exit_index:]
|
||||
start_point += exit_index
|
||||
|
||||
return result
|
||||
|
@ -1,2 +1,3 @@
|
||||
from freqtrade.exchange.exchange import Exchange # noqa: F401
|
||||
from freqtrade.exchange.kraken import Kraken # noqa: F401
|
||||
from freqtrade.exchange.binance import Binance # noqa: F401
|
||||
|
26
freqtrade/exchange/binance.py
Normal file
26
freqtrade/exchange/binance.py
Normal file
@ -0,0 +1,26 @@
|
||||
""" Binance exchange subclass """
|
||||
import logging
|
||||
from typing import Dict
|
||||
|
||||
from freqtrade.exchange import Exchange
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Binance(Exchange):
|
||||
|
||||
_ft_has: Dict = {
|
||||
"stoploss_on_exchange": True,
|
||||
}
|
||||
|
||||
def get_order_book(self, pair: str, limit: int = 100) -> dict:
|
||||
"""
|
||||
get order book level 2 from exchange
|
||||
|
||||
20180619: binance support limits but only on specific range
|
||||
"""
|
||||
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||
# get next-higher step in the limit_range list
|
||||
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||
|
||||
return super().get_order_book(pair, limit)
|
@ -24,7 +24,7 @@ API_RETRY_COUNT = 4
|
||||
# Urls to exchange markets, insert quote and base with .format()
|
||||
_EXCHANGE_URLS = {
|
||||
ccxt.bittrex.__name__: '/Market/Index?MarketName={quote}-{base}',
|
||||
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}'
|
||||
ccxt.binance.__name__: '/tradeDetail.html?symbol={base}_{quote}',
|
||||
}
|
||||
|
||||
|
||||
@ -69,11 +69,17 @@ class Exchange(object):
|
||||
_conf: Dict = {}
|
||||
_params: Dict = {}
|
||||
|
||||
# Dict to specify which options each exchange implements
|
||||
# TODO: this should be merged with attributes from subclasses
|
||||
# To avoid having to copy/paste this to all subclasses.
|
||||
_ft_has = {
|
||||
"stoploss_on_exchange": False,
|
||||
}
|
||||
|
||||
def __init__(self, config: dict) -> None:
|
||||
"""
|
||||
Initializes this module with the given config,
|
||||
it does basic validation whether the specified
|
||||
exchange and pairs are valid.
|
||||
it does basic validation whether the specified exchange and pairs are valid.
|
||||
:return: None
|
||||
"""
|
||||
self._conf.update(config)
|
||||
@ -236,11 +242,11 @@ class Exchange(object):
|
||||
raise OperationalException(
|
||||
f'Exchange {self.name} does not support market orders.')
|
||||
|
||||
if order_types.get('stoploss_on_exchange'):
|
||||
if self.name != 'Binance':
|
||||
raise OperationalException(
|
||||
'On exchange stoploss is not supported for %s.' % self.name
|
||||
)
|
||||
if (order_types.get("stoploss_on_exchange")
|
||||
and not self._ft_has.get("stoploss_on_exchange", False)):
|
||||
raise OperationalException(
|
||||
'On exchange stoploss is not supported for %s.' % self.name
|
||||
)
|
||||
|
||||
def validate_order_time_in_force(self, order_time_in_force: Dict) -> None:
|
||||
"""
|
||||
@ -282,104 +288,96 @@ class Exchange(object):
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return price
|
||||
|
||||
def buy(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force) -> Dict:
|
||||
if self._conf['dry_run']:
|
||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||
self._dry_run_open_orders[order_id] = {
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
'amount': amount,
|
||||
'type': ordertype,
|
||||
'side': 'buy',
|
||||
'remaining': 0.0,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'status': 'closed',
|
||||
'fee': None
|
||||
}
|
||||
return {'id': order_id}
|
||||
def dry_run_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict[str, Any]:
|
||||
order_id = f'dry_run_{side}_{randint(0, 10**6)}'
|
||||
dry_order = { # TODO: additional entry should be added for stoploss limit
|
||||
"id": order_id,
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
'amount': amount,
|
||||
"cost": amount * rate,
|
||||
'type': ordertype,
|
||||
'side': 'buy',
|
||||
'remaining': amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'status': "open",
|
||||
'fee': None,
|
||||
"info": {}
|
||||
}
|
||||
self._store_dry_order(dry_order)
|
||||
return dry_order
|
||||
|
||||
def _store_dry_order(self, dry_order: Dict) -> None:
|
||||
closed_order = dry_order.copy()
|
||||
if closed_order["type"] in ["market", "limit"]:
|
||||
closed_order.update({
|
||||
"status": "closed",
|
||||
"filled": closed_order["amount"],
|
||||
"remaining": 0
|
||||
})
|
||||
self._dry_run_open_orders[closed_order["id"]] = closed_order
|
||||
|
||||
def create_order(self, pair: str, ordertype: str, side: str, amount: float,
|
||||
rate: float, params: Dict = {}) -> Dict:
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
|
||||
return self._api.create_order(pair, ordertype, 'buy',
|
||||
return self._api.create_order(pair, ordertype, side,
|
||||
amount, rate, params)
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create limit buy order on market {pair}.'
|
||||
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Insufficient funds to create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
f'Could not create limit buy order on market {pair}.'
|
||||
f'Tried to buy amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Could not create {ordertype} {side} order on market {pair}.'
|
||||
f'Tried to {side} amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place buy order due to {e.__class__.__name__}. Message: {e}')
|
||||
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
def buy(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force) -> Dict:
|
||||
|
||||
if self._conf['dry_run']:
|
||||
dry_order = self.dry_run_order(pair, ordertype, "buy", amount, rate)
|
||||
return dry_order
|
||||
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
|
||||
return self.create_order(pair, ordertype, 'buy', amount, rate, params)
|
||||
|
||||
def sell(self, pair: str, ordertype: str, amount: float,
|
||||
rate: float, time_in_force='gtc') -> Dict:
|
||||
|
||||
if self._conf['dry_run']:
|
||||
order_id = f'dry_run_sell_{randint(0, 10**6)}'
|
||||
self._dry_run_open_orders[order_id] = {
|
||||
'pair': pair,
|
||||
'price': rate,
|
||||
'amount': amount,
|
||||
'type': ordertype,
|
||||
'side': 'sell',
|
||||
'remaining': 0.0,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'status': 'closed'
|
||||
}
|
||||
return {'id': order_id}
|
||||
dry_order = self.dry_run_order(pair, ordertype, "sell", amount, rate)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate) if ordertype != 'market' else None
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
|
||||
params = self._params.copy()
|
||||
if time_in_force != 'gtc':
|
||||
params.update({'timeInForce': time_in_force})
|
||||
|
||||
return self._api.create_order(pair, ordertype, 'sell',
|
||||
amount, rate, params)
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to create limit sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
f'Could not create limit sell order on market {pair}.'
|
||||
f'Tried to sell amount {amount} at rate {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
return self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||
|
||||
def stoploss_limit(self, pair: str, amount: float, stop_price: float, rate: float) -> Dict:
|
||||
"""
|
||||
creates a stoploss limit order.
|
||||
NOTICE: it is not supported by all exchanges. only binance is tested for now.
|
||||
TODO: implementation maybe needs to be moved to the binance subclass
|
||||
"""
|
||||
ordertype = "stop_loss_limit"
|
||||
|
||||
# Set the precision for amount and price(rate) as accepted by the exchange
|
||||
amount = self.symbol_amount_prec(pair, amount)
|
||||
rate = self.symbol_price_prec(pair, rate)
|
||||
stop_price = self.symbol_price_prec(pair, stop_price)
|
||||
|
||||
# Ensure rate is less than stop price
|
||||
@ -388,50 +386,17 @@ class Exchange(object):
|
||||
'In stoploss limit order, stop price should be more than limit price')
|
||||
|
||||
if self._conf['dry_run']:
|
||||
order_id = f'dry_run_buy_{randint(0, 10**6)}'
|
||||
self._dry_run_open_orders[order_id] = {
|
||||
'info': {},
|
||||
'id': order_id,
|
||||
'pair': pair,
|
||||
'price': stop_price,
|
||||
'amount': amount,
|
||||
'type': 'stop_loss_limit',
|
||||
'side': 'sell',
|
||||
'remaining': amount,
|
||||
'datetime': arrow.utcnow().isoformat(),
|
||||
'status': 'open',
|
||||
'fee': None
|
||||
}
|
||||
return self._dry_run_open_orders[order_id]
|
||||
dry_order = self.dry_run_order(
|
||||
pair, ordertype, "sell", amount, stop_price)
|
||||
return dry_order
|
||||
|
||||
try:
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
params = self._params.copy()
|
||||
params.update({'stopPrice': stop_price})
|
||||
|
||||
order = self._api.create_order(pair, 'stop_loss_limit', 'sell',
|
||||
amount, rate, params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
||||
return order
|
||||
|
||||
except ccxt.InsufficientFunds as e:
|
||||
raise DependencyException(
|
||||
f'Insufficient funds to place stoploss limit order on market {pair}. '
|
||||
f'Tried to put a stoploss amount {amount} with '
|
||||
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except ccxt.InvalidOrder as e:
|
||||
raise DependencyException(
|
||||
f'Could not place stoploss limit order on market {pair}.'
|
||||
f'Tried to place stoploss amount {amount} with '
|
||||
f'stop {stop_price} and limit {rate} (total {rate*amount}).'
|
||||
f'Message: {e}')
|
||||
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
|
||||
raise TemporaryError(
|
||||
f'Could not place stoploss limit order due to {e.__class__.__name__}. Message: {e}')
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
order = self.create_order(pair, ordertype, 'sell', amount, rate, params)
|
||||
logger.info('stoploss limit order added for %s. '
|
||||
'stop price: %s. limit: %s' % (pair, stop_price, rate))
|
||||
return order
|
||||
|
||||
@retrier
|
||||
def get_balance(self, currency: str) -> float:
|
||||
@ -578,7 +543,7 @@ class Exchange(object):
|
||||
interval_in_sec = constants.TICKER_INTERVAL_MINUTES[ticker_interval] * 60
|
||||
|
||||
return not ((self._pairs_last_refresh_time.get((pair, ticker_interval), 0)
|
||||
+ interval_in_sec) >= arrow.utcnow().timestamp)
|
||||
+ interval_in_sec) >= arrow.utcnow().timestamp)
|
||||
|
||||
@retrier_async
|
||||
async def _async_get_candle_history(self, pair: str, tick_interval: str,
|
||||
@ -637,9 +602,6 @@ class Exchange(object):
|
||||
def get_order(self, order_id: str, pair: str) -> Dict:
|
||||
if self._conf['dry_run']:
|
||||
order = self._dry_run_open_orders[order_id]
|
||||
order.update({
|
||||
'id': order_id
|
||||
})
|
||||
return order
|
||||
try:
|
||||
return self._api.fetch_order(order_id, pair)
|
||||
@ -659,18 +621,8 @@ class Exchange(object):
|
||||
|
||||
Notes:
|
||||
20180619: bittrex doesnt support limits -.-
|
||||
20180619: binance support limits but only on specific range
|
||||
"""
|
||||
try:
|
||||
if self._api.name == 'Binance':
|
||||
limit_range = [5, 10, 20, 50, 100, 500, 1000]
|
||||
# get next-higher step in the limit_range list
|
||||
limit = min(list(filter(lambda x: limit <= x, limit_range)))
|
||||
# above script works like loop below (but with slightly better performance):
|
||||
# for limitx in limit_range:
|
||||
# if limit <= limitx:
|
||||
# limit = limitx
|
||||
# break
|
||||
|
||||
return self._api.fetch_l2_order_book(pair, limit)
|
||||
except ccxt.NotSupported as e:
|
||||
@ -702,16 +654,6 @@ class Exchange(object):
|
||||
except ccxt.BaseError as e:
|
||||
raise OperationalException(e)
|
||||
|
||||
def get_pair_detail_url(self, pair: str) -> str:
|
||||
try:
|
||||
url_base = self._api.urls.get('www')
|
||||
base, quote = pair.split('/')
|
||||
|
||||
return url_base + _EXCHANGE_URLS[self._api.id].format(base=base, quote=quote)
|
||||
except KeyError:
|
||||
logger.warning('Could not get exchange url for %s', self.name)
|
||||
return ""
|
||||
|
||||
@retrier
|
||||
def get_markets(self) -> List[dict]:
|
||||
try:
|
||||
|
@ -7,7 +7,7 @@ import logging
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import arrow
|
||||
from requests.exceptions import RequestException
|
||||
@ -159,24 +159,20 @@ class FreqtradeBot(object):
|
||||
self.pairlists.refresh_pairlist()
|
||||
self.active_pair_whitelist = self.pairlists.whitelist
|
||||
|
||||
# Calculating Edge positiong
|
||||
# Calculating Edge positioning
|
||||
if self.edge:
|
||||
self.edge.calculate()
|
||||
self.active_pair_whitelist = self.edge.adjust(self.active_pair_whitelist)
|
||||
|
||||
# Query trades from persistence layer
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
trades = Trade.get_open_trades()
|
||||
|
||||
# Extend active-pair whitelist with pairs from open trades
|
||||
# ensures that tickers are downloaded for open trades
|
||||
self.active_pair_whitelist.extend([trade.pair for trade in trades
|
||||
if trade.pair not in self.active_pair_whitelist])
|
||||
# It ensures that tickers are downloaded for open trades
|
||||
self._extend_whitelist_with_trades(self.active_pair_whitelist, trades)
|
||||
|
||||
# Create pair-whitelist tuple with (pair, ticker_interval)
|
||||
pair_whitelist_tuple = [(pair, self.config['ticker_interval'])
|
||||
for pair in self.active_pair_whitelist]
|
||||
# Refreshing candles
|
||||
self.dataprovider.refresh(pair_whitelist_tuple,
|
||||
self.dataprovider.refresh(self._create_pair_whitelist(self.active_pair_whitelist),
|
||||
self.strategy.informative_pairs())
|
||||
|
||||
# First process current opened trades
|
||||
@ -206,6 +202,18 @@ class FreqtradeBot(object):
|
||||
self.state = State.STOPPED
|
||||
return state_changed
|
||||
|
||||
def _extend_whitelist_with_trades(self, whitelist: List[str], trades: List[Any]):
|
||||
"""
|
||||
Extend whitelist with pairs from open trades
|
||||
"""
|
||||
whitelist.extend([trade.pair for trade in trades if trade.pair not in whitelist])
|
||||
|
||||
def _create_pair_whitelist(self, pairs: List[str]) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Create pair-whitelist tuple with (pair, ticker_interval)
|
||||
"""
|
||||
return [(pair, self.config['ticker_interval']) for pair in pairs]
|
||||
|
||||
def get_target_bid(self, pair: str, tick: Dict = None) -> float:
|
||||
"""
|
||||
Calculates bid target between current ask price and last price
|
||||
@ -256,7 +264,7 @@ class FreqtradeBot(object):
|
||||
avaliable_amount = self.wallets.get_free(self.config['stake_currency'])
|
||||
|
||||
if stake_amount == constants.UNLIMITED_STAKE_AMOUNT:
|
||||
open_trades = len(Trade.query.filter(Trade.is_open.is_(True)).all())
|
||||
open_trades = len(Trade.get_open_trades())
|
||||
if open_trades >= self.config['max_open_trades']:
|
||||
logger.warning('Can\'t open a new trade: max number of trades is reached')
|
||||
return None
|
||||
@ -314,7 +322,7 @@ class FreqtradeBot(object):
|
||||
whitelist = copy.deepcopy(self.active_pair_whitelist)
|
||||
|
||||
# Remove currently opened and latest pairs from whitelist
|
||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||
for trade in Trade.get_open_trades():
|
||||
if trade.pair in whitelist:
|
||||
whitelist.remove(trade.pair)
|
||||
logger.debug('Ignoring %s in pair whitelist', trade.pair)
|
||||
@ -371,7 +379,6 @@ class FreqtradeBot(object):
|
||||
:return: None
|
||||
"""
|
||||
pair_s = pair.replace('_', '/')
|
||||
pair_url = self.exchange.get_pair_detail_url(pair)
|
||||
stake_currency = self.config['stake_currency']
|
||||
fiat_currency = self.config.get('fiat_display_currency', None)
|
||||
time_in_force = self.strategy.order_time_in_force['buy']
|
||||
@ -436,7 +443,6 @@ class FreqtradeBot(object):
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': self.exchange.name.capitalize(),
|
||||
'pair': pair_s,
|
||||
'market_url': pair_url,
|
||||
'limit': buy_limit_filled_price,
|
||||
'stake_amount': stake_amount,
|
||||
'stake_currency': stake_currency,
|
||||
@ -844,7 +850,6 @@ class FreqtradeBot(object):
|
||||
profit_trade = trade.calc_profit(rate=limit)
|
||||
current_rate = self.exchange.get_ticker(trade.pair)['bid']
|
||||
profit_percent = trade.calc_profit_percent(limit)
|
||||
pair_url = self.exchange.get_pair_detail_url(trade.pair)
|
||||
gain = "profit" if profit_percent > 0 else "loss"
|
||||
|
||||
msg = {
|
||||
@ -852,7 +857,6 @@ class FreqtradeBot(object):
|
||||
'exchange': trade.exchange.capitalize(),
|
||||
'pair': trade.pair,
|
||||
'gain': gain,
|
||||
'market_url': pair_url,
|
||||
'limit': limit,
|
||||
'amount': trade.amount,
|
||||
'open_rate': trade.open_rate,
|
||||
|
@ -113,3 +113,21 @@ def format_ms_time(date: int) -> str:
|
||||
: epoch-string in ms
|
||||
"""
|
||||
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')
|
||||
|
||||
|
||||
def deep_merge_dicts(source, destination):
|
||||
"""
|
||||
>>> a = { 'first' : { 'rows' : { 'pass' : 'dog', 'number' : '1' } } }
|
||||
>>> b = { 'first' : { 'rows' : { 'fail' : 'cat', 'number' : '5' } } }
|
||||
>>> merge(b, a) == { 'first' : { 'rows' : { 'pass' : 'dog', 'fail' : 'cat', 'number' : '5' } } }
|
||||
True
|
||||
"""
|
||||
for key, value in source.items():
|
||||
if isinstance(value, dict):
|
||||
# get node or create one
|
||||
node = destination.setdefault(key, {})
|
||||
deep_merge_dicts(value, node)
|
||||
else:
|
||||
destination[key] = value
|
||||
|
||||
return destination
|
||||
|
@ -5,7 +5,7 @@ This module contains the class to persist trades into SQLite
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import arrow
|
||||
from sqlalchemy import (Boolean, Column, DateTime, Float, Integer, String,
|
||||
@ -371,3 +371,10 @@ class Trade(_DECL_BASE):
|
||||
.filter(Trade.is_open.is_(True))\
|
||||
.scalar()
|
||||
return total_open_stake_amount or 0
|
||||
|
||||
@staticmethod
|
||||
def get_open_trades() -> List[Any]:
|
||||
"""
|
||||
Query trades from persistence layer
|
||||
"""
|
||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
|
@ -83,7 +83,7 @@ class RPC(object):
|
||||
a remotely exposed function
|
||||
"""
|
||||
# Fetch open trade
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
trades = Trade.get_open_trades()
|
||||
if not trades:
|
||||
raise RPCException('no active trade')
|
||||
else:
|
||||
@ -103,7 +103,6 @@ class RPC(object):
|
||||
results.append(dict(
|
||||
trade_id=trade.id,
|
||||
pair=trade.pair,
|
||||
market_url=self._freqtrade.exchange.get_pair_detail_url(trade.pair),
|
||||
date=arrow.get(trade.open_date),
|
||||
open_rate=trade.open_rate,
|
||||
close_rate=trade.close_rate,
|
||||
@ -118,7 +117,7 @@ class RPC(object):
|
||||
return results
|
||||
|
||||
def _rpc_status_table(self) -> DataFrame:
|
||||
trades = Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
trades = Trade.get_open_trades()
|
||||
if not trades:
|
||||
raise RPCException('no active order')
|
||||
else:
|
||||
@ -366,7 +365,7 @@ class RPC(object):
|
||||
|
||||
if trade_id == 'all':
|
||||
# Execute sell for all open orders
|
||||
for trade in Trade.query.filter(Trade.is_open.is_(True)).all():
|
||||
for trade in Trade.get_open_trades():
|
||||
_exec_forcesell(trade)
|
||||
Trade.session.flush()
|
||||
return
|
||||
@ -442,7 +441,7 @@ class RPC(object):
|
||||
if self._freqtrade.state != State.RUNNING:
|
||||
raise RPCException('trader is not running')
|
||||
|
||||
return Trade.query.filter(Trade.is_open.is_(True)).all()
|
||||
return Trade.get_open_trades()
|
||||
|
||||
def _rpc_whitelist(self) -> Dict:
|
||||
""" Returns the currently active whitelist"""
|
||||
|
@ -125,7 +125,7 @@ class Telegram(RPC):
|
||||
else:
|
||||
msg['stake_amount_fiat'] = 0
|
||||
|
||||
message = ("*{exchange}:* Buying [{pair}]({market_url})\n"
|
||||
message = ("*{exchange}:* Buying {pair}\n"
|
||||
"with limit `{limit:.8f}\n"
|
||||
"({stake_amount:.6f} {stake_currency}").format(**msg)
|
||||
|
||||
@ -137,7 +137,7 @@ class Telegram(RPC):
|
||||
msg['amount'] = round(msg['amount'], 8)
|
||||
msg['profit_percent'] = round(msg['profit_percent'] * 100, 2)
|
||||
|
||||
message = ("*{exchange}:* Selling [{pair}]({market_url})\n"
|
||||
message = ("*{exchange}:* Selling {pair}\n"
|
||||
"*Limit:* `{limit:.8f}`\n"
|
||||
"*Amount:* `{amount:.8f}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
@ -193,7 +193,7 @@ class Telegram(RPC):
|
||||
|
||||
messages = [
|
||||
"*Trade ID:* `{trade_id}`\n"
|
||||
"*Current Pair:* [{pair}]({market_url})\n"
|
||||
"*Current Pair:* {pair}\n"
|
||||
"*Open Since:* `{date}`\n"
|
||||
"*Amount:* `{amount}`\n"
|
||||
"*Open Rate:* `{open_rate:.8f}`\n"
|
||||
|
@ -12,12 +12,16 @@ import pytest
|
||||
from pandas import DataFrame
|
||||
|
||||
from freqtrade import DependencyException, OperationalException, TemporaryError
|
||||
from freqtrade.exchange import Exchange, Kraken
|
||||
from freqtrade.exchange import Exchange, Kraken, Binance
|
||||
from freqtrade.exchange.exchange import API_RETRY_COUNT
|
||||
from freqtrade.tests.conftest import get_patched_exchange, log_has, log_has_re
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
|
||||
|
||||
# Make sure to always keep one exchange here which is NOT subclassed!!
|
||||
EXCHANGES = ['bittrex', 'binance', 'kraken', ]
|
||||
|
||||
|
||||
# Source: https://stackoverflow.com/questions/29881236/how-to-mock-asyncio-coroutines
|
||||
def get_mock_coro(return_value):
|
||||
async def mock_coro(*args, **kwargs):
|
||||
@ -26,16 +30,17 @@ def get_mock_coro(return_value):
|
||||
return Mock(wraps=mock_coro)
|
||||
|
||||
|
||||
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, fun, mock_ccxt_fun, **kwargs):
|
||||
def ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
fun, mock_ccxt_fun, **kwargs):
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == API_RETRY_COUNT + 1
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.__dict__[mock_ccxt_fun] = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
getattr(exchange, fun)(**kwargs)
|
||||
assert api_mock.__dict__[mock_ccxt_fun].call_count == 1
|
||||
|
||||
@ -113,7 +118,7 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_async_markets', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_pairs', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.validate_timeframes', MagicMock())
|
||||
exchange = ExchangeResolver('Binance', default_conf).exchange
|
||||
exchange = ExchangeResolver('Bittrex', default_conf).exchange
|
||||
assert isinstance(exchange, Exchange)
|
||||
assert log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||
caplog.record_tuples)
|
||||
@ -122,6 +127,15 @@ def test_exchange_resolver(default_conf, mocker, caplog):
|
||||
exchange = ExchangeResolver('Kraken', default_conf).exchange
|
||||
assert isinstance(exchange, Exchange)
|
||||
assert isinstance(exchange, Kraken)
|
||||
assert not isinstance(exchange, Binance)
|
||||
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||
caplog.record_tuples)
|
||||
|
||||
exchange = ExchangeResolver('Binance', default_conf).exchange
|
||||
assert isinstance(exchange, Exchange)
|
||||
assert isinstance(exchange, Binance)
|
||||
assert not isinstance(exchange, Kraken)
|
||||
|
||||
assert not log_has_re(r"No .* specific subclass found. Using the generic class instead.",
|
||||
caplog.record_tuples)
|
||||
|
||||
@ -443,6 +457,58 @@ def test_exchange_has(default_conf, mocker):
|
||||
assert not exchange.exchange_has("deadbeef")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side", [
|
||||
("buy"),
|
||||
("sell")
|
||||
])
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_dry_run_order(default_conf, mocker, side, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
|
||||
order = exchange.dry_run_order(
|
||||
pair='ETH/BTC', ordertype='limit', side=side, amount=1, rate=200)
|
||||
assert 'id' in order
|
||||
assert f'dry_run_{side}_' in order["id"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("side", [
|
||||
("buy"),
|
||||
("sell")
|
||||
])
|
||||
@pytest.mark.parametrize("ordertype,rate", [
|
||||
("market", None),
|
||||
("limit", 200),
|
||||
("stop_loss_limit", 200)
|
||||
])
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_create_order(default_conf, mocker, side, ordertype, rate, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_{}_{}'.format(side, randint(0, 10 ** 6))
|
||||
api_mock.create_order = MagicMock(return_value={
|
||||
'id': order_id,
|
||||
'info': {
|
||||
'foo': 'bar'
|
||||
}
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.create_order(
|
||||
pair='ETH/BTC', ordertype=ordertype, side=side, amount=1, rate=200)
|
||||
|
||||
assert 'id' in order
|
||||
assert 'info' in order
|
||||
assert order['id'] == order_id
|
||||
assert api_mock.create_order.call_args[0][0] == 'ETH/BTC'
|
||||
assert api_mock.create_order.call_args[0][1] == ordertype
|
||||
assert api_mock.create_order.call_args[0][2] == side
|
||||
assert api_mock.create_order.call_args[0][3] == 1
|
||||
assert api_mock.create_order.call_args[0][4] is rate
|
||||
|
||||
|
||||
def test_buy_dry_run(default_conf, mocker):
|
||||
default_conf['dry_run'] = True
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@ -453,7 +519,8 @@ def test_buy_dry_run(default_conf, mocker):
|
||||
assert 'dry_run_buy_' in order['id']
|
||||
|
||||
|
||||
def test_buy_prod(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_buy_prod(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_buy_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'market'
|
||||
@ -467,7 +534,7 @@ def test_buy_prod(default_conf, mocker):
|
||||
default_conf['dry_run'] = False
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
@ -498,25 +565,25 @@ def test_buy_prod(default_conf, mocker):
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.buy(pair='ETH/BTC', ordertype=order_type,
|
||||
amount=1, rate=200, time_in_force=time_in_force)
|
||||
|
||||
@ -621,7 +688,8 @@ def test_sell_dry_run(default_conf, mocker):
|
||||
assert 'dry_run_sell_' in order['id']
|
||||
|
||||
|
||||
def test_sell_prod(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_sell_prod(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
order_id = 'test_prod_sell_{}'.format(randint(0, 10 ** 6))
|
||||
order_type = 'market'
|
||||
@ -635,7 +703,7 @@ def test_sell_prod(default_conf, mocker):
|
||||
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_amount_prec', lambda s, x, y: y)
|
||||
mocker.patch('freqtrade.exchange.Exchange.symbol_price_prec', lambda s, x, y: y)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
order = exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
@ -660,22 +728,22 @@ def test_sell_prod(default_conf, mocker):
|
||||
# test exception handling
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.create_order = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.sell(pair='ETH/BTC', ordertype=order_type, amount=1, rate=200)
|
||||
|
||||
|
||||
@ -686,23 +754,24 @@ def test_get_balance_dry_run(default_conf, mocker):
|
||||
assert exchange.get_balance(currency='BTC') == 999.9
|
||||
|
||||
|
||||
def test_get_balance_prod(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_balance_prod(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_balance = MagicMock(return_value={'BTC': {'free': 123.4}})
|
||||
default_conf['dry_run'] = False
|
||||
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
assert exchange.get_balance(currency='BTC') == 123.4
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_balance = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
exchange.get_balance(currency='BTC')
|
||||
|
||||
with pytest.raises(TemporaryError, match=r'.*balance due to malformed exchange response:.*'):
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_balances', MagicMock(return_value={}))
|
||||
exchange.get_balance(currency='BTC')
|
||||
|
||||
@ -713,7 +782,8 @@ def test_get_balances_dry_run(default_conf, mocker):
|
||||
assert exchange.get_balances() == {}
|
||||
|
||||
|
||||
def test_get_balances_prod(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_balances_prod(default_conf, mocker, exchange_name):
|
||||
balance_item = {
|
||||
'free': 10.0,
|
||||
'total': 10.0,
|
||||
@ -727,17 +797,18 @@ def test_get_balances_prod(default_conf, mocker):
|
||||
'3ST': balance_item
|
||||
})
|
||||
default_conf['dry_run'] = False
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
assert len(exchange.get_balances()) == 3
|
||||
assert exchange.get_balances()['1ST']['free'] == 10.0
|
||||
assert exchange.get_balances()['1ST']['total'] == 10.0
|
||||
assert exchange.get_balances()['1ST']['used'] == 0.0
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
"get_balances", "fetch_balance")
|
||||
|
||||
|
||||
def test_get_tickers(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_tickers(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
tick = {'ETH/BTC': {
|
||||
'symbol': 'ETH/BTC',
|
||||
@ -752,7 +823,7 @@ def test_get_tickers(default_conf, mocker):
|
||||
}
|
||||
}
|
||||
api_mock.fetch_tickers = MagicMock(return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
# retrieve original ticker
|
||||
tickers = exchange.get_tickers()
|
||||
|
||||
@ -763,20 +834,21 @@ def test_get_tickers(default_conf, mocker):
|
||||
assert tickers['BCH/BTC']['bid'] == 0.6
|
||||
assert tickers['BCH/BTC']['ask'] == 0.5
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
"get_tickers", "fetch_tickers")
|
||||
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_tickers = MagicMock(side_effect=ccxt.NotSupported)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_tickers()
|
||||
|
||||
api_mock.fetch_tickers = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_tickers()
|
||||
|
||||
|
||||
def test_get_ticker(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_ticker(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
tick = {
|
||||
'symbol': 'ETH/BTC',
|
||||
@ -786,7 +858,7 @@ def test_get_ticker(default_conf, mocker):
|
||||
}
|
||||
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
||||
api_mock.markets = {'ETH/BTC': {}}
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
# retrieve original ticker
|
||||
ticker = exchange.get_ticker(pair='ETH/BTC')
|
||||
|
||||
@ -801,7 +873,7 @@ def test_get_ticker(default_conf, mocker):
|
||||
'last': 42,
|
||||
}
|
||||
api_mock.fetch_ticker = MagicMock(return_value=tick)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
# if not caching the result we should get the same ticker
|
||||
# if not fetching a new result we should get the cached ticker
|
||||
@ -820,20 +892,21 @@ def test_get_ticker(default_conf, mocker):
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=False)
|
||||
assert api_mock.fetch_ticker.call_count == 0
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
"get_ticker", "fetch_ticker",
|
||||
pair='ETH/BTC', refresh=True)
|
||||
|
||||
api_mock.fetch_ticker = MagicMock(return_value={})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_ticker(pair='ETH/BTC', refresh=True)
|
||||
|
||||
with pytest.raises(DependencyException, match=r'Pair XRP/ETH not available'):
|
||||
exchange.get_ticker(pair='XRP/ETH', refresh=True)
|
||||
|
||||
|
||||
def test_get_history(default_conf, mocker, caplog):
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_history(default_conf, mocker, caplog, exchange_name):
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
tick = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
@ -912,7 +985,8 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
async def test__async_get_candle_history(default_conf, mocker, caplog, exchange_name):
|
||||
tick = [
|
||||
[
|
||||
arrow.utcnow().timestamp * 1000, # unix timestamp ms
|
||||
@ -925,11 +999,10 @@ async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||
]
|
||||
|
||||
caplog.set_level(logging.DEBUG)
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
# Monkey-patch async function
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||
|
||||
exchange = Exchange(default_conf)
|
||||
pair = 'ETH/BTC'
|
||||
res = await exchange._async_get_candle_history(pair, "5m")
|
||||
assert type(res) is tuple
|
||||
@ -948,7 +1021,7 @@ async def test__async_get_candle_history(default_conf, mocker, caplog):
|
||||
api_mock = MagicMock()
|
||||
with pytest.raises(OperationalException, match=r'Could not fetch ticker data*'):
|
||||
api_mock.fetch_ohlcv = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
await exchange._async_get_candle_history(pair, "5m",
|
||||
(arrow.utcnow().timestamp - 2000) * 1000)
|
||||
|
||||
@ -1001,12 +1074,13 @@ def test_refresh_latest_ohlcv_inv_result(default_conf, mocker, caplog):
|
||||
assert log_has("Async code raised an exception: TypeError", caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_order_book(default_conf, mocker, order_book_l2):
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_order_book(default_conf, mocker, order_book_l2, exchange_name):
|
||||
default_conf['exchange']['name'] = exchange_name
|
||||
api_mock = MagicMock()
|
||||
|
||||
api_mock.fetch_l2_order_book = order_book_l2
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
order_book = exchange.get_order_book(pair='ETH/BTC', limit=10)
|
||||
assert 'bids' in order_book
|
||||
assert 'asks' in order_book
|
||||
@ -1014,19 +1088,20 @@ def test_get_order_book(default_conf, mocker, order_book_l2):
|
||||
assert len(order_book['asks']) == 10
|
||||
|
||||
|
||||
def test_get_order_book_exception(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_order_book_exception(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NotSupported)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
with pytest.raises(TemporaryError):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.NetworkError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
with pytest.raises(OperationalException):
|
||||
api_mock.fetch_l2_order_book = MagicMock(side_effect=ccxt.BaseError)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_order_book(pair='ETH/BTC', limit=50)
|
||||
|
||||
|
||||
@ -1039,8 +1114,9 @@ def make_fetch_ohlcv_mock(data):
|
||||
return fetch_ohlcv_mock
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
@pytest.mark.asyncio
|
||||
async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||
async def test___async_get_candle_history_sort(default_conf, mocker, exchange_name):
|
||||
def sort_data(data, key):
|
||||
return sorted(data, key=key)
|
||||
|
||||
@ -1058,7 +1134,7 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||
[1527830700000, 0.07652, 0.07652, 0.07651, 0.07652, 10.04822687],
|
||||
[1527830400000, 0.07649, 0.07651, 0.07649, 0.07651, 2.5734867]
|
||||
]
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
exchange._api_async.fetch_ohlcv = get_mock_coro(tick)
|
||||
sort_mock = mocker.patch('freqtrade.exchange.exchange.sorted', MagicMock(side_effect=sort_data))
|
||||
# Test the ticker history sort
|
||||
@ -1120,36 +1196,39 @@ async def test___async_get_candle_history_sort(default_conf, mocker):
|
||||
assert ticks[9][5] == 2.31452783
|
||||
|
||||
|
||||
def test_cancel_order_dry_run(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_order_dry_run(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
assert exchange.cancel_order(order_id='123', pair='TKN/BTC') is None
|
||||
|
||||
|
||||
# Ensure that if not dry_run, we should call API
|
||||
def test_cancel_order(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_cancel_order(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.cancel_order = MagicMock(return_value=123)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
assert exchange.cancel_order(order_id='_', pair='TKN/BTC') == 123
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.cancel_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.cancel_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.cancel_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
"cancel_order", "cancel_order",
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
def test_get_order(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_order(default_conf, mocker, exchange_name):
|
||||
default_conf['dry_run'] = True
|
||||
order = MagicMock()
|
||||
order.myid = 123
|
||||
exchange = get_patched_exchange(mocker, default_conf)
|
||||
exchange = get_patched_exchange(mocker, default_conf, id=exchange_name)
|
||||
exchange._dry_run_open_orders['X'] = order
|
||||
print(exchange.get_order('X', 'TKN/BTC'))
|
||||
assert exchange.get_order('X', 'TKN/BTC').myid == 123
|
||||
@ -1157,67 +1236,32 @@ def test_get_order(default_conf, mocker):
|
||||
default_conf['dry_run'] = False
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_order = MagicMock(return_value=456)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
assert exchange.get_order('X', 'TKN/BTC') == 456
|
||||
|
||||
with pytest.raises(DependencyException):
|
||||
api_mock.fetch_order = MagicMock(side_effect=ccxt.InvalidOrder)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
exchange.get_order(order_id='_', pair='TKN/BTC')
|
||||
assert api_mock.fetch_order.call_count == API_RETRY_COUNT + 1
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_order', 'fetch_order',
|
||||
order_id='_', pair='TKN/BTC')
|
||||
|
||||
|
||||
def test_name(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_name(default_conf, mocker, exchange_name):
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
default_conf['exchange']['name'] = exchange_name
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
assert exchange.name == 'Binance'
|
||||
assert exchange.name == exchange_name.title()
|
||||
assert exchange.id == exchange_name
|
||||
|
||||
|
||||
def test_id(default_conf, mocker):
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
exchange = Exchange(default_conf)
|
||||
assert exchange.id == 'binance'
|
||||
|
||||
|
||||
def test_get_pair_detail_url(default_conf, mocker, caplog):
|
||||
mocker.patch('freqtrade.exchange.Exchange._load_markets', MagicMock(return_value={}))
|
||||
default_conf['exchange']['name'] = 'binance'
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
url = exchange.get_pair_detail_url('TKN/ETH')
|
||||
assert 'TKN' in url
|
||||
assert 'ETH' in url
|
||||
|
||||
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||
assert 'LOOONG' in url
|
||||
assert 'BTC' in url
|
||||
|
||||
default_conf['exchange']['name'] = 'bittrex'
|
||||
exchange = Exchange(default_conf)
|
||||
|
||||
url = exchange.get_pair_detail_url('TKN/ETH')
|
||||
assert 'TKN' in url
|
||||
assert 'ETH' in url
|
||||
|
||||
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||
assert 'LOOONG' in url
|
||||
assert 'BTC' in url
|
||||
|
||||
default_conf['exchange']['name'] = 'poloniex'
|
||||
exchange = Exchange(default_conf)
|
||||
url = exchange.get_pair_detail_url('LOOONG/BTC')
|
||||
assert '' == url
|
||||
assert log_has('Could not get exchange url for Poloniex', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_get_trades_for_order(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_trades_for_order(default_conf, mocker, exchange_name):
|
||||
order_id = 'ABCD-ABCD'
|
||||
since = datetime(2018, 5, 5)
|
||||
default_conf["dry_run"] = False
|
||||
@ -1244,13 +1288,13 @@ def test_get_trades_for_order(default_conf, mocker):
|
||||
'amount': 0.2340606,
|
||||
'fee': {'cost': 0.06179, 'currency': 'BTC'}
|
||||
}])
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
orders = exchange.get_trades_for_order(order_id, 'LTC/BTC', since)
|
||||
assert len(orders) == 1
|
||||
assert orders[0]['price'] == 165
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_trades_for_order', 'fetch_my_trades',
|
||||
order_id=order_id, pair='LTC/BTC', since=since)
|
||||
|
||||
@ -1258,10 +1302,11 @@ def test_get_trades_for_order(default_conf, mocker):
|
||||
assert exchange.get_trades_for_order(order_id, 'LTC/BTC', since) == []
|
||||
|
||||
|
||||
def test_get_markets(default_conf, mocker, markets):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_markets(default_conf, mocker, markets, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
api_mock.fetch_markets = markets
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
ret = exchange.get_markets()
|
||||
assert isinstance(ret, list)
|
||||
assert len(ret) == 9
|
||||
@ -1269,11 +1314,12 @@ def test_get_markets(default_conf, mocker, markets):
|
||||
assert ret[0]["id"] == "ethbtc"
|
||||
assert ret[0]["symbol"] == "ETH/BTC"
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_markets', 'fetch_markets')
|
||||
|
||||
|
||||
def test_get_fee(default_conf, mocker):
|
||||
@pytest.mark.parametrize("exchange_name", EXCHANGES)
|
||||
def test_get_fee(default_conf, mocker, exchange_name):
|
||||
api_mock = MagicMock()
|
||||
api_mock.calculate_fee = MagicMock(return_value={
|
||||
'type': 'taker',
|
||||
@ -1281,11 +1327,11 @@ def test_get_fee(default_conf, mocker):
|
||||
'rate': 0.025,
|
||||
'cost': 0.05
|
||||
})
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock)
|
||||
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
|
||||
|
||||
assert exchange.get_fee() == 0.025
|
||||
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock,
|
||||
ccxt_exceptionhandlers(mocker, default_conf, api_mock, exchange_name,
|
||||
'get_fee', 'calculate_fee')
|
||||
|
||||
|
||||
|
@ -51,7 +51,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'date': ANY,
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
@ -72,7 +71,6 @@ def test_rpc_trade_status(default_conf, ticker, fee, markets, mocker) -> None:
|
||||
assert {
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'date': ANY,
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
|
@ -5,7 +5,7 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
from random import randint
|
||||
from unittest.mock import MagicMock, ANY
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import arrow
|
||||
import pytest
|
||||
@ -183,7 +183,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
get_pair_detail_url=MagicMock(),
|
||||
get_fee=fee,
|
||||
get_markets=markets
|
||||
)
|
||||
@ -195,7 +194,6 @@ def test_status(default_conf, update, mocker, fee, ticker, markets) -> None:
|
||||
_rpc_trade_status=MagicMock(return_value=[{
|
||||
'trade_id': 1,
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'date': arrow.utcnow(),
|
||||
'open_rate': 1.099e-05,
|
||||
'close_rate': None,
|
||||
@ -270,7 +268,7 @@ def test_status_handle(default_conf, update, ticker, fee, markets, mocker) -> No
|
||||
telegram._status(bot=MagicMock(), update=update)
|
||||
|
||||
assert msg_mock.call_count == 1
|
||||
assert '[ETH/BTC]' in msg_mock.call_args_list[0][0][0]
|
||||
assert 'ETH/BTC' in msg_mock.call_args_list[0][0][0]
|
||||
|
||||
|
||||
def test_status_table_handle(default_conf, update, ticker, fee, markets, mocker) -> None:
|
||||
@ -721,7 +719,6 @@ def test_forcesell_handle(default_conf, update, ticker, fee,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'profit',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.172e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -776,7 +773,6 @@ def test_forcesell_down_handle(default_conf, update, ticker, fee,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.044e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -796,7 +792,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||
return_value=15000.0)
|
||||
rpc_mock = mocker.patch('freqtrade.rpc.telegram.Telegram.send_msg', MagicMock())
|
||||
mocker.patch('freqtrade.rpc.telegram.Telegram._init', MagicMock())
|
||||
mocker.patch('freqtrade.exchange.Exchange.get_pair_detail_url', MagicMock())
|
||||
mocker.patch.multiple(
|
||||
'freqtrade.exchange.Exchange',
|
||||
get_ticker=ticker,
|
||||
@ -823,7 +818,6 @@ def test_forcesell_all_handle(default_conf, update, ticker, fee, markets, mocker
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': ANY,
|
||||
'limit': 1.098e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -1100,7 +1094,6 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.099e-05,
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
@ -1108,7 +1101,7 @@ def test_send_msg_buy_notification(default_conf, mocker) -> None:
|
||||
'fiat_currency': 'USD'
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||
'with limit `0.00001099\n' \
|
||||
'(0.001000 BTC,0.000 USD)`'
|
||||
|
||||
@ -1129,7 +1122,6 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
@ -1141,8 +1133,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
'sell_reason': SellType.STOP_LOSS.value
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Selling [KEY/ETH]'
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n'
|
||||
== ('*Binance:* Selling KEY/ETH\n'
|
||||
'*Limit:* `0.00003201`\n'
|
||||
'*Amount:* `1333.33333333`\n'
|
||||
'*Open Rate:* `0.00007500`\n'
|
||||
@ -1156,7 +1147,6 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
@ -1167,8 +1157,7 @@ def test_send_msg_sell_notification(default_conf, mocker) -> None:
|
||||
'sell_reason': SellType.STOP_LOSS.value
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== ('*Binance:* Selling [KEY/ETH]'
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n'
|
||||
== ('*Binance:* Selling KEY/ETH\n'
|
||||
'*Limit:* `0.00003201`\n'
|
||||
'*Amount:* `1333.33333333`\n'
|
||||
'*Open Rate:* `0.00007500`\n'
|
||||
@ -1256,7 +1245,6 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.099e-05,
|
||||
'stake_amount': 0.001,
|
||||
'stake_amount_fiat': 0.0,
|
||||
@ -1264,7 +1252,7 @@ def test_send_msg_buy_notification_no_fiat(default_conf, mocker) -> None:
|
||||
'fiat_currency': None
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Bittrex:* Buying [ETH/BTC](https://bittrex.com/Market/Index?MarketName=BTC-ETH)\n' \
|
||||
== '*Bittrex:* Buying ETH/BTC\n' \
|
||||
'with limit `0.00001099\n' \
|
||||
'(0.001000 BTC)`'
|
||||
|
||||
@ -1284,7 +1272,6 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||
'exchange': 'Binance',
|
||||
'pair': 'KEY/ETH',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://www.binance.com/tradeDetail.html?symbol=KEY_ETH',
|
||||
'limit': 3.201e-05,
|
||||
'amount': 1333.3333333333335,
|
||||
'open_rate': 7.5e-05,
|
||||
@ -1296,8 +1283,7 @@ def test_send_msg_sell_notification_no_fiat(default_conf, mocker) -> None:
|
||||
'sell_reason': SellType.STOP_LOSS.value
|
||||
})
|
||||
assert msg_mock.call_args[0][0] \
|
||||
== '*Binance:* Selling [KEY/ETH]' \
|
||||
'(https://www.binance.com/tradeDetail.html?symbol=KEY_ETH)\n' \
|
||||
== '*Binance:* Selling KEY/ETH\n' \
|
||||
'*Limit:* `0.00003201`\n' \
|
||||
'*Amount:* `1333.33333333`\n' \
|
||||
'*Open Rate:* `0.00007500`\n' \
|
||||
|
@ -48,7 +48,6 @@ def test_send_msg(default_conf, mocker):
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
@ -73,7 +72,6 @@ def test_send_msg(default_conf, mocker):
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': "profit",
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'amount': 0.8,
|
||||
'open_rate': 0.004,
|
||||
@ -127,7 +125,6 @@ def test_exception_send_msg(default_conf, mocker, caplog):
|
||||
'type': RPCMessageType.BUY_NOTIFICATION,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'market_url': "http://mockedurl/ETH_BTC",
|
||||
'limit': 0.005,
|
||||
'stake_amount': 0.8,
|
||||
'stake_amount_fiat': 500,
|
||||
|
@ -16,7 +16,7 @@ def test_parse_args_none() -> None:
|
||||
|
||||
def test_parse_args_defaults() -> None:
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
assert args.config == 'config.json'
|
||||
assert args.config == ['config.json']
|
||||
assert args.strategy_path is None
|
||||
assert args.datadir is None
|
||||
assert args.loglevel == 0
|
||||
@ -24,10 +24,15 @@ def test_parse_args_defaults() -> None:
|
||||
|
||||
def test_parse_args_config() -> None:
|
||||
args = Arguments(['-c', '/dev/null'], '').get_parsed_arg()
|
||||
assert args.config == '/dev/null'
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null'], '').get_parsed_arg()
|
||||
assert args.config == '/dev/null'
|
||||
assert args.config == ['/dev/null']
|
||||
|
||||
args = Arguments(['--config', '/dev/null',
|
||||
'--config', '/dev/zero'],
|
||||
'').get_parsed_arg()
|
||||
assert args.config == ['/dev/null', '/dev/zero']
|
||||
|
||||
|
||||
def test_parse_args_db_url() -> None:
|
||||
@ -139,7 +144,7 @@ def test_parse_args_backtesting_custom() -> None:
|
||||
'TestStrategy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == 'test_conf.json'
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.live is True
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
@ -158,7 +163,7 @@ def test_parse_args_hyperopt_custom() -> None:
|
||||
'--spaces', 'buy'
|
||||
]
|
||||
call_args = Arguments(args, '').get_parsed_arg()
|
||||
assert call_args.config == 'test_conf.json'
|
||||
assert call_args.config == ['test_conf.json']
|
||||
assert call_args.epochs == 20
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
|
@ -1,15 +1,15 @@
|
||||
# pragma pylint: disable=missing-docstring, protected-access, invalid-name
|
||||
|
||||
import json
|
||||
from argparse import Namespace
|
||||
import logging
|
||||
from argparse import Namespace
|
||||
from copy import deepcopy
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
from jsonschema import validate, ValidationError, Draft4Validator
|
||||
from jsonschema import Draft4Validator, ValidationError, validate
|
||||
|
||||
from freqtrade import constants
|
||||
from freqtrade import OperationalException
|
||||
from freqtrade import OperationalException, constants
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.configuration import Configuration, set_loggers
|
||||
from freqtrade.constants import DEFAULT_DB_DRYRUN_URL, DEFAULT_DB_PROD_URL
|
||||
@ -50,18 +50,49 @@ def test_load_config_file(default_conf, mocker, caplog) -> None:
|
||||
validated_conf = configuration._load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
assert validated_conf.items() >= default_conf.items()
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_load_config_max_open_trades_zero(default_conf, mocker, caplog) -> None:
|
||||
default_conf['max_open_trades'] = 0
|
||||
file_mock = mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
mocker.patch('freqtrade.configuration.open', mocker.mock_open(
|
||||
read_data=json.dumps(default_conf)
|
||||
))
|
||||
|
||||
Configuration(Namespace())._load_config_file('somefile')
|
||||
assert file_mock.call_count == 1
|
||||
args = Arguments([], '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
assert validated_conf['max_open_trades'] == 0
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
def test_load_config_combine_dicts(default_conf, mocker, caplog) -> None:
|
||||
conf1 = deepcopy(default_conf)
|
||||
conf2 = deepcopy(default_conf)
|
||||
del conf1['exchange']['key']
|
||||
del conf1['exchange']['secret']
|
||||
del conf2['exchange']['name']
|
||||
conf2['exchange']['pair_whitelist'] += ['NANO/BTC']
|
||||
|
||||
config_files = [conf1, conf2]
|
||||
|
||||
configsmock = MagicMock(side_effect=config_files)
|
||||
mocker.patch('freqtrade.configuration.Configuration._load_config_file', configsmock)
|
||||
|
||||
arg_list = ['-c', 'test_conf.json', '--config', 'test2_conf.json', ]
|
||||
args = Arguments(arg_list, '').get_parsed_arg()
|
||||
configuration = Configuration(args)
|
||||
validated_conf = configuration.load_config()
|
||||
|
||||
exchange_conf = default_conf['exchange']
|
||||
assert validated_conf['exchange']['name'] == exchange_conf['name']
|
||||
assert validated_conf['exchange']['key'] == exchange_conf['key']
|
||||
assert validated_conf['exchange']['secret'] == exchange_conf['secret']
|
||||
assert validated_conf['exchange']['pair_whitelist'] != conf1['exchange']['pair_whitelist']
|
||||
assert validated_conf['exchange']['pair_whitelist'] == conf2['exchange']['pair_whitelist']
|
||||
|
||||
assert 'internals' in validated_conf
|
||||
assert log_has('Validating configuration ...', caplog.record_tuples)
|
||||
|
||||
|
||||
|
@ -1872,7 +1872,6 @@ def test_execute_sell_up(default_conf, ticker, fee, ticker_sell_up, markets, moc
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'profit',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.172e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -1919,7 +1918,6 @@ def test_execute_sell_down(default_conf, ticker, fee, ticker_sell_down, markets,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.044e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -1974,7 +1972,6 @@ def test_execute_sell_down_stoploss_on_exchange_dry_run(default_conf, ticker, fe
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.08801e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -2146,7 +2143,6 @@ def test_execute_sell_without_conf_sell_up(default_conf, ticker, fee,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'profit',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.172e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
@ -2194,7 +2190,6 @@ def test_execute_sell_without_conf_sell_down(default_conf, ticker, fee,
|
||||
'exchange': 'Bittrex',
|
||||
'pair': 'ETH/BTC',
|
||||
'gain': 'loss',
|
||||
'market_url': 'https://bittrex.com/Market/Index?MarketName=BTC-ETH',
|
||||
'limit': 1.044e-05,
|
||||
'amount': 90.99181073703367,
|
||||
'open_rate': 1.099e-05,
|
||||
|
@ -22,7 +22,7 @@ def test_parse_args_backtesting(mocker) -> None:
|
||||
main(['backtesting'])
|
||||
assert backtesting_mock.call_count == 1
|
||||
call_args = backtesting_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
assert call_args.config == ['config.json']
|
||||
assert call_args.live is False
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'backtesting'
|
||||
@ -35,7 +35,7 @@ def test_main_start_hyperopt(mocker) -> None:
|
||||
main(['hyperopt'])
|
||||
assert hyperopt_mock.call_count == 1
|
||||
call_args = hyperopt_mock.call_args[0][0]
|
||||
assert call_args.config == 'config.json'
|
||||
assert call_args.config == ['config.json']
|
||||
assert call_args.loglevel == 0
|
||||
assert call_args.subparser == 'hyperopt'
|
||||
assert call_args.func is not None
|
||||
|
@ -629,3 +629,48 @@ def test_adjust_stop_loss(limit_buy_order, limit_sell_order, fee):
|
||||
assert round(trade.stop_loss, 8) == 1.26
|
||||
assert trade.max_rate == 1.4
|
||||
assert trade.initial_stop_loss == 0.95
|
||||
|
||||
|
||||
def test_get_open(default_conf, fee):
|
||||
init(default_conf)
|
||||
|
||||
# Simulate dry_run entries
|
||||
trade = Trade(
|
||||
pair='ETH/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='dry_run_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
is_open=False,
|
||||
open_order_id='dry_run_sell_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
# Simulate prod entry
|
||||
trade = Trade(
|
||||
pair='ETC/BTC',
|
||||
stake_amount=0.001,
|
||||
amount=123.0,
|
||||
fee_open=fee.return_value,
|
||||
fee_close=fee.return_value,
|
||||
open_rate=0.123,
|
||||
exchange='bittrex',
|
||||
open_order_id='prod_buy_12345'
|
||||
)
|
||||
Trade.session.add(trade)
|
||||
|
||||
assert len(Trade.get_open_trades()) == 2
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
flake8==3.7.6
|
||||
flake8==3.7.7
|
||||
flake8-type-annotations==0.1.0
|
||||
flake8-tidy-imports==2.0.0
|
||||
pytest==4.3.0
|
||||
|
@ -1,4 +1,4 @@
|
||||
ccxt==1.18.281
|
||||
ccxt==1.18.313
|
||||
SQLAlchemy==1.2.18
|
||||
python-telegram-bot==11.1.0
|
||||
arrow==0.13.1
|
||||
@ -6,12 +6,12 @@ cachetools==3.1.0
|
||||
requests==2.21.0
|
||||
urllib3==1.24.1
|
||||
wrapt==1.11.1
|
||||
numpy==1.16.1
|
||||
numpy==1.16.2
|
||||
pandas==0.24.1
|
||||
scikit-learn==0.20.2
|
||||
scikit-learn==0.20.3
|
||||
joblib==0.13.2
|
||||
scipy==1.2.1
|
||||
jsonschema==2.6.0
|
||||
jsonschema==3.0.1
|
||||
TA-Lib==0.4.17
|
||||
tabulate==0.8.3
|
||||
coinmarketcap==5.0.3
|
||||
|
@ -5,12 +5,14 @@ import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import arrow
|
||||
from typing import Any, Dict
|
||||
|
||||
from freqtrade import arguments
|
||||
from freqtrade.arguments import Arguments
|
||||
from freqtrade.arguments import TimeRange
|
||||
from freqtrade.exchange import Exchange
|
||||
from freqtrade.data.history import download_pair_history
|
||||
from freqtrade.configuration import Configuration, set_loggers
|
||||
from freqtrade.misc import deep_merge_dicts
|
||||
|
||||
import logging
|
||||
logging.basicConfig(
|
||||
@ -21,7 +23,7 @@ set_loggers(0)
|
||||
|
||||
DEFAULT_DL_PATH = 'user_data/data'
|
||||
|
||||
arguments = arguments.Arguments(sys.argv[1:], 'download utility')
|
||||
arguments = Arguments(sys.argv[1:], 'download utility')
|
||||
arguments.testdata_dl_options()
|
||||
args = arguments.parse_args()
|
||||
|
||||
@ -29,7 +31,13 @@ timeframes = args.timeframes
|
||||
|
||||
if args.config:
|
||||
configuration = Configuration(args)
|
||||
config = configuration._load_config_file(args.config)
|
||||
|
||||
config: Dict[str, Any] = {}
|
||||
# Now expecting a list of config filenames here, not a string
|
||||
for path in args.config:
|
||||
print('Using config: %s ...', path)
|
||||
# Merge config options, overwriting old values
|
||||
config = deep_merge_dicts(configuration._load_config_file(path), config)
|
||||
|
||||
config['stake_currency'] = ''
|
||||
# Ensure we do not use Exchange credentials
|
||||
|
Loading…
Reference in New Issue
Block a user