~/blog / engineering / Talk to Chart: Chatting With a TradingView Chart Using Jev, GPT-5 and My Own Pine Interpreter
dispatch - Engineering
ENGINEERING10/15/2616 MIN

Talk to Chart: Chatting With a TradingView Chart Using Jev, GPT-5 and My Own Pine Interpreter

byDhruv VakharwalaBackend & Systems Engineer

Chat with a TradingView chart: TypeSafe's Jev routes asks into typed commands, GPT-5 plans the hard ones, and Pine runs in a Web Worker on my own interpreter.

⚠ TL;DR

Talk to Chart is a side hack: a chat box next to a TradingView chart. Type "mark support and resistance", "plot RSI and mark where it crosses back above 30", "pitchfork on the last three swings" or "make my RSI 21", and the chart does it. My own JavaScript engine is injected into the chart page. Jev, TypeSafe AI's new decision model, turns simple asks into typed commands in 0.41-0.85 s, and a repeat of the same ask comes back in 20-30 ms. Harder asks go to a GPT-5 model, which returns a strict JSON plan of drawings, indicators and Pine scripts. Pine runs in a Web Worker on my own interpreter, so custom indicators appear without rebuilding the chart. The first version took under 30 minutes. The rest of the afternoon went into a Pine interpreter, the full TradingView toolset, and making it feel instant. It was only an experiment - Sahi, one of India's new-generation brokers, is building the real version of this idea, and theirs is much better.

◆ EMBED𝕏 POST

The idea: a chat box any broker's chart could use

Many Indian broker apps show their charts the same way: TradingView's Advanced Charts library inside a WebView, talking to the native app over a message bridge. The chart looks much the same from app to app. Mostly the data feed and the colors change.

That made me wonder what it would take to bolt a conversation onto that chart without touching the chart itself. If the talking layer lives next to the chart instead of inside a patched copy of it, any broker that already embeds TradingView could drop it in. So I set myself three rules:

  • No patched TradingView bundles. Everything goes through the public widget API.
  • No new protocol. The chat speaks over the same bridge the app already uses, as a few extra message types: ask, draft, run these ops, clear.
  • No real market data. The demo serves synthetic candles, seeded per symbol from scripted scenes - a bull run, a crash and V-recovery, a squeeze into a breakout, a head and shoulders - with 1-minute bars aggregated up to every timeframe, so 5-minute and daily charts agree with each other.

A small test harness plays the part of the native app over the real bridge protocol, so the whole thing runs in a browser at localhost.

Code computes, the model decides, the chart draws

The first decision, and the one everything else hangs off: the models never see raw candles. Language models are bad at reading 2,000 rows of OHLC and worse at returning exact prices. So the work is split:

  1. The engine in the chart page computes. It analyzes the loaded candles - EMAs, RSI, ATR, pivots, swing highs and lows, support and resistance clusters, candle and chart patterns - and gives every important point a short name: H1 is the latest swing high, L2 the swing low before the last one, R1 the nearest resistance, F the candle you tapped. Analyzing 2,000 bars takes about 0.13 ms, once per chart state.
  2. The models decide. They get a digest of about 2-5 KB: those named anchors, the latest swing legs, every indicator and drawing already on the chart (including the ones you drew), and what you tapped. They answer with typed operations that refer to anchors, not prices.
  3. The chart draws. Each operation is executed next to the candles with exact prices, through the TradingView widget API, and appears in the next frame.

The contract between them is a list of 16 typed ops - indicator, study, study_edit, script, levels, swings, trendline, pattern, draw, hline, position, compare, note, view, clear and undo. There is no tool-calling loop. A plan is just data:

● ● ●JSON
{
  "ops": [
    { "op": "draw", "shape": "abcd_pattern", "at": ["L2", "H2", "L1", "H1"], "text": "ABCD" }
  ],
  "say": "Drew an ABCD pattern on the last four swings, L2 to H1."
}

Because draw can name any of TradingView's 80 drawing tools and study any of its 106 built-in indicators (both read straight from the live widget, with input labels and anchor counts), the model can drive the whole library: pitchforks, anchored VWAP, fixed-range volume profile, Elliott waves, Fibonacci, long and short position boxes. Adding a new capability means one new op: a Pydantic model on the server and one function in the engine. Nothing else changes.

Jev: a model that answers typed questions

Jev came out on 15 September 2026, ten days before this hack. TypeSafe AI calls it a "System One" model and describes it as "unstructured state in, typed probabilistic decisions out". It does not write text. You give it some state and a set of typed questions - pick one of these options, yes or no - and it returns the answers with a probability for every option, all in one call.

That is exactly the shape of a chat router. Most of what people type at a chart is one simple command, and a command is a handful of choices: which action, which indicator, which side, which timeframe, which item on the chart. With PydanticAI's TypeSafe model, the router is just a Pydantic class where every field is a question:

● ● ●PYTHON
class Route(BaseModel):
    """Route one chat message to the chart action it asks for."""

    action: Action
    """Which single chart action does the message ask for?"""
    study: Literal[(*STUDIES, NOT_LISTED)] | None = None
    """Which TradingView indicator by name does the message want added to the chart?"""
    asks_question: Annotated[bool, BoolCriteria(
        true="The message asks something about the chart that needs an answer.",
        false="The message only tells the assistant to draw, add, remove, switch or change something.",
    )] = False
    wants_advice: Annotated[bool, BoolCriteria(
        true="Asks whether to buy, sell or hold, for a price target or prediction, or for an entry or stop-loss.",
        false="Asks to draw, find, show, explain or describe something on the chart.",
    )] = False
    # ... indicator, pattern, sides, timeframe, chart type, topic, custom, edits, simple_task

router = Agent(TypeSafeModel("jev-latest"), output_type=Route, instructions=ROUTER_PROMPT)

The real class has about 17 questions, and one more is added on the fly: which item on the chart the message is about. Jev picks from a short label for each item, like your RSI or support levels, so it never has to spell an ID. "Make that RSI 21" works on an indicator you added yourself.

Every answer comes with a probability, which turns routing into plain code. Jev's command is used only if the action is at least 75% likely, every argument at least 60%, and the message is not a question, not a custom rule or formula, not pasted Pine and not something anchored to particular swings. Everything else goes to the language model. The thresholds are starting values, and every routing decision is logged so they can be tuned.

Jev and GPT-5 race on every message

Waiting for Jev and then calling the language model would add Jev's latency to every hard question. So both start the moment a message arrives:

  • Jev is sure it is one simple command. The server builds the ops in code from the live chart and cancels the language model call before it writes anything.
  • It is a question ("is this an uptrend?"). Code draws the evidence at about 0.45 s - swings and a trendline for trend questions, levels, RSI, ATR or volume spikes for the others - while the model writes the answer. A question never clears or edits anything.
  • Anything else. The model's plan streams in and every op is drawn the moment it is complete, so a five-step answer starts appearing after the first step.

The planner is a GPT-5 model on Azure AI Foundry, called through PydanticAI with a strict JSON schema for the plan, streamed. There are actually two deployments in the race. A strong model (gpt-5-mini) handles scripts, drawings at specific points and analysis, and a light one handles plain questions. Both start at t=0, and Jev's answer to "is this a simple task?" decides which one keeps the turn. If the light one fails, the strong one takes over.

I measured the light candidates instead of trusting the names. gpt-5-nano was actually slower to first output than gpt-5-mini (3.0 s against 2.3 s). gpt-5.4-nano with reasoning turned off was faster than gpt-5-mini in the same run (2.61 s to first output and 2.74 s done, against 2.82 s and 3.11 s) and answered simple questions correctly. Trimming the prompt made no difference at all - the planner's time is the deployment's time to first token, so the only real lever is a faster deployment.

When a plan is broken - a bad Pine line, a wrong input label, the wrong number of anchors for a tool - the chart reports the error, and the failed op goes back to the model for one repair round (3.5-4.5 s). A plan that is cut off mid-stream keeps its complete, valid ops instead of being thrown away.

Pine on my own interpreter, in a Web Worker

