Kairos is an AI-assisted crypto trading system: a CrewAI pipeline that researches coins, generates a rule-based strategy, and backtests it honestly, paired with a headless, restart-safe trading engine that runs that strategy live — one shared decision function driving both the backtest and the live loop, so they can never quietly disagree with each other.
Kairos = the opportune moment, as against chronos, sequential time. In its original archery and weaving senses it meant the aperture — the opening an arrow must pass through, the gap in the warp a shuttle must cross — which exists for an instant and then closes. That's a trade entry, described literally: act on the bar that just closed, not a wall-clock poll.
This replaced an earlier bot that was left running unattended and audited on return. The audit found roughly thirty defects in the live path, several of them compounding: mainnet market data feeding a strategy that executed on testnet; a quantity-rounding bug that could permanently deadlock closing a position; no live stop-loss at all — risk parameters were computed and displayed, never enforced; backtest metrics that were retyped by the LLM rather than written by the tool that computed them. None of it was patched. The engine, the backtester, and the strategy-eval path were all rebuilt around a small number of fixes that structurally can't regress — a shared decide() function, a Decimal-exact ledger, an AST-sandboxed rule evaluator instead of eval(), real reconciliation on startup.
-
AI-Assisted Strategy Generation
- Three sequential CrewAI agents —
trending_coin_finder→coin_picker→backtester— research coins, pick one, and iterate a rule-based strategy until it clears a bar. - Runs on Vertex AI (Gemini) by default, with Google AI Studio as a fallback via one env var — model names live unprefixed in
agents.yamland resolve to whichever provider is active.
- Three sequential CrewAI agents —
-
Backtesting That Doesn't Lie
- Intrabar stop-loss/take-profit fills at the trigger price using the bar's real high/low, not the close — a take-profit that gaps past its target books the target, not the gap.
- Real round-trip fees and slippage, charged in currency terms, not tacked on as an afterthought.
- Sharpe ratio annualizes off the strategy's actual timeframe, not a fixed trading-days constant.
- The tool that runs the backtest writes its own result file, atomically, only on a
Keeprecommendation — the LLM reports a summary string, it never gets a chance to retype a number.
-
A Rule Sandbox, Not
eval()- Entry/exit rules are LLM-authored pandas boolean expressions, validated by an AST whitelist before they ever touch a DataFrame — no
__import__, no attribute-chain sandbox escapes, no silentand/orscalar bugs.
- Entry/exit rules are LLM-authored pandas boolean expressions, validated by an AST whitelist before they ever touch a DataFrame — no
-
A Headless Engine Built on One Shared Decision
signals.decide()is the single function that decides enter/exit/hold — driven bar-by-bar in the backtester, and driven live off every closed candle. Backtest and live can't diverge on what counts as a signal, because they're calling the same code.- Two independent cadences: a bar tick evaluates the compiled strategy on every closed candle; a risk tick polls price every few seconds so a stop-loss fires within seconds of being breached, not at the next bar close.
-
Real Persistence, Real Recovery
- Every fill, position, and log line lives in SQLite with money stored as exact Decimal text — never a float.
- On startup, the ledger is reconciled against the exchange's real balances and trade history before anything trades; it never invents an entry price it can't account for.
- A heartbeat-based single-instance guard refuses to start a second copy against the same account — the old dashboard's biggest risk was two browser tabs quietly placing duplicate orders; this engine has no client-side ticking at all.
-
Pluggable Venues
paper(simulated fills over real market data),testnet, andmainnet— switchable by one config value, default testnet. Mainnet requires an explicitKAIROS_I_UNDERSTAND_REAL_MONEY=1on top of selecting it.
-
A Live Dashboard, No Build Step
- FastAPI + Server-Sent Events, a single static page (Tailwind via CDN, vanilla JS), hand-rolled inline-SVG candlestick chart with entry/exit markers and stop/take lines — no charting library, no bundler.
- Optional bearer-token auth (
KAIROS_AUTH_TOKEN) gates the API for a public deployment; unset, everything stays frictionless for local use.
-
Ships as a Container
- A multi-stage Dockerfile + Compose file (Kairos behind Caddy for TLS) ready for a droplet — built for an unattended, multi-week run, not a demo.
sequenceDiagram
participant User
participant Crew as CrewAI crew
participant Finder as trending_coin_finder
participant Picker as coin_picker
participant Backtester as backtester
participant Tool as BacktestTool
participant File as backtest_results.json
User->>Crew: uv run run_crew
Crew->>Finder: research trending coins
Finder-->>Crew: candidate list
Crew->>Picker: pick the best candidate
Picker-->>Crew: chosen coin
Crew->>Backtester: generate and validate a strategy
loop until recommendation is Keep
Backtester->>Tool: entry/exit rules, risk params
Tool->>Tool: compile_rule (AST sandbox), then decide() bar by bar
Tool-->>Backtester: win rate, Sharpe, drawdown, trade count, recommendation
end
Tool->>File: atomic write, Keep only, generated_by stamped
Tool-->>Backtester: summary string only, never the raw numbers
graph TD
Feed["feed.py<br/>CandleFeed - closed candles only"]
BarTick["bar tick<br/>every closed candle"]
RiskTick["risk tick<br/>every KAIROS_RISK_POLL_SECONDS"]
Decide["signals.decide()"]
Broker["broker.py<br/>Broker Protocol"]
PaperB["paper_broker.py"]
BinanceB["binance_broker.py<br/>testnet / mainnet"]
Store[("store.py<br/>SQLite ledger")]
API["api/server.py<br/>FastAPI + SSE"]
UI["api/static<br/>Dashboard"]
Feed --> BarTick
BarTick --> Decide
Decide -->|enter / exit| Broker
RiskTick -->|ticker price vs stop/take| Broker
Broker --> PaperB
Broker --> BinanceB
BinanceB -->|real market data + fills| Feed
BarTick <--> Store
RiskTick <--> Store
BarTick -->|state / bar / fill / log events| API
RiskTick -->|state / bar / fill / log events| API
API -->|SSE + REST| UI
kairos/
├── src/kairos/
│ ├── main.py # crew entry point
│ ├── crew.py # KairosCrew - agent/task wiring
│ ├── llm.py # Vertex AI / AI Studio provider resolver
│ ├── indicators.py # one shared indicator definition (backtest + live)
│ ├── rules.py # AST-whitelist rule compiler/evaluator
│ ├── signals.py # decide() - the shared bar-decision function
│ ├── timeframes.py # timeframe <-> minutes
│ ├── config/ # agents.yaml, tasks.yaml
│ ├── tools/ # fetch_tool.py, backtest_tool.py
│ ├── engine/ # the headless trading engine
│ │ ├── config.py # Settings from KAIROS_* env vars
│ │ ├── broker.py # Broker Protocol + make_broker() factory
│ │ ├── binance_broker.py # testnet/mainnet REST wiring
│ │ ├── paper_broker.py # simulated fills over real market data
│ │ ├── feed.py # closed-candle-only pipeline
│ │ ├── store.py # SQLite ledger, Decimal-exact
│ │ └── service.py # the asyncio loop + reconciliation
│ └── api/
│ ├── server.py # FastAPI + SSE, KAIROS_AUTH_TOKEN gate
│ └── static/ # index.html, app.js, theme.css - no build step
├── tests/ # ~175 tests, all offline (FakeBroker, no network)
├── Dockerfile
├── docker-compose.yml # kairos + caddy
├── Caddyfile
├── output/ # generated strategies (gitignored)
└── data/ # SQLite ledger (gitignored)
| Dependency | Purpose |
|---|---|
| Python 3.10–3.13 | Core runtime |
| uv | Package management |
| A Binance account | Mainnet API key (market data, and paper venue) + Testnet API key (default trading venue) |
| A GCP project with Vertex AI enabled, or a Google AI Studio key | Powers the CrewAI agents |
| ntfy (optional) | Push notifications on fills |
git clone https://github.com/pushkqr/kairos
cd kairos
uv syncCopy .env.example to .env and fill in your values — every variable the app reads is documented there. A few worth calling out:
KAIROS_LLM_PROVIDER=vertex # vertex (default) | aistudio
VERTEX_PROJECT=your-gcp-project-id
VERTEX_LOCATION=us-central1
KAIROS_VENUE=testnet # paper | testnet (default) | mainnet
KAIROS_I_UNDERSTAND_REAL_MONEY= # must be exactly "1" to allow mainnetVertex AI auth: locally, run gcloud auth application-default login once. On a headless deployment (Docker/droplet), that interactive flow isn't available — use a service-account key instead (GOOGLE_APPLICATION_CREDENTIALS pointed at the JSON; docker-compose.yml already wires this up, you just supply gcp-key.json).
Some AI Studio API keys 404 on older model names (e.g.
gemini-2.5-flash→ "no longer available to new users") even though they're still listed by the models endpoint.gemini-3.5-flashis confirmed working on both providers — swap to it inagents.yamlif you hit this on theaistudiofallback.
uv run run_crewThe crew researches, picks a coin, and iterates a strategy against real historical data until the backtester recommends Keep — at which point output/backtest_results.json is written atomically. A Modify/Discard recommendation writes nothing there; every attempt is still logged under output/backtest_runs/ for reference.
uv run serveOpen http://localhost:8000. This starts the trading loop (against whatever KAIROS_VENUE and KAIROS_STRATEGY_FILE point at) and the live dashboard in one process — Ctrl+C stops both.
docker compose up -dBuilds the image, starts Kairos bound to 127.0.0.1:8000 (debugging access only), and starts Caddy in front of it on 80/443. Before this is actually reachable publicly:
- Point a real domain's DNS A record at the host, and replace
your-domain.cominCaddyfilewith it. - Set
KAIROS_AUTH_TOKENin.env— a random string (e.g.python -c "import secrets; print(secrets.token_urlsafe(16))") — before anything is public. - Make sure
gcp-key.jsonis present next todocker-compose.ymlif you're on Vertex AI.
./data and ./output are bind-mounted, so the ledger and any generated strategy survive a container restart or redeploy.
uv run pytest # ~175 tests, fully offline - no network calls anywhere
uv run ruff check src/ tests/The keystone fixture is FakeBroker (tests/conftest.py), a full in-memory implementation of the Broker Protocol — it's what makes the feed, the ledger, reconciliation, and the entire trading loop testable without ever touching a live exchange.
- Default venue is
testnet. Promotion tomainnetrequires changingKAIROS_VENUEand settingKAIROS_I_UNDERSTAND_REAL_MONEY=1— a config change, deliberately not something a strategy or a bug can trigger on its own. papertrades simulated fills over real mainnet market data. It validates the engine's plumbing (orders round-trip, stop-loss fires, restart reconciles) — it is not evidence about real fill quality, since there's no real order book behind it.- Kairos will place real orders on whatever venue is configured, including testnet. Nothing here is financial advice, and a strategy's backtest performance is not a promise about how it trades live.
Kairos executes trades autonomously once running. Review any strategy before letting it trade, understand the venue you've pointed it at, and never enable mainnet without being genuinely comfortable with what that means for real funds.