Skip to content
Draft
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ wheels/
.installed.cfg
*.egg

node_modules

# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
Expand Down Expand Up @@ -136,6 +138,7 @@ cython_debug/
.env

*.zip
*.db

# tensorboard
tensorboard_logs
2 changes: 2 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
recursive-include octobot_script/config *.json *.ini
recursive-include octobot_script/resources *.js *.css *.html
recursive-exclude octobot_script/resources/report/node_modules *
recursive-exclude octobot_script/resources/report/src *

include README.md
include LICENSE
Expand Down
42 changes: 42 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.8/schema.json",
"assist": { "actions": { "source": { "organizeImports": "on" } } },
"files": {
"includes": [
"**",
"!**/dist/**/*",
"!**/node_modules/**/*",
"!**/src/components/ui/**/*"
]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off",
"noArrayIndexKey": "off"
},
"style": {
"noNonNullAssertion": "off",
"noParameterAssign": "error",
"useSelfClosingElements": "error",
"noUselessElse": "error"
}
}
},
"formatter": {
"indentStyle": "space"
},
"javascript": {
"formatter": {
"quoteStyle": "double",
"semicolons": "asNeeded"
}
},
"css": {
"parser": {
"tailwindDirectives": true
}
}
}
23 changes: 23 additions & 0 deletions components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "octobot_script/resources/report/src/index.css",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
45 changes: 23 additions & 22 deletions example.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,28 @@
import octobot_script as obs


async def rsi_test():
async def example():
async def initialize(ctx):
# Compute entries only once per backtest.
closes = await obs.Close(ctx, max_history=True)
times = await obs.Time(ctx, max_history=True, use_close_time=True)
rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"])
delta = len(closes) - len(rsi_v)
# Populate entries with timestamps of candles where RSI is
# bellow the "rsi_value_buy_threshold" configuration.
run_data["entries"] = {
times[index + delta]
for index, rsi_val in enumerate(rsi_v)
if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"]
}
await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"])

async def strategy(ctx):
# Will be called at each candle.
if run_data["entries"] is None:
# Compute entries only once per backtest.
closes = await obs.Close(ctx, max_history=True)
times = await obs.Time(ctx, max_history=True, use_close_time=True)
rsi_v = tulipy.rsi(closes, period=ctx.tentacle.trading_config["period"])
delta = len(closes) - len(rsi_v)
# Populate entries with timestamps of candles where RSI is
# bellow the "rsi_value_buy_threshold" configuration.
run_data["entries"] = {
times[index + delta]
for index, rsi_val in enumerate(rsi_v)
if rsi_val < ctx.tentacle.trading_config["rsi_value_buy_threshold"]
}
await obs.plot_indicator(ctx, "RSI", times[delta:], rsi_v, run_data["entries"])
# Called at each candle.
# Uses pre-computed entries times to enter positions when relevant.
# Also, instantly set take profits and stop losses.
# Position exits could also be set separately.
if obs.current_live_time(ctx) in run_data["entries"]:
# Uses pre-computed entries times to enter positions when relevant.
# Also, instantly set take profits and stop losses.
# Position exists could also be set separately.
await obs.market(ctx, "buy", amount="10%", stop_loss_offset="-15%", take_profit_offset="25%")

# Configuration that will be passed to each run.
Expand All @@ -34,12 +35,12 @@ async def strategy(ctx):
}

# Read and cache candle data to make subsequent backtesting runs faster.
data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400)
data = await obs.get_data("BTC/USDT", "1d", start_timestamp=1505606400, social_services=[])
run_data = {
"entries": None,
}
# Run a backtest using the above data, strategy and configuration.
res = await obs.run(data, strategy, config)
res = await obs.run(data, config, initialize_func=initialize, strategy_func=strategy)
print(res.describe())
# Generate and open report including indicators plots
await res.plot(show=True)
Expand All @@ -49,4 +50,4 @@ async def strategy(ctx):

# Call the execution of the script inside "asyncio.run" as
# OctoBot-Script runs using the python asyncio framework.
asyncio.run(rsi_test())
asyncio.run(example())
118 changes: 108 additions & 10 deletions octobot_script/api/data_fetching.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,44 +14,142 @@
# You should have received a copy of the GNU General Public
# License along with OctoBot-Script. If not, see <https://www.gnu.org/licenses/>.

import datetime

import octobot_backtesting.api as backtesting_api
import octobot_commons.symbols as commons_symbols
import octobot_commons.enums as commons_enums
import octobot_trading.enums as trading_enums
import octobot_script.internal.octobot_mocks as octobot_mocks


def _validate_tentacles_source(tentacles_config, profile_id):
if tentacles_config is not None and profile_id is not None:
raise ValueError("Only one of tentacles_config or profile_id can be provided.")


def _ensure_ms_timestamp(timestamp):
if timestamp is None:
return timestamp
if timestamp < 16737955050: # Friday 28 May 2500 07:57:30
return timestamp * 1000


def _yesterday_midnight_ms() -> int:
"""Return today at 00:00:00 UTC (= end of yesterday) in milliseconds.
Used as a stable default end_timestamp so data files collected on the
same calendar day share an identical end boundary and can be cached."""
today_midnight = datetime.datetime.now(datetime.timezone.utc).replace(
hour=0, minute=0, second=0, microsecond=0
)
return int(today_midnight.timestamp() * 1000)


