Files
web-page/src/pages/stock/stockUtils.js
gahusb 6533743100 fix(stock): 총 매입을 각 종목 매입가의 단순 합으로 표시
요약카드(백엔드 매입가×수량)와 증권사별(매입가 단순 합) 총 매입이 서로
달라 혼란. 박재오 정의대로 총 매입 = Σ매입가(수량 미곱산)로 통일.
getBrokerSummary를 stockUtils.computeBrokerSummary로 추출(테스트 5건),
usePortfolio가 portfolioSummary.total_buy를 프론트 단순 합으로 override해
요약카드·증권사별·AI 프롬프트가 동일 값 사용. 손익은 avg_price×수량 유지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 11:15:58 +09:00

152 lines
5.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ── helpers ─────────────────────────────────────────────────────── */
export const formatNumber = (value) => {
if (value === null || value === undefined || value === '') return '-';
const numeric = Number(value);
if (Number.isNaN(numeric)) return value;
return new Intl.NumberFormat('ko-KR').format(numeric);
};
export const formatPercent = (value) => {
if (value === null || value === undefined || value === '') return '-';
if (typeof value === 'string' && value.includes('%')) return value;
const numeric = Number(value);
if (Number.isNaN(numeric)) return value;
return `${numeric >= 0 ? '+' : ''}${numeric.toFixed(2)}%`;
};
export const pickFirst = (...values) =>
values.find((value) => value !== undefined && value !== null && value !== '');
export const getQty = (item) =>
pickFirst(item?.qty, item?.quantity, item?.holding, item?.hold_qty);
export const getBuyPrice = (item) =>
pickFirst(
item?.buy_price,
item?.avg_price,
item?.avg,
item?.purchase_price,
item?.buyPrice,
item?.price
);
export const getCurrentPrice = (item) =>
pickFirst(
item?.current_price,
item?.current,
item?.cur_price,
item?.now_price,
item?.market_price
);
export const getProfitRate = (item) =>
pickFirst(
item?.profit_rate,
item?.profitRate,
item?.profit_pct,
item?.profitPercent,
item?.pnl_rate,
item?.return_rate,
item?.yield
);
export const getProfitLoss = (item) =>
pickFirst(item?.profit_loss, item?.pnl, item?.profitLoss);
export const toNumeric = (value) => {
if (value === null || value === undefined || value === '') return null;
const numeric = Number(String(value).replace(/[^0-9.-]/g, ''));
return Number.isNaN(numeric) ? null : numeric;
};
/* ── Chart colors ──────────────────────────────────────────────── */
export const CHART_COLORS = ['#818cf8', '#fbbf24', '#34d399', '#f472b6', '#fb923c', '#a78bfa', '#38bdf8', '#4ade80'];
export const profitColorClass = (numericValue) => {
if (numericValue > 0) return 'is-up';
if (numericValue < 0) return 'is-down';
if (numericValue === 0) return 'is-flat';
return '';
};
export const numFitClass = (text) => {
const len = String(text ?? '').length;
if (len >= 13) return 'is-fit-xs';
if (len >= 10) return 'is-fit-sm';
return '';
};
export const getVixLabel = (vix) => {
if (vix < 12) return '극히 낮음 (안일 주의)';
if (vix < 20) return '정상 (안정적)';
if (vix < 30) return '주의 (불확실성 증가)';
if (vix < 40) return '높음 (극도의 공포)';
return '극단 (패닉)';
};
export const getFgLabel = (score) => {
if (score <= 25) return '극단적 공포';
if (score <= 45) return '공포';
if (score <= 55) return '중립';
if (score <= 75) return '탐욕';
return '극단적 탐욕';
};
/* ── empty portfolio form ────────────────────────────────────────── */
export const emptyPortfolioForm = {
broker: '',
ticker: '',
name: '',
quantity: '',
avg_price: '',
purchase_price: '',
};
/* ── empty sell-history form ─────────────────────────────────────── */
export const toLocalDatetimeValue = (isoStr) => {
if (!isoStr) return '';
const d = new Date(isoStr);
const pad = (n) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
export const emptySellForm = () => ({
broker: '',
ticker: '',
name: '',
quantity: '',
avg_price: '',
sell_price: '',
commission: '',
sold_at: toLocalDatetimeValue(new Date().toISOString()),
});
/* ── 증권사별 요약 집계 ──────────────────────────────────────────── */
// totalBuy: 총 매입 = 각 종목 매입가(purchase_price)의 단순 합 (수량 미곱산, 박재오 정의).
// 매입가 미설정 시 avg_price 폴백. 백엔드 total_buy(×수량)는 표시에 쓰지 않음.
// totalCostBasis: 손익 계산용 매입원가 = SUM(avg_price × quantity) — 손익은 수량 곱산 유지.
export const computeBrokerSummary = (items) => {
let totalBuy = 0, totalCostBasis = 0, totalEval = 0, hasNullPrice = false;
for (const item of items) {
const qty = item.quantity ?? 0;
const purchase = item.purchase_price ?? item.avg_price ?? 0;
totalBuy += purchase;
totalCostBasis += (item.avg_price ?? 0) * qty;
if (item.eval_amount != null) totalEval += item.eval_amount;
else hasNullPrice = true;
}
const totalProfit = totalEval - totalCostBasis;
const totalProfitRate = totalCostBasis > 0 ? (totalProfit / totalCostBasis) * 100 : 0;
return { totalBuy, totalEval, totalProfit, totalProfitRate, hasNullPrice };
};
/* ── TAB IDs ─────────────────────────────────────────────────────── */
export const TAB_PORTFOLIO = 'portfolio';
export const TAB_REPORT = 'report';
export const TAB_ADVISOR = 'advisor';