Ask a vision model to read a chart and it will tell you "RSI is at 71.4". Confident, specific, and - most of
the time - invented. Multimodal models are very good at the shape of a technical narrative and terrible at
the arithmetic behind it. They will also happily agree with whatever you pasted, because agreeing is the
statistically safe answer.
I wanted the read anyway - screenshots are the only format a chart exists in when you are mid-session - so I
built ChartSense around one rule: the model never gets to
be the source of a number it can be wrong about silently.
Local-first Next.js, MIT, your API key only ever goes to the provider you choose. Here are the three mechanics
that enforce that rule.
1. Every value carries its provenance
Each reading in the output is tagged:
-
calculated- computed by the app from candles you pasted -
read- actually legible off the screenshot (the axis labels, the legend, the LTP) -
estimated- interpolated by the model, and always printed with a~so you cannot mistake it for a reading -
no basis- the model wanted to say something and had nothing to say it from
That last category is the uncomfortable one, and it is the reason this is not just a prompt wrapper. A model
that is allowed to output no basis produces noticeably fewer confident fabrications, because the schema gives
it an honest exit. Here is a trimmed real output from a Gemini read of a NIFTY 50 5-minute chart:
NIFTY 50 · 5m · LTP 22395.0
Verdict: Strong Sell (TradingView gauge 15/100) · Signal clarity 78/100 — how clean the read is, not the odds of profit
Nifty 50 is in a firm intraday downtrend after breaking below the 22474.7 breakdown level. Price continues to
print lower highs and lower lows with all key moving averages sloping overhead.
• Support: 22355.5 and 22305.6
• Resistance: 22474.7, 22576.7 and 22719.9
+15 Moving average alignment: Price is trading well below all estimated short and intermediate moving averages.
+15 Trend strength (ADX): ADX is at 40.28 with DI- dominant at 27.4, indicating a strong downtrend.
-5 Oscillator agreement: RSI has dipped into oversold territory (28.25), warning of potential short-term pullbacks.
-10 Signal freshness: The sell signal originated at 22583.85 and the move is already quite extended.
• Trigger: Rejection on a retest of 22440.0 - 22474.7 • Invalidation: 22500.0 • Target: 22355.5 • RR: 2.3:1
Note the second clause on the clarity line: how clean the read is, not the odds of profit. A score that
looks like a probability will get treated like one.
2. Paste candles, and the maths stops being AI
lib/indicators.ts is plain TypeScript: SMA, EMA, RSI, MACD, Stochastic, ADX and ATR. When you paste OHLCV
data alongside the screenshot, those get computed from the data and overwrite whatever the model guessed.
So the same chart produces two different quality of answer:
- screenshot only → levels and readings are
readorestimated, and the output says so - screenshot + candles → the moving averages and oscillators become
calculated, and the narrative is pinned to numbers you can re-derive yourself
This is the cheapest reliability trick I know for anything vision-model-shaped: let the model do what it is
good at (describe structure, name the trend, assemble a plan) and take away the keyboard for anything with an
equals sign.
3. The confidence score is code, not vibes
If the model is allowed to return its own confidence, it returns what you would like to hear. So the field it
returns is only a suggestion - the server recomputes it from the factor list:
if (analysis.confidenceFactors.length > 0) {
const summed = 50 + analysis.confidenceFactors.reduce((total, factor) => total + factor.points, 0);
analysis.confidence = clampNumber(summed, 5, 85, analysis.confidence);
}
Base 50. Capped contributions per factor. De-duplicated by label, so a model that lists "strong trend" three
times in different words does not get paid three times:
const confidence = factors.length
? clampNumber(50 + factors.reduce((total, factor) => total + factor.points, 0), 5, 85, analysis.confidence)
: analysis.confidence;
And the clamp is the part I would defend hardest: 5 to 85, never higher. One screenshot can never be
certain, and a score that can reach 100 trains people to stop reading the reasons. Every individual factor is
bounded too - e.g. oscillator agreement:
points: clampNumber(((agree - against) / (agree + against)) * 20, -10, 20, 0),
The same idea runs the gauge: clampNumber(50 + lean * 50, 0, 100, 50), and if nothing scored, it returns
{ index: 50, verdict: "neutral" } instead of guessing a direction. Neutrality is a real answer, and code that
has no path to it will lie to get out of it.
There is also an audit trail by construction: confidenceReason prints the arithmetic - how many factors went
into the base of 50, how many of them the app scored from your candles, and which factor was strongest and
weakest. If you disagree with a number, you can find the line that produced it.
No news. No fundamentals. No predictions. Levels come from the chart or your data, and every plan ends with
wait for clear price confirmation.
Run it locally (the security shape)
npm install
npm run dev # http://localhost:3000
Click Connect AI keys, paste one key - Grok, OpenAI, Gemini or OpenRouter - hit Test key, then Save
keys. No key yet? Demo mode still runs the real indicator maths on candles you paste.
Three deliberate decisions, since this is a tool that holds an API key:
- keys are stored in
~/.chartsense, outside the project folder - copying or publishing the source can never carry one - localhost is open by design; any other host is refused until you set
CHARTSENSE_ACCESS_TOKEN - every route has per-IP rate limits plus body, image and input validation (details in
SECURITY.md)
Local-first also means the privacy claim is structural rather than promotional: there is no account, no
telemetry, and no server of mine. Your screenshot goes to the provider you keyed in, and nowhere else.
What it still gets wrong
It is a single-screenshot read: no multi-timeframe confluence beyond what is visible, and no tick data, so a
level that would need the order book to validate is not something this can give you. The estimated tag is
honest, but honest wrong numbers are still wrong numbers - which is exactly why pasting candles changes the
quality of the answer so much.
Where do you draw the line between letting a model interpret and letting it compute - and has anyone actually
gotten a vision model to read a chart's axis scale reliably without feeding it the underlying data?
Repo: 85599/tradingview-chartsense, MIT.
Top comments (0)