Read and draw on a live crypto, stock or forex chart, run scripts and backtests.
Free, open-source charts for crypto, stocks and forex, and your AI agent can drive them.
Indicators, drawing tools, your own scripts and strategy backtests, alerts, bar replay and up to four charts on screen. No account, no paywall, no limit on indicators. It runs in your browser and keeps everything on your machine.
Claude Code, Codex, Cursor or Gemini CLI can read the chart, draw support and resistance, add indicators, write scripts and set alerts while you watch.
Open Kandle in your browser · Connect your agent · Write a script

claude mcp add kandle -- npx -y --package=https://github.com/Kernel-Hunter/kandle/releases/download/v0.5.0/kandle-mcp-0.5.0.tgz kandle-mcp
Then open Kandle, click Agent, paste the pairing code your agent gives you and turn on Agent bridge. Ask it something like "mark the last swing high and low on BTC and draw a Fibonacci retracement between them". Setup for other agents is below.

? in the app to see them all.request.security.
| Keys | Action |
|---|---|
| Any letter or digit | Search symbols |
/ or Alt+I | Indicators |
Alt+S | Script editor |
Ctrl+Enter or Cmd+Enter | Add or update the script on the chart (in the editor) |
Alt+1 to Alt+7 | Timeframes 1m to 1W |
Alt+T | Trend line |
Alt+R | Ray |
Alt+H | Horizontal line |
Alt+V | Vertical line |
Alt+B | Rectangle |
Alt+F | Fibonacci retracement |
Alt+P | Price range |
Alt+N | Text note |
Alt+M | Magnet on or off |
Delete | Delete the selected drawing |
Esc | Cancel the current drawing or close a dialog |
Alt+L | Switch between light and dark |
Kandle ships an MCP server, kandle-mcp. Your agent starts it, and the Kandle tab in your browser connects to it over a WebSocket on 127.0.0.1. The agent can then read the chart and change it while you watch.
get_bridge_status and shows a code like K7QD-9MXR-2HPA. The code is also saved in ~/.kandle/bridge-token.The code is saved in your browser and in ~/.kandle/bridge-token, so you only pair once.
This works with the hosted app and with a copy you run yourself. With the hosted app, your browser may ask whether the page can reach devices on your local network; allow it so the page can talk to kandle-mcp on 127.0.0.1.
npx runs kandle-mcp from a GitHub release, so you only need Node.js 22.12 or newer. When a new version of Kandle comes out, change the version in the URL; get_bridge_status tells your agent when the app is newer than its server. To run it from a clone instead, use pnpm install && pnpm build and point your agent at node /path/to/kandle/packages/mcp/bin/kandle-mcp.js.
Claude Code:
claude mcp add kandle -- npx -y --package=https://github.com/Kernel-Hunter/kandle/releases/download/v0.5.0/kandle-mcp-0.5.0.tgz kandle-mcp
Codex (~/.codex/config.toml):
[mcp_servers.kandle]
command = "npx"
args = ["-y", "--package=https://github.com/Kernel-Hunter/kandle/releases/download/v0.5.0/kandle-mcp-0.5.0.tgz", "kandle-mcp"]
Cursor (~/.cursor/mcp.json or .cursor/mcp.json in a project) and Gemini CLI (~/.gemini/settings.json):
{
"mcpServers": {
"kandle": {
"command": "npx",
"args": ["-y", "--package=https://github.com/Kernel-Hunter/kandle/releases/download/v0.5.0/kandle-mcp-0.5.0.tgz", "kandle-mcp"]
}
}
}
Claude Desktop: download kandle-mcp-0.5.0.mcpb and open it to install the server.
Kandle is also listed in the MCP Registry as io.github.Kernel-Hunter/kandle, so clients that browse the registry can find it.
| Tool | What it does |
|---|---|
get_bridge_status | Connection state and the pairing code |
get_chart_state | Symbol, timeframe, last bar, visible range, indicators with latest values, drawings with ids, alerts, replay and layout |
set_symbol, set_timeframe | Change the market or bar size. Exchange csv opens a dataset you imported |
get_candles | OHLCV bars by count or time range, optionally with indicator values per bar |
add_indicator, remove_indicator, list_indicators | Manage indicators and list the 36 available |
draw | Trend line, horizontal line or ray, vertical line, ray, extended line, rectangle, Fibonacci retracement, text note, price range, all by price and time |
remove_drawing, clear_drawings | Remove drawings |
screenshot | PNG of a chart, returned as an image |
set_alert, list_alerts, remove_alert | Alerts on price, drawn lines and indicators |
start_replay, step_replay, stop_replay | Bar replay |
set_layout | 1, 2 or 4 charts, sync options, symbol and timeframe per chart |
run_script | Adds a Kandle script to the main chart and returns its id, input values and latest plot values, or the error with line and column. inputs sets input values by title and id replaces a script added earlier. For a strategy it also returns the backtest summary and the last 20 trades, and strategy changes its backtest settings |
get_backtest | Settings (and which ones differ from the script), summary and up to 500 trades of a strategy script on the chart |
Most tools take an optional pane (1 to 4) when several charts are showing. Times can be ISO 8601 dates or Unix times in seconds or milliseconds. Inputs are validated and errors say what to fix.
skills/kandle/SKILL.md is an agent skill with chart analysis workflows: support and resistance, trend, indicator confluence, and annotating a chart with an explanation. Copy it into your agent's skills folder (for Claude Code, ~/.claude/skills/kandle/).

