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:
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user