import time import threading import logging from sqlalchemy.orm import Session from database import SessionLocal, TradeSetting, Order, Stock, AccountBalance, Holding import datetime from kis_api import kis from telegram_notifier import notifier logger = logging.getLogger("TRADER") class TradingBot: def __init__(self): self.is_running = False self.thread = None self.holdings = {} # Local cache for holdings: {code: {qty: int, price: float}} self.last_chart_update = 0 def refresh_assets(self): """ Fetch Balance and Holdings from KIS and save to DB """ logger.info("Syncing Assets to Database...") db = SessionLocal() try: # 1. Domestic Balance balance = kis.get_balance(source="Automated_Sync") if balance and 'output2' in balance and balance['output2']: summary = balance['output2'][0] # Upsert AccountBalance fn_status = db.query(AccountBalance).first() if not fn_status: fn_status = AccountBalance() db.add(fn_status) fn_status.total_eval = float(summary['tot_evlu_amt']) fn_status.deposit = float(summary['dnca_tot_amt']) fn_status.total_profit = float(summary['evlu_pfls_smtl_amt']) fn_status.updated_at = datetime.datetime.now() # 2. Holdings (Domestic) # Clear existing DOMESTIC db.query(Holding).filter(Holding.market == 'DOMESTIC').delete() if balance and 'output1' in balance: self.holdings = {} # Keep memory cache for trading logic for item in balance['output1']: code = item['pdno'] qty = int(item['hldg_qty']) if qty > 0: buy_price = float(item['pchs_avg_pric']) current_price = float(item['prpr']) profit_rate = float(item['evlu_pfls_rt']) # Save to DB db.add(Holding( code=code, name=item['prdt_name'], quantity=qty, price=buy_price, current_price=current_price, profit_rate=profit_rate, market="DOMESTIC" )) # Memory Cache for Trade Logic self.holdings[code] = {'qty': qty, 'price': buy_price} # 3. Overseas Balance (NASD default) # TODO: Multi-market support if needed overseas = kis.get_overseas_balance(exchange="NASD") # Clear existing NASD db.query(Holding).filter(Holding.market == 'NASD').delete() if overseas and 'output1' in overseas: for item in overseas['output1']: qty = float(item['ovrs_cblc_qty']) # Overseas can be fractional? KIS is usually int but check. if qty > 0: code = item['ovrs_pdno'] # name = item.get('ovrs_item_name') or item.get('prdt_name') # KIS overseas output keys vary. db.add(Holding( code=code, name=item.get('ovrs_item_name', code), quantity=int(qty), price=float(item.get('frcr_pchs_amt1', 0)), # Avg Price? Check API current_price=float(item.get('now_pric2', 0)), profit_rate=float(item.get('evlu_pfls_rt', 0)), market="NASD" )) db.commit() logger.info("Assets Synced Successfully.") except Exception as e: logger.error(f"Failed to sync assets: {e}") db.rollback() finally: db.close() def start(self): if self.is_running: return self.is_running = True self.refresh_assets() # Fetch on start self.thread = threading.Thread(target=self._run_loop, daemon=True) self.thread.start() logger.info("Trading Bot Started") def stop(self): self.is_running = False if self.thread: self.thread.join() logger.info("Trading Bot Stopped") def _run_loop(self): while self.is_running: try: self._process_cycle() except Exception as e: logger.error(f"Error in trading loop: {e}") # Sleep 1 second to avoid hammering time.sleep(1) def _process_cycle(self): db = SessionLocal() try: # Get active trade settings settings = db.query(TradeSetting).filter(TradeSetting.is_active == True).all() for setting in settings: self._check_and_trade(db, setting) finally: db.close() def _check_and_trade(self, db: Session, setting: TradeSetting): code = setting.code # Get Current Price # Optimization: Ideally read from a shared cache from WebSocket # For now, we still poll price or should use WS logic? # User said "Websocket... automatic decision". # But trader.py is isolated. # For simplicity in this step (removing balance poll), we keep price fetch but remove balance poll. price_data = kis.get_current_price(code) if not price_data: return current_price = float(price_data.get('stck_prpr', 0)) if current_price == 0: return # Check holdings from Cache if code not in self.holdings: return # No holdings, nothing to sell (if logic is Sell) holding = self.holdings[code] holding_qty = holding['qty'] # SELL Logic if holding_qty > 0: # Stop Loss if setting.stop_loss_price and current_price <= setting.stop_loss_price: logger.info(f"Stop Loss Triggered for {code}. Price: {current_price}, SL: {setting.stop_loss_price}") self._place_order(db, code, 'sell', holding_qty, 0) # 0 means Market Price return # Target Profit if setting.target_price and current_price >= setting.target_price: logger.info(f"Target Price Triggered for {code}. Price: {current_price}, TP: {setting.target_price}") self._place_order(db, code, 'sell', holding_qty, 0) return def _place_order(self, db: Session, code: str, type: str, qty: int, price: int): logger.info(f"Placing Order: {code} {type} {qty} @ {price}") res = kis.place_order(code, type, qty, price) status = "FAILED" order_id = "" if res and res.get('rt_cd') == '0': status = "PENDING" order_id = res.get('output', {}).get('ODNO', '') logger.info(f"Order Success: {order_id}") notifier.send_message(f"🔔 주문 전송 완료\n[{type.upper()}] {code}\n수량: {qty}\n가격: {price if price > 0 else '시장가'}") # Optimistic Update or Refresh? # User said "If execution happens, update list". # We should schedule a refresh. time.sleep(1) # Wait for execution self.refresh_assets() else: logger.error(f"Order Failed: {res}") notifier.send_message(f"⚠️ 주문 실패\n[{type.upper()}] {code}\n이유: {res}") # Record to DB new_order = Order( code=code, order_id=order_id, type=type.upper(), price=price, quantity=qty, status=status ) db.add(new_order) db.commit() trader = TradingBot()