173 lines
8.7 KiB
TypeScript
173 lines
8.7 KiB
TypeScript
import type { ChatMode } from '@/types/chat'
|
|
|
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000'
|
|
const API_ENDPOINT = import.meta.env.VITE_API_ENDPOINT || '/chat/stream'
|
|
const USE_MOCK_DATA = false // Set to true to use mock data for testing
|
|
|
|
// Session management
|
|
export const getChatSessionId = (): string | null => {
|
|
return localStorage.getItem('tyndale_session_id')
|
|
}
|
|
|
|
export const setChatSessionId = (sessionId: string): void => {
|
|
localStorage.setItem('tyndale_session_id', sessionId)
|
|
}
|
|
|
|
export const clearChatSessionId = (): void => {
|
|
localStorage.removeItem('tyndale_session_id')
|
|
}
|
|
|
|
// Mock responses for testing
|
|
const MOCK_RESPONSES = {
|
|
general: [
|
|
"A **moving average** is one of the most fundamental technical indicators in trading. It smooths out price data by creating a constantly updated average price over a specific time period.\n\nThere are two main types:\n- **Simple Moving Average (SMA)**: The arithmetic mean of prices over a period\n- **Exponential Moving Average (EMA)**: Gives more weight to recent prices\n\nTraders use moving averages to identify trends, support/resistance levels, and generate trading signals when different MAs cross each other.",
|
|
"**Algorithmic trading** uses computer programs to execute trades based on predefined criteria. The main advantages include:\n\n1. **Speed**: Algorithms can analyze market conditions and execute orders in milliseconds\n2. **Emotion-free**: Removes psychological biases from trading decisions\n3. **Backtesting**: Strategies can be tested on historical data before risking real capital\n4. **Consistency**: Executes trades exactly as programmed\n\nCommon strategies include market making, arbitrage, trend following, and mean reversion.",
|
|
"**Risk management** is crucial in trading. Key principles include:\n\n- **Position sizing**: Never risk more than 1-2% of capital on a single trade\n- **Stop losses**: Set predetermined exit points to limit losses\n- **Diversification**: Spread risk across multiple assets or strategies\n- **Risk-reward ratio**: Aim for at least 2:1 reward-to-risk\n\nThe goal is to preserve capital during losing streaks while maximizing gains during winning periods.",
|
|
"**Market volatility** refers to the rate and magnitude of price changes in a financial instrument. High volatility means larger price swings, while low volatility indicates stability.\n\nTraders measure volatility using:\n- **Standard deviation** of returns\n- **Average True Range (ATR)**\n- **Bollinger Bands**\n- **VIX index** (for equity markets)\n\nVolatility creates both opportunities and risks - more movement means more profit potential but also higher risk.",
|
|
],
|
|
strategy: [
|
|
"When analyzing a **mean reversion strategy**, consider these key metrics:\n\n**Performance Metrics:**\n- Sharpe Ratio: Risk-adjusted returns (target > 1.5)\n- Maximum Drawdown: Largest peak-to-trough decline (keep < 20%)\n- Win Rate: Percentage of profitable trades\n- Profit Factor: Gross profit / Gross loss (target > 1.5)\n\n**Strategy-Specific:**\n- Mean reversion typically works best in range-bound markets\n- Watch for regime changes where markets shift from mean-reverting to trending\n- Consider transaction costs - they can significantly impact high-frequency mean reversion strategies",
|
|
"**Backtesting results** should be interpreted carefully to avoid overfitting:\n\n**Red flags:**\n- Too many parameters (> 5-6 tunable variables)\n- Perfect or near-perfect equity curve\n- Performance degrades significantly on out-of-sample data\n- Very high Sharpe ratio (> 3) - often indicates curve fitting\n\n**Best practices:**\n- Use walk-forward analysis\n- Test on multiple market regimes\n- Include realistic transaction costs and slippage\n- Reserve 30-40% of data for out-of-sample testing\n\nRemember: Past performance does not guarantee future results.",
|
|
"For **momentum strategies**, optimization should focus on:\n\n**Entry signals:**\n- Lookback period for momentum calculation (20-250 days typical)\n- Threshold for signal generation (% gain required)\n- Volume confirmation requirements\n\n**Risk management:**\n- Trailing stops to protect profits\n- Maximum holding period\n- Correlation filters to avoid overcrowding\n\n**Portfolio construction:**\n- Number of positions (5-20 for diversification)\n- Position sizing (equal weight vs volatility-adjusted)\n- Rebalancing frequency (weekly to monthly)\n\nMomentum works well in trending markets but can suffer during reversals.",
|
|
"**Strategy robustness** can be evaluated through:\n\n1. **Parameter sensitivity**: Test nearby parameter values - robust strategies shouldn't degrade rapidly\n2. **Market regime analysis**: Performance across bull/bear/sideways markets\n3. **Asset class generalization**: Does it work on similar instruments?\n4. **Time frame stability**: Consistent performance across different time periods\n5. **Monte Carlo simulation**: Randomize trade sequences to assess statistical significance\n\nA robust strategy maintains profitability across reasonable parameter ranges and different market conditions.",
|
|
],
|
|
}
|
|
|
|
// Mock streaming function
|
|
async function* mockChatStream(
|
|
_question: string,
|
|
mode: ChatMode
|
|
): AsyncGenerator<StreamEvent> {
|
|
const sessionId = crypto.randomUUID()
|
|
yield { type: 'session_id', data: sessionId }
|
|
|
|
// Select a random response based on mode
|
|
const responses = MOCK_RESPONSES[mode]
|
|
const response = responses[Math.floor(Math.random() * responses.length)]
|
|
|
|
// Simulate streaming by yielding words with delays
|
|
const words = response.split(' ')
|
|
for (let i = 0; i < words.length; i++) {
|
|
// Add space before word (except first word)
|
|
const chunk = i === 0 ? words[i] : ' ' + words[i]
|
|
yield { type: 'chunk', data: chunk }
|
|
|
|
// Random delay between 30-80ms to simulate typing
|
|
await new Promise(resolve => setTimeout(resolve, Math.random() * 50 + 30))
|
|
}
|
|
|
|
yield { type: 'done', data: null }
|
|
}
|
|
|
|
// SSE stream event types
|
|
export type StreamEvent =
|
|
| { type: 'chunk'; data: string }
|
|
| { type: 'done'; data: null }
|
|
| { type: 'error'; data: string }
|
|
| { type: 'session_id'; data: string }
|
|
|
|
class ApiClient {
|
|
private sessionId: string | null = null
|
|
|
|
getSessionId(): string | null {
|
|
return this.sessionId || getChatSessionId()
|
|
}
|
|
|
|
setSessionId(sessionId: string): void {
|
|
this.sessionId = sessionId
|
|
setChatSessionId(sessionId)
|
|
}
|
|
|
|
clearSessionId(): void {
|
|
this.sessionId = null
|
|
clearChatSessionId()
|
|
}
|
|
|
|
// Streaming chat endpoint (SSE)
|
|
async *chatStream(
|
|
question: string,
|
|
mode: ChatMode,
|
|
sessionId?: string
|
|
): AsyncGenerator<StreamEvent> {
|
|
// Use mock data if enabled
|
|
if (USE_MOCK_DATA) {
|
|
yield* mockChatStream(question, mode)
|
|
return
|
|
}
|
|
|
|
// Generate or reuse session ID
|
|
const actualSessionId = sessionId || this.getSessionId() || crypto.randomUUID()
|
|
this.setSessionId(actualSessionId)
|
|
|
|
// Yield session ID first
|
|
yield { type: 'session_id', data: actualSessionId }
|
|
|
|
try {
|
|
const response = await fetch(`${API_BASE_URL}${API_ENDPOINT}`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
message: question,
|
|
conversation_id: actualSessionId,
|
|
}),
|
|
})
|
|
|
|
if (!response.ok) {
|
|
yield { type: 'error', data: `HTTP error! status: ${response.status}` }
|
|
return
|
|
}
|
|
|
|
if (!response.body) {
|
|
yield { type: 'error', data: 'Response body is null' }
|
|
return
|
|
}
|
|
|
|
const reader = response.body.getReader()
|
|
const decoder = new TextDecoder()
|
|
let buffer = ''
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read()
|
|
|
|
if (done) break
|
|
|
|
buffer += decoder.decode(value, { stream: true })
|
|
const lines = buffer.split('\n')
|
|
buffer = lines.pop() || ''
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith('data: ')) {
|
|
const data = line.slice(6)
|
|
|
|
try {
|
|
const parsed = JSON.parse(data)
|
|
if (parsed.type === 'chunk' && parsed.content) {
|
|
yield { type: 'chunk', data: parsed.content }
|
|
} else if (parsed.type === 'done') {
|
|
yield { type: 'done', data: null }
|
|
return
|
|
} else if (parsed.type === 'error') {
|
|
yield { type: 'error', data: parsed.message || 'Unknown error' }
|
|
return
|
|
}
|
|
} catch {
|
|
// Skip non-JSON lines
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
yield { type: 'done', data: null }
|
|
} catch (error) {
|
|
yield {
|
|
type: 'error',
|
|
data: error instanceof Error ? error.message : 'Network error',
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export const apiClient = new ApiClient()
|