This commit is contained in:
dsyoon
2026-01-28 18:58:33 +09:00
commit c45ad151b6
24 changed files with 7599 additions and 0 deletions

306
HTS2.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

1056
simulation_1min.py Normal file

File diff suppressed because it is too large Load Diff

1097
simulation_1mon.py Normal file

File diff suppressed because it is too large Load Diff

332
simulation_30min.py Normal file
View 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
View 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")