feat(chat): explain waiting trade signals
This commit is contained in:
parent
f3dfe31d1f
commit
3f73310b4e
@ -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[];
|
||||||
@ -1027,6 +1042,7 @@ export class ApiServer {
|
|||||||
profiles: context,
|
profiles: context,
|
||||||
runtime: {
|
runtime: {
|
||||||
positions: [],
|
positions: [],
|
||||||
|
signalContexts: [],
|
||||||
recentOrders: [],
|
recentOrders: [],
|
||||||
recentHistory: [],
|
recentHistory: [],
|
||||||
orderFailures: [],
|
orderFailures: [],
|
||||||
@ -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'
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -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',
|
||||||
|
})
|
||||||
|
])
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user