feat(settings): add settings route and API updates\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

This commit is contained in:
2026-05-16 20:19:35 +02:00
parent 9b63d981b0
commit 0ee89cf052
38 changed files with 1426 additions and 562 deletions
@@ -0,0 +1,67 @@
import { describe, it, expect, vi } from "vitest";
import { Trader } from "../trader";
import type { AnalystReport, DebateRound } from "../../types/agents";
const mockReports: AnalystReport[] = [
{
analyst: "fundamentals",
report: "Strong earnings growth",
signal: {
agent: "fundamentals",
signal: "bullish",
confidence: 0.8,
reasoning: "Revenue up 20%",
timestamp: "2024-01-01",
},
},
];
const mockDebates: DebateRound[] = [
{
bullishView: "Strong fundamentals",
bearishView: "Market volatility",
researcher: "bullish",
},
];
describe("Trader executionPlan parsing", () => {
it("includes executionPlan for buy decisions", async () => {
const mockBuyClient = {
createChatCompletion: vi.fn().mockResolvedValue({
choices: [{ message: { content: JSON.stringify({
action: "buy",
confidence: 0.8,
reasoning: "Enter position",
executionPlan: { amount: 10, stopLoss: 95, riskManagement: { maxLossPercent: 1 }, takeProfit: 110 }
}) } }]
}),
};
const trader = new Trader(mockBuyClient as any);
const decision = await trader.decide("AAPL", mockReports, mockDebates);
expect(decision.action).toBe("buy");
expect(decision.executionPlan).toBeDefined();
expect(decision.executionPlan?.amount).toBe(10);
expect(decision.executionPlan?.stopLoss).toBe(95);
});
it("parses stopLoss from malformed executionPlan text (fallback)", async () => {
const malformed = 'Model reply: "action": "sell", "executionPlan": { amount: 7, takeProfit: 120, stopLoss: 115, riskManagement: { maxLossPercent: 2 } } and commentary.';
const mockMalformedClient = {
createChatCompletion: vi.fn().mockResolvedValue({
choices: [{ message: { content: malformed } }],
}),
};
const trader = new Trader(mockMalformedClient as any);
const decision = await trader.decide("AAPL", mockReports, mockDebates);
// action may be unspecified in this malformed reply; ensure executionPlan fields parsed when present
expect(decision.executionPlan).toBeDefined();
expect(decision.executionPlan?.amount).toBe(7);
expect(decision.executionPlan?.stopLoss).toBe(115);
expect(decision.executionPlan?.takeProfit).toBe(120);
expect(decision.executionPlan?.riskManagement?.maxLossPercent).toBe(2);
});
});
@@ -1,3 +1,5 @@
/* TRADINGGRAPH related file */
import { describe, it, expect, vi } from "vitest";
import { TradingGraph } from "../tradingGraph";
@@ -28,3 +30,4 @@ describe("TradingGraph execution step", () => {
expect(decision.executionPlan?.amount).toBe(100);
});
});
+3 -1
View File
@@ -1,3 +1,5 @@
/* TRADINGGRAPH related file */
import { describe, it, expect, vi } from "vitest";
import { TradingGraph } from "../tradingGraph";
@@ -29,4 +31,4 @@ describe("TradingGraph", () => {
expect(decision).toHaveProperty("action");
expect(decision).toHaveProperty("confidence");
});
});
});
+13 -10
View File
@@ -42,16 +42,17 @@ Based on all the information above, make a trading decision. Respond with JSON c
- confidence: a number between 0 and 1
- reasoning: brief explanation
If the action is "sell", also include an "executionPlan" object with:
- amount: number (shares to sell)
If the action is "buy" or "sell", also include an "executionPlan" object with:
- amount: number (shares to trade)
- riskManagement: object (e.g., { maxLossPercent: 2 })
- takeProfit: number (target take-profit price)
- stopLoss: number (stop-loss price or absolute value)
Format your response as JSON with these fields.`;
const response = await this.client.createChatCompletion(
[
{ role: "system", content: "You are a trading agent that makes buy/sell/hold decisions and provides execution guidance when selling." },
{ role: "system", content: "You are a trading agent that makes buy/sell/hold decisions and provides execution guidance for buy and sell actions." },
{ role: "user", content: prompt },
],
this.model
@@ -86,13 +87,15 @@ Format your response as JSON with these fields.`;
executionPlan = JSON.parse(execMatch[1]);
} catch (err) {
// fallback: try to extract primitive fields
const amountMatch = content.match(/"amount"\s*:\s*([0-9.]+)/);
const takeProfitMatch = content.match(/"takeProfit"\s*:\s*([0-9.]+)/);
const maxLossMatch = content.match(/"maxLossPercent"\s*:\s*([0-9.]+)/);
const methodMatch = content.match(/"method"\s*:\s*"([^"]+)"/);
const amountMatch = content.match(/(?:"amount"|\bamount\b)\s*:\s*([0-9.]+)/);
const takeProfitMatch = content.match(/(?:"takeProfit"|\btakeProfit\b)\s*:\s*([0-9.]+)/);
const stopLossMatch = content.match(/(?:"stopLoss"|\bstopLoss\b)\s*:\s*([0-9.]+)/);
const maxLossMatch = content.match(/(?:"maxLossPercent"|\bmaxLossPercent\b)\s*:\s*([0-9.]+)/);
const methodMatch = content.match(/(?:"method"|\bmethod\b)\s*:\s*"([^"]+)"/);
executionPlan = {} as any;
if (amountMatch) executionPlan.amount = parseFloat(amountMatch[1]);
if (takeProfitMatch) executionPlan.takeProfit = parseFloat(takeProfitMatch[1]);
if (stopLossMatch) executionPlan.stopLoss = parseFloat(stopLossMatch[1]);
executionPlan.riskManagement = {};
if (maxLossMatch) executionPlan.riskManagement.maxLossPercent = parseFloat(maxLossMatch[1]);
if (methodMatch) executionPlan.riskManagement.method = methodMatch[1];
@@ -101,8 +104,8 @@ Format your response as JSON with these fields.`;
// Additional fallback: if executionPlan parsed but missing nested riskManagement fields, try to extract them
if (executionPlan && executionPlan.riskManagement == null) {
const maxLossMatch2 = content.match(/"maxLossPercent"\s*:\s*([0-9.]+)/);
const methodMatch2 = content.match(/"method"\s*:\s*"([^"]+)"/);
const maxLossMatch2 = content.match(/(?:"maxLossPercent"|\bmaxLossPercent\b)\s*:\s*([0-9.]+)/);
const methodMatch2 = content.match(/(?:"method"|\bmethod\b)\s*:\s*"([^"]+)"/);
if (maxLossMatch2 || methodMatch2) {
executionPlan.riskManagement = executionPlan.riskManagement || {};
if (maxLossMatch2) executionPlan.riskManagement.maxLossPercent = parseFloat(maxLossMatch2[1]);
@@ -118,7 +121,7 @@ Format your response as JSON with these fields.`;
debateRounds: debates,
};
if (action === 'sell' && executionPlan) {
if ((action === 'sell' || action === 'buy') && executionPlan) {
decision.executionPlan = executionPlan;
}
+13 -15
View File
@@ -1,3 +1,5 @@
/* TRADINGGRAPH related file */
import { OpenRouterClient } from "../lib/openrouter";
import { FundamentalsAnalyst } from "./fundamentals";
import { TechnicalAnalyst } from "./technical";
@@ -41,14 +43,14 @@ export class TradingGraph {
sentimentData: { headlines: string[]; source?: "news" | "social" | "stocktwits" };
}
): Promise<TradingDecision> {
console.log(`[TradingGraph] Starting analysis for ${ticker} with model ${this.model}`);
const reports = await this.runAnalysts(ticker, input);
const debates = await this.runDebate(ticker, reports);
const decision = await this.trader.decide(ticker, reports, debates);
console.log(`[TradingGraph] Analysis complete for ${ticker}`);
console.log(`[TradingGraph] Decision: ${decision.action} (confidence: ${decision.confidence})`);
// Build workflow steps for observability. Include an execution step when selling.
const steps: GraphStep[] = [
@@ -57,13 +59,13 @@ export class TradingGraph {
{ step: "trader", data: decision },
];
if (decision.action === 'sell' && decision.executionPlan) {
if (decision.executionPlan) {
steps.push({ step: "execution", data: decision.executionPlan });
console.log(`[TradingGraph] Execution plan: ${JSON.stringify(decision.executionPlan)}`);
}
// Log steps for debugging; external systems can be extended to consume GraphStep sequence.
console.log(`[TradingGraph] Workflow steps: ${JSON.stringify(steps)}`);
return decision;
}
@@ -76,7 +78,7 @@ export class TradingGraph {
sentimentData: { headlines: string[]; source?: "news" | "social" | "stocktwits" };
}
): Promise<AnalystReport[]> {
console.log(`[TradingGraph] Running analysts for ${ticker}...`);
const [fundamentals, technical, sentiment] = await Promise.all([
this.fundamentalsAnalyst.analyze(ticker, input.financialData),
@@ -84,24 +86,20 @@ export class TradingGraph {
this.sentimentAnalyst.analyze(ticker, input.sentimentData),
]);
console.log(`[TradingGraph] Analyst reports complete:`, {
fundamentals: fundamentals.signal,
technical: technical.signal,
sentiment: sentiment.signal,
});
return [fundamentals, technical, sentiment];
}
private async runDebate(ticker: string, reports: AnalystReport[]): Promise<DebateRound[]> {
console.log(`[TradingGraph] Running debate for ${ticker}...`);
const [bullish, bearish] = await Promise.all([
this.bullishResearcher.research(ticker, reports),
this.bearishResearcher.research(ticker, reports),
]);
console.log(`[TradingGraph] Debate complete`);
return [
{
@@ -111,4 +109,4 @@ export class TradingGraph {
},
];
}
}
}