127.0.0.1. Nothing is reachable from other machines.localhost, 127.0.0.1 or [::1], so a website you visit cannot connect even if it guesses the port. The hosted app at https://kernel-hunter.github.io is also allowed. Add other origins, such as your own hosted copy, with KANDLE_ALLOWED_ORIGINS.Host header is not a loopback address, which blocks DNS rebinding.~/.kandle/bridge-token. That is the same trust level as the agent itself.Settings: --port or KANDLE_PORT (default 7457, change the port in the Agent dialog to match), KANDLE_TOKEN for a fixed code, KANDLE_HOME for the token folder.
Kandle Script lets you write your own indicators and backtest strategies. It is compatible with the scripting language most chart users already know, so many existing indicator scripts run with few or no changes.
Click Scripts (or press Alt+S) to open the editor under the chart. Write a script, or pick one from Examples, and press Ctrl+Enter (Cmd+Enter on a Mac) to add it to the chart. Errors are underlined as you type, with the line, column and message below the editor. Pick a script from the list at the top left of the editor to change it later.
//@version=5
indicator("EMA cross", overlay = true)
fastLen = input.int(9, "Fast", minval = 1)
slowLen = input.int(21, "Slow", minval = 1)
fast = ta.ema(close, fastLen)
slow = ta.ema(close, slowLen)
plot(fast, "Fast", color.orange, 2)
plot(slow, "Slow", color.blue, 2)
plotshape(ta.crossover(fast, slow), "Up", shape.triangleup, location.belowbar, color.green)
Scripts with overlay = true draw on the price chart; the others get their own pane. Inputs show up in the Indicators dialog, where you can change them without touching the code. Scripts and their input values are saved with your layout. When a new candle arrives or the last one changes, only the last bar is calculated again; a full run over 1,500 bars takes a few milliseconds for typical scripts.
The indicator examples are a moving average cross, RSI with bands, MACD, Bollinger Bands, Supertrend, VWAP bands, Stochastic, an ATR trailing stop, a Donchian breakout, volume spikes and a daily EMA trend filter that uses request.security. The strategy examples are an EMA cross that is always in the market, RSI mean reversion with a stop and a target, and a Donchian channel breakout.

