Add deposit-based total return, session persistence flag, dual return rates

- Add fetch_total_deposits() with hourly local cache (deposit_cache.json)
- Use deposit total as capital base for accurate total return calculation
- Add --no-session-persistence to claude CLI subprocess calls
- Show both total return (deposit-based) and change rate (cost-based) in reports
- Update portfolio summary with Total Return line

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kroutony 2026-03-15 10:21:39 +00:00
parent ddc1b9e3eb
commit 17303d5d3d
6 changed files with 118 additions and 34 deletions

View File

@ -1,15 +1,16 @@
{ {
"tETHUST": null, "tETHUST": 1773352562967,
"tSUIUST": 1773362755397, "tSUIUST": null,
"tSOLUST": null, "tSOLUST": 1773363657396,
"tLTCUST": null, "tLTCUST": null,
"tADAUST": null, "tADAUST": null,
"tAVAX:UST": 1773380455009, "tAVAX:UST": null,
"tUNIUST": 1773380455473, "tUNIUST": null,
"tXRPUST": null, "tXRPUST": 1773363657836,
"tDOGE:UST": 1773407762194, "tDOGE:UST": null,
"tSHIB:UST": null, "tSHIB:UST": null,
"tBTCUST": null, "tBTCUST": 1773345335546,
"tXLMUST": null, "tXLMUST": null,
"tLINK:UST": null "tLINK:UST": 1773361253747,
"tDOTUST": 1773362453371
} }

View File

