fix: 저가 코인 소수점 표시 + VOL_MIN 6.0 조정
- fp() 헬퍼: 100원 미만 코인 소수점 표시 (HOLO 등) - VOL_MIN 8→6: 신호 빈도 적정화 - LLM 로그 가격도 :,.2f 통일 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -590,7 +590,7 @@ F&G지수: {fng} ({'공포' if fng <= 40 else '중립' if fng <= 50 else '탐욕
|
|||||||
|
|
||||||
if data.get('action') == 'buy':
|
if data.get('action') == 'buy':
|
||||||
data['price'] = float(data['price'])
|
data['price'] = float(data['price'])
|
||||||
log.info(f'[LLM매수] {ticker} → buy {data["price"]:,.0f}원 | {confidence} | {status} | {reason}')
|
log.info(f'[LLM매수] {ticker} → buy {data["price"]:,.2f}원 | {confidence} | {status} | {reason}')
|
||||||
return data # action=buy, price=float
|
return data # action=buy, price=float
|
||||||
|
|
||||||
log.warning(f'[LLM매수] {ticker} 알 수 없는 action: {data}')
|
log.warning(f'[LLM매수] {ticker} 알 수 없는 action: {data}')
|
||||||
@@ -646,7 +646,7 @@ def get_exit_price(
|
|||||||
data['price'] = float(data['price'])
|
data['price'] = float(data['price'])
|
||||||
pnl_from_entry = (data['price'] - entry_price) / entry_price * 100
|
pnl_from_entry = (data['price'] - entry_price) / entry_price * 100
|
||||||
log.info(
|
log.info(
|
||||||
f'[LLM매도] {ticker} 지정가 교체: {current_target:,.0f} → {data["price"]:,.0f}원 '
|
f'[LLM매도] {ticker} 지정가 교체: {current_target:,.2f} → {data["price"]:,.2f}원 '
|
||||||
f'(진입 대비 {pnl_from_entry:+.2f}%) | {confidence} | {status} | watch={watch} | {reason}'
|
f'(진입 대비 {pnl_from_entry:+.2f}%) | {confidence} | {status} | watch={watch} | {reason}'
|
||||||
)
|
)
|
||||||
return data # action=sell, price=float
|
return data # action=sell, price=float
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ TICKERS = [
|
|||||||
BAR_SEC = 20 # 봉 주기 (초)
|
BAR_SEC = 20 # 봉 주기 (초)
|
||||||
VOL_LOOKBACK = 61 # 거래량 평균 기준 봉 수
|
VOL_LOOKBACK = 61 # 거래량 평균 기준 봉 수
|
||||||
ATR_LOOKBACK = 28 # ATR 계산 봉 수
|
ATR_LOOKBACK = 28 # ATR 계산 봉 수
|
||||||
VOL_MIN = 8.0 # 거래량 배수 임계값
|
VOL_MIN = 6.0 # 거래량 배수 임계값
|
||||||
BUY_TIMEOUT = 60 # 지정가 매수 미체결 타임아웃 (초)
|
BUY_TIMEOUT = 60 # 지정가 매수 미체결 타임아웃 (초)
|
||||||
|
|
||||||
MAX_POS = int(os.environ.get('MAX_POSITIONS', 3))
|
MAX_POS = int(os.environ.get('MAX_POSITIONS', 3))
|
||||||
@@ -80,6 +80,16 @@ logging.basicConfig(
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def fp(price: float) -> str:
|
||||||
|
"""가격을 단위에 맞게 포맷. 100원 미만은 소수점 표시."""
|
||||||
|
if price >= 100:
|
||||||
|
return f"{price:,.0f}"
|
||||||
|
elif price >= 10:
|
||||||
|
return f"{price:,.1f}"
|
||||||
|
else:
|
||||||
|
return f"{price:,.2f}"
|
||||||
|
|
||||||
|
|
||||||
def tg(msg: str) -> None:
|
def tg(msg: str) -> None:
|
||||||
if not TG_TOKEN or not TG_CHAT_ID:
|
if not TG_TOKEN or not TG_CHAT_ID:
|
||||||
return
|
return
|
||||||
@@ -301,7 +311,7 @@ def process_signal(sig: dict) -> None:
|
|||||||
log.info(f"[시그널] {ticker} 포지션 한도 도달 → 스킵")
|
log.info(f"[시그널] {ticker} 포지션 한도 도달 → 스킵")
|
||||||
return
|
return
|
||||||
|
|
||||||
log.info(f"[시그널] {ticker} {cur_price:,.0f}원 vol {vol_ratio:.1f}x → LLM 판단 요청")
|
log.info(f"[시그널] {ticker} {fp(cur_price)}원 vol {vol_ratio:.1f}x → LLM 판단 요청")
|
||||||
|
|
||||||
llm_result = get_entry_price(
|
llm_result = get_entry_price(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
@@ -318,7 +328,7 @@ def process_signal(sig: dict) -> None:
|
|||||||
log.info(f"[매수/LLM] {ticker} → 스킵 | {reason}")
|
log.info(f"[매수/LLM] {ticker} → 스킵 | {reason}")
|
||||||
tg(
|
tg(
|
||||||
f"⏭️ <b>매수 스킵</b> {ticker}\n"
|
f"⏭️ <b>매수 스킵</b> {ticker}\n"
|
||||||
f"현재가: {cur_price:,.0f}원 볼륨: {vol_ratio:.1f}x\n"
|
f"현재가: {fp(cur_price)}원 볼륨: {vol_ratio:.1f}x\n"
|
||||||
f"시장: {status}\n"
|
f"시장: {status}\n"
|
||||||
f"사유: {reason}"
|
f"사유: {reason}"
|
||||||
)
|
)
|
||||||
@@ -335,7 +345,7 @@ def process_signal(sig: dict) -> None:
|
|||||||
status = llm_result.get('market_status', '')
|
status = llm_result.get('market_status', '')
|
||||||
qty = PER_POS * (1 - FEE) / buy_price
|
qty = PER_POS * (1 - FEE) / buy_price
|
||||||
diff_pct = (buy_price - cur_price) / cur_price * 100
|
diff_pct = (buy_price - cur_price) / cur_price * 100
|
||||||
log.info(f"[매수/LLM] {ticker} → 승인 {buy_price:,.0f}원 (현재가 {cur_price:,.0f}원, 차이 {diff_pct:+.2f}%)")
|
log.info(f"[매수/LLM] {ticker} → 승인 {fp(buy_price)}원 (현재가 {fp(cur_price)}원, 차이 {diff_pct:+.2f}%)")
|
||||||
|
|
||||||
if SIM_MODE:
|
if SIM_MODE:
|
||||||
uuid = f"sim-buy-{ticker}"
|
uuid = f"sim-buy-{ticker}"
|
||||||
@@ -357,10 +367,10 @@ def process_signal(sig: dict) -> None:
|
|||||||
'ts': datetime.now(),
|
'ts': datetime.now(),
|
||||||
'vol_ratio': vol_ratio,
|
'vol_ratio': vol_ratio,
|
||||||
}
|
}
|
||||||
log.info(f"[지정가매수] {ticker} {buy_price:,.0f}원 수량: {qty:.6f}")
|
log.info(f"[지정가매수] {ticker} {fp(buy_price)}원 수량: {qty:.6f}")
|
||||||
tg(
|
tg(
|
||||||
f"📥 <b>지정가 매수</b> {ticker}\n"
|
f"📥 <b>지정가 매수</b> {ticker}\n"
|
||||||
f"지정가: {buy_price:,.0f}원 (현재가 대비 {diff_pct:+.2f}%)\n"
|
f"지정가: {fp(buy_price)}원 (현재가 대비 {diff_pct:+.2f}%)\n"
|
||||||
f"수량: {qty:.6f} 볼륨: {vol_ratio:.1f}x\n"
|
f"수량: {qty:.6f} 볼륨: {vol_ratio:.1f}x\n"
|
||||||
f"확신: {confidence} 시장: {status}\n"
|
f"확신: {confidence} 시장: {status}\n"
|
||||||
f"LLM: {reason}\n"
|
f"LLM: {reason}\n"
|
||||||
@@ -384,7 +394,7 @@ def check_pending_buys() -> None:
|
|||||||
if SIM_MODE:
|
if SIM_MODE:
|
||||||
bar_list = list(bars.get(ticker, []))
|
bar_list = list(bars.get(ticker, []))
|
||||||
if bar_list and bar_list[-1]['low'] <= pb['price']:
|
if bar_list and bar_list[-1]['low'] <= pb['price']:
|
||||||
log.info(f"[SIM 매수체결] {ticker} {pb['price']:,.0f}원")
|
log.info(f"[SIM 매수체결] {ticker} {fp(pb['price'])}원")
|
||||||
_activate_position(ticker, pb['price'], pb['qty'], pb['vol_ratio'])
|
_activate_position(ticker, pb['price'], pb['qty'], pb['vol_ratio'])
|
||||||
del pending_buys[ticker]
|
del pending_buys[ticker]
|
||||||
continue
|
continue
|
||||||
@@ -401,7 +411,7 @@ def check_pending_buys() -> None:
|
|||||||
if elapsed >= BUY_TIMEOUT:
|
if elapsed >= BUY_TIMEOUT:
|
||||||
cancel_order_safe(pb['uuid'])
|
cancel_order_safe(pb['uuid'])
|
||||||
log.info(f"[매수취소] {ticker} {elapsed:.0f}초 미체결 → 취소")
|
log.info(f"[매수취소] {ticker} {elapsed:.0f}초 미체결 → 취소")
|
||||||
tg(f"❌ <b>매수 취소</b> {ticker}\n{pb['price']:,.0f}원 {elapsed:.0f}초 미체결")
|
tg(f"❌ <b>매수 취소</b> {ticker}\n{fp(pb['price'])}원 {elapsed:.0f}초 미체결")
|
||||||
del pending_buys[ticker]
|
del pending_buys[ticker]
|
||||||
|
|
||||||
|
|
||||||
@@ -422,11 +432,11 @@ def _activate_position(ticker: str, entry_price: float, qty: float, vol_ratio: f
|
|||||||
'llm_last_ts': None,
|
'llm_last_ts': None,
|
||||||
'llm_active': False,
|
'llm_active': False,
|
||||||
}
|
}
|
||||||
log.info(f"[진입] {ticker} {entry_price:,.0f}원 vol {vol_ratio:.1f}x 지정가 {tag} {target:,.0f}원")
|
log.info(f"[진입] {ticker} {fp(entry_price)}원 vol {vol_ratio:.1f}x 지정가 {tag} {fp(target)}원")
|
||||||
tg(
|
tg(
|
||||||
f"🟢 <b>매수 체결</b> {ticker}\n"
|
f"🟢 <b>매수 체결</b> {ticker}\n"
|
||||||
f"체결가: {entry_price:,.0f}원 수량: {qty:.6f}\n"
|
f"체결가: {fp(entry_price)}원 수량: {qty:.6f}\n"
|
||||||
f"지정가 매도: {tag} {target:,.0f}원 (+{lr*100:.1f}%)\n"
|
f"지정가 매도: {tag} {fp(target)}원 (+{lr*100:.1f}%)\n"
|
||||||
f"{'[시뮬]' if SIM_MODE else '[실거래]'}"
|
f"{'[시뮬]' if SIM_MODE else '[실거래]'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -448,7 +458,7 @@ def _advance_stage(ticker: str) -> None:
|
|||||||
uuid = submit_limit_sell(ticker, pos['qty'], target)
|
uuid = submit_limit_sell(ticker, pos['qty'], target)
|
||||||
pos['sell_uuid'] = uuid
|
pos['sell_uuid'] = uuid
|
||||||
pos['sell_price'] = target
|
pos['sell_price'] = target
|
||||||
log.info(f"[단계전환] {ticker} → {tag} 목표가 {target:,.0f}원")
|
log.info(f"[단계전환] {ticker} → {tag} 목표가 {fp(target)}원")
|
||||||
else:
|
else:
|
||||||
pos['sell_uuid'] = None
|
pos['sell_uuid'] = None
|
||||||
pos['sell_price'] = None
|
pos['sell_price'] = None
|
||||||
@@ -470,11 +480,11 @@ def _record_exit(ticker: str, exit_price: float, tag: str) -> None:
|
|||||||
|
|
||||||
llm_flag = 'LLM' if pos.get('llm_active') else 'cascade'
|
llm_flag = 'LLM' if pos.get('llm_active') else 'cascade'
|
||||||
icon = "✅" if pnl > 0 else "🔴"
|
icon = "✅" if pnl > 0 else "🔴"
|
||||||
log.info(f"[청산/{tag}/{llm_flag}] {ticker} {exit_price:,.0f}원 PNL {pnl:+.2f}% {krw:+,.0f}원 {held}초 보유")
|
log.info(f"[청산/{tag}/{llm_flag}] {ticker} {fp(exit_price)}원 PNL {pnl:+.2f}% {krw:+,.0f}원 {held}초 보유")
|
||||||
tg(
|
tg(
|
||||||
f"{icon} <b>청산</b> {ticker} [{reason_tag}] ({llm_flag})\n"
|
f"{icon} <b>청산</b> {ticker} [{reason_tag}] ({llm_flag})\n"
|
||||||
f"진입: {pos['entry_price']:,.0f}원\n"
|
f"진입: {fp(pos['entry_price'])}원\n"
|
||||||
f"청산: {exit_price:,.0f}원\n"
|
f"청산: {fp(exit_price)}원\n"
|
||||||
f"PNL: <b>{pnl:+.2f}%</b> ({krw:+,.0f}원) {held}초 보유\n"
|
f"PNL: <b>{pnl:+.2f}%</b> ({krw:+,.0f}원) {held}초 보유\n"
|
||||||
f"{'[시뮬]' if SIM_MODE else '[실거래]'}"
|
f"{'[시뮬]' if SIM_MODE else '[실거래]'}"
|
||||||
)
|
)
|
||||||
@@ -548,10 +558,10 @@ def check_filled_positions() -> None:
|
|||||||
pos['sell_uuid'] = new_uuid
|
pos['sell_uuid'] = new_uuid
|
||||||
pos['sell_price'] = new_price
|
pos['sell_price'] = new_price
|
||||||
pos['llm_active'] = True
|
pos['llm_active'] = True
|
||||||
log.info(f"[매도/LLM] {ticker} 지정가 {new_price:,.0f}원 설정")
|
log.info(f"[매도/LLM] {ticker} 지정가 {fp(new_price)}원 설정")
|
||||||
tg(
|
tg(
|
||||||
f"🤖 <b>LLM 매도 설정</b> {ticker}\n"
|
f"🤖 <b>LLM 매도 설정</b> {ticker}\n"
|
||||||
f"지정가: {new_price:,.0f}원 (진입 대비 {pnl_pct:+.2f}%)\n"
|
f"지정가: {fp(new_price)}원 (진입 대비 {pnl_pct:+.2f}%)\n"
|
||||||
f"확신: {confidence} 시장: {status} 관망: {'Y' if watch else 'N'}\n"
|
f"확신: {confidence} 시장: {status} 관망: {'Y' if watch else 'N'}\n"
|
||||||
f"LLM: {reason}"
|
f"LLM: {reason}"
|
||||||
)
|
)
|
||||||
@@ -675,8 +685,8 @@ def restore_positions() -> None:
|
|||||||
'llm_last_ts': None,
|
'llm_last_ts': None,
|
||||||
'llm_active': False,
|
'llm_active': False,
|
||||||
}
|
}
|
||||||
log.info(f"[복구] {ticker} 수량:{actual_bal:.6f} 매수평균:{avg:,.0f}원")
|
log.info(f"[복구] {ticker} 수량:{actual_bal:.6f} 매수평균:{fp(avg)}원")
|
||||||
tg(f"♻️ <b>포지션 복구</b> {ticker}\n매수평균: {avg:,.0f}원 수량: {actual_bal:.6f}")
|
tg(f"♻️ <b>포지션 복구</b> {ticker}\n매수평균: {fp(avg)}원 수량: {actual_bal:.6f}")
|
||||||
if positions:
|
if positions:
|
||||||
log.info(f"[복구] 총 {len(positions)}개 포지션 복구됨")
|
log.info(f"[복구] 총 {len(positions)}개 포지션 복구됨")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
Reference in New Issue
Block a user