Files
cointrader/docs/plans/2026-03-02-reverse-signal-reenter-plan.md
21in7 9ec78d76bd feat: implement immediate re-entry after closing position on reverse signal
- Added `_close_and_reenter` method to handle immediate re-entry after closing a position when a reverse signal is detected, contingent on passing the ML filter.
- Updated `process_candle` to call `_close_and_reenter` instead of `_close_position` for reverse signals.
- Enhanced test coverage for the new functionality, ensuring correct behavior under various conditions, including ML filter checks and position limits.
2026-03-02 01:34:36 +09:00

8.2 KiB

반대 시그널 시 청산 후 즉시 재진입 구현 플랜

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: 반대 방향 시그널이 오면 기존 포지션을 청산하고 ML 필터 통과 시 즉시 반대 방향으로 재진입한다.

Architecture: src/bot.py_close_and_reenter 메서드를 추가하고, process_candle의 반대 시그널 분기에서 이를 호출한다. 기존 _close_position_open_position을 그대로 재사용하므로 중복 없음.

Tech Stack: Python 3.12, pytest, unittest.mock


테스트 스크립트

각 태스크 단계마다 아래 스크립트로 테스트를 실행한다.

# Task 1 — 신규 테스트 실행 (구현 전, FAIL 확인용)
bash scripts/test_reverse_reenter.sh 1

# Task 2 — _close_and_reenter 메서드 테스트 (구현 후, PASS 확인)
bash scripts/test_reverse_reenter.sh 2

# Task 3 — process_candle 분기 테스트 (수정 후, PASS 확인)
bash scripts/test_reverse_reenter.sh 3

# test_bot.py 전체
bash scripts/test_reverse_reenter.sh bot

# 전체 테스트 스위트
bash scripts/test_reverse_reenter.sh all

참고 파일

  • 설계 문서: docs/plans/2026-03-02-reverse-signal-reenter-design.md
  • 구현 대상: src/bot.py
  • 기존 테스트: tests/test_bot.py

Task 1: _close_and_reenter 테스트 작성

Files:

  • Modify: tests/test_bot.py

Step 1: 테스트 3개 추가

tests/test_bot.py 맨 아래에 다음 테스트를 추가한다.

@pytest.mark.asyncio
async def test_close_and_reenter_calls_open_when_ml_passes(config, sample_df):
    """반대 시그널 + ML 필터 통과 시 청산 후 재진입해야 한다."""
    with patch("src.bot.BinanceFuturesClient"):
        bot = TradingBot(config)

    bot._close_position = AsyncMock()
    bot._open_position = AsyncMock()
    bot.ml_filter = MagicMock()
    bot.ml_filter.is_model_loaded.return_value = True
    bot.ml_filter.should_enter.return_value = True

    position = {"positionAmt": "100", "entryPrice": "0.5", "markPrice": "0.52"}
    await bot._close_and_reenter(position, "SHORT", sample_df)

    bot._close_position.assert_awaited_once_with(position)
    bot._open_position.assert_awaited_once_with("SHORT", sample_df)


@pytest.mark.asyncio
async def test_close_and_reenter_skips_open_when_ml_blocks(config, sample_df):
    """ML 필터 차단 시 청산만 하고 재진입하지 않아야 한다."""
    with patch("src.bot.BinanceFuturesClient"):
        bot = TradingBot(config)

    bot._close_position = AsyncMock()
    bot._open_position = AsyncMock()
    bot.ml_filter = MagicMock()
    bot.ml_filter.is_model_loaded.return_value = True
    bot.ml_filter.should_enter.return_value = False

    position = {"positionAmt": "100", "entryPrice": "0.5", "markPrice": "0.52"}
    await bot._close_and_reenter(position, "SHORT", sample_df)

    bot._close_position.assert_awaited_once_with(position)
    bot._open_position.assert_not_called()


@pytest.mark.asyncio
async def test_close_and_reenter_skips_open_when_max_positions_reached(config, sample_df):
    """최대 포지션 수 도달 시 청산만 하고 재진입하지 않아야 한다."""
    with patch("src.bot.BinanceFuturesClient"):
        bot = TradingBot(config)

    bot._close_position = AsyncMock()
    bot._open_position = AsyncMock()
    bot.risk = MagicMock()
    bot.risk.can_open_new_position.return_value = False

    position = {"positionAmt": "100", "entryPrice": "0.5", "markPrice": "0.52"}
    await bot._close_and_reenter(position, "SHORT", sample_df)

    bot._close_position.assert_awaited_once_with(position)
    bot._open_position.assert_not_called()

Step 2: 테스트 실행 — 실패 확인

