Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
A quick introduction: what they do, why traders use them, and how they can help you spot market trends and signals.
Get your tools ready — install Cursor IDE and prepare your project so you’re set to start building indicators.
This page shows you how to set up your environment for writing custom indicators in FTO. Think of it as laying out your pencils before sketching.
This guide shows you how to take your freshly built indicator and bring it into Forex Tester Online.




npm installnpm run buildcannot be loaded because running scripts is disabled on this system.
For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170. At line:1 char:1Teach Cursor your custom rules so it knows how to handle indicators smarter and faster.
Some quick tips to get the most out of Cursor and your docs
In this step, we’ll give Cursor its “textbook” — the official FTO indicator documentation.
Learn how to create and run a simple Moving Average indicator in FTO — perfect for getting started with custom indicators.
Ready-made examples of built-in and starter indicators — source code, downloads, and quick reference for each example.
Catalog of built-in FTO indicator examples — download ready-made projects, explore source code, and use them as reference implementations.
ATR (Average True Range) built-in indicator — download the project and explore volatility measurement on the chart.
Bollinger Bands built-in indicator — download the project and explore price channels based on standard deviation.
CCI (Commodity Channel Index) built-in indicator — download the project and explore cyclical overbought/oversold levels.
Doji built-in indicator — download the project and explore Doji candlestick pattern detection on the chart.
Engulfing Bar built-in indicator — download the project and explore bullish and bearish engulfing candlestick pattern detection.
Fractals built-in indicator — download the project and explore Bill Williams fractal swing high and low markers.
MACD built-in indicator — download the project and explore Moving Average Convergence Divergence trend and momentum signals.
Rate of Change built-in indicator — download the project and explore momentum measurement as the speed of price change.

