Declare a script with strategy() instead of indicator() and it becomes a backtest. Orders placed with strategy.entry, strategy.exit and strategy.close are filled by a broker emulator on the bars loaded in the chart. Each fill is drawn on the price chart as an arrow with its signal name, with a dashed line from entry to exit (green for a win, red for a loss). The Strategy tab under the chart has two views:
//@version=5
strategy("EMA cross", overlay = true, initial_capital = 10000,
default_qty_type = strategy.percent_of_equity, default_qty_value = 100,
commission_type = strategy.commission.percent, commission_value = 0.05)
fast = ta.ema(close, input.int(12, "Fast"))
slow = ta.ema(close, input.int(26, "Slow"))
if ta.crossover(fast, slow)
strategy.entry("Long", strategy.long)
// Stop 3% below and target 6% above the signal bar's close.
strategy.exit("Exit", "Long", stop = close * 0.97, limit = close * 1.06)
if ta.crossunder(fast, slow)
strategy.close("Long")
plot(fast, "Fast", color.orange)
plot(slow, "Slow", color.blue)
The values in the strategy() call are the script's defaults. To try others without editing the code, open the Indicators dialog: a strategy has a Backtest settings group under its inputs with initial capital, order size and its type, commission and its type, slippage in ticks, pyramiding, and filling market orders on the bar close. A change runs the backtest again, and the trades on the chart and the Strategy tab follow. Changed settings are highlighted and saved with the layout like input values, and Reset goes back to the values in the script.
How orders fill:
process_orders_on_close = true, market orders fill at the close of the bar that placed them.slippage ticks worse. Commission is charged on every entry and every exit.pyramiding (0, the default, allows one).strategy.exit levels given in ticks (profit, loss, trail_points, trail_offset) are measured from each trade's fill price. An exit placed on the same bar as its entry protects the trade from the bar it fills on.default_qty_type and default_qty_value unless qty is given. Cash and percent of equity sizes use the fill price. Quantities are not rounded to lot sizes and there are no margin calls.How the figures are measured: max drawdown is the largest fall of bar-close equity from an earlier peak. The Sharpe ratio uses the equity at the end of each UTC day (each bar on daily and weekly charts): the mean return over its standard deviation, annualized over 365 days, with a risk-free rate of 0. Buy and hold is the price change from the first loaded close to the last close, applied to the initial capital. Open profit counts the commission already paid on entry.
Supported: strategy() with initial_capital (default 100000), default_qty_type (strategy.fixed, strategy.cash, strategy.percent_of_equity), default_qty_value, commission_type (strategy.commission.percent, cash_per_contract, cash_per_order), commission_value, slippage, pyramiding and process_orders_on_close. Orders: strategy.entry, strategy.order, strategy.exit (profit, limit, loss, stop, trail_price or trail_points with trail_offset, qty, qty_percent, comment_profit, comment_loss, comment_trailing), strategy.close (with qty, qty_percent and immediately), strategy.close_all, strategy.cancel and strategy.cancel_all, with strategy.long and strategy.short. State: strategy.position_size, position_avg_price, equity, netprofit, openprofit, grossprofit, grossloss, max_drawdown, closedtrades, opentrades, wintrades, losstrades, eventrades, initial_capital, and per-trade values such as strategy.closedtrades.profit(i) and strategy.opentrades.entry_price(i). currency and calc_on_every_tick are accepted and ignored; OCA group arguments are accepted and not modelled.
Agents get the same results: run_script returns the backtest summary and the last 20 trades of a strategy, and get_backtest returns up to 500 trades. run_script also takes strategy, backtest settings named like the strategy() arguments, such as {"initial_capital": 5000, "commission_value": 0.1}; null puts a setting back to the script's value, and with id it reruns a strategy already on the chart. In the page, window.kandle.setStrategySettings(id, settings) does the same for a script on the chart, listScripts() shows each strategy's own settings and the ones in use, and getBacktest() lists the settings that differ in changedSettings.

