πŸš€Step 5: Start Building

In this step, we use Cursor to generate our first real trading strategy with just one smart prompt.

Alright, it's time to get our hands dirty (metaphorically β€” we’re still at our keyboards). But before we dive in, make sure you’ve set up your environment to access the FTO Strategy API. If that sounds unfamiliar, pause here and check this guide first β€” we’ll wait.

For this step, we’re going to ask Cursor to implement a classic: the Breakout Day High/Low strategy. We'll start with an empty file to keep things simple and focused. Later on, you’ll be able to build on this with reusable components from our Foundations section β€” but baby steps first.

To keep things smooth, we gave Cursor next things:

  1. The official FTO documentation

  2. The rules for the writing the strategy code (the ones we discussed earlier)

Cursor took that prompt and decided which parts of the documentation to use.

It then rolled up its sleeves (again, metaphorically), and got to work. Here’s what it came up with:

import {
    TOptValue_number,
    FTODate,
    TOptionType,
    TTradePositionType,
    TPriceType,
  } from "../node_modules/forex-tester-custom-strategy-api-test/dist/IStrategyProcRec";
  import { StrategyImplementation } from "../node_modules/forex-tester-custom-strategy-api-test/dist/UserStrategy";
  
  export default class BreakoutStrategy extends StrategyImplementation {
    public LotSize!: TOptValue_number;
    public StopLoss!: TOptValue_number;
    public TakeProfit!: TOptValue_number;
    public MagicNumber!: TOptValue_number;
  
    private lastTradeDate!: FTODate;
    private previousD1High!: number;
    private previousD1Low!: number;
    private lastD1BarTime!: FTODate;
    private lastBarTime!: FTODate;
    private wasAboveHigh!: boolean;
    private wasBelowLow!: boolean;
  
    public Init() {
      // strategy info
      this.api.setStrategyShortName("D1 Breakout Strategy");
      this.api.setStrategyDescription(
        "A strategy that opens orders when price crosses over/under the previous D1 candle's high or low. Only one order per day allowed."
      );
  
      // initialize and register options
      this.LotSize = this.api.createTOptValue_number(0.1);
      this.StopLoss = this.api.createTOptValue_number(50);
      this.TakeProfit = this.api.createTOptValue_number(100);
      this.MagicNumber = this.api.createTOptValue_number(123456);
  
      this.api.RegOption("Lot Size", TOptionType.DOUBLE, this.LotSize);
      this.api.RegOption("Stop Loss (points)", TOptionType.INTEGER, this.StopLoss);
      this.api.RegOption("Take Profit (points)", TOptionType.INTEGER, this.TakeProfit);
      this.api.RegOption("Magic Number", TOptionType.INTEGER, this.MagicNumber);
  
      // Set ranges for validation
      this.api.SetOptionRange("Lot Size", 0.01, 1000);
      this.api.SetOptionRange("Stop Loss (points)", 1, 100000);
      this.api.SetOptionRange("Take Profit (points)", 1, 100000);
      this.api.SetOptionRange("Magic Number", 1, 999999);
  
      // initialize global variables
      this.lastTradeDate = this.api.createFTODate(0);
      this.previousD1High = 0;
      this.previousD1Low = 0;
      this.lastD1BarTime = this.api.createFTODate(0);
      this.lastBarTime = this.api.createFTODate(0);
      this.wasAboveHigh = false;
      this.wasBelowLow = false;
    }
  
    public OnTick() {
      // Process logic only once per bar opening to prevent lag
      const currentTime = this.api.Time(0);
      if (currentTime.valueOf() === this.lastBarTime.valueOf()) {
        return;
      }
      this.lastBarTime = currentTime;
  
      // Update D1 levels if we have a new D1 bar
      this.updateD1Levels();
  
      // Check if we already traded today
      if (this.hasTradedToday()) {
        return;
      }
  
      // Get current price
      const currentPrice = this.api.Close(0);
      const symbol = this.api.Symbol();
  
      // Check for crossover above previous D1 high
      if (this.previousD1High > 0) {
        const isAboveHigh = currentPrice > this.previousD1High;
        
        if (isAboveHigh && !this.wasAboveHigh) {
          console.log(`BUY Breakout detected! Price: ${currentPrice}, D1 High: ${this.previousD1High}`);
          this.placeBreakoutOrder(TTradePositionType.BUY, symbol, currentPrice);
          // Reset flags after placing order to prevent multiple signals
          this.wasAboveHigh = true;
          this.wasBelowLow = false;
          return;
        }
        
        this.wasAboveHigh = isAboveHigh;
      }
  
      // Check for crossunder below previous D1 low
      if (this.previousD1Low > 0) {
        const isBelowLow = currentPrice < this.previousD1Low;
        
        if (isBelowLow && !this.wasBelowLow) {
          console.log(`SELL Breakout detected! Price: ${currentPrice}, D1 Low: ${this.previousD1Low}`);
          this.placeBreakoutOrder(TTradePositionType.SELL, symbol, currentPrice);
          // Reset flags after placing order to prevent multiple signals
          this.wasBelowLow = true;
          this.wasAboveHigh = false;
          return;
        }
        
        this.wasBelowLow = isBelowLow;
      }
    }
  
    private updateD1Levels() {
      try {
        // Get the current D1 bar time
        const currentD1BarTime = this.getD1BarTime(this.api.Time(0));
  
        // If we have a new D1 bar, update the levels
        if (currentD1BarTime.valueOf() !== this.lastD1BarTime.valueOf()) {
          // Get the previous D1 bar index
          const previousD1BarIndex = 1; // Previous bar
          
          const high = this.api.iHigh(this.api.Symbol(), 1440, previousD1BarIndex); // 1440 = D1 timeframe
          const low = this.api.iLow(this.api.Symbol(), 1440, previousD1BarIndex);
          
          // Check for valid values
          if (high > 0 && low > 0 && !isNaN(high) && !isNaN(low)) {
            this.previousD1High = high;
            this.previousD1Low = low;
            console.log(`Updated D1 levels - High: ${high}, Low: ${low}`);
          }
          
          this.lastD1BarTime = currentD1BarTime;
        }
      } catch (error) {
        console.error("Error updating D1 levels:", error);
      }
    }
  
    private getD1BarTime(currentTime: FTODate): FTODate {
      // Create a new date object for the D1 bar
      const date = new Date(currentTime.valueOf());
      date.setHours(0, 0, 0, 0);
      
      return this.api.createFTODate(date.getTime());
    }
  
    private hasTradedToday(): boolean {
      const currentDate = this.getD1BarTime(this.api.Time(0));
      const hasTraded = currentDate.valueOf() === this.lastTradeDate.valueOf();
      
      if (hasTraded) {
        console.log(`Already traded today: ${new Date(currentDate.valueOf()).toDateString()}`);
      }
      
      return hasTraded;
    }
  
    private placeBreakoutOrder(
      positionType: TTradePositionType,
      symbol: string,
      entryPrice: number
    ) {
      // Calculate stop loss and take profit prices
      const point = this.api.Point();
      const stopLossPips = this.StopLoss.value;
      const takeProfitPips = this.TakeProfit.value;
      
      let stopLossPrice: number;
      let takeProfitPrice: number;
      
      if (positionType === TTradePositionType.BUY) {
        stopLossPrice = entryPrice - (stopLossPips * point);
        takeProfitPrice = entryPrice + (takeProfitPips * point);
      } else {
        // For SELL orders, stop loss must be ABOVE entry price
        stopLossPrice = entryPrice + (stopLossPips * point);
        takeProfitPrice = entryPrice - (takeProfitPips * point);
      }
  
      // Validate stop loss and take profit for sell orders
      if (positionType === TTradePositionType.SELL) {
        const currentPrice = this.api.Close(0);
        if (stopLossPrice <= currentPrice) {
          console.error(`Invalid stop loss for SELL order: ${stopLossPrice} <= ${currentPrice}`);
          return; // Don't place the order
        }
      }
  
      // Place the order
      const ticket = this.api.PlaceOrder(
        symbol,
        positionType,
        0, // Market order
        this.LotSize.value,
        stopLossPrice,
        takeProfitPrice,
        "D1 Breakout Strategy",
        this.MagicNumber.value
      );
  
      if (ticket !== null) {
        // Mark that we've traded today
        this.lastTradeDate = this.getD1BarTime(this.api.Time(0));
        console.log(`Breakout order placed: ${TTradePositionType[positionType]} at ${entryPrice}, SL: ${stopLossPrice}, TP: ${takeProfitPrice}`);
        console.log(`Trade date set to: ${new Date(this.lastTradeDate.valueOf()).toDateString()}`);
        
        // Immediately check if we've traded today to prevent multiple orders
        if (this.hasTradedToday()) {
          console.log("Trade date successfully set - no more orders today");
        }
      } else {
        console.error(
          `Breakout order failed with error #${this.api.GetLastError()}`
        );
      }
    }
  }
  
  

Pretty neat, right? But that’s not all. Since we used Agent mode, Cursor didn't just throw code at us and walk away. It reviewed the code, caught linter issues, made adjustments, and polished the implementation until it looked just right.

All of this β€” from zero to ready-to-test strategy β€” happened through a single well-crafted prompt. No endless tweaking, no back-and-forth.

Takeaway: When Cursor is set up properly and given clear rules + documentation, it can generate solid strategies with minimal input. Treat it like a junior developer with super speed and access to your brain.

And that’s how we got a fully working strategy in just one prompt β€” pretty cool, right?

Now let’s make sure Cursor knows where to look and what to use. On to Step 6: Setting Up Cursor for Success.

Last updated