def _resolve_end_timestamp_ms(end_timestamp) -> int:
if end_timestamp is None:
return _yesterday_midnight_ms()
return _ensure_ms_timestamp(end_timestamp)


async def historical_data(symbol, timeframe, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value,
start_timestamp=None, end_timestamp=None):
start_timestamp=None, end_timestamp=None, tentacles_config=None, profile_id=None):
_validate_tentacles_source(tentacles_config, profile_id)
symbols = [symbol]
time_frames = [commons_enums.TimeFrames(timeframe)]
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
existing_file = await backtesting_api.find_matching_data_file(
exchange_name=exchange,
symbols=symbols,
time_frames=time_frames,
start_timestamp=start_timestamp_ms,
end_timestamp=end_timestamp_ms,
)
if existing_file:
return existing_file
data_collector_instance = backtesting_api.exchange_historical_data_collector_factory(
exchange,
trading_enums.ExchangeTypes(exchange_type),
octobot_mocks.get_tentacles_config(),
octobot_mocks.get_tentacles_config(tentacles_config, profile_id, activate_strategy_tentacles=False),
[commons_symbols.parse_symbol(symbol) for symbol in symbols],
time_frames=time_frames,
start_timestamp=_ensure_ms_timestamp(start_timestamp),
end_timestamp=_ensure_ms_timestamp(end_timestamp)
start_timestamp=start_timestamp_ms,
end_timestamp=end_timestamp_ms,
)
return await backtesting_api.initialize_and_run_data_collector(data_collector_instance)


async def social_historical_data(services: list[str], sources: list[str] | None = None,
symbols: list[str] | None = None, start_timestamp=None, end_timestamp=None,
tentacles_config=None, profile_id=None):
_validate_tentacles_source(tentacles_config, profile_id)
start_timestamp_ms = _ensure_ms_timestamp(start_timestamp)
end_timestamp_ms = _resolve_end_timestamp_ms(end_timestamp)
existing_file = await backtesting_api.find_matching_data_file(
services=services,
symbols=symbols or [],
start_timestamp=start_timestamp_ms,
end_timestamp=end_timestamp_ms,
)
if existing_file:
return existing_file
data_collector_instance = backtesting_api.social_historical_data_collector_factory(
services=services,
tentacles_setup_config=octobot_mocks.get_tentacles_config(
tentacles_config, profile_id, activate_strategy_tentacles=False
),
sources=sources,
symbols=[commons_symbols.parse_symbol(symbol) for symbol in symbols] if symbols else None,
start_timestamp=start_timestamp_ms,
end_timestamp=end_timestamp_ms,
config=octobot_mocks.get_config(),
)
return await backtesting_api.initialize_and_run_data_collector(data_collector_instance)


async def get_data(symbol, time_frame, exchange="binance", exchange_type=trading_enums.ExchangeTypes.SPOT.value,
start_timestamp=None, end_timestamp=None, data_file=None):
data = data_file or \
await historical_data(symbol, timeframe=time_frame, exchange=exchange, exchange_type=exchange_type,
start_timestamp=start_timestamp, end_timestamp=end_timestamp)
start_timestamp=None, end_timestamp=None, data_file=None,
social_data_files: list[str] | None = None, social_services: list[str] | None = None,
social_sources: list[str] | None = None, social_symbols: list[str] | None = None,
tentacles_config=None, profile_id=None):
_validate_tentacles_source(tentacles_config, profile_id)
data_files = [data_file] if data_file else [
await historical_data(
symbol,
timeframe=time_frame,
exchange=exchange,
exchange_type=exchange_type,
start_timestamp=start_timestamp,
end_timestamp=end_timestamp,
tentacles_config=tentacles_config,
profile_id=profile_id,
)
]

if social_data_files is not None:
data_files.extend(social_data_files)
elif profile_id is not None:
social_services = social_services if social_services is not None \
else octobot_mocks.get_activated_social_services(
tentacles_config, profile_id, requested_sources=social_sources
)
if social_services:
for service in social_services:
data_files.append(
await social_historical_data(
[service],
sources=social_sources,
symbols=social_symbols,
start_timestamp=start_timestamp,
end_timestamp=end_timestamp,
tentacles_config=tentacles_config,
profile_id=profile_id,
)
)

return await backtesting_api.create_and_init_backtest_data(
[data],
data_files,
octobot_mocks.get_config(),
octobot_mocks.get_tentacles_config(),
octobot_mocks.get_tentacles_config(tentacles_config, profile_id, activate_strategy_tentacles=False),
use_accurate_price_time_frame=True
)
14 changes: 10 additions & 4 deletions octobot_script/api/execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,17 @@
import octobot_script.internal.runners as runners


async def run(backtesting_data, update_func, strategy_config,
enable_logs=False, enable_storage=True):
async def run(backtesting_data, strategy_config,
enable_logs=False, enable_storage=True,
strategy_func=None, initialize_func=None,
tentacles_config=None, profile_id=None):
if tentacles_config is not None and profile_id is not None:
raise ValueError("Only one of tentacles_config or profile_id can be provided.")
if enable_logs:
logging_util.load_logging_config()
return await runners.run(
backtesting_data, update_func, strategy_config,
enable_logs=enable_logs, enable_storage=enable_storage
backtesting_data, strategy_config,
enable_logs=enable_logs, enable_storage=enable_storage,
strategy_func=strategy_func, initialize_func=initialize_func,
tentacles_config=tentacles_config, profile_id=profile_id,
)
Loading
Loading