drl-trading-bot-dev2 / src /ui /api_server.py
DRL Trading Bot
Feature: HTF Agent integration — live trading, API endpoints, UI tab
fc115d5
Raw
History Blame Contribute Delete
38.6 kB
"""
API endpoints for live streaming data to the dashboard.
This provides JSON endpoints that JavaScript can poll for updates.
"""
from flask import Flask, jsonify, request
from flask_cors import CORS
import json
from pathlib import Path
from datetime import datetime
import time
# Load .env for local development (no-op if vars already set)
try:
from dotenv import load_dotenv
load_dotenv(Path(__file__).parent.parent.parent / '.env', override=False)
except ImportError:
pass
app = Flask(__name__)
CORS(app, origins="*")
@app.route('/api/ping')
def ping():
"""Lightweight connectivity check for remote clients."""
return jsonify({"ok": True, "timestamp": datetime.now().isoformat()})
@app.route('/health')
def health_check():
"""Health check endpoint for system status."""
return jsonify({"status": "healthy", "timestamp": datetime.now().isoformat()})
import logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
import sys
from pathlib import Path
# Add project root to path for imports
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from src.data.storage import get_storage
# Initialize storage
storage = get_storage()
WHALE_TRACKERS = {}
# Cache for /api/market
_MARKET_CACHE: dict = {}
MARKET_CACHE_TTL = 30 # 30 seconds cache (restored from 1 hour since we removed proxy)
@app.route('/api/state')
def get_state():
"""Get current trading state."""
try:
state = storage.load_state()
# MATHEMATICAL OVERRIDE: Calculate true global equity from trade history
# bypassing the historically corrupted bot balance tracker.
try:
all_trades = storage.get_trades(limit=1000)
realized_pnl = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper())
raw_assets = state.get('raw_state', {}).get('assets', {})
open_pnl = sum(a.get('pnl', 0) for a in raw_assets.values() if a.get('position', 0) != 0)
if all_trades or raw_assets:
state['total_pnl'] = realized_pnl + open_pnl
state['realized_pnl'] = realized_pnl + open_pnl
# Do not override balance — use real value from stored state
except Exception as math_err:
logger.error(f"Failed mathematical override: {math_err}")
# Normalize keys for frontend compatibility
if 'total_balance' in state and 'balance' not in state:
state['balance'] = state['total_balance']
if 'total_pnl' in state and 'realized_pnl' not in state:
state['realized_pnl'] = state['total_pnl']
# Expose available assets for the frontend selector
# Always include all configured trading assets so user can switch
configured_assets = ['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT']
raw_assets_keys = list(state.get('raw_state', {}).get('assets', {}).keys())
if not raw_assets_keys:
raw_assets_keys = list(state.get('assets', {}).keys())
# Merge: configured assets + any additional ones from state
all_assets = list(dict.fromkeys(configured_assets + raw_assets_keys))
state['available_assets'] = all_assets
# Inject Recent On-Chain Whale Alerts from tracked wallets
try:
whale_alerts = []
whale_dir = PROJECT_ROOT / "data" / "whale_wallets"
if whale_dir.exists():
from src.features.whale_wallet_registry import get_wallets_by_chain
for chain_dir in whale_dir.iterdir():
if chain_dir.is_dir():
chain = chain_dir.name.upper()
for wallet_file in chain_dir.glob("*.json"):
try:
with open(wallet_file, "r") as f:
w_data = json.load(f)
addr = w_data.get("address", "")
# Cross-reference live address with registry identities
chain_wallets = get_wallets_by_chain(chain)
wallet = next((w for w in chain_wallets if w.address.lower() == addr.lower()), None)
w_label = wallet.label if wallet else f"Unknown {chain} Whale"
w_type = wallet.wallet_type if wallet else "unknown"
for tx in w_data.get("transactions", [])[-10:]:
val = float(tx.get('value', 0))
# Strict $50k USD minimum threshold
price_map = {'BTC': 70000, 'ETH': 3500, 'SOL': 150, 'XRP': 0.6}
usd_val = val * price_map.get(chain, 1)
if usd_val > 50000:
alert = {
'chain': chain,
'value': val,
'currency': tx.get('asset', chain),
'timestamp': tx.get('timestamp', int(time.time())),
'link': tx.get('link', '#'),
'wallet_label': w_label,
'wallet_type': w_type,
'wallet_address': addr
}
whale_alerts.append(alert)
except:
pass
# Sort globally by timestamp descending and take top 50 alerts
if whale_alerts:
whale_alerts = sorted(whale_alerts, key=lambda x: x.get('timestamp', 0), reverse=True)[:50]
state['whale_alerts'] = whale_alerts
except Exception as e:
logger.error(f"Failed to load whale alerts: {e}")
return jsonify(state)
except Exception as e:
logger.error(f"Failed to load state: {e}")
return jsonify({})
@app.route('/api/trades')
def get_trades():
"""Get recent trades."""
try:
trades = storage.get_trades(limit=1000)
return jsonify(trades)
except Exception as e:
logger.error(f"Failed to load trades: {e}")
return jsonify([])
@app.route('/api/trades/count')
def get_trade_count():
"""Get trade count for change detection."""
try:
# Simple count estimation via list length
trades = storage.get_trades(limit=1000)
count = len(trades)
return jsonify({'count': count, 'timestamp': datetime.now().isoformat()})
except Exception as e:
logger.error(f"Failed to count trades: {e}")
return jsonify({'count': 0, 'timestamp': datetime.now().isoformat()})
@app.route('/api/model')
def get_model_info():
"""Get active model information."""
import os
model_path = PROJECT_ROOT / 'data' / 'models' / 'ultimate_agent.zip'
# Model info
model_exists = model_path.exists()
if model_exists:
model_mtime = datetime.fromtimestamp(os.path.getmtime(model_path))
model_date = model_mtime.strftime("%Y-%m-%d")
else:
model_date = "Not found"
# State info via storage
state = {}
try:
state = storage.load_state()
except Exception as e:
logger.error(f"Failed to load state for model info: {e}")
# Handle multi-asset state structure
balance = state.get('total_balance', state.get('balance'))
total_return = None # Cannot compute without knowing real initial capital
# Trade stats via storage
trades = []
try:
trades = storage.get_trades(limit=1000)
except Exception as e:
logger.error(f"Failed to load trades for model info: {e}")
winning = sum(1 for t in trades if t.get('pnl', 0) > 0)
total = len(trades)
win_rate = (winning / total * 100) if total > 0 else 0
return jsonify({
'model_name': 'Ultimate Agent (PPO)',
'model_exists': model_exists,
'model_date': model_date,
'total_return': None,
'win_rate': round(win_rate, 1),
'total_trades': total,
'winning_trades': winning,
'balance': balance,
'timestamp': datetime.now().isoformat()
})
@app.route('/api/market')
def get_market_analysis():
"""Get comprehensive market analysis from all analyzers."""
import sys
import os
import time
from flask import request
sys.path.insert(0, str(PROJECT_ROOT))
# Get symbol from query params, default to BTC/USDT
symbol = request.args.get('symbol', 'BTCUSDT')
clean_symbol = symbol.replace('/', '').upper()
# ── 60-second cache to limit proxy bandwidth ──────────────────────────
global _MARKET_CACHE
cached = _MARKET_CACHE.get(clean_symbol)
if cached and (time.time() - cached['_fetched_at']) < MARKET_CACHE_TTL:
return jsonify(cached)
# ─────────────────────────────────────────────────────────────────────
result = {
'timestamp': datetime.now().isoformat(),
'symbol': symbol,
'whale': None,
'regime': None,
'mtf': None,
'funding': None,
'order_flow': None,
'forecast': None,
'news': None
}
# Try to load from state first (Consistency with Bot)
state_file = PROJECT_ROOT / 'logs' / 'trading_state.json'
state_analysis = None
if state_file.exists():
try:
with open(state_file, 'r') as f:
state = json.load(f)
assets = state.get('assets', {})
# Try explicit symbol or clean symbol
asset_data = assets.get(symbol, assets.get(clean_symbol))
if asset_data:
state_analysis = asset_data.get('analysis')
except Exception as e:
logger.error(f"Failed to load state for market analysis: {e}")
# Use state analysis if available
if state_analysis:
# Whale
whale_data = state_analysis.get('whale', {})
if whale_data:
result['whale'] = {
'score': round(whale_data.get('score', 0), 2),
'direction': whale_data.get('direction', 'NEUTRAL'),
'confidence': int(whale_data.get('confidence', 0) * 100),
'bullish': whale_data.get('bullish_signals', 0),
'bearish': whale_data.get('bearish_signals', 0),
'neutral': whale_data.get('neutral_signals', 0),
'flow_metrics': whale_data.get('flow_metrics', {})
}
# Funding
funding_data = state_analysis.get('funding', {})
if funding_data:
result['funding'] = {
'rate': round(funding_data.get('rate', 0) * 100, 4), # rate is usually float
'bias': funding_data.get('signal', 'neutral'),
'annualized': round(funding_data.get('rate', 0) * 3 * 365 * 100, 1)
}
# Order Flow
of_data = state_analysis.get('order_flow', {})
if of_data:
result['order_flow'] = {
'large_buys': of_data.get('large_buys', 0),
'large_sells': of_data.get('large_sells', 0),
'bias': of_data.get('bias', 'neutral'),
'net_flow': of_data.get('large_buy_volume', 0) - of_data.get('large_sell_volume', 0),
'score': of_data.get('score', 0),
# Layer details (enhanced signal)
'cvd': of_data.get('cvd', {}),
'taker': of_data.get('taker', {}),
'notable': of_data.get('notable', {})
}
# Forecast
forecast_data = state_analysis.get('forecast')
if forecast_data:
result['forecast'] = {
'return_1h': round(forecast_data.get('return_1h', 0) * 100, 3),
'return_4h': round(forecast_data.get('return_4h', 0) * 100, 3),
'return_12h': round(forecast_data.get('return_12h', 0) * 100, 3),
'return_24h': round(forecast_data.get('return_24h', 0) * 100, 3),
'confidence': round(forecast_data.get('confidence_4h', 0), 2),
'consensus': round(forecast_data.get('direction_consensus', 0), 2),
}
# Confidence Engine
confidence = state_analysis.get('confidence')
if confidence is not None:
# Adding at root since it applies to the whole ensemble
result['ensemble_confidence'] = round(confidence, 2)
# Regime (New Phase 11.3 addition, HMM model)
regime_data = state_analysis.get('regime')
if regime_data:
result['regime'] = regime_data
# News Sentiment - DISABLED (not reliable, removed per user request)
# Commented out - news sentiment disabled in trading logic
# news_data = state_analysis.get('news', {})
# if news_data:
# result['news'] = {
# 'sentiment': round(news_data.get('sentiment', 0), 3),
# 'confidence': round(news_data.get('confidence', 0), 3),
# 'trend': news_data.get('trend', 'unknown'),
# 'sources': news_data.get('sources', 0)
# }
# Fallbacks and Regime (Regime is not in state yet, calculate it)
# ... (Keep existing regime calculation as it's fast) ...
if not result['whale']:
try:
# Whale Tracker - Use persistent instance for WebSocket
from src.features.whale_tracker import WhaleTracker
# Global store for active trackers (defined at module level)
global WHALE_TRACKERS
if clean_symbol not in WHALE_TRACKERS:
logger.info(f"Initializing new WhaleTracker for {clean_symbol}")
tracker = WhaleTracker(symbol=clean_symbol, enable_ml=False)
# DO NOT start live websocket stream inside the API server memory space
# as it leaks sockets and hangs threads. We only use it for REST fetch here.
# tracker.start_stream()
WHALE_TRACKERS[clean_symbol] = tracker
whale = WHALE_TRACKERS[clean_symbol]
signals = whale.get_whale_signals()
result['whale'] = {
'score': round(signals.get('score', 0), 2),
'direction': signals.get('direction', 'NEUTRAL'),
'confidence': int(signals.get('confidence', 0) * 100),
'bullish': signals.get('bullish_signals', 0),
'bearish': signals.get('bearish_signals', 0),
'neutral': signals.get('neutral_signals', 0),
'flow_metrics': signals.get('flow_metrics', {})
}
except Exception as e:
result['whale'] = {'error': str(e)}
try:
# Fetch current market data for regime detection (fast REST call)
import requests as req
import os as _os
url = _os.environ.get("BINANCE_FUTURES_URL", "https://data-api.binance.vision") + "/api/v3/klines"
params = {"symbol": clean_symbol, "interval": "1h", "limit": 100}
resp = req.get(url, params=params, timeout=10)
data = resp.json()
import pandas as pd
df = pd.DataFrame(data, columns=[
'timestamp', 'open', 'high', 'low', 'close', 'volume',
'close_time', 'quote_volume', 'trades', 'taker_buy_base',
'taker_buy_quote', 'ignore'
])
for col in ['open', 'high', 'low', 'close', 'volume']:
df[col] = df[col].astype(float)
# Add current price to result
if not df.empty:
result['price'] = float(df.iloc[-1]['close'])
# Regime Detector
from src.features.regime_detector import MarketRegimeDetector
regime = MarketRegimeDetector()
regime_result = regime.detect_regime(df)
regime_type = getattr(regime_result, 'regime', 'UNKNOWN')
result['regime'] = {
'type': str(regime_type.name) if hasattr(regime_type, 'name') else str(regime_type),
'adx': round(getattr(regime_result, 'trend_strength', 0), 1),
'direction': str(getattr(regime_result, 'trend_direction', 'NEUTRAL')),
'volatility': round(getattr(regime_result, 'volatility_ratio', 1), 2)
}
except Exception as e:
logger.error(f"Market data fetch error: {e}")
result['regime'] = {'error': str(e)}
if not result['mtf'] and state_analysis:
mtf_data = state_analysis.get('mtf', {})
if mtf_data:
result['mtf'] = {
'bias': mtf_data.get('bias', 'NEUTRAL'),
'aligned': mtf_data.get('aligned', False),
'reason': mtf_data.get('reason', 'Syncing...'),
'4h': mtf_data.get('4h', 'neutral'),
'1h': mtf_data.get('1h', 'neutral'),
'15m': mtf_data.get('15m', 'neutral')
}
# API Server should NOT perform heavy analysis on the fly.
# It must rely on the live_trading_multi.py to populate the state.
# If the state is missing data, we return empty/null to indicate "System Syncing".
if not result['mtf']:
result['mtf'] = {'reason': 'Syncing...', 'aligned': False, 'bias': 'NEUTRAL'}
if not result['funding']:
try:
from src.features.order_flow import FundingRateAnalyzer
fa = FundingRateAnalyzer(symbol=clean_symbol)
sig = fa.get_signal()
result['funding'] = {
'rate': round(sig.rate * 100, 4),
'bias': sig.signal,
'annualized': round(sig.rate * 3 * 365 * 100, 1)
}
except Exception as e:
logger.error(f"Funding fallback error: {e}")
result['funding'] = {'rate': 0, 'bias': 'neutral', 'annualized': 0}
if not result['order_flow']:
try:
from src.features.order_flow import OrderFlowAnalyzer
oa = OrderFlowAnalyzer(symbol=clean_symbol)
# Use enhanced signal instead of just large orders
enhanced = oa.get_enhanced_signal(df)
result['order_flow'] = {
'large_buys': enhanced.get('large_buys', 0),
'large_sells': enhanced.get('large_sells', 0),
'bias': enhanced.get('bias', 'neutral'),
'net_flow': enhanced.get('large_buy_volume', 0) - enhanced.get('large_sell_volume', 0),
'score': enhanced.get('score', 0),
# Layer details
'cvd': enhanced.get('cvd', {}),
'taker': enhanced.get('taker', {}),
'notable': enhanced.get('notable', {})
}
except Exception as e:
logger.error(f"OrderFlow fallback error: {e}")
result['order_flow'] = {
'bias': 'neutral', 'net_flow': 0, 'large_buys': 0, 'large_sells': 0,
'score': 0, 'cvd': {}, 'taker': {'ratio': 0.5}, 'notable': {}
}
# API Server should NOT perform heavy ML analysis on the fly.
# TFT Forecast (Phase 11.1) requires massive PyTorch model loading that hangs the main thread.
if not result['forecast']:
# Return empty forecast instead of loading PyTorch on CPU to prevent API deadlock
pass
if not result['mtf']:
result['mtf'] = {'reason': 'Syncing...', 'aligned': False, 'bias': 'NEUTRAL'}
# Store result in 60s cache before returning
result['_fetched_at'] = time.time()
_MARKET_CACHE[clean_symbol] = result
response = {k: v for k, v in result.items() if k != '_fetched_at'}
return jsonify(response)
@app.route('/api/ohlcv')
def get_ohlcv():
"""Get OHLCV candlestick data from Binance for the dashboard chart."""
import requests as _req
import os as _os
from flask import request as flask_req
symbol = flask_req.args.get('symbol', 'BTCUSDT').upper().replace('/', '')
interval = flask_req.args.get('interval', '1h')
limit = min(int(flask_req.args.get('limit', 500)), 1000)
try:
base_url = _os.environ.get("BINANCE_FUTURES_URL", "https://data-api.binance.vision")
url = base_url + "/api/v3/klines"
resp = _req.get(url, params={"symbol": symbol, "interval": interval, "limit": limit}, timeout=15)
data = resp.json()
if isinstance(data, dict) and data.get('code'):
logger.error(f"Binance klines error for {symbol}: {data}")
return jsonify([])
candles = []
for row in data:
candles.append({
'time': int(row[0]) // 1000, # ms → seconds
'open': float(row[1]),
'high': float(row[2]),
'low': float(row[3]),
'close': float(row[4]),
'volume': float(row[5]),
})
return jsonify(candles)
except Exception as e:
logger.error(f"OHLCV fetch error for {symbol}: {e}")
return jsonify([])
@app.route('/api/debug/log')
def get_crash_log():
"""Get crash log if exists."""
log_file = PROJECT_ROOT / 'crash.log'
if log_file.exists():
with open(log_file, 'r') as f:
return f.read(), 200, {'Content-Type': 'text/plain'}
return "No crash log found. Bot might be running or log not written.", 404
@app.route('/api/testnet/status')
def get_testnet_status():
"""Get Binance testnet account status, balances and positions."""
import os
try:
api_key = os.getenv('BINANCE_TESTNET_API_KEY', '').strip()
api_secret = os.getenv('BINANCE_TESTNET_API_SECRET', '').strip()
if not api_key or not api_secret:
return jsonify({'error': 'Testnet API keys not configured on server', 'configured': False})
from src.api.binance import BinanceConnector
testnet = BinanceConnector(api_key=api_key, api_secret=api_secret, testnet=True)
connectivity = testnet.test_connectivity()
balances = testnet.get_all_balances() or {}
portfolio_value = 0.0
positions_data = []
usdt_balance = 0.0
# Binance spot testnet only supports a limited set of trading pairs.
# Attempting to fetch tickers for unsupported symbols returns "Invalid symbol"
# and can cause slow timeouts if repeated. Limit to known testnet pairs.
TESTNET_QUOTE_USDT = {'BTC', 'ETH', 'BNB', 'LTC', 'TRX', 'XRP', 'SOL', 'ADA', 'DOGE'}
for currency, amounts in balances.items():
total = float(amounts.get('total', 0))
if total > 0:
if currency == 'USDT':
portfolio_value += total
usdt_balance = float(amounts.get('free', 0))
elif currency in TESTNET_QUOTE_USDT:
try:
# Use slash format — BinanceConnector.get_ticker strips the slash internally
ticker = testnet.get_ticker(f"{currency}/USDT")
price = float(ticker.get('last', 0))
if price > 0:
value_usdt = total * price
portfolio_value += value_usdt
positions_data.append({
'asset': currency,
'amount': total,
'price': price,
'value_usdt': value_usdt
})
except Exception:
pass
# Skip currencies not in the testnet whitelist to avoid Invalid symbol hangs
return jsonify({
'configured': True,
'connected': bool(connectivity),
'api_key_prefix': f"{api_key[:8]}...{api_key[-4:]}",
'portfolio_value': portfolio_value,
'usdt_balance': usdt_balance,
'pnl_pct': None,
'pnl_usdt': None,
'positions': positions_data,
'balance_count': len(balances)
})
except Exception as e:
logger.error(f"Testnet status error: {e}")
return jsonify({'error': str(e), 'configured': True, 'connected': False})
@app.route('/api/testnet/order', methods=['POST'])
def place_testnet_order():
"""Place a market order on Binance testnet."""
import os
from flask import request as flask_request
try:
api_key = os.getenv('BINANCE_TESTNET_API_KEY', '').strip()
api_secret = os.getenv('BINANCE_TESTNET_API_SECRET', '').strip()
if not api_key or not api_secret:
return jsonify({'success': False, 'error': 'Testnet API keys not configured on server'}), 400
data = flask_request.get_json() or {}
symbol = data.get('symbol', 'BTC/USDT')
side = data.get('side', 'buy')
amount_usdt = float(data.get('amount_usdt', 100))
from src.api.binance import BinanceConnector
testnet = BinanceConnector(api_key=api_key, api_secret=api_secret, testnet=True)
ticker = testnet.get_ticker(symbol)
current_price = float(ticker['last'])
if side == 'buy':
amount_base = amount_usdt / current_price
else:
base_currency = symbol.split('/')[0]
balances = testnet.get_all_balances() or {}
amount_base = float(balances.get(base_currency, {}).get('free', 0))
if amount_base == 0:
return jsonify({'success': False, 'error': f'No {base_currency} to sell'}), 400
order = testnet.place_market_order(symbol=symbol, side=side, amount=amount_base)
return jsonify({
'success': bool(order),
'order': order,
'price': current_price,
'amount': amount_base,
'symbol': symbol,
'side': side
})
except Exception as e:
logger.error(f"Testnet order error: {e}")
return jsonify({'success': False, 'error': str(e)}), 400
@app.route('/api/testnet/orders')
def get_testnet_orders():
"""Get open orders on Binance testnet."""
import os
try:
api_key = os.getenv('BINANCE_TESTNET_API_KEY', '').strip()
api_secret = os.getenv('BINANCE_TESTNET_API_SECRET', '').strip()
if not api_key or not api_secret:
return jsonify({'error': 'Testnet API keys not configured on server', 'orders': []})
from src.api.binance import BinanceConnector
testnet = BinanceConnector(api_key=api_key, api_secret=api_secret, testnet=True)
open_orders = testnet.get_open_orders()
return jsonify({'orders': open_orders or []})
except Exception as e:
logger.error(f"Testnet orders error: {e}")
return jsonify({'error': str(e), 'orders': []})
@app.route('/api/testnet/trades')
def get_testnet_trades():
"""Get testnet trade history (bot-mirrored real orders)."""
try:
from src.api.testnet_executor import get_testnet_executor
executor = get_testnet_executor()
if not executor:
return jsonify({'trades': [], 'error': 'Testnet not configured'})
limit = int(request.args.get('limit', 200))
trades = executor.get_trades(limit=limit)
return jsonify({'trades': trades, 'total': len(trades)})
except Exception as e:
logger.error(f"GET /api/testnet/trades error: {e}")
return jsonify({'trades': [], 'error': str(e)})
@app.route('/api/testnet/positions')
def get_testnet_positions():
"""Get current open positions on testnet with live prices and unrealized PNL."""
try:
from src.api.testnet_executor import get_testnet_executor
executor = get_testnet_executor()
if not executor:
return jsonify({'positions': [], 'error': 'Testnet not configured'})
positions = executor.get_current_positions()
return jsonify({'positions': positions})
except Exception as e:
logger.error(f"GET /api/testnet/positions error: {e}")
return jsonify({'positions': [], 'error': str(e)})
@app.route('/api/testnet/pnl')
def get_testnet_pnl():
"""Get testnet PNL summary: realized, unrealized, win rate, equity curve."""
try:
from src.api.testnet_executor import get_testnet_executor
executor = get_testnet_executor()
if not executor:
return jsonify({'error': 'Testnet not configured', 'realized_pnl': 0, 'unrealized_pnl': 0})
summary = executor.get_pnl_summary()
return jsonify(summary)
except Exception as e:
logger.error(f"GET /api/testnet/pnl error: {e}")
return jsonify({'error': str(e), 'realized_pnl': 0, 'unrealized_pnl': 0})
@app.route('/api/testnet/execute', methods=['POST'])
def execute_testnet_trade():
"""Manually trigger a testnet trade (for testing). Body: {action, symbol, price, confidence, sl, tp}."""
import os
from flask import request as flask_request
try:
data = flask_request.get_json() or {}
action = data.get('action', 'OPEN_LONG_SPLIT')
symbol = data.get('symbol', 'BTCUSDT')
confidence = float(data.get('confidence', 0.6))
sl = float(data.get('sl', 0))
tp = float(data.get('tp', 0))
from src.api.testnet_executor import get_testnet_executor
executor = get_testnet_executor()
if not executor:
return jsonify({'success': False, 'error': 'Testnet not configured'}), 400
# Get current price from connector
from src.api.binance import BinanceConnector
api_key = os.getenv('BINANCE_TESTNET_API_KEY', '').strip()
api_secret = os.getenv('BINANCE_TESTNET_API_SECRET', '').strip()
connector = BinanceConnector(api_key=api_key, api_secret=api_secret, testnet=True)
ccxt_symbol = symbol if '/' in symbol else symbol[:-4] + '/USDT'
ticker = connector.get_ticker(ccxt_symbol)
price = float(ticker.get('last', data.get('price', 0)))
# Build a synthetic bot_trade dict
bot_trade = {
'action': action,
'symbol': symbol,
'price': price,
'confidence': confidence,
'sl': sl if sl > 0 else price * 0.95,
'tp': tp if tp > 0 else price * 1.05,
'units': 0,
'pnl': 0,
}
record = executor.mirror_trade(bot_trade, {})
return jsonify({'success': bool(record and record.get('executed')), 'trade': record})
except Exception as e:
logger.error(f"POST /api/testnet/execute error: {e}")
return jsonify({'success': False, 'error': str(e)}), 400
# ---------------------------------------------------------------------------
# HTF Agent endpoints
# ---------------------------------------------------------------------------
HTF_STATE_FILE = PROJECT_ROOT / "logs" / "htf_trading_state.json"
HTF_TRADES_FILE = PROJECT_ROOT / "logs" / "htf_trades.json"
def _load_htf_state() -> dict:
"""Load HTF bot state from disk."""
if HTF_STATE_FILE.exists():
try:
return json.loads(HTF_STATE_FILE.read_text())
except Exception:
pass
return {}
def _load_htf_trades(limit: int = 200) -> list:
"""Load HTF trade history from line-delimited JSON."""
if not HTF_TRADES_FILE.exists():
return []
trades = []
try:
with open(HTF_TRADES_FILE) as f:
for line in f:
line = line.strip()
if line:
trades.append(json.loads(line))
except Exception as e:
logger.error(f"HTF trades read error: {e}")
return trades[-limit:]
@app.route('/api/htf/status')
def get_htf_status():
"""Return HTF agent status: position, last signal, model info, feature summary."""
try:
state = _load_htf_state()
if not state:
return jsonify({
'running': False,
'symbol': 'BTCUSDT',
'position': 0,
'position_label': 'FLAT',
'balance': None,
'realized_pnl': 0.0,
'unrealized_pnl': 0.0,
'message': 'HTF bot not running or no state file found',
})
# Compute unrealized PnL from current price if position is open
position = int(state.get('position', 0))
position_price = float(state.get('position_price', 0.0))
position_units = float(state.get('position_units', 0.0))
sl_price = float(state.get('sl_price', 0.0))
tp_price = float(state.get('tp_price', 0.0))
realized_pnl = float(state.get('realized_pnl', 0.0))
# Try to get current price for unrealized PnL
unrealized_pnl = 0.0
current_price = 0.0
try:
from src.api.binance import BinanceConnector
connector = BinanceConnector()
ticker = connector.get_ticker('BTC/USDT')
current_price = float(ticker.get('last', 0))
if position != 0 and position_price > 0 and position_units > 0:
if position == 1:
unrealized_pnl = (current_price - position_price) * position_units
else:
unrealized_pnl = (position_price - current_price) * position_units
except Exception:
pass
trades = _load_htf_trades(limit=500)
close_trades = [t for t in trades if 'CLOSE' in t.get('action', '').upper()]
wins = [t for t in close_trades if t.get('pnl', 0) > 0]
win_rate = len(wins) / len(close_trades) if close_trades else 0.0
return jsonify({
'running': True,
'symbol': state.get('symbol', 'BTCUSDT'),
'position': position,
'position_label': {1: 'LONG', -1: 'SHORT', 0: 'FLAT'}.get(position, 'FLAT'),
'position_price': position_price,
'position_units': position_units,
'sl_price': sl_price,
'tp_price': tp_price,
'current_price': current_price,
'balance': float(state.get('balance', 0)),
'realized_pnl': realized_pnl,
'unrealized_pnl': unrealized_pnl,
'total_pnl': realized_pnl + unrealized_pnl,
'win_rate': win_rate,
'trade_count': len(close_trades),
'model_path': state.get('model_path'),
'start_time': state.get('start_time'),
'updated_at': state.get('updated_at'),
'dry_run': state.get('dry_run', True),
})
except Exception as e:
logger.error(f"GET /api/htf/status error: {e}")
return jsonify({'error': str(e), 'running': False}), 500
@app.route('/api/htf/trades')
def get_htf_trades():
"""Return HTF agent trade history."""
try:
limit = int(request.args.get('limit', 200))
trades = _load_htf_trades(limit=limit)
return jsonify({'trades': trades, 'total': len(trades)})
except Exception as e:
logger.error(f"GET /api/htf/trades error: {e}")
return jsonify({'trades': [], 'error': str(e)})
@app.route('/api/htf/performance')
def get_htf_performance():
"""Return HTF performance metrics: Sharpe ratio, return, drawdown, win rate."""
try:
trades = _load_htf_trades(limit=1000)
state = _load_htf_state()
close_trades = [t for t in trades if 'CLOSE' in t.get('action', '').upper()]
pnls = [float(t.get('pnl', 0)) for t in close_trades]
if not pnls:
return jsonify({
'total_trades': 0,
'win_rate': 0.0,
'total_pnl': 0.0,
'avg_pnl': 0.0,
'sharpe': 0.0,
'max_drawdown': 0.0,
'return_pct': 0.0,
'message': 'No closed trades yet',
})
wins = [p for p in pnls if p > 0]
win_rate = len(wins) / len(pnls)
total_pnl = sum(pnls)
avg_pnl = total_pnl / len(pnls)
# Sharpe ratio (daily, assuming each trade ~4h average)
if len(pnls) > 1:
mean_r = float(np.mean(pnls))
std_r = float(np.std(pnls))
sharpe = (mean_r / (std_r + 1e-10)) * (6 ** 0.5) # annualise ~6 trades/day
else:
sharpe = 0.0
# Max drawdown via equity curve
initial_balance = float(state.get('balance', 10000)) - total_pnl
equity = initial_balance
peak = equity
max_dd = 0.0
for p in pnls:
equity += p
peak = max(peak, equity)
dd = (peak - equity) / (peak + 1e-10)
max_dd = max(max_dd, dd)
return_pct = (total_pnl / (initial_balance + 1e-10)) * 100
return jsonify({
'total_trades': len(close_trades),
'wins': len(wins),
'losses': len(pnls) - len(wins),
'win_rate': round(win_rate, 4),
'total_pnl': round(total_pnl, 2),
'avg_pnl': round(avg_pnl, 2),
'best_trade': round(max(pnls), 2),
'worst_trade': round(min(pnls), 2),
'sharpe': round(sharpe, 3),
'max_drawdown': round(max_dd * 100, 2),
'return_pct': round(return_pct, 2),
'start_time': state.get('start_time'),
})
except Exception as e:
logger.error(f"GET /api/htf/performance error: {e}")
return jsonify({'error': str(e)}), 500
if __name__ == '__main__':
# threaded=True is CRITICAL to prevent single requests (like Market Analysis fallback)
# from locking up the entire dashboard and causing 'Trades: 0' sidebars
app.run(port=5001, debug=False, threaded=True)