feat(chat): explain waiting trade signals
This commit is contained in:
parent
f3dfe31d1f
commit
3f73310b4e
@ -234,6 +234,7 @@ type ChatAction =
|
||||
| 'update_profile'
|
||||
| 'explain'
|
||||
| 'explain_position'
|
||||
| 'explain_waiting'
|
||||
| 'explain_blocker'
|
||||
| 'summarize_reconciliation';
|
||||
|
||||
@ -252,6 +253,19 @@ interface ChatRuntimeContextPosition {
|
||||
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 {
|
||||
id?: string;
|
||||
symbol?: string;
|
||||
@ -319,6 +333,7 @@ interface ChatRuntimeContextHealth {
|
||||
}
|
||||
|
||||
interface ChatRuntimeContextPayload {
|
||||
signalContexts: ChatRuntimeSignalContext[];
|
||||
positions: ChatRuntimeContextPosition[];
|
||||
recentOrders: ChatRuntimeContextOrder[];
|
||||
recentHistory: ChatRuntimeContextHistory[];
|
||||
@ -1026,8 +1041,9 @@ export class ApiServer {
|
||||
return {
|
||||
profiles: context,
|
||||
runtime: {
|
||||
positions: [],
|
||||
recentOrders: [],
|
||||
positions: [],
|
||||
signalContexts: [],
|
||||
recentOrders: [],
|
||||
recentHistory: [],
|
||||
orderFailures: [],
|
||||
operationalEvents: [],
|
||||
@ -1045,6 +1061,7 @@ export class ApiServer {
|
||||
profiles: Array.isArray(record.profiles) ? record.profiles : [],
|
||||
runtime: {
|
||||
positions: Array.isArray(runtime.positions) ? runtime.positions : [],
|
||||
signalContexts: Array.isArray(runtime.signalContexts) ? runtime.signalContexts : [],
|
||||
recentOrders: Array.isArray(runtime.recentOrders) ? runtime.recentOrders : [],
|
||||
recentHistory: Array.isArray(runtime.recentHistory) ? runtime.recentHistory : [],
|
||||
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 {
|
||||
const health = chatContext.runtime.health || {};
|
||||
const mismatchCount = Number(health.reconciliationMismatchCount || 0);
|
||||
@ -1177,6 +1218,7 @@ export class ApiServer {
|
||||
private buildLocalChatFallback(message: string, context: ChatRequestContext): ChatResponsePayload {
|
||||
const lower = String(message || '').toLowerCase();
|
||||
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 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)
|
||||
@ -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) {
|
||||
return this.buildPositionExplanation(message, context)
|
||||
|| {
|
||||
@ -2940,7 +2991,7 @@ AVAILABLE RULES (use these exact ruleId values):
|
||||
|
||||
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": {
|
||||
"name": string,
|
||||
"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.
|
||||
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.
|
||||
5. For "explain_blocker": explain the most relevant trade/order/operational blocker from runtime context. No profile needed.
|
||||
6. For "summarize_reconciliation": summarize reconciliation health, mismatches, quarantines, and operational implications. No profile needed.
|
||||
7. Only include "profile" when the action is create_profile or update_profile.
|
||||
8. If runtime context is relevant, prefer concrete symbols, profile names, counts, and current state over generic advice.
|
||||
9. Match the user's risk appetite: "conservative" = low risk (0.5-1%), low capital. "aggressive" = higher risk (2-5%), more rules enabled.
|
||||
10. Always include at least TrendBiasRule and RiskManagementRule as enabled for safety.
|
||||
11. Output ONLY valid JSON. No markdown, no backticks, no explanation outside the JSON.`;
|
||||
5. For "explain_waiting": explain why a profile/symbol has not entered yet using signal/rule/execution context. No profile needed.
|
||||
6. For "explain_blocker": explain the most relevant trade/order/operational blocker from runtime context. No profile needed.
|
||||
7. For "summarize_reconciliation": summarize reconciliation health, mismatches, quarantines, and operational implications. No profile needed.
|
||||
8. Only include "profile" when the action is create_profile or update_profile.
|
||||
9. If runtime context is relevant, prefer concrete symbols, profile names, counts, and current state over generic advice.
|
||||
10. Match the user's risk appetite: "conservative" = low risk (0.5-1%), low capital. "aggressive" = higher risk (2-5%), more rules enabled.
|
||||
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 {
|
||||
let aiResponse: string | null = null;
|
||||
@ -3016,6 +3068,7 @@ RULES:
|
||||
'update_profile',
|
||||
'explain',
|
||||
'explain_position',
|
||||
'explain_waiting',
|
||||
'explain_blocker',
|
||||
'summarize_reconciliation'
|
||||
]);
|
||||
|
||||
@ -22,6 +22,32 @@ const profilesFixture = [
|
||||
|
||||
const botStateFixture: BotState = {
|
||||
...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: [
|
||||
{
|
||||
id: 'pos-1',
|
||||
@ -219,5 +245,14 @@ describe('ChatControl DOM flow', () => {
|
||||
reconciliationNoGoTrades: 1,
|
||||
})
|
||||
);
|
||||
expect(body.context.runtime.signalContexts).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
symbol: 'BTC/USDT',
|
||||
executionCode: 'rule_ratio_not_met',
|
||||
executionStatus: 'SKIPPED',
|
||||
})
|
||||
])
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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: '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: '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: '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?' },
|
||||
@ -103,6 +104,22 @@ const isProfileMutationAction = (action?: ChatAssistantAction): action is 'creat
|
||||
action === 'create_profile' || action === 'update_profile';
|
||||
|
||||
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) => ({
|
||||
symbol: position.symbol,
|
||||
side: position.side,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user