Skip to main content

Overview

Deltalytix leverages OpenAI’s GPT models for intelligent trade data processing, analysis, and interactive chat assistance. The integration powers CSV field mapping, trade formatting, and a conversational trading assistant.

Configuration

Environment Variables

Add your OpenAI API key to .env:
# OpenAI Configuration
OPENAI_API_KEY='sk-proj-...'
Never expose your OpenAI API key in client-side code. Always make API calls from server-side routes.

SDK Setup

Deltalytix uses the Vercel AI SDK with OpenAI provider:
import { openai } from "@ai-sdk/openai";
import { streamText, streamObject } from "ai";

AI Field Mapping

Overview

The AI field mapper (app/api/ai/mappings/route.ts) automatically detects CSV column mappings for trade data import.

Endpoint Configuration

export const maxDuration = 30; // Allow up to 30 seconds for processing

export async function POST(req: NextRequest) {
  const body = await req.json();
  const { fieldColumns, firstRows } = body;

  if (!fieldColumns || !firstRows) {
    return new Response(
      JSON.stringify({ error: "Missing required fields" }),
      { status: 400 }
    );
  }

  const result = streamText({
    model: "openai/gpt-5-mini",
    output: Output.object({ schema: mappingSchema }),
    prompt: buildMappingPrompt(fieldColumns, firstRows),
  });

  return result.toTextStreamResponse();
}

Mapping Schema

The mapping schema defines expected trade fields:
import { z } from 'zod';

const mappingSchema = z.object({
  accountNumber: z.string().nullable(),
  instrument: z.string().nullable(),
  entryId: z.string().nullable(),
  closeId: z.string().nullable(),
  quantity: z.string().nullable(),
  entryPrice: z.string().nullable(),
  closePrice: z.string().nullable(),
  entryDate: z.string().nullable(),
  closeDate: z.string().nullable(),
  pnl: z.string().nullable(),
  timeInPosition: z.string().nullable(),
  side: z.string().nullable(),
  commission: z.string().nullable(),
});

Intelligent Mapping Prompt

The AI analyzes both column names and data patterns:
const prompt = `
You are a trading data expert. Analyze the CSV columns and their data patterns 
to map them to the correct database fields.

Look at BOTH the column names AND the actual data values to make intelligent mappings.

Available database fields:
- accountNumber: Account identifier (numbers, letters, or alphanumeric)
- instrument: Trading symbol/ticker (e.g., EURNZD, BTCUSD, ES)
- entryId: Unique buy transaction ID
- closeId: Unique sell transaction ID
- quantity: Number of units traded (decimal numbers)
- entryPrice: Buy/entry price (decimal numbers)
- closePrice: Sell/exit price (decimal numbers)
- entryDate: Entry/buy date (e.g., "2025-09-12 09:41:09")
- closeDate: Exit/sell date (e.g., "2025-09-18 02:12:02")
- pnl: Profit/loss amount (decimal numbers, can be negative)
- timeInPosition: Duration in seconds (numeric values)
- side: Trade direction ("buy", "sell", "long", "short")
- commission: Trading fees (decimal numbers)

CRITICAL: Analyze column CONTEXT and ORDER:
- Column order matters: entryDate → entryPrice → closeDate → closePrice is typical
- If duplicate column names exist (like "Prix"), use POSITION to distinguish:
  * First "Prix" after entryDate = entryPrice
  * Second "Prix" after closeDate = closePrice
- Look for logical sequences: Date → Price → Date → Price

IMPORTANT: For duplicate column names, include the column position (1-based index).
Format: "ColumnName_Position" (e.g., "Prix_1", "Prix_2")

Column order and context:
${fieldColumns.map((col, index) => `${index + 1}. ${col}`).join('\n')}

Sample data (first few rows):
${firstRows.map((row, index) => 
  `Row ${index + 1}: ${Object.entries(row)
    .map(([col, val]) => `${col}: "${val}"`)
    .join(", ")}`
).join('\n')}
`;
The AI considers column position and data patterns, making it highly accurate even with ambiguous column names or duplicate headers.

Usage Example

const response = await fetch('/api/ai/mappings', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    fieldColumns: ['Symbol', 'Buy Date', 'Buy Price', 'Sell Date', 'Sell Price', 'P&L'],
    firstRows: [
      {
        'Symbol': 'BTCUSD',
        'Buy Date': '2025-01-15 09:30:00',
        'Buy Price': '45000.50',
        'Sell Date': '2025-01-15 15:30:00',
        'Sell Price': '46500.00',
        'P&L': '1499.50'
      }
    ]
  })
});

const mapping = await response.json();
// Returns: { instrument: 'Symbol', entryDate: 'Buy Date', ... }

Trade Formatting

