This commit is contained in:
dsyoon
2025-08-04 22:54:13 +09:00
parent 8faabc2fe1
commit 6feb9c5a97
2 changed files with 317 additions and 17 deletions

301
HTS2.py Normal file
View File

@@ -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

View File

@@ -1,6 +1,6 @@
import pandas as pd import pandas as pd
import yfinance as yf from HTS2 import HTS
import pandas as pd import pandas as pd
from datetime import datetime, timedelta from datetime import datetime, timedelta
import telegram import telegram
@@ -13,6 +13,7 @@ import schedule
from config import * from config import *
import FinanceDataReader as fdr import FinanceDataReader as fdr
hts = HTS()
def send_coin_msg(text): def send_coin_msg(text):
coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN) coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN)
@@ -36,6 +37,13 @@ def send_coin_telegram_message(message_list, header):
return 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): def send_stock_msg(text):
stock_client = telegram.Bot(token=STOCK_TELEGRAM_BOT_TOKEN) stock_client = telegram.Bot(token=STOCK_TELEGRAM_BOT_TOKEN)
@@ -425,6 +433,7 @@ def monitor_kr_stocks():
def monitor_coins(): def monitor_coins():
message_list = [] message_list = []
buy_ticker_list = []
print("KRW COINs {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) print("KRW COINs {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
for symbol in KR_COINS: for symbol in KR_COINS:
@@ -438,11 +447,11 @@ def monitor_coins():
if info is None: if info is None:
continue continue
info['name'] = KR_COINS[symbol] info['name'] = KR_COINS[symbol]
print( print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})")
f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})")
if info['alert']: if info['alert']:
message_list.append(format_ma_message(info, 'KR')) message_list.append(format_ma_message(info, 'KR'))
buy_ticker_list.append(info)
except Exception as e: except Exception as e:
print(f"Error processing data for {symbol}: {str(e)}") print(f"Error processing data for {symbol}: {str(e)}")
else: else:
@@ -459,8 +468,7 @@ def monitor_coins():
if info is None: if info is None:
continue continue
info['name'] = KR_COINS[symbol] info['name'] = KR_COINS[symbol]
print( print(f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})")
f" - {info['name']} ({symbol}): {info['price']:.2f} -> {info['alert']} ({info['details']['interval']})")
if info['alert']: if info['alert']:
message_list.append(format_ma_message(info, 'KR')) message_list.append(format_ma_message(info, 'KR'))
@@ -472,24 +480,15 @@ def monitor_coins():
if len(message_list) > 0: if len(message_list) > 0:
try: try:
# buy
buy_ticker(buy_ticker_list)
# send message
send_coin_telegram_message(message_list, header="[KRW-COIN]") send_coin_telegram_message(message_list, header="[KRW-COIN]")
except Exception as e: except Exception as e:
print(f"Error sending Telegram message: {str(e)}") print(f"Error sending Telegram message: {str(e)}")
return 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 # Turnaround Detector v6
# ---------------------- # ----------------------