mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-08-22 15:18:09 +00:00
feat(web): add Next.js portfolio with live MT5 dashboard
- Linear design system (dark canvas, lavender accent) - Live account stats, open positions, pending orders (30s polling) - Trade history with equity curve chart - Backtest results table - Proxy API routes — MT5_BASE_URL server-side only, never exposed to browser - Deploy on Vercel: set MT5_BASE_URL env var Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
4fdc3da4ed
commit
9716fc0955
@@ -0,0 +1,22 @@
|
||||
export function formatCurrency(n: number, currency = "USD"): string {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency", currency,
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
}).format(n);
|
||||
}
|
||||
|
||||
export function formatPct(n: number, decimals = 1): string {
|
||||
return `${n >= 0 ? "+" : ""}${n.toFixed(decimals)}%`;
|
||||
}
|
||||
|
||||
export function formatDate(iso: string): string {
|
||||
return new Date(iso).toLocaleString("en-US", {
|
||||
month: "short", day: "numeric",
|
||||
hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
export function formatPrice(n: number, digits = 2): string {
|
||||
return n.toFixed(digits);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Server-side only — bridge URL never leaves this file
|
||||
|
||||
async function apiFetch<T>(path: string): Promise<T> {
|
||||
const base = process.env.MT5_BASE_URL ?? "http://localhost:8000";
|
||||
const res = await fetch(`${base}${path}`, {
|
||||
next: { revalidate: 0 }, // always fresh
|
||||
});
|
||||
if (!res.ok) throw new Error(`MT5 bridge ${path} → ${res.status}`);
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AccountInfo {
|
||||
login: number;
|
||||
balance: number;
|
||||
equity: number;
|
||||
profit: number;
|
||||
margin: number;
|
||||
margin_free: number;
|
||||
margin_level: number;
|
||||
currency: string;
|
||||
leverage: number;
|
||||
name: string;
|
||||
server: string;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
ticket: number;
|
||||
symbol: string;
|
||||
type: number; // 0=buy, 1=sell
|
||||
volume: number;
|
||||
price_open: number;
|
||||
price_current: number;
|
||||
sl: number;
|
||||
tp: number;
|
||||
profit: number;
|
||||
swap: number;
|
||||
comment: string;
|
||||
magic: number;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export interface PendingOrder {
|
||||
ticket: number;
|
||||
symbol: string;
|
||||
type: number; // 2=BUY_LIMIT, 3=SELL_LIMIT, 4=BUY_STOP, 5=SELL_STOP
|
||||
volume_initial: number;
|
||||
volume_current: number;
|
||||
price_open: number;
|
||||
price_current: number;
|
||||
sl: number;
|
||||
tp: number;
|
||||
magic: number;
|
||||
comment: string;
|
||||
time_setup: string;
|
||||
}
|
||||
|
||||
export interface Deal {
|
||||
ticket: number;
|
||||
order: number;
|
||||
time: string;
|
||||
type: number; // 0=buy, 1=sell, 2=balance
|
||||
entry: number; // 0=in, 1=out
|
||||
symbol: string;
|
||||
volume: number;
|
||||
price: number;
|
||||
commission: number;
|
||||
swap: number;
|
||||
profit: number;
|
||||
comment: string;
|
||||
magic: number;
|
||||
}
|
||||
|
||||
// ── Wrappers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
interface DataVec<T> { data: T[]; count: number }
|
||||
|
||||
export async function getAccount(): Promise<AccountInfo> {
|
||||
const w = await apiFetch<DataVec<AccountInfo>>("/account");
|
||||
const item = w.data[0];
|
||||
if (!item) throw new Error("empty /account response");
|
||||
return item;
|
||||
}
|
||||
|
||||
export async function getPositions(): Promise<Position[]> {
|
||||
const w = await apiFetch<DataVec<Position>>("/positions");
|
||||
return w.data;
|
||||
}
|
||||
|
||||
export async function getOrders(symbol?: string): Promise<PendingOrder[]> {
|
||||
const path = symbol ? `/orders?symbol=${encodeURIComponent(symbol)}` : "/orders";
|
||||
const w = await apiFetch<DataVec<PendingOrder>>(path);
|
||||
return w.data;
|
||||
}
|
||||
|
||||
export async function getDeals(dateFrom: string, dateTo: string, symbol?: string): Promise<Deal[]> {
|
||||
let path = `/history/deals?date_from=${encodeURIComponent(dateFrom)}&date_to=${encodeURIComponent(dateTo)}`;
|
||||
if (symbol) path += `&symbol=${encodeURIComponent(symbol)}`;
|
||||
const w = await apiFetch<DataVec<Deal>>(path);
|
||||
return w.data;
|
||||
}
|
||||
Reference in New Issue
Block a user