import { IndicatorImplementation, TDrawStyle, TPenStyle, TOutputWindow, TIndexBuffer } from "forex-tester-custom-indicator-api";
export default class OBVIndicator extends IndicatorImplementation {
// Declare the buffer as a class property
public obvBuffer!: TIndexBuffer;
Init(): void {
this.api.RecalculateMeAlways();
// Set indicator name
this.api.IndicatorShortName("On Balance Volume (OBV)");
// Configure to display in separate window since OBV is an oscillator
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW);
// Register the number of buffers we'll use
this.api.IndicatorBuffers(1);
// Create and initialize the OBV buffer
this.obvBuffer = this.api.CreateIndexBuffer();
// Bind buffer to index 0
this.api.SetIndexBuffer(0, this.obvBuffer);
// Configure buffer appearance
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#0000FF"); // Blue line
this.api.SetIndexLabel(0, "OBV");
}
Calculate(index: number): void {
// If this is the first bar (rightmost/newest), initialize OBV
if (index === this.api.Bars() - 1) {
this.obvBuffer.setValue(index, this.api.Volume(index));
return;
}
// Get current and previous close prices
const currentClose = this.api.Close(index);
const previousClose = this.api.Close(index + 1);
// Get current volume
const volume = this.api.Volume(index);
// Get previous OBV value
const previousOBV = this.obvBuffer.getValue(index + 1);
let currentOBV;
// Calculate OBV based on price movement
if (currentClose > previousClose) {
// If price increased, add volume
currentOBV = previousOBV + volume;
} else if (currentClose < previousClose) {
// If price decreased, subtract volume
currentOBV = previousOBV - volume;
} else {
// If price unchanged, OBV remains the same
currentOBV = previousOBV;
}
// Set the calculated OBV value
this.obvBuffer.setValue(index, currentOBV);
}
}import {
IndicatorImplementation,
TDrawStyle,
TPenStyle,
TOutputWindow,
TIndexBuffer,
TOptionType,
TOptValue_number
} from "forex-tester-custom-indicator-api";
export default class OBVIndicator extends IndicatorImplementation {
// Declare the buffer as a class property
public obvBuffer!: TIndexBuffer;
// Declare price type parameter
public priceType!: TOptValue_number;
Init(): void {
this.api.RecalculateMeAlways();
// Set indicator name
this.api.IndicatorShortName("On Balance Volume (OBV)");
// Configure to display in separate window since OBV is an oscillator
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW);
// Initialize price type parameter
this.priceType = this.api.createTOptValue_number(0); // Default to Close price
// Register price type parameter as an enum
this.api.RegOption(
"Price Type",
TOptionType.ENUM_TYPE,
this.priceType
);
// Add price type options
this.api.AddOptionValue("Price Type", "Close");
this.api.AddOptionValue("Price Type", "Open");
this.api.AddOptionValue("Price Type", "High");
this.api.AddOptionValue("Price Type", "Low");
this.api.AddOptionValue("Price Type", "Median ((H+L)/2)");
this.api.AddOptionValue("Price Type", "Typical ((H+L+C)/3)");
// Register the number of buffers we'll use
this.api.IndicatorBuffers(1);
// Create and initialize the OBV buffer
this.obvBuffer = this.api.CreateIndexBuffer();
// Bind buffer to index 0
this.api.SetIndexBuffer(0, this.obvBuffer);
// Configure buffer appearance
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#0000FF"); // Blue line
this.api.SetIndexLabel(0, "OBV");
}
private getPrice(index: number): number {
switch (this.priceType.value) {
case 0: // Close
return this.api.Close(index);
case 1: // Open
return this.api.Open(index);
case 2: // High
return this.api.High(index);
case 3: // Low
return this.api.Low(index);
case 4: // Median
return (this.api.High(index) + this.api.Low(index)) / 2;
case 5: // Typical
return (this.api.High(index) + this.api.Low(index) + this.api.Close(index)) / 3;
default:
return this.api.Close(index); // Fallback to Close
}
}
Calculate(index: number): void {
// If this is the first bar (rightmost/newest), initialize OBV
if (index === this.api.Bars() - 1) {
this.obvBuffer.setValue(index, this.api.Volume(index));
return;
}
// Get current and previous prices using selected price type
const currentPrice = this.getPrice(index);
const previousPrice = this.getPrice(index + 1);
// Get current volume
const volume = this.api.Volume(index);
// Get previous OBV value
const previousOBV = this.obvBuffer.getValue(index + 1);
let currentOBV;
// Calculate OBV based on price movement
if (currentPrice > previousPrice) {
// If price increased, add volume
currentOBV = previousOBV + volume;
} else if (currentPrice < previousPrice) {
// If price decreased, subtract volume
currentOBV = previousOBV - volume;
} else {
// If price unchanged, OBV remains the same
currentOBV = previousOBV;
}
// Set the calculated OBV value
this.obvBuffer.setValue(index, currentOBV);
}
}import { IndicatorImplementation } from "forex-tester-custom-indicator-api";
export default class MovingAverage extends IndicatorImplementation {
// indicator logic
}export default class MovingAverage extends IndicatorImplementation {
// Declaring class-level fields
public Period!: TOptValue_number;
public Shift!: TOptValue_number;
public MAtype!: TOptValue_number;
public ApplyToPrice!: TOptValue_number;
public VShift!: TOptValue_number;
Init(): void {
// Create parameters using factory method
this.Period = this.api.createTOptValue_number(8);
this.Shift = this.api.createTOptValue_number(0);
this.MAtype = this.api.createTOptValue_number(E_MAType.SMA);
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE);
this.VShift = this.api.createTOptValue_number(0);
...existing code...
}
public Init(): void {
...existing code...
// Register parameter this.Period so it's shown in the indicator settings
this.api.RegOption(
'Period',
TOptionType.INTEGER,
this.Period
);
// Setting the maximum avalable range that can be used for Period value
this.api.SetOptionRange(
'Period',
1,
Number.MAX_SAFE_INTEGER
);
// Register parameter this.Shift so it's shown in the indicator settings
this.api.RegOption(
'Shift',
TOptionType.INTEGER,
this.Shift
);
// Register parameter this.VShift so it's its shown in the indicator settings
this.api.RegOption(
'VShift',
TOptionType.INTEGER,
this.VShift
);
// Register the MA type so it has a drowdown in the indicator settings
this.api.RegMATypeOption(
this.MAtype,
'MAtype'
);
// Register the price type so it has a dropdown in the indicator settings.
this.api.RegApplyToPriceOption(
this.ApplyToPrice,
'ApplyToPrice'
);
...existing code...
}public SSMA!: TIndexBuffer
private SMA!: TIndexBufferthis.SMA = this.api.CreateIndexBuffer();
this.SSMA = this.api.CreateIndexBuffer();this.api.IndicatorBuffers(1);
this.api.SetIndexBuffer(0, this.SSMA);this.api.SetIndexLabel(0, "MA");
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#FF0000");
this.api.SetIndexDrawBegin(0, this.Period.value - 1 + this.Shift.value);public Calculate(index: number): void {
// check if the index is in the valid range
if (index + this.Period.value >= this.api.Bars()) {
return
}
// calculate the SMA value
const calculatedSMA = this.api.GetMA(
index,
0,
this.Period.value,
this.MAtype.value,
this.ApplyToPrice.value,
// here we get the value of the previous bar
this.SMA.getValue(index + 1)
)
this.SMA.setValue(index, calculatedSMA)
// set the value which is going to be displayed on the chart
this.SSMA.setValue(index, calculatedSMA + this.VShift.value * this.api.Point())
}public OnParamsChange(): void {
this.api.SetBufferShift(0, this.Shift.value)
}import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
E_MAType,
TIndexBuffer,
TOutputWindow,
TPenStyle,
TOptionType,
TDrawStyle
} from 'forex-tester-custom-indicator-api'
export default class ATR extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
public ApplyToPrice!: TOptValue_number
public Method!: TOptValue_number
// buffers
public ATRBuffer!: TIndexBuffer
public TrueRange!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('ATR')
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.AddLevel(0, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(14)
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE)
this.Method = this.api.createTOptValue_number(E_MAType.SMMA)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, 1000)
this.api.RegMATypeOption(this.Method, '')
this.api.RegApplyToPriceOption(this.ApplyToPrice, '')
// create buffers
this.ATRBuffer = this.api.CreateIndexBuffer()
this.TrueRange = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(1)
this.api.SetIndexBuffer(0, this.ATRBuffer)
this.api.SetIndexLabel(0, 'ATR')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#FF0000')
}
public Calculate(index: number): void {
const high = this.api.High(index)
const low = this.api.Low(index)
if (index === this.api.Bars() - 1) {
this.TrueRange.setValue(index, high - low)
} else {
const prevPrice = this.api.GetPrice(index + 1, this.ApplyToPrice.value)
this.TrueRange.setValue(index, Math.max(high, prevPrice) - Math.min(low, prevPrice))
}
this.MAOnBuffer(index, this.TrueRange, this.Period.value, this.ATRBuffer)
}
public OnParamsChange(): void {
const backOffset = this.calculateMABackOffset(this.Method.value, this.Period.value, 2)
this.api.SetBackOffsetForCalculation(backOffset)
}
private MAOnBuffer(index: number, source: TIndexBuffer, period: number, out: TIndexBuffer): void {
switch (this.Method.value) {
case E_MAType.SMA: {
this.SMA(index, source, period, out)
break
}
case E_MAType.EMA: {
this.EMA(index, source, period, out)
break
}
case E_MAType.SMMA: {
this.SMMA(index, source, period, out)
break
}
case E_MAType.LWMA: {
this.LWMA(index, source, period, out)
break
}
default: {
this.SMA(index, source, period, out)
break
}
}
}
private EMA(index: number, source: TIndexBuffer, period: number, out: TIndexBuffer): void {
const alpha = 2.0 / (period + 1.0)
const prev = out.getValue(index + 1) || this.SMA(index, source, period)
const value = alpha * source.getValue(index) + (1 - alpha) * prev
out.setValue(index, value)
}
private LWMA(index: number, source: TIndexBuffer, period: number, out: TIndexBuffer): void {
const weight = (period * (period + 1)) / 2.0
let sum = 0.0
for (let i = 0; i < period; i++) {
sum += source.getValue(index + i) * (period - i)
}
out.setValue(index, sum / weight)
}
private SMMA(index: number, source: TIndexBuffer, period: number, out: TIndexBuffer): void {
const prev = out.getValue(index + 1) || this.SMA(index, source, period)
const value = (prev * (period - 1) + source.getValue(index)) / period
out.setValue(index, value)
}
private SMA(index: number, source: TIndexBuffer, period: number, out?: TIndexBuffer): number {
let sum = 0.0
for (let i = 0; i <= period - 1; i++) {
sum += source.getValue(index + i)
}
if (out) {
out.setValue(index, sum / period)
}
return sum / period
}
private calculateMABackOffset(method: E_MAType, period: number, precision: number): number {
const maxLookback = 1000
const nPeriod = Math.min(Math.max(period, 1), maxLookback)
switch (method) {
case E_MAType.EMA:
case E_MAType.SMMA: {
const epsilon = Math.pow(10, -Math.max(precision, 1))
const alpha = method === E_MAType.EMA ? 2 / (nPeriod + 1) : 1 / nPeriod
if (alpha >= 1 - Number.EPSILON) {
return 1
}
const k = Math.ceil(Math.log(epsilon) / Math.log1p(-alpha))
return Math.min(k, maxLookback)
}
default: {
return nPeriod
}
}
}
}import {
IndicatorImplementation,
TOptValue_number,
E_MAType,
TPriceType,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
enum StdDevType {
SOURCE_VALUES = 0, // Standard deviation of source values
MA_DEVIATIONS = 1 // Standard deviation of deviations from MA
}
export default class BollingerBands extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
public Shift!: TOptValue_number
public Deviation!: TOptValue_number
public MAType!: TOptValue_number
public ApplyToPrice!: TOptValue_number
public StdDevType!: TOptValue_number
// buffers
public MABuffer!: TIndexBuffer
public UpBandBuffer!: TIndexBuffer
public DownBandBuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Bollinger Bands')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(8)
this.Shift = this.api.createTOptValue_number(0)
this.Deviation = this.api.createTOptValue_number(2.0)
this.MAType = this.api.createTOptValue_number(E_MAType.SMA)
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE)
this.StdDevType = this.api.createTOptValue_number(StdDevType.SOURCE_VALUES)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, 1000)
this.api.RegOption('Deviation', TOptionType.DOUBLE, this.Deviation)
this.api.SetOptionRange('Deviation', 0.1, 200)
this.api.RegOption('Shift', TOptionType.INTEGER, this.Shift)
this.api.RegMATypeOption(this.MAType)
this.api.RegOption('StdDev Type', TOptionType.ENUM_TYPE, this.StdDevType)
this.api.AddOptionValue('StdDev Type', 'Source values')
this.api.AddOptionValue('StdDev Type', 'MA deviations')
this.api.RegApplyToPriceOption(this.ApplyToPrice, 'Apply to price')
// create buffers
this.MABuffer = this.api.CreateIndexBuffer()
this.UpBandBuffer = this.api.CreateIndexBuffer()
this.DownBandBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(3)
this.api.SetIndexBuffer(0, this.UpBandBuffer)
this.api.SetIndexLabel(0, 'Upper Band')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#008080')
this.api.SetIndexDrawBegin(0, this.Period.value - 1 + this.Shift.value)
this.api.SetIndexBuffer(1, this.MABuffer)
this.api.SetIndexLabel(1, 'MA')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#008080')
this.api.SetIndexDrawBegin(1, this.Period.value - 1 + this.Shift.value)
this.api.SetIndexBuffer(2, this.DownBandBuffer)
this.api.SetIndexLabel(2, 'Lower Band')
this.api.SetIndexStyle(2, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#008080')
this.api.SetIndexDrawBegin(2, this.Period.value - 1 + this.Shift.value)
}
public Calculate(index: number): void {
if (index + this.Period.value >= this.api.Bars()) {
return
}
const ma = this.api.GetMA(
index,
0,
this.Period.value,
this.MAType.value,
this.ApplyToPrice.value,
this.MABuffer.getValue(index + 1)
)
this.MABuffer.setValue(index, ma)
const stdDev = this.calculateStandardDeviation(index, ma)
this.UpBandBuffer.setValue(index, ma + stdDev * this.Deviation.value)
this.DownBandBuffer.setValue(index, ma - stdDev * this.Deviation.value)
}
private calculateStandardDeviation(index: number, ma: number): number {
if (this.StdDevType.value === StdDevType.MA_DEVIATIONS) {
let sumSquaredDiff = 0.0
for (let i = 0; i < this.Period.value; i++) {
if (index + i >= this.api.Bars()) continue
const price = this.api.GetPrice(index + i, this.ApplyToPrice.value)
const diff = price - ma
sumSquaredDiff += diff * diff
}
return Math.sqrt(sumSquaredDiff / this.Period.value)
} else {
return this.StandardDeviation(index, this.ApplyToPrice.value, this.Period.value)
}
}
private StandardDeviation(index: number, source: TPriceType, period: number): number {
if (period <= 0) return 0.0
let sum = 0.0
let counter = 0
for (let i = 0; i < period; i++) {
if (index + i >= this.api.Bars()) continue
sum += this.api.GetPrice(index + i, source)
counter++
}
if (counter === 0) return 0.0
const mean = sum / period
let sumSquaredDiff = 0.0
for (let i = 0; i < period; i++) {
if (index + i >= this.api.Bars()) continue
const diff = this.api.GetPrice(index + i, source) - mean
sumSquaredDiff += diff * diff
}
const variance = sumSquaredDiff / period
return Math.sqrt(variance)
}
public OnParamsChange(): void {
const backOffset = this.calculateMABackOffset(this.MAType.value, this.Period.value, 2)
this.api.SetBackOffsetForCalculation(backOffset + this.Shift.value)
this.api.SetBufferShift(0, this.Shift.value)
this.api.SetBufferShift(1, this.Shift.value)
this.api.SetBufferShift(2, this.Shift.value)
}
private calculateMABackOffset(method: E_MAType, period: number, precision: number): number {
const maxLookback = 1000
const nPeriod = Math.min(Math.max(period, 1), maxLookback)
switch (method) {
case E_MAType.EMA:
case E_MAType.SMMA: {
const epsilon = Math.pow(10, -Math.max(precision, 1))
const alpha = method === E_MAType.EMA ? 2 / (nPeriod + 1) : 1 / nPeriod
if (alpha >= 1 - Number.EPSILON) {
return 1
}
const k = Math.ceil(Math.log(epsilon) / Math.log1p(-alpha))
return Math.min(k, maxLookback)
}
default: {
return nPeriod
}
}
}
}import {
IndicatorImplementation,
TOptValue_number,
TIndexBuffer,
TOutputWindow,
TPenStyle,
TOptionType,
TDrawStyle,
TPriceType
} from 'forex-tester-custom-indicator-api'
export default class CCI extends IndicatorImplementation {
// inputs
public CCIPeriod!: TOptValue_number
// buffers
public CCIBuffer!: TIndexBuffer
public MABuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('CCI')
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.AddLevel(-100, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.AddLevel(100, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.AddLevel(0, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.CCIPeriod = this.api.createTOptValue_number(14)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.CCIPeriod)
this.api.SetOptionRange('Period', 1, Number.MAX_SAFE_INTEGER)
// create buffers
this.MABuffer = this.api.CreateIndexBuffer()
this.CCIBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(1)
this.api.SetIndexBuffer(0, this.CCIBuffer)
this.api.SetIndexLabel(0, 'CCI')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#1E90FF')
}
public Calculate(index: number): void {
// calculate MA first
let movingAverage = 0
for (let i = 0; i < this.CCIPeriod.value; i++) {
movingAverage += this.api.GetPrice(index + i, TPriceType.HLC3) / this.CCIPeriod.value
}
this.MABuffer.setValue(index, movingAverage)
// return if not enough bars
if (index + this.CCIPeriod.value >= this.api.Bars()) {
return
}
// calculate average mean deviation
let averageDeviation = 0
for (let i = 0; i < this.CCIPeriod.value; i++) {
const typicalPrice = this.api.GetPrice(index + i, TPriceType.HLC3)
averageDeviation += Math.abs(typicalPrice - this.MABuffer.getValue(index)) / this.CCIPeriod.value
}
// calculate CCI value
if (averageDeviation === 0) {
this.CCIBuffer.setValue(index, this.CCIBuffer.getValue(index + 1))
} else {
const typicalPrice = this.api.GetPrice(index, TPriceType.HLC3)
const priceDeviation = typicalPrice - this.MABuffer.getValue(index)
this.CCIBuffer.setValue(index, priceDeviation / (0.015 * averageDeviation))
}
}
}import {
IndicatorImplementation,
TIndexBuffer,
TOutputWindow,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
interface CandleStructure {
bodyTop: number
bodyBottom: number
bodySize: number
upperWick: number
lowerWick: number
totalRange: number
}
export default class Doji extends IndicatorImplementation {
public DojiBuffer!: TIndexBuffer
private readonly shadowTolerancePercent = 100.0
private readonly bodySizePercent = 5.0
public Init(): void {
this.api.IndicatorShortName('Doji')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.HideIndicatorValueMarks()
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
this.DojiBuffer = this.api.CreateIndexBuffer()
this.api.IndicatorBuffers(1)
this.api.SetIndexBuffer(0, this.DojiBuffer)
this.api.SetIndexLabel(0, 'Doji')
this.api.SetIndexStyle(0, TDrawStyle.SYMBOL, TPenStyle.SOLID, 2, '#72778a')
this.api.SetIndexSymbol(0, 233, 0, -15)
}
public Calculate(index: number): void {
this.DojiBuffer.setValue(index, 0)
const openPrice = this.api.Open(index)
const closePrice = this.api.Close(index)
const highPrice = this.api.High(index)
const lowPrice = this.api.Low(index)
const candleStructure = this.calculateCandleStructure(openPrice, closePrice, highPrice, lowPrice)
if (!this.isValidCandle(candleStructure)) {
return
}
const hasSmallBody = this.checkSmallBodyCondition(candleStructure)
const hasBalancedShadows = this.checkShadowBalance(candleStructure)
if (hasSmallBody && hasBalancedShadows) {
const isDragonfly = this.isDragonflyDoji(candleStructure)
const isGravestone = this.isGravestoneDoji(candleStructure)
if (!isDragonfly && !isGravestone) {
this.DojiBuffer.setValue(index, lowPrice)
}
}
}
private calculateCandleStructure(open: number, close: number, high: number, low: number): CandleStructure {
const bodyTop = Math.max(close, open)
const bodyBottom = Math.min(close, open)
const bodySize = bodyTop - bodyBottom
const upperWick = high - bodyTop
const lowerWick = bodyBottom - low
const totalRange = high - low
return {
bodyTop,
bodyBottom,
bodySize,
upperWick,
lowerWick,
totalRange
}
}
private isValidCandle(structure: CandleStructure): boolean {
return structure.totalRange > 0
}
private checkSmallBodyCondition(structure: CandleStructure): boolean {
const maxBodySize = (structure.totalRange * this.bodySizePercent) / 100.0
return structure.bodySize <= maxBodySize
}
private checkShadowBalance(structure: CandleStructure): boolean {
if (structure.upperWick === structure.lowerWick) {
return true
}
const upperWickPercent =
structure.lowerWick === 0
? Number.POSITIVE_INFINITY
: (Math.abs(structure.upperWick - structure.lowerWick) / structure.lowerWick) * 100.0
const lowerWickPercent =
structure.upperWick === 0
? Number.POSITIVE_INFINITY
: (Math.abs(structure.lowerWick - structure.upperWick) / structure.upperWick) * 100.0
return upperWickPercent < this.shadowTolerancePercent && lowerWickPercent < this.shadowTolerancePercent
}
private isDragonflyDoji(structure: CandleStructure): boolean {
return structure.upperWick <= structure.bodySize
}
private isGravestoneDoji(structure: CandleStructure): boolean {
return structure.lowerWick <= structure.bodySize
}
}import {
IndicatorImplementation,
TIndexBuffer,
TOutputWindow,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
export default class EngulfingBar extends IndicatorImplementation {
// buffers
public BullishBuffer!: TIndexBuffer
public BearishBuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Engulfing Bar')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.HideIndicatorValueMarks()
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create buffers
this.BullishBuffer = this.api.CreateIndexBuffer()
this.BearishBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(2)
this.api.SetIndexBuffer(0, this.BullishBuffer)
this.api.SetIndexLabel(0, 'Bullish')
this.api.SetIndexStyle(0, TDrawStyle.SYMBOL, TPenStyle.SOLID, 2, '#00bfff')
this.api.SetIndexSymbol(0, 233, 0, -15)
this.api.SetIndexBuffer(1, this.BearishBuffer)
this.api.SetIndexLabel(1, 'Bearish')
this.api.SetIndexStyle(1, TDrawStyle.SYMBOL, TPenStyle.SOLID, 2, '#9370db')
this.api.SetIndexSymbol(1, 234, 0, 15)
}
public Calculate(index: number): void {
const currClose = this.api.Close(index)
const currOpen = this.api.Open(index)
const prevClose = this.api.Close(index + 1) || currClose
const prevOpen = this.api.Open(index + 1) || currOpen
this.BullishBuffer.setValue(index, 0)
this.BearishBuffer.setValue(index, 0)
if (currClose > currOpen && prevClose < prevOpen && currClose >= prevOpen && currOpen <= prevClose) {
this.BullishBuffer.setValue(index, this.api.Low(index))
}
if (currClose < currOpen && prevClose > prevOpen && currClose <= prevOpen && currOpen >= prevClose) {
this.BearishBuffer.setValue(index, this.api.High(index))
}
}
}import {
IndicatorImplementation,
TOptValue_number,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
export default class Fractals extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
// buffers
public UpperFractal!: TIndexBuffer
public LowerFractal!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Fractals')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(2)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, 1000)
// create buffers
this.UpperFractal = this.api.CreateIndexBuffer()
this.LowerFractal = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(2)
this.api.SetIndexBuffer(0, this.UpperFractal)
this.api.SetIndexLabel(0, 'Fractal Up')
this.api.SetIndexStyle(0, TDrawStyle.SYMBOL, TPenStyle.SOLID, 5, '#26A69A')
this.api.SetIndexSymbol(0, 217, 0, 20)
this.api.SetIndexBuffer(1, this.LowerFractal)
this.api.SetIndexLabel(1, 'Fractal Down')
this.api.SetIndexStyle(1, TDrawStyle.SYMBOL, TPenStyle.SOLID, 5, '#EF5350')
this.api.SetIndexSymbol(1, 218, 0, -20)
}
public Calculate(index: number): void {
this.UpperFractal.setValue(index, 0)
this.LowerFractal.setValue(index, 0)
const period = this.Period.value
const fIndex = index + period
if (fIndex >= this.api.Bars() - 1) {
return
}
const high = this.api.High(fIndex)
const low = this.api.Low(fIndex)
let upperFractal = 0
let lowerFractal = 0
let isUpperFractal = true
let isLowerFractal = true
for (let i = 1; i <= period; i++) {
const highLeft = this.api.High(fIndex + i)
const lowLeft = this.api.Low(fIndex + i)
const highRight = this.api.High(fIndex - i)
const lowRight = this.api.Low(fIndex - i)
if (high < highLeft || high <= highRight) {
isUpperFractal = false
}
if (low > lowLeft || low >= lowRight) {
isLowerFractal = false
}
}
if (isUpperFractal) {
upperFractal = high
}
if (isLowerFractal) {
lowerFractal = low
}
this.UpperFractal.setValue(fIndex, upperFractal)
this.LowerFractal.setValue(fIndex, lowerFractal)
}
}import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
TIndexBuffer,
TOutputWindow,
TPenStyle,
TOptionType,
TDrawStyle,
E_MAType
} from 'forex-tester-custom-indicator-api'
export default class MACD extends IndicatorImplementation {
// inputs
public FastEMAPeriod!: TOptValue_number
public SlowEMAPeriod!: TOptValue_number
public SMAPeriod!: TOptValue_number
public ApplyToPrice!: TOptValue_number
// buffers
public MACD!: TIndexBuffer
public Signal!: TIndexBuffer
public Histogram!: TIndexBuffer
private FastEMA!: TIndexBuffer
private SlowEMA!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('MACD')
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.AddLevel(0, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.FastEMAPeriod = this.api.createTOptValue_number(5)
this.SlowEMAPeriod = this.api.createTOptValue_number(13)
this.SMAPeriod = this.api.createTOptValue_number(3)
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE)
// register/set inputs
this.api.RegOption('Fast EMA Period', TOptionType.INTEGER, this.FastEMAPeriod)
this.api.SetOptionRange('Fast EMA Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegOption('Slow EMA Period', TOptionType.INTEGER, this.SlowEMAPeriod)
this.api.SetOptionRange('Slow EMA Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegOption('SMA Period', TOptionType.INTEGER, this.SMAPeriod)
this.api.SetOptionRange('SMA Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegApplyToPriceOption(this.ApplyToPrice, '')
// create buffers
this.MACD = this.api.CreateIndexBuffer()
this.Signal = this.api.CreateIndexBuffer()
this.Histogram = this.api.CreateIndexBuffer()
this.FastEMA = this.api.CreateIndexBuffer()
this.SlowEMA = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(3)
this.api.SetIndexBuffer(1, this.MACD)
this.api.SetIndexLabel(1, 'MACD')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#0000FF')
this.api.SetIndexBuffer(2, this.Signal)
this.api.SetIndexLabel(2, 'Signal Line')
this.api.SetIndexStyle(2, TDrawStyle.LINE, TPenStyle.DOT, 1, '#FF0000')
this.api.SetIndexBuffer(0, this.Histogram)
this.api.SetIndexLabel(0, 'Histogram')
this.api.SetIndexStyle(0, TDrawStyle.HISTOGRAM, TPenStyle.SOLID, 5, '#C0C0C0')
}
public Calculate(index: number): void {
this.FastEMA.setValue(
index,
this.api.GetMA(
index,
0,
this.FastEMAPeriod.value,
E_MAType.EMA,
this.ApplyToPrice.value,
this.FastEMA.getValue(index + 1)
)
)
this.SlowEMA.setValue(
index,
this.api.GetMA(
index,
0,
this.SlowEMAPeriod.value,
E_MAType.EMA,
this.ApplyToPrice.value,
this.SlowEMA.getValue(index + 1)
)
)
// calculate MACD
const macd = this.FastEMA.getValue(index) - this.SlowEMA.getValue(index)
this.MACD.setValue(index, macd)
// calculate Signal
let sum = 0
for (let i = index; i < index + this.SMAPeriod.value; i++) {
sum += this.MACD.getValue(i)
}
const signal = sum / this.SMAPeriod.value
this.Signal.setValue(index, signal)
// calculate Histogram
this.Histogram.setValue(index, macd - signal)
}
public OnParamsChange(): void {
this.api.SetBackOffsetForCalculation(
Math.max(this.FastEMAPeriod.value, this.SlowEMAPeriod.value, this.SMAPeriod.value) * 3
)
}
}import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
TIndexBuffer,
TOutputWindow,
TPenStyle,
TOptionType,
TDrawStyle
} from 'forex-tester-custom-indicator-api'
export default class RateOfChange extends IndicatorImplementation {
// inputs
public ROCPeriod!: TOptValue_number
public Source!: TOptValue_number
// buffers
public ROCBuffer!: TIndexBuffer
public override Init(): void {
this.api.IndicatorShortName('Rate of Change')
this.api.IndicatorDigits(2)
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.SetEmptyValue(Number.MAX_SAFE_INTEGER)
this.api.AddLevel(0, TPenStyle.DOT, 1, '#a8a8a8', 1)
this.api.RecalculateMeAlways()
// create inputs
this.ROCPeriod = this.api.createTOptValue_number(9)
this.Source = this.api.createTOptValue_number(TPriceType.CLOSE)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.ROCPeriod)
this.api.SetOptionRange('Period', 1, 500)
this.api.RegApplyToPriceOption(this.Source, 'Source')
// create buffers
this.ROCBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(1)
this.api.SetIndexBuffer(0, this.ROCBuffer)
this.api.SetIndexLabel(0, 'ROC')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#2962ff')
}
public override Calculate(index: number): void {
this.ROCBuffer.setValue(index, Number.MAX_SAFE_INTEGER)
if (index + this.ROCPeriod.value >= this.api.Bars()) {
return
}
const currPrice = this.api.GetPrice(index, this.Source.value)
const pastPrice = this.api.GetPrice(index + this.ROCPeriod.value, this.Source.value)
const rateOfChange = (100.0 * (currPrice - pastPrice)) / pastPrice
this.ROCBuffer.setValue(index, rateOfChange)
}
}npm installnpm run buildcannot be loaded because running scripts is disabled on this system.
For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170. At line:1 char:1Rolling VWAP built-in indicator — download the project and explore rolling Volume Weighted Average Price on the chart.
import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
export default class RollingVWAP extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
public ApplyToPrice!: TOptValue_number
public BandsMult1!: TOptValue_number
public BandsMult2!: TOptValue_number
public BandsMult3!: TOptValue_number
// buffers
public RVWAPBuffer!: TIndexBuffer
public UpperBand1Buffer!: TIndexBuffer
public LowerBand1Buffer!: TIndexBuffer
public UpperBand2Buffer!: TIndexBuffer
public LowerBand2Buffer!: TIndexBuffer
public UpperBand3Buffer!: TIndexBuffer
public LowerBand3Buffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Rolling VWAP')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(14)
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.HLC3)
this.BandsMult1 = this.api.createTOptValue_number(1.0)
this.BandsMult2 = this.api.createTOptValue_number(1.5)
this.BandsMult3 = this.api.createTOptValue_number(2.5)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegApplyToPriceOption(this.ApplyToPrice, 'Apply to price')
this.api.RegOption('Bands Mult 1', TOptionType.DOUBLE, this.BandsMult1)
this.api.SetOptionRange('Bands Mult 1', 0, Number.MAX_VALUE)
this.api.RegOption('Bands Mult 2', TOptionType.DOUBLE, this.BandsMult2)
this.api.SetOptionRange('Bands Mult 2', 0, Number.MAX_VALUE)
this.api.RegOption('Bands Mult 3', TOptionType.DOUBLE, this.BandsMult3)
this.api.SetOptionRange('Bands Mult 3', 0, Number.MAX_VALUE)
// create buffers
this.RVWAPBuffer = this.api.CreateIndexBuffer()
this.UpperBand1Buffer = this.api.CreateIndexBuffer()
this.LowerBand1Buffer = this.api.CreateIndexBuffer()
this.UpperBand2Buffer = this.api.CreateIndexBuffer()
this.LowerBand2Buffer = this.api.CreateIndexBuffer()
this.UpperBand3Buffer = this.api.CreateIndexBuffer()
this.LowerBand3Buffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(7)
this.api.SetIndexBuffer(0, this.RVWAPBuffer)
this.api.SetIndexLabel(0, 'Rolling VWAP')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#8358f3')
this.api.SetIndexBuffer(1, this.UpperBand1Buffer)
this.api.SetIndexLabel(1, 'Upper Band 1')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ffe443')
this.api.SetIndexBuffer(2, this.LowerBand1Buffer)
this.api.SetIndexLabel(2, 'Lower Band 1')
this.api.SetIndexStyle(2, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ffe443')
this.api.SetIndexBuffer(3, this.UpperBand2Buffer)
this.api.SetIndexLabel(3, 'Upper Band 2')
this.api.SetIndexStyle(3, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ff9213')
this.api.SetIndexBuffer(4, this.LowerBand2Buffer)
this.api.SetIndexLabel(4, 'Lower Band 2')
this.api.SetIndexStyle(4, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ff9213')
this.api.SetIndexBuffer(5, this.UpperBand3Buffer)
this.api.SetIndexLabel(5, 'Upper Band 3')
this.api.SetIndexStyle(5, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ff3847')
this.api.SetIndexBuffer(6, this.LowerBand3Buffer)
this.api.SetIndexLabel(6, 'Lower Band 3')
this.api.SetIndexStyle(6, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ff3847')
}
public Calculate(index: number): void {
if (index > this.api.Bars() - this.Period.value - 1) {
return
}
let cumulativeVolume = 0
let cumulativePriceVolume = 0
let cumulativeSquaredPriceVolume = 0
for (let i = index; i < index + this.Period.value; i++) {
const price = this.api.GetPrice(i, this.ApplyToPrice.value)
const volume = this.api.Volume(i)
cumulativeVolume += volume
cumulativePriceVolume += price * volume
cumulativeSquaredPriceVolume += price * price * volume
}
if (cumulativeVolume > 0) {
this.RVWAPBuffer.setValue(index, cumulativePriceVolume / cumulativeVolume)
} else {
this.RVWAPBuffer.setValue(index, this.RVWAPBuffer.getValue(index + 1))
}
const vwap = this.RVWAPBuffer.getValue(index)
const expectedSquaredPrice = cumulativeSquaredPriceVolume / cumulativeVolume
const variance = Math.max(expectedSquaredPrice - vwap * vwap, 0.0)
const stdDev = Math.sqrt(variance)
this.UpperBand1Buffer.setValue(index, this.BandsMult1.value === 0 ? 0 : vwap + stdDev * this.BandsMult1.value)
this.LowerBand1Buffer.setValue(index, this.BandsMult1.value === 0 ? 0 : vwap - stdDev * this.BandsMult1.value)
this.UpperBand2Buffer.setValue(index, this.BandsMult2.value === 0 ? 0 : vwap + stdDev * this.BandsMult2.value)
this.LowerBand2Buffer.setValue(index, this.BandsMult2.value === 0 ? 0 : vwap - stdDev * this.BandsMult2.value)
this.UpperBand3Buffer.setValue(index, this.BandsMult3.value === 0 ? 0 : vwap + stdDev * this.BandsMult3.value)
this.LowerBand3Buffer.setValue(index, this.BandsMult3.value === 0 ? 0 : vwap - stdDev * this.BandsMult3.value)
}
public OnParamsChange(): void {
this.api.SetBackOffsetForCalculation(this.Period.value)
}
}Round Numbers built-in indicator — download the project and explore psychological round-number price levels on the chart.
import {
IndicatorImplementation,
TOptValue_number,
TOptValue_LineStyle,
TPenStyle,
TOptValue_bool,
TOutputWindow,
TOptionType,
TOptionTab,
FTODate,
TObjectType,
ObjProp
} from 'forex-tester-custom-indicator-api'
interface LevelGroup {
step: number
upper: number
lower: number
style: TOptValue_LineStyle
prefix: string
stepInput: number
}
export default class RoundNumbers extends IndicatorImplementation {
public upperLevels1!: TOptValue_number
public lowerLevels1!: TOptValue_number
public levelsStep1!: TOptValue_number
public levelStyle1!: TOptValue_LineStyle
public upperLevels2!: TOptValue_number
public lowerLevels2!: TOptValue_number
public levelsStep2!: TOptValue_number
public levelStyle2!: TOptValue_LineStyle
public upperLevels3!: TOptValue_number
public lowerLevels3!: TOptValue_number
public levelsStep3!: TOptValue_number
public levelStyle3!: TOptValue_LineStyle
public showValues!: TOptValue_bool
private static readonly Prefix: string = `RoundNumbers-${crypto.randomUUID()}-`
private _lastBarTime = 0
private _levelGroups: LevelGroup[] = []
public override Init(): void {
this.api.IndicatorShortName('Round Numbers')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.HideIndicatorValueMarks()
// create inputs
this.upperLevels1 = this.api.createTOptValue_number(3)
this.lowerLevels1 = this.api.createTOptValue_number(3)
this.levelsStep1 = this.api.createTOptValue_number(200)
this.levelStyle1 = this.api.createTOptValue_LineStyle(true, '#0f1ccf', TPenStyle.SOLID, 2, false)
this.upperLevels2 = this.api.createTOptValue_number(5)
this.lowerLevels2 = this.api.createTOptValue_number(5)
this.levelsStep2 = this.api.createTOptValue_number(100)
this.levelStyle2 = this.api.createTOptValue_LineStyle(true, '#d2e716', TPenStyle.DASH, 1, false)
this.upperLevels3 = this.api.createTOptValue_number(10)
this.lowerLevels3 = this.api.createTOptValue_number(10)
this.levelsStep3 = this.api.createTOptValue_number(50)
this.levelStyle3 = this.api.createTOptValue_LineStyle(true, '#ff6600', TPenStyle.DASH_DOT, 1, false)
this.showValues = this.api.createTOptValue_bool(true)
// register/set inputs
this.api.AddSeparator('Group 1')
this.api.RegOption('Upper Levels 1', TOptionType.INTEGER, this.upperLevels1)
this.api.SetOptionRange('Upper Levels 1', 0, 50)
this.api.RegOption('Lower Levels 1', TOptionType.INTEGER, this.lowerLevels1)
this.api.SetOptionRange('Lower Levels 1', 0, 50)
this.api.RegOption('Levels Step 1', TOptionType.INTEGER, this.levelsStep1)
this.api.SetOptionRange('Levels Step 1', 0, Number.MAX_SAFE_INTEGER)
this.api.RegOption('Levels 1 Style', TOptionType.LINE, this.levelStyle1, undefined, TOptionTab.STYLE)
this.api.AddSeparator('Group 2')
this.api.RegOption('Upper Levels 2', TOptionType.INTEGER, this.upperLevels2)
this.api.SetOptionRange('Upper Levels 2', 0, 50)
this.api.RegOption('Lower Levels 2', TOptionType.INTEGER, this.lowerLevels2)
this.api.SetOptionRange('Lower Levels 2', 0, 50)
this.api.RegOption('Levels Step 2', TOptionType.INTEGER, this.levelsStep2)
this.api.SetOptionRange('Levels Step 2', 0, Number.MAX_SAFE_INTEGER)
this.api.RegOption('Levels 2 Style', TOptionType.LINE, this.levelStyle2, undefined, TOptionTab.STYLE)
this.api.AddSeparator('Group 3')
this.api.RegOption('Upper Levels 3', TOptionType.INTEGER, this.upperLevels3)
this.api.SetOptionRange('Upper Levels 3', 0, 50)
this.api.RegOption('Lower Levels 3', TOptionType.INTEGER, this.lowerLevels3)
this.api.SetOptionRange('Lower Levels 3', 0, 50)
this.api.RegOption('Levels Step 3', TOptionType.INTEGER, this.levelsStep3)
this.api.SetOptionRange('Levels Step 3', 0, Number.MAX_SAFE_INTEGER)
this.api.RegOption('Levels 3 Style', TOptionType.LINE, this.levelStyle3, undefined, TOptionTab.STYLE)
this.api.AddSeparator('')
this.api.RegOption('Show Values', TOptionType.BOOLEAN, this.showValues)
}
public override Calculate(index: number): void {
const chartInfo = this.api.GetChartInformation()
if (!chartInfo || chartInfo.firstIndex < 0 || chartInfo.lastIndex < 0) {
return
}
if (index !== chartInfo.lastIndex || index !== 0) {
return
}
const time = this.api.Time(0)
const close = this.api.Close(0)
// proceed only on new bar
if (time.valueOf() === this._lastBarTime) {
return
}
this._lastBarTime = time.valueOf()
this.removeAllLevels()
if (!this.levelStyle1.isVisible && !this.levelStyle2.isVisible && !this.levelStyle3.isVisible) {
return
}
const digits = this.api.Digits()
const drawnPrices = new Set<number>()
const priceKey = (p: number) => Math.round(p * Math.pow(10, digits))
for (const group of this._levelGroups) {
let counter = 0
for (let i = 0; i < group.upper; i++) {
const price = Math.ceil(close / group.step) * group.step + i * group.step
const key = priceKey(price)
if (!drawnPrices.has(key)) {
this.drawLine(
`${RoundNumbers.Prefix}${group.prefix}-up-${counter}`,
time,
price,
group.style.style,
group.style.width,
group.style.color,
this.showValues.value
)
drawnPrices.add(key)
counter++
}
}
counter = 0
for (let i = 0; i < group.lower; i++) {
const price = Math.floor(close / group.step) * group.step - i * group.step
const key = priceKey(price)
if (!drawnPrices.has(key)) {
this.drawLine(
`${RoundNumbers.Prefix}${group.prefix}-down-${counter}`,
time,
price,
group.style.style,
group.style.width,
group.style.color,
this.showValues.value
)
drawnPrices.add(key)
counter++
}
}
}
}
public override Done(): void {
this.removeAllLevels()
}
public override OnHide(): void {
this.removeAllLevels()
this._lastBarTime = 0
}
public override OnParamsChange(): void {
this._lastBarTime = 0
this._levelGroups = this.buildLevelGroups()
}
private buildLevelGroups(): LevelGroup[] {
const point = this.api.Point()
return [
{
step: Math.max(point, point * this.levelsStep1.value),
upper: this.upperLevels1.value,
lower: this.lowerLevels1.value,
style: this.levelStyle1,
prefix: 'Major',
stepInput: this.levelsStep1.value
},
{
step: Math.max(point, point * this.levelsStep2.value),
upper: this.upperLevels2.value,
lower: this.lowerLevels2.value,
style: this.levelStyle2,
prefix: 'Minor',
stepInput: this.levelsStep2.value
},
{
step: Math.max(point, point * this.levelsStep3.value),
upper: this.upperLevels3.value,
lower: this.lowerLevels3.value,
style: this.levelStyle3,
prefix: 'Trace',
stepInput: this.levelsStep3.value
}
]
.filter((g) => g.style.isVisible && g.stepInput !== 0)
.sort((a, b) => b.step - a.step)
}
private drawLine(
name: string,
time: FTODate,
price: number,
style: TPenStyle,
width: number,
color: string,
showValue: boolean
): void {
this.api.CreateChartObject(
name,
TObjectType.H_LINE,
0,
time,
price,
undefined,
undefined,
undefined,
undefined,
true
)
this.api.SetObjectProperty(name, ObjProp.OBJPROP_COLOR, color, true)
this.api.SetObjectProperty(name, ObjProp.OBJPROP_WIDTH, width, true)
this.api.SetObjectProperty(name, ObjProp.OBJPROP_STYLE, style, true)
this.api.SetObjectProperty(name, ObjProp.OBJPROP_SHOW_PRICE_LABEL, showValue, true)
}
private removeAllLevels(): void {
const objCount = this.api.GetObjectCount(true)
if (objCount !== null) {
for (let i = objCount - 1; i >= 0; i--) {
const name = this.api.GetObjectName(i, true)
if (name?.startsWith(RoundNumbers.Prefix)) {
this.api.RemoveChartObject(name, true)
}
}
}
}
}Supertrend built-in indicator — download the project and explore ATR-based trend-following overlay signals.
import {
IndicatorImplementation,
TOptValue_number,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle,
TPriceType
} from 'forex-tester-custom-indicator-api'
export default class Supertrend extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
public Multiplier!: TOptValue_number
public Shift!: TOptValue_number
// buffers
public UpTrendBuffer!: TIndexBuffer
public DownTrendBuffer!: TIndexBuffer
private UpperBandBuffer!: TIndexBuffer
private LowerBandBuffer!: TIndexBuffer
private DirrBuffer!: TIndexBuffer
private ATRBuffer!: TIndexBuffer
private TRBuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Supertrend')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(10)
this.Multiplier = this.api.createTOptValue_number(3)
this.Shift = this.api.createTOptValue_number(0)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegOption('Multiplier', TOptionType.DOUBLE, this.Multiplier)
this.api.SetOptionRange('Multiplier', 0.01, Number.MAX_VALUE)
this.api.RegOption('Shift', TOptionType.INTEGER, this.Shift)
// create buffers
this.UpTrendBuffer = this.api.CreateIndexBuffer()
this.DownTrendBuffer = this.api.CreateIndexBuffer()
this.UpperBandBuffer = this.api.CreateIndexBuffer()
this.LowerBandBuffer = this.api.CreateIndexBuffer()
this.DirrBuffer = this.api.CreateIndexBuffer()
this.ATRBuffer = this.api.CreateIndexBuffer()
this.TRBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(2)
this.api.SetIndexBuffer(0, this.UpTrendBuffer)
this.api.SetIndexLabel(0, 'Up Trend')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#00FF00')
this.api.SetIndexBuffer(1, this.DownTrendBuffer)
this.api.SetIndexLabel(1, 'Down Trend')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#FF0000')
}
public Calculate(index: number): void {
// ATR calculation
const high = this.api.High(index)
const low = this.api.Low(index)
if (index === this.api.Bars() - 1) {
this.TRBuffer.setValue(index, high - low)
} else {
const prevclose = this.api.GetPrice(index + 1, TPriceType.CLOSE)
this.TRBuffer.setValue(index, Math.max(high, prevclose) - Math.min(low, prevclose))
}
let atr = 0
for (let i = 0; i < this.Period.value && this.api.Bars() - index > this.Period.value; i++) {
atr += this.TRBuffer.getValue(index + i) / this.Period.value
}
this.ATRBuffer.setValue(index, atr)
// Supertrend calculation
const medianPrice = this.api.GetPrice(index, TPriceType.HL2)
let currLowerBand = medianPrice - this.Multiplier.value * this.ATRBuffer.getValue(index)
let currUpperBand = medianPrice + this.Multiplier.value * this.ATRBuffer.getValue(index)
const prevLowerBand = this.LowerBandBuffer.getValue(index + 1) || 0
const prevUpperBand = this.UpperBandBuffer.getValue(index + 1) || 0
const currClose = this.api.Close(index)
const prevClose = this.api.Close(index + 1) || currClose
currLowerBand = currLowerBand > prevLowerBand || prevClose < prevLowerBand ? currLowerBand : prevLowerBand
currUpperBand = currUpperBand < prevUpperBand || prevClose > prevUpperBand ? currUpperBand : prevUpperBand
this.LowerBandBuffer.setValue(index, currLowerBand)
this.UpperBandBuffer.setValue(index, currUpperBand)
this.DirrBuffer.setValue(index, this.DirrBuffer.getValue(index + 1) || 1)
if (currClose > currUpperBand) {
this.DirrBuffer.setValue(index, -1)
} else if (currClose < currLowerBand) {
this.DirrBuffer.setValue(index, 1)
}
if (this.DirrBuffer.getValue(index) < 0) {
this.UpTrendBuffer.setValue(index, currLowerBand)
this.DownTrendBuffer.setValue(index, 0)
} else if (this.DirrBuffer.getValue(index) > 0) {
this.DownTrendBuffer.setValue(index, currUpperBand)
this.UpTrendBuffer.setValue(index, 0)
}
}
public OnParamsChange(): void {
this.api.SetBackOffsetForCalculation(this.Period.value + 50)
this.api.SetBufferShift(0, this.Shift.value)
this.api.SetBufferShift(1, this.Shift.value)
}
}TDI (Traders Dynamic Index) built-in indicator — download the project and explore RSI-based momentum with signal lines.
import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
E_MAType,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle
} from 'forex-tester-custom-indicator-api'
export default class TDI extends IndicatorImplementation {
// inputs
public RSIPeriod!: TOptValue_number
public ApplyToPrice!: TOptValue_number
public VolatilityBand!: TOptValue_number
public RSIPriceLine!: TOptValue_number
public MAType!: TOptValue_number
public TradeSignalLine!: TOptValue_number
// buffers
public VBHighBuffer!: TIndexBuffer
public MarketBaseLineBuffer!: TIndexBuffer
public VBLowBuffer!: TIndexBuffer
public RSIPriceLineBuffer!: TIndexBuffer
public TradeSignalLineBuffer!: TIndexBuffer
// RSI buffers
public RSIBuffer!: TIndexBuffer
public AvgGainBuffer!: TIndexBuffer
public AvgLossBuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('TDI')
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.RSIPeriod = this.api.createTOptValue_number(13)
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE)
this.VolatilityBand = this.api.createTOptValue_number(34)
this.RSIPriceLine = this.api.createTOptValue_number(2)
this.MAType = this.api.createTOptValue_number(E_MAType.SMA)
this.TradeSignalLine = this.api.createTOptValue_number(7)
// register/set inputs
this.api.RegOption('RSI Period', TOptionType.INTEGER, this.RSIPeriod)
this.api.SetOptionRange('RSI Period', 2, Number.MAX_SAFE_INTEGER)
this.api.RegApplyToPriceOption(this.ApplyToPrice, 'Apply to price')
this.api.RegOption('Volatility Band', TOptionType.INTEGER, this.VolatilityBand)
this.api.SetOptionRange('Volatility Band', 1, Number.MAX_SAFE_INTEGER)
this.api.RegOption('RSI Price Line', TOptionType.INTEGER, this.RSIPriceLine)
this.api.SetOptionRange('RSI Price Line', 1, Number.MAX_SAFE_INTEGER)
this.api.RegMATypeOption(this.MAType)
this.api.RegOption('Trade Signal Line', TOptionType.INTEGER, this.TradeSignalLine)
this.api.SetOptionRange('Trade Signal Line', 1, Number.MAX_SAFE_INTEGER)
// create buffers
this.VBHighBuffer = this.api.CreateIndexBuffer()
this.MarketBaseLineBuffer = this.api.CreateIndexBuffer()
this.VBLowBuffer = this.api.CreateIndexBuffer()
this.RSIPriceLineBuffer = this.api.CreateIndexBuffer()
this.TradeSignalLineBuffer = this.api.CreateIndexBuffer()
this.RSIBuffer = this.api.CreateIndexBuffer()
this.AvgGainBuffer = this.api.CreateIndexBuffer()
this.AvgLossBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(5)
this.api.SetIndexBuffer(0, this.VBHighBuffer)
this.api.SetIndexLabel(0, 'VB High')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#0000ff')
this.api.SetIndexBuffer(1, this.MarketBaseLineBuffer)
this.api.SetIndexLabel(1, 'Market Base Line')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#ffd700')
this.api.SetIndexBuffer(2, this.VBLowBuffer)
this.api.SetIndexLabel(2, 'VB Low')
this.api.SetIndexStyle(2, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#0000ff')
this.api.SetIndexBuffer(3, this.RSIPriceLineBuffer)
this.api.SetIndexLabel(3, 'RSI Price Line')
this.api.SetIndexStyle(3, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#008000')
this.api.SetIndexBuffer(4, this.TradeSignalLineBuffer)
this.api.SetIndexLabel(4, 'Trade Signal Line')
this.api.SetIndexStyle(4, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#ff0000')
}
public Calculate(index: number): void {
// RSI calculation
if (index > this.api.Bars() - this.RSIPeriod.value - 1) {
return
}
if (index === this.api.Bars() - this.RSIPeriod.value - 1) {
this.calculateInitialRSI(index)
} else {
this.calculateCurrentRSI(index)
}
// TDI calculation
if (
this.api.Bars() - index <=
Math.max(
this.RSIPeriod.value,
this.VolatilityBand.value,
this.RSIPriceLine.value,
this.TradeSignalLine.value
)
) {
return
}
let maValue = 0
const tempRSI: number[] = []
for (let x = index; x < index + this.VolatilityBand.value; x++) {
tempRSI[x - index] = this.RSIBuffer.getValue(x)
if (this.VolatilityBand.value !== 0) {
maValue += this.RSIBuffer.getValue(x) / this.VolatilityBand.value
}
}
const stdDev = this.calculateStdDev(tempRSI, this.VolatilityBand.value)
const upZone = maValue + 1.6185 * stdDev
const downZone = maValue - 1.6185 * stdDev
this.VBHighBuffer.setValue(index, upZone)
this.VBLowBuffer.setValue(index, downZone)
this.MarketBaseLineBuffer.setValue(index, (upZone + downZone) / 2)
this.RSIPriceLineBuffer.setValue(
index,
this.MAOnBuffer(
this.RSIBuffer,
this.api.Bars(),
this.RSIPriceLine.value,
this.MAType.value,
index,
this.RSIPriceLineBuffer.getValue(index + 1)
)
)
this.TradeSignalLineBuffer.setValue(
index,
this.MAOnBuffer(
this.RSIBuffer,
this.api.Bars(),
this.TradeSignalLine.value,
this.MAType.value,
index,
this.TradeSignalLineBuffer.getValue(index + 1)
)
)
}
public OnParamsChange(): void {
const maxPeriod = Math.max(
this.RSIPeriod.value,
this.VolatilityBand.value,
this.RSIPriceLine.value,
this.TradeSignalLine.value
)
this.api.SetBackOffsetForCalculation(maxPeriod * 2)
}
private calculateInitialRSI(index: number): void {
let avgGain = 0.0,
avgLoss = 0.0
for (let i = index; i < index + this.RSIPeriod.value; i++) {
const currPrice = this.api.GetPrice(i, this.ApplyToPrice.value)
const prevPrice = this.api.GetPrice(i + 1, this.ApplyToPrice.value)
const priceDiff = this.normalizeValue(currPrice - prevPrice, this.api.Digits())
if (priceDiff > 0) {
avgGain += priceDiff / this.RSIPeriod.value
} else {
avgLoss -= priceDiff / this.RSIPeriod.value
}
}
this.AvgGainBuffer.setValue(index, avgGain)
this.AvgLossBuffer.setValue(index, avgLoss)
this.setRSIValue(index, avgGain, avgLoss)
}
private calculateCurrentRSI(index: number): void {
const currPrice = this.api.GetPrice(index, this.ApplyToPrice.value)
const prevPrice = this.api.GetPrice(index + 1, this.ApplyToPrice.value)
const priceDiff = this.normalizeValue(currPrice - prevPrice, this.api.Digits())
const currGain = priceDiff > 0 ? priceDiff : 0.0
const currLoss = priceDiff < 0 ? -priceDiff : 0.0
const prevAvgGain = this.AvgGainBuffer.getValue(index + 1)
const prevAvgLoss = this.AvgLossBuffer.getValue(index + 1)
const periodFactor = this.RSIPeriod.value - 1
const avgGain = (prevAvgGain * periodFactor + currGain) / this.RSIPeriod.value
const avgLoss = (prevAvgLoss * periodFactor + currLoss) / this.RSIPeriod.value
this.AvgGainBuffer.setValue(index, avgGain)
this.AvgLossBuffer.setValue(index, avgLoss)
this.setRSIValue(index, avgGain, avgLoss)
}
private setRSIValue(index: number, avgGain: number, avgLoss: number): void {
if (avgLoss === 0.0) {
this.RSIBuffer.setValue(index, avgGain === 0.0 ? 50.0 : 100.0)
} else {
const rs = avgGain / avgLoss
this.RSIBuffer.setValue(index, 100.0 - 100.0 / (1.0 + rs))
}
}
private normalizeValue(value: number, decimals: number): number {
const factor = Math.pow(10, decimals)
return Math.round(value * factor) / factor
}
private myVariance(data: number[], period: number): number {
let sum = 0
let ssum = 0
for (let i = 0; i < period; i++) {
sum += data[i]
ssum += Math.pow(data[i], 2)
}
return (ssum * period - sum * sum) / (period * (period - 1))
}
private calculateStdDev(data: number[], period: number): number {
return Math.sqrt(this.myVariance(data, period))
}
private MAOnBuffer(
buffer: TIndexBuffer,
total: number,
period: number,
type: E_MAType,
shift: number,
prev = 0
): number {
switch (type) {
case E_MAType.SMA: {
return this.simpleMA(buffer, shift, period, total)
}
case E_MAType.EMA: {
return this.exponentialMA(buffer, shift, period, prev)
}
case E_MAType.SMMA: {
return this.smoothedMA(buffer, shift, period, total, prev)
}
case E_MAType.LWMA: {
return this.linearWeightedMA(buffer, shift, period, total)
}
default: {
return 0
}
}
}
private simpleMA(buffer: TIndexBuffer, shift: number, period: number, total: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
for (let i = 0; i < period; i++) {
result += buffer.getValue(shift + i)
}
result /= period
}
return result
}
private exponentialMA(buffer: TIndexBuffer, shift: number, period: number, prev_value: number): number {
let result = 0.0
if (period > 0) {
const pr = 2.0 / (period + 1.0)
result = buffer.getValue(shift) * pr + prev_value * (1 - pr)
}
return result
}
private smoothedMA(buffer: TIndexBuffer, shift: number, period: number, total: number, prev_value: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
if (shift === total - period) {
for (let i = 0; i < period; i++) {
result += buffer.getValue(shift + i)
}
result /= period
}
result = (prev_value * (period - 1) + buffer.getValue(shift)) / period
}
return result
}
private linearWeightedMA(buffer: TIndexBuffer, shift: number, period: number, total: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
let sum = 0.0
let wsum = 0
for (let i = period; i > 0; i--) {
wsum += i
sum += buffer.getValue(shift + (period - i)) * i
}
result = sum / wsum
}
return result
}
}Donchian Channel built-in indicator — download the project and explore highest-high / lowest-low price channels.
import {
IndicatorImplementation,
TOptValue_number,
TIndexBuffer,
TOutputWindow,
TOptionType,
TDrawStyle,
TPenStyle,
TValueType
} from 'forex-tester-custom-indicator-api'
export default class DonchianChannel extends IndicatorImplementation {
// inputs
public Period!: TOptValue_number
public Shift!: TOptValue_number
// buffers
public UpperBuffer!: TIndexBuffer
public MiddleBuffer!: TIndexBuffer
public LowerBuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('Donchian Channel')
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW)
this.api.SetEmptyValue(0)
this.api.RecalculateMeAlways()
// create inputs
this.Period = this.api.createTOptValue_number(14)
this.Shift = this.api.createTOptValue_number(0)
// register/set inputs
this.api.RegOption('Period', TOptionType.INTEGER, this.Period)
this.api.SetOptionRange('Period', 1, 1000)
this.api.RegOption('Shift', TOptionType.INTEGER, this.Shift)
this.api.SetOptionRange('Shift', -1000, 1000)
// create buffers
this.UpperBuffer = this.api.CreateIndexBuffer()
this.MiddleBuffer = this.api.CreateIndexBuffer()
this.LowerBuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(3)
this.api.SetIndexBuffer(0, this.UpperBuffer)
this.api.SetIndexLabel(0, 'Upper')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#f7525f')
this.api.SetIndexBuffer(1, this.MiddleBuffer)
this.api.SetIndexLabel(1, 'Middle')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#9598a1')
this.api.SetIndexBuffer(2, this.LowerBuffer)
this.api.SetIndexLabel(2, 'Lower')
this.api.SetIndexStyle(2, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#38b6ff')
}
public Calculate(index: number): void {
if (index + this.Period.value > this.api.Bars()) {
return
}
const highest = this.api.GetHighestValue(TValueType.HIGH, index, this.Period.value)
const lowest = this.api.GetLowestValue(TValueType.LOW, index, this.Period.value)
this.UpperBuffer.setValue(index, highest)
this.MiddleBuffer.setValue(index, (highest + lowest) / 2)
this.LowerBuffer.setValue(index, lowest)
}
public OnParamsChange(): void {
this.api.SetBackOffsetForCalculation(this.Shift.value)
this.api.SetBufferShift(0, this.Shift.value)
this.api.SetBufferShift(1, this.Shift.value)
this.api.SetBufferShift(2, this.Shift.value)
}
}RCI (Rank Correlation Index) built-in indicator — download the project and explore rank-based momentum on the chart.
import {
IndicatorImplementation,
TOptValue_number,
TPriceType,
E_MAType,
TIndexBuffer,
TOutputWindow,
TPenStyle,
TOptionType,
TDrawStyle
} from 'forex-tester-custom-indicator-api'
export default class RCI extends IndicatorImplementation {
// inputs
public ApplyToPrice!: TOptValue_number
public RCIPeriod!: TOptValue_number
public MAType!: TOptValue_number
public MAPeriod!: TOptValue_number
// buffers
public RCIBuffer!: TIndexBuffer
public MABuffer!: TIndexBuffer
public Init(): void {
this.api.IndicatorShortName('RCI')
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW)
this.api.AddLevel(80, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.AddLevel(0, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.AddLevel(-80, TPenStyle.DOT, 1, '#ada9a9', 1)
this.api.SetEmptyValue(Number.MAX_SAFE_INTEGER)
this.api.RecalculateMeAlways()
// create inputs
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE)
this.RCIPeriod = this.api.createTOptValue_number(10)
this.MAType = this.api.createTOptValue_number(E_MAType.SMA)
this.MAPeriod = this.api.createTOptValue_number(14)
// register/set inputs
this.api.RegApplyToPriceOption(this.ApplyToPrice, '')
this.api.RegOption('RCI Period', TOptionType.INTEGER, this.RCIPeriod)
this.api.SetOptionRange('RCI Period', 1, Number.MAX_SAFE_INTEGER)
this.api.RegMATypeOption(this.MAType)
this.api.RegOption('MA Period', TOptionType.INTEGER, this.MAPeriod)
this.api.SetOptionRange('MA Period', 1, Number.MAX_SAFE_INTEGER)
// create buffers
this.RCIBuffer = this.api.CreateIndexBuffer()
this.MABuffer = this.api.CreateIndexBuffer()
// setup visible buffers
this.api.IndicatorBuffers(2)
this.api.SetIndexBuffer(0, this.RCIBuffer)
this.api.SetIndexLabel(0, 'RCI')
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 2, '#a191ee')
this.api.SetIndexBuffer(1, this.MABuffer)
this.api.SetIndexLabel(1, 'MA')
this.api.SetIndexStyle(1, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#ffdf43')
}
public Calculate(index: number): void {
// RCI calculation
if (index > this.api.Bars() - this.RCIPeriod.value) {
return
}
const prices: number[] = []
for (let i = this.RCIPeriod.value - 1; i >= 0; i--) {
prices.push(this.api.GetPrice(index + i, this.ApplyToPrice.value))
}
const rci = this.calculateRCI(this.RCIPeriod.value, prices)
this.RCIBuffer.setValue(index, rci)
// RCI-based MA calculation
if (index > this.api.Bars() - (this.MAPeriod.value + this.RCIPeriod.value) + 1) {
return
}
const ma = this.MAOnBuffer(
this.RCIBuffer,
this.api.Bars(),
this.MAPeriod.value,
this.MAType.value,
index,
this.MABuffer.getValue(index + 1)
)
this.MABuffer.setValue(index, ma)
}
public OnParamsChange(): void {
this.api.SetBackOffsetForCalculation(Math.max(this.RCIPeriod.value, this.MAPeriod.value) + 1)
}
private calculateRCI(period: number, priceSource: number[]): number {
if (period <= 1 || period > priceSource.length) {
return 0
}
const prices = priceSource.slice(-period)
const priceRanks = this.rankWithAverageTies(prices)
const barRanks = Array.from({ length: period }, (_, index) => index)
const correlation = this.calculatePearsonCorrelation(priceRanks, barRanks)
return correlation * 100
}
private rankWithAverageTies(values: number[]): number[] {
const sortedIndices = values.map((_, index) => index).sort((a, b) => values[a] - values[b])
const ranks = Array.from({ length: values.length }, () => 0)
let i = 0
while (i < sortedIndices.length) {
const start = i
const tieValue = values[sortedIndices[i]]
while (i < sortedIndices.length && values[sortedIndices[i]] === tieValue) {
i++
}
const end = i - 1
const averageRank = (start + end) / 2
for (let j = start; j <= end; j++) {
ranks[sortedIndices[j]] = averageRank
}
}
return ranks
}
private calculatePearsonCorrelation(first: number[], second: number[]): number {
if (first.length === 0 || first.length !== second.length) {
return 0
}
const count = first.length
const firstMean = first.reduce((sum, value) => sum + value, 0) / count
const secondMean = second.reduce((sum, value) => sum + value, 0) / count
let covariance = 0
let firstVariance = 0
let secondVariance = 0
for (let i = 0; i < count; i++) {
const firstCentered = first[i] - firstMean
const secondCentered = second[i] - secondMean
covariance += firstCentered * secondCentered
firstVariance += firstCentered * firstCentered
secondVariance += secondCentered * secondCentered
}
if (firstVariance <= 0 || secondVariance <= 0) {
return 0
}
return covariance / Math.sqrt(firstVariance * secondVariance)
}
private MAOnBuffer(
buffer: TIndexBuffer,
total: number,
period: number,
type: E_MAType,
shift: number,
prev = 0
): number {
switch (type) {
case E_MAType.SMA: {
return this.simpleMA(buffer, shift, period, total)
}
case E_MAType.EMA: {
return this.exponentialMA(buffer, shift, period, prev)
}
case E_MAType.SMMA: {
return this.smoothedMA(buffer, shift, period, total, prev)
}
case E_MAType.LWMA: {
return this.linearWeightedMA(buffer, shift, period, total)
}
default: {
return 0
}
}
}
private simpleMA(buffer: TIndexBuffer, shift: number, period: number, total: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
for (let i = 0; i < period; i++) {
result += buffer.getValue(shift + i)
}
result /= period
}
return result
}
private exponentialMA(buffer: TIndexBuffer, shift: number, period: number, prev: number): number {
let result = 0.0
if (period > 0) {
const pr = 2.0 / (period + 1.0)
result = buffer.getValue(shift) * pr + prev * (1 - pr)
}
return result
}
private smoothedMA(buffer: TIndexBuffer, shift: number, period: number, total: number, prev: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
if (shift === total - period) {
for (let i = 0; i < period; i++) {
result += buffer.getValue(shift + i)
}
result /= period
}
result = (prev * (period - 1) + buffer.getValue(shift)) / period
}
return result
}
private linearWeightedMA(buffer: TIndexBuffer, shift: number, period: number, total: number): number {
let result = 0.0
if (period > 0 && shift + period <= total) {
let sum = 0.0
let wsum = 0
for (let i = period; i > 0; i--) {
wsum += i
sum += buffer.getValue(shift + (period - i)) * i
}
result = sum / wsum
}
return result
}
}This page will guide you how to install Cursor IDE and how to upload your custom indicator to FTO.