Overview

The trade formatter (app/api/ai/format-trades/route.ts) converts raw CSV data into structured trade objects.

Endpoint Configuration

export const maxDuration = 30;

export async function POST(req: NextRequest) {
  const body = await req.json();
  const { headers, rows } = requestSchema.parse(body);

  const result = streamObject({
    model: openai("gpt-4o-mini-2024-07-18"),
    schema: tradeSchema,
    output: 'array',
    system: buildFormattingSystemPrompt(),
    prompt: `Format the following ${rows.length} trades data.\nHeaders: ${headers.join(", ")}\nRows:\n${rows.map(row => row.join(", ")).join("\n")}`,
    temperature: 0.1,
  });

  return result.toTextStreamResponse();
}

Formatting Rules

The AI applies intelligent formatting rules:
const systemPrompt = `
You are a trading expert. Format trade data according to the schema.

Rules for formatting:

1. Instrument names - Apply these transformations:
  - CFD Instruments (crypto, forex, commodities): KEEP FULL NAMES
    * BTCUSD → BTCUSD (NOT BTC)
    * XAUUSD → XAUUSD (NOT XAU)
    * EURNZD → EURNZD (NOT EUR)
  - Futures with .cash suffix: REMOVE .cash suffix
    * US100.cash → US100
    * USOIL.cash → USOIL
  - Futures contracts with month/year codes: TRIM to base symbol
    * ESZ5 → ES
    * NQZ5 → NQ
  - Continuous contracts with .c suffix: REMOVE .c suffix
    * SOYBEAN.c → SOYBEAN
  - Stocks and other instruments: KEEP AS-IS
    * AAPL → AAPL

2. Convert all numeric values to numbers (remove currency symbols, commas)

3. Convert dates to ISO strings

4. Determine trade side based on:
  - If side is provided: normalize 'buy'/'long'/'b' to 'long', 
    'sell'/'short'/'s' to 'short'
  - If not provided: determine from entry/close dates and prices

5. Convert time in position to seconds

6. PnL (Profit/Loss) mapping - CRITICAL:
  - Use the "Profit" column for PnL values, NOT "Pips"
  - PnL should be the actual monetary profit/loss amount
  - Do NOT calculate or estimate PnL - use only provided data

7. Handle missing values appropriately:
  - If a required field is missing, omit it rather than making up values
  - Only populate fields that have actual data in the input

8. Required fields (only if data is available):
  - entryPrice, closePrice, commission, quantity, pnl
  - side, entryDate, closeDate
  - instrument, accountNumber
`;

Trade Schema

const tradeSchema = z.object({
  accountNumber: z.string(),
  instrument: z.string(),
  entryId: z.string().optional(),
  closeId: z.string().optional(),
  quantity: z.number(),
  entryPrice: z.string(),
  closePrice: z.string(),
  entryDate: z.string(), // ISO format
  closeDate: z.string(), // ISO format
  pnl: z.number(),
  timeInPosition: z.number().optional(), // seconds
  side: z.enum(['long', 'short']),
  commission: z.number().default(0),
});

AI Chat Assistant

Overview

The AI chat assistant (app/api/ai/chat/route.ts) provides conversational analysis of trading performance.

Endpoint Configuration

export const maxDuration = 60; // Longer timeout for complex queries

export async function POST(req: NextRequest) {
  const { messages, username, locale, timezone } = await req.json();

  const convertedMessages = await convertToModelMessages(messages);
  const systemPrompt = buildSystemPrompt({
    locale,
    username,
    timezone,
    currentWeekStart,
    currentWeekEnd,
    isFirstMessage,
  });

  const result = streamText({
    model: 'openai/gpt-5-mini',
    messages: convertedMessages,
    system: systemPrompt,
    stopWhen: stepCountIs(10),
    tools: {
      getJournalEntries,
      getMostTradedInstruments,
      getLastTradesData,
      getTradesDetails,
      getTradesSummary,
      getCurrentWeekSummary,
      getPreviousWeekSummary,
      getWeekSummaryForDate,
      getFinancialNews,
      generateEquityChart,
    },
  });

  return result.toUIMessageStreamResponse();
}

Available Tools

The chat assistant has access to these tools:
Retrieves user’s journal entries for analysis and reflection.
const journalEntries = await getJournalEntries({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
});
Returns the most frequently traded instruments by the user.
const instruments = await getMostTradedInstruments({ limit: 10 });
Provides aggregate trading statistics for a date range.
const summary = await getTradesSummary({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
});
Gets trading performance for the current week.
const weekSummary = await getCurrentWeekSummary();
Creates equity curve visualization data.
const chartData = await generateEquityChart({
  startDate: '2025-01-01',
  endDate: '2025-01-31',
});

