From 83a229dd2656b867d077dece8b090e229f5ecb1c Mon Sep 17 00:00:00 2001 From: joungmin Date: Sun, 1 Mar 2026 10:14:36 +0900 Subject: [PATCH] feat: add market regime filter and compound reinvestment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- backtest.py | 149 +++++++++++++++++++++++++++++++++++++++- core/market_regime.py | 110 +++++++++++++++++++++++++++++ core/notify.py | 74 ++++++++++++++++---- core/price_collector.py | 9 +++ core/price_db.py | 9 +++ core/strategy.py | 35 ++++++---- core/trader.py | 54 +++++++++++++-- main.py | 29 +++++--- 8 files changed, 423 insertions(+), 46 deletions(-) create mode 100644 core/market_regime.py diff --git a/backtest.py b/backtest.py index 7f3181a..edf287b 100644 --- a/backtest.py +++ b/backtest.py @@ -70,6 +70,8 @@ COMPARE_TOP50 = "--top50-cmp" in sys.argv # ์ข…๋ชฉ์ˆ˜ + ๊ฑฐ๋ž˜๋Œ€๊ธˆ COMPARE_TREND = "--trend-cmp" in sys.argv # ์ถ”์„ธ ์ƒํ•œ์„  ๋น„๊ต ๋ชจ๋“œ COMPARE_TICKER = "--ticker-cmp" in sys.argv # ์ข…๋ชฉ ์Šน๋ฅ  ํ•„ํ„ฐ ๋น„๊ต ๋ชจ๋“œ COMPARE_WF = "--walkforward-cmp" in sys.argv # walk-forward ํ•„ํ„ฐ ๋น„๊ต ๋ชจ๋“œ +COMPARE_TIMESTOP = "--timestop-cmp" in sys.argv # ํƒ€์ž„์Šคํƒ‘ ์กฐ๊ฑด ๋น„๊ต ๋ชจ๋“œ +COMPARE_COMBO = "--combo-cmp" in sys.argv # ์ถ”์„ธ+๊ฑฐ๋ž˜๋Ÿ‰ ์กฐํ•ฉ ๋น„๊ต ๋ชจ๋“œ if "--10m" in sys.argv: DEFAULT_INTERVAL = "minute10" @@ -574,6 +576,8 @@ def simulate( volume_mult: float = VOLUME_MULT, # ๊ฑฐ๋ž˜๋Œ€๊ธˆ ๊ธ‰์ฆ ๊ธฐ์ค€ ๋ฐฐ์ˆ˜ wf_min_wr: float = 0.0, # walk-forward: ์ง์ „ N๊ฑด ์Šน๋ฅ  ์ž„๊ณ„๊ฐ’ (0=๋น„ํ™œ์„ฑ) wf_window: int = 5, # walk-forward: ์Šน๋ฅ  ๊ณ„์‚ฐ ์œˆ๋„์šฐ ํฌ๊ธฐ + time_stop_h: float = TIME_STOP_H, # ํƒ€์ž„์Šคํƒ‘ ๊ธฐ์ค€ ์‹œ๊ฐ„ (0=๋น„ํ™œ์„ฑ) + time_stop_gain: float = TIME_STOP_GAIN, # ํƒ€์ž„์Šคํƒ‘ ์ตœ์†Œ ์ˆ˜์ต๋ฅ  ) -> list[dict]: """๋‹จ์ผ ์ข…๋ชฉ ์ „๋žต ์‹œ๋ฎฌ๋ ˆ์ด์…˜. @@ -582,6 +586,7 @@ def simulate( volume_mult : ์ง„์ž… ์กฐ๊ฑด โ€” ์ „์ผ ๊ฑฐ๋ž˜๋Ÿ‰ > N์ผ ํ‰๊ท  ร— volume_mult wf_min_wr : walk-forward ํ•„ํ„ฐ โ€” ์ง์ „ wf_window๊ฑด ์Šน๋ฅ ์ด ์ด ๊ฐ’ ๋ฏธ๋งŒ์ด๋ฉด ์ง„์ž… ์ฐจ๋‹จ ์œˆ๋„์šฐ๊ฐ€ ์ฑ„์›Œ์ง€๊ธฐ ์ „(์›Œ๋ฐ์—…)์—๋Š” ํ•„ํ„ฐ ๋ฏธ์ ์šฉ + time_stop_h : ํƒ€์ž„์Šคํƒ‘ ๊ธฐ์ค€ ์‹œ๊ฐ„ (0 ์ดํ•˜ = ๋น„ํ™œ์„ฑ) """ _hard_stop = hard_stop if hard_stop is not None else stop_loss trades: list[dict] = [] @@ -614,7 +619,7 @@ def simulate( reason = "trailing_stop" elif _hard_stop < 999 and drop_buy >= _hard_stop: reason = "hard_stop" - elif elapsed >= TIME_STOP_H and pnl < TIME_STOP_GAIN: + elif time_stop_h > 0 and elapsed >= time_stop_h and pnl < time_stop_gain: reason = "time_stop" if reason: @@ -851,6 +856,8 @@ def run_scenario( daily_features: Optional[dict] = None, # ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ daily_feat {ticker: df} wf_min_wr: float = 0.0, # walk-forward ์Šน๋ฅ  ์ž„๊ณ„๊ฐ’ wf_window: int = 5, # walk-forward ์œˆ๋„์šฐ + time_stop_h: float = TIME_STOP_H, # ํƒ€์ž„์Šคํƒ‘ ๊ธฐ์ค€ ์‹œ๊ฐ„ (0=๋น„ํ™œ์„ฑ) + time_stop_gain: float = TIME_STOP_GAIN, # ํƒ€์ž„์Šคํƒ‘ ์ตœ์†Œ ์ˆ˜์ต๋ฅ  ) -> list[dict]: """์ฃผ์–ด์ง„ ํŒŒ๋ผ๋ฏธํ„ฐ๋กœ ์ „์ฒด ์ข…๋ชฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฐ์ดํ„ฐ๋Š” ์™ธ๋ถ€์—์„œ ์ฃผ์ž…). @@ -868,6 +875,8 @@ def run_scenario( "reentry": reentry_above_sell, "wf_min_wr": round(wf_min_wr, 3), "wf_window": wf_window, + "time_stop_h": time_stop_h, + "time_stop_gain": round(time_stop_gain, 4), } if use_db_cache: @@ -894,6 +903,8 @@ def run_scenario( volume_mult=volume_mult, wf_min_wr=wf_min_wr, wf_window=wf_window, + time_stop_h=time_stop_h, + time_stop_gain=time_stop_gain, ) all_trades.extend(trades) @@ -1531,8 +1542,142 @@ def main_ticker_filter_cmp(interval: str = DEFAULT_INTERVAL) -> None: print(f" โœ— {row[0]:<12} ์Šน๋ฅ  {row[1]:.0f}%") +def main_timestop_cmp(interval: str = DEFAULT_INTERVAL) -> None: + """ํƒ€์ž„์Šคํƒ‘ ์กฐ๊ฑด ๋น„๊ต: ํŠธ๋ ˆ์ผ๋ง 1.5%+8h(ํ˜„ํ–‰) vs 10%+8h vs 10%+24h vs 10%+์—†์Œ.""" + cfg = INTERVAL_CONFIG[interval] + label = cfg["label"] + trend = cfg["trend"] + + print(f"\n{'='*60}") + print(f" ํƒ€์ž„์Šคํƒ‘ ์กฐ๊ฑด ๋น„๊ต | {MONTHS}๊ฐœ์›” | ์ƒ์œ„ {TOP_N}์ข…๋ชฉ | {label}") + print(f" (A) ํŠธ๋ ˆ์ผ๋ง 1.5% + ํƒ€์ž„์Šคํƒ‘ 8h โ† ํ˜„์žฌ ์„ค์ •") + print(f" (B) ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 8h") + print(f" (C) ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 24h") + print(f" (D) ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ ์—†์Œ") + print(f"{'='*60}") + + print("\nโ–ถ ์ข…๋ชฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ (DB ์บ์‹œ ์šฐ์„ )...") + tickers = get_top_tickers(TOP_N) + print(f" โ†’ {tickers}") + data = fetch_all_data(tickers, interval) + print(f" ์‚ฌ์šฉ ์ข…๋ชฉ: {list(data.keys())}") + daily_features = {t: build_daily_features(f["daily"]) for t, f in data.items()} + + common = dict( + reentry_above_sell=True, interval_cd=interval, + use_db_cache=True, daily_features=daily_features, + trend_min_gain=trend, + ) + + print("\nโ–ถ A: ํŠธ๋ ˆ์ผ๋ง 1.5% + ํƒ€์ž„์Šคํƒ‘ 8h (ํ˜„์žฌ ์„ค์ •) ...") + trades_a = run_scenario(data, stop_loss=0.015, time_stop_h=8, **common) + print(f" ์™„๋ฃŒ ({len(trades_a)}๊ฑด)") + + print("\nโ–ถ B: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 8h ...") + trades_b = run_scenario(data, stop_loss=0.10, time_stop_h=8, **common) + print(f" ์™„๋ฃŒ ({len(trades_b)}๊ฑด)") + + print("\nโ–ถ C: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 24h ...") + trades_c = run_scenario(data, stop_loss=0.10, time_stop_h=24, **common) + print(f" ์™„๋ฃŒ ({len(trades_c)}๊ฑด)") + + print("\nโ–ถ D: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ ์—†์Œ ...") + trades_d = run_scenario(data, stop_loss=0.10, time_stop_h=0, **common) + print(f" ์™„๋ฃŒ ({len(trades_d)}๊ฑด)") + + compare_report([ + ("1.5%+8h(ํ˜„ํ–‰)", trades_a), + ("10%+8h", trades_b), + ("10%+24h", trades_c), + ("10%+ํƒ€์ž„์—†์Œ", trades_d), + ], title=f"ํŠธ๋ ˆ์ผ๋ง + ํƒ€์ž„์Šคํƒ‘ ์กฐํ•ฉ ๋น„๊ต ({label})") + + report(trades_a, f"A: ํŠธ๋ ˆ์ผ๋ง 1.5% + ํƒ€์ž„์Šคํƒ‘ 8h | {label}") + report(trades_b, f"B: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 8h | {label}") + report(trades_c, f"C: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ 24h | {label}") + report(trades_d, f"D: ํŠธ๋ ˆ์ผ๋ง 10% + ํƒ€์ž„์Šคํƒ‘ ์—†์Œ | {label}") + + +def main_combo_cmp(interval: str = DEFAULT_INTERVAL) -> None: + """์ถ”์„ธ ์ž„๊ณ„๊ฐ’ + ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ์กฐํ•ฉ ๋น„๊ต. + + ๋ชฉ์ : ๊ฑฐ๋ž˜ ๋นˆ๋„๋ฅผ ์ค„์—ฌ ์ˆ˜์ˆ˜๋ฃŒ ๋ถ€๋‹ด์„ ๋‚ฎ์ถ”๋Š” ์ตœ์  ์กฐํ•ฉ ํƒ์ƒ‰. + + A (ํ˜„ํ–‰): trend=1.0%, vol=2x + B: trend=1.5%, vol=2x (์ถ”์„ธ๋งŒ ๊ฐ•ํ™”) + C: trend=1.0%, vol=3x (๊ฑฐ๋ž˜๋Ÿ‰๋งŒ ๊ฐ•ํ™”) + D: trend=1.5%, vol=3x (๋‘˜ ๋‹ค ๊ฐ•ํ™”) + E: trend=2.0%, vol=3x (์ตœ๊ฐ• ๊ฐ•ํ™”) + """ + cfg = INTERVAL_CONFIG[interval] + label = cfg["label"] + base_trend = cfg["trend"] # 15๋ถ„๋ด‰ ๊ธฐ์ค€ 1.0% + + combos = [ + ("A ํ˜„ํ–‰ 1.0%/2x", base_trend, 2.0), + ("B ์ถ”์„ธ1.5%/2x", base_trend * 1.5, 2.0), + ("C ์ถ”์„ธ1.0%/3x", base_trend, 3.0), + ("D ์ถ”์„ธ1.5%/3x", base_trend * 1.5, 3.0), + ("E ์ถ”์„ธ2.0%/3x", base_trend * 2.0, 3.0), + ] + + print(f"\n{'='*64}") + print(f" ์ถ”์„ธ+๊ฑฐ๋ž˜๋Ÿ‰ ์กฐํ•ฉ ๋น„๊ต | {MONTHS}๊ฐœ์›” | ์ƒ์œ„ {TOP_N}์ข…๋ชฉ | {label}") + print(f" ๋ชฉ์ : ๊ฑฐ๋ž˜ ๋นˆ๋„โ†“ โ†’ ์ˆ˜์ˆ˜๋ฃŒ ๋ถ€๋‹ดโ†“ โ†’ ์ˆœ์ˆ˜์ตโ†‘") + print(f" FEE = 0.05% ร— 2 = 0.1%/๊ฑด | ํ˜„ํ–‰ ~85๊ฑด/์›” = ์›” 8.5% ์ˆ˜์ˆ˜๋ฃŒ") + print(f"{'='*64}") + + print("\nโ–ถ ์ข…๋ชฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์ค‘ (DB ์บ์‹œ ์šฐ์„ )...") + tickers = get_top_tickers(TOP_N) + data = fetch_all_data(tickers, interval) + print(f" ์‚ฌ์šฉ ์ข…๋ชฉ: {list(data.keys())}") + daily_features = {t: build_daily_features(f["daily"]) for t, f in data.items()} + + results = [] + for combo_label, trend, vol in combos: + print(f"\nโ–ถ {combo_label} (trend={trend*100:.1f}%, vol={vol:.0f}x) ...") + trades = run_scenario( + data, stop_loss=0.015, + trend_min_gain=trend, volume_mult=vol, + reentry_above_sell=True, + interval_cd=interval, use_db_cache=True, + daily_features=daily_features, + ) + n = len(trades) + fee_pct = n * 0.1 # ์ด ์ˆ˜์ˆ˜๋ฃŒ ๋ถ€๋‹ด (%) + print(f" ์™„๋ฃŒ ({n}๊ฑด | ์˜ˆ์ƒ ์ˆ˜์ˆ˜๋ฃŒ {fee_pct:.1f}%)") + results.append((combo_label, trades)) + + compare_report(results, title=f"์ถ”์„ธ+๊ฑฐ๋ž˜๋Ÿ‰ ์กฐํ•ฉ ๋น„๊ต ({label})") + + # ์ˆ˜์ˆ˜๋ฃŒ ๋ถ„์„ ์ถ”๊ฐ€ ์ถœ๋ ฅ + print(" [์ˆ˜์ˆ˜๋ฃŒ ๋ถ„์„]") + print(f" {'์กฐํ•ฉ':<20} {'๊ฑฐ๋ž˜์ˆ˜':>6} {'์›”ํ‰๊ท ':>6} {'์ด์ˆ˜์ˆ˜๋ฃŒ':>8} {'์ˆ˜์ˆ˜๋ฃŒ ์ „':>10} {'์ˆ˜์ˆ˜๋ฃŒ ํ›„':>10}") + print(f" {'-'*64}") + for (clabel, trades) in results: + if not trades: + continue + df = pd.DataFrame(trades) + n = len(df) + span_days = (df["exit"].max() - df["entry"].min()).total_seconds() / 86400 + per_month = n / (span_days / 30) if span_days > 0 else 0 + fee_total = n * 0.1 + net_pnl = df["pnl_pct"].sum() + gross_pnl = net_pnl + fee_total # ์ˆ˜์ˆ˜๋ฃŒ ์ „ ์ถ”์ • + print(f" {clabel:<20} {n:>6}๊ฑด {per_month:>5.0f}/์›” {fee_total:>7.1f}% " + f"{gross_pnl:>+9.1f}% {net_pnl:>+9.1f}%") + print() + + for clabel, trades in results: + report(trades, clabel) + + def main() -> None: - if COMPARE_TOP50: + if COMPARE_COMBO: + main_combo_cmp(DEFAULT_INTERVAL) + elif COMPARE_TIMESTOP: + main_timestop_cmp(DEFAULT_INTERVAL) + elif COMPARE_TOP50: main_top50_cmp(DEFAULT_INTERVAL) elif COMPARE_HARDSTOP: main_hard_stop_cmp(DEFAULT_INTERVAL) diff --git a/core/market_regime.py b/core/market_regime.py new file mode 100644 index 0000000..0185c52 --- /dev/null +++ b/core/market_regime.py @@ -0,0 +1,110 @@ +"""์‹œ์žฅ ๋ ˆ์ง(Bull/Neutral/Bear) ํŒ๋‹จ. + +BTCยทETHยทSOLยทXRP ๊ฐ€์ค‘ ํ‰๊ท  2h ์ถ”์„ธ๋กœ ๋ ˆ์ง์„ ๊ฒฐ์ •ํ•˜๊ณ  +๋งค์ˆ˜ ์กฐ๊ฑด ํŒŒ๋ผ๋ฏธํ„ฐ(trend_pct, vol_mult)๋ฅผ ๋™์ ์œผ๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค. +๊ณ„์‚ฐ๋œ ํ˜„์žฌ๊ฐ€๋Š” price_history DB์— ์ €์žฅํ•ด ์žฌํ™œ์šฉํ•œ๋‹ค. +""" + +from __future__ import annotations + +import logging +import time + +import pyupbit + +from .price_db import get_price_n_hours_ago, insert_prices + +logger = logging.getLogger(__name__) + +# ๋Œ€์žฅ ์ฝ”์ธ ๊ฐ€์ค‘์น˜ +LEADERS: dict[str, float] = { + "KRW-BTC": 0.40, + "KRW-ETH": 0.30, + "KRW-SOL": 0.15, + "KRW-XRP": 0.15, +} +TREND_HOURS = 2 # 2h ์ถ”์„ธ ๊ธฐ์ค€ + +BULL_THRESHOLD = 1.5 # score โ‰ฅ 1.5% โ†’ Bull +BEAR_THRESHOLD = -1.0 # score < -1.0% โ†’ Bear + +# ๋ ˆ์ง๋ณ„ ๋งค์ˆ˜ ์กฐ๊ฑด ํŒŒ๋ผ๋ฏธํ„ฐ +REGIME_PARAMS: dict[str, dict] = { + "bull": {"trend_pct": 3.0, "vol_mult": 1.5, "emoji": "๐ŸŸข"}, + "neutral": {"trend_pct": 5.0, "vol_mult": 2.0, "emoji": "๐ŸŸก"}, + "bear": {"trend_pct": 8.0, "vol_mult": 3.5, "emoji": "๐Ÿ”ด"}, +} + +# 10๋ถ„ ์บ์‹œ (์Šค์บ” ๋ฃจํ”„๋งˆ๋‹ค API ํ˜ธ์ถœ ๋ฐฉ์ง€) +_cache: dict = {} +_cache_ts: float = 0.0 +_CACHE_TTL = 600 + + +def get_regime() -> dict: + """ํ˜„์žฌ ์‹œ์žฅ ๋ ˆ์ง ๋ฐ˜ํ™˜. + + Returns: + { + 'name': 'bull' | 'neutral' | 'bear', + 'score': float, # ๊ฐ€์ค‘ ํ‰๊ท  2h ์ถ”์„ธ(%) + 'trend_pct': float, # ๋งค์ˆ˜ ์ถ”์„ธ ์ž„๊ณ„๊ฐ’ + 'vol_mult': float, # ๊ฑฐ๋ž˜๋Ÿ‰ ๋ฐฐ์ˆ˜ ์ž„๊ณ„๊ฐ’ + 'emoji': str, + } + """ + global _cache, _cache_ts + if _cache and (time.time() - _cache_ts) < _CACHE_TTL: + return _cache + + score = 0.0 + current_prices: dict[str, float] = {} + + for ticker, weight in LEADERS.items(): + try: + current = pyupbit.get_current_price(ticker) + if not current: + continue + current_prices[ticker] = current + + # DB์—์„œ 2h ์ „ ๊ฐ€๊ฒฉ ์กฐํšŒ โ†’ ์—†์œผ๋ฉด API ์บ”๋“ค๋กœ ๋Œ€์ฒด + past = get_price_n_hours_ago(ticker, TREND_HOURS) + if past is None: + df = pyupbit.get_ohlcv(ticker, interval="minute60", count=4) + if df is not None and len(df) >= 3: + past = float(df["close"].iloc[-3]) + + if past: + trend = (current - past) / past * 100 + score += trend * weight + logger.debug(f"[๋ ˆ์ง] {ticker} {trend:+.2f}% (๊ธฐ์—ฌ {trend*weight:+.3f})") + + except Exception as e: + logger.warning(f"[๋ ˆ์ง] {ticker} ์˜ค๋ฅ˜: {e}") + + # ํ˜„์žฌ๊ฐ€ DB ์ €์žฅ (๋‹ค์Œ ๋ ˆ์ง ๊ณ„์‚ฐ ๋ฐ ์ถ”์„ธ ํŒ๋‹จ์— ์žฌํ™œ์šฉ) + if current_prices: + try: + insert_prices(current_prices) + except Exception as e: + logger.warning(f"[๋ ˆ์ง] ๊ฐ€๊ฒฉ ์ €์žฅ ์˜ค๋ฅ˜: {e}") + + # ๋ ˆ์ง ๊ฒฐ์ • + if score >= BULL_THRESHOLD: + name = "bull" + elif score < BEAR_THRESHOLD: + name = "bear" + else: + name = "neutral" + + params = REGIME_PARAMS[name] + result = {"name": name, "score": round(score, 3), **params} + + logger.info( + f"[๋ ˆ์ง] score={score:+.3f}% โ†’ {params['emoji']} {name.upper()} " + f"(TRENDโ‰ฅ{params['trend_pct']}% / VOLโ‰ฅ{params['vol_mult']}x)" + ) + + _cache = result + _cache_ts = time.time() + return result diff --git a/core/notify.py b/core/notify.py index 0226c9c..8f2a9dc 100644 --- a/core/notify.py +++ b/core/notify.py @@ -28,21 +28,36 @@ def _send(text: str) -> None: logger.error(f"Telegram ์•Œ๋ฆผ ์‹คํŒจ: {e}") -def notify_buy(ticker: str, price: float, amount: float, invested_krw: int) -> None: +def notify_buy( + ticker: str, price: float, amount: float, invested_krw: int, + max_budget: int = 0, per_position: int = 0, +) -> None: + budget_line = ( + f"์šด์šฉ์˜ˆ์‚ฐ: {max_budget:,}์› (ํฌ์ง€์…˜๋‹น {per_position:,}์›)\n" + if max_budget else "" + ) _send( f"๐Ÿ“ˆ [๋งค์ˆ˜] {ticker}\n" f"๊ฐ€๊ฒฉ: {price:,.0f}์›\n" f"์ˆ˜๋Ÿ‰: {amount}\n" - f"ํˆฌ์ž๊ธˆ: {invested_krw:,}์›" + f"ํˆฌ์ž๊ธˆ: {invested_krw:,}์›\n" + f"{budget_line}" ) -def notify_sell(ticker: str, price: float, pnl_pct: float, reason: str) -> None: - emoji = "โœ…" if pnl_pct >= 0 else "๐Ÿ”ด" +def notify_sell( + ticker: str, price: float, pnl_pct: float, reason: str, + krw_profit: float = 0.0, fee_krw: float = 0.0, + cum_profit: float = 0.0, +) -> None: + trade_emoji = "โœ…" if pnl_pct >= 0 else "โŒ" + cum_emoji = "๐Ÿ’š" if cum_profit >= 0 else "๐Ÿ”ด" _send( - f"{emoji} [๋งค๋„] {ticker}\n" + f"{trade_emoji} [๋งค๋„] {ticker}\n" f"๊ฐ€๊ฒฉ: {price:,.0f}์›\n" - f"์ˆ˜์ต๋ฅ : {pnl_pct:+.1f}%\n" + f"์ˆ˜์ต๋ฅ : {pnl_pct:+.2f}%\n" + f"์‹ค์†์ต: {krw_profit:+,.0f}์› (์ˆ˜์ˆ˜๋ฃŒ {fee_krw:,.0f}์›)\n" + f"{cum_emoji} ๋ˆ„์ ์†์ต: {cum_profit:+,.0f}์›\n" f"์‚ฌ์œ : {reason}" ) @@ -51,19 +66,50 @@ def notify_error(message: str) -> None: _send(f"โš ๏ธ [์˜ค๋ฅ˜]\n{message}") -def notify_status(positions: dict) -> None: - """1์‹œ๊ฐ„๋งˆ๋‹ค ํฌ์ง€์…˜ ํ˜„ํ™ฉ ์š”์•ฝ ์ „์†ก.""" +def notify_status( + positions: dict, + max_budget: int = 0, + per_position: int = 0, + cum_profit: float = 0.0, +) -> None: + """์ •๊ฐ๋งˆ๋‹ค ์‹œ์žฅ ๋ ˆ์ง + 1์‹œ๊ฐ„ ์ด์ƒ ๋ณด์œ  ํฌ์ง€์…˜ ํ˜„ํ™ฉ ์ „์†ก.""" from datetime import datetime import pyupbit + from .market_regime import get_regime now = datetime.now().strftime("%H:%M") + cum_sign = "+" if cum_profit >= 0 else "" - if not positions: - _send(f"๐Ÿ“Š [{now} ํ˜„ํ™ฉ]\n๋ณด์œ  ํฌ์ง€์…˜ ์—†์Œ โ€” ๋งค์ˆ˜ ์‹ ํ˜ธ ๋Œ€๊ธฐ ์ค‘") + # ์‹œ์žฅ ๋ ˆ์ง + regime = get_regime() + regime_line = ( + f"{regime['emoji']} ์‹œ์žฅ: {regime['name'].upper()} " + f"(score {regime['score']:+.2f}%) " + f"| ์กฐ๊ฑด TRENDโ‰ฅ{regime['trend_pct']}% / VOLโ‰ฅ{regime['vol_mult']}x\n" + ) + + # 1์‹œ๊ฐ„ ์ด์ƒ ๋ณด์œ  ํฌ์ง€์…˜๋งŒ ํ•„ํ„ฐ + long_positions = { + ticker: pos for ticker, pos in positions.items() + if (datetime.now() - pos["entry_time"]).total_seconds() >= 3600 + } + + cum_emoji = "๐Ÿ’š" if cum_profit >= 0 else "๐Ÿ”ด" + budget_info = ( + f"๐Ÿ’ฐ ์šด์šฉ์˜ˆ์‚ฐ: {max_budget:,}์› | ํฌ์ง€์…˜๋‹น: {per_position:,}์›\n" + f"{cum_emoji} ๋ˆ„์ ์†์ต: {cum_sign}{cum_profit:,.0f}์›\n" + if max_budget else "" + ) + + # ํฌ์ง€์…˜ ์—†์–ด๋„ ๋ ˆ์ง ์ •๋ณด๋Š” ์ „์†ก + header = f"๐Ÿ“Š [{now} ํ˜„ํ™ฉ]\n{regime_line}{budget_info}" + + if not long_positions: + _send(header + "1h+ ๋ณด์œ  ํฌ์ง€์…˜ ์—†์Œ") return - lines = [f"๐Ÿ“Š [{now} ํ˜„ํ™ฉ]"] - for ticker, pos in positions.items(): + lines = [header] + for ticker, pos in long_positions.items(): current = pyupbit.get_current_price(ticker) if not current: continue @@ -73,9 +119,9 @@ def notify_status(positions: dict) -> None: elapsed = (datetime.now() - pos["entry_time"]).total_seconds() / 3600 emoji = "๐Ÿ“ˆ" if pnl >= 0 else "๐Ÿ“‰" lines.append( - f"\n{emoji} {ticker}\n" + f"{emoji} {ticker}\n" f" ํ˜„์žฌ๊ฐ€: {current:,.0f}์›\n" - f" ์ˆ˜์ต๋ฅ : {pnl:+.1f}%\n" + f" ์ˆ˜์ต๋ฅ : {pnl:+.2f}%\n" f" ์ตœ๊ณ ๊ฐ€ ๋Œ€๋น„: -{drop:.1f}%\n" f" ๋ณด์œ : {elapsed:.1f}h" ) diff --git a/core/price_collector.py b/core/price_collector.py index 149544e..0dd1114 100644 --- a/core/price_collector.py +++ b/core/price_collector.py @@ -9,6 +9,7 @@ import pyupbit import requests from .market import get_top_tickers +from .market_regime import LEADERS from .price_db import cleanup_old_prices, insert_prices, insert_prices_with_time logger = logging.getLogger(__name__) @@ -27,6 +28,10 @@ def backfill_prices(hours: int = 48) -> None: if not tickers: logger.warning("[๋ฐฑํ•„] ์ข…๋ชฉ ๋ชฉ๋ก ์—†์Œ, ์Šคํ‚ต") return + # ๋Œ€์žฅ ์ฝ”์ธ ํ•ญ์ƒ ํฌํ•จ + for leader in LEADERS: + if leader not in tickers: + tickers = tickers + [leader] count = hours + 2 # ์—ฌ์œ  ์žˆ๊ฒŒ ์š”์ฒญ total_rows = 0 @@ -59,6 +64,10 @@ def run_collector(interval: int = COLLECT_INTERVAL) -> None: tickers = get_top_tickers() if not tickers: continue + # ๋Œ€์žฅ ์ฝ”์ธ์€ top20 ๋ฐ–์ด์–ด๋„ ํ•ญ์ƒ ํฌํ•จ + for leader in LEADERS: + if leader not in tickers: + tickers = tickers + [leader] resp = requests.get( "https://api.upbit.com/v1/ticker", params={"markets": ",".join(tickers)}, diff --git a/core/price_db.py b/core/price_db.py index 2c17a01..d68e36b 100644 --- a/core/price_db.py +++ b/core/price_db.py @@ -206,6 +206,15 @@ def record_trade( ) +def get_cumulative_krw_profit() -> float: + """์ „์ฒด ๊ฑฐ๋ž˜ ๋ˆ„์  KRW ์†์ต ๋ฐ˜ํ™˜ (์ˆ˜์ˆ˜๋ฃŒ ์ฐจ๊ฐ ํ›„). ๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด 0.""" + with _conn() as conn: + cur = conn.cursor() + cur.execute("SELECT SUM(krw_profit) FROM trade_results WHERE krw_profit IS NOT NULL") + row = cur.fetchone() + return float(row[0]) if row and row[0] is not None else 0.0 + + def load_recent_wins(ticker: str, n: int = 5) -> list[bool]: """์ง์ „ N๊ฑด ๊ฑฐ๋ž˜์˜ ์Šน/ํŒจ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜ (์˜ค๋ž˜๋œ ์ˆœ). ์—†์œผ๋ฉด ๋นˆ ๋ฆฌ์ŠคํŠธ.""" sql = """ diff --git a/core/strategy.py b/core/strategy.py index 782c1b7..67fd822 100644 --- a/core/strategy.py +++ b/core/strategy.py @@ -7,22 +7,23 @@ import os import pyupbit from .market import get_current_price, get_ohlcv +from .market_regime import get_regime from .price_db import get_price_n_hours_ago logger = logging.getLogger(__name__) # ์ถ”์„ธ ํŒ๋‹จ: ํ˜„์žฌ ๊ธฐ์ค€ N์‹œ๊ฐ„ ์ „ DB ๊ฐ€๊ฒฉ ๋Œ€๋น„ +M% ์ด์ƒ์ด๋ฉด ์ƒ์Šน ์ค‘ TREND_HOURS = float(os.getenv("TREND_HOURS", "12")) -TREND_MIN_GAIN_PCT = float(os.getenv("TREND_MIN_GAIN_PCT", "3")) +TREND_MIN_GAIN_PCT = float(os.getenv("TREND_MIN_GAIN_PCT", "5")) # ๋ ˆ์ง์ด ์—†์„ ๋•Œ ๊ธฐ๋ณธ๊ฐ’ # ๋ชจ๋ฉ˜ํ…€: MA ๊ธฐ๊ฐ„, ๊ฑฐ๋ž˜๋Ÿ‰ ๊ธ‰์ฆ ๋ฐฐ์ˆ˜ MA_PERIOD = 20 -VOLUME_MULTIPLIER = float(os.getenv("VOLUME_MULTIPLIER", "1.2")) # ๋กœ์ปฌ 5h ํ‰๊ท  ๋Œ€๋น„ +VOLUME_MULTIPLIER = float(os.getenv("VOLUME_MULTIPLIER", "2.0")) # ๋ ˆ์ง์ด ์—†์„ ๋•Œ ๊ธฐ๋ณธ๊ฐ’ LOCAL_VOL_HOURS = 5 # ๋กœ์ปฌ ๊ธฐ์ค€ ์‹œ๊ฐ„ (h) -def check_trend(ticker: str) -> bool: - """์ƒ์Šน ์ถ”์„ธ ์กฐ๊ฑด: ํ˜„์žฌ๊ฐ€๊ฐ€ DB์— ์ €์žฅ๋œ N์‹œ๊ฐ„ ์ „ ๊ฐ€๊ฒฉ ๋Œ€๋น„ +M% ์ด์ƒ.""" +def check_trend(ticker: str, min_gain_pct: float) -> bool: + """์ƒ์Šน ์ถ”์„ธ ์กฐ๊ฑด: ํ˜„์žฌ๊ฐ€๊ฐ€ DB์— ์ €์žฅ๋œ N์‹œ๊ฐ„ ์ „ ๊ฐ€๊ฒฉ ๋Œ€๋น„ +min_gain_pct% ์ด์ƒ.""" past_price = get_price_n_hours_ago(ticker, TREND_HOURS) if past_price is None: logger.debug(f"[์ถ”์„ธ] {ticker} {TREND_HOURS:.0f}h ์ „ ๊ฐ€๊ฒฉ ์—†์Œ (์ˆ˜์ง‘ ์ค‘)") @@ -33,22 +34,22 @@ def check_trend(ticker: str) -> bool: return False gain_pct = (current - past_price) / past_price * 100 - result = gain_pct >= TREND_MIN_GAIN_PCT + result = gain_pct >= min_gain_pct if result: logger.info( f"[์ถ”์„ธโ†‘] {ticker} {TREND_HOURS:.0f}h ์ „={past_price:,.2f} " - f"ํ˜„์žฌ={current:,.2f} (+{gain_pct:.1f}%)" + f"ํ˜„์žฌ={current:,.2f} (+{gain_pct:.1f}% โ‰ฅ {min_gain_pct}%)" ) else: logger.debug( - f"[์ถ”์„ธโœ—] {ticker} {gain_pct:+.1f}% (๊ธฐ์ค€={TREND_MIN_GAIN_PCT:+.0f}%)" + f"[์ถ”์„ธโœ—] {ticker} {gain_pct:+.1f}% (๊ธฐ์ค€={min_gain_pct:+.0f}%)" ) return result -def check_momentum(ticker: str) -> bool: - """๋ชจ๋ฉ˜ํ…€ ์กฐ๊ฑด: ํ˜„์žฌ๊ฐ€ > MA20(์ผ๋ด‰) AND ์ตœ๊ทผ 1h ๊ฑฐ๋ž˜๋Ÿ‰ > ๋กœ์ปฌ 5h ํ‰๊ท  ร— 1.2. +def check_momentum(ticker: str, vol_mult: float) -> bool: + """๋ชจ๋ฉ˜ํ…€ ์กฐ๊ฑด: ํ˜„์žฌ๊ฐ€ > MA20(์ผ๋ด‰) AND ์ตœ๊ทผ 1h ๊ฑฐ๋ž˜๋Ÿ‰ > ๋กœ์ปฌ 5h ํ‰๊ท  ร— vol_mult. 23h ํ‰๊ท ์€ ๋‚ฎ ์‹œ๊ฐ„๋Œ€ ๊ณ ๊ฑฐ๋ž˜๋Ÿ‰์ด ํฌํ•จ๋ผ ์ƒˆ๋ฒฝ์— ํ•ญ์ƒ ๋ฏธ๋‹ฌํ•˜๋ฏ€๋กœ, ๋กœ์ปฌ 5h ํ‰๊ท (๊ฐ™์€ ์‹œ๊ฐ„๋Œ€ ์ปจํ…์ŠคํŠธ)๊ณผ ๋น„๊ตํ•œ๋‹ค. @@ -79,24 +80,28 @@ def check_momentum(ticker: str) -> bool: recent_vol = df_hour["volume"].iloc[-2] # ์ง์ „ ์™„์„ฑ๋œ 1h ๋ด‰ local_avg = df_hour["volume"].iloc[-(LOCAL_VOL_HOURS + 1):-2].mean() # ์ด์ „ LOCAL_VOL_HOURS h ํ‰๊ท  - vol_ok = local_avg > 0 and recent_vol >= local_avg * VOLUME_MULTIPLIER + vol_ok = local_avg > 0 and recent_vol >= local_avg * vol_mult ratio = recent_vol / local_avg if local_avg > 0 else 0 if vol_ok: logger.info( f"[๋ชจ๋ฉ˜ํ…€โ†‘] {ticker} ํ˜„์žฌ={current:,.0f} MA20={ma:,.0f} " - f"1h๊ฑฐ๋ž˜๋Ÿ‰={recent_vol:.0f} ๋กœ์ปฌ{LOCAL_VOL_HOURS}hํ‰๊ท ={local_avg:.0f} ({ratio:.2f}x)" + f"1h๊ฑฐ๋ž˜๋Ÿ‰={recent_vol:.0f} ๋กœ์ปฌ{LOCAL_VOL_HOURS}hํ‰๊ท ={local_avg:.0f} ({ratio:.2f}x โ‰ฅ {vol_mult}x)" ) else: logger.debug( f"[๋ชจ๋ฉ˜ํ…€โœ—] {ticker} 1h๊ฑฐ๋ž˜๋Ÿ‰={recent_vol:.0f} ๋กœ์ปฌ{LOCAL_VOL_HOURS}hํ‰๊ท ={local_avg:.0f} " - f"({ratio:.2f}x < {VOLUME_MULTIPLIER}x)" + f"({ratio:.2f}x < {vol_mult}x)" ) return vol_ok def should_buy(ticker: str) -> bool: - """Strategy C: ์‹ค์‹œ๊ฐ„ ์ƒ์Šน ์ถ”์„ธ AND ๊ฑฐ๋ž˜๋Ÿ‰ ๋ชจ๋ฉ˜ํ…€ ๋ชจ๋‘ ์ถฉ์กฑ ์‹œ True.""" - if not check_trend(ticker): + """Strategy C + ์‹œ์žฅ ๋ ˆ์ง: ๋ ˆ์ง๋ณ„ ๋™์  ์ž„๊ณ„๊ฐ’์œผ๋กœ ์ถ”์„ธ AND ๋ชจ๋ฉ˜ํ…€ ํŒ๋‹จ.""" + regime = get_regime() + trend_pct = regime["trend_pct"] + vol_mult = regime["vol_mult"] + + if not check_trend(ticker, trend_pct): return False - return check_momentum(ticker) + return check_momentum(ticker, vol_mult) diff --git a/core/trader.py b/core/trader.py index 4f7aabf..34750fe 100644 --- a/core/trader.py +++ b/core/trader.py @@ -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}") diff --git a/main.py b/main.py index bb0e335..85f4634 100644 --- a/main.py +++ b/main.py @@ -20,23 +20,32 @@ logging.basicConfig( from core.monitor import run_monitor from core.notify import notify_error, notify_status from core.price_collector import backfill_prices, run_collector -from core.trader import get_positions, restore_positions +from core.price_db import get_cumulative_krw_profit +from core.trader import get_positions, get_budget_info, restore_positions from daemon.runner import run_scanner -STATUS_INTERVAL = 3600 # 1์‹œ๊ฐ„๋งˆ๋‹ค ์š”์•ฝ ์ „์†ก - -def run_status_reporter(interval: int = STATUS_INTERVAL) -> None: - """์ฃผ๊ธฐ์ ์œผ๋กœ ํฌ์ง€์…˜ ํ˜„ํ™ฉ์„ Telegram์œผ๋กœ ์ „์†ก.""" +def run_status_reporter() -> None: + """๋งค ์ •๊ฐ๋งˆ๋‹ค 1์‹œ๊ฐ„ ์ด์ƒ ๋ณด์œ  ํฌ์ง€์…˜ ํ˜„ํ™ฉ ์ „์†ก.""" + import datetime as _dt logger = logging.getLogger("status") - logger.info(f"์ƒํƒœ ๋ฆฌํฌํ„ฐ ์‹œ์ž‘ (์ฃผ๊ธฐ={interval//60}๋ถ„)") - time.sleep(interval) # ์ฒซ ์ „์†ก์€ 1์‹œ๊ฐ„ ํ›„ + logger.info("์ƒํƒœ ๋ฆฌํฌํ„ฐ ์‹œ์ž‘ (๋งค ์ •๊ฐ ํŠธ๋ฆฌ๊ฑฐ)") while True: + now = _dt.datetime.now() + # ๋‹ค์Œ ์ •๊ฐ๊นŒ์ง€ ๋Œ€๊ธฐ + secs_to_next_hour = (60 - now.minute) * 60 - now.second + time.sleep(secs_to_next_hour) try: - notify_status(dict(get_positions())) + budget = get_budget_info() + cum = get_cumulative_krw_profit() + notify_status( + dict(get_positions()), + max_budget=budget["max_budget"], + per_position=budget["per_position"], + cum_profit=cum, + ) except Exception as e: logger.error(f"์ƒํƒœ ๋ฆฌํฌํŠธ ์˜ค๋ฅ˜: {e}") - time.sleep(interval) def main() -> None: @@ -55,7 +64,7 @@ def main() -> None: ) monitor_thread.start() - # 1์‹œ๊ฐ„ ์ฃผ๊ธฐ ์ƒํƒœ ๋ฆฌํฌํŠธ ์Šค๋ ˆ๋“œ + # ๋งค ์ •๊ฐ ์ƒํƒœ ๋ฆฌํฌํŠธ ์Šค๋ ˆ๋“œ (1์‹œ๊ฐ„ ์ด์ƒ ๋ณด์œ  ํฌ์ง€์…˜๋งŒ) status_thread = threading.Thread( target=run_status_reporter, daemon=True, name="status" )