227 lines
8.7 KiB
Python
227 lines
8.7 KiB
Python
|
import argparse
|
||
|
import json
|
||
|
import os
|
||
|
import sqlite3
|
||
|
from typing import Dict, List
|
||
|
from render import *
|
||
|
import db as database
|
||
|
from jinja2 import Environment, PackageLoader, select_autoescape
|
||
|
import pandas as pd
|
||
|
import tqdm
|
||
|
|
||
|
class DataStore:
|
||
|
def __init__(self) -> None:
|
||
|
self.db = sqlite3.connect("stock.db")
|
||
|
self.pricesCache: Dict[str,] = {}
|
||
|
|
||
|
def getAllKRXCorp(self) -> List[database.KRXCorp]:
|
||
|
return database.GetAllKRXCorp(self.db)
|
||
|
|
||
|
def getStockPrice(self,code,length) -> pd.DataFrame:
|
||
|
if code in self.pricesCache and len(self.pricesCache[code]) >= length:
|
||
|
return self.pricesCache[code]
|
||
|
else:
|
||
|
s = GetStockPriceFrom(self.db,code,length)
|
||
|
s = pd.DataFrame(s,
|
||
|
columns=[s for s in database.STOCK_INDEX.__members__.keys()])
|
||
|
s.set_index("DATE", inplace=True)
|
||
|
self.pricesCache[code] = s
|
||
|
return self.pricesCache[code]
|
||
|
|
||
|
def clearCache(self) -> None:
|
||
|
self.pricesCache = {}
|
||
|
|
||
|
def __del__(self) -> None:
|
||
|
self.db.close()
|
||
|
|
||
|
|
||
|
class OutputCollectorElement:
|
||
|
def __init__(self, name: str, description: str) -> None:
|
||
|
self.name = name
|
||
|
self.description = description
|
||
|
self.corpListByDate:Dict[str,database.KRXCorp] = {}
|
||
|
|
||
|
def __str__(self) -> str:
|
||
|
return f"OutputCollectorElement:{self.name}"
|
||
|
|
||
|
def addCorp(self, date, corp):
|
||
|
self.corpListByDate.setdefault(date, []).append(corp)
|
||
|
|
||
|
def toDict(self) -> Dict:
|
||
|
return {
|
||
|
"name": self.name,
|
||
|
"description": self.description,
|
||
|
"corpListByDate": {k:[d.toDict() for d in v]
|
||
|
for k,v in self.corpListByDate.items()}
|
||
|
}
|
||
|
|
||
|
class OutputCollector:
|
||
|
def __init__(self) -> None:
|
||
|
self.data: Dict[str,OutputCollectorElement] = {}
|
||
|
|
||
|
def addResult(self, key, help = ""):
|
||
|
"""
|
||
|
add output category to collect
|
||
|
"""
|
||
|
self.data[key] = OutputCollectorElement(key, help)
|
||
|
|
||
|
def collect(self, key, corp, date):
|
||
|
self.data[key].addCorp(date, corp)
|
||
|
|
||
|
def isVolumeNTimes(stock: pd.DataFrame, mul: float, nday:int, order=1) -> bool:
|
||
|
return stock.iloc[nday]['VOLUME'] > stock.iloc[nday+order]['VOLUME'] * mul
|
||
|
|
||
|
def isVolumeMulPriceGreaterThan(stock: pd.DataFrame, threshold: int, nday: int) -> bool:
|
||
|
return stock.iloc[nday]['VOLUME'] * stock.iloc[nday]['CLOSE'] > threshold
|
||
|
|
||
|
def isMACDCrossSignal(signal: pd.Series, macd: pd.Series, nday: int, order=1) -> bool:
|
||
|
return (signal.iloc[nday] < macd.iloc[nday] and
|
||
|
signal.iloc[nday+order] > macd.iloc[nday+order])
|
||
|
|
||
|
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
|
||
|
return (a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < threshold
|
||
|
|
||
|
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
|
||
|
"""a is bigger than b"""
|
||
|
return (a.iloc[nday] > b.iloc[nday])
|
||
|
|
||
|
def prepareCollector(collector: OutputCollector) -> None:
|
||
|
collector.addResult("cross 2", """\
|
||
|
5일선과 20일선이 서로 만나는 시점 즉 상대 오차가 1% 이하이고
|
||
|
5일선과 60일선이 서로 만나는 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("cross 3", """\
|
||
|
cross 2의 조건에서 더해서 거래량이 이전 날짜보다 3배 증가하고
|
||
|
100000 이상인 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("cross 4", """\
|
||
|
20일선과 60일선이 서로 만나는 시점 즉 상대 오차가 1% 이하이고
|
||
|
거래량이 1000000 이상인 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("d20d5", """\
|
||
|
5일선이 20선보다 큰 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("d20d5VolumeX5", """\
|
||
|
d20d5의 조건에서 더해서 거래량이 이전 날짜보다 5배 증가한 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("DiffDistance", """\
|
||
|
5일선과 20일선이 서로 만나는 시점 즉 상대 오차가 3% 이하이고
|
||
|
5일선과 60일선이 서로 만나고 거래량이 이전 날짜보다 3배 증가한
|
||
|
시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("volume", """\
|
||
|
거래량이 이전 날짜보다 3배 증가한 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("volume5", """\
|
||
|
거래량과 가격의 곱이 50,000,000,000 이상인 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("volumeX5", """\
|
||
|
거래량이 이전 날짜보다 5배 증가한 시점을 찾습니다.
|
||
|
""")
|
||
|
collector.addResult("macd", """\
|
||
|
signal과 macd가 서로 교차한 시점을 찾습니다. 즉 signal이 아래로 떨어지고
|
||
|
macd가 위로 올라가는 시점을 찾습니다.
|
||
|
""")
|
||
|
|
||
|
def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||
|
, nday: int) -> None:
|
||
|
stock = data.getStockPrice(corp.Code,70)
|
||
|
if len(stock) < 70:
|
||
|
return
|
||
|
|
||
|
d5 = stock["CLOSE"].loc[::-1].rolling(window=5
|
||
|
).mean().dropna().loc[::-1]
|
||
|
d20 = stock["CLOSE"].loc[::-1].rolling(window=20
|
||
|
).mean().dropna().loc[::-1]
|
||
|
d60 = stock["CLOSE"].loc[::-1].rolling(window=60
|
||
|
).mean().dropna().loc[::-1]
|
||
|
|
||
|
if (isRelativeDiffLessThan(d5, d20, 0.01, nday) and
|
||
|
isRelativeDiffLessThan(d5, d60, 0.01, nday)):
|
||
|
collector.collect("cross 2", corp, stock.index[nday])
|
||
|
if (isVolumeNTimes(stock, 3, 0) and
|
||
|
isVolumeMulPriceGreaterThan(stock, 100000, nday)):
|
||
|
collector.collect("cross 3", corp, stock.index[nday])
|
||
|
|
||
|
if (isRelativeDiffLessThan(d20, d60, 0.01, nday) and
|
||
|
isVolumeMulPriceGreaterThan(stock, 1000000, nday)):
|
||
|
collector.collect("cross 4", corp, stock.index[nday])
|
||
|
|
||
|
if (isDiffGreaterThan(d5, d20, nday)):
|
||
|
collector.collect("d20d5", corp, stock.index[nday])
|
||
|
if (isVolumeNTimes(stock, 5, nday)):
|
||
|
collector.collect("d20d5VolumeX5", corp, stock.index[nday])
|
||
|
|
||
|
if (isRelativeDiffLessThan(d5, d20, 0.03, nday) and
|
||
|
isRelativeDiffLessThan(d5, d60, 0.03, nday) and
|
||
|
isVolumeNTimes(stock, 3, nday)):
|
||
|
collector.collect("DiffDistance", corp, stock.index[nday])
|
||
|
|
||
|
if (isVolumeNTimes(stock, 3, nday)):
|
||
|
collector.collect("volume", corp, stock.index[nday])
|
||
|
|
||
|
if (isVolumeMulPriceGreaterThan(stock, 50000000, nday)):
|
||
|
collector.collect("volume5", corp, stock.index[nday])
|
||
|
|
||
|
if (isVolumeNTimes(stock, 5, nday)):
|
||
|
collector.collect("volumeX5", corp, stock.index[nday])
|
||
|
|
||
|
ewm12 = stock["CLOSE"].loc[::-1].ewm(span=12).mean().loc[::-1]
|
||
|
ewm26 = stock["CLOSE"].loc[::-1].ewm(span=26).mean().loc[::-1]
|
||
|
macd = (ewm12 - ewm26)
|
||
|
signal = macd.ewm(span=9).mean()
|
||
|
|
||
|
if (isMACDCrossSignal(macd, signal, nday)):
|
||
|
collector.collect("macd", corp, stock.index[nday])
|
||
|
|
||
|
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
|
||
|
parser.add_argument("--format", "-f", choices=["json", "html"], default="html",
|
||
|
help="출력 포맷을 지정합니다. 기본값은 html입니다.")
|
||
|
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
|
||
|
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 모든 주식을 검색합니다.")
|
||
|
parser.add_argument("--printStdout", action="store_true", help="출력한 결과를 표준 출력으로 출력합니다.")
|
||
|
parser.add_argument("--version", "-v", action="version", version="%(prog)s 1.0")
|
||
|
parser.add_argument("--verbose", "-V", action="store_true", help="출력할 내용을 자세히 표시합니다.")
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
args = parser.parse_args()
|
||
|
dataStore = DataStore()
|
||
|
krx_corps = dataStore.getAllKRXCorp()
|
||
|
if args.corp:
|
||
|
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
|
||
|
|
||
|
env = Environment(
|
||
|
loader=PackageLoader('render', 'templates'),
|
||
|
autoescape=select_autoescape(['html', 'xml'])
|
||
|
)
|
||
|
collector = OutputCollector()
|
||
|
prepareCollector(collector)
|
||
|
|
||
|
for corp in tqdm.tqdm(krx_corps):
|
||
|
for nday in range(0, 5):
|
||
|
collect(dataStore, collector, corp, nday)
|
||
|
dataStore.clearCache()
|
||
|
|
||
|
for k,v in collector.data.items():
|
||
|
if args.format == "json":
|
||
|
data = json.dumps(v.toDict(), indent=4, ensure_ascii=False)
|
||
|
if args.printStdout:
|
||
|
print(k)
|
||
|
print(data)
|
||
|
else:
|
||
|
with open(os.path.join(args.dir, k + ".json", encoding="UTF-8"), "w") as f:
|
||
|
f.write(data)
|
||
|
else:
|
||
|
template = env.get_template("Lists.html")
|
||
|
|
||
|
days = v.corpListByDate.keys()
|
||
|
days = list(days)
|
||
|
days.sort(reverse=True)
|
||
|
days = days[:5]
|
||
|
|
||
|
html = template.render(collected=v, title=k, days=days)
|
||
|
if args.printStdout:
|
||
|
print(html)
|
||
|
else:
|
||
|
with open(os.path.join(args.dir, k + ".html"), "w", encoding="UTF-8") as f:
|
||
|
f.write(html)
|