diff --git a/Cargo.lock b/Cargo.lock index dcd801e..8c877d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -543,6 +543,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", + "ta", ] [[package]] @@ -645,6 +646,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ta" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "609409d472a0a7d8d4dd9e19891bbdef546b9dce670c3057d0e02192dc541226" + [[package]] name = "tap" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index a585117..138f8f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,4 +14,5 @@ extism-pdk = "1.4" rust_decimal = "1.37.2" serde = "1.0.219" serde_json = "1.0.143" +ta = "0.5.0" exchange_outpost_abi = { git = "https://github.com/ExchangeOutpost/exchange-outpost-abi", tag = "0.1.1"} \ No newline at end of file diff --git a/README.md b/README.md index 3c14e42..974f40f 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,130 @@ -# Rust Function Template for ExchangeOutpost +# Bollinger Bands Mean Reversion Backtest -A baseline template for creating Rust-based financial functions that can be deployed and executed on [ExchangeOutpost](https://www.exchangeoutpost.com/). +A Rust-based Bollinger Bands mean reversion trading strategy backtest that compiles to WebAssembly for deployment on [ExchangeOutpost](https://www.exchangeoutpost.com/). ## Overview -This project provides a foundational structure for developing financial analysis functions in Rust that compile to WebAssembly (WASM) and integrate seamlessly with the ExchangeOutpost platform. It includes pre-built data structures for handling financial market data, candlestick charts, and decimal precision calculations. +This project implements a complete Bollinger Bands trading strategy backtest using Rust and the `ta` crate for technical analysis. The strategy follows a mean reversion approach, buying when prices touch the lower band (oversold) and selling when prices touch the upper band (overbought), with configurable stop-loss and take-profit levels. ## Features +- **Bollinger Bands Mean Reversion Strategy**: Classic mean reversion approach using Bollinger Bands +- **Comprehensive Backtesting**: Full trade execution simulation with profit/loss calculation +- **Risk Management**: Configurable stop-loss and take-profit levels - **WebAssembly Compilation**: Functions compile to WASM for cross-platform execution -- **Financial Data Structures**: Built-in support for candlestick data, ticker information, and market data -- **Decimal Precision**: Integrated `rust_decimal` for accurate financial calculations -- **Extism Plugin Framework**: Uses Extism PDK for plugin development and execution -- **Type Safety**: Strongly typed financial data structures with serde serialization support +- **Technical Analysis Integration**: Uses the `ta` crate for Bollinger Bands calculation +- **Type Safety**: Strongly typed trading structures with serde serialization support + +## Strategy Logic + +The Bollinger Bands mean reversion strategy operates on the following principles: + +### Entry Signals +- **Long Entry**: When price touches or falls below the lower Bollinger Band (oversold condition) +- **Short Entry**: When price touches or rises above the upper Bollinger Band (overbought condition) + +### Exit Conditions +- **Stop Loss**: Configurable percentage loss limit (e.g., 2%) +- **Take Profit**: Configurable percentage gain target (e.g., 4%) +- **Opposite Signal**: Position is closed when an opposite entry signal occurs + +### Risk Management +- Only one position is held at a time +- Fixed position size of 1.0 unit per trade +- Automatic position closure at the end of the backtest period ## Project Structure ``` src/ -├── lib.rs # Main entry point and plugin function, edit this file to implement your function -└── exchange_outpost/ # Contains financial data structures and utility functions, you should not edit this directory +├── lib.rs # Main strategy implementation and backtest logic +└── exchange_outpost/ # Financial data structures and utility functions +├── manifest.json # Function configuration and parameter schema +├── STRATEGY.md # Detailed strategy documentation +└── Cargo.toml # Dependencies including ta crate ``` -## Function Manifest +## Strategy Parameters -The `manifest.json` file is a configuration file that defines metadata and behavior for your function when deployed on ExchangeOutpost. This file is crucial for proper function registration and execution. +The backtest accepts the following configurable parameters: -### Manifest Structure +| Parameter | Type | Range | Default | Description | +|-----------|------|-------|---------|-------------| +| `period` | integer | 2-200 | 20 | Number of periods for moving average calculation | +| `multiplier` | number | 0.1-5.0 | 2.0 | Standard deviation multiplier for Bollinger Bands | +| `sl` | number | 0.001-0.5 | 0.02 | Stop loss percentage (0.02 = 2%) | +| `tp` | number | 0.001-1.0 | 0.04 | Take profit percentage (0.04 = 4%) | +| `usd_balance` | number | 1.0+ | 1000.0 | Amount in USD to allocate per trade | + +### Example Configuration ```json { - "function_name": "Your Function Name", - "description": "A brief description of what your function does", - "financial_data_keys": ["symbol1", "symbol2"], - "call_arguments_schema": { - "type": "object", - "properties": { - "parameter_name": { - "type": ["number", "string"], - "description": "Parameter description" - } - } - }, - "enforce_schemas": false + "period": 20, + "multiplier": 2.0, + "sl": 0.02, + "tp": 0.04, + "usd_balance": 1000.0 } ``` -### Manifest Fields +This configuration creates Bollinger Bands with: +- 20-period moving average +- 2 standard deviations for upper/lower bands +- 2% stop loss +- 4% take profit target +- $1000 USD allocation per trade -- **`function_name`**: The display name for your function in the ExchangeOutpost interface -- **`description`**: A clear description of what your function analyzes or computes -- **`financial_data_keys`**: An array of strings specifying what are the labels for the financial data your function requires. -- **`call_arguments_schema`**: A JSON schema defining the parameters your function accepts. This creates a user interface for function configuration -- **`enforce_schemas`**: Boolean flag that determines whether the system should validate input against the defined schemas. +## Backtest Output -### Example Configurations +The backtest returns comprehensive results in the following format: -**Simple Moving Average Function:** -```json -{ - "function_name": "Simple Moving Average", - "description": "Calculates simple moving average for specified periods", - "financial_data_keys": ["symbol_data"], - "call_arguments_schema": { - "type": "object", - "properties": { - "period": { - "type": ["integer", "string"], - "description": "Number of periods for moving average calculation", - "minimum": 1, - "maximum": 200, - "default": 20 - } - }, - "required": ["period"] - }, - "enforce_schemas": true +```rust +BacktestResult { + trades: Vec, // Detailed list of all executed trades + total_profit: f64, // Total profit/loss from all trades } ``` +### Trade Details +Each trade contains: +- `open_price`: Entry price +- `close_price`: Exit price +- `amount`: Position size (fixed at 1.0) +- `side`: Trade direction (LONG or SHORT) + ## Core Components -### FinData -The main data container that provides access to: -- **Ticker Data**: Market data for multiple trading symbols -- **Piped Data**: Additional data passed between functions -- **Call Arguments**: Runtime parameters for function execution +### Strategy Implementation +The main strategy logic is implemented in `src/lib.rs` and includes: -### Candle -Represents financial candlestick data with: -- Timestamp (Unix epoch) -- OHLCV data (Open, High, Low, Close, Volume) -- Support for both `f64` and `Decimal` precision +- **BollingerBands**: Technical indicator from the `ta` crate +- **OpenTrade**: Struct representing an active position +- **ClosedTrade**: Struct representing a completed trade +- **BacktestResult**: Final output containing all trades and total profit + +### Data Structures + +```rust +struct OpenTrade { + pub open_price: f64, // Entry price + pub amount: f64, // Position size + pub side: Side, // LONG or SHORT +} + +struct ClosedTrade { + pub open_price: f64, // Entry price + pub close_price: f64, // Exit price + pub amount: f64, // Position size + pub side: Side, // LONG or SHORT +} + +struct BacktestResult { + pub trades: Vec, // All executed trades + pub total_profit: f64, // Total P&L +} +``` ## Getting Started @@ -105,8 +137,8 @@ Represents financial candlestick data with: 1. Clone this repository: ```bash -git clone https://github.com/ExchangeOutpost/rust-function-template.git -cd rust-function-template +git clone https://github.com/AlessandroRuggiero/bollinger-bands-backtest.git +cd bollinger-bands-backtest ``` 2. Install the WebAssembly target: @@ -119,92 +151,63 @@ rustup target add wasm32-unknown-unknown cargo build --target wasm32-unknown-unknown --release ``` -## Development +### Local Testing -### Creating Your Function +You can test the strategy by modifying the parameters in `manifest.json` and building the project. The backtest will process candlestick data according to the Bollinger Bands strategy logic. -The main entry point is in `src/lib.rs`. Modify the `run` function to implement your financial analysis logic: +## Strategy Development -```rust -#[plugin_fn] -pub fn run(fin_data: FinData) -> FnResult { - // Access ticker data - let labels = fin_data.get_labels(); - - // Get candlestick data for a specific symbol - let candles = fin_data.get_candles("symbol_data")?; - - // Use decimal precision for calculations - let decimal_candles = fin_data.get_candles_decimal("symbol_data")?; - - // Your analysis logic here - - Ok(Output { - // Return your results - }) -} -``` - -### Working with Financial Data - -```rust -// Get available ticker symbols -let symbols = fin_data.get_labels(); +### Algorithm Overview -// Access candlestick data -let candles = fin_data.get_candles("symbol_data")?; -for candle in candles { - println!("Close price: {}", candle.close); -} - -// Use decimal precision for accurate calculations -let decimal_candles = fin_data.get_candles_decimal("symbol_data")?; - -// Access piped data from other functions (usage of this is discouraged) -let pipe_sources = fin_data.get_pipe_sources(); -let data = fin_data.get_data_from_pipe("previous_analysis")?; - -// Get function call arguments -let args = fin_data.get_call_arguments(); -``` +The backtest implements the following algorithm: -### Output Structure +1. **Initialize Bollinger Bands** with specified period and multiplier +2. **Process each candle** after the initialization period +3. **Check for entry signals**: + - Long when price touches lower band + - Short when price touches upper band +4. **Manage open positions**: + - Apply stop-loss and take-profit rules + - Close position if hit +5. **Calculate final results** including total profit and trade details -Define your output structure by modifying the `Output` struct: +### Key Implementation Details ```rust -#[derive(Serialize, ToBytes)] -#[encoding(Json)] -pub struct Output { - pub result: f64, - pub signal: String, - pub confidence: f64, - // Add your fields here +// Initialize Bollinger Bands indicator +let mut bb = BollingerBands::new(bb_period, multiplier)?; + +// Entry logic example +if candle.close > v.upper { + // Open short position (overbought) + open_trade = Some(OpenTrade { + open_price: candle.close, + amount: 1.0, + side: Side::SHORT, + }); +} else if candle.close < v.lower { + // Open long position (oversold) + open_trade = Some(OpenTrade { + open_price: candle.close, + amount: 1.0, + side: Side::LONG, + }); } ``` -### Sending webhooks +### Profit Calculation -You can send webhooks to external services by using the `schedule_webhook` function: +Profits are calculated differently based on position side: +- **Long positions**: `(close_price - open_price) * amount` +- **Short positions**: `(open_price - close_price) * amount` -```rust -use crate::exchange_outpost::schedule_webhook; - -schedule_webhook("/webhook", payload); -``` -This will send a POST request to the specified webhook URL with the given payload. The base url will be set to the one you configured for webhooks in your Organization settings. +### Customization -### Sending emails - -You can send email notifications by using the `schedule_email` function: - -```rust -use crate::exchange_outpost::schedule_email; - -let email_body = "Alert: Price threshold reached!"; -schedule_email("user@example.com", email_body); -``` -This will send an email to the specified email address with the given body content. +You can modify the strategy by: +1. Adjusting entry/exit conditions in the main loop +2. Implementing different position sizing rules +3. Adding additional technical indicators +4. Modifying risk management parameters ## Building and Deployment @@ -237,26 +240,66 @@ You can use this release to test your function on the ExchangeOutpost platform. ## Dependencies -- **extism-pdk** (1.4.0): Plugin development kit for WebAssembly functions -- **rust_decimal** (1.37.1): High-precision decimal arithmetic for financial calculations +- **extism-pdk** (1.4.1): Plugin development kit for WebAssembly functions +- **rust_decimal** (1.37.2): High-precision decimal arithmetic for financial calculations - **serde** (1.0.219): Serialization/deserialization framework -- **serde_json** (1.0.140): JSON support for serde +- **serde_json** (1.0.143): JSON support for serde +- **ta** (0.5.0): Technical analysis library for Bollinger Bands calculation + +## Performance Considerations + +- The backtest processes data sequentially for accurate simulation +- Memory usage is optimized by storing only necessary trade history +- Bollinger Bands calculation uses efficient rolling window algorithms +- WebAssembly compilation ensures fast execution across platforms + +## Usage Examples + +### Basic Backtest +Deploy the function on ExchangeOutpost with default parameters: +- Period: 20 +- Multiplier: 2.0 +- Stop Loss: 2% +- Take Profit: 4% + +### Conservative Strategy +For lower risk tolerance: +- Period: 30 (longer moving average) +- Multiplier: 2.5 (wider bands) +- Stop Loss: 1.5% +- Take Profit: 3% + +### Aggressive Strategy +For higher risk tolerance: +- Period: 10 (shorter moving average) +- Multiplier: 1.5 (tighter bands) +- Stop Loss: 3% +- Take Profit: 6% + +## Limitations + +- Fixed position sizing (1.0 unit per trade) +- No transaction costs or slippage modeling +- Single timeframe analysis +- No portfolio or correlation considerations +- Mean reversion assumption may not hold in trending markets ## Contributing 1. Fork the repository -2. Create a feature branch (`git checkout -b feature/new-feature`) -3. Commit your changes (`git commit -m 'Add new feature'`) -4. Push to the branch (`git push origin feature/new-feature`) +2. Create a feature branch (`git checkout -b feature/strategy-enhancement`) +3. Commit your changes (`git commit -m 'Add enhanced exit conditions'`) +4. Push to the branch (`git push origin feature/strategy-enhancement`) 5. Open a Pull Request ## License This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for more details. - ## Related Links - [ExchangeOutpost Platform](https://www.exchangeoutpost.com/) +- [Bollinger Bands Documentation](https://en.wikipedia.org/wiki/Bollinger_Bands) +- [TA Crate Documentation](https://docs.rs/ta/latest/ta/) - [Extism Documentation](https://extism.org/) - [Rust WebAssembly Book](https://rustwasm.github.io/docs/book/) diff --git a/STRATEGY.md b/STRATEGY.md new file mode 100644 index 0000000..9810aef --- /dev/null +++ b/STRATEGY.md @@ -0,0 +1,44 @@ +## Bollinger Bands Backtest Implementation + +This implementation provides a complete Bollinger Bands mean reversion trading strategy backtest. + +### Strategy Logic + +The strategy implements a classic mean reversion approach using Bollinger Bands: + +1. **Buy Signal**: When the price touches the lower Bollinger Band (indicating oversold conditions) +2. **Sell Signal**: When the price touches the upper Bollinger Band (indicating overbought conditions) +3. **Risk Management**: Each position includes stop-loss and take-profit levels + +### Parameters + +The backtest expects the following parameters via the `FinData` object: + +- `period`: Number of periods for the moving average (e.g., 20) +- `multiplier`: Standard deviation multiplier for the bands (e.g., 2.0) +- `sl`: Stop loss percentage (e.g., 0.02 for 2%) +- `tp`: Take profit percentage (e.g., 0.04 for 4%) + +### Output + +The backtest returns detailed results including: + +- `trades`: Complete list of all executed trades +- `total_profit`: Total profit/loss from all trades +- `num_trades`: Total number of trades executed +- `winning_trades`: Number of profitable trades +- `losing_trades`: Number of losing trades +- `win_rate`: Percentage of winning trades + +### Example Usage + +The function is designed to be used as an Extism plugin. It expects candle data in the "symbol_data" field and trading parameters as call arguments. + +### Strategy Behavior + +- **Position Size**: Fixed at 1.0 unit per trade +- **Position Management**: Only one position open at a time +- **Entry Conditions**: Price must touch the band and then move away from it +- **Exit Conditions**: Either stop-loss, take-profit, or opposite signal + +This implementation provides a solid foundation for backtesting Bollinger Bands strategies and can be extended with additional features like dynamic position sizing, trailing stops, or multiple timeframe analysis. \ No newline at end of file diff --git a/manifest.json b/manifest.json index 22ad5ba..3829dfc 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,48 @@ { - "function_name": "Rust Function Template", - "description": "A template for creating Rust functions", - "financial_data_keys": [], - "call_arguments_schema": {}, - "enforce_schemas": false + "function_name": "Bollinger Bands Backtest", + "description": "A mean reversion trading strategy backtest using Bollinger Bands. Buys when price touches lower band (oversold) and sells when price touches upper band (overbought).", + "financial_data_keys": ["symbol_data"], + "call_arguments_schema": { + "type": "object", + "properties": { + "period": { + "type": ["integer","string"], + "description": "Number of periods for the moving average calculation (e.g., 20)", + "minimum": 2, + "maximum": 200, + "default": 20 + }, + "multiplier": { + "type": ["number","string"], + "description": "Standard deviation multiplier for Bollinger Bands (e.g., 2.0)", + "minimum": 0.1, + "maximum": 5.0, + "default": 2.0 + }, + "sl": { + "type": ["number","string"], + "description": "Stop loss percentage as decimal (e.g., 0.02 for 2%)", + "minimum": 0.001, + "maximum": 0.5, + "default": 0.02 + }, + "tp": { + "type": ["number","string"], + "description": "Take profit percentage as decimal (e.g., 0.04 for 4%)", + "minimum": 0.001, + "maximum": 1.0, + "default": 0.04 + }, + "usd_balance": { + "type": ["number","string"], + "description": "Amount in USD to allocate per trade (e.g., 1000.0)", + "minimum": 1.0, + "maximum": 1000000.0, + "default": 1000.0 + } + }, + "required": ["period", "multiplier", "sl", "tp"], + "additionalProperties": false + }, + "enforce_schemas": true } \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..7530651 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 120 diff --git a/src/lib.rs b/src/lib.rs index 1bf2313..6f736a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,12 +1,147 @@ use exchange_outpost_abi::FunctionArgs; use extism_pdk::{FnResult, Json, ToBytes, encoding, plugin_fn}; use serde::Serialize; +use ta::{Next, indicators::BollingerBands}; + +#[derive(Debug, Clone, PartialEq, Copy, Serialize)] +enum Side { + LONG, + SHORT, +} +#[derive(Debug, Clone, Copy)] +struct OpenTrade { + pub open_price: f64, + pub amount: f64, + pub side: Side, +} + +#[derive(Serialize)] +struct ClosedTrade { + pub open_price: f64, + pub close_price: f64, + pub amount: f64, + pub side: Side, +} #[derive(Serialize, ToBytes)] #[encoding(Json)] -pub struct Output {} - +struct BacktestResult { + pub trades: Vec, + pub total_profit: f64, + pub symbol: String, + pub exchange: String, +} +/// Bollinger Bands Mean Reversion Strategy Backtest +/// +/// Strategy Logic: +/// - BUY: When price touches the lower Bollinger Band (oversold condition) +/// - SELL: When price touches the upper Bollinger Band (overbought condition) +/// - Stop Loss: Configurable percentage below/above entry price +/// - Take Profit: Configurable percentage above/below entry price +/// +/// Parameters: +/// - period: Number of periods for moving average calculation +/// - multiplier: Standard deviation multiplier for bands +/// - sl: Stop loss percentage (e.g., 0.02 = 2%) +/// - tp: Take profit percentage (e.g., 0.04 = 4%) +/// - usd_balance: Amount in USD to allocate per trade #[plugin_fn] -pub fn run(call_args: FunctionArgs) -> FnResult { - Ok(Output {}) +pub fn run(call_args: FunctionArgs) -> FnResult { + let ticker = call_args.get_ticker("symbol_data")?; + let bb_period = call_args.get_call_argument("period")?; + let multiplier: f64 = call_args.get_call_argument("multiplier")?; + let sl: f64 = call_args.get_call_argument("sl")?; + let tp: f64 = call_args.get_call_argument("tp")?; + let usd_balance: f64 = call_args.get_call_argument("usd_balance")?; + + // Validate input parameters + if ticker.candles.len() < bb_period { + return Ok(BacktestResult { + trades: vec![], + total_profit: 0.0, + symbol: ticker.symbol.clone(), + exchange: ticker.exchange.clone(), + }); + } + let mut trades: Vec = vec![]; + let mut open_trade: Option = None; + + let mut bb = BollingerBands::new(bb_period, multiplier).expect("Failed to create Bollinger Bands"); + + // Initialize the Bollinger Bands with the first bb_period candles + let mut candles_iter = ticker.candles.iter(); + candles_iter.by_ref().take(bb_period).for_each(|candle| { + bb.next(candle.close); + }); + + // Process remaining candles for the backtest + for candle in candles_iter { + let v = bb.next(candle.close); + + match open_trade { + Some(trade) => { + let sl_price = match trade.side { + Side::LONG => trade.open_price * (1.0 - sl), + Side::SHORT => trade.open_price * (1.0 + sl), + }; + let tp_price = match trade.side { + Side::LONG => trade.open_price * (1.0 + tp), + Side::SHORT => trade.open_price * (1.0 - tp), + }; + + let should_close_long = + trade.side == Side::LONG && (candle.close < sl_price || candle.close > tp_price); + let should_close_short = + trade.side == Side::SHORT && (candle.close > sl_price || candle.close < tp_price); + + if should_close_long || should_close_short { + trades.push(ClosedTrade { + open_price: trade.open_price, + close_price: candle.close, + amount: trade.amount, + side: trade.side, + }); + open_trade = None; + } + } + None => { + if candle.close > v.upper { + // Open a short trade + open_trade = Some(OpenTrade { + open_price: candle.close, + amount: usd_balance / candle.close, + side: Side::SHORT, + }); + } else if candle.close < v.lower { + // Open a long trade + open_trade = Some(OpenTrade { + open_price: candle.close, + amount: usd_balance / candle.close, + side: Side::LONG, + }); + } + } + } + } + if let Some(trade) = open_trade { + trades.push(ClosedTrade { + open_price: trade.open_price, + close_price: ticker.candles.last().expect("No candles").close, + amount: trade.amount, + side: trade.side, + }); + } + + Ok(BacktestResult { + total_profit: trades + .iter() + .map(|t| match t.side { + Side::LONG => (t.close_price - t.open_price) * t.amount, + Side::SHORT => (t.open_price - t.close_price) * t.amount, + }) + .sum(), + trades, + symbol: ticker.symbol.clone(), + exchange: ticker.exchange.clone(), + }) }