Skip to content

Write your first strategy ​

This page is for developers who want to write their own strategy in Rust. To run the ready shot strategy, you do not need any of this: see Getting started.

Prerequisites ​

1. Install the CLI ​

bash
curl -fsSL https://tradectl.com/install.sh | sh

See the installation guide for manual download options and PATH setup.

2. Log in ​

Create an API key on your account page (app.tradectl.com/account, API Keys), then:

bash
echo "st_live_<your-key>" | tradectl login

3. Scaffold a strategy ​

bash
tradectl init my-strategy
cd my-strategy

This creates a Rust crate with:

my-strategy/
├── Cargo.toml
├── config.json          # Exchange config and strategy parameters
├── src/
│   ├── lib.rs           # Strategy implementation
│   └── params.rs        # Parameter definitions
├── tests/
│   └── test.rs          # Unit test
├── data/                # Backtest data files
├── README.md
├── STRATEGY.md          # Optional — surfaced via MCP / AI agent
└── .gitignore

4. Write your strategy ​

Edit src/lib.rs. The scaffold uses the current Action enum (v2):

rust
use tradectl_sdk::{
    Strategy, Action, OrderKind, ExitOrder, ExitType,
    Side, TickerEvent, StrategyContext, Params, ParamDef,
};

tradectl_sdk::declare_strategy!("my_strategy", MyStrategy::new);

pub struct MyStrategy {
    order_size: f64,
    tp_pct: f64,
    sl_pct: f64,
}

impl MyStrategy {
    pub fn new(params: &Params) -> Self {
        Self {
            order_size: params.get("order_size", 0.001),
            tp_pct:     params.get("tp_pct", 0.5),
            sl_pct:     params.get("sl_pct", 1.0),
        }
    }
}

impl Strategy for MyStrategy {
    fn name(&self) -> &str { "my_strategy" }
    fn describe(&self) -> &str { "My first strategy" }

    fn on_ticker(&mut self, ticker: &TickerEvent, ctx: &StrategyContext) -> Action {
        // Honor the gate. The runner sets ctx.can_enter = false when the
        // strategy is blocked by triggers, sessions, penalties, etc.
        if !ctx.can_enter || !ctx.positions.is_empty() {
            return Action::Hold;
        }

        // Place a market entry with TP (limit) and SL (stop, 3s delay).
        let entry = ticker.ask_price;
        let tp = entry * (1.0 + self.tp_pct / 100.0);
        let sl = entry * (1.0 - self.sl_pct / 100.0);

        Action::PlaceEntry {
            side: Side::Long,
            price: None,                           // None = market; Some(p) = limit
            size: self.order_size,
            kind: OrderKind::Market,
            exits: vec![
                ExitOrder { id: "tp".into(), price: tp, size: self.order_size, kind: ExitType::Limit, delay_ms: 0 },
                ExitOrder { id: "sl".into(), price: sl, size: self.order_size, kind: ExitType::Stop,  delay_ms: 3000 },
            ],
            entry_id: None,
        }
    }

    fn params_schema(&self) -> Vec<ParamDef> {
        MyStrategyParams::schema()
    }
}

The full Action enum has nine variants — see Strategies and Strategy Trait:

  • Hold
  • PlaceEntry { side, price, size, kind, exits, entry_id }
  • EditEntry, CancelEntry
  • SetExits, AddExit, UpdateExit, RemoveExit
  • CloseAll

5. Test ​

bash
cargo test

6. Build ​

bash
tradectl build

Produces target/release/libmy_strategy.so (or .dylib on macOS). Cargo replaces dashes with underscores in the cdylib name.

7. Run (paper trading) ​

bash
# Edit config.json — set isEmulator: true for paper, false for live
tradectl run my-bot -c config.json

my-bot is the bot's name. -c copies config.json into the bot's own folder, ~/.tradectl/bot/my-bot/, and the bot uses that copy from then on.

Paper mode connects to the live exchange WebSocket for real market data but simulates fills locally — no exchange API keys needed.

Next steps ​