391 lines
18 KiB
TypeScript
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>
|
|
);
|
|
}
|