feat(chat): explain waiting trade signals

This commit is contained in:
root 2026-05-07 06:59:40 +00:00
parent f3dfe31d1f
commit 3f73310b4e
3 changed files with 115 additions and 10 deletions

View File

@ -234,6 +234,7 @@ type ChatAction =
| 'update_profile' | 'update_profile'
| 'explain' | 'explain'
| 'explain_position' | 'explain_position'
| 'explain_waiting'
| 'explain_blocker' | 'explain_blocker'
| 'summarize_reconciliation'; | 'summarize_reconciliation';
@ -252,6 +253,19 @@ interface ChatRuntimeContextPosition {
takeProfit?: number; takeProfit?: number;
} }
interface ChatRuntimeSignalContext {
symbol?: string;
profileId?: string;
profileName?: string;
signal?: string;
passed?: boolean;
reason?: string;
executionStatus?: 'EXECUTED' | 'BLOCKED' | 'SKIPPED';
executionCode?: string;
executionReason?: string;
orderId?: string;
}
interface ChatRuntimeContextOrder { interface ChatRuntimeContextOrder {
id?: string; id?: string;
symbol?: string; symbol?: string;
@ -319,6 +333,7 @@ interface ChatRuntimeContextHealth {
} }
interface ChatRuntimeContextPayload { interface ChatRuntimeContextPayload {
signalContexts: ChatRuntimeSignalContext[];
positions: ChatRuntimeContextPosition[]; positions: ChatRuntimeContextPosition[];
recentOrders: ChatRuntimeContextOrder[]; recentOrders: ChatRuntimeContextOrder[];
recentHistory: ChatRuntimeContextHistory[]; recentHistory: ChatRuntimeContextHistory[];
@ -1026,8 +1041,9 @@ export class ApiServer {
return { return {
profiles: context, profiles: context,
runtime: { runtime: {
positions: [], positions: [],
recentOrders: [], signalContexts: [],
recentOrders: [],
recentHistory: [], recentHistory: [],
orderFailures: [], orderFailures: [],
operationalEvents: [], operationalEvents: [],
@ -1045,6 +1061,7 @@ export class ApiServer {
profiles: Array.isArray(record.profiles) ? record.profiles : [], profiles: Array.isArray(record.profiles) ? record.profiles : [],
runtime: { runtime: {
positions: Array.isArray(runtime.positions) ? runtime.positions : [], positions: Array.isArray(runtime.positions) ? runtime.positions : [],
signalContexts: Array.isArray(runtime.signalContexts) ? runtime.signalContexts : [],
recentOrders: Array.isArray(runtime.recentOrders) ? runtime.recentOrders : [], recentOrders: Array.isArray(runtime.recentOrders) ? runtime.recentOrders : [],
recentHistory: Array.isArray(runtime.recentHistory) ? runtime.recentHistory : [], recentHistory: Array.isArray(runtime.recentHistory) ? runtime.recentHistory : [],
orderFailures: Array.isArray(runtime.orderFailures) ? runtime.orderFailures : [], orderFailures: Array.isArray(runtime.orderFailures) ? runtime.orderFailures : [],
@ -1136,6 +1153,30 @@ export class ApiServer {
}; };
} }
private buildWaitingExplanation(message: string, chatContext: ChatRequestContext): ChatResponsePayload | null {
const symbolHint = this.extractPrimaryMentionedSymbol(message);
const signals = (chatContext.runtime.signalContexts || []).filter((signal) =>
!symbolHint || String(signal.symbol || '').toUpperCase() === symbolHint
);
const target = signals.find((signal) => signal.executionStatus === 'BLOCKED' || signal.executionStatus === 'SKIPPED')
|| signals.find((signal) => signal.passed === false)
|| signals[0];
if (!target) return null;
const label = String(target.profileName || target.profileId || 'the active profile');
const summarySource = String(target.executionReason || target.reason || '').trim();
const signalDirection = String(target.signal || 'NEUTRAL').toUpperCase();
const executionStatus = String(target.executionStatus || (target.passed === false ? 'BLOCKED' : 'WAITING')).toUpperCase();
return {
action: 'explain_waiting',
summary: `${label} on ${target.symbol || 'the current symbol'} is currently ${executionStatus.toLowerCase()} with signal ${signalDirection}.${summarySource ? ` ${summarySource}` : ''}`,
reasoning: target.executionCode
? `Execution code: ${target.executionCode}. This means the bot evaluated the setup but did not proceed yet.${target.orderId ? ` Related order: ${target.orderId}.` : ''}`
: 'The bot is still evaluating rule alignment, risk constraints, or session eligibility before entering.'
};
}
private buildReconciliationSummary(chatContext: ChatRequestContext): ChatResponsePayload { private buildReconciliationSummary(chatContext: ChatRequestContext): ChatResponsePayload {
const health = chatContext.runtime.health || {}; const health = chatContext.runtime.health || {};
const mismatchCount = Number(health.reconciliationMismatchCount || 0); const mismatchCount = Number(health.reconciliationMismatchCount || 0);
@ -1177,6 +1218,7 @@ export class ApiServer {
private buildLocalChatFallback(message: string, context: ChatRequestContext): ChatResponsePayload { private buildLocalChatFallback(message: string, context: ChatRequestContext): ChatResponsePayload {
const lower = String(message || '').toLowerCase(); const lower = String(message || '').toLowerCase();
const asksForReconciliation = /\b(reconciliation|reconcile|quarantine|manual review|stale order|no[_\s-]*go|drift)\b/i.test(lower); const asksForReconciliation = /\b(reconciliation|reconcile|quarantine|manual review|stale order|no[_\s-]*go|drift)\b/i.test(lower);
const asksForWaiting = /\b(why no trade|why.*no trade|why didn'?t.*trade|why did not.*trade|why.*enter|waiting for|no entry|no signal)\b/i.test(lower);
const asksForBlocker = /\b(why.*(blocked|stuck|failed|not execute|didn'?t execute|did not execute)|blocker|blocked|manual trader unavailable|unauthorized|forbidden|failed order)\b/i.test(lower); const asksForBlocker = /\b(why.*(blocked|stuck|failed|not execute|didn'?t execute|did not execute)|blocker|blocked|manual trader unavailable|unauthorized|forbidden|failed order)\b/i.test(lower);
const asksForPosition = /\b(position|holding|portfolio|pnl|profit|loss|exit|take profit|stop loss)\b/i.test(lower); const asksForPosition = /\b(position|holding|portfolio|pnl|profit|loss|exit|take profit|stop loss)\b/i.test(lower);
const asksForExplain = /(what|how|why|help|explain|suggest)/i.test(lower) const asksForExplain = /(what|how|why|help|explain|suggest)/i.test(lower)
@ -1195,6 +1237,15 @@ export class ApiServer {
}; };
} }
if (asksForWaiting) {
return this.buildWaitingExplanation(message, context)
|| {
action: 'explain_waiting',
summary: 'I did not find a recent rule-evaluation or execution-waiting signal in the scoped runtime context.',
reasoning: 'If you expected a trade, check the active profile, the relevant symbol signal panel, and any recent blocked or skipped execution reasons.'
};
}
if (asksForPosition) { if (asksForPosition) {
return this.buildPositionExplanation(message, context) return this.buildPositionExplanation(message, context)
|| { || {
@ -2940,7 +2991,7 @@ AVAILABLE RULES (use these exact ruleId values):
PROFILE SCHEMA: PROFILE SCHEMA:
{ {
"action": "create_profile" | "update_profile" | "explain" | "explain_position" | "explain_blocker" | "summarize_reconciliation", "action": "create_profile" | "update_profile" | "explain" | "explain_position" | "explain_waiting" | "explain_blocker" | "summarize_reconciliation",
"profile": { "profile": {
"name": string, "name": string,
"allocated_capital": number, "allocated_capital": number,
@ -2968,13 +3019,14 @@ RULES:
2. For "update_profile": include the profile "id" field and only change what the user asked for. Keep everything else the same. 2. For "update_profile": include the profile "id" field and only change what the user asked for. Keep everything else the same.
3. For "explain": use it for general educational answers. No profile needed. 3. For "explain": use it for general educational answers. No profile needed.
4. For "explain_position": use live position context and explain the current holding plus likely next step. No profile needed. 4. For "explain_position": use live position context and explain the current holding plus likely next step. No profile needed.
5. For "explain_blocker": explain the most relevant trade/order/operational blocker from runtime context. No profile needed. 5. For "explain_waiting": explain why a profile/symbol has not entered yet using signal/rule/execution context. No profile needed.
6. For "summarize_reconciliation": summarize reconciliation health, mismatches, quarantines, and operational implications. No profile needed. 6. For "explain_blocker": explain the most relevant trade/order/operational blocker from runtime context. No profile needed.
7. Only include "profile" when the action is create_profile or update_profile. 7. For "summarize_reconciliation": summarize reconciliation health, mismatches, quarantines, and operational implications. No profile needed.
8. If runtime context is relevant, prefer concrete symbols, profile names, counts, and current state over generic advice. 8. Only include "profile" when the action is create_profile or update_profile.
9. Match the user's risk appetite: "conservative" = low risk (0.5-1%), low capital. "aggressive" = higher risk (2-5%), more rules enabled. 9. If runtime context is relevant, prefer concrete symbols, profile names, counts, and current state over generic advice.
10. Always include at least TrendBiasRule and RiskManagementRule as enabled for safety. 10. Match the user's risk appetite: "conservative" = low risk (0.5-1%), low capital. "aggressive" = higher risk (2-5%), more rules enabled.
11. Output ONLY valid JSON. No markdown, no backticks, no explanation outside the JSON.`; 11. Always include at least TrendBiasRule and RiskManagementRule as enabled for safety.
12. Output ONLY valid JSON. No markdown, no backticks, no explanation outside the JSON.`;
try { try {
let aiResponse: string | null = null; let aiResponse: string | null = null;
@ -3016,6 +3068,7 @@ RULES:
'update_profile', 'update_profile',
'explain', 'explain',
'explain_position', 'explain_position',
'explain_waiting',
'explain_blocker', 'explain_blocker',
'summarize_reconciliation' 'summarize_reconciliation'
]); ]);

View File

@ -22,6 +22,32 @@ const profilesFixture = [
const botStateFixture: BotState = { const botStateFixture: BotState = {
...DEFAULT_BOT_STATE, ...DEFAULT_BOT_STATE,
symbols: {
'BTC/USDT': {
price: 61500,
change24h: 2.1,
changeToday: 1.2,
session: 'London',
volatility: 'High',
signal: 'BUY',
priceHistory: [],
rules: {},
profileSignals: {
p1: {
profileName: 'High Risk Scalper ⚡',
signal: 'BUY',
passed: false,
reason: 'Waiting for stronger rule alignment.',
execution: {
status: 'SKIPPED',
code: 'rule_ratio_not_met',
reason: 'Only 2 of 4 voting rules passed.',
}
}
},
indicators: {},
}
},
positions: [ positions: [
{ {
id: 'pos-1', id: 'pos-1',
@ -219,5 +245,14 @@ describe('ChatControl DOM flow', () => {
reconciliationNoGoTrades: 1, reconciliationNoGoTrades: 1,
}) })
); );
expect(body.context.runtime.signalContexts).toEqual(
expect.arrayContaining([
expect.objectContaining({
symbol: 'BTC/USDT',
executionCode: 'rule_ratio_not_met',
executionStatus: 'SKIPPED',
})
])
);
}); });
}); });

View File

@ -47,6 +47,7 @@ export const BASE_QUICK_ACTIONS: QuickAction[] = [
{ label: 'Low Risk Profile', prompt: 'Create a low-risk profile that only trades BTC during London and NY sessions with $5000 capital' }, { label: 'Low Risk Profile', prompt: 'Create a low-risk profile that only trades BTC during London and NY sessions with $5000 capital' },
{ label: 'AI Momentum', prompt: 'Create a momentum strategy with AI analysis enabled, focusing on ETH/SOL with 2% risk' }, { label: 'AI Momentum', prompt: 'Create a momentum strategy with AI analysis enabled, focusing on ETH/SOL with 2% risk' },
{ label: 'Explain holding', prompt: 'Explain my current open holding and what the bot is waiting for next.' }, { label: 'Explain holding', prompt: 'Explain my current open holding and what the bot is waiting for next.' },
{ label: 'Why no trade?', prompt: 'Why has no trade fired yet for my active profile? Explain what the bot is waiting for.' },
{ label: 'Explain blocker', prompt: 'Why is a trade or exit blocked right now? Explain the main blocker.' }, { label: 'Explain blocker', prompt: 'Why is a trade or exit blocked right now? Explain the main blocker.' },
{ label: 'Recon summary', prompt: 'Summarize reconciliation health, stale orders, and any manual review risk right now.' }, { label: 'Recon summary', prompt: 'Summarize reconciliation health, stale orders, and any manual review risk right now.' },
{ label: 'What rules?', prompt: 'What rules should I use for a day trading strategy?' }, { label: 'What rules?', prompt: 'What rules should I use for a day trading strategy?' },
@ -103,6 +104,22 @@ const isProfileMutationAction = (action?: ChatAssistantAction): action is 'creat
action === 'create_profile' || action === 'update_profile'; action === 'create_profile' || action === 'update_profile';
const summarizeRuntimeContext = (botState: BotState) => ({ const summarizeRuntimeContext = (botState: BotState) => ({
signalContexts: Object.entries(botState.symbols ?? {})
.flatMap(([symbol, symbolState]) =>
Object.entries(symbolState?.profileSignals || {}).map(([profileId, profileSignal]) => ({
symbol,
profileId,
profileName: profileSignal?.profileName,
signal: profileSignal?.signal,
passed: profileSignal?.passed,
reason: profileSignal?.reason,
executionStatus: profileSignal?.execution?.status,
executionCode: profileSignal?.execution?.code,
executionReason: profileSignal?.execution?.reason,
orderId: profileSignal?.execution?.orderId,
}))
)
.slice(0, 20),
positions: (botState.positions ?? []).slice(0, 10).map((position) => ({ positions: (botState.positions ?? []).slice(0, 10).map((position) => ({
symbol: position.symbol, symbol: position.symbol,
side: position.side, side: position.side,