feat: Implement comprehensive Forex Trading Journal platform with Myfxbook analytics

This commit is contained in:
Antigravity Bot
2026-08-31 01:38:36 +03:30
parent ded79a9a9e
commit a6bf56fafb
12 changed files with 3549 additions and 1933 deletions
+596 -306
View File
@@ -1,12 +1,12 @@
<?php
// api.php - RESTful API handlers for ShahrNegar
/**
* Forex Trading Journal REST API
* Clean JSON API for accounts, trades, analytics, chart uploads, MT4/MT5 statement import, and exports.
*/
require_once __DIR__ . '/db.php';
// Set response headers
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PATCH, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
@@ -14,321 +14,611 @@ if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
exit;
}
initDB();
$pdo = getDB();
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/analytics.php';
$requestUri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$pdo = Database::getConnection();
$analyticsEngine = new AnalyticsEngine($pdo);
// Parse request path
$uri = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$method = $_SERVER['REQUEST_METHOD'];
// Helper JSON response
function jsonResponse($data, int $statusCode = 200) {
function sendJson(array $data, int $statusCode = 200): void {
http_response_code($statusCode);
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
exit;
}
// Helper to get raw JSON payload
function getJsonInput(): array {
$input = file_get_contents('php://input');
return json_decode($input, true) ?? [];
$raw = file_get_contents('php://input');
$decoded = json_decode($raw, true);
return is_array($decoded) ? $decoded : [];
}
// 1. GET /api/stats
if ($requestUri === '/api/stats' && $method === 'GET') {
$total = (int)$pdo->query("SELECT COUNT(*) FROM issues")->fetchColumn();
$pending = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'pending'")->fetchColumn();
$reviewing = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'reviewing'")->fetchColumn();
$inProgress = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'in_progress'")->fetchColumn();
$resolved = (int)$pdo->query("SELECT COUNT(*) FROM issues WHERE status = 'resolved'")->fetchColumn();
$upvotes = (int)$pdo->query("SELECT COALESCE(SUM(upvotes), 0) FROM issues")->fetchColumn();
// Helper Pip Calculator
function calculatePips(string $symbol, string $type, float $openPrice, float $closePrice): float {
$sym = strtoupper(trim($symbol));
$type = strtolower(trim($type));
$diff = ($type === 'buy') ? ($closePrice - $openPrice) : ($openPrice - $closePrice);
$catStmt = $pdo->query("SELECT category, COUNT(*) as count FROM issues GROUP BY category ORDER BY count DESC");
$categories = $catStmt->fetchAll();
$distStmt = $pdo->query("SELECT district, COUNT(*) as count FROM issues GROUP BY district ORDER BY district ASC");
$districts = $distStmt->fetchAll();
$prioStmt = $pdo->query("SELECT priority, COUNT(*) as count FROM issues GROUP BY priority");
$priorities = [];
foreach ($prioStmt->fetchAll() as $row) {
$priorities[$row['priority']] = (int)$row['count'];
}
$resolutionRate = $total > 0 ? round(($resolved / $total) * 100, 1) : 0;
jsonResponse([
'total' => $total,
'pending' => $pending,
'reviewing' => $reviewing,
'in_progress' => $inProgress,
'resolved' => $resolved,
'total_upvotes' => $upvotes,
'categories' => $categories,
'districts' => $districts,
'priorities' => $priorities,
'resolution_rate' => $resolutionRate
]);
}
// 2. GET /api/issues
if ($requestUri === '/api/issues' && $method === 'GET') {
$category = $_GET['category'] ?? null;
$status = $_GET['status'] ?? null;
$district = isset($_GET['district']) ? (int)$_GET['district'] : 0;
$priority = $_GET['priority'] ?? null;
$search = $_GET['search'] ?? null;
$sortBy = $_GET['sort_by'] ?? 'newest';
$sql = "SELECT * FROM issues WHERE 1=1";
$params = [];
if ($category && $category !== 'all') {
$sql .= " AND category = ?";
$params[] = $category;
}
if ($status && $status !== 'all') {
$sql .= " AND status = ?";
$params[] = $status;
}
if ($district > 0) {
$sql .= " AND district = ?";
$params[] = $district;
}
if ($priority && $priority !== 'all') {
$sql .= " AND priority = ?";
$params[] = $priority;
}
if ($search) {
$sql .= " AND (title LIKE ? OR description LIKE ? OR address LIKE ? OR tracking_code LIKE ?)";
$searchParam = "%{$search}%";
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
$params[] = $searchParam;
}
if ($sortBy === 'upvotes') {
$sql .= " ORDER BY upvotes DESC, id DESC";
} elseif ($sortBy === 'oldest') {
$sql .= " ORDER BY id ASC";
} elseif ($sortBy === 'emergency') {
$sql .= " ORDER BY CASE priority WHEN 'emergency' THEN 1 WHEN 'high' THEN 2 WHEN 'medium' THEN 3 ELSE 4 END, id DESC";
if (strpos($sym, 'JPY') !== false) {
return round($diff * 100, 1);
} elseif (strpos($sym, 'XAU') !== false || strpos($sym, 'GOLD') !== false) {
return round($diff * 10, 1); // 10 pips per $1.00 move
} elseif (strpos($sym, 'BTC') !== false || strpos($sym, 'ETH') !== false || strpos($sym, 'US30') !== false || strpos($sym, 'NAS') !== false) {
return round($diff, 1); // 1 point
} else {
$sql .= " ORDER BY id DESC";
}
$stmt = $pdo->prepare($sql);
$stmt->execute($params);
$rows = $stmt->fetchAll();
$commentCountStmt = $pdo->prepare("SELECT COUNT(*) FROM comments WHERE issue_id = ?");
$issues = [];
foreach ($rows as $row) {
$commentCountStmt->execute([$row['id']]);
$row['comment_count'] = (int)$commentCountStmt->fetchColumn();
$issues[] = $row;
}
jsonResponse($issues);
}
// 3. POST /api/issues (Create new issue)
if ($requestUri === '/api/issues' && $method === 'POST') {
$data = getJsonInput();
if (empty($data['title']) || empty($data['category']) || empty($data['address'])) {
jsonResponse(['error' => 'اطلاعات ضروری تکمیل نشده است.'], 400);
}
$trackingCode = 'SHR-' . rand(10000, 99999);
$now = date('Y-m-d H:i');
$stmt = $pdo->prepare("INSERT INTO issues (
tracking_code, title, category, description, address, district, priority, status,
lat, lng, image_url, reporter_name, reporter_phone, upvotes, created_at
) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, 1, ?)");
$stmt->execute([
$trackingCode,
$data['title'],
$data['category'],
$data['description'] ?? '',
$data['address'],
(int)($data['district'] ?? 1),
$data['priority'] ?? 'medium',
(float)($data['lat'] ?? 35.7538),
(float)($data['lng'] ?? 51.4172),
$data['image_url'] ?? 'https://images.unsplash.com/photo-1515162816999-a0c47dc192f7?auto=format&fit=crop&w=800&q=80',
$data['reporter_name'] ?? 'شهروند مسئول',
$data['reporter_phone'] ?? '',
$now
]);
$issueId = $pdo->lastInsertId();
// Timeline initial record
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, 'pending', 'ثبت اولیه گزارش در سامانه', 'گزارش شما با موفقیت در سامانه شهرنگار ثبت گردید و در نوبت بررسی کارشناسی قرار گرفت.', ?)");
$tlStmt->execute([$issueId, $now]);
$fetchStmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$fetchStmt->execute([$issueId]);
jsonResponse($fetchStmt->fetch(), 201);
}
// 4. GET /api/track/{code}
if (preg_match('#^/api/track/([^/]+)$#', $requestUri, $matches) && $method === 'GET') {
$code = strtoupper(trim(urldecode($matches[1])));
$stmt = $pdo->prepare("SELECT * FROM issues WHERE UPPER(tracking_code) = ?");
$stmt->execute([$code]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => "هیچ گزارشی با کد رهگیری «{$code}» یافت نشد."], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issue['id']]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issue['id']]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 5. GET /api/issues/{id}
if (preg_match('#^/api/issues/(\d+)$#', $requestUri, $matches) && $method === 'GET') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$stmt->execute([$issueId]);
$issue = $stmt->fetch();
if (!$issue) {
jsonResponse(['error' => 'گزارش مورد نظر یافت نشد.'], 404);
}
$tlStmt = $pdo->prepare("SELECT * FROM timeline WHERE issue_id = ? ORDER BY id ASC");
$tlStmt->execute([$issueId]);
$issue['timeline'] = $tlStmt->fetchAll();
$cmStmt = $pdo->prepare("SELECT * FROM comments WHERE issue_id = ? ORDER BY id DESC");
$cmStmt->execute([$issueId]);
$issue['comments'] = $cmStmt->fetchAll();
jsonResponse($issue);
}
// 6. POST /api/issues/{id}/upvote
if (preg_match('#^/api/issues/(\d+)/upvote$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$stmt = $pdo->prepare("UPDATE issues SET upvotes = upvotes + 1 WHERE id = ?");
$stmt->execute([$issueId]);
if ($stmt->rowCount() === 0) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$fetch = $pdo->prepare("SELECT upvotes FROM issues WHERE id = ?");
$fetch->execute([$issueId]);
$newUpvotes = (int)$fetch->fetchColumn();
jsonResponse(['id' => $issueId, 'upvotes' => $newUpvotes]);
}
// 7. POST /api/issues/{id}/comment
if (preg_match('#^/api/issues/(\d+)/comment$#', $requestUri, $matches) && $method === 'POST') {
$issueId = (int)$matches[1];
$data = getJsonInput();
if (empty($data['content'])) {
jsonResponse(['error' => 'متن نظر نمی‌تواند خالی باشد.'], 400);
}
$check = $pdo->prepare("SELECT id FROM issues WHERE id = ?");
$check->execute([$issueId]);
if (!$check->fetch()) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$now = date('Y-m-d H:i');
$author = !empty($data['author_name']) ? trim($data['author_name']) : 'شهروند محترم';
$stmt = $pdo->prepare("INSERT INTO comments (issue_id, author_name, content, created_at) VALUES (?, ?, ?, ?)");
$stmt->execute([$issueId, $author, trim($data['content']), $now]);
$commentId = $pdo->lastInsertId();
$cm = $pdo->prepare("SELECT * FROM comments WHERE id = ?");
$cm->execute([$commentId]);
jsonResponse($cm->fetch(), 201);
}
// 8. PATCH / POST /api/issues/{id}/status (Admin Status Update)
if (preg_match('#^/api/issues/(\d+)/status$#', $requestUri, $matches) && in_array($method, ['PATCH', 'POST'])) {
$issueId = (int)$matches[1];
$data = getJsonInput();
$check = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$check->execute([$issueId]);
$current = $check->fetch();
if (!$current) {
jsonResponse(['error' => 'گزارش یافت نشد.'], 404);
}
$newStatus = $data['status'] ?? $current['status'];
$officialResponse = $data['official_response'] ?? $current['official_response'];
$resolvedImageUrl = $data['resolved_image_url'] ?? $current['resolved_image_url'];
$now = date('Y-m-d H:i');
$resolvedAt = ($newStatus === 'resolved') ? $now : $current['resolved_at'];
$upd = $pdo->prepare("UPDATE issues SET status = ?, official_response = ?, resolved_image_url = ?, resolved_at = ? WHERE id = ?");
$upd->execute([$newStatus, $officialResponse, $resolvedImageUrl, $resolvedAt, $issueId]);
$statusTitles = [
'pending' => 'در صف بررسی مجدد',
'reviewing' => 'بررسی کارشناسی و ارجاع به معاونت مربوطه',
'in_progress' => 'اعزام اکیپ اجرایی و آغاز عملیات میدانی',
'resolved' => 'اتمام عملیات و رفع کامل مسئله',
'rejected' => 'عدم احراز یا خارج از حیطه اختیارات شهرداری'
];
$tlTitle = !empty($data['timeline_title']) ? $data['timeline_title'] : ($statusTitles[$newStatus] ?? "تغییر وضعیت به {$newStatus}");
$tlDesc = !empty($data['timeline_desc']) ? $data['timeline_desc'] : ($officialResponse ?: 'وضعیت پرونده توسط مدیریت سامانه به‌روزرسانی شد.');
$tlStmt = $pdo->prepare("INSERT INTO timeline (issue_id, status, title, description, created_at) VALUES (?, ?, ?, ?, ?)");
$tlStmt->execute([$issueId, $newStatus, $tlTitle, $tlDesc, $now]);
$updated = $pdo->prepare("SELECT * FROM issues WHERE id = ?");
$updated->execute([$issueId]);
jsonResponse($updated->fetch());
}
// 9. POST /api/upload
if ($requestUri === '/api/upload' && $method === 'POST') {
if (!isset($_FILES['file'])) {
jsonResponse(['error' => 'فایلی ارسال نشده است.'], 400);
}
$uploadDir = __DIR__ . '/static/uploads/';
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0777, true);
}
$ext = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
$filename = uniqid('img_', true) . '.' . $ext;
$target = $uploadDir . $filename;
if (move_uploaded_file($_FILES['file']['tmp_name'], $target)) {
jsonResponse(['url' => '/static/uploads/' . $filename]);
} else {
jsonResponse(['error' => 'خطا در ذخیره‌سازی فایل.'], 500);
return round($diff * 10000, 1); // Standard forex 4/5 digits
}
}
// Default 404 for API
jsonResponse(['error' => 'مسیر مورد نظر در API یافت نشد.'], 404);
try {
// -------------------------------------------------------------
// 1. ACCOUNTS ENDPOINTS
// -------------------------------------------------------------
if (preg_match('#^/api/accounts/?$#', $uri)) {
if ($method === 'GET') {
$stmt = $pdo->query("
SELECT a.*,
(SELECT COUNT(*) FROM trades t WHERE t.account_id = a.id) as total_trades,
(SELECT COALESCE(SUM(profit + commission + swap), 0) FROM trades t WHERE t.account_id = a.id AND t.status = 'closed') as net_closed_profit,
(SELECT COALESCE(SUM(profit + commission + swap), 0) FROM trades t WHERE t.account_id = a.id AND t.status = 'open') as net_open_profit
FROM accounts a ORDER BY a.is_default DESC, a.id ASC
");
$accounts = $stmt->fetchAll();
sendJson(['success' => true, 'accounts' => $accounts]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$name = trim($input['name'] ?? 'حساب معاملاتی جدید');
$broker = trim($input['broker'] ?? 'IC Markets');
$accNum = trim($input['account_number'] ?? '');
$currency = trim($input['currency'] ?? 'USD');
$initBal = (float)($input['initial_balance'] ?? 10000.0);
$leverage = (float)($input['leverage'] ?? 100.0);
$accType = trim($input['account_type'] ?? 'Real');
$isDefault = !empty($input['is_default']) ? 1 : 0;
if ($isDefault) {
$pdo->exec("UPDATE accounts SET is_default = 0");
}
$stmt = $pdo->prepare("
INSERT INTO accounts (name, broker, account_number, currency, initial_balance, leverage, account_type, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([$name, $broker, $accNum, $currency, $initBal, $leverage, $accType, $isDefault]);
$newId = (int)$pdo->lastInsertId();
// Optionally generate sample data
if (!empty($input['with_sample_data'])) {
Database::generateSampleTrades($newId, $initBal);
}
sendJson(['success' => true, 'id' => $newId, 'message' => 'حساب معاملاتی با موفقیت ایجاد شد']);
}
}
if (preg_match('#^/api/accounts/(\d+)$#', $uri, $matches)) {
$accId = (int)$matches[1];
if ($method === 'GET') {
$stmt = $pdo->prepare("SELECT * FROM accounts WHERE id = ?");
$stmt->execute([$accId]);
$acc = $stmt->fetch();
if (!$acc) sendJson(['success' => false, 'error' => 'حساب یافت نشد'], 404);
sendJson(['success' => true, 'account' => $acc]);
} elseif ($method === 'PUT') {
$input = getJsonInput();
$stmt = $pdo->prepare("
UPDATE accounts SET
name = COALESCE(?, name),
broker = COALESCE(?, broker),
account_number = COALESCE(?, account_number),
currency = COALESCE(?, currency),
initial_balance = COALESCE(?, initial_balance),
leverage = COALESCE(?, leverage),
account_type = COALESCE(?, account_type),
is_default = COALESCE(?, is_default),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([
$input['name'] ?? null,
$input['broker'] ?? null,
$input['account_number'] ?? null,
$input['currency'] ?? null,
isset($input['initial_balance']) ? (float)$input['initial_balance'] : null,
isset($input['leverage']) ? (float)$input['leverage'] : null,
$input['account_type'] ?? null,
isset($input['is_default']) ? (int)$input['is_default'] : null,
$accId
]);
sendJson(['success' => true, 'message' => 'حساب به‌روزرسانی شد']);
} elseif ($method === 'DELETE') {
$stmt = $pdo->prepare("DELETE FROM accounts WHERE id = ?");
$stmt->execute([$accId]);
sendJson(['success' => true, 'message' => 'حساب و معاملات آن حذف شدند']);
}
}
// -------------------------------------------------------------
// 2. ANALYTICS ENDPOINT (Myfxbook Style)
// -------------------------------------------------------------
if (preg_match('#^/api/analytics/?$#', $uri)) {
if ($method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 0);
if ($accountId <= 0) {
// Get default account
$stmtDef = $pdo->query("SELECT id FROM accounts ORDER BY is_default DESC, id ASC LIMIT 1");
$accountId = (int)($stmtDef->fetchColumn() ?: 1);
}
$from = $_GET['from'] ?? null;
$to = $_GET['to'] ?? null;
$symbol = $_GET['symbol'] ?? null;
$strategy = $_GET['strategy'] ?? null;
$data = $analyticsEngine->getFullAnalytics($accountId, $from, $to, $symbol, $strategy);
sendJson(['success' => true, 'data' => $data]);
}
}
// -------------------------------------------------------------
// 3. TRADES CRUD ENDPOINTS
// -------------------------------------------------------------
if (preg_match('#^/api/trades/?$#', $uri)) {
if ($method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 0);
if ($accountId <= 0) {
$stmtDef = $pdo->query("SELECT id FROM accounts ORDER BY is_default DESC, id ASC LIMIT 1");
$accountId = (int)($stmtDef->fetchColumn() ?: 1);
}
$page = max(1, (int)($_GET['page'] ?? 1));
$limit = max(5, min(100, (int)($_GET['limit'] ?? 50)));
$offset = ($page - 1) * $limit;
$status = $_GET['status'] ?? 'all';
$symbol = $_GET['symbol'] ?? 'all';
$strategy = $_GET['strategy'] ?? 'all';
$search = trim($_GET['search'] ?? '');
$sortBy = $_GET['sort_by'] ?? 'open_time';
$sortDir = strtolower($_GET['sort_dir'] ?? 'desc') === 'asc' ? 'ASC' : 'DESC';
$allowedSorts = ['open_time', 'close_time', 'profit', 'pips', 'lot_size', 'symbol', 'id'];
if (!in_array($sortBy, $allowedSorts)) $sortBy = 'open_time';
$where = ["account_id = ?"];
$params = [$accountId];
if ($status !== 'all') {
$where[] = "status = ?";
$params[] = $status;
}
if ($symbol !== 'all' && !empty($symbol)) {
$where[] = "symbol = ?";
$params[] = $symbol;
}
if ($strategy !== 'all' && !empty($strategy)) {
$where[] = "strategy = ?";
$params[] = $strategy;
}
if (!empty($search)) {
$where[] = "(symbol LIKE ? OR ticket LIKE ? OR entry_notes LIKE ? OR exit_notes LIKE ? OR tags LIKE ?)";
$sParam = "%$search%";
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
$params[] = $sParam;
}
$whereClause = implode(" AND ", $where);
// Total count
$stmtCount = $pdo->prepare("SELECT COUNT(*) FROM trades WHERE $whereClause");
$stmtCount->execute($params);
$totalCount = (int)$stmtCount->fetchColumn();
// Fetch records
$sql = "SELECT * FROM trades WHERE $whereClause ORDER BY $sortBy $sortDir LIMIT $limit OFFSET $offset";
$stmtList = $pdo->prepare($sql);
$stmtList->execute($params);
$trades = $stmtList->fetchAll();
sendJson([
'success' => true,
'trades' => $trades,
'pagination' => [
'total' => $totalCount,
'page' => $page,
'limit' => $limit,
'total_pages' => ceil($totalCount / $limit)
]
]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$accountId = (int)($input['account_id'] ?? 1);
$symbol = strtoupper(trim($input['symbol'] ?? 'EURUSD'));
$tradeType = strtolower(trim($input['trade_type'] ?? 'buy'));
$lotSize = (float)($input['lot_size'] ?? 0.1);
$openPrice = (float)($input['open_price'] ?? 0.0);
$closePrice = isset($input['close_price']) && $input['close_price'] !== '' ? (float)$input['close_price'] : null;
$stopLoss = isset($input['stop_loss']) && $input['stop_loss'] !== '' ? (float)$input['stop_loss'] : null;
$takeProfit = isset($input['take_profit']) && $input['take_profit'] !== '' ? (float)$input['take_profit'] : null;
$openTime = !empty($input['open_time']) ? date('Y-m-d H:i:s', strtotime($input['open_time'])) : date('Y-m-d H:i:s');
$closeTime = !empty($input['close_time']) ? date('Y-m-d H:i:s', strtotime($input['close_time'])) : null;
$status = trim($input['status'] ?? ($closePrice !== null ? 'closed' : 'open'));
$strategy = trim($input['strategy'] ?? 'Price Action');
$timeframe = trim($input['timeframe'] ?? 'M15');
$session = trim($input['session'] ?? 'London');
$emotion = trim($input['emotion'] ?? 'Disciplined');
$comm = (float)($input['commission'] ?? 0.0);
$swap = (float)($input['swap'] ?? 0.0);
$profit = isset($input['profit']) ? (float)$input['profit'] : 0.0;
$pips = isset($input['pips']) ? (float)$input['pips'] : 0.0;
$rr = isset($input['risk_reward']) ? (float)$input['risk_reward'] : null;
// Auto calculate pips if not provided and closed
if ($closePrice !== null && empty($input['pips'])) {
$pips = calculatePips($symbol, $tradeType, $openPrice, $closePrice);
}
// Auto calculate profit if not provided and closed
if ($closePrice !== null && !isset($input['profit'])) {
// Rough standard lot pip value estimation ($10 per lot per pip for EURUSD)
$pipVal = 10.0 * $lotSize;
$profit = round($pips * $pipVal, 2);
}
$stmt = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, stop_loss, take_profit,
open_time, close_time, pips, profit, commission, swap,
status, strategy, timeframe, session, risk_reward, emotion,
entry_notes, exit_notes, lessons, screenshot_entry, screenshot_exit, tags
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$accountId,
trim($input['ticket'] ?? ''),
$symbol,
$tradeType,
$lotSize,
$openPrice,
$closePrice,
$stopLoss,
$takeProfit,
$openTime,
$closeTime,
$pips,
$profit,
$comm,
$swap,
$status,
$strategy,
$timeframe,
$session,
$rr,
$emotion,
trim($input['entry_notes'] ?? ''),
trim($input['exit_notes'] ?? ''),
trim($input['lessons'] ?? ''),
trim($input['screenshot_entry'] ?? ''),
trim($input['screenshot_exit'] ?? ''),
trim($input['tags'] ?? '')
]);
$tradeId = (int)$pdo->lastInsertId();
sendJson(['success' => true, 'id' => $tradeId, 'message' => 'معامله با موفقیت ثبت شد']);
}
}
if (preg_match('#^/api/trades/(\d+)$#', $uri, $matches)) {
$tradeId = (int)$matches[1];
if ($method === 'GET') {
$stmt = $pdo->prepare("SELECT * FROM trades WHERE id = ?");
$stmt->execute([$tradeId]);
$trade = $stmt->fetch();
if (!$trade) sendJson(['success' => false, 'error' => 'معامله یافت نشد'], 404);
sendJson(['success' => true, 'trade' => $trade]);
} elseif ($method === 'PUT') {
$input = getJsonInput();
$stmtGet = $pdo->prepare("SELECT * FROM trades WHERE id = ?");
$stmtGet->execute([$tradeId]);
$curr = $stmtGet->fetch();
if (!$curr) sendJson(['success' => false, 'error' => 'معامله یافت نشد'], 404);
$symbol = strtoupper(trim($input['symbol'] ?? $curr['symbol']));
$tradeType = strtolower(trim($input['trade_type'] ?? $curr['trade_type']));
$openPrice = isset($input['open_price']) ? (float)$input['open_price'] : (float)$curr['open_price'];
$closePrice = array_key_exists('close_price', $input) ? ($input['close_price'] !== null && $input['close_price'] !== '' ? (float)$input['close_price'] : null) : $curr['close_price'];
$pips = isset($input['pips']) ? (float)$input['pips'] : $curr['pips'];
$profit = isset($input['profit']) ? (float)$input['profit'] : $curr['profit'];
if ($closePrice !== null && (!isset($input['pips']) || $input['pips'] === '')) {
$pips = calculatePips($symbol, $tradeType, $openPrice, (float)$closePrice);
}
$status = $input['status'] ?? ($closePrice !== null ? 'closed' : $curr['status']);
$closeTime = array_key_exists('close_time', $input) ? (!empty($input['close_time']) ? date('Y-m-d H:i:s', strtotime($input['close_time'])) : null) : $curr['close_time'];
$stmt = $pdo->prepare("
UPDATE trades SET
ticket = COALESCE(?, ticket),
symbol = ?,
trade_type = ?,
lot_size = COALESCE(?, lot_size),
open_price = ?,
close_price = ?,
stop_loss = ?,
take_profit = ?,
open_time = COALESCE(?, open_time),
close_time = ?,
pips = ?,
profit = ?,
commission = COALESCE(?, commission),
swap = COALESCE(?, swap),
status = ?,
strategy = COALESCE(?, strategy),
timeframe = COALESCE(?, timeframe),
session = COALESCE(?, session),
risk_reward = ?,
emotion = COALESCE(?, emotion),
entry_notes = COALESCE(?, entry_notes),
exit_notes = COALESCE(?, exit_notes),
lessons = COALESCE(?, lessons),
screenshot_entry = COALESCE(?, screenshot_entry),
screenshot_exit = COALESCE(?, screenshot_exit),
tags = COALESCE(?, tags),
updated_at = CURRENT_TIMESTAMP
WHERE id = ?
");
$stmt->execute([
$input['ticket'] ?? null,
$symbol,
$tradeType,
isset($input['lot_size']) ? (float)$input['lot_size'] : null,
$openPrice,
$closePrice,
isset($input['stop_loss']) && $input['stop_loss'] !== '' ? (float)$input['stop_loss'] : null,
isset($input['take_profit']) && $input['take_profit'] !== '' ? (float)$input['take_profit'] : null,
!empty($input['open_time']) ? date('Y-m-d H:i:s', strtotime($input['open_time'])) : null,
$closeTime,
$pips,
$profit,
isset($input['commission']) ? (float)$input['commission'] : null,
isset($input['swap']) ? (float)$input['swap'] : null,
$status,
$input['strategy'] ?? null,
$input['timeframe'] ?? null,
$input['session'] ?? null,
isset($input['risk_reward']) ? (float)$input['risk_reward'] : null,
$input['emotion'] ?? null,
$input['entry_notes'] ?? null,
$input['exit_notes'] ?? null,
$input['lessons'] ?? null,
$input['screenshot_entry'] ?? null,
$input['screenshot_exit'] ?? null,
$input['tags'] ?? null,
$tradeId
]);
sendJson(['success' => true, 'message' => 'معامله با موفقیت ویرایش شد']);
} elseif ($method === 'DELETE') {
$stmt = $pdo->prepare("DELETE FROM trades WHERE id = ?");
$stmt->execute([$tradeId]);
sendJson(['success' => true, 'message' => 'معامله حذف شد']);
}
}
// -------------------------------------------------------------
// 4. MT4 / MT5 STATEMENT IMPORTER (CSV & HTML)
// -------------------------------------------------------------
if (preg_match('#^/api/trades/import/?$#', $uri) && $method === 'POST') {
$accountId = (int)($_POST['account_id'] ?? 1);
if (!isset($_FILES['statement']) || $_FILES['statement']['error'] !== UPLOAD_ERR_OK) {
sendJson(['success' => false, 'error' => 'فایل گزارش انتخاب نشده یا آپلود ناموفق بود'], 400);
}
$fileTmp = $_FILES['statement']['tmp_name'];
$fileName = $_FILES['statement']['name'];
$content = file_get_contents($fileTmp);
$importedCount = 0;
// Check if file is CSV
if (str_ends_with(strtolower($fileName), '.csv') || strpos($content, ',') !== false && strpos($content, '<html') === false) {
$lines = explode("\n", $content);
$header = [];
foreach ($lines as $lineNum => $line) {
$line = trim($line);
if (empty($line)) continue;
$row = str_getcsv($line);
if ($lineNum === 0 || empty($header)) {
$header = array_map('strtolower', array_map('trim', $row));
continue;
}
if (count($row) < 5) continue;
$rowAssoc = [];
foreach ($header as $idx => $colName) {
$rowAssoc[$colName] = $row[$idx] ?? '';
}
$ticket = $rowAssoc['ticket'] ?? $rowAssoc['order'] ?? (string)rand(1000000, 9999999);
$symbol = strtoupper($rowAssoc['symbol'] ?? $rowAssoc['item'] ?? 'EURUSD');
$type = strtolower($rowAssoc['type'] ?? $rowAssoc['cmd'] ?? 'buy');
if (!in_array($type, ['buy', 'sell'])) continue;
$size = (float)($rowAssoc['size'] ?? $rowAssoc['volume'] ?? $rowAssoc['lots'] ?? 0.1);
$openPrice = (float)($rowAssoc['open price'] ?? $rowAssoc['price'] ?? $rowAssoc['open_price'] ?? 0.0);
$closePrice = (float)($rowAssoc['close price'] ?? $rowAssoc['close_price'] ?? 0.0);
$profit = (float)($rowAssoc['profit'] ?? $rowAssoc['p/l'] ?? 0.0);
$comm = (float)($rowAssoc['commission'] ?? $rowAssoc['taxes'] ?? 0.0);
$swap = (float)($rowAssoc['swap'] ?? 0.0);
$openTime = $rowAssoc['open time'] ?? $rowAssoc['time'] ?? date('Y-m-d H:i:s');
$closeTime = $rowAssoc['close time'] ?? $rowAssoc['time'] ?? date('Y-m-d H:i:s');
$pips = calculatePips($symbol, $type, $openPrice, $closePrice);
$stmtIns = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, open_time, close_time, pips, profit, commission, swap, status, strategy, emotion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'closed', 'Imported MT4/MT5', 'Disciplined')
");
$stmtIns->execute([$accountId, $ticket, $symbol, $type, $size, $openPrice, $closePrice, $openTime, $closeTime, $pips, $profit, $comm, $swap]);
$importedCount++;
}
} else {
// HTML Statement Parser (MT4 / MT5 Standard HTML Reports)
preg_match_all('#<tr[^>]*>(.*?)</tr>#is', $content, $trMatches);
foreach ($trMatches[1] as $trHtml) {
preg_match_all('#<td[^>]*>(.*?)</td>#is', $trHtml, $tdMatches);
$tds = array_map(fn($t) => trim(strip_tags($t)), $tdMatches[1] ?? []);
if (count($tds) >= 10 && (strtolower($tds[2] ?? '') === 'buy' || strtolower($tds[2] ?? '') === 'sell')) {
$ticket = $tds[0];
$openTime = date('Y-m-d H:i:s', strtotime($tds[1]));
$type = strtolower($tds[2]);
$size = (float)$tds[3];
$symbol = strtoupper($tds[4]);
$openPrice = (float)$tds[5];
$sl = (float)($tds[6] ?? 0);
$tp = (float)($tds[7] ?? 0);
$closeTime = isset($tds[8]) ? date('Y-m-d H:i:s', strtotime($tds[8])) : $openTime;
$closePrice = (float)($tds[9] ?? $openPrice);
$comm = (float)($tds[10] ?? 0);
$swap = (float)($tds[11] ?? 0);
$profit = (float)($tds[12] ?? 0);
$pips = calculatePips($symbol, $type, $openPrice, $closePrice);
$stmtIns = $pdo->prepare("
INSERT INTO trades (
account_id, ticket, symbol, trade_type, lot_size,
open_price, close_price, stop_loss, take_profit,
open_time, close_time, pips, profit, commission, swap, status, strategy, emotion
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'closed', 'MT4/MT5 Statement', 'Disciplined')
");
$stmtIns->execute([$accountId, $ticket, $symbol, $type, $size, $openPrice, $closePrice, $sl, $tp, $openTime, $closeTime, $pips, $profit, $comm, $swap]);
$importedCount++;
}
}
}
sendJson(['success' => true, 'imported_count' => $importedCount, 'message' => "تعداد $importedCount معامله با موفقیت ایمپورت شد"]);
}
// -------------------------------------------------------------
// 5. EXPORT TRADES (CSV)
// -------------------------------------------------------------
if (preg_match('#^/api/trades/export/?$#', $uri) && $method === 'GET') {
$accountId = (int)($_GET['account_id'] ?? 1);
$stmt = $pdo->prepare("SELECT * FROM trades WHERE account_id = ? ORDER BY open_time ASC");
$stmt->execute([$accountId]);
$trades = $stmt->fetchAll();
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="forex_trades_' . date('Ymd_His') . '.csv"');
$out = fopen('php://output', 'w');
// UTF-8 BOM for Excel
fputs($out, "\xEF\xBB\xBF");
fputcsv($out, ['ID', 'Ticket', 'Symbol', 'Type', 'Volume', 'Open Price', 'Close Price', 'S/L', 'T/P', 'Open Time', 'Close Time', 'Pips', 'Profit ($)', 'Commission', 'Swap', 'Status', 'Strategy', 'Timeframe', 'Session', 'Emotion', 'Notes']);
foreach ($trades as $t) {
fputcsv($out, [
$t['id'],
$t['ticket'],
$t['symbol'],
$t['trade_type'],
$t['lot_size'],
$t['open_price'],
$t['close_price'],
$t['stop_loss'],
$t['take_profit'],
$t['open_time'],
$t['close_time'],
$t['pips'],
$t['profit'],
$t['commission'],
$t['swap'],
$t['status'],
$t['strategy'],
$t['timeframe'],
$t['session'],
$t['emotion'],
$t['entry_notes']
]);
}
fclose($out);
exit;
}
// -------------------------------------------------------------
// 6. SCREENSHOT IMAGE UPLOAD
// -------------------------------------------------------------
if (preg_match('#^/api/upload/?$#', $uri) && $method === 'POST') {
if (!isset($_FILES['image']) || $_FILES['image']['error'] !== UPLOAD_ERR_OK) {
sendJson(['success' => false, 'error' => 'تصویری ارسال نشد یا خطایی رخ داد'], 400);
}
$file = $_FILES['image'];
$allowed = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'];
if (!in_array($file['type'], $allowed)) {
sendJson(['success' => false, 'error' => 'فرمت تصویر نامعتبر است (تنها JPG, PNG, WEBP مجاز است)'], 400);
}
$uploadDir = __DIR__ . '/uploads';
if (!is_dir($uploadDir)) mkdir($uploadDir, 0777, true);
$ext = pathinfo($file['name'], PATHINFO_EXTENSION);
$newFilename = 'chart_' . uniqid() . '.' . $ext;
$dest = $uploadDir . '/' . $newFilename;
if (move_uploaded_file($file['tmp_name'], $dest)) {
sendJson(['success' => true, 'url' => '/uploads/' . $newFilename]);
} else {
sendJson(['success' => false, 'error' => 'ذخیره تصویر با خطا مواجه شد'], 500);
}
}
// -------------------------------------------------------------
// 7. STRATEGIES & DAILY LOGS
// -------------------------------------------------------------
if (preg_match('#^/api/strategies/?$#', $uri)) {
if ($method === 'GET') {
$stmt = $pdo->query("SELECT * FROM strategies ORDER BY name ASC");
sendJson(['success' => true, 'strategies' => $stmt->fetchAll()]);
} elseif ($method === 'POST') {
$input = getJsonInput();
$name = trim($input['name'] ?? '');
if (empty($name)) sendJson(['success' => false, 'error' => 'نام استراتژی الزامی است'], 400);
$stmt = $pdo->prepare("INSERT OR IGNORE INTO strategies (name, description, color) VALUES (?, ?, ?)");
$stmt->execute([$name, $input['description'] ?? '', $input['color'] ?? '#3b82f6']);
sendJson(['success' => true, 'message' => 'استراتژی ثبت شد']);
}
}
// -------------------------------------------------------------
// 8. RESET & SEED SAMPLE DATA
// -------------------------------------------------------------
if (preg_match('#^/api/reset_sample/?$#', $uri) && $method === 'POST') {
$accountId = (int)($_GET['account_id'] ?? 1);
$pdo->exec("DELETE FROM trades WHERE account_id = $accountId");
Database::generateSampleTrades($accountId, 10000.0);
sendJson(['success' => true, 'message' => 'داده‌های نمونه مای‌اف‌ایکس‌بوک با موفقیت بازیابی شدند']);
}
sendJson(['success' => false, 'error' => 'مسیر مورد نظر یافت نشد (API Not Found)'], 404);
} catch (Throwable $e) {
sendJson(['success' => false, 'error' => 'Server Error: ' . $e->getMessage()], 500);
}