Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/backtest-kit/backtest-ollama-crontab/llms.txt

Use this file to discover all available pages before exploring further.

The parser is the second stage of the signal pipeline. After ScraperService retrieves raw Telegram message text, CryptoYodaScreenService passes each message through a set of regex-based extraction rules that produce a structured trading signal. Any message that fails a required extraction or fails a field-level validation receives a null data payload and is skipped by the crawler — only fully-formed signals reach the database.

Signal Format

The channel posts signals in Russian. A typical message looks like this (translated for illustration):
SIGNAL #TRX/USDT

Open SHORT in the zone $0.2898 – $0.2930 with 25x leverage.

Take-profit targets:
TP1: $0.2875
TP2: $0.2864
TP3: $0.2838

STOP-LOSS: $0.3027
The original messages are written in Russian. The regex patterns in SIGNAL_FORMAT match Russian keywords (e.g. ШОРТ, ЛОНГ, зоне, Закрыть, СТОП-ЛОСС). The translated example above is provided for readability only.

SIGNAL_FORMAT

CryptoYodaScreenService defines a SIGNAL_FORMAT constant that maps each field name to an ExtractConfig descriptor. This is the complete definition from the source:
const SIGNAL_FORMAT: ParseFormat<SignalFields> = {
  symbol: {
    pattern: /#([A-Z0-9]+)\/USDT/,
    group: 1,
    validate: (v) => v.length > 0,
  },
  direction: {
    pattern: /(ШОРТ|ЛОНГ)/i,
    transform: (raw) => (raw.toUpperCase() === "ШОРТ" ? "short" : "long"),
    validate: (v) => v === "short" || v === "long",
  },
  entry: {
    pattern: /зоне\s+\$?([\d.,]+)\s*[-–—]\s*(?:\$?[\d.,]+\s*[-–—]\s*)?\$?([\d.,]+)(?=\s)/i,
    transform: (_, m) => ({ from: num(m[1]), to: num(m[2]) }),
    validate: (v) => isNum(v.from) && isNum(v.to) && v.from < v.to,
  },
  targets: {
    pattern: /Закрыть(?:\s+ордер)?\s+по(?:\s+цене)?\s+\$?([\d.,]+)/gi,
    transform: (_, m) => num(m[1]),
    validate: (v) => isNum(v),
    multi: true,
  },
  stoploss: {
    pattern: /СТОП-?ЛОСС:\s*\$?([\d.,]+)/i,
    transform: (_, m) => num(m[1]),
    validate: (v) => isNum(v),
  },
};
The helper num(s) normalises decimal separators (",""."), and isNum(v) checks that the result is a finite positive number.

Field breakdown

FieldPatternOutput typeNotes
symbol/#([A-Z0-9]+)\/USDT/stringCapture group 1 — ticker without /USDT. CrawlerService appends USDT when storing.
direction/(ШОРТ|ЛОНГ)/i"short" | "long"Russian keyword transformed to lowercase English.
entryZone price range regex{ from: number; to: number }Validates from < to and both positive.
targetsGlobal TP regexnumber[]multi: true — collects all matches. Requires the global flag (g).
stoplossСТОП-ЛОСС regexnumberValidates positive finite number.

ParseFormat Type

Each field in a ParseFormat object maps to an ExtractConfig<T> with the following interface:
interface ExtractConfig<T> {
  /** The extraction regex. Must have the global flag when multi = true. */
  pattern: RegExp;

  /** Capture group index to extract. Defaults to 1. */
  group?: number;

  /** Transforms the raw string match into the desired type. Receives the
   *  raw captured string and the full RegExpMatchArray. */
  transform?: (raw: string, match: RegExpMatchArray) => T;

  /** Returns false to reject the extracted value (the whole message fails). */
  validate?: (value: T) => boolean;

  /** If true, collect all matches via matchAll (requires global flag on pattern). */
  multi?: boolean;

  /** If true, a missing match does not fail the whole message. */
  optional?: boolean;
}
The EXTRACT_DATA_FN inside ParserService iterates over each key in the format object and applies these rules in order:
1

Single vs. multi match

If multi is true, text.matchAll(pattern) collects all matches. If the result is empty and the field is not optional, the entire message returns null.
2

Group extraction

The raw value is taken from match[cfg.group ?? 1], falling back to match[0] if the indexed group is undefined.
3

Transform

If transform is defined, it is called with the raw string and the full match array.
4

Validate

If validate is defined and returns false, the message returns null (or every element fails for multi fields).

ParserService

ParserService (packages/core/src/lib/services/core/ParserService.ts) provides a single public method: parseDay. It accepts a list of raw ScraperMessage objects and a FieldMapping format, runs EXTRACT_DATA_FN against each message’s content, and returns an array of ParserMessageBase records:
public parseDay = async <M extends FieldMapping>(
  messages: ScraperMessage[],
  format: M,
): Promise<ParserMessageBase<M>[]> => {
  this.loggerService.log("parserService parseDay", {
    messagesLen: messages.length,
  });

  const result: ParserMessageBase<M>[] = [];

  for (const msg of messages) {
    const extracted = EXTRACT_DATA_FN(msg.content, format);
    console.log("Parsed: ", {
      message: msg.content.slice(0, MAX_PREVIEW_CHARS).split("\n").join(" "),
      extracted,
    });
    result.push({
      ...msg,
      data: extracted,
    });
  }

  return result;
};
Every input message produces exactly one output record. If EXTRACT_DATA_FN returns null (any required field failed), the record has data: null. The crawler checks if (!msg.data) and skips those records without writing them to MongoDB.

Custom Channel Parsers

To add support for a different Telegram channel, follow these steps:
1

Define SIGNAL_FORMAT

Create a new ParseFormat object with ExtractConfig entries that match the new channel’s message structure. Test each regex against real sample messages before integrating.
2

Create a ScreenService

Create a new class (e.g. MyChannelScreenService) with screenDay(date) and parseDay(messages) methods following the pattern of CryptoYodaScreenService. Set CHANNEL_NAME to the new channel’s Telegram identifier.
3

Register with CrawlerService

Inject the new screen service into CrawlerService and add newScreenService.screenDay to the RUN_CRAWLER_FN call. Add the corresponding msg.type === "my_channel" branch in crawlRange to persist its records.
The packages/core/test/ suite contains unit tests for the parser. Run them with real channel messages to validate your regex patterns before connecting to the live Telegram API. A pattern with an off-by-one capture group or a missing g flag on a multi field will silently produce null for every message.

Build docs developers (and LLMs) love