# Welcome

This documentation is here to answer your questions. You'll find guides, tutorials and explanations to help you get the most out of Obside.

If you can't find what you're looking for, or you think something is missing, feel free to reach out by email at [contact@obside.com](mailto:contact@obside.com). We'll be glad to help!

## What can you do with Obside?

Obside is built around a conversational interface. You describe what you want to achieve in natural language, and the platform takes care of the rest. It runs in your **browser** and as a native mobile app on [**iPhone**](https://apps.apple.com/app/obside/id6774629000) and [**Android**](https://play.google.com/store/apps/details?id=com.obside.webapp) — same account, same agents, same chat everywhere.

Three main use cases are available:

### Automate your trading

Turn your ideas into concrete actions on the markets.

* [**Plan a trade**](/docs/obside-chat/automate-your-trading/schedule-a-trade.md) — execute a specific operation at a given time or under a specific condition.
* [**Create a trading bot**](/docs/obside-chat/automate-your-trading/create-a-trading-bot.md) — an agent that follows defined rules, with no human intervention.
* [**Deploy an AI trader**](/docs/obside-chat/automate-your-trading/deploy-an-ai-trader.md) — an autonomous agent powered by a large language model.

### Get alerted

Stay informed in real time about the events that matter to you.

* [**Create a smart alert**](/docs/obside-chat/get-alerted/create-a-smart-alert.md) — triggered by your conditions on prices, indicators, news or tweets.
* [**Monitor my holdings**](/docs/obside-chat/get-alerted/monitor-your-portfolio.md) — track performance, risk and events affecting your positions.
* [**Spot opportunities**](/docs/obside-chat/get-alerted/spot-opportunities.md) — identify assets matching your market criteria.

### Understand the market

Get clear analyses and learn how to read the markets.

* [**Make sense of the news**](/docs/obside-chat/understand-the-market/decode-the-news.md) — summaries and impact of economic events.
* [**Get a market read**](/docs/obside-chat/understand-the-market/read-the-market.md) — directional reads and macro overviews.
* [**Learn the basics**](/docs/obside-chat/understand-the-market/learn-the-basics.md) — pedagogical explanations on trading and investing.

***

## Useful links

* **Website:** [https://obside.com](https://obside.com/)
* **iPhone app:** [App Store](https://apps.apple.com/app/obside/id6774629000)
* **Android app:** [Google Play](https://play.google.com/store/apps/details?id=com.obside.webapp)
* **Telegram (FR):** [https://t.me/obsideai\_fr](https://t.me/obsideai_fr)
* **Telegram (EN):** [https://t.me/obsideai](https://t.me/obsideai)
* **YouTube (FR):** [https://youtube.com/@obside\_fr](https://www.youtube.com/@obside_fr)
* **YouTube (EN):** [https://youtube.com/@obside\_ai](https://www.youtube.com/@obside_ai)
* **X:** [https://x.com/obsideai](https://x.com/obsideai)
* **Reddit:** [https://www.reddit.com/r/ObsideAI](https://www.reddit.com/r/ObsideAI/)
* **Instagram:** [https://www.instagram.com/obsideai](https://www.instagram.com/obsideai/)
* **LinkedIn:** [https://www.linkedin.com/company/obsideai](https://www.linkedin.com/company/obsideai)

## Contact us

* **Email:** [contact@obside.com](mailto:contact@obside.com)

# About

## Your AI copilot for the markets ⚡️

Obside is a platform that lets you **understand your portfolio, monitor the markets and automate your investing using natural language**.

You don't need any technical skills: **connect your brokerage account, describe what you want, and the platform does the rest** — from reading your positions to alerting you, and even placing orders where available.

Obside is available in your **browser** and as a **mobile app on iOS and Android** — same account, same agents, same chat.

Out of inspiration? Obside can suggest ideas and strategy logic in a single click.

***

### Who is Obside for?

#### Everyday investors

For those who want to understand and manage their portfolio with discipline. **Connect your broker, get AI insights, briefs and smart alerts on what actually moves your holdings.**

#### Experienced traders

For those who want to build advanced strategies, test them on up to 20 years of history, and deploy them in real or simulated conditions. **Build, optimize and deploy your own agents.**

#### Curious learners

For those who want to understand the markets without sifting through dozens of sources. **Ask your questions, get clear and contextualized answers.**

***

### What Obside enables

**Portfolio reading**: connect your brokerage account and get a clear, live view of your holdings — allocation, exposure, expected income, related news and a weekly brief.

**Natural-language automation**: describe what you want to accomplish, and the platform turns it into a ready-to-run agent — an alert, a monitor, or a trading strategy.

**Smart alerts**: receive real-time notifications via email, Telegram or push notifications.

**Fast backtests**: test your agents on up to 20 years of historical data with detailed metrics.

**Live or simulated execution**: deploy your agents in demo (paper trading) or live mode through a supported broker — with every order submitted for your approval by default.

**Market understanding**: Obside Chat explains the news, analyzes the markets and answers your in-depth questions.

**Market Radar**: a daily scan that flags US stocks showing unusual options or insider activity, each with a 0–100 score and the triggers behind it.

**AI connections**: plug Claude, ChatGPT or any MCP-compatible assistant directly into your Obside account.

***

### Obside Chat at the heart of the experience

Obside is built around **Obside Chat**, a conversational assistant that supports you in:

* Understanding your portfolio and the markets,
* Creating and optimizing your agents,
* Analyzing backtest results,
* Setting up smart alerts.

A simple question is all it takes to get started.

# Introduction

## Your intelligent assistant for trading and investing

**Obside Chat** is the central interface of Obside. Whether you're a beginner or an expert, an investor or an active trader, it guides you, informs you, and helps you make the best decisions possible — all in natural language.

It's also where **you create your agents**: simply describe what you want to achieve, and Obside turns your intent into a ready-to-use automation.

***

## Three families of use cases

Everything you can do with Obside Chat falls into three main families:

### 1. Automate your trading

Turn your ideas into concrete actions on the markets.

* [**Plan a trade**](/docs/obside-chat/automate-your-trading/schedule-a-trade.md) — execute a specific operation at a given time or under a specific condition.
* [**Create a trading bot**](/docs/obside-chat/automate-your-trading/create-a-trading-bot.md) — an agent that follows defined rules, with no human intervention.
* [**Deploy an AI trader**](/docs/obside-chat/automate-your-trading/deploy-an-ai-trader.md) — an autonomous agent powered by a large language model.

### 2. Get alerted

Stay informed in real time about the events that matter to you.

* [**Create a smart alert**](/docs/obside-chat/get-alerted/create-a-smart-alert.md) — triggered by your conditions on prices, indicators, news or tweets.
* [**Monitor my holdings**](/docs/obside-chat/get-alerted/monitor-your-portfolio.md) — track performance, risk and events affecting your positions.
* [**Spot opportunities**](/docs/obside-chat/get-alerted/spot-opportunities.md) — identify assets matching your criteria.

### 3. Understand the market

Get clear analyses and learn how to read the markets.

* [**Make sense of the news**](/docs/obside-chat/understand-the-market/decode-the-news.md) — summaries and impact of economic events.
* [**Get a market read**](/docs/obside-chat/understand-the-market/read-the-market.md) — directional reads and macro overviews.
* [**Learn the basics**](/docs/obside-chat/understand-the-market/learn-the-basics.md) — pedagogical explanations on trading and investing.

***

## How does Obside Chat work?

You write (or dictate) your request in the chat. To frame the conversation, you can **enrich your message** through the **Add** menu (or by typing **@**):

* **Assets** — e.g. Bitcoin, AAPL, EURUSD — selected from 10,000+ instruments.
* **Technical indicators** — RSI, MACD, EMA, etc. — with your custom parameters.
* **Files** — images, PDFs, chart screenshots, datasets.
* **Connections** — so it can read your balances or rely on the assets available at your broker.
* **Your agents and portfolios** — to analyze, adjust or discuss them directly.

Obside Chat rephrases your request, asks for clarifications if needed, and then executes the action or creates the corresponding agent.

{% hint style="success" %}
Prefer talking? Obside Chat also supports **live voice conversations** — start one from the microphone button and speak with the assistant hands-free.
{% endhint %}

{% hint style="info" %}
Messages count toward your monthly AI usage, which the app shows as a gauge. A question usually uses well under 1% of the Plus monthly allowance, and creating or editing an agent an estimated 1% with the code-generation pipeline in use since October 2026, so the allowance covers about 100 agent creations a month on Plus, 300 on Pro and 500 on Max. See [When are credits consumed?](/docs/credits/how-it-works.md#in-the-chat).
{% endhint %}

***

## Why use Obside Chat?

* Available for any strategic, technical or methodological question.
* Boosts your efficiency in creating, analyzing and improving your agents.
* Saves you time on platform configuration.
* Supports decision-making with market data and your own history.

A simple question is all it takes to get started.

# Automate your trading

Obside lets you turn your ideas into concrete actions on the markets, without writing a single line of code. Three levels of automation are available, from the simplest to the most autonomous:

* [**Plan a trade**](/docs/obside-chat/automate-your-trading/schedule-a-trade.md) — a specific operation, executed at a given time or under a specific condition. Ideal for one-off actions.
* [**Create a trading bot**](/docs/obside-chat/automate-your-trading/create-a-trading-bot.md) — an agent that follows defined rules, with no human intervention. Ideal for repeatable systematic strategies.
* [**Deploy an AI trader**](/docs/obside-chat/automate-your-trading/deploy-an-ai-trader.md) — an autonomous agent powered by a large language model, able to analyze context and decide on its own.

{% hint style="info" %}
Whichever level you choose, the agent can be launched in **demo mode** (paper trading) before being deployed live. Live execution availability depends on your region and your broker — see [Availability by region](/docs/platforms/availability-by-region.md).
{% endhint %}

# Plan a trade

## Overview

Planning a trade means asking Obside to **execute a specific operation**, at a given time or when a condition is met. It's the simplest level of automation: no continuous strategy to design, just an action to schedule.

You can schedule:

* A buy or sell at a fixed time.
* A buy or sell conditional on a market event (price breakout, macro release, earnings…).
* An order with built-in risk management (stop-loss, take-profit, position size).

## How to phrase your request

Describe the operation as if you were briefing an assistant: **what to buy or sell, when, and under which conditions**.

{% hint style="success" %}
The more precise your request (amount, asset, time, conditions, risk management), the less Obside will need to come back to you for clarification.
{% endhint %}

## Example requests

{% hint style="info" %}
**Example #1 — Buy at a fixed time**

"Buy $500 of QQQ at market open next Monday."
{% endhint %}

{% hint style="info" %}
**Example #2 — Conditional buy on macro event**

"Buy 1 micro gold contract if US inflation YoY beats expectations, with a -5% stop-loss and +7% take-profit."
{% endhint %}

{% hint style="info" %}
**Example #3 — Price breakout with volume filter**

"Buy $2,000 of AAPL if it breaks above $230 with volume 30% above the 20-day average."
{% endhint %}

***

## What's next?

Once your trade is scheduled, it becomes an **agent** accessible in the [Agents](/docs/agents/introduction.md) section. You can track its status, edit it, or pause it at any time.

# Create a trading bot

## Overview

A **trading bot** is an agent that follows **defined rules** to open and close positions, with no human intervention. Unlike a one-off trade, it runs **continuously** on the assets and timeframe you choose.

A bot is the ideal tool to automate:

* A systematic strategy based on technical indicators.
* A progressive investment plan (DCA, rebalancing).
* Complex entry and exit rules combining multiple conditions.

## How to phrase your request

Describe your strategy as if you were teaching it to someone: **which assets**, **when to enter**, **when to exit**, **how to manage risk**.

{% hint style="success" %}
A good way to write clear rules is to ask yourself:

"If I were teaching my strategy to someone else using only these rules, would they understand everything or would they have questions on unclear aspects?".

This approach helps you maximize the precision of your rules.
{% endhint %}

To go deeper on rule writing, see [Description in natural language](/docs/agents/create-an-agent/description.md), [Entry rules](/docs/agents/create-an-agent/entry-rules.md) and [Exit rules](/docs/agents/create-an-agent/exit-rules.md).

## Example requests

{% hint style="info" %}
**Example #1 — Recurring DCA bot with filter**

"DCA $200 into Bitcoin every Friday, and skip the buy if the RSI is above 70 on the daily."
{% endhint %}

{% hint style="info" %}
**Example #2 — Mean reversion**

"Buy $1,000 of SPY whenever it closes more than 2% below its 20-day moving average, sell when it returns to the average."
{% endhint %}

{% hint style="info" %}
**Example #3 — News-driven bot**

"Buy $500 of TSLA whenever Tesla unveils a new model, and sell after 5 trading days."
{% endhint %}

For more examples (short-term trading, long-term investing, multi-asset), see [Example requests](/docs/agents/create-an-agent/examples.md).

***

## What's next?

Once the bot is created, it becomes an agent that you can:

* **Backtest** on up to 20 years of historical data to evaluate past performance.
* **Run in demo mode** (paper trading) to see how it behaves in real time without risk.
* **Run in live mode** on a supported broker (depending on your region).

See [Backtest an agent](/docs/agents/backtest-an-agent.md) and [Run an agent](/docs/agents/run-an-agent.md).

# Deploy an AI trader

## Overview

An **AI trader** is an **autonomous** agent powered by a large language model (LLM). Unlike a bot that follows strict rules, an AI trader **interprets the market context** (price, volume, news, indicators…) and **makes its own decisions** within the boundaries you set.

{% hint style="info" %}
You create an AI trader the same way as any other agent — by **describing it in [Obside Chat](/docs/obside-chat/introduction.md)**. There's no dedicated button: just spell out the mandate, and the agent's **AI** (and usually **News**) capability is enabled automatically. See [Agent capabilities](/docs/agents/capabilities.md).
{% endhint %}

You define:

* **The assets** it can trade.
* **The risk management** (position size, stop-loss, maximum exposure).
* **The style** (intraday, swing, momentum, macro, contrarian…).
* **The sources** it can consult (price, indicators, news, X/Twitter).

It then decides **when to enter, when to exit, and how much to risk**, based on its read of the market.

{% hint style="info" %}
An AI trader runs in **paper trading** (demo) mode by default. It's an excellent way to evaluate its behavior before any live deployment.
{% endhint %}

## How to phrase your request

Define a **clear mandate**: asset universe, risk constraints, expected style. The more precise the framing, the more consistent the AI's decisions will be with your intent.

## Example requests

{% hint style="info" %}
**Example #1 — Risk-conscious agent**

"Deploy an AI trader that paper trades MNQ and MGC with strict risk management. On each trade, set a stop-loss and take-profit, and size the position so that hitting the stop costs at most 0.25% of capital. Only enter when the reward/risk ratio is clearly favorable. Use only market orders."
{% endhint %}

{% hint style="info" %}
**Example #2 — Momentum hunter**

"Deploy an AI trader that paper trades TSLA, NVDA, PLTR, MSFT, COIN and SMCI, focusing on stocks showing strong intraday momentum and unusual volume. Risk at most 0.5% of capital per trade. Use tight stops and trail profits with the move. Exit any position by end of day."
{% endhint %}

{% hint style="info" %}
**Example #3 — Macro agent**

"Deploy an AI trader that paper trades MES and MGC, based on macro catalysts and news. Watch economic releases, central bank announcements and major geopolitical events. Take positions when a clear directional thesis emerges. Risk 0.5% of capital per trade and hold positions 1 to 5 days."
{% endhint %}

***

## AI trader vs trading bot: when to choose what?

| | **Trading bot** | **AI trader** |
|---|---|---|
| **Logic** | Strict, deterministic rules | Contextual decisions |
| **Reproducibility** | Identical on every run | Variable depending on context |
| **Backtest** | Relevant and reliable | Limited — the AI exploits the present context |
| **Best for** | Systematic strategies | Reading the market and adapting |

{% hint style="success" %}
The two approaches are complementary. You can absolutely have several agents running in parallel, each with its own mandate.
{% endhint %}

# Get alerted

Stay informed in real time about the events that matter to you, without having to watch the markets constantly. Three main use cases:

* [**Create a smart alert**](/docs/obside-chat/get-alerted/create-a-smart-alert.md) — triggered by your conditions on prices, indicators, news or tweets.
* [**Monitor my holdings**](/docs/obside-chat/get-alerted/monitor-your-portfolio.md) — track performance, risk and events affecting your positions.
* [**Spot opportunities**](/docs/obside-chat/get-alerted/spot-opportunities.md) — identify assets matching your criteria.

{% hint style="info" %}
Alerts are delivered through the channels you've set up: Telegram, push notifications or email. See [Notification channels](/docs/alerts-and-notifications/channels.md).
{% endhint %}

An alert is a type of agent whose **Notifications capability** is enabled — it doesn't place orders, it notifies you. See [Agent capabilities](/docs/agents/capabilities.md).

# Create a smart alert

## Overview

A **smart alert** notifies you as soon as a condition you've defined is met. Unlike a classic price alert, it can combine **multiple heterogeneous signals**: prices, technical indicators, news, earnings reports, tweets, macroeconomic data…

You describe the condition in natural language, Obside creates the agent that watches continuously, and sends you the notification at the precise moment it triggers.

## How to phrase your request

Specify **what should trigger the alert** and, ideally, **in which context**. The more specific the condition, the fewer false alerts you'll get.

{% hint style="success" %}
Don't hesitate to **combine multiple filters** (technical + fundamental + price action) to only trigger the alert in truly interesting contexts.
{% endhint %}

## Example requests

{% hint style="info" %}
**Example #1 — Earnings + price filter**

"Alert me if Apple beats earnings AND gives guidance above consensus AND the stock doesn't move more than 4% up in after-hours."
{% endhint %}

{% hint style="info" %}
**Example #2 — RSI divergence + trend filter**

"Alert me on a bullish RSI divergence on the daily of MSFT with price above the EMA 200."
{% endhint %}

{% hint style="info" %}
**Example #3 — Multi-timeframe confirmation**

"Alert me if SPY shows a bullish MACD crossover on 1H while the daily RSI is above 50."
{% endhint %}

***

## Channel setup

When activating the alert, Obside will ask you for at least one delivery channel: **Telegram, push notification or email**. You can enable several in parallel.

See [Notification channels](/docs/alerts-and-notifications/channels.md) for setup details.

# Monitor my holdings

## Overview

You can ask Obside to **continuously monitor** your portfolio and notify you of significant events: concentration risk, underperformance, news affecting your positions, etc.

This monitoring relies on the **balances and positions read from your connected broker**. See [Connections](/docs/platforms/connectors.md) to link an account and [Portfolios](/docs/portfolios/portfolios.md) to organize your holdings.

## How to phrase your request

Specify **what you want to monitor** (concentration, relative performance, news, drawdown…) and **the threshold or condition** that should trigger the notification.

## Example requests

{% hint style="info" %}
**Example #1 — Concentration risk**

"Email me if any single stock position exceeds 30% of my portfolio."
{% endhint %}

{% hint style="info" %}
**Example #2 — Catalyst tracking**

"Alert me on any earnings release, rating change or significant news affecting the stocks I hold."
{% endhint %}

{% hint style="info" %}
**Example #3 — Underperformance**

"Notify me if any of my positions underperforms the S\&P 500 by more than 10% over the last 30 days."
{% endhint %}

***

{% hint style="info" %}
Portfolio monitoring is available on the **Plus plan and above**, with a connected broker, **regardless of region** — including in the EU. See [Availability by region](/docs/platforms/availability-by-region.md) and [Pricing](/docs/misc/pricing.md).
{% endhint %}

# Spot opportunities

## Overview

You can ask Obside to **scan the market** and identify assets matching your criteria — fundamental, technical, macroeconomic or sectoral. It's useful to prepare a shortlist, spot weak signals, or refine your selection before opening a position.

## How to phrase your request

Describe the **selection criteria** (asset universe, technical filters, financial ratios, sectors, geography…) and, when relevant, **the context you're looking for** (breakout, reversal, momentum, undervaluation…).

## Example requests

{% hint style="info" %}
**Example #1 — Short candidates**

"Find European stocks with declining YoY revenue, P/E above 30 and debt/EBITDA above 4."
{% endhint %}

{% hint style="info" %}
**Example #2 — Beat & cheap**

"Find stocks that beat earnings this week and whose P/E is below the sector average."
{% endhint %}

{% hint style="info" %}
**Example #3 — Undervalued stocks**

"Find European stocks with a P/E below 12 and a dividend yield above 3%."
{% endhint %}

***

{% hint style="success" %}
Once you have the shortlist, you can chain it with a [smart alert](/docs/obside-chat/get-alerted/create-a-smart-alert.md) on those assets, or jump directly to creating a [trading bot](/docs/obside-chat/automate-your-trading/create-a-trading-bot.md) targeting that shortlist.
{% endhint %}

# Understand the market

Obside Chat isn't only an automation tool — it's also a **market assistant** that helps you interpret what's happening and improve your skills.

* [**Make sense of the news**](/docs/obside-chat/understand-the-market/decode-the-news.md) — summaries and impact of economic events.
* [**Get a market read**](/docs/obside-chat/understand-the-market/read-the-market.md) — directional reads and macro overviews.
* [**Learn the basics**](/docs/obside-chat/understand-the-market/learn-the-basics.md) — pedagogical explanations on trading and investing.

{% hint style="info" %}
These conversations don't create agents: they give you contextualized answers based on market data, news and financial knowledge. Each message uses a small share of your monthly AI usage (well under 1% of the Plus monthly allowance on a typical message, see [When are credits consumed?](/docs/credits/how-it-works.md#in-the-chat)).
{% endhint %}

# Make sense of the news

## Overview

Ask questions about economic, financial or geopolitical news, and Obside Chat gives you a **clear summary** along with an **analysis of the potential impact** on the markets or your positions.

The assistant draws on recent news and can **put events in perspective** (market positioning, macro calendar, affected sectors, comparable historical reactions).

## How to phrase your request

Specify **the event or topic**, and if possible **the scope** for the analysis (your positions, a sector, an asset class).

## Example requests

{% hint style="info" %}
**Example #1 — Fed decision**

"Summarize today's Fed decision and explain what it implies for my equity positions."
{% endhint %}

{% hint style="info" %}
**Example #2 — Earnings reading**

"Apple just released earnings. Break down the key points and the likely market reaction."
{% endhint %}

{% hint style="info" %}
**Example #3 — Geopolitical impact**

"Explain how the latest developments in the Middle East affect the oil market and which stocks are most exposed."
{% endhint %}

# Get a market read

## Overview

Ask Obside for a **directional read** on an asset or index, or for an **overview** of the upcoming key events. The assistant synthesizes positioning, macro context, earnings calendar and technical signals to give you a clear picture.

This read is not investment advice — it's a **lens** to help you build your own view.

## How to phrase your request

Specify **the asset or index**, **the timeframe** you're interested in, and, if relevant, **the angles** you want covered (technical, macro, flows, on-chain for crypto…).

## Example requests

{% hint style="info" %}
**Example #1 — S\&P 500 outlook**

"Give me a directional read on the S\&P 500 for the next two weeks based on positioning, macro calendar and earnings."
{% endhint %}

{% hint style="info" %}
**Example #2 — Earnings preview**

"What are this week's most important earnings reports and what to watch in each?"
{% endhint %}

{% hint style="info" %}
**Example #3 — Bitcoin read**

"Give me a directional read on Bitcoin for the next 2-4 weeks based on flows, macro conditions and on-chain activity."
{% endhint %}

# Learn the basics

## Overview

Obside Chat acts as a **built-in coach**. Ask substantive questions — about a trading concept, a financial term, a risk-management method, or how to read a document — and get **pedagogical, simple and contextualized** answers.

It's the ideal tool to grow your skills at your own pace, without going through dozens of tutorials.

## How to phrase your request

Ask your question as you would to a mentor: **mention your level** if you wish (beginner, intermediate), and **the context** in which you want to apply what you learn.

## Example requests

{% hint style="info" %}
**Example #1 — Reading an earnings report**

"Teach me how to read a quarterly earnings report and what to focus on."
{% endhint %}

{% hint style="info" %}
**Example #2 — Position sizing**

"Explain how to size positions based on account risk and stop distance."
{% endhint %}

{% hint style="info" %}
**Example #3 — Options basics**

"Explain how options work, with beginner-friendly examples."
{% endhint %}

# Introduction

## What is an agent?

An **agent** is an automation you create by describing your intent in natural language. On Obside, anything that runs on your behalf — a scheduled trade, a trading bot, an AI trader, a smart alert — is an agent.

Each agent has:

* A **description** written in natural language (your intent).
* A **code** automatically generated by Obside's AI, formalizing this description.
* One or more **assets** it operates on.
* One or more **capabilities** enabled (trading, notifications, AI, news).
* A history of **backtests** and **executions**.

***

## Types of agents

Depending on what you describe in the chat, the agent takes a different form:

| Type | Description | Typical capabilities |
|---|---|---|
| **Scheduled trade** | A one-off operation to execute | Trading |
| **Trading bot** | A strategy following deterministic rules | Trading |
| **AI trader** | An autonomous agent powered by an LLM | Trading + AI + News |
| **Smart alert** | A condition that notifies you | Notifications |
| **Portfolio monitoring** | Continuous tracking of your positions | Notifications |

{% hint style="success" %}
You can **combine multiple capabilities** within a single agent — for example, a trading bot that also notifies you of every trade.
{% endhint %}

***

## The lifecycle of an agent

1. **Creation** — you describe your intent in [Obside Chat](/docs/obside-chat/introduction.md). The AI rephrases, you confirm, and the agent is created.
2. **Backtest** _(for trading agents)_ — you test the agent on historical data to evaluate past performance. See [Backtest an agent](/docs/agents/backtest-an-agent.md).
3. **Execution** — you run the agent on a **[portfolio](/docs/portfolios/portfolios.md)**, in **demo** (paper trading) or **live** mode. See [Run an agent](/docs/agents/run-an-agent.md).
4. **Monitoring & adjustments** — you review performance, trades, triggered notifications, and iterate via the chat linked to the agent.

{% hint style="info" %}
All your agents are accessible from the **My agents** section in the sidebar. You can organize them in folders, filter by capability or status, and find them via search.
{% endhint %}

You can also **share an agent** with a public link, and **publish it to the Library** for the community to copy — or start from an agent someone else published. See [Share an agent](/docs/agents/share-an-agent.md) and [Library](/docs/agents/library.md).

To go further:

* [Agent capabilities](/docs/agents/capabilities.md)
* [Create an agent](/docs/agents/create-an-agent.md)
* [Backtest an agent](/docs/agents/backtest-an-agent.md)
* [Run an agent](/docs/agents/run-an-agent.md)
* [Share an agent](/docs/agents/share-an-agent.md)
* [Library](/docs/agents/library.md)
* [Smart Chart](/docs/agents/smart-chart.md)

# Agent capabilities

Each agent has **one or more capabilities** that define what it can do. These capabilities are activated automatically based on the description you provide in the chat. You can view them in the **Overview** tab of the agent's page.

***

## Trading

The agent can **place orders** on the markets.

* In **demo mode (paper trading)**, the agent simulates its orders with virtual capital.
* In **live mode**, the agent executes its orders through a connected broker.

{% hint style="info" %}
Live execution availability depends on your region and the broker used. See [Availability by region](/docs/platforms/availability-by-region.md).
{% endhint %}

## Notifications

The agent can **send you messages** when a defined event occurs (entry signal, exit, alert, met condition, etc.).

The available channels are **Telegram**, **push notifications** and **email**. See [Notification channels](/docs/alerts-and-notifications/channels.md).

{% hint style="success" %}
A smart alert is an agent that has **only the Notifications capability** enabled: it doesn't place orders, it notifies you.
{% endhint %}

## Powered by AI

The agent uses a **large language model** (LLM) to analyze context and make its decisions. This is the signature capability of **AI traders**.

This capability opens possibilities that go beyond deterministic rules: news interpretation, reading complex market structures, adaptive decisions based on the market regime.

## News

The agent can **read real-time news** from X (Twitter) and other sources, and integrate them into its decisions or alerts.

Especially useful for:

* Catalyst-driven alerts (earnings, announcements, geopolitical events).
* Bots or AI traders that react to news.
* Portfolio monitoring with news sensitivity.

***

## Combining capabilities

A single agent can enable multiple capabilities at the same time.

{% hint style="info" %}
**Example combinations:**

* **Trading + Notifications** — a bot that places orders and notifies you on every trade.
* **Trading + AI + News** — an AI trader that reads the news and decides autonomously.
* **Notifications + News** — an alert that triggers on specific news.
{% endhint %}

# Create an agent

## Creation through natural language

On Obside, you create an agent by **describing your intent in the chat**. No more step-by-step forms: you write what you want to achieve, and the AI takes care of turning your description into a ready-to-use agent.

The process unfolds in a few steps:

1. **You describe your intent** in Obside Chat — what to automate, on which assets, with which rules.
2. **The AI rephrases** your request to make sure it's correctly understood. You can correct or clarify if needed.
3. **The agent is created**: its description, code, assets and capabilities are defined automatically.
4. **You can iterate** through the chat linked to the agent to refine, test or optimize it.

***

## The building blocks of an agent

To write your request well, it helps to understand what an agent is made of. The following pages detail each element:

* [**Description in natural language**](/docs/agents/create-an-agent/description.md) — how to phrase your intent so it's faithfully interpreted.
* [**Assets**](/docs/agents/create-an-agent/assets.md) — selecting the instruments the agent operates on.
* [**Entry rules**](/docs/agents/create-an-agent/entry-rules.md) — when and how the agent opens a position.
* [**Exit rules**](/docs/agents/create-an-agent/exit-rules.md) — when and how the agent closes a position.
* [**Rules reliability**](/docs/agents/create-an-agent/rules-reliability.md) — how Obside guarantees the agent does what you described.
* [**Example requests**](/docs/agents/create-an-agent/examples.md) — concrete examples for trading bots, investing, alerts and market analysis.
* [**Indicators list**](/docs/agents/create-an-agent/indicators-list.md) — reference of available technical indicators.

{% hint style="success" %}
**Tip**: if you're out of inspiration, the chat offers **idea suggestions** in each of the three families of use cases (automation, alerts, market understanding). Click a suggestion to start from an example and adapt it to your needs.
{% endhint %}

## Prefer a guided path? The Agent Builder

If you'd rather not start from a blank message, the **Agent Builder** creates your agent **step by step, by answering a few questions** — what to watch, on which assets, what to do when it triggers. It's the same result as describing it in the chat, with more guidance along the way.

# Description in natural language

## Overview

The **description** is the heart of your agent: it's the text you write in the chat to express your intent. From this description, Obside:

* Rephrases your request to verify correct understanding.
* Selects the relevant assets.
* Activates the necessary capabilities (trading, notifications, AI, news).
* Generates the code that formalizes your intent.

{% hint style="info" %}
The description and the code of the agent are accessible at any time in the **Overview** and **How it works** tabs of the agent's page. You can iterate on the description through the linked chat to fine-tune the behavior.
{% endhint %}

***

## How to write a good description

Obside's AI understands a very wide range of phrasings, but a **smooth and logical syntax** maximizes interpretation efficiency. Well-structured, clearly punctuated sentences reduce the risk of error.

{% hint style="danger" %}
The AI is not a magical technology that's supposed to guess what hasn't been explicitly stated. An omission or an unspecified inference can result in a partial or incorrect interpretation of your request.
{% endhint %}

{% hint style="success" %}
A good way to write a clear description is to ask yourself:

"If I were teaching my strategy to someone else using only this description, would they understand everything or would they have questions on unclear aspects?".

This approach helps you maximize the precision of your request.
{% endhint %}

***

## What you can express

Depending on the type of agent you want to create, your description can include:

* **For a scheduled trade** — what to buy/sell, at what time, under what conditions.
* **For a trading bot** — entry rules, exit rules, risk management, target assets. See [Entry rules](/docs/agents/create-an-agent/entry-rules.md) and [Exit rules](/docs/agents/create-an-agent/exit-rules.md).
* **For an AI trader** — mandate, asset universe, risk constraints, style.
* **For an alert** — the condition to watch, the notification channel.
* **For monitoring** — the metrics or events to track on your portfolio.

***

## Iterating on your agent

Once your agent is created, you can **continue the conversation** in the chat linked to the agent to:

* Adjust a parameter (e.g. change the entry threshold).
* Add a condition (e.g. exclude certain hours).
* Modify risk management.
* Extend the agent to new assets.

The agent is **automatically updated** with each validated adjustment.

# Assets

## Selecting assets

When you create an agent, you specify **which assets it should operate on**. There are several ways to do this:

* **Mention them directly in your description** — e.g. "DCA $200 into Bitcoin every Friday".
* **Select them in the chat** through the **Add** → **Assets** button: browse the catalog (10,000+ instruments) and attach them to the conversation.
* **Pick them from your broker** — if you've connected a platform, you can choose the assets available at that broker.

{% hint style="info" %}
You can **mix asset classes** within a single agent: stocks, cryptos, currencies, indices, futures, ETFs, commodities.
{% endhint %}

## Available asset categories

More than **10,000 instruments** are available, organized by category:

* **Stocks** (Apple, Tesla, Nvidia, etc.)
* **Cryptos** (Bitcoin, Ethereum, etc.)
* **Currencies** (EURUSD, USDJPY, etc.)
* **Indices** (S\&P 500, Nasdaq, CAC 40, etc.)
* **Commodities** (gold, oil, etc.)
* **ETFs**
* **Futures**

The **number of assets per agent** depends on your subscription plan. See [Pricing](/docs/misc/pricing.md).

{% hint style="info" %}
You can browse the full catalog in the **Assets** section of the sidebar. Every asset has its own page — price, chart, key stats and related news — and you can keep a **watchlist** of the ones you follow.
{% endhint %}

***

## Where each asset trades

* In a **backtest**, you tune the per-asset trading conditions (commission, spread, swap fees, leverage) in the **Fees** step, so the simulation reflects realistic costs. See [Backtest an agent](/docs/agents/backtest-an-agent.md).
* In a **demo or live execution**, the agent runs on a **portfolio**. A demo portfolio uses Obside's market data; a real portfolio trades on its connected broker, with that broker's fees and market specifications. See [Portfolios](/docs/portfolios/portfolios.md) and [Run an agent](/docs/agents/run-an-agent.md).

# Entry rules

## Overview

**Entry rules** define **when your agent opens a position**, whether it's a buy (Long) or a sell (Short). They are part of the [description](/docs/agents/create-an-agent/description.md) you write in natural language.

{% hint style="info" %}
**Good to know:**

* In **trading**, an agent must include both **entry and exit rules** to manage positions actively.
* In **investing**, you can define entry rules only if you wish to accumulate assets without systematic selling.
{% endhint %}

***

## What can Obside's AI currently do?

Obside's AI keeps improving, and we ultimately aim to support automation of all kinds of strategies, even the most complex ones.

A wide range of possibilities is available, including:

### Using different timeframes

The agent's backtest runs on the timeframe you specify in your rules. You can **combine conditions occurring on different timeframes**. For example, one condition must hold on a 1-hour chart, while another must hold on a 15-minute chart. Specify the timeframe to analyze for each condition involving a different unit.

The smallest available timeframe is **1 minute**.

{% hint style="info" %}
**Examples:**

"Buy if the RSI is below 30 on the 1-hour chart and the 50-period moving average crosses above the 200-period moving average on the 4-hour chart."

"Enter long if the price breaks the high of the last 20 candles on the 1-minute chart, but only if the trend is bullish on the 1-hour chart."
{% endhint %}

### Managing rule timing

Not all entry conditions need to occur simultaneously. You can wait for one condition to materialize, then wait for another. This is the **timing of events**. Be **explicit and clear** when writing your rules so the AI understands the timing you have in mind.

{% hint style="info" %}
**Examples:**

"Take a short position if the RSI exceeds 70, then drops back below this level within the next 5 candles."

"If a bullish crossover of the 50 and 200 moving averages occurs, then wait for a bullish candle to close above the 200 moving average before buying."

"Sell if the MACD crosses below its signal line and at the same time the RSI is bearish."
{% endhint %}

### Using technical indicators

You can include [**technical indicators**](/docs/agents/create-an-agent/indicators-list.md) in your rules. Obside lets you **combine multiple indicators** within a single agent, on different timeframes and timings.

Each indicator uses default values. If you want to use a different value or parameter for an indicator, simply specify it in your rules. For example: "We use the RSI with a period of 35" or "RSI (35)".

{% hint style="success" %}
You can also select and configure an indicator **directly from the chat** through the **Add** → **Indicators** button. It's then automatically referenced in your conversation with its parameters.
{% endhint %}

{% hint style="danger" %}
Using an indicator not included in the available indicators list could trigger an interpretation error or a backtest failure.
{% endhint %}

### Using Japanese candlestick patterns

You can include **chart patterns** and **Japanese candlestick structures** (candles) in your agent's rules. Many patterns are understood by our AI, including the most common ones (engulfing, morning star, three white soldiers, etc.).

{% hint style="info" %}
**Examples:**

"Buy when the price forms a bullish engulfing after three consecutive red candles."

"Buy after the formation of a bullish hammer with a lower wick representing at least 50% of the candle."

"Buy when three white soldiers form on a 1-hour chart."
{% endhint %}

### Using measurement units

You can use units of measure such as **pips, prices, ticks, percentages, ATR multiples (volatility) or time**. You can even specify from which reference point a measurement should apply. This reference point can be a moment in time, or a price or indicator value.

{% hint style="info" %}
**Examples:**

"If the price drops 2% from the previous day's close, then…"

"When the maximum price of the last two weeks is exceeded by 2 pips, buy."

"When the average price of the last 20 days rises for 4 consecutive days, then…"
{% endhint %}

### Custom risk and position sizing management

Obside lets you dynamically adjust your position size based on specific criteria. You can define evolving rules to manage risk based on trade outcomes or other market conditions.

{% hint style="info" %}
**Examples:**

"Start with a 0.5% risk, then decrease it by 10% on a losing trade and increase it by 5% on a winning trade."

"Invest $50 in Bitcoin every week. If BTC's price is below its 200-day moving average, double my investment."

"I want to risk 1% of my capital on Tesla stock if the price is below the EMA 200, or 0.5% if the price is above."
{% endhint %}

### Asset correlations

With Obside, your trading decisions can be influenced by the behavior of other assets. You can trigger orders based on conditions observed on another market.

{% hint style="info" %}
**Example:**

"Buy Bitcoin with 5% of my capital when the NASDAQ rises for 3 consecutive days."
{% endhint %}

### Time-based execution

Obside's AI handles precise time conditions to execute your orders. You can specify **fixed hours** or use **events tied to market sessions**.

{% hint style="info" %}
**Examples:**

"Buy at 2 PM UTC."

"Buy at the New York session open."
{% endhint %}

### Mid- to long-term investing concepts: DCA and rebalancing

Obside supports strategies tailored for long-term investors, including **Dollar Cost Averaging (DCA)** and **portfolio rebalancing**.

{% hint style="info" %}
**Examples:**

"I want to DCA (Dollar Cost Averaging) $30 into Bitcoin every first Monday of the month at 10 AM."

"Build a portfolio of 30% Bitcoin, 40% S\&P 500 and 30% CAC40. Each quarter, invest $500 and rebalance the portfolio to maintain this allocation."
{% endhint %}

***

For complete description examples, see [Example requests](/docs/agents/create-an-agent/examples.md).

# Exit rules

## Overview

**Exit rules** define **when your agent closes a position**, whether it's a buy (Long) or a sell (Short). Like entry rules, they are part of the [description](/docs/agents/create-an-agent/description.md) in natural language.

Everything that applies to entry rules also applies to exit rules.

{% content-ref url="entry-rules.md" %}
[entry-rules.md](/docs/agents/create-an-agent/entry-rules.md)
{% endcontent-ref %}

{% hint style="info" %}
**Good to know:**

* In **trading**, an agent must include both **entry and exit rules** to manage positions actively.
* In **investing**, you can define entry rules only if you wish to accumulate assets without systematic selling. In other words, **it's possible to have no exit rules**.
{% endhint %}

Your exit rules can be based on technical indicators, patterns, units of measure, or any other **condition already used in the entry rules**.

You can also use a **stop-loss** as well as **take-profits**.

It's also possible to perform **partial exits** of your positions, allowing you to lock in gains or minimize losses before fully closing a position.

***

## Using a stop-loss

To limit the loss on a trade, a stop-loss can be specified in your exit rules. The stop-loss can be expressed as a percentage, in price, in price change, in ticks, in pips, in points, in indicator value change, or in ATR multiples.

You can also place your stop-loss on a technical level, such as the high or low of the last X days.

The stop-loss can be fixed, trailing, or moved to breakeven if needed. Just specify what you want. The possibilities are wide.

{% hint style="success" %}
Obside's AI knows that for a buy trade, the stop-loss goes below the entry price, and for a sell trade, it goes above the entry price.
{% endhint %}

You can however dissociate buy and sell trades, giving a different stop-loss for each side.

{% hint style="info" %}
**Examples:**

"Place a stop-loss at 2 ATR below the low or above the high of the last 5 days."

"Cut the trade if the price moves 4% against us."

"Trailing stop-loss at 1%, moved to breakeven once we reach 1.5% profit."

"SL on the low of the last 10 candles for a buy trade. SL on the high of the last 10 candles for a sell trade."
{% endhint %}

***

## Using a take-profit

As the name suggests, **take-profit** is meant to "take your profits", i.e. close your trade to lock in gains.

It can also be expressed as a percentage, in price, in price change, in ticks, in pips, in points, in indicator value change, or in ATR multiples.

Another notable unit is the **R**, which represents the gain/risk ratio. In other words, it's the distance between the entry price and the stop-loss on your trade. By expressing the take-profit in multiples of R (e.g. 1 R, 2 R, 2.5 R), your trade closes when the specified gain/risk ratio is reached.

The take-profit can be full or partial, depending on whether you want to lock in all or part of the gains on each trade. Like the stop-loss, it can also be trailing.

{% hint style="info" %}
**Examples:**

"Take-profit at 2 R."

"Partial TP of 50% when the price has risen by 10 pips, and 50% when it has risen by 20 pips."

"Set a take-profit at 5% from our entry price."

"TP at 1 R with a 0.25 R trailing."

"Take profits at +2 ATR."
{% endhint %}

# Rules reliability

## How can you be sure the AI-generated code matches your description?

Several platform features are designed to guarantee the **reliability, precision and transparency** of AI-generated agents.

### 1. Guided creation

You're encouraged to write your description as if you were teaching it to someone else. The more detailed and precise the description, the fewer interpretation gaps remain.

### 2. Rephrasing before agent creation

Before creating the agent, Obside Chat **analyzes, interprets and rephrases** your description. This step lets you verify that your intent is correctly understood and highlights any ambiguities. You can correct or complete it before launching the creation.

### 3. Consolidated description after creation

Once the agent is created, its **consolidated description** is accessible at any time in the **Overview** tab of the agent's page. It restates in a structured way what the agent actually does, and offers an additional layer of verification.

### 4. Generated code accessible

The **agent's code** (automatically generated by the AI) is accessible in the **How it works** tab. It formalizes the description in executable form and allows precise inspection of the behavior.

### 5. Backtest with full trades list

When running a backtest, you can **view or export the full list of trades** generated by the agent. This transparency lets you verify each trade in detail and confirm overall consistency.

### 6. Built-in chart

Each backtest comes with a price chart showing all entry and exit points of the trades.

* The chart displays not only the positions, but also the reason for each entry and exit.
* You can add technical indicators on the chart to verify that all signals match the defined rules.

### 7. Detailed performance metrics

Dozens of **performance metrics** are calculated and displayed automatically. They give you deep insight into the agent's behavior, profitability and risk profile.

### 8. Iterating through the chat

If you notice a behavior that doesn't match your intent, you can **continue the conversation** in the chat linked to the agent to correct or fine-tune it.

{% hint style="success" %}
Thanks to these combined features, Obside guarantees that every agent is **transparent, verifiable and interpretable** at every step.
{% endhint %}

# Example requests

To help you write better descriptions and get the most out of Obside's AI, we've grouped **concrete examples** by use case.

* [**Trading bots**](/docs/agents/create-an-agent/examples/trading.md) — dynamic strategies with precise entry and exit rules.

{% content-ref url="trading.md" %}
[trading.md](/docs/agents/create-an-agent/examples/trading.md)
{% endcontent-ref %}

* [**Investing**](/docs/agents/create-an-agent/examples/investing.md) — mid- and long-term approaches: DCA, rebalancing, progressive capital management.

{% content-ref url="investing.md" %}
[investing.md](/docs/agents/create-an-agent/examples/investing.md)
{% endcontent-ref %}

* [**Alerts**](/docs/agents/create-an-agent/examples/alerts.md) — notifications triggered by technical, fundamental or news-based conditions.

{% content-ref url="alerts.md" %}
[alerts.md](/docs/agents/create-an-agent/examples/alerts.md)
{% endcontent-ref %}

* [**Market analysis**](/docs/agents/create-an-agent/examples/market.md) — questions to decode the news, read the market or learn.

{% content-ref url="market.md" %}
[market.md](/docs/agents/create-an-agent/examples/market.md)
{% endcontent-ref %}

***

{% hint style="success" %}
A good way to write clear rules is to ask yourself:

**"If I were teaching my strategy to someone else using only these rules, would they understand everything or would they have questions on unclear aspects?".**

This approach helps you maximize the precision of your rules.

If the answer is **YES**, your rules are clear and you can create your agent.\
If the answer is **NO**, you should clarify them to avoid any ambiguity.
{% endhint %}

# Trading bots

**Trading bots** rely on active position management, with **well-defined entry and exit rules**. They let you exploit market opportunities across different timeframes, from scalping to swing trading.

On this page, you'll find **concrete examples** to help you write your own descriptions.

{% hint style="success" %}
A good way to write clear rules is to ask yourself:

"If I were teaching my strategy to someone else using only these rules, would they understand everything or would they have questions on unclear aspects?".

If the answer is YES, your rules are clear and you can create your agent.\
If the answer is NO, you should clarify them to avoid any ambiguity.
{% endhint %}

## Examples

<mark style="color:purple;">Example #1</mark>

**Entry rules:** On a 4-hour chart, buy when the 5-period moving average crosses above the 10-period moving average.

**Exit rules:** Place a 50% stop-loss at 2 ATR. Set up a 50% stop-loss if the moving average crosses below the 10-period moving average. Take a partial profit of 40% at 1 ATR. Take a partial profit of 60% at 2 ATR.

***

<mark style="color:purple;">Example #2</mark>

**Entry rules:** On a 1-hour chart, enter long when the MACD is bullish for 5 consecutive candles.

**Exit rules:** Exit 50% of the position when the MACD turns from positive to negative and the other 50% when the EMA 9 crosses below the EMA 21.

***

<mark style="color:purple;">Example #3</mark>

**Entry rules:** On a 15-minute timeframe, buy when the MACD line crosses above the signal line and the relative strength index is above 50. Sell when the MACD line crosses below the signal line and the RSI is below 50.

**Exit rules:** Close when the RSI drops below 50 for a buy, or rises above 50 for a sell.

***

<mark style="color:purple;">Example #4</mark>

**Entry rules:** When the Tenkan is above the Kijun for at least 10 periods, we buy on the 2-hour chart. The reverse for selling.

**Exit rules:** Sell when there's a bearish crossover between Tenkan and Kijun. The reverse for sells.

***

<mark style="color:purple;">Example #5</mark>

**Entry rules:** When the average ADX of the last 10 periods crosses the average ADX of the last 20 periods, we buy. The opposite for selling. All on a 4-hour chart.

**Exit rules:** SL at 3 times the ATR distance. TP at 2.15 R.

***

<mark style="color:purple;">Example #6</mark>

**Entry rules:** Use the 4-hour chart, the 30-minute chart, and the 50 and 200 simple moving averages on each timeframe. To buy, the price must be above the 200 moving average on the 4H, and the 50 moving average must cross the 200 moving average on the 30M.

**Exit rules:** The trade is closed when the reverse crossover occurs between the 50 and 200 moving averages on the 30M chart.

***

<mark style="color:purple;">Example #7</mark>

**Entry rules:** On an H1 chart, wait for the price to break and close above the Ichimoku Cloud. Then wait for the crossover where the Conversion Line (Tenkan) goes above the Base Line (Kijun). Buy at the open of the next candle after the crossover.

**Exit rules:** Place a stop-loss below the breakout candle. Take profits when the Conversion Line crosses below the Base Line.

***

<mark style="color:purple;">Example #8</mark>

**Entry rules:** If there's a bearish RSI divergence on the 4-hour chart, and at the same time a regular bearish RSI divergence on the 30-minute chart, then sell.

**Exit rules:** SL on the high of the 4H candle that shows the divergence. TP when the RSI drops below 50.

# Investing

**Investing** agents differ from **trading** with a mid- and long-term approach. They aim to gradually accumulate assets or to optimize a portfolio according to defined goals.

Unlike trading, an investing agent can include **only entry rules** (with no exit rules), notably if the goal is to **build a portfolio without systematic selling**.

{% hint style="success" %}
A good way to write clear rules is to ask yourself:

"If I were teaching my strategy to someone else using only these rules, would they understand everything or would they have questions on unclear aspects?".

If the answer is YES, your rules are clear and you can create your agent.\
If the answer is NO, you should clarify them to avoid any ambiguity.
{% endhint %}

## Examples

<mark style="color:purple;">Example #1 — Progressive investing (DCA)</mark>

Buy $50 of Bitcoin every first Monday of the month at 10 AM.

***

<mark style="color:purple;">Example #2 — Automatic portfolio rebalancing</mark>

Build a portfolio of 30% Bitcoin, 40% S\&P 500 and 30% CAC40.

Each quarter, invest $500 and rebalance the portfolio to maintain this allocation.

***

<mark style="color:purple;">Example #3 — Conditional investing</mark>

Start a $1,000 portfolio with 40% stocks (Tesla, Microsoft), 30% crypto (Bitcoin, Ethereum) and 30% currencies (EURUSD, JPYUSD).

Inject $100 each month into the asset with the lowest performance the previous month to smooth volatility.

***

<mark style="color:purple;">Example #4</mark>

I want to build a portfolio of 25% Apple, 25% Bitcoin, 25% Tesla and 25% Microsoft, injecting $100 of capital every month.

Sell 50% of my portfolio if gains reach +30%.

***

<mark style="color:purple;">Example #5</mark>

Create a portfolio with 25% Apple, 25% Bitcoin, 25% S\&P 500 and 25% Gold.

Rebalance every month and inject $100.

If an asset drops more than 10% in a month, halve its allocation and redistribute on the others.

***

<mark style="color:purple;">Example #6</mark>

Create a portfolio with 25% Tesla, 25% Bitcoin, 25% S\&P 500 and 25% oil.

Every month, inject $100 and allocate the capital only to the two assets with the best performance over the last 6 months.

***

<mark style="color:purple;">Example #7</mark>

Build a portfolio of 50% Bitcoin and 50% Nasdaq with $200 starting capital.

Then, invest $200 each month, split across these 2 assets.

If the Nasdaq drops at least 5% from one month to the next, invest in Gold instead of the Nasdaq for the following month.

For Bitcoin, same logic but if the drop is at least 10%.

***

<mark style="color:purple;">Example #8</mark>

Build a portfolio of 30% defensive assets (Gold, Silver) and 70% speculative assets (Tesla, Apple, Alphabet, Meta, Amazon).

Invest $500 per quarter.

If a speculative asset performs above +15% over a quarter, sell half of the gains and redistribute equally on the defensive assets.

***

<mark style="color:purple;">Example #9</mark>

Invest $1,000 initially split between Bitcoin, USDJPY, Amazon and S\&P 500.

Each month, inject $100 split across the two assets with the lowest performance over the last 90 days.

***

<mark style="color:purple;">Example #10</mark>

Invest $100 per month split between Bitcoin, Gold and Nasdaq.

If an asset is below its 200-day moving average, double the investment on it the following month to take advantage of a possible rebound.

# Alerts

**Smart alerts** are agents whose only enabled capability is **Notifications**: they don't place any orders, they notify you as soon as a condition is met.

You can combine **technical, fundamental, macroeconomic or news-based signals** to only trigger the alert in truly interesting contexts.

{% hint style="info" %}
Alerts can be sent via **Telegram, push notifications or email**. See [Notification channels](/docs/alerts-and-notifications/channels.md).
{% endhint %}

## Examples — one-off alerts

<mark style="color:purple;">Example #1 — Earnings + price filter</mark>

Alert me if Apple beats earnings AND gives guidance above consensus AND the stock doesn't move more than 4% up in after-hours.

***

<mark style="color:purple;">Example #2 — RSI divergence + trend filter</mark>

Alert me on a bullish RSI divergence on the daily of MSFT with price above the EMA 200.

***

<mark style="color:purple;">Example #3 — Multi-timeframe confirmation</mark>

Alert me if SPY shows a bullish MACD crossover on 1H while the daily RSI is above 50.

***

## Examples — portfolio monitoring

<mark style="color:purple;">Example #4 — Concentration risk</mark>

Email me if any single stock position exceeds 30% of my portfolio.

***

<mark style="color:purple;">Example #5 — Catalyst tracking</mark>

Alert me on any earnings release, rating change or significant news affecting the stocks I hold.

***

<mark style="color:purple;">Example #6 — Underperformance</mark>

Notify me if any of my positions underperforms the S\&P 500 by more than 10% over the last 30 days.

***

## Examples — opportunities

<mark style="color:purple;">Example #7 — Short candidates</mark>

Notify me as soon as European stocks appear with declining YoY revenue, P/E above 30 and debt/EBITDA above 4.

***

<mark style="color:purple;">Example #8 — Beat & cheap</mark>

Alert me weekly on stocks that beat earnings and whose P/E is below the sector average.

***

<mark style="color:purple;">Example #9 — Persistent undervaluation</mark>

Alert me monthly on European stocks with a P/E below 12 and a dividend yield above 3%.

# Market analysis

Not every request leads to creating an agent. You can also use Obside Chat to **understand the news, read the market or grow your skills**. These conversations use a small share of your monthly AI usage (well under 1% of the Plus monthly allowance on a typical message, see [When are credits consumed?](/docs/credits/how-it-works.md#in-the-chat)) and don't create any automation.

## Examples — make sense of the news

<mark style="color:purple;">Example #1 — Fed decision</mark>

Summarize today's Fed decision and explain what it implies for my equity positions.

***

<mark style="color:purple;">Example #2 — Earnings reading</mark>

Apple just released earnings. Break down the key points and the likely market reaction.

***

<mark style="color:purple;">Example #3 — Geopolitical impact</mark>

Explain how the latest developments in the Middle East affect the oil market and which stocks are most exposed.

***

## Examples — get a market read

<mark style="color:purple;">Example #4 — S\&P 500 outlook</mark>

Give me a directional read on the S\&P 500 for the next two weeks based on positioning, macro calendar and earnings.

***

<mark style="color:purple;">Example #5 — Earnings preview</mark>

What are this week's most important earnings reports and what to watch in each?

***

<mark style="color:purple;">Example #6 — Bitcoin read</mark>

Give me a directional read on Bitcoin for the next 2-4 weeks based on flows, macro conditions and on-chain activity.

***

## Examples — learn the basics

<mark style="color:purple;">Example #7 — Reading an earnings report</mark>

Teach me how to read a quarterly earnings report and what to focus on.

***

<mark style="color:purple;">Example #8 — Position sizing</mark>

Explain how to size positions based on account risk and stop distance.

***

<mark style="color:purple;">Example #9 — Options basics</mark>

Explain how options work, with beginner-friendly examples.

# Indicators list

## Custom indicators

Can't find the indicator you need? **Ask the assistant to build it for you.** Custom indicators are authored for you in the chat: describe the logic, and Obside writes the indicator. You can then:

* **Use it in your agents**, exactly like a built-in indicator.
* **Plot it on [Smart Chart](/docs/agents/smart-chart.md)**.
* Review its **source code**, and iterate on it through the chat.

Already have an indicator written in TradingView Pine Script, MetaTrader MQL4/MQL5 or Python? Use **Import from code** in [Smart Chart](/docs/agents/smart-chart.md#import-an-indicator-from-code): the assistant converts it into a custom indicator and plots it on your chart.

Your custom indicators are listed in the **Custom** group of the Smart Chart indicator picker (**Add indicator**), on every chart.

## Built-in indicators

{% hint style="success" %}
The list of available technical indicators is regularly updated to integrate new indicators based on market trends and user feedback.

If you have suggestions or wish to see a specific indicator added to Obside, feel free to [contact us](/docs/readme.md#contact-us). Your contribution is valuable in helping us adapt our services to your needs.
{% endhint %}

**This list is not exhaustive**: the complete list of available indicators can be browsed directly in Obside Chat through the **Add** → **Indicators** button, where you can also configure their parameters before referencing them in your conversation.

<table data-full-width="true"><thead><tr><th>Indicator</th><th>Default value</th><th>Possible values</th></tr></thead><tbody><tr><td>Average Directional Index (ADX)</td><td>Period: 14</td><td>Period: any number</td></tr><tr><td>Average True Range (ATR)</td><td>Period: 14</td><td>Period: any number</td></tr><tr><td>Beepboop</td><td></td><td></td></tr><tr><td>Bollinger Bands (BB)</td><td><ul><li>Source: close</li><li>Moving average period: 20</li><li>Standard deviations: 2</li></ul></td><td><ul><li>Source: open, high, low, close</li><li>Moving average period: any number</li><li>Standard deviations: any number</li></ul></td></tr><tr><td>Bollinger Band Width Percentile (BBWP)</td><td></td><td></td></tr><tr><td>Chaikin Money Flow (CMF)</td><td></td><td></td></tr><tr><td>Choppiness Index</td><td></td><td></td></tr><tr><td>Commodity Channel Index (CCI)</td><td>Period: 20</td><td>Period: any number</td></tr><tr><td>Divergences</td><td><ul><li>Source: close</li><li>Period: 5</li><li>Pivot points between 2 points: 10</li><li>Max candles between 2 points: 70</li><li>Indicator to compare with price: RSI</li><li>Min candles between 2 points: 5</li></ul></td><td><ul><li>Source: close, high/low</li><li>Period: any number</li><li>Pivot points between 2 points: any number</li><li>Max candles between 2 points: any number</li><li>Indicator to compare with price: any indicator</li><li>Min candles between 2 points: any number</li></ul></td></tr><tr><td>Donchian Channels (DC)</td><td></td><td></td></tr><tr><td>Elder Impulse System</td><td></td><td></td></tr><tr><td>Exponential Moving Average (EMA)</td><td>Period: no default value</td><td>Period: any number</td></tr><tr><td>Fair Value Gap (FVG)</td><td></td><td></td></tr><tr><td>HalfTrend</td><td><ul><li>Amplitude: 2</li><li>Channel deviation: 2</li></ul></td><td><ul><li>Amplitude: any number</li><li>Channel deviation: any number</li></ul></td></tr><tr><td>Heikin Ashi Relative Strength Indicator  (HARSI)</td><td>Period: 14</td><td>Period: any number</td></tr><tr><td>Highs and lows of the <em>x</em> last candles</td><td>Period: no default value</td><td>Period: any number</td></tr><tr><td>Hull Moving Average (HMA)</td><td></td><td></td></tr><tr><td>Ichimoku Cloud</td><td><ul><li>Tenkan period (Conversion Line): 9</li><li>Kijun period (Base Line): 26</li><li>Senkou Span B (Leading Span B): 52</li><li>Chikou Span (Lagging Span): 26</li></ul></td><td><ul><li>Tenkan period (Conversion Line): any number</li><li>Kijun period (Base Line): any number</li><li>Senkou Span B (Leading Span B): any number</li><li>Chikou Span (Lagging Span): any number</li></ul></td></tr><tr><td>KDJ</td><td>Period: 9</td><td>Period: any number</td></tr><tr><td>Keltner Channels</td><td></td><td></td></tr><tr><td>Money Flow Index (MFI)</td><td></td><td></td></tr><tr><td>Moving Average (MA)</td><td>Period: no default value</td><td>Period: any number</td></tr><tr><td>Moving Average Convergence/Divergence (MACD)</td><td><ul><li>Source: close</li><li>Moving average type: SMA</li><li>Fast period: 12</li><li>Slow period: 26</li><li>Signal moving average type: SMA</li><li>Signal moving average period: 9</li></ul></td><td><ul><li>Source: open, high, low, close</li><li>Moving average type: SMA, EMA</li><li>Fast period: any number</li><li>Slow period: any number</li><li>Signal moving average type: SMA, EMA</li><li>Signal moving average period: any number</li></ul></td></tr><tr><td>Moving Volume Weighted Average Price (MVWAP)</td><td><ul><li>Window: day</li><li>Period: 10</li></ul></td><td><ul><li>Window: day, week, month</li><li>Period: any number</li></ul></td></tr><tr><td>On-Balance Volume (OBV)</td><td></td><td></td></tr><tr><td>Parabolic Stop And Reverse (Parabolic SAR, PSAR)</td><td>Not applicable</td><td>Not applicable</td></tr><tr><td>Pivot Points</td><td></td><td></td></tr><tr><td>Price Moving Average Ratio per Percentile (PMAR)</td><td></td><td></td></tr><tr><td>Rate Of Change (ROC)</td><td></td><td></td></tr><tr><td>Relative Strength Index (RSI)</td><td>Period: 14</td><td>Period: any number</td></tr><tr><td>Simple Moving Average (SMA)</td><td>Period: no default value</td><td>Period: any number</td></tr><tr><td>Stochastic</td><td><ul><li>%K period: 14</li><li>%K smoothing: 3</li><li>%D smoothing: 3</li></ul></td><td><ul><li>%K period: any number</li><li>%K smoothing: any number</li><li>%D smoothing: any number</li></ul></td></tr><tr><td>Supertrend</td><td><ul><li>Period: 10</li><li>Multiplier: 3</li></ul></td><td><ul><li>Period: any number</li><li>Multiplier: any number</li></ul></td></tr><tr><td>Swing Points</td><td><ul><li>Source: close</li><li>Pivot point types: any</li><li>Period: </li><li>Number of pivot points to use: </li><li>Max candles between 2 pivot points: </li></ul></td><td><ul><li>Source: close, high/low</li><li>Pivot point types: any, top, bottom</li><li>Period: any number</li><li>Number of pivot points to use: any number</li><li>Max candles between 2 pivot points: any number</li></ul></td></tr><tr><td>Triple Exponential Moving Average (TRIX)</td><td></td><td></td></tr><tr><td>True Range (TR)</td><td>Not applicable</td><td>Not applicable</td></tr><tr><td>Volume Profile</td><td></td><td></td></tr><tr><td>Volume Weighted Average Price (VWAP)</td><td>Window: day</td><td>Window: day, week, month</td></tr><tr><td>Weighted Moving Average (WMA)</td><td>Period: no default value</td><td>Period: any number</td></tr><tr><td>William's Percent Range (WPR)</td><td></td><td></td></tr></tbody></table>

# Backtest an agent

## Overview

Before running an agent live, it's recommended to **test its past performance** on historical data. This is called a **backtest**.

The goal is to understand how the agent would have reacted in different market conditions — bullish phases, bearish phases, periods of high volatility, drawdowns, etc.

{% hint style="info" %}
Backtests are relevant for **trading bots** and **investing agents** with deterministic rules. For an **AI trader**, backtesting is less representative since the AI exploits the present context (news, positioning) which can't be faithfully replayed in the past.
{% endhint %}

***

## Launching a backtest

From an agent's page, click **Create a backtest**. A dialog guides you through a few steps: **Parameters → Assets → Fees → Review** (only the steps relevant to your agent are shown).

### 1. Parameters

#### Backtest period

Define a **start date** and **end date** for your backtest. Only this period will be used to simulate past operations and analyze the agent's performance.

**Presets** are available: last month, last 6 months, last year, last 5 years.

* We have **over 20 years of history** for most assets.
* If the chosen start date predates our price history, the backtest will automatically start at **the earliest date available** in our database.
* Our data is updated **daily** to integrate the latest price changes.

{% hint style="info" %}
Available history depth depends on your plan: **5 years** (Free) and **20 years** (paid plans). See [Pricing](/docs/misc/pricing.md).
{% endhint %}

#### Starting capital

Define a **starting capital** for your backtest.

* **For a trading bot**: starting capital is required, as it evolves dynamically based on gains and losses generated by each trade.
* **For an investing agent**: capital can be defined directly in the description (DCA, conditional purchases, etc.). If an initial amount must be invested in one go, it should also be specified in the description.

{% hint style="success" %}
While the starting capital doesn't affect the trades historically executed, choosing a realistic value helps you better project the evolution.
{% endhint %}

### 2. Assets

If needed, pick the **target symbol** for each asset used by the agent, so the simulation runs on exactly the instruments you have in mind.

### 3. Fees

For each asset referenced in the agent, you can fine-tune the trading conditions applied during the simulation:

* **Commission** — as a percentage, a flat fee, or per contract.
* **Spread** — as a percentage or a flat amount.
* **Swap fees** — overnight financing costs for held positions.
* **Leverage** — the leverage applied to the asset.

{% hint style="success" %}
Default values are pre-filled per asset. Tuning fees to match your broker makes the backtest more realistic, especially for high-frequency strategies where costs add up.
{% endhint %}

### 4. Review

Review the period, starting capital and per-asset configuration, then **run** the backtest. Results appear automatically as soon as the run completes.

***

## Backtest results

Once the backtest is complete, you can access several tabs to analyze performance:

* **Metrics** — dozens of detailed indicators (return, CAGR, max drawdown, Sharpe, Sortino, win rate, profit factor, expectancy, etc.) plus the **Obside Score**.
* **Performance** — capital curve over the period, with monthly and yearly performance and drawdown views.
* **Chart** — visualization of all entry and exit points on the price chart, with the reason for each trade.
* **Trades** — full list of executed operations, exportable.
* **Simulation** — a "simulate my investment" calculator: project a starting capital and monthly contributions on the backtest's performance to see what it would have meant for you.

{% hint style="info" %}
The **Obside Score** estimates the quality of a backtest by combining performance, risk and statistical reliability. See [Obside Score](/docs/misc/obside-score.md).
{% endhint %}

***

## Iterating on the agent

If the backtest results aren't satisfactory, you can **continue the conversation** in the chat linked to the agent to adjust its description, then run a new backtest with the same parameters or different ones.

# Run an agent

## Overview

Once your agent is created (and ideally [backtested](/docs/agents/backtest-an-agent.md)), you can **launch it in execution**. An agent runs in one of three modes:

* **Demo (paper trading)** — the agent runs on a **demo portfolio** with virtual capital. Ideal to observe its behavior in real time without risk.
* **Live** — the agent runs on a **real portfolio** connected to a broker, placing real orders.
* **Monitoring** — the agent only watches and notifies (no orders), so it doesn't need a portfolio.

{% hint style="info" %}
**Live** execution requires a **Pro or Max plan** and depends on your region and broker. See [Availability by region](/docs/platforms/availability-by-region.md).
{% endhint %}

***

## Launching an execution

From an agent's page, click **Activate agent**. A dialog guides you through the necessary steps — only the ones relevant to your agent are shown:

### 1. Mode & portfolio

Choose how to activate the agent — **Demo** or **Live** — then pick the **portfolio** it will run on:

* A **demo portfolio**: virtual capital, for paper trading.
* A **real portfolio**: a connected broker account, for live trading _(requires a Pro or Max plan and an eligible region)_.

You can create a new demo or real portfolio directly from this step. See [Portfolios](/docs/portfolios/portfolios.md). The portfolio's capital (demo) or connected broker (real) is used automatically.

{% hint style="info" %}
Agents that only **monitor or alert** (no orders) don't need a portfolio. Obside resolves the assets to watch and skips straight to notifications.
{% endhint %}

### 2. Assets

Obside maps the agent's assets to the instruments actually available on the selected account, and tells you if one of them isn't tradable there. On multi-venue brokers, you also pick the **target exchange**.

### 3. Order execution mode _(live only)_

Choose how the agent's orders reach your broker:

* **Approval** _(default)_ — every order the agent wants to place is first **submitted for your approval**. You get notified, review the order, and approve or reject it. Pending orders are gathered on the **Orders to approve** page.
* **Automated** — orders are sent to your broker automatically, without asking. Enabling it requires an explicit real-money acknowledgment.

{% hint style="success" %}
Approval mode means an agent **never trades real money behind your back** by default: you keep the final word on every order.
{% endhint %}

### 4. Notifications _(if the Notifications capability is enabled)_

Select at least one **channel** to receive notifications from the agent (**Telegram, push or email**). In approval mode, you also choose where to be alerted when an order needs your approval. See [Notification channels](/docs/alerts-and-notifications/channels.md).

### 5. Review & start

Review the mode, portfolio and settings, then **start** the agent. Live trading agents ask you to confirm a short risk disclaimer before activation.

***

## Monitoring the execution

Once the agent is running, you can track its activity in real time from its execution page:

* **Performance** — value/PnL curve and metrics since activation, plus open positions and their unrealized P&L.
* **Orders & trades** — list of open and closed positions, pending orders included.
* **Activity** — chronological log of everything the agent does: trades, orders, AI reasoning, news matches, notifications sent. Especially useful for AI traders and agents with the News capability.

{% hint style="info" %}
The **maximum number of simultaneous executions** depends on your subscription plan. See [Pricing](/docs/misc/pricing.md).
{% endhint %}

***

## Start, pause, stop

You can **pause** or **stop** an execution at any time from the agent's page. A stopped execution preserves its viewable history, but no longer generates new trades or notifications.

{% hint style="success" %}
**Best practice**: before launching an agent in live mode, run it in demo mode for a few days or weeks to validate its behavior in current market conditions.
{% endhint %}

# Share an agent

## Public link

You can share any agent with a **public link**: anyone with the link can view it, **no account needed**. Click **Share** on the agent's page to enable it.

{% hint style="success" %}
The agent's **code always stays private** — viewers see what you choose to show, never how the agent works inside.
{% endhint %}

## What viewers see

You control exactly what the public page shows:

* **Current run** — the running execution: open positions, live P&L and the capital curve.
* **Run history** — past runs and their performance.
* **Backtests** — the backtest results.
* **Trades & orders** — the trade and order log of the shared runs.
* **Real amounts** — off by default: every figure is rebased to 100, so viewers only see percentages. Turn it on to show your actual amounts.

## Public profile

You can also list shared agents (and shared portfolios) on your **public profile page**, visible to anyone. Your profile is accessible from the sidebar footer.

## Revoking access

You can **revoke** a public link at any time: it stops working right away. If you share the agent again later, a **new, different link** is created.

{% hint style="info" %}
Sharing a link makes the selected data publicly visible to anyone who has it. Share thoughtfully — and prefer percentages (real amounts off) if you don't want to expose figures.
{% endhint %}

To let others **copy and run** your agent rather than just view it, publish it to the [Library](/docs/agents/library.md).

# Library

The **Agent library** is where the Obside community shares its agents. Browse it from the **Agent library** section in the sidebar.

The Library is **public**: anyone can browse the listings — even without an Obside account — and every listing has a **shareable link**. You need an account to copy an agent.

## Discover and copy agents

Each Library listing shows the agent's description, its assets and indicators, its performance, and community feedback (ratings and comments). If an agent inspires you:

* **Copy it** — a full copy is added to your account. From there it behaves like any of your agents: you can backtest it, adjust it through the chat, and run it in demo or live mode.
* **Filter and search** — by capability (trading, notifications, AI, news), asset or indicator, and sort by popularity or recency.

{% hint style="info" %}
A copied agent is **yours**: modifying your copy never affects the original, and the original author can't see what you do with it.
{% endhint %}

## Publish your own agent

You can publish any of your agents to the Library from its page:

1. Click **Publish to library**.
2. Your agent is submitted for **review by the Obside team** before going live.
3. Once approved, it becomes publicly visible and other members can copy it.

{% hint style="success" %}
**Approved agents earn you a reward**, added to your [bonus usage](/docs/credits/how-it-works.md#bonus-usage), and your listing shows how many times it's been copied.
{% endhint %}

The published listing shows your agent's description and performance, but like [shared links](/docs/agents/share-an-agent.md), **its code stays private**.

# Smart Chart

**Smart Chart** is Obside's interactive chart — with a twist: what you draw on it can become an automation. Draw trend lines, levels and indicators, then **let your agents watch them for you**. Open it from the **Smart Chart** section in the sidebar.

## A full-featured chart

* **Any asset** from the catalog, on timeframes from **1 minute** to daily and beyond.
* **Chart types** — candles, line, Heikin Ashi.
* **Drawing tools** — trend lines, horizontal/vertical lines, segments, rectangles, Fibonacci retracements, long/short position tools, annotations, brush.
* **Indicators** — add the [available indicators](/docs/agents/create-an-agent/indicators-list.md) (including your [custom indicators](/docs/agents/create-an-agent/indicators-list.md#custom-indicators)) directly on the chart.
* **Multiple saved charts** — keep separate workspaces per asset or per idea.
* **Followed accounts** — pin the posts of X (Twitter) accounts and public Telegram channels directly on the chart's timeline, to see price action next to what they posted.

## Import an indicator from code

Already have an indicator written for another platform? Bring it with you. In the Smart Chart toolbar, click **Import from code** (you also find it at the top of the **Add indicator** window), then paste the code or upload the file:

* **TradingView Pine Script** (`.pine`)
* **MetaTrader MQL4 or MQL5** (`.mq4`, `.mq5`, `.mqh`)
* **Python**, such as a pandas or TA-Lib function (`.py`)

The assistant opens next to your chart, rebuilds the logic as your own [custom indicator](/docs/agents/create-an-agent/indicators-list.md#custom-indicators) and plots it on the chart you are looking at. It then tells you what was reproduced as is and what had to be approximated. The indicator stays saved: it is listed in the **Custom** group of the indicator picker on every chart, and your agents can use it. On the mobile app, the conversation opens in the chat tab and the indicator then waits for you in that same **Custom** group.

{% hint style="info" %}
The code is rebuilt as an Obside indicator, not run as is, so values can differ slightly from the original platform. Drawings (labels, boxes, tables), alerts, logic that repaints and data read from another timeframe may be approximated or left out. Files are limited to 50 KB of plain text.
{% endhint %}

## From drawing to automation

The point of Smart Chart is that your chart work doesn't stay decorative:

* **Automations** — turn chart signals into actions: link a drawing or an indicator to a **condition** (crossing, touch, threshold, with tolerance and confirmation bars) and an **action** (alert or trading action).
* **Agents linked to a drawing** — agents can be created from a drawing's geometry, and you can see at a glance which agents watch which drawing.
* **Chart context in the chat** — add the chart (asset, drawings, indicators) to a conversation and ask Obside to build the corresponding agent.

{% hint style="info" %}
If an agent was created from a drawing and you later **modify that drawing**, the agent is disabled to avoid acting on a geometry you've changed — relaunch it from the updated drawing.
{% endhint %}

# Portfolios

A **portfolio** is where your agents run and where your holdings live. Every [agent execution](/docs/agents/run-an-agent.md) is tied to a portfolio, and you can track performance, positions and the agents working on it from a single page.

There are two types of portfolio:

* **Demo** — a paper-trading wallet with **virtual capital**. Perfect to test ideas risk-free.
* **Real** — an account linked to a **connected broker** (see [Connections](/docs/platforms/connectors.md)), synchronized automatically.

***

## Creating a portfolio

From the **Portfolios** section, click **Add portfolio** and choose the type.

### Demo portfolio

* **Name** your portfolio.
* Set the **cash** your agents can spend (defaults to $10,000).
* Optionally **seed holdings** — add positions with a quantity and average price.
* Or **import an existing portfolio**: upload a screenshot or a CSV / XLSX / PDF export from any broker, or **copy a connected account**. Obside detects the holdings for you to review.

### Real portfolio

* **Pick your broker** from the list of [supported platforms](/docs/platforms/supported-brokers.md).
* Authorize the connection through the broker's secure portal.

{% hint style="info" %}
Many broker connections are **read-only**: they let you read and monitor positions, but not place live orders. Live execution requires a trading-capable broker, a US/Canada residence and a Pro or Max plan. See [Availability by region](/docs/platforms/availability-by-region.md).
{% endhint %}

***

## The portfolio page

The Portfolios section opens on an **aggregated view** — the total value of all your portfolios, with a per-portfolio breakdown. Each portfolio then has its own detail page, organized in tabs:

### Overview

* **Portfolio value** — an equity curve over time, with **Value / PnL** views and a **Compare to a benchmark** option to overlay an index or asset.
* **Weekly brief** — your portfolio's week, decoded in a one-minute read (see below).
* **Allocation** — a breakdown of your holdings, by asset or by class (crypto, stocks, ETFs, FX, indices, futures, commodities, cash).
* **Holdings** — every position with its quantity, value, weight and P&L, plus a **cash** row. On demo portfolios you can **add capital** or adjust positions directly.
* **Active agents** — the agents currently running on the portfolio, each with its exposure and P&L. You can **add** an agent or **stop** one from here.
* **Recent news** — recently published articles that touch your positions, exposed sectors or countries.
* **Portfolio calendar** — upcoming events affecting your holdings (earnings, dividends…).
* **Expected income** — upcoming dividends and coupons over the next 12 months.

### Exposure

A full breakdown of what your portfolio is really exposed to: **sectors**, **geographies** and asset classes — beyond the ticker list.

### Orders

The orders and trades executed on the portfolio, filterable by agent.

### Activity

A chronological log of what happened on the portfolio: trades, orders, agent activations, capital flows, notifications.

***

## Weekly brief

Obside generates a **weekly brief** of each portfolio — how it moved and which holdings drove it, the news that actually mattered, and what's coming next week. It's delivered after the market week closes, with an optional email heads-up (see [Notification channels](/docs/alerts-and-notifications/channels.md)).

* On **paid plans**, the weekly brief is **included** and doesn't count toward your monthly AI usage.
* On the **Free plan**, generating it **uses credits you bought earlier**, if any remain.

You can also ask the chat for a **custom recurring brief** (different focus, frequency or notes) — it becomes an agent you fully control.

***

## Sharing a portfolio

You can create a **public link** to a portfolio: anyone with the link can view it, no account needed. You choose what it shows — your real amounts, or **percentages only** (performance and allocation, no balance) — and whether the agents running on it are visible. You can also list it on your **public profile**, and revoke the link at any time.

***

## Plans & limits

* **Portfolio monitoring** (AI insights on your positions) is available on the **Plus plan and above**. See [Pricing](/docs/misc/pricing.md).
* **Demo** portfolios are unlimited.
* **Real** portfolios depend on your **broker connections**: 1 on Free, unlimited on paid plans.

# Market Radar

The **Market Radar** flags **US stocks showing unusual activity** — in the options market, in insider filings or in positioning data. Open it from the **Radar** entry in the sidebar.

## What it scans

Every day before the US market opens, Obside scans the previous session's data on **S&P 500 stocks plus a curated watchlist of biotech and mid-cap catalyst names**, looking for footprints that experienced traders watch closely:

* **Unusually high call-option volume** versus the stock's own average.
* **Day volume exceeding open interest** on short-dated, out-of-the-money calls — a sign of fresh positioning rather than existing positions changing hands.
* Option activity **concentrated in far out-of-the-money, near-expiry calls**.
* A **jump in implied volatility**.
* **Stock purchases reported by members of the US Congress**.
* **Significant insider buying** on the open market.
* **Elevated or rising short interest**.

## Reading an alert

Each flagged stock appears with an **alert score (0–100)** that condenses the strength of its triggers, the date the alert was **first detected**, and the **price move since detection**.

Click a row to see **exactly which triggers fired** for that stock — and jump to its asset page for the full picture (chart, key stats, news, upcoming catalysts).

Alerts **age out automatically**: a stock drops off the radar after about **two weeks without a new detection**.

{% hint style="warning" %}
The radar is an **informational signal, not a recommendation**. Unusual flow has many benign explanations — hedging, rolled positions, institutional blocks — and is never proof of insider knowledge. Treat an alert as a starting point for your own research, not as a reason to trade on its own.
{% endhint %}

The Market Radar is included on **every plan**.

# Data & sources

Good answers need good data. This page explains where Obside's data comes from, what our AI (chat and agents) can actually see, and how we keep it fresh and honest.

## The short version

* **Market data** for stocks, ETFs, indices, forex and crypto — real-time and historical — from institutional-grade licensed providers and exchange-direct feeds.
* **A proprietary real-time data layer** that continuously ingests **130+ specialist feeds** beyond prices: macro, positioning, regulation, energy, crypto on-chain, sentiment, world events.
* **Official sources cited by name**: central banks, regulators and international institutions (Federal Reserve, ECB, IMF, World Bank, BIS, SEC, FINRA, CFTC, EIA…).
* **Point-in-time discipline**: backtests only see what was known at the time — no lookahead bias.
* **Transparency in chat**: ask the assistant what it used, and it will tell you.

{% hint style="info" %}
We name official and public institutions openly. Some commercial data partners are not named individually in public documentation for contractual reasons — all of them are licensed, institutional-grade providers.
{% endhint %}

## Market data (prices)

Real-time and historical prices for **stocks, ETFs, indices, forex and crypto**, sourced from licensed institutional providers and, for crypto, exchange-direct feeds. Historical depth goes back years, which is what powers charting and [backtesting](/docs/agents/backtest-an-agent.md).

**Futures** are served as **continuous front-month series, back-adjusted at each roll** — the standard for strategy research. Distant history therefore reflects adjusted levels rather than the raw prices that traded on those dates, and the affected asset pages say so explicitly.

## Fundamentals & company data

* **Financial statements, earnings and estimates** — income statements, balance sheets, cash flows, earnings surprises.
* **Valuation and factor metrics** — P/E, growth, quality, momentum-style factor scores.
* **Regulatory filings** — SEC EDGAR filings, monitored continuously.
* **Positioning** — short interest (FINRA), futures positioning (CFTC Commitments of Traders), institutional holdings.
* **Insider & congressional activity** — insider open-market transactions and stock trades disclosed by members of the US Congress. Combined with options data, these power the daily [Market Radar](/docs/explore/market-radar.md).

## Macro & central banks

Official series and announcements from the **Federal Reserve (FRED)**, **ECB**, **IMF**, **World Bank**, **BIS** and national central banks, plus a global **economic calendar** (rate decisions, CPI, employment reports…). Agents can react to these events the moment they are published.

## Energy & commodities

Official energy data (**EIA**), commodity prices, and fuel-price series — useful context for inflation, energy stocks and macro-driven strategies.

## Crypto-specific data

Beyond spot prices: global market metrics, **DeFi** activity, **network fees**, **derivatives data** (funding rates, open interest, options skew), and crypto **Fear & Greed**.

## News, sentiment & social

* A continuous **financial news flow**, deduplicated and classified, that agents can subscribe to.
* **Social sentiment** and **prediction-market** signals.
* **Accounts you choose to follow**: agents and the [Smart Chart](/docs/agents/smart-chart.md) can track posts from specific **X (Twitter) accounts** and **public Telegram channels**.
* **Live web search** in chat, so answers aren't limited to a training cutoff.

## World events

Structured monitoring of **geopolitical developments**, **sanctions lists**, **natural disasters**, **cyber incidents** and **public-health events** — the kind of context that moves markets but never shows up in a price feed on its own.

## Composite signals

On top of the raw feeds, Obside computes **proprietary composite signals** — normalized 0–100 scores that condense many raw series into one readable gauge: market stress, volatility regime, fear & greed, funding divergence, sanctions pressure, surprise momentum, and more. Agents can use them directly as conditions.

## Freshness & reliability

* **Multi-provider redundancy** — key concepts are covered by more than one source, with priority-based routing between them.
* **Staleness detection** — every data point carries its timestamp; the system knows when a series is out of date instead of silently serving old values.
* **Point-in-time storage** — history is stored as it was known at each moment. When a backtest asks for "the ranking on March 3rd", it gets what was actually knowable on March 3rd, not today's revised version. No lookahead bias.
* **Deduplication and normalization** — the same event reported by multiple sources is stored once, in a consistent format.

## Ask the assistant

Transparency is enforced at the AI level: the assistant is designed to **never invent a source**. It only cites data it actually retrieved, and if it doesn't have something, it says so. If you want to know where a specific answer came from, just ask — *"what sources did you use?"* — directly in the chat.

# Connections

## Overview

**Connections** let you link your Obside account to your **brokerage accounts**. Once a connection is established, Obside can:

* **Read your balances and positions** in real time.
* **Monitor your portfolio** and trigger smart alerts.
* **Execute orders** _(on trading-capable brokers, for US & Canada residents — see [Availability by region](/docs/platforms/availability-by-region.md))_.

***

## How connections work

Obside connects to your broker through a secure **broker portal**. You authorize Obside directly with your broker, without ever sharing your password with us — and you can revoke the access at any time, from Obside or from your broker.

{% hint style="success" %}
Obside never stores your broker password. Connections rely on a secure OAuth-style authorization that you control and can revoke at any time.
{% endhint %}

Connecting an account is available **worldwide**. What you can do with it depends on your region and on the broker:

* **Reading balances, positions and history** — every connection, everywhere.
* **Live order execution** — US & Canada residents, on brokers that support order placement. See [Supported brokers](/docs/platforms/supported-brokers.md) for which brokers can trade and [Availability by region](/docs/platforms/availability-by-region.md) for the regional rules.

***

## Connecting a broker

You connect an account from **Settings → Connections**:

1. Pick your broker from the list of [supported platforms](/docs/platforms/supported-brokers.md).
2. Follow the broker's secure authorization flow.
3. Your balances and positions appear in Obside within moments.

You can connect **multiple accounts**, including several accounts at the same broker.

***

## Use in the chat and in agents

Once an account is connected, you can:

* **Reference its assets** in the chat through the **Add** menu → **Connections** → choose the account → select its tradable assets.
* Run an agent on a **[portfolio](/docs/portfolios/portfolios.md)** backed by that connection.
* **Enable portfolio monitoring** on the positions held there.

***

## Multi-account

Depending on your plan, you can connect **several accounts** and run agents across them. The **Free** plan allows **1 connection**; **paid plans** allow **unlimited** connections. See [Pricing](/docs/misc/pricing.md).

To go further:

* [Availability by region](/docs/platforms/availability-by-region.md)
* [Supported brokers](/docs/platforms/supported-brokers.md)
* [AI connections (MCP)](/docs/platforms/ai-connections.md)

# Availability by region

Broker-related features depend on **your country of residence**. This distinction is required by regulations applicable to executing orders on behalf of third parties.

Connecting a brokerage account is available **worldwide** — the region only determines whether Obside can **place live orders** for you.

***

## United States and Canada residents

| Feature | Availability |
|---|---|
| **Connecting a brokerage account** | ✅ Available |
| **Reading balances and positions** | ✅ Available |
| **Portfolio monitoring** | ✅ Available |
| **Smart alerts** | ✅ Available |
| **Live order execution** | ✅ Available on trading-capable brokers |
| **Demo mode (paper trading)** | ✅ Available |

Live order execution covers the following asset classes, depending on the broker:

* **Stocks**
* **ETFs**
* **Options**
* **Cryptos**

{% hint style="info" %}
Beyond your region, **live execution also requires a Pro or Max plan**, and a broker that supports order placement (see [Supported brokers](/docs/platforms/supported-brokers.md)). Without live execution, you can still connect your accounts for reading, monitoring and alerts, and run agents in demo mode.
{% endhint %}

***

## All other regions (including the European Union)

| Feature | Availability |
|---|---|
| **Connecting a brokerage account** | ✅ Available |
| **Reading balances and positions** | ✅ Available |
| **Portfolio monitoring** | ✅ Available |
| **Smart alerts** | ✅ Available |
| **Live order execution** | ❌ Not available yet |
| **Demo mode (paper trading)** | ✅ Available |

{% hint style="info" %}
Outside the US and Canada, you can **connect your accounts to benefit from real-time reading, monitoring and alerts**, but Obside cannot place orders on your behalf yet. You can still **simulate your agents in demo mode** without any restriction — and the pricing page lets you ask to be notified when live trading reaches your region.
{% endhint %}

***

## Regardless of region

**Demo mode (paper trading)** and **all analysis, learning and chat features** are available everywhere. You can create, backtest and run your agents in simulation without restriction.

For the full list of supported platforms, see [Supported brokers](/docs/platforms/supported-brokers.md).

# Supported brokers

Obside connects to brokers through a secure broker portal — see [Connections](/docs/platforms/connectors.md) for how it works. Every connection can **read balances, positions and history**. Whether Obside can also **place orders** depends on the broker and on your region (US & Canada residents only — see [Availability by region](/docs/platforms/availability-by-region.md)).

## Brokers with live order execution

These brokers support **order placement** through Obside, in addition to reading your positions:

| Broker | Tradable through Obside |
|---|---|
| Alpaca | Stocks, ETFs, options |
| Binance | Crypto |
| Charles Schwab | Stocks, ETFs, options |
| Coinbase | Crypto |
| E\*TRADE | Stocks, ETFs, options |
| Kraken | Crypto |
| Moomoo | Stocks, ETFs, options |
| Public | Stocks, ETFs, options |
| tastytrade | Stocks, ETFs, options, crypto |
| Tradier | Stocks, ETFs, options |
| TradeStation | Stocks, ETFs, options |
| Wealthsimple | Stocks, ETFs, options |
| Webull (US & Canada) | Stocks, ETFs, options |

{% hint style="info" %}
Order placement also requires a **Pro or Max plan** and a **US or Canada residence**. Otherwise, these brokers connect in **read-only** mode: balances, positions, monitoring and alerts.
{% endhint %}

## Read-only brokers

These brokers connect for **reading and monitoring**: Obside imports your balances, positions and history, monitors them and can alert you, but doesn't place orders through them:

* AJ Bell
* BUX
* Chase
* Citi
* CommSec
* DEGIRO
* Edward Jones
* Empower
* eToro
* Fidelity
* Interactive Brokers
* PNC
* Questrade
* Robinhood
* TD Direct Investing
* TIAA
* Trading 212
* Transamerica
* Upstox
* US Bank
* Vanguard
* Wells Fargo
* Zerodha

{% hint style="info" %}
The list of supported brokers is regularly expanded, and the in-app list (**Settings → Connections**) is always the most up to date. If you'd like to see a specific platform added, feel free to [contact us](/docs/readme.md#contact-us).
{% endhint %}

***

## What you can do with a connected broker

Depending on the broker and your region:

* **Reading balances and positions** _(every connection, everywhere)_.
* **Portfolio monitoring and smart alerts** _(everywhere, on Plus and above)_.
* **Live order execution** _(US/Canada residents, trading-capable brokers, on a Pro or Max plan)_.

See the details in [Availability by region](/docs/platforms/availability-by-region.md).

***

## Connecting a broker

The connection process is described on the [Connections](/docs/platforms/connectors.md) page.

# AI connections (MCP)

## Overview

**AI connections** let you plug your favorite AI assistant — **Claude, ChatGPT, Claude Code or any client supporting the Model Context Protocol (MCP)** — directly into your Obside account.

Once connected, your assistant can work with your real Obside data and act on your behalf:

* **Read your portfolios** — positions, performance, exposure, income forecast, related news, calendar.
* **Look up markets** — search assets, get prices and price history.
* **Manage your agents** — list, create, update, activate or stop agents, organize them in folders.
* **Import TradingView Pine Scripts**: Obside runs them as-is, and your assistant builds agents on them.
* **Run backtests** and read their results.
* **Place or cancel orders** on your connected accounts _(same regional and plan rules as in the app)_.
* **Check your credit balance.**

{% hint style="info" %}
AI connections are available on **paid plans** (Plus, Pro and Max). See [Pricing](/docs/misc/pricing.md).
{% endhint %}

***

## How to connect

Everything happens in **Settings → AI connections**, where the MCP server URL is displayed with step-by-step guides for the most popular clients.

### Clients with sign-in (Claude, ChatGPT…)

Most modern AI clients connect with a simple sign-in — **no token needed**:

1. In your AI client, add a **custom connector** and paste Obside's MCP server URL.
2. Leave any "Client ID" or "Client Secret" fields empty.
3. Sign in to Obside when prompted to authorize the connection.

For example, in **Claude**: Settings → Connectors → Add custom connector. In **ChatGPT**: enable Developer mode, then Settings → Connectors → Create.

### Clients using a token (Claude Code, Codex…)

Some clients authenticate with a bearer token instead of a sign-in:

1. In **Settings → AI connections**, generate a **token** and copy it.
2. Add the Obside MCP server in your client with the token as the Authorization header — the settings page shows the exact ready-to-paste command per client.

For **Claude Code**, add it with the token as a header:

```bash
claude mcp add --transport http --scope user obside https://api.obside.com/mcp \
  --header "Authorization: Bearer YOUR_TOKEN"
```

For **Codex**, declare the server in `~/.codex/config.toml` and keep the token in an environment variable (Codex does not expand placeholders inside the file):

```toml
[mcp_servers.obside]
url = "https://api.obside.com/mcp"
bearer_token_env_var = "OBSIDE_TOKEN"
```

You can name your tokens (one per client is a good habit), see when each was last used, and **revoke** any of them at any time.

### Writing agent code from your assistant

Claude Code and Codex can go further than reading and managing: they can write the code behind an agent and hand it to Obside. See [Build with Claude Code and Codex](/docs/developers/code-with-claude-code-and-codex.md) for the full workflow and the reusable skill, and the [Agent code (STSE)](/docs/developers/stse.md) reference for the runtime itself.

The connection itself also serves that material, so there is nothing to install. The skill and the developer reference pages are exposed as **MCP resources** (the skill is `obside://skills/obside-stse`), and a **build_agent** prompt starts a conversation with the skill already loaded and your idea attached. In Claude Code, the prompt shows up as the slash command `/mcp__obside__build_agent` and the resources can be attached with an @ mention. In a chat app such as claude.ai or Claude Desktop, attach a resource yourself or run the prompt, since the assistant does not load a resource on its own. Clients that only list tools simply ignore them.

{% hint style="success" %}
Connections are scoped to **your** account and respect the same permissions as the app: an assistant can't do anything you couldn't do yourself in Obside.
{% endhint %}

# Build with Claude Code and Codex

You can drive Obside from a coding assistant such as Claude Code or Codex over the Model Context Protocol (MCP). Once connected, the assistant can search assets, write agent code, validate it, create the agent, back it and read the results, all from your editor or terminal. This page is the end-to-end workflow, the setup for each client, and the security model that makes it safe.

Connecting requires an eligible paid plan. See [AI connections (MCP)](/docs/platforms/ai-connections.md) for the account side.

## Get a token

Go to **app.obside.com/settings/mcp**, generate a token, and copy it. It is shown once. Name one token per client so you can revoke them independently.

## Connect Claude Code

Add the Obside MCP server (keep the header after the URL):

```bash
claude mcp add --transport http --scope user obside https://api.obside.com/mcp \
  --header "Authorization: Bearer YOUR_TOKEN"
```

Then, in a session, `/mcp` shows the connection and the Obside tools become available.

## Connect Codex

Codex declares MCP servers in its config file, `~/.codex/config.toml`. Keep the token in an environment variable rather than in the file: Codex reads it from the variable you name, and never expands `${...}` placeholders inside the file.

```toml
[mcp_servers.obside]
url = "https://api.obside.com/mcp"
bearer_token_env_var = "OBSIDE_TOKEN"
```

Export the variable (`export OBSIDE_TOKEN="YOUR_TOKEN"`) where Codex can see it. The same can be done in one command:

```bash
codex mcp add obside --url https://api.obside.com/mcp --bearer-token-env-var OBSIDE_TOKEN
```

No feature flag is needed. If a tool does not appear, run `codex mcp list` or `/mcp` to confirm the server started and the variable was set.

## The workflow

1. **Resolve the asset.** Call `search_assets` to turn a name or ticker into the identifiers Obside uses. Pin exactly the listing you mean.
2. **Write the code.** Use this developer section as the reference: [Agent code (STSE)](/docs/developers/stse.md), [Structure and lifecycle](/docs/developers/stse/agent-structure.md), the [Manager API](/docs/developers/stse/manager-api.md), [Types](/docs/developers/stse/types.md), the [Indicator reference](/docs/developers/stse/indicator-reference.md), [Orders, trades and risk](/docs/developers/stse/orders-and-risk.md), [Schedules](/docs/developers/stse/scheduling.md), [News, data and AI](/docs/developers/stse/data-and-ai.md), the [Validation rules](/docs/developers/stse/rules.md), [Pitfalls](/docs/developers/stse/pitfalls.md) and the [Examples](/docs/developers/stse/examples.md).
3. **Validate.** Call `validate_agent_code` with your code. It returns either that the code is valid, or a list of errors with a code, line and column. Fix and repeat until it is valid. This is free and runs nothing.
4. **Create the agent.** Call `create_agent` with a plain-language `prompt` describing the intent, and optionally your validated `code`. Obside resolves the assets, reviews the code and builds the agent. If the code was invalid it tells you so without building. Large multi-asset builds run in the background: when the result says the build is pending, poll `get_agent_build` with the returned build id rather than calling `create_agent` again.
5. **Backtest and run.** Call `start_backtest` and poll `get_backtest` for the metrics and equity curve. Activate the agent when you are satisfied. Live trading and monitoring follow the same regional and plan rules as the app, and real-money actions always require an explicit confirmation.

### Starting from a TradingView Pine Script

A Pine Script needs no translation: call `import_pine_script` with the script exactly as it is, plus the source of each library it imports. Obside's own Pine engine runs it as-is, with TradingView's rules, and returns a custom indicator: its class, and the titles of its alerts, markers and plots. Then call `create_agent` without `code`, with a `prompt` that names that indicator: Obside's builder writes the agent on it, following a strategy's positions or reading an indicator's signals. If the engine refuses the script, the result says why (a line to fix, a library to add, or a Pine feature Obside does not support yet). A Pine Script passed in `code`, or in a prompt before it is imported, is refused without building.

## The security model, briefly

Your code is treated as a **specification, not something Obside runs as-is**. When you pass `code` to `create_agent`, Obside validates it deterministically, then its own builder reimplements the intent into a reviewed, type-checked agent, and only that result is saved. Anything the code does that the engine does not support is reported back to you instead of being forced through. This is why you always get a clean, checkable agent, and why a mistake in your draft cannot do anything unsafe.

Two more guarantees worth knowing: the connection is scoped to your account and respects the same permissions as the app, and every real-money action needs an explicit confirmation flag on the call, so an assistant can never place a real order silently.

## The Obside skill

Obside publishes a reusable Agent Skill that packages this workflow and the STSE essentials for a coding assistant. It is a single Markdown file:

```
https://obside.com/skills/obside-stse/SKILL.md
```

Install it once so your assistant loads it automatically.

- **Claude Code:** save it as `~/.claude/skills/obside-stse/SKILL.md`.
- **Codex:** save it as `~/.agents/skills/obside-stse/SKILL.md` (older Codex versions read `~/.codex/skills/obside-stse/SKILL.md`). Invoke it with `$obside-stse` or list skills with `/skills`.

The skill links back to the raw Markdown of these pages (append `.md` to any docs URL) so the assistant can pull the full reference on demand.

### Without installing anything

The Obside MCP connection serves the same material itself, so there is nothing to install: it is available through the connection. The skill is the MCP resource `obside://skills/obside-stse`, and every page of this developer section is a resource under `obside://docs/developers/` (for example `obside://docs/developers/stse/manager-api`). They are the same files that are published on this site, shipped together with the server, so the reference you read always matches the validation that checks your code.

There is also a prompt, `build_agent`, that loads the skill and starts the workflow above. It takes an optional `idea` argument, the agent you want in plain language. In Claude Code it appears as `/mcp__obside__build_agent`, and Claude Code can read the resources on its own. In a chat app such as claude.ai or Claude Desktop, an assistant does not load a resource by itself: attach it yourself, or run `build_agent`, which loads the skill for you. Reading resources and running prompts needs the same token and paid plan as the tools.

# Agent code (STSE)

Every Obside agent is backed by a small piece of code that the platform runs for you. Most people never see it: you describe what you want in plain language and Obside writes, checks and runs the code. But if you are a developer, or you connect a coding assistant like Claude Code or Codex through the [AI connections (MCP)](/docs/platforms/ai-connections.md), you can write that code yourself and hand it to Obside.

This section documents the agent runtime, the same one the assistant targets, so a coding agent can build strategies faithfully. Obside calls this engine STSE.

## The mental model

Your code is the **body of one function** that receives a single object called `manager`. You never write the function wrapper or any `import`. Everything you need is already in scope: the `manager` object, every technical indicator and [helper](/docs/developers/stse/helpers.md), the engine [types](/docs/developers/stse/types.md), the `z` schema helper, and the Luxon date classes (`DateTime`, `Duration`, `Interval`). Writing an `import` is an error, and so is declaring your own variable with one of those names.

Think of the code as running in two phases.

**Root phase (runs once, at startup).** This is the top level of your code. Here you set things up: you create indicators, you read launch settings, and you attach listeners that say "when this happens, run that". You do not place orders or read live prices here, because at startup there is no "now" yet.

**Callback phase (runs many times, later).** Each listener you attached runs its callback whenever its event fires: a candle closes, a schedule ticks, a trade fills. Inside a callback you read values (`getValue()`), read the current time and price, and take actions (place an order, open or close a trade, send a notification).

The rule that follows from this: **construct indicators and attach every listener at the root; read values and act only inside callbacks.**

## Assets are not in the code

Your code refers to an asset by a short symbol string you choose, for example `'BTC'` or `'AAPL'`. The mapping from that symbol to a real, tradable listing is decided separately when the agent runs (in a backtest or a live run), not written into the code. When you create an agent you declare, alongside the code, which real asset each symbol stands for. This is what lets one agent run on Binance in a backtest and on your broker live without any code change.

You never put an exchange-prefixed ticker or an internal identifier in the code. Just the short symbol.

## A minimal example

An agent that buys a little Bitcoin every Monday morning:

```ts
const symbol = 'BTC';

// Root: subscribe to the price feed so a scheduled callback can act on this symbol.
manager.onPreciseCandle(symbol, () => {});

// Root: attach a weekly schedule.
manager.cron('0 9 * * 1', () => {
  // Callback: this runs every Monday at 09:00.
  manager.placeOrder({
    symbol,
    type: 'MARKET',
    position: 'buy',
    amount: 50,
    amountCurrency: 'USD',
  });
}, 'UTC');
```

An agent that watches an indicator and alerts you once:

```ts
const symbol = 'ETH';
const candles = manager.getIntervalData(symbol, 60); // 60-minute candles
const rsi = new RSI(new PartialIndicator(candles, 'close'), 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!rsi.hasValue()) return;
  const value = rsi.getValue();
  if (value === undefined || value > 30) return;
  manager.sendNotification(`ETH RSI is ${value.toFixed(1)}, below 30.`);
  manager.stop();
});
```

## The lifecycle

1. **Write** the code (this section is your reference).
2. **Validate** it: the deterministic checker reports type and rule errors so you can fix them before spending anything. Over the MCP connection this is the `validate_agent_code` tool.
3. **Create** the agent from your intent, optionally seeding it with your validated code. Obside resolves the assets, reviews the code and persists the agent (`create_agent`).
4. **Backtest** it over history, or **run** it live or in paper mode, and read the results.

Every complete code block in this section is compiled by the real validator as part of Obside's test suite, so what you copy passes as written. Blocks marked as fragments are partial code, and blocks under "refused" in the [Validation rules](/docs/developers/stse/rules.md) are rejected on purpose.

## Where to go next

Read in this order when writing your first agent:

1. [Structure and lifecycle](/docs/developers/stse/agent-structure.md): the root and callback phases, listeners, time zones, state, launch variables.
2. [Manager API](/docs/developers/stse/manager-api.md): every method, with its exact signature and behaviour in backtests and live runs.
3. [Indicators](/docs/developers/stse/indicators.md) and the [Indicator reference](/docs/developers/stse/indicator-reference.md): every indicator class, its constructor, value and warm-up.
4. [Orders, trades and risk](/docs/developers/stse/orders-and-risk.md): sizing, capital and auto-invest, rebalancing, stops.
5. [Schedules and launch actions](/docs/developers/stse/scheduling.md): cron, acting at launch, period keys.
6. [News, data and AI](/docs/developers/stse/data-and-ai.md): news, account follows, fundamentals, language models, notifications.
7. [Validation rules](/docs/developers/stse/rules.md): every rule the checker enforces, with a refused example and the accepted rewrite.
8. [Examples](/docs/developers/stse/examples.md): complete agents for the common shapes.

Reference pages: [Types](/docs/developers/stse/types.md) (every object shape), [Helpers](/docs/developers/stse/helpers.md) (utility functions in scope), [Pitfalls](/docs/developers/stse/pitfalls.md) (a pre-submit checklist).

- [Build with Claude Code and Codex](/docs/developers/code-with-claude-code-and-codex.md): the end-to-end workflow over MCP, plus the reusable skill.

# Structure and lifecycle

How an agent is laid out, when each part runs, and how it keeps state. Read this before the [Manager API](/docs/developers/stse/manager-api.md): nearly every validation error on a first draft comes from one of the rules on this page.

## The shape of an agent

Agent code is the body of `async (manager) => { … }`. You write the body only: no wrapper, no `import`, no `export`. Top-level `await` is allowed. Already in scope, and never to be imported or redeclared:

- `manager`, the object every capability hangs off ([Manager API](/docs/developers/stse/manager-api.md));
- every indicator class and helper ([Indicator reference](/docs/developers/stse/indicator-reference.md), [Helpers](/docs/developers/stse/helpers.md));
- every engine type (`Candle`, `Order`, `Trade`, `CreateOrder`, …) for annotations ([Types](/docs/developers/stse/types.md));
- `z`, the Zod schema builder, for `manager.llm` result schemas;
- `DateTime`, `Duration` and `Interval` from Luxon.

Declaring a name that already exists (`const RSI = 5`, `interface News {}`, `let manager`) fails validation. Give your own declarations distinct names (`rsiValue`, `NewsItem`, …).

A complete agent always has the same three parts, in this order:

```ts
// 1. Constants and launch settings (root).
const symbol = 'ETH';
const zone = 'UTC';
const period = manager.getVariable('rsiPeriod', 14);

// 2. Data and indicators (root).
const daily = manager.getIntervalData(symbol, 1440, zone);
const rsi = new RSI(daily.close, period);

// 3. Listeners, whose callbacks hold all the logic (root).
manager.onIntervalCandle(symbol, 1440, () => {
  if (!rsi.hasValue()) return;
  const value = rsi.getValue();
  if (value === undefined) return;
  if (value < 30) manager.sendNotification(`**ETH** daily RSI is **${value.toFixed(1)}**.`);
}, zone);
```

## Root phase and callback phase

The code runs in two phases, and the validator enforces the split.

**Root phase: once, at start-up.** The top level of your code. It declares constants, reads launch variables, creates candle series and indicators, and attaches listeners. At this point there is no "now": no current time, no current price, no wallet to trade.

**Callback phase: many times, later.** Each listener's callback runs when its event fires (a candle closes, a schedule ticks, an order fills, a message arrives). Callbacks read values and act.

| Allowed at the root only | Allowed in callbacks only |
|---|---|
| `new` of any indicator (including `History`, `PartialIndicator`) | `manager.getCurrentTime()`, `manager.getCurrentPrice()` |
| `manager.getIntervalData(...)` | `getValue()` on an indicator |
| attaching listeners: `manager.on…(...)`, `manager.cron(...)` | orders and trades: `placeOrder`, `openTrade`, `closeTrade`, `closePosition`, `cancelOrder` |
| | `manager.increaseCapital(...)`, `manager.sendNotification(...)` |

`manager.getVariable` works anywhere but belongs at the root. `manager.getSymbolMetadata` is asynchronous: `await` it at the root, or call it inside an event callback (`onNews`, `onMessage`, `onPieuvreEvent`), never inside a candle or schedule callback.

Two consequences trip first drafts:

- **A listener attached inside a callback is refused.** Attach every listener at the root, once. If behaviour depends on a condition, check the condition inside the callback.
- **A helper function inherits the scope of its call site.** A function that calls `getValue()` is fine when called from a callback and refused when called from the root. A function that constructs an indicator must only be called from the root.

## Assets are symbols you choose

Code refers to an asset by a short symbol string you pick (`'BTC'`, `'AAPL'`, `'SPY'`). It is not a ticker lookup: when you create the agent you declare which real asset each symbol stands for, and the engine binds it to a concrete listing when the agent runs (a backtest venue, your broker live). Never write an exchange-prefixed ticker or an internal identifier in code.

Keep one symbol per asset and reuse it everywhere. If the agent trades one asset and only reads another (a filter, a correlation), give each its own symbol.

Symbols that the code discovers at run time (a ranking, a position you already hold, a resolved ticker) are opaque trading keys. Pass them to trading and price calls as they are, and never show them to a user: name assets in messages with `position.ticker ?? position.symbol`, a ranked entry's `ticker`, or your own declared symbol.

## Listeners and what their callbacks receive

| Listener | Fires | Callback argument |
|---|---|---|
| `manager.onIntervalCandle(symbol, interval, cb, timeZone?)` | when a candle of that interval closes | none: read `candles.getValue()` |
| `manager.onIntervalCandleOpen(symbol, interval, cb, timeZone?)` | when a new candle of that interval opens | none: read `candles.getPending()` |
| `manager.onPreciseCandle(symbol, cb)` | on every price update | `(candle: Candle)` |
| `manager.cron(expression, cb, timeZone?)` | on a cron schedule | none |
| `manager.onTradeFilled(cb)` / `manager.onTradeFinished(cb)` | a trade of this agent fills / closes | `(trade: Trade)` |
| `manager.onOrderExecuted(cb)` / `manager.onOrderCanceled(cb)` | an order of this agent executes / is cancelled | `(order: Order)` |
| `manager.onNews(request, cb)` | a matching news post arrives | `(news: News)`, i.e. `{ content }` |
| `manager.onMessage(request, cb)` | a followed account or channel posts | `(message: Message)` |
| `manager.onPieuvreEvent(request, cb)` | a matching data update arrives | `(event: PieuvreEvent)` |
| `manager.onAltData(identifier, cb)` | an alternative-data point arrives | `(point: AltDataPoint<T>)` |

A callback may be `async`. Its errors must not escape: wrap awaited work in `try { … } catch (error) { manager.handleError(error); }`, and never leave a promise unhandled.

### Subscribing to a symbol's prices

The engine only loads prices for symbols the code subscribes to. A symbol used by `getIntervalData`, `onIntervalCandle` or `onPreciseCandle` is subscribed. A symbol you only trade or price from a `cron` callback is not: subscribe it at the root with an empty listener.

```ts
const symbol = 'BTC';
manager.onPreciseCandle(symbol, () => {}); // load BTC prices for the schedule below

manager.cron('0 12 * * *', () => {
  const price = manager.getCurrentPrice(symbol);
  if (price === undefined) return; // no price yet (market closed, feed not started)
  manager.debugLog('BTC at noon', price);
}, 'UTC');
```

Symbols that the code discovers at run time (rankings, resolved tickers, positions you hold) are subscribed by the engine when they are returned or traded, so they need no empty listener.

## Intervals and time zones

An interval is a number of **minutes** (`1`, `5`, `15`, `60`, `240`, `1440`), or `'week'` / `'month'`. Write `60` for hourly and `1440` for daily; there is no `'1h'` or `'1d'`.

Time zones are IANA names (`'America/New_York'`, `'Europe/Paris'`, `'UTC'`). Two separate rules:

- **Candles of a day or longer** (`1440`, `'week'`, `'month'`) must state the asset's exchange time zone on both `getIntervalData` and the listener, or the validator refuses them: `'America/New_York'` for US stocks and ETFs, `'Europe/Paris'` for Euronext, `'UTC'` for 24/7 crypto. A UTC day boundary would split an exchange session in two.
- **Schedules** (`manager.cron`) default to UTC. Pass the zone of the wall-clock time you mean: `manager.cron('0 9 * * 1', cb, 'Europe/Paris')` is 09:00 Paris, winter and summer.

Intraday candles need no zone.

## Reading candles and indicators

`manager.getIntervalData(symbol, interval, timeZone?)` returns the candle series. Inside a callback, `candles.getValue()` is the last closed candle, `{ openTime, closeTime, open, high, low, close, volume }`, times in epoch milliseconds. `candles.close`, `candles.high`, `candles.volume`, `candles.hl2`, `candles.hlc3` and `candles.ohlc4` are ready-made number streams to feed indicators.

Every indicator follows the same protocol:

1. `hasValue()` is false until the indicator has been fed once. It is a type guard: after it, `getValue()` is typed without `undefined` wrapping.
2. Many indicators still return `number | undefined` (or objects with `| undefined` fields) during their warm-up. Check `value === undefined` (or each field you use) before arithmetic.
3. Never use `!` to silence a missing value, and never coerce it to `0`.

The [Indicator reference](/docs/developers/stse/indicator-reference.md) gives each class's value type and warm-up length.

### Looking back

`new History(indicator, n)` keeps the last `n` values, oldest first. It is the tool for "the previous candle", "the highest high of the last 20 candles" and "two consecutive closes".

```ts
const symbol = 'SPY';
const zone = 'America/New_York';
const daily = manager.getIntervalData(symbol, 1440, zone);
const last21 = new History(daily, 21);

manager.onIntervalCandle(symbol, 1440, () => {
  if (!last21.hasValue()) return;
  const window = last21.getValue();
  if (window.length < 21) return; // still filling up
  const previous20 = window.slice(0, -1); // exclude the current candle
  const current = window[window.length - 1];
  if (current === undefined) return;
  const highest = Math.max(...previous20.map((c) => c.high));
  if (current.close > highest) manager.debugLog('20-day breakout', current.close);
}, zone);
```

Compare the current value with a window that excludes it: the current close can never exceed a maximum that includes it.

### Several timeframes and several assets

Create one series per symbol and interval at the root. In a callback, every other series may not have ticked yet (another market's hours, a later listing, a higher timeframe still forming): guard each one with `hasValue()` before reading it. A higher-timeframe series read from a lower-timeframe callback returns its last **closed** candle.

```ts
const symbol = 'BTC';
const zone = 'UTC';
const daily = manager.getIntervalData(symbol, 1440, zone);
const hourly = manager.getIntervalData(symbol, 60);
const trend = new EMA(daily.close, 200);
const rsi = new RSI(hourly.close, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!trend.hasValue() || !rsi.hasValue() || !daily.hasValue()) return;
  const ema200 = trend.getValue();
  const rsiValue = rsi.getValue();
  if (ema200 === undefined || rsiValue === undefined) return;
  const uptrend = daily.getValue().close > ema200;
  if (uptrend && rsiValue < 30) manager.debugLog('pullback in an uptrend', rsiValue);
});
```

For several assets on the same schedule, prefer one listener that loops over the symbols over one listener per symbol: it keeps the logic, the budget and the notifications in one place.

## State that survives restarts

Local variables live only as long as the process. The engine restarts agents (deployments, maintenance), and on restart the root runs again, candles and indicators are rebuilt from history, and local variables start from scratch. Anything that must survive goes in the status bag:

```ts
interface CounterState {
  alerts?: number;
  lastAlertTime?: number;
}
const symbol = 'BTC';
const hourly = manager.getIntervalData(symbol, 60);

manager.onIntervalCandle(symbol, 60, () => {
  if (!hourly.hasValue()) return;
  const state = manager.getStatus<CounterState>('default');
  const candle = hourly.getValue();
  if (state.lastAlertTime === candle.closeTime) return; // already handled this candle
  state.lastAlertTime = candle.closeTime;
  state.alerts = (state.alerts ?? 0) + 1;
  if (state.alerts >= 3) manager.stop('three alerts sent');
});
```

- `manager.getStatus<T>(key)` returns a persistent object for that key. Mutate it in place; the engine saves it.
- Every field is optional (the bag starts as `{}`), so declare them all with `?` and initialise before use. Pass your shape as the type argument; never cast.
- Keep it small and JSON-shaped: numbers, strings, booleans, arrays, plain objects. Trim lists you append to.
- Saves are periodic, not transactional. After an abrupt crash the last few seconds of changes can be lost, so an agent that places orders should also check the real book (`manager.getPositions()`, `manager.getSymbolPendingOrders()`) rather than trust the status alone.

Use state only for what must span callbacks. A condition evaluated within one candle needs no state.

## Launch variables

`manager.getVariable(name, default)` reads a setting the user can change when starting a backtest or a live run, without editing the code: an indicator period, a threshold, a size.

```ts fragment
const rsiPeriod = manager.getVariable('rsiPeriod', 14);   // number
const mode = manager.getVariable('mode', 'long');         // string
const trail = manager.getVariable('useTrailing', false);  // boolean
```

Rules the validator enforces:

- Both arguments are literals: the name matches `[A-Za-z][A-Za-z0-9_]{0,63}`, the default is a finite number, a string or a boolean. No expressions.
- Call it directly on `manager` (no alias, no destructuring, no stored method reference), once per name, into a root `const`, and pass the value around.
- A value that feeds an indicator period or length must be a whole number, so its default must be an integer.
- The default is canonical: a run that sets nothing behaves exactly as the code reads. The returned value always has the default's type.

Make only the knobs a user would realistically tune into variables (a handful, 16 at most). Structure (which indicator, which schedule, which asset) stays in code.

## Time

Never read the wall clock: `Date.now()`, `new Date()` without an argument, `DateTime.now()`, `DateTime.local()` and `DateTime.utc()` are refused, because a backtest replays the past. Use `manager.getCurrentTime()` (in a callback) or a candle's `openTime` / `closeTime`, and build dates from them:

```ts fragment
const now = DateTime.fromMillis(manager.getCurrentTime()).setZone('America/New_York');
if (now.weekday > 5) return; // Saturday or Sunday in New York
```

`setTimeout` and `setInterval` are refused too: schedule with `manager.cron`.

## How many times should it act?

Decide before writing the code:

- **Once.** Act, then call `manager.stop()` in the same callback. Without it the listener keeps firing. `stop()` does not cancel pending orders or close trades: do that first if needed.
- **N times.** Count in the status bag and call `manager.stop()` at N.
- **Every time the condition becomes true.** Latch the state in the status bag so a condition that stays true does not re-fire on every candle, and re-arm when it clears.
- **Forever.** A strategy or a recurring alert: never stop. The user controls it.

```ts
interface LatchState { below?: boolean }
const symbol = 'NVDA';
const zone = 'America/New_York';
const daily = manager.getIntervalData(symbol, 1440, zone);
const ema = new EMA(daily.close, 200);

manager.onIntervalCandle(symbol, 1440, () => {
  if (!ema.hasValue() || !daily.hasValue()) return;
  const average = ema.getValue();
  if (average === undefined) return;
  const state = manager.getStatus<LatchState>('default');
  const isBelow = daily.getValue().close < average;
  if (isBelow && state.below !== true) {
    state.below = true;
    manager.sendNotification(`**NVDA** closed below its 200-day average (**$${average.toFixed(2)}**).`);
  } else if (!isBelow && state.below === true) {
    state.below = false; // re-arm for the next crossing
  }
}, zone);
```

## Errors and diagnostics

- `manager.handleError(error)` reports a caught error without crashing the run. Give it the error, never a promise.
- `manager.debugFlag('label')` records that a line was reached; `manager.debugLog(...values)` records values. Both show in backtest results and help answer "why did it never trade?". Put a flag before each early `return` that can swallow the expected action. Labels are string literals.
- `manager.comment(text)` writes a note to the agent's journal, visible to the user in live runs too.

## The lifecycle outside the code

1. Write the code, using these pages.
2. Validate it (`validate_agent_code` over MCP): free, deterministic, runs nothing. Iterate until it passes. See [Validation rules](/docs/developers/stse/rules.md).
3. Create the agent (`create_agent`) with a plain-language description and, optionally, your code. Obside's builder reimplements it into a reviewed agent; you never run code directly on Obside.
4. Backtest it, then run it on a demo portfolio or live.

Not every agent can be backtested: one that reads your live portfolio, calls an AI model on a schedule, reads news or waits for events can only be validated in a demo run. See [Pitfalls](/docs/developers/stse/pitfalls.md#backtest-or-demo).

# Manager API

`manager` is the single object your agent code receives. Every capability the engine exposes is a method on it. This page gives each method's exact signature (generated from the engine and checked in CI), what its parameters mean, what it returns and how it behaves in a backtest and in a live or demo run. Read [Structure and lifecycle](/docs/developers/stse/agent-structure.md) first for the root-versus-callback model that governs where each call is allowed. The object shapes named in signatures (`CreateOrder`, `Trade`, `Order`, …) are spelled out in [Types](/docs/developers/stse/types.md).

Two conventions run through the whole API:

- **Root versus callbacks.** Setup calls (create an indicator, attach a listener) belong at the root. Calls that read "now" or act on the market belong inside a callback. Where it matters, it is noted below, and the validator enforces it.
- **Guard before you read.** Anything that may not have a value yet returns `undefined` (a price before the market opens, an indicator still warming up). Check for `undefined` before using the value. Never coerce a missing value to `0`.

Units used everywhere: times are epoch **milliseconds**; intervals are **minutes** (or `'week'` / `'month'`); prices are in the instrument's quote currency; `risk` and `proportion` are **fractions** (0.01 = 1%); `delta` on exits is a **percentage** (0 to 100).

## How the engine runs your agent

Where a run is a **backtest** (history replayed on a fresh paper wallet), a **demo** run (live prices, paper wallet) or a **live** run (your broker account), the behaviour below differs as noted.

**Start-up.** The root runs once. No callback fires until it has finished, including everything it `await`s. In a demo or live run the engine then replays recent history into every candle series (callbacks stay silent during this replay), so indicators are warm when the first live callback fires. **A backtest has no warm-up:** indicators start empty on the backtest's start date, so a 200-day average has no value for the first 200 days of the window. Choose a start date that leaves room for your longest indicator.

**Clock.** A backtest advances minute by minute on 1-minute candles, and also stops at every schedule time and candle boundary even when the market is closed. A demo or live run ticks every second, a couple of seconds behind real time.

**Order of events within one tick.** When several things happen at the same instant, they run in this order:

1. Resting orders are processed by the wallet (fills, cancellations, and their `onOrderExecuted` / `onOrderCanceled` / `onTradeFilled` callbacks).
2. The current price moves to the new candle.
3. Stop-losses and take-profits are checked, then `onPreciseCandle` callbacks run.
4. `onAltData` callbacks, then **`cron` callbacks**, in the order they were attached.
5. Candle series update, then `onIntervalCandle` callbacks run (all series that close at this instant are updated before the first of these callbacks).
6. `onIntervalCandleOpen` callbacks.

So a `cron` that fires exactly on a candle boundary runs **before** that candle closes: a `'0 0 * * *'` schedule reads daily indicators that do not yet include the day that just ended. Schedule a minute later, or use `onIntervalCandle`, when you need the closed candle.

**Async callbacks.** A callback may be `async`. In a backtest the engine waits for it before advancing the clock, so awaited work completes at its simulated time. In a live or demo run it is not awaited: the next tick can start before it finishes. Return (or `await`) every promise you start; work left running after a backtest's last tick fails the run.

**Errors.** An exception that escapes a callback fails a backtest, and stops a live or demo agent (it is restarted automatically some minutes later). Catch errors from awaited calls and report them with `manager.handleError(error)`.

**Restarts.** Live and demo agents restart on deployments and after a crash. A restart runs the root again, rebuilds indicators from history, restores status bags, open orders, open trades and positions, and re-arms schedules. Local variables are lost, and a schedule occurrence that fell during the downtime is not replayed.

## Time and price

```ts signature
getCurrentTime(): number
```

`manager.getCurrentTime()` returns the engine's current time, in epoch milliseconds: the simulated time in a backtest, the real time otherwise. Use it instead of the real clock so backtests stay deterministic. Callback only.

```ts signature
getCurrentPrice(symbol: string): number | undefined
```

`manager.getCurrentPrice(symbol)` returns the latest price for a symbol, or `undefined` if none has been seen yet. In a backtest a symbol has no price until its first candle inside the window; in a demo or live run subscribed symbols are primed before the first callback. Inside `onIntervalCandle` and `cron` callbacks this is normally the open of the minute that just started. A symbol you never subscribed (see [subscribing](/docs/developers/stse/agent-structure.md#subscribing-to-a-symbols-prices)) stays `undefined`. Callback only.

```ts signature
hasPriceData(symbol: string): boolean
```

`manager.hasPriceData(symbol)` reports whether a price has been seen for the symbol.

```ts signature
getSymbolMetadata(symbol: string): Promise<SymbolMetadata>
```

`manager.getSymbolMetadata(symbol)` returns a symbol's `tickSize`, and when known `pipSize`, `pointSize`, `contractSize`, `lotSize`, `timeZone` (the listing's IANA session zone), `quoteAssetSymbol` and display identity (`ticker`, `name`). It is asynchronous: `await` it at the root, or call it inside an event callback (`onNews`, `onMessage`, `onPieuvreEvent`), never in a candle or schedule callback. It rejects when no metadata exists for the symbol. On futures, `qty` counts contracts and the engine applies `contractSize` itself; use `contractSize` only to convert a dollar risk into a contract count.

## Candles and indicators

```ts signature
getIntervalData(symbol: string, interval: Interval, timeZone?: string): ICandleIndicator
```

`manager.getIntervalData(symbol, interval, timeZone?)` returns the candle series of a symbol at an interval given in minutes (`60` for hourly, `1440` for daily; `'week'` or `'month'` for coarser). Create it at the root; it also subscribes the symbol. Bars align on the time zone: `1440` closes at local midnight of `timeZone`, `'week'` on Monday 00:00, `'month'` on the 1st. When `timeZone` is omitted the asset's exchange zone is used; for daily or coarser intervals always pass it (see [Intervals and time zones](/docs/developers/stse/agent-structure.md#intervals-and-time-zones)). The same symbol, interval and zone always return the same series. Candles with no trades are not emitted (no gap filling).

The series' value is the last closed `Candle` (`{ openTime, closeTime, open, high, low, close, volume }`); `getPending()` returns the candle still forming. `.open`, `.high`, `.low`, `.close`, `.volume`, `.hl2`, `.hlc3`, `.ohlc4`, `.openTime` and `.closeTime` are number streams you can feed to indicators.

```ts signature
onIntervalCandle(symbol: string, interval: Interval, callback: () => void, timeZone?: string): void
```

`manager.onIntervalCandle(symbol, interval, callback, timeZone?)` runs the callback when a candle closes. For an exchange-listed asset, a daily candle closes at midnight in the exchange's zone, after the session: a market order placed from that callback fills at the last traded price on paper, and at the next session's open on a live broker. To trade during the session, act at the next candle's open (`onIntervalCandleOpen`) or on a schedule inside market hours (for example `'55 15 * * 1-5'` New York for "at the close"). The callback takes no argument: read the closed candle with `getIntervalData(...).getValue()` and your indicators with `getValue()`; they are already updated. A daily-or-coarser interval must pass the time zone. Attach at the root.

```ts signature
onIntervalCandleOpen(symbol: string, interval: Interval, callback: () => void, timeZone?: string): void
```

`manager.onIntervalCandleOpen(symbol, interval, callback, timeZone?)` runs the callback when a new candle opens, that is on the first trade after the boundary (for a US stock's daily candle, the first minute of the session, not midnight). `getCurrentPrice` then returns the opening price. Read the new candle with `getPending()`. Attach at the root.

```ts signature
onPreciseCandle(symbol: string, callback: (candle: Candle) => void): void
```

`manager.onPreciseCandle(symbol, callback)` runs on each price update with the latest candle: once per 1-minute candle in a backtest, at most once per second in a demo or live run. It runs before candle series and indicators include that update. An empty `manager.onPreciseCandle(symbol, () => {})` at the root is also how you subscribe a symbol so a scheduled callback can read its price. Attach at the root.

Indicator construction and reading is covered in [Indicators](/docs/developers/stse/indicators.md), and every class in the [Indicator reference](/docs/developers/stse/indicator-reference.md).

## Scheduling and other events

```ts signature
cron(cronStr: string, callback: () => void, timeZone?: string): void
```

`manager.cron(expression, callback, timeZone?)` runs the callback on a cron schedule: five fields (minute, hour, day of month, month, day of week), or six with a leading seconds field; ranges, steps, lists, month and day names, and `@hourly` / `@daily` / `@weekly` / `@monthly` are accepted. Sunday is 0 or 7; when both day of month and day of week are restricted, either one matching fires. It defaults to **UTC**: pass a time zone whenever a wall-clock time matters. A cron **never fires at the moment the agent starts** (or restarts); its first run is the next matching time, and occurrences missed while the agent was down are not replayed. In a backtest every fire is a simulation step, so a per-minute schedule makes long backtests slow or impossible: see [Schedules and launch actions](/docs/developers/stse/scheduling.md). Attach at the root.

```ts signature
onAltData<T>(identifier: string, callback: (point: AltDataPoint<T>) => void): void
```

`manager.onAltData(identifier, callback)` runs when an alternative-data point arrives on a named topic. The identifier must be an exact topic (no wildcards). Attach at the root.

```ts signature
onNews(request: NewsRequest, callback: (news: News) => void): void
```

`manager.onNews(request, callback)` runs when a post matching your description arrives, `request` being `{ source: 'x', matchPrompt, handle? }`. `matchPrompt` must be a precise, headline-shaped event ("A major exchange halts withdrawals"): relevance is judged upstream, so act on `news.content` directly without re-checking it. Only posts from monitored X accounts are pushed, not press articles: for press coverage, poll `searchNews` on a schedule. `handle` does not filter anything. Live and demo only (not backtestable). Attach at the root.

```ts signature
onMessage(request: MessageRequest, callback: (message: Message) => void): void
```

`manager.onMessage(request, callback)` runs for **every** post of one followed source: `{ source: 'x', handle: 'elonmusk' }` (without `@`) or `{ source: 'telegram', channel: 'channelname' }` (a public channel). The callback receives `{ content, author, url, publishedAt, media }`; `content` is `''` on an image-only post, and `media` holds `{ id, kind }` handles you can pass to `manager.llm(..., { images })`. There is no upstream filter: filter in code. Following an X account is billed per delivered post; Telegram is free. An X follow with a literal handle is backtestable (up to 5 handles: past posts are replayed at their time, without images); Telegram is not. Attach at the root.

```ts signature
onPieuvreEvent(request: PieuvreEventRequest, callback: (event: PieuvreEvent) => void): void
```

`manager.onPieuvreEvent(request, callback)` runs on a matching data-engine update (an economic release, a macro event). `request` is `{ matchPrompt, sourceIds?, composites?, minSeverity?, titleContains? }`; set `sourceIds` (for example `['eodhd.economic_calendar']` for the economic calendar) and `minSeverity` whenever you can, because every candidate event is matched against `matchPrompt` and billed. The event is `{ sourceId, summary, value?, severity?, timestamp, payload? }`; for an economic release, `payload.actual`, `payload.estimate` and `payload.previous` hold the figures at release time. Not backtestable. Attach at the root.

## Orders

Size an order with **either** `qty` (a number of units you compute) **or** `amount` (a cash notional). Use `amount` when you do not know the price up front. Every cash `amount` must carry an explicit `amountCurrency` such as `'USD'`.

```ts signature
placeOrder(order: CreateOrder): Order | undefined
```

`manager.placeOrder(order)` submits an order and returns it, or `undefined`. Use this for buying, selling, accumulating and rebalancing. Callback only. The fields you set:

| Field | Meaning |
|---|---|
| `symbol` | Your declared symbol, or a trading key returned by the engine. |
| `type` | `'MARKET'`, `'LIMIT'`, or `'MARKET_TRIGGER'` (a stop-market). `LIMIT` and `MARKET_TRIGGER` are paper and backtest only for now: a live broker refuses them. |
| `position` | `'buy'` or `'sell'`. A sell with nothing held opens a short on paper. |
| `qty` | Units (contracts on futures). Exactly one of `qty` / `amount`. |
| `amount` | Cash notional. `MARKET` orders only. Needs `amountCurrency`. |
| `amountCurrency` | `'USD'` by default choice; `'USDT'` / `'USDC'` are treated as USD in backtests; any other currency only sizes in demo and live runs. Only with `amount`. |
| `limitPrice` | For `LIMIT` only. |
| `triggerPrice` | For `MARKET_TRIGGER` only. |
| `inhibitAutoInvest` | `true` when the order spends cash the wallet already holds; omit it for a contribution that brings new money in. See [capital and auto-invest](/docs/developers/stse/orders-and-risk.md#capital-and-auto-invest). |

The other fields of `CreateOrder` are engine-internal: do not set them.

How orders execute:

- **Paper (backtest and demo):** a `MARKET` order with a known price fills immediately, inside `placeOrder`, at the current price plus the configured spread; its `onOrderExecuted` callback fires before `placeOrder` returns. A `MARKET` order on a symbol with no price yet rests and fills on the next candle. A `LIMIT` order fills when a 1-minute close crosses the limit, **at the limit price** (a buy limit above the market fills at the limit, not at the better market price). A `MARKET_TRIGGER` fires when a close crosses the trigger and fills at that close. Paper fills are never partial.
- **Live:** only `MARKET` orders, for now. The order goes to your broker, which decides the fill; fills are observed a few seconds later and can be partial (`filledQty` smaller than `qty`).
- **Fees:** backtests apply the venue's fee schedule (for example 0% commission plus a 0.01% spread on stocks and ETFs, 0.10% commission on spot crypto). Demo runs apply none.
- **Returns `undefined`** when the size is 0, when the symbol is outside the agent's allowed scope, in a live run when a cash `amount` cannot be priced yet, or for a `LIMIT` order on a live broker (for now). In the last two cases the order is skipped and the agent's journal says so.
- **Throws** on an invalid shape (both or neither of `qty` / `amount`, a negative or non-finite size, `amount` on a non-`MARKET` order, a price field on the wrong type, a `LIMIT` or `MARKET_TRIGGER` price that is missing or not a positive number), on a symbol that was never subscribed in a backtest, beyond 500 pending orders, and for a `MARKET_TRIGGER` order on a live broker.

The returned `Order` is a live object the engine updates (`executedTime`, `filledQty`, `price`, `canceledTime`, `cancelCause`). Its `_id` is how you match it in `onOrderExecuted` / `onOrderCanceled`.

```ts signature
cancelOrder(order: Order): void
```

`manager.cancelOrder(order)` cancels a pending order of this agent. It throws if the order is already executed or cancelled: check `manager.getSymbolPendingOrders(symbol)` first. Callback only.

```ts signature
getSymbolPendingOrders(symbol: string): readonly Order[]
```

```ts signature
getPendingOrders(): readonly Order[]
```

`manager.getSymbolPendingOrders(symbol)` and `manager.getPendingOrders()` list this agent's orders that have neither executed nor been cancelled.

```ts signature
onOrderExecuted(callback: (order: Order) => void): void
```

```ts signature
onOrderCanceled(callback: (order: Order) => void): void
```

`manager.onOrderExecuted(callback)` and `manager.onOrderCanceled(callback)` observe this agent's own orders (including trade entries and exits and `closePosition` orders) reaching a terminal state. Match on `order._id`, and read `filledQty` for the executed quantity. A cancellation carries `canceledTime` and, when known, `cancelCause` (`'insufficientMargin'`, `'noLivePrice'`) or `cancelMessage` (a broker refusal). On paper these can fire before `placeOrder` returns, so record the order id first and reconcile, or look the id up when the call returns. Past events are not replayed after a restart. Attach at the root.

## Trades (entries with a matching exit)

A trade is an entry that you intend to close later, so it carries take-profit and stop-loss and produces realized profit when it closes. For buy-and-hold or accumulation, prefer orders.

```ts signature
openTrade(signal: V2EnterSignal): Trade | undefined
```

`manager.openTrade(signal)` opens a trade and returns it, or `undefined`. Callback only. The signal:

| Field | Meaning |
|---|---|
| `symbol`, `position` | As for orders: `'buy'` (long) or `'sell'` (short). |
| `type` | `'MARKET'`, `'LIMIT'` or `'MARKET_TRIGGER'` for the entry. A live broker takes `'MARKET'` only, for now. |
| `price` | The entry price for `LIMIT` / `MARKET_TRIGGER` (required for those). |
| `positionSize` | `{ type: 'proportion', proportion }` (fraction of the wallet's current value), `{ type: 'risk', risk }` (fraction of the wallet's value lost if the stop is hit), `{ type: 'amount', amount }` (cash) or `{ type: 'qty', qty }` (units). |
| `stoploss`, `stoploss2` … `stoploss4` | `{ type: 'fixed' \| 'trailing' \| 'special', value, delta? }`. `value` is an **absolute price**. `delta` is the percentage of the **initial** position closed at that level (omit it to close everything that is left). |
| `takeprofit`, `takeprofit2` … `takeprofit4` | `{ type: 'fixed', value, delta? }`, `{ type: 'trigger', trigger, value, delta? }` or `{ type: 'trailing', trigger, relative, delta? }`. Prices are absolute. |
| `comment` | One line naming the rule and the values that fired, shown in the trades table. |

Sizing uses the whole wallet's current (mark-to-market) value. `risk` divides by the stop distance, so it needs a stop: without one, it behaves like `proportion`. The engine does not cap `risk` sizing; a very tight stop can ask for more than the wallet can margin, and the entry is then cancelled. A stop on the wrong side of the entry (a buy stop at or above the entry price) makes `openTrade` return `undefined`. If the symbol has no price yet, the request is queued until its first candle and `undefined` is returned. A `LIMIT` entry on a live broker also returns `undefined`, without being queued: it is skipped (for now), and the journal says so.

Stops and take-profits are watched by the engine, not placed at the broker: they are checked on every 1-minute candle's high and low in a backtest, and every second in a demo or live run. A level triggers when price crosses it strictly. A backtest exit fills at the level itself, even on a gap through it; a demo or live exit fills at the market price when triggered. A `'trailing'` stop keeps its initial distance behind the best price; a `'trigger'` take-profit arms when price reaches `trigger` and then exits at `value`; a `'trailing'` take-profit arms at `trigger` and then trails `relative` behind the best price. To move a stop later, use the `updateStoploss` [helper](/docs/developers/stse/helpers.md).

```ts signature
closeTrade(trade: Trade, signal: LeaveSignal): void
```

`manager.closeTrade(trade, { cause, delta? })` closes a trade with a market order. `cause` is one line naming the rule and its values ("EMA20 crossed back below EMA50"). `delta` is an integer percentage (0 to 100) **of the initial position**; omit it to close everything left. If the entry has not filled yet, the entry order is cancelled instead. Callback only.

```ts signature
getSymbolPendingTrades(symbol: string): readonly Trade[]
```

```ts signature
getPendingTrades(): readonly Trade[]
```

`manager.getSymbolPendingTrades(symbol)` and `manager.getPendingTrades()` list this agent's **open** trades (entry pending or filled, not yet fully closed). There is no list of closed trades: collect what you need in `onTradeFinished`. Useful trade fields: `_id`, `symbol`, `position`, `qty`, `filledQty`, `enterPrice`, `enterTime`, `stoploss` (with `absolute`, the current level), `takeprofit`, `exitHistory`, and `intelligence.earnings` (realized profit), `latent?.earnings` (unrealized).

```ts signature
onTradeFilled(callback: (trade: Trade) => void): void
```

```ts signature
onTradeFinished(callback: (trade: Trade) => void): void
```

`manager.onTradeFilled(callback)` fires when a trade's entry executes (on paper, for a `MARKET` entry, before `openTrade` returns). `manager.onTradeFinished(callback)` fires once a trade is fully closed, with its final statistics. Attach at the root.

## Positions and the account book

These read the portfolio the agent runs against. They let an agent act on positions it did not open (yours, or another agent's), not only on trades it created. The book belongs to the wallet, not to the agent: several agents on one demo portfolio see the same positions.

```ts signature
getPositions(): readonly OpenPosition[]
```

`manager.getPositions()` returns every open position: `{ symbol, ticker?, name?, qty, value, pnl }`. `qty` is negative for a short. `value` is the capital committed as a signed cash flow (negative for a long, positive for a short), not the market value: compute a position's market value as `qty × getCurrentPrice(symbol)`. `pnl` is the unrealized profit. Name a position by `position.ticker ?? position.symbol` in any user-facing text; pass `position.symbol` to order and price calls. Flat positions are omitted.

```ts signature
getPositionQty(symbol: string): number
```

```ts signature
getAveragePrice(symbol: string): number
```

`manager.getPositionQty(symbol)` returns the position's signed quantity (0 when flat). `manager.getAveragePrice(symbol)` returns its cost basis per unit (positive for longs and shorts, entry fees included, 0 when flat). For a position opened outside the agent, the average is the broker's reported one when available.

Both describe the whole portfolio the agent runs on. In a backtest that portfolio is the agent's alone, but a demo or live portfolio is shared: your own holdings and other agents' fills on the same symbol are part of the position. To know what the agent itself holds, read its trades (`getSymbolPendingTrades(symbol)`, each with its `enterPrice`) or count its own fills in `onOrderExecuted`. A strategy that decides on `getPositionQty` is refused (TS9047, see [Rules](/docs/developers/stse/rules.md#ts9047-a-strategy-decides-on-its-own-position)).

```ts signature
closePosition(symbol: string, qty?: number): Order | undefined
```

`manager.closePosition(symbol, qty?)` closes or reduces the position held right now (whoever opened it: this agent's orders, you, or another agent) with a market order on the reducing side, sized from the position at that instant (all of it, or `qty` units, capped at the position). This is the safe way to exit a position the agent did not open: it is bound to the position, so on a live account it can never flip into a reverse position on a race. It returns `undefined` when there is nothing to close. For a trade opened with `openTrade`, use `closeTrade`. Callback only.

```ts signature
getBalances(): readonly BalanceView[]
```

`manager.getBalances()` returns per-asset cash balances, each with its own currency (`{ asset, total, available?, reserved?, borrowed? }`). Never add up different currencies. On a demo portfolio or in a backtest it is one row, the free cash, as long as everything traded is quoted in the wallet's currency.

```ts signature
getHoldings(): readonly HoldingView[]
```

`manager.getHoldings()` returns the full investment book, one line per holding, each amount with its currency. Read each line's `valuation` rather than computing `quantity × price`: a holding may have no live price, and an absent field means "not reported", never zero.

```ts signature
getReportedEquity(): MoneyView | undefined
```

```ts signature
getReportedAvailableFunds(): MoneyView | undefined
```

```ts signature
getReportedBuyingPower(): MoneyView | undefined
```

`manager.getReportedEquity()`, `manager.getReportedAvailableFunds()` and `manager.getReportedBuyingPower()` return provider-reported account figures, each `{ amount, currency }`, or `undefined` when not reported. They are not spendable cash; do not size orders from them.

```ts signature
getMetrics(): Metrics
```

`manager.getMetrics()` returns nested run metrics: `orderMetrics`, `tradeMetrics` (counts, win rate, …), `capitalMetrics` (realized equity, `maxDrawdown`), `swapFeesMetrics`, `latentMetrics` and `latentCapitalMetrics` (mark-to-market, `drawdown`). There is no top-level `drawdown`. The high-water mark behind a drawdown is the highest value since the agent started, not the portfolio's all-time high.

```ts signature
getLatent(): LatentData
```

`manager.getLatent()` returns latent (unrealized) profit information. It throws until the wallet has been valued once, so call it from a callback after the first tick.

## Capital (paper and demo runs)

Read [Orders, trades and risk](/docs/developers/stse/orders-and-risk.md#capital-and-auto-invest) before sizing any order: these are not interchangeable. On a backtest or demo wallet the figures are in the wallet's currency (USD for backtests). When the agent runs against a connected real account, the same methods describe that account, in its currency; for an exact multi-currency report prefer `getBalances()`, `getHoldings()` and `getReportedEquity()`.

```ts signature
getCurrentCapital(): number
```

`manager.getCurrentCapital()` is realized capital: everything invested plus realized profit and fees. It moves only when a position is closed or reduced (or capital is added); **opening a position does not lower it**.

```ts signature
getAvailableCapital(): number
```

`manager.getAvailableCapital()` is the free cash: current capital minus the cost of every open position. Deploy it without triggering an automatic top-up. It is 0 when fully invested and can be negative. Use this for "invest the remaining cash".

```ts signature
getLatentCapital(): number
```

`manager.getLatentCapital()` is the whole wallet's mark-to-market value (realized capital plus open positions at their last price). It is a value to monitor, not spendable cash, and it only refreshes on the next tick after a fill.

```ts signature
getInvestedCapital(): number
```

`manager.getInvestedCapital()` is the cumulative capital ever put in: the starting capital, every `increaseCapital` and every automatic top-up.

```ts signature
increaseCapital(amount: number): void
```

`manager.increaseCapital(amount)` adds cash to the wallet, for a contribution or a lump sum. Callback only.

## State and settings

```ts signature
getStatus<T extends Status = StatusBag>(key: string): Partial<T>
```

`manager.getStatus<T>(key)` returns a persistent state bag that survives restarts. Pass a state shape as a type argument, for example `manager.getStatus<{ count: number }>('default')`; every field is optional because the bag starts empty, so initialize before you rely on it. The same key always returns the same live object: mutate it in place. In a live or demo run it is saved about every minute and on a clean shutdown (a crash can lose the last minute of changes); a backtest starts with an empty bag. Store JSON-like values only, and remove a field with `delete` (an `undefined` value is saved as `null`).

```ts signature
getVariable(name: string, defaultValue: number): number
```

```ts signature
getVariable(name: string, defaultValue: string): string
```

```ts signature
getVariable(name: string, defaultValue: boolean): boolean
```

`manager.getVariable(name, defaultValue)` reads a launch-time setting a user can change without editing the code (an indicator period, a threshold, a size). It returns the run's value when one was set with the same type, otherwise the default. Both arguments must be literals. Call it directly, once, into a root constant. Rules: [Launch variables](/docs/developers/stse/agent-structure.md#launch-variables).

## Communication and analysis

```ts signature
sendNotification(message: string): void
```

`manager.sendNotification(message)` sends the user a notification through the channels they enabled (the channel is never chosen in code). Mark key figures with `**bold**`. Send at most one notification per callback run: build a digest instead of notifying inside a loop (the builder refuses a notification sent from a loop). Does nothing in a backtest. Only notify when the user asked to be notified.

```ts signature
comment(comment: string): void
```

`manager.comment(text)` writes a note into the agent's journal, visible to the user in the app. Use it for an agent's reasoning or a one-off status. Not shown for backtests.

```ts signature
sendWebhook(payload: unknown): void
```

`manager.sendWebhook(payload)` posts a JSON payload to the endpoint the user configured on the agent. There is no URL argument; with none configured it simply does nothing. It never throws and returns nothing: do not await it or retry. Payloads over 64 KB are dropped, and deliveries are rate-limited (about 60 an hour per agent). Does nothing in a backtest.

```ts signature
llm<T>(llmName: string, systemPrompt: string, userPrompt: string, responseSchema: ZodType<T>, options?: { webSearch?: boolean; pieuvre?: boolean; news?: boolean; marketData?: boolean; images?: string[]; }): Promise<T | undefined>
```

`manager.llm(name, systemPrompt, userPrompt, responseSchema, options?)` asks a language model for a structured result validated against a `z` schema, and resolves to that object, or `undefined` on any failure (invalid result, credits exhausted, limits reached). Guard the result.

- `name` is the model id. When you hand code to `create_agent`, Obside's builder sets the model (name the one you want in the prompt); any string passes validation, so a placeholder such as `'default'` is fine in a draft.
- `options` gives the model tools it calls on its own: `webSearch` (open web, billed), `news` (Obside's news search, billed), `pieuvre` (fundamental and macro reads, including the economic calendar), `marketData` (quotes, daily price history, and `search_assets`, which resolves names to trading keys for this run), and `images` (media ids from `onMessage`).
- Limits per call: 12 model turns (tool calls included) and a spend cap; beyond them the call resolves to `undefined`. Out of AI usage in a live or demo run, the call also resolves to `undefined` until the monthly reset; the agent keeps running and keeps its positions.
- On a schedule or candle callback, guard the call with a period key saved in the status bag (the validator requires it: TS9026). See [News, data and AI](/docs/developers/stse/data-and-ai.md#calling-a-language-model).
- In a backtest, `llm` only works inside an `onMessage` callback (X follows are replayed); anywhere else the backtest fails.

Use it for open-ended reasoning over unstructured input, never to source a hard number you can read from the market. Every schema field the model might not know must be `.nullable()`.

```ts signature
searchNews(query: string, options?: SearchNewsOptions): Promise<NewsResult[]>
```

`manager.searchNews(query, options?)` returns matching news items, most similar first: `{ id, title, summary, url, source, publishedAt, score, instruments?, industries?, countries? }`. Put tickers and keywords in `query`. `options`: `limit` (default 10, max 25), `since` and `until` (epoch ms), `instruments`, `industries`, `countries`, `source`, `minScore` (0 to 1). It resolves to `[]` when nothing matches and rejects when the news service is unavailable. Billed per query. Not backtestable.

```ts signature
getPieuvreMetric(concept: string, query?: PieuvreMetricQuery): Promise<PieuvreMetric | undefined>
```

`manager.getPieuvreMetric(concept, { entity, asOf? })` returns a point-in-time fundamental or macro number for one entity as an object: read `.value`. `entity` (a ticker such as `'AAPL'`) is required for fundamentals. Margin, yield and return metrics come back as fractions (a 20% margin is `0.20`). It resolves to `undefined` when the data point does not exist and rejects when the data service is unavailable. Backtestable (point in time).

```ts signature
getPieuvreSeries(sourceId: string, query?: PieuvreSeriesQuery): Promise<PieuvreSeriesPoint[]>
```

`manager.getPieuvreSeries(sourceId, { from?, to?, limit?, resolution? })` returns a historical series (for example `'fred.fedfunds'`, `'fred.dgs10'`, `'composite.fear_greed'`). Not backtestable.

```ts signature
getPieuvreRank(request: PieuvreRankRequest): Promise<PieuvreRankedEntity[]>
```

`manager.getPieuvreRank(request)` returns a point-in-time ranked list of entities, for a dynamic universe: `{ metric, universe?, order?, limit?, country?, sector?, … }`. `metric` must be one of the [metric names](/docs/developers/stse/types.md#metric-names); `universe` is a universe tag such as `'sp500'`, `'us_equities'`, `'eu_equities'`, `'asia_equities'` or `'crypto_top30'` (an unknown tag silently returns `[]`); `order` is `'asc'` or `'desc'`. Each entry is `{ ticker, name?, value, rank, snaptradeSymbol, backtestSymbol }`: trade `entry.snaptradeSymbol ?? entry.backtestSymbol` and skip the entry when both are `null`. It ranks fundamentals only (no RSI or volume spike). Backtestable (point in time).

```ts signature
getPieuvreUpcoming(request: PieuvreUpcomingRequest): Promise<PieuvreEvent[]>
```

`manager.getPieuvreUpcoming({ withinMs?, sourceIds?, minSeverity?, eventTypes?, tickers?, limit? })` lists scheduled, not-yet-released calendar events (earnings, macro releases) between now and `withinMs` milliseconds ahead. `timestamp` is the scheduled release time. For an upcoming earnings, `eventType` is `'earnings'`, `ticker` is the company and `value` is the consensus EPS estimate. Always narrow it with `tickers` (at most 200) or `eventTypes`, or a busy day's thousands of events hit the row limit. A calendar `ticker` is an identity, not a trading key. Not backtestable.

```ts signature
resolveAssets(request: ResolveAssetsRequest): Promise<ResolvedAsset[]>
```

`manager.resolveAssets({ tickers, category? })` maps up to 200 tickers or names to the trading keys valid for the current run, one `{ ticker, name, class, tradingKey }` per input, in order. Trade only a non-null `tradingKey`, verbatim; a `null` means not tradable on this run's venue. Keys it returns have their price warmed before it resolves. In a backtest it only resolves symbols the run loaded (your declared assets).

## Control and diagnostics

```ts signature
stop(reason?: string): void
```

`manager.stop(reason?)` stops the agent from inside its own code: no callback fires afterwards, stops and take-profits are no longer watched. Use it for a one-shot agent after its single action fires. It does not cancel pending orders or close trades; do that first if needed.

```ts signature
handleError(error: unknown): void
```

`manager.handleError(error)` reports an error from a failed asynchronous operation. In a live or demo run it logs and the agent keeps running; **in a backtest it fails the run**, so reserve it for real failures. Pass it the error, never the work (a promise).

```ts signature
debugLog(...args: unknown[]): void
```

```ts signature
debugFlag(flag: string): void
```

`manager.debugLog(...args)` records values and `manager.debugFlag(flag)` records that a line was reached (a string-literal label). Both are visible in backtest results and are the way to answer "why did it never trade?". `debugLog` does nothing in a live run: use `comment` there.

## Not part of the public API

`V2Manager` has a few more members that are internal or deprecated and should not be used: `askNews` (use `searchNews`), `askPieuvre` (use the structured `getPieuvre…` reads) and `onAnyData`.

# Types

<!-- Generated by marketing-site/scripts/stse-docs/types-reference.mjs from stse-common. Do not edit by hand. -->

The exact shape of every object agent code passes to or receives from the `manager`, generated from the engine source that the validator compiles against. The [Manager API](/docs/developers/stse/manager-api.md) explains what each field means where it is not obvious.

Two kinds of entries:

- **In scope**: the name exists in agent code. Use it in annotations (`const pending: readonly Order[] = manager.getPendingOrders();`), never import or redeclare it. Where the name in code differs from the declaration, the entry says so (`Interval` is written `StseInterval`, because `Interval` is Luxon's class in agent code).
- **Shape only**: the shape of a value you receive, but not a name you can write. Let TypeScript infer it (`const positions = manager.getPositions();`), or declare your own interface with the fields you need.

Reading the declarations: a field marked `?` may be absent; `readonly` arrays cannot be mutated (copy with `[...array]`); a union of string literals lists every accepted value.

## Candles and intervals

### Candle

In scope as `Candle`.

```ts signature
interface Candle {
  openTime: number;
  closeTime: number;
  open: number;
  high: number;
  low: number;
  close: number;
  volume: number;
}
```

### Interval

In scope as `StseInterval` (declared as `Interval`).

```ts signature
type Interval = number | 'week' | 'month';
```

### OpenedCandle

In scope as `OpenedCandle`.

```ts signature
type OpenedCandle = Pick<Candle, "openTime" | "open">;
```

## Orders

### CreateOrder

In scope as `CreateOrder`.

```ts signature
interface CreateOrder {
  readonly type: 'MARKET' | 'LIMIT' | 'MARKET_TRIGGER';
  readonly symbol: string;
  readonly position: Position;
  readonly qty?: number;
  readonly amount?: number;
  readonly amountCurrency?: string;
  readonly priceOverride?: number | undefined;
  readonly limitPrice?: number | undefined;
  readonly triggerPrice?: number | undefined;
  readonly inhibitAutoInvest?: boolean;
  readonly inhibitReduceCapital?: boolean;
  readonly inhibitMargin?: boolean;
  readonly positionCapitalOverride?: number | undefined;
  readonly crumbsOrder?: boolean | undefined;
  readonly seedOrder?: boolean | undefined;
}
```

### Order

In scope as `Order`.

```ts signature
interface Order extends CreateOrder {
  readonly _id: string;
  readonly model: string;
  readonly realSymbol: string;
  readonly sizingQuoteSymbol?: string;
  readonly createdTime: number;
  qty: number;
  executedTime?: number;
  filledQty?: number | undefined;
  price?: number | undefined;
  priceAfterSlippage?: number | undefined;
  slippageWorstPrice?: number | undefined;
  slippageError?: boolean | undefined;
  capitalWithoutFees?: number;
  capital?: number;
  canceledTime?: number;
  cancelCause?: OrderCancelCause | undefined;
  cancelMessage?: string | undefined;
  evolutiveQty: number;
  evolutiveCapital: number;
}
```

### OrderCancelCause

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
type OrderCancelCause = 'insufficientMargin' | 'noLivePrice';
```

## Trades

### AmountPositionSize

In scope as `AmountPositionSize`.

```ts signature
interface AmountPositionSize {
  type: 'amount';
  amount: number;
}
```

### Exit

In scope as `Exit`.

```ts signature
interface Exit {
  time: number;
  delta: number;
  cause: string;
  waitingEnterOrder?: boolean | undefined;
  price?: number | undefined;
  orderQty?: number | undefined;
  filledQty?: number | undefined;
  orderId?: string | undefined;
  orderPending?: boolean | undefined;
  priceWithSlippage?: number | undefined;
  orderCapitalWithoutFees?: number | undefined;
  orderCapital?: number | undefined;
  earnings?: number | undefined;
  inhibitFinish?: boolean | undefined;
}
```

### LeaveSignal

In scope as `LeaveSignal`.

```ts signature
interface LeaveSignal {
  readonly cause: string;
  readonly delta?: number | undefined;
  readonly price?: number | undefined;
  readonly inhibitFinish?: boolean | undefined;
}
```

### PositionSize

In scope as `PositionSize`.

```ts signature
type PositionSize = ProportionPositionSize | RiskPositionSize | AmountPositionSize | QtyPositionSize;
```

### ProportionPositionSize

In scope as `ProportionPositionSize`.

```ts signature
interface ProportionPositionSize {
  type: 'proportion';
  proportion: number;
}
```

### QtyPositionSize

In scope as `QtyPositionSize`.

```ts signature
interface QtyPositionSize {
  type: 'qty';
  qty: number;
}
```

### RiskPositionSize

In scope as `RiskPositionSize`.

```ts signature
interface RiskPositionSize {
  type: 'risk';
  risk: number;
}
```

### Slippage

In scope as `Slippage`.

```ts signature
interface Slippage {
  readonly name: string;
  readonly error?: boolean | undefined;
  readonly slippage: number;
  readonly riskSlippage: number;
}
```

### Trade

In scope as `Trade`.

```ts signature
interface Trade {
  readonly _id: string;
  readonly model: string;
  readonly symbol: string;
  readonly realSymbol: string;
  readonly enterTime: number;
  readonly enterPrice: number;
  readonly position: Position;
  readonly qty: number;
  filledQty: number;
  enterOrderId?: string | undefined;
  enterOrderPending: boolean;
  enterPriceWithSlippage?: number | undefined;
  enterOrderCapitalWithoutFees?: number | undefined;
  enterOrderCapital?: number | undefined;
  readonly type: 'MARKET' | 'LIMIT' | 'MARKET_TRIGGER';
  readonly price?: number | undefined;
  finished: boolean;
  finishTime?: number;
  readonly exitHistory: Exit[];
  stoploss?: Stoploss | undefined;
  stoploss2?: Stoploss | undefined;
  stoploss3?: Stoploss | undefined;
  stoploss4?: Stoploss | undefined;
  takeprofit?: Takeprofit | undefined;
  takeprofit2?: Takeprofit | undefined;
  takeprofit3?: Takeprofit | undefined;
  takeprofit4?: Takeprofit | undefined;
  highestPrice?: number;
  lowestPrice?: number;
  intelligence: {
    readonly stoplossDistance: number;
    readonly positionRatio: number;
    readonly positionSize: number;
    readonly contractSize?: number | undefined;
    slippageHistory?: Slippage[] | undefined;
    earnings: number;
    capital: number;
    drawdown?: number | undefined;
  };
  readonly statistics: {
    rewardRisk: number;
  };
  endStatistics?: {
    readonly result: 'win' | 'loss';
    readonly duration: number;
  };
  expectancy?: {
    readonly full?: number | undefined;
    readonly t21?: number | undefined;
    readonly t50?: number | undefined;
    readonly t100?: number | undefined;
    readonly t200?: number | undefined;
    readonly d10?: number | undefined;
    readonly d30?: number | undefined;
    readonly d90?: number | undefined;
  };
  latent?: {
    readonly time: number;
    readonly earnings: number;
    readonly profit?: number | undefined;
    readonly rewardRisk?: number | undefined;
  } | undefined;
  gotham?: boolean | undefined;
  gothamTime?: number | undefined;
  piggy?: boolean | undefined;
  ricky?: boolean | undefined;
  comment?: string | undefined;
  lastUpdate?: number | undefined;
}
```

### V2EnterSignal

In scope as `V2EnterSignal`.

```ts signature
interface V2EnterSignal {
  symbol: string;
  position: Position;
  type: 'MARKET' | 'LIMIT' | 'MARKET_TRIGGER';
  price?: number | undefined;
  positionSize: PositionSize;
  stoploss?: StoplossRequest | Stoploss | undefined;
  stoploss2?: StoplossRequest | Stoploss | undefined;
  stoploss3?: StoplossRequest | Stoploss | undefined;
  stoploss4?: StoplossRequest | Stoploss | undefined;
  takeprofit?: TakeprofitRequest | Takeprofit | undefined;
  takeprofit2?: TakeprofitRequest | Takeprofit | undefined;
  takeprofit3?: TakeprofitRequest | Takeprofit | undefined;
  takeprofit4?: TakeprofitRequest | Takeprofit | undefined;
  comment?: string | undefined;
}
```

## Stop-losses and take-profits

### FixedStoploss

In scope as `FixedStoploss`.

```ts signature
interface FixedStoploss {
  readonly type: 'fixed';
  readonly initial: number;
  absolute: number;
  readonly delta?: number | undefined;
  disengaged?: boolean;
}
```

### FixedStoplossRequest

In scope as `FixedStoplossRequest`.

```ts signature
interface FixedStoplossRequest {
  readonly type: 'fixed';
  readonly value: number;
  readonly delta?: number | undefined;
}
```

### FixedTakeprofit

In scope as `FixedTakeprofit`.

```ts signature
interface FixedTakeprofit extends FixedTakeprofitRequest {
  disengaged?: boolean;
}
```

### FixedTakeprofitRequest

In scope as `FixedTakeprofitRequest`.

```ts signature
interface FixedTakeprofitRequest {
  readonly type: 'fixed';
  readonly value: number;
  readonly delta?: number | undefined;
}
```

### SpecialStoploss

In scope as `SpecialStoploss`.

```ts signature
interface SpecialStoploss {
  type: 'special';
  readonly initial: number;
  absolute: number;
  readonly delta?: number | undefined;
  disengaged?: boolean;
}
```

### SpecialStoplossRequest

In scope as `SpecialStoplossRequest`.

```ts signature
interface SpecialStoplossRequest {
  readonly type: 'special';
  readonly value: number;
  readonly delta?: number | undefined;
}
```

### Stoploss

In scope as `Stoploss`.

```ts signature
type Stoploss = FixedStoploss | TrailingStoploss | SpecialStoploss;
```

### StoplossKey

In scope as `StoplossKey`.

```ts signature
type StoplossKey = 'stoploss' | 'stoploss2' | 'stoploss3' | 'stoploss4';
```

### StoplossRequest

In scope as `StoplossRequest`.

```ts signature
type StoplossRequest = FixedStoplossRequest | SpecialStoplossRequest | TrailingStoplossRequest;
```

### Takeprofit

In scope as `Takeprofit`.

```ts signature
type Takeprofit = FixedTakeprofit | TriggerTakeprofit | TrailingTakeprofit;
```

### TakeprofitKey

In scope as `TakeprofitKey`.

```ts signature
type TakeprofitKey = 'takeprofit' | 'takeprofit2' | 'takeprofit3' | 'takeprofit4';
```

### TakeprofitRequest

In scope as `TakeprofitRequest`.

```ts signature
type TakeprofitRequest = FixedTakeprofitRequest | TriggerTakeprofitRequest | TrailingTakeprofitRequest;
```

### TrailingStoploss

In scope as `TrailingStoploss`.

```ts signature
interface TrailingStoploss {
  readonly type: 'trailing';
  readonly initial: number;
  absolute: number;
  readonly delta?: number | undefined;
  disengaged?: boolean;
}
```

### TrailingStoplossRequest

In scope as `TrailingStoplossRequest`.

```ts signature
interface TrailingStoplossRequest {
  readonly type: 'trailing';
  readonly value: number;
  readonly delta?: number | undefined;
}
```

### TrailingTakeprofit

In scope as `TrailingTakeprofit`.

```ts signature
interface TrailingTakeprofit extends TrailingTakeprofitRequest {
  absolute?: number;
  disengaged?: boolean;
}
```

### TrailingTakeprofitRequest

In scope as `TrailingTakeprofitRequest`.

```ts signature
interface TrailingTakeprofitRequest {
  readonly type: 'trailing';
  readonly trigger: number;
  readonly relative: number;
  readonly delta?: number | undefined;
}
```

### TriggerTakeprofit

In scope as `TriggerTakeprofit`.

```ts signature
interface TriggerTakeprofit extends TriggerTakeprofitRequest {
  triggered?: boolean;
  disengaged?: boolean;
}
```

### TriggerTakeprofitRequest

In scope as `TriggerTakeprofitRequest`.

```ts signature
interface TriggerTakeprofitRequest {
  readonly type: 'trigger';
  readonly trigger: number;
  readonly value: number;
  readonly delta?: number | undefined;
}
```

## Positions and account

### BalanceView

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface BalanceView {
  asset: string;
  total: number;
  available?: number;
  reserved?: number;
  borrowed?: number;
}
```

### HoldingView

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface HoldingView {
  holding: string;
  asset?: string;
  name: string;
  quantity?: number;
  valuation?: MoneyView;
  averageUnitPrice?: MoneyView;
  reportedUnitValue?: MoneyView;
  pnl?: MoneyView;
  pnlRatio?: number;
  assetClass?: string;
  valuationSource: 'provider' | 'market';
}
```

### LatentData

In scope as `LatentData`.

```ts signature
interface LatentData {
  readonly wallet: string;
  readonly time: number;
  readonly date: Date;
  readonly invested: number;
  readonly capital: number;
  readonly earnings: number;
  readonly sum: number;
  readonly ratio: number;
  readonly exposition: number;
  readonly ricky: number;
}
```

### MoneyView

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface MoneyView {
  amount: number;
  currency: string;
}
```

### OpenPosition

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface OpenPosition {
  symbol: string;
  ticker?: string | undefined;
  name?: string | undefined;
  qty: number;
  value: number;
  pnl: number;
}
```

### Position

In scope as `Position`.

```ts signature
type Position = 'buy' | 'sell';
```

## Run metrics

### CapitalMetrics

In scope as `CapitalMetrics`.

```ts signature
interface CapitalMetrics {
  maxCapital: number | undefined;
  drawdown: number | undefined;
  maxDrawdown: number | undefined;
  avgDrawdown: number | undefined;
  performance: number | undefined;
  performanceMaxDrawdownRatio: number | undefined;
  cagr: number | undefined;
  cagrMaxDrawdownRatio: number | undefined;
  cagrAvgDrawdownRatio: number | undefined;
  sharpe: number | undefined;
  sortino: number | undefined;
}
```

### LatentMetrics

In scope as `LatentMetrics`.

```ts signature
interface LatentMetrics {
  timeInMarket: number | undefined;
  maxExposition: number | undefined;
}
```

### Metrics

In scope as `Metrics`.

```ts signature
interface Metrics {
  orderMetrics: OrderMetrics;
  tradeMetrics: TradeMetrics;
  capitalMetrics: CapitalMetrics;
  swapFeesMetrics: SwapFeesMetrics;
  latentMetrics: LatentMetrics;
  latentCapitalMetrics: CapitalMetrics;
  pareto?: number | undefined;
}
```

### ModelMetrics

In scope as `ModelMetrics`.

```ts signature
interface ModelMetrics {
  orderMetrics: OrderMetrics;
  tradeMetrics: TradeMetrics;
  pareto?: number | undefined;
}
```

### OrderMetrics

In scope as `OrderMetrics`.

```ts signature
interface OrderMetrics {
  orders: number;
  volume: number;
  orderFees: number;
}
```

### SwapFeesMetrics

In scope as `SwapFeesMetrics`.

```ts signature
interface SwapFeesMetrics {
  total: number;
  gains: number;
  losses: number;
}
```

### TradeMetrics

In scope as `TradeMetrics`.

```ts signature
interface TradeMetrics {
  trades: number;
  yearlyTrades: number | undefined;
  avgTradeDuration: number | undefined;
  wins: number;
  maxConsecutiveWins: number;
  losses: number;
  maxConsecutiveLosses: number;
  winrate: number | undefined;
  evR: number;
  maxEvR: number;
  evRDrawdown: number;
  maxEvRDrawdown: number;
  expectancy: number | undefined;
  t21Expectancy: number | undefined;
  t50Expectancy: number | undefined;
  t100Expectancy: number | undefined;
  t200Expectancy: number | undefined;
  d10Expectancy: number | undefined;
  d30Expectancy: number | undefined;
  d90Expectancy: number | undefined;
  sqn: number | undefined;
  rewardRiskRatio: number | undefined;
  profitFactor: number | undefined;
  avgPositionRatio: number | undefined;
  avgStoplossDistance: number | undefined;
  avgSlippage: number | undefined;
  avgSlippageR: number | undefined;
  longs: number;
  longProportion: number | undefined;
  shorts: number;
  shortProportion: number | undefined;
  fees: number;
}
```

### WalletMetrics

In scope as `WalletMetrics`.

```ts signature
interface WalletMetrics {
  capitalMetrics: CapitalMetrics;
  swapFeesMetrics: SwapFeesMetrics;
  latentMetrics: LatentMetrics;
  latentCapitalMetrics: CapitalMetrics;
}
```

## State

### Status

In scope as `Status`.

```ts signature
type Status = object;
```

### StatusBag

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
type StatusBag = Record<string, unknown>;
```

## Symbols and assets

### ResolveAssetsRequest

In scope as `ResolveAssetsRequest`.

```ts signature
interface ResolveAssetsRequest {
  tickers: string[];
  category?: string;
}
```

### ResolvedAsset

In scope as `ResolvedAsset`.

```ts signature
interface ResolvedAsset {
  ticker: string;
  name: string | null;
  class: string | null;
  tradingKey: string | null;
}
```

### SymbolMetadata

In scope as `SymbolMetadata`.

```ts signature
interface SymbolMetadata {
  readonly symbol: string;
  readonly tickSize: number;
  readonly pipSize?: number | undefined;
  readonly pointSize?: number | undefined;
  readonly contractSize?: number | undefined;
  readonly lotSize?: number | undefined;
  readonly timeZone?: string | undefined;
  readonly quoteAssetSymbol?: string | undefined;
  readonly ticker?: string | undefined;
  readonly name?: string | undefined;
  readonly assetClass?: string | undefined;
}
```

## News and messages

### BaseNewsRequest

In scope as `BaseNewsRequest`.

```ts signature
interface BaseNewsRequest {
  matchPrompt: string;
}
```

### Message

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface Message {
  content: string;
  author: string;
  url: string;
  publishedAt: number;
  media: MessageMedia[];
}
```

### MessageMedia

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface MessageMedia {
  id: string;
  kind: 'photo' | 'video_thumbnail';
}
```

### MessageRequest

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
type MessageRequest = XMessageRequest | TelegramMessageRequest;
```

### News

In scope as `News`.

```ts signature
interface News {
  content: string;
}
```

### NewsRequest

In scope as `NewsRequest`.

```ts signature
type NewsRequest = XNewsRequest;
```

### NewsResult

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface NewsResult {
  id: string;
  title: string;
  summary: string;
  url: string;
  source: string;
  publishedAt: number;
  score: number;
  instruments?: string[];
  industries?: string[];
  countries?: string[];
}
```

### SearchNewsOptions

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface SearchNewsOptions {
  limit?: number;
  since?: number;
  until?: number;
  instruments?: string[];
  industries?: string[];
  countries?: string[];
  source?: string;
  minScore?: number;
}
```

### TelegramMessageRequest

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface TelegramMessageRequest {
  source: 'telegram';
  channel: string;
}
```

### XMessageRequest

Shape only: not a name in agent code, let TypeScript infer it.

```ts signature
interface XMessageRequest {
  source: 'x';
  handle: string;
}
```

### XNewsRequest

In scope as `XNewsRequest`.

```ts signature
interface XNewsRequest extends BaseNewsRequest {
  source: 'x';
  handle?: string;
}
```

## Alternative data

### AltDataPoint

In scope as `AltDataPoint`.

```ts signature
interface AltDataPoint<T> {
  time: number;
  value: T;
}
```

## Fundamental and macro data

### PieuvreDataType

In scope as `PieuvreDataType`.

```ts signature
type PieuvreDataType = 'observation' | 'event' | 'snapshot' | 'composite';
```

### PieuvreEvent

In scope as `PieuvreEvent`.

```ts signature
interface PieuvreEvent {
  deliveryId?: string;
  sentAt?: number;
  sourceId: string;
  dataType: PieuvreDataType;
  summary: string;
  value?: number;
  severity?: PieuvreSeverity;
  timestamp: number;
  eventType?: string;
  ticker?: string;
  previousTimestamp?: number;
  payload?: Record<string, unknown>;
}
```

### PieuvreEventRequest

In scope as `PieuvreEventRequest`.

```ts signature
interface PieuvreEventRequest {
  matchPrompt: string;
  sourceIds?: string[];
  composites?: string[];
  minSeverity?: PieuvreSeverity;
  titleContains?: string;
}
```

### PieuvreMetric

In scope as `PieuvreMetric`.

```ts signature
interface PieuvreMetric {
  sourceId: string;
  periodType?: string;
  value: number;
  unit?: string;
  period?: string;
  asOf: number;
}
```

### PieuvreMetricQuery

In scope as `PieuvreMetricQuery`.

```ts signature
interface PieuvreMetricQuery {
  sourceId?: string;
  periodType?: string;
  entity?: string;
  period?: string;
  asOf?: number;
}
```

### PieuvreRankedEntity

In scope as `PieuvreRankedEntity`.

```ts signature
interface PieuvreRankedEntity {
  sourceId?: string;
  periodType?: string;
  period?: string;
  unit?: string;
  asOf?: number;
  sampleCount?: number;
  ticker: string;
  name?: string;
  type?: string;
  figi?: string;
  isin?: string;
  value: number;
  rank: number;
  snaptradeSymbol: string | null;
  backtestSymbol: string | null;
}
```

### PieuvreRankRequest

In scope as `PieuvreRankRequest`.

```ts signature
interface PieuvreRankRequest {
  sourceId?: string;
  periodType?: string;
  metric: PieuvreMetricName;
  universe?: string;
  country?: string;
  region?: string;
  sector?: string;
  industry?: string;
  type?: string;
  order?: 'asc' | 'desc';
  limit?: number;
  mode?: 'latest' | 'min' | 'max' | 'avg' | 'sum';
  from?: number;
  to?: number;
  asOf?: number;
}
```

### PieuvreSeriesPoint

In scope as `PieuvreSeriesPoint`.

```ts signature
interface PieuvreSeriesPoint {
  time: number;
  value: number;
}
```

### PieuvreSeriesQuery

In scope as `PieuvreSeriesQuery`.

```ts signature
interface PieuvreSeriesQuery {
  from?: number;
  to?: number;
  limit?: number;
  resolution?: 'raw' | 'daily' | 'weekly';
}
```

### PieuvreSeverity

In scope as `PieuvreSeverity`.

```ts signature
type PieuvreSeverity = 'low' | 'medium' | 'high' | 'critical';
```

### PieuvreUpcomingRequest

In scope as `PieuvreUpcomingRequest`.

```ts signature
interface PieuvreUpcomingRequest {
  withinMs?: number;
  sourceIds?: string[];
  minSeverity?: PieuvreSeverity;
  eventTypes?: string[];
  tickers?: string[];
  limit?: number;
}
```

## Every name in scope

This block uses every type name marked "in scope" above. It is compiled by the validator in Obside's test suite, which proves each of those names exists in agent code.

```ts
type InScopeTypes = [AltDataPoint<number>, AmountPositionSize, BaseNewsRequest, Candle, CapitalMetrics, CreateOrder, Exit, FixedStoploss, FixedStoplossRequest, FixedTakeprofit, FixedTakeprofitRequest, LatentData, LatentMetrics, LeaveSignal, Metrics, ModelMetrics, News, NewsRequest, OpenedCandle, Order, OrderMetrics, PieuvreDataType, PieuvreEvent, PieuvreEventRequest, PieuvreMetric, PieuvreMetricQuery, PieuvreRankRequest, PieuvreRankedEntity, PieuvreSeriesPoint, PieuvreSeriesQuery, PieuvreSeverity, PieuvreUpcomingRequest, Position, PositionSize, ProportionPositionSize, QtyPositionSize, ResolveAssetsRequest, ResolvedAsset, RiskPositionSize, Slippage, SpecialStoploss, SpecialStoplossRequest, Status, Stoploss, StoplossKey, StoplossRequest, StseInterval, SwapFeesMetrics, SymbolMetadata, Takeprofit, TakeprofitKey, TakeprofitRequest, Trade, TradeMetrics, TrailingStoploss, TrailingStoplossRequest, TrailingTakeprofit, TrailingTakeprofitRequest, TriggerTakeprofit, TriggerTakeprofitRequest, V2EnterSignal, WalletMetrics, XNewsRequest];
const inScopeTypeCount: InScopeTypes['length'] = 63;
manager.debugLog(inScopeTypeCount);
```

The "shape only" names are refused if you write them (TypeScript reports "Cannot find name"):

```ts refused TS2304
type ShapeOnlyTypes = [BalanceView, HoldingView, Message, MessageMedia, MessageRequest, MoneyView, NewsResult, OpenPosition, OrderCancelCause, SearchNewsOptions, StatusBag, TelegramMessageRequest, XMessageRequest];
manager.debugLog('refused');
```

## Metric names

`getPieuvreRank({ metric })` accepts exactly these 470 names (the `PieuvreMetricName` type), and they are the names `getPieuvreMetric(concept, …)` understands. Any other string is refused by the validator.

`200_day_ma`, `50_day_ma`, `52_week_high`, `52_week_low`, `account_payables`, `accounts_payable`, `accounts_payables`, `accounts_receivable`, `accounts_receivables`, `accrued_expenses`, `accumulated_amortization`, `accumulated_depreciation`, `accumulated_other_comprehensive_income`, `accumulated_other_comprehensive_income_loss`, `acquisitions_net`, `additional_paid_in_capital`, `altman_z_score`, `analyst_buy`, `analyst_buy_count`, `analyst_earnings_estimate_avg_current_q`, `analyst_earnings_estimate_avg_current_y`, `analyst_earnings_estimate_avg_next_1q`, `analyst_earnings_estimate_avg_next_1y`, `analyst_earnings_estimate_growth_current_q`, `analyst_earnings_estimate_growth_current_y`, `analyst_earnings_estimate_growth_next_1q`, `analyst_earnings_estimate_growth_next_1y`, `analyst_earnings_estimate_high_current_q`, `analyst_earnings_estimate_high_current_y`, `analyst_earnings_estimate_high_next_1q`, `analyst_earnings_estimate_high_next_1y`, `analyst_earnings_estimate_low_current_q`, `analyst_earnings_estimate_low_current_y`, `analyst_earnings_estimate_low_next_1q`, `analyst_earnings_estimate_low_next_1y`, `analyst_earnings_estimate_number_of_analysts_current_q`, `analyst_earnings_estimate_number_of_analysts_current_y`, `analyst_earnings_estimate_number_of_analysts_next_1q`, `analyst_earnings_estimate_number_of_analysts_next_1y`, `analyst_earnings_estimate_year_ago_eps_current_q`, `analyst_earnings_estimate_year_ago_eps_current_y`, `analyst_earnings_estimate_year_ago_eps_next_1q`, `analyst_earnings_estimate_year_ago_eps_next_1y`, `analyst_eps_revisions_down_last30days_current_q`, `analyst_eps_revisions_down_last30days_current_y`, `analyst_eps_revisions_down_last30days_next_1q`, `analyst_eps_revisions_down_last30days_next_1y`, `analyst_eps_revisions_down_last7days_current_q`, `analyst_eps_revisions_down_last7days_current_y`, `analyst_eps_revisions_down_last7days_next_1q`, `analyst_eps_revisions_down_last7days_next_1y`, `analyst_eps_revisions_up_last30days_current_q`, `analyst_eps_revisions_up_last30days_current_y`, `analyst_eps_revisions_up_last30days_next_1q`, `analyst_eps_revisions_up_last30days_next_1y`, `analyst_eps_revisions_up_last7days_current_q`, `analyst_eps_revisions_up_last7days_current_y`, `analyst_eps_revisions_up_last7days_next_1q`, `analyst_eps_revisions_up_last7days_next_1y`, `analyst_eps_trend30days_ago_current_q`, `analyst_eps_trend30days_ago_current_y`, `analyst_eps_trend30days_ago_next_1q`, `analyst_eps_trend30days_ago_next_1y`, `analyst_eps_trend60days_ago_current_q`, `analyst_eps_trend60days_ago_current_y`, `analyst_eps_trend60days_ago_next_1q`, `analyst_eps_trend60days_ago_next_1y`, `analyst_eps_trend7days_ago_current_q`, `analyst_eps_trend7days_ago_current_y`, `analyst_eps_trend7days_ago_next_1q`, `analyst_eps_trend7days_ago_next_1y`, `analyst_eps_trend90days_ago_current_q`, `analyst_eps_trend90days_ago_current_y`, `analyst_eps_trend90days_ago_next_1q`, `analyst_eps_trend90days_ago_next_1y`, `analyst_eps_trend_current_current_q`, `analyst_eps_trend_current_current_y`, `analyst_eps_trend_current_next_1q`, `analyst_eps_trend_current_next_1y`, `analyst_growth_current_q`, `analyst_growth_current_y`, `analyst_growth_next_1q`, `analyst_growth_next_1y`, `analyst_hold`, `analyst_hold_count`, `analyst_net_bullish_count`, `analyst_rating`, `analyst_revenue_estimate_avg_current_q`, `analyst_revenue_estimate_avg_current_y`, `analyst_revenue_estimate_avg_next_1q`, `analyst_revenue_estimate_avg_next_1y`, `analyst_revenue_estimate_growth_current_q`, `analyst_revenue_estimate_growth_current_y`, `analyst_revenue_estimate_growth_next_1q`, `analyst_revenue_estimate_growth_next_1y`, `analyst_revenue_estimate_high_current_q`, `analyst_revenue_estimate_high_current_y`, `analyst_revenue_estimate_high_next_1q`, `analyst_revenue_estimate_high_next_1y`, `analyst_revenue_estimate_low_current_q`, `analyst_revenue_estimate_low_current_y`, `analyst_revenue_estimate_low_next_1q`, `analyst_revenue_estimate_low_next_1y`, `analyst_revenue_estimate_number_of_analysts_current_q`, `analyst_revenue_estimate_number_of_analysts_current_y`, `analyst_revenue_estimate_number_of_analysts_next_1q`, `analyst_revenue_estimate_number_of_analysts_next_1y`, `analyst_revenue_estimate_year_ago_revenue_current_q`, `analyst_revenue_estimate_year_ago_revenue_current_y`, `analyst_revenue_estimate_year_ago_revenue_next_1q`, `analyst_revenue_estimate_year_ago_revenue_next_1y`, `analyst_sell`, `analyst_sell_count`, `analyst_strong_buy`, `analyst_strong_buy_count`, `analyst_strong_sell`, `analyst_strong_sell_count`, `begin_period_cash_flow`, `beta`, `book_value`, `book_value_per_share`, `bottom_line_net_income`, `capex`, `capital_expenditure`, `capital_expenditures`, `capital_lease_obligations`, `capital_lease_obligations_current`, `capital_lease_obligations_non_current`, `capital_stock`, `capital_surpluse`, `cash`, `cash_and_cash_equivalents`, `cash_and_cash_equivalents_changes`, `cash_and_equivalents`, `cash_and_short_term_investments`, `cash_at_beginning_of_period`, `cash_at_end_of_period`, `cash_equivalents`, `cash_flows_other_operating`, `cash_per_share`, `cash_ratio`, `change_in_cash`, `change_in_working_capital`, `change_receivables`, `change_to_account_receivables`, `change_to_inventory`, `change_to_liabilities`, `change_to_netincome`, `change_to_operating_activities`, `circulating_supply`, `common_dividends`, `common_dividends_paid`, `common_stock`, `common_stock_issuance`, `common_stock_repurchase`, `common_stock_repurchased`, `common_stock_shares_outstanding`, `common_stock_total_equity`, `congress_buy_disclosures_90d`, `congress_buy_disclosures_filed_30d`, `congress_distinct_buyers_90d`, `congress_distinct_buyers_filed_30d`, `congress_net_buy_pressure_90d`, `congress_net_buy_pressure_filed_30d`, `cost_and_expenses`, `cost_of_revenue`, `current_deferred_revenue`, `current_ratio`, `debt_to_assets`, `debt_to_capital`, `debt_to_equity`, `deferred_income_tax`, `deferred_long_term_asset_charges`, `deferred_long_term_liab`, `deferred_revenue`, `deferred_revenue_non_current`, `deferred_tax_liabilities_non_current`, `deferred_taxes`, `depreciation`, `depreciation_and_amortization`, `diluted_eps_ttm`, `discontinued_operations`, `dividend_per_share`, `dividend_share`, `dividend_yield`, `dividends_paid`, `earning_assets`, `earnings_yield`, `ebit`, `ebit_margin`, `ebitda`, `ebitda_margin`, `effect_of_accounting_charges`, `effect_of_forex_changes_on_cash`, `effective_tax_rate`, `end_cash_position`, `end_period_cash_flow`, `enterprise_value`, `enterprise_value_revenue`, `eps`, `eps_actual`, `eps_basic`, `eps_difference`, `eps_diluted`, `eps_estimate`, `eps_estimate_current_quarter`, `eps_estimate_current_year`, `eps_estimate_next_quarter`, `eps_estimate_next_year`, `estimated_ebit_avg`, `estimated_ebit_high`, `estimated_ebit_low`, `estimated_ebitda_avg`, `estimated_ebitda_high`, `estimated_ebitda_low`, `estimated_eps_avg`, `estimated_eps_high`, `estimated_eps_low`, `estimated_net_income_avg`, `estimated_net_income_high`, `estimated_net_income_low`, `estimated_revenue_avg`, `estimated_revenue_high`, `estimated_revenue_low`, `estimated_sga_expense_avg`, `estimated_sga_expense_high`, `estimated_sga_expense_low`, `etf_average_duration`, `etf_average_maturity`, `etf_distribution_yield`, `etf_expense_ratio`, `etf_holdings_count`, `etf_total_assets`, `ev_to_ebitda`, `ev_to_free_cash_flow`, `ev_to_sales`, `exchange_rate_changes`, `extraordinary_items`, `fails_to_deliver_notional`, `fails_to_deliver_shares`, `financial_leverage`, `financing_cash_flow`, `fiscal_year`, `float_shares`, `forward_pe`, `free_cash_flow`, `free_cash_flow_per_share`, `free_cash_flow_yield`, `free_float_percent`, `fully_diluted_valuation`, `general_and_administrative_expenses`, `good_will`, `goodwill`, `goodwill_and_intangible_assets`, `gross_margin`, `gross_profit`, `gross_profit_ttm`, `growth_score`, `income_before_tax`, `income_tax`, `income_tax_expense`, `income_tax_paid`, `income_taxes_paid`, `intangible_assets`, `interest_coverage`, `interest_expense`, `interest_income`, `interest_paid`, `inventory`, `investing_cash_flow`, `investments`, `investments_and_advances`, `investments_in_property_plant_and_equipment`, `issuance_of_capital_stock`, `last_dividend`, `liabilities_and_stockholders_equity`, `long_term_debt`, `long_term_debt_issuance`, `long_term_debt_payments`, `long_term_debt_total`, `long_term_investments`, `long_term_net_debt_issuance`, `market_cap`, `market_cap_rank`, `market_capitalization_mln`, `market_dominance`, `max_supply`, `minority_interest`, `minority_interests`, `negative_goodwill`, `net_acquisitions`, `net_borrowings`, `net_cash_provided_by_financing_activities`, `net_cash_provided_by_investing_activities`, `net_cash_provided_by_operating_activities`, `net_change_in_cash`, `net_common_stock_issuance`, `net_debt`, `net_debt_issuance`, `net_debt_to_ebitda`, `net_dividends_paid`, `net_income`, `net_income_applicable_to_common_shares`, `net_income_continuous_operations`, `net_income_deductions`, `net_income_from_continuing_operations`, `net_income_from_continuing_ops`, `net_income_from_discontinued_operations`, `net_income_per_share`, `net_interest_income`, `net_invested_capital`, `net_margin`, `net_preferred_stock_issuance`, `net_receivables`, `net_stock_issuance`, `net_tangible_assets`, `net_working_capital`, `non_current_assets_total`, `non_current_liabilities_other`, `non_current_liabilities_total`, `non_currrent_assets_other`, `non_operating_income_excluding_interest`, `non_operating_income_net_other`, `non_recurring`, `noncontrolling_interest_in_consolidated_entity`, `operating_cash_flow`, `operating_cash_flow_per_share`, `operating_expenses`, `operating_income`, `operating_margin`, `other_adjustments_to_net_income`, `other_assets`, `other_cashflows_from_financing_activities`, `other_cashflows_from_investing_activities`, `other_current_assets`, `other_current_liab`, `other_current_liabilities`, `other_expenses`, `other_financing_activities`, `other_income_expense`, `other_investing_activities`, `other_items`, `other_liab`, `other_liabilities`, `other_non_cash_items`, `other_non_current_assets`, `other_non_current_liabilities`, `other_operating_expenses`, `other_payables`, `other_receivables`, `other_short_term_investments`, `other_stockholder_equity`, `other_total_stockholders_equity`, `other_working_capital`, `payout_ratio`, `pe_ratio`, `peg_ratio`, `percent_insiders`, `percent_institutions`, `piotroski_score`, `preferred_dividends_paid`, `preferred_stock`, `preferred_stock_and_other_adjustments`, `preferred_stock_dividends`, `preferred_stock_redeemable`, `preferred_stock_total_equity`, `prepaid_assets`, `prepaids`, `pretax_income`, `price`, `price_target_1m`, `price_target_1y`, `price_target_3m`, `price_target_average`, `price_target_high`, `price_target_low`, `price_target_median`, `price_to_book`, `price_to_free_cash_flow`, `price_to_operating_cash_flow`, `price_to_sales`, `property_plant_and_equipment_gross`, `property_plant_and_equipment_net`, `property_plant_equipment`, `property_plant_equipment_net`, `purchase_of_investments`, `purchases_of_investments`, `quality_score`, `quarterly_earnings_growth_yoy`, `quarterly_revenue_growth_yoy`, `quick_ratio`, `reconciled_depreciation`, `research_and_development`, `research_and_development_expenses`, `research_development`, `restricted_cash`, `retained_earnings`, `retained_earnings_total_equity`, `return_on_assets`, `return_on_capital_employed`, `return_on_equity`, `return_on_invested_capital`, `revenue`, `revenue_per_share`, `revenue_per_share_ttm`, `rnd_expense`, `sale_of_investments`, `sale_purchase_of_stock`, `sales_maturities_of_investments`, `selling_and_marketing_expenses`, `selling_general_administrative`, `selling_general_and_administrative`, `selling_general_and_administrative_expenses`, `sga_expense`, `shares_float`, `shares_outstanding`, `shares_outstanding_basic`, `shares_outstanding_diluted`, `shares_short`, `shares_short_prior_month`, `short_long_term_debt`, `short_long_term_debt_total`, `short_percent`, `short_percent_float`, `short_percent_outstanding`, `short_ratio`, `short_term_debt`, `short_term_debt_issuance`, `short_term_investments`, `short_term_net_debt_issuance`, `stock_based_compensation`, `stock_compensation`, `stock_repurchases`, `stockholders_equity`, `surprise_percent`, `tax_assets`, `tax_payable`, `tax_payables`, `tax_provision`, `temporary_equity_redeemable_noncontrolling_interests`, `total_assets`, `total_cash_from_financing_activities`, `total_cash_from_operating_activities`, `total_cashflows_from_investing_activities`, `total_current_assets`, `total_current_liabilities`, `total_debt`, `total_equity`, `total_investments`, `total_liab`, `total_liabilities`, `total_liabilities_and_total_equity`, `total_non_current_assets`, `total_non_current_liabilities`, `total_operating_expenses`, `total_other_income_expense_net`, `total_other_income_expenses_net`, `total_payables`, `total_permanent_equity`, `total_revenue`, `total_stockholder_equity`, `total_stockholders_equity`, `total_supply`, `treasury_stock`, `value_score`, `volume_avg`, `wall_street_target_price`, `warrants`, `weighted_average_shs_out`, `weighted_average_shs_out_dil`

### Metrics returned as fractions

These come back as fractions, not percentages: a 20% margin is `0.20`. Compare against `0.20`, never `20`.

`dividend_yield`, `payout_ratio`, `effective_tax_rate`, `earnings_yield`, `free_cash_flow_yield`, `gross_margin`, `operating_margin`, `net_margin`, `ebit_margin`, `ebitda_margin`, `return_on_assets`, `return_on_equity`, `return_on_invested_capital`, `return_on_capital_employed`, `market_dominance`

# Indicators

Technical indicators are already in scope in agent code. You construct them with `new`, at the root, and you read their values inside callbacks. You never import them.

## Construct at the root, read in a callback

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60); // hourly candles
const ema = new EMA(new PartialIndicator(candles, 'close'), 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!ema.hasValue()) return;        // guard first
  const value = ema.getValue();
  if (value === undefined) return;    // many indicators stay `| undefined`
  manager.debugLog(`EMA20 is ${value.toFixed(2)}`);
});
```

Two building blocks appear constantly:

- `PartialIndicator` picks one field of a candle series. Most numeric indicators (EMA, SMA, RSI, and so on) take a stream of numbers, not candles, so you wrap the series: `new RSI(new PartialIndicator(candles, 'close'), 14)`.
- `History` keeps the last N values of any indicator so you can look back: `new History(candles, 4)` gives the four most recent candles, oldest first, newest last.

## Guarding values

`getValue()` returns nothing until an indicator has been fed enough data. The rules:

- Always call `hasValue()` before `getValue()`. It is a type guard, so no non-null assertion is needed after it.
- Many single-number indicators are still typed as possibly undefined even after `hasValue()`, so add a `value === undefined` check before doing arithmetic.
- For `History`, `hasValue()` only means the series has started, not that the buffer is full. Return early while the array is shorter than the period you need, then read an element into a constant and check it before use.
- Composite indicators (for example MACD, Stochastic) return an object whose fields are each independently possibly undefined. Read the value into one constant, then guard exactly the fields you use.

## Common families

The catalog has more than 70 classes. The [Indicator reference](/docs/developers/stse/indicator-reference.md) gives each one's exact constructor, input kind, parameters and defaults, value type and fields, warm-up length and a working snippet, grouped by family:

- [Building blocks](/docs/developers/stse/indicator-reference/building-blocks.md): `PartialIndicator`, `LambdaIndicator`, `DerivedIndicator`, `ManualIndicator`, `History`, `Crossover`, `Extreme`, `Percentile`.
- New reference classes: `ALMA`, `DEMA`, `TEMA`, `LSMA`, `Median`, `Envelope`, `MovingAverageCross`, `BBTrend`, `ChandeKrollStop`, `ChandelierExit`.
- [Moving averages and trend](/docs/developers/stse/indicator-reference/trend.md): `SMA`, `EMA`, `WMA`, `RMA`, `HullMovingAverage`, `KAMA`, `VIDYA`, `VWAP`, `SuperTrend`, `SAR`, `Ichimoku`, `ADX`, `WilliamsAlligator`.
- [Momentum and oscillators](/docs/developers/stse/indicator-reference/momentum.md): `Aroon`, `AroonOscillator`, `AwesomeOscillator`, `BalanceOfPower`, `BullBearPower`, `BollingerPercentB`, `ConnorsRSI`, `CoppockCurve`, `DMI`, `FisherTransform`, `KnowSureThing`, `MassIndex`, `PriceOscillator`, `SMIErgodicIndicator`, `SMIErgodicOscillator`, `TrueStrengthIndex`, `StochasticMomentumIndex`, `UltimateOscillator`, `Vortex`, `UlcerIndex`, `HistoricalVolatility`, `AverageDailyRange`, `AdvanceDeclineRatioBars`, `ChopZone`, `PringsSpecialK`, `RCIRibbon`, `RankCorrelationIndex`, `WoodiesCCI`, `RSI`, `MACD`, `Stochastic`, `CCI`, `Momentum`, `ROC`, `WilliamsPercentR`, `TRIX`, `SqueezeMomentum`, `Divergences`.
- [Volatility, bands and ranges](/docs/developers/stse/indicator-reference/volatility.md): `TR` (smooth it with `RMA` for an ATR), `Bollinger`, `BBW`, `KeltnerChannels`, `DC`, `Chop`.
- [Volume](/docs/developers/stse/indicator-reference/volume.md): `OnBalanceVolume`, `MFI`, `ChaikinMoneyFlow`, `ChaikinOscillator`, `EaseOfMovement`, `EldersForceIndex`, `KlingerOscillator`, `PercentageVolumeOscillator`, `VolumeProfile`, `VWMA`.
- [Market structure and patterns](/docs/developers/stse/indicator-reference/structure.md): `OrderBlocks`, `FVG`, `MarketStructure` (break of structure and change of character), `Swings`, `SupportResistance`, `ZigZag`, `PivotPoints`, `PivotsHighLow`, `BollingerBars`, `Engulfing`, `Heikinashi`, `Renko` and `RenkoAtr`.

Three things to check in a signature before using a class:

- **The input kind.** `ICandleIndicator` or `Indicator<Candle>` takes the candle series itself; `Indicator<number>` takes a number stream that is never `undefined` (`candles.close`); `Indicator<number | undefined>` also accepts another indicator's output. `new RSI(new EMA(candles.close, 10), 14)` does not compile, because `RSI` wants `Indicator<number>`.
- **Required parameters.** Some classes have no defaults: `new MACD(candles.close, 'ema', 12, 26, 'ema', 9)` needs all six arguments.
- **The value type.** Numbers, objects with fields that are each possibly `undefined` (`MACD`, `Bollinger`, `Stochastic`), or labels (`Crossover` returns `'firstUp' | 'secondUp' | null`, never a number).

If the concept you need is not in the catalog, compute it in code from candles (with `History`, `DerivedIndicator` or `LambdaIndicator`) rather than inventing a class name: an unknown class fails validation.

## A worked reading

An ATR-style volatility read from true range, then a stop distance:

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 240); // 4h
const tr = new TR(candles);
const atr = new RMA(tr, 14); // a smoothed average of true range

manager.onIntervalCandle(symbol, 240, () => {
  if (!atr.hasValue()) return;
  const atrValue = atr.getValue();
  const price = manager.getCurrentPrice(symbol);
  if (atrValue === undefined || price === undefined) return;
  const stopPrice = price - 3 * atrValue; // an absolute price, not a distance
  manager.debugLog(`stop would sit at ${stopPrice.toFixed(2)}`);
});
```

# Indicator reference

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Every indicator class available in agent code, 125 in total, with its exact constructor, the kind of input it takes, its parameters and defaults, the value it returns, how long it takes to warm up and a snippet that passes the validator. Read [Indicators](/docs/developers/stse/indicators.md) first for the rules that apply to all of them.

Signatures on these pages are generated from the engine source, so they match what the validator compiles against.

## Reading a signature

- `Indicator<Candle>` and `ICandleIndicator` mean the candle series from `manager.getIntervalData(...)`. `ICandleIndicator` accepts only that series itself, not a derived one.
- `Indicator<number>` means a number stream whose values are never `undefined`: `candles.close` (or `new PartialIndicator(candles, 'close')`), `candles.volume`, `candles.hl2`, `candles.hlc3`, `candles.ohlc4`.
- `Indicator<number | undefined>` also accepts the output of another indicator that warms up, such as an `EMA` or an `RSI`. You cannot pass such an output where `Indicator<number>` is required (for example `new RSI(new EMA(...), 14)` does not compile).
- A default shown as `= 20` makes the parameter optional.
- Value type names such as `MACDValue` or `IchimokuOutput` describe the shape but are not names you can use in code: let TypeScript infer them (`const value = macd.getValue();`).

## All indicators

| Class | Page | What it computes |
|---|---|---|
| [CandleIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#candleindicator) | Building blocks | The candle series returned by `manager.getIntervalData(symbol, interval)`. |
| [Crossover](/docs/developers/stse/indicator-reference/building-blocks.md#crossover) | Building blocks | Detects the candle on which one series crosses another. |
| [DerivedIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#derivedindicator) | Building blocks | Computes a value from several indicators each time all of them have updated. |
| [Extreme](/docs/developers/stse/indicator-reference/building-blocks.md#extreme) | Building blocks | The highest and lowest value over the last `period` updates, the current one included. |
| [History](/docs/developers/stse/indicator-reference/building-blocks.md#history) | Building blocks | Keeps the last `period` values of another indicator as an array, oldest first and newest last. |
| [Index](/docs/developers/stse/indicator-reference/building-blocks.md#index) | Building blocks | The zero-based update count of another indicator (0 on its first value). |
| [IteratorIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#iteratorindicator) | Building blocks | Wraps an iterator that yields one value per update of its dependencies. |
| [LambdaIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#lambdaindicator) | Building blocks | Applies a pure function to each value of another indicator, without storing anything. |
| [ManualIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#manualindicator) | Building blocks | An indicator you feed yourself with `setValue(value)`. |
| [PartialIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#partialindicator) | Building blocks | Selects one field of an indicator's object value, producing a new indicator. |
| [Percentile](/docs/developers/stse/indicator-reference/building-blocks.md#percentile) | Building blocks | The percentile rank (0 to 100) of the current value among the last `period` values: the share of those values strictly below it. |
| [PineIndicator](/docs/developers/stse/indicator-reference/building-blocks.md#pineindicator) | Building blocks | Runs a TradingView Pine Script indicator or strategy as-is, with TradingView's series semantics: on each candle it gives what the script shows on a chart of the same candles (plots, markers, alertconditions, labels, boxes, lines, tables and strategy orders). |
| [ADX](/docs/developers/stse/indicator-reference/trend.md#adx) | Moving averages and trend | Average directional index with the +DI and −DI lines (Wilder smoothing), like TradingView's DMI: DI length `period`, ADX smoothing `adxSmoothing`. |
| [ALMA](/docs/developers/stse/indicator-reference/trend.md#alma) | Moving averages and trend | Gaussian-weighted moving average of closes. |
| [BeepBoop](/docs/developers/stse/indicator-reference/trend.md#beepboop) | Moving averages and trend | Trend label from a MACD(12, 26, 9) histogram sign and the candle position relative to a 50-period EMA: `'up'` when the histogram is positive and the whole candle is above the EMA, `'down'` for the mirror case, `'neutral'` otherwise. |
| [ChandeKrollStop](/docs/developers/stse/indicator-reference/trend.md#chandekrollstop) | Moving averages and trend | Rolling extrema of ATR-adjusted high and low stops. |
| [ChandelierExit](/docs/developers/stse/indicator-reference/trend.md#chandelierexit) | Moving averages and trend | Highest high minus ATR distance and lowest low plus ATR distance, as in the TradingView ta library. |
| [DEMA](/docs/developers/stse/indicator-reference/trend.md#dema) | Moving averages and trend | Twice an EMA minus the EMA of that EMA. |
| [EIT](/docs/developers/stse/indicator-reference/trend.md#eit) | Moving averages and trend | Ehlers instantaneous trendline, computed with a Hilbert-transform estimate of the dominant cycle. |
| [Elder](/docs/developers/stse/indicator-reference/trend.md#elder) | Moving averages and trend | Elder impulse system: `'bullish'` when both the EMA and the MACD histogram rose since the previous candle, `'bearish'` when both fell, `'neutral'` otherwise. |
| [EMA](/docs/developers/stse/indicator-reference/trend.md#ema) | Moving averages and trend | Exponential moving average, seeded with the simple average of the first `period` values, then `value = source × α + previous × (1 − α)` with `α = smoothing / (1 + period)`. |
| [FIR](/docs/developers/stse/indicator-reference/trend.md#fir) | Moving averages and trend | A 4-tap finite-impulse-response smoother: `(4·x0 + 3·x1 + 2·x2 + x3) / 10`. |
| [HalfTrend](/docs/developers/stse/indicator-reference/trend.md#halftrend) | Moving averages and trend | HalfTrend: a trend line that follows the `amplitude`-candle high/low averages and flips on a break, with ATR channels. |
| [HullMovingAverage](/docs/developers/stse/indicator-reference/trend.md#hullmovingaverage) | Moving averages and trend | Hull moving average: `WMA(2 × WMA(n/2) − WMA(n), √n)`, a low-lag average. |
| [Ichimoku](/docs/developers/stse/indicator-reference/trend.md#ichimoku) | Moving averages and trend | Ichimoku cloud: Tenkan and Kijun (midpoints of the `tPeriod` / `kPeriod` high-low range), Senkou spans A and B, and the lagging span. |
| [KAMA](/docs/developers/stse/indicator-reference/trend.md#kama) | Moving averages and trend | Kaufman adaptive moving average: its smoothing speeds up in trends and slows down in noise, based on the efficiency ratio over `length` values. |
| [LinearRegression](/docs/developers/stse/indicator-reference/trend.md#linearregression) | Moving averages and trend | The value, at the current candle, of the least-squares line fitted to the last `period` values. |
| [LSMA](/docs/developers/stse/indicator-reference/trend.md#lsma) | Moving averages and trend | Least-squares regression of the current source window, evaluated at length - 1 - offset. |
| [MovingAverageCross](/docs/developers/stse/indicator-reference/trend.md#movingaveragecross) | Moving averages and trend | Two close SMAs with a cross value only where either line crosses the other. |
| [MVWAP](/docs/developers/stse/indicator-reference/trend.md#mvwap) | Moving averages and trend | Moving VWAP: the average of the last `period` completed VWAP windows (days, weeks or months). |
| [RMA](/docs/developers/stse/indicator-reference/trend.md#rma) | Moving averages and trend | Wilder's running moving average (the smoothing used by RSI and ATR): seeded with a simple average, then `value = (previous × (period − 1) + source) / period`. |
| [SAR](/docs/developers/stse/indicator-reference/trend.md#sar) | Moving averages and trend | Parabolic SAR: a trailing stop-and-reverse level that accelerates with the trend. |
| [SMA](/docs/developers/stse/indicator-reference/trend.md#sma) | Moving averages and trend | Simple moving average: the arithmetic mean of the last `period` values. |
| [SuperTrend](/docs/developers/stse/indicator-reference/trend.md#supertrend) | Moving averages and trend | SuperTrend: ATR bands around the median price (`hl2 ± multiplier × ATR`), flipping direction when the close crosses the active band. |
| [TEMA](/docs/developers/stse/indicator-reference/trend.md#tema) | Moving averages and trend | Three cascaded close EMAs combined as 3 * (EMA1 - EMA2) + EMA3. |
| [TTS](/docs/developers/stse/indicator-reference/trend.md#tts) | Moving averages and trend | Trend Trader Strategy line: a trailing level at the `period` extreme offset by a weighted ATR multiple, with `up` / `down` direction flags. |
| [VIDYA](/docs/developers/stse/indicator-reference/trend.md#vidya) | Moving averages and trend | Variable Index Dynamic Average: an EMA whose smoothing is scaled by the absolute Chande momentum (or by a standard-deviation ratio when `useCMO` is false). |
| [VWAP](/docs/developers/stse/indicator-reference/trend.md#vwap) | Moving averages and trend | Volume-weighted average price, reset at each UTC day, week (Monday) or calendar month. |
| [VWMA](/docs/developers/stse/indicator-reference/trend.md#vwma) | Moving averages and trend | Volume-weighted moving average: `Σ(price × volume) / Σ(volume)` over the last `period` candles. |
| [WilliamsAlligator](/docs/developers/stse/indicator-reference/trend.md#williamsalligator) | Moving averages and trend | Williams Alligator: three Wilder averages of the median price (jaw, teeth, lips), each shifted forward by its offset. |
| [WMA](/docs/developers/stse/indicator-reference/trend.md#wma) | Moving averages and trend | Linearly weighted moving average: the newest value weighs `period`, the oldest weighs 1. |
| [AdvanceDeclineRatioBars](/docs/developers/stse/indicator-reference/momentum.md#advancedeclineratiobars) | Momentum and oscillators | Ratio of up (including doji) to down candles; an all-up window returns the up count. |
| [Aroon](/docs/developers/stse/indicator-reference/momentum.md#aroon) | Momentum and oscillators | Aroon on length + 1 candles; the newest equal high/low wins. |
| [AroonOscillator](/docs/developers/stse/indicator-reference/momentum.md#aroonoscillator) | Momentum and oscillators | Aroon Up minus Aroon Down. |
| [AverageDailyRange](/docs/developers/stse/indicator-reference/momentum.md#averagedailyrange) | Momentum and oscillators | Simple average of each chart candle range; the name does not imply a request for daily candles. |
| [AwesomeOscillator](/docs/developers/stse/indicator-reference/momentum.md#awesomeoscillator) | Momentum and oscillators | Five-bar minus 34-bar simple average of hl2. |
| [BalanceOfPower](/docs/developers/stse/indicator-reference/momentum.md#balanceofpower) | Momentum and oscillators | Candle body divided by its range; a zero range has no finite value. |
| [BBTrend](/docs/developers/stse/indicator-reference/momentum.md#bbtrend) | Momentum and oscillators | Difference of the short and long Bollinger lower-band distance and upper-band distance, divided by the short basis, in percent. |
| [BollingerPercentB](/docs/developers/stse/indicator-reference/momentum.md#bollingerpercentb) | Momentum and oscillators | Position of the source within population-standard-deviation Bollinger Bands. |
| [BullBearPower](/docs/developers/stse/indicator-reference/momentum.md#bullbearpower) | Momentum and oscillators | Bull power plus bear power relative to the EMA of close. |
| [CCI](/docs/developers/stse/indicator-reference/momentum.md#cci) | Momentum and oscillators | Commodity channel index: `(typical price − its mean) / (0.015 × mean deviation)` over `period`. |
| [ChandeMomentumOscillator](/docs/developers/stse/indicator-reference/momentum.md#chandemomentumoscillator) | Momentum and oscillators | Chande momentum oscillator: `100 × (sum of gains − sum of losses) / (sum of gains + sum of losses)` over `length`, between −100 and 100. |
| [ChopZone](/docs/developers/stse/indicator-reference/momentum.md#chopzone) | Momentum and oscillators | One column per candle; zone carries the rounded EMA-angle color classification. |
| [ConnorsRSI](/docs/developers/stse/indicator-reference/momentum.md#connorsrsi) | Momentum and oscillators | Mean of price RSI, consecutive-close streak RSI and the rank of the one-bar return against earlier returns. |
| [CoppockCurve](/docs/developers/stse/indicator-reference/momentum.md#coppockcurve) | Momentum and oscillators | Weighted average of the sum of the long and short close returns. |
| [Divergences](/docs/developers/stse/indicator-reference/momentum.md#divergences) | Momentum and oscillators | Finds regular and hidden divergences between price swings and a target series (an oscillator), using `Swings` pivots. |
| [DMI](/docs/developers/stse/indicator-reference/momentum.md#dmi) | Momentum and oscillators | Independent TradingView DMI: strict directional dominance, fixnan DIs and a zero-DX denominator of one. |
| [DPO](/docs/developers/stse/indicator-reference/momentum.md#dpo) | Momentum and oscillators | Detrended price oscillator: `close − SMA(period)` as it was `period / 2 + 1` candles ago. |
| [FisherTransform](/docs/developers/stse/indicator-reference/momentum.md#fishertransform) | Momentum and oscillators | Recursive Fisher transform of hl2, with its preceding value as the trigger. |
| [HARSI](/docs/developers/stse/indicator-reference/momentum.md#harsi) | Momentum and oscillators | JayRogers' Heikin Ashi RSI Oscillator: Heikin Ashi candles of zero-centred RSIs (`RSI − 50`, from −50 to +50; its levels are ±20 and ±30). |
| [HistoricalVolatility](/docs/developers/stse/indicator-reference/momentum.md#historicalvolatility) | Momentum and oscillators | Population standard deviation of log returns, annualized with TradingView's intraday/daily versus higher-timeframe rule. |
| [KDJ](/docs/developers/stse/indicator-reference/momentum.md#kdj) | Momentum and oscillators | KDJ oscillator: raw stochastic `dK`, then `K` and `D` smoothed with a 1/3 weight, and `J = 3K − 2D`. |
| [KnowSureThing](/docs/developers/stse/indicator-reference/momentum.md#knowsurething) | Momentum and oscillators | Weighted sum of four SMA-smoothed rates of change and its SMA signal. |
| [MACD](/docs/developers/stse/indicator-reference/momentum.md#macd) | Momentum and oscillators | Moving average convergence divergence: `macd = fast MA − slow MA`, `signal = MA(macd)`, `histogram = macd − signal`. |
| [MassIndex](/docs/developers/stse/indicator-reference/momentum.md#massindex) | Momentum and oscillators | Rolling sum of candle range EMA divided by its double EMA. |
| [Momentum](/docs/developers/stse/indicator-reference/momentum.md#momentum) | Momentum and oscillators | Momentum: `value − value length candles ago`. |
| [PriceOscillator](/docs/developers/stse/indicator-reference/momentum.md#priceoscillator) | Momentum and oscillators | Percentage price oscillator, configurable EMA/SMA signal and histogram. |
| [PringsSpecialK](/docs/developers/stse/indicator-reference/momentum.md#pringsspecialk) | Momentum and oscillators | Weighted sum of twelve smoothed returns, followed by two SMA signal stages. |
| [RankCorrelationIndex](/docs/developers/stse/indicator-reference/momentum.md#rankcorrelationindex) | Momentum and oscillators | Spearman price/time rank correlation, optional smoothing and population Bollinger Bands. |
| [RCIRibbon](/docs/developers/stse/indicator-reference/momentum.md#rciribbon) | Momentum and oscillators | Three Spearman rank correlations of price with time, using average ranks for ties. |
| [ROC](/docs/developers/stse/indicator-reference/momentum.md#roc) | Momentum and oscillators | Rate of change, in percent: `100 × (value − value length ago) / value length ago`. |
| [RSI](/docs/developers/stse/indicator-reference/momentum.md#rsi) | Momentum and oscillators | Relative strength index (Wilder): `100 − 100 / (1 + average gain / average loss)` over `period`, between 0 and 100. |
| [RSILambda](/docs/developers/stse/indicator-reference/momentum.md#rsilambda) | Momentum and oscillators | A function `(price) => rsi` that returns the RSI the current candle would have if it closed at `price`. |
| [SMIErgodicIndicator](/docs/developers/stse/indicator-reference/momentum.md#smiergodicindicator) | Momentum and oscillators | Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change. |
| [SMIErgodicOscillator](/docs/developers/stse/indicator-reference/momentum.md#smiergodicoscillator) | Momentum and oscillators | Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change. |
| [SqueezeMomentum](/docs/developers/stse/indicator-reference/momentum.md#squeezemomentum) | Momentum and oscillators | Squeeze momentum (LazyBear): detects Bollinger bands inside Keltner channels (`squeezeState: 'on'`) and measures momentum with a linear regression of the price against its range midpoint. |
| [Stochastic](/docs/developers/stse/indicator-reference/momentum.md#stochastic) | Momentum and oscillators | Stochastic oscillator: `stochastic = 100 × (close − lowest low) / (highest high − lowest low)` over `extremePeriod`, `k` = its `kPeriod` SMA, `d` = the `dPeriod` SMA of `k`. |
| [StochasticMomentumIndex](/docs/developers/stse/indicator-reference/momentum.md#stochasticmomentumindex) | Momentum and oscillators | Double-EMA-smoothed position in the rolling high/low range and its EMA signal. |
| [TRIX](/docs/developers/stse/indicator-reference/momentum.md#trix) | Momentum and oscillators | TRIX: the one-candle change of a triple EMA of the log price, times 10,000. |
| [TrueStrengthIndex](/docs/developers/stse/indicator-reference/momentum.md#truestrengthindex) | Momentum and oscillators | Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change. |
| [UlcerIndex](/docs/developers/stse/indicator-reference/momentum.md#ulcerindex) | Momentum and oscillators | Root mean square of percentage drawdowns from the rolling maximum. |
| [UltimateOscillator](/docs/developers/stse/indicator-reference/momentum.md#ultimateoscillator) | Momentum and oscillators | Buying pressure relative to true range over three weighted periods. |
| [Vortex](/docs/developers/stse/indicator-reference/momentum.md#vortex) | Momentum and oscillators | Rolling cross-candle high/low distances divided by true range, including the first candle range. |
| [WilliamsPercentR](/docs/developers/stse/indicator-reference/momentum.md#williamspercentr) | Momentum and oscillators | Williams %R: `100 × (close − highest high) / (highest high − lowest low)` over `length`, between −100 and 0. |
| [WoodiesCCI](/docs/developers/stse/indicator-reference/momentum.md#woodiescci) | Momentum and oscillators | Two CCIs of close, with the preceding five-bar sign rule for histogram colors. |
| [BBW](/docs/developers/stse/indicator-reference/volatility.md#bbw) | Volatility, bands and ranges | Bollinger BandWidth as TradingView plots it: the distance between the bands in percent of their basis, `(upper − lower) / basis × 100` (4.5 for bands 4.5% apart). |
| [BBWP](/docs/developers/stse/indicator-reference/volatility.md#bbwp) | Volatility, bands and ranges | Bollinger BandWidth Percentile, The_Caretaker's: the share, in percent, of the previous `lookback` BBW values (fewer at first) lower than or equal to the current one. |
| [Bollinger](/docs/developers/stse/indicator-reference/volatility.md#bollinger) | Volatility, bands and ranges | Bollinger bands: the `maPeriod` SMA (`mean`) plus and minus `stdDevMultiplier` standard deviations. |
| [Chop](/docs/developers/stse/indicator-reference/volatility.md#chop) | Volatility, bands and ranges | Choppiness index: `100 × log10(Σ TR / (highest high − lowest low)) / log10(period)`; high values mean a sideways market. |
| [Choppy](/docs/developers/stse/indicator-reference/volatility.md#choppy) | Volatility, bands and ranges | Counts monotonic runs among the last six closes: `type` rates the current run (`'none'`, `'weak'`, `'medium'`, `'strong'`) and `line` counts trending candles among the last ten. |
| [DC](/docs/developers/stse/indicator-reference/volatility.md#dc) | Volatility, bands and ranges | Donchian channel MIDLINE: `(highest high + lowest low) / 2` over `extremePeriod` candles, current included. |
| [Envelope](/docs/developers/stse/indicator-reference/volatility.md#envelope) | Volatility, bands and ranges | Percentage bands around an SMA or EMA basis. |
| [FindMarketBottom](/docs/developers/stse/indicator-reference/volatility.md#findmarketbottom) | Volatility, bands and ranges | Williams VIX Fix bottom finder: `wvf = 100 × (highest close over 22 − low) / highest close`, with Bollinger and percentile ranges; `alert` is true when `wvf` spikes above them. |
| [KeltnerChannels](/docs/developers/stse/indicator-reference/volatility.md#keltnerchannels) | Volatility, bands and ranges | Keltner channels: an EMA of the close (`middle`) plus and minus `multiplier` ATRs. |
| [Median](/docs/developers/stse/indicator-reference/volatility.md#median) | Volatility, bands and ranges | Nearest-rank median with ATR bands and an EMA of the median. |
| [TR](/docs/developers/stse/indicator-reference/volatility.md#tr) | Volatility, bands and ranges | True range: `max(high − low, \|high − previous close\|, \|low − previous close\|)`. |
| [ChaikinMoneyFlow](/docs/developers/stse/indicator-reference/volume.md#chaikinmoneyflow) | Volume | Chaikin money flow: `Σ(money-flow volume) / Σ(volume)` over `length`, between −1 and 1. |
| [ChaikinOscillator](/docs/developers/stse/indicator-reference/volume.md#chaikinoscillator) | Volume | Fast EMA minus slow EMA of cumulative accumulation/distribution. |
| [EaseOfMovement](/docs/developers/stse/indicator-reference/volume.md#easeofmovement) | Volume | SMA of midpoint change multiplied by the candle range and a divisor, divided by volume. |
| [EldersForceIndex](/docs/developers/stse/indicator-reference/volume.md#eldersforceindex) | Volume | EMA of close-to-close price change multiplied by volume. |
| [KlingerOscillator](/docs/developers/stse/indicator-reference/volume.md#klingeroscillator) | Volume | EMA 34 minus EMA 55 of signed volume, with an EMA 13 signal. |
| [MFI](/docs/developers/stse/indicator-reference/volume.md#mfi) | Volume | Money flow index: a volume-weighted RSI of the typical price, between 0 and 100. |
| [OnBalanceVolume](/docs/developers/stse/indicator-reference/volume.md#onbalancevolume) | Volume | On-balance volume: a running total that adds the candle volume when the price rises and subtracts it when it falls. |
| [PercentageVolumeOscillator](/docs/developers/stse/indicator-reference/volume.md#percentagevolumeoscillator) | Volume | Percentage difference between fast and slow volume averages, with a signal average and a histogram. |
| [PMAR](/docs/developers/stse/indicator-reference/volume.md#pmar) | Volume | Price-to-moving-average ratio: `price / VWMA(price, volume, period)`. |
| [PMARP](/docs/developers/stse/indicator-reference/volume.md#pmarp) | Volume | PMAR percentile, The_Caretaker's: the share, in percent, of the previous `lookback` PMAR values (fewer at first) not above the current one. |
| [VolumeProfile](/docs/developers/stse/indicator-reference/volume.md#volumeprofile) | Volume | Volume profile over the last `numberOfBars` candles: volume per price row, point of control and value area. |
| [BollingerBars](/docs/developers/stse/indicator-reference/structure.md#bollingerbars) | Market structure and patterns | The original candle OHLC used by Bollinger Bars: a full-width blue high-to-low rectangle under a green or red body, over the price candles. |
| [BosNChoch](/docs/developers/stse/indicator-reference/structure.md#bosnchoch) | Market structure and patterns | Trend state machine that tracks the last break of structure and the change-of-character level. |
| [Engulfing](/docs/developers/stse/indicator-reference/structure.md#engulfing) | Market structure and patterns | Two-candle engulfing pattern: `'bullish'` when an up candle's body engulfs the previous down candle's body, `'bearish'` for the mirror case, `'none'` otherwise. |
| [FVG](/docs/developers/stse/indicator-reference/structure.md#fvg) | Market structure and patterns | Fair value gap on the last three candles: bullish when the first candle's high is below the third candle's low, bearish for the mirror case. |
| [Heikinashi](/docs/developers/stse/indicator-reference/structure.md#heikinashi) | Market structure and patterns | Heikin-Ashi candles: `close = (o + h + l + c) / 4`, `open = (previous HA open + previous HA close) / 2`, high/low extended to include them. |
| [HeikinashiCandles](/docs/developers/stse/indicator-reference/structure.md#heikinashicandles) | Market structure and patterns | The candles of a Heikin Ashi chart: each candle keeps its times and volume, with the Heikin-Ashi open, high, low and close. |
| [Lunar](/docs/developers/stse/indicator-reference/structure.md#lunar) | Market structure and patterns | Moon phase from a timestamp series: 0 new moon, 2 first quarter, 4 full moon, 6 last quarter (8 phases). |
| [MarketStructure](/docs/developers/stse/indicator-reference/structure.md#marketstructure) | Market structure and patterns | Market structure (swing highs/lows labelled HH, LH, HL, LL) with break of structure (BOS) and change of character (CHoCH) events. |
| [OrderBlocks](/docs/developers/stse/indicator-reference/structure.md#orderblocks) | Market structure and patterns | The most recent bullish and bearish order blocks (a down candle followed by an up candle closing above its high, and the mirror case), with 25/50/75% lines. |
| [PivotPoints](/docs/developers/stse/indicator-reference/structure.md#pivotpoints) | Market structure and patterns | Classic floor pivot levels (P, R1 to R5, S1 to S5) computed from the previous day, week or month. |
| [PivotsHighLow](/docs/developers/stse/indicator-reference/structure.md#pivotshighlow) | Market structure and patterns | Independent high and low pivots with their original candle timestamps, emitted only when the right-hand confirmation candles have arrived. |
| [Renko](/docs/developers/stse/indicator-reference/structure.md#renko) | Market structure and patterns | Renko bricks from a price series: a new brick forms when price travels `brickSize` in the trend direction; a reversal needs `2 × brickSize`. |
| [RenkoAtr](/docs/developers/stse/indicator-reference/structure.md#renkoatr) | Market structure and patterns | Renko bricks whose size is the `period` ATR (Wilder), either live or frozen at its first value (`anchored`). |
| [StrongCandleStructure](/docs/developers/stse/indicator-reference/structure.md#strongcandlestructure) | Market structure and patterns | Market structure by the Strong Candle method: valid highs and lows, break of structure (BOS) and change of character (CHoCH) on candle closes, and the demand or supply zone of each break. |
| [SupportResistance](/docs/developers/stse/indicator-reference/structure.md#supportresistance) | Market structure and patterns | Support and resistance levels from pivot lows/highs, with break detection confirmed by a volume oscillator. |
| [Swings](/docs/developers/stse/indicator-reference/structure.md#swings) | Market structure and patterns | Swing highs and lows: a candle is a top (bottom) when it is the highest (lowest) of the `2 × period + 1` candles centred on it. |
| [WilliamsFractal](/docs/developers/stse/indicator-reference/structure.md#williamsfractal) | Market structure and patterns | Williams fractals: an up fractal is a high with `period` lower highs on each side, a down fractal a low with `period` higher lows. |
| [ZigZag](/docs/developers/stse/indicator-reference/structure.md#zigzag) | Market structure and patterns | TradingView's Zig Zag: alternating swing highs and lows. |

# Building blocks

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

The classes you combine to feed and shape other indicators: field selection, custom formulas, look-back buffers, crossings and rolling extremes. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## CandleIndicator

The candle series returned by `manager.getIntervalData(symbol, interval)`. You never construct it yourself.

```ts signature
new CandleIndicator(getPendingCandle: () => Candle | undefined, resolveAssetClass: () => string | undefined = () => undefined, interval?: Interval)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `getPendingCandle` | `() => Candle \| undefined` | required | Internal. Do not construct this class; call `manager.getIntervalData` instead. |
| `resolveAssetClass` | `() => string \| undefined` | `() => undefined` | Internal: the engine gives the class of the asset, which `getAssetClass()` returns. |
| `interval` | `Interval` | optional | Internal: the engine gives the interval passed to `getIntervalData`, which `getInterval()` returns. |

- **Input:** None: the engine creates and feeds it.
- **Value** (`getValue()` after `hasValue()`): `Candle`
  - `openTime: number`
  - `closeTime: number`
  - `open: number`
  - `high: number`
  - `low: number`
  - `close: number`
  - `volume: number`
- `Interval` is `number | 'week' | 'month'`.
- **Sub-indicators:** `.openTime`, `.closeTime`, `.open`, `.high`, `.low`, `.close`, `.volume`, `.hl2`, `.hlc3`, `.ohlc4`, `.hlcc4`, usable as inputs to other indicators.
- **Methods:** `getPending(): Candle | undefined`, `getAssetClass(): string | undefined`, `getInterval(): Interval | undefined`.
- **Warm-up:** Ready from the first closed candle. `getValue()` is the last CLOSED candle; `getPending()` is the candle still forming (`undefined` before the first tick of a new candle).
- Its sub-indicators (`candles.close`, `candles.hl2`, …) are ready-made number streams: `new EMA(candles.close, 20)` is the same as `new EMA(new PartialIndicator(candles, 'close'), 20)`.
- A candle is `{ openTime, closeTime, open, high, low, close, volume }`; times are epoch milliseconds.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);

manager.onIntervalCandle(symbol, 60, () => {
  if (!candles.hasValue()) return;
  const candle = candles.getValue(); // the candle that just closed
  const forming = candles.getPending(); // the candle in progress, or undefined
  manager.debugLog('closed', candle.close, 'range', candle.high - candle.low, 'forming', forming?.close);
});
```

## Crossover

Detects the candle on which one series crosses another. Emits `'firstUp'` when `source1` crosses above `source2`, `'secondUp'` when `source2` crosses above `source1`, and `null` on every other candle.

```ts signature
new Crossover(source1: Indicator<number | undefined>, source2: Indicator<number | undefined>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `source1` | `Indicator<number \| undefined>` | required | The first series. |
| `source2` | `Indicator<number \| undefined>` | required | The second series. |

- **Input:** Two number streams (for example two moving averages). Fields of a composite indicator work through `PartialIndicator`: `new Crossover(new PartialIndicator(macd, 'macd'), new PartialIndicator(macd, 'signal'))`. To cross a fixed level, feed a constant series such as `new LambdaIndicator(candles, () => 30)`.
- **Value** (`getValue()` after `hasValue()`): `"firstUp" | "secondUp" | null`
- **Warm-up:** Emits `null` until both inputs have values and each side has been seen at least once; the first cross can only be reported after that. The value is a string or `null`, never a number: compare it with `'firstUp'` / `'secondUp'`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Crossover(new EMA(candles.close, 20), new EMA(candles.close, 50));

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === 'firstUp') manager.debugLog('EMA20 crossed above EMA50');
  if (value === 'secondUp') manager.debugLog('EMA20 crossed below EMA50');
});
```

## DerivedIndicator

Computes a value from several indicators each time all of them have updated. Use it to combine indicators into one series that other indicators can consume.

```ts signature
new DerivedIndicator<T>(_fn: () => T, _dependencies: Indicator<unknown>[])
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `_fn` | `() => T` | required | A function computing the value from the dependencies' current values (read them with `getValue()` inside). |
| `_dependencies` | `Indicator<unknown>[]` | required | The indicators it depends on. It updates once all of them have updated for the candle. |

- **Input:** A list of indicators it depends on.
- **Value** (`getValue()` after `hasValue()`): `T`
- **Warm-up:** Updates on the first candle all dependencies have seen; its value is whatever `_fn` returns (return `undefined` while an input is warming up).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const fast = new EMA(candles.close, 12);
const slow = new EMA(candles.close, 26);
const spread = new DerivedIndicator(() => {
  const f = fast.getValue();
  const s = slow.getValue();
  return f === undefined || s === undefined ? undefined : f - s;
}, [fast, slow]);
const smoothed = new SMA(spread, 9); // a derived series feeds other indicators

manager.onIntervalCandle(symbol, 60, () => {
  if (!smoothed.hasValue()) return;
  const value = smoothed.getValue();
  if (value === undefined) return;
  manager.debugLog('smoothed spread', value);
});
```

## Extreme

The highest and lowest value over the last `period` updates, the current one included. On candles it uses `high` and `low`; on numbers, the numbers themselves.

```ts signature
new Extreme<T extends Candle | number | undefined = Candle>(sourceIndicator: Indicator<T>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<T>` | required | Candles or a number stream. |
| `period` | `number` | required | The window length, current candle included. |

- **Input:** The candle series, or a number stream.
- **Value** (`getValue()` after `hasValue()`): `ExtremeValue<T>`
- `ExtremeValue` is `T extends undefined ? { max: number | undefined; min: number | undefined } : { max: number; min: number }`.
- **Sub-indicators:** `.max`, `.min`, usable as inputs to other indicators.
- **Warm-up:** Ready from the first candle; the window is partial until `period` candles have passed. On a number stream that can be `undefined`, `max` and `min` are `number | undefined`.
- The window includes the current candle, so `close > max` can never be true. For a breakout against the previous N candles, use `History` and exclude the last element, or compare with the previous candle's `Extreme` through `new History(extreme, 2)`.
- `.max` and `.min` are also exposed as sub-indicators.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Extreme(candles, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('20-candle high', value.max, 'low', value.min);
});
```

## History

Keeps the last `period` values of another indicator as an array, oldest first and newest last.

```ts signature
new History<T>(source: Indicator<T>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `source` | `Indicator<T>` | required | The indicator to record. Pass the whole indicator (`candles`, `rsi`), not a property of its value. |
| `period` | `number` | required | How many values to keep. Must be a finite number. |

- **Input:** Any indicator (candles, numbers, objects).
- **Value** (`getValue()` after `hasValue()`): `readonly T[]`
- **Warm-up:** `hasValue()` is true from the first candle, but the array only reaches `period` elements after `period` candles. Check `value.length` before indexing.
- The array is `readonly`; type helper parameters as `readonly Candle[]` or `readonly number[]`.
- For a number stream, wrap first: `new History(candles.close, 5)` or `new History(new PartialIndicator(candles, 'close'), 5)`. Elements of a `History` over an `Indicator<number | undefined>` can be `undefined`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new History(candles, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.length < 20) return; // shorter while filling up
  const previous = value.slice(0, -1); // exclude the current candle
  const highest = Math.max(...previous.map((c) => c.high));
  const last = value[value.length - 1];
  if (last === undefined) return;
  manager.debugLog('breakout', last.close > highest);
});
```

## Index

The zero-based update count of another indicator (0 on its first value).

```ts signature
new Index(anyIndicator: Indicator<unknown>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `anyIndicator` | `Indicator<unknown>` | required | The indicator whose update count to expose. |

- **Input:** Any indicator.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** Same as its source.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Index(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('candles seen', value + 1);
});
```

## IteratorIndicator

Wraps an iterator that yields one value per update of its dependencies. This is how most built-in indicators are written; prefer `DerivedIndicator` for your own formulas.

```ts signature
new IteratorIndicator<T>(iterator: Iterator<T>, dependencies: Indicator<unknown>[])
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `iterator` | `Iterator<T>` | required | An iterator (for example a generator object) whose `next()` is called once per update. |
| `dependencies` | `Indicator<unknown>[]` | required | The indicators it depends on. |

- **Input:** A list of indicators it depends on.
- **Value** (`getValue()` after `hasValue()`): `T`
- **Warm-up:** Updates on the first candle all dependencies have seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
function* countUpCloses(): Generator<number> {
  let count = 0;
  while (true) {
    const candle = candles.getValue();
    if (candle !== undefined && candle.close > candle.open) count++;
    yield count;
  }
}
const upCloses = new IteratorIndicator(countUpCloses(), [candles]);

manager.onIntervalCandle(symbol, 60, () => {
  if (!upCloses.hasValue()) return;
  manager.debugLog('up closes so far', upCloses.getValue());
});
```

## LambdaIndicator

Applies a pure function to each value of another indicator, without storing anything. Use it for a custom per-candle formula.

```ts signature
new LambdaIndicator<T, U>(_indicator: Indicator<T>, _lambda: (value: T) => U)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `_indicator` | `Indicator<T>` | required | The source indicator. |
| `_lambda` | `(value: T) => U` | required | A pure function `(value) => result`, called when the value is read. It must not call `manager` or keep state. |

- **Input:** Any indicator.
- **Value** (`getValue()` after `hasValue()`): `U`
- **Warm-up:** Same as its source.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new LambdaIndicator(candles, (candle) => candle.close - candle.open);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('body', value);
});
```

## ManualIndicator

An indicator you feed yourself with `setValue(value)`. Useful to run a built-in indicator over values you compute in a callback.

```ts signature
new ManualIndicator<T>()
```

- **Input:** Values you push with `setValue`.
- **Value** (`getValue()` after `hasValue()`): `T`
- **Methods:** `setValue(value: T): void`.
- **Warm-up:** Has a value after the first `setValue` call.
- Construct it at the root like any indicator; calling `setValue` inside a callback is allowed and is how it is fed. Feed it exactly once per update, or indicators built on it count too many samples.
- It only holds what your callbacks fed it: the start-up history replay runs no callbacks, so after a restart it starts empty. When the value can be computed from other indicators, prefer a `DerivedIndicator`, which is rebuilt from history like any indicator.

```ts
const symbol = 'BTC';
const other = 'ETH';
const btc = manager.getIntervalData(symbol, 60);
const eth = manager.getIntervalData(other, 60);
const ratio = new ManualIndicator<number>();
const ratioAverage = new SMA(ratio, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!btc.hasValue() || !eth.hasValue()) return;
  ratio.setValue(btc.getValue().close / eth.getValue().close); // feed once per candle
  if (!ratioAverage.hasValue()) return;
  const value = ratioAverage.getValue();
  if (value === undefined) return;
  manager.debugLog('BTC/ETH ratio average', value);
});
```

## PartialIndicator

Selects one field of an indicator's object value, producing a new indicator. The standard way to turn a candle series into a number stream.

```ts signature
new PartialIndicator<T, K extends keyof T>(indicator: Indicator<T>, key: K)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `indicator` | `Indicator<T>` | required | The source indicator. |
| `key` | `K` | required | The field to read, checked against the source value type (`'close'`, `'high'`, `'volume'`, …). |

- **Input:** Any indicator whose value is an object, typically the candle series.
- **Value** (`getValue()` after `hasValue()`): `T[K]`
- **Warm-up:** Same as its source (the first closed candle for a candle series).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PartialIndicator(candles, 'close');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('close', value);
});
```

## Percentile

The percentile rank (0 to 100) of the current value among the last `period` values: the share of those values strictly below it.

```ts signature
new Percentile(sourceIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to rank. |
| `period` | `number` | required | How many past values to rank against, current included. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` defined values have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Percentile(candles.close, 100);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('close percentile', value);
});
```

## PineIndicator

Runs a TradingView Pine Script indicator or strategy as-is, with TradingView's series semantics: on each candle it gives what the script shows on a chart of the same candles (plots, markers, alertconditions, labels, boxes, lines, tables and strategy orders).

```ts signature
new PineIndicator(candles: ICandleIndicator, source: string, options: PineIndicatorOptions = {}, libraries: Readonly<Record<string, string>> = {})
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candles` | `ICandleIndicator` | required | The candle series the script runs on. |
| `source` | `string` | required | The Pine source, any version from 1 to 6 (`//@version=N`). |
| `options` | `PineIndicatorOptions` | `{}` | `inputs`: overrides by input title (`"Title #2"` for a repeated title). `heikinAshi`: run on Heikin Ashi candles, as on a Heikin Ashi chart. `ticker` and `mintick`: the script's `syminfo`. `timeframe`: read from the candles when omitted. `warmupBars`: candles kept for a first read after a long warm-up (5,000 by default, at most 50,000). `visibleBars`: for a script that reads the chart's visible range, how many candles ending at the current one the chart shows (the number the user gives, else 150). Such a script runs from scratch on every read, as TradingView recomputes it whenever the range moves, over its last 2,000 candles (twice `visibleBars` when more, within the 5,000 kept; `warmupBars` when set), about 9 ms a read and paid on every candle of a backtest; its zones are rebuilt each time, so watch the close entering one rather than remembering a zone. `lowerTimeframes`: for a script that reads intrabars (`request.security_lower_tf`, or `request.security` on a lower timeframe), candles of the same symbol on each lower timeframe it reads, with its Pine timeframe: `[{ timeframe: '1', candles: manager.getIntervalData(symbol, 1) }]`, got before the candles the script runs on so that, on a tick closing both, the lower ones are in first. The last 50,000 candles are kept, split between the timeframes; without them those values are `na`. |
| `libraries` | `Readonly<Record<string, string>>` | `{}` | The sources of the Pine libraries the script imports, by import path (`user/name/version`): only the ones it calls into are needed. A Pine import on the Smart Chart passes the libraries it keeps. |

- **Input:** The candle series. The script runs on its interval.
- **Value** (`getValue()` after `hasValue()`): `number`
- `PineIndicatorOptions` is `{ plotTransparency?: "source" | "legacy" | undefined; namedColors?: "source" | "uniform-v6" | undefined; inputs?: Record<string, PineInputValue> | undefined; timeframe?: string | undefined; heikinAshi?: boolean | undefined; ticker?: string | undefined; mintick?: number | undefined; warmupBars?: number | undefined; visibleBars?: number | undefined; lowerTimeframes?: { timeframe: string; candles: ICandleIndicator; }[] | undefined }`.
- `PineInputValue` is `string | number | boolean`.
- **Methods:** `output(): PineIndicatorOutput | undefined`, `alert(title: string): boolean`, `marker(title: string): number | undefined`, `plot(title: string): number | undefined`, `plotColor(title: string): string | undefined`, `plotDrawn(title: string): number | undefined`, `labels(): string[]`, `label(text: string): boolean`, `boxes(): PineIndicatorBox[]`, `lines(): PineIndicatorLine[]`, `tables(): PineIndicatorTable[]`, `orders(): PineIndicatorOrder[]`, `fills(): PineIndicatorFill[]`, `position(): number`, `nextPosition(): number`.
- **Warm-up:** Each read runs the script on the candles fed since the previous read. `plot(title)` is `undefined` while the script plots `na`.
- Read on the current candle: `alert(title)` (an `alertcondition` held), `marker(title)` (the value of a `plotshape`, `plotchar` or `plotarrow`, `undefined` when it shows nothing), `plot(title)` (the value computed on the candle), `plotColor(title)` (the plot's color, `#RRGGBBAA` or `undefined`: some scripts show a state by a color only, such as Squeeze Momentum's squeeze, a dot plotted at 0), `plotDrawn(title)` (the value the chart draws under the candle: a plot with an `offset` shows there what it computed that many candles earlier, such as Ichimoku's cloud 25 candles ahead, and `undefined` when drawn back), `label(text)` and `labels()`, and for a strategy `nextPosition()` (its signed position once the market orders it decided on the candle fill at the next open, every fill of the candle included: follow its direction to trade like the strategy), `position()` (before those orders fill), `fills()` (the orders it filled on the candle) and `orders()` (the orders still working after the candle, its `strategy.exit` brackets as `kind: "exit"`).
- `boxes()` and `lines()` give the zones and levels the script shows on the current candle, oldest first. A box is `{ id, active, left, right, top, bottom, extend, text, color, borderColor }` and a line `{ id, time1, price1, time2, price2, extend, color, style, width, price }`: times are candle open times in ms (an edge past the current candle is projected at the candle interval), a box's `top` is above its `bottom`, a line's `price` is its level on the current candle (`null` past its ends), colors are `#RRGGBBAA` or `null`, a box is `active` while it spans the current candle (a zone that ended stays drawn), and `id` names the drawing: a script may move a drawing to a newer zone (Order Block Detector keeps a few boxes per side), so remember a zone by its `left`, `top` and `bottom`, and a level by its `time1` and `price1`. Until the first `boxes()` or `lines()` call, the script skips the work only drawings show.
- `tables()` gives the tables the script shows on the current candle, one per position: its dashboards as `{ id, position, columns, rows, cells }`, a cell being `{ column, row, text, textColor, bgColor }`, row by row (a merged cell is listed once, at its top-left). Many dashboards tell states apart by color only. Until the first `tables()` call, the script skips the work only its tables show.
- `namedColors: "source"` follows each Pine module's RGB palette. Omitted or `"uniform-v6"` retains the uniform v6 named RGB of existing agents. Independently, `plotTransparency: "source"` follows the source version's plot/marker alpha; omitted or `"legacy"` retains historical alpha. `plotTransparency` also picks the values: `"source"` reads TradingView's (a `plotchar()` marker is its series value, e.g. `-2`, not `1`; `ta.highestbars`/`ta.lowestbars` keep the newest equal extreme, so an equal high gives `0`; `format.volume` text is a whole number below 1000), omitted or `"legacy"` those of agents built before 2026-10-21 (marker `1`, oldest equal extreme, up to three decimals). Imported classes fix both saved policies across agent edits. Named RGB does not determine rendered alpha: compare a state with the complete sampled RGBA in the import's description, accounting for the source's plotting transparency and any overridden color input.
- A Pine script imported on the Smart Chart is a custom indicator: `new <ClassName>(candles, { inputs })` runs it without pasting its source.
- In agents, `request.security` on a lower timeframe of the symbol reads the `lowerTimeframes` candles given (`na` without them), on another symbol it gives `na`, and on a higher timeframe with `lookahead_on` it reads the last completed bar.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const pine = new PineIndicator(candles, `//@version=6
indicator("Cross", overlay = true)
fast = ta.ema(close, 9)
slow = ta.ema(close, 21)
plot(fast, "fast")
alertcondition(ta.crossover(fast, slow), "Buy", "fast crossed above slow")`);

manager.onIntervalCandle(symbol, 60, () => {
  if (pine.alert('Buy')) manager.debugLog('Buy signal, fast EMA at', pine.plot('fast'));
});
```

# Moving averages and trend

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Moving averages, adaptive averages and trend-following overlays. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## ADX

Average directional index with the +DI and −DI lines (Wilder smoothing), like TradingView's DMI: DI length `period`, ADX smoothing `adxSmoothing`.

```ts signature
new ADX(candleIndicator: Indicator<Candle>, period: number, adxSmoothing: number = period)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | required | DI length (14 classically). |
| `adxSmoothing` | `number` | `period` | ADX smoothing length (`period` by default). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `ADXValue`
  - `posDI: number | undefined`
  - `negDI: number | undefined`
  - `adx: number | undefined`
- **Warm-up:** `posDI` / `negDI` from candle `period + 1`; `adx` from candle `period + adxSmoothing`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ADX(candles, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.adx === undefined || value.posDI === undefined || value.negDI === undefined) return;
  manager.debugLog('ADX', value.adx, '+DI', value.posDI, '-DI', value.negDI);
});
```

## ALMA

Gaussian-weighted moving average of closes.

```ts signature
new ALMA(candleIndicator: Indicator<Candle>, length: number = 9, offset: number = 0.85, sigma: number = 6)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `length` | `number` | `9` | Gaussian window length, 1 to 5000. |
| `offset` | `number` | `0.85` | Gaussian centre as a fraction of length - 1; not a plot displacement. |
| `sigma` | `number` | `6` | Gaussian width divisor. Zero gives equal weights; either sign gives the same weights. |

- **Input:** Candle stream; the built-in fixes its source to close.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value at bar length - 1. Exact finite-window restart once length candles have been supplied; chart preloads 30 periods.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ALMA(candles, 9, 0.85, 6);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('value', value);
});
```

## BeepBoop

Trend label from a MACD(12, 26, 9) histogram sign and the candle position relative to a 50-period EMA: `'up'` when the histogram is positive and the whole candle is above the EMA, `'down'` for the mirror case, `'neutral'` otherwise.

```ts signature
new BeepBoop(candleIndicator: ICandleIndicator, maType: 'ema' | 'sma')
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `maType` | `'ema' \| 'sma'` | required | Averages used by the MACD: `'ema'` or `'sma'`. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `BeepBoopValue | undefined`
  - `trend: "up" | "down" | "neutral"`
- **Warm-up:** `undefined` until the MACD histogram and the EMA50 are both ready (about 50 candles).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BeepBoop(candles, 'ema');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('trend', value.trend);
});
```

## ChandeKrollStop

Rolling extrema of ATR-adjusted high and low stops.

```ts signature
new ChandeKrollStop(candleIndicator: Indicator<Candle>, atrLength: number = 10, atrCoefficient: number = 1, stopLength: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `atrLength` | `number` | `10` | ATR and first high/low lookback, 1 to 5000. |
| `atrCoefficient` | `number` | `1` | Positive integer ATR distance coefficient; not a length. |
| `stopLength` | `number` | `9` | Second high/low extrema lookback, 1 to 5000. |

- **Input:** Candle stream.
- **Value** (`getValue()` after `hasValue()`): `ChandeKrollStopValue`
  - `long: number | undefined`
  - `short: number | undefined`
- **Warm-up:** Default first value is bar 9, since outer extrema use available non-missing observations once enough chart bars exist. Restart convergence measured up to 1797 bars at length 100 across three markets. Chart preloads max(200, 30 * largest length); coefficient is excluded.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChandeKrollStop(candles, 10, 1, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.long === undefined || value.short === undefined) return;
  manager.debugLog('Kroll', value.long, value.short);
});
```

## ChandelierExit

Highest high minus ATR distance and lowest low plus ATR distance, as in the TradingView ta library.

```ts signature
new ChandelierExit(candleIndicator: Indicator<Candle>, length: number = 22, atrLength: number = 22, atrMultiplier: number = 3)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `length` | `number` | `22` | High/low extrema lookback, 1 to 5000. |
| `atrLength` | `number` | `22` | SMA-seeded RMA true-range length, 1 to 5000. |
| `atrMultiplier` | `number` | `3` | Non-negative ATR distance multiplier. |

- **Input:** Candle stream.
- **Value** (`getValue()` after `hasValue()`): `ChandelierExitValue`
  - `long: number | undefined`
  - `short: number | undefined`
- **Warm-up:** First value at bar max(length, atrLength) - 1. Restart convergence measured up to 1717 bars at length 100 across three markets. Chart preloads max(200, 30 * largest length); multiplier is excluded.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChandelierExit(candles, 22, 22, 3);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.long === undefined || value.short === undefined) return;
  manager.debugLog('chandelier', value.long, value.short);
});
```

## DEMA

Twice an EMA minus the EMA of that EMA.

```ts signature
new DEMA(sourceIndicator: Indicator<number | undefined>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | Numeric input stream. |
| `length` | `number` | `9` | Length of both SMA-seeded EMAs, 1 to 5000. |

- **Input:** Numeric source stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value at bar 2 * (length - 1). Measured restart convergence at defaults/15/100 on BTC hourly/daily and ETH 4h: up to 1131 bars. Chart preloads max(200, 30 * length), capped at 5000 bars; at long lengths the first values can differ from a full-history run, or fall outside the chart window.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new DEMA(candles.close, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('value', value);
});
```

## EIT

Ehlers instantaneous trendline, computed with a Hilbert-transform estimate of the dominant cycle.

```ts signature
new EIT(sourceIndicator: Indicator<number>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream (usually `candles.hl2`). |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Needs a few dozen candles before the cycle estimate settles; treat the first ~50 values as unreliable.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new EIT(candles.hl2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('EIT', value);
});
```

## Elder

Elder impulse system: `'bullish'` when both the EMA and the MACD histogram rose since the previous candle, `'bearish'` when both fell, `'neutral'` otherwise.

```ts signature
new Elder(emaIndicator: EMA, macdIndicator: MACD)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `emaIndicator` | `EMA` | required | An `EMA` (13 periods classically). |
| `macdIndicator` | `MACD` | required | A `MACD` (12, 26, 9 classically). |

- **Input:** An `EMA` instance and a `MACD` instance built on the same series.
- **Value** (`getValue()` after `hasValue()`): `"neutral" | "bullish" | "bearish"`
- **Warm-up:** Returns `'neutral'` until both inputs have two consecutive values (about 35 candles with the classic settings).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Elder(new EMA(candles.close, 13), new MACD(candles.close, 'ema', 12, 26, 'ema', 9));

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('impulse', value);
});
```

## EMA

Exponential moving average, seeded with the simple average of the first `period` values, then `value = source × α + previous × (1 − α)` with `α = smoothing / (1 + period)`.

```ts signature
new EMA(sourceIndicator: Indicator<number | undefined>, period: number, smoothing: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `period` | `number` | required | The averaging length. |
| `smoothing` | `number` | `2` | The smoothing factor numerator (2 gives the standard EMA). |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` values have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new EMA(candles.close, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('EMA20', value);
});
```

## FIR

A 4-tap finite-impulse-response smoother: `(4·x0 + 3·x1 + 2·x2 + x3) / 10`.

```ts signature
new FIR(sourceIndicator: Indicator<number>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream to smooth. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** Returns a number from the first candle (partial weights for the first 3).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new FIR(candles.close);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('FIR', value);
});
```

## HalfTrend

HalfTrend: a trend line that follows the `amplitude`-candle high/low averages and flips on a break, with ATR channels.

```ts signature
new HalfTrend(candleIndicator: ICandleIndicator, amplitude: number, channelDeviation: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `amplitude` | `number` | required | Look-back for the high/low averages. |
| `channelDeviation` | `number` | required | ATR channel width multiplier (ATR is a fixed 100-period Wilder average). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `HalfTrendValue | undefined`
  - `halftrendValue: number`
  - `atrHigh: number`
  - `atrLow: number`
  - `trend: "up" | "down"`
- **Warm-up:** `undefined` until its 100-period ATR is ready: first value on candle 100.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new HalfTrend(candles, 2, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('trend', value.trend, 'line', value.halftrendValue);
});
```

## HullMovingAverage

Hull moving average: `WMA(2 × WMA(n/2) − WMA(n), √n)`, a low-lag average.

```ts signature
new HullMovingAverage(sourceIndicator: Indicator<number | undefined>, period: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `period` | `number` | `9` | The Hull length. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until candle `2 × period − 1` (17 for period 9).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new HullMovingAverage(candles.close, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('HMA9', value);
});
```

## Ichimoku

Ichimoku cloud: Tenkan and Kijun (midpoints of the `tPeriod` / `kPeriod` high-low range), Senkou spans A and B, and the lagging span.

```ts signature
new Ichimoku(candleIndicator: ICandleIndicator, tPeriod: number, kPeriod: number, ssbPeriod: number, laggingSpanPeriod: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `tPeriod` | `number` | required | Tenkan-sen length (9 classically). |
| `kPeriod` | `number` | required | Kijun-sen length (26). |
| `ssbPeriod` | `number` | required | Senkou span B length (52). |
| `laggingSpanPeriod` | `number` | required | Displacement of the spans and the lagging span (26). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `IchimokuOutput`
  - `tenkan: number`
  - `kijun: number`
  - `senkouSpanA: number | undefined`
  - `senkouSpanB: number | undefined`
  - `chikou?: number | undefined`
- **Warm-up:** `tenkan` and `kijun` from the first candle (partial windows); the Senkou spans are `undefined` until `laggingSpanPeriod` candles have passed. `chikou` is always `undefined` on the current candle.
- `senkouSpanA` and `senkouSpanB` are the cloud under the CURRENT candle (the spans computed `laggingSpanPeriod` candles ago), so "price above the cloud" is `close > Math.max(senkouSpanA, senkouSpanB)`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Ichimoku(candles, 9, 26, 52, 26);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.senkouSpanA === undefined || value.senkouSpanB === undefined) return;
  manager.debugLog('tenkan', value.tenkan, 'kijun', value.kijun, 'cloud', value.senkouSpanA, value.senkouSpanB);
});
```

## KAMA

Kaufman adaptive moving average: its smoothing speeds up in trends and slows down in noise, based on the efficiency ratio over `length` values.

```ts signature
new KAMA(sourceIndicator: Indicator<number | undefined>, length: number, fastAlpha: number = 2, slowAlpha: number = 30, useLog: boolean = false)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `length` | `number` | required | The efficiency-ratio lookback. |
| `fastAlpha` | `number` | `2` | Fast smoothing length (2 by default). |
| `slowAlpha` | `number` | `30` | Slow smoothing length (30 by default). |
| `useLog` | `boolean` | `false` | Average the logarithm of the source and return `exp` of the result. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `length + 1`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new KAMA(candles.close, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('KAMA10', value);
});
```

## LinearRegression

The value, at the current candle, of the least-squares line fitted to the last `period` values.

```ts signature
new LinearRegression(sourceIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to fit. |
| `period` | `number` | required | The fitting window. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` values have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new LinearRegression(candles.close, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('linreg20', value);
});
```

## LSMA

Least-squares regression of the current source window, evaluated at length - 1 - offset.

```ts signature
new LSMA(sourceIndicator: Indicator<number | undefined>, length: number = 25, offset: number = 0)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | Numeric input stream. |
| `length` | `number` | `25` | Regression window length, 1 to 5000. |
| `offset` | `number` | `0` | Signed integer regression evaluation offset, -5000 to 5000. It uses the same past window and never moves the output to another candle. |

- **Input:** Numeric source stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value at bar length - 1. Exact finite-window restart after length source bars. Missing values invalidate the window until they leave it. The chart excludes offset from its 30-period lookback.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new LSMA(candles.close, 25, 0);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('value', value);
});
```

## MovingAverageCross

Two close SMAs with a cross value only where either line crosses the other.

```ts signature
new MovingAverageCross(candleIndicator: Indicator<Candle>, shortLength: number = 9, longLength: number = 21)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `shortLength` | `number` | `9` | Short SMA length, 1 to 5000. |
| `longLength` | `number` | `21` | Long SMA length, 1 to 5000. |

- **Input:** Candle stream; the built-in fixes its source to close.
- **Value** (`getValue()` after `hasValue()`): `MovingAverageCrossValue`
  - `short: number | undefined`
  - `long: number | undefined`
  - `cross: number | undefined`
- **Warm-up:** Each SMA starts at its length - 1. A cross also needs their preceding values. Restart is exact after the longer length; measured up to 33 bars for the 5/34 variant. Chart preloads 30 periods.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MovingAverageCross(candles, 9, 21);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.short === undefined || value.long === undefined) return;
  manager.debugLog('MA cross', value.short, value.long, value.cross);
});
```

## MVWAP

Moving VWAP: the average of the last `period` completed VWAP windows (days, weeks or months).

```ts signature
new MVWAP(candleIndicator: ICandleIndicator, window: 'day' | 'week' | 'month', period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `window` | `'day' \| 'week' \| 'month'` | required | The VWAP window: `'day'`, `'week'` or `'month'`. |
| `period` | `number` | required | How many completed windows to average. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` windows have completed (5 days of candles for `'day', 5`).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MVWAP(candles, 'day', 5);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('MVWAP', value);
});
```

## RMA

Wilder's running moving average (the smoothing used by RSI and ATR): seeded with a simple average, then `value = (previous × (period − 1) + source) / period`.

```ts signature
new RMA(sourceIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to smooth. |
| `period` | `number` | required | The smoothing length. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` defined values have been seen. `new RMA(new TR(candles), 14)` is the standard 14-period ATR.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RMA(new TR(candles), 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('ATR14', value);
});
```

## SAR

Parabolic SAR: a trailing stop-and-reverse level that accelerates with the trend.

```ts signature
new SAR(candleIndicator: Indicator<Candle>, accfStart: number = 0.02, accfIncrement: number = 0.02, accfMax: number = 0.20)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `accfStart` | `number` | `0.02` | Initial acceleration factor. |
| `accfIncrement` | `number` | `0.02` | Acceleration increment at each new extreme. |
| `accfMax` | `number` | `0.20` | Maximum acceleration factor. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `SARValue`
  - `trend: "bullish" | "bearish"`
  - `value: number`
- **Warm-up:** Ready from the first candle; the first few values are an initialisation and should not drive trades.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SAR(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('SAR', value.value, value.trend);
});
```

## SMA

Simple moving average: the arithmetic mean of the last `period` values.

```ts signature
new SMA(sourceIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `period` | `number` | required | Number of values averaged. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` values have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SMA(candles.close, 50);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('SMA50', value);
});
```

## SuperTrend

SuperTrend: ATR bands around the median price (`hl2 ± multiplier × ATR`), flipping direction when the close crosses the active band.

```ts signature
new SuperTrend(candleIndicator: Indicator<Candle>, period: number, multiplier: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | required | ATR length (Wilder smoothing). |
| `multiplier` | `number` | required | Band width in ATRs. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `SuperTrendValue | undefined`
  - `close: number`
  - `bottomLine: number`
  - `topLine: number`
  - `up: boolean`
  - `down: boolean`
- **Warm-up:** `undefined` until the ATR has `period` values.
- `up` and `down` are the current direction. To act on a flip, compare with the previous value through `new History(supertrend, 2)`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SuperTrend(candles, 10, 3);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog(value.up ? 'bullish' : 'bearish', 'stop line', value.up ? value.bottomLine : value.topLine);
});
```

## TEMA

Three cascaded close EMAs combined as 3 * (EMA1 - EMA2) + EMA3.

```ts signature
new TEMA(candleIndicator: Indicator<Candle>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `length` | `number` | `9` | Length of all three SMA-seeded EMAs, 1 to 5000. |

- **Input:** Candle stream; the built-in fixes its source to close.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value at bar 3 * (length - 1). Measured restart convergence at defaults/15/100 on BTC hourly/daily and ETH 4h: up to 1245 bars. Chart preloads max(200, 30 * length), capped at 5000 bars; at long lengths the first value (bar 3 * (length - 1)) can fall outside the chart window.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new TEMA(candles, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('value', value);
});
```

## TTS

Trend Trader Strategy line: a trailing level at the `period` extreme offset by a weighted ATR multiple, with `up` / `down` direction flags.

```ts signature
new TTS(candleIndicator: Indicator<Candle>, period: number, multiplier: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | required | Look-back for the extreme and the weighted ATR. |
| `multiplier` | `number` | required | ATR multiple subtracted from / added to the extreme. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `TTSValue`
  - `high?: number | undefined`
  - `low?: number | undefined`
  - `value?: number | undefined`
  - `up: boolean`
  - `down: boolean`
  - `extreme?: number | undefined`
- **Warm-up:** `up` and `down` are booleans from the first candle; `value`, `high`, `low` and `extreme` are `undefined` until candle `period + 2`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new TTS(candles, 21, 3);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.value === undefined) return;
  manager.debugLog('TTS', value.value, value.up ? 'up' : value.down ? 'down' : 'flat');
});
```

## VIDYA

Variable Index Dynamic Average: an EMA whose smoothing is scaled by the absolute Chande momentum (or by a standard-deviation ratio when `useCMO` is false).

```ts signature
new VIDYA(sourceIndicator: Indicator<number | undefined>, options: VIDYAOptions = {})
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `options` | `VIDYAOptions` | `{}` | Optional `{ period?: number (9), fixedCMOLength?: boolean (true: CMO over 9 values), useCMO?: boolean (true) }`. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- `VIDYAOptions` is `{ period?: number | undefined; fixedCMOLength?: boolean | undefined; useCMO?: boolean | undefined }`.
- **Warm-up:** First value on candle 9 with the default `fixedCMOLength: true` (the CMO window is fixed at 9), or on candle `period` when `fixedCMOLength` is false.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new VIDYA(candles.close, { period: 14 });

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('VIDYA14', value);
});
```

## VWAP

Volume-weighted average price, reset at each UTC day, week (Monday) or calendar month.

```ts signature
new VWAP(candleIndicator: Indicator<Candle>, sourceIndicator: Indicator<number>, window: 'day' | 'week' | 'month')
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series (volume and time come from it). |
| `sourceIndicator` | `Indicator<number>` | required | The price averaged (typically `candles.hlc3`). |
| `window` | `'day' \| 'week' \| 'month'` | required | When the average resets: `'day'`, `'week'` or `'month'`. |

- **Input:** The candle series plus a price stream.
- **Value** (`getValue()` after `hasValue()`): `VWAPValue`
  - `window: number`
  - `vwap: number`
- **Warm-up:** Ready from the first candle; it restarts at each window boundary (UTC). `value.window` identifies the current window.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new VWAP(candles, candles.hlc3, 'day');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('VWAP', value.vwap);
});
```

## VWMA

Volume-weighted moving average: `Σ(price × volume) / Σ(volume)` over the last `period` candles.

```ts signature
new VWMA(priceIndicator: Indicator<number>, volumeIndicator: Indicator<number>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `priceIndicator` | `Indicator<number>` | required | The price stream (for example `candles.close`). |
| `volumeIndicator` | `Indicator<number>` | required | The volume stream (`candles.volume`). |
| `period` | `number` | required | The window length. |

- **Input:** Two number streams: price and volume.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** Returns a number from the first candle; it averages a partial window until `period` candles have passed, and is `NaN` if the window's volume is zero.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new VWMA(candles.close, candles.volume, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('VWMA20', value);
});
```

## WilliamsAlligator

Williams Alligator: three Wilder averages of the median price (jaw, teeth, lips), each shifted forward by its offset.

```ts signature
new WilliamsAlligator(candleIndicator: ICandleIndicator, jawPeriod: number = 13, jawOffset: number = 8, teethPeriod: number = 8, teethOffset: number = 5, lipsPeriod: number = 5, lipsOffset: number = 3)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `jawPeriod` | `number` | `13` | Jaw average length. |
| `jawOffset` | `number` | `8` | Jaw forward shift, in candles. |
| `teethPeriod` | `number` | `8` | Teeth average length. |
| `teethOffset` | `number` | `5` | Teeth forward shift. |
| `lipsPeriod` | `number` | `5` | Lips average length. |
| `lipsOffset` | `number` | `3` | Lips forward shift. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `AlligatorValue`
  - `jaw: number | undefined`
  - `teeth: number | undefined`
  - `lips: number | undefined`
- **Warm-up:** Each line is `undefined` until its average plus its offset have passed (jaw: 13 + 8 = 21 candles with the defaults).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new WilliamsAlligator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.jaw === undefined || value.teeth === undefined || value.lips === undefined) return;
  manager.debugLog('jaw', value.jaw, 'teeth', value.teeth, 'lips', value.lips);
});
```

## WMA

Linearly weighted moving average: the newest value weighs `period`, the oldest weighs 1.

```ts signature
new WMA(sourceIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream to average. |
| `period` | `number` | required | Number of values averaged. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` values have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new WMA(candles.close, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('WMA20', value);
});
```

# Momentum and oscillators

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Bounded and unbounded oscillators, momentum measures and divergence detection. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## AdvanceDeclineRatioBars

Ratio of up (including doji) to down candles; an all-up window returns the up count.

```ts signature
new AdvanceDeclineRatioBars(candleIndicator: Indicator<Candle>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `9` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 8. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 16 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new AdvanceDeclineRatioBars(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('AdvanceDeclineRatioBars', value);
});
```

## Aroon

Aroon on length + 1 candles; the newest equal high/low wins.

```ts signature
new Aroon(candleIndicator: Indicator<Candle>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `14` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `AroonValue`
  - `up: number | undefined`
  - `down: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars up: 14; down: 14. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 23 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Aroon(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('Aroon', value);
});
```

## AroonOscillator

Aroon Up minus Aroon Down.

```ts signature
new AroonOscillator(candleIndicator: Indicator<Candle>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `14` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 14. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 23 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new AroonOscillator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('AroonOscillator', value);
});
```

## AverageDailyRange

Simple average of each chart candle range; the name does not imply a request for daily candles.

```ts signature
new AverageDailyRange(candleIndicator: Indicator<Candle>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `14` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 13. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 22 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new AverageDailyRange(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('AverageDailyRange', value);
});
```

## AwesomeOscillator

Five-bar minus 34-bar simple average of hl2.

```ts signature
new AwesomeOscillator(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 33. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 33 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new AwesomeOscillator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('AwesomeOscillator', value);
});
```

## BalanceOfPower

Candle body divided by its range; a zero range has no finite value.

```ts signature
new BalanceOfPower(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 0. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 0 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BalanceOfPower(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('BalanceOfPower', value);
});
```

## BBTrend

Difference of the short and long Bollinger lower-band distance and upper-band distance, divided by the short basis, in percent.

```ts signature
new BBTrend(candleIndicator: Indicator<Candle>, shortLength: number = 20, longLength: number = 50, multiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream. |
| `shortLength` | `number` | `20` | Short Bollinger length, 1 to 5000. |
| `longLength` | `number` | `50` | Long Bollinger length, 1 to 5000. |
| `multiplier` | `number` | `2` | Population-standard-deviation multiplier, at least 0.001. |

- **Input:** Candle stream; the built-in fixes its source to close.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value at bar max(shortLength, longLength) - 1. Exact finite-window restart; a zero basis emits undefined. The chart preloads 30 periods and draws columns around zero.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BBTrend(candles, 20, 50, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('value', value);
});
```

## BollingerPercentB

Position of the source within population-standard-deviation Bollinger Bands.

```ts signature
new BollingerPercentB(sourceIndicator: Indicator<number | undefined>, length: number = 20, multiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The selected price source. |
| `length` | `number` | `20` | Number of bars in the calculation window. |
| `multiplier` | `number` | `2` | Number of population standard deviations for the Bollinger bands. |

- **Input:** A numeric price series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 19. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 19 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BollingerPercentB(candles.close);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('BollingerPercentB', value);
});
```

## BullBearPower

Bull power plus bear power relative to the EMA of close.

```ts signature
new BullBearPower(candleIndicator: Indicator<Candle>, length: number = 13)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `13` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 12. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 261 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BullBearPower(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('BullBearPower', value);
});
```

## CCI

Commodity channel index: `(typical price − its mean) / (0.015 × mean deviation)` over `period`.

```ts signature
new CCI(candleIndicator: Indicator<Candle>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | required | The window length (20 classically). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `period` candles have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new CCI(candles, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('CCI20', value);
});
```

## ChandeMomentumOscillator

Chande momentum oscillator: `100 × (sum of gains − sum of losses) / (sum of gains + sum of losses)` over `length`, between −100 and 100.

```ts signature
new ChandeMomentumOscillator(sourceIndicator: Indicator<number | undefined>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream. |
| `length` | `number` | `9` | The window length. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Returns a number from the first candle, `NaN` while there has been no price change; the window is complete after `length` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChandeMomentumOscillator(candles.close, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || Number.isNaN(value)) return;
  manager.debugLog('CMO', value);
});
```

## ChopZone

One column per candle; zone carries the rounded EMA-angle color classification.

```ts signature
new ChopZone(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `ChopZoneValue`
  - `value: number | undefined`
  - `zone: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 0. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 122 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChopZone(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('ChopZone', value);
});
```

## ConnorsRSI

Mean of price RSI, consecutive-close streak RSI and the rank of the one-bar return against earlier returns.

```ts signature
new ConnorsRSI(candleIndicator: Indicator<Candle>, rsiLength: number = 3, streakLength: number = 2, rankLength: number = 100)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `rsiLength` | `number` | `3` | Wilder RSI length for closing prices. |
| `streakLength` | `number` | `2` | Wilder RSI length for the consecutive up/down streak. |
| `rankLength` | `number` | `100` | Lookback for the percentage rank of one-bar returns. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 100. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 101 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ConnorsRSI(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('ConnorsRSI', value);
});
```

## CoppockCurve

Weighted average of the sum of the long and short close returns.

```ts signature
new CoppockCurve(candleIndicator: Indicator<Candle>, wmaLength: number = 10, longLength: number = 14, shortLength: number = 11)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `wmaLength` | `number` | `10` | Weighted moving average length for the sum of returns. |
| `longLength` | `number` | `14` | Long rate-of-change lookback. |
| `shortLength` | `number` | `11` | Short rate-of-change lookback. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 23. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 26 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new CoppockCurve(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('CoppockCurve', value);
});
```

## Divergences

Finds regular and hidden divergences between price swings and a target series (an oscillator), using `Swings` pivots.

```ts signature
new Divergences(candleIndicator: Indicator<Candle>, source: 'close' | 'high/low', period: number, maxPivots: number, maxPivotAge: number, targetIndicator: Indicator<number | undefined>, minPivotAge: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `source` | `'close' \| 'high/low'` | required | Pivot price: `'close'` or `'high/low'`. |
| `period` | `number` | required | Candles on each side that define a pivot. |
| `maxPivots` | `number` | required | Maximum pivots kept. |
| `maxPivotAge` | `number` | required | Pivots older than this many candles are dropped. |
| `targetIndicator` | `Indicator<number \| undefined>` | required | The series compared with price (for example `new RSI(candles.close, 14)`). |
| `minPivotAge` | `number` | required | Minimum age, in candles, of the older pivot of a divergence. |

- **Input:** The candle series plus a target number stream (for example an RSI).
- **Value** (`getValue()` after `hasValue()`): `DivergenceInfo`
  - `index: number`
  - `positiveRegular: readonly Divergence[]`
  - `positiveHidden: readonly Divergence[]`
  - `negativeRegular: readonly Divergence[]`
  - `negativeHidden: readonly Divergence[]`
- `Divergence` is `{ pivotIndex: number; pivotTime: number; pivotPrice: number; pivotTarget: number }`.
- **Warm-up:** Ready from the first candle, with empty lists until pivots exist; a pivot is only confirmed `period` candles after it forms.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Divergences(candles, 'close', 5, 10, 60, new RSI(candles.close, 14), 5);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.positiveRegular.length > 0) manager.debugLog('bullish divergence', value.positiveRegular.length);
});
```

## DMI

Independent TradingView DMI: strict directional dominance, fixnan DIs and a zero-DX denominator of one. Existing ADX stays unchanged.

```ts signature
new DMI(candleIndicator: Indicator<Candle>, adxSmoothing: number = 14, diLength: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `adxSmoothing` | `number` | `14` | Wilder smoothing length of directional strength. |
| `diLength` | `number` | `14` | Wilder smoothing length of true range and directional movement. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `DMIValue`
  - `adx: number | undefined`
  - `posDI: number | undefined`
  - `negDI: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars adx: 27; posDI: 14; negDI: 14. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 419 candles to 1e-9. The catalog reserves 500 bars in addition to its parameter-dependent minimum. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new DMI(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('DMI', value);
});
```

## DPO

Detrended price oscillator: `close − SMA(period)` as it was `period / 2 + 1` candles ago.

```ts signature
new DPO(sourceIndicator: Indicator<number | undefined>, period: number = 20)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream. |
| `period` | `number` | `20` | The SMA length. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `period + floor(period / 2) + 1` (31 for period 20).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new DPO(candles.close, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('DPO', value);
});
```

## FisherTransform

Recursive Fisher transform of hl2, with its preceding value as the trigger.

```ts signature
new FisherTransform(candleIndicator: Indicator<Candle>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `9` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `FisherTransformValue`
  - `fisher: number | undefined`
  - `trigger: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars fisher: 8; trigger: 9. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 71 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new FisherTransform(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('FisherTransform', value);
});
```

## HARSI

JayRogers' Heikin Ashi RSI Oscillator: Heikin Ashi candles of zero-centred RSIs (`RSI − 50`, from −50 to +50; its levels are ±20 and ±30). The open is the previous candle's close RSI, the high and the low the higher and the lower of the high's and the low's RSIs.

```ts signature
new HARSI(candleIndicator: ICandleIndicator, period: number, openSmoothingLength: number = 1)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `period` | `number` | required | The RSI length. |
| `openSmoothingLength` | `number` | `1` | How many times the Heikin Ashi open weighs the previous open against the previous close (1, the classic Heikin Ashi open, by default). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `HARSIValue | undefined`
  - `open: number`
  - `high: number`
  - `low: number`
  - `close: number`
- **Sub-indicators:** `.open`, `.high`, `.low`, `.close`, usable as inputs to other indicators.
- **Warm-up:** First value on candle `period + 1`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new HARSI(candles, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('HA-RSI close', value.close, value.close > value.open ? 'green' : 'red');
});
```

## HistoricalVolatility

Population standard deviation of log returns, annualized with TradingView's intraday/daily versus higher-timeframe rule.

```ts signature
new HistoricalVolatility(candleIndicator: Indicator<Candle>, length: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `10` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 10. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 17 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new HistoricalVolatility(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('HistoricalVolatility', value);
});
```

## KDJ

KDJ oscillator: raw stochastic `dK`, then `K` and `D` smoothed with a 1/3 weight, and `J = 3K − 2D`.

```ts signature
new KDJ(candleIndicator: ICandleIndicator, extremePeriod: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `extremePeriod` | `number` | required | High/low look-back (9 classically). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `KDJValue`
  - `K: number`
  - `D: number`
  - `J: number`
  - `dK: number`
- **Warm-up:** Values from the first candle (K and D start at 50); allow `extremePeriod` candles before trusting them.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new KDJ(candles, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('K', value.K, 'D', value.D, 'J', value.J);
});
```

## KnowSureThing

Weighted sum of four SMA-smoothed rates of change and its SMA signal.

```ts signature
new KnowSureThing(candleIndicator: Indicator<Candle>, rocLength1: number = 10, rocLength2: number = 15, rocLength3: number = 20, rocLength4: number = 30, smaLength1: number = 10, smaLength2: number = 10, smaLength3: number = 10, smaLength4: number = 15, signalLength: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `rocLength1` | `number` | `10` | Lookback of rate-of-change component 1. |
| `rocLength2` | `number` | `15` | Lookback of rate-of-change component 2. |
| `rocLength3` | `number` | `20` | Lookback of rate-of-change component 3. |
| `rocLength4` | `number` | `30` | Lookback of rate-of-change component 4. |
| `smaLength1` | `number` | `10` | SMA length of rate-of-change component 1. |
| `smaLength2` | `number` | `10` | SMA length of rate-of-change component 2. |
| `smaLength3` | `number` | `10` | SMA length of rate-of-change component 3. |
| `smaLength4` | `number` | `15` | SMA length of rate-of-change component 4. |
| `signalLength` | `number` | `9` | Length of the signal moving average. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `KnowSureThingValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 44; signal: 52. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 52 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new KnowSureThing(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('KnowSureThing', value);
});
```

## MACD

Moving average convergence divergence: `macd = fast MA − slow MA`, `signal = MA(macd)`, `histogram = macd − signal`.

```ts signature
new MACD(sourceIndicator: Indicator<number | undefined>, maType: 'sma' | 'ema', fastPeriod: number, slowPeriod: number, signalMaType: 'sma' | 'ema', signalPeriod: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream (usually closes). |
| `maType` | `'sma' \| 'ema'` | required | Average for the fast and slow lines: `'ema'` or `'sma'`. |
| `fastPeriod` | `number` | required | Fast average length (12 classically). |
| `slowPeriod` | `number` | required | Slow average length (26). |
| `signalMaType` | `'sma' \| 'ema'` | required | Average for the signal line: `'ema'` or `'sma'`. |
| `signalPeriod` | `number` | required | Signal average length (9). |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `MACDValue`
  - `macd: number | undefined`
  - `signal: number | undefined`
  - `histogram: number | undefined`
- **Warm-up:** `macd` after `slowPeriod` candles; `signal` and `histogram` after `slowPeriod + signalPeriod − 1` candles (34 with 12/26/9). `hasValue()` is true from the first candle: guard each field.
- All six arguments are required; there are no defaults.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MACD(candles.close, 'ema', 12, 26, 'ema', 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.macd === undefined || value.signal === undefined || value.histogram === undefined) return;
  manager.debugLog('MACD', value.macd, 'signal', value.signal, 'histogram', value.histogram);
});
```

## MassIndex

Rolling sum of candle range EMA divided by its double EMA.

```ts signature
new MassIndex(candleIndicator: Indicator<Candle>, length: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `10` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 25. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 121 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MassIndex(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('MassIndex', value);
});
```

## Momentum

Momentum: `value − value length candles ago`.

```ts signature
new Momentum(sourceIndicator: Indicator<number | undefined>, length: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream. |
| `length` | `number` | `10` | How many candles back to compare. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` for the first `length` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Momentum(candles.close, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('momentum', value);
});
```

## PriceOscillator

Percentage price oscillator, configurable EMA/SMA signal and histogram.

```ts signature
new PriceOscillator(sourceIndicator: Indicator<number | undefined>, fastLength: number = 12, slowLength: number = 26, signalLength: number = 9, maType: string = "EMA", signalMaType: string = "EMA")
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The selected price source. |
| `fastLength` | `number` | `12` | Fast calculation length. |
| `slowLength` | `number` | `26` | Slow calculation length. |
| `signalLength` | `number` | `9` | Length of the signal moving average. |
| `maType` | `string` | `"EMA"` | Moving average of the fast and slow prices: EMA or SMA. |
| `signalMaType` | `string` | `"EMA"` | Moving average of the oscillator signal: EMA or SMA. |

- **Input:** A numeric price series.
- **Value** (`getValue()` after `hasValue()`): `PriceOscillatorValue`
  - `value: number | undefined`
  - `signal: number | undefined`
  - `histogram: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 25; signal: 33; histogram: 33. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 306 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PriceOscillator(candles.close);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('PriceOscillator', value);
});
```

## PringsSpecialK

Weighted sum of twelve smoothed returns, followed by two SMA signal stages.

```ts signature
new PringsSpecialK(sourceIndicator: Indicator<number | undefined>, signalLength1: number = 100, signalLength2: number = 100)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The selected price source. |
| `signalLength1` | `number` | `100` | First SMA signal length. |
| `signalLength2` | `number` | `100` | Second SMA signal length. |

- **Input:** A numeric price series.
- **Value** (`getValue()` after `hasValue()`): `PringsSpecialKValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 724; signal: 922. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 922 candles to 1e-9. Fixed ROC components require at least 724 bars even with short signal lengths; the catalog reserves 950. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PringsSpecialK(candles.close);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('PringsSpecialK', value);
});
```

## RankCorrelationIndex

Spearman price/time rank correlation, optional smoothing and population Bollinger Bands.

```ts signature
new RankCorrelationIndex(candleIndicator: Indicator<Candle>, source: string = "close", length: number = 10, smoothingType: string = "SMA", smoothingLength: number = 14, bbMultiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `source` | `string` | `"close"` | Price field: open, high, low, close, hl2, hlc3, ohlc4 or hlcc4. |
| `length` | `number` | `10` | Number of bars in the calculation window. |
| `smoothingType` | `string` | `"SMA"` | Optional smoothing: None, SMA, SMA + Bollinger Bands, EMA, SMMA (RMA), WMA or VWMA. |
| `smoothingLength` | `number` | `14` | Length of the optional smoothing average. |
| `bbMultiplier` | `number` | `2` | Population standard deviation multiplier of the optional Bollinger bands. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `RankCorrelationIndexValue`
  - `value: number | undefined`
  - `average: number | undefined`
  - `upper: number | undefined`
  - `lower: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 10; average: 23; upper: -1; lower: -1. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 25 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RankCorrelationIndex(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('RankCorrelationIndex', value);
});
```

## RCIRibbon

Three Spearman rank correlations of price with time, using average ranks for ties.

```ts signature
new RCIRibbon(candleIndicator: Indicator<Candle>, source: string = "close", shortLength: number = 10, middleLength: number = 30, longLength: number = 50)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `source` | `string` | `"close"` | Price field: open, high, low, close, hl2, hlc3, ohlc4 or hlcc4. |
| `shortLength` | `number` | `10` | Short rank-correlation window. |
| `middleLength` | `number` | `30` | Middle rank-correlation window. |
| `longLength` | `number` | `50` | Long rank-correlation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `RCIRibbonValue`
  - `short: number | undefined`
  - `middle: number | undefined`
  - `long: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars short: 10; middle: 30; long: 50. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 50 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RCIRibbon(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('RCIRibbon', value);
});
```

## ROC

Rate of change, in percent: `100 × (value − value length ago) / value length ago`.

```ts signature
new ROC(sourceIndicator: Indicator<number | undefined>, length: number = 9)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream. |
| `length` | `number` | `9` | How many candles back to compare. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` for the first `length` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ROC(candles.close, 9);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('ROC %', value);
});
```

## RSI

Relative strength index (Wilder): `100 − 100 / (1 + average gain / average loss)` over `period`, between 0 and 100.

```ts signature
new RSI(sourceIndicator: Indicator<number>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream (usually closes). |
| `period` | `number` | required | The averaging length (14 classically). |

- **Input:** A number stream whose values are never `undefined` (`candles.close`, a `PartialIndicator`). It does not accept the output of another averaging indicator, which is `number | undefined`.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` for the first `period` candles; the first value arrives on candle `period + 1`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RSI(candles.close, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  if (value < 30) manager.debugLog('oversold', value);
});
```

## RSILambda

A function `(price) => rsi` that returns the RSI the current candle would have if it closed at `price`. Use it to find the price that would push RSI through a level.

```ts signature
new RSILambda(sourceIndicator: Indicator<number>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream (usually closes). |
| `period` | `number` | required | The RSI length. |

- **Input:** A number stream whose values are never `undefined`.
- **Value** (`getValue()` after `hasValue()`): `(value: number) => number | undefined`
- **Warm-up:** The function returns `undefined` during the first `period` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RSILambda(candles.close, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  const ifDown2pct = value(0.98 * (candles.getValue()?.close ?? 0));
  if (ifDown2pct === undefined) return;
  manager.debugLog('RSI if the next close is 2% lower', ifDown2pct);
});
```

## SMIErgodicIndicator

Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change.

```ts signature
new SMIErgodicIndicator(candleIndicator: Indicator<Candle>, longLength: number = 20, shortLength: number = 5, signalLength: number = 5)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `longLength` | `number` | `20` | Long calculation length. |
| `shortLength` | `number` | `5` | Short calculation length. |
| `signalLength` | `number` | `5` | Length of the signal moving average. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `SMIErgodicIndicatorValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 24; signal: 28. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 336 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SMIErgodicIndicator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('SMIErgodicIndicator', value);
});
```

## SMIErgodicOscillator

Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change.

```ts signature
new SMIErgodicOscillator(candleIndicator: Indicator<Candle>, longLength: number = 20, shortLength: number = 5, signalLength: number = 5)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `longLength` | `number` | `20` | Long calculation length. |
| `shortLength` | `number` | `5` | Short calculation length. |
| `signalLength` | `number` | `5` | Length of the signal moving average. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 28. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 315 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SMIErgodicOscillator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('SMIErgodicOscillator', value);
});
```

## SqueezeMomentum

Squeeze momentum (LazyBear): detects Bollinger bands inside Keltner channels (`squeezeState: 'on'`) and measures momentum with a linear regression of the price against its range midpoint.

```ts signature
new SqueezeMomentum(candleIndicator: ICandleIndicator, length: number = 20, mult: number = 2.0, lengthKC: number = 20, multKC: number = 1.5, useTrueRange: boolean = true)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `length` | `number` | `20` | Bollinger length. |
| `mult` | `number` | `2.0` | Bollinger multiplier (see note). |
| `lengthKC` | `number` | `20` | Keltner length. |
| `multKC` | `number` | `1.5` | Keltner multiplier. |
| `useTrueRange` | `boolean` | `true` | Use true range (true) or high − low (false) for the Keltner width. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `SqueezeMomentumValue`
  - `momentum: number | undefined`
  - `squeezeState: SqueezeState | undefined`
  - `momentumColor: MomentumColor | undefined`
- `SqueezeState` is `'on' | 'off' | 'none'`.
- `MomentumColor` is `'lime' | 'green' | 'red' | 'maroon'`.
- **Warm-up:** Fields are `undefined` until the Bollinger, Keltner and regression windows are full (about `length + lengthKC` candles).
- Faithful to the original script, the Bollinger width uses `multKC` rather than `mult`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SqueezeMomentum(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.momentum === undefined || value.squeezeState === undefined) return;
  manager.debugLog('squeeze', value.squeezeState, 'momentum', value.momentum, value.momentumColor);
});
```

## Stochastic

Stochastic oscillator: `stochastic = 100 × (close − lowest low) / (highest high − lowest low)` over `extremePeriod`, `k` = its `kPeriod` SMA, `d` = the `dPeriod` SMA of `k`.

```ts signature
new Stochastic(sourceIndicator: Indicator<Candle | number | undefined>, extremePeriod: number, kPeriod: number, dPeriod: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<Candle \| number \| undefined>` | required | Candles, or a number stream. |
| `extremePeriod` | `number` | required | High/low look-back (14 classically). |
| `kPeriod` | `number` | required | %K smoothing (3 for the slow stochastic, 1 for the fast one). |
| `dPeriod` | `number` | required | %D smoothing (3). |

- **Input:** The candle series (uses high/low/close), or a number stream (then the same stream serves as high, low and close).
- **Value** (`getValue()` after `hasValue()`): `StochasticOutput`
  - `stochastic: number | undefined`
  - `k: number | undefined`
  - `d: number | undefined`
- **Warm-up:** `stochastic` from the first candle; `k` after `kPeriod` candles; `d` after `kPeriod + dPeriod − 1` candles. The range uses a partial window during the first `extremePeriod` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Stochastic(candles, 14, 3, 3);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.k === undefined || value.d === undefined) return;
  manager.debugLog('%K', value.k, '%D', value.d);
});
```

## StochasticMomentumIndex

Double-EMA-smoothed position in the rolling high/low range and its EMA signal.

```ts signature
new StochasticMomentumIndex(candleIndicator: Indicator<Candle>, kLength: number = 10, dLength: number = 3, emaLength: number = 3)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `kLength` | `number` | `10` | High/low range length (1 to 15000). |
| `dLength` | `number` | `3` | Length of both EMA stages (1 to 4999). |
| `emaLength` | `number` | `3` | Signal EMA length (1 to 4999). |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `StochasticMomentumIndexValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 13; signal: 15. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 105 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new StochasticMomentumIndex(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('StochasticMomentumIndex', value);
});
```

## TRIX

TRIX: the one-candle change of a triple EMA of the log price, times 10,000.

```ts signature
new TRIX(sourceIndicator: Indicator<number | undefined>, period: number = 18)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The number stream. |
| `period` | `number` | `18` | Length of each of the three EMAs. |

- **Input:** A number stream.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until the triple EMA has two values (about `3 × period − 1` candles).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new TRIX(candles.close, 18);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('TRIX', value);
});
```

## TrueStrengthIndex

Double-EMA-smoothed price change divided by double-EMA-smoothed absolute change.

```ts signature
new TrueStrengthIndex(candleIndicator: Indicator<Candle>, longLength: number = 25, shortLength: number = 13, signalLength: number = 13)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `longLength` | `number` | `25` | Long calculation length. |
| `shortLength` | `number` | `13` | Short calculation length. |
| `signalLength` | `number` | `13` | Length of the signal moving average. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `TrueStrengthIndexValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 37; signal: 49. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 396 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new TrueStrengthIndex(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('TrueStrengthIndex', value);
});
```

## UlcerIndex

Root mean square of percentage drawdowns from the rolling maximum.

```ts signature
new UlcerIndex(sourceIndicator: Indicator<number | undefined>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The selected price source. |
| `length` | `number` | `14` | Number of bars in the calculation window. |

- **Input:** A numeric price series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 26. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 44 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new UlcerIndex(candles.close);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('UlcerIndex', value);
});
```

## UltimateOscillator

Buying pressure relative to true range over three weighted periods.

```ts signature
new UltimateOscillator(candleIndicator: Indicator<Candle>, fastLength: number = 7, middleLength: number = 14, slowLength: number = 28)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `fastLength` | `number` | `7` | Fast calculation length. |
| `middleLength` | `number` | `14` | Middle calculation length. |
| `slowLength` | `number` | `28` | Slow calculation length. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars value: 28. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 31 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new UltimateOscillator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('UltimateOscillator', value);
});
```

## Vortex

Rolling cross-candle high/low distances divided by true range, including the first candle range.

```ts signature
new Vortex(candleIndicator: Indicator<Candle>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `length` | `number` | `14` | Number of bars in the calculation window. |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `VortexValue`
  - `positive: number | undefined`
  - `negative: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars positive: 14; negative: 14. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 23 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Vortex(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('Vortex', value);
});
```

## WilliamsPercentR

Williams %R: `100 × (close − highest high) / (highest high − lowest low)` over `length`, between −100 and 0.

```ts signature
new WilliamsPercentR(sourceIndicator: Indicator<Candle>, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<Candle>` | required | The candle series. |
| `length` | `number` | `14` | The look-back length. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** `undefined` until `length` candles have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new WilliamsPercentR(candles, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('%R', value);
});
```

## WoodiesCCI

Two CCIs of close, with the preceding five-bar sign rule for histogram colors.

```ts signature
new WoodiesCCI(candleIndicator: Indicator<Candle>, turboLength: number = 6, length: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The chart candle series. |
| `turboLength` | `number` | `6` | Close-based turbo CCI length (3 to 14). |
| `length` | `number` | `14` | Close-based slow CCI length (7 to 20). |

- **Input:** Chart candles.
- **Value** (`getValue()` after `hasValue()`): `WoodiesCCIValue`
  - `turbo: number | undefined`
  - `value: number | undefined`
  - `histogram: number | undefined`
  - `colorState: number | undefined`
- **Warm-up:** Default plotted outputs first appear at zero-based bars turbo: 5; value: 13; histogram: 13. Restart convergence measured on BTCUSDT 1h/1D and ETHUSDT 4h, at default and changed settings: at most 21 candles to 1e-9. Smart Chart preloads 30 times the largest calculation length (DMI: at least 500 bars; Special K: at least 950), capped at 5000 bars: a longer setting is still drawn, but its first visible values can differ from a full-history run, or be missing when the indicator needs more valid candles than the chart loads. Agent constructor bounds are unchanged.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new WoodiesCCI(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('WoodiesCCI', value);
});
```

# Volatility, bands and ranges

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

True range, bands, channels and choppiness measures. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## BBW

Bollinger BandWidth as TradingView plots it: the distance between the bands in percent of their basis, `(upper − lower) / basis × 100` (4.5 for bands 4.5% apart).

```ts signature
new BBW(sourceIndicator: Indicator<number>, period: number, stdDevMultiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream. |
| `period` | `number` | required | Bollinger length. |
| `stdDevMultiplier` | `number` | `2` | Standard deviations between the basis and each band (2 by default). |

- **Input:** A number stream whose values are never `undefined`.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `period`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BBW(candles.close, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('BBW', value);
});
```

## BBWP

Bollinger BandWidth Percentile, The_Caretaker's: the share, in percent, of the previous `lookback` BBW values (fewer at first) lower than or equal to the current one.

```ts signature
new BBWP(sourceIndicator: Indicator<number>, period: number, lookback: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream. |
| `period` | `number` | required | Bollinger length. |
| `lookback` | `number` | required | How many BBW values to rank against. |

- **Input:** A number stream whose values are never `undefined`.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `period + 2`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BBWP(candles.close, 13, 252);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('BBWP', value);
});
```

## Bollinger

Bollinger bands: the `maPeriod` SMA (`mean`) plus and minus `stdDevMultiplier` standard deviations.

```ts signature
new Bollinger(sourceIndicator: Indicator<number>, maPeriod: number = 20, stdDevMultiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number>` | required | The number stream (usually closes). |
| `maPeriod` | `number` | `20` | Window length. |
| `stdDevMultiplier` | `number` | `2` | Band width in standard deviations. |

- **Input:** A number stream whose values are never `undefined` (`candles.close`).
- **Value** (`getValue()` after `hasValue()`): `BollingerValue`
  - `upper: number | undefined`
  - `mean: number | undefined`
  - `lower: number | undefined`
- **Warm-up:** Fields are `undefined` until `maPeriod` candles have been seen.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Bollinger(candles.close, 20, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.upper === undefined || value.lower === undefined || value.mean === undefined) return;
  manager.debugLog('upper', value.upper, 'mean', value.mean, 'lower', value.lower);
});
```

## Chop

Choppiness index: `100 × log10(Σ TR / (highest high − lowest low)) / log10(period)`; high values mean a sideways market.

```ts signature
new Chop(highIndicator: Indicator<number | undefined>, lowIndicator: Indicator<number | undefined>, trIndicator: Indicator<number | undefined>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `highIndicator` | `Indicator<number \| undefined>` | required | The high stream (`candles.high`). |
| `lowIndicator` | `Indicator<number \| undefined>` | required | The low stream (`candles.low`). |
| `trIndicator` | `Indicator<number \| undefined>` | required | The true range (`new TR(candles)`). |
| `period` | `number` | required | Window length. |

- **Input:** Three number streams: highs, lows and true range.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `period`; not finite when the window's highest high equals its lowest low.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Chop(candles.high, candles.low, new TR(candles), 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || !Number.isFinite(value)) return;
  manager.debugLog('chop', value);
});
```

## Choppy

Counts monotonic runs among the last six closes: `type` rates the current run (`'none'`, `'weak'`, `'medium'`, `'strong'`) and `line` counts trending candles among the last ten.

```ts signature
new Choppy(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `ChoppyValue`
  - `type: ChoppyType`
  - `line: number`
- `ChoppyType` is `'none' | 'weak' | 'medium' | 'strong'`.
- **Warm-up:** `type` is `'none'` until six candles have been seen; `line` fills over ten candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Choppy(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('run', value.type, 'trending candles out of 10', value.line);
});
```

## DC

Donchian channel MIDLINE: `(highest high + lowest low) / 2` over `extremePeriod` candles, current included. For the upper and lower channel lines use `Extreme`.

```ts signature
new DC(candleIndicator: Indicator<Candle>, extremePeriod: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `extremePeriod` | `number` | required | Window length, current candle included. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** A number from the first candle (partial window until `extremePeriod` candles).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new DC(candles, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('Donchian midline', value);
});
```

## Envelope

Percentage bands around an SMA or EMA basis.

```ts signature
new Envelope(sourceIndicator: Indicator<number | undefined>, length: number = 20, percent: number = 10, exponential: boolean = false)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | Numeric input stream. |
| `length` | `number` | `20` | Basis length, 1 to 5000. |
| `percent` | `number` | `10` | Signed band percentage, divided by 100. |
| `exponential` | `boolean` | `false` | True for an EMA basis; false for an SMA. |

- **Input:** Numeric source stream.
- **Value** (`getValue()` after `hasValue()`): `EnvelopeValue`
  - `basis: number | undefined`
  - `upper: number | undefined`
  - `lower: number | undefined`
- **Warm-up:** All three values start at bar length - 1. EMA restart convergence measured up to 958 bars at length 100 across three markets. Chart preloads max(200, 30 * length); percent is not a lookback.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Envelope(candles.close, 20, 10, false);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.basis === undefined) return;
  manager.debugLog('envelope', value.basis, value.upper, value.lower);
});
```

## FindMarketBottom

Williams VIX Fix bottom finder: `wvf = 100 × (highest close over 22 − low) / highest close`, with Bollinger and percentile ranges; `alert` is true when `wvf` spikes above them.

```ts signature
new FindMarketBottom(candleIndicator: ICandleIndicator, bollingerLength: number = 20, bollingerMult: number = 2.0, percentileLookback: number = 50, highPercentile: number = 0.85, lowPercentile: number = 1.01)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `bollingerLength` | `number` | `20` | Bollinger length on `wvf`. |
| `bollingerMult` | `number` | `2.0` | Bollinger multiplier. |
| `percentileLookback` | `number` | `50` | Look-back for the percentile range. |
| `highPercentile` | `number` | `0.85` | Upper range factor (0.85 = 85% of the highest `wvf`). |
| `lowPercentile` | `number` | `1.01` | Lower range factor. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `FindMarketBottomValue`
  - `wvf: number | undefined`
  - `upperBand: number | undefined`
  - `midLine: number | undefined`
  - `lowerBand: number | undefined`
  - `rangeHigh: number | undefined`
  - `rangeLow: number | undefined`
  - `alert: boolean`
- **Warm-up:** `alert` is always a boolean; band fields are `undefined` until `bollingerLength` and `percentileLookback` values exist (about 50 candles with the defaults).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new FindMarketBottom(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.alert) manager.debugLog('possible bottom, wvf', value.wvf);
});
```

## KeltnerChannels

Keltner channels: an EMA of the close (`middle`) plus and minus `multiplier` ATRs.

```ts signature
new KeltnerChannels(candleIndicator: ICandleIndicator, period: number = 20, multiplier: number = 2, atrPeriod: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `period` | `number` | `20` | EMA length. |
| `multiplier` | `number` | `2` | Channel width in ATRs. |
| `atrPeriod` | `number` | `10` | ATR length. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `{ upper: number | undefined; middle: number | undefined; lower: number | undefined; }`
  - `upper: number | undefined`
  - `middle: number | undefined`
  - `lower: number | undefined`
- **Warm-up:** Fields are `undefined` until both the EMA (candle `period`) and the ATR (candle `atrPeriod + 1`) are ready.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new KeltnerChannels(candles, 20, 2, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.upper === undefined || value.lower === undefined) return;
  manager.debugLog('upper', value.upper, 'lower', value.lower);
});
```

## Median

Nearest-rank median with ATR bands and an EMA of the median.

```ts signature
new Median(candleIndicator: Indicator<Candle>, sourceIndicator: Indicator<number | undefined>, length: number = 3, atrLength: number = 14, atrMultiplier: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | OHLC candle stream for true range. |
| `sourceIndicator` | `Indicator<number \| undefined>` | required | Numeric median source; the built-in defaults to hl2. |
| `length` | `number` | `3` | Nearest-rank median and median EMA length, 1 to 5000. |
| `atrLength` | `number` | `14` | SMA-seeded RMA true-range length, 1 to 5000. |
| `atrMultiplier` | `number` | `2` | Signed integer ATR distance multiplier. |

- **Input:** Candle stream for ATR and a numeric stream for the median.
- **Value** (`getValue()` after `hasValue()`): `MedianValue`
  - `value: number | undefined`
  - `upper: number | undefined`
  - `lower: number | undefined`
  - `ema: number | undefined`
- **Warm-up:** Median begins at bar length - 1, its EMA at 2 * (length - 1), and bands at max(length, atrLength) - 1 on complete data. Measured restart convergence up to 1691 bars at length/ATR 100 across three markets. Chart preloads max(200, 30 * largest length); the multiplier is not a lookback.
- The nearest-rank median of [1, 2, 3, 4] is 2, rather than 2.5. Missing median observations are skipped.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Median(candles, candles.hl2, 3, 14, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.value === undefined) return;
  manager.debugLog('median', value.value, value.upper, value.lower, value.ema);
});
```

## TR

True range: `max(high − low, |high − previous close|, |low − previous close|)`. Smooth it with `RMA` for an ATR (TradingView's `ta.atr`).

```ts signature
new TR(candleIndicator: Indicator<Candle>, handleNa: boolean = true)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `handleNa` | `boolean` | `true` | Count `high − low` on the first candle, which has no previous close (`true` by default, as TradingView's ATR does); `false` leaves that candle `undefined`, as its DMI does. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** A value from the first candle; from the second with `handleNa` set to `false`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new TR(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('true range', value);
});
```

# Volume

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Indicators that weigh price by traded volume. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## ChaikinMoneyFlow

Chaikin money flow: `Σ(money-flow volume) / Σ(volume)` over `length`, between −1 and 1.

```ts signature
new ChaikinMoneyFlow(candleIndicator: Indicator<Candle>, length: number = 20)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `length` | `number` | `20` | Window length. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** A value from the first candle with non-zero volume; the window is complete after `length` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChaikinMoneyFlow(candles, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('CMF', value);
});
```

## ChaikinOscillator

Fast EMA minus slow EMA of cumulative accumulation/distribution.

```ts signature
new ChaikinOscillator(candleIndicator: Indicator<Candle>, fastLength: number = 3, slowLength: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `fastLength` | `number` | `3` | Fast EMA length; integer from 1 to 5,000. |
| `slowLength` | `number` | `10` | Slow EMA length; integer from 1 to 5,000. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value after the longer EMA has enough valid samples (10 candles with defaults). The Smart Chart preloads at least 2,000 candles and 30 times the longest length, capped at 5,000 candles.
- A flat high/low contributes zero. A missing contribution leaves the cumulative state intact and produces no value on that candle; the next valid candle resumes it.
- Restart precision was measured against the full-history reference on BTCUSDT 1h, BTCUSDT 1D and ETHUSDT 4h. The 2,000-candle floor also covers cancellation round-off from different cumulative origins.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ChaikinOscillator(candles, 3, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('Chaikin oscillator', value);
});
```

## EaseOfMovement

SMA of midpoint change multiplied by the candle range and a divisor, divided by volume.

```ts signature
new EaseOfMovement(candleIndicator: Indicator<Candle>, length: number = 14, divisor: number = 10000)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `length` | `number` | `14` | SMA length; integer from 1 to 5,000. |
| `divisor` | `number` | `10000` | Volume scaling factor; integer from 1 to 1,000,000,000,000. It is not a lookback length. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value after length valid midpoint changes (15 candles with defaults). The Smart Chart preloads 30 times length, with a 200-candle floor.
- A zero or missing volume gives a missing movement sample; SMA skips that sample instead of treating it as zero.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new EaseOfMovement(candles, 14, 10000);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('EOM', value);
});
```

## EldersForceIndex

EMA of close-to-close price change multiplied by volume.

```ts signature
new EldersForceIndex(candleIndicator: Indicator<Candle>, length: number = 13)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `length` | `number` | `13` | EMA length; integer from 1 to 5,000. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value after length valid close changes (14 candles with defaults). The Smart Chart preloads 30 times length, with a 200-candle floor, capped at 5,000 candles.
- EMA is seeded with an SMA of valid force samples. Zero volume contributes zero; missing volume produces no value for that candle and preserves the EMA state.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new EldersForceIndex(candles, 13);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('EFI', value);
});
```

## KlingerOscillator

EMA 34 minus EMA 55 of signed volume, with an EMA 13 signal.

```ts signature
new KlingerOscillator(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `KlingerOscillatorValue`
  - `value: number | undefined`
  - `signal: number | undefined`
- **Warm-up:** Oscillator after 55 valid volume samples; signal after 67. The Smart Chart preloads 1,000 candles, measured against full-history references.
- These three periods are fixed by the built-in. Volume is positive when hlc3 rises or is unchanged, negative otherwise; the first comparison takes negative volume.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new KlingerOscillator(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.value === undefined) return;
  manager.debugLog('Klinger', value.value, 'signal', value.signal);
});
```

## MFI

Money flow index: a volume-weighted RSI of the typical price, between 0 and 100.

```ts signature
new MFI(candleIndicator: Indicator<Candle>, period: number = 14)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | `14` | Window length. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** Returns 50 on the first candle and a partial-window value until `period + 1` candles.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MFI(candles, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('MFI', value);
});
```

## OnBalanceVolume

On-balance volume: a running total that adds the candle volume when the price rises and subtracts it when it falls.

```ts signature
new OnBalanceVolume(priceIndicator: Indicator<number | undefined>, volumeIndicator: Indicator<number>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `priceIndicator` | `Indicator<number \| undefined>` | required | The price stream (`candles.close`). |
| `volumeIndicator` | `Indicator<number>` | required | The volume stream (`candles.volume`). |

- **Input:** Two number streams: price and volume.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** A number (starting at 0) from the first candle.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new OnBalanceVolume(candles.close, candles.volume);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('OBV', value);
});
```

## PercentageVolumeOscillator

Percentage difference between fast and slow volume averages, with a signal average and a histogram.

```ts signature
new PercentageVolumeOscillator(candleIndicator: Indicator<Candle>, fastLength: number = 12, slowLength: number = 26, signalLength: number = 9, maType: string = 'EMA', signalMaType: string = 'EMA')
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `fastLength` | `number` | `12` | Fast average length; integer from 1 to 5,000. |
| `slowLength` | `number` | `26` | Slow average length; integer from 1 to 5,000. |
| `signalLength` | `number` | `9` | Signal average length; integer from 1 to 5,000. |
| `maType` | `string` | `'EMA'` | Oscillator average type: EMA or SMA. |
| `signalMaType` | `string` | `'EMA'` | Signal average type, chosen independently: EMA or SMA. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `PercentageVolumeOscillatorValue`
  - `value: number | undefined`
  - `signal: number | undefined`
  - `histogram: number | undefined`
- **Warm-up:** With defaults, oscillator after 26 valid volume samples; signal and histogram after 34. The Smart Chart preloads 30 times the longest length, with a 200-candle floor, capped at 5,000 candles.
- Missing volume is skipped by the averages. A zero slow average produces no oscillator value.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PercentageVolumeOscillator(candles, 12, 26, 9, 'EMA', 'EMA');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.value === undefined) return;
  manager.debugLog('PVO', value.value, 'signal', value.signal, 'histogram', value.histogram);
});
```

## PMAR

Price-to-moving-average ratio: `price / VWMA(price, volume, period)`.

```ts signature
new PMAR(priceIndicator: Indicator<number>, volumeIndicator: Indicator<number>, period: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `priceIndicator` | `Indicator<number>` | required | The price stream. |
| `volumeIndicator` | `Indicator<number>` | required | The volume stream. |
| `period` | `number` | required | VWMA length. |

- **Input:** Two number streams: price and volume.
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** A number from the first candle (partial VWMA window until `period`).

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PMAR(candles.close, candles.volume, 20);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('price / VWMA', value);
});
```

## PMARP

PMAR percentile, The_Caretaker's: the share, in percent, of the previous `lookback` PMAR values (fewer at first) not above the current one.

```ts signature
new PMARP(priceIndicator: Indicator<number>, volumeIndicator: Indicator<number>, period: number, lookback: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `priceIndicator` | `Indicator<number>` | required | The price stream. |
| `volumeIndicator` | `Indicator<number>` | required | The volume stream. |
| `period` | `number` | required | VWMA length. |
| `lookback` | `number` | required | How many PMAR values to rank against. |

- **Input:** Two number streams: price and volume.
- **Value** (`getValue()` after `hasValue()`): `number | undefined`
- **Warm-up:** First value on candle `period + 1`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PMARP(candles.close, candles.volume, 20, 350);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('PMARP', value);
});
```

## VolumeProfile

Volume profile over the last `numberOfBars` candles: volume per price row, point of control and value area.

```ts signature
new VolumeProfile(candleIndicator: Indicator<Candle>, options: VolumeProfileOptions = { numberOfBars: 150, rowSize: 24, valueAreaPercent: 70 })
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `options` | `VolumeProfileOptions` | `{ numberOfBars: 150, rowSize: 24, valueAreaPercent: 70 }` | `{ numberOfBars: 1 to 500, rowSize: price rows, valueAreaPercent: share of volume in the value area }`. All three are required when you pass the object. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `VolumeProfileValue | undefined`
  - `pocLevel: number | undefined`
  - `pocVolume: number | undefined`
  - `valueAreaHigh: number | undefined`
  - `valueAreaLow: number | undefined`
  - `profileData: VolumeProfileRow[] | undefined`
  - `totalVolume: number | undefined`
- `VolumeProfileOptions` is `{ numberOfBars: number; rowSize: number; valueAreaPercent: number }`.
- `VolumeProfileRow` is `{ priceLevel: number; upVolume: number; downVolume: number; totalVolume: number; isValueArea: boolean; isPOC: boolean }`.
- **Warm-up:** Available from the first candle; representative once `numberOfBars` candles are in the window.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new VolumeProfile(candles, { numberOfBars: 100, rowSize: 24, valueAreaPercent: 70 });

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || value.pocLevel === undefined) return;
  manager.debugLog('POC', value.pocLevel, 'value area', value.valueAreaLow, value.valueAreaHigh);
});
```

# Market structure and patterns

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common. Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Swings, structure breaks, order blocks, gaps, pivots, candle patterns and alternative chart types. See the [indicator reference](/docs/developers/stse/indicator-reference.md) for how to read a signature, and [Indicators](/docs/developers/stse/indicators.md) for the construction and guarding rules.

## BollingerBars

The original candle OHLC used by Bollinger Bars: a full-width blue high-to-low rectangle under a green or red body, over the price candles.

```ts signature
new BollingerBars(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `BollingerBarsValue`
  - `open: number | undefined`
  - `high: number | undefined`
  - `low: number | undefined`
  - `close: number | undefined`
- **Warm-up:** No warm-up: the first candle is available immediately. Each missing or non-finite OHLC field is undefined.
- This indicator has no Bollinger-band calculation or length input. It paints the same OHLC values as the source candles.
- The chart paints the blue rectangle first, then green when close > open and red otherwise (including a doji), with no extra grey wick.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BollingerBars(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || value.close === undefined) return;
  manager.debugLog('Bollinger Bars', value.open, value.high, value.low, value.close);
});
```

## BosNChoch

Trend state machine that tracks the last break of structure and the change-of-character level.

```ts signature
new BosNChoch(candleIndicator: Indicator<Candle>, chochType: ChochType, minExtremeCandlesChochRecalculation: number, minCounterTrendCandlesBos: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `chochType` | `ChochType` | required | What breaks the CHoCH level: `'body'`, `'wick'` or `'both'`. |
| `minExtremeCandlesChochRecalculation` | `number` | required | Candles the extreme must hold before the CHoCH level is recomputed. |
| `minCounterTrendCandlesBos` | `number` | required | Counter-trend candles required before a new BOS counts. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `BosNChochValue | undefined`
  - `trend: "BULLISH" | "BEARISH"`
  - `chochLevel: number`
  - `bosLevel: number`
  - `action: "BOS" | "CHOCH" | undefined`
  - `type: "BODY" | "WICK" | undefined`
- `ChochType` is `'body' | 'wick' | 'both'`.
- **Warm-up:** `undefined` for the first `minExtremeCandlesChochRecalculation` candles; `action` and `type` are set only on the candle of a BOS or CHoCH.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new BosNChoch(candles, 'body', 3, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('trend', value.trend, 'action', value.action, 'CHoCH level', value.chochLevel);
});
```

## Engulfing

Two-candle engulfing pattern: `'bullish'` when an up candle's body engulfs the previous down candle's body, `'bearish'` for the mirror case, `'none'` otherwise.

```ts signature
new Engulfing(candleIndicator: Indicator<Candle>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `"none" | "bullish" | "bearish"`
- **Warm-up:** `'none'` on the first candle.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Engulfing(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value !== 'none') manager.debugLog('engulfing', value);
});
```

## FVG

Fair value gap on the last three candles: bullish when the first candle's high is below the third candle's low, bearish for the mirror case.

```ts signature
new FVG(candleIndicator: ICandleIndicator)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `FVGValue | null`
  - `type: "bullish" | "bearish"`
  - `upper: number`
  - `lower: number`
- **Warm-up:** `null` until three candles have been seen, and on every candle without a gap.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new FVG(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === null) return;
  manager.debugLog(value.type, 'gap', value.lower, value.upper);
});
```

## Heikinashi

Heikin-Ashi candles: `close = (o + h + l + c) / 4`, `open = (previous HA open + previous HA close) / 2`, high/low extended to include them.

```ts signature
new Heikinashi(candleIndicator: Indicator<OHLCValue | undefined>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<OHLCValue \| undefined>` | required | The candle series (or any OHLC series). |

- **Input:** An indicator of `{ open, high, low, close }` objects: the candle series itself works.
- **Value** (`getValue()` after `hasValue()`): `OHLCValue | undefined`
  - `open: number`
  - `high: number`
  - `low: number`
  - `close: number`
- **Sub-indicators:** `.open`, `.high`, `.low`, `.close`, usable as inputs to other indicators.
- **Warm-up:** A candle from the first update (its open seeds from the first real candle).
- `.open`, `.high`, `.low`, `.close` are also exposed as number sub-indicators, so `new EMA(ha.close, 20)` averages Heikin-Ashi closes.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Heikinashi(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  manager.debugLog('HA', value.open, value.close, value.close > value.open ? 'green' : 'red');
});
```

## HeikinashiCandles

The candles of a Heikin Ashi chart: each candle keeps its times and volume, with the Heikin-Ashi open, high, low and close. Any indicator that takes candles runs on it, as a Heikin Ashi chart computes its indicators on these candles.

```ts signature
new HeikinashiCandles(candleIndicator: ICandleIndicator)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `Candle`
  - `openTime: number`
  - `closeTime: number`
  - `open: number`
  - `high: number`
  - `low: number`
  - `close: number`
  - `volume: number`
- **Sub-indicators:** `.openTime`, `.closeTime`, `.open`, `.high`, `.low`, `.close`, `.volume`, `.hl2`, `.hlc3`, `.ohlc4`, `.hlcc4`, usable as inputs to other indicators.
- **Methods:** `getPending(): Candle | undefined`, `getAssetClass(): string | undefined`, `getInterval(): Interval | undefined`.
- **Warm-up:** A candle from the first update (its open seeds from the first real candle).
- Pass it where an indicator takes candles: `new ADX(ha, 14)`, `new KeltnerChannels(ha)`, `new VWAP(ha, ha.hlc3, 'day')`, or `ha.close` for a price stream (`new RSI(ha.close, 14)`).
- `getPending()` gives the forming candle in Heikin Ashi, after the last closed one.
- Orders still fill at real prices: compare a level computed on these candles with `ha.close`, not with the real close.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new HeikinashiCandles(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  manager.debugLog('HA', value.open, value.close, 'volume', value.volume);
});
```

## Lunar

Moon phase from a timestamp series: 0 new moon, 2 first quarter, 4 full moon, 6 last quarter (8 phases).

```ts signature
new Lunar(timeIndicator: Indicator<number>)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `timeIndicator` | `Indicator<number>` | required | Epoch-millisecond timestamps. |

- **Input:** A timestamp stream (`candles.openTime` or `candles.closeTime`).
- **Value** (`getValue()` after `hasValue()`): `number`
- **Warm-up:** Ready from the first candle.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Lunar(candles.closeTime);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === 4) manager.debugLog('full moon');
});
```

## MarketStructure

Market structure (swing highs/lows labelled HH, LH, HL, LL) with break of structure (BOS) and change of character (CHoCH) events.

```ts signature
new MarketStructure(candleIndicator: ICandleIndicator, swingSize: number = 20, bosConfType: 'close' | 'wick' = 'close', showChoch: boolean = true)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `swingSize` | `number` | `20` | Candles on each side that confirm a swing. |
| `bosConfType` | `'close' \| 'wick'` | `'close'` | What confirms a break: the candle `'close'` or its `'wick'`. |
| `showChoch` | `boolean` | `true` | Label the first counter-trend break as `'CHoCH'` (true) or report every break as `'BOS'` (false). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `MarketStructureValue`
  - `swingType?: SwingType | undefined`
  - `swingBarIndex?: number | undefined`
  - `bosType?: BosType | undefined`
  - `bosDirection?: BosDirection | undefined`
  - `bosBarIndex?: number | undefined`
  - `prevHigh?: number | undefined`
  - `prevLow?: number | undefined`
  - `prevHighIndex?: number | undefined`
  - `prevLowIndex?: number | undefined`
  - `highActive: boolean`
  - `lowActive: boolean`
- `SwingType` is `'HH' | 'LH' | 'HL' | 'LL'`.
- `BosType` is `'BOS' | 'CHoCH'`.
- `BosDirection` is `'bullish' | 'bearish'`.
- **Warm-up:** Ready from the first candle; events appear once swings are confirmed (`swingSize` candles after they form). Event fields are only set on the candle where the event happens.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new MarketStructure(candles, 10, 'close', true);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.bosType === undefined || value.bosDirection === undefined) return;
  manager.debugLog(value.bosType, value.bosDirection, 'previous high', value.prevHigh);
});
```

## OrderBlocks

The most recent bullish and bearish order blocks (a down candle followed by an up candle closing above its high, and the mirror case), with 25/50/75% lines.

```ts signature
new OrderBlocks(candleIndicator: ICandleIndicator, maxBoxSet: number = 5)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `maxBoxSet` | `number` | `5` | How many blocks of each side are tracked. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `OrderBlocksValue`
  - `bullish: OrderBlock | null`
  - `bearish: OrderBlock | null`
- `OrderBlock` is `{ type: "bullish" | "bearish"; upper: number; lower: number; timestamp: number; line25: number; line50: number; line75: number }`.
- **Warm-up:** Values from the third candle; `bullish` / `bearish` are `null` until a block exists.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new OrderBlocks(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  const block = value.bullish;
  if (block === null) return;
  manager.debugLog('bullish block', block.lower, block.upper, 'mid', block.line50);
});
```

## PivotPoints

Classic floor pivot levels (P, R1 to R5, S1 to S5) computed from the previous day, week or month.

```ts signature
new PivotPoints(candleIndicator: ICandleIndicator, pivotType: PivotType = 'Traditional', timeframe: Timeframe = 'day')
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `pivotType` | `PivotType` | `'Traditional'` | Formula: `'Traditional'`, `'Fibonacci'`, `'Woodie'`, `'Classic'`, `'DM'` or `'Camarilla'`. |
| `timeframe` | `Timeframe` | `'day'` | The period the levels are computed from: `'day'`, `'week'` or `'month'` (UTC boundaries). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `PivotPointsValue`
  - `P: number | undefined`
  - `R1: number | undefined`
  - `R2: number | undefined`
  - `R3: number | undefined`
  - `R4: number | undefined`
  - `R5: number | undefined`
  - `S1: number | undefined`
  - `S2: number | undefined`
  - `S3: number | undefined`
  - `S4: number | undefined`
  - `S5: number | undefined`
- `PivotType` is `'Traditional' | 'Fibonacci' | 'Woodie' | 'Classic' | 'DM' | 'Camarilla'`.
- `Timeframe` is `'day' | 'week' | 'month'`.
- **Warm-up:** Levels are `undefined` until one full period has completed. Formulas that define fewer levels leave the others `undefined`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PivotPoints(candles, 'Traditional', 'day');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.P === undefined || value.R1 === undefined || value.S1 === undefined) return;
  manager.debugLog('P', value.P, 'R1', value.R1, 'S1', value.S1);
});
```

## PivotsHighLow

Independent high and low pivots with their original candle timestamps, emitted only when the right-hand confirmation candles have arrived.

```ts signature
new PivotsHighLow(candleIndicator: Indicator<Candle>, leftHigh: number = 10, rightHigh: number = 10, leftLow: number = 10, rightLow: number = 10)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `leftHigh` | `number` | `10` | Candles before a candidate high (integer 1..5000). |
| `rightHigh` | `number` | `10` | Candles after a high required to confirm it (integer 1..5000). |
| `leftLow` | `number` | `10` | Candles before a candidate low (integer 1..5000). |
| `rightLow` | `number` | `10` | Candles after a low required to confirm it (integer 1..5000). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `PivotsHighLowValue`
  - `high: number | undefined`
  - `highAt: number | undefined`
  - `low: number | undefined`
  - `lowAt: number | undefined`
- **Warm-up:** Each side needs left + right + 1 candles; defaults first allow a pivot at candle 21. Exact page pre-roll is max(leftHigh + rightHigh, leftLow + rightLow), with a chart minimum of 200 and refusal above 5000.
- An equal extreme before the candidate is allowed; an equal extreme after it rejects the candidate. A missing neighbour stops that side of the scan at its first missing value.
- highAt and lowAt are epoch-millisecond open times. Read the event on its confirmation candle, never retroactively act at its earlier anchor. A forming confirmation candle can invalidate the event.
- The chart retains the latest 500 price labels across both sides. Four chart-only RGBA colours do not change native agent calculations.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new PivotsHighLow(candles, 10, 10, 10, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  if (value.high !== undefined) manager.debugLog('high pivot', value.high, 'original open time', value.highAt);
  if (value.low !== undefined) manager.debugLog('low pivot', value.low, 'original open time', value.lowAt);
});
```

## Renko

Renko bricks from a price series: a new brick forms when price travels `brickSize` in the trend direction; a reversal needs `2 × brickSize`.

```ts signature
new Renko(sourceIndicator: Indicator<number | undefined>, brickSize: number | Indicator<number | undefined>, gridAligned: boolean = false)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `sourceIndicator` | `Indicator<number \| undefined>` | required | The price stream. |
| `brickSize` | `number \| Indicator<number \| undefined>` | required | Brick height in price units, or an indicator giving a dynamic size. |
| `gridAligned` | `boolean` | `false` | Anchor bricks on multiples of `brickSize` (true) instead of the first price (false). |

- **Input:** A number stream (usually closes).
- **Value** (`getValue()` after `hasValue()`): `RenkoValue | undefined`
  - `open: number`
  - `close: number`
  - `direction: 1 | -1`
  - `count: number`
- **Sub-indicators:** `.open`, `.close`, `.high`, `.low`, `.direction`, `.count`, usable as inputs to other indicators.
- **Warm-up:** `undefined` until the first brick forms. `count` is how many bricks formed on this candle (0 when none), so act on `count > 0`.
- Build Renko on a small base timeframe (1 or 5 minutes); the brick size is in the traded instrument's price units.
- `.open`, `.close`, `.high`, `.low`, `.direction`, `.count` are also exposed as sub-indicators.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Renko(candles.close, 50);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || value.count === 0) return;
  manager.debugLog(value.count, 'new brick(s), direction', value.direction, 'last close', value.close);
});
```

## RenkoAtr

Renko bricks whose size is the `period` ATR (Wilder), either live or frozen at its first value (`anchored`).

```ts signature
new RenkoAtr(candleIndicator: Indicator<Candle>, period: number = 14, anchored: boolean = false, gridAligned: boolean = false)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `period` | `number` | `14` | ATR length. |
| `anchored` | `boolean` | `false` | Freeze the brick size at the first ATR value (true) or follow the ATR (false). |
| `gridAligned` | `boolean` | `false` | Anchor bricks on multiples of the brick size. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `RenkoValue | undefined`
  - `open: number`
  - `close: number`
  - `direction: 1 | -1`
  - `count: number`
- **Warm-up:** `undefined` until the ATR is ready (`period + 1` candles) and a first brick forms.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new RenkoAtr(candles, 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || value.count === 0) return;
  manager.debugLog('brick', value.direction, value.close);
});
```

## StrongCandleStructure

Market structure by the Strong Candle method: valid highs and lows, break of structure (BOS) and change of character (CHoCH) on candle closes, and the demand or supply zone of each break. A swing high only becomes valid once a candle closes below the previous candle's low (a candle close down), a swing low once a candle closes above the previous candle's high (a candle close up); each break anchors the new CHoCH level and its zone on the last strong candle before it.

```ts signature
new StrongCandleStructure(candleIndicator: Indicator<OHLC | undefined>, breakConfirmation: BreakConfirmation = 'body', anchor: StructureAnchor = 'last-candle', zoneMemory: ZoneMemory = 'current', zoneLimit: ZoneLimit = { above: 5, below: 5 })
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<OHLC \| undefined>` | required | The candle series (or any OHLC series). |
| `breakConfirmation` | `BreakConfirmation` | `'body'` | What breaks a valid high or low: a candle close beyond it (`'body'`, where a wick alone is a liquidity grab and the level holds) or any wick through it (`'wick'`). |
| `anchor` | `StructureAnchor` | `'last-candle'` | Where a break anchors the new CHoCH level and its zone: `'last-candle'` on the last strong candle before the break, with the level at the extreme reached since it; `'extreme-ccd'` at the most extreme low (high) of the range that a strong candle opening above (below) it produced, with the zone on that candle, falling back to `'last-candle'` when the range has no such extreme. |
| `zoneMemory` | `ZoneMemory` | `'current'` | `'current'` publishes only the zone of the current structure, replaced at every break; `'historical'` also keeps every zone until a candle breaks its outer edge by the `breakConfirmation` rule (a close beyond it, or a wick with `'wick'`), in `zones` and `brokenZones`. |
| `zoneLimit` | `ZoneLimit` | `{ above: 5, below: 5 }` | With `'historical'`: how many kept zones are published above (supply) and below (demand) the close. The current zone is always published on top of them; hidden zones are still tracked and come back when price nears them. |

- **Input:** The candle series, or `new Heikinashi(candles)` for the Heikin Ashi variant the method is usually traded on.
- **Value** (`getValue()` after `hasValue()`): `StrongCandleStructureValue | undefined`
  - `index: number`
  - `trend?: StructureTrend | undefined`
  - `validHigh?: number | undefined`
  - `validLow?: number | undefined`
  - `waitingForValidHigh: boolean`
  - `waitingForValidLow: boolean`
  - `action?: StructureAction | undefined`
  - `actionType?: StructureBreakType | undefined`
  - `liquidityGrab?: "ABOVE_HIGH" | "BELOW_LOW" | undefined`
  - `liquidityGrabCount?: number | undefined`
  - `candleCloseDown: boolean`
  - `candleCloseUp: boolean`
  - `zone?: StructureZone | undefined`
  - `zones?: readonly StructureZone[] | undefined`
  - `brokenZones?: readonly StructureZone[] | undefined`
- `BreakConfirmation` is `'body' | 'wick'`.
- `StructureAnchor` is `'last-candle' | 'extreme-ccd'`.
- `ZoneMemory` is `'current' | 'historical'`.
- `ZoneLimit` is `{ above: number; below: number }`.
- `StructureZone` is `{ side: "DEMAND" | "SUPPLY"; top: number; bottom: number; fresh: boolean; testedDepth?: number | undefined; createdIndex: number }`.
- `OHLC` is `{ open: number; high: number; low: number; close: number }`.
- `StructureTrend` is `'BULLISH' | 'BEARISH'`.
- `StructureAction` is `'BOS' | 'CHOCH'`.
- `StructureBreakType` is `'BODY' | 'WICK'`.
- **Warm-up:** A value from the first candle. `validHigh` and `validLow` stay `undefined` until a first candle close down or up validates them, `trend` and `zone` until the first break. `action`, `actionType` and `liquidityGrab` are set only on the candle of the event.
- A zone is `fresh` until price trades into it, and `testedDepth` is the deepest price reached inside it. A demand zone runs from the CHoCH level up to the open of the candle close down that anchors it; a supply zone from the CHoCH level down to the open of its anchoring candle close up.
- `liquidityGrabCount` numbers the candles that swept the same level (1, 2, 3), per side, and restarts when the level changes: count one setup per level, not one per candle. A candle that sweeps both levels is reported as `'ABOVE_HIGH'` and counts on both sides.
- Heikin Ashi and standard candles give different structures: pick one and keep it.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new StrongCandleStructure(candles, 'body', 'last-candle');

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined || value.action === undefined) return;
  manager.debugLog(value.action, value.trend, 'zone', value.zone?.bottom, value.zone?.top);
});
```

## SupportResistance

Support and resistance levels from pivot lows/highs, with break detection confirmed by a volume oscillator.

```ts signature
new SupportResistance(candleIndicator: ICandleIndicator, leftBars: number = 15, rightBars: number = 15, volumeThreshold: number = 20)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `leftBars` | `number` | `15` | Candles left of a pivot. |
| `rightBars` | `number` | `15` | Candles right of a pivot (confirmation delay). |
| `volumeThreshold` | `number` | `20` | Volume oscillator level (percent) a break needs to count as confirmed. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `SupportResistanceValue`
  - `resistance: number | undefined`
  - `support: number | undefined`
  - `volumeOsc: number | undefined`
  - `breakType: BreakType | undefined`
  - `breakWithVolume: boolean`
- `BreakType` is `'support' | 'resistance' | 'bullWick' | 'bearWick'`.
- **Warm-up:** `breakWithVolume` is a boolean from the first candle; `support` / `resistance` stay `undefined` until a first pivot of that side is confirmed (at least `leftBars + rightBars + 2` candles, often more: like LuxAlgo's script, a level starts the candle after its pivot is confirmed). `breakType` is set only on the candle that breaks a level, so do not early-return on it.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new SupportResistance(candles);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value.breakType === 'resistance' && value.breakWithVolume) manager.debugLog('resistance broken with volume', value.resistance);
});
```

## Swings

Swing highs and lows: a candle is a top (bottom) when it is the highest (lowest) of the `2 × period + 1` candles centred on it.

```ts signature
new Swings(candleIndicator: Indicator<Candle>, source: 'close' | 'high/low', side: 'top' | 'bottom' | 'both', period: number, maxPivots: number, maxPivotAge: number)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `Indicator<Candle>` | required | The candle series. |
| `source` | `'close' \| 'high/low'` | required | `'close'` or `'high/low'`. |
| `side` | `'top' \| 'bottom' \| 'both'` | required | Pivots to track: `'top'`, `'bottom'` or `'both'`. |
| `period` | `number` | required | Candles required on each side. |
| `maxPivots` | `number` | required | Maximum pivots kept (oldest dropped first). |
| `maxPivotAge` | `number` | required | Pivots older than this many candles are dropped. |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `SwingInfo`
  - `index: number`
  - `swings: Swing[]`
- `Swing` is `{ type: "top" | "bottom"; index: number; time: number; value: number }`.
- **Warm-up:** Ready from the first candle with an empty list; a pivot is confirmed `period` candles after it forms (non-repainting).
- `index` is the candle count of the pivot, `time` its close time and `value` its price: the pivot candle's own low for a bottom and high for a top, or its close with `source: 'close'`.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new Swings(candles, 'high/low', 'both', 5, 10, 100);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  const lastTop = value.swings.filter((s) => s.type === 'top').pop();
  if (lastTop === undefined) return;
  manager.debugLog('last swing high at candle', lastTop.index, 'time', lastTop.time);
});
```

## WilliamsFractal

Williams fractals: an up fractal is a high with `period` lower highs on each side, a down fractal a low with `period` higher lows.

```ts signature
new WilliamsFractal(candleIndicator: ICandleIndicator, period: number = 2)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `period` | `number` | `2` | Candles on each side (minimum 2). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `WilliamsFractalValue | undefined`
  - `upFractal: boolean`
  - `downFractal: boolean`
  - `fractalHigh?: number | undefined`
  - `fractalLow?: number | undefined`
- **Warm-up:** `undefined` until `2 × period + 1` candles; a fractal is reported `period` candles after the pivot candle.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new WilliamsFractal(candles, 2);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  if (value === undefined) return;
  if (value.downFractal) manager.debugLog('bullish fractal at', value.fractalLow);
});
```

## ZigZag

TradingView's Zig Zag: alternating swing highs and lows. A pivot is confirmed `period / 2` candles after it (at least 2), and a swing in the other direction starts a new leg once it is `deviation` percent away from the newest vertex.

```ts signature
new ZigZag(candleIndicator: ICandleIndicator, period: number = 10, maxPoints: number = 5, deviation: number = 0)
```

| Parameter | Type | Default | Meaning |
|---|---|---|---|
| `candleIndicator` | `ICandleIndicator` | required | The candle series. |
| `period` | `number` | `10` | TradingView's Pivot legs: the candles that confirm a pivot, half on each side of it (10 by default). |
| `maxPoints` | `number` | `5` | How many vertices are kept (newest first). |
| `deviation` | `number` | `0` | TradingView's Price deviation for reversals, in percent (0 by default: every confirmed swing; TradingView uses 5). |

- **Input:** The candle series.
- **Value** (`getValue()` after `hasValue()`): `ZigZagValue`
  - `points: readonly ZigZagPoint[]`
  - `direction: 0 | 1 | -1`
- `ZigZagPoint` is `{ value: number; barIndex: number; time: number }`.
- **Warm-up:** Ready from the first candle; `points` fills as pivots are confirmed, `period / 2` candles after their own. `points[0]` is the newest vertex and still moves while its leg extends.

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 60);
const indicator = new ZigZag(candles, 10, 5, 5);

manager.onIntervalCandle(symbol, 60, () => {
  if (!indicator.hasValue()) return;
  const value = indicator.getValue();
  const last = value.points[0];
  if (last === undefined) return;
  manager.debugLog('last zigzag point', last.value, 'direction', value.direction);
});
```

# Helpers

<!-- Generated by marketing-site/scripts/stse-docs/indicator-reference.mjs from stse-common (HELPER_NOTES). Do not edit by hand: edit indicator-notes.mjs and regenerate. -->

Utility functions and constants that are already in scope in agent code, next to the indicators. Like everything in scope, never import or redeclare them. Signatures are generated from the engine source.

## calculateFibonacciRetracements

```ts signature
calculateFibonacciRetracements(start: number, end: number): FibonacciLevels
```

Returns the retracement levels between two prices: `{ level0, level236, level382, level500, level618, level786, level1000 }`, where `level0` is `start` and `level1000` is `end`.

## down

```ts signature
down(candle: Candle): boolean
```

`true` when a candle closed below its open (`open > close`).

## ensureSymbolSingleTrade

```ts signature
ensureSymbolSingleTrade(manager: V2Manager, symbol: string, position: Position): boolean
```

Closes every open trade on `symbol` in the opposite direction, and returns `true` when no trade in the requested direction is open (so a new one can be opened).

```ts
const symbol = 'BTC';
const hourly = manager.getIntervalData(symbol, 60);
const cross = new Crossover(new EMA(hourly.close, 20), new EMA(hourly.close, 50));

manager.onIntervalCandle(symbol, 60, () => {
  if (!cross.hasValue()) return;
  const signal = cross.getValue();
  if (signal === null) return;
  const side = signal === 'firstUp' ? 'buy' : 'sell';
  if (ensureSymbolSingleTrade(manager, symbol, side)) {
    manager.openTrade({ symbol, position: side, type: 'MARKET', positionSize: { type: 'proportion', proportion: 0.1 }, comment: `EMA20/EMA50 cross, going ${side}` });
  }
});
```

## generateStoploss

```ts signature
generateStoploss(request: StoplossRequest | Stoploss): Stoploss
```

Turns a stop request (`{ type, value, delta? }`) into the stop object stored on a trade (`{ type, initial, absolute, delta? }`). The engine does this for you in `openTrade`; you rarely need it.

## generateTakeprofit

```ts signature
generateTakeprofit(request: TakeprofitRequest): Takeprofit
```

Validates a take-profit request and returns it as stored on a trade. The engine does this for you in `openTrade`.

## getStoploss

```ts signature
getStoploss(trade: StoplossTrade, key: StoplossKey = stoplossMainKey): number
```

The initial price of one of a trade's stops (`key` defaults to the main `stoploss`); 0 for a buy and twice the entry for a sell when the trade has no such stop.

## getStoplossDistance

```ts signature
getStoplossDistance(trade: StoplossTrade, key: StoplossKey = stoplossMainKey): number
```

The same distance as a fraction of the entry price (0.05 for a stop 5% away).

## getStoplossRisk

```ts signature
getStoplossRisk(trade: StoplossTrade, key: StoplossKey = stoplossMainKey): number
```

The distance between a trade's entry and its initial stop, in price units.

## rebalance

```ts signature
rebalance(manager: V2Manager, target: Record<string, number>): void
```

Places market orders that bring each symbol to a target weight of the wallet's market value.

- Only symbols with a current price take part, and their weights are re-normalised to sum to 1: `{ SPY: 0.5 }` invests the whole wallet in SPY, so this helper cannot hold cash.
- Sells run before buys. Orders are plain `MARKET` orders without `inhibitAutoInvest`, so on a paper wallet a buy larger than the free cash tops up invested capital, and quantities ignore futures contract sizes.
- For a budget-controlled rebalance, write the cycle yourself as described in [Rebalancing to target weights](/docs/developers/stse/orders-and-risk.md#rebalancing-to-target-weights).

## up

```ts signature
up(candle: Candle): boolean
```

`true` when a candle closed above its open (`open < close`).

## updateStoploss

```ts signature
updateStoploss(manager: V2Manager, trade: Trade, stoploss: Stoploss, value: number): void
```

Moves an existing stop of an open trade to a new absolute price.

- Pass the trade's own stop object (`trade.stoploss`, `trade.stoploss2`, …), after checking it is not `undefined`: the helper changes the object it receives, so a fresh object literal moves nothing (the validator refuses it: TS9018).
- The price is clamped to the market: a buy stop is never set above the current price, a sell stop never below. It does not ratchet by itself: compare with `stop.absolute` before calling if the stop must only tighten.
- Throws when the symbol has no current price. To give a trade a stop it does not have, set it in `openTrade`.

```ts
const symbol = 'ETH';
manager.onPreciseCandle(symbol, () => {});

manager.cron('*/5 * * * *', () => {
  const price = manager.getCurrentPrice(symbol);
  if (price === undefined) return;
  for (const trade of manager.getSymbolPendingTrades(symbol)) {
    const stop = trade.stoploss;
    if (trade.position !== 'buy' || stop === undefined) continue;
    const trailed = price * 0.95;
    if (trailed > stop.absolute) updateStoploss(manager, trade, stop, trailed); // only tighten
  }
});
```

## Constants

- `stoplossKeys: StoplossKey[]`: The names of a trade's stop fields: `stoploss`, `stoploss2`, `stoploss3`, `stoploss4`. Useful to loop over every stop of a trade.
- `stoplossMainKey: StoplossKey`: The main stop field name, `'stoploss'`.
- `takeprofitKeys: TakeprofitKey[]`: The names of a trade's take-profit fields: `takeprofit` to `takeprofit4`.

Types named in these signatures (`Trade`, `Stoploss`, `StoplossKey`, …) are in [Types](/docs/developers/stse/types.md). `StoplossTrade` is any object with a trade's `position`, `enterPrice` and stop fields, such as a `Trade`.

# Orders, trades and risk

How to buy and sell, how to size, how the wallet's capital works, and how to protect positions. The method reference is in the [Manager API](/docs/developers/stse/manager-api.md#orders); the exact object shapes in [Types](/docs/developers/stse/types.md).

## Trade or order?

- **A trade** (`manager.openTrade` / `manager.closeTrade`) is an entry with a planned exit: a stop-loss, a take-profit or an exit rule. It produces realized profit when it closes, and the engine manages its stops. Use trades for active strategies where every entry has an exit.
- **An order** (`manager.placeOrder`) changes a position's quantity with no exit attached. Use orders for buy-and-hold, dollar-cost averaging, accumulation and rebalancing. Their profit is measured mark-to-market on the position.

An operation with no exit plan must use orders: an open trade shows no realized profit until it closes, so a buy-and-hold written with `openTrade` shows a flat equity curve.

The two calls share `symbol`, `position` (`'buy'` or `'sell'`, never `'long'` / `'short'` or `side`) and `type` (`'MARKET'`, `'LIMIT'`, `'MARKET_TRIGGER'`). They differ in the rest:

| | `placeOrder` (`CreateOrder`) | `openTrade` (`V2EnterSignal`) |
|---|---|---|
| Size | `qty` (units) **or** `amount` + `amountCurrency` (cash, MARKET only) | `positionSize: { type, … }` (required) |
| Limit or trigger price | `limitPrice` (LIMIT), `triggerPrice` (MARKET_TRIGGER) | `price` |
| Stops | none | `stoploss`, `takeprofit` (and `2`…`4` variants) |
| Note | none (no `comment` field) | `comment` |

## Sizing an order

```ts fragment
// Units you computed:
manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', qty: 250 / price });
// Cash, when the price is unknown or irrelevant:
manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 250, amountCurrency: 'USD' });
// A resting limit:
manager.placeOrder({ symbol, type: 'LIMIT', position: 'buy', qty: 0.01, limitPrice: 58000 });
```

- **`amountCurrency` is required with `amount`** (TS9031) and refused without it (TS9032). Use `'USD'` unless the user sized in another currency. Backtests have no exchange rates: they treat `'USD'`, `'USDT'` and `'USDC'` as the same cash, which sizes one-to-one on US stocks, ETFs and dollar-quoted crypto. Any other currency (`'EUR'`) only sizes in demo and live runs, so such an agent cannot be backtested.
- **`amount` is for MARKET orders only.** For a LIMIT order compute `qty` from a price.
- **Symbols discovered at run time** (a ranking, a resolved ticker) have no price on the first cycle: size them with `amount`, and let the engine size the order at the first price. Do not skip the order because `getCurrentPrice` is `undefined`: the order is what subscribes the price.
- **Futures:** `qty` counts contracts; the engine applies the contract size to value and profit. Do not multiply `qty` by `contractSize`.

## Sizing a trade

`positionSize` is an object whose `type` names the field that follows:

```ts fragment
positionSize: { type: 'proportion', proportion: 0.1 } // 10% of the wallet's value opened
positionSize: { type: 'risk', risk: 0.01 }            // 1% of the wallet's value lost at the stop
positionSize: { type: 'amount', amount: 500 }         // $500 opened
positionSize: { type: 'qty', qty: 2 }                 // 2 units
```

`risk` and `proportion` are fractions; the validator refuses `risk` of 0.5 or more and `proportion` above 1 (TS9019). `risk` sizing divides by the stop distance: it needs a stop, and a very tight stop gives a very large position, which the wallet then refuses for lack of margin.

## Capital and auto-invest

A paper wallet (backtests and demo runs) exposes several capital figures, and they are not interchangeable:

| Method | What it is | Use it for |
|---|---|---|
| `getAvailableCapital()` | free cash: realized capital minus the cost of open positions (0 when fully invested) | "invest the remaining cash", clamping buys |
| `getCurrentCapital()` | realized capital; opening a position does **not** lower it | reporting realized results |
| `getLatentCapital()` | the whole wallet at market value | monitoring, weights, drawdowns |
| `getInvestedCapital()` | all money ever put in | return on invested capital |

A backtest wallet starts with the starting capital chosen when the backtest is launched (in USD), deposited on the first tick; a demo run uses the demo portfolio's cash and positions. `increaseCapital(amount)` adds to whatever is there, in the same tick: `getAvailableCapital()` includes it right away.

**Auto-invest.** When a plain `placeOrder` buys more than the wallet's cash, the paper wallet does not refuse it: it silently adds the missing money as new invested capital. This is what makes a recurring contribution work (a dollar-cost-averaging backtest starts with no cash: every buy brings its own money in), and it is also how a badly sized order inflates invested capital. One flag decides which behaviour you get, and one question decides the flag: **does this order spend money the wallet already has, or bring new money in?**

- **Spending existing cash** (a rebalance, "invest my remaining cash", a sell that reduces or closes a long): set `inhibitAutoInvest: true`. The validator requires it on any order sized from a capital reading (TS9017). `closePosition`, `closeTrade` and engine exits never auto-invest, whatever you set.
- **Bringing new money in** (a fixed contribution, a DCA buy, "invest $500 every month"): set nothing. The validator refuses `inhibitAutoInvest: true` on a fixed-size buy unless the agent funds itself with `increaseCapital` (TS9030), because with the flag the order is checked against a wallet that has no cash, and every buy is cancelled.

```ts fragment
// Fixed contribution: exactly $100, new money, no flag.
manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 100, amountCurrency: 'USD' });
// Spend what is free: clamp to the free cash, flag it.
const cash = Math.max(0, manager.getAvailableCapital());
if (cash > 1) manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: cash, amountCurrency: 'USD', inhibitAutoInvest: true });
```

A fixed amount per period is sized on that amount, never on `getAvailableCapital()`: on its first run the latter would buy all the idle cash, not $100.

**A budget of your own is not the wallet.** When the agent manages a sleeve ("a $2,000 basket") in a wallet that holds other money, `getAvailableCapital()` measures all free cash and `getLatentCapital()` the whole wallet. Track the sleeve yourself in the status bag: the amount invested, the symbols bought and the quantity your own fills bought of each (count them in `onOrderExecuted`), with the sleeve's value computed as the sum of that quantity × `getCurrentPrice(symbol)` over your symbols. `getPositionQty(symbol)` is the whole portfolio's position, so it also counts shares of the same symbol that are not the sleeve's.

## Rebalancing to target weights

Rebalance by the **difference** from target, never by buying each target in full: a portfolio's value includes what you already hold, so buying "value × weight" for every name re-buys it and exposure compounds at every rebalance. The validator refuses a buy loop sized from portfolio value without a cash clamp (TS9038). The correct cycle:

1. Resolve the targets. If the data is missing (a price, a ranking), abort the cycle and retry later; never trade on a partial picture.
2. Sell first: positions no longer targeted, and overweights down to target. Add up the estimated proceeds (`qty × price`) in the same loop that sells.
3. Compute the investable total as the value of the targets you hold plus the proceeds plus `getAvailableCapital()` (not floored at 0, so past overspending shrinks the targets), and keep 2 to 5% headroom for fees and slippage.
4. Buy each target's deficit only, every buy clamped by one shared budget that each buy decrements, starting at the free cash **read before the sells** plus the proceeds: `let remaining = Math.max(0, cash) + proceeds`, where `const cash = manager.getAvailableCapital()` was read before the first sell. (On paper a sell settles at once, so free cash read after the sells already contains their proceeds; live it does not yet. Reading it first is right in both.) Every order in the cycle sets `inhibitAutoInvest: true`.

The validator proves the budget from the shape of the code, so keep it recognisable: add each sell's proceeds (`qty × manager.getCurrentPrice(symbol)` of the sold line) in the same loop and branch that places that sell. A `closePosition` sell counts only under a `position.qty > 0` check (closing a short releases no cash). Sells planned into an array and executed in a later loop cannot be matched with their proceeds and are refused (TS9038).

The 60/40 example in [Schedules and launch actions](/docs/developers/stse/scheduling.md#acting-at-launch) and the top-10 example in [Examples](/docs/developers/stse/examples.md#dynamic-universe-top-10-by-market-cap) follow this shape.

On a live broker, sells do not settle instantly: the cash from a sell may not be usable by a buy placed in the same callback. Size buys from your own estimate of the proceeds (as above), and accept that a buy may be refused and caught up on the next cycle.

## Stops and take-profits

On a trade, a stop or take-profit `value` is an **absolute price**, never a distance:

```ts fragment
const atrValue = 120;
stoploss: { type: 'fixed', value: entry - 2 * atrValue },  // right: a price below the entry
stoploss: { type: 'fixed', value: 2 * atrValue },          // wrong: a stop at 240 on a $50,000 asset
```

- `'fixed'` stays where it is set. `'trailing'` keeps its initial distance (entry minus the initial stop, in price units) behind the highest high since entry (lowest low for a sell); `{ type: 'trailing', value: entry * 0.9 }` therefore trails 10% of the entry price below the best high. `'special'` behaves like `'fixed'`.
- Levels are placed from the price you pass, usually `getCurrentPrice(symbol)` when you call `openTrade`; a market entry fills at the next price plus spread, so the level is relative to the signal price, not to the fill. Read the fill from `trade.enterPrice` in `onTradeFilled` if you need to re-anchor a level with `updateStoploss`.
- Take-profit `'fixed'` exits at `value`; `'trigger'` arms when price reaches `trigger`, then exits at `value`; `'trailing'` arms at `trigger`, then trails `relative` behind the best price.
- `delta` (0 to 100) is the percentage of the **initial** position closed at that level; several levels (`stoploss2`, `takeprofit2`, …) scale out. Without `delta`, the level closes everything that is left. The stops keep covering whatever remains after a partial take-profit:

```ts fragment
takeprofit: { type: 'fixed', value: entry * 1.03, delta: 50 }, // half at +3%
takeprofit2: { type: 'fixed', value: entry * 1.06 },           // the rest at +6%
stoploss: { type: 'fixed', value: entry * 0.98 },              // covers whatever is still open
```
- A buy's stop must be below its entry (and a sell's above), or `openTrade` returns `undefined`.
- Do not also code the same stop in a callback: the engine already watches it.

**Moving a stop** uses the `updateStoploss` [helper](/docs/developers/stse/helpers.md) with the trade's **own** stop object (the validator refuses a fresh object, which would move nothing: TS9018). It never moves a buy stop above the current price.

```ts
// Move the stop to break-even once a trade is 2% in profit.
const symbol = 'SOL';
manager.onPreciseCandle(symbol, () => {});

manager.cron('*/5 * * * *', () => {
  const price = manager.getCurrentPrice(symbol);
  if (price === undefined) return;
  for (const trade of manager.getSymbolPendingTrades(symbol)) {
    if (trade.position !== 'buy' || trade.filledQty <= 0) continue; // entry not filled yet
    const stop = trade.stoploss;
    if (stop === undefined || stop.absolute >= trade.enterPrice) continue; // no stop, or already at entry
    if (price >= trade.enterPrice * 1.02) updateStoploss(manager, trade, stop, trade.enterPrice);
  }
});
```

**Reading a take-profit** requires narrowing on its `type` first: `value` exists on `'fixed'` and `'trigger'`, not on `'trailing'`. A stop's `initial` and `absolute` fields are readable without narrowing.

## Record why

Every `openTrade` should carry a `comment` naming the rule and the values that fired it, and every `closeTrade` a `cause`: they are the only trace of the agent's reasoning in the trades table. Keep them to one line.

```ts fragment
manager.openTrade({ symbol, position: 'buy', type: 'MARKET', positionSize: { type: 'proportion', proportion: 0.1 },
  comment: `EMA20 ${fast.toFixed(2)} crossed above EMA50 ${slow.toFixed(2)}` });
manager.closeTrade(trade, { cause: `RSI ${rsiValue.toFixed(1)} above 70` });
```

`closeTrade`'s second argument is `{ cause, delta? }` and nothing else. `placeOrder` has no comment field.

## Positions the agent did not open

An agent can manage what you already hold ("trail a stop on everything I own", "close all positions on Friday"):

- Declare no assets. Iterate `manager.getPositions()` each cycle; never snapshot the holdings into the code.
- Use `position.symbol` for trading and price calls and `position.ticker ?? position.symbol` in messages.
- The entry price is `manager.getAveragePrice(position.symbol)`; the direction is the sign of `position.qty`.
- These positions have no engine stop. Keep the stop level in the status bag, only ever tighten it, and exit with **`manager.closePosition(symbol)`** when price crosses it. Never exit with an opposite-side `placeOrder`: on a live account, if the position changed meanwhile, that order could open a reverse position; `closePosition` cannot.
- Before exiting, skip symbols that already have a pending exit order on the closing side, and forget a stored level only once the position is actually gone (a refused exit must fire again on the next tick).
- Run the check from a `manager.cron('* * * * *', …)`. It cannot be backtested (a backtest starts with an empty wallet): validate it on a demo portfolio that holds positions.

A complete example is in [Examples](/docs/developers/stse/examples.md#trailing-stop-on-positions-you-already-hold).

## Live accounts

- `LIMIT` and `MARKET_TRIGGER` orders exist only on paper for now; a live broker accepts `MARKET` only (a `LIMIT` is skipped: the call returns `undefined` and the journal says so; a `MARKET_TRIGGER` throws). To approach a price, watch the price and send a `MARKET` order when it reaches your level. That level is a trigger, not a limit: the order fills at what the market offers when it arrives, which can be worse than your level. For a stop on a live position, keep the level yourself and exit with `closePosition`, or use a trade's engine-watched stop.
- Engine stops are watched while the agent runs; they are not orders resting at the broker.
- Fills arrive asynchronously and can be partial. Confirm an exit with `onOrderExecuted` and the order's `filledQty`, not by a missing position alone.
- Size live orders from `getAvailableCapital()` or the relevant `getBalances()` row, never from `getReportedEquity()` or buying power.

# Schedules and launch actions

`manager.cron` is how an agent acts at a time of day, every week, on the first of the month. It has two properties that shape every scheduled agent: it **never fires when the agent starts**, and a missed occurrence (the agent was restarting) is **not replayed**. This page gives the patterns that stay correct despite both.

## Cron basics

```ts fragment
manager.cron('0 9 * * 1', callback, 'Europe/Paris');   // Mondays 09:00 Paris
manager.cron('31 9 * * 1-5', callback, 'America/New_York'); // weekdays 09:31 New York
manager.cron('0 10 1 * *', callback, 'America/New_York');   // the 1st of each month, 10:00
manager.cron('*/15 * * * *', callback);                      // every 15 minutes (zone irrelevant)
```

- Five fields: minute, hour, day of month, month, day of week (0 or 7 is Sunday). A sixth, leading, field adds seconds.
- The zone defaults to **UTC**. Pass the IANA zone of the wall-clock time you mean; daylight saving is handled.
- A schedule that trades US stocks must fire inside exchange hours, 09:30 to 16:00 New York: use `'31 9 * * 1-5'` for "at the open" and `'55 15 * * 1-5'` for "before the close". An order at 09:00 is pre-market.
- A schedule firing exactly on a candle boundary runs before that candle closes (see [the order of events](/docs/developers/stse/manager-api.md#how-the-engine-runs-your-agent)). Schedule one minute later if you need the closed candle.
- A callback that trades a symbol needs that symbol subscribed at the root (`manager.onPreciseCandle(symbol, () => {})`, or any candle series).

## Choosing a cadence

Derive the cadence from the strategy and round **up**: a monthly rebalance runs monthly, a daily signal daily. When the request states no frequency, use daily. A sub-daily cadence has to be earned by the request (an intraday signal, "check every 5 minutes").

Cadence decides what a backtest costs. In a backtest every schedule fire is a simulation step, around the clock, weekends included: a per-minute schedule is about half a million steps a year, and very long runs are refused at launch. Hourly and coarser schedules backtest over many years without trouble.

## Acting at launch

Because a schedule never fires at start-up, "buy now, then every Monday" needs two triggers: a launch catch-up and the recurring schedule. The catch-up is a per-minute cron that does nothing once its job is done. Two rules make the pair safe:

1. **Both triggers call the same function, and that function claims a period key** in the status bag before doing anything else, so the period runs exactly once whichever trigger fires first. Claim the key before the first `await`.
2. **The catch-up mirrors the schedule's own boundary** (same weekdays, same time of day, same zone) instead of latching on a persisted "initialized" flag. A latch survives restarts while schedules do not, so after a restart that falls past the scheduled time, a latched catch-up would skip that period forever. A boundary check has no such hole.

The key must change exactly as often as the schedule fires: a day key (`yyyy-LL-dd`) for a daily schedule, a week key for weekly, a month key (`yyyy-LL`) for monthly. Compute it in the schedule's zone from `manager.getCurrentTime()`.

Release the key when the cycle failed **before** doing anything visible (no order, notification, comment or capital change), so the next tick retries; once something went out, keep it claimed so a duplicate delivery cannot repeat it. When the cycle calls a paid model, stop retrying after a few failures in the same period.

```ts
// Buy $100 of BTC every Monday at 09:00 Paris, with the first buy right away.
const symbol = 'BTC';
const zone = 'Europe/Paris';
const WEEKLY_AMOUNT = 100;

interface DcaState { lastWeek?: string }

manager.onPreciseCandle(symbol, () => {}); // subscribe BTC prices for the schedules

// The period starts at the schedule's boundary, Monday 09:00 Paris, not at midnight: shift the
// clock back 9 hours before taking the ISO week, so Monday 08:59 still belongs to last week.
function weekKey(nowMs: number): string {
  return DateTime.fromMillis(nowMs).setZone(zone).minus({ hours: 9 }).toFormat("kkkk-'W'WW");
}

function runWeek(): void {
  const state = manager.getStatus<DcaState>('default');
  const key = weekKey(manager.getCurrentTime());
  if (state.lastWeek === key) return; // this week already bought
  state.lastWeek = key; // claimed before anything else
  manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: WEEKLY_AMOUNT, amountCurrency: 'USD' });
}

// Launch catch-up: fires within a minute of the start and buys if this week has not been
// bought yet; afterwards it finds the week claimed. Because the week key flips exactly at
// Monday 09:00, it cannot buy before the schedule does.
manager.cron('* * * * *', () => {
  runWeek();
});

manager.cron('0 9 * * 1', () => {
  runWeek();
}, zone);
```

The weekly buy above needs no separate boundary gate because its key flips at the schedule's own boundary (Monday 09:00 Paris), and "buy now" makes any launch time valid for the first buy. When the action must also happen inside certain hours (a market session), gate the catch-up on them, as in the next example.

```ts
// Rebalance SPY/TLT 60/40 on weekdays at 09:31 New York, and at launch once the market is open.
const zone = 'America/New_York';
const TARGETS: Record<string, number> = { SPY: 0.6, TLT: 0.4 };
const symbols = Object.keys(TARGETS);
for (const s of symbols) manager.onPreciseCandle(s, () => {});

interface CycleState { lastDay?: string }

function pastOpen(nowMs: number): boolean {
  const ny = DateTime.fromMillis(nowMs).setZone(zone);
  if (ny.weekday > 5) return false;
  const minutes = ny.hour * 60 + ny.minute;
  return minutes >= 9 * 60 + 31 && minutes < 16 * 60;
}

function runDay(): void {
  const state = manager.getStatus<CycleState>('default');
  const day = DateTime.fromMillis(manager.getCurrentTime()).setZone(zone).toFormat('yyyy-LL-dd');
  if (state.lastDay === day) return;
  const previous = state.lastDay;
  state.lastDay = day;

  const values = new Map<string, number>();
  let held = 0;
  for (const s of symbols) {
    const price = manager.getCurrentPrice(s);
    if (price === undefined || price <= 0) {
      state.lastDay = previous; // nothing done yet: release and retry next minute
      return;
    }
    const value = manager.getPositionQty(s) * price;
    values.set(s, value);
    held += value;
  }

  // Read the free cash once, before selling: paper sells settle at once (reading it after them
  // would count their proceeds twice), live sells do not (the proceeds are not there yet).
  const cash = manager.getAvailableCapital();
  // Sell overweights first and count the proceeds, then buy deficits within one cash budget.
  const total = (held + cash) * 0.98;
  let proceeds = 0;
  for (const s of symbols) {
    const price = manager.getCurrentPrice(s);
    const excess = (values.get(s) ?? 0) - total * (TARGETS[s] ?? 0);
    if (price === undefined || excess <= 1) continue;
    const sellQty = excess / price;
    manager.placeOrder({ symbol: s, type: 'MARKET', position: 'sell', qty: sellQty, inhibitAutoInvest: true });
    proceeds += sellQty * price; // counted next to the sell, as the validator expects
  }
  let remaining = Math.max(0, cash) + proceeds;
  for (const s of symbols) {
    const deficit = total * (TARGETS[s] ?? 0) - (values.get(s) ?? 0);
    const spend = Math.min(deficit, remaining);
    if (spend <= 1) continue;
    manager.placeOrder({ symbol: s, type: 'MARKET', position: 'buy', amount: spend, amountCurrency: 'USD', inhibitAutoInvest: true });
    remaining -= spend;
  }
}

manager.cron('* * * * *', () => {
  if (!pastOpen(manager.getCurrentTime())) return;
  runDay();
});
manager.cron('31 9 * * 1-5', () => {
  runDay();
}, zone);
```

The per-minute catch-up has a cost: in a backtest every minute becomes a simulation step, so a backtest longer than about a year may be refused at launch. When multi-year backtests matter more than acting in the first minute, drop the catch-up and let the schedule make the first move.

Test your design with one question: if you deleted the per-minute catch-up, would the agent still do the same thing, just later? If not, the two triggers are doing different jobs. In particular, never pass the shared function an "is this the launch?" flag that changes what it does: the catch-up fires first in every period, so the other branch would never run.

## One-time actions inside a recurring cycle

A lump sum invested once, a one-off initial transfer: give it its own lifetime flag in the status bag, checked **inside** the shared cycle, separate from the period key.

```ts fragment
interface MyState { lastMonth?: string; seeded?: boolean }
// inside the cycle, after claiming the month:
if (state.seeded !== true) {
  manager.increaseCapital(10000); // the initial lump sum, once in the agent's life
  state.seeded = true;
}
```

## Calendar rules cron cannot express

When a schedule restricts both the day of the month and the day of the week, cron fires when **either** matches: `'0 9 1-7 * 1'` runs on days 1 to 7 **and** on every Monday. For "the first Monday of the month", schedule every Monday and check the date in code:

```ts fragment
manager.cron('0 9 * * 1', () => {
  const today = DateTime.fromMillis(manager.getCurrentTime()).setZone('America/New_York');
  if (today.day > 7) return; // not the first Monday
  runMonthlyCycle();
}, 'America/New_York');
```

The same approach covers "the last trading day of the month" (check that the next weekday is in another month) and "every other week" (check the ISO week number). Cron has no holiday calendar: on an exchange holiday a scheduled order simply finds the market closed, so prefer checking that the symbol traded recently (`hasPriceData`, a fresh candle) before acting when that matters.

## Market hours for scheduled orders

Crons fire around the clock. For stocks and ETFs, gate every order-placing path (catch-ups and retries included) on the exchange session, as `pastOpen` does above. Crypto trades 24/7 and needs no gate.

## Schedules and AI calls

A `manager.llm` call reached from a schedule or candle callback must be guarded by a period key read from `manager.getStatus` in the same function, claimed before the call (the validator enforces this: TS9026). A candle or a period can be delivered twice; without the key you pay for the same analysis twice. Event callbacks (`onMessage`, `onNews`, `onPieuvreEvent`) fire once per event and need no key.

```ts
const zone = 'America/New_York';
interface BriefState { lastDay?: string; failures?: number }
const briefSchema = z.object({ summary: z.string().describe('Three sentences on today\'s market mood.') });

manager.cron('0 8 * * 1-5', async () => {
  const state = manager.getStatus<BriefState>('default');
  const day = DateTime.fromMillis(manager.getCurrentTime()).setZone(zone).toFormat('yyyy-LL-dd');
  if (state.lastDay === day) return;
  const previous = state.lastDay;
  state.lastDay = day; // claimed before the call
  let summary: string | undefined;
  try {
    const result = await manager.llm('default', 'You write short, factual market briefs.', `Brief for ${day}.`, briefSchema, { news: true });
    summary = result?.summary;
  } catch (error) {
    manager.handleError(error);
  }
  if (summary === undefined) {
    state.failures = (state.failures ?? 0) + 1;
    if (state.failures < 3) state.lastDay = previous; // nothing sent: allow a retry
    return;
  }
  state.failures = 0;
  manager.sendNotification(`**Morning brief, ${day}**\n\n${summary}`);
}, zone);
```

# News, data and AI

Beyond prices, an agent can read news, follow accounts, query fundamental and macro data, and call a language model. This page covers when to use each and the patterns that keep them correct and affordable. Signatures and return shapes are in the [Manager API](/docs/developers/stse/manager-api.md#communication-and-analysis) and [Types](/docs/developers/stse/types.md).

## Code first, the model for what code cannot express

Prefer deterministic code whenever the trigger and the action are a computation over structured data: prices, indicators, positions, schedules, fundamentals. It is cheaper, repeatable and usually backtestable. Reach for `manager.llm` only when the value of the agent is the reasoning itself: summarising, explaining, classifying free text, judging a post, choosing assets from open research.

Before adding a model call, ask:

- **Did the user leave it a job?** "Every time @account posts, buy" states both the trigger and the action: wire them directly. A model earns its place when the request leaves something to interpret ("when the tone is bearish").
- **Does removing it change what the agent does?** If the orders and notifications would be identical without it, it only adds cost and a failure mode.
- **Who decides?** When the model decides (picks assets, classifies), a missing answer must abort the action. When the code decides (a price alert, a scheduled rebalance), a failed model call must never cancel the action: act, and add the model's text only when it arrived.

## News

**Poll news on a schedule with `searchNews`.** It searches the whole news store (financial press and monitored posts) and returns structured items you filter yourself.

```ts
// Every 15 minutes, one digest of new NVIDIA and AMD headlines, never the same story twice.
interface NewsState { since?: number; seen?: string[] }
const QUERIES = ['NVDA NVIDIA', 'AMD Advanced Micro Devices'];

manager.cron('*/15 * * * *', async () => {
  const state = manager.getStatus<NewsState>('default');
  const now = manager.getCurrentTime();
  const since = state.since ?? now - 15 * 60 * 1000;
  state.since = now; // advance the window before processing
  const seen = state.seen ?? [];
  const lines: string[] = [];
  for (const query of QUERIES) {
    try {
      const items = await manager.searchNews(query, { since, limit: 10 });
      for (const item of items) {
        if (seen.includes(item.id)) continue;
        seen.push(item.id);
        lines.push(`**${item.title}** (${item.source}) ${item.url}`);
      }
    } catch (error) {
      manager.handleError(error); // news service unavailable: skip this query
    }
  }
  state.seen = seen.slice(-200);
  if (lines.length > 0) manager.sendNotification(`New headlines:\n\n${lines.join('\n')}`);
});
```

- One query per company or theme: a query mixing dozens of names finds less.
- Each query is billed; a very frequent schedule times many tickers adds up.
- Send one digest per run, never one notification per item.
- `searchNews` rejects when the service is down; `[]` means nothing matched.

**`onNews` pushes one precise event** as it breaks, with no schedule: `manager.onNews({ source: 'x', matchPrompt: 'A major crypto exchange halts withdrawals' }, (news) => …)`. It is fed only by posts from monitored X accounts, never by press articles, so an event that surfaces as press coverage (an earnings release, a regulator's ruling) belongs to the `searchNews` pattern. The match is already judged when the callback runs: act on `news.content` directly.

News agents are not backtestable; validate them in a demo run.

## Following an account or a channel

`manager.onMessage` fires for every post of one X account or public Telegram channel, with no filtering: filter in code.

```ts
// Buy $50 of DOGE each time @elonmusk mentions dogecoin.
const symbol = 'DOGE';
manager.onPreciseCandle(symbol, () => {});

manager.onMessage({ source: 'x', handle: 'elonmusk' }, (message) => {
  if (!/doge/i.test(message.content)) return;
  manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 50, amountCurrency: 'USD' });
});
```

- `handle` is the username without `@`; keep it a literal so the agent stays backtestable (past posts are replayed at their original time).
- `message` is `{ content, author, url, publishedAt, media }`. `media` ids can be passed to `manager.llm(..., { images })` when the pictures matter; never print them.
- An X follow bills per delivered post, whether or not your code acts on it. Telegram is free but not backtestable.
- `manager.llm` is allowed in this callback (there is no upstream filter), and it is the one place a model call works in a backtest.

## Fundamental and macro data

| Need | Method | Backtestable |
|---|---|---|
| One number for one company (P/E, revenue, margin) | `getPieuvreMetric(concept, { entity })` | yes, point in time |
| A ranked universe (top 10 by market cap) | `getPieuvreRank({ metric, universe, order, limit })` | yes, point in time |
| A macro time series (rates, fear and greed) | `getPieuvreSeries(sourceId, { from, to })` | no |
| What is scheduled (earnings, CPI, Fed) | `getPieuvreUpcoming({ withinMs, eventTypes, tickers })` | no |
| React when a release lands | `onPieuvreEvent({ matchPrompt, sourceIds, minSeverity }, cb)` | no |

Rules that avoid silent failures:

- **Read `.value`.** `getPieuvreMetric` returns an object (`{ value, unit?, period?, asOf, sourceId }`) or `undefined`; comparing the object itself to a number never matches.
- **Ratios are fractions.** Margins, yields and returns come back as `0.20` for 20%. Compare against `0.20`. The list is in [Types](/docs/developers/stse/types.md#metrics-returned-as-fractions).
- **Use real metric names.** `getPieuvreRank` accepts only the [listed names](/docs/developers/stse/types.md#metric-names) (the validator checks literals: TS9027 and the type). There is no RSI or volume-spike ranking: compute those from candles.
- **Use real universe tags**, such as `'sp500'`, `'us_equities'`, `'eu_equities'`, `'asia_equities'`, `'crypto_top30'`. An unknown tag returns `[]` without error.
- **Trade rank entries by their key**: `entry.snaptradeSymbol ?? entry.backtestSymbol`, skipping entries where both are `null`, and size with a cash `amount`. Name them to users by `entry.ticker`.
- **An outage is not an empty answer.** These reads reject when the data service is down. Catch, and skip the cycle: never treat an error as "no constituents" (a rebalancer would sell everything).
- **Narrow the calendar.** `getPieuvreUpcoming` without `tickers` or `eventTypes` returns thousands of rows on a busy day and the ones you want may be cut. For macro releases read `sourceIds: ['eodhd.economic_calendar'], eventTypes: ['economic_release']` and classify by `payload.country` and `payload.type` (for example `'Inflation Rate'`, `'Non Farm Payrolls'`, `'Fed Interest Rate Decision'`).
- **A calendar ticker is not a trading key.** To trade it, map it onto a declared symbol, or resolve it with `resolveAssets`.

```ts
// React to a US CPI surprise: buy TLT when inflation prints below consensus, sell what the agent bought when above.
const symbol = 'TLT';
manager.onPreciseCandle(symbol, () => {});

manager.onPieuvreEvent({ matchPrompt: 'US CPI inflation release', sourceIds: ['eodhd.economic_calendar'], minSeverity: 'medium' }, (event) => {
  const actual = event.payload?.['actual'];
  const estimate = event.payload?.['estimate'];
  if (typeof actual !== 'number' || typeof estimate !== 'number') return;
  const open = manager.getSymbolPendingTrades(symbol);
  if (actual < estimate && open.length === 0) {
    manager.openTrade({ symbol, type: 'MARKET', position: 'buy', positionSize: { type: 'amount', amount: 2000 }, comment: `CPI ${actual} below the ${estimate} consensus` });
  } else if (actual > estimate) {
    for (const trade of open) manager.closeTrade(trade, { cause: `CPI ${actual} above the ${estimate} consensus` });
  }
});
```

## Calling a language model

`manager.llm(name, systemPrompt, userPrompt, schema, options?)` returns an object matching a `z` schema, or `undefined`. The patterns:

- **Guard the result.** `undefined` covers every failure. Validate the whole answer before acting on any of it; never interleave validation and orders.
- **Hard numbers come from feeds, never from the model.** Read prices with `getCurrentPrice` or the model's `marketData` tool, fundamentals with `getPieuvreMetric`. Any number the model may not know must be `.nullable()` in the schema (a required `z.number()` forces it to invent one); check for `null` before comparing, and reject values far from a real reference.
- **Cap lists in code.** If the request says "3 stocks", `slice(0, 3)` the model's list.
- **Trade only keys you can trust:** a declared symbol, a rank entry's key, or a `tradingKey` the model copied from its `search_assets` tool (with `marketData: true`). Never trade a ticker string the model wrote.
- **Every schema key is required;** make optional ones `.nullable()`. The result type is inferred; to name it, write `type Verdict = z.infer<typeof verdictSchema>`.
- **On a schedule or candle, claim a period key first** (TS9026, see [Schedules](/docs/developers/stse/scheduling.md#schedules-and-ai-calls)).
- **Facts first.** When the code decides a notice is due, the code writes its facts (what, when, the figures) and the model only adds analysis below them. If the call fails, send the facts alone.
- **Tools:** `webSearch` (open web, billed), `news` (Obside news search, billed), `pieuvre` (fundamentals and the economic calendar), `marketData` (quotes, daily history, asset resolution). Enable only what the task needs.
- **Limits:** 12 model turns per call and a per-call spend cap; beyond them the result is `undefined`. Out of AI usage in a live or demo run, the call also resolves to `undefined` until the monthly reset; the agent keeps running and keeps its positions.
- **Backtests:** a model call works only inside an `onMessage` callback. An agent calling a model from a schedule or candle is validated in a demo run.

```ts
// Alert when NVDA closes below a level, with a short AI explanation under the facts.
const symbol = 'NVDA';
const zone = 'America/New_York';
const level = manager.getVariable('level', 120);
const daily = manager.getIntervalData(symbol, 1440, zone);

interface AlertState { lastCloseTime?: number; below?: boolean }
const explanationSchema = z.object({
  explanation: z.string().describe('Two sentences on what likely drove the move. No prices or dates.'),
});

manager.onIntervalCandle(symbol, 1440, async () => {
  if (!daily.hasValue()) return;
  const candle = daily.getValue();
  const state = manager.getStatus<AlertState>('default');
  if (state.lastCloseTime === candle.closeTime) return;
  state.lastCloseTime = candle.closeTime; // claimed before any await
  const below = candle.close < level;
  if (!below || state.below === true) {
    state.below = below; // a close back above re-arms the alert
    return;
  }
  state.below = true;
  const day = DateTime.fromMillis(candle.openTime).setZone(zone).toFormat('yyyy-LL-dd');
  const facts = `**NVDA** closed at **$${candle.close.toFixed(2)}** on ${day}, below your $${level} level.`;
  let explanation: string | undefined;
  try {
    const result = await manager.llm('default', 'You explain a stock move in two sentences. Never state a price or a date.', `NVDA closed at ${candle.close.toFixed(2)} on ${day}. Explain the session.`, explanationSchema, { news: true });
    explanation = result?.explanation.trim();
  } catch (error) {
    manager.handleError(error);
  }
  manager.sendNotification(explanation === undefined || explanation === '' ? facts : `${facts}\n\n${explanation}`);
}, zone);
```

The model id: when you hand code to `create_agent`, Obside's builder sets it (name the model you want in the prompt). Any string passes validation, so `'default'` is a clear placeholder in a draft.

## Notifications, journal and webhooks

- **Notify only when asked.** An agent the user did not ask to alert them stays silent.
- **One notification per callback run.** Collect lines in an array and send one digest; a notification sent from inside a loop is refused when the agent is built.
- **Say what happened, with runtime values:** the asset, the quantity or amount, the price. Bold the one or two key figures with `**…**`.
- **Alert once per change of state.** Latch the state in the status bag so a condition that stays true does not alert on every tick, and re-arm when it clears.
- **Never show internal identifiers**: trading keys (from rankings, resolved assets or held positions), data-source ids, media ids. Name assets by your symbol, `ticker` or `name`.
- `manager.comment(text)` writes to the agent's journal (live and demo runs), the right place for an autonomous agent's reasoning. `manager.debugLog` is for backtests only.
- `manager.sendWebhook(payload)` posts JSON to the URL the user configured; it does nothing without one and never throws. Keep payloads small and stable. It does nothing in a backtest.

# Validation rules

Before an agent is saved or run, its code passes a deterministic checker: the TypeScript compiler in strict mode, plus a set of Obside rules. The checker never runs your code. It reads it, and it either passes or returns precise errors (with a code, a line and a column) for you to fix. When you connect a coding assistant over MCP, the `validate_agent_code` tool runs exactly this checker, so you can iterate until it passes before creating anything. The same checker runs again when `create_agent` receives code.

This page is the contract: what agent code may and may not do, and why. It lists every rule the checker enforces, each with a refused example and the accepted rewrite. If the checker rejects your code, the fix is always to correct the code, never to silence the checker.

There are no warnings: every error, from the compiler or from a rule, blocks the save. `validate_agent_code` returns `{ valid: true }` or `{ valid: false, errors: [{ code, message, line, column }] }`, with lines and columns counted from the first line of your code. Code larger than 60 KB (UTF-8) is refused before it is compiled.

Every example on this page is a complete agent in the submitted form, and each one is compiled by the real checker as part of Obside's test suite: blocks marked "refused" fail with the code named, plain blocks pass.

## What agent code may do

- Read market data through the manager (`getIntervalData`, `getCurrentPrice`, `getSymbolMetadata`) and build indicators with `new`.
- Attach listeners (`onIntervalCandle`, `cron`, `onNews`, and the others) at the root, and act inside their callbacks.
- Place and cancel orders, open and close trades, manage positions, read the account book, and keep state in `getStatus`.
- Send notifications, write journal comments, post to the configured webhook, and call a language model for structured reasoning.
- Use the `z` schema helper and the Luxon date classes, which are already in scope.

## What agent code may not do

Each of these is rejected with a clear message. The rule catalog below gives the code and an example for each.

- **No imports.** Everything you can use is already in scope. Writing `import` (or `require`) is an error (TS9007), and it collides with the names the engine already provides.
- **No wall-clock time.** `new Date()`, `Date.now()`, and `DateTime.now()` / `.local()` / `.utc()` are rejected (TS9010), because they would make backtests non-deterministic. Use `manager.getCurrentTime()`, or a candle's `openTime` / `closeTime`. Build a date from engine time with `DateTime.fromMillis(manager.getCurrentTime())`.
- **No direct network or I/O.** `fetch`, `WebSocket`, `XMLHttpRequest` and similar are rejected (TS9014). Outbound communication goes through the manager (`searchNews`, `llm`, `onNews`, `sendWebhook`).
- **No timers.** `setTimeout` and `setInterval` are rejected (TS9006): schedule with `manager.cron`.
- **No dynamic code.** `eval` and the `Function` constructor are rejected (TS9015, TS9039), because they would run text as code and defeat the checks.
- **No host environment.** The host runtime is not exposed: `process` (TS9016), the global object, and reflection over the prototype chain (TS9039) are all unavailable. Work with plain values and the manager API. This keeps agent code to a clear, reviewable surface.
- **No silencing the types.** `as any`, a `: any` annotation, casting through `unknown`, and the `@ts-ignore` family are rejected (TS9025). If the compiler says a property does not exist or a value is not assignable, the name or the value is wrong. Fix it.
- **Indicators at the root, actions in callbacks.** Constructing an indicator in a callback (TS9001), creating candle data in a callback (TS9002), reading live data or placing an order at the top level (TS9003), and attaching a listener inside another listener (TS9004) are rejected. Set up at the root; read and act inside callbacks.
- **No wrapper.** Submit the body only, not `async (manager) => { … }` (TS9035).

## Rules that protect your results

These are correctness rails specific to trading, not general programming errors. The checker catches the most damaging ones.

- **Order sizing.** `risk` and `proportion` are fractions of capital (`0.01` is one percent), and the checker rejects an implausibly large fraction (TS9019). A stop-loss or take-profit `value` is an absolute price, not a distance.
- **Order denomination.** A cash `amount` order must state its `amountCurrency` (for example `'USD'`) (TS9031), and `amountCurrency` is only valid with `amount` (TS9032). This keeps a dollar notional meaning the same thing in a backtest and live.
- **Existing cash versus new money.** An order sized from the wallet's capital must set `inhibitAutoInvest: true` (TS9017); a fixed contribution buy must not (TS9030). See [Orders, trades and risk](/docs/developers/stse/orders-and-risk.md#capital-and-auto-invest).
- **Capital sizing.** Reading the whole wallet's value and buying that amount every cycle silently compounds your exposure. The right shape trades each position's difference from its target and clamps every buy by the free cash. The checker flags the runaway shape (TS9038).
- **Time zones.** A daily-or-coarser candle listener must state the asset's time zone, so its daily boundary is unambiguous (TS9017).
- **Paid AI calls.** A `manager.llm` call on a schedule or candle must be guarded by a key saved in the status bag, so a repeated tick never pays twice (TS9026).
- **Reading the right bar.** A daily series holds completed days only, so inside an intraday callback the last element of its `History` is yesterday. Reading only an older element takes the day before yesterday for yesterday (TS9040).
- **Exits that work.** A take-profit or stop-loss `delta` is a whole percentage (TS9041), and a stop trailed from the current price only ever tightens (TS9042).
- **Your agent's own position.** A demo or live portfolio is shared with your own holdings and your other agents, so a strategy decides "am I in a position?" on its own trades or fills, never on `getPositionQty`, and sells only what it bought (TS9047). Its own trades exist only if it opens them with `openTrade` (TS9048).

## Why a checker at all

Two reasons. First, safety: agent code runs on Obside's infrastructure, so it is held to a small, reviewable surface with no access to the host or the network except through the manager. Second, reliability: strict typing and the trading rails catch the mistakes that would otherwise turn into a silent zero-return backtest or an order that never fills. The checker is fast and free to run, so treat a rejection as useful feedback and iterate.

## How your code is compiled

### Compiler settings

| Setting | Value | What it means for you |
|---|---|---|
| `target` | `ES2020` | The standard library is ES2020. Newer methods do not exist on the types (see below). Newer syntax such as `??=` is fine: it is compiled down. |
| `module` / `moduleResolution` | `NodeNext` | Top-level `await` is allowed. |
| `strict` | `true` | Enables the whole strict family: `strictNullChecks`, `noImplicitAny`, `noImplicitThis`, `strictFunctionTypes`, `strictBindCallApply`, `strictPropertyInitialization`, `alwaysStrict`, `useUnknownInCatchVariables`. |
| `skipLibCheck` | `true` | Only your code is reported. |
| `lib` | not set | The default library for ES2020 applies, which includes browser (DOM) typings. That is why `setTimeout`, `fetch` or `window` type-check; Obside rules refuse them (TS9006, TS9014, TS9039). |
| Node.js typings | not installed | `require`, `process` and other Node globals do not exist: they fail with TS2591 on top of the Obside rule. |

What strict mode means in practice:

- A value that can be `undefined` must be checked before use. `manager.getCurrentPrice()` returns
  `number | undefined`, and `indicator.getValue()` returns `T | undefined` unless you narrowed the
  indicator with `hasValue()` first.
- Every parameter of a function you declare needs a type. Callback parameters of `manager` APIs
  are typed for you.
- A variable declared without a type and without an initializer (`let last;`) becomes an implicit
  `any` and is refused when used in a closure.
- A `catch (error)` variable is `unknown`. Narrow it (`error instanceof Error`) before reading
  `.message`.

Options that are NOT enabled: `noUncheckedIndexedAccess` (so `closes[0]` is `number`, not
`number | undefined`), `noUnusedLocals`, `noUnusedParameters`, `noImplicitReturns`. Unused
variables do not fail the check.

Two consequences of the code being compiled as a module:

- Top-level `await` works: `const metadata = await manager.getSymbolMetadata('AAPL');`.
- A top-level `return` does not: it fails with TS1108. Wrap the code in an `if` instead.

### What ES2020 gives you

These all pass:

```ts
manager.cron('0 * * * *', async () => {
    const settled = await Promise.allSettled([manager.searchNews('AAPL Apple')]);
    const pairs = Object.fromEntries([['a', 1]]);
    const flat = [[1], [2]].flat();
    const digits = [...'a1b2'.matchAll(/\d/g)].map((m) => m[0]);
    const big = BigInt(10) * BigInt(2);
    const padded = '7'.padStart(3, '0').trimEnd();
    let counter: number | undefined;
    counter ??= 0;
    const nested: { a?: { b?: number } } = {};
    manager.debugLog(settled.length, pairs, flat, digits, big.toString(), padded, counter, nested.a?.b ?? 0);
});
```

These are newer than ES2020 and fail with TS2550 ("Do you need to change your target library?"):

| Not available | Added in | Use instead |
|---|---|---|
| `Array.prototype.at` | ES2022 | `arr[arr.length - 1]` |
| `String.prototype.replaceAll` | ES2021 | `s.replace(/-/g, '_')` or `s.split('-').join('_')` |
| `Object.hasOwn` | ES2022 | `({}).hasOwnProperty.call(obj, key)` (and `Object.hasOwn` is also refused by TS9039) |
| `Array.prototype.findLast` / `findLastIndex` | ES2023 | `[...arr].reverse().find(...)` or a reverse `for` loop |

### Names already in scope

The following names are pre-declared for your code. Use them directly. Do not import them (TS9007)
and do not declare anything with the same name: `const RSI = 14` or `interface News {}` fails. A
clash with one of these names is reported as a single error with code `0` whose message names the
clashing identifier (see "Redeclaring a pre-declared name" below).

- **`manager`**: the agent API (type `V2Manager`).
- **Indicators**: `Aroon`, `AroonOscillator`, `AwesomeOscillator`, `BalanceOfPower`, `BullBearPower`, `BollingerPercentB`, `ConnorsRSI`, `CoppockCurve`, `DMI`, `FisherTransform`, `KnowSureThing`, `MassIndex`, `PriceOscillator`, `SMIErgodicIndicator`, `SMIErgodicOscillator`, `TrueStrengthIndex`, `StochasticMomentumIndex`, `UltimateOscillator`, `Vortex`, `UlcerIndex`, `HistoricalVolatility`, `AverageDailyRange`, `AdvanceDeclineRatioBars`, `ChopZone`, `PringsSpecialK`, `RCIRibbon`, `RankCorrelationIndex`, `WoodiesCCI`, `ADX`, `BBW`, `BBWP`, `BeepBoop`, `Bollinger`, `BosNChoch`, `CCI`, `Chop`, `Choppy`, `ChaikinMoneyFlow`, `ChandeMomentumOscillator`, `Crossover`, `DC`, `Divergences`, `DPO`, `EIT`, `Elder`, `EMA`, `Engulfing`, `Extreme`, `FindMarketBottom`, `FIR`, `FVG`, `ChaikinOscillator`, `EaseOfMovement`, `EldersForceIndex`, `ALMA`, `DEMA`, `TEMA`, `LSMA`, `Median`, `Envelope`, `MovingAverageCross`, `BBTrend`, `ChandeKrollStop`, `ChandelierExit`, `HalfTrend`, `HARSI`, `Heikinashi`, `HeikinashiCandles`, `History`, `HullMovingAverage`, `Ichimoku`, `Index`, `KAMA`, `KeltnerChannels`, `KDJ`, `LinearRegression`, `Lunar`, `MACD`, `MarketStructure`, `MFI`, `KlingerOscillator`, `Momentum`, `MVWAP`, `OnBalanceVolume`, `OrderBlocks`, `Percentile`, `PercentageVolumeOscillator`, `PivotPoints`, `PMAR`, `BollingerBars`, `PivotsHighLow`,
  `PMARP`, `Renko`, `RenkoAtr`, `RMA`, `ROC`, `RSI`, `RSILambda`, `SAR`, `SMA`, `SqueezeMomentum`,
  `Stochastic`, `StrongCandleStructure`, `SuperTrend`, `SupportResistance`, `Swings`, `TR`, `TRIX`,
  `TTS`, `VIDYA`, `VolumeProfile`, `VWAP`, `VWMA`, `WilliamsAlligator`, `WilliamsFractal`, `WMA`,
  `WilliamsPercentR`, `ZigZag`.
- **Indicator building blocks**: `CandleIndicator`, `DerivedIndicator`, `IteratorIndicator`,
  `LambdaIndicator`, `ManualIndicator`, `PartialIndicator`, `PineIndicator`.
- **Helpers**: `updateStoploss`, `rebalance`, `ensureSymbolSingleTrade`, `up`, `down`,
  `calculateFibonacciRetracements`, `generateStoploss`, `getStoploss`, `getStoplossDistance`,
  `getStoplossRisk`, `generateTakeprofit`, `stoplossKeys`, `stoplossMainKey`, `takeprofitKeys`.
- **Libraries**: `DateTime`, `Duration` and `Interval` from Luxon; `z` from Zod. Note that
  `Interval` is Luxon's class. The engine's candle interval type is named `StseInterval`.
- **Types**: `AltDataPoint`, `ResolveAssetsRequest`, `ResolvedAsset`, `Candle`, `OpenedCandle`,
  `Indicator`, `IDerivedIndicator`, `ICandleIndicator`, `StseInterval`, `LatentData`,
  `OrderMetrics`, `TradeMetrics`, `CapitalMetrics`, `SwapFeesMetrics`, `LatentMetrics`,
  `WalletMetrics`, `ModelMetrics`, `Metrics`, `BaseNewsRequest`, `XNewsRequest`, `NewsRequest`,
  `News`, `CreateOrder`, `Order`, `PieuvreSeverity`, `PieuvreDataType`, `PieuvreEventRequest`,
  `PieuvreEvent`, `PieuvreMetricQuery`, `PieuvreMetric`, `PieuvreSeriesQuery`,
  `PieuvreSeriesPoint`, `PieuvreRankRequest`, `PieuvreRankedEntity`, `PieuvreUpcomingRequest`,
  `Position`, `ProportionPositionSize`, `RiskPositionSize`, `AmountPositionSize`,
  `QtyPositionSize`, `PositionSize`, `V2EnterSignal`, `LeaveSignal`, `Status`, `FixedStoploss`,
  `TrailingStoploss`, `SpecialStoploss`, `Stoploss`, `FixedStoplossRequest`,
  `SpecialStoplossRequest`, `TrailingStoplossRequest`, `StoplossRequest`, `StoplossKey`,
  `SymbolMetadata`, `FixedTakeprofit`, `TriggerTakeprofit`, `TrailingTakeprofit`, `Takeprofit`,
  `FixedTakeprofitRequest`, `TriggerTakeprofitRequest`, `TrailingTakeprofitRequest`,
  `TakeprofitRequest`, `TakeprofitKey`, `Exit`, `Slippage`, `Trade`, `V2Manager`.

## Obside rules (TS90xx)

### Vocabulary

- **Root scope**: the top level of your code, which runs once when the agent is built.
- **Callback**: the function you pass to one of the listener methods: `onPreciseCandle`,
  `onIntervalCandleOpen`, `onIntervalCandle`, `onAltData`, `onAnyData`, `cron`, `onTradeFilled`,
  `onTradeFinished`, `onOrderExecuted`, `onOrderCanceled`, `onNews`, `onPieuvreEvent`,
  `onMessage`.
- **Periodic callback**: a callback that fires on a clock or on market data rather than on a
  specific event: `onPreciseCandle`, `onIntervalCandleOpen`, `onIntervalCandle`, `onAnyData`,
  `cron`.
- Scope rules follow calls: a helper function called from a callback counts as running in that
  callback, and a helper called from the root counts as running at the root. The message then
  names the helper.

### Live codes

| Code | Rule |
|---|---|
| TS9001 | Indicators are created at the root |
| TS9002 | `getIntervalData` (and `getSymbolMetadata` in periodic callbacks) is called at the root |
| TS9003 | Live-data and trading calls run inside a callback |
| TS9004 | Listeners are attached at the root |
| TS9006 | No `setTimeout` / `setInterval` |
| TS9007 | No `import` / `require` / `import()` |
| TS9008 | `manager` cannot be redeclared or reassigned |
| TS9010 | No wall-clock time |
| TS9014 | No direct network access |
| TS9015 | No `eval` / `Function` |
| TS9016 | No `process` |
| TS9017 | Daily-or-higher interval listeners pass a time zone; capital-sized orders set `inhibitAutoInvest: true` (two rules share this code) |
| TS9018 | `updateStoploss` receives the trade's own stop |
| TS9019 | Position size is a plausible fraction of capital |
| TS9020 | Variable names are string literals |
| TS9021 | Variable defaults are literals |
| TS9022 | One default per variable |
| TS9023 | At most 16 variables |
| TS9024 | `getVariable` is called directly |
| TS9025 | No `any`, no `as unknown`, no `@ts-ignore` |
| TS9026 | LLM calls in periodic callbacks are guarded by a stored key |
| TS9027 | `getPieuvreRank` requests use known fields and values |
| TS9028 | Every `getVariable` mention yields a variable |
| TS9029 | `handleError` receives an error, not a promise |
| TS9030 | A contribution buy does not set `inhibitAutoInvest: true` |
| TS9031 | Cash-sized orders state `amountCurrency` |
| TS9032 | `amountCurrency` only with `amount` |
| TS9033 | Your functions type their parameters |
| TS9034 | `getStatus` takes a state type when you read values |
| TS9035 | No wrapper function around the code |
| TS9036 | Whole-number indicator arguments get whole-number defaults |
| TS9037 | Order payloads are visible to the check |
| TS9038 | Rebalancing buys are clamped by a cash budget |
| TS9039 | No access to the host environment |
| TS9040 | A daily-or-higher `History` is read at its last element for the previous bar |
| TS9041 | A take-profit or stop-loss `delta` is a whole number from 0 to 100 |
| TS9042 | A stop trailed from the current price only tightens |
| TS9047 | A strategy decides and sizes its exits on its own position, not the portfolio's |
| TS9048 | An order is not decided on trades the code never opens |

Codes TS9005, TS9009, TS9011, TS9012 and TS9013 are not used.

### TS9001: indicators are created at the root

Create every indicator once, at the root, and keep it in a `const`. Read it inside callbacks.

Why: an indicator created inside a callback is rebuilt on every call, starts empty each time and
never warms up, so it never produces a value you can trade on.

Message: "Indicator 'RSI' must be instantiated at the root scope, not inside a
'manager.onIntervalCandle(...)' callback. Move the 'new RSI(...)' to the top level and capture the
indicator in a const."

```ts refused TS9001
const hourly = manager.getIntervalData('AAPL', 60);
manager.onIntervalCandle('AAPL', 60, () => {
    const rsi = new RSI(hourly.close, 14);
    if (rsi.hasValue()) manager.debugLog(rsi.getValue());
});
```

```ts
const hourly = manager.getIntervalData('AAPL', 60);
const rsi = new RSI(hourly.close, 14);
manager.onIntervalCandle('AAPL', 60, () => {
    if (rsi.hasValue()) manager.debugLog(rsi.getValue());
});
```

### TS9002: data subscriptions are made at the root

`manager.getIntervalData(...)` registers a data subscription, so it must be called at the root.
`manager.getSymbolMetadata(...)` is refused inside periodic callbacks for the same reason (hoist
it to the root with top-level `await`); it is allowed inside event callbacks such as `onNews`,
`onMessage` or `onPieuvreEvent`, where the symbol is only known when the event fires.

Why: the engine only accepts new subscriptions while the agent is being built. Calling
`getIntervalData` from a callback throws at run time and the agent keeps crashing.

Message: "'manager.getIntervalData(...)' must be called at the root scope, not inside a
'manager.onIntervalCandle(...)' callback. Move the call to the top level (await it if it returns a
Promise), capture the result in a const, and read that const inside the callback."

```ts refused TS9002
manager.onIntervalCandle('AAPL', 60, () => {
    const hourly = manager.getIntervalData('AAPL', 60);
    manager.debugLog(hourly.getPending());
});
```

```ts
const hourly = manager.getIntervalData('AAPL', 60);
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(hourly.getPending());
});
```

### TS9003: live-data and trading calls run inside a callback

`getCurrentTime`, `getCurrentPrice`, `placeOrder`, `closePosition`, `cancelOrder`, `openTrade`,
`closeTrade` and `increaseCapital` must run inside a callback, never at the root or in a helper
called from the root.

Why: when the root runs there is no current tick, price, time or capital yet.

Message: "'manager.getCurrentPrice(...)' must be called inside a 'manager.on*(...)' callback (e.g.
'manager.onIntervalCandle', 'manager.cron'), not at the root scope. Live data is not available at
module load."

```ts refused TS9003
const price = manager.getCurrentPrice('AAPL');
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(price);
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    manager.debugLog(price);
});
```

### TS9004: listeners are attached at the root

Call the listener methods (`manager.on…` and `manager.cron`) only at the root. A root-level
`for` loop or `forEach` that registers one listener per symbol is fine.

Why: a listener attached inside a callback is attached again every time the outer callback fires,
so the handler runs once, then twice, then three times.

Message: "Listener 'manager.onTradeFilled(...)' must be attached at the root scope, not inside a
'manager.onIntervalCandle(...)' callback. A new listener would be added every time the outer
callback fires."

```ts refused TS9004
manager.onIntervalCandle('AAPL', 60, () => {
    manager.onTradeFilled((trade) => {
        manager.debugLog(trade.symbol);
    });
});
```

```ts
manager.onTradeFilled((trade) => {
    manager.debugLog(trade.symbol);
});
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog('hourly candle closed');
});
```

### TS9006: no timers

`setTimeout(...)` and `setInterval(...)` are refused. Schedule work with `manager.cron(...)`.

Why: timers run outside the engine's clock. In a backtest they do not follow simulated time, and
in a live run they do not survive a restart.

Message: "'setTimeout(...)' is forbidden in agent code. Use 'manager.cron(...)' for scheduled
work."

```ts refused TS9006
manager.onIntervalCandle('AAPL', 60, () => {
    setTimeout(() => manager.debugLog('five minutes later'), 5 * 60 * 1000);
});
```

```ts
manager.cron('*/5 * * * *', () => {
    manager.debugLog('every five minutes');
});
```

### TS9007: no imports

`import` declarations, `require(...)` and dynamic `import(...)` are refused. Everything you can use
is already in scope (see "Names already in scope").

Why: agent code cannot load modules. Importing a name that is already provided (such as `z` or
`DateTime`) also collides with it (TS2300).

Message: "'import' is forbidden in agent code. The system already provides every module you can
use"

```ts refused TS9007
import { z } from 'zod';
const Verdict = z.object({ buy: z.boolean() });
manager.debugLog(Verdict);
```

```ts
const Verdict = z.object({ buy: z.boolean() });
manager.debugLog(Verdict);
```

### TS9008: `manager` is provided

Do not declare a variable named `manager` and do not assign to `manager`. Passing `manager` to a
helper as an argument is fine.

Why: the engine provides the one `manager` your agent talks to. Shadowing or replacing it
disconnects the code from the engine. TypeScript also reports TS2451 or TS2588 for the same line.

Message: "Cannot redeclare 'manager'. The system provides 'manager' in scope"

```ts refused TS9008
const manager = { debugLog: (...args: unknown[]) => args.length };
manager.debugLog('hello');
```

```ts
function logPrice(api: V2Manager, symbol: string): void {
    api.debugLog(symbol, api.getCurrentPrice(symbol));
}
manager.onIntervalCandle('AAPL', 60, () => {
    logPrice(manager, 'AAPL');
});
```

### TS9010: no wall-clock time

`new Date()` with no argument, `Date.now()`, `DateTime.now()`, `DateTime.local()` and
`DateTime.utc()` are refused. Use `manager.getCurrentTime()` (milliseconds) for "now", and
`candle.openTime` / `candle.closeTime` for candle times. `new Date(timestamp)`,
`Date.parse(...)` and `DateTime.fromMillis(...)` are fine.

Why: in a backtest "now" is the simulated time, not the time of your computer. Wall-clock time
makes backtests wrong and non-reproducible.

Message: "'Date.now()' is forbidden in agent code. Use 'manager.getCurrentTime()' for the engine's
current time, or read 'candle.openTime' / 'candle.closeTime' from candle data."

```ts refused TS9010
manager.onIntervalCandle('AAPL', 60, () => {
    const now = Date.now();
    manager.debugLog(new Date(now).toISOString());
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const now = manager.getCurrentTime();
    const newYork = DateTime.fromMillis(now, { zone: 'America/New_York' });
    manager.debugLog(new Date(now).toISOString(), newYork.hour);
});
```

### TS9014: no direct network access

`fetch(...)`, `new XMLHttpRequest()`, `new WebSocket(...)` and `new EventSource(...)` are refused.
Reach outside data through the `manager` API: `searchNews`, `onNews`, `onMessage`, `llm` and the
data methods.

Why: agent code has no network access of its own. The `manager` methods are the supported, metered
and backtest-aware way in.

Message: "'fetch(...)' is forbidden in agent code."

```ts refused TS9014
manager.cron('0 * * * *', async () => {
    const response = await fetch('https://example.com/aapl-news.json');
    manager.debugLog(response.status);
});
```

```ts
manager.cron('0 * * * *', async () => {
    const items = await manager.searchNews('AAPL Apple', { limit: 5 });
    manager.debugLog(items.map((item) => item.title));
});
```

### TS9015: no `eval` or `Function`

Calling `eval(...)`, `Function(...)` or `new Function(...)` is refused. TS9039 also refuses these
names in any other form (an alias, an argument), so a refused example reports both codes.

Why: code built from a string at run time escapes every check on this page.

Message: "'new Function(...)' is forbidden in agent code. Dynamic code execution defeats the static
checks the engine relies on."

```ts refused TS9015
const double = new Function('a', 'return a * 2');
manager.debugLog(double(21));
```

```ts
function double(a: number): number {
    return a * 2;
}
manager.debugLog(double(21));
```

### TS9016: no `process`

Any reference to `process` (environment variables, arguments, working directory) is refused. A
property that happens to be named `process` (`job.process`, `{ process: 1 }`) is fine. For a
setting a user should be able to change per run, use a launch variable
(`manager.getVariable`).

Why: the host process is not exposed to agent code. TypeScript also reports TS2591, since Node.js
typings are not loaded.

Message: "'process' is not available in agent code. Configuration, env vars and host runtime details
are managed by the engine and not exposed to user code."

```ts refused TS9016
const threshold = Number(process.env.THRESHOLD ?? '30');
manager.debugLog(threshold);
```

```ts
const threshold = manager.getVariable('threshold', 30);
manager.debugLog(threshold);
```

### TS9017 (1 of 2): daily candles need a time zone

When `onIntervalCandle` or `onIntervalCandleOpen` uses a daily or longer interval (`1440` minutes
or more, `'week'`, `'month'`), pass the asset's IANA time zone as the last argument:
`'America/New_York'` for US stocks and ETFs, `'Europe/Paris'` for a Paris listing, `'UTC'` for
crypto. Only literal intervals are checked.

Why: without a zone the daily boundary can fall back to UTC. For an instrument that trades
extended hours, a UTC boundary makes one day's close and the next day's open fire on the same
candle, so "buy at the close, sell at the next open" becomes a flat round trip.

Message: "'manager.onIntervalCandle(...)' with a daily-or-higher interval must pass the asset's
IANA time zone as the last argument (e.g. 'America/New_York' for a US stock/ETF, 'Europe/Paris'
for an EU listing, 'UTC' for 24/7 crypto)."

```ts refused TS9017
manager.onIntervalCandle('AAPL', 1440, () => {
    manager.debugLog(manager.getCurrentPrice('AAPL'));
});
```

```ts
manager.onIntervalCandle('AAPL', 1440, () => {
    manager.debugLog(manager.getCurrentPrice('AAPL'));
}, 'America/New_York');
```

### TS9017 (2 of 2): orders sized from wallet capital set `inhibitAutoInvest: true`

A `placeOrder` whose `amount` or `qty` is computed from a wallet capital reading
(`getAvailableCapital`, `getCurrentCapital`, `getLatentCapital`, or the reported equity, available
funds and buying power readings) must set `inhibitAutoInvest: true`.

Why: on a paper wallet, an order larger than the cash available is topped up automatically unless
this flag is set. An order meant to spend the cash you already have would otherwise create money
out of nothing and inflate the results. For a fixed recurring contribution, use a fixed amount
instead (see TS9030).

Message: "This placeOrder is sized from wallet capital but does not set 'inhibitAutoInvest: true'."

```ts refused TS9017
manager.onIntervalCandle('AAPL', 60, () => {
    const cash = manager.getAvailableCapital();
    manager.placeOrder({ symbol: 'AAPL', type: 'MARKET', position: 'buy', amount: cash * 0.5, amountCurrency: 'USD' });
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const cash = manager.getAvailableCapital();
    manager.placeOrder({ symbol: 'AAPL', type: 'MARKET', position: 'buy', amount: cash * 0.5, amountCurrency: 'USD', inhibitAutoInvest: true });
});
```

### TS9018: `updateStoploss` moves the trade's own stop

The third argument of `updateStoploss(manager, trade, stoploss, price)` must be the trade's own
stop (`trade.stoploss`), not a new object literal.

Why: `updateStoploss` changes the stop object it receives. A fresh object is not attached to any
trade, so the call does nothing and the real stop never moves. To give a trade a stop it does not
have yet, set `stoploss` when you call `openTrade`.

Message: "'updateStoploss' mutates the stoploss object you pass: a fresh object is not attached to
any trade, so this call is a silent no-op (the trade's actual stop never moves)." (The real message
uses a dash instead of the colon.)

```ts refused TS9018
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    for (const trade of manager.getSymbolPendingTrades('AAPL')) {
        updateStoploss(manager, trade, { type: 'fixed', absolute: price * 0.98, initial: price * 0.98 }, price * 0.98);
    }
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    for (const trade of manager.getSymbolPendingTrades('AAPL')) {
        if (trade.position === 'buy' && trade.stoploss !== undefined && price * 0.98 > trade.stoploss.absolute) {
            updateStoploss(manager, trade, trade.stoploss, price * 0.98);
        }
    }
});
```

### TS9019: position size is a fraction of capital

In a position size object, `{ type: 'risk', risk }` must be below `0.5`, and
`{ type: 'proportion', proportion }` must be at most `1`. Both are fractions: `risk: 0.01` risks
1% of capital at the stop, `proportion: 0.1` opens 10% of capital. Only literal numbers are
checked.

Why: `risk: 1` means "lose 100% of the account if the stop is hit", which is almost always a
percent written as a whole number. If you really mean to commit a large sum, use
`{ type: 'amount', ... }` or `{ type: 'proportion', ... }`.

Message: "'risk: 1' risks 100% of the capital on a single trade"

```ts refused TS9019
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    manager.openTrade({
        symbol: 'AAPL',
        position: 'buy',
        type: 'MARKET',
        positionSize: { type: 'risk', risk: 1 },
        stoploss: { type: 'fixed', value: price * 0.95 },
    });
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    manager.openTrade({
        symbol: 'AAPL',
        position: 'buy',
        type: 'MARKET',
        positionSize: { type: 'risk', risk: 0.01 },
        stoploss: { type: 'fixed', value: price * 0.95 },
    });
});
```

### Launch variables (TS9020 to TS9024, TS9028, TS9036)

`manager.getVariable('name', default)` declares a setting the user can change when starting a run.
The launch form is built by reading your code, not by running it, so each call has to be readable
as written: `manager.getVariable` called directly, a string literal name, a literal default.

### TS9020: variable names are string literals

The name must be a string literal matching `[A-Za-z][A-Za-z0-9_]{0,63}`: no variable, no
expression, no template string.

Why: the name labels the field in the launch form, so it must be known without running the code.

Message: "'manager.getVariable(...)' needs a literal variable name matching
[A-Za-z][A-Za-z0-9_]{0,63} (e.g. 'rsiPeriod')."

```ts refused TS9020
const periodName = 'rsiPeriod';
const period = manager.getVariable(periodName, 14);
manager.debugLog(period);
```

```ts
const period = manager.getVariable('rsiPeriod', 14);
manager.debugLog(period);
```

### TS9021: variable defaults are literals

The default must be a literal: a finite number (a leading minus is fine), a string, or
`true` / `false`. Not a variable, not an expression, not `Infinity` or `NaN`.

Why: the default is the value used when the run provides none, and it is shown in the launch form.

Message: "'manager.getVariable(...)' needs a literal default: a finite number (e.g. 14 or -0.5), a
string (e.g. 'long') or true/false."

```ts refused TS9021
const period = manager.getVariable('rsiPeriod', 7 * 2);
manager.debugLog(period);
```

```ts
const period = manager.getVariable('rsiPeriod', 14);
manager.debugLog(period);
```

### TS9022: one default per variable

Every `getVariable` call for the same name must use the same default (same type and value).
Simplest: read each variable once at the root and reuse the `const`.

Why: otherwise the launch form and the value used when none is provided would depend on which call
runs first.

Message: "Variable 'threshold' is read elsewhere with a different default (30)."

```ts refused TS9022
const threshold = manager.getVariable('threshold', 30);
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(threshold, manager.getVariable('threshold', 25));
});
```

```ts
const threshold = manager.getVariable('threshold', 30);
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(threshold);
});
```

### TS9023: at most 16 variables

An agent declares at most 16 distinct launch variables.

Why: keeps the launch form usable. Keep the settings a user would realistically change between
runs, and hardcode the rest.

Message: "Too many launch-time variables: an agent may declare at most 16."

```ts refused TS9023
const settings: number[] = [
    manager.getVariable('v1', 1),
    manager.getVariable('v2', 2),
    manager.getVariable('v3', 3),
    manager.getVariable('v4', 4),
    manager.getVariable('v5', 5),
    manager.getVariable('v6', 6),
    manager.getVariable('v7', 7),
    manager.getVariable('v8', 8),
    manager.getVariable('v9', 9),
    manager.getVariable('v10', 10),
    manager.getVariable('v11', 11),
    manager.getVariable('v12', 12),
    manager.getVariable('v13', 13),
    manager.getVariable('v14', 14),
    manager.getVariable('v15', 15),
    manager.getVariable('v16', 16),
    manager.getVariable('v17', 17),
];
manager.debugLog(settings);
```

```ts
const settings: number[] = [
    manager.getVariable('v1', 1),
    manager.getVariable('v2', 2),
    manager.getVariable('v3', 3),
    manager.getVariable('v4', 4),
    manager.getVariable('v5', 5),
    manager.getVariable('v6', 6),
    manager.getVariable('v7', 7),
    manager.getVariable('v8', 8),
    manager.getVariable('v9', 9),
    manager.getVariable('v10', 10),
    manager.getVariable('v11', 11),
    manager.getVariable('v12', 12),
    manager.getVariable('v13', 13),
    manager.getVariable('v14', 14),
    manager.getVariable('v15', 15),
    manager.getVariable('v16', 16),
];
manager.debugLog(settings);
```

### TS9024: `getVariable` is called directly

Always write `manager.getVariable('name', default)` with exactly two inline arguments. Do not store
the method in a variable, bind it, destructure it, pass it to a helper, call it through bracket
access or spread its arguments. Any call to a method named `getVariable` on something other than
`manager` is refused too.

Why: indirect calls work at run time but are invisible to the launch form, so the setting could
never be changed. Read the value once into a `const` at the root and pass the value around.

Message: "'getVariable' must be called directly as manager.getVariable('name', <literal default>)"

```ts refused TS9024
const read = manager.getVariable;
manager.debugLog(read('rsiPeriod', 14));
```

```ts
const rsiPeriod = manager.getVariable('rsiPeriod', 14);
manager.debugLog(rsiPeriod);
```

### TS9025: no type suppression

`as any`, `<any>`, a `: any` annotation on a variable or parameter, `as unknown` (the first half of
`x as unknown as T`), and the `@ts-ignore`, `@ts-expect-error` and `@ts-nocheck` comments are
refused. A `catch (error: any)` annotation is tolerated, but `catch (error)` plus a narrowing check
is cleaner.

Why: they switch the compiler off for that value, and the compiler is what catches a misspelled
field or a wrong value before your agent fails at run time.

Message: "Widening to 'any' is forbidden in agent code"

```ts refused TS9025
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL') as any;
    manager.debugLog(price.toFixed(2));
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    manager.debugLog(price.toFixed(2));
});
```

### TS9026: LLM calls in periodic callbacks use a stored key

A `manager.llm(...)` call reached from a periodic callback must be preceded, in the same function,
by a `manager.getStatus(...)` read. Use it to store a key for the current candle or period, claim
the key before the call, and release it if the call fails. Event callbacks (`onMessage`, `onNews`,
`onPieuvreEvent`, ...) are not concerned.

Why: a candle or a cron period can be delivered more than once. Without a stored key, the same
period can trigger the same paid LLM call several times.

Message: "'manager.llm(...)' inside a 'manager.cron(...)' callback must use a persisted idempotency
key from 'manager.getStatus(...)' before the LLM call."

```ts refused TS9026
const Verdict = z.object({ buy: z.boolean() });
manager.cron('0 14 * * 1-5', async () => {
    const verdict = await manager.llm('default', 'You are a trading assistant.', 'Should I buy AAPL today?', Verdict);
    if (verdict?.buy === true) {
        manager.placeOrder({ symbol: 'AAPL', type: 'MARKET', position: 'buy', amount: 100, amountCurrency: 'USD' });
    }
}, 'America/New_York');
```

```ts
interface LlmGuard { lastDay?: string }
const Verdict = z.object({ buy: z.boolean() });
manager.cron('0 14 * * 1-5', async () => {
    const guard = manager.getStatus<LlmGuard>('llm-guard');
    const day = DateTime.fromMillis(manager.getCurrentTime(), { zone: 'America/New_York' }).toISODate() ?? '';
    if (guard.lastDay === day) return;
    guard.lastDay = day;
    try {
        const verdict = await manager.llm('default', 'You are a trading assistant.', 'Should I buy AAPL today?', Verdict);
        if (verdict?.buy === true) {
            manager.placeOrder({ symbol: 'AAPL', type: 'MARKET', position: 'buy', amount: 100, amountCurrency: 'USD' });
        }
    } catch (error) {
        guard.lastDay = undefined;
        manager.handleError(error);
    }
}, 'America/New_York');
```

### TS9027: `getPieuvreRank` requests use known fields and values

Every field of a `manager.getPieuvreRank(...)` request must be one of `sourceId`, `periodType`,
`metric`, `universe`, `country`, `region`, `sector`, `industry`, `type`, `order`, `limit`, `mode`,
and `order` / `mode` must use an allowed value (`order`: `'asc' | 'desc'`). The check looks through
casts and through one `const` holding the request.

Why: TypeScript does not flag an unknown field once the request is stored in a variable or cast,
but the ranking service rejects it at run time, so every run of the agent fails.

Message: "Unknown field 'sort' in the 'manager.getPieuvreRank(...)' request" followed by "Did you
mean 'order'?" and the list of allowed fields.

```ts refused TS9027
manager.cron('0 0 1 * *', async () => {
    const request = { metric: 'market_cap', universe: 'sp500', limit: 10, sort: 'desc' } as const;
    const ranked = await manager.getPieuvreRank(request);
    manager.debugLog(ranked.length);
});
```

```ts
manager.cron('0 0 1 * *', async () => {
    const request = { metric: 'market_cap', universe: 'sp500', limit: 10, order: 'desc' } as const;
    const ranked = await manager.getPieuvreRank(request);
    manager.debugLog(ranked.length);
});
```

### TS9028: a `getVariable` mention that yields no variable

A last-resort check: it fires only when nothing else failed, your code mentions
`.getVariable` somewhere (outside comments and strings), and yet no launch variable could be read
from it. The usual cause is `getVariable` used in a position that is not a direct call, such as a
type query.

Why: saving such an agent would store settings that can never be set at launch.

Message: "This code names 'getVariable' but no launch-time variable could be read from it."

```ts refused TS9028
type ReadVariable = typeof manager.getVariable;
manager.debugLog('ready');
```

```ts
const rsiPeriod = manager.getVariable('rsiPeriod', 14);
manager.debugLog('ready', rsiPeriod);
```

### TS9029: `handleError` receives the error, not the work

Pass `manager.handleError(...)` the error a failed operation threw, from a `catch` block. Do not
pass it a promise.

Why: a promise passed to `handleError` is never awaited. The work runs unsupervised, the report
carries no reason, and a failure has nobody to catch it.

Message: "'manager.handleError' takes the error an operation FAILED with, not the operation itself"

```ts refused TS9029
async function runCycle(): Promise<void> {
    const items = await manager.searchNews('AAPL Apple', { limit: 5 });
    manager.debugLog(items.length);
}
manager.cron('0 18 * * 1-5', () => {
    manager.handleError(runCycle());
}, 'America/New_York');
```

```ts
async function runCycle(): Promise<void> {
    const items = await manager.searchNews('AAPL Apple', { limit: 5 });
    manager.debugLog(items.length);
}
manager.cron('0 18 * * 1-5', async () => {
    try {
        await runCycle();
    } catch (error) {
        manager.handleError(error);
    }
}, 'America/New_York');
```

### TS9030: a contribution buy does not set `inhibitAutoInvest: true`

A buy with a fixed size (not computed from wallet capital) must not set `inhibitAutoInvest: true`,
unless the agent funds its wallet with `manager.increaseCapital(...)` somewhere. This is the mirror
of TS9017.

Why: investing and dollar-cost-averaging backtests start with no cash: each contribution is the
money. `inhibitAutoInvest: true` turns off that top-up and turns on the margin check, so every such
order is cancelled for insufficient funds and the backtest shows zero fills.

Message: "This BUY sets 'inhibitAutoInvest: true', but its size is not derived from wallet capital
and the agent never calls 'manager.increaseCapital(...)'"

```ts refused TS9030
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', amount: 500, amountCurrency: 'USD', inhibitAutoInvest: true });
}, 'America/New_York');
```

```ts
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', amount: 500, amountCurrency: 'USD' });
}, 'America/New_York');
```

### TS9031: cash-sized orders state their currency

A `placeOrder` with `amount` must also set `amountCurrency`. Use `'USD'` unless you mean another
currency.

Why: the currency of a cash amount must be explicit. Backtests have no exchange rates: they treat
`'USD'`, `'USDT'` and `'USDC'` as the same cash, so those size one-to-one on a listing quoted in any
of them (US stocks and ETFs, dollar-quoted crypto pairs). Any other currency only sizes on demo and
live runs, and such an agent cannot be backtested.

Message: "This cash-sized order ('amount') does not state its denomination. New agent code must
always set 'amountCurrency' explicitly on 'amount' orders."

```ts refused TS9031
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', amount: 500 });
}, 'America/New_York');
```

```ts
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', amount: 500, amountCurrency: 'USD' });
}, 'America/New_York');
```

### TS9032: `amountCurrency` only with `amount`

`amountCurrency` is refused on an order that has no `amount` (a `qty` order is in units and has no
currency).

Why: the engine rejects such an order at run time with "amountCurrency is only valid with amount".

Message: "'amountCurrency' denominates the cash 'amount' and is only valid with it"

```ts refused TS9032
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', qty: 2, amountCurrency: 'USD' });
}, 'America/New_York');
```

```ts
manager.cron('0 10 1 * *', () => {
    manager.placeOrder({ symbol: 'SPY', type: 'MARKET', position: 'buy', qty: 2 });
}, 'America/New_York');
```

### TS9033: your functions type their parameters

Every parameter of a function you declare needs a type annotation. This one error replaces the
per-parameter TS7006 errors.

Why: strict mode makes an untyped parameter an implicit `any`. Fix the declaration rather than
casting at each call site.

Message: "'changePct' declares untyped parameters (from, to). The gate compiles in strict mode:
every parameter of a function you declare needs a type annotation"

```ts refused TS9033
function changePct(from, to) {
    return (to - from) / from;
}
manager.debugLog(changePct(100, 110));
```

```ts
function changePct(from: number, to: number): number {
    return (to - from) / from;
}
manager.debugLog(changePct(100, 110));
```

### TS9034: `getStatus` takes a state type

When you read values out of `manager.getStatus('key')`, give it a type argument:
`manager.getStatus<State>('key')`. Reads then come back as `State[key] | undefined`, so initialise
before use (`status.count ??= 0`). This one error replaces the cascade of "is of type 'unknown'"
errors on every read.

Why: without a type argument every stored value is `unknown`, so any comparison, arithmetic or
property access on it fails. Declaring the shape once is the fix; narrowing or casting each read is
not.

Message: "manager.getStatus(...) without a type argument returns Partial<Record<string, unknown>>:
every value read from it is 'unknown' and fails on any comparison, arithmetic or property access."

```ts refused TS9034
manager.onIntervalCandle('AAPL', 60, () => {
    const status = manager.getStatus('counter');
    status.count = (status.count ?? 0) + 1;
    manager.debugLog(status.count);
});
```

```ts
interface Counter { count: number }
manager.onIntervalCandle('AAPL', 60, () => {
    const status = manager.getStatus<Counter>('counter');
    status.count = (status.count ?? 0) + 1;
    manager.debugLog(status.count);
});
```

### TS9035: no wrapper function

Submit the body only. A top-level arrow or function expression that nothing calls, typically
`async (manager) => { ... }`, is refused.

Why: the engine already runs your code as the body of such a function. Wrapped again, the inner
function is never called, nothing registers, and the run is empty.

Message: "The code is wrapped in a function that nothing invokes, so no listener, cron or indicator
would ever register and the run would be empty."

```ts refused TS9035
async (manager: V2Manager) => {
    manager.onIntervalCandle('AAPL', 60, () => {
        manager.debugLog(manager.getCurrentPrice('AAPL'));
    });
};
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(manager.getCurrentPrice('AAPL'));
});
```

### TS9036: whole-number indicator arguments get whole-number defaults

When a launch variable feeds an indicator argument that must be a whole number (a period, a
length, a bar count), its default must be an integer.

Why: `new RSI(closes, 14.5)` compiles and runs, but computes nonsense.

Message: "Variable 'rsiPeriod' feeds a whole-number indicator argument (a period, a length, a bar
count) but its literal default 14.5 is not an integer."

```ts refused TS9036
const hourly = manager.getIntervalData('AAPL', 60);
const rsiPeriod = manager.getVariable('rsiPeriod', 14.5);
const rsi = new RSI(hourly.close, rsiPeriod);
manager.onIntervalCandle('AAPL', 60, () => {
    if (rsi.hasValue()) manager.debugLog(rsi.getValue());
});
```

```ts
const hourly = manager.getIntervalData('AAPL', 60);
const rsiPeriod = manager.getVariable('rsiPeriod', 14);
const rsi = new RSI(hourly.close, rsiPeriod);
manager.onIntervalCandle('AAPL', 60, () => {
    if (rsi.hasValue()) manager.debugLog(rsi.getValue());
});
```

### TS9037: order payloads are visible to the check

The argument of `placeOrder` must be an object literal, a local variable holding one, or an object
built by spreading only local object literals. Do not pass the result of a function, a parameter,
or spread a runtime value into an order.

Why: TS9017, TS9030, TS9031 and TS9032 check `amountCurrency` and `inhibitAutoInvest` on the order
object. An order produced at run time could change or drop those fields after they were checked.

Message: "This placeOrder payload cannot be fully resolved at save time, so its final
'amountCurrency' and 'inhibitAutoInvest' values cannot be safety-checked."

```ts refused TS9037
function buyOrder(symbol: string, amount: number): CreateOrder {
    return { symbol, type: 'MARKET', position: 'buy', amount, amountCurrency: 'USD' };
}
manager.cron('0 10 1 * *', () => {
    manager.placeOrder(buyOrder('SPY', 100));
}, 'America/New_York');
```

```ts
function buy(symbol: string, amount: number): void {
    const order: CreateOrder = { symbol, type: 'MARKET', position: 'buy', amount, amountCurrency: 'USD' };
    manager.placeOrder(order);
}
manager.cron('0 10 1 * *', () => {
    buy('SPY', 100);
}, 'America/New_York');
```

### TS9038: rebalancing buys are clamped by a cash budget

A buy placed in a loop and sized from portfolio or position value (`getLatentCapital`,
`getCurrentCapital`, position values) must be limited by one shared cash budget that each buy
decrements.

Why: portfolio value includes the positions you already hold. Buying "total value x weight" for
each name buys again what you already own, so exposure grows at every rebalance until the account
is wiped out. `inhibitAutoInvest: true` alone does not prevent it. Rebalance by difference instead:

1. sell exits and overweight positions first;
2. for each target, compute the deficit (target minus what you hold) and skip it when it is zero or
   less;
3. clamp each buy with `Math.min(deficit, remaining)`, where `remaining` starts from
   `Math.max(0, manager.getAvailableCapital())` plus this cycle's sale proceeds, and subtract each
   spend from `remaining`.

Do not apply this clamp to contribution buys that bring new money in (see TS9030).

The check proves the budget from the shape of the code, so two more rules apply to sale proceeds:

- add each sell's proceeds, as `qty × manager.getCurrentPrice(symbol)` of the sold line, in the
  **same loop and branch** that places that sell (a sell `placeOrder`, or
  `manager.closePosition(symbol)` / `closePosition(symbol, qty)` under a `position.qty > 0` check:
  closing a short releases no cash, so only a proven long counts, and never `Math.abs(qty)`);
- sells planned into an array and executed in a later loop cannot be matched with their proceeds,
  and are refused.

Read the free cash once, before the sells: on paper a sell settles at once, so cash read after it
already includes the proceeds.

Message: "This BUY is sized from portfolio/position VALUE (a total that includes positions already
held) and placed in a loop without a cash-budget clamp."

```ts refused TS9038
const TARGETS = ['AAPL', 'MSFT'];
manager.cron('0 10 1 * *', () => {
    const total = manager.getLatentCapital();
    for (const symbol of TARGETS) {
        manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: total / TARGETS.length, amountCurrency: 'USD', inhibitAutoInvest: true });
    }
}, 'America/New_York');
```

```ts
const TARGETS = ['AAPL', 'MSFT'];
manager.cron('0 10 1 * *', () => {
    const held = new Map<string, number>();
    for (const position of manager.getPositions()) {
        const price = manager.getCurrentPrice(position.symbol);
        if (position.qty > 0 && price !== undefined && price > 0) held.set(position.symbol, position.qty * price);
    }
    const cash = manager.getAvailableCapital();
    let total = Math.max(0, cash);
    for (const value of held.values()) total += value;
    const target = (total * 0.95) / TARGETS.length;
    let remaining = Math.max(0, cash);
    for (const symbol of TARGETS) {
        const deficit = target - (held.get(symbol) ?? 0);
        const spend = Math.min(deficit, remaining);
        if (spend <= 0) continue;
        manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: spend, amountCurrency: 'USD', inhibitAutoInvest: true });
        remaining -= spend;
    }
}, 'America/New_York');
```

### TS9039: no access to the host environment

Agent code works through `manager`, the pre-declared names and plain values. Constructs whose
purpose is to reach the environment around the agent are refused:

- the global object (`globalThis`, `global`, `window`, `self`) and the reflection and low-level
  globals (`Reflect`, `Proxy`, `WebAssembly`, `SharedArrayBuffer`, `Atomics`);
- dynamic code (`eval`, `Function`) in any form, including as a value;
- prototype and constructor access: member names such as `prototype`, `constructor`, `__proto__`,
  `caller`, `callee`, the legacy getter/setter helpers, and the `Error` stack-trace hooks, wherever
  they appear as a property name;
- `Object` used as anything other than one of its safe statics: `Object.keys`, `values`, `entries`,
  `fromEntries`, `assign`, `freeze`, `isFrozen`, `seal`, `isSealed`, `preventExtensions`,
  `isExtensible`, `is`;
- the `arguments` object, `with`, `import.meta`, `new.target`, and `declare` statements;
- indexing a value typed `any` or `unknown` (or a function) with a key the compiler cannot pin
  down, and destructuring with a computed key.

A local variable you declare with one of these names (`const self = ...`) is not the global and is
allowed. Type positions (`let f: Function`) are allowed.

Why: these are not needed to write a strategy, and they are the constructs used to step outside
the agent API. Every message starts with "Implementation guard on this expression, not a missing
feature: rewrite it and keep the agent's behaviour", then names a rewrite that passes. The
rewrites below are the ones the messages suggest.

**Own-key check.** `Object.prototype.hasOwnProperty.call(obj, key)` is refused (it reads
`prototype`, and uses `Object` outside its safe statics). Use `({}).hasOwnProperty.call(obj, key)`.
Not `Object.hasOwn`: it is newer than ES2020 (TS2550) and is not a safe static. Not `key in obj`:
it is also true for inherited keys such as `'toString'`. Not `obj.hasOwnProperty(key)`: it breaks
when the data has its own `hasOwnProperty` key.

Message: "'prototype' may not be accessed. For an own-key check use '({}).hasOwnProperty.call(obj,
key)'"

```ts refused TS9039
interface Seen { symbols?: Record<string, boolean> }
manager.cron('0 * * * *', () => {
    const status = manager.getStatus<Seen>('seen');
    const seen = status.symbols ?? {};
    if (!Object.prototype.hasOwnProperty.call(seen, 'AAPL')) seen['AAPL'] = true;
    status.symbols = seen;
});
```

```ts
interface Seen { symbols?: Record<string, boolean> }
manager.cron('0 * * * *', () => {
    const status = manager.getStatus<Seen>('seen');
    const seen = status.symbols ?? {};
    if (!({}).hasOwnProperty.call(seen, 'AAPL')) seen['AAPL'] = true;
    status.symbols = seen;
});
```

**The global object.** To share a value between callbacks, declare a variable at the root: every
callback of the agent sees it. For state that must survive a restart, use `manager.getStatus`.
(In the refused example TypeScript also reports TS7017, since the global object has no such
property.)

Message: "'globalThis' is not reachable here: use the 'manager' API or plain local values." (The
real message uses a dash instead of the colon.)

```ts refused TS9039
manager.onPreciseCandle('AAPL', (candle) => {
    globalThis.lastClose = candle.close;
});
```

```ts
let lastClose = 0;
manager.onPreciseCandle('AAPL', (candle) => {
    lastClose = candle.close;
});
manager.cron('0 * * * *', () => {
    manager.debugLog(lastClose);
});
```

**Parsed JSON.** `JSON.parse` returns `any`, and indexing it with a runtime key is refused. Type the
result first, for example as a `Record`.

Message: "A runtime key cannot index an untyped value or a function. Type the value first"

```ts refused TS9039
interface Config { raw?: string }
manager.cron('0 * * * *', () => {
    const status = manager.getStatus<Config>('config');
    const weights = JSON.parse(status.raw ?? '{}');
    const symbol: string = 'AAPL';
    manager.debugLog(weights[symbol]);
});
```

```ts
interface Config { raw?: string }
manager.cron('0 * * * *', () => {
    const status = manager.getStatus<Config>('config');
    const weights = JSON.parse(status.raw ?? '{}') as Record<string, number>;
    const symbol: string = 'AAPL';
    manager.debugLog(weights[symbol] ?? 0);
});
```

**Reflection.** Use the safe `Object` statics, spreads and plain property access instead of
`Reflect` or the reflective `Object` methods.

Message: "'Reflect' is not reachable here"

```ts refused TS9039
const weights: Record<string, number> = { AAPL: 0.6, MSFT: 0.4 };
manager.debugLog(Reflect.ownKeys(weights));
```

```ts
const weights: Record<string, number> = { AAPL: 0.6, MSFT: 0.4 };
const copy = { ...weights };
manager.debugLog(Object.keys(copy), Object.entries(copy).length);
```

**Constructor access.** Tell values apart with `typeof`, `Array.isArray` or `instanceof`, not
through `.constructor`.

Message: "'constructor' may not be accessed."

```ts refused TS9039
function kind(value: object): string {
    return value.constructor.name;
}
manager.debugLog(kind([1, 2]));
```

```ts
function kind(value: object): string {
    return Array.isArray(value) ? 'Array' : typeof value;
}
manager.debugLog(kind([1, 2]));
```

### TS9040: the previous day is the last element of a daily `History`

Inside a callback that runs more often than the bars of a `History` (an intraday `onIntervalCandle`
or `onIntervalCandleOpen`, `onPreciseCandle`, `cron`), a `History` over daily, weekly or monthly
candles is read at its last element for the previous bar. Code that reads only an older element
(`[0]` of a two-bar `History`, `[length - 2]`) and never the last one is refused.

Why: a candle series holds completed bars only, oldest first. During the day the forming day is
not in the daily series, so the last element is yesterday and `[0]` of `new History(daily, 2)` is
the day before yesterday: every level taken from it is a day stale. In the daily callback itself the
last element is the day that just closed, so `[0]` is the previous day there and the rule does not
apply. When you really need the bar before the previous one, read it next to the previous bar
(`bars[bars.length - 1]`, or the series itself, `daily.getValue()`), for example to compare the two
days.

A `cron` that fires exactly when a bar closes (00:00 in the series' time zone for daily bars) runs
before that close in the same tick, so its last element is still the day before. Schedule such a
cron a few minutes later.

Message: "'bars[0]' reads an older element of a History over day candles, and nothing in the code
reads its LAST element."

```ts refused TS9040
const daily = manager.getIntervalData('AAPL', 1440, 'America/New_York');
const days = new History(daily, 2);
manager.onIntervalCandle('AAPL', 60, () => {
    if (!days.hasValue()) return;
    const bars = days.getValue();
    const yesterday = bars[0];
    const price = manager.getCurrentPrice('AAPL');
    if (yesterday === undefined || price === undefined) return;
    if (price > yesterday.high) manager.sendNotification(`AAPL is above yesterday's high ${yesterday.high}`);
});
```

```ts
const daily = manager.getIntervalData('AAPL', 1440, 'America/New_York');
const days = new History(daily, 2);
manager.onIntervalCandle('AAPL', 60, () => {
    if (!days.hasValue()) return;
    const bars = days.getValue();
    const yesterday = bars[bars.length - 1];
    const price = manager.getCurrentPrice('AAPL');
    if (yesterday === undefined || price === undefined) return;
    if (price > yesterday.high) manager.sendNotification(`AAPL is above yesterday's high ${yesterday.high}`);
});
```

### TS9041: a take-profit or stop-loss `delta` is a whole percentage

The `delta` of a take-profit or stop-loss request, or of `closeTrade`, is the percentage of the
trade's initial quantity that the exit closes: a whole number from 0 to 100. Values the checker can
compute (a literal, arithmetic, a constant, a launch variable's default) are checked.

Why: the engine checks `delta` and throws otherwise, so a share such as `100 / 3` stops the agent at
its first signal. For thirds, use 33, 33 and 34, or leave `delta` off the last level: an exit
without `delta` closes whatever remains.

Message: "'delta: 100 / 3' (= 33.333333333333336) is not a whole number from 0 to 100."

```ts refused TS9041
manager.onIntervalCandle('BTC', 240, () => {
    const price = manager.getCurrentPrice('BTC');
    if (price === undefined || manager.getSymbolPendingTrades('BTC').length > 0) return;
    manager.openTrade({
        symbol: 'BTC', position: 'buy', type: 'MARKET',
        positionSize: { type: 'proportion', proportion: 0.1 },
        stoploss: { type: 'fixed', value: price * 0.95 },
        takeprofit: { type: 'fixed', value: price * 1.05, delta: 100 / 3 },
        takeprofit2: { type: 'fixed', value: price * 1.1, delta: 100 / 3 },
        takeprofit3: { type: 'fixed', value: price * 1.15, delta: 100 / 3 },
    });
});
```

```ts
manager.onIntervalCandle('BTC', 240, () => {
    const price = manager.getCurrentPrice('BTC');
    if (price === undefined || manager.getSymbolPendingTrades('BTC').length > 0) return;
    manager.openTrade({
        symbol: 'BTC', position: 'buy', type: 'MARKET',
        positionSize: { type: 'proportion', proportion: 0.1 },
        stoploss: { type: 'fixed', value: price * 0.95 },
        takeprofit: { type: 'fixed', value: price * 1.05, delta: 33 },
        takeprofit2: { type: 'fixed', value: price * 1.1, delta: 33 },
        takeprofit3: { type: 'fixed', value: price * 1.15, delta: 34 },
    });
});
```

### TS9042: a stop trailed from the current price only tightens

A stop moved with `updateStoploss` to a level computed from the current price (a candle's
`close`, `high` or `low`, or `getCurrentPrice`) is compared with the stop already in place first:
`level > trade.stoploss.absolute` for a long, `<` for a short (in the callback or in a helper it
calls), or clamped with `Math.max` / `Math.min` against the current stop. A level that is a fixed
fraction of a running peak (`trade.highestPrice * 0.95`, or a peak your code stores and only
raises) already only tightens and needs no comparison.

Why: `updateStoploss` keeps the stop on the right side of the current price but accepts a looser
level. Recomputed on every bar without that comparison, a long's stop follows the price down on a
decline and rarely fires, and the loss runs past the distance you set. A stop at a fixed distance
needs no code: open the trade with `stoploss: { type: 'trailing', value }` and the engine trails
it.

Message: "'updateStoploss' moves this stop to 'price * 0.98', a level computed from the current
price, and nothing compares it with the stop already in place."

```ts refused TS9042
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    for (const trade of manager.getSymbolPendingTrades('AAPL')) {
        if (trade.position === 'buy' && trade.stoploss !== undefined) {
            updateStoploss(manager, trade, trade.stoploss, price * 0.98);
        }
    }
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    for (const trade of manager.getSymbolPendingTrades('AAPL')) {
        if (trade.position === 'buy' && trade.stoploss !== undefined && price * 0.98 > trade.stoploss.absolute) {
            updateStoploss(manager, trade, trade.stoploss, price * 0.98);
        }
    }
});
```

### TS9047: a strategy decides on its own position

An order is refused when a flat-or-not test of `manager.getPositionQty(symbol)` decides whether it
runs (`=== 0`, `!== 0`, `> 0`, `<= 0`, a flag computed from one, a guard clause that returns), unless
the order is sized from the holding itself. An exit is refused when it sells a quantity read from
`getPositionQty` (all of it or a share of it), or calls `closePosition`, on a symbol the agent also
buys on its own signals.

Why: `getPositionQty` is the position of the whole portfolio the agent runs on. In a backtest that
portfolio is the agent's alone, so the code looks right. A demo or live portfolio is shared with
your own holdings and with your other agents: a strategy waiting for `getPositionQty(symbol) === 0`
never enters while anyone holds the symbol, and a sell of `getPositionQty(symbol)` sells shares the
agent never bought. Decide on the agent's own book instead: its open trades
(`getSymbolPendingTrades(symbol)`, closed with `closeTrade`, which sells only what the trade bought),
or for an order-based agent the quantity its own fills bought, counted in `onOrderExecuted` (see the
[RSI mean reversion example](/docs/developers/stse/examples.md#rsi-mean-reversion)). Agents whose job is the whole
portfolio keep `getPositionQty`: a rebalancer trades the difference to a target, which this rule
does not refuse, and an agent managing positions it did not open exits with `closePosition`.

Message: "'manager.getPositionQty(symbol) === 0' decides this openTrade on manager.getPositionQty,
the quantity the WHOLE portfolio holds."

```ts refused TS9047
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 240, 'UTC');
const fast = new EMA(new PartialIndicator(candles, 'close'), 9);
const slow = new EMA(new PartialIndicator(candles, 'close'), 21);
manager.onIntervalCandle(symbol, 240, () => {
    const f = fast.getValue();
    const s = slow.getValue();
    if (f === undefined || s === undefined) return;
    if (f > s && manager.getPositionQty(symbol) === 0) {
        manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 500, amountCurrency: 'USD' });
    } else if (f < s && manager.getPositionQty(symbol) > 0) {
        manager.placeOrder({ symbol, type: 'MARKET', position: 'sell', qty: manager.getPositionQty(symbol), inhibitAutoInvest: true });
    }
}, 'UTC');
```

```ts
const symbol = 'BTC';
const candles = manager.getIntervalData(symbol, 240, 'UTC');
const fast = new EMA(new PartialIndicator(candles, 'close'), 9);
const slow = new EMA(new PartialIndicator(candles, 'close'), 21);
manager.onIntervalCandle(symbol, 240, () => {
    const f = fast.getValue();
    const s = slow.getValue();
    if (f === undefined || s === undefined) return;
    const open = manager.getSymbolPendingTrades(symbol);
    if (f > s && open.length === 0) {
        manager.openTrade({ symbol, type: 'MARKET', position: 'buy', positionSize: { type: 'amount', amount: 500 }, comment: `EMA9 ${f.toFixed(0)} above EMA21 ${s.toFixed(0)}` });
    } else if (f < s) {
        for (const trade of open) manager.closeTrade(trade, { cause: `EMA9 ${f.toFixed(0)} back below EMA21 ${s.toFixed(0)}` });
    }
}, 'UTC');
```

### TS9048: an order is not decided on trades the code never opens

An order is refused when `getSymbolPendingTrades` or `getPendingTrades` decides whether it runs, in
code that never calls `openTrade`.

Why: only `openTrade` creates trades. Orders placed with `placeOrder` change the position but open
no trade, so in such an agent the list of its trades is always empty: a test of it reads "flat" on
every signal and the agent buys again each time. Either open trades (`openTrade`, then
`closeTrade`), or count the quantity the agent's own fills bought in `onOrderExecuted` and test that.

Message: "'manager.getSymbolPendingTrades(symbol)' reads this agent's open TRADES, but the code
never calls manager.openTrade."

```ts refused TS9048
const symbol = 'ETH';
const candles = manager.getIntervalData(symbol, 60, 'UTC');
const rsi = new RSI(new PartialIndicator(candles, 'close'), 14);
manager.onIntervalCandle(symbol, 60, () => {
    const value = rsi.getValue();
    if (value === undefined || value >= 30) return;
    if (manager.getSymbolPendingTrades(symbol).length > 0) return;
    manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 200, amountCurrency: 'USD' });
}, 'UTC');
```

## Common TypeScript errors

The compiler errors external authors hit most often on this API, with the fix.

### TS2339: property does not exist

The name is wrong for that type. A candle has `open`, `high`, `low`, `close`, `volume`, `openTime`
and `closeTime`; the current price comes from `manager.getCurrentPrice(symbol)`.

```ts refused TS2339
manager.onPreciseCandle('AAPL', (candle) => {
    manager.debugLog(candle.price);
});
```

```ts
manager.onPreciseCandle('AAPL', (candle) => {
    manager.debugLog(candle.close);
});
```

### TS18048 / TS2532: possibly undefined

`manager.getCurrentPrice()` returns `number | undefined` (TS18048 names the variable). An
indicator's `getValue()` returns `T | undefined` (TS2532 "Object is possibly 'undefined'"). Check
first: an `if (x === undefined) return;` guard, or `indicator.hasValue()` which narrows the next
`getValue()`.

```ts refused TS18048
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    manager.debugLog(price * 2);
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const price = manager.getCurrentPrice('AAPL');
    if (price === undefined) return;
    manager.debugLog(price * 2);
});
```

```ts refused TS2532
const hourly = manager.getIntervalData('AAPL', 60);
const sma = new SMA(hourly.close, 20);
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(sma.getValue().toFixed(2));
});
```

```ts
const hourly = manager.getIntervalData('AAPL', 60);
const sma = new SMA(hourly.close, 20);
manager.onIntervalCandle('AAPL', 60, () => {
    const value = sma.getValue();
    if (value === undefined) return;
    manager.debugLog(value.toFixed(2));
});
```

### TS2345: argument not assignable

A value of the wrong type is passed to an API. A frequent case: intervals are minutes (`60`,
`240`, `1440`) or `'week'` / `'month'`, not strings such as `'1h'`.

```ts refused TS2345
manager.onIntervalCandle('AAPL', '1h', () => {
    manager.debugLog('hourly');
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog('hourly');
});
```

### TS7034 / TS7005: implicit any on a variable

A `let` declared without a type or an initializer and then used inside a callback. Give it a type.

```ts refused TS7034
let lastSignal;
manager.onIntervalCandle('AAPL', 60, () => {
    lastSignal = manager.getCurrentTime();
});
manager.onTradeFilled(() => {
    manager.debugLog(lastSignal);
});
```

```ts
let lastSignal: number | undefined;
manager.onIntervalCandle('AAPL', 60, () => {
    lastSignal = manager.getCurrentTime();
});
manager.onTradeFilled(() => {
    manager.debugLog(lastSignal);
});
```

### TS2550: method from a newer library

The method exists in a later JavaScript version than ES2020 (see the table under "What ES2020 gives you").

```ts refused TS2550
manager.onIntervalCandle('AAPL', 60, () => {
    const closes: number[] = [101, 102, 103];
    const last = closes.at(-1);
    const label = 'AAPL-US'.replaceAll('-', '_');
    manager.debugLog(last, label);
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    const closes: number[] = [101, 102, 103];
    const last = closes[closes.length - 1];
    const label = 'AAPL-US'.replace(/-/g, '_');
    manager.debugLog(last, label);
});
```

### Redeclaring a pre-declared name (TS2300, code 0)

Importing a pre-declared name reports TS2300 "Duplicate identifier" (plus TS9007). Declaring your
own variable, function, class or interface with a pre-declared name (`RSI`, `News`, `Trade`,
`Interval`, ...) collides with the declaration Obside provides: the check returns a single error
with code `0` and the message "Your code declares 'RSI', which is a name already provided in the
agent's global scope". Rename your declaration.

```ts refused TS2300
import { DateTime } from 'luxon';
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(DateTime.fromMillis(manager.getCurrentTime()).toISO());
});
```

```ts fragment
const RSI = 14;
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(RSI);
});
```

```ts
const rsiPeriod = 14;
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(rsiPeriod, DateTime.fromMillis(manager.getCurrentTime()).toISO());
});
```

### TS2365: comparing an indicator's label with a number

Some indicators return labels, not numbers: `Crossover` returns `'firstUp' | 'secondUp' | null`,
`Elder` returns `'bullish' | 'bearish' | 'neutral'`, `Engulfing` returns
`'bullish' | 'bearish' | 'none'`. Comparing them with a number fails (here TypeScript also reports
TS2533 for the possible `null`). Compare with the label.

```ts refused TS2365
const hourly = manager.getIntervalData('AAPL', 60);
const fast = new EMA(hourly.close, 12);
const slow = new EMA(hourly.close, 26);
const cross = new Crossover(fast, slow);
manager.onIntervalCandle('AAPL', 60, () => {
    if (cross.getValue() > 0) manager.debugLog('fast EMA crossed above');
});
```

```ts
const hourly = manager.getIntervalData('AAPL', 60);
const fast = new EMA(hourly.close, 12);
const slow = new EMA(hourly.close, 26);
const cross = new Crossover(fast, slow);
manager.onIntervalCandle('AAPL', 60, () => {
    if (cross.getValue() === 'firstUp') manager.debugLog('fast EMA crossed above');
});
```

### TS18046: the caught error is `unknown`

```ts refused TS18046
manager.cron('0 * * * *', async () => {
    try {
        await manager.searchNews('AAPL Apple');
    } catch (error) {
        manager.debugLog(error.message);
    }
});
```

```ts
manager.cron('0 * * * *', async () => {
    try {
        await manager.searchNews('AAPL Apple');
    } catch (error) {
        manager.debugLog(error instanceof Error ? error.message : String(error));
    }
});
```

### TS1108: `return` at the top level

Your code is compiled as a module, where a top-level `return` is not allowed. Use an `if`.

```ts refused TS1108
const enabled = manager.getVariable('enabled', true);
if (!enabled) return;
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog('running');
});
```

```ts
const enabled = manager.getVariable('enabled', true);
if (enabled) {
    manager.onIntervalCandle('AAPL', 60, () => {
        manager.debugLog('running');
    });
}
```

### TS2304: cannot find name

The name is not in scope: there is no Lodash, no Node.js module and no browser library. Use plain
JavaScript and the pre-declared names.

```ts refused TS2304
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog(_.sum([1, 2, 3]));
});
```

```ts
manager.onIntervalCandle('AAPL', 60, () => {
    manager.debugLog([1, 2, 3].reduce((sum, n) => sum + n, 0));
});
```

# Pitfalls

The mistakes that most often make a first draft fail validation, or pass it and then do the wrong thing. Check your code against this list before calling `validate_agent_code`.

## Before you submit

- [ ] No `import`, no wrapper function, no redeclared built-in name (`RSI`, `News`, `Trade`, `manager`, …).
- [ ] Every indicator, `getIntervalData` and listener is created at the root; every `getValue()`, price read, order and notification is inside a callback.
- [ ] Every symbol you trade or price from a `cron` is subscribed at the root (`manager.onPreciseCandle(symbol, () => {})` or a candle series).
- [ ] Intervals are minutes (`60`, `240`, `1440`) or `'week'` / `'month'`; daily and longer listeners pass the asset's time zone; schedules pass the zone of their wall-clock time.
- [ ] Every value that can be missing is checked: `hasValue()` before `getValue()`, then `=== undefined` on numbers and on each object field you use; `getCurrentPrice` can be `undefined`.
- [ ] Indicator inputs match the signature: `new RSI(candles.close, 14)`, not `new RSI(candles, 14)`; `Bollinger` and `RSI` want a stream that is never `undefined` (`candles.close`), not another indicator's output. Check the [Indicator reference](/docs/developers/stse/indicator-reference.md).
- [ ] Label values are compared with labels: `Crossover` gives `'firstUp' | 'secondUp' | null`, not a number.
- [ ] Orders: `position: 'buy' | 'sell'`; `qty` **or** `amount`; every `amount` has `amountCurrency`; `LIMIT` uses `limitPrice` (on `placeOrder`) or `price` (on `openTrade`).
- [ ] Trades: `positionSize` is an object (`{ type: 'proportion', proportion: 0.1 }`); stop and take-profit `value`s are prices, not distances; `closeTrade(trade, { cause })` takes only `cause` and `delta`.
- [ ] `inhibitAutoInvest: true` on orders that spend existing cash (sized from a capital reading, rebalances, sells); none on fixed contribution buys.
- [ ] Rebalances trade the difference from target within one decremented cash budget.
- [ ] `getStatus<MyState>('default')` with an interface whose fields are all optional; initialised before use.
- [ ] `getVariable('name', literal)` called directly, once per name, at the root.
- [ ] No `Date.now()`, `new Date()`, `DateTime.now()`, `setTimeout`, `fetch`, `process`, `globalThis`, `as any`, `@ts-ignore`.
- [ ] Function parameters are typed; empty arrays are typed (`const lines: string[] = []`).
- [ ] Only ES2020 library methods: no `.at()`, `replaceAll`, `Object.hasOwn`, `findLast`.
- [ ] `manager.llm` on a schedule or candle claims a period key from `getStatus` first.
- [ ] At most one `sendNotification` per callback run, and only if the user asked for alerts.

## Behaviour traps that pass validation

**A schedule never fires at launch.** "Buy now, then weekly" needs a launch catch-up (see [Acting at launch](/docs/developers/stse/scheduling.md#acting-at-launch)). A restart does not replay a missed occurrence either.

**A schedule on a candle boundary runs before the candle closes.** A `'0 0 * * *'` schedule reads yesterday's daily indicators. Use `onIntervalCandle`, or schedule a minute later.

**Backtests start cold.** Indicators are empty on the backtest start date: a 200-day average needs 200 days of the window before it has a value.

**A condition that stays true fires every time.** Without a latch in the status bag, "RSI below 30" re-alerts or re-buys on every candle it stays below 30.

**Comparing the current value with a window that contains it.** `close > highest high of the last 20 candles` is never true when the window includes the current candle. Exclude it with `slice(0, -1)`.

**`Extreme` and `DC` include the current candle, and `DC` is the channel's midline.** For Donchian upper and lower bands, use `History` (or `Extreme` on the previous candle).

**A missing value becomes 0.** `price ?? 0` or `Number(undefined)` turns "no data" into a price of zero, which satisfies every "below" condition. Skip the cycle instead.

**Buying the whole target again at each rebalance.** Portfolio value includes what you already hold; buying `value × weight` compounds exposure. Trade the difference.

**Sizing a fixed contribution on free cash.** "Buy $100 a week" sized on `getAvailableCapital()` buys all the idle cash the first time.

**Sizing by `getCurrentCapital()`.** It does not go down when you buy: it is not free cash. Use `getAvailableCapital()`.

**Deciding "am I in a position?" on the portfolio.** `getPositionQty(symbol)` counts everything the portfolio holds, your own shares and other agents' included. A strategy gated on `getPositionQty(symbol) === 0` never buys on a portfolio that already holds the symbol, and a sell of `getPositionQty(symbol)` sells shares the agent never bought. A backtest cannot show it: there the portfolio is the agent's alone. Test the agent's own trades, or its own fills (TS9047).

**Exiting a position you did not open with an opposite order.** Use `closePosition(symbol)`: on a live account an opposite order can open a reverse position if the position changed meanwhile.

**`risk` sizing without a stop.** It then behaves like `proportion`, and a very tight stop asks for a very large position.

**Trusting the status bag alone for orders.** It is saved about every minute; after a crash the last changes can be lost. Also check `getPositions()` and pending orders before placing again.

**`handleError` in a backtest fails the run.** It is right for real failures (a data outage), not for expected conditions such as "no signal".

**Ratios are fractions.** A 20% margin from `getPieuvreMetric` is `0.20`: `margin > 20` never fires.

**An unknown ranking universe returns nothing.** `universe: 'US'` silently returns `[]`; use a real tag such as `'us_equities'` or `'sp500'`.

**Trading a ticker string.** A calendar `ticker`, a model's answer or a name is not a trading key. Trade declared symbols, rank keys, `resolveAssets` keys or `search_assets` keys.

**Showing internal keys to the user.** Run-time trading keys are opaque identifiers. Name assets by `ticker`, `name` or your own symbol.

**A US stock order at 09:00 New York.** It is pre-market. Schedule at 09:31 or later, and gate catch-up schedules on the session.

## Backtest or demo

A backtest replays history on a fresh paper wallet. These agents cannot be backtested and are validated in a demo run instead:

- agents that read or manage the positions you already hold (a backtest wallet starts empty);
- agents using `searchNews`, `onNews`, `getPieuvreSeries`, `getPieuvreUpcoming` or `onPieuvreEvent`;
- agents calling `manager.llm` from a schedule or candle callback (a model call in a backtest only works inside `onMessage` on an X account);
- agents following a Telegram channel, or an X handle that is not a literal;
- agents with no declared asset whose universe does not come from `getPieuvreRank`;
- alert-only agents that never trade.

`getPieuvreRank` and `getPieuvreMetric` universes, and X follows with a literal handle, are backtestable, point in time. A schedule that fires every minute makes long backtests too expensive: prefer hourly or coarser schedules for recurring work.

Also note that `sendNotification`, `sendWebhook` and `comment` do nothing in a backtest: use `debugFlag` and `debugLog` to see what a backtest did.

# Examples

Complete agents you can adapt, one per common shape. Each one passes the checker: every block on this page is compiled by the real validator in Obside's test suite. Remember that the short symbol strings (`'BTC'`, `'SPY'`) are mapped to real listings when you create the agent, not in the code.

| Shape | Example |
|---|---|
| Recurring buy | [Recurring buy (dollar-cost averaging)](#recurring-buy-dollar-cost-averaging) |
| Alert once | [One-shot alert on a level](#one-shot-alert-on-a-level) |
| Indicator cross with stop and target | [Trend-following trade with a stop and target](#trend-following-trade-with-a-stop-and-target) |
| Mean reversion with orders | [RSI mean reversion](#rsi-mean-reversion) |
| Breakout with risk sizing | [Bollinger breakout sized by risk](#bollinger-breakout-sized-by-risk) |
| Channel breakout with an exit rule | [Donchian breakout](#donchian-breakout) |
| Higher-timeframe filter | [Multi-timeframe entry](#multi-timeframe-entry) |
| Tunable parameters | [SuperTrend with launch variables](#supertrend-with-launch-variables) |
| Two legs | [Pairs trade on a z-score](#pairs-trade-on-a-z-score) |
| Resting limit orders | [Grid of limit orders](#grid-of-limit-orders) |
| Dynamic universe | [Dynamic universe: top 10 by market cap](#dynamic-universe-top-10-by-market-cap) |
| Calendar | [Earnings reminder](#earnings-reminder) |
| Existing positions | [Trailing stop on positions you already hold](#trailing-stop-on-positions-you-already-hold) |
| Portfolio monitor | [Portfolio drawdown alert](#portfolio-drawdown-alert) |

More patterns live next to their topic: a scheduled rebalance and a weekly buy that starts at launch in [Schedules and launch actions](/docs/developers/stse/scheduling.md), a break-even stop in [Orders, trades and risk](/docs/developers/stse/orders-and-risk.md#stops-and-take-profits), news digests, account follows, macro events and an AI-annotated alert in [News, data and AI](/docs/developers/stse/data-and-ai.md).

## Recurring buy (dollar-cost averaging)

Buys a fixed amount every Monday. A contribution buy brings new money in, so it does not set `inhibitAutoInvest` (see [capital and auto-invest](/docs/developers/stse/orders-and-risk.md#capital-and-auto-invest)). To also buy at launch, use the pattern in [Acting at launch](/docs/developers/stse/scheduling.md#acting-at-launch).

```ts
const symbol = 'BTC';
manager.onPreciseCandle(symbol, () => {}); // subscribe the feed so the schedule can act

manager.cron('0 9 * * 1', () => {
  manager.placeOrder({
    symbol,
    type: 'MARKET',
    position: 'buy',
    amount: 50,
    amountCurrency: 'USD',
  });
}, 'UTC');
```

## One-shot alert on a level

Alerts you the first time an indicator condition is met, then stops itself.

```ts
const symbol = 'ETH';
const candles = manager.getIntervalData(symbol, 60);
const rsi = new RSI(new PartialIndicator(candles, 'close'), 14);

manager.onIntervalCandle(symbol, 60, () => {
  if (!rsi.hasValue()) return;
  const value = rsi.getValue();
  if (value === undefined || value > 30) return;
  manager.sendNotification(`ETH RSI is ${value.toFixed(1)} (below 30).`);
  manager.stop();
});
```

## Trend-following trade with a stop and target

Enters on an EMA cross and lets the engine manage the exit through a stop-loss and take-profit. Because there is a defined exit, this uses a trade rather than a plain order. `Crossover` returns `'firstUp'` on the candle where the first series crosses above the second, `'secondUp'` on the opposite cross, and `null` otherwise.

```ts
const symbol = 'SPY';
const zone = 'America/New_York';
const candles = manager.getIntervalData(symbol, 1440, zone);
const fast = new EMA(new PartialIndicator(candles, 'close'), 20);
const slow = new EMA(new PartialIndicator(candles, 'close'), 50);
const cross = new Crossover(fast, slow);

manager.onIntervalCandle(symbol, 1440, () => {
  if (!cross.hasValue()) return;
  const signal = cross.getValue();
  const price = manager.getCurrentPrice(symbol);
  if (price === undefined) return;
  if (signal === 'firstUp' && manager.getSymbolPendingTrades(symbol).length === 0) {
    manager.openTrade({
      symbol,
      position: 'buy',
      type: 'MARKET',
      positionSize: { type: 'proportion', proportion: 0.2 },
      stoploss: { type: 'fixed', value: price * 0.95 },
      takeprofit: { type: 'fixed', value: price * 1.15 },
      comment: `EMA20 crossed above EMA50 at ${price.toFixed(2)}`,
    });
  }
  if (signal === 'secondUp') {
    for (const trade of manager.getSymbolPendingTrades(symbol)) {
      manager.closeTrade(trade, { cause: `EMA20 crossed back below EMA50 at ${price.toFixed(2)}` });
    }
  }
}, zone);
```

## RSI mean reversion

Buys a fixed cash amount when the daily RSI is oversold and the agent holds none of its own, and sells what it bought when the RSI is overbought. Orders, not trades: there is no stop or target, only an exit rule. The agent counts its own fills, because the portfolio it runs on can hold QQQ for other reasons (your own shares, another agent's): `getPositionQty` would see those too.

```ts
const symbol = 'QQQ';
const zone = 'America/New_York';
const daily = manager.getIntervalData(symbol, 1440, zone);
const rsi = new RSI(daily.close, 14);

interface OwnBook { ownQty?: number }

manager.onOrderExecuted((order) => {
  if (order.symbol !== symbol || order.filledQty === undefined || !(order.filledQty > 0)) return;
  const book = manager.getStatus<OwnBook>('default');
  const signed = order.position === 'buy' ? order.filledQty : -order.filledQty;
  book.ownQty = Math.max(0, (book.ownQty ?? 0) + signed);
});

manager.onIntervalCandle(symbol, 1440, () => {
  if (!rsi.hasValue()) return;
  const value = rsi.getValue();
  if (value === undefined) return;
  if (manager.getSymbolPendingOrders(symbol).length > 0) return;
  const owned = manager.getStatus<OwnBook>('default').ownQty ?? 0;
  if (value < 30 && owned === 0) {
    manager.placeOrder({ symbol, type: 'MARKET', position: 'buy', amount: 1000, amountCurrency: 'USD' });
  } else if (value > 70 && owned > 0) {
    manager.placeOrder({ symbol, type: 'MARKET', position: 'sell', qty: Math.min(owned, manager.getPositionQty(symbol)), inhibitAutoInvest: true });
  }
}, zone);
```

## Bollinger breakout sized by risk

Opens a long when a 4-hour candle closes above the upper band, with the stop and target set from the ATR, risking 1% of the wallet at the stop.

```ts
const symbol = 'ETH';
const candles = manager.getIntervalData(symbol, 240);
const bands = new Bollinger(candles.close, 20, 2);
const atr = new RMA(new TR(candles), 14);

manager.onIntervalCandle(symbol, 240, () => {
  if (!bands.hasValue() || !atr.hasValue() || !candles.hasValue()) return;
  const band = bands.getValue();
  const atrValue = atr.getValue();
  const close = candles.getValue().close;
  if (band.upper === undefined || atrValue === undefined) return;
  if (close <= band.upper || manager.getSymbolPendingTrades(symbol).length > 0) return;
  manager.openTrade({
    symbol,
    position: 'buy',
    type: 'MARKET',
    positionSize: { type: 'risk', risk: 0.01 },
    stoploss: { type: 'fixed', value: close - 2 * atrValue },
    takeprofit: { type: 'fixed', value: close + 4 * atrValue },
    comment: `Close ${close.toFixed(2)} above the upper band ${band.upper.toFixed(2)}, ATR ${atrValue.toFixed(2)}`,
  });
});
```

## Donchian breakout

Enters when the close breaks the highest high of the previous 20 days and exits when it breaks the lowest low of the previous 10. The windows exclude the current candle.

```ts
const symbol = 'GLD';
const zone = 'America/New_York';
const daily = manager.getIntervalData(symbol, 1440, zone);
const last21 = new History(daily, 21);

manager.onIntervalCandle(symbol, 1440, () => {
  if (!last21.hasValue()) return;
  const candles = last21.getValue();
  if (candles.length < 21) return;
  const current = candles[candles.length - 1];
  if (current === undefined) return;
  const previous = candles.slice(0, -1);
  const high20 = Math.max(...previous.map((c) => c.high));
  const low10 = Math.min(...previous.slice(-10).map((c) => c.low));
  const open = manager.getSymbolPendingTrades(symbol);
  if (open.length === 0 && current.close > high20) {
    manager.openTrade({
      symbol,
      position: 'buy',
      type: 'MARKET',
      positionSize: { type: 'proportion', proportion: 0.25 },
      comment: `Close ${current.close.toFixed(2)} above the 20-day high ${high20.toFixed(2)}`,
    });
  } else if (current.close < low10) {
    for (const trade of open) {
      manager.closeTrade(trade, { cause: `Close ${current.close.toFixed(2)} below the 10-day low ${low10.toFixed(2)}` });
    }
  }
}, zone);
```

## Multi-timeframe entry

Takes longs only while the daily close is above the daily 200 EMA, entering when the hourly RSI crosses up through 30. The stop sits at the lowest low of the last 10 hourly candles and the target at twice the risk.

```ts
const symbol = 'BTC';
const daily = manager.getIntervalData(symbol, 1440, 'UTC');
const hourly = manager.getIntervalData(symbol, 60);
const trend = new EMA(daily.close, 200);
const rsi = new RSI(hourly.close, 14);
const rsiHistory = new History(rsi, 2);
const lastTen = new History(hourly, 10);

manager.onIntervalCandle(symbol, 60, () => {
  if (!daily.hasValue() || !trend.hasValue() || !rsiHistory.hasValue() || !lastTen.hasValue() || !hourly.hasValue()) return;
  const ema200 = trend.getValue();
  const [previousRsi, currentRsi] = rsiHistory.getValue();
  const candles = lastTen.getValue();
  if (ema200 === undefined || previousRsi === undefined || currentRsi === undefined || candles.length < 10) return;
  if (daily.getValue().close <= ema200) return; // no longs below the daily trend
  if (!(previousRsi < 30 && currentRsi >= 30)) return;
  if (manager.getSymbolPendingTrades(symbol).length > 0) return;
  const entry = hourly.getValue().close;
  const stop = Math.min(...candles.map((c) => c.low));
  if (stop >= entry) return;
  manager.openTrade({
    symbol,
    position: 'buy',
    type: 'MARKET',
    positionSize: { type: 'risk', risk: 0.01 },
    stoploss: { type: 'fixed', value: stop },
    takeprofit: { type: 'fixed', value: entry + 2 * (entry - stop) },
    comment: `RSI crossed up through 30 (${currentRsi.toFixed(1)}) above the daily EMA200 ${ema200.toFixed(0)}`,
  });
});
```

## SuperTrend with launch variables

Long-only SuperTrend on 4-hour candles, whose ATR period and multiplier the user can change when starting a run. A flip is detected by comparing the last two values.

```ts
const symbol = 'QQQ';
const period = manager.getVariable('atrPeriod', 10);
const multiplier = manager.getVariable('multiplier', 3);
const candles = manager.getIntervalData(symbol, 240);
const supertrend = new SuperTrend(candles, period, multiplier);
const recent = new History(supertrend, 2);

manager.onIntervalCandle(symbol, 240, () => {
  if (!recent.hasValue()) return;
  const [previous, current] = recent.getValue();
  if (previous === undefined || current === undefined) return;
  const open = manager.getSymbolPendingTrades(symbol);
  if (!previous.up && current.up && open.length === 0) {
    manager.openTrade({
      symbol,
      position: 'buy',
      type: 'MARKET',
      positionSize: { type: 'proportion', proportion: 0.5 },
      comment: `SuperTrend(${period}, ${multiplier}) turned bullish at ${current.close.toFixed(2)}`,
    });
  } else if (previous.up && current.down) {
    for (const trade of open) {
      manager.closeTrade(trade, { cause: `SuperTrend turned bearish at ${current.close.toFixed(2)}` });
    }
  }
});
```

## Pairs trade on a z-score

Trades the spread between two related stocks: short the rich leg and buy the cheap one when the 60-day z-score of the log price ratio passes 2, and close both legs when it falls back under 0.5. Each leg is a trade, so the exit closes exactly what the agent opened, whatever else the portfolio holds of KO or PEP. The ratio is a `DerivedIndicator` over both daily series, so it is part of the indicator graph: it is rebuilt from history at every start-up like any indicator, and updates once both legs have closed.

```ts
const legA = 'KO';
const legB = 'PEP';
const zone = 'America/New_York';
const LOOKBACK = 60;
const LEG_USD = 5000;
const a = manager.getIntervalData(legA, 1440, zone);
const b = manager.getIntervalData(legB, 1440, zone);
const ratio = new DerivedIndicator(() => {
  const candleA = a.getValue();
  const candleB = b.getValue();
  return candleA === undefined || candleB === undefined ? undefined : Math.log(candleA.close / candleB.close);
}, [a, b]);
const ratios = new History(ratio, LOOKBACK);

interface PairState { side?: 'longA' | 'shortA' }

manager.onIntervalCandle(legA, 1440, () => {
  if (!ratios.hasValue()) return;
  const state = manager.getStatus<PairState>('default');
  const values = ratios.getValue().filter((v): v is number => v !== undefined);
  if (values.length < LOOKBACK) return;
  const mean = values.reduce((sum, v) => sum + v, 0) / values.length;
  const sd = Math.sqrt(values.reduce((sum, v) => sum + (v - mean) ** 2, 0) / values.length);
  const last = values[values.length - 1];
  if (sd === 0 || last === undefined) return;
  const z = (last - mean) / sd;

  if (state.side !== undefined && Math.abs(z) < 0.5) {
    for (const trade of [...manager.getSymbolPendingTrades(legA), ...manager.getSymbolPendingTrades(legB)]) {
      manager.closeTrade(trade, { cause: `z-score ${z.toFixed(2)} back under 0.5` });
    }
    state.side = undefined;
    return;
  }
  const size = { type: 'amount' as const, amount: LEG_USD };
  if (state.side === undefined && z > 2) {
    manager.openTrade({ symbol: legA, type: 'MARKET', position: 'sell', positionSize: size, comment: `z-score ${z.toFixed(2)}: ${legA} rich` });
    manager.openTrade({ symbol: legB, type: 'MARKET', position: 'buy', positionSize: size, comment: `z-score ${z.toFixed(2)}: ${legB} cheap` });
    state.side = 'shortA';
  } else if (state.side === undefined && z < -2) {
    manager.openTrade({ symbol: legA, type: 'MARKET', position: 'buy', positionSize: size, comment: `z-score ${z.toFixed(2)}: ${legA} cheap` });
    manager.openTrade({ symbol: legB, type: 'MARKET', position: 'sell', positionSize: size, comment: `z-score ${z.toFixed(2)}: ${legB} rich` });
    state.side = 'longA';
  }
}, zone);
```

Shorting works on paper and in backtests; live, it depends on your broker.

## Grid of limit orders

Keeps five resting limit buys spaced 1% apart under the price, and places a limit sell 1% above each filled buy. `onOrderExecuted` reports this agent's fills; the price of a fill is `order.price`. It runs on paper (backtest and demo) only: a live broker skips `LIMIT` orders for now.

```ts
const symbol = 'BTC';
const LEVELS = 5;
const STEP = 0.01;
const QTY = 0.001;
manager.onPreciseCandle(symbol, () => {});

manager.onOrderExecuted((order) => {
  if (order.symbol !== symbol || order.position !== 'buy' || order.type !== 'LIMIT') return;
  const fill = order.price ?? order.limitPrice;
  if (fill === undefined) return;
  manager.placeOrder({ symbol, type: 'LIMIT', position: 'sell', qty: order.filledQty ?? QTY, limitPrice: fill * (1 + STEP), inhibitAutoInvest: true });
});

manager.cron('0 * * * *', () => {
  const price = manager.getCurrentPrice(symbol);
  if (price === undefined) return;
  for (const order of manager.getSymbolPendingOrders(symbol)) {
    if (order.position === 'buy') manager.cancelOrder(order); // re-centre the buy ladder every hour
  }
  for (let level = 1; level <= LEVELS; level++) {
    manager.placeOrder({ symbol, type: 'LIMIT', position: 'buy', qty: QTY, limitPrice: price * (1 - STEP * level) });
  }
});
```

## Dynamic universe: top 10 by market cap

Holds the ten largest S&P 500 companies, equal weight, rebalanced on the first weekday of each month (names that left the top 10 are sold, names above their weight are trimmed, deficits are bought), with a lump sum invested once at launch. No asset is declared: the universe comes from the ranking. Orders are cash-sized because newly ranked names have no price yet.

```ts
const zone = 'America/New_York';
const LUMP_SUM = 10000;
interface TopState { lastMonth?: string; funded?: boolean }

function marketOpen(nowMs: number): boolean {
  const ny = DateTime.fromMillis(nowMs).setZone(zone);
  const minutes = ny.hour * 60 + ny.minute;
  return ny.weekday <= 5 && minutes >= 9 * 60 + 31 && minutes < 15 * 60 + 55;
}

async function runMonth(): Promise<void> {
  const state = manager.getStatus<TopState>('default');
  const month = DateTime.fromMillis(manager.getCurrentTime()).setZone(zone).toFormat('yyyy-LL');
  if (state.lastMonth === month) return;
  const previous = state.lastMonth;
  state.lastMonth = month; // claimed before the first await

  let ranked: PieuvreRankedEntity[];
  try {
    ranked = await manager.getPieuvreRank({ metric: 'market_cap', universe: 'sp500', order: 'desc', limit: 10 });
  } catch (error) {
    manager.handleError(error);
    state.lastMonth = previous; // data unavailable, nothing done: retry next minute
    return;
  }
  const targets: string[] = [];
  for (const entry of ranked) {
    const key = entry.snaptradeSymbol ?? entry.backtestSymbol;
    if (key !== null) targets.push(key);
  }
  if (targets.length === 0) {
    state.lastMonth = previous;
    return;
  }
  if (state.funded !== true) {
    manager.increaseCapital(LUMP_SUM); // once in the agent's life
    state.funded = true;
  }

  // Value the book and set the per-name target BEFORE any sell. Free cash is read once, here:
  // paper sells settle at once, so reading it after them would count their proceeds twice.
  const cash = manager.getAvailableCapital();
  const keep = new Set(targets);
  let bookValue = 0;
  for (const position of manager.getPositions()) {
    const price = manager.getCurrentPrice(position.symbol);
    if (position.qty > 0 && price !== undefined) bookValue += position.qty * price;
  }
  const perName = ((bookValue + cash) * 0.97) / targets.length; // 3% headroom for fees and drift

  // Sell what left the top 10, and trim names kept above their target, counting the proceeds
  // next to each sell.
  const held = new Map<string, number>();
  let proceeds = 0;
  for (const position of manager.getPositions()) {
    const price = manager.getCurrentPrice(position.symbol);
    if (position.qty <= 0 || price === undefined) continue;
    const value = position.qty * price;
    if (!keep.has(position.symbol)) {
      manager.placeOrder({ symbol: position.symbol, type: 'MARKET', position: 'sell', qty: position.qty, inhibitAutoInvest: true });
      proceeds += position.qty * price;
    } else if (value > perName * 1.02) {
      const sellQty = (value - perName) / price;
      manager.placeOrder({ symbol: position.symbol, type: 'MARKET', position: 'sell', qty: sellQty, inhibitAutoInvest: true });
      proceeds += sellQty * price;
      held.set(position.symbol, perName);
    } else {
      held.set(position.symbol, value);
    }
  }

  // Buy each target's deficit within one shared cash budget.
  let remaining = Math.max(0, cash) + proceeds;
  for (const key of targets) {
    const deficit = perName - (held.get(key) ?? 0);
    const spend = Math.min(deficit, remaining);
    if (spend <= 1) continue;
    manager.placeOrder({ symbol: key, type: 'MARKET', position: 'buy', amount: spend, amountCurrency: 'USD', inhibitAutoInvest: true });
    remaining -= spend;
  }
}

manager.cron('* * * * *', async () => {
  if (!marketOpen(manager.getCurrentTime())) return;
  await runMonth();
});
```

The single per-minute trigger both invests at launch and runs each month's first open session; the month key limits it to one cycle a month. It backtests point in time (the ranking is the one known on each simulated date), but a per-minute schedule keeps backtests to about a year: for longer ones, replace it with a daily `'31 9 * * 1-5'` schedule in New York time.

## Earnings reminder

Each weekday morning, lists which companies of a watchlist report in the next 24 hours, with the consensus EPS. The calendar is narrowed to the watchlist so the answer is never truncated.

```ts
const zone = 'America/New_York';
const WATCHLIST = ['AAPL', 'MSFT', 'NVDA', 'AMZN', 'GOOGL'];

manager.cron('0 8 * * 1-5', async () => {
  let events: PieuvreEvent[];
  try {
    events = await manager.getPieuvreUpcoming({ withinMs: 24 * 60 * 60 * 1000, eventTypes: ['earnings'], tickers: WATCHLIST });
  } catch (error) {
    manager.handleError(error);
    return;
  }
  const lines = events.map((event) => {
    const when = DateTime.fromMillis(event.timestamp).setZone(zone).toFormat('ccc HH:mm');
    const eps = event.value === undefined ? 'no consensus' : `consensus EPS **${event.value.toFixed(2)}**`;
    return `**${event.ticker ?? event.summary}**: ${when} New York, ${eps}`;
  });
  if (lines.length > 0) manager.sendNotification(`Earnings in the next 24 hours:\n\n${lines.join('\n')}`);
}, zone);
```

## Trailing stop on positions you already hold

Watches the whole account book and trails a protective stop under each position, without declaring any asset. It exits with `closePosition`, the safe way to reduce a position the agent did not open.

```ts
interface Guard { high: number }
interface State { guards?: Record<string, Guard> }
const TRAIL = 0.05; // 5%

manager.cron('* * * * *', () => {
  const state = manager.getStatus<State>('default');
  const guards = state.guards ?? {};
  const held = new Set<string>();

  for (const position of manager.getPositions()) {
    if (position.qty <= 0) continue; // longs only in this example
    held.add(position.symbol);
    const price = manager.getCurrentPrice(position.symbol);
    if (price === undefined || price <= 0) continue;

    const guard = guards[position.symbol];
    const high = guard === undefined ? price : Math.max(guard.high, price);
    guards[position.symbol] = { high };

    if (price <= high * (1 - TRAIL)) {
      const pending = manager.getSymbolPendingOrders(position.symbol);
      if (!pending.some(order => order.position === 'sell')) {
        manager.closePosition(position.symbol);
      }
    }
  }

  for (const symbol of Object.keys(guards)) {
    if (!held.has(symbol)) delete guards[symbol];
  }
  state.guards = guards;
});
```

This one runs live or in a paper portfolio that holds positions; it is not backtestable, because a backtest starts from an empty wallet with nothing to trail.

## Portfolio drawdown alert

Notifies once when the portfolio's value falls 10% below its highest value since the agent started, and again only after it recovers above that line and falls again.

```ts
interface DrawdownState { peak?: number; alerted?: boolean }
const LIMIT = 0.10;

manager.cron('*/5 * * * *', () => {
  const value = manager.getLatentCapital();
  if (!Number.isFinite(value) || value <= 0) return;
  const state = manager.getStatus<DrawdownState>('default');
  const peak = Math.max(state.peak ?? value, value);
  state.peak = peak;
  const drawdown = 1 - value / peak;
  if (drawdown >= LIMIT && state.alerted !== true) {
    state.alerted = true;
    manager.sendNotification(`Your portfolio is **${(drawdown * 100).toFixed(1)}%** below its high since monitoring began (**${value.toFixed(0)}** vs ${peak.toFixed(0)}).`);
  } else if (drawdown < LIMIT && state.alerted === true) {
    state.alerted = false; // re-arm once it recovers above the line
  }
});
```

The high is the peak observed since the agent started, not the portfolio's all-time high. Like the trailing stop, it runs against a real or demo portfolio, not in a backtest.

# Notification channels

## Overview

Obside can notify you on multiple **channels** when an agent triggers an alert or when an important activity occurs on your account. Three channels are available today:

* **Telegram**
* **Push notifications** (mobile app, browser or installed web app)
* **Email**

{% hint style="info" %}
Channel configuration is done from **Settings → Notifications**. You can enable or disable each channel independently, and choose which ones to use for each agent.
{% endhint %}

***

## Email

Email notifications are enabled by default. You can configure several categories:

* **Activity alerts** — email alerts when one of your agents triggers or when there's important activity on your account.
* **Weekly brief** — a heads-up when your weekly portfolio brief is ready, after the market week closes.
* **Getting-started tips** _(optional)_ — occasional emails to help you get set up.
* **News and offers** _(optional)_ — product news, new features and occasional offers.

***

## Telegram

To receive instant notifications on Telegram:

1. From **Settings → Notifications**, click **Connect** next to Telegram.
2. You're redirected to the Obside bot.
3. Click **Start** in the bot to authorize message delivery.

{% hint style="success" %}
Once connected, Telegram becomes one of the fastest and most reliable channels to receive your alerts in real time.
{% endhint %}

{% hint style="danger" %}
If you see **"Telegram cannot send you messages"**, open the conversation with the bot and send it a message to re-authorize delivery.
{% endhint %}

***

## Push notifications

Push notifications reach your devices **even when Obside is closed**. You can enable them on several devices in parallel (phone, computer, tablet).

* **On your phone**, the simplest path is the **Obside app** for [iPhone](https://apps.apple.com/app/obside/id6774629000) and [Android](https://play.google.com/store/apps/details?id=com.obside.webapp): sign in and allow notifications when the app asks.
* **In your browser**: from **Settings → Notifications**, click **Enable** in the Push notifications section, then allow notifications in the browser's prompt.
* On iPhone/iPad **without the app**, first add Obside to your **home screen** to be able to receive push notifications from the web version.

From **Settings → Notifications** you can then **manage the list of registered devices** and send a **test notification** to verify everything works.

***

## Choosing channels for an agent

When **launching an agent** that has the **Notifications** capability, Obside asks you to select **at least one channel** among **Telegram, push notifications and email**. You can enable several in parallel. You can also choose whether the agent notifies you about **general activity**, **trade executions**, or both.

See [Agent capabilities](/docs/agents/capabilities.md) to understand how the Notifications capability is enabled.

# How it works

## Credits

Obside measures the **AI work** it does for you in credits: a quick question uses little, building an agent uses more, and anything without AI is free. You don't see them as a number. Your plan includes a **monthly AI usage allowance**, and the app shows how much of it you have used as a **gauge from 0 to 100%**.

On **Plus**, the monthly allowance covers **about 100 agent creations**, or any mix of questions, agent creations and AI-powered agents. **Pro** covers about 300 and **Max** about 500. The allowance resets **at each renewal, every month on your subscription date** (see [Renewal](#renewal)). Credits you earn as rewards, and credits you bought before top-ups were discontinued, become [bonus usage](#bonus-usage), which takes over once the monthly allowance is used up.

***

## When are credits consumed?

Your monthly usage goes up **only when AI works on your behalf**. These don't count toward it:

* placing, changing or closing orders, in live or paper trading
* notifications, emails and webhooks sent by your agents
* prices, charts, indicators and portfolio data
* running a backtest (with one exception, see [Backtests](#backtests))

The figures below were observed in September 2026, except agent creations, which are estimated for the code-generation pipeline in use since October 2026. They are given as a share of the **Plus monthly allowance** (on Pro, each share is three times smaller, and five times smaller on Max). They are indicative: the actual cost depends on each request.

### In the chat

Each message uses **all the AI work needed to answer it**: understanding your request, fetching data, analyzing it, and writing or editing an agent's code.

| What you do | Typical share of the Plus monthly allowance |
|---|---|
| Ask a question or request an analysis | About 0.4%, usually under 1% |
| Create or edit an agent | About 1% (October 2026 estimate), more for a complex agent |
| Dictate a message by voice | About 0.2% per minute of audio |
| Start a live voice conversation | About 0.6% per session, plus the messages exchanged |
| Import a demo portfolio from a screenshot or file | Under 0.1% |

From Claude Code, Codex, Claude or ChatGPT, through [AI connections](/docs/platforms/ai-connections.md), reading your data doesn't count toward your monthly usage. Tools that put Obside's AI to work, such as building an agent, count the same way as in the chat.

### Running agents

An agent adds to your monthly usage **only when its own logic calls AI**. Trading itself is free: nothing is counted per order, trade or notification.

* **Rule-based agents** act on precise conditions: price levels, indicators, schedules, portfolio rules. They use **nothing while they run**, whatever they trade or send, unless they follow an X account (see below). Most agents work this way.
* **AI-powered agents** ask Obside's AI to read, research or decide while they run: for example to summarize the news, judge a post, research a company on the web or pick assets. Each of these AI steps counts when it runs.

| Event in a running agent | Share of the Plus monthly allowance |
|---|---|
| An AI analysis or decision | Usually under 0.1%, more with a more capable model or web research |
| A web search within an AI analysis | About 0.15% per search |
| A news search | Well under 0.01% |
| A news or alternative-data event checked against the agent's condition | Under 0.01% per event checked, whether or not it matches (most unrelated events are filtered out first, for free) |
| A post from an X account the agent follows | Under 0.1% per post, counted once even if several of your agents follow the account |
| Orders, trades, notifications, webhooks, prices and data | Free |

Posts from Telegram channels are free. Obside interrupts a single AI analysis once it has used about 6% of the Plus monthly allowance.

What makes an AI-powered agent use more:

* **How often its AI step runs.** An agent that analyzes the market on every 1-minute candle adds to your usage every minute, while one that runs once a day does so once a day.
* **The AI model.** More capable models cost more per step. When an agent lets you choose its model, its settings show the relative cost as $, $$ or $$$.
* **Research.** Web research costs more than a news search.
* **What it listens to.** A listener counts each event it receives, so a busy X account or news topic costs more than a quiet one.

For reference, an AI-powered agent used about 0.4% of the Plus monthly allowance on a typical day, and about 6% or less on 9 days out of 10.

### Backtests

Running a backtest **doesn't count toward your monthly usage** (backtests require a paid plan). The exception is agents that analyze the posts of an X account with AI: in a backtest, each replayed analysis counts like a live one, so a long backtest of such an agent can use a large share of the allowance.

{% hint style="info" %}
**Weekly brief:** on **paid plans**, the weekly portfolio brief is **included** and doesn't count toward your monthly usage. On the **Free plan**, it uses credits you bought earlier, if any remain.
{% endhint %}

***

## When you run out of credits

Running out now means reaching **100% of your monthly usage**, once any [bonus usage](#bonus-usage) is spent too. Until the next reset:

* **AI actions are paused**: new messages to the assistant, agent creations and the AI steps of your agents.
* **Rule-based agents keep running**: they don't use AI.
* **AI-powered agents keep running.** Obside closes and cancels nothing because of the limit, and their stops and coded exits keep working. Only their AI steps go unanswered, so no decision that depends on AI is taken until the reset, an exit included. If you would rather not stay invested without AI, stop the agent and choose to close its positions: that closes the trades it opened and cancels its pending orders, but anything it bought with simple orders stays in your portfolio.
* **Listeners skip events.** News, alternative-data and X-post listeners skip new events until the reset.
* **AI calls are answered again at the reset.** What each agent does then depends on how it is built: one that gives up after a few failed AI calls may not try again, so check its activity after the reset.

Obside warns you when **20%**, then **5%**, of your monthly usage is left (bonus usage included), in the app, by push and by email, with the AI-powered agents concerned and the time of the reset. To keep AI for the agents that matter most, stop the ones you need least, or move to a plan with more AI usage.

***

## Renewal

Your monthly allowance **resets at each renewal, every month on your subscription date**, and the gauge goes back to 0%. Yearly plans work the same way: the usage resets every month on the day you subscribed. If you subscribed on the 29th, 30th or 31st, the reset falls on the last day of shorter months. Unused usage **doesn't roll over** to the next month.

The **Free plan** includes **no AI usage allowance**.

***

## Bonus usage

Some credits sit outside the monthly allowance and act as **bonus usage**:

* credits earned as **rewards**, such as an approved listing in the [Library](/docs/agents/library.md) or your birthday gift
* credits you **bought before top-ups were discontinued** (see [Buying more credits](#buying-more-credits))

Obside uses bonus usage **automatically once your monthly usage reaches 100%**, so AI keeps working until the reset. Rewards don't expire, and purchased credits expire one year after purchase.

***

## Allocation per plan

* **Free**: no AI usage allowance
* **Plus**: a monthly allowance that covers about 100 agent creations
* **Pro**: about 300 agent creations a month, three times Plus
* **Max**: about 500 agent creations a month, five times Plus

Monthly and yearly billing get the same monthly allowance. These are standard allowances: a yearly subscription paid when its plan included more AI usage keeps that larger allowance until its year ends, and a Plus subscription taken before Pro and Max came back keeps its allowance until its next renewal. Your usage gauge always measures against the allowance you actually have. For the full breakdown, see [Pricing](/docs/misc/pricing.md#plans).

***

## Buying more credits

**Extra credits can no longer be bought.** Top-ups have been discontinued, so your plan's monthly allowance, plus any bonus usage, is what you can use. Pro and Max include three and five times the AI usage of Plus.

Credits you bought before the change **remain usable as bonus usage until they expire**, one year after purchase, even if your plan ends.

# Pricing

Obside has four plans: **Free**, to connect a portfolio and view your holdings, and three paid plans, **Plus**, **Pro** and **Max**, which add Obside Chat, agents, smart alerts, backtests and AI connections (MCP). Pro and Max add live trading, backtests on 1-minute candles, trade alerts from the arenas and the Signals API.

Paid plans are billed **monthly** or **yearly**, and yearly billing costs **about 20% to 23% less** than 12 monthly payments. Prices on this page are in US dollars. Checkout shows your local currency where available and confirms the final amount, including taxes. The Free plan needs no credit card.

## 7-day free trial

Your first subscription to Plus, Pro or Max includes a **7-day free trial** when you subscribe on the web. A payment method is required. Cancel before the trial ends and you won't be charged.

Subscribing to Pro from an arena page is the exception: the subscription starts right away, without a trial.

***

## Plans

### Free: Connect a portfolio and discover Obside

For connecting an account and seeing your holdings.

**$0.** No credit card required.

* **1 broker connection**
* View your holdings and their performance
* A **demo portfolio** with sample holdings
* **No AI usage**: Obside Chat, agents, smart alerts and backtests come with a paid plan

### Plus: AI insights & alerts on what moves your portfolio

For those who want AI agents that watch their portfolio and the markets and alert them, in the app or from their own AI tools.

**$29/month** or **$279/year**.

* **Monthly AI usage**: one allowance for chat, agent creation and AI-powered agents, enough for about **100 agent creations a month** (see [Credits](/docs/credits/how-it-works.md))
* **AI connections (MCP)**: use Obside from **Claude Code**, **Codex**, Claude, ChatGPT or any other MCP client (see [AI connections](/docs/platforms/ai-connections.md))
* **Obside Chat**
* Up to **10 active agents**
* **Portfolio monitoring** (AI insights that come to you)
* **Smart alerts**
* **Conditional orders** (stop-loss, take-profit and other exit rules inside your agents)
* **Paper trading** (demo)
* **Unlimited broker connections**
* Full market data for your agents: **price, macro & news**
* Backtests on **20 years** of historical data
* Smallest backtest timeframe: **1 hour**. This is the shortest candle timeframe an agent can use in a backtest; longer timeframes are included, and it is separate from how many years of history you can test.
* Everything in Free

### Pro: Automate your strategy

For those who want to go from idea to live automation, and follow the arenas.

**$59/month** or **$549/year**.

* **Monthly AI usage** for about **300 agent creations a month**
* Up to **25 active agents**
* **Live trading** _(US & Canada, see [Availability by region](/docs/platforms/availability-by-region.md))_
* **Trade alerts** from the arenas' AI traders, in the app, by push and on Telegram
* The **Signals API**, to connect the arenas' live signals to your own tools
* Smallest backtest timeframe: **1 min**
* Everything in Plus

### Max: Unlimited agents, maximum AI usage

For advanced users who want maximum capacity.

**$89/month** or **$819/year**.

* **Monthly AI usage** for about **500 agent creations a month**
* **Unlimited active agents**
* Smallest backtest timeframe: **1 min**
* Everything in Pro

{% hint style="info" %}
**Already subscribed?** If you subscribed to Plus before Pro and Max came back, you keep the features you signed up with (live trading, trade alerts, the Signals API and up to 25 active agents) until your next renewal; your plan then follows the Plus described above. A yearly subscription paid when its plan included more AI usage keeps that larger allowance until its year ends. The usage gauge in your account always measures against the allowance you actually have.
{% endhint %}

***

## Feature breakdown

| Feature | Free | Plus | Pro | Max |
|---|:-:|:-:|:-:|:-:|
| Price (USD) | $0 | $29/mo · $279/yr | $59/mo · $549/yr | $89/mo · $819/yr |
| Monthly AI usage | ✗ | About 100 agent creations | About 300 agent creations | About 500 agent creations |
| AI connections (MCP): Claude Code, Codex, Claude, ChatGPT | ✗ | ✓ | ✓ | ✓ |
| Broker connections | 1 | Unlimited | Unlimited | Unlimited |
| Holdings & demo portfolio | ✓ | ✓ | ✓ | ✓ |
| Obside Chat | ✗ | ✓ | ✓ | ✓ |
| Active agents | ✗ | 10 | 25 | Unlimited |
| Smart alerts | ✗ | ✓ | ✓ | ✓ |
| Paper trading (demo) | ✗ | ✓ | ✓ | ✓ |
| Market data for agents | ✗ | Price, macro & news | Price, macro & news | Price, macro & news |
| Backtesting | ✗ | 20 years | 20 years | 20 years |
| Smallest backtest timeframe | ✗ | 1 hour | 1 min | 1 min |
| Portfolio monitoring | ✗ | ✓ | ✓ | ✓ |
| Conditional orders | ✗ | ✓ | ✓ | ✓ |
| Live trading | ✗ | ✗ | ✓ | ✓ |
| Trade alerts from the arenas | ✗ | ✗ | ✓ | ✓ |
| Signals API | ✗ | ✗ | ✓ | ✓ |

{% hint style="info" %}
**Live trading** requires a **Pro** or **Max** plan **and** depends on your region (US & Canada today). See [Availability by region](/docs/platforms/availability-by-region.md).
{% endhint %}

***

## Need more credits?

**Extra usage can't be bought.** Once your monthly usage reaches 100%, Obside draws on any bonus usage you have (rewards, and credits you bought before top-ups were discontinued), then pauses AI actions until your usage resets, every month on your subscription date (yearly plans included). Agents keep running and keep their positions: only their AI steps wait for the reset. If you regularly need more, Pro and Max include three and five times the AI usage of Plus. See [Credits → Bonus usage](/docs/credits/how-it-works.md#bonus-usage).

***

## Obside for businesses

For businesses looking for more control, dedicated support and a tailored experience.

[**Contact us**](/docs/readme.md#contact-us) for an offer adapted to your needs.

# Referral

Obside lets you earn rewards by inviting your friends to join the platform.

## How does it work?

1. **Activate your referral** from your Obside account (free).
2. **Share your referral link** with your network.
3. Your friend gets **20% off their first subscription**.
4. You earn a reward of **50%** of the plan's **monthly** price, or **20%** of its **yearly** price (up to **$163.80**).

## Reward schedule

| Plan chosen by the referee | Reward (monthly plan) | Reward (yearly plan) |
| -------------------------- | --------------------- | -------------------- |
| Plus                       | $14.50                | $55.80               |
| Pro                        | $29.50                | $109.80              |
| Max                        | $44.50                | $163.80              |

## Conditions

Each user has a **personal referral link** accessible from their settings.

The reward is credited as soon as your referee **subscribes to a paid plan**, and you can transfer it to your **bank account at any time** from the referral portal.

The 20% discount for the referee applies immediately on their **first subscription**.

# Obside Score

The **Obside Score** is a synthetic estimate of the quality of a **backtest**. It combines three dimensions:

* **Performance** over the tested period.
* **Risk** (drawdown, volatility, loss profile).
* **Statistical reliability** (number of trades, robustness, stability).

The score is displayed automatically at the end of each backtest, and lets you quickly compare multiple versions of the same agent or several agents against each other. It is summarized by a rating: **Excellent**, **Good**, **Average** or **Poor**.

Under the hood, it weighs several dimensions, including the CAGR-to-drawdown ratio, the number of trades, expectancy, the concentration of gains (Pareto 80/20), the share of quarters spent in the market, the average drawdown duration, and the percentage of quarters beating the market.

{% hint style="warning" %}
The Obside Score is provided **for informational purposes only** and does not constitute investment advice.
{% endhint %}

{% hint style="info" %}
The Obside Score applies to **backtests** only. It doesn't reflect live execution performance, which depends on current market context and may diverge from historical results.
{% endhint %}

# Labs

**Labs** is Obside's experiments space — live benchmarks you can watch, compare and learn from. Open it from the **Labs** section in the sidebar.

## AI Trading Benchmark

The flagship experiment: **AI models trading live and fully autonomously** — same capital, same prompts, same markets. Watch how the leading LLMs behave as traders, in real conditions and in real time:

* **Live view** — each model's open positions, direction (long/short), size and unrealized P&L.
* **AI chat** — the models' actual reasoning logs behind each decision.
* **Rankings** — balance, return, P&L, fees, max drawdown, win rate, number of trades, average duration.
* **Seasons** — the benchmark runs in seasons, so models can be compared over a defined period.

## Other benchmarks

Labs also hosts themed benchmarks and experiments, each with its members, performance charts and a **reference index** to compare against (buy & hold over the same period).

{% hint style="info" %}
Labs is about **observation and comparison** — it's not investment advice, and benchmark performance says nothing about future results.
{% endhint %}
