@@ -733,6 +733,155 @@ async def notify_toss_sell_recommendation(
733733 logger .error (f"Failed to send Toss sell recommendation: { e } " )
734734 return False
735735
736+ def _format_toss_price_recommendation (
737+ self ,
738+ symbol : str ,
739+ korean_name : str ,
740+ current_price : float ,
741+ toss_quantity : int ,
742+ toss_avg_price : float ,
743+ decision : str ,
744+ confidence : float ,
745+ reasons : List [str ],
746+ appropriate_buy_min : float | None ,
747+ appropriate_buy_max : float | None ,
748+ appropriate_sell_min : float | None ,
749+ appropriate_sell_max : float | None ,
750+ buy_hope_min : float | None = None ,
751+ buy_hope_max : float | None = None ,
752+ sell_target_min : float | None = None ,
753+ sell_target_max : float | None = None ,
754+ currency : str = "원" ,
755+ ) -> str :
756+ """
757+ Format Toss price recommendation notification with AI analysis.
758+ """
759+ is_usd = currency == "$"
760+ price_fmt = lambda p : f"${ p :,.2f} " if is_usd else f"{ p :,.0f} { currency } "
761+
762+ # 수익률 계산
763+ profit_percent = ((current_price / toss_avg_price ) - 1 ) * 100 if toss_avg_price > 0 else 0
764+ profit_sign = "+" if profit_percent >= 0 else ""
765+
766+ # Decision emoji mapping
767+ decision_emoji = {"buy" : "🟢" , "hold" : "🟡" , "sell" : "🔴" }
768+ decision_text = {"buy" : "매수" , "hold" : "보유" , "sell" : "매도" }
769+ emoji = decision_emoji .get (decision .lower (), "⚪" )
770+ decision_kr = decision_text .get (decision .lower (), decision )
771+
772+ parts = [
773+ f"📊 *\\ [토스\\ ] { korean_name } ({ symbol } )*" ,
774+ "" ,
775+ f"*현재가:* { price_fmt (current_price )} " ,
776+ f"*보유:* { toss_quantity } 주 (평단가 { price_fmt (toss_avg_price )} , { profit_sign } { profit_percent :.1f} %)" ,
777+ "" ,
778+ f"{ emoji } *AI 판단:* { decision_kr } (신뢰도 { confidence :.0f} %)" ,
779+ ]
780+
781+ # 근거 추가
782+ if reasons :
783+ parts .append ("" )
784+ parts .append ("*근거:*" )
785+ for i , reason in enumerate (reasons [:3 ], 1 ):
786+ # 긴 근거는 줄임
787+ short_reason = reason [:80 ] + "..." if len (reason ) > 80 else reason
788+ parts .append (f" { i } . { short_reason } " )
789+
790+ # 가격 제안 추가
791+ parts .append ("" )
792+ parts .append ("*가격 제안:*" )
793+
794+ if appropriate_buy_min or appropriate_buy_max :
795+ buy_range = []
796+ if appropriate_buy_min :
797+ buy_range .append (price_fmt (appropriate_buy_min ))
798+ if appropriate_buy_max :
799+ buy_range .append (price_fmt (appropriate_buy_max ))
800+ parts .append (f" • 적정 매수: { ' ~ ' .join (buy_range )} " )
801+
802+ if appropriate_sell_min or appropriate_sell_max :
803+ sell_range = []
804+ if appropriate_sell_min :
805+ sell_range .append (price_fmt (appropriate_sell_min ))
806+ if appropriate_sell_max :
807+ sell_range .append (price_fmt (appropriate_sell_max ))
808+ parts .append (f" • 적정 매도: { ' ~ ' .join (sell_range )} " )
809+
810+ if buy_hope_min or buy_hope_max :
811+ hope_range = []
812+ if buy_hope_min :
813+ hope_range .append (price_fmt (buy_hope_min ))
814+ if buy_hope_max :
815+ hope_range .append (price_fmt (buy_hope_max ))
816+ parts .append (f" • 매수 희망: { ' ~ ' .join (hope_range )} " )
817+
818+ if sell_target_min or sell_target_max :
819+ target_range = []
820+ if sell_target_min :
821+ target_range .append (price_fmt (sell_target_min ))
822+ if sell_target_max :
823+ target_range .append (price_fmt (sell_target_max ))
824+ parts .append (f" • 매도 목표: { ' ~ ' .join (target_range )} " )
825+
826+ return "\n " .join (parts )
827+
828+ async def notify_toss_price_recommendation (
829+ self ,
830+ symbol : str ,
831+ korean_name : str ,
832+ current_price : float ,
833+ toss_quantity : int ,
834+ toss_avg_price : float ,
835+ decision : str ,
836+ confidence : float ,
837+ reasons : List [str ],
838+ appropriate_buy_min : float | None ,
839+ appropriate_buy_max : float | None ,
840+ appropriate_sell_min : float | None ,
841+ appropriate_sell_max : float | None ,
842+ buy_hope_min : float | None = None ,
843+ buy_hope_max : float | None = None ,
844+ sell_target_min : float | None = None ,
845+ sell_target_max : float | None = None ,
846+ currency : str = "원" ,
847+ ) -> bool :
848+ """
849+ Send Toss price recommendation notification with AI analysis.
850+
851+ Always sends regardless of AI decision (buy/hold/sell).
852+ """
853+ if not self ._enabled :
854+ return False
855+
856+ if toss_quantity <= 0 :
857+ logger .debug (f"Skipping Toss notification for { symbol } : no Toss holdings" )
858+ return False
859+
860+ try :
861+ message = self ._format_toss_price_recommendation (
862+ symbol = symbol ,
863+ korean_name = korean_name ,
864+ current_price = current_price ,
865+ toss_quantity = toss_quantity ,
866+ toss_avg_price = toss_avg_price ,
867+ decision = decision ,
868+ confidence = confidence ,
869+ reasons = reasons ,
870+ appropriate_buy_min = appropriate_buy_min ,
871+ appropriate_buy_max = appropriate_buy_max ,
872+ appropriate_sell_min = appropriate_sell_min ,
873+ appropriate_sell_max = appropriate_sell_max ,
874+ buy_hope_min = buy_hope_min ,
875+ buy_hope_max = buy_hope_max ,
876+ sell_target_min = sell_target_min ,
877+ sell_target_max = sell_target_max ,
878+ currency = currency ,
879+ )
880+ return await self ._send_to_telegram (message )
881+ except Exception as e :
882+ logger .error (f"Failed to send Toss price recommendation: { e } " )
883+ return False
884+
736885 async def test_connection (self ) -> bool :
737886 """
738887 Test Telegram connection by sending a test message.
0 commit comments