프로젝트/코인 투자 매크로

2025-06-09 [9] [v1.2] 예외 종목(HODL LIST) 선택 및 수익실현/손절매

훈 님의 개발 블로그 2025. 6. 11. 00:13

매크로를 만들었는데 내가 존버하려고 매수한 특정 종목을 매크로가 팔아버리면 곤란하겠죠?!
그래서 오늘은 매크로가 건드리지 말았으면 하는 예외종목을 지정하는 기능을 진행하겠습니다.


시작하기 전, 왜 [HOLD]가 아닌 [HODL]인지에 대해서는 재미있는 일화가 있어요.
2013년 비트코인 가격이 크게 하락하던 어느 날, BitcoinTalk 포럼에 'GameKyuubi'라는 아이디를 쓰는 한 유저가 술에 취한 채로 글을 올렸다고 해요.
"나는 가격이 떨어져도 절대 팔지 않고 계속 보유할 것이다!"
하지만 여기서 "I AM HOLDING"이 아닌 "I AM HODLING"으로 오타를 쳤고,
그후 밈이되어 "Hold On for Dear Life" (살기 위해 꽉 붙잡아라 / 목숨 걸고 버텨라) 라는 새로운 의미가 부여됐다고 합니다.🤣


1. 예외 종목(HODL LIST)

포트폴리오 포맷 변경

포트폴리오를 초기화하는 initialize_vportfolio()함수에서 포맷을 변경합니다.

- 기존 포맷

{
    "cash": 1000000.0,  # 초기 가상 현금 100만원
    "coins_owned": [],   # 보유 코인 목록 (비어있음)
    "strategy_params": {
        "profit_take_percentage": 0.1,
        "stop_loss_percentage": 0.05,
    },  
}

- 변경 된 포맷

{
    "cash": 1000000.0,  # 초기 가상 현금 100만원
    "coins_owned": [],   # 보유 코인 목록 (비어있음)
    "strategy_params": {
        "profit_take_percentage": 0.1,
        "stop_loss_percentage": 0.05,
    },
    "hodl_list": ["PEPE", "ETH"]   # 예외 항목 예시
}

 

HODL LIST 변경

hodl_list변경은 간단하게 vportfolio_state.json파일을 열어 변경하면 됩니다.

 

2. 수익실현/손절매

수익실현/손절매 목표에 도달했을 때 매도하는 로직을 구현하도록 하겠습니다.
이 로직은 main에 추가하는 함수로 3. 전체 테스트 진행에 포함되어 있습니다.

owned_coin_info = None
for coin in my_vportfolio["coins_owned"]:
    if coin["symbol"] == coin_symbol:
        owned_coin_info = coin
        break

# --- 🔴 매도 조건 우선 확인 (보유 코인에 대해서만) ---
if owned_coin_info:
    # 1. 현재가 조회
    ticker = bithumb_api_client.get_ticker_info(coin_symbol)
    if not ticker:
        print(f"-> [{coin_symbol}] 현재가 조회 실패. 매도 판단을 건너뜁니다.")
        continue  # 다음 코인으로 넘어감

    current_price = float(ticker['closing_price'])
    avg_buy_price = owned_coin_info['avg_buy_price']

    # 2. 수익률 계산
    profit_rate = (current_price - avg_buy_price) / avg_buy_price

    pt_rate = my_vportfolio['strategy_params']['profit_take_percentage']
    sl_rate = my_vportfolio['strategy_params']['stop_loss_percentage']

    print(f"-> [{coin_symbol}] 보유 중. 현재 수익률: {profit_rate:+.2%}")

    # 3. 수익실현 또는 손절매 조건 확인
    should_sell = False
    if profit_rate >= pt_rate:
        print(f"-> [{coin_symbol}] 💰 수익실현 조건 달성! (+{pt_rate:.1%}) 매도를 시도합니다.")
        should_sell = True
    elif profit_rate <= -sl_rate:
        print(f"-> [{coin_symbol}] 🛡️ 손절매 조건 달성! (-{sl_rate:.1%}) 매도를 시도합니다.")
        should_sell = True

    if should_sell:
        quantity_to_sell = owned_coin_info['quantity']  # 보유량 전량 매도
        updated_portfolio = virtual_portfolio_manager.record_vportfolio_trade(
            my_vportfolio, "sell", coin_symbol, quantity_to_sell, current_price
        )
        if updated_portfolio: my_vportfolio = updated_portfolio

 

