Merge branch 'fix-docs' of https://github.com/stash86/freqtrade into fix-docs
This commit is contained in:
commit
3b4051488f
110
docs/plotting.md
110
docs/plotting.md
@ -164,16 +164,17 @@ The resulting plot will have the following elements:
|
|||||||
|
|
||||||
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
An advanced plot configuration can be specified in the strategy in the `plot_config` parameter.
|
||||||
|
|
||||||
Additional features when using plot_config include:
|
Additional features when using `plot_config` include:
|
||||||
|
|
||||||
* Specify colors per indicator
|
* Specify colors per indicator
|
||||||
* Specify additional subplots
|
* Specify additional subplots
|
||||||
* Specify indicator pairs to fill area in between
|
* Specify indicator pairs to fill area in between
|
||||||
|
|
||||||
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
The sample plot configuration below specifies fixed colors for the indicators. Otherwise, consecutive plots may produce different color schemes each time, making comparisons difficult.
|
||||||
It also allows multiple subplots to display both MACD and RSI at the same time.
|
It also allows multiple subplots to display both MACD and RSI at the same time.
|
||||||
|
|
||||||
Plot type can be configured using `type` key. Possible types are:
|
Plot type can be configured using `type` key. Possible types are:
|
||||||
|
|
||||||
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
* `scatter` corresponding to `plotly.graph_objects.Scatter` class (default).
|
||||||
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
* `bar` corresponding to `plotly.graph_objects.Bar` class.
|
||||||
|
|
||||||
@ -182,40 +183,89 @@ Extra parameters to `plotly.graph_objects.*` constructor can be specified in `pl
|
|||||||
Sample configuration with inline comments explaining the process:
|
Sample configuration with inline comments explaining the process:
|
||||||
|
|
||||||
``` python
|
``` python
|
||||||
plot_config = {
|
@property
|
||||||
'main_plot': {
|
def plot_config(self):
|
||||||
# Configuration for main plot indicators.
|
"""
|
||||||
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
There are a lot of solutions how to build the return dictionary.
|
||||||
'ema10': {'color': 'red'},
|
The only important point is the return value.
|
||||||
'ema50': {'color': '#CCCCCC'},
|
Example:
|
||||||
# By omitting color, a random color is selected.
|
plot_config = {'main_plot': {}, 'subplots': {}}
|
||||||
'sar': {},
|
|
||||||
# fill area between senkou_a and senkou_b
|
"""
|
||||||
'senkou_a': {
|
plot_config = {}
|
||||||
'color': 'green', #optional
|
plot_config['main_plot'] = {
|
||||||
'fill_to': 'senkou_b',
|
# Configuration for main plot indicators.
|
||||||
'fill_label': 'Ichimoku Cloud', #optional
|
# Assumes 2 parameters, emashort and emalong to be specified.
|
||||||
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
f'ema_{self.emashort.value}': {'color': 'red'},
|
||||||
},
|
f'ema_{self.emalong.value}': {'color': '#CCCCCC'},
|
||||||
# plot senkou_b, too. Not only the area to it.
|
# By omitting color, a random color is selected.
|
||||||
'senkou_b': {}
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
},
|
},
|
||||||
'subplots': {
|
# plot senkou_b, too. Not only the area to it.
|
||||||
# Create subplot MACD
|
'senkou_b': {}
|
||||||
"MACD": {
|
}
|
||||||
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
plot_config['subplots'] = {
|
||||||
'macdsignal': {'color': 'orange'},
|
# Create subplot MACD
|
||||||
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
"MACD": {
|
||||||
},
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
# Additional subplot RSI
|
'macdsignal': {'color': 'orange'},
|
||||||
"RSI": {
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
'rsi': {'color': 'red'}
|
},
|
||||||
}
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return plot_config
|
||||||
```
|
```
|
||||||
|
|
||||||
|
??? Note "As attribute (former method)"
|
||||||
|
Assigning plot_config is also possible as Attribute (this used to be the default way).
|
||||||
|
This has the disadvantage that strategy parameters are not available, preventing certain configurations from working.
|
||||||
|
|
||||||
|
``` python
|
||||||
|
plot_config = {
|
||||||
|
'main_plot': {
|
||||||
|
# Configuration for main plot indicators.
|
||||||
|
# Specifies `ema10` to be red, and `ema50` to be a shade of gray
|
||||||
|
'ema10': {'color': 'red'},
|
||||||
|
'ema50': {'color': '#CCCCCC'},
|
||||||
|
# By omitting color, a random color is selected.
|
||||||
|
'sar': {},
|
||||||
|
# fill area between senkou_a and senkou_b
|
||||||
|
'senkou_a': {
|
||||||
|
'color': 'green', #optional
|
||||||
|
'fill_to': 'senkou_b',
|
||||||
|
'fill_label': 'Ichimoku Cloud', #optional
|
||||||
|
'fill_color': 'rgba(255,76,46,0.2)', #optional
|
||||||
|
},
|
||||||
|
# plot senkou_b, too. Not only the area to it.
|
||||||
|
'senkou_b': {}
|
||||||
|
},
|
||||||
|
'subplots': {
|
||||||
|
# Create subplot MACD
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue', 'fill_to': 'macdhist'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
'macdhist': {'type': 'bar', 'plotly': {'opacity': 0.9}}
|
||||||
|
},
|
||||||
|
# Additional subplot RSI
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
!!! Note
|
!!! Note
|
||||||
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
The above configuration assumes that `ema10`, `ema50`, `senkou_a`, `senkou_b`,
|
||||||
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
`macd`, `macdsignal`, `macdhist` and `rsi` are columns in the DataFrame created by the strategy.
|
||||||
|
@ -50,7 +50,9 @@ candles.head()
|
|||||||
```python
|
```python
|
||||||
# Load strategy using values set above
|
# Load strategy using values set above
|
||||||
from freqtrade.resolvers import StrategyResolver
|
from freqtrade.resolvers import StrategyResolver
|
||||||
|
from freqtrade.data.dataprovider import DataProvider
|
||||||
strategy = StrategyResolver.load_strategy(config)
|
strategy = StrategyResolver.load_strategy(config)
|
||||||
|
strategy.dp = DataProvider(config, None, None)
|
||||||
|
|
||||||
# Generate buy/sell signals using strategy
|
# Generate buy/sell signals using strategy
|
||||||
df = strategy.analyze_ticker(candles, {'pair': pair})
|
df = strategy.analyze_ticker(candles, {'pair': pair})
|
||||||
@ -228,7 +230,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a separate window
|
# Render graph in a seperate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -1294,7 +1294,7 @@ class Exchange:
|
|||||||
cached_pairs = []
|
cached_pairs = []
|
||||||
# Gather coroutines to run
|
# Gather coroutines to run
|
||||||
for pair, timeframe in set(pair_list):
|
for pair, timeframe in set(pair_list):
|
||||||
if ((pair, timeframe) not in self._klines
|
if ((pair, timeframe) not in self._klines or not cache
|
||||||
or self._now_is_time_to_refresh(pair, timeframe)):
|
or self._now_is_time_to_refresh(pair, timeframe)):
|
||||||
if not since_ms and self.required_candle_call_count > 1:
|
if not since_ms and self.required_candle_call_count > 1:
|
||||||
# Multiple calls for one pair - to get more history
|
# Multiple calls for one pair - to get more history
|
||||||
|
@ -466,8 +466,8 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
logger.info(f"Bids to asks delta for {pair} does not satisfy condition.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None,
|
def execute_entry(self, pair: str, stake_amount: float, price: Optional[float] = None, *,
|
||||||
forcebuy: bool = False, buy_tag: Optional[str] = None) -> bool:
|
ordertype: Optional[str] = None, buy_tag: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a limit buy for the given pair
|
Executes a limit buy for the given pair
|
||||||
:param pair: pair for which we want to create a LIMIT_BUY
|
:param pair: pair for which we want to create a LIMIT_BUY
|
||||||
@ -510,10 +510,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
f"{stake_amount} ...")
|
f"{stake_amount} ...")
|
||||||
|
|
||||||
amount = stake_amount / enter_limit_requested
|
amount = stake_amount / enter_limit_requested
|
||||||
order_type = self.strategy.order_types['buy']
|
order_type = ordertype or self.strategy.order_types['buy']
|
||||||
if forcebuy:
|
|
||||||
# Forcebuy can define a different ordertype
|
|
||||||
order_type = self.strategy.order_types.get('forcebuy', order_type)
|
|
||||||
|
|
||||||
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
|
||||||
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
pair=pair, order_type=order_type, amount=amount, rate=enter_limit_requested,
|
||||||
@ -868,7 +865,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
logger.info(
|
logger.info(
|
||||||
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
f'Executing Sell for {trade.pair}. Reason: {should_sell.sell_type}. '
|
||||||
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
f'Tag: {exit_tag if exit_tag is not None else "None"}')
|
||||||
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag)
|
self.execute_trade_exit(trade, exit_rate, should_sell, exit_tag=exit_tag)
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -1081,7 +1078,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
trade: Trade,
|
trade: Trade,
|
||||||
limit: float,
|
limit: float,
|
||||||
sell_reason: SellCheckTuple,
|
sell_reason: SellCheckTuple,
|
||||||
exit_tag: Optional[str] = None) -> bool:
|
*,
|
||||||
|
exit_tag: Optional[str] = None,
|
||||||
|
ordertype: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Executes a trade exit for the given trade and limit
|
Executes a trade exit for the given trade and limit
|
||||||
:param trade: Trade instance
|
:param trade: Trade instance
|
||||||
@ -1119,14 +1119,10 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
except InvalidOrderException:
|
except InvalidOrderException:
|
||||||
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
logger.exception(f"Could not cancel stoploss order {trade.stoploss_order_id}")
|
||||||
|
|
||||||
order_type = self.strategy.order_types[sell_type]
|
order_type = ordertype or self.strategy.order_types[sell_type]
|
||||||
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
if sell_reason.sell_type == SellType.EMERGENCY_SELL:
|
||||||
# Emergency sells (default to market!)
|
# Emergency sells (default to market!)
|
||||||
order_type = self.strategy.order_types.get("emergencysell", "market")
|
order_type = self.strategy.order_types.get("emergencysell", "market")
|
||||||
if sell_reason.sell_type == SellType.FORCE_SELL:
|
|
||||||
# Force sells (default to the sell_type defined in the strategy,
|
|
||||||
# but we allow this value to be changed)
|
|
||||||
order_type = self.strategy.order_types.get("forcesell", order_type)
|
|
||||||
|
|
||||||
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
amount = self._safe_exit_amount(trade.pair, trade.amount)
|
||||||
time_in_force = self.strategy.order_time_in_force['sell']
|
time_in_force = self.strategy.order_time_in_force['sell']
|
||||||
|
@ -46,20 +46,11 @@ def _get_line_floatfmt(stake_currency: str) -> List[str]:
|
|||||||
'.2f', 'd', 's', 's']
|
'.2f', 'd', 's', 's']
|
||||||
|
|
||||||
|
|
||||||
def _get_line_header(first_column: str, stake_currency: str) -> List[str]:
|
def _get_line_header(first_column: str, stake_currency: str, direction: str = 'Buys') -> List[str]:
|
||||||
"""
|
"""
|
||||||
Generate header lines (goes in line with _generate_result_line())
|
Generate header lines (goes in line with _generate_result_line())
|
||||||
"""
|
"""
|
||||||
return [first_column, 'Buys', 'Avg Profit %', 'Cum Profit %',
|
return [first_column, direction, 'Avg Profit %', 'Cum Profit %',
|
||||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
|
||||||
'Win Draw Loss Win%']
|
|
||||||
|
|
||||||
|
|
||||||
def _get_line_header_sell(first_column: str, stake_currency: str) -> List[str]:
|
|
||||||
"""
|
|
||||||
Generate header lines (goes in line with _generate_result_line())
|
|
||||||
"""
|
|
||||||
return [first_column, 'Sells', 'Avg Profit %', 'Cum Profit %',
|
|
||||||
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
f'Tot Profit {stake_currency}', 'Tot Profit %', 'Avg Duration',
|
||||||
'Win Draw Loss Win%']
|
'Win Draw Loss Win%']
|
||||||
|
|
||||||
@ -156,7 +147,7 @@ def generate_tag_metrics(tag_type: str,
|
|||||||
if skip_nan and result['profit_abs'].isnull().all():
|
if skip_nan and result['profit_abs'].isnull().all():
|
||||||
continue
|
continue
|
||||||
|
|
||||||
tabular_data.append(_generate_tag_result_line(result, starting_balance, tag))
|
tabular_data.append(_generate_result_line(result, starting_balance, tag))
|
||||||
|
|
||||||
# Sort by total profit %:
|
# Sort by total profit %:
|
||||||
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
tabular_data = sorted(tabular_data, key=lambda k: k['profit_total_abs'], reverse=True)
|
||||||
@ -168,39 +159,6 @@ def generate_tag_metrics(tag_type: str,
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
def _generate_tag_result_line(result: DataFrame, starting_balance: int, first_column: str) -> Dict:
|
|
||||||
"""
|
|
||||||
Generate one result dict, with "first_column" as key.
|
|
||||||
"""
|
|
||||||
profit_sum = result['profit_ratio'].sum()
|
|
||||||
# (end-capital - starting capital) / starting capital
|
|
||||||
profit_total = result['profit_abs'].sum() / starting_balance
|
|
||||||
|
|
||||||
return {
|
|
||||||
'key': first_column,
|
|
||||||
'trades': len(result),
|
|
||||||
'profit_mean': result['profit_ratio'].mean() if len(result) > 0 else 0.0,
|
|
||||||
'profit_mean_pct': result['profit_ratio'].mean() * 100.0 if len(result) > 0 else 0.0,
|
|
||||||
'profit_sum': profit_sum,
|
|
||||||
'profit_sum_pct': round(profit_sum * 100.0, 2),
|
|
||||||
'profit_total_abs': result['profit_abs'].sum(),
|
|
||||||
'profit_total': profit_total,
|
|
||||||
'profit_total_pct': round(profit_total * 100.0, 2),
|
|
||||||
'duration_avg': str(timedelta(
|
|
||||||
minutes=round(result['trade_duration'].mean()))
|
|
||||||
) if not result.empty else '0:00',
|
|
||||||
# 'duration_max': str(timedelta(
|
|
||||||
# minutes=round(result['trade_duration'].max()))
|
|
||||||
# ) if not result.empty else '0:00',
|
|
||||||
# 'duration_min': str(timedelta(
|
|
||||||
# minutes=round(result['trade_duration'].min()))
|
|
||||||
# ) if not result.empty else '0:00',
|
|
||||||
'wins': len(result[result['profit_abs'] > 0]),
|
|
||||||
'draws': len(result[result['profit_abs'] == 0]),
|
|
||||||
'losses': len(result[result['profit_abs'] < 0]),
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
def generate_sell_reason_stats(max_open_trades: int, results: DataFrame) -> List[Dict]:
|
||||||
"""
|
"""
|
||||||
Generate small table outlining Backtest results
|
Generate small table outlining Backtest results
|
||||||
@ -631,7 +589,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
|
|||||||
if(tag_type == "buy_tag"):
|
if(tag_type == "buy_tag"):
|
||||||
headers = _get_line_header("TAG", stake_currency)
|
headers = _get_line_header("TAG", stake_currency)
|
||||||
else:
|
else:
|
||||||
headers = _get_line_header_sell("TAG", stake_currency)
|
headers = _get_line_header("TAG", stake_currency, 'Sells')
|
||||||
floatfmt = _get_line_floatfmt(stake_currency)
|
floatfmt = _get_line_floatfmt(stake_currency)
|
||||||
output = [
|
output = [
|
||||||
[
|
[
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
from enum import Enum
|
||||||
from typing import Any, Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Union
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@ -131,13 +132,21 @@ class UnfilledTimeout(BaseModel):
|
|||||||
exit_timeout_count: Optional[int]
|
exit_timeout_count: Optional[int]
|
||||||
|
|
||||||
|
|
||||||
|
class OrderTypeValues(Enum):
|
||||||
|
limit = 'limit'
|
||||||
|
market = 'market'
|
||||||
|
|
||||||
|
class Config:
|
||||||
|
use_enum_values = True
|
||||||
|
|
||||||
|
|
||||||
class OrderTypes(BaseModel):
|
class OrderTypes(BaseModel):
|
||||||
buy: str
|
buy: OrderTypeValues
|
||||||
sell: str
|
sell: OrderTypeValues
|
||||||
emergencysell: Optional[str]
|
emergencysell: Optional[OrderTypeValues]
|
||||||
forcesell: Optional[str]
|
forcesell: Optional[OrderTypeValues]
|
||||||
forcebuy: Optional[str]
|
forcebuy: Optional[OrderTypeValues]
|
||||||
stoploss: str
|
stoploss: OrderTypeValues
|
||||||
stoploss_on_exchange: bool
|
stoploss_on_exchange: bool
|
||||||
stoploss_on_exchange_interval: Optional[int]
|
stoploss_on_exchange_interval: Optional[int]
|
||||||
|
|
||||||
@ -274,10 +283,12 @@ class Logs(BaseModel):
|
|||||||
class ForceBuyPayload(BaseModel):
|
class ForceBuyPayload(BaseModel):
|
||||||
pair: str
|
pair: str
|
||||||
price: Optional[float]
|
price: Optional[float]
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class ForceSellPayload(BaseModel):
|
class ForceSellPayload(BaseModel):
|
||||||
tradeid: str
|
tradeid: str
|
||||||
|
ordertype: Optional[OrderTypeValues]
|
||||||
|
|
||||||
|
|
||||||
class BlacklistPayload(BaseModel):
|
class BlacklistPayload(BaseModel):
|
||||||
|
@ -29,7 +29,8 @@ logger = logging.getLogger(__name__)
|
|||||||
# API version
|
# API version
|
||||||
# Pre-1.1, no version was provided
|
# Pre-1.1, no version was provided
|
||||||
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
# Version increments should happen in "small" steps (1.1, 1.12, ...) unless big changes happen.
|
||||||
API_VERSION = 1.1
|
# 1.11: forcebuy and forcesell accept ordertype
|
||||||
|
API_VERSION = 1.11
|
||||||
|
|
||||||
# Public API, requires no auth.
|
# Public API, requires no auth.
|
||||||
router_public = APIRouter()
|
router_public = APIRouter()
|
||||||
@ -129,7 +130,8 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
|
|||||||
|
|
||||||
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
|
||||||
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
trade = rpc._rpc_forcebuy(payload.pair, payload.price)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype)
|
||||||
|
|
||||||
if trade:
|
if trade:
|
||||||
return ForceBuyResponse.parse_obj(trade.to_json())
|
return ForceBuyResponse.parse_obj(trade.to_json())
|
||||||
@ -139,7 +141,8 @@ def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
|
|||||||
|
|
||||||
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
|
||||||
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
|
||||||
return rpc._rpc_forcesell(payload.tradeid)
|
ordertype = payload.ordertype.value if payload.ordertype else None
|
||||||
|
return rpc._rpc_forcesell(payload.tradeid, ordertype)
|
||||||
|
|
||||||
|
|
||||||
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
|
||||||
|
@ -640,7 +640,7 @@ class RPC:
|
|||||||
|
|
||||||
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
|
||||||
|
|
||||||
def _rpc_forcesell(self, trade_id: str) -> Dict[str, str]:
|
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Handler for forcesell <id>.
|
Handler for forcesell <id>.
|
||||||
Sells the given trade at current price
|
Sells the given trade at current price
|
||||||
@ -664,7 +664,11 @@ class RPC:
|
|||||||
current_rate = self._freqtrade.exchange.get_rate(
|
current_rate = self._freqtrade.exchange.get_rate(
|
||||||
trade.pair, refresh=False, side="sell")
|
trade.pair, refresh=False, side="sell")
|
||||||
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
|
||||||
self._freqtrade.execute_trade_exit(trade, current_rate, sell_reason)
|
order_type = ordertype or self._freqtrade.strategy.order_types.get(
|
||||||
|
"forcesell", self._freqtrade.strategy.order_types["sell"])
|
||||||
|
|
||||||
|
self._freqtrade.execute_trade_exit(
|
||||||
|
trade, current_rate, sell_reason, ordertype=order_type)
|
||||||
# ---- EOF def _exec_forcesell ----
|
# ---- EOF def _exec_forcesell ----
|
||||||
|
|
||||||
if self._freqtrade.state != State.RUNNING:
|
if self._freqtrade.state != State.RUNNING:
|
||||||
@ -692,7 +696,8 @@ class RPC:
|
|||||||
self._freqtrade.wallets.update()
|
self._freqtrade.wallets.update()
|
||||||
return {'result': f'Created sell order for trade {trade_id}.'}
|
return {'result': f'Created sell order for trade {trade_id}.'}
|
||||||
|
|
||||||
def _rpc_forcebuy(self, pair: str, price: Optional[float]) -> Optional[Trade]:
|
def _rpc_forcebuy(self, pair: str, price: Optional[float],
|
||||||
|
order_type: Optional[str] = None) -> Optional[Trade]:
|
||||||
"""
|
"""
|
||||||
Handler for forcebuy <asset> <price>
|
Handler for forcebuy <asset> <price>
|
||||||
Buys a pair trade at the given or current price
|
Buys a pair trade at the given or current price
|
||||||
@ -720,7 +725,10 @@ class RPC:
|
|||||||
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
stakeamount = self._freqtrade.wallets.get_trade_stake_amount(pair)
|
||||||
|
|
||||||
# execute buy
|
# execute buy
|
||||||
if self._freqtrade.execute_entry(pair, stakeamount, price, forcebuy=True):
|
if not order_type:
|
||||||
|
order_type = self._freqtrade.strategy.order_types.get(
|
||||||
|
'forcebuy', self._freqtrade.strategy.order_types['buy'])
|
||||||
|
if self._freqtrade.execute_entry(pair, stakeamount, price, ordertype=order_type):
|
||||||
Trade.commit()
|
Trade.commit()
|
||||||
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
|
||||||
return trade
|
return trade
|
||||||
|
@ -87,6 +87,7 @@ class {{ strategy }}(IStrategy):
|
|||||||
'sell': 'gtc'
|
'sell': 'gtc'
|
||||||
}
|
}
|
||||||
{{ plot_config | indent(4) }}
|
{{ plot_config | indent(4) }}
|
||||||
|
|
||||||
def informative_pairs(self):
|
def informative_pairs(self):
|
||||||
"""
|
"""
|
||||||
Define additional, informative pair/interval combinations to be cached from the exchange.
|
Define additional, informative pair/interval combinations to be cached from the exchange.
|
||||||
|
@ -79,7 +79,9 @@
|
|||||||
"source": [
|
"source": [
|
||||||
"# Load strategy using values set above\n",
|
"# Load strategy using values set above\n",
|
||||||
"from freqtrade.resolvers import StrategyResolver\n",
|
"from freqtrade.resolvers import StrategyResolver\n",
|
||||||
|
"from freqtrade.data.dataprovider import DataProvider\n",
|
||||||
"strategy = StrategyResolver.load_strategy(config)\n",
|
"strategy = StrategyResolver.load_strategy(config)\n",
|
||||||
|
"strategy.dp = DataProvider(config, None, None)\n",
|
||||||
"\n",
|
"\n",
|
||||||
"# Generate buy/sell signals using strategy\n",
|
"# Generate buy/sell signals using strategy\n",
|
||||||
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
"df = strategy.analyze_ticker(candles, {'pair': pair})\n",
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
|
|
||||||
plot_config = {
|
@property
|
||||||
# Main plot indicators (Moving averages, ...)
|
def plot_config(self):
|
||||||
'main_plot': {
|
return {
|
||||||
'tema': {},
|
# Main plot indicators (Moving averages, ...)
|
||||||
'sar': {'color': 'white'},
|
'main_plot': {
|
||||||
},
|
'tema': {},
|
||||||
'subplots': {
|
'sar': {'color': 'white'},
|
||||||
# Subplots - each dict defines one additional plot
|
|
||||||
"MACD": {
|
|
||||||
'macd': {'color': 'blue'},
|
|
||||||
'macdsignal': {'color': 'orange'},
|
|
||||||
},
|
},
|
||||||
"RSI": {
|
'subplots': {
|
||||||
'rsi': {'color': 'red'},
|
# Subplots - each dict defines one additional plot
|
||||||
|
"MACD": {
|
||||||
|
'macd': {'color': 'blue'},
|
||||||
|
'macdsignal': {'color': 'orange'},
|
||||||
|
},
|
||||||
|
"RSI": {
|
||||||
|
'rsi': {'color': 'red'},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@ -1667,12 +1667,21 @@ def test_refresh_latest_ohlcv(mocker, default_conf, caplog) -> None:
|
|||||||
assert len(res) == len(pairs)
|
assert len(res) == len(pairs)
|
||||||
|
|
||||||
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
assert exchange._api_async.fetch_ohlcv.call_count == 0
|
||||||
|
exchange.required_candle_call_count = 1
|
||||||
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
assert log_has(f"Using cached candle (OHLCV) data for pair {pairs[0][0]}, "
|
||||||
f"timeframe {pairs[0][1]} ...",
|
f"timeframe {pairs[0][1]} ...",
|
||||||
caplog)
|
caplog)
|
||||||
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
cache=False)
|
cache=False)
|
||||||
assert len(res) == 3
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
# Test the same again, should NOT return from cache!
|
||||||
|
exchange._api_async.fetch_ohlcv.reset_mock()
|
||||||
|
res = exchange.refresh_latest_ohlcv([('IOTA/ETH', '5m'), ('XRP/ETH', '5m'), ('XRP/ETH', '1d')],
|
||||||
|
cache=False)
|
||||||
|
assert len(res) == 3
|
||||||
|
assert exchange._api_async.fetch_ohlcv.call_count == 3
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
@ -1093,7 +1093,7 @@ def test_rpcforcebuy(mocker, default_conf, ticker, fee, limit_buy_order_open) ->
|
|||||||
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
with pytest.raises(RPCException, match=r'position for ETH/BTC already open - id: 1'):
|
||||||
rpc._rpc_forcebuy(pair, 0.0001)
|
rpc._rpc_forcebuy(pair, 0.0001)
|
||||||
pair = 'XRP/BTC'
|
pair = 'XRP/BTC'
|
||||||
trade = rpc._rpc_forcebuy(pair, 0.0001)
|
trade = rpc._rpc_forcebuy(pair, 0.0001, order_type='limit')
|
||||||
assert isinstance(trade, Trade)
|
assert isinstance(trade, Trade)
|
||||||
assert trade.pair == pair
|
assert trade.pair == pair
|
||||||
assert trade.open_rate == 0.0001
|
assert trade.open_rate == 0.0001
|
||||||
|
@ -24,6 +24,7 @@ from freqtrade.freqtradebot import FreqtradeBot
|
|||||||
from freqtrade.loggers import setup_logging
|
from freqtrade.loggers import setup_logging
|
||||||
from freqtrade.persistence import PairLocks, Trade
|
from freqtrade.persistence import PairLocks, Trade
|
||||||
from freqtrade.rpc import RPC
|
from freqtrade.rpc import RPC
|
||||||
|
from freqtrade.rpc.rpc import RPCException
|
||||||
from freqtrade.rpc.telegram import Telegram, authorized_only
|
from freqtrade.rpc.telegram import Telegram, authorized_only
|
||||||
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
|
from tests.conftest import (create_mock_trades, get_patched_freqtradebot, log_has, log_has_re,
|
||||||
patch_exchange, patch_get_signal, patch_whitelist)
|
patch_exchange, patch_get_signal, patch_whitelist)
|
||||||
@ -1186,8 +1187,8 @@ def test_forcebuy_no_pair(default_conf, update, mocker) -> None:
|
|||||||
assert fbuy_mock.call_count == 1
|
assert fbuy_mock.call_count == 1
|
||||||
|
|
||||||
|
|
||||||
def test_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
|
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
@ -1216,8 +1217,8 @@ def test_performance_handle(default_conf, update, ticker, fee,
|
|||||||
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>ETH/BTC\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1240,15 +1241,27 @@ def test_buy_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
context = MagicMock()
|
||||||
telegram._buy_tag_performance(update=update, context=MagicMock())
|
telegram._buy_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Buy Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>TESTBUY\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
context.args = [trade.pair]
|
||||||
|
telegram._buy_tag_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
def test_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
msg_mock.reset_mock()
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_buy_tag_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._buy_tag_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
|
def test_telegram_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
||||||
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1271,15 +1284,27 @@ def test_sell_reason_performance_handle(default_conf, update, ticker, fee,
|
|||||||
|
|
||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
context = MagicMock()
|
||||||
telegram._sell_reason_performance(update=update, context=MagicMock())
|
telegram._sell_reason_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Sell Reason Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
assert '<code>TESTSELL\t0.00006217 BTC (6.20%) (1)</code>' in msg_mock.call_args_list[0][0][0]
|
||||||
|
context.args = [trade.pair]
|
||||||
|
|
||||||
|
telegram._sell_reason_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_sell_reason_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._sell_reason_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
def test_telegram_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
||||||
limit_buy_order, limit_sell_order, mocker) -> None:
|
limit_buy_order, limit_sell_order, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
'freqtrade.exchange.Exchange',
|
'freqtrade.exchange.Exchange',
|
||||||
fetch_ticker=ticker,
|
fetch_ticker=ticker,
|
||||||
@ -1305,12 +1330,25 @@ def test_mix_tag_performance_handle(default_conf, update, ticker, fee,
|
|||||||
trade.close_date = datetime.utcnow()
|
trade.close_date = datetime.utcnow()
|
||||||
trade.is_open = False
|
trade.is_open = False
|
||||||
|
|
||||||
telegram._mix_tag_performance(update=update, context=MagicMock())
|
context = MagicMock()
|
||||||
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
assert msg_mock.call_count == 1
|
assert msg_mock.call_count == 1
|
||||||
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
assert 'Mix Tag Performance' in msg_mock.call_args_list[0][0][0]
|
||||||
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
assert ('<code>TESTBUY TESTSELL\t0.00006217 BTC (6.20%) (1)</code>'
|
||||||
in msg_mock.call_args_list[0][0][0])
|
in msg_mock.call_args_list[0][0][0])
|
||||||
|
|
||||||
|
context.args = [trade.pair]
|
||||||
|
telegram._mix_tag_performance(update=update, context=context)
|
||||||
|
assert msg_mock.call_count == 2
|
||||||
|
|
||||||
|
msg_mock.reset_mock()
|
||||||
|
mocker.patch('freqtrade.rpc.rpc.RPC._rpc_mix_tag_performance',
|
||||||
|
side_effect=RPCException('Error'))
|
||||||
|
telegram._mix_tag_performance(update=update, context=MagicMock())
|
||||||
|
|
||||||
|
assert msg_mock.call_count == 1
|
||||||
|
assert "Error" in msg_mock.call_args_list[0][0][0]
|
||||||
|
|
||||||
|
|
||||||
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
def test_count_handle(default_conf, update, ticker, fee, mocker) -> None:
|
||||||
mocker.patch.multiple(
|
mocker.patch.multiple(
|
||||||
|
Loading…
Reference in New Issue
Block a user