request.security(symbol, timeframe, expression, gaps, lookahead) returns the value of expression calculated on another timeframe, or on another symbol from the chart's data source.
//@version=5
indicator("Daily EMA trend", overlay = true)
dailyEma = request.security(syminfo.tickerid, "D", ta.ema(close, 20))
ethClose = request.security("ETHUSDT", "60", close)
plot(dailyEma, "Daily EMA", color.orange, 2, plot.style_stepline)
symbol is syminfo.tickerid (or syminfo.ticker) for the chart's symbol, or a symbol on the same data source such as "ETHUSDT" while you chart BTCUSDT on Binance. A prefix such as "BINANCE:ETHUSDT" picks that data source when Kandle has it and is ignored otherwise.timeframe is "1", "5", "15", "60", "240", "D" or "W" ("1D", "1W", "1h" and "4h" also work), or timeframe.period or "" for the chart's own.input.symbol, input.timeframe, input.string), variables set once to one of these, or text joined from them such as "BINANCE:" + sym. Anything that can change from bar to bar, such as close > open ? "ETHUSDT" : "SOLUSDT", gives the error "request.security needs constant symbol and timeframe". A script can request up to 5 different symbol and timeframe pairs; an input counts as one pair whatever it holds.ta.ema(close, 20) in a "D" request is a 20-day EMA. Variables, inputs and functions it uses are calculated again on the requested bars. Variables declared with var, variables changed with :=, function parameters and loop variables cannot be used in it. Tuples work: [o, c] = request.security(syminfo.tickerid, "D", [open, close]).Inputs let you pick the series in the Indicators dialog. Changing one runs the script again and loads the new series; series loaded before stay cached, so switching back is instant.
//@version=5
indicator("HTF close", overlay = true)
tf = input.timeframe("D", "Higher timeframe")
sym = input.symbol("ETHUSDT", "Symbol")
plot(request.security(sym, tf, close))
No lookahead by default. With lookahead = barmerge.lookahead_off, a chart bar gets the value of the last requested bar that had closed when the chart bar closed. On a 1H chart a daily value changes on the last hourly bar of each day, and the bars before it show the previous day. History never sees later data. Only the newest chart bar, while the current day is still forming, shows that day's value as it develops.
lookahead = barmerge.lookahead_on uses the requested bar that contains the chart bar, even before it has closed. On history this looks ahead: the 09:00 bar sees that day's close. request.security(syminfo.tickerid, "D", close[1], lookahead = barmerge.lookahead_on) is the usual way to get the previous day's close on every bar of today without looking ahead.gaps = barmerge.gaps_on returns na except on the chart bar where a new requested bar closes (with lookahead on, the first chart bar inside each requested bar).Requested series load through the chart's data source with the same caching and rate limits, from 500 bars before the first bar on the chart, and load more when you scroll back. On sources without a key they update live; on sources that need a key they refresh every few minutes to stay within the free plan. While a series loads its values are na, and if it cannot load the script shows the error. run_script waits for the data before it returns.
//@version=5 and //@version=6 headers, comments, indented blocks, line wrapping inside brackets or after an operator, var and varip, type names on declarations (float x = na), =, := and += -= *= /= %=, ?:, and, or, not, if / else if / else, switch with or without a value, for i = a to b by step, for x in array, for [i, x] in array, while, break, continue, functions on one line or several, default and named arguments, tuples ([a, b] = f()), user methods (method f(float x) =>) and method calls such as values.push(x).x[n]. na, na(), nz() and fixnan(). Variables declared with var keep their value from bar to bar. Each call of a function keeps its own history.open, high, low, close, volume, hl2, hlc3, ohlc4, hlcc4, time, bar_index, last_bar_index, year, month, dayofmonth, dayofweek, hour, minute, barstate.*, syminfo.ticker, syminfo.mintick, timeframe.period, timeframe.change().ta.*: sma, ema, rma, wma, vwma, hma, rsi, macd, stoch, atr, tr, bb, bbw, kc, cci, mfi, obv, vwap (with optional anchor and bands), supertrend, dmi, wpr, change, mom, roc, highest, lowest, highestbars, lowestbars, crossover, crossunder, cross, rising, falling, stdev, variance, dev, cum, valuewhen, barssince, pivothigh, pivotlow, linreg, correlation, percentrank, median.math.*: abs, max, min, avg, sum, round, floor, ceil, sqrt, pow, log, log10, exp, sign, trigonometry, pi and e.input, input.int, input.float, input.bool, input.string (with options), input.source, input.color, input.price, input.timeframe (a dropdown of Kandle's timeframes, or its options, plus Chart, which follows the chart's timeframe) and input.symbol (a text field that suggests symbols from the chart's data source; empty follows the chart's symbol). input.timeframe(timeframe.period) and input.symbol(syminfo.tickerid) start out following the chart.color.* constants, color.new, color.rgb, color.from_gradient, and #RRGGBB or #RRGGBBAA literals.str.tostring, str.format, str.length, str.contains, str.upper, str.lower and a few more. array.* for float, int, bool, string and color arrays.request.security with barmerge.gaps_on, gaps_off, lookahead_on and lookahead_off. See Other symbols and timeframes.indicator() with title, shorttitle, overlay, format and precision; plot with the line, step line, histogram, columns, circles, cross and area styles, per-bar colors, linewidth and offset; plotshape; plotchar; hline; fill between two plots or two hlines; bgcolor; barcolor (on price-chart scripts).Not supported yet: request.* functions other than request.security (such as request.security_lower_tf and request.financial), strategy.risk.*, drawing objects (label, line, box, table), user-defined types and enums, import, matrix and map, and gradient fills. Using one of these gives a clear "not supported yet" error that points at the line.
Click Alerts to create and manage alerts for the main chart's symbol:
Each alert fires once, or every time (at most once per bar). When one fires you get a browser notification, a message in the app and a short sound. Kandle asks for notification permission when you create your first alert. Alerts are saved in your browser and are checked against live data while a Kandle tab is open; they do not run when the tab is closed. Alerts on a symbol shown in any of the charts on screen are checked.
Click Replay, then click a bar on the main chart. Every bar after it is hidden and indicators are calculated only on what is visible. Use the replay bar to play, pause, step forward one bar and pick a speed from 1x to 10x (bars per second). Keyboard: Space plays or pauses, Right arrow steps, Esc exits. Exiting returns the chart to live data. Changing the symbol or timeframe also ends replay. Replay can go back as far as the exchange has history; Kandle loads older bars when needed.
Click Layout to show 1 chart, 2 side by side or 4 in a grid. The first chart is the main one and uses the top bar and drawing tools. The others each have their own symbol, timeframe and indicators in a small header. Turn on sync to keep the crosshair, the timeframe or the symbol the same across charts. The layout and each chart's settings are saved in your browser.
strategy.risk.* and order groups (OCA).You need Node.js 22.12 or newer and pnpm.
pnpm install
pnpm dev
Then open the URL that Vite prints, usually http://localhost:5173.
Other commands:
pnpm test # unit tests
pnpm build # type check, production build in dist/, and build kandle-mcp
pnpm preview # serve the production build
pnpm mcp # build and run kandle-mcp (normally your agent runs it)
To build for a subpath, such as a GitHub Pages project site, set BASE_PATH:
BASE_PATH=/kandle/ pnpm build
src/
config.ts app name and identifiers
data/ market data adapters behind a DataSource interface, API keys, rate limits
chart/ chart engine glue, extra indicators, extra drawing tools
store/ layout persistence
script/ Kandle Script: lexer, parser, runtime, built-ins, chart renderer
ui/ symbol search, indicator panel, dialogs
agent/ agent bridge client, request handlers, Agent dialog
alerts/ alert evaluation, notifications, alert manager
replay/ bar replay and the data loader it sits behind
layout/ multi-chart layouts
features/ connects the above to the app
packages/mcp/ kandle-mcp, the MCP server
skills/kandle/ agent skill for chart analysis
tests/ unit tests
To add an exchange, implement the DataSource interface in src/data/types.ts and register it in src/data/sources.ts.
Click Data in the top bar to see every source, add keys and test them. In symbol search, pick the source above the results.
| Source | Markets | Key needed | Updates |
|---|---|---|---|
| Binance | Crypto | No | Live |
| Coinbase | Crypto | No | Live |
| Bybit | Crypto | No | Live |
| OKX | Crypto | No | Live |
| Kraken | Crypto | No | Live. History is limited to the latest 720 bars per timeframe. |
| Twelve Data | Stocks, ETFs, forex, crypto | Free key | Live for the symbols your plan streams, otherwise refreshed once a minute |
| Alpaca | US stocks and ETFs | Free key ID and secret | Live from the IEX feed (volume covers IEX trades only) |
| Polygon | Stocks, ETFs, forex, crypto, indices | Free key | End of day on the free plan, marked Delayed |
| CSV | Your own data | No | Never. See CSV import |
Free keys:
Finnhub is not included: its free plan does not include historical candles, so it cannot draw a chart.
How keys are handled:
kandle:apikey:<source> and are never sent anywhere except to that provider's own API. Kandle has no server.Free plans have tight limits, so Kandle caches responses, counts requests against each plan's per-minute limit, and backs off when a provider answers 429. When a limit is reached the chart says "Free plan limit reached, try again in N s" instead of retrying in a loop.
Stock charts only have bars for trading hours. Nights, weekends and holidays get no empty bars, and the chart places bars side by side, so there are no gaps on the time axis.
Market data comes straight from each provider's official public API in your browser. Kandle does not store or redistribute it, and each provider's terms apply. Exchange availability depends on your region; for Binance the app uses the public market data hosts first.

