백테스트 수정중
This commit is contained in:
@@ -27,10 +27,57 @@ from app.services.backtest_engine import (
|
||||
from app.services.stock_universe import StockUniverse, StockFilter
|
||||
from app.services.ml_ensemble import get_ensemble
|
||||
from app.services.feature_engineering import FEATURE_COLUMNS
|
||||
from app.services.strategy_config_service import strategy_cfg
|
||||
|
||||
router = APIRouter()
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# ── 전략 프리셋 정의 (모듈 레벨) ───────────────────────────────────────────────
|
||||
|
||||
_STRATEGY_PRESETS: Dict[str, Dict[str, Any]] = {
|
||||
"conservative": {
|
||||
"name": "보수적",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -5.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 8.0,
|
||||
"MAX_TOTAL_POSITIONS": 3,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 25,
|
||||
},
|
||||
},
|
||||
"balanced": {
|
||||
"name": "균형형",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -6.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 12.0,
|
||||
"MAX_TOTAL_POSITIONS": 5,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 30,
|
||||
},
|
||||
},
|
||||
"aggressive": {
|
||||
"name": "공격적",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -8.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 20.0,
|
||||
"MAX_TOTAL_POSITIONS": 8,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 35,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
async def _get_preset(preset_key: str) -> Optional[Dict[str, Any]]:
|
||||
"""프리셋 이름으로 파라미터 반환. 'current'이면 DB 현재 설정 로드."""
|
||||
if preset_key == "current":
|
||||
await strategy_cfg._refresh()
|
||||
params = {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": strategy_cfg.get("STOP_LOSS_PERCENT", -6.0),
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": strategy_cfg.get("TAKE_PROFIT_PERCENT", 12.0),
|
||||
"MAX_TOTAL_POSITIONS": strategy_cfg.get("MAX_HOLDINGS_COUNT", 5),
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": strategy_cfg.get("MEAN_REVERSION_RSI_THRESHOLD", 30.0),
|
||||
}
|
||||
return {"name": "현재 설정", "params": params}
|
||||
return _STRATEGY_PRESETS.get(preset_key)
|
||||
|
||||
# 전역 변수: 생성된 학습 데이터 임시 저장
|
||||
_cached_training_data: Optional[Dict[str, Any]] = None
|
||||
|
||||
@@ -65,6 +112,14 @@ class ComparePresetsRequest(BaseModel):
|
||||
presets: List[str] = Field(["conservative", "balanced", "aggressive"], description="비교할 프리셋 목록")
|
||||
|
||||
|
||||
class ComparePresetsUniverseRequest(BaseModel):
|
||||
universe_preset: str = Field("large_cap", description="유니버스 프리셋")
|
||||
start_date: str
|
||||
end_date: str
|
||||
initial_capital: float = 10_000_000
|
||||
presets: List[str] = Field(["conservative", "balanced", "aggressive"], description="비교할 프리셋 목록")
|
||||
|
||||
|
||||
class RunUniverseBacktestRequest(BaseModel):
|
||||
"""전체 종목 백테스트 요청"""
|
||||
universe_preset: str = Field("large_cap", description="유니버스 프리셋 (large_cap, kospi_all, ...)")
|
||||
@@ -76,6 +131,17 @@ class RunUniverseBacktestRequest(BaseModel):
|
||||
top_n_results: int = Field(20, description="성과 상위 N개만 반환")
|
||||
|
||||
|
||||
class GenerateTrainingDataUniverseRequest(BaseModel):
|
||||
"""전체 종목 ML 학습 데이터 생성 요청"""
|
||||
universe_preset: str = Field("large_cap", description="유니버스 프리셋")
|
||||
start_date: str = Field(..., description="시작일 (YYYY-MM-DD)")
|
||||
end_date: str = Field(..., description="종료일 (YYYY-MM-DD)")
|
||||
forward_days: int = Field(5, description="N일 후 수익률로 라벨링")
|
||||
up_threshold: float = Field(0.03, description="UP 레이블 임계값")
|
||||
down_threshold: float = Field(-0.03, description="DOWN 레이블 임계값")
|
||||
min_samples_per_stock: int = Field(50, description="종목당 최소 샘플 수")
|
||||
|
||||
|
||||
# ── API 엔드포인트 ─────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/universe-presets")
|
||||
@@ -129,31 +195,13 @@ async def api_run_universe_backtest(req: RunUniverseBacktestRequest) -> Dict[str
|
||||
|
||||
logger.info(f"전체 백테스트 시작: {len(stock_codes)}종목")
|
||||
|
||||
# 2. 전략 파라미터
|
||||
PRESETS = {
|
||||
"conservative": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -5.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 8.0,
|
||||
"MAX_TOTAL_POSITIONS": 3,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 25,
|
||||
},
|
||||
"balanced": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -6.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 12.0,
|
||||
"MAX_TOTAL_POSITIONS": 5,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 30,
|
||||
},
|
||||
"aggressive": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -8.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 20.0,
|
||||
"MAX_TOTAL_POSITIONS": 8,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 35,
|
||||
},
|
||||
}
|
||||
|
||||
strategy_params = PRESETS.get(req.strategy_preset, PRESETS["balanced"])
|
||||
# 2. 전략 파라미터 ('current'이면 DB에서 로드)
|
||||
preset = await _get_preset(req.strategy_preset) or await _get_preset("balanced")
|
||||
strategy_params = preset["params"]
|
||||
|
||||
# 3. 각 종목별로 백테스트 (병렬)
|
||||
no_data_count = 0
|
||||
|
||||
async def backtest_single(code: str) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
config = BacktestConfig(
|
||||
@@ -167,7 +215,11 @@ async def api_run_universe_backtest(req: RunUniverseBacktestRequest) -> Dict[str
|
||||
)
|
||||
engine = BacktestEngine()
|
||||
result = await engine.run_backtest(config)
|
||||
|
||||
|
||||
# DB에 해당 종목/기간 데이터 없으면 제외
|
||||
if not result.equity_curve:
|
||||
return None
|
||||
|
||||
return {
|
||||
"stock_code": code,
|
||||
"total_return_pct": result.total_return_pct,
|
||||
@@ -179,15 +231,17 @@ async def api_run_universe_backtest(req: RunUniverseBacktestRequest) -> Dict[str
|
||||
except Exception as e:
|
||||
logger.error(f"{code} 백테스트 실패: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# 병렬 실행 (10개씩 배치)
|
||||
results = []
|
||||
batch_size = 10
|
||||
for i in range(0, len(stock_codes), batch_size):
|
||||
batch = stock_codes[i:i + batch_size]
|
||||
batch_results = await asyncio.gather(*[backtest_single(code) for code in batch])
|
||||
results.extend([r for r in batch_results if r is not None])
|
||||
logger.info(f"진행률: {len(results)}/{len(stock_codes)}")
|
||||
valid = [r for r in batch_results if r is not None]
|
||||
no_data_count += len(batch) - len(valid)
|
||||
results.extend(valid)
|
||||
logger.info(f"진행률: {len(results)}/{len(stock_codes)} (데이터없음: {no_data_count})")
|
||||
|
||||
# 4. 수익률 상위 N개 정렬
|
||||
results_sorted = sorted(results, key=lambda x: x["total_return_pct"], reverse=True)
|
||||
@@ -197,6 +251,7 @@ async def api_run_universe_backtest(req: RunUniverseBacktestRequest) -> Dict[str
|
||||
|
||||
return {
|
||||
"total_tested": len(results),
|
||||
"no_data_count": no_data_count,
|
||||
"top_performers": top_results,
|
||||
"universe_preset": req.universe_preset,
|
||||
"strategy_preset": req.strategy_preset,
|
||||
@@ -205,6 +260,61 @@ async def api_run_universe_backtest(req: RunUniverseBacktestRequest) -> Dict[str
|
||||
}
|
||||
|
||||
|
||||
@router.post("/generate-training-data-universe")
|
||||
async def api_generate_training_data_universe(req: GenerateTrainingDataUniverseRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
전체 종목 ML 학습 데이터 생성 (유니버스 프리셋 기반)
|
||||
|
||||
유니버스에 속한 모든 종목의 과거 데이터를 병렬로 수집하여 학습 데이터를 생성합니다.
|
||||
"""
|
||||
global _cached_training_data
|
||||
|
||||
try:
|
||||
start_date = datetime.strptime(req.start_date, "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(req.end_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="날짜 형식 오류 (YYYY-MM-DD)")
|
||||
|
||||
# 유니버스 프리셋에서 종목 리스트
|
||||
presets = StockUniverse.get_preset_universes()
|
||||
if req.universe_preset not in presets:
|
||||
raise HTTPException(status_code=400, detail=f"잘못된 유니버스 프리셋: {req.universe_preset}")
|
||||
|
||||
stock_filter = presets[req.universe_preset]["filter"]
|
||||
stock_codes = await StockUniverse.get_filtered_stocks(stock_filter)
|
||||
|
||||
if not stock_codes:
|
||||
raise HTTPException(status_code=404, detail="해당 유니버스에 종목이 없습니다")
|
||||
|
||||
logger.info(f"전체 종목 학습 데이터 생성 시작: {len(stock_codes)}종목")
|
||||
|
||||
config = TrainingDataConfig(
|
||||
stock_codes=stock_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
forward_days=req.forward_days,
|
||||
up_threshold=req.up_threshold,
|
||||
down_threshold=req.down_threshold,
|
||||
min_samples_per_stock=req.min_samples_per_stock,
|
||||
)
|
||||
|
||||
result = await generate_training_data(config)
|
||||
|
||||
if "error" in result:
|
||||
raise HTTPException(status_code=500, detail=result["error"])
|
||||
|
||||
_cached_training_data = result
|
||||
|
||||
return {
|
||||
"total_samples": result["total_samples"],
|
||||
"label_distribution": result["label_distribution"],
|
||||
"config": result["config"],
|
||||
"universe_preset": req.universe_preset,
|
||||
"total_stocks_requested": len(stock_codes),
|
||||
"message": f"{len(stock_codes)}종목에서 {result['total_samples']}개 샘플 생성 완료. /train-model로 학습하거나 /download-training-data로 다운로드하세요.",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/generate-training-data")
|
||||
async def api_generate_training_data(req: GenerateTrainingDataRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
@@ -308,28 +418,35 @@ async def api_train_model(background_tasks: BackgroundTasks) -> Dict[str, Any]:
|
||||
async def api_run_backtest(req: RunBacktestRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
전략 성과 백테스트 실행
|
||||
|
||||
과거 데이터로 전략을 실행하여 수익률, MDD, 승률 등을 계산합니다.
|
||||
|
||||
strategy_params가 없으면 전략 설정 페이지의 현재 DB 값을 자동 사용합니다.
|
||||
"""
|
||||
try:
|
||||
start_date = datetime.strptime(req.start_date, "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(req.end_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="날짜 형식 오류")
|
||||
|
||||
|
||||
# strategy_params 없으면 DB 현재 설정 자동 로드
|
||||
strategy_params = req.strategy_params
|
||||
if strategy_params is None:
|
||||
preset = await _get_preset("current")
|
||||
strategy_params = preset["params"]
|
||||
logger.info(f"strategy_params 미지정 → DB 현재 설정 사용: {strategy_params}")
|
||||
|
||||
config = BacktestConfig(
|
||||
stock_codes=req.stock_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
initial_capital=req.initial_capital,
|
||||
strategy_params=req.strategy_params,
|
||||
strategy_params=strategy_params,
|
||||
max_positions=req.max_positions,
|
||||
position_size=req.position_size,
|
||||
)
|
||||
|
||||
|
||||
engine = BacktestEngine()
|
||||
result = await engine.run_backtest(config)
|
||||
|
||||
|
||||
# BacktestResult를 dict로 변환
|
||||
from dataclasses import asdict
|
||||
return asdict(result)
|
||||
@@ -348,44 +465,13 @@ async def api_compare_presets(req: ComparePresetsRequest) -> Dict[str, Any]:
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="날짜 형식 오류")
|
||||
|
||||
# 프리셋 정의 (frontend/src/utils/strategyPresets.ts와 동기화)
|
||||
PRESETS = {
|
||||
"conservative": {
|
||||
"name": "보수적",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -5.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 8.0,
|
||||
"MAX_TOTAL_POSITIONS": 3,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 25,
|
||||
}
|
||||
},
|
||||
"balanced": {
|
||||
"name": "균형형",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -6.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 12.0,
|
||||
"MAX_TOTAL_POSITIONS": 5,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 30,
|
||||
}
|
||||
},
|
||||
"aggressive": {
|
||||
"name": "공격적",
|
||||
"params": {
|
||||
"MEAN_REVERSION_STOP_LOSS_PCT": -8.0,
|
||||
"MEAN_REVERSION_TAKE_PROFIT_PCT": 20.0,
|
||||
"MAX_TOTAL_POSITIONS": 8,
|
||||
"MEAN_REVERSION_RSI_THRESHOLD": 35,
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
results = {}
|
||||
|
||||
|
||||
for preset_key in req.presets:
|
||||
if preset_key not in PRESETS:
|
||||
preset = await _get_preset(preset_key)
|
||||
if preset is None:
|
||||
continue
|
||||
|
||||
preset = PRESETS[preset_key]
|
||||
|
||||
config = BacktestConfig(
|
||||
stock_codes=req.stock_codes,
|
||||
start_date=start_date,
|
||||
@@ -394,10 +480,10 @@ async def api_compare_presets(req: ComparePresetsRequest) -> Dict[str, Any]:
|
||||
strategy_params=preset["params"],
|
||||
max_positions=preset["params"]["MAX_TOTAL_POSITIONS"],
|
||||
)
|
||||
|
||||
|
||||
engine = BacktestEngine()
|
||||
result = await engine.run_backtest(config)
|
||||
|
||||
|
||||
results[preset_key] = {
|
||||
"name": preset["name"],
|
||||
"total_return_pct": result.total_return_pct,
|
||||
@@ -407,9 +493,71 @@ async def api_compare_presets(req: ComparePresetsRequest) -> Dict[str, Any]:
|
||||
"total_trades": result.total_trades,
|
||||
"equity_curve": result.equity_curve,
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
"presets": results,
|
||||
"period": f"{req.start_date} ~ {req.end_date}",
|
||||
"stock_count": len(req.stock_codes),
|
||||
}
|
||||
|
||||
|
||||
@router.post("/compare-presets-universe")
|
||||
async def api_compare_presets_universe(req: ComparePresetsUniverseRequest) -> Dict[str, Any]:
|
||||
"""
|
||||
프리셋 비교 백테스트 (유니버스 프리셋 기반)
|
||||
|
||||
선택한 유니버스의 모든 종목을 대상으로 보수적/균형형/공격적 프리셋을 비교합니다.
|
||||
"""
|
||||
try:
|
||||
start_date = datetime.strptime(req.start_date, "%Y-%m-%d").date()
|
||||
end_date = datetime.strptime(req.end_date, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail="날짜 형식 오류")
|
||||
|
||||
presets_map = StockUniverse.get_preset_universes()
|
||||
if req.universe_preset not in presets_map:
|
||||
raise HTTPException(status_code=400, detail=f"잘못된 유니버스 프리셋: {req.universe_preset}")
|
||||
|
||||
stock_filter = presets_map[req.universe_preset]["filter"]
|
||||
stock_codes = await StockUniverse.get_filtered_stocks(stock_filter)
|
||||
|
||||
if not stock_codes:
|
||||
raise HTTPException(status_code=404, detail="해당 유니버스에 종목이 없습니다")
|
||||
|
||||
logger.info(f"유니버스 프리셋 비교 시작: {req.universe_preset} ({len(stock_codes)}종목)")
|
||||
|
||||
results = {}
|
||||
|
||||
for preset_key in req.presets:
|
||||
preset = await _get_preset(preset_key)
|
||||
if preset is None:
|
||||
continue
|
||||
|
||||
config = BacktestConfig(
|
||||
stock_codes=stock_codes,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
initial_capital=req.initial_capital,
|
||||
strategy_params=preset["params"],
|
||||
max_positions=preset["params"]["MAX_TOTAL_POSITIONS"],
|
||||
)
|
||||
|
||||
engine = BacktestEngine()
|
||||
result = await engine.run_backtest(config)
|
||||
|
||||
results[preset_key] = {
|
||||
"name": preset["name"],
|
||||
"total_return_pct": result.total_return_pct,
|
||||
"mdd_pct": result.mdd_pct,
|
||||
"win_rate": result.win_rate,
|
||||
"sharpe_ratio": result.sharpe_ratio,
|
||||
"total_trades": result.total_trades,
|
||||
"equity_curve": result.equity_curve,
|
||||
}
|
||||
|
||||
return {
|
||||
"presets": results,
|
||||
"period": f"{req.start_date} ~ {req.end_date}",
|
||||
"stock_count": len(stock_codes),
|
||||
"universe_preset": req.universe_preset,
|
||||
}
|
||||
|
||||
@@ -334,7 +334,7 @@ class BacktestEngine:
|
||||
avg_lose=0.0,
|
||||
max_win=0.0,
|
||||
max_lose=0.0,
|
||||
equity_curve=[],
|
||||
equity_curve=self.equity_curve,
|
||||
trades=[],
|
||||
final_positions=[]
|
||||
)
|
||||
@@ -445,7 +445,8 @@ class BacktestEngine:
|
||||
prev_portfolio_value = config.initial_capital
|
||||
|
||||
for current_date in all_dates:
|
||||
current_date_obj = datetime.strptime(current_date, "%Y%m%d").date()
|
||||
fmt = "%Y%m%d" if len(current_date) == 8 else "%Y-%m-%d"
|
||||
current_date_obj = datetime.strptime(current_date, fmt).date()
|
||||
|
||||
# 각 종목별 신호 생성 및 주문 실행
|
||||
current_prices = {}
|
||||
|
||||
@@ -111,8 +111,8 @@ class StockLSTM:
|
||||
self.seq_std = X_seq.std(axis=(0, 1)) + 1e-8
|
||||
X_norm = (X_seq - self.seq_mean) / self.seq_std
|
||||
|
||||
X_tensor = torch.FloatTensor(X_norm).to(self.device)
|
||||
y_tensor = torch.LongTensor(y).to(self.device)
|
||||
X_tensor = torch.FloatTensor(X_norm.copy()).to(self.device)
|
||||
y_tensor = torch.LongTensor(y.copy()).to(self.device)
|
||||
|
||||
dataset = TensorDataset(X_tensor, y_tensor)
|
||||
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True)
|
||||
@@ -157,7 +157,7 @@ class StockLSTM:
|
||||
|
||||
self.model.eval()
|
||||
X_norm = (X_seq - self.seq_mean) / self.seq_std
|
||||
X_tensor = torch.FloatTensor(X_norm).to(self.device)
|
||||
X_tensor = torch.FloatTensor(X_norm.copy()).to(self.device)
|
||||
|
||||
with torch.no_grad():
|
||||
outputs = self.model(X_tensor)
|
||||
|
||||
@@ -274,8 +274,24 @@ async def get_all_krx_stocks_from_fdr(
|
||||
return stocks
|
||||
|
||||
except ImportError:
|
||||
logger.error("FinanceDataReader 미설치. pip install finance-datareader")
|
||||
return []
|
||||
logger.warning("FinanceDataReader 미설치. DB 폴백 사용")
|
||||
return await _get_stocks_from_db(market)
|
||||
except Exception as e:
|
||||
logger.error(f"FDR 조회 실패: {e}")
|
||||
logger.warning(f"FDR 조회 실패: {e}. DB 폴백 사용")
|
||||
return await _get_stocks_from_db(market)
|
||||
|
||||
|
||||
async def _get_stocks_from_db(market: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""FDR 실패 시 DB의 StockDailyPrice에서 고유 종목 코드 조회"""
|
||||
try:
|
||||
from sqlalchemy import distinct
|
||||
async with AsyncSessionLocal() as session:
|
||||
stmt = select(distinct(StockDailyPrice.stock_code))
|
||||
result = await session.execute(stmt)
|
||||
codes = [row[0] for row in result.all()]
|
||||
|
||||
logger.info(f"DB 폴백: {len(codes)}개 종목 조회 (market 필터 미적용)")
|
||||
return [{'code': c, 'name': c, 'market': market or 'KOSPI'} for c in codes]
|
||||
except Exception as e:
|
||||
logger.error(f"DB 폴백 조회 실패: {e}")
|
||||
return []
|
||||
|
||||
BIN
backend/models/ensemble/lgbm_model.pkl
Normal file
BIN
backend/models/ensemble/lgbm_model.pkl
Normal file
Binary file not shown.
BIN
backend/models/ensemble/lstm_model.pt
Normal file
BIN
backend/models/ensemble/lstm_model.pt
Normal file
Binary file not shown.
1
backend/models/ensemble/metrics.json
Normal file
1
backend/models/ensemble/metrics.json
Normal file
@@ -0,0 +1 @@
|
||||
{}
|
||||
BIN
backend/models/ensemble/rf_model.pkl
Normal file
BIN
backend/models/ensemble/rf_model.pkl
Normal file
Binary file not shown.
BIN
backend/models/ensemble/scaler.pkl
Normal file
BIN
backend/models/ensemble/scaler.pkl
Normal file
Binary file not shown.
@@ -29,6 +29,7 @@ feedparser==6.0.11
|
||||
joblib>=1.3.0
|
||||
beautifulsoup4>=4.12.0
|
||||
httpx>=0.27.1
|
||||
finance-datareader>=0.9.50
|
||||
# ML Ensemble
|
||||
torch>=2.1.0
|
||||
lightgbm>=4.1.0
|
||||
|
||||
Reference in New Issue
Block a user