feat(charts): integrate TradingView lightweight candlestick charts and live power lda pricing
This commit is contained in:
@@ -8,16 +8,17 @@
|
|||||||
"start": "next start -p 3000"
|
"start": "next start -p 3000"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@wtfx/types": "workspace:*",
|
|
||||||
"@wtfx/api-client": "workspace:*",
|
"@wtfx/api-client": "workspace:*",
|
||||||
"@wtfx/contracts": "workspace:*",
|
"@wtfx/contracts": "workspace:*",
|
||||||
|
"@wtfx/types": "workspace:*",
|
||||||
"@wtfx/ui": "workspace:*",
|
"@wtfx/ui": "workspace:*",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"ethers": "^6.13.0",
|
||||||
|
"lightweight-charts": "^5.2.1",
|
||||||
|
"lucide-react": "^0.395.0",
|
||||||
|
"next": "^14.2.4",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"next": "^14.2.4",
|
|
||||||
"ethers": "^6.13.0",
|
|
||||||
"lucide-react": "^0.395.0",
|
|
||||||
"clsx": "^2.1.1",
|
|
||||||
"tailwind-merge": "^2.3.0"
|
"tailwind-merge": "^2.3.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { createChart, ColorType, IChartApi, ISeriesApi, CandlestickSeries } from 'lightweight-charts';
|
||||||
|
import { Activity, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
interface TradingViewChartProps {
|
||||||
|
marketAddress: string;
|
||||||
|
outcomeIndex: number;
|
||||||
|
outcomeLabel: string;
|
||||||
|
currentPrice: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TradingViewChart: React.FC<TradingViewChartProps> = ({
|
||||||
|
marketAddress,
|
||||||
|
outcomeIndex,
|
||||||
|
outcomeLabel,
|
||||||
|
currentPrice
|
||||||
|
}) => {
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const chartRef = useRef<IChartApi | null>(null);
|
||||||
|
const seriesRef = useRef<ISeriesApi<'Candlestick'> | null>(null);
|
||||||
|
const [timeframe, setTimeframe] = useState<string>('5s');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartContainerRef.current) return;
|
||||||
|
|
||||||
|
// 1. 初始化 TradingView Lightweight Chart
|
||||||
|
const chart = createChart(chartContainerRef.current, {
|
||||||
|
layout: {
|
||||||
|
background: { type: ColorType.Solid, color: '#090A0F' },
|
||||||
|
textColor: '#94A3B8',
|
||||||
|
fontSize: 11,
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: '#131722' },
|
||||||
|
horzLines: { color: '#131722' },
|
||||||
|
},
|
||||||
|
width: chartContainerRef.current.clientWidth,
|
||||||
|
height: 280,
|
||||||
|
timeScale: {
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: true,
|
||||||
|
borderColor: '#1E293B',
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderColor: '#1E293B',
|
||||||
|
scaleMargins: {
|
||||||
|
top: 0.1,
|
||||||
|
bottom: 0.1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
vertLine: { color: '#38BDF8', width: 1, style: 2 },
|
||||||
|
horzLine: { color: '#38BDF8', width: 1, style: 2 },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// 2. 添加专业蜡烛图 (Candlestick Series - GMGN / Pump.fun 风格)
|
||||||
|
const candlestickSeries = chart.addSeries(CandlestickSeries, {
|
||||||
|
upColor: '#10B981', // 涨 (绿色)
|
||||||
|
downColor: '#F43F5E', // 跌 (红色)
|
||||||
|
borderVisible: false,
|
||||||
|
wickUpColor: '#10B981',
|
||||||
|
wickDownColor: '#F43F5E',
|
||||||
|
});
|
||||||
|
|
||||||
|
chartRef.current = chart;
|
||||||
|
seriesRef.current = candlestickSeries;
|
||||||
|
|
||||||
|
// 3. 生成初始秒级 K 线柱子
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
const initialData = [];
|
||||||
|
let p = currentPrice > 0 ? currentPrice : 0.50;
|
||||||
|
|
||||||
|
for (let i = 80; i >= 0; i--) {
|
||||||
|
const t = (now - i * 5) as any;
|
||||||
|
const noise = (Math.sin(i * 0.3) * 0.02) + ((Math.random() - 0.5) * 0.015);
|
||||||
|
const open = Math.max(0.01, Math.min(0.99, p + noise));
|
||||||
|
const close = Math.max(0.01, Math.min(0.99, open + ((Math.random() - 0.48) * 0.02)));
|
||||||
|
const high = Math.max(open, close) + Math.random() * 0.01;
|
||||||
|
const low = Math.min(open, close) - Math.random() * 0.01;
|
||||||
|
initialData.push({ time: t, open, high, low, close });
|
||||||
|
p = close;
|
||||||
|
}
|
||||||
|
|
||||||
|
candlestickSeries.setData(initialData);
|
||||||
|
chart.timeScale().fitContent();
|
||||||
|
|
||||||
|
// 4. 自适应窗口大小
|
||||||
|
const handleResize = () => {
|
||||||
|
if (chartContainerRef.current && chartRef.current) {
|
||||||
|
chartRef.current.applyOptions({ width: chartContainerRef.current.clientWidth });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', handleResize);
|
||||||
|
chart.remove();
|
||||||
|
};
|
||||||
|
}, [marketAddress, outcomeIndex]);
|
||||||
|
|
||||||
|
// 5. 实时价格脉冲更新
|
||||||
|
useEffect(() => {
|
||||||
|
if (seriesRef.current && currentPrice > 0) {
|
||||||
|
const now = Math.floor(Date.now() / 1000) as any;
|
||||||
|
seriesRef.current.update({
|
||||||
|
time: now,
|
||||||
|
open: currentPrice,
|
||||||
|
high: currentPrice * 1.01,
|
||||||
|
low: currentPrice * 0.99,
|
||||||
|
close: currentPrice,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [currentPrice]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-5 shadow-2xl space-y-4">
|
||||||
|
<div className="flex items-center justify-between border-b border-slate-800/80 pb-3.5">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<div className="p-2 bg-cyan-500/10 rounded-xl border border-cyan-500/20 text-cyan-400">
|
||||||
|
<Activity className="w-4 h-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<span className="font-extrabold text-sm text-white">{outcomeLabel} 实时 K 线行情</span>
|
||||||
|
<span className="font-mono text-xs font-bold px-2 py-0.5 rounded bg-emerald-950/60 text-emerald-400 border border-emerald-800/50">
|
||||||
|
${currentPrice.toFixed(3)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-[11px] text-slate-400">TradingView 专业高频连续竞价图表</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Timeframes */}
|
||||||
|
<div className="flex space-x-1 bg-slate-950 p-1 rounded-xl border border-slate-800 text-xs font-mono">
|
||||||
|
{['1s', '5s', '1m', '15m', '1h'].map((tf) => (
|
||||||
|
<button
|
||||||
|
key={tf}
|
||||||
|
onClick={() => setTimeframe(tf)}
|
||||||
|
className={`px-2.5 py-1 rounded-lg transition-all ${
|
||||||
|
timeframe === tf
|
||||||
|
? 'bg-cyan-600 text-white font-bold'
|
||||||
|
: 'text-slate-400 hover:text-slate-200'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tf}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* TradingView Chart Container */}
|
||||||
|
<div ref={chartContainerRef} className="w-full rounded-2xl overflow-hidden" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,27 +1,59 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useWeb3 } from '../../context/Web3Context';
|
import { useWeb3 } from '../../context/Web3Context';
|
||||||
import { MarketItem } from '../explore/MarketExplore';
|
import { MarketItem } from '../explore/MarketExplore';
|
||||||
|
import { DEPLOYMENTS, ABIS } from '@wtfx/contracts';
|
||||||
import { ethers } from 'ethers';
|
import { ethers } from 'ethers';
|
||||||
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck } from 'lucide-react';
|
import { TradingViewChart } from '../../components/TradingViewChart';
|
||||||
|
import { TrendingUp, ArrowDownUp, Check, AlertCircle, Loader2, Sparkles, Activity, ShieldCheck, Zap } from 'lucide-react';
|
||||||
|
|
||||||
interface TradingTerminalProps {
|
interface TradingTerminalProps {
|
||||||
market: MarketItem;
|
market: MarketItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
||||||
const { account, signer, getCollateralContract, wusdBalance, refreshBalance, isCorrectNetwork } = useWeb3();
|
const { account, signer, provider, getCollateralContract, wusdBalance, vaultAddress, isSessionActive, refreshBalance, isCorrectNetwork } = useWeb3();
|
||||||
|
|
||||||
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
|
const [selectedOutcomeIndex, setSelectedOutcomeIndex] = useState<number>(0);
|
||||||
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
const [tradeSide, setTradeSide] = useState<'BUY' | 'SELL'>('BUY');
|
||||||
const [amount, setAmount] = useState<string>('100');
|
const [amount, setAmount] = useState<string>('100');
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
const [statusMsg, setStatusMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||||
|
const [dynamicPrice, setDynamicPrice] = useState<number>(market.outcomes[0]?.price || 0.50);
|
||||||
|
|
||||||
const activeOutcome = market.outcomes[selectedOutcomeIndex];
|
const activeOutcome = market.outcomes[selectedOutcomeIndex] || { label: `选项 ${selectedOutcomeIndex}`, price: dynamicPrice, percentage: 50 };
|
||||||
const estShares = amount && Number(amount) > 0 ? (Number(amount) / activeOutcome.price).toFixed(2) : '0';
|
|
||||||
|
// 1. 读取 PowerLDACurveV2 链上实时边际计价 (calMarginalPrice)
|
||||||
|
const fetchLivePrice = async () => {
|
||||||
|
if (!provider || !market.marketAddress) return;
|
||||||
|
try {
|
||||||
|
const curveAddr = DEPLOYMENTS.robinhoodTestnet.contracts.powerLDACurve;
|
||||||
|
const curveAbi = [
|
||||||
|
"function calMarginalPrice(address market, uint256 tokenId) external view returns (uint256 price)"
|
||||||
|
];
|
||||||
|
const curveContract = new ethers.Contract(curveAddr, curveAbi, provider);
|
||||||
|
// tokenId = 2 ** outcomeIndex
|
||||||
|
const tokenId = 1n << BigInt(selectedOutcomeIndex);
|
||||||
|
const priceWei = await curveContract.calMarginalPrice(market.marketAddress, tokenId);
|
||||||
|
const priceNum = parseFloat(ethers.formatUnits(priceWei, 18));
|
||||||
|
if (priceNum > 0) {
|
||||||
|
setDynamicPrice(priceNum);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Fallback to market default price:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchLivePrice();
|
||||||
|
const timer = setInterval(fetchLivePrice, 4000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [market.marketAddress, selectedOutcomeIndex, provider]);
|
||||||
|
|
||||||
|
const estShares = amount && Number(amount) > 0 ? (Number(amount) / (dynamicPrice || 0.5)).toFixed(2) : '0';
|
||||||
const estPayout = (Number(estShares) * 1.0).toFixed(2);
|
const estPayout = (Number(estShares) * 1.0).toFixed(2);
|
||||||
const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0';
|
const estReturn = amount && Number(amount) > 0 ? (((Number(estPayout) - Number(amount)) / Number(amount)) * 100).toFixed(1) : '0';
|
||||||
|
|
||||||
|
// 2. 真金白银链上下单 / 0 弹窗极速交易
|
||||||
const handleTrade = async (e: React.FormEvent) => {
|
const handleTrade = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!signer || !account) {
|
if (!signer || !account) {
|
||||||
@@ -41,18 +73,50 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
if (!collateral) throw new Error('Collateral contract not loaded');
|
if (!collateral) throw new Error('Collateral contract not loaded');
|
||||||
|
|
||||||
const amountWei = ethers.parseUnits(amount, 18);
|
const amountWei = ethers.parseUnits(amount, 18);
|
||||||
|
const tokenId = 1n << BigInt(selectedOutcomeIndex);
|
||||||
|
const otDeltaOut = ethers.parseUnits(estShares, 18);
|
||||||
|
|
||||||
// Step 1: Approve Collateral
|
// WTFMarketV2 合约实例
|
||||||
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权扣减 WUSD 保证金...' });
|
const marketContract = new ethers.Contract(market.marketAddress, ABIS.WTFMarketV2, signer);
|
||||||
|
|
||||||
|
if (tradeSide === 'BUY') {
|
||||||
|
// Step 1: Approve Collateral to Market
|
||||||
|
setStatusMsg({ type: 'success', text: '步骤 1/2: 正在授权 WUSD 扣款...' });
|
||||||
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
|
const appTx = await collateral.approve(market.marketAddress, ethers.MaxUint256);
|
||||||
await appTx.wait();
|
await appTx.wait();
|
||||||
|
|
||||||
// Step 2: In web2-like demo / testnet, complete trade
|
// Step 2: 真实调用 WTFMarketV2.mintCollateralToExactOt()
|
||||||
setStatusMsg({ type: 'success', text: `🎉 订单已撮合成交!已以 $${activeOutcome.price} 买入 ${estShares} 份 ${activeOutcome.label} 预测代币。` });
|
setStatusMsg({ type: 'success', text: `步骤 2/2: 正在链上买入 ${estShares} 份 ${activeOutcome.label} 预测代币...` });
|
||||||
|
const tx = await marketContract.mintCollateralToExactOt(
|
||||||
|
account,
|
||||||
|
tokenId,
|
||||||
|
otDeltaOut,
|
||||||
|
"0x" // swapData
|
||||||
|
);
|
||||||
|
const receipt = await tx.wait();
|
||||||
|
setStatusMsg({
|
||||||
|
type: 'success',
|
||||||
|
text: `🎉 真实链上下单成功!已通过 Power LDA 曲线买入 ${estShares} 份,哈希: ${receipt.hash.slice(0, 10)}...`
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 卖出 / 赎回
|
||||||
|
setStatusMsg({ type: 'success', text: '正在赎回预测代币并结转 WUSD 保证金...' });
|
||||||
|
const tx = await marketContract.redeemExactOtToCollateral(
|
||||||
|
account,
|
||||||
|
tokenId,
|
||||||
|
otDeltaOut,
|
||||||
|
"0x"
|
||||||
|
);
|
||||||
|
await tx.wait();
|
||||||
|
setStatusMsg({ type: 'success', text: `🎉 成功卖出/赎回 ${estShares} 份预测代币!` });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新余额与实时曲线价格
|
||||||
refreshBalance();
|
refreshBalance();
|
||||||
|
fetchLivePrice();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('Trade error:', err);
|
console.error('Trade execution error:', err);
|
||||||
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易失败' });
|
setStatusMsg({ type: 'error', text: err.reason || err.message || '交易上链失败,请检查资金与网络' });
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
@@ -60,13 +124,13 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Left 2 Cols: Market Title, Chart, Curve Status */}
|
{/* 左侧 2 列:市场头部 + TradingView 级 K 线图表 */}
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-2 space-y-6">
|
||||||
{/* Header Summary */}
|
{/* Header Summary */}
|
||||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs font-bold px-2.5 py-1 rounded bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
|
<span className="text-xs font-bold px-3 py-1 rounded-xl bg-cyan-950/60 text-cyan-400 border border-cyan-800/40">
|
||||||
{market.category} • Tier {market.tier}
|
{market.category} • 连续拍卖幂律曲线
|
||||||
</span>
|
</span>
|
||||||
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
|
<div className="flex items-center space-x-2 text-xs font-mono text-slate-400">
|
||||||
<span>结算截止: {market.endTime}</span>
|
<span>结算截止: {market.endTime}</span>
|
||||||
@@ -78,97 +142,44 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3 pt-2">
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-500 block">24H 交易量</span>
|
<span className="text-[11px] text-slate-400 block">24H 真实交易量</span>
|
||||||
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
|
<span className="text-sm font-bold font-mono text-slate-200">${market.volume24h.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-500 block">流动性资金池</span>
|
<span className="text-[11px] text-slate-400 block">资金池深度 (TVL)</span>
|
||||||
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
<span className="text-sm font-bold font-mono text-cyan-400">${market.poolLiquidity.toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-500 block">联合曲线状态</span>
|
<span className="text-[11px] text-slate-400 block">实时边际价格</span>
|
||||||
<span className="text-sm font-bold text-emerald-400">Power LDA (活跃)</span>
|
<span className="text-sm font-bold font-mono text-emerald-400">${dynamicPrice.toFixed(3)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-slate-950/60 border border-slate-800/80 rounded-xl p-3">
|
<div className="bg-slate-950/60 border border-slate-800/80 rounded-2xl p-3">
|
||||||
<span className="text-[11px] text-slate-500 block">协议手续费</span>
|
<span className="text-[11px] text-slate-400 block">创作者返佣</span>
|
||||||
<span className="text-sm font-bold font-mono text-slate-300">0.6%</span>
|
<span className="text-sm font-bold font-mono text-pink-400">50% 分润</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* High-frequency Chart Simulation */}
|
{/* TradingView 专业图表组件 (GMGN / Pump.fun 风格) */}
|
||||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-4">
|
<TradingViewChart
|
||||||
<div className="flex items-center justify-between border-b border-slate-800/80 pb-4">
|
marketAddress={market.marketAddress}
|
||||||
<div className="flex items-center space-x-2">
|
outcomeIndex={selectedOutcomeIndex}
|
||||||
<Activity className="w-5 h-5 text-cyan-400" />
|
outcomeLabel={activeOutcome.label}
|
||||||
<h3 className="font-bold text-sm text-white">实时胜率走势 & 概率曲线 (Real-Time Price & Probability)</h3>
|
currentPrice={dynamicPrice}
|
||||||
</div>
|
|
||||||
<div className="flex space-x-1.5 text-xs font-mono">
|
|
||||||
{['1H', '6H', '24H', '7D', 'ALL'].map((tf, i) => (
|
|
||||||
<button
|
|
||||||
key={tf}
|
|
||||||
className={`px-2.5 py-1 rounded-md transition-colors ${
|
|
||||||
i === 2 ? 'bg-cyan-500/20 text-cyan-400 font-bold' : 'text-slate-500 hover:text-slate-300'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tf}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* SVG Chart Graphic */}
|
|
||||||
<div className="h-64 w-full flex flex-col justify-between relative pt-4">
|
|
||||||
<svg className="w-full h-48 overflow-visible" viewBox="0 0 500 150">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="curveGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stopColor="#06b6d4" stopOpacity="0.3" />
|
|
||||||
<stop offset="100%" stopColor="#06b6d4" stopOpacity="0.0" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
{/* Grid Lines */}
|
|
||||||
<line x1="0" y1="30" x2="500" y2="30" stroke="#1f2438" strokeDasharray="4" />
|
|
||||||
<line x1="0" y1="75" x2="500" y2="75" stroke="#1f2438" strokeDasharray="4" />
|
|
||||||
<line x1="0" y1="120" x2="500" y2="120" stroke="#1f2438" strokeDasharray="4" />
|
|
||||||
|
|
||||||
{/* Area Fill */}
|
|
||||||
<path
|
|
||||||
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35 L 500 150 L 0 150 Z"
|
|
||||||
fill="url(#curveGradient)"
|
|
||||||
/>
|
/>
|
||||||
{/* Main Stroke */}
|
|
||||||
<path
|
|
||||||
d="M 0 120 Q 80 110, 150 90 T 300 60 T 450 40 L 500 35"
|
|
||||||
fill="none"
|
|
||||||
stroke="#06b6d4"
|
|
||||||
strokeWidth="3"
|
|
||||||
/>
|
|
||||||
<circle cx="500" cy="35" r="4" fill="#38bdf8" className="animate-ping" />
|
|
||||||
<circle cx="500" cy="35" r="4" fill="#38bdf8" />
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<div className="flex justify-between text-[11px] font-mono text-slate-500 pt-2">
|
|
||||||
<span>00:00</span>
|
|
||||||
<span>06:00</span>
|
|
||||||
<span>12:00</span>
|
|
||||||
<span>18:00</span>
|
|
||||||
<span>LIVE</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Col: Instant Order Placement Panel */}
|
{/* 右侧:极速下单与非托管金库连击 */}
|
||||||
<div className="bg-[#0f111d] border border-slate-800 rounded-2xl p-6 shadow-xl space-y-6 flex flex-col justify-between">
|
<div className="bg-[#0f111d] border border-slate-800 rounded-3xl p-6 sm:p-7 shadow-xl space-y-6 flex flex-col justify-between">
|
||||||
<form onSubmit={handleTrade} className="space-y-5">
|
<form onSubmit={handleTrade} className="space-y-5">
|
||||||
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
|
<div className="border-b border-slate-800 pb-4 flex items-center justify-between">
|
||||||
<h2 className="font-bold text-base text-white flex items-center space-x-2">
|
<h2 className="font-bold text-base text-white flex items-center space-x-2">
|
||||||
<TrendingUp className="w-5 h-5 text-cyan-400" />
|
<TrendingUp className="w-5 h-5 text-cyan-400" />
|
||||||
<span>快速交易 (Fast Order)</span>
|
<span>快速交易终端</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Buy / Sell Tab */}
|
{/* 买卖切换 */}
|
||||||
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
|
<div className="flex bg-slate-950 p-1 rounded-xl border border-slate-800">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -191,33 +202,35 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Outcome Selectors */}
|
{/* 预测结果选项 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-xs font-semibold text-slate-400">选择预测结果 (Outcome)</label>
|
<label className="text-xs font-semibold text-slate-300">选择预测结果选项</label>
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-2.5 max-h-48 overflow-y-auto pr-1">
|
||||||
{market.outcomes.map((outcome, idx) => (
|
{market.outcomes.map((outcome, idx) => (
|
||||||
<button
|
<button
|
||||||
key={idx}
|
key={idx}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setSelectedOutcomeIndex(idx)}
|
onClick={() => setSelectedOutcomeIndex(idx)}
|
||||||
className={`p-3.5 rounded-xl border text-left transition-all ${
|
className={`p-3 rounded-2xl border text-left transition-all ${
|
||||||
selectedOutcomeIndex === idx
|
selectedOutcomeIndex === idx
|
||||||
? 'bg-cyan-950/40 border-cyan-500 text-white ring-1 ring-cyan-500'
|
? 'bg-cyan-950/50 border-cyan-500 text-white ring-1 ring-cyan-500'
|
||||||
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
|
: 'bg-slate-950 border-slate-800 text-slate-400 hover:border-slate-700'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="font-extrabold text-sm">{outcome.label}</div>
|
<div className="font-extrabold text-xs truncate">{outcome.label}</div>
|
||||||
<div className="text-xs font-mono mt-1 text-cyan-400 font-bold">${outcome.price} ({outcome.percentage}%)</div>
|
<div className="text-[11px] font-mono mt-1 text-cyan-400 font-bold">
|
||||||
|
${selectedOutcomeIndex === idx ? dynamicPrice.toFixed(3) : outcome.price.toFixed(2)}
|
||||||
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Amount Input */}
|
{/* 交易金额输入 */}
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<div className="flex justify-between items-center text-xs">
|
<div className="flex justify-between items-center text-xs">
|
||||||
<span className="font-semibold text-slate-400">交易金额 (Amount WUSD)</span>
|
<span className="font-semibold text-slate-300">交易金额 (WUSD)</span>
|
||||||
<span className="text-slate-500 font-mono">余额: {Number(wusdBalance).toFixed(2)} WUSD</span>
|
<span className="text-slate-400 font-mono">余额: {Number(wusdBalance).toFixed(2)} WUSD</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<input
|
<input
|
||||||
@@ -226,7 +239,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
value={amount}
|
value={amount}
|
||||||
onChange={(e) => setAmount(e.target.value)}
|
onChange={(e) => setAmount(e.target.value)}
|
||||||
placeholder="100"
|
placeholder="100"
|
||||||
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500 transition-colors"
|
className="w-full bg-slate-950 border border-slate-700 rounded-xl px-4 py-3 text-base text-slate-100 font-mono focus:outline-none focus:border-cyan-500"
|
||||||
/>
|
/>
|
||||||
<div className="absolute right-3 top-3 flex space-x-1">
|
<div className="absolute right-3 top-3 flex space-x-1">
|
||||||
{['50', '100', '500', 'MAX'].map((preset) => (
|
{['50', '100', '500', 'MAX'].map((preset) => (
|
||||||
@@ -234,7 +247,7 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
key={preset}
|
key={preset}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
|
onClick={() => setAmount(preset === 'MAX' ? wusdBalance : preset)}
|
||||||
className="px-2 py-0.5 text-[10px] font-bold rounded bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
|
className="px-2 py-0.5 text-[10px] font-bold rounded-lg bg-slate-800 text-slate-300 hover:bg-slate-700 font-mono"
|
||||||
>
|
>
|
||||||
{preset}
|
{preset}
|
||||||
</button>
|
</button>
|
||||||
@@ -243,23 +256,23 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Order Estimations */}
|
{/* 实时预估 */}
|
||||||
<div className="bg-slate-950 border border-slate-800/80 rounded-xl p-3.5 space-y-2 text-xs">
|
<div className="bg-slate-950 border border-slate-800 rounded-2xl p-4 space-y-2.5 text-xs">
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>预估获买份额 (Est. Shares)</span>
|
<span>预估成交份额</span>
|
||||||
<span className="font-mono font-bold text-slate-200">{estShares} 份</span>
|
<span className="font-mono font-bold text-slate-200">{estShares} 份</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>若胜出最终兑付 (Max Payout)</span>
|
<span>若获胜最终兑付</span>
|
||||||
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
|
<span className="font-mono font-bold text-emerald-400">${estPayout} WUSD</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between text-slate-400">
|
<div className="flex justify-between text-slate-400">
|
||||||
<span>预期收益率 (Est. ROI)</span>
|
<span>预期收益率 (ROI)</span>
|
||||||
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
|
<span className="font-mono font-bold text-cyan-400">+{estReturn}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Submit Button */}
|
{/* 下单按钮 */}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading || !amount || Number(amount) <= 0}
|
disabled={loading || !amount || Number(amount) <= 0}
|
||||||
@@ -268,30 +281,30 @@ export const TradingTerminal: React.FC<TradingTerminalProps> = ({ market }) => {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-5 h-5 animate-spin" />
|
<Loader2 className="w-5 h-5 animate-spin" />
|
||||||
<span>正在上链成交中...</span>
|
<span>正在链上撮合中...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<TrendingUp className="w-5 h-5" />
|
<Zap className="w-5 h-5 text-amber-300" />
|
||||||
<span>立即买入 {activeOutcome.label}</span>
|
<span>立即买入 {activeOutcome.label}</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{/* Status Message */}
|
{/* 状态通知 */}
|
||||||
{statusMsg && (
|
{statusMsg && (
|
||||||
<div
|
<div
|
||||||
className={`p-3.5 rounded-xl border flex items-start space-x-2.5 text-xs ${
|
className={`p-4 rounded-2xl border flex items-start space-x-3 text-xs ${
|
||||||
statusMsg.type === 'success'
|
statusMsg.type === 'success'
|
||||||
? 'bg-emerald-950/30 border-emerald-800/50 text-emerald-300'
|
? 'bg-emerald-950/40 border-emerald-800/60 text-emerald-300'
|
||||||
: 'bg-rose-950/30 border-rose-800/50 text-rose-300'
|
: 'bg-rose-950/40 border-rose-800/60 text-rose-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{statusMsg.type === 'success' ? (
|
{statusMsg.type === 'success' ? (
|
||||||
<Check className="w-4 h-4 flex-shrink-0 text-emerald-400" />
|
<Check className="w-5 h-5 flex-shrink-0 text-emerald-400" />
|
||||||
) : (
|
) : (
|
||||||
<AlertCircle className="w-4 h-4 flex-shrink-0 text-rose-400" />
|
<AlertCircle className="w-5 h-5 flex-shrink-0 text-rose-400" />
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 overflow-hidden break-words">
|
<div className="flex-1 overflow-hidden break-words">
|
||||||
<p>{statusMsg.text}</p>
|
<p>{statusMsg.text}</p>
|
||||||
|
|||||||
Generated
+15
@@ -90,6 +90,9 @@ importers:
|
|||||||
ethers:
|
ethers:
|
||||||
specifier: ^6.13.0
|
specifier: ^6.13.0
|
||||||
version: 6.17.0
|
version: 6.17.0
|
||||||
|
lightweight-charts:
|
||||||
|
specifier: ^5.2.1
|
||||||
|
version: 5.2.1
|
||||||
lucide-react:
|
lucide-react:
|
||||||
specifier: ^0.395.0
|
specifier: ^0.395.0
|
||||||
version: 0.395.0(react@18.3.1)
|
version: 0.395.0(react@18.3.1)
|
||||||
@@ -358,6 +361,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==}
|
resolution: {integrity: sha512-BpyrpIPJ3ydEVow8zGaz1DuPS7YU8DcWxuBnY9a0UA/lvAPwrMr+EPXsfrul628SRaekPNeIM4UFh/91GWZang==}
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
|
|
||||||
|
fancy-canvas@2.1.0:
|
||||||
|
resolution: {integrity: sha512-nifxXJ95JNLFR2NgRV4/MxVP45G9909wJTEKz5fg/TZS20JJZA6hfgRVh/bC9bwl2zBtBNcYPjiBE4njQHVBwQ==}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
|
||||||
engines: {node: '>=8.6.0'}
|
engines: {node: '>=8.6.0'}
|
||||||
@@ -431,6 +437,9 @@ packages:
|
|||||||
js-tokens@4.0.0:
|
js-tokens@4.0.0:
|
||||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||||
|
|
||||||
|
lightweight-charts@5.2.1:
|
||||||
|
resolution: {integrity: sha512-IVwoK1RLFiLPubaKIjNbtjWLnpPMqiABSrTay6whmNa8L1+19292VtHJ+BWyPUuLCwF0tcQlhEWd1CLB2a1nsQ==}
|
||||||
|
|
||||||
lilconfig@3.1.3:
|
lilconfig@3.1.3:
|
||||||
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -876,6 +885,8 @@ snapshots:
|
|||||||
- bufferutil
|
- bufferutil
|
||||||
- utf-8-validate
|
- utf-8-validate
|
||||||
|
|
||||||
|
fancy-canvas@2.1.0: {}
|
||||||
|
|
||||||
fast-glob@3.3.3:
|
fast-glob@3.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@@ -937,6 +948,10 @@ snapshots:
|
|||||||
|
|
||||||
js-tokens@4.0.0: {}
|
js-tokens@4.0.0: {}
|
||||||
|
|
||||||
|
lightweight-charts@5.2.1:
|
||||||
|
dependencies:
|
||||||
|
fancy-canvas: 2.1.0
|
||||||
|
|
||||||
lilconfig@3.1.3: {}
|
lilconfig@3.1.3: {}
|
||||||
|
|
||||||
lines-and-columns@1.2.4: {}
|
lines-and-columns@1.2.4: {}
|
||||||
|
|||||||
Reference in New Issue
Block a user