From 6feb9c5a976bd8081485abfa962713a10dc94088 Mon Sep 17 00:00:00 2001 From: dsyoon Date: Mon, 4 Aug 2025 22:54:13 +0900 Subject: [PATCH] init --- HTS2.py | 301 +++++++++++++++++++++++++++++++++++++++++++++++ stock_monitor.py | 33 +++--- 2 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 HTS2.py diff --git a/HTS2.py b/HTS2.py new file mode 100644 index 0000000..8237aa8 --- /dev/null +++ b/HTS2.py @@ -0,0 +1,301 @@ +import pandas as pd +import jwt +import uuid +import time +import requests +import json +import hashlib +from urllib.parse import urlencode + +class HTS: + + bithumb = None + accessKey = None + secretKey = None + apiUrl = None + + def __init__(self): + #self.bithumb = pybithumb.Bithumb(self.con_key, self.sec_key) + + self.bithumb = None + self.accessKey = "a5d33ce55f598185d37cd26272341b7b965c31a59457f7" # 본인의 Connect Key를 입력한다. + self.secretKey = "ODBiYWFmNWE2MTkwYjdhMTNhZTM1YjU5OGY4OGE2MGNkNDY2NzMzMjE2Nzc5NDVlMzBhMDk3NTNmM2M2Mg==" # 본인의 Secret Key를 입력한다. + self.apiUrl = 'https://api.bithumb.com' + + return + + def append(self, stock, df=None, data_1=None): + if df is not None: + for i in range(len(df)): + stock['PRICE'].append( + { + "ymd": df.index[i], + "close": df['close'].iloc[i], + "diff": 0, + "open": df['open'].iloc[i], + "high": df['high'].iloc[i], + "low": df['low'].iloc[i], + "volume": df['volume'].iloc[i], + "avg5": -1, "avg20": -1, "avg60": -1, "avg120": -1, "avg240": -1, "avg480": -1, + "bolingerband_upper": -1, "bolingerband_lower": -1, "bolingerband_middle": -1, "bolingerband_bwi": -1, + "ichimokucloud_changeLine": -1, "ichimokucloud_baseLine": -1, "ichimokucloud_leadingSpan1": -1, "ichimokucloud_leadingSpan2": -1, + "stochastic_fast_k_1": -1, "stochastic_slow_k_1": -1, "stochastic_slow_d_1": -1, + "stochastic_fast_k_2": -1, "stochastic_slow_k_2": -1, "stochastic_slow_d_2": -1, + "stochastic_fast_k_3": -1, "stochastic_slow_k_3": -1, "stochastic_slow_d_3": -1, + "rsi": -1, "rsis": -1, + "macd": -1, "macds": -1, "macdo": -1, "nor_macd": -1, "nor_macds": -1, "nor_macdo": -1, + }) + + if data_1 is not None: + stock['PRICE'].append( + { + "ymd": data_1.index[-1], + "close": data_1['close'].iloc[-1], + "diff": 0, + "open": data_1['open'].iloc[-1], + "high": data_1['high'].iloc[-1], + "low": data_1['low'].iloc[-1], + "volume": data_1['volume'].iloc[-1], + "avg5": -1, "avg20": -1, "avg60": -1, "avg120": -1, "avg240": -1, "avg480": -1, + "bolingerband_upper": -1, "bolingerband_lower": -1, "bolingerband_middle": -1, "bolingerband_bwi": -1, "bolingerband_nor_bwi": -1, + "envelope_upper": -1, "envelope_lower": -1, "envelope_middle": -1, + "ichimokucloud_changeLine": -1, "ichimokucloud_baseLine": -1, "ichimokucloud_leadingSpan1": -1, + "ichimokucloud_leadingSpan2": -1, + "stochastic_fast_k_1": -1, "stochastic_slow_k_1": -1, "stochastic_slow_d_1": -1, + "stochastic_fast_k_2": -1, "stochastic_slow_k_2": -1, "stochastic_slow_d_2": -1, + "stochastic_fast_k_3": -1, "stochastic_slow_k_3": -1, "stochastic_slow_d_3": -1, + "rsi": -1, "rsis": -1, + "macd": -1, "macds": -1, "macdo": -1, "nor_macd": -1, "nor_macds": -1, "nor_macdo": -1, + }) + return + + def getCoinRawData(self, ticker_code, minute=None, day=False, week=False, month=False, to=None, endpoint='/v1/candles'): + url = None + if minute == 0: + # 현재가 정보 + url = (self.apiUrl + "/v1/ticker?markets=KRW-{}").format(ticker_code) + + headers = {"accept": "application/json"} + response = requests.get(url, headers=headers) + json_data = json.loads(response.text) + df_temp = pd.DataFrame(json_data) + if 'trade_date_kst' not in df_temp or 'trade_time_kst' not in df_temp: + return None + df = pd.DataFrame() + df['datetime'] = pd.to_datetime(df_temp['trade_date_kst'], format='%Y-%m-%dT%H:%M:%S') + df['open'] = df_temp['opening_price'] + df['close'] = df_temp['trade_price'] + df['high'] = df_temp['high_price'] + df['low'] = df_temp['low_price'] + df['volume'] = df_temp['trade_volume'] + df = df.set_index('datetime') + df = df.astype(float) + df["datetime"] = df.index + else: + # 분봉 + if minute is not None and minute in {1, 3, 5, 10, 15, 30, 60, 240}: + if to is None: + url = (self.apiUrl + endpoint + "/minutes/{}?market=KRW-{}&count=3000").format(minute, ticker_code) + else: + url = (self.apiUrl + endpoint + "/minutes/{}?market=KRW-{}&count=3000&to={}").format(minute, ticker_code, to) + if day: + if to is None: + url = (self.apiUrl + endpoint + "/days?market=KRW-{}&count=3000").format(ticker_code) + else: + url = (self.apiUrl + endpoint + "/days?market=KRW-{}&count=3000&to={}").format(ticker_code, to) + if week: + if to is None: + url = (self.apiUrl + endpoint + "/weeks?market=KRW-{}&count=3000").format(ticker_code) + else: + url = (self.apiUrl + endpoint + "/weeks?market=KRW-{}&count=3000&to={}").format(ticker_code, to) + if month: + if to is None: + url = (self.apiUrl + endpoint + "/months?market=KRW-{}&count=3000").format(ticker_code) + else: + url = (self.apiUrl + endpoint + "/months?market=KRW-{}&count=3000&to={}").format(ticker_code, to) + + if url is None: + return None + + headers = {"accept": "application/json"} + response = requests.get(url, headers=headers) + json_data = json.loads(response.text) + df_temp = pd.DataFrame(json_data) + if 'candle_date_time_kst' not in df_temp: + return None + df = pd.DataFrame() + #df.columns = ['datetime', 'open', 'close', 'high', 'low', 'volume'] + #df['datetime'] = pd.to_datetime(df_temp['candle_date_time_kst']) + df['datetime'] = pd.to_datetime(df_temp['candle_date_time_kst'], format='%Y-%m-%dT%H:%M:%S') + df['open'] = df_temp['opening_price'] + df['close'] = df_temp['trade_price'] + df['high'] = df_temp['high_price'] + df['low'] = df_temp['low_price'] + df['volume'] = df_temp['candle_acc_trade_volume'] + df = df.set_index('datetime') + df = df.astype(float) + df["datetime"] = df.index + + if df is None: + return None + + return df + + def getTickerList(self): + url = "https://api.bithumb.com/v1/market/all?isDetails=false" + headers = {"accept": "application/json"} + response = requests.get(url, headers=headers) + + tickets = response.json() + return tickets + + def getVirtual_asset_warning(self): + url = "https://api.bithumb.com/v1/market/virtual_asset_warning" + headers = {"accept": "application/json"} + response = requests.get(url, headers=headers) + warning_list = response.json() + return warning_list + + # 거래대금이 많은 순으로 코인리스트를 얻는다. + def getTopCoinList(self, interval, top): + return + + # 현재 가격 얻어오기 + def getCurrentPrice(self, ticker_code, endpoint='/v1/ticker'): + headers = {"accept": "application/json"} + url = (self.apiUrl + endpoint + "?markets=KRW-{}").format(ticker_code) + response = requests.get(url, headers=headers) + + ticker_state = response.json() + return ticker_state + + # 잔고 가져오기 + def getBalances(self, ticker_code=None, endpoint='/v1/accounts'): + + payload = { + 'access_key': self.accessKey, + 'nonce': str(uuid.uuid4()), + 'timestamp': round(time.time() * 1000) + } + jwt_token = jwt.encode(payload, self.secretKey) + authorization_token = 'Bearer {}'.format(jwt_token) + headers = { + 'Authorization': authorization_token + } + response = requests.get(self.apiUrl + endpoint, headers=headers) + balances = response.json() + + """ + [ + {'currency': 'P', 'balance': '78290', 'locked': '0', 'avg_buy_price': '0', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'KRW', 'balance': '4218.401653', 'locked': '0', 'avg_buy_price': '0', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'XRP', 'balance': '13069.27647861', 'locked': '0', 'avg_buy_price': '1917', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'ADA', 'balance': '6941.65484013', 'locked': '0', 'avg_buy_price': '1260', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'BSV', 'balance': '0.00005656', 'locked': '0', 'avg_buy_price': '65450', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'SAND', 'balance': '0.00001158', 'locked': '0', 'avg_buy_price': '544.8', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'AVAX', 'balance': '26.43960509', 'locked': '0', 'avg_buy_price': '60882', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'}, + {'currency': 'XCORE', 'balance': '0.2119', 'locked': '0', 'avg_buy_price': '0', 'avg_buy_price_modified': False, 'unit_currency': 'KRW'} + ] + """ + if ticker_code is None: + return balances + else: + for balance in balances: + if balance['currency'] == ticker_code: + return balance + + return None + + + def order(self, ticker_code, side, ord_type, volume, price=None, endpoint='/v1/orders'): + if ord_type=='limit': + # 지정가 매수 (limit, side=bid) / 매도 (limit, side=ask) + if price is None: + return + + requestBody = dict(market='KRW-'+ticker_code, side=side, volume=volume, price=price, ord_type=ord_type) + else: + # 시장가 매수 (price, side=bid) / 매도 (market, side=ask) + + if ord_type == 'price': + requestBody = dict(market='KRW-' + ticker_code, side=side, price=price, ord_type=ord_type) + else: + requestBody = dict(market='KRW-' + ticker_code, side=side, volume=volume, ord_type=ord_type) + + # Generate access token + query = urlencode(requestBody).encode() + hash = hashlib.sha512() + hash.update(query) + query_hash = hash.hexdigest() + payload = { + 'access_key': self.accessKey, + 'nonce': str(uuid.uuid4()), + 'timestamp': round(time.time() * 1000), + 'query_hash': query_hash, + 'query_hash_alg': 'SHA512', + } + jwt_token = jwt.encode(payload, self.secretKey) + authorization_token = 'Bearer {}'.format(jwt_token) + headers = { + 'Authorization': authorization_token, + 'Content-Type': 'application/json' + } + + response = requests.post(self.apiUrl + endpoint, data=json.dumps(requestBody), headers=headers) + # handle to success or fail + #print(response.json()) + if response.status_code == 200: + return True + return False + + # 시장가 매수한다. 2초 뒤 잔고 데이터 리스트 리턴 + def buyCoinMarket(self, ticker_code, price, count=None): + if price > 5000: + if price < 50000: + self.order(ticker_code, side='bid', ord_type='price', volume=count, price=price) + buy_price = price + else: + repeat = 10 + buy_price = int(price / 1000) * 1000 + buy_amount = int(buy_price / repeat) + while repeat > 0: + self.order(ticker_code, side='bid', ord_type='price', volume=count, price=buy_amount) + repeat -= 1 + time.sleep(0.5) + else: + buy_price = 0 + return buy_price + + # 시장가 매도한다. 2초 뒤 잔고 데이터 리스트 리턴 + def sellCoinMarket(self, ticker_code, price, count): + return self.order(ticker_code, side='ask', ord_type='market', volume=count, price=price) + + # 지정가 매수한다. 2초 뒤 잔고 데이터 리스트 리턴 + def buyCoinLimit(self, ticker_code, price, count): + return self.order(ticker_code, side='bid', ord_type='limit', volume=count, price=price) + + # 지정가 매도한다. 2초 뒤 잔고 데이터 리스트 리턴 + def sellCoinLimit(self, ticker_code, price, count): + return self.order(ticker_code, side='ask', ord_type='limit', volume=count, price=price) + + def getOrderBook(self, ticker_code, endpoint='/v1/orderbook'): + """ + 필드 설명 타입 + market 마켓 코드 String + timestamp 호가 생성 시각 Long + total_ask_size 호가 매도 총 잔량 Double + total_bid_size 호가 매수 총 잔량 Double + orderbook_units 호가 List of Objects + > ask_price 매도호가 Double + > bid_price 매수호가 Double + > ask_size 매도 잔량 Double + > bid_size 매수 잔량 Double + """ + headers = {"accept": "application/json"} + url = (self.apiUrl + endpoint + "?markets=KRW-{}").format(ticker_code) + response = requests.get(url, headers=headers) + + # 매도 총 잔량: sum([units['ask_size'] for units in orders[0]['orderbook_units']]) + # 매수 총 잔량: sum([units['bid_size'] for units in orders[0]['orderbook_units']]) + orders = response.json() + return orders \ No newline at end of file diff --git a/stock_monitor.py b/stock_monitor.py index 2be8399..2704582 100644 --- a/stock_monitor.py +++ b/stock_monitor.py @@ -1,6 +1,6 @@ import pandas as pd -import yfinance as yf +from HTS2 import HTS import pandas as pd from datetime import datetime, timedelta import telegram @@ -13,6 +13,7 @@ import schedule from config import * import FinanceDataReader as fdr +hts = HTS() def send_coin_msg(text): coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN) @@ -36,6 +37,13 @@ def send_coin_telegram_message(message_list, header): return +def buy_ticker(buy_ticker_list): + for buy_ticker in buy_ticker_list: + ticker_code = buy_ticker['symbol'] + _ = hts.buyCoinMarket(ticker_code, 50000) + return + + def send_stock_msg(text): stock_client = telegram.Bot(token=STOCK_TELEGRAM_BOT_TOKEN) @@ -425,6 +433,7 @@ def monitor_kr_stocks(): def monitor_coins(): message_list = [] + buy_ticker_list = [] print("KRW COINs {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) for symbol in KR_COINS: @@ -438,11 +447,11 @@ def monitor_coins(): if info is None: continue info['name'] = KR_COINS[symbol] - print( - f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})") + print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})") if info['alert']: message_list.append(format_ma_message(info, 'KR')) + buy_ticker_list.append(info) except Exception as e: print(f"Error processing data for {symbol}: {str(e)}") else: @@ -459,8 +468,7 @@ def monitor_coins(): if info is None: continue info['name'] = KR_COINS[symbol] - print( - f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})") + print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})") if info['alert']: message_list.append(format_ma_message(info, 'KR')) @@ -472,24 +480,15 @@ def monitor_coins(): if len(message_list) > 0: try: + # buy + buy_ticker(buy_ticker_list) + # send message send_coin_telegram_message(message_list, header="[KRW-COIN]") except Exception as e: print(f"Error sending Telegram message: {str(e)}") return -# ---------------------- -# Indicator utilities -# ---------------------- - -def calculate_bollinger_bands(data: pd.DataFrame, period: int = 20, std: int = 2): - data = data.copy() - data['MA'] = data['Close'].rolling(window=period).mean() - data['STD'] = data['Close'].rolling(window=period).std() - data['Upper'] = data['MA'] + std * data['STD'] - data['Lower'] = data['MA'] - std * data['STD'] - return data - # ---------------------- # Turnaround Detector v6 # ----------------------