Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TMP Positions #1394

Closed
wants to merge 9 commits into from
Closed
31 changes: 9 additions & 22 deletions Meta/Keywords/scripting_library/orders/position_size/amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,28 +31,15 @@ async def get_amount(
unknown_portfolio_on_creation=False,
target_price=None
):
try:
amount_value = await script_keywords.get_amount_from_input_amount(
context=context,
input_amount=input_amount,
side=side,
reduce_only=reduce_only,
is_stop_order=is_stop_order,
use_total_holding=use_total_holding,
target_price=target_price
)
except NotImplementedError:
amount_type, amount_value = script_keywords.parse_quantity(input_amount)
if amount_type is script_keywords.QuantityType.POSITION_PERCENT: # todo handle existing open short position
amount_value = \
exchange_private_data.open_position_size(context, side,
amount_type=commons_constants.PORTFOLIO_AVAILABLE) \
* amount_value / 100
else:
raise trading_errors.InvalidArgumentError("make sure to use a supported syntax for amount")
return await script_keywords.adapt_amount_to_holdings(context, amount_value, side,
use_total_holding, reduce_only, is_stop_order,
target_price=target_price)
amount_value = await script_keywords.get_amount_from_input_amount(
context=context,
input_amount=input_amount,
side=side,
reduce_only=reduce_only,
is_stop_order=is_stop_order,
use_total_holding=use_total_holding,
target_price=target_price
)
if unknown_portfolio_on_creation:
# no way to check if the amount is valid when creating order
_, amount_value = script_keywords.parse_quantity(input_amount)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import octobot_trading.personal_data.orders.order_util as order_util
import octobot_trading.api as api
import octobot_trading.errors as errors
import octobot_trading.enums as trading_enums
import octobot_trading.constants as trading_constants
import tentacles.Meta.Keywords.scripting_library as scripting_library

Expand Down Expand Up @@ -102,6 +103,13 @@ async def test_orders_with_invalid_values(mock_context, skip_if_octobot_trading_
@pytest.mark.parametrize("backtesting_config", ["USDT"], indirect=["backtesting_config"])
async def test_orders_amount_then_position_sequence(mock_context):
initial_usdt_holdings, btc_price = await _usdt_trading_context(mock_context)
mock_context.exchange_manager.is_future = True
api.load_pair_contract(
mock_context.exchange_manager,
api.create_default_future_contract(
mock_context.symbol, decimal.Decimal(1), trading_enums.FutureContractType.LINEAR_PERPETUAL
).to_dict()
)

if os.getenv('CYTHON_IGNORE'):
return
Expand Down Expand Up @@ -193,7 +201,7 @@ async def test_concurrent_orders(mock_context):

# create 3 sell orders (at price = 500 + 10 = 510)
# that would end up selling more than what we have if not executed sequentially
# 1st order is 80% of position, second is 80% of the remaining 20% and so on
# 1st order is 80% of available btc, second is 80% of the remaining 20% and so on

orders = []
async def create_order(amount):
Expand All @@ -207,13 +215,13 @@ async def create_order(amount):
)
await asyncio.gather(
*(
create_order("80%p")
create_order("80%a")
for _ in range(3)
)
)

initial_btc_holdings = btc_val
btc_val = initial_btc_holdings * decimal.Decimal("0.2") ** 3 # 0.16
btc_val = initial_btc_holdings * (decimal.Decimal("0.2") ** 3)
usdt_val = usdt_val + (initial_btc_holdings - btc_val) * (btc_price + 10) # 50118.40
await _fill_and_check(mock_context, btc_val, usdt_val, orders, orders_count=3)

Expand Down

This file was deleted.

30 changes: 11 additions & 19 deletions Trading/Exchange/binance/binance_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,17 @@ def get_adapter_class(self):
return BinanceCCXTAdapter

async def get_account_id(self, **kwargs: dict) -> str:
raw_balance = await self.connector.client.fetch_balance()
try:
return raw_balance[ccxt_constants.CCXT_INFO]["uid"]
except KeyError:
if self.exchange_manager.is_future:
raise NotImplementedError("get_account_id is not implemented on binance futures account")
# should not happen in spot
raw_binance_balance = await self.connector.client.fapiPrivateV3GetBalance()
# accountAlias = unique account code
# from https://binance-docs.github.io/apidocs/futures/en/#futures-account-balance-v3-user_data
return raw_binance_balance[0]["accountAlias"]
else:
raw_balance = await self.connector.client.fetch_balance()
return raw_balance[ccxt_constants.CCXT_INFO]["uid"]
except (KeyError, IndexError):
# should not happen
raise

