Finding high-probability directional structures in price, backed by fundamentals.
Live alerts: t.me/till_infinity_signals
A directional call is only worth making when the price structure and the fundamentals point the same way. Most setups see one or the other. This one is built to see both at once, and to write down why it thought so at the time.
Five parts, in dependency order:
prices |
one instrument from many venues, because the disagreement between feeds carries information no single feed does |
structures |
arithmetic, not judgement — online models over the venues, and the key levels price keeps turning at, answered per approach side |
news |
the calendar and the headlines on the same clock, because a move with a release behind it is a different animal |
agents |
a model over the stored data, told plainly that "nothing is happening" is a correct answer. The only part needing a credential, and the only optional one |
journal |
what was decided, why at that moment, and what followed. Prices can be recomputed forever; the reasoning cannot be reconstructed once lost |
The load-bearing idea is that a level is where volatility turns, not where price poked: the leg in and the leg out meet at an origin, and the wick beyond it is the zone's width rather than its position. Everything is measured in volatility units, so gold and EURUSD, 3m and 1w, are comparable without per-instrument tuning.
The full version, with the reasoning behind each choice →
docker compose up -d # one container per service, over RedisOr push to main and let CI build, publish and deploy it. Sizing matters more
than preference — six services need about 861 MB, so a small box wants the
single process instead. Full guide: docs/deployment.md.
Requires uv. Python 3.11 is pinned in .python-version.
uv sync # runtime + dev deps
cp .env.example .env # optional: every setting, documented
uv run till-infinity run # collectors, levels, journal — all of itThat is the whole thing running: prices and news collecting, structures
finding levels and anomalies, the journal recording, notifications delivering.
One bus, one process.
till infinity · in-process bus · journal, structures, prices, news
00:33:18 structures dislocation gold/PEPPERSTONE +1.06bps off where 5 other venues agree
00:33:22 structures spread gold/SAXO 2.2x the group at 0.88bps
Agents are off by default — they are the only part needing a paid credential, and the rest should not be hostage to it:
AGENTS_ENABLED=1 uv run till-infinity run # or --agents
uv run till-infinity run --for 300 -s gold # one instrument, five minutes
uv run till-infinity run --once # one collection pass, then stopAnything that cannot start says why at second zero and the others carry on.
Settings come from .env — see .env.example — with real
environment variables winning, so a deployment is never overridden by a file.
For a shared bus across machines, set TILL_REDIS_URL and run the services
separately; see how the parts talk.
OHLCV candles and realtime bid/ask for the same instrument across many brokers, from TradingView and Yahoo.
uv run till-infinity prices backfill # deep history
uv run till-infinity prices collect # bars + quotes together, with a ticker
uv run till-infinity prices bars # new bars every 60s, forever
uv run till-infinity prices quotes # stream live bid/ask, forever
uv run till-infinity prices info # what is storedSix instruments by default — EURUSD, GBPUSD, gold, BTC, US100 (Nasdaq 100)
and SPX500 — each from five to seven venues, on intervals from 1m to 1w.
Indices answer to whatever you call them (-s nasdaq, -s sp500, -s ndx),
and -s also takes VENUE:TICKER or a bare Yahoo ticker. Full guide:
docs/prices.md.
Headlines, the economic calendar around them, and central bank reserves — from five RSS feeds, TradingView, ForexFactory and the IMF.
uv run till-infinity news collect # poll headlines + calendars + IMF
uv run till-infinity news upcoming --high # next high-impact releases
uv run till-infinity news latest # recent headlinesTwo calendars are kept side by side on purpose, so a print can be cross-checked between providers. Full guide: docs/news.md.
Online models over the price data: every venue measured against the consensus of the others, and the key levels price keeps turning at.
uv run till-infinity structures levels # what it has found
uv run till-infinity structures levels --at 4405 # what the history says
uv run till-infinity structures watch # run it on its ownA level is tracked as a Kalman state rather than a line — each touch is a noisy observation of where it sits, so the filter's variance is the zone. Statistics are kept per approach side, because the same price met from above and from below are two different objects:
gold arriving at 4405.5
· 4405.5 (from above) ↑ 59% vs 47% base push +0.42v
! 4401.3 (from above) ↑ 80% vs 47% base push +1.78v
Every conditional sits beside its base rate. A level whose P(up) matches the unconditional rate has said nothing, however confident it looks.
Three things happen at a level and the model tells them apart, because one with only "held" and "broke" scores the other two wrong — on the stored history 27 of 70 breakout attempts were false, and every one had counted as a break that worked:
| what it is | |
|---|---|
| break | through, and it stayed through |
| false breakout | through, then given back |
| back check | broke, pulled back, held, carried on — risk defined by the flipped level |
Built on 5m through 1w, each timeframe with its own volatility and its own rate of forgetting — one volatility unit on gold is $0.75 on 5m and $52.23 on 1w, seventy times end to end. Levels at one price across timeframes combine, the higher carrying significance and the lower placement.
Guides: docs/structures.md and docs/levels.md.
Everything published says the same three things: what happened, how unusual that is for this venue, and what it is being compared against. The last one is the part most signals leave out.
stale BINANCE btc has not moved in 67s while 4 other venues have
spread FOREXCOM gold 2.4x the group at 2.11bps, wide even for this venue
dislocation DERIV btc +3.93bps from consensus, outside anything this venue normally does
| what it means | needs a human? | |
|---|---|---|
stale |
this venue stopped updating while the others carried on | yes, now — a dead feed needs no interpretation |
spread |
its spread is wide for the group and for its own history | only with context |
dislocation |
its price is away from where the others agree | only with context |
drift |
the volatility regime itself changed | it invalidates thresholds |
"Wide even for this venue" is doing real work. A venue quoting BTC at 20bps is not wide; EURUSD at 3bps is. Each venue is scored against its own distribution, so one number never has to be right for both.
A level is a price the market has turned at before. What it produces looks like:
us100 1h 29618 tested from above 7.6x +1.87v from below 6.6x -1.52v strength 0.94
us100 1h 29391 tested from above 17.3x +1.57v from below 6.1x -1.71v strength 0.88
Read left to right: the instrument and timeframe, the price, its state, then what it did to price arriving from each side and how much evidence there is.
7.6x— effective touches, decayed by age. Ten touches last quarter count for less than three this week, and the number already accounts for that.+1.87v— the average push in volatility units:1vis one typical move for that instrument on that timeframe. On gold 5m that is about $0.75; on gold weekly, about $52. It is the same number on BTC and EURUSD, which is the point.from above/from below— kept apart because they are different objects. At 29618 price arriving from above gets pushed up and arriving from below gets pushed down: that is a level holding both ways, and an average over the two would show roughly nothing.
gold arriving at 4405.5
· 4405.5 (from above, ~4.2h) ↑ 59% vs 47% base push +0.42v
! 4401.3 (from above, ~3.2d) ↑ 80% vs 47% base push +1.78v
vs 47% base is the whole thing. 59% sounds like an edge until you see the
unconditional rate is 47%; 80% against the same 47% is one. A level whose
probability matches the base rate has told you nothing, and you will see that
rather than a confident-looking number. ! marks the ones clearing all three
bars — enough evidence, enough separation from the base rate, and a move big
enough to be worth the risk.
~4.2h is how long price typically takes to get there, from the distance and
current volatility. Time goes as the square of distance, so a level twice as
far away is four times as long, not twice.
| break | through, and it stayed through — provisional until it survives |
| false breakout | through, then given back. Recorded with the push it ended on |
| back check | broke, pulled back, held, carried on — the stop is the flipped level |
Told apart because a model with only "held" and "broke" scores a trap as a break that worked. On the stored history 27 of 70 breakout attempts were false.
No performance figures, and none until there are enough resolved outcomes to compute them honestly. The system records every call with the state it was made from and attaches what followed, so that question becomes answerable — it is not answerable yet.
A model over the stored data — ask it a question, or leave one watching and alerting when price and fundamentals line up.
uv run till-infinity agents ask "is anyone quoting gold out of line?"
uv run till-infinity agents watch --redis redis://localhost:6379
uv run till-infinity agents roles # market, macro, riskEvery store an analyst reads is opened read-only, so a prompt injection in a headline reaches a model whose only verbs are SELECT. Full guide: docs/agents.md.
What was decided, why at that moment, and what happened next.
uv run till-infinity journal listen --redis redis://localhost:6379 # record
uv run till-infinity journal list
uv run till-infinity journal add "Widened the spread threshold to 12bps" \
--why "8bps fired six times overnight on TVC, none of them real"
uv run till-infinity journal export -o data/journal.jsonlAppend-only and point-in-time: the state behind a decision is copied in, not referenced, so an entry read back a year later still shows the world it was actually made in. Full guide: docs/journal.md.
Alerts to Telegram and Discord, fanned out across as many chats or webhooks as you list, with per-channel level routing.
uv run till-infinity notify chats # discover Telegram chat ids
uv run till-infinity notify listen # deliver what the agents publish
uv run till-infinity notify send "..." -l warningWhat this instance publishes goes to t.me/till_infinity_signals.
Full guide: docs/notifications.md.
Collectors publish what they store, agents consume it and publish alerts, notifications deliver those. The databases stay the source of truth — the bus carries notice that something happened, not the data itself.
prices ──┬─▶ structures ─┬─▶ structures.signals ─┐
│ └───────────────────────┼─▶ alerts ─▶ notifications
news ───┴─────────────────────▶ agents ─────────┘
│
structures, agents ───┴──▶ journal ──▶ journal.db
Every part is a service and every arrow is a bus topic — including the
journal, so one process writes it and a service on another machine can record
a decision at all. structures reaches alerts directly for findings that
interpret themselves: a feed that has stopped needs no model and no calendar.
till-infinity run starts all of these in one process against one bus, which
is what you want for a laptop or an end-to-end check. Run them separately when
they should scale or fail independently:
uv run till-infinity prices collect --publish redis://localhost:6379 &
uv run till-infinity news collect --publish redis://localhost:6379 &
uv run till-infinity structures watch --redis redis://localhost:6379 &
uv run till-infinity agents watch --redis redis://localhost:6379 &
uv run till-infinity journal listen --redis redis://localhost:6379 &
uv run till-infinity notify listen --redis redis://localhost:6379 &Full guide: docs/bus.md.
SQLite by default, under .data/ and gitignored. JSONL alongside it with
--store both.
.data/prices/prices.db bars + quotes
.data/news/news.db articles + events + observations
.data/journal/journal.db decisions + observations + outcomes
.data/structures/ online model state, restored on restart
Nothing is regenerated on restart that does not have to be: the online models are restored, and the level windows warm from the stored bars.
Everything is stored as epoch seconds in UTC — local time never enters the project. Re-running a collector is cheap and safe: bars key on their open time, headlines on their id, calendar events get rewritten in place when the print lands, and journal entries are content-addressed.
| docs/getting-started.md | start here — install to stored data, and how to read it back |
| docs/prices.md | candles, quotes, sources, storage, schema, library use |
| docs/news.md | headlines, economic calendar, event storage |
| docs/structures.md | online models, cross-venue features, avoiding false positives |
| docs/levels.md | key levels — PIP swings, Kalman tracking, per-side directional inference |
| docs/agents.md | analysts, tools, models, read-only access, watching the bus |
| docs/journal.md | decisions, reasoning, outcomes, exporting for training |
| docs/notifications.md | Telegram and Discord alerts, channels, chat discovery |
| docs/bus.md | topics, publishing, fan-out, Redis |
| docs/deployment.md | one process, compose, or CI to a server — and how to size it |
| docs/logging.md | log levels, JSON log files, adding a logger |
uv run pytest # tests
uv run ruff check . # lint
uv run ruff format . # format
uv add <package> # add a dependency
uv add --dev <package> # add a dev dependencyLinting is ruff only. It covers pylint's checks
(the PL rules) alongside pyflakes, isort, bugbear, async correctness and the
rest, so there is one tool and one config in pyproject.toml rather than two
that overlap and disagree.
Run the hooks on every commit:
uv run pre-commit install # one time
uv run pre-commit run --all-filesThe same checks — lint, format, tests — run in CI on every push and pull
request (.github/workflows/ci.yml). No test touches the network.