Kronos is an open-source foundation model for financial candlesticks, trained on K-line data from 45+ exchanges. It is the LLM recipe pointed at price bars: a tokenizer turns OHLCV candles into discrete tokens, and a decoder-only transformer predicts the next ones.
Fair warning before you get excited. It forecasts candles, not profit, and the repo publishes no accuracy numbers at all. I got it running anyway, because the interesting part turned out not to be the forecast.
I'll cover three things: getting it onto a Blackwell GPU, feeding it Indian market data, and the conditioning bug that silently poisons your predictions while the output still looks perfectly reasonable. That last one is the actual point of this post.
Install PyTorch for the Right Architecture
Clone it and build an isolated venv. Don't install into your system Python, this pulls about 4.5 GB of CUDA libraries.
git clone https://github.com/shiyu-coder/Kronos
cd Kronos
python -m venv .venv
Now the part that bites Blackwell owners. My RTX 5060 Ti is compute capability sm_120, and a torch wheel built without sm_120 kernels will import fine, report cuda.is_available() == True, and then fall over the moment you run a real kernel. Install from the CUDA 12.8 index explicitly:
./.venv/Scripts/python.exe -m pip install torch==2.11.0 --index-url https://download.pytorch.org/whl/cu128
Then verify, because this is the check that saves you an hour of confusing errors later:
./.venv/Scripts/python.exe -c "import torch; print(torch.cuda.get_arch_list())"
You want sm_120 in that list:
['sm_75', 'sm_80', 'sm_86', 'sm_90', 'sm_100', 'sm_120']
If it's missing, you got a wheel built for older cards. Uninstall torch and reinstall from the cu128 index or newer. Re-run this check after any torch upgrade, it's the thing that regresses.
Then the rest of the dependencies:
./.venv/Scripts/python.exe -m pip install numpy pandas einops huggingface_hub matplotlib tqdm safetensors
Confirm It Actually Runs Locally
"Local" gets used loosely, so here is the proof. The weights download once from Hugging Face, then never again. Kronos-small is 24.7M parameters, 94 MB on disk, plus a 15 MB tokenizer. That is tiny. It ran at roughly 47 bars/sec on my card and would run on a CPU without complaint.
Load it once to populate the cache, then force offline mode and load again:
HF_HUB_OFFLINE=1 ./.venv/Scripts/python.exe -c "
from model import Kronos, KronosTokenizer
t = KronosTokenizer.from_pretrained('NeoQuasar/Kronos-Tokenizer-base')
m = Kronos.from_pretrained('NeoQuasar/Kronos-small')
print('params: %.1fM' % (sum(p.numel() for p in m.parameters())/1e6))
"
If that prints params: 24.7M with no network, you're genuinely offline. No API key, no per-call cost, and your candles never leave the machine.
One thing the README doesn't mention: examples/prediction_example.py reads ./data/XSHG_5min_600977.csv, and that file is not in the repo. Don't waste time looking for it, bring your own data.
Find Data That Isn't Blocked
I went for Binance first, since the repo's own demo is BTC/USDT. From an Indian connection:
curl -s --max-time 20 "https://api.binance.com/api/v3/ping"
# exit code 35
Kraken and Coinbase gave me the same thing. Exit 35 is an SSL connect failure, which here means blocked rather than broken.
Yahoo Finance works, and it's the better source anyway, because it covers NSE stocks, Indian indices and crypto through one unauthenticated endpoint:
curl -s -H "User-Agent: Mozilla/5.0"
"https://query1.finance.yahoo.com/v8/finance/chart/RELIANCE.NS?interval=1h&range=730d"
Tickers go RELIANCE.NS, TCS.NS, ^NSEI for Nifty 50, ^BSESN for Sensex, BTC-USD for crypto. The User-Agent header is not optional, Yahoo rejects the default curl agent.
Yahoo caps intraday history by interval, which matters because Kronos wants a 512-bar context window and degrades badly on short input. 1m gives you 7 days, 5m/15m/30m give 60 days, 1h gives 730 days. For 400 bars of hourly context, request the full 730d range and take the tail.
The Timestamp Trap
Here's the one that cost me real time, and the reason I bothered writing this up.
To forecast forward you have to hand the model timestamps for the bars you want predicted. The obvious approach is to take the median spacing of your history and extrapolate:
step = hist["timestamps"].diff().median()
last = hist["timestamps"].iloc[-1]
future = [last + step * (i + 1) for i in range(pred_len)]
That is wrong, and nothing tells you it's wrong. Here's what it produced for Reliance, an NSE stock:
2026-08-13 17:49:19+05:30 1304.30
2026-08-13 23:49:19+05:30 1326.57
2026-08-14 03:49:19+05:30 1336.35
NSE closes at 15:30. I was asking for a forecast at 3:49 in the morning.
Plenty of models would just ignore a timestamp column. Kronos doesn't. Look at the predictor:
self.time_cols = ['minute', 'hour', 'weekday', 'day', 'month']
Those are conditioning inputs. The model was trained on real session data, so it learned that 09:15 behaves differently from 14:30, and that Saturday doesn't exist for equities. Feed it 03:49 on a Sunday and you've handed it a combination it never saw in training. It doesn't error. It doesn't warn. It returns confident, plausible-looking numbers built on nonsense conditioning, and you would never catch it by eyeballing the output.
The fix is to generate future timestamps that match the session pattern. I deliberately didn't hardcode NSE hours, because then the script breaks on crypto and US markets. Instead, learn the grid from the history you already have:
recent = ts.tail(800)
tod_counts = recent.dt.time.value_counts()
# Yahoo stamps the in-progress bar at "now", which is not on the session grid.
# Requiring a time-of-day to recur filters that single stray out.
keep = tod_counts[tod_counts >= max(2, tod_counts.max() * 0.2)]
tods = sorted(keep.index)
wdays = sorted(set(recent.dt.weekday))
Then walk forward through those times of day, rolling to the next valid weekday when you run off the end of a session. If the data has 20+ distinct times of day, or all seven weekdays, it's a 24/7 series and plain median spacing is correct, so fall back to it.
That stray-bar filter matters more than it looks. Yahoo stamps the currently-forming candle with the actual wall-clock time, so my last bar came in at 14:51:06 instead of sitting on the 09:15, 10:15, 11:15... grid. Without the frequency filter, that one bar pollutes the whole schedule.
After the fix, same request:
2026-08-13 15:15:00+05:30 1316.79
2026-08-14 09:15:00+05:30 1316.86
2026-08-14 15:15:00+05:30 1313.25
2026-08-17 09:15:00+05:30 1318.72
Full sessions, and it jumps from Friday the 14th straight to Monday the 17th. August 15 and 16 were a weekend.
This is the kind of logic that deserves one runnable check, because it fails silently by definition. Mine asserts three cases: NSE bars land only on weekdays inside session hours, crypto stays contiguous across midnight, and daily bars fall back to median spacing.
Want proof the conditioning is doing real work? Look at forecast volume across a session:
09:15 2.4e5
12:15 1.7e6
15:15 1.2e6
That is the intraday volume U-shape, light at the open and heavy midday. The model only knows to do that because the hour feature is meaningful. Which is precisely why feeding it 3 AM was destroying the forecast.
Watch the Volume Channel
Two smaller traps, both worth a guard.
Indices report zero volume. ^NSEI returns volume: 0 on every single bar, because an index has no volume to report. Handing the model a constant-zero channel is worse than handing it nothing, since the predictor fills a missing column itself. Check and drop:
if (hist["volume"] == 0).all():
cols.remove("volume")
Individual stocks like RELIANCE.NS carry real volume, so this only fires on indices.
Forecast volume can come out negative. The model predicts in normalized space and denormalization is unconstrained, so I got bars like -1.47e5. Clamp it:
pred_df[c] = pred_df[c].clip(lower=0)
What It Actually Predicts
Now the honest part, and the reason I'd push back on anyone selling this as a trading edge.
With the timestamps fixed and five sampled paths averaged, here's what I got:
- Reliance, 18 hourly bars ahead: -0.08%
- Nifty 50, 12 hourly bars ahead: -0.11%
Roughly nothing, in both cases. And that is the correct answer. A 100M-parameter model trained on public OHLCV has no informational edge, because everyone can see the same candles. If it had confidently predicted +3% I would trust it less, not more.
Update: I Scored One Against the Close
I published this at 15:36 IST, then realised I could check the model's homework. The Nifty forecast above was generated at 14:51, before the 15:15 bar existed, so the first predicted bar is a genuine out-of-sample call. Nifty closed at 24,395.85.
Predicted Actual Error
Open 24,353.67 24,353.35 +0.32
High 24,373.46 24,395.85 -22.39
Low 24,326.24 24,353.35 -27.11
Close 24,345.75 24,395.85 -50.10
It got the direction wrong. From the 24,350.80 it last saw, it predicted -0.02%, essentially flat. The actual move was +0.18%. The index closed up into the bell and the model had it drifting down.
Two details worth more than the miss itself. The predicted open was 0.32 points off, which looks impressive and is not: the open was already pinned by the last price the model saw, so ignore it. The predicted range was 47 points against an actual 42, a reasonable read on volatility. It has some feel for how much the index would move and none for which way. That is the usual shape of these results.
Price also closed above the entire predicted range, so it never saw the move coming at all.
One bar is one sample, and this proves no more than a hit would have. But it is a clean picture of what the section above describes: a table of confident numbers, wrong on the only question that pays. The real test is a few hundred of these scored automatically for directional hit rate.
One flag on methodology: --samples 1 is a single stochastic draw and jumps around between runs. Use 5 or more. Averaging cuts variance, it does not add skill, so don't mistake a smoother line for a better forecast.
Where I think it's genuinely useful is as a learned feature generator. Its internal representation of a price series is a more interesting input to your own model than hand-rolled indicators. As a signal you trade directly, no.
Done
If you want to reproduce this: install from the cu128 index, verify sm_120, pull data from Yahoo, and generate your future timestamps from the observed session grid instead of a median timedelta.
That middle bug is the transferable lesson. Any time-series model that takes calendar features as conditioning inputs has this failure mode, and none of them will tell you when you trip it. If your forecasts feel vaguely off and you can't say why, print the timestamps you're actually asking for.
Repo is at github.com/shiyu-coder/Kronos, MIT licensed. It's had a lot of recent activity, so expect the API to drift.