mirror of
https://github.com/freqtrade/freqtrade.git
synced 2025-11-29 08:33:07 +00:00
Merge branch 'develop' into feat/pairlistconfig
This commit is contained in:
@@ -8,7 +8,7 @@ repos:
|
||||
# stages: [push]
|
||||
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: "v1.0.1"
|
||||
rev: "v1.3.0"
|
||||
hooks:
|
||||
- id: mypy
|
||||
exclude: build_helpers
|
||||
|
||||
@@ -76,7 +76,7 @@ pip install -r requirements-freqai.txt
|
||||
|
||||
### Usage with docker
|
||||
|
||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices.
|
||||
If you are using docker, a dedicated tag with FreqAI dependencies is available as `:freqai`. As such - you can replace the image line in your docker compose file with `image: freqtradeorg/freqtrade:develop_freqai`. This image contains the regular FreqAI dependencies. Similar to native installs, Catboost will not be available on ARM based devices. If you would like to use PyTorch or Reinforcement learning, you should use the torch or RL tags, `image: freqtradeorg/freqtrade:develop_freqaitorch`, `image: freqtradeorg/freqtrade:develop_freqairl`.
|
||||
|
||||
!!! note "docker-compose-freqai.yml"
|
||||
We do provide an explicit docker-compose file for this in `docker/docker-compose-freqai.yml` - which can be used via `docker compose -f docker/docker-compose-freqai.yml run ...` - or can be copied to replace the original docker file. This docker-compose file also contains a (disabled) section to enable GPU resources within docker containers. This obviously assumes the system has GPU resources available.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import csv
|
||||
import logging
|
||||
import sys
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Union
|
||||
|
||||
import rapidjson
|
||||
from colorama import Fore, Style
|
||||
@@ -11,9 +11,10 @@ from tabulate import tabulate
|
||||
from freqtrade.configuration import setup_utils_configuration
|
||||
from freqtrade.enums import RunMode
|
||||
from freqtrade.exceptions import OperationalException
|
||||
from freqtrade.exchange import market_is_active, validate_exchanges
|
||||
from freqtrade.exchange import list_available_exchanges, market_is_active
|
||||
from freqtrade.misc import parse_db_uri_for_logging, plural
|
||||
from freqtrade.resolvers import ExchangeResolver, StrategyResolver
|
||||
from freqtrade.types import ValidExchangesType
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -25,18 +26,42 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
|
||||
:param args: Cli args from Arguments()
|
||||
:return: None
|
||||
"""
|
||||
exchanges = validate_exchanges(args['list_exchanges_all'])
|
||||
exchanges = list_available_exchanges(args['list_exchanges_all'])
|
||||
|
||||
if args['print_one_column']:
|
||||
print('\n'.join([e[0] for e in exchanges]))
|
||||
print('\n'.join([e['name'] for e in exchanges]))
|
||||
else:
|
||||
headers = {
|
||||
'name': 'Exchange name',
|
||||
'valid': 'Valid',
|
||||
'supported': 'Supported',
|
||||
'trade_modes': 'Markets',
|
||||
'comment': 'Reason',
|
||||
}
|
||||
|
||||
def build_entry(exchange: ValidExchangesType, valid: bool):
|
||||
valid_entry = {'valid': exchange['valid']} if valid else {}
|
||||
result: Dict[str, Union[str, bool]] = {
|
||||
'name': exchange['name'],
|
||||
**valid_entry,
|
||||
'supported': 'Official' if exchange['supported'] else '',
|
||||
'trade_modes': ', '.join(
|
||||
(f"{a['margin_mode']} " if a['margin_mode'] else '') + a['trading_mode']
|
||||
for a in exchange['trade_modes']
|
||||
),
|
||||
'comment': exchange['comment'],
|
||||
}
|
||||
|
||||
return result
|
||||
|
||||
if args['list_exchanges_all']:
|
||||
print("All exchanges supported by the ccxt library:")
|
||||
exchanges = [build_entry(e, True) for e in exchanges]
|
||||
else:
|
||||
print("Exchanges available for Freqtrade:")
|
||||
exchanges = [e for e in exchanges if e[1] is not False]
|
||||
exchanges = [build_entry(e, False) for e in exchanges if e['valid'] is not False]
|
||||
|
||||
print(tabulate(exchanges, headers=['Exchange name', 'Valid', 'reason']))
|
||||
print(tabulate(exchanges, headers=headers, ))
|
||||
|
||||
|
||||
def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
|
||||
|
||||
@@ -13,11 +13,11 @@ from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_c
|
||||
amount_to_contracts, amount_to_precision,
|
||||
available_exchanges, ccxt_exchanges,
|
||||
contracts_to_amount, date_minus_candles,
|
||||
is_exchange_known_ccxt, market_is_active,
|
||||
price_to_precision, timeframe_to_minutes,
|
||||
timeframe_to_msecs, timeframe_to_next_date,
|
||||
timeframe_to_prev_date, timeframe_to_seconds,
|
||||
validate_exchange, validate_exchanges)
|
||||
is_exchange_known_ccxt, list_available_exchanges,
|
||||
market_is_active, price_to_precision,
|
||||
timeframe_to_minutes, timeframe_to_msecs,
|
||||
timeframe_to_next_date, timeframe_to_prev_date,
|
||||
timeframe_to_seconds, validate_exchange)
|
||||
from freqtrade.exchange.gate import Gate
|
||||
from freqtrade.exchange.hitbtc import Hitbtc
|
||||
from freqtrade.exchange.huobi import Huobi
|
||||
|
||||
@@ -9,7 +9,9 @@ import ccxt
|
||||
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
|
||||
TRUNCATE, decimal_to_precision)
|
||||
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||
from freqtrade.exchange.common import (BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED,
|
||||
SUPPORTED_EXCHANGES)
|
||||
from freqtrade.types import ValidExchangesType
|
||||
from freqtrade.util import FtPrecise
|
||||
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts
|
||||
|
||||
@@ -55,14 +57,41 @@ def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
||||
return True, ''
|
||||
|
||||
|
||||
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
|
||||
def _build_exchange_list_entry(
|
||||
exchange_name: str, exchangeClasses: Dict[str, Any]) -> ValidExchangesType:
|
||||
valid, comment = validate_exchange(exchange_name)
|
||||
result: ValidExchangesType = {
|
||||
'name': exchange_name,
|
||||
'valid': valid,
|
||||
'supported': exchange_name.lower() in SUPPORTED_EXCHANGES,
|
||||
'comment': comment,
|
||||
'trade_modes': [{'trading_mode': 'spot', 'margin_mode': ''}],
|
||||
}
|
||||
if resolved := exchangeClasses.get(exchange_name.lower()):
|
||||
supported_modes = [{'trading_mode': 'spot', 'margin_mode': ''}] + [
|
||||
{'trading_mode': tm.value, 'margin_mode': mm.value}
|
||||
for tm, mm in resolved['class']._supported_trading_mode_margin_pairs
|
||||
]
|
||||
result.update({
|
||||
'trade_modes': supported_modes,
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def list_available_exchanges(all_exchanges: bool) -> List[ValidExchangesType]:
|
||||
"""
|
||||
:return: List of tuples with exchangename, valid, reason.
|
||||
"""
|
||||
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
|
||||
exchanges_valid = [
|
||||
(e, *validate_exchange(e)) for e in exchanges
|
||||
from freqtrade.resolvers.exchange_resolver import ExchangeResolver
|
||||
|
||||
subclassed = {e['name'].lower(): e for e in ExchangeResolver.search_all_objects({}, False)}
|
||||
|
||||
exchanges_valid: List[ValidExchangesType] = [
|
||||
_build_exchange_list_entry(e, subclassed) for e in exchanges
|
||||
]
|
||||
|
||||
return exchanges_valid
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
This module loads custom exchanges
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
from inspect import isclass
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import freqtrade.exchange as exchanges
|
||||
from freqtrade.constants import Config, ExchangeConfig
|
||||
@@ -72,3 +73,26 @@ class ExchangeResolver(IResolver):
|
||||
f"Impossible to load Exchange '{exchange_name}'. This class does not exist "
|
||||
"or contains Python code errors."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def search_all_objects(cls, config: Config, enum_failed: bool,
|
||||
recursive: bool = False) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Searches for valid objects
|
||||
:param config: Config object
|
||||
:param enum_failed: If True, will return None for modules which fail.
|
||||
Otherwise, failing modules are skipped.
|
||||
:param recursive: Recursively walk directory tree searching for strategies
|
||||
:return: List of dicts containing 'name', 'class' and 'location' entries
|
||||
"""
|
||||
result = []
|
||||
for exchange_name in dir(exchanges):
|
||||
exchange = getattr(exchanges, exchange_name)
|
||||
if isclass(exchange) and issubclass(exchange, Exchange):
|
||||
result.append({
|
||||
'name': exchange_name,
|
||||
'class': exchange,
|
||||
'location': exchange.__module__,
|
||||
'location_rel: ': exchange.__module__.replace('freqtrade.', ''),
|
||||
})
|
||||
return result
|
||||
|
||||
@@ -41,7 +41,7 @@ class IResolver:
|
||||
object_type: Type[Any]
|
||||
object_type_str: str
|
||||
user_subdir: Optional[str] = None
|
||||
initial_search_path: Optional[Path]
|
||||
initial_search_path: Optional[Path] = None
|
||||
# Optional config setting containing a path (strategy_path, freqaimodel_path)
|
||||
extra_path: Optional[str] = None
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ from pydantic import BaseModel
|
||||
|
||||
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
|
||||
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
|
||||
from freqtrade.types import ValidExchangesType
|
||||
|
||||
|
||||
class ExchangeModePayloadMixin(BaseModel):
|
||||
@@ -422,6 +423,10 @@ class StrategyListResponse(BaseModel):
|
||||
strategies: List[str]
|
||||
|
||||
|
||||
class ExchangeListResponse(BaseModel):
|
||||
exchanges: List[ValidExchangesType]
|
||||
|
||||
|
||||
class PairListResponse(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
|
||||
@@ -12,7 +12,8 @@ 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, ForceEnterPayload,
|
||||
DeleteLockRequest, DeleteTrade,
|
||||
ExchangeListResponse, ForceEnterPayload,
|
||||
ForceEnterResponse, ForceExitPayload,
|
||||
FreqAIModelListResponse, Health, Locks, Logs,
|
||||
OpenTradeSchema, PairHistory, PerformanceEntry,
|
||||
@@ -46,8 +47,9 @@ logger = logging.getLogger(__name__)
|
||||
# 2.26: increase /balance output
|
||||
# 2.27: Add /trades/<id>/reload endpoint
|
||||
# 2.28: Switch reload endpoint to Post
|
||||
# 2.29: new /pairlists endpoint
|
||||
API_VERSION = 2.29
|
||||
# 2.29: Add /exchanges endpoint
|
||||
# 2.30: new /pairlists endpoint
|
||||
API_VERSION = 2.30
|
||||
|
||||
# Public API, requires no auth.
|
||||
router_public = APIRouter()
|
||||
@@ -313,6 +315,15 @@ def get_strategy(strategy: str, config=Depends(get_config)):
|
||||
}
|
||||
|
||||
|
||||
@router.get('/exchanges', response_model=ExchangeListResponse, tags=[])
|
||||
def list_exchanges(config=Depends(get_config)):
|
||||
from freqtrade.exchange import list_available_exchanges
|
||||
exchanges = list_available_exchanges(config)
|
||||
return {
|
||||
'exchanges': exchanges,
|
||||
}
|
||||
|
||||
|
||||
@router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai'])
|
||||
def list_freqaimodels(config=Depends(get_config)):
|
||||
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
|
||||
|
||||
1
freqtrade/types/__init__.py
Normal file
1
freqtrade/types/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from freqtrade.types.valid_exchanges_type import ValidExchangesType # noqa: F401
|
||||
17
freqtrade/types/valid_exchanges_type.py
Normal file
17
freqtrade/types/valid_exchanges_type.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Used for list-exchanges
|
||||
from typing import List
|
||||
|
||||
from typing_extensions import TypedDict
|
||||
|
||||
|
||||
class TradeModeType(TypedDict):
|
||||
trading_mode: str
|
||||
margin_mode: str
|
||||
|
||||
|
||||
class ValidExchangesType(TypedDict):
|
||||
name: str
|
||||
valid: bool
|
||||
supported: bool
|
||||
comment: str
|
||||
trade_modes: List[TradeModeType]
|
||||
@@ -3,7 +3,7 @@ pandas==2.0.1
|
||||
pandas-ta==0.3.14b
|
||||
|
||||
ccxt==3.1.13
|
||||
cryptography==40.0.2; platform_machine != 'armv7l'
|
||||
cryptography==41.0.1; platform_machine != 'armv7l'
|
||||
cryptography==40.0.1; platform_machine == 'armv7l'
|
||||
aiohttp==3.8.4
|
||||
SQLAlchemy==2.0.15
|
||||
|
||||
@@ -1578,6 +1578,47 @@ def test_api_strategy(botclient):
|
||||
assert_response(rc, 500)
|
||||
|
||||
|
||||
def test_api_exchanges(botclient):
|
||||
ftbot, client = botclient
|
||||
|
||||
rc = client_get(client, f"{BASE_URI}/exchanges")
|
||||
assert_response(rc)
|
||||
response = rc.json()
|
||||
assert isinstance(response['exchanges'], list)
|
||||
assert len(response['exchanges']) > 20
|
||||
okx = [x for x in response['exchanges'] if x['name'] == 'okx'][0]
|
||||
assert okx == {
|
||||
"name": "okx",
|
||||
"valid": True,
|
||||
"supported": True,
|
||||
"comment": "",
|
||||
"trade_modes": [
|
||||
{
|
||||
"trading_mode": "spot",
|
||||
"margin_mode": ""
|
||||
},
|
||||
{
|
||||
"trading_mode": "futures",
|
||||
"margin_mode": "isolated"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
mexc = [x for x in response['exchanges'] if x['name'] == 'mexc'][0]
|
||||
assert mexc == {
|
||||
"name": "mexc",
|
||||
"valid": True,
|
||||
"supported": False,
|
||||
"comment": "",
|
||||
"trade_modes": [
|
||||
{
|
||||
"trading_mode": "spot",
|
||||
"margin_mode": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
def test_api_freqaimodels(botclient, tmpdir, mocker):
|
||||
ftbot, client = botclient
|
||||
ftbot.config['user_data_dir'] = Path(tmpdir)
|
||||
@@ -2047,3 +2088,4 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
|
||||
|
||||
finally:
|
||||
ApiServer.shutdown()
|
||||
ApiServer.shutdown()
|
||||
|
||||
@@ -118,7 +118,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
|
||||
def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
|
||||
patch_exchange(mocker)
|
||||
mocker.patch(
|
||||
'freqtrade.commands.list_commands.validate_exchanges',
|
||||
'freqtrade.commands.list_commands.list_available_exchanges',
|
||||
MagicMock(side_effect=ValueError('Oh snap!'))
|
||||
)
|
||||
patched_configuration_load_config_file(mocker, default_conf)
|
||||
@@ -132,7 +132,7 @@ def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
|
||||
assert log_has('Fatal exception!', caplog)
|
||||
assert not log_has_re(r'SIGINT.*', caplog)
|
||||
mocker.patch(
|
||||
'freqtrade.commands.list_commands.validate_exchanges',
|
||||
'freqtrade.commands.list_commands.list_available_exchanges',
|
||||
MagicMock(side_effect=KeyboardInterrupt)
|
||||
)
|
||||
with pytest.raises(SystemExit):
|
||||
|
||||
Reference in New Issue
Block a user