188 lines
6.3 KiB
TypeScript
188 lines
6.3 KiB
TypeScript
/**
|
|
* 분석레포트 — 거래 이력 타임라인 뷰 (목록 / 좌우 분할)
|
|
*/
|
|
import React from 'react';
|
|
import {
|
|
LIVE_TIMELINE_KIND_LABEL,
|
|
LIVE_TIMELINE_SIDE_LABEL,
|
|
type LiveTimelineEvent,
|
|
} from '../../utils/liveTradeTimeline';
|
|
import { formatSignalPrice } from '../../utils/tradeSignalDisplay';
|
|
import { resolveVirtualTargetNames } from '../../utils/virtualTargetNames';
|
|
import TimelineBarTimeBox, { eventToBarTimeSec } from './TimelineBarTimeBox';
|
|
import TimelineSignalCardShell from './TimelineSignalCardShell';
|
|
import type { Timeframe } from '../../types';
|
|
|
|
export type LiveTimelineViewMode = 'list' | 'split' | 'axis';
|
|
|
|
export const LIVE_TIMELINE_VIEW_OPTIONS: { id: LiveTimelineViewMode; label: string }[] = [
|
|
{ id: 'list', label: '세로 목록' },
|
|
{ id: 'split', label: '좌우 분할' },
|
|
{ id: 'axis', label: '봉 시간축' },
|
|
];
|
|
|
|
function kindClass(kind: LiveTimelineEvent['kind']): string {
|
|
if (kind === 'SIGNAL') return 'signal';
|
|
if (kind === 'ORDER_PENDING') return 'pending';
|
|
return 'fill';
|
|
}
|
|
|
|
function sideClass(side: LiveTimelineEvent['side']): 'buy' | 'sell' {
|
|
return side === 'BUY' ? 'buy' : 'sell';
|
|
}
|
|
|
|
interface EventBodyProps {
|
|
ev: LiveTimelineEvent;
|
|
compact?: boolean;
|
|
}
|
|
|
|
export const TimelineEventBody: React.FC<EventBodyProps> = ({ ev, compact = false }) => {
|
|
const { koreanName } = resolveVirtualTargetNames(ev.symbol);
|
|
const kCls = kindClass(ev.kind);
|
|
const sCls = sideClass(ev.side);
|
|
|
|
return (
|
|
<>
|
|
<div className="arp-timeline-card-tags">
|
|
<span className={`arp-timeline-kind arp-timeline-kind--${kCls}`}>
|
|
{LIVE_TIMELINE_KIND_LABEL[ev.kind]}
|
|
</span>
|
|
<span className={`arp-timeline-side arp-timeline-side--${sCls}`}>
|
|
{LIVE_TIMELINE_SIDE_LABEL[ev.side]}
|
|
</span>
|
|
</div>
|
|
<div className={`arp-timeline-card-title${compact ? ' arp-timeline-card-title--compact' : ''}`}>
|
|
{koreanName || ev.symbol}
|
|
<span className="arp-timeline-symbol">{ev.symbol.replace(/^KRW-/, '')}</span>
|
|
</div>
|
|
<div className="arp-timeline-meta">
|
|
{ev.price > 0 && <span>{formatSignalPrice(ev.price)}</span>}
|
|
{ev.quantity != null && ev.quantity > 0 && (
|
|
<span>{ev.quantity.toLocaleString(undefined, { maximumFractionDigits: 8 })}</span>
|
|
)}
|
|
{ev.strategyName && <span>{ev.strategyName}</span>}
|
|
{ev.sourceLabel && <span>{ev.sourceLabel}</span>}
|
|
{ev.detail && <span className="arp-timeline-detail">{ev.detail}</span>}
|
|
</div>
|
|
</>
|
|
);
|
|
};
|
|
|
|
interface ListViewProps {
|
|
events: LiveTimelineEvent[];
|
|
timeframe: Timeframe;
|
|
onSignalDetail?: (ev: LiveTimelineEvent) => void;
|
|
}
|
|
|
|
export const TimelineListView: React.FC<ListViewProps> = ({ events, timeframe, onSignalDetail }) => (
|
|
<div className="btd-timeline arp-timeline-scroll arp-timeline-scroll--list" aria-label="거래 이력 타임라인">
|
|
<div className="btd-timeline-line" aria-hidden />
|
|
{events.map(ev => {
|
|
const kCls = kindClass(ev.kind);
|
|
const sCls = sideClass(ev.side);
|
|
const barTimeSec = eventToBarTimeSec(ev.ts, timeframe);
|
|
return (
|
|
<article
|
|
key={ev.id}
|
|
className={[
|
|
'btd-timeline-item',
|
|
'arp-timeline-item',
|
|
'arp-timeline-item--bar-anchor',
|
|
`arp-timeline-item--${kCls}`,
|
|
`arp-timeline-item--${sCls}`,
|
|
].join(' ')}
|
|
>
|
|
<TimelineBarTimeBox
|
|
barTimeSec={barTimeSec}
|
|
timeframe={timeframe}
|
|
side={sCls}
|
|
variant="compact"
|
|
className="arp-timeline-bar-time--list-node"
|
|
/>
|
|
<TimelineSignalCardShell
|
|
ev={ev}
|
|
className="btd-timeline-card"
|
|
onSignalDetail={onSignalDetail}
|
|
>
|
|
<TimelineEventBody ev={ev} />
|
|
</TimelineSignalCardShell>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
|
|
interface SplitViewProps {
|
|
events: LiveTimelineEvent[];
|
|
timeframe: Timeframe;
|
|
onSignalDetail?: (ev: LiveTimelineEvent) => void;
|
|
}
|
|
|
|
export const TimelineSplitView: React.FC<SplitViewProps> = ({ events, timeframe, onSignalDetail }) => (
|
|
<div className="arp-timeline-split-scroll" aria-label="좌우 분할 거래 이력 타임라인">
|
|
<div className="arp-timeline-split-track">
|
|
<div className="arp-timeline-split-axis" aria-hidden />
|
|
{events.map(ev => {
|
|
const isBuy = ev.side === 'BUY';
|
|
const kCls = kindClass(ev.kind);
|
|
const sCls = sideClass(ev.side);
|
|
const barTimeSec = eventToBarTimeSec(ev.ts, timeframe);
|
|
const cardCls = [
|
|
'arp-timeline-split-card',
|
|
`arp-timeline-split-card--${sCls}`,
|
|
`arp-timeline-split-card--${kCls}`,
|
|
isBuy ? 'arp-timeline-split-card--left' : 'arp-timeline-split-card--right',
|
|
].join(' ');
|
|
|
|
const card = (
|
|
<TimelineSignalCardShell
|
|
ev={ev}
|
|
className={cardCls}
|
|
onSignalDetail={onSignalDetail}
|
|
>
|
|
<TimelineEventBody ev={ev} compact />
|
|
</TimelineSignalCardShell>
|
|
);
|
|
|
|
return (
|
|
<div
|
|
key={ev.id}
|
|
className={`arp-timeline-split-row arp-timeline-split-row--${sCls}`}
|
|
>
|
|
<div className="arp-timeline-split-col arp-timeline-split-col--left">
|
|
{isBuy ? (
|
|
<div className="arp-timeline-split-stack arp-timeline-split-stack--buy">
|
|
{card}
|
|
<div
|
|
className="arp-timeline-split-connector arp-timeline-split-connector--buy"
|
|
aria-hidden
|
|
/>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
<div className="arp-timeline-split-col arp-timeline-split-col--center">
|
|
<TimelineBarTimeBox
|
|
barTimeSec={barTimeSec}
|
|
timeframe={timeframe}
|
|
side={sCls}
|
|
variant="compact"
|
|
className="arp-timeline-bar-time--split-node"
|
|
/>
|
|
</div>
|
|
<div className="arp-timeline-split-col arp-timeline-split-col--right">
|
|
{!isBuy ? (
|
|
<div className="arp-timeline-split-stack arp-timeline-split-stack--sell">
|
|
<div
|
|
className="arp-timeline-split-connector arp-timeline-split-connector--sell"
|
|
aria-hidden
|
|
/>
|
|
{card}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
); |