분석레포트 타임라인 표시
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* 분석레포트 — 거래 이력 타임라인 뷰 (목록 / 좌우 분할)
|
||||
*/
|
||||
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 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;
|
||||
}
|
||||
|
||||
export const TimelineListView: React.FC<ListViewProps> = ({ events, timeframe }) => (
|
||||
<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"
|
||||
/>
|
||||
<div className="btd-timeline-card">
|
||||
<TimelineEventBody ev={ev} />
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
interface SplitViewProps {
|
||||
events: LiveTimelineEvent[];
|
||||
timeframe: Timeframe;
|
||||
}
|
||||
|
||||
export const TimelineSplitView: React.FC<SplitViewProps> = ({ events, timeframe }) => (
|
||||
<div className="arp-timeline-split-scroll" aria-label="좌우 분할 거래 이력 타임라인">
|
||||
<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 = (
|
||||
<article className={cardCls}>
|
||||
<TimelineEventBody ev={ev} compact />
|
||||
</article>
|
||||
);
|
||||
|
||||
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>
|
||||
);
|
||||
Reference in New Issue
Block a user