fix(web): require active profile for simple trades
This commit is contained in:
parent
feee2028aa
commit
b33afc6c8c
@ -5,6 +5,7 @@ import { fetchChartBars } from '../lib/marketApi';
|
|||||||
import { getPlatformAccessToken } from '../lib/authSession';
|
import { getPlatformAccessToken } from '../lib/authSession';
|
||||||
import { tradingRuntime } from '../lib/runtime';
|
import { tradingRuntime } from '../lib/runtime';
|
||||||
import { createRequestId } from '../../../shared/request-id.js';
|
import { createRequestId } from '../../../shared/request-id.js';
|
||||||
|
import { fetchTradeProfiles, type TradeProfilePayload } from '../lib/profileApi';
|
||||||
|
|
||||||
type SimpleSide = 'buy' | 'sell';
|
type SimpleSide = 'buy' | 'sell';
|
||||||
type SimpleOrderType = 'market' | 'limit';
|
type SimpleOrderType = 'market' | 'limit';
|
||||||
@ -22,6 +23,7 @@ type SimpleTradeDraft = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type SimpleTradePayload = {
|
type SimpleTradePayload = {
|
||||||
|
profile_id?: string;
|
||||||
symbol: string;
|
symbol: string;
|
||||||
side: SimpleSide;
|
side: SimpleSide;
|
||||||
qty: number;
|
qty: number;
|
||||||
@ -144,6 +146,8 @@ function buildExecutionPreview(payload: SimpleTradePayload | null) {
|
|||||||
|
|
||||||
export function SimpleView() {
|
export function SimpleView() {
|
||||||
const { botState } = useAppContext();
|
const { botState } = useAppContext();
|
||||||
|
const [profiles, setProfiles] = useState<TradeProfilePayload[]>([]);
|
||||||
|
const [selectedProfileId, setSelectedProfileId] = useState('');
|
||||||
const [draft, setDraft] = useState<SimpleTradeDraft>(DEFAULT_DRAFT);
|
const [draft, setDraft] = useState<SimpleTradeDraft>(DEFAULT_DRAFT);
|
||||||
const [submitting, setSubmitting] = useState(false);
|
const [submitting, setSubmitting] = useState(false);
|
||||||
const [loadingPrice, setLoadingPrice] = useState(false);
|
const [loadingPrice, setLoadingPrice] = useState(false);
|
||||||
@ -154,6 +158,43 @@ export function SimpleView() {
|
|||||||
const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0;
|
const livePrice = normalizedSymbol ? Number(botState.symbols?.[normalizedSymbol]?.price || 0) : 0;
|
||||||
const resolvedMarketPrice = parsePositiveNumber(draft.currentMarketPrice);
|
const resolvedMarketPrice = parsePositiveNumber(draft.currentMarketPrice);
|
||||||
const marketPriceSource = livePrice > 0 ? 'live' : (resolvedMarketPrice !== null ? 'recent_close' : null);
|
const marketPriceSource = livePrice > 0 ? 'live' : (resolvedMarketPrice !== null ? 'recent_close' : null);
|
||||||
|
const activeProfiles = useMemo(
|
||||||
|
() => profiles.filter((profile) => Boolean(profile.is_active)),
|
||||||
|
[profiles],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
|
async function loadProfiles() {
|
||||||
|
try {
|
||||||
|
const rows = await fetchTradeProfiles();
|
||||||
|
if (cancelled) return;
|
||||||
|
setProfiles(rows);
|
||||||
|
} catch (err: any) {
|
||||||
|
if (cancelled) return;
|
||||||
|
setError(err?.message ?? 'Failed to load trade profiles');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadProfiles();
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeProfiles.length) {
|
||||||
|
setSelectedProfileId('');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedProfileId && activeProfiles.some((profile) => profile.id === selectedProfileId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedProfileId(String(activeProfiles[0]?.id || ''));
|
||||||
|
}, [activeProfiles, selectedProfileId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!livePrice) return;
|
if (!livePrice) return;
|
||||||
@ -221,7 +262,14 @@ export function SimpleView() {
|
|||||||
setMessage(null);
|
setMessage(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (!activeProfiles.length) {
|
||||||
|
throw new Error('No active trade profile available. Activate a profile first.');
|
||||||
|
}
|
||||||
|
|
||||||
const payload = buildSimpleTradePayload(draft);
|
const payload = buildSimpleTradePayload(draft);
|
||||||
|
if (selectedProfileId) {
|
||||||
|
payload.profile_id = selectedProfileId;
|
||||||
|
}
|
||||||
const accessToken = await getPlatformAccessToken();
|
const accessToken = await getPlatformAccessToken();
|
||||||
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/trade`, {
|
const response = await fetch(`${tradingRuntime.tradingApiUrl}/api/trade`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -315,6 +363,23 @@ export function SimpleView() {
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
|
<span style={{ fontSize: 12, fontWeight: 700, color: '#374151' }}>Profile</span>
|
||||||
|
<select
|
||||||
|
value={selectedProfileId}
|
||||||
|
onChange={e => setSelectedProfileId(e.target.value)}
|
||||||
|
disabled={!activeProfiles.length}
|
||||||
|
style={{ border: '1px solid #D1D5DB', borderRadius: 10, padding: '10px 12px', fontSize: 14, background: '#fff' }}
|
||||||
|
>
|
||||||
|
{!activeProfiles.length && <option value="">No active profile</option>}
|
||||||
|
{activeProfiles.map((profile) => (
|
||||||
|
<option key={profile.id} value={profile.id}>
|
||||||
|
{profile.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
<label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
<label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||||
<span style={{ fontSize: 12, fontWeight: 700, color: '#374151' }}>Quantity</span>
|
<span style={{ fontSize: 12, fontWeight: 700, color: '#374151' }}>Quantity</span>
|
||||||
<input
|
<input
|
||||||
@ -435,6 +500,11 @@ export function SimpleView() {
|
|||||||
<div style={{ color: '#111827', fontSize: 14, fontWeight: 700 }}>
|
<div style={{ color: '#111827', fontSize: 14, fontWeight: 700 }}>
|
||||||
{executionPreview ?? 'Complete the fields to preview the direct trade request.'}
|
{executionPreview ?? 'Complete the fields to preview the direct trade request.'}
|
||||||
</div>
|
</div>
|
||||||
|
{!activeProfiles.length && (
|
||||||
|
<div style={{ color: '#B91C1C', fontSize: 13, fontWeight: 800, marginTop: 8 }}>
|
||||||
|
No active trade profile is available. Activate one in Strategies before submitting.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{payloadPreview?.sl && (
|
{payloadPreview?.sl && (
|
||||||
<div style={{ color: '#B91C1C', fontSize: 13, fontWeight: 800, marginTop: 8 }}>
|
<div style={{ color: '#B91C1C', fontSize: 13, fontWeight: 800, marginTop: 8 }}>
|
||||||
Stop loss: {payloadPreview.sl.toFixed(4)}
|
Stop loss: {payloadPreview.sl.toFixed(4)}
|
||||||
@ -466,7 +536,7 @@ export function SimpleView() {
|
|||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={submitting}
|
disabled={submitting || !activeProfiles.length}
|
||||||
style={{
|
style={{
|
||||||
border: 'none',
|
border: 'none',
|
||||||
borderRadius: 999,
|
borderRadius: 999,
|
||||||
@ -475,7 +545,8 @@ export function SimpleView() {
|
|||||||
padding: '12px 18px',
|
padding: '12px 18px',
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: 800,
|
fontWeight: 800,
|
||||||
cursor: submitting ? 'wait' : 'pointer',
|
cursor: submitting ? 'wait' : (!activeProfiles.length ? 'not-allowed' : 'pointer'),
|
||||||
|
opacity: !activeProfiles.length ? 0.6 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{submitting ? 'Submitting…' : `Submit ${draft.side === 'buy' ? 'Buy' : 'Sell'}`}
|
{submitting ? 'Submitting…' : `Submit ${draft.side === 'buy' ? 'Buy' : 'Sell'}`}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user