def _infer_account_types(self, exchange_manager):
Expand Down Expand Up @@ -237,7 +241,7 @@ async def set_symbol_margin_type(self, symbol: str, isolated: bool, **kwargs: di
:return: the update result
"""
try:
return await super(). set_symbol_margin_type(symbol, isolated, **kwargs)
return await super().set_symbol_margin_type(symbol, isolated, **kwargs)
except ccxt.ExchangeError as err:
raise errors.NotSupported() from err

Expand Down Expand Up @@ -275,19 +279,7 @@ def fix_trades(self, raw, **kwargs):

def parse_position(self, fixed, force_empty=False, **kwargs):
try:
parsed = super().parse_position(fixed, force_empty=force_empty, **kwargs)
parsed[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] = \
trading_enums.MarginType(
fixed.get(ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value)
)
# use one way by default.
if parsed[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] is None:
parsed[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] = (
trading_enums.PositionMode.HEDGE if fixed.get(ccxt_enums.ExchangePositionCCXTColumns.HEDGED.value,
True)
else trading_enums.PositionMode.ONE_WAY
)
return parsed
return super().parse_position(fixed, force_empty=force_empty, **kwargs)
except decimal.InvalidOperation:
# on binance, positions might be invalid (ex: LUNAUSD_PERP as None contact size)
return None
Expand Down
35 changes: 27 additions & 8 deletions Trading/Exchange/kucoin/kucoin_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ class Kucoin(exchanges.RestExchange):
("order does not exist",),
]

DEFAULT_BALANCE_CURRENCIES_TO_FETCH = ["USDT"]

@classmethod
def get_name(cls):
return 'kucoin'
Expand Down Expand Up @@ -288,9 +290,16 @@ async def get_balance(self, **kwargs: dict):
if self.exchange_manager.is_future:
# on futures, balance has to be fetched per currency
# use gather to fetch everything at once (and not allow other requests to get in between)
currencies = self.exchange_manager.exchange_config.get_all_traded_currencies()
if not currencies:
currencies = self.DEFAULT_BALANCE_CURRENCIES_TO_FETCH
self.logger.warning(
f"Can't fetch balance on {self.exchange_manager.exchange_name} futures when no traded currencies "
f"are set, fetching {currencies[0]} balance instead"
)
await asyncio.gather(*(
self._update_balance(balance, currency, **kwargs)
for currency in self.exchange_manager.exchange_config.get_all_traded_currencies()
for currency in currencies
))
return balance
return await super().get_balance(**kwargs)
Expand Down Expand Up @@ -333,8 +342,21 @@ async def create_order(self, order_type: trading_enums.TraderOrderType, symbol:
side: trading_enums.TradeOrderSide = None, current_price: decimal.Decimal = None,
reduce_only: bool = False, params: dict = None) -> typing.Optional[dict]:
if self.exchange_manager.is_future:
params = params or {}
# on futures exchange expects, quantity in contracts: convert quantity into contracts
quantity = quantity / self.get_contract_size(symbol)
try:
# "marginMode": "ISOLATED" // Added field for margin mode: ISOLATED, CROSS, default: ISOLATED
# from https://www.kucoin.com/docs/rest/futures-trading/orders/place-order
if (
KucoinCCXTAdapter.KUCOIN_MARGIN_MODE not in params and
self.exchange_manager.exchange_personal_data.positions_manager.get_symbol_position_margin_type(
symbol
) is trading_enums.MarginType.CROSS
):
params[KucoinCCXTAdapter.KUCOIN_MARGIN_MODE] = "CROSS"
except ValueError as err:
self.logger.error(f"Impossible to add {KucoinCCXTAdapter.KUCOIN_MARGIN_MODE} to order: {err}")
return await super().create_order(order_type, symbol, quantity,
price=price, stop_price=stop_price,
side=side, current_price=current_price,
Expand Down Expand Up @@ -448,6 +470,7 @@ class KucoinCCXTAdapter(exchanges.CCXTAdapter):

# ORDER
KUCOIN_LEVERAGE = "leverage"
KUCOIN_MARGIN_MODE = "marginMode"

def fix_order(self, raw, symbol=None, **kwargs):
raw_order_info = raw[ccxt_enums.ExchangePositionCCXTColumns.INFO.value]
Expand Down Expand Up @@ -515,13 +538,9 @@ def parse_funding_rate(self, fixed, from_ticker=False, **kwargs):
def parse_position(self, fixed, **kwargs):
raw_position_info = fixed[ccxt_enums.ExchangePositionCCXTColumns.INFO.value]
parsed = super().parse_position(fixed, **kwargs)
parsed[trading_enums.ExchangeConstantsPositionColumns.MARGIN_TYPE.value] = \
trading_enums.MarginType(
fixed.get(ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value)
)
parsed[trading_enums.ExchangeConstantsPositionColumns.POSITION_MODE.value] = \
trading_enums.PositionMode.HEDGE if raw_position_info[self.KUCOIN_AUTO_DEPOSIT] \
else trading_enums.PositionMode.ONE_WAY
parsed[trading_enums.ExchangeConstantsPositionColumns.AUTO_DEPOSIT_MARGIN.value] = (
raw_position_info.get(self.KUCOIN_AUTO_DEPOSIT, False) # unset for cross positions
)
parsed_leverage = self.safe_decimal(
parsed, trading_enums.ExchangeConstantsPositionColumns.LEVERAGE.value, constants.ZERO
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,19 +195,20 @@ async def test_trading_view_signal_callback(tools):
context = script_keywords.get_base_context(producer.trading_mode)
with mock.patch.object(script_keywords, "get_base_context", mock.Mock(return_value=context)) \
as get_base_context_mock:
# ensure exception is caught
with mock.patch.object(
producer, "signal_callback", mock.AsyncMock(side_effect=errors.MissingFunds)
) as signal_callback_mock:
signal = f"""
EXCHANGE={exchange_manager.exchange_name}
SYMBOL={symbol}
SIGNAL=BUY
"""
await mode._trading_view_signal_callback({"metadata": signal})
signal_callback_mock.assert_awaited_once()
get_base_context_mock.assert_called_once()
get_base_context_mock.reset_mock()
for exception in (errors.MissingFunds, errors.InvalidArgumentError):
# ensure exception is caught
with mock.patch.object(
producer, "signal_callback", mock.AsyncMock(side_effect=exception)
) as signal_callback_mock:
signal = f"""
EXCHANGE={exchange_manager.exchange_name}
SYMBOL={symbol}
SIGNAL=BUY
"""
await mode._trading_view_signal_callback({"metadata": signal})
signal_callback_mock.assert_awaited_once()
get_base_context_mock.assert_called_once()
get_base_context_mock.reset_mock()

with mock.patch.object(producer, "signal_callback", mock.AsyncMock()) as signal_callback_mock:
# invalid data
Expand Down Expand Up @@ -427,6 +428,23 @@ async def test_signal_callback(tools):
}, context)
_set_state_mock.assert_not_called()

with pytest.raises(errors.InvalidArgumentError):
await producer.signal_callback({
mode.EXCHANGE_KEY: exchange_manager.exchange_name,
mode.SYMBOL_KEY: "unused",
mode.SIGNAL_KEY: "DSDSDDSS",
mode.PRICE_KEY: "123000q", # price = 123
mode.VOLUME_KEY: "11111b", # base amount: not enough funds
mode.REDUCE_ONLY_KEY: True,
mode.ORDER_TYPE_SIGNAL: "LiMiT",
mode.STOP_PRICE_KEY: "-10%", # price - 10%
mode.TAKE_PROFIT_PRICE_KEY: "120.333333333333333d", # price + 120.333333333333333
mode.EXCHANGE_ORDER_IDS: ["ab1", "aaaaa"],
"PARAM_TAG_1": "ttt",
"PARAM_Plop": False,
}, context)
_set_state_mock.assert_not_called()


def compare_dict_with_nan(d_1, d_2):
try:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ async def _trading_view_signal_callback(self, data):
(parsed_data[self.SYMBOL_KEY] == self.merged_simple_symbol or
parsed_data[self.SYMBOL_KEY] == self.str_symbol):
await self.producers[0].signal_callback(parsed_data, script_keywords.get_base_context(self))
except trading_errors.InvalidArgumentError as e:
self.logger.error(f"Error when handling trading view signal: {e}")
except trading_errors.MissingFunds as e:
self.logger.error(f"Error when handling trading view signal: not enough funds: {e}")
except KeyError as e:
Expand Down Expand Up @@ -261,10 +263,9 @@ async def _parse_order_details(self, ctx, parsed_data):
elif side == TradingViewSignalsTradingMode.CANCEL_SIGNAL:
state = trading_enums.EvaluatorStates.NEUTRAL
else:
self.logger.error(
raise trading_errors.InvalidArgumentError(
f"Unknown signal: {parsed_data[TradingViewSignalsTradingMode.SIGNAL_KEY]}, full data= {parsed_data}"
)
state = trading_enums.EvaluatorStates.NEUTRAL
target_price = 0 if order_type == TradingViewSignalsTradingMode.MARKET_SIGNAL else (
await self._parse_price(ctx, parsed_data, TradingViewSignalsTradingMode.PRICE_KEY, 0))
stop_price = await self._parse_price(
Expand Down
Loading