System Prompt Structure

The system prompt is built dynamically based on user context:
function buildSystemPrompt({
  locale,
  username,
  timezone,
  currentWeekStart,
  currentWeekEnd,
  isFirstMessage,
}) {
  return `
You are an expert trading coach and analyst. Your role is to help traders 
improve their performance through insightful analysis and guidance.

User Context:
- Name: ${username}
- Locale: ${locale}
- Timezone: ${timezone}
- Current Week: ${currentWeekStart} to ${currentWeekEnd}

${isFirstMessage ? `
This is the user's first message. Start with a warm greeting and offer to 
analyze their recent trading performance.
` : ''}

Capabilities:
- Analyze trading patterns and performance
- Identify strengths and areas for improvement
- Provide actionable feedback
- Generate visualizations and charts
- Access journal entries and trade history

Communication Style:
- Professional yet approachable
- Data-driven insights
- Actionable recommendations
- Encouraging and supportive
  `;
}

Usage Example

const response = await fetch('/api/ai/chat', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    messages: [
      { role: 'user', content: 'How did I perform this week?' }
    ],
    username: 'John',
    locale: 'en',
    timezone: 'America/New_York',
  })
});

const reader = response.body.getReader();
// Stream the AI response

Cost Optimization

Model Selection

For field mapping and formatting, use the cost-effective mini model:
model: openai("gpt-4o-mini-2024-07-18")
For chat and advanced reasoning, use the more capable model:
model: 'openai/gpt-5-mini'
Use low temperature (0.1) for structured data formatting:
temperature: 0.1 // More deterministic

Request Optimization

// Limit the number of rows processed
const requestSchema = z.object({
  headers: z.array(z.string()),
  rows: z.array(z.array(z.string())).max(100, "Too many rows to process")
});

// Set appropriate timeouts
export const maxDuration = 30; // 30 seconds for most operations

// Use streaming for real-time feedback
return result.toTextStreamResponse();

Caching Strategies

// Cache field mappings for similar CSV structures
const cachedMapping = await redis.get(`mapping:${csvHash}`);
if (cachedMapping) return cachedMapping;

// Cache formatted trades to avoid reprocessing
const cachedTrades = await redis.get(`trades:${dataHash}`);
if (cachedTrades) return cachedTrades;

Error Handling

Rate Limiting

try {
  const result = await streamText({ model, prompt });
  return result.toTextStreamResponse();
} catch (error) {
  if (error.status === 429) {
    return new Response(
      JSON.stringify({ error: "Rate limit exceeded. Please try again later." }),
      { status: 429 }
    );
  }
  throw error;
}

Validation Errors

try {
  const { headers, rows } = requestSchema.parse(body);
} catch (error) {
  if (error instanceof z.ZodError) {
    return new Response(
      JSON.stringify({ error: error.errors }),
      { status: 400 }
    );
  }
}

Timeout Handling

// Set appropriate timeouts per endpoint
export const maxDuration = 30; // Field mapping
export const maxDuration = 60; // Chat assistant

// Handle timeout errors
try {
  const result = await streamText({ model, prompt });
} catch (error) {
  if (error.name === 'TimeoutError') {
    return new Response(
      JSON.stringify({ error: "Request timeout. Please try with less data." }),
      { status: 504 }
    );
  }
}

Security Best Practices

  • Store API key in environment variables only
  • Never expose in client-side code
  • Rotate keys periodically
  • Use separate keys for development and production
  • Validate all inputs with Zod schemas
  • Limit request sizes to prevent abuse
  • Sanitize user-provided data
  • Implement per-user rate limits
  • Use Redis for distributed rate limiting
  • Return 429 status for exceeded limits
  • Require authentication for all AI endpoints
  • Verify user identity server-side
  • Check user subscription status

Monitoring

Token Usage Tracking

const result = streamText({
  model: 'openai/gpt-5-mini',
  prompt,
  onFinish: (result) => {
    console.log('Token usage:', result.usage);
    // Log to analytics service
    analytics.track('ai_request', {
      tokens: result.usage.totalTokens,
      cost: calculateCost(result.usage),
    });
  },
});

Error Logging

const result = streamText({
  model: 'openai/gpt-5-mini',
  prompt,
  onError: (error) => {
    console.error('AI request failed:', error);
    // Report to error tracking service
    Sentry.captureException(error, {
      tags: { service: 'openai', endpoint: 'chat' },
    });
  },
});

Additional Resources

OpenAI API Reference

Official OpenAI API documentation

Vercel AI SDK

Complete guide to Vercel AI SDK

OpenAI Pricing

Token pricing and cost calculator

Best Practices

OpenAI safety and best practices

Build docs developers (and LLMs) love