diff --git a/check_errors.py b/check_errors.py index 5872882..6618b65 100755 --- a/check_errors.py +++ b/check_errors.py @@ -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 diff --git a/cost_tracking.json b/cost_tracking.json index b6636f7..e8ebad8 100644 --- a/cost_tracking.json +++ b/cost_tracking.json @@ -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 } \ No newline at end of file diff --git a/data_fetcher.py b/data_fetcher.py index 699b69f..835d073 100644 --- a/data_fetcher.py +++ b/data_fetcher.py @@ -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( diff --git a/llm_analyzer.py b/llm_analyzer.py index a2165bc..4ff4708 100644 --- a/llm_analyzer.py +++ b/llm_analyzer.py @@ -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()) diff --git a/main.py b/main.py index da36461..ab12413 100644 --- a/main.py +++ b/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) diff --git a/portfolio.py b/portfolio.py index 1ae439c..d576f83 100644 --- a/portfolio.py +++ b/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: diff --git a/risk_manager.py b/risk_manager.py index 6184456..9a69323 100644 --- a/risk_manager.py +++ b/risk_manager.py @@ -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: diff --git a/sync_cost_basis.py b/sync_cost_basis.py index c1a73f9..3b77c01 100644 --- a/sync_cost_basis.py +++ b/sync_cost_basis.py @@ -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") diff --git a/trade_logger.py b/trade_logger.py index 9720799..c901468 100644 --- a/trade_logger.py +++ b/trade_logger.py @@ -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", ""), diff --git a/trader.py b/trader.py index f021444..ccbab60 100644 --- a/trader.py +++ b/trader.py @@ -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