Skip to content

Commit 7351d93

Browse files
committed
[Automations] support DSL condition and add DSL docs
1 parent cc08852 commit 7351d93

File tree

24 files changed

+501
-12
lines changed

24 files changed

+501
-12
lines changed

Automation/conditions/scripted_condition/scripted_condition.py

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,26 +33,29 @@ def __init__(self):
3333
self.script: str = ""
3434
self.exchange_name: str = ""
3535

36-
self._dsl_interpreter: dsl_interpreter.Interpreter = None #type: ignore
36+
self._dsl_interpreter: typing.Optional[dsl_interpreter.Interpreter] = None
3737

3838
async def evaluate(self) -> bool:
39-
return bool(await self._dsl_interpreter.interprete(self.script))
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.")
4043

4144
@staticmethod
4245
def get_description() -> str:
4346
return "Evaluates a scripted condition using the OctoBot DSL."
4447

4548
def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step_name: str) -> dict:
46-
exchanges = trading_api.get_exchange_names()
49+
exchanges = list(trading_api.get_exchange_names())
4750
return {
4851
self.SCRIPT: UI.user_input(
4952
self.SCRIPT, commons_enums.UserInputTypes.TEXT, "", inputs,
50-
title="Scripted condition: the script to evaluate. Its return value will be converted to a boolean using \"bool()\" to determine if the condition is met.",
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.",
5154
parent_input_name=step_name,
5255
),
5356
self.EXCHANGE: UI.user_input(
5457
self.EXCHANGE, commons_enums.UserInputTypes.OPTIONS, exchanges[0], inputs,
55-
options=list(exchanges),
58+
options=exchanges,
5659
title="Exchange: the name of the exchange to use for the condition.",
5760
parent_input_name=step_name,
5861
)
@@ -61,8 +64,11 @@ def get_user_inputs(self, UI: configuration.UserInputFactory, inputs: dict, step
6164
def apply_config(self, config):
6265
self.script = config[self.SCRIPT]
6366
self.exchange_name = config[self.EXCHANGE]
64-
self._dsl_interpreter = self._create_dsl_interpreter()
65-
self._validate_script()
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
6672

6773
def _validate_script(self):
6874
try:
@@ -77,12 +83,16 @@ def _validate_script(self):
7783
def _create_dsl_interpreter(self):
7884
exchange_manager = self._get_exchange_manager()
7985
ohlcv_operators = []
86+
portfolio_operators = []
8087
if exchange_manager is not None:
8188
ohlcv_operators = dsl_operators.exchange_operators.create_ohlcv_operators(
8289
exchange_manager, None, None
8390
)
91+
portfolio_operators = dsl_operators.exchange_operators.create_portfolio_operators(
92+
exchange_manager
93+
)
8494
return dsl_interpreter.Interpreter(
85-
dsl_interpreter.get_all_operators() + ohlcv_operators
95+
dsl_interpreter.get_all_operators() + ohlcv_operators + portfolio_operators
8696
)
8797

8898
def _get_exchange_manager(self):

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Meta/DSL_operators/python_std_operators/base_binary_operators.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222

2323
class AddOperator(dsl_interpreter_binary_operator.BinaryOperator):
24+
NAME = "+"
25+
DESCRIPTION = "Addition operator. Adds two operands together."
26+
EXAMPLE = "5 + 3"
27+
2428
@staticmethod
2529
def get_name() -> str:
2630
return ast.Add.__name__
@@ -31,6 +35,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
3135

3236

3337
class SubOperator(dsl_interpreter_binary_operator.BinaryOperator):
38+
NAME = "-"
39+
DESCRIPTION = "Subtraction operator. Subtracts the right operand from the left operand."
40+
EXAMPLE = "5 - 3"
41+
3442
@staticmethod
3543
def get_name() -> str:
3644
return ast.Sub.__name__
@@ -41,6 +49,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
4149

4250

4351
class MultOperator(dsl_interpreter_binary_operator.BinaryOperator):
52+
NAME = "*"
53+
DESCRIPTION = "Multiplication operator. Multiplies two operands."
54+
EXAMPLE = "5 * 3"
55+
4456
@staticmethod
4557
def get_name() -> str:
4658
return ast.Mult.__name__
@@ -51,6 +63,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
5163

5264

5365
class DivOperator(dsl_interpreter_binary_operator.BinaryOperator):
66+
NAME = "/"
67+
DESCRIPTION = "Division operator. Divides the left operand by the right operand."
68+
EXAMPLE = "10 / 2"
69+
5470
@staticmethod
5571
def get_name() -> str:
5672
return ast.Div.__name__
@@ -61,6 +77,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
6177

6278

6379
class FloorDivOperator(dsl_interpreter_binary_operator.BinaryOperator):
80+
NAME = "//"
81+
DESCRIPTION = "Floor division operator. Divides the left operand by the right operand and returns the floor of the result."
82+
EXAMPLE = "10 // 3"
83+
6484
@staticmethod
6585
def get_name() -> str:
6686
return ast.FloorDiv.__name__
@@ -71,6 +91,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
7191

7292

7393
class ModOperator(dsl_interpreter_binary_operator.BinaryOperator):
94+
NAME = "%"
95+
DESCRIPTION = "Modulo operator. Returns the remainder after dividing the left operand by the right operand."
96+
EXAMPLE = "10 % 3"
97+
7498
@staticmethod
7599
def get_name() -> str:
76100
return ast.Mod.__name__
@@ -81,6 +105,10 @@ def compute(self) -> dsl_interpreter_operator.ComputedOperatorParameterType:
81105

82106

83107
class PowOperator(dsl_interpreter_binary_operator.BinaryOperator):
108+
NAME = "**"
109+
DESCRIPTION = "Exponentiation operator. Raises the left operand to the power of the right operand."
110+
EXAMPLE = "2 ** 3"
111+
84112
@staticmethod
85113
def get_name() -> str:
86114
return ast.Pow.__name__

0 commit comments

Comments
 (0)