코드 변경
This commit is contained in:
1
.claude/worktrees/naughty-carson
Submodule
1
.claude/worktrees/naughty-carson
Submodule
Submodule .claude/worktrees/naughty-carson added at 3a08cd8609
@@ -74,6 +74,8 @@ async def _get_preset(preset_key: str) -> Optional[Dict[str, Any]]:
|
|||||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": strategy_cfg.get("TAKE_PROFIT_PERCENT", 12.0),
|
"MEAN_REVERSION_TAKE_PROFIT_PCT": strategy_cfg.get("TAKE_PROFIT_PERCENT", 12.0),
|
||||||
"MAX_TOTAL_POSITIONS": strategy_cfg.get("MAX_HOLDINGS_COUNT", 5),
|
"MAX_TOTAL_POSITIONS": strategy_cfg.get("MAX_HOLDINGS_COUNT", 5),
|
||||||
"MEAN_REVERSION_RSI_THRESHOLD": strategy_cfg.get("MEAN_REVERSION_RSI_THRESHOLD", 30.0),
|
"MEAN_REVERSION_RSI_THRESHOLD": strategy_cfg.get("MEAN_REVERSION_RSI_THRESHOLD", 30.0),
|
||||||
|
"RSI_OVERBOUGHT_LEVEL": strategy_cfg.get("RSI_OVERBOUGHT_LEVEL", 70.0),
|
||||||
|
"ATR_STOP_MULTIPLIER": strategy_cfg.get("ATR_STOP_MULTIPLIER", 2.5),
|
||||||
}
|
}
|
||||||
return {"name": "현재 설정", "params": params}
|
return {"name": "현재 설정", "params": params}
|
||||||
return _STRATEGY_PRESETS.get(preset_key)
|
return _STRATEGY_PRESETS.get(preset_key)
|
||||||
|
|||||||
@@ -268,9 +268,10 @@ class BacktestEngine:
|
|||||||
rsi_threshold = params.get("MEAN_REVERSION_RSI_THRESHOLD", 30)
|
rsi_threshold = params.get("MEAN_REVERSION_RSI_THRESHOLD", 30)
|
||||||
stop_loss = params.get("MEAN_REVERSION_STOP_LOSS_PCT", -6.0) / 100
|
stop_loss = params.get("MEAN_REVERSION_STOP_LOSS_PCT", -6.0) / 100
|
||||||
take_profit = params.get("MEAN_REVERSION_TAKE_PROFIT_PCT", 12.0) / 100
|
take_profit = params.get("MEAN_REVERSION_TAKE_PROFIT_PCT", 12.0) / 100
|
||||||
|
rsi_overbought = params.get("RSI_OVERBOUGHT_LEVEL", 70.0)
|
||||||
|
|
||||||
current_price = df.loc[current_idx, "close"]
|
current_price = df.loc[current_idx, "close"]
|
||||||
|
|
||||||
# 매수 신호 (과매도 + BB 하단)
|
# 매수 신호 (과매도 + BB 하단)
|
||||||
if rsi < rsi_threshold and bb_pos < 0.2:
|
if rsi < rsi_threshold and bb_pos < 0.2:
|
||||||
if stock_code not in self.positions:
|
if stock_code not in self.positions:
|
||||||
@@ -279,12 +280,12 @@ class BacktestEngine:
|
|||||||
"price": current_price,
|
"price": current_price,
|
||||||
"reason": f"RSI={rsi:.1f} BB하단"
|
"reason": f"RSI={rsi:.1f} BB하단"
|
||||||
}
|
}
|
||||||
|
|
||||||
# 매도 신호 (손절/익절)
|
# 매도 신호 (손절/익절)
|
||||||
if stock_code in self.positions:
|
if stock_code in self.positions:
|
||||||
pos = self.positions[stock_code]
|
pos = self.positions[stock_code]
|
||||||
pnl_pct = (current_price - pos.avg_price) / pos.avg_price
|
pnl_pct = (current_price - pos.avg_price) / pos.avg_price
|
||||||
|
|
||||||
if pnl_pct <= stop_loss:
|
if pnl_pct <= stop_loss:
|
||||||
return {
|
return {
|
||||||
"action": "SELL",
|
"action": "SELL",
|
||||||
@@ -297,7 +298,7 @@ class BacktestEngine:
|
|||||||
"price": current_price,
|
"price": current_price,
|
||||||
"reason": f"익절 {pnl_pct*100:.1f}%"
|
"reason": f"익절 {pnl_pct*100:.1f}%"
|
||||||
}
|
}
|
||||||
elif rsi > 70 and bb_pos > 0.8:
|
elif rsi > rsi_overbought and bb_pos > 0.8:
|
||||||
return {
|
return {
|
||||||
"action": "SELL",
|
"action": "SELL",
|
||||||
"price": current_price,
|
"price": current_price,
|
||||||
|
|||||||
@@ -534,31 +534,15 @@ async def analyze_stock_with_indicators(stock_code: str, stock_name: str) -> Opt
|
|||||||
return await analyze_stock(stock_code, stock_name, criteria)
|
return await analyze_stock(stock_code, stock_name, criteria)
|
||||||
|
|
||||||
async def get_all_tradable_stocks() -> List[tuple]:
|
async def get_all_tradable_stocks() -> List[tuple]:
|
||||||
SKIP_KEYWORDS = [
|
"""네이버 파이낸스에서 KOSPI+KOSDAQ 시가총액 순위 종목 조회.
|
||||||
"ETF", "레버리지", "인버스", "KODEX", "TIGER", "ARIRANG",
|
|
||||||
"KBSTAR", "HANARO", "SOL", "ACE", "KOSEF", "KINDEX", "RISE",
|
KIS volume-rank API 대신 네이버 파이낸스 크롤링을 사용한다.
|
||||||
"선물", "스팩", "리츠", "SPAC",
|
실서버 API 키 불필요.
|
||||||
]
|
"""
|
||||||
try:
|
try:
|
||||||
_list_api = _real_market_api or korea_investment_api
|
from app.services.naver_finance import get_naver_stock_list
|
||||||
await asyncio.sleep(3.0)
|
stocks = await get_naver_stock_list(max_pages_per_market=10)
|
||||||
kospi_result = await _list_api.get_stock_list("J")
|
|
||||||
await asyncio.sleep(3.0)
|
|
||||||
kosdaq_result = await _list_api.get_stock_list("Q")
|
|
||||||
|
|
||||||
stocks = []
|
|
||||||
for result in (kospi_result, kosdaq_result):
|
|
||||||
if "output" in result:
|
|
||||||
for item in result["output"]:
|
|
||||||
stock_code = item.get("mksc_shrn_iscd", "")
|
|
||||||
stock_name = item.get("hts_kor_isnm", "")
|
|
||||||
if not stock_code or not stock_name:
|
|
||||||
continue
|
|
||||||
if any(kw in stock_name for kw in SKIP_KEYWORDS):
|
|
||||||
continue
|
|
||||||
stocks.append((stock_code, stock_name))
|
|
||||||
|
|
||||||
return stocks
|
return stocks
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error fetching stock list from KIS API: {e}", exc_info=True)
|
logger.error(f"네이버 파이낸스 종목 조회 실패: {e}", exc_info=True)
|
||||||
return []
|
return []
|
||||||
@@ -25,7 +25,7 @@ from typing import Optional, Dict, List, Tuple
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
from app.core.config import settings
|
from app.services.strategy_config_service import strategy_cfg
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -225,7 +225,7 @@ def check_buy_signal(
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
if regime == MarketRegime.BULL:
|
if regime == MarketRegime.BULL:
|
||||||
rsi_ok = rsi <= settings.MEAN_REVERSION_RSI_THRESHOLD # ← 기본값 30
|
rsi_ok = rsi <= strategy_cfg.get("MEAN_REVERSION_RSI_THRESHOLD", 30.0)
|
||||||
|
|
||||||
if rsi_ok:
|
if rsi_ok:
|
||||||
# [2026-03] RSI 반등 확인: rsi_prev 제공 시 RSI 하락 중이면 진입 금지
|
# [2026-03] RSI 반등 확인: rsi_prev 제공 시 RSI 하락 중이면 진입 금지
|
||||||
@@ -303,8 +303,10 @@ def check_exit_signal(
|
|||||||
# ── 1. 손절 (최우선) ──────────────────────────────────────────
|
# ── 1. 손절 (최우선) ──────────────────────────────────────────
|
||||||
# [2026-03] ATR 손절 기준: lowest_since_entry → entry_price
|
# [2026-03] ATR 손절 기준: lowest_since_entry → entry_price
|
||||||
# 이유: 매수 직후 저점이 낮으면 ATR 손절가가 현재가보다 훨씬 아래 → 손절 사실상 무력화
|
# 이유: 매수 직후 저점이 낮으면 ATR 손절가가 현재가보다 훨씬 아래 → 손절 사실상 무력화
|
||||||
atr_stop = entry_price - 2.5 * atr
|
atr_stop_mult = strategy_cfg.get("ATR_STOP_MULTIPLIER", 2.5)
|
||||||
pct_stop = entry_price * 0.94 # -6%
|
stop_loss_pct = strategy_cfg.get("STOP_LOSS_PERCENT", -6.0) # e.g. -6.0
|
||||||
|
atr_stop = entry_price - atr_stop_mult * atr
|
||||||
|
pct_stop = entry_price * (1 + stop_loss_pct / 100)
|
||||||
stop_price = max(atr_stop, pct_stop) # 높은 값 = 타이트한 손절
|
stop_price = max(atr_stop, pct_stop) # 높은 값 = 타이트한 손절
|
||||||
|
|
||||||
if close <= stop_price:
|
if close <= stop_price:
|
||||||
@@ -392,13 +394,13 @@ def calculate_position_size(
|
|||||||
|
|
||||||
if rsi is not None:
|
if rsi is not None:
|
||||||
if rsi <= 20:
|
if rsi <= 20:
|
||||||
multiplier = settings.MEAN_REVERSION_MULTIPLIER_STRONG
|
multiplier = strategy_cfg.get("MEAN_REVERSION_MULTIPLIER_STRONG", 1.50)
|
||||||
elif rsi <= 25:
|
elif rsi <= 25:
|
||||||
multiplier = settings.MEAN_REVERSION_MULTIPLIER_NORMAL
|
multiplier = strategy_cfg.get("MEAN_REVERSION_MULTIPLIER_NORMAL", 1.25)
|
||||||
else:
|
else:
|
||||||
multiplier = settings.MEAN_REVERSION_MULTIPLIER_WEAK
|
multiplier = strategy_cfg.get("MEAN_REVERSION_MULTIPLIER_WEAK", 1.00)
|
||||||
else:
|
else:
|
||||||
multiplier = settings.MEAN_REVERSION_MULTIPLIER_WEAK
|
multiplier = strategy_cfg.get("MEAN_REVERSION_MULTIPLIER_WEAK", 1.00)
|
||||||
|
|
||||||
if adx is not None and adx < 15:
|
if adx is not None and adx < 15:
|
||||||
multiplier = min(multiplier + 0.25, 2.0)
|
multiplier = min(multiplier + 0.25, 2.0)
|
||||||
|
|||||||
121
backend/app/services/naver_finance.py
Normal file
121
backend/app/services/naver_finance.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
"""네이버 파이낸스 종목 목록 크롤러
|
||||||
|
|
||||||
|
KIS volume-rank API 대신 네이버 시가총액 순위 페이지에서
|
||||||
|
KOSPI/KOSDAQ 종목 목록을 가져온다.
|
||||||
|
|
||||||
|
사용처:
|
||||||
|
- batch_daily_analysis.get_all_tradable_stocks() DB 폴백 이전 1차 시도
|
||||||
|
- market_analyzer.get_all_tradable_stocks() 대체
|
||||||
|
|
||||||
|
URL: https://finance.naver.com/sise/sise_market_sum.naver?sosok=0&page=N
|
||||||
|
sosok=0 : KOSPI
|
||||||
|
sosok=1 : KOSDAQ
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from typing import List, Tuple
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
_HEADERS = {"User-Agent": "Mozilla/5.0 (compatible; autostock-batch/1.0)"}
|
||||||
|
_BASE_URL = "https://finance.naver.com/sise/sise_market_sum.naver"
|
||||||
|
|
||||||
|
SKIP_KEYWORDS = [
|
||||||
|
"ETF", "레버리지", "인버스", "KODEX", "TIGER", "ARIRANG",
|
||||||
|
"KBSTAR", "HANARO", "SOL", "ACE", "KOSEF", "KINDEX", "RISE",
|
||||||
|
"선물", "스팩", "리츠", "SPAC",
|
||||||
|
]
|
||||||
|
|
||||||
|
_MIN_PRICE = 5_000 # 5,000원 미만 제외
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_page(html: bytes) -> List[Tuple[str, str, int]]:
|
||||||
|
"""HTML 파싱 → [(종목코드, 종목명, 현재가), ...]"""
|
||||||
|
soup = BeautifulSoup(html, "html.parser", from_encoding="euc-kr")
|
||||||
|
table = soup.find("table", {"class": "type_2"})
|
||||||
|
if not table:
|
||||||
|
return []
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for row in table.find_all("tr"):
|
||||||
|
a = row.find("a", class_="tltle")
|
||||||
|
if not a:
|
||||||
|
continue
|
||||||
|
href = a.get("href", "")
|
||||||
|
if "code=" not in href:
|
||||||
|
continue
|
||||||
|
code = href.split("code=")[1]
|
||||||
|
name = a.text.strip()
|
||||||
|
|
||||||
|
# 현재가
|
||||||
|
tds = row.find_all("td", class_="number")
|
||||||
|
try:
|
||||||
|
price = int(tds[0].text.strip().replace(",", "")) if tds else 0
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
price = 0
|
||||||
|
|
||||||
|
result.append((code, name, price))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
async def _fetch_page(client: httpx.AsyncClient, sosok: int, page: int) -> List[Tuple[str, str, int]]:
|
||||||
|
try:
|
||||||
|
resp = await client.get(
|
||||||
|
_BASE_URL,
|
||||||
|
params={"sosok": sosok, "page": page},
|
||||||
|
headers=_HEADERS,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
return _parse_page(resp.content)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning("네이버 파이낸스 조회 실패 sosok=%d page=%d: %s", sosok, page, exc)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
async def get_naver_stock_list(
|
||||||
|
max_pages_per_market: int = 10,
|
||||||
|
min_price: int = _MIN_PRICE,
|
||||||
|
) -> List[Tuple[str, str]]:
|
||||||
|
"""KOSPI + KOSDAQ 시가총액 순위 종목 조회.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_pages_per_market: 시장별 최대 페이지 수 (페이지당 ~50종목)
|
||||||
|
min_price: 최소 현재가 필터 (기본 5,000원)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
[(종목코드, 종목명), ...] — ETF/레버리지/스팩 제외, 가격 필터 적용
|
||||||
|
"""
|
||||||
|
stocks: List[Tuple[str, str]] = []
|
||||||
|
seen_codes: set = set()
|
||||||
|
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
for sosok, market_name in ((0, "KOSPI"), (1, "KOSDAQ")):
|
||||||
|
market_stocks = []
|
||||||
|
for page in range(1, max_pages_per_market + 1):
|
||||||
|
rows = await _fetch_page(client, sosok, page)
|
||||||
|
if not rows:
|
||||||
|
break # 마지막 페이지
|
||||||
|
|
||||||
|
for code, name, price in rows:
|
||||||
|
if code in seen_codes:
|
||||||
|
continue
|
||||||
|
if price < min_price:
|
||||||
|
continue
|
||||||
|
name_upper = name.upper()
|
||||||
|
if any(kw.upper() in name_upper for kw in SKIP_KEYWORDS):
|
||||||
|
continue
|
||||||
|
seen_codes.add(code)
|
||||||
|
market_stocks.append((code, name))
|
||||||
|
|
||||||
|
await asyncio.sleep(0.3) # 서버 부하 방지
|
||||||
|
|
||||||
|
logger.info("네이버 %s: %d개 종목 수집", market_name, len(market_stocks))
|
||||||
|
stocks.extend(market_stocks)
|
||||||
|
|
||||||
|
logger.info("네이버 파이낸스 총 %d개 종목 수집", len(stocks))
|
||||||
|
return stocks
|
||||||
@@ -31,7 +31,7 @@ sys.path.insert(0, str(Path(__file__).parent))
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.database import AsyncSessionLocal # 🔧 공통 DB 모듈 사용
|
from app.core.database import AsyncSessionLocal # 🔧 공통 DB 모듈 사용
|
||||||
from sqlalchemy import select as sa_select
|
from sqlalchemy import select as sa_select, func as sa_func
|
||||||
from app.models.models import StockDailyPrice
|
from app.models.models import StockDailyPrice
|
||||||
from app.services.analysis_db_service import bulk_upsert_analysis, passes_price_filter, run_db_scanner # 🔧 단일화
|
from app.services.analysis_db_service import bulk_upsert_analysis, passes_price_filter, run_db_scanner # 🔧 단일화
|
||||||
from app.services.market_analyzer import analyze_stock_for_batch_db, StockRecommendation
|
from app.services.market_analyzer import analyze_stock_for_batch_db, StockRecommendation
|
||||||
@@ -118,14 +118,64 @@ async def detect_and_log_market_regime() -> str:
|
|||||||
# 2. 종목 목록 조회
|
# 2. 종목 목록 조회
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
|
|
||||||
|
async def _get_stocks_from_db(limit: int = 500) -> list[tuple[str, str]]:
|
||||||
|
"""stock_daily_prices DB에서 종목 목록 조회 (KIS API 실패 시 폴백).
|
||||||
|
|
||||||
|
최근 거래일 기준으로 데이터가 있는 종목을 조회한다.
|
||||||
|
ETF / 레버리지 / 인버스 / 스팩 등 특수종목은 이름 기반으로 제외.
|
||||||
|
"""
|
||||||
|
SKIP_KEYWORDS = [
|
||||||
|
"ETF", "레버리지", "인버스", "KODEX", "TIGER", "ARIRANG",
|
||||||
|
"KBSTAR", "HANARO", "SOL", "ACE", "KOSEF", "KINDEX", "RISE",
|
||||||
|
"선물", "스팩", "리츠", "SPAC",
|
||||||
|
]
|
||||||
|
|
||||||
|
async with AsyncSessionLocal() as db:
|
||||||
|
# 최근 거래일 조회
|
||||||
|
latest_date_result = await db.execute(
|
||||||
|
sa_select(sa_func.max(StockDailyPrice.trade_date))
|
||||||
|
)
|
||||||
|
latest_date = latest_date_result.scalar()
|
||||||
|
|
||||||
|
if not latest_date:
|
||||||
|
logger.warning("⚠️ stock_daily_prices 테이블이 비어 있음")
|
||||||
|
return []
|
||||||
|
|
||||||
|
logger.info("📂 DB 폴백: %s 기준 종목 조회", latest_date)
|
||||||
|
|
||||||
|
# 최근 거래일 종목 전체 조회 (종목코드, 종목명)
|
||||||
|
result = await db.execute(
|
||||||
|
sa_select(StockDailyPrice.stock_code, StockDailyPrice.stock_name)
|
||||||
|
.where(StockDailyPrice.trade_date == latest_date)
|
||||||
|
.where(StockDailyPrice.close_price > 5000)
|
||||||
|
.order_by(StockDailyPrice.volume.desc())
|
||||||
|
.limit(limit * 3) # 필터 후 충분한 수가 남도록 여유분 조회
|
||||||
|
)
|
||||||
|
rows = result.all()
|
||||||
|
|
||||||
|
stocks = []
|
||||||
|
for code, name in rows:
|
||||||
|
if not name:
|
||||||
|
continue
|
||||||
|
name_upper = name.upper()
|
||||||
|
if any(kw.upper() in name_upper for kw in SKIP_KEYWORDS):
|
||||||
|
continue
|
||||||
|
stocks.append((code, name))
|
||||||
|
if len(stocks) >= limit:
|
||||||
|
break
|
||||||
|
|
||||||
|
logger.info("📂 DB 폴백 결과: %d개 종목 (%s 기준)", len(stocks), latest_date)
|
||||||
|
return stocks
|
||||||
|
|
||||||
|
|
||||||
async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
|
async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
|
||||||
"""KIS 거래량 순위 API로 거래 가능 종목 조회 (KOSPI+KOSDAQ 각 top 40).
|
"""KIS 거래량 순위 API로 거래 가능 종목 조회 (KOSPI+KOSDAQ 각 top 40).
|
||||||
|
|
||||||
KIS FHPST01710000: 거래량 순위 — 5,000원 이상 활성 종목.
|
KIS FHPST01710000: 거래량 순위 — 5,000원 이상 활성 종목.
|
||||||
ETF / 레버리지 / 스팩 등 특수종목 자동 제외.
|
ETF / 레버리지 / 스팩 등 특수종목 자동 제외.
|
||||||
|
|
||||||
Raises:
|
KIS API 결과가 MIN_STOCK_COUNT(50) 미만이면 stock_daily_prices DB 폴백을 사용한다.
|
||||||
RuntimeError: 조회 결과가 MIN_STOCK_COUNT(50) 미만인 경우 배치 중단
|
DB 폴백도 실패하면 RuntimeError를 발생시켜 배치를 중단한다.
|
||||||
"""
|
"""
|
||||||
from app.services.market_analyzer import get_all_tradable_stocks as _kis_get_stocks
|
from app.services.market_analyzer import get_all_tradable_stocks as _kis_get_stocks
|
||||||
|
|
||||||
@@ -133,10 +183,21 @@ async def get_all_tradable_stocks(limit: int = 500) -> list[tuple[str, str]]:
|
|||||||
stocks = await _kis_get_stocks()
|
stocks = await _kis_get_stocks()
|
||||||
|
|
||||||
if len(stocks) < MIN_STOCK_COUNT:
|
if len(stocks) < MIN_STOCK_COUNT:
|
||||||
raise RuntimeError(
|
logger.warning(
|
||||||
f"종목 조회 실패: KIS API 결과 부족 ({len(stocks)}개 < {MIN_STOCK_COUNT}개). "
|
"⚠️ KIS API 결과 부족 (%d개 < %d개) — DB 폴백 사용",
|
||||||
f"KIS API 설정을 확인하세요."
|
len(stocks), MIN_STOCK_COUNT,
|
||||||
)
|
)
|
||||||
|
db_stocks = await _get_stocks_from_db(limit)
|
||||||
|
|
||||||
|
if len(db_stocks) < MIN_STOCK_COUNT:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"종목 조회 실패: KIS API({len(stocks)}개) 및 DB 폴백({len(db_stocks)}개) 모두 "
|
||||||
|
f"최소 기준({MIN_STOCK_COUNT}개) 미달. KIS API 설정 및 DB 데이터를 확인하세요."
|
||||||
|
)
|
||||||
|
|
||||||
|
result = db_stocks[:limit]
|
||||||
|
logger.info("✅ DB 폴백 선정 %d개", len(result))
|
||||||
|
return result
|
||||||
|
|
||||||
result = stocks[:limit]
|
result = stocks[:limit]
|
||||||
logger.info("✅ 선정 %d개", len(result))
|
logger.info("✅ 선정 %d개", len(result))
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user