모의투자 관리 기능 추가
This commit is contained in:
@@ -17,6 +17,8 @@ export interface TradeOrderPanelProps {
|
||||
/** 현재가 (KRW) — 종목 변경 시 초기값 */
|
||||
tradePrice: number | null;
|
||||
availableKrw?: number;
|
||||
/** 종목별 추가 매수 가능 (한도 기준) */
|
||||
availableSymbolBuyKrw?: number;
|
||||
/** 매도 시 보유 수량 (모의투자) */
|
||||
availableCoinQty?: number;
|
||||
/** 호가·차트에서 자동 입력 */
|
||||
@@ -89,6 +91,7 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
market,
|
||||
tradePrice,
|
||||
availableKrw = 0,
|
||||
availableSymbolBuyKrw,
|
||||
availableCoinQty = 0,
|
||||
fillRequest,
|
||||
searchAnchorRef,
|
||||
@@ -99,6 +102,9 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
onPaperOrderFilled,
|
||||
}) => {
|
||||
const isBuy = side === 'buy';
|
||||
const buyBudgetKrw = isBuy && availableSymbolBuyKrw != null
|
||||
? Math.min(availableKrw, availableSymbolBuyKrw)
|
||||
: availableKrw;
|
||||
const displayMarket = (fillRequest?.side === side ? fillRequest.market : null) ?? market;
|
||||
const code = coinCode(displayMarket);
|
||||
const koreanName = getKoreanName(displayMarket);
|
||||
@@ -156,8 +162,8 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
const applyPct = useCallback((pct: number) => {
|
||||
setPctMode(pct);
|
||||
if (isBuy) {
|
||||
if (orderKind === 'market' || availableKrw <= 0) return;
|
||||
const budget = (availableKrw * pct) / 100;
|
||||
if (orderKind === 'market' || buyBudgetKrw <= 0) return;
|
||||
const budget = (buyBudgetKrw * pct) / 100;
|
||||
const p = orderKind === 'limit' && price > 0 ? price : refPrice;
|
||||
if (p > 0) {
|
||||
const q = budget / p;
|
||||
@@ -168,7 +174,7 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
if (availableCoinQty <= 0) return;
|
||||
const q = (availableCoinQty * pct) / 100;
|
||||
setQtyStr(q >= 1 ? q.toFixed(8).replace(/\.?0+$/, '') : q.toFixed(8));
|
||||
}, [isBuy, availableKrw, availableCoinQty, orderKind, price, refPrice]);
|
||||
}, [isBuy, buyBudgetKrw, availableCoinQty, orderKind, price, refPrice]);
|
||||
|
||||
const bumpPrice = useCallback((delta: number) => {
|
||||
const base = price > 0 ? price : refPrice;
|
||||
@@ -202,26 +208,36 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const trade = await placePaperOrder({
|
||||
const result = await placePaperOrder({
|
||||
market: displayMarket,
|
||||
side: isBuy ? 'BUY' : 'SELL',
|
||||
orderKind: orderKind === 'market' ? 'market' : 'limit',
|
||||
orderType: orderKind === 'market' ? 'MARKET' : 'LIMIT',
|
||||
price: execPrice,
|
||||
quantity: qty,
|
||||
source: 'MANUAL',
|
||||
});
|
||||
if (!trade) {
|
||||
if (!result?.trade && !result?.order) {
|
||||
window.alert('모의 주문에 실패했습니다.');
|
||||
return;
|
||||
}
|
||||
onPaperOrderFilled?.();
|
||||
window.alert(
|
||||
`[모의투자] ${koreanName || code} ${label} 체결\n` +
|
||||
`가격: ${fmtKrw(trade.price)} KRW\n` +
|
||||
`수량: ${trade.quantity} ${code}\n` +
|
||||
`수수료: ${fmtKrw(trade.feeAmount)} KRW\n` +
|
||||
`체결 후 현금: ${fmtKrw(trade.cashAfter)} KRW`,
|
||||
);
|
||||
if (result.order) {
|
||||
window.alert(
|
||||
`[모의투자] ${koreanName || code} ${label} 미체결 등록\n` +
|
||||
`지정가: ${fmtKrw(result.order.limitPrice ?? execPrice)} KRW\n` +
|
||||
`수량: ${result.order.quantity} ${code}`,
|
||||
);
|
||||
} else if (result.trade) {
|
||||
const trade = result.trade;
|
||||
window.alert(
|
||||
`[모의투자] ${koreanName || code} ${label} 체결\n` +
|
||||
`가격: ${fmtKrw(trade.price)} KRW\n` +
|
||||
`수량: ${trade.quantity} ${code}\n` +
|
||||
`수수료: ${fmtKrw(trade.feeAmount)} KRW\n` +
|
||||
`체결 후 현금: ${fmtKrw(trade.cashAfter)} KRW`,
|
||||
);
|
||||
}
|
||||
setQtyStr('');
|
||||
setPctMode(null);
|
||||
} catch (e) {
|
||||
@@ -245,8 +261,12 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
const minOrder = 5000;
|
||||
const feeRate = 0.05;
|
||||
|
||||
const balanceDisplay = isBuy
|
||||
? `${buyBudgetKrw > 0 ? fmtKrw(buyBudgetKrw) : '-'} KRW`
|
||||
: `${availableCoinQty > 0 ? availableCoinQty.toFixed(8).replace(/\.?0+$/, '') : '0'} ${code}`;
|
||||
|
||||
return (
|
||||
<div className={`top-panel top-panel--${side}`}>
|
||||
<div className={`top-panel top-panel--upbit top-panel--${side}`}>
|
||||
<div className="top-panel-fields">
|
||||
{paperTradingEnabled && !paperAutoTradeEnabled && showSymbolField && (
|
||||
<p className="top-paper-hint">자동매매 OFF · 수동 주문만 모의 계좌에 체결됩니다.</p>
|
||||
@@ -317,71 +337,70 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-row top-row--balance">
|
||||
<div
|
||||
className="top-row top-row--balance"
|
||||
title={isBuy && availableSymbolBuyKrw != null
|
||||
? `종목별 한도 ${fmtKrw(availableSymbolBuyKrw)} KRW`
|
||||
: undefined}
|
||||
>
|
||||
<span className="top-label">{isBuy ? '주문가능' : '보유수량'}</span>
|
||||
<span className="top-balance">
|
||||
{isBuy
|
||||
? `${fmtKrw(availableKrw)} KRW`
|
||||
: `${availableCoinQty > 0 ? availableCoinQty.toFixed(8).replace(/\.?0+$/, '') : '0'} ${code}`}
|
||||
</span>
|
||||
<span className="top-balance">{balanceDisplay}</span>
|
||||
</div>
|
||||
|
||||
<div className="top-price-qty-block">
|
||||
<div className="top-field top-field--price">
|
||||
<label className="top-label">{priceLabel} (KRW)</label>
|
||||
<div className="top-input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="top-input"
|
||||
value={priceStr}
|
||||
disabled={orderKind === 'market'}
|
||||
onChange={handlePriceChange}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9,]*"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button type="button" className="top-step" onClick={() => bumpPrice(-step)} disabled={orderKind === 'market'}>−</button>
|
||||
<button type="button" className="top-step" onClick={() => bumpPrice(step)} disabled={orderKind === 'market'}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-field top-field--qty">
|
||||
<label className="top-label">주문수량 ({code})</label>
|
||||
<div className="top-field top-field--price">
|
||||
<label className="top-label">{priceLabel} (KRW)</label>
|
||||
<div className="top-input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="top-input top-input--full"
|
||||
value={qtyStr}
|
||||
className="top-input"
|
||||
value={priceStr}
|
||||
disabled={orderKind === 'market'}
|
||||
onChange={handleQtyChange}
|
||||
inputMode="decimal"
|
||||
pattern="[0-9.]*"
|
||||
onChange={handlePriceChange}
|
||||
inputMode="numeric"
|
||||
pattern="[0-9,]*"
|
||||
autoComplete="off"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="top-pct-row top-pct-row--block">
|
||||
{PCT_BTNS.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={`top-pct-btn${pctMode === p ? ' top-pct-btn--active' : ''}`}
|
||||
onClick={() => applyPct(p)}
|
||||
>
|
||||
{p === 100 ? '최대' : `${p}%`}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`top-pct-btn top-pct-btn--direct${pctMode === -1 ? ' top-pct-btn--active' : ''}`}
|
||||
onClick={() => setPctMode(-1)}
|
||||
>
|
||||
직접입력
|
||||
</button>
|
||||
<button type="button" className="top-step" onClick={() => bumpPrice(-step)} disabled={orderKind === 'market'}>−</button>
|
||||
<button type="button" className="top-step" onClick={() => bumpPrice(step)} disabled={orderKind === 'market'}>+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="top-field">
|
||||
<div className="top-field top-field--qty">
|
||||
<label className="top-label">주문수량 ({code})</label>
|
||||
<input
|
||||
type="text"
|
||||
className="top-input top-input--full"
|
||||
value={qtyStr}
|
||||
disabled={orderKind === 'market'}
|
||||
onChange={handleQtyChange}
|
||||
inputMode="decimal"
|
||||
pattern="[0-9.]*"
|
||||
autoComplete="off"
|
||||
placeholder="0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="top-pct-row top-pct-row--under-qty">
|
||||
{PCT_BTNS.map(p => (
|
||||
<button
|
||||
key={p}
|
||||
type="button"
|
||||
className={`top-pct-btn${pctMode === p ? ' top-pct-btn--active' : ''}`}
|
||||
onClick={() => applyPct(p)}
|
||||
>
|
||||
{`${p}%`}
|
||||
</button>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`top-pct-btn top-pct-btn--direct${pctMode === -1 ? ' top-pct-btn--active' : ''}`}
|
||||
onClick={() => setPctMode(-1)}
|
||||
>
|
||||
직접입력
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="top-field top-field--total">
|
||||
<label className="top-label">주문총액 (KRW)</label>
|
||||
<input
|
||||
type="text"
|
||||
@@ -393,11 +412,10 @@ const TradeOrderPanel: React.FC<TradeOrderPanelProps> = ({
|
||||
|
||||
<div className="top-meta">
|
||||
<span className="top-meta-line">
|
||||
<span>조건: 보통 ▾</span>
|
||||
<span className="top-meta-sep">·</span>
|
||||
<span>최소 {fmtKrw(minOrder)}</span>
|
||||
<span>최소주문: {fmtKrw(minOrder)} KRW</span>
|
||||
<span className="top-meta-sep">·</span>
|
||||
<span>수수료 {feeRate}%</span>
|
||||
<span>수수료(부가세 포함): {feeRate}%</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user