Skip to content

Commit be4e317

Browse files
authored
Merge pull request #1667 from Drakkar-Software/dev
Master update
2 parents 2b26ee4 + b0191b8 commit be4e317

File tree

27 files changed

+616
-11
lines changed

27 files changed

+616
-11
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .scripted_condition import ScriptedCondition
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"version": "1.2.0",
3+
"origin_package": "OctoBot-Default-Tentacles",
4+
"tentacles": ["ScriptedCondition"],
5+
"tentacles-requirements": []
6+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# This file is part of OctoBot (https://github.com/Drakkar-Software/OctoBot)
2+
# Copyright (c) 2023 Drakkar-Software, All rights reserved.
3+
#
4+
# OctoBot is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU General Public License
6+
# as published by the Free Software Foundation; either
7+
# version 3.0 of the License, or (at your option) any later version.
8+
#
9+
# OctoBot is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public
15+
# License along with OctoBot. If not, see <https://www.gnu.org/licenses/>.
16+
import typing
17+
18+
import octobot_commons.configuration as configuration
19+
import octobot_trading.api as trading_api
20+
import octobot_commons.enums as commons_enums
21+
import octobot.automation.bases.abstract_condition as abstract_condition
22+
import octobot_commons.dsl_interpreter as dsl_interpreter
23+
24+
import tentacles.Meta.DSL_operators as dsl_operators
25+
26+
27+
class ScriptedCondition(abstract_condition.AbstractCondition):
28+
SCRIPT = "script"
29+
EXCHANGE = "exchange"
30+
31+
def __init__(self):
32+
super().__init__()
33+
self.script: str = ""
34+
self.exchange_name: str = ""
35+
36+
self._dsl_interpreter: typing.Optional[dsl_interpreter.Interpreter] = None
37+
38+
async def evaluate(self) -> bool:
39+
if self._dsl_interpreter:
40+
script_result = await self._dsl_interpreter.interprete(self.script)
41+
return bool(script_result)
42+
raise ValueError("Scripted condition is not properly configured, the script is likely invalid.")
43+
44+
@staticmethod
45+
def get_description() -> str:
46+
return "Evaluates a scripted condition using the OctoBot DSL."
47+
48+
def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:
49+
exchanges = list(trading_api.get_exchange_names())
50+
return {
51+
self.SCRIPT: UI.user_input(
52+
self.SCRIPT, commons_enums.UserInputTypes.TEXT, "", inputs,
53+
title="Scripted condition: the OctoBot DSL expression to evaluate (more info in automation details). Its return value will be converted to a boolean using \"bool()\" to determine if the condition is met.",
54+
parent_input_name=step_name,
55+
),
56+
self.EXCHANGE: UI.user_input(
57+
self.EXCHANGE, commons_enums.UserInputTypes.OPTIONS, exchanges[0], inputs,
58+
options=exchanges,
59+
title="Exchange: the name of the exchange to use for the condition.",
60+
parent_input_name=step_name,
61+
)
62+
}
63+
64+
def apply_config(self, config):
65+
self.script = config[self.SCRIPT]
66+
self.exchange_name = config[self.EXCHANGE]
67+
if self.script and self.exchange_name:
68+
self._dsl_interpreter = self._create_dsl_interpreter()
69+
self._validate_script()
70+
else:
71+
self._dsl_interpreter = None
72+
73+
def _validate_script(self):
74+
try:
75+
self._dsl_interpreter.prepare(self.script)
76+
self.logger.info(
77+
f"Formula interpreter successfully prepared \"{self.script}\" condition"
78+
)
79+
except Exception as e:
80+
self.logger.error(f"Error when parsing condition {self.script}: {e}")
81+
raise e
82+
83+
def _create_dsl_interpreter(self):
84+
exchange_manager = self._get_exchange_manager()
85+
ohlcv_operators = []
86+
portfolio_operators = []
87+
if exchange_manager is not None:
88+
ohlcv_operators = dsl_operators.exchange_operators.create_ohlcv_operators(
89+
exchange_manager, None, None
90+
)
91+
portfolio_operators = dsl_operators.exchange_operators.create_portfolio_operators(
92+
exchange_manager
93+
)
94+
return dsl_interpreter.Interpreter(
95+
dsl_interpreter.get_all_operators() + ohlcv_operators + portfolio_operators
96+
)
97+
98+
def _get_exchange_manager(self):
99+
for exchange_id in trading_api.get_exchange_ids():
100+
exchange_manager = trading_api.get_exchange_manager_from_exchange_id(exchange_id)
101+
if exchange_manager.exchange_name == self.exchange_name and exchange_manager.is_backtesting == False:
102+
return exchange_manager
103+
raise ValueError(f"No exchange manager found for exchange name: {self.exchange_name}")

Meta/DSL_operators/exchange_operators/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,17 @@
2121
ExchangeDataDependency,
2222
create_ohlcv_operators,
2323
)
24-
24+
import tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators
25+
from tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators import (
26+
PortfolioOperator,
27+
create_portfolio_operators,
28+
)
2529

2630

2731
__all__ = [
2832
"OHLCVOperator",
2933
"ExchangeDataDependency",
3034
"create_ohlcv_operators",
35+
"PortfolioOperator",
36+
"create_portfolio_operators",
3137
]
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# pylint: disable=R0801
2+
# Drakkar-Software OctoBot-Commons
3+
# Copyright (c) Drakkar-Software, All rights reserved.
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 3.0 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library.
17+
18+
import tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators.portfolio_operators
19+
from tentacles.Meta.DSL_operators.exchange_operators.exchange_private_data_operators.portfolio_operators import (
20+
PortfolioOperator,
21+
create_portfolio_operators,
22+
)
23+
__all__ = [
24+
"PortfolioOperator",
25+
"create_portfolio_operators",
26+
]
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# pylint: disable=missing-class-docstring,missing-function-docstring
2+
# Drakkar-Software OctoBot-Commons
3+
# Copyright (c) Drakkar-Software, All rights reserved.
4+
#
5+
# This library is free software; you can redistribute it and/or
6+
# modify it under the terms of the GNU Lesser General Public
7+
# License as published by the Free Software Foundation; either
8+
# version 3.0 of the License, or (at your option) any later version.
9+
#
10+
# This library is distributed in the hope that it will be useful,
11+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13+
# Lesser General Public License for more details.
14+
#
15+
# You should have received a copy of the GNU Lesser General Public
16+
# License along with this library.
17+
import typing
18+
19+
import octobot_commons.constants
20+
import octobot_commons.errors
21+
import octobot_commons.dsl_interpreter as dsl_interpreter
22+
import octobot_trading.personal_data
23+
import octobot_trading.exchanges
24+
import octobot_trading.api
25+
26+
import tentacles.Meta.DSL_operators.exchange_operators.exchange_operator as exchange_operator
27+
28+
29+
class PortfolioOperator(exchange_operator.ExchangeOperator):
30+
def __init__(self, *parameters: dsl_interpreter.OperatorParameterType, **kwargs: typing.Any):
31+
super().__init__(*parameters, **kwargs)
32+
self.value: dsl_interpreter_operator.ComputedOperatorParameterType = exchange_operator.UNINITIALIZED_VALUE # type: ignore
33+
34+
@staticmethod
35+
def get_library() -> str:
36+
# this is a contextual operator, so it should not be included by default in the get_all_operators function return values
37+
return octobot_commons.constants.CONTEXTUAL_OPERATORS_LIBRARY
38+
39+
@staticmethod
40+
def get_parameters() -> list[dsl_interpreter.OperatorParameter]:
41+
return [
42+
dsl_interpreter.OperatorParameter(name="asset", description="the asset to get the value for", required=False, type=str),
43+
]
44+
45+
def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:
46+
if self.value is exchange_operator.UNINITIALIZED_VALUE:
47+
raise octobot_commons.errors.DSLInterpreterError("{self.__class__.__name__} has not been initialized")
48+
return self.value
49+
50+
51+
def create_portfolio_operators(
52+
exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager],
53+
) -> typing.List[type[PortfolioOperator]]:
54+
55+
def _get_asset_holdings(asset: str) -> octobot_trading.personal_data.Asset:
56+
return octobot_trading.api.get_portfolio_currency(exchange_manager, asset)
57+
58+
class _TotalOperator(PortfolioOperator):
59+
DESCRIPTION = "Returns the total holdings of the asset in the portfolio"
60+
EXAMPLE = "total('BTC')"
61+
62+
@staticmethod
63+
def get_name() -> str:
64+
return "total"
65+
66+
async def pre_compute(self) -> None:
67+
await super().pre_compute()
68+
asset = self.get_computed_parameters()[0]
69+
self.value = float(_get_asset_holdings(asset).total)
70+
71+
class _AvailableOperator(PortfolioOperator):
72+
DESCRIPTION = "Returns the available holdings of the asset in the portfolio"
73+
EXAMPLE = "available('BTC')"
74+
75+
@staticmethod
76+
def get_name() -> str:
77+
return "available"
78+
79+
async def pre_compute(self) -> None:
80+
await super().pre_compute()
81+
asset = self.get_computed_parameters()[0]
82+
self.value = float(_get_asset_holdings(asset).available)
83+
84+
85+
return [_TotalOperator, _AvailableOperator]

