1593 lines
72 KiB
Python
1593 lines
72 KiB
Python
import logging
|
|
import os
|
|
import shutil
|
|
import requests
|
|
from datetime import datetime
|
|
from typing import List
|
|
from module.plugin.database import Database
|
|
from typing import Dict
|
|
import pandas as pd
|
|
|
|
class MasterFileManager:
|
|
"""도구별 마스터파일 관리 클래스 (1:N 매핑 지원)"""
|
|
|
|
# 툴별 필요한 마스터파일 매핑 (기존 마스터파일들을 툴별로 그룹화)
|
|
TOOL_MASTER_MAPPING = {
|
|
"domestic_stock": [
|
|
"domestic_stock_master", # 코스닥
|
|
"domestic_stock_kospi_master", # 코스피
|
|
"domestic_stock_konex_master" # 코넥스
|
|
],
|
|
"overseas_stock": [
|
|
"overseas_stock_master", # 나스닥
|
|
"overseas_stock_nys_master", # 뉴욕
|
|
"overseas_stock_ams_master", # 아멕스
|
|
"overseas_stock_shs_master", # 상해
|
|
"overseas_stock_shi_master", # 상해지수
|
|
"overseas_stock_szs_master", # 심천
|
|
"overseas_stock_szi_master", # 심천지수
|
|
"overseas_stock_tse_master", # 도쿄
|
|
"overseas_stock_hks_master", # 홍콩
|
|
"overseas_stock_hnx_master", # 하노이
|
|
"overseas_stock_hsx_master" # 호치민
|
|
],
|
|
"domestic_futureoption": [
|
|
"domestic_future_master",
|
|
"domestic_index_future_master",
|
|
"domestic_cme_future_master",
|
|
"domestic_commodity_future_master",
|
|
"domestic_eurex_option_master"
|
|
],
|
|
"overseas_futureoption": [
|
|
"overseas_future_master",
|
|
"overseas_index_master"
|
|
],
|
|
"domestic_bond": [
|
|
"domestic_bond_master"
|
|
],
|
|
"etfetn": [], # ETF/ETN 마스터파일 없음
|
|
"elw": [
|
|
"elw_master"
|
|
],
|
|
"auth": [] # 인증은 마스터파일 불필요
|
|
}
|
|
|
|
# 마스터파일별 URL과 처리 함수 매핑 (한국투자증권 기준)
|
|
MASTER_FILE_PROCESS = {
|
|
# 국내주식 마스터파일 (코스닥)
|
|
"domestic_stock_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/kosdaq_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_stock",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "kosdaq"
|
|
},
|
|
|
|
# 국내주식 마스터파일 (코스피)
|
|
"domestic_stock_kospi_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/kospi_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_stock_kospi",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "kospi"
|
|
},
|
|
|
|
# 국내주식 마스터파일 (코넥스)
|
|
"domestic_stock_konex_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/konex_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_stock_konex",
|
|
"name_key": "stock_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "konex"
|
|
},
|
|
|
|
# 해외주식 마스터파일 (나스닥 중심) - 실제 URL 확인됨
|
|
"overseas_stock_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/nasmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "NAS"
|
|
},
|
|
"overseas_stock_nys_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/nysmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "NYS"
|
|
},
|
|
"overseas_stock_ams_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/amsmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "AMS"
|
|
},
|
|
"overseas_stock_shs_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/shsmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "SHS"
|
|
},
|
|
"overseas_stock_shi_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/shimst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "SHI"
|
|
},
|
|
"overseas_stock_szs_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/szsmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "SZS"
|
|
},
|
|
"overseas_stock_szi_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/szimst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "SZI"
|
|
},
|
|
"overseas_stock_tse_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/tsemst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "TSE"
|
|
},
|
|
"overseas_stock_hks_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/hksmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "HKS"
|
|
},
|
|
"overseas_stock_hnx_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/hnxmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "HNX"
|
|
},
|
|
"overseas_stock_hsx_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/hsxmst.cod.zip",
|
|
"process": "_MasterFileManager__process_overseas_stock",
|
|
"name_key": "korea_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "HSX"
|
|
},
|
|
|
|
# 해외지수 마스터파일 (실제 URL 확인됨)
|
|
"overseas_index_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/frgn_code.mst.zip",
|
|
"process": "_MasterFileManager__process_overseas_index",
|
|
"name_key": "korean_name",
|
|
"code_key": "symbol",
|
|
"ex_value": "index"
|
|
},
|
|
|
|
# 국내선물 마스터파일 (주식선물옵션)
|
|
"domestic_future_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/fo_stk_code_mts.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_future",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "future"
|
|
},
|
|
|
|
# 국내선물 마스터파일 (지수선물옵션)
|
|
"domestic_index_future_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/fo_idx_code_mts.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_index_future",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "index"
|
|
},
|
|
|
|
# 국내선물 마스터파일 (CME연계 야간선물)
|
|
"domestic_cme_future_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/fo_cme_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_cme_future",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "cme"
|
|
},
|
|
|
|
# 국내선물 마스터파일 (상품선물옵션)
|
|
"domestic_commodity_future_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/fo_com_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_commodity_future",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "commodity"
|
|
},
|
|
|
|
# 국내옵션 마스터파일 (EUREX연계 야간옵션)
|
|
"domestic_eurex_option_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/fo_eurex_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_eurex_option",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "eurex"
|
|
},
|
|
|
|
# 해외선물 마스터파일 (실제 URL 확인됨)
|
|
"overseas_future_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/ffcode.mst.zip",
|
|
"process": "_MasterFileManager__process_overseas_future",
|
|
"name_key": "korean_name",
|
|
"code_key": "stock_code",
|
|
"ex_value": "future"
|
|
},
|
|
|
|
# 국내채권 마스터파일 (실제 URL 확인됨)
|
|
"domestic_bond_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/bond_code.mst.zip",
|
|
"process": "_MasterFileManager__process_domestic_bond",
|
|
"name_key": "bond_name",
|
|
"code_key": "standard_code",
|
|
"ex_value": "bond"
|
|
},
|
|
|
|
# ELW 마스터파일 (실제 URL 확인됨)
|
|
"elw_master": {
|
|
"file": "https://new.real.download.dws.co.kr/common/master/elw_code.mst.zip",
|
|
"process": "_MasterFileManager__process_elw",
|
|
"name_key": "korean_name",
|
|
"code_key": "short_code",
|
|
"ex_value": "elw"
|
|
}
|
|
}
|
|
|
|
def __init__(self, tool_name: str):
|
|
self.tool_name = tool_name
|
|
self.master_dir = f"./configs/master/{tool_name}"
|
|
self.error_log_path = os.path.join(os.getcwd(), "configs", "master", "error.log")
|
|
|
|
# 툴별 필요한 마스터파일 목록 가져오기
|
|
self.required_masters = self.TOOL_MASTER_MAPPING.get(tool_name, [])
|
|
|
|
# 마스터 디렉토리 생성
|
|
os.makedirs(self.master_dir, exist_ok=True)
|
|
os.makedirs(os.path.dirname(self.error_log_path), exist_ok=True)
|
|
|
|
# 데이터베이스 연결 (Singleton 패턴 활용)
|
|
self.db = Database()
|
|
# 필요시에만 초기화 (이미 초기화된 경우 스킵)
|
|
if not self.db.ensure_initialized():
|
|
raise RuntimeError("데이터베이스 초기화에 실패했습니다.")
|
|
self.db_engine = self.db.get_by_name("master")
|
|
|
|
# 오류 로그 설정
|
|
self.error_logger = logging.getLogger(f"master_file_error_{self.tool_name}")
|
|
self.error_logger.setLevel(logging.ERROR) # ERROR 레벨부터 기록
|
|
|
|
# 기존 핸들러 제거 (중복 방지)
|
|
for handler in self.error_logger.handlers[:]:
|
|
self.error_logger.removeHandler(handler)
|
|
|
|
# 파일 핸들러 추가
|
|
file_handler = logging.FileHandler(self.error_log_path, encoding='utf-8')
|
|
file_handler.setLevel(logging.ERROR) # ERROR 레벨부터 기록
|
|
|
|
# 포맷터 설정
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
file_handler.setFormatter(formatter)
|
|
|
|
self.error_logger.addHandler(file_handler)
|
|
|
|
def _log(self, level: str, master_file: str, operation: str, message: str):
|
|
"""로그 기록 (통합)"""
|
|
log_msg = f"Tool: {self.tool_name}, Master: {master_file}, Operation: {operation}, {level}: {message}"
|
|
|
|
|
|
if level.lower() == "error":
|
|
self.error_logger.error(log_msg)
|
|
elif level.lower() == "warning":
|
|
self.error_logger.warning(log_msg)
|
|
else:
|
|
self.error_logger.info(log_msg)
|
|
|
|
@staticmethod
|
|
def get_master_models_for_tool(tool_name: str) -> List:
|
|
"""툴별 마스터 모델 목록 반환 (정적 메서드)"""
|
|
from model import ALL_MODELS
|
|
|
|
# 툴명을 모델 클래스명으로 변환하는 매핑
|
|
tool_to_model_mapping = {
|
|
"domestic_stock": "DomesticStockMaster",
|
|
"overseas_stock": "OverseasStockMaster",
|
|
"domestic_futureoption": "DomesticFutureoptionMaster",
|
|
"overseas_futureoption": "OverseasFutureoptionMaster",
|
|
"domestic_bond": "DomesticBondMaster",
|
|
"etfetn": "EtfetnMaster",
|
|
"elw": "ElwMaster",
|
|
"auth": "AuthMaster"
|
|
}
|
|
|
|
model_name = tool_to_model_mapping.get(tool_name)
|
|
if not model_name:
|
|
return []
|
|
|
|
# ALL_MODELS에서 해당하는 모델 클래스 반환
|
|
return [model for model in ALL_MODELS if model.__name__ == model_name]
|
|
|
|
# ==============================================
|
|
|
|
async def ensure_master_file_updated(self, ctx, force_update: bool = False):
|
|
"""툴에 필요한 모든 마스터파일 체크 및 업데이트"""
|
|
try:
|
|
# 마스터파일이 필요 없는 툴인 경우 스킵
|
|
if not self.required_masters:
|
|
await ctx.info(f"{self.tool_name} 툴은 마스터파일이 필요하지 않습니다.")
|
|
return
|
|
|
|
# 1. 강제 업데이트가 아닌 경우 툴 전체 업데이트 시간 확인
|
|
if not force_update:
|
|
last_update = self.db_engine.get_master_update_time(self.tool_name)
|
|
if last_update and not self.__should_update_from_db(last_update):
|
|
await ctx.info(f"{self.tool_name} 툴의 마스터파일들이 최신 상태입니다.")
|
|
return
|
|
|
|
# 2. 카테고리 레벨에서 테이블 전체 삭제 (한 번만)
|
|
await self.__clear_category_tables(ctx)
|
|
|
|
# 2-1. 해당 폴더의 CSV 파일들 삭제
|
|
await self.__clear_category_csv_files(ctx)
|
|
|
|
# 3. 각 마스터파일별로 업데이트 (삭제 없이 추가만)
|
|
total_record_count = 0
|
|
for master_name in self.required_masters:
|
|
record_count = await self.__ensure_single_master_updated(ctx, master_name, force_update)
|
|
total_record_count += record_count
|
|
|
|
# 4. 모든 마스터파일 처리 완료 후 툴 전체 업데이트 시간 기록
|
|
if total_record_count > 0:
|
|
self.db_engine.update_master_timestamp(self.tool_name, total_record_count)
|
|
await ctx.info(f"{self.tool_name} 툴의 모든 마스터파일 업데이트 완료 (총 {total_record_count}개 레코드)")
|
|
|
|
except Exception as e:
|
|
# 오류 로그 기록
|
|
self._log("error", "all_masters", "check_update", str(e))
|
|
await ctx.error(f"마스터파일 체크 실패: {str(e)}")
|
|
raise
|
|
|
|
def is_master_file_available(self) -> bool:
|
|
"""마스터파일들이 사용 가능한지 확인"""
|
|
try:
|
|
# 마스터파일이 필요 없는 툴인 경우 True 반환
|
|
if not self.required_masters:
|
|
return True
|
|
|
|
# 툴별 모델에서 데이터가 있는지 확인 (여러 마스터파일이 하나의 모델로 통합됨)
|
|
master_models = self.get_master_models_for_tool(self.tool_name)
|
|
|
|
for model_class in master_models:
|
|
# 데이터베이스에서 레코드 수 확인
|
|
record_count = self.db_engine.count(model_class)
|
|
if record_count == 0:
|
|
return False
|
|
|
|
return True
|
|
|
|
except Exception as e:
|
|
self._log("error", "all_masters", "check_availability", str(e))
|
|
return False
|
|
|
|
# def get_master_file_paths(self, master_name: str) -> str:
|
|
# """마스터파일 경로 반환"""
|
|
# return os.path.join(self.master_dir, f"{master_name}.tmp")
|
|
|
|
async def __ensure_single_master_updated(self, ctx, master_name: str, force_update: bool = False) -> int:
|
|
"""단일 마스터파일 체크 및 업데이트"""
|
|
# 마스터파일 다운로드 및 DB 저장
|
|
await ctx.info(f"{master_name} 마스터파일 업데이트 중...")
|
|
|
|
try:
|
|
# 1. URL 확인
|
|
master_config = self.MASTER_FILE_PROCESS.get(master_name)
|
|
if not master_config:
|
|
raise Exception(f"{master_name}에 대한 마스터파일 설정이 없습니다.")
|
|
|
|
master_url = master_config["file"]
|
|
|
|
# 2. 임시 파일로 다운로드
|
|
temp_file = os.path.join(self.master_dir, f"{master_name}.tmp")
|
|
await ctx.info(f"마스터파일 다운로드 중: {master_name}")
|
|
|
|
success = await self.__download_file(master_url, temp_file)
|
|
if not success:
|
|
raise Exception(f"{master_name} 마스터파일 다운로드 실패")
|
|
|
|
# 3. 파일 가공 (마스터파일별 특화 로직) - DataFrame 반환
|
|
import pandas as pd
|
|
|
|
# MASTER_FILE_PROCESS에서 처리 함수명 가져오기
|
|
process_func_name = master_config.get("process")
|
|
if process_func_name:
|
|
try:
|
|
# 처리 함수 가져오기
|
|
process_func = getattr(self, process_func_name)
|
|
# 가공 함수에서 DataFrame 반환받기
|
|
df = await process_func(temp_file, ctx)
|
|
except Exception as e:
|
|
# 오류 로그 기록
|
|
self._log("error", master_name, "process", str(e))
|
|
await ctx.error(f"마스터파일 가공 실패: {master_name}, 오류: {str(e)}")
|
|
# 빈 DataFrame 반환
|
|
df = pd.DataFrame()
|
|
else:
|
|
# 기본 처리 - 빈 DataFrame 반환
|
|
await ctx.warning(f"지원하지 않는 마스터파일: {master_name}")
|
|
df = pd.DataFrame()
|
|
|
|
# 4. CSV 파일 저장
|
|
await ctx.info(f"CSV 파일 저장 중: {master_name} ({len(df)}개 레코드)")
|
|
csv_saved = await self.__save_csv_file(df, master_name, ctx)
|
|
|
|
# 5. 모델에 저장
|
|
await ctx.info(f"모델에 저장 중: {master_name} ({len(df)}개 레코드)")
|
|
|
|
# 5-1. 모델용 데이터 변환
|
|
model_data = self.__convert_to_model_data(df, master_name)
|
|
|
|
# 5-2. 모델에 저장
|
|
try:
|
|
model_class = self.__get_model_class(master_name)
|
|
if not model_class:
|
|
raise Exception(f"{master_name}에 대한 모델 클래스를 찾을 수 없습니다.")
|
|
|
|
# bulk_replace_master_data 사용 (카테고리 레벨에서 이미 삭제되었으므로 INSERT만)
|
|
record_count = self.db_engine.bulk_replace_master_data(
|
|
model_class=model_class,
|
|
data_list=model_data,
|
|
master_name=master_name
|
|
)
|
|
except Exception as e:
|
|
# 에러 로깅 시스템에 기록
|
|
self._log("error", master_name, "bulk_replace_master_data", str(e))
|
|
raise
|
|
|
|
await ctx.info(f"데이터베이스 저장 완료: {master_name} ({record_count}개 레코드)")
|
|
|
|
# 6. 임시 파일 정리 (보존)
|
|
# if os.path.exists(temp_file):
|
|
# os.remove(temp_file)
|
|
|
|
await ctx.info(f"{master_name} 마스터파일 업데이트 완료")
|
|
return record_count
|
|
|
|
except Exception as e:
|
|
# 오류 로그 기록
|
|
self._log("error", master_name, "download_and_save", str(e))
|
|
# 임시 파일 정리 (보존)
|
|
# temp_file = os.path.join(self.master_dir, f"{master_name}.tmp")
|
|
# if os.path.exists(temp_file):
|
|
# os.remove(temp_file)
|
|
raise
|
|
|
|
def __should_update_from_db(self, last_update: datetime) -> bool:
|
|
"""DB 기반 업데이트 필요 여부 확인"""
|
|
try:
|
|
return last_update.date() < datetime.now().date()
|
|
except (ValueError, AttributeError):
|
|
return True # 날짜 파싱 실패 시 업데이트
|
|
|
|
async def __clear_category_tables(self, ctx):
|
|
"""카테고리 레벨에서 해당 툴의 모든 테이블 데이터 삭제"""
|
|
try:
|
|
# 툴별 모델 클래스들 가져오기
|
|
master_models = self.get_master_models_for_tool(self.tool_name)
|
|
|
|
if not master_models:
|
|
await ctx.info(f"{self.tool_name} 툴에 해당하는 모델이 없습니다.")
|
|
return
|
|
|
|
# 각 모델의 테이블 데이터 삭제
|
|
for model_class in master_models:
|
|
try:
|
|
# 기존 데이터 개수 확인
|
|
before_count = self.db_engine.count(model_class)
|
|
|
|
# 테이블 전체 삭제
|
|
session = self.db_engine.get_session()
|
|
try:
|
|
deleted_count = session.query(model_class).delete()
|
|
session.commit()
|
|
await ctx.info(f"{model_class.__name__} 테이블 데이터 삭제 완료: {deleted_count}개 레코드")
|
|
finally:
|
|
session.close()
|
|
|
|
except Exception as e:
|
|
self._log("error", model_class.__name__, "clear_table", str(e))
|
|
await ctx.error(f"{model_class.__name__} 테이블 삭제 실패: {str(e)}")
|
|
raise
|
|
|
|
except Exception as e:
|
|
self._log("error", "all_models", "clear_category_tables", str(e))
|
|
await ctx.error(f"카테고리 테이블 삭제 실패: {str(e)}")
|
|
raise
|
|
|
|
async def __clear_category_csv_files(self, ctx):
|
|
"""카테고리 레벨에서 해당 툴의 모든 CSV 파일 및 임시 파일 삭제"""
|
|
try:
|
|
import glob
|
|
|
|
# 해당 툴의 마스터 디렉토리 경로
|
|
master_dir = self.master_dir
|
|
|
|
if not os.path.exists(master_dir):
|
|
await ctx.info(f"마스터 디렉토리가 존재하지 않습니다: {master_dir}")
|
|
return
|
|
|
|
# CSV 파일 패턴으로 검색
|
|
csv_pattern = os.path.join(master_dir, "*.csv")
|
|
csv_files = glob.glob(csv_pattern)
|
|
|
|
# 임시 파일 패턴으로 검색 (.tmp, .tmp.zip 등)
|
|
tmp_patterns = [
|
|
os.path.join(master_dir, "*.tmp"),
|
|
os.path.join(master_dir, "*.tmp.*"),
|
|
os.path.join(master_dir, "*_part*.tmp"),
|
|
os.path.join(master_dir, "*_part*.tmp.*")
|
|
]
|
|
|
|
tmp_files = []
|
|
for pattern in tmp_patterns:
|
|
tmp_files.extend(glob.glob(pattern))
|
|
|
|
all_files = csv_files + tmp_files
|
|
|
|
if not all_files:
|
|
await ctx.info(f"삭제할 파일이 없습니다: {master_dir}")
|
|
return
|
|
|
|
# 파일들 삭제
|
|
deleted_count = 0
|
|
for file_path in all_files:
|
|
try:
|
|
os.remove(file_path)
|
|
deleted_count += 1
|
|
file_type = "CSV" if file_path.endswith('.csv') else "TMP"
|
|
await ctx.info(f"{file_type} 파일 삭제: {os.path.basename(file_path)}")
|
|
except Exception as e:
|
|
self._log("error", os.path.basename(file_path), "delete_file", str(e))
|
|
await ctx.warning(f"파일 삭제 실패: {os.path.basename(file_path)} - {str(e)}")
|
|
|
|
await ctx.info(f"파일 삭제 완료: {deleted_count}개 파일 (CSV + TMP)")
|
|
|
|
except Exception as e:
|
|
self._log("error", "all_files", "clear_category_csv_files", str(e))
|
|
await ctx.error(f"카테고리 파일 삭제 실패: {str(e)}")
|
|
raise
|
|
|
|
def __get_model_class(self, master_name: str):
|
|
"""마스터파일명에 해당하는 모델 클래스 반환 - TOOL_MASTER_MAPPING 활용"""
|
|
# TOOL_MASTER_MAPPING을 역방향으로 검색하여 마스터파일이 속한 툴 찾기
|
|
for tool_name, master_files in self.TOOL_MASTER_MAPPING.items():
|
|
if master_name in master_files:
|
|
# 해당 툴의 모델 클래스들 가져오기
|
|
master_models = self.get_master_models_for_tool(tool_name)
|
|
if master_models:
|
|
# 첫 번째 모델 클래스 반환 (각 툴당 하나의 모델만 있음)
|
|
return master_models[0]
|
|
|
|
# 마스터파일이 TOOL_MASTER_MAPPING에 없는 경우 None 반환
|
|
return None
|
|
|
|
async def __download_file(self, url: str, file_path: str) -> bool:
|
|
"""파일 다운로드 (ZIP 파일 지원)"""
|
|
try:
|
|
import zipfile
|
|
import ssl
|
|
|
|
# SSL 컨텍스트 설정 (한국투자증권 서버용)
|
|
ssl._create_default_https_context = ssl._create_unverified_context
|
|
|
|
response = requests.get(url, timeout=60) # 대용량 파일을 위해 타임아웃 증가
|
|
response.raise_for_status()
|
|
|
|
# ZIP 파일인지 확인
|
|
if url.endswith('.zip'):
|
|
# ZIP 파일로 저장
|
|
zip_file_path = file_path + '.zip'
|
|
with open(zip_file_path, 'wb') as f:
|
|
f.write(response.content)
|
|
|
|
# ZIP 파일 압축 해제
|
|
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
|
|
zip_ref.extractall(os.path.dirname(file_path))
|
|
|
|
# 압축 해제된 파일명 찾기
|
|
extracted_files = zip_ref.namelist()
|
|
if extracted_files:
|
|
# .mst 파일 찾기
|
|
mst_file = next((f for f in extracted_files if f.endswith('.mst')), extracted_files[0])
|
|
extracted_path = os.path.join(os.path.dirname(file_path), mst_file)
|
|
|
|
# 원본 파일명으로 이동
|
|
if extracted_path != file_path:
|
|
shutil.move(extracted_path, file_path)
|
|
|
|
# ZIP 파일 삭제
|
|
os.remove(zip_file_path)
|
|
else:
|
|
# 일반 파일로 저장
|
|
with open(file_path, 'wb') as f:
|
|
f.write(response.content)
|
|
|
|
return True
|
|
except Exception as e:
|
|
# 오류 로그 기록
|
|
master_file = os.path.basename(file_path)
|
|
self._log("error", master_file, "download", str(e))
|
|
return False
|
|
|
|
def __get_master_update_status(self, master_name: str = None):
|
|
"""마스터파일 업데이트 상태 조회"""
|
|
if master_name:
|
|
last_update = self.db_engine.get_master_update_time(master_name)
|
|
model_class = self.__get_model_class(master_name)
|
|
record_count = 0
|
|
if model_class:
|
|
record_count = self.db_engine.count(model_class)
|
|
|
|
return {
|
|
"master_name": master_name,
|
|
"last_updated": last_update,
|
|
"record_count": record_count,
|
|
"needs_update": self.__should_update_from_db(last_update) if last_update else True
|
|
}
|
|
|
|
# 툴 전체 상태 조회
|
|
last_update = self.db_engine.get_master_update_time(self.tool_name)
|
|
total_record_count = 0
|
|
|
|
# 툴의 모든 마스터 모델에서 레코드 수 합계
|
|
master_models = self.get_master_models_for_tool(self.tool_name)
|
|
for model_class in master_models:
|
|
total_record_count += self.db_engine.count(model_class)
|
|
|
|
return {
|
|
"tool_name": self.tool_name,
|
|
"last_updated": last_update,
|
|
"total_record_count": total_record_count,
|
|
"needs_update": self.__should_update_from_db(last_update) if last_update else True,
|
|
"master_files": self.required_masters
|
|
}
|
|
|
|
# ========== 공통 유틸리티 메서드들 ==========
|
|
|
|
async def _read_file_with_encoding(self, file_path: str, ctx) -> str:
|
|
"""여러 인코딩을 시도하여 파일 읽기"""
|
|
encodings = ['cp949', 'euc-kr', 'utf-8', 'utf-8-sig', 'iso-8859-1', 'latin1']
|
|
|
|
for encoding in encodings:
|
|
try:
|
|
with open(file_path, mode="r", encoding=encoding) as f:
|
|
content = f.read()
|
|
await ctx.info(f"파일을 {encoding} 인코딩으로 성공적으로 읽었습니다.")
|
|
return content
|
|
except UnicodeDecodeError:
|
|
continue
|
|
except Exception as e:
|
|
await ctx.warning(f"{encoding} 인코딩 시도 중 오류: {str(e)}")
|
|
continue
|
|
|
|
raise Exception("모든 인코딩 시도 실패")
|
|
|
|
def _create_dataframe(self, data, columns):
|
|
"""DataFrame 생성 및 공통 처리"""
|
|
import pandas as pd
|
|
|
|
df = pd.DataFrame(data, columns=columns)
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
return df
|
|
|
|
def _read_csv_with_encoding(self, file_path: str, **kwargs):
|
|
"""여러 인코딩을 시도하여 CSV 파일 읽기"""
|
|
import pandas as pd
|
|
|
|
encodings = ['cp949', 'euc-kr', 'utf-8', 'utf-8-sig', 'iso-8859-1', 'latin1']
|
|
|
|
for encoding in encodings:
|
|
try:
|
|
df = pd.read_csv(file_path, encoding=encoding, **kwargs)
|
|
return df
|
|
except UnicodeDecodeError:
|
|
continue
|
|
except Exception as e:
|
|
continue
|
|
|
|
raise Exception("모든 인코딩 시도 실패")
|
|
|
|
def _read_fwf_with_encoding(self, file_path: str, **kwargs):
|
|
"""여러 인코딩을 시도하여 고정폭 파일 읽기"""
|
|
import pandas as pd
|
|
|
|
encodings = ['cp949', 'euc-kr', 'utf-8', 'utf-8-sig', 'iso-8859-1', 'latin1']
|
|
|
|
for encoding in encodings:
|
|
try:
|
|
df = pd.read_fwf(file_path, encoding=encoding, **kwargs)
|
|
return df
|
|
except UnicodeDecodeError:
|
|
continue
|
|
except Exception as e:
|
|
continue
|
|
|
|
raise Exception("모든 인코딩 시도 실패")
|
|
|
|
def __convert_to_model_data(self, df, master_name: str) -> List[Dict]:
|
|
"""DataFrame을 모델용 데이터로 변환 - MASTER_FILE_PROCESS의 name_key, code_key, ex_value 사용"""
|
|
try:
|
|
model_data = []
|
|
|
|
# MASTER_FILE_PROCESS에서 name_key, code_key, ex_value 가져오기
|
|
master_config = self.MASTER_FILE_PROCESS.get(master_name, {})
|
|
name_key = master_config.get("name_key", "name")
|
|
code_key = master_config.get("code_key", "code")
|
|
ex_value = master_config.get("ex_value", "")
|
|
|
|
for _, row in df.iterrows():
|
|
# 지정된 키로 종목명과 종목코드 추출
|
|
name = None
|
|
code = None
|
|
|
|
# 종목명 추출 (띄어쓰기 제거)
|
|
if name_key in row and pd.notna(row[name_key]) and str(row[name_key]).strip():
|
|
name_str = str(row[name_key]).strip()
|
|
if name_str not in ['nan', 'NaN', 'None', 'null', '']:
|
|
name = name_str.replace(" ", "")
|
|
|
|
# 종목코드 추출
|
|
if code_key in row and pd.notna(row[code_key]) and str(row[code_key]).strip():
|
|
code_str = str(row[code_key]).strip()
|
|
if code_str not in ['nan', 'NaN', 'None', 'null', '']:
|
|
code = code_str
|
|
|
|
# 유효한 데이터만 추가
|
|
if name and code:
|
|
model_data.append({
|
|
'name': name,
|
|
'code': code,
|
|
'ex': ex_value
|
|
})
|
|
|
|
return model_data
|
|
|
|
except Exception as e:
|
|
self._log("error", master_name, "convert_to_model_data", str(e))
|
|
return []
|
|
|
|
async def __save_csv_file(self, df, master_name: str, ctx) -> bool:
|
|
"""DataFrame을 CSV 파일로 저장"""
|
|
try:
|
|
if df.empty:
|
|
await ctx.warning(f"빈 DataFrame으로 CSV 파일을 생성하지 않습니다: {master_name}")
|
|
return False
|
|
|
|
# CSV 저장 전 추가 nan 처리
|
|
df_clean = df.copy()
|
|
df_clean = df_clean.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# CSV 파일 경로 설정
|
|
csv_file_path = os.path.join(self.master_dir, f"{master_name}.csv")
|
|
|
|
# CSV 파일 저장 (UTF-8 BOM 인코딩으로 한글 지원)
|
|
df_clean.to_csv(csv_file_path, index=False, encoding='utf-8-sig')
|
|
|
|
await ctx.info(f"CSV 파일 저장 완료: {csv_file_path} ({len(df_clean)}개 레코드)")
|
|
return True
|
|
|
|
except Exception as e:
|
|
self._log("error", master_name, "save_csv", str(e))
|
|
await ctx.error(f"CSV 파일 저장 실패: {master_name}, 오류: {str(e)}")
|
|
return False
|
|
|
|
|
|
|
|
# ========== 마스터파일별 특화 가공 메서드들 ==========
|
|
|
|
async def __process_domestic_stock(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내주식 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내주식 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
import os
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
tmp_fil1 = raw_file.replace('.tmp', '_part1.tmp')
|
|
tmp_fil2 = raw_file.replace('.tmp', '_part2.tmp')
|
|
wf1 = open(tmp_fil1, mode="w")
|
|
wf2 = open(tmp_fil2, mode="w")
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
rf1 = row[0:len(row) - 222]
|
|
rf1_1 = rf1[0:9].rstrip()
|
|
rf1_2 = rf1[9:21].rstrip()
|
|
rf1_3 = rf1[21:].strip()
|
|
wf1.write(rf1_1 + ',' + rf1_2 + ',' + rf1_3 + '\n')
|
|
rf2 = row[-222:]
|
|
wf2.write(rf2)
|
|
|
|
wf1.close()
|
|
wf2.close()
|
|
|
|
part1_columns = ['short_code', 'standard_code', 'korean_name']
|
|
df1 = self._read_csv_with_encoding(tmp_fil1, header=None, names=part1_columns, dtype=str)
|
|
|
|
field_specs = [2, 1,
|
|
4, 4, 4, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 9,
|
|
5, 5, 1, 1, 1,
|
|
2, 1, 1, 1, 2,
|
|
2, 2, 3, 1, 3,
|
|
12, 12, 8, 15, 21,
|
|
2, 7, 1, 1, 1,
|
|
1, 9, 9, 9, 5,
|
|
9, 8, 9, 3, 1,
|
|
1, 1
|
|
]
|
|
|
|
part2_columns = ['security_group_code', 'market_cap_scale_code',
|
|
'industry_large_code', 'industry_medium_code', 'industry_small_code', 'venture_company_yn',
|
|
'low_liquidity_yn', 'krx_stock_yn', 'etp_product_code', 'krx100_stock_yn',
|
|
'krx_auto_yn', 'krx_semiconductor_yn', 'krx_bio_yn', 'krx_bank_yn', 'spac_yn',
|
|
'krx_energy_chemical_yn', 'krx_steel_yn', 'short_term_overheat_code', 'krx_media_telecom_yn',
|
|
'krx_construction_yn', 'kosdaq_investment_caution_yn', 'krx_security_division', 'krx_ship_division',
|
|
'krx_sector_insurance_yn', 'krx_sector_transport_yn', 'kosdaq150_index_yn', 'stock_base_price',
|
|
'regular_market_unit', 'after_hours_market_unit', 'trading_halt_yn', 'liquidation_yn',
|
|
'management_stock_yn', 'market_warning_code', 'market_warning_risk_yn', 'dishonest_disclosure_yn',
|
|
'bypass_listing_yn', 'lock_division_code', 'par_value_change_code', 'capital_increase_code', 'margin_rate',
|
|
'credit_order_yn', 'credit_period', 'prev_day_volume', 'stock_par_value', 'stock_listing_date', 'listed_shares_thousand',
|
|
'capital', 'settlement_month', 'public_offering_price', 'preferred_stock_code', 'short_sale_overheat_yn', 'unusual_rise_yn',
|
|
'krx300_stock_yn', 'sales', 'operating_profit', 'ordinary_profit', 'net_income', 'roe',
|
|
'base_year_month', 'prev_day_market_cap_billion', 'group_company_code', 'company_credit_limit_exceed_yn', 'collateral_loan_yn', 'securities_lending_yn'
|
|
]
|
|
|
|
df2 = self._read_fwf_with_encoding(tmp_fil2, widths=field_specs, names=part2_columns)
|
|
|
|
df = pd.merge(df1, df2, how='outer', left_index=True, right_index=True)
|
|
|
|
# 공통 DataFrame 처리
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# clean temporary file and dataframe
|
|
del (df1)
|
|
del (df2)
|
|
os.remove(tmp_fil1)
|
|
os.remove(tmp_fil2)
|
|
|
|
await ctx.info(f"국내주식 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_stock_master", "process", str(e))
|
|
await ctx.error(f"국내주식 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_stock_kospi(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내주식 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내주식 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
import os
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
tmp_fil1 = raw_file.replace('.tmp', '_part1.tmp')
|
|
tmp_fil2 = raw_file.replace('.tmp', '_part2.tmp')
|
|
wf1 = open(tmp_fil1, mode="w")
|
|
wf2 = open(tmp_fil2, mode="w")
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
rf1 = row[0:len(row) - 228]
|
|
rf1_1 = rf1[0:9].rstrip()
|
|
rf1_2 = rf1[9:21].rstrip()
|
|
rf1_3 = rf1[21:].strip()
|
|
wf1.write(rf1_1 + ',' + rf1_2 + ',' + rf1_3 + '\n')
|
|
rf2 = row[-228:]
|
|
wf2.write(rf2)
|
|
|
|
wf1.close()
|
|
wf2.close()
|
|
|
|
part1_columns = ['short_code', 'standard_code', 'korean_name']
|
|
df1 = self._read_csv_with_encoding(tmp_fil1, header=None, names=part1_columns, dtype=str)
|
|
|
|
field_specs = [2, 1, 4, 4, 4,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1,
|
|
1, 9, 5, 5, 1,
|
|
1, 1, 2, 1, 1,
|
|
1, 2, 2, 2, 3,
|
|
1, 3, 12, 12, 8,
|
|
15, 21, 2, 7, 1,
|
|
1, 1, 1, 1, 9,
|
|
9, 9, 5, 9, 8,
|
|
9, 3, 1, 1, 1
|
|
]
|
|
|
|
part2_columns = ['group_code', 'market_cap_scale', 'industry_large', 'industry_medium', 'industry_small',
|
|
'manufacturing', 'low_liquidity', 'governance_index_stock', 'kospi200_sector_industry', 'kospi100',
|
|
'kospi50', 'krx', 'etp', 'elw_issuance', 'krx100',
|
|
'krx_auto', 'krx_semiconductor', 'krx_bio', 'krx_bank', 'spac',
|
|
'krx_energy_chemical', 'krx_steel', 'short_term_overheat', 'krx_media_telecom', 'krx_construction',
|
|
'non1', 'krx_security', 'krx_ship', 'krx_sector_insurance', 'krx_sector_transport',
|
|
'sri', 'base_price', 'trading_unit', 'after_hours_unit', 'trading_halt',
|
|
'liquidation', 'management_stock', 'market_warning', 'warning_forecast', 'dishonest_disclosure',
|
|
'bypass_listing', 'lock_division', 'par_value_change', 'capital_increase', 'margin_rate',
|
|
'credit_available', 'credit_period', 'prev_day_volume', 'par_value', 'listing_date',
|
|
'listed_shares', 'capital', 'settlement_month', 'public_offering_price', 'preferred_stock',
|
|
'short_sale_overheat', 'unusual_rise', 'krx300', 'kospi', 'sales',
|
|
'operating_profit', 'ordinary_profit', 'net_income', 'roe', 'base_year_month',
|
|
'market_cap', 'group_company_code', 'company_credit_limit_exceed', 'collateral_loan_available', 'securities_lending_available'
|
|
]
|
|
|
|
df2 = self._read_fwf_with_encoding(tmp_fil2, widths=field_specs, names=part2_columns)
|
|
|
|
df = pd.merge(df1, df2, how='outer', left_index=True, right_index=True)
|
|
|
|
# 공통 DataFrame 처리
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# clean temporary file and dataframe
|
|
del (df1)
|
|
del (df2)
|
|
os.remove(tmp_fil1)
|
|
os.remove(tmp_fil2)
|
|
|
|
await ctx.info(f"국내주식 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_stock_kospi_master", "process", str(e))
|
|
await ctx.error(f"국내주식 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_stock_konex(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내주식 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내주식 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
await ctx.info("복잡한 고정폭 텍스트 파일 파싱 중...")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
lines = file_content.splitlines()
|
|
|
|
data = []
|
|
for row in lines:
|
|
row = row.strip()
|
|
mksc_shrn_iscd = row[0:9].strip()
|
|
stnd_iscd = row[9:21].strip()
|
|
scrt_grp_cls_code = row[-184:-182].strip()
|
|
stck_sdpr = row[-182:-173].strip()
|
|
frml_mrkt_deal_qty_unit = row[-173:-168].strip()
|
|
ovtm_mrkt_deal_qty_unit = row[-168:-163].strip()
|
|
trht_yn = row[-163:-162].strip()
|
|
sltr_yn = row[-162:-161].strip()
|
|
mang_issu_yn = row[-161:-160].strip()
|
|
mrkt_alrm_cls_code = row[-160:-158].strip()
|
|
mrkt_alrm_risk_adnt_yn = row[-158:-157].strip()
|
|
insn_pbnt_yn = row[-157:-156].strip()
|
|
byps_lstn_yn = row[-156:-155].strip()
|
|
flng_cls_code = row[-155:-153].strip()
|
|
fcam_mod_cls_code = row[-153:-151].strip()
|
|
icic_cls_code = row[-151:-149].strip()
|
|
marg_rate = row[-149:-146].strip()
|
|
crdt_able = row[-146:-145].strip()
|
|
crdt_days = row[-145:-142].strip()
|
|
prdy_vol = row[-142:-130].strip()
|
|
stck_fcam = row[-130:-118].strip()
|
|
stck_lstn_date = row[-118:-110].strip()
|
|
lstn_stcn = row[-110:-95].strip()
|
|
cpfn = row[-95:-74].strip()
|
|
stac_month = row[-74:-72].strip()
|
|
po_prc = row[-72:-65].strip()
|
|
prst_cls_code = row[-65:-64].strip()
|
|
ssts_hot_yn = row[-64:-63].strip()
|
|
stange_runup_yn = row[-63:-62].strip()
|
|
krx300_issu_yn = row[-62:-61].strip()
|
|
sale_account = row[-61:-52].strip()
|
|
bsop_prfi = row[-52:-43].strip()
|
|
op_prfi = row[-43:-34].strip()
|
|
thtr_ntin = row[-34:-29].strip()
|
|
roe = row[-29:-20].strip()
|
|
base_date = row[-20:-12].strip()
|
|
prdy_avls_scal = row[-12:-3].strip()
|
|
co_crdt_limt_over_yn = row[-3:-2].strip()
|
|
secu_lend_able_yn = row[-2:-1].strip()
|
|
stln_able_yn = row[-1:].strip()
|
|
hts_kor_isnm = row[21:-184].strip()
|
|
|
|
data.append([mksc_shrn_iscd, stnd_iscd, hts_kor_isnm, scrt_grp_cls_code,
|
|
stck_sdpr, frml_mrkt_deal_qty_unit, ovtm_mrkt_deal_qty_unit,
|
|
trht_yn, sltr_yn, mang_issu_yn, mrkt_alrm_cls_code,
|
|
mrkt_alrm_risk_adnt_yn, insn_pbnt_yn, byps_lstn_yn,
|
|
flng_cls_code, fcam_mod_cls_code, icic_cls_code, marg_rate,
|
|
crdt_able, crdt_days, prdy_vol, stck_fcam, stck_lstn_date,
|
|
lstn_stcn, cpfn, stac_month, po_prc, prst_cls_code, ssts_hot_yn,
|
|
stange_runup_yn, krx300_issu_yn, sale_account, bsop_prfi,
|
|
op_prfi, thtr_ntin, roe, base_date, prdy_avls_scal,
|
|
co_crdt_limt_over_yn, secu_lend_able_yn, stln_able_yn])
|
|
|
|
columns = ['short_code', 'standard_code', 'stock_name', 'security_group_code', 'stock_base_price',
|
|
'regular_market_unit', 'after_hours_market_unit', 'trading_halt_yn',
|
|
'liquidation_yn', 'management_stock_yn', 'market_warning_code', 'market_warning_risk_yn',
|
|
'dishonest_disclosure_yn', 'bypass_listing_yn', 'lock_division_code', 'par_value_change_code',
|
|
'capital_increase_code', 'margin_rate', 'credit_order_yn', 'credit_period', 'prev_day_volume',
|
|
'stock_par_value', 'stock_listing_date', 'listed_shares_thousand', 'capital', 'settlement_month', 'public_offering_price',
|
|
'preferred_stock_code', 'short_sale_overheat_yn', 'unusual_rise_yn', 'krx300_stock_yn',
|
|
'sales', 'operating_profit', 'ordinary_profit', 'net_income', 'roe', 'base_year_month', 'prev_day_market_cap_billion',
|
|
'company_credit_limit_exceed_yn', 'collateral_loan_yn', 'securities_lending_yn']
|
|
|
|
# 공통 DataFrame 처리
|
|
df = self._create_dataframe(data, columns)
|
|
|
|
await ctx.info(f"국내주식 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_stock_konex_master", "process", str(e))
|
|
await ctx.error(f"국내주식 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_overseas_stock(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""해외주식 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("해외주식 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
columns = ['national_code', 'exchange_id', 'exchange_code', 'exchange_name', 'symbol', 'realtime_symbol',
|
|
'korea_name', 'english_name', 'security_type', 'currency',
|
|
'float_position', 'data_type', 'base_price', 'bid_order_size', 'ask_order_size',
|
|
'market_start_time', 'market_end_time', 'dr_yn', 'dr_country_code', 'industry_classification_code',
|
|
'index_constituent_yn', 'tick_size_type',
|
|
'classification_code',
|
|
'tick_size_type_detail']
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
df = self._read_csv_with_encoding(raw_file, sep='\t', dtype=str)
|
|
df.columns = columns
|
|
|
|
# 공통 DataFrame 처리
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
await ctx.info(f"해외주식 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "overseas_stock_master", "process", str(e))
|
|
await ctx.error(f"해외주식 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_overseas_index(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""해외지수 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("해외지수 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
import os
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
tmp_fil1 = raw_file.replace('.tmp', '_part1.tmp')
|
|
tmp_fil2 = raw_file.replace('.tmp', '_part2.tmp')
|
|
wf1 = open(tmp_fil1, mode="w")
|
|
wf2 = open(tmp_fil2, mode="w")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
if row[0:1] == 'X':
|
|
rf1 = row[0:len(row) - 14]
|
|
rf_1 = rf1[0:1]
|
|
rf1_2 = rf1[1:11]
|
|
rf1_3 = rf1[11:40].replace(",", "")
|
|
rf1_4 = rf1[40:80].replace(",", "").strip()
|
|
wf1.write(rf_1 + ',' + rf1_2 + ',' + rf1_3 + ',' + rf1_4 + '\n')
|
|
rf2 = row[-15:]
|
|
wf2.write(rf2 + '\n')
|
|
continue
|
|
rf1 = row[0:len(row) - 14]
|
|
rf1_1 = rf1[0:1]
|
|
rf1_2 = rf1[1:11]
|
|
rf1_3 = rf1[11:50].replace(",", "")
|
|
rf1_4 = row[50:75].replace(",", "").strip()
|
|
wf1.write(rf1_1 + ',' + rf1_2 + ',' + rf1_3 + ',' + rf1_4 + '\n')
|
|
rf2 = row[-15:]
|
|
wf2.write(rf2 + '\n')
|
|
|
|
wf1.close()
|
|
wf2.close()
|
|
|
|
part1_columns = ['division_code', 'symbol', 'english_name', 'korean_name']
|
|
df1 = self._read_csv_with_encoding(tmp_fil1, header=None, names=part1_columns, dtype=str)
|
|
|
|
field_specs = [4, 1, 1, 1, 4, 3]
|
|
part2_columns = ['industry_code', 'dow30_inclusion_yn', 'nasdaq100_inclusion_yn', 'sp500_inclusion_yn', 'exchange_code', 'country_division_code']
|
|
df2 = self._read_fwf_with_encoding(tmp_fil2, widths=field_specs, names=part2_columns)
|
|
df2['industry_code'] = df2['industry_code'].str.replace(pat=r'[^A-Z]', repl=r'', regex=True)
|
|
df2['dow30_inclusion_yn'] = df2['dow30_inclusion_yn'].str.replace(pat=r'[^0-1]+', repl=r'', regex=True)
|
|
df2['nasdaq100_inclusion_yn'] = df2['nasdaq100_inclusion_yn'].str.replace(pat=r'[^0-1]+', repl=r'', regex=True)
|
|
df2['sp500_inclusion_yn'] = df2['sp500_inclusion_yn'].str.replace(pat=r'[^0-1]+', repl=r'', regex=True)
|
|
|
|
DF = pd.concat([df1, df2], axis=1)
|
|
|
|
# 공통 DataFrame 처리
|
|
DF = DF.astype(str)
|
|
DF = DF.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# 임시 파일들 정리
|
|
os.remove(tmp_fil1)
|
|
os.remove(tmp_fil2)
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"해외지수 마스터파일 가공 완료: {len(DF)}개 종목")
|
|
return DF
|
|
|
|
except Exception as e:
|
|
self._log("error", "overseas_index_master", "process", str(e))
|
|
await ctx.error(f"해외지수 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_future(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내선물 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내선물 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
columns = ['product_type', 'short_code', 'standard_code', 'korean_name', 'atm_division',
|
|
'strike_price', 'maturity_division_code', 'underlying_short_code', 'underlying_name']
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
df = self._read_csv_with_encoding(raw_file, sep='|', header=None, dtype=str)
|
|
df.columns = columns
|
|
|
|
# 공통 DataFrame 처리
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"국내선물 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_future_master", "process", str(e))
|
|
await ctx.error(f"국내선물 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_index_future(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내지수선물 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내지수선물 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
columns = ['product_type', 'short_code', 'standard_code', 'korean_name', 'atm_division',
|
|
'strike_price', 'maturity_division_code', 'underlying_short_code', 'underlying_name']
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
df = self._read_csv_with_encoding(raw_file, sep='|', header=None, dtype=str)
|
|
df.columns = columns
|
|
|
|
# 공통 DataFrame 처리
|
|
df = df.astype(str)
|
|
df = df.replace(['nan', 'NaN', 'None', 'null'], '')
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"국내지수선물 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_index_future_master", "process", str(e))
|
|
await ctx.error(f"국내지수선물 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_cme_future(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내CME연계 야간선물 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내CME연계 야간선물 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
columns = ['product_type', 'short_code', 'standard_code', 'korean_name', 'strike_price', 'underlying_short_code', 'underlying_name']
|
|
df = pd.DataFrame(columns=columns)
|
|
ridx = 1
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
a = row[0:1]
|
|
b = row[1:10].strip()
|
|
c = row[10:22].strip()
|
|
d = row[22:63].strip()
|
|
e = row[63:72].strip()
|
|
f = row[72:81].strip()
|
|
g = row[81:].strip()
|
|
df.loc[ridx] = [a, b, c, d, e, f, g]
|
|
ridx += 1
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"국내CME연계 야간선물 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_cme_future_master", "process", str(e))
|
|
await ctx.error(f"국내CME연계 야간선물 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_commodity_future(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내상품선물 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내상품선물 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
import os
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
# df1 : '상품구분','상품종류','단축코드','표준코드','한글종목명'
|
|
tmp_fil1 = raw_file.replace('.tmp', '_part1.tmp')
|
|
tmp_fil2 = raw_file.replace('.tmp', '_part2.tmp')
|
|
wf1 = open(tmp_fil1, mode="w")
|
|
wf2 = open(tmp_fil2, mode="w")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
rf1 = row[0:55]
|
|
rf1_1 = rf1[0:1]
|
|
rf1_2 = rf1[1:2]
|
|
rf1_3 = rf1[2:11].strip()
|
|
rf1_4 = rf1[11:23].strip()
|
|
rf1_5 = rf1[23:55].strip()
|
|
wf1.write(rf1_1 + ',' + rf1_2 + ',' + rf1_3 + ',' + rf1_4 + ',' + rf1_5 + '\n')
|
|
rf2 = row[55:].lstrip()
|
|
wf2.write(rf2)
|
|
|
|
wf1.close()
|
|
wf2.close()
|
|
|
|
part1_columns = ['product_division', 'product_type', 'short_code', 'standard_code', 'korean_name']
|
|
df1 = self._read_csv_with_encoding(tmp_fil1, header=None, names=part1_columns)
|
|
|
|
# df2 : '월물구분코드','기초자산 단축코드','기초자산 명'
|
|
tmp_fil3 = raw_file.replace('.tmp', '_part3.tmp')
|
|
wf3 = open(tmp_fil3, mode="w")
|
|
|
|
# tmp_fil2 읽기 (공통 함수 사용)
|
|
tmp_fil2_content = await self._read_file_with_encoding(tmp_fil2, ctx)
|
|
for row in tmp_fil2_content.splitlines():
|
|
rf2 = row[:]
|
|
rf2_1 = rf2[8:9]
|
|
rf2_2 = rf2[9:12]
|
|
rf2_3 = rf2[12:].strip()
|
|
wf3.write(rf2_1 + ',' + rf2_2 + ',' + rf2_3 + '\n')
|
|
|
|
wf3.close()
|
|
|
|
part2_columns = ['maturity_division_code', 'underlying_short_code', 'underlying_name']
|
|
df2 = self._read_csv_with_encoding(tmp_fil3, header=None, names=part2_columns)
|
|
|
|
# DF : df1 + df2
|
|
df = pd.concat([df1, df2], axis=1)
|
|
|
|
# 임시 파일들 정리
|
|
for tmp_file in [tmp_fil1, tmp_fil2, tmp_fil3]:
|
|
if os.path.exists(tmp_file):
|
|
os.unlink(tmp_file)
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"국내상품선물 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_commodity_future_master", "process", str(e))
|
|
await ctx.error(f"국내상품선물 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_eurex_option(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내EUREX연계 야간옵션 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내EUREX연계 야간옵션 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
import os
|
|
|
|
# 파일이 존재하지 않는 경우 빈 DataFrame 반환
|
|
if not os.path.exists(raw_file):
|
|
await ctx.warning(f"파일이 존재하지 않습니다: {raw_file}")
|
|
return pd.DataFrame()
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
# df1 : '상품종류','단축코드','표준코드','한글종목명'
|
|
tmp_fil1 = raw_file.replace('.tmp', '_part1.tmp')
|
|
tmp_fil2 = raw_file.replace('.tmp', '_part2.tmp')
|
|
wf1 = open(tmp_fil1, mode="w")
|
|
wf2 = open(tmp_fil2, mode="w")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 파일 내용을 라인별로 처리 (참조 사이트와 정확히 동일)
|
|
for row in file_content.splitlines():
|
|
rf1 = row[0:59]
|
|
rf1_1 = rf1[0:1]
|
|
rf1_2 = rf1[1:10]
|
|
rf1_3 = rf1[10:22].strip()
|
|
rf1_4 = rf1[22:59].strip()
|
|
# 마지막 필드에 콤마가 포함될 수 있으므로 따옴표로 감싸기
|
|
wf1.write(rf1_1 + ',' + rf1_2 + ',' + rf1_3 + ',"' + rf1_4 + '"' + '\n')
|
|
rf2 = row[59:].lstrip()
|
|
wf2.write(rf2 + '\n')
|
|
|
|
wf1.close()
|
|
wf2.close()
|
|
|
|
part1_columns = ['product_type', 'short_code', 'standard_code', 'korean_name']
|
|
df1 = self._read_csv_with_encoding(tmp_fil1, header=None, names=part1_columns)
|
|
|
|
|
|
# df2 : 'ATM구분','행사가','기초자산 단축코드','기초자산 명'
|
|
tmp_fil3 = raw_file.replace('.tmp', '_part3.tmp')
|
|
wf3 = open(tmp_fil3, mode="w")
|
|
|
|
# tmp_fil2 읽기 (참조 사이트와 동일하게 cp949 직접 사용)
|
|
with open(tmp_fil2, mode="r", encoding="cp949") as f:
|
|
for row in f:
|
|
rf2 = row[:]
|
|
rf2_1 = rf2[0:1]
|
|
rf2_2 = rf2[1:9]
|
|
rf2_3 = rf2[9:17]
|
|
rf2_4 = rf2[17:].strip()
|
|
wf3.write(rf2_1 + ',' + rf2_2 + ',' + rf2_3 + ',' + rf2_4 + '\n')
|
|
|
|
wf3.close()
|
|
|
|
part2_columns = ['atm_division', 'strike_price', 'underlying_short_code', 'underlying_name']
|
|
df2 = self._read_csv_with_encoding(tmp_fil3, header=None, names=part2_columns)
|
|
|
|
# DF : df1 + df2
|
|
df = pd.concat([df1, df2], axis=1)
|
|
|
|
# 임시 파일들 정리
|
|
for tmp_file in [tmp_fil1, tmp_fil2, tmp_fil3]:
|
|
if os.path.exists(tmp_file):
|
|
os.unlink(tmp_file)
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
await ctx.info(f"국내EUREX연계 야간옵션 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_eurex_option_master", "process", str(e))
|
|
await ctx.error(f"국내EUREX연계 야간옵션 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_overseas_future(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""해외선물 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("해외선물 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
columns = ['stock_code', 'server_auto_order_yn', 'server_auto_twap_yn', 'server_auto_economic_order_yn',
|
|
'filler', 'korean_name', 'exchange_code', 'item_code', 'item_type', 'output_decimal', 'calculation_decimal',
|
|
'tick_size', 'tick_value', 'contract_size', 'price_display_base', 'conversion_multiplier', 'most_active_month_yn',
|
|
'nearest_month_yn', 'spread_yn', 'spread_leg1_yn', 'sub_exchange_code']
|
|
df = pd.DataFrame(columns=columns)
|
|
ridx = 1
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
|
|
# 파일 내용을 라인별로 처리
|
|
for row in file_content.splitlines():
|
|
a = row[:32] # 종목코드
|
|
b = row[32:33].rstrip() # 서버자동주문 가능 종목 여부
|
|
c = row[33:34].rstrip() # 서버자동주문 TWAP 가능 종목 여부
|
|
d = row[34:35] # 서버자동 경제지표 주문 가능 종목 여부
|
|
e = row[35:82].rstrip() # 필러
|
|
f = row[82:107].rstrip() # 종목한글명
|
|
g = row[-92:-82] # 거래소코드 (ISAM KEY 1)
|
|
h = row[-82:-72].rstrip() # 품목코드 (ISAM KEY 2)
|
|
i = row[-72:-69].rstrip() # 품목종류
|
|
j = row[-69:-64] # 출력 소수점
|
|
k = row[-64:-59].rstrip() # 계산 소수점
|
|
l = row[-59:-45].rstrip() # 틱사이즈
|
|
m = row[-45:-31] # 틱가치
|
|
n = row[-31:-21].rstrip() # 계약크기
|
|
o = row[-21:-17].rstrip() # 가격표시진법
|
|
p = row[-17:-7] # 환산승수
|
|
q = row[-7:-6].rstrip() # 최다월물여부 0:원월물 1:최다월물
|
|
r = row[-6:-5].rstrip() # 최근월물여부 0:원월물 1:최근월물
|
|
s = row[-5:-4].rstrip() # 스프레드여부
|
|
t = row[-4:-3].rstrip() # 스프레드기준종목 LEG1 여부 Y/N
|
|
u = row[-3:].rstrip() # 서브 거래소 코드
|
|
|
|
df.loc[ridx] = [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u]
|
|
ridx += 1
|
|
|
|
# DataFrame 직접 반환 (CSV 저장 제거됨)
|
|
|
|
await ctx.info(f"해외선물 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "overseas_future_master", "process", str(e))
|
|
await ctx.error(f"해외선물 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_domestic_bond(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""국내채권 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("국내채권 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
await ctx.info("고정폭 텍스트 파일 파싱 중...")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
lines = file_content.splitlines()
|
|
|
|
data = []
|
|
for row in lines:
|
|
row = row.strip()
|
|
bond_type = row[0:2].strip()
|
|
bond_cls_code = row[2:4].strip()
|
|
stnd_iscd = row[4:16].strip()
|
|
rdmp_date = row[-8:].strip()
|
|
pblc_date = row[-16:-8].strip()
|
|
lstn_date = row[-24:-16].strip()
|
|
bond_int_cls_code = row[-26:-24].strip()
|
|
sname = row[16:-26].rstrip() # 종목명을 뒤에서부터 추출하여 남은 부분
|
|
|
|
data.append(
|
|
[bond_type, bond_cls_code, stnd_iscd, sname, bond_int_cls_code, lstn_date, pblc_date, rdmp_date])
|
|
|
|
columns = ['bond_type', 'bond_classification_code', 'standard_code', 'bond_name', 'bond_interest_classification_code', 'listing_date', 'issue_date', 'redemption_date']
|
|
|
|
# 공통 DataFrame 처리
|
|
df = self._create_dataframe(data, columns)
|
|
|
|
await ctx.info(f"국내채권 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "domestic_bond_master", "process", str(e))
|
|
await ctx.error(f"국내채권 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
async def __process_elw(self, raw_file: str, ctx) -> pd.DataFrame:
|
|
"""ELW 마스터파일 가공 (원본 코드와 동일)"""
|
|
await ctx.info("ELW 마스터파일 가공 중...")
|
|
|
|
try:
|
|
import pandas as pd
|
|
|
|
# 원본 코드와 정확히 동일한 파싱
|
|
await ctx.info("복잡한 고정폭 텍스트 파일 파싱 중...")
|
|
|
|
# 파일 읽기 (공통 로직)
|
|
file_content = await self._read_file_with_encoding(raw_file, ctx)
|
|
lines = file_content.splitlines()
|
|
|
|
data = []
|
|
for row in lines:
|
|
# print(row)
|
|
mksc_shrn_iscd = row[0:9].strip() # 단축코드
|
|
stnd_iscd = row[9:21].strip() # 표준코드
|
|
hts_kor_isnm = row[21:50].strip() # 한글 종목명
|
|
|
|
crow = row[50:].strip()
|
|
elw_nvlt_optn_cls_code = crow[:1].strip() # ELW권리형태
|
|
elw_ko_barrier = crow[1:14].strip() # ELW조기종료발생기준가격
|
|
bskt_yn = crow[14:15].strip() # 바스켓 여부 (Y/N)
|
|
unas_iscd1 = crow[15:24].strip() # 기초자산코드1
|
|
unas_iscd2 = crow[24:33].strip() # 기초자산코드2
|
|
unas_iscd3 = crow[33:42].strip() # 기초자산코드3
|
|
unas_iscd4 = crow[42:51].strip() # 기초자산코드4
|
|
unas_iscd5 = crow[51:60].strip() # 기초자산코드5
|
|
|
|
# Calculate positions from the end of the row
|
|
mrkt_prtt_no10 = row[-6:].strip() # 시장 참가자 번호10
|
|
mrkt_prtt_no9 = row[-11:-6].strip() # 시장 참가자 번호9
|
|
mrkt_prtt_no8 = row[-16:-11].strip() # 시장 참가자 번호8
|
|
mrkt_prtt_no7 = row[-21:-16].strip() # 시장 참가자 번호7
|
|
mrkt_prtt_no6 = row[-26:-21].strip() # 시장 참가자 번호6
|
|
mrkt_prtt_no5 = row[-31:-26].strip() # 시장 참가자 번호5
|
|
mrkt_prtt_no4 = row[-36:-31].strip() # 시장 참가자 번호4
|
|
mrkt_prtt_no3 = row[-41:-36].strip() # 시장 참가자 번호3
|
|
mrkt_prtt_no2 = row[-46:-41].strip() # 시장 참가자 번호2
|
|
mrkt_prtt_no1 = row[-51:-46].strip() # 시장 참가자 번호1
|
|
lstn_stcn = row[-66:-51].strip() # 상장주수(천)
|
|
prdy_avls = row[-75:-66].strip() # 전일시가총액(억)
|
|
paym_date = row[-83:-75].strip() # 지급일
|
|
rght_type_cls_code = row[-84:-83].strip() # 권리 유형 구분 코드
|
|
rmnn_dynu = row[-88:-84].strip() # 잔존 일수
|
|
stck_last_tr_month = row[-96:-88].strip() # 최종거래일
|
|
acpr = row[-105:-96].strip() # 행사가
|
|
elw_pblc_mrkt_prtt_no = row[-110:-105].strip() # 발행사코드
|
|
|
|
elw_pblc_istu_name = row[-11:-110].strip() # 발행사 한글 종목명
|
|
|
|
data.append([mksc_shrn_iscd, stnd_iscd, hts_kor_isnm,
|
|
elw_nvlt_optn_cls_code, elw_ko_barrier, bskt_yn,
|
|
unas_iscd1, unas_iscd2, unas_iscd3, unas_iscd4,
|
|
unas_iscd5, elw_pblc_istu_name, elw_pblc_mrkt_prtt_no,
|
|
acpr, stck_last_tr_month, rmnn_dynu, rght_type_cls_code,
|
|
paym_date, prdy_avls, lstn_stcn, mrkt_prtt_no1,
|
|
mrkt_prtt_no2, mrkt_prtt_no3, mrkt_prtt_no4,
|
|
mrkt_prtt_no5, mrkt_prtt_no6, mrkt_prtt_no7,
|
|
mrkt_prtt_no8, mrkt_prtt_no9, mrkt_prtt_no10])
|
|
|
|
columns = ['short_code', 'standard_code', 'korean_name', 'elw_right_type', 'elw_early_termination_price',
|
|
'basket_yn', 'underlying_code1', 'underlying_code2', 'underlying_code3',
|
|
'underlying_code4', 'underlying_code5', 'issuer_korean_name', 'issuer_code',
|
|
'strike_price', 'last_trading_date', 'remaining_days', 'right_type_division_code', 'payment_date',
|
|
'prev_day_market_cap_billion', 'listed_shares_thousand', 'market_participant_no1',
|
|
'market_participant_no2', 'market_participant_no3', 'market_participant_no4',
|
|
'market_participant_no5', 'market_participant_no6', 'market_participant_no7',
|
|
'market_participant_no8', 'market_participant_no9', 'market_participant_no10']
|
|
|
|
# 공통 DataFrame 처리
|
|
df = self._create_dataframe(data, columns)
|
|
|
|
await ctx.info(f"ELW 마스터파일 가공 완료: {len(df)}개 종목")
|
|
return df
|
|
|
|
except Exception as e:
|
|
self._log("error", "elw_master", "process", str(e))
|
|
await ctx.error(f"ELW 마스터파일 가공 실패: {str(e)}")
|
|
return pd.DataFrame()
|
|
|
|
|