Files
web-page/src/pages/stock/stockUtils.js
gahusb 18d2cd5a51 feat(stock): 포트폴리오 매입가/평균단가 분리 + 총 매입 금액 반영
- 기존 카드의 "매입가" → "평균단가" (avg_price) 로 라벨 변경
- 신규 "매입가" (purchase_price) 컬럼 추가. 추가/수정 폼에 입력 필드 노출
  (미입력 시 평균단가 값으로 자동 설정)
- 브로커별 총 매입 금액은 purchase_price × quantity 합계 기준
- 손익/수익률은 평균단가(avg_price) 기준 유지

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-15 01:58:10 +09:00

127 lines
4.2 KiB
JavaScript

/* ── 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 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()),
});
/* ── TAB IDs ─────────────────────────────────────────────────────── */
export const TAB_PORTFOLIO = 'portfolio';
export const TAB_AI = 'ai';
export const TAB_REPORT = 'report';
export const TAB_ADVISOR = 'advisor';