learning_ai_invt_trdg/web/src/components/EntryForm.tsx

391 lines
18 KiB
TypeScript

import { useState, useEffect } from 'react';
import type { FormEvent } from 'react';
import { useAuth } from '../components/AuthContext';
import { tradingRuntime } from '../lib/runtime';
import { createManualEntry, updateManualEntry } from '../lib/manualEntriesApi';
import { getPlatformAccessToken } from '../lib/authSession';
import { createRequestId } from '../../../shared/request-id.js';
import { Button } from './ui/button';
import { Input } from './ui/input';
interface EntryFormProps {
onSuccess: () => void;
initialData?: any;
}
const tableInputClass = 'h-9 rounded border-[var(--border)] bg-[var(--input)] px-2 py-1 text-sm';
const numericInputClass = `${tableInputClass} font-mono text-right`;
const checkboxLabelClass = 'flex cursor-pointer items-center text-[10px] text-[var(--muted-foreground)] hover:text-[var(--foreground)]';
export function EntryForm({ onSuccess, initialData }: EntryFormProps) {
const { user } = useAuth();
const [formData, setFormData] = useState({
symbol: '',
label: '',
status: 'active',
active: true,
quantity: '',
filled_quantity: '',
entry_price: '',
buy_price: '',
sell_price: '',
gain_threshold_for_sell: '',
drop_threshold_for_buy: '',
buy_time: '',
sell_time: '',
notes: '',
is_real_trade: false,
is_crypto: false,
execute_order: false,
});
useEffect(() => {
if (initialData) {
setFormData({
...initialData,
quantity: initialData.quantity || '',
filled_quantity: initialData.filled_quantity || '',
entry_price: initialData.entry_price || '',
buy_price: initialData.buy_price || '',
sell_price: initialData.sell_price || '',
gain_threshold_for_sell: initialData.gain_threshold_for_sell || '',
drop_threshold_for_buy: initialData.drop_threshold_for_buy || '',
buy_time: initialData.buy_time || '',
sell_time: initialData.sell_time || '',
// Preserve execute_order as false on edit init
execute_order: false,
});
} else {
// Reset form for new entry
setFormData({
symbol: '',
label: '',
status: 'active',
active: true,
quantity: '',
filled_quantity: '',
entry_price: '',
buy_price: '',
sell_price: '',
gain_threshold_for_sell: '',
drop_threshold_for_buy: '',
buy_time: '',
sell_time: '',
notes: '',
is_real_trade: false,
is_crypto: false,
execute_order: false,
});
}
}, [initialData]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
const { name, value, type } = e.target;
setFormData(prev => ({
...prev,
[name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : value,
}));
};
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
if (!user) return;
try {
const payload = {
...formData,
user_id: user.id,
// Convert numeric fields
quantity: formData.quantity ? parseFloat(formData.quantity) : null,
filled_quantity: formData.filled_quantity ? parseFloat(formData.filled_quantity) : null,
entry_price: formData.entry_price ? parseFloat(formData.entry_price) : null,
buy_price: formData.buy_price ? parseFloat(formData.buy_price) : null,
sell_price: formData.sell_price ? parseFloat(formData.sell_price) : null,
gain_threshold_for_sell: formData.gain_threshold_for_sell ? parseFloat(formData.gain_threshold_for_sell) : null,
drop_threshold_for_buy: formData.drop_threshold_for_buy ? parseFloat(formData.drop_threshold_for_buy) : null,
buy_time: formData.buy_time ? formData.buy_time : null,
sell_time: formData.sell_time ? formData.sell_time : null,
updated_at: new Date().toISOString(),
};
// Remove temp fields from DB payload
delete (payload as any).execute_order;
// --- REAL TRADE EXECUTION ---
if (formData.execute_order && !initialData) {
if (!Number.isFinite(payload.quantity) || !payload.symbol.trim()) {
alert('Symbol and a valid quantity are required to execute a trade.');
return;
}
// Determine side (Buy if buying, Sell if selling - simplistic for now assuming Entry = Buy)
// If closing, we handle differently (via close button usually), but here handle Entry.
const apiPayload = {
symbol: payload.symbol,
side: 'buy',
qty: payload.quantity,
type: 'market',
price: payload.buy_price,
sl: payload.drop_threshold_for_buy,
tp: payload.gain_threshold_for_sell
};
const confirmTrade = window.confirm(`EXECUTE LIVE TRADE (via Bot)?\n(Note: Uses currently configured Bot account)\n\nSymbol: ${payload.symbol}\nSide: BUY\nQty: ${payload.quantity}`);
if (!confirmTrade) return;
const apiUrl = tradingRuntime.tradingApiUrl;
const accessToken = await getPlatformAccessToken();
const response = await fetch(`${apiUrl}/api/trade`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${accessToken}`,
'x-request-id': createRequestId('web-entry')
},
body: JSON.stringify(apiPayload)
});
const resData = await response.json();
if (!resData.success) {
throw new Error(`Execution Failed: ${resData.error}`);
}
alert(`Trade Executed! Order ID: ${resData.orderId}`);
}
if (initialData?.stock_instance_id) {
await updateManualEntry(initialData.stock_instance_id, payload);
} else {
await createManualEntry({ ...payload, stock_instance_id: crypto.randomUUID() });
}
onSuccess();
// Reset only if not editing
if (!initialData) {
setFormData(prev => ({ ...prev, symbol: '', label: '', notes: '' }));
}
} catch (error: any) {
alert(`Error saving entry: ${error.message}`);
}
};
return (
<div className="entry-form-container">
{!initialData && (
<div className="tab-header mb-4">
<h2>New Entry / Watchlist</h2>
<p>Add a position or watchlist item.</p>
</div>
)}
<form onSubmit={handleSubmit} className="table-container">
<table className="pro-table">
<thead>
<tr>
<th style={{ width: '15%' }}>Symbol</th>
<th style={{ width: '10%' }}>Tag</th>
<th style={{ width: '15%' }}>Options</th>
<th style={{ width: '12%' }} className="text-right">Buy Price</th>
<th style={{ width: '10%' }} className="text-right">Qty</th>
<th style={{ width: '10%' }} className="text-right">Stop Loss</th>
<th style={{ width: '10%' }} className="text-right">Take Profit</th>
<th style={{ width: '12%' }}>Buy Time</th>
<th style={{ width: '10%' }} className="text-right">Exit Price</th>
<th style={{ width: '11%' }}>Actions</th>
</tr>
</thead>
<tbody>
<tr>
{/* Symbol */}
<td>
<Input
type="text"
name="symbol"
required
value={formData.symbol}
onChange={handleChange}
placeholder="BTC/USD"
className={tableInputClass}
/>
</td>
{/* Label */}
<td>
<Input
type="text"
name="label"
value={formData.label}
onChange={handleChange}
placeholder="Swing"
className={tableInputClass}
/>
</td>
{/* Options (Real/Crypto) */}
<td>
<div className="flex flex-col gap-1">
<label className="flex cursor-pointer items-center text-xs text-amber-600 hover:text-amber-500 dark:text-amber-400 dark:hover:text-amber-300" title="Places actual order on Bot">
<input
type="checkbox"
name="execute_order"
checked={formData.execute_order}
onChange={handleChange}
className="mr-1.5 accent-yellow-500"
/>
Execute
</label>
<div className="flex items-center space-x-2">
<label className={checkboxLabelClass} title="Marks as Active Trade in Journal">
<input
type="checkbox"
name="is_real_trade"
checked={formData.is_real_trade}
onChange={handleChange}
className="mr-1 accent-blue-500 scale-75"
/>
Trade
</label>
<label className={checkboxLabelClass}>
<input
type="checkbox"
name="is_crypto"
checked={formData.is_crypto}
onChange={handleChange}
className="mr-1 accent-purple-500 scale-75"
/>
Crypto
</label>
</div>
</div>
</td>
{/* Buy Price */}
<td>
<Input
type="number"
name="buy_price"
step="any"
value={formData.buy_price}
onChange={handleChange}
className={`${numericInputClass} text-emerald-600 dark:text-emerald-400`}
placeholder="0.00"
/>
</td>
{/* Qty */}
<td>
<Input
type="number"
name="quantity"
step="any"
value={formData.quantity}
onChange={handleChange}
className={numericInputClass}
placeholder="0"
/>
</td>
{/* SL */}
<td>
<Input
type="number"
name="drop_threshold_for_buy"
step="any"
value={formData.drop_threshold_for_buy}
onChange={handleChange}
className={`${numericInputClass} text-red-600 dark:text-red-400`}
placeholder="SL"
/>
</td>
{/* TP */}
<td>
<Input
type="number"
name="gain_threshold_for_sell"
step="any"
value={formData.gain_threshold_for_sell}
onChange={handleChange}
className={`${numericInputClass} text-blue-600 dark:text-blue-400`}
placeholder="TP"
/>
</td>
{/* Time */}
<td>
<Input
type="datetime-local"
name="buy_time"
value={formData.buy_time ? new Date(formData.buy_time).toISOString().slice(0, 16) : ''}
onChange={(e) => setFormData(prev => ({ ...prev, buy_time: new Date(e.target.value).toISOString() }))}
className="h-9 rounded border-[var(--border)] bg-[var(--input)] px-2 py-1 font-mono text-xs"
/>
</td>
{/* Sell Price (Exit) */}
<td className="relative">
<Input
type="number"
name="sell_price"
step="any"
value={formData.sell_price}
onChange={handleChange}
className={`${numericInputClass} text-purple-600 dark:text-purple-400`}
placeholder="Exit"
/>
{formData.sell_price && formData.buy_price && (
<div className={`absolute top-0 right-0 -mt-3 text-[9px] font-bold ${(Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 0) >= 0
? 'text-green-500'
: 'text-red-500'
}`}>
${((Number(formData.sell_price) - Number(formData.buy_price)) * Number(formData.quantity || 1)).toFixed(2)}
</div>
)}
</td>
{/* Actions */}
<td>
<div className="flex items-center justify-end gap-2">
{initialData && (
<Button
type="button"
onClick={onSuccess}
variant="ghost"
size="sm"
className="h-8 text-xs underline"
>
Cancel
</Button>
)}
<Button
type="submit"
size="sm"
className="h-8 text-xs"
>
{initialData ? 'Update' : 'Add'}
</Button>
</div>
</td>
</tr>
{/* Notes Row */}
<tr>
<td colSpan={10} className="border-t border-[var(--border)] pt-2">
<Input
type="text"
name="notes"
value={formData.notes}
onChange={handleChange}
placeholder="Add strategy notes here..."
className="h-9 border-transparent bg-transparent px-0 text-xs italic shadow-none focus:border-transparent focus:ring-0"
/>
</td>
</tr>
</tbody>
</table>
</form>
</div>
);
}