Fix SLTP for backtests
This commit is contained in:
@@ -1706,12 +1706,22 @@ public class TradingBotBase : ITradingBot
|
|||||||
|
|
||||||
if (wasStopLossHit)
|
if (wasStopLossHit)
|
||||||
{
|
{
|
||||||
// Use actual execution price based on direction
|
// For backtesting: use the configured SL price to ensure consistent PnL per money management
|
||||||
closingPrice = position.OriginDirection == TradeDirection.Long
|
// For live trading: use actual execution price to reflect real market conditions (slippage)
|
||||||
? minPriceRecent // For LONG, SL hits at the low
|
if (Config.IsForBacktest)
|
||||||
: maxPriceRecent; // For SHORT, SL hits at the high
|
{
|
||||||
|
closingPrice = position.StopLoss.Price;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use actual execution price based on direction for live trading
|
||||||
|
closingPrice = position.OriginDirection == TradeDirection.Long
|
||||||
|
? minPriceRecent // For LONG, SL hits at the low
|
||||||
|
: maxPriceRecent; // For SHORT, SL hits at the high
|
||||||
|
|
||||||
|
position.StopLoss.Price = closingPrice;
|
||||||
|
}
|
||||||
|
|
||||||
position.StopLoss.SetPrice(closingPrice, 2);
|
|
||||||
position.StopLoss.SetDate(currentCandle.Date);
|
position.StopLoss.SetDate(currentCandle.Date);
|
||||||
position.StopLoss.SetStatus(TradeStatus.Filled);
|
position.StopLoss.SetStatus(TradeStatus.Filled);
|
||||||
|
|
||||||
@@ -1729,17 +1739,28 @@ public class TradingBotBase : ITradingBot
|
|||||||
await LogDebug(
|
await LogDebug(
|
||||||
$"🛑 Stop Loss Execution Confirmed\n" +
|
$"🛑 Stop Loss Execution Confirmed\n" +
|
||||||
$"Position: `{position.Identifier}`\n" +
|
$"Position: `{position.Identifier}`\n" +
|
||||||
$"SL Price: `${closingPrice:F2}` was hit (was `${position.StopLoss.Price:F2}`)\n" +
|
$"Closing Price: `${closingPrice:F2}`\n" +
|
||||||
|
$"Configured SL: `${position.StopLoss.Price:F2}`\n" +
|
||||||
$"Recent Low: `${minPriceRecent:F2}` | Recent High: `${maxPriceRecent:F2}`");
|
$"Recent Low: `${minPriceRecent:F2}` | Recent High: `${maxPriceRecent:F2}`");
|
||||||
}
|
}
|
||||||
else if (wasTakeProfitHit)
|
else if (wasTakeProfitHit)
|
||||||
{
|
{
|
||||||
// Use actual execution price based on direction
|
// For backtesting: use the configured TP price to ensure consistent PnL per money management
|
||||||
closingPrice = position.OriginDirection == TradeDirection.Long
|
// For live trading: use actual execution price to reflect real market conditions (slippage)
|
||||||
? maxPriceRecent // For LONG, TP hits at the high
|
if (Config.IsForBacktest)
|
||||||
: minPriceRecent; // For SHORT, TP hits at the low
|
{
|
||||||
|
closingPrice = position.TakeProfit1.Price;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Use actual execution price based on direction for live trading
|
||||||
|
closingPrice = position.OriginDirection == TradeDirection.Long
|
||||||
|
? maxPriceRecent // For LONG, TP hits at the high
|
||||||
|
: minPriceRecent; // FOR SHORT, TP hits at the low
|
||||||
|
|
||||||
|
position.TakeProfit1.Price = closingPrice;
|
||||||
|
}
|
||||||
|
|
||||||
position.TakeProfit1.SetPrice(closingPrice, 2);
|
|
||||||
position.TakeProfit1.SetDate(currentCandle.Date);
|
position.TakeProfit1.SetDate(currentCandle.Date);
|
||||||
position.TakeProfit1.SetStatus(TradeStatus.Filled);
|
position.TakeProfit1.SetStatus(TradeStatus.Filled);
|
||||||
|
|
||||||
@@ -1752,7 +1773,8 @@ public class TradingBotBase : ITradingBot
|
|||||||
await LogDebug(
|
await LogDebug(
|
||||||
$"🎯 Take Profit Execution Confirmed\n" +
|
$"🎯 Take Profit Execution Confirmed\n" +
|
||||||
$"Position: `{position.Identifier}`\n" +
|
$"Position: `{position.Identifier}`\n" +
|
||||||
$"TP Price: `${closingPrice:F2}` was hit (was `${position.TakeProfit1.Price:F2}`)\n" +
|
$"Closing Price: `${closingPrice:F2}`\n" +
|
||||||
|
$"Configured TP: `${position.TakeProfit1.Price:F2}`\n" +
|
||||||
$"Recent Low: `${minPriceRecent:F2}` | Recent High: `${maxPriceRecent:F2}`");
|
$"Recent Low: `${minPriceRecent:F2}` | Recent High: `${maxPriceRecent:F2}`");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {CardPositionItem, TradeChart} from '..'
|
import {CardPositionItem, PositionsModal, TradeChart} from '..'
|
||||||
import {
|
import {
|
||||||
Backtest,
|
Backtest,
|
||||||
BacktestClient,
|
BacktestClient,
|
||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
import {CardPosition, CardText} from '../../mollecules'
|
import {CardPosition, CardText} from '../../mollecules'
|
||||||
import {useQuery} from '@tanstack/react-query'
|
import {useQuery} from '@tanstack/react-query'
|
||||||
import useApiUrlStore from '../../../app/store/apiStore'
|
import useApiUrlStore from '../../../app/store/apiStore'
|
||||||
|
import {useState} from 'react'
|
||||||
|
|
||||||
interface IBacktestRowDetailsProps {
|
interface IBacktestRowDetailsProps {
|
||||||
backtest: Backtest;
|
backtest: Backtest;
|
||||||
@@ -24,6 +25,7 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
|||||||
const {apiUrl} = useApiUrlStore();
|
const {apiUrl} = useApiUrlStore();
|
||||||
const dataClient = new DataClient({}, apiUrl);
|
const dataClient = new DataClient({}, apiUrl);
|
||||||
const backtestClient = new BacktestClient({}, apiUrl);
|
const backtestClient = new BacktestClient({}, apiUrl);
|
||||||
|
const [showPositionsModal, setShowPositionsModal] = useState(false);
|
||||||
|
|
||||||
// Use TanStack Query to load full backtest data
|
// Use TanStack Query to load full backtest data
|
||||||
const {data: fullBacktestData, isLoading: isLoadingFullBacktest, error: fullBacktestError} = useQuery({
|
const {data: fullBacktestData, isLoading: isLoadingFullBacktest, error: fullBacktestError} = useQuery({
|
||||||
@@ -427,6 +429,14 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
|||||||
content={getAverageTradesPerDay() + " trades/day"}
|
content={getAverageTradesPerDay() + " trades/day"}
|
||||||
></CardText>
|
></CardText>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="p-5">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() => setShowPositionsModal(true)}
|
||||||
|
>
|
||||||
|
View All Positions ({positionsArray.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
{!isLoadingCandles && !isLoadingFullBacktest && (
|
{!isLoadingCandles && !isLoadingFullBacktest && (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<figure className="w-full">
|
<figure className="w-full">
|
||||||
@@ -442,6 +452,11 @@ const BacktestRowDetails: React.FC<IBacktestRowDetailsProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<PositionsModal
|
||||||
|
showModal={showPositionsModal}
|
||||||
|
onClose={() => setShowPositionsModal(false)}
|
||||||
|
positions={positionsArray}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {Position, TradeDirection} from '../../../generated/ManagingApi'
|
||||||
|
import {Modal} from '../../mollecules'
|
||||||
|
|
||||||
|
interface PositionsModalProps {
|
||||||
|
showModal: boolean
|
||||||
|
onClose: () => void
|
||||||
|
positions: Position[]
|
||||||
|
}
|
||||||
|
|
||||||
|
function PositionsModal({ showModal, onClose, positions }: PositionsModalProps) {
|
||||||
|
// Sort positions by date (most recent first)
|
||||||
|
const sortedPositions = [...positions].sort((a, b) => {
|
||||||
|
return new Date(b.date).getTime() - new Date(a.date).getTime()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Helper function to get exit price based on PnL
|
||||||
|
const getExitPrice = (position: Position): number | null => {
|
||||||
|
if (position.ProfitAndLoss?.realized != null) {
|
||||||
|
if (position.ProfitAndLoss.realized > 0) {
|
||||||
|
return position.TakeProfit1?.price || null
|
||||||
|
} else {
|
||||||
|
return position.StopLoss?.price || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get exit date based on PnL
|
||||||
|
const getExitDate = (position: Position): Date | null => {
|
||||||
|
if (position.ProfitAndLoss?.realized != null) {
|
||||||
|
if (position.ProfitAndLoss.realized > 0) {
|
||||||
|
return position.TakeProfit1?.date || null
|
||||||
|
} else {
|
||||||
|
return position.StopLoss?.date || null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format date
|
||||||
|
const formatDate = (date: Date | null): string => {
|
||||||
|
if (!date) return 'N/A'
|
||||||
|
return new Date(date).toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format currency
|
||||||
|
const formatCurrency = (value: number | null | undefined): string => {
|
||||||
|
if (value == null) return 'N/A'
|
||||||
|
return `$${value.toFixed(4)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get PnL color class
|
||||||
|
const getPnLColorClass = (pnl: number | undefined | null): string => {
|
||||||
|
if (pnl == null) return ''
|
||||||
|
return pnl > 0 ? 'text-success' : pnl < 0 ? 'text-error' : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to calculate ROI
|
||||||
|
const calculateROI = (position: Position): number | null => {
|
||||||
|
const pnl = position.ProfitAndLoss?.realized
|
||||||
|
if (pnl == null || !position.Open) return null
|
||||||
|
|
||||||
|
// Calculate initial position value (considering leverage)
|
||||||
|
const entryPrice = position.Open.price
|
||||||
|
const quantity = position.Open.quantity
|
||||||
|
const leverage = position.Open.leverage || 1
|
||||||
|
const fee = position.Open.fee || 0
|
||||||
|
|
||||||
|
// Initial capital invested = (Entry Price * Quantity) / Leverage + Fee
|
||||||
|
const initialCapital = (entryPrice * quantity / leverage) + fee
|
||||||
|
|
||||||
|
if (initialCapital === 0) return null
|
||||||
|
|
||||||
|
// ROI = (Net PnL / Initial Capital) * 100
|
||||||
|
const roi = (pnl / initialCapital) * 100
|
||||||
|
return roi
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format ROI
|
||||||
|
const formatROI = (roi: number | null): string => {
|
||||||
|
if (roi == null) return 'N/A'
|
||||||
|
return `${roi.toFixed(2)}%`
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
showModal={showModal}
|
||||||
|
onClose={onClose}
|
||||||
|
titleHeader="Backtest Positions"
|
||||||
|
size="7xl"
|
||||||
|
>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="table table-zebra table-sm w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th className="text-xs">#</th>
|
||||||
|
<th className="text-xs">Open Date</th>
|
||||||
|
<th className="text-xs">Close Date</th>
|
||||||
|
<th className="text-xs">Direction</th>
|
||||||
|
<th className="text-xs">Entry Price</th>
|
||||||
|
<th className="text-xs">Exit Price</th>
|
||||||
|
<th className="text-xs">Quantity</th>
|
||||||
|
<th className="text-xs">Leverage</th>
|
||||||
|
<th className="text-xs">Realized P&L</th>
|
||||||
|
<th className="text-xs">ROI</th>
|
||||||
|
<th className="text-xs">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedPositions.map((position, index) => {
|
||||||
|
const exitPrice = getExitPrice(position)
|
||||||
|
const exitDate = getExitDate(position)
|
||||||
|
const pnl = position.ProfitAndLoss?.realized
|
||||||
|
const roi = calculateROI(position)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={position.identifier || index}>
|
||||||
|
<td className="text-xs">{index + 1}</td>
|
||||||
|
<td className="text-xs">{formatDate(position.Open?.date)}</td>
|
||||||
|
<td className="text-xs">{formatDate(exitDate)}</td>
|
||||||
|
<td className="text-xs">
|
||||||
|
<span className={`badge badge-sm ${
|
||||||
|
position.originDirection === TradeDirection.Long
|
||||||
|
? 'badge-success'
|
||||||
|
: 'badge-error'
|
||||||
|
}`}>
|
||||||
|
{position.originDirection}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="text-xs">{formatCurrency(position.Open?.price)}</td>
|
||||||
|
<td className="text-xs">{formatCurrency(exitPrice)}</td>
|
||||||
|
<td className="text-xs">{position.Open?.quantity?.toFixed(4) || 'N/A'}</td>
|
||||||
|
<td className="text-xs">x{position.Open?.leverage || 1}</td>
|
||||||
|
<td className={`text-xs font-semibold ${getPnLColorClass(pnl)}`}>
|
||||||
|
{formatCurrency(pnl)}
|
||||||
|
</td>
|
||||||
|
<td className={`text-xs font-semibold ${getPnLColorClass(roi)}`}>
|
||||||
|
{formatROI(roi)}
|
||||||
|
</td>
|
||||||
|
<td className="text-xs">
|
||||||
|
<span className="badge badge-sm badge-outline">
|
||||||
|
{position.status}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{sortedPositions.length === 0 && (
|
||||||
|
<div className="text-center py-8 text-base-content/60">
|
||||||
|
No positions found
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-action">
|
||||||
|
<button type="button" className="btn" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PositionsModal
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as PositionsModal } from './PositionsModal'
|
||||||
|
|
||||||
@@ -13,3 +13,4 @@ export { default as StatusBadge } from './StatusBadge/StatusBadge'
|
|||||||
export { default as PositionsList } from './Positions/PositionList'
|
export { default as PositionsList } from './Positions/PositionList'
|
||||||
export { default as ScenarioModal } from './ScenarioModal'
|
export { default as ScenarioModal } from './ScenarioModal'
|
||||||
export { default as BotNameModal } from './BotNameModal/BotNameModal'
|
export { default as BotNameModal } from './BotNameModal/BotNameModal'
|
||||||
|
export { default as PositionsModal } from './PositionsModal/PositionsModal'
|
||||||
|
|||||||
Reference in New Issue
Block a user