Merge branch 'develop' of https://github.com/freqtrade/freqtrade into custom_order_price
This commit is contained in:
commit
6ab99369f2
@ -105,7 +105,7 @@ Mandatory parameters are marked as **Required**, which means that they are requi
|
|||||||
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
| `ask_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to sell. I.e. a value of 2 will allow the bot to pick the 2nd ask rate in [Order Book Asks](#sell-price-with-orderbook-enabled)<br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
|
||||||
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
| `use_sell_signal` | Use sell signals produced by the strategy in addition to the `minimal_roi`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `true`.* <br> **Datatype:** Boolean
|
||||||
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `sell_profit_only` | Wait until the bot reaches `sell_profit_offset` before taking a sell decision. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `sell_profit_offset` | Sell-signal is only active above this value. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
| `sell_profit_offset` | Sell-signal is only active above this value. Only active in combination with `sell_profit_only=True`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0`.* <br> **Datatype:** Float (as ratio)
|
||||||
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
| `ignore_roi_if_buy_signal` | Do not sell if the buy signal is still active. This setting takes preference over `minimal_roi` and `use_sell_signal`. [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
|
||||||
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
| `ignore_buying_expired_candle_after` | Specifies the number of seconds until a buy signal is no longer used. <br> **Datatype:** Integer
|
||||||
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
| `order_types` | Configure order-types depending on the action (`"buy"`, `"sell"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
|
||||||
|
@ -105,7 +105,7 @@ To use subaccounts with FTX, you need to edit the configuration and add the foll
|
|||||||
|
|
||||||
## Kucoin
|
## Kucoin
|
||||||
|
|
||||||
Kucoin requries a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
Kucoin requires a passphrase for each api key, you will therefore need to add this key into the configuration so your exchange section looks as follows:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
"exchange": {
|
"exchange": {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
mkdocs==1.2.2
|
mkdocs==1.2.2
|
||||||
mkdocs-material==7.2.2
|
mkdocs-material==7.2.4
|
||||||
mdx_truly_sane_lists==1.2
|
mdx_truly_sane_lists==1.2
|
||||||
pymdown-extensions==8.2
|
pymdown-extensions==8.2
|
||||||
|
@ -228,7 +228,7 @@ graph = generate_candlestick_graph(pair=pair,
|
|||||||
# Show graph inline
|
# Show graph inline
|
||||||
# graph.show()
|
# graph.show()
|
||||||
|
|
||||||
# Render graph in a seperate window
|
# Render graph in a separate window
|
||||||
graph.show(renderer="browser")
|
graph.show(renderer="browser")
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -19,7 +19,7 @@ logger = logging.getLogger(__name__)
|
|||||||
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
BT_DATA_COLUMNS_OLD = ["pair", "profit_percent", "open_date", "close_date", "index",
|
||||||
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
"trade_duration", "open_rate", "close_rate", "open_at_end", "sell_reason"]
|
||||||
|
|
||||||
# Mid-term format, crated by BacktestResult Named Tuple
|
# Mid-term format, created by BacktestResult Named Tuple
|
||||||
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
BT_DATA_COLUMNS_MID = ['pair', 'profit_percent', 'open_date', 'close_date', 'trade_duration',
|
||||||
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
'open_rate', 'close_rate', 'open_at_end', 'sell_reason', 'fee_open',
|
||||||
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
'fee_close', 'amount', 'profit_abs', 'profit_ratio']
|
||||||
|
@ -242,7 +242,7 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
|
|||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
:param convert_from: Source format
|
:param convert_from: Source format
|
||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase souce data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
@ -267,7 +267,7 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
|
|||||||
:param config: Config dictionary
|
:param config: Config dictionary
|
||||||
:param convert_from: Source format
|
:param convert_from: Source format
|
||||||
:param convert_to: Target format
|
:param convert_to: Target format
|
||||||
:param erase: Erase souce data (does not apply if source and target format are identical)
|
:param erase: Erase source data (does not apply if source and target format are identical)
|
||||||
"""
|
"""
|
||||||
from freqtrade.data.history.idatahandler import get_datahandler
|
from freqtrade.data.history.idatahandler import get_datahandler
|
||||||
src = get_datahandler(config['datadir'], convert_from)
|
src = get_datahandler(config['datadir'], convert_from)
|
||||||
|
@ -1497,7 +1497,7 @@ class Exchange:
|
|||||||
:returns List of trade data
|
:returns List of trade data
|
||||||
"""
|
"""
|
||||||
if not self.exchange_has("fetchTrades"):
|
if not self.exchange_has("fetchTrades"):
|
||||||
raise OperationalException("This exchange does not suport downloading Trades.")
|
raise OperationalException("This exchange does not support downloading Trades.")
|
||||||
|
|
||||||
return asyncio.get_event_loop().run_until_complete(
|
return asyncio.get_event_loop().run_until_complete(
|
||||||
self._async_get_trade_history(pair=pair, since=since,
|
self._async_get_trade_history(pair=pair, since=since,
|
||||||
|
@ -983,7 +983,7 @@ class FreqtradeBot(LoggingMixin):
|
|||||||
# if trade is partially complete, edit the stake details for the trade
|
# if trade is partially complete, edit the stake details for the trade
|
||||||
# and close the order
|
# and close the order
|
||||||
# cancel_order may not contain the full order dict, so we need to fallback
|
# cancel_order may not contain the full order dict, so we need to fallback
|
||||||
# to the order dict aquired before cancelling.
|
# to the order dict acquired before cancelling.
|
||||||
# we need to fall back to the values from order if corder does not contain these keys.
|
# we need to fall back to the values from order if corder does not contain these keys.
|
||||||
trade.amount = filled_amount
|
trade.amount = filled_amount
|
||||||
trade.stake_amount = trade.amount * trade.open_rate
|
trade.stake_amount = trade.amount * trade.open_rate
|
||||||
|
@ -538,7 +538,7 @@ def load_and_plot_trades(config: Dict[str, Any]):
|
|||||||
- Initializes plot-script
|
- Initializes plot-script
|
||||||
- Get candle (OHLCV) data
|
- Get candle (OHLCV) data
|
||||||
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
- Generate Dafaframes populated with indicators and signals based on configured strategy
|
||||||
- Load trades excecuted during the selected period
|
- Load trades executed during the selected period
|
||||||
- Generate Plotly plot objects
|
- Generate Plotly plot objects
|
||||||
- Generate plot files
|
- Generate plot files
|
||||||
:return: None
|
:return: None
|
||||||
|
@ -150,18 +150,20 @@ class IPairList(LoggingMixin, ABC):
|
|||||||
for pair in pairlist:
|
for pair in pairlist:
|
||||||
# pair is not in the generated dynamic market or has the wrong stake currency
|
# pair is not in the generated dynamic market or has the wrong stake currency
|
||||||
if pair not in markets:
|
if pair not in markets:
|
||||||
logger.warning(f"Pair {pair} is not compatible with exchange "
|
self.log_once(f"Pair {pair} is not compatible with exchange "
|
||||||
f"{self._exchange.name}. Removing it from whitelist..")
|
f"{self._exchange.name}. Removing it from whitelist..",
|
||||||
|
logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not self._exchange.market_is_tradable(markets[pair]):
|
if not self._exchange.market_is_tradable(markets[pair]):
|
||||||
logger.warning(f"Pair {pair} is not tradable with Freqtrade."
|
self.log_once(f"Pair {pair} is not tradable with Freqtrade."
|
||||||
"Removing it from whitelist..")
|
"Removing it from whitelist..", logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
|
if self._exchange.get_pair_quote_currency(pair) != self._config['stake_currency']:
|
||||||
logger.warning(f"Pair {pair} is not compatible with your stake currency "
|
self.log_once(f"Pair {pair} is not compatible with your stake currency "
|
||||||
f"{self._config['stake_currency']}. Removing it from whitelist..")
|
f"{self._config['stake_currency']}. Removing it from whitelist..",
|
||||||
|
logger.warning)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Check if market is active
|
# Check if market is active
|
||||||
|
@ -126,7 +126,7 @@ class VolumePairList(IPairList):
|
|||||||
pairlist = [s['symbol'] for s in filtered_tickers]
|
pairlist = [s['symbol'] for s in filtered_tickers]
|
||||||
|
|
||||||
pairlist = self.filter_pairlist(pairlist, tickers)
|
pairlist = self.filter_pairlist(pairlist, tickers)
|
||||||
self._pair_cache['pairlist'] = pairlist
|
self._pair_cache['pairlist'] = pairlist.copy()
|
||||||
|
|
||||||
return pairlist
|
return pairlist
|
||||||
|
|
||||||
|
@ -120,5 +120,6 @@ class RangeStabilityFilter(IPairList):
|
|||||||
logger.info)
|
logger.info)
|
||||||
result = False
|
result = False
|
||||||
self._pair_cache[pair] = result
|
self._pair_cache[pair] = result
|
||||||
|
else:
|
||||||
|
self.log_once(f"Removed {pair} from whitelist, no candles found.", logger.info)
|
||||||
return result
|
return result
|
||||||
|
@ -223,11 +223,11 @@ def list_strategies(config=Depends(get_config)):
|
|||||||
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
@router.get('/strategy/{strategy}', response_model=StrategyResponse, tags=['strategy'])
|
||||||
def get_strategy(strategy: str, config=Depends(get_config)):
|
def get_strategy(strategy: str, config=Depends(get_config)):
|
||||||
|
|
||||||
config = deepcopy(config)
|
config_ = deepcopy(config)
|
||||||
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
from freqtrade.resolvers.strategy_resolver import StrategyResolver
|
||||||
try:
|
try:
|
||||||
strategy_obj = StrategyResolver._load_strategy(strategy, config,
|
strategy_obj = StrategyResolver._load_strategy(strategy, config_,
|
||||||
extra_dir=config.get('strategy_path'))
|
extra_dir=config_.get('strategy_path'))
|
||||||
except OperationalException:
|
except OperationalException:
|
||||||
raise HTTPException(status_code=404, detail='Strategy not found')
|
raise HTTPException(status_code=404, detail='Strategy not found')
|
||||||
|
|
||||||
|
@ -29,6 +29,16 @@ async def ui_version():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def is_relative_to(path, base) -> bool:
|
||||||
|
# Helper function simulating behaviour of is_relative_to, which was only added in python 3.9
|
||||||
|
try:
|
||||||
|
path.relative_to(base)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
|
@router_ui.get('/{rest_of_path:path}', include_in_schema=False)
|
||||||
async def index_html(rest_of_path: str):
|
async def index_html(rest_of_path: str):
|
||||||
"""
|
"""
|
||||||
@ -37,8 +47,11 @@ async def index_html(rest_of_path: str):
|
|||||||
if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
|
if rest_of_path.startswith('api') or rest_of_path.startswith('.'):
|
||||||
raise HTTPException(status_code=404, detail="Not Found")
|
raise HTTPException(status_code=404, detail="Not Found")
|
||||||
uibase = Path(__file__).parent / 'ui/installed/'
|
uibase = Path(__file__).parent / 'ui/installed/'
|
||||||
if (uibase / rest_of_path).is_file():
|
filename = uibase / rest_of_path
|
||||||
return FileResponse(str(uibase / rest_of_path))
|
# It's security relevant to check "relative_to".
|
||||||
|
# Without this, Directory-traversal is possible.
|
||||||
|
if filename.is_file() and is_relative_to(filename, uibase):
|
||||||
|
return FileResponse(str(filename))
|
||||||
|
|
||||||
index_file = uibase / 'index.html'
|
index_file = uibase / 'index.html'
|
||||||
if not index_file.is_file():
|
if not index_file.is_file():
|
||||||
|
@ -776,7 +776,7 @@ class RPC:
|
|||||||
if has_content:
|
if has_content:
|
||||||
|
|
||||||
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
|
||||||
# Move open to seperate column when signal for easy plotting
|
# Move open to separate column when signal for easy plotting
|
||||||
if 'buy' in dataframe.columns:
|
if 'buy' in dataframe.columns:
|
||||||
buy_mask = (dataframe['buy'] == 1)
|
buy_mask = (dataframe['buy'] == 1)
|
||||||
buy_signals = int(buy_mask.sum())
|
buy_signals = int(buy_mask.sum())
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
coveralls==3.2.0
|
coveralls==3.2.0
|
||||||
flake8==3.9.2
|
flake8==3.9.2
|
||||||
flake8-type-annotations==0.1.0
|
flake8-type-annotations==0.1.0
|
||||||
flake8-tidy-imports==4.3.0
|
flake8-tidy-imports==4.4.1
|
||||||
mypy==0.910
|
mypy==0.910
|
||||||
pytest==6.2.4
|
pytest==6.2.4
|
||||||
pytest-asyncio==0.15.1
|
pytest-asyncio==0.15.1
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Include all requirements to run the bot.
|
# Include all requirements to run the bot.
|
||||||
-r requirements.txt
|
-r requirements.txt
|
||||||
|
|
||||||
plotly==5.1.0
|
plotly==5.2.1
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
numpy==1.21.1
|
numpy==1.21.2
|
||||||
pandas==1.3.1
|
pandas==1.3.2
|
||||||
|
|
||||||
ccxt==1.54.74
|
ccxt==1.55.6
|
||||||
# Pin cryptography for now due to rust build errors with piwheels
|
# Pin cryptography for now due to rust build errors with piwheels
|
||||||
cryptography==3.4.7
|
cryptography==3.4.7
|
||||||
aiohttp==3.7.4.post0
|
aiohttp==3.7.4.post0
|
||||||
@ -32,7 +32,7 @@ sdnotify==0.3.2
|
|||||||
|
|
||||||
# API Server
|
# API Server
|
||||||
fastapi==0.68.0
|
fastapi==0.68.0
|
||||||
uvicorn==0.14.0
|
uvicorn==0.15.0
|
||||||
pyjwt==2.1.0
|
pyjwt==2.1.0
|
||||||
aiofiles==0.7.0
|
aiofiles==0.7.0
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ def test_ohlcv_fill_up_missing_data2(caplog):
|
|||||||
# 3rd candle has been filled
|
# 3rd candle has been filled
|
||||||
row = data2.loc[2, :]
|
row = data2.loc[2, :]
|
||||||
assert row['volume'] == 0
|
assert row['volume'] == 0
|
||||||
# close shoult match close of previous candle
|
# close should match close of previous candle
|
||||||
assert row['close'] == data.loc[1, 'close']
|
assert row['close'] == data.loc[1, 'close']
|
||||||
assert row['open'] == row['close']
|
assert row['open'] == row['close']
|
||||||
assert row['high'] == row['close']
|
assert row['high'] == row['close']
|
||||||
|
@ -66,7 +66,7 @@ def test_historic_ohlcv_dataformat(mocker, default_conf, ohlcv_history):
|
|||||||
hdf5loadmock.assert_not_called()
|
hdf5loadmock.assert_not_called()
|
||||||
jsonloadmock.assert_called_once()
|
jsonloadmock.assert_called_once()
|
||||||
|
|
||||||
# Swiching to dataformat hdf5
|
# Switching to dataformat hdf5
|
||||||
hdf5loadmock.reset_mock()
|
hdf5loadmock.reset_mock()
|
||||||
jsonloadmock.reset_mock()
|
jsonloadmock.reset_mock()
|
||||||
default_conf["dataformat_ohlcv"] = "hdf5"
|
default_conf["dataformat_ohlcv"] = "hdf5"
|
||||||
|
@ -200,15 +200,15 @@ def test_load_cached_data_for_updating(mocker, testdatadir) -> None:
|
|||||||
assert start_ts == test_data[0][0] - 1000
|
assert start_ts == test_data[0][0] - 1000
|
||||||
|
|
||||||
# timeframe starts in the center of the cached data
|
# timeframe starts in the center of the cached data
|
||||||
# should return the chached data w/o the last item
|
# should return the cached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
timerange = TimeRange('date', None, test_data[0][0] / 1000 + 1, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
||||||
|
|
||||||
assert_frame_equal(data, test_data_df.iloc[:-1])
|
assert_frame_equal(data, test_data_df.iloc[:-1])
|
||||||
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
assert test_data[-2][0] <= start_ts < test_data[-1][0]
|
||||||
|
|
||||||
# timeframe starts after the chached data
|
# timeframe starts after the cached data
|
||||||
# should return the chached data w/o the last item
|
# should return the cached data w/o the last item
|
||||||
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0)
|
timerange = TimeRange('date', None, test_data[-1][0] / 1000 + 100, 0)
|
||||||
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
data, start_ts = _load_cached_data_for_updating('UNITTEST/BTC', '1m', timerange, data_handler)
|
||||||
assert_frame_equal(data, test_data_df.iloc[:-1])
|
assert_frame_equal(data, test_data_df.iloc[:-1])
|
||||||
|
@ -2182,7 +2182,7 @@ def test_get_historic_trades_notsupported(default_conf, mocker, caplog, exchange
|
|||||||
pair = 'ETH/BTC'
|
pair = 'ETH/BTC'
|
||||||
|
|
||||||
with pytest.raises(OperationalException,
|
with pytest.raises(OperationalException,
|
||||||
match="This exchange does not suport downloading Trades."):
|
match="This exchange does not support downloading Trades."):
|
||||||
exchange.get_historic_trades(pair, since=trades_history[0][0],
|
exchange.get_historic_trades(pair, since=trades_history[0][0],
|
||||||
until=trades_history[-1][0])
|
until=trades_history[-1][0])
|
||||||
|
|
||||||
|
@ -109,6 +109,11 @@ def test_api_ui_fallback(botclient):
|
|||||||
rc = client_get(client, "/something")
|
rc = client_get(client, "/something")
|
||||||
assert rc.status_code == 200
|
assert rc.status_code == 200
|
||||||
|
|
||||||
|
# Test directory traversal
|
||||||
|
rc = client_get(client, '%2F%2F%2Fetc/passwd')
|
||||||
|
assert rc.status_code == 200
|
||||||
|
assert '`freqtrade install-ui`' in rc.text
|
||||||
|
|
||||||
|
|
||||||
def test_api_ui_version(botclient, mocker):
|
def test_api_ui_version(botclient, mocker):
|
||||||
ftbot, client = botclient
|
ftbot, client = botclient
|
||||||
|
@ -630,7 +630,7 @@ def test_strategy_safe_wrapper_error(caplog, error):
|
|||||||
assert ret
|
assert ret
|
||||||
|
|
||||||
caplog.clear()
|
caplog.clear()
|
||||||
# Test supressing error
|
# Test suppressing error
|
||||||
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)()
|
ret = strategy_safe_wrapper(failing_method, message='DeadBeef', supress_error=True)()
|
||||||
assert log_has_re(r'DeadBeef.*', caplog)
|
assert log_has_re(r'DeadBeef.*', caplog)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user