Universal LLM adapter for backtest-kit trading strategies. One higher-order-function API across 12 providers, schema-enforced structured output, userspace prompt modules, token rotation โ plus an LLM strategy optimizer that generates runnable strategy code.

๐ Docs ยท ๐ Reference implementation ยท ๐ GitHub
npm install @backtest-kit/ollama backtest-kit agent-swarm-kit
AI strategies normally mean per-provider SDK boilerplate and JSON you can't trust. This package collapses all of it: wrap any async function with a provider HOF and it runs inside that provider's inference context โ swap deepseek() โ claude() โ gpt5() with no other change. Structured output is schema-enforced (Zod or JSON schema via agent-swarm-kit's addOutline), prompts live as memoized userspace modules
di-scoped; same signature in, same out..cjs modules in config/prompt/, or inline; memoized via functools-kit.Optimizer generates complete executable strategy code from LLM analysis across training ranges.The whole adapter is one shape, repeated for 12 providers: provider(fn, model, apiKey?) => fn. It returns a function with the same signature as fn, executed inside the provider's inference context (so any agent-swarm-kit completion inside resolves to that provider).
import { deepseek } from '@backtest-kit/ollama';
import { addStrategy } from 'backtest-kit';
addStrategy({
strategyName: 'llm-signal', interval: '5m',
// swap deepseek() โ claude() / gpt5() / ollama() / groq() with no other change
getSignal: deepseek(getSignal, 'deepseek-chat', process.env.DEEPSEEK_API_KEY),
});
| Provider | Function | Inference | Base URL |
|---|---|---|---|
| OpenAI | gpt5() |
gpt5_inference |
https://api.openai.com/v1/ |
| Claude | claude() |
claude_inference |
https://api.anthropic.com/v1/ |
| DeepSeek | deepseek() |
deepseek_inference |
https://api.deepseek.com/ |
| Grok (xAI) | grok() |
grok_inference |
https://api.x.ai/v1/ |
| Groq | groq() |
groq_inference |
https://api.groq.com/ |
| Mistral | mistral() |
mistral_inference |
https://api.mistral.ai/v1/ |
| Perplexity | perplexity() |
perplexity_inference |
https://api.perplexity.ai/ |
| Cohere | cohere() |
cohere_inference |
https://api.cohere.ai/compatibility/v1/ |
| Alibaba (Qwen) | alibaba() |
alibaba_inference |
https://dashscope-intl.aliyuncs.com/compatible-mode/v1/ |
| Hugging Face | hf() |
hf_inference |
https://router.huggingface.co/v1/ |
| Ollama (local) | ollama() |
ollama_inference |
http://localhost:11434/ |
| GLM-4 (Z.ai) | glm4() |
glm4_inference |
https://open.bigmodel.cn/api/paas/v4/ |
// apiKey accepts a single key OR an array โ automatic rotation across calls
const wrappedFn = ollama(myFn, 'llama3.3:70b', ['key1', 'key2', 'key3']);
All twelve share one signature โ <T>(fn: T, model: string, apiKey?: string | string[]) => T โ and run fn inside ContextService.runInContext({ apiKey, inference, model }). The matching InferenceName enum + per-provider client/*Provider.client.ts + config/*.ts resolve the actual SDK call.
Define a schema (Zod or raw JSON), register it as an outline against this package's CompletionName, and the LLM is forced to return valid JSON โ with custom validations that reject bad signals (e.g. "SL must be below entry for LONG").
// schema/Signal.schema.ts
import { z } from 'zod';
export const SignalSchema = z.object({
position: z.enum(['long', 'short', 'wait']).describe('long: bullish ยท short: bearish ยท wait: unclear'),
price_open: z.number().describe('Entry price in USD'),
price_stop_loss: z.number().describe('LONG: below entry ยท SHORT: above entry'),
price_take_profit: z.number().describe('LONG: above entry ยท SHORT: below entry'),
minute_estimated_time: z.number().describe('Estimated minutes to reach TP'),
risk_note: z.string().describe('Whale manipulation, order-book imbalance, divergences โ with numbers'),
});
export type TSignalSchema = z.infer<typeof SignalSchema>;
// outline/signal.outline.ts
import { addOutline } from 'agent-swarm-kit';
import { zodResponseFormat } from 'openai/helpers/zod';
import { SignalSchema, TSignalSchema } from '../schema/Signal.schema';
import { CompletionName } from '@backtest-kit/ollama';
addOutline<TSignalSchema>({
outlineName: 'SignalOutline',
completion: CompletionName.RunnerOutlineCompletion,
format: zodResponseFormat(SignalSchema, 'position_decision'),
getOutlineHistory: async ({ history, param: messages = [] }) => { await history.push(messages); },
validations: [{
validate: ({ data }) => {
if (data.position === 'long' && data.price_stop_loss >= data.price_open) throw new Error('LONG: SL must be below entry');
if (data.position === 'short' && data.price_stop_loss <= data.price_open) throw new Error('SHORT: SL must be above entry');
},
}],
});
import { addOutline, IOutlineFormat } from 'agent-swarm-kit';
import { CompletionName } from '@backtest-kit/ollama';
const format: IOutlineFormat = {
type: 'object',
properties: {
take_profit_price: { type: 'number', description: 'Take profit price in USD' },
stop_loss_price: { type: 'number', description: 'Stop-loss price in USD' },
description: { type: 'string', description: 'User-friendly risk explanation, min 10 sentences' },
reasoning: { type: 'string', description: 'Technical analysis, min 15 sentences' },
},
required: ['take_profit_price', 'stop_loss_price', 'description', 'reasoning'],
};
addOutline({
outlineName: 'SignalOutline', format, completion: CompletionName.RunnerOutlineCompletion,
prompt: 'Generate crypto trading signals from price & volume indicators in JSON.',
getOutlineHistory: async ({ history, param }) => {
const report = await ioc.signalReportService.getSignalReport(param);
await commitReports(history, report);
await history.push({ role: 'user', content: 'Generate JSON based on reports.' });
},
validations: [
{ docDescription: 'Stop-loss vs max loss %', validate: ({ data }) => { if (data.action === 'buy' && percentDiff(data.current_price, data.stop_loss_price) > CC_LADDER_STOP_LOSS) throw new Error(`SL must not exceed -${CC_LADDER_STOP_LOSS}%`); } },
{ docDescription: 'Take-profit vs max profit %', validate: ({ data }) => { if (data.action === 'buy' && percentDiff(data.current_price, data.take_profit_price) > CC_LADDER_TAKE_PROFIT) throw new Error(`TP must not exceed +${CC_LADDER_TAKE_PROFIT}%`); } },
],
});
Prompt modules receive trading context automatically. system may be a string array or a function of (symbol, strategyName, exchangeName, frameName, backtest); user likewise.
// config/prompt/signal.prompt.cjs
module.exports = {
system: (symbol, strategyName, exchangeName, frameName, backtest) => [
`You are analyzing ${symbol} on ${exchangeName}`,
`Strategy: ${strategyName}, Timeframe: ${frameName}`,
backtest ? 'Backtest mode' : 'Live mode',
],
user: (symbol) => `Analyze ${symbol} and return a trading decision`,
};
import { Module, Prompt, commitPrompt, MessageModel } from '@backtest-kit/ollama';
// from a .cjs module (default baseDir: {cwd}/config/prompt/), memoized
const signalModule = Module.fromPath('./signal.prompt.cjs');
// or inline
const inline = Prompt.fromPrompt({ system: ['You are a trading bot'], user: (symbol) => `Trend for ${symbol}?` });
const messages: MessageModel[] = [];
await commitPrompt(signalModule, messages); // pushes rendered system + user messages with context
Full strategy: register the outline, build messages from a prompt, request structured JSON, wrap with a provider HOF:
import './outline/signal.outline';
import { deepseek, Module, commitPrompt, MessageModel } from '@backtest-kit/ollama';
import { addStrategy } from 'backtest-kit';
import { json } from 'agent-swarm-kit';
const signalModule = Module.fromPath('./signal.prompt.cjs');
const getSignal = async () => {
const messages: MessageModel[] = [];
await commitPrompt(signalModule, messages);
const { data } = await json('SignalOutline', messages);
return data;
};
addStrategy({ strategyName: 'llm-signal', interval: '5m',
getSignal: deepseek(getSignal, 'deepseek-chat', process.env.DEEPSEEK_API_KEY) });
dumpSignalData(signalId, history, signal, outputDir?) archives the full LLM conversation attached to a signal, so an opaque model decision becomes a readable record. Skips if the directory already exists (never overwrites prior runs).
Into {outputDir}/{signalId}/ (default ./dump/strategy): 00_system_prompt.md (system messages + output summary), numbered XX_user_message.md / XX_assistant_message.md per turn, and a final XX_llm_output.md with the signal DTO. Call it from getSignal right before returning the signal.
The most powerful piece, and the one the rest of the package feeds: Optimizer uses an LLM to analyze a symbol across training ranges and emit a complete, executable strategy file โ imports, helpers, strategies, walker, and launcher โ that you can run with backtest-kit directly.
import { Optimizer, addOptimizerSchema, listenOptimizerProgress } from '@backtest-kit/ollama';
// describe sources, training ranges, strategy/template generation (see IOptimizer* interfaces)
addOptimizerSchema({ optimizerName: 'my-optimizer', /* sources, ranges, strategy, template */ });
listenOptimizerProgress((p) => console.log(p)); // ProgressOptimizerContract
const strategies = await Optimizer.getData('BTCUSDT', { optimizerName: 'my-optimizer' }); // metadata + LLM context per range
const code = await Optimizer.getCode('BTCUSDT', { optimizerName: 'my-optimizer' }); // full TS/JS source as string
await Optimizer.dump('BTCUSDT', { optimizerName: 'my-optimizer' }, './output'); // writes {optimizerName}_{symbol}.mjs
getData fetches from all sources and builds the LLM conversation per training range; getCode assembles the executable strategy; dump writes it to {optimizerName}_{symbol}.mjs. Companion registry functions: getOptimizerSchema, listOptimizerSchema, and listenError. The engine behind it is common/ClientOptimizer.ts driven by the IOptimizer* interfaces (IOptimizerSchema, IOptimizerSource, IOptimizerStrategy, IOptimizerTemplate, IOptimizerRange, IOptimizerData, IOptimizerFetchArgs, IOptimizerFilterArgs, IOptimizerCallbacks).
| Export | Description |
|---|---|
ollama gpt5 claude deepseek grok groq mistral perplexity cohere alibaba hf glm4 |
Provider HOFs โ (fn, model, apiKey?) => fn |
CompletionName |
Completion-name enum for agent-swarm-kit outlines (RunnerOutlineCompletion, โฆ) |
Module.fromPath(path, baseDir?) |
Load a prompt .cjs module (default baseDir {cwd}/config/prompt/) |
Prompt.fromPrompt(source) |
Build a prompt from an inline PromptModel |
commitPrompt(source, history) |
Render a Module/Prompt's system+user messages into history |
dumpSignalData(id, history, signal, dir?) |
Archive the LLM conversation for one signal |
validate(...) |
Validate an outline result |
Optimizer |
.getData / .getCode / .dump โ LLM strategy-code generation |
addOptimizerSchema ยท getOptimizerSchema ยท listOptimizerSchema |
Optimizer schema registry |
listenOptimizerProgress ยท listenError |
Optimizer progress / error events |
MessageModel MessageRole PromptModel |
Message & prompt models |
IOptimizer* ยท ProgressOptimizerContract |
Optimizer interfaces & progress contract |
lib |
The internal engine (IoC container) for advanced use |
function/signal.function.ts โ the 12 provider HOFs. function/{add,get,list,event,setup,validate,history,dump,signal}.ts โ registry, events, setLogger, commitPrompt, dumpSignalData.client/*Provider.client.ts (12) โ per-provider SDK adapters. config/*.ts โ per-provider base URLs/params, ollama.rotate.ts (token rotation), params.ts, emitters.ts.classes/ โ Module, Prompt, Optimizer. common/ClientOptimizer.ts โ the optimizer engine.enum/ โ InferenceName (12), CompletionName. interface/Optimizer.interface.ts, contract/ProgressOptimizer.contract.ts, model/{Message,Prompt}.model.ts.helpers/{toLintMarkdown,toPlainString}.ts, lib/ (IoC: core/{di,provide,types}, services). Nothing in src/ is undocumented.Fork / PR on GitHub.
MIT ยฉ tripolskypetr