πStep 5: Start Building
In this step, we use Cursor to generate our first real trading strategy with just one smart prompt.
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()}`
);
}
}
}
Last updated