TOptValue types:export default class MovingAverage extends IndicatorImplementation {
// Declaring class-level fields
public Period!: TOptValue_number;
public Shift!: TOptValue_number;
public MAtype!: TOptValue_number;
public ApplyToPrice!: TOptValue_number;
public VShift!: TOptValue_number;
Init(): void {
// Create parameters using factory method
this.Period = this.api.createTOptValue_number(8);
this.Shift = this.api.createTOptValue_number(0);
this.MAtype = this.api.createTOptValue_number(E_MAType.SMA);
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE);
this.VShift = this.api.createTOptValue_number(0);
...existing code...
}
public Init(): void {
...existing code...
// Register parameter this.Period so it's shown in the indicator settings
this.api.RegOption(
'Period',
TOptionType.INTEGER,
this.Period
);
// Setting the maximum avalable range that can be used for Period value
this.api.SetOptionRange(
'Period',
1,
Number.MAX_SAFE_INTEGER
);
// Register parameter this.Shift so it's shown in the indicator settings
this.api.RegOption(
'Shift',
TOptionType.INTEGER,
this.Shift
);
// Register parameter this.VShift so it's its shown in the indicator settings
this.api.RegOption(
'VShift',
TOptionType.INTEGER,
this.VShift
);
// Register the MA type so it has a drowdown in the indicator settings
this.api.RegMATypeOption(
this.MAtype,
'MAtype'
);
// Register the price type so it has a dropdown in the indicator settings.
this.api.RegApplyToPriceOption(
this.ApplyToPrice,
'ApplyToPrice'
);
...existing code...
}public SSMA!: TIndexBuffer
private SMA!: TIndexBufferthis.SMA = this.api.CreateIndexBuffer();
this.SSMA = this.api.CreateIndexBuffer();this.api.IndicatorBuffers(1);
this.api.SetIndexBuffer(0, this.SSMA);this.api.SetIndexLabel(0, "MA");
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#FF0000");
this.api.SetIndexDrawBegin(0, this.Period.value - 1 + this.Shift.value);public Calculate(index: number): void {
// check if the index is in the valid range
if (index + this.Period.value >= this.api.Bars()) {
return
}
// calculate the SMA value
const calculatedSMA = this.api.GetMA(
index,
0,
this.Period.value,
this.MAtype.value,
this.ApplyToPrice.value,
// here we get the value of the previous bar
this.SMA.getValue(index + 1)
)
this.SMA.setValue(index, calculatedSMA)
// set the value which is going to be displayed on the chart
this.SSMA.setValue(index, calculatedSMA + this.VShift.value * this.api.Point())
}public OnParamsChange(): void {
this.api.SetBufferShift(0, this.Shift.value)
}cannot be loaded because running scripts is disabled on this system.
For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170. At line:1 char:1import { IndicatorImplementation } from "forex-tester-custom-indicator-api";
export default class IndicatorName extends IndicatorImplementation {
// parameters
public Init(): void {
// initialization logic
}
public Calculate(index: number): void {
// calculation logic
}
public OnParamsChange(): void {
// logic after parameters change
}
public Done(): void {
// logic after finishing the calculation
}
public OnHide(): void {
// logic after hiding the indicator
}
public OnShow(): void {
// logic after showing the indicator
}
}cannot be loaded because running scripts is disabled on this system.
For more information, see about_Execution_Policies at
https:/go.microsoft.com/fwlink/?LinkID=135170. At line:1 char:1public Calculate(index: number): void {
// Skip if not enough bars to calculate moving average
if (index + this.Period.value >= this.api.Bars()) {
return
}
// Get the calculated value of the Moving Average
const calculatedSMA = this.api.GetMA(
index,
0, // Shift (usually 0)
this.Period.value, // Period for MA
this.MAtype.value, // Type of MA (SMA, EMA, etc.)
this.ApplyToPrice.value, // Price type (Close, Open, etc.)
this.SMA.getValue(index + 1) // Previous value for smoothing (optional)
)
// Save the value to the SMA buffer
this.SMA.setValue(index, calculatedSMA)
// Save a shifted version to another buffer
this.SSMA.setValue(index, calculatedSMA + this.VShift.value * this.api.Point())
}this.SSMA.setValue(index, calculatedSMA + this.VShift.value * this.api.Point())public Done(): void {
// logic after finishing the calculation
}public Done(): void {
// Draw a horizontal line based on final SMA value
const lastIndex = 0
const finalValue = this.SMA.getValue(lastIndex)
// custom method CreateHorizontalLine
this.api.CreateHorizontalLine("FinalSMA", finalValue, "red")
}export default class CustomIndicator extends IndicatorImplementation {
// Configurable parameters
public Period!: TOptValue_number;
public ShowLabels!: TOptValue_bool;
public ApplyToPrice!: TOptValue_number;
// Internal parameter (not configurable)
public internalParameter: number = 0;
public Init(): void {
// Create parameters
this.Period = this.api.createTOptValue_number(8);
this.ShowLabels = this.api.createTOptValue_bool(true);
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE);
// Register parameters so they show up in the UI
this.api.RegOption("Period", TOptionType.INTEGER, this.Period);
this.api.RegOption("ShowLabels", TOptionType.BOOLEAN, this.ShowLabels);
this.api.RegOption("ApplyToPrice", TOptionType.INTEGER, this.ApplyToPrice);
}
}public OnShow(): void {
// logic after showing the indicator
}public OnShow(): void {
// Custom method to re-draw label when the indicator is shown
this.CreateTextLabel("InfoLabel", 0, this.api.High(0), "SMA Active", "blue")
}public OnHide(): void {
// logic after hiding the indicator
}public OnHide(): void {
// Custom method to remove a label created when the indicator was shown
this.DeleteObject("InfoLabel")
}public OnParamsChange(): void {
// custom logic after parameter change
}public OnParamsChange(): void {
// DON'T DO THIS - heavy calculations
for (let i = 0; i < 1000; i++) {
let value = this.api.Close(i) * this.period.value; // ❌ Wrong!
this.mainBuffer.setValue(i, value); // ❌ Wrong!
}
// DON'T DO THIS - creating new parameters
this.newParam = this.api.createTOptValue_number(10); // ❌ Wrong!
this.api.RegOption("New Param", TOptionType.INTEGER, this.newParam); // ❌ Wrong!
}public OnParamsChange(): void {
// ✅ Correct - lightweight parameter-dependent logic
this.internalMultiplier = this.period.value * 2;
// ✅ Correct - update buffer styling based on parameters
if (this.showLine.value) {
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, this.lineColor.value);
} else {
this.api.SetIndexVisibility(0, false);
}
// ✅ Correct - reset internal state
this.calculationCounter = 0;
}export default class CustomIndicator extends IndicatorImplementation {
public period!: TOptValue_number;
public lineColor!: TOptValue_str;
public showLine!: TOptValue_bool;
public mainBuffer!: TIndexBuffer;
public OnParamsChange(): void {
// Update line color when user changes it
this.api.SetIndexStyle(
0,
TDrawStyle.LINE,
TPenStyle.SOLID,
1,
this.lineColor.value
);
// Show/hide line based on boolean parameter
this.api.SetIndexVisibility(0, this.showLine.value);
// Adjust drawing start based on period
this.api.SetIndexDrawBegin(0, this.period.value);
}
}export default class AdvancedIndicator extends IndicatorImplementation {
public fastPeriod!: TOptValue_number;
public slowPeriod!: TOptValue_number;
public validConfiguration: boolean = true;
public OnParamsChange(): void {
// Validate parameter relationship
if (this.fastPeriod.value >= this.slowPeriod.value) {
this.validConfiguration = false;
// Could log warning or set visual indicator
} else {
this.validConfiguration = true;
}
// Update internal calculation variables
this.periodDifference = this.slowPeriod.value - this.fastPeriod.value;
}
private periodDifference: number = 0;
}export default class LevelIndicator extends IndicatorImplementation {
public levelValue!: TOptValue_number;
public showLevel!: TOptValue_bool;
public OnParamsChange(): void {
// Remove existing level line
this.api.RemoveAllObjects();
// Create new level line if enabled
if (this.showLevel.value) {
// Create horizontal line at new level value
this.CreateLevelLine(this.levelValue.value);
}
}
private CreateLevelLine(value: number): void {
// Custom method to create chart objects
// Implementation depends on your specific needs
}
}!public Init(): void {
// initialization logic here
}// Declare as class-level field
public period!: TOptValue_number;
public showLine!: TOptValue_bool;
public mode!: TOptValue_str;
public Init(): void {
// Create parameters with factory methods
this.period = this.api.createTOptValue_number(14);
this.showLine = this.api.createTOptValue_bool(true);
this.mode = this.api.createTOptValue_str("Simple");
// Register parameters (required for UI visibility)
this.api.RegOption("Period", TOptionType.INTEGER, this.period);
this.api.RegOption("Show Line", TOptionType.BOOLEAN, this.showLine);
this.api.RegOption("Mode", TOptionType.STRING, this.mode);
// Optional: Set parameter constraints
this.api.SetOptionRange("Period", 1, 200);
this.api.SetOptionStep("Period", 1);
this.api.SetOptionDigits("Period", 0);
}// Declare as class-level field
public mainBuffer!: TIndexBuffer;
public Init(): void {
// 1. Register total number of buffers (MUST be called first)
this.api.IndicatorBuffers(1);
// 2. Create the buffer
this.mainBuffer = this.api.CreateIndexBuffer();
// 3. Bind buffer to index
this.api.SetIndexBuffer(0, this.mainBuffer);
// 4. Configure buffer appearance
this.api.SetIndexLabel(0, "Main Line");
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, '#FF0000');
// 5. Optional: Set drawing start point
this.api.SetIndexDrawBegin(0, 10);
}public Init(): void {
// DON'T DO THIS - heavy calculations
for (let i = 0; i < 1000; i++) {
let value = this.api.Close(i) * 2; // ❌ Wrong!
this.mainBuffer.setValue(i, value); // ❌ Wrong!
}
}public Init(): void {
// ✅ Correct - only setup and configuration
this.api.IndicatorShortName("My Indicator");
this.api.IndicatorBuffers(1);
this.mainBuffer = this.api.CreateIndexBuffer();
// ... other setup code
}
public Calculate(index: number): void {
// ✅ Correct - calculations go here
let value = this.api.Close(index) * 2;
this.mainBuffer.setValue(index, value);
}export default class MovingAverage extends IndicatorImplementation {
// Parameters - declared as class-level fields
public Period!: TOptValue_number;
public ShowLine!: TOptValue_bool;
// Buffers - declared as class-level fields
public MA!: TIndexBuffer;
public Init(): void {
// 1. Create and register parameters
this.Period = this.api.createTOptValue_number(14);
this.ShowLine = this.api.createTOptValue_bool(true);
this.api.RegOption("Period", TOptionType.INTEGER, this.Period);
this.api.RegOption("Show Line", TOptionType.BOOLEAN, this.ShowLine);
this.api.SetOptionRange("Period", 1, 9999);
this.api.SetOptionStep("Period", 1);
// 2. Set indicator properties
this.api.IndicatorShortName("Moving Average");
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW);
this.api.RecalculateMeAlways(); // Recommended
// 3. Create and configure buffers
this.api.IndicatorBuffers(1);
this.MA = this.api.CreateIndexBuffer();
this.api.SetIndexBuffer(0, this.MA);
this.api.SetIndexLabel(0, "Moving Average");
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#FF0000");
}
public Calculate(index: number): void {
// Actual calculations happen here, not in Init()
const periodValue = this.Period.value; // Access parameter value
// ... calculation logic
}
}iTime : Retrieves the timestamp of a specific bar.index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)// Declare the parameter in the class fields
public MyDateParameter!: TOptValue_DateOnly;
public Init(): void {
// Create the parameter
this.MyDateParameter = this.api.createTOptValue_DateOnly(defaultValue);
// Register the parameter
this.api.RegOption("MyDateParameter", TOptionType.DATE_ONLY, this.MyDateParameter);
}export default class DateRangeIndicator extends IndicatorImplementation {
public StartDate!: TOptValue_DateOnly
public EndDate!: TOptValue_DateOnly
public Init(): void {
// Create the parameters
const startDate = this.api.createFTODate('2024-01-01')
const endDate = this.api.createFTODate('2024-12-31'')
this.StartDate = this.api.createTOptValue_DateOnly(startDate)
this.EndDate = this.api.createTOptValue_DateOnly(endDate)
// Register the parameters
this.api.RegOption('StartDate', TOptionType.DATE_ONLY, this.StartDate)
this.api.RegOption('EndDate', TOptionType.DATE_ONLY, this.EndDate)
}
}// Declare the parameter in the class fields
public MyTimeParameter!: TOptValue_TimeOnly;
public Init(): void {
// Create the parameter
this.MyTimeParameter = this.api.createTOptValue_TimeOnly(defaultTimeValue);
// Register the parameter
this.api.RegOption("MyTimeParameter", TOptionType.TIME_ONLY, this.MyTimeParameter);
}export default class SessionIndicator extends IndicatorImplementation {
public SessionStart!: TOptValue_TimeOnly
public SessionEnd!: TOptValue_TimeOnly
public Init(): void {
// Create the parameters
this.SessionStart = this.api.createTOptValue_TimeOnly(TimeValue['09:00'])
this.SessionEnd = this.api.createTOptValue_TimeOnly(TimeValue['17:00'])
// Register the parameters
this.api.RegOption('SessionStart', TOptionType.TIME_ONLY, this.SessionStart)
this.api.RegOption('SessionEnd', TOptionType.TIME_ONLY, this.SessionEnd)
}
}public SSMA!: TIndexBuffer;this.SSMA = this.api.CreateIndexBuffer();this.api.IndicatorBuffers(1);this.api.SetIndexBuffer(0, this.SSMA);this.api.SetIndexLabel(0, "SSMA"); // Label shown in the legend
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#FF0000"); // Style
this.api.SetIndexDrawBegin(0, this.Period.value - 1 + this.Shift.value); // Starting barimport { TIndexBuffer } from "forex-tester-custom-indicator-api";
export default class MovingAverage extends IndicatorImplementation {
// Declare parameters as class fields
public Period!: TOptValue_number;
public Shift!: TOptValue_number;
public SSMA!: TIndexBuffer;
public Init(): void {
// Create parameters
this.Period = this.api.createTOptValue_number(8);
this.Shift = this.api.createTOptValue_number(0);
// Create and configure the buffer
this.SSMA = this.api.CreateIndexBuffer();
this.api.IndicatorBuffers(1);
this.api.SetIndexBuffer(0, this.SSMA);
this.api.SetIndexLabel(0, "SSMA");
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#FF0000");
this.api.SetIndexDrawBegin(0, this.Period.value - 1 + this.Shift.value);
}
}iOpen(Symbol: string, TimeFrame: number, index: number): number// Get the open price of the current bar for EURUSD on H1 timeframe
const currentOpen = this.api.iOpen("EURUSD", 60, 0);
// Get the open price from 5 bars ago
const pastOpen = this.api.iOpen("EURUSD", 60, 5);
// Calculate the difference between current and previous bar's open prices
const openDiff =
this.api.iOpen("EURUSD", 60, 0) - this.api.iOpen("EURUSD", 60, 1);
// Check if current bar opened higher than previous bar
if (this.api.iOpen("EURUSD", 60, 0) > this.api.iOpen("EURUSD", 60, 1)) {
console.log("Current bar opened higher");
}iTime(Symbol: string, TimeFrame: number, index: number): FTODate// Get the time of the current bar for EURUSD on H1 timeframe
const currentTime = this.api.iTime("EURUSD", 60, 0);
// Get the time from 5 bars ago
const pastTime = this.api.iTime("EURUSD", 60, 5);
// Calculate time difference between bars
const timeDiff =
this.api.iTime("EURUSD", 60, 0).toMilliseconds() -
this.api.iTime("EURUSD", 60, 1).toMilliseconds();
// Check if bar is from today
const now = this.api.createFTODate(Date.now());
const barTime = this.api.iTime("EURUSD", 60, 0);
const isToday =
barTime.getUTCDate() === now.getUTCDate() &&
barTime.getUTCMonth() === now.getUTCMonth() &&
barTime.getUTCFullYear() === now.getUTCFullYear();
// Get bar times for the last 3 bars
const barTimes = [];
for (let i = 0; i < 3; i++) {
barTimes.push(this.api.iTime("EURUSD", 60, i));
}iVolume(Symbol: string, TimeFrame: number, index: number): number// Get the volume of the current bar for EURUSD on H1 timeframe
const currentVolume = this.api.iVolume("EURUSD", 60, 0);
// Get the volume from 5 bars ago
const pastVolume = this.api.iVolume("EURUSD", 60, 5);
// Calculate the total volume over the last 3 bars
const totalVolume =
this.api.iVolume("EURUSD", 60, 0) +
this.api.iVolume("EURUSD", 60, 1) +
this.api.iVolume("EURUSD", 60, 2);
// Calculate average volume over last 3 bars
const avgVolume = totalVolume / 3;
// Check if current volume is higher than previous bar
if (this.api.iVolume("EURUSD", 60, 0) > this.api.iVolume("EURUSD", 60, 1)) {
console.log("Volume is increasing");
}
// Check for volume spike (2x average)
const isVolumeSpiking = this.api.iVolume("EURUSD", 60, 0) > avgVolume * 2;Volume(shift: number): number// Get current bar's volume
const currentVolume = this.api.Volume(0);
// Get previous bar's volume
const previousVolume = this.api.Volume(1);
// Calculate average volume over last 3 bars
let totalVolume = 0;
for (let i = 0; i < 3; i++) {
totalVolume += this.api.Volume(i);
}
const averageVolume = totalVolume / 3;
console.log(`Average volume over last 3 bars: ${averageVolume}`);
// Check for volume spike
if (this.api.Volume(0) > this.api.Volume(1) * 2) {
console.log("Volume spike detected on current bar");
}// Find lowest low price in last 10 bars
const lowestIndex = this.api.iLowest("EURUSD", 60, 2, 10, 0);
if (lowestIndex !== -1) {
const lowestPrice = this.api.iLow("EURUSD", 60, lowestIndex);
console.log(`Lowest price: ${lowestPrice} at index ${lowestIndex}`);
}
// Find lowest close in last 20 bars
const lowestCloseIndex = this.api.iLowest("EURUSD", 60, 3, 20, 0);
// Find lowest volume in last 5 bars
const lowestVolumeIndex = this.api.iLowest("EURUSD", 60, 4, 5, 0);
// Check if current bar is lowest in last 50 bars
const isNewLow = this.api.iLowest("EURUSD", 60, 2, 50, 0) === 0;
// Find lowest low starting from a specific bar
const startIndex = 10;
const lookback = 5;
const lowIndex = this.api.iLowest("EURUSD", 60, 2, lookback, startIndex);
// Get lowest price values for different types
const types = [0, 1, 2, 3]; // OPEN, HIGH, LOW, CLOSE
const lowestValues = types.map((type) => {
const idx = this.api.iLowest("EURUSD", 60, type, 10, 0);
return idx !== -1 ? this.api.iLow("EURUSD", 60, idx) : null;
});
// Find price channel
const highestHigh = this.api.iHigh(
"EURUSD",
60,
this.api.iHighest("EURUSD", 60, 1, 20, 0)
);
const lowestLow = this.api.iLow(
"EURUSD",
60,
this.api.iLowest("EURUSD", 60, 2, 20, 0)
);
const channelHeight = highestHigh - lowestLow;// Find highest high price in last 10 bars
const highestIndex = this.api.iHighest("EURUSD", 60, 1, 10, 0);
if (highestIndex !== -1) {
const highestPrice = this.api.iHigh("EURUSD", 60, highestIndex);
console.log(`Highest price: ${highestPrice} at index ${highestIndex}`);
}
// Find highest close in last 20 bars
const highestCloseIndex = this.api.iHighest("EURUSD", 60, 3, 20, 0);
// Find highest volume in last 5 bars
const highestVolumeIndex = this.api.iHighest("EURUSD", 60, 4, 5, 0);
// Check if current bar is highest in last 50 bars
const isNewHigh = this.api.iHighest("EURUSD", 60, 1, 50, 0) === 0;
// Find highest high starting from a specific bar
const startIndex = 10;
const lookback = 5;
const highIndex = this.api.iHighest("EURUSD", 60, 1, lookback, startIndex);
// Get highest price values for different types
const types = [0, 1, 2, 3]; // OPEN, HIGH, LOW, CLOSE
const highestValues = types.map((type) => {
const idx = this.api.iHighest("EURUSD", 60, type, 10, 0);
return idx !== -1 ? this.api.iHigh("EURUSD", 60, idx) : null;
});// Get the high price of the current bar for EURUSD on H1 timeframe
const currentHigh = this.api.iHigh("EURUSD", 60, 0);
// Get the high price from 5 bars ago
const pastHigh = this.api.iHigh("EURUSD", 60, 5);
// Calculate the highest price over the last 3 bars
const highest = Math.max(
this.api.iHigh("EURUSD", 60, 0),
this.api.iHigh("EURUSD", 60, 1),
this.api.iHigh("EURUSD", 60, 2)
);
// Check if current bar's high is a new local high
if (this.api.iHigh("EURUSD", 60, 0) > this.api.iHigh("EURUSD", 60, 1)) {
console.log("New local high formed");
}
// Calculate the average high price of last 3 bars
const avgHigh =
(this.api.iHigh("EURUSD", 60, 0) +
this.api.iHigh("EURUSD", 60, 1) +
this.api.iHigh("EURUSD", 60, 2)) /
3;// Find bar index for a specific time
const searchTime = this.api.createFTODate("2023-01-01T10:00:00Z");
const barIndex = this.api.iBarShift("EURUSD", 60, searchTime, true);
// Check if specific time exists in history
if (this.api.iBarShift("EURUSD", 60, searchTime, true) !== -1) {
console.log("Bar found for the specified time");
}
// Find nearest bar before a time
const approxIndex = this.api.iBarShift("EURUSD", 60, searchTime, false);
// Get price at specific historical time
const historicalTime = this.api.createFTODate("2023-06-01T14:30:00Z");
const index = this.api.iBarShift("EURUSD", 60, historicalTime, false);
if (index !== -1) {
const price = this.api.iClose("EURUSD", 60, index);
console.log(`Price at ${historicalTime}: ${price}`);
}
// Find bar index for current time
const now = this.api.createFTODate(Date.now());
const currentIndex = this.api.iBarShift("EURUSD", 60, now, false);// Get the close price of the current bar for EURUSD on H1 timeframe
const currentClose = this.api.iClose("EURUSD", 60, 0);
// Get the close price from 5 bars ago
const pastClose = this.api.iClose("EURUSD", 60, 5);
// Calculate the difference between current and previous bar's close prices
const closeDiff =
this.api.iClose("EURUSD", 60, 0) - this.api.iClose("EURUSD", 60, 1);
// Check if current bar closed higher than previous bar
if (this.api.iClose("EURUSD", 60, 0) > this.api.iClose("EURUSD", 60, 1)) {
console.log("Current bar closed higher");
}
// Calculate average closing price of last 3 bars
const avgClose =
(this.api.iClose("EURUSD", 60, 0) +
this.api.iClose("EURUSD", 60, 1) +
this.api.iClose("EURUSD", 60, 2)) /
3;// Get the low price of the current bar for EURUSD on H1 timeframe
const currentLow = this.api.iLow("EURUSD", 60, 0);
// Get the low price from 5 bars ago
const pastLow = this.api.iLow("EURUSD", 60, 5);
// Calculate the lowest price over the last 3 bars
const lowest = Math.min(
this.api.iLow("EURUSD", 60, 0),
this.api.iLow("EURUSD", 60, 1),
this.api.iLow("EURUSD", 60, 2)
);
// Check if current bar's low is a new local low
if (this.api.iLow("EURUSD", 60, 0) < this.api.iLow("EURUSD", 60, 1)) {
console.log("New local low formed");
}
// Calculate the average low price of last 3 bars
const avgLow =
(this.api.iLow("EURUSD", 60, 0) +
this.api.iLow("EURUSD", 60, 1) +
this.api.iLow("EURUSD", 60, 2)) /
3;
// Calculate bar range
const barRange =
this.api.iHigh("EURUSD", 60, 0) - this.api.iLow("EURUSD", 60, 0);Bars(): number// Get total number of bars
const totalBars = this.api.Bars();
console.log(`Total available bars: ${totalBars}`);
// Check if enough history for analysis
const requiredBars = 20;
if (this.api.Bars() >= requiredBars) {
// Perform analysis requiring 20 bars of history
}
// Process last 10 bars (if available)
const barsToProcess = Math.min(10, this.api.Bars());
for (let i = 0; i < barsToProcess; i++) {
const close = this.api.Close(i);
console.log(`Bar -${i} close price: ${close}`);
}
// Calculate valid shift range
const maxShift = this.api.Bars() - 1;
console.log(`Valid shift range: 0 to ${maxShift}`);Close(shift: number): number// Get current bar's closing price
const currentClose = this.api.Close(0);
// Get previous bar's closing price
const previousClose = this.api.Close(1);
// Calculate price change
const priceChange = this.api.Close(0) - this.api.Close(1);
console.log(`Price changed by ${priceChange} points`);
// Get closing prices for last 3 bars
for (let i = 0; i < 3; i++) {
const closePrice = this.api.Close(i);
console.log(`Bar -${i} close price: ${closePrice}`);
}High(shift: number): number// Get current bar's high price
const currentHigh = this.api.High(0);
// Get previous bar's high price
const previousHigh = this.api.High(1);
// Find highest price over last 3 bars
let highestPrice = this.api.High(0);
for (let i = 1; i < 3; i++) {
const high = this.api.High(i);
if (high > highestPrice) {
highestPrice = high;
}
}
console.log(`Highest price in last 3 bars: ${highestPrice}`);
// Check if current bar made new high
if (this.api.High(0) > this.api.High(1)) {
console.log("New high formed on current bar");
}Low(shift: number): number// Get current bar's low price
const currentLow = this.api.Low(0);
// Get previous bar's low price
const previousLow = this.api.Low(1);
// Find lowest price over last 3 bars
let lowestPrice = this.api.Low(0);
for (let i = 1; i < 3; i++) {
const low = this.api.Low(i);
if (low < lowestPrice) {
lowestPrice = low;
}
}
console.log(`Lowest price in last 3 bars: ${lowestPrice}`);
// Check if current bar made new low
if (this.api.Low(0) < this.api.Low(1)) {
console.log("New low formed on current bar");
}// Get current bar's time in project timezone
const currentTime = this.api.Time(0);
console.log(`Current bar time: ${currentTime.toString()}`);
// Get current bar's time in UTC
const currentTimeUTC = this.api.Time(0, TimeZoneMode.UTC);
console.log(`Current bar UTC time: ${currentTimeUTC.toString()}`);
// Get previous bar's time
const previousTime = this.api.Time(1);
// Calculate time difference between bars
const timeDiff = currentTime.getTime() - previousTime.getTime();
console.log(`Time between bars: ${timeDiff} milliseconds`);
// Get opening times for last 3 bars
for (let i = 0; i < 3; i++) {
const time = this.api.Time(i);
console.log(`Bar -${i} opened at: ${time.toString()}`);
}// Create text object
this.api.CreateChartObject('MyLabel', TObjectType.TEXT, 0, this.api.Time(0), this.api.Close(0))
// Set text content and styling
this.api.SetObjectProperty('MyLabel', ObjProp.OBJPROP_TEXT, 'Support Level')
this.api.SetObjectProperty('MyLabel', ObjProp.OBJPROP_FONTNAME, 'Arial')
this.api.SetObjectProperty('MyLabel', ObjProp.OBJPROP_FONTSIZE, 12)
this.api.SetObjectProperty('MyLabel', ObjProp.OBJPROP_COLOR, '#0000FF')
this.api.SetObjectProperty('MyLabel', ObjProp.OBJPROP_ANCHOR_POINT, AnchorPoint.CENTER)// Create rectangle
this.api.CreateChartObject(
'MyRectangle',
TObjectType.RECTANGLE,
0,
this.api.Time(10),
this.api.Close(10),
this.api.Time(0),
this.api.Close(0)
)
// Set rectangle styling
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_COLOR, '#00FF00')
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_FILLCOLOR, '#00FF0020')
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_FILLINSIDE, true)
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_WIDTH, 1)
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_BACK, true)
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_MIDDLE_LINE, true)
// Set text content and styling for rectangle
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_TEXT, 'Text')
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_FONTNAME, 'Arial')
this.api.SetObjectProperty('MyRectangle', ObjProp.OBJPROP_FONTSIZE, 12)// Create a text object with screen coordinates
this.api.CreateChartObject('FixedLabel', TObjectType.TEXT, 0, 0, 0)
// Enable screen coordinates mode
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_SCREENCOORDS, true)
// Set fixed position relative to chart corner (top-left)
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_XDISTANCE, 50) // 50 pixels from left
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_YDISTANCE, 30) // 30 pixels from top
// Set fixed size
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_XSIZE, 200) // 200 pixels wide
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_YSIZE, 40) // 40 pixels tall
// Configure text properties
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_TEXT, 'Fixed Position Label')
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_FONTNAME, 'Arial')
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_FONTSIZE, 14)
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_COLOR, '#333333')
this.api.SetObjectProperty('FixedLabel', ObjProp.OBJPROP_ANCHOR_POINT, AnchorPoint.LEFT_TOP)// Remove all objects with names starting with "MyIndicator_"
this.api.RemoveAllObjectsByPrefix("MyIndicator_");
// Remove all static objects with prefix "Label"
this.api.RemoveAllObjectsByPrefix("Label", true);
// Remove all objects with prefix from MainChart
this.api.RemoveAllObjectsByPrefix("Temp_", false, 0);GetCurrentWindowIndex(): number// Check if running in MainChart or indicator window
const windowIndex = this.api.GetCurrentWindowIndex();
if (windowIndex === 0) {
console.log("Running in MainChart");
} else {
console.log(`Running in indicator window ${windowIndex}`);
}
// Count objects in current window only
const count = this.api.GetObjectCount(false, this.api.GetCurrentWindowIndex());GetObjectCount(isStatic?: boolean, window?: number): number// Get count of regular objects
const regularCount = this.api.GetObjectCount();
console.log(`Regular objects: ${regularCount}`);
// Get count of static objects
const staticCount = this.api.GetObjectCount(true);
console.log(`Static objects: ${staticCount}`);
// Use counts in a loop
for (let i = 0; i < this.api.GetObjectCount(); i++) {
const objectName = this.api.GetObjectName(i);
console.log(`Object ${i}: ${objectName}`);
}
// Count objects in MainChart (window = 0)
const mainChartCount = this.api.GetObjectCount(false, 0);
// Count objects across all windows
const totalCount = this.api.GetObjectCount(false, -1);SetIndexDrawBegin method sets the starting bar for drawing a buffer. This is useful for indicators that require a certain number of bars to initialize before they can produce meaningful values. By setting the draw begin point, you can prevent the indicator from displaying potentially misleading values during its initialization period.SetBufferShift method sets the horizontal shift for a buffer. This allows you to offset the display of the buffer by a specified number of bars. Positive values shift the buffer to the right (into the future), while negative values shift it to the left (into the past).// Set basic text
const success1 = this.api.SetObjectText("MyLabel", "Hello World");
console.log(`Text set: ${success1}`);
// Set text with custom formatting
const success2 = this.api.SetObjectText(
"MyLabel",
"Custom Text",
14, // font size
"Arial",
0xff0000 // red color
);
console.log(`Formatted text set: ${success2}`);// Create a buffer for a moving average with display properties
const maBuffer = this.api.CreateIndexBufferWithArgs(
0, // Index
"Moving Average", // Label
TDrawStyle.LINE, // Draw as a line
TPenStyle.SOLID, // Solid line
2, // Width of 2 pixels
"#0000ff" // Blue color
);
// Calculate and store values in the buffer
for (let i = period; i < this.api.Bars(); i++) {
maBuffer[i] = calculateMA(i, period);
}// Get object coordinates
const time1 = this.api.GetObjectProperty("MyTrendLine", ObjProp.OBJPROP_TIME1);
const price1 = this.api.GetObjectProperty(
"MyTrendLine",
ObjProp.OBJPROP_PRICE1
);
console.log(`First point: Time=${time1}, Price=${price1}`);
// Get object color
const color = this.api.GetObjectProperty("MyTrendLine", ObjProp.OBJPROP_COLOR);
console.log(`Object color: ${color}`);
// Get text content
const text = this.api.GetObjectProperty("MyLabel", ObjProp.OBJPROP_TEXT);
console.log(`Label text: ${text}`);
// Mouse lock: 1 = locked, 0 = unlocked
const locked = this.api.GetObjectProperty("MyTrendLine", ObjProp.OBJPROP_LOCKED);public Init(): void {
// Set initial style with visibility
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 2, "#0000ff", true); // Initially visible
this.api.SetIndexStyle(1, TDrawStyle.NONE, TPenStyle.SOLID, 1, "#000000", false); // Initially hidden
}public Calculate(index: number): void {
// Show different buffers based on market state
if (this.isInTrendingMarket(index)) {
this.api.SetIndexVisibility(0, true); // Trend buffer
this.api.SetIndexVisibility(1, false); // Range buffer
} else {
this.api.SetIndexVisibility(0, false); // Trend buffer
this.api.SetIndexVisibility(1, true); // Range buffer
}
}public Init(): void {
// Initial buffer setup with visibility
// Buffer 0: Always visible main line
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 2, "#0000ff", true);
// Buffer 1: Initially hidden calculation buffer
this.api.SetIndexStyle(1, TDrawStyle.NONE, TPenStyle.SOLID, 1, "#000000", false);
// Buffer 2: Initially hidden, will be shown conditionally
this.api.SetIndexStyle(2, TDrawStyle.HISTOGRAM, TPenStyle.SOLID, 3, "#00ff00", false);
}
public Calculate(index: number): void {
// Algorithmic visibility control - use SetIndexVisibility
const signalStrength = this.calculateSignalStrength(index);
if (signalStrength > 0.8) {
this.api.SetIndexVisibility(2, true); // Show strong signals only
} else {
this.api.SetIndexVisibility(2, false); // Hide weak signals
}
// DON'T do this in Calculate() or any method thats not Init():
// this.api.SetIndexStyle(2, TDrawStyle.HISTOGRAM, TPenStyle.SOLID, 3, "#00ff00", true); // ❌ Wrong!
}// Add an overbought level at 70 (red line)
this.api.AddLevel(70, TPenStyle.SOLID, 1, "#ff0000", 1);
// Add an oversold level at 30 (green line)
this.api.AddLevel(30, TPenStyle.SOLID, 1, "#00ff00", 1);
// Add a middle level with a dashed line (gray line)
this.api.AddLevel(50, TPenStyle.DASH, 1, "#808080", 0.7);Counted_bars(): number// Get the number of already calculated bars
const counted = this.api.Counted_bars()
// Use it to optimize calculations
const total = this.api.Bars()
const limit = counted > 0 ? total - counted : total - 1
// Only calculate for new bars
for (let i = limit; i >= 0; i--) {
// Perform indicator calculations for bar at index i
}SetIndexDrawBegin(bufferIndex: number, paintFrom: number): void// For a 14-period moving average, don't draw the first 13 bars
this.api.SetIndexDrawBegin(0, 13);
// For a 26-period EMA, don't draw until we have enough data
this.api.SetIndexDrawBegin(0, 25);
// For MACD with 12 and 26 periods, don't draw until we have enough data for both
this.api.SetIndexDrawBegin(0, 25); // MACD line
this.api.SetIndexDrawBegin(1, 33); // Signal line (26 + 9 - 1)GetBufferInfo(index: number): TVisibleBufferInfo// Get information about buffer 0
const bufferInfo = this.api.GetBufferInfo(0);
// Log buffer properties
console.log(`Buffer Name: ${bufferInfo.name}`);
console.log(`Paint From: ${bufferInfo.paintFrom}`);
// Modify buffer visibility based on a condition
if (bufferInfo.paintFrom > 0) {
this.api.SetIndexVisibility(0, true);
} else {
this.api.SetIndexVisibility(0, false);
}IndicatorDigits(digits: number): void// Set indicator to display 2 decimal places
this.api.IndicatorDigits(2)
// For a price-based indicator on EURUSD (which typically has 5 decimal places)
this.api.IndicatorDigits(5)
// For an RSI indicator (values between 0-100)
this.api.IndicatorDigits(1)SetBufferShift(bufferIndex: number, shift: number): void// Shift buffer 0 forward by 5 bars (into the future)
this.api.SetBufferShift(0, 5)
// Shift buffer 1 backward by 3 bars (into the past)
this.api.SetBufferShift(1, -3)
// Use shifting to create a predictive indicator
const predictionPeriod = 10
this.api.SetBufferShift(0, predictionPeriod)


















