From 9f6443aaa931f78b511894690bb9965b1d30d204 Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:24:44 +0200 Subject: [PATCH 01/10] Implement Bollinger Bands backtest strategy with configurable parameters and integrate TA library --- Cargo.lock | 7 +++ Cargo.toml | 1 + STRATEGY.md | 44 ++++++++++++++++ manifest.json | 44 ++++++++++++++-- src/lib.rs | 142 ++++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 STRATEGY.md diff --git a/Cargo.lock b/Cargo.lock index 829057c..5fefd4d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -531,6 +531,7 @@ dependencies = [ "rust_decimal", "serde", "serde_json", + "ta", ] [[package]] @@ -633,6 +634,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 afca3cc..38be675 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ extism-pdk = "1.4.1" rust_decimal = "1.37.2" serde = "1.0.219" serde_json = "1.0.143" +ta = "0.5.0" 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..753249e 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,41 @@ { - "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", + "description": "Number of periods for the moving average calculation (e.g., 20)", + "minimum": 2, + "maximum": 200, + "default": 20 + }, + "multiplier": { + "type": "number", + "description": "Standard deviation multiplier for Bollinger Bands (e.g., 2.0)", + "minimum": 0.1, + "maximum": 5.0, + "default": 2.0 + }, + "sl": { + "type": "number", + "description": "Stop loss percentage as decimal (e.g., 0.02 for 2%)", + "minimum": 0.001, + "maximum": 0.5, + "default": 0.02 + }, + "tp": { + "type": "number", + "description": "Take profit percentage as decimal (e.g., 0.04 for 4%)", + "minimum": 0.001, + "maximum": 1.0, + "default": 0.04 + } + }, + "required": ["period", "multiplier", "sl", "tp"], + "additionalProperties": false + }, + "enforce_schemas": true } \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 1ab8dc8..c9dae50 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,12 +2,146 @@ mod exchange_outpost; use crate::exchange_outpost::FinData; 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, +} +/// 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%) #[plugin_fn] -pub fn run(fin_data: FinData) -> FnResult { - Ok(Output {}) +pub fn run(fin_data: FinData) -> FnResult { + let candles = fin_data.get_candles("symbol_data")?; + let bb_period = fin_data.get_call_argument("period")?; + let multiplier: f64 = fin_data.get_call_argument("multiplier")?; + let sl: f64 = fin_data.get_call_argument("sl")?; + let tp: f64 = fin_data.get_call_argument("tp")?; + + // Validate input parameters + if candles.len() < bb_period { + return Ok(BacktestResult { + trades: vec![], + total_profit: 0.0, + }); + } + 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 = 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), + }; + if (trade.side == Side::LONG && candle.close < sl_price) + || (trade.side == Side::SHORT && candle.close > sl_price) + { + trades.push(ClosedTrade { + open_price: trade.open_price, + close_price: candle.close, + amount: trade.amount, + side: trade.side, + }); + open_trade = None; + } else if (trade.side == Side::LONG && candle.close > tp_price) + || (trade.side == Side::SHORT && candle.close < tp_price) + { + 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: 1 as f64, + side: Side::SHORT, + }); + } else if candle.close < v.lower { + // Open a long trade + open_trade = Some(OpenTrade { + open_price: candle.close, + amount: 1 as f64, + side: Side::LONG, + }); + } + } + } + } + if let Some(trade) = open_trade { + trades.push(ClosedTrade { + open_price: trade.open_price, + close_price: 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, + }) } From 92bdbdb7ea7c7a1b28c25420e2d67453896d46cf Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:36:09 +0200 Subject: [PATCH 02/10] Revise README to reflect Bollinger Bands mean reversion strategy, enhancing overview, features, strategy logic, and parameters sections --- README.md | 326 ++++++++++++++++++++++++++++++------------------------ 1 file changed, 183 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 3c14e42..743d71c 100644 --- a/README.md +++ b/README.md @@ -1,98 +1,127 @@ -# 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%) | + +### 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 } ``` -### 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 -- **`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 +134,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 +148,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 +237,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/) From 7d9d56f9a3b91ccdc0995dc6308ef6ccdf09738c Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Fri, 19 Sep 2025 18:58:24 +0200 Subject: [PATCH 03/10] better typing --- manifest.json | 8 ++++---- src/lib.rs | 14 ++++++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/manifest.json b/manifest.json index 753249e..47a1616 100644 --- a/manifest.json +++ b/manifest.json @@ -6,28 +6,28 @@ "type": "object", "properties": { "period": { - "type": "integer", + "type": ["integer","string"], "description": "Number of periods for the moving average calculation (e.g., 20)", "minimum": 2, "maximum": 200, "default": 20 }, "multiplier": { - "type": "number", + "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", + "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", + "type": ["number","string"], "description": "Take profit percentage as decimal (e.g., 0.04 for 4%)", "minimum": 0.001, "maximum": 1.0, diff --git a/src/lib.rs b/src/lib.rs index c9dae50..15f6f44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,8 @@ struct ClosedTrade { struct BacktestResult { pub trades: Vec, pub total_profit: f64, + pub symbol: String, + pub exchange: String, } /// Bollinger Bands Mean Reversion Strategy Backtest /// @@ -45,17 +47,19 @@ struct BacktestResult { /// - tp: Take profit percentage (e.g., 0.04 = 4%) #[plugin_fn] pub fn run(fin_data: FinData) -> FnResult { - let candles = fin_data.get_candles("symbol_data")?; + let ticker = fin_data.get_ticker("symbol_data")?; let bb_period = fin_data.get_call_argument("period")?; let multiplier: f64 = fin_data.get_call_argument("multiplier")?; let sl: f64 = fin_data.get_call_argument("sl")?; let tp: f64 = fin_data.get_call_argument("tp")?; // Validate input parameters - if candles.len() < bb_period { + 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![]; @@ -65,7 +69,7 @@ pub fn run(fin_data: FinData) -> FnResult { 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 = candles.iter(); + let mut candles_iter = ticker.candles.iter(); candles_iter.by_ref().take(bb_period).for_each(|candle| { bb.next(candle.close); }); @@ -128,7 +132,7 @@ pub fn run(fin_data: FinData) -> FnResult { if let Some(trade) = open_trade { trades.push(ClosedTrade { open_price: trade.open_price, - close_price: candles.last().expect("No candles").close, + close_price: ticker.candles.last().expect("No candles").close, amount: trade.amount, side: trade.side, }); @@ -143,5 +147,7 @@ pub fn run(fin_data: FinData) -> FnResult { }) .sum(), trades, + symbol: ticker.symbol.clone(), + exchange: ticker.exchange.clone(), }) } From 3842bb93a09ea342db52f5764bc41930eda8ec3f Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:47:40 +0200 Subject: [PATCH 04/10] fix: make trade open usd based --- manifest.json | 7 +++++++ src/lib.rs | 6 ++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/manifest.json b/manifest.json index 47a1616..3829dfc 100644 --- a/manifest.json +++ b/manifest.json @@ -32,6 +32,13 @@ "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"], diff --git a/src/lib.rs b/src/lib.rs index 15f6f44..35202b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,6 +45,7 @@ struct BacktestResult { /// - 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(fin_data: FinData) -> FnResult { let ticker = fin_data.get_ticker("symbol_data")?; @@ -52,6 +53,7 @@ pub fn run(fin_data: FinData) -> FnResult { let multiplier: f64 = fin_data.get_call_argument("multiplier")?; let sl: f64 = fin_data.get_call_argument("sl")?; let tp: f64 = fin_data.get_call_argument("tp")?; + let usd_balance: f64 = fin_data.get_call_argument("usd_balance")?; // Validate input parameters if ticker.candles.len() < bb_period { @@ -115,14 +117,14 @@ pub fn run(fin_data: FinData) -> FnResult { // Open a short trade open_trade = Some(OpenTrade { open_price: candle.close, - amount: 1 as f64, + 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: 1 as f64, + amount: usd_balance / candle.close, side: Side::LONG, }); } From 1f9fd85f3f6fa6c939ac9c0b1e67449d3c0a0afa Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Sun, 12 Oct 2025 22:56:19 +0200 Subject: [PATCH 05/10] feat: add usd_balance parameter for trade allocation in backtest configuration --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 743d71c..974f40f 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ The backtest accepts the following configurable parameters: | `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 @@ -62,7 +63,8 @@ The backtest accepts the following configurable parameters: "period": 20, "multiplier": 2.0, "sl": 0.02, - "tp": 0.04 + "tp": 0.04, + "usd_balance": 1000.0 } ``` @@ -71,6 +73,7 @@ This configuration creates Bollinger Bands with: - 2 standard deviations for upper/lower bands - 2% stop loss - 4% take profit target +- $1000 USD allocation per trade ## Backtest Output From fcad495f58028d79b6fce36c473be2b0abd6d7cf Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:02:53 +0100 Subject: [PATCH 06/10] refactor: simplify trade closing logic in run function --- rustfmt.toml | 1 + src/lib.rs | 21 ++++++--------------- 2 files changed, 7 insertions(+), 15 deletions(-) create mode 100644 rustfmt.toml diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..8449be0 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +max_width = 140 diff --git a/src/lib.rs b/src/lib.rs index 35202b9..cce2670 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -67,8 +67,7 @@ pub fn run(fin_data: FinData) -> FnResult { 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"); + 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(); @@ -90,19 +89,11 @@ pub fn run(fin_data: FinData) -> FnResult { Side::LONG => trade.open_price * (1.0 + tp), Side::SHORT => trade.open_price * (1.0 - tp), }; - if (trade.side == Side::LONG && candle.close < sl_price) - || (trade.side == Side::SHORT && candle.close > sl_price) - { - trades.push(ClosedTrade { - open_price: trade.open_price, - close_price: candle.close, - amount: trade.amount, - side: trade.side, - }); - open_trade = None; - } else if (trade.side == Side::LONG && candle.close > tp_price) - || (trade.side == Side::SHORT && candle.close < tp_price) - { + + 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, From 81d878d5cfc76bb502f620a087fd5eecb53b2896 Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:08:22 +0100 Subject: [PATCH 07/10] chore: run cargo fmt --- src/exchange_outpost/candle.rs | 20 +++++--------------- src/exchange_outpost/fin_data.rs | 19 ++++--------------- src/exchange_outpost/notifications.rs | 10 ++-------- 3 files changed, 11 insertions(+), 38 deletions(-) diff --git a/src/exchange_outpost/candle.rs b/src/exchange_outpost/candle.rs index 12a095b..fb06dcf 100644 --- a/src/exchange_outpost/candle.rs +++ b/src/exchange_outpost/candle.rs @@ -43,21 +43,11 @@ impl Candle { pub fn to_decimal(&self, precision: i32) -> Candle { Candle { timestamp: self.timestamp, - open: Decimal::from_f64(self.open) - .unwrap_or(Decimal::ZERO) - .round_dp(precision as u32), - high: Decimal::from_f64(self.high) - .unwrap_or(Decimal::ZERO) - .round_dp(precision as u32), - low: Decimal::from_f64(self.low) - .unwrap_or(Decimal::ZERO) - .round_dp(precision as u32), - close: Decimal::from_f64(self.close) - .unwrap_or(Decimal::ZERO) - .round_dp(precision as u32), - volume: Decimal::from_f64(self.volume) - .unwrap_or(Decimal::ZERO) - .round_dp(precision as u32), + open: Decimal::from_f64(self.open).unwrap_or(Decimal::ZERO).round_dp(precision as u32), + high: Decimal::from_f64(self.high).unwrap_or(Decimal::ZERO).round_dp(precision as u32), + low: Decimal::from_f64(self.low).unwrap_or(Decimal::ZERO).round_dp(precision as u32), + close: Decimal::from_f64(self.close).unwrap_or(Decimal::ZERO).round_dp(precision as u32), + volume: Decimal::from_f64(self.volume).unwrap_or(Decimal::ZERO).round_dp(precision as u32), } } } diff --git a/src/exchange_outpost/fin_data.rs b/src/exchange_outpost/fin_data.rs index 8ac8f3b..4df2260 100644 --- a/src/exchange_outpost/fin_data.rs +++ b/src/exchange_outpost/fin_data.rs @@ -24,9 +24,7 @@ impl TickersData { } pub fn get_candles_decimal_iter(&self) -> impl Iterator> { let precision = self.precision; - self.candles - .iter() - .map(move |candle| candle.to_decimal(precision)) + self.candles.iter().map(move |candle| candle.to_decimal(precision)) } pub fn get_candles_decimal(&self) -> Vec> { self.get_candles_decimal_iter().collect() @@ -66,10 +64,7 @@ impl FinData { )) } - pub fn get_candles_iter( - &self, - label: &str, - ) -> Result>, WithReturnCode> { + pub fn get_candles_iter(&self, label: &str) -> Result>, WithReturnCode> { let candles = self.get_candles(label)?; Ok(candles.iter()) } @@ -98,18 +93,12 @@ impl FinData { )) } /// Returns the candles as Decimal, precision is taken from the ticker - pub fn get_candles_decimal_iter( - &self, - label: &str, - ) -> Result>, WithReturnCode> { + pub fn get_candles_decimal_iter(&self, label: &str) -> Result>, WithReturnCode> { let ticker = self.get_ticker(label)?; Ok(ticker.get_candles_decimal_iter()) } /// Returns the candles as Decimal, precision is taken from the ticker - pub fn get_candles_decimal( - &self, - label: &str, - ) -> Result>, WithReturnCode> { + pub fn get_candles_decimal(&self, label: &str) -> Result>, WithReturnCode> { Ok(self.get_candles_decimal_iter(label)?.collect()) } diff --git a/src/exchange_outpost/notifications.rs b/src/exchange_outpost/notifications.rs index 871ba92..9b6a123 100644 --- a/src/exchange_outpost/notifications.rs +++ b/src/exchange_outpost/notifications.rs @@ -11,10 +11,7 @@ pub fn schedule_webhook(path: &str, body: &str) -> Result<(), WithReturnCode Result<(), WithReturnCode Date: Wed, 17 Dec 2025 10:53:06 +0100 Subject: [PATCH 08/10] refactor: adjust max_width in rustfmt.toml and improve readability of trade closing logic --- rustfmt.toml | 2 +- src/lib.rs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 8449be0..7530651 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -max_width = 140 +max_width = 120 diff --git a/src/lib.rs b/src/lib.rs index cce2670..7faee64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,8 +90,10 @@ pub fn run(fin_data: FinData) -> FnResult { 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); + 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 { From e50c94f4b8f3c730e6ba7a28007eb002998cc061 Mon Sep 17 00:00:00 2001 From: AlessandroRuggiero <50781152+AlessandroRuggiero@users.noreply.github.com> Date: Wed, 17 Dec 2025 11:01:02 +0100 Subject: [PATCH 09/10] refactor: improve formatting and readability in Candle and notification functions --- src/exchange_outpost/candle.rs | 20 +++++++++++++++----- src/exchange_outpost/fin_data.rs | 5 ++++- src/exchange_outpost/notifications.rs | 10 ++++++++-- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/exchange_outpost/candle.rs b/src/exchange_outpost/candle.rs index fb06dcf..12a095b 100644 --- a/src/exchange_outpost/candle.rs +++ b/src/exchange_outpost/candle.rs @@ -43,11 +43,21 @@ impl Candle { pub fn to_decimal(&self, precision: i32) -> Candle { Candle { timestamp: self.timestamp, - open: Decimal::from_f64(self.open).unwrap_or(Decimal::ZERO).round_dp(precision as u32), - high: Decimal::from_f64(self.high).unwrap_or(Decimal::ZERO).round_dp(precision as u32), - low: Decimal::from_f64(self.low).unwrap_or(Decimal::ZERO).round_dp(precision as u32), - close: Decimal::from_f64(self.close).unwrap_or(Decimal::ZERO).round_dp(precision as u32), - volume: Decimal::from_f64(self.volume).unwrap_or(Decimal::ZERO).round_dp(precision as u32), + open: Decimal::from_f64(self.open) + .unwrap_or(Decimal::ZERO) + .round_dp(precision as u32), + high: Decimal::from_f64(self.high) + .unwrap_or(Decimal::ZERO) + .round_dp(precision as u32), + low: Decimal::from_f64(self.low) + .unwrap_or(Decimal::ZERO) + .round_dp(precision as u32), + close: Decimal::from_f64(self.close) + .unwrap_or(Decimal::ZERO) + .round_dp(precision as u32), + volume: Decimal::from_f64(self.volume) + .unwrap_or(Decimal::ZERO) + .round_dp(precision as u32), } } } diff --git a/src/exchange_outpost/fin_data.rs b/src/exchange_outpost/fin_data.rs index 4df2260..2fb53fe 100644 --- a/src/exchange_outpost/fin_data.rs +++ b/src/exchange_outpost/fin_data.rs @@ -93,7 +93,10 @@ impl FinData { )) } /// Returns the candles as Decimal, precision is taken from the ticker - pub fn get_candles_decimal_iter(&self, label: &str) -> Result>, WithReturnCode> { + pub fn get_candles_decimal_iter( + &self, + label: &str, + ) -> Result>, WithReturnCode> { let ticker = self.get_ticker(label)?; Ok(ticker.get_candles_decimal_iter()) } diff --git a/src/exchange_outpost/notifications.rs b/src/exchange_outpost/notifications.rs index 9b6a123..871ba92 100644 --- a/src/exchange_outpost/notifications.rs +++ b/src/exchange_outpost/notifications.rs @@ -11,7 +11,10 @@ pub fn schedule_webhook(path: &str, body: &str) -> Result<(), WithReturnCode Result<(), WithReturnCode Date: Wed, 24 Dec 2025 13:08:02 +0100 Subject: [PATCH 10/10] refactor: update function parameter type in run function to FunctionArgs --- src/lib.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 5d4b3f7..6f736a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,13 +46,13 @@ struct BacktestResult { /// - tp: Take profit percentage (e.g., 0.04 = 4%) /// - usd_balance: Amount in USD to allocate per trade #[plugin_fn] -pub fn run(fin_data: FinData) -> FnResult { - let ticker = fin_data.get_ticker("symbol_data")?; - let bb_period = fin_data.get_call_argument("period")?; - let multiplier: f64 = fin_data.get_call_argument("multiplier")?; - let sl: f64 = fin_data.get_call_argument("sl")?; - let tp: f64 = fin_data.get_call_argument("tp")?; - let usd_balance: f64 = fin_data.get_call_argument("usd_balance")?; +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 {