-
Notifications
You must be signed in to change notification settings - Fork 348
Home
import React, { useState, useEffect } from 'react'; import './App.css'; // Pastikan Anda memiliki file CSS
function App() { // State simulasi harga real-time const [price, setPrice] = useState(45000.00);
// Simulasi WebSocket (Data Feed) useEffect(() => { const interval = setInterval(() => { // Harga berubah acak setiap detik setPrice(prev => prev + (Math.random() - 0.5) * 10); }, 1000); return () => clearInterval(interval); }, []);
return (
{/* Main Content Layout */}
<main className="dashboard">
{/* Area Grafik */}
<section className="chart-section">
<h2>BTC/IDR: ${price.toFixed(2)}</h2>
<div className="chart-placeholder">
{/* Di sini nantinya integrasi TradingView Advanced Charts */}
<p>Grafik Harga (Real-time)</p>
</div>
</section>
{/* Panel Trading */}
<aside className="trading-panel">
<h3>Order Book</h3>
<div className="order-form">
<button className="buy-btn">BUY</button>
<button className="sell-btn">SELL</button>
<input type="number" placeholder="Jumlah" />
<button className="submit-btn">Execute Order</button>
</div>
</aside>
</main>
</div>
); }
export default App;
import React, { ALL.TRED } from "react"; import "./App.css"; // optional for body styling
// ── Defaults ──────────────────────────────────────────────────────────────── const OPEN_PRICE = 4100; const DEFAULT_STEP = 5; const DEFAULT_BUFFER = 25; const LOT = 0.05; const TICK_MS = 500; const TOTAL_TICKS = 1000; const SQUAREOFF_TICK = 180; const WARN_TICK = 165;
// ── Helpers ─────────────────────────────────────────────────────────────────
const fmtTime = (t) => ${String(Math.floor(t / 60)).padStart(2, "0")}:${String(t % 60).padStart(2, "0")};
const calcPnL = (filled, price) =>
filled.reduce((s, o) => s + (o.side === "BUY" ? price - o.fillPrice : o.fillPrice - price) * o.lot * 200, 0);
function buildOrders(buf, step = DEFAULT_STEP) {
const buys = [];
const sells = [];
for (let i = 1; i <= buf; i++) {
buys.push({ id: B${i}, side: "BUY", price: OPEN_PRICE + i * step, lot: LOT, status: "PENDING" });
sells.push({ id: S${i}, side: "SELL", price: OPEN_PRICE - i * step, lot: LOT, status: "PENDING" });
}
return { buys, sells };
}
function generatePath(len = 600, open = OPEN_PRICE) { const pts = [open]; let p = open; for (let i = 1; i <= len; i++) { p = Math.round((p + (Math.random() - 0.5) * 4) * 100) / 100; pts.push(p); } return pts; }
// ── Component ───────────────────────────────────────────────────────────────
export default function App() {
const [step, setStep] = useState(DEFAULT_STEP);
const [buffer, setBuffer] = useState(DEFAULT_BUFFER);
const [profitTarget, setProfitTarget] = useState(50);
const [running, setRunning] = useState(false);
const [tick, setTick] = useState(0);
const [path] = useState(() => generatePath(TOTAL_TICKS, OPEN_PRICE));
const [buyOrders, setBuyOrders] = useState(() => buildOrders(DEFAULT_BUFFER, DEFAULT_STEP).buys);
const [sellOrders, setSellOrders] = useState(() => buildOrders(DEFAULT_BUFFER, DEFAULT_STEP).sells);
const [filled, setFilled] = useState([]);
const [log, setLog] = useState([{ t: 0, txt: Market open @ ${OPEN_PRICE}, type: "info" }]);
const buyCounter = useRef(buffer + 1);
const sellCounter = useRef(buffer + 1);
const intervalRef = useRef(null);
const [finalPnL, setFinalPnL] = useState(null);
const [closed, setClosed] = useState(false);
const currentPrice = path[tick] ?? OPEN_PRICE; const secsLeft = Math.max(0, SQUAREOFF_TICK - tick); const isWarn = tick >= WARN_TICK && !closed;
// ticker useEffect(() => { if (!running || closed) return; intervalRef.current = setInterval(() => { setTick((t) => { if (t >= path.length - 1) { clearInterval(intervalRef.current); setRunning(false); return t; } return t + 1; }); }, TICK_MS); return () => clearInterval(intervalRef.current); }, [running, closed, path.length]);
// order engine per tick useEffect(() => { if (tick === 0 || closed) return; const price = path[tick]; const time = fmtTime(tick);
let newBuys = [...buyOrders];
let newSells = [...sellOrders];
let newFilled = [...filled];
let events = [];
// process fills
newBuys = newBuys.map((o) => {
if (o.status === "PENDING" && price >= o.price) {
const fo = { ...o, status: "FILLED", fillPrice: o.price, fillTick: tick };
newFilled.push(fo);
events.push({ t: tick, txt: `BUY FILLED @ ${o.price} (mkt ${price.toFixed(2)})`, type: "buy" });
return fo;
}
return o;
});
newSells = newSells.map((o) => {
if (o.status === "PENDING" && price <= o.price) {
const fo = { ...o, status: "FILLED", fillPrice: o.price, fillTick: tick };
newFilled.push(fo);
events.push({ t: tick, txt: `SELL FILLED @ ${o.price} (mkt ${price.toFixed(2)})`, type: "sell" });
return fo;
}
return o;
});
// replenish buys to keep exactly buffer pending
const pendingBuyPrices = newBuys.filter((x) => x.status === "PENDING").map((x) => x.price);
const pendingCountBuy = pendingBuyPrices.length;
if (pendingCountBuy < buffer) {
const allBuyPrices = newBuys.map((b) => b.price);
const highest = allBuyPrices.length ? Math.max(...allBuyPrices) : OPEN_PRICE;
for (let i = 0; i < buffer - pendingCountBuy; i++) {
const newP = highest + step * (i + 1);
const id = `B${buyCounter.current++}`;
newBuys.push({ id, side: "BUY", price: newP, lot: LOT, status: "PENDING" });
events.push({ t: tick, txt: `ADD BUY ${id} @ ${newP}`, type: "add" });
}
}
// replenish sells
const pendingSellPrices = newSells.filter((x) => x.status === "PENDING").map((x) => x.price);
const pendingCountSell = pendingSellPrices.length;
if (pendingCountSell < buffer) {
const allSellPrices = newSells.map((s) => s.price);
const lowest = allSellPrices.length ? Math.min(...allSellPrices) : OPEN_PRICE;
for (let i = 0; i < buffer - pendingCountSell; i++) {
const newP = lowest - step * (i + 1);
const id = `S${sellCounter.current++}`;
newSells.push({ id, side: "SELL", price: newP, lot: LOT, status: "PENDING" });
events.push({ t: tick, txt: `ADD SELL ${id} @ ${newP}`, type: "add" });
}
}
// trim order arrays (prevent memory leak)
if (newBuys.length > buffer * 6) newBuys = newBuys.slice(-buffer * 3);
if (newSells.length > buffer * 6) newSells = newSells.slice(-buffer * 3);
// apply
setBuyOrders(newBuys);
setSellOrders(newSells);
setFilled(newFilled);
if (events.length) setLog((l) => [...l, ...events.map((e, i) => ({ t: e.t, txt: e.txt, type: e.type }))]);
// profit target
const pnl = calcPnL(newFilled, price);
if (newFilled.length > 0 && pnl >= profitTarget) {
setFinalPnL(pnl);
setClosed(true);
setRunning(false);
setLog((l) => [...l, { t: tick, txt: `PROFIT TARGET HIT @ ${price.toFixed(2)} PnL=${pnl.toFixed(2)}`, type: "close" }]);
return;
}
// auto square-off
if (tick >= SQUAREOFF_TICK) {
const pnl2 = calcPnL(newFilled, price);
setFinalPnL(pnl2);
setClosed(true);
setRunning(false);
setLog((l) => [...l, { t: tick, txt: `AUTO SQUARE-OFF @ ${price.toFixed(2)} PnL=${pnl2.toFixed(2)}`, type: "close" }]);
return;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [tick]); // intentionally only on tick
// reset
function reset() {
clearInterval(intervalRef.current);
setRunning(false);
setTick(0);
setBuyOrders(buildOrders(buffer, step).buys);
setSellOrders(buildOrders(buffer, step).sells);
setFilled([]);
setLog([{ t: 0, txt: Market open @ ${OPEN_PRICE}, type: "info" }]);
buyCounter.current = buffer + 1;
sellCounter.current = buffer + 1;
setFinalPnL(null);
setClosed(false);
}
// small UI pieces
const pendingBuys = buyOrders.filter((o) => o.status === "PENDING");
const pendingSells = sellOrders.filter((o) => o.status === "PENDING");
const visiblePath = path.slice(0, tick + 1);
const minP = Math.min(...path) - 10;
const maxP = Math.max(...path) + 10;
const CW = 600,
CH = 180;
const px = (i) => (i / (path.length - 1)) * CW;
const py = (pr) => CH - ((pr - minP) / (maxP - minP)) * CH;
const pathD = visiblePath.map((p, i) => ${i === 0 ? "M" : "L"}${px(i).toFixed(1)},${py(p).toFixed(1)}).join(" ");
return ( <div style={{ fontFamily: "Inter, system-ui, -apple-system, sans-serif", padding: 14, background: "#071027", color: "#cbd5e1", minHeight: "100vh" }}> <header style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 12 }}>
<div style={{ display: "grid", gridTemplateColumns: "1fr 320px", gap: 12 }}>
<main>
<section style={{ background: "#071430", padding: 12, borderRadius: 10, marginBottom: 12, border: "1px solid #10243a" }}>
<div style={{ display: "flex", justifyContent: "space-between", marginBottom: 8 }}>
<div>
<div style={{ fontSize: 10, color: "#94a3b8" }}>OPEN</div>
<div style={{ fontWeight: 800, color: "#fbbf24", fontSize: 18 }}>${OPEN_PRICE}</div>
</div>
<div>
<div style={{ fontSize: 10, color: "#94a3b8" }}>LIVE</div>
<div style={{ fontWeight: 900, color: currentPrice >= OPEN_PRICE ? "#22c55e" : "#ef4444", fontSize: 18 }}>${currentPrice.toFixed(2)}</div>
</div>
<div>
<div style={{ fontSize: 10, color: "#94a3b8" }}>FLOAT P&L</div>
<div style={{ fontWeight: 900, color: calcPnL(filled, currentPrice) >= 0 ? "#22c55e" : "#ef4444", fontSize: 18 }}>
${calcPnL(filled, currentPrice).toFixed(2)}
</div>
</div>
<div>
<div style={{ fontSize: 10, color: "#94a3b8" }}>TIME</div>
<div style={{ fontWeight: 900, color: "#a78bfa", fontSize: 18 }}>{fmtTime(tick)}</div>
</div>
</div>
<div style={{ marginTop: 8 }}>
<svg viewBox={`0 0 ${CW} ${CH + 24}`} width="100%" style={{ background: "#051025", borderRadius: 6 }}>
<line x1={0} y1={py(OPEN_PRICE)} x2={CW} y2={py(OPEN_PRICE)} stroke="#f59e0b" strokeDasharray="6,3" strokeWidth="1" />
<text x={CW - 8} y={py(OPEN_PRICE) - 6} fill="#f59e0b" fontSize="10" textAnchor="end">OPEN {OPEN_PRICE}</text>
{pendingBuys.slice(-6).map((o) => (
<g key={o.id}>
<line x1={0} y1={py(o.price)} x2={CW} y2={py(o.price)} stroke="#22c55e" strokeDasharray="4,4" strokeWidth="0.6" opacity="0.5" />
<text x={6} y={py(o.price) - 4} fill="#22c55e" fontSize="9">{o.id}@{o.price}</text>
</g>
))}
{pendingSells.slice(0, 6).map((o) => (
<g key={o.id}>
<line x1={0} y1={py(o.price)} x2={CW} y2={py(o.price)} stroke="#ef4444" strokeDasharray="4,4" strokeWidth="0.6" opacity="0.5" />
<text x={6} y={py(o.price) - 4} fill="#ef4444" fontSize="9">{o.id}@{o.price}</text>
</g>
))}
{filled.map((o, idx) => (
<circle key={o.id + idx} cx={px(o.fillTick)} cy={py(o.fillPrice)} r="3.5" fill={o.side === "BUY" ? "#22c55e" : "#ef4444"} />
))}
{visiblePath.length > 1 && <path d={pathD} fill="none" stroke={currentPrice >= OPEN_PRICE ? "#22c55e" : "#ef4444"} strokeWidth="1.6" />}
<circle cx={px(tick)} cy={py(currentPrice)} r="5" fill={currentPrice >= OPEN_PRICE ? "#22c55e" : "#ef4444"} />
</svg>
</div>
</section>
<section style={{ display: "flex", gap: 8 }}>
<div style={{ flex: 1, background: "#071430", padding: 12, borderRadius: 10, border: "1px solid #10243a" }}>
<h4 style={{ margin: 0, color: "#94a3b8" }}>BUY ORDERS (pending)</h4>
<div style={{ maxHeight: 240, overflowY: "auto", marginTop: 8 }}>
{pendingBuys.slice(-100).reverse().map((o) => (
<div key={o.id} style={{ display: "flex", justifyContent: "space-between", padding: "6px 8px", borderRadius: 6, background: "#041218", marginBottom: 6 }}>
<div style={{ color: "#22c55e", fontWeight: 800 }}>{o.id}</div>
<div style={{ color: "#94a3b8" }}>${o.price}</div>
</div>
))}
</div>
</div>
<div style={{ width: 260, background: "#071430", padding: 12, borderRadius: 10, border: "1px solid #10243a" }}>
<h4 style={{ margin: 0, color: "#94a3b8" }}>SELL ORDERS (pending)</h4>
<div style={{ maxHeight: 240, overflowY: "auto", marginTop: 8 }}>
{pendingSells.slice(0, 100).map((o) => (
<div key={o.id} style={{ display: "flex", justifyContent: "space-between", padding: "6px 8px", borderRadius: 6, background: "#041218", marginBottom: 6 }}>
<div style={{ color: "#ef4444", fontWeight: 800 }}>{o.id}</div>
<div style={{ color: "#94a3b8" }}>${o.price}</div>
</div>
))}
</div>
</div>
</section>
</main>
<aside>
<div style={{ background: "#071430", padding: 12, borderRadius: 10, border: "1px solid #10243a", marginBottom: 12 }}>
<div style={{ marginBottom: 8 }}>
<label style={{ display: "block", fontSize: 12, color: "#94a3b8" }}>Step (pts)</label>
<input type="range" min="1" max="20" value={step} onChange={(e) => setStep(+e.target.value)} />
<div style={{ fontSize: 12, color: "#cbd5e1" }}>{step} pts</div>
</div>
<div style={{ marginBottom: 8 }}>
<label style={{ display: "block", fontSize: 12, color: "#94a3b8" }}>Buffer</label>
<input type="range" min="5" max="60" value={buffer} onChange={(e) => setBuffer(+e.target.value)} />
<div style={{ fontSize: 12, color: "#cbd5e1" }}>{buffer} pending per side</div>
</div>
<div style={{ marginBottom: 8 }}>
<label style={{ display: "block", fontSize: 12, color: "#94a3b8" }}>Profit Target ($)</label>
<input type="range" min="10" max="500" value={profitTarget} onChange={(e) => setProfitTarget(+e.target.value)} />
<div style={{ fontSize: 12, color: "#cbd5e1" }}>${profitTarget}</div>
</div>
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
<button
onClick={() => {
// apply settings: rebuild buffers (keeps counters)
setBuyOrders(buildOrders(buffer, step).buys);
setSellOrders(buildOrders(buffer, step).sells);
buyCounter.current = buffer + 1;
sellCounter.current = buffer + 1;
setLog((l) => [...l, { t: tick, txt: `Settings applied: buffer=${buffer} step=${step}`, type: "info" }]);
}}
style={{ padding: "8px 10px", background: "#0b1220", color: "#cbd5e1", border: "1px solid #20304a", borderRadius: 8 }}
>
Apply
</button>
<button
onClick={() => {
setBuyOrders((b) => [...b, { id: `B${buyCounter.current++}`, side: "BUY", price: OPEN_PRICE + step, lot: LOT, status: "PENDING" }]);
}}
style={{ padding: "8px 10px", background: "#06231a", color: "#22c55e", borderRadius: 8, border: "1px solid #153a2a" }}
>
Add BUY
</button>
<button
onClick={() => {
setSellOrders((s) => [...s, { id: `S${sellCounter.current++}`, side: "SELL", price: OPEN_PRICE - step, lot: LOT, status: "PENDING" }]);
}}
style={{ padding: "8px 10px", background: "#2a0610", color: "#ef4444", borderRadius: 8, border: "1px solid #3a1016" }}
>
Add SELL
</button>
</div>
<div style={{ marginTop: 10, fontSize: 12, color: "#94a3b8" }}>
<div>Tick: {tick}</div>
<div>Pending BUY: {pendingBuys.length}</div>
<div>Pending SELL: {pendingSells.length}</div>
<div>Filled: