diff --git a/src/Managing.Application/Bots/TradingBotBase.cs b/src/Managing.Application/Bots/TradingBotBase.cs index 666bb1b5..558277a0 100644 --- a/src/Managing.Application/Bots/TradingBotBase.cs +++ b/src/Managing.Application/Bots/TradingBotBase.cs @@ -1706,12 +1706,22 @@ public class TradingBotBase : ITradingBot if (wasStopLossHit) { - // Use actual execution price based on direction - closingPrice = position.OriginDirection == TradeDirection.Long - ? minPriceRecent // For LONG, SL hits at the low - : maxPriceRecent; // For SHORT, SL hits at the high + // For backtesting: use the configured SL price to ensure consistent PnL per money management + // For live trading: use actual execution price to reflect real market conditions (slippage) + if (Config.IsForBacktest) + { + 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.SetStatus(TradeStatus.Filled); @@ -1729,17 +1739,28 @@ public class TradingBotBase : ITradingBot await LogDebug( $"🛑 Stop Loss Execution Confirmed\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}`"); } else if (wasTakeProfitHit) { - // Use actual execution price based on direction - closingPrice = position.OriginDirection == TradeDirection.Long - ? maxPriceRecent // For LONG, TP hits at the high - : minPriceRecent; // For SHORT, TP hits at the low + // For backtesting: use the configured TP price to ensure consistent PnL per money management + // For live trading: use actual execution price to reflect real market conditions (slippage) + if (Config.IsForBacktest) + { + 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.SetStatus(TradeStatus.Filled); @@ -1752,7 +1773,8 @@ public class TradingBotBase : ITradingBot await LogDebug( $"🎯 Take Profit Execution Confirmed\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}`"); } else diff --git a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx index d19db15e..e916290c 100644 --- a/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx +++ b/src/Managing.WebApp/src/components/organism/Backtest/backtestRowDetails.tsx @@ -1,4 +1,4 @@ -import {CardPositionItem, TradeChart} from '..' +import {CardPositionItem, PositionsModal, TradeChart} from '..' import { Backtest, BacktestClient, @@ -13,6 +13,7 @@ import { import {CardPosition, CardText} from '../../mollecules' import {useQuery} from '@tanstack/react-query' import useApiUrlStore from '../../../app/store/apiStore' +import {useState} from 'react' interface IBacktestRowDetailsProps { backtest: Backtest; @@ -24,6 +25,7 @@ const BacktestRowDetails: React.FC = ({ const {apiUrl} = useApiUrlStore(); const dataClient = new DataClient({}, apiUrl); const backtestClient = new BacktestClient({}, apiUrl); + const [showPositionsModal, setShowPositionsModal] = useState(false); // Use TanStack Query to load full backtest data const {data: fullBacktestData, isLoading: isLoadingFullBacktest, error: fullBacktestError} = useQuery({ @@ -427,6 +429,14 @@ const BacktestRowDetails: React.FC = ({ content={getAverageTradesPerDay() + " trades/day"} > +
+ +
{!isLoadingCandles && !isLoadingFullBacktest && (
@@ -442,6 +452,11 @@ const BacktestRowDetails: React.FC = ({
)} + setShowPositionsModal(false)} + positions={positionsArray} + /> ) } diff --git a/src/Managing.WebApp/src/components/organism/PositionsModal/PositionsModal.tsx b/src/Managing.WebApp/src/components/organism/PositionsModal/PositionsModal.tsx new file mode 100644 index 00000000..d9f17509 --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/PositionsModal/PositionsModal.tsx @@ -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 ( + +
+ + + + + + + + + + + + + + + + + + {sortedPositions.map((position, index) => { + const exitPrice = getExitPrice(position) + const exitDate = getExitDate(position) + const pnl = position.ProfitAndLoss?.realized + const roi = calculateROI(position) + + return ( + + + + + + + + + + + + + + ) + })} + +
#Open DateClose DateDirectionEntry PriceExit PriceQuantityLeverageRealized P&LROIStatus
{index + 1}{formatDate(position.Open?.date)}{formatDate(exitDate)} + + {position.originDirection} + + {formatCurrency(position.Open?.price)}{formatCurrency(exitPrice)}{position.Open?.quantity?.toFixed(4) || 'N/A'}x{position.Open?.leverage || 1} + {formatCurrency(pnl)} + + {formatROI(roi)} + + + {position.status} + +
+ {sortedPositions.length === 0 && ( +
+ No positions found +
+ )} +
+
+ +
+
+ ) +} + +export default PositionsModal + diff --git a/src/Managing.WebApp/src/components/organism/PositionsModal/index.tsx b/src/Managing.WebApp/src/components/organism/PositionsModal/index.tsx new file mode 100644 index 00000000..d587d02e --- /dev/null +++ b/src/Managing.WebApp/src/components/organism/PositionsModal/index.tsx @@ -0,0 +1,2 @@ +export { default as PositionsModal } from './PositionsModal' + diff --git a/src/Managing.WebApp/src/components/organism/index.tsx b/src/Managing.WebApp/src/components/organism/index.tsx index 280577aa..f7add5ad 100644 --- a/src/Managing.WebApp/src/components/organism/index.tsx +++ b/src/Managing.WebApp/src/components/organism/index.tsx @@ -13,3 +13,4 @@ export { default as StatusBadge } from './StatusBadge/StatusBadge' export { default as PositionsList } from './Positions/PositionList' export { default as ScenarioModal } from './ScenarioModal' export { default as BotNameModal } from './BotNameModal/BotNameModal' +export { default as PositionsModal } from './PositionsModal/PositionsModal'