3. 예외 종목 & 수익실현/손절매를 추가한 전체 테스트 진행

1). 코드 변경

main_trader.py의 main을 다음과 같이 변경합니다.
2025-06-08 [8] [v1.0.0 완료] 캔들스틱, 단순 이동 평균(SMA), 골든/데드 크로스의 로직에서 일부를 변경하였습니다.

if __name__ == "__main__":
    # --- 봇 설정 ---
    ANALYSIS_INTERVAL = "30m"  # 분석할 캔들스틱 간격 (30분봉)
    CHECK_INTERVAL_SECONDS = 30 * 60  # 다음 분석까지 대기할 시간 (1800초)

    # --- 봇 시작 ---
    trade_history.set_trade_mode(is_virtual=True)
    my_vportfolio = virtual_portfolio_manager.load_vportfolio()

    # while True: 루프 시작 전에 HODL 리스트 한번 출력 (확인용)
    hodl_list = my_vportfolio.get("hodl_list", [])
    print("=" * 50)
    print(f"🚀 가상 자동매매 봇을 시작합니다! (HODL 리스트: {hodl_list})")
    print(f"분석 간격: {ANALYSIS_INTERVAL}, 체크 주기: {CHECK_INTERVAL_SECONDS}초")
    print("봇을 중지하려면 Ctrl+C 를 누르세요.")
    print("=" * 50)

    while True:
        try:
            # --- 1. 관심 종목 선정 ---
            all_ticker_info = bithumb_api_client.get_ticker_info("ALL")
            target_coins = filter_top_coins_by_trade_value(all_ticker_info, top_n=20)

            # --- 2. 최종 분석 대상 확정 (HODL 리스트 필터링) ---
            owned_symbols = [coin['symbol'] for coin in my_vportfolio.get('coins_owned', [])]
            combined_list = list(set(target_coins + owned_symbols))

            # HODL 리스트에 있는 코인들을 분석 대상에서 최종적으로 제외!
            final_analysis_list = [coin for coin in combined_list if coin not in hodl_list]

            print(f"\n--- 최종 분석 대상 ({len(final_analysis_list)}개) ---")
            print(final_analysis_list)
            print("-" * 50)

            # --- 3. 최종 분석 대상을 순회하며 분석 및 매매 실행 ---
            for coin_symbol in final_analysis_list:
                print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {coin_symbol} 분석 시작...", end=" ")
                # --- 보유 코인 여부 확인 ---
                owned_coin_info = None
                for coin in my_vportfolio["coins_owned"]:
                    if coin["symbol"] == coin_symbol:
                        owned_coin_info = coin
                        break

                # --- 🔴 매도 조건 우선 확인 (보유 코인에 대해서만) ---
                if owned_coin_info:
                    # 1. 현재가 조회
                    ticker = bithumb_api_client.get_ticker_info(coin_symbol)
                    if not ticker:
                        print(f"-> [{coin_symbol}] 현재가 조회 실패. 매도 판단을 건너뜁니다.")
                        continue  # 다음 코인으로 넘어감

                    current_price = float(ticker['closing_price'])
                    avg_buy_price = owned_coin_info['avg_buy_price']

                    # 2. 수익률 계산
                    profit_rate = (current_price - avg_buy_price) / avg_buy_price

                    pt_rate = my_vportfolio['strategy_params']['profit_take_percentage']
                    sl_rate = my_vportfolio['strategy_params']['stop_loss_percentage']

                    print(f"-> [{coin_symbol}] 보유 중. 현재 수익률: {profit_rate:+.2%}")

                    # 3. 수익실현 또는 손절매 조건 확인
                    should_sell = False
                    if profit_rate >= pt_rate:
                        print(f"-> [{coin_symbol}] 💰 수익실현 조건 달성! (+{pt_rate:.1%}) 매도를 시도합니다.")
                        should_sell = True
                    elif profit_rate <= -sl_rate:
                        print(f"-> [{coin_symbol}] 🛡️ 손절매 조건 달성! (-{sl_rate:.1%}) 매도를 시도합니다.")
                        should_sell = True

                    if should_sell:
                        quantity_to_sell = owned_coin_info['quantity']  # 보유량 전량 매도
                        updated_portfolio = virtual_portfolio_manager.record_vportfolio_trade(
                            my_vportfolio, "sell", coin_symbol, quantity_to_sell, current_price
                        )
                        if updated_portfolio: my_vportfolio = updated_portfolio

                    # 매도 조건이 충족되면, 굳이 매수 조건을 볼 필요가 없으니 다음 코인으로 넘어감
                    continue

                # --- 🟢 매수 조건 확인 (보유하지 않은 코인에 대해서만) ---
                else:
                    # 캔들스틱 데이터 가져오기
                    candlestick_data = bithumb_api_client.get_candlestick_data(coin_symbol, chart_intervals=ANALYSIS_INTERVAL)

                    signal = "HOLD"  # 기본 신호는 유지
                    if candlestick_data and len(candlestick_data) > 20:
                        import pandas as pd

                        df = pd.DataFrame(candlestick_data, columns=['timestamp', 'open', 'close', 'high', 'low', 'volume'])
                        df['close'] = pd.to_numeric(df['close'])
                        df['SMA5'] = technical_analyzer.calculate_sma(candlestick_data, period=5)
                        df['SMA20'] = technical_analyzer.calculate_sma(candlestick_data, period=20)

                        signal = check_cross_signal(df)
                        print(f"-> 현재 신호: {signal}")
                    else:
                        print(f"-> 현재 신호: HOLD (데이터가 충분하지 않아 매매 신호를 판단할 수 없습니다.)")

                    # --- 2. 신호에 따른 액션 수행 ---
                    if signal == "BUY":
                        print(f"[{coin_symbol}] 🟢 골든 크로스 발생! 매수 주문을 시도합니다.")
                        is_owned = False
                        for coin in my_vportfolio["coins_owned"]:
                            if coin["symbol"] == coin_symbol:
                                is_owned = True
                                break

                        if is_owned:
                            print(f"-> [{coin_symbol}] 이미 보유 중인 코인이므로 추가 매수하지 않습니다.")
                        else:
                            cash_to_invest = my_vportfolio['cash'] * 0.1  # 가용 현금의 10% 투자

                            # 현재가 조회
                            ticker = bithumb_api_client.get_ticker_info(coin_symbol)
                            if ticker:
                                current_price = float(ticker['closing_price'])
                                quantity_to_buy = cash_to_invest / current_price

                                # 가상 매수 실행
                                updated_portfolio = virtual_portfolio_manager.record_vportfolio_trade(my_vportfolio, "buy",
                                                                                                      coin_symbol, quantity_to_buy,
                                                                                                      current_price)
                                if updated_portfolio: my_vportfolio = updated_portfolio

                    elif signal == "SELL":
                        print(f"[{coin_symbol}] 🔴 데드 크로스 발생! 매도 주문을 시도합니다.")
                        quantity_to_sell = 0
                        for coin in my_vportfolio["coins_owned"]:
                            if coin["symbol"] == coin_symbol.upper():
                                quantity_to_sell = coin["quantity"]
                                break

                        if quantity_to_sell > 0:
                            ticker = bithumb_api_client.get_ticker_info(coin_symbol)
                            if ticker:
                                current_price = float(ticker['closing_price'])
                                # 가상 매도 실행
                                updated_portfolio = virtual_portfolio_manager.record_vportfolio_trade(my_vportfolio, "sell",
                                                                                                      coin_symbol,
                                                                                                      quantity_to_sell,
                                                                                                      current_price)
                                if updated_portfolio: my_vportfolio = updated_portfolio
                        else:
                            print(f"-> {coin_symbol} 보유 수량이 없어 매도하지 않습니다.")

            print(f"\n분석 완료. 다음 분석은 {CHECK_INTERVAL_SECONDS}초 후에 시작됩니다.")
            time.sleep(CHECK_INTERVAL_SECONDS) # <--- 실제 운영 시 활성화

        except KeyboardInterrupt:
            # 사용자가 Ctrl+C를 눌러서 프로그램을 중단시킬 때
            print("\n\n사용자에 의해 프로그램이 중단되었습니다. 최종 포트폴리오 상태를 저장합니다...")
            virtual_portfolio_manager.save_vportfolio(my_vportfolio)
            print("프로그램을 종료합니다.")
            break  # while 루프 탈출

        except Exception as e:
            # 예기치 못한 다른 에러 발생 시
            print(f"⚠️ 예상치 못한 오류 발생: {e}")
            print("60초 후 재시도합니다...")
            time.sleep(60)

 

2). 테스트 진행

아래와 같이 출력되면 성공!
(전체 테스트를 위해 일부 data를 변경하였습니다.)

 

다음 단계

  • 진행 방향 재 설정

 

끝!