Every trade inDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/theonetrade/backtest-ollama-crontab/llms.txt
Use this file to discover all available pages before exploring further.
backtest-ollama-crontab is the result of a deterministic, five-stage assembly line that starts the moment a message lands in a public Telegram signals channel and ends only when a local Ollama model has either blessed or vetoed it. Nothing is opened until all five stages have completed. Understanding this pipeline is the fastest way to understand the entire project.
Crawl — Telegram messages into parser-items
CrawlerService.crawlDay(stamp) calls crawlRange(stamp, stamp) which drives CryptoYodaScreenService.screenDay(date). That method first calls ScraperService.scrapeDay(channelName, date) to pull raw ScraperMessage objects from the Telegram MTProto session, then forwards them through CryptoYodaScreenService.parseDay() to apply the SIGNAL_FORMAT regex map (see Stage 2).For each message that yields a valid parsed result with type === "crypto_yoda_channel", CrawlerService calls ParserDbService.create() to upsert the record into the parser-items MongoDB collection. Duplicate messages on the same (channel, messageId) pair are silently ignored via the unique compound index.Parse — regex extraction of structured signal fields
CryptoYodaScreenService delegates to ParserService.parseDay() which applies the SIGNAL_FORMAT regex map against each raw message string. The map extracts five structured fields from the Telegram message text:| Field | Regex pattern | Notes |
|---|---|---|
symbol | /#([A-Z0-9]+)\/USDT/ | Captures ticker before /USDT |
direction | /(ШОРТ|ЛОНГ)/i | Normalised to "short" / "long" |
entry | Range pattern after "зоне" keyword | Returns { from: number, to: number } |
targets | Repeating "Закрыть по" pattern | Multi-match, returns number[] |
stoploss | "СТОП-ЛОСС:" followed by price | Returns single number |
validate() predicate are dropped. Only fully-parsed messages reach parser-items.LLM Risk Gate — Ollama outline veto
SignalJobService subscribes to the signalJobSubject observable (a Subject<void> from functools-kit). Whenever signalJobSubject.next() fires — after every crawl — SignalJobService.run() is invoked through a queued wrapper that serialises concurrent calls.For each IParserRow where visited === false (and no matching screen-items record exists), the service calls SignalLogicService.execute(row) inside an ExecutionContextService.runInContext(...) scope that binds { symbol, when, backtest }. SignalLogicService calls json() from agent-swarm-kit against OutlineName.RiskOutline, which builds a multi-turn conversation history (1m candles, 15m candles, pre-computed metrics, draft signal) and then calls the local Ollama model to produce a zod-validated RiskOutlineContract response.next() and the serialised job queue processes all pending rows before the next tick arrives.Store result — screen-items written by ScreenDbService
After
If a
SignalLogicService.execute() returns a complete IScreenDto, SignalJobService calls ScreenDbService.create(dto) to persist it in the screen-items MongoDB collection, then calls ParserDbService.markVisited(row.id) to flip visited = true on the originating parser row.The IScreenDto carries every field from the parser item plus five new LLM-generated fields:| Field | Type | Source |
|---|---|---|
riskAction | "skip" | "follow" | Ollama outline verdict |
riskSureLevel | 5-value enum | Ollama audit field |
riskConfidence | "reliable" | "not_reliable" | Ollama audit field |
riskDescription | string | 2-3 sentence human-readable verdict |
riskReasoning | string | Step-by-step audit trail |
screen-items document for this parserItemId already exists (e.g. from a previous run), the row is skipped entirely — the LLM is never called twice for the same signal.Strategy — getSignal reads screen-items and opens position
addStrategySchema in jan_2026.strategy.ts registers a getSignal callback that backtest-kit calls on every price tick for each tracked symbol. The callback first queries SignalMainService.getLast4HourSignal(symbol, when) against screen-items. If the most recent signal is absent, or its riskAction is "skip", the function returns null immediately — no position opens.If riskAction === "follow", the strategy checks whether the current market close price falls within [signal.entryFrom, signal.entryTo]. Only when price is inside the entry zone is a position returned with priceTakeProfit = signal.targets[2] and priceStopLoss = signal.stoploss.The signalJobSubject observable
The hand-off between crawling and LLM processing uses a single-valueSubject<void> from functools-kit, defined in packages/core/src/config/emitters.ts:
CrawlerMainService.crawlLiveFrame() and CrawlerMainService.crawlBacktestFrame() call await signalJobSubject.next() after completing a crawl pass. SignalJobService.enable() subscribes this.run (a queued async function) to this subject via signalJobSubject.subscribe(this.run). The queued wrapper from functools-kit guarantees that overlapping emissions are serialised — if a 15-minute crawl fires next() while a previous LLM job is still running, the new job waits in a queue rather than spawning a concurrent run.
signalJobSubject carries no payload (void). The job runner always re-reads all unvisited rows from parser-items on each trigger, so it naturally picks up everything added since the last run.Collection, service, and field map
| Collection | Key fields | Written by | Read by |
|---|---|---|---|
parser-items | channel, messageId, symbol, direction, entry, targets, stoploss, visited | ParserDbService.create() via CrawlerService | SignalJobService via ParserDbService.findAllByVisited() |
screen-items | parserItemId, riskAction, riskSureLevel, riskConfidence, riskDescription, riskReasoning, entryFrom, entryTo | SignalJobService via ScreenDbService.create() | SignalMainService.getLast4HourSignal() → strategy getSignal |