The most fun problem was custom indicators. "Color the candles green when RSI is above 60" or "mark where the 9 EMA crosses the 21" are not built-in TradingView studies, and the library only lets you register custom indicators when the widget is created. Rebuilding the chart for every request was not an option.

The trick: register two generic "slot" indicators once at startup - AI Overlay on the price pane and AI Pane below it - with a few plot lines, 12 shape kinds, background and bar colors, bands and a fill. They do not compute anything. Each bar, they look up their values in a table keyed by an input. A new indicator is just a new table, and switching the input points a slot at it.

The tables are filled by a Pine Script v5 subset interpreter I wrote for this, with a lexer, a Pratt parser and a bar-by-bar evaluator. The model writes ordinary Pine:

● ● ●TEXT
//@version=5
indicator("RSI reclaims 30")
r = ta.rsi(close, 14)
plot(r)
hline(30)
plotshape(ta.crossover(r, 30), style=shape.triangleup, location=location.bottom, color=color.green)

It supports variables, var, :=, if/else blocks, ternaries, history (x[n]), tuple destructuring, over 30 ta.* functions (from sma and rsi to supertrend, pivothigh and valuewhen), math.*, and the usual outputs: plot, plotshape, plotchar, hline, fill, bgcolor and barcolor. Paste your own Pine and it is loaded as it is.

Running model-written code in someone's chart has to be boring and safe:

  • There is no eval. Every identifier and function must be in the interpreter's own tables, and lookups go through own-property checks, so a name like constructor can never reach a JavaScript prototype. A script can compute series and nothing else.
  • Unsupported features fail loudly. Loops, custom functions, request.* and strategy.* are rejected with a clear error, and drawing objects like label.new are skipped with a warning.
  • Every run has a budget. About 100 ms of work, strings up to 1,000 characters and nesting up to 200 deep. A runaway script stops with a line-numbered error instead of freezing the app.
  • It runs off the main thread. Scripts compile once and run in a Web Worker on typed columns of candles, with a 3-second watchdog. A script over 2,000 bars takes about 3 ms in the worker and about 0.3 ms on the main thread. The worst stall near the budget dropped from 82 ms to 29 ms.

When new candles arrive, every script on the chart reruns and its slot is refreshed.

Making it feel instant

The first version worked in under 30 minutes, but it felt slow: a simple command could take 1.4-1.8 s after a pause, and anything the model planned took about 3 s. The whole afternoon came to about five hours and 27 commits - the Pine interpreter, the full TradingView toolset, hardening, and then a run of performance work on the gap between "works" and "feels instant":

  • Keep connections warm. A Jev call after an 8-second pause took 1.47 s instead of 0.40 s, because TLS was being reopened every turn. One pooled HTTP client and a keep-alive ping every 45 s brought a routed command to 0.41-0.85 s, no matter how long you paused.
  • Cache Jev's decision, not the result. The same words get the same routing decision, so it is cached on the server for every user. The ops are still rebuilt from the live chart on every turn, so a cache hit is exactly as dynamic as a miss - just 0.7 s sooner. "Mark support and resistance" again costs 20-30 ms end to end.
  • Preload the popular commands. Chart-independent commands that people use often are shipped to the page in advance, and run in about 1 ms.
  • Start before you press Enter. A 600 ms pause in typing starts the whole turn in the background and records its stream. If you send the same text, the recording replays. A question sent after a typing pause was answered 0.33 s after Enter, and a command 11 ms after Enter.
  • Analyze once per chart state. The candle analysis and digest are reused until the chart actually changes.
  • Race a light model for plain questions, as above.

Measured on my MacBook against Azure and TypeSafe:

WhatTime
Candle analysis and digest, 2,000 bars0.13 ms, once per chart state
One engine op (levels, swings, patterns)0.1-2 ms, then TradingView draws in 10-25 ms
Pine script over 2,000 bars, in the workerabout 3 ms (main thread about 0.3 ms)
Jev-routed command, after any pause0.41-0.85 s (was 1.4-1.8 s)
The same message again, any user20-30 ms end to end
Popular command, preloadedabout 1 ms
Question to first evidence on the chartabout 0.45 s
GPT-5 plan, first op / doneabout 2.4 s / about 3 s
Question sent after a typing pauseabout 0.33 s from Enter
Command sent after a typing pauseabout 11 ms from Enter
Repair round, at most once per turn+3.5-4.5 s
Screenshot sent with the messageabout 35 ms, about 90 KB JPEG

