跳至主要内容

交易历史

StableFX 中的每笔外汇交易都遵循从报价请求(RFQ)到最终结算的定义生命周期。本页介绍 ForexTradeRecord 类型、ForexTradeStatus 生命周期,以及如何获取和显示交易历史。

交易生命周期

StableFX 交易按照以下状态流转:

  RFQ ──> QUOTED ──> MATCHED ──> SETTLED
| | |
| | +──> FAILED
| +──> FAILED
+──> FAILED
状态描述
RFQ报价请求已提交。系统正在从流动性池中获取报价。
QUOTED已收到报价。价格在短时间窗口内被锁定。
MATCHED交易已与对手方匹配。结算进行中。
SETTLED交易成功完成。资金已分配。
FAILED交易在任何阶段失败。清算池中的资金将被退回。

ForexTradeStatus 类型

import type { ForexTradeStatus } from '@one_deploy/sdk';

type ForexTradeStatus = 'RFQ' | 'QUOTED' | 'MATCHED' | 'SETTLED' | 'FAILED';

ForexTradeRecord 类型

import type { ForexTradeRecord } from '@one_deploy/sdk';

interface ForexTradeRecord {
/** Unique trade identifier. */
id: string;

/** The investment this trade belongs to. */
investmentId: string;

/** Currency pair traded, e.g. "EUR/USD". */
currencyPair: string;

/** Trade direction. */
side: 'buy' | 'sell';

/** Trade amount in base currency units. */
amount: number;

/** Quoted price at time of trade. */
quotePrice: number;

/** Execution price (may differ from quotePrice due to slippage). */
executionPrice: number | null;

/** Current trade status. */
status: ForexTradeStatus;

/** Realized profit or loss (in USDC), set after settlement. */
pnl: number | null;

/** Fee charged for this trade (in USDC). */
fee: number;

/** Slippage between quote and execution price (decimal). */
slippage: number | null;

/** ISO-8601 timestamp when the RFQ was submitted. */
createdAt: string;

/** ISO-8601 timestamp when the trade was last updated. */
updatedAt: string;

/** ISO-8601 timestamp when the trade was settled, if applicable. */
settledAt: string | null;

/** On-chain transaction hash, if applicable. */
txHash: string | null;

/** Reason for failure, if status is FAILED. */
failureReason: string | null;
}

获取交易历史

交易记录可通过 useForexInvestments hook 的投资数据获取,也可直接通过 useForexTrading hook 获取。

使用 useForexTrading

import { useForexTrading } from '@one_deploy/sdk';

function useTradeHistory() {
const { getTradeHistory } = useForexTrading();

const fetchTrades = async (investmentId: string) => {
const trades = await getTradeHistory({
investmentId,
limit: 50,
offset: 0,
});
return trades;
};

return { fetchTrades };
}

完整的交易历史页面

import React, { useEffect, useState } from 'react';
import { View, Text, FlatList, StyleSheet, ActivityIndicator } from 'react-native';
import { useForexTrading } from '@one_deploy/sdk';
import type { ForexTradeRecord, ForexTradeStatus } from '@one_deploy/sdk';

function TradeHistoryScreen({ investmentId }: { investmentId: string }) {
const { getTradeHistory } = useForexTrading();
const [trades, setTrades] = useState<ForexTradeRecord[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
getTradeHistory({ investmentId, limit: 100 })
.then(setTrades)
.finally(() => setLoading(false));
}, [investmentId, getTradeHistory]);

if (loading) return <ActivityIndicator size="large" style={{ marginTop: 40 }} />;

return (
<FlatList
data={trades}
keyExtractor={(t) => t.id}
ListHeaderComponent={
<Text style={styles.header}>
Trade History ({trades.length} trades)
</Text>
}
renderItem={({ item }) => <TradeRow trade={item} />}
ListEmptyComponent={
<Text style={styles.empty}>No trades found.</Text>
}
/>
);
}

