Only this pageAll pages
Powered by GitBook
Couldn't generate the PDF for 149 pages, generation stopped at 100.
Extend with 50 more pages.
1 of 100

FTO Indicators Docs

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...

Indicators

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...

Indicators: your trading super-vision

A quick introduction: what they do, why traders use them, and how they can help you spot market trends and signals.

If strategies are the brain and hands of trading, indicators are the eyes.They don’t open or close trades for you, but they help you see the market more clearly — drawing lines, showing averages, or highlighting trends. Think of them as your personal set of night-vision goggles: they light up what’s happening on the chart so you can decide what to do next (or let your strategy decide).In short:

  • Strategies = decision-makers (they trade)

  • Indicators = signal-givers (they show the picture)

With indicators, you can track momentum, spot entry points, or just make your charts look way cooler. Ready to add some trading super-vision? Let’s dive in!\

Tutorial: Create indicator with Cursor IDE

Introduction

Want to build your own indicator from scratch? You’re in the right place.

This tutorial walks you through creating a custom indicator using Cursor IDE.

Don’t worry if it sounds complicated — we’ll go step by step, with screenshots and examples. By the end, you’ll have your very own indicator running in FTO, powered by Cursor’s AI.

Please note that Cursor is a paid IDE, but it has a free version, the free version is limited to 50 requests and 2000 completions total.

Step 1: Install Cursor and set up the project

Get your tools ready — install Cursor IDE and prepare your project so you’re set to start building indicators.

First things first — you’ll need Cursor IDE.

If you don’t have it yet, go ahead and install it (full setup guide is right here).

Once Cursor is ready, set up your project.

If you’re new to the FTO Indicator API, don’t worry — that same guide also covers how to prep your environment so you’re good to go.

Overview

Overview

Welcome to the documentation for creating custom indicators in Forex Tester Online (FTO).

This guide is designed to help developers and strategy testers understand the structure, lifecycle, and capabilities of custom scripts written for FTO. You’ll learn how to set up your environment, build your own indicators, and access key platform features via the available API.


📌 What You’ll Find Here

  • Quick Guide Get started with setting up your development environment and creating your first indicator using a simple Moving Average example.

  • Understand the lifecycle of an indicator through core functions like Init, Calculate, Done, and more.

  • Learn how to create and manage visible buffers to display your indicator’s values on the chart.

  • Understand how to retrieve bar data, price arrays, and time series to power your custom logic.

  • Customize the behavior and appearance of indicators through parameters and configuration blocks.

  • Use external inputs to make your indicators dynamic and easily customizable by users.

  • Retrieve metadata about the active instrument, such as symbol name, point size, and tick value.

  • Use the FTODate structure to manage time-based logic and align calculations with bar timestamps.

  • Explore helper functions and additional features available to streamline development.


This documentation is for:

  • Developers building and debugging trading indicators

  • Algorithmic traders backtesting and optimizing their strategies

  • Power users looking to customize and extend FTO’s capabilities


Start with the guide and move on to the to see how it all works in action.

Set up indicator

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 prepare your environment so you can start writing your own custom indicator for FTO.

To use the custom indicator API, you’ll first need an example to work from. Don’t worry, we’ve got you covered — you can either browse a few ready-made examples here, or just grab the suggested starter file below. Think of it as your “Hello World” for indicators.

3KB
moving-average.zip
archive
Open

Why start with an example?

Jumping into custom indicators from scratch can feel like walking into a movie halfway through — you’ll miss the setup.

That’s why we suggest starting with a ready-made example:

  • You see the basic structure in action

  • You avoid silly mistakes on the first steps

  • You can tweak and play instead of reinventing the wheel

It’s faster, safer, and way more fun. Once you’re comfortable, you can always build your own indicator logic from scratch.

Open the example project in Cursor (or any IDE you like — but honestly, Cursor makes life easier).

