Merge pull request #6552 from freqtrade/feat/short

Feat/short
This commit is contained in:
Matthias 2022-03-31 21:23:57 +02:00 committed by GitHub
commit c57d807845
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
170 changed files with 32780 additions and 4305 deletions

View File

@ -2,5 +2,6 @@ include LICENSE
include README.md
recursive-include freqtrade *.py
recursive-include freqtrade/templates/ *.j2 *.ipynb
include freqtrade/exchange/binance_leverage_tiers.json
include freqtrade/rpc/api_server/ui/fallback_file.html
include freqtrade/rpc/api_server/ui/favicon.ico

View File

@ -128,7 +128,7 @@ Telegram is not mandatory. However, this is a great way to control your bot. Mor
- `/stopbuy`: Stop entering new trades.
- `/status <trade_id>|[table]`: Lists all or specific open trades.
- `/profit [<n>]`: Lists cumulative profit from all finished trades, over the last n days.
- `/forcesell <trade_id>|all`: Instantly sells the given trade (Ignoring `minimum_roi`).
- `/forceexit <trade_id>|all`: Instantly exits the given trade (Ignoring `minimum_roi`).
- `/performance`: Show performance of each finished trade grouped by pair
- `/balance`: Show account balance per currency.
- `/daily <n>`: Shows profit or loss per day, over the last n days.

View File

@ -42,7 +42,7 @@ docker build --cache-from freqtrade:${TAG_ARM} --build-arg sourceimage=${CACHE_I
docker tag freqtrade:$TAG_PLOT_ARM ${CACHE_IMAGE}:$TAG_PLOT_ARM
# Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG_ARM} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
if [ $? -ne 0 ]; then
echo "failed running backtest"

View File

@ -53,7 +53,7 @@ docker build --cache-from freqtrade:${TAG} --build-arg sourceimage=${CACHE_IMAGE
docker tag freqtrade:$TAG_PLOT ${CACHE_IMAGE}:$TAG_PLOT
# Run backtest
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV2
docker run --rm -v $(pwd)/config_examples/config_bittrex.example.json:/freqtrade/config.json:ro -v $(pwd)/tests:/tests freqtrade:${TAG} backtesting --datadir /tests/testdata --strategy-path /tests/strategy/strats/ --strategy StrategyTestV3
if [ $? -ne 0 ]; then
echo "failed running backtest"

View File

@ -8,21 +8,23 @@
"dry_run": true,
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"ask_last_balance": 0.0,
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy": {
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},

View File

@ -8,21 +8,23 @@
"dry_run": true,
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy":{
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},

View File

@ -8,21 +8,23 @@
"dry_run": true,
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"ask_last_balance": 0.0,
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy": {
"exit_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},

View File

@ -20,6 +20,8 @@
"sell_profit_offset": 0.0,
"ignore_roi_if_buy_signal": false,
"ignore_buying_expired_candle_after": 300,
"trading_mode": "spot",
"margin_mode": "",
"minimal_roi": {
"40": 0.0,
"30": 0.01,
@ -28,40 +30,41 @@
},
"stoploss": -0.10,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"price_side": "bid",
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy":{
"price_side": "ask",
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
"order_book_top": 1,
"price_last_balance": 0.0
},
"order_types": {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcesell": "market",
"forcebuy": "market",
"entry": "limit",
"exit": "limit",
"emergencyexit": "market",
"forceexit": "market",
"forceentry": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_limit_ratio": 0.99
},
"order_time_in_force": {
"buy": "gtc",
"sell": "gtc"
"entry": "gtc",
"exit": "gtc"
},
"pairlists": [
{"method": "StaticPairList"},

View File

@ -8,21 +8,23 @@
"dry_run": true,
"cancel_open_orders_on_exit": false,
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"ask_last_balance": 0.0,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy":{
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},

View File

@ -274,8 +274,8 @@ A backtesting result will look like that:
| XRP/BTC | 35 | 0.66 | 22.96 | 0.00114897 | 11.48 | 3:49:00 | 12 0 23 34.3 |
| ZEC/BTC | 22 | -0.46 | -10.18 | -0.00050971 | -5.09 | 2:22:00 | 7 0 15 31.8 |
| TOTAL | 429 | 0.36 | 152.41 | 0.00762792 | 76.20 | 4:12:00 | 186 0 243 43.4 |
========================================================= SELL REASON STATS ==========================================================
| Sell Reason | Sells | Wins | Draws | Losses |
========================================================= EXIT REASON STATS ==========================================================
| Exit Reason | Sells | Wins | Draws | Losses |
|:-------------------|--------:|------:|-------:|--------:|
| trailing_stop_loss | 205 | 150 | 0 | 55 |
| stop_loss | 166 | 0 | 0 | 166 |
@ -287,9 +287,9 @@ A backtesting result will look like that:
| ADA/BTC | 1 | 0.89 | 0.89 | 0.00004434 | 0.44 | 6:00:00 | 1 0 0 100 |
| LTC/BTC | 1 | 0.68 | 0.68 | 0.00003421 | 0.34 | 2:00:00 | 1 0 0 100 |
| TOTAL | 2 | 0.78 | 1.57 | 0.00007855 | 0.78 | 4:00:00 | 2 0 0 100 |
=============== SUMMARY METRICS ===============
================ SUMMARY METRICS ===============
| Metric | Value |
|-----------------------+---------------------|
|------------------------+---------------------|
| Backtesting from | 2019-01-01 00:00:00 |
| Backtesting to | 2019-05-01 00:00:00 |
| Max open trades | 3 |
@ -312,7 +312,7 @@ A backtesting result will look like that:
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Rejected Buy signals | 3089 |
| Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| | |
| Min balance | 0.00945123 BTC |
@ -359,14 +359,14 @@ On the other hand, if you set a too high `minimal_roi` like `"0": 0.55`
(55%), there is almost no chance that the bot will ever reach this profit.
Hence, keep in mind that your performance is an integral mix of all different elements of the strategy, your configuration, and the crypto-currency pairs you have set up.
### Sell reasons table
### Exit reasons table
The 2nd table contains a recap of sell reasons.
The 2nd table contains a recap of exit reasons.
This table can tell you which area needs some additional work (e.g. all or many of the `sell_signal` trades are losses, so you should work on improving the sell signal, or consider disabling it).
### Left open trades table
The 3rd table contains all trades the bot had to `forcesell` at the end of the backtesting period to present you the full picture.
The 3rd table contains all trades the bot had to `forceexit` at the end of the backtesting period to present you the full picture.
This is necessary to simulate realistic behavior, since the backtest period has to end at some point, while realistically, you could leave the bot running forever.
These trades are also included in the first table, but are also shown separately in this table for clarity.
@ -376,9 +376,9 @@ The last element of the backtest report is the summary metrics table.
It contains some useful key metrics about performance of your strategy on backtesting data.
```
=============== SUMMARY METRICS ===============
================ SUMMARY METRICS ===============
| Metric | Value |
|-----------------------+---------------------|
|------------------------+---------------------|
| Backtesting from | 2019-01-01 00:00:00 |
| Backtesting to | 2019-05-01 00:00:00 |
| Max open trades | 3 |
@ -391,6 +391,12 @@ It contains some useful key metrics about performance of your strategy on backte
| Avg. stake amount | 0.001 BTC |
| Total trade volume | 0.429 BTC |
| | |
| Long / Short | 352 / 77 |
| Total profit Long % | 1250.58% |
| Total profit Short % | -15.02% |
| Absolute profit Long | 0.00838792 BTC |
| Absolute profit Short | -0.00076 BTC |
| | |
| Best Pair | LSK/BTC 26.26% |
| Worst Pair | ZEC/BTC -10.18% |
| Best Trade | LSK/BTC 4.25% |
@ -400,7 +406,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Days win/draw/lose | 12 / 82 / 25 |
| Avg. Duration Winners | 4:23:00 |
| Avg. Duration Loser | 6:55:00 |
| Rejected Buy signals | 3089 |
| Rejected Entry signals | 3089 |
| Entry/Exit Timeouts | 0 / 0 |
| | |
| Min balance | 0.00945123 BTC |
@ -412,7 +418,7 @@ It contains some useful key metrics about performance of your strategy on backte
| Drawdown Start | 2019-02-15 14:10:00 |
| Drawdown End | 2019-04-11 18:15:00 |
| Market change | -5.88% |
===============================================
================================================
```
@ -430,7 +436,7 @@ It contains some useful key metrics about performance of your strategy on backte
- `Best day` / `Worst day`: Best and worst day based on daily profit.
- `Days win/draw/lose`: Winning / Losing days (draws are usually days without closed trade).
- `Avg. Duration Winners` / `Avg. Duration Loser`: Average durations for winning and losing trades.
- `Rejected Buy signals`: Buy signals that could not be acted upon due to max_open_trades being reached.
- `Rejected Entry signals`: Trade entry signals that could not be acted upon due to `max_open_trades` being reached.
- `Entry/Exit Timeouts`: Entry/exit orders which did not fill (only applicable if custom pricing is used).
- `Min balance` / `Max balance`: Lowest and Highest Wallet balance during the backtest period.
- `Drawdown (Account)`: Maximum Account Drawdown experienced. Calculated as $(Absolute Drawdown) / (DrawdownHigh + startingBalance)$.
@ -438,6 +444,9 @@ It contains some useful key metrics about performance of your strategy on backte
- `Drawdown high` / `Drawdown low`: Profit at the beginning and end of the largest drawdown period. A negative low value means initial capital lost.
- `Drawdown Start` / `Drawdown End`: Start and end datetime for this largest drawdown (can also be visualized via the `plot-dataframe` sub-command).
- `Market change`: Change of the market during the backtest period. Calculated as average of all pairs changes from the first to the last candle using the "close" column.
- `Long / Short`: Split long/short values (Only shown when short trades were made).
- `Total profit Long %` / `Absolute profit Long`: Profit long trades only (Only shown when short trades were made).
- `Total profit Short %` / `Absolute profit Short`: Profit short trades only (Only shown when short trades were made).
### Daily / Weekly / Monthly breakdown
@ -524,7 +533,7 @@ freqtrade backtesting --strategy AwesomeStrategy --timeframe 1h --timeframe-deta
```
This will load 1h data as well as 5m data for the timeframe. The strategy will be analyzed with the 1h timeframe - and for every "open trade candle" (candles where a trade is open) the 5m data will be used to simulate intra-candle movements.
All callback functions (`custom_sell()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
All callback functions (`custom_exit()`, `custom_stoploss()`, ... ) will be running for each 5m candle once the trade is opened (so 12 times in the above example of 1h timeframe, and 5m detailed timeframe).
`--timeframe-detail` must be smaller than the original timeframe, otherwise backtesting will fail to start.

View File

@ -29,21 +29,22 @@ By default, loop runs every few seconds (`internals.process_throttle_secs`) and
* Call `bot_loop_start()` strategy callback.
* Analyze strategy per pair.
* Call `populate_indicators()`
* Call `populate_buy_trend()`
* Call `populate_sell_trend()`
* Call `populate_entry_trend()`
* Call `populate_exit_trend()`
* Check timeouts for open orders.
* Calls `check_buy_timeout()` strategy callback for open buy orders.
* Calls `check_sell_timeout()` strategy callback for open sell orders.
* Verifies existing positions and eventually places sell orders.
* Considers stoploss, ROI and sell-signal, `custom_sell()` and `custom_stoploss()`.
* Determine sell-price based on `ask_strategy` configuration setting or by using the `custom_exit_price()` callback.
* Before a sell order is placed, `confirm_trade_exit()` strategy callback is called.
* Calls `check_entry_timeout()` strategy callback for open entry orders.
* Calls `check_exit_timeout()` strategy callback for open exit orders.
* Verifies existing positions and eventually places exit orders.
* Considers stoploss, ROI and exit-signal, `custom_exit()` and `custom_stoploss()`.
* Determine exit-price based on `exit_pricing` configuration setting or by using the `custom_exit_price()` callback.
* Before a exit order is placed, `confirm_trade_exit()` strategy callback is called.
* Check position adjustments for open trades if enabled by calling `adjust_trade_position()` and place additional order if required.
* Check if trade-slots are still available (if `max_open_trades` is reached).
* Verifies buy signal trying to enter new positions.
* Determine buy-price based on `bid_strategy` configuration setting, or by using the `custom_entry_price()` callback.
* Verifies entry signal trying to enter new positions.
* Determine entry-price based on `entry_pricing` configuration setting, or by using the `custom_entry_price()` callback.
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Before a buy order is placed, `confirm_trade_entry()` strategy callback is called.
* Before an entry order is placed, `confirm_trade_entry()` strategy callback is called.
This loop will be repeated again and again until the bot is stopped.
@ -54,15 +55,17 @@ This loop will be repeated again and again until the bot is stopped.
* Load historic data for configured pairlist.
* Calls `bot_loop_start()` once.
* Calculate indicators (calls `populate_indicators()` once per pair).
* Calculate buy / sell signals (calls `populate_buy_trend()` and `populate_sell_trend()` once per pair).
* Calculate entry / exit signals (calls `populate_entry_trend()` and `populate_exit_trend()` once per pair).
* Loops per candle simulating entry and exit points.
* Confirm trade buy / sell (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_entry_timeout()` / `check_exit_timeout()` strategy callbacks.
* Check for trade entry signals (`enter_long` / `enter_short` columns).
* Confirm trade entry / exits (calls `confirm_trade_entry()` and `confirm_trade_exit()` if implemented in the strategy).
* Call `custom_entry_price()` (if implemented in the strategy) to determine entry price (Prices are moved to be within the opening candle).
* In Margin and Futures mode, `leverage()` strategy callback is called to determine the desired leverage.
* Determine stake size by calling the `custom_stake_amount()` callback.
* Check position adjustments for open trades if enabled and call `adjust_trade_position()` to determine if an additional order is requested.
* Call `custom_stoploss()` and `custom_sell()` to find custom exit points.
* For sells based on sell-signal and custom-sell: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Check for Order timeouts, either via the `unfilledtimeout` configuration, or via `check_buy_timeout()` / `check_sell_timeout()` strategy callbacks.
* Call `custom_stoploss()` and `custom_exit()` to find custom exit points.
* For exits based on exit-signal and custom-exit: Call `custom_exit_price()` to determine exit price (Prices are moved to be within the closing candle).
* Generate backtest report output
!!! Note

View File

@ -99,27 +99,30 @@ Mandatory parameters are marked as **Required**, which means that they are requi
| `trailing_stop_positive_offset` | Offset on when to apply `trailing_stop_positive`. Percentage value which should be positive. More details in the [stoploss documentation](stoploss.md#trailing-stop-loss-only-once-the-trade-has-reached-a-certain-offset). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `0.0` (no offset).* <br> **Datatype:** Float
| `trailing_only_offset_is_reached` | Only apply trailing stoploss when the offset is reached. [stoploss documentation](stoploss.md). [Strategy Override](#parameters-in-the-strategy). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `fee` | Fee used during backtesting / dry-runs. Should normally not be configured, which has freqtrade fall back to the exchange default fee. Set as ratio (e.g. 0.001 = 0.1%). Fee is applied twice for each trade, once when buying, once when selling. <br> **Datatype:** Float (as ratio)
| `unfilledtimeout.buy` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled buy order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.sell` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled sell order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `trading_mode` | Specifies if you want to trade regularly, trade with leverage, or trade contracts whose prices are derived from matching cryptocurrency prices. [leverage documentation](leverage.md). <br>*Defaults to `"spot"`.* <br> **Datatype:** String
| `margin_mode` | When trading with leverage, this determines if the collateral owned by the trader will be shared or isolated to each trading pair [leverage documentation](leverage.md). <br> **Datatype:** String
| `liquidation_buffer` | A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price [leverage documentation](leverage.md). <br>*Defaults to `0.05`.* <br> **Datatype:** Float
| `unfilledtimeout.entry` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled entry order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.exit` | **Required.** How long (in minutes or seconds) the bot will wait for an unfilled exit order to complete, after which the order will be cancelled and repeated at current (new) price, as long as there is a signal. [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Integer
| `unfilledtimeout.unit` | Unit to use in unfilledtimeout setting. Note: If you set unfilledtimeout.unit to "seconds", "internals.process_throttle_secs" must be inferior or equal to timeout [Strategy Override](#parameters-in-the-strategy). <br> *Defaults to `minutes`.* <br> **Datatype:** String
| `unfilledtimeout.exit_timeout_count` | How many times can exit orders time out. Once this number of timeouts is reached, an emergency sell is triggered. 0 to disable and allow unlimited order cancels. [Strategy Override](#parameters-in-the-strategy).<br>*Defaults to `0`.* <br> **Datatype:** Integer
| `bid_strategy.price_side` | Select the side of the spread the bot should look at to get the buy rate. [More information below](#buy-price-side).<br> *Defaults to `bid`.* <br> **Datatype:** String (either `ask` or `bid`).
| `bid_strategy.ask_last_balance` | **Required.** Interpolate the bidding price. More information [below](#buy-price-without-orderbook-enabled).
| `bid_strategy.use_order_book` | Enable buying using the rates in [Order Book Bids](#buy-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `bid_strategy.order_book_top` | Bot will use the top N rate in Order Book "price_side" to buy. I.e. a value of 2 will allow the bot to pick the 2nd bid rate in [Order Book Bids](#buy-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `bid_strategy. check_depth_of_market.enabled` | Do not buy if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `bid_strategy. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `ask_strategy.price_side` | Select the side of the spread the bot should look at to get the sell rate. [More information below](#sell-price-side).<br> *Defaults to `ask`.* <br> **Datatype:** String (either `ask` or `bid`).
| `ask_strategy.bid_last_balance` | Interpolate the selling price. More information [below](#sell-price-without-orderbook-enabled).
| `ask_strategy.use_order_book` | Enable selling of open trades using [Order Book Asks](#sell-price-with-orderbook-enabled). <br> **Datatype:** Boolean
| `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
| `entry_pricing.price_side` | Select the side of the spread the bot should look at to get the entry rate. [More information below](#buy-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `entry_pricing.price_last_balance` | **Required.** Interpolate the bidding price. More information [below](#entry-price-without-orderbook-enabled).
| `entry_pricing.use_order_book` | Enable entering using the rates in [Order Book Entry](#entry-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
| `entry_pricing.order_book_top` | Bot will use the top N rate in Order Book "price_side" to enter a trade. I.e. a value of 2 will allow the bot to pick the 2nd entry in [Order Book Entry](#entry-price-with-orderbook-enabled). <br>*Defaults to `1`.* <br> **Datatype:** Positive Integer
| `entry_pricing. check_depth_of_market.enabled` | Do not enter if the difference of buy orders and sell orders is met in Order Book. [Check market depth](#check-depth-of-market). <br>*Defaults to `false`.* <br> **Datatype:** Boolean
| `entry_pricing. check_depth_of_market.bids_to_ask_delta` | The difference ratio of buy orders and sell orders found in Order Book. A value below 1 means sell order size is greater, while value greater than 1 means buy order size is higher. [Check market depth](#check-depth-of-market) <br> *Defaults to `0`.* <br> **Datatype:** Float (as ratio)
| `exit_pricing.price_side` | Select the side of the spread the bot should look at to get the exit rate. [More information below](#exit-price-side).<br> *Defaults to `same`.* <br> **Datatype:** String (either `ask`, `bid`, `same` or `other`).
| `exit_pricing.price_last_balance` | Interpolate the exiting price. More information [below](#exit-price-without-orderbook-enabled).
| `exit_pricing.use_order_book` | Enable exiting of open trades using [Order Book Exit](#exit-price-with-orderbook-enabled). <br> *Defaults to `True`.*<br> **Datatype:** Boolean
| `exit_pricing.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 Exit](#exit-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
| `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. 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_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_time_in_force` | Configure time in force for buy and sell orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `order_types` | Configure order-types depending on the action (`"entry"`, `"exit"`, `"stoploss"`, `"stoploss_on_exchange"`). [More information below](#understand-order_types). [Strategy Override](#parameters-in-the-strategy).<br> **Datatype:** Dict
| `order_time_in_force` | Configure time in force for entry and exit orders. [More information below](#understand-order_time_in_force). [Strategy Override](#parameters-in-the-strategy). <br> **Datatype:** Dict
| `custom_price_max_distance_ratio` | Configure maximum distance ratio between current and custom entry or exit price. <br>*Defaults to `0.02` 2%).*<br> **Datatype:** Positive float
| `exchange.name` | **Required.** Name of the exchange class to use. [List below](#user-content-what-values-for-exchangename). <br> **Datatype:** String
| `exchange.sandbox` | Use the 'sandbox' version of the exchange, where the exchange provides a sandbox for risk-free integration. See [here](sandbox-testing.md) in more details.<br> **Datatype:** Boolean
@ -371,7 +374,7 @@ For example, if your strategy is using a 1h timeframe, and you only want to buy
### Understand order_types
The `order_types` configuration parameter maps actions (`buy`, `sell`, `stoploss`, `emergencysell`, `forcesell`, `forcebuy`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
The `order_types` configuration parameter maps actions (`entry`, `exit`, `stoploss`, `emergencyexit`, `forceexit`, `forceentry`) to order-types (`market`, `limit`, ...) as well as configures stoploss to be on the exchange and defines stoploss on exchange update interval in seconds.
This allows to buy using limit orders, sell using
limit-orders, and create stoplosses using market orders. It also allows to set the
@ -380,20 +383,19 @@ the buy order is fulfilled.
`order_types` set in the configuration file overwrites values set in the strategy as a whole, so you need to configure the whole `order_types` dictionary in one place.
If this is configured, the following 4 values (`buy`, `sell`, `stoploss` and
`stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
If this is configured, the following 4 values (`entry`, `exit`, `stoploss` and `stoploss_on_exchange`) need to be present, otherwise, the bot will fail to start.
For information on (`emergencysell`,`forcesell`, `forcebuy`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
For information on (`emergencyexit`,`forceexit`, `forceentry`, `stoploss_on_exchange`,`stoploss_on_exchange_interval`,`stoploss_on_exchange_limit_ratio`) please see stop loss documentation [stop loss on exchange](stoploss.md)
Syntax for Strategy:
```python
order_types = {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"entry": "limit",
"exit": "limit",
"emergencyexit": "market",
"forceentry": "market",
"forceexit": "market",
"stoploss": "market",
"stoploss_on_exchange": False,
"stoploss_on_exchange_interval": 60,
@ -405,11 +407,11 @@ Configuration:
```json
"order_types": {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcebuy": "market",
"forcesell": "market",
"entry": "limit",
"exit": "limit",
"emergencyexit": "market",
"forceentry": "market",
"forceexit": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
@ -432,7 +434,7 @@ Configuration:
If `stoploss_on_exchange` is enabled and the stoploss is cancelled manually on the exchange, then the bot will create a new stoploss order.
!!! Warning "Warning: stoploss_on_exchange failures"
If stoploss on exchange creation fails for some reason, then an "emergency sell" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencysell` value in the `order_types` dictionary - however, this is not advised.
If stoploss on exchange creation fails for some reason, then an "emergency exit" is initiated. By default, this will sell the asset using a market order. The order-type for the emergency-sell can be changed by setting the `emergencyexit` value in the `order_types` dictionary - however, this is not advised.
### Understand order_time_in_force
@ -462,8 +464,8 @@ The possible values are: `gtc` (default), `fok` or `ioc`.
``` python
"order_time_in_force": {
"buy": "gtc",
"sell": "gtc"
"entry": "gtc",
"exit": "gtc"
},
```

View File

@ -29,6 +29,7 @@ usage: freqtrade download-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[--erase]
[--data-format-ohlcv {json,jsongz,hdf5}]
[--data-format-trades {json,jsongz,hdf5}]
[--trading-mode {spot,margin,futures}]
optional arguments:
-h, --help show this help message and exit
@ -59,6 +60,8 @@ optional arguments:
--data-format-trades {json,jsongz,hdf5}
Storage format for downloaded trades data. (default:
`jsongz`).
--trading-mode {spot,margin,futures}
Select Trading mode
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -193,11 +196,14 @@ usage: freqtrade convert-data [-h] [-v] [--logfile FILE] [-V] [-c PATH]
{json,jsongz,hdf5} --format-to
{json,jsongz,hdf5} [--erase]
[-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]]
[--exchange EXCHANGE]
[--trading-mode {spot,margin,futures}]
[--candle-types {spot,,futures,mark,index,premiumIndex,funding_rate} [{spot,,futures,mark,index,premiumIndex,funding_rate} ...]]
optional arguments:
-h, --help show this help message and exit
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
Limit command to these pairs. Pairs are space-
separated.
--format-from {json,jsongz,hdf5}
Source format for data conversion.
@ -208,6 +214,12 @@ optional arguments:
-t {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...], --timeframes {1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} [{1m,3m,5m,15m,30m,1h,2h,4h,6h,8h,12h,1d,3d,1w,2w,1M,1y} ...]
Specify which tickers to download. Space-separated
list. Default: `1m 5m`.
--exchange EXCHANGE Exchange name (default: `bittrex`). Only valid if no
config is provided.
--trading-mode {spot,margin,futures}
Select Trading mode
--candle-types {spot,,futures,mark,index,premiumIndex,funding_rate} [{spot,,futures,mark,index,premiumIndex,funding_rate} ...]
Select candle type to use
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).
@ -224,6 +236,7 @@ Common arguments:
Path to directory with historical backtesting data.
--userdir PATH, --user-data-dir PATH
Path to userdata directory.
```
##### Example converting data
@ -347,6 +360,7 @@ usage: freqtrade list-data [-h] [-v] [--logfile FILE] [-V] [-c PATH] [-d PATH]
[--userdir PATH] [--exchange EXCHANGE]
[--data-format-ohlcv {json,jsongz,hdf5}]
[-p PAIRS [PAIRS ...]]
[--trading-mode {spot,margin,futures}]
optional arguments:
-h, --help show this help message and exit
@ -356,8 +370,10 @@ optional arguments:
Storage format for downloaded candle (OHLCV) data.
(default: `json`).
-p PAIRS [PAIRS ...], --pairs PAIRS [PAIRS ...]
Show profits for only these pairs. Pairs are space-
Limit command to these pairs. Pairs are space-
separated.
--trading-mode {spot,margin,futures}
Select Trading mode
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -47,3 +47,17 @@ As this does however increase risk and provides no benefit, it's been removed fo
Using separate hyperopt files was deprecated in 2021.4 and was removed in 2021.9.
Please switch to the new [Parametrized Strategies](hyperopt.md) to benefit from the new hyperopt interface.
## Strategy changes between V2 and V3
Isolated Futures / short trading was introduced in 2022.4. This required major changes to configuration settings, strategy interfaces, ...
We have put a great effort into keeping compatibility with existing strategies, so if you just want to continue using freqtrade in spot markets, there are no changes necessary.
While we may drop support for the current interface sometime in the future, we will announce this separately and have an appropriate transition period.
Please follow the [Strategy migration](strategy_migration.md) guide to migrate your strategy to the new format to start using the new functionalities.
### webhooks - `buy_tag` has been renamed to `enter_tag`
This should apply only to your strategy and potentially to webhooks.
We will keep a compatibility layer for 1-2 versions (so both `buy_tag` and `enter_tag` will still work), but support for this in webhooks will disappear after that.

View File

@ -1,6 +1,6 @@
# Exchange-specific Notes
This page combines common gotchas and informations which are exchange-specific and most likely don't apply to other exchanges.
This page combines common gotchas and Information which are exchange-specific and most likely don't apply to other exchanges.
## Exchange configuration
@ -64,6 +64,25 @@ Binance supports [time_in_force](configuration.md#understand-order_time_in_force
For Binance, please add `"BNB/<STAKE>"` to your blacklist to avoid issues.
Accounts having BNB accounts use this to pay for fees - if your first trade happens to be on `BNB`, further trades will consume this position and make the initial BNB trade unsellable as the expected amount is not there anymore.
### Binance Futures' order pricing
When trading on Binance Futures market, orderbook must be used because there is no price ticker data for futures.
``` jsonc
"entry_pricing": {
"use_order_book": true,
"order_book_top": 1,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing": {
"use_order_book": true,
"order_book_top": 1
},
```
### Binance sites
Binance has been split into 2, and users must use the correct ccxt exchange ID for their exchange, otherwise API keys are not recognized.

View File

@ -6,13 +6,15 @@ Freqtrade supports spot trading only.
### Can I open short positions?
No, Freqtrade does not support trading with margin / leverage, and cannot open short positions.
Freqtrade can open short positions in futures markets.
This requires the strategy to be made for this - and `"trading_mode": "futures"` in the configuration.
Please make sure to read the [relevant documentation page](leverage.md) first.
In some cases, your exchange may provide leveraged spot tokens which can be traded with Freqtrade eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD, etc...
In spot markets, you can in some cases use leveraged spot tokens, which reflect an inverted pair (eg. BTCUP/USD, BTCDOWN/USD, ETHBULL/USD, ETHBEAR/USD,...) which can be traded with Freqtrade.
### Can I trade options or futures?
No, options and futures trading are not supported.
Futures trading is supported for selected exchanges.
## Beginner Tips & Tricks
@ -77,7 +79,7 @@ You can use "current" market data by using the [dataprovider](strategy-customiza
### Is there a setting to only SELL the coins being held and not perform anymore BUYS?
You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forcesell all` (sell all open trades).
You can use the `/stopbuy` command in Telegram to prevent future buys, followed by `/forceexit all` (sell all open trades).
### I want to run multiple bots on the same machine
@ -117,10 +119,10 @@ As the message says, your exchange does not support market orders and you have o
To fix this, redefine order types in the strategy to use "limit" instead of "market":
```
``` python
order_types = {
...
'stoploss': 'limit',
"stoploss": "limit",
...
}
```

View File

@ -153,8 +153,8 @@ Checklist on all tasks / possibilities in hyperopt
Depending on the space you want to optimize, only some of the below are required:
* define parameters with `space='buy'` - for buy signal optimization
* define parameters with `space='sell'` - for sell signal optimization
* define parameters with `space='buy'` - for entry signal optimization
* define parameters with `space='sell'` - for exit signal optimization
!!! Note
`populate_indicators` needs to create all indicators any of the spaces may use, otherwise hyperopt will not work.
@ -180,7 +180,7 @@ Hyperopt will first load your data into memory and will then run `populate_indic
Hyperopt will then spawn into different processes (number of processors, or `-j <n>`), and run backtesting over and over again, changing the parameters that are part of the `--spaces` defined.
For every new set of parameters, freqtrade will run first `populate_buy_trend()` followed by `populate_sell_trend()`, and then run the regular backtesting process to simulate trades.
For every new set of parameters, freqtrade will run first `populate_entry_trend()` followed by `populate_exit_trend()`, and then run the regular backtesting process to simulate trades.
After backtesting, the results are passed into the [loss function](#loss-functions), which will evaluate if this result was better or worse than previous results.
Based on the loss function result, hyperopt will determine the next set of parameters to try in the next round of backtesting.
@ -190,7 +190,7 @@ Based on the loss function result, hyperopt will determine the next set of param
There are two places you need to change in your strategy file to add a new buy hyperopt for testing:
* Define the parameters at the class level hyperopt shall be optimizing.
* Within `populate_buy_trend()` - use defined parameter values instead of raw constants.
* Within `populate_entry_trend()` - use defined parameter values instead of raw constants.
There you have two different types of indicators: 1. `guards` and 2. `triggers`.
@ -200,24 +200,24 @@ There you have two different types of indicators: 1. `guards` and 2. `triggers`.
!!! Hint "Guards and Triggers"
Technically, there is no difference between Guards and Triggers.
However, this guide will make this distinction to make it clear that signals should not be "sticking".
Sticking signals are signals that are active for multiple candles. This can lead into buying a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
Sticking signals are signals that are active for multiple candles. This can lead into entering a signal late (right before the signal disappears - which means that the chance of success is a lot lower than right at the beginning).
Hyper-optimization will, for each epoch round, pick one trigger and possibly multiple guards.
#### Sell optimization
#### Exit signal optimization
Similar to the buy-signal above, sell-signals can also be optimized.
Similar to the entry-signal above, exit-signals can also be optimized.
Place the corresponding settings into the following methods
* Define the parameters at the class level hyperopt shall be optimizing, either naming them `sell_*`, or by explicitly defining `space='sell'`.
* Within `populate_sell_trend()` - use defined parameter values instead of raw constants.
* Within `populate_exit_trend()` - use defined parameter values instead of raw constants.
The configuration and rules are the same than for buy signals.
## Solving a Mystery
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your buys.
And you also wonder should you use RSI or ADX to help with those buy decisions.
Let's say you are curious: should you use MACD crossings or lower Bollinger Bands to trigger your long entries.
And you also wonder should you use RSI or ADX to help with those decisions.
If you decide to use RSI or ADX, which values should I use for them?
So let's use hyperparameter optimization to solve this mystery.
@ -274,7 +274,7 @@ The last one we call `trigger` and use it to decide which buy trigger we want to
So let's write the buy strategy using these values:
```python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
# GUARDS AND TRENDS
if self.buy_adx_enabled.value:
@ -296,12 +296,12 @@ So let's write the buy strategy using these values:
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
'enter_long'] = 1
return dataframe
```
Hyperopt will now call `populate_buy_trend()` many times (`epochs`) with different value combinations.
Hyperopt will now call `populate_entry_trend()` many times (`epochs`) with different value combinations.
It will use the given historical data and simulate buys based on the buy signals generated with the above function.
Based on the results, hyperopt will tell you which parameter combination produced the best results (based on the configured [loss function](#loss-functions)).
@ -364,7 +364,7 @@ class MyAwesomeStrategy(IStrategy):
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions.append(qtpylib.crossed_above(
dataframe[f'ema_short_{self.buy_ema_short.value}'], dataframe[f'ema_long_{self.buy_ema_long.value}']
@ -376,10 +376,10 @@ class MyAwesomeStrategy(IStrategy):
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'buy'] = 1
'enter_long'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = []
conditions.append(qtpylib.crossed_above(
dataframe[f'ema_long_{self.buy_ema_long.value}'], dataframe[f'ema_short_{self.buy_ema_short.value}']
@ -391,7 +391,7 @@ class MyAwesomeStrategy(IStrategy):
if conditions:
dataframe.loc[
reduce(lambda x, y: x & y, conditions),
'sell'] = 1
'exit_long'] = 1
return dataframe
```

View File

@ -1,6 +1,6 @@
## Prices used for orders
Prices for regular orders can be controlled via the parameter structures `bid_strategy` for buying and `ask_strategy` for selling.
Prices for regular orders can be controlled via the parameter structures `entry_pricing` for trade entries and `exit_pricing` for trade exits.
Prices are always retrieved right before an order is placed, either by querying the exchange tickers or by using the orderbook data.
!!! Note
@ -9,20 +9,11 @@ Prices are always retrieved right before an order is placed, either by querying
!!! Warning "Using market orders"
Please read the section [Market order pricing](#market-order-pricing) section when using market orders.
### Buy price
### Entry price
#### Check depth of market
#### Enter price side
When check depth of market is enabled (`bid_strategy.check_depth_of_market.enabled=True`), the buy signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `bid_strategy.check_depth_of_market.bids_to_ask_delta` parameter. The buy order is only executed if the orderbook delta is greater than or equal to the configured delta value.
!!! Note
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
#### Buy price side
The configuration setting `bid_strategy.price_side` defines the side of the spread the bot looks for when buying.
The configuration setting `entry_pricing.price_side` defines the side of the orderbook the bot looks for when buying.
The following displays an orderbook.
@ -38,30 +29,53 @@ The following displays an orderbook.
...
```
If `bid_strategy.price_side` is set to `"bid"`, then the bot will use 99 as buying price.
In line with that, if `bid_strategy.price_side` is set to `"ask"`, then the bot will use 101 as buying price.
If `entry_pricing.price_side` is set to `"bid"`, then the bot will use 99 as entry price.
In line with that, if `entry_pricing.price_side` is set to `"ask"`, then the bot will use 101 as entry price.
Using `ask` price often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
Depending on the order direction (_long_/_short_), this will lead to different results. Therefore we recommend to use `"same"` or `"other"` for this configuration instead.
This would result in the following pricing matrix:
| direction | Order | setting | price | crosses spread |
|------ |--------|-----|-----|-----|
| long | buy | ask | 101 | yes |
| long | buy | bid | 99 | no |
| long | buy | same | 99 | no |
| long | buy | other | 101 | yes |
| short | sell | ask | 101 | no |
| short | sell | bid | 99 | yes |
| short | sell | same | 101 | no |
| short | sell | other | 99 | yes |
Using the other side of the orderbook often guarantees quicker filled orders, but the bot can also end up paying more than what would have been necessary.
Taker fees instead of maker fees will most likely apply even when using limit buy orders.
Also, prices at the "ask" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
Also, prices at the "other" side of the spread are higher than prices at the "bid" side in the orderbook, so the order behaves similar to a market order (however with a maximum price).
#### Buy price with Orderbook enabled
#### Entry price with Orderbook enabled
When buying with the orderbook enabled (`bid_strategy.use_order_book=True`), Freqtrade fetches the `bid_strategy.order_book_top` entries from the orderbook and uses the entry specified as `bid_strategy.order_book_top` on the configured side (`bid_strategy.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
When entering a trade with the orderbook enabled (`entry_pricing.use_order_book=True`), Freqtrade fetches the `entry_pricing.order_book_top` entries from the orderbook and uses the entry specified as `entry_pricing.order_book_top` on the configured side (`entry_pricing.price_side`) of the orderbook. 1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Buy price without Orderbook enabled
#### Entry price without Orderbook enabled
The following section uses `side` as the configured `bid_strategy.price_side` (defaults to `"bid"`).
The following section uses `side` as the configured `entry_pricing.price_side` (defaults to `"same"`).
When not using orderbook (`bid_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `bid_strategy.ask_last_balance`..
When not using orderbook (`entry_pricing.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's below the `last` traded price from the ticker. Otherwise (when the `side` price is above the `last` price), it calculates a rate between `side` and `last` price based on `entry_pricing.price_last_balance`.
The `bid_strategy.ask_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
The `entry_pricing.price_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the `last` price and values between those interpolate between ask and last price.
### Sell price
#### Check depth of market
#### Sell price side
When check depth of market is enabled (`entry_pricing.check_depth_of_market.enabled=True`), the entry signals are filtered based on the orderbook depth (sum of all amounts) for each orderbook side.
The configuration setting `ask_strategy.price_side` defines the side of the spread the bot looks for when selling.
Orderbook `bid` (buy) side depth is then divided by the orderbook `ask` (sell) side depth and the resulting delta is compared to the value of the `entry_pricing.check_depth_of_market.bids_to_ask_delta` parameter. The entry order is only executed if the orderbook delta is greater than or equal to the configured delta value.
!!! Note
A delta value below 1 means that `ask` (sell) orderbook side depth is greater than the depth of the `bid` (buy) orderbook side, while a value greater than 1 means opposite (depth of the buy side is higher than the depth of the sell side).
### Exit price
#### Exit price side
The configuration setting `exit_pricing.price_side` defines the side of the spread the bot looks for when exiting a trade.
The following displays an orderbook:
@ -77,40 +91,54 @@ The following displays an orderbook:
...
```
If `ask_strategy.price_side` is set to `"ask"`, then the bot will use 101 as selling price.
In line with that, if `ask_strategy.price_side` is set to `"bid"`, then the bot will use 99 as selling price.
If `exit_pricing.price_side` is set to `"ask"`, then the bot will use 101 as exiting price.
In line with that, if `exit_pricing.price_side` is set to `"bid"`, then the bot will use 99 as exiting price.
#### Sell price with Orderbook enabled
Depending on the order direction (_long_/_short_), this will lead to different results. Therefore we recommend to use `"same"` or `"other"` for this configuration instead.
This would result in the following pricing matrix:
When selling with the orderbook enabled (`ask_strategy.use_order_book=True`), Freqtrade fetches the `ask_strategy.order_book_top` entries in the orderbook and uses the entry specified as `ask_strategy.order_book_top` from the configured side (`ask_strategy.price_side`) as selling price.
| Direction | Order | setting | price | crosses spread |
|------ |--------|-----|-----|-----|
| long | sell | ask | 101 | no |
| long | sell | bid | 99 | yes |
| long | sell | same | 101 | no |
| long | sell | other | 99 | yes |
| short | buy | ask | 101 | yes |
| short | buy | bid | 99 | no |
| short | buy | same | 99 | no |
| short | buy | other | 101 | yes |
#### Exit price with Orderbook enabled
When exiting with the orderbook enabled (`exit_pricing.use_order_book=True`), Freqtrade fetches the `exit_pricing.order_book_top` entries in the orderbook and uses the entry specified as `exit_pricing.order_book_top` from the configured side (`exit_pricing.price_side`) as trade exit price.
1 specifies the topmost entry in the orderbook, while 2 would use the 2nd entry in the orderbook, and so on.
#### Sell price without Orderbook enabled
#### Exit price without Orderbook enabled
The following section uses `side` as the configured `ask_strategy.price_side` (defaults to `"ask"`).
The following section uses `side` as the configured `exit_pricing.price_side` (defaults to `"ask"`).
When not using orderbook (`ask_strategy.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `ask_strategy.bid_last_balance`.
When not using orderbook (`exit_pricing.use_order_book=False`), Freqtrade uses the best `side` price from the ticker if it's above the `last` traded price from the ticker. Otherwise (when the `side` price is below the `last` price), it calculates a rate between `side` and `last` price based on `exit_pricing.price_last_balance`.
The `ask_strategy.bid_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
The `exit_pricing.price_last_balance` configuration parameter controls this. A value of `0.0` will use `side` price, while `1.0` will use the last price and values between those interpolate between `side` and last price.
### Market order pricing
When using market orders, prices should be configured to use the "correct" side of the orderbook to allow realistic pricing detection.
Assuming both buy and sell are using market orders, a configuration similar to the following might be used
Assuming both entry and exits are using market orders, a configuration similar to the following must be used
``` jsonc
"order_types": {
"buy": "market",
"sell": "market"
"entry": "market",
"exit": "market"
// ...
},
"bid_strategy": {
"price_side": "ask",
"entry_pricing": {
"price_side": "other",
// ...
},
"ask_strategy":{
"price_side": "bid",
"exit_pricing":{
"price_side": "other",
// ...
},
```

106
docs/leverage.md Normal file
View File

@ -0,0 +1,106 @@
# Trading with Leverage
!!! Warning "Beta feature"
This feature is still in it's testing phase. Should you notice something you think is wrong please let us know via Discord or via Github Issue.
!!! Note "Multiple bots on one account"
You can't run 2 bots on the same account with leverage. For leveraged / margin trading, freqtrade assumes it's the only user of the account, and all liquidation levels are calculated based on this assumption.
!!! Danger "Trading with leverage is very risky"
Do not trade with a leverage > 1 using a strategy that hasn't shown positive results in a live run using the spot market. Check the stoploss of your strategy. With a leverage of 2, a stoploss of 0.5 (50%) would be too low, and these trades would be liquidated before reaching that stoploss.
We do not assume any responsibility for eventual losses that occur from using this software or this mode.
Please only use advanced trading modes when you know how freqtrade (and your strategy) works.
Also, never risk more than what you can afford to lose.
## Understand `trading_mode`
The possible values are: `spot` (default), `margin`(*Currently unavailable*) or `futures`.
### Spot
Regular trading mode (low risk)
- Long trades only (No short trades).
- No leverage.
- No Liquidation.
- Profits gained/lost are equal to the change in value of the assets (minus trading fees).
### Leverage trading modes
With leverage, a trader borrows capital from the exchange. The capital must be re-payed fully to the exchange (potentially with interest), and the trader keeps any profits, or pays any losses, from any trades made using the borrowed capital.
Because the capital must always be re-payed, exchanges will **liquidate** (forcefully sell the traders assets) a trade made using borrowed capital when the total value of assets in the leverage account drops to a certain point (a point where the total value of losses is less than the value of the collateral that the trader actually owns in the leverage account), in order to ensure that the trader has enough capital to pay the borrowed assets back to the exchange. The exchange will also charge a **liquidation fee**, adding to the traders losses.
For this reason, **DO NOT TRADE WITH LEVERAGE IF YOU DON'T KNOW EXACTLY WHAT YOUR DOING. LEVERAGE TRADING IS HIGH RISK, AND CAN RESULT IN THE VALUE OF YOUR ASSETS DROPPING TO 0 VERY QUICKLY, WITH NO CHANCE OF INCREASING IN VALUE AGAIN.**
#### Margin (currently unavailable)
Trading occurs on the spot market, but the exchange lends currency to you in an amount equal to the chosen leverage. You pay the amount lent to you back to the exchange with interest, and your profits/losses are multiplied by the leverage specified.
#### Futures
Perpetual swaps (also known as Perpetual Futures) are contracts traded at a price that is closely tied to the underlying asset they are based off of (ex.). You are not trading the actual asset but instead are trading a derivative contract. Perpetual swap contracts can last indefinitely, in contrast to futures or option contracts.
In addition to the gains/losses from the change in price of the futures contract, traders also exchange _funding fees_, which are gains/losses worth an amount that is derived from the difference in price between the futures contract and the underlying asset. The difference in price between a futures contract and the underlying asset varies between exchanges.
To trade in futures markets, you'll have to set `trading_mode` to "futures".
You will also have to pick a "margin mode" (explanation below) - with freqtrade currently only supporting isolated margin.
``` json
"trading_mode": "futures",
"margin_mode": "isolated"
```
### Margin mode
The possible values are: `isolated`, or `cross`(*currently unavailable*)
#### Isolated margin mode
Each market(trading pair), keeps collateral in a separate account
``` json
"margin_mode": "isolated"
```
#### Cross margin mode (currently unavailable)
One account is used to share collateral between markets (trading pairs). Margin is taken from total account balance to avoid liquidation when needed.
``` json
"margin_mode": "cross"
```
## Understand `liquidation_buffer`
*Defaults to `0.05`*
A ratio specifying how large of a safety net to place between the liquidation price and the stoploss to prevent a position from reaching the liquidation price.
This artificial liquidation price is calculated as:
`freqtrade_liquidation_price = liquidation_price ± (abs(open_rate - liquidation_price) * liquidation_buffer)`
- `±` = `+` for long trades
- `±` = `-` for short trades
Possible values are any floats between 0.0 and 0.99
**ex:** If a trade is entered at a price of 10 coin/USDT, and the liquidation price of this trade is 8 coin/USDT, then with `liquidation_buffer` set to `0.05` the minimum stoploss for this trade would be $8 + ((10 - 8) * 0.05) = 8 + 0.1 = 8.1$
!!! Danger "A `liquidation_buffer` of 0.0, or a low `liquidation_buffer` is likely to result in liquidations, and liquidation fees"
Currently Freqtrade is able to calculate liquidation prices, but does not calculate liquidation fees. Setting your `liquidation_buffer` to 0.0, or using a low `liquidation_buffer` could result in your positions being liquidated. Freqtrade does not track liquidation fees, so liquidations will result in inaccurate profit/loss results for your bot. If you use a low `liquidation_buffer`, it is recommended to use `stoploss_on_exchange` if your exchange supports this.
### Developer
#### Margin mode
For shorts, the currency which pays the interest fee for the `borrowed` currency is purchased at the same time of the closing trade (This means that the amount purchased in short closing trades is greater than the amount sold in short opening trades).
For longs, the currency which pays the interest fee for the `borrowed` will already be owned by the user and does not need to be purchased. The interest is subtracted from the `close_value` of the trade.
All Fees are included in `current_profit` calculations during the trade.
#### Futures mode
Funding fees are either added or subtracted from the total amount of a trade

View File

@ -14,7 +14,7 @@ pip install -U -r requirements-plot.txt
The `freqtrade plot-dataframe` subcommand shows an interactive graph with three subplots:
* Main plot with candlestics and indicators following price (sma/ema)
* Main plot with candlesticks and indicators following price (sma/ema)
* Volume bars
* Additional indicators as specified by `--indicators2`
@ -276,6 +276,10 @@ def plot_config(self):
!!! Note "Trade position adjustments"
If `position_adjustment_enable` / `adjust_trade_position()` is used, the trade initial buy price is averaged over multiple orders and the trade start price will most likely appear outside the candle range.
!!! Note "Futures / Margin trading"
`plot-dataframe` does not support Futures / short trades, so these trades will simply be missing, and it's unlikely we'll be adding this functionality to this command.
Please use freqUI instead by starting freqtrade in [webserver mode](utils.md#webserver-mode) and use the Chart page to plot your dataframe.
## Plot profit
![plot-profit](assets/plot-profit.png)

View File

@ -145,9 +145,10 @@ python3 scripts/rest_client.py --config rest_config.json <command> [optional par
| `locks` | Displays currently locked pairs.
| `delete_lock <lock_id>` | Deletes (disables) the lock by id.
| `profit` | Display a summary of your profit/loss from close trades and some stats about your performance.
| `forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `forceenter <pair> [rate]` | Instantly enters the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `forceenter <pair> <side> [rate]` | Instantly longs or shorts the given pair. Rate is optional. (`forcebuy_enable` must be set to True)
| `performance` | Show performance of each finished trade grouped by pair.
| `balance` | Show account balance per currency.
| `daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7).
@ -215,6 +216,13 @@ forcebuy
:param pair: Pair to buy (ETH/BTC)
:param price: Optional - price to buy
forceenter
Force entering a trade
:param pair: Pair to buy (ETH/BTC)
:param side: 'long' or 'short'
:param price: Optional - price to buy
forcesell
Force-sell a trade.
@ -285,6 +293,9 @@ strategy
:param strategy: Strategy class name
sysinfo
Provides system information (CPU, RAM usage)
trade
Return specific trade

View File

@ -104,16 +104,16 @@ To mitigate this, you can try to match the first order on the opposite orderbook
``` jsonc
"order_types": {
"buy": "limit",
"sell": "limit"
"entry": "limit",
"exit": "limit"
// ...
},
"bid_strategy": {
"price_side": "ask",
"entry_pricing": {
"price_side": "other",
// ...
},
"ask_strategy":{
"price_side": "bid",
"exit_pricing":{
"price_side": "other",
// ...
},
```

View File

@ -49,14 +49,14 @@ sqlite3
SELECT * FROM trades;
```
## Fix trade still open after a manual sell on the exchange
## Fix trade still open after a manual exit on the exchange
!!! Warning
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forcesell <tradeid> should be used to accomplish the same thing.
Manually selling a pair on the exchange will not be detected by the bot and it will try to sell anyway. Whenever possible, forceexit <tradeid> should be used to accomplish the same thing.
It is strongly advised to backup your database file before making any manual changes.
!!! Note
This should not be necessary after /forcesell, as forcesell orders are closed automatically by the bot on the next iteration.
This should not be necessary after /forceexit, as forceexit orders are closed automatically by the bot on the next iteration.
```sql
UPDATE trades

View File

@ -17,7 +17,7 @@ Those stoploss modes can be *on exchange* or *off exchange*.
These modes can be configured with these values:
``` python
'emergencysell': 'market',
'emergencyexit': 'market',
'stoploss_on_exchange': False
'stoploss_on_exchange_interval': 60,
'stoploss_on_exchange_limit_ratio': 0.99
@ -52,30 +52,30 @@ The bot cannot do these every 5 seconds (at each iteration), otherwise it would
So this parameter will tell the bot how often it should update the stoploss order. The default value is 60 (1 minute).
This same logic will reapply a stoploss order on the exchange should you cancel it accidentally.
### forcesell
### forceexit
`forcesell` is an optional value, which defaults to the same value as `sell` and is used when sending a `/forcesell` command from Telegram or from the Rest API.
`forceexit` is an optional value, which defaults to the same value as `exit` and is used when sending a `/forceexit` command from Telegram or from the Rest API.
### forcebuy
### forceentry
`forcebuy` is an optional value, which defaults to the same value as `buy` and is used when sending a `/forcebuy` command from Telegram or from the Rest API.
`forceentry` is an optional value, which defaults to the same value as `entry` and is used when sending a `/forceentry` command from Telegram or from the Rest API.
### emergencysell
### emergencyexit
`emergencysell` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
`emergencyexit` is an optional value, which defaults to `market` and is used when creating stop loss on exchange orders fails.
The below is the default which is used if not changed in strategy or configuration file.
Example from strategy file:
``` python
order_types = {
'buy': 'limit',
'sell': 'limit',
'emergencysell': 'market',
'stoploss': 'market',
'stoploss_on_exchange': True,
'stoploss_on_exchange_interval': 60,
'stoploss_on_exchange_limit_ratio': 0.99
"entry": "limit",
"exit": "limit",
"emergencyexit": "market",
"stoploss": "market",
"stoploss_on_exchange": True,
"stoploss_on_exchange_interval": 60,
"stoploss_on_exchange_limit_ratio": 0.99
}
```

View File

@ -49,7 +49,7 @@ from freqtrade.exchange import timeframe_to_prev_date
class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: 'Trade', order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
rate: float, time_in_force: str, exit_reason: str,
current_time: 'datetime', **kwargs) -> bool:
# Obtain pair dataframe.
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
@ -77,47 +77,47 @@ class AwesomeStrategy(IStrategy):
***
## Buy Tag
## Enter Tag
When your strategy has multiple buy signals, you can name the signal that triggered.
Then you can access you buy signal on `custom_sell`
Then you can access you buy signal on `custom_exit`
```python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] < 35) &
(dataframe['volume'] > 0)
),
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
return dataframe
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
if trade.buy_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
if trade.enter_tag == 'buy_signal_rsi' and last_candle['rsi'] > 80:
return 'sell_signal_rsi'
return None
```
!!! Note
`buy_tag` is limited to 100 characters, remaining data will be truncated.
`enter_tag` is limited to 100 characters, remaining data will be truncated.
## Exit tag
Similar to [Buy Tagging](#buy-tag), you can also specify a sell tag.
``` python
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe['rsi'] > 70) &
(dataframe['volume'] > 0)
),
['sell', 'exit_tag']] = (1, 'exit_rsi')
['exit_long', 'exit_tag']] = (1, 'exit_rsi')
return dataframe
```
@ -125,7 +125,7 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
The provided exit-tag is then used as sell-reason - and shown as such in backtest results.
!!! Note
`sell_reason` is limited to 100 characters, remaining data will be truncated.
`exit_reason` is limited to 100 characters, remaining data will be truncated.
## Strategy version

View File

@ -1,6 +1,6 @@
# Strategy Callbacks
While the main strategy functions (`populate_indicators()`, `populate_buy_trend()`, `populate_sell_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed".
While the main strategy functions (`populate_indicators()`, `populate_entry_trend()`, `populate_exit_trend()`) should be used in a vectorized way, and are only called [once during backtesting](bot-basics.md#backtesting-hyperopt-execution-logic), callbacks are called "whenever needed".
As such, you should avoid doing heavy calculations in callbacks to avoid delays during operations.
Depending on the callback used, they may be called when entering / exiting a trade, or throughout the duration of a trade.
@ -8,14 +8,15 @@ Depending on the callback used, they may be called when entering / exiting a tra
Currently available callbacks:
* [`bot_loop_start()`](#bot-loop-start)
* [`custom_stake_amount()`](#custom-stake-size)
* [`custom_sell()`](#custom-sell-signal)
* [`custom_stake_amount()`](#stake-size-management)
* [`custom_exit()`](#custom-exit-signal)
* [`custom_stoploss()`](#custom-stoploss)
* [`custom_entry_price()` and `custom_exit_price()`](#custom-order-price-rules)
* [`check_buy_timeout()` and `check_sell_timeout()](#custom-order-timeout-rules)
* [`check_entry_timeout()` and `check_exit_timeout()`](#custom-order-timeout-rules)
* [`confirm_trade_entry()`](#trade-entry-buy-order-confirmation)
* [`confirm_trade_exit()`](#trade-exit-sell-order-confirmation)
* [`adjust_trade_position()`](#adjust-trade-position)
* [`leverage()`](#leverage-callback)
!!! Tip "Callback calling sequence"
You can find the callback calling sequence in [bot-basics](bot-basics.md#bot-execution-logic)
@ -46,7 +47,7 @@ class AwesomeStrategy(IStrategy):
```
## Custom Stake size
### Stake size management
Called before entering a trade, makes it possible to manage your position size when placing a new trade.
@ -54,7 +55,7 @@ Called before entering a trade, makes it possible to manage your position size w
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
entry_tag: Optional[str], side: str, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair=pair, timeframe=self.timeframe)
current_candle = dataframe.iloc[-1].squeeze()
@ -79,15 +80,15 @@ Freqtrade will fall back to the `proposed_stake` value should your code raise an
!!! Tip
Returning `0` or `None` will prevent trades from being placed.
## Custom sell signal
## Custom exit signal
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
Allows to define custom sell signals, indicating that specified position should be sold. This is very useful when we need to customize sell conditions for each individual trade, or if you need trade data to make an exit decision.
For example you could implement a 1:2 risk-reward ROI with `custom_sell()`.
For example you could implement a 1:2 risk-reward ROI with `custom_exit()`.
Using custom_sell() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
Using custom_exit() signals in place of stoploss though *is not recommended*. It is a inferior method to using `custom_stoploss()` in this regard - which also allows you to keep the stoploss on exchange.
!!! Note
Returning a (none-empty) `string` or `True` from this method is equal to setting sell signal on a candle at specified time. This method is not called when sell signal is set already, or if sell signals are disabled (`use_sell_signal=False` or `sell_profit_only=True` while profit is below `sell_profit_offset`). `string` max length is 64 characters. Exceeding this limit will cause the message to be truncated to 64 characters.
@ -96,7 +97,7 @@ An example of how we can use different indicators depending on the current profi
``` python
class AwesomeStrategy(IStrategy):
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
@ -121,6 +122,7 @@ See [Dataframe access](strategy-advanced.md#dataframe-access) for more informati
## Custom stoploss
Called for open trade every throttling iteration (roughly every 5 seconds) until a trade is closed.
The usage of the custom stoploss method must be enabled by setting `use_custom_stoploss=True` on the strategy object.
The stoploss price can only ever move upwards - if the stoploss value returned from `custom_stoploss` would result in a lower stoploss price than was previously set, it will be ignored. The traditional `stoploss` value serves as an absolute lower level and will be instated as the initial stoploss (before this method is called for the first time for a trade).
@ -158,7 +160,7 @@ class AwesomeStrategy(IStrategy):
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current rate
@ -283,11 +285,11 @@ class AwesomeStrategy(IStrategy):
# evaluate highest to lowest, so that highest possible stop is used
if current_profit > 0.40:
return stoploss_from_open(0.25, current_profit)
return stoploss_from_open(0.25, current_profit, is_short=trade.is_short)
elif current_profit > 0.25:
return stoploss_from_open(0.15, current_profit)
return stoploss_from_open(0.15, current_profit, is_short=trade.is_short)
elif current_profit > 0.20:
return stoploss_from_open(0.07, current_profit)
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
# return maximum stoploss value, keeping current stoploss price unchanged
return 1
@ -406,7 +408,7 @@ However, freqtrade also offers a custom callback for both order types, which all
### Custom order timeout example
Called for every open order until that order is either filled or cancelled.
`check_buy_timeout()` is called for trade entries, while `check_sell_timeout()` is called for trade exit orders.
`check_entry_timeout()` is called for trade entries, while `check_exit_timeout()` is called for trade exit orders.
A simple example, which applies different unfilled-timeouts depending on the price of the asset can be seen below.
It applies a tight timeout for higher priced assets, while allowing more time to fill on cheap coins.
@ -423,11 +425,11 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = {
'buy': 60 * 25,
'sell': 60 * 25
'entry': 60 * 25,
'exit': 60 * 25
}
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True
@ -438,7 +440,7 @@ class AwesomeStrategy(IStrategy):
return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
if trade.open_rate > 100 and trade.open_date_utc < current_time - timedelta(minutes=5):
return True
@ -464,11 +466,11 @@ class AwesomeStrategy(IStrategy):
# Set unfilledtimeout to 25 hours, since the maximum timeout from below is 24 hours.
unfilledtimeout = {
'buy': 60 * 25,
'sell': 60 * 25
'entry': 60 * 25,
'exit': 60 * 25
}
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1)
current_price = ob['bids'][0][0]
@ -478,7 +480,7 @@ class AwesomeStrategy(IStrategy):
return False
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
ob = self.dp.orderbook(pair, 1)
current_price = ob['asks'][0][0]
@ -506,9 +508,9 @@ class AwesomeStrategy(IStrategy):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
side: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
@ -516,12 +518,13 @@ class AwesomeStrategy(IStrategy):
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@ -543,7 +546,7 @@ class AwesomeStrategy(IStrategy):
# ... populate_* methods
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
@ -559,7 +562,7 @@ class AwesomeStrategy(IStrategy):
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime
@ -567,7 +570,7 @@ class AwesomeStrategy(IStrategy):
:return bool: When True is returned, then the sell-order is placed on the exchange.
False aborts the process
"""
if sell_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
if exit_reason == 'force_sell' and trade.calc_profit_ratio(rate) < 0:
# Reject force-sells with negative profit
# This is just a sample, please adjust to your needs
# (this does not necessarily make sense, assuming you know when you're force-selling)
@ -591,6 +594,8 @@ Additional orders also result in additional fees and those orders don't count to
This callback is **not** called when there is an open order (either buy or sell) waiting for execution, or when you have reached the maximum amount of extra buys that you have set on `max_entry_position_adjustment`.
`adjust_trade_position()` is called very frequently for the duration of a trade, so you must keep your implementation as performant as possible.
Position adjustments will always be applied in the direction of the trade, so a positive value will always increase your position, no matter if it's a long or short trade. Modifications to leverage are not possible.
!!! Note "About stake size"
Using fixed stake size means it will be the amount used for the first order, just like without position adjustment.
If you wish to buy additional orders with DCA, then make sure to leave enough funds in the wallet for that.
@ -626,7 +631,7 @@ class DigDeeperStrategy(IStrategy):
# This is called when placing the initial order (opening trade)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
entry_tag: Optional[str], side: str, **kwargs) -> float:
# We need to leave most of the funds for possible further DCA orders
# This also applies to fixed stakes
@ -660,8 +665,8 @@ class DigDeeperStrategy(IStrategy):
if last_candle['close'] < previous_candle['close']:
return None
filled_buys = trade.select_filled_orders('buy')
count_of_buys = trade.nr_of_successful_buys
filled_entries = trade.select_filled_orders(trade.enter_side)
count_of_entries = trade.nr_of_successful_entries
# Allow up to 3 additional increasingly larger buys (4 in total)
# Initial buy is 1x
# If that falls to -5% profit, we buy 1.25x more, average profit should increase to roughly -2.2%
@ -672,9 +677,9 @@ class DigDeeperStrategy(IStrategy):
# Hope you have a deep wallet!
try:
# This returns first order stake size
stake_amount = filled_buys[0].cost
stake_amount = filled_entries[0].cost
# This then calculates current safety order size
stake_amount = stake_amount * (1 + (count_of_buys * 0.25))
stake_amount = stake_amount * (1 + (count_of_entries * 0.25))
return stake_amount
except Exception as exception:
return None
@ -682,3 +687,31 @@ class DigDeeperStrategy(IStrategy):
return None
```
## Leverage Callback
When trading in markets that allow leverage, this method must return the desired Leverage (Defaults to 1 -> No leverage).
Assuming a capital of 500USDT, a trade with leverage=3 would result in a position with 500 x 3 = 1500 USDT.
Values that are above `max_leverage` will be adjusted to `max_leverage`.
For markets / exchanges that don't support leverage, this method is ignored.
``` python
class AwesomeStrategy(IStrategy):
def leverage(self, pair: str, current_time: 'datetime', current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
```

View File

@ -26,8 +26,8 @@ This will create a new strategy file from a template, which will be located unde
A strategy file contains all the information needed to build a good strategy:
- Indicators
- Buy strategy rules
- Sell strategy rules
- Entry strategy rules
- Exit strategy rules
- Minimal ROI recommended
- Stoploss strongly recommended
@ -35,7 +35,7 @@ The bot also include a sample strategy called `SampleStrategy` you can update: `
You can test it with the parameter: `--strategy SampleStrategy`
Additionally, there is an attribute called `INTERFACE_VERSION`, which defines the version of the strategy interface the bot should use.
The current version is 2 - which is also the default when it's not set explicitly in the strategy.
The current version is 3 - which is also the default when it's not set explicitly in the strategy.
Future versions will require this to be set.
@ -82,7 +82,7 @@ As a dataframe is a table, simple python comparisons like the following will not
``` python
if dataframe['rsi'] > 30:
dataframe['buy'] = 1
dataframe['enter_long'] = 1
```
The above section will fail with `The truth value of a Series is ambiguous. [...]`.
@ -92,7 +92,7 @@ This must instead be written in a pandas-compatible way, so the operation is per
``` python
dataframe.loc[
(dataframe['rsi'] > 30)
, 'buy'] = 1
, 'enter_long'] = 1
```
With this section, you have a new column in your dataframe, which has `1` assigned whenever RSI is above 30.
@ -101,7 +101,7 @@ With this section, you have a new column in your dataframe, which has `1` assign
Buy and sell strategies need indicators. You can add more indicators by extending the list contained in the method `populate_indicators()` from your strategy file.
You should only add the indicators used in either `populate_buy_trend()`, `populate_sell_trend()`, or to populate another indicator, otherwise performance may suffer.
You should only add the indicators used in either `populate_entry_trend()`, `populate_exit_trend()`, or to populate another indicator, otherwise performance may suffer.
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
@ -199,18 +199,18 @@ If this data is available, indicators will be calculated with this extended time
!!! Note
If data for the startup period is not available, then the timerange will be adjusted to account for this startup period - so Backtesting would start at 2019-01-01 08:30:00.
### Buy signal rules
### Entry signal rules
Edit the method `populate_buy_trend()` in your strategy file to update your buy strategy.
Edit the method `populate_entry_trend()` in your strategy file to update your entry strategy.
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
This method will also define a new column, `"buy"`, which needs to contain 1 for buys, and 0 for "no action".
This method will also define a new column, `"enter_long"`, which needs to contain 1 for entries, and 0 for "no action". `enter_long` column is a mandatory column that must be set even if the strategy is shorting only.
Sample from `user_data/strategies/sample_strategy.py`:
```python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
@ -224,7 +224,36 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'buy'] = 1
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
return dataframe
```
??? Note "Enter short trades"
Short-entries can be created by setting `enter_short` (corresponds to `enter_long` for long trades).
The `enter_tag` column remains identical.
Short-trades need to be supported by your exchange and market configuration!
Please make sure to set [`can_short`]() appropriately on your strategy if you intend to short.
```python
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
dataframe.loc[
(
(qtpylib.crossed_below(dataframe['rsi'], 70)) & # Signal: RSI crosses below 70
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['enter_short', 'enter_tag']] = (1, 'rsi_cross')
return dataframe
```
@ -232,19 +261,19 @@ def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
!!! Note
Buying requires sellers to buy from - therefore volume needs to be > 0 (`dataframe['volume'] > 0`) to make sure that the bot does not buy/sell in no-activity periods.
### Sell signal rules
### Exit signal rules
Edit the method `populate_sell_trend()` into your strategy file to update your sell strategy.
Edit the method `populate_exit_trend()` into your strategy file to update your sell strategy.
Please note that the sell-signal is only used if `use_sell_signal` is set to true in the configuration.
It's important to always return the dataframe without removing/modifying the columns `"open", "high", "low", "close", "volume"`, otherwise these fields would contain something unexpected.
This method will also define a new column, `"sell"`, which needs to contain 1 for sells, and 0 for "no action".
This method will also define a new column, `"exit_long"`, which needs to contain 1 for sells, and 0 for "no action".
Sample from `user_data/strategies/sample_strategy.py`:
```python
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
@ -258,7 +287,33 @@ def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'sell'] = 1
['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
return dataframe
```
??? Note "Exit short trades"
Short-exits can be created by setting `exit_short` (corresponds to `exit_long`).
The `exit_tag` column remains identical.
Short-trades need to be supported by your exchange and market configuration!
```python
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['exit_long', 'exit_tag']] = (1, 'rsi_too_high')
dataframe.loc[
(
(qtpylib.crossed_below(dataframe['rsi'], 30)) & # Signal: RSI crosses below 30
(dataframe['tema'] < dataframe['bb_middleband']) & # Guard
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['exit_short', 'exit_tag']] = (1, 'rsi_too_low')
return dataframe
```
@ -334,9 +389,15 @@ Please note that the same buy/sell signals may work well with one timeframe, but
This setting is accessible within the strategy methods as the `self.timeframe` attribute.
### Can short
To use short signals in futures markets, you will have to let us know to do so by setting `can_short=True`.
Strategies which enable this will fail to load on spot markets.
Disabling of this will have short signals ignored (also in futures markets).
### Metadata dict
The metadata-dict (available for `populate_buy_trend`, `populate_sell_trend`, `populate_indicators`) contains additional information.
The metadata-dict (available for `populate_entry_trend`, `populate_exit_trend`, `populate_indicators`) contains additional information.
Currently this is `pair`, which can be accessed using `metadata['pair']` - and will return a pair in the format `XRP/BTC`.
The Metadata-dict should not be modified and does not persist information across multiple calls.
@ -382,6 +443,19 @@ A full sample can be found [in the DataProvider section](#complete-data-provider
It is however better to use resampling to longer timeframes whenever possible
to avoid hammering the exchange with too many requests and risk being blocked.
??? Note "Alternative candle types"
Informative_pairs can also provide a 3rd tuple element defining the candle type explicitly.
Availability of alternative candle-types will depend on the trading-mode and the exchange. Details about this can be found in the exchange documentation.
``` python
def informative_pairs(self):
return [
("ETH/USDT", "5m", ""), # Uses default candletype, depends on trading_mode
("ETH/USDT", "5m", "spot"), # Forces usage of spot candles
("BTC/TUSD", "15m", "futures"), # Uses futures candles
("BTC/TUSD", "15m", "mark"), # Uses mark candles
]
```
***
### Informative pairs decorator (`@informative()`)
@ -395,6 +469,8 @@ for more information.
``` python
def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[KwArg(str)], str]]] = None,
*,
candle_type: Optional[CandleType] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
@ -423,6 +499,7 @@ for more information.
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
```
@ -451,7 +528,7 @@ for more information.
# Define BTC/STAKE informative pair. Available in populate_indicators and other methods as
# 'btc_rsi_1h'. Current stake currency should be specified as {stake} format variable
# instead of hardcoding actual stake currency. Available in populate_indicators and other
# instead of hard-coding actual stake currency. Available in populate_indicators and other
# methods as 'btc_usdt_rsi_1h' (when stake currency is USDT).
@informative('1h', 'BTC/{stake}')
def populate_indicators_btc_1h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -490,7 +567,7 @@ for more information.
Use string formatting when accessing informative dataframes of other pairs. This will allow easily changing stake currency in config without having to adjust strategy code.
``` python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
stake = self.config['stake_currency']
dataframe.loc[
(
@ -498,7 +575,7 @@ for more information.
&
(dataframe['volume'] > 0)
),
['buy', 'buy_tag']] = (1, 'buy_signal_rsi')
['enter_long', 'enter_tag']] = (1, 'buy_signal_rsi')
return dataframe
```
@ -510,7 +587,6 @@ for more information.
will overwrite previously defined method and not produce any errors due to limitations of Python programming language. In such cases you will find that indicators
created in earlier-defined methods are not available in the dataframe. Carefully review method names and make sure they are unique!
## Additional data (DataProvider)
The strategy provides access to the `DataProvider`. This allows you to get additional data to use in your strategy.
@ -706,7 +782,7 @@ class SampleStrategy(IStrategy):
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
@ -714,7 +790,7 @@ class SampleStrategy(IStrategy):
(dataframe['rsi_1d'] < 30) & # Ensure daily RSI is < 30
(dataframe['volume'] > 0) # Ensure this candle had volume (important for backtesting)
),
'buy'] = 1
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
```
@ -791,7 +867,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
Say the open price was $100, and `current_price` is $121 (`current_profit` will be `0.21`).
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
If we want a stop price at 7% above the open price we can call `stoploss_from_open(0.07, current_profit, False)` which will return `0.1157024793`. 11.57% below $121 is $107, which is the same as 7% above $100.
``` python
@ -811,7 +887,7 @@ Stoploss values returned from `custom_stoploss` must specify a percentage relati
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit)
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
return 1
@ -832,7 +908,7 @@ In some situations it may be confusing to deal with stops relative to current ra
??? Example "Returning a stoploss using absolute price from the custom stoploss function"
If we want to trail a stop price at 2xATR below current proce we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)`.
If we want to trail a stop price at 2xATR below current price we can call `stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)`.
``` python
@ -852,7 +928,7 @@ In some situations it may be confusing to deal with stops relative to current ra
current_rate: float, current_profit: float, **kwargs) -> float:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
candle = dataframe.iloc[-1].squeeze()
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
```
@ -920,7 +996,7 @@ if self.config['runmode'].value in ('live', 'dry_run'):
Sample return value: ETH/BTC had 5 trades, with a total profit of 1.5% (ratio of 0.015).
``` json
{'pair': "ETH/BTC", 'profit': 0.015, 'count': 5}
{"pair": "ETH/BTC", "profit": 0.015, "count": 5}
```
!!! Warning
@ -974,16 +1050,16 @@ if self.config['runmode'].value in ('live', 'dry_run'):
## Print created dataframe
To inspect the created dataframe, you can issue a print-statement in either `populate_buy_trend()` or `populate_sell_trend()`.
To inspect the created dataframe, you can issue a print-statement in either `populate_entry_trend()` or `populate_exit_trend()`.
You may also want to print the pair so it's clear what data is currently shown.
``` python
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
#>> whatever condition<<<
),
'buy'] = 1
['enter_long', 'enter_tag']] = (1, 'somestring')
# Print the Analyzed pair
print(f"result for {metadata['pair']}")
@ -1012,7 +1088,12 @@ The following lists some common patterns which should be avoided to prevent frus
### Colliding signals
When buy and sell signals collide (both `'buy'` and `'sell'` are 1), freqtrade will do nothing and ignore the entry (buy) signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries.
When conflicting signals collide (e.g. both `'enter_long'` and `'exit_long'` are 1), freqtrade will do nothing and ignore the entry signal. This will avoid trades that buy, and sell immediately. Obviously, this can potentially lead to missed entries.
The following rules apply, and entry signals will be ignored if more than one of the 3 signals is set:
- `enter_long` -> `exit_long`, `enter_short`
- `enter_short` -> `exit_short`, `enter_long`
## Further strategy ideas

View File

@ -73,7 +73,7 @@ df.tail()
```python
# Report results
print(f"Generated {df['buy'].sum()} buy signals")
print(f"Generated {df['enter_long'].sum()} entry signals")
data = df.set_index('date', drop=False)
data.tail()
```

388
docs/strategy_migration.md Normal file
View File

@ -0,0 +1,388 @@
# Strategy Migration between V2 and V3
To support new markets and trade-types (namely short trades / trades with leverage), some things had to change in the interface.
If you intend on using markets other than spot markets, please migrate your strategy to the new format.
We have put a great effort into keeping compatibility with existing strategies, so if you just want to continue using freqtrade in __spot markets__, there should be no changes necessary for now.
You can use the quick summary as checklist. Please refer to the detailed sections below for full migration details.
## Quick summary / migration checklist
* Strategy methods:
* [`populate_buy_trend()` -> `populate_entry_trend()`](#populate_buy_trend)
* [`populate_sell_trend()` -> `populate_exit_trend()`](#populate_sell_trend)
* [`custom_sell()` -> `custom_exit()`](#custom_sell)
* [`check_buy_timeout()` -> `check_entry_timeout()`](#custom_entry_timeout)
* [`check_sell_timeout()` -> `check_exit_timeout()`](#custom_entry_timeout)
* New `side` argument to callbacks without trade object
* [`custom_stake_amount`](#custom-stake-amount)
* [`confirm_trade_entry`](#confirm_trade_entry)
* [Changed argument name in `confirm_trade_exit`](#confirm_trade_exit)
* Dataframe columns:
* [`buy` -> `enter_long`](#populate_buy_trend)
* [`sell` -> `exit_long`](#populate_sell_trend)
* [`buy_tag` -> `enter_tag` (used for both long and short trades)](#populate_buy_trend)
* [New column `enter_short` and corresponding new column `exit_short`](#populate_sell_trend)
* trade-object now has the following new properties: `is_short`, `enter_side`, `exit_side` and `trade_direction`.
* [Renamed `trade.nr_of_successful_buys` to `trade.nr_of_successful_entries` (mostly relevant for `adjust_trade_position()`)](#adjust-trade-position-changes)
* Introduced new [`leverage` callback](strategy-callbacks.md#leverage-callback).
* Informative pairs can now pass a 3rd element in the Tuple, defining the candle type.
* `@informative` decorator now takes an optional `candle_type` argument.
* [helper methods](#helper-methods) `stoploss_from_open` and `stoploss_from_absolute` now take `is_short` as additional argument.
* `INTERFACE_VERSION` should be set to 3.
* [Strategy/Configuration settings](#strategyconfiguration-settings).
* `order_time_in_force` buy -> entry, sell -> exit.
* `order_types` buy -> entry, sell -> exit.
* `unfilledtimeout` buy -> entry, sell -> exit.
## Extensive explanation
### `populate_buy_trend`
In `populate_buy_trend()` - you will want to change the columns you assign from `'buy`' to `'enter_long`, as well as the method name from `populate_buy_trend` to `populate_entry_trend`.
```python hl_lines="1 9"
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['buy', 'buy_tag']] = (1, 'rsi_cross')
return dataframe
```
After:
```python hl_lines="1 9"
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 30)) & # Signal: RSI crosses above 30
(dataframe['tema'] <= dataframe['bb_middleband']) & # Guard
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['enter_long', 'enter_tag']] = (1, 'rsi_cross')
return dataframe
```
Please refer to the [Strategy documentation](strategy-customization.md#entry-signal-rules) on how to enter and exit short trades.
### `populate_sell_trend`
Similar to `populate_buy_trend`, `populate_sell_trend()` will be renamed to `populate_exit_trend()`.
We'll also change the column from `"sell"` to `"exit_long"`.
``` python hl_lines="1 9"
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['sell', 'exit_tag']] = (1, 'some_exit_tag')
return dataframe
```
After
``` python hl_lines="1 9"
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(qtpylib.crossed_above(dataframe['rsi'], 70)) & # Signal: RSI crosses above 70
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
['exit_long', 'exit_tag']] = (1, 'some_exit_tag')
return dataframe
```
Please refer to the [Strategy documentation](strategy-customization.md#exit-signal-rules) on how to enter and exit short trades.
### `custom_sell`
``` python hl_lines="2"
class AwesomeStrategy(IStrategy):
def custom_sell(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# ...
```
``` python hl_lines="2"
class AwesomeStrategy(IStrategy):
def custom_exit(self, pair: str, trade: 'Trade', current_time: 'datetime', current_rate: float,
current_profit: float, **kwargs):
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last_candle = dataframe.iloc[-1].squeeze()
# ...
```
### `custom_entry_timeout`
`check_buy_timeout()` has been renamed to `check_entry_timeout()`, and `check_sell_timeout()` has been renamed to `check_exit_timeout()`.
``` python hl_lines="2 6"
class AwesomeStrategy(IStrategy):
def check_buy_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
def check_sell_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
```
``` python hl_lines="2 6"
class AwesomeStrategy(IStrategy):
def check_entry_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
def check_exit_timeout(self, pair: str, trade: 'Trade', order: dict,
current_time: datetime, **kwargs) -> bool:
return False
```
### Custom-stake-amount
New string argument `side` - which can be either `"long"` or `"short"`.
``` python hl_lines="4"
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
# ...
return proposed_stake
```
``` python hl_lines="4"
class AwesomeStrategy(IStrategy):
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], side: str, **kwargs) -> float:
# ...
return proposed_stake
```
### `confirm_trade_entry`
New string argument `side` - which can be either `"long"` or `"short"`.
``` python hl_lines="4"
class AwesomeStrategy(IStrategy):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
return True
```
After:
``` python hl_lines="4"
class AwesomeStrategy(IStrategy):
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
side: str, **kwargs) -> bool:
return True
```
### `confirm_trade_exit`
Changed argument `sell_reason` to `exit_reason`.
For compatibility, `sell_reason` will still be provided for a limited time.
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
current_time: datetime, **kwargs) -> bool:
return True
```
After:
``` python hl_lines="3"
class AwesomeStrategy(IStrategy):
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
return True
```
### Adjust trade position changes
While adjust-trade-position itself did not change, you should no longer use `trade.nr_of_successful_buys` - and instead use `trade.nr_of_successful_entries`, which will also include short entries.
### Helper methods
Added argument "is_short" to `stoploss_from_open` and `stoploss_from_absolute`.
This should be given the value of `trade.is_short`.
``` python hl_lines="5 7"
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit)
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate)
return 1
```
After:
``` python hl_lines="5 7"
def custom_stoploss(self, pair: str, trade: 'Trade', current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
# once the profit has risen above 10%, keep the stoploss at 7% above the open price
if current_profit > 0.10:
return stoploss_from_open(0.07, current_profit, is_short=trade.is_short)
return stoploss_from_absolute(current_rate - (candle['atr'] * 2), current_rate, is_short=trade.is_short)
```
### Strategy/Configuration settings
#### `order_time_in_force`
`order_time_in_force` attributes changed from `"buy"` to `"entry"` and `"sell"` to `"exit"`.
``` python
order_time_in_force: Dict = {
"buy": "gtc",
"sell": "gtc",
}
```
After:
``` python hl_lines="2 3"
order_time_in_force: Dict = {
"entry": "gtc",
"exit": "gtc",
}
```
#### `order_types`
`order_types` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
``` python hl_lines="2-6"
order_types = {
"buy": "limit",
"sell": "limit",
"emergencysell": "market",
"forcesell": "market",
"forcebuy": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
}
```
After:
``` python hl_lines="2-6"
order_types = {
"entry": "limit",
"exit": "limit",
"emergencyexit": "market",
"forceexit": "market",
"forceentry": "market",
"stoploss": "market",
"stoploss_on_exchange": false,
"stoploss_on_exchange_interval": 60
}
```
#### `unfilledtimeout`
`unfilledtimeout` have changed all wordings from `buy` to `entry` - and `sell` to `exit`.
``` python hl_lines="2-3"
unfilledtimeout = {
"buy": 10,
"sell": 10,
"exit_timeout_count": 0,
"unit": "minutes"
}
```
After:
``` python hl_lines="2-3"
unfilledtimeout = {
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
}
```
#### `order pricing`
Order pricing changed in 2 ways. `bid_strategy` was renamed to `entry_pricing` and `ask_strategy` was renamed to `exit_pricing`.
The attributes `ask_last_balance` -> `price_last_balance` and `bid_last_balance` -> `price_last_balance` were renamed as well.
Also, price-side can now be defined as `ask`, `bid`, `same` or `other`.
Please refer to the [pricing documentation](configuration.md#prices-used-for-orders) for more information.
``` json hl_lines="2-3 6 12-13 16"
{
"bid_strategy": {
"price_side": "bid",
"use_order_book": true,
"order_book_top": 1,
"ask_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy":{
"price_side": "ask",
"use_order_book": true,
"order_book_top": 1,
"bid_last_balance": 0.0
}
}
```
after:
``` json hl_lines="2-3 6 12-13 16"
{
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0
}
}
```

View File

@ -171,9 +171,10 @@ official commands. You can ask at any moment for help with `/help`.
| `/locks` | Show currently locked pairs.
| `/unlock <pair or lock_id>` | Remove the lock for this pair (or for this lock id).
| `/profit [<n>]` | Display a summary of your profit/loss from close trades and some stats about your performance, over the last n days (all trades by default)
| `/forcesell <trade_id>` | Instantly sells the given trade (Ignoring `minimum_roi`).
| `/forcesell all` | Instantly sells all open trades (Ignoring `minimum_roi`).
| `/forcebuy <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
| `/forceexit <trade_id>` | Instantly exits the given trade (Ignoring `minimum_roi`).
| `/forceexit all` | Instantly exits all open trades (Ignoring `minimum_roi`).
| `/forcelong <pair> [rate]` | Instantly buys the given pair. Rate is optional and only applies to limit orders. (`forcebuy_enable` must be set to True)
| `/forceshort <pair> [rate]` | Instantly shorts the given pair. Rate is optional and only applies to limit orders. This will only work on non-spot markets. (`forcebuy_enable` must be set to True)
| `/performance` | Show performance of each finished trade grouped by pair
| `/balance` | Show account balance per currency
| `/daily <n>` | Shows profit or loss per day, over the last n days (n defaults to 7)
@ -216,11 +217,14 @@ Once all positions are sold, run `/stop` to completely stop the bot.
### /status
For each open trade, the bot will send you the following message.
Enter Tag is configurable via Strategy.
> **Trade ID:** `123` `(since 1 days ago)`
> **Current Pair:** CVC/BTC
> **Open Since:** `1 days ago`
> **Direction:** Long
> **Leverage:** 1.0
> **Amount:** `26.64180098`
> **Enter Tag:** Awesome Long Signal
> **Open Rate:** `0.00007489`
> **Current Rate:** `0.00007489`
> **Current Profit:** `12.95%`
@ -231,10 +235,10 @@ For each open trade, the bot will send you the following message.
Return the status of all open trades in a table format.
```
ID Pair Since Profit
ID L/S Pair Since Profit
---- -------- ------- --------
67 SC/BTC 1 d 13.33%
123 CVC/BTC 1 h 12.95%
67 L SC/BTC 1 d 13.33%
123 S CVC/BTC 1 h 12.95%
```
### /count
@ -270,14 +274,16 @@ Starting capital is either taken from the `available_capital` setting, or calcul
### /forcesell <trade_id>
> **BITTREX:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
> **BINANCE:** Selling BTC/LTC with limit `0.01650000 (profit: ~-4.07%, -0.00008168)`
### /forcebuy <pair> [rate]
### /forcelong <pair> [rate] | /forceshort <pair> [rate]
> **BITTREX:** Buying ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
`/forcebuy <pair> [rate]` is also supported for longs but should be considered deprecated.
Omitting the pair will open a query asking for the pair to buy (based on the current whitelist).
Trades crated through `/forcebuy` will have the buy-tag of `forceentry`.
> **BINANCE:** Long ETH/BTC with limit `0.03400000` (`1.000000 ETH`, `225.290 USD`)
Omitting the pair will open a query asking for the pair to trade (based on the current whitelist).
Trades crated through `/forceentry` will have the buy-tag of `forceentry`.
![Telegram force-buy screenshot](assets/telegram_forcebuy.png)

View File

@ -439,14 +439,15 @@ usage: freqtrade list-markets [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
[--print-list] [--print-json] [-1] [--print-csv]
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]]
[-a]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
[--trading-mode {spot,margin,futures}]
usage: freqtrade list-pairs [-h] [-v] [--logfile FILE] [-V] [-c PATH]
[-d PATH] [--userdir PATH] [--exchange EXCHANGE]
[--print-list] [--print-json] [-1] [--print-csv]
[--base BASE_CURRENCY [BASE_CURRENCY ...]]
[--quote QUOTE_CURRENCY [QUOTE_CURRENCY ...]] [-a]
[--trading-mode {spot,margin,futures}]
optional arguments:
-h, --help show this help message and exit
@ -463,6 +464,8 @@ optional arguments:
Specify quote currency(-ies). Space-separated list.
-a, --all Print all pairs or market symbols. By default only
active ones are shown.
--trading-mode {spot,margin,futures}
Select Trading mode
Common arguments:
-v, --verbose Verbose mode (-vv for more, -vvv to get all messages).

View File

@ -98,12 +98,14 @@ Different payloads can be configured for different events. Not all fields are ne
### Webhookbuy
The fields in `webhook.webhookbuy` are filled when the bot executes a buy. Parameters are filled using string.format.
The fields in `webhook.webhookbuy` are filled when the bot executes a long/short. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* ~~`limit` # Deprecated - should no longer be used.~~
* `open_rate`
* `amount`
@ -114,16 +116,18 @@ Possible parameters are:
* `fiat_currency`
* `order_type`
* `current_rate`
* `buy_tag`
* `enter_tag`
### Webhookbuycancel
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a buy order. Parameters are filled using string.format.
The fields in `webhook.webhookbuycancel` are filled when the bot cancels a long/short order. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `limit`
* `amount`
* `open_date`
@ -133,16 +137,18 @@ Possible parameters are:
* `fiat_currency`
* `order_type`
* `current_rate`
* `buy_tag`
* `enter_tag`
### Webhookbuyfill
The fields in `webhook.webhookbuyfill` are filled when the bot filled a buy order. Parameters are filled using string.format.
The fields in `webhook.webhookbuyfill` are filled when the bot filled a long/short order. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `open_rate`
* `amount`
* `open_date`
@ -152,16 +158,17 @@ Possible parameters are:
* `fiat_currency`
* `order_type`
* `current_rate`
* `buy_tag`
* `enter_tag`
### Webhooksell
The fields in `webhook.webhooksell` are filled when the bot sells a trade. Parameters are filled using string.format.
Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `limit`
* `amount`
@ -184,6 +191,8 @@ Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `close_rate`
* `amount`
@ -207,6 +216,8 @@ Possible parameters are:
* `trade_id`
* `exchange`
* `pair`
* `direction`
* `leverage`
* `gain`
* `limit`
* `amount`

View File

@ -30,6 +30,7 @@ dependencies:
- colorama
- questionary
- prompt-toolkit
- schedule
- python-dateutil

View File

@ -48,7 +48,8 @@ ARGS_LIST_EXCHANGES = ["print_one_column", "list_exchanges_all"]
ARGS_LIST_TIMEFRAMES = ["exchange", "print_one_column"]
ARGS_LIST_PAIRS = ["exchange", "print_list", "list_pairs_print_json", "print_one_column",
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all"]
"print_csv", "base_currencies", "quote_currencies", "list_pairs_all",
"trading_mode"]
ARGS_TEST_PAIRLIST = ["verbosity", "config", "quote_currencies", "print_one_column",
"list_pairs_print_json", "exchange"]
@ -60,15 +61,17 @@ ARGS_BUILD_CONFIG = ["config"]
ARGS_BUILD_STRATEGY = ["user_data_dir", "strategy", "template"]
ARGS_CONVERT_DATA = ["pairs", "format_from", "format_to", "erase"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes"]
ARGS_CONVERT_DATA_OHLCV = ARGS_CONVERT_DATA + ["timeframes", "exchange", "trading_mode",
"candle_types"]
ARGS_CONVERT_TRADES = ["pairs", "timeframes", "exchange", "dataformat_ohlcv", "dataformat_trades"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs"]
ARGS_LIST_DATA = ["exchange", "dataformat_ohlcv", "pairs", "trading_mode"]
ARGS_DOWNLOAD_DATA = ["pairs", "pairs_file", "days", "new_pairs_days", "include_inactive",
"timerange", "download_trades", "exchange", "timeframes",
"erase", "dataformat_ohlcv", "dataformat_trades"]
"erase", "dataformat_ohlcv", "dataformat_trades", "trading_mode"]
ARGS_PLOT_DATAFRAME = ["pairs", "indicators1", "indicators2", "plot_limit",
"db_url", "trade_source", "export", "exportfilename",

View File

@ -104,7 +104,7 @@ def ask_user_config() -> Dict[str, Any]:
"type": "select",
"name": "exchange_name",
"message": "Select exchange",
"choices": [
"choices": lambda x: [
"binance",
"binanceus",
"bittrex",
@ -114,10 +114,18 @@ def ask_user_config() -> Dict[str, Any]:
"kraken",
"kucoin",
"okx",
Separator(),
Separator("------------------"),
"other",
],
},
{
"type": "confirm",
"name": "trading_mode",
"message": "Do you want to trade Perpetual Swaps (perpetual futures)?",
"default": False,
"filter": lambda val: 'futures' if val else 'spot',
"when": lambda x: x["exchange_name"] in ['binance', 'gateio', 'okx'],
},
{
"type": "autocomplete",
"name": "exchange_name",
@ -194,7 +202,11 @@ def ask_user_config() -> Dict[str, Any]:
if not answers:
# Interrupted questionary sessions return an empty dict.
raise OperationalException("User interrupted interactive questions.")
answers['margin_mode'] = (
'isolated'
if answers.get('trading_mode') == 'futures'
else ''
)
# Force JWT token to be a random string
answers['api_server_jwt_key'] = secrets.token_hex()

View File

@ -5,6 +5,7 @@ from argparse import SUPPRESS, ArgumentTypeError
from freqtrade import __version__, constants
from freqtrade.constants import HYPEROPT_LOSS_BUILTIN
from freqtrade.enums import CandleType
def check_int_positive(value: str) -> int:
@ -179,7 +180,6 @@ AVAILABLE_CLI_OPTIONS = {
'--export',
help='Export backtest results (default: trades).',
choices=constants.EXPORT_OPTIONS,
),
"exportfilename": Arg(
"--export-filename",
@ -356,6 +356,17 @@ AVAILABLE_CLI_OPTIONS = {
nargs='+',
metavar='BASE_CURRENCY',
),
"trading_mode": Arg(
'--trading-mode',
help='Select Trading mode',
choices=constants.TRADING_MODES,
),
"candle_types": Arg(
'--candle-types',
help='Select candle type to use',
choices=[c.value for c in CandleType],
nargs='+',
),
# Script options
"pairs": Arg(
'-p', '--pairs',

View File

@ -8,7 +8,7 @@ from freqtrade.configuration import TimeRange, setup_utils_configuration
from freqtrade.data.converter import convert_ohlcv_format, convert_trades_format
from freqtrade.data.history import (convert_trades_to_ohlcv, refresh_backtest_ohlcv_data,
refresh_backtest_trades_data)
from freqtrade.enums import RunMode
from freqtrade.enums import CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.exchange.exchange import market_is_active
@ -64,6 +64,8 @@ def start_download_data(args: Dict[str, Any]) -> None:
try:
if config.get('download_trades'):
if config.get('trading_mode') == 'futures':
raise OperationalException("Trade download not supported for futures.")
pairs_not_available = refresh_backtest_trades_data(
exchange, pairs=expanded_pairs, datadir=config['datadir'],
timerange=timerange, new_pairs_days=config['new_pairs_days'],
@ -81,7 +83,9 @@ def start_download_data(args: Dict[str, Any]) -> None:
exchange, pairs=expanded_pairs, timeframes=config['timeframes'],
datadir=config['datadir'], timerange=timerange,
new_pairs_days=config['new_pairs_days'],
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'])
erase=bool(config.get('erase')), data_format=config['dataformat_ohlcv'],
trading_mode=config.get('trading_mode', 'spot'),
)
except KeyboardInterrupt:
sys.exit("SIGINT received, aborting ...")
@ -133,9 +137,11 @@ def start_convert_data(args: Dict[str, Any], ohlcv: bool = True) -> None:
"""
config = setup_utils_configuration(args, RunMode.UTIL_NO_EXCHANGE)
if ohlcv:
candle_types = [CandleType.from_string(ct) for ct in config.get('candle_types', ['spot'])]
for candle_type in candle_types:
convert_ohlcv_format(config,
convert_from=args['format_from'], convert_to=args['format_to'],
erase=args['erase'])
erase=args['erase'], candle_type=candle_type)
else:
convert_trades_format(config,
convert_from=args['format_from'], convert_to=args['format_to'],
@ -154,17 +160,26 @@ def start_list_data(args: Dict[str, Any]) -> None:
from freqtrade.data.history.idatahandler import get_datahandler
dhc = get_datahandler(config['datadir'], config['dataformat_ohlcv'])
paircombs = dhc.ohlcv_get_available_data(config['datadir'])
paircombs = dhc.ohlcv_get_available_data(
config['datadir'],
config.get('trading_mode', TradingMode.SPOT)
)
if args['pairs']:
paircombs = [comb for comb in paircombs if comb[0] in args['pairs']]
print(f"Found {len(paircombs)} pair / timeframe combinations.")
groupedpair = defaultdict(list)
for pair, timeframe in sorted(paircombs, key=lambda x: (x[0], timeframe_to_minutes(x[1]))):
groupedpair[pair].append(timeframe)
for pair, timeframe, candle_type in sorted(
paircombs,
key=lambda x: (x[0], timeframe_to_minutes(x[1]), x[2])
):
groupedpair[(pair, candle_type)].append(timeframe)
if groupedpair:
print(tabulate([(pair, ', '.join(timeframes)) for pair, timeframes in groupedpair.items()],
headers=("Pair", "Timeframe"),
print(tabulate([
(pair, ', '.join(timeframes), candle_type)
for (pair, candle_type), timeframes in groupedpair.items()
],
headers=("Pair", "Timeframe", "Type"),
tablefmt='psql', stralign='right'))

View File

@ -131,7 +131,7 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
try:
pairs = exchange.get_markets(base_currencies=base_currencies,
quote_currencies=quote_currencies,
pairs_only=pairs_only,
tradable_only=pairs_only,
active_only=active_only)
# Sort the pairs/markets by symbol
pairs = dict(sorted(pairs.items()))
@ -151,15 +151,19 @@ def start_list_markets(args: Dict[str, Any], pairs_only: bool = False) -> None:
if quote_currencies else ""))
headers = ["Id", "Symbol", "Base", "Quote", "Active",
*(['Is pair'] if not pairs_only else [])]
"Spot", "Margin", "Future", "Leverage"]
tabular_data = []
for _, v in pairs.items():
tabular_data.append({'Id': v['id'], 'Symbol': v['symbol'],
'Base': v['base'], 'Quote': v['quote'],
tabular_data = [{
'Id': v['id'],
'Symbol': v['symbol'],
'Base': v['base'],
'Quote': v['quote'],
'Active': market_is_active(v),
**({'Is pair': exchange.market_is_tradable(v)}
if not pairs_only else {})})
'Spot': 'Spot' if exchange.market_is_spot(v) else '',
'Margin': 'Margin' if exchange.market_is_margin(v) else '',
'Future': 'Future' if exchange.market_is_future(v) else '',
'Leverage': exchange.get_max_leverage(v['symbol'], 20)
} for _, v in pairs.items()]
if (args.get('print_one_column', False) or
args.get('list_pairs_print_json', False) or

View File

@ -6,7 +6,8 @@ from jsonschema import Draft4Validator, validators
from jsonschema.exceptions import ValidationError, best_match
from freqtrade import constants
from freqtrade.enums import RunMode
from freqtrade.configuration.deprecated_settings import process_deprecated_setting
from freqtrade.enums import RunMode, TradingMode
from freqtrade.exceptions import OperationalException
@ -80,6 +81,7 @@ def validate_config_consistency(conf: Dict[str, Any]) -> None:
_validate_protections(conf)
_validate_unlimited_amount(conf)
_validate_ask_orderbook(conf)
validate_migrated_strategy_settings(conf)
# validate configuration before returning
logger.info('Validating configuration ...')
@ -101,13 +103,15 @@ def _validate_price_config(conf: Dict[str, Any]) -> None:
"""
When using market orders, price sides must be using the "other" side of the price
"""
if (conf.get('order_types', {}).get('buy') == 'market'
and conf.get('bid_strategy', {}).get('price_side') != 'ask'):
raise OperationalException('Market buy orders require bid_strategy.price_side = "ask".')
# TODO: The below could be an enforced setting when using market orders
if (conf.get('order_types', {}).get('entry') == 'market'
and conf.get('entry_pricing', {}).get('price_side') not in ('ask', 'other')):
raise OperationalException(
'Market entry orders require entry_pricing.price_side = "other".')
if (conf.get('order_types', {}).get('sell') == 'market'
and conf.get('ask_strategy', {}).get('price_side') != 'bid'):
raise OperationalException('Market sell orders require ask_strategy.price_side = "bid".')
if (conf.get('order_types', {}).get('exit') == 'market'
and conf.get('exit_pricing', {}).get('price_side') not in ('bid', 'other')):
raise OperationalException('Market exit orders require exit_pricing.price_side = "other".')
def _validate_trailing_stoploss(conf: Dict[str, Any]) -> None:
@ -190,13 +194,13 @@ def _validate_protections(conf: Dict[str, Any]) -> None:
def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
ask_strategy = conf.get('ask_strategy', {})
ask_strategy = conf.get('exit_pricing', {})
ob_min = ask_strategy.get('order_book_min')
ob_max = ask_strategy.get('order_book_max')
if ob_min is not None and ob_max is not None and ask_strategy.get('use_order_book'):
if ob_min != ob_max:
raise OperationalException(
"Using order_book_max != order_book_min in ask_strategy is no longer supported."
"Using order_book_max != order_book_min in exit_pricing is no longer supported."
"Please pick one value and use `order_book_top` in the future."
)
else:
@ -205,5 +209,106 @@ def _validate_ask_orderbook(conf: Dict[str, Any]) -> None:
logger.warning(
"DEPRECATED: "
"Please use `order_book_top` instead of `order_book_min` and `order_book_max` "
"for your `ask_strategy` configuration."
"for your `exit_pricing` configuration."
)
def validate_migrated_strategy_settings(conf: Dict[str, Any]) -> None:
_validate_time_in_force(conf)
_validate_order_types(conf)
_validate_unfilledtimeout(conf)
_validate_pricing_rules(conf)
def _validate_time_in_force(conf: Dict[str, Any]) -> None:
time_in_force = conf.get('order_time_in_force', {})
if 'buy' in time_in_force or 'sell' in time_in_force:
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
"Please migrate your time_in_force settings to use 'entry' and 'exit'.")
else:
logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for time_in_force is deprecated."
"Please migrate your time_in_force settings to use 'entry' and 'exit'."
)
process_deprecated_setting(
conf, 'order_time_in_force', 'buy', 'order_time_in_force', 'entry')
process_deprecated_setting(
conf, 'order_time_in_force', 'sell', 'order_time_in_force', 'exit')
def _validate_order_types(conf: Dict[str, Any]) -> None:
order_types = conf.get('order_types', {})
if any(x in order_types for x in ['buy', 'sell', 'emergencysell', 'forcebuy', 'forcesell']):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
"Please migrate your order_types settings to use the new wording.")
else:
logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for order_types is deprecated."
"Please migrate your order_types settings to use 'entry' and 'exit' wording."
)
for o, n in [
('buy', 'entry'),
('sell', 'exit'),
('emergencysell', 'emergencyexit'),
('forcesell', 'forceexit'),
('forcebuy', 'forceentry'),
]:
process_deprecated_setting(conf, 'order_types', o, 'order_types', n)
def _validate_unfilledtimeout(conf: Dict[str, Any]) -> None:
unfilledtimeout = conf.get('unfilledtimeout', {})
if any(x in unfilledtimeout for x in ['buy', 'sell']):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
"Please migrate your unfilledtimeout settings to use the new wording.")
else:
logger.warning(
"DEPRECATED: Using 'buy' and 'sell' for unfilledtimeout is deprecated."
"Please migrate your unfilledtimeout settings to use 'entry' and 'exit' wording."
)
for o, n in [
('buy', 'entry'),
('sell', 'exit'),
]:
process_deprecated_setting(conf, 'unfilledtimeout', o, 'unfilledtimeout', n)
def _validate_pricing_rules(conf: Dict[str, Any]) -> None:
if conf.get('ask_strategy') or conf.get('bid_strategy'):
if conf.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
raise OperationalException(
"Please migrate your pricing settings to use the new wording.")
else:
logger.warning(
"DEPRECATED: Using 'ask_strategy' and 'bid_strategy' is deprecated."
"Please migrate your settings to use 'entry_pricing' and 'exit_pricing'."
)
conf['entry_pricing'] = {}
for obj in list(conf.get('bid_strategy', {}).keys()):
if obj == 'ask_last_balance':
process_deprecated_setting(conf, 'bid_strategy', obj,
'entry_pricing', 'price_last_balance')
else:
process_deprecated_setting(conf, 'bid_strategy', obj, 'entry_pricing', obj)
del conf['bid_strategy']
conf['exit_pricing'] = {}
for obj in list(conf.get('ask_strategy', {}).keys()):
if obj == 'bid_last_balance':
process_deprecated_setting(conf, 'ask_strategy', obj,
'exit_pricing', 'price_last_balance')
else:
process_deprecated_setting(conf, 'ask_strategy', obj, 'exit_pricing', obj)
del conf['ask_strategy']

View File

@ -13,7 +13,7 @@ from freqtrade.configuration.deprecated_settings import process_temporary_deprec
from freqtrade.configuration.directory_operations import create_datadir, create_userdata_dir
from freqtrade.configuration.environment_vars import enironment_vars_to_dict
from freqtrade.configuration.load_config import load_config_file, load_file
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, RunMode
from freqtrade.enums import NON_UTIL_MODES, TRADING_MODES, CandleType, RunMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.loggers import setup_logging
from freqtrade.misc import deep_merge_dicts, parse_db_uri_for_logging
@ -81,8 +81,6 @@ class Configuration:
# Normalize config
if 'internals' not in config:
config['internals'] = {}
if 'ask_strategy' not in config:
config['ask_strategy'] = {}
if 'pairlists' not in config:
config['pairlists'] = []
@ -433,6 +431,12 @@ class Configuration:
def _process_data_options(self, config: Dict[str, Any]) -> None:
self._args_to_config(config, argname='new_pairs_days',
logstring='Detected --new-pairs-days: {}')
self._args_to_config(config, argname='trading_mode',
logstring='Detected --trading-mode: {}')
config['candle_type_def'] = CandleType.get_default(config.get('trading_mode', 'spot'))
config['trading_mode'] = TradingMode(config.get('trading_mode', 'spot'))
self._args_to_config(config, argname='candle_types',
logstring='Detected --candle-types: {}')
def _process_runmode(self, config: Dict[str, Any]) -> None:

View File

@ -64,6 +64,7 @@ def process_deprecated_setting(config: Dict[str, Any],
section_new_config = config.get(section_new, {}) if section_new else config
section_new_config[name_new] = section_old_config[name_old]
del section_old_config[name_old]
def process_temporary_deprecated_settings(config: Dict[str, Any]) -> None:

View File

@ -5,6 +5,8 @@ bot constants
"""
from typing import List, Tuple
from freqtrade.enums import CandleType
DEFAULT_CONFIG = 'config.json'
DEFAULT_EXCHANGE = 'bittrex'
@ -17,9 +19,9 @@ DEFAULT_DB_PROD_URL = 'sqlite:///tradesv3.sqlite'
DEFAULT_DB_DRYRUN_URL = 'sqlite:///tradesv3.dryrun.sqlite'
UNLIMITED_STAKE_AMOUNT = 'unlimited'
DEFAULT_AMOUNT_RESERVE_PERCENT = 0.05
REQUIRED_ORDERTIF = ['buy', 'sell']
REQUIRED_ORDERTYPES = ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
ORDERBOOK_SIDES = ['ask', 'bid']
REQUIRED_ORDERTIF = ['entry', 'exit']
REQUIRED_ORDERTYPES = ['entry', 'exit', 'stoploss', 'stoploss_on_exchange']
PRICING_SIDES = ['ask', 'bid', 'same', 'other']
ORDERTYPE_POSSIBILITIES = ['limit', 'market']
ORDERTIF_POSSIBILITIES = ['gtc', 'fok', 'ioc']
HYPEROPT_LOSS_BUILTIN = ['ShortTradeDurHyperOptLoss', 'OnlyProfitHyperOptLoss',
@ -43,6 +45,8 @@ DEFAULT_DATAFRAME_COLUMNS = ['date', 'open', 'high', 'low', 'close', 'volume']
# Don't modify sequence of DEFAULT_TRADES_COLUMNS
# it has wide consequences for stored trades files
DEFAULT_TRADES_COLUMNS = ['timestamp', 'id', 'type', 'side', 'price', 'amount', 'cost']
TRADING_MODES = ['spot', 'margin', 'futures']
MARGIN_MODES = ['cross', 'isolated', '']
LAST_BT_RESULT_FN = '.last_result.json'
FTHYPT_FILEVERSION = 'fthypt_fileversion'
@ -150,6 +154,9 @@ CONF_SCHEMA = {
'sell_profit_offset': {'type': 'number'},
'ignore_roi_if_buy_signal': {'type': 'boolean'},
'ignore_buying_expired_candle_after': {'type': 'number'},
'trading_mode': {'type': 'string', 'enum': TRADING_MODES},
'margin_mode': {'type': 'string', 'enum': MARGIN_MODES},
'liquidation_buffer': {'type': 'number', 'minimum': 0.0, 'maximum': 0.99},
'backtest_breakdown': {
'type': 'array',
'items': {'type': 'string', 'enum': BACKTEST_BREAKDOWNS}
@ -158,22 +165,22 @@ CONF_SCHEMA = {
'unfilledtimeout': {
'type': 'object',
'properties': {
'buy': {'type': 'number', 'minimum': 1},
'sell': {'type': 'number', 'minimum': 1},
'entry': {'type': 'number', 'minimum': 1},
'exit': {'type': 'number', 'minimum': 1},
'exit_timeout_count': {'type': 'number', 'minimum': 0, 'default': 0},
'unit': {'type': 'string', 'enum': TIMEOUT_UNITS, 'default': 'minutes'}
}
},
'bid_strategy': {
'entry_pricing': {
'type': 'object',
'properties': {
'ask_last_balance': {
'price_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
'exclusiveMaximum': False,
},
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'bid'},
'price_side': {'type': 'string', 'enum': PRICING_SIDES, 'default': 'same'},
'use_order_book': {'type': 'boolean'},
'order_book_top': {'type': 'integer', 'minimum': 1, 'maximum': 50, },
'check_depth_of_market': {
@ -186,11 +193,11 @@ CONF_SCHEMA = {
},
'required': ['price_side']
},
'ask_strategy': {
'exit_pricing': {
'type': 'object',
'properties': {
'price_side': {'type': 'string', 'enum': ORDERBOOK_SIDES, 'default': 'ask'},
'bid_last_balance': {
'price_side': {'type': 'string', 'enum': PRICING_SIDES, 'default': 'same'},
'price_last_balance': {
'type': 'number',
'minimum': 0,
'maximum': 1,
@ -207,11 +214,11 @@ CONF_SCHEMA = {
'order_types': {
'type': 'object',
'properties': {
'buy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcesell': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forcebuy': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencysell': {
'entry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'exit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forceexit': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'forceentry': {'type': 'string', 'enum': ORDERTYPE_POSSIBILITIES},
'emergencyexit': {
'type': 'string',
'enum': ORDERTYPE_POSSIBILITIES,
'default': 'market'},
@ -221,15 +228,15 @@ CONF_SCHEMA = {
'stoploss_on_exchange_limit_ratio': {'type': 'number', 'minimum': 0.0,
'maximum': 1.0}
},
'required': ['buy', 'sell', 'stoploss', 'stoploss_on_exchange']
'required': ['entry', 'exit', 'stoploss', 'stoploss_on_exchange']
},
'order_time_in_force': {
'type': 'object',
'properties': {
'buy': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
'sell': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
'entry': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES},
'exit': {'type': 'string', 'enum': ORDERTIF_POSSIBILITIES}
},
'required': ['buy', 'sell']
'required': REQUIRED_ORDERTIF
},
'exchange': {'$ref': '#/definitions/exchange'},
'edge': {'$ref': '#/definitions/edge'},
@ -438,8 +445,8 @@ SCHEMA_TRADE_REQUIRED = [
'last_stake_amount_min_ratio',
'dry_run',
'dry_run_wallet',
'ask_strategy',
'bid_strategy',
'exit_pricing',
'entry_pricing',
'stoploss',
'minimal_roi',
'internals',
@ -475,7 +482,7 @@ CANCEL_REASON = {
}
# List of pairs with their timeframes
PairWithTimeframe = Tuple[str, str]
PairWithTimeframe = Tuple[str, str, CandleType]
ListPairsWithTimeframes = List[PairWithTimeframe]
# Type for trades list

View File

@ -24,7 +24,9 @@ BT_DATA_COLUMNS = ['pair', 'stake_amount', 'amount', 'open_date', 'close_date',
'fee_open', 'fee_close', 'trade_duration',
'profit_ratio', 'profit_abs', 'sell_reason',
'initial_stop_loss_abs', 'initial_stop_loss_ratio', 'stop_loss_abs',
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'buy_tag']
'stop_loss_ratio', 'min_rate', 'max_rate', 'is_open', 'enter_tag',
'is_short'
]
def get_latest_optimize_filename(directory: Union[Path, str], variant: str) -> str:
@ -250,6 +252,13 @@ def load_backtest_data(filename: Union[Path, str], strategy: Optional[str] = Non
utc=True,
infer_datetime_format=True
)
# Compatibility support for pre short Columns
if 'is_short' not in df.columns:
df['is_short'] = 0
if 'enter_tag' not in df.columns:
df['enter_tag'] = df['buy_tag']
df = df.drop(['buy_tag'], axis=1)
else:
# old format - only with lists.
raise OperationalException(

View File

@ -11,6 +11,7 @@ import pandas as pd
from pandas import DataFrame, to_datetime
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS, TradeList
from freqtrade.enums import CandleType
logger = logging.getLogger(__name__)
@ -261,13 +262,20 @@ def convert_trades_format(config: Dict[str, Any], convert_from: str, convert_to:
src.trades_purge(pair=pair)
def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to: str, erase: bool):
def convert_ohlcv_format(
config: Dict[str, Any],
convert_from: str,
convert_to: str,
erase: bool,
candle_type: CandleType
):
"""
Convert OHLCV from one format to another
:param config: Config dictionary
:param convert_from: Source format
:param convert_to: Target format
:param erase: Erase source data (does not apply if source and target format are identical)
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
from freqtrade.data.history.idatahandler import get_datahandler
src = get_datahandler(config['datadir'], convert_from)
@ -279,8 +287,11 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
config['pairs'] = []
# Check timeframes or fall back to timeframe.
for timeframe in timeframes:
config['pairs'].extend(src.ohlcv_get_pairs(config['datadir'],
timeframe))
config['pairs'].extend(src.ohlcv_get_pairs(
config['datadir'],
timeframe,
candle_type=candle_type
))
logger.info(f"Converting candle (OHLCV) data for {config['pairs']}")
for timeframe in timeframes:
@ -289,10 +300,16 @@ def convert_ohlcv_format(config: Dict[str, Any], convert_from: str, convert_to:
timerange=None,
fill_missing=False,
drop_incomplete=False,
startup_candles=0)
logger.info(f"Converting {len(data)} candles for {pair}")
startup_candles=0,
candle_type=candle_type)
logger.info(f"Converting {len(data)} {candle_type} candles for {pair}")
if len(data) > 0:
trg.ohlcv_store(pair=pair, timeframe=timeframe, data=data)
trg.ohlcv_store(
pair=pair,
timeframe=timeframe,
data=data,
candle_type=candle_type
)
if erase and convert_from != convert_to:
logger.info(f"Deleting source data for {pair} / {timeframe}")
src.ohlcv_purge(pair=pair, timeframe=timeframe)
src.ohlcv_purge(pair=pair, timeframe=timeframe, candle_type=candle_type)

View File

@ -13,7 +13,7 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes, PairWithTimeframe
from freqtrade.data.history import load_pair_history
from freqtrade.enums import RunMode
from freqtrade.enums import CandleType, RunMode
from freqtrade.exceptions import ExchangeError, OperationalException
from freqtrade.exchange import Exchange, timeframe_to_seconds
@ -41,7 +41,13 @@ class DataProvider:
"""
self.__slice_index = limit_index
def _set_cached_df(self, pair: str, timeframe: str, dataframe: DataFrame) -> None:
def _set_cached_df(
self,
pair: str,
timeframe: str,
dataframe: DataFrame,
candle_type: CandleType
) -> None:
"""
Store cached Dataframe.
Using private method as this should never be used by a user
@ -49,8 +55,10 @@ class DataProvider:
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param dataframe: analyzed dataframe
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
self.__cached_pairs[(pair, timeframe)] = (dataframe, datetime.now(timezone.utc))
self.__cached_pairs[(pair, timeframe, candle_type)] = (
dataframe, datetime.now(timezone.utc))
def add_pairlisthandler(self, pairlists) -> None:
"""
@ -58,13 +66,21 @@ class DataProvider:
"""
self._pairlists = pairlists
def historic_ohlcv(self, pair: str, timeframe: str = None) -> DataFrame:
def historic_ohlcv(
self,
pair: str,
timeframe: str = None,
candle_type: str = ''
) -> DataFrame:
"""
Get stored historical candle (OHLCV) data
:param pair: pair to get the data for
:param timeframe: timeframe to get data for
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
saved_pair = (pair, str(timeframe))
_candle_type = CandleType.from_string(
candle_type) if candle_type != '' else self._config['candle_type_def']
saved_pair = (pair, str(timeframe), _candle_type)
if saved_pair not in self.__cached_pairs_backtesting:
timerange = TimeRange.parse_timerange(None if self._config.get(
'timerange') is None else str(self._config.get('timerange')))
@ -77,26 +93,36 @@ class DataProvider:
timeframe=timeframe or self._config['timeframe'],
datadir=self._config['datadir'],
timerange=timerange,
data_format=self._config.get('dataformat_ohlcv', 'json')
data_format=self._config.get('dataformat_ohlcv', 'json'),
candle_type=_candle_type,
)
return self.__cached_pairs_backtesting[saved_pair].copy()
def get_pair_dataframe(self, pair: str, timeframe: str = None) -> DataFrame:
def get_pair_dataframe(
self,
pair: str,
timeframe: str = None,
candle_type: str = ''
) -> DataFrame:
"""
Return pair candle (OHLCV) data, either live or cached historical -- depending
on the runmode.
Only combinations in the pairlist or which have been specified as informative pairs
will be available.
:param pair: pair to get the data for
:param timeframe: timeframe to get data for
:return: Dataframe for this pair
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
# Get live OHLCV data.
data = self.ohlcv(pair=pair, timeframe=timeframe)
data = self.ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
else:
# Get historical OHLCV data (cached on disk).
data = self.historic_ohlcv(pair=pair, timeframe=timeframe)
data = self.historic_ohlcv(pair=pair, timeframe=timeframe, candle_type=candle_type)
if len(data) == 0:
logger.warning(f"No data found for ({pair}, {timeframe}).")
logger.warning(f"No data found for ({pair}, {timeframe}, {candle_type}).")
return data
def get_analyzed_dataframe(self, pair: str, timeframe: str) -> Tuple[DataFrame, datetime]:
@ -109,7 +135,7 @@ class DataProvider:
combination.
Returns empty dataframe and Epoch 0 (1970-01-01) if no dataframe was cached.
"""
pair_key = (pair, timeframe)
pair_key = (pair, timeframe, self._config.get('candle_type_def', CandleType.SPOT))
if pair_key in self.__cached_pairs:
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
df, date = self.__cached_pairs[pair_key]
@ -177,20 +203,31 @@ class DataProvider:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
return list(self._exchange._klines.keys())
def ohlcv(self, pair: str, timeframe: str = None, copy: bool = True) -> DataFrame:
def ohlcv(
self,
pair: str,
timeframe: str = None,
copy: bool = True,
candle_type: str = ''
) -> DataFrame:
"""
Get candle (OHLCV) data for the given pair as DataFrame
Please use the `available_pairs` method to verify which pairs are currently cached.
:param pair: pair to get the data for
:param timeframe: Timeframe to get data for
:param candle_type: '', mark, index, premiumIndex, or funding_rate
:param copy: copy dataframe before returning if True.
Use False only for read-only operations (where the dataframe is not modified)
"""
if self._exchange is None:
raise OperationalException(NO_EXCHANGE_EXCEPTION)
if self.runmode in (RunMode.DRY_RUN, RunMode.LIVE):
return self._exchange.klines((pair, timeframe or self._config['timeframe']),
copy=copy)
_candle_type = CandleType.from_string(
candle_type) if candle_type != '' else self._config['candle_type_def']
return self._exchange.klines(
(pair, timeframe or self._config['timeframe'], _candle_type),
copy=copy
)
else:
return DataFrame()

View File

@ -9,6 +9,7 @@ import pandas as pd
from freqtrade.configuration import TimeRange
from freqtrade.constants import (DEFAULT_DATAFRAME_COLUMNS, DEFAULT_TRADES_COLUMNS,
ListPairsWithTimeframes, TradeList)
from freqtrade.enums import CandleType, TradingMode
from .idatahandler import IDataHandler
@ -21,44 +22,63 @@ class HDF5DataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.h5)', p.name)
for p in datadir.glob("*.h5")]
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
if match and len(match.groups()) > 1]
if trading_mode == TradingMode.FUTURES:
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob("*.h5")
]
return [
(
cls.rebuild_pair_from_filename(match[1]),
match[2],
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
"""
Returns a list of all pairs with ohlcv data available in this datadir
for the specified timeframe
:param datadir: Directory to search for ohlcv files
:param timeframe: Timeframe to search pairs for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: List of Pairs
"""
candle = ""
if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures')
candle = f"-{candle_type}"
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.h5)', p.name)
for p in datadir.glob(f"*{timeframe}.h5")]
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + '.h5)', p.name)
for p in datadir.glob(f"*{timeframe}{candle}.h5")]
# Check if regex found something and only return these results
return [match[0].replace('_', '/') for match in _tmp if match]
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
def ohlcv_store(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
def ohlcv_store(
self, pair: str, timeframe: str, data: pd.DataFrame, candle_type: CandleType) -> None:
"""
Store data in hdf5 file.
:param pair: Pair - used to generate filename
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: None
"""
key = self._pair_ohlcv_key(pair, timeframe)
_data = data.copy()
filename = self._pair_data_filename(self._datadir, pair, timeframe)
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
self.create_dir_if_needed(filename)
_data.loc[:, self._columns].to_hdf(
filename, key, mode='a', complevel=9, complib='blosc',
@ -66,7 +86,8 @@ class HDF5DataHandler(IDataHandler):
)
def _ohlcv_load(self, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None) -> pd.DataFrame:
timerange: Optional[TimeRange], candle_type: CandleType
) -> pd.DataFrame:
"""
Internal method used to load data for one pair from disk.
Implements the loading and conversion to a Pandas dataframe.
@ -76,10 +97,16 @@ class HDF5DataHandler(IDataHandler):
:param timerange: Limit data to be loaded to this timerange.
Optionally implemented by subclasses to avoid loading
all data where possible.
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame
"""
key = self._pair_ohlcv_key(pair, timeframe)
filename = self._pair_data_filename(self._datadir, pair, timeframe)
filename = self._pair_data_filename(
self._datadir,
pair,
timeframe,
candle_type=candle_type
)
if not filename.exists():
return pd.DataFrame(columns=self._columns)
@ -98,12 +125,19 @@ class HDF5DataHandler(IDataHandler):
'low': 'float', 'close': 'float', 'volume': 'float'})
return pairdata
def ohlcv_append(self, pair: str, timeframe: str, data: pd.DataFrame) -> None:
def ohlcv_append(
self,
pair: str,
timeframe: str,
data: pd.DataFrame,
candle_type: CandleType
) -> None:
"""
Append data to existing data structures
:param pair: Pair
:param timeframe: Timeframe this ohlcv data is for
:param data: Data to append.
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
raise NotImplementedError()
@ -117,7 +151,7 @@ class HDF5DataHandler(IDataHandler):
_tmp = [re.search(r'^(\S+)(?=\-trades.h5)', p.name)
for p in datadir.glob("*trades.h5")]
# Check if regex found something and only return these results to avoid exceptions.
return [match[0].replace('_', '/') for match in _tmp if match]
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
def trades_store(self, pair: str, data: TradeList) -> None:
"""
@ -172,7 +206,9 @@ class HDF5DataHandler(IDataHandler):
@classmethod
def _pair_ohlcv_key(cls, pair: str, timeframe: str) -> str:
return f"{pair}/ohlcv/tf_{timeframe}"
# Escape futures pairs to avoid warnings
pair_esc = pair.replace(':', '_')
return f"{pair_esc}/ohlcv/tf_{timeframe}"
@classmethod
def _pair_trades_key(cls, pair: str) -> str:

View File

@ -12,6 +12,7 @@ from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS
from freqtrade.data.converter import (clean_ohlcv_dataframe, ohlcv_to_dataframe,
trades_remove_duplicates, trades_to_ohlcv)
from freqtrade.data.history.idatahandler import IDataHandler, get_datahandler
from freqtrade.enums import CandleType
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
from freqtrade.misc import format_ms_time
@ -29,6 +30,7 @@ def load_pair_history(pair: str,
startup_candles: int = 0,
data_format: str = None,
data_handler: IDataHandler = None,
candle_type: CandleType = CandleType.SPOT
) -> DataFrame:
"""
Load cached ohlcv history for the given pair.
@ -43,6 +45,7 @@ def load_pair_history(pair: str,
:param startup_candles: Additional candles to load at the start of the period
:param data_handler: Initialized data-handler to use.
Will be initialized from data_format if not set
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame
"""
data_handler = get_datahandler(datadir, data_format, data_handler)
@ -53,6 +56,7 @@ def load_pair_history(pair: str,
fill_missing=fill_up_missing,
drop_incomplete=drop_incomplete,
startup_candles=startup_candles,
candle_type=candle_type
)
@ -64,6 +68,7 @@ def load_data(datadir: Path,
startup_candles: int = 0,
fail_without_data: bool = False,
data_format: str = 'json',
candle_type: CandleType = CandleType.SPOT
) -> Dict[str, DataFrame]:
"""
Load ohlcv history data for a list of pairs.
@ -76,6 +81,7 @@ def load_data(datadir: Path,
:param startup_candles: Additional candles to load at the start of the period
:param fail_without_data: Raise OperationalException if no data is found.
:param data_format: Data format which should be used. Defaults to json
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: dict(<pair>:<Dataframe>)
"""
result: Dict[str, DataFrame] = {}
@ -89,7 +95,8 @@ def load_data(datadir: Path,
datadir=datadir, timerange=timerange,
fill_up_missing=fill_up_missing,
startup_candles=startup_candles,
data_handler=data_handler
data_handler=data_handler,
candle_type=candle_type
)
if not hist.empty:
result[pair] = hist
@ -99,12 +106,13 @@ def load_data(datadir: Path,
return result
def refresh_data(datadir: Path,
def refresh_data(*, datadir: Path,
timeframe: str,
pairs: List[str],
exchange: Exchange,
data_format: str = None,
timerange: Optional[TimeRange] = None,
candle_type: CandleType,
) -> None:
"""
Refresh ohlcv history data for a list of pairs.
@ -115,17 +123,24 @@ def refresh_data(datadir: Path,
:param exchange: Exchange object
:param data_format: dataformat to use
:param timerange: Limit data to be loaded to this timerange
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
data_handler = get_datahandler(datadir, data_format)
for idx, pair in enumerate(pairs):
process = f'{idx}/{len(pairs)}'
_download_pair_history(pair=pair, process=process,
timeframe=timeframe, datadir=datadir,
timerange=timerange, exchange=exchange, data_handler=data_handler)
timerange=timerange, exchange=exchange, data_handler=data_handler,
candle_type=candle_type)
def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optional[TimeRange],
data_handler: IDataHandler) -> Tuple[DataFrame, Optional[int]]:
def _load_cached_data_for_updating(
pair: str,
timeframe: str,
timerange: Optional[TimeRange],
data_handler: IDataHandler,
candle_type: CandleType
) -> Tuple[DataFrame, Optional[int]]:
"""
Load cached data to download more data.
If timerange is passed in, checks whether data from an before the stored data will be
@ -142,7 +157,8 @@ def _load_cached_data_for_updating(pair: str, timeframe: str, timerange: Optiona
# Intentionally don't pass timerange in - since we need to load the full dataset.
data = data_handler.ohlcv_load(pair, timeframe=timeframe,
timerange=None, fill_missing=False,
drop_incomplete=True, warn_no_data=False)
drop_incomplete=True, warn_no_data=False,
candle_type=candle_type)
if not data.empty:
if start and start < data.iloc[0]['date']:
# Earlier data than existing data requested, redownload all
@ -161,7 +177,9 @@ def _download_pair_history(pair: str, *,
process: str = '',
new_pairs_days: int = 30,
data_handler: IDataHandler = None,
timerange: Optional[TimeRange] = None) -> bool:
timerange: Optional[TimeRange] = None,
candle_type: CandleType,
) -> bool:
"""
Download latest candles from the exchange for the pair and timeframe passed in parameters
The data is downloaded starting from the last correct data that
@ -173,19 +191,20 @@ def _download_pair_history(pair: str, *,
:param pair: pair to download
:param timeframe: Timeframe (e.g "5m")
:param timerange: range of time to download
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: bool with success state
"""
data_handler = get_datahandler(datadir, data_handler=data_handler)
try:
logger.info(
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe} '
f'and store in {datadir}.'
f'Download history data for pair: "{pair}" ({process}), timeframe: {timeframe}, '
f'candle type: {candle_type} and store in {datadir}.'
)
# data, since_ms = _load_cached_data_for_updating_old(datadir, pair, timeframe, timerange)
data, since_ms = _load_cached_data_for_updating(pair, timeframe, timerange,
data_handler=data_handler)
data_handler=data_handler,
candle_type=candle_type)
logger.debug("Current Start: %s",
f"{data.iloc[0]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
@ -198,7 +217,8 @@ def _download_pair_history(pair: str, *,
since_ms=since_ms if since_ms else
arrow.utcnow().shift(
days=-new_pairs_days).int_timestamp * 1000,
is_new_pair=data.empty
is_new_pair=data.empty,
candle_type=candle_type,
)
# TODO: Maybe move parsing to exchange class (?)
new_dataframe = ohlcv_to_dataframe(new_data, timeframe, pair,
@ -216,7 +236,7 @@ def _download_pair_history(pair: str, *,
logger.debug("New End: %s",
f"{data.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}" if not data.empty else 'None')
data_handler.ohlcv_store(pair, timeframe, data=data)
data_handler.ohlcv_store(pair, timeframe, data=data, candle_type=candle_type)
return True
except Exception:
@ -227,9 +247,11 @@ def _download_pair_history(pair: str, *,
def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes: List[str],
datadir: Path, timerange: Optional[TimeRange] = None,
datadir: Path, trading_mode: str,
timerange: Optional[TimeRange] = None,
new_pairs_days: int = 30, erase: bool = False,
data_format: str = None) -> List[str]:
data_format: str = None,
) -> List[str]:
"""
Refresh stored ohlcv data for backtesting and hyperopt operations.
Used by freqtrade download-data subcommand.
@ -237,6 +259,7 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
"""
pairs_not_available = []
data_handler = get_datahandler(datadir, data_format)
candle_type = CandleType.get_default(trading_mode)
for idx, pair in enumerate(pairs, start=1):
if pair not in exchange.markets:
pairs_not_available.append(pair)
@ -245,16 +268,35 @@ def refresh_backtest_ohlcv_data(exchange: Exchange, pairs: List[str], timeframes
for timeframe in timeframes:
if erase:
if data_handler.ohlcv_purge(pair, timeframe):
logger.info(
f'Deleting existing data for pair {pair}, interval {timeframe}.')
if data_handler.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
logger.info(f'Downloading pair {pair}, interval {timeframe}.')
process = f'{idx}/{len(pairs)}'
_download_pair_history(pair=pair, process=process,
datadir=datadir, exchange=exchange,
timerange=timerange, data_handler=data_handler,
timeframe=str(timeframe), new_pairs_days=new_pairs_days)
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
candle_type=candle_type)
if trading_mode == 'futures':
# Predefined candletype (and timeframe) depending on exchange
# Downloads what is necessary to backtest based on futures data.
timeframe = exchange._ft_has['mark_ohlcv_timeframe']
fr_candle_type = CandleType.from_string(exchange._ft_has['mark_ohlcv_price'])
# All exchanges need FundingRate for futures trading.
# The timeframe is aligned to the mark-price timeframe.
for funding_candle_type in (CandleType.FUNDING_RATE, fr_candle_type):
# TODO: this could be in most parts to the above.
if erase:
if data_handler.ohlcv_purge(pair, timeframe, candle_type=funding_candle_type):
logger.info(
f'Deleting existing data for pair {pair}, interval {timeframe}.')
_download_pair_history(pair=pair, process=process,
datadir=datadir, exchange=exchange,
timerange=timerange, data_handler=data_handler,
timeframe=str(timeframe), new_pairs_days=new_pairs_days,
candle_type=funding_candle_type)
return pairs_not_available
@ -353,10 +395,16 @@ def refresh_backtest_trades_data(exchange: Exchange, pairs: List[str], datadir:
return pairs_not_available
def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
datadir: Path, timerange: TimeRange, erase: bool = False,
def convert_trades_to_ohlcv(
pairs: List[str],
timeframes: List[str],
datadir: Path,
timerange: TimeRange,
erase: bool = False,
data_format_ohlcv: str = 'json',
data_format_trades: str = 'jsongz') -> None:
data_format_trades: str = 'jsongz',
candle_type: CandleType = CandleType.SPOT
) -> None:
"""
Convert stored trades data to ohlcv data
"""
@ -367,12 +415,12 @@ def convert_trades_to_ohlcv(pairs: List[str], timeframes: List[str],
trades = data_handler_trades.trades_load(pair)
for timeframe in timeframes:
if erase:
if data_handler_ohlcv.ohlcv_purge(pair, timeframe):
if data_handler_ohlcv.ohlcv_purge(pair, timeframe, candle_type=candle_type):
logger.info(f'Deleting existing data for pair {pair}, interval {timeframe}.')
try:
ohlcv = trades_to_ohlcv(trades, timeframe)
# Store ohlcv
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv)
data_handler_ohlcv.ohlcv_store(pair, timeframe, data=ohlcv, candle_type=candle_type)
except ValueError:
logger.exception(f'Could not convert {pair} to OHLCV.')

View File

@ -4,6 +4,7 @@ It's subclasses handle and storing data from disk.
"""
import logging
import re
from abc import ABC, abstractclassmethod, abstractmethod
from copy import deepcopy
from datetime import datetime, timezone
@ -16,6 +17,7 @@ from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import ListPairsWithTimeframes, TradeList
from freqtrade.data.converter import clean_ohlcv_dataframe, trades_remove_duplicates, trim_dataframe
from freqtrade.enums import CandleType, TradingMode
from freqtrade.exchange import timeframe_to_seconds
@ -24,6 +26,8 @@ logger = logging.getLogger(__name__)
class IDataHandler(ABC):
_OHLCV_REGEX = r'^([a-zA-Z_-]+)\-(\d+\S)\-?([a-zA-Z_]*)?(?=\.)'
def __init__(self, datadir: Path) -> None:
self._datadir = datadir
@ -35,36 +39,41 @@ class IDataHandler(ABC):
raise NotImplementedError()
@abstractclassmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
@abstractclassmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
"""
Returns a list of all pairs with ohlcv data available in this datadir
for the specified timeframe
:param datadir: Directory to search for ohlcv files
:param timeframe: Timeframe to search pairs for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: List of Pairs
"""
@abstractmethod
def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None:
def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
"""
Store ohlcv data.
:param pair: Pair - used to generate filename
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: None
"""
@abstractmethod
def _ohlcv_load(self, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None,
def _ohlcv_load(self, pair: str, timeframe: str, timerange: Optional[TimeRange],
candle_type: CandleType
) -> DataFrame:
"""
Internal method used to load data for one pair from disk.
@ -75,29 +84,38 @@ class IDataHandler(ABC):
:param timerange: Limit data to be loaded to this timerange.
Optionally implemented by subclasses to avoid loading
all data where possible.
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame
"""
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
def ohlcv_purge(self, pair: str, timeframe: str, candle_type: CandleType) -> bool:
"""
Remove data for this pair
:param pair: Delete data for this pair.
:param timeframe: Timeframe (e.g. "5m")
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: True when deleted, false if file did not exist.
"""
filename = self._pair_data_filename(self._datadir, pair, timeframe)
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
if filename.exists():
filename.unlink()
return True
return False
@abstractmethod
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
def ohlcv_append(
self,
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None:
"""
Append data to existing data structures
:param pair: Pair
:param timeframe: Timeframe this ohlcv data is for
:param data: Data to append.
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
@abstractclassmethod
@ -158,9 +176,29 @@ class IDataHandler(ABC):
return trades_remove_duplicates(self._trades_load(pair, timerange=timerange))
@classmethod
def _pair_data_filename(cls, datadir: Path, pair: str, timeframe: str) -> Path:
def create_dir_if_needed(cls, datadir: Path):
"""
Creates datadir if necessary
should only create directories for "futures" mode at the moment.
"""
if not datadir.parent.is_dir():
datadir.parent.mkdir()
@classmethod
def _pair_data_filename(
cls,
datadir: Path,
pair: str,
timeframe: str,
candle_type: CandleType
) -> Path:
pair_s = misc.pair_to_filename(pair)
filename = datadir.joinpath(f'{pair_s}-{timeframe}.{cls._get_file_extension()}')
candle = ""
if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures')
candle = f"-{candle_type}"
filename = datadir.joinpath(
f'{pair_s}-{timeframe}{candle}.{cls._get_file_extension()}')
return filename
@classmethod
@ -169,12 +207,23 @@ class IDataHandler(ABC):
filename = datadir.joinpath(f'{pair_s}-trades.{cls._get_file_extension()}')
return filename
@staticmethod
def rebuild_pair_from_filename(pair: str) -> str:
"""
Rebuild pair name from filename
Assumes a asset name of max. 7 length to also support BTC-PERP and BTC-PERP:USD names.
"""
res = re.sub(r'^(([A-Za-z]{1,10})|^([A-Za-z\-]{1,6}))(_)', r'\g<1>/', pair, 1)
res = re.sub('_', ':', res, 1)
return res
def ohlcv_load(self, pair, timeframe: str,
candle_type: CandleType,
timerange: Optional[TimeRange] = None,
fill_missing: bool = True,
drop_incomplete: bool = True,
startup_candles: int = 0,
warn_no_data: bool = True
warn_no_data: bool = True,
) -> DataFrame:
"""
Load cached candle (OHLCV) data for the given pair.
@ -186,6 +235,7 @@ class IDataHandler(ABC):
:param drop_incomplete: Drop last candle assuming it may be incomplete.
:param startup_candles: Additional candles to load at the start of the period
:param warn_no_data: Log a warning message when no data is found
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame
"""
# Fix startup period
@ -193,17 +243,21 @@ class IDataHandler(ABC):
if startup_candles > 0 and timerange_startup:
timerange_startup.subtract_start(timeframe_to_seconds(timeframe) * startup_candles)
pairdf = self._ohlcv_load(pair, timeframe,
timerange=timerange_startup)
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
pairdf = self._ohlcv_load(
pair,
timeframe,
timerange=timerange_startup,
candle_type=candle_type
)
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf
else:
enddate = pairdf.iloc[-1]['date']
if timerange_startup:
self._validate_pairdata(pair, pairdf, timeframe, timerange_startup)
self._validate_pairdata(pair, pairdf, timeframe, candle_type, timerange_startup)
pairdf = trim_dataframe(pairdf, timerange_startup)
if self._check_empty_df(pairdf, pair, timeframe, warn_no_data):
if self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data):
return pairdf
# incomplete candles should only be dropped if we didn't trim the end beforehand.
@ -212,23 +266,25 @@ class IDataHandler(ABC):
fill_missing=fill_missing,
drop_incomplete=(drop_incomplete and
enddate == pairdf.iloc[-1]['date']))
self._check_empty_df(pairdf, pair, timeframe, warn_no_data)
self._check_empty_df(pairdf, pair, timeframe, candle_type, warn_no_data)
return pairdf
def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str, warn_no_data: bool):
def _check_empty_df(self, pairdf: DataFrame, pair: str, timeframe: str,
candle_type: CandleType, warn_no_data: bool):
"""
Warn on empty dataframe
"""
if pairdf.empty:
if warn_no_data:
logger.warning(
f'No history data for pair: "{pair}", timeframe: {timeframe}. '
'Use `freqtrade download-data` to download the data'
f"No history for {pair}, {candle_type}, {timeframe} found. "
"Use `freqtrade download-data` to download the data"
)
return True
return False
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str, timerange: TimeRange):
def _validate_pairdata(self, pair, pairdata: DataFrame, timeframe: str,
candle_type: CandleType, timerange: TimeRange):
"""
Validates pairdata for missing data at start end end and logs warnings.
:param pairdata: Dataframe to validate
@ -238,12 +294,12 @@ class IDataHandler(ABC):
if timerange.starttype == 'date':
start = datetime.fromtimestamp(timerange.startts, tz=timezone.utc)
if pairdata.iloc[0]['date'] > start:
logger.warning(f"Missing data at start for pair {pair} at {timeframe}, "
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data starts at {pairdata.iloc[0]['date']:%Y-%m-%d %H:%M:%S}")
if timerange.stoptype == 'date':
stop = datetime.fromtimestamp(timerange.stopts, tz=timezone.utc)
if pairdata.iloc[-1]['date'] < stop:
logger.warning(f"Missing data at end for pair {pair} at {timeframe}, "
logger.warning(f"{pair}, {candle_type}, {timeframe}, "
f"data ends at {pairdata.iloc[-1]['date']:%Y-%m-%d %H:%M:%S}")

View File

@ -10,6 +10,7 @@ from freqtrade import misc
from freqtrade.configuration import TimeRange
from freqtrade.constants import DEFAULT_DATAFRAME_COLUMNS, ListPairsWithTimeframes, TradeList
from freqtrade.data.converter import trades_dict_to_list
from freqtrade.enums import CandleType, TradingMode
from .idatahandler import IDataHandler
@ -23,33 +24,49 @@ class JsonDataHandler(IDataHandler):
_columns = DEFAULT_DATAFRAME_COLUMNS
@classmethod
def ohlcv_get_available_data(cls, datadir: Path) -> ListPairsWithTimeframes:
def ohlcv_get_available_data(
cls, datadir: Path, trading_mode: TradingMode) -> ListPairsWithTimeframes:
"""
Returns a list of all pairs with ohlcv data available in this datadir
:param datadir: Directory to search for ohlcv files
:param trading_mode: trading-mode to be used
:return: List of Tuples of (pair, timeframe)
"""
_tmp = [re.search(r'^([a-zA-Z_]+)\-(\d+\S+)(?=.json)', p.name)
for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [(match[1].replace('_', '/'), match[2]) for match in _tmp
if match and len(match.groups()) > 1]
if trading_mode == 'futures':
datadir = datadir.joinpath('futures')
_tmp = [
re.search(
cls._OHLCV_REGEX, p.name
) for p in datadir.glob(f"*.{cls._get_file_extension()}")]
return [
(
cls.rebuild_pair_from_filename(match[1]),
match[2],
CandleType.from_string(match[3])
) for match in _tmp if match and len(match.groups()) > 1]
@classmethod
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str) -> List[str]:
def ohlcv_get_pairs(cls, datadir: Path, timeframe: str, candle_type: CandleType) -> List[str]:
"""
Returns a list of all pairs with ohlcv data available in this datadir
for the specified timeframe
:param datadir: Directory to search for ohlcv files
:param timeframe: Timeframe to search pairs for
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: List of Pairs
"""
candle = ""
if candle_type != CandleType.SPOT:
datadir = datadir.joinpath('futures')
candle = f"-{candle_type}"
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + '.json)', p.name)
for p in datadir.glob(f"*{timeframe}.{cls._get_file_extension()}")]
_tmp = [re.search(r'^(\S+)(?=\-' + timeframe + candle + '.json)', p.name)
for p in datadir.glob(f"*{timeframe}{candle}.{cls._get_file_extension()}")]
# Check if regex found something and only return these results
return [match[0].replace('_', '/') for match in _tmp if match]
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
def ohlcv_store(self, pair: str, timeframe: str, data: DataFrame) -> None:
def ohlcv_store(
self, pair: str, timeframe: str, data: DataFrame, candle_type: CandleType) -> None:
"""
Store data in json format "values".
format looks as follows:
@ -57,9 +74,11 @@ class JsonDataHandler(IDataHandler):
:param pair: Pair - used to generate filename
:param timeframe: Timeframe - used to generate filename
:param data: Dataframe containing OHLCV data
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: None
"""
filename = self._pair_data_filename(self._datadir, pair, timeframe)
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type)
self.create_dir_if_needed(filename)
_data = data.copy()
# Convert date to int
_data['date'] = _data['date'].view(np.int64) // 1000 // 1000
@ -70,7 +89,7 @@ class JsonDataHandler(IDataHandler):
compression='gzip' if self._use_zip else None)
def _ohlcv_load(self, pair: str, timeframe: str,
timerange: Optional[TimeRange] = None,
timerange: Optional[TimeRange], candle_type: CandleType
) -> DataFrame:
"""
Internal method used to load data for one pair from disk.
@ -81,9 +100,10 @@ class JsonDataHandler(IDataHandler):
:param timerange: Limit data to be loaded to this timerange.
Optionally implemented by subclasses to avoid loading
all data where possible.
:param candle_type: Any of the enum CandleType (must match trading mode!)
:return: DataFrame with ohlcv data, or empty DataFrame
"""
filename = self._pair_data_filename(self._datadir, pair, timeframe)
filename = self._pair_data_filename(self._datadir, pair, timeframe, candle_type=candle_type)
if not filename.exists():
return DataFrame(columns=self._columns)
try:
@ -100,25 +120,19 @@ class JsonDataHandler(IDataHandler):
infer_datetime_format=True)
return pairdata
def ohlcv_purge(self, pair: str, timeframe: str) -> bool:
"""
Remove data for this pair
:param pair: Delete data for this pair.
:param timeframe: Timeframe (e.g. "5m")
:return: True when deleted, false if file did not exist.
"""
filename = self._pair_data_filename(self._datadir, pair, timeframe)
if filename.exists():
filename.unlink()
return True
return False
def ohlcv_append(self, pair: str, timeframe: str, data: DataFrame) -> None:
def ohlcv_append(
self,
pair: str,
timeframe: str,
data: DataFrame,
candle_type: CandleType
) -> None:
"""
Append data to existing data structures
:param pair: Pair
:param timeframe: Timeframe this ohlcv data is for
:param data: Data to append.
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
raise NotImplementedError()
@ -132,7 +146,7 @@ class JsonDataHandler(IDataHandler):
_tmp = [re.search(r'^(\S+)(?=\-trades.json)', p.name)
for p in datadir.glob(f"*trades.{cls._get_file_extension()}")]
# Check if regex found something and only return these results to avoid exceptions.
return [match[0].replace('_', '/') for match in _tmp if match]
return [cls.rebuild_pair_from_filename(match[0]) for match in _tmp if match]
def trades_store(self, pair: str, data: TradeList) -> None:
"""

View File

@ -13,7 +13,7 @@ from pandas import DataFrame
from freqtrade.configuration import TimeRange
from freqtrade.constants import DATETIME_PRINT_FORMAT, UNLIMITED_STAKE_AMOUNT
from freqtrade.data.history import get_timerange, load_data, refresh_data
from freqtrade.enums import RunMode, SellType
from freqtrade.enums import CandleType, ExitType, RunMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange.exchange import timeframe_to_seconds
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
@ -116,11 +116,12 @@ class Edge:
timeframe=self.strategy.timeframe,
timerange=timerange_startup,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
)
# Download informative pairs too
res = defaultdict(list)
for p, t in self.strategy.gather_informative_pairs():
res[t].append(p)
for pair, timeframe, _ in self.strategy.gather_informative_pairs():
res[timeframe].append(pair)
for timeframe, inf_pairs in res.items():
timerange_startup = deepcopy(self._timerange)
timerange_startup.subtract_start(timeframe_to_seconds(
@ -132,6 +133,7 @@ class Edge:
timeframe=timeframe,
timerange=timerange_startup,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
)
data = load_data(
@ -141,6 +143,7 @@ class Edge:
timerange=self._timerange,
startup_candles=self.strategy.startup_candle_count,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=self.config.get('candle_type_def', CandleType.SPOT),
)
if not data:
@ -159,7 +162,9 @@ class Edge:
logger.info(f'Measuring data from {min_date.strftime(DATETIME_PRINT_FORMAT)} '
f'up to {max_date.strftime(DATETIME_PRINT_FORMAT)} '
f'({(max_date - min_date).days} days)..')
headers = ['date', 'buy', 'open', 'close', 'sell', 'high', 'low']
# TODO: Should edge support shorts? needs to be investigated further
# * (add enter_short exit_short)
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long']
trades: list = []
for pair, pair_data in preprocessed.items():
@ -167,8 +172,13 @@ class Edge:
pair_data = pair_data.sort_values(by=['date'])
pair_data = pair_data.reset_index(drop=True)
df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair})[headers].copy()
df_analyzed = self.strategy.advise_exit(
dataframe=self.strategy.advise_entry(
dataframe=pair_data,
metadata={'pair': pair}
),
metadata={'pair': pair}
)[headers].copy()
trades += self._find_trades_for_stoploss_range(df_analyzed, pair, self._stoploss_range)
@ -384,8 +394,8 @@ class Edge:
return final
def _find_trades_for_stoploss_range(self, df, pair, stoploss_range):
buy_column = df['buy'].values
sell_column = df['sell'].values
buy_column = df['enter_long'].values
sell_column = df['exit_long'].values
date_column = df['date'].values
ohlc_columns = df[['open', 'high', 'low', 'close']].values
@ -450,7 +460,7 @@ class Edge:
if stop_index <= sell_index:
exit_index = open_trade_index + stop_index
exit_type = SellType.STOP_LOSS
exit_type = ExitType.STOP_LOSS
exit_price = stop_price
elif stop_index > sell_index:
# If exit is SELL then we exit at the next candle
@ -460,7 +470,7 @@ class Edge:
if len(ohlc_columns) - 1 < exit_index:
break
exit_type = SellType.SELL_SIGNAL
exit_type = ExitType.SELL_SIGNAL
exit_price = ohlc_columns[exit_index, 0]
trade = {'pair': pair,

View File

@ -1,8 +1,12 @@
# flake8: noqa: F401
from freqtrade.enums.backteststate import BacktestState
from freqtrade.enums.candletype import CandleType
from freqtrade.enums.exitchecktuple import ExitCheckTuple
from freqtrade.enums.exittype import ExitType
from freqtrade.enums.marginmode import MarginMode
from freqtrade.enums.ordertypevalue import OrderTypeValues
from freqtrade.enums.rpcmessagetype import RPCMessageType
from freqtrade.enums.runmode import NON_UTIL_MODES, OPTIMIZE_MODES, TRADING_MODES, RunMode
from freqtrade.enums.selltype import SellType
from freqtrade.enums.signaltype import SignalTagType, SignalType
from freqtrade.enums.signaltype import SignalDirection, SignalTagType, SignalType
from freqtrade.enums.state import State
from freqtrade.enums.tradingmode import TradingMode

View File

@ -0,0 +1,27 @@
from enum import Enum
class CandleType(str, Enum):
"""Enum to distinguish candle types"""
SPOT = "spot"
FUTURES = "futures"
MARK = "mark"
INDEX = "index"
PREMIUMINDEX = "premiumIndex"
# TODO: Could take up less memory if these weren't a CandleType
FUNDING_RATE = "funding_rate"
# BORROW_RATE = "borrow_rate" # * unimplemented
@staticmethod
def from_string(value: str) -> 'CandleType':
if not value:
# Default to spot
return CandleType.SPOT
return CandleType(value)
@staticmethod
def get_default(trading_mode: str) -> 'CandleType':
if trading_mode == 'futures':
return CandleType.FUTURES
return CandleType.SPOT

View File

@ -0,0 +1,17 @@
from freqtrade.enums.exittype import ExitType
class ExitCheckTuple:
"""
NamedTuple for Exit type + reason
"""
exit_type: ExitType
exit_reason: str = ''
def __init__(self, exit_type: ExitType, exit_reason: str = ''):
self.exit_type = exit_type
self.exit_reason = exit_reason or exit_type.value
@property
def exit_flag(self):
return self.exit_type != ExitType.NONE

View File

@ -1,7 +1,7 @@
from enum import Enum
class SellType(Enum):
class ExitType(Enum):
"""
Enum to distinguish between sell reasons
"""

View File

@ -0,0 +1,12 @@
from enum import Enum
class MarginMode(Enum):
"""
Enum to distinguish between
cross margin/futures margin_mode and
isolated margin/futures margin_mode
"""
CROSS = "cross"
ISOLATED = "isolated"
NONE = ''

View File

@ -5,12 +5,21 @@ class RPCMessageType(Enum):
STATUS = 'status'
WARNING = 'warning'
STARTUP = 'startup'
BUY = 'buy'
BUY_FILL = 'buy_fill'
BUY_CANCEL = 'buy_cancel'
SHORT = 'short'
SHORT_FILL = 'short_fill'
SHORT_CANCEL = 'short_cancel'
# TODO: The below messagetypes should be renamed to "exit"!
# Careful - has an impact on webhooks, therefore needs proper communication
SELL = 'sell'
SELL_FILL = 'sell_fill'
SELL_CANCEL = 'sell_cancel'
PROTECTION_TRIGGER = 'protection_trigger'
PROTECTION_TRIGGER_GLOBAL = 'protection_trigger_global'

View File

@ -3,15 +3,22 @@ from enum import Enum
class SignalType(Enum):
"""
Enum to distinguish between buy and sell signals
Enum to distinguish between enter and exit signals
"""
BUY = "buy"
SELL = "sell"
ENTER_LONG = "enter_long"
EXIT_LONG = "exit_long"
ENTER_SHORT = "enter_short"
EXIT_SHORT = "exit_short"
class SignalTagType(Enum):
"""
Enum for signal columns
"""
BUY_TAG = "buy_tag"
ENTER_TAG = "enter_tag"
EXIT_TAG = "exit_tag"
class SignalDirection(str, Enum):
LONG = 'long'
SHORT = 'short'

View File

@ -0,0 +1,11 @@
from enum import Enum
class TradingMode(str, Enum):
"""
Enum to distinguish between
spot, margin, futures or any other trading method
"""
SPOT = "spot"
MARGIN = "margin"
FUTURES = "futures"

View File

@ -20,4 +20,9 @@ class Bibox(Exchange):
# fetchCurrencies API point requires authentication for Bibox,
# so switch it off for Freqtrade load_markets()
_ccxt_config: Dict = {"has": {"fetchCurrencies": False}}
@property
def _ccxt_config(self) -> Dict:
# Parameters to add directly to ccxt sync/async initialization.
config = {"has": {"fetchCurrencies": False}}
config.update(super()._ccxt_config)
return config

View File

@ -1,10 +1,18 @@
""" Binance exchange subclass """
import json
import logging
from typing import Dict, List, Tuple
from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
import arrow
import ccxt
from freqtrade.enums import CandleType, MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
from freqtrade.misc import deep_merge_dicts
logger = logging.getLogger(__name__)
@ -21,30 +29,179 @@ class Binance(Exchange):
"trades_pagination": "id",
"trades_pagination_arg": "fromId",
"l2_limit_range": [5, 10, 20, 50, 100, 500, 1000],
"ccxt_futures_name": "future"
}
_ft_has_futures: Dict = {
"stoploss_order_types": {"limit": "stop"},
"tickers_have_price": False,
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
:param side: "buy" or "sell"
"""
return order['type'] == 'stop_loss_limit' and stop_loss > float(order['info']['stopPrice'])
ordertype = 'stop' if self.trading_mode == TradingMode.FUTURES else 'stop_loss_limit'
return order['type'] == ordertype and (
(side == "sell" and stop_loss > float(order['info']['stopPrice'])) or
(side == "buy" and stop_loss < float(order['info']['stopPrice']))
)
def get_tickers(self, symbols: List[str] = None, cached: bool = False) -> Dict:
tickers = super().get_tickers(symbols=symbols, cached=cached)
if self.trading_mode == TradingMode.FUTURES:
# Binance's future result has no bid/ask values.
# Therefore we must fetch that from fetch_bids_asks and combine the two results.
bidsasks = self.fetch_bids_asks(symbols, cached)
tickers = deep_merge_dicts(bidsasks, tickers, allow_null_overrides=False)
return tickers
@retrier
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
):
"""
Set's the leverage before making a trade, in order to not
have the same leverage on every trade
"""
trading_mode = trading_mode or self.trading_mode
if self._config['dry_run'] or trading_mode != TradingMode.FUTURES:
return
try:
self._api.set_leverage(symbol=pair, leverage=round(leverage))
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
async def _async_get_historic_ohlcv(self, pair: str, timeframe: str,
since_ms: int, is_new_pair: bool = False,
raise_: bool = False
) -> Tuple[str, str, List]:
since_ms: int, candle_type: CandleType,
is_new_pair: bool = False, raise_: bool = False,
) -> Tuple[str, str, str, List]:
"""
Overwrite to introduce "fast new pair" functionality by detecting the pair's listing date
Does not work for other exchanges, which don't return the earliest data when called with "0"
:param candle_type: Any of the enum CandleType (must match trading mode!)
"""
if is_new_pair:
x = await self._async_get_candle_history(pair, timeframe, 0)
if x and x[2] and x[2][0] and x[2][0][0] > since_ms:
x = await self._async_get_candle_history(pair, timeframe, candle_type, 0)
if x and x[3] and x[3][0] and x[3][0][0] > since_ms:
# Set starting date to first available candle.
since_ms = x[2][0][0]
since_ms = x[3][0][0]
logger.info(f"Candle-data for {pair} available starting with "
f"{arrow.get(since_ms // 1000).isoformat()}.")
return await super()._async_get_historic_ohlcv(
pair=pair, timeframe=timeframe, since_ms=since_ms, is_new_pair=is_new_pair,
raise_=raise_)
pair=pair,
timeframe=timeframe,
since_ms=since_ms,
is_new_pair=is_new_pair,
raise_=raise_,
candle_type=candle_type
)
def funding_fee_cutoff(self, open_date: datetime):
"""
:param open_date: The open date for a trade
:return: The cutoff open time for when a funding fee is charged
"""
return open_date.minute > 0 or (open_date.minute == 0 and open_date.second > 15)
def dry_run_liquidation_price(
self,
pair: str,
open_rate: float, # Entry price of position
is_short: bool,
position: float, # Absolute value of position size
wallet_balance: float, # Or margin balance
mm_ex_1: float = 0.0, # (Binance) Cross only
upnl_ex_1: float = 0.0, # (Binance) Cross only
) -> Optional[float]:
"""
MARGIN: https://www.binance.com/en/support/faq/f6b010588e55413aa58b7d63ee0125ed
PERPETUAL: https://www.binance.com/en/support/faq/b3c689c1f50a44cabb3a84e663b81d93
:param exchange_name:
:param open_rate: (EP1) Entry price of position
:param is_short: True if the trade is a short, false otherwise
:param position: Absolute value of position size (in base currency)
:param wallet_balance: (WB)
Cross-Margin Mode: crossWalletBalance
Isolated-Margin Mode: isolatedWalletBalance
:param maintenance_amt:
# * Only required for Cross
:param mm_ex_1: (TMM)
Cross-Margin Mode: Maintenance Margin of all other contracts, excluding Contract 1
Isolated-Margin Mode: 0
:param upnl_ex_1: (UPNL)
Cross-Margin Mode: Unrealized PNL of all other contracts, excluding Contract 1.
Isolated-Margin Mode: 0
"""
side_1 = -1 if is_short else 1
position = abs(position)
cross_vars = upnl_ex_1 - mm_ex_1 if self.margin_mode == MarginMode.CROSS else 0.0
# mm_ratio: Binance's formula specifies maintenance margin rate which is mm_ratio * 100%
# maintenance_amt: (CUM) Maintenance Amount of position
mm_ratio, maintenance_amt = self.get_maintenance_ratio_and_amt(pair, position)
if (maintenance_amt is None):
raise OperationalException(
"Parameter maintenance_amt is required by Binance.liquidation_price"
f"for {self.trading_mode.value}"
)
if self.trading_mode == TradingMode.FUTURES:
return (
(
(wallet_balance + cross_vars + maintenance_amt) -
(side_1 * position * open_rate)
) / (
(position * mm_ratio) - (side_1 * position)
)
)
else:
raise OperationalException(
"Freqtrade only supports isolated futures for leverage trading")
@retrier
def load_leverage_tiers(self) -> Dict[str, List[Dict]]:
if self.trading_mode == TradingMode.FUTURES:
if self._config['dry_run']:
leverage_tiers_path = (
Path(__file__).parent / 'binance_leverage_tiers.json'
)
with open(leverage_tiers_path) as json_file:
return json.load(json_file)
else:
try:
return self._api.fetch_leverage_tiers()
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(f'Could not fetch leverage amounts due to'
f'{e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
else:
return {}

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,8 @@
""" Bybit exchange subclass """
import logging
from typing import Dict
from typing import Dict, List, Tuple
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exchange import Exchange
@ -20,4 +21,11 @@ class Bybit(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 200,
"ccxt_futures_name": "linear"
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.FUTURES, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.ISOLATED)
]

View File

@ -35,9 +35,19 @@ BAD_EXCHANGES = {
MAP_EXCHANGE_CHILDCLASS = {
'binanceus': 'binance',
'binanceje': 'binance',
'binanceusdm': 'binance',
'okex': 'okx',
}
SUPPORTED_EXCHANGES = [
'binance',
'bittrex',
'ftx',
'gateio',
'huobi',
'kraken',
'okx',
]
EXCHANGE_HAS_REQUIRED = [
# Required / private
@ -55,10 +65,17 @@ EXCHANGE_HAS_REQUIRED = [
EXCHANGE_HAS_OPTIONAL = [
# Private
'fetchMyTrades', # Trades for order - fee detection
# 'setLeverage', # Margin/Futures trading
# 'setMarginMode', # Margin/Futures trading
# 'fetchFundingHistory', # Futures trading
# Public
'fetchOrderBook', 'fetchL2OrderBook', 'fetchTicker', # OR for pricing
'fetchTickers', # For volumepairlist?
'fetchTrades', # Downloading trades data
# 'fetchFundingRateHistory', # Futures trading
# 'fetchPositions', # Futures trading
# 'fetchLeverageTiers', # Futures initialization
# 'fetchMarketLeverageTiers', # Futures initialization
]

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
""" FTX exchange subclass """
import logging
from typing import Any, Dict
from typing import Any, Dict, List, Tuple
import ccxt
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
@ -20,27 +21,29 @@ class Ftx(Exchange):
"stoploss_on_exchange": True,
"ohlcv_candle_limit": 1500,
"ohlcv_volume_currency": "quote",
"mark_ohlcv_price": "index",
"mark_ohlcv_timeframe": "1h",
}
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
Default checks + check if pair is spot pair (no futures trading yet).
"""
parent_check = super().market_is_tradable(market)
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
]
return (parent_check and
market.get('spot', False) is True)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return order['type'] == 'stop' and stop_loss > float(order['price'])
return order['type'] == 'stop' and (
side == "sell" and stop_loss > float(order['price']) or
side == "buy" and stop_loss < float(order['price'])
)
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
"""
Creates a stoploss order.
depending on order_types.stoploss configuration, uses 'market' or limit order.
@ -48,7 +51,10 @@ class Ftx(Exchange):
Limit orders are defined by having orderPrice set, otherwise a market order is used.
"""
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
limit_rate = stop_price * limit_price_pct
else:
limit_rate = stop_price * (2 - limit_price_pct)
ordertype = "stop"
@ -56,7 +62,7 @@ class Ftx(Exchange):
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order
try:
@ -64,11 +70,14 @@ class Ftx(Exchange):
if order_types.get('stoploss', 'market') == 'limit':
# set orderPrice to place limit order, otherwise it's a market order
params['orderPrice'] = limit_rate
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
params['stopPrice'] = stop_price
amount = self.amount_to_precision(pair, amount)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
self._lev_prep(pair, leverage, side)
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, params=params)
self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
@ -76,19 +85,19 @@ class Ftx(Exchange):
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e

View File

@ -1,7 +1,9 @@
""" Gate.io exchange subclass """
import logging
from typing import Dict
from datetime import datetime
from typing import Dict, List, Optional, Tuple
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import Exchange
@ -26,13 +28,49 @@ class Gateio(Exchange):
"stoploss_on_exchange": True,
}
_ft_has_futures: Dict = {
"needs_trading_fees": True
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED)
]
def validate_ordertypes(self, order_types: Dict) -> None:
super().validate_ordertypes(order_types)
if self.trading_mode != TradingMode.FUTURES:
if any(v == 'market' for k, v in order_types.items()):
raise OperationalException(
f'Exchange {self.name} does not support market orders.')
def get_trades_for_order(self, order_id: str, pair: str, since: datetime,
params: Optional[Dict] = None) -> List:
trades = super().get_trades_for_order(order_id, pair, since, params)
if self.trading_mode == TradingMode.FUTURES:
# Futures usually don't contain fees in the response.
# As such, futures orders on gateio will not contain a fee, which causes
# a repeated "update fee" cycle and wrong calculations.
# Therefore we patch the response with fees if it's not available.
# An alternative also contianing fees would be
# privateFuturesGetSettleAccountBook({"settle": "usdt"})
pair_fees = self._trading_fees.get(pair, {})
if pair_fees:
for idx, trade in enumerate(trades):
if trade.get('fee', {}).get('cost') is None:
takerOrMaker = trade.get('takerOrMaker', 'taker')
if pair_fees.get(takerOrMaker) is not None:
trades[idx]['fee'] = {
'currency': self.get_pair_quote_currency(pair),
'cost': trade['cost'] * pair_fees[takerOrMaker],
'rate': pair_fees[takerOrMaker],
}
return trades
def fetch_stoploss_order(self, order_id: str, pair: str, params={}) -> Dict:
return self.fetch_order(
order_id=order_id,
@ -47,9 +85,10 @@ class Gateio(Exchange):
params={'stop': True}
)
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return stop_loss > float(order['stopPrice'])
return ((side == "sell" and stop_loss > float(order['stopPrice'])) or
(side == "buy" and stop_loss < float(order['stopPrice'])))

View File

@ -22,7 +22,7 @@ class Huobi(Exchange):
"l2_limit_range_required": False,
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.

View File

@ -1,9 +1,12 @@
""" Kraken exchange subclass """
import logging
from typing import Any, Dict, List
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
import ccxt
from pandas import DataFrame
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import (DDosProtection, InsufficientFundsError, InvalidOrderException,
OperationalException, TemporaryError)
from freqtrade.exchange import Exchange
@ -21,8 +24,15 @@ class Kraken(Exchange):
"ohlcv_candle_limit": 720,
"trades_pagination": "id",
"trades_pagination_arg": "since",
"mark_ohlcv_timeframe": "4h",
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS)
]
def market_is_tradable(self, market: Dict[str, Any]) -> bool:
"""
Check if the market symbol is tradable by Freqtrade.
@ -73,16 +83,19 @@ class Kraken(Exchange):
except ccxt.BaseError as e:
raise OperationalException(e) from e
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.
"""
return (order['type'] in ('stop-loss', 'stop-loss-limit')
and stop_loss > float(order['price']))
return (order['type'] in ('stop-loss', 'stop-loss-limit') and (
(side == "sell" and stop_loss > float(order['price'])) or
(side == "buy" and stop_loss < float(order['price']))
))
@retrier(retries=0)
def stoploss(self, pair: str, amount: float, stop_price: float, order_types: Dict) -> Dict:
def stoploss(self, pair: str, amount: float, stop_price: float,
order_types: Dict, side: str, leverage: float) -> Dict:
"""
Creates a stoploss market order.
Stoploss market orders is the only stoploss type supported by kraken.
@ -90,11 +103,16 @@ class Kraken(Exchange):
(careful, prices are reversed)
"""
params = self._params.copy()
if self.trading_mode == TradingMode.FUTURES:
params.update({'reduceOnly': True})
if order_types.get('stoploss', 'market') == 'limit':
ordertype = "stop-loss-limit"
limit_price_pct = order_types.get('stoploss_on_exchange_limit_ratio', 0.99)
if side == "sell":
limit_rate = stop_price * limit_price_pct
else:
limit_rate = stop_price * (2 - limit_price_pct)
params['price2'] = self.price_to_precision(pair, limit_rate)
else:
ordertype = "stop-loss"
@ -103,13 +121,13 @@ class Kraken(Exchange):
if self._config['dry_run']:
dry_order = self.create_dry_run_order(
pair, ordertype, "sell", amount, stop_price, stop_loss=True)
pair, ordertype, side, amount, stop_price, leverage, stop_loss=True)
return dry_order
try:
amount = self.amount_to_precision(pair, amount)
order = self._api.create_order(symbol=pair, type=ordertype, side='sell',
order = self._api.create_order(symbol=pair, type=ordertype, side=side,
amount=amount, price=stop_price, params=params)
self._log_exchange_response('create_stoploss_order', order)
logger.info('stoploss order added for %s. '
@ -117,18 +135,81 @@ class Kraken(Exchange):
return order
except ccxt.InsufficientFunds as e:
raise InsufficientFundsError(
f'Insufficient funds to create {ordertype} sell order on market {pair}. '
f'Insufficient funds to create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.InvalidOrder as e:
raise InvalidOrderException(
f'Could not create {ordertype} sell order on market {pair}. '
f'Could not create {ordertype} {side} order on market {pair}. '
f'Tried to create stoploss with amount {amount} at stoploss {stop_price}. '
f'Message: {e}') from e
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not place sell order due to {e.__class__.__name__}. Message: {e}') from e
f'Could not place {side} order due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def _set_leverage(
self,
leverage: float,
pair: Optional[str] = None,
trading_mode: Optional[TradingMode] = None
):
"""
Kraken set's the leverage as an option in the order object, so we need to
add it to params
"""
return
def _get_params(
self,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc'
) -> Dict:
params = super()._get_params(
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if leverage > 1.0:
params['leverage'] = round(leverage)
return params
def calculate_funding_fees(
self,
df: DataFrame,
amount: float,
is_short: bool,
open_date: datetime,
close_date: Optional[datetime] = None,
time_in_ratio: Optional[float] = None
) -> float:
"""
# ! This method will always error when run by Freqtrade because time_in_ratio is never
# ! passed to _get_funding_fee. For kraken futures to work in dry run and backtesting
# ! functionality must be added that passes the parameter time_in_ratio to
# ! _get_funding_fee when using Kraken
calculates the sum of all funding fees that occurred for a pair during a futures trade
:param df: Dataframe containing combined funding and mark rates
as `open_fund` and `open_mark`.
:param amount: The quantity of the trade
:param is_short: trade direction
:param open_date: The date and time that the trade started
:param close_date: The date and time that the trade ended
:param time_in_ratio: Not used by most exchange classes
"""
if not time_in_ratio:
raise OperationalException(
f"time_in_ratio is required for {self.name}._get_funding_fee")
fees: float = 0
if not df.empty:
df = df[(df['date'] >= open_date) & (df['date'] <= close_date)]
fees = sum(df['open_fund'] * df['open_mark'] * amount * time_in_ratio)
return fees if is_short else -fees

View File

@ -28,7 +28,7 @@ class Kucoin(Exchange):
"ohlcv_candle_limit": 1500,
}
def stoploss_adjust(self, stop_loss: float, order: Dict) -> bool:
def stoploss_adjust(self, stop_loss: float, order: Dict, side: str) -> bool:
"""
Verify stop_loss against stoploss-order value (limit or price)
Returns True if adjustment is necessary.

View File

@ -1,7 +1,12 @@
import logging
from typing import Dict
from typing import Dict, List, Tuple
import ccxt
from freqtrade.enums import MarginMode, TradingMode
from freqtrade.exceptions import DDosProtection, OperationalException, TemporaryError
from freqtrade.exchange import Exchange
from freqtrade.exchange.common import retrier
logger = logging.getLogger(__name__)
@ -15,4 +20,69 @@ class Okx(Exchange):
_ft_has: Dict = {
"ohlcv_candle_limit": 300,
"mark_ohlcv_timeframe": "4h",
"funding_fee_timeframe": "8h",
}
_ft_has_futures: Dict = {
"tickers_have_quoteVolume": False,
}
_supported_trading_mode_margin_pairs: List[Tuple[TradingMode, MarginMode]] = [
# TradingMode.SPOT always supported and not required in this list
# (TradingMode.MARGIN, MarginMode.CROSS),
# (TradingMode.FUTURES, MarginMode.CROSS),
(TradingMode.FUTURES, MarginMode.ISOLATED),
]
def _get_params(
self,
ordertype: str,
leverage: float,
reduceOnly: bool,
time_in_force: str = 'gtc',
) -> Dict:
params = super()._get_params(
ordertype=ordertype,
leverage=leverage,
reduceOnly=reduceOnly,
time_in_force=time_in_force,
)
if self.trading_mode == TradingMode.FUTURES and self.margin_mode:
params['tdMode'] = self.margin_mode.value
return params
@retrier
def _lev_prep(self, pair: str, leverage: float, side: str):
if self.trading_mode != TradingMode.SPOT and self.margin_mode is not None:
try:
# TODO-lev: Test me properly (check mgnMode passed)
self._api.set_leverage(
leverage=leverage,
symbol=pair,
params={
"mgnMode": self.margin_mode.value,
# "posSide": "net"",
})
except ccxt.DDoSProtection as e:
raise DDosProtection(e) from e
except (ccxt.NetworkError, ccxt.ExchangeError) as e:
raise TemporaryError(
f'Could not set leverage due to {e.__class__.__name__}. Message: {e}') from e
except ccxt.BaseError as e:
raise OperationalException(e) from e
def get_max_pair_stake_amount(
self,
pair: str,
price: float,
leverage: float = 1.0
) -> float:
if self.trading_mode == TradingMode.SPOT:
return float('inf') # Not actually inf, but this probably won't matter for SPOT
if pair not in self._leverage_tiers:
return float('inf')
pair_tiers = self._leverage_tiers[pair]
return pair_tiers[-1]['max'] / leverage

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
# flake8: noqa: F401
from freqtrade.leverage.interest import interest

View File

@ -0,0 +1,43 @@
from decimal import Decimal
from math import ceil
from freqtrade.exceptions import OperationalException
one = Decimal(1.0)
four = Decimal(4.0)
twenty_four = Decimal(24.0)
def interest(
exchange_name: str,
borrowed: Decimal,
rate: Decimal,
hours: Decimal
) -> Decimal:
"""
Equation to calculate interest on margin trades
:param exchange_name: The exchanged being trading on
:param borrowed: The amount of currency being borrowed
:param rate: The rate of interest (i.e daily interest rate)
:param hours: The time in hours that the currency has been borrowed for
Raises:
OperationalException: Raised if freqtrade does
not support margin trading for this exchange
Returns: The amount of interest owed (currency matches borrowed)
"""
exchange_name = exchange_name.lower()
if exchange_name == "binance":
return borrowed * rate * ceil(hours)/twenty_four
elif exchange_name == "kraken":
# Rounded based on https://kraken-fees-calculator.github.io/
return borrowed * rate * (one+ceil(hours/four))
elif exchange_name == "ftx":
# As Explained under #Interest rates section in
# https://help.ftx.com/hc/en-us/articles/360053007671-Spot-Margin-Trading-Explainer
return borrowed * rate * ceil(hours)/twenty_four
else:
raise OperationalException(f"Leverage not available on {exchange_name} with freqtrade")

View File

@ -116,7 +116,7 @@ def file_load_json(file):
def pair_to_filename(pair: str) -> str:
for ch in ['/', '-', ' ', '.', '@', '$', '+', ':']:
for ch in ['/', ' ', '.', '@', '$', '+', ':']:
pair = pair.replace(ch, '_')
return pair
@ -129,7 +129,7 @@ def format_ms_time(date: int) -> str:
return datetime.fromtimestamp(date/1000.0).strftime('%Y-%m-%dT%H:%M:%S')
def deep_merge_dicts(source, destination):
def deep_merge_dicts(source, destination, allow_null_overrides: bool = True):
"""
Values from Source override destination, destination is returned (and modified!!)
Sample:
@ -142,8 +142,8 @@ def deep_merge_dicts(source, destination):
if isinstance(value, dict):
# get node or create one
node = destination.setdefault(key, {})
deep_merge_dicts(value, node)
else:
deep_merge_dicts(value, node, allow_null_overrides)
elif value is not None or allow_null_overrides:
destination[key] = value
return destination

View File

@ -9,6 +9,7 @@ from copy import deepcopy
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List, Optional, Tuple
from numpy import nan
from pandas import DataFrame
from freqtrade import constants
@ -18,7 +19,7 @@ from freqtrade.data import history
from freqtrade.data.btanalysis import find_existing_backtest_stats, trade_list_to_dataframe
from freqtrade.data.converter import trim_dataframe, trim_dataframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import BacktestState, SellType
from freqtrade.enums import BacktestState, CandleType, ExitCheckTuple, ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.misc import get_strategy_run_id
@ -30,7 +31,7 @@ from freqtrade.persistence import LocalTrade, Order, PairLocks, Trade
from freqtrade.plugins.pairlistmanager import PairListManager
from freqtrade.plugins.protectionmanager import ProtectionManager
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.strategy.interface import IStrategy, SellCheckTuple
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy.strategy_wrapper import strategy_safe_wrapper
from freqtrade.wallets import Wallets
@ -39,14 +40,16 @@ logger = logging.getLogger(__name__)
# Indexes for backtest tuples
DATE_IDX = 0
BUY_IDX = 1
OPEN_IDX = 2
CLOSE_IDX = 3
SELL_IDX = 4
LOW_IDX = 5
HIGH_IDX = 6
BUY_TAG_IDX = 7
EXIT_TAG_IDX = 8
OPEN_IDX = 1
HIGH_IDX = 2
LOW_IDX = 3
CLOSE_IDX = 4
LONG_IDX = 5
ELONG_IDX = 6 # Exit long
SHORT_IDX = 7
ESHORT_IDX = 8 # Exit short
ENTER_TAG_IDX = 9
EXIT_TAG_IDX = 10
class Backtesting:
@ -70,8 +73,8 @@ class Backtesting:
self.run_ids: Dict[str, str] = {}
self.strategylist: List[IStrategy] = []
self.all_results: Dict[str, Dict] = {}
self.exchange = ExchangeResolver.load_exchange(self.config['exchange']['name'], self.config)
self._exchange_name = self.config['exchange']['name']
self.exchange = ExchangeResolver.load_exchange(self._exchange_name, self.config)
self.dataprovider = DataProvider(self.config, self.exchange)
if self.config.get('strategy_list', None):
@ -123,6 +126,11 @@ class Backtesting:
# Add maximum startup candle count to configuration for informative pairs support
self.config['startup_candle_count'] = self.required_startup
self.exchange.validate_required_startup_candles(self.required_startup, self.timeframe)
self.trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
# strategies which define "can_short=True" will fail to load in Spot mode.
self._can_short = self.trading_mode != TradingMode.SPOT
self.init_backtest()
def __del__(self):
@ -146,6 +154,7 @@ class Backtesting:
else:
self.timeframe_detail_min = 0
self.detail_data: Dict[str, DataFrame] = {}
self.futures_data: Dict[str, DataFrame] = {}
def init_backtest(self):
@ -192,6 +201,7 @@ class Backtesting:
startup_candles=self.required_startup,
fail_without_data=True,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
)
min_date, max_date = history.get_timerange(data)
@ -220,9 +230,41 @@ class Backtesting:
startup_candles=0,
fail_without_data=True,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=self.config.get('candle_type_def', CandleType.SPOT)
)
else:
self.detail_data = {}
if self.trading_mode == TradingMode.FUTURES:
# Load additional futures data.
funding_rates_dict = history.load_data(
datadir=self.config['datadir'],
pairs=self.pairlists.whitelist,
timeframe=self.exchange._ft_has['mark_ohlcv_timeframe'],
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=CandleType.FUNDING_RATE
)
# For simplicity, assign to CandleType.Mark (might contian index candles!)
mark_rates_dict = history.load_data(
datadir=self.config['datadir'],
pairs=self.pairlists.whitelist,
timeframe=self.exchange._ft_has['mark_ohlcv_timeframe'],
timerange=self.timerange,
startup_candles=0,
fail_without_data=True,
data_format=self.config.get('dataformat_ohlcv', 'json'),
candle_type=CandleType.from_string(self.exchange._ft_has["mark_ohlcv_price"])
)
# Combine data to avoid combining the data per trade.
for pair in self.pairlists.whitelist:
self.futures_data[pair] = funding_rates_dict[pair].merge(
mark_rates_dict[pair], on='date', how="inner", suffixes=["_fund", "_mark"])
else:
self.futures_data = {}
def prepare_backtest(self, enable_protections):
"""
@ -260,7 +302,8 @@ class Backtesting:
"""
# Every change to this headers list must evaluate further usages of the resulting tuple
# and eventually change the constants for indexes at the top
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag']
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
data: Dict = {}
self.progress.init_step(BacktestState.CONVERT, len(processed))
@ -269,19 +312,21 @@ class Backtesting:
pair_data = processed[pair]
self.check_abort()
self.progress.increment()
if not pair_data.empty:
pair_data.loc[:, 'buy'] = 0 # cleanup if buy_signal is exist
pair_data.loc[:, 'sell'] = 0 # cleanup if sell_signal is exist
pair_data.loc[:, 'buy_tag'] = None # cleanup if buy_tag is exist
pair_data.loc[:, 'exit_tag'] = None # cleanup if exit_tag is exist
df_analyzed = self.strategy.advise_sell(
self.strategy.advise_buy(pair_data, {'pair': pair}), {'pair': pair}).copy()
if not pair_data.empty:
# Cleanup from prior runs
pair_data.drop(headers[5:] + ['buy', 'sell'], axis=1, errors='ignore')
df_analyzed = self.strategy.advise_exit(
self.strategy.advise_entry(pair_data, {'pair': pair}),
{'pair': pair}
).copy()
# Trim startup period from analyzed dataframe
df_analyzed = processed[pair] = pair_data = trim_dataframe(
df_analyzed, self.timerange, startup_candles=self.required_startup)
# Update dataprovider cache
self.dataprovider._set_cached_df(pair, self.timeframe, df_analyzed)
self.dataprovider._set_cached_df(
pair, self.timeframe, df_analyzed, self.config['candle_type_def'])
# Create a copy of the dataframe before shifting, that way the buy signal/tag
# remains on the correct candle for callbacks.
@ -289,10 +334,13 @@ class Backtesting:
# To avoid using data from future, we use buy/sell signals shifted
# from the previous candle
df_analyzed.loc[:, 'buy'] = df_analyzed.loc[:, 'buy'].shift(1)
df_analyzed.loc[:, 'sell'] = df_analyzed.loc[:, 'sell'].shift(1)
df_analyzed.loc[:, 'buy_tag'] = df_analyzed.loc[:, 'buy_tag'].shift(1)
df_analyzed.loc[:, 'exit_tag'] = df_analyzed.loc[:, 'exit_tag'].shift(1)
for col in headers[5:]:
tag_col = col in ('enter_tag', 'exit_tag')
if col in df_analyzed.columns:
df_analyzed.loc[:, col] = df_analyzed.loc[:, col].replace(
[nan], [0 if not tag_col else None]).shift(1)
else:
df_analyzed.loc[:, col] = 0 if not tag_col else None
df_analyzed = df_analyzed.drop(df_analyzed.head(1).index)
@ -301,23 +349,38 @@ class Backtesting:
data[pair] = df_analyzed[headers].values.tolist()
return data
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: SellCheckTuple,
def _get_close_rate(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float:
"""
Get close rate for backtesting result
"""
# Special handling if high or low hit STOP_LOSS or ROI
if sell.sell_type in (SellType.STOP_LOSS, SellType.TRAILING_STOP_LOSS):
if trade.stop_loss > sell_row[HIGH_IDX]:
# our stoploss was already higher than candle high,
if sell.exit_type in (ExitType.STOP_LOSS, ExitType.TRAILING_STOP_LOSS):
return self._get_close_rate_for_stoploss(sell_row, trade, sell, trade_dur)
elif sell.exit_type == (ExitType.ROI):
return self._get_close_rate_for_roi(sell_row, trade, sell, trade_dur)
else:
return sell_row[OPEN_IDX]
def _get_close_rate_for_stoploss(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float:
# our stoploss was already lower than candle high,
# possibly due to a cancelled trade exit.
# sell at open price.
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
if is_short:
if trade.stop_loss < sell_row[LOW_IDX]:
return sell_row[OPEN_IDX]
else:
if trade.stop_loss > sell_row[HIGH_IDX]:
return sell_row[OPEN_IDX]
# Special case: trailing triggers within same candle as trade opened. Assume most
# pessimistic price movement, which is moving just enough to arm stoploss and
# immediately going down to stop price.
if sell.sell_type == SellType.TRAILING_STOP_LOSS and trade_dur == 0:
if sell.exit_type == ExitType.TRAILING_STOP_LOSS and trade_dur == 0:
if (
not self.strategy.use_custom_stoploss and self.strategy.trailing_stop
and self.strategy.trailing_only_offset_is_reached
@ -326,19 +389,32 @@ class Backtesting:
):
# Worst case: price reaches stop_positive_offset and dives down.
stop_rate = (sell_row[OPEN_IDX] *
(1 + abs(self.strategy.trailing_stop_positive_offset) -
abs(self.strategy.trailing_stop_positive)))
(1 + side_1 * abs(self.strategy.trailing_stop_positive_offset) -
side_1 * abs(self.strategy.trailing_stop_positive / leverage)))
else:
# Worst case: price ticks tiny bit above open and dives down.
stop_rate = sell_row[OPEN_IDX] * (1 - abs(trade.stop_loss_pct))
stop_rate = sell_row[OPEN_IDX] * (1 -
side_1 * abs(trade.stop_loss_pct / leverage))
if is_short:
assert stop_rate > sell_row[LOW_IDX]
else:
assert stop_rate < sell_row[HIGH_IDX]
# Limit lower-end to candle low to avoid sells below the low.
# This still remains "worst case" - but "worst realistic case".
if is_short:
return min(sell_row[HIGH_IDX], stop_rate)
else:
return max(sell_row[LOW_IDX], stop_rate)
# Set close_rate to stoploss
return trade.stop_loss
elif sell.sell_type == (SellType.ROI):
def _get_close_rate_for_roi(self, sell_row: Tuple, trade: LocalTrade, sell: ExitCheckTuple,
trade_dur: int) -> float:
is_short = trade.is_short or False
leverage = trade.leverage or 1.0
side_1 = -1 if is_short else 1
roi_entry, roi = self.strategy.min_roi_reached_entry(trade_dur)
if roi is not None and roi_entry is not None:
if roi == -1 and roi_entry % self.timeframe_min == 0:
@ -347,29 +423,44 @@ class Backtesting:
# - we'll use open instead of close
return sell_row[OPEN_IDX]
# - (Expected abs profit + open_rate + open_fee) / (fee_close -1)
close_rate = - (trade.open_rate * roi + trade.open_rate *
(1 + trade.fee_open)) / (trade.fee_close - 1)
# - (Expected abs profit - open_rate - open_fee) / (fee_close -1)
roi_rate = trade.open_rate * roi / leverage
open_fee_rate = side_1 * trade.open_rate * (1 + side_1 * trade.fee_open)
close_rate = -(roi_rate + open_fee_rate) / (trade.fee_close - side_1 * 1)
if is_short:
is_new_roi = sell_row[OPEN_IDX] < close_rate
else:
is_new_roi = sell_row[OPEN_IDX] > close_rate
if (trade_dur > 0 and trade_dur == roi_entry
and roi_entry % self.timeframe_min == 0
and sell_row[OPEN_IDX] > close_rate):
and is_new_roi):
# new ROI entry came into effect.
# use Open rate if open_rate > calculated sell rate
return sell_row[OPEN_IDX]
if (
trade_dur == 0
# Red candle (for longs), TODO: green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # Red candle
if (trade_dur == 0 and (
(
is_short
# Red candle (for longs)
and sell_row[OPEN_IDX] < sell_row[CLOSE_IDX] # Red candle
and trade.open_rate > sell_row[OPEN_IDX] # trade-open above open_rate
and close_rate < sell_row[CLOSE_IDX] # closes below close
)
or
(
not is_short
# green candle (for shorts)
and sell_row[OPEN_IDX] > sell_row[CLOSE_IDX] # green candle
and trade.open_rate < sell_row[OPEN_IDX] # trade-open below open_rate
and close_rate > sell_row[CLOSE_IDX]
):
and close_rate > sell_row[CLOSE_IDX] # closes above close
)
)):
# ROI on opening candles with custom pricing can only
# trigger if the entry was at Open or lower.
# trigger if the entry was at Open or lower wick.
# details: https: // github.com/freqtrade/freqtrade/issues/6261
# If open_rate is < open, only allow sells below the close on red candles.
raise ValueError("Opening candle ROI on red candles.")
# Use the maximum between close_rate and low as we
# cannot sell outside of a candle.
# Applies when a new ROI setting comes in place and the whole candle is above that.
@ -378,23 +469,24 @@ class Backtesting:
else:
# This should not be reached...
return sell_row[OPEN_IDX]
else:
return sell_row[OPEN_IDX]
def _get_adjust_trade_entry_for_candle(self, trade: LocalTrade, row: Tuple
) -> LocalTrade:
current_profit = trade.calc_profit_ratio(row[OPEN_IDX])
min_stake = self.exchange.get_min_pair_stake_amount(trade.pair, row[OPEN_IDX], -0.1)
max_stake = self.wallets.get_available_stake_amount()
max_stake = self.exchange.get_max_pair_stake_amount(trade.pair, row[OPEN_IDX])
stake_available = self.wallets.get_available_stake_amount()
stake_amount = strategy_safe_wrapper(self.strategy.adjust_trade_position,
default_retval=None)(
trade=trade, current_time=row[DATE_IDX].to_pydatetime(), current_rate=row[OPEN_IDX],
current_profit=current_profit, min_stake=min_stake, max_stake=max_stake)
current_profit=current_profit, min_stake=min_stake,
max_stake=min(max_stake, stake_available))
# Check if we should increase our position
if stake_amount is not None and stake_amount > 0.0:
pos_trade = self._enter_trade(trade.pair, row, stake_amount, trade)
pos_trade = self._enter_trade(
trade.pair, row, 'short' if trade.is_short else 'long', stake_amount, trade)
if pos_trade is not None:
self.wallets.update()
return pos_trade
@ -410,20 +502,23 @@ class Backtesting:
# Check if we need to adjust our current positions
if self.strategy.position_adjustment_enable:
check_adjust_buy = True
check_adjust_entry = True
if self.strategy.max_entry_position_adjustment > -1:
count_of_buys = trade.nr_of_successful_buys
check_adjust_buy = (count_of_buys <= self.strategy.max_entry_position_adjustment)
if check_adjust_buy:
entry_count = trade.nr_of_successful_entries
check_adjust_entry = (entry_count <= self.strategy.max_entry_position_adjustment)
if check_adjust_entry:
trade = self._get_adjust_trade_entry_for_candle(trade, sell_row)
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell = self.strategy.should_sell(trade, sell_row[OPEN_IDX], # type: ignore
sell_candle_time, sell_row[BUY_IDX],
sell_row[SELL_IDX],
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX])
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
enter = sell_row[SHORT_IDX] if trade.is_short else sell_row[LONG_IDX]
exit_ = sell_row[ESHORT_IDX] if trade.is_short else sell_row[ELONG_IDX]
sell = self.strategy.should_exit(
trade, sell_row[OPEN_IDX], sell_candle_time, # type: ignore
enter=enter, exit_=exit_,
low=sell_row[LOW_IDX], high=sell_row[HIGH_IDX]
)
if sell.sell_flag:
if sell.exit_flag:
trade.close_date = sell_candle_time
trade_dur = int((trade.close_date_utc - trade.open_date_utc).total_seconds() // 60)
@ -433,8 +528,8 @@ class Backtesting:
return None
# call the custom exit price,with default value as previous closerate
current_profit = trade.calc_profit_ratio(closerate)
order_type = self.strategy.order_types['sell']
if sell.sell_type in (SellType.SELL_SIGNAL, SellType.CUSTOM_SELL):
order_type = self.strategy.order_types['exit']
if sell.exit_type in (ExitType.SELL_SIGNAL, ExitType.CUSTOM_SELL):
# Custom exit pricing only for sell-signals
if order_type == 'limit':
closerate = strategy_safe_wrapper(self.strategy.custom_exit_price,
@ -444,19 +539,23 @@ class Backtesting:
proposed_rate=closerate, current_profit=current_profit)
# We can't place orders lower than current low.
# freqtrade does not support this in live, and the order would fill immediately
if trade.is_short:
closerate = min(closerate, sell_row[HIGH_IDX])
else:
closerate = max(closerate, sell_row[LOW_IDX])
# Confirm trade exit:
time_in_force = self.strategy.order_time_in_force['sell']
time_in_force = self.strategy.order_time_in_force['exit']
if not strategy_safe_wrapper(self.strategy.confirm_trade_exit, default_retval=True)(
pair=trade.pair, trade=trade, order_type='limit', amount=trade.amount,
rate=closerate,
time_in_force=time_in_force,
sell_reason=sell.sell_reason,
sell_reason=sell.exit_reason, # deprecated
exit_reason=sell.exit_reason,
current_time=sell_candle_time):
return None
trade.sell_reason = sell.sell_reason
trade.sell_reason = sell.exit_reason
# Checks and adds an exit tag, after checking that the length of the
# sell_row has the length for an exit tag column
@ -477,8 +576,8 @@ class Backtesting:
ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair,
ft_order_side="sell",
side="sell",
ft_order_side=trade.exit_side,
side=trade.exit_side,
order_type=order_type,
status="open",
price=closerate,
@ -494,8 +593,18 @@ class Backtesting:
return None
def _get_sell_trade_entry(self, trade: LocalTrade, sell_row: Tuple) -> Optional[LocalTrade]:
sell_candle_time: datetime = sell_row[DATE_IDX].to_pydatetime()
if self.trading_mode == TradingMode.FUTURES:
trade.funding_fees = self.exchange.calculate_funding_fees(
self.futures_data[trade.pair],
amount=trade.amount,
is_short=trade.is_short,
open_date=trade.open_date_utc,
close_date=sell_candle_time,
)
if self.timeframe_detail and trade.pair in self.detail_data:
sell_candle_time = sell_row[DATE_IDX].to_pydatetime()
sell_candle_end = sell_candle_time + timedelta(minutes=self.timeframe_min)
detail_data = self.detail_data[trade.pair]
@ -506,11 +615,14 @@ class Backtesting:
if len(detail_data) == 0:
# Fall back to "regular" data if no detail data was found for this candle
return self._get_sell_trade_entry_for_candle(trade, sell_row)
detail_data.loc[:, 'buy'] = sell_row[BUY_IDX]
detail_data.loc[:, 'sell'] = sell_row[SELL_IDX]
detail_data.loc[:, 'buy_tag'] = sell_row[BUY_TAG_IDX]
detail_data.loc[:, 'enter_long'] = sell_row[LONG_IDX]
detail_data.loc[:, 'exit_long'] = sell_row[ELONG_IDX]
detail_data.loc[:, 'enter_short'] = sell_row[SHORT_IDX]
detail_data.loc[:, 'exit_short'] = sell_row[ESHORT_IDX]
detail_data.loc[:, 'enter_tag'] = sell_row[ENTER_TAG_IDX]
detail_data.loc[:, 'exit_tag'] = sell_row[EXIT_TAG_IDX]
headers = ['date', 'buy', 'open', 'close', 'sell', 'low', 'high', 'buy_tag', 'exit_tag']
headers = ['date', 'open', 'high', 'low', 'close', 'enter_long', 'exit_long',
'enter_short', 'exit_short', 'enter_tag', 'exit_tag']
for det_row in detail_data[headers].values.tolist():
res = self._get_sell_trade_entry_for_candle(trade, det_row)
if res:
@ -521,58 +633,103 @@ class Backtesting:
else:
return self._get_sell_trade_entry_for_candle(trade, sell_row)
def _enter_trade(self, pair: str, row: Tuple, stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
def get_valid_price_and_stake(
self, pair: str, row: Tuple, propose_rate: float, stake_amount: Optional[float],
direction: str, current_time: datetime, entry_tag: Optional[str],
trade: Optional[LocalTrade], order_type: str
) -> Tuple[float, float, float, float]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[BUY_TAG_IDX] if len(row) >= BUY_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['buy']
propose_rate = row[OPEN_IDX]
if order_type == 'limit':
propose_rate = strategy_safe_wrapper(self.strategy.custom_entry_price,
default_retval=row[OPEN_IDX])(
default_retval=propose_rate)(
pair=pair, current_time=current_time,
proposed_rate=propose_rate, entry_tag=entry_tag) # default value is the open rate
# We can't place orders higher than current high (otherwise it'd be a stop limit buy)
# which freqtrade does not support in live.
if direction == "short":
propose_rate = max(propose_rate, row[LOW_IDX])
else:
propose_rate = min(propose_rate, row[HIGH_IDX])
min_stake_amount = self.exchange.get_min_pair_stake_amount(pair, propose_rate, -0.05) or 0
max_stake_amount = self.wallets.get_available_stake_amount()
pos_adjust = trade is not None
leverage = trade.leverage if trade else 1.0
if not pos_adjust:
try:
stake_amount = self.wallets.get_trade_stake_amount(pair, None, update=False)
except DependencyException:
return None
return 0, 0, 0, 0
max_leverage = self.exchange.get_max_leverage(pair, stake_amount)
leverage = strategy_safe_wrapper(self.strategy.leverage, default_retval=1.0)(
pair=pair,
current_time=current_time,
current_rate=row[OPEN_IDX],
proposed_leverage=1.0,
max_leverage=max_leverage,
side=direction,
) if self._can_short else 1.0
# Cap leverage between 1.0 and max_leverage.
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
max_stake_amount = self.exchange.get_max_pair_stake_amount(
pair, propose_rate, leverage=leverage)
stake_available = self.wallets.get_available_stake_amount()
if not pos_adjust:
stake_amount = strategy_safe_wrapper(self.strategy.custom_stake_amount,
default_retval=stake_amount)(
pair=pair, current_time=current_time, current_rate=propose_rate,
proposed_stake=stake_amount, min_stake=min_stake_amount, max_stake=max_stake_amount,
entry_tag=entry_tag)
proposed_stake=stake_amount, min_stake=min_stake_amount,
max_stake=min(stake_available, max_stake_amount),
entry_tag=entry_tag, side=direction)
stake_amount = self.wallets.validate_stake_amount(pair, stake_amount, min_stake_amount)
stake_amount_val = self.wallets.validate_stake_amount(
pair=pair,
stake_amount=stake_amount,
min_stake_amount=min_stake_amount,
max_stake_amount=max_stake_amount,
)
return propose_rate, stake_amount_val, leverage, min_stake_amount
def _enter_trade(self, pair: str, row: Tuple, direction: str,
stake_amount: Optional[float] = None,
trade: Optional[LocalTrade] = None) -> Optional[LocalTrade]:
current_time = row[DATE_IDX].to_pydatetime()
entry_tag = row[ENTER_TAG_IDX] if len(row) >= ENTER_TAG_IDX + 1 else None
# let's call the custom entry price, using the open price as default price
order_type = self.strategy.order_types['entry']
pos_adjust = trade is not None
propose_rate, stake_amount, leverage, min_stake_amount = self.get_valid_price_and_stake(
pair, row, row[OPEN_IDX], stake_amount, direction, current_time, entry_tag, trade,
order_type
)
if not stake_amount:
# In case of pos adjust, still return the original trade
# If not pos adjust, trade is None
return trade
time_in_force = self.strategy.order_time_in_force['entry']
time_in_force = self.strategy.order_time_in_force['buy']
# Confirm trade entry:
if not pos_adjust:
# Confirm trade entry:
if not strategy_safe_wrapper(self.strategy.confirm_trade_entry, default_retval=True)(
pair=pair, order_type=order_type, amount=stake_amount, rate=propose_rate,
time_in_force=time_in_force, current_time=current_time,
entry_tag=entry_tag):
return None
entry_tag=entry_tag, side=direction):
return trade
if stake_amount and (not min_stake_amount or stake_amount > min_stake_amount):
self.order_id_counter += 1
amount = round(stake_amount / propose_rate, 8)
amount = round((stake_amount / propose_rate) * leverage, 8)
is_short = (direction == 'short')
# Necessary for Margin trading. Disabled until support is enabled.
# interest_rate = self.exchange.get_interest_rate()
if trade is None:
# Enter trade
self.trade_id_counter += 1
@ -589,13 +746,25 @@ class Backtesting:
fee_open=self.fee,
fee_close=self.fee,
is_open=True,
buy_tag=entry_tag,
exchange='backtesting',
orders=[]
enter_tag=entry_tag,
exchange=self._exchange_name,
is_short=is_short,
trading_mode=self.trading_mode,
leverage=leverage,
# interest_rate=interest_rate,
orders=[],
)
trade.adjust_stop_loss(trade.open_rate, self.strategy.stoploss, initial=True)
trade.set_isolated_liq(self.exchange.get_liquidation_price(
pair=pair,
open_rate=propose_rate,
amount=amount,
leverage=leverage,
is_short=is_short,
))
order = Order(
id=self.order_id_counter,
ft_trade_id=trade.id,
@ -603,8 +772,8 @@ class Backtesting:
ft_pair=trade.pair,
order_id=str(self.order_id_counter),
symbol=trade.pair,
ft_order_side="buy",
side="buy",
ft_order_side=trade.enter_side,
side=trade.enter_side,
order_type=order_type,
status="open",
order_date=current_time,
@ -635,13 +804,13 @@ class Backtesting:
for pair in open_trades.keys():
if len(open_trades[pair]) > 0:
for trade in open_trades[pair]:
if trade.open_order_id and trade.nr_of_successful_buys == 0:
if trade.open_order_id and trade.nr_of_successful_entries == 0:
# Ignore trade if buy-order did not fill yet
continue
sell_row = data[pair][-1]
trade.close_date = sell_row[DATE_IDX].to_pydatetime()
trade.sell_reason = SellType.FORCE_SELL.value
trade.sell_reason = ExitType.FORCE_SELL.value
trade.close(sell_row[OPEN_IDX], show_msg=False)
LocalTrade.close_bt_trade(trade)
# Deepcopy object to have wallets update correctly
@ -658,6 +827,20 @@ class Backtesting:
self.rejected_trades += 1
return False
def check_for_trade_entry(self, row) -> Optional[str]:
enter_long = row[LONG_IDX] == 1
exit_long = row[ELONG_IDX] == 1
enter_short = self._can_short and row[SHORT_IDX] == 1
exit_short = self._can_short and row[ESHORT_IDX] == 1
if enter_long == 1 and not any([exit_long, enter_short]):
# Long
return 'long'
if enter_short == 1 and not any([exit_short, enter_long]):
# Short
return 'short'
return None
def run_protections(self, enable_protections, pair: str, current_time: datetime):
if enable_protections:
self.protections.stop_per_pair(pair, current_time)
@ -670,19 +853,19 @@ class Backtesting:
"""
for order in [o for o in trade.orders if o.ft_is_open]:
timedout = self.strategy.ft_check_timed_out(order.side, trade, order, current_time)
timedout = self.strategy.ft_check_timed_out(trade, order, current_time)
if timedout:
if order.side == 'buy':
if order.side == trade.enter_side:
self.timedout_entry_orders += 1
if trade.nr_of_successful_buys == 0:
# Remove trade due to buy timeout expiration.
if trade.nr_of_successful_entries == 0:
# Remove trade due to entry timeout expiration.
return True
else:
# Close additional buy order
del trade.orders[trade.orders.index(order)]
if order.side == 'sell':
if order.side == trade.exit_side:
self.timedout_exit_orders += 1
# Close sell order and retry selling on next signal.
# Close exit order and retry exiting on next signal.
del trade.orders[trade.orders.index(order)]
return False
@ -755,19 +938,27 @@ class Backtesting:
indexes[pair] = row_index
self.dataprovider._set_dataframe_max_index(row_index)
# 1. Process buys.
for t in list(open_trades[pair]):
# 1. Cancel expired buy/sell orders.
if self.check_order_cancel(t, current_time):
# Close trade due to buy timeout expiration.
open_trade_count -= 1
open_trades[pair].remove(t)
self.wallets.update()
# 2. Process buys.
# without positionstacking, we can only have one open trade per pair.
# max_open_trades must be respected
# don't open on the last row
trade_dir = self.check_for_trade_entry(row)
if (
(position_stacking or len(open_trades[pair]) == 0)
and self.trade_slot_available(max_open_trades, open_trade_count_start)
and current_time != end_date
and row[BUY_IDX] == 1
and row[SELL_IDX] != 1
and trade_dir is not None
and not PairLocks.is_pair_locked(pair, row[DATE_IDX])
):
trade = self._enter_trade(pair, row)
trade = self._enter_trade(pair, row, trade_dir)
if trade:
# TODO: hacky workaround to avoid opening > max_open_trades
# This emulates previous behavior - not sure if this is correct
@ -778,20 +969,20 @@ class Backtesting:
open_trades[pair].append(trade)
for trade in list(open_trades[pair]):
# 2. Process buy orders.
order = trade.select_order('buy', is_open=True)
# 3. Process entry orders.
order = trade.select_order(trade.enter_side, is_open=True)
if order and self._get_order_filled(order.price, row):
order.close_bt_order(current_time)
trade.open_order_id = None
LocalTrade.add_bt_trade(trade)
self.wallets.update()
# 3. Create sell orders (if any)
# 4. Create sell orders (if any)
if not trade.open_order_id:
self._get_sell_trade_entry(trade, row) # Place sell order if necessary
# 4. Process sell orders.
order = trade.select_order('sell', is_open=True)
# 5. Process sell orders.
order = trade.select_order(trade.exit_side, is_open=True)
if order and self._get_order_filled(order.price, row):
trade.open_order_id = None
trade.close_date = current_time
@ -805,13 +996,6 @@ class Backtesting:
self.wallets.update()
self.run_protections(enable_protections, pair, current_time)
# 5. Cancel expired buy/sell orders.
if self.check_order_cancel(trade, current_time):
# Close trade due to buy timeout expiration.
open_trade_count -= 1
open_trades[pair].remove(trade)
self.wallets.update()
# Move time one configured time_interval ahead.
self.progress.increment()
current_time += timedelta(minutes=self.timeframe_min)

View File

@ -115,9 +115,7 @@ class Hyperopt:
if HyperoptTools.has_space(self.config, 'sell'):
# Make sure use_sell_signal is enabled
if 'ask_strategy' not in self.config:
self.config['ask_strategy'] = {}
self.config['ask_strategy']['use_sell_signal'] = True
self.config['use_sell_signal'] = True
self.print_all = self.config.get('print_all', False)
self.hyperopt_table_header = 0
@ -396,6 +394,7 @@ class Hyperopt:
def prepare_hyperopt_data(self) -> None:
data, timerange = self.backtesting.load_bt_data()
self.backtesting.load_bt_data_detail()
logger.info("Dataload complete. Calculating indicators")
preprocessed = self.backtesting.strategy.advise_all_indicators(data)

View File

@ -372,20 +372,20 @@ def generate_strategy_stats(pairlist: List[str],
return {}
config = content['config']
max_open_trades = min(config['max_open_trades'], len(pairlist))
starting_balance = config['dry_run_wallet']
start_balance = config['dry_run_wallet']
stake_currency = config['stake_currency']
pair_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
starting_balance=starting_balance,
starting_balance=start_balance,
results=results, skip_nan=False)
buy_tag_results = generate_tag_metrics("buy_tag", starting_balance=starting_balance,
enter_tag_results = generate_tag_metrics("enter_tag", starting_balance=start_balance,
results=results, skip_nan=False)
sell_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
exit_reason_stats = generate_sell_reason_stats(max_open_trades=max_open_trades,
results=results)
left_open_results = generate_pair_metrics(pairlist, stake_currency=stake_currency,
starting_balance=starting_balance,
starting_balance=start_balance,
results=results.loc[results['is_open']],
skip_nan=True)
daily_stats = generate_daily_stats(results)
@ -405,18 +405,24 @@ def generate_strategy_stats(pairlist: List[str],
'best_pair': best_pair,
'worst_pair': worst_pair,
'results_per_pair': pair_results,
'results_per_buy_tag': buy_tag_results,
'sell_reason_summary': sell_reason_stats,
'results_per_enter_tag': enter_tag_results,
'sell_reason_summary': exit_reason_stats,
'left_open_trades': left_open_results,
# 'days_breakdown_stats': days_breakdown_stats,
'total_trades': len(results),
'trade_count_long': len(results.loc[~results['is_short']]),
'trade_count_short': len(results.loc[results['is_short']]),
'total_volume': float(results['stake_amount'].sum()),
'avg_stake_amount': results['stake_amount'].mean() if len(results) > 0 else 0,
'profit_mean': results['profit_ratio'].mean() if len(results) > 0 else 0,
'profit_median': results['profit_ratio'].median() if len(results) > 0 else 0,
'profit_total': results['profit_abs'].sum() / starting_balance,
'profit_total': results['profit_abs'].sum() / start_balance,
'profit_total_long': results.loc[~results['is_short'], 'profit_abs'].sum() / start_balance,
'profit_total_short': results.loc[results['is_short'], 'profit_abs'].sum() / start_balance,
'profit_total_abs': results['profit_abs'].sum(),
'profit_total_long_abs': results.loc[~results['is_short'], 'profit_abs'].sum(),
'profit_total_short_abs': results.loc[results['is_short'], 'profit_abs'].sum(),
'backtest_start': min_date.strftime(DATETIME_PRINT_FORMAT),
'backtest_start_ts': int(min_date.timestamp() * 1000),
'backtest_end': max_date.strftime(DATETIME_PRINT_FORMAT),
@ -432,8 +438,8 @@ def generate_strategy_stats(pairlist: List[str],
'stake_amount': config['stake_amount'],
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'starting_balance': starting_balance,
'dry_run_wallet': starting_balance,
'starting_balance': start_balance,
'dry_run_wallet': start_balance,
'final_balance': content['final_balance'],
'rejected_signals': content['rejected_signals'],
'timedout_entry_orders': content['timedout_entry_orders'],
@ -467,7 +473,7 @@ def generate_strategy_stats(pairlist: List[str],
results, value_col='profit_ratio')
(drawdown_abs, drawdown_start, drawdown_end, high_val, low_val,
max_drawdown) = calculate_max_drawdown(
results, value_col='profit_abs', starting_balance=starting_balance)
results, value_col='profit_abs', starting_balance=start_balance)
strat_stats.update({
'max_drawdown': max_drawdown_legacy, # Deprecated - do not use
'max_drawdown_account': max_drawdown,
@ -481,7 +487,7 @@ def generate_strategy_stats(pairlist: List[str],
'max_drawdown_high': high_val,
})
csum_min, csum_max = calculate_csum(results, starting_balance)
csum_min, csum_max = calculate_csum(results, start_balance)
strat_stats.update({
'csum_min': csum_min,
'csum_max': csum_max
@ -566,16 +572,16 @@ def text_table_bt_results(pair_results: List[Dict[str, Any]], stake_currency: st
floatfmt=floatfmt, tablefmt="orgtbl", stralign="right")
def text_table_sell_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
def text_table_exit_reason(sell_reason_stats: List[Dict[str, Any]], stake_currency: str) -> str:
"""
Generate small table outlining Backtest results
:param sell_reason_stats: Sell reason metrics
:param sell_reason_stats: Exit reason metrics
:param stake_currency: Stakecurrency used
:return: pretty printed table with tabulate as string
"""
headers = [
'Sell Reason',
'Sells',
'Exit Reason',
'Exits',
'Win Draws Loss Win%',
'Avg Profit %',
'Cum Profit %',
@ -600,7 +606,7 @@ def text_table_tags(tag_type: str, tag_results: List[Dict[str, Any]], stake_curr
:param stake_currency: stake-currency - used to correctly name headers
:return: pretty printed table with tabulate as string
"""
if(tag_type == "buy_tag"):
if(tag_type == "enter_tag"):
headers = _get_line_header("TAG", stake_currency)
else:
headers = _get_line_header("TAG", stake_currency, 'Sells')
@ -686,6 +692,19 @@ def text_table_add_metrics(strat_results: Dict) -> str:
best_trade = max(strat_results['trades'], key=lambda x: x['profit_ratio'])
worst_trade = min(strat_results['trades'], key=lambda x: x['profit_ratio'])
short_metrics = [
('', ''), # Empty line to improve readability
('Long / Short',
f"{strat_results.get('trade_count_long', 'total_trades')} / "
f"{strat_results.get('trade_count_short', 0)}"),
('Total profit Long %', f"{strat_results['profit_total_long']:.2%}"),
('Total profit Short %', f"{strat_results['profit_total_short']:.2%}"),
('Absolute profit Long', round_coin_value(strat_results['profit_total_long_abs'],
strat_results['stake_currency'])),
('Absolute profit Short', round_coin_value(strat_results['profit_total_short_abs'],
strat_results['stake_currency'])),
] if strat_results.get('trade_count_short', 0) > 0 else []
# Newly added fields should be ignored if they are missing in strat_results. hyperopt-show
# command stores these results and newer version of freqtrade must be able to handle old
# results with missing new fields.
@ -696,6 +715,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
('', ''), # Empty line to improve readability
('Total/Daily Avg Trades',
f"{strat_results['total_trades']} / {strat_results['trades_per_day']}"),
('Starting balance', round_coin_value(strat_results['starting_balance'],
strat_results['stake_currency'])),
('Final balance', round_coin_value(strat_results['final_balance'],
@ -710,6 +730,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
strat_results['stake_currency'])),
('Total trade volume', round_coin_value(strat_results['total_volume'],
strat_results['stake_currency'])),
*short_metrics,
('', ''), # Empty line to improve readability
('Best Pair', f"{strat_results['best_pair']['key']} "
f"{strat_results['best_pair']['profit_sum']:.2%}"),
@ -727,7 +748,7 @@ def text_table_add_metrics(strat_results: Dict) -> str:
f"{strat_results['draw_days']} / {strat_results['losing_days']}"),
('Avg. Duration Winners', f"{strat_results['winner_holding_avg']}"),
('Avg. Duration Loser', f"{strat_results['loser_holding_avg']}"),
('Rejected Buy signals', strat_results.get('rejected_signals', 'N/A')),
('Rejected Entry signals', strat_results.get('rejected_signals', 'N/A')),
('Entry/Exit Timeouts',
f"{strat_results.get('timedout_entry_orders', 'N/A')} / "
f"{strat_results.get('timedout_exit_orders', 'N/A')}"),
@ -780,20 +801,22 @@ def show_backtest_result(strategy: str, results: Dict[str, Any], stake_currency:
print(' BACKTESTING REPORT '.center(len(table.splitlines()[0]), '='))
print(table)
if results.get('results_per_buy_tag') is not None:
if (results.get('results_per_enter_tag') is not None
or results.get('results_per_buy_tag') is not None):
# results_per_buy_tag is deprecated and should be removed 2 versions after short golive.
table = text_table_tags(
"buy_tag",
results['results_per_buy_tag'],
"enter_tag",
results.get('results_per_enter_tag', results.get('results_per_buy_tag')),
stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' BUY TAG STATS '.center(len(table.splitlines()[0]), '='))
print(' ENTER TAG STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_sell_reason(sell_reason_stats=results['sell_reason_summary'],
table = text_table_exit_reason(sell_reason_stats=results['sell_reason_summary'],
stake_currency=stake_currency)
if isinstance(table, str) and len(table) > 0:
print(' SELL REASON STATS '.center(len(table.splitlines()[0]), '='))
print(' EXIT REASON STATS '.center(len(table.splitlines()[0]), '='))
print(table)
table = text_table_bt_results(results['left_open_trades'], stake_currency=stake_currency)

View File

@ -76,7 +76,23 @@ def migrate_trades_and_orders_table(
min_rate = get_column_def(cols, 'min_rate', 'null')
sell_reason = get_column_def(cols, 'sell_reason', 'null')
strategy = get_column_def(cols, 'strategy', 'null')
buy_tag = get_column_def(cols, 'buy_tag', 'null')
enter_tag = get_column_def(cols, 'buy_tag', get_column_def(cols, 'enter_tag', 'null'))
trading_mode = get_column_def(cols, 'trading_mode', 'null')
# Leverage Properties
leverage = get_column_def(cols, 'leverage', '1.0')
liquidation_price = get_column_def(cols, 'liquidation_price',
get_column_def(cols, 'isolated_liq', 'null'))
# sqlite does not support literals for booleans
is_short = get_column_def(cols, 'is_short', '0')
# Margin Properties
interest_rate = get_column_def(cols, 'interest_rate', '0.0')
# Futures properties
funding_fees = get_column_def(cols, 'funding_fees', '0.0')
# If ticker-interval existed use that, else null.
if has_column(cols, 'ticker_interval'):
timeframe = get_column_def(cols, 'timeframe', 'ticker_interval')
@ -120,8 +136,10 @@ def migrate_trades_and_orders_table(
stake_amount, amount, amount_requested, open_date, close_date, open_order_id,
stop_loss, stop_loss_pct, initial_stop_loss, initial_stop_loss_pct,
stoploss_order_id, stoploss_last_update,
max_rate, min_rate, sell_reason, sell_order_status, strategy, buy_tag,
timeframe, open_trade_value, close_profit_abs
max_rate, min_rate, sell_reason, sell_order_status, strategy, enter_tag,
timeframe, open_trade_value, close_profit_abs,
trading_mode, leverage, liquidation_price, is_short,
interest_rate, funding_fees
)
select id, lower(exchange), pair,
is_open, {fee_open} fee_open, {fee_open_cost} fee_open_cost,
@ -136,8 +154,11 @@ def migrate_trades_and_orders_table(
{stoploss_order_id} stoploss_order_id, {stoploss_last_update} stoploss_last_update,
{max_rate} max_rate, {min_rate} min_rate, {sell_reason} sell_reason,
{sell_order_status} sell_order_status,
{strategy} strategy, {buy_tag} buy_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs
{strategy} strategy, {enter_tag} enter_tag, {timeframe} timeframe,
{open_trade_value} open_trade_value, {close_profit_abs} close_profit_abs,
{trading_mode} trading_mode, {leverage} leverage, {liquidation_price} liquidation_price,
{is_short} is_short, {interest_rate} interest_rate,
{funding_fees} funding_fees
from {trade_back_name}
"""))
@ -176,12 +197,12 @@ def migrate_orders_table(engine, table_back_name: str, cols_order: List):
ft_fee_base = get_column_def(cols_order, 'ft_fee_base', 'null')
average = get_column_def(cols_order, 'average', 'null')
# let SQLAlchemy create the schema as required
# sqlite does not support literals for booleans
with engine.begin() as connection:
connection.execute(text(f"""
insert into orders ( id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, average, remaining,
cost, order_date, order_filled_date, order_update_date, ft_fee_base)
status, symbol, order_type, side, price, amount, filled, average, remaining, cost,
order_date, order_filled_date, order_update_date, ft_fee_base)
select id, ft_trade_id, ft_order_side, ft_pair, ft_is_open, order_id,
status, symbol, order_type, side, price, amount, filled, {average} average, remaining,
cost, order_date, order_filled_date, order_update_date, {ft_fee_base} ft_fee_base
@ -211,8 +232,9 @@ def check_migrate(engine, decl_base, previous_tables) -> None:
# Check if migration necessary
# Migrates both trades and orders table!
# if not has_column(cols, 'buy_tag'):
if 'orders' not in previous_tables or not has_column(cols_orders, 'ft_fee_base'):
# if ('orders' not in previous_tables
# or not has_column(cols_orders, 'leverage')):
if not has_column(cols, 'liquidation_price'):
logger.info(f"Running database migration for trades - "
f"backup: {table_back_name}, {order_table_bak_name}")
migrate_trades_and_orders_table(

View File

@ -6,7 +6,7 @@ from datetime import datetime, timedelta, timezone
from decimal import Decimal
from typing import Any, Dict, List, Optional
from sqlalchemy import (Boolean, Column, DateTime, Float, ForeignKey, Integer, String,
from sqlalchemy import (Boolean, Column, DateTime, Enum, Float, ForeignKey, Integer, String,
create_engine, desc, func, inspect)
from sqlalchemy.exc import NoSuchModuleError
from sqlalchemy.orm import Query, declarative_base, relationship, scoped_session, sessionmaker
@ -14,8 +14,9 @@ from sqlalchemy.pool import StaticPool
from sqlalchemy.sql.schema import UniqueConstraint
from freqtrade.constants import DATETIME_PRINT_FORMAT, NON_OPEN_EXCHANGE_STATES
from freqtrade.enums import SellType
from freqtrade.enums import ExitType, TradingMode
from freqtrade.exceptions import DependencyException, OperationalException
from freqtrade.leverage import interest
from freqtrade.persistence.migrations import check_migrate
@ -181,6 +182,7 @@ class Order(_DECL_BASE):
self.average = order.get('average', self.average)
self.remaining = order.get('remaining', self.remaining)
self.cost = order.get('cost', self.cost)
if 'timestamp' in order and order['timestamp'] is not None:
self.order_date = datetime.fromtimestamp(order['timestamp'] / 1000, tz=timezone.utc)
@ -191,7 +193,7 @@ class Order(_DECL_BASE):
self.order_filled_date = datetime.now(timezone.utc)
self.order_update_date = datetime.now(timezone.utc)
def to_json(self) -> Dict[str, Any]:
def to_json(self, entry_side: str) -> Dict[str, Any]:
return {
'pair': self.ft_pair,
'order_id': self.order_id,
@ -213,6 +215,7 @@ class Order(_DECL_BASE):
tzinfo=timezone.utc).timestamp() * 1000) if self.order_filled_date else None,
'order_type': self.order_type,
'price': self.price,
'ft_is_entry': self.ft_order_side == entry_side,
'remaining': self.remaining,
}
@ -316,19 +319,48 @@ class LocalTrade():
sell_reason: str = ''
sell_order_status: str = ''
strategy: str = ''
buy_tag: Optional[str] = None
enter_tag: Optional[str] = None
timeframe: Optional[int] = None
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
self.recalc_open_trade_value()
trading_mode: TradingMode = TradingMode.SPOT
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
# Leverage trading properties
liquidation_price: Optional[float] = None
is_short: bool = False
leverage: float = 1.0
return (f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})')
# Margin trading properties
interest_rate: float = 0.0
# Futures properties
funding_fees: Optional[float] = None
@property
def buy_tag(self) -> Optional[str]:
"""
Compatibility between buy_tag (old) and enter_tag (new)
Consider buy_tag deprecated
"""
return self.enter_tag
@property
def has_no_leverage(self) -> bool:
"""Returns true if this is a non-leverage, non-short trade"""
return ((self.leverage == 1.0 or self.leverage is None) and not self.is_short)
@property
def borrowed(self) -> float:
"""
The amount of currency borrowed from the exchange for leverage trades
If a long trade, the amount is in base currency
If a short trade, the amount is in the other currency being traded
"""
if self.has_no_leverage:
return 0.0
elif not self.is_short:
return (self.amount * self.open_rate) * ((self.leverage-1)/self.leverage)
else:
return self.amount
@property
def open_date_utc(self):
@ -338,9 +370,49 @@ class LocalTrade():
def close_date_utc(self):
return self.close_date.replace(tzinfo=timezone.utc)
@property
def enter_side(self) -> str:
if self.is_short:
return "sell"
else:
return "buy"
@property
def exit_side(self) -> str:
if self.is_short:
return "buy"
else:
return "sell"
@property
def trade_direction(self) -> str:
if self.is_short:
return "short"
else:
return "long"
def __init__(self, **kwargs):
for key in kwargs:
setattr(self, key, kwargs[key])
self.recalc_open_trade_value()
if self.trading_mode == TradingMode.MARGIN and self.interest_rate is None:
raise OperationalException(
f"{self.trading_mode.value} trading requires param interest_rate on trades")
def __repr__(self):
open_since = self.open_date.strftime(DATETIME_PRINT_FORMAT) if self.is_open else 'closed'
leverage = self.leverage or 1.0
is_short = self.is_short or False
return (
f'Trade(id={self.id}, pair={self.pair}, amount={self.amount:.8f}, '
f'is_short={is_short}, leverage={leverage}, '
f'open_rate={self.open_rate:.8f}, open_since={open_since})'
)
def to_json(self) -> Dict[str, Any]:
filled_orders = self.select_filled_orders()
orders = [order.to_json() for order in filled_orders]
orders = [order.to_json(self.enter_side) for order in filled_orders]
return {
'trade_id': self.id,
@ -351,7 +423,8 @@ class LocalTrade():
'amount_requested': round(self.amount_requested, 8) if self.amount_requested else None,
'stake_amount': round(self.stake_amount, 8),
'strategy': self.strategy,
'buy_tag': self.buy_tag,
'buy_tag': self.enter_tag,
'enter_tag': self.enter_tag,
'timeframe': self.timeframe,
'fee_open': self.fee_open,
@ -404,6 +477,12 @@ class LocalTrade():
'min_rate': self.min_rate,
'max_rate': self.max_rate,
'leverage': self.leverage,
'interest_rate': self.interest_rate,
'liquidation_price': self.liquidation_price,
'is_short': self.is_short,
'trading_mode': self.trading_mode,
'funding_fees': self.funding_fees,
'open_order_id': self.open_order_id,
'orders': orders,
}
@ -424,10 +503,33 @@ class LocalTrade():
self.max_rate = max(current_price, self.max_rate or self.open_rate)
self.min_rate = min(current_price_low, self.min_rate or self.open_rate)
def _set_new_stoploss(self, new_loss: float, stoploss: float):
"""Assign new stop value"""
self.stop_loss = new_loss
self.stop_loss_pct = -1 * abs(stoploss)
def set_isolated_liq(self, liquidation_price: Optional[float]):
"""
Method you should use to set self.liquidation price.
Assures stop_loss is not passed the liquidation price
"""
if not liquidation_price:
return
self.liquidation_price = liquidation_price
def _set_stop_loss(self, stop_loss: float, percent: float):
"""
Method you should use to set self.stop_loss.
Assures stop_loss is not passed the liquidation price
"""
if self.liquidation_price is not None:
if self.is_short:
sl = min(stop_loss, self.liquidation_price)
else:
sl = max(stop_loss, self.liquidation_price)
else:
sl = stop_loss
if not self.stop_loss:
self.initial_stop_loss = sl
self.stop_loss = sl
self.stop_loss_pct = -1 * abs(percent)
self.stoploss_last_update = datetime.utcnow()
def adjust_stop_loss(self, current_price: float, stoploss: float,
@ -443,27 +545,43 @@ class LocalTrade():
# Don't modify if called with initial and nothing to do
return
new_loss = float(current_price * (1 - abs(stoploss)))
leverage = self.leverage or 1.0
if self.is_short:
new_loss = float(current_price * (1 + abs(stoploss / leverage)))
# If trading with leverage, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = min(self.liquidation_price, new_loss)
else:
new_loss = float(current_price * (1 - abs(stoploss / leverage)))
# If trading with leverage, don't set the stoploss below the liquidation price
if self.liquidation_price:
new_loss = max(self.liquidation_price, new_loss)
# no stop loss assigned yet
# if not self.stop_loss:
if self.initial_stop_loss_pct is None:
logger.debug(f"{self.pair} - Assigning new stoploss...")
self._set_new_stoploss(new_loss, stoploss)
self._set_stop_loss(new_loss, stoploss)
self.initial_stop_loss = new_loss
self.initial_stop_loss_pct = -1 * abs(stoploss)
# evaluate if the stop loss needs to be updated
else:
if new_loss > self.stop_loss: # stop losses only walk up, never down!
higher_stop = new_loss > self.stop_loss
lower_stop = new_loss < self.stop_loss
# stop losses only walk up, never down!,
# ? But adding more to a leveraged trade would create a lower liquidation price,
# ? decreasing the minimum stoploss
if (higher_stop and not self.is_short) or (lower_stop and self.is_short):
logger.debug(f"{self.pair} - Adjusting stoploss...")
self._set_new_stoploss(new_loss, stoploss)
self._set_stop_loss(new_loss, stoploss)
else:
logger.debug(f"{self.pair} - Keeping current stoploss...")
logger.debug(
f"{self.pair} - Stoploss adjusted. current_price={current_price:.8f}, "
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate:.8f}, "
f"open_rate={self.open_rate:.8f}, max_rate={self.max_rate or self.open_rate:.8f}, "
f"initial_stop_loss={self.initial_stop_loss:.8f}, "
f"stop_loss={self.stop_loss:.8f}. "
f"Trailing stoploss saved us: "
@ -475,28 +593,32 @@ class LocalTrade():
:param order: order retrieved by exchange.fetch_order()
:return: None
"""
# Ignore open and cancelled orders
if order.status == 'open' or order.safe_price is None:
return
logger.info(f'Updating trade (id={self.id}) ...')
if order.ft_order_side == 'buy':
if order.ft_order_side == self.enter_side:
# Update open rate and actual amount
self.open_rate = order.safe_price
self.amount = order.safe_amount_after_fee
if self.is_open:
logger.info(f'{order.order_type.upper()}_BUY has been fulfilled for {self}.')
payment = "SELL" if self.is_short else "BUY"
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
self.open_order_id = None
self.recalc_trade_from_orders()
elif order.ft_order_side == 'sell':
elif order.ft_order_side == self.exit_side:
if self.is_open:
logger.info(f'{order.order_type.upper()}_SELL has been fulfilled for {self}.')
payment = "BUY" if self.is_short else "SELL"
# * On margin shorts, you buy a little bit more than the amount (amount + interest)
logger.info(f'{order.order_type.upper()}_{payment} has been fulfilled for {self}.')
self.close(order.safe_price)
elif order.ft_order_side == 'stoploss':
self.stoploss_order_id = None
self.close_rate_requested = self.stop_loss
self.sell_reason = SellType.STOPLOSS_ON_EXCHANGE.value
self.sell_reason = ExitType.STOPLOSS_ON_EXCHANGE.value
if self.is_open:
logger.info(f'{order.order_type.upper()} is hit for {self}.')
self.close(order.safe_price)
@ -510,9 +632,9 @@ class LocalTrade():
and marks trade as closed
"""
self.close_rate = rate
self.close_date = self.close_date or datetime.utcnow()
self.close_profit = self.calc_profit_ratio()
self.close_profit_abs = self.calc_profit()
self.close_date = self.close_date or datetime.utcnow()
self.is_open = False
self.sell_order_status = 'closed'
self.open_order_id = None
@ -527,14 +649,14 @@ class LocalTrade():
"""
Update Fee parameters. Only acts once per side
"""
if side == 'buy' and self.fee_open_currency is None:
if self.enter_side == side and self.fee_open_currency is None:
self.fee_open_cost = fee_cost
self.fee_open_currency = fee_currency
if fee_rate is not None:
self.fee_open = fee_rate
# Assume close-fee will fall into the same fee category and take an educated guess
self.fee_close = fee_rate
elif side == 'sell' and self.fee_close_currency is None:
elif self.exit_side == side and self.fee_close_currency is None:
self.fee_close_cost = fee_cost
self.fee_close_currency = fee_currency
if fee_rate is not None:
@ -544,9 +666,9 @@ class LocalTrade():
"""
Verify if this side (buy / sell) has already been updated
"""
if side == 'buy':
if self.enter_side == side:
return self.fee_open_currency is not None
elif side == 'sell':
elif self.exit_side == side:
return self.fee_close_currency is not None
else:
return False
@ -559,79 +681,167 @@ class LocalTrade():
Get amount of failed exiting orders
assumes full exits.
"""
return len([o for o in self.orders if o.ft_order_side == 'sell'])
return len([o for o in self.orders if o.ft_order_side == self.exit_side])
def _calc_open_trade_value(self) -> float:
"""
Calculate the open_rate including open_fee.
:return: Price in of the open trade incl. Fees
"""
buy_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = buy_trade * Decimal(self.fee_open)
return float(buy_trade + fees)
open_trade = Decimal(self.amount) * Decimal(self.open_rate)
fees = open_trade * Decimal(self.fee_open)
if self.is_short:
return float(open_trade - fees)
else:
return float(open_trade + fees)
def recalc_open_trade_value(self) -> None:
"""
Recalculate open_trade_value.
Must be called whenever open_rate or fee_open is changed.
Must be called whenever open_rate, fee_open or is_short is changed.
"""
self.open_trade_value = self._calc_open_trade_value()
def calculate_interest(self, interest_rate: Optional[float] = None) -> Decimal:
"""
:param interest_rate: interest_charge for borrowing this coin(optional).
If interest_rate is not set self.interest_rate will be used
"""
zero = Decimal(0.0)
# If nothing was borrowed
if self.trading_mode != TradingMode.MARGIN or self.has_no_leverage:
return zero
open_date = self.open_date.replace(tzinfo=None)
now = (self.close_date or datetime.now(timezone.utc)).replace(tzinfo=None)
sec_per_hour = Decimal(3600)
total_seconds = Decimal((now - open_date).total_seconds())
hours = total_seconds/sec_per_hour or zero
rate = Decimal(interest_rate or self.interest_rate)
borrowed = Decimal(self.borrowed)
return interest(exchange_name=self.exchange, borrowed=borrowed, rate=rate, hours=hours)
def _calc_base_close(self, amount: Decimal, rate: Optional[float] = None,
fee: Optional[float] = None) -> Decimal:
close_trade = Decimal(amount) * Decimal(rate or self.close_rate) # type: ignore
fees = close_trade * Decimal(fee or self.fee_close)
if self.is_short:
return close_trade + fees
else:
return close_trade - fees
def calc_close_trade_value(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the close_rate including fee
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: Price in BTC of the open trade
"""
if rate is None and not self.close_rate:
return 0.0
sell_trade = Decimal(self.amount) * Decimal(rate or self.close_rate) # type: ignore
fees = sell_trade * Decimal(fee or self.fee_close)
return float(sell_trade - fees)
amount = Decimal(self.amount)
trading_mode = self.trading_mode or TradingMode.SPOT
if trading_mode == TradingMode.SPOT:
return float(self._calc_base_close(amount, rate, fee))
elif (trading_mode == TradingMode.MARGIN):
total_interest = self.calculate_interest(interest_rate)
if self.is_short:
amount = amount + total_interest
return float(self._calc_base_close(amount, rate, fee))
else:
# Currency already owned for longs, no need to purchase
return float(self._calc_base_close(amount, rate, fee) - total_interest)
elif (trading_mode == TradingMode.FUTURES):
funding_fees = self.funding_fees or 0.0
# Positive funding_fees -> Trade has gained from fees.
# Negative funding_fees -> Trade had to pay the fees.
if self.is_short:
return float(self._calc_base_close(amount, rate, fee)) - funding_fees
else:
return float(self._calc_base_close(amount, rate, fee)) + funding_fees
else:
raise OperationalException(
f"{self.trading_mode.value} trading is not yet available using freqtrade")
def calc_profit(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculate the absolute profit in stake currency between Close and Open trade
:param fee: fee to use on the close rate (optional).
If rate is not set self.fee will be used
If fee is not set self.fee will be used
:param rate: close rate to compare with (optional).
If rate is not set self.close_rate will be used
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit in stake currency as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.is_short:
profit = self.open_trade_value - close_trade_value
else:
profit = close_trade_value - self.open_trade_value
return float(f"{profit:.8f}")
def calc_profit_ratio(self, rate: Optional[float] = None,
fee: Optional[float] = None) -> float:
fee: Optional[float] = None,
interest_rate: Optional[float] = None) -> float:
"""
Calculates the profit as ratio (including fee).
:param rate: rate to compare with (optional).
If rate is not set self.close_rate will be used
:param fee: fee to use on the close rate (optional).
:param interest_rate: interest_charge for borrowing this coin (optional).
If interest_rate is not set self.interest_rate will be used
:return: profit ratio as float
"""
close_trade_value = self.calc_close_trade_value(
rate=(rate or self.close_rate),
fee=(fee or self.fee_close)
fee=(fee or self.fee_close),
interest_rate=(interest_rate or self.interest_rate)
)
if self.open_trade_value == 0.0:
short_close_zero = (self.is_short and close_trade_value == 0.0)
long_close_zero = (not self.is_short and self.open_trade_value == 0.0)
leverage = self.leverage or 1.0
if (short_close_zero or long_close_zero):
return 0.0
profit_ratio = (close_trade_value / self.open_trade_value) - 1
else:
if self.is_short:
profit_ratio = (1 - (close_trade_value/self.open_trade_value)) * leverage
else:
profit_ratio = ((close_trade_value/self.open_trade_value) - 1) * leverage
return float(f"{profit_ratio:.8f}")
def recalc_trade_from_orders(self):
# We need at least 2 entry orders for averaging amounts and rates.
if len(self.select_filled_orders('buy')) < 2:
# TODO: this condition could probably be removed
if len(self.select_filled_orders(self.enter_side)) < 2:
self.stake_amount = self.amount * self.open_rate / self.leverage
# Just in case, still recalc open trade value
self.recalc_open_trade_value()
return
@ -640,7 +850,7 @@ class LocalTrade():
total_stake = 0.0
for o in self.orders:
if (o.ft_is_open or
(o.ft_order_side != 'buy') or
(o.ft_order_side != self.enter_side) or
(o.status not in NON_OPEN_EXCHANGE_STATES)):
continue
@ -653,8 +863,9 @@ class LocalTrade():
total_stake += tmp_price * tmp_amount
if total_amount > 0:
# Leverage not updated, as we don't allow changing leverage through DCA at the moment.
self.open_rate = total_stake / total_amount
self.stake_amount = total_stake
self.stake_amount = total_stake / (self.leverage or 1.0)
self.amount = total_amount
self.fee_open_cost = self.fee_open * self.stake_amount
self.recalc_open_trade_value()
@ -700,10 +911,28 @@ class LocalTrade():
(o.filled or 0) > 0 and
o.status in NON_OPEN_EXCHANGE_STATES]
@property
def nr_of_successful_entries(self) -> int:
"""
Helper function to count the number of entry orders that have been filled.
:return: int count of entry orders that have been filled for this trade.
"""
return len(self.select_filled_orders(self.enter_side))
@property
def nr_of_successful_exits(self) -> int:
"""
Helper function to count the number of exit orders that have been filled.
:return: int count of exit orders that have been filled for this trade.
"""
return len(self.select_filled_orders(self.exit_side))
@property
def nr_of_successful_buys(self) -> int:
"""
Helper function to count the number of buy orders that have been filled.
WARNING: Please use nr_of_successful_entries for short support.
:return: int count of buy orders that have been filled for this trade.
"""
@ -713,6 +942,7 @@ class LocalTrade():
def nr_of_successful_sells(self) -> int:
"""
Helper function to count the number of sell orders that have been filled.
WARNING: Please use nr_of_successful_exits for short support.
:return: int count of sell orders that have been filled for this trade.
"""
return len(self.select_filled_orders('sell'))
@ -849,9 +1079,22 @@ class Trade(_DECL_BASE, LocalTrade):
sell_reason = Column(String(100), nullable=True)
sell_order_status = Column(String(100), nullable=True)
strategy = Column(String(100), nullable=True)
buy_tag = Column(String(100), nullable=True)
enter_tag = Column(String(100), nullable=True)
timeframe = Column(Integer, nullable=True)
trading_mode = Column(Enum(TradingMode), nullable=True)
# Leverage trading properties
leverage = Column(Float, nullable=True, default=1.0)
is_short = Column(Boolean, nullable=False, default=False)
liquidation_price = Column(Float, nullable=True)
# Margin Trading Properties
interest_rate = Column(Float, nullable=False, default=0.0)
# Futures properties
funding_fees = Column(Float, nullable=True, default=None)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.recalc_open_trade_value()
@ -938,7 +1181,7 @@ class Trade(_DECL_BASE, LocalTrade):
]).all()
@staticmethod
def get_sold_trades_without_assigned_fees():
def get_closed_trades_without_assigned_fees():
"""
Returns all closed trades which don't have fees set correctly
NOTE: Not supported in Backtesting.
@ -1007,7 +1250,7 @@ class Trade(_DECL_BASE, LocalTrade):
]
@staticmethod
def get_buy_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
def get_enter_tag_performance(pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Returns List of dicts containing all Trades, based on buy tag performance
Can either be average for all pairs or a specific pair provided
@ -1018,25 +1261,25 @@ class Trade(_DECL_BASE, LocalTrade):
if(pair is not None):
filters.append(Trade.pair == pair)
buy_tag_perf = Trade.query.with_entities(
Trade.buy_tag,
enter_tag_perf = Trade.query.with_entities(
Trade.enter_tag,
func.sum(Trade.close_profit).label('profit_sum'),
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
func.count(Trade.pair).label('count')
).filter(*filters)\
.group_by(Trade.buy_tag) \
.group_by(Trade.enter_tag) \
.order_by(desc('profit_sum_abs')) \
.all()
return [
{
'buy_tag': buy_tag if buy_tag is not None else "Other",
'enter_tag': enter_tag if enter_tag is not None else "Other",
'profit_ratio': profit,
'profit_pct': round(profit * 100, 2),
'profit_abs': profit_abs,
'count': count
}
for buy_tag, profit, profit_abs, count in buy_tag_perf
for enter_tag, profit, profit_abs, count in enter_tag_perf
]
@staticmethod
@ -1086,7 +1329,7 @@ class Trade(_DECL_BASE, LocalTrade):
mix_tag_perf = Trade.query.with_entities(
Trade.id,
Trade.buy_tag,
Trade.enter_tag,
Trade.sell_reason,
func.sum(Trade.close_profit).label('profit_sum'),
func.sum(Trade.close_profit_abs).label('profit_sum_abs'),
@ -1097,12 +1340,12 @@ class Trade(_DECL_BASE, LocalTrade):
.all()
return_list: List[Dict] = []
for id, buy_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
buy_tag = buy_tag if buy_tag is not None else "Other"
for id, enter_tag, sell_reason, profit, profit_abs, count in mix_tag_perf:
enter_tag = enter_tag if enter_tag is not None else "Other"
sell_reason = sell_reason if sell_reason is not None else "Other"
if(sell_reason is not None and buy_tag is not None):
mix_tag = buy_tag + " " + sell_reason
if(sell_reason is not None and enter_tag is not None):
mix_tag = enter_tag + " " + sell_reason
i = 0
if not any(item["mix_tag"] == mix_tag for item in return_list):
return_list.append({'mix_tag': mix_tag,

View File

@ -237,7 +237,7 @@ def plot_trades(fig, trades: pd.DataFrame) -> make_subplots:
# Create description for sell summarizing the trade
trades['desc'] = trades.apply(
lambda row: f"{row['profit_ratio']:.2%}, " +
(f"{row['buy_tag']}, " if row['buy_tag'] is not None else "") +
(f"{row['enter_tag']}, " if row['enter_tag'] is not None else "") +
f"{row['sell_reason']}, " +
f"{row['trade_duration']} min",
axis=1)
@ -431,8 +431,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
)
fig.add_trace(candles, 1, 1)
if 'buy' in data.columns:
df_buy = data[data['buy'] == 1]
if 'enter_long' in data.columns:
df_buy = data[data['enter_long'] == 1]
if len(df_buy) > 0:
buys = go.Scatter(
x=df_buy.date,
@ -450,8 +450,8 @@ def generate_candlestick_graph(pair: str, data: pd.DataFrame, trades: pd.DataFra
else:
logger.warning("No buy-signals found.")
if 'sell' in data.columns:
df_sell = data[data['sell'] == 1]
if 'exit_long' in data.columns:
df_sell = data[data['exit_long'] == 1]
if len(df_sell) > 0:
sells = go.Scatter(
x=df_sell.date,

View File

@ -9,6 +9,7 @@ import arrow
from pandas import DataFrame
from freqtrade.configuration import PeriodicCache
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -71,8 +72,8 @@ class AgeFilter(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new allowlist
"""
needed_pairs = [
(p, '1d') for p in pairlist
needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._config['candle_type_def']) for p in pairlist
if p not in self._symbolsChecked and p not in self._symbolsCheckFailed]
if not needed_pairs:
# Remove pairs that have been removed before
@ -88,7 +89,8 @@ class AgeFilter(IPairList):
candles = self._exchange.refresh_latest_ohlcv(needed_pairs, since_ms=since_ms, cache=False)
if self._enabled:
for p in deepcopy(pairlist):
daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None
daily_candles = candles[(p, '1d', self._config['candle_type_def'])] if (
p, '1d', self._config['candle_type_def']) in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
self.log_once(f"Validated {len(pairlist)} pairs.", logger.info)

View File

@ -90,8 +90,7 @@ class PriceFilter(IPairList):
price = ticker['last']
market = self._exchange.markets[pair]
limits = market['limits']
if ('amount' in limits and 'min' in limits['amount']
and limits['amount']['min'] is not None):
if (limits['amount']['min'] is not None):
min_amount = limits['amount']['min']
min_precision = market['precision']['amount']

View File

@ -11,6 +11,7 @@ import numpy as np
from cachetools import TTLCache
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -33,6 +34,7 @@ class VolatilityFilter(IPairList):
self._min_volatility = pairlistconfig.get('min_volatility', 0)
self._max_volatility = pairlistconfig.get('max_volatility', sys.maxsize)
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
self._def_candletype = self._config['candle_type_def']
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
@ -67,7 +69,8 @@ class VolatilityFilter(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new allowlist
"""
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache]
since_ms = (arrow.utcnow()
.floor('day')
@ -81,7 +84,8 @@ class VolatilityFilter(IPairList):
if self._enabled:
for p in deepcopy(pairlist):
daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None
daily_candles = candles[(p, '1d', self._def_candletype)] if (
p, '1d', self._def_candletype) in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
return pairlist

View File

@ -9,6 +9,7 @@ from typing import Any, Dict, List
import arrow
from cachetools import TTLCache
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.exchange import timeframe_to_minutes
from freqtrade.misc import format_ms_time
@ -42,6 +43,7 @@ class VolumePairList(IPairList):
self._lookback_days = self._pairlistconfig.get('lookback_days', 0)
self._lookback_timeframe = self._pairlistconfig.get('lookback_timeframe', '1d')
self._lookback_period = self._pairlistconfig.get('lookback_period', 0)
self._def_candletype = self._config['candle_type_def']
if (self._lookback_days > 0) & (self._lookback_period > 0):
raise OperationalException(
@ -69,10 +71,13 @@ class VolumePairList(IPairList):
f'to at least {self._tf_in_sec} and restart the bot.'
)
if not self._exchange.exchange_has('fetchTickers'):
if (not self._use_range and not (
self._exchange.exchange_has('fetchTickers')
and self._exchange._ft_has["tickers_have_quoteVolume"])):
raise OperationalException(
'Exchange does not support dynamic whitelist. '
'Please edit your config and restart the bot.'
"Exchange does not support dynamic whitelist in this configuration. "
"Please edit your config and either remove Volumepairlist, "
"or switch to using candles. and restart the bot."
)
if not self._validate_keys(self._sort_key):
@ -93,7 +98,7 @@ class VolumePairList(IPairList):
If no Pairlist requires tickers, an empty Dict is passed
as tickers argument to filter_pairlist
"""
return True
return not self._use_range
def _validate_keys(self, key):
return key in SORT_VALUES
@ -121,16 +126,18 @@ class VolumePairList(IPairList):
# Check if pair quote currency equals to the stake currency.
_pairlist = [k for k in self._exchange.get_markets(
quote_currencies=[self._stake_currency],
pairs_only=True, active_only=True).keys()]
tradable_only=True, active_only=True).keys()]
# No point in testing for blacklisted pairs...
_pairlist = self.verify_blacklist(_pairlist, logger.info)
if not self._use_range:
filtered_tickers = [
v for k, v in tickers.items()
if (self._exchange.get_pair_quote_currency(k) == self._stake_currency
and (self._use_range or v[self._sort_key] is not None)
and v['symbol'] in _pairlist)]
pairlist = [s['symbol'] for s in filtered_tickers]
else:
pairlist = _pairlist
pairlist = self.filter_pairlist(pairlist, tickers)
self._pair_cache['pairlist'] = pairlist.copy()
@ -145,11 +152,11 @@ class VolumePairList(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new whitelist
"""
# Use the incoming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if self._use_range:
# Create bare minimum from tickers structure.
filtered_tickers: List[Dict[str, Any]] = [{'symbol': k} for k in pairlist]
# get lookback period in ms, for exchange ohlcv fetch
if self._use_range:
since_ms = int(arrow.utcnow()
.floor('minute')
.shift(minutes=-(self._lookback_period * self._tf_in_min)
@ -165,11 +172,10 @@ class VolumePairList(IPairList):
self.log_once(f"Using volume range of {self._lookback_period} candles, timeframe: "
f"{self._lookback_timeframe}, starting from {format_ms_time(since_ms)} "
f"till {format_ms_time(to_ms)}", logger.info)
needed_pairs = [
(p, self._lookback_timeframe) for p in
[
s['symbol'] for s in filtered_tickers
] if p not in self._pair_cache
needed_pairs: ListPairsWithTimeframes = [
(p, self._lookback_timeframe, self._def_candletype) for p in
[s['symbol'] for s in filtered_tickers]
if p not in self._pair_cache
]
# Get all candles
@ -180,8 +186,10 @@ class VolumePairList(IPairList):
)
for i, p in enumerate(filtered_tickers):
pair_candles = candles[
(p['symbol'], self._lookback_timeframe)
] if (p['symbol'], self._lookback_timeframe) in candles else None
(p['symbol'], self._lookback_timeframe, self._def_candletype)
] if (
p['symbol'], self._lookback_timeframe, self._def_candletype
) in candles else None
# in case of candle data calculate typical price and quoteVolume for candle
if pair_candles is not None and not pair_candles.empty:
if self._exchange._ft_has["ohlcv_volume_currency"] == "base":
@ -205,6 +213,9 @@ class VolumePairList(IPairList):
filtered_tickers[i]['quoteVolume'] = quoteVolume
else:
filtered_tickers[i]['quoteVolume'] = 0
else:
# Tickers mode - filter based on incomming pairlist.
filtered_tickers = [v for k, v in tickers.items() if k in pairlist]
if self._min_value > 0:
filtered_tickers = [

View File

@ -9,6 +9,7 @@ import arrow
from cachetools import TTLCache
from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.exceptions import OperationalException
from freqtrade.misc import plural
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -28,6 +29,7 @@ class RangeStabilityFilter(IPairList):
self._min_rate_of_change = pairlistconfig.get('min_rate_of_change', 0.01)
self._max_rate_of_change = pairlistconfig.get('max_rate_of_change', None)
self._refresh_period = pairlistconfig.get('refresh_period', 1440)
self._def_candletype = self._config['candle_type_def']
self._pair_cache: TTLCache = TTLCache(maxsize=1000, ttl=self._refresh_period)
@ -65,7 +67,8 @@ class RangeStabilityFilter(IPairList):
:param tickers: Tickers (from exchange.get_tickers()). May be cached.
:return: new allowlist
"""
needed_pairs = [(p, '1d') for p in pairlist if p not in self._pair_cache]
needed_pairs: ListPairsWithTimeframes = [
(p, '1d', self._def_candletype) for p in pairlist if p not in self._pair_cache]
since_ms = (arrow.utcnow()
.floor('day')
@ -79,7 +82,8 @@ class RangeStabilityFilter(IPairList):
if self._enabled:
for p in deepcopy(pairlist):
daily_candles = candles[(p, '1d')] if (p, '1d') in candles else None
daily_candles = candles[(p, '1d', self._def_candletype)] if (
p, '1d', self._def_candletype) in candles else None
if not self._validate_pair_loc(p, daily_candles):
pairlist.remove(p)
return pairlist

View File

@ -8,6 +8,7 @@ from typing import Dict, List
from cachetools import TTLCache, cached
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.enums import CandleType
from freqtrade.exceptions import OperationalException
from freqtrade.mixins import LoggingMixin
from freqtrade.plugins.pairlist.IPairList import IPairList
@ -132,7 +133,6 @@ class PairListManager(LoggingMixin):
:return: pairlist - whitelisted pairs
"""
try:
whitelist = expand_pairlist(pairlist, self._exchange.get_markets().keys(), keep_invalid)
except ValueError as err:
logger.error(f"Pair whitelist contains an invalid Wildcard: {err}")
@ -143,4 +143,10 @@ class PairListManager(LoggingMixin):
"""
Create list of pair tuples with (pair, timeframe)
"""
return [(pair, timeframe or self._config['timeframe']) for pair in pairs]
return [
(
pair,
timeframe or self._config['timeframe'],
self._config.get('candle_type_def', CandleType.SPOT)
) for pair in pairs
]

View File

@ -36,7 +36,7 @@ class MaxDrawdown(IProtection):
"""
LockReason to use
"""
return (f'{drawdown} > {self._max_allowed_drawdown} in {self.lookback_period_str}, '
return (f'{drawdown} passed {self._max_allowed_drawdown} in {self.lookback_period_str}, '
f'locking for {self.stop_duration_str}.')
def _max_drawdown(self, date_now: datetime) -> ProtectionReturn:

View File

@ -3,7 +3,7 @@ import logging
from datetime import datetime, timedelta
from typing import Any, Dict
from freqtrade.enums import SellType
from freqtrade.enums import ExitType
from freqtrade.persistence import Trade
from freqtrade.plugins.protections import IProtection, ProtectionReturn
@ -44,8 +44,8 @@ class StoplossGuard(IProtection):
# filters = [
# Trade.is_open.is_(False),
# Trade.close_date > look_back_until,
# or_(Trade.sell_reason == SellType.STOP_LOSS.value,
# and_(Trade.sell_reason == SellType.TRAILING_STOP_LOSS.value,
# or_(Trade.sell_reason == ExitType.STOP_LOSS.value,
# and_(Trade.sell_reason == ExitType.TRAILING_STOP_LOSS.value,
# Trade.close_profit < 0))
# ]
# if pair:
@ -54,8 +54,8 @@ class StoplossGuard(IProtection):
trades1 = Trade.get_trades_proxy(pair=pair, is_open=False, close_date=look_back_until)
trades = [trade for trade in trades1 if (str(trade.sell_reason) in (
SellType.TRAILING_STOP_LOSS.value, SellType.STOP_LOSS.value,
SellType.STOPLOSS_ON_EXCHANGE.value)
ExitType.TRAILING_STOP_LOSS.value, ExitType.STOP_LOSS.value,
ExitType.STOPLOSS_ON_EXCHANGE.value)
and trade.close_profit and trade.close_profit < 0)]
if len(trades) < self._trade_limit:

View File

@ -10,7 +10,9 @@ from inspect import getfullargspec
from pathlib import Path
from typing import Any, Dict, Optional
from freqtrade.configuration.config_validation import validate_migrated_strategy_settings
from freqtrade.constants import REQUIRED_ORDERTIF, REQUIRED_ORDERTYPES, USERPATH_STRATEGIES
from freqtrade.enums import TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.resolvers import IResolver
from freqtrade.strategy.interface import IStrategy
@ -147,14 +149,70 @@ class StrategyResolver(IResolver):
return strategy
@staticmethod
def _strategy_sanity_validations(strategy):
def _strategy_sanity_validations(strategy: IStrategy):
# Ensure necessary migrations are performed first.
validate_migrated_strategy_settings(strategy.config)
if not all(k in strategy.order_types for k in REQUIRED_ORDERTYPES):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-types mapping is incomplete.")
if not all(k in strategy.order_time_in_force for k in REQUIRED_ORDERTIF):
raise ImportError(f"Impossible to load Strategy '{strategy.__class__.__name__}'. "
f"Order-time-in-force mapping is incomplete.")
trading_mode = strategy.config.get('trading_mode', TradingMode.SPOT)
if (strategy.can_short and trading_mode == TradingMode.SPOT):
raise ImportError(
"Short strategies cannot run in spot markets. Please make sure that this "
"is the correct strategy and that your trading mode configuration is correct. "
"You can run this strategy in spot markets by setting `can_short=False`"
" in your strategy. Please note that short signals will be ignored in that case."
)
@staticmethod
def validate_strategy(strategy: IStrategy) -> IStrategy:
if strategy.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# Require new method
if not check_override(strategy, IStrategy, 'populate_entry_trend'):
raise OperationalException("`populate_entry_trend` must be implemented.")
if not check_override(strategy, IStrategy, 'populate_exit_trend'):
raise OperationalException("`populate_exit_trend` must be implemented.")
if check_override(strategy, IStrategy, 'check_buy_timeout'):
raise OperationalException("Please migrate your implementation "
"of `check_buy_timeout` to `check_entry_timeout`.")
if check_override(strategy, IStrategy, 'check_sell_timeout'):
raise OperationalException("Please migrate your implementation "
"of `check_sell_timeout` to `check_exit_timeout`.")
if check_override(strategy, IStrategy, 'custom_sell'):
raise OperationalException(
"Please migrate your implementation of `custom_sell` to `custom_exit`.")
else:
# TODO: Implementing one of the following methods should show a deprecation warning
# buy_trend and sell_trend, custom_sell
if (
not check_override(strategy, IStrategy, 'populate_buy_trend')
and not check_override(strategy, IStrategy, 'populate_entry_trend')
):
raise OperationalException(
"`populate_entry_trend` or `populate_buy_trend` must be implemented.")
if (
not check_override(strategy, IStrategy, 'populate_sell_trend')
and not check_override(strategy, IStrategy, 'populate_exit_trend')
):
raise OperationalException(
"`populate_exit_trend` or `populate_sell_trend` must be implemented.")
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [
strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len
]):
strategy.INTERFACE_VERSION = 1
return strategy
@staticmethod
def _load_strategy(strategy_name: str,
@ -187,23 +245,26 @@ class StrategyResolver(IResolver):
# register temp path with the bot
abs_paths.insert(0, temp.resolve())
strategy = StrategyResolver._load_object(paths=abs_paths,
strategy = StrategyResolver._load_object(
paths=abs_paths,
object_name=strategy_name,
add_source=True,
kwargs={'config': config},
)
if strategy:
strategy._populate_fun_len = len(getfullargspec(strategy.populate_indicators).args)
strategy._buy_fun_len = len(getfullargspec(strategy.populate_buy_trend).args)
strategy._sell_fun_len = len(getfullargspec(strategy.populate_sell_trend).args)
if any(x == 2 for x in [strategy._populate_fun_len,
strategy._buy_fun_len,
strategy._sell_fun_len]):
strategy.INTERFACE_VERSION = 1
return strategy
if strategy:
return StrategyResolver.validate_strategy(strategy)
raise OperationalException(
f"Impossible to load Strategy '{strategy_name}'. This class does not exist "
"or contains Python code errors."
)
def check_override(object, parentclass, attribute):
"""
Checks if a object overrides the parent class attribute.
:returns: True if the object is overridden.
"""
return getattr(type(object), attribute) != getattr(parentclass, attribute)

View File

@ -4,7 +4,7 @@ from typing import Any, Dict, List, Optional, Union
from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT
from freqtrade.enums import OrderTypeValues
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
class Ping(BaseModel):
@ -38,6 +38,11 @@ class Balance(BaseModel):
used: float
est_stake: float
stake: str
# Starting with 2.x
side: str
leverage: float
is_position: bool
position: float
class Balances(BaseModel):
@ -126,18 +131,18 @@ class Daily(BaseModel):
class UnfilledTimeout(BaseModel):
buy: Optional[int]
sell: Optional[int]
entry: Optional[int]
exit: Optional[int]
unit: Optional[str]
exit_timeout_count: Optional[int]
class OrderTypes(BaseModel):
buy: OrderTypeValues
sell: OrderTypeValues
emergencysell: Optional[OrderTypeValues]
forcesell: Optional[OrderTypeValues]
forcebuy: Optional[OrderTypeValues]
entry: OrderTypeValues
exit: OrderTypeValues
emergencyexit: Optional[OrderTypeValues]
forceexit: Optional[OrderTypeValues]
forceentry: Optional[OrderTypeValues]
stoploss: OrderTypeValues
stoploss_on_exchange: bool
stoploss_on_exchange_interval: Optional[int]
@ -148,6 +153,8 @@ class ShowConfig(BaseModel):
strategy_version: Optional[str]
api_version: float
dry_run: bool
trading_mode: str
short_allowed: bool
stake_currency: str
stake_amount: str
available_capital: Optional[float]
@ -168,8 +175,8 @@ class ShowConfig(BaseModel):
exchange: str
strategy: Optional[str]
forcebuy_enabled: bool
ask_strategy: Dict[str, Any]
bid_strategy: Dict[str, Any]
exit_pricing: Dict[str, Any]
entry_pricing: Dict[str, Any]
bot_name: str
state: str
runmode: str
@ -197,12 +204,14 @@ class TradeSchema(BaseModel):
trade_id: int
pair: str
is_open: bool
is_short: bool
exchange: str
amount: float
amount_requested: float
stake_amount: float
strategy: str
buy_tag: Optional[str]
buy_tag: Optional[str] # Deprecated
enter_tag: Optional[str]
timeframe: int
fee_open: Optional[float]
fee_open_cost: Optional[float]
@ -242,6 +251,11 @@ class TradeSchema(BaseModel):
open_order_id: Optional[str]
orders: List[OrderSchema]
leverage: Optional[float]
interest_rate: Optional[float]
funding_fees: Optional[float]
trading_mode: Optional[TradingMode]
class OpenTradeSchema(TradeSchema):
stoploss_current_dist: Optional[float]
@ -262,7 +276,7 @@ class TradeResponse(BaseModel):
total_trades: int
class ForceBuyResponse(BaseModel):
class ForceEnterResponse(BaseModel):
__root__: Union[TradeSchema, StatusMsg]
@ -292,15 +306,16 @@ class Logs(BaseModel):
logs: List[List]
class ForceBuyPayload(BaseModel):
class ForceEnterPayload(BaseModel):
pair: str
side: SignalDirection = SignalDirection.LONG
price: Optional[float]
ordertype: Optional[OrderTypeValues]
stakeamount: Optional[float]
entry_tag: Optional[str]
class ForceSellPayload(BaseModel):
class ForceExitPayload(BaseModel):
tradeid: str
ordertype: Optional[OrderTypeValues]
@ -364,6 +379,10 @@ class PairHistory(BaseModel):
length: int
buy_signals: int
sell_signals: int
enter_long_signals: int
exit_long_signals: int
enter_short_signals: int
exit_short_signals: int
last_analyzed: datetime
last_analyzed_ts: int
data_start_ts: int

View File

@ -9,13 +9,14 @@ from fastapi.exceptions import HTTPException
from freqtrade import __version__
from freqtrade.constants import USERPATH_STRATEGIES
from freqtrade.data.history import get_datahandler
from freqtrade.enums import CandleType, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceBuyPayload,
ForceBuyResponse, ForceSellPayload, Health, Locks,
Logs, OpenTradeSchema, PairHistory,
DeleteLockRequest, DeleteTrade, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload, Health,
Locks, Logs, OpenTradeSchema, PairHistory,
PerformanceEntry, Ping, PlotConfig, Profit,
ResultMsg, ShowConfig, Stats, StatusMsg,
StrategyListResponse, StrategyResponse, SysInfo,
@ -32,8 +33,9 @@ logger = logging.getLogger(__name__)
# 1.11: forcebuy and forcesell accept ordertype
# 1.12: add blacklist delete endpoint
# 1.13: forcebuy supports stake_amount
# 1.14: Add entry/exit orders to trade response
API_VERSION = 1.14
# versions 2.xx -> futures/short branch
# 2.14: Add entry/exit orders to trade response
API_VERSION = 2.14
# Public API, requires no auth.
router_public = APIRouter()
@ -133,24 +135,30 @@ def show_config(rpc: Optional[RPC] = Depends(get_rpc_optional), config=Depends(g
return resp
@router.post('/forcebuy', response_model=ForceBuyResponse, tags=['trading'])
def forcebuy(payload: ForceBuyPayload, rpc: RPC = Depends(get_rpc)):
# /forcebuy is deprecated with short addition. use ForceEntry instead
@router.post('/forceenter', response_model=ForceEnterResponse, tags=['trading'])
@router.post('/forcebuy', response_model=ForceEnterResponse, tags=['trading'])
def forceentry(payload: ForceEnterPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
stake_amount = payload.stakeamount if payload.stakeamount else None
entry_tag = payload.entry_tag if payload.entry_tag else 'forceentry'
trade = rpc._rpc_forcebuy(payload.pair, payload.price, ordertype, stake_amount, entry_tag)
trade = rpc._rpc_force_entry(payload.pair, payload.price, order_side=payload.side,
order_type=ordertype, stake_amount=stake_amount,
enter_tag=entry_tag)
if trade:
return ForceBuyResponse.parse_obj(trade.to_json())
return ForceEnterResponse.parse_obj(trade.to_json())
else:
return ForceBuyResponse.parse_obj({"status": f"Error buying pair {payload.pair}."})
return ForceEnterResponse.parse_obj(
{"status": f"Error entering {payload.side} trade for pair {payload.pair}."})
@router.post('/forceexit', response_model=ResultMsg, tags=['trading'])
@router.post('/forcesell', response_model=ResultMsg, tags=['trading'])
def forcesell(payload: ForceSellPayload, rpc: RPC = Depends(get_rpc)):
def forcesell(payload: ForceExitPayload, rpc: RPC = Depends(get_rpc)):
ordertype = payload.ordertype.value if payload.ordertype else None
return rpc._rpc_forcesell(payload.tradeid, ordertype)
return rpc._rpc_forceexit(payload.tradeid, ordertype)
@router.get('/blacklist', response_model=BlacklistResponse, tags=['info', 'pairlist'])
@ -268,16 +276,22 @@ def get_strategy(strategy: str, config=Depends(get_config)):
@router.get('/available_pairs', response_model=AvailablePairs, tags=['candle data'])
def list_available_pairs(timeframe: Optional[str] = None, stake_currency: Optional[str] = None,
config=Depends(get_config)):
candletype: Optional[CandleType] = None, config=Depends(get_config)):
dh = get_datahandler(config['datadir'], config.get('dataformat_ohlcv', None))
pair_interval = dh.ohlcv_get_available_data(config['datadir'])
trading_mode: TradingMode = config.get('trading_mode', TradingMode.SPOT)
pair_interval = dh.ohlcv_get_available_data(config['datadir'], trading_mode)
if timeframe:
pair_interval = [pair for pair in pair_interval if pair[1] == timeframe]
if stake_currency:
pair_interval = [pair for pair in pair_interval if pair[0].endswith(stake_currency)]
if candletype:
pair_interval = [pair for pair in pair_interval if pair[2] == candletype]
else:
candle_type = CandleType.get_default(trading_mode)
pair_interval = [pair for pair in pair_interval if pair[2] == candle_type]
pair_interval = sorted(pair_interval, key=lambda x: x[0])
pairs = list({x[0] for x in pair_interval})

View File

@ -18,7 +18,7 @@ from freqtrade import __version__
from freqtrade.configuration.timerange import TimeRange
from freqtrade.constants import CANCEL_REASON, DATETIME_PRINT_FORMAT
from freqtrade.data.history import load_data
from freqtrade.enums import SellType, State
from freqtrade.enums import ExitCheckTuple, ExitType, SignalDirection, State, TradingMode
from freqtrade.exceptions import ExchangeError, PricingError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_msecs
from freqtrade.loggers import bufferHandler
@ -27,7 +27,7 @@ from freqtrade.persistence import PairLocks, Trade
from freqtrade.persistence.models import PairLock
from freqtrade.plugins.pairlist.pairlist_helpers import expand_pairlist
from freqtrade.rpc.fiat_convert import CryptoToFiatConverter
from freqtrade.strategy.interface import SellCheckTuple
from freqtrade.wallets import PositionWallet, Wallet
logger = logging.getLogger(__name__)
@ -110,6 +110,8 @@ class RPC:
'version': __version__,
'strategy_version': strategy_version,
'dry_run': config['dry_run'],
'trading_mode': config.get('trading_mode', 'spot'),
'short_allowed': config.get('trading_mode', 'spot') != 'spot',
'stake_currency': config['stake_currency'],
'stake_currency_decimals': decimals_per_coin(config['stake_currency']),
'stake_amount': str(config['stake_amount']),
@ -134,8 +136,8 @@ class RPC:
'exchange': config['exchange']['name'],
'strategy': config['strategy'],
'forcebuy_enabled': config.get('forcebuy_enable', False),
'ask_strategy': config.get('ask_strategy', {}),
'bid_strategy': config.get('bid_strategy', {}),
'exit_pricing': config.get('exit_pricing', {}),
'entry_pricing': config.get('entry_pricing', {}),
'state': str(botstate),
'runmode': config['runmode'].value,
'position_adjustment_enable': config.get('position_adjustment_enable', False),
@ -153,7 +155,7 @@ class RPC:
"""
# Fetch open trades
if trade_ids:
trades = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
trades: List[Trade] = Trade.get_trades(trade_filter=Trade.id.in_(trade_ids)).all()
else:
trades = Trade.get_open_trades()
@ -169,7 +171,7 @@ class RPC:
if trade.is_open:
try:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell")
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (ExchangeError, PricingError):
current_rate = NAN
else:
@ -219,7 +221,7 @@ class RPC:
def _rpc_status_table(self, stake_currency: str,
fiat_display_currency: str) -> Tuple[List, List, float]:
trades = Trade.get_open_trades()
trades: List[Trade] = Trade.get_open_trades()
if not trades:
raise RPCException('no active trade')
else:
@ -229,11 +231,12 @@ class RPC:
# calculate profit and send message to user
try:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell")
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError):
current_rate = NAN
trade_profit = trade.calc_profit(current_rate)
profit_str = f'{trade.calc_profit_ratio(current_rate):.2%}'
direction_str = 'S' if trade.is_short else 'L'
if self._fiat_converter:
fiat_profit = self._fiat_converter.convert_amount(
trade_profit,
@ -245,7 +248,7 @@ class RPC:
fiat_profit_sum = fiat_profit if isnan(fiat_profit_sum) \
else fiat_profit_sum + fiat_profit
detail_trade = [
trade.id,
f'{trade.id} {direction_str}',
trade.pair + ('*' if (trade.open_order_id is not None
and trade.close_rate_requested is None) else '')
+ ('**' if (trade.close_rate_requested is not None) else ''),
@ -253,20 +256,19 @@ class RPC:
profit_str
]
if self._config.get('position_adjustment_enable', False):
max_buy_str = ''
max_entry_str = ''
if self._config.get('max_entry_position_adjustment', -1) > 0:
max_buy_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_buys = trade.nr_of_successful_buys
detail_trade.append(f"{filled_buys}{max_buy_str}")
max_entry_str = f"/{self._config['max_entry_position_adjustment'] + 1}"
filled_entries = trade.nr_of_successful_entries
detail_trade.append(f"{filled_entries}{max_entry_str}")
trades_list.append(detail_trade)
profitcol = "Profit"
if self._fiat_converter:
profitcol += " (" + fiat_display_currency + ")"
columns = ['ID L/S', 'Pair', 'Since', profitcol]
if self._config.get('position_adjustment_enable', False):
columns = ['ID', 'Pair', 'Since', profitcol, '# Entries']
else:
columns = ['ID', 'Pair', 'Since', profitcol]
columns.append('# Entries')
return trades_list, columns, fiat_profit_sum
def _rpc_daily_profit(
@ -453,7 +455,7 @@ class RPC:
""" Returns cumulative profit statistics """
trade_filter = ((Trade.is_open.is_(False) & (Trade.close_date >= start_date)) |
Trade.is_open.is_(True))
trades = Trade.get_trades(trade_filter).order_by(Trade.id).all()
trades: List[Trade] = Trade.get_trades(trade_filter).order_by(Trade.id).all()
profit_all_coin = []
profit_all_ratio = []
@ -483,7 +485,7 @@ class RPC:
# Get current rate
try:
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell")
trade.pair, side='exit', is_short=trade.is_short, refresh=False)
except (PricingError, ExchangeError):
current_rate = NAN
profit_ratio = trade.calc_profit_ratio(rate=current_rate)
@ -559,7 +561,7 @@ class RPC:
def _rpc_balance(self, stake_currency: str, fiat_display_currency: str) -> Dict:
""" Returns current account balance per crypto """
output = []
currencies = []
total = 0.0
try:
tickers = self._freqtrade.exchange.get_tickers(cached=True)
@ -570,7 +572,8 @@ class RPC:
starting_capital = self._freqtrade.wallets.get_starting_balance()
starting_cap_fiat = self._fiat_converter.convert_amount(
starting_capital, stake_currency, fiat_display_currency) if self._fiat_converter else 0
coin: str
balance: Wallet
for coin, balance in self._freqtrade.wallets.get_all_balances().items():
if not balance.total:
continue
@ -579,6 +582,9 @@ class RPC:
if coin == stake_currency:
rate = 1.0
est_stake = balance.total
if self._config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT:
# in Futures, "total" includes the locked stake, and therefore all positions
est_stake = balance.free
else:
try:
pair = self._freqtrade.exchange.get_valid_pair_combination(coin, stake_currency)
@ -591,13 +597,35 @@ class RPC:
logger.warning(f" Could not get rate for pair {coin}.")
continue
total = total + (est_stake or 0)
output.append({
currencies.append({
'currency': coin,
# TODO: The below can be simplified if we don't assign None to values.
'free': balance.free if balance.free is not None else 0,
'balance': balance.total if balance.total is not None else 0,
'used': balance.used if balance.used is not None else 0,
'est_stake': est_stake or 0,
'stake': stake_currency,
'side': 'long',
'leverage': 1,
'position': 0,
'is_position': False,
})
symbol: str
position: PositionWallet
for symbol, position in self._freqtrade.wallets.get_all_positions().items():
total += position.collateral
currencies.append({
'currency': symbol,
'free': 0,
'balance': 0,
'used': 0,
'position': position.position,
'est_stake': position.collateral,
'stake': stake_currency,
'leverage': position.leverage,
'side': position.side,
'is_position': True
})
value = self._fiat_converter.convert_amount(
@ -609,7 +637,7 @@ class RPC:
starting_cap_fiat_ratio = (value / starting_cap_fiat) - 1 if starting_cap_fiat else 0.0
return {
'currencies': output,
'currencies': currencies,
'total': total,
'symbol': fiat_display_currency,
'value': value,
@ -655,7 +683,7 @@ class RPC:
return {'status': 'No more buy will occur from now. Run /reload_config to reset.'}
def _rpc_forcesell(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
def _rpc_forceexit(self, trade_id: str, ordertype: Optional[str] = None) -> Dict[str, str]:
"""
Handler for forcesell <id>.
Sells the given trade at current price
@ -666,24 +694,24 @@ class RPC:
if trade.open_order_id:
order = self._freqtrade.exchange.fetch_order(trade.open_order_id, trade.pair)
if order['side'] == 'buy':
if order['side'] == trade.enter_side:
fully_canceled = self._freqtrade.handle_cancel_enter(
trade, order, CANCEL_REASON['FORCE_SELL'])
if order['side'] == 'sell':
if order['side'] == trade.exit_side:
# Cancel order - so it is placed anew with a fresh price.
self._freqtrade.handle_cancel_exit(trade, order, CANCEL_REASON['FORCE_SELL'])
if not fully_canceled:
# Get current rate and execute sell
current_rate = self._freqtrade.exchange.get_rate(
trade.pair, refresh=False, side="sell")
sell_reason = SellCheckTuple(sell_type=SellType.FORCE_SELL)
trade.pair, side='exit', is_short=trade.is_short, refresh=True)
exit_check = ExitCheckTuple(exit_type=ExitType.FORCE_SELL)
order_type = ordertype or self._freqtrade.strategy.order_types.get(
"forcesell", self._freqtrade.strategy.order_types["sell"])
"forceexit", self._freqtrade.strategy.order_types["exit"])
self._freqtrade.execute_trade_exit(
trade, current_rate, sell_reason, ordertype=order_type)
trade, current_rate, exit_check, ordertype=order_type)
# ---- EOF def _exec_forcesell ----
if self._freqtrade.state != State.RUNNING:
@ -703,7 +731,7 @@ class RPC:
trade_filter=[Trade.id == trade_id, Trade.is_open.is_(True), ]
).first()
if not trade:
logger.warning('forcesell: Invalid argument received')
logger.warning('forceexit: Invalid argument received')
raise RPCException('invalid argument')
_exec_forcesell(trade)
@ -711,20 +739,25 @@ class RPC:
self._freqtrade.wallets.update()
return {'result': f'Created sell order for trade {trade_id}.'}
def _rpc_forcebuy(self, pair: str, price: Optional[float], order_type: Optional[str] = None,
def _rpc_force_entry(self, pair: str, price: Optional[float], *,
order_type: Optional[str] = None,
order_side: SignalDirection = SignalDirection.LONG,
stake_amount: Optional[float] = None,
buy_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
enter_tag: Optional[str] = 'forceentry') -> Optional[Trade]:
"""
Handler for forcebuy <asset> <price>
Buys a pair trade at the given or current price
"""
if not self._freqtrade.config.get('forcebuy_enable', False):
raise RPCException('Forcebuy not enabled.')
raise RPCException('Forceentry not enabled.')
if self._freqtrade.state != State.RUNNING:
raise RPCException('trader is not running')
if order_side == SignalDirection.SHORT and self._freqtrade.trading_mode == TradingMode.SPOT:
raise RPCException("Can't go short on Spot markets.")
# Check if pair quote currency equals to the stake currency.
stake_currency = self._freqtrade.config.get('stake_currency')
if not self._freqtrade.exchange.get_pair_quote_currency(pair) == stake_currency:
@ -733,8 +766,10 @@ class RPC:
# check if valid pair
# check if pair already has an open pair
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
trade: Trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
is_short = (order_side == SignalDirection.SHORT)
if trade:
is_short = trade.is_short
if not self._freqtrade.strategy.position_adjustment_enable:
raise RPCException(f'position for {pair} already open - id: {trade.id}')
@ -745,9 +780,12 @@ class RPC:
# execute buy
if not order_type:
order_type = self._freqtrade.strategy.order_types.get(
'forcebuy', self._freqtrade.strategy.order_types['buy'])
'forceentry', self._freqtrade.strategy.order_types['entry'])
if self._freqtrade.execute_entry(pair, stake_amount, price,
ordertype=order_type, trade=trade, buy_tag=buy_tag):
ordertype=order_type, trade=trade,
is_short=is_short,
enter_tag=enter_tag,
):
Trade.commit()
trade = Trade.get_trades([Trade.is_open.is_(True), Trade.pair == pair]).first()
return trade
@ -802,27 +840,23 @@ class RPC:
return pair_rates
def _rpc_buy_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
def _rpc_enter_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for buy tag performance.
Shows a performance statistic from finished trades
"""
buy_tags = Trade.get_buy_tag_performance(pair)
return buy_tags
return Trade.get_enter_tag_performance(pair)
def _rpc_sell_reason_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for sell reason performance.
Shows a performance statistic from finished trades
"""
sell_reasons = Trade.get_sell_reason_performance(pair)
return sell_reasons
return Trade.get_sell_reason_performance(pair)
def _rpc_mix_tag_performance(self, pair: Optional[str]) -> List[Dict[str, Any]]:
"""
Handler for mix tag (buy_tag + sell_reason) performance.
Handler for mix tag (enter_tag + sell_reason) performance.
Shows a performance statistic from finished trades
"""
mix_tags = Trade.get_mix_tag_performance(pair)
@ -945,20 +979,21 @@ class RPC:
def _convert_dataframe_to_dict(strategy: str, pair: str, timeframe: str, dataframe: DataFrame,
last_analyzed: datetime) -> Dict[str, Any]:
has_content = len(dataframe) != 0
buy_signals = 0
sell_signals = 0
signals = {
'enter_long': 0,
'exit_long': 0,
'enter_short': 0,
'exit_short': 0,
}
if has_content:
dataframe.loc[:, '__date_ts'] = dataframe.loc[:, 'date'].view(int64) // 1000 // 1000
# Move signal close to separate column when signal for easy plotting
if 'buy' in dataframe.columns:
buy_mask = (dataframe['buy'] == 1)
buy_signals = int(buy_mask.sum())
dataframe.loc[buy_mask, '_buy_signal_close'] = dataframe.loc[buy_mask, 'close']
if 'sell' in dataframe.columns:
sell_mask = (dataframe['sell'] == 1)
sell_signals = int(sell_mask.sum())
dataframe.loc[sell_mask, '_sell_signal_close'] = dataframe.loc[sell_mask, 'close']
for sig_type in signals.keys():
if sig_type in dataframe.columns:
mask = (dataframe[sig_type] == 1)
signals[sig_type] = int(mask.sum())
dataframe.loc[mask, f'_{sig_type}_signal_close'] = dataframe.loc[mask, 'close']
# band-aid until this is fixed:
# https://github.com/pandas-dev/pandas/issues/45836
@ -978,8 +1013,12 @@ class RPC:
'columns': list(dataframe.columns),
'data': dataframe.values.tolist(),
'length': len(dataframe),
'buy_signals': buy_signals,
'sell_signals': sell_signals,
'buy_signals': signals['enter_long'], # Deprecated
'sell_signals': signals['exit_long'], # Deprecated
'enter_long_signals': signals['enter_long'],
'exit_long_signals': signals['exit_long'],
'enter_short_signals': signals['enter_short'],
'exit_short_signals': signals['exit_short'],
'last_analyzed': last_analyzed,
'last_analyzed_ts': int(last_analyzed.timestamp()),
'data_start': '',

View File

@ -7,6 +7,7 @@ import json
import logging
import re
from datetime import date, datetime, timedelta
from functools import partial
from html import escape
from itertools import chain
from math import isnan
@ -22,7 +23,7 @@ from telegram.utils.helpers import escape_markdown
from freqtrade.__init__ import __version__
from freqtrade.constants import DUST_PER_COIN
from freqtrade.enums import RPCMessageType
from freqtrade.enums import RPCMessageType, SignalDirection, TradingMode
from freqtrade.exceptions import OperationalException
from freqtrade.misc import chunks, plural, round_coin_value
from freqtrade.persistence import Trade
@ -113,7 +114,8 @@ class Telegram(RPCHandler):
r'/stopbuy$', r'/reload_config$', r'/show_config$',
r'/logs$', r'/whitelist$', r'/blacklist$', r'/bl_delete$',
r'/weekly$', r'/weekly \d+$', r'/monthly$', r'/monthly \d+$',
r'/forcebuy$', r'/edge$', r'/health$', r'/help$', r'/version$']
r'/forcebuy$', r'/forcelong$', r'/forceshort$',
r'/edge$', r'/health$', r'/help$', r'/version$']
# Create keys for generation
valid_keys_print = [k.replace('$', '') for k in valid_keys]
@ -150,12 +152,15 @@ class Telegram(RPCHandler):
CommandHandler('balance', self._balance),
CommandHandler('start', self._start),
CommandHandler('stop', self._stop),
CommandHandler('forcesell', self._forcesell),
CommandHandler('forcebuy', self._forcebuy),
CommandHandler(['forcesell', 'forceexit'], self._forceexit),
CommandHandler(['forcebuy', 'forcelong'], partial(
self._forceenter, order_side=SignalDirection.LONG)),
CommandHandler('forceshort', partial(
self._forceenter, order_side=SignalDirection.SHORT)),
CommandHandler('trades', self._trades),
CommandHandler('delete', self._delete_trade),
CommandHandler('performance', self._performance),
CommandHandler('buys', self._buy_tag_performance),
CommandHandler(['buys', 'entries'], self._enter_tag_performance),
CommandHandler('sells', self._sell_reason_performance),
CommandHandler('mix_tags', self._mix_tag_performance),
CommandHandler('stats', self._stats),
@ -185,12 +190,13 @@ class Telegram(RPCHandler):
CallbackQueryHandler(self._profit, pattern='update_profit'),
CallbackQueryHandler(self._balance, pattern='update_balance'),
CallbackQueryHandler(self._performance, pattern='update_performance'),
CallbackQueryHandler(self._buy_tag_performance, pattern='update_buy_tag_performance'),
CallbackQueryHandler(self._enter_tag_performance,
pattern='update_enter_tag_performance'),
CallbackQueryHandler(self._sell_reason_performance,
pattern='update_sell_reason_performance'),
CallbackQueryHandler(self._mix_tag_performance, pattern='update_mix_tag_performance'),
CallbackQueryHandler(self._count, pattern='update_count'),
CallbackQueryHandler(self._forcebuy_inline),
CallbackQueryHandler(self._forceenter_inline),
]
for handle in handles:
self._updater.dispatcher.add_handler(handle)
@ -223,20 +229,25 @@ class Telegram(RPCHandler):
msg['stake_amount'], msg['stake_currency'], msg['fiat_currency'])
else:
msg['stake_amount_fiat'] = 0
is_fill = msg['type'] == RPCMessageType.BUY_FILL
is_fill = msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]
emoji = '\N{CHECK MARK}' if is_fill else '\N{LARGE BLUE CIRCLE}'
enter_side = ({'enter': 'Long', 'entered': 'Longed'} if msg['type']
in [RPCMessageType.BUY_FILL, RPCMessageType.BUY]
else {'enter': 'Short', 'entered': 'Shorted'})
message = (
f"{emoji} *{msg['exchange']}:* {'Bought' if is_fill else 'Buying'} {msg['pair']}"
f"{emoji} *{msg['exchange']}:*"
f" {enter_side['entered'] if is_fill else enter_side['enter']} {msg['pair']}"
f" (#{msg['trade_id']})\n"
)
message += f"*Buy Tag:* `{msg['buy_tag']}`\n" if msg.get('buy_tag', None) else ""
message += f"*Enter Tag:* `{msg['enter_tag']}`\n" if msg.get('enter_tag', None) else ""
message += f"*Amount:* `{msg['amount']:.8f}`\n"
if msg.get('leverage') and msg.get('leverage', 1.0) != 1.0:
message += f"*Leverage:* `{msg['leverage']}`\n"
if msg['type'] == RPCMessageType.BUY_FILL:
if msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
message += f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
elif msg['type'] == RPCMessageType.BUY:
elif msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
message += f"*Open Rate:* `{msg['limit']:.8f}`\n"\
f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
@ -255,8 +266,11 @@ class Telegram(RPCHandler):
microsecond=0) - msg['open_date'].replace(microsecond=0)
msg['duration_min'] = msg['duration'].total_seconds() / 60
msg['buy_tag'] = msg['buy_tag'] if "buy_tag" in msg.keys() else None
msg['enter_tag'] = msg['enter_tag'] if "enter_tag" in msg.keys() else None
msg['emoji'] = self._get_sell_emoji(msg)
msg['leverage_text'] = (f"*Leverage:* `{msg['leverage']:.1f}`\n"
if msg.get('leverage', None) and msg.get('leverage', 1.0) != 1.0
else "")
# Check if all sell properties are available.
# This might not be the case if the message origin is triggered by /forcesell
@ -272,15 +286,17 @@ class Telegram(RPCHandler):
is_fill = msg['type'] == RPCMessageType.SELL_FILL
message = (
f"{msg['emoji']} *{msg['exchange']}:* "
f"{'Sold' if is_fill else 'Selling'} {msg['pair']} (#{msg['trade_id']})\n"
f"{'Exited' if is_fill else 'Exiting'} {msg['pair']} (#{msg['trade_id']})\n"
f"*{'Profit' if is_fill else 'Unrealized Profit'}:* "
f"`{msg['profit_ratio']:.2%}{msg['profit_extra']}`\n"
f"*Buy Tag:* `{msg['buy_tag']}`\n"
f"*Sell Reason:* `{msg['sell_reason']}`\n"
f"*Enter Tag:* `{msg['enter_tag']}`\n"
f"*Exit Reason:* `{msg['sell_reason']}`\n"
f"*Duration:* `{msg['duration']} ({msg['duration_min']:.1f} min)`\n"
f"*Direction:* `{msg['direction']}`\n"
f"{msg['leverage_text']}"
f"*Amount:* `{msg['amount']:.8f}`\n"
f"*Open Rate:* `{msg['open_rate']:.8f}`\n")
f"*Open Rate:* `{msg['open_rate']:.8f}`\n"
)
if msg['type'] == RPCMessageType.SELL:
message += (f"*Current Rate:* `{msg['current_rate']:.8f}`\n"
f"*Close Rate:* `{msg['limit']:.8f}`")
@ -291,16 +307,19 @@ class Telegram(RPCHandler):
return message
def compose_message(self, msg: Dict[str, Any], msg_type: RPCMessageType) -> str:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL]:
if msg_type in [RPCMessageType.BUY, RPCMessageType.BUY_FILL, RPCMessageType.SHORT,
RPCMessageType.SHORT_FILL]:
message = self._format_buy_msg(msg)
elif msg_type in [RPCMessageType.SELL, RPCMessageType.SELL_FILL]:
message = self._format_sell_msg(msg)
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'buy' if msg_type == RPCMessageType.BUY_CANCEL else 'sell'
elif msg_type in (RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL,
RPCMessageType.SELL_CANCEL):
msg['message_side'] = 'enter' if msg_type in [RPCMessageType.BUY_CANCEL,
RPCMessageType.SHORT_CANCEL] else 'exit'
message = ("\N{WARNING SIGN} *{exchange}:* "
"Cancelling open {message_side} Order for {pair} (#{trade_id}). "
"Cancelling {message_side} Order for {pair} (#{trade_id}). "
"Reason: {reason}.".format(**msg))
elif msg_type == RPCMessageType.PROTECTION_TRIGGER:
@ -379,7 +398,7 @@ class Telegram(RPCHandler):
first_avg = filled_orders[0]["safe_price"]
for x, order in enumerate(filled_orders):
if order['ft_order_side'] != 'buy':
if not order['ft_is_entry']:
continue
cur_entry_datetime = arrow.get(order["order_filled_date"])
cur_entry_amount = order["amount"]
@ -446,14 +465,16 @@ class Telegram(RPCHandler):
messages = []
for r in results:
r['open_date_hum'] = arrow.get(r['open_date']).humanize()
r['num_entries'] = len([o for o in r['orders'] if o['ft_order_side'] == 'buy'])
r['num_entries'] = len([o for o in r['orders'] if o['ft_is_entry']])
r['sell_reason'] = r.get('sell_reason', "")
lines = [
"*Trade ID:* `{trade_id}`" +
("` (since {open_date_hum})`" if r['is_open'] else ""),
"*Current Pair:* {pair}",
"*Direction:* " + ("`Short`" if r.get('is_short') else "`Long`"),
"*Leverage:* `{leverage}`" if r.get('leverage') else "",
"*Amount:* `{amount} ({stake_amount} {base_currency})`",
"*Entry Tag:* `{buy_tag}`" if r['buy_tag'] else "",
"*Enter Tag:* `{enter_tag}`" if r['enter_tag'] else "",
"*Exit Reason:* `{sell_reason}`" if r['sell_reason'] else "",
]
@ -810,6 +831,14 @@ class Telegram(RPCHandler):
for curr in result['currencies']:
curr_output = ''
if curr['est_stake'] > balance_dust_level:
if curr['is_position']:
curr_output = (
f"*{curr['currency']}:*\n"
f"\t`{curr['side']}: {curr['position']:.8f}`\n"
f"\t`Leverage: {curr['leverage']:.1f}`\n"
f"\t`Est. {curr['stake']}: "
f"{round_coin_value(curr['est_stake'], curr['stake'], False)}`\n")
else:
curr_output = (
f"*{curr['currency']}:*\n"
f"\t`Available: {curr['free']:.8f}`\n"
@ -900,7 +929,7 @@ class Telegram(RPCHandler):
self._send_msg('Status: `{status}`'.format(**msg))
@authorized_only
def _forcesell(self, update: Update, context: CallbackContext) -> None:
def _forceexit(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /forcesell <id>.
Sells the given trade at current price
@ -914,26 +943,28 @@ class Telegram(RPCHandler):
self._send_msg("You must specify a trade-id or 'all'.")
return
try:
msg = self._rpc._rpc_forcesell(trade_id)
self._send_msg('Forcesell Result: `{result}`'.format(**msg))
msg = self._rpc._rpc_forceexit(trade_id)
self._send_msg('Forceexit Result: `{result}`'.format(**msg))
except RPCException as e:
self._send_msg(str(e))
def _forcebuy_action(self, pair, price=None):
def _forceenter_action(self, pair, price: Optional[float], order_side: SignalDirection):
if pair != 'cancel':
try:
self._rpc._rpc_forcebuy(pair, price)
self._rpc._rpc_force_entry(pair, price, order_side=order_side)
except RPCException as e:
self._send_msg(str(e))
def _forcebuy_inline(self, update: Update, _: CallbackContext) -> None:
def _forceenter_inline(self, update: Update, _: CallbackContext) -> None:
if update.callback_query:
query = update.callback_query
pair = query.data
if query.data and '_||_' in query.data:
pair, side = query.data.split('_||_')
order_side = SignalDirection(side)
query.answer()
query.edit_message_text(text=f"Force Buying: {pair}")
self._forcebuy_action(pair)
query.edit_message_text(text=f"Manually entering {order_side} for {pair}")
self._forceenter_action(pair, None, order_side)
@staticmethod
def _layout_inline_keyboard(buttons: List[InlineKeyboardButton],
@ -941,9 +972,10 @@ class Telegram(RPCHandler):
return [buttons[i:i + cols] for i in range(0, len(buttons), cols)]
@authorized_only
def _forcebuy(self, update: Update, context: CallbackContext) -> None:
def _forceenter(
self, update: Update, context: CallbackContext, order_side: SignalDirection) -> None:
"""
Handler for /forcebuy <asset> <price>.
Handler for /forcelong <asset> <price> and `/forceshort <asset> <price>
Buys a pair trade at the given or current price
:param bot: telegram bot
:param update: message update
@ -952,16 +984,19 @@ class Telegram(RPCHandler):
if context.args:
pair = context.args[0]
price = float(context.args[1]) if len(context.args) > 1 else None
self._forcebuy_action(pair, price)
self._forceenter_action(pair, price, order_side)
else:
whitelist = self._rpc._rpc_whitelist()['whitelist']
pair_buttons = [
InlineKeyboardButton(text=pair, callback_data=pair) for pair in sorted(whitelist)]
InlineKeyboardButton(text=pair, callback_data=f"{pair}_||_{order_side}")
for pair in sorted(whitelist)
]
buttons_aligned = self._layout_inline_keyboard(pair_buttons)
buttons_aligned.append([InlineKeyboardButton(text='Cancel', callback_data='cancel')])
self._send_msg(msg="Which pair?",
keyboard=buttons_aligned)
keyboard=buttons_aligned,
query=update.callback_query)
@authorized_only
def _trades(self, update: Update, context: CallbackContext) -> None:
@ -1052,7 +1087,7 @@ class Telegram(RPCHandler):
self._send_msg(str(e))
@authorized_only
def _buy_tag_performance(self, update: Update, context: CallbackContext) -> None:
def _enter_tag_performance(self, update: Update, context: CallbackContext) -> None:
"""
Handler for /buys PAIR .
Shows a performance statistic from finished trades
@ -1065,11 +1100,11 @@ class Telegram(RPCHandler):
if context.args and isinstance(context.args[0], str):
pair = context.args[0]
trades = self._rpc._rpc_buy_tag_performance(pair)
trades = self._rpc._rpc_enter_tag_performance(pair)
output = "<b>Buy Tag Performance:</b>\n"
for i, trade in enumerate(trades):
stat_line = (
f"{i+1}.\t <code>{trade['buy_tag']}\t"
f"{i+1}.\t <code>{trade['enter_tag']}\t"
f"{round_coin_value(trade['profit_abs'], self._config['stake_currency'])} "
f"({trade['profit_ratio']:.2%}) "
f"({trade['count']})</code>\n")
@ -1081,7 +1116,7 @@ class Telegram(RPCHandler):
output += stat_line
self._send_msg(output, parse_mode=ParseMode.HTML,
reload_able=True, callback_path="update_buy_tag_performance",
reload_able=True, callback_path="update_enter_tag_performance",
query=update.callback_query)
except RPCException as e:
self._send_msg(str(e))
@ -1327,8 +1362,13 @@ class Telegram(RPCHandler):
:param update: message update
:return: None
"""
forcebuy_text = ("*/forcebuy <pair> [<rate>]:* `Instantly buys the given pair. "
forceenter_text = ("*/forcelong <pair> [<rate>]:* `Instantly buys the given pair. "
"Optionally takes a rate at which to buy "
"(only applies to limit orders).` \n"
)
if self._rpc._freqtrade.trading_mode != TradingMode.SPOT:
forceenter_text += ("*/forceshort <pair> [<rate>]:* `Instantly shorts the given pair. "
"Optionally takes a rate at which to sell "
"(only applies to limit orders).` \n")
message = (
"_BotControl_\n"
@ -1336,9 +1376,9 @@ class Telegram(RPCHandler):
"*/start:* `Starts the trader`\n"
"*/stop:* Stops the trader\n"
"*/stopbuy:* `Stops buying, but handles open trades gracefully` \n"
"*/forcesell <trade_id>|all:* `Instantly sells the given trade or all trades, "
"*/forceexit <trade_id>|all:* `Instantly exits the given trade or all trades, "
"regardless of profit`\n"
f"{forcebuy_text if self._config.get('forcebuy_enable', False) else ''}"
f"{forceenter_text if self._config.get('forcebuy_enable', False) else ''}"
"*/delete <trade_id>:* `Instantly delete the given trade in the database`\n"
"*/whitelist:* `Show current whitelist` \n"
"*/blacklist [pair]:* `Show current blacklist, or adds one or more pairs "
@ -1366,7 +1406,7 @@ class Telegram(RPCHandler):
" *table :* `will display trades in a table`\n"
" `pending buy orders are marked with an asterisk (*)`\n"
" `pending sell orders are marked with a double asterisk (**)`\n"
"*/buys <pair|none>:* `Shows the buy_tag performance`\n"
"*/buys <pair|none>:* `Shows the enter_tag performance`\n"
"*/sells <pair|none>:* `Shows the sell reason performance`\n"
"*/mix_tags <pair|none>:* `Shows combined buy tag + sell reason performance`\n"
"*/trades [limit]:* `Lists last closed trades (limited to 10 by default)`\n"
@ -1446,11 +1486,12 @@ class Telegram(RPCHandler):
self._send_msg(
f"*Mode:* `{'Dry-run' if val['dry_run'] else 'Live'}`\n"
f"*Exchange:* `{val['exchange']}`\n"
f"*Market: * `{val['trading_mode']}`\n"
f"*Stake per trade:* `{val['stake_amount']} {val['stake_currency']}`\n"
f"*Max open Trades:* `{val['max_open_trades']}`\n"
f"*Minimum ROI:* `{val['minimal_roi']}`\n"
f"*Ask strategy:* ```\n{json.dumps(val['ask_strategy'])}```\n"
f"*Bid strategy:* ```\n{json.dumps(val['bid_strategy'])}```\n"
f"*Entry strategy:* ```\n{json.dumps(val['entry_pricing'])}```\n"
f"*Exit strategy:* ```\n{json.dumps(val['exit_pricing'])}```\n"
f"{sl_info}"
f"{pa_info}"
f"*Timeframe:* `{val['timeframe']}`\n"

View File

@ -44,11 +44,11 @@ class Webhook(RPCHandler):
""" Send a message to telegram channel """
try:
if msg['type'] == RPCMessageType.BUY:
if msg['type'] in [RPCMessageType.BUY, RPCMessageType.SHORT]:
valuedict = self._config['webhook'].get('webhookbuy', None)
elif msg['type'] == RPCMessageType.BUY_CANCEL:
elif msg['type'] in [RPCMessageType.BUY_CANCEL, RPCMessageType.SHORT_CANCEL]:
valuedict = self._config['webhook'].get('webhookbuycancel', None)
elif msg['type'] == RPCMessageType.BUY_FILL:
elif msg['type'] in [RPCMessageType.BUY_FILL, RPCMessageType.SHORT_FILL]:
valuedict = self._config['webhook'].get('webhookbuyfill', None)
elif msg['type'] == RPCMessageType.SELL:
valuedict = self._config['webhook'].get('webhooksell', None)

View File

@ -1,7 +1,9 @@
from typing import Any, Callable, NamedTuple, Optional, Union
from dataclasses import dataclass
from typing import Any, Callable, Optional, Union
from pandas import DataFrame
from freqtrade.enums import CandleType
from freqtrade.exceptions import OperationalException
from freqtrade.strategy.strategy_helper import merge_informative_pair
@ -9,15 +11,19 @@ from freqtrade.strategy.strategy_helper import merge_informative_pair
PopulateIndicators = Callable[[Any, DataFrame, dict], DataFrame]
class InformativeData(NamedTuple):
@dataclass
class InformativeData:
asset: Optional[str]
timeframe: str
fmt: Union[str, Callable[[Any], str], None]
ffill: bool
candle_type: Optional[CandleType]
def informative(timeframe: str, asset: str = '',
fmt: Optional[Union[str, Callable[[Any], str]]] = None,
*,
candle_type: Optional[CandleType] = None,
ffill: bool = True) -> Callable[[PopulateIndicators], PopulateIndicators]:
"""
A decorator for populate_indicators_Nn(self, dataframe, metadata), allowing these functions to
@ -46,15 +52,17 @@ def informative(timeframe: str, asset: str = '',
* {column} - name of dataframe column.
* {timeframe} - timeframe of informative dataframe.
:param ffill: ffill dataframe after merging informative pair.
:param candle_type: '', mark, index, premiumIndex, or funding_rate
"""
_asset = asset
_timeframe = timeframe
_fmt = fmt
_ffill = ffill
_candle_type = CandleType.from_string(candle_type) if candle_type else None
def decorator(fn: PopulateIndicators):
informative_pairs = getattr(fn, '_ft_informative', [])
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill))
informative_pairs.append(InformativeData(_asset, _timeframe, _fmt, _ffill, _candle_type))
setattr(fn, '_ft_informative', informative_pairs)
return fn
return decorator
@ -71,6 +79,8 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
asset = inf_data.asset or ''
timeframe = inf_data.timeframe
fmt = inf_data.fmt
candle_type = inf_data.candle_type
config = strategy.config
if asset:
@ -97,7 +107,7 @@ def _create_and_merge_informative_pair(strategy, dataframe: DataFrame, metadata:
fmt = '{base}_{quote}_' + fmt # Informatives of other pairs
inf_metadata = {'pair': asset, 'timeframe': timeframe}
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe)
inf_dataframe = strategy.dp.get_pair_dataframe(asset, timeframe, candle_type)
inf_dataframe = populate_indicators(strategy, inf_dataframe, inf_metadata)
formatter: Any = None

View File

@ -13,7 +13,8 @@ from pandas import DataFrame
from freqtrade.constants import ListPairsWithTimeframes
from freqtrade.data.dataprovider import DataProvider
from freqtrade.enums import SellType, SignalTagType, SignalType
from freqtrade.enums import (CandleType, ExitCheckTuple, ExitType, SignalDirection, SignalTagType,
SignalType, TradingMode)
from freqtrade.exceptions import OperationalException, StrategyError
from freqtrade.exchange import timeframe_to_minutes, timeframe_to_seconds
from freqtrade.exchange.exchange import timeframe_to_next_date
@ -28,23 +29,7 @@ from freqtrade.wallets import Wallets
logger = logging.getLogger(__name__)
CUSTOM_SELL_MAX_LENGTH = 64
class SellCheckTuple:
"""
NamedTuple for Sell type + reason
"""
sell_type: SellType
sell_reason: str = ''
def __init__(self, sell_type: SellType, sell_reason: str = ''):
self.sell_type = sell_type
self.sell_reason = sell_reason or sell_type.value
@property
def sell_flag(self):
return self.sell_type != SellType.NONE
CUSTOM_EXIT_MAX_LENGTH = 64
class IStrategy(ABC, HyperStrategyMixin):
@ -61,7 +46,8 @@ class IStrategy(ABC, HyperStrategyMixin):
# Default to version 2
# Version 1 is the initial interface without metadata dict
# Version 2 populate_* include metadata dict
INTERFACE_VERSION: int = 2
# Version 3 - First version with short and leverage support
INTERFACE_VERSION: int = 3
_populate_fun_len: int = 0
_buy_fun_len: int = 0
@ -80,13 +66,16 @@ class IStrategy(ABC, HyperStrategyMixin):
trailing_only_offset_is_reached = False
use_custom_stoploss: bool = False
# Can this strategy go short?
can_short: bool = False
# associated timeframe
timeframe: str
# Optional order types
order_types: Dict = {
'buy': 'limit',
'sell': 'limit',
'entry': 'limit',
'exit': 'limit',
'stoploss': 'limit',
'stoploss_on_exchange': False,
'stoploss_on_exchange_interval': 60,
@ -94,8 +83,8 @@ class IStrategy(ABC, HyperStrategyMixin):
# Optional time in force
order_time_in_force: Dict = {
'buy': 'gtc',
'sell': 'gtc',
'entry': 'gtc',
'exit': 'gtc',
}
# run "populate_indicators" only for new candle
@ -157,31 +146,41 @@ class IStrategy(ABC, HyperStrategyMixin):
if timeframe_to_minutes(informative_data.timeframe) < strategy_timeframe_minutes:
raise OperationalException('Informative timeframe must be equal or higher than '
'strategy timeframe!')
if not informative_data.candle_type:
informative_data.candle_type = config['candle_type_def']
self._ft_informative.append((informative_data, cls_method))
@abstractmethod
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
Populate indicators that will be used in the Buy, Sell, Short, Exit_short strategy
:param dataframe: DataFrame with data from the exchange
:param metadata: Additional information, like the currently traded pair
:return: a Dataframe with all mandatory indicators for the strategies
"""
return dataframe
@abstractmethod
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
DEPRECATED - please migrate to populate_entry_trend
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
"""
return dataframe
@abstractmethod
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with entry columns populated
"""
return self.populate_buy_trend(dataframe, metadata)
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
DEPRECATED - please migrate to populate_exit_trend
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
@ -189,6 +188,15 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
return self.populate_sell_trend(dataframe, metadata)
def bot_loop_start(self, **kwargs) -> None:
"""
Called at the start of the bot iteration (one loop).
@ -201,9 +209,16 @@ class IStrategy(ABC, HyperStrategyMixin):
def check_buy_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check buy timeout function callback.
This method can be used to override the buy-timeout.
It is called whenever a limit buy order has been created,
DEPRECATED: Please use `check_entry_timeout` instead.
"""
return False
def check_entry_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check entry timeout function callback.
This method can be used to override the enter-timeout.
It is called whenever a limit entry order has been created,
and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
@ -214,17 +229,25 @@ class IStrategy(ABC, HyperStrategyMixin):
:param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is cancelled.
:return bool: When True is returned, then the entry order is cancelled.
"""
return False
return self.check_buy_timeout(
pair=pair, trade=trade, order=order, current_time=current_time)
def check_sell_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
DEPRECATED: Please use `check_exit_timeout` instead.
"""
return False
def check_exit_timeout(self, pair: str, trade: Trade, order: dict,
current_time: datetime, **kwargs) -> bool:
"""
Check sell timeout function callback.
This method can be used to override the sell-timeout.
It is called whenever a limit sell order has been created,
and is not yet fully filled.
This method can be used to override the exit-timeout.
It is called whenever a (long) limit sell order or (short) limit buy
has been created, and is not yet fully filled.
Configuration options in `unfilledtimeout` will be verified before this,
so ensure to set these timeouts high enough.
@ -234,15 +257,16 @@ class IStrategy(ABC, HyperStrategyMixin):
:param order: Order dictionary as returned from CCXT.
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is cancelled.
:return bool: When True is returned, then the (long)sell/(short)buy-order is cancelled.
"""
return False
return self.check_sell_timeout(
pair=pair, trade=trade, order=order, current_time=current_time)
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
time_in_force: str, current_time: datetime, entry_tag: Optional[str],
**kwargs) -> bool:
side: str, **kwargs) -> bool:
"""
Called right before placing a buy order.
Called right before placing a entry order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
@ -250,13 +274,14 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be bought.
:param pair: Pair that's about to be bought/shorted.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in target (quote) currency that's going to be traded.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param current_time: datetime object, containing the current datetime
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the buy-order is placed on the exchange.
False aborts the process
@ -264,10 +289,10 @@ class IStrategy(ABC, HyperStrategyMixin):
return True
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
rate: float, time_in_force: str, sell_reason: str,
rate: float, time_in_force: str, exit_reason: str,
current_time: datetime, **kwargs) -> bool:
"""
Called right before placing a regular sell order.
Called right before placing a regular exit order.
Timing for this function is critical, so avoid doing heavy computations or
network requests in this method.
@ -275,18 +300,18 @@ class IStrategy(ABC, HyperStrategyMixin):
When not implemented by a strategy, returns True (always confirming).
:param pair: Pair that's about to be sold.
:param pair: Pair for trade that's about to be exited.
:param trade: trade object.
:param order_type: Order type (as configured in order_types). usually limit or market.
:param amount: Amount in quote currency.
:param rate: Rate that's going to be used when using limit orders
:param time_in_force: Time in force. Defaults to GTC (Good-til-cancelled).
:param sell_reason: Sell reason.
:param exit_reason: Exit reason.
Can be any of ['roi', 'stop_loss', 'stoploss_on_exchange', 'trailing_stop_loss',
'sell_signal', 'force_sell', 'emergency_sell']
:param current_time: datetime object, containing the current datetime
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return bool: When True is returned, then the sell-order is placed on the exchange.
:return bool: When True, then the sell-order/exit_short-order is placed on the exchange.
False aborts the process
"""
return True
@ -306,7 +331,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New stoploss value, relative to the current_rate
@ -324,7 +349,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New entry price value if provided
@ -344,7 +369,7 @@ class IStrategy(ABC, HyperStrategyMixin):
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param proposed_rate: Rate, calculated based on pricing settings in ask_strategy.
:param proposed_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return float: New exit price value if provided
@ -354,41 +379,66 @@ class IStrategy(ABC, HyperStrategyMixin):
def custom_sell(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
Custom sell signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting sell signal on a candle at specified
time. This method is not called when sell signal is set.
DEPRECATED - please use custom_exit instead.
Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting exit signal on a candle at specified
time. This method is not called when exit signal is set.
This method should be overridden to create sell signals that depend on trade parameters. For
example you could implement a sell relative to the candle when the trade was opened,
This method should be overridden to create exit signals that depend on trade parameters. For
example you could implement an exit relative to the candle when the trade was opened,
or a custom 1:2 risk-reward ROI.
Custom sell reason max length is 64. Exceeding characters will be removed.
Custom exit reason max length is 64. Exceeding characters will be removed.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute sell, return a string with custom sell reason or True. Otherwise return
:return: To execute exit, return a string with custom sell reason or True. Otherwise return
None or False.
"""
return None
def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
current_profit: float, **kwargs) -> Optional[Union[str, bool]]:
"""
Custom exit signal logic indicating that specified position should be sold. Returning a
string or True from this method is equal to setting exit signal on a candle at specified
time. This method is not called when exit signal is set.
This method should be overridden to create exit signals that depend on trade parameters. For
example you could implement an exit relative to the candle when the trade was opened,
or a custom 1:2 risk-reward ROI.
Custom exit reason max length is 64. Exceeding characters will be removed.
:param pair: Pair that's currently analyzed
:param trade: trade object.
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param current_profit: Current profit (as ratio), calculated based on current_rate.
:param **kwargs: Ensure to keep this here so updates to this won't break your strategy.
:return: To execute exit, return a string with custom sell reason or True. Otherwise return
None or False.
"""
return self.custom_sell(pair, trade, current_time, current_rate, current_profit, **kwargs)
def custom_stake_amount(self, pair: str, current_time: datetime, current_rate: float,
proposed_stake: float, min_stake: float, max_stake: float,
entry_tag: Optional[str], **kwargs) -> float:
entry_tag: Optional[str], side: str, **kwargs) -> float:
"""
Customize stake size for each new trade. This method is not called when edge module is
enabled.
Customize stake size for each new trade.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in ask_strategy.
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_stake: A stake amount proposed by the bot.
:param min_stake: Minimal stake size allowed by exchange.
:param max_stake: Balance available for trading.
:param entry_tag: Optional entry_tag (buy_tag) if provided with the buy signal.
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A stake size, which is between min_stake and max_stake.
"""
return proposed_stake
@ -416,6 +466,22 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
return None
def leverage(self, pair: str, current_time: datetime, current_rate: float,
proposed_leverage: float, max_leverage: float, side: str,
**kwargs) -> float:
"""
Customize leverage for each new trade. This method is only called in futures mode.
:param pair: Pair that's currently analyzed
:param current_time: datetime object, containing the current datetime
:param current_rate: Rate, calculated based on pricing settings in exit_pricing.
:param proposed_leverage: A leverage proposed by the bot.
:param max_leverage: Max leverage allowed on this pair
:param side: 'long' or 'short' - indicating the direction of the proposed trade
:return: A leverage amount, which is between 1.0 and max_leverage.
"""
return 1.0
def informative_pairs(self) -> ListPairsWithTimeframes:
"""
Define additional, informative pair/interval combinations to be cached from the exchange.
@ -444,16 +510,28 @@ class IStrategy(ABC, HyperStrategyMixin):
Internal method which gathers all informative pairs (user or automatically defined).
"""
informative_pairs = self.informative_pairs()
# Compatibility code for 2 tuple informative pairs
informative_pairs = [
(p[0], p[1], CandleType.from_string(p[2]) if len(
p) > 2 and p[2] != '' else self.config.get('candle_type_def', CandleType.SPOT))
for p in informative_pairs]
for inf_data, _ in self._ft_informative:
# Get default candle type if not provided explicitly.
candle_type = (inf_data.candle_type if inf_data.candle_type
else self.config.get('candle_type_def', CandleType.SPOT))
if inf_data.asset:
pair_tf = (_format_pair_name(self.config, inf_data.asset), inf_data.timeframe)
pair_tf = (
_format_pair_name(self.config, inf_data.asset),
inf_data.timeframe,
candle_type,
)
informative_pairs.append(pair_tf)
else:
if not self.dp:
raise OperationalException('@informative decorator with unspecified asset '
'requires DataProvider instance.')
for pair in self.dp.current_whitelist():
informative_pairs.append((pair, inf_data.timeframe))
informative_pairs.append((pair, inf_data.timeframe, candle_type))
return list(set(informative_pairs))
def get_strategy_name(self) -> str:
@ -498,7 +576,7 @@ class IStrategy(ABC, HyperStrategyMixin):
Checks if a pair is currently locked
The 2nd, optional parameter ensures that locks are applied until the new candle arrives,
and not stop at 14:00:00 - while the next candle arrives at 14:00:02 leaving a gap
of 2 seconds for a buy to happen on an old signal.
of 2 seconds for an entry order to happen on an old signal.
:param pair: "Pair to check"
:param candle_date: Date of the last candle. Optional, defaults to current date
:returns: locking state of the pair in question.
@ -514,15 +592,15 @@ class IStrategy(ABC, HyperStrategyMixin):
def analyze_ticker(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Parses the given candle (OHLCV) data and returns a populated DataFrame
add several TA indicators and buy signal to it
add several TA indicators and entry order signal to it
:param dataframe: Dataframe containing data from exchange
:param metadata: Metadata dictionary with additional data (e.g. 'pair')
:return: DataFrame of candle (OHLCV) data with indicator data and signals added
"""
logger.debug("TA Analysis Launched")
dataframe = self.advise_indicators(dataframe, metadata)
dataframe = self.advise_buy(dataframe, metadata)
dataframe = self.advise_sell(dataframe, metadata)
dataframe = self.advise_entry(dataframe, metadata)
dataframe = self.advise_exit(dataframe, metadata)
return dataframe
def _analyze_ticker_internal(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
@ -544,13 +622,17 @@ class IStrategy(ABC, HyperStrategyMixin):
dataframe = self.analyze_ticker(dataframe, metadata)
self._last_candle_seen_per_pair[pair] = dataframe.iloc[-1]['date']
if self.dp:
self.dp._set_cached_df(pair, self.timeframe, dataframe)
self.dp._set_cached_df(
pair, self.timeframe, dataframe,
candle_type=self.config.get('candle_type_def', CandleType.SPOT))
else:
logger.debug("Skipping TA Analysis for already analyzed candle")
dataframe['buy'] = 0
dataframe['sell'] = 0
dataframe['buy_tag'] = None
dataframe['exit_tag'] = None
dataframe[SignalType.ENTER_LONG.value] = 0
dataframe[SignalType.EXIT_LONG.value] = 0
dataframe[SignalType.ENTER_SHORT.value] = 0
dataframe[SignalType.EXIT_SHORT.value] = 0
dataframe[SignalTagType.ENTER_TAG.value] = None
dataframe[SignalTagType.EXIT_TAG.value] = None
# Other Defs in strategy that want to be called every loop here
# twitter_sell = self.watch_twitter_feed(dataframe, metadata)
@ -567,7 +649,9 @@ class IStrategy(ABC, HyperStrategyMixin):
"""
if not self.dp:
raise OperationalException("DataProvider not found.")
dataframe = self.dp.ohlcv(pair, self.timeframe)
dataframe = self.dp.ohlcv(
pair, self.timeframe, candle_type=self.config.get('candle_type_def', CandleType.SPOT)
)
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning('Empty candle (OHLCV) data for pair %s', pair)
return
@ -609,8 +693,8 @@ class IStrategy(ABC, HyperStrategyMixin):
message = ""
if dataframe is None:
message = "No dataframe returned (return statement missing?)."
elif 'buy' not in dataframe:
message = "Buy column not set."
elif 'enter_long' not in dataframe:
message = "enter_long/buy column not set."
elif df_len != len(dataframe):
message = message_template.format("length")
elif df_close != dataframe["close"].iloc[-1]:
@ -623,23 +707,24 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
raise StrategyError(message)
def get_signal(
def get_latest_candle(
self,
pair: str,
timeframe: str,
dataframe: DataFrame
) -> Tuple[bool, bool, Optional[str], Optional[str]]:
dataframe: DataFrame,
) -> Tuple[Optional[DataFrame], Optional[arrow.Arrow]]:
"""
Calculates current signal based based on the buy / sell columns of the dataframe.
Used by Bot to get the signal to buy or sell
Calculates current signal based based on the entry order or exit order
columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short
:param pair: pair in format ANT/BTC
:param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from.
:return: (Buy, Sell) A bool-tuple indicating buy/sell signal
:return: (None, None) or (Dataframe, latest_date) - corresponding to the last candle
"""
if not isinstance(dataframe, DataFrame) or dataframe.empty:
logger.warning(f'Empty candle (OHLCV) data for pair {pair}')
return False, False, None, None
return None, None
latest_date = dataframe['date'].max()
latest = dataframe.loc[dataframe['date'] == latest_date].iloc[-1]
@ -654,49 +739,124 @@ class IStrategy(ABC, HyperStrategyMixin):
'Outdated history for pair %s. Last tick is %s minutes old',
pair, int((arrow.utcnow() - latest_date).total_seconds() // 60)
)
return False, False, None, None
return None, None
return latest, latest_date
buy = latest[SignalType.BUY.value] == 1
def get_exit_signal(
self,
pair: str,
timeframe: str,
dataframe: DataFrame,
is_short: bool = None
) -> Tuple[bool, bool, Optional[str]]:
"""
Calculates current exit signal based based on the buy/short or sell/exit_short
columns of the dataframe.
Used by Bot to get the signal to exit.
depending on is_short, looks at "short" or "long" columns.
:param pair: pair in format ANT/BTC
:param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from.
:param is_short: Indicating existing trade direction.
:return: (enter, exit) A bool-tuple with enter / exit values.
"""
latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe)
if latest is None:
return False, False, None
sell = False
if SignalType.SELL.value in latest:
sell = latest[SignalType.SELL.value] == 1
if is_short:
enter = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit_ = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
buy_tag = latest.get(SignalTagType.BUY_TAG.value, None)
else:
enter = latest[SignalType.ENTER_LONG.value] == 1
exit_ = latest.get(SignalType.EXIT_LONG.value, 0) == 1
exit_tag = latest.get(SignalTagType.EXIT_TAG.value, None)
# Tags can be None, which does not resolve to False.
buy_tag = buy_tag if isinstance(buy_tag, str) else None
exit_tag = exit_tag if isinstance(exit_tag, str) else None
logger.debug('trigger: %s (pair=%s) buy=%s sell=%s',
latest['date'], pair, str(buy), str(sell))
logger.debug(f"exit-trigger: {latest['date']} (pair={pair}) "
f"enter={enter} exit={exit_}")
return enter, exit_, exit_tag
def get_entry_signal(
self,
pair: str,
timeframe: str,
dataframe: DataFrame,
) -> Tuple[Optional[SignalDirection], Optional[str]]:
"""
Calculates current entry signal based based on the buy/short or sell/exit_short
columns of the dataframe.
Used by Bot to get the signal to buy, sell, short, or exit_short
:param pair: pair in format ANT/BTC
:param timeframe: timeframe to use
:param dataframe: Analyzed dataframe to get signal from.
:return: (SignalDirection, entry_tag)
"""
latest, latest_date = self.get_latest_candle(pair, timeframe, dataframe)
if latest is None or latest_date is None:
return None, None
enter_long = latest[SignalType.ENTER_LONG.value] == 1
exit_long = latest.get(SignalType.EXIT_LONG.value, 0) == 1
enter_short = latest.get(SignalType.ENTER_SHORT.value, 0) == 1
exit_short = latest.get(SignalType.EXIT_SHORT.value, 0) == 1
enter_signal: Optional[SignalDirection] = None
enter_tag_value: Optional[str] = None
if enter_long == 1 and not any([exit_long, enter_short]):
enter_signal = SignalDirection.LONG
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
if (self.config.get('trading_mode', TradingMode.SPOT) != TradingMode.SPOT
and self.can_short
and enter_short == 1 and not any([exit_short, enter_long])):
enter_signal = SignalDirection.SHORT
enter_tag_value = latest.get(SignalTagType.ENTER_TAG.value, None)
enter_tag_value = enter_tag_value if isinstance(enter_tag_value, str) else None
timeframe_seconds = timeframe_to_seconds(timeframe)
if self.ignore_expired_candle(latest_date=latest_date,
if self.ignore_expired_candle(
latest_date=latest_date.datetime,
current_time=datetime.now(timezone.utc),
timeframe_seconds=timeframe_seconds,
buy=buy):
return False, sell, buy_tag, exit_tag
return buy, sell, buy_tag, exit_tag
enter=bool(enter_signal)
):
return None, enter_tag_value
def ignore_expired_candle(self, latest_date: datetime, current_time: datetime,
timeframe_seconds: int, buy: bool):
if self.ignore_buying_expired_candle_after and buy:
logger.debug(f"entry trigger: {latest['date']} (pair={pair}) "
f"enter={enter_long} enter_tag_value={enter_tag_value}")
return enter_signal, enter_tag_value
def ignore_expired_candle(
self,
latest_date: datetime,
current_time: datetime,
timeframe_seconds: int,
enter: bool
):
if self.ignore_buying_expired_candle_after and enter:
time_delta = current_time - (latest_date + timedelta(seconds=timeframe_seconds))
return time_delta.total_seconds() > self.ignore_buying_expired_candle_after
else:
return False
def should_sell(self, trade: Trade, rate: float, current_time: datetime, buy: bool,
sell: bool, low: float = None, high: float = None,
force_stoploss: float = 0) -> SellCheckTuple:
def should_exit(self, trade: Trade, rate: float, current_time: datetime, *,
enter: bool, exit_: bool,
low: float = None, high: float = None,
force_stoploss: float = 0) -> ExitCheckTuple:
"""
This function evaluates if one of the conditions required to trigger a sell
has been reached, which can either be a stop-loss, ROI or sell-signal.
:param low: Only used during backtesting to simulate stoploss
:param high: Only used during backtesting, to simulate ROI
This function evaluates if one of the conditions required to trigger an exit order
has been reached, which can either be a stop-loss, ROI or exit-signal.
:param low: Only used during backtesting to simulate (long)stoploss/(short)ROI
:param high: Only used during backtesting, to simulate (short)stoploss/(long)ROI
:param force_stoploss: Externally provided stoploss
:return: True if trade should be sold, False otherwise
:return: True if trade should be exited, False otherwise
"""
current_rate = rate
current_profit = trade.calc_profit_ratio(current_rate)
@ -708,15 +868,15 @@ class IStrategy(ABC, HyperStrategyMixin):
force_stoploss=force_stoploss, low=low, high=high)
# Set current rate to high for backtesting sell
current_rate = high or rate
current_rate = (low if trade.is_short else high) or rate
current_profit = trade.calc_profit_ratio(current_rate)
# if buy signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (buy and self.ignore_roi_if_buy_signal)
# if enter signal and ignore_roi is set, we don't need to evaluate min_roi.
roi_reached = (not (enter and self.ignore_roi_if_buy_signal)
and self.min_roi_reached(trade=trade, current_profit=current_profit,
current_time=current_time))
sell_signal = SellType.NONE
sell_signal = ExitType.NONE
custom_reason = ''
# use provided rate in backtesting, not high/low.
current_rate = rate
@ -725,54 +885,54 @@ class IStrategy(ABC, HyperStrategyMixin):
if (self.sell_profit_only and current_profit <= self.sell_profit_offset):
# sell_profit_only and profit doesn't reach the offset - ignore sell signal
pass
elif self.use_sell_signal and not buy:
if sell:
sell_signal = SellType.SELL_SIGNAL
elif self.use_sell_signal and not enter:
if exit_:
sell_signal = ExitType.SELL_SIGNAL
else:
custom_reason = strategy_safe_wrapper(self.custom_sell, default_retval=False)(
trade_type = "exit_short" if trade.is_short else "sell"
custom_reason = strategy_safe_wrapper(self.custom_exit, default_retval=False)(
pair=trade.pair, trade=trade, current_time=current_time,
current_rate=current_rate, current_profit=current_profit)
if custom_reason:
sell_signal = SellType.CUSTOM_SELL
sell_signal = ExitType.CUSTOM_SELL
if isinstance(custom_reason, str):
if len(custom_reason) > CUSTOM_SELL_MAX_LENGTH:
logger.warning(f'Custom sell reason returned from custom_sell is too '
f'long and was trimmed to {CUSTOM_SELL_MAX_LENGTH} '
f'characters.')
custom_reason = custom_reason[:CUSTOM_SELL_MAX_LENGTH]
if len(custom_reason) > CUSTOM_EXIT_MAX_LENGTH:
logger.warning(f'Custom {trade_type} reason returned from '
f'custom_exit is too long and was trimmed'
f'to {CUSTOM_EXIT_MAX_LENGTH} characters.')
custom_reason = custom_reason[:CUSTOM_EXIT_MAX_LENGTH]
else:
custom_reason = None
if sell_signal in (SellType.CUSTOM_SELL, SellType.SELL_SIGNAL):
if sell_signal in (ExitType.CUSTOM_SELL, ExitType.SELL_SIGNAL):
logger.debug(f"{trade.pair} - Sell signal received. "
f"sell_type=SellType.{sell_signal.name}" +
f"sell_type=ExitType.{sell_signal.name}" +
(f", custom_reason={custom_reason}" if custom_reason else ""))
return SellCheckTuple(sell_type=sell_signal, sell_reason=custom_reason)
return ExitCheckTuple(exit_type=sell_signal, exit_reason=custom_reason)
# Start evaluations
# Sequence:
# Sell-signal
# Exit-signal
# ROI (if not stoploss)
# Stoploss
if roi_reached and stoplossflag.sell_type != SellType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_type=SellType.ROI")
return SellCheckTuple(sell_type=SellType.ROI)
if roi_reached and stoplossflag.exit_type != ExitType.STOP_LOSS:
logger.debug(f"{trade.pair} - Required profit reached. sell_type=ExitType.ROI")
return ExitCheckTuple(exit_type=ExitType.ROI)
if stoplossflag.sell_flag:
if stoplossflag.exit_flag:
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.sell_type}")
logger.debug(f"{trade.pair} - Stoploss hit. sell_type={stoplossflag.exit_type}")
return stoplossflag
# This one is noisy, commented out...
# logger.debug(f"{trade.pair} - No sell signal.")
return SellCheckTuple(sell_type=SellType.NONE)
# logger.debug(f"{trade.pair} - No exit signal.")
return ExitCheckTuple(exit_type=ExitType.NONE)
def stop_loss_reached(self, current_rate: float, trade: Trade,
current_time: datetime, current_profit: float,
force_stoploss: float, low: float = None,
high: float = None) -> SellCheckTuple:
high: float = None) -> ExitCheckTuple:
"""
Based on current profit of the trade and configured (trailing) stoploss,
decides to sell or not
decides to exit or not
:param current_profit: current profit as ratio
:param low: Low value of this candle, only set in backtesting
:param high: High value of this candle, only set in backtesting
@ -782,7 +942,12 @@ class IStrategy(ABC, HyperStrategyMixin):
# Initiate stoploss with open_rate. Does nothing if stoploss is already set.
trade.adjust_stop_loss(trade.open_rate, stop_loss_value, initial=True)
if self.use_custom_stoploss and trade.stop_loss < (low or current_rate):
dir_correct = (trade.stop_loss < (low or current_rate)
if not trade.is_short else
trade.stop_loss > (high or current_rate)
)
if self.use_custom_stoploss and dir_correct:
stop_loss_value = strategy_safe_wrapper(self.custom_stoploss, default_retval=None
)(pair=trade.pair, trade=trade,
current_time=current_time,
@ -795,45 +960,56 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
logger.warning("CustomStoploss function did not return valid stoploss")
if self.trailing_stop and trade.stop_loss < (low or current_rate):
sl_lower_long = (trade.stop_loss < (low or current_rate) and not trade.is_short)
sl_higher_short = (trade.stop_loss > (high or current_rate) and trade.is_short)
if self.trailing_stop and (sl_lower_long or sl_higher_short):
# trailing stoploss handling
sl_offset = self.trailing_stop_positive_offset
# Make sure current_profit is calculated using high for backtesting.
high_profit = current_profit if not high else trade.calc_profit_ratio(high)
bound = low if trade.is_short else high
bound_profit = current_profit if not bound else trade.calc_profit_ratio(bound)
# Don't update stoploss if trailing_only_offset_is_reached is true.
if not (self.trailing_only_offset_is_reached and high_profit < sl_offset):
if not (self.trailing_only_offset_is_reached and bound_profit < sl_offset):
# Specific handling for trailing_stop_positive
if self.trailing_stop_positive is not None and high_profit > sl_offset:
if self.trailing_stop_positive is not None and bound_profit > sl_offset:
stop_loss_value = self.trailing_stop_positive
logger.debug(f"{trade.pair} - Using positive stoploss: {stop_loss_value} "
f"offset: {sl_offset:.4g} profit: {current_profit:.2%}")
trade.adjust_stop_loss(high or current_rate, stop_loss_value)
trade.adjust_stop_loss(bound or current_rate, stop_loss_value)
sl_higher_long = (trade.stop_loss >= (low or current_rate) and not trade.is_short)
sl_lower_short = (trade.stop_loss <= (high or current_rate) and trade.is_short)
# evaluate if the stoploss was hit if stoploss is not on exchange
# in Dry-Run, this handles stoploss logic as well, as the logic will not be different to
# regular stoploss handling.
if ((trade.stop_loss >= (low or current_rate)) and
if ((sl_higher_long or sl_lower_short) and
(not self.order_types.get('stoploss_on_exchange') or self.config['dry_run'])):
sell_type = SellType.STOP_LOSS
sell_type = ExitType.STOP_LOSS
# If initial stoploss is not the same as current one then it is trailing.
if trade.initial_stop_loss != trade.stop_loss:
sell_type = SellType.TRAILING_STOP_LOSS
sell_type = ExitType.TRAILING_STOP_LOSS
logger.debug(
f"{trade.pair} - HIT STOP: current price at {(low or current_rate):.6f}, "
f"{trade.pair} - HIT STOP: current price at "
f"{((high if trade.is_short else low) or current_rate):.6f}, "
f"stoploss is {trade.stop_loss:.6f}, "
f"initial stoploss was at {trade.initial_stop_loss:.6f}, "
f"trade opened at {trade.open_rate:.6f}")
new_stoploss = (
trade.stop_loss + trade.initial_stop_loss
if trade.is_short else
trade.stop_loss - trade.initial_stop_loss
)
logger.debug(f"{trade.pair} - Trailing stop saved "
f"{trade.stop_loss - trade.initial_stop_loss:.6f}")
f"{new_stoploss:.6f}")
return SellCheckTuple(sell_type=sell_type)
return ExitCheckTuple(exit_type=sell_type)
return SellCheckTuple(sell_type=SellType.NONE)
return ExitCheckTuple(exit_type=ExitType.NONE)
def min_roi_reached_entry(self, trade_dur: int) -> Tuple[Optional[int], Optional[float]]:
"""
@ -863,22 +1039,24 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
return current_profit > roi
def ft_check_timed_out(self, side: str, trade: LocalTrade, order: Order,
def ft_check_timed_out(self, trade: LocalTrade, order: Order,
current_time: datetime) -> bool:
"""
FT Internal method.
Check if timeout is active, and if the order is still open and timed out
"""
side = 'entry' if order.ft_order_side == trade.enter_side else 'exit'
timeout = self.config.get('unfilledtimeout', {}).get(side)
if timeout is not None:
timeout_unit = self.config.get('unfilledtimeout', {}).get('unit', 'minutes')
timeout_kwargs = {timeout_unit: -timeout}
timeout_threshold = current_time + timedelta(**timeout_kwargs)
timedout = (order.status == 'open' and order.side == side
and order.order_date_utc < timeout_threshold)
timedout = (order.status == 'open' and order.order_date_utc < timeout_threshold)
if timedout:
return True
time_method = self.check_sell_timeout if order.side == 'sell' else self.check_buy_timeout
time_method = (self.check_exit_timeout if order.side == trade.exit_side
else self.check_entry_timeout)
return strategy_safe_wrapper(time_method,
default_retval=False)(
@ -888,7 +1066,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def advise_all_indicators(self, data: Dict[str, DataFrame]) -> Dict[str, DataFrame]:
"""
Populates indicators for given candle (OHLCV) data (for multiple pairs)
Does not run advise_buy or advise_sell!
Does not run advise_entry or advise_exit!
Used by optimize operations only, not during dry / live runs.
Using .copy() to get a fresh copy of the dataframe for every strategy run.
Also copy on output to avoid PerformanceWarnings pandas 1.3.0 started to show.
@ -900,7 +1078,7 @@ class IStrategy(ABC, HyperStrategyMixin):
def advise_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Populate indicators that will be used in the Buy and Sell strategy
Populate indicators that will be used in the Buy, Sell, short, exit_short strategy
This method should not be overridden.
:param dataframe: Dataframe with data from the exchange
:param metadata: Additional information, like the currently traded pair
@ -920,37 +1098,46 @@ class IStrategy(ABC, HyperStrategyMixin):
else:
return self.populate_indicators(dataframe, metadata)
def advise_buy(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def advise_entry(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
Based on TA indicators, populates the entry order signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param metadata: Additional information dictionary, with details like the
currently traded pair
:return: DataFrame with buy column
"""
logger.debug(f"Populating buy signals for pair {metadata.get('pair')}.")
logger.debug(f"Populating enter signals for pair {metadata.get('pair')}.")
if self._buy_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_buy_trend(dataframe) # type: ignore
df = self.populate_buy_trend(dataframe) # type: ignore
else:
return self.populate_buy_trend(dataframe, metadata)
df = self.populate_entry_trend(dataframe, metadata)
if 'enter_long' not in df.columns:
df = df.rename({'buy': 'enter_long', 'buy_tag': 'enter_tag'}, axis='columns')
def advise_sell(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
return df
def advise_exit(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
Based on TA indicators, populates the exit order signal for the given dataframe
This method should not be overridden.
:param dataframe: DataFrame
:param metadata: Additional information dictionary, with details like the
currently traded pair
:return: DataFrame with sell column
"""
logger.debug(f"Populating sell signals for pair {metadata.get('pair')}.")
logger.debug(f"Populating exit signals for pair {metadata.get('pair')}.")
if self._sell_fun_len == 2:
warnings.warn("deprecated - check out the Sample strategy to see "
"the current function headers!", DeprecationWarning)
return self.populate_sell_trend(dataframe) # type: ignore
df = self.populate_sell_trend(dataframe) # type: ignore
else:
return self.populate_sell_trend(dataframe, metadata)
df = self.populate_exit_trend(dataframe, metadata)
if 'exit_long' not in df.columns:
df = df.rename({'sell': 'exit_long'}, axis='columns')
return df

View File

@ -66,7 +66,11 @@ def merge_informative_pair(dataframe: pd.DataFrame, informative: pd.DataFrame,
return dataframe
def stoploss_from_open(open_relative_stop: float, current_profit: float) -> float:
def stoploss_from_open(
open_relative_stop: float,
current_profit: float,
is_short: bool = False
) -> float:
"""
Given the current profit, and a desired stop loss value relative to the open price,
@ -76,24 +80,29 @@ def stoploss_from_open(open_relative_stop: float, current_profit: float) -> floa
The requested stop can be positive for a stop above the open price, or negative for
a stop below the open price. The return value is always >= 0.
Returns 0 if the resulting stop price would be above the current price.
Returns 0 if the resulting stop price would be above/below (longs/shorts) the current price
:param open_relative_stop: Desired stop loss percentage relative to open price
:param current_profit: The current profit percentage
:return: Positive stop loss value relative to current price
:param is_short: When true, perform the calculation for short instead of long
:return: Stop loss value relative to current price
"""
# formula is undefined for current_profit -1, return maximum value
if current_profit == -1:
# formula is undefined for current_profit -1 (longs) or 1 (shorts), return maximum value
if (current_profit == -1 and not is_short) or (is_short and current_profit == 1):
return 1
if is_short is True:
stoploss = -1+((1-open_relative_stop)/(1-current_profit))
else:
stoploss = 1-((1+open_relative_stop)/(1+current_profit))
# negative stoploss values indicate the requested stop price is higher than the current price
# negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price
return max(stoploss, 0.0)
def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
def stoploss_from_absolute(stop_rate: float, current_rate: float, is_short: bool = False) -> float:
"""
Given current price and desired stop price, return a stop loss value that is relative to current
price.
@ -105,6 +114,7 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
:param stop_rate: Stop loss price.
:param current_rate: Current asset price.
:param is_short: When true, perform the calculation for short instead of long
:return: Positive stop loss value relative to current price
"""
@ -113,6 +123,10 @@ def stoploss_from_absolute(stop_rate: float, current_rate: float) -> float:
return 1
stoploss = 1 - (stop_rate / current_rate)
if is_short:
stoploss = -stoploss
# negative stoploss values indicate the requested stop price is higher than the current price
return max(stoploss, 0.0)
# negative stoploss values indicate the requested stop price is higher/lower
# (long/short) than the current price
# shorts can yield stoploss values higher than 1, so limit that as well
return max(min(stoploss, 1.0), 0.0)

View File

@ -13,24 +13,26 @@
"fiat_display_currency": "{{ fiat_display_currency }}",{{ ('\n "timeframe": "' + timeframe + '",') if timeframe else '' }}
"dry_run": {{ dry_run | lower }},
"cancel_open_orders_on_exit": false,
"trading_mode": "{{ trading_mode }}",
"margin_mode": "{{ margin_mode }}",
"unfilledtimeout": {
"buy": 10,
"sell": 10,
"entry": 10,
"exit": 10,
"exit_timeout_count": 0,
"unit": "minutes"
},
"bid_strategy": {
"price_side": "bid",
"ask_last_balance": 0.0,
"entry_pricing": {
"price_side": "same",
"use_order_book": true,
"order_book_top": 1,
"price_last_balance": 0.0,
"check_depth_of_market": {
"enabled": false,
"bids_to_ask_delta": 1
}
},
"ask_strategy": {
"price_side": "ask",
"exit_pricing":{
"price_side": "same",
"use_order_book": true,
"order_book_top": 1
},

View File

@ -29,17 +29,20 @@ class {{ strategy }}(IStrategy):
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 2
INTERFACE_VERSION = 3
# Optimal timeframe for the strategy.
timeframe = '5m'
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
minimal_roi = {
@ -61,7 +64,7 @@ class {{ strategy }}(IStrategy):
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False
# These values can be overridden in the "ask_strategy" section in the config.
# These values can be overridden in the config.
use_sell_signal = True
sell_profit_only = False
ignore_roi_if_buy_signal = False
@ -75,16 +78,16 @@ class {{ strategy }}(IStrategy):
# Optional order type mapping.
order_types = {
'buy': 'limit',
'sell': 'limit',
'entry': 'limit',
'exit': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
# Optional order time in force.
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc'
'entry': 'gtc',
'exit': 'gtc'
}
{{ plot_config | indent(4) }}
@ -116,34 +119,52 @@ class {{ strategy }}(IStrategy):
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
{{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'buy'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
'enter_long'] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
"""
dataframe.loc[
(
{{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'sell'] = 1
'enter_short'] = 1
"""
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(
{{ sell_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_long'] = 1
# Uncomment to use shorts (Only used in futures/margin mode. Check the documentation for more info)
"""
dataframe.loc[
(
{{ buy_trend | indent(16) }}
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
"""
return dataframe
{{ additional_methods | indent(4) }}

View File

@ -29,13 +29,16 @@ class SampleStrategy(IStrategy):
You must keep:
- the lib in the section "Do not remove these libs"
- the methods: populate_indicators, populate_buy_trend, populate_sell_trend
- the methods: populate_indicators, populate_entry_trend, populate_exit_trend
You should keep:
- timeframe, minimal_roi, stoploss, trailing_*
"""
# Strategy interface version - allow new iterations of the strategy interface.
# Check the documentation or the Sample strategy to get the latest version.
INTERFACE_VERSION = 2
INTERFACE_VERSION = 3
# Can this strategy go short?
can_short: bool = False
# Minimal ROI designed for the strategy.
# This attribute will be overridden if the config file contains "minimal_roi".
@ -55,36 +58,38 @@ class SampleStrategy(IStrategy):
# trailing_stop_positive = 0.01
# trailing_stop_positive_offset = 0.0 # Disabled / not configured
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
# Optimal timeframe for the strategy.
timeframe = '5m'
# Run "populate_indicators()" only for new candle.
process_only_new_candles = False
# These values can be overridden in the "ask_strategy" section in the config.
# These values can be overridden in the config.
use_sell_signal = True
sell_profit_only = False
ignore_roi_if_buy_signal = False
# Hyperoptable parameters
buy_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
sell_rsi = IntParameter(low=50, high=100, default=70, space='sell', optimize=True, load=True)
short_rsi = IntParameter(low=51, high=100, default=70, space='sell', optimize=True, load=True)
exit_short_rsi = IntParameter(low=1, high=50, default=30, space='buy', optimize=True, load=True)
# Number of candles the strategy requires before producing valid signals
startup_candle_count: int = 30
# Optional order type mapping.
order_types = {
'buy': 'limit',
'sell': 'limit',
'entry': 'limit',
'exit': 'limit',
'stoploss': 'market',
'stoploss_on_exchange': False
}
# Optional order time in force.
order_time_in_force = {
'buy': 'gtc',
'sell': 'gtc'
'entry': 'gtc',
'exit': 'gtc'
}
plot_config = {
@ -337,12 +342,12 @@ class SampleStrategy(IStrategy):
return dataframe
def populate_buy_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the buy signal for the given dataframe
:param dataframe: DataFrame populated with indicators
Based on TA indicators, populates the entry signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with buy column
:return: DataFrame with entry columns populated
"""
dataframe.loc[
(
@ -352,16 +357,26 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'buy'] = 1
'enter_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 70
(qtpylib.crossed_above(dataframe['rsi'], self.short_rsi.value)) &
(dataframe['tema'] > dataframe['bb_middleband']) & # Guard: tema above BB middle
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'enter_short'] = 1
return dataframe
def populate_sell_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Based on TA indicators, populates the sell signal for the given dataframe
:param dataframe: DataFrame populated with indicators
Based on TA indicators, populates the exit signal for the given dataframe
:param dataframe: DataFrame
:param metadata: Additional information, like the currently traded pair
:return: DataFrame with sell column
:return: DataFrame with exit columns populated
"""
dataframe.loc[
(
@ -371,5 +386,18 @@ class SampleStrategy(IStrategy):
(dataframe['tema'] < dataframe['tema'].shift(1)) & # Guard: tema is falling
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'sell'] = 1
'exit_long'] = 1
dataframe.loc[
(
# Signal: RSI crosses above 30
(qtpylib.crossed_above(dataframe['rsi'], self.exit_short_rsi.value)) &
# Guard: tema below BB middle
(dataframe['tema'] <= dataframe['bb_middleband']) &
(dataframe['tema'] > dataframe['tema'].shift(1)) & # Guard: tema is raising
(dataframe['volume'] > 0) # Make sure Volume is not 0
),
'exit_short'] = 1
return dataframe

Some files were not shown because too many files have changed in this diff Show More