Files
myapp/static/js/app.js
T

1102 lines
50 KiB
JavaScript

/**
* FX Journal Pro • Frontend JavaScript Engine
* Complete interactive state, Chart.js integrations, trade management & analytics.
*/
const app = {
currentAccountId: 1,
accounts: [],
analytics: null,
trades: [],
pagination: { page: 1, limit: 50, total: 0, total_pages: 1 },
calendarYear: 2026,
calendarMonth: 8, // 1 to 12
charts: {},
activeTab: 'overview',
// Initialization
async init() {
const today = new Date();
this.calendarYear = today.getFullYear();
this.calendarMonth = today.getMonth() + 1;
await this.loadAccounts();
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
},
// -------------------------------------------------------------
// ACCOUNTS MANAGEMENT
// -------------------------------------------------------------
async loadAccounts() {
try {
const res = await fetch('/api/accounts');
const data = await res.json();
if (data.success && data.accounts) {
this.accounts = data.accounts;
const select = document.getElementById('accountSelect');
select.innerHTML = '';
let defaultAcc = this.accounts.find(a => a.is_default == 1) || this.accounts[0];
if (defaultAcc) {
this.currentAccountId = defaultAcc.id;
}
this.accounts.forEach(acc => {
const opt = document.createElement('option');
opt.value = acc.id;
opt.textContent = `${acc.name} (${acc.broker} • $${parseFloat(acc.initial_balance).toLocaleString()})`;
if (acc.id === this.currentAccountId) opt.selected = true;
select.appendChild(opt);
});
this.renderSettingsAccounts();
}
} catch (e) {
console.error('Error loading accounts:', e);
}
},
async switchAccount(accId) {
this.currentAccountId = parseInt(accId);
this.pagination.page = 1;
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
},
renderSettingsAccounts() {
const container = document.getElementById('accountsListSettings');
if (!container) return;
container.innerHTML = '';
this.accounts.forEach(acc => {
const isCurrent = acc.id === this.currentAccountId;
const netProfit = (parseFloat(acc.net_closed_profit) || 0) + (parseFloat(acc.net_open_profit) || 0);
const profitClass = netProfit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const profitSign = netProfit >= 0 ? '+' : '';
const el = document.createElement('div');
el.className = `p-4 rounded-xl border ${isCurrent ? 'bg-blue-900/20 border-blue-500/50' : 'bg-gray-800/40 border-gray-700/40'} flex items-center justify-between flex-wrap gap-2`;
el.innerHTML = `
<div>
<div class="flex items-center gap-2">
<span class="font-bold text-white text-sm">${acc.name}</span>
<span class="text-xs px-2 py-0.5 rounded-full ${acc.account_type === 'Real' ? 'bg-emerald-500/20 text-emerald-400' : 'bg-purple-500/20 text-purple-400'}">${acc.account_type}</span>
${isCurrent ? '<span class="text-xs bg-blue-500/20 text-blue-400 px-2 py-0.5 rounded-full">فعال</span>' : ''}
</div>
<div class="text-xs text-gray-400 mt-1 flex items-center gap-3">
<span>بروکر: ${acc.broker}</span>
<span>بالانس اولیه: $${parseFloat(acc.initial_balance).toLocaleString()}</span>
<span>معاملات: ${acc.total_trades || 0}</span>
</div>
</div>
<div class="flex items-center gap-3">
<div class="text-right">
<span class="text-xs text-gray-400 block">سود/زیان کل:</span>
<span class="font-bold text-sm num-font ${profitClass}">${profitSign}$${netProfit.toFixed(2)}</span>
</div>
${this.accounts.length > 1 ? `
<button onclick="app.deleteAccount(${acc.id})" class="text-rose-400 hover:text-rose-300 text-xs p-1.5" title="حذف حساب">
<i class="fa-solid fa-trash"></i>
</button>
` : ''}
</div>
`;
container.appendChild(el);
});
},
async createAccount(e) {
e.preventDefault();
const name = document.getElementById('newAccName').value.trim();
const broker = document.getElementById('newAccBroker').value.trim();
const balance = parseFloat(document.getElementById('newAccBalance').value) || 10000;
const accType = document.getElementById('newAccType').value;
const accNum = document.getElementById('newAccNumber').value.trim();
try {
const res = await fetch('/api/accounts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name, broker, initial_balance: balance, account_type: accType, account_number: accNum, is_default: 1
})
});
const data = await res.json();
if (data.success) {
this.closeModal('modalAccount');
await this.loadAccounts();
if (data.id) await this.switchAccount(data.id);
}
} catch (e) {
alert('خطا در ایجاد حساب: ' + e.message);
}
},
async deleteAccount(id) {
if (!confirm('آیا از حذف این حساب و تمام معاملات آن مطمئن هستید؟')) return;
try {
const res = await fetch(`/api/accounts/${id}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) {
await this.loadAccounts();
await this.switchAccount(this.accounts[0]?.id || 1);
}
} catch (e) {
alert('خطا در حذف حساب: ' + e.message);
}
},
// -------------------------------------------------------------
// ANALYTICS & CHARTS
// -------------------------------------------------------------
async loadAnalytics() {
try {
const res = await fetch(`/api/analytics?account_id=${this.currentAccountId}`);
const json = await res.json();
if (json.success && json.data) {
this.analytics = json.data;
this.renderSummaryTicker();
this.renderKeyMetrics();
this.renderGrowthChart();
this.renderDrawdownChart();
this.renderWinRateChart();
this.renderMonthlyReturns();
this.renderDeepAnalytics();
}
} catch (e) {
console.error('Error loading analytics:', e);
}
},
renderSummaryTicker() {
const sum = this.analytics.summary;
const container = document.getElementById('accountSummaryTicker');
if (!container) return;
const gainSign = sum.total_gain_percent >= 0 ? '+' : '';
const gainClass = sum.total_gain_percent >= 0 ? 'text-emerald-400' : 'text-rose-400';
const gainBg = sum.total_gain_percent >= 0 ? 'bg-emerald-500/10 border-emerald-500/20' : 'bg-rose-500/10 border-rose-500/20';
const closedProfitSign = sum.total_closed_profit >= 0 ? '+' : '';
const closedProfitClass = sum.total_closed_profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const openProfitSign = sum.open_floating_profit >= 0 ? '+' : '';
const openProfitClass = sum.open_floating_profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
container.innerHTML = `
<!-- Card 1: Balance -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">بالانس (Balance)</span>
<div class="text-lg font-extrabold text-white num-font">$${sum.current_balance.toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
<span class="text-xs text-gray-400 mt-1 block">پایه: $${sum.initial_balance.toLocaleString()}</span>
</div>
<!-- Card 2: Equity -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">اکوئیتی (Equity)</span>
<div class="text-lg font-extrabold text-blue-400 num-font">$${sum.current_equity.toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
<span class="text-xs num-font ${openProfitClass} mt-1 block">شناور: ${openProfitSign}$${sum.open_floating_profit.toFixed(2)}</span>
</div>
<!-- Card 3: Total Gain % -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">درصد رشد کل (Gain)</span>
<div class="text-lg font-extrabold num-font ${gainClass}">${gainSign}${sum.total_gain_percent.toFixed(2)}%</div>
<span class="text-xs text-gray-400 mt-1 block">بسته‌شده: ${closedProfitSign}${sum.closed_gain_percent.toFixed(1)}%</span>
</div>
<!-- Card 4: Net Closed Profit -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">سود خالص (Net Profit)</span>
<div class="text-lg font-extrabold num-font ${closedProfitClass}">${closedProfitSign}$${sum.total_closed_profit.toLocaleString('en-US', {minimumFractionDigits: 2})}</div>
<span class="text-xs text-gray-400 mt-1 block">امروز: $${sum.today_profit.toFixed(2)}</span>
</div>
<!-- Card 5: Profit Factor -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">فاکتور سود (Profit Factor)</span>
<div class="text-lg font-extrabold text-purple-400 num-font">${sum.profit_factor}</div>
<span class="text-xs text-gray-400 mt-1 block">ناخالص: +$${sum.gross_profit.toFixed(0)}</span>
</div>
<!-- Card 6: Win Rate -->
<div class="glass-card stat-card p-3.5">
<span class="text-xs text-gray-400 block mb-1">نرخ برد (Win Rate)</span>
<div class="text-lg font-extrabold text-emerald-400 num-font">${sum.win_rate}%</div>
<span class="text-xs text-gray-400 mt-1 block">${sum.won_trades} برد / ${sum.lost_trades} باخت</span>
</div>
`;
document.getElementById('chartGainPct').textContent = `${gainSign}${sum.total_gain_percent.toFixed(2)}%`;
},
renderKeyMetrics() {
const sum = this.analytics.summary;
document.getElementById('metricProfitFactor').textContent = sum.profit_factor;
document.getElementById('metricMaxDD').textContent = `${sum.max_drawdown_percent.toFixed(1)}% ($${sum.max_drawdown_amount.toFixed(0)})`;
document.getElementById('metricExpectancy').textContent = `$${sum.expectancy.toFixed(2)}`;
document.getElementById('metricWinLossRatio').textContent = sum.win_loss_ratio;
document.getElementById('metricSharpe').textContent = sum.sharpe_ratio;
document.getElementById('metricTotalPips').textContent = sum.total_pips.toLocaleString();
document.getElementById('metricAvgWin').textContent = `$${sum.avg_win.toFixed(2)} (${sum.avg_pips_win.toFixed(1)} pips)`;
document.getElementById('metricAvgLoss').textContent = `$${sum.avg_loss.toFixed(2)} (${sum.avg_pips_loss.toFixed(1)} pips)`;
document.getElementById('metricMaxConsecWins').textContent = `${sum.max_consecutive_wins} معامله`;
const durHours = (sum.avg_trade_duration_seconds / 3600).toFixed(1);
document.getElementById('metricAvgDuration').textContent = `${durHours} ساعت`;
document.getElementById('longWinRate').textContent = `${sum.long_win_rate}% (${sum.long_won}/${sum.long_trades})`;
document.getElementById('longProfit').textContent = `$${sum.long_profit.toFixed(0)}`;
document.getElementById('shortWinRate').textContent = `${sum.short_win_rate}% (${sum.short_won}/${sum.short_trades})`;
document.getElementById('shortProfit').textContent = `$${sum.short_profit.toFixed(0)}`;
},
renderGrowthChart() {
const ctx = document.getElementById('chartGrowth');
if (!ctx) return;
if (this.charts.growth) this.charts.growth.destroy();
const curve = this.analytics.growth_curve || [];
const labels = curve.map((pt, i) => i === 0 ? 'شروع' : (pt.ticket || `#${i}`));
const balances = curve.map(pt => pt.balance);
const gainPcts = curve.map(pt => pt.gain_percent);
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 300);
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.35)');
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.0)');
this.charts.growth = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [
{
label: 'بالانس ($)',
data: balances,
borderColor: '#3B82F6',
backgroundColor: gradient,
borderWidth: 2.5,
fill: true,
tension: 0.25,
pointRadius: curve.length > 30 ? 2 : 4,
pointHoverRadius: 6,
yAxisID: 'y'
},
{
label: 'درصد رشد (%)',
data: gainPcts,
borderColor: '#10B981',
borderWidth: 1.8,
borderDash: [4, 4],
fill: false,
tension: 0.25,
pointRadius: 0,
yAxisID: 'y1'
}
]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: {
legend: {
labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } }
},
tooltip: {
callbacks: {
label: function(context) {
if (context.datasetIndex === 0) return ` بالانس: $${context.parsed.y.toLocaleString()}`;
return ` رشد: ${context.parsed.y.toFixed(2)}%`;
},
afterBody: function(items) {
const idx = items[0]?.dataIndex;
if (idx > 0 && curve[idx]) {
const pt = curve[idx];
return [`نماد: ${pt.symbol}`, `سود ترید: $${pt.profit}`, `تاریخ: ${pt.date}`];
}
return [];
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(31, 41, 61, 0.5)' },
ticks: { color: '#6B7280', font: { size: 10 } }
},
y: {
type: 'linear',
display: true,
position: 'left',
grid: { color: 'rgba(31, 41, 61, 0.5)' },
ticks: {
color: '#9CA3AF',
callback: v => '$' + v.toLocaleString()
}
},
y1: {
type: 'linear',
display: true,
position: 'right',
grid: { drawOnChartArea: false },
ticks: {
color: '#10B981',
callback: v => v + '%'
}
}
}
}
});
},
renderDrawdownChart() {
const ctx = document.getElementById('chartDrawdown');
if (!ctx) return;
if (this.charts.drawdown) this.charts.drawdown.destroy();
const curve = this.analytics.drawdown_curve || [];
const labels = curve.map((_, i) => `#${i + 1}`);
const ddData = curve.map(pt => pt.drawdown_percent);
const gradient = ctx.getContext('2d').createLinearGradient(0, 0, 0, 200);
gradient.addColorStop(0, 'rgba(239, 68, 68, 0.0)');
gradient.addColorStop(1, 'rgba(239, 68, 68, 0.35)');
this.charts.drawdown = new Chart(ctx, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'درصد افت (Drawdown %)',
data: ddData,
borderColor: '#EF4444',
backgroundColor: gradient,
borderWidth: 2,
fill: true,
tension: 0.2,
pointRadius: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } } },
tooltip: {
callbacks: {
label: ctx => ` افت سرمایه: ${Math.abs(ctx.parsed.y).toFixed(2)}%`
}
}
},
scales: {
x: { grid: { color: 'rgba(31, 41, 61, 0.4)' }, ticks: { color: '#6B7280' } },
y: {
grid: { color: 'rgba(31, 41, 61, 0.4)' },
ticks: { color: '#EF4444', callback: v => v + '%' }
}
}
}
});
},
renderWinRateChart() {
const ctx = document.getElementById('chartWinRate');
if (!ctx) return;
if (this.charts.winRate) this.charts.winRate.destroy();
const sum = this.analytics.summary;
this.charts.winRate = new Chart(ctx, {
type: 'doughnut',
data: {
labels: ['برد (Won)', 'باخت (Lost)', 'سر‌به‌سر (BE)'],
datasets: [{
data: [sum.won_trades, sum.lost_trades, sum.breakeven_trades],
backgroundColor: ['#10B981', '#EF4444', '#6B7280'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
cutout: '70%',
plugins: {
legend: {
position: 'bottom',
labels: { color: '#9CA3AF', font: { family: 'Vazirmatn', size: 11 } }
}
}
}
});
},
renderMonthlyReturns() {
const table = document.getElementById('monthlyReturnsTable');
if (!table) return;
table.innerHTML = '';
const returns = this.analytics.monthly_returns || [];
if (returns.length === 0) {
table.innerHTML = `<tr><td colspan="14" class="py-4 text-gray-500">معامله‌ای در این بازه ثبت نشده است</td></tr>`;
return;
}
returns.forEach(y => {
const tr = document.createElement('tr');
let cols = `<td class="font-bold text-white bg-gray-800/80">${y.year}</td>`;
for (let m = 1; m <= 12; m++) {
const mData = y.months[m];
if (mData && mData.trades > 0) {
const pct = mData.gain_percent;
const bg = pct > 0 ? 'bg-emerald-500/20 text-emerald-400 font-bold' : (pct < 0 ? 'bg-rose-500/20 text-rose-400 font-bold' : 'text-gray-400');
const sign = pct > 0 ? '+' : '';
cols += `<td class="heatmap-cell ${bg}" title="سود: $${mData.profit.toFixed(2)} (${mData.trades} ترید)">${sign}${pct.toFixed(1)}%</td>`;
} else {
cols += `<td class="text-gray-600">-</td>`;
}
}
const totalPct = y.total_gain_percent;
const totalBg = totalPct >= 0 ? 'bg-emerald-600/30 text-emerald-300 font-extrabold' : 'bg-rose-600/30 text-rose-300 font-extrabold';
const totalSign = totalPct >= 0 ? '+' : '';
cols += `<td class="${totalBg} num-font">${totalSign}${totalPct.toFixed(1)}%</td>`;
tr.innerHTML = cols;
table.appendChild(tr);
});
},
renderDeepAnalytics() {
// Performance by Symbol Chart & Table
const symCtx = document.getElementById('chartSymbols');
if (symCtx) {
if (this.charts.symbols) this.charts.symbols.destroy();
const syms = this.analytics.by_symbol || [];
const labels = syms.map(s => s.symbol);
const profits = syms.map(s => s.profit);
const colors = profits.map(p => p >= 0 ? '#10B981' : '#EF4444');
this.charts.symbols = new Chart(symCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'سود دلاری ($)',
data: profits,
backgroundColor: colors,
borderRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#9CA3AF' }, grid: { display: false } },
y: { ticks: { color: '#9CA3AF', callback: v => '$' + v }, grid: { color: 'rgba(31, 41, 61, 0.4)' } }
}
}
});
const symTable = document.getElementById('symbolsTable');
if (symTable) {
symTable.innerHTML = '';
syms.forEach(s => {
const tr = document.createElement('tr');
const profitClass = s.profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const sign = s.profit >= 0 ? '+' : '';
tr.innerHTML = `
<td class="font-bold text-white">${s.symbol}</td>
<td>${s.count}</td>
<td class="text-emerald-400 num-font">${s.win_rate}%</td>
<td class="num-font">${s.pips.toFixed(1)}</td>
<td class="font-bold num-font ${profitClass}">${sign}$${s.profit.toFixed(2)}</td>
`;
symTable.appendChild(tr);
});
}
}
// Performance by Strategy Table
const stratTable = document.getElementById('strategiesTable');
if (stratTable) {
stratTable.innerHTML = '';
(this.analytics.by_strategy || []).forEach(st => {
const tr = document.createElement('tr');
const profitClass = st.profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const sign = st.profit >= 0 ? '+' : '';
tr.innerHTML = `
<td class="font-bold text-white">${st.strategy}</td>
<td>${st.count}</td>
<td class="text-emerald-400 num-font">${st.win_rate}%</td>
<td class="num-font text-blue-400">${st.profit_factor}</td>
<td class="font-bold num-font ${profitClass}">${sign}$${st.profit.toFixed(2)}</td>
`;
stratTable.appendChild(tr);
});
}
// Hourly Chart
const hrCtx = document.getElementById('chartHourly');
if (hrCtx) {
if (this.charts.hourly) this.charts.hourly.destroy();
const hourly = this.analytics.by_hour || [];
const labels = hourly.map(h => `${h.hour}:00`);
const counts = hourly.map(h => h.count);
this.charts.hourly = new Chart(hrCtx, {
type: 'bar',
data: {
labels: labels,
datasets: [{
label: 'تعداد معاملات',
data: counts,
backgroundColor: '#3B82F6',
borderRadius: 4
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
x: { ticks: { color: '#6B7280', font: { size: 9 } }, grid: { display: false } },
y: { ticks: { color: '#9CA3AF', stepSize: 1 }, grid: { color: 'rgba(31, 41, 61, 0.4)' } }
}
}
});
}
// Day of Week List
const dowContainer = document.getElementById('dayOfWeekList');
if (dowContainer) {
dowContainer.innerHTML = '';
(this.analytics.by_day_of_week || []).forEach(d => {
const profitClass = d.profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const sign = d.profit >= 0 ? '+' : '';
const el = document.createElement('div');
el.className = 'flex items-center justify-between p-2 rounded-lg bg-gray-800/40 border border-gray-700/30';
el.innerHTML = `
<span class="font-semibold text-gray-200">${d.name_fa} (${d.name})</span>
<div class="flex items-center gap-3">
<span class="text-gray-400">${d.count} ترید</span>
<span class="font-bold num-font ${profitClass}">${sign}$${d.profit.toFixed(0)}</span>
</div>
`;
dowContainer.appendChild(el);
});
}
// Emotion List
const emoContainer = document.getElementById('emotionList');
if (emoContainer) {
emoContainer.innerHTML = '';
(this.analytics.by_emotion || []).forEach(em => {
const profitClass = em.profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
const sign = em.profit >= 0 ? '+' : '';
const el = document.createElement('div');
el.className = 'flex items-center justify-between p-2 rounded-lg bg-gray-800/40 border border-gray-700/30';
el.innerHTML = `
<span class="font-semibold text-gray-200">${em.emotion}</span>
<div class="flex items-center gap-3">
<span class="text-emerald-400 num-font">${em.win_rate}% برد</span>
<span class="font-bold num-font ${profitClass}">${sign}$${em.profit.toFixed(0)}</span>
</div>
`;
emoContainer.appendChild(el);
});
}
},
// -------------------------------------------------------------
// TRADES MANAGEMENT & LOG
// -------------------------------------------------------------
async loadTrades() {
const status = document.getElementById('tradeFilterStatus')?.value || 'all';
const symbol = document.getElementById('tradeFilterSymbol')?.value || 'all';
const search = document.getElementById('tradeSearch')?.value || '';
try {
const res = await fetch(`/api/trades?account_id=${this.currentAccountId}&page=${this.pagination.page}&limit=${this.pagination.limit}&status=${status}&symbol=${symbol}&search=${encodeURIComponent(search)}`);
const json = await res.json();
if (json.success) {
this.trades = json.trades || [];
this.pagination = json.pagination;
this.renderTradesTable();
}
} catch (e) {
console.error('Error loading trades:', e);
}
},
renderTradesTable() {
const tbody = document.getElementById('tradesTableBody');
if (!tbody) return;
tbody.innerHTML = '';
document.getElementById('tradesCountLabel').textContent = this.pagination.total;
document.getElementById('paginationInfo').textContent = `صفحه ${this.pagination.page} از ${this.pagination.total_pages || 1}`;
document.getElementById('btnPrevPage').disabled = this.pagination.page <= 1;
document.getElementById('btnNextPage').disabled = this.pagination.page >= this.pagination.total_pages;
if (this.trades.length === 0) {
tbody.innerHTML = `<tr><td colspan="13" class="text-center py-6 text-gray-500">معامله‌ای یافت نشد</td></tr>`;
return;
}
this.trades.forEach((t, i) => {
const tr = document.createElement('tr');
const isBuy = t.trade_type.toLowerCase() === 'buy';
const badgeType = isBuy ? '<span class="px-2 py-0.5 rounded text-xs badge-buy">BUY</span>' : '<span class="px-2 py-0.5 rounded text-xs badge-sell">SELL</span>';
const profitVal = parseFloat(t.profit) + parseFloat(t.commission) + parseFloat(t.swap);
const profitClass = profitVal >= 0 ? 'profit-pos' : 'profit-neg';
const profitSign = profitVal >= 0 ? '+' : '';
const openTimeFormatted = t.open_time ? t.open_time.substring(5, 16) : '-';
const hasChart = t.screenshot_entry || t.screenshot_exit;
tr.innerHTML = `
<td class="text-gray-500 num-font">${t.id}</td>
<td>
<div class="font-bold text-white">${t.symbol}</div>
<div class="text-[10px] text-gray-400 num-font">${t.ticket || '-'}</div>
</td>
<td>${badgeType}</td>
<td class="font-semibold num-font">${parseFloat(t.lot_size).toFixed(2)}</td>
<td class="num-font">${parseFloat(t.open_price).toFixed(t.open_price > 50 ? 2 : 5)}</td>
<td class="num-font">${t.close_price ? parseFloat(t.close_price).toFixed(t.close_price > 50 ? 2 : 5) : '<span class="text-blue-400">باز</span>'}</td>
<td class="text-[11px] num-font text-gray-400">
<div>SL: ${t.stop_loss ? parseFloat(t.stop_loss) : '-'}</div>
<div>TP: ${t.take_profit ? parseFloat(t.take_profit) : '-'}</div>
</td>
<td class="text-gray-300 num-font text-xs">${openTimeFormatted}</td>
<td class="num-font font-semibold ${t.pips >= 0 ? 'text-emerald-400' : 'text-rose-400'}">${t.pips ? (t.pips > 0 ? '+' : '') + parseFloat(t.pips).toFixed(1) : '-'}</td>
<td class="num-font text-sm ${profitClass}">${profitSign}$${profitVal.toFixed(2)}</td>
<td>
<div class="text-xs text-gray-200">${t.strategy || '-'}</div>
<div class="text-[10px] text-gray-400">${t.emotion || ''}</div>
</td>
<td>
${hasChart ? `
<button onclick="app.openLightbox('${t.screenshot_entry || t.screenshot_exit}')" class="text-blue-400 hover:text-blue-300 text-sm" title="مشاهده چارت">
<i class="fa-solid fa-image"></i>
</button>
` : '<span class="text-gray-600">-</span>'}
</td>
<td>
<div class="flex items-center gap-2">
<button onclick="app.editTrade(${t.id})" class="text-gray-400 hover:text-blue-400 text-xs" title="ویرایش">
<i class="fa-solid fa-pen"></i>
</button>
<button onclick="app.deleteTrade(${t.id})" class="text-gray-400 hover:text-rose-400 text-xs" title="حذف">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</td>
`;
tbody.appendChild(tr);
});
},
handleSearch(e) {
if (e.key === 'Enter' || !e.target.value) {
this.pagination.page = 1;
this.loadTrades();
}
},
prevPage() {
if (this.pagination.page > 1) {
this.pagination.page--;
this.loadTrades();
}
},
nextPage() {
if (this.pagination.page < this.pagination.total_pages) {
this.pagination.page++;
this.loadTrades();
}
},
// -------------------------------------------------------------
// TRADE FORM & CALCULATION
// -------------------------------------------------------------
openNewTradeModal() {
document.getElementById('modalTradeTitle').innerHTML = '<i class="fa-solid fa-plus-circle text-emerald-400"></i><span>ثبت معامله جدید</span>';
document.getElementById('tradeId').value = '';
document.getElementById('tradeSymbol').value = 'EURUSD';
document.querySelector('input[name="tradeType"][value="buy"]').checked = true;
document.getElementById('tradeLotSize').value = '0.10';
document.getElementById('tradeOpenPrice').value = '';
document.getElementById('tradeClosePrice').value = '';
document.getElementById('tradeStopLoss').value = '';
document.getElementById('tradeTakeProfit').value = '';
document.getElementById('tradeProfit').value = '';
document.getElementById('tradePips').value = '';
document.getElementById('tradeEntryNotes').value = '';
document.getElementById('tradeLessons').value = '';
document.getElementById('tradeScreenshotEntryUrl').value = '';
document.getElementById('tradeScreenshotExitUrl').value = '';
this.openModal('modalTrade');
},
async editTrade(id) {
try {
const res = await fetch(`/api/trades/${id}`);
const json = await res.json();
if (json.success && json.trade) {
const t = json.trade;
document.getElementById('modalTradeTitle').innerHTML = '<i class="fa-solid fa-pen text-blue-400"></i><span>ویرایش معامله</span>';
document.getElementById('tradeId').value = t.id;
document.getElementById('tradeSymbol').value = t.symbol;
const typeRadio = document.querySelector(`input[name="tradeType"][value="${t.trade_type.toLowerCase()}"]`);
if (typeRadio) typeRadio.checked = true;
document.getElementById('tradeLotSize').value = t.lot_size;
document.getElementById('tradeOpenPrice').value = t.open_price;
document.getElementById('tradeClosePrice').value = t.close_price || '';
document.getElementById('tradeStopLoss').value = t.stop_loss || '';
document.getElementById('tradeTakeProfit').value = t.take_profit || '';
document.getElementById('tradeProfit').value = t.profit || '';
document.getElementById('tradePips').value = t.pips || '';
document.getElementById('tradeCommission').value = t.commission || '0.00';
document.getElementById('tradeSwap').value = t.swap || '0.00';
document.getElementById('tradeStrategy').value = t.strategy || 'Price Action';
document.getElementById('tradeSession').value = t.session || 'London';
document.getElementById('tradeTimeframe').value = t.timeframe || 'M15';
document.getElementById('tradeEmotion').value = t.emotion || 'Disciplined';
document.getElementById('tradeEntryNotes').value = t.entry_notes || '';
document.getElementById('tradeLessons').value = t.lessons || '';
document.getElementById('tradeScreenshotEntryUrl').value = t.screenshot_entry || '';
document.getElementById('tradeScreenshotExitUrl').value = t.screenshot_exit || '';
this.openModal('modalTrade');
}
} catch (e) {
alert('خطا در دریافت اطلاعات معامله: ' + e.message);
}
},
calcTradeFields() {
const symbol = document.getElementById('tradeSymbol').value.toUpperCase().trim();
const type = document.querySelector('input[name="tradeType"]:checked')?.value || 'buy';
const lot = parseFloat(document.getElementById('tradeLotSize').value) || 0.1;
const openPrice = parseFloat(document.getElementById('tradeOpenPrice').value);
const closePrice = parseFloat(document.getElementById('tradeClosePrice').value);
if (!isNaN(openPrice) && !isNaN(closePrice) && closePrice > 0) {
const diff = (type === 'buy') ? (closePrice - openPrice) : (openPrice - closePrice);
let pips = 0;
let pipVal = 10 * lot; // $10 per standard lot
if (symbol.includes('JPY')) {
pips = diff * 100;
} else if (symbol.includes('XAU') || symbol.includes('GOLD')) {
pips = diff * 10;
pipVal = 10 * lot;
} else if (symbol.includes('BTC') || symbol.includes('US30') || symbol.includes('NAS')) {
pips = diff;
pipVal = 1 * lot;
} else {
pips = diff * 10000;
}
const estimatedProfit = pips * pipVal;
document.getElementById('tradePips').value = pips.toFixed(1);
document.getElementById('tradeProfit').value = estimatedProfit.toFixed(2);
}
},
async saveTrade(e) {
e.preventDefault();
const tradeId = document.getElementById('tradeId').value;
const symbol = document.getElementById('tradeSymbol').value.toUpperCase().trim();
const tradeType = document.querySelector('input[name="tradeType"]:checked')?.value || 'buy';
const lotSize = parseFloat(document.getElementById('tradeLotSize').value) || 0.1;
const openPrice = parseFloat(document.getElementById('tradeOpenPrice').value);
const closePriceVal = document.getElementById('tradeClosePrice').value;
const closePrice = closePriceVal !== '' ? parseFloat(closePriceVal) : null;
const stopLoss = document.getElementById('tradeStopLoss').value ? parseFloat(document.getElementById('tradeStopLoss').value) : null;
const takeProfit = document.getElementById('tradeTakeProfit').value ? parseFloat(document.getElementById('tradeTakeProfit').value) : null;
const profit = document.getElementById('tradeProfit').value !== '' ? parseFloat(document.getElementById('tradeProfit').value) : 0.0;
const pips = document.getElementById('tradePips').value !== '' ? parseFloat(document.getElementById('tradePips').value) : 0.0;
const commission = parseFloat(document.getElementById('tradeCommission').value) || 0.0;
const swap = parseFloat(document.getElementById('tradeSwap').value) || 0.0;
const strategy = document.getElementById('tradeStrategy').value;
const session = document.getElementById('tradeSession').value;
const timeframe = document.getElementById('tradeTimeframe').value;
const emotion = document.getElementById('tradeEmotion').value;
const entryNotes = document.getElementById('tradeEntryNotes').value;
const lessons = document.getElementById('tradeLessons').value;
const screenshotEntry = document.getElementById('tradeScreenshotEntryUrl').value;
const screenshotExit = document.getElementById('tradeScreenshotExitUrl').value;
const payload = {
account_id: this.currentAccountId,
symbol, trade_type: tradeType, lot_size: lotSize, open_price: openPrice,
close_price: closePrice, stop_loss: stopLoss, take_profit: takeProfit,
profit, pips, commission, swap, strategy, session, timeframe, emotion,
entry_notes: entryNotes, lessons, screenshot_entry: screenshotEntry, screenshot_exit: screenshotExit,
status: closePrice !== null ? 'closed' : 'open'
};
try {
const url = tradeId ? `/api/trades/${tradeId}` : '/api/trades';
const method = tradeId ? 'PUT' : 'POST';
const res = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const json = await res.json();
if (json.success) {
this.closeModal('modalTrade');
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
} else {
alert('خطا: ' + json.error);
}
} catch (err) {
alert('خطا در ذخیره معامله: ' + err.message);
}
},
async deleteTrade(id) {
if (!confirm('آیا از حذف این معامله مطمئن هستید؟')) return;
try {
const res = await fetch(`/api/trades/${id}`, { method: 'DELETE' });
const json = await res.json();
if (json.success) {
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
}
} catch (e) {
alert('خطا در حذف معامله: ' + e.message);
}
},
async uploadScreenshot(input, targetHiddenId) {
if (!input.files || !input.files[0]) return;
const formData = new FormData();
formData.append('image', input.files[0]);
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const json = await res.json();
if (json.success && json.url) {
document.getElementById(targetHiddenId).value = json.url;
} else {
alert('خطا در بارگذاری تصویر: ' + json.error);
}
} catch (e) {
alert('خطا در ارسال تصویر: ' + e.message);
}
},
// -------------------------------------------------------------
// MT4 / MT5 IMPORT & EXPORT
// -------------------------------------------------------------
async importStatement(e) {
e.preventDefault();
const fileInput = document.getElementById('statementFile');
if (!fileInput.files || !fileInput.files[0]) {
alert('لطفاً ابتدا فایل گزارش را انتخاب کنید');
return;
}
const formData = new FormData();
formData.append('account_id', this.currentAccountId);
formData.append('statement', fileInput.files[0]);
const btn = document.getElementById('btnSubmitImport');
btn.disabled = true;
btn.textContent = 'در حال پردازش...';
try {
const res = await fetch('/api/trades/import', { method: 'POST', body: formData });
const json = await res.json();
if (json.success) {
alert(json.message);
this.closeModal('modalImport');
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
} else {
alert('خطا در ایمپورت: ' + json.error);
}
} catch (err) {
alert('خطا در ارسال استیتمنت: ' + err.message);
} finally {
btn.disabled = false;
btn.textContent = 'شروع پردازش و ایمپورت';
}
},
exportTradesCSV() {
window.location.href = `/api/trades/export?account_id=${this.currentAccountId}`;
},
async resetSampleData() {
if (!confirm('آیا مایلید تمام داده‌های این حساب با نمونه‌های واقعی مای‌اف‌ایکس‌بوک جایگزین شوند؟')) return;
try {
const res = await fetch(`/api/reset_sample?account_id=${this.currentAccountId}`, { method: 'POST' });
const json = await res.json();
if (json.success) {
await this.loadAnalytics();
await this.loadTrades();
this.renderCalendar();
}
} catch (e) {
alert('خطا در بازیابی داده‌ها: ' + e.message);
}
},
// -------------------------------------------------------------
// CALENDAR VIEW
// -------------------------------------------------------------
prevMonth() {
this.calendarMonth--;
if (this.calendarMonth < 1) {
this.calendarMonth = 12;
this.calendarYear--;
}
this.renderCalendar();
},
nextMonth() {
this.calendarMonth++;
if (this.calendarMonth > 12) {
this.calendarMonth = 1;
this.calendarYear++;
}
this.renderCalendar();
},
renderCalendar() {
const grid = document.getElementById('calendarGrid');
if (!grid) return;
grid.innerHTML = '';
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
document.getElementById('calendarMonthLabel').textContent = `${monthNames[this.calendarMonth - 1]} ${this.calendarYear}`;
const firstDay = new Date(this.calendarYear, this.calendarMonth - 1, 1);
const lastDay = new Date(this.calendarYear, this.calendarMonth, 0);
const totalDays = lastDay.getDate();
// Sunday is 0, Monday is 1 in JS getDay(). We want Monday = 0 .. Sunday = 6
let startDayIndex = firstDay.getDay() - 1;
if (startDayIndex === -1) startDayIndex = 6;
// Empty cells for preceding month
for (let i = 0; i < startDayIndex; i++) {
const emptyCell = document.createElement('div');
emptyCell.className = 'bg-gray-900/30 rounded-xl p-2 min-h-[70px] border border-gray-800/40 opacity-30';
grid.appendChild(emptyCell);
}
const dailyPnL = this.analytics?.daily_pnl || {};
for (let day = 1; day <= totalDays; day++) {
const dateStr = `${this.calendarYear}-${String(this.calendarMonth).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
const dayData = dailyPnL[dateStr];
const cell = document.createElement('div');
cell.className = 'rounded-xl p-2.5 min-h-[70px] border transition flex flex-col justify-between ';
if (dayData && dayData.trades > 0) {
const isPos = dayData.profit >= 0;
cell.className += isPos
? 'bg-emerald-500/10 border-emerald-500/30 hover:border-emerald-400'
: 'bg-rose-500/10 border-rose-500/30 hover:border-rose-400';
const sign = dayData.profit >= 0 ? '+' : '';
const profitClass = dayData.profit >= 0 ? 'text-emerald-400' : 'text-rose-400';
cell.innerHTML = `
<div class="flex items-center justify-between text-xs">
<span class="font-bold text-white num-font">${day}</span>
<span class="text-[10px] text-gray-400">${dayData.trades} معامله</span>
</div>
<div class="text-right">
<div class="font-bold text-xs num-font ${profitClass}">${sign}$${dayData.profit.toFixed(2)}</div>
<div class="text-[10px] text-gray-400 num-font">${dayData.pips ? (dayData.pips > 0 ? '+' : '') + dayData.pips.toFixed(1) + ' p' : ''}</div>
</div>
`;
} else {
cell.className += 'bg-gray-800/20 border-gray-800/60 text-gray-500';
cell.innerHTML = `
<div class="text-xs font-bold text-gray-400 num-font">${day}</div>
<div class="text-[10px] text-gray-600 text-center">-</div>
`;
}
grid.appendChild(cell);
}
},
// -------------------------------------------------------------
// UI UTILITIES & MODALS
// -------------------------------------------------------------
switchTab(tabId) {
this.activeTab = tabId;
['overview', 'deep_analytics', 'trades', 'calendar', 'settings'].forEach(t => {
const btn = document.getElementById(`tab-${t}`);
const view = document.getElementById(`view-${t}`);
if (btn && view) {
if (t === tabId) {
btn.classList.add('active');
view.classList.remove('hidden');
} else {
btn.classList.remove('active');
view.classList.add('hidden');
}
}
});
},
openModal(id) {
const m = document.getElementById(id);
if (m) m.classList.remove('hidden');
},
closeModal(id) {
const m = document.getElementById(id);
if (m) m.classList.add('hidden');
},
openLightbox(imgUrl) {
if (!imgUrl) return;
const m = document.getElementById('modalLightbox');
const img = document.getElementById('lightboxImg');
img.src = imgUrl;
m.classList.remove('hidden');
},
toggleTheme() {
const body = document.body;
const icon = document.getElementById('themeIcon');
if (body.classList.contains('light-mode')) {
body.classList.remove('light-mode');
icon.className = 'fa-solid fa-moon text-amber-400';
} else {
body.classList.add('light-mode');
icon.className = 'fa-solid fa-sun text-amber-500';
}
}
};
// Bootstrap application on DOM ready
document.addEventListener('DOMContentLoaded', () => app.init());