Next, click the little terminal icon (see Picture #1) — it’s that black box where all the behind-the-scenes magic happens.

Now run:

Once the dependencies are in place, it’s time to build your project.

Pop open your terminal (still in that same black box of magic) and run:

This will compile your indicator into a nice, ready-to-use .js file.

Think of it as baking the dough you just mixed — after this step, you’ll actually have bread instead of just flour and water.

If everything goes well, you should see a fresh build appear in your dist folder. That’s your indicator, ready for action!

After building, you’ll see a fresh file named my-indicator-project.js appear in your dist folder. That’s your ready-to-use indicator, hot and shiny.

Curious about the next step? Head over to the to learn how to bring your indicator into FTO and watch it in action on a chart.

Upload indicator to FTO

This guide shows you how to take your freshly built indicator and bring it into Forex Tester Online.

f you’ve already run the npm run build command and everything went smoothly, you should now have a file called my-indicator-project.js sitting inside your dist folder. That’s the one we’re going to upload.

Open FTO, head over to your project, and in the Indicators tab hit Upload Indicator. Then just drag-and-drop your .js file into the upload area or select it manually (see Picture #1 and Picture #2).

Pro tip: Treat this file like your golden ticket — once it’s in FTO, you’ll finally get to see your indicator in action.

Picture #1
Picture #2

After uploading, your indicator will show up in the “My Indicators” dropdown menu (see Picture #3).

From here, just pick it, hit Apply, and watch it come alive on your chart.

Pro tip: If you don’t see it right away, double-check that you uploaded the .js file from your dist folder — that’s the only one FTO recognizes.

👨‍💻 Who is This For?

🚀 Ready to Begin?

Indicator Structure
Working with Buffers
Accessing Chart Data
Indicator Configuration
External Parameters
Getting Currency Information
Working with Dates and Time
Other Utilities
Setup and Installation
Moving Average Tutorial
Picture #3

Moving Average

Indicator skeleton

Download indicator examples

Not sure where to start? No worries — we’ve prepared a few ready-made indicator examples for you. Think of them as training wheels: you can try them out are indicator examples you can download

Below you’ll find three example files — grab them, test them in FTO, and use them as a base for your custom indicators.

3KB
moving-average.zip
archive
Open
2KB
indicator-skeleton.zip
archive
Open
2KB
empty-indicator.zip
archive
Open

Built-in indicators

Looking for ready-made implementations of common indicators (ATR, MACD, Supertrend, and others)? Browse the full catalog with downloads and source code in Built-in indicators.

Empty indicator

Open and Set Things Up

Sometimes Windows likes to be extra careful and won’t let you run scripts right away.

If you see an error like this:

Don’t panic — it’s just a security setting. You need to enter command Set-ExecutionPolicy RemoteSigned -Scope CurrentUser and then trynpm install and then npm run build command again.

Upload Guide
Picture #1
npm install
npm run build
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:1

Step 5: Start building

Alright, the warm-up is done — now it’s time to get your hands dirty (figuratively).

This is where we actually start writing code for your custom indicator.

Before we dive into coding, make sure your setup is ready to roll. You’ll need access to the FTO Indicator API to make things work smoothly.

Not there yet? No problem — check out the setup guide first, get your environment prepped, and then come back here. It’s a quick read and will save you a ton of head-scratching later.

For this example, we’ll let Cursor do some of the heavy lifting: we’ll ask it to create the On Balance Volume (OBV) indicator for us. We’ll start simple — with a completely empty file — so you can see the process from scratch. Later, when you’re more comfortable, you can speed things up by using some of our pre-built indicator foundations (you’ll find them in this section).

Think of this as baking your first cake from flour and sugar — once you’ve got the hang of it, you can use ready-made mixes to save time.

When we asked Cursor to create the OBV indicator, we didn’t just say “do it.” We gave it some context — included the FTO Indicator documentation and the we set up earlier. In the screenshot (Picture #1), you can see exactly which parts of the documentation Cursor decided to use. It’s like watching your AI buddy flip through the manual before giving you the best answer.

In the result, we got code below

The current logic looks solid so far, but there’s a catch: it’s locked to using the Close price only. That’s a bit limiting, right? What if someone wants to use the High, Low, or Open price instead?

No worries — let’s get Cursor to step up its game. We’ll ask it to tweak the code so users can pick their preferred price type. (See Picture #2 to follow along.)

After we asked Cursor to improve the OBV indicator, here’s what it came up with in its second response.

This version lets users choose the price type instead of being stuck with the Close price — nice upgrade! (See below.)

With that new parameter and internal method added, Cursor gives us a neat bonus: a dropdown menu for choosing the price type.

No more hardcoded Close price — now you can pick what you need right from the list. (See Picture #3 — that’s your shiny new option selector!)

And that’s it — with just two simple requests, Cursor delivered a fully working OBV indicator.

All it took was a proper setup, some clear , and a bit of documentation magic to give it the right context.

Not bad for a few clicks, right? Less typing, more building.

Step 2: Apply Cursor Rules

Teach Cursor your custom rules so it knows how to handle indicators smarter and faster.

Cursor has a Rules feature — basically a way to teach the AI how you want it to behave when generating code.

Think of it as house rules for a guest: “shoes off, fridge is fair game, don’t touch the cat.”

By setting up rules that explain how indicators work in FTO, you’re giving Cursor the context it needs to create consistent, accurate code. No more random guesses — just well-informed suggestions.

To set global rules:

  1. Click the Settings icon (see Picture #1)

Picture 1
  1. From the Settings menu, click on Rules (see Picture #2).

This is where you’ll drop in the custom guidelines that will teach Cursor how to behave when working with your indicators.

Think of this as setting the ground rules before starting a board game — once everyone knows the rules, things run smoother (and there’s less arguing).\

  1. In the User Rules field (see Picture #3), paste in the contents of the file called Rules.txt.

That’s your playbook — the set of instructions that teaches Cursor how to “think” when working with indicators in FTO.

Pro tip: It’s like giving your AI a cheat sheet before the test. The more detail you put in here, the less Cursor will bother you later with questions.

Recommendations

Some quick tips to get the most out of Cursor and your docs

Remember the documentation we uploaded back in Step 3? Now it’s time to make Cursor use it like a pro.

Here’s how:

  • Switch Cursor to Ask Mode (it’s the one for questions and answers).

  • In your request, type @ and choose Docs from the drop-down menu (see Picture #1).

This tells Cursor, “Hey, use the official FTO indicator docs for this answer.”

It’s like giving your AI a quick shot of espresso before it writes code — smarter, faster, and right on point.

From that drop-down menu, choose FTO Indicator documentation — the same one we uploaded back in Step 3 (see Picture #2).

This basically tells Cursor, “Hey, use the official playbook for this answer.” It’s like handing your AI the cheat sheet before an exam — fewer mistakes, better results.

Cursor needs to know which file to use for your indicator. You can simply drag and drop it straight from the Explorer panel (on the left) into the chat window, or use the @ menu, scroll down to Files & Folders, and select it from the list (see Picture #3). It’s like pointing Cursor to the right canvas before asking it to paint — now it knows exactly where to put the code.

Once you’ve selected the docs and the right file, Cursor has everything it needs to work its magic. From here, it’s just you, the AI, and some clean indicator-building fun. Time to let the code flow!

Step 4: Getting familiar with Cursor

Get to know the Cursor AI panel — the control room for your indicator magic. We’ll guide you through its modes, buttons, and little tricks to speed up your workflow (and keep things fun).

To open the Cursor AI panel, press Ctrl+L or click the icon in the top right corner of the Cursor IDE (see Picture #1).

Picture 1

This will open the chat interface — your command center for talking to the AI (yes, it listens — and it’s surprisingly helpful). In Cursor version 1.3.9, there are three available modes: Agent, Ask, and Background (see Picture #2).

Picture 2

Choosing Your AI Mode

Each one plays a different role in your coding adventure.

Agent Mode

Think of it as your AI pair programmer. It sees your project, suggests edits, and you can thumbs-up or thumbs-down its ideas like a benevolent overlord.

Ask Mode

Your Q&A corner. Need to know how a moving average works? Why your code screams in red? Ask away.

Background Mode

This one works behind the scenes. Cursor quietly watches your code and offers suggestions only when relevant. It won’t interrupt you — unless it has something genuinely useful to say. The real magic? You can feed Cursor extra context from your files or docs — like giving it caffeine for smarter replies.

For now, select Agent Mode.

Next to the mode selector, you’ll also see an AI model dropdown. Just leave it on Auto for now — let Cursor pick the right brain for the job (see Picture #3).

Quick warning:

Skip Gemini. Cursor and Gemini aren’t exactly BFFs — context issues, buggy behavior, you name it. If you do want to pick a model manually, stick to Claude or OpenAI. Cursor was trained to jam with them, and the experience is smooth.

Once you’ve taken this quick tour, you’re all set to dive into building your first custom indicator. Next step — sleeves up (real or metaphorical) and let’s create something awesome for FTO.\

Step 3: Upload FTO indicator documentation to Cursor

In this step, we’ll give Cursor its “textbook” — the official FTO indicator documentation.

Now that Cursor knows the rules, let’s give it the actual manual.

By uploading the official FTO indicator documentation, Cursor will have a knowledge base it can use to generate better code for you. No more guessing, no more “hmm, what does this method do?” — Cursor will actually know.

Here’s how to do it:

  1. Click the Settings icon in the top right corner of Cursor IDE (see Picture #1).

    Think of this step as handing Cursor the instruction booklet — without it, it’s like trying to build IKEA furniture without the tiny hex key.

2. Next stop: Features.

Think of it as the secret drawer where all the cool settings live. Click on Features in the left sidebar — that’s where you’ll find everything you need to feed Cursor the right docs. (See Picture #2.)

  1. Almost there! Scroll down the Features page until you spot the Docs section — that’s your gateway to feeding Cursor some serious brain food. (See Picture #3.)

  1. Time to feed Cursor some serious knowledge! Hit the “Add new doc” button and paste this link: https://fto-2.gitbook.io/fto-indicators-docs.

    This tells Cursor, “Here’s everything you need to know about indicators in FTO — study up!” (See Picture #4.)

Once you add the link, Cursor will take a moment to index the docs. Grab a coffee (or stretch your fingers) while it works.

After that, Cursor will have all the smarts it needs to reference the documentation and help you craft your shiny new indicators like a pro.

Tutorial: Moving Average

Learn how to create and run a simple Moving Average indicator in FTO — perfect for getting started with custom indicators.

Ready to create your first custom indicator? Let’s start with a classic: the Moving Average.

In this tutorial, we’ll walk step by step through building, coding, and running this popular indicator in FTO.

Don’t worry — even if you’re new to indicators, this one’s a great warm-up. By the end, you’ll have a working Moving Average ready to test and tweak on your charts.

To see how things work in practice, start by downloading the archive with the indicator example.

Once it’s on your computer, open it in any IDE you like — but we highly recommend Cursor IDE for the smoothest experience.

For more details on how to open it in Cursor IDE, you can refer to this .

You can download the archive of Moving Average from here

3KB
moving-average.zip
archive
Open

How Custom Indicators Are Built

Every custom indicator in FTO starts with a simple but powerful idea: extend the IndicatorImplementation class from the forex-tester-custom-indicator-api library.

This class gives you all the building blocks you need — methods, structures, and tools — so you can focus on your logic instead of reinventing the wheel.

You can see a working version of this in action in the Moving Average example, which we described earlier in the section. It’s a great place to peek at the code and understand how everything fits together.

Every custom indicator can have its own set of adjustable parameters — things like periods, colors, or calculation methods.

These parameters are defined using the class, which handles different types (numbers, booleans, lists, etc.) and makes them easy to edit.

When you add or edit your indicator in FTO, these parameters will appear in a user-friendly window where you (or your users) can tweak the settings to get just the right behavior.

Think of it as the dashboard for your indicator — all the knobs and switches in one place.

For this, they need to be registered in the function.

Defining parameters is only half the story — to actually use them in your indicator, they need to be registered inside the function. This is where you “introduce” your parameters to FTO so they’ll show up in the indicator settings window.

Without registration, your parameters will just sit quietly in the code, never making it to the user interface.

Think of Init as the guest list at a party — if you don’t put your parameter on the list, it won’t get through the door.

You can see the methods for registering parameters in the

All the methods you’ll need to register your parameters live in the section of the API.

That’s the place where FTO tells you: “Here’s how to make your parameters visible in the UI and ready for users to tweak.”

So whenever you’re setting up parameters in the Init function, just look back at those definitions — they’re your cheat sheet for getting everything hooked up the right way.

Tip: Without registration, parameters stay hidden in the code. With registration, they become clickable, adjustable settings in the indicator window.

are the backstage crew of your indicator.

They’re used to store all those calculated values and then display them as lines, histograms, or other visuals right on your chart.

Think of them as the buckets where your indicator keeps its data before painting it onto the screen.

No buffers = no output. With buffers, your calculations finally become visible magic on the chart.

Buffers need to be both declared as class fields and initialized inside the function.

Creating buffers is just the start — now you need to tell FTO how many buffers will actually show up on the chart, and then bind each one by its index (starting from 0).

Each index must be unique, like seat numbers in a cinema.

In this example, we only need one buffer, so the setup looks like this:

  • IndicatorBuffers(1) → tells FTO, “I’ve got one buffer to draw.”

  • SetIndexBuffer(0, this.SSMA) → assigns our buffer SSMA to slot #0 on the chart.

Tip: If you ever add more buffers later (say, for multiple lines), just increase the number in IndicatorBuffers() and assign each one to its own index.

Each registered buffer isn’t just a data bucket — you can also how it’s displayed on the chart.

For example:

  • SetIndexLabel(0, "MA") → gives your buffer a friendly name (“MA”) so it shows up in the legend.

  • SetIndexStyle(...) → sets how it looks: here it’s a solid red line with thickness 1.

  • SetIndexDrawBegin(...) → defines the point on the chart where drawing should start (usually after enough data has been collected).

Tip: Don’t be afraid to play with colors, line styles, and labels. It’s your indicator — make it not only useful but also easy to read at a glance.

Inside the function, you can use a handy setting called . By default, FTO calculates each buffer index only once to save resources. That’s efficient, but sometimes it means your indicator won’t be as accurate as you’d like.

If your calculations aren’t too heavy for the processor, we recommend always enabling this option. It ensures your indicator updates on every tick, keeping results fresh and reliable.

Tip: Think of it as “live mode” for your indicator — better accuracy in exchange for a tiny bit more CPU work.

Every indicator deserves an identity. With ., you set the name that will show up in the indicator settings window and the context menu.

Think of it as the little nametag your indicator wears — something short, clear, and easy to recognize when you’re scrolling through the list.

Tip: Keep it simple (like “Moving Average” or “OBV”) so you instantly know what it is at a glance.

By default, some indicators want to open their own little subwindow under the main chart. But for a Moving Average, it makes much more sense to see it directly on the main chart, hugging the price candles where it belongs.

That’s where comes in — it tells FTO exactly where to draw your indicator. In this case, we point it to the main chart so the Moving Average is right there in the action.

Tip: If you’re building other indicators (like oscillators), you can send them to their own window instead. For Moving Average, keep it on the main chart for the clearest view.

Sometimes indicators calculate values that are just 0 — and honestly, those don’t tell us much. With , you can tell FTO not to draw these points at all.

That way, the chart stays clean, without random flat lines cluttering your view.

Tip: Think of it like editing out background noise from a song — you only keep the meaningful parts, so the final output looks and sounds better.

At the heart of every custom indicator is the function. This is where the real action happens — all the math, logic, and updates take place here on each tick.

Sometimes you want your indicator to react when the user changes its settings — that’s where the method comes in.

This method lets you add custom logic that runs right after parameters are updated. For example, in the Moving Average indicator it’s used to handle the horizontal shift, so the line moves correctly on the chart when the shift value changes.

OnParamsChange is especially handy if your indicator uses custom objects or needs to redraw elements whenever a parameter is tweaked.

Tip: Think of it as your indicator’s “refresh button” — it makes sure the chart always reflects the latest settings the user has chosen.

And there you have it — you’ve just built and understood your first custom indicator: the Moving Average.

From setting up parameters and buffers to configuring styles and handling recalculations, you now know the essential steps behind making an indicator come alive in FTO.

This is a solid foundation — once you’ve mastered the Moving Average, you’re ready to experiment, tweak, and create even more powerful custom tools for your trading.

Indicator examples

Ready-made examples of built-in and starter indicators — source code, downloads, and quick reference for each example.

This section collects downloadable indicator examples with source code. Use them as starting points, reference implementations, or copy-paste templates when building your own indicators.

Each page follows the same structure:

  1. What it does — short description and when to use it

  2. Download — ZIP archive ready to open in your IDE and upload to FTO

  3. Source code — key parts of the implementation

  4. Parameters — configurable options (if any)

  5. See also — links to API docs and tutorials


Example
Description

For the full catalog of built-in FTO indicators (ATR, MACD, Bollinger Bands, and more), see . To add a new starter example, copy and follow the checklist inside.


  1. Download the ZIP from the example page.

  2. Unpack and open the project in (recommended) or any IDE.

  3. Build and upload to FTO — see .

  4. Tweak parameters and logic for your use case.

For a guided walkthrough of the Moving Average, see .


  1. Prepare the indicator project and export as .zip into .

  2. Copy to your-indicator-name.md (kebab-case filename).

  3. Fill in all sections and add the page to under Indicator Examples.

Tutorial: Open and upload indicator

Ready to see your hard work come alive? This tutorial shows you how to open your indicator project and upload it into Forex Tester Online so you can test it in action.

1. Download and Install Cursor

First, grab the Cursor editor from cursor.com/downloads.

Run the installer and, when it asks about options, tick all the checkboxes — trust us, you’ll want those extras (see Picture #1).

Think of it like ordering pizza with all the toppings. The more boxes you check, the more powerful Cursor becomes.

Picture #1

2. Install Node.js and Get the Example Project

Next up, you’ll need Node.js. If you don’t already have it, grab it here: nodejs.org.

Run the installer and follow the steps — nothing fancy.

Once Node.js is ready, download the indicator example archive we’ve provided below and extract it to a convenient spot. Desktop, Downloads — anywhere you’ll remember.

Tip: Keep it somewhere easy to find — we’ll open it in Cursor soon.

Once you’ve unzipped the archive, open the Moving Average folder.

Inside, you’ll find another folder called custom_indicator — that’s where the action is.

Open it with Cursor (see Picture #2 and #3).

Tip: If you see those folders exactly as described, you’re in the right place. Cursor is now ready to load your project.

If a pop-up appears saying something like “Do you trust the authors?”, don’t panic. It’s just Cursor being cautious.

Click “Yes, I trust the authors” — this simply lets the IDE know it’s safe to open the files.

Tip: This is a normal step whenever you open a project for the first time. Cursor just wants to make sure you’re cool with the code you’re about to run.

Next, click the little terminal icon (see Picture #5) — it’s your gateway to the behind-the-scenes magic. Once it’s open, type this command and hit Enter:

This will download and set up everything your indicator needs to run. Think of it as unpacking your toolbox before you start building.

Tip: If your screen starts showing a bunch of green and white text, you’re doing great — Cursor is fetching all the right pieces.

Once the dependencies are installed, it’s showtime — open the index.ts file and start coding your indicator. This is your playground: add logic, tweak parameters, and let your creativity flow. When you’re happy with your code, it’s time to turn it into something FTO can actually use.

Open the terminal again and run:

Once the build is complete, you’ll see a shiny new file appear in your dist folder called my-indicator-project.js (or whatever name you gave it). This is your ready-to-use indicator — the one you’ll upload into Forex Tester Online.

Tip: Don’t move or rename it yet; FTO expects it just like this when you upload.

Upload this file to the FTO (Picture #6 and #7).

Once you’ve uploaded the file, your indicator will appear in the “My Indicators” dropdown menu (see Picture #8).

From here, it’s just a click away: pick your indicator, apply it, and watch it show up on your chart.

That’s a wrap!

You’ve gone from setting up Cursor to building, uploading, and running your own custom indicator in Forex Tester Online. Now you know how to prepare your environment, guide the AI, and see your work come to life on a chart. Keep experimenting, keep refining, and if you ever get stuck, teams like are there to help. Build more, debug less, and enjoy the process!

Built-in indicators

Catalog of built-in FTO indicator examples — download ready-made projects, explore source code, and use them as reference implementations.

This section lists ready-made built-in indicators shipped as downloadable ZIP archives. Each page includes a short overview, a download link, and source code you can study or adapt.

Use them when you need a working reference for a specific indicator type rather than starting from an empty template.


Indicator
Description

ATR

ATR (Average True Range) built-in indicator — download the project and explore volatility measurement on the chart.

Average True Range (ATR) measures market volatility by averaging the true range over a selected period. Traders use it for stop placement, position sizing, and volatility filters.




Bollinger Bands

Bollinger Bands built-in indicator — download the project and explore price channels based on standard deviation.

Bollinger Bands plot upper and lower bands around a moving average using standard deviation. They help identify volatility expansion, contraction, and potential mean-reversion zones.




CCI

CCI (Commodity Channel Index) built-in indicator — download the project and explore cyclical overbought/oversold levels.

The Commodity Channel Index (CCI) is an oscillator that compares the current price to its statistical average. It is often used to spot overbought and oversold conditions and short-term reversals.




Doji

Doji built-in indicator — download the project and explore Doji candlestick pattern detection on the chart.

The Doji indicator highlights candlesticks where open and close are nearly equal, signaling indecision. Useful for pattern-based entries and confluence with other signals.




Engulfing Bar

Engulfing Bar built-in indicator — download the project and explore bullish and bearish engulfing candlestick pattern detection.

The Engulfing Bar indicator marks candlestick patterns where the body of the current bar fully engulfs the previous bar’s body. It helps spot potential reversal points after a short-term move.




Fractals

Fractals built-in indicator — download the project and explore Bill Williams fractal swing high and low markers.

Fractals mark local swing highs and lows using Bill Williams’ five-bar pattern. They help identify support/resistance pivots and structure in trending markets.




MACD

MACD built-in indicator — download the project and explore Moving Average Convergence Divergence trend and momentum signals.

MACD (Moving Average Convergence Divergence) shows the relationship between two moving averages. The histogram and signal line help assess trend direction, momentum, and potential crossovers.




Rate of Change

Rate of Change built-in indicator — download the project and explore momentum measurement as the speed of price change.

Rate of Change (ROC) measures the percentage change in price over a given period. It is a momentum oscillator used to compare current momentum with historical readings.




Optionally add the main .ts file under indicator-examples/source/ for version control (ZIP remains the primary download for users).

Moving Average

Classic MA with configurable period, type, and price source

Indicator skeleton

Minimal indicator with one buffer and one parameter

Empty indicator

Blank project — fastest way to start from scratch

Available examples

How to use an example

Adding a new example

Built-in indicators
TEMPLATE.md
Cursor IDE
Tutorial: Open and upload indicator
Tutorial: Moving Average
.gitbook/assets/
TEMPLATE.md
SUMMARY.md
Picture 1
Picture 2
Picture #3
Picture 1
Picture 2
Picture 3
Picture #4
Picture #2
10KB
Rules.txt
Open
Picture #3

Choosing the Right Brain for the Job

Picture 3

Time for a Quick Fix

Cursor’s Updated Code

A Little Upgrade Goes a Long Way

Wrapping Up

Rules
Rules
Picture 1
Picture 2
Picture 3

Indicator Parameters

Registering Parameters

Where to Find Registration Methods

Buffers setup

Connecting Buffers to the Chart

Configuring Buffers

Other settings

Always Recalculate (or Not?)

Give Your Indicator a Name

Choose Where to Draw

Skip the Zeroes

Indicator's main function

Changing parameters

Wrapping Up

Setup and Installation
TOptValue
Init
Init
external parameters definition
external parameters definition
Buffers
Init
configure
Init
RecalculateMeAlways
IndicatorShortName
SetOutputWindow
SetEmptyValue
Calculate
OnParamsChange
section

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
3KB
ATR.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
3KB
BollingerBands.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
2KB
CCI.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
3KB
Doji.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
2KB
EngulfingBar.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
2KB
Fractals.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
3KB
MACD.zip
archive
Open

Tutorial: Open and upload indicator

Download

Source code

See also

Indicator structure
Set up indicator
2KB
RateOfChange.zip
archive
Open

3. Open the Project in Cursor

4. Trust the Authors

5. Open the Terminal and Install Dependencies

6. Start Writing and Build Your Indicator

If you get an error that goes like this:

Then you need to enter command Set-ExecutionPolicy RemoteSigned -Scope CurrentUser and then trynpm install and then npm run build command again.

7. Find Your Finished Indicator

4xdev.com
3KB
moving-average.zip
archive
Open
Picture #2
Picture #3
Picture #4
Picture #5
Picture #6
Picture #7
Picture #8
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!: TIndexBuffer
this.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 install
npm run build
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:1

Average True Range — measures market volatility

Price channels based on standard deviation around a moving average

Commodity Channel Index — oscillator for cyclical overbought/oversold levels

Highlights Doji candlestick patterns on the chart

Upper and lower bands from highest high and lowest low over a period

Marks bullish and bearish engulfing candlestick patterns

Bill Williams fractals — local swing highs and lows

Moving Average Convergence Divergence — trend and momentum

Momentum oscillator showing the speed of price change

Rank Correlation Index — oscillator based on price rank correlation

Rolling Volume Weighted Average Price

Draws psychological round-number price levels

Trend-following overlay based on ATR

Traders Dynamic Index — RSI-based momentum with signal lines

To add a new built-in indicator page, copy TEMPLATE.md and register it in SUMMARY.md.


  1. Open the indicator page and download the ZIP.

  2. Unpack and open the project in your IDE — see Set up indicator.

  3. Build and upload to FTO — see Tutorial: Open and upload indicator.

  4. Adjust parameters and logic for your strategy.

For starter templates (empty project, skeleton, Moving Average tutorial), see Indicator examples.


  • Indicator structure

  • Download indicator examples

Available indicators

How to use

See also

Rolling VWAP

Rolling VWAP built-in indicator — download the project and explore rolling Volume Weighted Average Price on the chart.

Rolling VWAP computes a volume-weighted average price over a rolling window. It is useful for intraday fair-value reference and mean-reversion around volume-weighted levels.


Download

3KB
RollingVWAP.zip
archive
Open

Source code

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)
    }
}

See also

  • Indicator structure

  • Set up indicator

Round Numbers

Round Numbers built-in indicator — download the project and explore psychological round-number price levels on the chart.

The Round Numbers indicator draws horizontal levels at psychologically significant prices (e.g. 1.1000, 1.1050). Traders use them as support/resistance and order-cluster zones.


Download

4KB
RoundNumbers.zip
archive
Open

Source code

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)
                }
            }
        }
    }
}

See also

  • Indicator structure

  • Set up indicator

Supertrend

Supertrend built-in indicator — download the project and explore ATR-based trend-following overlay signals.

Supertrend is a trend-following overlay that flips direction based on ATR-derived bands. It gives a simple visual of uptrend vs downtrend and potential stop/trail levels.


Download

3KB
Supertrend.zip
archive
Open

Source code

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)
    }
}

See also

  • Indicator structure

  • Set up indicator

TDI

TDI (Traders Dynamic Index) built-in indicator — download the project and explore RSI-based momentum with signal lines.

The Traders Dynamic Index (TDI) combines RSI-based momentum with fast and slow signal lines and a volatility band. It is used to gauge trend strength and timing entries in ranging or trending markets.


Download

4KB
TDI.zip
archive
Open

Source code

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
    }
}

See also

  • Indicator structure

  • Set up indicator

Donchian Channel

Donchian Channel built-in indicator — download the project and explore highest-high / lowest-low price channels.

The Donchian Channel draws upper and lower bands from the highest high and lowest low over a lookback period. It is widely used for breakout and trend-following strategies.


Download

2KB
DonchianChannel.zip
archive
Open

Source code

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)
    }
}

See also

  • Indicator structure

  • Set up indicator

RCI

RCI (Rank Correlation Index) built-in indicator — download the project and explore rank-based momentum on the chart.

The Rank Correlation Index (RCI) applies rank correlation to price over a lookback window. It oscillates between overbought and oversold zones similar to other momentum indicators.


Download

3KB
RCI.zip
archive
Open

Source code

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
    }
}

See also

  • Indicator structure

  • Set up indicator

Set up indicator

This is a separate guide on how to set up the environment to write your custom indicator for FTO.

To to use our custom indicator API, you need to first download indicator example. You can find some of the examples here or download the suggested example file below.

2KB
empty-indicator.zip
archive
Open

Open it with Cursor or any IDE of your choice — we suggest Cursor. Then, by clicking the icon shown in Picture #1, open the terminal and install the dependencies using the command npm install.

Picture #1

After installing the dependencies, build the project using the command npm run build.

If you get an error that goes like this:

Then you need to enter command Set-ExecutionPolicy RemoteSigned -Scope CurrentUser and then trynpm install and then npm run build command again.

After building the project, a file my-indicator-project.js (Indicator file name) will appear in the dist folder.

If you want to know how to upload your indicator to FTO, you can go to .

Upload indicator to FTO

This is a separate guide on how to upload your indicator to FTO.

If you have created an indicator that you want to upload and have run the npm run build command in the terminal, a file named my-indicator-project.js (indicator's file name) will appear in the dist folder. Upload this file to FTO (see Picture #1 and #2).

Picture #1
Picture #2

From there, your indicator should appear in "My Indicators" dropdown menu (Picture #3).

Picture #3

Tutorial: Open and upload indicator

This page will guide you how to install Cursor IDE and how to upload your custom indicator to FTO.

Download and install the Cursor editor: https://www.cursor.com/downloads, while installing make sure to check all the checkmarks (Picture #1)

Picture #1

Then, you have to install Node.js if you don't already have it. Use the link below. https://nodejs.org/en

After installing node.js, download the archive with the indicator example below and extract it to a convenient location.

3KB
moving-average.zip
archive
Open

Open the extracted folder "Moving Average" and inside of it you should find another folder "custom_indicator", open it with Cursor (Picture #2 and #3).

Picture #2
Picture #3

If you get a pop-up like this, click "Yes, I trust the authors"

Then, by clicking the icon shown on Picture #5 you should open the terminal and install the dependencies using the command npm install.

After installing the dependencies, you can start writing your indicator in the index.ts file.

Once the implementation is complete, you can build the project using the command npm run build.

After building the project, a file my-indicator-project.js (Indicator file name) will appear in the dist folder.

Upload this file to the FTO (Picture #6 and #7).

From there, your indicator should appear in "My Indicators" dropdown menu (Picture #8).

Tutorial: Moving Average

In this tutorial, we will look at an example implementation of the MovingAverage indicator.

To get acquainted with the implementation example, download the archive with the indicator example and open it in IDE of your choice, we suggest you use Cursor IDE.

For more details on how to open it in Cursor IDE, you can refer to this .

You can download the archive of Moving Average from here

Indicator structure

A custom indicator is built by extending the IndicatorImplementation class, provided by the forex-tester-custom-indicator-api library. You can find this implementation in the source code of the Moving Average example described in the Setup and Installation section.

import { IndicatorImplementation } from "forex-tester-custom-indicator-api";

export default class MovingAverage extends IndicatorImplementation {
  // indicator logic
}

Indicator parameters

These parameters can be of different types, they are determined by the class, and they will be displayed in the indicator addition/editing window

For this, they need to be registered in the function.

You can see the methods for registering parameters in the

are used to store and display indicator values on the chart.

They need to be declared with all class fields and initialized in the function

After their creation, you need to tell how many buffers will be displayed on the chart and bind them by index, starting from 0 (the indices must be unique) In this case, there is one buffer

Each registered buffer can be

Also, other methods for configuring the indicator are used in the function. To ensure the indicator recalculates on each tick, use the function . If this setting is not used, each buffer index will be calculated only once to save resources, but some indicators may be calculated inaccurately. If the calculations do not heavily load the processor, we recommend always using it.

Set the indicator name, which will be displayed in the indicator settings window and in the context menu: .

We want the Moving Average indicator to be displayed on the main chart, so we use .

Using this call, specify that values equal to 0 will not be drawn on the chart: .

The main function of the indicator is the function, where the indicator values are calculated on each tick.

To add custom logic after changing the indicator parameters, we use the method. In Moving average, it is applied to the horizontal shift of the indicator. This method is often used to work with custom objects or to shift the indicator.

Calculate

The Calculate method is one of the core functions used in a custom indicator. It’s responsible for calculating the logic of your indicator for each bar (candle) on the chart.

This function runs automatically on each price update (tick) and recalculates values for the given bar.


  • index — This parameter tells you which bar you’re working with.

OnParamsChange

The OnParamsChange method is called automatically when the user changes any parameter of your indicator — for example, the period, color, MA type, etc.

It allows you to respond to these changes and implement custom logic that should happen when parameters are modified.

Important: This method is NOT used for the actual parameter values to change — that happens internally. This method is only for custom logic that needs to be executed when parameters are changed.



This method is called after the user updates the settings in the indicator panel, but before the indicator is recalculated.

Done

The Done method is called once, after the indicator has finished calculating all bars. It marks the end of the calculation cycle and is typically used for final steps or cleanup.



Use Done when you need to:

  • Perform post-processing after all Calculate() calls are complete

Indicator structure

Main methods of each indicator are and , Init is called once when the indicator is created, and Calculate is called on each tick. Other methods are optional and can be used to add additional logic to the indicator.

To get familiar with other methods, you can read the documentation for each method.

Init

The Init method is called once, right when your custom indicator is created or loaded onto the chart. This is where you set everything up — like naming your indicator, creating buffers, creating parameters and registering them, and more.



This method is like a constructor for your indicator. Here's what must happen inside:

All user-configurable parameters must be:

  • Declared

Parameters

Indicator parameters are configurable settings that allow users to customize how an indicator behaves and looks. They appear in the indicator’s settings panel and can be modified without changing the code.


To make a parameter configurable, it must be created using one of the TOptValue types , created and registered inside the Init() method.

Any variable that does not extend TOptValue is considered internal and will not be visible or editable in the user interface.


OnShow

The OnShow method is called when the indicator is made visible on the chart. This includes the moment it’s re-enabled after being hidden.



Use OnShow when you need to:

  • Re-create or re-draw custom chart objects

TOptValue

This is documentation for different types of TOptValue classes which are used to create parameters for indicators.

OnHide

The OnHide method is called automatically when the indicator is hidden from the chart — for example, when the user disables its visibility.

This gives you a place to clean up any visual elements or perform other logic when the indicator is no longer visible.



Use OnHide when you want to:

  • Remove custom chart objects

Tutorial: Open and upload indicator
Tutorial: Open and upload indicator
Tutorial: Open and upload indicator
Tutorial: Open and upload indicator
Tutorial: Open and upload indicator
Tutorial: Open and upload indicator
ATR
Bollinger Bands
CCI
Doji
Donchian Channel
Engulfing Bar
Fractals
MACD
Rate of Change
RCI
Rolling VWAP
Round Numbers
Supertrend
TDI

TOptValue_LineStyle

  • TOptValue_DateTime

  • TOptValue_DateOnly

  • TOptValue_TimeOnly

  • TOptValue_str
    TOptValue_number
    TOptValue_bool

    Buffers setup

    Other settings

    Indicator's main function

    Changing parameters

    TOptValue
    Init
    external parameters definition
    Buffers
    Init
    configured
    Init
    RecalculateMeAlways
    IndicatorShortName
    SetOutputWindow
    SetEmptyValue
    Calculate
    OnParamsChange
    section
    upload guide

    OnHide

  • OnShow

  • Description of the structure

    Init
    Calculate
    OnParamsChange
    Done

    If you get an error that goes like this:

    Then you need to enter command Set-ExecutionPolicy RemoteSigned -Scope CurrentUser and then trynpm install and then npm run build command again.

    Picture #4
    Picture #5
    Picture #6
    Picture #7
    Picture #8

    index = 0 → the latest bar (rightmost on the chart).

  • index = 1 → the previous bar, and so on.

  • The method runs once per tick (price change) and calculates the indicator value only for the specified bar.



    1. Checks if there are enough bars to calculate the moving average. (We need Period number of candles; otherwise, we skip.)

    2. Calculates a Moving Average value using GetMA().

    3. Stores the result in the SMA buffer, which is used for drawing on the chart.

    4. Applies a vertical shift (VShift) and stores the result in a second buffer (SSMA).


    • This method is where you put your main logic.

    • It runs automatically for each bar.

    • You should use it to calculate and store indicator values.

    • Buffers like SMA and SSMA are how your indicator shows up visually on the chart.

    What is it?

    How It Works

    Example Explained

    What This Code Does

    In Short

    Draw or update custom chart objects that rely on full data

  • Clean up temporary data or buffers

  • Log or store final values

  • This method is especially useful if your indicator logic depends on seeing the entire dataset.



    • Done is called after all bars have been processed in Calculate().

    • It runs once per full calculation cycle — not on every tick.

    • It’s safe to use this method to add chart decorations, logs, or summary calculations.

    What is it?

    Syntax

    When and Why to Use It

    Example

    Important Notes

    Here are some of the commonly used TOptValue types:
    • TOptValue_number — numeric values (e.g., period, shift)

    • TOptValue_bool — true/false switches

    • TOptValue_string — string input



    • Only TOptValue-based parameters are configurable

    • You must register each parameter inside the Init method using this.api.RegOption

    • Parameters control how the indicator works and looks — use them for anything the user might want to tweak

    What Are Parameters?

    How It Works

    Common Parameter Types

    Example

    Key Rules

    Restore visual elements that were removed or hidden earlier
  • Trigger any logic that should happen only when the indicator is visible

  • This method is helpful when your indicator includes dynamic elements (like shapes, labels, or highlights) that should appear only when the indicator is active.



    • This method is not called during every tick or calculation — only when visibility changes.

    • If your indicator does not rely on custom objects or UI elements, you may not need to implement OnShow.

    What is it?

    Syntax

    When and Why to Use It

    Example

    Important Notes

    (labels, lines, shapes) added by the indicator
  • Free resources or stop background logic tied to visualization

  • Prepare for a clean re-render when the indicator is shown again



    • OnHide only runs when the indicator becomes hidden — not when it is removed completely.

    • It pairs naturally with OnShow, helping you manage custom visual elements.

    What is it?

    Syntax

    When and Why to Use It

    Example

    Important Notes

    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!: TIndexBuffer
    this.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:1
    import { 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:1
    public 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")
    }
  • Recalculate internal values based on new parameter values

  • Adjust or re-create custom chart objects (lines, labels, etc.)

  • Update dependencies between parameters

  • Perform validation or parameter-based setup

  • Reset internal state that depends on parameters

  • Apply dynamic styling or configuration changes

    • You only need basic parameter changes (values update automatically)

    • You don't have any custom logic dependent on parameter changes

    • Your indicator works fine with just Init() and Calculate()


    • Modify buffer properties (styles, labels, visibility)

    • Update buffer configuration based on parameter changes

    • Reconfigure drawing styles and colors

    • Adjust buffer drawing ranges with

    • Create, modify, or remove custom chart objects

    • Update object positions based on parameter changes

    • Reconfigure object properties (colors, styles, positions)

    • Manage dynamic visual elements

    • Reset calculation counters or accumulators

    • Update lookup tables or cached calculations

    • Reconfigure internal algorithms based on parameters

    • Initialize parameter-dependent variables


    • Heavy calculations or complex mathematical operations

    • Per-bar data processing (this should be in Calculate())

    • Modifying the actual parameter values (handled internally)

    • Creating new parameters (must be done in )

    • Changing buffer count with (must be in )

    • Loops through large datasets or historical data processing



    • Keep it lightweight - only essential parameter-dependent logic

    • Use it for setup/configuration that depends on parameters

    • Reset internal state when parameters change

    • Update visual properties based on parameter values

    • Validate parameter combinations if needed

    • Heavy computational work (belongs in Calculate())

    • Complex loops or data processing

    • Modifying parameter values

    • Creating new parameters or buffers (belongs in Init())


    You can skip implementing this method if:

    • Your indicator only uses basic parameters for calculations

    • No custom logic is needed when parameters change

    • All parameter-dependent behavior happens in Calculate()

    • You don't have dynamic visual elements or chart objects


    1. OnParamsChange() is optional - only implement if you need custom logic

    2. Keep it lightweight - no heavy calculations or data processing

    3. Parameter values update automatically - this method is for additional logic only

    4. Use it for configuration and setup that depends on parameter values

    5. Cannot create new parameters or buffers - only modify existing ones

    6. Runs before recalculation - perfect for resetting internal state

    7. Great for dynamic visuals - updating styles, objects, and display properties


    Think of OnParamsChange() as your "parameter change reaction" method. It's the perfect place to update anything that depends on parameter values but doesn't belong in the per-bar Calculate() method. Keep it fast and focused on configuration rather than calculation.

    public OnParamsChange(): void {
        // custom logic after parameter change
    }

    What is it?

    Syntax

    When and Why to Use It

    Use OnParamsChange() When You Need To:

    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
      }
    }

    Don't Use OnParamsChange() If:

    What CAN Be Done in OnParamsChange

    Buffer and Display Updates

    Chart Object Management

    Internal State Management

    What CANNOT Be Done in OnParamsChange

    ❌ Forbidden Operations

    ❌ Wrong Approach

    ✅ Correct Approach

    Practical Examples

    Example 1: Dynamic Buffer Styling

    Example 2: Parameter Validation and Dependencies

    Example 3: Chart Object Management

    Best Practices

    ✅ Do This

    ❌ Avoid This

    When NOT to Implement OnParamsChange()

    Key Rules Summary

    Pro Tip

    as class-level fields using the
    !
    syntax
  • Created in Init() using appropriate factory methods

  • Registered using this.api.RegOption() to appear in the UI

  • Buffers determine visualization and must be properly set up:

    • Name: this.api.IndicatorShortName("Your Indicator Name")

    • Output Window: this.api.SetOutputWindow(TOutputWindow.CHART_WINDOW) or TOutputWindow.SEPARATE_WINDOW


    • this.api.IndicatorShortName(name) - Set indicator display name

    • this.api.IndicatorBuffers(count) - Register number of buffers

    • this.api.CreateIndexBuffer() - Create buffer instances

    • this.api.SetIndexBuffer(index, buffer) - Bind buffers to indexes

    • this.api.RegOption(name, type, parameter) - Register parameters

    • this.api.SetIndexLabel(index, label) - Set buffer legend label

    • this.api.SetIndexStyle(index, drawStyle, penStyle, width, color) - Set visual style

    • this.api.SetIndexDrawBegin(index, startBar) - Set drawing start point

    • this.api.SetIndexVisibility(index, visible) - Control buffer visibility

    • this.api.SetIndexChartOverlay(index, chartOverlay) - For oscillators: draw buffer on main chart instead of subwindow

    • this.api.SetOptionRange(name, min, max) - Set parameter limits

    • this.api.SetOptionStep(name, step) - Set UI step for numeric options

    • this.api.SetOptionDigits(name, digits) - Set decimal places

    • this.api.AddOptionValue(name, value) - Add dropdown options

    • this.api.RecalculateMeAlways() - Improves calculation accuracy but could slow down the performance of the indicator

    • this.api.SetOutputWindow(window) - Configure chart vs oscillator window

    • this.api.AddLevel(value) - Add horizontal reference lines

    • this.api.SetLevelValue(index, value) - Set level values

    • this.api.SetFixedMinMaxValues(min, max) - Set fixed scale range


    • Heavy calculations or complex mathematical operations

    • Per-bar data processing (use Calculate() instead)

    • Accessing bar data like this.api.Close(index), this.api.High(index), etc.

    • Setting buffer values using buffer.setValue(index, value)

    • Time-based calculations or market data analysis

    • Loops through historical data



    1. Init() is for setup only - no calculations or data processing

    2. All parameters must be created AND registered in Init()

    3. All buffers must be declared, created, and configured in Init()

    4. Use factory methods for parameter creation (, etc.)

    5. Register buffer count first with before configuring buffers

    6. Try including for better accuracy

    7. Use for all data processing and calculations


    Remember: Init() runs once when the indicator loads. It defines the structure, settings, and appearance. All dynamic calculations and data processing must happen in Calculate() which runs once per bar.

    public Init(): void {
        // initialization logic here
    }

    What is it?

    Syntax

    What MUST Be Done in Init

    1. Create and Register Parameters

    // 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
      }
    }

    2. Create and Configure Buffers

    3. Set Indicator Properties

    What CAN Be Done in Init

    Core Setup Methods

    Buffer Configuration Methods

    Parameter Configuration Methods

    Recommended Methods

    Advanced Configuration Methods

    What CANNOT Be Done in Init

    ❌ Forbidden Operations

    ❌ Wrong Approach

    ✅ Correct Approach

    Complete Example

    Key Rules Summary

    Pro Tip

    TOptValue_DateOnly

    What Is It?

    TOptValue_DateOnly is a class used to define date-only parameters for custom indicators. These parameters appear in the indicator settings panel and allow the user to input or adjust date values without time information, such as specific trading days, start dates, or end dates.

    You must use the createTOptValue_DateOnly() method of the api object inside Init() method to create an instance.


    When to Use

    Use TOptValue_DateOnly when you need a configurable parameter of date only, such as:

    • Start date for calculations (without specific time)

    • End date for a period (without specific time)

    • Specific trading day to highlight or filter

    • Date-based filters that don't require time precision



    In this example:

    • StartDate defines the beginning of the analysis period.

    • EndDate defines the end of the analysis period.


    • After creating a parameter, don't forget to register it using in the method.

    • You can access the value using this.MyDateParameter.value.

    • Use TOptionType.DATE_ONLY when registering this parameter type.

    TOptValue_TimeOnly

    What Is It?

    TOptValue_TimeOnly is a class used to define time-only parameters for custom indicators. These parameters appear in the indicator settings panel and allow the user to input or adjust time values without date information, such as daily trading session times, specific hours of the day, or recurring time-based events.

    You must use the createTOptValue_TimeOnly() method of the api object inside Init() method to create an instance.

    The UI for this parameter type uses the TimeValue enum, which provides predefined time values in 15-minute intervals from 00:00 to 23:59.


    Use TOptValue_TimeOnly when you need a configurable parameter for time selection, such as:

    • Trading session start/end times

    • Market open/close times

    • Recurring time-based events

    • Any time input from the user (without date)



    In this example:

    • SessionStart defines when the trading session begins.

    • SessionEnd defines when the trading session ends.


    • After creating a parameter, don't forget to register it using in the method.

    • Use TOptionType.TIME_ONLY when registering this parameter type.

    • Default values should use the enum.

    Visible buffers

    To display indicator values on the chart in Forex Tester Online, you need to use buffers. Buffers store the calculated values and define how they should be visualized (e.g., lines, histograms, dots).

    Each buffer is created as a TIndexBuffer and configured through API calls.


    All buffers must be declared as class fields using the TIndexBuffer type.


    Create the buffer instance using the CreateIndexBuffer() method.


    iOpen

    Returns the open price of a bar in the specified symbol's price history.

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    iTime

    Returns the opening time of a bar in the specified symbol's price history.

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    Access to Bar Arrays

    This section provides functions to access historical bar data such as open, high, low, close, volume, and time. These functions are essential for building logic in custom indicators.


    • : Gets the volume value of the current bar.

    • : Retrieves volume value from a specific bar index.

    iVolume

    Returns the tick volume of a bar in the specified symbol's price history.

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    Volume

    Returns the volume for a specific bar.

    • shift: A number representing the shift from the current bar

    Returns a number representing the trading volume during the specified bar.

    The Volume method returns the trading volume for a bar at the specified shift from the current bar. Volume represents the total amount of trading activity during the bar's timeframe. The shift parameter determines which bar's volume to return:

    SetIndexDrawBegin()
    Init()
    IndicatorBuffers()
    Init()
    createTOptValue_number
    IndicatorBuffers()
    RecalculateMeAlways()
    Calculate()
    iTime : Retrieves the timestamp of a specific bar.
  • iOpen : Retrieves the open price of a specific bar.

  • iLowest : Finds the lowest value over a range of bars.

  • iLow : Gets the low price of a specific bar.

  • iHighest : Finds the highest value over a range of bars.

  • iHigh : Gets the high price of a specific bar.

  • iClose : Retrieves the close price of a specific bar.

  • iBarShift : Finds the index of a bar by time.

  • iBars : Returns the number of bars between two dates.

  • Close : Gets the close price of the current bar.

  • High : Gets the high price of the current bar.

  • Low : Gets the low price of the current bar.

  • Open : Gets the open price of the current bar.

  • Bars : Returns the total number of bars.

  • Time : Gets the time of the current bar.


  • Click on any function name to view its detailed documentation.

    Available Functions

    Volume
    iVolume

    This is useful when you only care about the date and not the specific time of day.

    Syntax

    Example

    Notes

    this.RegOption
    Init

    You can access the value using this.MyTimeParameter.value.

  • The UI presents users with a dropdown of time values in 15-minute intervals.

  • This is useful for recurring daily events or session-based filtering.

  • When to Use

    Syntax

    Example

    Notes

    this.RegOption
    Init
    TimeValue
    Tell the API how many buffers you plan to use. In this case — one:

    This must be called before setting buffer styles or assignments.


    Each buffer must be assigned a unique index:


    You can now customize how the buffer will appear on the chart:



    • Each buffer must be declared, created, and registered properly to be visible on the chart.

    • Indices must be unique and zero-based (0, 1, 2, etc.).

    • You can use multiple buffers to display several lines or visual elements.

    Overview

    Step-by-Step Guide

    1. Declare the Buffer

    2. Create the Buffer in Init()

    3. Register the Number of Buffers

    4. Bind the Buffer to an Index

    5. Configure the Buffer

    Full Example

    Notes

    index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the open price of the specified bar.

    The iOpen method retrieves the opening price of a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar.

    Syntax

    Parameters

    Return Value

    Description

    Example

    index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns an FTODate object representing the opening time of the specified bar.

    The iTime method retrieves the opening time of a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar. The returned time is in UTC timezone.

    Syntax

    Parameters

    Return Value

    Description

    Example

    index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the tick volume of the specified bar.

    The iVolume method retrieves the tick volume of a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar. The volume represents the number of price changes (ticks) that occurred during the bar period.

    Syntax

    Parameters

    Return Value

    Description

    Example

    0: Current bar

  • 1: Previous bar

  • 2: Two bars ago

  • And so on

  • Syntax

    Parameters

    Return Value

    Description

    Example

    // 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 bar
    import { 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");
    }

    iLowest

    Returns the index of the bar with the lowest value over a specified range.

    Syntax

    iLowest(symbol: string, timeFrame: number, type: number, count: number, index: number): number

    Parameters

    • symbol: The symbol to get data for

    • timeFrame: The timeframe of the data (in minutes)

    • type: The price type to compare (0=OPEN, 1=HIGH, 2=LOW, 3=CLOSE, 4=VOLUME)

    • count: Number of bars to search through

    • index: The starting bar index (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the index of the bar with the lowest value. Returns -1 if no valid bar is found.

    The iLowest method searches for the bar with the lowest value of the specified price type (open, high, low, close, or volume) within a range of bars. The search starts from the specified index and looks back for the specified number of bars. The method is useful for finding local minima and implementing various technical analysis strategies.

    iHighest

    Returns the index of the bar with the highest value over a specified range.

    Syntax

    iHighest(symbol: string, timeFrame: number, type: number, count: number, index: number): number

    Parameters

    • symbol: The symbol to get data for

    • timeFrame: The timeframe of the data (in minutes)

    • type: The price type to compare (0=OPEN, 1=HIGH, 2=LOW, 3=CLOSE, 4=VOLUME)

    • count: Number of bars to search through

    • index: The starting bar index (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the index of the bar with the highest value. Returns -1 if no valid bar is found.

    The iHighest method searches for the bar with the highest value of the specified price type (open, high, low, close, or volume) within a range of bars. The search starts from the specified index and looks back for the specified number of bars. The method is useful for finding local maxima and implementing various technical analysis strategies.

    iHigh

    Returns the highest price of a bar in the specified symbol's price history.

    Syntax

    iHigh(Symbol: string, TimeFrame: number, index: number): number

    Parameters

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    • index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the highest price of the specified bar.

    The iHigh method retrieves the highest price reached during a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar.

    iBarShift

    Returns the bar index for a specified time in the symbol's price history.

    Syntax

    iBarShift(symbol: string, timeframe: number, time: FTODate, exact: boolean): number

    Parameters

    • symbol: The symbol to get data for

    • timeframe: The timeframe of the data (in minutes)

    • time: The time to search for

    • exact: Whether to require an exact match

    Returns a number representing the index of the bar corresponding to the specified time. Returns -1 if no matching bar is found.

    The iBarShift method searches for a bar with a specific opening time and returns its index. If exact is true, only bars with exactly matching times will be considered. If exact is false, the method will return the index of the nearest bar that opened before the specified time.

    iClose

    Returns the close price of a bar in the specified symbol's price history.

    Syntax

    iClose(Symbol: string, TimeFrame: number, index: number): number

    Parameters

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    • index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the close price of the specified bar.

    The iClose method retrieves the closing price of a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar.

    iLow

    Returns the lowest price of a bar in the specified symbol's price history.

    Syntax

    iLow(Symbol: string, TimeFrame: number, index: number): number

    Parameters

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    • index: The index of the bar (0 is current/last bar, 1 is previous bar, etc.)

    Returns a number representing the lowest price of the specified bar.

    The iLow method retrieves the lowest price reached during a bar at the specified index from the price history of a given symbol and timeframe. The index parameter uses zero-based indexing where 0 represents the current (most recent) bar.

    Close

    Returns the closing price for a specific bar.

    • shift: A number representing the shift from the current bar

    Returns a number representing the closing price of the specified bar.

    The Close method returns the closing price of a bar at the specified shift from the current bar. The shift parameter determines which bar's closing price to return:

    High

    Returns the highest price for a specific bar.

    • shift: A number representing the shift from the current bar

    Returns a number representing the highest price reached during the specified bar.

    The High method returns the highest price reached during a bar at the specified shift from the current bar. The shift parameter determines which bar's high price to return:

    Low

    Returns the lowest price for a specific bar.

    • shift: A number representing the shift from the current bar

    Returns a number representing the lowest price reached during the specified bar.

    The Low method returns the lowest price reached during a bar at the specified shift from the current bar. The shift parameter determines which bar's low price to return:

    Bars

    Returns the total number of bars.

    Returns a number representing the total count of available bars.

    The Bars method returns the total number of price bars available in the current symbol's history. This count includes all bars from the earliest available data point up to and including the current bar.

    Return Value

    Description

    Example

    Return Value

    Description

    Example

    Return Value

    Description

    Example

    Return Value

    Description

    Example

    FTODate

    Return Value

    Description

    Example

    Return Value

    Description

    Example

    Syntax

    Return Value

    Description

    Example

    0: Current bar

  • 1: Previous bar

  • 2: Two bars ago

  • And so on

  • Syntax

    Parameters

    Return Value

    Description

    Example

    0: Current bar

  • 1: Previous bar

  • 2: Two bars ago

  • And so on

  • Syntax

    Parameters

    Return Value

    Description

    Example

    0: Current bar

  • 1: Previous bar

  • 2: Two bars ago

  • And so on

  • Syntax

    Parameters

    Return Value

    Description

    Example

    // 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");
    }

    Time

    Returns the time for a specific bar.

    Syntax

    Time(shift: number, timeZoneMode?: TimeZoneMode): FTODate

    Parameters

    • shift: A number representing the shift from the current bar

    • timeZoneMode: Optional. Default value is project timezone. A TimeZoneMode enum value representing the timezone mode to use for the returned date

    Return Value

    Returns an object representing the bar's opening time.

    The Time method returns the opening time of a bar at the specified shift from the current bar. The time is returned as an FTODate object, which provides various date/time manipulation capabilities. The shift parameter determines which bar's time to return:

    • 0: Current bar

    • 1: Previous bar

    • 2: Two bars ago

    • And so on

    The timeZoneMode parameter allows you to specify how the time should be interpreted:

    • TimeZoneMode.PROJECT: Returns time in the project's timezone (default)

    • TimeZoneMode.UTC: Returns time in UTC

    CreateChartObject

    Creates a new chart object with specified parameters.

    Syntax

    СreateChartObject(
        name: string,
        objType: TObjectType,
        window: number,
        ftoDate1: FTODate,
        price1: number,
        ftoDate2?: FTODate,
        price2?: number,
        ftoDate3?: FTODate,
        price3?: number,
        isStatic?: boolean
    ): boolean

    Parameters

    Parameter
    Type
    Description

    name

    string

    Unique identifier for the object

    Returns boolean - true if the object was created successfully, false otherwise.

    The СreateChartObject method creates a new chart object with the specified parameters. Different object types require different sets of coordinates

    Access to Objects

    This section provides functions to manage and interact with chart objects. These functions allow you to create, remove, and modify objects on a chart.


    Available Functions

    • CreateChartObject: Creates a new chart object.

    • RemoveAllObjects: Removes all objects from a chart.

    • : Removes all objects whose names start with a prefix.

    • : Checks if a chart object exists.

    • : Retrieves the text of a chart object.

    • : Retrieves the type of a chart object.

    • : Retrieves the name of a chart object.

    • : Retrieves the count of objects on a chart.

    • : Returns the index of the current chart window.

    • : Removes a specific chart object.

    • : Sets the text of a chart object.

    • : Retrieves a property of a chart object.

    • : Sets a property of a chart object.


    Click on any function name to view its detailed documentation.

    RemoveAllObjectsByPrefix

    Removes all chart objects whose names start with a specified prefix.

    Syntax

    RemoveAllObjectsByPrefix(prefix: string, isStatic?: boolean, window?: number): void

    Parameters

    Parameter
    Type
    Description

    prefix

    string

    Name prefix to match. Matching is case-sensitive. Empty prefix removes all objects.

    The RemoveAllObjectsByPrefix method removes all chart objects whose names begin with the given prefix. This is useful for cleaning up objects created with a common naming convention (e.g. "MyIndicator_Line_1", "MyIndicator_Line_2").

    GetCurrentWindowIndex

    Returns the index of the current chart window.

    Returns a number:

    • 0 — MainChart (price chart)

    • 1

    GetObjectCount

    Returns the total number of chart objects.

    Parameter
    Type
    Description
    RemoveAllObjectsByPrefix
    DoesChartObjectExist
    GetObjectText
    GetObjectType
    GetObjectName
    GetObjectCount
    GetCurrentWindowIndex
    RemoveChartObject
    SetObjectText
    GetObjectProperty
    SetObjectProperty

    Description

    Example

    FTODate

    objType

    TObjectType

    Type of object to create (e.g., trend line, rectangle)

    window

    number

    Chart window: 0 = MainChart, 1+ = OscWin. Object is created in the specified window.

    ftoDate1

    FTODate

    First time coordinate

    price1

    number

    First price coordinate

    ftoDate2

    FTODate

    Optional. Second time coordinate (required for some objects)

    price2

    number

    Optional. Second price coordinate (required for some objects)

    ftoDate3

    FTODate

    Optional. Third time coordinate (required for triangles)

    price3

    number

    Optional. Third price coordinate (required for triangles)

    isStatic

    boolean

    Optional. Whether the object is static (persists across timeframes)

    Return Value

    Description

    Examples

    Text Object

    Rectangle Object

    Fixed Object (Screen Coordinates)

    isStatic

    boolean

    Optional. Whether to remove static objects (default: false)

    window

    number

    Optional. Target window: 0 = MainChart, 1+ = OscWin. Default: current chart

    Description

    Example

    or higher — OscWin (indicator window; index corresponds to OscWins[returnValue - 1])

    The GetCurrentWindowIndex method returns the index of the window where the indicator or strategy is currently executing. Use this together with the window parameter of GetObjectName, GetObjectCount, RemoveAllObjects, and RemoveAllObjectsByPrefix to scope operations to the current window.

    Syntax

    Return Value

    Description

    Example

    number

    Optional. Target window: 0 = MainChart, 1+ = OscWin, -1 = count across all windows. Default: current chart

    Returns a number representing the total count of chart objects.

    The GetObjectCount method returns the total number of objects in the chart. It can count either regular objects or static objects, depending on the isStatic parameter. Use the optional window parameter to scope the count to a specific chart window (MainChart or OscWin) or to count across all windows.

    isStatic

    boolean

    Optional. Whether to count static objects (default: false)

    Syntax

    Parameters

    window

    Return Value

    Description

    Example

    // 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);

    SetObjectText

    Sets the text content and formatting for a chart object.

    Syntax

    SetObjectText(
        name: string,
        text: string,
        fontSize: number = 14,
        fontName: string = Roboto Flex,
        fontColor: string = '#000000',
        isStatic: boolean = false
    ): boolean

    Parameters

    Parameter
    Type
    Description

    name

    string

    The name of the object

    Returns boolean - true if the text was set successfully, false otherwise.

    The SetObjectText method sets the text content and formatting properties for a specified chart object. This method is primarily used with text-based objects like labels, but can also be used with other objects that support text properties.

    CreateIndexBufferWithArgs

    Creates a new buffer with specified display properties.

    Syntax

    CreateIndexBufferWithArgs(
        index: number,
        aLabel: string,
        drawStyle: TDrawStyle,
        style: TPenStyle,
        width: number,
        color: string
    ): TIndexBuffer

    Parameters

    • index - A number representing the buffer index.

    • aLabel - A string containing the label for the buffer.

    • drawStyle - A value from the enum specifying how to draw the buffer.

    • style - A value from the enum specifying the line style.

    • width - A number representing the line width in pixels.

    • color - A string hex color value for the buffer.

    Returns a TIndexBuffer object that can be used to store indicator values.

    The CreateIndexBufferWithArgs method creates a new buffer with specified display properties and assigns it to the given index. This is a convenient way to create and configure a buffer in a single call.

    GetObjectProperty

    Retrieves a property value from a chart object.

    Syntax

    GetObjectProperty(
      name: string,
      index: ObjProp | number,
      isStatic: boolean = false
      ): number | string

    Parameters

    Parameter
    Type
    Description

    name

    string

    The name of the object

    Returns either a number or string depending on the property type.

    The GetObjectProperty method retrieves a property value from a specified chart object. It can return either numeric or string properties depending on the property identifier provided.

    See for a complete list of available object properties.

    SetIndexStyle

    Sets the visual style for a buffer.

    Important: This method should only be used inside the Init() method for initial buffer configuration.

    Syntax

    SetIndexStyle(
        bufferIndex: number,
        type: TDrawStyle,
        style: TPenStyle,
        width: number,
        clr: string,
        isVisible?: boolean
    ): void

    Parameters

    • bufferIndex - A number representing the index of the buffer to style.

    • type - A value from the enum specifying how to draw the buffer.

    • style - A value from the enum specifying the line style.

    • width - A number representing the line width in pixels.

    • clr - A string hex color value for the buffer.

    • isVisible - (Optional) A boolean for initial visibility setup only. For algorithmic visibility control, use instead.

    This method does not return a value.

    The SetIndexStyle method sets the visual appearance of a buffer on the chart during indicator initialization. This includes the drawing style (line, histogram, etc.), line style (solid, dashed, etc.), width, color, and initial visibility.

    Usage Guidelines:

    • Use in only - for setting up buffer appearance during indicator creation

    • For algorithmic visibility control - use in based on conditions/calculations

    See for available drawing styles, for line styles

    Use the isVisible parameter in SetIndexStyle for setting the initial visibility state:

    Use SetIndexVisibility for algorithmic visibility control:

    1. SetIndexStyle = Initial setup in only

    2. SetIndexVisibility = Dynamic changes in or

    3. Both methods control visibility, but serve different purposes in the indicator lifecycle

    AddLevel

    Adds a horizontal level line to the indicator.

    Syntax

    AddLevel(value: number, style: TPenStyle, width: number, color: string, opacity: number): void

    Parameters

    • value - A number representing the Y-value where the level should be drawn.

    • style - A value from the TPenStyle enum specifying the line style.

    • width - A number representing the line width in pixels.

    • color - A string hex color value for the level line.

    • opacity - A number between 0 and 1 representing the opacity of the line.

    This method does not return a value.

    The AddLevel method adds a horizontal level line to the indicator window. This is commonly used for indicators like RSI or Stochastic to mark overbought and oversold levels.

    See for available line styles

    Configure Indicator

    This section provides functions to configure and customize indicators. These functions allow you to set styles, buffers, and other properties of indicators.


    Available Functions

    • AddLevel: Adds a level to an indicator.

    • SetIndexStyle: Sets the style of an indicator index.

    • : Creates an index buffer with arguments.

    • : Sets the output window for an indicator.

    • : Sets the number of digits for an indicator.

    • : Retrieves the number of counted bars.

    • : Sets the buffer shift for an indicator.

    • : Retrieves information about a buffer.

    • : Sets the draw begin index for an indicator.

    • : Retrieves the minimum value of a buffer.

    • : Retrieves the count of a buffer.

    • : Retrieves the maximum value of a buffer.

    • : Sets a value in a buffer.

    • : Retrieves a value from a buffer.

    • : Sets the label for an indicator index.

    • : Sets the symbol for an indicator index.

    • : Sets the visibility of an indicator index.

    • : For oscillator indicators, draws a selected buffer on the main chart instead of the oscillator subwindow.

    • : Sets the buffer for an indicator index.

    • : Creates an index buffer.

    • : Manages indicator buffers.

    • : Sets the value of a level.

    • : Sets the indicator to always recalculate.

    • : Sets the back offset for calculation.

    • : Sets fixed min and max values for an indicator.

    • : Sets the ID key for an indicator.

    • : Sets the empty value for an indicator.

    • : Sets the short name for an indicator.


    Click on any function name to view its detailed documentation.

    SetIndexDrawBegin

    Sets the starting bar for drawing a buffer.

    • bufferIndex - A number representing the index of the buffer.

    • paintFrom - A number representing the bar index to start drawing from.

    This method does not return a value.

    GetBufferInfo

    Retrieves information about a buffer.

    • index - A number representing the buffer index.

    Returns a object containing information about the buffer.

    The GetBufferInfo method retrieves information about a buffer, such as its label, drawing style, line style, width, color, and visibility. This can be useful for dynamically adjusting buffer properties based on the current state.

    See for more information about the buffer information structure.

    IndicatorDigits

    Sets the number of decimal places for indicator values.

    • digits - A number representing the number of decimal digits to display for indicator values.

    This method does not return a value.

    The IndicatorDigits method sets the number of decimal places that will be used when displaying indicator values. This affects how values are formatted in tooltips, indicator configuration panels, and other UI elements.

    Setting the appropriate number of decimal places is important for readability and precision. For example, oscillators that range between 0 and 100 might use 1 or 2 decimal places, while price-based indicators might need 4 or 5 decimal places for currency pairs.

    CountedBars

    Returns the number of bars that have already been calculated in previous calls.

    This method does not take any parameters.

    Returns a number representing the count of bars that have already been calculated.

    The Counted_bars method returns the number of bars that have already been processed in previous calls to the indicator's calculation function. This is useful for optimization, as it allows the indicator to only calculate values for new bars rather than recalculating all bars.

    SetBufferShift

    Sets the horizontal shift for a buffer.

    • bufferIndex - A number representing the index of the buffer.

    • shift - A number representing the number of bars to shift the buffer.

    This method does not return a value.

    CreateIndexBufferWithArgs
    SetOutputWindow
    IndicatorDigits
    CountedBars
    SetBufferShift
    GetBufferInfo
    SetIndexDrawBegin
    GetBufferMin
    GetBufferCount
    GetBufferMax
    Set buffer value
    Get buffer value
    SetIndexLabel
    SetIndexSymbol
    SetIndexVisibility
    SetIndexChartOverlay
    SetIndexBuffer
    CreateIndexBuffer
    IndicatorBuffers
    SetLevelValue
    RecalculateMeAlways
    SetBackOffsetForCalculation
    SetFixedMinMaxValues
    SetIndicatorIdKey
    SetEmptyValue
    IndicatorShortName

    Download indicator examples

    These are indicator examples you can download

    3KB
    moving-average.zip
    archive
    Open
    2KB
    indicator-skeleton.zip
    archive
    Open
    2KB
    empty-indicator.zip
    archive
    Open

    Built-in indicators

    Looking for ready-made implementations of common indicators (ATR, MACD, Supertrend, and others)? Browse the full catalog with downloads and source code in Built-in indicators.

    text

    string

    The text content to set

    fontSize

    number

    Optional. The font size (default: 14)

    fontName

    string

    Optional. The font name (default: Roboto Flex)

    fontColor

    string

    Optional. The font color in hex value (default: '#000000')

    isStatic

    boolean

    Optional. Whether the object is static (default: false)

    Return Value

    Description

    Example

    Return Value

    Description

    Example

    TDrawStyle
    TPenStyle

    index

    ObjProp | number

    The property identifier

    isStatic

    boolean

    Optional. Whether the object is static (default: false)

    Return Value

    Description

    Example

    ObjProp

    Return Value

    Description

    Visibility Parameter vs SetIndexVisibility

    Initial Setup (in Init())

    Dynamic Changes (in OnParamsChange() or Calculate())

    Example

    Key Rules

    TDrawStyle
    TPenStyle
    SetIndexVisibility
    Init()
    SetIndexVisibility
    Calculate()
    TDrawStyle
    TPenStyle
    Init()
    OnParamsChange()
    Calculate()

    Return Value

    Description

    Example

    TPenStyle

    Syntax

    Parameters

    Return Value

    Description

    Example

    The 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.

    Syntax

    Parameters

    Return Value

    Description

    Example

    Syntax

    Parameters

    Return Value

    Description

    TVisibleBufferInfo
    TVisibleBufferInfo

    Example

    Syntax

    Parameters

    Return Value

    Description

    Example

    The 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).

    Syntax

    Parameters

    Return Value

    Description

    Example

    // 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)

    TOptValue_str

    What Is It?

    TOptValue_str is a class used to define string parameters in custom indicators. It allows users to enter or modify text values in the indicator settings panel.

    Use the createTOptValue_str() method from the api object inside Init() method to create an instance.


    When to Use

    Use TOptValue_str when you want to let the user:

    • Input custom labels or names

    • Define identifiers or tags

    • Set any free-form text value



    In this example:

    • Name is a string parameter that can be changed by the user.

    • The value can be accessed via this.Name.value.


    • Don't forget to register string parameters using inside the method.

    • You can use the value directly in Calculate, OnShow, or other methods as needed.

    TOptValue_DateTime

    What Is It?

    TOptValue_DateTime is a class used to define date and time parameters for custom indicators. These parameters appear in the indicator settings panel and allow the user to input or adjust date and time values such as start times, end times, or specific moments in trading history.

    You must use the createTOptValue_DateTime() method of the api object inside Init() method to create an instance.


    When to Use

    Use TOptValue_DateTime when you need a configurable parameter of date and time, such as:

    • Start date and time for calculations

    • End date and time for a period

    • Specific moment to trigger indicator behavior

    • Time-based filters for trading sessions


    • After creating a parameter, don't forget to register it using in the method.

    • You can access the value using this.MyDateTimeParameter.value.

    • Use TOptionType.DATE_TIME when registering this parameter type.

    Tutorial: Create indicator with Cursor IDE

    This tutorial will guide you through the process of creating a new indicator using Cursor IDE.

    Introduction

    Cursor IDE is a fork of VS Code that has powerful AI capabilities, allowing it to access your code directly and have information about your project.

    Cursor can also be provided with FTO custom indicator documentation to assist in creating new indicators.

    Please note that Cursor is a paid IDE, but it has a free version, the free version is limited to 50 requests and 2000 completions total.

    Step 1: Install Cursor and set up the project

    How to install Cursor and set up the project is described in this guide. If you don’t yet know how to set up your environment to work with the FTO API for indicators, that is also covered in the same guide.

    Step 2: Apply Cursor Rules

    The Rules feature in Cursor IDE allows users to define custom guidelines or behaviors that the AI should follow when generating code. By setting up rules that describe how indicators work in FTO, you can effectively teach Cursor the context it needs to produce accurate and consistent indicators.

    To set up global rules for Cursor, click the setting icon (Picture #1)

    Picture #1

    From here, click the Rules setting (Picture #2)\

    In the User Rules field (Picture #3), download and paste contents of the file below called Rules.txt

    You can upload the FTO indicator documentation to Cursor, and Cursor will be able to use it to help you create new indicators.

    To upload the FTO indicator documentation to Cursor, you need to follow these steps:

    1. Click the setting icon in the top right corner of the Cursor IDE. (Picture #4)

    2. Select "Features" on the left sidebar (Picture #5).

    1. Scroll down until you see Docs (Picture #6)

    1. Click on the "Add new doc" button and insert link https://fto-2.gitbook.io/fto-indicators-docs to the FTO indicator documentation (Picture #7).

    After some indexing, Cursor will be able to refer to the documentation to help you create new indicators.

    To become more familiar with how to use this added documentation in your requests to Cursor, go to the section below.

    To open Cursors AI window, you can press Ctrl+L or click on the icon in the top right corner of the Cursor IDE (Picture #8)

    This panel has a chat interface and in Cursor version 0.47.8 it has 3 modes, Agent, Ask and Edit,

    each catering to different needs during development (Picture #9).

    • Agent Mode: This mode allows for automated code generation and completion by interacting with the AI. It helps streamline the coding process by providing intelligent suggestions and solutions. I access your project files directly and suggests code which you can accept or reject

    • Ask Mode: In this mode, developers can ask questions about their codebase, programming concepts, or any development-related topics. The AI will respond with helpful guidance and explanations.

    • Edit Mode: Edit Mode assists in making modifications to the existing code. It aids in refactoring, simplifying, or improving code sections as needed.

    What makes these modes powerful is the ability to add context directly from your project or documentation

    For now, select the Ask mode

    Next to the "select mode menu" there is also an option to select a model of AI that we want to use, for now we should choose Auto-select (Picture#10).

    Before we begin writing the indicator, it is expected that you are familiar with how to set up the environment to access the FTO Indicator API. If not, please read guide first before continuing.

    For this example, we will ask Cursor to implement the On Balance Volume (OBV) indicator for us. In this guide, we will start with an empty file, but for future indicator implementations, you can use some of our indicator foundations provided in .

    In the request to create the OBV indicator for us, I included the FTO documentation for and added the discussed earlier. In the screenshot, you can see the sections of the documentation it decided to use (Picture #11).

    In the result, we got code below

    The current logic seems correct, but it's using only the Close price, and there is currently no way for the user to specify their own price type. Let's ask it to fix that (Picture #12).

    The code that Cursor wrote in his second response is below

    And with the addition of this new parameter and internal method, we get a nice dropdown with price types (Picture #13).

    So in the end, we got a proper indicator with just two simple requests by setting up Cursor properly and providing it with and documentation for context.

    For Cursor to use the documentation that we provided in we need to do the following:

    Start by selecting the Ask mode and add context to your query by typing @ and selecting Docs from the drop-down menu (Picture #14).

    In the drop-down menu, select FTO Indicator documentation, which we added earlier in (Picture#15)

    And provide add a file in which you want to create the indicator, you can do it by drag and dropping it from explorer window on the left onto the chat window or finding it in the @ drop-down menu under Files & folders (Picture #16)

    Set up Cursor Rules

    The Rules feature in Cursor IDE allows users to define custom guidelines or behaviors that the AI should follow when generating code. By setting up rules that describe how indicators work in FTO, you can effectively teach Cursor the context it needs to produce accurate and consistent indicators.

    To set up global rules for Cursor, click the setting icon (Picture #1)

    Picture #1

    From here, click the Rules setting (Picture #2)

    Picture #2

    In the User Rules field (Picture #3), download and paste contents of the file below called Rules.txt

    10KB
    Rules.txt
    Open
    Picture #3

    TOptValue_number

    What Is It?

    TOptValue_number is a class used to define numeric parameters for custom indicators. These parameters appear in the indicator settings panel and allow the user to input or adjust numbers such as periods, shifts, price types, and more.

    You must use the createTOptValue_number() method of the api object inside Init() method to create an instance.


    When to Use

    Use TOptValue_number when you need a configurable parameter of type number, such as:

    • Period length for moving averages

    • Shift values

    • Enum values (e.g., MA type, price type)

    • Any numeric input from the user



    In this example:

    • Period controls how many bars are used in the moving average calculation.

    • Shift can offset the indicator horizontally.

    • MAtype selects the type of moving average (e.g., SMA, EMA).


    • After creating a parameter, don’t forget to register it using in the method.

    • You can access the value using this.MyParameter.value.

    TOptValue_LineStyle

    TOptValue_LineStyle is a class used to define line style parameters for custom indicators. It allows users to configure line appearance (visibility, color, style, width) through the indicator settings panel.

    Use the createTOptValue_LineStyle() method from the api object inside Init() method to create an instance.


    Use TOptValue_LineStyle when you want to let the user configure line appearance for:

    iBars

    Returns the total number of bars available in the specified symbol's price history.

    • Symbol: The symbol to get data for

    • TimeFrame: The timeframe of the data (in minutes)

    Returns a number

    Open

    Returns the opening price for a specific bar.

    • shift: A number representing the shift from the current bar

    Returns a number representing the opening price of the specified bar.

    The Open method returns the opening price of a bar at the specified shift from the current bar. The shift parameter determines which bar's opening price to return:

    DoesChartObjectExist

    Checks if a chart object with the specified name exists.

    Parameter
    Type
    Description

    RemoveAllObjects

    Removes all chart objects of a specified type.

    Parameter
    Type
    Description

    TOptValue_bool

    TOptValue_bool is a class used to define boolean (true/false) parameters in custom indicators. It allows users to enable or disable certain features through the indicator settings panel.

    Use the createTOptValue_bool() method from the api object inside Init() method to create an instance.


    Use TOptValue_bool when you want to let the user:

    Syntax

    Example

    Notes

    this.RegOption
    Init

    Syntax

    Notes

    this.RegOption
    Init

    Step 3: Upload FTO indicator documentation to Cursor

    Step 4: Getting familiar with Cursor

    Step 5: Start building

    Recommendations

    Recommendations
    this
    this section
    context
    Rules
    Rules
    Step 3
    Step 3
    Picture #2
    10KB
    Rules.txt
    Open
    Picture #3
    Picture #4
    Picture #5
    Picture #6
    Picture #7
    Picture #8
    Picture #9
    Picture #10
    Picture #11
    Picture #12
    Picture #13
    Picture #14
    Picture #15
    Picture #16

    ApplyToPrice defines which price (close, open, high, low) the MA should use.

  • VShift applies a vertical offset to the line.

  • Syntax

    Example

    Notes

    this.RegOption
    Init

    Support/resistance levels

  • Trend lines

  • Other visual elements on charts



  • In this example:

    • LineStyle allows the user to configure line appearance

    • Inside Calculate(), a horizontal line is created with the configured style

    • The line is positioned at the current bar's close price


    • Don't forget to register line style parameters using this.RegOption inside the Init method.

    • Use TOptionType.LINE when registering TOptValue_LineStyle parameters.

    • Access properties with this.MyLineStyle.isVisible, this.MyLineStyle.color, this.MyLineStyle.style, this.MyLineStyle.width.

    What Is It?

    When to Use

    Syntax

    Example

    Notes

    representing the total count of available bars.

    The iBars method returns the total number of bars available in the price history for a given symbol and timeframe. This count includes all bars from the oldest available bar up to the current (most recent) bar. The method is useful for determining the size of the historical data and for implementing lookback periods in technical analysis.

    Syntax

    Parameters

    Return Value

    Description

    Example

    0: Current bar

  • 1: Previous bar

  • 2: Two bars ago

  • And so on

  • Syntax

    Parameters

    Return Value

    Description

    Example

    boolean

    Optional. Whether to check static objects (default: false)

    Returns a boolean indicating whether the object exists (true) or not (false).

    The DoesChartObjectExist method checks for the existence of a chart object with the specified name. It can check for both regular and static objects, depending on the isStatic parameter.

    uniqueObjectName

    string

    The unique name of the object to check

    Syntax

    Parameters

    isStatic

    Return Value

    Description

    Example

    boolean

    Optional. Whether to remove static objects (default: false)

    window

    number

    Optional. Target window: 0 = MainChart, 1+ = OscWin. Default: current chart

    The RemoveAllObjects method removes all chart objects of a specified type from the chart. This is useful for cleaning up multiple objects at once. The method can remove either regular objects or static objects, depending on the isStatic parameter. Use the optional window parameter to scope removal to a specific chart window.

    objType

    TObjectType

    The type of objects to remove

    Syntax

    Parameters

    isStatic

    Description

    Example

    Toggle a feature on or off

  • Show or hide additional elements

  • Enable conditional behavior in your indicator



  • In this example:

    • IsEnabled allows the user to toggle indicator logic on or off.

    • Inside Calculate(), the logic runs only if the toggle is true.


    • Don't forget to register string parameters using this.RegOption inside the Init method.

    • You can use the value directly in Calculate, OnShow, or other methods as needed.

    • Access the value with this.MyFlag.value.

    What Is It?

    When to Use

    Syntax

    Example

    Notes

    // 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
      }
    }

    GetObjectName

    Returns the name of a chart object by its index.

    Syntax

    GetObjectName(index: number, isStatic?: boolean, window?: number): string

    Parameters

    Parameter
    Type
    Description

    index

    number

    The index of the object

    Returns a string representing the object's name.

    The GetObjectName method retrieves the name of a chart object based on its index in the list of objects. Objects are indexed from 0 to GetObjectCount() - 1. This method is useful for iterating through all objects on a chart. Use the optional window parameter to read from a specific window (MainChart or OscWin).

    GetObjectText

    Returns the text content of a chart object.

    Syntax

    GetObjectText(name: string, isStatic: boolean = false): string

    Parameters

    Parameter
    Type
    Description

    name

    string

    The name of the object

    Returns a string containing the object's text content.

    The GetObjectText method retrieves the text content of a specified chart object. This is primarily used with text-based objects like labels, but can also be used with other objects that have text properties.

    GetObjectType

    Returns the type of a chart object.

    Syntax

    GetObjectType(name: string, isStatic: boolean = false): TObjectType

    Parameters

    Parameter
    Type
    Description

    name

    string

    The name of the object

    Returns a enum value representing the object's type.

    The GetObjectType method retrieves the type of a specified chart object. The type is returned as a TObjectType enumeration value.

    RemoveChartObject

    Removes a chart object with the specified name.

    Syntax

    RemoveChartObject(uniqueObjectName: string, isStatic: boolean = false): void

    Parameters

    Parameter
    Type
    Description

    uniqueObjectName

    string

    The unique name of the object to remove

    The RemoveChartObject method removes a specified chart object from the chart. The object is identified by its unique name. If the object is static (persists across all timeframes), set the isStatic parameter to true.

    SetObjectProperty

    Sets a property value for a chart object.

    Syntax

    SetObjectProperty(
      name: string,
      index: number,
      value: any,
      isStatic: boolean = false
      ): boolean

    Parameters

    Parameter
    Type
    Description

    name

    string

    The name of the object

    Returns boolean - true if the property was set successfully, false otherwise.

    The SetObjectProperty method sets a property value for a specified chart object. It can handle both numeric and string properties, and automatically converts time values from FTODate to the internal format.

    SetOutputWindow

    Sets the window where the indicator will be displayed.

    Syntax

    SetOutputWindow(outputWindow: TOutputWindow): void

    Parameters

    • outputWindow - A value from the TOutputWindow enum specifying where the indicator should be displayed.

    Return Value

    This method does not return a value.

    Description

    The SetOutputWindow method determines where the indicator will be displayed on the chart. Indicators can be displayed either in the main chart window or in a separate window below the main chart.

    See TOutputWindow for the complete list of available window types.

    • — overlay an individual buffer on the main price chart

    isStatic

    boolean

    Optional. Whether to look in static objects (default: false)

    window

    number

    Optional. Target window: 0 = MainChart, 1+ = OscWin. Default: current chart

    Return Value

    Description

    Example

    isStatic

    boolean

    Optional. Whether to look in static objects (default: false)

    Return Value

    Description

    Example

    isStatic

    boolean

    Optional. Whether to look in static objects (default: false)

    Return Value

    Description

    Example

    TObjectType

    isStatic

    boolean

    Optional. Whether the object is static (default: false)

    Description

    Example

    index

    number

    The property identifier

    value

    any

    The value to set

    isStatic

    boolean

    Optional. Whether the object is static (default: false)

    Return Value

    Description

    Example

    Example

    Related Methods

    SetIndexChartOverlay
    // 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);