Skip to content

Commit 87d70fe

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

File tree

13 files changed

+770
-110
lines changed

13 files changed

+770
-110
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: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,44 +14,142 @@
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

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+
3154
async def historical_data(symbol, timeframe, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value,
32-
start_timestamp=None, end_timestamp=None):
55+
start_timestamp=None, end_timestamp=None, tentacles_config=None, profile_id=None):
56+
_validate_tentacles_source(tentacles_config, profile_id)
3357
symbols = [symbol]
3458
time_frames = [commons_enums.TimeFrames(timeframe)]
59+
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
60+
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
61+
existing_file = await backtesting_api.find_matching_data_file(
62+
exchange_name=exchange,
63+
symbols=symbols,
64+
time_frames=time_frames,
65+
start_timestamp=start_timestamp_ms,
66+
end_timestamp=end_timestamp_ms,
67+
)
68+
if existing_file:
69+
return existing_file
3570
data_collector_instance = backtesting_api.exchange_historical_data_collector_factory(
3671
exchange,
3772
trading_enums.ExchangeTypes(exchange_type),
38-
octobot_mocks.get_tentacles_config(),
73+
octobot_mocks.get_tentacles_config(tentacles_config, profile_id, activate_strategy_tentacles=False),
3974
[commons_symbols.parse_symbol(symbol) for symbol in symbols],
4075
time_frames=time_frames,
41-
start_timestamp=_ensure_ms_timestamp(start_timestamp),
42-
end_timestamp=_ensure_ms_timestamp(end_timestamp)
76+
start_timestamp=start_timestamp_ms,
77+
end_timestamp=end_timestamp_ms,
78+
)
79+
return await backtesting_api.initialize_and_run_data_collector(data_collector_instance)
80+
81+
82+
async def social_historical_data(services: list[str], sources: list[str] | None = None,
83+
symbols: list[str] | None = None, start_timestamp=None, end_timestamp=None,
84+
tentacles_config=None, profile_id=None):
85+
_validate_tentacles_source(tentacles_config, profile_id)
86+
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
87+
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
88+
existing_file = await backtesting_api.find_matching_data_file(
89+
services=services,
90+
symbols=symbols or [],
91+
start_timestamp=start_timestamp_ms,
92+
end_timestamp=end_timestamp_ms,
93+
)
94+
if existing_file:
95+
return existing_file
96+
data_collector_instance = backtesting_api.social_historical_data_collector_factory(
97+
services=services,
98+
tentacles_setup_config=octobot_mocks.get_tentacles_config(
99+
tentacles_config, profile_id, activate_strategy_tentacles=False
100+
),
101+
sources=sources,
102+
symbols=[commons_symbols.parse_symbol(symbol) for symbol in symbols] if symbols else None,
103+
start_timestamp=start_timestamp_ms,
104+
end_timestamp=end_timestamp_ms,
105+
config=octobot_mocks.get_config(),
43106
)
44107
return await backtesting_api.initialize_and_run_data_collector(data_collector_instance)
45108

46109

47110
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)
111+
start_timestamp=None, end_timestamp=None, data_file=None,
112+
social_data_files: list[str] | None = None, social_services: list[str] | None = None,
113+
social_sources: list[str] | None = None, social_symbols: list[str] | None = None,
114+
tentacles_config=None, profile_id=None):
115+
_validate_tentacles_source(tentacles_config, profile_id)
116+
data_files = [data_file] if data_file else [
117+
await historical_data(
118+
symbol,
119+
timeframe=time_frame,
120+
exchange=exchange,
121+
exchange_type=exchange_type,
122+
start_timestamp=start_timestamp,
123+
end_timestamp=end_timestamp,
124+
tentacles_config=tentacles_config,
125+
profile_id=profile_id,
126+
)
127+
]
128+
129+
if social_data_files is not None:
130+
data_files.extend(social_data_files)
131+
elif profile_id is not None:
132+
social_services = social_services if social_services is not None \
133+
else octobot_mocks.get_activated_social_services(
134+
tentacles_config, profile_id, requested_sources=social_sources
135+
)
136+
if social_services:
137+
for service in social_services:
138+
data_files.append(
139+
await social_historical_data(
140+
[service],
141+
sources=social_sources,
142+
symbols=social_symbols,
143+
start_timestamp=start_timestamp,
144+
end_timestamp=end_timestamp,
145+
tentacles_config=tentacles_config,
146+
profile_id=profile_id,
147+
)
148+
)
149+
52150
return await backtesting_api.create_and_init_backtest_data(
53-
[data],
151+
data_files,
54152
octobot_mocks.get_config(),
55-
octobot_mocks.get_tentacles_config(),
153+
octobot_mocks.get_tentacles_config(tentacles_config, profile_id, activate_strategy_tentacles=False),
56154
use_accurate_price_time_frame=True
57155
)

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)