1102 lines
50 KiB
JavaScript
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());
|