Load your own OHLCV data: click Import CSV in symbol search or in the CSV card of the Data dialog and pick a file, or drop a file on the chart. Kandle reads it in your browser and shows what it found, you give the dataset a name, and it opens on the chart. Nothing is uploaded.
Accepted files:
time, date, datetime, timestamp or open time), open, high, low, close and an optional volume, in any order and any case. Separate date and time columns are joined. A file with no header is read as time, open, high, low, close and volume.1234,5) works.2024-03-05T14:30:00Z, with a zone), YYYY-MM-DD, YYYY-MM-DD HH:mm or YYYY-MM-DD HH:mm:ss (read as UTC), or Unix time in seconds or milliseconds.The timeframe comes from the median gap between bars, so daily stock data without weekends is still 1D. Bars that fit no timeframe but divide one, such as 30-minute bars, are combined into the next one (1H). The chart can show a dataset in its own timeframe and longer ones, which are built from its bars; shorter timeframe buttons are disabled.
Datasets are saved in this browser's local storage (up to 2.5 MB each and 4 MB in total, about 40,000 bars per dataset), appear in symbol search under CSV with a CSV badge, come back when you reload, and can be deleted in the Data dialog. Scripts, strategy backtests, request.security on higher timeframes, drawings, bar replay and multi-chart layouts work on them. The data never updates, so alerts on a CSV chart never fire. Agents can open a dataset with set_symbol and exchange csv.

MIT. See LICENSE.
Charts are drawn with KLineChart, licensed under Apache-2.0. The script editor uses CodeMirror, licensed under MIT. Their notices are in public/third-party-notices.txt.
This listing does not have a supported local package template. Use the maintainer’s documentation for its hosted endpoint, authentication, and client-specific setup. No install command has been inferred.
https://github.com/Kernel-Hunter/kandle/releases/download/v0.10.1/kandle-mcp-0.10.1.mcpbotherKandle works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.