The small things that made it feel real

  • The model sees what you see. Your own drawings and indicators are in the digest - their points, their text, and whether price is above your trend line - so "is price above my trend line?" has a real answer.
  • Tap a candle, and it becomes anchor F for the next message: "anchored VWAP from here".
  • Undo takes back the whole last answer, including edits to your own indicators, newest first.
  • Indian formats. Prices as 1,23,456.50 and volume in lakh and crore, in the chart and in the replies.
  • A screenshot can go with a message when the model needs to look rather than read.
  • Honest failures. If a pasted script still fails after its one repair, the reply says so instead of pretending.
  • Not investment advice. When Jev flags a message as asking for advice - buy or sell, a target, an entry or a stop - the reply carries a disclaimer.

Under all of it are around 120 tests: the op contract, the routing and the race, the repair round and the guards on the server, plus the engine, the digest, the Pine interpreter and the worker protocol. Azure and Jev are mocked, so no test spends a real key.

Sahi is building the real thing

While I was hacking on this, Sahi - one of the new generation of Indian brokers - was building the same idea properly. Their AI trading assistant is called Kyro, and on their charts the pitch is exactly this: "Describe the indicator you want in plain words. Kyro writes the script and plots it on your chart directly." You can also paste an indicator script you already use, or write your own in SAHI Script. Beyond charts, Kyro reviews your past trades, compares companies, explains concepts like the Greeks, and coaches you on discipline - and it does not give buy or sell recommendations or place orders for you.

Theirs is much better, for one reason above all: Sahi's charts are, in their own words, "an in-house charting engine not a wrapped third-party library, so the chart and the trade are one system". Mine is a wrapper around someone else's chart, built in an afternoon. Owning the engine means owning the indicator runtime, the data, the drawing layer and the latency budget, instead of finding clever ways around them. Kyro has a waitlist open, and it is free during the beta.

What I learned

  • TradingView is the wrong base for this. It is a superb chart, but a wrapper fights it at every step: custom indicators need the slot trick, another timeframe cannot be fetched beside the current one (so "the 1-hour RSI on this 15-minute chart" has to switch the chart or draw a labelled approximation), and you can never change the bundle itself. The better path is an open-source engine like KLineChart with a WebAssembly runtime for indicator scripts - which is exactly the direction I took in nalgo.
  • A small typed model in front of a big one is a great pattern. Most chat traffic is simple commands. A model that answers typed questions with probabilities makes the routing honest: code can see how sure it is and decide what to do.
  • Time to first token is the floor. No amount of prompt trimming moved the planner. The levers were a faster deployment, starting earlier and not calling the model at all.
  • Make the model's output data, not actions. A plan of typed ops can be validated, streamed, salvaged, repaired, undone and tested. A tool-calling loop is much harder to make fast or safe.

Conclusion

Talk to Chart was a fun afternoon that turned into a small lesson in latency. A chart that understands "pitchfork on the last three swings" is not hard to build once the work is split properly: the engine computes, a typed model routes, a large model plans only when it has to, and the chart draws. Making it feel instant is where the afternoon actually went. It was only an experiment, and Sahi's Kyro shows what the real product looks like. The idea - a conversation sitting on top of the chart every trader already uses - is clearly where charting is heading.

◆ KEY TAKEAWAY

Keep the numbers in code and the decisions in the model. Give the model a small digest with named anchors instead of raw candles, let it answer with typed ops instead of tool calls, and put a fast typed router in front of the large model so most messages never reach it. Then go after latency in order: warm connections, cache decisions rather than results, start before Enter, and measure every model instead of trusting its name.


References

© 2026 Dhruv VakharwalaNext.js · TypeScript · Tailwind · PartyKit