openpondai/agents/news-bot
OpenTool app
typescript
import type {
NewsContinuationGateResult,
NewsEventSignal,
NewsPredictionMarketMatchedMarket,
NewsPropositionSignal,
} from "opentool/adapters/news";
import type { NewsTradeSignal, SimpleNewsBotConfig } from "./config";
import type { SimpleNewsBotSignal } from "./news-core";
export type NewsTradeDecision = {
signal: NewsTradeSignal;
reason: string;
blockedByGate: boolean;
eventState: string | null;
propositionAnswer: string | null;
confidence: number | null;
};
function resolveBlockedSignal(config: SimpleNewsBotConfig): NewsTradeSignal {
return config.tradePolicy?.blockAction === "close" ? "sell" : "hold";
}
function includes<T extends string>(value: readonly T[] | undefined, target: string): boolean {
return Boolean(value?.includes(target as T));
}
function parsePredictionMarketProbability(
market: NewsPredictionMarketMatchedMarket,
preferredOutcome: "yes" | "no",
): number | null {
return preferredOutcome === "no"
? (market.noProbability ?? null)
: (market.yesProbability ?? null);
}
function selectBestPredictionMarket(
signal: SimpleNewsBotSignal,
): NewsPredictionMarketMatchedMarket | null {
const markets = signal.predictionMarketContext?.matchedMarkets ?? [];
if (markets.length === 0) return null;
return [...markets].sort((left, right) => {
const leftLiquidity = left.liquidity ?? left.volume ?? 0;
const rightLiquidity = right.liquidity ?? right.volume ?? 0;
if (rightLiquidity !== leftLiquidity) return rightLiquidity - leftLiquidity;
const leftProbability = left.leadingProbability ?? 0;
const rightProbability = right.leadingProbability ?? 0;
return rightProbability - leftProbability;
})[0] ?? null;
}
function resolvePredictionMarketSignalDecision(
config: SimpleNewsBotConfig,
signal: SimpleNewsBotSignal,
): NewsTradeDecision | null {
const policy = config.predictionMarketSignal;
if (!policy) return null;
const topMarket = selectBestPredictionMarket(signal);
if (!topMarket) {
return {
signal: policy.onNoMatch ?? "unknown",
reason: "prediction market not found",
blockedByGate: false,
eventState: "question" in signal ? signal.eventState : signal.eventState,
propositionAnswer: "question" in signal ? signal.answer : null,
confidence: "question" in signal ? signal.propositionConfidence : signal.eventConfidence,
};
}
const liquidity = topMarket.liquidity ?? topMarket.volume ?? 0;
if (typeof policy.minLiquidity === "number" && liquidity < policy.minLiquidity) {
return {
signal: policy.onNoMatch ?? "unknown",
reason: `prediction market liquidity ${liquidity.toFixed(2)} below minimum ${policy.minLiquidity.toFixed(2)}`,
blockedByGate: false,
eventState: "question" in signal ? signal.eventState : signal.eventState,
propositionAnswer: "question" in signal ? signal.answer : null,
confidence: "question" in signal ? signal.propositionConfidence : signal.eventConfidence,
};
}
const probability = parsePredictionMarketProbability(topMarket, policy.preferredOutcome);
if (probability == null || probability < policy.minProbability) {
return {
signal: policy.onNoMatch ?? "unknown",
reason:
probability == null
? `prediction market missing ${policy.preferredOutcome} probability`
: `prediction market ${policy.preferredOutcome} probability ${probability.toFixed(4)} below ${policy.minProbability.toFixed(4)}`,
blockedByGate: false,
eventState: "question" in signal ? signal.eventState : signal.eventState,
propositionAnswer: "question" in signal ? signal.answer : null,
confidence: "question" in signal ? signal.propositionConfidence : signal.eventConfidence,
};
}
return {
signal: policy.action ?? "buy",
reason: `prediction market ${policy.preferredOutcome} ${probability.toFixed(4)} on ${topMarket.title}`,
blockedByGate: false,
eventState: "question" in signal ? signal.eventState : signal.eventState,
propositionAnswer: "question" in signal ? signal.answer : null,
confidence: probability,
};
}
function resolveEventSignalDecision(
config: SimpleNewsBotConfig,
signal: NewsEventSignal,
gate: NewsContinuationGateResult | null,
): NewsTradeDecision {
if (gate && !gate.allowed) {
return {
signal: resolveBlockedSignal(config),
reason: gate.reason,
blockedByGate: true,
eventState: signal.eventState,
propositionAnswer: null,
confidence: signal.eventConfidence,
};
}
const entrySignals = config.tradePolicy?.eventEntrySignals ?? ["escalation"];
const exitSignals = config.tradePolicy?.eventExitSignals ?? ["de_escalation", "resolved", "contradiction"];
if (includes(entrySignals, signal.signal) || includes(entrySignals, signal.eventState)) {
return {
signal: "buy",
reason: `event signal ${signal.signal}`,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: null,
confidence: signal.eventConfidence,
};
}
if (includes(exitSignals, signal.signal) || includes(exitSignals, signal.eventState)) {
return {
signal: "sell",
reason: `event signal ${signal.signal}`,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: null,
confidence: signal.eventConfidence,
};
}
return {
signal: signal.triggerPassed ? "hold" : "unknown",
reason: signal.whyNotTriggered ?? `event signal ${signal.signal}`,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: null,
confidence: signal.eventConfidence,
};
}
function resolvePropositionDecision(
config: SimpleNewsBotConfig,
signal: NewsPropositionSignal,
gate: NewsContinuationGateResult | null,
): NewsTradeDecision {
if (gate && !gate.allowed) {
return {
signal: resolveBlockedSignal(config),
reason: gate.reason,
blockedByGate: true,
eventState: signal.eventState,
propositionAnswer: signal.answer,
confidence: signal.propositionConfidence,
};
}
const entryAnswers = config.tradePolicy?.propositionEntryAnswers ?? ["yes"];
const exitAnswers = config.tradePolicy?.propositionExitAnswers ?? ["no", "unclear"];
if (includes(entryAnswers, signal.answer)) {
return {
signal: "buy",
reason: `proposition answer ${signal.answer}`,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: signal.answer,
confidence: signal.propositionConfidence,
};
}
if (includes(exitAnswers, signal.answer)) {
return {
signal: "sell",
reason: `proposition answer ${signal.answer}`,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: signal.answer,
confidence: signal.propositionConfidence,
};
}
return {
signal: signal.answer === "unclear" ? "hold" : "unknown",
reason: signal.reasoning,
blockedByGate: false,
eventState: signal.eventState,
propositionAnswer: signal.answer,
confidence: signal.propositionConfidence,
};
}
export function resolveNewsTradeDecision(
config: SimpleNewsBotConfig,
signal: SimpleNewsBotSignal,
gate: NewsContinuationGateResult | null,
): NewsTradeDecision {
const predictionMarketDecision = resolvePredictionMarketSignalDecision(config, signal);
if (predictionMarketDecision) {
return predictionMarketDecision;
}
if ("question" in signal) {
return resolvePropositionDecision(config, signal, gate);
}
return resolveEventSignalDecision(config, signal, gate);
}