diff --git a/app/src/screens/virtual/VirtualHistoryScreen.tsx b/app/src/screens/virtual/VirtualHistoryScreen.tsx
index d395623..464ea11 100644
--- a/app/src/screens/virtual/VirtualHistoryScreen.tsx
+++ b/app/src/screens/virtual/VirtualHistoryScreen.tsx
@@ -1,5 +1,6 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import type { PaperTradeDto } from '../../lib/shared';
+import { compareTimeAsc } from '@frontend/utils/tradeListSort';
import MobileStackHeader from '../../components/MobileStackHeader';
interface Props {
@@ -9,7 +10,13 @@ interface Props {
}
export default function VirtualHistoryScreen({ market, trades, onBack }: Props) {
- const filtered = trades.filter(t => t.symbol === market).slice(0, 50);
+ const filtered = useMemo(
+ () => trades
+ .filter(t => t.symbol === market)
+ .sort((a, b) => compareTimeAsc(a.createdAt, b.createdAt))
+ .slice(0, 50),
+ [trades, market],
+ );
return (
diff --git a/frontend/src/components/TradeNotificationListPage.tsx b/frontend/src/components/TradeNotificationListPage.tsx
index 14fd0b8..f1fea82 100644
--- a/frontend/src/components/TradeNotificationListPage.tsx
+++ b/frontend/src/components/TradeNotificationListPage.tsx
@@ -45,6 +45,7 @@ import {
getTradeNotificationGridPreset,
type TradeNotificationGridPresetId,
} from '../utils/tradeNotificationGridPresets';
+import { sortTradeNotifications } from '../utils/tradeListSort';
type RightTab = 'trade' | 'orderbook';
@@ -88,24 +89,6 @@ function fillBothSides(
setFillSell({ market, price, side: 'sell', seq: seq + 1 });
}
-function sortNotifications(
- items: TradeNotificationItem[],
- sort: TradeNotificationListSort,
-): TradeNotificationItem[] {
- const arr = [...items];
- if (sort === 'market') {
- return arr.sort((a, b) => {
- const byMarket = a.market.localeCompare(b.market, 'ko');
- if (byMarket !== 0) return byMarket;
- return b.receivedAt - a.receivedAt;
- });
- }
- if (sort === 'oldest') {
- return arr.sort((a, b) => a.receivedAt - b.receivedAt);
- }
- return arr.sort((a, b) => b.receivedAt - a.receivedAt);
-}
-
export const TradeNotificationListPage: React.FC
= ({
theme,
onGoToChart,
@@ -273,7 +256,7 @@ export const TradeNotificationListPage: React.FC = ({
}, [allNotifications, filter, candleFilter]);
const sorted = useMemo(
- () => sortNotifications(filtered, listSort),
+ () => sortTradeNotifications(filtered, listSort),
[filtered, listSort],
);
diff --git a/frontend/src/components/backtest/BacktestExecutionList.tsx b/frontend/src/components/backtest/BacktestExecutionList.tsx
index 8246fa9..b72751f 100644
--- a/frontend/src/components/backtest/BacktestExecutionList.tsx
+++ b/frontend/src/components/backtest/BacktestExecutionList.tsx
@@ -13,6 +13,7 @@ import {
toUpbitMarket,
} from '../../utils/backtestUiUtils';
import { getKoreanName } from '../../utils/marketNameCache';
+import { compareMarketSymbol, compareTimeAsc } from '../../utils/tradeListSort';
export type ExecutionListTab = 'backtest' | 'live';
export type BacktestListSort = 'strategy' | 'symbol' | 'return';
@@ -58,7 +59,13 @@ export default function BacktestExecutionList({
const sortedBacktests = useMemo(() => {
const list = [...backtestRecords];
if (sort === 'strategy') list.sort((a, b) => (a.strategyName ?? '').localeCompare(b.strategyName ?? ''));
- else if (sort === 'symbol') list.sort((a, b) => a.symbol.localeCompare(b.symbol));
+ else if (sort === 'symbol') {
+ list.sort((a, b) => {
+ const bySymbol = compareMarketSymbol(a.symbol, b.symbol);
+ if (bySymbol !== 0) return bySymbol;
+ return compareTimeAsc(a.createdAt, b.createdAt);
+ });
+ }
else list.sort((a, b) => (b.totalReturn ?? 0) - (a.totalReturn ?? 0));
return list;
}, [backtestRecords, sort]);
@@ -117,7 +124,13 @@ export default function BacktestExecutionList({
가상투자 화면에서 자동·수동
매매를 실행하세요.
) : (
- liveItems.map(item => {
+ [...liveItems]
+ .sort((a, b) => {
+ const bySymbol = compareMarketSymbol(a.symbol, b.symbol);
+ if (bySymbol !== 0) return bySymbol;
+ return compareTimeAsc(a.createdAt, b.createdAt);
+ })
+ .map(item => {
const positive = item.totalReturnPct >= 0;
const active = selectedLiveId === item.id;
const market = toUpbitMarket(item.symbol);
diff --git a/frontend/src/components/paper/PaperTradeHistoryList.tsx b/frontend/src/components/paper/PaperTradeHistoryList.tsx
index fe66707..b585d5c 100644
--- a/frontend/src/components/paper/PaperTradeHistoryList.tsx
+++ b/frontend/src/components/paper/PaperTradeHistoryList.tsx
@@ -1,8 +1,9 @@
-import React from 'react';
+import React, { useMemo } from 'react';
import type { PaperTradeDto } from '../../utils/backendApi';
import type { TickerData } from '../../hooks/useMarketTicker';
import { resolveVirtualTargetNames } from '../../utils/virtualTargetNames';
import { fmtKrw } from '../TradeOrderPanel';
+import { sortPaperTradesForDisplay } from '../../utils/tradeListSort';
function coinCode(symbol: string): string {
return symbol.replace(/^KRW-/, '');
@@ -43,7 +44,10 @@ const PaperTradeHistoryList: React.FC = ({
selectedTradeId = null,
onSelectTrade,
onSelectMarket,
-}) => (
+}) => {
+ const displayTrades = useMemo(() => sortPaperTradesForDisplay(trades), [trades]);
+
+ return (
@@ -56,7 +60,7 @@ const PaperTradeHistoryList: React.FC
= ({
) : (
- {trades.map(t => {
+ {displayTrades.map(t => {
const { koreanName: ko, englishName: en } = resolveVirtualTargetNames(
t.symbol,
tickers?.get(t.symbol)?.koreanName,
@@ -164,6 +168,7 @@ const PaperTradeHistoryList: React.FC
= ({
)}
-);
+ );
+};
export default PaperTradeHistoryList;
diff --git a/frontend/src/utils/tradeListSort.ts b/frontend/src/utils/tradeListSort.ts
new file mode 100644
index 0000000..c0de24f
--- /dev/null
+++ b/frontend/src/utils/tradeListSort.ts
@@ -0,0 +1,57 @@
+import type { PaperTradeDto } from './backendApi';
+import type { TradeNotificationItem } from '../contexts/TradeNotificationContext';
+import type { TradeNotificationListSort } from './tradeNotificationListLayout';
+
+/** 종목(마켓) 코드 비교 — 한글 정렬 */
+export function compareMarketSymbol(a: string, b: string): number {
+ return a.localeCompare(b, 'ko');
+}
+
+/** epoch ms·ISO 문자열·초 단위 타임스탬프 → 오름차순(이전 → 최근) */
+export function compareTimeAsc(
+ a: number | string | null | undefined,
+ b: number | string | null | undefined,
+): number {
+ return toTimeMs(a) - toTimeMs(b);
+}
+
+function toTimeMs(t: number | string | null | undefined): number {
+ if (t == null) return 0;
+ if (typeof t === 'number') {
+ if (!Number.isFinite(t)) return 0;
+ return t < 1e12 ? t * 1000 : t;
+ }
+ const d = new Date(t);
+ return Number.isNaN(d.getTime()) ? 0 : d.getTime();
+}
+
+/** 종목순 → 동일 종목은 체결·수신 시각 오름차순 (매수·매도 흐름) */
+export function sortByMarketThenTimeAsc
(
+ items: readonly T[],
+ getMarket: (item: T) => string,
+ getTime: (item: T) => number | string | null | undefined,
+): T[] {
+ return [...items].sort((a, b) => {
+ const byMarket = compareMarketSymbol(getMarket(a), getMarket(b));
+ if (byMarket !== 0) return byMarket;
+ return compareTimeAsc(getTime(a), getTime(b));
+ });
+}
+
+export function sortPaperTradesForDisplay(trades: readonly PaperTradeDto[]): PaperTradeDto[] {
+ return sortByMarketThenTimeAsc(trades, t => t.symbol, t => t.createdAt);
+}
+
+export function sortTradeNotifications(
+ items: TradeNotificationItem[],
+ sort: TradeNotificationListSort,
+): TradeNotificationItem[] {
+ const arr = [...items];
+ if (sort === 'market') {
+ return sortByMarketThenTimeAsc(arr, n => n.market, n => n.receivedAt);
+ }
+ if (sort === 'oldest') {
+ return arr.sort((a, b) => compareTimeAsc(a.receivedAt, b.receivedAt));
+ }
+ return arr.sort((a, b) => compareTimeAsc(b.receivedAt, a.receivedAt));
+}