// Declare the parameter in the class fields
public MyText!: TOptValue_str;
public Init(): void {
// Create the parameter
this.MyText = this.api.createTOptValue_str("default text");
// Register the parameter
this.api.RegOption("MyText", TOptionType.STRING, this.MyText);
}export default class CustomIndicator extends IndicatorImplementation {
public Name!: TOptValue_str;
public Init(): void {
this.Name = this.api.createTOptValue_str("Custom Indicator");
this.api.RegOption("Name", TOptionType.STRING, this.Name);
}
}// Declare the parameter in the class fields
public MyDateTimeParameter!: TOptValue_DateTime;
public Init(): void {
// Create the parameter
this.MyDateTimeParameter = this.api.createTOptValue_DateTime(defaultValue);
// Register the parameter
this.api.RegOption("MyDateTimeParameter", TOptionType.DATE_TIME, this.MyDateTimeParameter);
}import { IndicatorImplementation, TDrawStyle, TPenStyle, TOutputWindow, TIndexBuffer } from "forex-tester-custom-indicator-api";
export default class OBVIndicator extends IndicatorImplementation {
// Declare the buffer as a class property
public obvBuffer!: TIndexBuffer;
Init(): void {
this.api.RecalculateMeAlways();
// Set indicator name
this.api.IndicatorShortName("On Balance Volume (OBV)");
// Configure to display in separate window since OBV is an oscillator
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW);
// Register the number of buffers we'll use
this.api.IndicatorBuffers(1);
// Create and initialize the OBV buffer
this.obvBuffer = this.api.CreateIndexBuffer();
// Bind buffer to index 0
this.api.SetIndexBuffer(0, this.obvBuffer);
// Configure buffer appearance
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#0000FF"); // Blue line
this.api.SetIndexLabel(0, "OBV");
}
Calculate(index: number): void {
// If this is the first bar (rightmost/newest), initialize OBV
if (index === this.api.Bars() - 1) {
this.obvBuffer.setValue(index, this.api.Volume(index));
return;
}
// Get current and previous close prices
const currentClose = this.api.Close(index);
const previousClose = this.api.Close(index + 1);
// Get current volume
const volume = this.api.Volume(index);
// Get previous OBV value
const previousOBV = this.obvBuffer.getValue(index + 1);
let currentOBV;
// Calculate OBV based on price movement
if (currentClose > previousClose) {
// If price increased, add volume
currentOBV = previousOBV + volume;
} else if (currentClose < previousClose) {
// If price decreased, subtract volume
currentOBV = previousOBV - volume;
} else {
// If price unchanged, OBV remains the same
currentOBV = previousOBV;
}
// Set the calculated OBV value
this.obvBuffer.setValue(index, currentOBV);
}
}import {
IndicatorImplementation,
TDrawStyle,
TPenStyle,
TOutputWindow,
TIndexBuffer,
TOptionType,
TOptValue_number
} from "forex-tester-custom-indicator-api";
export default class OBVIndicator extends IndicatorImplementation {
// Declare the buffer as a class property
public obvBuffer!: TIndexBuffer;
// Declare price type parameter
public priceType!: TOptValue_number;
Init(): void {
this.api.RecalculateMeAlways();
// Set indicator name
this.api.IndicatorShortName("On Balance Volume (OBV)");
// Configure to display in separate window since OBV is an oscillator
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW);
// Initialize price type parameter
this.priceType = this.api.createTOptValue_number(0); // Default to Close price
// Register price type parameter as an enum
this.api.RegOption(
"Price Type",
TOptionType.ENUM_TYPE,
this.priceType
);
// Add price type options
this.api.AddOptionValue("Price Type", "Close");
this.api.AddOptionValue("Price Type", "Open");
this.api.AddOptionValue("Price Type", "High");
this.api.AddOptionValue("Price Type", "Low");
this.api.AddOptionValue("Price Type", "Median ((H+L)/2)");
this.api.AddOptionValue("Price Type", "Typical ((H+L+C)/3)");
// Register the number of buffers we'll use
this.api.IndicatorBuffers(1);
// Create and initialize the OBV buffer
this.obvBuffer = this.api.CreateIndexBuffer();
// Bind buffer to index 0
this.api.SetIndexBuffer(0, this.obvBuffer);
// Configure buffer appearance
this.api.SetIndexStyle(0, TDrawStyle.LINE, TPenStyle.SOLID, 1, "#0000FF"); // Blue line
this.api.SetIndexLabel(0, "OBV");
}
private getPrice(index: number): number {
switch (this.priceType.value) {
case 0: // Close
return this.api.Close(index);
case 1: // Open
return this.api.Open(index);
case 2: // High
return this.api.High(index);
case 3: // Low
return this.api.Low(index);
case 4: // Median
return (this.api.High(index) + this.api.Low(index)) / 2;
case 5: // Typical
return (this.api.High(index) + this.api.Low(index) + this.api.Close(index)) / 3;
default:
return this.api.Close(index); // Fallback to Close
}
}
Calculate(index: number): void {
// If this is the first bar (rightmost/newest), initialize OBV
if (index === this.api.Bars() - 1) {
this.obvBuffer.setValue(index, this.api.Volume(index));
return;
}
// Get current and previous prices using selected price type
const currentPrice = this.getPrice(index);
const previousPrice = this.getPrice(index + 1);
// Get current volume
const volume = this.api.Volume(index);
// Get previous OBV value
const previousOBV = this.obvBuffer.getValue(index + 1);
let currentOBV;
// Calculate OBV based on price movement
if (currentPrice > previousPrice) {
// If price increased, add volume
currentOBV = previousOBV + volume;
} else if (currentPrice < previousPrice) {
// If price decreased, subtract volume
currentOBV = previousOBV - volume;
} else {
// If price unchanged, OBV remains the same
currentOBV = previousOBV;
}
// Set the calculated OBV value
this.obvBuffer.setValue(index, currentOBV);
}
}// Declare the parameter in the class fields
public MyParameter!: TOptValue_number;
public Init(): void {
// Create the parameter
this.MyParameter = this.api.createTOptValue_number(defaultValue);
// Register the parameter
this.api.RegOption("MyParameter", TOptionType.INTEGER, this.MyParameter);
}export default class MovingAverage extends IndicatorImplementation {
public Period!: TOptValue_number;
public Shift!: TOptValue_number;
public MAtype!: TOptValue_number;
public ApplyToPrice!: TOptValue_number;
public VShift!: TOptValue_number;
public Init(): void {
// Create the parameter
this.Period = this.api.createTOptValue_number(8);
this.Shift = this.api.createTOptValue_number(0);
this.MAtype = this.api.createTOptValue_number(E_MAType.SMA);
this.ApplyToPrice = this.api.createTOptValue_number(TPriceType.CLOSE);
this.VShift = this.api.createTOptValue_number(0);
// Register the parameter
this.api.RegOption("Period", TOptionType.INTEGER, this.Period);
this.api.RegOption("Shift", TOptionType.INTEGER, this.Shift);
this.api.RegOption("MAtype", TOptionType.INTEGER, this.MAtype);
this.api.RegOption("ApplyToPrice", TOptionType.INTEGER, this.ApplyToPrice);
this.api.RegOption("VShift", TOptionType.INTEGER, this.VShift);
}
}public MyLineStyle!: TOptValue_LineStyle;
public Init(): void {
// Create the parameter
this.MyLineStyle = this.api.createTOptValue_LineStyle(isVisible, color, style, width, ignoreColor);
// Register the parameter
this.api.RegOption("MyLineStyle", TOptionType.LINE, this.MyLineStyle);
}export default class CustomIndicator extends IndicatorImplementation {
public LineStyle!: TOptValue_LineStyle;
public Init(): void {
this.LineStyle = this.api.createTOptValue_LineStyle(true, '#FF0000', TPenStyle.SOLID, 2, false);
this.api.RegOption("LineStyle", TOptionType.LINE, this.LineStyle);
}
public Calculate(index: number): void {
if (this.LineStyle.isVisible) {
const objName = "MyHorizontalLine";
// Remove existing object if it exists
if (this.api.DoesChartObjectExist(objName)) {
this.api.RemoveChartObject(objName);
}
// Create horizontal line object
this.api.CreateChartObject(objName, TObjectType.H_LINE, 0, undefined, this.api.Close(index));
// Apply line style properties
this.api.SetObjectProperty(objName, ObjProp.OBJPROP_COLOR, this.LineStyle.color);
this.api.SetObjectProperty(objName, ObjProp.OBJPROP_STYLE, this.LineStyle.style);
this.api.SetObjectProperty(objName, ObjProp.OBJPROP_WIDTH, this.LineStyle.width);
}
}
}iBars(Symbol: string, TimeFrame: number): number// Get total number of bars for EURUSD on H1 timeframe
const totalBars = this.api.iBars("EURUSD", 60);
// Check if enough historical data is available
const requiredBars = 100;
if (this.api.iBars("EURUSD", 60) >= requiredBars) {
console.log("Sufficient historical data available");
}
// Calculate average over all available bars
let sum = 0;
const bars = this.api.iBars("EURUSD", 60);
for (let i = 0; i < bars; i++) {
sum += this.api.iClose("EURUSD", 60, i);
}
const average = sum / bars;
// Find the oldest available bar's time
const oldestBarIndex = this.api.iBars("EURUSD", 60) - 1;
const oldestTime = this.api.iTime("EURUSD", 60, oldestBarIndex);
// Check data availability across timeframes
const m1Bars = this.api.iBars("EURUSD", 1);
const h1Bars = this.api.iBars("EURUSD", 60);
const d1Bars = this.api.iBars("EURUSD", 1440);Open(shift: number): number// Get current bar's opening price
const currentOpen = this.api.Open(0);
// Get previous bar's opening price
const previousOpen = this.api.Open(1);
// Compare current and previous opening prices
const openDiff = this.api.Open(0) - this.api.Open(1);
console.log(
`Price opened ${openDiff > 0 ? "higher" : "lower"} than previous bar`
);
// Get opening prices for last 3 bars
for (let i = 0; i < 3; i++) {
const openPrice = this.api.Open(i);
console.log(`Bar -${i} open price: ${openPrice}`);
}DoesChartObjectExist(uniqueObjectName: string, isStatic: boolean = false): boolean// Check if object exists before using it
if (this.api.DoesChartObjectExist("MyTrendLine")) {
// Object exists, safe to use
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_COLOR, 0xff0000);
} else {
console.log("Object not found");
}
// Check static object
const staticExists = this.api.DoesChartObjectExist("MyStaticLabel", true);
console.log(`Static object exists: ${staticExists}`);
// Create object only if it doesn't exist
const objectName = "UniqueObject";
if (!this.api.DoesChartObjectExist(objectName)) {
this.api.СreateChartObject(
objectName,
TObjectType.TEXT,
0,
this.api.createFTODate(Date.now()),
1.2345
);
}
// Remove object if it exists
if (this.api.DoesChartObjectExist("OldObject")) {
this.api.RemoveChartObject("OldObject");
}RemoveAllObjects(objType: TObjectType, isStatic?: boolean, window?: number): void// Remove all trend lines
this.api.RemoveAllObjects(TObjectType.TREND_LINE);
// Remove all static text labels
this.api.RemoveAllObjects(TObjectType.TEXT, true);
// Clean up all drawing objects
const objectTypes = [
TObjectType.TREND_LINE,
TObjectType.RECTANGLE,
TObjectType.TRIANGLE,
TObjectType.TEXT,
];
for (const type of objectTypes) {
this.api.RemoveAllObjects(type);
}
// Remove objects and log count
const beforeCount = this.api.GetObjectCount();
this.api.RemoveAllObjects(TObjectType.RECTANGLE);
const afterCount = this.api.GetObjectCount();
console.log(`Removed ${beforeCount - afterCount} rectangle objects`);public MyFlag!: TOptValue_bool;
public Init(): void {
// Create the parameter
this.MyFlag = this.api.createTOptValue_bool(defaultValue);
// Register the parameter
this.api.RegOption("MyFlag", TOptionType.BOOLEAN, this.MyFlag);
}export default class CustomIndicator extends IndicatorImplementation {
public IsEnabled!: TOptValue_bool;
public Init(): void {
this.IsEnabled = this.api.createTOptValue_bool(true);
this.api.RegOption("IsEnabled", TOptionType.BOOLEAN, this.IsEnabled);
}
public Calculate(index: number): void {
if (!this.IsEnabled.value) {
return;
}
// Perform calculations only if enabled
}
}// Get name of first object
const firstName = this.api.GetObjectName(0)
console.log(`First object name: ${firstName}`)
// Get name of first static object
const firstStaticName = this.api.GetObjectName(0, true)
console.log(`First static object name: ${firstStaticName}`)
// List all objects
const count = this.api.GetObjectCount()
for (let i = 0; i < count; i++) {
const name = this.api.GetObjectName(i)
const type = this.api.GetObjectType(name)
console.log(`Object ${i}: Name=${name}, Type=${type}`)
}
// List all static objects
const staticCount = this.api.GetObjectCount(true)
for (let i = 0; i < staticCount; i++) {
const name = this.api.GetObjectName(i, true)
const type = this.api.GetObjectType(name, true)
console.log(`Static object ${i}: Name=${name}, Type=${type}`)
}
// List objects from MainChart (window = 0)
const mainChartCount = this.api.GetObjectCount(false, 0)
for (let i = 0; i < mainChartCount; i++) {
const name = this.api.GetObjectName(i, false, 0)
console.log(`MainChart object ${i}: ${name}`)
}// Get text from a text label
const labelText = this.api.GetObjectText('MyLabel')
console.log(`Label text: ${labelText}`)
// Get text from a static label
const staticText = this.api.GetObjectText('MyStaticLabel', true)
console.log(`Static label text: ${staticText}`)
// List all text objects with their content
const count = this.api.GetObjectCount()
for (let i = 0; i < count; i++) {
const name = this.api.GetObjectName(i)
if (this.api.GetObjectType(name) === TObjectType.TEXT) {
const text = this.api.GetObjectText(name)
console.log(`Text object ${name}: "${text}"`)
}
}
// Error handling example
try {
const text = this.api.GetObjectText('NonExistentObject')
} catch (error) {
console.log('Error getting object text:', error.message)
}// Get type of a specific object
const type = this.api.GetObjectType("MyTrendLine");
console.log(`Object type: ${type}`);
// Check object type
if (this.api.GetObjectType("MyLine") === TObjectType.TREND_LINE) {
console.log("Object is a trend line");
}
// List all objects with their types
const count = this.api.GetObjectCount();
for (let i = 0; i < count; i++) {
const name = this.api.GetObjectName(i);
const type = this.api.GetObjectType(name);
console.log(`Object ${name} is of type ${type}`);
}
// Check static object type
const staticType = this.api.GetObjectType("MyStaticLine", true);
if (staticType === TObjectType.V_LINE) {
console.log("Static object is a vertical line");
}// Remove a regular chart object
this.api.RemoveChartObject('MyTrendLine')
// Remove a static chart object
this.api.RemoveChartObject('MyStaticLabel', true)
// Remove object after checking existence
if (this.api.DoesChartObjectExist('MyObject')) {
this.api.RemoveChartObject('MyObject')
console.log('Object removed successfully')
}
// Remove multiple related objects
const objectPrefix = 'Signal_'
for (let i = 0; i < this.api.GetObjectCount(); i++) {
const name = this.api.GetObjectName(i)
if (name.startsWith(objectPrefix)) {
this.api.RemoveChartObject(name)
}
}// Set object coordinates
const success1 = this.api.SetObjectProperty(
"MyTrendLine",
ObjProp.OBJPROP_TIME1,
this.api.createFTODate(1641024000000)
);
const success2 = this.api.SetObjectProperty(
"MyTrendLine",
ObjProp.OBJPROP_PRICE1,
1.2
);
// Set visual properties
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_COLOR, 0xff0000); // Red color
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_STYLE, 1); // Solid line
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_WIDTH, 2); // Line width
// Set text properties
this.api.SetObjectProperty("MyLabel", ObjProp.OBJPROP_TEXT, "New Label Text");
this.api.SetObjectProperty("MyLabel", ObjProp.OBJPROP_FONTSIZE, 12);
// Set object state
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_HIDDEN, true);
// Lock from mouse move/resize (indicator code can still use MoveObject)
this.api.SetObjectProperty("MyTrendLine", ObjProp.OBJPROP_LOCKED, true);// Display indicator in the main chart window (like Moving Averages, Bollinger Bands)
this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW);
// Display indicator in a separate window (like RSI, MACD, Stochastic)
this.api.SetOutputWindow(TOutputWindow.SEPARATE_WINDOW);