diff --git a/src/components/open_position_item/open_position_item.tsx b/src/components/open_position_item/open_position_item.tsx index 6c39acdbb..c9a86122e 100644 --- a/src/components/open_position_item/open_position_item.tsx +++ b/src/components/open_position_item/open_position_item.tsx @@ -18,6 +18,11 @@ import {POSITION_CLOSE_COUNTDOWN_SECONDS, FRACTION_DIGITS} from '../../constants import {MarketContext} from '../../contexts/market_context'; import {IDisplayCFDOrder} from '../../interfaces/tidebit_defi_background/display_accepted_cfd_order'; import {useTranslation} from 'react-i18next'; +import {defaultResultFailed} from '../../interfaces/tidebit_defi_background/result'; +import {IQuotation} from '../../interfaces/tidebit_defi_background/quotation'; +import {ToastTypeAndText} from '../../constants/toast_type'; +import {ToastId} from '../../constants/toast_id'; +import {Code} from '../../constants/code'; type TranslateFunction = (s: string) => string; interface IOpenPositionItemProps { @@ -33,6 +38,7 @@ const OpenPositionItem = ({openCfdDetails}: IOpenPositionItemProps) => { dataUpdateFormModalHandler, visiblePositionClosedModalHandler, dataPositionClosedModalHandler, + toast, } = useGlobal(); const openItemClickHandler = () => { @@ -67,6 +73,7 @@ const OpenPositionItem = ({openCfdDetails}: IOpenPositionItemProps) => { openCfdDetails.targetAsset, openCfdDetails.typeOfPosition ); + const pnl = toPnl({ openPrice, closePrice, @@ -94,9 +101,70 @@ const OpenPositionItem = ({openCfdDetails}: IOpenPositionItemProps) => { const denominator = remainSecs < 60 ? 60 : remainSecs < 3600 ? 60 : 24; - const squareClickHandler = () => { - dataPositionClosedModalHandler(openCfdDetails); - visiblePositionClosedModalHandler(); + const squareClickHandler = async () => { + await getQuotation(); + }; + + const toDisplayCloseOrder = (cfd: IDisplayCFDOrder, quotation: IQuotation): IDisplayCFDOrder => { + const pnlSoFar = toPnl({ + openPrice: cfd.openPrice, + closePrice: quotation.price, + amount: cfd.amount, + typeOfPosition: cfd.typeOfPosition, + spread: marketCtx.getTickerSpread(cfd.targetAsset), + }); + + return { + ...cfd, + closePrice: quotation.price, + pnl: pnlSoFar, + }; + }; + + const getQuotation = async () => { + let quotation = {...defaultResultFailed}; + const oppositeTypeOfPosition = + openCfdDetails?.typeOfPosition === TypeOfPosition.BUY + ? TypeOfPosition.SELL + : TypeOfPosition.BUY; + + try { + quotation = await marketCtx.getCFDQuotation(openCfdDetails?.ticker, oppositeTypeOfPosition); + + const data = quotation.data as IQuotation; + if ( + quotation.success && + data.typeOfPosition === oppositeTypeOfPosition && + data.ticker.split('-')[0] === openCfdDetails.ticker && + quotation.data !== null + ) { + const displayedCloseOrder = toDisplayCloseOrder(openCfdDetails, data); + dataPositionClosedModalHandler(displayedCloseOrder); + + visiblePositionClosedModalHandler(); + } else { + toast({ + type: ToastTypeAndText.ERROR.type, + toastId: ToastId.GET_QUOTATION_ERROR, + message: `${t('POSITION_MODAL.ERROR_MESSAGE')} (${ + Code.CANNOT_GET_QUOTATION_FROM_CONTEXT + })`, + typeText: t(ToastTypeAndText.ERROR.text), + isLoading: false, + autoClose: false, + }); + } + } catch (err) { + // TODO: Report error to backend (20230613 - Shirley) + toast({ + type: ToastTypeAndText.ERROR.type, + toastId: ToastId.GET_QUOTATION_ERROR, + message: `${t('POSITION_MODAL.ERROR_MESSAGE')} (${Code.UNKNOWN_ERROR_IN_COMPONENT})`, + typeText: t(ToastTypeAndText.ERROR.text), + isLoading: false, + autoClose: false, + }); + } }; /* Info: (20230411 - Julian) 折線圖參考線的優先顯示順序: diff --git a/src/components/open_sub_tab/open_sub_tab.tsx b/src/components/open_sub_tab/open_sub_tab.tsx index 8db5cbb24..fb488807b 100644 --- a/src/components/open_sub_tab/open_sub_tab.tsx +++ b/src/components/open_sub_tab/open_sub_tab.tsx @@ -18,10 +18,6 @@ const OpenSubTab = () => { const userCtx = useContext(UserContext); const marketCtx = useContext(MarketContext); - // const initState = { - // longPrice: 0, - // shortPrice: 0, - // }; //const [caledPrice, setCaledPrice, caledPriceRef] = useStateRef(initState); const [isLoading, setIsLoading] = useState(true); const [cfds, setCfds] = useState<IDisplayCFDOrder[]>([]); @@ -37,30 +33,6 @@ const OpenSubTab = () => { }; */ - // Deprecated (20230610 - Julian) - // useEffect(() => { - // const buyPrice = !!marketCtx.selectedTicker?.price - // ? roundToDecimalPlaces( - // marketCtx.selectedTicker.price * - // (1 + (marketCtx.tickerLiveStatistics?.spread ?? DEFAULT_SPREAD)), - // 2 - // ) - // : 0; - - // const sellPrice = !!marketCtx.selectedTicker?.price - // ? roundToDecimalPlaces( - // marketCtx.selectedTicker.price * - // (1 - (marketCtx.tickerLiveStatistics?.spread ?? DEFAULT_SPREAD)), - // 2 - // ) - // : 0; - - // setCaledPrice({ - // longPrice: buyPrice, - // shortPrice: sellPrice, - // }); - // }, [marketCtx.selectedTicker?.price]); - useEffect(() => { const cfdList = userCtx.openCFDs .map(cfd => { @@ -71,12 +43,6 @@ const OpenSubTab = () => { const spread = marketCtx.getTickerSpread(cfd.targetAsset); */ const tickerPrice = marketCtx.availableTickers[cfd.targetAsset]?.price; - /** - * Info: (20230428 - Shirley) - * without `positionLineGraph`, use market price to calculate - * without `market price`, use the open price of the CFD to get PNL === 0 and display `--` - * (OpenPositionItem & UpdateFormModal) - */ /** Deprecated: (20230608 - tzuhan) const currentPrice = @@ -105,9 +71,11 @@ const OpenSubTab = () => { setCfds(cfdList); }, [userCtx.openCFDs]); + useEffect(() => { setTimeout(() => setIsLoading(false), SKELETON_DISPLAY_TIME); }, [userCtx.openCFDs]); + const openPositionList = isLoading || userCtx.isLoadingCFDs ? ( <Skeleton count={10} height={150} /> diff --git a/src/components/position_closed_modal/position_closed_modal.tsx b/src/components/position_closed_modal/position_closed_modal.tsx index c4fc67c8c..5d9376d10 100644 --- a/src/components/position_closed_modal/position_closed_modal.tsx +++ b/src/components/position_closed_modal/position_closed_modal.tsx @@ -80,19 +80,20 @@ const PositionClosedModal = ({ const globalCtx = useGlobal(); const userCtx = useContext(UserContext); - const predictedClosePrice = marketCtx.predictCFDClosePrice( - openCfdDetails.targetAsset, - openCfdDetails.typeOfPosition - ); - - const spread = marketCtx.getTickerSpread(openCfdDetails.targetAsset); - const pnl = toPnl({ - openPrice: openCfdDetails.openPrice, - closePrice: predictedClosePrice, - amount: openCfdDetails.amount, - typeOfPosition: openCfdDetails.typeOfPosition, - spread: spread, - }); + /* Deprecated: changing pnl when fixed closed price + // const predictedClosePrice = marketCtx.predictCFDClosePrice( + // openCfdDetails.targetAsset, + // openCfdDetails.typeOfPosition + // ); + // const spread = marketCtx.getTickerSpread(openCfdDetails.targetAsset); + // const pnl = toPnl({ + // openPrice: openCfdDetails.openPrice, + // closePrice: predictedClosePrice, + // amount: openCfdDetails.amount, + // typeOfPosition: openCfdDetails.typeOfPosition, + // spread: spread, + // }); + */ const [quotationError, setQuotationError, quotationErrorRef] = useStateRef(false); const [quotationErrorMessage, setQuotationErrorMessage, quotationErrorMessageRef] = @@ -121,7 +122,11 @@ const PositionClosedModal = ({ const displayedGuaranteedStopSetting = !!openCfdDetails?.guaranteedStop ? 'Yes' : 'No'; const displayedPnLSymbol = - pnl?.type === ProfitState.PROFIT ? '+' : pnl?.type === ProfitState.LOSS ? '-' : ''; + openCfdDetails?.pnl?.type === ProfitState.PROFIT + ? '+' + : openCfdDetails?.pnl?.type === ProfitState.LOSS + ? '-' + : ''; const displayedTypeOfPosition = openCfdDetails?.typeOfPosition === TypeOfPosition.BUY @@ -134,16 +139,16 @@ const PositionClosedModal = ({ : t('POSITION_MODAL.TYPE_SELL'); const displayedPnLColor = - pnl.type === ProfitState.PROFIT + openCfdDetails?.pnl?.type === ProfitState.PROFIT ? TypeOfPnLColor.PROFIT - : pnl.type === ProfitState.LOSS + : openCfdDetails?.pnl?.type === ProfitState.LOSS ? TypeOfPnLColor.LOSS : TypeOfPnLColor.EQUAL; const displayedBorderColor = - pnl.type === ProfitState.PROFIT + openCfdDetails?.pnl?.type === ProfitState.PROFIT ? TypeOfBorderColor.PROFIT - : pnl.type === ProfitState.LOSS + : openCfdDetails?.pnl?.type === ProfitState.LOSS ? TypeOfBorderColor.LOSS : TypeOfBorderColor.EQUAL; @@ -163,6 +168,7 @@ const PositionClosedModal = ({ }); return { ...cfd, + closePrice: 0, pnl: pnlSoFar, }; }; @@ -273,7 +279,7 @@ const PositionClosedModal = ({ globalCtx.toast({ type: ToastTypeAndText.ERROR.type, toastId: ToastId.INCONSISTENT_TICKER_OF_QUOTATION, - message: `[dev] ${quotationErrorMessageRef.current.reason} (${quotationErrorMessageRef.current.code})`, + message: `[t] ${quotationErrorMessageRef.current.reason} (${quotationErrorMessageRef.current.code})`, typeText: t(ToastTypeAndText.ERROR.text), isLoading: false, autoClose: false, @@ -287,6 +293,15 @@ const PositionClosedModal = ({ setQuotationError(true); /* Info: (20230508 - Julian) get quotation error message */ setQuotationErrorMessage({success: false, code: quotation.code, reason: quotation.reason}); + // Deprecated: for debug (20230609 - Shirley) + globalCtx.toast({ + type: ToastTypeAndText.ERROR.type, + toastId: ToastId.UNKNOWN_ERROR_IN_COMPONENT, + message: `[t] ${quotationErrorMessageRef.current.reason} (${quotationErrorMessageRef.current.code})`, + typeText: t(ToastTypeAndText.ERROR.text), + isLoading: false, + autoClose: false, + }); } }; @@ -438,11 +453,15 @@ const PositionClosedModal = ({ autoClose: false, }); return; - } - const displayedCloseOrder = toDisplayCloseOrder(openCfdDetails, quotation); - globalCtx.dataPositionClosedModalHandler(displayedCloseOrder); + // TODO: check users' signature in userCtx (20230613 - Shirley) + } else if (quotation.ticker.split('-')[0] === openCfdDetails.ticker) { + const displayedCloseOrder = toDisplayCloseOrder(openCfdDetails, quotation); + globalCtx.dataPositionClosedModalHandler(displayedCloseOrder); - setGQuotation(quotation); + setGQuotation(quotation); + setDataRenewedStyle('text-lightWhite'); + setQuotationError(false); + } })(); }, [globalCtx.visiblePositionClosedModal]); @@ -451,7 +470,7 @@ const PositionClosedModal = ({ if (!globalCtx.visiblePositionClosedModal) { setSecondsLeft(DISPLAY_QUOTATION_RENEWAL_INTERVAL_SECONDS); setDataRenewedStyle('text-lightWhite'); - + setQuotationError(true); return; } @@ -535,10 +554,15 @@ const PositionClosedModal = ({ <div className={`${layoutInsideBorder}`}> <div className="text-lightGray">{t('POSITION_MODAL.CLOSED_PRICE')}</div> <div className={`${dataRenewedStyle}`}> - {gQuotationRef.current.price?.toLocaleString( - UNIVERSAL_NUMBER_FORMAT_LOCALE, - FRACTION_DIGITS - ) ?? 0}{' '} + {openCfdDetails.closePrice + ? openCfdDetails.closePrice.toLocaleString( + UNIVERSAL_NUMBER_FORMAT_LOCALE, + FRACTION_DIGITS + ) + : gQuotationRef.current.price?.toLocaleString( + UNIVERSAL_NUMBER_FORMAT_LOCALE, + FRACTION_DIGITS + ) ?? 0}{' '} <span className="ml-1 text-lightGray">{unitAsset}</span> </div> </div> @@ -547,7 +571,10 @@ const PositionClosedModal = ({ <div className="text-lightGray">{t('POSITION_MODAL.PNL')}</div> <div className={`${pnlRenewedStyle} ${displayedPnLColor}`}> {displayedPnLSymbol} ${' '} - {pnl?.value.toLocaleString(UNIVERSAL_NUMBER_FORMAT_LOCALE, FRACTION_DIGITS)} + {openCfdDetails?.pnl?.value.toLocaleString( + UNIVERSAL_NUMBER_FORMAT_LOCALE, + FRACTION_DIGITS + )} </div> </div> diff --git a/src/constants/toast_id.ts b/src/constants/toast_id.ts index 5168ffbc5..19de7e4cd 100644 --- a/src/constants/toast_id.ts +++ b/src/constants/toast_id.ts @@ -4,6 +4,7 @@ export type IToastId = { GET_QUOTATION_ERROR: string; INVALID_CFD_OPEN_REQUEST: string; INCONSISTENT_TICKER_OF_QUOTATION: string; + UNKNOWN_ERROR_IN_COMPONENT: string; }; export const ToastId: IToastId = { @@ -12,4 +13,5 @@ export const ToastId: IToastId = { GET_QUOTATION_ERROR: 'GetQuotationError', INVALID_CFD_OPEN_REQUEST: 'InvalidCFDOpenRequest', INCONSISTENT_TICKER_OF_QUOTATION: 'InconsistentTickerOfQuotation', + UNKNOWN_ERROR_IN_COMPONENT: 'UnknownErrorInComponent', }; diff --git a/src/contexts/global_context.tsx b/src/contexts/global_context.tsx index dc67f3203..2aa91b322 100644 --- a/src/contexts/global_context.tsx +++ b/src/contexts/global_context.tsx @@ -573,8 +573,8 @@ export const GlobalProvider = ({children}: IGlobalProvider) => { const [visiblePositionClosedModal, setVisiblePositionClosedModal, visiblePositionClosedModalRef] = useStateRef<boolean>(false); - const [dataPositionClosedModal, setDataPositionClosedModal] = - useState<IDisplayCFDOrder>(dummyOpenCFD); + const [dataPositionClosedModal, setDataPositionClosedModal, dataPositionClosedModalRef] = + useStateRef<IDisplayCFDOrder>(dummyOpenCFD); const [visiblePositionOpenModal, setVisiblePositionOpenModal] = useState(false); const [dataPositionOpenModal, setDataPositionOpenModal] = useState<IDataPositionOpenModal>( @@ -1163,7 +1163,7 @@ export const GlobalProvider = ({children}: IGlobalProvider) => { visiblePositionClosedModal, visiblePositionClosedModalRef, visiblePositionClosedModalHandler, - dataPositionClosedModal, + dataPositionClosedModal: dataPositionClosedModalRef.current, dataPositionClosedModalHandler, visiblePositionOpenModal,