Merge branch 'freqtrade:develop' into strategy_utils

This commit is contained in:
hippocritical 2023-03-17 08:48:52 +01:00 committed by GitHub
commit 209811d23a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 2984 additions and 1206 deletions

View File

@ -16,7 +16,8 @@ on:
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.ref }} group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true cancel-in-progress: true
permissions:
repository-projects: read
jobs: jobs:
build_linux: build_linux:
@ -321,7 +322,6 @@ jobs:
build_linux_online: build_linux_online:
# Run pytest with "live" checks # Run pytest with "live" checks
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
# permissions:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -425,7 +425,7 @@ jobs:
python setup.py sdist bdist_wheel python setup.py sdist bdist_wheel
- name: Publish to PyPI (Test) - name: Publish to PyPI (Test)
uses: pypa/gh-action-pypi-publish@v1.6.4 uses: pypa/gh-action-pypi-publish@v1.7.1
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__
@ -433,7 +433,7 @@ jobs:
repository_url: https://test.pypi.org/legacy/ repository_url: https://test.pypi.org/legacy/
- name: Publish to PyPI - name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@v1.6.4 uses: pypa/gh-action-pypi-publish@v1.7.1
if: (github.event_name == 'release') if: (github.event_name == 'release')
with: with:
user: __token__ user: __token__
@ -466,12 +466,13 @@ jobs:
- name: Build and test and push docker images - name: Build and test and push docker images
env: env:
IMAGE_NAME: freqtradeorg/freqtrade
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
run: | run: |
build_helpers/publish_docker_multi.sh build_helpers/publish_docker_multi.sh
deploy_arm: deploy_arm:
permissions:
packages: write
needs: [ deploy ] needs: [ deploy ]
# Only run on 64bit machines # Only run on 64bit machines
runs-on: [self-hosted, linux, ARM64] runs-on: [self-hosted, linux, ARM64]
@ -494,8 +495,9 @@ jobs:
- name: Build and test and push docker images - name: Build and test and push docker images
env: env:
IMAGE_NAME: freqtradeorg/freqtrade
BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }}
GHCR_USERNAME: ${{ github.actor }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
build_helpers/publish_docker_arm64.sh build_helpers/publish_docker_arm64.sh

View File

@ -30,7 +30,7 @@ repos:
- repo: https://github.com/charliermarsh/ruff-pre-commit - repo: https://github.com/charliermarsh/ruff-pre-commit
# Ruff version. # Ruff version.
rev: 'v0.0.251' rev: 'v0.0.255'
hooks: hooks:
- id: ruff - id: ruff

View File

@ -3,6 +3,10 @@
# Use BuildKit, otherwise building on ARM fails # Use BuildKit, otherwise building on ARM fails
export DOCKER_BUILDKIT=1 export DOCKER_BUILDKIT=1
IMAGE_NAME=freqtradeorg/freqtrade
CACHE_IMAGE=freqtradeorg/freqtrade_cache
GHCR_IMAGE_NAME=ghcr.io/freqtrade/freqtrade
# Replace / with _ to create a valid tag # Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot TAG_PLOT=${TAG}_plot
@ -14,7 +18,6 @@ TAG_ARM=${TAG}_arm
TAG_PLOT_ARM=${TAG_PLOT}_arm TAG_PLOT_ARM=${TAG_PLOT}_arm
TAG_FREQAI_ARM=${TAG_FREQAI}_arm TAG_FREQAI_ARM=${TAG_FREQAI}_arm
TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm
CACHE_IMAGE=freqtradeorg/freqtrade_cache
echo "Running for ${TAG}" echo "Running for ${TAG}"
@ -38,13 +41,13 @@ if [ $? -ne 0 ]; then
echo "failed building multiarch images" echo "failed building multiarch images"
return 1 return 1
fi fi
# Tag image for upload and next build step
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_PLOT_ARM} -f docker/Dockerfile.plot .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_ARM} -f docker/Dockerfile.freqai .
docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl . docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_IMAGE} --build-arg sourcetag=${TAG_ARM} -t freqtrade:${TAG_FREQAI_RL_ARM} -f docker/Dockerfile.freqai_rl .
# Tag image for upload and next build step
docker tag freqtrade:$TAG_ARM ${CACHE_IMAGE}:$TAG_ARM
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker tag freqtrade:$TAG_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM
docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
@ -59,7 +62,6 @@ fi
docker images docker images
# docker push ${IMAGE_NAME}
docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM
docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_ARM
docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM docker push ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM
@ -82,14 +84,30 @@ docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI}
docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM} docker manifest create ${IMAGE_NAME}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL} ${CACHE_IMAGE}:${TAG_FREQAI_RL_ARM}
docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL} docker manifest push -p ${IMAGE_NAME}:${TAG_FREQAI_RL}
# copy images to ghcr.io
alias crane="docker run --rm -i -v $(pwd)/.crane:/home/nonroot/.docker/ gcr.io/go-containerregistry/crane"
mkdir .crane
chmod a+rwx .crane
echo "${GHCR_TOKEN}" | crane auth login ghcr.io -u "${GHCR_USERNAME}" --password-stdin
crane copy ${IMAGE_NAME}:${TAG} ${GHCR_IMAGE_NAME}:${TAG}
crane copy ${IMAGE_NAME}:${TAG_PLOT} ${GHCR_IMAGE_NAME}:${TAG_PLOT}
crane copy ${IMAGE_NAME}:${TAG_FREQAI} ${GHCR_IMAGE_NAME}:${TAG_FREQAI}
crane copy ${IMAGE_NAME}:${TAG_FREQAI_RL} ${GHCR_IMAGE_NAME}:${TAG_FREQAI_RL}
# Tag as latest for develop builds # Tag as latest for develop builds
if [ "${TAG}" = "develop" ]; then if [ "${TAG}" = "develop" ]; then
echo 'Tagging image as latest' echo 'Tagging image as latest'
docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG}
docker manifest push -p ${IMAGE_NAME}:latest docker manifest push -p ${IMAGE_NAME}:latest
crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest
fi fi
docker images docker images
rm -rf .crane
# Cleanup old images from arm64 node. # Cleanup old images from arm64 node.
docker image prune -a --force --filter "until=24h" docker image prune -a --force --filter "until=24h"

