Welcome to Botmarley

Botmarley is a self-hosted, sovereign cryptocurrency trading bot built in Rust. It runs entirely on your own hardware, giving you full control over your trading strategies, API keys, and market data. No cloud services, no third-party access to your funds, no monthly subscriptions.

You install it, you run it, you own it.

Botmarley dashboard showing active sessions, portfolio summary, and quick navigation

Info

Botmarley supports Kraken and Binance exchanges. It automatically detects which exchange to use based on your account type and trading pair.

What Botmarley Does

At its core, Botmarley automates cryptocurrency trading based on rules you define. You write a strategy that says things like "buy BTC when RSI drops below 30" or "sell 50% of my position when price rises 5% from entry," and Botmarley executes those rules 24/7 without you needing to watch charts.

Here is how the pieces fit together:

graph LR
    Browser["Browser<br/>(Web Interface)"]
    Bot["Botmarley<br/>(Trading Bot)"]
    PG["PostgreSQL<br/>(Your Data)"]
    Exchange["Exchange API<br/>(Kraken / Binance)"]

    Browser <-->|"http://localhost:3000"| Bot
    Bot <-->|"State & Logs"| PG
    Bot <-->|"Market Data & Orders"| Exchange
  • Browser -- You interact with Botmarley through a web interface at http://localhost:3000. Pages update in real time as your bot trades.
  • Botmarley -- A blazing-fast trading engine built in Rust with Tokio and Axum. Handles strategy execution, order placement, data fetching, and serves a reactive web interface powered by HTMX.
  • PostgreSQL -- Stores your accounts, trading sessions, backtest results, PnL history, activity logs, and configuration.
  • Exchange API -- Botmarley connects to Kraken and Binance to fetch market data and place orders on your behalf.

Key Features

FeatureDescription
DashboardAt-a-glance overview of active sessions, total PnL, and quick navigation to key areas.
Strategy EditorVisual builder and raw TOML editor for defining trading rules. Real-time validation as you build.
BacktestingTest strategies against historical data before risking real money. See PnL, trade count, win rate, and individual actions on a chart.
Live TradingExecute strategies in real-time on Kraken and Binance. Start, pause, resume, and stop sessions. Live-updating charts with trade markers.
AccountsManage exchange API keys and paper (simulated) accounts. Each account is isolated.
PortfolioTrack total portfolio value over time in USD and BTC, with asset breakdowns per account.
Market DataDownload, browse, chart, and analyze historical candle data. Run indicators on stored data.
History SyncFetch historical 1-minute candles from your exchange. Derived timeframes (5m, 15m, 1h) are calculated locally.
Task QueueBackground job system for long-running operations like data sync and bulk backtests.
Activity LogsDetailed log of every action the bot takes, filterable by category (strategy, trading, backtest, account, data, system).
SettingsConfigure server host/port, storage paths, active trading pairs, Telegram notifications, password protection, and UI themes.

Who This Manual Is For

This manual is for cryptocurrency traders -- both beginners exploring automated trading for the first time and experienced traders who want full control over their bot.

  • If you are new to trading bots, start with the Getting Started section. It walks you through installation, your first run, and a tour of every page.
  • If you want to jump straight into building strategies, head to Strategies.
  • If you are deploying Botmarley on a server, see Deployment.

Tip

You do not need any programming knowledge to use Botmarley. Strategies are defined in TOML files (a simple configuration format), and everything else is done through the web interface.

How to Use This Manual

The sidebar on the left organizes content into sections:

  • Getting Started -- Installation, first run walkthrough, and a tour of the interface.
  • Features -- Deep dives into each major feature: Dashboard, Accounts, Strategies, Backtesting, Live Trading, and Market Data.
  • System -- Settings, task queue, activity logs, authentication, and Telegram integration.
  • Deployment -- Running Botmarley locally and in production environments.
  • Reference -- Strategy TOML specification, indicator reference, and glossary.

Use the search bar at the top to find specific topics. Each page is self-contained, so you can read them in any order.

Installation

This page walks you through installing Botmarley on your machine. By the end, you will have the bot running and accessible in your browser.

What You Need Before Starting

Botmarley is distributed as a pre-built binary. The only external dependency is PostgreSQL for data storage.

PrerequisiteWhyHow to check
PostgreSQL 15+Stores all bot state, trades, and logs.psql --version

The easiest way to run PostgreSQL is with Docker:

OptionalWhyHow to check
DockerRun PostgreSQL in a container (no manual install).docker --version
Docker ComposeOrchestrates the PostgreSQL container.docker compose version

Tip

If you already have PostgreSQL 15+ running on your system, you can use that instead of Docker. See the Environment Variables page for how to configure a custom database connection.

Installing Docker

If you do not have Docker installed:

macOS:

brew install --cask docker

Ubuntu/Debian Linux:

sudo apt update
sudo apt install docker.io docker-compose-v2

Windows:

Download and install Docker Desktop for Windows.

Download Botmarley

Download the latest release for your platform:

Download Latest Release

ArchivePlatform
botmarley-arm64.tar.gzLinux ARM64 — AWS Graviton, Raspberry Pi
botmarley-amd64.tar.gzLinux x86_64 — Most servers, Intel/AMD desktops
botmarley-macos-arm64.tar.gzmacOS Apple Silicon (M1/M2/M3/M4)
botmarley-windows-amd64.tar.gzWindows x64

Note

Not sure which one you need?

  • Linux: Run uname -m. aarch64 → ARM64, x86_64 → AMD64
  • macOS: Use the Apple Silicon build (all modern Macs since late 2020)
  • Windows: Use the Windows x64 build

Extract the Archive

Linux / macOS:

# Replace with your downloaded archive name
tar -xzf botmarley-arm64.tar.gz
cd botmarley

Windows (PowerShell):

tar -xzf botmarley-windows-amd64.tar.gz
cd botmarley

This creates the following files:

PathPurpose
server (or server.exe on Windows)The Botmarley binary
templates/Web interface templates
static/CSS, JavaScript, and images

Initialize Botmarley

Before running for the first time, initialize the data directory:

Linux / macOS:

./server init

Windows (PowerShell):

.\server.exe init

This creates ~/.botmarley/ (or %USERPROFILE%\.botmarley\ on Windows) with:

  • Default settings.toml configuration
  • Built-in trading strategies
  • Example strategies for learning
  • Data directories for market history

Note

You only need to run init once. If the data directory already exists, the command will skip files that are already present and only add missing ones.

Start PostgreSQL

Botmarley stores all its state in PostgreSQL. The quickest way to get started is with Docker:

docker run -d \
  --name botmarley-db \
  -e POSTGRES_USER=botmarley \
  -e POSTGRES_PASSWORD=botmarley_dev \
  -e POSTGRES_DB=botmarley \
  -p 5432:5432 \
  postgres:17

Tip

If you prefer Docker Compose, create a docker-compose.yml with a postgres service, or use the one from the Production Deployment guide.

Verify the database is running:

docker ps | grep botmarley-db

Run Botmarley

Linux / macOS:

./server

Windows (PowerShell):

.\server.exe

Botmarley initializes the database schema on first run and starts listening on port 3000.

Verify the Installation

Open your browser and navigate to:

http://localhost:3000

You will see the license setup page. Follow the instructions in First Run to configure your API key and start trading.

Danger

If you see a connection error, check that:

  1. PostgreSQL is running: docker ps
  2. The server started without errors in the terminal
  3. Port 3000 is not already in use by another application

Stopping Botmarley

To stop the server, press Ctrl+C in the terminal where it is running.

To stop PostgreSQL:

docker stop botmarley-db

Tip

Your PostgreSQL data persists inside the Docker volume. Running docker stop keeps your data. To wipe the database and start fresh, run docker rm botmarley-db and create a new container.

Next Steps

With Botmarley running, continue to First Run for a guided walkthrough of the initial setup — including license activation and exchange configuration.

First Run

You have Botmarley installed and running at http://localhost:3000. This page walks you through the essential first-time setup: configuring settings, adding an account, downloading market data, creating a strategy, and running your first backtest.

By the end, you will have tested a strategy against historical data without risking any real money.

graph LR
    A["1. Settings"] --> B["2. Add Account"]
    B --> C["3. Download Data"]
    C --> D["4. Create Strategy"]
    D --> E["5. Run Backtest"]

When you open http://localhost:3000 for the first time, you will see the main dashboard. This is your starting point for everything that follows.

Botmarley dashboard after first launch

License Setup

Botmarley requires an API key to unlock all features.

  1. Visit lipinski.work to request your API key
  2. Open Settings in the Botmarley web interface
  3. Enter your API key in the License section
  4. Click Save

Botmarley validates your key immediately. Once validated, all features become available.

Without a valid key, only the Settings page is accessible.

Step 1: Configure Settings

Navigate to Settings in the sidebar (or go to http://localhost:3000/settings).

The Settings page has several sections. For your first run, focus on these:

Trading Pairs

In the Trading card, set your Active Pairs. These are the cryptocurrency pairs you want to trade and download data for. Enter them comma-separated in the format BASE/QUOTE:

BTC/USDC, ETH/USDC

Tip

Start with one or two pairs. You can always add more later. BTC/USDC is a good starting point because it has the most historical data and liquidity.

Data Storage

The Storage Path tells Botmarley where to save Arrow files (historical candle data). The default path works for most setups. If you want to store data on a different drive, change it here.

History Start Date

Set History Start Date to control how far back Botmarley fetches historical data. A reasonable starting point is 6-12 months ago. The more history you have, the more thorough your backtests will be, but downloading takes longer.

Save

Click Save Settings at the bottom. A green success banner confirms your settings were saved.

Note

You can skip the Telegram Bot and Authentication sections for now. They are useful for production deployments but not required for getting started.

Step 2: Add Your First Account

Navigate to Accounts in the sidebar.

Click the Add Account button. A modal dialog appears with these fields:

FieldWhat to enter
NameA friendly name, e.g., "Paper Trading"
DescriptionOptional, e.g., "For testing strategies"
Account TypeSelect Paper (Simulated)

A paper account simulates trades without connecting to any exchange. No API keys are needed. This is the safest way to learn how Botmarley works.

Click Create to save the account.

Warning

Do not use a live exchange account with real API keys until you are comfortable with how strategies behave. Always test with a paper account first.

When You Are Ready for Live Trading

When you eventually want to trade with real funds, create a new account with:

  • Account Type: Kraken or Binance
  • API Key and API Secret: Generated from your exchange account (see Managing Accounts for step-by-step instructions for each exchange)

Danger

Your API keys are stored locally in Botmarley's PostgreSQL database. Never share your API secret. When generating keys on your exchange, grant only the permissions you need (query funds, create orders) and avoid granting withdrawal permissions.

Step 3: Download Market Data

Navigate to History in the sidebar.

Before you can backtest, Botmarley needs historical candle data. The History Sync page lets you download it from Kraken or Binance.

  1. Select an exchange (Kraken or Binance) in the dropdown.
  2. Your active pairs (configured in Settings) appear as checkboxes. Make sure the pairs you want are checked.
  3. Click Start Sync.

Botmarley downloads 1-minute candles from the selected exchange and stores them in Apache Arrow format. This runs as a background task -- you can watch progress on the same page, which auto-refreshes every 2 seconds.

Note

The initial download can take several minutes depending on how far back your history start date is. Subsequent syncs are incremental -- they only fetch new candles since the last sync.

Once the sync completes, navigate to Data in the sidebar to verify. You should see entries in the table showing your pairs, the number of records, date range, and file size.

Step 4: Create Your First Strategy

Navigate to Strategies in the sidebar and click + New Strategy.

The Strategy Editor opens with two modes:

  • Visual (default) -- A form-based builder with dropdowns and input fields.
  • TOML -- A raw text editor for the strategy configuration file.

For your first strategy, use the Visual mode. Here is a simple RSI-based strategy to start with:

Strategy Info

  • Name: RSI Bounce
  • Description: Buy when RSI is oversold, sell when overbought
  • Max Open Positions: 1

Action 1: Open Long

Click + Add Action, then configure:

  • Action Type: Open Long
  • Amount: 100 USDC

Now add a trigger: Click + Trigger within this action.

  • Type: Technical Indicator
  • Indicator: RSI 14
  • Operator: Less Than (<)
  • Target: 30
  • Timeframe: 1h

This means: "Open a long position worth 100 USDC when the 1-hour RSI(14) drops below 30."

Action 2: Sell

Click + Add Action again:

  • Action Type: Sell
  • Amount: 100%

Add a trigger:

  • Type: Technical Indicator
  • Indicator: RSI 14
  • Operator: Greater Than (>)
  • Target: 70
  • Timeframe: 1h

This means: "Sell the entire position when the 1-hour RSI(14) rises above 70."

Validate and Save

As you build, the right panel shows a live TOML Preview of your strategy and a validation status. It should show a green "Valid strategy: 2 action(s)" banner.

The generated TOML looks like this:

[meta]
name = "RSI Bounce"
description = "Buy when RSI is oversold, sell when overbought"
max_open_positions = 1

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "70"
  timeframe = "1h"

Click Save Strategy to store it.

Step 5: Run Your First Backtest

Go back to the Strategies list. You should see "RSI Bounce" in the table. Click the Backtest button on its row.

A modal dialog appears asking for:

FieldWhat to enter
PairSelect a pair you downloaded data for (e.g., BTC/USDC)
Initial Capital10000 (USD)
Start DateThe start of your downloaded data range
End DateThe end of your downloaded data range

Click Run Backtest. Botmarley runs the strategy against the historical data and redirects you to the Backtests page.

Find your backtest in the list and click View to see the results:

  • PnL -- Total profit or loss in dollars and as a percentage.
  • Trade count -- How many buy/sell actions were executed.
  • Win rate -- Percentage of profitable trades.
  • Chart -- A candlestick chart with trade markers showing where each action occurred.
  • Actions table -- A detailed log of every trade with timestamps, prices, and amounts.

Tip

Your first strategy probably will not be profitable -- and that is perfectly fine. Backtesting is about learning and iterating. Go back to the Strategy Editor, adjust the RSI thresholds or add additional triggers, and run another backtest. Repeat until you find settings that produce results you are comfortable with.

What to Do Next

You now understand the core workflow: settings, accounts, data, strategies, backtesting. From here:

  • Quick Tour -- Walk through every page in the interface.
  • Strategies -- Learn about all the trigger types, indicators, and strategy patterns.
  • Backtesting -- Understand backtest results in depth and run bulk backtests.
  • Live Trading -- When you are ready, start a live session with a paper, Kraken, or Binance account.

Quick Tour

This quick tour walks you through every major section of Botmarley so you know where everything lives.

The sidebar on the left provides access to all sections:

SectionWhat It Does
DashboardPortfolio overview and active session status
AccountsManage exchange connections
PortfolioTrack total portfolio value over time
StrategiesCreate and edit trading strategies
BacktestingTest strategies against historical data
TradingRun live trading sessions
DataDownload and browse market data
TasksView background job queue
LogsActivity and event history
SettingsApplication configuration

Dashboard

The dashboard (/) shows a summary of your portfolio value and any active trading sessions. It's your home base for monitoring at a glance.

Dashboard showing stats cards for Active Sessions, Total Sessions, and Total PnL alongside Quick Actions

Accounts

Navigate to Accounts to connect your Kraken or Binance exchange account or create a Paper (simulated) account for testing. Each account stores API credentials securely and can be verified with a balance check.

Tip

Start with a Paper account to test strategies before risking real funds.

Strategies

The Strategies section lets you create, edit, and manage trading strategies defined in TOML format. Each strategy consists of:

  • Actions — buy/sell operations, each with their own trigger conditions
  • Indicators — technical analysis tools (SMA, EMA, RSI, Bollinger Bands, MACD, OBV, OBV-SMA, TTM Trend, StochRSI, ROC, ATR, Vol SMA, Price)
  • Position management — DCA settings, max open positions, allocation percentages

Backtesting

Before going live, test your strategies against historical data in the Backtesting section. You'll see:

  • Profit/Loss (absolute and percentage)
  • Win rate and trade count
  • Maximum drawdown
  • An interactive chart showing entries and exits

Live Trading

When you're confident in a strategy, start a Trading Session. Select an account, a strategy, and a trading pair, then hit Start. You can:

  • Pause — temporarily halt execution
  • Resume — continue from where you paused
  • Stop — end the session permanently

Real-time updates stream via SSE (Server-Sent Events), so the page updates automatically.

Market Data

The Data section lets you download historical candle data from Kraken or Binance. Data is stored in Apache Arrow format for fast access. You can browse downloaded datasets and run indicator calculations.

Settings

Configure application-wide settings including:

  • Server host and port
  • Telegram bot notifications
  • Password protection
  • Data storage paths

Note

Changes to server settings require a restart to take effect.

What's Next?

Now that you know the layout, dive into the specific sections that interest you most. We recommend starting with Accounts to set up your first connection, then exploring Strategies to build your first trading strategy.

Dashboard

The Dashboard is your home page — the first thing you see when you open Botmarley at http://localhost:3000.

What You'll See

The dashboard provides a high-level overview of your trading operation:

Botmarley dashboard with the Bot Marley logo, stats cards for Active Sessions, Total Sessions, and Total PnL, and a Quick Actions section

Portfolio Summary

If you have accounts connected and portfolio sync running, you'll see:

  • Total Portfolio Value (USD) — combined value across all accounts
  • Total Portfolio Value (BTC) — your holdings measured in Bitcoin
  • BTC Price — current Bitcoin price in USD
  • Last Sync — when portfolio data was last refreshed

Active Trading Sessions

Any currently running or paused trading sessions appear here with:

  • Session name and trading pair
  • Current status (Running, Paused)
  • Profit/Loss so far
  • Number of actions executed

Tip

Click on any active session to jump directly to its detail page with real-time charts and controls.

Empty State

If this is your first time using Botmarley, the dashboard will show helpful links to get started:

  1. Add an Account — connect to Kraken or create a Paper account
  2. Create a Strategy — build your first trading strategy
  3. Download Data — fetch historical market data for backtesting

Auto-Refresh

The dashboard uses HTMX polling to stay up-to-date. Active session stats refresh automatically without needing to reload the page.

From the dashboard, you can quickly navigate to any section using the sidebar. The most common workflows from here:

graph LR
    D[Dashboard] --> A[Accounts]
    D --> S[Strategies]
    D --> T[Trading]
    D --> B[Backtesting]
    A --> T
    S --> B
    B --> T

This flow represents the typical journey: set up accounts, create strategies, backtest them, then go live.

Accounts

Accounts are the bridge between Botmarley and the outside world. Every action the bot takes -- whether it is placing a live trade on an exchange or simulating one locally -- happens through an account. Before you can backtest a strategy, run a live session, or track a portfolio, you need at least one account configured.

Accounts list showing account names, types, and verification status badges

What Is an Account?

An account in Botmarley represents a single source of funds. It stores:

  • Name and description -- a human-readable label you choose (e.g. "My Kraken Spot" or "Paper Testing").
  • Account type -- determines whether the account connects to a real exchange or runs in simulation.
  • API credentials -- only required for real exchange accounts.
  • Verification status -- tells you whether Botmarley has successfully tested the credentials.
  • Assets -- the token balances held in that account (BTC, ETH, USD, etc.).

You can have as many accounts as you like. A common setup is one Paper account for testing and one Kraken account for live trading.

Account Types

Botmarley supports three account types. Each behaves differently with respect to credentials, verification, and trading.

Paper (Simulated)

PropertyValue
Requires credentialsNo
Connects to exchangeNo
Verification statusAlways "N/A"

Paper accounts are purely local. No money leaves your machine, and no API calls are made to any exchange. Paper accounts are ideal for:

  • Learning how Botmarley works without financial risk.
  • Testing a new strategy before going live.
  • Running backtests against historical data.

When you create a Paper account, Botmarley assigns it a starting balance that you can adjust manually. Trades executed against a Paper account update its local balances but never touch a real order book.

Tip

Start with a Paper account. You can always add a real exchange account later once you are comfortable with how your strategies behave.

Kraken (Real Exchange)

PropertyValue
Requires credentialsYes (API key + secret)
Connects to exchangeYes -- Kraken REST + WebSocket APIs
Verification statusMust be verified before use

Kraken accounts connect Botmarley to your real Kraken exchange account using API credentials that you generate on Kraken's website. Once verified, Botmarley can:

  • Sync balances -- pull your current holdings from Kraken.
  • Place orders -- execute buy/sell orders during live trading sessions.
  • Fetch market data -- download candle history via the Kraken REST API.

You control what Botmarley is allowed to do by setting the appropriate API key permissions on Kraken (see the Managing Accounts chapter for details).

Warning

A Kraken account operates with real money. Double-check your strategy in Paper mode before pointing it at a Kraken account.

Binance

PropertyValue
Requires credentialsYes (API key + secret)
Connects to exchangeYes
VerificationHMAC-SHA256 signed request to /api/v3/account

A Binance account connects to the Binance exchange using your API key and secret. Botmarley supports:

  • Verify credentials -- test the connection with a signed request.
  • Sync balances -- pull your current holdings from Binance.
  • Place orders -- execute spot market orders (BUY with quoteOrderQty, SELL with quantity).
  • Fetch market data -- download candle history via the Binance REST API.

Botmarley automatically applies LOT_SIZE and MARKET_LOT_SIZE filters from Binance exchangeInfo to ensure order quantities are valid.

Warning

A Binance account operates with real money. Double-check your strategy in Paper mode before pointing it at a Binance account.

Verification States

Every account has a verification status that reflects whether Botmarley has successfully tested the connection to the exchange.

StatusBadge ColorMeaning
UnverifiedYellowCredentials have been entered but not yet tested.
VerifiedGreenBotmarley connected to the exchange and confirmed the credentials work.
FailedRedThe last verification attempt failed (bad key, wrong permissions, network error).
N/AGrayVerification does not apply to this account type (Paper accounts).

Verification is performed by making a signed API request to the exchange's private balance endpoint. If the exchange responds without errors, the credentials are marked Verified. If the exchange returns an authentication error or the request fails, the status is set to Failed.

Tip

If verification fails, check that your API key has not expired and that you copied both the key and the secret correctly. Kraken secrets are base64-encoded strings -- make sure you copied the entire value without extra whitespace.

When to Use Paper vs. Real Accounts

flowchart TD
    A["New strategy idea"] --> B{"Tested in backtesting?"}
    B -- No --> C["Run backtest with Paper account"]
    C --> B
    B -- Yes --> D{"Happy with results?"}
    D -- No --> E["Adjust strategy parameters"]
    E --> C
    D -- Yes --> F{"Ready for real money?"}
    F -- No --> G["Run live Paper session\n(simulated orders)"]
    G --> F
    F -- Yes --> H["Switch to Kraken account\nfor live trading"]

The recommended workflow:

  1. Backtest first -- use a Paper account and historical data to validate your strategy logic.
  2. Paper-trade live -- run the strategy in live mode against a Paper account. This exercises the real-time data feed without risking funds.
  3. Go live -- once confident, create or select a verified Kraken account and start a live trading session.

This progression lets you catch problems early, before any real capital is at stake.

Managing Accounts

This chapter walks through every account management operation: creating, verifying, syncing, editing, and deleting accounts. All of these actions are performed from the Accounts page at /accounts.

Creating a New Account

The Accounts page lists all your configured accounts with their type, verification status, and available actions.

Accounts page showing the account list with verification badges and action buttons

  1. Navigate to the Accounts page using the sidebar or by visiting /accounts.
  2. Click the Add Account button in the top-right corner. A modal dialog opens.
  3. Fill in the form fields:
FieldRequiredDescription
NameYesA label for the account (e.g. "Kraken Main", "Paper Test").
DescriptionNoOptional notes for your own reference.
Account TypeYesChoose Paper, Kraken, or Binance from the dropdown.
API KeyKraken/Binance onlyYour exchange API key. Hidden when Paper is selected.
API SecretKraken/Binance onlyYour exchange API secret. Hidden when Paper is selected.
  1. Click Create.

Botmarley redirects you back to the accounts list. Your new account appears in the table with a verification status of Unverified (for exchange accounts) or N/A (for Paper accounts).

Tip

For Paper accounts, you can skip the API fields entirely. Just give it a name, select "Paper (Simulated)", and click Create.

Getting Exchange API Keys

To connect an exchange account, you need an API key pair generated from the exchange's website.

Kraken API Keys

  1. Log in to your Kraken account at https://www.kraken.com.
  2. Navigate to Settings (gear icon) then API.
  3. Click Create API Key (or Generate New Key).
  4. Give the key a descriptive name (e.g. "Botmarley Trading Bot").
  5. Set the permissions (see below).
  6. Click Generate Key.
  7. Copy both the API Key and the Private Key (secret) immediately. Kraken only shows the secret once. If you lose it, you must generate a new key pair.
  8. Paste the key and secret into Botmarley's account creation form.

Copy the secret now

Kraken displays the API secret (Private Key) only at the moment of creation. There is no way to retrieve it later. If you navigate away before copying it, you will need to delete the key and generate a new one.

Binance API Keys

  1. Log in to your Binance account at https://www.binance.com.
  2. Navigate to Account then API Management.
  3. Click Create API and give the key a label (e.g. "Botmarley Trading Bot").
  4. Complete any required security verification (2FA).
  5. Copy both the API Key and the Secret Key immediately. Binance only shows the secret at creation time.
  6. Paste the key and secret into Botmarley's account creation form.

Copy the secret now

Binance displays the API Secret Key only at the moment of creation. If you close the dialog before copying it, you will need to delete the key and generate a new one.

Setting API Key Permissions

Both Kraken and Binance let you control what each API key can do. The permissions you need depend on how you plan to use Botmarley.

Kraken Permissions

Read-only (monitoring and portfolio tracking)

If you only want to sync balances and track your portfolio, enable:

  • Query Funds -- allows Botmarley to read your account balances.
  • Query Open Orders & Trades -- allows Botmarley to read trade history.

This is the safest option. The key cannot place or cancel orders.

Trading (live bot execution)

If you want Botmarley to place orders on your behalf during live trading sessions, you also need:

  • Create & Modify Orders -- allows Botmarley to submit new orders.
  • Cancel/Close Orders -- allows Botmarley to cancel open orders.

Permissions to avoid

Unless you have a specific reason, do not enable:

  • Withdraw Funds -- Botmarley never needs withdrawal access.
  • Access WebSockets API -- not currently required for Botmarley's Kraken integration (REST is used).

Binance Permissions

Read-only (monitoring and portfolio tracking)

  • Enable Reading -- allows Botmarley to read your account balances and trade history.

Trading (live bot execution)

  • Enable Spot & Margin Trading -- allows Botmarley to place spot market orders.

Permissions to avoid

  • Enable Withdrawals -- Botmarley never needs withdrawal access.
  • Enable Futures -- Botmarley currently supports spot trading only.

Principle of least privilege

Only grant the permissions Botmarley actually needs. Never enable withdrawal permissions for a bot API key. If a key is ever compromised, withdrawal access would put your funds at direct risk.

Verifying Credentials

After creating an exchange account, its status is Unverified. Verification confirms that the API key and secret are correct and that Botmarley can reach the exchange.

How to verify

  1. On the Accounts page, find the account row in the table.
  2. Click the Verify button (checkmark icon or "Verify" label).
  3. Botmarley enqueues an AccountVerify task and redirects you to the Tasks page.
  4. The task worker picks up the job and makes a signed request to the exchange's balance endpoint. Kraken uses HMAC-SHA512 signing against /0/private/Balance; Binance uses HMAC-SHA256 signing against /api/v3/account.
  5. If the exchange responds successfully, the account status changes to Verified.
  6. If authentication fails, the status changes to Failed.

You can check the result by returning to the Accounts page or watching the task complete on the Tasks page.

Note

Verification is a one-time check. If you later rotate your API keys on the exchange, you will need to update the credentials in Botmarley and re-verify.

Troubleshooting verification failures

SymptomLikely causeFix
Status stays "Failed"Incorrect API key or secretDouble-check the values; re-paste from the exchange if needed.
Status stays "Failed"API key expired or revokedGenerate a new key on the exchange.
Status stays "Failed"Missing read/query permissionsEdit the key on the exchange and enable the required permission.
Task never completesNetwork issue or server not runningCheck the server logs and internet connectivity.

Syncing Account Assets

Syncing pulls the current token balances from the exchange into Botmarley's database. This is how Botmarley knows what you hold.

How to sync

  1. On the Accounts page, click the Sync button on the account row.
  2. Botmarley enqueues an AccountSync task.
  3. The task worker calls the exchange API (Kraken's /0/private/Balance or Binance's /api/v3/account), retrieves all non-zero balances, normalizes the token names, and upserts them into the account_assets table.
  4. Once complete, the account's asset list is updated.

You can expand an account row to see its assets, or visit the Portfolio page for an aggregated view across all accounts.

Note

Paper accounts cannot be synced from an exchange (there is no exchange). Their balances are set manually or updated by the trading engine during simulated sessions.

Token name normalization

Kraken uses internal token names that differ from standard symbols. Botmarley normalizes them automatically during sync:

Kraken nameNormalized
XXBTXBT
XETHETH
ZUSDUSD
XLTCLTC
XXRPXRP

Tokens that do not match a known pattern (e.g. DOT, USDC) are stored as-is.

Binance uses standard token symbols (BTC, ETH, USDC, etc.), so no normalization is needed for Binance accounts.

Editing an Account

  1. On the Accounts page, click the Edit button (pencil icon) on the account row.
  2. The edit modal opens with the current name, description, and type pre-filled.
  3. Modify the fields you want to change.
  4. For credentials: leave the API Key and API Secret fields empty to keep the existing values. Only fill them in if you want to replace them.
  5. Click Save Changes.

After editing credentials, you should re-verify the account to confirm the new keys work.

Tip

If you rotated your exchange API key, update it here, save, and then click Verify again.

Deleting an Account

  1. On the Accounts page, click the Delete button on the account row.
  2. Botmarley deletes the account and all associated assets from the database.

Deletion is permanent

Deleting an account removes it and all its stored asset data from Botmarley's database. This cannot be undone. Your funds on the actual exchange are not affected -- only Botmarley's local record is removed.

Deletion also cascades to portfolio snapshots associated with that account. Aggregate (cross-account) portfolio snapshots are not deleted, but they will no longer include the removed account in future syncs.

API Key Security

Botmarley stores API credentials in the local PostgreSQL database. Here are the safeguards in place and the precautions you should take.

What Botmarley does

  • Secrets are never sent to the browser. The Account struct has a to_view() method that masks the API key (showing only the first and last 4 characters) and omits the secret entirely.
  • Credentials stay local. Botmarley is a single-tenant application running on your machine. There is no cloud service, no third-party server, and no telemetry that transmits your keys.

What you should do

Protect your API keys

  • Never share your API secret with anyone.
  • Never enable Withdraw permissions on keys used by bots.
  • Use a dedicated API key for Botmarley rather than reusing one from another tool.
  • Rotate keys periodically -- delete the old key on the exchange and generate a new one.
  • Restrict IP access if the exchange supports it for your key -- lock the key to the IP address where Botmarley runs.
  • Back up your database carefully -- the PostgreSQL database contains the raw API secret. Treat database dumps with the same care as the secrets themselves.

Portfolio Tracking

The Portfolio page gives you a consolidated view of everything you hold across all your accounts -- Paper and exchange alike -- valued in both USD and BTC, updated automatically over time.

What Is Portfolio Tracking?

Portfolio tracking answers a simple question: how much is everything worth, right now and over time?

Botmarley periodically takes a "snapshot" of your portfolio. Each snapshot records:

  • The total value in USD across all accounts.
  • The total value in BTC (your USD total divided by the current BTC price).
  • The BTC/USD price at that moment.
  • A per-token breakdown showing balance and USD value for each asset.

By storing snapshots over time, Botmarley can draw charts showing how your portfolio value has changed -- and whether you are gaining or losing ground relative to Bitcoin itself.

How It Works

Portfolio syncing follows a pipeline that runs on a fixed schedule and can also be triggered manually.

Automatic sync schedule

A background scheduler starts when the Botmarley server boots. After an initial 60-second startup delay, it enqueues a PortfolioSync task every 1 hour. The task worker picks it up and runs the full sync pipeline.

The sync pipeline

flowchart LR
    A["Scheduler\n(every 1h)"] -->|enqueues task| B["Task Worker"]
    B --> C["Sync Exchange\nAccounts"]
    C --> D["Query All\nAccount Assets"]
    D --> E["Fetch USD Prices\n(Kraken Ticker API)"]
    E --> F["Compute Totals\n(USD + BTC)"]
    F --> G["Store Snapshot\n(PostgreSQL)"]

In detail, each sync performs these steps:

  1. Sync verified exchange accounts -- for every exchange account with status "Verified", Botmarley calls the exchange API to pull the latest balances. Failed syncs for individual accounts are logged but do not stop the pipeline.
  2. Query all account assets -- reads every non-zero asset balance across all accounts (including Paper) from the database.
  3. Fetch USD prices -- calls the Kraken public Ticker API to get current USD prices for each unique token. Stablecoins and fiat are handled locally (see below).
  4. Compute totals -- sums up balance * price for every asset to get total_value_usd. Fetches the BTC/USD price separately and calculates total_value_btc = total_value_usd / btc_price.
  5. Store snapshot -- inserts one aggregate row (with account_id = NULL) into the portfolio_snapshots table, including a JSON breakdown of per-token values.

The Portfolio Page

The portfolio page is located at /portfolio. It has several sections, described from top to bottom.

Portfolio page showing summary cards, value charts, and per-token asset breakdown

Summary Cards

Four stat cards appear at the top of the page:

CardDescriptionExample
Total Value (USD)Sum of all assets across all accounts, converted to USD.$12,345.67
Total Value (BTC)Your USD total divided by the current BTC price.0.142857
BTC PriceThe latest BTC/USD price fetched from Kraken.$86,420.00
Last SyncTimestamp of the most recent portfolio snapshot.2026-03-11 14:00 UTC

If no snapshots exist yet (fresh install), the page shows an empty state with buttons to manage accounts or run a first sync.

Range Selector

Below the summary cards, a row of small buttons lets you control the time range for the charts:

ButtonTime range
7dLast 7 days
30dLast 30 days (default)
90dLast 90 days
AllAll available history

Switching ranges fetches new data from /api/portfolio/chart?range=... and updates both charts without a full page reload. A small label next to the buttons shows how many data points are in the current view.

USD Chart: Portfolio Value + BTC Price Overlay

The first chart displays two lines on a dual-axis layout:

  • Portfolio USD (blue, right axis) -- your total portfolio value over time in US dollars.
  • BTC Price (amber/dashed, left axis) -- the Bitcoin price over the same period.

This overlay lets you visually compare whether your portfolio is outperforming or underperforming a simple Bitcoin hold. The charts are rendered using Lightweight Charts loaded from a CDN.

BTC Chart: Portfolio Value in BTC

The second chart shows a single amber line: your portfolio's value expressed in Bitcoin. If this line trends upward, you are accumulating more BTC-equivalent value over time, even if the USD value moves sideways.

Asset Breakdown Table

Below the charts, a table lists every token you hold across all accounts:

ColumnDescription
TokenThe asset symbol (BTC, ETH, USDC, etc.)
BalanceTotal holdings of that token across all accounts.
USD ValueBalance multiplied by the current USD price.
Account(s)Which accounts hold this token and how much each one holds.

The table includes a Refresh button that fetches the latest data via HTMX without reloading the page. Assets are sorted by USD value in descending order, so your largest holdings appear first.

Manual Sync

You do not have to wait for the hourly scheduler. To trigger a portfolio sync on demand:

  1. Go to the Portfolio page (/portfolio).
  2. Click the Sync Now button in the top-right corner.
  3. Botmarley enqueues a PortfolioSync task and redirects you back to the portfolio page.
  4. The task worker processes the sync (usually completes within a few seconds).
  5. Refresh the page to see updated values and charts.

Tip

After adding a new exchange account and verifying it, click "Sync Now" on the Portfolio page to immediately pull in its balances rather than waiting for the next scheduled sync.

How Prices Are Fetched

Botmarley uses the Kraken public Ticker API to fetch USD prices. This endpoint requires no authentication and has generous rate limits.

The API call

A single request is made to https://api.kraken.com/0/public/Ticker?pair=XXBTZUSD,XETHZUSD,... with all needed trading pairs joined by commas. The response includes the last trade price for each pair, which Botmarley uses as the current price.

Token-to-pair mapping

Botmarley maps normalized token names to Kraken trading pair identifiers:

TokenKraken Pair
BTC / XBTXXBTZUSD
ETHXETHZUSD
LTCXLTCZUSD
XRPXXRPZUSD
XLMXXLMZUSD
(others){TOKEN}USD

For tokens that Kraken does not list, the price falls back to 0.0 and a warning is logged.

Stablecoin Handling

Stablecoins and fiat currencies are handled without an API call:

TokensAssumed USD price
USD, USDC, USDT, BUSD, DAI, TUSD, USDP, GUSD$1.00
EUR, GBP, CAD, AUD, CHF, JPY~$1.00 (rough approximation)

Note

The fiat approximation (treating EUR, GBP, etc. as $1.00) is a simplification. For portfolios with significant fiat holdings in non-USD currencies, the total USD value will be approximate. A more accurate forex conversion may be added in a future release.

Architecture: Full Sync Flow

The following diagram shows the complete lifecycle of a portfolio sync, from trigger through to the stored snapshot.

sequenceDiagram
    participant S as Scheduler (1h) /<br>Manual Button
    participant Q as Task Queue
    participant W as Task Worker
    participant K as Kraken API
    participant DB as PostgreSQL

    S->>Q: Enqueue PortfolioSync task
    Q->>W: Dequeue task

    Note over W: Step 1: Sync exchange accounts
    W->>DB: List verified exchange accounts
    DB-->>W: [Kraken accounts]
    loop Each verified account
        W->>K: POST /0/private/Balance (signed)
        K-->>W: Token balances
        W->>DB: Upsert account_assets
    end

    Note over W: Step 2: Gather all assets
    W->>DB: SELECT token, balance FROM account_assets
    DB-->>W: All non-zero balances

    Note over W: Step 3: Fetch prices
    W->>K: GET /0/public/Ticker?pair=...
    K-->>W: USD prices per pair

    Note over W: Step 4-5: Compute and store
    W->>W: total_usd = SUM(balance * price)<br>total_btc = total_usd / btc_price
    W->>DB: INSERT INTO portfolio_snapshots
    DB-->>W: Snapshot ID

    W->>Q: Mark task complete

Note

If a single exchange account fails to sync (e.g., temporary network error), the pipeline continues with the remaining accounts. The snapshot will still be created using whatever data was successfully retrieved. Check the Activity Logs or Task Queue for details on any failures.

Strategies Overview

What Is a Trading Strategy?

A trading strategy is a set of rules that tell Botmarley when to buy and when to sell a cryptocurrency. Instead of watching charts all day and making emotional decisions, you define your rules once, and Botmarley follows them automatically -- 24 hours a day, 7 days a week.

Think of a strategy as a recipe:

  • Ingredients are market data (prices, indicators, candle patterns).
  • Instructions are the rules (if RSI drops below 30, buy).
  • The result is consistent, disciplined trading without human emotion.

The Strategies List

The web interface provides a central place to manage all your strategies. From the list page you can edit, duplicate, delete, backtest, or start live trading for any strategy with a single click.

Strategies list page showing saved strategies with action buttons for Edit, TOML, Duplicate, Delete, Backtest, and Start Live

How Botmarley Strategies Work

Botmarley strategies are defined as TOML files -- simple, human-readable text files that describe a rule engine. Each strategy file lives in the strats/ directory and contains:

  1. Metadata -- the strategy's name, description, and position limits.
  2. Actions -- a list of things the bot can do (open a position, add to it, or sell).
  3. Triggers -- conditions that must be true for each action to fire.

Note

Botmarley's strategy engine is exchange-agnostic. The same strategy TOML file works for any trading pair -- you select the pair when you start a backtest or live session, not in the strategy itself.

Strategy Evaluation Flow

Every time a new candle closes, Botmarley evaluates your strategy. Here is how the evaluation loop works:

flowchart TD
    A["New Candle Received"] --> B["For Each Action in Strategy"]
    B --> C{"Check ALL Triggers<br/>(AND logic)"}
    C -- "All triggers TRUE" --> D["Execute Action<br/>(open_long / buy / sell)"]
    C -- "Any trigger FALSE" --> E["Skip This Action"]
    D --> F["Update Position State"]
    E --> F
    F --> G{"More Actions?"}
    G -- "Yes" --> B
    G -- "No" --> H["Wait for Next Candle"]
    H --> A

    style A fill:#4a9eff,color:#fff
    style D fill:#22c55e,color:#fff
    style E fill:#6b7280,color:#fff
    style H fill:#4a9eff,color:#fff

Key points about evaluation:

  • Each action is evaluated independently -- one strategy can have multiple buy and sell actions, and each is checked every tick.
  • Triggers within an action are AND-combined -- ALL triggers must be true for the action to fire.
  • Actions are evaluated in order -- from first to last as written in the TOML file.

Basic Strategy Anatomy

A Botmarley strategy TOML file has two main sections:

1. The [meta] Section

This defines your strategy's identity and global settings:

[meta]
name = "My First Strategy"
description = "A simple RSI-based entry and exit"
max_open_positions = 3
FieldRequiredDescription
nameYesHuman-readable name for the strategy
descriptionNoOptional explanation of the strategy's logic
max_open_positionsNoMaximum concurrent open positions. Omit for unlimited.

2. The [[actions]] Array

Each [[actions]] block defines one thing the bot can do, along with the conditions that must be met:

[[actions]]
type = "open_long"          # What to do: open_long, buy, or sell
amount = "100 USDC"         # How much: fixed amount or percentage
average_price = false        # Whether to average the entry price (DCA)

  [[actions.triggers]]       # When to do it: one or more trigger conditions
  indicator = "rsi_14"
  operator = "<"
  target = "30"

A Simple Complete Example

Here is a complete, working strategy that buys when RSI is oversold and sells when it recovers:

[meta]
name = "RSI_Scalper"

# ACTION 1: Open a position when RSI drops below 30
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"

# ACTION 2: Sell half when RSI recovers to 50
[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "50"

# ACTION 3: Sell everything when RSI reaches 70
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "70"

What this strategy does, step by step:

  1. Action 1 watches RSI(14). When it drops below 30 (oversold territory), the bot opens a new position by buying 100 USDC worth of the asset.
  2. Action 2 watches for RSI to rise above 50. When it does, the bot sells 50% of the position to lock in partial profit.
  3. Action 3 watches for RSI to rise above 70 (overbought territory). When it does, the bot sells the remaining 100% of the position to fully exit.

Tip

Start simple. A strategy with 2-3 actions is often more effective than a complex one with 10 actions. You can always add complexity after backtesting confirms the base logic works.

What's Next?

Now that you understand how strategies are structured, explore the following topics:

Strategy Editor

The Strategy Editor is where you create, edit, and manage your trading strategies. Access it from the sidebar by clicking Strategies or navigating directly to /strats.

The Strategies List Page

The strategies list page (/strats) shows all your saved strategies. For each strategy you can see:

  • The strategy name and description
  • When it was created and last updated
  • Action buttons for editing, duplicating, backtesting, and deleting
flowchart LR
    A["Strategies List<br/>/strats"] --> B["Edit<br/>/strats/edit/{id}"]
    A --> C["New Strategy<br/>/strats/new"]
    A --> D["Duplicate"]
    A --> E["Delete"]
    A --> F["Backtest"]
    B --> G["Save"]
    C --> G
    G --> A

    style A fill:#4a9eff,color:#fff
    style G fill:#22c55e,color:#fff

Creating a New Strategy

To create a new strategy:

  1. Navigate to /strats (click Strategies in the sidebar).
  2. Click the New Strategy button.
  3. The editor opens with an empty TOML document.
  4. Write your strategy in TOML format (see Strategy Overview for the structure).
  5. Click Save.

The editor assigns a unique ID (UUID) to your new strategy automatically.

Tip

If you are new to writing strategies, start by duplicating one of the existing strategies and modifying it. This is faster than writing from scratch and helps you learn the TOML structure by example.

The Visual Strategy Editor

The visual editor provides a form-based interface for building strategies without writing TOML by hand. You can add actions, configure triggers, set indicator parameters, and define position management rules -- all through a structured UI.

Visual strategy editor with form-based UI for building trading strategies

The TOML Text Editor

The strategy editor at /strats/edit/{id} provides a text-based TOML editor where you write your strategy directly. The editor shows the raw TOML content of your strategy file.

Raw TOML editing view for direct strategy file editing

A typical editing session looks like this:

  1. The [meta] section is at the top -- set your strategy's name and description here.
  2. Below that, define your [[actions]] blocks with their triggers.
  3. As you type, the editor validates your TOML in real time.

Editor Layout

The editor page provides:

  • Strategy name displayed at the top
  • TOML text area for editing the strategy content
  • Save button to persist changes
  • Back to list link to return to /strats

Real-Time Validation

Every time you save or validate your strategy, Botmarley checks the TOML content for errors. The validation endpoint (POST /api/strats/validate) checks:

  • TOML syntax -- is the file valid TOML?
  • Required fields -- does [meta] have a name? Does each action have a type and amount?
  • Valid action types -- only open_long, buy, and sell are accepted.
  • Amount format -- must be "100 USDC" (fixed) or "50%" (percentage).
  • Indicator format -- must match known patterns like rsi_14, sma_50, bb_lower.
  • Operator validity -- must be one of >, <, =, cross_above, cross_below.
  • Timeframe validity -- if specified, must be 1m, 5m, 15m, 1h, 4h, or 1d.
  • Period ranges -- indicator periods must be between 1 and 200.
  • max_open_positions -- if set, must be 1 or greater.

When validation fails, the error response includes:

FieldDescription
field_pathWhere the error is, e.g. actions[0].triggers[1].operator
error_codeMachine-readable code like INVALID_FORMAT or REQUIRED
messageHuman-readable description of the problem
suggestionHint for how to fix the error

Warning

Botmarley will not let you save an invalid strategy. Fix all validation errors before saving. This prevents broken strategies from being used in backtests or live trading.

Common Validation Errors

ErrorCauseFix
INVALID_ACTION_TYPETypo in action typeUse open_long, buy, or sell
INVALID_AMOUNT_FORMATMissing unit or wrong formatUse "100 USDC" or "50%"
INVALID_PERIODNon-numeric indicator periodUse rsi_14 not rsi_fourteen
INVALID_PERIOD_RANGEPeriod 0 or > 200Keep periods between 1 and 200
REQUIREDMissing indicator or operatorAdd the missing field to your trigger
INVALID_TIMEFRAMEUnknown timeframe stringUse 1m, 5m, 15m, 1h, 4h, or 1d
INVALID_OPERATORUnknown comparison operatorUse >, <, =, cross_above, cross_below

Saving Strategies

When you click Save, Botmarley:

  1. Validates the TOML content server-side.
  2. If valid, writes the TOML file to the strats/ directory with the strategy's UUID as the filename.
  3. Returns a success response with the strategy ID.
  4. Logs the save action to the activity log.

If the strategy is new (no ID yet), a new UUID is generated. If you are editing an existing strategy, the same ID is reused and the file is overwritten.

Note

Strategy files are stored as plain .toml files in the strats/ directory. The filename is the UUID (e.g., 6f814a05-73e8-483f-8963-aa8a4ab8e1e7.toml). You can also edit these files directly with any text editor -- Botmarley reads them from disk.

Duplicating Strategies

To duplicate a strategy:

  1. Go to the strategies list (/strats).
  2. Click the Duplicate button on the strategy you want to copy.
  3. Botmarley creates a copy with " (Copy)" appended to the name.
  4. You are redirected to the editor for the new copy.

This is useful when you want to create variations of an existing strategy (for example, testing different RSI thresholds or DCA levels).

Deleting Strategies

To delete a strategy:

  1. Go to the strategies list (/strats).
  2. Click the Delete button on the strategy you want to remove.
  3. The TOML file is removed from the strats/ directory.
  4. You are redirected back to the list.

Danger

Deleting a strategy is permanent. The TOML file is removed from disk. If you think you might want the strategy later, duplicate it first or keep a backup.

Bulk Delete

You can also select multiple strategies and delete them all at once using the bulk delete feature on the strategies list page. Select the checkboxes next to the strategies you want to remove, then click the bulk delete button.

Discovering Available Pairs

When you open the backtest modal from the strategies list page, Botmarley automatically scans your downloaded market data to show you which trading pairs are available. This is based on the .arrow files in your data storage directory.

The available pairs and their date ranges are shown in the backtest form, so you can select:

  • Which pair to backtest against (e.g., XBTUSD, ETHUSD)
  • Start and end dates -- pre-filled based on your actual data range

Tip

If you do not see the pair you want, you need to download its historical data first. Go to the Market Data section to download candle history from Kraken.

Actions

Actions are the building blocks of your strategy. Each action defines what the bot should do when its trigger conditions are met. A strategy can have any number of actions, and each is evaluated independently on every candle.

The Three Action Types

Botmarley supports three action types, each serving a distinct purpose in the lifecycle of a trade:

flowchart LR
    A["open_long<br/>Open new position"] --> B["buy<br/>Add to position (DCA)"]
    B --> C["sell<br/>Exit position"]

    style A fill:#22c55e,color:#fff
    style B fill:#4a9eff,color:#fff
    style C fill:#ef4444,color:#fff

open_long -- Open a New Position

The open_long action creates a new position by buying an asset. This is the entry point of any trade.

When to use: Use open_long for the initial entry into a trade. This action only fires when there is no existing open position (or when max_open_positions has not been reached).

Example:

[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"

This opens a new position worth 100 USDC when RSI(14) drops below 30.

Note

open_long is the only action type that creates a new position. If you already have an open position and want to add more, use buy instead.


buy -- Add to an Existing Position (DCA)

The buy action adds to an already open position. This is how you implement Dollar-Cost Averaging (DCA) -- buying more when the price drops to lower your average entry price.

When to use: Use buy to add to a position that was opened by open_long. This action only fires when a position is already open.

Example:

[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1

This adds 200 USDC to the existing position when the price drops 2% from the entry price. The average_price = true flag means the entry price is recalculated (see average_price explained below). The max_count = 1 ensures this DCA level only triggers once per position.


sell -- Exit a Position

The sell action exits part or all of an open position. You can sell a percentage of your position or sell everything.

When to use: Use sell for taking profit, cutting losses (stop-loss), or any exit condition.

Example -- partial exit:

[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "50"

This sells half the position when RSI rises above 50, locking in partial profit while letting the rest ride.

Example -- full exit:

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

This sells the entire position when the price rises 3% from entry, closing the trade completely.


Amount Formats

Every action requires an amount field that specifies how much to buy or sell. Botmarley supports two formats:

Fixed Amount: "100 USDC"

A fixed dollar amount. The bot buys or sells exactly this much.

amount = "100 USDC"

When to use: For entries and DCA levels where you want predictable position sizing. Most strategies use fixed amounts for open_long and buy actions.

Supported currencies: USDC, USD, BTC, ETH, and other standard ticker symbols.

Percentage Amount: "50%"

A percentage of either the available balance (for buys) or the current position size (for sells).

amount = "50%"    # Sell half the position
amount = "100%"   # Sell the entire position

When to use:

  • For sell actions: "50%" means sell half your position; "100%" means exit completely.
  • For buy actions: "50%" means use 50% of available balance.

Tip

Use percentage amounts for sells and fixed amounts for buys. This gives you predictable entry sizing while allowing flexible exit sizing. For example, selling "50%" first and then "100%" of the remainder lets you scale out of a position as the price rises.

Amount Format Rules

FormatExampleValid ForDescription
Fixed"100 USDC"open_long, buyBuy exactly 100 USDC worth
Fixed"0.01 BTC"open_long, buyBuy exactly 0.01 BTC
Percentage"50%"buy, sell50% of balance or position
Percentage"100%"sellSell the entire position

Warning

Negative amounts are not allowed. The amount must be a positive number. Use the action type (sell) to indicate selling, not a negative amount.


average_price Explained

The average_price flag controls whether the bot recalculates the position's entry price after a buy or open_long action. This is the core mechanism behind Dollar-Cost Averaging (DCA).

How It Works

When average_price = true and the bot buys more of an asset, the entry price is recalculated as a weighted average:

new_entry_price = (old_qty * old_price + new_qty * new_price) / (old_qty + new_qty)

Example:

  1. You open a position: buy 100 USDC worth of BTC at $50,000. Entry price = $50,000.
  2. Price drops to $48,000. A DCA buy triggers: buy 200 USDC more at $48,000.
  3. With average_price = true, the new entry price becomes:
    • Total cost: $100 + $200 = $300
    • Total BTC: 0.002 + 0.004167 = 0.006167 BTC
    • New entry price: $300 / 0.006167 = $48,649
  4. Now the price only needs to rise to $48,649 (not $50,000) to break even.

When to Use It

Scenarioaverage_priceWhy
DCA buy on diptrueLower your average entry so you break even sooner
Adding to a winnerfalseKeep the original entry price for P&L tracking
Initial open_longfalseNo previous position to average with
All sell actionsfalseSelling does not affect entry price

Note

The average_price flag is only meaningful for open_long and buy actions. Setting it on a sell action has no effect.


Multiple Actions: How They Work Together

A strategy typically has multiple actions -- at least one entry (open_long) and one exit (sell). Here is how they interact:

Evaluation Order

Actions are evaluated independently and in order on every candle:

flowchart TD
    A["Candle Closes"] --> B["Evaluate Action 1<br/>(open_long)"]
    B --> C["Evaluate Action 2<br/>(buy / DCA level 1)"]
    C --> D["Evaluate Action 3<br/>(buy / DCA level 2)"]
    D --> E["Evaluate Action 4<br/>(sell / take profit)"]
    E --> F["Evaluate Action 5<br/>(sell / stop loss)"]

    style A fill:#4a9eff,color:#fff

Key Rules

  1. open_long only fires when no position is open (or when max_open_positions allows another).
  2. buy only fires when a position IS open -- it has nothing to add to otherwise.
  3. sell only fires when a position IS open -- you cannot sell what you do not have.
  4. Multiple sell actions can coexist -- you might have a take-profit sell at +3% AND a stop-loss sell at -8%. Whichever condition is met first will fire.
  5. After a 100% sell, the position is closed -- subsequent buy triggers will not fire until open_long creates a new position.

Typical Action Structure

Most strategies follow this pattern:

# 1. Entry
[[actions]]
type = "open_long"
amount = "100 USDC"
# ... entry triggers ...

# 2. DCA Level 1 (optional)
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true
# ... dip trigger ...

# 3. DCA Level 2 (optional)
[[actions]]
type = "buy"
amount = "300 USDC"
average_price = true
# ... deeper dip trigger ...

# 4. Take Profit
[[actions]]
type = "sell"
amount = "100%"
# ... profit trigger ...

# 5. Stop Loss
[[actions]]
type = "sell"
amount = "100%"
# ... loss trigger ...

Tip

You can have multiple sell actions with different conditions. For example, sell 50% at +2% profit and the remaining 100% at +5% profit. This lets you scale out of a position gradually.

Triggers Overview

Triggers are the decision engine of every Botmarley strategy. An action (buy, sell, open position) only executes when its triggers say "go." Without triggers, an action would never fire. Without the right triggers, your strategy would trade at the wrong time.

Think of it this way:

  • An action answers what to do (buy 100 USDC, sell 50%, open a long position).
  • A trigger answers when to do it (RSI dropped below 30, price fell 3% from entry, the 4h candle just closed).

Every action has one or more triggers attached to it. When all of them are satisfied at the same moment, the action fires.

The Three Trigger Types

Botmarley supports three categories of triggers. Each watches a different kind of market signal:

TypeWhat It WatchesExample
TechnicalIndicator values and crossoversRSI(14) drops below 30, SMA(50) crosses above SMA(200)
PriceChangePrice movement, position P&L, time, momentumPrice dropped 3% in the last hour, position is up 2% from entry
NextCandleCandle close eventsThe 4h candle just closed, the daily candle just closed

You will often combine these types on the same action. For example, "buy when the 4h candle closes AND RSI is below 35" uses a NextCandle trigger together with a Technical trigger.

The dedicated chapters cover each type in depth:

AND Logic: Multiple Triggers on One Action

When you attach multiple triggers to a single action, Botmarley treats them as AND conditions. Every trigger must be true simultaneously for the action to fire.

graph LR
    T1["Trigger 1<br/>RSI(14) < 30"]
    T2["Trigger 2<br/>SMA(50) > SMA(200)"]
    T3["Trigger 3<br/>Price fell -3%"]
    AND{{"ALL must<br/>be true"}}
    A["Action fires:<br/>Buy 100 USDC"]

    T1 --> AND
    T2 --> AND
    T3 --> AND
    AND --> A

In TOML, this looks like multiple [[actions.triggers]] blocks under the same [[actions]]:

# This action requires ALL THREE triggers to be true
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

[[actions.triggers]]
indicator = "sma_50"
operator = ">"
target = "sma_200"
timeframe = "1d"

[[actions.triggers]]
type = "price_change"
value = "-3%"
timeframe = "1h"

Note

There is no OR logic within a single action. If you want "buy when RSI < 30 OR price drops 5%," create two separate actions -- one with the RSI trigger, one with the price drop trigger. Each action fires independently.

The max_count Field

Every trigger type supports an optional max_count field. It limits how many times that trigger can fire per position.

[[actions.triggers]]
type = "pos_price_change"
value = "-2%"
max_count = 1
max_count valueBehavior
Not set (or None)Trigger can fire unlimited times per position
1Trigger fires once per position, then is disabled for that position
3Trigger fires up to 3 times per position

This is essential for DCA (Dollar-Cost Averaging) strategies where you want layered entries. Without max_count, a DCA trigger at -2% would keep firing every time the price bounces back and forth across the -2% threshold. With max_count = 1, it fires once, and you rely on the next DCA level (-4%, -6%) to add more capital.

Tip

A common pattern is to set max_count = 1 on each DCA level so each level fires exactly once per position. This prevents accidental double-buying at the same price level.

How Triggers Reset

Trigger fire counts reset when a new position opens. If a trigger on a DCA action has max_count = 1 and it fires once during Position A, that counter goes back to zero when Position A is closed and Position B is opened later.

The lifecycle looks like this:

graph TD
    A["Position opens"] --> B["Triggers active<br/>fire counts = 0"]
    B --> C{"Trigger<br/>conditions met?"}
    C -- "Yes" --> D["Action fires<br/>fire count += 1"]
    D --> E{"fire count<br/>< max_count?"}
    E -- "Yes" --> C
    E -- "No (or max_count reached)" --> F["Trigger disabled<br/>for this position"]
    C -- "No" --> G["Wait for<br/>next evaluation"]
    G --> C
    F --> H["Position closes"]
    H --> I["All fire counts<br/>reset to 0"]
    I --> A

Warning

max_count is tracked per trigger, per position. If you have two separate actions each with their own -2% trigger and max_count = 1, each of them fires independently -- you would get two buys at the -2% level. Make sure your DCA levels are on separate actions with distinct thresholds.

Putting It All Together

Here is a minimal strategy that demonstrates all three trigger types working together:

[meta]
name = "Triggers Overview Demo"
description = "Shows all three trigger types in one strategy"
max_open_positions = 2

# ENTRY: open when RSI is oversold (Technical)
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

# DCA: buy more if position drops 3% (PriceChange)
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-3%"
max_count = 1

# ACCUMULATE: buy on every 4h candle close (NextCandle)
[[actions]]
type = "buy"
amount = "25 USDC"
average_price = true

[[actions.triggers]]
type = "next_candle"
timeframe = "4h"
max_count = 5

# EXIT: sell when position is up 2% (PriceChange)
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "2%"

This strategy opens a position when RSI dips, adds to it on dips and on a schedule, and exits at +2% profit. The max_count values prevent runaway accumulation: the DCA fires once, and the scheduled buys stop after 5 candles.

Continue reading the dedicated chapters for each trigger type to learn all the available fields and see more examples.

Technical Indicator Triggers

Technical triggers compare an indicator's current value against a target. They answer questions like "is RSI below 30?", "did the 50-day SMA just cross above the 200-day SMA?", or "is price above the upper Bollinger Band?"

These are the most common triggers in any strategy. If you have used TradingView alerts or any charting tool, you will recognize the logic immediately.

Anatomy of a Technical Trigger

A technical trigger has four fields:

[[actions.triggers]]
indicator = "rsi_14"        # What to measure
operator = "<"              # How to compare
target = "30"               # What to compare against
timeframe = "1h"            # On which candle timeframe (optional)
max_count = 1               # How many times it can fire per position (optional)
FieldRequiredDescription
indicatorYesThe indicator to read. Format: type_period (e.g., rsi_14, sma_50, ema_200) or a special name (bb_lower, macd_line, price, ttm_trend).
operatorYesHow to compare the indicator to the target. One of: >, <, =, cross_above, cross_below.
targetYesThe value to compare against. Either a number ("30", "0", "-3") or another indicator ("sma_200", "bb_upper", "price").
timeframeNoWhich candle timeframe to evaluate on: 1m, 5m, 15m, 1h, 4h, 1d. If omitted, uses the session's default timeframe.
max_countNoMaximum number of times this trigger can fire per position. Omit for unlimited.

Available Indicators

All of these can be used in the indicator field or as a target:

IndicatorFormatExampleDescription
SMAsma_{period}sma_50, sma_200Simple Moving Average
EMAema_{period}ema_9, ema_21, ema_200Exponential Moving Average
RSIrsi_{period}rsi_14, rsi_7Relative Strength Index (0-100)
Bollinger Bandsbb_{band}bb_lower, bb_upper, bb_middleBollinger Band levels
MACDmacd_{component}macd_line, macd_signal, macd_histogramMACD components
StochRSIstoch_rsi_{period}stoch_rsi_14Stochastic RSI (0-100)
ROCroc_{period}roc_10Rate of Change (%)
ATRatr_{period}atr_14Average True Range
OBVobvobvOn-Balance Volume (no period)
OBV SMAobv_sma_{period}obv_sma_20SMA of On-Balance Volume
Volume SMAvol_sma_{period}vol_sma_20SMA of Volume
TTM Trendttm_trendttm_trendTTM Squeeze trend (1 = bullish, -1 = bearish)
PricepricepriceCurrent market price (close)

Note

Period values must be between 1 and 200. Using sma_0 or rsi_300 will fail validation.

Operators Explained

> (Greater Than) and < (Less Than)

Simple threshold comparisons. The trigger is true whenever the indicator is above or below the target value.

# True whenever RSI(14) is below 30
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

These fire on every candle where the condition is true, not just the moment it crosses the threshold. If RSI stays below 30 for ten candles, the trigger is "active" on all ten. (Use max_count if you only want it to fire once.)

cross_above and cross_below (Crossover Detection)

A crossover trigger fires at the moment of transition. It checks two consecutive candles:

  • cross_above: The indicator was at or below the target on the previous candle, and is above it on the current candle.
  • cross_below: The indicator was at or above the target on the previous candle, and is below it on the current candle.
graph LR
    subgraph "cross_above"
        P1["Previous candle:<br/>SMA(50) <= SMA(200)"]
        C1["Current candle:<br/>SMA(50) > SMA(200)"]
        P1 -->|"transition"| C1
    end
    subgraph "cross_below"
        P2["Previous candle:<br/>SMA(50) >= SMA(200)"]
        C2["Current candle:<br/>SMA(50) < SMA(200)"]
        P2 -->|"transition"| C2
    end
# Golden cross: SMA(50) crosses above SMA(200)
[[actions.triggers]]
indicator = "sma_50"
operator = "cross_above"
target = "sma_200"
timeframe = "1d"

Crossovers are the most powerful tool for trend detection

Use cross_above and cross_below instead of > and < when you want to catch the turning point rather than the sustained condition. A golden cross (sma_50 cross_above sma_200) fires once when the trend shifts bullish, rather than on every single candle where SMA(50) happens to be above SMA(200).

= (Equal)

Exact match comparison. This is rarely used because indicator values are continuous floating-point numbers and exact equality is unlikely. The primary use case is the TTM Trend indicator, which outputs discrete integer values:

  • 1 = bullish trend
  • -1 = bearish trend
# TTM Trend is bullish
[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "1"

Warning

Do not use = with floating-point indicators like RSI or SMA. A condition like rsi_14 = 30 will almost never be exactly true because RSI values are typically decimals like 29.87 or 30.12. Use < or > instead.

Target Types

The target field accepts two kinds of values:

Numeric Target

A fixed number. Used for absolute thresholds like RSI levels, zero-line crossings, or price levels.

target = "30"      # RSI threshold
target = "0"       # MACD zero-line
target = "-3"      # Negative value (ROC dropped below -3%)
target = "50000"   # A specific price level

Indicator Target

Another indicator. Used for comparing two moving averages, checking if price is above/below a band, or volume breakout detection.

target = "sma_200"     # Compare against 200-period SMA
target = "ema_50"      # Compare against 50-period EMA
target = "bb_upper"    # Compare against upper Bollinger Band
target = "price"       # Compare indicator against current price
target = "obv_sma_20"  # Compare OBV against its SMA
target = "macd_signal" # Compare MACD line against its signal line

Note

When using an indicator as a target, both the indicator and target are evaluated on the same candle at the same timeframe. The order matters: indicator = "sma_50" with target = "sma_200" means "is SMA(50) above/below SMA(200)?", not the other way around.

Examples

Example 1: RSI Oversold Entry

Open a position when RSI(14) drops below 30 on the 1-hour chart.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

When to use: Classic mean-reversion entry. RSI below 30 suggests the asset is oversold and may bounce.

Example 2: Golden Cross (SMA 50/200)

Open a position when the daily SMA(50) crosses above SMA(200).

[[actions]]
type = "open_long"
amount = "200 USDC"

[[actions.triggers]]
indicator = "sma_50"
operator = "cross_above"
target = "sma_200"
timeframe = "1d"

When to use: Long-term trend following. The golden cross is one of the most widely watched signals in traditional finance and crypto alike.

Example 3: Death Cross Exit

Sell the entire position when SMA(50) crosses below SMA(200), signaling a trend reversal.

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
indicator = "sma_50"
operator = "cross_below"
target = "sma_200"
timeframe = "1d"

When to use: As a safety exit. Even if your take-profit has not been reached, a death cross suggests the uptrend is over.

Example 4: Bollinger Band Bounce

Open a position when price drops below the lower Bollinger Band on the 1-hour chart (and RSI confirms oversold conditions on the daily chart).

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "bb_lower"
operator = ">"
target = "price"
timeframe = "1h"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "40"
timeframe = "1d"

When to use: Volatility-based entry. When price punches below the lower band, it has moved more than two standard deviations from the mean. Combining with RSI filters out false signals during strong downtrends.

Note

Notice the operator direction: bb_lower > price means "the lower band is above the current price," which is the same as saying "price is below the lower band." Botmarley always reads left-to-right: indicator operator target.

Example 5: MACD Zero-Line Crossover

Buy when the MACD line crosses above zero, confirming bullish momentum.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "macd_line"
operator = "cross_above"
target = "0"
timeframe = "4h"

When to use: Momentum confirmation. The MACD crossing above zero means the short-term EMA has moved above the long-term EMA, confirming upward momentum.

Example 6: TTM Trend Filter with RSI

Only enter when RSI is oversold AND the TTM Trend confirms bullish direction.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"

[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "1"

When to use: Filtered entries. RSI alone can give false signals in a strong downtrend. Adding TTM Trend = 1 ensures you only buy dips during an overall uptrend.

Example 7: EMA Crossover (Fast over Slow)

Enter when EMA(9) crosses above EMA(21) on the 5-minute chart for short-term scalping.

[[actions]]
type = "open_long"
amount = "50 USDC"

[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "ema_21"
timeframe = "5m"

When to use: Short-term momentum trading. Faster EMA pairs (9/21, 12/26) react more quickly than SMA pairs, making them suitable for scalping on lower timeframes.

Example 8: Uptrend Filter (SMA 50 Above SMA 200)

Use as a guard condition on another action. Only allow entries while the macro trend is bullish.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "sma_50"
operator = ">"
target = "sma_200"
timeframe = "1d"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "1h"

When to use: Multi-timeframe filtering. The daily SMA check acts as a macro trend gate (only buy in uptrends), while the hourly RSI finds dip entries within that uptrend. This is the foundation of the "trend-safe DCA" pattern.

Timeframe Considerations

When you set a timeframe on a technical trigger, Botmarley evaluates the indicator on candles of that timeframe. A few things to keep in mind:

  • If you omit timeframe, the trigger uses the trading session's default timeframe.
  • You can mix timeframes on the same action. For example, one trigger checks the daily SMA while another checks the hourly RSI. Both must be true for the action to fire.
  • Valid timeframes are: 1m, 5m, 15m, 1h, 4h, 1d.
  • Lower timeframes (1m, 5m) produce more signals but are noisier. Higher timeframes (4h, 1d) produce fewer signals but are more reliable.

Tip

A powerful pattern is to use a higher timeframe for trend direction (daily SMA or TTM Trend) and a lower timeframe for entry timing (hourly RSI or 5-minute EMA crossover). This gives you the best of both worlds: reliable trend identification with precise entry points.


Deep-Dive: Indicators Reference

For in-depth explanations of each indicator — with annotated chart screenshots, operator behavior, and TOML examples — see the Indicators Reference and its sub-pages:

Indicators Reference

Botmarley supports 27 technical indicators that you can use in strategy triggers. Each indicator has a dedicated page with in-depth explanations and TOML trigger examples.

Quick Reference Table

Classic Technical Indicators

IndicatorFormatPeriodValue RangeDescription
SMAsma_201-200price-basedSimple Moving Average
EMAema_501-200price-basedExponential Moving Average
RSIrsi_141-2000-100Relative Strength Index
Bollinger Bandsbb_upper, bb_middle, bb_lowerfixed 20price-basedVolatility bands
MACDmacd_line, macd_signal, macd_histogramfixed 12/26/9unboundedTrend/momentum
StochRSIstoch_rsi_141-2000-100Stochastic RSI
ROCroc_101-200unboundedRate of Change (%)
ATRatr_141-2000+Average True Range
OBVobvnoneunboundedOn-Balance Volume
OBV SMAobv_sma_201-200unboundedSMA of OBV
VolSMAvol_sma_201-2000+Volume Simple Moving Average
TTM Trendttm_trendnone-1 / 0 / 1Trend direction indicator
PricepricenoneactualCurrent candle close price

Statistical Indicators

IndicatorFormatPeriodValue RangeDescription
RSTDrstd_205-5000+Rolling Standard Deviation
Z-Scorezscore_205-500-4 to +4Standard score (deviations from mean)
Percentile Rankprank_5010-5000-100Price percentile within rolling window
Parkinsonparkinson_205-5000+High-low range volatility estimator
Garman-Klassgk_vol_205-5000+OHLC-based volatility estimator
Yang-Zhangyz_vol_205-5000+Comprehensive volatility (overnight + intraday)
Hursthurst_20050-5000-1Hurst exponent (trend vs mean-reversion)
Half-Lifehalflife_20050-5001+Mean reversion speed (candles to half-revert)
Vol Regimevol_regime_10020-5001 / 2 / 3Volatility regime classifier
VWAPvwaprollingprice-basedVolume Weighted Average Price
VWAP Devvwap_dev_205-500unboundedPrice deviation from VWAP (in %)
Skewnessskew_6010-500-3 to +3Return distribution asymmetry
Kurtosiskurt_6010-500-2 to +10Tail risk (fat vs thin tails)
Autocorrelationautocorr_1lag 1-50-1 to +1Momentum persistence measurement

Indicator Categories

Trend Indicators

  • SMA — The classic moving average. Equal weight to all candles. Best for identifying long-term trends and support/resistance levels.
  • EMA — Weighted toward recent prices. Reacts faster than SMA. Best for short-term trading and entry timing.
  • TTM Trend — Simplified trend direction (1 = bullish, -1 = bearish). Great as a filter on other triggers.

Momentum Indicators

  • RSI — Measures overbought/oversold conditions on a 0-100 scale. The most popular momentum indicator.
  • MACD — Tracks the relationship between two EMAs. Shows both trend direction and momentum strength.
  • StochRSI — More sensitive version of RSI. Catches short-term reversals.
  • ROC — Percentage price change. Detects sharp dips and pumps.
  • Autocorrelation — Measures momentum persistence. Positive = trends continue; near zero = random; negative = reversals likely.

Volatility Indicators

  • Bollinger Bands — Dynamic envelope around price. Widens in volatility, narrows in calm. Great for mean-reversion strategies.
  • ATR — Measures volatility magnitude. Use for position sizing and volatility filters.
  • RSTD — Rolling standard deviation of price. The raw building block of volatility measurement.
  • Parkinson — Volatility from high-low range. More efficient than close-to-close estimators.
  • Garman-Klass — Volatility from full OHLC data. Even more efficient than Parkinson.
  • Yang-Zhang — The most comprehensive volatility estimator. Combines overnight gaps + intraday range.
  • Vol Regime — Classifies volatility into low/normal/high regimes. Self-calibrating across all assets.

Statistical / Mean-Reversion Indicators

  • Z-Score — How many standard deviations from the mean. Universal thresholds across all assets. The core mean-reversion signal.
  • Percentile Rank — Where current price sits in its rolling range (0-100). Great for detecting historically rare levels.
  • Hurst — THE regime detector. Tells you if the market is trending, mean-reverting, or random walk.
  • Half-Life — How fast price reverts to the mean (in candles). Determines if mean reversion happens fast enough to trade.

Institutional / Fair Value Indicators

  • VWAP — Volume Weighted Average Price. The benchmark institutions use for fair value.
  • VWAP Dev — Percentage deviation from VWAP. Measures how far price has strayed from institutional fair value.

Distribution Intelligence

  • Skewness — Measures return distribution asymmetry. Positive = upside surprises likely; negative = crash risk elevated.
  • Kurtosis — Measures tail fatness. Low = predictable; high = extreme moves likely. A pure risk indicator.

Volume Indicators

  • OBV — Cumulative volume flow. Confirms trends and detects divergences.
  • VolSMA — Average volume. Detect unusual volume spikes.

Special

  • Price — Current candle close. Compare against any indicator or absolute level.

Operators

All indicators are compared using one of five operators. See the Operators Reference for the complete guide, including the critical difference between state-based operators (>, <, =) that fire on every matching candle and event-based operators (cross_above, cross_below) that fire once at the moment of crossing.

OperatorTypeDescription
>State-basedTrue whenever indicator is above target
<State-basedTrue whenever indicator is below target
=State-basedTrue when indicator equals target (use for TTM Trend only)
cross_aboveEvent-basedFires once when indicator crosses from below to above target
cross_belowEvent-basedFires once when indicator crosses from above to below target

Period Constraints

  • Minimum period: 1
  • Maximum period: 200
  • Invalid period (0 or >200): validation error

Tip

Shorter periods react faster but generate more false signals. Longer periods are smoother but lag behind price. Choose based on your strategy's timeframe and risk tolerance.

SMA -- Simple Moving Average

Contents


The Simple Moving Average is the most fundamental technical indicator in trading. It calculates the arithmetic mean of the last N closing prices, giving equal weight to every candle in the window. If you set the period to 50, the SMA takes the last 50 closing prices, adds them up, and divides by 50. Each new candle drops the oldest price and adds the newest.

Because every candle carries the same weight, SMA is a smooth, stable line that filters out short-term noise. The trade-off is lag -- SMA reacts slowly to sudden price changes because old prices influence the average just as much as the most recent one.

SMA(50) and SMA(200) overlaid on BTC/USDC 1h candle chart with color labels

BTC/USDC 1-hour chart with SMA(50) (orange) and SMA(200) (blue). The faster SMA(50) reacts more quickly to price changes while the slower SMA(200) shows the long-term trend.


Format

sma_{period}

The period must be an integer between 1 and 200.

Examples:

TOML valueMeaning
sma_2020-period Simple Moving Average
sma_5050-period Simple Moving Average
sma_200200-period Simple Moving Average

Common Periods

PeriodStyleTypical Use
sma_20Short-termTracks recent momentum. On daily candles this covers roughly one trading month. Good for timing entries in active markets.
sma_50Medium-termThe "workhorse" moving average. Widely watched by traders as a dynamic support/resistance level. A popular component of crossover strategies.
sma_200Long-termThe institutional benchmark. Price above the 200-period SMA is generally considered a bull market; below it, a bear market. Often used as a macro trend filter.

Understanding Operators with SMA

Every trigger in Botmarley compares an indicator to a target using an operator. Here is what each operator means visually on the chart when used with SMA.

> (Greater Than)

"Price is ABOVE the SMA line on the chart."

As long as the candle closes above the line, this condition is true. Think of it as "price is in bullish territory." This is a state-based operator -- it remains true on every candle where the condition holds, not just the first one.

Price above SMA(200) — bullish territory

Price candles closing above the SMA(200) line. The green zone represents "bullish territory" — every candle here makes price > sma_200 true.

# True on every candle where price closes above the SMA(200)
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "sma_200"

< (Less Than)

"Price is BELOW the SMA line on the chart."

The mirror image of >. When the candle closes below the SMA line, this condition is true. Think of it as "price is in bearish territory."

Price below SMA(200) — bearish territory

Price candles closing below the SMA(200) line. The red zone represents "bearish territory" — every candle here makes price < sma_200 true.

# True on every candle where price closes below the SMA(20)
[[actions.triggers]]
indicator = "price"
operator = "<"
target = "sma_20"

= (Equal)

Rarely useful with SMA. Since SMA values are floating-point decimals (e.g., 67,432.18), exact equality with another floating-point value almost never happens. This operator exists mainly for discrete indicators like TTM Trend. Use > or < instead when working with SMA.

cross_above

"The MOMENT the indicator line crosses from below to above the target."

On the chart, look for where two lines intersect and the indicator moves from underneath to on top. This fires once at the crossing candle, not continuously. It checks two consecutive candles: the indicator was at or below the target on the previous candle, and is above it on the current candle.

This is the famous Golden Cross when SMA(50) crosses above SMA(200) -- one of the most widely watched signals in both traditional finance and crypto.

SMA Golden Cross — SMA(50) crossing above SMA(200)

The moment SMA(50) crosses above SMA(200) — the Golden Cross. This event-based signal fires once at the crossing candle.

# Golden Cross: fires once when SMA(50) crosses above SMA(200)
[[actions.triggers]]
indicator = "sma_50"
operator = "cross_above"
target = "sma_200"
timeframe = "1d"

cross_below

"The MOMENT the indicator line crosses from above to below the target."

The opposite crossing -- from above to below. On the chart, the indicator line drops through the target line. Like cross_above, this fires once at the crossing candle.

This is the Death Cross when SMA(50) crosses below SMA(200) -- a widely watched bearish signal.

SMA Death Cross — SMA(50) crossing below SMA(200)

The moment SMA(50) crosses below SMA(200) — the Death Cross. The bearish mirror of the Golden Cross.

# Death Cross: fires once when SMA(50) crosses below SMA(200)
[[actions.triggers]]
indicator = "sma_50"
operator = "cross_below"
target = "sma_200"
timeframe = "1d"

Warning

cross_above and cross_below are event-based -- they fire only on the candle where the crossing happens. In contrast, > and < are state-based -- they fire on every candle where the condition holds. Choose the right operator for your strategy logic: use crossovers for entry signals, use comparisons for ongoing filters.


TOML Examples

Golden Cross Entry

Buy when the 50-period SMA crosses above the 200-period SMA on the daily chart. This is a classic long-term trend-following signal.

[[actions]]
type = "open_long"
amount = "200 USDC"

[[actions.triggers]]
indicator = "sma_50"
operator = "cross_above"
target = "sma_200"
timeframe = "1d"

Death Cross Exit

Sell the entire position when SMA(50) crosses below SMA(200). Even if your take-profit has not been reached, a death cross signals the uptrend may be over.

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
indicator = "sma_50"
operator = "cross_below"
target = "sma_200"
timeframe = "1d"

Price Above SMA(200) as Trend Filter

Use the 200-period SMA as a macro trend gate. Only allow entries while price is above the long-term average. This is a state-based filter that stays active as long as the condition holds. Combined with an RSI dip on a lower timeframe, this creates the classic "trend-safe DCA" pattern.

SMA(200) as trend filter — bullish zone above, bearish zone below

The SMA(200) divides the chart into a bullish zone (above) and a bearish zone (below). Only buy RSI dips when price is in the bullish zone. This multi-indicator, multi-timeframe pattern avoids buying into falling markets.

[[actions]]
type = "open_long"
amount = "100 USDC"

# Macro filter: only buy when price is above the 200 SMA
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "sma_200"
timeframe = "1d"

# Entry signal: RSI dip on lower timeframe
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "1h"

Price Drops Below Short-Term SMA

Detect when price falls below the 50-period SMA. This can be used as an early warning or exit signal on shorter timeframes.

Price dropping below SMA(50) — early warning signal

Price falling below the SMA(50) line — an early warning that the short-term trend may be weakening. This sell trigger lets you reduce exposure before a larger drop develops.

[[actions]]
type = "sell"
amount = "50%"

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "sma_20"
timeframe = "4h"

Multi-SMA Trend Confirmation

Combine multiple SMA conditions. This entry requires all three SMAs to be in the correct alignment (fast above medium above slow), confirming a strong uptrend before entering.

Multi-SMA alignment — perfect uptrend stacking

Perfect SMA alignment: price > SMA(20) > SMA(50) > SMA(200). When all three layers stack in the correct order, it confirms a strong uptrend across all timeframes.

[[actions]]
type = "open_long"
amount = "150 USDC"

# SMA(20) above SMA(50) -- short-term trend is bullish
[[actions.triggers]]
indicator = "sma_20"
operator = ">"
target = "sma_50"
timeframe = "1d"

# SMA(50) above SMA(200) -- medium-term trend is bullish
[[actions.triggers]]
indicator = "sma_50"
operator = ">"
target = "sma_200"
timeframe = "1d"

# Price above SMA(20) -- price is in the strongest zone
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "sma_20"
timeframe = "1d"

Tips

Shorter period = faster but noisier

SMA(20) reacts more quickly to price changes but generates more false signals. SMA(200) is very smooth and reliable but slow to respond. Match the period to your strategy's timeframe: use shorter periods for scalping on 5m/15m charts, longer periods for swing trading on 4h/1d charts.

SMA works best as a filter, not a standalone signal

While the Golden Cross and Death Cross are famous, relying on a single SMA crossover can produce late entries and many false signals in sideways markets. The real power of SMA is as a trend filter layered with other indicators. For example: "only buy RSI dips when price is above SMA(200)."

Use cross_above for entries, > for filters

If you want to enter on a crossover event (one-time signal), use cross_above or cross_below. If you want to gate entries behind a trend condition that stays active as long as it holds, use > or <. Mixing them up is a common mistake -- a > trigger fires on every candle, which can cause repeated entries if you do not use max_count.

SMA vs EMA

SMA gives equal weight to all candles in the window, making it smoother but slower to react. If you need faster response to price changes with less lag, consider using EMA instead. Many strategies combine both: SMA for the macro trend view and EMA for entry timing.

EMA -- Exponential Moving Average

Contents


The Exponential Moving Average is a weighted moving average that gives more weight to recent prices. Unlike SMA, which treats every candle in the window equally, EMA applies a multiplier that makes the most recent price data count more. This means EMA reacts faster to price changes, hugging the price line more closely and reducing lag.

The weighting is exponential: the most recent candle has the highest weight, the second most recent has slightly less, and so on -- with the influence of older candles decaying smoothly rather than dropping off a cliff. The formula uses a smoothing factor of 2 / (period + 1), so a 9-period EMA gives roughly 20% weight to the latest candle, while a 50-period EMA gives roughly 4%.

EMA(9) and EMA(21) overlaid on BTC/USDC 1h candle chart with color labels

BTC/USDC 1-hour chart with EMA(9) (orange) and EMA(21) (purple). The fast EMA(9) hugs price more closely and generates more frequent crossover signals than SMA pairs.


Format

ema_{period}

The period must be an integer between 1 and 200.

Examples:

TOML valueMeaning
ema_99-period Exponential Moving Average
ema_1212-period EMA (classic MACD fast line)
ema_2121-period EMA (popular scalping pair)
ema_2626-period EMA (classic MACD slow line)
ema_5050-period EMA (medium-term trend)

SMA vs EMA -- When to Use Each

Both SMA and EMA smooth price data to identify trends, but they behave differently in practice.

CharacteristicSMAEMA
WeightingEqual weight to all candlesMore weight to recent candles
LagHigher -- slow to reactLower -- reacts faster to new data
SmoothnessSmoother, fewer whipsawsMore responsive, can be choppier
False signalsFewer, but entries may be lateMore frequent, but catches moves earlier
Best forMacro trend filters (daily/weekly)Entry timing, scalping, short timeframes
Classic pairsSMA(50)/SMA(200) -- Golden/Death CrossEMA(9)/EMA(21), EMA(12)/EMA(26)

Rule of thumb: Use SMA when you want a stable, macro view of the trend (is the market bullish or bearish over weeks/months?). Use EMA when you want to time entries and exits more precisely, especially on shorter timeframes where speed matters.

Many strategies combine both: an SMA(200) as a macro trend gate with EMA crossovers for entry timing within that trend.


Understanding Operators with EMA

The operators work identically to SMA, but because EMA reacts faster, crossover signals happen earlier and more frequently than with equivalent SMA periods.

> (Greater Than)

"Price is ABOVE the EMA line on the chart."

A state-based check that remains true on every candle where the condition holds. Since EMA tracks price more closely than SMA, the line stays nearer to the candle bodies and acts as tighter dynamic support.

Price above EMA — bullish territory

Price candles closing above the EMA(21) line. The green zone represents "bullish territory" — every candle here makes price > ema_21 true.

# True on every candle where price closes above EMA(21)
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "ema_21"

< (Less Than)

"Price is BELOW the EMA line on the chart."

The bearish mirror. Because EMA is more responsive, price falling below a short-period EMA is a faster warning signal than falling below the same-period SMA.

Price below EMA — bearish territory

Price candles closing below the EMA(9) line. The red zone represents "bearish territory" — an early warning that momentum is fading.

# Price has dropped below the fast EMA -- early warning
[[actions.triggers]]
indicator = "price"
operator = "<"
target = "ema_9"
timeframe = "1h"

= (Equal)

Rarely useful with EMA. Like SMA, EMA values are floating-point decimals, and exact equality almost never occurs. Use > or < instead.

cross_above

"The MOMENT the fast EMA line crosses from below to above the slow EMA (or target)."

Because EMA reacts faster, crossovers happen sooner than with SMA. An EMA(9)/EMA(21) crossover catches trend shifts earlier than an SMA(50)/SMA(200) crossover -- but also produces more false signals in choppy, sideways markets.

EMA crossover — EMA(9) crossing above EMA(21)

EMA(9) crosses above EMA(21) — a bullish momentum shift. Notice how EMA crossovers happen earlier and more frequently than SMA crossovers.

# Fast EMA crosses above slow EMA -- bullish momentum shift
[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "ema_21"
timeframe = "1h"

cross_below

"The MOMENT the fast EMA line crosses from above to below the target."

The bearish crossover. With short-period EMAs this can fire multiple times in a ranging market, so consider adding a trend filter or max_count to prevent repeated signals.

EMA bearish crossover — EMA(9) crossing below EMA(21)

EMA(9) crosses below EMA(21) — a bearish momentum shift. The fast EMA dropping below the slow EMA signals that short-term momentum has turned negative.

# Fast EMA crosses below slow EMA -- bearish momentum shift
[[actions.triggers]]
indicator = "ema_9"
operator = "cross_below"
target = "ema_21"
timeframe = "1h"

Warning

Short-period EMA crossovers (like 9/21) are fast but noisy. In a sideways market, the two EMA lines can weave back and forth, generating multiple false crossover signals. Always consider pairing EMA crossovers with a trend filter (such as price > sma_200 or ttm_trend = 1) to avoid whipsaws.


TOML Examples

EMA Crossover Scalping

Enter when the 9-period EMA crosses above the 21-period EMA on the 5-minute chart. This is a classic short-term momentum entry for scalping strategies.

[[actions]]
type = "open_long"
amount = "50 USDC"

[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "ema_21"
timeframe = "5m"

EMA Crossover Exit

Close the position when the fast EMA crosses back below the slow EMA, signaling momentum has reversed.

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
indicator = "ema_9"
operator = "cross_below"
target = "ema_21"
timeframe = "5m"

EMA as Dynamic Support

Use the 21-period EMA as a dynamic support level. If price drops below it on the 1-hour chart, reduce your position as a precaution.

EMA as dynamic support — price bouncing off the EMA line

The EMA(21) acts as a dynamic support level. Price repeatedly bounces off this line during an uptrend. When it finally breaks below, it can signal a trend weakening.

[[actions]]
type = "sell"
amount = "25%"

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "ema_21"
timeframe = "1h"
max_count = 1

Trend-Filtered EMA Entry

Combine a macro SMA trend filter with a fast EMA crossover for entry timing. This only buys EMA crossovers when the overall market is in an uptrend.

Trend-filtered EMA entry — macro SMA gate with EMA crossover timing

Multi-timeframe confirmation: the daily SMA(200) confirms the macro uptrend (green zone), while the hourly EMA(9)/EMA(21) crossover provides the precise entry timing. This pattern avoids buying EMA crossovers in bear markets.

[[actions]]
type = "open_long"
amount = "100 USDC"

# Macro filter: only trade in confirmed uptrend
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "sma_200"
timeframe = "1d"

# Entry signal: fast EMA crossover on lower timeframe
[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "ema_21"
timeframe = "1h"

EMA + RSI Confluence Entry

Wait for the EMA crossover AND RSI to confirm oversold conditions before entering. Multiple confirmations reduce false signals.

[[actions]]
type = "open_long"
amount = "100 USDC"

# Momentum shift: EMA crossover
[[actions.triggers]]
indicator = "ema_12"
operator = "cross_above"
target = "ema_26"
timeframe = "1h"

# Confirmation: RSI was recently in oversold territory
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "40"
timeframe = "1h"

Classic MACD-Style Crossover

The MACD indicator is built from EMA(12) and EMA(26). You can replicate MACD-like signals directly with EMA crossovers, giving you visual confirmation on the price chart itself rather than a separate oscillator panel.

[[actions]]
type = "open_long"
amount = "150 USDC"

# This is equivalent to watching the MACD line cross above zero
[[actions.triggers]]
indicator = "ema_12"
operator = "cross_above"
target = "ema_26"
timeframe = "4h"

Tips

EMA(9)/EMA(21) is the go-to scalping pair

The 9/21 EMA combination is one of the most widely used short-term crossover pairs. It generates signals fast enough for scalping on 5m and 15m charts while filtering out some of the candle-to-candle noise. For day trading on 1h charts, consider 12/26 instead for slightly smoother signals.

Use EMA for entries, SMA for macro trend

A powerful pattern is to use SMA(200) on the daily chart as a bull/bear market filter, then use EMA crossovers on lower timeframes (1h or 4h) for actual entry timing. This gives you reliable trend identification with responsive entry signals.

Prevent whipsaws with max_count

In choppy markets, EMA crossovers can fire repeatedly as the lines weave back and forth. Add max_count = 1 to a trigger if you only want it to fire once per position. This prevents your strategy from opening and closing positions on every minor crossover.

Match EMA period to your timeframe

Short EMAs (9, 12) work best on short timeframes (5m, 15m, 1h). Medium EMAs (21, 50) suit 1h and 4h charts. Using a very short EMA on a daily chart produces too few data points per crossover, while a long EMA on a 1m chart barely moves. Match the period length to the timeframe for meaningful signals.

EMA vs SMA for the same period

An EMA(50) and SMA(50) over the same data will have different values. The EMA will be closer to the current price because of its recency bias. In a strong uptrend, EMA(50) will be above SMA(50); in a strong downtrend, EMA(50) will be below SMA(50). Neither is "better" -- they serve different purposes.

RSI -- Relative Strength Index

Contents


Overview

The Relative Strength Index (RSI) is a momentum oscillator that measures the speed and magnitude of recent price changes. It answers one question: how strong is the current move compared to recent moves?

RSI is calculated by comparing the average size of recent gains to the average size of recent losses over a lookback period. The result is normalized to a scale of 0 to 100:

  • RSI near 100 means almost all recent candles were bullish (strong upward momentum).
  • RSI near 0 means almost all recent candles were bearish (strong downward momentum).
  • RSI near 50 means gains and losses are roughly equal (no clear momentum).

The standard RSI period is 14, which looks at the last 14 candles. Shorter periods make RSI more reactive; longer periods make it smoother.

RSI(14) shown as oscillator below BTC/USDC 1h candle chart with zone labels

BTC/USDC 1-hour chart with RSI(14) displayed as a separate oscillator below the price chart. The horizontal lines at 70 (overbought) and 30 (oversold) mark the classic trading zones.


Format

rsi_{period}

The period defines how many candles RSI looks back to calculate average gains and losses.

ExamplePeriodUse Case
rsi_77Fast, reactive -- good for scalping on low timeframes
rsi_1414Standard -- the default for most strategies
rsi_2121Slower, smoother -- fewer false signals

Period range: 1 to 200.

Value range: 0 to 100 (always).


RSI Zones

RSI RangeZoneInterpretation
Below 30OversoldPrice has dropped sharply relative to recent history. Selling pressure may be exhausted. Potential buy opportunity.
30 -- 50Weak / NeutralBelow the midpoint. Momentum is bearish-leaning or undecided.
50 -- 70BullishAbove the midpoint. Momentum favors buyers. In a healthy uptrend, RSI typically stays in this range.
Above 70OverboughtPrice has risen sharply relative to recent history. Buying pressure may be exhausted. Potential sell opportunity or caution zone.

Note

"Oversold" and "overbought" do not mean price must reverse. In a strong trend, RSI can stay oversold or overbought for extended periods. RSI below 30 during a crash can stay there for days. Always combine RSI with other confirmations.


Understanding Operators with RSI

Each operator behaves differently with RSI. Understanding the distinction is critical for building strategies that fire when you intend.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where RSI is below the threshold.

On the chart: Imagine the RSI line sitting below the horizontal 30 line. For as long as the line stays below 30, this trigger is active. If RSI stays below 30 for eight consecutive candles, the trigger fires on all eight.

RSI dropping into the oversold zone below 30

RSI(14) dipping below the 30 threshold — the oversold zone highlighted in red. While RSI remains below 30, the < operator fires on every candle.

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

Warning

Because < fires on every qualifying candle, an "open_long" action with rsi_14 < 30 could attempt to open a new position on each candle where RSI remains below 30. Use max_count to limit this, or use cross_below if you only want the initial dip.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where RSI is above the threshold.

On the chart: The RSI line is above the horizontal 70 line. Every candle it stays there, this trigger fires.

RSI rising into the overbought zone above 70

RSI(14) pushing above the 70 threshold — the overbought zone highlighted in orange. This is where selling pressure may start building.

[[actions.triggers]]
indicator = "rsi_14"
operator = ">"
target = "70"

Typical use: Sell when RSI is overbought. Combined with max_count = 1, it fires once when RSI first exceeds 70 and then stops.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where RSI transitions from above the threshold to below it. The previous candle had RSI >= the target, and the current candle has RSI < the target.

On the chart: Picture the RSI line descending through the 70 line. The moment it pierces the line from above to below, this trigger fires -- and then goes silent until the next crossing.

RSI crossing below 70 — leaving overbought territory

RSI(14) crossing below the 70 threshold — the moment momentum starts fading. This event-based signal fires once at the crossing candle.

[[actions.triggers]]
indicator = "rsi_14"
operator = "cross_below"
target = "70"

Typical use: Detect when RSI "leaves overbought territory." This is often a stronger sell signal than simply being above 70, because it captures the turning point -- the moment momentum starts fading.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where RSI transitions from below the threshold to above it. The previous candle had RSI <= the target, and the current candle has RSI > the target.

On the chart: The RSI line climbing up through the 30 line. The moment it crosses from below to above, the trigger fires once.

RSI crossing above 30 — recovery from oversold

RSI(14) crossing above the 30 threshold — recovery from oversold. Rather than buying while RSI is still falling, this waits for the first sign of recovery.

[[actions.triggers]]
indicator = "rsi_14"
operator = "cross_above"
target = "30"

Typical use: Detect when RSI "recovers from oversold." Rather than buying while RSI is still falling, this waits for the first sign of recovery -- RSI turning back up through 30.

Why: RSI produces floating-point values like 29.87 or 70.12. Exact equality (rsi_14 = 30) will almost never be true. Use < or > instead.

Choosing the right operator

  • Use < / > when you want to act while RSI is in a zone (state-based). Add max_count to limit repeated firing.
  • Use cross_above / cross_below when you want to act at the moment RSI enters or leaves a zone (event-based). No max_count needed -- crossovers fire once by nature.

TOML Examples

Buy on Oversold RSI

The simplest RSI strategy. Enter when RSI drops below 30 on the 1-hour chart.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

Sell on Overbought RSI

Exit when RSI exceeds 70. The max_count = 1 ensures this only fires once per position.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "70"
  max_count = 1

Buy on RSI Recovery (Cross Above 30)

Instead of buying while RSI is still falling, wait for the first sign of recovery.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_above"
  target = "30"
  timeframe = "1h"

Sell on Leaving Overbought (Cross Below 70)

Sell at the moment RSI drops back below 70 -- the overbought momentum is fading.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_below"
  target = "70"
  timeframe = "1h"

StochRSI Extreme Dip

StochRSI is more sensitive than regular RSI and reaches extremes more often. This catches deep short-term dips.

[[actions]]
type = "open_long"
amount = "50 USDC"

  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = "<"
  target = "10"
  timeframe = "5m"
  max_count = 1

RSI with Trend Filter

Only buy RSI dips when the macro trend is bullish (daily SMA 50 above SMA 200).

RSI with trend filter — only buy dips in confirmed uptrends

Multi-timeframe confirmation: the daily SMA crossover confirms the macro uptrend, while the hourly RSI catches dip entries within that trend. This "trend-safe DCA" pattern avoids buying into falling markets.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Tips

Period selection

Shorter periods (7, 9) make RSI oscillate faster between extremes. You get more signals, but more of them are false. Best suited for scalping on lower timeframes (1m, 5m).

Longer periods (21, 28) smooth out RSI and produce fewer, more reliable signals. Better for swing trading on higher timeframes (4h, 1d).

RSI(14) is the standard middle ground. Start here and adjust based on backtest results.

Combine with trend filters

RSI works best as a timing tool within a confirmed trend, not as a standalone signal. RSI < 30 in a strong downtrend is often a trap -- price can stay oversold for days. Pair RSI entries with a trend filter like sma_50 > sma_200 on the daily chart to avoid buying into falling markets.

Two-stage exits

A powerful pattern is to sell in stages: sell 50% at RSI > 50 (partial profit) and sell the remaining 100% at RSI > 70 (full exit). This captures more upside on strong bounces while locking in some profit early.

RSI vs. StochRSI

If you find RSI(14) too slow to reach extremes, try stoch_rsi_14. StochRSI applies the stochastic formula to RSI values, making it far more volatile -- it frequently hits 0 and 100. Use StochRSI for catching short-term dips on low timeframes; use RSI for broader momentum reads.

MACD -- Moving Average Convergence Divergence

Contents


Overview

MACD is a trend-following momentum indicator that reveals the relationship between two exponential moving averages of price. It answers the question: is short-term momentum accelerating or decelerating relative to the longer-term trend?

MACD produces three components that work together:

  1. MACD Line -- the difference between the 12-period EMA and the 26-period EMA. When the fast EMA pulls away from the slow EMA, the MACD line moves further from zero. When they converge, it moves toward zero.
  2. Signal Line -- a 9-period EMA of the MACD line itself. It acts as a smoothed version of the MACD line and is used for crossover signals.
  3. Histogram -- the difference between the MACD line and the signal line, displayed as bars. It visualizes how far apart the two lines are and how quickly they are converging or diverging.

The interplay between these three components gives traders a rich view of momentum, trend direction, and potential turning points.

MACD indicator below BTC/USDC 1h candle chart with component labels

BTC/USDC 1-hour chart with MACD displayed below. The MACD line (cyan), signal line (pink), and histogram (green bars) show momentum and trend direction. The zero line separates bullish (above) from bearish (below) territory.


Components

ComponentTOML FormatCalculationWhat It Shows
MACD Linemacd_lineEMA(12) minus EMA(26)Direction and strength of short-term momentum relative to the longer-term trend. Positive = bullish, negative = bearish.
Signal Linemacd_signal9-period EMA of macd_lineA smoothed lagging reference for the MACD line. Crossovers between these two lines generate buy/sell signals.
Histogrammacd_histogrammacd_line minus macd_signalThe gap between the MACD and signal lines. Growing histogram = momentum increasing. Shrinking histogram = momentum fading.

Note

MACD uses fixed parameters (12/26/9). You cannot customize the EMA periods. The format is always macd_line, macd_signal, or macd_histogram -- there is no period suffix.

Value range: MACD values are unbounded. They can be positive or negative, and the magnitude depends on the asset's price. For BTC, MACD values might range from -500 to +500 on hourly candles. The zero line is the critical reference point: MACD above zero means the 12-period EMA is above the 26-period EMA (bullish); below zero means bearish.


Reading the MACD Chart

Understanding how the three components relate visually is key to using MACD effectively.

MACD Line above Signal Line:

  • The histogram bars are positive (above the zero line).
  • Momentum is bullish -- short-term price action is outpacing the smoothed average.

MACD Line below Signal Line:

  • The histogram bars are negative (below the zero line).
  • Momentum is bearish -- short-term price action is lagging behind the smoothed average.

MACD Line above Zero:

  • The 12-period EMA is above the 26-period EMA.
  • The overall short-to-medium-term trend is bullish.

MACD Line below Zero:

  • The 12-period EMA is below the 26-period EMA.
  • The overall short-to-medium-term trend is bearish.

Histogram growing (bars getting taller):

  • The MACD line is pulling away from the signal line.
  • Momentum is accelerating in the current direction.

Histogram shrinking (bars getting shorter):

  • The MACD line is moving toward the signal line.
  • Momentum is decelerating. A crossover may be approaching.

MACD histogram showing bullish and bearish zones

The MACD histogram visualizes momentum direction. Green bars above zero = bullish momentum. Red bars below zero = bearish momentum. Watch for bars shrinking as an early warning of a crossover.


Understanding Operators with MACD

cross_above -- Event-Based

This is the most important MACD operator. It fires at the exact candle where one component crosses above another.

MACD Line crosses above Signal Line (Bullish Crossover):

On the chart, look for where the blue MACD line crosses above the orange signal line. At that moment, the histogram flips from negative to positive. This is the classic MACD buy signal -- short-term momentum has turned bullish.

MACD bullish crossover — MACD line crossing above signal line

The MACD line (cyan) crosses above the signal line (pink) — a bullish crossover. The histogram flips from negative to positive at this moment.

[[actions.triggers]]
indicator = "macd_line"
operator = "cross_above"
target = "macd_signal"
timeframe = "1h"

MACD Line crosses above the Zero Line:

On the chart, the MACD line moves from below zero to above zero. This is a stronger confirmation than the signal crossover -- it means the 12-period EMA has actually crossed above the 26-period EMA. The trend itself has shifted bullish, not just the momentum.

[[actions.triggers]]
indicator = "macd_line"
operator = "cross_above"
target = "0"
timeframe = "4h"

cross_below -- Event-Based

The mirror of cross_above. Fires at the moment of a downward crossing.

MACD Line crosses below Signal Line (Bearish Crossover):

The MACD line dips below the signal line. The histogram flips negative. Momentum has turned bearish.

MACD bearish crossover — MACD line crossing below signal line

The MACD line crosses below the signal line — a bearish crossover. Momentum has turned negative and the histogram bars flip to the downside.

[[actions.triggers]]
indicator = "macd_line"
operator = "cross_below"
target = "macd_signal"
timeframe = "1h"

MACD Line crosses below Zero:

The trend itself has shifted bearish. The 12-period EMA has dropped below the 26-period EMA.

[[actions.triggers]]
indicator = "macd_line"
operator = "cross_below"
target = "0"
timeframe = "4h"

> (Greater Than) -- State-Based

True on every candle where the condition holds. Useful for trend filters rather than entry signals.

MACD Line above Zero = Bullish bias:

MACD above zero — bullish trend bias

MACD line above the zero line — the 12-period EMA is above the 26-period EMA, confirming a bullish bias. Use this as a trend filter for other entry triggers.

[[actions.triggers]]
indicator = "macd_line"
operator = ">"
target = "0"

This fires on every candle where MACD is positive. Use it as a filter alongside other entry triggers -- "only buy when the overall MACD trend is bullish."

Histogram above Zero = Bullish momentum:

[[actions.triggers]]
indicator = "macd_histogram"
operator = ">"
target = "0"

The histogram is positive whenever the MACD line is above the signal line. Using > with the histogram is equivalent to saying "MACD momentum is currently bullish."

< (Less Than) -- State-Based

True on every candle where the condition holds.

MACD Line below Zero = Bearish bias:

MACD below zero — bearish trend bias

MACD line below the zero line — bearish bias. The 12-period EMA is below the 26-period EMA, confirming a downtrend.

[[actions.triggers]]
indicator = "macd_line"
operator = "<"
target = "0"

Histogram below Zero = Bearish momentum:

[[actions.triggers]]
indicator = "macd_histogram"
operator = "<"
target = "0"

Crossovers vs. state operators

Use cross_above / cross_below for entry and exit signals -- they fire once at the turning point. Use > / < as filters that confirm the current trend direction for another trigger. For example: "buy when RSI < 30 AND macd_line > 0" uses MACD as a bullish trend gate.


TOML Examples

Bullish Crossover Entry

Enter when the MACD line crosses above the signal line on the 1-hour chart. This is the most common MACD strategy.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_above"
  target = "macd_signal"
  timeframe = "1h"

Bearish Crossover Exit

Exit the position when MACD momentum reverses. This acts as a dynamic stop-loss -- instead of a fixed percentage, you exit when the momentum signal that justified your entry flips bearish.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_below"
  target = "macd_signal"
  timeframe = "1h"

Zero-Line Cross Entry

A more conservative entry than the signal crossover. Waits for the MACD line to cross above zero, confirming that the 12-period EMA is above the 26-period EMA.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_above"
  target = "0"
  timeframe = "4h"

Histogram Turns Positive

Buy when the MACD histogram crosses above zero. This is equivalent to the MACD line crossing above the signal line but expressed through the histogram component.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_histogram"
  operator = "cross_above"
  target = "0"
  timeframe = "1h"

MACD as Trend Filter with RSI Entry

Use MACD above zero as a bullish trend gate, and RSI for timing the entry. This avoids buying RSI dips in a bearish trend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = ">"
  target = "0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

Full MACD Strategy (Entry + Exit)

Enter on bullish crossover, exit on bearish crossover or fixed profit target -- whichever comes first.

[meta]
name = "MACD_Momentum"
description = "Enter on MACD bullish cross, exit on bearish cross or +3%"

# ENTRY
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_above"
  target = "macd_signal"
  timeframe = "1h"

# TAKE PROFIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# MOMENTUM EXIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_below"
  target = "macd_signal"
  timeframe = "1h"

Tips

Best timeframes for MACD

MACD was designed for daily charts but works well on hourly and 4-hour candles in crypto. On very short timeframes (1m, 5m), MACD crossovers happen frequently and most are noise. Stick to 1h or higher for reliable MACD signals.

Signal crossover vs. zero-line crossover

The signal crossover (macd_line cross_above macd_signal) is the earlier, more aggressive signal. The zero-line crossover (macd_line cross_above 0) is later but confirms the actual trend shift. Use signal crossovers for faster entries and zero-line crossovers for higher-confidence entries.

Watch the histogram for early warnings

The histogram starts shrinking before a crossover happens. If you see the histogram bars getting shorter, momentum is fading -- a crossover (and potential exit signal) may be approaching. This gives you a visual heads-up before the signal fires.

MACD whipsaws in ranging markets

MACD performs poorly in sideways, choppy markets. The MACD line oscillates around zero, generating frequent false crossovers. If you see the MACD line hugging the zero line with tiny crossovers, the market is likely ranging -- consider pausing the strategy or adding a volatility filter (e.g., ATR above a threshold).

MACD is a lagging indicator

Because MACD is built from EMAs, it inherently lags price. A MACD bullish crossover confirms that a move has already started -- it does not predict the future. By the time MACD signals "buy," price may have already moved 2-5% off the bottom. Accept this lag as the cost of confirmation, or combine MACD with a leading indicator like RSI for earlier entries.

Bollinger Bands

Contents


Overview

Bollinger Bands are a volatility-based indicator that creates a dynamic envelope around price. The indicator consists of three lines:

  • Middle band -- a 20-period Simple Moving Average (SMA) of the closing price.
  • Upper band -- the middle band plus 2 standard deviations.
  • Lower band -- the middle band minus 2 standard deviations.

Because the bands are derived from standard deviation, they automatically widen when the market is volatile and narrow (squeeze) when the market is calm. Roughly 95% of price action stays within the bands under normal conditions, so a move outside them is statistically significant.

Bollinger Bands squeeze — bands narrowing during low volatility

A Bollinger Squeeze — the bands narrow when volatility drops. This compression often precedes a strong breakout move in either direction.

Bollinger Bands overlaid on BTC/USDC 1h candle chart with band labels

BTC/USDC 1-hour chart with Bollinger Bands. The upper band (bb_upper), middle band (SMA 20), and lower band (bb_lower) create a dynamic envelope around price. Notice how the bands widen during volatile periods and squeeze during consolidation.


Components

ComponentTOML FormatDescription
Upper Bandbb_upperMiddle band + 2 standard deviations. Acts as dynamic resistance.
Middle Bandbb_middle20-period SMA. The "fair value" center line.
Lower Bandbb_lowerMiddle band - 2 standard deviations. Acts as dynamic support.

All three components can be used as either the indicator or the target in a trigger.

Note

Bollinger Bands use fixed parameters: 20-period lookback and 2 standard deviations. You cannot change these values -- the format is always bb_upper, bb_middle, or bb_lower.


Understanding Operators with Bollinger Bands

Bollinger Bands are price-level indicators, so they pair naturally with comparison and crossover operators. Here is how each operator behaves in context:

< -- Price Below a Band

price < bb_lower

Price drops below the lower band. On the chart, the candle closes below the bottom line of the envelope. This is the classic Bollinger Bounce buy signal -- price has moved more than 2 standard deviations below the mean and is statistically likely to revert.

Price touching the lower Bollinger Band

Price dropping to touch the lower Bollinger Band — a potential mean-reversion buy signal. The upper and lower bands define the 2-standard-deviation envelope.

This is a state-based operator. It fires on every candle where price remains below the band. Use max_count if you only want it to fire once per position.

> -- Price Above a Band

price > bb_upper

Price breaks above the upper band. This can signal either a breakout (strong momentum continuing upward) or an overbought condition (price extended too far and likely to pull back). Context matters -- in a strong uptrend, price can ride the upper band for extended periods.

Price breaking above the upper Bollinger Band

Price extending above the upper Bollinger Band — overbought territory. The red zone highlights where price has moved more than 2 standard deviations above the mean.

cross_below -- Crossing Below a Band

price cross_below bb_lower

Price crosses from above to below the lower band. This is an event-based operator -- it fires exactly once at the moment the crossing happens. On the previous candle, price was at or above bb_lower; on the current candle, price is below it.

Price crossing below the lower Bollinger Band

The moment price breaks below the lower Bollinger Band — an event-based signal that fires once at the crossing candle. Use this instead of < when you want the precise breakout moment.

Use this instead of < when you want to catch the precise moment price breaks below the band rather than the sustained condition of being below it.

cross_above -- Crossing Above a Band

price cross_above bb_middle

Price crosses from below to above the middle band. This is a mean reversion confirmation signal -- after a dip below the lower band, price crossing back above the middle band confirms that the reversion is underway. This is an event-based operator and fires once at the crossing.

Price crossing above the middle Bollinger Band

Price crossing back above the middle Bollinger Band (SMA 20) — mean reversion confirmed. After touching the lower band, this crossing signals the recovery is underway.

Tip

The direction you read a trigger matters. bb_lower > price is equivalent to price < bb_lower -- both mean "price is below the lower Bollinger Band." Botmarley reads left-to-right: indicator operator target.


TOML Examples

Buy When Price Drops Below Lower Band

The classic Bollinger Bounce entry. Price below the lower band means it is extended more than 2 standard deviations from the mean.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "bb_lower"
timeframe = "1h"

Alternative Form -- Lower Band Above Price

This is identical in meaning to the example above but written with the indicator and target swapped:

[[actions.triggers]]
indicator = "bb_lower"
operator = ">"
target = "price"
timeframe = "1h"

Sell When Price Breaks Above Upper Band

Price above the upper band may indicate an overbought condition -- a good time to take profit.

[[actions]]
type = "sell"
amount = "50%"

[[actions.triggers]]
indicator = "price"
operator = ">"
target = "bb_upper"
timeframe = "1h"

Mean Reversion -- Price Crosses Back Above Middle Band

After a dip below the lower band, wait for price to cross back above the middle band before entering. This confirms the reversion is in progress rather than catching a still-falling knife.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "price"
operator = "cross_above"
target = "bb_middle"
timeframe = "1h"

Bollinger Bounce with RSI Confirmation

Combine Bollinger Bands with RSI to filter out false signals. Only buy when price is below the lower band AND RSI confirms oversold conditions.

Bollinger Bands with RSI confirmation — multi-indicator buy signal

Multi-indicator confirmation: price below the lower Bollinger Band AND RSI below 35. Combining two independent signals filters out false positives and increases the reliability of the entry.

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "bb_lower"
timeframe = "1h"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "1h"

Tips

Bollinger Bounce vs Bollinger Breakout

There are two opposing strategies with Bollinger Bands:

  • Bollinger Bounce -- buy when price touches the lower band, expecting a reversion to the middle. Works best in ranging, sideways markets.
  • Bollinger Breakout -- buy when price breaks above the upper band, expecting momentum to continue. Works best in trending markets.

Backtest both approaches on your target pair to see which fits the market regime.

Combine with Volume

A Bollinger Band breakout is more meaningful when accompanied by a volume spike. Add a volume confirmation trigger (e.g., vol_sma_20) to filter out low-conviction breakouts.

StochRSI -- Stochastic RSI

Contents


Overview

Stochastic RSI (StochRSI) applies the Stochastic oscillator formula to RSI values instead of raw price. The result is an indicator that is significantly more sensitive than standard RSI -- it reaches extreme values (0 and 100) far more frequently.

Where RSI might hover around 40--60 for extended periods, StochRSI will swing aggressively between 0 and 100 within the same timeframe. This makes it ideal for catching short-term reversals, but it also means more false signals.

Think of it this way:

  • RSI answers: "How strong is the current momentum compared to recent history?"
  • StochRSI answers: "Where is RSI right now relative to its own recent range?"

If RSI has ranged between 35 and 65 over the last 14 candles and currently sits at 35, StochRSI would be near 0 -- even though RSI itself is not at a traditional "oversold" level. This relative measurement is what makes StochRSI more reactive.

StochRSI(14) shown as oscillator below BTC/USDC 1h candle chart with overbought/oversold zone labels


Format

stoch_rsi_{period}

The period defines the lookback window for both the underlying RSI calculation and the stochastic normalization applied on top.

ExamplePeriodUse Case
stoch_rsi_77Very fast, extremely reactive -- ideal for scalping on 1m/5m charts
stoch_rsi_1414Standard -- the most common period, balances sensitivity and reliability
stoch_rsi_2121Smoother, fewer whipsaws -- better for 15m/1h swing entries

Period range: 1 to 200.

Value range: 0 to 100 (always).


StochRSI Zones

StochRSI RangeZoneInterpretation
Below 10Deeply OversoldRSI is at the very bottom of its recent range. Strong short-term reversal candidate.
10 -- 20OversoldRSI is near the low end of its recent range. Selling pressure may be exhausting.
20 -- 80NeutralStochRSI is mid-range. No strong directional signal from this indicator alone.
80 -- 90OverboughtRSI is near the high end of its recent range. Buying pressure may be peaking.
Above 90Deeply OverboughtRSI is at the very top of its recent range. Strong short-term reversal candidate.

Note

StochRSI reaches 0 and 100 much more frequently than RSI reaches 30 and 70. A StochRSI value of 0 does not carry the same weight as an RSI of 0 (which would be extraordinary). Treat StochRSI extremes as short-term timing signals, not as strong standalone conviction signals.


Understanding Operators with StochRSI

Each operator behaves differently with StochRSI. Because StochRSI oscillates rapidly, operator choice has a major impact on signal frequency.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where StochRSI is below the threshold.

On the chart: StochRSI drops below the 10 line. For every candle it remains below 10, this trigger fires. Because StochRSI moves fast, these episodes tend to be short-lived (2--5 candles), but they can still fire multiple times.

[[actions.triggers]]
indicator = "stoch_rsi_14"
operator = "<"
target = "10"
timeframe = "5m"

Warning

StochRSI spends more time at extremes than RSI does. Even with a tight threshold like 10, you may get repeated triggers in quick succession. Always use max_count to limit entries, or switch to cross_below for a single-fire signal.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where StochRSI is above the threshold.

On the chart: StochRSI climbs above the 80 or 90 line. Every candle it stays there, this trigger fires.

[[actions.triggers]]
indicator = "stoch_rsi_14"
operator = ">"
target = "85"
timeframe = "5m"

Typical use: Exit or take profit when StochRSI signals short-term overbought conditions. Combine with max_count = 1 to fire once.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where StochRSI transitions from above the threshold to below it. The previous candle had StochRSI >= the target, and the current candle has StochRSI < the target.

On the chart: StochRSI pierces the 20 line from above to below. This single moment is the trigger -- it then goes silent until the next crossing.

[[actions.triggers]]
indicator = "stoch_rsi_14"
operator = "cross_below"
target = "20"
timeframe = "5m"

Typical use: Detect the moment StochRSI enters oversold territory. More precise than < because it fires exactly once per entry into the zone, making it useful for timing dip-buy entries without repeated signals.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where StochRSI transitions from below the threshold to above it. The previous candle had StochRSI <= the target, and the current candle has StochRSI > the target.

On the chart: StochRSI climbing up through the 20 line from below. The moment it crosses, the trigger fires once.

[[actions.triggers]]
indicator = "stoch_rsi_14"
operator = "cross_above"
target = "20"
timeframe = "5m"

Typical use: Detect when StochRSI recovers from oversold. Instead of buying while the indicator is still falling, this waits for the first sign of reversal -- StochRSI turning back up through the oversold boundary. This is often a safer entry than raw < 10.

Why: StochRSI produces floating-point values. Exact equality (e.g., stoch_rsi_14 = 50) will almost never match. Use < or > instead.

Choosing the right operator

  • Use < / > when you want to act while StochRSI is in an extreme zone (state-based). Always add max_count to limit repeated firing.
  • Use cross_above / cross_below when you want to act at the moment StochRSI enters or leaves a zone (event-based). This is often the better choice for StochRSI because it naturally fires once per event.

TOML Examples

Buy on Deeply Oversold StochRSI

Catch short-term dips when StochRSI drops below 10 on the 5-minute chart. The max_count = 1 prevents repeated entries while StochRSI remains pinned near zero.

[[actions]]
type = "open_long"
amount = "50 USDC"

  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = "<"
  target = "10"
  timeframe = "5m"
  max_count = 1

Sell on Overbought StochRSI

Take profit when short-term momentum peaks. StochRSI above 85 on the 5-minute chart signals the move may be overextended.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = ">"
  target = "85"
  timeframe = "5m"
  max_count = 1

Buy on StochRSI Recovery (Cross Above 20)

Instead of buying while StochRSI is still falling, wait for the first sign of recovery. This catches the reversal rather than the bottom.

[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = "cross_above"
  target = "20"
  timeframe = "5m"

StochRSI with RSI Filter

Combine the sensitivity of StochRSI with the broader context of RSI. Only enter when StochRSI is deeply oversold and RSI confirms the broader momentum is also depressed. This filters out the many false signals StochRSI generates in trending markets.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = "<"
  target = "10"
  timeframe = "5m"
  max_count = 1

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

Tips

StochRSI is noisier than RSI

StochRSI reaches 0 and 100 routinely -- sometimes multiple times per hour on low timeframes. By itself, a StochRSI extreme is a weak signal. Always combine it with at least one confirmation layer: a trend filter (sma_50 > sma_200), an RSI range check, or a higher-timeframe directional bias.

Period selection

stoch_rsi_7 is extremely reactive and suitable for ultra-short scalping on 1m charts. Expect many signals, most of them marginal.

stoch_rsi_14 is the standard and the best starting point for most strategies. It balances sensitivity with signal quality.

stoch_rsi_21 smooths out the noise and produces fewer, more reliable signals. Good for 15m or 1h timeframes where you want fewer entries with higher conviction.

Best timeframe pairing

StochRSI shines on low timeframes (1m, 5m, 15m) where its sensitivity catches quick reversals. On higher timeframes like 4h or 1d, regular RSI is usually sufficient because momentum shifts play out over more candles. A powerful combination is StochRSI on 5m for entry timing with RSI on 1h for directional confirmation.

ROC -- Rate of Change

Contents


Overview

Rate of Change (ROC) measures the percentage change in price over the last N candles. It answers a simple question: how much has the price moved, in percentage terms, over a fixed lookback period?

The calculation is straightforward:

ROC = ((current_price - price_N_candles_ago) / price_N_candles_ago) * 100
  • ROC of -5 means price has dropped 5% over the lookback period.
  • ROC of 3 means price has risen 3% over the lookback period.
  • ROC of 0 means price is unchanged.

Unlike bounded oscillators (RSI, StochRSI), ROC has no fixed range. It can be any positive or negative value. A flash crash could produce ROC of -20 or worse. This makes ROC uniquely suited for detecting the magnitude of price moves -- you set the threshold based on what you consider a significant move for the asset and timeframe you are trading.

ROC(14) shown as oscillator below BTC/USDC 1h candle chart with zero line label


Format

roc_{period}

The period defines how many candles back ROC compares the current price to.

ExamplePeriodUse Case
roc_55Very short lookback -- captures sudden spikes and drops within minutes
roc_1010Standard short-term -- good balance of sensitivity and reliability
roc_1414Medium lookback -- smooths out noise, shows broader momentum
roc_2020Longer lookback -- measures sustained moves, fewer false signals

Period range: 1 to 200.

Value range: Unbounded (any positive or negative number, expressed as a percentage).


ROC Interpretation

ROC RangeInterpretation
Below -5Sharp drop -- crash-level dip. Price has fallen more than 5% over the lookback period. Strong potential buy-the-dip signal.
-5 to -3Significant dip -- meaningful downward momentum. Worth monitoring or entering with confirmation.
-3 to 0Mild bearish -- price is drifting lower but not at an unusual rate.
0Unchanged -- price is exactly where it was N candles ago.
0 to 3Mild bullish -- price is drifting higher but not at an unusual rate.
3 to 5Significant pump -- meaningful upward momentum. May signal strength or overextension.
Above 5Sharp rally -- price has risen more than 5% over the lookback period. Potential overbought or breakout confirmation.

Note

The thresholds above are general guidelines. What constitutes a "significant" move depends entirely on the asset and timeframe. A 3% ROC on a 15-minute chart for BTC is a major move. A 3% ROC on a daily chart for a small-cap altcoin may be routine. Calibrate your thresholds through backtesting on the specific pair and timeframe you trade.


Understanding Operators with ROC

Each operator behaves differently with ROC. Because ROC is unbounded, careful threshold selection is critical.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where ROC is below the threshold.

On the chart: Price has been dropping. ROC dips below your threshold (say, -5). For every candle that ROC remains below -5, this trigger fires. This captures the entire duration of the dip, not just the initial plunge.

[[actions.triggers]]
indicator = "roc_10"
operator = "<"
target = "-5"
timeframe = "15m"

Warning

During a sustained crash, ROC can stay deeply negative for many candles. Using < without max_count will fire on every single one, potentially opening many positions into a falling market. Always use max_count or combine with a recovery signal.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where ROC is above the threshold.

On the chart: Price has been rising. ROC exceeds your threshold (say, 3). Every candle it stays above 3, this trigger fires.

[[actions.triggers]]
indicator = "roc_10"
operator = ">"
target = "3"
timeframe = "15m"

Typical use: Momentum confirmation for exits -- sell when upward momentum is strong. Or as a breakout filter -- only enter when price is moving decisively upward.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where ROC transitions from above the threshold to below it. The previous candle had ROC >= the target, and the current candle has ROC < the target.

On the chart: ROC was hovering above zero and suddenly drops below zero. The moment it crosses the line, the trigger fires once.

[[actions.triggers]]
indicator = "roc_10"
operator = "cross_below"
target = "0"
timeframe = "1h"

Typical use: Detect the zero-line crossing. ROC crossing below 0 means price is now lower than it was N candles ago -- momentum has turned negative. This is a clean trend-change signal without the noise of staying in a zone.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where ROC transitions from below the threshold to above it. The previous candle had ROC <= the target, and the current candle has ROC > the target.

On the chart: ROC was negative and climbs back above zero. The crossing moment is the trigger.

[[actions.triggers]]
indicator = "roc_10"
operator = "cross_above"
target = "0"
timeframe = "1h"

Typical use: Detect momentum recovery. ROC crossing above 0 means price is now higher than it was N candles ago -- momentum has turned positive. This signals the beginning of a potential upward move.

Why: ROC produces floating-point values. Exact equality (e.g., roc_10 = 0) will almost never be true because ROC values are calculated to many decimal places. Use cross_above or cross_below with a target of 0 to detect zero-line crossings instead.

Choosing the right operator

  • Use < for dip buying -- "price has dropped X%." Add max_count to avoid stacking entries during prolonged drops.
  • Use > for momentum confirmation or overbought exits.
  • Use cross_above / cross_below with target 0 for clean trend-change signals at the zero line.

TOML Examples

Buy After a Sharp Dip

Enter when price has dropped 5% over the last 10 candles on the 15-minute chart. This targets sharp, rapid dips that often precede bounces.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = "<"
  target = "-5"
  timeframe = "15m"
  max_count = 1

Sell When Momentum Weakens

Exit the position when upward momentum fades. ROC crossing below 0 means the recent rally has stalled and price is now below where it was 10 candles ago.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = "cross_below"
  target = "0"
  timeframe = "1h"

Buy on Zero-Line Recovery

Wait for ROC to cross back above zero after a dip. This confirms that the price drop is over and momentum is turning positive, rather than trying to catch a still-falling knife.

[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = "cross_above"
  target = "0"
  timeframe = "15m"

Sharp Dip with Consecutive Candle Confirmation

Combine ROC with a consecutive red candles trigger for higher-conviction dip entries. This fires only when price has dropped 3% and there have been 5 consecutive bearish candles -- confirming a real sell-off rather than a single volatile candle.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = "<"
  target = "-3"
  timeframe = "15m"
  max_count = 1

  [[actions.triggers]]
  type = "consecutive_candles"
  direction = "bearish"
  count = 5
  timeframe = "15m"

Tips

Calibrate thresholds per asset

A 5% drop in BTC over 10 candles on the 15-minute chart is a significant event. The same 5% drop for a small-cap altcoin might happen daily. Backtest your ROC thresholds on the specific trading pair to understand what constitutes a "significant" move for that asset and timeframe.

Short periods for catching momentum shifts

ROC works best on shorter periods (5--10) for dip-buying strategies because it captures rapid price changes. Longer periods (20+) smooth out the signal and are better suited for identifying broader trend shifts via zero-line crossovers.

Zero-line crossovers as trend signals

ROC crossing above zero means the asset is now trading higher than it was N candles ago. ROC crossing below zero means the opposite. These zero-line crossovers are clean, event-based trend signals that pair well with other entry conditions. Use cross_above with target 0 for bullish confirmation, cross_below with target 0 for bearish confirmation.

ROC during flash crashes

During sudden market crashes, ROC can reach extreme negative values (-10, -15, or worse). While these can be excellent dip-buy opportunities, they can also be the beginning of a much larger move. Never rely solely on ROC magnitude for entries during high-volatility events. Combine with a trend filter or wait for a cross_above 0 recovery signal before entering.

ATR -- Average True Range

Contents


Overview

Average True Range (ATR) measures the average range of price movement over the last N candles. It tells you how much the price typically moves per candle -- it measures volatility, not direction.

ATR uses "True Range" rather than simple high-low range. True Range accounts for gaps between candles by taking the maximum of:

  1. Current high minus current low
  2. Absolute value of current high minus previous close
  3. Absolute value of current low minus previous close

This ensures that a gap-up or gap-down is reflected as volatility even if the current candle's body is small.

The key insight about ATR:

  • High ATR means the market is making large moves per candle -- high volatility. More risk and more reward per trade.
  • Low ATR means the market is quiet -- low volatility. Prices are moving in small increments. Breakouts from low-ATR environments can be explosive.
  • ATR says nothing about direction. A high ATR during a crash is the same as a high ATR during a rally. It only measures the size of moves, not whether they are up or down.

ATR(14) shown as oscillator below BTC/USDC 1h candle chart with volatility label


Format

atr_{period}

The period defines how many candles ATR averages the True Range over.

ExamplePeriodUse Case
atr_77Fast, reactive -- tracks recent volatility shifts quickly
atr_1414Standard -- the most common period, smooth and reliable
atr_2020Slower -- better for filtering out short-term volatility spikes

Period range: 1 to 200.

Value range: 0 to infinity (always positive, denominated in the asset's price currency).


Understanding ATR Values

Unlike RSI or StochRSI, ATR values are not normalized to a fixed scale. They are expressed in the same units as the asset's price. This has a critical implication: ATR thresholds must be set relative to the asset and timeframe you are trading.

AssetTimeframeTypical ATR(14) RangeWhat It Means
BTC/USDC1h200 -- 800BTC typically moves $200--$800 per hour
BTC/USDC1d1,000 -- 3,000BTC typically moves $1,000--$3,000 per day
ETH/USDC1h15 -- 60ETH typically moves $15--$60 per hour
SOL/USDC1h0.50 -- 3.00SOL typically moves $0.50--$3.00 per hour

Note

The values in the table above are illustrative and will vary with market conditions. During bull runs or crisis events, ATR can be 3--5x its typical range. Always check recent ATR values for your specific trading pair before setting thresholds. Use backtesting to calibrate.


Understanding Operators with ATR

Each operator behaves differently with ATR. Because ATR is a filter indicator (it describes market conditions, not trading signals), it is almost always used in combination with other triggers.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where ATR is above the threshold.

On the chart: Volatility is elevated. The market is making large moves per candle. This trigger stays active for as long as ATR remains above the threshold.

[[actions.triggers]]
indicator = "atr_14"
operator = ">"
target = "500"
timeframe = "1h"

Typical use: "Only trade when the market is volatile enough." High ATR means there is enough price movement to make a trade worthwhile. If ATR is too low, the profit potential may not justify the spread and fees.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where ATR is below the threshold.

On the chart: Volatility is compressed. The market is quiet, moving in small increments. This is often called a "volatility squeeze."

[[actions.triggers]]
indicator = "atr_14"
operator = "<"
target = "200"
timeframe = "1h"

Typical use: Detect low-volatility environments. Extended periods of low ATR often precede explosive breakouts. Some strategies wait for a squeeze (low ATR) and then enter on a directional signal, expecting the breakout to produce outsized moves.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where ATR transitions from below the threshold to above it. The previous candle had ATR <= the target, and the current candle has ATR > the target.

[[actions.triggers]]
indicator = "atr_14"
operator = "cross_above"
target = "400"
timeframe = "1h"

Typical use: Detect the moment volatility expands. This captures the transition from a quiet market to an active one -- a potential breakout is underway. Combine with a directional indicator to determine whether to go long or short.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where ATR transitions from above the threshold to below it. The previous candle had ATR >= the target, and the current candle has ATR < the target.

[[actions.triggers]]
indicator = "atr_14"
operator = "cross_below"
target = "300"
timeframe = "1h"

Typical use: Detect the moment volatility contracts. This captures the transition from an active market to a quiet one. Can be used to exit positions if you only want to be in trades during volatile periods, or to start watching for squeeze-breakout setups.

Why: ATR produces floating-point values that are rarely exactly equal to any specific number. Use > or < instead.

Choosing the right operator

  • Use > to ensure volatility is high enough for profitable trading. This is the most common ATR operator.
  • Use < to detect low-volatility squeezes that often precede breakouts.
  • Use cross_above / cross_below to detect the exact moment a volatility regime changes.

TOML Examples

Only Trade When Volatility Is Elevated

Use ATR as a gate on an RSI dip-buy strategy. Only enter when ATR confirms the market is moving enough to make the trade worthwhile.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "atr_14"
  operator = ">"
  target = "500"
  timeframe = "1h"

Low Volatility Squeeze Detection

Identify compression periods where the market is unusually quiet. When ATR drops below the threshold and then a directional breakout occurs (price breaks above the upper Bollinger Band), enter the trade.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "atr_14"
  operator = "<"
  target = "200"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "bb_upper"
  timeframe = "1h"

ATR Filter Combined with RSI Entry

A comprehensive dip-buying strategy: only buy when RSI is oversold, ATR shows sufficient volatility (so the bounce potential is meaningful), and the daily trend is bullish.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "atr_14"
  operator = ">"
  target = "400"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Volatility Regime Change

Detect the moment volatility expands from a quiet period. When ATR crosses above the threshold, it signals that the market is waking up -- combine with momentum direction for entry.

[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "atr_14"
  operator = "cross_above"
  target = "400"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "1"
  timeframe = "1h"

Tips

ATR is NOT directional

ATR measures the size of price moves, not their direction. An ATR of 500 during a crash is the same as an ATR of 500 during a rally. Never use ATR as a buy or sell signal on its own. It is a filter -- it tells you whether conditions are right for trading, not which direction to trade.

ATR values are asset-specific

You cannot use the same ATR threshold for BTC and SOL. BTC's ATR(14) on the hourly chart might be 500, while SOL's might be 1.50. Always calibrate your ATR thresholds by checking the indicator's recent values for the specific pair and timeframe. Backtesting is the best way to find appropriate thresholds.

Volatility squeeze breakouts

One of the most powerful ATR patterns is the squeeze breakout. When ATR drops to unusually low levels (the market goes quiet), it often precedes an explosive move. Combine low ATR (atr_14 < threshold) with a directional breakout trigger (e.g., price > bb_upper or roc_10 > 2) to catch these moves early.

Use ATR to avoid low-volatility chop

In flat, choppy markets, dip-buy signals from RSI or StochRSI often lead to tiny bounces that barely cover fees. Adding an ATR floor (atr_14 > threshold) to your entry triggers ensures you only trade when price is moving enough to produce meaningful profits.

Parkinson Volatility -- Parkinson Historical Volatility Estimator

Contents


Overview

Parkinson Volatility is a historical volatility estimator that uses only the high and low prices of each candle. It was developed by physicist Michael Parkinson in 1980 as a more efficient alternative to the traditional close-to-close volatility calculation.

The formula is:

Parkinson = sqrt( (1 / (4 * N * ln(2))) * sum( ln(H_i / L_i) )^2 )

Where H_i and L_i are the high and low prices for each candle, and N is the lookback period. The result is an annualized volatility estimate.

The key insight: Parkinson volatility is approximately 5x more efficient than close-to-close volatility. "Efficient" here means it extracts more information from the same amount of data. Consider a candle with a huge wick -- price spiked up and came back down, closing near its open. A close-to-close volatility estimator sees almost no movement. Parkinson catches the full intraday range, revealing the true volatility that close-only methods miss entirely. This makes it exceptionally useful for detecting volatility squeezes and expansions in cryptocurrency markets where wicks are common.


Format

parkinson_{period}

The period defines how many candles the estimator averages over.

ExamplePeriodUse Case
parkinson_1010Short-term -- reactive to recent volatility shifts
parkinson_2020Standard -- good balance of smoothness and responsiveness
parkinson_5050Long-term -- captures the broader volatility regime

Period range: 5 to 500.

Value range: 0 to infinity (annualized volatility, always positive). Typical crypto range: 0.005 to 0.10.


Understanding Parkinson Values

Parkinson values are annualized volatility estimates expressed as decimals. They are not normalized to a fixed scale like RSI. The values depend on the asset, timeframe, and current market conditions.

Parkinson RangeInterpretation
Below 0.01Ultra-low volatility -- the market is barely moving. A squeeze is in effect. Breakout potential is high.
0.01 -- 0.03Low volatility -- calm market conditions. Good environment for squeeze-based strategies.
0.03 -- 0.06Moderate volatility -- normal trading conditions for most crypto pairs.
0.06 -- 0.10High volatility -- the market is making large intraday moves. Elevated risk and reward.
Above 0.10Extreme volatility -- crisis-level moves. Caution advised for new entries.

Note

The ranges above are general guidelines for major crypto pairs on hourly timeframes. Smaller-cap altcoins will typically show higher Parkinson values even in calm markets. Always calibrate thresholds through backtesting on your specific trading pair and timeframe.


Understanding Operators with Parkinson

Each operator behaves differently with Parkinson volatility. Because Parkinson is a filter indicator (it describes market conditions, not trading direction), it is almost always used in combination with directional triggers.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where Parkinson volatility is below the threshold.

On the chart: Volatility is compressed. Candle ranges are small and consistent. The market is in a squeeze state, which often precedes explosive breakouts.

[[actions.triggers]]
indicator = "parkinson_20"
operator = "<"
target = "0.02"
timeframe = "1h"

Typical use: Detect volatility squeezes. When Parkinson drops to unusually low levels, the market is coiling. Combine with a directional trigger (RSI dip, EMA cross, Bollinger breakout) to enter at the moment the squeeze resolves.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where Parkinson volatility is above the threshold.

On the chart: Volatility is elevated. Candles have large ranges with significant wicks. The market is making big moves.

[[actions.triggers]]
indicator = "parkinson_20"
operator = ">"
target = "0.07"
timeframe = "1h"

Typical use: High-volatility filter. Use > to either (1) avoid entering new positions during chaotic markets, or (2) only trade when volatility is high enough for profitable moves. The interpretation depends on your strategy design.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where Parkinson transitions from above the threshold to below it. The previous candle had Parkinson >= the target, and the current candle has Parkinson < the target.

[[actions.triggers]]
indicator = "parkinson_20"
operator = "cross_below"
target = "0.03"
timeframe = "1h"

Typical use: Detect the moment the market enters a squeeze. Volatility has just dropped below your calm-market threshold. This signals the beginning of a compression phase -- start watching for directional breakout triggers.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where Parkinson transitions from below the threshold to above it. The previous candle had Parkinson <= the target, and the current candle has Parkinson > the target.

[[actions.triggers]]
indicator = "parkinson_20"
operator = "cross_above"
target = "0.05"
timeframe = "1h"

Typical use: Detect the moment volatility expands. The market was quiet and has just started making larger moves. This often marks the beginning of a trending phase -- combine with a momentum indicator to determine direction.

Why: Parkinson produces floating-point values calculated to many decimal places. Exact equality will almost never be true. Use < or > instead.

Choosing the right operator

  • Use < to detect volatility squeezes -- the most common Parkinson operator. Low Parkinson = calm market = breakout potential.
  • Use > to filter out chaotic markets or to require sufficient volatility for profitable trades.
  • Use cross_below / cross_above to detect the exact moment a volatility regime changes.

TOML Examples

Low Volatility Squeeze with Bullish RSI

When Parkinson volatility is ultra-low (squeeze) and RSI signals oversold conditions, enter long. The logic: a quiet market with a dip often precedes a strong bounce.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "parkinson_20"
  operator = "<"
  target = "0.02"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

High Volatility Filter -- Avoid Entry

Use Parkinson as a safety gate. Only enter when volatility is not extreme. This protects against buying into violent, unpredictable markets.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_above"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "parkinson_20"
  operator = "<"
  target = "0.08"
  timeframe = "1h"

Squeeze Breakout with Trend Confirmation

Wait for the market to be in a volatility squeeze, then enter when price breaks above the upper Bollinger Band with a bullish daily trend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "parkinson_20"
  operator = "<"
  target = "0.025"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "bb_upper"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Volatility Compression DCA

During ultra-calm markets, dollar-cost average into positions. When the squeeze resolves, you will have accumulated at compressed prices.

[[actions]]
type = "open_long"
amount = "50 USDC"

  [[actions.triggers]]
  indicator = "parkinson_50"
  operator = "<"
  target = "0.015"
  timeframe = "4h"
  max_count = 3

Tips

More sensitive than ATR to intraday range

Parkinson uses the full high-low range of each candle, making it more sensitive to wick-heavy price action than ATR. In crypto markets where candles frequently have large wicks that close near the open, Parkinson reveals the true intraday volatility that ATR and close-to-close methods underestimate. If you see calm ATR but elevated Parkinson, the market is quietly volatile -- large moves are happening within candles but not persisting across them.

Asset-specific thresholds are essential

Parkinson values vary enormously between assets. BTC/USDC on the 1-hour chart might have a typical Parkinson of 0.03, while a small-cap altcoin might sit at 0.08 in the same conditions. Always backtest on your specific pair and timeframe to determine what constitutes "low" and "high" volatility. Never copy thresholds from one asset to another without recalibrating.

Compare with GK and YZ for the full picture

Parkinson uses only high and low prices. Garman-Klass adds open and close information. Yang-Zhang adds overnight drift. Using all three together gives you a comprehensive volatility profile. If all three estimators agree that volatility is low, you have high-confidence squeeze detection. If they diverge, the type of divergence reveals what kind of volatility the market is experiencing.

Parkinson assumes no drift

Parkinson volatility assumes that the underlying asset has zero drift (no trending behavior). In a strongly trending market, Parkinson can underestimate true volatility because the consistent directional movement is not fully captured by the high-low range alone. For trending markets, consider using Yang-Zhang volatility which accounts for drift.

GK Vol -- Garman-Klass Volatility Estimator

Contents


Overview

Garman-Klass Volatility is the most efficient single-estimator for historical volatility because it uses all four OHLC prices -- open, high, low, and close -- from each candle. Developed by Mark Garman and Michael Klass in 1980, it extracts the maximum amount of volatility information from standard OHLC data.

The formula incorporates two components:

GK = sqrt( (1/N) * sum( 0.5 * ln(H/L)^2 - (2*ln(2) - 1) * ln(C/O)^2 ) )

The first term ln(H/L)^2 captures the intraday range (similar to Parkinson). The second term ln(C/O)^2 captures the open-to-close movement. By combining both, Garman-Klass achieves approximately 7.4x the efficiency of close-to-close volatility -- meaning it produces equally accurate estimates with far fewer data points.

This makes Garman-Klass the gold standard among single-estimator volatility measures. It is strictly better than Parkinson because it uses additional information (open and close) at no extra cost. The only limitation it does not address is overnight gaps -- the drift between one candle's close and the next candle's open. For that, you need Yang-Zhang volatility.


Format

gk_vol_{period}

The period defines how many candles the estimator averages over.

ExamplePeriodUse Case
gk_vol_1010Short-term -- tracks recent volatility changes quickly
gk_vol_2020Standard -- the recommended default for most strategies
gk_vol_5050Long-term -- captures the broader volatility regime, very smooth

Period range: 5 to 500.

Value range: 0 to infinity (annualized volatility, always positive). Typical crypto range: 0.005 to 0.15.


Understanding GK Vol Values

Like Parkinson, GK Vol values are annualized volatility estimates expressed as decimals. They are not bounded to a fixed range. Values depend on the asset, timeframe, and market conditions.

GK Vol RangeInterpretation
Below 0.01Ultra-low volatility -- extreme squeeze. The market is barely moving on any dimension (range or body). Strong breakout potential.
0.01 -- 0.04Low volatility -- calm, orderly market. Ideal environment for squeeze-based entry strategies.
0.04 -- 0.08Moderate volatility -- normal trading conditions for major crypto pairs. Healthy for trend-following strategies.
0.08 -- 0.15High volatility -- large moves with significant candle bodies and wicks. Elevated risk per trade.
Above 0.15Extreme volatility -- crisis or euphoria. Exercise extreme caution with new entries.

Note

GK Vol tends to produce slightly higher values than Parkinson because it incorporates the open-close component. Do not use Parkinson thresholds for GK Vol directly -- recalibrate through backtesting. The ranges above are approximate for major crypto pairs on hourly timeframes.


Understanding Operators with GK Vol

Each operator behaves differently with GK Vol. As a filter indicator measuring market conditions rather than direction, GK Vol is almost always combined with directional triggers.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where GK Vol is below the threshold.

On the chart: Volatility is compressed across all dimensions -- small wicks and small candle bodies. The market is coiling. This state-based trigger stays active for as long as GK Vol remains below the threshold.

[[actions.triggers]]
indicator = "gk_vol_20"
operator = "<"
target = "0.03"
timeframe = "1h"

Typical use: Squeeze detection. When GK Vol is low, both the range and the body of candles are small. This is an even more comprehensive squeeze signal than Parkinson alone, because it confirms that neither the wicks nor the closes are showing significant movement.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where GK Vol is above the threshold.

On the chart: Volatility is elevated. Candles have large bodies, large wicks, or both. The market is making decisive moves.

[[actions.triggers]]
indicator = "gk_vol_20"
operator = ">"
target = "0.08"
timeframe = "1h"

Typical use: High-volatility filter. Either avoid entries during chaotic conditions, or require that enough volatility exists for trades to be profitable after fees and slippage.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where GK Vol transitions from above the threshold to below it. The previous candle had GK Vol >= the target, and the current candle has GK Vol < the target.

[[actions.triggers]]
indicator = "gk_vol_20"
operator = "cross_below"
target = "0.04"
timeframe = "1h"

Typical use: Detect the transition into a low-volatility regime. The market has just calmed down after a volatile period. This can signal the beginning of a consolidation phase that will eventually resolve with a breakout.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where GK Vol transitions from below the threshold to above it. The previous candle had GK Vol <= the target, and the current candle has GK Vol > the target.

[[actions.triggers]]
indicator = "gk_vol_20"
operator = "cross_above"
target = "0.06"
timeframe = "1h"

Typical use: Detect the moment volatility expands. This captures the exact transition from a quiet market to an active one. A volatility expansion often marks the start of a new trend -- combine with a directional indicator (RSI, ROC, EMA cross) to determine which side to trade.

Why: GK Vol produces floating-point values calculated to many decimal places. Exact equality will almost never be true. Use < or > instead.

Choosing the right operator

  • Use < for squeeze detection -- low GK Vol means the market is quiet on all fronts.
  • Use > to filter for sufficient volatility or to block entries during chaos.
  • Use cross_below / cross_above to detect the exact moment a volatility regime shifts.

TOML Examples

Moderate Volatility with Trend Entry

Enter when GK Vol shows the market is in a healthy range (not too quiet, not too chaotic) and the EMA signals a bullish trend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = ">"
  target = "0.02"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = "<"
  target = "0.08"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_21"
  operator = ">"
  target = "ema_55"
  timeframe = "1h"

GK Vol Band Filter -- Goldilocks Zone

Only trade when volatility is in the "goldilocks zone" -- enough movement for profitable trades, but not so much that risk is uncontrollable. Combine with an RSI dip entry.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = ">"
  target = "0.025"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = "<"
  target = "0.07"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

EMA Cross with Volatility Confirmation

Only act on an EMA crossover when GK Vol confirms the market has enough energy for the trend to follow through.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "ema_9"
  operator = "cross_above"
  target = "ema_21"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = ">"
  target = "0.03"
  timeframe = "1h"

Volatility Regime Change Detection

Detect the exact moment the market transitions from low to higher volatility. When GK Vol crosses above the threshold while momentum is positive, ride the expansion.

[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = "cross_above"
  target = "0.04"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "1"
  timeframe = "1h"

Tips

Most efficient single estimator

Garman-Klass is theoretically 7.4x more efficient than close-to-close volatility, meaning it needs far fewer candles to produce an equally accurate estimate. This makes it the best single indicator for volatility measurement when you have standard OHLC data. If you can only pick one volatility estimator, GK Vol is the strongest choice.

Compare across all three volatility estimators

Running Parkinson, GK Vol, and Yang-Zhang simultaneously gives you a complete volatility picture. If all three agree that volatility is low, you have a high-confidence squeeze. If Parkinson is low but GK Vol is moderate, it means the open-close moves are significant even though the high-low range is small -- the market is trending within its range. These divergences provide additional strategic insight.

Asset-specific calibration is mandatory

Like all volatility estimators, GK Vol values differ dramatically between assets. BTC/USDC might show GK Vol of 0.04 in normal conditions, while a volatile altcoin might sit at 0.12. Never reuse thresholds across different trading pairs. Backtest on each specific pair and timeframe to determine what "low," "moderate," and "high" mean for that asset.

Does not account for overnight gaps

Garman-Klass assumes that the open of each candle equals the close of the previous candle -- it ignores any gap between them. While cryptocurrency markets trade 24/7 (minimizing true overnight gaps), there can still be significant jumps between candle closes and opens during exchange maintenance, liquidity gaps, or fast-moving markets. If gap-driven volatility is important to your strategy, use Yang-Zhang volatility which explicitly accounts for inter-candle drift.

YZ Vol -- Yang-Zhang Volatility Estimator

Contents


Overview

Yang-Zhang Volatility is the most comprehensive classical volatility estimator. Developed by Dennis Yang and Qiang Zhang in 2000, it combines three separate volatility components into a single, robust measure:

  1. Overnight volatility -- the variance of the open relative to the previous close (captures inter-candle drift).
  2. Open-to-close volatility -- the variance of the close relative to the open (captures intraday directional movement).
  3. Rogers-Satchell volatility -- a component similar to Garman-Klass that uses high, low, open, and close (captures intraday range efficiently).

The formula optimally weights these three components to minimize the estimator's variance:

YZ = sqrt( overnight_var + k * open_close_var + (1 - k) * rogers_satchell_var )

Where k is chosen to minimize the total variance of the estimator.

What makes Yang-Zhang unique is its handling of overnight drift -- the gap between one candle's close and the next candle's open. Parkinson and Garman-Klass both assume this gap is zero, which is incorrect whenever there are jumps between candles. While cryptocurrency markets trade 24/7 (no true "overnight" close), there are still meaningful open-vs-previous-close differences caused by rapid price movement between candles, exchange maintenance windows, and liquidity gaps. Yang-Zhang captures this drift, producing the most robust volatility estimate when market patterns are irregular.


Format

yz_vol_{period}

The period defines how many candles the estimator averages over.

ExamplePeriodUse Case
yz_vol_1010Short-term -- responsive to recent volatility changes
yz_vol_2020Standard -- the recommended default for most strategies
yz_vol_5050Long-term -- captures the broad volatility regime, very stable

Period range: 5 to 500.

Value range: 0 to infinity (annualized volatility, always positive). Typical crypto range: 0.005 to 0.12.


Understanding YZ Vol Values

Yang-Zhang values are annualized volatility estimates expressed as decimals. Like all volatility estimators, they are not bounded to a fixed range and depend on the asset, timeframe, and market conditions.

YZ Vol RangeInterpretation
Below 0.01Ultra-calm -- all three volatility components are minimal. Strongest possible squeeze signal.
0.01 -- 0.035Low volatility -- quiet market with minimal inter-candle drift. Excellent squeeze environment.
0.035 -- 0.07Moderate volatility -- normal conditions for major crypto pairs. Suitable for most strategies.
0.07 -- 0.12High volatility -- large moves with significant drift between candles. Elevated risk.
Above 0.12Extreme volatility -- rapid, gapped price action. Very high risk for new entries.

Note

Yang-Zhang values may differ from Parkinson and GK Vol for the same period because of the overnight component. In markets with frequent gaps between candle close and next open, YZ Vol will be higher than GK Vol. In smooth, continuous markets, they will be very similar. These differences are themselves informative -- a large gap between YZ Vol and GK Vol reveals that inter-candle drift is a significant source of volatility.


Understanding Operators with YZ Vol

Each operator behaves differently with YZ Vol. As the most comprehensive volatility filter indicator, YZ Vol is almost always combined with directional entry signals.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where YZ Vol is below the threshold.

On the chart: The market is calm on every dimension -- small wicks, small bodies, and minimal drift between candles. This is the most thorough squeeze confirmation available from a single indicator.

[[actions.triggers]]
indicator = "yz_vol_20"
operator = "<"
target = "0.025"
timeframe = "1h"

Typical use: High-confidence squeeze detection. When YZ Vol is low, it means not only are the candles small, but there is minimal jump between consecutive candle opens and prior closes. The market is truly quiet. Combine with a directional trigger to enter when the squeeze resolves.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where YZ Vol is above the threshold.

On the chart: Volatility is elevated across all components. The market is making large moves and potentially gapping between candles.

[[actions.triggers]]
indicator = "yz_vol_20"
operator = ">"
target = "0.08"
timeframe = "1h"

Typical use: Volatile market filter. Use > to avoid entering new positions when the market is chaotic, or to exit existing positions when volatility spikes to dangerous levels. YZ Vol > threshold as an exit trigger captures risk expansion that other estimators might miss.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where YZ Vol transitions from above the threshold to below it. The previous candle had YZ Vol >= the target, and the current candle has YZ Vol < the target.

[[actions.triggers]]
indicator = "yz_vol_20"
operator = "cross_below"
target = "0.04"
timeframe = "1h"

Typical use: Detect the exact moment the market settles into a calm regime. Volatility has just dropped below the threshold -- the market is transitioning from active to quiet. This begins the watch period for a squeeze breakout setup.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where YZ Vol transitions from below the threshold to above it. The previous candle had YZ Vol <= the target, and the current candle has YZ Vol > the target.

[[actions.triggers]]
indicator = "yz_vol_20"
operator = "cross_above"
target = "0.06"
timeframe = "1h"

Typical use: Detect the moment volatility expands from a calm state. The market was quiet and is now making larger, more gapped moves. This regime change often marks the beginning of a significant trending move. Combine with direction indicators to trade the expansion.

Why: YZ Vol produces floating-point values calculated to many decimal places. Exact equality will almost never be true. Use < or > instead.

Choosing the right operator

  • Use < for the most comprehensive squeeze detection available -- low YZ Vol means the market is quiet on every axis.
  • Use > to avoid chaotic markets or to trigger exits when volatility spikes.
  • Use cross_below / cross_above to detect precise volatility regime transitions.

TOML Examples

Ultra-Calm Market RSI Dip Buy

When YZ Vol confirms an ultra-calm market and RSI drops into oversold territory, buy the dip. The logic: in a truly quiet market, an RSI dip is more likely a temporary pullback than the start of a crash.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "yz_vol_20"
  operator = "<"
  target = "0.02"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

YZ Vol Spike -- Exit or Avoid

When volatility explodes above the danger threshold, exit the position. A sudden YZ Vol spike means the market has become unpredictable, with large gaps between candles.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "yz_vol_20"
  operator = "cross_above"
  target = "0.10"
  timeframe = "1h"

Triple Volatility Confirmation Squeeze

The highest-confidence squeeze setup: require all three volatility estimators (Parkinson, GK Vol, and YZ Vol) to confirm low volatility simultaneously. When all three agree, enter on a bullish RSI signal.

[[actions]]
type = "open_long"
amount = "150 USDC"

  [[actions.triggers]]
  indicator = "parkinson_20"
  operator = "<"
  target = "0.02"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "gk_vol_20"
  operator = "<"
  target = "0.025"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "yz_vol_20"
  operator = "<"
  target = "0.025"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_above"
  target = "30"
  timeframe = "1h"

Regime-Based Trend Entry

Use YZ Vol to confirm the market is in a moderate volatility regime (trending, not chaotic), then enter on an EMA crossover with a macro trend filter.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "yz_vol_20"
  operator = ">"
  target = "0.02"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "yz_vol_20"
  operator = "<"
  target = "0.07"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_9"
  operator = "cross_above"
  target = "ema_21"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Tips

Most comprehensive classical estimator

Yang-Zhang is the only classical volatility estimator that accounts for overnight (inter-candle) drift in addition to intraday range and open-close movement. This makes it the most robust single number for answering "how volatile is this market right now?" If you can only use one volatility indicator, YZ Vol gives you the most complete picture.

Accounts for inter-candle drift

Even though crypto markets trade 24/7, the open of a new candle often differs from the prior candle's close -- especially during fast-moving markets or lower-liquidity periods. Parkinson and GK Vol ignore this gap. Yang-Zhang captures it. If your strategy is sensitive to gap risk (e.g., stop-loss slippage between candles), YZ Vol is the right volatility measure to monitor.

Use for regime-based strategies

Rather than using fixed thresholds, consider building strategies around volatility regimes: low YZ Vol (squeeze) = accumulate, moderate YZ Vol = trend-follow, high YZ Vol = reduce exposure. This regime-based approach adapts naturally to changing market conditions without requiring constant threshold recalibration.

Compare all three estimators for deeper insight

Running Parkinson, GK Vol, and YZ Vol together reveals what kind of volatility the market is experiencing. If all three are similar, the market is behaving normally. If YZ Vol is significantly higher than GK Vol, inter-candle gaps are a major volatility source. If Parkinson is high but GK Vol is lower, candles have large wicks but close near their opens. These divergence patterns can inform whether to use tight stops, wide stops, or no stops at all.

OBV -- On-Balance Volume

Contents


Overview

On-Balance Volume (OBV) is a cumulative volume indicator that connects volume flow to price direction. It answers a simple question: is more volume flowing into the asset (buying pressure) or out of it (selling pressure)?

The calculation is straightforward:

  • If the current candle closes higher than the previous candle, the candle's volume is added to the running OBV total.
  • If the current candle closes lower than the previous candle, the candle's volume is subtracted from the running OBV total.
  • If the close is unchanged, OBV stays the same.

The result is a running total that rises when buying dominates and falls when selling dominates. The absolute value of OBV is meaningless on its own -- what matters is its direction and how it relates to price.

Why OBV matters: Volume leads price. If OBV is rising while price is flat, smart money may be accumulating -- a breakout could follow. If price makes a new high but OBV does not, the rally lacks conviction and may reverse.

OBV shown as oscillator below BTC/USDC 1h candle chart with buying/selling pressure labels


Format

OBV has two related formats:

FormatPeriodDescription
obvNone (cumulative)The raw On-Balance Volume line. No period parameter -- it is a running total from the start of data.
obv_sma_{period}ConfigurableA Simple Moving Average of OBV. Smooths the OBV line to create a crossover reference.

OBV SMA examples:

ExamplePeriodUse Case
obv_sma_1010Fast -- more responsive, more crossovers
obv_sma_2020Standard -- balanced signal frequency
obv_sma_5050Slow -- fewer but higher-conviction signals

Value range: Unbounded. OBV can be any positive or negative number depending on cumulative volume flow. The scale depends entirely on the asset's trading volume.

Note

Because OBV is cumulative, its absolute value varies enormously between assets and timeframes. Never compare OBV values across different pairs. Focus on the direction and crossovers with its SMA.


How OBV Works

Rising OBV: Volume is flowing into the asset on up-candles more than it is flowing out on down-candles. Buyers are in control. If price is also rising, the uptrend is healthy and supported by volume.

Falling OBV: Volume is flowing out on down-candles more than it is flowing in on up-candles. Sellers are in control. If price is also falling, the downtrend has conviction.

OBV Divergence: The most powerful OBV signal. When price and OBV move in opposite directions, something is changing beneath the surface:

  • Bullish divergence: Price makes a lower low, but OBV makes a higher low. Selling pressure is diminishing despite the price drop. A reversal upward may be imminent.
  • Bearish divergence: Price makes a higher high, but OBV makes a lower high. Buying pressure is weakening despite the price rise. A reversal downward may follow.

OBV vs. OBV SMA: The raw OBV line is noisy. Smoothing it with an SMA creates a baseline for crossover signals -- the same way a moving average smooths price. When OBV crosses above its SMA, volume momentum is turning bullish. When it crosses below, volume momentum is turning bearish.


Understanding Operators with OBV

OBV is most commonly used with crossover operators against its own SMA. This is its primary signal mechanism.

cross_above -- OBV Crosses Above Its SMA

What it does: Fires once at the exact candle where OBV transitions from below its SMA to above it. Volume momentum has just turned bullish.

On the chart: The OBV line was sitting below its smoothed average, then climbs above it. This moment marks the shift from net selling pressure to net buying pressure (relative to recent history).

[[actions.triggers]]
indicator = "obv"
operator = "cross_above"
target = "obv_sma_20"
timeframe = "1h"

Typical use: Entry signal. OBV crossing above its SMA confirms that buying volume is increasing. This is the primary OBV buy signal and pairs well with price-based triggers.

cross_below -- OBV Crosses Below Its SMA

What it does: Fires once at the exact candle where OBV transitions from above its SMA to below it. Volume momentum has just turned bearish.

On the chart: The OBV line drops below its smoothed average. Selling volume is now outpacing buying volume relative to recent history.

[[actions.triggers]]
indicator = "obv"
operator = "cross_below"
target = "obv_sma_20"
timeframe = "1h"

Typical use: Exit signal or short entry. When OBV drops below its SMA, the buying pressure that supported the price move is fading.

> OBV Above Its SMA -- State-Based

What it does: True on every candle where OBV remains above its SMA. Volume momentum is currently bullish.

[[actions.triggers]]
indicator = "obv"
operator = ">"
target = "obv_sma_20"
timeframe = "1h"

Typical use: Trend filter. Use this alongside another entry trigger to gate trades so they only execute when volume momentum is favorable. "Buy RSI dips only when OBV is above its SMA."

< OBV Below Its SMA -- State-Based

What it does: True on every candle where OBV remains below its SMA. Volume momentum is currently bearish.

[[actions.triggers]]
indicator = "obv"
operator = ">"
target = "obv_sma_20"
timeframe = "1h"

Typical use: Bearish filter. Avoid opening long positions when selling pressure dominates. Can also be used as an exit condition.

Choosing the right operator

  • Use cross_above / cross_below for entry and exit signals -- they fire once at the turning point when volume momentum shifts.
  • Use > / < as filters alongside other triggers to confirm that volume momentum supports the trade direction.

TOML Examples

Buying Pressure Emerges

Enter when OBV crosses above its 20-period SMA on the 1-hour chart. This signals that buying volume is starting to dominate.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "obv"
  operator = "cross_above"
  target = "obv_sma_20"
  timeframe = "1h"

Volume Momentum Exit

Exit the position when OBV drops below its SMA. The buying pressure that justified the entry has faded.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "obv"
  operator = "cross_below"
  target = "obv_sma_20"
  timeframe = "1h"

OBV Crossover with Price Breakout

Combine OBV with a Bollinger Band breakout to confirm that the price move has real volume behind it. This filters out low-conviction breakouts.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "bb_upper"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "obv"
  operator = ">"
  target = "obv_sma_20"
  timeframe = "1h"

Full OBV Strategy (Entry + Exit)

Enter on bullish OBV crossover with RSI confirmation, exit on bearish OBV crossover or fixed profit.

[meta]
name = "OBV_Volume_Momentum"
description = "Enter on OBV bullish cross + RSI dip, exit on bearish cross or +3%"

# ENTRY
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "obv"
  operator = "cross_above"
  target = "obv_sma_20"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

# TAKE PROFIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# VOLUME EXIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "obv"
  operator = "cross_below"
  target = "obv_sma_20"
  timeframe = "1h"

Tips

Direction matters, not magnitude

OBV's absolute value is meaningless. Whether OBV reads 500,000 or -2,000,000 tells you nothing on its own. What matters is: is it rising or falling? Always use OBV with its SMA or look at the slope of the line. The crossover against obv_sma_20 is the standard way to quantify direction changes.

OBV SMA period selection

A shorter SMA period (10) produces more crossovers -- more signals but more noise. A longer period (50) produces fewer, higher-confidence signals but reacts slower. obv_sma_20 is the standard starting point. Adjust based on backtest results for your specific pair and timeframe.

Best as confirmation, not standalone

OBV shines as a confirmation layer for price-based signals. A price breakout above resistance combined with OBV crossing above its SMA is far more reliable than either signal alone. Volume confirms conviction -- use it to validate what price is telling you.

OBV in low-liquidity markets

OBV is only as meaningful as the underlying volume data. On thinly traded pairs, a single large order can spike OBV dramatically, creating false signals. Stick to high-liquidity pairs (BTC, ETH, major alts) where volume reflects broad market participation.

Volume SMA

Contents


Overview

Volume SMA calculates the Simple Moving Average of trading volume over the last N candles. It establishes a baseline for what "normal" volume looks like, so you can detect when trading activity is unusually high or unusually low.

Volume is the fuel behind price moves. A price breakout on high volume carries conviction -- many participants are driving the move. A breakout on low volume is suspect -- it may lack the momentum to sustain itself and could reverse quickly.

By comparing current volume to its average, you turn raw volume into an actionable signal: is trading activity above or below normal right now?

VolSMA(20) shown as oscillator below BTC/USDC 1h candle chart with average volume label


Format

vol_sma_{period}

The period defines how many candles are averaged to establish the "normal" volume baseline.

ExamplePeriodUse Case
vol_sma_1010Short window -- reacts quickly to volume changes
vol_sma_2020Standard -- smooth baseline for most strategies
vol_sma_5050Long window -- captures the broader volume trend

Period range: 1 to 200.

Value range: 0+ (always positive, volume-denominated). The actual values depend entirely on the asset and timeframe. BTC on hourly candles might have a vol_sma_20 of 500 BTC. A small-cap altcoin might have a vol_sma_20 of 10,000 tokens. Never compare Volume SMA values across different pairs.

Note

Volume SMA is denominated in the asset's trading volume units, not in price. The absolute values vary enormously between pairs and timeframes. When using Volume SMA as a target for comparison, you are comparing current volume to average volume on the same pair and timeframe -- the scale is always consistent within that context.


How Volume SMA Works

Current volume above Volume SMA: Trading activity is above the recent average. Something is happening -- a breakout, news event, or institutional activity. Price moves during above-average volume are more likely to sustain.

Current volume below Volume SMA: Trading activity is below the recent average. The market is quiet. Price moves during below-average volume often lack follow-through and may reverse. This is typical of consolidation phases.

Volume spike: When current volume crosses from below to above its SMA, a volume surge has begun. This is the event-based way to detect the start of unusual activity.

Volume drying up: When current volume drops below its SMA, the market is settling down. If this happens after a big move, it may signal that the move is losing steam.


Understanding Operators with Volume SMA

Volume SMA is typically used as a confirmation filter -- you pair it with a price-based trigger to ensure the price move has volume backing it.

> Above Average Volume -- State-Based

What it does: True on every candle where current volume exceeds the Volume SMA. Trading activity is above average.

[[actions.triggers]]
indicator = "vol_sma_20"
operator = ">"
target = "0"
timeframe = "1h"

Note

The example above triggers whenever vol_sma_20 is positive, which is effectively always. In practice, you use Volume SMA as a target rather than the indicator. The typical pattern compares the current candle's volume to the SMA: use vol_sma_20 as a filter alongside price triggers to confirm that volume supports the move.

Typical use: Confirmation layer. Add this trigger to a price breakout action so it only fires when volume is elevated. "Buy when price breaks above the upper Bollinger Band AND volume is above average."

< Below Average Volume -- State-Based

What it does: True on every candle where current volume is below the Volume SMA. The market is quiet.

Typical use: Detect consolidation. Low-volume periods often precede breakouts. You can also use this to avoid entering trades during dead markets where price moves are unreliable.

cross_above -- Volume Spike Begins

What it does: Fires once at the exact candle where volume transitions from below the SMA to above it. A volume surge has just started.

[[actions.triggers]]
indicator = "vol_sma_20"
operator = "cross_above"
target = "0"
timeframe = "1h"

Typical use: Detect the start of a volume spike. Combine with a trend filter to determine if the spike is bullish or bearish. Volume alone does not tell you direction -- it tells you that something significant is happening.

cross_below -- Volume Returns to Normal

What it does: Fires once when volume drops from above the SMA to below it. The surge is ending.

Typical use: Detect when a high-volume event is winding down. Can be used as a trailing exit signal -- if you entered on a volume spike, exit when volume normalizes.

Volume as a filter, not a signal

Volume SMA is almost always a supporting trigger, not a standalone entry signal. High volume confirms a move. Low volume warns of a fake-out. Pair volume triggers with price or momentum indicators for reliable strategies.


TOML Examples

Confirm Breakout with High Volume

Only enter on a Bollinger Band breakout when volume confirms the move. Price above the upper band with elevated volume signals a genuine breakout, not a low-conviction spike.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "bb_upper"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vol_sma_20"
  operator = ">"
  target = "0"
  timeframe = "1h"

Detect Low-Volume Consolidation

Avoid buying RSI dips during dead markets. Low volume + oversold RSI often means the market is drifting, not reversing. Wait for volume to confirm interest.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "obv"
  operator = ">"
  target = "obv_sma_20"
  timeframe = "1h"

Volume Spike Entry

Enter when a volume surge begins during an uptrend. The MACD filter ensures the trend is bullish, and the volume crossover catches the moment activity spikes.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = ">"
  target = "0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vol_sma_20"
  operator = "cross_above"
  target = "0"
  timeframe = "1h"

Multi-Signal Breakout with Volume Confirmation

A high-confidence entry: price crosses above the 50-period SMA, MACD is bullish, and volume is above average. Three independent signals aligning greatly reduces false positives.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "sma_50"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = ">"
  target = "0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vol_sma_20"
  operator = ">"
  target = "0"
  timeframe = "1h"

Tips

Most useful as a confirmation layer

Volume SMA rarely makes sense as a standalone signal. It does not tell you direction -- only that activity is above or below normal. Always pair it with a directional indicator (MACD, SMA crossover, RSI) to determine whether the volume supports a bullish or bearish thesis.

Period selection

Shorter periods (10) give you a more reactive baseline. Volume only needs to exceed the recent few candles to qualify as "above average." Good for catching quick spikes on lower timeframes.

Longer periods (50) establish a broader sense of normal. Volume needs to exceed a longer history to qualify as elevated. Better for filtering out noise on higher timeframes.

vol_sma_20 is the standard starting point for most strategies.

Volume values are asset-specific

Volume SMA values are denominated in trading volume units, which vary enormously between assets. BTC might trade 500 units per hourly candle while a small-cap altcoin trades 50,000 tokens. Never hardcode absolute volume thresholds across different pairs. Use Volume SMA as a relative comparison within the same pair and timeframe.

Volume precedes price

One of the oldest principles in technical analysis: volume changes often precede price changes. A surge in volume during a consolidation phase -- before price breaks out -- can be an early warning that a significant move is about to begin. Watch for volume rising while price is flat.

TTM Trend

Contents


Overview

TTM Trend is a simplified trend direction indicator that distills the current market regime into one of three discrete values: bullish, neutral, or bearish. Instead of producing a continuous range of values like RSI or MACD, TTM Trend gives you a clean, unambiguous answer: what direction is the trend?

This makes TTM Trend uniquely suited for use as a trend gate -- a filter that allows or blocks trades based on the current trend direction. Rather than trying to time exact entries with TTM Trend, you use it to ensure that your other signals (RSI dips, MACD crossovers, Bollinger touches) only fire when the broader trend supports the trade.

TTM Trend shown as oscillator below BTC/USDC 1h candle chart with bullish/bearish labels


Format

ttm_trend

TTM Trend has no period parameter. It uses a fixed internal calculation to determine trend direction. The format is always just ttm_trend.

Value range: Discrete integers only -- 1, 0, or -1.


TTM Trend Values

ValueMeaningInterpretation
1BullishThe market is in an uptrend. Favorable for long entries.
0NeutralNo clear trend direction. The market is transitioning or consolidating.
-1BearishThe market is in a downtrend. Favorable for exits or short entries.

Note

TTM Trend outputs integers, not floating-point numbers. This is why the = (equals) operator works perfectly here -- there are only three possible values, and they are exact. This is the exception to the general rule that = is rarely useful for technical indicators.


Understanding Operators with TTM Trend

TTM Trend is the primary use case for the = operator. Its discrete values make equality comparisons meaningful and reliable.

= (Equals) -- The Primary Operator

What it does: True on every candle where TTM Trend equals the specified value. This is a state-based operator -- it fires as long as the condition holds.

Bullish gate (= 1):

[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "1"
timeframe = "1h"

This fires on every candle where the hourly trend is bullish. Use it as a filter alongside entry triggers to prevent buying in downtrends or neutral markets.

Bearish gate (= -1):

[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "-1"
timeframe = "1h"

This fires on every candle where the trend is bearish. Use it as an exit condition -- when the trend turns against your position, close it.

Neutral gate (= 0):

[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "0"
timeframe = "1h"

This fires when the market has no clear direction. Useful for avoiding trades during choppy, directionless periods.

> (Greater Than) -- Not Bearish

What it does: True when TTM Trend is greater than the target value. Since TTM Trend only outputs -1, 0, and 1:

  • ttm_trend > 0 is true when TTM Trend = 1 (bullish only).
  • ttm_trend > -1 is true when TTM Trend = 0 or 1 (not bearish).
[[actions.triggers]]
indicator = "ttm_trend"
operator = ">"
target = "0"
timeframe = "1h"

Typical use: A slightly looser filter than = 1. Allows trades when the trend is bullish. Since there is only one integer above 0 (which is 1), this behaves identically to = 1 in practice.

The more useful form is > -1 (not bearish), which allows trades in both bullish and neutral conditions:

[[actions.triggers]]
indicator = "ttm_trend"
operator = ">"
target = "-1"
timeframe = "1h"

< (Less Than) -- Not Bullish

What it does: True when TTM Trend is less than the target value.

  • ttm_trend < 0 is true when TTM Trend = -1 (bearish only).
  • ttm_trend < 1 is true when TTM Trend = 0 or -1 (not bullish).
[[actions.triggers]]
indicator = "ttm_trend"
operator = "<"
target = "1"
timeframe = "1h"

Typical use: Exit filter. Close positions when the trend is no longer bullish. Using < 1 catches both the neutral and bearish states -- a more conservative exit than waiting for full bearish confirmation.

cross_above / cross_below -- Trend Transitions

What they do: Fire once at the moment TTM Trend transitions through a value. Because TTM Trend changes in discrete steps (e.g., from 0 to 1), these operators catch the exact candle where the trend regime shifts.

# Trend just turned bullish
[[actions.triggers]]
indicator = "ttm_trend"
operator = "cross_above"
target = "0"
timeframe = "1h"
# Trend just turned bearish
[[actions.triggers]]
indicator = "ttm_trend"
operator = "cross_below"
target = "0"
timeframe = "1h"

Typical use: Catching the moment a trend change occurs. Unlike = which fires on every candle during the trend, cross_above fires once at the transition. This is useful for one-time entries or exits at the trend shift.

= vs cross_above for trend gates

Use = when TTM Trend is a filter for another trigger. The filter needs to be true on the same candle the other trigger fires, so = (state-based, fires every candle) is correct.

Use cross_above / cross_below when TTM Trend is the primary signal and you want to act at the exact moment the trend changes.


TOML Examples

Only Enter During Uptrend

Gate RSI entries so they only fire when the hourly trend is bullish. This prevents buying oversold dips in a downtrend -- a common trap.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ttm_trend"
  operator = "="
  target = "1"
  timeframe = "1h"

Exit on Downtrend Confirmation

Sell the entire position when the trend turns bearish. This acts as a trend-based stop-loss.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "ttm_trend"
  operator = "="
  target = "-1"
  timeframe = "1h"

RSI Dip Buy with Trend Filter

A refined dip-buying strategy. The daily TTM Trend confirms the macro direction is bullish, while the hourly RSI catches oversold dips for entry timing. Multi-timeframe confirmation.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "ttm_trend"
  operator = "="
  target = "1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = ">"
  target = "0"
  timeframe = "1h"

Full Trend-Filtered Strategy (Entry + Exit)

Enter on RSI dips in an uptrend, exit when the trend turns bearish or a profit target is hit.

[meta]
name = "Trend_Filtered_DCA"
description = "Buy RSI dips in uptrend, exit on trend reversal or +4%"

# ENTRY -- only in uptrend
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "ttm_trend"
  operator = "="
  target = "1"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

# TAKE PROFIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "4%"

# TREND EXIT
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "ttm_trend"
  operator = "="
  target = "-1"
  timeframe = "1h"

Tips

Use TTM Trend as a gate, not a signal

TTM Trend is at its best when it acts as a filter for other indicators. RSI < 30 is a decent buy signal. RSI < 30 WHILE TTM Trend = 1 is a much better buy signal -- you are buying a dip within a confirmed uptrend rather than catching a falling knife in a downtrend.

Multi-timeframe trend filtering

A powerful pattern: use daily TTM Trend for the macro direction and hourly indicators for entry timing. ttm_trend = 1 on the daily chart confirms you are in a bull market, then RSI or MACD on the hourly chart picks the exact entry point. This dramatically reduces false entries during corrections within a larger uptrend.

Do not use = with floating-point indicators

The = operator works reliably with TTM Trend because it outputs discrete integers (1, 0, -1). Do not use = with indicators like RSI, SMA, EMA, or MACD -- those produce floating-point values like 29.87 or -142.53, and exact equality almost never matches. Use < or > for continuous indicators.

Neutral (0) as a caution zone

TTM Trend = 0 (neutral) often occurs during trend transitions. If the trend was bullish and shifts to neutral, it may be weakening before turning bearish. Consider using ttm_trend < 1 (not bullish) as a conservative exit trigger rather than waiting for full bearish confirmation at -1.

Price

Contents


Overview

price is not a calculated indicator -- it is the current candle's closing price. It allows you to compare raw price directly against indicator values, moving averages, Bollinger Bands, or fixed numeric levels.

Every other indicator in Botmarley derives a signal from price. Sometimes, though, you want to work with price itself: "Is the price above the 200-period SMA?" or "Has price dropped below the lower Bollinger Band?" or "Is price above 100,000?" The price field makes these comparisons possible.

This gives you the building block for classic technical analysis rules: support/resistance levels, moving average crossovers from price's perspective, and price-relative-to-band signals.


Format

price

Price has no period parameter. It always represents the close of the current candle on the specified timeframe. The format is simply price.

Value range: The actual market price of the asset. For BTC, this might be 50,000--100,000+. For a small-cap altcoin, it might be 0.001. Values are always positive.

Note

price always refers to the closing price of the current candle on the trigger's timeframe. On a 1-hour chart, it is the close of the current hourly candle. On a daily chart, it is the close of the current daily candle.


How Price Works in Triggers

Price is unique because it can be used on either side of a trigger -- as the indicator or as the target.

Price as the indicator:

indicator = "price"
operator = "<"
target = "bb_lower"

Read as: "price is below the lower Bollinger Band."

Price as the target:

indicator = "bb_lower"
operator = ">"
target = "price"

Read as: "the lower Bollinger Band is above price." This is logically identical to the first form -- both mean price is below the lower Bollinger Band.

Price compared to a fixed value:

indicator = "price"
operator = ">"
target = "100000"

Read as: "price is above 100,000." This allows you to set absolute level-based triggers.

The flexibility of price means you can express the same condition in multiple ways. Choose whichever reads most naturally to you. Most traders find indicator = "price" with the comparison target to be the most intuitive form.


Understanding Operators with Price

All five operators work with price. The behavior follows the same state-based vs. event-based rules as other indicators.

< Price Below a Level -- State-Based

What it does: True on every candle where price is below the target value. This is a sustained condition.

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "bb_lower"
timeframe = "1h"

On the chart: Price is sitting below the lower Bollinger Band. For as long as it stays there, this trigger fires on every candle. Use max_count to limit repeated firing, or switch to cross_below if you only want the initial break.

Common pairings:

  • price < bb_lower -- below the volatility envelope (oversold by Bollinger standards)
  • price < sma_200 -- below the long-term moving average (bearish territory)
  • price < ema_50 -- below the medium-term trend line

> Price Above a Level -- State-Based

What it does: True on every candle where price is above the target value. This is a sustained condition.

[[actions.triggers]]
indicator = "price"
operator = ">"
target = "sma_200"
timeframe = "1d"

Typical use: Trend filter. "Only allow entries when price is above the 200-day SMA" -- a classic bull-market confirmation. Because this is state-based, it works perfectly as a background filter for other entry signals.

Common pairings:

  • price > sma_200 -- above the long-term trend (bullish territory)
  • price > bb_upper -- above the volatility envelope (breakout or overbought)
  • price > ema_20 -- above the short-term trend

cross_above -- Price Crossing Above

What it does: Fires once at the exact candle where price transitions from below the target to above it. The previous candle's close was at or below the target; the current candle's close is above it.

[[actions.triggers]]
indicator = "price"
operator = "cross_above"
target = "ema_50"
timeframe = "1h"

On the chart: Price was below the EMA(50) line, then closes above it. This crossing moment fires the trigger once. This is the classic "golden cross from price's perspective" -- the moment price reclaims a key moving average.

Typical use: Breakout entries. When price crosses above a significant level (moving average, Bollinger middle band), it signals a potential trend shift. Event-based operators fire once, making them ideal for entry signals without needing max_count.

cross_below -- Price Crossing Below

What it does: Fires once at the exact candle where price transitions from above the target to below it.

[[actions.triggers]]
indicator = "price"
operator = "cross_below"
target = "sma_50"
timeframe = "1h"

Typical use: Breakdown signals. Price dropping below a key moving average can signal trend weakness. Use as an exit trigger or as a warning that the trend is changing.

= Exact Price -- Rarely Useful

What it does: True only when price exactly equals the target value.

[[actions.triggers]]
indicator = "price"
operator = "="
target = "100000"

Warning

Price is a floating-point value with many decimal places. The chance of price landing on exactly 100000.000000 is extremely low. In practice, = almost never fires with price. Use > or < instead, or use cross_above / cross_below to catch the moment price passes through a level.


TOML Examples

Price Below Bollinger Lower Band

The classic Bollinger Bounce entry. Price has extended more than 2 standard deviations below the mean -- a statistically significant move that often reverts.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "bb_lower"
  timeframe = "1h"

Price Above Long-Term Moving Average

Use price above the 200-period SMA on the daily chart as a macro trend filter. Only allow entries when the market is in confirmed bullish territory.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

Price Crosses Above EMA

Enter when price reclaims the 50-period EMA. This catches the moment price moves back above the medium-term trend line -- a potential trend resumption signal.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "ema_50"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = ">"
  target = "0"
  timeframe = "1h"

Absolute Price Target

Sell when price breaks above a specific level. Useful for taking profit at a predetermined price target.

[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "100000"
  timeframe = "1h"
  max_count = 1

Tips

Indicator or target -- your choice

price can be used on either side of the trigger. These two triggers are logically identical:

  • indicator = "price", operator = "<", target = "bb_lower" -- "price is below the lower band"
  • indicator = "bb_lower", operator = ">", target = "price" -- "the lower band is above price"

Choose whichever reads more naturally. Most strategies use indicator = "price" because it reads like plain English: "price below X."

Price + moving average = trend confirmation

The simplest trend filter in technical analysis: is price above or below a key moving average? price > sma_200 on the daily chart has been used for decades to define bull vs. bear markets. Add it as a background filter to virtually any strategy to avoid trading against the major trend.

Use cross operators for levels

If you want to act when price hits a specific level (a round number like 100,000, or a moving average), use cross_above or cross_below instead of > or <. The cross operator fires once at the moment of crossing. The comparison operators fire on every candle while the condition holds, which can cause repeated actions without max_count.

Absolute price targets are pair-specific

When using fixed numeric targets like target = "100000", remember that this value is specific to the trading pair. A strategy with price > 100000 makes sense for BTC/USDC but is meaningless for ETH/USDC. If you reuse strategy templates across pairs, prefer indicator-relative targets (like bb_lower or sma_200) over hardcoded price levels.

RSTD -- Rolling Standard Deviation

Contents


Overview

Rolling Standard Deviation (RSTD) measures the dispersion of closing prices around their rolling mean over the last N candles. In simple terms, it tells you how spread out recent prices are -- it quantifies volatility based on close-to-close variation.

The calculation is straightforward: take the last N closing prices, compute their mean, then compute the standard deviation. A high RSTD means closing prices have been scattered far from their average -- the market is volatile. A low RSTD means closing prices are clustered tightly around their average -- the market is quiet and compressed.

The key distinction from ATR: ATR uses the high-low-close True Range, capturing intrabar volatility (wicks, gaps). RSTD uses only closing prices, capturing close-to-close variation. This makes RSTD the foundation for both Z-Score and Bollinger Bands. If you think of ATR as "how wide do candles get," think of RSTD as "how much do closes jump around." Both are volatility measures, but they capture different dimensions of it.


Format

rstd_{period}

The period defines the lookback window -- how many closing prices are included in the standard deviation calculation.

ExamplePeriodUse Case
rstd_2020Standard -- matches Bollinger Band default, good balance of responsiveness and smoothness
rstd_5050Slower -- captures longer-term volatility regime, filters out short spikes
rstd_1010Fast -- reacts quickly to sudden volatility shifts, noisier

Period range: 5 to 500.

Value range: 0 to infinity (always positive, denominated in the asset's price currency -- same units as price and ATR).


Understanding RSTD Values

Like ATR, RSTD values are not normalized to a fixed scale. They are denominated in the same currency as the asset's price. This means RSTD thresholds must be calibrated per asset and timeframe.

AssetTimeframeTypical RSTD(20) RangeWhat It Means
BTC/USDC1h150 -- 600BTC closes typically deviate $150--$600 from their 20-period mean
BTC/USDC1d800 -- 2,500BTC daily closes deviate $800--$2,500 from their 20-day mean
ETH/USDC1h10 -- 50ETH closes typically deviate $10--$50 from their 20-period mean
SOL/USDC1h0.30 -- 2.50SOL closes typically deviate $0.30--$2.50 from their 20-period mean

Note

The values above are illustrative and vary with market conditions. During high-volatility events, RSTD can spike to 3--5x its normal range. Always check recent RSTD values for your specific pair and timeframe. Backtesting is the best way to calibrate thresholds.


Understanding Operators with RSTD

Each operator behaves differently with RSTD. Because RSTD is a volatility filter (it describes the market's current dispersion, not direction), it is almost always used in combination with directional triggers.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where RSTD is above the threshold.

On the chart: Price dispersion is elevated. Closing prices are jumping around significantly from candle to candle. This trigger stays active for as long as RSTD remains above the threshold.

[[actions.triggers]]
indicator = "rstd_20"
operator = ">"
target = "400"
timeframe = "1h"

Typical use: "Only trade when close-to-close variation is high enough." High RSTD means there is enough directional movement in closing prices to justify entry. This filters out sideways chop where candles have large wicks but close near each other.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where RSTD is below the threshold.

On the chart: Price dispersion is compressed. Closes are clustering tightly around the mean -- a volatility squeeze. This is the setup phase before many breakouts.

[[actions.triggers]]
indicator = "rstd_20"
operator = "<"
target = "150"
timeframe = "1h"

Typical use: Detect low-dispersion squeeze environments. When RSTD drops to unusually low levels, it means prices are coiling. Combine with a directional breakout trigger to enter when the squeeze resolves.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where RSTD transitions from below the threshold to above it. The previous candle had RSTD <= the target, and the current candle has RSTD > the target.

[[actions.triggers]]
indicator = "rstd_20"
operator = "cross_above"
target = "300"
timeframe = "1h"

Typical use: Detect the moment volatility expands. This captures the transition from a compressed market to a dispersed one -- the squeeze is breaking. Combine with a directional indicator (RSI, ROC, or MACD) to determine entry direction.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where RSTD transitions from above the threshold to below it. The previous candle had RSTD >= the target, and the current candle has RSTD < the target.

[[actions.triggers]]
indicator = "rstd_20"
operator = "cross_below"
target = "200"
timeframe = "1h"

Typical use: Detect the moment volatility contracts. Useful for exiting positions when the market quiets down, or for initiating squeeze-detection logic -- once RSTD drops below the threshold, start watching for a breakout.

Why: RSTD produces floating-point values that are rarely exactly equal to any specific number. Use > or < instead.

Choosing the right operator

  • Use > to ensure close-to-close dispersion is high enough for meaningful moves. This is the most common RSTD operator.
  • Use < to detect volatility squeezes where price is coiling before a breakout.
  • Use cross_above / cross_below to detect the exact moment a volatility regime changes.

TOML Examples

Volatility Expansion: Short vs Long Period

Compare short-period RSTD against long-period RSTD to detect volatility expansion. When the fast RSTD exceeds the slow RSTD, recent volatility is accelerating relative to the longer-term norm -- a breakout may be underway. Combine with positive momentum to enter long.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rstd_20"
  operator = ">"
  target = "rstd_50"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "1.5"
  timeframe = "1h"

Low Volatility Squeeze Entry

Wait for a volatility squeeze (RSTD drops to unusually low levels) and then enter when price breaks above the upper Bollinger Band. The combination of compressed volatility + directional breakout often produces outsized moves.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rstd_20"
  operator = "<"
  target = "150"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "bb_upper"
  timeframe = "1h"

RSI Dip Buy with Volatility Filter

A classic dip-buy strategy enhanced with an RSTD filter. Only enter when RSI is oversold AND close-to-close dispersion is high enough that a meaningful bounce is likely. This avoids buying dips in dead, flat markets where the "bounce" is a few cents.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rstd_20"
  operator = ">"
  target = "300"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Tips

RSTD is NOT directional

Like ATR, RSTD measures the magnitude of price variation, not the direction. A high RSTD during a selloff is the same as a high RSTD during a rally. Never use RSTD as a buy or sell signal on its own. It is a filter that tells you whether the market is moving, not which way.

RSTD values are asset-specific

You cannot use the same RSTD threshold for BTC and SOL. BTC's RSTD(20) on the hourly chart might be 400, while SOL's might be 1.20. Always calibrate your RSTD thresholds by checking the indicator's recent values for the specific pair and timeframe. Backtesting is the best way to find appropriate thresholds.

Compare short vs long periods for squeeze-breakout

One of the most effective RSTD patterns is comparing a short-period RSTD against a long-period RSTD. When rstd_20 < rstd_50, recent volatility is below the longer-term norm -- a squeeze is forming. When rstd_20 > rstd_50, volatility is expanding. This relative comparison adapts automatically to changing market conditions, unlike a fixed threshold.

RSTD is the foundation of Z-Score

Z-Score is calculated as (price - mean) / rstd. If you understand RSTD, you understand the denominator of Z-Score. When RSTD is low, even a small price move produces a large Z-Score. When RSTD is high, it takes a big price move to produce an extreme Z-Score. Combining RSTD with Z-Score gives you both the volatility context and the statistical position of price.

Z-Score -- Standard Score

Contents


Overview

Z-Score measures how many standard deviations the current price is from its rolling mean. The formula is: Z = (price - mean) / standard_deviation, where both the mean and standard deviation are calculated over the last N closing prices.

The result translates raw price into a universal, dimensionless scale. A Z-Score of -2 means the current price is 2 standard deviations below the recent average -- statistically, this puts it in approximately the bottom 2.5% of the recent price distribution (assuming normality). A Z-Score of +2 means the opposite: price is in the top 2.5%.

This universality is what makes Z-Score powerful. Unlike ATR or RSTD, where thresholds must be recalibrated for every asset and timeframe, Z-Score thresholds are asset-agnostic. A Z-Score of -2 means "statistically cheap relative to recent history" whether you are trading BTC at $60,000 or SOL at $150. This makes Z-Score one of the most portable indicators for mean-reversion strategies -- you write the logic once, and it works across any pair.


Format

zscore_{period}

The period defines the lookback window used to calculate both the rolling mean and the rolling standard deviation.

ExamplePeriodUse Case
zscore_2020Standard -- responsive to recent deviations, good for intraday mean reversion
zscore_5050Slower -- captures deviations from a longer-term average, fewer false signals
zscore_100100Long-term -- identifies major statistical outliers, best for swing/position trading

Period range: 5 to 500.

Value range: Typically -4 to +4 (theoretically unbounded). 0 = price is exactly at its rolling mean. Negative = below mean. Positive = above mean.


Understanding Z-Score Values

Z-Score values are normalized and universal. The same thresholds apply across all assets and timeframes, which is a major advantage over price-denominated indicators like ATR and RSTD.

Z-Score RangeStatistical MeaningTrading Interpretation
> +3.0Top ~0.1% -- extreme outlierExtremely overbought. Price has deviated far above the mean. High risk of reversion.
+2.0 to +3.0Top ~2.5%Overbought. Statistically expensive. Mean-reversion sell zone.
+1.0 to +2.0Above averageModerately elevated. Price is above the mean but within a normal range.
-1.0 to +1.0Within 1 standard deviationNormal range. ~68% of prices fall here. No strong signal.
-2.0 to -1.0Below averageModerately depressed. Price is below the mean but within a normal range.
-3.0 to -2.0Bottom ~2.5%Oversold. Statistically cheap. Mean-reversion buy zone.
< -3.0Bottom ~0.1% -- extreme outlierExtremely oversold. Price has deviated far below the mean. High probability of reversion.

Note

These statistical percentages assume a normal distribution. Crypto price returns have fat tails -- Z-Scores of -3 or -4 occur more frequently than a normal distribution would predict. This actually works in favor of mean-reversion strategies: the extremes are more common, giving you more trading opportunities, and the reversion tendency still holds.


Understanding Operators with Z-Score

Z-Score is primarily a mean-reversion indicator. It tells you when price has deviated "too far" from its recent average. The operators let you define what "too far" means for your strategy.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where Z-Score is above the threshold.

On the chart: Price is elevated relative to its recent mean. The higher the Z-Score, the more statistically extended the price is. This trigger stays active for as long as the Z-Score remains above the threshold.

[[actions.triggers]]
indicator = "zscore_20"
operator = ">"
target = "2.0"
timeframe = "1h"

Typical use: "Price is statistically overbought -- consider taking profit or selling." When Z-Score exceeds +2.0, price is in the top ~2.5% of its recent range. For mean-reversion strategies, this is a sell or exit signal.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where Z-Score is below the threshold.

On the chart: Price is depressed relative to its recent mean. The more negative the Z-Score, the more statistically cheap the price is. This trigger stays active for as long as Z-Score remains below the threshold.

[[actions.triggers]]
indicator = "zscore_20"
operator = "<"
target = "-2.0"
timeframe = "1h"

Typical use: "Price is statistically oversold -- consider buying." When Z-Score drops below -2.0, price is in the bottom ~2.5% of its recent range. This is the classic mean-reversion buy signal. The assumption is that price will revert toward the mean.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where Z-Score transitions from below the threshold to above it. The previous candle had Z-Score <= the target, and the current candle has Z-Score > the target.

[[actions.triggers]]
indicator = "zscore_20"
operator = "cross_above"
target = "0"
timeframe = "1h"

Typical use: Detect the moment price crosses back above the mean (Z-Score crosses above 0). In a mean-reversion strategy, this can signal that the reversion is complete -- price has returned to "normal." Also useful for detecting the moment price transitions into overbought territory (crossing above +2.0).

cross_below -- Event-Based

What it does: Fires once, at the exact candle where Z-Score transitions from above the threshold to below it. The previous candle had Z-Score >= the target, and the current candle has Z-Score < the target.

[[actions.triggers]]
indicator = "zscore_20"
operator = "cross_below"
target = "-2.0"
timeframe = "1h"

Typical use: Detect the exact moment price enters the oversold zone. Unlike < which stays active the entire time Z-Score is below -2.0, cross_below fires only at the transition -- ensuring you enter once per dip rather than on every candle during an extended decline.

Why: Z-Score produces floating-point values that are rarely exactly equal to any specific number. Use > or < instead.

Choosing the right operator

  • Use < with a negative target (e.g., -2.0) for mean-reversion buy entries. This is the most common Z-Score operator.
  • Use > with a positive target (e.g., +1.0 or +2.0) for mean-reversion exits or overbought detection.
  • Use cross_below to enter once per dip (fires at the moment of transition, not on every candle).
  • Use cross_above to detect the moment price returns to the mean (exit signal).

TOML Examples

Mean Reversion at Statistical Extreme

The classic Z-Score trade: buy when price drops to 2 standard deviations below the mean. This is a statistically rare event (~2.5% of the time under normal assumptions), and prices tend to revert toward the mean. Add a trend filter to avoid catching falling knives in a downtrend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "zscore_20"
  operator = "<"
  target = "-2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Exit When Mean Reversion Completes

Close the position when Z-Score crosses back above +1.0 -- price has not only returned to the mean but has moved one standard deviation above it. This captures the full reversion move plus some continuation.

[[actions]]
type = "close_long"
amount = "100%"

  [[actions.triggers]]
  indicator = "zscore_20"
  operator = "cross_above"
  target = "1.0"
  timeframe = "1h"

Z-Score with Trend Filter

Buy statistically cheap price levels, but only when the broader trend supports it. The Z-Score provides the entry timing (price is 2.5 standard deviations below the mean), while the EMA crossover confirms the macro trend is bullish. This prevents mean-reversion entries during sustained downtrends.

[[actions]]
type = "open_long"
amount = "150 USDC"

  [[actions.triggers]]
  indicator = "zscore_50"
  operator = "<"
  target = "-2.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_21"
  operator = ">"
  target = "ema_55"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

Multi-Timeframe Z-Score Confluence

Enter only when Z-Score is oversold on BOTH the hourly and 4-hour timeframes. When two timeframes agree that price is statistically cheap, the mean-reversion signal is much stronger than a single-timeframe reading.

[[actions]]
type = "open_long"
amount = "200 USDC"

  [[actions.triggers]]
  indicator = "zscore_20"
  operator = "<"
  target = "-2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "zscore_20"
  operator = "<"
  target = "-1.5"
  timeframe = "4h"

Tips

Z-Score thresholds are universal

Unlike ATR or RSTD where you need different thresholds for every asset, Z-Score thresholds work the same everywhere. A Z-Score of -2 means "bottom 2.5% of recent prices" whether you are trading BTC, ETH, SOL, or any other pair. This makes Z-Score strategies highly portable -- write once, deploy on any asset.

Pair Z-Score entries with Z-Score exits

The most natural way to use Z-Score is as a complete mean-reversion system: enter at one extreme, exit at the opposite extreme or at the mean. For example, enter long when zscore_20 < -2.0 and exit when zscore_20 > 1.0. This creates a symmetric strategy that captures the full reversion move.

Z-Score is the normalized version of RSTD

Z-Score divides the deviation from the mean by the rolling standard deviation (RSTD). This normalization is what makes the thresholds universal. If you want to understand why a Z-Score is extreme, look at the RSTD: if RSTD is very low (squeeze), even a small price move creates a large Z-Score. If RSTD is high, it takes a much bigger move to reach the same Z-Score level.

PRANK -- Percentile Rank

Contents


Overview

Percentile Rank measures where the current closing price ranks among the last N closing prices, expressed as a percentage from 0 to 100. If the current price is the lowest close in the lookback window, the Percentile Rank is 0. If it is the highest, the Percentile Rank is 100. A value of 25 means the current price is higher than 25% of the closes in the window.

The calculation is simple: count how many of the last N closes are less than or equal to the current close, divide by N, and multiply by 100. No assumptions about distribution shape, no parameters beyond the lookback period. This simplicity is its strength.

The key advantage of Percentile Rank over Z-Score: it is distribution-free. Z-Score assumes prices are approximately normally distributed to interpret its thresholds (e.g., "Z = -2 means bottom 2.5%"). Percentile Rank makes no such assumption. A Percentile Rank of 5 means "the current price is in the bottom 5% of the last N closes" regardless of whether prices are normally distributed, skewed, or have fat tails. For crypto markets, where returns are decidedly non-normal, this non-parametric property is valuable. Percentile Rank tells you exactly what it says -- no statistical assumptions required.


Format

prank_{period}

The period defines the lookback window -- how many recent closing prices are compared against the current close.

ExamplePeriodUse Case
prank_5050Standard -- ranks price among the last 50 closes, good for short-to-medium-term context
prank_100100Slower -- ranks price within a broader window, better for swing trading and position sizing
prank_200200Long-term -- provides context against a large historical sample, fewer extremes

Period range: 10 to 500.

Value range: 0 to 100 (always). 0 = current price is the lowest close in the window. 100 = current price is the highest close in the window.


Understanding Percentile Rank Values

Percentile Rank values are bounded between 0 and 100 and have an intuitive interpretation that does not depend on the asset or timeframe. A value of 10 means "cheaper than 90% of recent closes" whether you are looking at BTC on the hourly chart or ETH on the daily chart.

Percentile RankMeaningTrading Interpretation
95 -- 100Price is at or near the highest close in the windowExtremely expensive relative to recent history. Potential top. Contrarian sell zone.
75 -- 95Price is in the upper quartileElevated. Market has been rallying. Trend-following strategies may stay long; mean-reversion strategies may start scaling out.
25 -- 75Price is in the middle rangeNormal territory. No strong signal from percentile rank alone.
5 -- 25Price is in the lower quartileDepressed. Market has pulled back. Starting to get interesting for dip buyers.
0 -- 5Price is at or near the lowest close in the windowExtremely cheap relative to recent history. Potential bottom. Contrarian buy zone.

Note

Percentile Rank is relative to the lookback window. A prank_50 of 5 means "bottom 5% of the last 50 closes" -- it says nothing about where price stands relative to the last 200 or 500 closes. Longer periods provide more historical context but react more slowly to recent trends.


Understanding Operators with Percentile Rank

Percentile Rank is a contrarian positioning indicator. It tells you where price sits in the context of recent history. Extreme lows suggest buying opportunities; extreme highs suggest selling opportunities.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where Percentile Rank is above the threshold.

On the chart: Price is ranking high among recent closes. The closer to 100, the closer price is to its recent high watermark. This trigger stays active for as long as the Percentile Rank remains above the threshold.

[[actions.triggers]]
indicator = "prank_100"
operator = ">"
target = "95"
timeframe = "1h"

Typical use: "Price is near the top of its recent range -- consider taking profit or reducing exposure." When Percentile Rank exceeds 95, the current price is higher than 95% of the last 100 closes. This is a contrarian signal that the move may be overextended.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where Percentile Rank is below the threshold.

On the chart: Price is ranking low among recent closes. The closer to 0, the closer price is to its recent low point. This trigger stays active for as long as Percentile Rank remains below the threshold.

[[actions.triggers]]
indicator = "prank_100"
operator = "<"
target = "5"
timeframe = "1h"

Typical use: "Price is near the bottom of its recent range -- consider buying." When Percentile Rank drops below 5, the current price is lower than 95% of the last 100 closes. This is the core contrarian buy signal. Price is historically cheap within the lookback window.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where Percentile Rank transitions from below the threshold to above it. The previous candle had Percentile Rank <= the target, and the current candle has Percentile Rank > the target.

[[actions.triggers]]
indicator = "prank_100"
operator = "cross_above"
target = "50"
timeframe = "1h"

Typical use: Detect the moment price crosses above the median of its recent range. This can serve as an exit signal for mean-reversion trades: you bought in the bottom percentile, and now price has recovered to the middle of its range -- the reversion is complete.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where Percentile Rank transitions from above the threshold to below it. The previous candle had Percentile Rank >= the target, and the current candle has Percentile Rank < the target.

[[actions.triggers]]
indicator = "prank_100"
operator = "cross_below"
target = "10"
timeframe = "1h"

Typical use: Detect the exact moment price falls into the bottom percentile zone. Unlike < which stays active the entire time Percentile Rank is below 10, cross_below fires only at the transition -- giving you a single entry signal per dip.

Why: While Percentile Rank produces integer-like values more often than Z-Score, exact matches are still unreliable for triggering. Use > or < instead.

Choosing the right operator

  • Use < with a low threshold (e.g., 5 or 10) to buy when price is at the bottom of its recent range. This is the most common Percentile Rank operator.
  • Use > with a high threshold (e.g., 90 or 95) to sell or take profit when price is at the top of its recent range.
  • Use cross_below to enter once per dip (fires at the moment of transition, not on every candle).
  • Use cross_above to detect the moment price recovers to mid-range (exit signal).

TOML Examples

Buy the Bottom 5th Percentile

The classic contrarian trade: buy when the current price is lower than 95% of the last 100 closes. This is a purely statistical signal -- price is at the extreme low of its recent range. Add a trend filter to avoid buying into a sustained downtrend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "<"
  target = "5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Exit at 60th Percentile

Close the position when price recovers to the 60th percentile of its recent range. This exits above the median, capturing the majority of the reversion move without waiting for price to reach the top of the range (which may never happen).

[[actions]]
type = "close_long"
amount = "100%"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "cross_above"
  target = "60"
  timeframe = "1h"

Percentile Rank with RSI Confluence

Combine Percentile Rank with RSI for double confirmation. Both indicators must agree that price is depressed: Percentile Rank says the price is in the bottom 10% of its recent range, and RSI says momentum is oversold. The confluence of two independent signals increases confidence in the entry.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "<"
  target = "10"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_21"
  operator = ">"
  target = "ema_55"
  timeframe = "1d"

DCA at Different Percentile Levels

Dollar-cost average into a position at progressively lower percentile levels. The deeper the dip, the larger the allocation. This creates a tiered entry strategy that averages into a position more aggressively as price becomes cheaper relative to recent history.

# Light entry at 15th percentile
[[actions]]
type = "open_long"
amount = "50 USDC"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "cross_below"
  target = "15"
  timeframe = "4h"

# Medium entry at 8th percentile
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "cross_below"
  target = "8"
  timeframe = "4h"

# Heavy entry at 3rd percentile
[[actions]]
type = "open_long"
amount = "200 USDC"

  [[actions.triggers]]
  indicator = "prank_100"
  operator = "cross_below"
  target = "3"
  timeframe = "4h"

Tips

Percentile Rank is backward-looking

A Percentile Rank of 0 means price is the lowest close in the last N periods -- but it says nothing about whether price will continue falling. In a sustained downtrend, price can print a Percentile Rank of 0 on every new candle as it keeps making new lows. Always combine Percentile Rank with a trend filter to avoid buying into an accelerating decline.

Distribution-free -- no assumptions needed

Unlike Z-Score, which interprets its thresholds based on a normal distribution assumption, Percentile Rank is completely non-parametric. A Percentile Rank of 5 literally means "current price is lower than 95% of the last N closes." No bell curve required. For crypto markets with fat tails and skewed returns, this is an advantage -- the number means exactly what it says.

Compare with Z-Score for a fuller picture

Z-Score and Percentile Rank often agree, but when they diverge, it is informative. If Z-Score says -3 (extreme) but Percentile Rank says 15 (not that extreme), it means price is far from the mean but not actually near the lowest close -- volatility has expanded the Z-Score. Combining both gives you a parametric and non-parametric view of the same question: "Is this price cheap?"

Longer periods for swing trading, shorter for scalping

A prank_50 reaching 5 means price is the cheapest of the last ~2 days of hourly candles -- a short-term dip. A prank_200 reaching 5 means price is the cheapest of the last ~8 days of hourly candles -- a more significant pullback. Choose your period based on your holding horizon: shorter periods for quick mean-reversion trades, longer periods for swing trades that may take days to play out.

Hurst -- Hurst Exponent

Contents


Overview

The Hurst Exponent measures the "memory" of a price series using Rescaled Range (R/S) analysis. It answers the most fundamental question in quantitative trading: is this market trending, mean-reverting, or random?

The calculation works by dividing the price series into sub-periods of varying lengths, computing the rescaled range (the range of cumulative deviations from the mean, divided by the standard deviation) for each, and then fitting a power law to find the exponent H. A Hurst Exponent of 0.5 corresponds to a random walk -- past price changes have no bearing on future ones. Values above 0.5 indicate persistence (trending behavior -- an up move is more likely to be followed by another up move). Values below 0.5 indicate anti-persistence (mean-reverting behavior -- an up move is more likely to be followed by a down move).

This is THE regime detector. If Hurst says the market is random (H near 0.5), no technical indicator has a statistical edge -- you are just gambling. Hurst above 0.55 means momentum strategies (MACD, EMA crossovers, breakout entries) have a mathematical basis. Hurst below 0.45 means mean-reversion strategies (RSI dip-buys, Z-Score entries, Bollinger Band bounces) have a mathematical basis. This is the foundation for all regime-adaptive strategies.


Format

hurst_{period}

The period defines the lookback window -- how many candles of price data are used to estimate the Hurst Exponent via R/S analysis.

ExamplePeriodUse Case
hurst_100100Moderate lookback -- reacts faster to regime changes, noisier
hurst_200200Standard -- good balance of stability and responsiveness
hurst_500500Very stable -- captures the dominant long-term regime, slow to update

Period range: 50 to 500.

Value range: 0 to 1 (always).


Understanding Hurst Values

Hurst RangeRegimeInterpretation
0.00 -- 0.40Strong mean reversionPrice frequently reverses direction. Past moves are strongly anti-persistent. Mean-reversion strategies (RSI, Z-Score, BB bounces) have strong edge.
0.40 -- 0.50Weak mean reversion / randomSlight anti-persistence but close to random. The edge for mean reversion is marginal. Proceed with caution.
0.50Pure random walkNo memory. Past prices give zero information about future direction. No technical strategy has a statistical edge.
0.50 -- 0.60Weak trendSlight persistence. Momentum strategies may work but the edge is marginal. Require strong confirmation signals.
0.60 -- 1.00Strong trendPrice moves are highly persistent. Trends tend to continue. Momentum strategies (MACD, EMA crossovers, breakout entries) have strong edge.

Note

Hurst Exponent is a statistical property of the price series, not a timing signal. It tells you what kind of strategies will work in the current regime, not when to enter or exit. Always combine Hurst with a directional indicator for actual trade signals.


Understanding Operators with Hurst

Each operator behaves differently with Hurst. Because Hurst is a regime filter (it classifies the market's behavior, not its direction), it is almost always used as a gate on top of directional entry triggers.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where the Hurst Exponent is above the threshold.

On the chart: The market is in a trending regime. Price moves tend to persist -- up follows up, down follows down. This trigger stays active for as long as Hurst remains above the threshold.

[[actions.triggers]]
indicator = "hurst_200"
operator = ">"
target = "0.55"
timeframe = "1h"

Typical use: "Only trade momentum strategies when the market is actually trending." A Hurst above 0.55 confirms that trend-following indicators (MACD, EMA crossovers) have a statistical basis. Without this filter, momentum signals in random-walk markets are just noise.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where the Hurst Exponent is below the threshold.

On the chart: The market is in a mean-reverting regime. Price moves tend to reverse -- deviations from the mean are corrected. This trigger stays active for as long as Hurst remains below the threshold.

[[actions.triggers]]
indicator = "hurst_200"
operator = "<"
target = "0.45"
timeframe = "1h"

Typical use: "Only trade mean-reversion strategies when the market is actually mean-reverting." A Hurst below 0.45 confirms that dip-buy strategies (RSI oversold, Z-Score below -2, price at lower Bollinger Band) have a mathematical edge.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where the Hurst Exponent transitions from below the threshold to above it. The previous candle had Hurst <= the target, and the current candle has Hurst > the target.

[[actions.triggers]]
indicator = "hurst_200"
operator = "cross_above"
target = "0.55"
timeframe = "1h"

Typical use: Detect the moment the market shifts from random/mean-reverting into a trending regime. This is a regime change signal -- you may want to switch from mean-reversion strategies to momentum strategies, or enable a trend-following bot.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where the Hurst Exponent transitions from above the threshold to below it. The previous candle had Hurst >= the target, and the current candle has Hurst < the target.

[[actions.triggers]]
indicator = "hurst_200"
operator = "cross_below"
target = "0.50"
timeframe = "1h"

Typical use: Detect the moment the market loses its trending character. When Hurst drops below 0.50, the trend has broken down and momentum strategies lose their edge. This can be used to exit trend-following positions or pause a momentum bot.

Why: Hurst produces floating-point values calculated to many decimal places. Exact equality (e.g., hurst_200 = 0.50) will almost never be true. Use > or < instead.

Choosing the right operator

  • Use > to gate momentum strategies behind a trending regime. This is the most common Hurst operator.
  • Use < to gate mean-reversion strategies behind an anti-persistent regime.
  • Use cross_above / cross_below to detect regime transitions and switch strategy modes.

TOML Examples

Trending Regime + MACD Entry

Only enter on MACD bullish crossovers when Hurst confirms the market is trending. Without the Hurst filter, MACD crossovers in random-walk markets produce many false signals.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = ">"
  target = "0.55"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "macd_12_26_9"
  operator = "cross_above"
  target = "macd_signal"
  timeframe = "1h"

Mean-Reverting Regime + RSI Entry

Only buy RSI oversold dips when Hurst confirms the market is mean-reverting. In a trending regime, RSI can stay oversold for extended periods as the trend continues lower.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = "<"
  target = "0.45"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

Avoid Random Walk Markets

Gate all entries behind a Hurst filter that excludes the random-walk zone. This strategy only enters when the market has clear character -- either trending (momentum entry) or mean-reverting (dip entry) -- and stays flat when the market is random.

# Momentum entry in trending regime
[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = ">"
  target = "0.55"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_12"
  operator = "cross_above"
  target = "ema_26"
  timeframe = "1h"

# Dip entry in mean-reverting regime
[[actions]]
type = "open_long"
amount = "75 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = "<"
  target = "0.45"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

Hurst + Half-Life Combo

Combine Hurst with Half-Life for a complete mean-reversion setup. Hurst confirms the regime is mean-reverting, and Half-Life confirms the reversion happens fast enough to be tradeable.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = "<"
  target = "0.45"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "halflife_200"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

Tips

Use long periods (200+) for stability

Hurst Exponent estimation is noisy on short lookback windows. With a period of 50 or 100, the value can jump around significantly from candle to candle, making it unreliable as a regime classifier. Use 200 or more candles for a stable reading. The trade-off is slower reaction to regime changes, but for a structural indicator like Hurst, stability matters more than speed.

Hurst is a regime detector, not a timing tool

Hurst tells you what kind of market you are in, not when to buy or sell. Always pair Hurst with a directional indicator. Hurst > 0.55 + MACD cross = trending momentum entry. Hurst < 0.45 + RSI oversold = mean-reversion dip entry. Hurst alone is useless for trade timing.

Combine with directional indicators per regime

Build regime-adaptive strategies: when Hurst says "trending," use momentum indicators (MACD, EMA crossovers, ROC). When Hurst says "mean-reverting," use oscillators (RSI, StochRSI, Z-Score, Bollinger Bands). When Hurst says "random," stay flat. This single indicator decides which strategy gets activated.

Re-evaluate regimes on higher timeframes

Regimes are fractal -- a market can be trending on the daily timeframe but mean-reverting on the 5-minute chart. Compute Hurst on the timeframe that matches your trading horizon. For swing trades (holding days to weeks), use Hurst on daily candles. For intraday trades, use Hurst on hourly candles. Do not mix regime reads across timeframes.

Half-Life -- Half-Life of Mean Reversion

Contents


Overview

The Half-Life of Mean Reversion estimates how many candles it takes for price to revert halfway back to its mean, using OLS regression on an Ornstein-Uhlenbeck process. It answers the question: if price deviates from the mean, how fast does it snap back?

The calculation fits a linear regression to the price series' first differences against its lagged levels (the discrete Ornstein-Uhlenbeck model). The regression coefficient gives the speed of mean reversion, and the half-life is derived as -log(2) / coefficient. A short half-life means price deviations are corrected quickly -- the market snaps back to its mean within a few candles. A long half-life means reversion is sluggish or essentially non-existent.

The key insight: if Hurst Exponent tells you whether the market is mean-reverting, Half-Life tells you how fast that reversion happens. Knowing a market is mean-reverting is only half the picture -- you also need to know the timescale. If the half-life is 15 candles, you can expect a dip to recover halfway within 15 candles and calibrate your exits accordingly. If the half-life is 500 candles, reversion is so slow that it is practically useless for trading -- the statistical property exists but the time horizon is impractical.


Format

halflife_{period}

The period defines the lookback window -- how many candles of price data are used to estimate the mean-reversion speed via OLS regression.

ExamplePeriodUse Case
halflife_100100Moderate lookback -- captures recent regime, reacts faster to changes
halflife_200200Standard -- stable estimate, good balance of accuracy and responsiveness
halflife_500500Very stable -- captures the dominant long-term mean-reversion speed

Period range: 50 to 500.

Value range: 1 to very large (in candles). Practical useful range: 5 to 100 candles.


Understanding Half-Life Values

Half-Life values are expressed in candles (the number of bars on your chart). The interpretation depends on your timeframe:

Half-Life (candles)SpeedInterpretation
1 -- 20Very fastDay-trading territory. Price reverts halfway to the mean within 20 candles. On a 1h chart, that is less than a day. Excellent for short-term mean-reversion trades with tight time exits.
20 -- 50ModerateSwing-trading territory. On a 1h chart, reversion takes 1--2 days. Good for multi-day positions with RSI or Z-Score entries. Set time_in_position to roughly 2x the half-life.
50 -- 100SlowPosition-trading territory. On a 1h chart, reversion takes 2--4 days. Only worthwhile if the deviation is large enough to justify holding that long.
100+Impractically slowReversion exists statistically but takes too long to be tradeable. A half-life of 500 on the hourly chart means roughly 3 weeks to revert halfway. Other market dynamics will dominate before reversion completes.

Note

Half-Life values depend heavily on the timeframe. A half-life of 20 on the 1h chart means ~20 hours. A half-life of 20 on the 1d chart means ~20 days. Always interpret half-life in the context of your chart's timeframe.


Understanding Operators with Half-Life

Each operator behaves differently with Half-Life. Because Half-Life is a regime qualifier (it tells you how fast mean reversion works), it is almost always used in combination with a regime detector (Hurst) and a directional entry trigger (RSI, Z-Score).

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where the Half-Life is below the threshold.

On the chart: Mean reversion is happening fast. Price deviations are being corrected quickly. This trigger stays active for as long as the half-life remains short.

[[actions.triggers]]
indicator = "halflife_200"
operator = "<"
target = "30"
timeframe = "1h"

Typical use: "Only trade mean-reversion strategies when reversion is fast enough to be profitable." A half-life below 30 candles means price should recover halfway within about 30 hours on the hourly chart -- fast enough for swing trades. This filters out markets where reversion is too slow to be practical.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where the Half-Life is above the threshold.

On the chart: Mean reversion is slow or absent. Price deviations persist for long periods before correcting.

[[actions.triggers]]
indicator = "halflife_200"
operator = ">"
target = "100"
timeframe = "1h"

Typical use: Defensive filter -- "do not enter mean-reversion trades when the half-life is too long." If reversion takes 100+ candles, the trade will tie up capital for too long. Some strategies use halflife > 100 as a condition to exit or avoid mean-reversion positions entirely.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where the Half-Life transitions from above the threshold to below it. The previous candle had Half-Life >= the target, and the current candle has Half-Life < the target.

[[actions.triggers]]
indicator = "halflife_200"
operator = "cross_below"
target = "30"
timeframe = "1h"

Typical use: Detect the moment the market transitions into a fast-reverting regime. When the half-life drops below 30, mean reversion just became tradeable. This can trigger the activation of mean-reversion entries.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where the Half-Life transitions from below the threshold to above it. The previous candle had Half-Life <= the target, and the current candle has Half-Life > the target.

[[actions.triggers]]
indicator = "halflife_200"
operator = "cross_above"
target = "50"
timeframe = "1h"

Typical use: Detect the moment mean reversion slows down. When the half-life crosses above 50, the market is no longer reverting fast enough for swing trades. This can be used to disable mean-reversion entries or exit existing positions.

Why: Half-Life produces floating-point values calculated to many decimal places. Exact equality (e.g., halflife_200 = 20) will almost never be true. Use < or > instead.

Choosing the right operator

  • Use < to ensure mean reversion is fast enough to trade. This is the most common Half-Life operator.
  • Use > to avoid or exit positions when reversion is too slow.
  • Use cross_below / cross_above to detect the exact moment the reversion speed changes regime.

TOML Examples

Short Half-Life + RSI Dip Buy

Only buy RSI oversold dips when the half-life confirms fast mean reversion. A half-life below 25 candles on the hourly chart means price should recover halfway within about a day -- ideal for swing dip-buying.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "halflife_200"
  operator = "<"
  target = "25"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

Half-Life-Based Time Exit

Use the half-life to calibrate your exit timing. If the half-life is 20 candles, the market should revert halfway in 20 bars and fully in roughly 40--60 bars. Set time_in_position to 2x the estimated half-life (here ~40 candles) as a time-based stop.

# Entry: dip buy with half-life confirmation
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "halflife_200"
  operator = "<"
  target = "20"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

# Exit: time-based stop calibrated to ~2x half-life
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "time_in_position"
  candles = 40
  timeframe = "1h"

Complete Regime Setup: Hurst + Half-Life + Entry

The full mean-reversion stack: Hurst confirms the market IS mean-reverting, Half-Life confirms reversion is fast enough, and RSI provides the actual entry signal.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = "<"
  target = "0.45"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "halflife_200"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"
  max_count = 1

Tips

Pair with Hurst: Hurst says IF, Half-Life says WHEN

Hurst Exponent and Half-Life are complementary. Hurst tells you whether the market is mean-reverting (H < 0.5). Half-Life tells you how fast that reversion happens. A market with H = 0.35 and a half-life of 15 candles is a mean-reversion paradise. A market with H = 0.40 and a half-life of 300 candles is mean-reverting in theory but untradeable in practice. Always use both together.

Use Half-Life to calibrate time_in_position

If the estimated half-life is N candles, price should revert halfway in N candles and approximately fully in 2--3x N candles. Set your time_in_position exit to roughly 2x the half-life as a safety stop. If the trade has not recovered by then, the regime may have changed and you should exit.

Long periods for stable estimation

Like Hurst, the Half-Life estimate is noisy on short lookback windows. The OLS regression needs enough data points to produce a reliable slope estimate. Use 200+ candles for the period. Shorter periods may produce wildly fluctuating half-life values that are not actionable.

High half-life = do not trade mean reversion

When the half-life exceeds 100 candles on your timeframe, mean reversion is too slow to be profitable. On the hourly chart, 100 candles is over 4 days -- other market dynamics (news, macro events, sentiment shifts) will dominate before the statistical reversion completes. If the half-life is high, either switch to momentum strategies or stay flat.

Vol Regime -- Volatility Regime Classifier

Contents


Overview

The Volatility Regime Classifier categorizes the current volatility environment into three discrete regimes: low (1), medium (2), and high (3). Instead of requiring you to set raw ATR or RSTD thresholds that are asset-specific and timeframe-specific, vol_regime automatically classifies the current volatility by comparing it against its own recent history using rolling standard deviation percentiles.

The calculation works by computing the rolling standard deviation of price over the lookback period, then determining which percentile the current value falls into relative to the distribution of recent values. The bottom third maps to regime 1 (low), the middle third to regime 2 (medium), and the top third to regime 3 (high). This makes the indicator self-calibrating -- it adapts to the asset and timeframe automatically.

The key insight: instead of guessing absolute volatility thresholds (which differ for every trading pair and timeframe), vol_regime gives you a universal classification. Regime 1 means volatility is compressed relative to recent history -- compression precedes breakout, so this is the setup phase. Regime 2 means normal conditions -- most strategies work here. Regime 3 means volatility is elevated relative to recent history -- chaotic conditions where wider stops are needed and risk is higher. This single number replaces the need to manually calibrate ATR or RSTD thresholds for each pair.


Format

vol_regime_{period}

The period defines the lookback window -- how many candles of data are used to compute the rolling standard deviation and its percentile classification.

ExamplePeriodUse Case
vol_regime_5050Faster adaptation -- reacts quickly to volatility shifts, but more regime flickering
vol_regime_100100Standard -- good balance of stability and responsiveness
vol_regime_200200Very stable -- slower to reclassify, but fewer false regime changes

Period range: 50 to 500.

Value range: Discrete: 1, 2, or 3.


Understanding Vol Regime Values

Unlike most indicators that produce continuous floating-point values, Vol Regime outputs one of three discrete integers:

ValueRegimeInterpretation
1Low volatilityThe market is calm and compressed. Volatility is in the bottom third of its recent range. This is the squeeze/setup phase -- low volatility periods often precede explosive breakouts. Good for accumulation and squeeze-breakout strategies.
2Medium volatilityNormal market conditions. Volatility is in the middle third of its recent range. Most strategies work here without special adjustments. This is the "default" regime.
3High volatilityThe market is chaotic and volatile. Volatility is in the top third of its recent range. Wider stops are needed, position sizes should be reduced, and false signals increase. Some strategies should stay flat entirely in regime 3.

Note

Because vol_regime outputs discrete values (1, 2, 3), you use fractional thresholds like 1.5 and 2.5 with < and > operators to identify specific regimes. vol_regime < 1.5 means "regime 1 (low)." vol_regime > 2.5 means "regime 3 (high)." This avoids the floating-point equality problem.


Understanding Operators with Vol Regime

Each operator behaves differently with Vol Regime. Because the indicator outputs discrete values (1, 2, 3), the operator thresholds are set between values (1.5 and 2.5) to cleanly identify regimes.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where the vol regime value is below the threshold.

On the chart: The market is in a low-volatility phase. Price is moving in small increments relative to recent history. This trigger stays active for the entire duration of the low-vol regime.

[[actions.triggers]]
indicator = "vol_regime_100"
operator = "<"
target = "1.5"
timeframe = "1h"

Typical use: "Only enter when volatility is compressed." Low-volatility regimes are the setup phase for squeeze breakouts. Combining vol_regime < 1.5 with a directional breakout signal captures the expansion move from a compressed base.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where the vol regime value is above the threshold.

On the chart: The market is in a high-volatility phase. Price is swinging wildly relative to recent history. This trigger stays active for the entire duration of the high-vol regime.

[[actions.triggers]]
indicator = "vol_regime_100"
operator = ">"
target = "2.5"
timeframe = "1h"

Typical use: Defensive filter -- "exit or do not enter during high-volatility regimes." Regime 3 conditions produce more false signals, wider spreads, and larger drawdowns. Some strategies use vol_regime > 2.5 as a sell trigger or as a gate to prevent new entries.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where vol regime transitions from above the threshold to below it. The previous candle had vol_regime >= the target, and the current candle has vol_regime < the target.

[[actions.triggers]]
indicator = "vol_regime_100"
operator = "cross_below"
target = "1.5"
timeframe = "1h"

Typical use: Detect the moment the market enters a low-volatility regime. This signals that the squeeze phase has begun -- start watching for directional breakout signals.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where vol regime transitions from below the threshold to above it. The previous candle had vol_regime <= the target, and the current candle has vol_regime > the target.

[[actions.triggers]]
indicator = "vol_regime_100"
operator = "cross_above"
target = "2.5"
timeframe = "1h"

Typical use: Detect the moment volatility spikes into the high regime. This can trigger a defensive exit -- when vol regime crosses above 2.5, the market just became chaotic and existing positions may need to be closed or hedged.

Why: Although vol_regime outputs integers (1, 2, 3), the underlying computation may produce floating-point values that are very close to but not exactly equal to these integers. Use < 1.5 for regime 1, > 1.5 combined with < 2.5 for regime 2, and > 2.5 for regime 3.

Choosing the right operator

  • Use < 1.5 to identify low-volatility regimes (regime 1) for squeeze-breakout strategies.
  • Use > 2.5 to identify high-volatility regimes (regime 3) for defensive exits or position sizing.
  • Use cross_below 1.5 / cross_above 2.5 to detect the exact moment of regime transitions.

TOML Examples

Buy Only in Low Volatility Regime

Enter when the market is in a low-volatility squeeze and price breaks above the upper Bollinger Band. The compressed volatility (regime 1) combined with a directional breakout often produces outsized moves.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "vol_regime_100"
  operator = "<"
  target = "1.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "bb_upper"
  timeframe = "1h"

Exit When High Volatility Arrives

Sell the position when the market enters a high-volatility regime. Regime 3 conditions are chaotic -- protect profits by exiting before volatility causes a sharp drawdown.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "vol_regime_100"
  operator = "cross_above"
  target = "2.5"
  timeframe = "1h"

Adaptive Entry: Low Vol + Trend

Combine the low-volatility squeeze with a trend confirmation. Only buy when volatility is compressed (regime 1) AND the daily trend is bullish (SMA 50 above SMA 200). This is the classic "buy the quiet before the storm" setup within a confirmed uptrend.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "vol_regime_100"
  operator = "<"
  target = "1.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "0.5"
  timeframe = "1h"

Vol Regime + Hurst Combo

The ultimate regime-aware strategy. Hurst confirms the market is trending, vol regime confirms volatility is not chaotic, and MACD provides the entry signal. This triple filter produces high-conviction entries in favorable conditions.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst_200"
  operator = ">"
  target = "0.55"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vol_regime_100"
  operator = "<"
  target = "2.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "macd_12_26_9"
  operator = "cross_above"
  target = "macd_signal"
  timeframe = "1h"

Tips

Use 1.5 and 2.5 as thresholds

Because vol_regime outputs discrete integers (1, 2, 3), use fractional thresholds to cleanly identify regimes: < 1.5 for low volatility (regime 1), > 2.5 for high volatility (regime 3). For medium volatility, use > 1.5 AND < 2.5 as two separate triggers. This is cleaner and more reliable than trying to use = with integer values.

Longer periods = more stable classification

Short periods (50) cause more frequent regime flickering -- the classification bounces between regimes as individual candles enter and leave the lookback window. Longer periods (100--200) produce more stable regime reads. Use 100 as a good default, and increase to 200 if you see too many false regime transitions.

Combine with Hurst for full regime awareness

Vol Regime tells you about volatility conditions. Hurst tells you about trend/mean-reversion character. Together they give you a complete market regime picture: Is the market trending or mean-reverting? (Hurst.) Is volatility compressed, normal, or elevated? (Vol Regime.) Use both to select the right strategy AND the right risk parameters for the current environment.

VWAP -- Volume-Weighted Average Price

Contents


Overview

Volume-Weighted Average Price (VWAP) is the average price an asset has traded at, weighted by volume, cumulative from the session start. Unlike a simple moving average that treats every candle equally, VWAP gives more weight to prices where more volume transacted. It answers the question: what is the true average price that reflects where actual trading occurred?

The calculation is cumulative:

VWAP = cumulative(typical_price x volume) / cumulative(volume)

Where typical_price = (high + low + close) / 3. At each candle, the numerator and denominator are accumulated from the first available candle. This means VWAP is anchored -- it does not have a sliding window like SMA or EMA. Early in the session, VWAP is more responsive to new data. As the session progresses, it becomes increasingly stable because it incorporates more and more historical volume.

VWAP is THE institutional benchmark. Large funds and algorithmic traders use VWAP to measure execution quality -- a buy order filled below VWAP is considered a "good" fill, while a fill above VWAP is considered "poor." This institutional usage creates a self-fulfilling dynamic: price below VWAP attracts institutional buying (they are getting a discount), while price above VWAP attracts institutional selling (they are locking in above-average prices). For retail traders, this means VWAP acts as a dynamic support/resistance level that institutional players actually respect.


Format

vwap

VWAP has no period parameter. It is cumulative from the first available candle in the session.

ExamplePeriodUse Case
vwapNone (cumulative)The volume-weighted average price -- institutional benchmark for fair value

Period range: Not applicable -- VWAP is always cumulative.

Value range: Price-denominated (same units as the asset's price). VWAP tracks very close to the actual price since it is an average of traded prices.


Understanding VWAP Values

VWAP values are in the same currency as the asset's price. The key is not the absolute value but the relationship between price and VWAP.

Price vs. VWAPInterpretation
Price well below VWAPTrading at a discount to volume-weighted fair value. Institutional buyers may see this as attractive. Potential support zone.
Price slightly below VWAPNear fair value, leaning cheap. VWAP may act as a magnet pulling price back up.
Price at VWAPAt the volume-weighted average -- equilibrium. No directional edge.
Price slightly above VWAPNear fair value, leaning expensive. VWAP may act as a magnet pulling price back down.
Price well above VWAPTrading at a premium to volume-weighted fair value. Institutional sellers may see this as a good exit. Potential resistance zone.

Note

VWAP is cumulative and anchored to the session start. As the session progresses, VWAP becomes increasingly stable and harder to move. Early in a session, VWAP is more volatile and reactive. The most reliable VWAP signals come after enough volume has been accumulated to establish a meaningful average.


Understanding Operators with VWAP

Each operator behaves differently with VWAP. Because VWAP is a dynamic price level (not an oscillator), it is most commonly compared against price or used as a target for other indicators.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where price is above VWAP. This indicates the asset is trading at a premium to the volume-weighted average.

On the chart: Imagine the VWAP line drawn through the candles. Whenever price is above this line, the trigger is active. The asset is "expensive" relative to where most volume traded.

[[actions.triggers]]
indicator = "price"
operator = ">"
target = "vwap"
timeframe = "1h"

Typical use: Filter for bullish conditions. Price above VWAP means buyers are in control and willing to pay above the average. Combine with momentum indicators for long entries.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where price is below VWAP. This indicates the asset is trading at a discount to the volume-weighted average.

On the chart: Price is below the VWAP line. The asset is "cheap" relative to where most volume traded -- institutional buyers may be accumulating.

[[actions.triggers]]
indicator = "price"
operator = "<"
target = "vwap"
timeframe = "1h"

Typical use: Discount-buying strategy. When price is below VWAP, you are buying at a price that institutional algorithms consider a bargain. Combine with an RSI dip or support level for precision.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where price transitions from below VWAP to above it. The previous candle had price <= VWAP, and the current candle has price > VWAP.

[[actions.triggers]]
indicator = "price"
operator = "cross_above"
target = "vwap"
timeframe = "1h"

Typical use: Bullish VWAP crossover entry. When price crosses above VWAP, it signals that buyers have seized control and pushed the price above the institutional fair value line. This is a commonly used intraday entry signal.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where price transitions from above VWAP to below it. The previous candle had price >= VWAP, and the current candle has price < VWAP.

[[actions.triggers]]
indicator = "price"
operator = "cross_below"
target = "vwap"
timeframe = "1h"

Typical use: Bearish VWAP crossover exit. When price drops below VWAP, sellers have pushed the market below institutional fair value. This can signal the start of distribution or a shift to bearish control.

Why: VWAP and price are both floating-point values that change with every candle. Exact equality between price and VWAP will almost never occur. Use < or > instead, or use cross_above / cross_below to detect the moment price transitions through VWAP.

Choosing the right operator

  • Use < / > when you want to act while price is on one side of VWAP (state-based). Pair with max_count to limit repeated firing.
  • Use cross_above / cross_below when you want to act at the moment price pierces through VWAP (event-based). No max_count needed -- crossovers fire once by nature.

TOML Examples

Buy Below VWAP

The simplest VWAP strategy. Accumulate when price is trading below the institutional benchmark. The max_count limits entries so you do not keep buying on every candle below VWAP.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "vwap"
  timeframe = "1h"
  max_count = 3

Sell Above VWAP

Exit positions when price is trading above VWAP -- you are selling at a premium to the volume-weighted average.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "price"
  operator = ">"
  target = "vwap"
  timeframe = "1h"
  max_count = 1

VWAP Bounce with Deviation Confirmation

Buy when price is below VWAP (discount zone) AND VWAP Deviation confirms the discount is statistically significant (more than 1.5 standard deviations below VWAP). This avoids buying minor VWAP dips that lack statistical edge.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "vwap"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vwap_dev_20"
  operator = "<"
  target = "-1.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

Price Crosses Above VWAP Entry

Enter at the moment price crosses above VWAP with a bullish macro trend. This is the classic intraday VWAP breakout.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "cross_above"
  target = "vwap"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

Tips

VWAP acts as dynamic support and resistance

Because institutional algorithms buy below VWAP and sell above it, VWAP functions as a dynamic support/resistance level that adapts to each session's trading activity. Price tends to gravitate toward VWAP and often bounces off it. This makes VWAP a powerful level for entries and exits -- far more meaningful than arbitrary fixed support/resistance lines.

Combine with VWAP Deviation for statistical rigor

Raw VWAP tells you "price is below fair value." VWAP Deviation (vwap_dev_20) tells you "price is 2.3 standard deviations below fair value." The combination is far more powerful: VWAP gives you the level, VWAP Dev gives you the statistical significance. A VWAP dip at -0.5 dev is noise; a VWAP dip at -2.0 dev is a rare statistical event worth acting on.

VWAP is cumulative -- it smooths over time

VWAP accumulates from the session start, so it becomes increasingly stable as more data is incorporated. Early in a session, VWAP is volatile and reactive. After many candles, it takes massive volume to move VWAP significantly. This means VWAP signals are most reliable after the session has been running long enough to establish a meaningful average.

VWAP is an institutional benchmark, not a crystal ball

VWAP tells you where the market considers fair value based on volume-weighted prices. It does not predict direction. In a strong downtrend, price can stay below VWAP for the entire session -- buying every VWAP discount would be disastrous. Always combine VWAP with trend confirmation (SMA crossovers, RSI) and risk management (stop-losses, max_count).

VWAP Dev -- VWAP Deviation

Contents


Overview

VWAP Deviation measures how many standard deviations the current closing price is from VWAP. It adds statistical rigor to VWAP analysis by quantifying not just whether price is above or below VWAP, but how extreme the deviation is relative to recent behavior.

The calculation is:

VWAP Dev = (close - VWAP) / rolling_std(close - VWAP, period)

Where rolling_std is the standard deviation of the difference between close and VWAP over the lookback period. This is conceptually identical to a Z-Score, but measured relative to VWAP instead of a simple moving average. The result tells you: "the current close is X standard deviations away from VWAP."

The key insight: instead of just knowing "price is below VWAP," VWAP Deviation tells you "price is 2.5 standard deviations below VWAP" -- a statistically rare event. Under normal distribution assumptions, values beyond +/-2.0 occur only about 5% of the time. The further from zero, the more extreme and unusual the current price is relative to the volume-weighted average. This transforms VWAP from a simple level into a statistically grounded framework for identifying high-probability mean-reversion opportunities.


Format

vwap_dev_{period}

The period defines the lookback window used to calculate the rolling standard deviation of the close-to-VWAP difference.

ExamplePeriodUse Case
vwap_dev_2020Standard -- responsive to recent VWAP deviation patterns
vwap_dev_5050Slower -- captures longer-term VWAP deviation norms, fewer false extremes
vwap_dev_1010Fast -- reacts quickly but noisier, more frequent extreme readings

Period range: 5 to 500.

Value range: Typically -4.0 to +4.0 (like Z-Score). 0 = price is at VWAP. Negative = price is below VWAP. Positive = price is above VWAP. Extreme values beyond +/-3.0 are rare and represent significant statistical events.


Understanding VWAP Dev Values

VWAP Dev uses a standardized scale (standard deviations), so its thresholds are universal -- unlike raw VWAP which is denominated in the asset's price currency. A VWAP Dev of -2.0 means the same thing for BTC, ETH, or SOL: price is 2 standard deviations below VWAP.

VWAP Dev RangeZoneInterpretation
Below -2.0Extreme discountPrice is far below VWAP -- a statistically rare event. Strong mean-reversion potential. Institutional buyers may view this as a significant bargain.
-2.0 to -1.0Moderate discountPrice is meaningfully below VWAP. Discount is notable but not extreme. Worth watching for confirmation.
-1.0 to 0Slight discountPrice is near VWAP, slightly below. Weak directional signal. Normal variation.
0 to +1.0Slight premiumPrice is near VWAP, slightly above. Normal variation. No strong signal.
+1.0 to +2.0Moderate premiumPrice is meaningfully above VWAP. Profits may be taken. Overbought relative to volume-weighted average.
Above +2.0Extreme premiumPrice is far above VWAP -- a statistically rare event. Strong mean-reversion potential to the downside. Consider taking profits or exiting.

Note

VWAP Dev is based on the same statistical framework as Z-Score. Under a normal distribution, values beyond +/-2.0 occur about 5% of the time, and values beyond +/-3.0 occur about 0.3% of the time. Crypto markets are not perfectly normal (they have fat tails), so extreme values occur somewhat more often -- but they still represent meaningful statistical events.


Understanding Operators with VWAP Dev

Each operator behaves differently with VWAP Dev. Because VWAP Dev is a standardized measure of deviation, it excels at identifying extremes for mean-reversion strategies.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where VWAP Dev is below the threshold. This detects periods when price is trading at a statistically significant discount to VWAP.

On the chart: Price has drifted far below the VWAP line. The further negative the VWAP Dev reading, the more extreme the discount. This trigger stays active for as long as VWAP Dev remains below the threshold.

[[actions.triggers]]
indicator = "vwap_dev_20"
operator = "<"
target = "-2.0"
timeframe = "1h"

Typical use: Mean-reversion buy signal. When VWAP Dev is below -2.0, price is at a statistically rare discount to VWAP. This is the primary VWAP Dev operator for entry strategies.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where VWAP Dev is above the threshold. This detects periods when price is trading at a statistically significant premium to VWAP.

On the chart: Price has drifted far above the VWAP line. The higher the VWAP Dev reading, the more stretched the premium.

[[actions.triggers]]
indicator = "vwap_dev_20"
operator = ">"
target = "1.5"
timeframe = "1h"

Typical use: Exit signal or overextension warning. When VWAP Dev exceeds +1.5 or +2.0, price is stretched above its volume-weighted average. Profits should be considered. For aggressive strategies, this can be a short signal.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where VWAP Dev transitions from above the threshold to below it. The previous candle had VWAP Dev >= the target, and the current candle has VWAP Dev < the target.

[[actions.triggers]]
indicator = "vwap_dev_20"
operator = "cross_below"
target = "-2.0"
timeframe = "1h"

Typical use: Detect the moment price enters extreme discount territory. This captures the exact candle where the deviation becomes statistically significant -- useful for precision entries that fire only once at the critical moment.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where VWAP Dev transitions from below the threshold to above it. The previous candle had VWAP Dev <= the target, and the current candle has VWAP Dev > the target.

[[actions.triggers]]
indicator = "vwap_dev_20"
operator = "cross_above"
target = "2.0"
timeframe = "1h"

Typical use: Detect the moment price enters extreme premium territory. Useful as an exit trigger -- the exact candle where the premium becomes statistically overextended.

Why: VWAP Dev produces floating-point values that are rarely exactly equal to any specific number. Use < or > instead.

Choosing the right operator

  • Use < for mean-reversion buys -- price is at a statistically significant discount to VWAP. This is the most common VWAP Dev operator.
  • Use > for exit signals or profit-taking -- price is at a statistically significant premium.
  • Use cross_below / cross_above to detect the exact moment price enters an extreme deviation zone.

TOML Examples

Extreme VWAP Discount

Buy when price is more than 2 standard deviations below VWAP. This is a statistically rare event that offers a high-probability bounce. The max_count prevents repeated entries while price remains deeply discounted.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "vwap_dev_20"
  operator = "<"
  target = "-2.0"
  timeframe = "1h"
  max_count = 2

VWAP Premium Exit

Take profits when price is extended more than 1.5 standard deviations above VWAP. The premium is significant enough that mean reversion is likely.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "vwap_dev_20"
  operator = ">"
  target = "1.5"
  timeframe = "1h"
  max_count = 1

VWAP Dev Combined with VWAP Level

Use both raw VWAP (the level) and VWAP Dev (the significance). Price must be below VWAP AND the deviation must be statistically meaningful. This ensures you are not just buying a trivial dip below VWAP but a significant one.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "vwap"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "vwap_dev_20"
  operator = "<"
  target = "-1.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

VWAP Dev and RSI Confluence

Combine VWAP Dev with RSI for double confirmation. When both statistical deviation from VWAP AND momentum oscillator agree that price is oversold, the setup has strong confluence.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "vwap_dev_20"
  operator = "<"
  target = "-2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "obv"
  operator = ">"
  target = "obv_sma_20"
  timeframe = "1h"

Tips

Universal thresholds -- works across all assets

Unlike raw VWAP (which is denominated in the asset's price currency), VWAP Dev is standardized in standard deviations. A reading of -2.0 means the same thing for BTC, ETH, SOL, or any asset: price is 2 standard deviations below VWAP. This makes it easy to apply the same threshold logic across your entire portfolio without recalibrating per asset.

Combine raw VWAP + VWAP Dev for level and significance

Raw VWAP gives you the price level -- where institutional fair value sits. VWAP Dev gives you the statistical significance -- how extreme the current deviation is. Used together, they answer two questions at once: "Is price below fair value?" (VWAP) and "Is this discount large enough to matter?" (VWAP Dev). The combination is far more powerful than either alone.

VWAP Dev is more meaningful than raw price distance

Knowing that BTC is "$500 below VWAP" is not actionable -- you need context. Is $500 a big move or a small one? VWAP Dev normalizes this by telling you "price is 2.1 standard deviations below VWAP." Now you know it is statistically significant regardless of the dollar amount. This normalization is what makes VWAP Dev valuable for systematic trading.

Mean reversion is not guaranteed

VWAP Dev identifies statistically extreme deviations, not guaranteed reversals. In a strong trend, VWAP Dev can stay at -3.0 for extended periods as VWAP lags behind a rapidly falling price. Always pair VWAP Dev with trend filters (SMA crossovers) and risk management (stop-losses, max_count). A -2.0 reading during a confirmed downtrend may just be the beginning of a larger selloff.

Skew -- Rolling Skewness

Contents


Overview

Rolling Skewness measures the asymmetry of the return distribution over a rolling window. It is computed on log returns (the natural logarithm of price ratios between consecutive candles). Skewness answers a fundamental question: is the market more prone to sudden crashes or sudden rallies?

A symmetric distribution (skewness = 0) means upside moves and downside moves are roughly mirror images of each other. But financial markets are rarely symmetric. Skewness quantifies the direction of that asymmetry:

  • Negative skew means the return distribution has a long left tail. Most returns are clustered slightly positive, but occasional large drops drag the tail to the left. The market is prone to sudden crashes -- "left-tail events." This is the classic risk profile before major crypto selloffs.
  • Positive skew means the return distribution has a long right tail. Most returns are clustered slightly negative or flat, but occasional large rallies push the tail to the right. Upside surprises are more likely than downside surprises.
  • Zero skew means the distribution is roughly symmetric. Neither tail is dominant.

The key insight: skewness tells you WHERE the risk is hiding. In a negatively skewed market, the average return may look fine, but the risk of a sudden crash is elevated. For contrarian strategies, negative skew combined with oversold conditions creates high-probability bounce setups -- the market has already experienced panic, and the left tail has been "spent." For trend-following strategies, positive skew confirms that upside momentum has room to run.


Format

skew_{period}

The period defines the rolling lookback window -- how many candles of log returns are included in the skewness calculation.

ExamplePeriodUse Case
skew_6060Standard -- captures distribution shape over a meaningful window without excessive smoothing
skew_100100Slower -- more stable reading, better for regime identification
skew_3030Faster -- reacts quicker to distribution shifts, noisier

Period range: 20 to 500.

Value range: Typically -3.0 to +3.0. Values near 0 indicate a symmetric distribution. Values beyond +/-1.0 indicate significant asymmetry. Extreme values beyond +/-2.0 are rare.


Understanding Skewness Values

Skewness values are standardized and universal -- a reading of -1.5 means the same degree of left-tail asymmetry regardless of the asset or timeframe.

Skewness RangeZoneInterpretation
Below -1.0Heavily left-skewedPanic mode. The return distribution has a pronounced left tail -- large drops have dominated. The market is crash-prone. For contrarian traders: if combined with oversold momentum, the crash may be exhausting itself.
-1.0 to -0.3Moderately negativeReturns skew to the downside. Downside risk is elevated but not extreme. Caution is warranted for new long entries without confirmation.
-0.3 to +0.3Roughly symmetricNo significant asymmetry. The distribution of returns is balanced. Neither tail dominates. Neutral reading.
+0.3 to +1.0Moderately positiveReturns skew to the upside. Upside surprises are more likely than downside crashes. Favorable environment for trend-following longs.
Above +1.0Heavily right-skewedStrong upside bias. The return distribution has a pronounced right tail -- large rallies have been occurring. Momentum is heavily bullish. For contrarian traders: the upside may be overextended.

Note

Skewness is a lagging measure -- it describes what the distribution of returns has looked like over the past N candles, not what it will look like in the future. However, skewness regimes tend to persist for meaningful periods, making it a useful regime filter. A market that has been negatively skewed is statistically more likely to experience further left-tail events until the regime shifts.


Understanding Operators with Skewness

Each operator behaves differently with skewness. Because skewness is a distribution shape indicator, it is most powerful as a regime filter combined with directional triggers.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where skewness is below the threshold. This detects periods when the return distribution is negatively skewed -- crash risk is elevated or a crash has recently occurred.

On the chart: Recent returns have been dominated by sharp drops. The left tail of the distribution is elongated. This trigger stays active for as long as the skewness reading remains below the threshold.

[[actions.triggers]]
indicator = "skew_60"
operator = "<"
target = "-1.0"
timeframe = "1h"

Typical use: Contrarian panic-buying. When skewness is deeply negative, the market has been experiencing sharp drops. Combined with oversold RSI, this identifies washout moments where selling pressure is exhausted. Also used as a risk filter to avoid entries during crash-prone regimes.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where skewness is above the threshold. This detects periods when the return distribution is positively skewed -- upside surprises are more frequent.

On the chart: Recent returns have included sharp rallies. The right tail of the distribution is elongated. Momentum favors the upside.

[[actions.triggers]]
indicator = "skew_60"
operator = ">"
target = "0.5"
timeframe = "1h"

Typical use: Trend confirmation filter. Positive skewness confirms that the market's return distribution favors upside moves. Combine with trend-following entries (EMA cross, price above SMA) for higher-conviction longs.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where skewness transitions from above the threshold to below it. The previous candle had skewness >= the target, and the current candle has skewness < the target.

[[actions.triggers]]
indicator = "skew_60"
operator = "cross_below"
target = "-1.0"
timeframe = "1h"

Typical use: Detect the moment the market enters a crash regime. Skewness has just shifted to heavily negative -- the distribution has tilted toward left-tail events. This can be an exit signal to protect existing positions.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where skewness transitions from below the threshold to above it. The previous candle had skewness <= the target, and the current candle has skewness > the target.

[[actions.triggers]]
indicator = "skew_60"
operator = "cross_above"
target = "0"
timeframe = "1h"

Typical use: Detect the moment the market exits a negative skew regime. Skewness returning to zero or positive means the distribution is normalizing -- the crash regime may be ending. This can signal the "all clear" to resume normal entries.

Why: Skewness produces floating-point values that are rarely exactly equal to any specific number. Use < or > instead.

Choosing the right operator

  • Use < to detect negative skew regimes -- crash-prone or post-crash environments. The most common skewness operator.
  • Use > to confirm positive skew -- upside momentum in the return distribution.
  • Use cross_below / cross_above to detect regime transitions -- the exact moment the distribution shifts.

TOML Examples

Panic Buyer: Negative Skew + RSI Oversold

The contrarian play: when skewness is deeply negative (the market has been crashing) AND RSI confirms oversold conditions, buy the panic. The logic: left-tail events have already occurred, selling pressure is likely exhausted, and a mean-reversion bounce is probable.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "skew_60"
  operator = "<"
  target = "-1.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "25"
  timeframe = "1h"
  max_count = 2

Ride the Positive Skew Upside

When skewness is positive (upside surprises dominate) and the trend is confirmed by EMA, enter long. The return distribution itself confirms that momentum favors buyers.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "skew_100"
  operator = ">"
  target = "0.5"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_20"
  operator = ">"
  target = "ema_50"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "60"
  timeframe = "1h"

Exit on Extreme Negative Skew Shift

Protect existing positions when skewness suddenly plunges below -1.5. This signals a regime shift toward crash-prone conditions -- the distribution is becoming dangerously asymmetric.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "skew_60"
  operator = "cross_below"
  target = "-1.5"
  timeframe = "1h"

Skewness Combined with Kurtosis

The full distribution picture: buy when skewness is negative (crash has happened) but kurtosis is low (the market is not producing extreme moves anymore -- the volatility storm has passed). This ensures you are buying after the crash, not during it.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "skew_60"
  operator = "<"
  target = "-0.8"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "kurt_60"
  operator = "<"
  target = "2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_above"
  target = "30"
  timeframe = "1h"

Tips

Negative skew = crash risk, not crash certainty

A heavily negative skewness reading tells you that the recent return distribution is dominated by left-tail events -- large drops have been happening. This does not guarantee another crash is imminent. It means the environment is crash-prone. For contrarian traders, this is the setup: when combined with oversold indicators, negative skew identifies moments where the crash has already happened and a bounce is probable.

Combine with Kurtosis for the full distribution picture

Skewness tells you which direction the tail risk is. Kurtosis tells you how fat the tails are. Together, they describe the complete shape of the return distribution. Negative skew + high kurtosis = active crash in progress (large, frequent extreme moves to the downside). Negative skew + low kurtosis = crash has exhausted itself (the skew persists in the data but extreme moves have stopped). The second scenario is the higher-probability bounce setup.

Use longer periods for stability

Short-period skewness (20-30 candles) is noisy and can flip rapidly between positive and negative. For regime identification, longer periods (60-100 candles) produce more stable readings that better capture the underlying distribution shape. Reserve short periods for lower timeframes (1m, 5m) where you need faster reaction. For hourly and daily charts, 60-100 periods are recommended.

Skewness is backward-looking

Skewness describes what the return distribution looked like over the past N candles. It does not predict what will happen next. However, distribution regimes tend to be persistent -- a market that has been negatively skewed often remains so for a meaningful period. Use skewness as a regime filter to set context, then rely on real-time indicators (RSI, price crossovers) for precise timing.

Kurt -- Rolling Kurtosis

Contents


Overview

Rolling Kurtosis measures how fat the tails of the return distribution are over a rolling window. It is computed on log returns using excess kurtosis (kurtosis relative to the normal distribution). Kurtosis answers a critical risk question: is the market producing more extreme moves than expected?

The formula computes the fourth moment of the return distribution, normalized by its variance, minus 3:

Kurtosis = E[(r - mean)^4] / std^4 - 3

The subtraction of 3 makes the result relative to the normal distribution (which has a kurtosis of 3). This means a reading of 0 indicates the tails match what you would expect from a normal distribution. Positive values mean the tails are fatter -- extreme moves are happening more often than a bell curve would predict.

  • High kurtosis (fat tails) means the market is producing extreme moves -- both up and down -- more frequently than normal. This is the domain of gap opens, flash crashes, and "impossible" events. Stop-losses get blown through because price jumps past them.
  • Low kurtosis (thin tails) means the market is behaving predictably. Returns cluster near the mean with few outliers. This is a safe, stable environment where systematic strategies perform well.
  • Zero kurtosis means the tails match the normal distribution exactly. This is the theoretical baseline.

The key insight: kurtosis is a risk indicator. It does not tell you which direction the market will move -- it tells you how extreme the moves are likely to be. High kurtosis environments are dangerous because they produce the outsized moves that destroy positions. Low kurtosis environments are where systematic strategies thrive because price behavior is predictable and well-behaved.


Format

kurt_{period}

The period defines the rolling lookback window -- how many candles of log returns are included in the kurtosis calculation.

ExamplePeriodUse Case
kurt_6060Standard -- captures tail behavior over a meaningful window
kurt_100100Slower -- more stable regime identification, filters out transient spikes
kurt_3030Faster -- reacts quicker to changing tail behavior, noisier

Period range: 20 to 500.

Value range: Typically -2.0 to +10.0. 0 = normal distribution tails. Positive = fat tails (more extremes than normal). Negative = thin tails (fewer extremes than normal). Values above 4 indicate a highly volatile, tail-heavy environment.


Understanding Kurtosis Values

Kurtosis values are standardized (excess kurtosis relative to the normal distribution), making them universal across assets and timeframes.

Kurtosis RangeZoneInterpretation
Below 0Thin tailsFewer extreme moves than a normal distribution. The market is highly predictable. Ideal environment for systematic strategies -- returns are well-behaved.
0 to 2Near-normalTail behavior is close to what a normal distribution would produce. Safe for most strategies. Occasional outliers occur but at expected frequency.
2 to 4Moderately fat tailsExtreme moves are happening more often than expected. The market is producing occasional surprises. Be cautious with tight stops and leveraged positions.
Above 4Very fat tailsDanger zone. Extreme moves are frequent and large. This is when "impossible" events occur -- gap moves, flash crashes, violent whipsaws. Risk management is critical. Reduce position size or avoid new entries entirely.

Note

Crypto markets tend to have higher baseline kurtosis than traditional markets because of their 24/7 nature, thin order books, and susceptibility to news-driven events. A kurtosis of 2-3 is relatively common for crypto on hourly timeframes. Calibrate your thresholds through backtesting on your specific pair.


Understanding Operators with Kurtosis

Each operator behaves differently with kurtosis. Because kurtosis is a risk/environment indicator, it is almost always used as a filter to gate entries and exits, not as a standalone signal.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where kurtosis is below the threshold. This detects periods when the return distribution has thin or normal tails -- a safe, predictable trading environment.

On the chart: Returns are well-behaved. There are no extreme outliers in the recent lookback window. Price moves are consistent and predictable.

[[actions.triggers]]
indicator = "kurt_60"
operator = "<"
target = "2.0"
timeframe = "1h"

Typical use: Safety filter for entries. Only enter new positions when kurtosis is low -- the environment is predictable and stop-losses are likely to function as intended. This protects against opening positions right before extreme events.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where kurtosis is above the threshold. This detects periods when the return distribution has fat tails -- extreme moves are happening more often than expected.

On the chart: Price has been making sharp, unexpected moves. The recent return history includes large outliers -- spikes, crashes, or violent reversals.

[[actions.triggers]]
indicator = "kurt_60"
operator = ">"
target = "4.0"
timeframe = "1h"

Typical use: Risk management exit. When kurtosis spikes above 4, the market is in a dangerous state. Existing positions may need to be closed or reduced. Can also be used as a negative filter -- "do NOT enter when kurtosis > 4."

cross_below -- Event-Based

What it does: Fires once, at the exact candle where kurtosis transitions from above the threshold to below it. The previous candle had kurtosis >= the target, and the current candle has kurtosis < the target.

[[actions.triggers]]
indicator = "kurt_60"
operator = "cross_below"
target = "2.0"
timeframe = "1h"

Typical use: Detect the moment the market transitions from a fat-tail regime to a normal regime. The volatility storm has passed. This can signal that it is safe to resume systematic trading.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where kurtosis transitions from below the threshold to above it. The previous candle had kurtosis <= the target, and the current candle has kurtosis > the target.

[[actions.triggers]]
indicator = "kurt_60"
operator = "cross_above"
target = "4.0"
timeframe = "1h"

Typical use: Detect the moment the market enters a fat-tail regime. Kurtosis has just spiked -- extreme moves have started. This is an early warning signal to tighten stops, reduce exposure, or exit positions entirely.

Why: Kurtosis produces floating-point values that are rarely exactly equal to any specific number. Use < or > instead.

Choosing the right operator

  • Use < to identify safe trading environments -- low kurtosis means predictable markets. The most common kurtosis operator.
  • Use > to detect dangerous environments -- high kurtosis means extreme moves are likely. Use for exits or as a negative filter.
  • Use cross_below / cross_above to detect the exact moment a risk regime changes.

TOML Examples

Low Kurtosis Safe Entry

Enter long positions only when the market is in a predictable, low-kurtosis environment. Combined with a bullish trend filter, this ensures you are trading in calm conditions where systematic strategies perform best.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "kurt_60"
  operator = "<"
  target = "2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "ema_20"
  operator = ">"
  target = "ema_50"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

High Kurtosis Exit

Exit existing positions when kurtosis spikes above 4. The market is producing extreme moves and the risk of a catastrophic loss increases dramatically. This is a defensive risk management action.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "kurt_60"
  operator = "cross_above"
  target = "4.0"
  timeframe = "1h"

Full Distribution: Kurtosis + Skewness

The complete distribution picture. Buy when: (1) skewness is negative (crash has occurred), (2) kurtosis is low (extreme moves have stopped -- the storm has passed), and (3) RSI confirms recovery. This is the highest-conviction contrarian setup.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "skew_60"
  operator = "<"
  target = "-0.8"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "kurt_60"
  operator = "<"
  target = "2.0"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "cross_above"
  target = "30"
  timeframe = "1h"

Kurtosis Volatility Regime Filter

Use kurtosis as a volatility regime gate. Only enter DCA positions when the market is calm (low kurtosis) and price is at a discount to VWAP. This avoids accumulating during chaotic markets where prices can gap unpredictably.

[[actions]]
type = "open_long"
amount = "50 USDC"

  [[actions.triggers]]
  indicator = "kurt_100"
  operator = "<"
  target = "2.5"
  timeframe = "4h"

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "vwap"
  timeframe = "4h"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "45"
  timeframe = "4h"
  max_count = 5

Tips

Kurtosis is a risk management tool, not an entry signal

Kurtosis does not tell you which direction to trade -- it tells you whether the market is safe to trade at all. Low kurtosis means the market is well-behaved and your stop-losses, position sizes, and strategy assumptions are likely to hold. High kurtosis means the market is producing moves that violate normal assumptions. Use kurtosis to decide whether to trade, then use directional indicators (RSI, EMA, MACD) to decide how to trade.

Low kurtosis = your strategies work as designed

Systematic trading strategies are typically developed and backtested under normal market conditions. When kurtosis is low, the market matches those conditions -- your strategies are operating within their design parameters. When kurtosis is high, the market has shifted to a regime your strategy was not designed for. Using kurtosis as a gate filter ensures your strategy only runs in the environment it was built for.

Combine with Skewness for the full picture

Kurtosis tells you how fat the tails are. Skewness tells you which direction the tail risk is. Together: high kurtosis + negative skew = active crash in progress (extreme downside moves). High kurtosis + positive skew = explosive rally with blow-off-top risk. Low kurtosis + negative skew = post-crash calm, high-probability bounce setup. The combination of both gives you the complete distribution shape.

Beware of high-kurtosis environments

When kurtosis exceeds 4, the market is in "fat tail" territory. This means: stop-losses may not execute at your target price (gaps and slippage), position sizing models underestimate risk, and "impossible" events become possible. During these regimes, reduce position sizes, widen stop-losses, or step aside entirely. The goal is capital preservation. You can always re-enter when kurtosis drops back to normal levels.

Autocorrelation -- Return Autocorrelation

Contents


Overview

Autocorrelation measures the correlation of an asset's returns with their own lagged values over a rolling 100-candle window. At lag 1, it answers the question: "does this candle's return predict the next candle's return?" At lag 5, it asks: "does this candle's return predict the return 5 candles from now?"

The calculation is a standard Pearson correlation between two series: the return series returns[t] and its lagged copy returns[t - lag], computed over the most recent 100 candles. The result is a single number between -1 and +1, updated every candle as the rolling window advances.

This is the momentum validator -- the indicator that tells you whether momentum strategies have any edge in the current market. Before running any trend-following or momentum strategy, autocorrelation answers the fundamental question: is there exploitable serial dependence in returns, or is the market a random walk? Positive autocorrelation means today's direction predicts tomorrow's -- momentum strategies will work. Negative autocorrelation means today's direction predicts tomorrow will reverse -- mean reversion strategies will work. Near-zero autocorrelation means neither has an edge -- the market is noise.


Format

autocorr_{lag}

The parameter is lag, not period. Lag defines how many steps back in the return series the correlation is computed against. The rolling window for the correlation calculation is always a fixed 100 candles internally.

ExampleLagUse Case
autocorr_11One-step momentum -- the primary signal. Does this candle predict the next?
autocorr_22Two-step -- captures slightly longer persistence patterns
autocorr_55Medium-horizon -- momentum at the 5-candle scale
autocorr_1010Longer-horizon -- structural momentum that persists across many candles

Lag range: 1 to 50. Recommended: 1, 2, 5, 10.

Value range: -1 to +1 (Pearson correlation coefficient).


Understanding Autocorrelation Values

Autocorrelation RangeInterpretation
Below -0.2Strong mean reversion -- today's direction strongly predicts tomorrow will reverse. Mean-reversion strategies have a significant edge. Statistically significant.
-0.2 to -0.1Weak mean reversion -- some tendency to reverse, but the signal is marginal. Momentum strategies should be avoided.
-0.1 to 0.1Random walk zone -- no exploitable serial dependence. Neither momentum nor mean-reversion strategies have a reliable edge. This is noise, not signal.
0.1 to 0.2Weak momentum -- some tendency to persist, but the signal is marginal. Trend-following can work but with reduced confidence.
Above 0.2Strong momentum -- today's direction strongly predicts tomorrow will continue. Trend-following and momentum strategies have a significant edge. Statistically significant.

Note

Statistical significance for a 100-candle window is |autocorrelation| > 2/sqrt(100) = 0.2. Values between -0.1 and 0.1 are almost certainly noise. Values between 0.1 and 0.2 (or -0.1 and -0.2) are suggestive but not statistically significant -- use them with caution and combine with other confirming indicators.


Understanding Operators with Autocorrelation

Each operator behaves differently with autocorrelation. Because autocorrelation is a regime filter (it describes the market's current statistical structure, not trading direction), it is almost always used in combination with directional triggers.

> (Greater Than) -- State-Based

What it does: The trigger is true on every candle where autocorrelation is above the threshold.

On the chart: The market is exhibiting momentum behavior. Returns are positively correlated with their lagged values -- moves tend to continue. This trigger stays active for as long as the momentum regime persists.

[[actions.triggers]]
indicator = "autocorr_1"
operator = ">"
target = "0.1"
timeframe = "1d"

Typical use: "Only run momentum strategies when autocorrelation confirms momentum is real." This is the primary use case -- gate your trend-following entries behind an autocorrelation check so you do not trade momentum in a random or mean-reverting market.

< (Less Than) -- State-Based

What it does: The trigger is true on every candle where autocorrelation is below the threshold.

On the chart: The market is exhibiting mean-reverting behavior. Returns are negatively correlated with their lagged values -- moves tend to reverse. This trigger stays active for as long as the mean-reversion regime persists.

[[actions.triggers]]
indicator = "autocorr_1"
operator = "<"
target = "-0.1"
timeframe = "1d"

Typical use: "Only run mean-reversion strategies when autocorrelation confirms reversion is real." Use this to gate fade-the-move entries. When autocorrelation is negative, buying dips and selling rips has a statistical edge.

cross_above -- Event-Based

What it does: Fires once, at the exact candle where autocorrelation transitions from below the threshold to above it. The previous candle had autocorrelation <= the target, and the current candle has autocorrelation > the target.

[[actions.triggers]]
indicator = "autocorr_1"
operator = "cross_above"
target = "0.1"
timeframe = "1d"

Typical use: Detect the moment the market shifts into a momentum regime. The market was random or mean-reverting and has just started exhibiting return persistence. This signals the beginning of a trend-friendly environment -- a good time to activate momentum strategies.

cross_below -- Event-Based

What it does: Fires once, at the exact candle where autocorrelation transitions from above the threshold to below it. The previous candle had autocorrelation >= the target, and the current candle has autocorrelation < the target.

[[actions.triggers]]
indicator = "autocorr_1"
operator = "cross_below"
target = "0"
timeframe = "1d"

Typical use: Detect the moment the market exits a momentum regime. Autocorrelation was positive (trending) and has dropped to zero or negative -- momentum is dying. This is an exit signal for trend-following positions, warning that the directional persistence that justified your trade is gone.

Why: Autocorrelation produces floating-point values calculated to many decimal places. Exact equality will almost never be true. Use > or < instead.

Choosing the right operator

  • Use > to validate momentum strategies -- "only trade trends when autocorrelation confirms persistence." This is the most common operator.
  • Use < to validate mean-reversion strategies -- "only fade moves when autocorrelation confirms reversion."
  • Use cross_above / cross_below to detect regime transitions -- the exact moment the market shifts between momentum and mean-reversion.

TOML Examples

Momentum Validated Entry

Only enter a momentum trade when autocorrelation confirms that returns are persisting AND ROC shows positive momentum. This prevents trading momentum in random-walk markets where trend strategies bleed to death from noise.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "autocorr_1"
  operator = ">"
  target = "0.1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "3"
  timeframe = "1d"

Mean Reversion Validated Dip Buy

Fade a sharp dip only when autocorrelation confirms the market is in a mean-reverting regime. When autocorrelation is negative, oversold conditions are more likely to reverse rather than continue lower.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "autocorr_1"
  operator = "<"
  target = "-0.1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "4h"

Exit on Regime Change

Exit a trending position when autocorrelation drops below -0.05, signaling that momentum is ending and the market may be shifting to mean-reversion. This exits before a full reversal rather than waiting for price-based stop losses.

[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "autocorr_1"
  operator = "cross_below"
  target = "-0.05"
  timeframe = "1d"

Multi-Lag Momentum Confirmation

Require positive autocorrelation at both lag 1 and lag 5. When momentum is present at multiple horizons, the trend is structurally stronger and more likely to persist. A single-lag signal can be noise; multi-lag agreement is conviction.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "autocorr_1"
  operator = ">"
  target = "0.1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "autocorr_5"
  operator = ">"
  target = "0.1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = ">"
  target = "2"
  timeframe = "1d"

Combined with Hurst for Double Confirmation

Use Hurst exponent for long-term regime classification and autocorrelation for short-term persistence confirmation. Hurst > 0.55 says the market is structurally trending. Autocorrelation > 0.1 says returns are currently persisting. Both together give the highest-confidence momentum signal.

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "hurst"
  operator = ">"
  target = "0.55"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "autocorr_1"
  operator = ">"
  target = "0.1"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "ema_20"
  operator = ">"
  target = "ema_50"
  timeframe = "1d"

Tips

THE filter for momentum strategies

Before running any momentum, trend-following, or breakout strategy, check autocorrelation first. If autocorr_1 is near zero or negative, your momentum strategy has no statistical edge -- it is trading noise. This single filter can prevent months of slow losses from running trend strategies in a trendless market. Gate every momentum entry behind autocorr_1 > 0.1 as a minimum.

Use lag 1 for most cases, check higher lags for confirmation

Lag 1 is the most important -- it captures the immediate one-step persistence that most short-term strategies exploit. Lag 5 and lag 10 reveal whether momentum extends to longer horizons. When autocorr_1, autocorr_5, and autocorr_10 are all positive, you have momentum at every scale -- a structurally strong trend. When only lag 1 is positive but lag 5 is near zero, momentum is short-lived and your holding period should be short.

Combine with Hurst for the full regime picture

Hurst exponent and autocorrelation are complementary. Hurst measures the long-term memory structure of the price series (is the market fundamentally trending or mean-reverting?). Autocorrelation measures the short-term persistence of returns (are current moves continuing?). Hurst is slow-moving and stable; autocorrelation is responsive and fast. Use Hurst for strategy selection and autocorrelation for entry timing.

Crypto momentum is short-lived

Academic research consistently shows that momentum in cryptocurrency markets is SHORT-LIVED compared to equities. For altcoins, momentum effects typically last 1--4 weeks before reversing. BTC has somewhat longer momentum persistence. This means: (1) use daily or 4-hour timeframes for reliable autocorrelation signals, (2) do not assume momentum will last indefinitely, and (3) always have an exit trigger based on autocorrelation declining -- when autocorr_1 drops toward zero, the momentum window is closing.

Values between -0.1 and 0.1 are noise

The statistical significance threshold for a 100-candle window is approximately |autocorrelation| > 0.2. Values between -0.1 and 0.1 are almost certainly random noise -- do not build strategies around them. Values between 0.1 and 0.2 are suggestive but not conclusive. Only values beyond 0.2 give you high confidence that the serial dependence is real and exploitable.

Operators Reference

Every technical trigger in Botmarley compares an indicator to a target using an operator. There are five operators, and choosing the right one is critical to how your strategy behaves.

[[actions.triggers]]
indicator = "rsi_14"       # left side
operator = "<"             # comparison
target = "30"              # right side

Botmarley always reads left-to-right: indicator operator target. The trigger is true when the statement indicator operator target evaluates to true on the current candle.


The Five Operators

> -- Greater Than

Type: State-based

The trigger is true on every candle where the indicator value is greater than the target. It remains true for as long as the condition holds.

# True on every candle where RSI is above 70
[[actions.triggers]]
indicator = "rsi_14"
operator = ">"
target = "70"

If RSI stays above 70 for ten consecutive candles, this trigger is active on all ten. The action associated with this trigger can fire multiple times unless you limit it with max_count.

When to use: Continuous conditions, zone detection, and filters. "Only enter when the macro trend is bullish" (sma_50 > sma_200), "sell when RSI is overbought" (rsi_14 > 70).


< -- Less Than

Type: State-based

The trigger is true on every candle where the indicator value is less than the target. Like >, it fires continuously as long as the condition holds.

# True on every candle where RSI is below 30
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

If price stays below the lower Bollinger Band for five candles, the trigger fires on all five. Use max_count to limit it to a single firing per position.

When to use: Oversold detection (rsi_14 < 30), price below a band (price < bb_lower), negative momentum (roc_10 < -3).


= -- Equal

Type: State-based

The trigger is true when the indicator value exactly equals the target. This operator is only useful for indicators that output discrete integer values.

# True when TTM Trend is bullish
[[actions.triggers]]
indicator = "ttm_trend"
operator = "="
target = "1"

Do not use = with floating-point indicators

Indicators like RSI, SMA, EMA, MACD, and Bollinger Bands produce continuous floating-point values (e.g., 29.87, 30.12). A condition like rsi_14 = 30 will almost never be exactly true. Use < or > for these indicators instead.

When to use: TTM Trend (ttm_trend = 1 for bullish, ttm_trend = -1 for bearish). This is effectively the only practical use case for =.


cross_above -- Cross Above

Type: Event-based

The trigger fires once at the exact moment the indicator crosses from below to above the target. It requires two consecutive candles:

  • Previous candle: indicator value <= target value
  • Current candle: indicator value > target value
# Fires once when SMA(50) crosses above SMA(200) -- the "Golden Cross"
[[actions.triggers]]
indicator = "sma_50"
operator = "cross_above"
target = "sma_200"
timeframe = "1d"

After the crossing, the trigger does not fire again until the indicator drops back below the target and crosses above it again. This makes cross_above ideal for catching turning points.

The most powerful tool for trend detection

Use cross_above instead of > when you want to catch the turning point rather than the sustained condition. A golden cross (sma_50 cross_above sma_200) fires once when the trend shifts bullish. Using sma_50 > sma_200 would fire on every candle where SMA(50) happens to be above SMA(200) -- potentially hundreds of candles.

When to use: Moving average crossovers (sma_50 cross_above sma_200), MACD signal crossovers (macd_line cross_above macd_signal), price crossing above a band (price cross_above bb_middle), zero-line crossings (macd_line cross_above 0).


cross_below -- Cross Below

Type: Event-based

The trigger fires once at the exact moment the indicator crosses from above to below the target. It requires two consecutive candles:

  • Previous candle: indicator value >= target value
  • Current candle: indicator value < target value
# Fires once when SMA(50) crosses below SMA(200) -- the "Death Cross"
[[actions.triggers]]
indicator = "sma_50"
operator = "cross_below"
target = "sma_200"
timeframe = "1d"

After the crossing, the trigger does not fire again until the indicator rises back above the target and crosses below it again.

When to use: Trend reversal exits (sma_50 cross_below sma_200), bearish MACD crossovers (macd_line cross_below macd_signal), price breaking below support (price cross_below bb_lower).


State-Based vs Event-Based

The most important distinction in Botmarley's operator system is between state-based and event-based operators.

TypeOperatorsFiresBest for
State-based>, <, =Every candle where the condition is trueFilters, zones, continuous conditions
Event-basedcross_above, cross_belowOnce at the moment of crossingEntry signals, exit signals, trend shifts

Why it matters

Consider RSI dropping below 30. With <, the trigger fires on every candle where RSI is below 30. If RSI stays oversold for 20 candles, the associated action can fire up to 20 times. With cross_below, the trigger fires once -- on the candle where RSI first drops below 30.

# STATE-BASED: fires on every candle where RSI < 30
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

# EVENT-BASED: fires once when RSI crosses below 30
[[actions.triggers]]
indicator = "rsi_14"
operator = "cross_below"
target = "30"

Neither is inherently better -- the right choice depends on your strategy:

  • Use state-based when the condition is a prerequisite (e.g., "only enter during uptrends" with sma_50 > sma_200).
  • Use event-based when you want a precise entry or exit signal (e.g., "enter when MACD crosses above signal").

Targets

The target field in a trigger accepts two types of values:

Numeric Targets

A fixed number, written as a string. Used for absolute thresholds.

target = "30"       # RSI threshold
target = "0"        # MACD zero-line
target = "-3"       # Negative value (ROC below -3%)
target = "50000"    # A specific price level
target = "500"      # ATR threshold

Indicator Targets

Another indicator name. Both the indicator and target are evaluated on the same candle at the same timeframe. This enables indicator-vs-indicator comparisons.

target = "sma_200"       # Compare against 200-period SMA
target = "ema_50"        # Compare against 50-period EMA
target = "bb_upper"      # Compare against upper Bollinger Band
target = "bb_lower"      # Compare against lower Bollinger Band
target = "bb_middle"     # Compare against middle Bollinger Band
target = "price"         # Compare indicator against current price
target = "obv_sma_20"    # Compare OBV against its SMA
target = "macd_signal"   # Compare MACD line against signal line

Note

When using an indicator as a target, the order matters. indicator = "sma_50" with target = "sma_200" and operator = ">" means "is SMA(50) greater than SMA(200)?" -- not the other way around. Botmarley always reads left-to-right.


Controlling State-Based Firing with max_count

State-based operators (>, <, =) fire on every candle where the condition holds. This can cause an action to execute many more times than intended. The max_count field limits how many times a trigger can fire per position.

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
max_count = 1          # Only fire once per position

Without max_count, a rsi_14 < 30 trigger would fire on every candle where RSI is below 30 -- potentially buying repeatedly. With max_count = 1, it fires once and then stays inactive for the remainder of the position.

Tip

max_count is especially important for DCA (Dollar Cost Averaging) actions. Each DCA level should typically have max_count = 1 so it only adds to the position once at each level, not repeatedly.

max_count has no effect on event-based operators (cross_above, cross_below) because crossovers inherently fire only once per crossing event.


Quick Reference

OperatorTypeFiresExampleReads as
>StateEvery matching candlersi_14 > 70"RSI is above 70"
<StateEvery matching candleprice < bb_lower"Price is below the lower band"
=StateEvery matching candlettm_trend = 1"TTM Trend is bullish"
cross_aboveEventOnce at crossingsma_50 cross_above sma_200"SMA 50 just crossed above SMA 200"
cross_belowEventOnce at crossingmacd_line cross_below macd_signal"MACD just crossed below signal"

Price Change Trigger

The price_change trigger fires when the market price has moved by a specified percentage over a lookback window. It compares the current candle's close to a candle from N minutes ago. This trigger is global — it does not require an open position.

Use it for momentum entries ("buy after a 5% dip in the last 4 hours") or breakout detection ("price is up 3% over the last day").

Note

For price changes relative to your position's entry price, see Position Price Change instead. price_change watches the market; pos_price_change watches your P&L.

TOML Syntax

[[actions.triggers]]
type = "price_change"
value = "-5%"           # Required: ±N% threshold
timeframe = "1h"        # Optional: candle size (default: 1m)
lookback = 24           # Optional: number of candles to look back (default: 1)
max_value = "-15%"      # Optional: maximum allowed change (range cap)
max_count = 1           # Optional: max fires per position

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "price_change"
valuestringYesPercentage threshold in "±N%" format
timeframestringNo"1m"Candle size for the lookback window
lookbackintegerNo1Number of timeframe-sized candles to look back
max_valuestringNoMaximum change in "±N%" format (caps the range)
max_countintegerNounlimitedMaximum times this trigger can fire per position

How It Works

The engine computes a total lookback in 1-minute candles:

total_lookback = lookback × timeframe_in_minutes
timeframelookbackTotal 1m candlesReal window
"1h"1 (default)601 hour
"1h"42404 hours
"1h"24144024 hours
"4h"6144024 hours
"1d"1 (default)14401 day
"5m"12601 hour

When lookback is omitted, it defaults to 1 — meaning the window equals exactly one timeframe period. This is backwards-compatible with the original behavior.

The engine then calculates:

change_pct = (current_close - close_N_candles_ago) / close_N_candles_ago × 100
  • Negative value (e.g., "-5%"): trigger fires when change_pct <= -5.0
  • Positive value (e.g., "+3%"): trigger fires when change_pct >= 3.0
graph TD
    A["Current candle close"] --> C{"Calculate % change<br/>over lookback window"}
    B["Close from N candles ago<br/>(N = lookback × timeframe)"] --> C
    C --> D{"change meets<br/>threshold?"}
    D -->|Yes| E{"Within max_value<br/>range?"}
    D -->|No| F["No action"]
    E -->|Yes or no max_value| G["Trigger fires"]
    E -->|No — too extreme| F

The value Format

The value field uses a "±N%" string format:

ValueMeaning
"-5%"Price dropped at least 5%
"-2.0%"Price dropped at least 2%
"+3%"Price rose at least 3%
"+1.5%"Price rose at least 1.5%
"3%"Same as "+3%" (positive assumed)

Tip

The + sign is optional for positive values. "3%" and "+3%" are equivalent.

The lookback Parameter

The lookback parameter controls how many timeframe-sized candles to look back. Without it, the engine looks back exactly one timeframe period.

# Without lookback: compares to 1 hour ago
type = "price_change"
value = "-5%"
timeframe = "1h"

# With lookback: compares to 24 hours ago (24 × 1h)
type = "price_change"
value = "-5%"
timeframe = "1h"
lookback = 24

This is useful when you want a long lookback window but the timeframe options don't cover it directly. For example, "did the price drop 10% over the last 3 days?" can be expressed as:

type = "price_change"
value = "-10%"
timeframe = "1d"
lookback = 3

The max_value Parameter (Range Capping)

max_value caps how extreme the price change can be for the trigger to fire. It creates a range instead of a simple threshold — useful for avoiding "falling knife" entries.

# Fire when price drops between 5% and 15%
# (skip if it dropped MORE than 15% — that's a falling knife)
type = "price_change"
value = "-5%"
max_value = "-15%"
timeframe = "4h"

How it works:

  • For negative thresholds: value is the minimum drop, max_value is the maximum drop
  • For positive thresholds: value is the minimum rise, max_value is the maximum rise
valuemax_valueFires when change is...
"-5%""-15%"Between -5% and -15%
"-5%"(none)-5% or worse (no cap)
"+3%""+10%"Between +3% and +10%
"+3%"(none)+3% or better (no cap)

Tip

max_value works with all price-change family triggers: price_change, price_change_from_high, and price_change_from_low.

Valid Timeframes

The timeframe field accepts: 1m, 5m, 15m, 1h, 4h, 1d.

When omitted, the trigger uses 1m — meaning it compares the current close to the close 1 minute ago. This is rarely useful on its own. In practice, you almost always want a larger timeframe.

Examples

Buy After a 5% Dip Over 4 Hours

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change"
value = "-5%"
timeframe = "4h"

Buy After a 10% Dip Over 24 Hours (With Lookback)

[[actions]]
type = "open_long"
amount = "200 USDC"

[[actions.triggers]]
type = "price_change"
value = "-10%"
timeframe = "1h"
lookback = 24

Sell After a 3% Rise Over 1 Hour

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "price_change"
value = "+3%"
timeframe = "1h"

DCA Entry on Hourly Dip (Fire Once)

[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

[[actions.triggers]]
type = "price_change"
value = "-3%"
timeframe = "1h"
max_count = 1

Buy the Dip — But Not the Crash (Range Cap)

# Enter on 5-15% dips over 4 hours, skip if it's a free fall
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change"
value = "-5%"
max_value = "-15%"
timeframe = "4h"

Combine with Technical Indicator

# Buy when price dropped 3% in the last hour AND RSI is oversold
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change"
value = "-3%"
timeframe = "1h"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

Tips

Tip

price_change is a global market signal. It works whether or not you have an open position. This makes it ideal for entry triggers that watch for market-wide dips or rallies.

Tip

Use lookback when standard timeframes don't cover the window you need. timeframe = "1h" with lookback = 24 gives you a 24-hour window calculated from hourly candle boundaries.

Tip

max_value is your safety net. In volatile markets, a 5% dip might be normal — but a 30% crash could signal something fundamentally wrong. Use max_value to filter out extreme moves.

Warning

Without a timeframe, price_change compares to just 1 minute ago, which is extremely noisy. Always set a timeframe for meaningful signals.

TriggerWhat It Measures
Price Change from High% drop from the highest price in a lookback window
Price Change from Low% rise from the lowest price in a lookback window
Position Price Change% change from your entry price
Trailing Stop% drop from position's peak price
Consecutive CandlesStreaks of red or green candles

Price Change from High

The price_change_from_high trigger fires when the current price has dropped by a specified percentage from the highest price within a lookback window. It is a global trigger — no open position is required.

Use it to detect dip-from-peak opportunities: "price is down 5% from its high over the last 24 hours."

Note

This trigger measures the drop from a recent high in the market, not from your position's peak. For position-relative peak tracking, see Trailing Stop instead.

TOML Syntax

[[actions.triggers]]
type = "price_change_from_high"
value = "-5%"           # Required: drop threshold (always negative)
timeframe = "1h"        # Optional: candle size (default: 1m)
lookback = 24           # Optional: number of candles to scan (default: 24)
max_value = "-15%"      # Optional: maximum allowed drop (range cap)
max_count = 1           # Optional: max fires per position

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "price_change_from_high"
valuestringYesDrop threshold in "-N%" format (always negative)
timeframestringNo"1m"Candle size for the lookback window
lookbackintegerNo24Number of timeframe-sized candles to scan
max_valuestringNoMaximum drop in "-N%" format (range cap)
max_countintegerNounlimitedMaximum times this trigger can fire per position

How It Works

  1. Compute the lookback window: total_candles = lookback × timeframe_in_minutes
  2. Find the highest close in that window
  3. Calculate the drop: change_pct = (current_close - highest_close) / highest_close × 100
  4. Fire if change_pct <= threshold (and within max_value range if set)
graph TD
    A["Scan last N candles<br/>(lookback × timeframe)"] --> B["Find highest close"]
    B --> C["Calculate % drop<br/>from high to current"]
    C --> D{"Drop meets<br/>threshold?"}
    D -->|Yes| E{"Within max_value<br/>range?"}
    D -->|No| F["No action"]
    E -->|Yes or no max_value| G["Trigger fires"]
    E -->|No — too extreme| F

Example Calculation

With timeframe = "1h", lookback = 24, value = "-5%":

  • Window: last 24 × 60 = 1440 one-minute candles (24 hours)
  • Highest close in window: $50,000
  • Current close: $47,000
  • Change: ($47,000 - $50,000) / $50,000 × 100 = -6.0%
  • -6.0% ≤ -5.0% → trigger fires

The value Format

The value must be negative (you're measuring a drop from a high):

ValueMeaning
"-5%"Price dropped at least 5% from the window's high
"-2.5%"Price dropped at least 2.5% from the window's high
"-10%"Price dropped at least 10% from the window's high

Examples

Buy When Price Drops 5% from 24h High

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change_from_high"
value = "-5%"
timeframe = "1h"
lookback = 24

Buy the Dip — But Not the Crash

# Enter when price drops 5-15% from the 4h high window
# Skip if the drop exceeds 15% (falling knife protection)
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change_from_high"
value = "-5%"
max_value = "-15%"
timeframe = "1h"
lookback = 4

Combine with RSI for Confirmation

[[actions]]
type = "open_long"
amount = "150 USDC"

# Price dropped 3% from 12h high
[[actions.triggers]]
type = "price_change_from_high"
value = "-3%"
timeframe = "1h"
lookback = 12

# AND RSI confirms oversold
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "1h"

Short-Window Scalping (5m candles, 12-candle window)

[[actions]]
type = "open_long"
amount = "50 USDC"

[[actions.triggers]]
type = "price_change_from_high"
value = "-2%"
timeframe = "5m"
lookback = 12
max_count = 1

Tips

Tip

The default lookback of 24 candles works well with timeframe = "1h" — giving you a 24-hour window. Adjust both parameters to fit your trading timeframe.

Tip

Use max_value for falling knife protection. If the price dropped 30% from the high, it might be a fundamental breakdown rather than a buying opportunity.

Note

This trigger fires based on candle closes, not intra-candle wicks. The "highest close" is the highest closing price in the window, not the highest wick.

TriggerWhat It Measures
Price Change from Low% rise from the lowest price in a lookback window
Price Change% change over a fixed lookback period
Trailing Stop% drop from position's peak (requires open position)
Position Price Change% change from your entry price

Price Change from Low

The price_change_from_low trigger fires when the current price has risen by a specified percentage from the lowest price within a lookback window. It is a global trigger — no open position is required.

Use it to detect recovery-from-bottom signals: "price has bounced 3% off its low over the last 12 hours."

Note

This trigger measures the rise from a recent low in the market, not from your position's entry price. For position-relative tracking, see Position Price Change instead.

TOML Syntax

[[actions.triggers]]
type = "price_change_from_low"
value = "3%"            # Required: rise threshold (always positive)
timeframe = "1h"        # Optional: candle size (default: 1m)
lookback = 12           # Optional: number of candles to scan (default: 24)
max_value = "15%"       # Optional: maximum allowed rise (range cap)
max_count = 1           # Optional: max fires per position

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "price_change_from_low"
valuestringYesRise threshold in "N%" format (always positive)
timeframestringNo"1m"Candle size for the lookback window
lookbackintegerNo24Number of timeframe-sized candles to scan
max_valuestringNoMaximum rise in "N%" format (range cap)
max_countintegerNounlimitedMaximum times this trigger can fire per position

How It Works

  1. Compute the lookback window: total_candles = lookback × timeframe_in_minutes
  2. Find the lowest close in that window
  3. Calculate the rise: change_pct = (current_close - lowest_close) / lowest_close × 100
  4. Fire if change_pct >= threshold (and within max_value range if set)
graph TD
    A["Scan last N candles<br/>(lookback × timeframe)"] --> B["Find lowest close"]
    B --> C["Calculate % rise<br/>from low to current"]
    C --> D{"Rise meets<br/>threshold?"}
    D -->|Yes| E{"Within max_value<br/>range?"}
    D -->|No| F["No action"]
    E -->|Yes or no max_value| G["Trigger fires"]
    E -->|No — too extreme| F

Example Calculation

With timeframe = "1h", lookback = 12, value = "3%":

  • Window: last 12 × 60 = 720 one-minute candles (12 hours)
  • Lowest close in window: $45,000
  • Current close: $46,800
  • Change: ($46,800 - $45,000) / $45,000 × 100 = +4.0%
  • +4.0% ≥ +3.0% → trigger fires

The value Format

The value must be positive (you're measuring a rise from a low):

ValueMeaning
"3%"Price rose at least 3% from the window's low
"5%"Price rose at least 5% from the window's low
"1.5%"Price rose at least 1.5% from the window's low

Examples

Buy After 3% Bounce from 12h Low

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change_from_low"
value = "3%"
timeframe = "1h"
lookback = 12

Confirm Reversal — Not a Dead Cat Bounce

# Enter on a 5% bounce, but skip if it bounced more than 20%
# (might be too late / overbought)
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "price_change_from_low"
value = "5%"
max_value = "20%"
timeframe = "1h"
lookback = 24

Combine with Volume Confirmation

[[actions]]
type = "open_long"
amount = "150 USDC"

# Price bounced 3% from 6h low
[[actions.triggers]]
type = "price_change_from_low"
value = "3%"
timeframe = "1h"
lookback = 6

# AND volume is above average (confirmation)
[[actions.triggers]]
indicator = "volume_sma_20"
operator = ">"
target = "volume_sma_50"
timeframe = "1h"

Short-Window Recovery (5m candles)

[[actions]]
type = "open_long"
amount = "50 USDC"

[[actions.triggers]]
type = "price_change_from_low"
value = "1.5%"
timeframe = "5m"
lookback = 24
max_count = 1

Tips

Tip

price_change_from_low is a momentum confirmation trigger. A price bouncing off a low with strength suggests buying interest. Combine it with volume or RSI triggers for higher confidence entries.

Tip

Use max_value to avoid chasing. If the price already bounced 20% from the low, the easy gains may already be captured.

Note

Like all price triggers, this fires based on candle closes, not intra-candle wicks. The "lowest close" is the lowest closing price in the window.

TriggerWhat It Measures
Price Change from High% drop from the highest price in a lookback window
Price Change% change over a fixed lookback period
Position Price Change% change from your entry price
Trailing Take-ProfitLocks in profit as price moves up

Position Price Change Trigger

The pos_price_change trigger fires based on the price change relative to your open position's average entry price. It only activates when you have an open position. Use it for take-profit and stop-loss logic.

Note

For market-wide price movement (no position required), see Price Change instead. pos_price_change watches your P&L; price_change watches the market.

TOML Syntax

[[actions.triggers]]
type = "pos_price_change"
value = "-2%"           # Required: ±N% from entry price
max_count = 1           # Optional: max fires per position

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "pos_price_change"
valuestringYesPercentage threshold from entry price in "±N%" format
max_countintegerNounlimitedMaximum times this trigger can fire per position

Warning

pos_price_change does not support the timeframe field. It always compares the current price to the position's entry price, regardless of candle size.

How It Works

The engine calculates the percentage change from the position's average entry price:

change_pct = (current_close - entry_price) / entry_price * 100
  • Positive value (e.g., "2%"): fires when the position is profitable by at least 2%
  • Negative value (e.g., "-2%"): fires when the position is losing by at least 2%
graph TD
    A["Current candle close"] --> C{Calculate % from entry}
    B["Position entry price<br/>(average if DCA'd)"] --> C
    C --> D{"change meets<br/>threshold?"}
    D -->|Yes| E["Trigger fires"]
    D -->|No| F["No action"]

Average Price with DCA

When average_price = true is set on buy actions, the entry price is recalculated as a weighted average after each DCA buy. This means the trigger threshold shifts with each additional purchase.

For example, if you enter at $100 and DCA at $96, your average entry becomes ~$98. A pos_price_change of "2%" now fires at ~$99.96 instead of $102.

The value Format

ValueMeaning
"2%"Position is up at least 2% from entry
"+5%"Position is up at least 5% from entry
"-2%"Position is down at least 2% from entry
"-4%"Position is down at least 4% from entry

Examples

Simple Take-Profit at +2%

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "2%"

Simple Stop-Loss at -5%

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "-5%"

Layered DCA: Buy More on Dips

A classic DCA pattern — buy more as the position drops, each level firing once:

# DCA Level 1: buy more at -2%
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-2%"
max_count = 1

# DCA Level 2: buy more at -4%
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-4%"
max_count = 1

# DCA Level 3: buy more at -6%
[[actions]]
type = "buy"
amount = "300 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-6%"
max_count = 1

Tip

Always use max_count = 1 on DCA levels. Without it, the trigger fires repeatedly as price oscillates around the threshold, causing unintended double-buys.

Full Strategy: Entry + DCA + Take-Profit

[meta]
name = "Position Price DCA"
description = "RSI entry with layered DCA and profit target"
max_open_positions = 5

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1d"

[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-2%"
max_count = 1

[[actions]]
type = "buy"
amount = "500 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-4%"
max_count = 1

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "2%"

Tips

Tip

pos_price_change is evaluated per-position in multi-position strategies. If you have max_open_positions = 5, each position independently tracks its own entry price and fires its own DCA/TP/SL triggers.

Warning

Remember that DCA changes the average entry price. After buying more at a lower price, your take-profit target shifts closer to the current price. This is by design — it's the whole point of averaging down.

TriggerWhat It Measures
Price ChangeGlobal market price movement (no position needed)
Trailing Stop% drop from position's peak price
Trailing Take-ProfitDynamic TP that follows price up, exits on retrace
Time in PositionDuration-based exit

Trailing Stop Trigger

The trailing_stop trigger fires when the price drops a specified percentage from the highest price reached since the position was opened. It locks in profits during uptrends by selling when momentum reverses.

Unlike a fixed stop-loss (pos_price_change with a negative value), a trailing stop moves up as the price rises. It only looks down from the peak — never up.

TOML Syntax

[[actions.triggers]]
type = "trailing_stop"
value = "-3%"           # Required: always negative (drop from peak)

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "trailing_stop"
valuestringYesPercentage drop from peak in "-N%" format (always negative)
max_countintegerNounlimitedMaximum times this trigger can fire per position

Warning

trailing_stop does not support the timeframe field. It tracks the peak price across all candles since position entry.

How It Works

  1. When a position opens, the engine starts tracking the highest close price since entry
  2. On every candle, if the new close is higher than the tracked peak, the peak is updated
  3. The engine calculates: drop_pct = (current_close - peak_price) / peak_price * 100
  4. When drop_pct <= threshold (e.g., -3%), the trigger fires
graph TD
    A["Track peak price<br/>since position entry"] --> B["New candle close"]
    B --> C{"New close ><br/>peak?"}
    C -->|Yes| D["Update peak"]
    C -->|No| E{"Drop from peak<br/>meets threshold?"}
    D --> F["Continue"]
    E -->|Yes| G["Trigger fires"]
    E -->|No| F

Visual Example

Imagine you enter at $100, the price rises to $110 (new peak), then drops:

PricePeakDrop from Peak-3% trigger
$100$1000%
$105$1050%
$110$1100%
$108$110-1.8%
$107$110-2.7%
$106.70$110-3.0%Fires

The trailing stop at -3% would NOT have fired at $106.70 with a fixed stop-loss from entry ($100). The trailing mechanism locks in the gains from the $100 → $110 run.

The value Format

The value must be negative (it represents a drop):

ValueMeaning
"-2%"Sell when price drops 2% from peak
"-3%"Sell when price drops 3% from peak
"-5%"Sell when price drops 5% from peak
"-0.5%"Very tight trailing stop (0.5% from peak)

Examples

Basic Trailing Stop

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "trailing_stop"
value = "-4%"

Trailing Stop + DCA Strategy

Combine a trailing stop with DCA levels for a complete loss-prevention system:

[meta]
name = "Trend-Safe Trailing Stop"
description = "BB+RSI entry, DCA on dips, trailing stop locks profits"
max_open_positions = 1

# Entry: Bollinger Band + RSI filter
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "bb_lower"
operator = ">"
target = "price"
timeframe = "1h"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "40"
timeframe = "1d"

# DCA at -5%
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

[[actions.triggers]]
type = "pos_price_change"
value = "-5%"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "1h"

# Take profit at +3%
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "3%"

# Trailing stop: sell when price drops 4% from peak
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "trailing_stop"
value = "-4%"

# Time exit: free stuck capital after 7 days
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "time_in_position"
value = "7d"

Tips

Tip

Choose your trailing stop percentage based on the asset's typical volatility. A -2% trailing stop on BTC during a volatile week will trigger constantly. A -5% trailing stop gives more room for normal price swings.

Tip

Trailing stops and fixed take-profits (pos_price_change) work well together. The take-profit catches quick gains, while the trailing stop captures extended runs. Whichever fires first wins.

Warning

Trailing stops can fire during flash wicks (sudden price drops that recover quickly). In highly volatile markets, consider a wider percentage to avoid premature exits.

TriggerWhat It Measures
Position Price ChangeFixed % from entry price (stop-loss/take-profit)
Trailing Take-ProfitMore flexible trailing with activation threshold + tolerance
Time in PositionDuration-based exit (pairs well with trailing stop)

Consecutive Candles Trigger

The consecutive_candles trigger fires when a specified number of candles in a row are all the same color (red/bearish or green/bullish). It detects short-term momentum patterns — a streak of red candles often precedes a bounce, while a streak of green candles confirms a breakout.

TOML Syntax

[[actions.triggers]]
type = "consecutive_candles"
value = "3_red"         # Required: {count}_{direction}
timeframe = "5m"        # Required: candle size for counting

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "consecutive_candles"
valuestringYesPattern: "{count}_{direction}"
timeframestringYesCandle size for consecutive counting
max_countintegerNounlimitedMaximum times this trigger can fire per position

The value Format

The value follows the pattern {count}_{direction}:

ValueMeaning
"3_red"3 consecutive bearish (close < open) candles
"3_green"3 consecutive bullish (close >= open) candles
"5_red"5 consecutive bearish candles
"5_green"5 consecutive bullish candles
  • Red/bearish: candle close < candle open
  • Green/bullish: candle close >= candle open

How It Works

The engine groups 1-minute candles into the specified timeframe, then checks if the last N grouped candles are all the same color.

For example, with timeframe = "5m" and value = "3_red":

  1. Group every 5 consecutive 1-minute candles into one 5-minute candle
  2. Check the last 3 of these 5-minute candles
  3. If all 3 have close < open (red), the trigger fires
graph TD
    A["Group 1m candles<br/>into timeframe candles"] --> B["Check last N<br/>grouped candles"]
    B --> C{"All same<br/>color?"}
    C -->|Yes| D["Trigger fires"]
    C -->|No| E["No action"]

Valid Timeframes

The timeframe field accepts: 1m, 5m, 15m, 1h, 4h, 1d.

The timeframe determines the candle size used for consecutive counting. "3_red" on "5m" means three consecutive 5-minute red candles (15 minutes of selling). "3_red" on "1h" means three consecutive hourly red candles (3 hours of selling).

Examples

Buy After 3 Consecutive Red 5m Candles

A classic dip-buying pattern — three red candles in a row often precede a bounce:

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "consecutive_candles"
value = "3_red"
timeframe = "5m"

Combine with ROC for Sharp Dip Detection

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "roc_10"
operator = "<"
target = "-3"
timeframe = "5m"

[[actions.triggers]]
type = "consecutive_candles"
value = "3_red"
timeframe = "5m"

Breakout Confirmation with Green Candles

[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
type = "consecutive_candles"
value = "5_green"
timeframe = "1h"

[[actions.triggers]]
indicator = "sma_50"
operator = ">"
target = "sma_200"
timeframe = "1d"

Tips

Tip

Three consecutive red candles on 5-minute or 15-minute timeframes is a common dip-buying signal. It indicates short-term selling pressure that may be about to exhaust.

Warning

On higher timeframes (1h, 4h), even 3 consecutive red candles represents a significant trend. Make sure your strategy can handle the drawdown if the streak continues.

Tip

Pair consecutive_candles with an RSI or StochRSI confirmation for higher-quality entries. Three red candles alone do not guarantee a bounce — oversold confirmation makes the signal stronger.

TriggerWhat It Measures
Price ChangeGlobal % price movement over time
Next CandleCandle-close scheduling and delay

Time in Position Trigger

The time_in_position trigger fires when a position has been open for a specified duration. Use it as a safety net to free stuck capital when other exit triggers have not fired.

TOML Syntax

[[actions.triggers]]
type = "time_in_position"
value = "7d"            # Required: duration string

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "time_in_position"
valuestringYesDuration: "24h", "7d", or minutes as a number
max_countintegerNounlimitedMaximum times this trigger can fire per position

Warning

time_in_position does not support the timeframe field. Duration is absolute, not candle-dependent.

Duration Format

ValueDuration
"1h"1 hour (60 minutes)
"24h"24 hours
"7d"7 days
"30d"30 days
"60"60 minutes (numeric = minutes)

The engine converts the duration to minutes internally and checks if the position has been open for at least that many 1-minute candles.

How It Works

  1. When a position opens, the engine records the entry tick (candle index)
  2. On every candle, it calculates: minutes_open = current_tick - entry_tick
  3. When minutes_open >= duration_in_minutes, the trigger fires
graph TD
    A["Position opens<br/>at tick N"] --> B["Each candle:<br/>elapsed = tick - N"]
    B --> C{"elapsed >=<br/>duration?"}
    C -->|Yes| D["Trigger fires"]
    C -->|No| E["Continue waiting"]
    E --> B

Examples

Close After 7 Days

A safety exit that frees capital stuck in a position that is not moving:

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "time_in_position"
value = "7d"

Close After 24 Hours

For shorter-term strategies or scalping:

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "time_in_position"
value = "24h"

Complete Strategy with Time Exit

Time exits pair well with other exit triggers. The first one to fire wins:

[meta]
name = "Time-Protected DCA"
max_open_positions = 1

# Entry
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

# Take profit at +3%
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change"
value = "3%"

# Trailing stop at -4% from peak
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "trailing_stop"
value = "-4%"

# Time exit: close after 7 days no matter what
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "time_in_position"
value = "7d"

Tips

Tip

A time_in_position exit is your safety net. It prevents capital from being locked in a position that never hits its take-profit or stop-loss. A 7-day time exit is a good default for swing strategies.

Tip

In backtesting, time_in_position helps ensure positions close before the end of your data range. Without it, positions open near the end of your backtest data may never close, skewing results.

Warning

time_in_position fires regardless of the position's profitability. If your position is up 10% after 7 days, the time exit still sells at 100%. If you want to only force-close losing positions, combine it with a pos_price_change trigger in the same action (AND logic).

TriggerWhat It Measures
Position Price ChangeFixed % from entry price
Trailing Stop% drop from peak price
Next CandleCandle-close scheduling

Trailing Take-Profit / DCA Trigger

The pos_price_change_follow trigger is a dynamic exit (or entry) that activates when price reaches a threshold from the position entry, then fires when price retraces by a tolerance amount. Use it for trailing take-profits that ride momentum before exiting, or trailing DCA that buys on a bounce after a dip.

TOML Syntax

[[actions.triggers]]
type = "pos_price_change_follow"
value = "3%"            # Required: activation threshold from entry
tolerance = "-0.5%"     # Required: retrace tolerance to trigger

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "pos_price_change_follow"
valuestringYesActivation threshold: "±N%" from entry price
tolerancestringYesRetrace tolerance: how far price must pull back after activation
max_countintegerNounlimitedMaximum times this trigger can fire per position

Warning

pos_price_change_follow does not support the timeframe field. It tracks price relative to the position entry, like pos_price_change.

How It Works

This trigger has two phases:

Phase 1: Activation

The trigger activates when the position's P&L reaches the value threshold:

  • Positive value (e.g., "3%"): activates when position is up 3% from entry
  • Negative value (e.g., "-10%"): activates when position is down 10% from entry

Phase 2: Retrace and Fire

Once activated, the trigger tracks the extreme price (highest for positive, lowest for negative) and fires when price retraces from that extreme by the tolerance amount:

  • Negative tolerance (e.g., "-0.5%"): fires when price drops 0.5% from the tracked peak
  • Positive tolerance (e.g., "1%"): fires when price rises 1% from the tracked low
graph TD
    A["Position P&L<br/>reaches threshold"] --> B["Phase 1: Activated"]
    B --> C["Track extreme price<br/>(highest or lowest)"]
    C --> D{"Price retraces<br/>by tolerance?"}
    D -->|Yes| E["Trigger fires"]
    D -->|No| F["Update extreme<br/>if new high/low"]
    F --> D

Trailing Take-Profit Example

With value = "3%" and tolerance = "-0.5%":

  1. Position opens at $100
  2. Price rises to $103 → activation (up 3% from entry)
  3. Price continues to $108 (tracked peak = $108)
  4. Price drops to $107.46 → that's -0.5% from $108 → trigger fires

The trailing take-profit rode the run from $103 to $108, then exited on a small retrace. A fixed take-profit at 3% would have sold at $103 and missed the extra 5%.

Trailing DCA Example

With value = "-10%" and tolerance = "1%":

  1. Position opens at $100
  2. Price drops to $90 → activation (down 10% from entry)
  3. Price continues dropping to $85 (tracked low = $85)
  4. Price rises to $85.85 → that's +1% from $85 → trigger fires

Instead of buying at exactly -10%, the trailing DCA waited for the price to show signs of recovery (bouncing 1% from the local bottom) before adding capital.

Examples

Trailing Take-Profit: Sell on Retrace from Profit

[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change_follow"
value = "3%"
tolerance = "-0.5%"

Trailing DCA: Buy on Bounce After Deep Dip

[[actions]]
type = "buy"
amount = "100%"
average_price = true

[[actions.triggers]]
type = "pos_price_change_follow"
value = "-10%"
tolerance = "1%"

Full Strategy: Multi-Signal Entry + Trailing Exit

[meta]
name = "StochRSI Trailing"
description = "StochRSI extreme entry + trailing take-profit exit"
max_open_positions = 1

# Entry: StochRSI deeply oversold
[[actions]]
type = "open_long"
amount = "100 USDC"

[[actions.triggers]]
indicator = "stoch_rsi_14"
operator = "<"
target = "20"
timeframe = "15m"

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "35"
timeframe = "15m"

# Exit: trailing take-profit
[[actions]]
type = "sell"
amount = "100%"

[[actions.triggers]]
type = "pos_price_change_follow"
value = "3%"
tolerance = "-0.5%"

# DCA: trailing buy after deep dip shows recovery
[[actions]]
type = "buy"
amount = "100%"
average_price = true

[[actions.triggers]]
type = "pos_price_change_follow"
value = "-10%"
tolerance = "1%"

Choosing the Right Parameters

Use CasevaluetoleranceBehavior
Tight trailing TP"2%""-0.2%"Quick exit after small gain
Wide trailing TP"5%""-1%"Rides larger runs, accepts bigger retrace
DCA on bounce"-10%""1%"Buys after deep dip shows 1% recovery
DCA on slight bounce"-5%""0.5%"Buys after moderate dip with slight recovery

Tips

Tip

pos_price_change_follow is more sophisticated than a plain trailing_stop. The trailing stop tracks from peak; pos_price_change_follow only activates after a threshold is reached and then has a separate tolerance for exit. This gives you more control over when trailing begins.

Tip

For trailing take-profits, use a small negative tolerance (e.g., "-0.5%"). For trailing DCA buys, use a small positive tolerance (e.g., "1%"). The tolerance sign depends on which direction you are watching for a retrace.

Warning

If your tolerance is too tight (e.g., "-0.1%"), normal price fluctuations will trigger the exit almost immediately after activation. Give enough room for the price to breathe.

TriggerWhat It Measures
Trailing StopSimple trailing from peak (no activation threshold)
Position Price ChangeFixed % from entry price
Time in PositionDuration-based exit

Next Candle Trigger

The next_candle trigger fires when a candle closes at the specified timeframe. It introduces a scheduling element — instead of reacting immediately, the action waits for a candle boundary.

Use it for:

  • Scheduled accumulation: buy on every 4-hour candle close
  • Signal confirmation: wait for the current candle to close before acting on other triggers
  • Time-paced execution: space out actions by candle intervals

TOML Syntax

[[actions.triggers]]
type = "next_candle"
timeframe = "4h"        # Optional: candle size (default: 1m)
max_count = 5           # Optional: max fires per position

Parameters

ParameterTypeRequiredDefaultDescription
typestringYesMust be "next_candle"
timeframestringNo"1m"Candle size to wait for
max_countintegerNounlimitedMaximum times this trigger can fire per position

How It Works

The engine evaluates on every 1-minute candle. When the current tick aligns with a candle boundary for the specified timeframe, the trigger fires.

For example, with timeframe = "4h":

  • The trigger fires on every candle that falls on a 4-hour boundary (every 240th 1-minute candle)
  • Between boundaries, the trigger evaluates to false
graph TD
    A["Every 1m candle"] --> B{"On timeframe<br/>boundary?"}
    B -->|Yes| C["Trigger fires"]
    B -->|No| D["No action"]

When no timeframe is specified, the trigger fires on every 1-minute candle — effectively on every evaluation tick.

Valid Timeframes

TimeframeFires Every
"1m"Every candle (every evaluation)
"5m"Every 5 minutes
"15m"Every 15 minutes
"1h"Every hour
"4h"Every 4 hours
"1d"Every day

Examples

Scheduled Accumulation: Buy Every 4 Hours

[[actions]]
type = "buy"
amount = "25 USDC"
average_price = true

[[actions.triggers]]
type = "next_candle"
timeframe = "4h"
max_count = 5

This buys $25 every 4 hours, up to 5 times per position. Total additional investment: $125.

Signal Confirmation: RSI + Wait for Candle Close

When combined with other triggers (AND logic), next_candle acts as a "wait for confirmation" gate:

[[actions]]
type = "open_long"
amount = "100 USDC"

# Signal: RSI oversold on 1h candles
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

# Confirmation: wait for 1h candle to close
[[actions.triggers]]
type = "next_candle"
timeframe = "1h"

Both triggers must be true simultaneously. The action only fires when the hourly candle closes AND RSI is below 30 at that moment.

EMA Crossover with Candle Confirmation

[[actions]]
type = "open_long"
amount = "100 USDC"

# EMA fast crosses above EMA slow
[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "ema_21"
timeframe = "1h"

# Wait for hourly candle close
[[actions.triggers]]
type = "next_candle"
timeframe = "1h"

Daily DCA (Buy Every Day)

[[actions]]
type = "buy"
amount = "50 USDC"
average_price = true

[[actions.triggers]]
type = "next_candle"
timeframe = "1d"

Tips

Tip

Using next_candle with the same timeframe as your technical indicators makes backtest results more realistic. In live trading, you cannot act on a candle that has not closed yet — next_candle enforces that discipline.

Tip

max_count is essential for accumulation patterns. Without it, a next_candle trigger on "4h" would keep buying every 4 hours indefinitely. Set max_count to limit total accumulation per position.

Note

All triggers in an action use AND logic. When next_candle is combined with other triggers, both must be true at the same moment — the candle boundary must coincide with the other conditions being met. If RSI dips below 30 mid-candle but recovers before the candle close, the combined trigger does not fire.

TriggerWhat It Measures
Time in PositionDuration-based exit (time since entry)
Consecutive CandlesStreaks of red/green candles
Price ChangeGlobal price movement over time

Timeframes

Timeframes determine which candle interval is used to calculate an indicator's value. Botmarley supports multi-timeframe analysis, allowing you to combine signals from different time resolutions within a single strategy.

What Are Timeframes?

A timeframe defines the duration of each candle used for indicator calculation. A 1h candle aggregates 60 minutes of price action into a single open/high/low/close bar; a 1d candle aggregates an entire day.

Different timeframes reveal different aspects of the market:

TimeframePerspectiveBest For
1mMicro-scalePrecise entry timing, scalping
5mShort-termShort-term momentum, dip detection
15mIntra-dayDay trading signals
1hMedium-termSwing trading, trend confirmation
4hExtendedMacro trend direction
1dLong-termMajor trend identification

Available Timeframes

Botmarley supports six timeframes:

  • 1m -- 1-minute candles
  • 5m -- 5-minute candles
  • 15m -- 15-minute candles
  • 1h -- 1-hour candles
  • 4h -- 4-hour candles
  • 1d -- 1-day (daily) candles

Default Timeframe

If you omit the timeframe field from a trigger, it defaults to the base candle timeframe (typically 1m). This means the indicator is calculated on 1-minute candles:

# No timeframe specified -- uses the default (1m)
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

To explicitly use a different timeframe, add the timeframe field:

# RSI calculated on 1-hour candles
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"

Note

The timeframe field is optional on all trigger types -- Technical, PriceChange, and NextCandle. When omitted, the trigger evaluates against the default candle interval.

How Multi-Timeframe Works

Botmarley always fetches and stores 1-minute candles as the base data. Higher timeframes are computed from 1-minute data by aggregating candles:

flowchart TD
    A["1m Candles<br/>(Source Data)"] --> B["5m Candles<br/>(5 x 1m aggregated)"]
    A --> C["15m Candles<br/>(15 x 1m aggregated)"]
    A --> D["1h Candles<br/>(60 x 1m aggregated)"]
    A --> E["4h Candles<br/>(240 x 1m aggregated)"]
    A --> F["1d Candles<br/>(1440 x 1m aggregated)"]

    B --> G["Calculate Indicators<br/>on 5m candles"]
    C --> H["Calculate Indicators<br/>on 15m candles"]
    D --> I["Calculate Indicators<br/>on 1h candles"]
    E --> J["Calculate Indicators<br/>on 4h candles"]
    F --> K["Calculate Indicators<br/>on 1d candles"]

    style A fill:#4a9eff,color:#fff

This means:

  1. You do not need separate data downloads for each timeframe.
  2. All timeframes are derived from the same underlying 1-minute data.
  3. Indicators on higher timeframes use fewer data points but represent longer periods of market action.

Using Timeframes in Triggers

Single-Timeframe Strategy

Most simple strategies use a single timeframe (or the default):

[meta]
name = "Simple_RSI"

[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  # No timeframe -- uses default 1m candles

Multi-Timeframe Strategy

The real power comes from combining multiple timeframes. A common pattern is to use a higher timeframe for trend direction and a lower timeframe for entry timing:

[meta]
name = "Multi_TF_Trend_Entry"
description = "Use 1h RSI for trend, 5m for entry timing"
max_open_positions = 2

# ENTRY: 1h RSI oversold + 5m StochRSI extremely oversold
[[actions]]
type = "open_long"
amount = "100 USDC"

  # Higher timeframe: confirms oversold on the 1h chart
  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

  # Lower timeframe: precise entry on the 5m chart
  [[actions.triggers]]
  indicator = "stoch_rsi_14"
  operator = "<"
  target = "10"
  timeframe = "5m"

# EXIT: +3% profit
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

Multi-Timeframe Best Practice

Use higher timeframes (1h, 4h, 1d) to identify the trend direction or overall market condition. Use lower timeframes (1m, 5m, 15m) for precise entry and exit timing. This approach reduces false signals because you are aligning short-term actions with the longer-term trend.

Timeframe Mixing Within an Action

Within a single action, different triggers can use different timeframes. Since triggers are AND-combined, ALL must be true simultaneously:

[[actions]]
type = "open_long"
amount = "100 USDC"

  # Trigger 1: Daily uptrend confirmed
  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

  # Trigger 2: Hourly RSI oversold
  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

  # Trigger 3: 5-minute momentum dip
  [[actions.triggers]]
  indicator = "roc_10"
  operator = "<"
  target = "-3"
  timeframe = "5m"

This action fires only when ALL three conditions are true at the same time:

  • The daily trend is bullish (SMA 50 above SMA 200)
  • The hourly RSI shows an oversold condition
  • The 5-minute momentum shows a sharp dip

Timeframes with Non-Technical Triggers

The timeframe field also works with price_change and next_candle triggers:

price_change with timeframe

A price change trigger with a timeframe evaluates the percentage change over that candle interval:

[[actions.triggers]]
type = "price_change"
value = "-3%"
timeframe = "1h"

This triggers when price drops 3% within the 1-hour window.

next_candle with timeframe

A next_candle trigger with a timeframe waits for the next candle close on that specific interval:

[[actions.triggers]]
type = "next_candle"
timeframe = "4h"

This triggers on every 4-hour candle close.

Note

Some trigger types like pos_price_change and trailing_stop do not use timeframes because they measure changes relative to the position entry price, which is time-independent.

Choosing the Right Timeframe

Here is a guide to help you select timeframes for different purposes:

PurposeRecommended TimeframeWhy
Trend identification1d or 4hFilters out noise, shows the big picture
Swing trading entries1h or 4hBalanced between speed and reliability
Scalping entries1m or 5mFast signals for quick trades
Dip detection5m or 15mCatches intra-day price drops
Volume confirmation1hSmooths out minute-level volume spikes
Moving average crossovers1dDaily crossovers are more meaningful

Warning

Using very short timeframes (1m) for everything will generate many signals, but most may be noise. Using very long timeframes (1d) for everything may cause you to miss opportunities. The best strategies combine both.

Position Management

Position management is how Botmarley tracks your open trades, controls how many positions can be open at once, and handles adding to or exiting positions. Good position management is often the difference between a profitable strategy and a losing one.

What Is a Position?

A position is an active trade. When a strategy's open_long action fires, Botmarley creates a new position that tracks:

FieldDescription
Entry priceThe price at which the position was opened
QuantityHow much of the asset was bought
Total costThe total USDC (or other currency) spent
Current P&LThe unrealized profit or loss based on current price
Open timeWhen the position was created

A position remains open until a sell action with amount = "100%" fires, or until all of the position has been sold through partial sells.

flowchart LR
    A["open_long<br/>Creates Position"] --> B["Position Open<br/>Tracking P&L"]
    B --> C["buy (DCA)<br/>Adds to Position"]
    C --> B
    B --> D["sell 50%<br/>Partial Exit"]
    D --> B
    B --> E["sell 100%<br/>Full Exit"]
    E --> F["Position Closed"]

    style A fill:#22c55e,color:#fff
    style E fill:#ef4444,color:#fff
    style F fill:#6b7280,color:#fff

max_open_positions

The max_open_positions field in the [meta] section controls how many positions can be open simultaneously. This is your primary tool for controlling capital exposure.

How It Works

[meta]
name = "Conservative Strategy"
max_open_positions = 1
SettingBehavior
max_open_positions = 1Only one position at a time. New open_long triggers are ignored until the current position is fully closed.
max_open_positions = 3Up to three positions can be open simultaneously. The fourth open_long trigger is ignored.
Omitted (no setting)Unlimited positions. Every open_long trigger opens a new position.

Warning

Running with unlimited positions (no max_open_positions) can be dangerous. In a sharp market downturn, the bot could open many positions rapidly, using up all available capital. Always set a limit unless you have a specific reason not to.

Choosing a Limit

StyleRecommendedWhy
Conservative1One trade at a time. Full focus on quality.
Moderate2-3Allows some diversification across entries.
Aggressive5+Many concurrent positions. Higher capital risk.

Example:

[meta]
name = "Single Position Focus"
max_open_positions = 1

# This strategy opens at most one position.
# Even if the entry trigger fires again, no new position is created
# until the current one is fully closed.

DCA -- Dollar-Cost Averaging

DCA is a technique where you buy more of an asset as the price drops, lowering your average entry price. In Botmarley, DCA is implemented using buy actions with average_price = true.

Why Use DCA?

Without DCA, if you buy at $50,000 and the price drops to $48,000, you need the price to return to $50,000 to break even. With DCA, you buy more at $48,000, which lowers your average entry price so you break even at a lower price.

DCA in Practice

Here is how a two-level DCA strategy works:

flowchart TD
    A["open_long: Buy $100 at $50,000<br/>Entry Price = $50,000"] --> B{"Price drops 2%<br/>to $49,000?"}
    B -- "Yes" --> C["buy: Add $200 at $49,000<br/>average_price = true<br/>New Entry = $49,333"]
    B -- "No" --> D{"Price rises 3%<br/>from entry?"}
    C --> E{"Price drops 4%<br/>to $48,000?"}
    E -- "Yes" --> F["buy: Add $300 at $48,000<br/>average_price = true<br/>New Entry = $48,500"]
    E -- "No" --> D
    F --> D
    D -- "Yes" --> G["sell 100%<br/>Take Profit"]

    style A fill:#22c55e,color:#fff
    style C fill:#4a9eff,color:#fff
    style F fill:#4a9eff,color:#fff
    style G fill:#ef4444,color:#fff

Entry Price Recalculation

When average_price = true, the entry price is recalculated as a weighted average:

new_entry_price = total_cost / total_quantity

Step-by-step example:

StepActionPriceAmountTotal CostTotal QtyAvg Entry
1open_long$50,000$100$1000.002 BTC$50,000
2buy (DCA)$49,000$200$3000.006082 BTC$49,327
3buy (DCA)$48,000$300$6000.012332 BTC$48,654

After all three buys, the average entry is $48,654 instead of $50,000. The position needs only a 0% (break-even) to $48,654 recovery instead of a full return to $50,000.

Tip

DCA works best in markets that tend to recover (mean reversion). It can amplify losses in a prolonged downtrend. Always pair DCA with a stop-loss to limit maximum exposure.

max_count for DCA Levels

Use max_count on trigger to limit how many times a DCA level fires per position. Without it, a DCA trigger could fire repeatedly if the price oscillates around the threshold.

[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1        # Only fire once per position

With max_count = 1, this DCA level triggers exactly once. Even if the price bounces above and back below -2%, it will not trigger again for the same position.

Position Tracking

While a position is open, Botmarley continuously tracks:

Entry Price

The price at which the position was opened (or the weighted average if DCA has been applied). This is the reference point for all pos_price_change triggers.

Current P&L (Profit & Loss)

Calculated as:

P&L (%) = (current_price - entry_price) / entry_price * 100

For example, if entry is $48,654 and current price is $50,000:

P&L = (50000 - 48654) / 48654 * 100 = +2.77%

Position Size

The total quantity of the asset held and the total cost basis. This is used by percentage-based sell amounts ("50%" sells half the quantity).

Selling: Partial vs. Full Exit

Partial Sell

Selling less than 100% of the position lets you take some profit while keeping exposure:

# Sell half when profitable
[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "2%"

After this sell, 50% of the position remains open. The remaining position continues to be tracked with the same entry price.

Full Exit

Selling 100% closes the position entirely:

# Full exit on profit target
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "5%"

After a full exit, the position is closed. The strategy's open_long action can now create a new position (subject to max_open_positions).

Scaling Out

You can combine multiple sell actions to scale out of a position gradually:

# Sell 30% at +2%
[[actions]]
type = "sell"
amount = "30%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "2%"

# Sell 50% at +4%
[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "4%"

# Sell remaining 100% at +6%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "6%"

Note

Percentage sells are relative to the current position size, not the original. If you sell 50% and then 50% again, you have sold 75% of the original position (50% + 50% of the remaining 50%).

Complete DCA Strategy Example

Here is a full strategy that demonstrates position management in action -- it enters on a Bollinger Band touch, DCA buys at -2%, -4%, and -6%, and exits at +3% profit or -10% stop-loss:

[meta]
name = "BB DCA with Stop Loss"
description = "Bollinger Band entry, 3-level DCA, profit target, and hard stop-loss"
max_open_positions = 1

# ENTRY: Price touches lower Bollinger Band
[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "bb_lower"
  timeframe = "1h"

# DCA LEVEL 1: Buy more at -2% from entry
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1

# DCA LEVEL 2: Buy more at -4% from entry
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-4%"
  max_count = 1

# DCA LEVEL 3: Buy more at -6% from entry
[[actions]]
type = "buy"
amount = "300 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-6%"
  max_count = 1

# TAKE PROFIT: Sell everything at +3%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# STOP LOSS: Cut losses at -10%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-10%"

How this plays out:

  1. Price drops below the lower Bollinger Band on the 1h chart -- the bot buys $100.
  2. If price continues dropping 2% from entry, the bot buys another $100 and averages the entry price down.
  3. If it drops 4% from entry, another $200 is added. At 6%, another $300.
  4. Maximum total investment: $100 + $100 + $200 + $300 = $700.
  5. If price recovers 3% from the (averaged) entry price, everything is sold for profit.
  6. If price drops 10% from entry without recovery, the stop-loss fires and cuts losses.

Tip

Notice the increasing DCA amounts ($100, $100, $200, $300). This is called pyramid DCA -- you invest more at lower prices where the risk-reward is better. The largest buy happens at the deepest dip.

Strategy Examples

This page contains four complete, working strategies with line-by-line explanations. You can use these as starting points for your own strategies -- copy the TOML, modify the parameters, and backtest to see how they perform on your target pair.


1. RSI Scalper

A simple momentum strategy that buys when RSI signals oversold conditions and sells in two stages as price recovers.

Concept: RSI below 30 indicates extreme selling pressure. Statistically, price tends to bounce after reaching oversold levels. This strategy captures that bounce by entering at RSI < 30 and exiting in two steps: partial at RSI > 50 (neutral) and full at RSI > 70 (overbought).

Full TOML

[meta]
name = "RSI_Scalper"

# ENTRY: Buy when RSI drops below 30 (oversold)
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"

# PARTIAL EXIT: Sell 50% when RSI recovers to neutral
[[actions]]
type = "sell"
amount = "50%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "50"

# FULL EXIT: Sell remaining when RSI reaches overbought
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = ">"
  target = "70"

Line-by-Line Explanation

[meta] -- Strategy metadata. No max_open_positions set, so only one position at a time by default behavior of the entry trigger.

Action 1 -- Entry:

  • type = "open_long" -- Creates a new position.
  • amount = "100 USDC" -- Buys $100 worth of the asset.
  • Trigger: rsi_14 < 30 -- RSI with a 14-period lookback drops below 30. This is the classic oversold signal.

Action 2 -- Partial Exit:

  • type = "sell" -- Sells part of the existing position.
  • amount = "50%" -- Sells half. This locks in partial profit while leaving room for more upside.
  • Trigger: rsi_14 > 50 -- RSI recovers to neutral territory. The worst of the selling is over.

Action 3 -- Full Exit:

  • type = "sell" -- Sells the rest.
  • amount = "100%" -- Exits completely.
  • Trigger: rsi_14 > 70 -- RSI reaches overbought. The bounce is likely exhausted.

Tip

This strategy has no stop-loss. For production use, add a stop-loss action (e.g., sell 100% at pos_price_change of -5%). Without a stop-loss, the bot holds through any drawdown waiting for RSI to recover.

Strengths and Weaknesses

StrengthWeakness
Simple and easy to understandNo stop-loss -- can hold losing positions
Works well in ranging marketsPerforms poorly in strong downtrends
Two-stage exit captures more gainRSI can stay below 30 for extended periods

2. Bollinger Bounce DCA

A mean-reversion strategy that enters when price touches the lower Bollinger Band, uses DCA to average down on continued dips, and exits on recovery.

Concept: Bollinger Bands measure volatility. When price drops below the lower band, it is statistically "extended" and tends to revert toward the middle band. This strategy exploits that tendency with DCA to handle cases where price continues to drop after the initial entry.

Full TOML

[meta]
name = "BB_Bounce_DCA"
description = "Enter at BB lower, DCA at -2% and -4%, exit at +3%"
max_open_positions = 3

# ENTRY: Price drops below the lower Bollinger Band
[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "price"
  operator = "<"
  target = "bb_lower"

# DCA LEVEL 1: Position drops 2% -- buy more
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1

# DCA LEVEL 2: Position drops 4% -- buy even more
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-4%"
  max_count = 1

# TAKE PROFIT: Exit at +3% from averaged entry
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# STOP LOSS: Cut losses at -8%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-8%"

Line-by-Line Explanation

[meta]:

  • max_open_positions = 3 -- allows up to 3 simultaneous positions. If the entry trigger fires while 3 positions are open, it is ignored.

Action 1 -- Entry:

  • price < bb_lower -- current price is below the lower Bollinger Band. This means price is at least 2 standard deviations below the 20-period mean.
  • average_price = false -- this is the initial entry, no averaging needed.

Action 2 -- DCA Level 1:

  • type = "buy" -- adds to the existing position.
  • average_price = true -- recalculates entry price as a weighted average.
  • pos_price_change = "-2%" -- fires when price drops 2% below the entry price.
  • max_count = 1 -- only triggers once per position, preventing repeated buys if price oscillates.

Action 3 -- DCA Level 2:

  • Same logic as Level 1, but fires at -4% with a larger amount ($200). The increasing amounts at deeper dips is pyramid DCA -- more capital deployed where risk-reward improves.

Action 4 -- Take Profit:

  • pos_price_change = "3%" -- sell everything when price is 3% above the (averaged) entry. Thanks to DCA, the averaged entry is lower than the original entry, making this target easier to reach.

Action 5 -- Stop Loss:

  • pos_price_change = "-8%" -- if price drops 8% from entry without recovery, cut losses.

Note

The stop-loss at -8% means the maximum loss per position (before DCA) is 8% of $100 = $8. With DCA, total capital at risk is $100 + $100 + $200 = $400, but the averaged entry brings the stop-loss trigger closer to the deeper DCA levels.


3. MACD Crossover

A trend-following strategy that enters on a MACD bullish crossover and exits on a bearish crossover, with DCA and a fixed take-profit target.

Concept: When the MACD line crosses above its signal line, it indicates bullish momentum is increasing. When it crosses below, momentum is fading. This strategy rides the momentum wave.

Full TOML

[meta]
name = "MACD_Crossover"
description = "Enter on MACD bullish cross, DCA on dips, exit on bearish cross or profit"
max_open_positions = 3

# ENTRY: MACD line crosses above signal line on 1h chart
[[actions]]
type = "open_long"
amount = "100 USDC"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_above"
  target = "macd_signal"
  timeframe = "1h"

# DCA LEVEL 1: Buy more if price dips 2%
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1

# DCA LEVEL 2: Buy more if price dips 4%
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-4%"
  max_count = 1

# TAKE PROFIT: Exit at +3%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# EXIT ON BEARISH CROSS: MACD line crosses below signal
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "macd_line"
  operator = "cross_below"
  target = "macd_signal"
  timeframe = "1h"

Line-by-Line Explanation

Action 1 -- Entry:

  • macd_line cross_above macd_signal on 1h -- this is the classic MACD bullish crossover. It occurs when the difference between the 12-period and 26-period EMAs starts increasing, signaling that short-term momentum is turning bullish.
  • timeframe = "1h" -- using hourly candles filters out noise. A 1-minute MACD crossover is common and often meaningless; a 1-hour crossover is a stronger signal.

Actions 2 & 3 -- DCA:

  • Same pyramid DCA pattern as the previous example. If the crossover was a false signal and price drops, DCA lowers the average entry.
  • max_count = 1 prevents repeated buys.

Action 4 -- Take Profit:

  • pos_price_change = "3%" -- exits with profit regardless of MACD state.

Action 5 -- Bearish Exit:

  • macd_line cross_below macd_signal on 1h -- the opposite crossover. Momentum has shifted from bullish to bearish.
  • This acts as a dynamic stop-loss -- instead of a fixed percentage, you exit when the momentum signal that got you in reverses.

Tip

The MACD crossover strategy has two exit conditions: a fixed profit target (+3%) and a dynamic momentum reversal (bearish cross). Whichever happens first closes the position. This is a good pattern -- you take profit when available, but cut losses when the thesis breaks.

Strengths and Weaknesses

StrengthWeakness
Follows momentum -- rides trendsMACD can whipsaw in ranging/choppy markets
Dynamic exit based on momentum shiftCrossovers lag -- entry/exit is never at the top/bottom
DCA mitigates false signal entries1h timeframe means slower reaction to fast moves

4. Multi-Timeframe Trend Follower

An advanced strategy that uses daily moving averages for trend direction, hourly RSI for timing, 5-minute momentum for precise entry, and a trailing stop for exits.

Concept: This strategy only enters in confirmed uptrends (daily SMA golden cross), waits for hourly oversold conditions, and times the entry using 5-minute momentum dips. The trailing stop locks in profits on runs while allowing the position to grow.

Full TOML

[meta]
name = "Multi_TF_Trend_Follower"
description = "Daily trend filter + hourly RSI + 5m entry + trailing stop exit"
max_open_positions = 1

# ENTRY: Three-layer confirmation
# Layer 1: Daily uptrend (SMA 50 > SMA 200 = golden cross)
# Layer 2: Hourly RSI oversold (< 40)
# Layer 3: 5-minute sharp dip (ROC < -3)
[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "sma_50"
  operator = ">"
  target = "sma_200"
  timeframe = "1d"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "40"
  timeframe = "1h"

  [[actions.triggers]]
  indicator = "roc_10"
  operator = "<"
  target = "-3"
  timeframe = "5m"

# DCA: Buy more if price drops 5%, but only if RSI confirms oversold
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-5%"

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "35"
  timeframe = "1h"

# TAKE PROFIT: +5% fixed target
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "5%"

# TRAILING STOP: Sell when price drops 3% from its peak since entry
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "trailing_stop"
  value = "-3%"

# TREND BREAK EXIT: Sell if the daily trend reverses (death cross)
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  indicator = "sma_50"
  operator = "cross_below"
  target = "sma_200"
  timeframe = "1d"

# STOP LOSS: Hard stop at -8%
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-8%"

Line-by-Line Explanation

[meta]:

  • max_open_positions = 1 -- conservative, only one position at a time. Quality over quantity.

Action 1 -- Entry (3 triggers, ALL must be true):

  • Trigger 1: sma_50 > sma_200 on 1d -- the daily 50-period SMA is above the 200-period SMA. This is the "golden cross" -- the strongest trend confirmation in traditional technical analysis. It ensures we only buy in a macro uptrend.
  • Trigger 2: rsi_14 < 40 on 1h -- hourly RSI is oversold. Even in an uptrend, price dips temporarily. This catches those dips.
  • Trigger 3: roc_10 < -3 on 5m -- the 5-minute Rate of Change shows a sharp 3%+ dip in the last 10 candles. This is the precise "buy the dip" moment.

All three must be true simultaneously. This is a very selective entry that fires rarely but with high conviction.

Action 2 -- DCA with Confirmation:

  • pos_price_change = "-5%" AND rsi_14 < 35 on 1h -- adds to the position only if price has dropped significantly AND the hourly RSI confirms oversold. The RSI confirmation prevents DCA during a trend break.

Action 3 -- Take Profit:

  • pos_price_change = "5%" -- exits with +5% profit. A higher target than the simpler strategies because the entry is more selective.

Action 4 -- Trailing Stop:

  • trailing_stop = "-3%" -- this is the key feature. A trailing stop tracks the highest price since entry and sells when price drops 3% from that peak. If price rises 10% and then drops 3%, the trailing stop fires with approximately +7% profit locked in.

Action 5 -- Trend Break Exit:

  • sma_50 cross_below sma_200 on 1d -- the daily death cross. If the macro trend that justified our entry reverses, exit immediately regardless of P&L.

Action 6 -- Hard Stop Loss:

  • pos_price_change = "-8%" -- absolute worst-case exit. If all other exits fail to trigger, this limits loss to 8%.

Note

This strategy has four separate exit conditions. Whichever fires first closes the position:

  1. +5% profit target (ideal case)
  2. -3% trailing stop (locks in gains on a run)
  3. Daily death cross (trend reversal)
  4. -8% hard stop (absolute maximum loss)

Multiple exit conditions provide layered protection -- this is a hallmark of well-designed strategies.

Strengths and Weaknesses

StrengthWeakness
Very selective -- high-quality entriesFires infrequently; may miss opportunities
Multi-timeframe reduces false signalsRequires data on all timeframes
Trailing stop captures large movesComplex -- harder to debug and optimize
Trend filter avoids buying in downtrendsDaily golden cross can be late to form

Comparing the Strategies

StrategyComplexityEntry FrequencyRisk LevelBest Market Condition
RSI ScalperLowHighMediumRanging / choppy
Bollinger Bounce DCAMediumMediumLow-MediumMean-reverting
MACD CrossoverMediumMediumMediumTrending
Multi-TF Trend FollowerHighLowLowStrong uptrend

Tip

Start with the RSI Scalper or Bollinger Bounce DCA to learn how strategies work. Once you are comfortable, try the MACD Crossover. Graduate to the Multi-TF Trend Follower when you understand multi-timeframe analysis and trailing stops. Always backtest before going live.

Backtesting

Backtesting is the process of running a trading strategy against historical market data to see how it would have performed in the past. Instead of risking real money to find out whether your strategy works, you let Botmarley replay weeks or months of price history and apply your strategy rules to every candle, exactly as the live trading engine would.

The result is a detailed report: how much money you would have made or lost, how many trades fired, what your win rate was, and where the strategy struggled.

Backtests list page with filters, PnL percentages, and action counts for each backtest run

Why Backtesting Matters

Building a strategy based on intuition alone is gambling. Backtesting gives you evidence. Specifically, it lets you:

  • Validate logic before risking capital. A strategy that sounds good on paper might produce terrible results when applied to actual price data. Backtesting exposes that before you lose money.
  • Compare strategies objectively. Instead of guessing which of your three strategies is "better," you can run all three against the same data and compare the numbers.
  • Tune parameters with feedback. If your RSI threshold is set to 30, what happens at 25 or 35? Backtesting lets you iterate quickly.
  • Build confidence. Even a profitable strategy will have losing streaks. Seeing those drawdowns in a backtest prepares you for what live trading actually feels like.

How Botmarley Backtesting Works

Botmarley's backtest engine processes candles one at a time (tick-by-tick), evaluating your strategy rules at each step. The pipeline looks like this:

flowchart TD
    A["Load historical candles<br/>(Arrow files on disk)"] --> B["Pre-compute indicator cache<br/>(RSI, SMA, MACD, etc.)"]
    B --> C["Initialize portfolio<br/>(USD balance = initial capital)"]
    C --> D["Process next candle"]
    D --> E{"Evaluate triggers<br/>for each action"}
    E -- "Trigger fired" --> F["Execute action<br/>(buy/sell, update portfolio)"]
    E -- "No match" --> D
    F --> G{"More candles?"}
    G -- Yes --> D
    G -- No --> H["Calculate summary metrics<br/>(PnL, win rate, fees, etc.)"]
    H --> I["Save results to database"]

The engine follows a three-phase execution model:

  1. Pre-computation phase. Before processing any candles, Botmarley calculates all indicator values (RSI, SMA, Bollinger Bands, MACD, etc.) for the entire candle history and stores them in a cache. This means each tick lookup during execution is instant -- the engine never recalculates an indicator mid-run.

  2. Execution phase. The engine walks through every candle in order. At each tick, it evaluates every action in your strategy. If an action's triggers are satisfied, it executes (opens a position, buys more, or sells). The portfolio state updates immediately: USD balance goes down when buying, crypto balance goes up, and vice versa for sells.

  3. Summary phase. After the last candle, the engine calculates final metrics: net PnL, win rate, trading fees, max drawdown, and more. Everything is saved to PostgreSQL along with the full action log.

The 0.1% Trading Fee

Every executed trade in a backtest incurs a simulated 0.1% fee on the trade amount. This fee is not deducted from your portfolio during execution -- it is calculated separately and subtracted from the net result at the end.

For example, if you buy $1,000 worth of BTC, the fee for that trade is $1.00. If you later sell all of it for $1,050, the sell fee is $1.05. Your net result (before fees) is +$50. Your final result (after fees) is $50 - $1.00 - $1.05 = $47.95.

The 0.1% rate is a simplified approximation of exchange fees. Actual fee schedules on Kraken and Binance vary by trading volume tier and maker/taker status, but 0.1% is a reasonable middle ground for most users.

Limitations of Backtesting

Backtesting is a powerful tool, but it has real limitations. Understanding these will save you from nasty surprises when you go live.

LimitationWhat it means
No slippage simulationIn a backtest, your order fills at the exact candle close price. In live trading, the actual fill price may be slightly different, especially during fast-moving markets.
No liquidity modelingThe backtest assumes your order size is small enough to fill completely. In reality, large orders on thin order books may only partially fill or move the price against you.
No latencyBacktests execute instantly. Live trading involves network round-trips to the exchange, which can introduce delays of hundreds of milliseconds to seconds.
Perfect informationThe backtest "sees" the full candle (open, high, low, close) for each tick. In live trading, you only see the close price at the end of the candle interval.
Simplified feesThe flat 0.1% fee does not account for tiered fee schedules, funding rates, or other exchange-specific costs.

Backtest vs. Live Trading

The backtest engine and the live trading engine share the same core evaluation logic -- the same trigger evaluation code, the same portfolio tracking, the same action execution. This means a strategy that "works" in backtesting will behave identically in live trading under ideal conditions.

The differences are:

AspectBacktestLive Trading
Data sourceArrow files on diskExchange REST API (polled every 60s)
Execution speedThousands of candles per secondOne candle per poll interval
Order fillsInstant at candle close priceExchange order book (may slip)
FeesSimulated 0.1% flatActual exchange fee schedule
PortfolioSimulated in memoryPaper (simulated) or Real (Kraken/Binance)
RiskZeroPaper = zero, Exchange = real money

Warning

Past performance does not guarantee future results. A strategy that returned +40% in a backtest against last month's data may lose money next month. Markets change, volatility shifts, and conditions that produced your backtest profits may never repeat. Always treat backtest results as one input among many, not as a prediction.

Running a Backtest

This chapter walks you through running a backtest from start to finish. By the end, you will have a completed backtest result with PnL, trade markers on a chart, and a full action log.

Prerequisites

Before you can run a backtest, you need two things:

  1. A strategy. You must have at least one strategy saved in the Strategy Editor. If you have not created one yet, see the Strategy Editor chapter.

  2. Historical candle data. Botmarley runs backtests against Arrow files stored on disk. You must download historical data for the pair you want to test before running a backtest. See Downloading History for how to do this.

Note

If you try to run a backtest without historical data for the selected pair and date range, the backtest will fail with an "insufficient data" error.

Step-by-Step: Running a Backtest

1. Navigate to the Strategy Editor

Open the Botmarley web interface at http://localhost:3000 and click Strategies in the sidebar. Find the strategy you want to test and click on it to open the detail page.

2. Click the "Backtest" Button

On the strategy detail page, you will see a Backtest button (or section). Clicking it opens the backtest configuration form.

3. Configure the Backtest

Fill in the backtest parameters:

ParameterDescriptionExample
PairThe trading pair to test against.BTC/USD, ETH/USD
Start dateThe beginning of the historical period.2025-09-01
End dateThe end of the historical period.2025-10-01
Initial capital (USD)The starting balance for the simulated portfolio.1000

Choosing a date range

  • Start with a 1-month range to get meaningful results without waiting too long.
  • Make sure your downloaded historical data covers the entire date range. If you have data from Sept 15 to Oct 15 but set the start date to Sept 1, the backtest will only use data from Sept 15 onward.
  • Avoid testing against extremely short periods (a few hours). Strategy triggers may not fire often enough to produce useful results.

Choosing initial capital

  • Use an amount that reflects what you would actually trade with. A strategy tested with $100,000 may behave differently than one tested with $500 because position sizes scale with capital.
  • Common starting points: $1,000 for exploration, $10,000 for more realistic testing.

4. Submit and Wait

Click Run Backtest. Botmarley creates a background task for the backtest and adds it to the task queue. You will see a confirmation that the task has been queued.

The backtest runs asynchronously -- you do not need to keep the page open. The pipeline is:

flowchart LR
    A["Click Run Backtest"] --> B["Task queued"]
    B --> C["Task worker picks up task"]
    C --> D["Engine processes candles"]
    D --> E["Results saved to database"]
    E --> F["Backtest appears on<br/>Backtests page"]

For a typical 1-month period (about 43,000 one-minute candles), the backtest completes in a few seconds to under 30 seconds, depending on your hardware and strategy complexity.

5. View the Results

Navigate to the Backtests page by clicking Backtests in the sidebar. You will see a table of all backtest runs, sorted by creation date. Your new backtest will appear at the top with a status of Completed (or Failed if something went wrong).

Backtests list showing completed backtest runs with PnL, action counts, and status

Click on the backtest row to open the full results page. See Understanding Results for a detailed breakdown of what you will find there.

What Happens Behind the Scenes

When you click Run Backtest, here is what Botmarley does internally:

  1. Validates the strategy TOML and parses it into the internal strategy format.
  2. Loads candle data from the Arrow files on disk for the selected pair and date range.
  3. Enqueues a BacktestRun task in the task queue with your configuration.
  4. The task worker picks up the task, creates a BacktestEngine, and calls BacktestEngine::run().
  5. The engine pre-computes all indicator values (RSI, SMA, Bollinger Bands, etc.) into a cache for fast lookups.
  6. The engine walks through every candle tick-by-tick, evaluating your triggers and executing actions when conditions are met.
  7. After the last candle, it calculates summary metrics (PnL, win rate, fees, etc.).
  8. Results and the full action log are saved to PostgreSQL.
  9. The task is marked complete, and the backtest appears on the Backtests page.

Troubleshooting

ProblemLikely causeFix
Backtest status is "Failed"Missing historical data for the pair/date rangeDownload history for that pair first
Zero trades executedStrategy triggers never fired during the periodCheck your trigger thresholds -- they may be too strict for the selected period
Backtest takes very longVery large date range (several months of 1m data)Try a shorter date range, or wait -- performance targets are under 30 seconds for one month
"Insufficient data" errorDate range extends beyond available Arrow dataAdjust dates to match your downloaded data

Understanding Results

After a backtest completes, Botmarley saves a detailed results page with summary statistics, interactive charts, and a full action log. This chapter explains every piece of that page so you know exactly what you are looking at.

The Results Page Layout

The backtest detail page is organized into several sections:

  1. Summary statistics -- the headline numbers at the top.
  2. Price chart -- candlestick chart with buy/sell markers overlaid.
  3. Equity curve -- a line chart showing your portfolio value over time.
  4. Action log -- a table of every trade the strategy executed.

Backtest detail page showing the candlestick chart with trade markers and summary statistics

Summary Statistics

At the top of the results page, you will find a grid of key metrics. Here is what each one means:

MetricWhat it tells you
Net Result (USD)The raw profit or loss: final portfolio value minus initial capital. This is calculated before fees.
Net Result (%)Net result as a percentage of your initial capital. +15% means your portfolio grew by 15%.
Trading Fees (USD)Total simulated fees across all executed trades (0.1% per trade).
Final Result (USD)Net result minus trading fees. This is your true bottom line.
Final Result (%)Final result as a percentage of initial capital. This is the number that matters most.
PnL at Last TradePortfolio value at the moment of the last executed trade, minus initial capital. Useful when the strategy ends with an open position -- the final result includes unrealized gains/losses from the last candle's close price, but PnL at last trade shows where you stood at the last actual trade.
Total TradesNumber of executed trade actions (buys + sells).
Profitable TradesTrades where the realized PnL was positive (sell price > buy price).
Losing TradesTrades where the realized PnL was negative.
Win Rate (%)Percentage of profitable trades out of total trades.
Max Drawdown (%)The largest peak-to-trough decline in portfolio value during the backtest. A drawdown of 12% means at some point your portfolio dropped 12% from its highest point.
Sharpe RatioA measure of risk-adjusted return. Higher is better. Values above 1.0 are generally considered good; above 2.0 is excellent.
DurationHow long the backtest engine took to run (in milliseconds).

Note

Max Drawdown and Sharpe Ratio are planned for a future release and may show as 0 or "N/A" in the current version.

How to Read These Numbers

  • Positive Final Result means the strategy was profitable over the test period.
  • Negative Final Result means it lost money.
  • Win Rate alone does not tell the whole story. A strategy with a 30% win rate can still be very profitable if the average winning trade is much larger than the average losing trade.
  • Net Result vs. Final Result: The gap between these two is your fee burden. If the difference is large relative to your profit, the strategy might be trading too frequently.

Price Chart

The price chart displays candlesticks for the backtest period with trade markers overlaid:

  • Green upward arrows mark buy actions (open long and additional buys).
  • Red downward arrows mark sell actions.
  • Hovering over a marker shows the trade details (price, amount, trigger).

Timeframe Switching

By default, the chart aggregates candles to a readable timeframe. You can switch between:

TimeframeCandle durationBest for
1m1 minuteShort backtests (hours to a day), seeing exact entry/exit points
5m5 minutesMulti-day backtests, good balance of detail and overview
15m15 minutesWeek-long backtests
1h1 hourMonth-long backtests, big-picture view

When you switch timeframes, the candles are re-aggregated and trade markers are snapped to the nearest candle. The underlying data does not change -- only the visual grouping.

Indicator Overlays

If your strategy uses technical indicators (RSI, SMA, Bollinger Bands, etc.), you can toggle them on and off using the indicator panel. Indicators are pre-computed and included in the chart data, so toggling is instant -- no server round-trip required.

  • Price overlays (SMA, EMA, Bollinger Bands) render as lines on the main price chart.
  • Separate pane indicators (RSI) render in a dedicated panel below the price chart.

Equity Curve

The equity curve is a line chart that plots your portfolio's total value (USD balance + crypto holdings valued at market price) at each point in time.

How to Interpret the Equity Curve

  • Upward slope means the portfolio is growing.
  • Downward slope means the portfolio is declining.
  • Flat sections mean the strategy had no open positions (sitting in cash) or the market was moving sideways with no triggers firing.
  • Sharp drops indicate significant losing trades or drawdown periods.

A healthy equity curve generally trends upward over time with manageable dips. An equity curve that rises steeply then crashes suggests the strategy was lucky during one period and then gave it all back.

Tip

Compare the equity curve to the raw price chart. If your equity curve closely mirrors the asset's price, your strategy might just be "buy and hold with extra steps." The value of a strategy is in doing better than simply holding the asset, or achieving similar returns with less risk (smaller drawdowns).

Action Log

Below the charts, you will find a table listing every trade action the strategy executed during the backtest. Each row contains:

ColumnDescription
TickThe candle index (0-based) where the action occurred.
TimestampThe date and time of the candle.
ActionThe type of action: open_long, buy, or sell.
Position #Which position this action belongs to (for multi-position strategies).
PriceThe candle close price at execution.
Amount (USD)The dollar amount of the trade.
Amount (Crypto)The crypto amount bought or sold.
TriggerThe trigger condition that caused this action (e.g., "RSI(14) < 30" or "Position price change +5%").
Portfolio BalanceTotal portfolio value (USD + crypto) after this action.
PnL (USD)Realized profit or loss on this specific trade (for sells). Blank for buys.
Statusexecuted, skipped, or impossible.

Action Statuses

  • executed -- The action was carried out. Portfolio updated.
  • skipped -- The trigger conditions were met, but the action was skipped for a reason (e.g., max_count reached, no open position to sell).
  • impossible -- The action could not execute (e.g., trying to buy with insufficient USD balance).

Note

By default, only executed actions are shown. Enable Verbose Logging when running the backtest to also save skipped and impossible actions. This is useful for debugging why a strategy is not trading as expected.

Red Flags in Results

Not all positive results are good results. Watch out for these warning signs:

Red flagWhat it might mean
Very few trades (< 5)The strategy triggers are too restrictive, or the date range is too short. The results may not be statistically meaningful.
Win rate below 30%The strategy loses most of its trades. It can still be profitable if winners are large, but verify that the average winning trade significantly exceeds the average loser.
Max drawdown above 30%The strategy has periods of severe loss. Ask yourself: could you tolerate watching your account drop by a third?
Final result is positive but PnL at last trade is negativeThe profit came from an unrealized position at the end of the backtest (the last candle happened to be at a favorable price). This is fragile -- the price could have gone the other way.
Fees eat most of the profitIf trading fees are more than 20-30% of the net result, the strategy is overtrading. Consider widening trigger thresholds to trade less frequently.
Equity curve has a single spikeOne lucky trade drove all the profit. Remove that trade mentally and ask if the strategy still makes sense.

Warning

A backtest with zero trades is not a "safe" result -- it means your strategy never did anything. Check your trigger conditions and make sure the historical data covers a period where those conditions would actually occur.

Bulk Backtesting

When you have multiple strategies and want to find out which one performs best under the same market conditions, running them one at a time is tedious. Bulk backtesting lets you run the same configuration (pair, date range, initial capital) against multiple strategies in a single action.

What Bulk Backtesting Does

Bulk backtesting takes a list of strategies and enqueues an individual backtest task for each one, all sharing the same parameters:

  • Same trading pair (e.g., BTC/USD)
  • Same date range (e.g., Sept 1 to Oct 1)
  • Same initial capital (e.g., $1,000)

Each strategy runs independently through the backtest engine, producing its own full result set (PnL, win rate, action log, charts). When all runs complete, you can compare them side by side on the Backtests page.

flowchart TD
    A["Select strategies<br/>Strategy A, B, C"] --> B["Set shared config<br/>Pair, dates, capital"]
    B --> C["Enqueue bulk task"]
    C --> D["Task worker creates<br/>individual backtest tasks"]
    D --> E["Strategy A<br/>backtest"]
    D --> F["Strategy B<br/>backtest"]
    D --> G["Strategy C<br/>backtest"]
    E --> H["Results page:<br/>compare all runs"]
    F --> H
    G --> H

How to Run a Bulk Backtest

1. Navigate to the Strategies Page

Open the Strategies page from the sidebar. You will see a list of all saved strategies.

2. Select Multiple Strategies

Use the checkboxes next to each strategy to select the ones you want to test. You can select as many as you like.

3. Click "Bulk Backtest"

With strategies selected, click the Bulk Backtest button. A configuration form appears.

4. Configure the Shared Parameters

Fill in the same fields you would for a single backtest:

ParameterDescription
PairThe trading pair all strategies will be tested against.
Start dateBeginning of the test period.
End dateEnd of the test period.
Initial capitalStarting USD balance (same for all strategies).

5. Submit

Click Start. Botmarley enqueues a BulkBacktest task, which in turn creates one BacktestRun task per selected strategy. You can monitor progress on the Tasks page.

Comparing Results

Once all backtests complete, navigate to the Backtests page. All runs from the bulk job will appear in the table. To compare effectively:

  • Sort by Final Result (%) to rank strategies by profitability.
  • Filter by pair if you have mixed results from different bulk runs.
  • Look at win rate alongside PnL -- a high PnL with a low win rate means the strategy relies on a few big winners.
  • Check total trades -- strategies with very different trade counts are hard to compare directly. A strategy with 3 trades and +10% is less reliable than one with 50 trades and +8%.

Tip

When comparing strategies, pay attention to consistency across different time periods. If Strategy A beats Strategy B on Sept data but loses on Oct data, neither has a clear edge. Run bulk backtests against multiple date ranges to build a fuller picture.

Use Cases

Strategy Selection

You have five strategy ideas. Run all five against the same 3-month period. Eliminate the ones that lose money or have unacceptable drawdowns. Shortlist the top performers for paper trading.

Parameter Optimization

Create multiple copies of the same strategy with different parameter values (e.g., RSI period 10, 14, 20). Bulk backtest all variants to find the parameter set that performs best for your target pair and timeframe.

Pair Comparison

If you want to test one strategy against multiple pairs, create the backtest runs individually (bulk backtesting uses a single pair). Then compare results across pairs on the Backtests page using the pair filter.

Note

Bulk backtesting runs all selected strategies against the same pair. To test across multiple pairs, run separate bulk backtests for each pair.

Live Trading

Live trading is where your strategy meets the real market. Instead of replaying historical candles, Botmarley connects to your exchange (Kraken or Binance) in real time, fetches new candle data as it becomes available, evaluates your strategy triggers, and executes trades -- either simulated (Paper) or with real money (Binance account).

Danger

Live trading with a Binance account uses real money. Orders placed through the Binance API are real, irreversible market orders. Make sure you have thoroughly backtested and paper-traded your strategy before connecting it to a real exchange account. Start with small amounts.

Trading sessions list page

Paper vs. Real Trading

Botmarley supports two modes of live trading, determined by which account you select when starting a session:

ModeAccount typeOrdersRisk
Paper tradingPaper accountSimulated locally. No exchange API calls for orders.Zero. No real money involved.
Real tradingBinance accountPlaced on Binance via the signed REST API.Real. Funds can be gained or lost.

In both modes, the trading engine behaves identically: same candle fetching, same indicator calculation, same trigger evaluation, same action execution logic. The only difference is what happens when an action fires:

  • Paper mode: The engine updates the simulated portfolio in memory and in the database. No exchange order is placed.
  • Real mode: The engine sends a market order to Binance via the signed REST API, waits for confirmation, then updates the session state with the actual fill price and amount.

Tip

Always test a new strategy in Paper mode first. Run it for at least a few days to confirm it behaves as expected with live market data before switching to a real Binance account.

How the Trading Engine Works

The live trading engine runs as an async background task for each active session. Here is the cycle it repeats:

flowchart TD
    A["Wait for poll interval<br/>(default: 60 seconds)"] --> B["Fetch latest candle(s)<br/>from Exchange REST API"]
    B --> C["Append to candle buffer<br/>(rolling window of 500)"]
    C --> D["Rebuild indicator cache<br/>(RSI, SMA, MACD, etc.)"]
    D --> E{"Evaluate strategy<br/>triggers"}
    E -- "Trigger fired" --> F["Execute action<br/>(Paper or Binance order)"]
    E -- "No match" --> G["Update progress store<br/>(price, PnL, candle count)"]
    F --> G
    G --> H{"Check control<br/>channel"}
    H -- "Continue" --> A
    H -- "Pause" --> I["Enter paused state<br/>(wait for resume/stop)"]
    H -- "Stop" --> J["Graceful shutdown<br/>(save state to DB)"]

Poll Interval

The engine fetches new candles at a configurable interval, defaulting to 60 seconds. This aligns with 1-minute candle data from the exchange. Each poll:

  1. Requests the latest OHLCV candle(s) from the exchange REST API (Kraken or Binance, based on the account type).
  2. Deduplicates against the existing buffer (avoids processing the same candle twice).
  3. Appends new candles to a rolling buffer (capped at 500 candles to manage memory).

Candle Buffer and Indicators

The engine maintains a rolling buffer of the most recent candles. After appending new data, it rebuilds the indicator cache. This ensures that indicators like RSI(14) or SMA(50) always have enough historical context to produce accurate values.

Trigger Evaluation

The engine uses the exact same evaluation code as the backtest engine. This means:

  • Technical indicator triggers (RSI < 30, SMA crossover, etc.) work identically.
  • Price change triggers, position-based triggers, and trailing stops all behave the same.
  • Multi-position logic, max_count limits, and all other strategy features apply.

Order Execution

When a trigger fires:

  • Paper account: Portfolio balances are updated in memory. The action is recorded in the database with status "executed."
  • Binance account: A spot market order is sent to Binance via the signed REST API. The engine records the exchange's fill price and amount. If the order fails (insufficient balance, API error, etc.), the action is recorded with status "order_failed."

The Full Flow

Putting it all together, here is the complete data flow from market to database:

flowchart LR
    Exchange["Exchange API<br/>(Kraken or Binance)"] -->|"REST: fetch candles"| Engine["Trading Engine"]
    Engine -->|"Calculate"| Indicators["Indicator Cache<br/>(RSI, SMA, BB, MACD)"]
    Indicators -->|"Evaluate"| Triggers["Strategy Triggers"]
    Triggers -->|"Fire"| Execute["Execute Action"]
    Execute -->|"Paper: update locally"| DB["PostgreSQL<br/>(session state)"]
    Execute -->|"Binance: place order"| Exchange
    Engine -->|"SSE events"| Browser["Your Browser<br/>(live updates)"]

Key Concepts

  • Session. A single run of a strategy against a pair using a specific account. Each session has its own state: balance, PnL, action history, candle count.
  • Progress store. An in-memory map of session progress, updated every poll cycle. The UI reads from this via SSE for real-time updates.
  • Control channel. Each session has a tokio::sync::watch channel that the server uses to send pause, resume, and stop commands. The engine checks this channel after every poll cycle.
  • Strategy snapshot. When a session starts, the current strategy TOML is copied and stored with the session. This means editing the strategy later does not affect running sessions.

Trading Sessions

A trading session is a single run of a strategy. It begins when you click Start, runs continuously until you stop it (or it encounters an error), and maintains its own isolated state throughout its lifetime. This chapter covers every session lifecycle action: starting, pausing, resuming, stopping, and deleting.

Starting a Session

Step 1: Go to the Trading Page

Navigate to Trading in the sidebar. You will see a list of all sessions (active and stopped) and a form to start a new one.

Trading sessions list with active and stopped sessions

Step 2: Select a Strategy

Choose the strategy you want to run from the dropdown. Only saved strategies appear here. If you need to create or edit a strategy first, visit the Strategy Editor.

Step 3: Select a Trading Pair

Choose the pair you want to trade, such as BTC/USD or ETH/USD. The available pairs are configured in Settings.

Step 4: Select an Account

Choose the account that the session will trade through:

  • Paper account -- simulated trading, no real orders. Ideal for testing.
  • Exchange account (Kraken or Binance) -- real exchange trading. Real order execution is currently supported for Binance accounts only. Orders are placed on Binance with real money.

Warning

Selecting a Binance account means the bot will place real orders with real funds. Make sure you have tested the strategy thoroughly in Paper mode first. (Kraken accounts support candle fetching and data sync, but real order execution is Binance only.)

Step 5: Set Initial Capital

Enter the starting USD balance for this session. For Paper accounts, this creates a simulated balance. For exchange accounts, this sets the budget the strategy will work with (it does not transfer funds -- your exchange account must already have sufficient balance).

Step 6: Start

Click Start. Botmarley will:

  1. Validate that the strategy, pair, and account exist.
  2. Take a snapshot of the current strategy TOML (so future edits do not affect this session).
  3. Enqueue a TradingStart task in the task queue.
  4. The task worker spawns the trading engine as a background async task.
  5. The engine loads its initial candle buffer and begins polling for new data.

The session will appear in the session list with status Running.

Session Lifecycle

A trading session moves through a defined set of states:

stateDiagram-v2
    [*] --> Running: Start
    Running --> Paused: Pause
    Paused --> Running: Resume
    Running --> Stopped: Stop
    Paused --> Stopped: Stop
    Running --> Failed: Error
    Running --> Running: Server restart (auto-recovery)
    Paused --> Running: Server restart (auto-recovery)

Pausing a Session

Clicking Pause on a running session sends a pause command to the engine via its control channel. The engine:

  1. Finishes processing the current poll cycle (does not interrupt mid-evaluation).
  2. Enters a paused state -- it stops fetching new candles and evaluating triggers.
  3. Keeps the session state intact in memory and the database.
  4. Records a "paused" event in the session event log.

While paused:

  • No new candles are fetched. The market continues to move, but the engine is not watching.
  • No triggers are evaluated. Even if conditions are met, no trades execute.
  • Existing positions remain open. Pausing does not close positions.
  • The session appears as "Paused" in the UI with a Resume button.

Note

Pausing is useful when you want to temporarily stop a strategy without losing its state -- for example, during scheduled exchange maintenance, before a major news event, or while you review recent trades.

Resuming a Session

Clicking Resume on a paused session sends a resume command. The engine:

  1. Exits the paused state.
  2. Fetches any candles that were produced while paused (backfills the gap).
  3. Resumes normal polling and trigger evaluation.
  4. Records a "resumed" event in the session event log.

The session continues exactly where it left off, with all positions, balances, and counters intact.

Stopping a Session

Clicking Stop sends a stop command. The engine performs a graceful shutdown:

  1. Finishes the current poll cycle.
  2. Saves the final session state to the database (balance, PnL, candle count, portfolio snapshot).
  3. Records a "stopped" event in the session event log.
  4. Exits the background task.

The session is marked as Stopped in the database. Stopped sessions cannot be resumed -- they are finished. Their data (action log, charts, PnL history) remains available for review on the session detail page.

Tip

Stopping a session does not close open positions. If you have open positions on an exchange account, you will need to manage them manually on the exchange after stopping the session.

Stop All

The Stop All button on the Trading page sends a stop command to every active (running or paused) session simultaneously. This is an emergency measure for situations where you want to halt all automated trading immediately.

Each session receives the stop command independently and performs the same graceful shutdown described above.

Deleting a Session

Deleting a session removes it from the database entirely:

  • The session record is deleted.
  • All associated trade actions are deleted.
  • All session events are deleted.

This action is permanent and cannot be undone.

Warning

Deletion is irreversible. If you want to keep the session data for reference, stop the session instead of deleting it. Stopped sessions remain visible in the session list.

Only stopped or failed sessions can be deleted. You cannot delete a running or paused session -- stop it first.

Summary of Actions

ActionWhat it doesReversible?
StartCreates a new session and begins tradingNo (but you can stop it)
PauseTemporarily halts evaluation; state preservedYes (resume)
ResumeContinues a paused session from where it left offN/A
StopGracefully ends the session permanentlyNo
Stop AllStops all active sessions at onceNo
DeleteRemoves session and all its data from the databaseNo

Monitoring

Once a trading session is running, Botmarley provides a real-time monitoring dashboard on the session detail page. This chapter explains every element of that page and how to use it effectively.

Accessing the Session Detail Page

Click on any session in the Trading session list to open its detail page. The URL follows the pattern:

http://localhost:3000/trading/session/{session-id}

The page updates automatically -- you do not need to refresh.

Real-Time Data via SSE

Botmarley uses Server-Sent Events (SSE) to push live updates from the server to your browser. When you open a session detail page, your browser establishes a persistent connection to the SSE endpoint:

/sse/trading/{session-id}

Through this connection, the server sends updates whenever the engine processes a new candle, executes a trade, or changes status. The UI components on the page listen for these events and update themselves instantly via HTMX partial swaps.

This means:

  • No polling delays. You see changes within a second of them happening on the server.
  • No manual refresh needed. The page stays current as long as you have it open.
  • Low bandwidth. SSE sends only the data that changed, not the entire page.

Monitoring Elements

Current Price

The session displays the latest candle close price for the trading pair. This updates every poll cycle (default: every 60 seconds when a new candle arrives).

Balance and PnL

FieldDescription
Current BalanceTotal portfolio value in USD (cash + crypto holdings valued at current price).
PnL (USD)Absolute profit or loss: current balance minus initial capital.
PnL (%)PnL as a percentage of initial capital.

These values update after every candle is processed. During volatile markets, you will see them change with each tick.

Candles Processed

A counter showing how many candles the engine has evaluated since the session started. This gives you a sense of how long the session has been active and confirms that the engine is receiving data from the exchange.

Open Positions

Displays the number of currently open positions and their details:

  • Position number (e.g., #1, #2 for multi-position strategies)
  • Entry price -- the average price at which the position was opened
  • Current value -- the position's value at the current market price
  • Unrealized PnL -- how much the position would gain or lose if sold now

Trade Action History

A table of all executed trade actions for this session, ordered newest first. Each row shows:

ColumnDescription
TimeWhen the action occurred.
ActionType: open_long, buy, or sell.
PriceThe execution price.
AmountUSD and crypto amounts.
TriggerWhich trigger condition caused the action.
PnLRealized profit/loss (for sells).

New actions appear at the top of the table automatically via SSE.

Strategy Trigger Status

Shows the current state of each trigger in your strategy:

  • Whether the trigger condition is currently met or not.
  • For technical indicators: the current indicator value (e.g., RSI = 42.3).
  • For price-based triggers: how close the current price is to the trigger threshold.

This helps you understand what the strategy is "thinking" right now and anticipate when the next trade might fire.

Session Controls

At the top of the page, you will find buttons to control the session:

  • Pause -- temporarily stop evaluation (available when running).
  • Resume -- continue from pause (available when paused).
  • Stop -- permanently end the session.

See Trading Sessions for details on each action.

Live Candlestick Chart

The session detail page includes an interactive candlestick chart powered by Lightweight Charts. The chart displays:

  • Candlesticks showing OHLCV data for the session's trading pair.
  • Buy markers (green arrows) at the exact candles where buy actions executed.
  • Sell markers (red arrows) at sell action candles.
  • Indicator overlays if the strategy uses technical indicators (togglable).

The chart updates in real time as new candles arrive. You can:

  • Zoom in and out with the scroll wheel.
  • Pan by clicking and dragging.
  • Switch timeframes between 1m, 5m, 15m, and 1h for different levels of detail.

Tip

The 5-minute view is a good default for monitoring. It gives enough detail to see individual trades while keeping the chart readable over longer periods.

Tips for Monitoring During Volatile Markets

Volatile markets (high price swings, rapid moves) are when your strategy is most likely to trigger actions. Here are some practical tips:

  1. Keep the session page open. SSE updates ensure you see trades the moment they happen. If you close the page and come back later, you might miss critical context about why a trade fired.

  2. Watch the trigger status panel. During high volatility, indicators like RSI can swing rapidly between overbought and oversold zones. The trigger status panel shows you in real time how close the strategy is to firing.

  3. Check open positions. If your strategy opens positions during a volatile period, monitor unrealized PnL closely. A position that is +5% can swing to -5% quickly.

  4. Do not panic-stop. If the strategy is executing trades during a volatile period, that is what it is designed to do. Stopping the session mid-volatility may leave you with an unfavorable open position. If you are uncomfortable, pause the session instead -- this preserves state and lets you resume later.

  5. Review the action log after the event. Once volatility subsides, scroll through the action history to understand what the strategy did and why. This is valuable feedback for refining triggers.

Note

If you are running multiple sessions simultaneously, each one has its own SSE stream and monitoring page. You can open multiple browser tabs to monitor several sessions at once.

What Gets Persisted

Not everything you see on the monitoring page is stored permanently. Here is the breakdown:

DataStored in DB?Notes
Trade actionsYesFull action log with all details
Session state (balance, PnL, status)YesUpdated periodically
Session events (start, pause, resume, stop)YesTimestamped audit trail
Candle bufferNoIn-memory only; refetched on restart
Indicator cacheNoRebuilt from candle buffer each cycle
Progress store (current price, live PnL)NoIn-memory; reconstructed from DB on restart

This means if the server restarts, you will not lose any trade history or session state, but the live monitoring data (current price, indicator values) will take one poll cycle to repopulate. See Session Recovery for details.

Session Recovery

Servers restart. Power goes out, operating systems update, processes crash. When Botmarley runs trading sessions that manage real money (or paper money you care about tracking), it needs to handle restarts gracefully. This chapter explains what happens when the server goes down and comes back up.

Automatic Recovery

When the Botmarley server starts, one of the first things it does is check the database for trading sessions that were active at the time of the last shutdown. Specifically, it looks for sessions with a status of Running or Paused.

For each session it finds, the server:

  1. Logs a server_restart event in the session's event history. This creates an audit trail so you can see exactly when gaps occurred.
  2. Parses the strategy TOML snapshot stored when the session was originally started.
  3. Creates a new control channel (the mechanism used to send pause/resume/stop commands).
  4. Restores the portfolio state from the last saved snapshot in the database (positions, balances, fire counts).
  5. Spawns a new trading engine task that picks up where the previous one left off.

The entire process is automatic. You do not need to manually restart sessions after a server restart.

flowchart TD
    A["Server starts"] --> B["Query DB for sessions<br/>with status Running or Paused"]
    B --> C{"Sessions found?"}
    C -- No --> D["Nothing to recover<br/>Normal startup continues"]
    C -- Yes --> E["For each session:"]
    E --> F["Log server_restart event"]
    F --> G["Parse strategy TOML<br/>from stored snapshot"]
    G --> H["Restore portfolio state<br/>from DB snapshot"]
    H --> I["Spawn trading engine<br/>async task"]
    I --> J["Engine backfills missed<br/>candles from exchange"]
    J --> K["Normal operation resumes"]

Tip

Recovery is fully automatic. If your server restarts unexpectedly, just wait for it to come back up. Your trading sessions will resume on their own within seconds of the server starting.

What Gets Recovered

ComponentRecovery method
Session statusRead from database. Both "running" and "paused" sessions are recovered. Paused sessions resume as running.
Strategy rulesParsed from the TOML snapshot stored at session creation time. This means the strategy used is the one that was active when the session started, not the current version of the strategy file.
Portfolio stateDeserialized from the last_portfolio_snapshot JSON field in the database. This includes USD balance, crypto balances, open positions, entry prices, and action fire counts.
Trade action historyLoaded from the database. All previously executed actions are intact.
Session eventsAll previous events (started, paused, resumed) remain in the database, plus the new server_restart event.
Counterstotal_actions and candles_processed are restored from the database, preventing duplicate counting.

What Does Not Recover

ComponentWhat happens instead
Candle bufferThe in-memory rolling buffer of recent candles is lost on shutdown. On recovery, the engine rebuilds it by fetching historical candles from the exchange API (Kraken or Binance, based on the account type) -- typically the last 12-24 hours of 1-minute data. This usually takes a few seconds.
Indicator cacheRebuilt automatically from the refetched candle buffer. No data loss -- indicators are always recalculated from raw candle data.
Progress storeThe in-memory progress map (current price, live PnL display) is empty on restart. It repopulates after the first poll cycle.
SSE connectionsBrowser SSE connections are dropped when the server stops. Browsers will automatically reconnect when the server comes back up. You may see a brief "connection lost" state in the UI.

The Gap Period

Between the server stopping and restarting, the engine is not running. During this gap:

  • No candles are fetched. The market continues to move, but Botmarley is not watching.
  • No triggers are evaluated. Trading opportunities may be missed.
  • Open positions remain open. If you have positions on the exchange, they persist on the exchange regardless of whether Botmarley is running.

When the engine resumes, it backfills missed candles from the exchange API. This means it fetches the candles that were produced during the downtime, appends them to the buffer, and processes them. However, the backfilled candles are processed in bulk -- triggers that might have fired at specific moments during the gap may not fire in the same way when processed after the fact.

Note

For short gaps (a few minutes), the impact is negligible. For longer gaps (hours), you may miss trading signals that occurred during the downtime. The server_restart event in the session log tells you exactly when the gap happened, so you can review the price action during that period manually.

The server_restart Event

Every recovered session gets a server_restart event recorded in its event log. This event:

  • Has a timestamp of when the server restarted (not when it shut down).
  • Can include metadata about the recovery process (e.g., number of candles backfilled).
  • Is visible in the session detail page's event history.

You can use this event to:

  • Audit gaps -- know exactly when the bot was offline.
  • Correlate missed opportunities -- compare the gap period's price action with what the strategy would have done.
  • Monitor stability -- frequent server_restart events may indicate infrastructure issues.

Edge Cases

What if the strategy TOML snapshot is invalid?

If the stored TOML snapshot cannot be parsed (e.g., due to a bug in a previous version), the session is marked as Failed instead of being recovered. An error is logged explaining the parse failure. You will need to manually create a new session with the corrected strategy.

What if the portfolio snapshot is missing or corrupt?

If the last_portfolio_snapshot field is empty or cannot be deserialized, the engine starts with a fresh portfolio using the session's initial capital. This means positions tracked in the previous run are lost. A warning is logged so you can investigate.

What about exchange orders placed just before shutdown?

If the engine sent an order to the exchange and the server crashed before recording the result, there may be a discrepancy between the exchange account's actual balance and what Botmarley thinks the balance is. After recovery, check your exchange account balance manually to confirm alignment.

Warning

If you suspect a state mismatch between Botmarley and your exchange account after a crash, stop the session, verify your exchange balance, and start a new session. Do not continue trading with potentially incorrect state.

Market Data Overview

Market data is the foundation of everything Botmarley does. Every backtest, every live trading session, and every chart you view depends on accurate, well-organized historical price data. This chapter explains what market data is, how Botmarley stores it, and how data flows through the system.

What Is Market Data?

Market data in Botmarley means OHLCV candles -- structured snapshots of price activity over a fixed time period. Each candle captures five values:

FieldMeaningExample
OpenThe price at the start of the period67,250.00
HighThe highest price during the period67,410.50
LowThe lowest price during the period67,180.00
CloseThe price at the end of the period67,390.25
VolumeThe total amount traded during the period42.37 BTC

A single candle tells you a story: where the price started, how far it moved in each direction, and where it ended up. Thousands of candles together reveal trends, patterns, and opportunities that strategies can exploit.

Note

Botmarley always fetches and stores 1-minute candles as the base resolution. Larger timeframes (5m, 15m, 1h) are derived automatically by aggregating the 1-minute data. This means you only need to download once, and all timeframes are available immediately.

How Botmarley Stores Data

Botmarley stores candle data in Apache Arrow format -- a columnar, binary file format designed for high-performance analytics. Each trading pair gets its own directory with .arrow files inside.

~/.botmarley/data/
├── XBTUSD/
│   └── 1m.arrow        ← all 1-minute candles for BTC/USD
├── ETHUSD/
│   └── 1m.arrow
├── SOLUSD/
│   └── 1m.arrow
└── ...

Why Apache Arrow?

You might wonder why Botmarley does not just use CSV or JSON files like many other tools. The answer comes down to performance and efficiency:

PropertyArrowCSVJSON
Read speedExtremely fast (memory-mapped)Slow (must parse every line)Slowest (must parse structure)
File sizeCompact binaryMedium (text)Large (text + keys)
Type safetyColumns are typed (f64, i64, timestamp)Everything is a stringMixed types
Column accessRead only the columns you needMust read entire rowsMust read entire objects
Ideal forTime-series data, analyticsSimple data exchangeAPI responses

Arrow's columnar layout means that when Botmarley needs to calculate an indicator like EMA (which only requires the "close" column), it can read just that column without touching open, high, low, or volume data. For a dataset with millions of candles, this makes a significant difference.

Tip

You never need to interact with Arrow files directly. Botmarley handles reading and writing through the web interface and its internal engine. The files are mentioned here so you understand what is on disk and why.

Data Flow

Here is how market data moves through the system, from the exchange to your screen:

flowchart LR
    Exchange["Exchange REST API<br/>(Kraken or Binance)"] -->|"Fetch 1m candles"| Download["History Sync<br/>(Task Queue)"]
    Download -->|"Write .arrow"| Disk["Arrow Files<br/>~/.botmarley/data/"]
    Disk -->|"Read"| Backtest["Backtest Engine"]
    Disk -->|"Read"| Browser["Data Browser<br/>& Chart Viewer"]
    Disk -->|"Read"| Indicators["Indicator<br/>Calculator"]
    Disk -->|"Seed history"| Live["Live Trading<br/>Engine"]
    Exchange -->|"WebSocket feed"| Live

    style Exchange fill:#5865f2,color:#fff
    style Disk fill:#f59e0b,color:#fff
  1. Fetch -- The History Sync page lets you download candles from either Kraken's or Binance's REST API. Botmarley fetches 1-minute candles going as far back as you configure.
  2. Store -- Downloaded candles are written to .arrow files on disk, organized by trading pair.
  3. Use -- Stored data is consumed by multiple parts of the system:
    • Backtest engine reads Arrow files to simulate strategy execution against historical prices.
    • Data browser renders candles in tables and interactive charts.
    • Indicator calculator reads price data and computes technical indicators.
    • Live trading engine loads recent history from Arrow files as a starting seed, then switches to a WebSocket feed for real-time data.

Data Freshness

When you download history for a pair, Botmarley records the last timestamp stored. The next time you sync that pair, it resumes from where it left off -- no duplicate downloads, no gaps. This incremental approach means:

  • First sync may take a while (months of 1-minute candles add up).
  • Subsequent syncs are fast -- only new candles since the last sync are fetched.

Warning

Botmarley does not automatically keep historical data up to date. You need to trigger a History Sync manually or set it up before running a backtest on recent data. Live trading sessions handle their own real-time data via WebSocket and do not depend on Arrow files being current.

Data Browser interface showing pair selection and candle chart

What's Next

Downloading History

Before you can backtest a strategy or browse historical charts, you need candle data on disk. The History Sync page lets you download 1-minute candles from Kraken or Binance and store them as Arrow files.

The History Page

Navigate to /history in the sidebar. This page shows:

  • A list of trading pairs available for download.
  • The current sync status for each pair (last synced timestamp, number of candles stored).
  • A Sync button to start downloading.

History Sync page showing exchange selection, pair list, and sync status

Step by Step: Downloading Candle Data

1. Select the Exchange

At the top of the History page, choose which exchange to fetch data from using the exchange radio buttons:

  • Kraken -- uses the Kraken REST API (/0/public/OHLC). Returns up to 720 candles per request.
  • Binance -- uses the Binance REST API (/api/v3/klines). Returns up to 1000 candles per request.

2. Select Trading Pairs

Choose which pairs you want to download data for. Note that each exchange uses its own pair naming convention:

ExchangePairDescription
KrakenXBTUSDBitcoin / US Dollar
KrakenETHUSDEthereum / US Dollar
KrakenSOLUSDSolana / US Dollar
KrakenXBTEURBitcoin / Euro
BinanceBTCUSDCBitcoin / USDC
BinanceETHUSDCEthereum / USDC
BinanceSOLUSDCSolana / USDC
BinanceBTCUSDTBitcoin / Tether

Tip

Only download pairs you actually plan to trade or backtest. Each pair consumes disk space and sync time. You can always add more pairs later.

Note

Botmarley converts the user-friendly pair format internally. For example, "BTC/USD" becomes "XXBTZUSD" for Kraken and "BTC/USDC" becomes "BTCUSDC" for Binance. The History page handles this conversion automatically.

3. Click Sync

Click the Sync button next to a pair. This creates a HistorySync task in the Task Queue and begins downloading in the background.

4. Monitor Progress

The sync runs as a background task. You can monitor it in two ways:

  • History page -- the status updates with a progress indicator showing how many candles have been fetched so far.
  • Tasks page (/tasks) -- the HistorySync task shows its current state (Pending, Running, Completed, or Failed).

The page auto-refreshes, so you do not need to manually reload.

What Gets Downloaded

Botmarley fetches 1-minute candles from the selected exchange's REST API. For Kraken, each API call returns up to 720 candles (12 hours of 1-minute data). For Binance, each call returns up to 1000 candles (roughly 16.5 hours). For a full history going back several months, Botmarley makes many sequential requests, paging backward through time.

Derived Timeframes

Once 1-minute data is on disk, Botmarley can compute larger timeframes automatically by aggregating the base candles:

Derived TimeframeHow It Is Built
5mEvery 5 consecutive 1m candles are merged
15mEvery 15 consecutive 1m candles are merged
1hEvery 60 consecutive 1m candles are merged
flowchart LR
    A["1m candles<br/>(stored on disk)"] --> B["5m<br/>(aggregated)"]
    A --> C["15m<br/>(aggregated)"]
    A --> D["1h<br/>(aggregated)"]

    style A fill:#5865f2,color:#fff

The aggregation computes each derived candle as follows:

  • Open = open of the first 1m candle in the group.
  • High = maximum high across all 1m candles in the group.
  • Low = minimum low across all 1m candles in the group.
  • Close = close of the last 1m candle in the group.
  • Volume = sum of volumes across all 1m candles in the group.

Note

Derived timeframes are computed on the fly when needed -- they are not stored as separate files. This keeps storage requirements low while still supporting multi-timeframe strategies and analysis.

Storage Location

Downloaded candle data is stored in your Botmarley data directory:

~/.botmarley/data/
├── XBTUSD/
│   └── 1m.arrow
├── ETHUSD/
│   └── 1m.arrow
└── ...

The default base path is ~/.botmarley/data/. You can change this in Settings under the Data section.

Setting a Custom Start Date

By default, Botmarley downloads as much history as the exchange provides (which can go back several years for major pairs). If you want to limit the download to a specific date range:

  1. Go to Settings (/settings).
  2. Under Data Settings, find the History Start Date field.
  3. Enter the earliest date you want data for (e.g., 2024-01-01).
  4. Save settings.

The next time you run a History Sync, Botmarley will only fetch candles from that date onward. Candles already downloaded before that date are not deleted -- they remain on disk.

Incremental Downloads

Botmarley tracks the latest candle timestamp for each pair. When you click Sync again:

  • If data exists on disk, only candles after the last stored timestamp are fetched.
  • If no data exists, a full download starts from the configured start date (or from the earliest available on the exchange).

This makes subsequent syncs fast -- typically just a few seconds to catch up to the present.

Tips for Effective Data Management

Tip

Download at least 90 days of data for any pair you plan to backtest. Most indicators need a warm-up period (e.g., a 200-period EMA needs at least 200 candles before producing meaningful values), and short data ranges can lead to misleading backtest results.

Warning

Both Kraken and Binance rate-limit API requests. If you are downloading data for many pairs simultaneously, some sync tasks may take longer or temporarily pause while respecting rate limits. Botmarley handles this automatically -- just let the tasks run.

  • Disk space: 1-minute candles for one year of a major pair like XBTUSD or BTCUSDC typically use around 50-100 MB in Arrow format. This is very modest by modern standards.
  • Network: First syncs for pairs with years of history may take 10-30 minutes depending on your connection and the exchange's rate limits.
  • Freshness: Remember to re-sync before running backtests if you want results that include recent market activity.

Troubleshooting

ProblemSolution
Sync task stays "Pending"Check that the task worker is running. See Task Queue.
Sync fails with "Rate limit"Wait a few minutes and retry. Both Kraken and Binance impose request limits.
No data for a pairVerify the pair name matches the exchange's naming convention. Kraken uses names like XBTUSD (not BTCUSD). Binance uses names like BTCUSDC (not BTC/USDC).
Very slow first syncThis is normal for pairs with years of history. Let it run to completion.

Data Browser

The Data Browser lets you explore the candle data stored on disk. You can view raw OHLCV values in a table, render interactive charts, and inspect individual candles -- all from the web interface.

Accessing the Data Browser

Navigate to /data in the sidebar. The Data Browser page presents two main controls at the top:

  • Pair selector -- choose which trading pair to view (e.g., XBTUSD, ETHUSD from Kraken, or BTCUSDC, ETHUSDC from Binance).
  • Timeframe selector -- choose the candle resolution (1m, 5m, 15m, 1h).

Data Browser with pair selection, candlestick chart, and candle data table

Note

You can only browse pairs that have been downloaded. If a pair does not appear in the selector, go to Downloading History and run a History Sync first.

Interactive Chart Viewer

The chart viewer uses Lightweight Charts (the same library used by TradingView) to render a professional candlestick chart. The chart supports:

  • Pan and zoom -- click and drag to pan, scroll to zoom in/out on the time axis.
  • Crosshair -- hover over any candle to see its exact OHLCV values in the legend area.
  • Auto-fit -- the chart automatically scales the price axis to fit visible candles.
  • Time navigation -- scroll left to see older data, right for more recent data.

The chart loads candle data directly from the server and renders it in the browser using vanilla JavaScript. No external services are contacted.

Tip

Use the scroll wheel to zoom into specific time ranges. This is especially useful when looking at 1-minute candles, where the full dataset may span months of data.

Candle Table

Below the chart, a table displays the raw candle data for the selected pair and timeframe. Each row represents one candle:

ColumnDescription
TimestampThe start time of the candle (UTC)
OpenPrice at the start of the period
HighHighest price during the period
LowLowest price during the period
ClosePrice at the end of the period
VolumeTotal traded volume during the period

The table is sorted by timestamp in descending order (newest first), so the most recent candles appear at the top.

Switching Timeframes

When you switch from one timeframe to another (e.g., from 1m to 1h), the chart and table reload with the new data. Remember that all timeframes larger than 1m are derived from the stored 1-minute candles:

flowchart LR
    Store["1m.arrow on disk"] --> Derive["Server aggregation"]
    Derive --> TF5["5m candles"]
    Derive --> TF15["15m candles"]
    Derive --> TF60["1h candles"]
    TF5 --> Render["Chart + Table"]
    TF15 --> Render
    TF60 --> Render

    style Store fill:#f59e0b,color:#fff
    style Render fill:#22c55e,color:#fff

Switching timeframes does not require a new download -- the server reads the 1-minute Arrow file and computes the requested timeframe on the fly.

Practical Uses

The Data Browser is useful for several tasks:

  • Visual verification after downloading history -- confirm that the data looks correct, with no obvious gaps or anomalies.
  • Manual chart analysis -- look at price patterns, support/resistance levels, or significant moves before building a strategy.
  • Debugging strategies -- after a backtest, switch to the Data Browser to see exactly what the candles looked like during a period where the strategy made unexpected trades.
  • Comparing timeframes -- look at the same time period in 1m and 1h to understand how aggregation affects candle shapes and indicator values.

Warning

Very large datasets (years of 1-minute data) may take a moment to render in the chart. If performance is sluggish, switch to a larger timeframe like 15m or 1h for a smoother experience.

Indicator Calculator

The Indicator Calculator lets you compute technical indicators on your stored candle data and visualize them overlaid on the price chart. This is useful for exploring how an indicator behaves on a specific pair and timeframe before using it in a strategy.

What It Does

The calculator takes your stored candle data, applies a technical indicator formula, and displays the resulting values both as a line on the chart and in a data table. You can experiment with different indicator types and parameters without writing any strategy code.

Using the Calculator

Step 1: Select Your Data

Choose the inputs for the calculation:

SettingDescriptionExample
PairThe trading pair to analyzeXBTUSD, BTCUSDC, etc.
TimeframeThe candle resolution to use1h
Date rangeThe time window to calculate over2024-06-01 to 2024-09-01

Note

Make sure you have downloaded data for the selected pair and date range. The calculator works on locally stored Arrow files -- it does not fetch data from the exchange.

Step 2: Choose an Indicator

Select the indicator type and configure its parameters:

IndicatorParametersDescription
SMA (Simple Moving Average)Period (e.g., 20)Average of the last N closing prices
EMA (Exponential Moving Average)Period (e.g., 12, 26)Weighted average giving more weight to recent prices
RSI (Relative Strength Index)Period (e.g., 14)Momentum oscillator measuring overbought/oversold conditions
Bollinger BandsPeriod (e.g., 20), Std Dev (e.g., 2.0)Price envelope based on standard deviations from SMA
MACDFast (12), Slow (26), Signal (9)Trend-following momentum indicator using EMA differences

Tip

If you are new to indicators, start with SMA(20) or EMA(20) -- they are the simplest to interpret. The line follows the price trend, smoothing out noise. When price crosses above the line, it may signal an uptrend; when it crosses below, a downtrend.

Step 3: View the Results

After clicking calculate, the results appear in two forms:

Chart Overlay

The indicator is drawn directly on the price chart (or in a separate pane below the chart for oscillators like RSI and MACD):

  • SMA and EMA appear as lines overlaid on the candlestick chart.
  • Bollinger Bands appear as three lines (upper band, middle SMA, lower band) with the area between them shaded.
  • RSI appears in a separate pane below the price chart with horizontal reference lines at 30 and 70.
  • MACD appears in a separate pane with the MACD line, signal line, and histogram bars.

Data Table

A table shows the calculated indicator values alongside the candle data:

TimestampCloseSMA(20)
2024-08-15 14:0058,420.5058,195.30
2024-08-15 15:0058,510.0058,210.75
.........

Note

Indicators require a warm-up period before producing meaningful values. For example, SMA(20) needs at least 20 candles of data before it can output its first value. The first N-1 rows will show blank or null values for the indicator -- this is expected behavior, not a bug.

Practical Examples

Exploring EMA Crossovers

To see how a golden cross / death cross strategy might work:

  1. Select a pair and timeframe (e.g., XBTUSD 1h).
  2. Add EMA(12) -- this is the fast line.
  3. Add EMA(26) -- this is the slow line.
  4. Look for points where the fast EMA crosses above the slow EMA (potential buy signals) or below it (potential sell signals).

Finding RSI Extremes

To identify oversold and overbought zones:

  1. Select a pair and timeframe.
  2. Add RSI(14).
  3. Look for periods where RSI drops below 30 (oversold -- potential buy opportunity) or rises above 70 (overbought -- potential sell opportunity).

Evaluating Bollinger Band Squeezes

To spot potential breakout setups:

  1. Select a pair and timeframe.
  2. Add Bollinger Bands(20, 2.0).
  3. Look for periods where the bands narrow significantly (a "squeeze"), which often precedes a large price move.

Tip

The indicator calculator is a sandbox for experimentation. Use it to build intuition about how indicators respond to different market conditions before committing those indicators to a strategy TOML file.

Settings

The Settings page (/settings) lets you configure application-wide options for Botmarley.

Accessing Settings

Navigate to Settings in the sidebar or go directly to http://localhost:3000/settings.

Configuration File

Botmarley stores its settings in a TOML configuration file. The location is determined by the BOTMARLEY_HOME environment variable, defaulting to ~/.botmarley/.

The primary settings file is settings.toml inside the Botmarley home directory.

Server Settings

SettingDefaultDescription
server.host127.0.0.1IP address to bind to
server.port3000Port to listen on

Note

Server settings require a restart to take effect. They can also be overridden with HOST and PORT environment variables.

Telegram Settings

Configure Telegram bot notifications for trade alerts:

SettingDefaultDescription
telegram.enabledfalseEnable/disable Telegram bot
telegram.bot_token""Bot token from @BotFather
telegram.allowed_username""Your Telegram username (for access control)

See Telegram Notifications for setup instructions.

Data Settings

SettingDescription
data.directoryPath where Arrow data files are stored
data.extra_candlesAdditional timeframes to compute from 1-minute data

Editing Settings via UI

The Settings page provides a form-based interface for editing common settings. Changes are saved to the configuration file when you click Save.

Settings page showing configuration options for home directory, Telegram, database, and API keys

Editing Settings Manually

You can also edit settings.toml directly:

[server]
host = "0.0.0.0"
port = 3000

[telegram]
enabled = true
bot_token = "123456:ABC-DEF..."
allowed_username = "your_username"

Warning

If you edit settings manually while the server is running, you'll need to restart for changes to take effect.

Task Queue

Botmarley uses a background task queue to handle long-running operations without blocking the web interface. The Tasks page (/tasks) lets you monitor these background jobs.

How It Works

sequenceDiagram
    participant U as User / Scheduler
    participant Q as Task Queue (PostgreSQL)
    participant W as Task Worker
    participant E as Executor

    U->>Q: Enqueue task
    W->>Q: Poll for pending tasks
    Q->>W: Return next task
    W->>E: Execute task
    E->>Q: Update status (completed/failed)
  1. Enqueue — user actions or scheduled jobs create tasks in the PostgreSQL queue
  2. Worker — a background worker polls for pending tasks
  3. Execute — the task executor runs the appropriate handler
  4. Complete — status is updated to completed or failed

Task Types

TypeDescriptionTriggered By
HistorySyncDownload candle data from KrakenUser (Data page)
BacktestRun strategy against historical dataUser (Backtest page)
BulkBacktestRun multiple backtestsUser (Bulk backtest)
PortfolioSyncSync account balances and pricesScheduler (hourly) or User
IndicatorCalcCalculate indicators on a datasetUser (Data page)

Task Statuses

StatusMeaning
PendingWaiting in queue
RunningCurrently being executed
CompletedFinished successfully
FailedEncountered an error

Task Priority

Tasks have priority levels that determine execution order:

PriorityUsed For
HighUser-initiated actions (backtests, data downloads)
NormalScheduled background jobs
LowMaintenance tasks

Higher-priority tasks are picked up first by the worker.

Viewing Tasks

The Tasks page shows:

  • Active tasks — currently running jobs with progress indicators
  • Pending tasks — jobs waiting in the queue
  • Recent completed — last finished tasks with results
  • Failed tasks — jobs that encountered errors, with error details

Task queue interface showing pending, completed, and failed tasks

Task Recovery

If the server crashes or restarts, stale tasks (stuck in "Running" status) are automatically recovered on startup. Tasks that have been running for more than 1 hour are reset to "Pending" so the worker can retry them.

Tip

If a task is stuck, restarting the server will automatically recover it. You can also check the Activity Logs for error details.

Retry Behavior

Failed tasks are retried automatically based on their max_retries setting (typically 3 attempts). Each retry includes an exponential backoff delay.

Activity Logs

The Activity Logs page (/logs) provides a chronological record of important events across the system.

What Gets Logged

Botmarley logs significant events in the following categories:

CategoryExamples
TradingSession started, paused, stopped; actions executed (buy/sell)
BacktestBacktest started, completed, failed
AccountAccount created, verified, balance synced
PortfolioPortfolio sync triggered, completed
DataHistory sync started, completed
SystemServer started, task recovered, errors

Log Levels

LevelColorMeaning
InfoBlueNormal operations (session started, sync completed)
WarningAmberNon-critical issues (stale task recovered, API rate limit)
ErrorRedFailures requiring attention (API error, task failed)

Viewing Logs

The logs page shows the most recent events with:

  • Timestamp — when the event occurred
  • Category — which system area generated the log
  • Event — what happened
  • Level — severity indicator
  • Details — additional context (optional)

Filtering

You can filter logs by:

  • Category — show only trading, backtest, or system events
  • Level — filter by info, warning, or error
  • Time range — view logs from a specific period

Activity logs page with filtering controls for log level and category

Log Storage

Activity logs are stored in PostgreSQL and persist across server restarts. They are separate from the structured tracing logs that go to stdout.

Tip

Activity logs are designed for user-facing event tracking. For debugging server issues, check the terminal output where structured tracing logs provide more technical detail.

Common Log Events

Trading Session Logs

[INFO]  trading  session_started   Session "BTC Scalper" started on XBTUSDT
[INFO]  trading  action_executed   BUY 0.001 BTC @ $67,500.00
[INFO]  trading  session_paused    Session "BTC Scalper" paused by user
[INFO]  trading  session_stopped   Session "BTC Scalper" stopped - PnL: +$45.20 (+2.3%)

Error Logs

[ERROR] trading  engine_error      Failed to fetch candles: API timeout
[ERROR] account  sync_failed       Account verification failed: invalid API key
[WARN]  system   task_recovered    Recovered 2 stale tasks from previous crash

Authentication

Botmarley supports optional password protection to secure access to the web interface.

How It Works

Authentication in Botmarley is simple and file-based:

  1. If a .password file exists in the Botmarley home directory, authentication is enabled
  2. All page routes require a valid session cookie
  3. Public routes (login page, static files, /book) remain accessible
  4. Sessions are stored as secure cookies

Setting Up Password Protection

Via the Settings Page

  1. Navigate to Settings (/settings)
  2. Enter your desired password in the password field
  3. Click Save
  4. The .password file is created automatically
  5. You'll be redirected to the login page

The password is securely hashed before storage. When setting it through the Settings page, this is handled automatically.

Logging In

When authentication is enabled:

  1. Visit any page — you'll be redirected to /login
  2. Enter your password
  3. Click Login
  4. A session cookie is set and you're redirected to the dashboard

Logging Out

Click the Logout button in the sidebar footer, or navigate to /logout.

Disabling Authentication

To remove password protection:

  1. Delete the .password file from the Botmarley home directory
  2. Restart the server
  3. All pages become accessible without login
rm ~/.botmarley/.password

Security Notes

Danger

Botmarley is designed as a single-tenant, locally-run application. If you expose it to the internet (e.g., for remote access), always enable password protection and consider using HTTPS via a reverse proxy like Caddy or nginx.

  • Sessions expire after a configurable timeout
  • The cookie is marked HttpOnly and SameSite=Strict
  • Failed login attempts are logged
  • There is no username — only a single password protects the entire instance

Telegram Notifications

Botmarley can send trade notifications to your Telegram account, keeping you informed of trading actions even when you're away from the dashboard.

Setup

1. Create a Telegram Bot

  1. Open Telegram and search for @BotFather
  2. Send /newbot and follow the prompts
  3. Choose a name (e.g., "My Botmarley Alerts")
  4. Copy the bot token (looks like 123456789:ABCdefGHI...)

2. Get Your Username

Your Telegram username (the one starting with @) is used for access control. Only messages from this username will be accepted by the bot.

3. Configure in Botmarley

Navigate to Settings (/settings) and fill in:

FieldValue
EnabledCheck the box
Bot TokenPaste the token from BotFather
Allowed UsernameYour Telegram username (without @)

Click Save.

4. Start the Bot

Open a chat with your new bot in Telegram and send /start. The bot needs this initial message to establish the chat.

What Gets Notified

The Telegram bot sends notifications for trade actions during live trading sessions:

EventExample Message
Buy"BUY 0.001 BTC @ $67,500.00 (Session: BTC Scalper)"
Sell"SELL 0.001 BTC @ $69,200.00 — PnL: +$1.70 (+2.5%)"
Session Started"Trading session 'BTC Scalper' started on XBTUSDT"
Session Stopped"Session 'BTC Scalper' stopped — Total PnL: +$45.20"

Tip

Notifications are sent in real-time as actions execute. They use the same broadcast channel as the web interface's SSE updates.

Architecture

graph LR
    E[Trading Engine] -->|ActionNotification| B[Broadcast Channel]
    B --> T[Telegram Bot]
    B --> S[SSE Stream]
    T --> TG[Telegram API]
    S --> W[Web Browser]

The Telegram bot subscribes to the same ActionNotification broadcast channel that powers the web interface's real-time updates.

Troubleshooting

Bot Not Sending Messages

  1. Check Settings — verify the bot token and username are correct
  2. Send /start — you must initiate the chat first
  3. Check Logs — look for Telegram errors in the Activity Logs or terminal output
  4. Restart Server — the Telegram bot spawns at startup; changes require a restart

Bot Token Invalid

If you see "Telegram bot failed to start" in the logs:

  • Verify the token with BotFather
  • Make sure you copied the full token including the colon

Warning

Keep your bot token secret. Anyone with the token can control your bot. If compromised, use BotFather's /revoke command to generate a new token.

Running Locally

This guide covers running Botmarley on your local machine for day-to-day use.

Prerequisites

ToolPurpose
DockerRuns PostgreSQL in a container
Botmarley binaryDownloaded from GitHub Releases

See Installation for detailed setup instructions.

Starting Up

1. Start PostgreSQL

docker compose up -d postgres

This starts PostgreSQL on localhost:5433 with:

  • Database: botmarley
  • User: botmarley
  • Password: botmarley_dev

2. Start Botmarley

./server

The server will be available at http://localhost:3000.

The database schema is initialized automatically on first run. Tables are created if they do not already exist.

3. Stop Botmarley

Press Ctrl+C in the terminal to stop the server.

Configuration

Botmarley can be configured through environment variables or a settings.toml file. Environment variables take precedence.

Common Options

# Change the server port
PORT=8080 ./server

# Use a custom database
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb ./server

# Increase log verbosity
RUST_LOG=debug ./server

See Environment Variables for the full list of options.

Database Management

To reset the database and start fresh:

docker compose down -v
docker compose up -d postgres

Warning

This destroys all data including accounts, trading sessions, and backtest results. Use with caution.

To stop PostgreSQL without deleting data:

docker compose down

Your data persists in data/postgres/ on disk and will be available when you start the container again.

Production Deployment

This guide covers deploying Botmarley to a production server.

Download the Release

Download the latest ARM64 release from GitHub:

curl -LO https://github.com/mi4uu/botmarley/releases/latest/download/botmarley-arm64.tar.gz

For x86_64 servers, use botmarley-amd64.tar.gz instead.

Extract and Install

# Extract the archive
tar -xzf botmarley-arm64.tar.gz

# Create installation directory
sudo mkdir -p /opt/botmarley

# Copy files
sudo cp server /opt/botmarley/
sudo cp -r templates/ /opt/botmarley/
sudo cp -r static/ /opt/botmarley/
sudo cp -r strats/ /opt/botmarley/

Required Files

These files must be present in the installation directory:

PathPurpose
serverServer binary
templates/Web interface templates
static/CSS, JS, static assets
strats/Strategy TOML files

PostgreSQL Setup

Install PostgreSQL 17 on your server:

# Ubuntu/Debian
sudo apt install postgresql-17

# Create database and user
sudo -u postgres psql -c "CREATE USER botmarley_prod WITH PASSWORD 'your-secure-password';"
sudo -u postgres psql -c "CREATE DATABASE botmarley_prod OWNER botmarley_prod;"

systemd Service

Create a systemd unit file at /etc/systemd/system/botmarley.service:

[Unit]
Description=Botmarley Trading Bot
After=network.target postgresql.service
Wants=postgresql.service

[Service]
Type=simple
User=botmarley
Group=botmarley
WorkingDirectory=/opt/botmarley
ExecStart=/opt/botmarley/server
Restart=always
RestartSec=5

# Environment
Environment=DATABASE_URL=postgresql://botmarley_prod:your-secure-password@127.0.0.1:5432/botmarley_prod
Environment=HOST=0.0.0.0
Environment=PORT=3000
Environment=RUST_LOG=info

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl daemon-reload
sudo systemctl enable botmarley
sudo systemctl start botmarley

Reverse Proxy (HTTPS)

For HTTPS, use Caddy as a reverse proxy:

# /etc/caddy/Caddyfile
your-domain.com {
    reverse_proxy localhost:3000
}
sudo systemctl restart caddy

Caddy automatically obtains and renews TLS certificates via Let's Encrypt.

Tip

Always enable password protection when exposing Botmarley to the internet.

Monitoring

Check Status

sudo systemctl status botmarley

View Logs

sudo journalctl -u botmarley -f

Health Check

curl http://localhost:3000/

Updating

To update Botmarley to a new release:

# 1. Download the latest release
curl -LO https://github.com/mi4uu/botmarley/releases/latest/download/botmarley-arm64.tar.gz

# 2. Extract
tar -xzf botmarley-arm64.tar.gz

# 3. Copy new files to production
sudo cp server /opt/botmarley/
sudo cp -r templates/ /opt/botmarley/
sudo cp -r static/ /opt/botmarley/

# 4. Restart the service
sudo systemctl restart botmarley

Warning

Active trading sessions are automatically recovered after a restart. The server records a server_restart event and re-spawns all running/paused sessions. There may be a brief gap in candle data during the restart window.

Environment Variables

Botmarley can be configured using environment variables, which take precedence over settings file values.

Core Variables

VariableDefaultDescription
DATABASE_URLFrom settings.tomlPostgreSQL connection string
HOST127.0.0.1Server bind address
PORT3000Server listen port
BOTMARLEY_HOME~/.botmarleyConfiguration and data directory
RUST_LOGinfoControls log verbosity (trace, debug, info, warn, error)

Database URL Format

postgresql://username:password@host:port/database

Examples:

# Local development
DATABASE_URL=postgresql://botmarley:botmarley_dev@localhost:5432/botmarley

# Production
DATABASE_URL=postgresql://botmarley_prod:secure-password@127.0.0.1:5432/botmarley_prod

Setting Variables

In systemd

Add Environment= lines to your service file:

[Service]
Environment=DATABASE_URL=postgresql://user:pass@localhost:5432/db
Environment=HOST=0.0.0.0
Environment=PORT=3000

In Shell

export DATABASE_URL=postgresql://botmarley:botmarley_dev@localhost:5432/botmarley
export PORT=3000
./server

In Docker

docker run -e DATABASE_URL=postgresql://... -e PORT=3000 botmarley

Precedence

Environment variables override values from settings.toml:

Environment Variable > settings.toml > Default Value

Tip

Use environment variables for deployment-specific settings (database URLs, ports) and the settings file for application preferences (Telegram config, data paths).

Multi-Instance Deployment

To run multiple Botmarley instances on the same server, use different ports and databases:

# Instance 1
PORT=3000 DATABASE_URL=postgresql://...db1 ./server

# Instance 2
PORT=3001 DATABASE_URL=postgresql://...db2 ./server

Each instance operates independently with its own database, accounts, and trading sessions.

Strategy TOML Reference

This is the complete reference for the Botmarley strategy TOML format. Every field, type, and valid value is documented here.

Structure Overview

[meta]
name = "Strategy Name"
description = "Optional description"
max_open_positions = 5

[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

A strategy consists of:

  1. [meta] — metadata and global settings
  2. [[actions]] — one or more trading actions, each with triggers

[meta] Section

FieldTypeRequiredDefaultDescription
nameStringYesStrategy name (1–255 characters, must be unique)
descriptionStringNo""Human-readable description
max_open_positionsIntegerNoUnlimitedMaximum concurrent open positions (must be >= 1)
[meta]
name = "BTC_DCA_Conservative"
description = "RSI oversold entry with DCA on dips, trailing stop exit"
max_open_positions = 3

[[actions]] Section

Each action defines what to do and when (via triggers).

FieldTypeRequiredDefaultDescription
typeStringYesAction type: open_long, buy, or sell
amountStringYesTrade amount (see format below)
average_priceBooleanNofalseEnable DCA averaging (open_long and buy only)

Action Types

TypePurposeNotes
open_longOpen a new long positionInitial entry
buyAdditional buy within positionDCA / averaging
sellExit position (full or partial)Close trade

Amount Format

Currency amount:

amount = "100 USDC"
amount = "50.5 BTC"
amount = "200 USD"

Percentage:

amount = "50%"      # 50% of position
amount = "100%"     # Full exit

Warning

Negative amounts are rejected. Percentage must be between 0 and 100.


Triggers

Each action requires at least one trigger. All triggers within an action use AND logic — all must be true simultaneously for the action to execute.

Three Trigger Categories

  1. Technical Indicator — compare indicator values
  2. Price Change — react to price movements
  3. Next Candle — delay execution

Technical Indicator Triggers

Compare a technical indicator against a target value or another indicator.

[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"
timeframe = "1h"
max_count = 5
FieldTypeRequiredDescription
indicatorStringYesIndicator name with period (e.g., rsi_14, ema_20)
operatorStringYesComparison: >, <, =, cross_above, cross_below
targetStringYesNumeric value or another indicator
timeframeStringNoEvaluation timeframe (1m, 5m, 15m, 1h, 4h, 1d)
max_countIntegerNoMaximum times this trigger fires per position

Operators

OperatorMeaning
>Indicator is greater than target
<Indicator is less than target
=Indicator equals target
cross_aboveIndicator crosses above target (was below, now above)
cross_belowIndicator crosses below target (was above, now below)

Target Values

Numeric:

target = "30"       # RSI value
target = "67500.0"  # Price level

Another indicator:

target = "sma_50"   # Compare EMA to SMA
target = "bb_upper" # Compare to Bollinger upper band

Price Change Triggers

React to price movements relative to position entry or market-wide.

pos_price_change

Fires when position P&L reaches a threshold.

[[actions.triggers]]
type = "pos_price_change"
value = "+3%"       # Take profit at +3%
max_count = 1

pos_price_change_follow

Trailing trigger — tracks the peak price since entry, fires on retracement.

[[actions.triggers]]
type = "pos_price_change_follow"
value = "+3.0%"
tolerance = "-0.2%"

price_change

Market-wide price change (not position-relative).

[[actions.triggers]]
type = "price_change"
value = "-5%"
timeframe = "1h"

trailing_stop

Follows price up, triggers on drawdown from peak.

[[actions.triggers]]
type = "trailing_stop"
value = "-3%"

Price Change Fields

FieldTypeRequiredDescription
typeStringYesTrigger type (see variants above)
valueStringYesPercentage threshold (e.g., "+3%", "-5%")
toleranceStringNoRetrace tolerance (pos_price_change_follow only)
timeframeStringNoFor price_change and consecutive_candles
max_countIntegerNoMaximum fires per position

Next Candle Trigger

Delays action execution until the next candle closes.

[[actions.triggers]]
type = "next_candle"
timeframe = "1h"
FieldTypeRequiredDescription
typeStringYesMust be "next_candle"
timeframeStringNoCandle timeframe to wait for
max_countIntegerNoMaximum fires per position

Indicators Reference

FormatNameNotes
rsi_{period}Relative Strength Indexe.g., rsi_14
sma_{period}Simple Moving Averagee.g., sma_50
ema_{period}Exponential Moving Averagee.g., ema_20
bb_lower, bb_upper, bb_midBollinger BandsBand component
macd_line, macd_signal, macd_histMACDComponent
stoch_rsi_{period}Stochastic RSIe.g., stoch_rsi_14
roc_{period}Rate of Changee.g., roc_10
atr_{period}Average True Rangee.g., atr_14
obvOn Balance VolumeNo parameters
obv_sma_{period}OBV with SMAe.g., obv_sma_20
vol_sma_{period}Volume SMAe.g., vol_sma_20
ttm_trendTTM TrendAlternative: ttmtrend
priceCurrent PriceSpecial: market price

Period range: 1–200 (inclusive). Values outside this range cause a validation error.


Valid Timeframes

ValueMeaning
1m1 minute
5m5 minutes
15m15 minutes
1h1 hour
4h4 hours
1d1 day

Complete Example

[meta]
name = "Multi_Timeframe_DCA_v1"
description = "RSI oversold entry on 1h, DCA on dips, trailing stop exit"
max_open_positions = 3

# ENTRY: RSI < 30 on 1h timeframe
[[actions]]
type = "open_long"
amount = "100 USDC"
average_price = false

  [[actions.triggers]]
  indicator = "rsi_14"
  operator = "<"
  target = "30"
  timeframe = "1h"

# DCA: Buy more on -2% drop
[[actions]]
type = "buy"
amount = "100 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-2%"
  max_count = 1

# DCA: Buy more on -4% drop
[[actions]]
type = "buy"
amount = "200 USDC"
average_price = true

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-4%"
  max_count = 1

# TAKE PROFIT: +3% from average entry
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "3%"

# STOP LOSS: -5% from average entry
[[actions]]
type = "sell"
amount = "100%"

  [[actions.triggers]]
  type = "pos_price_change"
  value = "-5%"

Validation Errors

ErrorCause
REQUIREDMissing required field
INVALID_ACTION_TYPEType not open_long, buy, or sell
INVALID_AMOUNT_FORMATDoesn't match "NUMBER CURRENCY" or "NUMBER%"
INVALID_OPERATORUnknown operator
INVALID_INDICATORUnknown indicator format
INVALID_PERIODNon-numeric period
INVALID_PERIOD_RANGEPeriod outside 1–200
INVALID_BAND_TYPEBollinger band not lower/upper/mid
INVALID_MACD_COMPONENTMACD component not line/signal/hist
INVALID_TIMEFRAMETimeframe not in valid list

Indicators Reference

Complete reference table for all technical indicators supported by Botmarley.

Indicator Summary

IndicatorTOML FormatCategoryDefault PeriodSignal Range
SMAsma_{period}Trend20, 50, 200Price-level
EMAema_{period}Trend9, 12, 20, 26Price-level
RSIrsi_{period}Momentum140–100
MACDmacd_{component}Momentum12/26/9Oscillator
Bollinger Bandsbb_{band}Volatility20Price-level
Stochastic RSIstoch_rsi_{period}Momentum140–100
ATRatr_{period}Volatility14Positive
ROCroc_{period}Momentum10Percentage
OBVobvVolumeCumulative
OBV SMAobv_sma_{period}Volume20Cumulative
Volume SMAvol_sma_{period}Volume20Volume-level
TTM Trendttm_trendTrendBinary
RSTDrstd_{period}Statistical20Positive
Z-Scorezscore_{period}Statistical20-4 to +4
Percentile Rankprank_{period}Statistical500–100
Parkinsonparkinson_{period}Volatility20Positive
Garman-Klassgk_vol_{period}Volatility20Positive
Yang-Zhangyz_vol_{period}Volatility20Positive
Hursthurst_{period}Regime2000–1
Half-Lifehalflife_{period}Regime2001+ candles
Vol Regimevol_regime_{period}Regime1001 / 2 / 3
VWAPvwapInstitutionalPrice-level
VWAP Devvwap_dev_{period}Institutional20Percentage
Skewnessskew_{period}Distribution60-3 to +3
Kurtosiskurt_{period}Distribution60-2 to +10
Autocorrelationautocorr_{lag}Momentumlag 1-1 to +1

Trend Indicators

Simple Moving Average (SMA)

Format: sma_{period} (e.g., sma_50, sma_200)

The SMA calculates the arithmetic mean of the last N closing prices. It smooths out price data to identify trend direction.

Common uses:

  • sma_50 / sma_200 — Golden Cross / Death Cross signals
  • Price above SMA = uptrend, below = downtrend
[[actions.triggers]]
indicator = "ema_9"
operator = "cross_above"
target = "sma_21"

Exponential Moving Average (EMA)

Format: ema_{period} (e.g., ema_9, ema_20)

The EMA gives more weight to recent prices, making it more responsive to new information than the SMA.

Common uses:

  • Fast/slow EMA crossovers (e.g., EMA 9 crossing EMA 21)
  • Dynamic support/resistance levels
[[actions.triggers]]
indicator = "ema_12"
operator = "cross_above"
target = "ema_26"

TTM Trend

Format: ttm_trend or ttmtrend

A binary trend indicator. Returns a directional signal indicating whether the market is in an uptrend or downtrend.


Momentum Indicators

Relative Strength Index (RSI)

Format: rsi_{period} (e.g., rsi_14)

RSI measures the speed and magnitude of price changes on a scale of 0–100.

LevelInterpretation
< 30Oversold (potential buy)
> 70Overbought (potential sell)
30–70Neutral
# Buy when RSI drops below 30
[[actions.triggers]]
indicator = "rsi_14"
operator = "<"
target = "30"

MACD (Moving Average Convergence Divergence)

Components:

  • macd_line — MACD line (12-period EMA minus 26-period EMA)
  • macd_signal — Signal line (9-period EMA of MACD line)
  • macd_hist or macd_histogram — Histogram (MACD line minus signal)

Note

MACD periods (12, 26, 9) are hardcoded and cannot be customized.

# Buy when MACD crosses above signal
[[actions.triggers]]
indicator = "macd_line"
operator = "cross_above"
target = "macd_signal"

Stochastic RSI

Format: stoch_rsi_{period} (e.g., stoch_rsi_14)

Applies the Stochastic oscillator formula to RSI values instead of price. Ranges 0–100, more sensitive than standard RSI.

Rate of Change (ROC)

Format: roc_{period} (e.g., roc_10)

Measures the percentage change in price over a specified period. Positive values indicate upward momentum, negative values indicate downward.


Volatility Indicators

Bollinger Bands

Components:

  • bb_upper — Upper band (SMA + 2 standard deviations)
  • bb_mid or bb_middle — Middle band (20-period SMA)
  • bb_lower — Lower band (SMA - 2 standard deviations)
# Buy when price touches lower Bollinger Band
[[actions.triggers]]
indicator = "price"
operator = "<"
target = "bb_lower"

Average True Range (ATR)

Format: atr_{period} (e.g., atr_14)

Measures market volatility by calculating the average range of price bars. Higher ATR = more volatile market.


Volume Indicators

On Balance Volume (OBV)

Format: obv

Cumulative volume indicator — adds volume on up days, subtracts on down days. Helps confirm price trends.

OBV with SMA

Format: obv_sma_{period} (e.g., obv_sma_20)

Smoothed version of OBV using a Simple Moving Average.

Volume SMA

Format: vol_sma_{period} (e.g., vol_sma_20)

Simple Moving Average applied to trading volume. Useful for identifying volume breakouts.


Special Values

Price

Format: price

Represents the current market price (close of current candle). Use this to compare indicators against the actual price.

# EMA above current price = potential resistance
[[actions.triggers]]
indicator = "price"
operator = ">"
target = "ema_200"

Statistical Indicators

Rolling Standard Deviation (RSTD)

Format: rstd_{period} (e.g., rstd_20)

Measures the dispersion of closing prices around their mean over a rolling window. The raw building block of volatility measurement.

Z-Score

Format: zscore_{period} (e.g., zscore_20)

How many standard deviations the current price is from its rolling mean. Universal thresholds (-2 = oversold, +2 = overbought) work across all assets.

# Buy when price is 2 standard deviations below mean
[[actions.triggers]]
indicator = "zscore_20"
operator = "<"
target = "-2.0"

Percentile Rank

Format: prank_{period} (e.g., prank_50)

Where the current close sits within its rolling range, expressed as a percentile (0–100). 5 = bottom 5%, 95 = top 5%.


Advanced Volatility Estimators

Parkinson Volatility

Format: parkinson_{period} (e.g., parkinson_20)

Volatility estimated from high-low range. ~5x more efficient than close-to-close standard deviation.

Garman-Klass Volatility

Format: gk_vol_{period} (e.g., gk_vol_20)

Volatility from full OHLC data. ~8x more efficient than close-to-close estimators.

Yang-Zhang Volatility

Format: yz_vol_{period} (e.g., yz_vol_20)

The most comprehensive volatility estimator. Combines overnight (close-to-open) and intraday (open-to-close + high-low) components.


Regime Detection Indicators

Hurst Exponent

Format: hurst_{period} (e.g., hurst_200)

Measures market memory via R/S analysis. H > 0.55 = trending, H < 0.45 = mean-reverting, H ≈ 0.50 = random walk.

# Only trade momentum when market is trending
[[actions.triggers]]
indicator = "hurst_200"
operator = ">"
target = "0.55"

Half-Life

Format: halflife_{period} (e.g., halflife_200)

Estimates how many candles it takes for price to revert halfway to the mean. Lower = faster reversion. Used to assess if mean-reversion is tradeable.

Volatility Regime

Format: vol_regime_{period} (e.g., vol_regime_100)

Classifies current volatility into discrete regimes: 1 (low), 2 (normal), 3 (high). Self-calibrating across all assets.


Institutional / Fair Value Indicators

VWAP

Format: vwap

Volume Weighted Average Price — the institutional benchmark for fair value. Price below VWAP = cheap, above = expensive.

VWAP Deviation

Format: vwap_dev_{period} (e.g., vwap_dev_20)

Percentage deviation from VWAP. Negative = below fair value, positive = above. Quantifies the distance from institutional consensus price.


Distribution Intelligence

Skewness

Format: skew_{period} (e.g., skew_60)

Measures return distribution asymmetry. Positive = upside surprises likely, negative = crash risk elevated.

Kurtosis

Format: kurt_{period} (e.g., kurt_60)

Measures tail fatness (excess kurtosis). Low = predictable market, high = extreme moves likely. A pure risk indicator.


Momentum Persistence

Autocorrelation

Format: autocorr_{lag} (e.g., autocorr_1)

Measures how correlated current returns are with returns N candles ago. Positive = momentum persists, negative = reversals likely, near zero = random.

# Confirm momentum persistence for trend-following
[[actions.triggers]]
indicator = "autocorr_1"
operator = ">"
target = "0.1"

Period Constraints

Classic Indicators

  • Minimum period: 1
  • Maximum period: 200
  • Invalid period (0 or >200): validation error

Statistical Indicators

  • Minimum period: varies by indicator (5–50)
  • Maximum period: 500
  • See individual indicator pages for exact ranges

Tip

Shorter periods (e.g., EMA 9) react faster to price changes but generate more false signals. Longer periods (e.g., SMA 200) are smoother but lag behind price movements. Choose based on your strategy's timeframe and risk tolerance.

Glossary

Common terms used throughout Botmarley and cryptocurrency trading.


A

Action A trading command defined in a strategy: open_long (enter position), buy (DCA), or sell (exit).

Apache Arrow A columnar data format used by Botmarley to store historical candle data. Provides fast read/write performance for time-series data.

ATR (Average True Range) A volatility indicator that measures the average range between high and low prices over a period. Higher ATR means more volatile market.

Average Price / DCA Dollar-Cost Averaging — buying additional amounts at different price levels to reduce the average entry price of a position.


B

Backtest Running a strategy against historical market data to evaluate its performance without risking real funds.

Bollinger Bands A volatility indicator consisting of three lines: a middle SMA, an upper band (SMA + 2 standard deviations), and a lower band (SMA - 2 standard deviations).

Broadcast Channel An internal Rust channel that distributes trade action notifications to multiple consumers (web SSE, Telegram bot).


C

Candle (Candlestick) A data point representing price movement over a time period, containing: open, high, low, close, and volume.

Cross Above / Cross Below A signal that occurs when one value (e.g., fast EMA) moves from below to above (or above to below) another value (e.g., slow EMA).


D

DCA (Dollar-Cost Averaging) See Average Price.

Drawdown The peak-to-trough decline in portfolio or position value, usually expressed as a percentage.


E

EMA (Exponential Moving Average) A moving average that gives more weight to recent prices, making it more responsive than a Simple Moving Average.

Engine The TradingEngine — the core component that evaluates strategy triggers against live market data and executes actions.


H

HTMX A JavaScript library that allows server-driven HTML updates via HTML attributes (hx-get, hx-swap). Used by Botmarley for dynamic page updates without a JavaScript framework.


K

Kraken A cryptocurrency exchange supported by Botmarley for live trading and market data.


L

Lightweight Charts A charting library by TradingView used by Botmarley to render candlestick charts in the browser.

Lookback The number of historical candles considered when evaluating a trigger or indicator.


M

MACD (Moving Average Convergence Divergence) A momentum indicator showing the relationship between two EMAs (12-period and 26-period). Consists of a MACD line, signal line, and histogram.

max_count A trigger parameter that limits how many times the trigger can fire per position.

max_open_positions A strategy setting that caps the number of concurrent open positions.

MiniJinja A Rust template engine (similar to Jinja2) used by Botmarley to render HTML pages on the server.


O

OBV (On Balance Volume) A volume indicator that adds volume on up-days and subtracts on down-days, helping confirm price trends.

Open Position An active trade that has been entered but not yet closed.


P

Pair (Trading Pair) Two assets that can be traded against each other (e.g., XBTUSDT = Bitcoin vs Tether).

Paper Account A simulated account for testing strategies without real funds.

PnL (Profit and Loss) The net gain or loss from a trade or trading session, expressed as an absolute amount or percentage.

Position A trade — from entry (buy) to exit (sell). Can include multiple DCA entries.


R

ROC (Rate of Change) A momentum indicator measuring the percentage change in price over a specified period.

RSI (Relative Strength Index) A momentum oscillator (0–100) that measures the speed and change of price movements. Below 30 is considered oversold, above 70 is overbought.

Rule In Botmarley strategies, a rule is an action combined with its triggers.


S

Session (Trading Session) An instance of a strategy running against a specific trading pair on a specific account. Can be started, paused, resumed, or stopped.

SMA (Simple Moving Average) The arithmetic mean of closing prices over a specified period.

SSE (Server-Sent Events) A web technology for pushing real-time updates from server to browser over HTTP. Used by Botmarley for live trading updates.

Stochastic RSI A momentum indicator that applies the Stochastic formula to RSI values, providing a more sensitive oversold/overbought signal than standard RSI.

Strategy A set of trading rules defined in TOML format that tell Botmarley when to buy and sell.


T

Task Queue A background job system in Botmarley that handles long-running operations (backtests, data downloads, account syncs) without blocking the web interface.

Timeframe The duration of each candle: 1m (1 minute), 5m, 15m, 1h, 4h, 1d.

TOML Tom's Obvious Minimal Language — the configuration format used for defining strategies in Botmarley.

Trailing Stop An order type that follows the price in the profitable direction and triggers a sell when the price reverses by a specified percentage.

Trigger A condition that must be met for a strategy action to execute. Multiple triggers per action use AND logic.

TTM Trend A trend indicator that provides directional signals.


W

Win Rate The percentage of trades that were profitable. Calculated as (winning trades / total trades) x 100.