Skip to content

Commit 660b0a3

Browse files
committed
Allow to use profileID and service feeds
Signed-off-by: Herklos <herklos@drakkar.software>
1 parent 59c3a6a commit 660b0a3

File tree

16 files changed

+1093
-205
lines changed

16 files changed

+1093
-205
lines changed

example.py

Lines changed: 37 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,38 @@
11
import asyncio
2-
import tulipy # Can be any TA library.
2+
import tulipy # Can be any TA library.
33
import octobot_script as obs
4+
from octobot_script.api.ploting import generate_and_show_report
45

56

6-
async def rsi_test():
7+
async def example():
8+
async def initialize(ctx):
9+
# Compute entries only once per backtest.
10+
closes = await obs.Close(ctx, max_history=True)
11+
times = await obs.Time(ctx, max_history=True, use_close_time=True)
12+
rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"])
13+
delta = len(closes) - len(rsi_v)
14+
# Populate entries with timestamps of candles where RSI is
15+
# bellow the "rsi_value_buy_threshold" configuration.
16+
run_data["entries"] = {
17+
times[index + delta]
18+
for index, rsi_val in enumerate(rsi_v)
19+
if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"]
20+
}
21+
await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"])
22+
723
async def strategy(ctx):
8-
# Will be called at each candle.
9-
if run_data["entries"] is None:
10-
# Compute entries only once per backtest.
11-
closes = await obs.Close(ctx, max_history=True)
12-
times = await obs.Time(ctx, max_history=True, use_close_time=True)
13-
rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"])
14-
delta = len(closes) - len(rsi_v)
15-
# Populate entries with timestamps of candles where RSI is
16-
# bellow the "rsi_value_buy_threshold" configuration.
17-
run_data["entries"] = {
18-
times[index + delta]
19-
for index, rsi_val in enumerate(rsi_v)
20-
if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"]
21-
}
22-
await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"])
24+
# Called at each candle.
25+
# Uses pre-computed entries times to enter positions when relevant.
26+
# Also, instantly set take profits and stop losses.
27+
# Position exits could also be set separately.
2328
if obs.current_live_time(ctx) in run_data["entries"]:
24-
# Uses pre-computed entries times to enter positions when relevant.
25-
# Also, instantly set take profits and stop losses.
26-
# Position exists could also be set separately.
27-
await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%")
29+
await obs.market(
30+
ctx,
31+
"buy",
32+
amount="10%",
33+
stop_loss_offset="-15%",
34+
take_profit_offset="25%",
35+
)
2836

2937
# Configuration that will be passed to each run.
3038
# It will be accessible under "ctx.tentacle.trading_config".
@@ -34,19 +42,23 @@ async def strategy(ctx):
3442
}
3543

3644
# Read and cache candle data to make subsequent backtesting runs faster.
37-
data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400)
45+
data = await obs.get_data(
46+
"BTC/USDT", "1d", start_timestamp=1505606400, social_services=[]
47+
)
3848
run_data = {
3949
"entries": None,
4050
}
4151
# Run a backtest using the above data, strategy and configuration.
42-
res = await obs.run(data, strategy, config)
52+
res = await obs.run(
53+
data, config, initialize_func=initialize, strategy_func=strategy
54+
)
4355
print(res.describe())
4456
# Generate and open report including indicators plots
45-
await res.plot(show=True)
57+
await generate_and_show_report(res)
4658
# Stop data to release local databases.
4759
await data.stop()
4860

4961

5062
# Call the execution of the script inside "asyncio.run" as
5163
# OctoBot-Script runs using the python asyncio framework.
52-
asyncio.run(rsi_test())
64+
asyncio.run(example())

octobot_script/api/data_fetching.py

Lines changed: 163 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,189 @@
1414
# You should have received a copy of the GNU General Public
1515
# License along with OctoBot-Script. If not, see <https://www.gnu.org/licenses/>.
1616

17+
import datetime
18+
1719
import octobot_backtesting.api as backtesting_api
1820
import octobot_commons.symbols as commons_symbols
1921
import octobot_commons.enums as commons_enums
2022
import octobot_trading.enums as trading_enums
2123
import octobot_script.internal.octobot_mocks as octobot_mocks
2224

2325

26+
def _validate_tentacles_source(tentacles_config, profile_id):
27+
if tentacles_config is not None and profile_id is not None:
28+
raise ValueError("Only one of tentacles_config or profile_id can be provided.")
29+
30+
2431
def _ensure_ms_timestamp(timestamp):
2532
if timestamp is None:
2633
return timestamp
2734
if timestamp < 16737955050: # Friday 28 May 2500 07:57:30
2835
return timestamp * 1000
2936