View File

@ -2,6 +2,8 @@
# The below assumes a correctly setup docker buildx environment # The below assumes a correctly setup docker buildx environment
IMAGE_NAME=freqtradeorg/freqtrade
CACHE_IMAGE=freqtradeorg/freqtrade_cache
# Replace / with _ to create a valid tag # Replace / with _ to create a valid tag
TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g")
TAG_PLOT=${TAG}_plot TAG_PLOT=${TAG}_plot
@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi"
PI_PLATFORM="linux/arm/v7" PI_PLATFORM="linux/arm/v7"
echo "Running for ${TAG}" echo "Running for ${TAG}"
CACHE_IMAGE=freqtradeorg/freqtrade_cache
CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache
# Add commit and commit_message to docker container # Add commit and commit_message to docker container

View File

@ -248,13 +248,13 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard
""" """
def calculate_reward(self, action: int) -> float: def calculate_reward(self, action: int) -> float:
if not self._is_valid(action): if not self._is_valid(action):
self.tensorboard_log("is_valid") self.tensorboard_log("invalid")
return -2 return -2
``` ```
!!! Note !!! Note
The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)` would add 0.23 to `float_metric`. In this case you can also disable incrementing using `inc=False` parameter. The `self.tensorboard_log()` function is designed for tracking incremented objects only i.e. events, actions inside the training environment. If the event of interest is a float, the float can be passed as the second argument e.g. `self.tensorboard_log("float_metric1", 0.23)`. In this case the metric values are not incremented.
### Choosing a base environment ### Choosing a base environment

View File

@ -1,6 +1,6 @@
markdown==3.3.7 markdown==3.3.7
mkdocs==1.4.2 mkdocs==1.4.2
mkdocs-material==9.1.1 mkdocs-material==9.1.2
mdx_truly_sane_lists==1.3 mdx_truly_sane_lists==1.3
pymdown-extensions==9.10 pymdown-extensions==9.10
jinja2==3.1.2 jinja2==3.1.2

View File

@ -1041,10 +1041,9 @@ from datetime import timedelta, datetime, timezone
# Within populate indicators (or populate_buy): # Within populate indicators (or populate_buy):
if self.config['runmode'].value in ('live', 'dry_run'): if self.config['runmode'].value in ('live', 'dry_run'):
# fetch closed trades for the last 2 days # fetch closed trades for the last 2 days
trades = Trade.get_trades([Trade.pair == metadata['pair'], trades = Trade.get_trades_proxy(
Trade.open_date > datetime.utcnow() - timedelta(days=2), pair=metadata['pair'], is_open=False,
Trade.is_open.is_(False), open_date=datetime.now(timezone.utc) - timedelta(days=2))
]).all()
# Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy # Analyze the conditions you'd like to lock the pair .... will probably be different for every strategy
sumprofit = sum(trade.close_profit for trade in trades) sumprofit = sum(trade.close_profit for trade in trades)
if sumprofit < 0: if sumprofit < 0:

File diff suppressed because it is too large Load Diff

View File

@ -1087,7 +1087,7 @@ class Exchange:
f'Tried to {side} amount {amount} at rate {rate}.' f'Tried to {side} amount {amount} at rate {rate}.'
f'Message: {e}') from e f'Message: {e}') from e
except ccxt.InvalidOrder as e: except ccxt.InvalidOrder as e:
raise ExchangeError( raise InvalidOrderException(
f'Could not create {ordertype} {side} order on market {pair}. ' f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to {side} amount {amount} at rate {rate}. ' f'Tried to {side} amount {amount} at rate {rate}. '
f'Message: {e}') from e f'Message: {e}') from e
@ -1138,7 +1138,10 @@ class Exchange:
# Ensure rate is less than stop price # Ensure rate is less than stop price
if bad_stop_price: if bad_stop_price:
raise OperationalException( raise OperationalException(
'In stoploss limit order, stop price should be more than limit price') "In stoploss limit order, stop price should be more than limit price. "
f"Stop price: {stop_price}, Limit price: {limit_rate}, "
f"Limit Price pct: {limit_price_pct}"
)
return limit_rate return limit_rate
def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict:
@ -2755,10 +2758,10 @@ class Exchange:
raise OperationalException( raise OperationalException(
f"{self.name} does not support {self.margin_mode} {self.trading_mode}") f"{self.name} does not support {self.margin_mode} {self.trading_mode}")
isolated_liq = None liquidation_price = None
if self._config['dry_run'] or not self.exchange_has("fetchPositions"): if self._config['dry_run'] or not self.exchange_has("fetchPositions"):
isolated_liq = self.dry_run_liquidation_price( liquidation_price = self.dry_run_liquidation_price(
pair=pair, pair=pair,
open_rate=open_rate, open_rate=open_rate,
is_short=is_short, is_short=is_short,
@ -2773,16 +2776,16 @@ class Exchange:
positions = self.fetch_positions(pair) positions = self.fetch_positions(pair)
if len(positions) > 0: if len(positions) > 0:
pos = positions[0] pos = positions[0]
isolated_liq = pos['liquidationPrice'] liquidation_price = pos['liquidationPrice']
if isolated_liq is not None: if liquidation_price is not None:
buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer
isolated_liq = ( liquidation_price_buffer = (
isolated_liq - buffer_amount liquidation_price - buffer_amount
if is_short else if is_short else
isolated_liq + buffer_amount liquidation_price + buffer_amount
) )
return isolated_liq return max(liquidation_price_buffer, 0.0)
else: else:
return None return None

View File

@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit() self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action) step_reward = self.calculate_reward(action)
self.total_reward += step_reward self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action]) self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None trade_type = None
if self.is_tradesignal(action): if self.is_tradesignal(action):

View File

@ -48,7 +48,7 @@ class Base4ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit() self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action) step_reward = self.calculate_reward(action)
self.total_reward += step_reward self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action]) self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None trade_type = None
if self.is_tradesignal(action): if self.is_tradesignal(action):

View File

@ -49,7 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment):
self._update_unrealized_total_profit() self._update_unrealized_total_profit()
step_reward = self.calculate_reward(action) step_reward = self.calculate_reward(action)
self.total_reward += step_reward self.total_reward += step_reward
self.tensorboard_log(self.actions._member_names_[action]) self.tensorboard_log(self.actions._member_names_[action], category="actions")
trade_type = None trade_type = None
if self.is_tradesignal(action): if self.is_tradesignal(action):

View File

@ -137,7 +137,8 @@ class BaseEnvironment(gym.Env):
self.np_random, seed = seeding.np_random(seed) self.np_random, seed = seeding.np_random(seed)
return [seed] return [seed]
def tensorboard_log(self, metric: str, value: Union[int, float] = 1, inc: bool = True): def tensorboard_log(self, metric: str, value: Optional[Union[int, float]] = None,
inc: Optional[bool] = None, category: str = "custom"):
""" """
Function builds the tensorboard_metrics dictionary Function builds the tensorboard_metrics dictionary
to be parsed by the TensorboardCallback. This to be parsed by the TensorboardCallback. This
@ -149,17 +150,24 @@ class BaseEnvironment(gym.Env):
def calculate_reward(self, action: int) -> float: def calculate_reward(self, action: int) -> float:
if not self._is_valid(action): if not self._is_valid(action):
self.tensorboard_log("is_valid") self.tensorboard_log("invalid")
return -2 return -2
:param metric: metric to be tracked and incremented :param metric: metric to be tracked and incremented
:param value: value to increment `metric` by :param value: `metric` value
:param inc: sets whether the `value` is incremented or not :param inc: (deprecated) sets whether the `value` is incremented or not
:param category: `metric` category
""" """
if not inc or metric not in self.tensorboard_metrics: increment = True if value is None else False
self.tensorboard_metrics[metric] = value value = 1 if increment else value
if category not in self.tensorboard_metrics:
self.tensorboard_metrics[category] = {}
if not increment or metric not in self.tensorboard_metrics[category]:
self.tensorboard_metrics[category][metric] = value
else: else:
self.tensorboard_metrics[metric] += value self.tensorboard_metrics[category][metric] += value
def reset_tensorboard_log(self): def reset_tensorboard_log(self):
self.tensorboard_metrics = {} self.tensorboard_metrics = {}

View File

@ -46,14 +46,12 @@ class TensorboardCallback(BaseCallback):
local_info = self.locals["infos"][0] local_info = self.locals["infos"][0]
tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0]
for info in local_info: for metric in local_info:
if info not in ["episode", "terminal_observation"]: if metric not in ["episode", "terminal_observation"]:
self.logger.record(f"_info/{info}", local_info[info]) self.logger.record(f"info/{metric}", local_info[metric])
for info in tensorboard_metrics: for category in tensorboard_metrics:
if info in [action.name for action in self.actions]: for metric in tensorboard_metrics[category]:
self.logger.record(f"_actions/{info}", tensorboard_metrics[info]) self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric])
else:
self.logger.record(f"_custom/{info}", tensorboard_metrics[info])
return True return True

View File

@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel):
""" """
# first, penalize if the action is not valid # first, penalize if the action is not valid
if not self._is_valid(action): if not self._is_valid(action):
self.tensorboard_log("is_valid") self.tensorboard_log("invalid", category="actions")
return -2 return -2
pnl = self.get_unrealized_profit() pnl = self.get_unrealized_profit()

View File

@ -586,7 +586,7 @@ class FreqtradeBot(LoggingMixin):
min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_entry_rate, current_entry_rate,
self.strategy.stoploss) 0.0)
min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair,
current_exit_rate, current_exit_rate,
self.strategy.stoploss) self.strategy.stoploss)
@ -700,7 +700,8 @@ class FreqtradeBot(LoggingMixin):
pos_adjust = trade is not None pos_adjust = trade is not None
enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake( enter_limit_requested, stake_amount, leverage = self.get_valid_enter_price_and_stake(
pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_) pair, price, stake_amount, trade_side, enter_tag, trade, order_adjust, leverage_,
pos_adjust)
if not stake_amount: if not stake_amount:
return False return False
@ -860,7 +861,12 @@ class FreqtradeBot(LoggingMixin):
trade: Optional[Trade], trade: Optional[Trade],
order_adjust: bool, order_adjust: bool,
leverage_: Optional[float], leverage_: Optional[float],
pos_adjust: bool,
) -> Tuple[float, float, float]: ) -> Tuple[float, float, float]:
"""
Validate and eventually adjust (within limits) limit, amount and leverage
:return: Tuple with (price, amount, leverage)
"""
if price: if price:
enter_limit_requested = price enter_limit_requested = price
@ -906,7 +912,9 @@ class FreqtradeBot(LoggingMixin):
# We do however also need min-stake to determine leverage, therefore this is ignored as # We do however also need min-stake to determine leverage, therefore this is ignored as
# edge-case for now. # edge-case for now.
min_stake_amount = self.exchange.get_min_pair_stake_amount( min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, enter_limit_requested, self.strategy.stoploss, leverage) pair, enter_limit_requested,
self.strategy.stoploss if not pos_adjust else 0.0,
leverage)
max_stake_amount = self.exchange.get_max_pair_stake_amount( max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, enter_limit_requested, leverage) pair, enter_limit_requested, leverage)
@ -1122,8 +1130,7 @@ class FreqtradeBot(LoggingMixin):
trade.stoploss_order_id = None trade.stoploss_order_id = None
logger.error(f'Unable to place a stoploss order on exchange. {e}') logger.error(f'Unable to place a stoploss order on exchange. {e}')
logger.warning('Exiting the trade forcefully') logger.warning('Exiting the trade forcefully')
self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( self.emergency_exit(trade, stop_price)
exit_type=ExitType.EMERGENCY_EXIT))
except ExchangeError: except ExchangeError:
trade.stoploss_order_id = None trade.stoploss_order_id = None
@ -1281,13 +1288,16 @@ class FreqtradeBot(LoggingMixin):
if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: if canceled and max_timeouts > 0 and canceled_count >= max_timeouts:
logger.warning(f'Emergency exiting trade {trade}, as the exit order ' logger.warning(f'Emergency exiting trade {trade}, as the exit order '
f'timed out {max_timeouts} times.') f'timed out {max_timeouts} times.')
self.emergency_exit(trade, order['price'])
def emergency_exit(self, trade: Trade, price: float) -> None:
try: try:
self.execute_trade_exit( self.execute_trade_exit(
trade, order['price'], trade, price,
exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT))
except DependencyException as exception: except DependencyException as exception:
logger.warning( logger.warning(
f'Unable to emergency sell trade {trade.pair}: {exception}') f'Unable to emergency exit trade {trade.pair}: {exception}')
def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None: def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None:
""" """

View File

@ -749,7 +749,7 @@ class Backtesting:
leverage = min(max(leverage, 1.0), max_leverage) leverage = min(max(leverage, 1.0), max_leverage)
min_stake_amount = self.exchange.get_min_pair_stake_amount( min_stake_amount = self.exchange.get_min_pair_stake_amount(
pair, propose_rate, -0.05, leverage=leverage) or 0 pair, propose_rate, -0.05 if not pos_adjust else 0.0, leverage=leverage) or 0
max_stake_amount = self.exchange.get_max_pair_stake_amount( max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, propose_rate, leverage=leverage) pair, propose_rate, leverage=leverage)
stake_available = self.wallets.get_available_stake_amount() stake_available = self.wallets.get_available_stake_amount()

View File

@ -1088,6 +1088,11 @@ class LocalTrade():
In live mode, converts the filter to a database query and returns all rows In live mode, converts the filter to a database query and returns all rows
In Backtest mode, uses filters on Trade.trades to get the result. In Backtest mode, uses filters on Trade.trades to get the result.
:param pair: Filter by pair
:param is_open: Filter by open/closed status
:param open_date: Filter by open_date (filters via trade.open_date > input)
:param close_date: Filter by close_date (filters via trade.close_date > input)
Will implicitly only return closed trades.
:return: unsorted List[Trade] :return: unsorted List[Trade]
""" """

View File

@ -311,7 +311,7 @@ class LockModel(BaseModel):
lock_timestamp: int lock_timestamp: int
pair: str pair: str
side: str side: str
reason: str reason: Optional[str]
class Locks(BaseModel): class Locks(BaseModel):

View File

@ -388,12 +388,13 @@ class RPC:
Trade.close_date.desc()) Trade.close_date.desc())
output = [trade.to_json() for trade in trades] output = [trade.to_json() for trade in trades]
total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count()
return { return {
"trades": output, "trades": output,
"trades_count": len(output), "trades_count": len(output),
"offset": offset, "offset": offset,
"total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), "total_trades": total_trades,
} }
def _rpc_stats(self) -> Dict[str, Any]: def _rpc_stats(self) -> Dict[str, Any]:

View File

@ -7,10 +7,10 @@
-r docs/requirements-docs.txt -r docs/requirements-docs.txt
coveralls==3.3.1 coveralls==3.3.1
ruff==0.0.254 ruff==0.0.255
mypy==1.0.1 mypy==1.1.1
pre-commit==3.1.1 pre-commit==3.1.1
pytest==7.2.1 pytest==7.2.2
pytest-asyncio==0.20.3 pytest-asyncio==0.20.3
pytest-cov==4.0.0 pytest-cov==4.0.0
pytest-mock==3.10.0 pytest-mock==3.10.0

View File

@ -10,7 +10,7 @@ python-telegram-bot==13.15
arrow==1.2.3 arrow==1.2.3
cachetools==4.2.2 cachetools==4.2.2
requests==2.28.2 requests==2.28.2
urllib3==1.26.14 urllib3==1.26.15
jsonschema==4.17.3 jsonschema==4.17.3
TA-Lib==0.4.25 TA-Lib==0.4.25
technical==1.4.0 technical==1.4.0
@ -34,9 +34,9 @@ orjson==3.8.7
sdnotify==0.3.2 sdnotify==0.3.2
# API Server # API Server
fastapi==0.92.0 fastapi==0.94.0
pydantic==1.10.5 pydantic==1.10.6
uvicorn==0.20.0 uvicorn==0.21.0
pyjwt==2.6.0 pyjwt==2.6.0
aiofiles==23.1.0 aiofiles==23.1.0
psutil==5.9.4 psutil==5.9.4

View File

@ -12,8 +12,8 @@ from pandas import DataFrame
from freqtrade.enums import CandleType, MarginMode, TradingMode from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError,
InvalidOrderException, OperationalException, PricingError, InsufficientFundsError, InvalidOrderException,
TemporaryError) OperationalException, PricingError, TemporaryError)
from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision,
date_minus_candles, market_is_active, price_to_precision, date_minus_candles, market_is_active, price_to_precision,
timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date, timeframe_to_minutes, timeframe_to_msecs, timeframe_to_next_date,
@ -1599,13 +1599,13 @@ def test_sell_prod(default_conf, mocker, exchange_name):
assert api_mock.create_order.call_args[0][4] == 200 assert api_mock.create_order.call_args[0][4] == 200
# test exception handling # test exception handling
with pytest.raises(DependencyException): with pytest.raises(InsufficientFundsError):
api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200, exchange.create_order(pair='ETH/BTC', ordertype=order_type, side="sell", amount=1, rate=200,
leverage=1.0) leverage=1.0)
with pytest.raises(DependencyException): with pytest.raises(InvalidOrderException):
api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found")) api_mock.create_order = MagicMock(side_effect=ccxt.InvalidOrder("Order not found"))
exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name) exchange = get_patched_exchange(mocker, default_conf, api_mock, id=exchange_name)
exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200, exchange.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200,

View File

@ -302,8 +302,7 @@ def test_telegram_status_closed_trade(default_conf, update, mocker, fee) -> None
telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf) telegram, _, msg_mock = get_telegram_testobject(mocker, default_conf)
create_mock_trades(fee) create_mock_trades(fee)
trades = Trade.get_trades([Trade.is_open.is_(False)]) trade = Trade.get_trades([Trade.is_open.is_(False)]).first()
trade = trades[0]
context = MagicMock() context = MagicMock()
context.args = [str(trade.id)] context.args = [str(trade.id)]
telegram._status(update=update, context=context) telegram._status(update=update, context=context)

View File

@ -2724,21 +2724,21 @@ def test_manage_open_orders_exit_usercustom(
assert freqtrade.strategy.check_exit_timeout.call_count == 1 assert freqtrade.strategy.check_exit_timeout.call_count == 1
assert freqtrade.strategy.check_entry_timeout.call_count == 0 assert freqtrade.strategy.check_entry_timeout.call_count == 0
# 2nd canceled trade - Fail execute sell # 2nd canceled trade - Fail execute exit
caplog.clear() caplog.clear()
open_trade_usdt.open_order_id = limit_sell_order_old['id'] open_trade_usdt.open_order_id = limit_sell_order_old['id']
mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1) mocker.patch('freqtrade.persistence.Trade.get_exit_order_count', return_value=1)
mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit', mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit',
side_effect=DependencyException) side_effect=DependencyException)
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
assert log_has_re('Unable to emergency sell .*', caplog) assert log_has_re('Unable to emergency exit .*', caplog)
et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit') et_mock = mocker.patch('freqtrade.freqtradebot.FreqtradeBot.execute_trade_exit')
caplog.clear() caplog.clear()
# 2nd canceled trade ... # 2nd canceled trade ...
open_trade_usdt.open_order_id = limit_sell_order_old['id'] open_trade_usdt.open_order_id = limit_sell_order_old['id']
# If cancelling fails - no emergency sell! # If cancelling fails - no emergency exit!
with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False): with patch('freqtrade.freqtradebot.FreqtradeBot.handle_cancel_exit', return_value=False):
freqtrade.manage_open_orders() freqtrade.manage_open_orders()
assert et_mock.call_count == 0 assert et_mock.call_count == 0