bash scripts/test_reverse_reenter.sh 1

예상 결과: AttributeError: 'TradingBot' object has no attribute '_close_and_reenter' 로 3개 FAIL


Task 2: _close_and_reenter 메서드 구현

Files:

  • Modify: src/bot.py:148 (_close_position 메서드 바로 아래에 추가)

Step 1: _close_position 다음에 메서드 추가

src/bot.py에서 _close_position 메서드(148~167번째 줄) 바로 뒤에 다음을 추가한다.

    async def _close_and_reenter(
        self,
        position: dict,
        signal: str,
        df,
        btc_df=None,
        eth_df=None,
    ) -> None:
        """기존 포지션을 청산하고, ML 필터 통과 시 반대 방향으로 즉시 재진입한다."""
        await self._close_position(position)

        if not self.risk.can_open_new_position():
            logger.info("최대 포지션 수 도달 — 재진입 건너뜀")
            return

        if self.ml_filter.is_model_loaded():
            features = build_features(df, signal, btc_df=btc_df, eth_df=eth_df)
            if not self.ml_filter.should_enter(features):
                logger.info(f"ML 필터 차단: {signal} 재진입 무시")
                return

        await self._open_position(signal, df)

Step 2: 테스트 실행 — 통과 확인

bash scripts/test_reverse_reenter.sh 2

예상 결과: 3개 PASS

Step 3: 커밋

git add src/bot.py tests/test_bot.py
git commit -m "feat: add _close_and_reenter method for reverse signal handling"

Task 3: process_candle 분기 수정

Files:

  • Modify: src/bot.py:83-85

Step 1: 기존 분기 테스트 추가

tests/test_bot.py에 다음 테스트를 추가한다.

@pytest.mark.asyncio
async def test_process_candle_calls_close_and_reenter_on_reverse_signal(config, sample_df):
    """반대 시그널 시 process_candle이 _close_and_reenter를 호출해야 한다."""
    with patch("src.bot.BinanceFuturesClient"):
        bot = TradingBot(config)

    bot.exchange = AsyncMock()
    bot.exchange.get_position = AsyncMock(return_value={
        "positionAmt": "100",
        "entryPrice": "0.5",
        "markPrice": "0.52",
    })
    bot._close_and_reenter = AsyncMock()
    bot.ml_filter = MagicMock()
    bot.ml_filter.is_model_loaded.return_value = False
    bot.ml_filter.should_enter.return_value = True

    with patch("src.bot.Indicators") as MockInd:
        mock_ind = MagicMock()
        mock_ind.calculate_all.return_value = sample_df
        mock_ind.get_signal.return_value = "SHORT"  # 현재 LONG 포지션에 반대 시그널
        MockInd.return_value = mock_ind
        await bot.process_candle(sample_df)

    bot._close_and_reenter.assert_awaited_once()
    call_args = bot._close_and_reenter.call_args
    assert call_args.args[1] == "SHORT"

Step 2: 테스트 실행 — 실패 확인

bash scripts/test_reverse_reenter.sh 3

예상 결과: FAIL (_close_and_reenter가 아직 호출되지 않음)

Step 3: process_candle 수정

src/bot.py에서 아래 부분을 찾아 수정한다.

# 변경 전 (81~85번째 줄 근처)
        elif position is not None:
            pos_side = "LONG" if float(position["positionAmt"]) > 0 else "SHORT"
            if (pos_side == "LONG" and signal == "SHORT") or \
               (pos_side == "SHORT" and signal == "LONG"):
                await self._close_position(position)

# 변경 후
        elif position is not None:
            pos_side = "LONG" if float(position["positionAmt"]) > 0 else "SHORT"
            if (pos_side == "LONG" and signal == "SHORT") or \
               (pos_side == "SHORT" and signal == "LONG"):
                await self._close_and_reenter(
                    position, signal, df_with_indicators, btc_df=btc_df, eth_df=eth_df
                )

Step 4: 전체 테스트 실행 — 통과 확인

bash scripts/test_reverse_reenter.sh bot

예상 결과: 전체 PASS (기존 테스트 포함)

Step 5: 커밋

git add src/bot.py tests/test_bot.py
git commit -m "feat: call _close_and_reenter on reverse signal in process_candle"

Task 4: 전체 테스트 스위트 확인

Step 1: 전체 테스트 실행

bash scripts/test_reverse_reenter.sh all

예상 결과: 모든 테스트 PASS

Step 2: 실패 테스트 있으면 수정 후 재실행

실패가 있으면 원인을 파악하고 수정한다. 기존 테스트를 깨뜨리지 않도록 주의.