+
Freqtrade UI not installed.
+
Please run `freqtrade install-ui` in your terminal to install the UI files and restart your bot.
+
You can then refresh this page and you should see the UI.
+
+
+
diff --git a/freqtrade/rpc/api_server/ui/favicon.ico b/freqtrade/rpc/api_server/ui/favicon.ico
new file mode 100644
index 000000000..78c7e43b1
Binary files /dev/null and b/freqtrade/rpc/api_server/ui/favicon.ico differ
diff --git a/freqtrade/rpc/api_server/ui/installed/.gitkeep b/freqtrade/rpc/api_server/ui/installed/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/freqtrade/rpc/api_server/web_ui.py b/freqtrade/rpc/api_server/web_ui.py
new file mode 100644
index 000000000..6d7e77953
--- /dev/null
+++ b/freqtrade/rpc/api_server/web_ui.py
@@ -0,0 +1,31 @@
+from pathlib import Path
+
+from fastapi import APIRouter
+from fastapi.exceptions import HTTPException
+from starlette.responses import FileResponse
+
+
+router_ui = APIRouter()
+
+
+@router_ui.get('/favicon.ico', include_in_schema=False)
+async def favicon():
+ return FileResponse(Path(__file__).parent / 'ui/favicon.ico')
+
+
+@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
+async def index_html(rest_of_path: str):
+ """
+ Emulate path fallback to index.html.
+ """
+ if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
+ raise HTTPException(status_code=404, detail="Not Found")
+ uibase = Path(__file__).parent / 'ui/installed/'
+ if (uibase / rest_of_path).is_file():
+ return FileResponse(str(uibase / rest_of_path))
+
+ index_file = uibase / 'index.html'
+ if not index_file.is_file():
+ return FileResponse(str(uibase.parent / 'fallback_file.html'))
+ # Fall back to index.html, as indicated by vue router docs
+ return FileResponse(str(index_file))
diff --git a/freqtrade/rpc/api_server/webserver.py b/freqtrade/rpc/api_server/webserver.py
index 9c0779274..8a5c958e9 100644
--- a/freqtrade/rpc/api_server/webserver.py
+++ b/freqtrade/rpc/api_server/webserver.py
@@ -2,6 +2,7 @@ import logging
from ipaddress import IPv4Address
from typing import Any, Dict
+import rapidjson
import uvicorn
from fastapi import Depends, FastAPI
from fastapi.middleware.cors import CORSMiddleware
@@ -14,6 +15,17 @@ from freqtrade.rpc.rpc import RPC, RPCException, RPCHandler
logger = logging.getLogger(__name__)
+class FTJSONResponse(JSONResponse):
+ media_type = "application/json"
+
+ def render(self, content: Any) -> bytes:
+ """
+ Use rapidjson for responses
+ Handles NaN and Inf / -Inf in a javascript way by default.
+ """
+ return rapidjson.dumps(content).encode("utf-8")
+
+
class ApiServer(RPCHandler):
_rpc: RPC
@@ -32,6 +44,7 @@ class ApiServer(RPCHandler):
self.app = FastAPI(title="Freqtrade API",
docs_url='/docs' if api_config.get('enable_openapi', False) else None,
redoc_url=None,
+ default_response_class=FTJSONResponse,
)
self.configure_app(self.app, self._config)
@@ -57,12 +70,16 @@ class ApiServer(RPCHandler):
from freqtrade.rpc.api_server.api_auth import http_basic_or_jwt_token, router_login
from freqtrade.rpc.api_server.api_v1 import router as api_v1
from freqtrade.rpc.api_server.api_v1 import router_public as api_v1_public
+ from freqtrade.rpc.api_server.web_ui import router_ui
+
app.include_router(api_v1_public, prefix="/api/v1")
app.include_router(api_v1, prefix="/api/v1",
dependencies=[Depends(http_basic_or_jwt_token)],
)
app.include_router(router_login, prefix="/api/v1", tags=["auth"])
+ # UI Router MUST be last!
+ app.include_router(router_ui, prefix='')
app.add_middleware(
CORSMiddleware,
diff --git a/freqtrade/rpc/rpc.py b/freqtrade/rpc/rpc.py
index 92cd6caf9..7549c38be 100644
--- a/freqtrade/rpc/rpc.py
+++ b/freqtrade/rpc/rpc.py
@@ -9,7 +9,7 @@ from math import isnan
from typing import Any, Dict, List, Optional, Tuple, Union
import arrow
-from numpy import NAN, int64, mean
+from numpy import NAN, inf, int64, mean
from pandas import DataFrame
from freqtrade.configuration.timerange import TimeRange
@@ -129,6 +129,7 @@ class RPC:
'trailing_stop_positive': config.get('trailing_stop_positive'),
'trailing_stop_positive_offset': config.get('trailing_stop_positive_offset'),
'trailing_only_offset_is_reached': config.get('trailing_only_offset_is_reached'),
+ 'use_custom_stoploss': config.get('use_custom_stoploss'),
'bot_name': config.get('bot_name', 'freqtrade'),
'timeframe': config.get('timeframe'),
'timeframe_ms': timeframe_to_msecs(config['timeframe']
@@ -377,7 +378,7 @@ class RPC:
# Prepare data to display
profit_closed_coin_sum = round(sum(profit_closed_coin), 8)
- profit_closed_ratio_mean = mean(profit_closed_ratio) if profit_closed_ratio else 0.0
+ profit_closed_ratio_mean = float(mean(profit_closed_ratio) if profit_closed_ratio else 0.0)
profit_closed_ratio_sum = sum(profit_closed_ratio) if profit_closed_ratio else 0.0
profit_closed_fiat = self._fiat_converter.convert_amount(
@@ -387,7 +388,7 @@ class RPC:
) if self._fiat_converter else 0
profit_all_coin_sum = round(sum(profit_all_coin), 8)
- profit_all_ratio_mean = mean(profit_all_ratio) if profit_all_ratio else 0.0
+ profit_all_ratio_mean = float(mean(profit_all_ratio) if profit_all_ratio else 0.0)
profit_all_ratio_sum = sum(profit_all_ratio) if profit_all_ratio else 0.0
profit_all_fiat = self._fiat_converter.convert_amount(
profit_all_coin_sum,
@@ -450,7 +451,7 @@ class RPC:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
rate = tickers.get(pair, {}).get('bid', None)
if rate:
- if pair.startswith(stake_currency):
+ if pair.startswith(stake_currency) and not pair.endswith(stake_currency):
rate = 1.0 / rate
est_stake = rate * balance.total
except (ExchangeError):
@@ -589,7 +590,8 @@ class RPC:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
# gen stake amount
- stakeamount = self._freqtrade.get_trade_stake_amount(pair)
+ stakeamount = self._freqtrade.wallets.get_trade_stake_amount(
+ pair, self._freqtrade.get_free_open_trades())
# execute buy
if self._freqtrade.execute_buy(pair, stakeamount, price):
@@ -745,6 +747,7 @@ class RPC:
sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_open'] = dataframe.loc[sell_mask, 'open']
+ dataframe = dataframe.replace([inf, -inf], NAN)
dataframe = dataframe.replace({NAN: None})
res = {
@@ -773,7 +776,8 @@ class RPC:
})
return res
- def _rpc_analysed_dataframe(self, pair: str, timeframe: str, limit: int) -> Dict[str, Any]:
+ def _rpc_analysed_dataframe(self, pair: str, timeframe: str,
+ limit: Optional[int]) -> Dict[str, Any]:
_data, last_analyzed = self._freqtrade.dataprovider.get_analyzed_dataframe(
pair, timeframe)
diff --git a/freqtrade/rpc/telegram.py b/freqtrade/rpc/telegram.py
index 99f9a8a91..88019601c 100644
--- a/freqtrade/rpc/telegram.py
+++ b/freqtrade/rpc/telegram.py
@@ -18,6 +18,7 @@ from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.exceptions import OperationalException
+from freqtrade.misc import round_coin_value
from freqtrade.rpc import RPC, RPCException, RPCHandler, RPCMessageType
@@ -189,14 +190,14 @@ class Telegram(RPCHandler):
else:
msg['stake_amount_fiat'] = 0
- message = ("\N{LARGE BLUE CIRCLE} *{exchange}:* Buying {pair}\n"
- "*Amount:* `{amount:.8f}`\n"
- "*Open Rate:* `{limit:.8f}`\n"
- "*Current Rate:* `{current_rate:.8f}`\n"
- "*Total:* `({stake_amount:.6f} {stake_currency}").format(**msg)
+ message = (f"\N{LARGE BLUE CIRCLE} *{msg['exchange']}:* Buying {msg['pair']}\n"
+ f"*Amount:* `{msg['amount']:.8f}`\n"
+ f"*Open Rate:* `{msg['limit']:.8f}`\n"
+ f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
+ f"*Total:* `({round_coin_value(msg['stake_amount'], msg['stake_currency'])}")
if msg.get('fiat_currency', None):
- message += ", {stake_amount_fiat:.3f} {fiat_currency}".format(**msg)
+ message += f", {round_coin_value(msg['stake_amount_fiat'], msg['fiat_currency'])}"
message += ")`"
elif msg['type'] == RPCMessageType.BUY_CANCEL_NOTIFICATION:
@@ -365,7 +366,7 @@ class Telegram(RPCHandler):
)
stats_tab = tabulate(
[[day['date'],
- f"{day['abs_profit']:.8f} {stats['stake_currency']}",
+ f"{round_coin_value(day['abs_profit'], stats['stake_currency'])}",
f"{day['fiat_value']:.3f} {stats['fiat_display_currency']}",
f"{day['trade_count']} trades"] for day in stats['data']],
headers=[
@@ -415,18 +416,18 @@ class Telegram(RPCHandler):
# Message to display
if stats['closed_trade_count'] > 0:
markdown_msg = ("*ROI:* Closed trades\n"
- f"∙ `{profit_closed_coin:.8f} {stake_cur} "
+ f"∙ `{round_coin_value(profit_closed_coin, stake_cur)} "
f"({profit_closed_percent_mean:.2f}%) "
f"({profit_closed_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
- f"∙ `{profit_closed_fiat:.3f} {fiat_disp_cur}`\n")
+ f"∙ `{round_coin_value(profit_closed_fiat, fiat_disp_cur)}`\n")
else:
markdown_msg = "`No closed trade` \n"
markdown_msg += (f"*ROI:* All trades\n"
- f"∙ `{profit_all_coin:.8f} {stake_cur} "
+ f"∙ `{round_coin_value(profit_all_coin, stake_cur)} "
f"({profit_all_percent_mean:.2f}%) "
f"({profit_all_percent_sum} \N{GREEK CAPITAL LETTER SIGMA}%)`\n"
- f"∙ `{profit_all_fiat:.3f} {fiat_disp_cur}`\n"
+ f"∙ `{round_coin_value(profit_all_fiat, fiat_disp_cur)}`\n"
f"*Total Trade Count:* `{trade_count}`\n"
f"*First Trade opened:* `{first_trade_date}`\n"
f"*Latest Trade opened:* `{latest_trade_date}\n`"
@@ -494,15 +495,17 @@ class Telegram(RPCHandler):
"Starting capital: "
f"`{self._config['dry_run_wallet']}` {self._config['stake_currency']}.\n"
)
- for currency in result['currencies']:
- if currency['est_stake'] > 0.0001:
- curr_output = ("*{currency}:*\n"
- "\t`Available: {free: .8f}`\n"
- "\t`Balance: {balance: .8f}`\n"
- "\t`Pending: {used: .8f}`\n"
- "\t`Est. {stake}: {est_stake: .8f}`\n").format(**currency)
+ for curr in result['currencies']:
+ if curr['est_stake'] > 0.0001:
+ curr_output = (
+ f"*{curr['currency']}:*\n"
+ f"\t`Available: {curr['free']:.8f}`\n"
+ f"\t`Balance: {curr['balance']:.8f}`\n"
+ f"\t`Pending: {curr['used']:.8f}`\n"
+ f"\t`Est. {curr['stake']}: "
+ f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else:
- curr_output = "*{currency}:* not showing <1$ amount \n".format(**currency)
+ curr_output = f"*{curr['currency']}:* not showing <1$ amount \n"
# Handle overflowing messsage length
if len(output + curr_output) >= MAX_TELEGRAM_MESSAGE_LENGTH:
@@ -512,8 +515,9 @@ class Telegram(RPCHandler):
output += curr_output
output += ("\n*Estimated Value*:\n"
- "\t`{stake}: {total: .8f}`\n"
- "\t`{symbol}: {value: .2f}`\n").format(**result)
+ f"\t`{result['stake']}: {result['total']: .8f}`\n"
+ f"\t`{result['symbol']}: "
+ f"{round_coin_value(result['value'], result['symbol'], False)}`\n")
self._send_msg(output)
except RPCException as e:
self._send_msg(str(e))
@@ -910,7 +914,7 @@ class Telegram(RPCHandler):
:param parse_mode: telegram parse mode
:return: None
"""
- reply_markup = ReplyKeyboardMarkup(self._keyboard)
+ reply_markup = ReplyKeyboardMarkup(self._keyboard, resize_keyboard=True)
try:
try:
self._updater.bot.send_message(
diff --git a/freqtrade/strategy/interface.py b/freqtrade/strategy/interface.py
index c58d9aa5d..da4ce6c50 100644
--- a/freqtrade/strategy/interface.py
+++ b/freqtrade/strategy/interface.py
@@ -530,8 +530,8 @@ class IStrategy(ABC):
current_time=date))
if (ask_strategy.get('sell_profit_only', False)
- and trade.calc_profit(rate=rate) <= ask_strategy.get('sell_profit_offset', 0)):
- # Negative profits and sell_profit_only - ignore sell signal
+ and current_profit <= ask_strategy.get('sell_profit_offset', 0)):
+ # sell_profit_only and profit doesn't reach the offset - ignore sell signal
sell_signal = False
else:
sell_signal = sell and not buy and ask_strategy.get('use_sell_signal', True)
diff --git a/freqtrade/templates/base_strategy.py.j2 b/freqtrade/templates/base_strategy.py.j2
index 4a1b43e36..dd6b773e1 100644
--- a/freqtrade/templates/base_strategy.py.j2
+++ b/freqtrade/templates/base_strategy.py.j2
@@ -5,7 +5,7 @@ import numpy as np # noqa
import pandas as pd # noqa
from pandas import DataFrame
-from freqtrade.strategy.interface import IStrategy
+from freqtrade.strategy import IStrategy
# --------------------------------
# Add your lib to import here
diff --git a/freqtrade/templates/sample_hyperopt_loss.py b/freqtrade/templates/sample_hyperopt_loss.py
index 59e6d814a..343349508 100644
--- a/freqtrade/templates/sample_hyperopt_loss.py
+++ b/freqtrade/templates/sample_hyperopt_loss.py
@@ -1,5 +1,6 @@
from datetime import datetime
from math import exp
+from typing import Dict
from pandas import DataFrame
@@ -35,12 +36,13 @@ class SampleHyperOptLoss(IHyperOptLoss):
@staticmethod
def hyperopt_loss_function(results: DataFrame, trade_count: int,
min_date: datetime, max_date: datetime,
+ config: Dict, processed: Dict[str, DataFrame],
*args, **kwargs) -> float:
"""
Objective function, returns smaller number for better results
"""
- total_profit = results.profit_percent.sum()
- trade_duration = results.trade_duration.mean()
+ total_profit = results['profit_ratio'].sum()
+ trade_duration = results['trade_duration'].mean()
trade_loss = 1 - 0.25 * exp(-(trade_count - TARGET_TRADES) ** 2 / 10 ** 5.8)
profit_loss = max(0, 1 - total_profit / EXPECTED_MAX_PROFIT)
diff --git a/freqtrade/templates/sample_strategy.py b/freqtrade/templates/sample_strategy.py
index b3f9fef07..db1ba48b8 100644
--- a/freqtrade/templates/sample_strategy.py
+++ b/freqtrade/templates/sample_strategy.py
@@ -17,7 +17,7 @@ import freqtrade.vendor.qtpylib.indicators as qtpylib
class SampleStrategy(IStrategy):
"""
This is a sample strategy to inspire you.
- More information in https://github.com/freqtrade/freqtrade/blob/develop/docs/bot-optimization.md
+ More information in https://www.freqtrade.io/en/latest/strategy-customization/
You can:
:return: a Dataframe with all mandatory indicators for the strategies
diff --git a/freqtrade/templates/strategy_analysis_example.ipynb b/freqtrade/templates/strategy_analysis_example.ipynb
index c6e64c74e..491afbdd7 100644
--- a/freqtrade/templates/strategy_analysis_example.ipynb
+++ b/freqtrade/templates/strategy_analysis_example.ipynb
@@ -40,7 +40,7 @@
"# Location of the data\n",
"data_location = Path(config['user_data_dir'], 'data', 'binance')\n",
"# Pair to analyze - Only use one pair here\n",
- "pair = \"BTC_USDT\""
+ "pair = \"BTC/USDT\""
]
},
{
@@ -54,7 +54,9 @@
"\n",
"candles = load_pair_history(datadir=data_location,\n",
" timeframe=config[\"timeframe\"],\n",
- " pair=pair)\n",
+ " pair=pair,\n",
+ " data_format = \"hdf5\",\n",
+ " )\n",
"\n",
"# Confirm success\n",
"print(\"Loaded \" + str(len(candles)) + f\" rows of data for {pair} from {data_location}\")\n",
diff --git a/freqtrade/wallets.py b/freqtrade/wallets.py
index 3680dd416..d7dcfd487 100644
--- a/freqtrade/wallets.py
+++ b/freqtrade/wallets.py
@@ -7,6 +7,8 @@ from typing import Any, Dict, NamedTuple
import arrow
+from freqtrade.constants import UNLIMITED_STAKE_AMOUNT
+from freqtrade.exceptions import DependencyException
from freqtrade.exchange import Exchange
from freqtrade.persistence import Trade
@@ -118,3 +120,79 @@ class Wallets:
def get_all_balances(self) -> Dict[str, Any]:
return self._wallets
+
+ def _get_available_stake_amount(self) -> float:
+ """
+ Return the total currently available balance in stake currency,
+ respecting tradable_balance_ratio.
+ Calculated as
+ (