openpondai/agents/news-bot
OpenTool app
typescript
import {
NewsSignalClient,
evaluateNewsContinuationGate,
type NewsContinuationGateResult,
type NewsEventSignal,
type NewsPropositionSignal,
} from "opentool/adapters/news";
import type { SimpleNewsBotBacktest, SimpleNewsBotConfig } from "./config";
export type SimpleNewsBotSignal = NewsEventSignal | NewsPropositionSignal;
export type SimpleNewsBotBacktestPoint = {
asOf: string;
signal: SimpleNewsBotSignal;
gate: NewsContinuationGateResult | null;
};
function parseBoundaryDate(value: string, endOfDay: boolean): Date {
const trimmed = value.trim();
if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
const suffix = endOfDay ? "T23:59:59.000Z" : "T00:00:00.000Z";
return new Date(`${trimmed}${suffix}`);
}
return new Date(trimmed);
}
export function buildBacktestCheckpoints(backtest: SimpleNewsBotBacktest): string[] {
const start = parseBoundaryDate(backtest.startDate, false);
const end = parseBoundaryDate(backtest.endDate ?? new Date().toISOString(), true);
if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
throw new Error("backtest startDate/endDate must be valid dates.");
}
if (end.getTime() < start.getTime()) {
throw new Error("backtest endDate must be on or after startDate.");
}
const stepHours = backtest.stepHours ?? 24;
const stepMs = stepHours * 60 * 60 * 1000;
const checkpoints: string[] = [];
for (let cursor = start.getTime(); cursor <= end.getTime(); cursor += stepMs) {
checkpoints.push(new Date(cursor).toISOString());
if (checkpoints.length > 366) {
throw new Error("backtest produced too many checkpoints; reduce the date range.");
}
}
return checkpoints;
}
export async function fetchSignal(
config: SimpleNewsBotConfig,
asOf?: string | null,
): Promise<SimpleNewsBotSignal> {
const client = new NewsSignalClient();
if (config.mode === "event") {
return client.eventSignal({
...(config.query ? { query: config.query } : {}),
...(config.eventKey ? { eventKey: config.eventKey } : {}),
...(asOf ? { asOf } : {}),
includePredictionMarkets: config.includePredictionMarkets,
ingestOnRequest: config.ingestOnRequest,
maxAgeHours: config.maxAgeHours,
...(config.gate?.mode === "event" && typeof config.gate.minConfidence === "number"
? { minConfidence: config.gate.minConfidence }
: {}),
...(config.gate?.mode === "event" &&
typeof config.gate.minIndependentSources === "number"
? { minIndependentSources: config.gate.minIndependentSources }
: {}),
...(config.gate?.mode === "event" && typeof config.gate.minTierASources === "number"
? { minTierASources: config.gate.minTierASources }
: {}),
});
}
return client.propositionSignal({
question: config.question,
...(config.query ? { query: config.query } : {}),
...(config.eventKey ? { eventKey: config.eventKey } : {}),
...(config.propositionType ? { propositionType: config.propositionType } : {}),
...(asOf ? { asOf } : {}),
includePredictionMarkets: config.includePredictionMarkets,
ingestOnRequest: config.ingestOnRequest,
maxAgeHours: config.maxAgeHours,
candidateLimit: config.candidateLimit,
});
}
export function evaluateGate(
config: SimpleNewsBotConfig,
signal: SimpleNewsBotSignal,
): NewsContinuationGateResult | null {
if (!config.gate) return null;
return evaluateNewsContinuationGate(signal, config.gate);
}