Meta/DSL_operators/exchange_operators/exchange_public_data_operators/ohlcv_operators.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ def compute(self) -> dsl_interpreter.ComputedOperatorParameterType:
7777

7878
def create_ohlcv_operators(
7979
exchange_manager: typing.Optional[octobot_trading.exchanges.ExchangeManager],
80-
symbol: str,
81-
time_frame: str,
80+
symbol: typing.Optional[str],
81+
time_frame: typing.Optional[str],
8282
candle_manager_by_time_frame_by_symbol: typing.Optional[
8383
typing.Dict[str, typing.Dict[str, octobot_trading.exchange_data.CandlesManager]]
8484
] = None
@@ -120,7 +120,7 @@ def _get_candles_values_with_latest_kline_if_available(
120120
else:
121121
octobot_commons.logging.get_logger(OHLCVOperator.__name__).error(
122122
f"{exchange_manager.exchange_name + '' if exchange_manager is not None else ''}{_symbol} {_time_frame} "
123-
f"kline time ({kline_time}) is not equal to last candle time not the last time + {time_frame} "
123+
f"kline time ({kline_time}) is not equal to last candle time not the last time + {_time_frame} "
124124
f"({last_candle_time} + {tf_seconds}) seconds. Kline has been ignored."
125125
)
126126
return candles_values
@@ -145,41 +145,59 @@ async def pre_compute(self) -> None:
145145
self.value = _get_candles_values_with_latest_kline_if_available(*self.get_symbol_and_time_frame(), self.PRICE_INDEX, -1)
146146

147147
class _OpenPriceOperator(_LocalOHLCVOperator):
148+
DESCRIPTION = "Returns the candle's open price as array of floats"
149+
EXAMPLE = "open('BTC/USDT', '1h')"
150+
148151
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_OPEN
149152

150153
@staticmethod
151154
def get_name() -> str:
152155
return "open"
153156

154157
class _HighPriceOperator(_LocalOHLCVOperator):
158+
DESCRIPTION = "Returns the candle's high price as array of floats"
159+
EXAMPLE = "high('BTC/USDT', '1h')"
160+
155161
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_HIGH
156162

157163
@staticmethod
158164
def get_name() -> str:
159165
return "high"
160166

161167
class _LowPriceOperator(_LocalOHLCVOperator):
168+
DESCRIPTION = "Returns the candle's low price as array of floats"
169+
EXAMPLE = "low('BTC/USDT', '1h')"
170+
162171
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_LOW
163172

164173
@staticmethod
165174
def get_name() -> str:
166175
return "low"
167176

168177
class _ClosePriceOperator(_LocalOHLCVOperator):
178+
DESCRIPTION = "Returns the candle's close price as array of floats"
179+
EXAMPLE = "close('BTC/USDT', '1h')"
180+
169181
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_CLOSE
170182

171183
@staticmethod
172184
def get_name() -> str:
173185
return "close"
174186

175187
class _VolumePriceOperator(_LocalOHLCVOperator):
188+
DESCRIPTION = "Returns the candle's volume as array of floats"
189+
EXAMPLE = "volume('BTC/USDT', '1h')"
190+
176191
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_VOL
177192

178193
@staticmethod
179194
def get_name() -> str:
180195
return "volume"
181196

182197
class _TimePriceOperator(_LocalOHLCVOperator):
198+
DESCRIPTION = "Returns the candle's time as array of floats"
199+
EXAMPLE = "time('BTC/USDT', '1h')"
200+
183201
PRICE_INDEX = commons_enums.PriceIndexes.IND_PRICE_TIME
184202

185203
@staticmethod

0 commit comments

Comments
 (0)