commit
66d8ed6c0b
Binary file not shown.
@ -1,6 +1,6 @@
|
||||
markdown==3.3.7
|
||||
mkdocs==1.4.1
|
||||
mkdocs-material==8.5.6
|
||||
mkdocs-material==8.5.7
|
||||
mdx_truly_sane_lists==1.3
|
||||
pymdown-extensions==9.6
|
||||
pymdown-extensions==9.7
|
||||
jinja2==3.1.2
|
||||
|
||||
@ -0,0 +1,252 @@
|
||||
"""
|
||||
Exchange support utils
|
||||
"""
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from math import ceil
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
import ccxt
|
||||
from ccxt import ROUND_DOWN, ROUND_UP, TICK_SIZE, TRUNCATE, decimal_to_precision
|
||||
|
||||
from freqtrade.exchange.common import BAD_EXCHANGES, EXCHANGE_HAS_OPTIONAL, EXCHANGE_HAS_REQUIRED
|
||||
from freqtrade.util import FtPrecise
|
||||
|
||||
|
||||
CcxtModuleType = Any
|
||||
|
||||
|
||||
def is_exchange_known_ccxt(exchange_name: str, ccxt_module: CcxtModuleType = None) -> bool:
|
||||
return exchange_name in ccxt_exchanges(ccxt_module)
|
||||
|
||||
|
||||
def ccxt_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return the list of all exchanges known to ccxt
|
||||
"""
|
||||
return ccxt_module.exchanges if ccxt_module is not None else ccxt.exchanges
|
||||
|
||||
|
||||
def available_exchanges(ccxt_module: CcxtModuleType = None) -> List[str]:
|
||||
"""
|
||||
Return exchanges available to the bot, i.e. non-bad exchanges in the ccxt list
|
||||
"""
|
||||
exchanges = ccxt_exchanges(ccxt_module)
|
||||
return [x for x in exchanges if validate_exchange(x)[0]]
|
||||
|
||||
|
||||
def validate_exchange(exchange: str) -> Tuple[bool, str]:
|
||||
ex_mod = getattr(ccxt, exchange.lower())()
|
||||
if not ex_mod or not ex_mod.has:
|
||||
return False, ''
|
||||
missing = [k for k in EXCHANGE_HAS_REQUIRED if ex_mod.has.get(k) is not True]
|
||||
if missing:
|
||||
return False, f"missing: {', '.join(missing)}"
|
||||
|
||||
missing_opt = [k for k in EXCHANGE_HAS_OPTIONAL if not ex_mod.has.get(k)]
|
||||
|
||||
if exchange.lower() in BAD_EXCHANGES:
|
||||
return False, BAD_EXCHANGES.get(exchange.lower(), '')
|
||||
if missing_opt:
|
||||
return True, f"missing opt: {', '.join(missing_opt)}"
|
||||
|
||||
return True, ''
|
||||
|
||||
|
||||
def validate_exchanges(all_exchanges: bool) -> List[Tuple[str, bool, str]]:
|
||||
"""
|
||||
: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
|
||||
]
|
||||
return exchanges_valid
|
||||
|
||||
|
||||
def timeframe_to_seconds(timeframe: str) -> int:
|
||||
"""
|
||||
Translates the timeframe interval value written in the human readable
|
||||
form ('1m', '5m', '1h', '1d', '1w', etc.) to the number
|
||||
of seconds for one timeframe interval.
|
||||
"""
|
||||
return ccxt.Exchange.parse_timeframe(timeframe)
|
||||
|
||||
|
||||
def timeframe_to_minutes(timeframe: str) -> int:
|
||||
"""
|
||||
Same as timeframe_to_seconds, but returns minutes.
|
||||
"""
|
||||
return ccxt.Exchange.parse_timeframe(timeframe) // 60
|
||||
|
||||
|
||||
def timeframe_to_msecs(timeframe: str) -> int:
|
||||
"""
|
||||
Same as timeframe_to_seconds, but returns milliseconds.
|
||||
"""
|
||||
return ccxt.Exchange.parse_timeframe(timeframe) * 1000
|
||||
|
||||
|
||||
def timeframe_to_prev_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
"""
|
||||
Use Timeframe and determine the candle start date for this date.
|
||||
Does not round when given a candle start date.
|
||||
:param timeframe: timeframe in string format (e.g. "5m")
|
||||
:param date: date to use. Defaults to now(utc)
|
||||
:returns: date of previous candle (with utc timezone)
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_DOWN) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def timeframe_to_next_date(timeframe: str, date: datetime = None) -> datetime:
|
||||
"""
|
||||
Use Timeframe and determine next candle.
|
||||
:param timeframe: timeframe in string format (e.g. "5m")
|
||||
:param date: date to use. Defaults to now(utc)
|
||||
:returns: date of next candle (with utc timezone)
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
new_timestamp = ccxt.Exchange.round_timeframe(timeframe, date.timestamp() * 1000,
|
||||
ROUND_UP) // 1000
|
||||
return datetime.fromtimestamp(new_timestamp, tz=timezone.utc)
|
||||
|
||||
|
||||
def date_minus_candles(
|
||||
timeframe: str, candle_count: int, date: Optional[datetime] = None) -> datetime:
|
||||
"""
|
||||
subtract X candles from a date.
|
||||
:param timeframe: timeframe in string format (e.g. "5m")
|
||||
:param candle_count: Amount of candles to subtract.
|
||||
:param date: date to use. Defaults to now(utc)
|
||||
|
||||
"""
|
||||
if not date:
|
||||
date = datetime.now(timezone.utc)
|
||||
|
||||
tf_min = timeframe_to_minutes(timeframe)
|
||||
new_date = timeframe_to_prev_date(timeframe, date) - timedelta(minutes=tf_min * candle_count)
|
||||
return new_date
|
||||
|
||||
|
||||
def market_is_active(market: Dict) -> bool:
|
||||
"""
|
||||
Return True if the market is active.
|
||||
"""
|
||||
# "It's active, if the active flag isn't explicitly set to false. If it's missing or
|
||||
# true then it's true. If it's undefined, then it's most likely true, but not 100% )"
|
||||
# See https://github.com/ccxt/ccxt/issues/4874,
|
||||
# https://github.com/ccxt/ccxt/issues/4075#issuecomment-434760520
|
||||
return market.get('active', True) is not False
|
||||
|
||||
|
||||
def amount_to_contracts(amount: float, contract_size: Optional[float]) -> float:
|
||||
"""
|
||||
Convert amount to contracts.
|
||||
:param amount: amount to convert
|
||||
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
|
||||
:return: num-contracts
|
||||
"""
|
||||
if contract_size and contract_size != 1:
|
||||
return float(FtPrecise(amount) / FtPrecise(contract_size))
|
||||
else:
|
||||
return amount
|
||||
|
||||
|
||||
def contracts_to_amount(num_contracts: float, contract_size: Optional[float]) -> float:
|
||||
"""
|
||||
Takes num-contracts and converts it to contract size
|
||||
:param num_contracts: number of contracts
|
||||
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
|
||||
:return: Amount
|
||||
"""
|
||||
|
||||
if contract_size and contract_size != 1:
|
||||
return float(FtPrecise(num_contracts) * FtPrecise(contract_size))
|
||||
else:
|
||||
return num_contracts
|
||||
|
||||
|
||||
def amount_to_precision(amount: float, amount_precision: Optional[float],
|
||||
precisionMode: Optional[int]) -> float:
|
||||
"""
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
based on our definitions.
|
||||
:param amount: amount to truncate
|
||||
:param amount_precision: amount precision to use.
|
||||
should be retrieved from markets[pair]['precision']['amount']
|
||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||
:return: truncated amount
|
||||
"""
|
||||
if amount_precision is not None and precisionMode is not None:
|
||||
precision = int(amount_precision) if precisionMode != TICK_SIZE else amount_precision
|
||||
# precision must be an int for non-ticksize inputs.
|
||||
amount = float(decimal_to_precision(amount, rounding_mode=TRUNCATE,
|
||||
precision=precision,
|
||||
counting_mode=precisionMode,
|
||||
))
|
||||
|
||||
return amount
|
||||
|
||||
|
||||
def amount_to_contract_precision(
|
||||
amount, amount_precision: Optional[float], precisionMode: Optional[int],
|
||||
contract_size: Optional[float]) -> float:
|
||||
"""
|
||||
Returns the amount to buy or sell to a precision the Exchange accepts
|
||||
including calculation to and from contracts.
|
||||
Re-implementation of ccxt internal methods - ensuring we can test the result is correct
|
||||
based on our definitions.
|
||||
:param amount: amount to truncate
|
||||
:param amount_precision: amount precision to use.
|
||||
should be retrieved from markets[pair]['precision']['amount']
|
||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||
:param contract_size: contract size - taken from exchange.get_contract_size(pair)
|
||||
:return: truncated amount
|
||||
"""
|
||||
if amount_precision is not None and precisionMode is not None:
|
||||
contracts = amount_to_contracts(amount, contract_size)
|
||||
amount_p = amount_to_precision(contracts, amount_precision, precisionMode)
|
||||
return contracts_to_amount(amount_p, contract_size)
|
||||
return amount
|
||||
|
||||
|
||||
def price_to_precision(price: float, price_precision: Optional[float],
|
||||
precisionMode: Optional[int]) -> float:
|
||||
"""
|
||||
Returns the price rounded up to the precision the Exchange accepts.
|
||||
Partial Re-implementation of ccxt internal method decimal_to_precision(),
|
||||
which does not support rounding up
|
||||
TODO: If ccxt supports ROUND_UP for decimal_to_precision(), we could remove this and
|
||||
align with amount_to_precision().
|
||||
!!! Rounds up
|
||||
:param price: price to convert
|
||||
:param price_precision: price precision to use. Used from markets[pair]['precision']['price']
|
||||
:param precisionMode: precision mode to use. Should be used from precisionMode
|
||||
one of ccxt's DECIMAL_PLACES, SIGNIFICANT_DIGITS, or TICK_SIZE
|
||||
:return: price rounded up to the precision the Exchange accepts
|
||||
|
||||
"""
|
||||
if price_precision is not None and precisionMode is not None:
|
||||
# price = float(decimal_to_precision(price, rounding_mode=ROUND,
|
||||
# precision=price_precision,
|
||||
# counting_mode=self.precisionMode,
|
||||
# ))
|
||||
if precisionMode == TICK_SIZE:
|
||||
precision = FtPrecise(price_precision)
|
||||
price_str = FtPrecise(price)
|
||||
missing = price_str % precision
|
||||
if not missing == FtPrecise("0"):
|
||||
price = round(float(str(price_str - missing + precision)), 14)
|
||||
else:
|
||||
symbol_prec = price_precision
|
||||
big_price = price * pow(10, symbol_prec)
|
||||
price = ceil(big_price) / pow(10, symbol_prec)
|
||||
return price
|
||||
@ -1,4 +1,4 @@
|
||||
# Include all requirements to run the bot.
|
||||
-r requirements.txt
|
||||
|
||||
plotly==5.10.0
|
||||
plotly==5.11.0
|
||||
|
||||
@ -0,0 +1,181 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from freqtrade.persistence.trade_model import Trade
|
||||
|
||||
|
||||
def test_trade_fromjson():
|
||||
"""Test the Trade.from_json() method."""
|
||||
trade_string = """{
|
||||
"trade_id": 25,
|
||||
"pair": "ETH/USDT",
|
||||
"base_currency": "ETH",
|
||||
"quote_currency": "USDT",
|
||||
"is_open": false,
|
||||
"exchange": "binance",
|
||||
"amount": 407.0,
|
||||
"amount_requested": 102.92547026,
|
||||
"stake_amount": 102.7494348,
|
||||
"strategy": "SampleStrategy55",
|
||||
"buy_tag": "Strategy2",
|
||||
"enter_tag": "Strategy2",
|
||||
"timeframe": 5,
|
||||
"fee_open": 0.001,
|
||||
"fee_open_cost": 0.1027494,
|
||||
"fee_open_currency": "ETH",
|
||||
"fee_close": 0.001,
|
||||
"fee_close_cost": 0.1054944,
|
||||
"fee_close_currency": "USDT",
|
||||
"open_date": "2022-10-18 09:12:42",
|
||||
"open_timestamp": 1666084362912,
|
||||
"open_rate": 0.2518998249562391,
|
||||
"open_rate_requested": 0.2516,
|
||||
"open_trade_value": 102.62575199,
|
||||
"close_date": "2022-10-18 09:45:22",
|
||||
"close_timestamp": 1666086322208,
|
||||
"realized_profit": 2.76315361,
|
||||
"close_rate": 0.2592,
|
||||
"close_rate_requested": 0.2592,
|
||||
"close_profit": 0.026865,
|
||||
"close_profit_pct": 2.69,
|
||||
"close_profit_abs": 2.76315361,
|
||||
"trade_duration_s": 1959,
|
||||
"trade_duration": 32,
|
||||
"profit_ratio": 0.02686,
|
||||
"profit_pct": 2.69,
|
||||
"profit_abs": 2.76315361,
|
||||
"sell_reason": "no longer good",
|
||||
"exit_reason": "no longer good",
|
||||
"exit_order_status": "closed",
|
||||
"stop_loss_abs": 0.1981,
|
||||
"stop_loss_ratio": -0.216,
|
||||
"stop_loss_pct": -21.6,
|
||||
"stoploss_order_id": null,
|
||||
"stoploss_last_update": null,
|
||||
"stoploss_last_update_timestamp": null,
|
||||
"initial_stop_loss_abs": 0.1981,
|
||||
"initial_stop_loss_ratio": -0.216,
|
||||
"initial_stop_loss_pct": -21.6,
|
||||
"min_rate": 0.2495,
|
||||
"max_rate": 0.2592,
|
||||
"leverage": 1.0,
|
||||
"interest_rate": 0.0,
|
||||
"liquidation_price": null,
|
||||
"is_short": false,
|
||||
"trading_mode": "spot",
|
||||
"funding_fees": 0.0,
|
||||
"open_order_id": null,
|
||||
"orders": [
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2526,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084370887,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78404228",
|
||||
"status": "closed",
|
||||
"average": 0.2526,
|
||||
"cost": 25.7652,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:12:42",
|
||||
"order_timestamp": 1666084362684,
|
||||
"order_filled_date": "2022-10-18 09:12:50",
|
||||
"order_type": "limit",
|
||||
"price": 0.2526,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2517,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084379056,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405139",
|
||||
"status": "closed",
|
||||
"average": 0.2517,
|
||||
"cost": 25.6734,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:12:57",
|
||||
"order_timestamp": 1666084377681,
|
||||
"order_filled_date": "2022-10-18 09:12:59",
|
||||
"order_type": "limit",
|
||||
"price": 0.2517,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2517,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084389644,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405265",
|
||||
"status": "closed",
|
||||
"average": 0.2517,
|
||||
"cost": 25.6734,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:13:03",
|
||||
"order_timestamp": 1666084383295,
|
||||
"order_filled_date": "2022-10-18 09:13:09",
|
||||
"order_type": "limit",
|
||||
"price": 0.2517,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 102.0,
|
||||
"safe_price": 0.2516,
|
||||
"ft_order_side": "buy",
|
||||
"order_filled_timestamp": 1666084723521,
|
||||
"ft_is_entry": true,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78405395",
|
||||
"status": "closed",
|
||||
"average": 0.2516,
|
||||
"cost": 25.6632,
|
||||
"filled": 102.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:13:13",
|
||||
"order_timestamp": 1666084393920,
|
||||
"order_filled_date": "2022-10-18 09:18:43",
|
||||
"order_type": "limit",
|
||||
"price": 0.2516,
|
||||
"remaining": 0.0
|
||||
},
|
||||
{
|
||||
"amount": 407.0,
|
||||
"safe_price": 0.2592,
|
||||
"ft_order_side": "sell",
|
||||
"order_filled_timestamp": 1666086322198,
|
||||
"ft_is_entry": false,
|
||||
"pair": "ETH/USDT",
|
||||
"order_id": "78432649",
|
||||
"status": "closed",
|
||||
"average": 0.2592,
|
||||
"cost": 105.4944,
|
||||
"filled": 407.0,
|
||||
"is_open": false,
|
||||
"order_date": "2022-10-18 09:45:21",
|
||||
"order_timestamp": 1666086321435,
|
||||
"order_filled_date": "2022-10-18 09:45:22",
|
||||
"order_type": "market",
|
||||
"price": 0.2592,
|
||||
"remaining": 0.0
|
||||
}
|
||||
]
|
||||
}"""
|
||||
trade = Trade.from_json(trade_string)
|
||||
|
||||
assert trade.id == 25
|
||||
assert trade.pair == 'ETH/USDT'
|
||||
assert trade.open_date == datetime(2022, 10, 18, 9, 12, 42, tzinfo=timezone.utc)
|
||||
assert isinstance(trade.open_date, datetime)
|
||||
assert trade.exit_reason == 'no longer good'
|
||||
|
||||
assert len(trade.orders) == 5
|
||||
last_o = trade.orders[-1]
|
||||
assert last_o.order_filled_date == datetime(2022, 10, 18, 9, 45, 22, tzinfo=timezone.utc)
|
||||
assert isinstance(last_o.order_date, datetime)
|
||||
Loading…
Reference in new issue