function TradeRow({ trade }: { trade: ForexTradeRecord }) {
const statusColor = getStatusColor(trade.status);

return (
<View style={styles.row}>
<View style={styles.rowLeft}>
<View style={styles.rowHeader}>
<Text style={styles.pair}>{trade.currencyPair}</Text>
<Text style={[styles.side, trade.side === 'buy' ? styles.buy : styles.sell]}>
{trade.side.toUpperCase()}
</Text>
</View>
<Text style={styles.amount}>
{trade.amount.toLocaleString()} units @ {trade.quotePrice}
</Text>
{trade.executionPrice && (
<Text style={styles.exec}>
Executed @ {trade.executionPrice}
{trade.slippage !== null &&
` (slippage: ${(trade.slippage * 100).toFixed(3)}%)`}
</Text>
)}
<Text style={styles.date}>
{new Date(trade.createdAt).toLocaleString()}
</Text>
</View>

<View style={styles.rowRight}>
<Text style={[styles.status, { color: statusColor }]}>
{trade.status}
</Text>
{trade.pnl !== null && (
<Text
style={[
styles.pnl,
{ color: trade.pnl >= 0 ? '#44cc88' : '#cc4444' },
]}
>
{trade.pnl >= 0 ? '+' : ''}${trade.pnl.toFixed(2)}
</Text>
)}
{trade.status === 'FAILED' && trade.failureReason && (
<Text style={styles.failReason}>{trade.failureReason}</Text>
)}
</View>
</View>
);
}

function getStatusColor(status: ForexTradeStatus): string {
switch (status) {
case 'RFQ': return '#ffaa44';
case 'QUOTED': return '#44aaff';
case 'MATCHED': return '#aa88ff';
case 'SETTLED': return '#44cc88';
case 'FAILED': return '#cc4444';
default: return '#888888';
}
}

const styles = StyleSheet.create({
header: { fontSize: 18, fontWeight: '700', color: '#fff', padding: 16 },
empty: { color: '#666', textAlign: 'center', padding: 40 },
row: {
flexDirection: 'row',
justifyContent: 'space-between',
padding: 14,
borderBottomWidth: 1,
borderColor: '#1a1a2e',
},
rowLeft: { flex: 1 },
rowRight: { alignItems: 'flex-end', justifyContent: 'center' },
rowHeader: { flexDirection: 'row', alignItems: 'center', gap: 8 },
pair: { fontSize: 15, fontWeight: '700', color: '#fff' },
side: { fontSize: 11, fontWeight: '700', paddingHorizontal: 6, paddingVertical: 2, borderRadius: 4 },
buy: { backgroundColor: '#1a3a2a', color: '#44cc88' },
sell: { backgroundColor: '#3a1a1a', color: '#cc4444' },
amount: { fontSize: 13, color: '#aaa', marginTop: 4 },
exec: { fontSize: 12, color: '#888', marginTop: 2 },
date: { fontSize: 11, color: '#666', marginTop: 4 },
status: { fontSize: 12, fontWeight: '700' },
pnl: { fontSize: 14, fontWeight: '600', fontFamily: 'monospace', marginTop: 4 },
failReason: { fontSize: 11, color: '#cc4444', marginTop: 2, maxWidth: 120 },
});

追踪交易状态

你可以轮询进行中交易的状态更新:

import { useForexTrading } from '@one_deploy/sdk';
import type { ForexTradeRecord } from '@one_deploy/sdk';

function useTradeStatusPolling(tradeId: string, intervalMs: number = 3000) {
const { getTradeById } = useForexTrading();
const [trade, setTrade] = useState<ForexTradeRecord | null>(null);

useEffect(() => {
let active = true;

const poll = async () => {
const updated = await getTradeById(tradeId);
if (!active) return;
setTrade(updated);

// Stop polling when trade reaches a terminal status
if (updated.status === 'SETTLED' || updated.status === 'FAILED') {
return;
}

setTimeout(poll, intervalMs);
};

poll();
return () => { active = false; };
}, [tradeId, intervalMs, getTradeById]);

return trade;
}

// Usage
function TradeStatusTracker({ tradeId }: { tradeId: string }) {
const trade = useTradeStatusPolling(tradeId);

if (!trade) return <Text>Loading...</Text>;

return (
<View style={{ padding: 16 }}>
<Text style={{ color: '#fff', fontSize: 16 }}>
Trade {trade.id.slice(0, 8)}...
</Text>
<Text style={{ color: getStatusColor(trade.status), marginTop: 8 }}>
Status: {trade.status}
</Text>
{trade.status === 'SETTLED' && (
<Text style={{ color: '#44cc88', marginTop: 4 }}>
Settled at {new Date(trade.settledAt!).toLocaleString()}
</Text>
)}
</View>
);
}

交易记录摘要

字段可用时机描述
idcurrencyPairsideamountRFQ交易创建时设置
quotePriceQUOTED收到报价时设置
executionPriceslippageMATCHED交易匹配时设置
pnlsettledAttxHashSETTLED成功结算时设置
failureReasonFAILED交易失败时设置

后续步骤