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

Dev merge #1397

Merged
merged 11 commits into from
Dec 16, 2024
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.

64 changes: 33 additions & 31 deletions Trading/Exchange/binance/binance_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ class Binance(exchanges.RestExchange):
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
Expand Down 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 @@ -157,6 +161,18 @@ def get_additional_connector_config(self):
}
return config

def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:
signature_identifier = "signature="
return bool(
(
url
and signature_identifier in url # for GET & DELETE requests
) or (
body
and signature_identifier in body # for other requests
)
)

async def get_balance(self, **kwargs: dict):
if self.exchange_manager.is_future:
balance = []
Expand Down Expand Up @@ -193,17 +209,15 @@ async def set_symbol_partial_take_profit_stop_loss(self, symbol: str, inverse: b
"""

async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
if self.exchange_manager.is_future:
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order
return await super()._create_market_stop_loss_order(symbol, quantity, price, side, current_price, params=params)
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order

async def get_positions(self, symbols=None, **kwargs: dict) -> list:
positions = []
Expand Down Expand Up @@ -237,7 +251,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 +289,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
77 changes: 77 additions & 0 deletions Trading/Exchange/bingx/bingx_exchange.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,37 @@ class Bingx(exchanges.RestExchange):
# Set True when get_open_order() can return outdated orders (cancelled or not yet created)
CAN_HAVE_DELAYED_CANCELLED_ORDERS = True

# should be overridden locally to match exchange support
SUPPORTED_ELEMENTS = {
trading_enums.ExchangeTypes.FUTURE.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
trading_enums.TraderOrderType.STOP_LOSS,
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
# not supported or need custom mechanics with batch orders
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
},
trading_enums.ExchangeTypes.SPOT.value: {
# order that should be self-managed by OctoBot
trading_enums.ExchangeSupportedElements.UNSUPPORTED_ORDERS.value: [
# trading_enums.TraderOrderType.STOP_LOSS, # supported on spot
trading_enums.TraderOrderType.STOP_LOSS_LIMIT,
trading_enums.TraderOrderType.TAKE_PROFIT,
trading_enums.TraderOrderType.TAKE_PROFIT_LIMIT,
trading_enums.TraderOrderType.TRAILING_STOP,
trading_enums.TraderOrderType.TRAILING_STOP_LIMIT
],
# order that can be bundled together to create them all in one request
trading_enums.ExchangeSupportedElements.SUPPORTED_BUNDLED_ORDERS.value: {},
}
}

def get_adapter_class(self):
return BingxCCXTAdapter

Expand All @@ -57,18 +88,64 @@ async def get_my_recent_trades(self, symbol=None, since=None, limit=None, **kwar
return await super().get_my_recent_trades(symbol=symbol, since=since, limit=limit, **kwargs)
return await super().get_closed_orders(symbol=symbol, since=since, limit=limit, **kwargs)

def is_authenticated_request(self, url: str, method: str, headers: dict, body) -> bool:
signature_identifier = "signature="
return bool(
url
and signature_identifier in url
)

async def _create_market_stop_loss_order(self, symbol, quantity, price, side, current_price, params=None) -> dict:
params = params or {}
params["stopLossPrice"] = price # make ccxt understand that it's a stop loss
order = self.connector.adapter.adapt_order(
await self.connector.client.create_order(
symbol, trading_enums.TradeOrderType.MARKET.value, side, quantity, params=params
),
symbol=symbol, quantity=quantity
)
return order

class BingxCCXTAdapter(exchanges.CCXTAdapter):

def _update_stop_order_or_trade_type_and_price(self, order_or_trade: dict):
info = order_or_trade.get(ccxt_constants.CCXT_INFO, {})
if stop_price := order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.STOP_LOSS_PRICE.value):
# from https://bingx-api.github.io/docs/#/en-us/spot/trade-api.html#Current%20Open%20Orders
order_creation_price = float(
info.get("price") or order_or_trade.get(
trading_enums.ExchangeConstantsOrderColumns.PRICE.value
)
)
stop_price = float(stop_price)
# use stop price as order price to parse it properly
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.PRICE.value] = stop_price
# type is TAKE_STOP_LIMIT (not unified)
if order_or_trade.get(trading_enums.ExchangeConstantsOrderColumns.TYPE.value) not in (
trading_enums.TradeOrderType.STOP_LOSS.value, trading_enums.TradeOrderType.TAKE_PROFIT.value
):
if stop_price <= order_creation_price:
order_type = trading_enums.TradeOrderType.STOP_LOSS.value
else:
order_type = trading_enums.TradeOrderType.TAKE_PROFIT.value
order_or_trade[trading_enums.ExchangeConstantsOrderColumns.TYPE.value] = order_type

def fix_order(self, raw, **kwargs):
fixed = super().fix_order(raw, **kwargs)
self._update_stop_order_or_trade_type_and_price(fixed)
try:
info = fixed[ccxt_constants.CCXT_INFO]
fixed[trading_enums.ExchangeConstantsOrderColumns.EXCHANGE_ID.value] = info["orderId"]
except KeyError:
pass
return fixed

def fix_trades(self, raw, **kwargs):
fixed = super().fix_trades(raw, **kwargs)
for trade in fixed:
self._update_stop_order_or_trade_type_and_price(trade)
return fixed

def fix_market_status(self, raw, remove_price_limits=False, **kwargs):
fixed = super().fix_market_status(raw, remove_price_limits=remove_price_limits, **kwargs)
if not fixed:
Expand Down
Loading
Loading