Merge branch 'freqtrade:develop' into strategy_utils
This commit is contained in:
		
							
								
								
									
										14
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										14
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,8 @@ on: | ||||
| concurrency: | ||||
|   group: ${{ github.workflow }}-${{ github.ref }} | ||||
|   cancel-in-progress: true | ||||
|  | ||||
| permissions: | ||||
|   repository-projects: read | ||||
| jobs: | ||||
|   build_linux: | ||||
|  | ||||
| @@ -321,7 +322,6 @@ jobs: | ||||
|   build_linux_online: | ||||
|     # Run pytest with "live" checks | ||||
|     runs-on: ubuntu-22.04 | ||||
|     # permissions: | ||||
|     steps: | ||||
|     - uses: actions/checkout@v3 | ||||
|  | ||||
| @@ -425,7 +425,7 @@ jobs: | ||||
|         python setup.py sdist bdist_wheel | ||||
|  | ||||
|     - 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') | ||||
|       with: | ||||
|         user: __token__ | ||||
| @@ -433,7 +433,7 @@ jobs: | ||||
|         repository_url: https://test.pypi.org/legacy/ | ||||
|  | ||||
|     - 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') | ||||
|       with: | ||||
|         user: __token__ | ||||
| @@ -466,12 +466,13 @@ jobs: | ||||
|  | ||||
|     - name: Build and test and push docker images | ||||
|       env: | ||||
|         IMAGE_NAME: freqtradeorg/freqtrade | ||||
|         BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} | ||||
|       run: | | ||||
|         build_helpers/publish_docker_multi.sh | ||||
|  | ||||
|   deploy_arm: | ||||
|     permissions: | ||||
|       packages: write | ||||
|     needs: [ deploy ] | ||||
|     # Only run on 64bit machines | ||||
|     runs-on: [self-hosted, linux, ARM64] | ||||
| @@ -494,8 +495,9 @@ jobs: | ||||
|  | ||||
|     - name: Build and test and push docker images | ||||
|       env: | ||||
|         IMAGE_NAME: freqtradeorg/freqtrade | ||||
|         BRANCH_NAME: ${{ steps.extract_branch.outputs.branch }} | ||||
|         GHCR_USERNAME: ${{ github.actor }} | ||||
|         GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||||
|       run: | | ||||
|         build_helpers/publish_docker_arm64.sh | ||||
|  | ||||
|   | ||||
| @@ -30,7 +30,7 @@ repos: | ||||
|  | ||||
|   - repo: https://github.com/charliermarsh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: 'v0.0.251' | ||||
|     rev: 'v0.0.255' | ||||
|     hooks: | ||||
|       - id: ruff | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,10 @@ | ||||
| # Use BuildKit, otherwise building on ARM fails | ||||
| 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 | ||||
| TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") | ||||
| TAG_PLOT=${TAG}_plot | ||||
| @@ -14,7 +18,6 @@ TAG_ARM=${TAG}_arm | ||||
| TAG_PLOT_ARM=${TAG_PLOT}_arm | ||||
| TAG_FREQAI_ARM=${TAG_FREQAI}_arm | ||||
| TAG_FREQAI_RL_ARM=${TAG_FREQAI_RL}_arm | ||||
| CACHE_IMAGE=freqtradeorg/freqtrade_cache | ||||
|  | ||||
| echo "Running for ${TAG}" | ||||
|  | ||||
| @@ -38,13 +41,13 @@ if [ $? -ne 0 ]; then | ||||
|     echo "failed building multiarch images" | ||||
|     return 1 | ||||
| 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_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 . | ||||
|  | ||||
| # 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_FREQAI_ARM ${CACHE_IMAGE}:$TAG_FREQAI_ARM | ||||
| docker tag freqtrade:$TAG_FREQAI_RL_ARM ${CACHE_IMAGE}:$TAG_FREQAI_RL_ARM | ||||
| @@ -59,7 +62,6 @@ fi | ||||
|  | ||||
| docker images | ||||
|  | ||||
| # docker push ${IMAGE_NAME} | ||||
| docker push ${CACHE_IMAGE}:$TAG_PLOT_ARM | ||||
| docker push ${CACHE_IMAGE}:$TAG_FREQAI_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 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 | ||||
| if [ "${TAG}" = "develop" ]; then | ||||
|     echo 'Tagging image as latest' | ||||
|     docker manifest create ${IMAGE_NAME}:latest ${CACHE_IMAGE}:${TAG_ARM} ${IMAGE_NAME}:${TAG_PI} ${CACHE_IMAGE}:${TAG} | ||||
|     docker manifest push -p ${IMAGE_NAME}:latest | ||||
|  | ||||
|     crane copy ${IMAGE_NAME}:latest ${GHCR_IMAGE_NAME}:latest | ||||
| fi | ||||
|  | ||||
| docker images | ||||
| rm -rf .crane | ||||
|  | ||||
| # Cleanup old images from arm64 node. | ||||
| docker image prune -a --force --filter "until=24h" | ||||
|   | ||||
| @@ -2,6 +2,8 @@ | ||||
|  | ||||
| # 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 | ||||
| TAG=$(echo "${BRANCH_NAME}" | sed -e "s/\//_/g") | ||||
| TAG_PLOT=${TAG}_plot | ||||
| @@ -11,7 +13,6 @@ TAG_PI="${TAG}_pi" | ||||
|  | ||||
| PI_PLATFORM="linux/arm/v7" | ||||
| echo "Running for ${TAG}" | ||||
| CACHE_IMAGE=freqtradeorg/freqtrade_cache | ||||
| CACHE_TAG=${CACHE_IMAGE}:${TAG_PI}_cache | ||||
|  | ||||
| # Add commit and commit_message to docker container | ||||
|   | ||||
| @@ -248,13 +248,13 @@ FreqAI also provides a built in episodic summary logger called `self.tensorboard | ||||
|             """ | ||||
|             def calculate_reward(self, action: int) -> float: | ||||
|                 if not self._is_valid(action): | ||||
|                     self.tensorboard_log("is_valid") | ||||
|                     self.tensorboard_log("invalid") | ||||
|                     return -2 | ||||
|  | ||||
| ``` | ||||
|  | ||||
| !!! 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 | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| markdown==3.3.7 | ||||
| mkdocs==1.4.2 | ||||
| mkdocs-material==9.1.1 | ||||
| mkdocs-material==9.1.2 | ||||
| mdx_truly_sane_lists==1.3 | ||||
| pymdown-extensions==9.10 | ||||
| jinja2==3.1.2 | ||||
|   | ||||
| @@ -1040,11 +1040,10 @@ from datetime import timedelta, datetime, timezone | ||||
|  | ||||
| # Within populate indicators (or populate_buy): | ||||
| if self.config['runmode'].value in ('live', 'dry_run'): | ||||
|    # fetch closed trades for the last 2 days | ||||
|     trades = Trade.get_trades([Trade.pair == metadata['pair'], | ||||
|                                Trade.open_date > datetime.utcnow() - timedelta(days=2), | ||||
|                                Trade.is_open.is_(False), | ||||
|                 ]).all() | ||||
|     # fetch closed trades for the last 2 days | ||||
|     trades = Trade.get_trades_proxy( | ||||
|         pair=metadata['pair'], is_open=False,  | ||||
|         open_date=datetime.now(timezone.utc) - timedelta(days=2)) | ||||
|     # 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) | ||||
|     if sumprofit < 0: | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1087,7 +1087,7 @@ class Exchange: | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}.' | ||||
|                 f'Message: {e}') from e | ||||
|         except ccxt.InvalidOrder as e: | ||||
|             raise ExchangeError( | ||||
|             raise InvalidOrderException( | ||||
|                 f'Could not create {ordertype} {side} order on market {pair}. ' | ||||
|                 f'Tried to {side} amount {amount} at rate {rate}. ' | ||||
|                 f'Message: {e}') from e | ||||
| @@ -1138,7 +1138,10 @@ class Exchange: | ||||
|         # Ensure rate is less than stop price | ||||
|         if bad_stop_price: | ||||
|             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 | ||||
|  | ||||
|     def _get_stop_params(self, side: BuySell, ordertype: str, stop_price: float) -> Dict: | ||||
| @@ -2755,10 +2758,10 @@ class Exchange: | ||||
|             raise OperationalException( | ||||
|                 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"): | ||||
|  | ||||
|             isolated_liq = self.dry_run_liquidation_price( | ||||
|             liquidation_price = self.dry_run_liquidation_price( | ||||
|                 pair=pair, | ||||
|                 open_rate=open_rate, | ||||
|                 is_short=is_short, | ||||
| @@ -2773,16 +2776,16 @@ class Exchange: | ||||
|             positions = self.fetch_positions(pair) | ||||
|             if len(positions) > 0: | ||||
|                 pos = positions[0] | ||||
|                 isolated_liq = pos['liquidationPrice'] | ||||
|                 liquidation_price = pos['liquidationPrice'] | ||||
|  | ||||
|         if isolated_liq is not None: | ||||
|             buffer_amount = abs(open_rate - isolated_liq) * self.liquidation_buffer | ||||
|             isolated_liq = ( | ||||
|                 isolated_liq - buffer_amount | ||||
|         if liquidation_price is not None: | ||||
|             buffer_amount = abs(open_rate - liquidation_price) * self.liquidation_buffer | ||||
|             liquidation_price_buffer = ( | ||||
|                 liquidation_price - buffer_amount | ||||
|                 if is_short else | ||||
|                 isolated_liq + buffer_amount | ||||
|                 liquidation_price + buffer_amount | ||||
|             ) | ||||
|             return isolated_liq | ||||
|             return max(liquidation_price_buffer, 0.0) | ||||
|         else: | ||||
|             return None | ||||
|  | ||||
|   | ||||
| @@ -47,7 +47,7 @@ class Base3ActionRLEnv(BaseEnvironment): | ||||
|         self._update_unrealized_total_profit() | ||||
|         step_reward = self.calculate_reward(action) | ||||
|         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 | ||||
|         if self.is_tradesignal(action): | ||||
|   | ||||
| @@ -48,7 +48,7 @@ class Base4ActionRLEnv(BaseEnvironment): | ||||
|         self._update_unrealized_total_profit() | ||||
|         step_reward = self.calculate_reward(action) | ||||
|         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 | ||||
|         if self.is_tradesignal(action): | ||||
|   | ||||
| @@ -49,7 +49,7 @@ class Base5ActionRLEnv(BaseEnvironment): | ||||
|         self._update_unrealized_total_profit() | ||||
|         step_reward = self.calculate_reward(action) | ||||
|         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 | ||||
|         if self.is_tradesignal(action): | ||||
|   | ||||
| @@ -137,7 +137,8 @@ class BaseEnvironment(gym.Env): | ||||
|         self.np_random, seed = seeding.np_random(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 | ||||
|         to be parsed by the TensorboardCallback. This | ||||
| @@ -149,17 +150,24 @@ class BaseEnvironment(gym.Env): | ||||
|  | ||||
|         def calculate_reward(self, action: int) -> float: | ||||
|             if not self._is_valid(action): | ||||
|                 self.tensorboard_log("is_valid") | ||||
|                 self.tensorboard_log("invalid") | ||||
|                 return -2 | ||||
|  | ||||
|         :param metric: metric to be tracked and incremented | ||||
|         :param value: value to increment `metric` by | ||||
|         :param inc: sets whether the `value` is incremented or not | ||||
|         :param value: `metric` value | ||||
|         :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: | ||||
|             self.tensorboard_metrics[metric] = value | ||||
|         increment = True if value is None else False | ||||
|         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: | ||||
|             self.tensorboard_metrics[metric] += value | ||||
|             self.tensorboard_metrics[category][metric] += value | ||||
|  | ||||
|     def reset_tensorboard_log(self): | ||||
|         self.tensorboard_metrics = {} | ||||
|   | ||||
| @@ -46,14 +46,12 @@ class TensorboardCallback(BaseCallback): | ||||
|         local_info = self.locals["infos"][0] | ||||
|         tensorboard_metrics = self.training_env.get_attr("tensorboard_metrics")[0] | ||||
|  | ||||
|         for info in local_info: | ||||
|             if info not in ["episode", "terminal_observation"]: | ||||
|                 self.logger.record(f"_info/{info}", local_info[info]) | ||||
|         for metric in local_info: | ||||
|             if metric not in ["episode", "terminal_observation"]: | ||||
|                 self.logger.record(f"info/{metric}", local_info[metric]) | ||||
|  | ||||
|         for info in tensorboard_metrics: | ||||
|             if info in [action.name for action in self.actions]: | ||||
|                 self.logger.record(f"_actions/{info}", tensorboard_metrics[info]) | ||||
|             else: | ||||
|                 self.logger.record(f"_custom/{info}", tensorboard_metrics[info]) | ||||
|         for category in tensorboard_metrics: | ||||
|             for metric in tensorboard_metrics[category]: | ||||
|                 self.logger.record(f"{category}/{metric}", tensorboard_metrics[category][metric]) | ||||
|  | ||||
|         return True | ||||
|   | ||||
| @@ -100,7 +100,7 @@ class ReinforcementLearner(BaseReinforcementLearningModel): | ||||
|             """ | ||||
|             # first, penalize if the action is not valid | ||||
|             if not self._is_valid(action): | ||||
|                 self.tensorboard_log("is_valid") | ||||
|                 self.tensorboard_log("invalid", category="actions") | ||||
|                 return -2 | ||||
|  | ||||
|             pnl = self.get_unrealized_profit() | ||||
|   | ||||
| @@ -586,7 +586,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|  | ||||
|         min_entry_stake = self.exchange.get_min_pair_stake_amount(trade.pair, | ||||
|                                                                   current_entry_rate, | ||||
|                                                                   self.strategy.stoploss) | ||||
|                                                                   0.0) | ||||
|         min_exit_stake = self.exchange.get_min_pair_stake_amount(trade.pair, | ||||
|                                                                  current_exit_rate, | ||||
|                                                                  self.strategy.stoploss) | ||||
| @@ -700,7 +700,8 @@ class FreqtradeBot(LoggingMixin): | ||||
|         pos_adjust = trade is not None | ||||
|  | ||||
|         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: | ||||
|             return False | ||||
| @@ -860,7 +861,12 @@ class FreqtradeBot(LoggingMixin): | ||||
|         trade: Optional[Trade], | ||||
|         order_adjust: bool, | ||||
|         leverage_: Optional[float], | ||||
|         pos_adjust: bool, | ||||
|     ) -> Tuple[float, float, float]: | ||||
|         """ | ||||
|         Validate and eventually adjust (within limits) limit, amount and leverage | ||||
|         :return: Tuple with (price, amount, leverage) | ||||
|         """ | ||||
|  | ||||
|         if 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 | ||||
|         # edge-case for now. | ||||
|         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( | ||||
|             pair, enter_limit_requested, leverage) | ||||
|  | ||||
| @@ -1122,8 +1130,7 @@ class FreqtradeBot(LoggingMixin): | ||||
|             trade.stoploss_order_id = None | ||||
|             logger.error(f'Unable to place a stoploss order on exchange. {e}') | ||||
|             logger.warning('Exiting the trade forcefully') | ||||
|             self.execute_trade_exit(trade, stop_price, exit_check=ExitCheckTuple( | ||||
|                 exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|             self.emergency_exit(trade, stop_price) | ||||
|  | ||||
|         except ExchangeError: | ||||
|             trade.stoploss_order_id = None | ||||
| @@ -1281,13 +1288,16 @@ class FreqtradeBot(LoggingMixin): | ||||
|             if canceled and max_timeouts > 0 and canceled_count >= max_timeouts: | ||||
|                 logger.warning(f'Emergency exiting trade {trade}, as the exit order ' | ||||
|                                f'timed out {max_timeouts} times.') | ||||
|                 try: | ||||
|                     self.execute_trade_exit( | ||||
|                         trade, order['price'], | ||||
|                         exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|                 except DependencyException as exception: | ||||
|                     logger.warning( | ||||
|                         f'Unable to emergency sell trade {trade.pair}: {exception}') | ||||
|                 self.emergency_exit(trade, order['price']) | ||||
|  | ||||
|     def emergency_exit(self, trade: Trade, price: float) -> None: | ||||
|         try: | ||||
|             self.execute_trade_exit( | ||||
|                 trade, price, | ||||
|                 exit_check=ExitCheckTuple(exit_type=ExitType.EMERGENCY_EXIT)) | ||||
|         except DependencyException as exception: | ||||
|             logger.warning( | ||||
|                 f'Unable to emergency exit trade {trade.pair}: {exception}') | ||||
|  | ||||
|     def replace_order(self, order: Dict, order_obj: Optional[Order], trade: Trade) -> None: | ||||
|         """ | ||||
|   | ||||
| @@ -749,7 +749,7 @@ class Backtesting: | ||||
|             leverage = min(max(leverage, 1.0), max_leverage) | ||||
|  | ||||
|         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( | ||||
|             pair, propose_rate, leverage=leverage) | ||||
|         stake_available = self.wallets.get_available_stake_amount() | ||||
|   | ||||
| @@ -1088,6 +1088,11 @@ class LocalTrade(): | ||||
|         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. | ||||
|  | ||||
|         :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] | ||||
|         """ | ||||
|  | ||||
|   | ||||
| @@ -311,7 +311,7 @@ class LockModel(BaseModel): | ||||
|     lock_timestamp: int | ||||
|     pair: str | ||||
|     side: str | ||||
|     reason: str | ||||
|     reason: Optional[str] | ||||
|  | ||||
|  | ||||
| class Locks(BaseModel): | ||||
|   | ||||
| @@ -388,12 +388,13 @@ class RPC: | ||||
|                 Trade.close_date.desc()) | ||||
|  | ||||
|         output = [trade.to_json() for trade in trades] | ||||
|         total_trades = Trade.get_trades([Trade.is_open.is_(False)]).count() | ||||
|  | ||||
|         return { | ||||
|             "trades": output, | ||||
|             "trades_count": len(output), | ||||
|             "offset": offset, | ||||
|             "total_trades": Trade.get_trades([Trade.is_open.is_(False)]).count(), | ||||
|             "total_trades": total_trades, | ||||
|         } | ||||
|  | ||||
|     def _rpc_stats(self) -> Dict[str, Any]: | ||||
|   | ||||
| @@ -7,10 +7,10 @@ | ||||
| -r docs/requirements-docs.txt | ||||
|  | ||||
| coveralls==3.3.1 | ||||
| ruff==0.0.254 | ||||
| mypy==1.0.1 | ||||
| ruff==0.0.255 | ||||
| mypy==1.1.1 | ||||
| pre-commit==3.1.1 | ||||
| pytest==7.2.1 | ||||
| pytest==7.2.2 | ||||
| pytest-asyncio==0.20.3 | ||||
| pytest-cov==4.0.0 | ||||
| pytest-mock==3.10.0 | ||||
|   | ||||
| @@ -10,7 +10,7 @@ python-telegram-bot==13.15 | ||||
| arrow==1.2.3 | ||||
| cachetools==4.2.2 | ||||
| requests==2.28.2 | ||||
| urllib3==1.26.14 | ||||
| urllib3==1.26.15 | ||||
| jsonschema==4.17.3 | ||||
| TA-Lib==0.4.25 | ||||
| technical==1.4.0 | ||||
| @@ -34,9 +34,9 @@ orjson==3.8.7 | ||||
| sdnotify==0.3.2 | ||||
|  | ||||
| # API Server | ||||
| fastapi==0.92.0 | ||||
| pydantic==1.10.5 | ||||
| uvicorn==0.20.0 | ||||
| fastapi==0.94.0 | ||||
| pydantic==1.10.6 | ||||
| uvicorn==0.21.0 | ||||
| pyjwt==2.6.0 | ||||
| aiofiles==23.1.0 | ||||
| psutil==5.9.4 | ||||
|   | ||||
| @@ -12,8 +12,8 @@ from pandas import DataFrame | ||||
|  | ||||
| from freqtrade.enums import CandleType, MarginMode, TradingMode | ||||
| from freqtrade.exceptions import (DDosProtection, DependencyException, ExchangeError, | ||||
|                                   InvalidOrderException, OperationalException, PricingError, | ||||
|                                   TemporaryError) | ||||
|                                   InsufficientFundsError, InvalidOrderException, | ||||
|                                   OperationalException, PricingError, TemporaryError) | ||||
| from freqtrade.exchange import (Binance, Bittrex, Exchange, Kraken, amount_to_precision, | ||||
|                                 date_minus_candles, market_is_active, price_to_precision, | ||||
|                                 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 | ||||
|  | ||||
|     # test exception handling | ||||
|     with pytest.raises(DependencyException): | ||||
|     with pytest.raises(InsufficientFundsError): | ||||
|         api_mock.create_order = MagicMock(side_effect=ccxt.InsufficientFunds("0 balance")) | ||||
|         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, | ||||
|                               leverage=1.0) | ||||
|  | ||||
|     with pytest.raises(DependencyException): | ||||
|     with pytest.raises(InvalidOrderException): | ||||
|         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.create_order(pair='ETH/BTC', ordertype='limit', side="sell", amount=1, rate=200, | ||||
|   | ||||
| @@ -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) | ||||
|  | ||||
|     create_mock_trades(fee) | ||||
|     trades = Trade.get_trades([Trade.is_open.is_(False)]) | ||||
|     trade = trades[0] | ||||
|     trade = Trade.get_trades([Trade.is_open.is_(False)]).first() | ||||
|     context = MagicMock() | ||||
|     context.args = [str(trade.id)] | ||||
|     telegram._status(update=update, context=context) | ||||
|   | ||||
| @@ -2724,21 +2724,21 @@ def test_manage_open_orders_exit_usercustom( | ||||
|     assert freqtrade.strategy.check_exit_timeout.call_count == 1 | ||||
|     assert freqtrade.strategy.check_entry_timeout.call_count == 0 | ||||
|  | ||||
|     # 2nd canceled trade - Fail execute sell | ||||
|     # 2nd canceled trade - Fail execute exit | ||||
|     caplog.clear() | ||||
|     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.freqtradebot.FreqtradeBot.execute_trade_exit', | ||||
|                  side_effect=DependencyException) | ||||
|     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') | ||||
|     caplog.clear() | ||||
|     # 2nd canceled trade ... | ||||
|     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): | ||||
|         freqtrade.manage_open_orders() | ||||
|         assert et_mock.call_count == 0 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user