Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/flow/octobot_flow/logic/dsl/dsl_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ def get_flow_operator_classes(
return (
octobot_commons.dsl_interpreter.get_all_operators()
+ dsl_operators.create_ohlcv_operators(self._exchange_manager, None, None)
+ dsl_operators.create_price_operators(self._exchange_manager, None)
+ dsl_operators.create_portfolio_operators(self._exchange_manager)
+ dsl_operators.create_create_order_operators(
self._exchange_manager, trading_mode=None, dependencies=self._dependencies
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,20 @@ def _validate_script(self):
def _create_dsl_interpreter(self):
exchange_manager = self._get_exchange_manager()
ohlcv_operators = []
price_operators = []
portfolio_operators = []
if exchange_manager is not None:
ohlcv_operators = dsl_operators.exchange_operators.create_ohlcv_operators(
exchange_manager, None, None
)
price_operators = dsl_operators.exchange_operators.create_price_operators(
exchange_manager, None
)
portfolio_operators = dsl_operators.exchange_operators.create_portfolio_operators(
exchange_manager
)
return dsl_interpreter.Interpreter(
dsl_interpreter.get_all_operators() + ohlcv_operators + portfolio_operators
dsl_interpreter.get_all_operators() + ohlcv_operators + price_operators + portfolio_operators
)

def _get_exchange_manager(self):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
create_ohlcv_operators,
create_ticker_operators,
create_symbol_operators,
PriceOperator,
create_price_operators,
)
import tentacles.Meta.DSL_operators.exchange_operators.exchange_personal_data_operators
from tentacles.Meta.DSL_operators.exchange_operators.exchange_personal_data_operators import (
Expand All @@ -43,6 +45,8 @@
"create_ohlcv_operators",
"create_ticker_operators",
"create_symbol_operators",
"PriceOperator",
"create_price_operators",
"create_portfolio_operators",
"create_cancel_order_operators",
"create_fetch_order_operators",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,18 @@
from tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.symbol_operators import (
create_symbol_operators,
)
import tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.price_operators
from tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.price_operators import (
PriceOperator,
create_price_operators,
)

__all__ = [
"OHLCVOperator",
"ExchangeDataDependency",
"create_ohlcv_operators",
"create_ticker_operators",
"create_symbol_operators",
"PriceOperator",
"create_price_operators",
]
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@
class ExchangeDataDependency(octobot_trading.dsl.SymbolDependency):
data_source: str = octobot_trading.constants.OHLCV_CHANNEL

def resolve_symbol(
self, exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager]
):
if exchange_manager is not None:
self.symbol = exchange_manager.get_exchange_symbol(self.symbol)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

converts token address to unified symbol when exchange supports it


def __hash__(self) -> int:
return hash((self.symbol, self.time_frame, self.data_source))

Expand Down Expand Up @@ -87,6 +93,7 @@ def _get_candles_values_with_latest_kline_if_available(
candles_manager = candle_manager_by_time_frame_by_symbol[_time_frame][_symbol]
symbol_data = None
else:
_symbol = exchange_manager.get_exchange_symbol(_symbol)
symbol_data = octobot_trading.api.get_symbol_data(
exchange_manager, _symbol, allow_creation=False
)
Expand Down Expand Up @@ -135,6 +142,8 @@ def get_dependencies(self) -> typing.List[dsl_interpreter.InterpreterDependency]
)
if symbol_dep not in local_dependencies:
local_dependencies.append(symbol_dep)
for dependency in local_dependencies:
dependency.resolve_symbol(exchange_manager)
return super().get_dependencies() + local_dependencies

async def pre_compute(self) -> None:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# pylint: disable=missing-class-docstring,missing-function-docstring
# Drakkar-Software OctoBot-Commons
# Copyright (c) Drakkar-Software, All rights reserved.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 3.0 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library.
import typing

import octobot_commons.constants
import octobot_commons.errors
import octobot_commons.dsl_interpreter as dsl_interpreter
import octobot_trading.exchanges
import octobot_trading.api
import octobot_trading.constants

import tentacles.Meta.DSL_operators.exchange_operators.exchange_operator as exchange_operator
import tentacles.Meta.DSL_operators.exchange_operators.exchange_public_data_operators.ohlcv_operators as ohlcv_operators


class PriceOperator(exchange_operator.ExchangeOperator):
@staticmethod
def get_library() -> str:
# this is a contextual operator, so it should not be included by default in the get_all_operators function return values
return octobot_commons.constants.CONTEXTUAL_OPERATORS_LIBRARY

@classmethod
def get_parameters(cls) -> list[dsl_interpreter.OperatorParameter]:
return [
dsl_interpreter.OperatorParameter(
name="symbol",
description="the symbol to get the latest mark price for",
required=False,
type=str,
),
]

def get_symbol(self) -> typing.Optional[str]:
if parameters := self.get_computed_parameters():
symbol = parameters[0] if len(parameters) > 0 else None
return str(symbol) if symbol is not None else None
return None


def create_price_operators(
exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager],
symbol: typing.Optional[str],
) -> typing.List[type[PriceOperator]]:

def _get_latest_price(input_symbol: typing.Optional[str]) -> float:
if exchange_manager is None:
raise octobot_commons.errors.DSLInterpreterError(
"exchange_manager must be provided"
)
resolved_symbol = exchange_manager.get_exchange_symbol(input_symbol or symbol)
symbol_data = octobot_trading.api.get_symbol_data(
exchange_manager, resolved_symbol, allow_creation=False
)
try:
mark_price = symbol_data.prices_manager.get_mark_price_no_wait()
except ValueError as err:
raise octobot_commons.errors.DSLInterpreterError(
f"No up to date mark price for {resolved_symbol}"
) from err
return float(mark_price)

def _static_get_dependencies() -> typing.List[ohlcv_operators.ExchangeDataDependency]:
return [
ohlcv_operators.ExchangeDataDependency(
symbol=symbol,
time_frame=None,
data_source=octobot_trading.constants.MARK_PRICE_CHANNEL,
)
] if symbol else []

class _LocalPriceOperator(PriceOperator):
DESCRIPTION = "Returns the latest mark price for the symbol"
EXAMPLE = "price('BTC/USDT')"

@staticmethod
def get_name() -> str:
return "price"

def get_dependencies(self) -> typing.List[dsl_interpreter.InterpreterDependency]:
local_dependencies = _static_get_dependencies()
param_by_name = self.get_input_value_by_parameter()
if param_symbol := param_by_name.get("symbol"):
symbol_dep = ohlcv_operators.ExchangeDataDependency(
symbol=param_symbol,
time_frame=None,
data_source=octobot_trading.constants.MARK_PRICE_CHANNEL,
)
if symbol_dep not in local_dependencies:
local_dependencies.append(symbol_dep)
for dependency in local_dependencies:
dependency.resolve_symbol(exchange_manager)
return super().get_dependencies() + local_dependencies

async def pre_compute(self) -> None:
await super().pre_compute()
self.value = _get_latest_price(self.get_symbol())

return [_LocalPriceOperator]
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

SYMBOL = "BTC/USDT"
SYMBOL2 = "ETH/USDT"
RESOLVED_SYMBOL = "BTCUSDT"
RESOLVED_SYMBOL2 = "ETHUSDT"
TIME_FRAME = "1h"
TIME_FRAME2 = "4h"
KLINE_SIGNATURE = 0.00666
Expand Down Expand Up @@ -125,10 +127,23 @@ def _get_kline(candles_manager: mock.Mock, signature: float, kline_time_delta: t
return kline


def _identity_get_exchange_symbol(symbol):
return symbol


def _normalize_symbol(symbol: str) -> str:
symbol_aliases = {
RESOLVED_SYMBOL: SYMBOL,
RESOLVED_SYMBOL2: SYMBOL2,
}
return symbol_aliases.get(symbol, symbol)


def _get_symbol_data_factory(
btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, kline_type: str
):
def _get_symbol_data(symbol: str, **kwargs):
symbol = _normalize_symbol(symbol)
symbol_candles = {}
one_h_candles_manager = btc_1h_candles_manager if symbol == SYMBOL else eth_1h_candles_manager if symbol == SYMBOL2 else None
four_h_candles_manager = btc_4h_candles_manager if symbol == SYMBOL else None # no 4h eth candles
Expand Down Expand Up @@ -174,6 +189,7 @@ def exchange_manager_with_candles(historical_prices, historical_volume, historic
return mock.Mock(
id="exchange_manager_id",
exchange_name="binance",
get_exchange_symbol=mock.Mock(side_effect=_identity_get_exchange_symbol),
exchange_symbols_data=mock.Mock(
get_exchange_symbol_data=_get_symbol_data_factory(
btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, "no_kline"
Expand All @@ -190,6 +206,7 @@ def exchange_manager_with_candles_and_klines(historical_prices, historical_volum
return mock.Mock(
id="exchange_manager_id",
exchange_name="binance",
get_exchange_symbol=mock.Mock(side_effect=_identity_get_exchange_symbol),
exchange_symbols_data=mock.Mock(
get_exchange_symbol_data=_get_symbol_data_factory(
btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, "same_time_kline"
Expand All @@ -206,6 +223,7 @@ def exchange_manager_with_candles_and_new_candle_klines(historical_prices, histo
return mock.Mock(
id="exchange_manager_id",
exchange_name="binance",
get_exchange_symbol=mock.Mock(side_effect=_identity_get_exchange_symbol),
exchange_symbols_data=mock.Mock(
get_exchange_symbol_data=_get_symbol_data_factory(
btc_1h_candles_manager, eth_1h_candles_manager, btc_4h_candles_manager, "new_time_kline"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@

from tentacles.Meta.DSL_operators.exchange_operators.tests.exchange_public_data_operators import (
SYMBOL,
SYMBOL2,
TIME_FRAME,
KLINE_SIGNATURE,
RESOLVED_SYMBOL,
historical_prices,
historical_volume,
historical_times,
Expand Down Expand Up @@ -260,3 +262,56 @@ async def test_ohlcv_operators_dependencies(
data_source=octobot_trading.constants.OHLCV_CHANNEL
),
]


class TestGetExchangeSymbol:
@pytest.mark.asyncio
async def test_pre_compute_calls_get_exchange_symbol_with_context_symbol(
self, interpreter, exchange_manager_with_candles
):
exchange_manager_with_candles.get_exchange_symbol.reset_mock()
await interpreter.interprete("close")
exchange_manager_with_candles.get_exchange_symbol.assert_called_with(SYMBOL)

@pytest.mark.asyncio
async def test_pre_compute_calls_get_exchange_symbol_with_param_symbol(
self, interpreter, exchange_manager_with_candles
):
exchange_manager_with_candles.get_exchange_symbol.reset_mock()
await interpreter.interprete(f"close('{SYMBOL2}')")
exchange_manager_with_candles.get_exchange_symbol.assert_called_with(SYMBOL2)

@pytest.mark.asyncio
async def test_get_dependencies_calls_get_exchange_symbol(
self, interpreter, exchange_manager_with_candles
):
exchange_manager_with_candles.get_exchange_symbol.reset_mock()
interpreter.prepare("close")
interpreter.get_dependencies()
exchange_manager_with_candles.get_exchange_symbol.assert_called_with(SYMBOL)

@pytest.mark.asyncio
async def test_pre_compute_does_not_call_get_exchange_symbol_without_exchange_manager(
self, interpreter_with_candle_manager_by_time_frame_by_symbol, historical_prices
):
operator_value = await interpreter_with_candle_manager_by_time_frame_by_symbol.interprete("close")
assert np.array_equal(operator_value, historical_prices)

@pytest.mark.asyncio
async def test_pre_compute_uses_resolved_symbol_downstream(
self, exchange_manager_with_candles
):
exchange_manager_with_candles.get_exchange_symbol = mock.Mock(return_value=RESOLVED_SYMBOL)
ohlcv_interpreter = dsl_interpreter.Interpreter(
dsl_interpreter.get_all_operators()
+ exchange_operators.create_ohlcv_operators(
exchange_manager_with_candles, SYMBOL, TIME_FRAME
)
)
with mock.patch.object(
octobot_trading.api, "get_symbol_data", wraps=octobot_trading.api.get_symbol_data
) as get_symbol_data_spy:
await ohlcv_interpreter.interprete("close")
get_symbol_data_spy.assert_called_once_with(
exchange_manager_with_candles, RESOLVED_SYMBOL, allow_creation=False
)
Loading
Loading