Fix SELL P&L calculation, add debug logging, and multiple improvements
- Fix realized_pnl always being 0 on SELL (use amount*price instead of amount_usdt) - Add debug logging for all-HOLD LLM cycles (log sample HOLD reasons) - Fix nonce collision in Bitfinex API auth (add counter) - Fix timezone to UTC+8 in main, trade_logger, sync_cost_basis, check_errors - Fix stop-loss retry with longer delay after cancel - Add min order amount check in trader before BUY - Fix risk_manager to skip positions with amount <= 0 - Cap trade_history to 500 entries to prevent unbounded growth - Fix greedy regex in LLM response parser - Reset cost_tracking start dates to null Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
41ad549ae2
commit
b6bd45b151
@ -6,7 +6,7 @@ import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
@ -24,8 +24,9 @@ LOOKBACK_MINUTES = 60
|
||||
|
||||
|
||||
def collect_recent_errors() -> list[str]:
|
||||
since = datetime.now() - timedelta(minutes=LOOKBACK_MINUTES)
|
||||
since_str = since.strftime("%Y-%m-%d %H:%M")
|
||||
_TZ_UTC8 = timezone(timedelta(hours=8))
|
||||
since = datetime.now(_TZ_UTC8) - timedelta(minutes=LOOKBACK_MINUTES)
|
||||
since_str = since.strftime("%Y-%m-%d %H:%M:%S")
|
||||
errors = []
|
||||
|
||||
for logfile in LOG_FILES:
|
||||
@ -38,7 +39,7 @@ def collect_recent_errors() -> list[str]:
|
||||
continue
|
||||
if not line[:4].isdigit():
|
||||
continue
|
||||
if line[:16] >= since_str:
|
||||
if line[:19] >= since_str:
|
||||
errors.append(line.rstrip())
|
||||
|
||||
return errors
|
||||
|
||||
@ -1,16 +1,16 @@
|
||||
{
|
||||
"tETHUST": 1773100800000,
|
||||
"tSUIUST": 1773100800000,
|
||||
"tSOLUST": 1773100800000,
|
||||
"tLTCUST": 1773100800000,
|
||||
"tADAUST": 1773100800000,
|
||||
"tAVAX:UST": 1773100800000,
|
||||
"tUNIUST": 1773100800000,
|
||||
"tXRPUST": 1773100800000,
|
||||
"tDOGE:UST": 1773100800000,
|
||||
"tSHIB:UST": 1773100800000,
|
||||
"tBTCUST": 1773100800000,
|
||||
"tXLMUST": 1773100800000,
|
||||
"tLINK:UST": 1773100800000,
|
||||
"tDOTUST": 1773100800000
|
||||
"tETHUST": null,
|
||||
"tSUIUST": null,
|
||||
"tSOLUST": null,
|
||||
"tLTCUST": null,
|
||||
"tADAUST": null,
|
||||
"tAVAX:UST": null,
|
||||
"tUNIUST": null,
|
||||
"tXRPUST": null,
|
||||
"tDOGE:UST": null,
|
||||
"tSHIB:UST": null,
|
||||
"tBTCUST": null,
|
||||
"tXLMUST": null,
|
||||
"tLINK:UST": null,
|
||||
"tDOTUST": null
|
||||
}
|
||||
@ -27,9 +27,13 @@ def _pub_get(path: str, params: dict | None = None) -> dict | list:
|
||||
# Authenticated API helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_nonce_counter = 0
|
||||
|
||||
def _auth_post(path: str, body: dict | None = None) -> dict | list:
|
||||
global _nonce_counter
|
||||
body = body or {}
|
||||
nonce = str(int(time.time() * 1000000))
|
||||
_nonce_counter += 1
|
||||
nonce = str(int(time.time() * 1000000) + _nonce_counter)
|
||||
body_json = json.dumps(body)
|
||||
signature_payload = f"/api{path}{nonce}{body_json}"
|
||||
sig = hmac.new(
|
||||
|
||||
@ -113,7 +113,7 @@ def _parse_llm_response(raw: str) -> list[dict]:
|
||||
pass
|
||||
|
||||
# Try extracting JSON array from markdown code block or mixed text
|
||||
match = re.search(r'\[[\s\S]*?\]', raw)
|
||||
match = re.search(r'\[[\s\S]*\]', raw)
|
||||
if match:
|
||||
try:
|
||||
return json.loads(match.group())
|
||||
|
||||
28
main.py
28
main.py
@ -6,7 +6,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import config
|
||||
import data_fetcher
|
||||
@ -21,6 +21,8 @@ import trader
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging setup
|
||||
# ---------------------------------------------------------------------------
|
||||
_TZ_UTC8 = timezone(timedelta(hours=8))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
@ -29,6 +31,7 @@ logging.basicConfig(
|
||||
logging.FileHandler("trading.log"),
|
||||
],
|
||||
)
|
||||
logging.Formatter.converter = lambda *args: datetime.now(_TZ_UTC8).timetuple()
|
||||
logger = logging.getLogger("main")
|
||||
|
||||
STATE_FILE = "bot_state.json"
|
||||
@ -78,12 +81,13 @@ def run_cycle():
|
||||
state = _load_state()
|
||||
state["run_count"] = state.get("run_count", 0) + 1
|
||||
logger.info("=" * 60)
|
||||
logger.info("Trading cycle #%d at %s", state["run_count"], datetime.now().isoformat())
|
||||
logger.info("Trading cycle #%d at %s", state["run_count"], datetime.now(_TZ_UTC8).isoformat())
|
||||
|
||||
# 1. Load portfolio state
|
||||
port = pf.load_positions()
|
||||
|
||||
# 2. Fetch account status from exchange
|
||||
account_status = {"wallets": [], "positions": []}
|
||||
try:
|
||||
account_status = data_fetcher.fetch_account_status()
|
||||
port = pf.sync_with_exchange(port, account_status)
|
||||
@ -168,10 +172,15 @@ def run_cycle():
|
||||
logger.info("Cancelled outdated stop %s for %s (amt=%.6f, px=%.6g)",
|
||||
s["id"], sym, abs(s["amount"]), s["price"])
|
||||
tracked_stops.pop(sym, None)
|
||||
time.sleep(0.3) # Wait for Bitfinex to release locked balance
|
||||
time.sleep(1.0) # Wait for Bitfinex to release locked balance
|
||||
|
||||
logger.warning("Position %s missing/outdated stop-loss, placing now", sym)
|
||||
sl = trader.place_stop_loss_order(sym, amount, entry, stop_price=expected_stop)
|
||||
if sl is None and existing_stops:
|
||||
# Retry once — Bitfinex may need more time to release locked balance
|
||||
time.sleep(2.0)
|
||||
logger.info("Retrying stop-loss for %s after balance release delay", sym)
|
||||
sl = trader.place_stop_loss_order(sym, amount, entry, stop_price=expected_stop)
|
||||
if sl:
|
||||
# Update stop_orders_by_sym so step 10 sees the correct stop ID
|
||||
stop_orders_by_sym[sym] = [{"id": sl["order_id"], "symbol": sym,
|
||||
@ -343,6 +352,17 @@ def run_cycle():
|
||||
signals = llm_analyzer.analyze_market(indicator_summary, account_str)
|
||||
llm_ok = True
|
||||
logger.info("LLM returned %d signals", len(signals))
|
||||
non_hold = [s for s in signals if s.get("action") != "HOLD"]
|
||||
if non_hold:
|
||||
for s in non_hold:
|
||||
logger.info("LLM signal: %s %s conf=%.2f reason=%s",
|
||||
s.get("action"), s.get("symbol"), s.get("confidence", 0), s.get("reason", ""))
|
||||
else:
|
||||
# Debug: log a sample of HOLD reasons to diagnose all-HOLD cycles
|
||||
samples = signals[:3]
|
||||
for s in samples:
|
||||
logger.info("LLM HOLD sample: %s conf=%.2f reason=%s",
|
||||
s.get("symbol"), s.get("confidence", 0), s.get("reason", ""))
|
||||
except Exception as e:
|
||||
logger.error("LLM analysis failed: %s", e)
|
||||
slack_notifier.send_error_alert(f"LLM analysis failed: {e}")
|
||||
@ -398,7 +418,7 @@ def run_cycle():
|
||||
entry_price = pos.get("entry_price", 0)
|
||||
if entry_price:
|
||||
cost_basis = entry_price * amount
|
||||
realized_pnl = amount_usdt - cost_basis
|
||||
realized_pnl = amount * price - cost_basis
|
||||
realized_pnl_pct = (price - entry_price) / entry_price * 100
|
||||
|
||||
port = pf.update_position(port, sym, action, amount, price, amount_usdt)
|
||||
|
||||
11
portfolio.py
11
portfolio.py
@ -76,7 +76,8 @@ def update_position(portfolio: dict, symbol: str, action: str, amount: float, pr
|
||||
portfolio["available_usdt"] = portfolio.get("available_usdt", 0) + sell_amount * price
|
||||
|
||||
# Record trade
|
||||
portfolio.setdefault("trade_history", []).append({
|
||||
history = portfolio.setdefault("trade_history", [])
|
||||
history.append({
|
||||
"symbol": symbol,
|
||||
"action": action,
|
||||
"amount": amount,
|
||||
@ -84,6 +85,8 @@ def update_position(portfolio: dict, symbol: str, action: str, amount: float, pr
|
||||
"amount_usdt": amount_usdt,
|
||||
"timestamp": time.time(),
|
||||
})
|
||||
if len(history) > 500:
|
||||
portfolio["trade_history"] = history[-500:]
|
||||
|
||||
portfolio["last_updated"] = time.time()
|
||||
return portfolio
|
||||
@ -135,7 +138,11 @@ def get_portfolio_summary(portfolio: dict, current_prices: dict) -> str:
|
||||
lines.append("")
|
||||
|
||||
lines.append(f"*Total Portfolio Value:* {total_value:.2f} USDT")
|
||||
lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT ({total_unrealized / (total_value - total_unrealized) * 100:+.2f}%)" if (total_value - total_unrealized) > 0 else f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT")
|
||||
cost_basis = total_value - total_unrealized
|
||||
if cost_basis > 0:
|
||||
lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT ({total_unrealized / cost_basis * 100:+.2f}%)")
|
||||
else:
|
||||
lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.2f} USDT")
|
||||
|
||||
initial_capital = portfolio.get("initial_capital", 0)
|
||||
if initial_capital > 0:
|
||||
|
||||
@ -50,7 +50,7 @@ def apply_stop_loss_take_profit(positions: dict, current_prices: dict) -> list[d
|
||||
"""
|
||||
signals = []
|
||||
for sym, pos in positions.items():
|
||||
if pos.get("amount", 0) == 0:
|
||||
if pos.get("amount", 0) <= 0:
|
||||
continue
|
||||
entry = pos.get("entry_price", 0)
|
||||
if entry <= 0:
|
||||
|
||||
@ -9,6 +9,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
import config
|
||||
import data_fetcher
|
||||
@ -17,6 +18,8 @@ import slack_notifier
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging setup (separate log file)
|
||||
# ---------------------------------------------------------------------------
|
||||
_TZ_UTC8 = timezone(timedelta(hours=8))
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
@ -25,6 +28,7 @@ logging.basicConfig(
|
||||
logging.FileHandler("sync_cost_basis.log"),
|
||||
],
|
||||
)
|
||||
logging.Formatter.converter = lambda *args: datetime.now(_TZ_UTC8).timetuple()
|
||||
logger = logging.getLogger("sync_cost_basis")
|
||||
|
||||
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
|
||||
import csv
|
||||
import os
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
_TZ_UTC8 = timezone(timedelta(hours=8))
|
||||
|
||||
import config
|
||||
|
||||
@ -26,7 +28,7 @@ def log_trade(trade: dict):
|
||||
sym = trade.get("symbol", "")
|
||||
row = {
|
||||
"timestamp": trade.get("timestamp", ""),
|
||||
"datetime": datetime.now().isoformat(timespec="seconds"),
|
||||
"datetime": datetime.now(_TZ_UTC8).isoformat(timespec="seconds"),
|
||||
"symbol": sym,
|
||||
"name": config.SYMBOL_NAMES.get(sym, sym),
|
||||
"action": trade.get("action", ""),
|
||||
|
||||
@ -26,6 +26,11 @@ def execute_trade(signal: dict, current_prices: dict, mode: str | None = None) -
|
||||
amount = -math.floor(abs(signal["sell_amount"]) * 1e8) / 1e8
|
||||
else:
|
||||
amount = amount_usdt / price
|
||||
if action == "BUY":
|
||||
min_amt = config.MIN_ORDER_AMOUNT.get(symbol, 0)
|
||||
if min_amt > 0 and amount < min_amt:
|
||||
logger.warning("BUY %s: amount %.6f < min %s, skipping", symbol, amount, min_amt)
|
||||
return None
|
||||
if action == "SELL":
|
||||
amount = -abs(amount) # negative = sell on Bitfinex
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user