Skip to content

Commit

Permalink
Merge pull request #1397 from Drakkar-Software/dev
Browse files Browse the repository at this point in the history
Dev merge
  • Loading branch information
GuillaumeDSM authored Dec 16, 2024
2 parents 236dd82 + 129f0cc commit 3b79775
Show file tree
Hide file tree
Showing 10 changed files with 295 additions and 165 deletions.
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

0 comments on commit 3b79775

Please sign in to comment.