feat: add market regime filter and compound reinvestment

- Add market_regime.py: BTC/ETH/SOL/XRP weighted 2h trend score
  Bull(≥+1.5%) / Neutral / Bear(<-1%) regime detection with 10min cache
- strategy.py: dynamic TREND/VOL thresholds based on current regime
  Bull: 3%/1.5x, Neutral: 5%/2.0x, Bear: 8%/3.5x
- price_collector.py: always include leader coins in price history
- trader.py: compound reinvestment (profit added to budget, floor at initial)
- notify.py: regime info in hourly report, P&L icons (/, 💚/🔴)
- main.py: hourly status at top-of-hour, filter positions held 1h+
- backtest.py: timestop/combo comparison modes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
joungmin
2026-03-01 10:14:36 +09:00
parent 035b3e2f30
commit 83a229dd26
8 changed files with 423 additions and 46 deletions

View File

@@ -17,15 +17,39 @@ from .price_db import (
delete_position, load_positions, upsert_position,
ensure_trade_results_table, record_trade, load_recent_wins,
ensure_sell_prices_table, upsert_sell_price, load_sell_prices,
get_cumulative_krw_profit,
)
load_dotenv()
logger = logging.getLogger(__name__)
MAX_BUDGET = int(os.getenv("MAX_BUDGET", "10000000")) # 총 운용 한도
MAX_POSITIONS = int(os.getenv("MAX_POSITIONS", "3")) # 최대 동시 보유 종목 수
PER_POSITION = MAX_BUDGET // MAX_POSITIONS # 종목당 투자금
INITIAL_BUDGET = int(os.getenv("MAX_BUDGET", "10000000")) # 초기 원금 (고정)
MAX_POSITIONS = int(os.getenv("MAX_POSITIONS", "3")) # 최대 동시 보유 종목 수
# 복리 적용 예산 (매도 후 재계산) — 수익 발생 시만 증가, 손실 시 원금 유지
MAX_BUDGET = INITIAL_BUDGET
PER_POSITION = INITIAL_BUDGET // MAX_POSITIONS
def _recalc_compound_budget() -> None:
"""누적 수익을 반영해 MAX_BUDGET / PER_POSITION 재계산.
수익이 발생한 만큼만 예산에 더함 (손실 시 원금 아래로 내려가지 않음).
매도 완료 후 호출.
"""
global MAX_BUDGET, PER_POSITION
try:
cum_profit = get_cumulative_krw_profit()
effective = INITIAL_BUDGET + max(int(cum_profit), 0)
MAX_BUDGET = effective
PER_POSITION = effective // MAX_POSITIONS
logger.info(
f"[복리] 누적수익={cum_profit:+,.0f}원 | "
f"운용예산={MAX_BUDGET:,}원 | 포지션당={PER_POSITION:,}"
)
except Exception as e:
logger.warning(f"[복리] 예산 재계산 실패 (이전 값 유지): {e}")
# Walk-forward 필터 설정
WF_WINDOW = int(float(os.getenv("WF_WINDOW", "5"))) # 이력 윈도우 크기
@@ -103,6 +127,15 @@ def get_positions() -> dict:
return _positions
def get_budget_info() -> dict:
"""현재 복리 예산 정보 반환 (main.py 등 외부에서 동적 조회용)."""
return {
"max_budget": MAX_BUDGET,
"per_position": PER_POSITION,
"initial": INITIAL_BUDGET,
}
def restore_positions() -> None:
"""시작 시 Oracle DB + Upbit 잔고를 교차 확인하여 포지션 복원.
trade_results 테이블도 이 시점에 생성 (없으면).
@@ -115,6 +148,9 @@ def restore_positions() -> None:
except Exception as e:
logger.warning(f"trade_results 테이블 생성 실패 (무시): {e}")
# 시작 시 복리 예산 복원 (이전 세션 수익 반영)
_recalc_compound_budget()
try:
ensure_sell_prices_table()
except Exception as e:
@@ -278,7 +314,8 @@ def buy(ticker: str) -> bool:
f"[매수] {ticker} @ {actual_price:,.0f}원 (실체결가) | "
f"수량={amount} | 투자금={order_krw:,}원 | trade_id={trade_id[:8]}"
)
notify_buy(ticker, actual_price, amount, order_krw)
notify_buy(ticker, actual_price, amount, order_krw,
max_budget=MAX_BUDGET, per_position=PER_POSITION)
return True
except Exception as e:
logger.error(f"매수 예외 {ticker}: {e}")
@@ -387,7 +424,12 @@ def sell(ticker: str, reason: str = "") -> bool:
f"[매도] {ticker} @ {actual_sell_price:,.4f}원 | "
f"수익률={pnl:+.1f}% | 순익={krw_profit:+,.0f}원 (수수료 {fee:,.0f}원) | 사유={reason}"
)
notify_sell(ticker, actual_sell_price, pnl, reason)
try:
cum = get_cumulative_krw_profit() + krw_profit
except Exception:
cum = 0.0
notify_sell(ticker, actual_sell_price, pnl, reason,
krw_profit=krw_profit, fee_krw=fee, cum_profit=cum)
_last_sell_prices[ticker] = actual_sell_price
try:
upsert_sell_price(ticker, actual_sell_price)
@@ -406,6 +448,8 @@ def sell(ticker: str, reason: str = "") -> bool:
delete_position(ticker)
except Exception as e:
logger.error(f"포지션 DB 삭제 실패 {ticker}: {e}")
# 복리 예산 재계산: 수익 발생분만 다음 투자에 반영
_recalc_compound_budget()
return True
except Exception as e:
logger.error(f"매도 예외 {ticker}: {e}")