Initial commit: Gold Trading Simulator with AI-powered analysis
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
import type { DashboardConfig, TabConfig, LayoutPreset } from '@/types';
|
||||
|
||||
const STORAGE_KEY = 'gold-trading-dashboard-config';
|
||||
|
||||
export const DEFAULT_TAB_CONFIGS: TabConfig[] = [
|
||||
{
|
||||
id: 'chart',
|
||||
label: 'Price Chart',
|
||||
visible: true,
|
||||
order: 0,
|
||||
size: 'large',
|
||||
position: 'left',
|
||||
pinned: true,
|
||||
customization: {
|
||||
autoRefresh: true,
|
||||
refreshRate: 60,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trade-controls',
|
||||
label: 'Trade Controls',
|
||||
visible: true,
|
||||
order: 1,
|
||||
size: 'medium',
|
||||
position: 'center',
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
id: 'portfolio',
|
||||
label: 'Portfolio',
|
||||
visible: true,
|
||||
order: 2,
|
||||
size: 'medium',
|
||||
position: 'center',
|
||||
},
|
||||
{
|
||||
id: 'risk-management',
|
||||
label: 'Risk Management',
|
||||
visible: true,
|
||||
order: 3,
|
||||
size: 'medium',
|
||||
position: 'center',
|
||||
},
|
||||
{
|
||||
id: 'ai-analysis',
|
||||
label: 'AI Analysis',
|
||||
visible: true,
|
||||
order: 4,
|
||||
size: 'large',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
id: 'news',
|
||||
label: 'News Feed',
|
||||
visible: true,
|
||||
order: 5,
|
||||
size: 'medium',
|
||||
position: 'right',
|
||||
customization: {
|
||||
autoRefresh: true,
|
||||
refreshRate: 300,
|
||||
filters: { sentiment: 'ALL' },
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'alerts',
|
||||
label: 'Alerts',
|
||||
visible: true,
|
||||
order: 6,
|
||||
size: 'medium',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
id: 'analytics',
|
||||
label: 'Advanced Analytics',
|
||||
visible: true,
|
||||
order: 7,
|
||||
size: 'full',
|
||||
position: 'bottom',
|
||||
},
|
||||
// New Daily Trading Tools
|
||||
{
|
||||
id: 'daily-checklist',
|
||||
label: 'Daily Checklist',
|
||||
visible: false,
|
||||
order: 8,
|
||||
size: 'medium',
|
||||
position: 'left',
|
||||
},
|
||||
{
|
||||
id: 'daily-plan',
|
||||
label: 'Trading Plan',
|
||||
visible: false,
|
||||
order: 9,
|
||||
size: 'medium',
|
||||
position: 'center',
|
||||
},
|
||||
{
|
||||
id: 'trading-journal',
|
||||
label: 'Trading Journal',
|
||||
visible: false,
|
||||
order: 10,
|
||||
size: 'large',
|
||||
position: 'right',
|
||||
},
|
||||
{
|
||||
id: 'market-summary',
|
||||
label: 'Market Brief',
|
||||
visible: false,
|
||||
order: 11,
|
||||
size: 'medium',
|
||||
position: 'left',
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_PRESETS: LayoutPreset[] = [
|
||||
{
|
||||
id: 'trading-focus',
|
||||
name: 'Trading Focus',
|
||||
description: 'Optimized for active trading with chart and controls prominent',
|
||||
mode: 'grid',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0 }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 1, visible: true }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 2, visible: true }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 3, visible: true }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 4, visible: false }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'small', order: 5, visible: true }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'small', order: 6, visible: true }, // alerts
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 7, visible: false }, // analytics
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'analysis-focus',
|
||||
name: 'Analysis Focus',
|
||||
description: 'Full-screen analytics and AI insights',
|
||||
mode: 'tabs',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0, visible: true }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'full', order: 1, visible: true }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 2, visible: true }, // analytics
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'full', order: 3, visible: true }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'full', order: 4, visible: true }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'full', order: 5, visible: true }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'full', order: 6, visible: false }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'full', order: 7, visible: false }, // alerts
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'news-focus',
|
||||
name: 'News Focus',
|
||||
description: 'News and market updates at the forefront',
|
||||
mode: 'split',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 0, position: 'left' }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'large', order: 1, position: 'right' }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 2, position: 'right' }, // alerts
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 3, position: 'left' }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'small', order: 4, position: 'left', visible: false },
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'small', order: 5, position: 'left', visible: false },
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'small', order: 6, position: 'left', visible: false },
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 7, visible: false }, // analytics
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'balanced',
|
||||
name: 'Balanced View',
|
||||
description: 'Equal emphasis on all components',
|
||||
mode: 'grid',
|
||||
tabs: DEFAULT_TAB_CONFIGS.map((tab, index) => ({ ...tab, order: index, visible: true })),
|
||||
},
|
||||
// Daily Trading Workflow Presets
|
||||
{
|
||||
id: 'morning-setup',
|
||||
name: '🌅 Morning Setup',
|
||||
description: 'Pre-market routine: checklist, plan, and market brief',
|
||||
mode: 'grid',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[11], size: 'large', order: 0, visible: true }, // market summary
|
||||
{ ...DEFAULT_TAB_CONFIGS[8], size: 'medium', order: 1, visible: true }, // checklist
|
||||
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 2, visible: true }, // trading plan
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 3, visible: true }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 4, visible: true }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 5, visible: true }, // alerts
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 6, visible: true }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'small', order: 7, visible: false }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'small', order: 8, visible: false }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'small', order: 9, visible: false }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 10, visible: false }, // analytics
|
||||
{ ...DEFAULT_TAB_CONFIGS[10], size: 'large', order: 11, visible: false }, // journal
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'active-trading',
|
||||
name: '📈 Active Trading',
|
||||
description: 'During market hours: chart, controls, and execution',
|
||||
mode: 'grid',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 0, visible: true, pinned: true }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 1, visible: true, pinned: true }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 2, visible: true }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 3, visible: true }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 4, visible: true }, // trading plan
|
||||
{ ...DEFAULT_TAB_CONFIGS[8], size: 'small', order: 5, visible: true }, // checklist
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 6, visible: true }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 7, visible: true }, // alerts
|
||||
{ ...DEFAULT_TAB_CONFIGS[10], size: 'medium', order: 8, visible: false }, // journal (quick access)
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 9, visible: false }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 10, visible: false }, // analytics
|
||||
{ ...DEFAULT_TAB_CONFIGS[11], size: 'medium', order: 11, visible: false }, // market summary
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'end-of-day-review',
|
||||
name: '🌙 End-of-Day Review',
|
||||
description: 'Post-market analysis: journal, analytics, and planning for tomorrow',
|
||||
mode: 'tabs',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[10], size: 'full', order: 0, visible: true }, // journal
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 1, visible: true }, // analytics
|
||||
{ ...DEFAULT_TAB_CONFIGS[8], size: 'full', order: 2, visible: true }, // checklist
|
||||
{ ...DEFAULT_TAB_CONFIGS[9], size: 'full', order: 3, visible: true }, // trading plan
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'full', order: 4, visible: true }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'full', order: 5, visible: true }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[11], size: 'full', order: 6, visible: false }, // market summary
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'full', order: 7, visible: false }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'full', order: 8, visible: false }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'full', order: 9, visible: false }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'full', order: 10, visible: false }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'full', order: 11, visible: false }, // alerts
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'daily-trader',
|
||||
name: '⚡ Complete Daily Trader',
|
||||
description: 'All daily trading tools visible for comprehensive workflow',
|
||||
mode: 'grid',
|
||||
tabs: [
|
||||
{ ...DEFAULT_TAB_CONFIGS[0], size: 'large', order: 0, visible: true, position: 'left' }, // chart
|
||||
{ ...DEFAULT_TAB_CONFIGS[11], size: 'medium', order: 1, visible: true, position: 'left' }, // market summary
|
||||
{ ...DEFAULT_TAB_CONFIGS[1], size: 'medium', order: 2, visible: true, position: 'center' }, // trade controls
|
||||
{ ...DEFAULT_TAB_CONFIGS[9], size: 'medium', order: 3, visible: true, position: 'center' }, // trading plan
|
||||
{ ...DEFAULT_TAB_CONFIGS[8], size: 'medium', order: 4, visible: true, position: 'center' }, // checklist
|
||||
{ ...DEFAULT_TAB_CONFIGS[2], size: 'medium', order: 5, visible: true, position: 'center' }, // portfolio
|
||||
{ ...DEFAULT_TAB_CONFIGS[5], size: 'medium', order: 6, visible: true, position: 'right' }, // news
|
||||
{ ...DEFAULT_TAB_CONFIGS[6], size: 'medium', order: 7, visible: true, position: 'right' }, // alerts
|
||||
{ ...DEFAULT_TAB_CONFIGS[10], size: 'large', order: 8, visible: true, position: 'right' }, // journal
|
||||
{ ...DEFAULT_TAB_CONFIGS[3], size: 'medium', order: 9, visible: false }, // risk
|
||||
{ ...DEFAULT_TAB_CONFIGS[4], size: 'medium', order: 10, visible: false }, // ai
|
||||
{ ...DEFAULT_TAB_CONFIGS[7], size: 'full', order: 11, visible: false }, // analytics
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const DEFAULT_DASHBOARD_CONFIG: DashboardConfig = {
|
||||
mode: 'grid',
|
||||
tabs: DEFAULT_TAB_CONFIGS,
|
||||
activePreset: 'balanced',
|
||||
customPresets: [],
|
||||
};
|
||||
|
||||
export function loadDashboardConfig(): DashboardConfig {
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const config = JSON.parse(stored);
|
||||
const storedTabs: TabConfig[] = config.tabs || [];
|
||||
const defaultTabsById = new Map(DEFAULT_TAB_CONFIGS.map((tab) => [tab.id, tab]));
|
||||
|
||||
// Merge stored tab settings with defaults so required tabs always exist
|
||||
const mergedTabs = DEFAULT_TAB_CONFIGS.map((defaultTab) => {
|
||||
const storedTab = storedTabs.find((tab) => tab.id === defaultTab.id);
|
||||
return storedTab ? { ...defaultTab, ...storedTab } : defaultTab;
|
||||
});
|
||||
|
||||
// Preserve any custom tabs user may have added that aren't in defaults
|
||||
const additionalTabs = storedTabs.filter((tab) => !defaultTabsById.has(tab.id));
|
||||
|
||||
return {
|
||||
...DEFAULT_DASHBOARD_CONFIG,
|
||||
...config,
|
||||
tabs: [...mergedTabs, ...additionalTabs],
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading dashboard config:', error);
|
||||
}
|
||||
return DEFAULT_DASHBOARD_CONFIG;
|
||||
}
|
||||
|
||||
export function saveDashboardConfig(config: DashboardConfig): void {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||
} catch (error) {
|
||||
console.error('Error saving dashboard config:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export function getPresetById(presetId: string): LayoutPreset | undefined {
|
||||
return DEFAULT_PRESETS.find((p) => p.id === presetId);
|
||||
}
|
||||
|
||||
export function applyPreset(config: DashboardConfig, presetId: string): DashboardConfig {
|
||||
const preset = getPresetById(presetId);
|
||||
if (!preset) {
|
||||
// Check custom presets
|
||||
const customPreset = config.customPresets.find((p) => p.id === presetId);
|
||||
if (customPreset) {
|
||||
return {
|
||||
...config,
|
||||
mode: customPreset.mode,
|
||||
tabs: customPreset.tabs,
|
||||
activePreset: presetId,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
return {
|
||||
...config,
|
||||
mode: preset.mode,
|
||||
tabs: preset.tabs,
|
||||
activePreset: presetId,
|
||||
};
|
||||
}
|
||||
|
||||
export function saveCustomPreset(
|
||||
config: DashboardConfig,
|
||||
name: string,
|
||||
description: string
|
||||
): DashboardConfig {
|
||||
const newPreset: LayoutPreset = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name,
|
||||
description,
|
||||
mode: config.mode,
|
||||
tabs: [...config.tabs],
|
||||
};
|
||||
|
||||
return {
|
||||
...config,
|
||||
customPresets: [...config.customPresets, newPreset],
|
||||
};
|
||||
}
|
||||
|
||||
export function resetToDefault(): DashboardConfig {
|
||||
return { ...DEFAULT_DASHBOARD_CONFIG };
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
import type { Trade, Portfolio } from '@/types';
|
||||
|
||||
// Export trades to CSV
|
||||
export const exportTradesToCSV = (trades: Trade[], portfolio: Portfolio): void => {
|
||||
const headers = [
|
||||
'ID',
|
||||
'Timestamp',
|
||||
'Action',
|
||||
'Quantity',
|
||||
'Price',
|
||||
'Total',
|
||||
'P&L',
|
||||
'Date/Time',
|
||||
];
|
||||
|
||||
const rows = trades.map((trade) => [
|
||||
trade.id,
|
||||
trade.timestamp,
|
||||
trade.action,
|
||||
trade.quantity.toFixed(4),
|
||||
trade.price.toFixed(2),
|
||||
trade.total.toFixed(2),
|
||||
trade.pnl?.toFixed(2) || 'N/A',
|
||||
new Date(trade.timestamp).toLocaleString(),
|
||||
]);
|
||||
|
||||
// Add summary row
|
||||
rows.push([]);
|
||||
rows.push(['Summary', '', '', '', '', '', '']);
|
||||
rows.push(['Total Trades', trades.length.toString(), '', '', '', '', '']);
|
||||
rows.push(['Initial Capital', portfolio.initialCapital.toFixed(2), '', '', '', '', '']);
|
||||
rows.push(['Current Value', portfolio.totalValue.toFixed(2), '', '', '', '', '']);
|
||||
rows.push(['Total P&L', portfolio.totalPnl.toFixed(2), '', '', '', '', '']);
|
||||
rows.push([
|
||||
'Total P&L %',
|
||||
portfolio.totalPnlPercent.toFixed(2) + '%',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
]);
|
||||
|
||||
const csvContent = [headers, ...rows].map((row) => row.join(',')).join('\n');
|
||||
|
||||
downloadFile(csvContent, 'gold_trades_export.csv', 'text/csv');
|
||||
};
|
||||
|
||||
// Export portfolio summary
|
||||
export const exportPortfolioSummary = (portfolio: Portfolio): void => {
|
||||
const summary = {
|
||||
exportDate: new Date().toISOString(),
|
||||
initialCapital: portfolio.initialCapital,
|
||||
currentCash: portfolio.cash,
|
||||
totalValue: portfolio.totalValue,
|
||||
totalPnL: portfolio.totalPnl,
|
||||
totalPnLPercent: portfolio.totalPnlPercent,
|
||||
totalTrades: portfolio.trades.length,
|
||||
position: portfolio.position
|
||||
? {
|
||||
symbol: portfolio.position.symbol,
|
||||
quantity: portfolio.position.quantity,
|
||||
avgPrice: portfolio.position.avgPrice,
|
||||
currentPrice: portfolio.position.currentPrice,
|
||||
unrealizedPnL: portfolio.position.unrealizedPnl,
|
||||
unrealizedPnLPercent: portfolio.position.unrealizedPnlPercent,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
|
||||
const jsonContent = JSON.stringify(summary, null, 2);
|
||||
downloadFile(jsonContent, 'portfolio_summary.json', 'application/json');
|
||||
};
|
||||
|
||||
// Export analytics report
|
||||
export const exportAnalyticsReport = (
|
||||
portfolio: Portfolio,
|
||||
analytics: any
|
||||
): void => {
|
||||
const report = `
|
||||
=================================================
|
||||
GOLD TRADING SIMULATOR - ANALYTICS REPORT
|
||||
=================================================
|
||||
Generated: ${new Date().toLocaleString()}
|
||||
|
||||
PORTFOLIO SUMMARY
|
||||
-------------------------------------------------
|
||||
Initial Capital: $${portfolio.initialCapital.toFixed(2)}
|
||||
Current Value: $${portfolio.totalValue.toFixed(2)}
|
||||
Total P&L: $${portfolio.totalPnl.toFixed(2)} (${portfolio.totalPnlPercent.toFixed(2)}%)
|
||||
Cash Available: $${portfolio.cash.toFixed(2)}
|
||||
|
||||
PERFORMANCE METRICS
|
||||
-------------------------------------------------
|
||||
Total Trades: ${analytics.totalTrades}
|
||||
Win Rate: ${analytics.winRate}%
|
||||
Winning Trades: ${analytics.winningTrades}
|
||||
Losing Trades: ${analytics.losingTrades}
|
||||
|
||||
Average Win: $${analytics.avgWin.toFixed(2)}
|
||||
Average Loss: $${analytics.avgLoss.toFixed(2)}
|
||||
Largest Win: $${analytics.largestWin.toFixed(2)}
|
||||
Largest Loss: $${Math.abs(analytics.largestLoss).toFixed(2)}
|
||||
|
||||
Profit Factor: ${analytics.profitFactor === Infinity ? '∞' : analytics.profitFactor.toFixed(2)}
|
||||
Risk/Reward Ratio: 1:${analytics.riskRewardRatio.toFixed(2)}
|
||||
Sharpe Ratio: ${analytics.sharpeRatio.toFixed(2)}
|
||||
Max Drawdown: ${analytics.maxDrawdown.toFixed(2)}%
|
||||
|
||||
${portfolio.position ? `
|
||||
CURRENT POSITION
|
||||
-------------------------------------------------
|
||||
Symbol: ${portfolio.position.symbol}
|
||||
Quantity: ${portfolio.position.quantity.toFixed(4)} oz
|
||||
Avg Entry Price: $${portfolio.position.avgPrice.toFixed(2)}
|
||||
Current Price: $${portfolio.position.currentPrice.toFixed(2)}
|
||||
Unrealized P&L: $${portfolio.position.unrealizedPnl.toFixed(2)} (${portfolio.position.unrealizedPnlPercent.toFixed(2)}%)
|
||||
` : ''}
|
||||
|
||||
=================================================
|
||||
END OF REPORT
|
||||
=================================================
|
||||
`.trim();
|
||||
|
||||
downloadFile(report, 'trading_analytics_report.txt', 'text/plain');
|
||||
};
|
||||
|
||||
// Helper function to download file
|
||||
const downloadFile = (content: string, filename: string, mimeType: string): void => {
|
||||
const blob = new Blob([content], { type: mimeType });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
// Copy to clipboard
|
||||
export const copyToClipboard = (text: string): Promise<void> => {
|
||||
return navigator.clipboard.writeText(text);
|
||||
};
|
||||
@@ -0,0 +1,431 @@
|
||||
import type { PriceData } from '@/types';
|
||||
|
||||
export const calculateSMA = (
|
||||
data: PriceData[],
|
||||
period: number
|
||||
): { time: number; value: number }[] => {
|
||||
const result: { time: number; value: number }[] = [];
|
||||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
let sum = 0;
|
||||
for (let j = 0; j < period; j++) {
|
||||
sum += data[i - j].close;
|
||||
}
|
||||
const sma = sum / period;
|
||||
result.push({
|
||||
time: data[i].time,
|
||||
value: Number(sma.toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const calculateEMA = (
|
||||
data: PriceData[],
|
||||
period: number
|
||||
): { time: number; value: number }[] => {
|
||||
const result: { time: number; value: number }[] = [];
|
||||
const multiplier = 2 / (period + 1);
|
||||
|
||||
if (data.length < period) return result;
|
||||
|
||||
// Calculate initial SMA for first EMA value
|
||||
let sum = 0;
|
||||
for (let i = 0; i < period; i++) {
|
||||
sum += data[i].close;
|
||||
}
|
||||
let ema = sum / period;
|
||||
result.push({ time: data[period - 1].time, value: Number(ema.toFixed(2)) });
|
||||
|
||||
// Calculate EMA for remaining data
|
||||
for (let i = period; i < data.length; i++) {
|
||||
ema = (data[i].close - ema) * multiplier + ema;
|
||||
result.push({ time: data[i].time, value: Number(ema.toFixed(2)) });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const calculateRSI = (
|
||||
data: PriceData[],
|
||||
period: number = 14
|
||||
): { time: number; value: number }[] => {
|
||||
const result: { time: number; value: number }[] = [];
|
||||
|
||||
if (data.length < period + 1) return result;
|
||||
|
||||
let gains = 0;
|
||||
let losses = 0;
|
||||
|
||||
// Calculate initial average gain and loss
|
||||
for (let i = 1; i <= period; i++) {
|
||||
const change = data[i].close - data[i - 1].close;
|
||||
if (change > 0) {
|
||||
gains += change;
|
||||
} else {
|
||||
losses += Math.abs(change);
|
||||
}
|
||||
}
|
||||
|
||||
let avgGain = gains / period;
|
||||
let avgLoss = losses / period;
|
||||
|
||||
for (let i = period; i < data.length; i++) {
|
||||
const change = data[i].close - data[i - 1].close;
|
||||
const gain = change > 0 ? change : 0;
|
||||
const loss = change < 0 ? Math.abs(change) : 0;
|
||||
|
||||
avgGain = (avgGain * (period - 1) + gain) / period;
|
||||
avgLoss = (avgLoss * (period - 1) + loss) / period;
|
||||
|
||||
const rs = avgLoss === 0 ? 100 : avgGain / avgLoss;
|
||||
const rsi = 100 - 100 / (1 + rs);
|
||||
|
||||
result.push({ time: data[i].time, value: Number(rsi.toFixed(2)) });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const formatPrice = (price: number): string => {
|
||||
return `$${price.toFixed(2)}`;
|
||||
};
|
||||
|
||||
export const formatPercent = (value: number): string => {
|
||||
const sign = value >= 0 ? '+' : '';
|
||||
return `${sign}${value.toFixed(2)}%`;
|
||||
};
|
||||
|
||||
export const formatNumber = (value: number): string => {
|
||||
return new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(value);
|
||||
};
|
||||
|
||||
// MACD (Moving Average Convergence Divergence)
|
||||
export const calculateMACD = (
|
||||
data: PriceData[],
|
||||
fastPeriod: number = 12,
|
||||
slowPeriod: number = 26,
|
||||
signalPeriod: number = 9
|
||||
): {
|
||||
macd: { time: number; value: number }[];
|
||||
signal: { time: number; value: number }[];
|
||||
histogram: { time: number; value: number }[];
|
||||
} => {
|
||||
const fastEMA = calculateEMA(data, fastPeriod);
|
||||
const slowEMA = calculateEMA(data, slowPeriod);
|
||||
|
||||
const macdLine: { time: number; value: number }[] = [];
|
||||
|
||||
// Calculate MACD line (fast EMA - slow EMA)
|
||||
const startIndex = slowPeriod - 1;
|
||||
for (let i = 0; i < fastEMA.length; i++) {
|
||||
if (i + startIndex < slowEMA.length) {
|
||||
macdLine.push({
|
||||
time: fastEMA[i + fastPeriod - slowPeriod].time,
|
||||
value: Number((fastEMA[i + fastPeriod - slowPeriod].value - slowEMA[i].value).toFixed(2)),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate signal line (EMA of MACD)
|
||||
const signalLine: { time: number; value: number }[] = [];
|
||||
if (macdLine.length >= signalPeriod) {
|
||||
const multiplier = 2 / (signalPeriod + 1);
|
||||
let ema = macdLine.slice(0, signalPeriod).reduce((sum, d) => sum + d.value, 0) / signalPeriod;
|
||||
signalLine.push({ time: macdLine[signalPeriod - 1].time, value: Number(ema.toFixed(2)) });
|
||||
|
||||
for (let i = signalPeriod; i < macdLine.length; i++) {
|
||||
ema = (macdLine[i].value - ema) * multiplier + ema;
|
||||
signalLine.push({ time: macdLine[i].time, value: Number(ema.toFixed(2)) });
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate histogram (MACD - Signal)
|
||||
const histogram: { time: number; value: number }[] = [];
|
||||
for (let i = 0; i < signalLine.length; i++) {
|
||||
const macdValue = macdLine[i + macdLine.length - signalLine.length].value;
|
||||
histogram.push({
|
||||
time: signalLine[i].time,
|
||||
value: Number((macdValue - signalLine[i].value).toFixed(2)),
|
||||
});
|
||||
}
|
||||
|
||||
return { macd: macdLine, signal: signalLine, histogram };
|
||||
};
|
||||
|
||||
// Bollinger Bands
|
||||
export const calculateBollingerBands = (
|
||||
data: PriceData[],
|
||||
period: number = 20,
|
||||
stdDev: number = 2
|
||||
): {
|
||||
upper: { time: number; value: number }[];
|
||||
middle: { time: number; value: number }[];
|
||||
lower: { time: number; value: number }[];
|
||||
} => {
|
||||
const middle = calculateSMA(data, period);
|
||||
const upper: { time: number; value: number }[] = [];
|
||||
const lower: { time: number; value: number }[] = [];
|
||||
|
||||
for (let i = period - 1; i < data.length; i++) {
|
||||
const slice = data.slice(i - period + 1, i + 1);
|
||||
const sma = slice.reduce((sum, d) => sum + d.close, 0) / period;
|
||||
const variance = slice.reduce((sum, d) => sum + Math.pow(d.close - sma, 2), 0) / period;
|
||||
const sd = Math.sqrt(variance);
|
||||
|
||||
const time = data[i].time;
|
||||
upper.push({ time, value: Number((sma + stdDev * sd).toFixed(2)) });
|
||||
lower.push({ time, value: Number((sma - stdDev * sd).toFixed(2)) });
|
||||
}
|
||||
|
||||
return { upper, middle, lower };
|
||||
};
|
||||
|
||||
// ATR (Average True Range)
|
||||
export const calculateATR = (
|
||||
data: PriceData[],
|
||||
period: number = 14
|
||||
): { time: number; value: number }[] => {
|
||||
const result: { time: number; value: number }[] = [];
|
||||
|
||||
if (data.length < period + 1) return result;
|
||||
|
||||
const trueRanges: number[] = [];
|
||||
|
||||
// Calculate True Range for each period
|
||||
for (let i = 1; i < data.length; i++) {
|
||||
const high = data[i].high;
|
||||
const low = data[i].low;
|
||||
const prevClose = data[i - 1].close;
|
||||
|
||||
const tr = Math.max(
|
||||
high - low,
|
||||
Math.abs(high - prevClose),
|
||||
Math.abs(low - prevClose)
|
||||
);
|
||||
|
||||
trueRanges.push(tr);
|
||||
}
|
||||
|
||||
// Calculate initial ATR (SMA of TR)
|
||||
let atr = trueRanges.slice(0, period).reduce((sum, tr) => sum + tr, 0) / period;
|
||||
result.push({ time: data[period].time, value: Number(atr.toFixed(2)) });
|
||||
|
||||
// Calculate remaining ATR using smoothing
|
||||
for (let i = period; i < trueRanges.length; i++) {
|
||||
atr = (atr * (period - 1) + trueRanges[i]) / period;
|
||||
result.push({ time: data[i + 1].time, value: Number(atr.toFixed(2)) });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Fibonacci Retracement Levels
|
||||
export const calculateFibonacci = (
|
||||
high: number,
|
||||
low: number
|
||||
): { level: string; price: number }[] => {
|
||||
const diff = high - low;
|
||||
const levels = [
|
||||
{ level: '0%', ratio: 0 },
|
||||
{ level: '23.6%', ratio: 0.236 },
|
||||
{ level: '38.2%', ratio: 0.382 },
|
||||
{ level: '50%', ratio: 0.5 },
|
||||
{ level: '61.8%', ratio: 0.618 },
|
||||
{ level: '78.6%', ratio: 0.786 },
|
||||
{ level: '100%', ratio: 1 },
|
||||
];
|
||||
|
||||
return levels.map((l) => ({
|
||||
level: l.level,
|
||||
price: Number((high - diff * l.ratio).toFixed(2)),
|
||||
}));
|
||||
};
|
||||
|
||||
// Stochastic Oscillator
|
||||
export const calculateStochastic = (
|
||||
data: PriceData[],
|
||||
kPeriod: number = 14,
|
||||
dPeriod: number = 3
|
||||
): {
|
||||
k: { time: number; value: number }[];
|
||||
d: { time: number; value: number }[];
|
||||
} => {
|
||||
const kLine: { time: number; value: number }[] = [];
|
||||
|
||||
for (let i = kPeriod - 1; i < data.length; i++) {
|
||||
const slice = data.slice(i - kPeriod + 1, i + 1);
|
||||
const high = Math.max(...slice.map((d) => d.high));
|
||||
const low = Math.min(...slice.map((d) => d.low));
|
||||
const close = data[i].close;
|
||||
|
||||
const k = low === high ? 50 : ((close - low) / (high - low)) * 100;
|
||||
kLine.push({ time: data[i].time, value: Number(k.toFixed(2)) });
|
||||
}
|
||||
|
||||
// Calculate %D (SMA of %K)
|
||||
const dLine: { time: number; value: number }[] = [];
|
||||
for (let i = dPeriod - 1; i < kLine.length; i++) {
|
||||
const sum = kLine.slice(i - dPeriod + 1, i + 1).reduce((s, d) => s + d.value, 0);
|
||||
dLine.push({ time: kLine[i].time, value: Number((sum / dPeriod).toFixed(2)) });
|
||||
}
|
||||
|
||||
return { k: kLine, d: dLine };
|
||||
};
|
||||
|
||||
// Pivot Points (Standard)
|
||||
export const calculatePivotPoints = (
|
||||
high: number,
|
||||
low: number,
|
||||
close: number
|
||||
): {
|
||||
pivot: number;
|
||||
r1: number;
|
||||
r2: number;
|
||||
r3: number;
|
||||
s1: number;
|
||||
s2: number;
|
||||
s3: number;
|
||||
} => {
|
||||
const pivot = (high + low + close) / 3;
|
||||
const r1 = 2 * pivot - low;
|
||||
const s1 = 2 * pivot - high;
|
||||
const r2 = pivot + (high - low);
|
||||
const s2 = pivot - (high - low);
|
||||
const r3 = high + 2 * (pivot - low);
|
||||
const s3 = low - 2 * (high - pivot);
|
||||
|
||||
return {
|
||||
pivot: Number(pivot.toFixed(2)),
|
||||
r1: Number(r1.toFixed(2)),
|
||||
r2: Number(r2.toFixed(2)),
|
||||
r3: Number(r3.toFixed(2)),
|
||||
s1: Number(s1.toFixed(2)),
|
||||
s2: Number(s2.toFixed(2)),
|
||||
s3: Number(s3.toFixed(2)),
|
||||
};
|
||||
};
|
||||
|
||||
// Volume Weighted Average Price (VWAP)
|
||||
export const calculateVWAP = (
|
||||
data: PriceData[]
|
||||
): { time: number; value: number }[] => {
|
||||
const result: { time: number; value: number }[] = [];
|
||||
let cumulativeTPV = 0; // Typical Price * Volume
|
||||
let cumulativeVolume = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const typicalPrice = (data[i].high + data[i].low + data[i].close) / 3;
|
||||
const volume = data[i].volume || 1; // Default to 1 if volume not available
|
||||
|
||||
cumulativeTPV += typicalPrice * volume;
|
||||
cumulativeVolume += volume;
|
||||
|
||||
const vwap = cumulativeTPV / cumulativeVolume;
|
||||
result.push({ time: data[i].time, value: Number(vwap.toFixed(2)) });
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
// Support and Resistance Detection
|
||||
export const findSupportResistance = (
|
||||
data: PriceData[],
|
||||
lookback: number = 20,
|
||||
threshold: number = 0.02 // 2% threshold
|
||||
): { support: number[]; resistance: number[] } => {
|
||||
const support: number[] = [];
|
||||
const resistance: number[] = [];
|
||||
|
||||
for (let i = lookback; i < data.length - lookback; i++) {
|
||||
const slice = data.slice(i - lookback, i + lookback + 1);
|
||||
const current = data[i];
|
||||
|
||||
// Check if current low is a support (lowest in range)
|
||||
const isSupport = slice.every((d) => current.low <= d.low * (1 + threshold));
|
||||
if (isSupport && !support.includes(current.low)) {
|
||||
support.push(Number(current.low.toFixed(2)));
|
||||
}
|
||||
|
||||
// Check if current high is a resistance (highest in range)
|
||||
const isResistance = slice.every((d) => current.high >= d.high * (1 - threshold));
|
||||
if (isResistance && !resistance.includes(current.high)) {
|
||||
resistance.push(Number(current.high.toFixed(2)));
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates and sort
|
||||
return {
|
||||
support: [...new Set(support)].sort((a, b) => b - a).slice(0, 5),
|
||||
resistance: [...new Set(resistance)].sort((a, b) => a - b).slice(0, 5),
|
||||
};
|
||||
};
|
||||
|
||||
// Calculate Win Rate from trades
|
||||
export const calculateWinRate = (trades: any[]): number => {
|
||||
if (trades.length === 0) return 0;
|
||||
const winningTrades = trades.filter((t) => t.pnl && t.pnl > 0).length;
|
||||
return Number(((winningTrades / trades.length) * 100).toFixed(2));
|
||||
};
|
||||
|
||||
// Calculate Sharpe Ratio (simplified)
|
||||
export const calculateSharpeRatio = (
|
||||
returns: number[],
|
||||
riskFreeRate: number = 0.02
|
||||
): number => {
|
||||
if (returns.length === 0) return 0;
|
||||
|
||||
const avgReturn = returns.reduce((sum, r) => sum + r, 0) / returns.length;
|
||||
const variance = returns.reduce((sum, r) => sum + Math.pow(r - avgReturn, 2), 0) / returns.length;
|
||||
const stdDev = Math.sqrt(variance);
|
||||
|
||||
if (stdDev === 0) return 0;
|
||||
|
||||
const excessReturn = avgReturn - riskFreeRate / 252; // Daily risk-free rate
|
||||
return Number((excessReturn / stdDev).toFixed(2));
|
||||
};
|
||||
|
||||
// Calculate Maximum Drawdown
|
||||
export const calculateMaxDrawdown = (equity: number[]): number => {
|
||||
if (equity.length === 0) return 0;
|
||||
|
||||
let maxDrawdown = 0;
|
||||
let peak = equity[0];
|
||||
|
||||
for (const value of equity) {
|
||||
if (value > peak) {
|
||||
peak = value;
|
||||
}
|
||||
const drawdown = ((peak - value) / peak) * 100;
|
||||
if (drawdown > maxDrawdown) {
|
||||
maxDrawdown = drawdown;
|
||||
}
|
||||
}
|
||||
|
||||
return Number(maxDrawdown.toFixed(2));
|
||||
};
|
||||
|
||||
// Position Sizing (Kelly Criterion)
|
||||
export const calculatePositionSize = (
|
||||
capital: number,
|
||||
winRate: number,
|
||||
avgWin: number,
|
||||
avgLoss: number
|
||||
): number => {
|
||||
if (avgLoss === 0) return 0;
|
||||
|
||||
const winLossRatio = Math.abs(avgWin / avgLoss);
|
||||
const kellyPercent = (winRate - (1 - winRate) / winLossRatio) * 100;
|
||||
|
||||
// Use half-Kelly for safety
|
||||
const safeKelly = kellyPercent / 2;
|
||||
|
||||
// Cap at 10% of capital
|
||||
const maxPercent = Math.min(Math.max(safeKelly, 0), 10);
|
||||
|
||||
return Number((capital * (maxPercent / 100)).toFixed(2));
|
||||
};
|
||||
Reference in New Issue
Block a user