@ -52,6 +52,60 @@ def _auth_post(path: str, body: dict | None = None) -> dict | list:
return resp.json() return resp.json()
# ---------------------------------------------------------------------------
# Deposit history (cached locally, refreshed hourly)
# ---------------------------------------------------------------------------
# 2026-03-10 00:00:00 UTC in milliseconds
_DEPOSIT_START_MS = 1773100800000
_DEPOSIT_CACHE_FILE = "deposit_cache.json"
_DEPOSIT_CACHE_TTL = 3600 # 1 hour
def fetch_total_deposits() -> float:
"""Return net USDT deposits since 2026-03-10. Uses local cache, refreshes hourly."""
# Try cache first
if os.path.exists(_DEPOSIT_CACHE_FILE):
try:
with open(_DEPOSIT_CACHE_FILE) as f:
cache = json.load(f)
if time.time() - cache.get("ts", 0) < _DEPOSIT_CACHE_TTL:
return cache["total"]
except (json.JSONDecodeError, IOError, KeyError):
pass
# Cache miss or expired — fetch from API
total = _fetch_deposits_from_api()
if total > 0:
try:
with open(_DEPOSIT_CACHE_FILE, "w") as f:
json.dump({"total": total, "ts": time.time()}, f)
except IOError:
pass
return total
def _fetch_deposits_from_api() -> float:
"""Fetch net USDT deposits from Bitfinex API."""
try:
raw = _auth_post("/v2/auth/r/movements/UST/hist", {
"start": _DEPOSIT_START_MS,
"limit": 100,
})
except Exception as e:
logger.error("Failed to fetch deposit history: %s", e)
return 0.0
total = 0.0
for m in raw:
status = m[9] if len(m) > 9 else ""
if status != "COMPLETED":
continue
amount = float(m[12]) if len(m) > 12 else 0
total += amount # positive = deposit, negative = withdrawal
return total
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Public endpoints # Public endpoints
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -68,7 +68,7 @@ def analyze_market(indicator_summary: str, account_status: str) -> list[dict]:
try: try:
result = subprocess.run( result = subprocess.run(
["claude", "-p", prompt, "--output-format", "json"], ["claude", "-p", prompt, "--output-format", "json", "--no-session-persistence"],
capture_output=True, capture_output=True,
text=True, text=True,
timeout=120, timeout=120,

16
main.py
View File

@ -99,6 +99,12 @@ def run_cycle():
port["available_usdt"] = 10000 port["available_usdt"] = 10000
logger.info("Paper trading: using default 10000 USDT balance") logger.info("Paper trading: using default 10000 USDT balance")
# 2a. Fetch initial capital from deposit history
try:
port["initial_capital"] = data_fetcher.fetch_total_deposits()
except Exception as e:
logger.warning("Failed to fetch deposit history: %s", e)
# 2b. Sync stop-loss orders with exchange (real-time, not local memory) # 2b. Sync stop-loss orders with exchange (real-time, not local memory)
try: try:
active_orders = data_fetcher.fetch_active_orders() active_orders = data_fetcher.fetch_active_orders()
@ -503,13 +509,17 @@ def _build_portfolio_one_liner(port: dict, current_prices: dict) -> str:
total_cost += amount * entry total_cost += amount * entry
if current > 0: if current > 0:
total_market_value += amount * current total_market_value += amount * current
unrealized = total_market_value - total_cost
total_value = available + total_market_value total_value = available + total_market_value
# 總收益率(基於入金)
initial_capital = port.get("initial_capital", 0)
total_return_pct = ((total_value / initial_capital - 1) * 100) if initial_capital > 0 else 0.0
# 變動收益率(基於持倉成本)
unrealized = total_market_value - total_cost
total_capital = available + total_cost total_capital = available + total_cost
total_return_pct = ((total_value / total_capital - 1) * 100) if total_capital > 0 else 0.0 change_pct = ((total_value / total_capital - 1) * 100) if total_capital > 0 else 0.0
return ( return (
f"總值 {total_value:.2f} USDT | 總收益 {total_return_pct:+.2f}% | " f"總值 {total_value:.2f} USDT | 總收益 {total_return_pct:+.2f}% | "
f"{available:.2f} 可用 | 持倉 {pos_count}" f"變動 {change_pct:+.2f}% | {available:.2f} 可用 | 持倉 {pos_count}"
) )

View File

@ -135,7 +135,14 @@ def get_portfolio_summary(portfolio: dict, current_prices: dict) -> str:
lines.append("") lines.append("")
lines.append(f"*Total Portfolio Value:* {total_value:.2f} USDT") lines.append(f"*Total Portfolio Value:* {total_value:.2f} USDT")
lines.append(f"*Total Unrealized P&L:* {total_unrealized:+.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")
initial_capital = portfolio.get("initial_capital", 0)
if initial_capital > 0:
total_return = total_value - initial_capital
total_return_pct = (total_value / initial_capital - 1) * 100
sign = "+" if total_return >= 0 else ""
lines.append(f"*Total Return:* {sign}{total_return:.2f} USDT ({sign}{total_return_pct:.2f}%)")
history = portfolio.get("trade_history", []) history = portfolio.get("trade_history", [])
if history: if history:

View File

@ -1,26 +1,38 @@
{ {
"tDOGE:UST": { "tDOTUST": {
"order_id": 233282701621, "order_id": 233309387815,
"stop_price": 0.093946, "stop_price": 1.3849,
"entry_price": 0.09688198941787385, "entry_price": 1.4287,
"amount": 144.10790954 "amount": 10.03723093
}, },
"tSUIUST": { "tXRPUST": {
"order_id": 233265250698, "order_id": 233329683966,
"stop_price": 0.97101, "stop_price": 1.372,
"entry_price": 1.0009320066822542, "entry_price": 1.4153,
"amount": 54.57098037 "amount": 12.66451728
}, },
"tAVAX:UST": { "tSOLUST": {
"order_id": 233268826282, "order_id": 233329069994,
"stop_price": 9.4454, "stop_price": 85.644,
"entry_price": 9.734478498389963, "entry_price": 88.367,
"amount": 4.08390185 "amount": 0.16229378
}, },
"tUNIUST": { "tETHUST": {
"order_id": 233279675016, "order_id": 233338658543,
"stop_price": 3.8778, "stop_price": 2048,
"entry_price": 3.9954999999999994, "entry_price": 2110.9826633288,
"amount": 4.55798027 "amount": 0.01357354
},
"tLINK:UST": {
"order_id": 233331464285,
"stop_price": 8.9428,
"entry_price": 9.2243,
"amount": 1.16577353
},
"tBTCUST": {
"order_id": 233335923158,
"stop_price": 69413,
"entry_price": 71556.91752086183,
"amount": 0.00065071
} }
} }