Skip to content

Commit

Permalink
[Futures] add more flexibility in contracts and positions update
Browse files Browse the repository at this point in the history
  • Loading branch information
GuillaumeDSM committed Nov 22, 2024
1 parent 751817d commit 3ab2064
Show file tree
Hide file tree
Showing 22 changed files with 290 additions and 59 deletions.
6 changes: 6 additions & 0 deletions octobot_trading/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,13 +201,16 @@
from octobot_trading.api.positions import (
get_positions,
close_position,
set_is_exclusively_using_exchange_position_details,
)
from octobot_trading.api.contracts import (
is_inverse_future_contract,
is_perpetual_future_contract,
get_pair_contracts,
is_handled_contract,
has_pair_future_contract,
load_pair_contract,
create_default_future_contract,
)
from octobot_trading.api.storage import (
clear_trades_storage_history,
Expand Down Expand Up @@ -381,7 +384,10 @@
"is_perpetual_future_contract",
"get_pair_contracts",
"is_handled_contract",
"has_pair_future_contract",
"load_pair_contract",
"create_default_future_contract",
"set_is_exclusively_using_exchange_position_details",
"clear_trades_storage_history",
"clear_candles_storage_history",
"clear_database_storage_history",
Expand Down
14 changes: 13 additions & 1 deletion octobot_trading/api/contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import octobot_trading.exchange_data as exchange_data
import decimal

import octobot_trading.enums as enums
import octobot_trading.exchange_data as exchange_data

def is_inverse_future_contract(contract_type):
return exchange_data.FutureContract(None, None, contract_type).is_inverse_contract()
Expand All @@ -32,5 +34,15 @@ def is_handled_contract(contract) -> bool:
return contract.is_handled_contract()


def has_pair_future_contract(exchange_manager, pair: str) -> bool:
return exchange_manager.exchange.has_pair_future_contract(pair)


def load_pair_contract(exchange_manager, contract_dict: dict):
exchange_data.update_future_contract_from_dict(exchange_manager, contract_dict)


def create_default_future_contract(
pair: str, leverage: decimal.Decimal, contract_type: enums.FutureContractType
) -> exchange_data.FutureContract:
return exchange_data.create_default_future_contract(pair, leverage, contract_type)
8 changes: 6 additions & 2 deletions octobot_trading/api/positions.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,5 +36,9 @@ async def close_position(exchange_manager, symbol: str, side: enums.PositionSide
return 0


def load_pair_contract(exchange_manager, position_dict: dict):
exchange_data.update_future_contract_from_dict(exchange_manager, position_dict)
def set_is_exclusively_using_exchange_position_details(
exchange_manager, is_exclusively_using_exchange_position_details: bool
):
exchange_manager.exchange_personal_data.positions_manager.is_exclusively_using_exchange_position_details = (
is_exclusively_using_exchange_position_details
)
3 changes: 2 additions & 1 deletion octobot_trading/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ class TradeExtraConstants(enum.Enum):

class ExchangeConstantsPositionColumns(enum.Enum):
ID = "id"
LOCAL_ID = "local_id"
TIMESTAMP = "timestamp"
SYMBOL = "symbol"
ENTRY_PRICE = "entry_price"
Expand All @@ -355,6 +356,7 @@ class ExchangeConstantsPositionColumns(enum.Enum):
SIZE = "size"
NOTIONAL = "notional"
INITIAL_MARGIN = "initial_margin"
AUTO_DEPOSIT_MARGIN = "auto_deposit_margin"
COLLATERAL = "collateral"
LEVERAGE = "leverage"
MARGIN_TYPE = "margin_type"
Expand All @@ -364,7 +366,6 @@ class ExchangeConstantsPositionColumns(enum.Enum):
MAINTENANCE_MARGIN_RATE = "maintenance_margin_rate"
STATUS = "status"
SIDE = "side"
EXCHANGE_POSITION_ID = "exchange_position_id"


class ExchangeConstantsMarginContractColumns(enum.Enum):
Expand Down
2 changes: 2 additions & 0 deletions octobot_trading/exchange_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
FutureContract,
update_contracts_from_positions,
update_future_contract_from_dict,
create_default_future_contract,
)
from octobot_trading.exchange_data import exchange_symbol_data
from octobot_trading.exchange_data.exchange_symbol_data import (
Expand Down Expand Up @@ -195,6 +196,7 @@
"FutureContract",
"update_contracts_from_positions",
"update_future_contract_from_dict",
"create_default_future_contract",
"ExchangeSymbolsData",
"ExchangeSymbolData",
"UNAUTHENTICATED_UPDATER_PRODUCERS",
Expand Down
2 changes: 2 additions & 0 deletions octobot_trading/exchange_data/contracts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,13 @@
from octobot_trading.exchange_data.contracts.contract_factory import (
update_contracts_from_positions,
update_future_contract_from_dict,
create_default_future_contract,
)

__all__ = [
"MarginContract",
"FutureContract",
"update_contracts_from_positions",
"update_future_contract_from_dict",
"create_default_future_contract",
]
20 changes: 19 additions & 1 deletion octobot_trading/exchange_data/contracts/contract_factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import octobot_trading.enums as enums
import octobot_trading.constants as constants
import octobot_trading.exchange_data.contracts.future_contract as future_contract


def update_contracts_from_positions(exchange_manager, positions) -> bool:
Expand Down Expand Up @@ -64,7 +65,9 @@ def update_future_contract_from_dict(exchange_manager, contract: dict) -> bool:
)),
margin_type=enums.MarginType(contract[enums.ExchangeConstantsMarginContractColumns.MARGIN_TYPE.value]),
contract_type=enums.FutureContractType(contract[enums.ExchangeConstantsFutureContractColumns.CONTRACT_TYPE.value]),
position_mode=enums.PositionMode(contract[enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value]),
position_mode=enums.PositionMode(contract[enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value])
if contract[enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value]
else contract[enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value],
maintenance_margin_rate=decimal.Decimal(str(
contract[enums.ExchangeConstantsFutureContractColumns.MAINTENANCE_MARGIN_RATE.value]
)),
Expand All @@ -75,5 +78,20 @@ def update_future_contract_from_dict(exchange_manager, contract: dict) -> bool:
)


