Use this file to discover all available pages before exploring further.
The most critical component in backtest-ollama-crontab is the risk outline defined in packages/core/src/logic/outline/risk.outline.ts. It transforms every parsed Telegram signal into a structured verdict by feeding live candle data plus pre-computed market metrics to a local Ollama model (gpt-oss quantized). The output is a zod-validated JSON object that either passes the signal through ("follow") or kills it before it ever reaches the strategy ("skip"). The two rules embedded in the system prompt were derived empirically from backtesting and are intentionally narrow — the LLM is not asked to exercise judgment; it is asked to apply literal threshold checks.
The RISK_PROMPT constant (translated from Russian) encodes exactly three decision branches. Branches 1 and 2 are the veto conditions; branch 3 is the default pass-through.
Rule 1 — Sleeping Coin SHORT
Condition:direction = SHORTANDavgRangePct < 0.07%Action:"skip"A coin whose 1-minute candles have an average high-low range of less than 0.07% of close price has thin liquidity. Shorting into this state invites a stop-hunt: a single large buy order will sweep stops above the entry zone before the price resumes its direction. The prompt calls this a “stop-hunt target”.
Rule 2 — Knife-Catching LONG
Condition:direction = LONGANDmomentum24hPct < -1%Action:"skip"A coin that has fallen more than 1% over the past 24 hours is in active downward momentum. Buying a LONG signal into this move means subscribers enter on the way down; the stop-loss triggers as price continues falling. The prompt calls this “buying into a falling market”.
Rule 3 — Default
Condition: Neither Rule 1 nor Rule 2 triggeredAction:"follow"If neither skip condition is met the signal is passed through as-is. The LLM applies no further judgment — no trend analysis, no news, no momentum on shorter intervals. The rules are applied literally.
action is determined only by avgRangePct / momentum24hPct thresholds against direction. sure_level and confidence are audit-log fields filled independently and do not influence action. A signal can be action: "skip" with confidence: "not_reliable" — those two dimensions are orthogonal.
The following is the full English translation of the RISK_PROMPT constant from risk.outline.ts:
You are a Telegram channel scam-signal detector. Your only task is to answer whetherthe proposed channel signal can be traded. Answer with action: "follow" (open aposition in the channel's direction) or "skip" (pass on the signal).Make your decision ONLY based on two empirically derived rules. No otherconsiderations (trend, news, short-interval momentum) are relevant. Apply the rulesliterally.Available data: - Pre-computed metrics (sent as a separate user message): avgRangePct and momentum24hPct for the 24 hours before publication. - 1m candles for the last 4 hours and 15m candles for the last 8 hours — for evaluating sure_level and confidence (audit log). - Draft channel signal: symbol, direction (LONG or SHORT), targets, stoploss.Decision rules for action:RULE 1 (sleeping coin SHORT): If direction = SHORT AND avgRangePct < 0.07% → action = "skip". Explanation: a "sleeping" asset with thin liquidity (narrow 1m candle range) is a perfect stop-hunt target. The channel calls SHORT; a market-maker will sweep stops upward with a single large candle.RULE 2 (knife-catching LONG): If direction = LONG AND momentum24hPct < -1% → action = "skip". Explanation: the channel calls LONG in a falling market. Subscribers buy on the way down, the market continues falling, the stop-loss triggers.RULE 3 (default): If no skip rule triggered → action = "follow".IMPORTANT: action does NOT depend on sure_level and confidence. Rules are appliedstrictly from avgRangePct/momentum24hPct and direction. sure_level and confidence gointo the audit log for post-analysis.Steps for action: Step 1. Read avgRangePct and momentum24hPct from the metrics message (already pre-computed and provided). Step 2. Check RULE 1 (SHORT + low volatility). Step 3. Check RULE 2 (LONG + falling market). Step 4. If no rule triggered → action = "follow".Audit fields (fill honestly; they do NOT affect action): sure_level — confidence that candles contain accumulation traces: - low: volume structure is organic - low_medium: one fresh anomaly without price displacement - medium: one old anomaly or one with price displacement - medium_high: repeated accumulation - high: multiple accumulations with clear price displacement confidence — data reliability: - reliable: >= 60 1m candles available, metrics are unambiguous - not_reliable: data is sparse, contradictory, or missingRequired output:1. action: "skip" or "follow".2. sure_level: one of five levels.3. confidence: reliable or not_reliable.4. description: 2-3 sentences naming the applied rule with specific numbers.5. reasoning: ONE STRING (type string, NOT object/dict/array). Steps separated by \n.
Before the LLM is invoked, commitMetricsHistory fetches exactly 1440 one-minute candles (24 hours) via getCandles(symbol, "1m", 1440) from backtest-kit and computes two scalar metrics:
// packages/core/src/logic/outline/risk.outline.tsconst PRE_CANDLES_LIMIT = 1440; // 24 h of 1m candlesconst commitMetricsHistory = async (symbol: string, history: IOutlineHistory) => { const candles = await getCandles(symbol, "1m", PRE_CANDLES_LIMIT); if (candles.length === 0) { throw new Error("commitMetricsHistory: no candles returned"); } // Average candle range as % of close price — measures liquidity depth const avgRangePct = candles.reduce((acc, c) => acc + ((c.high - c.low) / c.close) * 100, 0) / candles.length; // 24-hour momentum: (lastClose - firstOpen) / firstOpen * 100 const momentum24hPct = ((candles[candles.length - 1].close - candles[0].open) / candles[0].open) * 100; await history.push( { role: "user", content: [ "Read pre-computed metrics for 24 hours before signal publication and say OK", "", `avgRangePct: ${avgRangePct.toFixed(4)}%`, `momentum24hPct: ${momentum24hPct.toFixed(2)}%`, `candlesCount: ${candles.length}`, ].join("\n"), }, { role: "assistant", content: "OK" }, );};
Metric
Formula
Threshold used
avgRangePct
mean((high − low) / close × 100) over 1440 candles
The getOutlineHistory function builds a multi-turn conversation in four sequential commits before the final instruction message. Each commit function pushes a user/assistant pair into the IOutlineHistory object, so the model receives all context in a single structured conversation.
1
commitOneMinuteHistory — last 4 hours of 1m candles
Calls ask(symbol, AdvisorName.StockData1mAdvisor) from agent-swarm-kit. StockData1mAdvisor retrieves a formatted candle report for the last 4 hours at 1-minute resolution. This data is used by the model to assess sure_level and confidence (audit fields only — it does not affect action).
const report = await ask(symbol, AdvisorName.StockData1mAdvisor);await history.push( { role: "user", content: "Read the latest 1m candles and say OK\n\n" + report }, { role: "assistant", content: "OK" },);
2
commitFifteenMinuteHistory — last 8 hours of 15m candles
Calls ask(symbol, AdvisorName.StockData15mAdvisor). Provides a broader 8-hour view at 15-minute resolution, also used for the audit-log fields sure_level and confidence.
const report = await ask(symbol, AdvisorName.StockData15mAdvisor);await history.push( { role: "user", content: "Read the latest 15m candles and say OK\n\n" + report }, { role: "assistant", content: "OK" },);
3
commitMetricsHistory — avgRangePct and momentum24hPct
Fetches 1440 × 1m candles via getCandles, computes both scalars inline, and pushes them as a pre-formatted metrics block. These are the only numbers the model reads when deciding action.
After all four commits, the final user message instructs the model to apply the rules literally from the pre-computed metrics and return all five output fields.
The RiskOutlineFormat zod schema (from packages/core/src/contract/RiskOutline.ts) is passed to zodResponseFormat() from openai/helpers/zod, which constrains the model’s output to valid JSON matching the schema:
// packages/core/src/contract/RiskOutline.tsimport { z } from "zod";export const RiskOutlineFormat = z.object({ action: z .enum(["skip", "follow"]) .describe( "follow — open a position in the channel's direction\n" + "skip — pass on the signal, do not open a position" ), sure_level: z .enum(["low", "low_medium", "medium", "medium_high", "high"]) .describe( "Confidence that candles contain manipulation traces (audit log):\n" + "low — no traces, organic volume structure\n" + "low_medium — one fresh anomaly without displacement\n" + "medium — one old anomaly or with price displacement\n" + "medium_high — repeated accumulation\n" + "high — multiple accumulations with clear price displacement" ), confidence: z .enum(["reliable", "not_reliable"]) .describe( "Data reliability (audit log):\n" + "reliable — data is unambiguous\n" + "not_reliable — data is contradictory, sparse, or missing" ), description: z .string() .describe( "Brief explanation of action for the trader (2-3 sentences). " + "Name the applied rule and specific numbers." ), reasoning: z .string() .describe( "Detailed justification (one string with \\n separators, NOT an object or array):\n" + "Step 1: avgRangePct=X%, momentum24hPct=Y% (from metrics)\n" + "Step 2: applied rule → action\n" + "Step 3: sure_level and confidence" ),});export type RiskOutlineContract = z.infer<typeof RiskOutlineFormat>;
After the model returns its response, addOutline runs five sequential validators before the result is accepted. A failed validation triggers a retry:
1
action is in allowed set
validate: ({ data }) => { const ACTIONS = ["skip", "follow"]; if (!ACTIONS.includes(data.action)) { throw new Error(`action "${data.action}" outside allowed set ${ACTIONS.join(", ")}`); }}
2
sure_level is one of five values
validate: ({ data }) => { const SURE_LEVELS = ["low", "low_medium", "medium", "medium_high", "high"]; if (!SURE_LEVELS.includes(data.sure_level)) { throw new Error(`sure_level "${data.sure_level}" outside allowed set`); }}
3
confidence is reliable or not_reliable
validate: ({ data }) => { if (data.confidence !== "reliable" && data.confidence !== "not_reliable") { throw new Error(`confidence must be "reliable" or "not_reliable"`); }}
4
description is at least 30 characters
validate: ({ data }) => { if (typeof data.description !== "string" || data.description.trim().length < 30) { throw new Error(`description too short (${data.description?.length ?? 0} chars)`); }}
5
reasoning is at least 80 characters
validate: ({ data }) => { if (typeof data.reasoning !== "string" || data.reasoning.trim().length < 80) { throw new Error(`reasoning too short (${data.reasoning?.length ?? 0} chars)`); }}
Valid documents are automatically dumped to ./dump/outline/risk via the onValidDocument callback, giving you a local audit trail of every LLM decision without any additional tooling.