Skip to content

Commit 428d139

Browse files
authored
Merge pull request freqtrade#12874 from stash86/main-stash
Add CrossMarketPairList
2 parents 9dd2a9e + aabda8d commit 428d139

5 files changed

Lines changed: 354 additions & 2 deletions

File tree

build_helpers/schema.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@
649649
"ProducerPairList",
650650
"RemotePairList",
651651
"MarketCapPairList",
652+
"CrossMarketPairList",
652653
"AgeFilter",
653654
"DelistFilter",
654655
"FullTradesFilter",

docs/includes/pairlists.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
Pairlist Handlers define the list of pairs (pairlist) that the bot should trade. They are configured in the `pairlists` section of the configuration settings.
44

5-
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
5+
In your configuration, you can use Static Pairlist (defined by the [`StaticPairList`](#static-pair-list) Pairlist Handler) and Dynamic Pairlist (defined by the [`VolumePairList`](#volume-pair-list), [`CrossMarketPairList`](#crossmarketpairlist), [`MarketCapPairlist`](#marketcappairlist) and [`PercentChangePairList`](#percent-change-pair-list) Pairlist Handlers).
66

77
Additionally, [`AgeFilter`](#agefilter), [`DelistFilter`](#delistfilter), [`PrecisionFilter`](#precisionfilter), [`PriceFilter`](#pricefilter), [`ShuffleFilter`](#shufflefilter), [`SpreadFilter`](#spreadfilter) and [`VolatilityFilter`](#volatilityfilter) act as Pairlist Filters, removing certain pairs and/or moving their positions in the pairlist.
88

9-
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList` or `PercentChangePairList` as the starting Pairlist Handler.
9+
If multiple Pairlist Handlers are used, they are chained and a combination of all Pairlist Handlers forms the resulting pairlist the bot uses for trading and backtesting. Pairlist Handlers are executed in the sequence they are configured. You can define either `StaticPairList`, `VolumePairList`, `ProducerPairList`, `RemotePairList`, `MarketCapPairList`, `PercentChangePairList` or `CrossMarketPairList` as the starting Pairlist Handler.
1010

1111
Inactive markets are always removed from the resulting pairlist. Explicitly blacklisted pairs (those in the `pair_blacklist` configuration setting) are also always removed from the resulting pairlist.
1212

@@ -26,6 +26,7 @@ You may also use something like `.*DOWN/BTC` or `.*UP/BTC` to exclude leveraged
2626
* [`ProducerPairList`](#producerpairlist)
2727
* [`RemotePairList`](#remotepairlist)
2828
* [`MarketCapPairList`](#marketcappairlist)
29+
* [`CrossMarketPairList`](#crossmarketpairlist)
2930
* [`AgeFilter`](#agefilter)
3031
* [`DelistFilter`](#delistfilter)
3132
* [`FullTradesFilter`](#fulltradesfilter)
@@ -402,6 +403,12 @@ Coins like 1000PEPE/USDT or KPEPE/USDT:USDT are detected on a best effort basis,
402403
!!! Danger "Duplicate symbols in coingecko"
403404
Coingecko often has duplicate symbols, where the same symbol is used for different coins. Freqtrade will use the symbol as is and try to search for it on the exchange. If the symbol exists - it will be used. Freqtrade will however not check if the _intended_ symbol is the one coingecko meant. This can sometimes lead to unexpected results, especially on low volume coins or with meme coin categories.
404405

406+
#### CrossMarketPairList
407+
408+
Generate or filter pairs based of their availability on the opposite market.
409+
410+
The `pairs_exis_on` setting defines whether the pairs should exists on both spot and futures market (`both_markets`) or only exist on the specified trading mode (`current_market_only`). By default, the plugin will be in `both_markets` setting, which means whitelisted pairs have to exists on both spot and futures markets.
411+
405412
#### AgeFilter
406413

407414
Removes pairs that have been listed on the exchange for less than `min_days_listed` days (defaults to `10`) or more than `max_days_listed` days (defaults `None` mean infinity).

freqtrade/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
"ProducerPairList",
6262
"RemotePairList",
6363
"MarketCapPairList",
64+
"CrossMarketPairList",
6465
"AgeFilter",
6566
"DelistFilter",
6667
"FullTradesFilter",
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
"""Cross Market pair list filter"""
2+
3+
import logging
4+
5+
from freqtrade.constants import PairPrefixes
6+
from freqtrade.exchange.exchange_types import Tickers
7+
from freqtrade.plugins.pairlist.IPairList import IPairList, PairlistParameter, SupportsBacktesting
8+
from freqtrade.util import FtTTLCache
9+
10+
11+
logger = logging.getLogger(__name__)
12+
13+
14+
class CrossMarketPairList(IPairList):
15+
is_pairlist_generator = True
16+
supports_backtesting = SupportsBacktesting.BIASED
17+
18+
def __init__(self, *args, **kwargs) -> None:
19+
super().__init__(*args, **kwargs)
20+
21+
self._pairs_exist_on: str = self._pairlistconfig.get("pairs_exist_on", "both_markets")
22+
self._stake_currency: str = self._config["stake_currency"]
23+
self._target_mode = "spot" if self._config["trading_mode"] == "futures" else "futures"
24+
self._refresh_period = self._pairlistconfig.get("refresh_period", 1800)
25+
self._pair_cache: FtTTLCache = FtTTLCache(maxsize=1, ttl=self._refresh_period)
26+
27+
@property
28+
def needstickers(self) -> bool:
29+
"""
30+
Boolean property defining if tickers are necessary.
31+
If no Pairlist requires tickers, an empty Dict is passed
32+
as tickers argument to filter_pairlist
33+
"""
34+
return False
35+
36+
def short_desc(self) -> str:
37+
"""
38+
Short whitelist method description - used for startup-messages
39+
"""
40+
pairs_exist_on = self._pairs_exist_on
41+
msg = f"{self.name} - Pairs that exists on {pairs_exist_on.capitalize()}."
42+
return msg
43+
44+
@staticmethod
45+
def description() -> str:
46+
return "Filter pairs if they exist or not on another market."
47+
48+
@staticmethod
49+
def available_parameters() -> dict[str, PairlistParameter]:
50+
return {
51+
"pairs_exist_on": {
52+
"type": "option",
53+
"default": "both_markets",
54+
"options": ["current_market_only", "both_markets"],
55+
"description": "Mode of operation",
56+
"help": "Mode of operation (current_market_only/both_markets)",
57+
},
58+
**IPairList.refresh_period_parameter(),
59+
}
60+
61+
def get_base_list(self) -> list[str]:
62+
target_mode = self._target_mode
63+
spot_only = True if target_mode == "spot" else False
64+
futures_only = True if target_mode == "futures" else False
65+
bases = [
66+
v.get("base", "")
67+
for _, v in self._exchange.get_markets(
68+
quote_currencies=[self._stake_currency],
69+
tradable_only=False,
70+
active_only=True,
71+
spot_only=spot_only,
72+
futures_only=futures_only,
73+
).items()
74+
]
75+
return bases
76+
77+
def gen_pairlist(self, tickers: Tickers) -> list[str]:
78+
"""
79+
Generate the pairlist
80+
:param tickers: Tickers (from exchange.get_tickers). May be cached.
81+
:return: List of pairs
82+
"""
83+
# Generate dynamic whitelist
84+
# Must always run if this pairlist is the first in the list.
85+
pairlist = self._pair_cache.get("pairlist")
86+
if pairlist:
87+
# Item found - no refresh necessary
88+
return pairlist.copy()
89+
else:
90+
# Use fresh pairlist
91+
# Check if pair quote currency equals to the stake currency.
92+
_pairlist = [
93+
k
94+
for k in self._exchange.get_markets(
95+
quote_currencies=[self._stake_currency], tradable_only=True, active_only=True
96+
).keys()
97+
]
98+
99+
_pairlist = self.verify_blacklist(_pairlist, logger.info)
100+
101+
pairlist = self.filter_pairlist(_pairlist, tickers)
102+
self._pair_cache["pairlist"] = pairlist.copy()
103+
104+
return pairlist
105+
106+
def filter_pairlist(self, pairlist: list[str], tickers: Tickers) -> list[str]:
107+
bases = self.get_base_list()
108+
pairs_exist_on = self._pairs_exist_on
109+
is_whitelist_mode = pairs_exist_on == "both_markets"
110+
whitelisted_pairlist: list[str] = []
111+
filtered_pairlist = pairlist.copy()
112+
113+
for pair in pairlist:
114+
base = self._exchange.get_pair_base_currency(pair)
115+
if not base:
116+
self.log_once(
117+
f"Unable to get base currency for pair {pair}, skipping it.", logger.warning
118+
)
119+
filtered_pairlist.remove(pair)
120+
continue
121+
found_in_bases = base in bases
122+
if not found_in_bases:
123+
for prefix in PairPrefixes:
124+
# Check in case of PEPE needs to be changed into 1000PEPE for example
125+
test_prefix = f"{prefix}{base}"
126+
found_in_bases = test_prefix in bases
127+
if found_in_bases:
128+
break
129+
130+
# Avoid false positive since there are KAVA and AVA pairs, which aren't related
131+
if prefix != "K":
132+
# Check in case of 1000PEPE needs to be changed into PEPE for example
133+
if base.startswith(prefix):
134+
temp_base = base.removeprefix(prefix)
135+
found_in_bases = temp_base in bases
136+
if found_in_bases:
137+
break
138+
if found_in_bases:
139+
whitelisted_pairlist.append(pair)
140+
filtered_pairlist.remove(pair)
141+
142+
return whitelisted_pairlist if is_whitelist_mode else filtered_pairlist

0 commit comments

Comments
 (0)