3037

31-
async def historical_data(symbol, timeframe, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value,
32-
start_timestamp=None, end_timestamp=None):
38+
def _yesterday_midnight_ms() -> int:
39+
"""Return today at 00:00:00 UTC (= end of yesterday) in milliseconds.
40+
Used as a stable default end_timestamp so data files collected on the
41+
same calendar day share an identical end boundary and can be cached."""
42+
today_midnight = datetime.datetime.now(datetime.timezone.utc).replace(
43+
hour=0, minute=0, second=0, microsecond=0
44+
)
45+
return int(today_midnight.timestamp() * 1000)
46+
47+
48+
def _resolve_end_timestamp_ms(end_timestamp) -> int:
49+
if end_timestamp is None:
50+
return _yesterday_midnight_ms()
51+
return _ensure_ms_timestamp(end_timestamp)
52+
53+
54+
async def historical_data(
55+
symbol,
56+
timeframe,
57+
exchange="binance",
58+
exchange_type=trading_enums.ExchangeTypes.SPOT.value,
59+
start_timestamp=None,
60+
end_timestamp=None,
61+
tentacles_config=None,
62+
profile_id=None,
63+
):
64+
_validate_tentacles_source(tentacles_config, profile_id)
3365
symbols = [symbol]
3466
time_frames = [commons_enums.TimeFrames(timeframe)]
35-
data_collector_instance = backtesting_api.exchange_historical_data_collector_factory(
36-
exchange,
37-
trading_enums.ExchangeTypes(exchange_type),
38-
octobot_mocks.get_tentacles_config(),
39-
[commons_symbols.parse_symbol(symbol) for symbol in symbols],
67+
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
68+
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
69+
existing_file = await backtesting_api.find_matching_data_file(
70+
exchange_name=exchange,
71+
symbols=symbols,
4072
time_frames=time_frames,
41-
start_timestamp=_ensure_ms_timestamp(start_timestamp),
42-
end_timestamp=_ensure_ms_timestamp(end_timestamp)
73+
start_timestamp=start_timestamp_ms,
74+
end_timestamp=end_timestamp_ms,
75+
)
76+
if existing_file:
77+
return existing_file
78+
data_collector_instance = (
79+
backtesting_api.exchange_historical_data_collector_factory(
80+
exchange,
81+
trading_enums.ExchangeTypes(exchange_type),
82+
octobot_mocks.get_tentacles_config(
83+
tentacles_config, profile_id, activate_strategy_tentacles=False
84+
),
85+
[commons_symbols.parse_symbol(symbol) for symbol in symbols],
86+
time_frames=time_frames,
87+
start_timestamp=start_timestamp_ms,
88+
end_timestamp=end_timestamp_ms,
89+
)
90+
)
91+
return await backtesting_api.initialize_and_run_data_collector(
92+
data_collector_instance
93+
)
94+
95+
96+
async def social_historical_data(
97+
services: list[str],
98+
sources: list[str] | None = None,
99+
symbols: list[str] | None = None,
100+
start_timestamp=None,
101+
end_timestamp=None,
102+
tentacles_config=None,
103+
profile_id=None,
104+
):
105+
_validate_tentacles_source(tentacles_config, profile_id)
106+
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
107+
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
108+
existing_file = await backtesting_api.find_matching_data_file(
109+
services=services,
110+
symbols=symbols or [],
111+
start_timestamp=start_timestamp_ms,
112+
end_timestamp=end_timestamp_ms,
113+
)
114+
if existing_file:
115+
return existing_file
116+
data_collector_instance = backtesting_api.social_historical_data_collector_factory(
117+
services=services,
118+
tentacles_setup_config=octobot_mocks.get_tentacles_config(
119+
tentacles_config, profile_id, activate_strategy_tentacles=False
120+
),
121+
sources=sources,
122+
symbols=[commons_symbols.parse_symbol(symbol) for symbol in symbols]
123+
if symbols
124+
else None,
125+
start_timestamp=start_timestamp_ms,
126+
end_timestamp=end_timestamp_ms,
127+
config=octobot_mocks.get_config(),
128+
)
129+
return await backtesting_api.initialize_and_run_data_collector(
130+
data_collector_instance
131+
)
132+
133+
134+
async def get_data(
135+
symbol,
136+
time_frame,
137+
exchange="binance",
138+
exchange_type=trading_enums.ExchangeTypes.SPOT.value,
139+
start_timestamp=None,
140+
end_timestamp=None,
141+
data_file=None,
142+
social_data_files: list[str] | None = None,
143+
social_services: list[str] | None = None,
144+
social_sources: list[str] | None = None,
145+
social_symbols: list[str] | None = None,
146+
tentacles_config=None,
147+
profile_id=None,
148+
):
149+
_validate_tentacles_source(tentacles_config, profile_id)
150+
data_files = (
151+
[data_file]
152+
if data_file
153+
else [
154+
await historical_data(
155+
symbol,
156+
timeframe=time_frame,
157+
exchange=exchange,
158+
exchange_type=exchange_type,
159+
start_timestamp=start_timestamp,
160+
end_timestamp=end_timestamp,
161+
tentacles_config=tentacles_config,
162+
profile_id=profile_id,
163+
)
164+
]
43165
)
44-
return await backtesting_api.initialize_and_run_data_collector(data_collector_instance)
45166

167+
if social_data_files is not None:
168+
data_files.extend(social_data_files)
169+
elif (
170+
profile_id is not None
171+
or social_sources is not None
172+
or tentacles_config is not None
173+
):
174+
social_services = (
175+
social_services
176+
if social_services is not None
177+
else octobot_mocks.get_activated_social_services(
178+
tentacles_config, profile_id, requested_sources=social_sources
179+
)
180+
)
181+
if social_services:
182+
for service in social_services:
183+
data_files.append(
184+
await social_historical_data(
185+
[service],
186+
sources=social_sources,
187+
symbols=social_symbols,
188+
start_timestamp=start_timestamp,
189+
end_timestamp=end_timestamp,
190+
tentacles_config=tentacles_config,
191+
profile_id=profile_id,
192+
)
193+
)
46194

47-
async def get_data(symbol, time_frame, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value,
48-
start_timestamp=None, end_timestamp=None, data_file=None):
49-
data = data_file or \
50-
await historical_data(symbol, timeframe=time_frame, exchange=exchange, exchange_type=exchange_type,
51-
start_timestamp=start_timestamp, end_timestamp=end_timestamp)
52195
return await backtesting_api.create_and_init_backtest_data(
53-
[data],
196+
data_files,
54197
octobot_mocks.get_config(),
55-
octobot_mocks.get_tentacles_config(),
56-
use_accurate_price_time_frame=True
198+
octobot_mocks.get_tentacles_config(
199+
tentacles_config, profile_id, activate_strategy_tentacles=False
200+
),
201+
use_accurate_price_time_frame=True,
57202
)

octobot_script/api/execution.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,17 @@
1818
import octobot_script.internal.runners as runners
1919

2020

21-
async def run(backtesting_data, update_func, strategy_config,
22-
enable_logs=False, enable_storage=True):
21+
async def run(backtesting_data, strategy_config,
22+
enable_logs=False, enable_storage=True,
23+
strategy_func=None, initialize_func=None,
24+
tentacles_config=None, profile_id=None):
25+
if tentacles_config is not None and profile_id is not None:
26+
raise ValueError("Only one of tentacles_config or profile_id can be provided.")
2327
if enable_logs:
2428
logging_util.load_logging_config()
2529
return await runners.run(
26-
backtesting_data, update_func, strategy_config,
27-
enable_logs=enable_logs, enable_storage=enable_storage
30+
backtesting_data, strategy_config,
31+
enable_logs=enable_logs, enable_storage=enable_storage,
32+
strategy_func=strategy_func, initialize_func=initialize_func,
33+
tentacles_config=tentacles_config, profile_id=profile_id,
2834
)

octobot_script/api/ploting.py

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,33 @@
1515
# You should have received a copy of the GNU General Public
1616
# License along with OctoBot-Script. If not, see <https://www.gnu.org/licenses/>.
1717

18+
1819
async def plot_indicator(ctx, name, x, y, signals=None):
1920
# lazy import
2021
import octobot_script as obs
2122

2223
await obs.plot(ctx, name, x=list(x), y=list(y))
23-
value_by_x = {
24-
x: y
25-
for x, y in zip(x, y)
26-
}
24+
value_by_x = {x: y for x, y in zip(x, y)}
2725
if signals:
28-
await obs.plot(ctx, "signals", x=list(signals), y=[value_by_x[x] for x in signals], mode="markers")
26+
await obs.plot(
27+
ctx,
28+
"signals",
29+
x=list(signals),
30+
y=[value_by_x[x] for x in signals],
31+
mode="markers",
32+
)
33+
34+
35+
async def generate_and_show_report(res):
36+
"""Generate a backtest report and show it.
37+
38+
Args:
39+
res: BacktestResult instance
2940
41+
Returns:
42+
The generated report object
43+
"""
44+
report = await res.plot(show=False)
45+
print(f"Report generated at: {report.report_file}")
46+
report.show()
47+
return report

0 commit comments

Comments
 (0)