Merge branch 'develop' into feat/pairlistconfig

This commit is contained in:
Matthias
2023-06-04 08:33:45 +02:00
14 changed files with 179 additions and 25 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ repos:
# stages: [push] # stages: [push]
- repo: https://github.com/pre-commit/mirrors-mypy - repo: https://github.com/pre-commit/mirrors-mypy
rev: "v1.0.1" rev: "v1.3.0"
hooks: hooks:
- id: mypy - id: mypy
exclude: build_helpers exclude: build_helpers
+1 -1
View File
@@ -76,7 +76,7 @@ pip install -r requirements-freqai.txt
### Usage with docker ### 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" !!! 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. 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.
+31 -6
View File
@@ -1,7 +1,7 @@
import csv import csv
import logging import logging
import sys import sys
from typing import Any, Dict, List from typing import Any, Dict, List, Union
import rapidjson import rapidjson
from colorama import Fore, Style from colorama import Fore, Style
@@ -11,9 +11,10 @@ from tabulate import tabulate
from freqtrade.configuration import setup_utils_configuration from freqtrade.configuration import setup_utils_configuration
from freqtrade.enums import RunMode from freqtrade.enums import RunMode
from freqtrade.exceptions import OperationalException 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.misc import parse_db_uri_for_logging, plural
from freqtrade.resolvers import ExchangeResolver, StrategyResolver from freqtrade.resolvers import ExchangeResolver, StrategyResolver
from freqtrade.types import ValidExchangesType
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -25,18 +26,42 @@ def start_list_exchanges(args: Dict[str, Any]) -> None:
:param args: Cli args from Arguments() :param args: Cli args from Arguments()
:return: None :return: None
""" """
exchanges = validate_exchanges(args['list_exchanges_all']) exchanges = list_available_exchanges(args['list_exchanges_all'])
if args['print_one_column']: if args['print_one_column']:
print('\n'.join([e[0] for e in exchanges])) print('\n'.join([e['name'] for e in exchanges]))
else: 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']: if args['list_exchanges_all']:
print("All exchanges supported by the ccxt library:") print("All exchanges supported by the ccxt library:")
exchanges = [build_entry(e, True) for e in exchanges]
else: else:
print("Exchanges available for Freqtrade:") 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: def _print_objs_tabular(objs: List, print_colorized: bool) -> None:
+5 -5
View File
@@ -13,11 +13,11 @@ from freqtrade.exchange.exchange_utils import (ROUND_DOWN, ROUND_UP, amount_to_c
amount_to_contracts, amount_to_precision, amount_to_contracts, amount_to_precision,
available_exchanges, ccxt_exchanges, available_exchanges, ccxt_exchanges,
contracts_to_amount, date_minus_candles, contracts_to_amount, date_minus_candles,
is_exchange_known_ccxt, market_is_active, is_exchange_known_ccxt, list_available_exchanges,
price_to_precision, timeframe_to_minutes, market_is_active, price_to_precision,
timeframe_to_msecs, timeframe_to_next_date, timeframe_to_minutes, timeframe_to_msecs,
timeframe_to_prev_date, timeframe_to_seconds, timeframe_to_next_date, timeframe_to_prev_date,
validate_exchange, validate_exchanges) timeframe_to_seconds, validate_exchange)
from freqtrade.exchange.gate import Gate from freqtrade.exchange.gate import Gate
from freqtrade.exchange.hitbtc import Hitbtc from freqtrade.exchange.hitbtc import Hitbtc
from freqtrade.exchange.huobi import Huobi from freqtrade.exchange.huobi import Huobi
+33 -4
View File
@@ -9,7 +9,9 @@ import ccxt
from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE, from ccxt import (DECIMAL_PLACES, ROUND, ROUND_DOWN, ROUND_UP, SIGNIFICANT_DIGITS, TICK_SIZE,
TRUNCATE, decimal_to_precision) 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 import FtPrecise
from freqtrade.util.datetime_helpers import dt_from_ts, dt_ts 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, '' 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. :return: List of tuples with exchangename, valid, reason.
""" """
exchanges = ccxt_exchanges() if all_exchanges else available_exchanges() exchanges = ccxt_exchanges() if all_exchanges else available_exchanges()
exchanges_valid = [ from freqtrade.resolvers.exchange_resolver import ExchangeResolver
(e, *validate_exchange(e)) for e in exchanges
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 return exchanges_valid
+25 -1
View File
@@ -2,7 +2,8 @@
This module loads custom exchanges This module loads custom exchanges
""" """
import logging import logging
from typing import Optional from inspect import isclass
from typing import Any, Dict, List, Optional
import freqtrade.exchange as exchanges import freqtrade.exchange as exchanges
from freqtrade.constants import Config, ExchangeConfig 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 " f"Impossible to load Exchange '{exchange_name}'. This class does not exist "
"or contains Python code errors." "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
+1 -1
View File
@@ -41,7 +41,7 @@ class IResolver:
object_type: Type[Any] object_type: Type[Any]
object_type_str: str object_type_str: str
user_subdir: Optional[str] = None 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) # Optional config setting containing a path (strategy_path, freqaimodel_path)
extra_path: Optional[str] = None extra_path: Optional[str] = None
+5
View File
@@ -5,6 +5,7 @@ from pydantic import BaseModel
from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf from freqtrade.constants import DATETIME_PRINT_FORMAT, IntOrInf
from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode from freqtrade.enums import OrderTypeValues, SignalDirection, TradingMode
from freqtrade.types import ValidExchangesType
class ExchangeModePayloadMixin(BaseModel): class ExchangeModePayloadMixin(BaseModel):
@@ -422,6 +423,10 @@ class StrategyListResponse(BaseModel):
strategies: List[str] strategies: List[str]
class ExchangeListResponse(BaseModel):
exchanges: List[ValidExchangesType]
class PairListResponse(BaseModel): class PairListResponse(BaseModel):
name: str name: str
description: str description: str
+14 -3
View File
@@ -12,7 +12,8 @@ from freqtrade.exceptions import OperationalException
from freqtrade.rpc import RPC from freqtrade.rpc import RPC
from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload, from freqtrade.rpc.api_server.api_schemas import (AvailablePairs, Balances, BlacklistPayload,
BlacklistResponse, Count, Daily, BlacklistResponse, Count, Daily,
DeleteLockRequest, DeleteTrade, ForceEnterPayload, DeleteLockRequest, DeleteTrade,
ExchangeListResponse, ForceEnterPayload,
ForceEnterResponse, ForceExitPayload, ForceEnterResponse, ForceExitPayload,
FreqAIModelListResponse, Health, Locks, Logs, FreqAIModelListResponse, Health, Locks, Logs,
OpenTradeSchema, PairHistory, PerformanceEntry, OpenTradeSchema, PairHistory, PerformanceEntry,
@@ -46,8 +47,9 @@ logger = logging.getLogger(__name__)
# 2.26: increase /balance output # 2.26: increase /balance output
# 2.27: Add /trades/<id>/reload endpoint # 2.27: Add /trades/<id>/reload endpoint
# 2.28: Switch reload endpoint to Post # 2.28: Switch reload endpoint to Post
# 2.29: new /pairlists endpoint # 2.29: Add /exchanges endpoint
API_VERSION = 2.29 # 2.30: new /pairlists endpoint
API_VERSION = 2.30
# Public API, requires no auth. # Public API, requires no auth.
router_public = APIRouter() 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']) @router.get('/freqaimodels', response_model=FreqAIModelListResponse, tags=['freqai'])
def list_freqaimodels(config=Depends(get_config)): def list_freqaimodels(config=Depends(get_config)):
from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver from freqtrade.resolvers.freqaimodel_resolver import FreqaiModelResolver
+1
View File
@@ -0,0 +1 @@
from freqtrade.types.valid_exchanges_type import ValidExchangesType # noqa: F401
+17
View 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]
+1 -1
View File
@@ -3,7 +3,7 @@ pandas==2.0.1
pandas-ta==0.3.14b pandas-ta==0.3.14b
ccxt==3.1.13 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' cryptography==40.0.1; platform_machine == 'armv7l'
aiohttp==3.8.4 aiohttp==3.8.4
SQLAlchemy==2.0.15 SQLAlchemy==2.0.15
+42
View File
@@ -1578,6 +1578,47 @@ def test_api_strategy(botclient):
assert_response(rc, 500) 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): def test_api_freqaimodels(botclient, tmpdir, mocker):
ftbot, client = botclient ftbot, client = botclient
ftbot.config['user_data_dir'] = Path(tmpdir) ftbot.config['user_data_dir'] = Path(tmpdir)
@@ -2047,3 +2088,4 @@ def test_api_ws_send_msg(default_conf, mocker, caplog):
finally: finally:
ApiServer.shutdown() ApiServer.shutdown()
ApiServer.shutdown()
+2 -2
View File
@@ -118,7 +118,7 @@ def test_main_operational_exception(mocker, default_conf, caplog) -> None:
def test_main_operational_exception1(mocker, default_conf, caplog) -> None: def test_main_operational_exception1(mocker, default_conf, caplog) -> None:
patch_exchange(mocker) patch_exchange(mocker)
mocker.patch( mocker.patch(
'freqtrade.commands.list_commands.validate_exchanges', 'freqtrade.commands.list_commands.list_available_exchanges',
MagicMock(side_effect=ValueError('Oh snap!')) MagicMock(side_effect=ValueError('Oh snap!'))
) )
patched_configuration_load_config_file(mocker, default_conf) 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 log_has('Fatal exception!', caplog)
assert not log_has_re(r'SIGINT.*', caplog) assert not log_has_re(r'SIGINT.*', caplog)
mocker.patch( mocker.patch(
'freqtrade.commands.list_commands.validate_exchanges', 'freqtrade.commands.list_commands.list_available_exchanges',
MagicMock(side_effect=KeyboardInterrupt) MagicMock(side_effect=KeyboardInterrupt)
) )
with pytest.raises(SystemExit): with pytest.raises(SystemExit):