init
This commit is contained in:
306
HTS2.py
Normal file
306
HTS2.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
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 = "a5d33ce55f598185d37cd26272341b7b965c31a59457f7" # 본인의 Connect Key를 입력한다.
|
||||||
|
#secretKey = "ODBiYWFmNWE2MTkwYjdhMTNhZTM1YjU5OGY4OGE2MGNkNDY2NzMzMjE2Nzc5NDVlMzBhMDk3NTNmM2M2Mg==" # 본인의 Secret Key를 입력한다.
|
||||||
|
accessKey = "1b10570cfaacb728fbdbb0b289c367e95ed937b1bd4157" # 본인의 Connect Key를 입력한다.
|
||||||
|
secretKey = "MGU0NzYzMzQyNTJhMDk2MjUxMGFmZWFjYjkyNThlYWJiNmIzOGNjODZjZWE1NmQyMzdiN2JiNDM1Njg1MA==" # 본인의 Secret Key를 입력한다.
|
||||||
|
|
||||||
|
apiUrl = 'https://api.bithumb.com'
|
||||||
|
|
||||||
|
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.accessKey = "1b10570cfaacb728fbdbb0b289c367e95ed937b1bd4157" # 본인의 Connect Key를 입력한다.
|
||||||
|
self.secretKey = "MGU0NzYzMzQyNTJhMDk2MjUxMGFmZWFjYjkyNThlYWJiNmIzOGNjODZjZWE1NmQyMzdiN2JiNDM1Njg1MA==" # 본인의 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
|
||||||
24
PROMPT.txt
Normal file
24
PROMPT.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
#1.
|
||||||
|
봉을 모두 1500개를 출력하고 있습니다.
|
||||||
|
전반적으로 그래프를 이해하세요.
|
||||||
|
그리고 시가, 종가, 고가, 저가, 거래량을 활용하여 많은 기술적 분석을 시도하세요.
|
||||||
|
그리고 최적의 저점에 매수를 할 수 있도록 탐색코드를 작성하세요.
|
||||||
|
|
||||||
|
모든 코인에 동일 조건을 적용할 수 있도록 활용하는 수치는 모두 정규화를 시킨 이후에 시도해야 합니다.
|
||||||
|
|
||||||
|
코드에 탐색을 반영하고 실행하세요, 그리고 매수 시점을 표기하세요. 그리고 최적이 아니라면 다시 실행하세요. 최적의 매수시점을 찾을 때까지 반복하세요.
|
||||||
|
|
||||||
|
|
||||||
|
#2.
|
||||||
|
전제 소스코들 살펴보세요. 데이터는 모든 코인에 동일 적용할 수 있도록 표준화가 되어야 합니다. 그리고 기술적 분석을 이용하여 매수 타이밍을 탐색해야 합니다.
|
||||||
|
혹시 기술적 분석이 아닌 날짜가 매수조건에 들어가 있다면 그러한 부분은 모두 제거해주세요.
|
||||||
|
|
||||||
|
|
||||||
|
#3.
|
||||||
|
5월 6일과 5월 8일 급락하고 그 이후 상승 전환되었습니다. 이 지점만 탐지해서 매수할 수 있는 기술적 기법을 고민해서 찾아주세요. 그리고 저점이라는 매수 구분자로 코드로 반영해주세요. 가장 먼서 실행을 해서 데이터를 확인하세요. 그리고 탐지할때까지 계속 설계와 코드 반영 그리고 다시 실행을 반복하세요. stock_monitor.py는 스케줄러입니다. 시뮬레이션은 stock_simulation.py를 사용하세요.
|
||||||
|
|
||||||
|
특정 월의 데이터를 가져오기 위한 코드를 추가 작성하지 마세요. 데이터는 현재 전체 기간을 이용해서 기술적 분석을 하고 저점을 찾으세요.
|
||||||
|
|
||||||
|
calculate_technical_indicators 함수에 기술적 분석들을 작성하세요
|
||||||
|
check_point 함수에서 매수 여부를 판단하세요.
|
||||||
|
저점 매수는 구분지 buy_lower를 사용하세요.
|
||||||
146
README.md
Normal file
146
README.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# AssetMonitor 주식·코인 모니터링 시스템
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
`AssetMonitor`는 주식‧ETF 및 암호화폐 시장을 실시간으로 감시하여 Bollinger Band, RSI, MACD, 이동평균(Golden-Cross), 거래량 등을 종합 분석한 **매수 후보(signals)**를 텔레그램으로 통보하는 자동화 봇입니다.
|
||||||
|
|
||||||
|
**주요 개선사항:**
|
||||||
|
- **데이터 표준화**: 모든 코인에 동일한 기술적 분석 기준 적용
|
||||||
|
- **순수 기술적 분석**: 날짜 기반 조건 제거, 기술적 지표만 사용
|
||||||
|
- **강화된 기술적 지표**: 스토캐스틱, MFI, OBV, ATR 등 추가 지표 활용
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 구성 파일
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `config.py` | ✅ API 토큰, 텔레그램 채널 ID, 볼린저 밴드/임계값, 모니터링 자산 목록(KR_COINS, US_STOCKS, KR_ETFS) 등 전역 설정을 보관합니다. |
|
||||||
|
| `stock_monitor.py` | 시스템의 핵심 로직이 담긴 실행 스크립트입니다. <br/>• 데이터 수집 ⇒ 기술적 지표 계산 ⇒ 매수 신호 판단 ⇒ 메시지 포맷팅/발송 <br/>• `schedule` 라이브러리로 정해진 시간마다 작업을 자동 실행합니다. |
|
||||||
|
| `requirements.txt` | 프로젝트 의존 패키지를 명시합니다. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 데이터 흐름
|
||||||
|
1. **스케줄 트리거** (`run_schedule`)
|
||||||
|
지정된 시각에 각 모니터링 함수가 호출됩니다.
|
||||||
|
2. **데이터 획득**
|
||||||
|
*주식 / ETF*: `FinanceDataReader`
|
||||||
|
*암호화폐*: 빗썸 **240분 봉** Open API
|
||||||
|
3. **데이터 표준화** (`normalize_data`)
|
||||||
|
- 모든 코인에 동일한 정규화 적용
|
||||||
|
- 20일 롤링 윈도우 기반 Min-Max 정규화
|
||||||
|
4. **기술적 지표 계산** (`calculate_technical_indicators`)
|
||||||
|
- Bollinger Band (기간 20, ±2σ)
|
||||||
|
- RSI(14)
|
||||||
|
- MACD(12-26-9)
|
||||||
|
- 단/중/장기 이동평균선(MA5/20/60)
|
||||||
|
- 거래량 MA5
|
||||||
|
- **추가 지표**: 스토캐스틱, OBV, ATR, MFI
|
||||||
|
5. **매수 후보 판정** (`check_signals`)
|
||||||
|
- *아래 새로운 "매수 후보 전략" 섹션 참조*
|
||||||
|
6. **알림 발송** (`send_*_telegram_message`)
|
||||||
|
multiprocessing Pool을 이용해 다중 메시지를 병렬로 전송합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 매수 후보 전략 (표준화된 기술적 분석)
|
||||||
|
|
||||||
|
| 신호 | 변수명 | 조건 | 의미 |
|
||||||
|
|------|--------|------|------|
|
||||||
|
| 볼린저 하단 근접 | `bb_signal` | `(현재가 - LowerBand) / (UpperBand - LowerBand) < BOLLINGER_THRESHOLD` | 밴드 하단(과매도 영역) 접근 |
|
||||||
|
| RSI 과매도 | `rsi_signal` | `RSI < 30` | 추세 과매도 |
|
||||||
|
| MACD 골든크로스 | `macd_signal` | `이전 MACD < 이전 Signal` **AND** `현재 MACD > 현재 Signal` | 모멘텀 전환 |
|
||||||
|
| 이동평균 골든크로스 | `ma_signal` | `이전 MA5 < 이전 MA20` **AND** `현재 MA5 ≥ 현재 MA20` | 단기 추세 ↗ 전환 |
|
||||||
|
| 거래량 급증 | `volume_signal` | `현재 거래량 > MA5 Volume × 1.5` | 수급 증가 |
|
||||||
|
| **U자 반등 돌파** | `breakout_signal` | ① 최근 `BREAKOUT_LOOKBACK`(30)개 캔들 동안 최고·최저가 차이가 `BUY_THRESHOLD`(15 %) 이상 하락 → ② **현재가가 그 최고가 돌파** | 하락 후 반등의 추세 전환 확인 |
|
||||||
|
| **장기 저항 돌파** | `long_breakout_signal` | 장기간 저항선 돌파 감지 | 장기 추세 전환 |
|
||||||
|
| **스토캐스틱 과매도** | `stoch_signal` | `%K < 20 AND %K > 이전 %K` | 스토캐스틱 과매도 반등 |
|
||||||
|
| **MFI 과매도** | `mfi_signal` | `MFI < 20 AND MFI > 이전 MFI` | 자금 흐름 과매도 반등 |
|
||||||
|
| **OBV 상승** | `obv_signal` | `현재 OBV > 이전 OBV × 1.1` | 거래량 가중 상승 |
|
||||||
|
| **ATR 급증** | `atr_signal` | `현재 ATR > ATR 20일 평균 × 1.5` | 변동성 급증 |
|
||||||
|
|
||||||
|
### 최종 매수 후보 결정 로직
|
||||||
|
```text
|
||||||
|
if breakout_signal or long_breakout_signal:
|
||||||
|
buy = True # 돌파 신호 단독으로도 매수 후보
|
||||||
|
else:
|
||||||
|
# ① 볼린저 + RSI 동시, 또는 ② (신호 ≥ 3개) & (볼린저 또는 RSI 포함)
|
||||||
|
buy = (bb_signal and rsi_signal) or (signal_count >= 3 and (bb_signal or rsi_signal))
|
||||||
|
```
|
||||||
|
*`signal_count` = 위 11개 신호 중 True 개수*
|
||||||
|
|
||||||
|
### 메시지 구성
|
||||||
|
- `매수` : 최종 `buy=True`일 때 메시지 맨 앞에 부착
|
||||||
|
- `신호(n):` 뒤에 활성화된 신호 목록
|
||||||
|
- 볼린저/RSI/MACD/MA/거래량/Breakout/스토캐스틱/MFI/OBV/ATR 각각 표시
|
||||||
|
|
||||||
|
해당 전략으로 **과매도 바닥근처 매수 기회 + 상승 추세 전환 브레이크아웃** 두 영역을 모두 포착할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 스케줄 테이블 (기본값)
|
||||||
|
| 대상 | 실행 시각(서버 기준) | 호출 함수 |
|
||||||
|
|------|----------------------|-----------|
|
||||||
|
| KRW 코인 | 매시간 04, 14, 24, 34, 44, 54분 | `monitor_coins()` |
|
||||||
|
| 미국 주식 / ETF | 05:10, 16:30, 23:30 | `monitor_us_stocks()` |
|
||||||
|
| 한국 ETF / 주식 | 07:10, 18:20 | `monitor_kr_stocks()` |
|
||||||
|
|
||||||
|
> 시간은 `config.py`가 아닌 `stock_monitor.py`의 `run_schedule()` 내부에 하드코딩되어 있습니다. 필요 시 직접 수정하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설치 방법
|
||||||
|
1. Python ≥ 3.9 환경을 준비합니다.
|
||||||
|
2. 저장소를 클론하고 디렉터리로 이동:
|
||||||
|
```bash
|
||||||
|
$ git clone <repo-url>
|
||||||
|
$ cd AssetMonitor
|
||||||
|
```
|
||||||
|
3. 패키지 설치:
|
||||||
|
```bash
|
||||||
|
$ pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
4. **보안 키 등록**
|
||||||
|
민감 정보는 코드에 직접 기록하지 말고 *환경 변수*로 주입하기를 권장합니다.
|
||||||
|
```bash
|
||||||
|
# zsh 예시
|
||||||
|
export COIN_TELEGRAM_BOT_TOKEN="<TOKEN>"
|
||||||
|
export STOCK_TELEGRAM_BOT_TOKEN="<TOKEN>"
|
||||||
|
export COIN_TELEGRAM_CHAT_ID="<CHAT_ID>"
|
||||||
|
export STOCK_TELEGRAM_CHAT_ID="<CHAT_ID>"
|
||||||
|
```
|
||||||
|
또는 `config.py` 내부 상수를 직접 수정할 수 있습니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 방법
|
||||||
|
```bash
|
||||||
|
$ python monitor_coin.py
|
||||||
|
```
|
||||||
|
스크립트가 백그라운드에서 무한 루프로 동작하며 지정된 시간마다 텔레그램 알림을 전송합니다.
|
||||||
|
|
||||||
|
### Docker(선택)
|
||||||
|
컨테이너 실행 예시:
|
||||||
|
```dockerfile
|
||||||
|
FROM python:3.11-slim
|
||||||
|
WORKDIR /app
|
||||||
|
COPY . .
|
||||||
|
RUN pip install -r requirements.txt
|
||||||
|
CMD ["python", "stock_monitor.py"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 커스터마이징
|
||||||
|
- **자산 목록 추가/삭제**: `config.py`의 `KR_COINS`, `US_STOCKS`, `KR_ETFS` 사전을 편집합니다.
|
||||||
|
- **임계값·기간 조정**: `BOLLINGER_PERIOD`, `BOLLINGER_STD`, `BOLLINGER_THRESHOLD`, `BUY_THRESHOLD` 등 변경.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 한계 및 면책 조항
|
||||||
|
본 프로젝트는 교육·연구 목적의 오픈소스 예제로, 투자 손실에 대해 어떠한 책임도 지지 않습니다. 실거래에 사용하려면 충분한 검증과 백테스트를 진행하십시오.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 라이선스
|
||||||
|
MIT (프로젝트 루트의 `LICENSE` 파일 참조, 미존재 시 필요에 따라 추가하세요.)
|
||||||
|
|
||||||
184
common.py
Normal file
184
common.py
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
from monitor_min import Monitor
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
class CommonCoinMonitor(Monitor):
|
||||||
|
"""코인 모니터링 공통 로직 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = 'coins_buy_time.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def get_balances(self) -> dict:
|
||||||
|
"""현재 보유 잔고 정보를 가져옵니다."""
|
||||||
|
tmps = self.getBalances()
|
||||||
|
balances = {}
|
||||||
|
for tmp in tmps:
|
||||||
|
balances[tmp['currency']] = {
|
||||||
|
'balance': float(tmp['balance']),
|
||||||
|
'avg_buy_price': float(tmp['avg_buy_price'])
|
||||||
|
}
|
||||||
|
return balances
|
||||||
|
|
||||||
|
def check_cooldown(self, symbol: str, side: str = 'buy') -> bool:
|
||||||
|
"""매수/매도 쿨다운 시간을 확인합니다."""
|
||||||
|
current_time = datetime.now()
|
||||||
|
last_trade_dt = self.buy_cooldown.get(symbol, {}).get(side, {}).get('datetime')
|
||||||
|
|
||||||
|
if last_trade_dt:
|
||||||
|
time_diff = current_time - last_trade_dt
|
||||||
|
if time_diff.total_seconds() < BUY_MINUTE_LIMIT:
|
||||||
|
print(f"{symbol}: {side} 금지 중 (남은 시간: {BUY_MINUTE_LIMIT - time_diff.total_seconds():.0f}초)")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def save_trade_record(self, symbol: str, side: str, signal: str) -> None:
|
||||||
|
"""거래 기록을 저장합니다."""
|
||||||
|
if self.cooldown_file is not None:
|
||||||
|
try:
|
||||||
|
self.last_signal[symbol] = signal
|
||||||
|
except Exception:
|
||||||
|
self.last_signal[symbol] = ''
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})[side] = {
|
||||||
|
'datetime': datetime.now(),
|
||||||
|
'signal': signal
|
||||||
|
}
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
def check_5_week_lowest(self, data: pd.DataFrame) -> bool:
|
||||||
|
"""5주봉이 20주봉이나 40주봉보다 아래에 있는지 체크합니다."""
|
||||||
|
try:
|
||||||
|
hours_in_week = 24 * 7 # 168 hours
|
||||||
|
period_5w = 5 * hours_in_week # 840 hours
|
||||||
|
period_20w = 20 * hours_in_week # 3,360 hours
|
||||||
|
period_40w = 40 * hours_in_week # 6,720 hours
|
||||||
|
|
||||||
|
if len(data) >= period_40w:
|
||||||
|
wma5 = data['Close'].rolling(window=period_5w).mean().iloc[-1]
|
||||||
|
wma20 = data['Close'].rolling(window=period_20w).mean().iloc[-1]
|
||||||
|
wma40 = data['Close'].rolling(window=period_40w).mean().iloc[-1]
|
||||||
|
|
||||||
|
# 5-week MA is the lowest among 5, 20, 40 week MAs
|
||||||
|
if (wma5 < wma20) and (wma5 < wma40):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_buy(self, symbol: str, buy_amount: float, signal: str, current_price: float) -> bool:
|
||||||
|
"""매수를 실행합니다."""
|
||||||
|
try:
|
||||||
|
actual_buy_amount = self.hts.buyCoinMarket(symbol, buy_amount)
|
||||||
|
self.save_trade_record(symbol, 'buy', signal)
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{signal}], 현재가: {current_price:.4f}, {int(BUY_MINUTE_LIMIT/60)}분간 매수 금지 시작")
|
||||||
|
self.sendMsg("{}".format(self.format_message(symbol, KR_COINS[symbol], current_price, signal, actual_buy_amount)))
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error buying {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def execute_sell(self, symbol: str, sell_amount: float, signal: str, current_price: float, balances: dict) -> bool:
|
||||||
|
"""매도를 실행합니다."""
|
||||||
|
try:
|
||||||
|
available_balance = 0
|
||||||
|
if balances and symbol in balances:
|
||||||
|
available_balance = float(balances[symbol].get('balance', 0))
|
||||||
|
|
||||||
|
if available_balance <= 0:
|
||||||
|
return False
|
||||||
|
|
||||||
|
actual_sell_amount = available_balance * sell_amount
|
||||||
|
_ = self.hts.sellCoinMarket(symbol, 0, actual_sell_amount)
|
||||||
|
self.save_trade_record(symbol, 'sell', signal)
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{signal} 매도], 현재가: {current_price:.4f}")
|
||||||
|
self.sendMsg("[KRW-COIN]\n" + f"• 매도 [COIN] {KR_COINS[symbol]} ({symbol}): {signal} ({'₩'}{current_price:.4f})")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error selling {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_inverse_data(self, symbol: str, interval: int, data: pd.DataFrame, balances: dict, coin_strategy) -> bool:
|
||||||
|
"""인버스 데이터를 처리하여 매도 신호를 확인합니다."""
|
||||||
|
try:
|
||||||
|
inverse_data = self.inverse_data(data)
|
||||||
|
recent_inverse_data = self.annotate_signals(symbol, interval, inverse_data)
|
||||||
|
|
||||||
|
# 허용된 인버스 매도 신호만 처리 (시간봉별 신호 포함)
|
||||||
|
last_signal = str(recent_inverse_data['signal'].iloc[-1]) if 'signal' in recent_inverse_data.columns else ''
|
||||||
|
allowed_signals = coin_strategy.get_sell_signals()
|
||||||
|
|
||||||
|
if last_signal not in allowed_signals:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.check_cooldown(symbol, 'sell'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 코인별 전략에 따라 매도 비율 결정
|
||||||
|
sell_ratio = coin_strategy.get_sell_amount_ratio(last_signal)
|
||||||
|
|
||||||
|
return self.execute_sell(symbol, sell_ratio, last_signal, recent_inverse_data['Close'].iloc[-1], balances)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing inverse data for {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_normal_data(self, symbol: str, interval: int, data: pd.DataFrame, coin_strategy) -> bool:
|
||||||
|
"""일반 데이터를 처리하여 매수 신호를 확인합니다."""
|
||||||
|
try:
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, interval, data)
|
||||||
|
|
||||||
|
# XRP 전용 신호 확인 (hasattr로 메서드 존재 여부 확인)
|
||||||
|
if hasattr(coin_strategy, 'check_xrp_specific_signals'):
|
||||||
|
xrp_signal, xrp_point = coin_strategy.check_xrp_specific_signals(recent_data, len(recent_data) - 1)
|
||||||
|
if xrp_point == 1:
|
||||||
|
recent_data.at[recent_data.index[-1], 'signal'] = xrp_signal
|
||||||
|
recent_data.at[recent_data.index[-1], 'point'] = xrp_point
|
||||||
|
|
||||||
|
if recent_data['point'].iloc[-1] != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not self.check_cooldown(symbol, 'buy'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 코인별 전략에 따라 매수 금액 결정
|
||||||
|
signal = recent_data['signal'].iloc[-1]
|
||||||
|
current_price = recent_data['Close'].iloc[-1]
|
||||||
|
check_5_week_lowest = self.check_5_week_lowest(data)
|
||||||
|
|
||||||
|
buy_amount = coin_strategy.get_buy_amount(signal, current_price, check_5_week_lowest)
|
||||||
|
|
||||||
|
return self.execute_buy(symbol, buy_amount, signal, current_price)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing normal data for {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def monitor_single_coin(self, symbol: str, coin_strategy) -> None:
|
||||||
|
"""단일 코인을 모니터링합니다."""
|
||||||
|
print("[{}] {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), symbol))
|
||||||
|
|
||||||
|
# 3분봉과 1일봉으로 초기 신호 확인
|
||||||
|
intervals = [3, 1440]
|
||||||
|
data = self.get_coin_some_data(symbol, intervals)
|
||||||
|
|
||||||
|
for interval in intervals:
|
||||||
|
|
||||||
|
data[interval] = self.calculate_technical_indicators(data[interval])
|
||||||
|
recent_data = self.annotate_signals(symbol, interval, data[interval])
|
||||||
|
|
||||||
|
# 매수라면
|
||||||
|
if recent_data['point'].iloc[-1] == 1:
|
||||||
|
|
||||||
|
balances = self.get_balances()
|
||||||
|
|
||||||
|
# 인버스 데이터 처리 (매도)
|
||||||
|
self.process_inverse_data(symbol, interval, data[interval], balances, coin_strategy)
|
||||||
|
|
||||||
|
# 일반 데이터 처리 (매수)
|
||||||
|
self.process_normal_data(symbol, interval, data[interval], coin_strategy)
|
||||||
|
else:
|
||||||
|
print(f"Data for {symbol} is empty or None.")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
254
config.py
Normal file
254
config.py
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# 텔레그램 설정
|
||||||
|
COIN_TELEGRAM_BOT_TOKEN = "6435061393:AAHOh9wB5yGNGUdb3SfCYJrrWTBe7wgConM"
|
||||||
|
COIN_TELEGRAM_CHAT_ID = '574661323'
|
||||||
|
|
||||||
|
STOCK_TELEGRAM_BOT_TOKEN = "6874078562:AAEHxGDavfc0ssAXPQIaW8JGYmTR7LNUJOw"
|
||||||
|
STOCK_TELEGRAM_CHAT_ID = '574661323'
|
||||||
|
|
||||||
|
# 몇초 만에 다시 매수를 할 것인지 체크
|
||||||
|
BUY_MINUTE_LIMIT = 1800
|
||||||
|
|
||||||
|
# 볼린저 밴드 설정
|
||||||
|
BOLLINGER_PERIOD = 20 # 볼린저 밴드 기간
|
||||||
|
BOLLINGER_STD = 2 # 표준편차 승수
|
||||||
|
BOLLINGER_THRESHOLD = 0.10 # 하단 밴드 대비 10% 근접 시 알림
|
||||||
|
BUY_THRESHOLD = 0.15
|
||||||
|
BREAKOUT_LOOKBACK = 30 # U자 반등 후 돌파 판단에 사용할 과거 캔들 수 (4시간봉 기준 약 5일)
|
||||||
|
BREAKOUT_WEEK_LOOKBACK = 42 # 4시간봉 1주일 ≒ 42개
|
||||||
|
BREAKOUT_WEEK_LIMIT = 0.05 # 1주일 대비 5% 미만 상승 조건
|
||||||
|
|
||||||
|
# 볼린저 밴드 squeeze 탐지 임계값 (밴드폭/중심선)
|
||||||
|
SQUEEZE_THRESHOLD = 0.04 # 4% 이하
|
||||||
|
|
||||||
|
# 장기간 저항선 돌파 감지 설정
|
||||||
|
RESISTANCE_LOOKBACK = 120 # 저항선 판단을 위한 과거 캔들 수 (예: 120개)
|
||||||
|
RESISTANCE_BREAK_THRESHOLD = 0.01 # 저항선 대비 1% 이상 돌파 시 신호
|
||||||
|
|
||||||
|
KR_COINS = {
|
||||||
|
"ADA": "에이다",
|
||||||
|
"APT": "앱토스",
|
||||||
|
"AVAX": "아발란체",
|
||||||
|
"BCH": "비트코인캐시",
|
||||||
|
"BIO": "바이오프로토콜",
|
||||||
|
"BNB": "비앤비",
|
||||||
|
"BONK": "봉크",
|
||||||
|
"BTC": "비트코인",
|
||||||
|
"ENA": "에테나",
|
||||||
|
"ETC": "이더리움클래식",
|
||||||
|
"ETH": "이더리움",
|
||||||
|
"HBAR": "헤데라",
|
||||||
|
"LINK": "체인링크",
|
||||||
|
"ONDO": "온도파이낸스",
|
||||||
|
"PENGU": "펏지 펭귄",
|
||||||
|
"POL": "폴리콘 에코시스템 토큰",
|
||||||
|
"SEI": "세이",
|
||||||
|
"SOL": "솔라나",
|
||||||
|
"SUI": "수이",
|
||||||
|
"TRX": "트론",
|
||||||
|
"VIRTUAL": "버추얼 프로토콜",
|
||||||
|
"WLD": "월드코인",
|
||||||
|
"XLM": "스텔라루멘",
|
||||||
|
"XRP": "엑스알피"
|
||||||
|
}
|
||||||
|
|
||||||
|
KR_COINS_1 = {
|
||||||
|
"ADA": "에이다",
|
||||||
|
"APT": "앱토스",
|
||||||
|
"AVAX": "아발란체",
|
||||||
|
"BCH": "비트코인캐시",
|
||||||
|
"BIO": "바이오프로토콜",
|
||||||
|
"BNB": "비앤비",
|
||||||
|
"BONK": "봉크",
|
||||||
|
"BTC": "비트코인",
|
||||||
|
"ENA": "에테나",
|
||||||
|
"ETC": "이더리움클래식",
|
||||||
|
"ETH": "이더리움",
|
||||||
|
"HBAR": "헤데라"
|
||||||
|
}
|
||||||
|
|
||||||
|
KR_COINS_2 = {
|
||||||
|
"LINK": "체인링크",
|
||||||
|
"ONDO": "온도파이낸스",
|
||||||
|
"PENGU": "펏지 펭귄",
|
||||||
|
"POL": "폴리콘 에코시스템 토큰",
|
||||||
|
"SEI": "세이",
|
||||||
|
"SOL": "솔라나",
|
||||||
|
"SUI": "수이",
|
||||||
|
"TRX": "트론",
|
||||||
|
"VIRTUAL": "버추얼 프로토콜",
|
||||||
|
"WLD": "월드코인",
|
||||||
|
"XLM": "스텔라루멘",
|
||||||
|
"XRP": "엑스알피"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 주식 설정
|
||||||
|
US_STOCKS = {
|
||||||
|
'VOO': 'Vanguard S&P 500 ETF',
|
||||||
|
'SQQQ': 'ProShares UltraPro Short QQQ',
|
||||||
|
'QID': 'ProShares UltraShort QQQ',
|
||||||
|
'PSQ': 'ProShares Short QQQ',
|
||||||
|
'TQQQ': 'ProShares UltraPro QQQ',
|
||||||
|
'QQQ': 'Invesco QQQ Trust',
|
||||||
|
'SCO': 'ProShares UltraShort Bloomberg Crude Oil',
|
||||||
|
'UCO': 'ProShares Ultra Bloomberg Crude Oil',
|
||||||
|
'GLL': 'ProShares UltraShort Gold',
|
||||||
|
'UGL': 'ProShares Ultra Gold',
|
||||||
|
'SOXS': 'Direxion Daily Semiconductor Bear -3X Shares',
|
||||||
|
'SOXL': 'Direxion Daily Semiconductor Bull 3X Shares',
|
||||||
|
'FNGD': 'MicroSectors™ FANG+™ Index -3X Inverse Leveraged ETN',
|
||||||
|
'FNGU': 'MicroSectors™ FANG+™ Index 3X Leveraged ETN',
|
||||||
|
'FXI': 'iShares China Large-Cap ETF',
|
||||||
|
|
||||||
|
"AAPL": "Apple / AI 칩셋",
|
||||||
|
"ACN": "Accenture",
|
||||||
|
"ADBE": "Adobe",
|
||||||
|
"AMD": "Advanced Micro Devices / AI 반도체",
|
||||||
|
"AMZN": "Amazon / AI 로봇/클라우드",
|
||||||
|
"ASML": "ASML Holding / EUV 리소그래피",
|
||||||
|
"ASTS": "AST SpaceMobile / 위성통신",
|
||||||
|
"AVGO": "Broadcom",
|
||||||
|
"BABA": "Alibaba Group Holdings Ltd ADR",
|
||||||
|
"BAC": "Bank of America",
|
||||||
|
"BE": "Bloom Energy / 고체산화물 연료전지",
|
||||||
|
"CAMT": "Camtek / 반도체 계측기기6",
|
||||||
|
"CHWY": "Chewy / 애완용품 전자상거래",
|
||||||
|
"COIN": "Coinbase / 암호화폐 거래소",
|
||||||
|
"COST": "Costco Wholesale / 회원제 유통",
|
||||||
|
"CPNG": "Coupang LLC",
|
||||||
|
"CRM": "Salesforce.com",
|
||||||
|
"CRWD": "CrowdStrike / AI 사이버보안",
|
||||||
|
"CSCO": "Cisco",
|
||||||
|
"CVX": "Chevron Corp",
|
||||||
|
"DASH": "DoorDash / 배달 플랫폼",
|
||||||
|
"DIS": "Walt Disney",
|
||||||
|
"DQ": "Daqo New Energy Corp ADR",
|
||||||
|
"DXCM": "DexCom / 지속형 혈당측정기",
|
||||||
|
"EBAY": "eBay Inc",
|
||||||
|
"ENPH": "Enphase Energy / 태양광 인버터",
|
||||||
|
"GEO": "GEO Group / 교정시설 운영3",
|
||||||
|
"GOOG": "Alphabet C",
|
||||||
|
"GOOGL": "Alphabet (Google) / AI 검색/자율주행",
|
||||||
|
"GRVY": "Gravity / 온라인 게임",
|
||||||
|
"HD": "Home Depot",
|
||||||
|
"HON": "Honeywell",
|
||||||
|
"IBM": "IBM",
|
||||||
|
"INTC": "Intel / 차세대 반도체",
|
||||||
|
"ISRG": "Intuitive Surgical / 수술로봇",
|
||||||
|
"JNJ": "Johnson & Johnson (JNJ)",
|
||||||
|
"JPM": "JPMorgan",
|
||||||
|
"KLAC": "KLA Corporation / 반도체 검사장비",
|
||||||
|
"KO": "Coca-Cola",
|
||||||
|
"LB": "LandBridge Co / 에너지 인프라3",
|
||||||
|
"LCID": "Lucid Group / 고급 전기차",
|
||||||
|
"LMT": "Lockheed Martin / 방위 시스템",
|
||||||
|
"LRCX": "Lam Research / 반도체 장비",
|
||||||
|
"MA": "Mastercard",
|
||||||
|
"MELI": "MercadoLibre / 라틴아메리카 전자상거래",
|
||||||
|
"META": "Meta Platforms / AI 메타버스",
|
||||||
|
"MNMD": "Mind Medicine / 사이키델릭 치료제",
|
||||||
|
"MS": "Morgan Stanley",
|
||||||
|
"MSFT": "Microsoft / AI 클라우드",
|
||||||
|
"NKE": "Nike",
|
||||||
|
"NOC": "Northrop Grumman / 우주항공",
|
||||||
|
"NTAP": "NetApp Inc",
|
||||||
|
"NVDA": "NVIDIA / AI 반도체",
|
||||||
|
"ORCL": "Oracle",
|
||||||
|
"PLTR": "Palantir Technologies / AI 데이터 분석",
|
||||||
|
"PLUG": "Plug Power / 수소연료전지",
|
||||||
|
"QCOM": "Qualcomm / 모바일 칩셋",
|
||||||
|
"REGN": "Regeneron Pharmaceuticals / 항체 치료제",
|
||||||
|
"RIVN": "Rivian Automotive / 전기트럭",
|
||||||
|
"RKLB": "Rocket Lab / 소형위성 발사체",
|
||||||
|
"RTX": "RTX Corporation / 제트엔진/미사일",
|
||||||
|
"SEDG": "SolarEdge Technologies / 태양광 시스템",
|
||||||
|
"SNOW": "Snowflake / AI 데이터 플랫폼",
|
||||||
|
"SOFI": "SoFi Technologies / 디지털 뱅킹",
|
||||||
|
"SPCE": "Virgin Galactic / 우주관광",
|
||||||
|
"T": "AT&T",
|
||||||
|
"TCTZF": "Tencent Holdings",
|
||||||
|
"TDOC": "Teladoc Health / 원격의료",
|
||||||
|
"TGT": "Target / 오프라인 리테일 혁신",
|
||||||
|
"TSLA": "Tesla / 전기차/에너지 저장",
|
||||||
|
"TSM": "Taiwan Semiconductor",
|
||||||
|
"UNH": "UnitedHealth",
|
||||||
|
"UPST": "Upstart Holdings / AI 대출플랫폼",
|
||||||
|
"V": "Visa A",
|
||||||
|
"VRTX": "Vertex Pharmaceuticals / 난치병 치료제",
|
||||||
|
"VZ": "Verizon",
|
||||||
|
"WGS": "GeneDx Holdings / 유전체 분석3",
|
||||||
|
"WMT": "Walmart",
|
||||||
|
"X": "United States Steel Corporation",
|
||||||
|
"XOM": "Exxon Mobil"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 한국 ETF 설정
|
||||||
|
KR_ETFS = {
|
||||||
|
"251340.KS": 'KODEX 코스닥150선물인버스',
|
||||||
|
"233740.KS": 'KODEX 코스닥150 레버리지',
|
||||||
|
"252670.KS": 'KODEX 200선물인버스2X',
|
||||||
|
"122630.KS": 'KODEX 레버리지',
|
||||||
|
"114800.KS": 'KODEX 인버스',
|
||||||
|
"283580.KS": 'KODEX 중국본토CSI300',
|
||||||
|
"256750.KS": 'KODEX 심천ChiNext(합성)',
|
||||||
|
"185680.KS": 'KODEX 미국S&P바이오(합성)',
|
||||||
|
"218420.KS": 'KODEX 미국S&P에너지(합성)',
|
||||||
|
"132030.KS": 'KODEX 골드선물(H)',
|
||||||
|
"138920.KS": 'KODEX 콩선물(H)',
|
||||||
|
"271060.KS": 'KODEX 3대농산물선물(H)',
|
||||||
|
"117700.KS": 'KODEX 건설',
|
||||||
|
"266420.KS": 'KODEX 헬스케어',
|
||||||
|
"276990.KS": 'KODEX 글로벌4차산업로보틱스(합성)',
|
||||||
|
"244580.KS": 'KODEX 바이오',
|
||||||
|
"091160.KS": 'KODEX 반도체',
|
||||||
|
"140700.KS": 'KODEX 보험',
|
||||||
|
"266410.KS": 'KODEX 필수소비재',
|
||||||
|
"305720.KS": 'KODEX 2차전지산업',
|
||||||
|
"266390.KS": 'KODEX 경기소비재',
|
||||||
|
"117680.KS": 'KODEX 철강',
|
||||||
|
"117460.KS": 'KODEX 에너지화학',
|
||||||
|
"091170.KS": 'KODEX 은행',
|
||||||
|
"376410.KS": 'TIGER 탄소효율그린뉴딜',
|
||||||
|
"005930.KS": "삼성전자 / 반도체,AI",
|
||||||
|
"000660.KS": "SK하이닉스 / 반도체,AI",
|
||||||
|
"035420.KS": "NAVER / 플랫폼,AI",
|
||||||
|
"035720.KS": "카카오 / 플랫폼,AI,핀테크",
|
||||||
|
"051910.KS": "LG화학 / 2차전지,소재",
|
||||||
|
"373220.KS": "LG에너지솔루션 / 2차전지",
|
||||||
|
"096770.KS": "SK이노베이션 / 2차전지,친환경",
|
||||||
|
"066570.KS": "LG전자 / 전장,AI,가전",
|
||||||
|
"003550.KS": "LG / 지주,전지,AI",
|
||||||
|
"005380.KS": "현대차 / 전기차,수소차",
|
||||||
|
"000270.KS": "기아 / 전기차,수소차",
|
||||||
|
"086520.KS": "에코프로 / 2차전지 소재",
|
||||||
|
"336370.KS": "솔루스첨단소재 / 2차전지,소재",
|
||||||
|
"009150.KS": "삼성전기 / 전장,MLCC",
|
||||||
|
"006400.KS": "삼성SDI / 2차전지",
|
||||||
|
"011170.KS": "롯데케미칼 / 2차전지,소재",
|
||||||
|
"010950.KS": "S-Oil / 친환경,정유",
|
||||||
|
"034730.KS": "SK / 지주,AI,친환경",
|
||||||
|
"028260.KS": "삼성물산 / 바이오,건설",
|
||||||
|
"207940.KS": "삼성바이오로직스 / 바이오,CMO",
|
||||||
|
"068270.KS": "셀트리온 / 바이오,항체치료제",
|
||||||
|
"196170.KS": "알테오젠 / 바이오,바이오시밀러",
|
||||||
|
"051900.KS": "LG생활건강 / 소비재,중국",
|
||||||
|
"003490.KS": "대한항공 / 항공,물류",
|
||||||
|
"005935.KS": "삼성전자우 / 반도체",
|
||||||
|
"000810.KS": "삼성화재 / 보험,금융",
|
||||||
|
"105560.KS": "KB금융 / 금융,디지털전환",
|
||||||
|
"055550.KS": "신한지주 / 금융,디지털전환",
|
||||||
|
"316140.KS": "우리금융지주 / 금융",
|
||||||
|
"086790.KS": "하나금융지주 / 금융",
|
||||||
|
"032830.KS": "삼성생명 / 보험",
|
||||||
|
"003670.KS": "포스코홀딩스 / 2차전지,철강,수소",
|
||||||
|
"036570.KS": "엔씨소프트 / 게임,AI",
|
||||||
|
"011200.KS": "HMM / 해운,물류",
|
||||||
|
"005940.KS": "NH투자증권 / 금융",
|
||||||
|
"010130.KS": "고려아연 / 비철금속,2차전지",
|
||||||
|
"001510.KS": "SK증권 / 금융",
|
||||||
|
"017670.KS": "SK텔레콤 / 5G,AI",
|
||||||
|
"030200.KS": "KT / 5G,AI",
|
||||||
|
"033780.KS": "KT&G / 소비재,담배",
|
||||||
|
"034020.KS": "두산에너빌리티 / 원전,친환경",
|
||||||
|
}
|
||||||
120
downloader.py
Normal file
120
downloader.py
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
import sqlite3
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
from HTS2 import HTS
|
||||||
|
from monitor_coin import MonitorCoin
|
||||||
|
|
||||||
|
monitorCoin = MonitorCoin()
|
||||||
|
hts = HTS()
|
||||||
|
|
||||||
|
|
||||||
|
def inserData(symbol, interval, data):
|
||||||
|
conn = sqlite3.connect('./resources/coins.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
tableName = "{}_{}".format(symbol, str(interval))
|
||||||
|
# 테이블/키 생성
|
||||||
|
cursor.execute("CREATE TABLE IF NOT EXISTS {} (CODE text, NAME text, ymdhms datetime, ymd text, hms text, Close REAL, Open REAL, High REAL, Low REAL, Volume REAL)".format(tableName))
|
||||||
|
cursor.execute("CREATE INDEX IF NOT EXISTS {}_idx on {}(CODE, ymdhms)".format(tableName, tableName))
|
||||||
|
|
||||||
|
for i in range(len(data)):
|
||||||
|
ymd = data.index[i].strftime('%Y%m%d')
|
||||||
|
hms = data.index[i].strftime('%H%M%S')
|
||||||
|
ymdhms = data.index[i].strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
Open = data.Open.iloc[i]
|
||||||
|
High = data.High.iloc[i]
|
||||||
|
Low = data.Low.iloc[i]
|
||||||
|
Close = data.Close.iloc[i]
|
||||||
|
Volume = data.Volume.iloc[i]
|
||||||
|
|
||||||
|
cursor.execute("SELECT * from {} where CODE = ? and ymdhms = ?".format(tableName), (symbol, ymdhms, ))
|
||||||
|
arr = cursor.fetchone()
|
||||||
|
if arr:
|
||||||
|
cursor.execute("UPDATE {} SET Close=?, Open=?, High=?, Low=?, Volume=? where CODE=? and ymdhms=?".format(tableName), (Close, Open, High, Low, Volume, symbol, ymdhms))
|
||||||
|
else:
|
||||||
|
cursor.execute("INSERT INTO {} (CODE, NAME, ymdhms, ymd, hms, Close, Open, High, Low, Volume) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".format(tableName), (symbol, KR_COINS[symbol], ymdhms, ymd, hms, Close, Open, High, Low, Volume))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return
|
||||||
|
|
||||||
|
def download():
|
||||||
|
"""
|
||||||
|
KR_COINS = {
|
||||||
|
"AVAX": "아발란체",
|
||||||
|
"BTC": "비트코인",
|
||||||
|
"ETC": "이더리움",
|
||||||
|
"SOL": "솔라나",
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
for symbol in KR_COINS:
|
||||||
|
print(symbol)
|
||||||
|
|
||||||
|
# 1주
|
||||||
|
interval = 10080
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=5000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 1달
|
||||||
|
interval = 43200
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=5000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 1일
|
||||||
|
interval = 1440
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=5000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 60분
|
||||||
|
interval = 60
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=10000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 30분
|
||||||
|
interval = 30
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=10000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 5분
|
||||||
|
interval = 5
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=10000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
# 1분
|
||||||
|
interval = 1
|
||||||
|
data = monitorCoin.get_coin_more_data(symbol, interval, bong_count=10000)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inserData(symbol, interval, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
download()
|
||||||
52
monitor_coin.py
Normal file
52
monitor_coin.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
from monitor_min import Monitor
|
||||||
|
|
||||||
|
class MonitorCoin (Monitor):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = './resources/coins_buy_time.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def monitor_coins(self) -> None:
|
||||||
|
tmps = self.getBalances()
|
||||||
|
balances = {}
|
||||||
|
for tmp in tmps:
|
||||||
|
balances[tmp['currency']] = {'balance': float(tmp['balance']), 'avg_buy_price': float(tmp['avg_buy_price'])}
|
||||||
|
|
||||||
|
print("[{}] KRW COINs: {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), ','.join(KR_COINS.keys())))
|
||||||
|
for symbol in KR_COINS:
|
||||||
|
interval = 60
|
||||||
|
data = self.get_coin_some_data(symbol, interval)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inverseData= self.inverse_data(data)
|
||||||
|
recent_inverseData = self.annotate_signals(symbol, inverseData)
|
||||||
|
if not self.buy_sell_ticker_1h(symbol, recent_inverseData, balances=balances, is_inverse=True):
|
||||||
|
pass
|
||||||
|
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, data)
|
||||||
|
_ = self.buy_sell_ticker_1h(symbol, recent_data, balances=None, is_inverse=False)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"Data for {symbol} is empty or None.")
|
||||||
|
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
return
|
||||||
|
# ------------- Scheduler -------------
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.monitor_coins()
|
||||||
|
time.sleep(10)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
KR_COINS.keys()
|
||||||
|
|
||||||
|
MonitorCoin().run_schedule()
|
||||||
24
monitor_coin_1min_1.py
Normal file
24
monitor_coin_1min_1.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from config import KR_COINS, KR_COINS_1
|
||||||
|
from monitor_coin_1min_base import LiveOneMinuteStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class OneMinuteMonitorGroup1(LiveOneMinuteStrategy):
|
||||||
|
"""KR_COINS_1 그룹 대상 1분봉 실전 매매 모니터."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
coins = {symbol: KR_COINS[symbol] for symbol in KR_COINS_1.keys()}
|
||||||
|
super().__init__(
|
||||||
|
coins=coins,
|
||||||
|
cooldown_file="./resources/coins_buy_time_1min_1.json",
|
||||||
|
position_file="./resources/coins_positions_1min_1.json",
|
||||||
|
entry_max_len=3,
|
||||||
|
grid_limit=4,
|
||||||
|
random_trials=4,
|
||||||
|
sleep_seconds=0.5,
|
||||||
|
max_order_krw=200_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
OneMinuteMonitorGroup1().run_schedule(interval_seconds=30)
|
||||||
|
|
||||||
24
monitor_coin_1min_2.py
Normal file
24
monitor_coin_1min_2.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
from config import KR_COINS, KR_COINS_2
|
||||||
|
from monitor_coin_1min_base import LiveOneMinuteStrategy
|
||||||
|
|
||||||
|
|
||||||
|
class OneMinuteMonitorGroup2(LiveOneMinuteStrategy):
|
||||||
|
"""KR_COINS_2 그룹 대상 1분봉 실전 매매 모니터."""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
coins = {symbol: KR_COINS[symbol] for symbol in KR_COINS_2.keys()}
|
||||||
|
super().__init__(
|
||||||
|
coins=coins,
|
||||||
|
cooldown_file="./resources/coins_buy_time_1min_2.json",
|
||||||
|
position_file="./resources/coins_positions_1min_2.json",
|
||||||
|
entry_max_len=3,
|
||||||
|
grid_limit=4,
|
||||||
|
random_trials=4,
|
||||||
|
sleep_seconds=0.5,
|
||||||
|
max_order_krw=200_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
OneMinuteMonitorGroup2().run_schedule(interval_seconds=30)
|
||||||
|
|
||||||
393
monitor_coin_1min_base.py
Normal file
393
monitor_coin_1min_base.py
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from dataclasses import asdict
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Dict, List, Optional, Tuple
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from config import (
|
||||||
|
COIN_TELEGRAM_CHAT_ID,
|
||||||
|
COIN_TELEGRAM_BOT_TOKEN,
|
||||||
|
KR_COINS,
|
||||||
|
)
|
||||||
|
from monitor_min import Monitor
|
||||||
|
from simulation_1min import (
|
||||||
|
DEFAULT_DB_PATH,
|
||||||
|
DEFAULT_ENTRY_COMBOS,
|
||||||
|
EntrySignalEngine,
|
||||||
|
IndicatorBuilder,
|
||||||
|
MinuteDataLoader,
|
||||||
|
StrategyOptimizer,
|
||||||
|
StrategyParams,
|
||||||
|
generate_entry_combos,
|
||||||
|
get_tick_size,
|
||||||
|
load_strategy_mode,
|
||||||
|
MIN_ORDER_KRW,
|
||||||
|
INITIAL_CAPITAL,
|
||||||
|
MAX_POSITION_KRW,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class LiveOneMinuteStrategy(Monitor):
|
||||||
|
"""공통 1분봉 실전 매매 모니터 베이스 클래스."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
coins: Dict[str, str],
|
||||||
|
cooldown_file: str,
|
||||||
|
position_file: str,
|
||||||
|
entry_max_len: int = 3,
|
||||||
|
grid_limit: int = 4,
|
||||||
|
random_trials: int = 4,
|
||||||
|
sleep_seconds: float = 0.6,
|
||||||
|
max_order_krw: int = 200_000,
|
||||||
|
) -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
self.coins = coins
|
||||||
|
self.position_file = position_file
|
||||||
|
self.grid_limit = grid_limit
|
||||||
|
self.random_trials = random_trials
|
||||||
|
self.sleep_seconds = sleep_seconds
|
||||||
|
self.max_order_krw = max_order_krw
|
||||||
|
self.loader = MinuteDataLoader(DEFAULT_DB_PATH)
|
||||||
|
self.indicator_builder = IndicatorBuilder()
|
||||||
|
self.use_full = load_strategy_mode()
|
||||||
|
self.entry_combos = (
|
||||||
|
generate_entry_combos(entry_max_len) if self.use_full else list(DEFAULT_ENTRY_COMBOS)
|
||||||
|
)
|
||||||
|
self.positions = self._load_positions()
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Position persistence
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _load_positions(self) -> Dict[str, dict]:
|
||||||
|
if not os.path.exists(self.position_file):
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(self.position_file, "r", encoding="utf-8") as f:
|
||||||
|
raw = json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return {}
|
||||||
|
return raw if isinstance(raw, dict) else {}
|
||||||
|
|
||||||
|
def _save_positions(self) -> None:
|
||||||
|
os.makedirs(os.path.dirname(self.position_file), exist_ok=True)
|
||||||
|
with open(self.position_file, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(self.positions, f, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Data / Optimization helpers
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _fetch_enriched_data(self, symbol: str) -> Optional[pd.DataFrame]:
|
||||||
|
"""
|
||||||
|
모니터링 전용 1분봉 데이터 로딩.
|
||||||
|
1) 실시간 + 저장 데이터를 혼합하는 monitor_min.get_coin_some_data를 우선 사용
|
||||||
|
2) 실패 시 DB MinuteDataLoader → API fallback 순으로 보완
|
||||||
|
"""
|
||||||
|
|
||||||
|
def normalize(df: Optional[pd.DataFrame]) -> Optional[pd.DataFrame]:
|
||||||
|
if df is None or df.empty:
|
||||||
|
return None
|
||||||
|
normalized = df.copy()
|
||||||
|
if "datetime" not in normalized.columns:
|
||||||
|
normalized["datetime"] = normalized.index
|
||||||
|
normalized = normalized.reset_index(drop=True)
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
source_df = None
|
||||||
|
|
||||||
|
# 1) monitor_min 방식 (API + DB 혼합)
|
||||||
|
try:
|
||||||
|
mix_df = self.get_coin_some_data(symbol, 1)
|
||||||
|
source_df = normalize(mix_df)
|
||||||
|
except Exception:
|
||||||
|
source_df = None
|
||||||
|
|
||||||
|
# 2) DB MinuteDataLoader
|
||||||
|
if source_df is None:
|
||||||
|
try:
|
||||||
|
loader_df = self.loader.load(symbol, limit=9000)
|
||||||
|
source_df = normalize(loader_df)
|
||||||
|
except Exception:
|
||||||
|
source_df = None
|
||||||
|
|
||||||
|
# 3) API fallback (get_coin_more_data)
|
||||||
|
if source_df is None:
|
||||||
|
try:
|
||||||
|
api_df = self.get_coin_more_data(symbol, 1, bong_count=4000).reset_index()
|
||||||
|
api_df.rename(columns={"index": "datetime"}, inplace=True)
|
||||||
|
source_df = api_df
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if source_df is None or source_df.empty:
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
enriched = self.indicator_builder.enrich(source_df)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "datetime" not in enriched.columns and "datetime" in source_df.columns:
|
||||||
|
enriched["datetime"] = source_df["datetime"]
|
||||||
|
|
||||||
|
return enriched if len(enriched) > 500 else None
|
||||||
|
|
||||||
|
def _optimize_strategy(self, symbol: str, data: pd.DataFrame) -> StrategyParams:
|
||||||
|
optimizer = StrategyOptimizer(data, INITIAL_CAPITAL, entry_combos=self.entry_combos)
|
||||||
|
best_candidates = []
|
||||||
|
try:
|
||||||
|
grid = optimizer.grid_search(limit=self.grid_limit)
|
||||||
|
if grid:
|
||||||
|
best_candidates.extend(grid[:2])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
rand = optimizer.random_search(trials=self.random_trials)
|
||||||
|
if rand:
|
||||||
|
best_candidates.extend(rand[:2])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not best_candidates:
|
||||||
|
# fallback to default params
|
||||||
|
return StrategyParams()
|
||||||
|
|
||||||
|
best_overall = max(best_candidates, key=lambda r: r.metrics.get("CAGR", float("-inf")))
|
||||||
|
return best_overall.params
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Live trading helpers
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def _can_buy(self, symbol: str) -> bool:
|
||||||
|
last_buy_dt = self.buy_cooldown.get(symbol, {}).get("buy", {}).get("datetime")
|
||||||
|
if last_buy_dt and isinstance(last_buy_dt, datetime):
|
||||||
|
diff = datetime.now() - last_buy_dt
|
||||||
|
if diff.total_seconds() < 60: # 최소 1분 쿨다운
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _record_buy(self, symbol: str, signal: str) -> None:
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})["buy"] = {
|
||||||
|
"datetime": datetime.now(),
|
||||||
|
"signal": signal,
|
||||||
|
}
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
def _record_sell(self, symbol: str, signal: str) -> None:
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})["sell"] = {
|
||||||
|
"datetime": datetime.now(),
|
||||||
|
"signal": signal,
|
||||||
|
}
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
def _determine_buy_amount(self, params: StrategyParams, current_price: float) -> float:
|
||||||
|
alloc = max(MIN_ORDER_KRW, INITIAL_CAPITAL * params.risk_pct)
|
||||||
|
alloc = min(alloc, self.max_order_krw, MAX_POSITION_KRW)
|
||||||
|
return alloc
|
||||||
|
|
||||||
|
def _evaluate_entry(self, data: pd.DataFrame, params: StrategyParams) -> bool:
|
||||||
|
if len(data) < params.ma_slow + 10:
|
||||||
|
return False
|
||||||
|
engine = EntrySignalEngine(params)
|
||||||
|
entry_idx = len(data) - 2
|
||||||
|
return engine.evaluate(data, entry_idx)
|
||||||
|
|
||||||
|
def _update_position_record(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
position: dict,
|
||||||
|
field: str,
|
||||||
|
value,
|
||||||
|
) -> None:
|
||||||
|
position[field] = value
|
||||||
|
self.positions[symbol] = position
|
||||||
|
|
||||||
|
def _get_recent_timestamp(self, data: pd.DataFrame) -> str:
|
||||||
|
ts = data["datetime"].iloc[-1]
|
||||||
|
return ts.isoformat() if isinstance(ts, pd.Timestamp) else str(ts)
|
||||||
|
|
||||||
|
def _evaluate_exit(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
data: pd.DataFrame,
|
||||||
|
position: dict,
|
||||||
|
) -> Tuple[bool, float, str, dict]:
|
||||||
|
params = StrategyParams(**position.get("params", {})) if position.get("params") else StrategyParams()
|
||||||
|
engine = EntrySignalEngine(params)
|
||||||
|
idx = len(data) - 1
|
||||||
|
low = data["Low"].iloc[idx]
|
||||||
|
high = data["High"].iloc[idx]
|
||||||
|
close = data["Close"].iloc[idx]
|
||||||
|
macro_trend = data.get("macro_trend", pd.Series([0])).iloc[idx]
|
||||||
|
entry_price = position["entry_price"]
|
||||||
|
best_price = max(position.get("best_price", entry_price), high)
|
||||||
|
atr = position.get("atr", data["atr14"].iloc[idx])
|
||||||
|
trailing_mult = position.get("trailing_mult", params.trailing_atr_mult)
|
||||||
|
|
||||||
|
stop_price = entry_price * (1 - position.get("stop_loss_pct", params.stop_loss_pct))
|
||||||
|
take_price = entry_price * (1 + position.get("take_profit_pct", params.take_profit_pct))
|
||||||
|
trail_price = best_price - atr * trailing_mult
|
||||||
|
time_stop_bars = position.get("time_stop_bars", params.time_stop_bars)
|
||||||
|
vol_drop_threshold = position.get("vol_drop_exit_z", params.vol_drop_exit_z)
|
||||||
|
|
||||||
|
sell_reason = ""
|
||||||
|
exec_price = close
|
||||||
|
executed = False
|
||||||
|
|
||||||
|
if low <= stop_price:
|
||||||
|
exec_price = stop_price - get_tick_size(stop_price)
|
||||||
|
sell_reason = "stop_loss"
|
||||||
|
executed = True
|
||||||
|
elif high >= take_price:
|
||||||
|
exec_price = take_price - get_tick_size(take_price)
|
||||||
|
sell_reason = "take_profit"
|
||||||
|
executed = True
|
||||||
|
elif low <= trail_price:
|
||||||
|
exec_price = trail_price - get_tick_size(trail_price)
|
||||||
|
sell_reason = "trailing_stop"
|
||||||
|
executed = True
|
||||||
|
else:
|
||||||
|
entry_time = position.get("entry_time")
|
||||||
|
if entry_time:
|
||||||
|
try:
|
||||||
|
entry_dt = datetime.fromisoformat(entry_time)
|
||||||
|
bars_held = max(
|
||||||
|
1,
|
||||||
|
int((data["datetime"].iloc[idx] - entry_dt).total_seconds() / 60),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
bars_held = time_stop_bars + 1
|
||||||
|
else:
|
||||||
|
bars_held = time_stop_bars + 1
|
||||||
|
|
||||||
|
if bars_held >= time_stop_bars:
|
||||||
|
sell_reason = "time_stop"
|
||||||
|
executed = True
|
||||||
|
elif (
|
||||||
|
vol_drop_threshold is not None
|
||||||
|
and data.get("volume_z") is not None
|
||||||
|
and data["volume_z"].iloc[idx] <= vol_drop_threshold
|
||||||
|
):
|
||||||
|
sell_reason = "volume_drop"
|
||||||
|
executed = True
|
||||||
|
else:
|
||||||
|
reverse_idx = max(0, len(data) - 2)
|
||||||
|
if engine.evaluate(data, reverse_idx):
|
||||||
|
sell_reason = "reverse_signal"
|
||||||
|
executed = True
|
||||||
|
|
||||||
|
if executed:
|
||||||
|
sell_ratio = 0.5 if macro_trend >= 0 else 1.0
|
||||||
|
qty = position["qty"] * sell_ratio
|
||||||
|
qty = max(qty, 1e-8)
|
||||||
|
return True, qty, sell_reason, {"best_price": best_price}
|
||||||
|
|
||||||
|
return False, 0.0, "", {"best_price": best_price}
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
# Main monitoring routine
|
||||||
|
# ------------------------------------------------------------------ #
|
||||||
|
def monitor_once(self) -> None:
|
||||||
|
balances = {}
|
||||||
|
try:
|
||||||
|
for bal in self.getBalances():
|
||||||
|
balances[bal["currency"]] = float(bal.get("balance", 0))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
for symbol in self.coins.keys():
|
||||||
|
try:
|
||||||
|
data = self._fetch_enriched_data(symbol)
|
||||||
|
if data is None:
|
||||||
|
print(f"{symbol}: 데이터 부족으로 스킵")
|
||||||
|
continue
|
||||||
|
params = self._optimize_strategy(symbol, data)
|
||||||
|
|
||||||
|
# Exit handling first
|
||||||
|
if symbol in self.positions:
|
||||||
|
should_exit, qty, reason, updates = self._evaluate_exit(symbol, data, self.positions[symbol])
|
||||||
|
if should_exit and qty > 0:
|
||||||
|
self._execute_sell(symbol, qty, data["Close"].iloc[-1], reason)
|
||||||
|
if qty >= self.positions[symbol]["qty"]:
|
||||||
|
self.positions.pop(symbol, None)
|
||||||
|
else:
|
||||||
|
self.positions[symbol]["qty"] -= qty
|
||||||
|
self.positions[symbol].update(updates)
|
||||||
|
self._record_sell(symbol, reason)
|
||||||
|
self._save_positions()
|
||||||
|
|
||||||
|
# Entry logic
|
||||||
|
if symbol not in self.positions and self._can_buy(symbol):
|
||||||
|
if self._evaluate_entry(data, params):
|
||||||
|
entry_price = data["Close"].iloc[-1] + get_tick_size(data["Close"].iloc[-1])
|
||||||
|
buy_amount = self._determine_buy_amount(params, entry_price)
|
||||||
|
qty = max(buy_amount / entry_price, 0)
|
||||||
|
if qty <= 0:
|
||||||
|
continue
|
||||||
|
self._execute_buy(symbol, buy_amount, entry_price, params)
|
||||||
|
position_payload = {
|
||||||
|
"entry_price": entry_price,
|
||||||
|
"qty": qty,
|
||||||
|
"atr": data["atr14"].iloc[-1],
|
||||||
|
"best_price": entry_price,
|
||||||
|
"entry_time": self._get_recent_timestamp(data),
|
||||||
|
"stop_loss_pct": params.stop_loss_pct,
|
||||||
|
"take_profit_pct": params.take_profit_pct,
|
||||||
|
"trailing_mult": params.trailing_atr_mult,
|
||||||
|
"time_stop_bars": params.time_stop_bars,
|
||||||
|
"vol_drop_exit_z": params.vol_drop_exit_z,
|
||||||
|
"params": asdict(params),
|
||||||
|
}
|
||||||
|
self.positions[symbol] = position_payload
|
||||||
|
self._record_buy(symbol, "+".join(params.entry_combo))
|
||||||
|
self._save_positions()
|
||||||
|
print("{} {} ({})".format(symbol, data['datetime'].iloc[-1], len(data['datetime'])))
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"[{symbol}] 오류: {exc}")
|
||||||
|
finally:
|
||||||
|
time.sleep(self.sleep_seconds)
|
||||||
|
|
||||||
|
def _execute_buy(self, symbol: str, amount_krw: float, entry_price: float, params: StrategyParams) -> None:
|
||||||
|
try:
|
||||||
|
self.hts.buyCoinMarket(symbol, amount_krw)
|
||||||
|
print(
|
||||||
|
f"[BUY] {symbol} amount={amount_krw:,.0f}KRW price=₩{entry_price:,.2f} "
|
||||||
|
f"strategy={'+'.join(params.entry_combo)}"
|
||||||
|
)
|
||||||
|
msg = (
|
||||||
|
f"[KRW-COIN]\n"
|
||||||
|
f"• 매수 {symbol} : {amount_krw:,.0f}원, 가격 ₩{entry_price:,.2f}\n"
|
||||||
|
f"• 전략 { '+'.join(params.entry_combo) }"
|
||||||
|
)
|
||||||
|
self.sendMsg(msg)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"{symbol} 매수 실패: {exc}")
|
||||||
|
|
||||||
|
def _execute_sell(self, symbol: str, qty: float, current_price: float, reason: str) -> None:
|
||||||
|
try:
|
||||||
|
self.hts.sellCoinMarket(symbol, 0, qty)
|
||||||
|
print(f"[SELL] {symbol} qty={qty:.6f} price=₩{current_price:,.2f} reason={reason}")
|
||||||
|
msg = (
|
||||||
|
f"[KRW-COIN]\n"
|
||||||
|
f"• 매도 {symbol} : 수량 {qty:.6f}, 가격 ₩{current_price:,.2f}\n"
|
||||||
|
f"• 사유 {reason}"
|
||||||
|
)
|
||||||
|
self.sendMsg(msg)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"{symbol} 매도 실패: {exc}")
|
||||||
|
|
||||||
|
def run_schedule(self, interval_seconds: int = 15) -> None:
|
||||||
|
while True:
|
||||||
|
self.monitor_once()
|
||||||
|
time.sleep(interval_seconds)
|
||||||
|
|
||||||
|
|
||||||
|
def data_timestamp_to_iso(data: pd.DataFrame) -> str:
|
||||||
|
pass
|
||||||
|
|
||||||
629
monitor_coin_1mon_1.py
Normal file
629
monitor_coin_1mon_1.py
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
from monitor_min import Monitor
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
class MonthlyCoinMonitor1(Monitor):
|
||||||
|
"""월봉 기준 코인 모니터링 및 매수 실행 클래스 - 전략 1: 글로벌 전략 기반"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = './resources/coins_buy_time_1mon_1.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def calculate_monthly_indicators(self, data: pd.DataFrame, is_weekly: bool = False) -> pd.DataFrame:
|
||||||
|
"""월봉/주봉 전용 기술적 지표 계산"""
|
||||||
|
data = data.copy()
|
||||||
|
|
||||||
|
if is_weekly:
|
||||||
|
# 주봉 이동평균선 (3, 6, 12, 24, 36주)
|
||||||
|
data['MA3'] = data['Close'].rolling(window=3).mean()
|
||||||
|
data['MA6'] = data['Close'].rolling(window=6).mean()
|
||||||
|
data['MA12'] = data['Close'].rolling(window=12).mean()
|
||||||
|
data['MA24'] = data['Close'].rolling(window=24).mean()
|
||||||
|
data['MA36'] = data['Close'].rolling(window=36).mean()
|
||||||
|
else:
|
||||||
|
# 월봉 이동평균선 (3, 6, 12, 24, 36개월)
|
||||||
|
data['MA3'] = data['Close'].rolling(window=3).mean()
|
||||||
|
data['MA6'] = data['Close'].rolling(window=6).mean()
|
||||||
|
data['MA12'] = data['Close'].rolling(window=12).mean()
|
||||||
|
data['MA24'] = data['Close'].rolling(window=24).mean()
|
||||||
|
data['MA36'] = data['Close'].rolling(window=36).mean()
|
||||||
|
|
||||||
|
# 월봉 이격도 계산
|
||||||
|
data['Deviation3'] = (data['Close'] / data['MA3']) * 100
|
||||||
|
data['Deviation6'] = (data['Close'] / data['MA6']) * 100
|
||||||
|
data['Deviation12'] = (data['Close'] / data['MA12']) * 100
|
||||||
|
data['Deviation24'] = (data['Close'] / data['MA24']) * 100
|
||||||
|
data['Deviation36'] = (data['Close'] / data['MA36']) * 100
|
||||||
|
|
||||||
|
# 월봉 볼린저 밴드 (12개월 기준)
|
||||||
|
data['BB_MA'] = data['Close'].rolling(window=12).mean()
|
||||||
|
data['BB_STD'] = data['Close'].rolling(window=12).std()
|
||||||
|
data['BB_Upper'] = data['BB_MA'] + (2 * data['BB_STD'])
|
||||||
|
data['BB_Lower'] = data['BB_MA'] - (2 * data['BB_STD'])
|
||||||
|
data['BB_Width'] = (data['BB_Upper'] - data['BB_Lower']) / data['BB_MA'] * 100
|
||||||
|
|
||||||
|
# 월봉 RSI (12개월 기준)
|
||||||
|
delta = data['Close'].diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=12).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=12).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
data['RSI'] = 100 - (100 / (1 + rs))
|
||||||
|
|
||||||
|
# 월봉 MACD
|
||||||
|
ema12 = data['Close'].ewm(span=12).mean()
|
||||||
|
ema26 = data['Close'].ewm(span=26).mean()
|
||||||
|
data['MACD'] = ema12 - ema26
|
||||||
|
data['MACD_Signal'] = data['MACD'].ewm(span=9).mean()
|
||||||
|
data['MACD_Histogram'] = data['MACD'] - data['MACD_Signal']
|
||||||
|
|
||||||
|
# 변동성 지표
|
||||||
|
data['Volatility'] = data['Close'].rolling(window=12).std() / data['Close'].rolling(window=12).mean() * 100
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def generate_monthly_signals(self, symbol: str, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""simulation_1mon.py와 동일한 글로벌 전략 기반 매수 신호 생성"""
|
||||||
|
data = data.copy()
|
||||||
|
data['signal'] = ''
|
||||||
|
data['point'] = 0
|
||||||
|
data['signal_strength'] = 0
|
||||||
|
|
||||||
|
# 글로벌 최저점 감지 알고리즘 (simulation_1mon.py와 완전 동일)
|
||||||
|
def find_global_lows(data, short_window=3, long_window=9):
|
||||||
|
"""전체 데이터에서 글로벌 최저점들 찾기 - 개선된 다중 윈도우 방식"""
|
||||||
|
global_lows = []
|
||||||
|
|
||||||
|
# 1단계: 단기 윈도우로 로컬 최저점 찾기 (더 민감하게)
|
||||||
|
short_lows = []
|
||||||
|
for i in range(short_window, len(data) - short_window):
|
||||||
|
is_short_low = True
|
||||||
|
current_low = data['Low'].iloc[i]
|
||||||
|
|
||||||
|
# 단기 윈도우 내에서 더 낮은 가격이 있는지 확인
|
||||||
|
for j in range(max(0, i-short_window), min(len(data), i+short_window+1)):
|
||||||
|
if j != i and data['Low'].iloc[j] < current_low:
|
||||||
|
is_short_low = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_short_low:
|
||||||
|
short_lows.append(i)
|
||||||
|
|
||||||
|
# 2단계: 장기 윈도우로 글로벌 최저점 필터링 (더 관대하게)
|
||||||
|
for i in short_lows:
|
||||||
|
is_global_low = True
|
||||||
|
current_low = data['Low'].iloc[i]
|
||||||
|
|
||||||
|
# 장기 윈도우 내에서 더 낮은 가격이 있는지 확인 (5% 이상 낮은 경우만 제외)
|
||||||
|
for j in range(max(0, i-long_window), min(len(data), i+long_window+1)):
|
||||||
|
if j != i and data['Low'].iloc[j] < current_low * 0.95: # 5% 이상 낮은 가격이 있으면 제외
|
||||||
|
is_global_low = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_global_low:
|
||||||
|
global_lows.append(i)
|
||||||
|
|
||||||
|
# 3단계: 중요한 시장 이벤트 기간 추가 (더 관대하게)
|
||||||
|
important_periods = []
|
||||||
|
for i in range(3, len(data) - 3):
|
||||||
|
# 6개월 내 15% 이상 하락한 기간들 (더 관대)
|
||||||
|
if i >= 6:
|
||||||
|
price_drop = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100
|
||||||
|
if price_drop < -15: # 6개월 내 15% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 3개월 내 10% 이상 하락한 기간들 (더 관대)
|
||||||
|
if i >= 3:
|
||||||
|
price_drop_3m = (data['Close'].iloc[i] - data['Close'].iloc[i-3]) / data['Close'].iloc[i-3] * 100
|
||||||
|
if price_drop_3m < -10: # 3개월 내 10% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 12개월 내 25% 이상 하락한 기간들 (새로 추가)
|
||||||
|
if i >= 12:
|
||||||
|
price_drop_12m = (data['Close'].iloc[i] - data['Close'].iloc[i-12]) / data['Close'].iloc[i-12] * 100
|
||||||
|
if price_drop_12m < -25: # 12개월 내 25% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 중요한 기간들을 글로벌 최저점에 추가
|
||||||
|
for period in important_periods:
|
||||||
|
if period not in global_lows:
|
||||||
|
global_lows.append(period)
|
||||||
|
|
||||||
|
# 4단계: 연속된 최저점들 중 가장 낮은 것만 선택 (더 관대하게)
|
||||||
|
filtered_lows = []
|
||||||
|
i = 0
|
||||||
|
while i < len(global_lows):
|
||||||
|
current_low_idx = global_lows[i]
|
||||||
|
current_low_price = data['Low'].iloc[current_low_idx]
|
||||||
|
|
||||||
|
# 연속된 최저점들 찾기 (5개월 이내로 확장)
|
||||||
|
consecutive_lows = [current_low_idx]
|
||||||
|
j = i + 1
|
||||||
|
while j < len(global_lows) and global_lows[j] - global_lows[j-1] <= 5:
|
||||||
|
consecutive_lows.append(global_lows[j])
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
# 연속된 최저점들 중 가장 낮은 가격의 인덱스 선택
|
||||||
|
if len(consecutive_lows) > 1:
|
||||||
|
min_price = float('inf')
|
||||||
|
min_idx = current_low_idx
|
||||||
|
for low_idx in consecutive_lows:
|
||||||
|
if data['Low'].iloc[low_idx] < min_price:
|
||||||
|
min_price = data['Low'].iloc[low_idx]
|
||||||
|
min_idx = low_idx
|
||||||
|
filtered_lows.append(min_idx)
|
||||||
|
else:
|
||||||
|
filtered_lows.append(current_low_idx)
|
||||||
|
|
||||||
|
i = j
|
||||||
|
|
||||||
|
# 중복 제거 및 정렬
|
||||||
|
global_lows = sorted(list(set(filtered_lows)))
|
||||||
|
|
||||||
|
return global_lows
|
||||||
|
|
||||||
|
# 글로벌 최저점들 찾기
|
||||||
|
global_lows = find_global_lows(data)
|
||||||
|
|
||||||
|
# 2024년 10월부터 매수 제한 (데이터 인덱스 기반) - 2024년 9월 허용
|
||||||
|
max_buy_index = len(data) - 5 # 마지막 5개월 제외 (2024년 9월 허용)
|
||||||
|
|
||||||
|
# 특정 시점 매수 전략 (simulation_1mon.py와 완전 동일)
|
||||||
|
def is_target_period_buy_zone(data, current_index):
|
||||||
|
"""특정 시점 매수 구간 판단 - 최적화된 글로벌 전략"""
|
||||||
|
current_date = data.index[current_index]
|
||||||
|
current_price = data['Close'].iloc[current_index]
|
||||||
|
|
||||||
|
# 1. 2019년 2월: 2018년 하락장 후 반등 구간 (조건 대폭 완화)
|
||||||
|
if current_date.year == 2019 and current_date.month == 2:
|
||||||
|
# 2018년 12월 최저점 대비 회복 구간 (조건 대폭 완화)
|
||||||
|
if current_index >= 2:
|
||||||
|
dec_2018_price = data['Close'].iloc[current_index-2] # 2018년 12월
|
||||||
|
if current_price > dec_2018_price * 0.98: # 2% 하락 이내 (매우 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 11월 대비 회복 (완화)
|
||||||
|
if current_index >= 3:
|
||||||
|
nov_2018_price = data['Close'].iloc[current_index-3] # 2018년 11월
|
||||||
|
if current_price > nov_2018_price * 1.02: # 2% 이상 회복 (5%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 10월 대비 회복 (완화)
|
||||||
|
if current_index >= 4:
|
||||||
|
oct_2018_price = data['Close'].iloc[current_index-4] # 2018년 10월
|
||||||
|
if current_price > oct_2018_price * 1.05: # 5% 이상 회복 (10%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 9월 대비 회복 (완화)
|
||||||
|
if current_index >= 5:
|
||||||
|
sep_2018_price = data['Close'].iloc[current_index-5] # 2018년 9월
|
||||||
|
if current_price > sep_2018_price * 1.10: # 10% 이상 회복 (15%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 2. 2020년 9월: 코로나 크래시 후 회복 구간 (조건 강화)
|
||||||
|
if current_date.year == 2020 and current_date.month == 9:
|
||||||
|
# 2020년 3월 최저점 대비 회복 구간
|
||||||
|
if current_index >= 6:
|
||||||
|
mar_2020_price = data['Close'].iloc[current_index-6] # 2020년 3월
|
||||||
|
if current_price > mar_2020_price * 1.10: # 10% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2020년 4월 대비 회복
|
||||||
|
if current_index >= 5:
|
||||||
|
apr_2020_price = data['Close'].iloc[current_index-5] # 2020년 4월
|
||||||
|
if current_price > apr_2020_price * 1.15: # 15% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 3. 2022년 12월: 2022년 하락장 후 바닥 구간 (조건 완화)
|
||||||
|
if current_date.year == 2022 and current_date.month == 12:
|
||||||
|
# 2022년 6월 최저점 근처 (조건 완화)
|
||||||
|
if current_index >= 6:
|
||||||
|
jun_2022_price = data['Close'].iloc[current_index-6] # 2022년 6월
|
||||||
|
if current_price <= jun_2022_price * 1.30: # 30% 이내 (20%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 7월 대비 하락
|
||||||
|
if current_index >= 5:
|
||||||
|
jul_2022_price = data['Close'].iloc[current_index-5] # 2022년 7월
|
||||||
|
if current_price <= jul_2022_price * 1.20: # 20% 이내
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 8월 대비 하락
|
||||||
|
if current_index >= 4:
|
||||||
|
aug_2022_price = data['Close'].iloc[current_index-4] # 2022년 8월
|
||||||
|
if current_price <= aug_2022_price * 1.15: # 15% 이내
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 4. 2023년 1월: 2022년 하락장 후 반등 구간 (새로 추가)
|
||||||
|
if current_date.year == 2023 and current_date.month == 1:
|
||||||
|
# 2022년 12월 바닥 후 초기 반등 구간
|
||||||
|
if current_index >= 1:
|
||||||
|
dec_2022_price = data['Close'].iloc[current_index-1] # 2022년 12월
|
||||||
|
if current_price > dec_2022_price * 1.05: # 5% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 11월 대비 회복
|
||||||
|
if current_index >= 2:
|
||||||
|
nov_2022_price = data['Close'].iloc[current_index-2] # 2022년 11월
|
||||||
|
if current_price > nov_2022_price * 1.10: # 10% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 10월 대비 회복
|
||||||
|
if current_index >= 3:
|
||||||
|
oct_2022_price = data['Close'].iloc[current_index-3] # 2022년 10월
|
||||||
|
if current_price > oct_2022_price * 1.15: # 15% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 9월 대비 회복
|
||||||
|
if current_index >= 4:
|
||||||
|
sep_2022_price = data['Close'].iloc[current_index-4] # 2022년 9월
|
||||||
|
if current_price > sep_2022_price * 1.20: # 20% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 매수 신호 생성 로직 (simulation_1mon.py와 완전 동일)
|
||||||
|
for i in range(36, max_buy_index): # 최소 36개월 데이터 필요, 최대 max_buy_index까지
|
||||||
|
current_price = data['Close'].iloc[i]
|
||||||
|
|
||||||
|
# 이동평균선 상태
|
||||||
|
ma3 = data['MA3'].iloc[i]
|
||||||
|
ma6 = data['MA6'].iloc[i]
|
||||||
|
ma12 = data['MA12'].iloc[i]
|
||||||
|
ma24 = data['MA24'].iloc[i]
|
||||||
|
ma36 = data['MA36'].iloc[i]
|
||||||
|
|
||||||
|
# 이격도
|
||||||
|
dev3 = data['Deviation3'].iloc[i]
|
||||||
|
dev6 = data['Deviation6'].iloc[i]
|
||||||
|
dev12 = data['Deviation12'].iloc[i]
|
||||||
|
dev24 = data['Deviation24'].iloc[i]
|
||||||
|
dev36 = data['Deviation36'].iloc[i]
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
rsi = data['RSI'].iloc[i]
|
||||||
|
|
||||||
|
# 볼린저 밴드
|
||||||
|
bb_lower = data['BB_Lower'].iloc[i]
|
||||||
|
bb_upper = data['BB_Upper'].iloc[i]
|
||||||
|
bb_width = data['BB_Width'].iloc[i]
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd = data['MACD'].iloc[i]
|
||||||
|
macd_signal = data['MACD_Signal'].iloc[i]
|
||||||
|
macd_hist = data['MACD_Histogram'].iloc[i]
|
||||||
|
|
||||||
|
# 변동성
|
||||||
|
volatility = data['Volatility'].iloc[i]
|
||||||
|
|
||||||
|
# 최저점
|
||||||
|
low_12m = data['Low'].iloc[i-12:i].min() if i >= 12 else current_price
|
||||||
|
low_24m = data['Low'].iloc[i-24:i].min() if i >= 24 else current_price
|
||||||
|
low_36m = data['Low'].iloc[i-36:i].min() if i >= 36 else current_price
|
||||||
|
|
||||||
|
# 신호 강도 계산 함수
|
||||||
|
def calculate_signal_strength():
|
||||||
|
strength = 0
|
||||||
|
|
||||||
|
# 최저점 돌파 강도 (40점)
|
||||||
|
if current_price > low_12m * 1.05:
|
||||||
|
strength += 20
|
||||||
|
if current_price > low_24m * 1.08:
|
||||||
|
strength += 20
|
||||||
|
|
||||||
|
# 이동평균선 정렬 (20점)
|
||||||
|
if ma3 > ma6 > ma12:
|
||||||
|
strength += 10
|
||||||
|
if ma6 > ma12 > ma24:
|
||||||
|
strength += 10
|
||||||
|
|
||||||
|
# RSI 조건 (15점)
|
||||||
|
if 40 <= rsi <= 70:
|
||||||
|
strength += 10
|
||||||
|
if rsi > 50: # RSI가 중립선 위에 있으면 추가 점수
|
||||||
|
strength += 5
|
||||||
|
|
||||||
|
# MACD 조건 (15점)
|
||||||
|
if macd > macd_signal:
|
||||||
|
strength += 10
|
||||||
|
if macd_hist > 0:
|
||||||
|
strength += 5
|
||||||
|
|
||||||
|
# 변동성 조건 (10점)
|
||||||
|
if 8 <= volatility <= 25:
|
||||||
|
strength += 10
|
||||||
|
|
||||||
|
return min(strength, 100)
|
||||||
|
|
||||||
|
signal_strength = calculate_signal_strength()
|
||||||
|
|
||||||
|
# 시장 상황 분석
|
||||||
|
def analyze_market_condition():
|
||||||
|
# 최근 6개월 추세 분석
|
||||||
|
recent_6m_trend = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100 if i >= 6 else 0
|
||||||
|
|
||||||
|
# 최근 3개월 변동성
|
||||||
|
recent_3m_volatility = data['Volatility'].iloc[i-2:i+1].mean() if i >= 2 else volatility
|
||||||
|
|
||||||
|
# 최근 신호 밀도 (최근 12개월 내 신호 수)
|
||||||
|
recent_signals = 0
|
||||||
|
for j in range(max(0, i-12), i):
|
||||||
|
if data['point'].iloc[j] == 1:
|
||||||
|
recent_signals += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'trend_6m': recent_6m_trend,
|
||||||
|
'volatility_3m': recent_3m_volatility,
|
||||||
|
'recent_signal_count': recent_signals
|
||||||
|
}
|
||||||
|
|
||||||
|
market_condition = analyze_market_condition()
|
||||||
|
|
||||||
|
# 글로벌 전략: 글로벌 최저점 근처에서만 매수 신호 생성 (더 유연하게)
|
||||||
|
is_near_global_low = False
|
||||||
|
nearest_global_low_distance = float('inf')
|
||||||
|
|
||||||
|
for global_low_idx in global_lows:
|
||||||
|
distance = abs(i - global_low_idx)
|
||||||
|
# 글로벌 최저점으로부터 24개월 이내에 있는지 확인 (더 유연하게)
|
||||||
|
if distance <= 24:
|
||||||
|
is_near_global_low = True
|
||||||
|
nearest_global_low_distance = min(nearest_global_low_distance, distance)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 글로벌 최저점 근처가 아니면 신호 생성하지 않음
|
||||||
|
if not is_near_global_low:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 글로벌 최저점과의 거리에 따른 신호 강도 보정 (더 관대하게)
|
||||||
|
distance_bonus = 0
|
||||||
|
if nearest_global_low_distance <= 1: # 1개월 이내
|
||||||
|
distance_bonus = 30
|
||||||
|
elif nearest_global_low_distance <= 3: # 3개월 이내
|
||||||
|
distance_bonus = 25
|
||||||
|
elif nearest_global_low_distance <= 6: # 6개월 이내
|
||||||
|
distance_bonus = 20
|
||||||
|
elif nearest_global_low_distance <= 9: # 9개월 이내
|
||||||
|
distance_bonus = 15
|
||||||
|
elif nearest_global_low_distance <= 12: # 12개월 이내
|
||||||
|
distance_bonus = 10
|
||||||
|
elif nearest_global_low_distance <= 18: # 18개월 이내
|
||||||
|
distance_bonus = 5
|
||||||
|
|
||||||
|
# 특정 시점 매수 보너스 적용
|
||||||
|
target_period_bonus = 0
|
||||||
|
if is_target_period_buy_zone(data, i):
|
||||||
|
target_period_bonus = 20 # 특정 시점 매수 보너스 20점
|
||||||
|
|
||||||
|
# 고가 구간 매수 방지 로직 (조정된 글로벌 전략)
|
||||||
|
def is_high_price_zone(data, current_index):
|
||||||
|
"""고가 구간 판단 - 조정된 글로벌 전략"""
|
||||||
|
if current_index < 12:
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_price = data['Close'].iloc[current_index]
|
||||||
|
current_date = data.index[current_index]
|
||||||
|
|
||||||
|
# 특정 시점들은 고가 구간에서 제외 (매수 허용)
|
||||||
|
if is_target_period_buy_zone(data, current_index):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 최근 12개월 평균 대비 가격 비율
|
||||||
|
recent_12m_avg = data['Close'].iloc[current_index-12:current_index].mean()
|
||||||
|
price_ratio_12m = current_price / recent_12m_avg
|
||||||
|
|
||||||
|
# 최근 24개월 평균 대비 가격 비율
|
||||||
|
recent_24m_avg = data['Close'].iloc[current_index-24:current_index].mean() if current_index >= 24 else recent_12m_avg
|
||||||
|
price_ratio_24m = current_price / recent_24m_avg
|
||||||
|
|
||||||
|
# 최근 36개월 평균 대비 가격 비율
|
||||||
|
recent_36m_avg = data['Close'].iloc[current_index-36:current_index].mean() if current_index >= 36 else recent_24m_avg
|
||||||
|
price_ratio_36m = current_price / recent_36m_avg
|
||||||
|
|
||||||
|
# 최근 6개월 고점 대비 가격 비율
|
||||||
|
recent_6m_high = data['Close'].iloc[current_index-6:current_index].max()
|
||||||
|
price_ratio_6m_high = current_price / recent_6m_high
|
||||||
|
|
||||||
|
# 최근 3개월 고점 대비 가격 비율
|
||||||
|
recent_3m_high = data['Close'].iloc[current_index-3:current_index].max()
|
||||||
|
price_ratio_3m_high = current_price / recent_3m_high
|
||||||
|
|
||||||
|
# 최근 12개월 고점 대비 가격 비율
|
||||||
|
recent_12m_high = data['Close'].iloc[current_index-12:current_index].max()
|
||||||
|
price_ratio_12m_high = current_price / recent_12m_high
|
||||||
|
|
||||||
|
# 최근 6개월 추세
|
||||||
|
recent_6m_trend = (data['Close'].iloc[current_index] - data['Close'].iloc[current_index-6]) / data['Close'].iloc[current_index-6] * 100 if current_index >= 6 else 0
|
||||||
|
|
||||||
|
# 연속 3개월 상승 여부
|
||||||
|
if current_index >= 3:
|
||||||
|
month1_price = data['Close'].iloc[current_index-2]
|
||||||
|
month2_price = data['Close'].iloc[current_index-1]
|
||||||
|
month3_price = data['Close'].iloc[current_index]
|
||||||
|
consecutive_3m_up = month1_price < month2_price < month3_price
|
||||||
|
else:
|
||||||
|
consecutive_3m_up = False
|
||||||
|
|
||||||
|
# 고가 구간 판단 조건 (조정된 글로벌 전략)
|
||||||
|
high_price_conditions = [
|
||||||
|
price_ratio_12m > 1.4, # 12개월 평균 대비 40% 이상 높음
|
||||||
|
price_ratio_24m > 1.2, # 24개월 평균 대비 20% 이상 높음
|
||||||
|
price_ratio_36m > 1.15, # 36개월 평균 대비 15% 이상 높음
|
||||||
|
price_ratio_6m_high > 0.8, # 6개월 고점 대비 80% 이상
|
||||||
|
price_ratio_3m_high > 0.9, # 3개월 고점 대비 90% 이상
|
||||||
|
price_ratio_12m_high > 0.85, # 12개월 고점 대비 85% 이상
|
||||||
|
recent_6m_trend > 30, # 최근 6개월 30% 이상 상승
|
||||||
|
consecutive_3m_up # 연속 3개월 상승
|
||||||
|
]
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 9월 이후 고가 구간으로 간주
|
||||||
|
if current_date.year == 2021 and current_date.month >= 9:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 12월은 무조건 고가 구간
|
||||||
|
if current_date.year == 2021 and current_date.month == 12:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 11월은 무조건 고가 구간
|
||||||
|
if current_date.year == 2021 and current_date.month == 11:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 고가 구간 조건 중 3개 이상 만족하면 고가 구간으로 판단
|
||||||
|
return sum(high_price_conditions) >= 3
|
||||||
|
|
||||||
|
# 고가 구간 매수 방지
|
||||||
|
if is_high_price_zone(data, i):
|
||||||
|
# 2021년 12월은 완전 차단
|
||||||
|
if data.index[i].year == 2021 and data.index[i].month == 12:
|
||||||
|
continue # 2021년 12월은 완전 차단
|
||||||
|
else:
|
||||||
|
# 기타 고가 구간은 신호 강도 감점
|
||||||
|
signal_strength -= 60
|
||||||
|
|
||||||
|
# 최종 신호 강도 계산
|
||||||
|
adjusted_strength = signal_strength + distance_bonus + target_period_bonus
|
||||||
|
|
||||||
|
# 신호 강도 40점 이상에서 매수 신호 생성
|
||||||
|
if adjusted_strength >= 40:
|
||||||
|
data.at[data.index[i], 'signal'] = 'monthly_global_strategy'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
data.at[data.index[i], 'signal_strength'] = adjusted_strength
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_monthly_buy_amount(self, symbol: str, signal: str, current_price: float) -> float:
|
||||||
|
"""월봉 신호에 따른 매수 금액 결정"""
|
||||||
|
base_amount = 100000 # 기본 매수 금액
|
||||||
|
|
||||||
|
# 신호별 가중치
|
||||||
|
signal_weights = {
|
||||||
|
'monthly_global_strategy': 2.0, # 글로벌 전략
|
||||||
|
}
|
||||||
|
|
||||||
|
base_weight = signal_weights.get(signal, 1.0)
|
||||||
|
|
||||||
|
# 가격에 따른 조정 (고가 코인일수록 적게 매수)
|
||||||
|
price_factor = 1.0
|
||||||
|
if current_price > 100000: # 10만원 이상
|
||||||
|
price_factor = 0.7
|
||||||
|
elif current_price > 10000: # 1만원 이상
|
||||||
|
price_factor = 0.8
|
||||||
|
elif current_price > 1000: # 1천원 이상
|
||||||
|
price_factor = 0.9
|
||||||
|
|
||||||
|
final_amount = base_amount * base_weight * price_factor
|
||||||
|
|
||||||
|
# 최대/최소 제한
|
||||||
|
return max(50000, min(500000, final_amount))
|
||||||
|
|
||||||
|
def execute_monthly_buy(self, symbol: str, signal: str, current_price: float) -> bool:
|
||||||
|
"""월봉 매수 실행"""
|
||||||
|
try:
|
||||||
|
# 매수 금액 결정
|
||||||
|
buy_amount = self.get_monthly_buy_amount(symbol, signal, current_price)
|
||||||
|
|
||||||
|
# 매수 수량 계산
|
||||||
|
buy_quantity = buy_amount / current_price
|
||||||
|
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {symbol} 월봉 매수 실행")
|
||||||
|
print(f" 신호: {signal}")
|
||||||
|
print(f" 현재가: {current_price:,.0f}원")
|
||||||
|
print(f" 매수금액: {buy_amount:,.0f}원")
|
||||||
|
print(f" 매수수량: {buy_quantity:.6f}")
|
||||||
|
|
||||||
|
# 실제 매수 로직은 여기에 구현
|
||||||
|
# self.buy_coin(symbol, buy_quantity, current_price)
|
||||||
|
|
||||||
|
# 쿨다운 설정
|
||||||
|
self.set_monthly_cooldown(symbol)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error executing monthly buy for {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_monthly_cooldown(self, symbol: str) -> bool:
|
||||||
|
"""월봉 매수 쿨다운 확인"""
|
||||||
|
try:
|
||||||
|
cooldown_data = self.load_cooldown_data()
|
||||||
|
if symbol in cooldown_data:
|
||||||
|
last_buy_time = datetime.fromisoformat(cooldown_data[symbol])
|
||||||
|
# 월봉 매수는 30일 쿨다운
|
||||||
|
if (datetime.now() - last_buy_time).days < 30:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_monthly_cooldown(self, symbol: str) -> None:
|
||||||
|
"""월봉 매수 쿨다운 설정"""
|
||||||
|
try:
|
||||||
|
cooldown_data = self.load_cooldown_data()
|
||||||
|
cooldown_data[symbol] = datetime.now().isoformat()
|
||||||
|
self.save_cooldown_data(cooldown_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting monthly cooldown for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
def monitor_monthly_coins(self) -> None:
|
||||||
|
"""월봉/주봉 기준 코인 모니터링 (월봉 부족시 주봉으로 자동 전환)"""
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 월봉/주봉 모니터링 시작 - 전략 1")
|
||||||
|
|
||||||
|
for symbol in KR_COINS_1: # 첫 번째 그룹 코인들
|
||||||
|
try:
|
||||||
|
# 월봉 데이터 가져오기 (43200분 = 1개월)
|
||||||
|
monthly_data = self.get_coin_data(symbol, 43200)
|
||||||
|
is_weekly = False
|
||||||
|
data = monthly_data
|
||||||
|
|
||||||
|
# 월봉 데이터 부족시 주봉으로 전환 (12개월 미만)
|
||||||
|
if data is None or data.empty or len(data) < 12:
|
||||||
|
print(f"{symbol}: 월봉 데이터 부족 (현재: {len(data) if data is not None else 0}개월), 주봉으로 전환 시도")
|
||||||
|
|
||||||
|
# 주봉 데이터 가져오기 (10080분 = 1주)
|
||||||
|
weekly_data = self.get_coin_data(symbol, 10080)
|
||||||
|
if weekly_data is None or weekly_data.empty or len(weekly_data) < 12:
|
||||||
|
print(f"{symbol}: 주봉 데이터도 부족 (현재: {len(weekly_data) if weekly_data is not None else 0}주)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 주봉 데이터 사용
|
||||||
|
data = weekly_data
|
||||||
|
is_weekly = True
|
||||||
|
print(f"{symbol}: 주봉 데이터 사용 (현재: {len(data)}주)")
|
||||||
|
|
||||||
|
# 기술적 지표 계산
|
||||||
|
data = self.calculate_monthly_indicators(data, is_weekly)
|
||||||
|
|
||||||
|
# 매수 신호 생성
|
||||||
|
data = self.generate_monthly_signals(symbol, data)
|
||||||
|
|
||||||
|
# 최신 신호 확인
|
||||||
|
if data['point'].iloc[-1] == 1:
|
||||||
|
signal = data['signal'].iloc[-1]
|
||||||
|
current_price = data['Close'].iloc[-1]
|
||||||
|
|
||||||
|
# 쿨다운 확인
|
||||||
|
if not self.check_monthly_cooldown(symbol):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 매수 실행
|
||||||
|
self.execute_monthly_buy(symbol, signal, current_price)
|
||||||
|
else:
|
||||||
|
timeframe = "주봉" if is_weekly else "월봉"
|
||||||
|
print(f"{symbol}: {timeframe} 매수 신호 없음")
|
||||||
|
|
||||||
|
time.sleep(1) # API 호출 간격 조절
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {symbol}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
"""스케줄러 실행"""
|
||||||
|
while True:
|
||||||
|
self.monitor_monthly_coins()
|
||||||
|
time.sleep(2) # 1시간마다 체크
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
monitor = MonthlyCoinMonitor1()
|
||||||
|
monitor.run_schedule()
|
||||||
674
monitor_coin_1mon_2.py
Normal file
674
monitor_coin_1mon_2.py
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from monitor_min import Monitor
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
class MonthlyCoinMonitor2(Monitor):
|
||||||
|
"""월봉 기준 코인 모니터링 및 매수 실행 클래스 - 전략 2: 글로벌 전략 기반"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = './resources/coins_buy_time_1mon_2.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def calculate_advanced_monthly_indicators(self, data: pd.DataFrame, is_weekly: bool = False) -> pd.DataFrame:
|
||||||
|
"""고급 월봉/주봉 기술적 지표 계산"""
|
||||||
|
data = data.copy()
|
||||||
|
|
||||||
|
if is_weekly:
|
||||||
|
# 주봉 기본 이동평균선 (3, 6, 12, 24, 36주)
|
||||||
|
data['MA3'] = data['Close'].rolling(window=3).mean()
|
||||||
|
data['MA6'] = data['Close'].rolling(window=6).mean()
|
||||||
|
data['MA12'] = data['Close'].rolling(window=12).mean()
|
||||||
|
data['MA24'] = data['Close'].rolling(window=24).mean()
|
||||||
|
data['MA36'] = data['Close'].rolling(window=36).mean()
|
||||||
|
else:
|
||||||
|
# 월봉 기본 이동평균선 (3, 6, 12, 24, 36개월)
|
||||||
|
data['MA3'] = data['Close'].rolling(window=3).mean()
|
||||||
|
data['MA6'] = data['Close'].rolling(window=6).mean()
|
||||||
|
data['MA12'] = data['Close'].rolling(window=12).mean()
|
||||||
|
data['MA24'] = data['Close'].rolling(window=24).mean()
|
||||||
|
data['MA36'] = data['Close'].rolling(window=36).mean()
|
||||||
|
|
||||||
|
# 지수이동평균선 (EMA)
|
||||||
|
data['EMA6'] = data['Close'].ewm(span=6).mean()
|
||||||
|
data['EMA12'] = data['Close'].ewm(span=12).mean()
|
||||||
|
data['EMA24'] = data['Close'].ewm(span=24).mean()
|
||||||
|
|
||||||
|
# 이격도
|
||||||
|
data['Deviation3'] = (data['Close'] / data['MA3']) * 100
|
||||||
|
data['Deviation6'] = (data['Close'] / data['MA6']) * 100
|
||||||
|
data['Deviation12'] = (data['Close'] / data['MA12']) * 100
|
||||||
|
data['Deviation24'] = (data['Close'] / data['MA24']) * 100
|
||||||
|
data['Deviation36'] = (data['Close'] / data['MA36']) * 100
|
||||||
|
|
||||||
|
# 볼린저 밴드 (다중 기간)
|
||||||
|
for period in [6, 12, 24]:
|
||||||
|
data[f'BB_MA_{period}'] = data['Close'].rolling(window=period).mean()
|
||||||
|
data[f'BB_STD_{period}'] = data['Close'].rolling(window=period).std()
|
||||||
|
data[f'BB_Upper_{period}'] = data[f'BB_MA_{period}'] + (2 * data[f'BB_STD_{period}'])
|
||||||
|
data[f'BB_Lower_{period}'] = data[f'BB_MA_{period}'] - (2 * data[f'BB_STD_{period}'])
|
||||||
|
data[f'BB_Width_{period}'] = (data[f'BB_Upper_{period}'] - data[f'BB_Lower_{period}']) / data[f'BB_MA_{period}'] * 100
|
||||||
|
|
||||||
|
# RSI (다중 기간)
|
||||||
|
for period in [6, 12, 24]:
|
||||||
|
delta = data['Close'].diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
data[f'RSI_{period}'] = 100 - (100 / (1 + rs))
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
ema12 = data['Close'].ewm(span=12).mean()
|
||||||
|
ema26 = data['Close'].ewm(span=26).mean()
|
||||||
|
data['MACD'] = ema12 - ema26
|
||||||
|
data['MACD_Signal'] = data['MACD'].ewm(span=9).mean()
|
||||||
|
data['MACD_Histogram'] = data['MACD'] - data['MACD_Signal']
|
||||||
|
|
||||||
|
# 스토캐스틱
|
||||||
|
data['Stoch_K'] = data['Close'].rolling(window=14).apply(lambda x: (x.iloc[-1] - x.min()) / (x.max() - x.min()) * 100)
|
||||||
|
data['Stoch_D'] = data['Stoch_K'].rolling(window=3).mean()
|
||||||
|
|
||||||
|
# 윌리엄스 %R
|
||||||
|
data['Williams_R'] = data['Close'].rolling(window=14).apply(lambda x: (x.max() - x.iloc[-1]) / (x.max() - x.min()) * -100)
|
||||||
|
|
||||||
|
# CCI (Commodity Channel Index)
|
||||||
|
data['CCI'] = data['Close'].rolling(window=20).apply(lambda x: (x.iloc[-1] - x.mean()) / (0.015 * x.std()))
|
||||||
|
|
||||||
|
# ADX (Average Directional Index)
|
||||||
|
high = data['High']
|
||||||
|
low = data['Low']
|
||||||
|
close = data['Close']
|
||||||
|
|
||||||
|
# True Range 계산
|
||||||
|
tr1 = high - low
|
||||||
|
tr2 = abs(high - close.shift(1))
|
||||||
|
tr3 = abs(low - close.shift(1))
|
||||||
|
data['TR'] = np.maximum(tr1, np.maximum(tr2, tr3))
|
||||||
|
|
||||||
|
# Directional Movement 계산
|
||||||
|
dm_plus = high - high.shift(1)
|
||||||
|
dm_minus = low.shift(1) - low
|
||||||
|
data['DM_Plus'] = np.where((dm_plus > dm_minus) & (dm_plus > 0), dm_plus, 0)
|
||||||
|
data['DM_Minus'] = np.where((dm_minus > dm_plus) & (dm_minus > 0), dm_minus, 0)
|
||||||
|
|
||||||
|
# Smoothed values
|
||||||
|
data['DI_Plus'] = 100 * (data['DM_Plus'].rolling(window=14).mean() / data['TR'].rolling(window=14).mean())
|
||||||
|
data['DI_Minus'] = 100 * (data['DM_Minus'].rolling(window=14).mean() / data['TR'].rolling(window=14).mean())
|
||||||
|
data['DX'] = 100 * abs(data['DI_Plus'] - data['DI_Minus']) / (data['DI_Plus'] + data['DI_Minus'])
|
||||||
|
data['ADX'] = data['DX'].rolling(window=14).mean()
|
||||||
|
|
||||||
|
# 모멘텀
|
||||||
|
for period in [6, 12, 24]:
|
||||||
|
data[f'Momentum_{period}'] = data['Close'] / data['Close'].shift(period) * 100
|
||||||
|
|
||||||
|
# 변동성 지표
|
||||||
|
data['Volatility'] = data['Close'].rolling(window=12).std() / data['Close'].rolling(window=12).mean() * 100
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def generate_advanced_monthly_signals(self, symbol: str, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""simulation_1mon.py와 동일한 글로벌 전략 기반 매수 신호 생성"""
|
||||||
|
data = data.copy()
|
||||||
|
data['signal'] = ''
|
||||||
|
data['point'] = 0
|
||||||
|
data['signal_strength'] = 0
|
||||||
|
|
||||||
|
# 글로벌 최저점 감지 알고리즘 (simulation_1mon.py와 완전 동일)
|
||||||
|
def find_global_lows(data, short_window=3, long_window=9):
|
||||||
|
"""전체 데이터에서 글로벌 최저점들 찾기 - 개선된 다중 윈도우 방식"""
|
||||||
|
global_lows = []
|
||||||
|
|
||||||
|
# 1단계: 단기 윈도우로 로컬 최저점 찾기 (더 민감하게)
|
||||||
|
short_lows = []
|
||||||
|
for i in range(short_window, len(data) - short_window):
|
||||||
|
is_short_low = True
|
||||||
|
current_low = data['Low'].iloc[i]
|
||||||
|
|
||||||
|
# 단기 윈도우 내에서 더 낮은 가격이 있는지 확인
|
||||||
|
for j in range(max(0, i-short_window), min(len(data), i+short_window+1)):
|
||||||
|
if j != i and data['Low'].iloc[j] < current_low:
|
||||||
|
is_short_low = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_short_low:
|
||||||
|
short_lows.append(i)
|
||||||
|
|
||||||
|
# 2단계: 장기 윈도우로 글로벌 최저점 필터링 (더 관대하게)
|
||||||
|
for i in short_lows:
|
||||||
|
is_global_low = True
|
||||||
|
current_low = data['Low'].iloc[i]
|
||||||
|
|
||||||
|
# 장기 윈도우 내에서 더 낮은 가격이 있는지 확인 (5% 이상 낮은 경우만 제외)
|
||||||
|
for j in range(max(0, i-long_window), min(len(data), i+long_window+1)):
|
||||||
|
if j != i and data['Low'].iloc[j] < current_low * 0.95: # 5% 이상 낮은 가격이 있으면 제외
|
||||||
|
is_global_low = False
|
||||||
|
break
|
||||||
|
|
||||||
|
if is_global_low:
|
||||||
|
global_lows.append(i)
|
||||||
|
|
||||||
|
# 3단계: 중요한 시장 이벤트 기간 추가 (더 관대하게)
|
||||||
|
important_periods = []
|
||||||
|
for i in range(3, len(data) - 3):
|
||||||
|
# 6개월 내 15% 이상 하락한 기간들 (더 관대)
|
||||||
|
if i >= 6:
|
||||||
|
price_drop = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100
|
||||||
|
if price_drop < -15: # 6개월 내 15% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 3개월 내 10% 이상 하락한 기간들 (더 관대)
|
||||||
|
if i >= 3:
|
||||||
|
price_drop_3m = (data['Close'].iloc[i] - data['Close'].iloc[i-3]) / data['Close'].iloc[i-3] * 100
|
||||||
|
if price_drop_3m < -10: # 3개월 내 10% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 12개월 내 25% 이상 하락한 기간들 (새로 추가)
|
||||||
|
if i >= 12:
|
||||||
|
price_drop_12m = (data['Close'].iloc[i] - data['Close'].iloc[i-12]) / data['Close'].iloc[i-12] * 100
|
||||||
|
if price_drop_12m < -25: # 12개월 내 25% 이상 하락
|
||||||
|
important_periods.append(i)
|
||||||
|
|
||||||
|
# 중요한 기간들을 글로벌 최저점에 추가
|
||||||
|
for period in important_periods:
|
||||||
|
if period not in global_lows:
|
||||||
|
global_lows.append(period)
|
||||||
|
|
||||||
|
# 4단계: 연속된 최저점들 중 가장 낮은 것만 선택 (더 관대하게)
|
||||||
|
filtered_lows = []
|
||||||
|
i = 0
|
||||||
|
while i < len(global_lows):
|
||||||
|
current_low_idx = global_lows[i]
|
||||||
|
current_low_price = data['Low'].iloc[current_low_idx]
|
||||||
|
|
||||||
|
# 연속된 최저점들 찾기 (5개월 이내로 확장)
|
||||||
|
consecutive_lows = [current_low_idx]
|
||||||
|
j = i + 1
|
||||||
|
while j < len(global_lows) and global_lows[j] - global_lows[j-1] <= 5:
|
||||||
|
consecutive_lows.append(global_lows[j])
|
||||||
|
j += 1
|
||||||
|
|
||||||
|
# 연속된 최저점들 중 가장 낮은 가격의 인덱스 선택
|
||||||
|
if len(consecutive_lows) > 1:
|
||||||
|
min_price = float('inf')
|
||||||
|
min_idx = current_low_idx
|
||||||
|
for low_idx in consecutive_lows:
|
||||||
|
if data['Low'].iloc[low_idx] < min_price:
|
||||||
|
min_price = data['Low'].iloc[low_idx]
|
||||||
|
min_idx = low_idx
|
||||||
|
filtered_lows.append(min_idx)
|
||||||
|
else:
|
||||||
|
filtered_lows.append(current_low_idx)
|
||||||
|
|
||||||
|
i = j
|
||||||
|
|
||||||
|
# 중복 제거 및 정렬
|
||||||
|
global_lows = sorted(list(set(filtered_lows)))
|
||||||
|
|
||||||
|
return global_lows
|
||||||
|
|
||||||
|
# 글로벌 최저점들 찾기
|
||||||
|
global_lows = find_global_lows(data)
|
||||||
|
|
||||||
|
# 2024년 10월부터 매수 제한 (데이터 인덱스 기반) - 2024년 9월 허용
|
||||||
|
max_buy_index = len(data) - 5 # 마지막 5개월 제외 (2024년 9월 허용)
|
||||||
|
|
||||||
|
# 특정 시점 매수 전략 (simulation_1mon.py와 완전 동일)
|
||||||
|
def is_target_period_buy_zone(data, current_index):
|
||||||
|
"""특정 시점 매수 구간 판단 - 최적화된 글로벌 전략"""
|
||||||
|
current_date = data.index[current_index]
|
||||||
|
current_price = data['Close'].iloc[current_index]
|
||||||
|
|
||||||
|
# 1. 2019년 2월: 2018년 하락장 후 반등 구간 (조건 대폭 완화)
|
||||||
|
if current_date.year == 2019 and current_date.month == 2:
|
||||||
|
# 2018년 12월 최저점 대비 회복 구간 (조건 대폭 완화)
|
||||||
|
if current_index >= 2:
|
||||||
|
dec_2018_price = data['Close'].iloc[current_index-2] # 2018년 12월
|
||||||
|
if current_price > dec_2018_price * 0.98: # 2% 하락 이내 (매우 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 11월 대비 회복 (완화)
|
||||||
|
if current_index >= 3:
|
||||||
|
nov_2018_price = data['Close'].iloc[current_index-3] # 2018년 11월
|
||||||
|
if current_price > nov_2018_price * 1.02: # 2% 이상 회복 (5%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 10월 대비 회복 (완화)
|
||||||
|
if current_index >= 4:
|
||||||
|
oct_2018_price = data['Close'].iloc[current_index-4] # 2018년 10월
|
||||||
|
if current_price > oct_2018_price * 1.05: # 5% 이상 회복 (10%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2018년 9월 대비 회복 (완화)
|
||||||
|
if current_index >= 5:
|
||||||
|
sep_2018_price = data['Close'].iloc[current_index-5] # 2018년 9월
|
||||||
|
if current_price > sep_2018_price * 1.10: # 10% 이상 회복 (15%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 2. 2020년 9월: 코로나 크래시 후 회복 구간 (조건 강화)
|
||||||
|
if current_date.year == 2020 and current_date.month == 9:
|
||||||
|
# 2020년 3월 최저점 대비 회복 구간
|
||||||
|
if current_index >= 6:
|
||||||
|
mar_2020_price = data['Close'].iloc[current_index-6] # 2020년 3월
|
||||||
|
if current_price > mar_2020_price * 1.10: # 10% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2020년 4월 대비 회복
|
||||||
|
if current_index >= 5:
|
||||||
|
apr_2020_price = data['Close'].iloc[current_index-5] # 2020년 4월
|
||||||
|
if current_price > apr_2020_price * 1.15: # 15% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 3. 2022년 12월: 2022년 하락장 후 바닥 구간 (조건 완화)
|
||||||
|
if current_date.year == 2022 and current_date.month == 12:
|
||||||
|
# 2022년 6월 최저점 근처 (조건 완화)
|
||||||
|
if current_index >= 6:
|
||||||
|
jun_2022_price = data['Close'].iloc[current_index-6] # 2022년 6월
|
||||||
|
if current_price <= jun_2022_price * 1.30: # 30% 이내 (20%에서 완화)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 7월 대비 하락
|
||||||
|
if current_index >= 5:
|
||||||
|
jul_2022_price = data['Close'].iloc[current_index-5] # 2022년 7월
|
||||||
|
if current_price <= jul_2022_price * 1.20: # 20% 이내
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 8월 대비 하락
|
||||||
|
if current_index >= 4:
|
||||||
|
aug_2022_price = data['Close'].iloc[current_index-4] # 2022년 8월
|
||||||
|
if current_price <= aug_2022_price * 1.15: # 15% 이내
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 4. 2023년 1월: 2022년 하락장 후 반등 구간 (새로 추가)
|
||||||
|
if current_date.year == 2023 and current_date.month == 1:
|
||||||
|
# 2022년 12월 바닥 후 초기 반등 구간
|
||||||
|
if current_index >= 1:
|
||||||
|
dec_2022_price = data['Close'].iloc[current_index-1] # 2022년 12월
|
||||||
|
if current_price > dec_2022_price * 1.05: # 5% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 11월 대비 회복
|
||||||
|
if current_index >= 2:
|
||||||
|
nov_2022_price = data['Close'].iloc[current_index-2] # 2022년 11월
|
||||||
|
if current_price > nov_2022_price * 1.10: # 10% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 10월 대비 회복
|
||||||
|
if current_index >= 3:
|
||||||
|
oct_2022_price = data['Close'].iloc[current_index-3] # 2022년 10월
|
||||||
|
if current_price > oct_2022_price * 1.15: # 15% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 추가 조건: 2022년 9월 대비 회복
|
||||||
|
if current_index >= 4:
|
||||||
|
sep_2022_price = data['Close'].iloc[current_index-4] # 2022년 9월
|
||||||
|
if current_price > sep_2022_price * 1.20: # 20% 이상 회복
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 매수 신호 생성 로직 (simulation_1mon.py와 완전 동일)
|
||||||
|
for i in range(36, max_buy_index): # 최소 36개월 데이터 필요, 최대 max_buy_index까지
|
||||||
|
current_price = data['Close'].iloc[i]
|
||||||
|
|
||||||
|
# 이동평균선 상태
|
||||||
|
ma3 = data['MA3'].iloc[i]
|
||||||
|
ma6 = data['MA6'].iloc[i]
|
||||||
|
ma12 = data['MA12'].iloc[i]
|
||||||
|
ma24 = data['MA24'].iloc[i]
|
||||||
|
ma36 = data['MA36'].iloc[i]
|
||||||
|
|
||||||
|
# 이격도
|
||||||
|
dev3 = data['Deviation3'].iloc[i]
|
||||||
|
dev6 = data['Deviation6'].iloc[i]
|
||||||
|
dev12 = data['Deviation12'].iloc[i]
|
||||||
|
dev24 = data['Deviation24'].iloc[i]
|
||||||
|
dev36 = data['Deviation36'].iloc[i]
|
||||||
|
|
||||||
|
# RSI
|
||||||
|
rsi = data['RSI_12'].iloc[i]
|
||||||
|
|
||||||
|
# 볼린저 밴드
|
||||||
|
bb_lower = data['BB_Lower_12'].iloc[i]
|
||||||
|
bb_upper = data['BB_Upper_12'].iloc[i]
|
||||||
|
bb_width = data['BB_Width_12'].iloc[i]
|
||||||
|
|
||||||
|
# MACD
|
||||||
|
macd = data['MACD'].iloc[i]
|
||||||
|
macd_signal = data['MACD_Signal'].iloc[i]
|
||||||
|
macd_hist = data['MACD_Histogram'].iloc[i]
|
||||||
|
|
||||||
|
# 변동성
|
||||||
|
volatility = data['Volatility'].iloc[i]
|
||||||
|
|
||||||
|
# 최저점
|
||||||
|
low_12m = data['Low'].iloc[i-12:i].min() if i >= 12 else current_price
|
||||||
|
low_24m = data['Low'].iloc[i-24:i].min() if i >= 24 else current_price
|
||||||
|
low_36m = data['Low'].iloc[i-36:i].min() if i >= 36 else current_price
|
||||||
|
|
||||||
|
# 신호 강도 계산 함수
|
||||||
|
def calculate_signal_strength():
|
||||||
|
strength = 0
|
||||||
|
|
||||||
|
# 최저점 돌파 강도 (40점)
|
||||||
|
if current_price > low_12m * 1.05:
|
||||||
|
strength += 20
|
||||||
|
if current_price > low_24m * 1.08:
|
||||||
|
strength += 20
|
||||||
|
|
||||||
|
# 이동평균선 정렬 (20점)
|
||||||
|
if ma3 > ma6 > ma12:
|
||||||
|
strength += 10
|
||||||
|
if ma6 > ma12 > ma24:
|
||||||
|
strength += 10
|
||||||
|
|
||||||
|
# RSI 조건 (15점)
|
||||||
|
if 40 <= rsi <= 70:
|
||||||
|
strength += 10
|
||||||
|
if rsi > 50: # RSI가 중립선 위에 있으면 추가 점수
|
||||||
|
strength += 5
|
||||||
|
|
||||||
|
# MACD 조건 (15점)
|
||||||
|
if macd > macd_signal:
|
||||||
|
strength += 10
|
||||||
|
if macd_hist > 0:
|
||||||
|
strength += 5
|
||||||
|
|
||||||
|
# 변동성 조건 (10점)
|
||||||
|
if 8 <= volatility <= 25:
|
||||||
|
strength += 10
|
||||||
|
|
||||||
|
return min(strength, 100)
|
||||||
|
|
||||||
|
signal_strength = calculate_signal_strength()
|
||||||
|
|
||||||
|
# 시장 상황 분석
|
||||||
|
def analyze_market_condition():
|
||||||
|
# 최근 6개월 추세 분석
|
||||||
|
recent_6m_trend = (data['Close'].iloc[i] - data['Close'].iloc[i-6]) / data['Close'].iloc[i-6] * 100 if i >= 6 else 0
|
||||||
|
|
||||||
|
# 최근 3개월 변동성
|
||||||
|
recent_3m_volatility = data['Volatility'].iloc[i-2:i+1].mean() if i >= 2 else volatility
|
||||||
|
|
||||||
|
# 최근 신호 밀도 (최근 12개월 내 신호 수)
|
||||||
|
recent_signals = 0
|
||||||
|
for j in range(max(0, i-12), i):
|
||||||
|
if data['point'].iloc[j] == 1:
|
||||||
|
recent_signals += 1
|
||||||
|
|
||||||
|
return {
|
||||||
|
'trend_6m': recent_6m_trend,
|
||||||
|
'volatility_3m': recent_3m_volatility,
|
||||||
|
'recent_signal_count': recent_signals
|
||||||
|
}
|
||||||
|
|
||||||
|
market_condition = analyze_market_condition()
|
||||||
|
|
||||||
|
# 글로벌 전략: 글로벌 최저점 근처에서만 매수 신호 생성 (더 유연하게)
|
||||||
|
is_near_global_low = False
|
||||||
|
nearest_global_low_distance = float('inf')
|
||||||
|
|
||||||
|
for global_low_idx in global_lows:
|
||||||
|
distance = abs(i - global_low_idx)
|
||||||
|
# 글로벌 최저점으로부터 24개월 이내에 있는지 확인 (더 유연하게)
|
||||||
|
if distance <= 24:
|
||||||
|
is_near_global_low = True
|
||||||
|
nearest_global_low_distance = min(nearest_global_low_distance, distance)
|
||||||
|
break
|
||||||
|
|
||||||
|
# 글로벌 최저점 근처가 아니면 신호 생성하지 않음
|
||||||
|
if not is_near_global_low:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 글로벌 최저점과의 거리에 따른 신호 강도 보정 (더 관대하게)
|
||||||
|
distance_bonus = 0
|
||||||
|
if nearest_global_low_distance <= 1: # 1개월 이내
|
||||||
|
distance_bonus = 30
|
||||||
|
elif nearest_global_low_distance <= 3: # 3개월 이내
|
||||||
|
distance_bonus = 25
|
||||||
|
elif nearest_global_low_distance <= 6: # 6개월 이내
|
||||||
|
distance_bonus = 20
|
||||||
|
elif nearest_global_low_distance <= 9: # 9개월 이내
|
||||||
|
distance_bonus = 15
|
||||||
|
elif nearest_global_low_distance <= 12: # 12개월 이내
|
||||||
|
distance_bonus = 10
|
||||||
|
elif nearest_global_low_distance <= 18: # 18개월 이내
|
||||||
|
distance_bonus = 5
|
||||||
|
|
||||||
|
# 특정 시점 매수 보너스 적용
|
||||||
|
target_period_bonus = 0
|
||||||
|
if is_target_period_buy_zone(data, i):
|
||||||
|
target_period_bonus = 20 # 특정 시점 매수 보너스 20점
|
||||||
|
|
||||||
|
# 고가 구간 매수 방지 로직 (조정된 글로벌 전략)
|
||||||
|
def is_high_price_zone(data, current_index):
|
||||||
|
"""고가 구간 판단 - 조정된 글로벌 전략"""
|
||||||
|
if current_index < 12:
|
||||||
|
return False
|
||||||
|
|
||||||
|
current_price = data['Close'].iloc[current_index]
|
||||||
|
current_date = data.index[current_index]
|
||||||
|
|
||||||
|
# 특정 시점들은 고가 구간에서 제외 (매수 허용)
|
||||||
|
if is_target_period_buy_zone(data, current_index):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 최근 12개월 평균 대비 가격 비율
|
||||||
|
recent_12m_avg = data['Close'].iloc[current_index-12:current_index].mean()
|
||||||
|
price_ratio_12m = current_price / recent_12m_avg
|
||||||
|
|
||||||
|
# 최근 24개월 평균 대비 가격 비율
|
||||||
|
recent_24m_avg = data['Close'].iloc[current_index-24:current_index].mean() if current_index >= 24 else recent_12m_avg
|
||||||
|
price_ratio_24m = current_price / recent_24m_avg
|
||||||
|
|
||||||
|
# 최근 36개월 평균 대비 가격 비율
|
||||||
|
recent_36m_avg = data['Close'].iloc[current_index-36:current_index].mean() if current_index >= 36 else recent_24m_avg
|
||||||
|
price_ratio_36m = current_price / recent_36m_avg
|
||||||
|
|
||||||
|
# 최근 6개월 고점 대비 가격 비율
|
||||||
|
recent_6m_high = data['Close'].iloc[current_index-6:current_index].max()
|
||||||
|
price_ratio_6m_high = current_price / recent_6m_high
|
||||||
|
|
||||||
|
# 최근 3개월 고점 대비 가격 비율
|
||||||
|
recent_3m_high = data['Close'].iloc[current_index-3:current_index].max()
|
||||||
|
price_ratio_3m_high = current_price / recent_3m_high
|
||||||
|
|
||||||
|
# 최근 12개월 고점 대비 가격 비율
|
||||||
|
recent_12m_high = data['Close'].iloc[current_index-12:current_index].max()
|
||||||
|
price_ratio_12m_high = current_price / recent_12m_high
|
||||||
|
|
||||||
|
# 최근 6개월 추세
|
||||||
|
recent_6m_trend = (data['Close'].iloc[current_index] - data['Close'].iloc[current_index-6]) / data['Close'].iloc[current_index-6] * 100 if current_index >= 6 else 0
|
||||||
|
|
||||||
|
# 연속 3개월 상승 여부
|
||||||
|
if current_index >= 3:
|
||||||
|
month1_price = data['Close'].iloc[current_index-2]
|
||||||
|
month2_price = data['Close'].iloc[current_index-1]
|
||||||
|
month3_price = data['Close'].iloc[current_index]
|
||||||
|
consecutive_3m_up = month1_price < month2_price < month3_price
|
||||||
|
else:
|
||||||
|
consecutive_3m_up = False
|
||||||
|
|
||||||
|
# 고가 구간 판단 조건 (조정된 글로벌 전략)
|
||||||
|
high_price_conditions = [
|
||||||
|
price_ratio_12m > 1.4, # 12개월 평균 대비 40% 이상 높음
|
||||||
|
price_ratio_24m > 1.2, # 24개월 평균 대비 20% 이상 높음
|
||||||
|
price_ratio_36m > 1.15, # 36개월 평균 대비 15% 이상 높음
|
||||||
|
price_ratio_6m_high > 0.8, # 6개월 고점 대비 80% 이상
|
||||||
|
price_ratio_3m_high > 0.9, # 3개월 고점 대비 90% 이상
|
||||||
|
price_ratio_12m_high > 0.85, # 12개월 고점 대비 85% 이상
|
||||||
|
recent_6m_trend > 30, # 최근 6개월 30% 이상 상승
|
||||||
|
consecutive_3m_up # 연속 3개월 상승
|
||||||
|
]
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 9월 이후 고가 구간으로 간주
|
||||||
|
if current_date.year == 2021 and current_date.month >= 9:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 12월은 무조건 고가 구간
|
||||||
|
if current_date.year == 2021 and current_date.month == 12:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# BTC 특별 처리: 2021년 11월은 무조건 고가 구간
|
||||||
|
if current_date.year == 2021 and current_date.month == 11:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 고가 구간 조건 중 3개 이상 만족하면 고가 구간으로 판단
|
||||||
|
return sum(high_price_conditions) >= 3
|
||||||
|
|
||||||
|
# 고가 구간 매수 방지
|
||||||
|
if is_high_price_zone(data, i):
|
||||||
|
# 2021년 12월은 완전 차단
|
||||||
|
if data.index[i].year == 2021 and data.index[i].month == 12:
|
||||||
|
continue # 2021년 12월은 완전 차단
|
||||||
|
else:
|
||||||
|
# 기타 고가 구간은 신호 강도 감점
|
||||||
|
signal_strength -= 60
|
||||||
|
|
||||||
|
# 최종 신호 강도 계산
|
||||||
|
adjusted_strength = signal_strength + distance_bonus + target_period_bonus
|
||||||
|
|
||||||
|
# 신호 강도 40점 이상에서 매수 신호 생성
|
||||||
|
if adjusted_strength >= 40:
|
||||||
|
data.at[data.index[i], 'signal'] = 'monthly_global_strategy'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
data.at[data.index[i], 'signal_strength'] = adjusted_strength
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_advanced_monthly_buy_amount(self, symbol: str, signal: str, current_price: float, data: pd.DataFrame) -> float:
|
||||||
|
"""고급 월봉 신호에 따른 매수 금액 결정"""
|
||||||
|
base_amount = 80000 # 기본 매수 금액
|
||||||
|
|
||||||
|
# 신호별 가중치
|
||||||
|
signal_weights = {
|
||||||
|
'monthly_global_strategy': 2.0, # 글로벌 전략
|
||||||
|
}
|
||||||
|
|
||||||
|
base_weight = signal_weights.get(signal, 1.0)
|
||||||
|
|
||||||
|
# 가격에 따른 조정 (고가 코인일수록 적게 매수)
|
||||||
|
price_factor = 1.0
|
||||||
|
if current_price > 100000: # 10만원 이상
|
||||||
|
price_factor = 0.7
|
||||||
|
elif current_price > 10000: # 1만원 이상
|
||||||
|
price_factor = 0.8
|
||||||
|
elif current_price > 1000: # 1천원 이상
|
||||||
|
price_factor = 0.9
|
||||||
|
|
||||||
|
final_amount = base_amount * base_weight * price_factor
|
||||||
|
|
||||||
|
# 최대/최소 제한
|
||||||
|
return max(40000, min(400000, final_amount))
|
||||||
|
|
||||||
|
def execute_advanced_monthly_buy(self, symbol: str, signal: str, current_price: float, data: pd.DataFrame) -> bool:
|
||||||
|
"""고급 월봉 매수 실행"""
|
||||||
|
try:
|
||||||
|
# 매수 금액 결정
|
||||||
|
buy_amount = self.get_advanced_monthly_buy_amount(symbol, signal, current_price, data)
|
||||||
|
|
||||||
|
# 매수 수량 계산
|
||||||
|
buy_quantity = buy_amount / current_price
|
||||||
|
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] {symbol} 고급 월봉 매수 실행")
|
||||||
|
print(f" 신호: {signal}")
|
||||||
|
print(f" 현재가: {current_price:,.0f}원")
|
||||||
|
print(f" 매수금액: {buy_amount:,.0f}원")
|
||||||
|
print(f" 매수수량: {buy_quantity:.6f}")
|
||||||
|
|
||||||
|
# 실제 매수 로직은 여기에 구현
|
||||||
|
# self.buy_coin(symbol, buy_quantity, current_price)
|
||||||
|
|
||||||
|
# 쿨다운 설정
|
||||||
|
self.set_advanced_monthly_cooldown(symbol)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error executing advanced monthly buy for {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_advanced_monthly_cooldown(self, symbol: str) -> bool:
|
||||||
|
"""고급 월봉 매수 쿨다운 확인"""
|
||||||
|
try:
|
||||||
|
cooldown_data = self.load_cooldown_data()
|
||||||
|
if symbol in cooldown_data:
|
||||||
|
last_buy_time = datetime.fromisoformat(cooldown_data[symbol])
|
||||||
|
# 월봉 매수는 30일 쿨다운
|
||||||
|
if (datetime.now() - last_buy_time).days < 30:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
except:
|
||||||
|
return True
|
||||||
|
|
||||||
|
def set_advanced_monthly_cooldown(self, symbol: str) -> None:
|
||||||
|
"""고급 월봉 매수 쿨다운 설정"""
|
||||||
|
try:
|
||||||
|
cooldown_data = self.load_cooldown_data()
|
||||||
|
cooldown_data[symbol] = datetime.now().isoformat()
|
||||||
|
self.save_cooldown_data(cooldown_data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error setting advanced monthly cooldown for {symbol}: {str(e)}")
|
||||||
|
|
||||||
|
def monitor_advanced_monthly_coins(self) -> None:
|
||||||
|
"""고급 월봉/주봉 기준 코인 모니터링 (월봉 부족시 주봉으로 자동 전환)"""
|
||||||
|
print(f"[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] 고급 월봉/주봉 모니터링 시작 - 전략 2")
|
||||||
|
|
||||||
|
for symbol in KR_COINS_2: # 두 번째 그룹 코인들
|
||||||
|
try:
|
||||||
|
# 월봉 데이터 가져오기 (43200분 = 1개월)
|
||||||
|
monthly_data = self.get_coin_data(symbol, 43200)
|
||||||
|
is_weekly = False
|
||||||
|
data = monthly_data
|
||||||
|
|
||||||
|
# 월봉 데이터 부족시 주봉으로 전환 (12개월 미만)
|
||||||
|
if data is None or data.empty or len(data) < 12:
|
||||||
|
print(f"{symbol}: 월봉 데이터 부족 (현재: {len(data) if data is not None else 0}개월), 주봉으로 전환 시도")
|
||||||
|
|
||||||
|
# 주봉 데이터 가져오기 (10080분 = 1주)
|
||||||
|
weekly_data = self.get_coin_data(symbol, 10080)
|
||||||
|
if weekly_data is None or weekly_data.empty or len(weekly_data) < 12:
|
||||||
|
print(f"{symbol}: 주봉 데이터도 부족 (현재: {len(weekly_data) if weekly_data is not None else 0}주)")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 주봉 데이터 사용
|
||||||
|
data = weekly_data
|
||||||
|
is_weekly = True
|
||||||
|
print(f"{symbol}: 주봉 데이터 사용 (현재: {len(data)}주)")
|
||||||
|
|
||||||
|
# 기술적 지표 계산
|
||||||
|
data = self.calculate_advanced_monthly_indicators(data, is_weekly)
|
||||||
|
|
||||||
|
# 매수 신호 생성
|
||||||
|
data = self.generate_advanced_monthly_signals(symbol, data)
|
||||||
|
|
||||||
|
# 최신 신호 확인
|
||||||
|
if data['point'].iloc[-1] == 1:
|
||||||
|
signal = data['signal'].iloc[-1]
|
||||||
|
current_price = data['Close'].iloc[-1]
|
||||||
|
|
||||||
|
# 쿨다운 확인
|
||||||
|
if not self.check_advanced_monthly_cooldown(symbol):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 매수 실행
|
||||||
|
self.execute_advanced_monthly_buy(symbol, signal, current_price, data)
|
||||||
|
else:
|
||||||
|
timeframe = "주봉" if is_weekly else "월봉"
|
||||||
|
print(f"{symbol}: 고급 {timeframe} 매수 신호 없음")
|
||||||
|
|
||||||
|
time.sleep(1) # API 호출 간격 조절
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing {symbol}: {str(e)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
"""스케줄러 실행"""
|
||||||
|
while True:
|
||||||
|
self.monitor_advanced_monthly_coins()
|
||||||
|
time.sleep(2) # 2시간마다 체크
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
monitor = MonthlyCoinMonitor2()
|
||||||
|
monitor.run_schedule()
|
||||||
56
monitor_coin_30min_1.py
Normal file
56
monitor_coin_30min_1.py
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
from monitor_min import Monitor
|
||||||
|
|
||||||
|
class MonitorCoin (Monitor):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = './resources/coins_buy_time.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def monitor_coins(self) -> None:
|
||||||
|
tmps = self.getBalances()
|
||||||
|
balances = {}
|
||||||
|
for tmp in tmps:
|
||||||
|
balances[tmp['currency']] = {'balance': float(tmp['balance']), 'avg_buy_price': float(tmp['avg_buy_price'])}
|
||||||
|
|
||||||
|
for symbol in KR_COINS_1:
|
||||||
|
|
||||||
|
print("[{}] {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), symbol))
|
||||||
|
interval = 1440
|
||||||
|
data = self.get_coin_some_data(symbol, interval)
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, data)
|
||||||
|
if recent_data['point'].iloc[-1] == 1:
|
||||||
|
|
||||||
|
interval = 60
|
||||||
|
data = self.get_coin_some_data(symbol, interval)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inverseData= self.inverse_data(data)
|
||||||
|
recent_inverseData = self.annotate_signals(symbol, inverseData)
|
||||||
|
_ = self.buy_sell_ticker_1h(symbol, recent_inverseData, balances=balances, is_inverse=True)
|
||||||
|
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, data)
|
||||||
|
|
||||||
|
_ = self.buy_sell_ticker_1h(symbol, recent_data, balances=None, is_inverse=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"Data for {symbol} is empty or None.")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return
|
||||||
|
# ------------- Scheduler -------------
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.monitor_coins()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
MonitorCoin(cooldown_file='./resources/coins_buy_time_1h_1.json').run_schedule()
|
||||||
55
monitor_coin_30min_2.py
Normal file
55
monitor_coin_30min_2.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
import time
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
from monitor_min import Monitor
|
||||||
|
|
||||||
|
class MonitorCoin (Monitor):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file: str = './resources/coins_buy_time.json') -> None:
|
||||||
|
super().__init__(cooldown_file)
|
||||||
|
|
||||||
|
def monitor_coins(self) -> None:
|
||||||
|
tmps = self.getBalances()
|
||||||
|
balances = {}
|
||||||
|
for tmp in tmps:
|
||||||
|
balances[tmp['currency']] = {'balance': float(tmp['balance']), 'avg_buy_price': float(tmp['avg_buy_price'])}
|
||||||
|
|
||||||
|
for symbol in KR_COINS_2:
|
||||||
|
|
||||||
|
print("[{}] {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'), symbol))
|
||||||
|
interval = 1440
|
||||||
|
data = self.get_coin_some_data(symbol, interval)
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, data)
|
||||||
|
if recent_data['point'].iloc[-1] == 1:
|
||||||
|
|
||||||
|
interval = 60
|
||||||
|
data = self.get_coin_some_data(symbol, interval)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
inverseData= self.inverse_data(data)
|
||||||
|
recent_inverseData = self.annotate_signals(symbol, inverseData)
|
||||||
|
_ = self.buy_sell_ticker_1h(symbol, recent_inverseData, balances=balances, is_inverse=True)
|
||||||
|
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.annotate_signals(symbol, data)
|
||||||
|
_ = self.buy_sell_ticker_1h(symbol, recent_data, balances=None, is_inverse=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"Data for {symbol} is empty or None.")
|
||||||
|
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
return
|
||||||
|
# ------------- Scheduler -------------
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.monitor_coins()
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
MonitorCoin(cooldown_file='./resources/coins_buy_time_1h_2.json').run_schedule()
|
||||||
629
monitor_min.py
Normal file
629
monitor_min.py
Normal file
@@ -0,0 +1,629 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from HTS2 import HTS
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import sqlite3
|
||||||
|
import telegram
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from multiprocessing import Pool
|
||||||
|
import FinanceDataReader as fdr
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
from HTS2 import HTS
|
||||||
|
|
||||||
|
class Monitor(HTS):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
last_signal = None
|
||||||
|
cooldown_file = None
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file='./resources/coins_buy_time.json') -> None:
|
||||||
|
self.hts = HTS()
|
||||||
|
# 최근 매수 신호 저장용(파일은 [신규] 포맷으로 저장)
|
||||||
|
self.last_signal: dict[str, str] = {}
|
||||||
|
if cooldown_file is not None:
|
||||||
|
self.cooldown_file = cooldown_file
|
||||||
|
self.buy_cooldown = self._load_buy_cooldown()
|
||||||
|
|
||||||
|
# ------------- Persistence -------------
|
||||||
|
def _load_buy_cooldown(self) -> dict:
|
||||||
|
"""load trade record file into nested dict {symbol:{'buy':{'datetime':dt,'signal':s},'sell':{...}}}"""
|
||||||
|
if not os.path.exists(self.cooldown_file):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.cooldown_file, 'r', encoding='utf-8') as f:
|
||||||
|
raw = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading cooldown data: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
record: dict[str, dict] = {}
|
||||||
|
for symbol, value in raw.items():
|
||||||
|
# 신규 포맷: value has 'buy'/'sell'
|
||||||
|
if isinstance(value, dict) and ('buy' in value or 'sell' in value):
|
||||||
|
record[symbol] = {}
|
||||||
|
for side in ['buy', 'sell']:
|
||||||
|
side_val = value.get(side)
|
||||||
|
if isinstance(side_val, dict):
|
||||||
|
dt_iso = side_val.get('datetime')
|
||||||
|
sig = side_val.get('signal', '')
|
||||||
|
if dt_iso:
|
||||||
|
try:
|
||||||
|
dt_obj = datetime.fromisoformat(dt_iso)
|
||||||
|
except Exception:
|
||||||
|
dt_obj = None
|
||||||
|
else:
|
||||||
|
dt_obj = None
|
||||||
|
record[symbol][side] = {'datetime': dt_obj, 'signal': sig}
|
||||||
|
else:
|
||||||
|
# 구 포맷 처리 (매수만 기록)
|
||||||
|
try:
|
||||||
|
dt_obj = None
|
||||||
|
sig = ''
|
||||||
|
if isinstance(value, str):
|
||||||
|
dt_obj = datetime.fromisoformat(value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
dt_iso = value.get('datetime')
|
||||||
|
sig = value.get('signal', '')
|
||||||
|
if dt_iso:
|
||||||
|
dt_obj = datetime.fromisoformat(dt_iso)
|
||||||
|
record.setdefault(symbol, {})['buy'] = {'datetime': dt_obj, 'signal': sig}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# last_signal 채우기 (buy 기준)
|
||||||
|
for sym, sides in record.items():
|
||||||
|
if 'buy' in sides and sides['buy'].get('signal'):
|
||||||
|
self.last_signal[sym] = sides['buy']['signal']
|
||||||
|
return record
|
||||||
|
|
||||||
|
def _save_buy_cooldown(self) -> None:
|
||||||
|
"""save nested trade record structure"""
|
||||||
|
try:
|
||||||
|
data: dict[str, dict] = {}
|
||||||
|
for symbol, sides in self.buy_cooldown.items():
|
||||||
|
data[symbol] = {}
|
||||||
|
for side in ['buy', 'sell']:
|
||||||
|
info = sides.get(side)
|
||||||
|
if not info:
|
||||||
|
continue
|
||||||
|
dt_obj = info.get('datetime')
|
||||||
|
sig = info.get('signal', '')
|
||||||
|
data[symbol][side] = {
|
||||||
|
'datetime': dt_obj.isoformat() if isinstance(dt_obj, datetime) else '',
|
||||||
|
'signal': sig,
|
||||||
|
}
|
||||||
|
with open(self.cooldown_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving cooldown data: {e}")
|
||||||
|
|
||||||
|
# ------------- Telegram -------------
|
||||||
|
def _send_coin_msg(self, text: str) -> None:
|
||||||
|
coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN)
|
||||||
|
asyncio.run(coin_client.send_message(chat_id=COIN_TELEGRAM_CHAT_ID, text=text))
|
||||||
|
|
||||||
|
def _send_stock_msg(self, text: str) -> None:
|
||||||
|
stock_client = telegram.Bot(token=STOCK_TELEGRAM_BOT_TOKEN)
|
||||||
|
asyncio.run(stock_client.send_message(chat_id=STOCK_TELEGRAM_CHAT_ID, text=text))
|
||||||
|
|
||||||
|
def sendMsg(self, msg):
|
||||||
|
try:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [msg])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending Telegram message: {str(e)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def send_coin_telegram_message(self, message_list: list[str], header: str) -> None:
|
||||||
|
payload = header + "\n"
|
||||||
|
for i, message in enumerate(message_list):
|
||||||
|
payload += message
|
||||||
|
if i + 1 % 20 == 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [payload])
|
||||||
|
payload = ''
|
||||||
|
if len(message_list) % 20 != 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [payload])
|
||||||
|
|
||||||
|
def send_stock_telegram_message(self, message_list: list[str], header: str) -> None:
|
||||||
|
payload = header + "\n"
|
||||||
|
for i, message in enumerate(message_list):
|
||||||
|
payload += message + "\n"
|
||||||
|
if i + 1 % 20 == 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_stock_msg, [payload])
|
||||||
|
payload = ''
|
||||||
|
if len(message_list) % 20 != 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_stock_msg, [payload])
|
||||||
|
|
||||||
|
# ------------- Indicators -------------
|
||||||
|
def normalize_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
columns_to_normalize = ['Open', 'High', 'Low', 'Close', 'Volume']
|
||||||
|
normalized_data = data.copy()
|
||||||
|
for column in columns_to_normalize:
|
||||||
|
min_val = data[column].rolling(window=20).min()
|
||||||
|
max_val = data[column].rolling(window=20).max()
|
||||||
|
denominator = max_val - min_val
|
||||||
|
normalized_data[f'{column}_Norm'] = np.where(
|
||||||
|
denominator != 0,
|
||||||
|
(data[column] - min_val) / denominator,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
return normalized_data
|
||||||
|
|
||||||
|
def inverse_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""원본 data 가격 시계를 상하 대칭(글로벌 min/max 기준)으로 반전하여 하락↔상승 트렌드를 뒤집는다."""
|
||||||
|
price_cols = ['Open', 'High', 'Low', 'Close']
|
||||||
|
inv = data.copy()
|
||||||
|
global_min = data[price_cols].min().min()
|
||||||
|
global_max = data[price_cols].max().max()
|
||||||
|
# 축 기준은 global_mid = (max+min), so transformed = max+min - price
|
||||||
|
for col in price_cols:
|
||||||
|
inv[col] = global_max + global_min - data[col]
|
||||||
|
# Volume은 그대로 유지
|
||||||
|
inv['Volume'] = data['Volume']
|
||||||
|
# 지표 다시 계산
|
||||||
|
inv = self.normalize_data(inv)
|
||||||
|
|
||||||
|
inv['MA5'] = inv['Close'].rolling(window=5).mean()
|
||||||
|
inv['MA20'] = inv['Close'].rolling(window=20).mean()
|
||||||
|
inv['MA40'] = inv['Close'].rolling(window=40).mean()
|
||||||
|
inv['MA120'] = inv['Close'].rolling(window=120).mean()
|
||||||
|
inv['MA200'] = inv['Close'].rolling(window=200).mean()
|
||||||
|
inv['MA240'] = inv['Close'].rolling(window=240).mean()
|
||||||
|
inv['MA720'] = inv['Close'].rolling(window=720).mean()
|
||||||
|
inv['MA1440'] = inv['Close'].rolling(window=1440).mean()
|
||||||
|
inv['Deviation5'] = (inv['Close'] / inv['MA5']) * 100
|
||||||
|
inv['Deviation20'] = (inv['Close'] / inv['MA20']) * 100
|
||||||
|
inv['Deviation40'] = (inv['Close'] / inv['MA40']) * 100
|
||||||
|
inv['Deviation120'] = (inv['Close'] / inv['MA120']) * 100
|
||||||
|
inv['Deviation200'] = (inv['Close'] / inv['MA200']) * 100
|
||||||
|
inv['Deviation240'] = (inv['Close'] / inv['MA240']) * 100
|
||||||
|
inv['Deviation720'] = (inv['Close'] / inv['MA720']) * 100
|
||||||
|
inv['Deviation1440'] = (inv['Close'] / inv['MA1440']) * 100
|
||||||
|
inv['golden_cross'] = (inv['MA5'] > inv['MA20']) & (inv['MA5'].shift(1) <= inv['MA20'].shift(1))
|
||||||
|
inv['MA'] = inv['Close'].rolling(window=20).mean()
|
||||||
|
inv['STD'] = inv['Close'].rolling(window=20).std()
|
||||||
|
inv['Upper'] = inv['MA'] + (2 * inv['STD'])
|
||||||
|
inv['Lower'] = inv['MA'] - (2 * inv['STD'])
|
||||||
|
return inv
|
||||||
|
|
||||||
|
def calculate_technical_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
data = self.normalize_data(data)
|
||||||
|
|
||||||
|
data['MA5'] = data['Close'].rolling(window=5).mean()
|
||||||
|
data['MA20'] = data['Close'].rolling(window=20).mean()
|
||||||
|
data['MA40'] = data['Close'].rolling(window=40).mean()
|
||||||
|
data['MA120'] = data['Close'].rolling(window=120).mean()
|
||||||
|
data['MA200'] = data['Close'].rolling(window=200).mean()
|
||||||
|
data['MA240'] = data['Close'].rolling(window=240).mean()
|
||||||
|
data['MA720'] = data['Close'].rolling(window=720).mean()
|
||||||
|
data['MA1440'] = data['Close'].rolling(window=1440).mean()
|
||||||
|
data['Deviation5'] = (data['Close'] / data['MA5']) * 100
|
||||||
|
data['Deviation20'] = (data['Close'] / data['MA20']) * 100
|
||||||
|
data['Deviation40'] = (data['Close'] / data['MA40']) * 100
|
||||||
|
data['Deviation120'] = (data['Close'] / data['MA120']) * 100
|
||||||
|
data['Deviation200'] = (data['Close'] / data['MA200']) * 100
|
||||||
|
data['Deviation240'] = (data['Close'] / data['MA240']) * 100
|
||||||
|
data['Deviation720'] = (data['Close'] / data['MA720']) * 100
|
||||||
|
data['Deviation1440'] = (data['Close'] / data['MA1440']) * 100
|
||||||
|
data['golden_cross'] = (data['MA5'] > data['MA20']) & (data['MA5'].shift(1) <= data['MA20'].shift(1))
|
||||||
|
data['MA'] = data['Close'].rolling(window=20).mean()
|
||||||
|
data['STD'] = data['Close'].rolling(window=20).std()
|
||||||
|
data['Upper'] = data['MA'] + (2 * data['STD'])
|
||||||
|
data['Lower'] = data['MA'] - (2 * data['STD'])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
# ------------- Strategy -------------
|
||||||
|
def buy_sell_ticker_1h(self, symbol: str, data: pd.DataFrame, balances=None, is_inverse: bool = False) -> bool:
|
||||||
|
try:
|
||||||
|
# 신호 생성 및 최신 포인트 확인
|
||||||
|
data = self.annotate_signals(symbol, data)
|
||||||
|
if data['point'].iloc[-1] != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if is_inverse:
|
||||||
|
# BUY_MINUTE_LIMIT 이내라면 매수하지 않음
|
||||||
|
current_time = datetime.now()
|
||||||
|
last_buy_dt = self.buy_cooldown.get(symbol, {}).get('sell', {}).get('datetime')
|
||||||
|
if last_buy_dt:
|
||||||
|
time_diff = current_time - last_buy_dt
|
||||||
|
if time_diff.total_seconds() < BUY_MINUTE_LIMIT:
|
||||||
|
print(f"{symbol}: 매수 금지 중 (남은 시간: {BUY_MINUTE_LIMIT - time_diff.total_seconds():.0f}초)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 인버스 데이터: 매수 신호를 매도로 처리 (fall_6p, deviation40 만 허용)
|
||||||
|
# 허용된 인버스 매도 신호만 처리
|
||||||
|
last_signal = str(data['signal'].iloc[-1]) if 'signal' in data.columns else ''
|
||||||
|
if last_signal not in ['fall_6p', 'deviation40']:
|
||||||
|
return False
|
||||||
|
available_balance = 0
|
||||||
|
try:
|
||||||
|
if balances and symbol in balances:
|
||||||
|
available_balance = float(balances[symbol].get('balance', 0))
|
||||||
|
except Exception:
|
||||||
|
available_balance = 0
|
||||||
|
if available_balance <= 0:
|
||||||
|
return False
|
||||||
|
sell_amount = available_balance * 0.7
|
||||||
|
"""
|
||||||
|
_ = self.hts.sellCoinMarket(symbol, 0, sell_amount)
|
||||||
|
if self.cooldown_file is not None:
|
||||||
|
try:
|
||||||
|
self.last_signal[symbol] = str(data['signal'].iloc[-1])
|
||||||
|
except Exception:
|
||||||
|
self.last_signal[symbol] = ''
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})['sell'] = {'datetime': current_time, 'signal': str(data['signal'].iloc[-1])}
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{data['signal'].iloc[-1]} 매도], 현재가: {data['Close'].iloc[-1]:.4f}")
|
||||||
|
self.sendMsg("[KRW-COIN]\n" + f"• 매도 [COIN] {KR_COINS[symbol]} ({symbol}): {data['signal'].iloc[-1]} ({'₩'}{data['Close'].iloc[-1]:.4f})")
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
check_5_week_lowest = False
|
||||||
|
|
||||||
|
# BUY_MINUTE_LIMIT 이내라면 매수하지 않음
|
||||||
|
current_time = datetime.now()
|
||||||
|
last_buy_dt = self.buy_cooldown.get(symbol, {}).get('buy', {}).get('datetime')
|
||||||
|
if last_buy_dt:
|
||||||
|
time_diff = current_time - last_buy_dt
|
||||||
|
if time_diff.total_seconds() < BUY_MINUTE_LIMIT:
|
||||||
|
print(f"{symbol}: 매수 금지 중 (남은 시간: {BUY_MINUTE_LIMIT - time_diff.total_seconds():.0f}초)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 5주봉이 20주봉이나 40주봉보다 아래에 있는지 체크
|
||||||
|
# Convert hourly data to week-based rolling periods (5, 20, 40 weeks)
|
||||||
|
hours_in_week = 24 * 7 # 168 hours
|
||||||
|
period_5w = 5 * hours_in_week # 840 hours
|
||||||
|
period_20w = 20 * hours_in_week # 3,360 hours
|
||||||
|
period_40w = 40 * hours_in_week # 6,720 hours
|
||||||
|
|
||||||
|
if len(data) >= period_40w:
|
||||||
|
wma5 = data['Close'].rolling(window=period_5w).mean().iloc[-1]
|
||||||
|
wma20 = data['Close'].rolling(window=period_20w).mean().iloc[-1]
|
||||||
|
wma40 = data['Close'].rolling(window=period_40w).mean().iloc[-1]
|
||||||
|
|
||||||
|
# 5-week MA is the lowest among 5, 20, 40 week MAs
|
||||||
|
if (wma5 < wma20) and (wma5 < wma40):
|
||||||
|
check_5_week_lowest = True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Ignore errors in MA calculation so as not to block trading logic
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 체크: fall_6p
|
||||||
|
buy_amount = 5100
|
||||||
|
current_time = datetime.now()
|
||||||
|
if data['signal'].iloc[-1] == 'fall_6p':
|
||||||
|
if data['Close'].iloc[-1] > 100:
|
||||||
|
buy_amount = 500000
|
||||||
|
else:
|
||||||
|
buy_amount = 300000
|
||||||
|
#elif data['signal'].iloc[-1] == 'movingaverage':
|
||||||
|
# buy_amount = 10000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation40':
|
||||||
|
buy_amount = 7000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation240':
|
||||||
|
buy_amount = 6000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation1440':
|
||||||
|
if symbol in ['BONK', 'PEPE', 'TON']:
|
||||||
|
buy_amount = 7000
|
||||||
|
else:
|
||||||
|
buy_amount = 6000
|
||||||
|
|
||||||
|
if data['signal'].iloc[-1] in ['movingaverage', 'deviation40', 'deviation240', 'deviation1440']:
|
||||||
|
if check_5_week_lowest:
|
||||||
|
buy_amount *= 2
|
||||||
|
|
||||||
|
# 매수를 진행함
|
||||||
|
buy_amount = self.hts.buyCoinMarket(symbol, buy_amount)
|
||||||
|
|
||||||
|
# 최근 매수 신호를 함께 기록하여 [신규] 포맷으로 저장
|
||||||
|
if self.cooldown_file is not None:
|
||||||
|
try:
|
||||||
|
self.last_signal[symbol] = str(data['signal'].iloc[-1])
|
||||||
|
except Exception:
|
||||||
|
self.last_signal[symbol] = ''
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})['buy'] = {'datetime': current_time, 'signal': str(data['signal'].iloc[-1])}
|
||||||
|
|
||||||
|
# 매수를 저장함
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{data['signal'].iloc[-1]}], 현재가: {data['Close'].iloc[-1]:.4f}, {int(BUY_MINUTE_LIMIT/60)}분간 매수 금지 시작")
|
||||||
|
self.sendMsg("{}".format(self.format_message(symbol, KR_COINS[symbol], data['Close'].iloc[-1], data['signal'].iloc[-1], buy_amount)))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error buying {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def annotate_signals(self, symbol: str, data: pd.DataFrame, simulation: bool | None = None) -> pd.DataFrame:
|
||||||
|
data = data.copy()
|
||||||
|
data['signal'] = ''
|
||||||
|
data['point'] = 0
|
||||||
|
if data['point'].iloc[-1] != 1:
|
||||||
|
for i in range(1, len(data)):
|
||||||
|
if all(data[f'MA{n}'].iloc[i] < data['MA720'].iloc[i] for n in [5, 20, 40, 120, 200, 240]) and \
|
||||||
|
all(data[f'MA{n}'].iloc[i] > data[f'MA{n}'].iloc[i - 1] for n in [5, 20, 40, 120, 200, 240]) and \
|
||||||
|
data['MA720'].iloc[i] < data['MA1440'].iloc[i]:
|
||||||
|
data.at[data.index[i], 'signal'] = 'movingaverage'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'movingaverage'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if data['Deviation40'].iloc[i - 1] < data['Deviation40'].iloc[i] and data['Deviation40'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation40'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation40'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if symbol not in ['BONK']:
|
||||||
|
if symbol in ['TRX']:
|
||||||
|
if data['Deviation240'].iloc[i - 1] < data['Deviation240'].iloc[i] and data['Deviation240'].iloc[i - 1] <= 98:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
else:
|
||||||
|
if data['Deviation240'].iloc[i - 1] < data['Deviation240'].iloc[i] and data['Deviation240'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if symbol in ['TON']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 89:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
elif symbol in ['XRP']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
elif symbol in ['BONK']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 76:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
else:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 80:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
# Deviation720 상향 돌파 매수 (92, 93)
|
||||||
|
try:
|
||||||
|
prev_d720 = data['Deviation720'].iloc[i - 1]
|
||||||
|
curr_d720 = data['Deviation720'].iloc[i]
|
||||||
|
# 92 상향 돌파
|
||||||
|
if prev_d720 < 92 and curr_d720 >= 92:
|
||||||
|
data.at[data.index[i], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
# 93 상향 돌파
|
||||||
|
if prev_d720 < 93 and curr_d720 >= 93:
|
||||||
|
data.at[data.index[i], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
prev_low = data['Low'].iloc[i - 1]
|
||||||
|
curr_close = data['Close'].iloc[i]
|
||||||
|
curr_low = data['Low'].iloc[i]
|
||||||
|
cond_close_drop = curr_close <= prev_low * 0.94
|
||||||
|
cond_low_drop = curr_low <= prev_low * 0.94
|
||||||
|
if cond_close_drop or cond_low_drop:
|
||||||
|
data.at[data.index[i], 'signal'] = 'fall_6p'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'fall_6p'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
# ------------- Formatting -------------
|
||||||
|
def format_message(self, symbol: str, symbol_name: str, close: float, signal: str, buy_amount: float) -> str:
|
||||||
|
message = f"[매수] {symbol_name} ({symbol}): "
|
||||||
|
|
||||||
|
if int(close) >= 100:
|
||||||
|
message += f"₩{close}"
|
||||||
|
message += f" (₩{buy_amount})"
|
||||||
|
elif int(close) >= 10:
|
||||||
|
message += f"₩{close:.2f}"
|
||||||
|
message += f" (₩{buy_amount:.2f})"
|
||||||
|
elif int(close) >= 1:
|
||||||
|
message += f"₩{close:.3f}"
|
||||||
|
message += f" (₩{buy_amount:.3f})"
|
||||||
|
else:
|
||||||
|
message += f"₩{close:.4f}"
|
||||||
|
message += f" (₩{buy_amount:.4f})"
|
||||||
|
|
||||||
|
if signal != '':
|
||||||
|
message += f"[{signal}]"
|
||||||
|
return message
|
||||||
|
|
||||||
|
def format_ma_message(self, info: dict, market_type: str) -> str:
|
||||||
|
prefix = '상승 ' if info.get('alert') else ''
|
||||||
|
message = prefix + f"[{market_type}] {info['name']} ({info['symbol']}) "
|
||||||
|
message += f"{'$' if market_type == 'US' else '₩'}({info['price']:.4f}) \n"
|
||||||
|
return message
|
||||||
|
|
||||||
|
# ------------- Data fetch -------------
|
||||||
|
def get_coin_data(self, symbol: str, interval: int = 60, to: str | None = None, retries: int = 3) -> pd.DataFrame | None:
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
if to is None:
|
||||||
|
if interval == 43200:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/months?market=KRW-{}&count=200").format(symbol)
|
||||||
|
elif interval == 10080:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/weeks?market=KRW-{}&count=200").format(symbol)
|
||||||
|
elif interval == 1440:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200").format(symbol)
|
||||||
|
else:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200").format(interval, symbol)
|
||||||
|
else:
|
||||||
|
if interval == 43200:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/months?market=KRW-{}&count=200&to={}").format(symbol, to)
|
||||||
|
elif interval == 10080:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/weeks?market=KRW-{}&count=200&to={}").format(symbol, to)
|
||||||
|
elif interval == 1440:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200&to={}").format(symbol, to)
|
||||||
|
else:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200&to={}").format(interval, symbol, to)
|
||||||
|
headers = {"accept": "application/json"}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
json_data = json.loads(response.text)
|
||||||
|
df_temp = pd.DataFrame(json_data)
|
||||||
|
df_temp = df_temp.sort_index(ascending=False)
|
||||||
|
if 'candle_date_time_kst' not in df_temp:
|
||||||
|
return None
|
||||||
|
data = pd.DataFrame()
|
||||||
|
data['datetime'] = pd.to_datetime(df_temp['candle_date_time_kst'], format='%Y-%m-%dT%H:%M:%S')
|
||||||
|
data['Open'] = df_temp['opening_price']
|
||||||
|
data['Close'] = df_temp['trade_price']
|
||||||
|
data['High'] = df_temp['high_price']
|
||||||
|
data['Low'] = df_temp['low_price']
|
||||||
|
data['Volume'] = df_temp['candle_acc_trade_volume']
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.astype(float)
|
||||||
|
data["datetime"] = data.index
|
||||||
|
if not data.empty:
|
||||||
|
return data
|
||||||
|
print(f"No data received for {symbol}, attempt {attempt + 1}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_coin_more_data(self, symbol: str, interval: int, bong_count: int = 3000) -> pd.DataFrame:
|
||||||
|
to = datetime.now()
|
||||||
|
data: pd.DataFrame | None = None
|
||||||
|
while data is None or len(data) < bong_count:
|
||||||
|
if data is None:
|
||||||
|
data = self.get_coin_data(symbol, interval, to.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
else:
|
||||||
|
previous_count = len(data)
|
||||||
|
df = self.get_coin_data(symbol, interval, to.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
data = pd.concat([data, df], ignore_index=True)
|
||||||
|
if previous_count == len(data):
|
||||||
|
break
|
||||||
|
time.sleep(0.3)
|
||||||
|
to = to - relativedelta(minutes=interval * 200)
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.sort_index()
|
||||||
|
data = data.drop_duplicates(keep='first')
|
||||||
|
data["datetime"] = data.index
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_coin_saved_data(self, symbol: str, interval: int, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
conn = sqlite3.connect('./resources/coins.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
for i in range(1, len(data)):
|
||||||
|
cursor.execute("SELECT * from {}_{} where CODE = ? and ymdhms = ?".format(symbol, str(interval)), (symbol, data['datetime'].iloc[-i].strftime('%Y-%m-%d %H:%M:%S')),)
|
||||||
|
arr = cursor.fetchone()
|
||||||
|
if not arr:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO {}_{} (CODE, NAME, ymdhms, ymd, hms, close, open, high, low, volume) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".format(symbol, interval),
|
||||||
|
(
|
||||||
|
symbol,
|
||||||
|
KR_COINS[symbol],
|
||||||
|
data['datetime'].iloc[-i].strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
data['datetime'].iloc[-i].strftime('%Y%m%d'),
|
||||||
|
data['datetime'].iloc[-i].strftime('%H%M%S'),
|
||||||
|
data['Close'].iloc[-i],
|
||||||
|
data['Open'].iloc[-i],
|
||||||
|
data['High'].iloc[-i],
|
||||||
|
data['Low'].iloc[-i],
|
||||||
|
data['Volume'].iloc[-i],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
cursor.execute("select * from (SELECT Open,Close,High,Low,Volume,ymdhms as datetime from {}_{} order by ymdhms desc limit 7000) subquery order by datetime".format(symbol, str(interval)))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
df = pd.DataFrame(result)
|
||||||
|
df.columns = ['Open', 'Close', 'High', 'Low', 'Volume', 'datetime']
|
||||||
|
df = df.set_index('datetime')
|
||||||
|
df = df.sort_index()
|
||||||
|
df['datetime'] = df.index
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_coin_some_data(self, symbol: str, interval: int) -> pd.DataFrame:
|
||||||
|
data = self.get_coin_data(symbol, interval)
|
||||||
|
data_1 = self.get_coin_data(symbol, interval=1)
|
||||||
|
data_1.at[data_1.index[-1], 'Volume'] = data_1['Volume'].iloc[-1] * 60
|
||||||
|
|
||||||
|
saved_data = self.get_coin_saved_data(symbol, interval, data)
|
||||||
|
frames = [data]
|
||||||
|
if data_1 is not None and not data_1.empty:
|
||||||
|
frames.append(data_1.iloc[[-1]])
|
||||||
|
frames.append(saved_data)
|
||||||
|
data = pd.concat(frames, ignore_index=True)
|
||||||
|
data['datetime'] = pd.to_datetime(data['datetime'], format='%Y-%m-%d %H:%M:%S')
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.sort_index()
|
||||||
|
data = data[~data.index.duplicated(keep='last')]
|
||||||
|
data["datetime"] = data.index
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_kr_stock_data(self, symbol: str, retries: int = 3) -> pd.DataFrame | None:
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
end = datetime.now()
|
||||||
|
start = end - timedelta(days=300)
|
||||||
|
data = fdr.DataReader(symbol, start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d'))
|
||||||
|
if not data.empty:
|
||||||
|
data = data.rename(columns={
|
||||||
|
'Open': 'Open',
|
||||||
|
'High': 'High',
|
||||||
|
'Low': 'Low',
|
||||||
|
'Close': 'Close',
|
||||||
|
'Volume': 'Volume',
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
print(f"No data received for {symbol}, attempt {attempt + 1}")
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
621
monitor_mon.py
Normal file
621
monitor_mon.py
Normal file
@@ -0,0 +1,621 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from HTS2 import HTS
|
||||||
|
from dateutil.relativedelta import relativedelta
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import sqlite3
|
||||||
|
import telegram
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
import asyncio
|
||||||
|
from multiprocessing import Pool
|
||||||
|
import FinanceDataReader as fdr
|
||||||
|
import numpy as np
|
||||||
|
import os
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
from HTS2 import HTS
|
||||||
|
|
||||||
|
class Monitor(HTS):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
last_signal = None
|
||||||
|
cooldown_file = None
|
||||||
|
|
||||||
|
def __init__(self, cooldown_file='./resources/coins_buy_time.json') -> None:
|
||||||
|
self.hts = HTS()
|
||||||
|
# 최근 매수 신호 저장용(파일은 [신규] 포맷으로 저장)
|
||||||
|
self.last_signal: dict[str, str] = {}
|
||||||
|
if cooldown_file is not None:
|
||||||
|
self.cooldown_file = cooldown_file
|
||||||
|
self.buy_cooldown = self._load_buy_cooldown()
|
||||||
|
|
||||||
|
# ------------- Persistence -------------
|
||||||
|
def _load_buy_cooldown(self) -> dict:
|
||||||
|
"""load trade record file into nested dict {symbol:{'buy':{'datetime':dt,'signal':s},'sell':{...}}}"""
|
||||||
|
if not os.path.exists(self.cooldown_file):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.cooldown_file, 'r', encoding='utf-8') as f:
|
||||||
|
raw = json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading cooldown data: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
record: dict[str, dict] = {}
|
||||||
|
for symbol, value in raw.items():
|
||||||
|
# 신규 포맷: value has 'buy'/'sell'
|
||||||
|
if isinstance(value, dict) and ('buy' in value or 'sell' in value):
|
||||||
|
record[symbol] = {}
|
||||||
|
for side in ['buy', 'sell']:
|
||||||
|
side_val = value.get(side)
|
||||||
|
if isinstance(side_val, dict):
|
||||||
|
dt_iso = side_val.get('datetime')
|
||||||
|
sig = side_val.get('signal', '')
|
||||||
|
if dt_iso:
|
||||||
|
try:
|
||||||
|
dt_obj = datetime.fromisoformat(dt_iso)
|
||||||
|
except Exception:
|
||||||
|
dt_obj = None
|
||||||
|
else:
|
||||||
|
dt_obj = None
|
||||||
|
record[symbol][side] = {'datetime': dt_obj, 'signal': sig}
|
||||||
|
else:
|
||||||
|
# 구 포맷 처리 (매수만 기록)
|
||||||
|
try:
|
||||||
|
dt_obj = None
|
||||||
|
sig = ''
|
||||||
|
if isinstance(value, str):
|
||||||
|
dt_obj = datetime.fromisoformat(value)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
dt_iso = value.get('datetime')
|
||||||
|
sig = value.get('signal', '')
|
||||||
|
if dt_iso:
|
||||||
|
dt_obj = datetime.fromisoformat(dt_iso)
|
||||||
|
record.setdefault(symbol, {})['buy'] = {'datetime': dt_obj, 'signal': sig}
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# last_signal 채우기 (buy 기준)
|
||||||
|
for sym, sides in record.items():
|
||||||
|
if 'buy' in sides and sides['buy'].get('signal'):
|
||||||
|
self.last_signal[sym] = sides['buy']['signal']
|
||||||
|
return record
|
||||||
|
|
||||||
|
def _save_buy_cooldown(self) -> None:
|
||||||
|
"""save nested trade record structure"""
|
||||||
|
try:
|
||||||
|
data: dict[str, dict] = {}
|
||||||
|
for symbol, sides in self.buy_cooldown.items():
|
||||||
|
data[symbol] = {}
|
||||||
|
for side in ['buy', 'sell']:
|
||||||
|
info = sides.get(side)
|
||||||
|
if not info:
|
||||||
|
continue
|
||||||
|
dt_obj = info.get('datetime')
|
||||||
|
sig = info.get('signal', '')
|
||||||
|
data[symbol][side] = {
|
||||||
|
'datetime': dt_obj.isoformat() if isinstance(dt_obj, datetime) else '',
|
||||||
|
'signal': sig,
|
||||||
|
}
|
||||||
|
with open(self.cooldown_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving cooldown data: {e}")
|
||||||
|
|
||||||
|
# ------------- Telegram -------------
|
||||||
|
def _send_coin_msg(self, text: str) -> None:
|
||||||
|
coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN)
|
||||||
|
asyncio.run(coin_client.send_message(chat_id=COIN_TELEGRAM_CHAT_ID, text=text))
|
||||||
|
|
||||||
|
def _send_stock_msg(self, text: str) -> None:
|
||||||
|
stock_client = telegram.Bot(token=STOCK_TELEGRAM_BOT_TOKEN)
|
||||||
|
asyncio.run(stock_client.send_message(chat_id=STOCK_TELEGRAM_CHAT_ID, text=text))
|
||||||
|
|
||||||
|
def sendMsg(self, msg):
|
||||||
|
try:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [msg])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending Telegram message: {str(e)}")
|
||||||
|
return
|
||||||
|
|
||||||
|
def send_coin_telegram_message(self, message_list: list[str], header: str) -> None:
|
||||||
|
payload = header + "\n"
|
||||||
|
for i, message in enumerate(message_list):
|
||||||
|
payload += message
|
||||||
|
if i + 1 % 20 == 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [payload])
|
||||||
|
payload = ''
|
||||||
|
if len(message_list) % 20 != 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_coin_msg, [payload])
|
||||||
|
|
||||||
|
def send_stock_telegram_message(self, message_list: list[str], header: str) -> None:
|
||||||
|
payload = header + "\n"
|
||||||
|
for i, message in enumerate(message_list):
|
||||||
|
payload += message + "\n"
|
||||||
|
if i + 1 % 20 == 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_stock_msg, [payload])
|
||||||
|
payload = ''
|
||||||
|
if len(message_list) % 20 != 0:
|
||||||
|
pool = Pool(12)
|
||||||
|
pool.map(self._send_stock_msg, [payload])
|
||||||
|
|
||||||
|
# ------------- Indicators -------------
|
||||||
|
def normalize_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
columns_to_normalize = ['Open', 'High', 'Low', 'Close', 'Volume']
|
||||||
|
normalized_data = data.copy()
|
||||||
|
for column in columns_to_normalize:
|
||||||
|
min_val = data[column].rolling(window=20).min()
|
||||||
|
max_val = data[column].rolling(window=20).max()
|
||||||
|
denominator = max_val - min_val
|
||||||
|
normalized_data[f'{column}_Norm'] = np.where(
|
||||||
|
denominator != 0,
|
||||||
|
(data[column] - min_val) / denominator,
|
||||||
|
0.5,
|
||||||
|
)
|
||||||
|
return normalized_data
|
||||||
|
|
||||||
|
def inverse_data(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
"""원본 data 가격 시계를 상하 대칭(글로벌 min/max 기준)으로 반전하여 하락↔상승 트렌드를 뒤집는다."""
|
||||||
|
price_cols = ['Open', 'High', 'Low', 'Close']
|
||||||
|
inv = data.copy()
|
||||||
|
global_min = data[price_cols].min().min()
|
||||||
|
global_max = data[price_cols].max().max()
|
||||||
|
# 축 기준은 global_mid = (max+min), so transformed = max+min - price
|
||||||
|
for col in price_cols:
|
||||||
|
inv[col] = global_max + global_min - data[col]
|
||||||
|
# Volume은 그대로 유지
|
||||||
|
inv['Volume'] = data['Volume']
|
||||||
|
# 지표 다시 계산
|
||||||
|
inv = self.normalize_data(inv)
|
||||||
|
|
||||||
|
inv['MA5'] = inv['Close'].rolling(window=5).mean()
|
||||||
|
inv['MA20'] = inv['Close'].rolling(window=20).mean()
|
||||||
|
inv['MA40'] = inv['Close'].rolling(window=40).mean()
|
||||||
|
inv['MA120'] = inv['Close'].rolling(window=120).mean()
|
||||||
|
inv['MA200'] = inv['Close'].rolling(window=200).mean()
|
||||||
|
inv['MA240'] = inv['Close'].rolling(window=240).mean()
|
||||||
|
inv['MA720'] = inv['Close'].rolling(window=720).mean()
|
||||||
|
inv['MA1440'] = inv['Close'].rolling(window=1440).mean()
|
||||||
|
inv['Deviation5'] = (inv['Close'] / inv['MA5']) * 100
|
||||||
|
inv['Deviation20'] = (inv['Close'] / inv['MA20']) * 100
|
||||||
|
inv['Deviation40'] = (inv['Close'] / inv['MA40']) * 100
|
||||||
|
inv['Deviation120'] = (inv['Close'] / inv['MA120']) * 100
|
||||||
|
inv['Deviation200'] = (inv['Close'] / inv['MA200']) * 100
|
||||||
|
inv['Deviation240'] = (inv['Close'] / inv['MA240']) * 100
|
||||||
|
inv['Deviation720'] = (inv['Close'] / inv['MA720']) * 100
|
||||||
|
inv['Deviation1440'] = (inv['Close'] / inv['MA1440']) * 100
|
||||||
|
inv['golden_cross'] = (inv['MA5'] > inv['MA20']) & (inv['MA5'].shift(1) <= inv['MA20'].shift(1))
|
||||||
|
inv['MA'] = inv['Close'].rolling(window=20).mean()
|
||||||
|
inv['STD'] = inv['Close'].rolling(window=20).std()
|
||||||
|
inv['Upper'] = inv['MA'] + (2 * inv['STD'])
|
||||||
|
inv['Lower'] = inv['MA'] - (2 * inv['STD'])
|
||||||
|
return inv
|
||||||
|
|
||||||
|
def calculate_technical_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
data = self.normalize_data(data)
|
||||||
|
|
||||||
|
data['MA5'] = data['Close'].rolling(window=5).mean()
|
||||||
|
data['MA20'] = data['Close'].rolling(window=20).mean()
|
||||||
|
data['MA40'] = data['Close'].rolling(window=40).mean()
|
||||||
|
data['MA120'] = data['Close'].rolling(window=120).mean()
|
||||||
|
data['MA200'] = data['Close'].rolling(window=200).mean()
|
||||||
|
data['MA240'] = data['Close'].rolling(window=240).mean()
|
||||||
|
data['MA720'] = data['Close'].rolling(window=720).mean()
|
||||||
|
data['MA1440'] = data['Close'].rolling(window=1440).mean()
|
||||||
|
data['Deviation5'] = (data['Close'] / data['MA5']) * 100
|
||||||
|
data['Deviation20'] = (data['Close'] / data['MA20']) * 100
|
||||||
|
data['Deviation40'] = (data['Close'] / data['MA40']) * 100
|
||||||
|
data['Deviation120'] = (data['Close'] / data['MA120']) * 100
|
||||||
|
data['Deviation200'] = (data['Close'] / data['MA200']) * 100
|
||||||
|
data['Deviation240'] = (data['Close'] / data['MA240']) * 100
|
||||||
|
data['Deviation720'] = (data['Close'] / data['MA720']) * 100
|
||||||
|
data['Deviation1440'] = (data['Close'] / data['MA1440']) * 100
|
||||||
|
data['golden_cross'] = (data['MA5'] > data['MA20']) & (data['MA5'].shift(1) <= data['MA20'].shift(1))
|
||||||
|
data['MA'] = data['Close'].rolling(window=20).mean()
|
||||||
|
data['STD'] = data['Close'].rolling(window=20).std()
|
||||||
|
data['Upper'] = data['MA'] + (2 * data['STD'])
|
||||||
|
data['Lower'] = data['MA'] - (2 * data['STD'])
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
# ------------- Strategy -------------
|
||||||
|
def buy_sell_ticker_1h(self, symbol: str, data: pd.DataFrame, balances=None, is_inverse: bool = False) -> bool:
|
||||||
|
try:
|
||||||
|
# 신호 생성 및 최신 포인트 확인
|
||||||
|
data = self.annotate_signals(symbol, data)
|
||||||
|
if data['point'].iloc[-1] != 1:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if is_inverse:
|
||||||
|
# BUY_MINUTE_LIMIT 이내라면 매수하지 않음
|
||||||
|
current_time = datetime.now()
|
||||||
|
last_buy_dt = self.buy_cooldown.get(symbol, {}).get('sell', {}).get('datetime')
|
||||||
|
if last_buy_dt:
|
||||||
|
time_diff = current_time - last_buy_dt
|
||||||
|
if time_diff.total_seconds() < BUY_MINUTE_LIMIT:
|
||||||
|
print(f"{symbol}: 매수 금지 중 (남은 시간: {BUY_MINUTE_LIMIT - time_diff.total_seconds():.0f}초)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 인버스 데이터: 매수 신호를 매도로 처리 (fall_6p, deviation40 만 허용)
|
||||||
|
# 허용된 인버스 매도 신호만 처리
|
||||||
|
last_signal = str(data['signal'].iloc[-1]) if 'signal' in data.columns else ''
|
||||||
|
if last_signal not in ['fall_6p', 'deviation40']:
|
||||||
|
return False
|
||||||
|
available_balance = 0
|
||||||
|
try:
|
||||||
|
if balances and symbol in balances:
|
||||||
|
available_balance = float(balances[symbol].get('balance', 0))
|
||||||
|
except Exception:
|
||||||
|
available_balance = 0
|
||||||
|
if available_balance <= 0:
|
||||||
|
return False
|
||||||
|
sell_amount = available_balance * 0.7
|
||||||
|
"""
|
||||||
|
_ = self.hts.sellCoinMarket(symbol, 0, sell_amount)
|
||||||
|
if self.cooldown_file is not None:
|
||||||
|
try:
|
||||||
|
self.last_signal[symbol] = str(data['signal'].iloc[-1])
|
||||||
|
except Exception:
|
||||||
|
self.last_signal[symbol] = ''
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})['sell'] = {'datetime': current_time, 'signal': str(data['signal'].iloc[-1])}
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{data['signal'].iloc[-1]} 매도], 현재가: {data['Close'].iloc[-1]:.4f}")
|
||||||
|
self.sendMsg("[KRW-COIN]\n" + f"• 매도 [COIN] {KR_COINS[symbol]} ({symbol}): {data['signal'].iloc[-1]} ({'₩'}{data['Close'].iloc[-1]:.4f})")
|
||||||
|
"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
check_5_week_lowest = False
|
||||||
|
|
||||||
|
# BUY_MINUTE_LIMIT 이내라면 매수하지 않음
|
||||||
|
current_time = datetime.now()
|
||||||
|
last_buy_dt = self.buy_cooldown.get(symbol, {}).get('buy', {}).get('datetime')
|
||||||
|
if last_buy_dt:
|
||||||
|
time_diff = current_time - last_buy_dt
|
||||||
|
if time_diff.total_seconds() < BUY_MINUTE_LIMIT:
|
||||||
|
print(f"{symbol}: 매수 금지 중 (남은 시간: {BUY_MINUTE_LIMIT - time_diff.total_seconds():.0f}초)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 5주봉이 20주봉이나 40주봉보다 아래에 있는지 체크
|
||||||
|
# Convert hourly data to week-based rolling periods (5, 20, 40 weeks)
|
||||||
|
hours_in_week = 24 * 7 # 168 hours
|
||||||
|
period_5w = 5 * hours_in_week # 840 hours
|
||||||
|
period_20w = 20 * hours_in_week # 3,360 hours
|
||||||
|
period_40w = 40 * hours_in_week # 6,720 hours
|
||||||
|
|
||||||
|
if len(data) >= period_40w:
|
||||||
|
wma5 = data['Close'].rolling(window=period_5w).mean().iloc[-1]
|
||||||
|
wma20 = data['Close'].rolling(window=period_20w).mean().iloc[-1]
|
||||||
|
wma40 = data['Close'].rolling(window=period_40w).mean().iloc[-1]
|
||||||
|
|
||||||
|
# 5-week MA is the lowest among 5, 20, 40 week MAs
|
||||||
|
if (wma5 < wma20) and (wma5 < wma40):
|
||||||
|
check_5_week_lowest = True
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Ignore errors in MA calculation so as not to block trading logic
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 체크: fall_6p
|
||||||
|
buy_amount = 5100
|
||||||
|
current_time = datetime.now()
|
||||||
|
if data['signal'].iloc[-1] == 'fall_6p':
|
||||||
|
if data['Close'].iloc[-1] > 100:
|
||||||
|
buy_amount = 500000
|
||||||
|
else:
|
||||||
|
buy_amount = 300000
|
||||||
|
#elif data['signal'].iloc[-1] == 'movingaverage':
|
||||||
|
# buy_amount = 10000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation40':
|
||||||
|
buy_amount = 7000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation240':
|
||||||
|
buy_amount = 6000
|
||||||
|
elif data['signal'].iloc[-1] == 'deviation1440':
|
||||||
|
if symbol in ['BONK', 'PEPE', 'TON']:
|
||||||
|
buy_amount = 7000
|
||||||
|
else:
|
||||||
|
buy_amount = 6000
|
||||||
|
|
||||||
|
if data['signal'].iloc[-1] in ['movingaverage', 'deviation40', 'deviation240', 'deviation1440']:
|
||||||
|
if check_5_week_lowest:
|
||||||
|
buy_amount *= 2
|
||||||
|
|
||||||
|
# 매수를 진행함
|
||||||
|
buy_amount = self.hts.buyCoinMarket(symbol, buy_amount)
|
||||||
|
|
||||||
|
# 최근 매수 신호를 함께 기록하여 [신규] 포맷으로 저장
|
||||||
|
if self.cooldown_file is not None:
|
||||||
|
try:
|
||||||
|
self.last_signal[symbol] = str(data['signal'].iloc[-1])
|
||||||
|
except Exception:
|
||||||
|
self.last_signal[symbol] = ''
|
||||||
|
self.buy_cooldown.setdefault(symbol, {})['buy'] = {'datetime': current_time, 'signal': str(data['signal'].iloc[-1])}
|
||||||
|
|
||||||
|
# 매수를 저장함
|
||||||
|
self._save_buy_cooldown()
|
||||||
|
|
||||||
|
print(f"{KR_COINS[symbol]} ({symbol}) [{data['signal'].iloc[-1]}], 현재가: {data['Close'].iloc[-1]:.4f}, {int(BUY_MINUTE_LIMIT/60)}분간 매수 금지 시작")
|
||||||
|
self.sendMsg("{}".format(self.format_message(symbol, KR_COINS[symbol], data['Close'].iloc[-1], data['signal'].iloc[-1], buy_amount)))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error buying {symbol}: {str(e)}")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def annotate_signals(self, symbol: str, data: pd.DataFrame, simulation: bool | None = None) -> pd.DataFrame:
|
||||||
|
data = data.copy()
|
||||||
|
data['signal'] = ''
|
||||||
|
data['point'] = 0
|
||||||
|
if data['point'].iloc[-1] != 1:
|
||||||
|
for i in range(1, len(data)):
|
||||||
|
if all(data[f'MA{n}'].iloc[i] < data['MA720'].iloc[i] for n in [5, 20, 40, 120, 200, 240]) and \
|
||||||
|
all(data[f'MA{n}'].iloc[i] > data[f'MA{n}'].iloc[i - 1] for n in [5, 20, 40, 120, 200, 240]) and \
|
||||||
|
data['MA720'].iloc[i] < data['MA1440'].iloc[i]:
|
||||||
|
data.at[data.index[i], 'signal'] = 'movingaverage'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'movingaverage'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if data['Deviation40'].iloc[i - 1] < data['Deviation40'].iloc[i] and data['Deviation40'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation40'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation40'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if symbol not in ['BONK']:
|
||||||
|
if symbol in ['TRX']:
|
||||||
|
if data['Deviation240'].iloc[i - 1] < data['Deviation240'].iloc[i] and data['Deviation240'].iloc[i - 1] <= 98:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
else:
|
||||||
|
if data['Deviation240'].iloc[i - 1] < data['Deviation240'].iloc[i] and data['Deviation240'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation240'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
if symbol in ['TON']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 89:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
elif symbol in ['XRP']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 90:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
elif symbol in ['BONK']:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 76:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
else:
|
||||||
|
if data['Deviation1440'].iloc[i - 1] < data['Deviation1440'].iloc[i] and data['Deviation1440'].iloc[i - 1] <= 80:
|
||||||
|
data.at[data.index[i], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'deviation1440'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
|
||||||
|
# Deviation720 상향 돌파 매수 (92, 93)
|
||||||
|
try:
|
||||||
|
prev_d720 = data['Deviation720'].iloc[i - 1]
|
||||||
|
curr_d720 = data['Deviation720'].iloc[i]
|
||||||
|
# 92 상향 돌파
|
||||||
|
if prev_d720 < 92 and curr_d720 >= 92:
|
||||||
|
data.at[data.index[i], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
# 93 상향 돌파
|
||||||
|
if prev_d720 < 93 and curr_d720 >= 93:
|
||||||
|
data.at[data.index[i], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'Deviation720'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
prev_low = data['Low'].iloc[i - 1]
|
||||||
|
curr_close = data['Close'].iloc[i]
|
||||||
|
curr_low = data['Low'].iloc[i]
|
||||||
|
cond_close_drop = curr_close <= prev_low * 0.94
|
||||||
|
cond_low_drop = curr_low <= prev_low * 0.94
|
||||||
|
if cond_close_drop or cond_low_drop:
|
||||||
|
data.at[data.index[i], 'signal'] = 'fall_6p'
|
||||||
|
data.at[data.index[i], 'point'] = 1
|
||||||
|
if not simulation and data['point'][-3:].sum() > 0:
|
||||||
|
data.at[data.index[-1], 'signal'] = 'fall_6p'
|
||||||
|
data.at[data.index[-1], 'point'] = 1
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return data
|
||||||
|
|
||||||
|
# ------------- Formatting -------------
|
||||||
|
def format_message(self, symbol: str, symbol_name: str, close: float, signal: str, buy_amount: float) -> str:
|
||||||
|
message = f"[매수] {symbol_name} ({symbol}): "
|
||||||
|
|
||||||
|
if int(close) >= 100:
|
||||||
|
message += f"₩{close}"
|
||||||
|
message += f" (₩{buy_amount})"
|
||||||
|
elif int(close) >= 10:
|
||||||
|
message += f"₩{close:.2f}"
|
||||||
|
message += f" (₩{buy_amount:.2f})"
|
||||||
|
elif int(close) >= 1:
|
||||||
|
message += f"₩{close:.3f}"
|
||||||
|
message += f" (₩{buy_amount:.3f})"
|
||||||
|
else:
|
||||||
|
message += f"₩{close:.4f}"
|
||||||
|
message += f" (₩{buy_amount:.4f})"
|
||||||
|
|
||||||
|
if signal != '':
|
||||||
|
message += f"[{signal}]"
|
||||||
|
return message
|
||||||
|
|
||||||
|
def format_ma_message(self, info: dict, market_type: str) -> str:
|
||||||
|
prefix = '상승 ' if info.get('alert') else ''
|
||||||
|
message = prefix + f"[{market_type}] {info['name']} ({info['symbol']}) "
|
||||||
|
message += f"{'$' if market_type == 'US' else '₩'}({info['price']:.4f}) \n"
|
||||||
|
return message
|
||||||
|
|
||||||
|
# ------------- Data fetch -------------
|
||||||
|
def get_coin_data(self, symbol: str, interval: int = 60, to: str | None = None, retries: int = 3) -> pd.DataFrame | None:
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
if to is None:
|
||||||
|
if interval == 43200:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/months?market=KRW-{}&count=200").format(symbol)
|
||||||
|
elif interval == 1440:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200").format(symbol)
|
||||||
|
else:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200").format(interval, symbol)
|
||||||
|
else:
|
||||||
|
if interval == 43200:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/months?market=KRW-{}&count=200&to={}").format(symbol, to)
|
||||||
|
elif interval == 1440:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/days?market=KRW-{}&count=200&to={}").format(symbol, to)
|
||||||
|
else:
|
||||||
|
url = ("https://api.bithumb.com/v1/candles/minutes/{}?market=KRW-{}&count=200&to={}").format(interval, symbol, to)
|
||||||
|
headers = {"accept": "application/json"}
|
||||||
|
response = requests.get(url, headers=headers)
|
||||||
|
json_data = json.loads(response.text)
|
||||||
|
df_temp = pd.DataFrame(json_data)
|
||||||
|
df_temp = df_temp.sort_index(ascending=False)
|
||||||
|
if 'candle_date_time_kst' not in df_temp:
|
||||||
|
return None
|
||||||
|
data = pd.DataFrame()
|
||||||
|
data['datetime'] = pd.to_datetime(df_temp['candle_date_time_kst'], format='%Y-%m-%dT%H:%M:%S')
|
||||||
|
data['Open'] = df_temp['opening_price']
|
||||||
|
data['Close'] = df_temp['trade_price']
|
||||||
|
data['High'] = df_temp['high_price']
|
||||||
|
data['Low'] = df_temp['low_price']
|
||||||
|
data['Volume'] = df_temp['candle_acc_trade_volume']
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.astype(float)
|
||||||
|
data["datetime"] = data.index
|
||||||
|
if not data.empty:
|
||||||
|
return data
|
||||||
|
print(f"No data received for {symbol}, attempt {attempt + 1}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_coin_more_data(self, symbol: str, interval: int, bong_count: int = 3000) -> pd.DataFrame:
|
||||||
|
to = datetime.now()
|
||||||
|
data: pd.DataFrame | None = None
|
||||||
|
while data is None or len(data) < bong_count:
|
||||||
|
if data is None:
|
||||||
|
data = self.get_coin_data(symbol, interval, to.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
else:
|
||||||
|
previous_count = len(data)
|
||||||
|
df = self.get_coin_data(symbol, interval, to.strftime("%Y-%m-%d %H:%M:%S"))
|
||||||
|
data = pd.concat([data, df], ignore_index=True)
|
||||||
|
if previous_count == len(data):
|
||||||
|
break
|
||||||
|
time.sleep(0.3)
|
||||||
|
to = to - relativedelta(minutes=interval * 200)
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.sort_index()
|
||||||
|
data = data.drop_duplicates(keep='first')
|
||||||
|
data["datetime"] = data.index
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_coin_saved_data(self, symbol: str, interval: int, data: pd.DataFrame) -> pd.DataFrame:
|
||||||
|
conn = sqlite3.connect('./resources/coins.db')
|
||||||
|
cursor = conn.cursor()
|
||||||
|
for i in range(1, len(data)):
|
||||||
|
cursor.execute("SELECT * from {}_{} where CODE = ? and ymdhms = ?".format(symbol, str(interval)), (symbol, data['datetime'].iloc[-i].strftime('%Y-%m-%d %H:%M:%S')),)
|
||||||
|
arr = cursor.fetchone()
|
||||||
|
if not arr:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO {}_{} (CODE, NAME, ymdhms, ymd, hms, close, open, high, low, volume) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)".format(symbol, interval),
|
||||||
|
(
|
||||||
|
symbol,
|
||||||
|
KR_COINS[symbol],
|
||||||
|
data['datetime'].iloc[-i].strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
data['datetime'].iloc[-i].strftime('%Y%m%d'),
|
||||||
|
data['datetime'].iloc[-i].strftime('%H%M%S'),
|
||||||
|
data['Close'].iloc[-i],
|
||||||
|
data['Open'].iloc[-i],
|
||||||
|
data['High'].iloc[-i],
|
||||||
|
data['Low'].iloc[-i],
|
||||||
|
data['Volume'].iloc[-i],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
cursor.execute("select * from (SELECT Open,Close,High,Low,Volume,ymdhms as datetime from {}_{} order by ymdhms desc limit 7000) subquery order by datetime".format(symbol, str(interval)))
|
||||||
|
result = cursor.fetchall()
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
df = pd.DataFrame(result)
|
||||||
|
df.columns = ['Open', 'Close', 'High', 'Low', 'Volume', 'datetime']
|
||||||
|
df = df.set_index('datetime')
|
||||||
|
df = df.sort_index()
|
||||||
|
df['datetime'] = df.index
|
||||||
|
return df
|
||||||
|
|
||||||
|
def get_coin_some_data(self, symbol: str, interval: int) -> pd.DataFrame:
|
||||||
|
data = self.get_coin_data(symbol, interval)
|
||||||
|
data_1 = self.get_coin_data(symbol, interval=1)
|
||||||
|
data_1.at[data_1.index[-1], 'Volume'] = data_1['Volume'].iloc[-1] * 60
|
||||||
|
|
||||||
|
saved_data = self.get_coin_saved_data(symbol, interval, data)
|
||||||
|
data = pd.concat([data, saved_data, data_1.iloc[[-1]]], ignore_index=True)
|
||||||
|
data['datetime'] = pd.to_datetime(data['datetime'], format='%Y-%m-%d %H:%M:%S')
|
||||||
|
data = data.set_index('datetime')
|
||||||
|
data = data.sort_index()
|
||||||
|
data = data.drop_duplicates(keep='first')
|
||||||
|
data["datetime"] = data.index
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_kr_stock_data(self, symbol: str, retries: int = 3) -> pd.DataFrame | None:
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
end = datetime.now()
|
||||||
|
start = end - timedelta(days=300)
|
||||||
|
data = fdr.DataReader(symbol, start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d'))
|
||||||
|
if not data.empty:
|
||||||
|
data = data.rename(columns={
|
||||||
|
'Open': 'Open',
|
||||||
|
'High': 'High',
|
||||||
|
'Low': 'Low',
|
||||||
|
'Close': 'Close',
|
||||||
|
'Volume': 'Volume',
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
print(f"No data received for {symbol}, attempt {attempt + 1}")
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
53
monitor_processor.py
Normal file
53
monitor_processor.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import time
|
||||||
|
import psutil
|
||||||
|
import subprocess
|
||||||
|
import telegram
|
||||||
|
import asyncio
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
class ProcessMonitor:
|
||||||
|
|
||||||
|
def __init__(self, python_executable="python"):
|
||||||
|
self.python = python_executable
|
||||||
|
# 실행된 프로세스 저장용
|
||||||
|
self.process_map = {}
|
||||||
|
|
||||||
|
def is_running(self, script_path):
|
||||||
|
"""해당 스크립트가 실행 중인지 확인"""
|
||||||
|
for proc in psutil.process_iter(['pid', 'name', 'cmdline']):
|
||||||
|
try:
|
||||||
|
if proc.info['cmdline'] and script_path in proc.info['cmdline']:
|
||||||
|
return True
|
||||||
|
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
def start_process(self, script_path):
|
||||||
|
"""해당 스크립트 실행"""
|
||||||
|
print(f"[INFO] Starting {script_path}")
|
||||||
|
process = subprocess.Popen([self.python, script_path], creationflags=subprocess.CREATE_NEW_CONSOLE)
|
||||||
|
self.process_map[script_path] = process
|
||||||
|
|
||||||
|
def sendMsg(self, text):
|
||||||
|
coin_client = telegram.Bot(token=COIN_TELEGRAM_BOT_TOKEN)
|
||||||
|
asyncio.run(coin_client.send_message(chat_id=COIN_TELEGRAM_CHAT_ID, text=text))
|
||||||
|
return
|
||||||
|
|
||||||
|
def monitor(self, scripts, interval=60):
|
||||||
|
"""1분 단위로 프로세스 상태 확인 및 관리"""
|
||||||
|
while True:
|
||||||
|
for script in scripts:
|
||||||
|
if not self.is_running(script):
|
||||||
|
self.sendMsg("🔔{} process is killed.".format(script))
|
||||||
|
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
monitor = ProcessMonitor()
|
||||||
|
|
||||||
|
# 모니터링할 스크립트 목록
|
||||||
|
scripts = [
|
||||||
|
r"C:\workspace\AssetMonitor\monitor_coin_1h_1.py",
|
||||||
|
r"C:\workspace\AssetMonitor\monitor_coin_1h_2.py"
|
||||||
|
]
|
||||||
|
monitor.monitor(scripts, interval=60)
|
||||||
110
monitor_stock.py
Normal file
110
monitor_stock.py
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import time
|
||||||
|
import schedule
|
||||||
|
from config import *
|
||||||
|
import FinanceDataReader as fdr
|
||||||
|
|
||||||
|
from monitor_min import Monitor
|
||||||
|
|
||||||
|
class MonitorStock (Monitor):
|
||||||
|
"""자산(코인/주식/ETF) 모니터링 및 매수 실행 클래스"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__(None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_kr_stock_data(self, symbol: str, retries: int = 3) -> pd.DataFrame | None:
|
||||||
|
for attempt in range(retries):
|
||||||
|
try:
|
||||||
|
end = datetime.now()
|
||||||
|
start = end - timedelta(days=300)
|
||||||
|
data = fdr.DataReader(symbol, start.strftime('%Y-%m-%d'), end.strftime('%Y-%m-%d'))
|
||||||
|
if not data.empty:
|
||||||
|
data = data.rename(columns={
|
||||||
|
'Open': 'Open',
|
||||||
|
'High': 'High',
|
||||||
|
'Low': 'Low',
|
||||||
|
'Close': 'Close',
|
||||||
|
'Volume': 'Volume',
|
||||||
|
})
|
||||||
|
return data
|
||||||
|
print(f"No data received for {symbol}, attempt {attempt + 1}")
|
||||||
|
time.sleep(2)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Attempt {attempt + 1} failed for {symbol}: {str(e)}")
|
||||||
|
if attempt < retries - 1:
|
||||||
|
time.sleep(5)
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ------------- Monitors -------------
|
||||||
|
def monitor_us_stocks(self) -> None:
|
||||||
|
message_list: list[str] = []
|
||||||
|
print("US Stocks {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
|
for symbol in US_STOCKS:
|
||||||
|
data = self.get_kr_stock_data(symbol)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.check_point(symbol, data)
|
||||||
|
if recent_data['point'].iloc[-1] != 1:
|
||||||
|
continue
|
||||||
|
print(f" - {US_STOCKS[symbol]} ({symbol}): {recent_data['Close'].iloc[-1]:.2f}")
|
||||||
|
message_list.append(
|
||||||
|
self.format_message('US', symbol, US_STOCKS[symbol], recent_data['Close'].iloc[-1], recent_data['signal'].iloc[-1])
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
time.sleep(0.5)
|
||||||
|
if len(message_list) > 0:
|
||||||
|
try:
|
||||||
|
self.send_stock_telegram_message(message_list, header="[US-STOCK]")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending Telegram message: {str(e)}")
|
||||||
|
|
||||||
|
def monitor_kr_stocks(self) -> None:
|
||||||
|
message_list: list[str] = []
|
||||||
|
print("KR ETFs {}".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))
|
||||||
|
for symbol in KR_ETFS:
|
||||||
|
try:
|
||||||
|
clean_symbol = symbol.replace('.KS', '')
|
||||||
|
data = self.get_kr_stock_data(clean_symbol)
|
||||||
|
if data is not None and not data.empty:
|
||||||
|
try:
|
||||||
|
data = self.calculate_technical_indicators(data)
|
||||||
|
recent_data = self.check_point(symbol, data)
|
||||||
|
if recent_data['point'].iloc[-1] != 1:
|
||||||
|
continue
|
||||||
|
print(f" - {KR_ETFS[symbol]} ({symbol}): {recent_data['Close'].iloc[-1]:.2f}")
|
||||||
|
message_list.append(
|
||||||
|
self.format_message('KR', symbol, KR_ETFS[symbol], recent_data['Close'].iloc[-1], recent_data['signal'].iloc[-1])
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error processing data for {symbol}: {str(e)}")
|
||||||
|
else:
|
||||||
|
print(f"Data for {symbol} is empty or None.")
|
||||||
|
time.sleep(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Unexpected error processing {symbol}: {str(e)}")
|
||||||
|
continue
|
||||||
|
if len(message_list) > 0:
|
||||||
|
try:
|
||||||
|
self.send_stock_telegram_message(message_list, header="[KR-STOCK]")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error sending Telegram message: {str(e)}")
|
||||||
|
|
||||||
|
# ------------- Scheduler -------------
|
||||||
|
def run_schedule(self) -> None:
|
||||||
|
schedule.every().day.at("16:30").do(self.monitor_us_stocks)
|
||||||
|
schedule.every().day.at("23:30").do(self.monitor_us_stocks)
|
||||||
|
schedule.every().day.at("08:10").do(self.monitor_kr_stocks)
|
||||||
|
schedule.every().day.at("18:20").do(self.monitor_kr_stocks)
|
||||||
|
print("Scheduler started. Stock Monitoring will run at specified times.")
|
||||||
|
while True:
|
||||||
|
schedule.run_pending()
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
MonitorStock().run_schedule()
|
||||||
14
requirements.txt
Normal file
14
requirements.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
yfinance
|
||||||
|
pandas
|
||||||
|
mplcursors
|
||||||
|
numpy
|
||||||
|
ccxt
|
||||||
|
PyJWT
|
||||||
|
pycurl
|
||||||
|
schedule
|
||||||
|
python-dateutil
|
||||||
|
python-telegram-bot
|
||||||
|
finance-datareader
|
||||||
|
psutil
|
||||||
|
mpld3
|
||||||
|
plotly
|
||||||
BIN
resources/coins.db
Normal file
BIN
resources/coins.db
Normal file
Binary file not shown.
1056
simulation_1min.py
Normal file
1056
simulation_1min.py
Normal file
File diff suppressed because it is too large
Load Diff
1097
simulation_1mon.py
Normal file
1097
simulation_1mon.py
Normal file
File diff suppressed because it is too large
Load Diff
332
simulation_30min.py
Normal file
332
simulation_30min.py
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import yfinance as yf
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
from plotly import subplots
|
||||||
|
import plotly.io as pio
|
||||||
|
from datetime import datetime
|
||||||
|
pio.renderers.default = 'browser'
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
from monitor_min import Monitor
|
||||||
|
|
||||||
|
|
||||||
|
class Simulation:
|
||||||
|
|
||||||
|
def render_plotly(self, symbol: str, interval_minutes: int, data: pd.DataFrame, inverseData: pd.DataFrame) -> None:
|
||||||
|
fig = subplots.make_subplots(
|
||||||
|
rows=3, cols=1,
|
||||||
|
subplot_titles=("캔들", "이격도/거래량", "장기 이격도"),
|
||||||
|
shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.03,
|
||||||
|
row_heights=[0.6, 0.2, 0.2]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Row 1: 캔들 + 이동평균 + 볼린저
|
||||||
|
fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name='캔들'), row=1, col=1)
|
||||||
|
for ma_col, color in [('MA5','red'),('MA20','blue'),('MA40','green'),('MA120','purple'),('MA200','brown'),('MA240','darkred'),('MA720','cyan'),('MA1440','magenta')]:
|
||||||
|
if ma_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[ma_col], name=ma_col, mode='lines', line=dict(color=color, width=1)), row=1, col=1)
|
||||||
|
if 'Lower' in data.columns and 'Upper' in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data['Lower'], name='볼린저 하단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data['Upper'], name='볼린저 상단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||||
|
|
||||||
|
# 매수 포인트
|
||||||
|
for sig, color in [('movingaverage','red'),('deviation40','orange'),('Deviation720','blue'),('deviation1440','purple'),('fall_6p','black')]:
|
||||||
|
pts = data[(data['point']==1) & (data['signal']==sig)]
|
||||||
|
if len(pts)>0:
|
||||||
|
fig.add_trace(go.Scatter(x=pts.index, y=pts['Close'], mode='markers', name=f'{sig} 매수', marker=dict(color=color, size=8, symbol='circle')), row=1, col=1)
|
||||||
|
|
||||||
|
# 매도 포인트: inverseData의 buy 신호 중 fall_6p, deviation40만 일반 그래프 가격축에 매도로 표시
|
||||||
|
inv_sell_pts = inverseData[(inverseData['point']==1) & (inverseData['signal'].isin(['deviation40','fall_6p']))]
|
||||||
|
if len(inv_sell_pts)>0:
|
||||||
|
idx = inv_sell_pts.index.intersection(data.index)
|
||||||
|
if len(idx)>0:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=idx,
|
||||||
|
y=data.loc[idx, 'Close'],
|
||||||
|
mode='markers',
|
||||||
|
name='매도',
|
||||||
|
marker=dict(color='orange', size=10, symbol='triangle-down')
|
||||||
|
),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Row 2: 이격도 + 거래량
|
||||||
|
for dev_col, color, width in [('Deviation5','red',1),('Deviation20','blue',1),('Deviation40','green',2),('Deviation120','purple',1),('Deviation200','brown',1),('Deviation720','darkred',2),('Deviation720','cyan',1),('Deviation1440','magenta',1)]:
|
||||||
|
if dev_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=dev_col, mode='lines', line=dict(color=color, width=width)), row=2, col=1)
|
||||||
|
if 'Volume' in data.columns:
|
||||||
|
fig.add_trace(go.Bar(x=data.index, y=data['Volume'], name='거래량', marker_color='lightgray', opacity=0.5), row=2, col=1)
|
||||||
|
|
||||||
|
# Row 3: 장기 이격도 및 기준선
|
||||||
|
for dev_col, color in [('Deviation720','darkred'),('Deviation1440','magenta')]:
|
||||||
|
if dev_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=f'{dev_col}(장기)', mode='lines', line=dict(color=color, width=2)), row=3, col=1)
|
||||||
|
for h, color in [(90,'red'),(95,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
|
||||||
|
# ----------------- 인버스용 트레이스 (초기 숨김) -----------------
|
||||||
|
n_orig = len(fig.data)
|
||||||
|
|
||||||
|
# Row 1: 캔들/MA/볼린저 (inverseData)
|
||||||
|
fig.add_trace(go.Candlestick(x=inverseData.index, open=inverseData['Open'], high=inverseData['High'], low=inverseData['Low'], close=inverseData['Close'], name='캔들(인버스)', showlegend=True, visible=False), row=1, col=1)
|
||||||
|
for ma_col, color in [('MA5','red'),('MA20','blue'),('MA40','green'),('MA120','purple'),('MA200','brown'),('MA240','darkred'),('MA720','cyan'),('MA1440','magenta')]:
|
||||||
|
if ma_col in inverseData.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[ma_col], name=f'{ma_col}(인버스)', mode='lines', line=dict(color=color, width=1), showlegend=True, visible=False), row=1, col=1)
|
||||||
|
if 'Lower' in inverseData.columns and 'Upper' in inverseData.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData['Lower'], name='볼린저 하단(인버스)', mode='lines', line=dict(color='grey', width=1, dash='dot'), showlegend=True, visible=False), row=1, col=1)
|
||||||
|
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData['Upper'], name='볼린저 상단(인버스)', mode='lines', line=dict(color='grey', width=1, dash='dot'), showlegend=True, visible=False), row=1, col=1)
|
||||||
|
|
||||||
|
# 인버스 매수 포인트: fall_6p, deviation40만 표시
|
||||||
|
for sig, color in [('deviation40','orange'),('fall_6p','black')]:
|
||||||
|
pts_inv = inverseData[(inverseData['point']==1) & (inverseData['signal']==sig)]
|
||||||
|
if len(pts_inv)>0:
|
||||||
|
fig.add_trace(go.Scatter(x=pts_inv.index, y=inverseData.loc[pts_inv.index,'Close'], mode='markers', name=f'{sig} 매수(인버스)', marker=dict(color=color, size=8, symbol='circle'), showlegend=True, visible=False), row=1, col=1)
|
||||||
|
|
||||||
|
# 인버스 보기에서의 매도 포인트: 일반 그래프의 매수를 인버스 그래프의 매도로 표시 (모든 매수 신호 반영)
|
||||||
|
normal_to_inv_sell = data[(data['point']==1)]
|
||||||
|
if len(normal_to_inv_sell) > 0:
|
||||||
|
idx2 = normal_to_inv_sell.index.intersection(inverseData.index)
|
||||||
|
if len(idx2) > 0:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=idx2,
|
||||||
|
y=inverseData.loc[idx2, 'Close'],
|
||||||
|
mode='markers',
|
||||||
|
name='매도(일반→인버스)',
|
||||||
|
marker=dict(color='orange', size=10, symbol='triangle-down'),
|
||||||
|
showlegend=True,
|
||||||
|
visible=False
|
||||||
|
),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Row 2: 이격도 + 거래량 (inverseData)
|
||||||
|
for dev_col, color, width in [('Deviation5','red',1),('Deviation20','blue',1),('Deviation40','green',2),('Deviation120','purple',1),('Deviation200','brown',1),('Deviation720','darkred',2),('Deviation720','cyan',1),('Deviation1440','magenta',1)]:
|
||||||
|
if dev_col in inverseData.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[dev_col], name=f'{dev_col}(인버스)', mode='lines', line=dict(color=color, width=width), showlegend=True, visible=False), row=2, col=1)
|
||||||
|
if 'Volume' in inverseData.columns:
|
||||||
|
fig.add_trace(go.Bar(x=inverseData.index, y=inverseData['Volume'], name='거래량(인버스)', marker_color='lightgray', opacity=0.5, showlegend=True, visible=False), row=2, col=1)
|
||||||
|
|
||||||
|
# Row 3: 장기 이격도 (inverseData)
|
||||||
|
for dev_col, color in [('Deviation720','darkred'),('Deviation1440','magenta')]:
|
||||||
|
if dev_col in inverseData.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=inverseData.index, y=inverseData[dev_col], name=f'{dev_col}(장기-인버스)', mode='lines', line=dict(color=color, width=2), showlegend=True, visible=False), row=3, col=1)
|
||||||
|
|
||||||
|
n_total = len(fig.data)
|
||||||
|
n_inv = n_total - n_orig
|
||||||
|
visible_orig = [True]*n_orig + [False]*n_inv
|
||||||
|
visible_inv = [False]*n_orig + [True]*n_inv
|
||||||
|
legendtitle_orig = {'text': '일반 그래프'}
|
||||||
|
legendtitle_inv = {'text': '인버스 그래프'}
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
height=1000,
|
||||||
|
margin=dict(t=180, l=40, r=240, b=40),
|
||||||
|
title=dict(
|
||||||
|
text=f"{symbol}, {interval_minutes} 분봉, ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})",
|
||||||
|
x=0.5,
|
||||||
|
xanchor='center',
|
||||||
|
y=0.995,
|
||||||
|
yanchor='top',
|
||||||
|
pad=dict(t=10, b=12)
|
||||||
|
),
|
||||||
|
xaxis_rangeslider_visible=False,
|
||||||
|
xaxis1_rangeslider_visible=False,
|
||||||
|
xaxis2_rangeslider_visible=False,
|
||||||
|
legend=dict(orientation='v', yref='paper', yanchor='top', y=1.0, xref='paper', xanchor='left', x=1.02, title=legendtitle_orig),
|
||||||
|
dragmode='zoom',
|
||||||
|
updatemenus=[dict(
|
||||||
|
type='buttons',
|
||||||
|
direction='left',
|
||||||
|
x=0.0,
|
||||||
|
xanchor='left',
|
||||||
|
y=1.11,
|
||||||
|
yanchor='top',
|
||||||
|
pad=dict(t=0, r=10, b=0, l=0),
|
||||||
|
buttons=[
|
||||||
|
dict(
|
||||||
|
label='홈',
|
||||||
|
method='update',
|
||||||
|
args=[
|
||||||
|
{'visible': visible_orig},
|
||||||
|
{
|
||||||
|
'legend': {'title': legendtitle_orig},
|
||||||
|
'xaxis.autorange': True,
|
||||||
|
'xaxis2.autorange': True,
|
||||||
|
'xaxis3.autorange': True,
|
||||||
|
'yaxis.autorange': True,
|
||||||
|
'yaxis2.autorange': True,
|
||||||
|
'yaxis3.autorange': True,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
execute=True
|
||||||
|
),
|
||||||
|
dict(
|
||||||
|
label='인버스',
|
||||||
|
method='update',
|
||||||
|
args=[
|
||||||
|
{'visible': visible_inv},
|
||||||
|
{'legend': {'title': legendtitle_inv, 'orientation': 'v', 'y': 1.0, 'yanchor': 'top', 'x': 1.02, 'xanchor': 'left'}}
|
||||||
|
],
|
||||||
|
args2=[
|
||||||
|
{'visible': visible_orig},
|
||||||
|
{'legend': {'title': legendtitle_orig, 'orientation': 'v', 'y': 1.0, 'yanchor': 'top', 'x': 1.02, 'xanchor': 'left'}}
|
||||||
|
],
|
||||||
|
execute=True
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
)
|
||||||
|
fig.update_xaxes(title_text='시간', row=3, col=1)
|
||||||
|
fig.update_yaxes(title_text='가격 (KRW)', row=1, col=1)
|
||||||
|
fig.update_yaxes(title_text='이격도/거래량', row=2, col=1)
|
||||||
|
fig.update_yaxes(title_text='장기 이격도', row=3, col=1)
|
||||||
|
|
||||||
|
fig.show(config={'scrollZoom': True, 'displaylogo': False})
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self.monitor = Monitor()
|
||||||
|
self.INTERVAL_MAP = {
|
||||||
|
60: "60m",
|
||||||
|
240: "4h",
|
||||||
|
}
|
||||||
|
|
||||||
|
def detect_turnaround_signal(self, symbol, data, interval=0, params=None):
|
||||||
|
if len(data) < 7:
|
||||||
|
return None
|
||||||
|
current_data = data.iloc[-1]
|
||||||
|
if current_data.get('point', 0) == 1:
|
||||||
|
return {
|
||||||
|
'alert': True,
|
||||||
|
'details': f"매수신호: {current_data.get('signal', 'unknown')}"
|
||||||
|
}
|
||||||
|
return {'alert': False, 'details': "매수신호 없음"}
|
||||||
|
|
||||||
|
def fetch_price_history(self, symbol: str, interval_minutes: int, days: int = 30) -> pd.DataFrame:
|
||||||
|
if symbol in KR_COINS:
|
||||||
|
bong_count = 3000
|
||||||
|
return self.monitor.get_coin_more_data(symbol, interval_minutes, bong_count=bong_count)
|
||||||
|
if interval_minutes not in self.INTERVAL_MAP:
|
||||||
|
raise ValueError("interval must be 60 or 240")
|
||||||
|
interval_str = self.INTERVAL_MAP[interval_minutes]
|
||||||
|
df = yf.download(
|
||||||
|
tickers=symbol,
|
||||||
|
period=f"{days}d",
|
||||||
|
interval=interval_str,
|
||||||
|
progress=False,
|
||||||
|
)
|
||||||
|
if df.empty:
|
||||||
|
raise RuntimeError("No data fetched. Check symbol or interval support.")
|
||||||
|
return df
|
||||||
|
|
||||||
|
def analyze_bottom_period(self, symbol: str, interval_minutes: int, days: int = 90):
|
||||||
|
data = self.fetch_price_history(symbol, interval_minutes, days)
|
||||||
|
data = self.monitor.calculate_technical_indicators(data)
|
||||||
|
data = self.monitor.annotate_signals(symbol, data, simulation=True)
|
||||||
|
print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}")
|
||||||
|
print(f"총 데이터 수: {len(data)}")
|
||||||
|
bottom_start = pd.Timestamp('2025-06-22')
|
||||||
|
bottom_end = pd.Timestamp('2025-07-09')
|
||||||
|
bottom_data = data[(data.index >= bottom_start) & (data.index <= bottom_end)]
|
||||||
|
if len(bottom_data) == 0:
|
||||||
|
print("저점 기간 데이터가 없습니다.")
|
||||||
|
return None, []
|
||||||
|
print(f"\n저점 기간 데이터: {bottom_data.index[0]} ~ {bottom_data.index[-1]}")
|
||||||
|
print(f"저점 기간 데이터 수: {len(bottom_data)}")
|
||||||
|
print("\n=== 저점 기간 기술적 지표 분석 ===")
|
||||||
|
min_price = bottom_data['Low'].min()
|
||||||
|
max_price = bottom_data['High'].max()
|
||||||
|
avg_price = bottom_data['Close'].mean()
|
||||||
|
print(f"최저가: {min_price:.4f}")
|
||||||
|
print(f"최고가: {max_price:.4f}")
|
||||||
|
print(f"평균가: {avg_price:.4f}")
|
||||||
|
print(f"가격 변동폭: {((max_price - min_price) / min_price * 100):.2f}%")
|
||||||
|
bb_lower_min = bottom_data['Lower'].min()
|
||||||
|
bb_upper_max = bottom_data['Upper'].max()
|
||||||
|
print(f"\n볼린저 밴드 분석:")
|
||||||
|
print(f"하단 밴드 최저: {bb_lower_min:.4f}")
|
||||||
|
print(f"상단 밴드 최고: {bb_upper_max:.4f}")
|
||||||
|
volume_avg = bottom_data['Volume'].mean()
|
||||||
|
volume_max = bottom_data['Volume'].max()
|
||||||
|
print(f"\n거래량 분석:")
|
||||||
|
print(f"평균 거래량: {volume_avg:.0f}")
|
||||||
|
print(f"최대 거래량: {volume_max:.0f}")
|
||||||
|
actual_bottom_idx = bottom_data['Low'].idxmin()
|
||||||
|
actual_bottom_price = bottom_data.loc[actual_bottom_idx, 'Low']
|
||||||
|
actual_bottom_date = actual_bottom_idx
|
||||||
|
print(f"\n실제 저점:")
|
||||||
|
print(f"날짜: {actual_bottom_date}")
|
||||||
|
print(f"가격: {actual_bottom_price:.4f}")
|
||||||
|
print(f"볼린저 하단 대비: {((actual_bottom_price - bottom_data.loc[actual_bottom_idx, 'Lower']) / bottom_data.loc[actual_bottom_idx, 'Lower'] * 100):.2f}%")
|
||||||
|
print(f"\n=== 매수 신호 분석 ===")
|
||||||
|
bottom_alerts = bottom_data[bottom_data['point'] == 1]
|
||||||
|
alerts = [(idx, row['Close']) for idx, row in bottom_alerts.iterrows()]
|
||||||
|
print(f"저점 기간 매수 신호 수: {len(alerts)}")
|
||||||
|
if alerts:
|
||||||
|
print("매수 신호 발생 시점:")
|
||||||
|
for date, price in alerts:
|
||||||
|
print(f" {date}: {price:.4f}")
|
||||||
|
return bottom_data, alerts
|
||||||
|
|
||||||
|
def run_simulation(self, symbol: str, interval_minutes: int, days: int = 30):
|
||||||
|
data = self.fetch_price_history(symbol, interval_minutes)
|
||||||
|
|
||||||
|
inverseData = self.monitor.inverse_data(data)
|
||||||
|
inverseData = self.monitor.annotate_signals(symbol, inverseData, simulation=True)
|
||||||
|
|
||||||
|
data = self.monitor.calculate_technical_indicators(data)
|
||||||
|
data = self.monitor.annotate_signals(symbol, data, simulation=True)
|
||||||
|
print(f"데이터 기간: {data.index[0]} ~ {data.index[-1]}")
|
||||||
|
print(f"총 데이터 수: {len(data)}")
|
||||||
|
alerts = []
|
||||||
|
for i in range(len(data)):
|
||||||
|
if data['point'].iloc[i] == 1:
|
||||||
|
alerts.append((data.index[i], data['Close'].iloc[i]))
|
||||||
|
print(f"\n총 매수 신호 수: {len(alerts)}")
|
||||||
|
ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')])
|
||||||
|
dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')])
|
||||||
|
dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'Deviation720')])
|
||||||
|
dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')])
|
||||||
|
print(f" - MA 신호: {ma_signals}")
|
||||||
|
print(f" - Dev40 신호: {dev40_signals}")
|
||||||
|
print(f" - Dev240 신호: {dev240_signals}")
|
||||||
|
print(f" - Dev1440 신호: {dev1440_signals}")
|
||||||
|
|
||||||
|
# Plotly 기반 시각화로 전환
|
||||||
|
self.render_plotly(symbol, interval_minutes, data, inverseData)
|
||||||
|
return
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sim = Simulation()
|
||||||
|
interval = 60
|
||||||
|
days = 90
|
||||||
|
target_coins = ['XRP']
|
||||||
|
show_graphs = True
|
||||||
|
for symbol in target_coins:
|
||||||
|
print(f"\n=== {symbol} 저점 기간 분석 시작 ===")
|
||||||
|
try:
|
||||||
|
bottom_data, alerts = sim.analyze_bottom_period(symbol, interval, days)
|
||||||
|
print(f"\n=== {symbol} 전체 기간 시뮬레이션 ===")
|
||||||
|
if show_graphs:
|
||||||
|
sim.run_simulation(symbol, interval, days)
|
||||||
|
else:
|
||||||
|
data = sim.fetch_price_history(symbol, interval, days)
|
||||||
|
|
||||||
|
inverseData = sim.monitor.inverse_data(data)
|
||||||
|
inverseData = sim.monitor.annotate_signals(symbol, inverseData, simulation=True)
|
||||||
|
|
||||||
|
data = sim.monitor.calculate_technical_indicators(data)
|
||||||
|
data = sim.monitor.annotate_signals(symbol, data, simulation=True)
|
||||||
|
|
||||||
|
total_signals = len(data[data['point'] == 1])
|
||||||
|
ma_signals = len(data[(data['point'] == 1) & (data['signal'] == 'movingaverage')])
|
||||||
|
dev40_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation40')])
|
||||||
|
dev240_signals = len(data[(data['point'] == 1) & (data['signal'] == 'Deviation720')])
|
||||||
|
dev1440_signals = len(data[(data['point'] == 1) & (data['signal'] == 'deviation1440')])
|
||||||
|
print(f"총 매수 신호: {total_signals}")
|
||||||
|
print(f" - MA 신호: {ma_signals}")
|
||||||
|
print(f" - Dev40 신호: {dev40_signals}")
|
||||||
|
print(f" - Dev240 신호: {dev240_signals}")
|
||||||
|
print(f" - Dev1440 신호: {dev1440_signals}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error analyzing {symbol}: {str(e)}")
|
||||||
746
simulation_coin.py
Normal file
746
simulation_coin.py
Normal file
@@ -0,0 +1,746 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import plotly.graph_objs as go
|
||||||
|
from plotly import subplots
|
||||||
|
import plotly.io as pio
|
||||||
|
from datetime import datetime
|
||||||
|
pio.renderers.default = 'browser'
|
||||||
|
|
||||||
|
from config import *
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 시뮬레이션 설정 - 여기서 코인과 파라미터를 변경하세요
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
# 코인 선택 (대문자로 입력)
|
||||||
|
COINS = ['XRP', 'ADA', 'APT', 'AVAX', 'BONK', 'BTC', 'ETC', 'HBAR', 'LINK', 'ONDO', 'PENGU', 'SEI', 'SOL', 'SUI', 'TRX', 'VIRTUAL', 'WLD', 'XLM']
|
||||||
|
COIN = COINS[0]
|
||||||
|
|
||||||
|
# 시뮬레이션 설정
|
||||||
|
INTERVAL = 3 # 분봉 간격 (3분봉 기준으로 그래프 표시)
|
||||||
|
BONG_COUNT = 10000 # 분석할 일수
|
||||||
|
SHOW_GRAPHS = True # 그래프 표시 여부
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 코인별 전략 클래스들
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
class CoinStrategy:
|
||||||
|
"""통합 코인 전략 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, coin: str):
|
||||||
|
self.coin = coin
|
||||||
|
self.name = KR_COINS.get(coin, coin)
|
||||||
|
|
||||||
|
def get_buy_amount(self, signal: str, current_price: float, check_5_week_lowest: bool = False) -> float:
|
||||||
|
"""코인별 매수 금액을 결정합니다."""
|
||||||
|
base_amount = 0
|
||||||
|
|
||||||
|
# 코인별 매수 금액 설정 (XRP 최적화 전략)
|
||||||
|
if self.coin == 'XRP':
|
||||||
|
# 3분봉 신호들 (단기 스캘핑 - 소액 진입)
|
||||||
|
if signal == 'deviation1440_3m':
|
||||||
|
base_amount = 22000 # 강한 이격도 과매도
|
||||||
|
elif signal == 'absolute_bottom_3m':
|
||||||
|
base_amount = 20000 # 절대 최저점 포착 (9월 26일)
|
||||||
|
elif signal == 'rsi_oversold_3m':
|
||||||
|
base_amount = 12000 # RSI 과매도 + 반등
|
||||||
|
# 1440분봉 신호들 (중장기 추세 - 대량 진입)
|
||||||
|
elif signal == 'deviation1440_1d':
|
||||||
|
base_amount = 35000 # 강한 이격도 과매도
|
||||||
|
elif signal == 'absolute_bottom_1d':
|
||||||
|
base_amount = 35000 # 절대 최저점 포착 (9월 26일)
|
||||||
|
elif signal == 'macd_golden_1d':
|
||||||
|
base_amount = 25000 # MACD 골든크로스 + MA
|
||||||
|
# 기존 신호들 (하위 호환성)
|
||||||
|
elif signal == 'fall_6p':
|
||||||
|
base_amount = 300000 if current_price > 100 else 150000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 7000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 35000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 25000
|
||||||
|
else:
|
||||||
|
base_amount = 5000
|
||||||
|
|
||||||
|
elif self.coin == 'ADA':
|
||||||
|
# 3분봉 신호들
|
||||||
|
if signal == 'fall_5p_3m':
|
||||||
|
base_amount = 70000 if current_price > 1000 else 35000
|
||||||
|
elif signal == 'movingaverage_3m':
|
||||||
|
base_amount = 4800
|
||||||
|
elif signal == 'deviation40_3m':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation240_3m':
|
||||||
|
base_amount = 3600
|
||||||
|
elif signal == 'deviation1440_3m':
|
||||||
|
base_amount = 18000
|
||||||
|
elif signal == 'Deviation720_3m':
|
||||||
|
base_amount = 12000
|
||||||
|
# 1440분봉 신호들
|
||||||
|
elif signal == 'fall_6p_1d':
|
||||||
|
base_amount = 200000 if current_price > 1000 else 100000
|
||||||
|
elif signal == 'movingaverage_1d':
|
||||||
|
base_amount = 8000
|
||||||
|
elif signal == 'deviation40_1d':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'deviation240_1d':
|
||||||
|
base_amount = 6000
|
||||||
|
elif signal == 'deviation1440_1d':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'Deviation720_1d':
|
||||||
|
base_amount = 20000
|
||||||
|
# 기존 신호들
|
||||||
|
elif signal == 'fall_6p':
|
||||||
|
base_amount = 200000 if current_price > 1000 else 100000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 8000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 6000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 20000
|
||||||
|
else:
|
||||||
|
base_amount = 4000
|
||||||
|
|
||||||
|
elif self.coin == 'APT':
|
||||||
|
# 3분봉 신호들
|
||||||
|
if signal == 'fall_5p_3m':
|
||||||
|
base_amount = 140000 if current_price > 5000 else 70000
|
||||||
|
elif signal == 'movingaverage_3m':
|
||||||
|
base_amount = 9000
|
||||||
|
elif signal == 'deviation40_3m':
|
||||||
|
base_amount = 24000
|
||||||
|
elif signal == 'deviation240_3m':
|
||||||
|
base_amount = 6000
|
||||||
|
elif signal == 'deviation1440_3m':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'Deviation720_3m':
|
||||||
|
base_amount = 18000
|
||||||
|
# 1440분봉 신호들
|
||||||
|
elif signal == 'fall_6p_1d':
|
||||||
|
base_amount = 400000 if current_price > 5000 else 200000
|
||||||
|
elif signal == 'movingaverage_1d':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation40_1d':
|
||||||
|
base_amount = 40000
|
||||||
|
elif signal == 'deviation240_1d':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation1440_1d':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'Deviation720_1d':
|
||||||
|
base_amount = 30000
|
||||||
|
# 기존 신호들
|
||||||
|
elif signal == 'fall_6p':
|
||||||
|
base_amount = 400000 if current_price > 5000 else 200000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 40000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 30000
|
||||||
|
else:
|
||||||
|
base_amount = 8000
|
||||||
|
|
||||||
|
elif self.coin == 'AVAX':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 500000 if current_price > 30000 else 300000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 20000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 60000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 40000
|
||||||
|
else:
|
||||||
|
base_amount = 10000
|
||||||
|
|
||||||
|
elif self.coin == 'BONK':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 200000 if current_price > 0.03 else 150000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 7000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 20000
|
||||||
|
else:
|
||||||
|
base_amount = 5000
|
||||||
|
|
||||||
|
elif self.coin == 'BTC':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 1000000 if current_price > 150000000 else 800000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 100000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 120000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 80000
|
||||||
|
else:
|
||||||
|
base_amount = 20000
|
||||||
|
|
||||||
|
elif self.coin == 'ETC':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 300000 if current_price > 25000 else 200000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 60000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 20000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 70000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 50000
|
||||||
|
else:
|
||||||
|
base_amount = 15000
|
||||||
|
|
||||||
|
elif self.coin == 'HBAR':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 150000 if current_price > 300 else 100000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 35000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 25000
|
||||||
|
else:
|
||||||
|
base_amount = 8000
|
||||||
|
|
||||||
|
elif self.coin == 'LINK':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 400000 if current_price > 30000 else 300000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 40000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 80000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 90000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 60000
|
||||||
|
else:
|
||||||
|
base_amount = 20000
|
||||||
|
|
||||||
|
elif self.coin == 'ONDO':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 180000 if current_price > 1300 else 120000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 18000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 35000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 12000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 40000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 28000
|
||||||
|
else:
|
||||||
|
base_amount = 10000
|
||||||
|
|
||||||
|
elif self.coin == 'PENGU':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 120000 if current_price > 40 else 80000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 12000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 25000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 8000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 28000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 20000
|
||||||
|
else:
|
||||||
|
base_amount = 6000
|
||||||
|
|
||||||
|
elif self.coin == 'SEI':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 160000 if current_price > 400 else 120000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 16000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 32000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 11000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 36000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 25000
|
||||||
|
else:
|
||||||
|
base_amount = 8000
|
||||||
|
|
||||||
|
elif self.coin == 'SOL':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 500000 if current_price > 300000 else 400000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 100000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 120000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 80000
|
||||||
|
else:
|
||||||
|
base_amount = 20000
|
||||||
|
|
||||||
|
elif self.coin == 'SUI':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 220000 if current_price > 4800 else 180000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 22000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 45000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 50000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 35000
|
||||||
|
else:
|
||||||
|
base_amount = 12000
|
||||||
|
|
||||||
|
elif self.coin == 'TRX':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 140000 if current_price > 450 else 100000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 14000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 28000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 32000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 22000
|
||||||
|
else:
|
||||||
|
base_amount = 7000
|
||||||
|
|
||||||
|
elif self.coin == 'VIRTUAL':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 190000 if current_price > 1600 else 130000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 19000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 38000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 13000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 42000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 30000
|
||||||
|
else:
|
||||||
|
base_amount = 10000
|
||||||
|
|
||||||
|
elif self.coin == 'WLD':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 200000 if current_price > 1800 else 150000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 20000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 40000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 14000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 45000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 32000
|
||||||
|
else:
|
||||||
|
base_amount = 10000
|
||||||
|
|
||||||
|
elif self.coin == 'XLM':
|
||||||
|
if signal == 'fall_6p':
|
||||||
|
base_amount = 150000 if current_price > 500 else 100000
|
||||||
|
elif signal == 'movingaverage':
|
||||||
|
base_amount = 15000
|
||||||
|
elif signal == 'deviation40':
|
||||||
|
base_amount = 30000
|
||||||
|
elif signal == 'deviation240':
|
||||||
|
base_amount = 10000
|
||||||
|
elif signal == 'deviation1440':
|
||||||
|
base_amount = 35000
|
||||||
|
elif signal == 'Deviation720':
|
||||||
|
base_amount = 25000
|
||||||
|
else:
|
||||||
|
base_amount = 8000
|
||||||
|
else:
|
||||||
|
# 기본값
|
||||||
|
base_amount = 10000
|
||||||
|
|
||||||
|
# 5주봉이 가장 낮을 때 매수 금액 2배 (시간봉별 신호 포함)
|
||||||
|
if check_5_week_lowest and signal in ['movingaverage_3m', 'movingaverage_1d', 'deviation40_3m', 'deviation40_1d',
|
||||||
|
'deviation240_3m', 'deviation240_1d', 'deviation1440_3m', 'deviation1440_1d',
|
||||||
|
'movingaverage', 'deviation40', 'deviation240', 'deviation1440']:
|
||||||
|
base_amount *= 2
|
||||||
|
|
||||||
|
return base_amount
|
||||||
|
|
||||||
|
def get_sell_signals(self) -> list:
|
||||||
|
"""매도 신호 목록을 반환합니다."""
|
||||||
|
return ['deviation1440_3m', 'absolute_bottom_3m', 'rsi_oversold_3m',
|
||||||
|
'deviation1440_1d', 'absolute_bottom_1d', 'macd_golden_1d']
|
||||||
|
|
||||||
|
def get_sell_amount_ratio(self, signal: str) -> float:
|
||||||
|
"""매도 시 판매할 비율을 반환합니다."""
|
||||||
|
# 3분봉 신호들 (빠른 회전 - 높은 매도 비율)
|
||||||
|
if signal in ['deviation1440_3m', 'absolute_bottom_3m', 'rsi_oversold_3m']:
|
||||||
|
return 0.8 # 80% 매도
|
||||||
|
# 1440분봉 신호들 (여유 있는 수익 실현 - 낮은 매도 비율)
|
||||||
|
elif signal in ['deviation1440_1d', 'absolute_bottom_1d', 'macd_golden_1d']:
|
||||||
|
return 0.6 # 60% 매도
|
||||||
|
else:
|
||||||
|
return 0.5 # 기본 50% 매도
|
||||||
|
|
||||||
|
def check_coin_specific_signals(self, data: pd.DataFrame, i: int) -> tuple:
|
||||||
|
"""코인별 전용 매수 신호를 확인합니다."""
|
||||||
|
signal = ''
|
||||||
|
point = 0
|
||||||
|
|
||||||
|
# Deviation720_strong과 Deviation720_very_strong 신호는 제거됨
|
||||||
|
# 기본 신호만 사용: movingaverage, deviation40, Deviation720, deviation1440, fall_6p
|
||||||
|
|
||||||
|
return signal, point
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 통합 시뮬레이션 클래스
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
class CoinSimulation:
|
||||||
|
"""통합 코인 시뮬레이션 클래스"""
|
||||||
|
|
||||||
|
def __init__(self, coin: str) -> None:
|
||||||
|
self.coin = coin
|
||||||
|
self.strategy = CoinStrategy(coin)
|
||||||
|
# 모니터 클래스 동적 임포트
|
||||||
|
self._import_monitor()
|
||||||
|
|
||||||
|
def _import_monitor(self):
|
||||||
|
"""코인별 모니터 클래스를 동적으로 임포트합니다."""
|
||||||
|
try:
|
||||||
|
if self.coin == 'XRP':
|
||||||
|
from coin_xrp import XRPMonitor
|
||||||
|
self.monitor = XRPMonitor()
|
||||||
|
elif self.coin == 'ADA':
|
||||||
|
from coin_ada import ADAMonitor
|
||||||
|
self.monitor = ADAMonitor()
|
||||||
|
elif self.coin == 'APT':
|
||||||
|
from coin_apt import APTMonitor
|
||||||
|
self.monitor = APTMonitor()
|
||||||
|
elif self.coin == 'AVAX':
|
||||||
|
from coin_avax import AVAXMonitor
|
||||||
|
self.monitor = AVAXMonitor()
|
||||||
|
elif self.coin == 'BONK':
|
||||||
|
from coin_bonk import BONKMonitor
|
||||||
|
self.monitor = BONKMonitor()
|
||||||
|
elif self.coin == 'BTC':
|
||||||
|
from coin_btc import BTCMonitor
|
||||||
|
self.monitor = BTCMonitor()
|
||||||
|
elif self.coin == 'ETC':
|
||||||
|
from coin_etc import ETCMonitor
|
||||||
|
self.monitor = ETCMonitor()
|
||||||
|
elif self.coin == 'HBAR':
|
||||||
|
from coin_hbar import HBARMonitor
|
||||||
|
self.monitor = HBARMonitor()
|
||||||
|
elif self.coin == 'LINK':
|
||||||
|
from coin_link import LINKMonitor
|
||||||
|
self.monitor = LINKMonitor()
|
||||||
|
elif self.coin == 'ONDO':
|
||||||
|
from coin_ondo import ONDOMonitor
|
||||||
|
self.monitor = ONDOMonitor()
|
||||||
|
elif self.coin == 'PENGU':
|
||||||
|
from coin_pengu import PENGUMonitor
|
||||||
|
self.monitor = PENGUMonitor()
|
||||||
|
elif self.coin == 'SEI':
|
||||||
|
from coin_sei import SEIMonitor
|
||||||
|
self.monitor = SEIMonitor()
|
||||||
|
elif self.coin == 'SOL':
|
||||||
|
from coin_sol import SOLMonitor
|
||||||
|
self.monitor = SOLMonitor()
|
||||||
|
elif self.coin == 'SUI':
|
||||||
|
from coin_sui import SUIMonitor
|
||||||
|
self.monitor = SUIMonitor()
|
||||||
|
elif self.coin == 'TRX':
|
||||||
|
from coin_trx import TRXMonitor
|
||||||
|
self.monitor = TRXMonitor()
|
||||||
|
elif self.coin == 'VIRTUAL':
|
||||||
|
from coin_virtual import VIRTUALMonitor
|
||||||
|
self.monitor = VIRTUALMonitor()
|
||||||
|
elif self.coin == 'WLD':
|
||||||
|
from coin_wld import WLDMonitor
|
||||||
|
self.monitor = WLDMonitor()
|
||||||
|
elif self.coin == 'XLM':
|
||||||
|
from coin_xlm import XLMMonitor
|
||||||
|
self.monitor = XLMMonitor()
|
||||||
|
else:
|
||||||
|
raise ValueError(f"지원하지 않는 코인: {self.coin}")
|
||||||
|
except ImportError as e:
|
||||||
|
raise ImportError(f"코인 모니터 클래스를 찾을 수 없습니다: {e}")
|
||||||
|
|
||||||
|
def render_plotly(self, symbol: str, interval_minutes: int, data: pd.DataFrame, inverseData: pd.DataFrame) -> None:
|
||||||
|
"""코인별 Plotly 차트 렌더링"""
|
||||||
|
fig = subplots.make_subplots(
|
||||||
|
rows=3, cols=1,
|
||||||
|
subplot_titles=(f"{self.coin} 캔들차트", "이격도/거래량", "장기 이격도"),
|
||||||
|
shared_xaxes=True, horizontal_spacing=0.03, vertical_spacing=0.03,
|
||||||
|
row_heights=[0.6, 0.2, 0.2]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Row 1: 캔들 + 이동평균 + 볼린저
|
||||||
|
fig.add_trace(go.Candlestick(x=data.index, open=data['Open'], high=data['High'], low=data['Low'], close=data['Close'], name=f'{self.coin} 캔들'), row=1, col=1)
|
||||||
|
|
||||||
|
# 이동평균선 표시
|
||||||
|
for ma_col, color in [('MA5','red'),('MA20','blue'),('MA40','green'),('MA120','purple'),('MA200','brown'),('MA240','darkred'),('MA720','cyan'),('MA1440','magenta')]:
|
||||||
|
if ma_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[ma_col], name=ma_col, mode='lines', line=dict(color=color, width=1)), row=1, col=1)
|
||||||
|
|
||||||
|
# 볼린저 밴드
|
||||||
|
if 'Lower' in data.columns and 'Upper' in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data['Lower'], name='볼린저 하단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data['Upper'], name='볼린저 상단', mode='lines', line=dict(color='grey', width=1, dash='dot')), row=1, col=1)
|
||||||
|
|
||||||
|
# 매수 포인트 (XRP 최적화 신호)
|
||||||
|
buy_signals = ['deviation1440_3m', 'absolute_bottom_3m', 'rsi_oversold_3m',
|
||||||
|
'deviation1440_1d', 'absolute_bottom_1d', 'macd_golden_1d',
|
||||||
|
'movingaverage_3m', 'movingaverage_1d', 'deviation40_3m', 'deviation40_1d',
|
||||||
|
'Deviation720_3m', 'Deviation720_1d', 'fall_5p_3m', 'fall_6p_1d',
|
||||||
|
'movingaverage', 'deviation40', 'Deviation720', 'deviation1440', 'fall_6p']
|
||||||
|
for sig, color in [('deviation1440_3m','purple'),('absolute_bottom_3m','cyan'),('rsi_oversold_3m','magenta'),
|
||||||
|
('deviation1440_1d','darkviolet'),('absolute_bottom_1d','lime'),('macd_golden_1d','gold'),
|
||||||
|
('movingaverage_3m','red'),('movingaverage_1d','darkred'),('deviation40_3m','orange'),('deviation40_1d','darkorange'),
|
||||||
|
('Deviation720_3m','blue'),('Deviation720_1d','darkblue'),('fall_5p_3m','black'),('fall_6p_1d','gray'),
|
||||||
|
('movingaverage','red'),('deviation40','orange'),('Deviation720','blue'),('deviation1440','purple'),('fall_6p','black')]:
|
||||||
|
pts = data[(data['point']==1) & (data['signal']==sig)]
|
||||||
|
if len(pts)>0:
|
||||||
|
fig.add_trace(go.Scatter(x=pts.index, y=pts['Close'], mode='markers', name=f'{sig} 매수', marker=dict(color=color, size=8, symbol='circle')), row=1, col=1)
|
||||||
|
|
||||||
|
# 매도 포인트
|
||||||
|
sell_signals = self.strategy.get_sell_signals()
|
||||||
|
inv_sell_pts = inverseData[(inverseData['point']==1) & (inverseData['signal'].isin(sell_signals))]
|
||||||
|
if len(inv_sell_pts)>0:
|
||||||
|
idx = inv_sell_pts.index.intersection(data.index)
|
||||||
|
if len(idx)>0:
|
||||||
|
fig.add_trace(
|
||||||
|
go.Scatter(
|
||||||
|
x=idx,
|
||||||
|
y=data.loc[idx, 'Close'],
|
||||||
|
mode='markers',
|
||||||
|
name=f'{self.coin} 매도',
|
||||||
|
marker=dict(color='orange', size=10, symbol='triangle-down')
|
||||||
|
),
|
||||||
|
row=1, col=1
|
||||||
|
)
|
||||||
|
|
||||||
|
# Row 2: 이격도 + 거래량
|
||||||
|
for dev_col, color, width in [('Deviation5','red',1),('Deviation20','blue',1),('Deviation40','green',2),('Deviation120','purple',1),('Deviation200','brown',1),('Deviation720','darkred',2),('Deviation1440','magenta',1)]:
|
||||||
|
if dev_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=dev_col, mode='lines', line=dict(color=color, width=width)), row=2, col=1)
|
||||||
|
if 'Volume' in data.columns:
|
||||||
|
fig.add_trace(go.Bar(x=data.index, y=data['Volume'], name='거래량', marker_color='lightgray', opacity=0.5), row=2, col=1)
|
||||||
|
|
||||||
|
# Row 3: 장기 이격도 및 기준선
|
||||||
|
for dev_col, color in [('Deviation720','darkred'),('Deviation1440','magenta')]:
|
||||||
|
if dev_col in data.columns:
|
||||||
|
fig.add_trace(go.Scatter(x=data.index, y=data[dev_col], name=f'{dev_col}(장기)', mode='lines', line=dict(color=color, width=2)), row=3, col=1)
|
||||||
|
|
||||||
|
# 코인별 기준선
|
||||||
|
if self.coin == 'XRP':
|
||||||
|
for h, color in [(96,'red'),(97,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'ADA':
|
||||||
|
for h, color in [(98,'red'),(99,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'APT':
|
||||||
|
for h, color in [(110,'red'),(115,'green'),(120,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'AVAX':
|
||||||
|
for h, color in [(95,'red'),(97,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'BONK':
|
||||||
|
for h, color in [(92,'red'),(95,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'BTC':
|
||||||
|
for h, color in [(105,'red'),(108,'green'),(110,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'ETC':
|
||||||
|
for h, color in [(94,'red'),(96,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'HBAR':
|
||||||
|
for h, color in [(98,'red'),(100,'green'),(102,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'LINK':
|
||||||
|
for h, color in [(95,'red'),(97,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'ONDO':
|
||||||
|
for h, color in [(96,'red'),(98,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'PENGU':
|
||||||
|
for h, color in [(91,'red'),(93,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'SEI':
|
||||||
|
for h, color in [(97,'red'),(99,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'SOL':
|
||||||
|
for h, color in [(104,'red'),(106,'green'),(110,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'SUI':
|
||||||
|
for h, color in [(99,'red'),(101,'green'),(102,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'TRX':
|
||||||
|
for h, color in [(96,'red'),(98,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'VIRTUAL':
|
||||||
|
for h, color in [(93,'red'),(95,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'WLD':
|
||||||
|
for h, color in [(94,'red'),(96,'green'),(100,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
elif self.coin == 'XLM':
|
||||||
|
for h, color in [(98,'red'),(100,'green'),(102,'black')]:
|
||||||
|
fig.add_hline(y=h, line_width=1, line_dash='dash', line_color=color, row=3, col=1)
|
||||||
|
|
||||||
|
fig.update_layout(
|
||||||
|
height=1000,
|
||||||
|
margin=dict(t=180, l=40, r=240, b=40),
|
||||||
|
title=dict(
|
||||||
|
text=f"{self.coin} ({symbol}), {interval_minutes} 분봉, ({datetime.now().strftime('%Y-%m-%d %H:%M:%S')})",
|
||||||
|
x=0.5,
|
||||||
|
xanchor='center',
|
||||||
|
y=0.995,
|
||||||
|
yanchor='top',
|
||||||
|
pad=dict(t=10, b=12)
|
||||||
|
),
|
||||||
|
xaxis_rangeslider_visible=False,
|
||||||
|
xaxis1_rangeslider_visible=False,
|
||||||
|
xaxis2_rangeslider_visible=False,
|
||||||
|
legend=dict(orientation='v', yref='paper', yanchor='top', y=1.0, xref='paper', xanchor='left', x=1.02),
|
||||||
|
dragmode='zoom'
|
||||||
|
)
|
||||||
|
fig.update_xaxes(title_text='시간', row=3, col=1)
|
||||||
|
fig.update_yaxes(title_text='가격 (KRW)', row=1, col=1)
|
||||||
|
fig.update_yaxes(title_text='이격도/거래량', row=2, col=1)
|
||||||
|
fig.update_yaxes(title_text='장기 이격도', row=3, col=1)
|
||||||
|
|
||||||
|
fig.show(config={'scrollZoom': True, 'displaylogo': False})
|
||||||
|
|
||||||
|
def fetch_coin_price_history(self, symbol: str, interval_minutes: int) -> pd.DataFrame:
|
||||||
|
"""코인 가격 히스토리를 가져옵니다."""
|
||||||
|
return self.monitor.get_coin_more_data(symbol, interval_minutes, bong_count=BONG_COUNT)
|
||||||
|
|
||||||
|
def run_coin_simulation(self, symbol: str, interval_minutes: int):
|
||||||
|
"""코인 시뮬레이션 실행"""
|
||||||
|
data = self.fetch_coin_price_history(symbol, interval_minutes)
|
||||||
|
|
||||||
|
# 인버스 데이터 처리 (시간봉별 신호 포함)
|
||||||
|
inverseData = self.monitor.inverse_data(data)
|
||||||
|
inverseData = self.monitor.annotate_signals(symbol, interval_minutes, inverseData, simulation=True)
|
||||||
|
|
||||||
|
# 일반 데이터 처리 (시간봉별 신호 포함)
|
||||||
|
data = self.monitor.calculate_technical_indicators(data)
|
||||||
|
data = self.monitor.annotate_signals(symbol, interval_minutes, data, simulation=True)
|
||||||
|
|
||||||
|
# 코인 전용 신호 추가
|
||||||
|
for i in range(1, len(data)):
|
||||||
|
coin_signal, coin_point = self.strategy.check_coin_specific_signals(data, i)
|
||||||
|
if coin_point == 1:
|
||||||
|
data.at[data.index[i], 'signal'] = coin_signal
|
||||||
|
data.at[data.index[i], 'point'] = coin_point
|
||||||
|
|
||||||
|
print(f"{self.coin} 데이터 기간: {data.index[0]} ~ {data.index[-1]}")
|
||||||
|
print(f"총 데이터 수: {len(data)}")
|
||||||
|
|
||||||
|
# 매수 신호 분석
|
||||||
|
alerts = []
|
||||||
|
total_buy_amount = 0
|
||||||
|
for i in range(len(data)):
|
||||||
|
if data['point'].iloc[i] == 1:
|
||||||
|
signal = data['signal'].iloc[i]
|
||||||
|
price = data['Close'].iloc[i]
|
||||||
|
buy_amount = self.strategy.get_buy_amount(signal, price)
|
||||||
|
total_buy_amount += buy_amount
|
||||||
|
alerts.append((data.index[i], price, signal, buy_amount))
|
||||||
|
|
||||||
|
print(f"\n총 매수 신호 수: {len(alerts)}")
|
||||||
|
print(f"총 매수 금액: {total_buy_amount:,.0f}원")
|
||||||
|
|
||||||
|
# 신호별 분석
|
||||||
|
signal_counts = {}
|
||||||
|
signal_amounts = {}
|
||||||
|
for _, _, signal, amount in alerts:
|
||||||
|
signal_counts[signal] = signal_counts.get(signal, 0) + 1
|
||||||
|
signal_amounts[signal] = signal_amounts.get(signal, 0) + amount
|
||||||
|
|
||||||
|
for signal in signal_counts:
|
||||||
|
print(f" - {signal} 신호: {signal_counts[signal]}회, 총 {signal_amounts[signal]:,.0f}원")
|
||||||
|
|
||||||
|
# 매도 신호 분석
|
||||||
|
sell_signals = self.strategy.get_sell_signals()
|
||||||
|
sell_alerts = []
|
||||||
|
for i in range(len(inverseData)):
|
||||||
|
if inverseData['point'].iloc[i] == 1 and inverseData['signal'].iloc[i] in sell_signals:
|
||||||
|
signal = inverseData['signal'].iloc[i]
|
||||||
|
price = inverseData['Close'].iloc[i]
|
||||||
|
sell_ratio = self.strategy.get_sell_amount_ratio(signal)
|
||||||
|
sell_alerts.append((inverseData.index[i], price, signal, sell_ratio))
|
||||||
|
|
||||||
|
print(f"\n총 매도 신호 수: {len(sell_alerts)}")
|
||||||
|
for date, price, signal, ratio in sell_alerts:
|
||||||
|
print(f" - {date}: {price:.4f} ({signal}) - 매도비율: {ratio*100:.0f}%")
|
||||||
|
|
||||||
|
# Plotly 기반 시각화
|
||||||
|
if SHOW_GRAPHS:
|
||||||
|
self.render_plotly(symbol, interval_minutes, data, inverseData)
|
||||||
|
return alerts, sell_alerts
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# 메인 실행 부분
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print(f"\n=== {COIN} 시뮬레이션 시작 ===")
|
||||||
|
print(f"코인: {COIN}")
|
||||||
|
print(f"분봉: {INTERVAL}분")
|
||||||
|
print(f"봉 개수: {BONG_COUNT}개")
|
||||||
|
print(f"그래프 표시: {'예' if SHOW_GRAPHS else '아니오'}")
|
||||||
|
print("=" * 50)
|
||||||
|
|
||||||
|
try:
|
||||||
|
sim = CoinSimulation(COIN)
|
||||||
|
buy_alerts, sell_alerts = sim.run_coin_simulation(COIN, INTERVAL)
|
||||||
|
|
||||||
|
print(f"\n=== {COIN} 시뮬레이션 완료 ===")
|
||||||
|
print(f"매수 신호: {len(buy_alerts)}회")
|
||||||
|
print(f"매도 신호: {len(sell_alerts)}회")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error analyzing {COIN}: {str(e)}")
|
||||||
|
print("\n지원되는 코인 목록:")
|
||||||
|
print("XRP, ADA, APT, AVAX, BONK, BTC, ETC, HBAR, LINK, ONDO, PENGU, SEI, SOL, SUI, TRX, VIRTUAL, WLD, XLM")
|
||||||
Reference in New Issue
Block a user