|
@@ -1,6 +1,12 @@
|
|
|
'use client';
|
|
'use client';
|
|
|
|
|
|
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
import { useState, useCallback, useEffect } from 'react';
|
|
|
|
|
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
|
|
|
|
+import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
|
|
|
+import { Checkbox } from '@/components/ui/checkbox';
|
|
|
|
|
+import { Input } from '@/components/ui/input';
|
|
|
|
|
+import { Button } from '@/components/ui/button';
|
|
|
|
|
+import { Badge } from '@/components/ui/badge';
|
|
|
import type { Signal } from '@/lib/types';
|
|
import type { Signal } from '@/lib/types';
|
|
|
|
|
|
|
|
type OrderStep = 'review' | 'pricing' | 'confirm' | 'result';
|
|
type OrderStep = 'review' | 'pricing' | 'confirm' | 'result';
|
|
@@ -15,17 +21,16 @@ interface PriceData {
|
|
|
signalId: string;
|
|
signalId: string;
|
|
|
yesPrice: number;
|
|
yesPrice: number;
|
|
|
noPrice: number;
|
|
noPrice: number;
|
|
|
- // YES side
|
|
|
|
|
yesBestBid: number;
|
|
yesBestBid: number;
|
|
|
yesBestAsk: number;
|
|
yesBestAsk: number;
|
|
|
yesSpread: number;
|
|
yesSpread: number;
|
|
|
- // NO side (direct from NO token orderbook)
|
|
|
|
|
|
|
+ yesMidpoint: number;
|
|
|
noBestBid: number;
|
|
noBestBid: number;
|
|
|
noBestAsk: number;
|
|
noBestAsk: number;
|
|
|
noSpread: number;
|
|
noSpread: number;
|
|
|
|
|
+ noMidpoint: number;
|
|
|
noAsks: BookLevel[];
|
|
noAsks: BookLevel[];
|
|
|
yesAsks: BookLevel[];
|
|
yesAsks: BookLevel[];
|
|
|
- // Legacy
|
|
|
|
|
bestBid: number;
|
|
bestBid: number;
|
|
|
bestAsk: number;
|
|
bestAsk: number;
|
|
|
spread: number;
|
|
spread: number;
|
|
@@ -75,7 +80,6 @@ export function OrderPanel({
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
const [isDryRun, setIsDryRun] = useState<boolean | null>(null);
|
|
const [isDryRun, setIsDryRun] = useState<boolean | null>(null);
|
|
|
|
|
|
|
|
- // Fetch actual DRY_RUN status from server
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
fetch('/api/bot-control')
|
|
fetch('/api/bot-control')
|
|
|
.then((r) => r.json())
|
|
.then((r) => r.json())
|
|
@@ -115,7 +119,6 @@ export function OrderPanel({
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // Step 1 → Step 2: Fetch latest prices
|
|
|
|
|
const fetchPrices = useCallback(async () => {
|
|
const fetchPrices = useCallback(async () => {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
setError(null);
|
|
setError(null);
|
|
@@ -150,7 +153,6 @@ export function OrderPanel({
|
|
|
}
|
|
}
|
|
|
}, [selectedSignals]);
|
|
}, [selectedSignals]);
|
|
|
|
|
|
|
|
- // Step 3: Place orders
|
|
|
|
|
const placeOrders = useCallback(async () => {
|
|
const placeOrders = useCallback(async () => {
|
|
|
setLoading(true);
|
|
setLoading(true);
|
|
|
setError(null);
|
|
setError(null);
|
|
@@ -159,16 +161,12 @@ export function OrderPanel({
|
|
|
const priceData = prices.get(signal.id);
|
|
const priceData = prices.get(signal.id);
|
|
|
let price: number;
|
|
let price: number;
|
|
|
if (orderMode === 'MARKET') {
|
|
if (orderMode === 'MARKET') {
|
|
|
- // Market order: use actual best ask from the respective orderbook
|
|
|
|
|
if (signal.side === 'BUY_NO') {
|
|
if (signal.side === 'BUY_NO') {
|
|
|
- // Use real NO best ask from NO token orderbook
|
|
|
|
|
price = priceData?.noBestAsk ?? (1 - signal.marketYesPrice);
|
|
price = priceData?.noBestAsk ?? (1 - signal.marketYesPrice);
|
|
|
} else {
|
|
} else {
|
|
|
- // Use real YES best ask from YES token orderbook
|
|
|
|
|
price = priceData?.yesBestAsk ?? signal.marketYesPrice;
|
|
price = priceData?.yesBestAsk ?? signal.marketYesPrice;
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- // Limit order: use suggested price from signal
|
|
|
|
|
price = signal.suggestedPrice;
|
|
price = signal.suggestedPrice;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -199,28 +197,18 @@ export function OrderPanel({
|
|
|
}, [selectedSignals, prices, orderMode]);
|
|
}, [selectedSignals, prices, orderMode]);
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
|
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
|
|
|
|
|
- <div className="bg-white dark:bg-slate-800 rounded-xl shadow-2xl border border-gray-200 dark:border-slate-700 w-full max-w-3xl max-h-[85vh] flex flex-col mx-4">
|
|
|
|
|
|
|
+ <Dialog open onOpenChange={(open) => { if (!open) onClose(); }}>
|
|
|
|
|
+ <DialogContent>
|
|
|
{/* Header */}
|
|
{/* Header */}
|
|
|
- <div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-slate-700">
|
|
|
|
|
- <div>
|
|
|
|
|
- <h2 className="text-lg font-semibold text-heading">Manual Order</h2>
|
|
|
|
|
- <p className="text-xs text-sub mt-0.5">
|
|
|
|
|
- {step === 'review' && 'Step 1: Review selected signals'}
|
|
|
|
|
- {step === 'pricing' && 'Step 2: Review live prices & select order type'}
|
|
|
|
|
- {step === 'confirm' && 'Step 3: Confirm and place orders'}
|
|
|
|
|
- {step === 'result' && 'Order results'}
|
|
|
|
|
- </p>
|
|
|
|
|
- </div>
|
|
|
|
|
- <button
|
|
|
|
|
- onClick={onClose}
|
|
|
|
|
- className="p-2 hover:bg-gray-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
|
|
|
- >
|
|
|
|
|
- <svg className="w-5 h-5 text-dim" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
|
|
|
- <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
|
|
|
|
- </svg>
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <DialogHeader>
|
|
|
|
|
+ <DialogTitle>Manual Order</DialogTitle>
|
|
|
|
|
+ <DialogDescription>
|
|
|
|
|
+ {step === 'review' && 'Step 1: Review selected signals'}
|
|
|
|
|
+ {step === 'pricing' && 'Step 2: Review live prices & select order type'}
|
|
|
|
|
+ {step === 'confirm' && 'Step 3: Confirm and place orders'}
|
|
|
|
|
+ {step === 'result' && 'Order results'}
|
|
|
|
|
+ </DialogDescription>
|
|
|
|
|
+ </DialogHeader>
|
|
|
|
|
|
|
|
{/* Step indicator */}
|
|
{/* Step indicator */}
|
|
|
<div className="px-6 py-3 border-b border-gray-100 dark:border-slate-700/50">
|
|
<div className="px-6 py-3 border-b border-gray-100 dark:border-slate-700/50">
|
|
@@ -286,11 +274,9 @@ export function OrderPanel({
|
|
|
: 'border-gray-200 dark:border-slate-700 hover:border-gray-300 dark:hover:border-slate-600'
|
|
: 'border-gray-200 dark:border-slate-700 hover:border-gray-300 dark:hover:border-slate-600'
|
|
|
}`}
|
|
}`}
|
|
|
>
|
|
>
|
|
|
- <input
|
|
|
|
|
- type="checkbox"
|
|
|
|
|
|
|
+ <Checkbox
|
|
|
checked={checked}
|
|
checked={checked}
|
|
|
- onChange={() => toggleSignal(signal.id)}
|
|
|
|
|
- className="w-4 h-4 rounded border-gray-300 dark:border-slate-600 text-blue-600 focus:ring-blue-500"
|
|
|
|
|
|
|
+ onCheckedChange={() => toggleSignal(signal.id)}
|
|
|
/>
|
|
/>
|
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex-1 min-w-0">
|
|
|
<div className="flex items-center gap-2">
|
|
<div className="flex items-center gap-2">
|
|
@@ -308,13 +294,13 @@ export function OrderPanel({
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex items-center gap-1.5" onClick={(e) => e.preventDefault()}>
|
|
<div className="flex items-center gap-1.5" onClick={(e) => e.preventDefault()}>
|
|
|
<span className="text-xs text-dim">Shares:</span>
|
|
<span className="text-xs text-dim">Shares:</span>
|
|
|
- <input
|
|
|
|
|
|
|
+ <Input
|
|
|
type="number"
|
|
type="number"
|
|
|
- min="1"
|
|
|
|
|
- step="1"
|
|
|
|
|
|
|
+ min={1}
|
|
|
|
|
+ step={1}
|
|
|
value={getSize(signal.id, signal.recommendedSize)}
|
|
value={getSize(signal.id, signal.recommendedSize)}
|
|
|
onChange={(e) => updateSize(signal.id, e.target.value)}
|
|
onChange={(e) => updateSize(signal.id, e.target.value)}
|
|
|
- className="w-20 text-right text-sm font-mono px-2 py-1 rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-heading focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
|
|
|
|
|
+ className="w-20 text-right text-sm font-mono h-8"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
</label>
|
|
</label>
|
|
@@ -327,135 +313,123 @@ export function OrderPanel({
|
|
|
{/* Step 2: Live Prices */}
|
|
{/* Step 2: Live Prices */}
|
|
|
{step === 'pricing' && (
|
|
{step === 'pricing' && (
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- <div className="overflow-x-auto">
|
|
|
|
|
- <table className="w-full text-sm">
|
|
|
|
|
- <thead>
|
|
|
|
|
- <tr className="border-b border-gray-200 dark:border-slate-700">
|
|
|
|
|
- <th className="table-header">
|
|
|
|
|
- <input
|
|
|
|
|
- type="checkbox"
|
|
|
|
|
- checked={selectedIds.size === signals.length}
|
|
|
|
|
- onChange={toggleAll}
|
|
|
|
|
- className="w-3.5 h-3.5 rounded border-gray-300 dark:border-slate-600 text-blue-600"
|
|
|
|
|
- />
|
|
|
|
|
- </th>
|
|
|
|
|
- <th className="table-header">Signal</th>
|
|
|
|
|
- <th className="table-header text-right">Best Ask</th>
|
|
|
|
|
- <th className="table-header text-right">Depth</th>
|
|
|
|
|
- <th className="table-header text-right">Spread</th>
|
|
|
|
|
- <th className="table-header text-right">Edge Now</th>
|
|
|
|
|
- <th className="table-header text-right">Shares</th>
|
|
|
|
|
- </tr>
|
|
|
|
|
- </thead>
|
|
|
|
|
- <tbody className="divide-y divide-gray-100 dark:divide-slate-700/50">
|
|
|
|
|
- {selectedSignals.map((signal) => {
|
|
|
|
|
- const priceData = prices.get(signal.id);
|
|
|
|
|
- const checked = selectedIds.has(signal.id);
|
|
|
|
|
- const side = sideLabels[signal.side];
|
|
|
|
|
- const isBuyNo = signal.side === 'BUY_NO';
|
|
|
|
|
- const isParity = signal.type === 'PARITY_ARB';
|
|
|
|
|
-
|
|
|
|
|
- // Live best ask from the actual orderbook for the side we're buying
|
|
|
|
|
- const liveBestAsk = priceData
|
|
|
|
|
- ? (isBuyNo ? priceData.noBestAsk : priceData.yesBestAsk)
|
|
|
|
|
- : 0;
|
|
|
|
|
- const liveSpread = priceData
|
|
|
|
|
- ? (isBuyNo ? priceData.noSpread : priceData.yesSpread)
|
|
|
|
|
- : 0;
|
|
|
|
|
-
|
|
|
|
|
- // Top asks for depth display
|
|
|
|
|
- const topAsks = priceData
|
|
|
|
|
- ? (isBuyNo ? priceData.noAsks : priceData.yesAsks)
|
|
|
|
|
- : [];
|
|
|
|
|
-
|
|
|
|
|
- // Compute live edge using YES midpoint
|
|
|
|
|
- const liveYesPrice = priceData?.midpoint ?? signal.marketYesPrice;
|
|
|
|
|
- let edgeNow = 0;
|
|
|
|
|
- if (signal.type === 'TAIL_NO') {
|
|
|
|
|
- edgeNow = liveYesPrice - signal.modelProbability;
|
|
|
|
|
- } else if (signal.type === 'LATENCY_ARB') {
|
|
|
|
|
- edgeNow = Math.abs(signal.modelProbability - liveYesPrice);
|
|
|
|
|
- } else {
|
|
|
|
|
- edgeNow = signal.edge;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Cost per share = best ask price, return per share = $1 if win
|
|
|
|
|
- // ROI = (1 - bestAsk) / bestAsk
|
|
|
|
|
- const roi = liveBestAsk > 0 && liveBestAsk < 1
|
|
|
|
|
- ? (1 - liveBestAsk) / liveBestAsk
|
|
|
|
|
- : 0;
|
|
|
|
|
-
|
|
|
|
|
- return (
|
|
|
|
|
- <tr key={signal.id} className="hover:bg-gray-50 dark:hover:bg-slate-800/50">
|
|
|
|
|
- <td className="table-cell">
|
|
|
|
|
- <input
|
|
|
|
|
- type="checkbox"
|
|
|
|
|
- checked={checked}
|
|
|
|
|
- onChange={() => toggleSignal(signal.id)}
|
|
|
|
|
- className="w-3.5 h-3.5 rounded border-gray-300 dark:border-slate-600 text-blue-600"
|
|
|
|
|
- />
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell">
|
|
|
|
|
- <div className="flex items-center gap-1.5">
|
|
|
|
|
- <span className={`text-xs font-semibold ${side.color}`}>{side.text}</span>
|
|
|
|
|
- <span className="text-xs text-heading">{formatCity(signal.city)}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <span className="text-xs text-sub font-mono">{signal.bucket}</span>
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell text-right font-mono">
|
|
|
|
|
- {priceData?.error ? (
|
|
|
|
|
- <span className="text-red-500 text-xs">{priceData.error}</span>
|
|
|
|
|
- ) : priceData ? (
|
|
|
|
|
|
|
+ <Table>
|
|
|
|
|
+ <TableHeader>
|
|
|
|
|
+ <TableRow className="border-b border-gray-200 dark:border-slate-700">
|
|
|
|
|
+ <TableHead>
|
|
|
|
|
+ <Checkbox
|
|
|
|
|
+ checked={selectedIds.size === signals.length}
|
|
|
|
|
+ onCheckedChange={toggleAll}
|
|
|
|
|
+ />
|
|
|
|
|
+ </TableHead>
|
|
|
|
|
+ <TableHead>Signal</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">Best Ask</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">Depth</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">Spread</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">Edge Now</TableHead>
|
|
|
|
|
+ <TableHead className="text-right">Shares</TableHead>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ </TableHeader>
|
|
|
|
|
+ <TableBody>
|
|
|
|
|
+ {selectedSignals.map((signal) => {
|
|
|
|
|
+ const priceData = prices.get(signal.id);
|
|
|
|
|
+ const checked = selectedIds.has(signal.id);
|
|
|
|
|
+ const side = sideLabels[signal.side];
|
|
|
|
|
+ const isBuyNo = signal.side === 'BUY_NO';
|
|
|
|
|
+
|
|
|
|
|
+ const liveBestAsk = priceData
|
|
|
|
|
+ ? (isBuyNo ? priceData.noBestAsk : priceData.yesBestAsk)
|
|
|
|
|
+ : 0;
|
|
|
|
|
+ const liveMidpoint = priceData
|
|
|
|
|
+ ? (isBuyNo ? (priceData.noMidpoint ?? 0) : (priceData.yesMidpoint ?? 0))
|
|
|
|
|
+ : 0;
|
|
|
|
|
+ const liveSpread = priceData
|
|
|
|
|
+ ? (isBuyNo ? priceData.noSpread : priceData.yesSpread)
|
|
|
|
|
+ : 0;
|
|
|
|
|
+
|
|
|
|
|
+ const topAsks = priceData
|
|
|
|
|
+ ? (isBuyNo ? priceData.noAsks : priceData.yesAsks)
|
|
|
|
|
+ : [];
|
|
|
|
|
+
|
|
|
|
|
+ const roi = liveBestAsk > 0 && liveBestAsk < 1
|
|
|
|
|
+ ? (1 - liveBestAsk) / liveBestAsk
|
|
|
|
|
+ : 0;
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <TableRow key={signal.id}>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ <Checkbox
|
|
|
|
|
+ checked={checked}
|
|
|
|
|
+ onCheckedChange={() => toggleSignal(signal.id)}
|
|
|
|
|
+ />
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell>
|
|
|
|
|
+ <div className="flex items-center gap-1.5">
|
|
|
|
|
+ <span className={`text-xs font-semibold ${side.color}`}>{side.text}</span>
|
|
|
|
|
+ <span className="text-xs text-heading">{formatCity(signal.city)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <span className="text-xs text-sub font-mono">{signal.bucket}</span>
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right font-mono">
|
|
|
|
|
+ {priceData?.error ? (
|
|
|
|
|
+ <span className="text-red-500 text-xs">{priceData.error}</span>
|
|
|
|
|
+ ) : priceData ? (
|
|
|
|
|
+ <div>
|
|
|
<span className="text-heading font-medium">
|
|
<span className="text-heading font-medium">
|
|
|
{(liveBestAsk * 100).toFixed(1)}c
|
|
{(liveBestAsk * 100).toFixed(1)}c
|
|
|
</span>
|
|
</span>
|
|
|
- ) : (
|
|
|
|
|
- <span className="text-dim">--</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell text-right font-mono text-xs">
|
|
|
|
|
- {topAsks.length > 0 ? (
|
|
|
|
|
- <div className="space-y-0.5">
|
|
|
|
|
- {topAsks.slice(0, 3).map((ask, i) => (
|
|
|
|
|
- <div key={i} className={i === 0 ? 'text-heading font-medium' : 'text-dim'}>
|
|
|
|
|
- {(ask.price * 100).toFixed(1)}c × {ask.size.toFixed(0)}
|
|
|
|
|
- </div>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="text-dim">--</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell text-right font-mono text-xs text-sub">
|
|
|
|
|
- {priceData && !priceData.error
|
|
|
|
|
- ? `${(liveSpread * 100).toFixed(1)}c`
|
|
|
|
|
- : '--'}
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell text-right font-mono">
|
|
|
|
|
- {roi > 0 ? (
|
|
|
|
|
- <span className={roi > 0.05 ? 'text-green-600 dark:text-green-400 font-medium' : 'text-yellow-600 dark:text-yellow-400'}>
|
|
|
|
|
- +{(roi * 100).toFixed(0)}% ROI
|
|
|
|
|
- </span>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <span className="text-dim">--</span>
|
|
|
|
|
- )}
|
|
|
|
|
- </td>
|
|
|
|
|
- <td className="table-cell text-right">
|
|
|
|
|
- <input
|
|
|
|
|
- type="number"
|
|
|
|
|
- min="1"
|
|
|
|
|
- step="1"
|
|
|
|
|
- value={getSize(signal.id, signal.recommendedSize)}
|
|
|
|
|
- onChange={(e) => updateSize(signal.id, e.target.value)}
|
|
|
|
|
- className="w-20 text-right text-sm font-mono px-2 py-1 rounded border border-gray-300 dark:border-slate-600 bg-white dark:bg-slate-700 text-heading focus:outline-none focus:ring-1 focus:ring-blue-500"
|
|
|
|
|
- />
|
|
|
|
|
- </td>
|
|
|
|
|
- </tr>
|
|
|
|
|
- );
|
|
|
|
|
- })}
|
|
|
|
|
- </tbody>
|
|
|
|
|
- </table>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ {liveMidpoint > 0 && (
|
|
|
|
|
+ <div className="text-[10px] text-dim">
|
|
|
|
|
+ mid {(liveMidpoint * 100).toFixed(1)}c
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="text-dim">--</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right font-mono text-xs">
|
|
|
|
|
+ {topAsks.length > 0 ? (
|
|
|
|
|
+ <div className="space-y-0.5">
|
|
|
|
|
+ {topAsks.slice(0, 3).map((ask, i) => (
|
|
|
|
|
+ <div key={i} className={i === 0 ? 'text-heading font-medium' : 'text-dim'}>
|
|
|
|
|
+ {(ask.price * 100).toFixed(1)}c × {ask.size.toFixed(0)}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="text-dim">--</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right font-mono text-xs text-sub">
|
|
|
|
|
+ {priceData && !priceData.error
|
|
|
|
|
+ ? `${(liveSpread * 100).toFixed(1)}c`
|
|
|
|
|
+ : '--'}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right font-mono">
|
|
|
|
|
+ {roi > 0 ? (
|
|
|
|
|
+ <span className={roi > 0.05 ? 'text-green-600 dark:text-green-400 font-medium' : 'text-yellow-600 dark:text-yellow-400'}>
|
|
|
|
|
+ +{(roi * 100).toFixed(0)}% ROI
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <span className="text-dim">--</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ <TableCell className="text-right">
|
|
|
|
|
+ <Input
|
|
|
|
|
+ type="number"
|
|
|
|
|
+ min={1}
|
|
|
|
|
+ step={1}
|
|
|
|
|
+ value={getSize(signal.id, signal.recommendedSize)}
|
|
|
|
|
+ onChange={(e) => updateSize(signal.id, e.target.value)}
|
|
|
|
|
+ className="w-20 text-right text-sm font-mono h-8"
|
|
|
|
|
+ />
|
|
|
|
|
+ </TableCell>
|
|
|
|
|
+ </TableRow>
|
|
|
|
|
+ );
|
|
|
|
|
+ })}
|
|
|
|
|
+ </TableBody>
|
|
|
|
|
+ </Table>
|
|
|
|
|
|
|
|
{/* Order type selection */}
|
|
{/* Order type selection */}
|
|
|
<div className="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4 border border-gray-200 dark:border-slate-700/50">
|
|
<div className="bg-gray-50 dark:bg-slate-900/50 rounded-lg p-4 border border-gray-200 dark:border-slate-700/50">
|
|
@@ -606,15 +580,17 @@ export function OrderPanel({
|
|
|
{signal ? `${formatCity(signal.city)} · ${signal.bucket}` : result.signalId}
|
|
{signal ? `${formatCity(signal.city)} · ${signal.bucket}` : result.signalId}
|
|
|
</span>
|
|
</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <span className={`text-xs font-medium px-2 py-0.5 rounded ${
|
|
|
|
|
- result.status === 'DRY_RUN'
|
|
|
|
|
- ? 'bg-yellow-100 text-yellow-700 dark:bg-yellow-500/20 dark:text-yellow-400'
|
|
|
|
|
- : result.status === 'SUBMITTED'
|
|
|
|
|
- ? 'bg-green-100 text-green-700 dark:bg-green-500/20 dark:text-green-400'
|
|
|
|
|
- : 'bg-red-100 text-red-700 dark:bg-red-500/20 dark:text-red-400'
|
|
|
|
|
- }`}>
|
|
|
|
|
|
|
+ <Badge
|
|
|
|
|
+ variant={
|
|
|
|
|
+ result.status === 'DRY_RUN'
|
|
|
|
|
+ ? 'yellow'
|
|
|
|
|
+ : result.status === 'SUBMITTED'
|
|
|
|
|
+ ? 'green'
|
|
|
|
|
+ : 'red'
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
{result.status}
|
|
{result.status}
|
|
|
- </span>
|
|
|
|
|
|
|
+ </Badge>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="mt-1 text-xs text-sub">
|
|
<div className="mt-1 text-xs text-sub">
|
|
|
{result.side} @ {(result.price * 100).toFixed(1)}c
|
|
{result.side} @ {(result.price * 100).toFixed(1)}c
|
|
@@ -635,36 +611,37 @@ export function OrderPanel({
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
{/* Footer actions */}
|
|
{/* Footer actions */}
|
|
|
- <div className="px-6 py-4 border-t border-gray-200 dark:border-slate-700 flex items-center justify-between">
|
|
|
|
|
|
|
+ <DialogFooter>
|
|
|
<div>
|
|
<div>
|
|
|
{step !== 'review' && step !== 'result' && (
|
|
{step !== 'review' && step !== 'result' && (
|
|
|
- <button
|
|
|
|
|
|
|
+ <Button
|
|
|
onClick={() => {
|
|
onClick={() => {
|
|
|
if (step === 'pricing') setStep('review');
|
|
if (step === 'pricing') setStep('review');
|
|
|
if (step === 'confirm') setStep('pricing');
|
|
if (step === 'confirm') setStep('pricing');
|
|
|
}}
|
|
}}
|
|
|
- className="btn-ghost btn-sm"
|
|
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ size="sm"
|
|
|
disabled={loading}
|
|
disabled={loading}
|
|
|
>
|
|
>
|
|
|
Back
|
|
Back
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex items-center gap-3">
|
|
|
{step === 'result' ? (
|
|
{step === 'result' ? (
|
|
|
- <button onClick={onComplete} className="btn-primary btn-sm">
|
|
|
|
|
|
|
+ <Button onClick={onComplete} size="sm">
|
|
|
Done
|
|
Done
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
) : (
|
|
) : (
|
|
|
<>
|
|
<>
|
|
|
- <button onClick={onClose} className="btn-ghost btn-sm">
|
|
|
|
|
|
|
+ <Button onClick={onClose} variant="ghost" size="sm">
|
|
|
Cancel
|
|
Cancel
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
{step === 'review' && (
|
|
{step === 'review' && (
|
|
|
- <button
|
|
|
|
|
|
|
+ <Button
|
|
|
onClick={fetchPrices}
|
|
onClick={fetchPrices}
|
|
|
disabled={selectedIds.size === 0 || loading}
|
|
disabled={selectedIds.size === 0 || loading}
|
|
|
- className="btn-primary btn-sm"
|
|
|
|
|
|
|
+ size="sm"
|
|
|
>
|
|
>
|
|
|
{loading ? (
|
|
{loading ? (
|
|
|
<span className="flex items-center gap-2">
|
|
<span className="flex items-center gap-2">
|
|
@@ -673,22 +650,23 @@ export function OrderPanel({
|
|
|
) : (
|
|
) : (
|
|
|
`Fetch Prices (${selectedIds.size})`
|
|
`Fetch Prices (${selectedIds.size})`
|
|
|
)}
|
|
)}
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
)}
|
|
)}
|
|
|
{step === 'pricing' && (
|
|
{step === 'pricing' && (
|
|
|
- <button
|
|
|
|
|
|
|
+ <Button
|
|
|
onClick={() => setStep('confirm')}
|
|
onClick={() => setStep('confirm')}
|
|
|
disabled={selectedIds.size === 0}
|
|
disabled={selectedIds.size === 0}
|
|
|
- className="btn-primary btn-sm"
|
|
|
|
|
|
|
+ size="sm"
|
|
|
>
|
|
>
|
|
|
Review Orders ({selectedIds.size})
|
|
Review Orders ({selectedIds.size})
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
)}
|
|
)}
|
|
|
{step === 'confirm' && (
|
|
{step === 'confirm' && (
|
|
|
- <button
|
|
|
|
|
|
|
+ <Button
|
|
|
onClick={placeOrders}
|
|
onClick={placeOrders}
|
|
|
disabled={loading}
|
|
disabled={loading}
|
|
|
- className={`btn-sm ${orderMode === 'MARKET' ? 'btn-danger' : 'btn-primary'}`}
|
|
|
|
|
|
|
+ variant={orderMode === 'MARKET' ? 'destructive' : 'default'}
|
|
|
|
|
+ size="sm"
|
|
|
>
|
|
>
|
|
|
{loading ? (
|
|
{loading ? (
|
|
|
<span className="flex items-center gap-2">
|
|
<span className="flex items-center gap-2">
|
|
@@ -697,14 +675,14 @@ export function OrderPanel({
|
|
|
) : (
|
|
) : (
|
|
|
`Place ${selectedIds.size} ${orderMode === 'LIMIT' ? 'Limit' : 'Market'} Order${selectedIds.size > 1 ? 's' : ''}`
|
|
`Place ${selectedIds.size} ${orderMode === 'LIMIT' ? 'Limit' : 'Market'} Order${selectedIds.size > 1 ? 's' : ''}`
|
|
|
)}
|
|
)}
|
|
|
- </button>
|
|
|
|
|
|
|
+ </Button>
|
|
|
)}
|
|
)}
|
|
|
</>
|
|
</>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ </DialogFooter>
|
|
|
|
|
+ </DialogContent>
|
|
|
|
|
+ </Dialog>
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|