def create_default_future_contract(
pair: str, leverage: decimal.Decimal, contract_type: enums.FutureContractType
) -> future_contract.FutureContract:
return future_contract.FutureContract(
pair=pair,
contract_size=constants.DEFAULT_SYMBOL_CONTRACT_SIZE,
margin_type=constants.DEFAULT_SYMBOL_MARGIN_TYPE,
contract_type=contract_type,
maximum_leverage=constants.DEFAULT_SYMBOL_MAX_LEVERAGE,
current_leverage=leverage,
position_mode=constants.DEFAULT_SYMBOL_POSITION_MODE,
maintenance_margin_rate=constants.DEFAULT_SYMBOL_MAINTENANCE_MARGIN_RATE
)


def _get_logger():
return logging.get_logger("contract_factory")
3 changes: 2 additions & 1 deletion octobot_trading/exchange_data/contracts/future_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ def to_dict(self):
enums.ExchangeConstantsFutureContractColumns.CONTRACT_TYPE.value:
self.contract_type.value if self.contract_type else self.contract_type,
enums.ExchangeConstantsFutureContractColumns.MINIMUM_TICK_SIZE.value: self.minimum_tick_size,
enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value: self.position_mode.value,
enums.ExchangeConstantsFutureContractColumns.POSITION_MODE.value:
self.position_mode.value if self.position_mode else self.position_mode,
enums.ExchangeConstantsFutureContractColumns.MAINTENANCE_MARGIN_RATE.value:
self.maintenance_margin_rate,
enums.ExchangeConstantsFutureContractColumns.TAKE_PROFIT_STOP_LOSS_MODE.value:
Expand Down
32 changes: 23 additions & 9 deletions octobot_trading/exchanges/connectors/ccxt/ccxt_adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -273,18 +273,33 @@ def parse_position(self, fixed, force_empty=False, **kwargs):
# CCXT standard position parsing logic
# if mode is enums.PositionMode.ONE_WAY:
original_side = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SIDE.value)
position_side = enums.PositionSide.BOTH
# todo when handling cross positions
# side = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SIDE.value, enums.PositionSide.UNKNOWN.value)
# position_side = enums.PositionSide.LONG \
# if side == enums.PositionSide.LONG.value else enums.PositionSide.
symbol = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SYMBOL.value)
contract_size = decimal.Decimal(str(fixed.get(ccxt_enums.ExchangePositionCCXTColumns.CONTRACT_SIZE.value, 0)))
contracts = constants.ZERO if force_empty \
else decimal.Decimal(str(fixed.get(ccxt_enums.ExchangePositionCCXTColumns.CONTRACTS.value, 0)))
is_empty = contracts == constants.ZERO
position_mode = (
enums.PositionMode.HEDGE if fixed.get(ccxt_enums.ExchangePositionCCXTColumns.HEDGED.value, False)
else enums.PositionMode.ONE_WAY
)
if position_mode is enums.PositionMode.HEDGE:
# todo when handling helge positions
side = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.SIDE.value, enums.PositionSide.UNKNOWN.value)
position_side = enums.PositionSide.LONG \
if side == enums.PositionSide.LONG.value else enums.PositionSide.SHORT
log_func = self.logger.debug
if is_empty:
log_func = self.logger.error
log_func(f"Unhandled {symbol} position mode ({position_mode.value}). This position can't be traded.")
else:
# One way position use BOTH side as there is always only one position per symbol.
# This position can turn long and short
position_side = enums.PositionSide.BOTH
liquidation_price = fixed.get(ccxt_enums.ExchangePositionCCXTColumns.LIQUIDATION_PRICE.value, 0)
if margin_type := fixed.get(ccxt_enums.ExchangePositionCCXTColumns.MARGIN_TYPE.value, None):
if margin_type := fixed.get(
ccxt_enums.ExchangePositionCCXTColumns.MARGIN_TYPE.value,
fixed.get(ccxt_enums.ExchangePositionCCXTColumns.MARGIN_MODE.value, None) # can also be contained in margin mode
):
margin_type = enums.MarginType(margin_type)
if force_empty or liquidation_price is None:
liquidation_price = constants.NaN
Expand All @@ -306,9 +321,7 @@ def parse_position(self, fixed, force_empty=False, **kwargs):
enums.ExchangeConstantsPositionColumns.LEVERAGE.value:
self.safe_decimal(fixed, ccxt_enums.ExchangePositionCCXTColumns.LEVERAGE.value,
constants.DEFAULT_SYMBOL_LEVERAGE),
enums.ExchangeConstantsPositionColumns.POSITION_MODE.value: None if is_empty else
enums.PositionMode.HEDGE if fixed.get(ccxt_enums.ExchangePositionCCXTColumns.HEDGED.value, True)
else enums.PositionMode.ONE_WAY,
enums.ExchangeConstantsPositionColumns.POSITION_MODE.value: position_mode,
# next values are always 0 when the position empty (0 contracts)
enums.ExchangeConstantsPositionColumns.COLLATERAL.value: constants.ZERO if is_empty else
decimal.Decimal(
Expand All @@ -319,6 +332,7 @@ def parse_position(self, fixed, force_empty=False, **kwargs):
enums.ExchangeConstantsPositionColumns.INITIAL_MARGIN.value: constants.ZERO if is_empty else
decimal.Decimal(
f"{fixed.get(ccxt_enums.ExchangePositionCCXTColumns.INITIAL_MARGIN.value, 0) or 0}"),
enums.ExchangeConstantsPositionColumns.AUTO_DEPOSIT_MARGIN.value: False, # default value
enums.ExchangeConstantsPositionColumns.UNREALIZED_PNL.value: constants.ZERO if is_empty else
decimal.Decimal(
f"{fixed.get(ccxt_enums.ExchangePositionCCXTColumns.UNREALISED_PNL.value, 0) or 0}"),
Expand Down
22 changes: 14 additions & 8 deletions octobot_trading/exchanges/implementations/exchange_simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import octobot_trading.exchanges.util as exchange_util
import octobot_trading.exchanges.connectors.simulator.exchange_simulator_connector as exchange_simulator_connector
import octobot_trading.exchanges.types.rest_exchange as rest_exchange
import octobot_trading.exchange_data.contracts.contract_factory as contract_factory


class ExchangeSimulator(rest_exchange.RestExchange):
Expand Down Expand Up @@ -119,15 +120,20 @@ async def load_pair_future_contract(self, pair: str):
Create a new FutureContract for the pair
:param pair: the pair
"""
contract = contract_factory.create_default_future_contract(
pair,
constants.DEFAULT_SYMBOL_LEVERAGE,
self.exchange_manager.exchange_config.backtesting_exchange_config.future_contract_type
)
return self.create_pair_contract(
pair=pair,
current_leverage=constants.DEFAULT_SYMBOL_LEVERAGE,
contract_size=constants.DEFAULT_SYMBOL_CONTRACT_SIZE,
margin_type=constants.DEFAULT_SYMBOL_MARGIN_TYPE,
contract_type=self.exchange_manager.exchange_config.backtesting_exchange_config.future_contract_type,
position_mode=constants.DEFAULT_SYMBOL_POSITION_MODE,
maintenance_margin_rate=constants.DEFAULT_SYMBOL_MAINTENANCE_MARGIN_RATE,
maximum_leverage=constants.DEFAULT_SYMBOL_MAX_LEVERAGE
contract.pair,
contract.current_leverage,
contract.contract_size,
contract.margin_type,
contract.contract_type,
contract.position_mode,
contract.maintenance_margin_rate,
maximum_leverage = contract.maximum_leverage,
)

async def get_symbol_leverage(self, symbol: str, **kwargs: dict):
Expand Down
6 changes: 3 additions & 3 deletions octobot_trading/modes/channel/abstract_mode_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def can_create_order(self, symbol, state):
)
can_create_order = max_order_size > symbol_min_amount
self.logger.debug(
f"can_create_order: {can_create_order} = "
f"can_create_order: {can_create_order} [{symbol}] = "
f"max_order_size > symbol_min_amount = {max_order_size} > {symbol_min_amount}"
)
return can_create_order
Expand All @@ -182,7 +182,7 @@ async def can_create_order(self, symbol, state):
if state == enums.EvaluatorStates.VERY_SHORT.value or state == enums.EvaluatorStates.SHORT.value:
can_create_order = portfolio.get_currency_portfolio(currency).available > symbol_min_amount
self.logger.debug(
f"can_create_order: {can_create_order} = "
f"can_create_order: {can_create_order} [{symbol}] = "
f"portfolio.get_currency_portfolio(currency).available > symbol_min_amount = "
f"{portfolio.get_currency_portfolio(currency).available} > {symbol_min_amount}"
)
Expand All @@ -192,7 +192,7 @@ async def can_create_order(self, symbol, state):
elif state == enums.EvaluatorStates.LONG.value or state == enums.EvaluatorStates.VERY_LONG.value:
can_create_order = portfolio.get_currency_portfolio(market).available > order_min_amount
self.logger.debug(
f"can_create_order: {can_create_order} = "
f"can_create_order: {can_create_order} [{symbol}] = "
f"portfolio.get_currency_portfolio(market).available > order_min_amount = "
f"{portfolio.get_currency_portfolio(market).available} > {order_min_amount}"
)
Expand Down
14 changes: 13 additions & 1 deletion octobot_trading/modes/script_keywords/basic_keywords/amount.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import octobot_commons.symbols as commons_symbols
import octobot_trading.modes.script_keywords.dsl as dsl
import octobot_trading.modes.script_keywords.basic_keywords.account_balance as account_balance
import octobot_trading.modes.script_keywords.basic_keywords.position as position_kw
import octobot_trading.personal_data as trading_personal_data
import octobot_trading.errors as trading_errors
import octobot_trading.enums as trading_enums
Expand Down Expand Up @@ -77,7 +78,18 @@ async def get_amount_from_input_amount(
)
amount_value = total_symbol_assets_holdings_value * amount_value / trading_constants.ONE_HUNDRED
elif amount_type is dsl.QuantityType.POSITION_PERCENT:
raise NotImplementedError(amount_type)
if context.exchange_manager.is_future:
if position_kw.is_in_one_way_position_mode(context):
# use abs() since short positions have negative size
amount_value = abs(
position_kw.get_position(
context, symbol=context.symbol, side=trading_enums.PositionSide.BOTH.value
).size
) * amount_value / trading_constants.ONE_HUNDRED
else:
raise NotImplementedError(f"{amount_type} input type is not implemented for non-one-way positions")
else:
raise NotImplementedError(f"{amount_type} input type is not implemented for non-future exchanges")
else:
raise trading_errors.InvalidArgumentError(f"Unsupported input: {input_amount} make sure to use a supported syntax for amount")
adapted_amount = await account_balance.adapt_amount_to_holdings(
Expand Down
12 changes: 12 additions & 0 deletions octobot_trading/personal_data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,14 @@
PositionsUpdater,
PositionsManager,
create_position_instance_from_raw,
create_position_instance_from_dict,
sanitize_raw_position,
create_position_from_type,
create_symbol_position,
parse_position_status,
parse_position_side,
parse_position_margin_type,
parse_position_mode,
LiquidatePositionState,
IdlePositionState,
ActivePositionState,
Expand All @@ -175,6 +180,7 @@
TradesProducer,
TradesChannel,
create_trade_instance_from_raw,
create_closed_order_instance_from_raw_trade,
create_trade_from_order,
create_trade_from_dict,
TradesUpdater,
Expand Down Expand Up @@ -374,13 +380,19 @@
"PositionsUpdater",
"PositionsManager",
"create_position_instance_from_raw",
"create_position_instance_from_dict",
"sanitize_raw_position",
"create_position_from_type",
"create_symbol_position",
"parse_position_status",
"parse_position_side",
"parse_position_margin_type",
"parse_position_mode",
"TradesManager",
"TradesProducer",
"TradesChannel",
"create_trade_instance_from_raw",
"create_closed_order_instance_from_raw_trade",
"create_trade_from_order",
"create_trade_from_dict",
"TradesUpdater",
Expand Down
1 change: 0 additions & 1 deletion octobot_trading/personal_data/exchange_personal_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,6 @@ async def handle_portfolio_and_position_update_from_order(
await self.handle_portfolio_update_notification(self.portfolio_manager.portfolio.portfolio)

if self.exchange_manager.is_future:
# should this be done only "if should_notify" ?
await self.handle_position_instance_update(
order.exchange_manager.exchange_personal_data.positions_manager.get_order_position(order),
should_notify=True
Expand Down
Loading

0 comments on commit 3ab2064

Please sign in to comment.