@@ -273,6 +273,13 @@ def place_order():
|
||||
tp_price = float(body.get("tp_price") or 0)
|
||||
sl_price = float(body.get("sl_price") or 0)
|
||||
source = str(body.get("source") or "manual").strip()
|
||||
margin_mode = str(body.get("margin_mode") or body.get("marginMode") or "").strip().lower()
|
||||
if margin_mode in ("cross", "crossed"):
|
||||
margin_mode = "cross"
|
||||
elif margin_mode in ("iso", "isolated"):
|
||||
margin_mode = "isolated"
|
||||
else:
|
||||
margin_mode = ""
|
||||
|
||||
# ---- validation ----
|
||||
if not credential_id:
|
||||
@@ -286,28 +293,35 @@ def place_order():
|
||||
if order_type == "limit" and price <= 0:
|
||||
return jsonify({"code": 0, "msg": "price required for limit orders"}), 400
|
||||
|
||||
# ---- Auto-determine market_type from leverage ----
|
||||
# leverage = 1 -> spot, leverage > 1 -> swap
|
||||
if not market_type:
|
||||
market_type = "spot" if leverage == 1 else "swap"
|
||||
# ---- market_type: leverage 1 => spot API, else perpetual (swap) ----
|
||||
if market_type in ("futures", "future", "perp", "perpetual"):
|
||||
market_type = "swap"
|
||||
# Override only when user did not explicitly choose market_type.
|
||||
if leverage > 1:
|
||||
market_type = "swap"
|
||||
elif leverage == 1 and not str(body.get("market_type") or "").strip():
|
||||
else:
|
||||
market_type = "spot"
|
||||
|
||||
# ---- build exchange client ----
|
||||
exchange_config = _build_exchange_config(credential_id, user_id, {
|
||||
"market_type": market_type,
|
||||
})
|
||||
cfg_overrides: Dict[str, Any] = {"market_type": market_type}
|
||||
if margin_mode in ("cross", "isolated"):
|
||||
cfg_overrides["margin_mode"] = margin_mode
|
||||
cfg_overrides["td_mode"] = margin_mode
|
||||
exchange_config = _build_exchange_config(credential_id, user_id, cfg_overrides)
|
||||
exchange_id = (exchange_config.get("exchange_id") or "").strip().lower()
|
||||
if not exchange_id:
|
||||
return jsonify({"code": 0, "msg": "Invalid credential: missing exchange_id"}), 400
|
||||
|
||||
client = _create_client(exchange_config, market_type=market_type)
|
||||
|
||||
# Binance USDT-M: sync isolated/cross margin mode (best-effort; may fail if open orders exist)
|
||||
if market_type != "spot" and margin_mode in ("cross", "isolated"):
|
||||
try:
|
||||
from app.services.live_trading.binance import BinanceFuturesClient
|
||||
if isinstance(client, BinanceFuturesClient):
|
||||
client.set_margin_type(symbol=symbol, margin_mode=margin_mode)
|
||||
except Exception as me:
|
||||
logger.warning(f"Binance set_margin_type failed (non-fatal): {me}")
|
||||
|
||||
# ---- Convert USDT amount to base asset quantity ----
|
||||
# Quick trade always accepts USDT amount, convert to base qty for all exchanges
|
||||
# For limit orders, use the provided price; for market orders, fetch current price
|
||||
@@ -543,7 +557,16 @@ def get_balance():
|
||||
raw = client.get_account()
|
||||
balance_data = _parse_balance(raw, exchange_id, market_type)
|
||||
elif hasattr(client, "get_accounts"):
|
||||
raw = client.get_accounts()
|
||||
from app.services.live_trading.bitget import BitgetMixClient
|
||||
|
||||
if isinstance(client, BitgetMixClient):
|
||||
pt = str(exchange_config.get("product_type") or exchange_config.get("productType") or "USDT-FUTURES")
|
||||
raw = client.get_accounts(product_type=pt)
|
||||
else:
|
||||
raw = client.get_accounts()
|
||||
balance_data = _parse_balance(raw, exchange_id, market_type)
|
||||
elif (exchange_id or "").lower() == "bitget" and market_type == "spot" and hasattr(client, "get_assets"):
|
||||
raw = client.get_assets()
|
||||
balance_data = _parse_balance(raw, exchange_id, market_type)
|
||||
except Exception as be:
|
||||
logger.warning(f"Balance fetch failed: {be}")
|
||||
@@ -575,22 +598,62 @@ def _parse_balance(raw: Any, exchange_id: str, market_type: str) -> Dict[str, An
|
||||
result["total"] = float(b.get("free") or 0) + float(b.get("locked") or 0)
|
||||
return result
|
||||
return result
|
||||
ex = (exchange_id or "").lower()
|
||||
# Bitget mix: { code, data: [ { marginCoin, available, accountEquity, ... } ] }
|
||||
# Must run before OKX — both use data as a list; OKX fallback would zero Bitget.
|
||||
if ex == "bitget" and (market_type or "").lower() != "spot":
|
||||
bg_data = raw.get("data")
|
||||
if isinstance(bg_data, list) and bg_data:
|
||||
row = None
|
||||
for item in bg_data:
|
||||
if isinstance(item, dict) and str(item.get("marginCoin") or "").upper() == "USDT":
|
||||
row = item
|
||||
break
|
||||
if row is None and isinstance(bg_data[0], dict):
|
||||
row = bg_data[0]
|
||||
if isinstance(row, dict):
|
||||
av = (
|
||||
row.get("available")
|
||||
or row.get("availableBalance")
|
||||
or row.get("crossedMaxAvailable")
|
||||
or row.get("isolatedMaxAvailable")
|
||||
or 0
|
||||
)
|
||||
eq = row.get("accountEquity") or row.get("usdtEquity") or row.get("equity") or av
|
||||
result["available"] = float(av or 0)
|
||||
result["total"] = float(eq or 0) if eq is not None else result["available"]
|
||||
return result
|
||||
# Bitget spot: GET /api/v2/spot/account/assets
|
||||
if ex == "bitget" and (market_type or "").lower() == "spot":
|
||||
bg_data = raw.get("data")
|
||||
if isinstance(bg_data, list):
|
||||
for b in bg_data:
|
||||
if isinstance(b, dict) and str(b.get("coin") or "").upper() == "USDT":
|
||||
avail = float(b.get("available") or 0)
|
||||
frozen = float(b.get("frozen") or b.get("locked") or 0)
|
||||
result["available"] = avail
|
||||
result["total"] = avail + frozen
|
||||
return result
|
||||
return result
|
||||
# OKX
|
||||
data = raw.get("data")
|
||||
if isinstance(data, list) and data:
|
||||
first = data[0] if isinstance(data[0], dict) else {}
|
||||
# Account balance
|
||||
details = first.get("details", [])
|
||||
if isinstance(details, list):
|
||||
if isinstance(details, list) and details:
|
||||
for d in details:
|
||||
if str(d.get("ccy") or "").upper() == "USDT":
|
||||
result["available"] = float(d.get("availBal") or d.get("availEq") or 0)
|
||||
result["total"] = float(d.get("eq") or d.get("cashBal") or 0)
|
||||
return result
|
||||
# Fallback
|
||||
result["available"] = float(first.get("availBal") or first.get("totalEq") or 0)
|
||||
result["total"] = float(first.get("totalEq") or 0)
|
||||
return result
|
||||
# OKX-style single-account row (not Bitget — Bitget handled above)
|
||||
if "availBal" in first or "availEq" in first or "totalEq" in first or "adjEq" in first:
|
||||
result["available"] = float(
|
||||
first.get("availBal") or first.get("availEq") or first.get("adjEq") or first.get("totalEq") or 0
|
||||
)
|
||||
result["total"] = float(first.get("totalEq") or first.get("adjEq") or 0)
|
||||
return result
|
||||
# Bybit
|
||||
if "result" in raw:
|
||||
res = raw["result"]
|
||||
@@ -636,6 +699,106 @@ def _parse_balance(raw: Any, exchange_id: str, market_type: str) -> Dict[str, An
|
||||
return result
|
||||
|
||||
|
||||
def _fetch_exchange_positions_raw(
|
||||
client: Any,
|
||||
exchange_config: Dict[str, Any],
|
||||
*,
|
||||
symbol: str,
|
||||
market_type: str,
|
||||
) -> Any:
|
||||
"""
|
||||
Fetch raw position payload for quick-trade / close-position.
|
||||
|
||||
Many clients do not accept ``symbol=`` on ``get_positions()`` (Gate, KuCoin, Bybit, Bitfinex),
|
||||
or need extra args (Bitget ``product_type``, OKX ``inst_type``). Centralize here.
|
||||
"""
|
||||
from app.services.live_trading.binance import BinanceFuturesClient
|
||||
from app.services.live_trading.bitget import BitgetMixClient
|
||||
from app.services.live_trading.bybit import BybitClient
|
||||
from app.services.live_trading.deepcoin import DeepcoinClient
|
||||
from app.services.live_trading.gate import GateUsdtFuturesClient
|
||||
from app.services.live_trading.htx import HtxClient
|
||||
from app.services.live_trading.kucoin import KucoinFuturesClient
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
from app.services.live_trading.symbols import (
|
||||
to_bybit_symbol,
|
||||
to_gate_currency_pair,
|
||||
to_kucoin_futures_symbol,
|
||||
to_okx_spot_inst_id,
|
||||
to_okx_swap_inst_id,
|
||||
)
|
||||
|
||||
mt = (market_type or "swap").strip().lower()
|
||||
|
||||
if isinstance(client, OkxClient):
|
||||
if mt == "spot":
|
||||
inst_id = to_okx_spot_inst_id(symbol)
|
||||
inst_type = "SPOT"
|
||||
else:
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
inst_type = "SWAP"
|
||||
return client.get_positions(inst_id=inst_id, inst_type=inst_type)
|
||||
|
||||
if isinstance(client, BinanceFuturesClient):
|
||||
return client.get_positions(symbol=symbol)
|
||||
|
||||
if isinstance(client, BitgetMixClient):
|
||||
pt = str(exchange_config.get("product_type") or exchange_config.get("productType") or "USDT-FUTURES")
|
||||
return client.get_positions(product_type=pt, symbol=symbol)
|
||||
|
||||
if isinstance(client, BybitClient):
|
||||
raw = client.get_positions()
|
||||
lst = (((raw or {}).get("result") or {}).get("list")) if isinstance(raw, dict) else None
|
||||
if not isinstance(lst, list):
|
||||
return raw
|
||||
sym_norm = to_bybit_symbol(symbol)
|
||||
filtered = [p for p in lst if isinstance(p, dict) and str(p.get("symbol") or "").strip() == sym_norm]
|
||||
if isinstance(raw, dict):
|
||||
out = dict(raw)
|
||||
res = dict((raw.get("result") or {}) if isinstance(raw.get("result"), dict) else {})
|
||||
res["list"] = filtered
|
||||
out["result"] = res
|
||||
return out
|
||||
return {"result": {"list": filtered}}
|
||||
|
||||
if isinstance(client, GateUsdtFuturesClient):
|
||||
raw = client.get_positions()
|
||||
items = raw if isinstance(raw, list) else []
|
||||
c = to_gate_currency_pair(symbol)
|
||||
filtered = [p for p in items if isinstance(p, dict) and str(p.get("contract") or "").strip() == c]
|
||||
return filtered
|
||||
|
||||
if isinstance(client, KucoinFuturesClient):
|
||||
raw = client.get_positions()
|
||||
data = raw.get("data") if isinstance(raw, dict) else []
|
||||
sym = to_kucoin_futures_symbol(symbol)
|
||||
if not isinstance(data, list):
|
||||
data = []
|
||||
filtered = [p for p in data if isinstance(p, dict) and str(p.get("symbol") or "").strip() == sym]
|
||||
if isinstance(raw, dict):
|
||||
out = dict(raw)
|
||||
out["data"] = filtered
|
||||
return out
|
||||
return {"data": filtered}
|
||||
|
||||
if isinstance(client, HtxClient):
|
||||
return client.get_positions(symbol=symbol)
|
||||
|
||||
if isinstance(client, DeepcoinClient):
|
||||
return client.get_positions(symbol=symbol)
|
||||
|
||||
if hasattr(client, "get_positions"):
|
||||
try:
|
||||
return client.get_positions(symbol=symbol)
|
||||
except TypeError:
|
||||
return client.get_positions()
|
||||
|
||||
if hasattr(client, "get_position"):
|
||||
return client.get_position(symbol=symbol)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@quick_trade_bp.route('/position', methods=['GET'])
|
||||
@login_required
|
||||
def get_position():
|
||||
@@ -658,25 +821,10 @@ def get_position():
|
||||
|
||||
positions = []
|
||||
try:
|
||||
# OKX requires inst_id instead of symbol
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
if isinstance(client, OkxClient):
|
||||
from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_inst_id
|
||||
if market_type == "spot":
|
||||
inst_id = to_okx_spot_inst_id(symbol)
|
||||
inst_type = "SPOT"
|
||||
else:
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
inst_type = "SWAP"
|
||||
raw = client.get_positions(inst_id=inst_id, inst_type=inst_type)
|
||||
positions = _parse_positions(raw)
|
||||
logger.info(f"OKX positions query: inst_id={inst_id}, inst_type={inst_type}, found {len(positions)} positions")
|
||||
elif hasattr(client, "get_positions"):
|
||||
raw = client.get_positions(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_position"):
|
||||
raw = client.get_position(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
raw = _fetch_exchange_positions_raw(
|
||||
client, exchange_config, symbol=symbol, market_type=market_type
|
||||
)
|
||||
positions = _parse_positions(raw)
|
||||
except Exception as pe:
|
||||
logger.warning(f"Position fetch failed: {pe}")
|
||||
logger.warning(traceback.format_exc())
|
||||
@@ -698,33 +846,62 @@ def _parse_positions(raw: Any) -> list:
|
||||
if isinstance(raw, list):
|
||||
items = raw
|
||||
elif isinstance(raw, dict):
|
||||
data = raw.get("data") or raw.get("result") or raw.get("positions") or []
|
||||
if isinstance(data, list):
|
||||
items = data
|
||||
elif isinstance(data, dict):
|
||||
items = data.get("list", []) if "list" in data else [data]
|
||||
if isinstance(raw.get("raw"), list):
|
||||
items = raw["raw"]
|
||||
else:
|
||||
data = raw.get("data") or raw.get("result") or raw.get("positions") or []
|
||||
if isinstance(data, list):
|
||||
items = data
|
||||
elif isinstance(data, dict):
|
||||
items = data.get("list", []) if "list" in data else [data]
|
||||
else:
|
||||
items = []
|
||||
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
# For OKX, position size can be in different fields
|
||||
# SWAP: posAmt, pos
|
||||
# Binance futures: positionAmt
|
||||
# SPOT: bal (balance), availBal (available balance)
|
||||
size = float(item.get("posAmt") or item.get("pos") or item.get("size") or item.get("contracts") or
|
||||
item.get("bal") or item.get("availBal") or item.get("volume") or 0)
|
||||
size = float(
|
||||
item.get("positionAmt")
|
||||
or item.get("posAmt")
|
||||
or item.get("pos")
|
||||
or item.get("total")
|
||||
or item.get("currentQty")
|
||||
or item.get("available")
|
||||
or item.get("size")
|
||||
or item.get("contracts")
|
||||
or item.get("bal")
|
||||
or item.get("availBal")
|
||||
or item.get("volume")
|
||||
or 0
|
||||
)
|
||||
if abs(size) < 1e-10:
|
||||
continue
|
||||
|
||||
# For spot, side is always "long" (you own the asset)
|
||||
# For swap, determine side from sign of size
|
||||
# Binance hedge: positionSide LONG/SHORT with positive positionAmt; one-way: BOTH + signed amt
|
||||
side = "long"
|
||||
if size < 0:
|
||||
psu = str(item.get("positionSide", "")).strip().upper()
|
||||
if psu == "SHORT":
|
||||
side = "short"
|
||||
elif psu == "LONG":
|
||||
side = "long"
|
||||
elif item.get("posSide"):
|
||||
# OKX may have posSide field: "long" or "short"
|
||||
pos_side = str(item.get("posSide", "")).strip().lower()
|
||||
if pos_side in ("long", "short"):
|
||||
side = pos_side
|
||||
elif str(item.get("holdSide") or "").strip().lower() == "short":
|
||||
side = "short"
|
||||
elif str(item.get("holdSide") or "").strip().lower() == "long":
|
||||
side = "long"
|
||||
elif str(item.get("side") or "").strip().lower() in ("sell", "s"):
|
||||
side = "short"
|
||||
elif str(item.get("side") or "").strip().lower() in ("buy", "b"):
|
||||
side = "long"
|
||||
elif size < 0:
|
||||
side = "short"
|
||||
elif item.get("direction"):
|
||||
dir_side = str(item.get("direction") or "").strip().lower()
|
||||
if dir_side in ("buy", "long"):
|
||||
@@ -736,10 +913,36 @@ def _parse_positions(raw: Any) -> list:
|
||||
"symbol": item.get("symbol") or item.get("instId") or "",
|
||||
"side": side,
|
||||
"size": abs(size),
|
||||
"entry_price": float(item.get("entryPrice") or item.get("avgCost") or item.get("avgPx") or item.get("cost_open") or 0),
|
||||
"unrealized_pnl": float(item.get("unRealizedProfit") or item.get("upl") or item.get("unrealisedPnl") or item.get("profit_unreal") or item.get("pnl") or 0),
|
||||
"entry_price": float(
|
||||
item.get("entryPrice")
|
||||
or item.get("openPriceAvg")
|
||||
or item.get("avgEntryPrice")
|
||||
or item.get("avgPrice")
|
||||
or item.get("avgCost")
|
||||
or item.get("avgPx")
|
||||
or item.get("cost_open")
|
||||
or item.get("trade_avg_price")
|
||||
or 0
|
||||
),
|
||||
"unrealized_pnl": float(
|
||||
item.get("unRealizedProfit")
|
||||
or item.get("unrealizedProfit")
|
||||
or item.get("unrealizedPnl")
|
||||
or item.get("upl")
|
||||
or item.get("unrealisedPnl")
|
||||
or item.get("profit_unreal")
|
||||
or item.get("pnl")
|
||||
or 0
|
||||
),
|
||||
"leverage": float(item.get("leverage") or item.get("lever") or 1),
|
||||
"mark_price": float(item.get("markPrice") or item.get("markPx") or item.get("last_price") or item.get("last") or 0),
|
||||
"mark_price": float(
|
||||
item.get("markPrice")
|
||||
or item.get("markPx")
|
||||
or item.get("last_price")
|
||||
or item.get("last")
|
||||
or item.get("indexPrice")
|
||||
or 0
|
||||
),
|
||||
})
|
||||
except Exception as e:
|
||||
logger.warning(f"_parse_positions error: {e}")
|
||||
@@ -791,21 +994,10 @@ def close_position():
|
||||
# ---- get current position ----
|
||||
positions = []
|
||||
try:
|
||||
from app.services.live_trading.okx import OkxClient
|
||||
if isinstance(client, OkxClient):
|
||||
from app.services.live_trading.symbols import to_okx_swap_inst_id, to_okx_spot_inst_id
|
||||
if market_type == "spot":
|
||||
inst_id = to_okx_spot_inst_id(symbol)
|
||||
else:
|
||||
inst_id = to_okx_swap_inst_id(symbol)
|
||||
raw = client.get_positions(inst_id=inst_id)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_positions"):
|
||||
raw = client.get_positions(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
elif hasattr(client, "get_position"):
|
||||
raw = client.get_position(symbol=symbol)
|
||||
positions = _parse_positions(raw)
|
||||
raw = _fetch_exchange_positions_raw(
|
||||
client, exchange_config, symbol=symbol, market_type=market_type
|
||||
)
|
||||
positions = _parse_positions(raw)
|
||||
except Exception as pe:
|
||||
logger.warning(f"Position fetch failed: {pe}")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ Trading Strategy API Routes
|
||||
from flask import Blueprint, request, jsonify, g
|
||||
from datetime import datetime
|
||||
import json
|
||||
import re
|
||||
import traceback
|
||||
import time
|
||||
|
||||
@@ -770,15 +771,16 @@ def start_strategy():
|
||||
|
||||
# Get strategy type
|
||||
strategy_type = get_strategy_service().get_strategy_type(strategy_id)
|
||||
|
||||
# Update strategy status
|
||||
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
|
||||
|
||||
# Local backend: AI strategy executor was removed. Only indicator strategies are supported.
|
||||
if strategy_type == 'PromptBasedStrategy':
|
||||
return jsonify({'code': 0, 'msg': 'AI strategy has been removed; local edition does not support starting AI strategies', 'data': None}), 400
|
||||
|
||||
# Indicator strategy
|
||||
# IndicatorStrategy and ScriptStrategy are executed by TradingExecutor.
|
||||
if strategy_type == 'PromptBasedStrategy':
|
||||
return jsonify({
|
||||
'code': 0,
|
||||
'msg': 'AI strategy has been removed; local edition does not support starting AI strategies',
|
||||
'data': None
|
||||
}), 400
|
||||
get_strategy_service().update_strategy_status(strategy_id, 'running', user_id=user_id)
|
||||
|
||||
success = get_trading_executor().start_strategy(strategy_id)
|
||||
|
||||
if not success:
|
||||
@@ -1241,12 +1243,65 @@ def verify_strategy_code():
|
||||
@strategy_bp.route('/strategies/ai-generate', methods=['POST'])
|
||||
@login_required
|
||||
def ai_generate_strategy():
|
||||
"""Generate strategy code using AI."""
|
||||
"""Generate strategy code or suggest template parameter updates using AI."""
|
||||
try:
|
||||
payload = request.get_json() or {}
|
||||
prompt = payload.get('prompt', '')
|
||||
if not prompt.strip():
|
||||
return jsonify({'code': '', 'msg': 'Prompt is empty'})
|
||||
return jsonify({'code': '', 'msg': 'Prompt is empty', 'params': None})
|
||||
|
||||
intent = (payload.get('intent') or 'generate_code').strip()
|
||||
from app.services.llm import LLMService
|
||||
llm = LLMService()
|
||||
api_key = llm.get_api_key()
|
||||
if not api_key:
|
||||
return jsonify({'code': '', 'msg': 'No LLM API key configured', 'params': None})
|
||||
|
||||
if intent == 'adjust_params':
|
||||
template_key = payload.get('template_key') or ''
|
||||
current_params = payload.get('params') or {}
|
||||
code_snapshot = (payload.get('code') or '')[:8000]
|
||||
system_prompt = """You tune quantitative strategy template parameters from the user's request.
|
||||
Return ONLY a single JSON object: keys are parameter names (strings), values are JSON numbers or booleans.
|
||||
You may return a partial object (only keys that should change) or a full object.
|
||||
Do not use markdown fences, do not add explanations before or after the JSON."""
|
||||
|
||||
user_content = (
|
||||
f"Template key: {template_key}\n"
|
||||
f"Current parameters (JSON):\n{json.dumps(current_params, ensure_ascii=False)}\n\n"
|
||||
f"Strategy code excerpt (context):\n{code_snapshot}\n\n"
|
||||
f"User request:\n{prompt.strip()}\n\n"
|
||||
"Respond with JSON only."
|
||||
)
|
||||
|
||||
content = llm.call_llm_api(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": user_content},
|
||||
],
|
||||
model=llm.get_code_generation_model(),
|
||||
temperature=0.3,
|
||||
use_json_mode=False
|
||||
)
|
||||
|
||||
raw = (content or '').strip()
|
||||
if raw.startswith('```'):
|
||||
raw = re.sub(r'^```[a-zA-Z]*', '', raw).strip()
|
||||
if raw.endswith('```'):
|
||||
raw = raw[:-3].strip()
|
||||
updates = None
|
||||
try:
|
||||
updates = json.loads(raw)
|
||||
except json.JSONDecodeError:
|
||||
m = re.search(r'\{[\s\S]*\}', raw)
|
||||
if m:
|
||||
try:
|
||||
updates = json.loads(m.group(0))
|
||||
except json.JSONDecodeError:
|
||||
updates = None
|
||||
if not isinstance(updates, dict):
|
||||
return jsonify({'code': '', 'params': None, 'msg': 'AI did not return valid JSON parameters'})
|
||||
return jsonify({'code': '', 'params': updates, 'msg': 'success'})
|
||||
|
||||
system_prompt = """You are a quantitative trading strategy code generator.
|
||||
Generate Python strategy code that follows this framework:
|
||||
@@ -1264,16 +1319,26 @@ Generate Python strategy code that follows this framework:
|
||||
|
||||
Return ONLY the Python code, no explanations."""
|
||||
|
||||
from app.services.llm import LLMService
|
||||
llm = LLMService()
|
||||
api_key = llm.get_api_key()
|
||||
if not api_key:
|
||||
return jsonify({'code': '', 'msg': 'No LLM API key configured'})
|
||||
extra = ''
|
||||
template_key = payload.get('template_key')
|
||||
params = payload.get('params')
|
||||
code_ctx = (payload.get('code') or '').strip()
|
||||
if template_key or params is not None or code_ctx:
|
||||
extra_parts = []
|
||||
if template_key:
|
||||
extra_parts.append(f"Current template key: {template_key}")
|
||||
if isinstance(params, dict) and params:
|
||||
extra_parts.append('Current template parameters (JSON):\n' + json.dumps(params, ensure_ascii=False))
|
||||
if code_ctx:
|
||||
extra_parts.append('Current code (may be long):\n' + code_ctx[:12000])
|
||||
extra = '\n\n' + '\n\n'.join(extra_parts)
|
||||
|
||||
user_prompt = prompt.strip() + extra
|
||||
|
||||
content = llm.call_llm_api(
|
||||
messages=[
|
||||
{"role": "system", "content": system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
{"role": "user", "content": user_prompt},
|
||||
],
|
||||
model=llm.get_code_generation_model(),
|
||||
temperature=0.7,
|
||||
@@ -1290,12 +1355,12 @@ Return ONLY the Python code, no explanations."""
|
||||
content = content.strip()
|
||||
|
||||
if content:
|
||||
return jsonify({'code': content, 'msg': 'success'})
|
||||
return jsonify({'code': content, 'msg': 'success', 'params': None})
|
||||
else:
|
||||
return jsonify({'code': '', 'msg': 'AI generation returned empty result'})
|
||||
return jsonify({'code': '', 'msg': 'AI generation returned empty result', 'params': None})
|
||||
except Exception as e:
|
||||
logger.error(f"ai_generate_strategy failed: {str(e)}")
|
||||
return jsonify({'code': '', 'msg': str(e)})
|
||||
return jsonify({'code': '', 'msg': str(e), 'params': None})
|
||||
|
||||
|
||||
@strategy_bp.route('/strategies/performance', methods=['GET'])
|
||||
|
||||
@@ -504,6 +504,12 @@ class BacktestService:
|
||||
result['precision_info']['message'] = 'Using standard backtest because scale rules are not fully supported in MTF mode'
|
||||
elif fallback_reason == 'signal_timing_not_supported_in_mtf':
|
||||
result['precision_info']['message'] = 'Using standard backtest because this execution timing is not fully supported in MTF mode'
|
||||
ea = result.get('executionAssumptions') or {}
|
||||
ea['mtfRequested'] = bool(enable_mtf)
|
||||
ea['mtfActive'] = False
|
||||
if fallback_reason:
|
||||
ea['mtfFallbackReason'] = fallback_reason
|
||||
result['executionAssumptions'] = ea
|
||||
return result
|
||||
|
||||
logger.info(f"Multi-timeframe backtest: strategy_tf={timeframe}, exec_tf={exec_tf}, range={start_date} ~ {end_date}")
|
||||
@@ -554,6 +560,11 @@ class BacktestService:
|
||||
'reason': 'data_unavailable',
|
||||
'message': f'Cannot fetch {exec_tf} data, using standard backtest'
|
||||
}
|
||||
ea = result.get('executionAssumptions') or {}
|
||||
ea['mtfRequested'] = bool(enable_mtf)
|
||||
ea['mtfActive'] = False
|
||||
ea['mtfFallbackReason'] = 'data_unavailable'
|
||||
result['executionAssumptions'] = ea
|
||||
return result
|
||||
|
||||
logger.info(f"Data fetched: signal_candles={len(df_signal)}, exec_candles={len(df_exec)}")
|
||||
@@ -598,6 +609,14 @@ class BacktestService:
|
||||
result['execution_timeframe'] = exec_tf
|
||||
result['signal_candles'] = len(df_signal)
|
||||
result['execution_candles'] = len(df_exec)
|
||||
result['executionAssumptions'] = self._execution_assumptions(
|
||||
strategy_config,
|
||||
simulation_mode='mtf',
|
||||
signal_timeframe=timeframe,
|
||||
execution_timeframe=exec_tf,
|
||||
mtf_requested=True,
|
||||
mtf_active=True,
|
||||
)
|
||||
logger.info("Backtest result formatted successfully")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to format result: {str(e)}")
|
||||
@@ -1487,6 +1506,11 @@ class BacktestService:
|
||||
'precision': 'standard',
|
||||
'message': 'Using standard strategy script backtest'
|
||||
}
|
||||
result['executionAssumptions'] = self._execution_assumptions(
|
||||
strategy_config,
|
||||
simulation_mode='standard',
|
||||
signal_timeframe=timeframe,
|
||||
)
|
||||
return result
|
||||
|
||||
def run_code_strategy(
|
||||
@@ -1607,7 +1631,13 @@ class BacktestService:
|
||||
metrics = self._calculate_metrics(equity_curve, trades, initial_capital, timeframe, start_date, end_date, total_commission)
|
||||
|
||||
# 5. Format result
|
||||
return self._format_result(metrics, equity_curve, trades)
|
||||
result = self._format_result(metrics, equity_curve, trades)
|
||||
result['executionAssumptions'] = self._execution_assumptions(
|
||||
strategy_config,
|
||||
simulation_mode='standard',
|
||||
signal_timeframe=timeframe,
|
||||
)
|
||||
return result
|
||||
|
||||
def _fetch_kline_data(
|
||||
self,
|
||||
@@ -4740,6 +4770,46 @@ import pandas as pd
|
||||
logger.warning(f"Sharpe ratio calculation failed: {e}")
|
||||
return 0
|
||||
|
||||
def _execution_assumptions(
|
||||
self,
|
||||
strategy_config: Optional[Dict[str, Any]],
|
||||
*,
|
||||
simulation_mode: str,
|
||||
signal_timeframe: Optional[str] = None,
|
||||
execution_timeframe: Optional[str] = None,
|
||||
mtf_requested: bool = False,
|
||||
mtf_active: bool = False,
|
||||
mtf_fallback_reason: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Human-facing metadata so the UI can explain how trades were timed vs chart markers.
|
||||
Keys use camelCase for JSON consumers (frontend).
|
||||
"""
|
||||
cfg = strategy_config or {}
|
||||
raw = str((cfg.get('execution') or {}).get('signalTiming') or 'next_bar_open').strip().lower()
|
||||
is_next_open = raw in ('next_bar_open', 'next_open', 'nextopen', 'next')
|
||||
if raw in ('bar_close', 'close', 'same_bar_close', 'current_bar_close'):
|
||||
timing_key = 'same_bar_close'
|
||||
elif is_next_open:
|
||||
timing_key = 'next_bar_open'
|
||||
else:
|
||||
timing_key = raw
|
||||
default_fill = 'open' if is_next_open else 'close'
|
||||
payload: Dict[str, Any] = {
|
||||
'signalTiming': timing_key,
|
||||
'signalTimingRaw': raw,
|
||||
'defaultFillPrice': default_fill,
|
||||
'simulationMode': simulation_mode,
|
||||
'strategyTimeframe': signal_timeframe,
|
||||
'executionTimeframe': execution_timeframe,
|
||||
'engineVersion': self.ENGINE_VERSION,
|
||||
'mtfRequested': bool(mtf_requested),
|
||||
'mtfActive': bool(mtf_active),
|
||||
}
|
||||
if mtf_fallback_reason:
|
||||
payload['mtfFallbackReason'] = mtf_fallback_reason
|
||||
return payload
|
||||
|
||||
def _format_result(
|
||||
self,
|
||||
metrics: Dict,
|
||||
|
||||
@@ -40,6 +40,10 @@ class BinanceFuturesClient(BaseRestClient):
|
||||
self._dual_side_cache: Optional[Tuple[float, bool]] = None
|
||||
self._dual_side_cache_ttl_sec = 60.0
|
||||
|
||||
# serverTime - local_ms; avoids Binance -1021 when the host clock is ahead of Binance.
|
||||
self._time_offset_ms: int = 0
|
||||
self._time_sync_monotonic: float = 0.0
|
||||
|
||||
@staticmethod
|
||||
def _to_dec(x: Any) -> Decimal:
|
||||
try:
|
||||
@@ -163,6 +167,26 @@ class BinanceFuturesClient(BaseRestClient):
|
||||
def _signed_headers(self) -> Dict[str, str]:
|
||||
return {"X-MBX-APIKEY": self.api_key}
|
||||
|
||||
def _ensure_server_time(self, *, force: bool = False) -> None:
|
||||
"""
|
||||
Align signed request timestamps with Binance server time (GET /fapi/v1/time).
|
||||
"""
|
||||
now_m = time.monotonic()
|
||||
if not force and (now_m - float(self._time_sync_monotonic or 0.0)) < 300.0:
|
||||
return
|
||||
try:
|
||||
code, data, _ = self._request("GET", "/fapi/v1/time")
|
||||
if code != 200 or not isinstance(data, dict):
|
||||
return
|
||||
server_ms = int(data.get("serverTime") or 0)
|
||||
if server_ms <= 0:
|
||||
return
|
||||
local_ms = int(time.time() * 1000)
|
||||
self._time_offset_ms = server_ms - local_ms
|
||||
self._time_sync_monotonic = now_m
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _format_client_order_id(self, client_order_id: Optional[str]) -> str:
|
||||
raw = str(client_order_id or "").strip()
|
||||
broker_id = str(self.broker_id or "").strip()
|
||||
@@ -179,17 +203,34 @@ class BinanceFuturesClient(BaseRestClient):
|
||||
return f"{prefix}{raw[:suffix_budget]}"
|
||||
|
||||
def _signed_request(self, method: str, path: str, *, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
p = dict(params or {})
|
||||
# Use server-accepted timestamp in ms.
|
||||
p["timestamp"] = int(time.time() * 1000)
|
||||
qs = urlencode(p, doseq=True)
|
||||
p["signature"] = self._sign(qs)
|
||||
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"Binance HTTP {code}: {text[:500]}")
|
||||
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
|
||||
raise LiveTradingError(f"Binance error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
self._ensure_server_time()
|
||||
last_err: Optional[LiveTradingError] = None
|
||||
for attempt in range(2):
|
||||
p = dict(params or {})
|
||||
p["timestamp"] = int(time.time() * 1000) + int(self._time_offset_ms)
|
||||
if "recvWindow" not in p:
|
||||
p["recvWindow"] = 10000
|
||||
qs = urlencode(p, doseq=True)
|
||||
p["signature"] = self._sign(qs)
|
||||
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
|
||||
if code >= 400:
|
||||
err = LiveTradingError(f"Binance HTTP {code}: {text[:500]}")
|
||||
if attempt == 0 and ("-1021" in text or "1021" in text):
|
||||
self._ensure_server_time(force=True)
|
||||
last_err = err
|
||||
continue
|
||||
raise err
|
||||
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
|
||||
err = LiveTradingError(f"Binance error: {data}")
|
||||
if attempt == 0 and int(data.get("code") or 0) == -1021:
|
||||
self._ensure_server_time(force=True)
|
||||
last_err = err
|
||||
continue
|
||||
raise err
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
if last_err:
|
||||
raise last_err
|
||||
raise LiveTradingError("Binance signed request failed")
|
||||
|
||||
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
|
||||
@@ -870,12 +911,41 @@ class BinanceFuturesClient(BaseRestClient):
|
||||
raise LiveTradingError("Binance cancel_order requires order_id or client_order_id")
|
||||
return self._signed_request("DELETE", "/fapi/v1/order", params=params)
|
||||
|
||||
def get_positions(self) -> Any:
|
||||
def set_margin_type(self, *, symbol: str, margin_mode: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Return all futures positions (position risk endpoint).
|
||||
Set symbol margin mode on USDT-M futures.
|
||||
|
||||
Endpoint: POST /fapi/v1/marginType
|
||||
margin_mode: cross | crossed | isolated
|
||||
"""
|
||||
sym = to_binance_futures_symbol(symbol)
|
||||
m = (margin_mode or "").strip().lower()
|
||||
if m in ("cross", "crossed"):
|
||||
mt = "CROSSED"
|
||||
elif m in ("isolated", "iso"):
|
||||
mt = "ISOLATED"
|
||||
else:
|
||||
raise LiveTradingError(f"Invalid margin_mode for Binance: {margin_mode}")
|
||||
return self._signed_request("POST", "/fapi/v1/marginType", params={"symbol": sym, "marginType": mt})
|
||||
|
||||
def get_positions(self, *, symbol: str = "") -> Any:
|
||||
"""
|
||||
Futures positions (position risk). Optional ``symbol`` filters to one contract.
|
||||
|
||||
Endpoint: GET /fapi/v2/positionRisk
|
||||
"""
|
||||
return self._signed_request("GET", "/fapi/v2/positionRisk", params={})
|
||||
raw = self._signed_request("GET", "/fapi/v2/positionRisk", params={})
|
||||
rows: list
|
||||
if isinstance(raw, list):
|
||||
rows = raw
|
||||
elif isinstance(raw, dict) and isinstance(raw.get("raw"), list):
|
||||
rows = raw["raw"]
|
||||
else:
|
||||
rows = []
|
||||
want = (symbol or "").strip()
|
||||
if not want:
|
||||
return rows
|
||||
sym = to_binance_futures_symbol(want)
|
||||
return [p for p in rows if isinstance(p, dict) and str(p.get("symbol") or "") == sym]
|
||||
|
||||
|
||||
|
||||
@@ -31,6 +31,9 @@ class BinanceSpotClient(BaseRestClient):
|
||||
self._sym_filter_cache: Dict[str, Tuple[float, Dict[str, Any]]] = {}
|
||||
self._sym_filter_cache_ttl_sec = 300.0
|
||||
|
||||
self._time_offset_ms: int = 0
|
||||
self._time_sync_monotonic: float = 0.0
|
||||
|
||||
@staticmethod
|
||||
def _to_dec(x: Any) -> Decimal:
|
||||
try:
|
||||
@@ -158,6 +161,24 @@ class BinanceSpotClient(BaseRestClient):
|
||||
def _signed_headers(self) -> Dict[str, str]:
|
||||
return {"X-MBX-APIKEY": self.api_key}
|
||||
|
||||
def _ensure_server_time(self, *, force: bool = False) -> None:
|
||||
"""Align signed request timestamps with Binance (GET /api/v3/time)."""
|
||||
now_m = time.monotonic()
|
||||
if not force and (now_m - float(self._time_sync_monotonic or 0.0)) < 300.0:
|
||||
return
|
||||
try:
|
||||
code, data, _ = self._request("GET", "/api/v3/time")
|
||||
if code != 200 or not isinstance(data, dict):
|
||||
return
|
||||
server_ms = int(data.get("serverTime") or 0)
|
||||
if server_ms <= 0:
|
||||
return
|
||||
local_ms = int(time.time() * 1000)
|
||||
self._time_offset_ms = server_ms - local_ms
|
||||
self._time_sync_monotonic = now_m
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _format_client_order_id(self, client_order_id: Optional[str]) -> str:
|
||||
raw = str(client_order_id or "").strip()
|
||||
broker_id = str(self.broker_id or "").strip()
|
||||
@@ -174,16 +195,34 @@ class BinanceSpotClient(BaseRestClient):
|
||||
return f"{prefix}{raw[:suffix_budget]}"
|
||||
|
||||
def _signed_request(self, method: str, path: str, *, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
p = dict(params or {})
|
||||
p["timestamp"] = int(time.time() * 1000)
|
||||
qs = urlencode(p, doseq=True)
|
||||
p["signature"] = self._sign(qs)
|
||||
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
|
||||
if code >= 400:
|
||||
raise LiveTradingError(f"BinanceSpot HTTP {code}: {text[:500]}")
|
||||
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
|
||||
raise LiveTradingError(f"BinanceSpot error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
self._ensure_server_time()
|
||||
last_err: Optional[LiveTradingError] = None
|
||||
for attempt in range(2):
|
||||
p = dict(params or {})
|
||||
p["timestamp"] = int(time.time() * 1000) + int(self._time_offset_ms)
|
||||
if "recvWindow" not in p:
|
||||
p["recvWindow"] = 10000
|
||||
qs = urlencode(p, doseq=True)
|
||||
p["signature"] = self._sign(qs)
|
||||
code, data, text = self._request(method, path, params=p, headers=self._signed_headers())
|
||||
if code >= 400:
|
||||
err = LiveTradingError(f"BinanceSpot HTTP {code}: {text[:500]}")
|
||||
if attempt == 0 and ("-1021" in text or "1021" in text):
|
||||
self._ensure_server_time(force=True)
|
||||
last_err = err
|
||||
continue
|
||||
raise err
|
||||
if isinstance(data, dict) and data.get("code") and int(data.get("code")) < 0:
|
||||
err = LiveTradingError(f"BinanceSpot error: {data}")
|
||||
if attempt == 0 and int(data.get("code") or 0) == -1021:
|
||||
self._ensure_server_time(force=True)
|
||||
last_err = err
|
||||
continue
|
||||
raise err
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
if last_err:
|
||||
raise last_err
|
||||
raise LiveTradingError("BinanceSpot signed request failed")
|
||||
|
||||
def ping(self) -> bool:
|
||||
"""
|
||||
|
||||
@@ -61,6 +61,10 @@ class BitgetMixClient(BaseRestClient):
|
||||
self._lev_cache: Dict[str, Tuple[float, bool]] = {}
|
||||
self._lev_cache_ttl_sec = 60.0
|
||||
|
||||
# posMode from GET /api/v2/mix/account/account (hedge_mode vs one_way_mode), cached per contract.
|
||||
self._pos_mode_cache: Dict[str, Tuple[float, str]] = {}
|
||||
self._pos_mode_cache_ttl_sec = 60.0
|
||||
|
||||
@staticmethod
|
||||
def _to_dec(x: Any) -> Decimal:
|
||||
try:
|
||||
@@ -242,6 +246,31 @@ class BitgetMixClient(BaseRestClient):
|
||||
raise LiveTradingError(f"Bitget error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
def _post_mix_place_order(
|
||||
self,
|
||||
body: Dict[str, Any],
|
||||
*,
|
||||
original_side: str,
|
||||
reduce_only: bool,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
POST place-order; on 40774 (hedge vs one-way mismatch) retry with alternate position fields.
|
||||
"""
|
||||
sd = (original_side or "").lower()
|
||||
try:
|
||||
return self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=body)
|
||||
except LiveTradingError as e:
|
||||
if "40774" not in str(e):
|
||||
raise
|
||||
b2: Dict[str, Any] = {k: v for k, v in body.items() if k not in ("side", "tradeSide", "reduceOnly")}
|
||||
if "tradeSide" in body:
|
||||
b2["side"] = sd
|
||||
b2["reduceOnly"] = "YES" if reduce_only else "NO"
|
||||
else:
|
||||
b2["tradeSide"] = "close" if reduce_only else "open"
|
||||
b2["side"] = ("sell" if sd == "buy" else "buy") if reduce_only else sd
|
||||
return self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=b2)
|
||||
|
||||
def _public_request(self, method: str, path: str, *, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
code, data, text = self._request(method, path, params=params, headers=None, json_body=None, data=None)
|
||||
if code >= 400:
|
||||
@@ -252,6 +281,117 @@ class BitgetMixClient(BaseRestClient):
|
||||
raise LiveTradingError(f"Bitget error: {data}")
|
||||
return data if isinstance(data, dict) else {"raw": data}
|
||||
|
||||
def get_ticker(self, *, symbol: str, **kwargs: Any) -> Dict[str, Any]:
|
||||
"""
|
||||
Public mix ticker (for USDT-notional -> base size conversion in quick trade).
|
||||
|
||||
Endpoint: GET /api/v2/mix/market/ticker
|
||||
"""
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
pt = str(kwargs.get("product_type") or "USDT-FUTURES")
|
||||
if not sym:
|
||||
return {}
|
||||
try:
|
||||
raw = self._public_request(
|
||||
"GET",
|
||||
"/api/v2/mix/market/ticker",
|
||||
params={"symbol": sym, "productType": pt},
|
||||
)
|
||||
except Exception:
|
||||
return {}
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
if isinstance(data, list) and data:
|
||||
data = data[0]
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
try:
|
||||
last = float(
|
||||
data.get("lastPr")
|
||||
or data.get("last")
|
||||
or data.get("close")
|
||||
or data.get("markPrice")
|
||||
or data.get("indexPrice")
|
||||
or 0
|
||||
)
|
||||
except Exception:
|
||||
last = 0.0
|
||||
if last <= 0:
|
||||
return {}
|
||||
return {"last": last, "price": last, "close": last}
|
||||
|
||||
def get_account_pos_mode(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
margin_coin: str = "USDT",
|
||||
product_type: str = "USDT-FUTURES",
|
||||
) -> str:
|
||||
"""
|
||||
Returns Bitget posMode for the contract account: 'hedge_mode', 'one_way_mode', or '' if unknown.
|
||||
|
||||
GET /api/v2/mix/account/account
|
||||
"""
|
||||
sym = to_bitget_um_symbol(symbol)
|
||||
if not sym:
|
||||
return ""
|
||||
mc = (margin_coin or "USDT").strip().upper()
|
||||
pt = str(product_type or "USDT-FUTURES")
|
||||
key = f"{pt}:{sym}:{mc}"
|
||||
now = time.time()
|
||||
cached = self._pos_mode_cache.get(key)
|
||||
if cached:
|
||||
ts, mode = cached
|
||||
if (now - float(ts or 0.0)) <= float(self._pos_mode_cache_ttl_sec or 60.0) and mode is not None:
|
||||
return str(mode)
|
||||
|
||||
try:
|
||||
resp = self._signed_request(
|
||||
"GET",
|
||||
"/api/v2/mix/account/account",
|
||||
params={
|
||||
"symbol": sym.lower(),
|
||||
"productType": pt,
|
||||
"marginCoin": mc.lower() or "usdt",
|
||||
},
|
||||
)
|
||||
d = resp.get("data") if isinstance(resp, dict) else None
|
||||
mode = ""
|
||||
if isinstance(d, dict):
|
||||
mode = str(d.get("posMode") or "").strip().lower()
|
||||
self._pos_mode_cache[key] = (now, mode)
|
||||
return mode
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _mix_order_position_fields(
|
||||
self,
|
||||
*,
|
||||
symbol: str,
|
||||
side: str,
|
||||
reduce_only: bool,
|
||||
margin_coin: str,
|
||||
product_type: str,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Bitget mix place-order: hedge_mode requires tradeSide open/close; one_way_mode requires reduceOnly YES/NO
|
||||
and must not send tradeSide (see Bitget API doc + CCXT bitget.py).
|
||||
"""
|
||||
sd = (side or "").lower()
|
||||
if sd not in ("buy", "sell"):
|
||||
raise LiveTradingError(f"Invalid side: {side}")
|
||||
pos_mode = self.get_account_pos_mode(
|
||||
symbol=symbol, margin_coin=margin_coin, product_type=product_type
|
||||
)
|
||||
hedge = pos_mode == "hedge_mode"
|
||||
if hedge:
|
||||
# Mirror CCXT: hedge close flips side; hedge open keeps side + tradeSide open.
|
||||
out: Dict[str, Any] = {
|
||||
"tradeSide": "close" if reduce_only else "open",
|
||||
"side": ("sell" if sd == "buy" else "buy") if reduce_only else sd,
|
||||
}
|
||||
return out
|
||||
return {"side": sd, "reduceOnly": "YES" if reduce_only else "NO"}
|
||||
|
||||
def get_contract(self, *, symbol: str, product_type: str = "USDT-FUTURES") -> Dict[str, Any]:
|
||||
"""
|
||||
Fetch contract metadata (best-effort) from public endpoint.
|
||||
@@ -354,13 +494,34 @@ class BitgetMixClient(BaseRestClient):
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v2/mix/account/accounts", params={"productType": str(product_type or "USDT-FUTURES")})
|
||||
|
||||
def get_positions(self, *, product_type: str = "USDT-FUTURES") -> Dict[str, Any]:
|
||||
def get_positions(self, *, product_type: str = "USDT-FUTURES", symbol: str = "") -> Dict[str, Any]:
|
||||
"""
|
||||
Get all positions (best-effort).
|
||||
Get positions (best-effort).
|
||||
|
||||
Endpoint: GET /api/v2/mix/position/all-position
|
||||
When ``symbol`` is set (e.g. ETH/USDT), filters the response list to that contract only.
|
||||
"""
|
||||
return self._signed_request("GET", "/api/v2/mix/position/all-position", params={"productType": str(product_type or "USDT-FUTURES")})
|
||||
resp = self._signed_request(
|
||||
"GET",
|
||||
"/api/v2/mix/position/all-position",
|
||||
params={"productType": str(product_type or "USDT-FUTURES")},
|
||||
)
|
||||
want = (symbol or "").strip()
|
||||
if not want:
|
||||
return resp
|
||||
sym_key = to_bitget_um_symbol(want).upper()
|
||||
if not isinstance(resp, dict):
|
||||
return resp
|
||||
data = resp.get("data")
|
||||
if not isinstance(data, list):
|
||||
return resp
|
||||
filtered = [
|
||||
p for p in data
|
||||
if isinstance(p, dict) and str(p.get("symbol") or "").strip().upper() == sym_key
|
||||
]
|
||||
out = dict(resp)
|
||||
out["data"] = filtered
|
||||
return out
|
||||
|
||||
def set_leverage(
|
||||
self,
|
||||
@@ -444,16 +605,22 @@ class BitgetMixClient(BaseRestClient):
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"marginCoin": str(margin_coin or "USDT"),
|
||||
"marginMode": self._normalize_margin_mode(margin_mode),
|
||||
"side": sd,
|
||||
"orderType": "market",
|
||||
"size": self._dec_str(sz_dec, strict_precision=sz_precision),
|
||||
}
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = "YES"
|
||||
body.update(
|
||||
self._mix_order_position_fields(
|
||||
symbol=symbol,
|
||||
side=sd,
|
||||
reduce_only=reduce_only,
|
||||
margin_coin=str(margin_coin or "USDT"),
|
||||
product_type=str(product_type or "USDT-FUTURES"),
|
||||
)
|
||||
)
|
||||
if client_order_id:
|
||||
body["clientOid"] = str(client_order_id)
|
||||
|
||||
raw = self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=body)
|
||||
raw = self._post_mix_place_order(body, original_side=sd, reduce_only=reduce_only)
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
exchange_order_id = ""
|
||||
if isinstance(data, dict):
|
||||
@@ -498,21 +665,27 @@ class BitgetMixClient(BaseRestClient):
|
||||
"productType": str(product_type or "USDT-FUTURES"),
|
||||
"marginCoin": str(margin_coin or "USDT"),
|
||||
"marginMode": self._normalize_margin_mode(margin_mode),
|
||||
"side": sd,
|
||||
"orderType": "limit",
|
||||
"price": str(px),
|
||||
"size": self._dec_str(sz_dec, strict_precision=sz_precision),
|
||||
}
|
||||
body.update(
|
||||
self._mix_order_position_fields(
|
||||
symbol=symbol,
|
||||
side=sd,
|
||||
reduce_only=reduce_only,
|
||||
margin_coin=str(margin_coin or "USDT"),
|
||||
product_type=str(product_type or "USDT-FUTURES"),
|
||||
)
|
||||
)
|
||||
# Force maker behavior when requested (avoid taker fills).
|
||||
if post_only:
|
||||
body["force"] = "post_only"
|
||||
else:
|
||||
body["force"] = "gtc"
|
||||
if reduce_only:
|
||||
body["reduceOnly"] = "YES"
|
||||
if client_order_id:
|
||||
body["clientOid"] = str(client_order_id)
|
||||
raw = self._signed_request("POST", "/api/v2/mix/order/place-order", json_body=body)
|
||||
raw = self._post_mix_place_order(body, original_side=sd, reduce_only=reduce_only)
|
||||
data = raw.get("data") if isinstance(raw, dict) else None
|
||||
exchange_order_id = str(data.get("orderId") or data.get("clientOid") or "") if isinstance(data, dict) else ""
|
||||
return LiveOrderResult(exchange_id="bitget", exchange_order_id=exchange_order_id, filled=0.0, avg_price=0.0, raw=raw)
|
||||
|
||||
@@ -489,16 +489,35 @@ class StrategyService:
|
||||
f"but fails for market_type={market_type}. This is almost always a permissions/product mismatch."
|
||||
)
|
||||
msg = f"{msg} | {hint}"
|
||||
hint_cn = (
|
||||
"币安接口返回 -2015(密钥/IP/权限不匹配)。请逐项核对:"
|
||||
"① API Key 是否勾选与当前测试一致的业务(现货选现货权限,合约选合约/U 本位权限);"
|
||||
"② 若启用 IP 白名单,是否包含当前服务器出口 IP(见下方 egress_ip);"
|
||||
"③ base_url 与密钥环境一致(主网密钥配 api.binance.com / fapi,模拟盘配 demo 域名与 demo Key);"
|
||||
"④ 无多余空格、复制完整 Secret。"
|
||||
)
|
||||
if alt_ok:
|
||||
hint_cn += (
|
||||
f" 自动探测:同一密钥在 market_type={alt_market_type} 可通过,"
|
||||
f"当前选择的 {market_type} 与密钥权限不一致的可能性很大。"
|
||||
)
|
||||
else:
|
||||
hint_cn = ""
|
||||
|
||||
fail_payload = {
|
||||
'exchange': safe_cfg,
|
||||
'client': client_kind,
|
||||
'market_type': market_type,
|
||||
'egress_ip': egress_ip,
|
||||
'base_url': getattr(client, "base_url", "") or "",
|
||||
}
|
||||
if hint_cn:
|
||||
fail_payload['hint_cn'] = hint_cn
|
||||
|
||||
return {
|
||||
'success': False,
|
||||
'message': f'Auth failed: {msg}',
|
||||
'data': {
|
||||
'exchange': safe_cfg,
|
||||
'client': client_kind,
|
||||
'market_type': market_type,
|
||||
'egress_ip': egress_ip,
|
||||
'base_url': getattr(client, "base_url", "") or "",
|
||||
},
|
||||
'data': fail_payload,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
"""
|
||||
Python 策略脚本(on_init / on_bar + ctx.buy/sell/close_position)运行时。
|
||||
与回测逻辑对齐,供 TradingExecutor 实盘逐根 K 线调用。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ScriptBar(dict):
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError(name) from exc
|
||||
|
||||
|
||||
class ScriptPosition(dict):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.clear_position()
|
||||
|
||||
def __getattr__(self, name: str) -> Any:
|
||||
try:
|
||||
return self[name]
|
||||
except KeyError as exc:
|
||||
raise AttributeError(name) from exc
|
||||
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self.get('side')) and float(self.get('size') or 0) > 0
|
||||
|
||||
def __int__(self) -> int:
|
||||
return int(self.get('direction') or 0)
|
||||
|
||||
def __float__(self) -> float:
|
||||
return float(self.get('direction') or 0)
|
||||
|
||||
def __eq__(self, other: Any) -> bool:
|
||||
try:
|
||||
return int(self) == int(other)
|
||||
except Exception:
|
||||
return dict.__eq__(self, other)
|
||||
|
||||
def __lt__(self, other: Any) -> bool:
|
||||
return int(self) < int(other)
|
||||
|
||||
def __le__(self, other: Any) -> bool:
|
||||
return int(self) <= int(other)
|
||||
|
||||
def __gt__(self, other: Any) -> bool:
|
||||
return int(self) > int(other)
|
||||
|
||||
def __ge__(self, other: Any) -> bool:
|
||||
return int(self) >= int(other)
|
||||
|
||||
def clear_position(self) -> None:
|
||||
self.clear()
|
||||
self.update({
|
||||
'side': '',
|
||||
'size': 0.0,
|
||||
'entry_price': 0.0,
|
||||
'direction': 0,
|
||||
'amount': 0.0,
|
||||
})
|
||||
|
||||
def open_position(self, side: str, entry_price: float, amount: float) -> None:
|
||||
direction = 1 if side == 'long' else (-1 if side == 'short' else 0)
|
||||
size = float(amount or 0.0)
|
||||
price = float(entry_price or 0.0)
|
||||
self.clear()
|
||||
self.update({
|
||||
'side': side,
|
||||
'size': size,
|
||||
'entry_price': price,
|
||||
'direction': direction,
|
||||
'amount': size,
|
||||
})
|
||||
|
||||
def add_position(self, entry_price: float, amount: float) -> None:
|
||||
extra = float(amount or 0.0)
|
||||
if extra <= 0:
|
||||
return
|
||||
current_size = float(self.get('size') or 0.0)
|
||||
current_price = float(self.get('entry_price') or 0.0)
|
||||
next_size = current_size + extra
|
||||
next_price = float(entry_price or current_price or 0.0)
|
||||
if current_size > 0 and current_price > 0 and next_size > 0:
|
||||
next_price = ((current_price * current_size) + (float(entry_price or current_price) * extra)) / next_size
|
||||
self['size'] = next_size
|
||||
self['amount'] = next_size
|
||||
self['entry_price'] = next_price
|
||||
|
||||
|
||||
class StrategyScriptContext:
|
||||
"""与回测 ScriptBacktestContext 行为一致,供实盘按根推进。"""
|
||||
|
||||
def __init__(self, bars_df: pd.DataFrame, initial_balance: float):
|
||||
self._bars_df = bars_df
|
||||
self._params: Dict[str, Any] = {}
|
||||
self._orders: List[Dict[str, Any]] = []
|
||||
self._logs: List[str] = []
|
||||
self.current_index = -1
|
||||
self.position = ScriptPosition()
|
||||
self.balance = float(initial_balance)
|
||||
self.equity = float(initial_balance)
|
||||
|
||||
def param(self, name: str, default: Any = None) -> Any:
|
||||
if name not in self._params:
|
||||
self._params[name] = default
|
||||
return self._params[name]
|
||||
|
||||
def bars(self, n: int = 1):
|
||||
start = max(0, self.current_index - int(n) + 1)
|
||||
out = []
|
||||
for _, row in self._bars_df.iloc[start:self.current_index + 1].iterrows():
|
||||
out.append(ScriptBar(
|
||||
open=float(row.get('open') or 0),
|
||||
high=float(row.get('high') or 0),
|
||||
low=float(row.get('low') or 0),
|
||||
close=float(row.get('close') or 0),
|
||||
volume=float(row.get('volume') or 0),
|
||||
timestamp=row.get('time')
|
||||
))
|
||||
return out
|
||||
|
||||
def log(self, message: Any):
|
||||
self._logs.append(str(message))
|
||||
|
||||
def buy(self, price: Any = None, amount: Any = None):
|
||||
self._orders.append({'action': 'buy', 'price': price, 'amount': amount})
|
||||
|
||||
def sell(self, price: Any = None, amount: Any = None):
|
||||
self._orders.append({'action': 'sell', 'price': price, 'amount': amount})
|
||||
|
||||
def close_position(self):
|
||||
self._orders.append({'action': 'close'})
|
||||
|
||||
|
||||
def compile_strategy_script_handlers(code: str) -> Tuple[Optional[Callable], Optional[Callable]]:
|
||||
"""
|
||||
校验并编译策略脚本,返回 (on_init, on_bar)。
|
||||
on_bar 不可缺省;on_init 可选。
|
||||
"""
|
||||
if not code or not str(code).strip():
|
||||
raise ValueError("Strategy script is empty")
|
||||
|
||||
import builtins
|
||||
|
||||
def safe_import(name, *args, **kwargs):
|
||||
allowed_modules = ['numpy', 'pandas', 'math', 'json', 'datetime', 'time']
|
||||
if name in allowed_modules or name.split('.')[0] in allowed_modules:
|
||||
return builtins.__import__(name, *args, **kwargs)
|
||||
raise ImportError(f"Import not allowed: {name}")
|
||||
|
||||
safe_builtins = {k: getattr(builtins, k) for k in dir(builtins)
|
||||
if not k.startswith('_') and k not in ['eval', 'exec', 'compile', 'open', 'input', 'help', 'exit', 'quit']}
|
||||
safe_builtins['__import__'] = safe_import
|
||||
|
||||
exec_env = {
|
||||
'__builtins__': safe_builtins,
|
||||
'np': np,
|
||||
'pd': pd,
|
||||
}
|
||||
|
||||
from app.utils.safe_exec import validate_code_safety, safe_exec_code
|
||||
|
||||
is_safe, error_msg = validate_code_safety(code)
|
||||
if not is_safe:
|
||||
raise ValueError(f"Code contains unsafe operations: {error_msg}")
|
||||
|
||||
exec_result = safe_exec_code(
|
||||
code=code,
|
||||
exec_globals=exec_env,
|
||||
exec_locals=exec_env,
|
||||
timeout=60
|
||||
)
|
||||
if not exec_result['success']:
|
||||
raise RuntimeError(f"Code execution failed: {exec_result.get('error')}")
|
||||
|
||||
on_init = exec_env.get('on_init')
|
||||
on_bar = exec_env.get('on_bar')
|
||||
if not callable(on_bar):
|
||||
raise ValueError("Strategy script must define on_bar(ctx, bar)")
|
||||
if on_init is not None and not callable(on_init):
|
||||
on_init = None
|
||||
return (on_init if callable(on_init) else None), on_bar
|
||||
@@ -1,5 +1,8 @@
|
||||
"""
|
||||
实时交易执行服务
|
||||
实时交易执行服务。
|
||||
|
||||
策略线程:拉 K 线/价格、算信号,将订单写入 pending_orders。
|
||||
实盘成交由 PendingOrderWorker + app.services.live_trading(各所直连 REST)完成,不在此模块使用 ccxt 下单。
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
@@ -15,13 +18,17 @@ import json
|
||||
from decimal import Decimal, ROUND_DOWN, ROUND_UP
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
import ccxt
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.db import get_db_connection
|
||||
from app.data_sources import DataSourceFactory
|
||||
from app.services.kline import KlineService
|
||||
from app.services.indicator_params import IndicatorParamsParser, IndicatorCaller
|
||||
from app.services.strategy_script_runtime import (
|
||||
ScriptBar,
|
||||
StrategyScriptContext,
|
||||
compile_strategy_script_handlers,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
@@ -465,6 +472,212 @@ class TradingExecutor:
|
||||
logger.error(f"Failed to stop strategy {strategy_id}: {str(e)}")
|
||||
logger.error(traceback.format_exc())
|
||||
return False
|
||||
|
||||
def _df_to_script_exec_df(self, df: pd.DataFrame) -> pd.DataFrame:
|
||||
out = df.reset_index()
|
||||
c0 = out.columns[0]
|
||||
if c0 != 'time':
|
||||
out.rename(columns={c0: 'time'}, inplace=True)
|
||||
return out
|
||||
|
||||
def _script_default_position_ratio(self, trading_config: Dict[str, Any]) -> float:
|
||||
try:
|
||||
ep = (trading_config or {}).get('entry_pct')
|
||||
if ep is not None:
|
||||
return float(self._to_ratio(ep, default=0.06))
|
||||
except Exception:
|
||||
pass
|
||||
return 0.06
|
||||
|
||||
def _hydrate_script_ctx_from_positions(self, ctx: StrategyScriptContext, strategy_id: int, symbol: str) -> None:
|
||||
ctx.position.clear_position()
|
||||
pl = self._get_current_positions(strategy_id, symbol)
|
||||
if not pl:
|
||||
return
|
||||
p = pl[0]
|
||||
side = (p.get('side') or 'long').strip().lower()
|
||||
if side not in ('long', 'short'):
|
||||
return
|
||||
size = float(p.get('size') or 0)
|
||||
ep = float(p.get('entry_price') or 0)
|
||||
if size > 0:
|
||||
ctx.position.open_position(side, ep, size)
|
||||
|
||||
def _init_script_strategy_context(
|
||||
self,
|
||||
strategy_id: int,
|
||||
df: pd.DataFrame,
|
||||
trading_config: Dict[str, Any],
|
||||
initial_capital: float,
|
||||
) -> Tuple[StrategyScriptContext, Optional[pd.Timestamp]]:
|
||||
df_exec = self._df_to_script_exec_df(df)
|
||||
ctx = StrategyScriptContext(df_exec, float(initial_capital or 0))
|
||||
raw = (trading_config or {}).get('script_runtime_state') or {}
|
||||
params = raw.get('params') if isinstance(raw, dict) else {}
|
||||
if isinstance(params, dict):
|
||||
ctx._params = dict(params)
|
||||
last_ts = None
|
||||
ts_s = raw.get('last_closed_bar_ts') if isinstance(raw, dict) else None
|
||||
if ts_s:
|
||||
try:
|
||||
last_ts = pd.Timestamp(ts_s)
|
||||
if last_ts.tzinfo is None:
|
||||
last_ts = last_ts.tz_localize('UTC')
|
||||
else:
|
||||
last_ts = last_ts.tz_convert('UTC')
|
||||
except Exception:
|
||||
last_ts = None
|
||||
return ctx, last_ts
|
||||
|
||||
def _persist_script_runtime_state(self, strategy_id: int, closed_ts: Any, params: Dict[str, Any]) -> None:
|
||||
try:
|
||||
safe_params = json.loads(json.dumps(params or {}, default=str))
|
||||
except Exception:
|
||||
safe_params = {}
|
||||
ts_str = ''
|
||||
try:
|
||||
if closed_ts is not None:
|
||||
ts_str = pd.Timestamp(closed_ts).isoformat()
|
||||
except Exception:
|
||||
ts_str = ''
|
||||
state = {'last_closed_bar_ts': ts_str, 'params': safe_params}
|
||||
try:
|
||||
with get_db_connection() as db:
|
||||
cur = db.cursor()
|
||||
cur.execute("SELECT trading_config FROM qd_strategies_trading WHERE id = %s", (strategy_id,))
|
||||
row = cur.fetchone()
|
||||
if not row:
|
||||
cur.close()
|
||||
return
|
||||
tc = row.get('trading_config')
|
||||
if isinstance(tc, str) and tc.strip():
|
||||
try:
|
||||
tc = json.loads(tc)
|
||||
except Exception:
|
||||
tc = {}
|
||||
elif not isinstance(tc, dict):
|
||||
tc = {}
|
||||
tc['script_runtime_state'] = state
|
||||
cur.execute(
|
||||
"UPDATE qd_strategies_trading SET trading_config = %s WHERE id = %s",
|
||||
(json.dumps(tc, ensure_ascii=False), strategy_id),
|
||||
)
|
||||
db.commit()
|
||||
cur.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Persist script runtime state failed: {e}")
|
||||
|
||||
def _script_orders_to_execution_signals(
|
||||
self,
|
||||
ctx: StrategyScriptContext,
|
||||
trade_direction: str,
|
||||
bar_close: float,
|
||||
closed_ts: pd.Timestamp,
|
||||
trading_config: Dict[str, Any],
|
||||
) -> List[Dict[str, Any]]:
|
||||
td = str(trade_direction or 'both').lower()
|
||||
if td not in ('long', 'short', 'both'):
|
||||
td = 'both'
|
||||
default_ratio = self._script_default_position_ratio(trading_config)
|
||||
try:
|
||||
ts_i = int(closed_ts.timestamp())
|
||||
except Exception:
|
||||
ts_i = int(time.time())
|
||||
out: List[Dict[str, Any]] = []
|
||||
trig = float(bar_close or 0)
|
||||
for order in list(ctx._orders or []):
|
||||
action = str(order.get('action') or '').lower()
|
||||
try:
|
||||
order_price = float(order.get('price') or bar_close or 0)
|
||||
except Exception:
|
||||
order_price = trig
|
||||
raw_amt = order.get('amount')
|
||||
pos_ratio = default_ratio
|
||||
if raw_amt is not None:
|
||||
try:
|
||||
v = float(raw_amt)
|
||||
if v > 0:
|
||||
pos_ratio = v
|
||||
except Exception:
|
||||
pass
|
||||
if action == 'close':
|
||||
if ctx.position > 0:
|
||||
out.append({'type': 'close_long', 'trigger_price': order_price or trig, 'position_size': 0, 'timestamp': ts_i})
|
||||
ctx.position.clear_position()
|
||||
elif ctx.position < 0:
|
||||
out.append({'type': 'close_short', 'trigger_price': order_price or trig, 'position_size': 0, 'timestamp': ts_i})
|
||||
ctx.position.clear_position()
|
||||
continue
|
||||
if action == 'buy':
|
||||
if ctx.position < 0:
|
||||
out.append({'type': 'close_short', 'trigger_price': order_price or trig, 'position_size': 0, 'timestamp': ts_i})
|
||||
ctx.position.clear_position()
|
||||
if td in ('long', 'both'):
|
||||
if ctx.position == 0:
|
||||
out.append({'type': 'open_long', 'trigger_price': order_price or trig, 'position_size': pos_ratio, 'timestamp': ts_i})
|
||||
ctx.position.open_position('long', order_price or trig, pos_ratio)
|
||||
else:
|
||||
out.append({'type': 'add_long', 'trigger_price': order_price or trig, 'position_size': pos_ratio, 'timestamp': ts_i})
|
||||
ctx.position.add_position(order_price or trig, pos_ratio)
|
||||
continue
|
||||
if action == 'sell':
|
||||
if ctx.position > 0:
|
||||
out.append({'type': 'close_long', 'trigger_price': order_price or trig, 'position_size': 0, 'timestamp': ts_i})
|
||||
ctx.position.clear_position()
|
||||
if td in ('short', 'both'):
|
||||
if ctx.position == 0:
|
||||
out.append({'type': 'open_short', 'trigger_price': order_price or trig, 'position_size': pos_ratio, 'timestamp': ts_i})
|
||||
ctx.position.open_position('short', order_price or trig, pos_ratio)
|
||||
else:
|
||||
out.append({'type': 'add_short', 'trigger_price': order_price or trig, 'position_size': pos_ratio, 'timestamp': ts_i})
|
||||
ctx.position.add_position(order_price or trig, pos_ratio)
|
||||
return out
|
||||
|
||||
def _script_evaluate_new_closed_bar(
|
||||
self,
|
||||
df: pd.DataFrame,
|
||||
ctx: StrategyScriptContext,
|
||||
on_bar,
|
||||
trade_direction: str,
|
||||
last_closed_ts: Optional[pd.Timestamp],
|
||||
strategy_id: int,
|
||||
symbol: str,
|
||||
trading_config: Dict[str, Any],
|
||||
) -> Tuple[List[Dict[str, Any]], Optional[pd.Timestamp]]:
|
||||
if df is None or len(df) < 2:
|
||||
return [], last_closed_ts
|
||||
closed_ts = df.index[-2]
|
||||
try:
|
||||
if last_closed_ts is not None and closed_ts <= last_closed_ts:
|
||||
return [], last_closed_ts
|
||||
except Exception:
|
||||
pass
|
||||
df_exec = self._df_to_script_exec_df(df)
|
||||
ctx._bars_df = df_exec
|
||||
pos = len(df) - 2
|
||||
ctx.current_index = int(pos)
|
||||
row = df_exec.iloc[pos]
|
||||
self._hydrate_script_ctx_from_positions(ctx, strategy_id, symbol)
|
||||
ctx._orders = []
|
||||
bar = ScriptBar(
|
||||
open=float(row.get('open') or 0),
|
||||
high=float(row.get('high') or 0),
|
||||
low=float(row.get('low') or 0),
|
||||
close=float(row.get('close') or 0),
|
||||
volume=float(row.get('volume') or 0),
|
||||
timestamp=row.get('time'),
|
||||
)
|
||||
try:
|
||||
on_bar(ctx, bar)
|
||||
except Exception as e:
|
||||
logger.error(f"Strategy {strategy_id} script on_bar error: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return [], last_closed_ts
|
||||
bar_close = float(row.get('close') or 0)
|
||||
pending = self._script_orders_to_execution_signals(ctx, trade_direction, bar_close, closed_ts, trading_config)
|
||||
self._persist_script_runtime_state(strategy_id, closed_ts, ctx._params)
|
||||
logger.info(f"Strategy {strategy_id} script closed bar {closed_ts} -> {len(pending)} signal(s)")
|
||||
return pending, closed_ts
|
||||
|
||||
def _run_strategy_loop(self, strategy_id: int):
|
||||
"""
|
||||
@@ -483,13 +696,15 @@ class TradingExecutor:
|
||||
logger.error(f"Strategy {strategy_id} not found")
|
||||
return
|
||||
|
||||
if strategy['strategy_type'] != 'IndicatorStrategy':
|
||||
logger.error(f"Strategy {strategy_id} has unsupported strategy_type for realtime execution: {strategy['strategy_type']}")
|
||||
stype = strategy.get('strategy_type') or ''
|
||||
if stype not in ('IndicatorStrategy', 'ScriptStrategy'):
|
||||
logger.error(f"Strategy {strategy_id} has unsupported strategy_type for realtime execution: {stype}")
|
||||
return
|
||||
|
||||
is_script = stype == 'ScriptStrategy'
|
||||
|
||||
# 初始化策略状态
|
||||
trading_config = strategy['trading_config']
|
||||
indicator_config = strategy['indicator_config']
|
||||
indicator_config = strategy.get('indicator_config') or {}
|
||||
ai_model_config = strategy.get('ai_model_config') or {}
|
||||
execution_mode = (strategy.get('execution_mode') or 'signal').strip().lower()
|
||||
if execution_mode not in ['signal', 'live']:
|
||||
@@ -545,68 +760,84 @@ class TradingExecutor:
|
||||
market_category = (strategy.get('market_category') or 'Crypto').strip()
|
||||
logger.info(f"Strategy {strategy_id} market_category: {market_category}")
|
||||
|
||||
# Check if this is a cross-sectional strategy
|
||||
cs_strategy_type = trading_config.get('cs_strategy_type', 'single')
|
||||
if cs_strategy_type == 'cross_sectional':
|
||||
# Run cross-sectional strategy loop
|
||||
self._run_cross_sectional_strategy_loop(
|
||||
strategy_id, strategy, trading_config, indicator_config,
|
||||
ai_model_config, execution_mode, notification_config,
|
||||
strategy_name, market_category, market_type, leverage,
|
||||
initial_capital, indicator_code, indicator_id
|
||||
)
|
||||
return
|
||||
|
||||
# 初始化交易所连接(信号模式下无需真实连接)
|
||||
exchange = None
|
||||
|
||||
# 安全获取 initial_capital
|
||||
# 安全获取 initial_capital(横截面分支也需要)
|
||||
try:
|
||||
initial_capital_val = strategy.get('initial_capital', 1000)
|
||||
if isinstance(initial_capital_val, (list, tuple)):
|
||||
initial_capital_val = initial_capital_val[0] if initial_capital_val else 1000
|
||||
initial_capital = float(initial_capital_val)
|
||||
except:
|
||||
except Exception:
|
||||
logger.warning(f"Strategy {strategy_id} invalid initial_capital format, reset to 1000: {strategy.get('initial_capital')}")
|
||||
initial_capital = 1000.0
|
||||
|
||||
# 净值会在首次更新持仓时自动计算和更新
|
||||
|
||||
# 获取指标代码
|
||||
indicator_id = indicator_config.get('indicator_id')
|
||||
indicator_code = indicator_config.get('indicator_code', '')
|
||||
|
||||
# 如果代码为空,尝试从数据库获取
|
||||
if not indicator_code and indicator_id:
|
||||
indicator_code = self._get_indicator_code_from_db(indicator_id)
|
||||
|
||||
if not indicator_code:
|
||||
logger.error(f"Strategy {strategy_id} indicator_code is empty")
|
||||
return
|
||||
|
||||
# 确保 indicator_code 是字符串(处理 JSON 转义问题)
|
||||
if not isinstance(indicator_code, str):
|
||||
indicator_code = str(indicator_code)
|
||||
|
||||
# 处理可能的 JSON 转义问题
|
||||
if '\\n' in indicator_code and '\n' not in indicator_code:
|
||||
|
||||
indicator_id = None
|
||||
indicator_code = ''
|
||||
strategy_code = ''
|
||||
on_init_script = None
|
||||
on_bar_script = None
|
||||
|
||||
if is_script:
|
||||
strategy_code = (strategy.get('strategy_code') or '').strip()
|
||||
if not strategy_code:
|
||||
logger.error(f"Strategy {strategy_id} strategy_code is empty")
|
||||
return
|
||||
if '\\n' in strategy_code and '\n' not in strategy_code:
|
||||
try:
|
||||
decoded = json.loads(f'"{strategy_code}"')
|
||||
if isinstance(decoded, str):
|
||||
strategy_code = decoded
|
||||
except Exception:
|
||||
strategy_code = (
|
||||
strategy_code.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
|
||||
.replace('\\"', '"').replace("\\'", "'").replace('\\\\', '\\')
|
||||
)
|
||||
try:
|
||||
import json
|
||||
decoded = json.loads(f'"{indicator_code}"')
|
||||
if isinstance(decoded, str):
|
||||
indicator_code = decoded
|
||||
logger.info(f"Strategy {strategy_id} decoded escaped indicator_code")
|
||||
on_init_script, on_bar_script = compile_strategy_script_handlers(strategy_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"Strategy {strategy_id} JSON decode failed; falling back to manual unescape: {str(e)}")
|
||||
indicator_code = (
|
||||
indicator_code
|
||||
.replace('\\n', '\n')
|
||||
.replace('\\t', '\t')
|
||||
.replace('\\r', '\r')
|
||||
.replace('\\"', '"')
|
||||
.replace("\\'", "'")
|
||||
.replace('\\\\', '\\')
|
||||
)
|
||||
logger.error(f"Strategy {strategy_id} script compile failed: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
return
|
||||
else:
|
||||
indicator_config = strategy['indicator_config']
|
||||
indicator_id = indicator_config.get('indicator_id')
|
||||
indicator_code = indicator_config.get('indicator_code', '')
|
||||
if not indicator_code and indicator_id:
|
||||
indicator_code = self._get_indicator_code_from_db(indicator_id)
|
||||
if not indicator_code:
|
||||
logger.error(f"Strategy {strategy_id} indicator_code is empty")
|
||||
return
|
||||
if not isinstance(indicator_code, str):
|
||||
indicator_code = str(indicator_code)
|
||||
if '\\n' in indicator_code and '\n' not in indicator_code:
|
||||
try:
|
||||
decoded = json.loads(f'"{indicator_code}"')
|
||||
if isinstance(decoded, str):
|
||||
indicator_code = decoded
|
||||
logger.info(f"Strategy {strategy_id} decoded escaped indicator_code")
|
||||
except Exception as e:
|
||||
logger.warning(f"Strategy {strategy_id} JSON decode failed; falling back to manual unescape: {str(e)}")
|
||||
indicator_code = (
|
||||
indicator_code.replace('\\n', '\n').replace('\\t', '\t').replace('\\r', '\r')
|
||||
.replace('\\"', '"').replace("\\'", "'").replace('\\\\', '\\')
|
||||
)
|
||||
|
||||
# Check if this is a cross-sectional strategy(仅指标策略支持)
|
||||
cs_strategy_type = trading_config.get('cs_strategy_type', 'single')
|
||||
if (not is_script) and cs_strategy_type == 'cross_sectional':
|
||||
self._run_cross_sectional_strategy_loop(
|
||||
strategy_id, strategy, trading_config, strategy['indicator_config'],
|
||||
ai_model_config, execution_mode, notification_config,
|
||||
strategy_name, market_category, market_type, leverage,
|
||||
initial_capital, indicator_code, indicator_id
|
||||
)
|
||||
return
|
||||
|
||||
if is_script and cs_strategy_type == 'cross_sectional':
|
||||
logger.error(f"Strategy {strategy_id} ScriptStrategy does not support cross_sectional mode")
|
||||
return
|
||||
|
||||
# 初始化交易所连接(信号模式下无需真实连接)
|
||||
exchange = None
|
||||
|
||||
# ============================================
|
||||
# 初始化阶段:获取历史K线并计算指标
|
||||
@@ -658,29 +889,47 @@ class TradingExecutor:
|
||||
initial_position_count = 1 # 简化处理,假设是单笔持仓
|
||||
initial_last_add_price = initial_avg_entry_price
|
||||
|
||||
# 关键诊断日志:确认指标是否拿到了持仓状态
|
||||
logger.info(
|
||||
f"策略 {strategy_id} 指标注入持仓状态: count={len(current_pos_list)}, "
|
||||
f"策略 {strategy_id} 持仓快照: count={len(current_pos_list)}, "
|
||||
f"position={initial_position}, entry_price={initial_avg_entry_price}, highest={initial_highest}"
|
||||
)
|
||||
|
||||
# 执行指标代码,获取信号和触发价格
|
||||
indicator_result = self._execute_indicator_with_prices(
|
||||
indicator_code, df, trading_config,
|
||||
initial_highest_price=initial_highest,
|
||||
initial_position=initial_position,
|
||||
initial_avg_entry_price=initial_avg_entry_price,
|
||||
initial_position_count=initial_position_count,
|
||||
initial_last_add_price=initial_last_add_price
|
||||
)
|
||||
if indicator_result is None:
|
||||
logger.error(f"Strategy {strategy_id} indicator execution failed")
|
||||
return
|
||||
|
||||
# 提取信号和触发价格
|
||||
pending_signals = indicator_result.get('pending_signals', []) # 待触发的信号列表
|
||||
last_kline_time = indicator_result.get('last_kline_time', 0) # 最后一根K线的时间
|
||||
|
||||
script_ctx = None
|
||||
last_script_closed_ts = None
|
||||
if is_script:
|
||||
script_ctx, last_script_closed_ts = self._init_script_strategy_context(
|
||||
strategy_id, df, trading_config, initial_capital
|
||||
)
|
||||
if on_init_script:
|
||||
self._hydrate_script_ctx_from_positions(script_ctx, strategy_id, symbol)
|
||||
try:
|
||||
on_init_script(script_ctx)
|
||||
except Exception as e:
|
||||
logger.error(f"Strategy {strategy_id} on_init error: {e}")
|
||||
logger.error(traceback.format_exc())
|
||||
pending_signals, last_script_closed_ts = self._script_evaluate_new_closed_bar(
|
||||
df, script_ctx, on_bar_script, trade_direction,
|
||||
last_script_closed_ts, strategy_id, symbol, trading_config,
|
||||
)
|
||||
try:
|
||||
last_kline_time = int(df.index[-1].timestamp())
|
||||
except Exception:
|
||||
last_kline_time = int(time.time())
|
||||
else:
|
||||
indicator_result = self._execute_indicator_with_prices(
|
||||
indicator_code, df, trading_config,
|
||||
initial_highest_price=initial_highest,
|
||||
initial_position=initial_position,
|
||||
initial_avg_entry_price=initial_avg_entry_price,
|
||||
initial_position_count=initial_position_count,
|
||||
initial_last_add_price=initial_last_add_price
|
||||
)
|
||||
if indicator_result is None:
|
||||
logger.error(f"Strategy {strategy_id} indicator execution failed")
|
||||
return
|
||||
pending_signals = indicator_result.get('pending_signals', [])
|
||||
last_kline_time = indicator_result.get('last_kline_time', 0)
|
||||
|
||||
logger.info(f"Strategy {strategy_id} initialized; pending_signals={len(pending_signals)}")
|
||||
if pending_signals:
|
||||
logger.info(f"Initial signals: {pending_signals}")
|
||||
@@ -744,52 +993,63 @@ class TradingExecutor:
|
||||
if klines and len(klines) >= 2:
|
||||
df = self._klines_to_dataframe(klines)
|
||||
if len(df) > 0:
|
||||
current_pos_list = self._get_current_positions(strategy_id, symbol)
|
||||
initial_highest = 0.0
|
||||
initial_position = 0
|
||||
initial_avg_entry_price = 0.0
|
||||
initial_position_count = 0
|
||||
initial_last_add_price = 0.0
|
||||
|
||||
if current_pos_list:
|
||||
pos = current_pos_list[0]
|
||||
initial_highest = float(pos.get('highest_price', 0) or 0)
|
||||
pos_side = pos.get('side', 'long')
|
||||
initial_position = 1 if pos_side == 'long' else -1
|
||||
initial_avg_entry_price = float(pos.get('entry_price', 0) or 0)
|
||||
initial_position_count = 1
|
||||
initial_last_add_price = initial_avg_entry_price
|
||||
|
||||
indicator_result = self._execute_indicator_with_prices(
|
||||
indicator_code, df, trading_config,
|
||||
initial_highest_price=initial_highest,
|
||||
initial_position=initial_position,
|
||||
initial_avg_entry_price=initial_avg_entry_price,
|
||||
initial_position_count=initial_position_count,
|
||||
initial_last_add_price=initial_last_add_price
|
||||
)
|
||||
if indicator_result:
|
||||
pending_signals = indicator_result.get('pending_signals', [])
|
||||
last_kline_time = indicator_result.get('last_kline_time', 0)
|
||||
new_hp = indicator_result.get('new_highest_price', 0)
|
||||
|
||||
if is_script:
|
||||
new_sig, last_script_closed_ts = self._script_evaluate_new_closed_bar(
|
||||
df, script_ctx, on_bar_script, trade_direction,
|
||||
last_script_closed_ts, strategy_id, symbol, trading_config,
|
||||
)
|
||||
pending_signals = new_sig
|
||||
try:
|
||||
last_kline_time = int(df.index[-1].timestamp())
|
||||
except Exception:
|
||||
last_kline_time = int(time.time())
|
||||
last_kline_update_time = current_time
|
||||
else:
|
||||
current_pos_list = self._get_current_positions(strategy_id, symbol)
|
||||
initial_highest = 0.0
|
||||
initial_position = 0
|
||||
initial_avg_entry_price = 0.0
|
||||
initial_position_count = 0
|
||||
initial_last_add_price = 0.0
|
||||
|
||||
# 更新 highest_price(使用最新 close 作为 current_price 的近似)
|
||||
if new_hp > 0 and current_pos_list:
|
||||
current_close = float(df['close'].iloc[-1])
|
||||
for p in current_pos_list:
|
||||
self._update_position(
|
||||
strategy_id, p['symbol'], p['side'],
|
||||
float(p['size']), float(p['entry_price']),
|
||||
current_close,
|
||||
highest_price=new_hp
|
||||
)
|
||||
if current_pos_list:
|
||||
pos = current_pos_list[0]
|
||||
initial_highest = float(pos.get('highest_price', 0) or 0)
|
||||
pos_side = pos.get('side', 'long')
|
||||
initial_position = 1 if pos_side == 'long' else -1
|
||||
initial_avg_entry_price = float(pos.get('entry_price', 0) or 0)
|
||||
initial_position_count = 1
|
||||
initial_last_add_price = initial_avg_entry_price
|
||||
|
||||
indicator_result = self._execute_indicator_with_prices(
|
||||
indicator_code, df, trading_config,
|
||||
initial_highest_price=initial_highest,
|
||||
initial_position=initial_position,
|
||||
initial_avg_entry_price=initial_avg_entry_price,
|
||||
initial_position_count=initial_position_count,
|
||||
initial_last_add_price=initial_last_add_price
|
||||
)
|
||||
if indicator_result:
|
||||
pending_signals = indicator_result.get('pending_signals', [])
|
||||
last_kline_time = indicator_result.get('last_kline_time', 0)
|
||||
new_hp = indicator_result.get('new_highest_price', 0)
|
||||
|
||||
last_kline_update_time = current_time
|
||||
|
||||
if new_hp > 0 and current_pos_list:
|
||||
current_close = float(df['close'].iloc[-1])
|
||||
for p in current_pos_list:
|
||||
self._update_position(
|
||||
strategy_id, p['symbol'], p['side'],
|
||||
float(p['size']), float(p['entry_price']),
|
||||
current_close,
|
||||
highest_price=new_hp
|
||||
)
|
||||
else:
|
||||
# ============================================
|
||||
# 3. 非K线更新tick:用当前价更新最后一根K线并重算指标(统一tick节奏)
|
||||
# 3. 非K线更新 tick:脚本策略不在这里重算(仅在新 K 收盘时 on_bar)
|
||||
# ============================================
|
||||
if 'df' in locals() and df is not None and len(df) > 0:
|
||||
if (not is_script) and 'df' in locals() and df is not None and len(df) > 0:
|
||||
try:
|
||||
realtime_df = df.copy()
|
||||
realtime_df = self._update_dataframe_with_current_price(realtime_df, current_price, timeframe)
|
||||
@@ -1055,7 +1315,7 @@ class TradingExecutor:
|
||||
initial_capital, leverage, decide_interval,
|
||||
execution_mode, notification_config,
|
||||
indicator_config, exchange_config, trading_config, ai_model_config,
|
||||
market_category
|
||||
market_category, strategy_code
|
||||
FROM qd_strategies_trading
|
||||
WHERE id = %s
|
||||
"""
|
||||
@@ -1144,8 +1404,14 @@ class TradingExecutor:
|
||||
market_type: str = None,
|
||||
leverage: float = None,
|
||||
strategy_id: int = None
|
||||
) -> Optional[ccxt.Exchange]:
|
||||
"""(Mock) 信号模式不需要真实交易所连接"""
|
||||
) -> Any:
|
||||
"""
|
||||
占位:策略线程内不创建交易所 SDK 实例。
|
||||
|
||||
实盘下单不经过本方法。信号经 _execute_exchange_order 写入 pending_orders,
|
||||
由 PendingOrderWorker 使用 app.services.live_trading 下的直连 REST 客户端执行。
|
||||
K 线/现价由 KlineService、DataSourceFactory 等数据层提供(该层可能使用 ccxt 拉行情,与下单解耦)。
|
||||
"""
|
||||
return None
|
||||
|
||||
def _fetch_latest_kline(self, symbol: str, timeframe: str, limit: int = 500, market_category: str = 'Crypto') -> List[Dict[str, Any]]:
|
||||
@@ -2434,14 +2700,13 @@ class TradingExecutor:
|
||||
signal_ts: int = 0,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
Convert a signal into a concrete pending order and enqueue it into DB.
|
||||
将信号转为 pending_orders 队列记录(本方法不直连交易所、不使用 ccxt)。
|
||||
|
||||
A separate worker will poll `pending_orders` and dispatch:
|
||||
- execution_mode='signal': dispatch notifications (no real trading).
|
||||
- execution_mode='live': reserved for future live trading execution (not implemented).
|
||||
PendingOrderWorker 轮询执行:
|
||||
- execution_mode='signal':仅通知/模拟路径。
|
||||
- execution_mode='live':通过 live_trading 包内的各交易所 REST 客户端下单(非 ccxt)。
|
||||
|
||||
Note: Order execution settings (order_mode, maker_wait_sec, maker_offset_bps) are now
|
||||
configured via environment variables and not passed from strategy config.
|
||||
行情/K 线不在此处拉取;order_mode 等由环境变量配置。
|
||||
"""
|
||||
try:
|
||||
# Reference price at enqueue time: use current tick price if provided to avoid extra fetch.
|
||||
|
||||
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+110
-100
@@ -39,9 +39,9 @@ body.dark .ant-pagination-item-active,body.realdark .ant-pagination-item-active{
|
||||
body.dark .ant-btn-default:hover,body.realdark .ant-btn-default:hover{border-color:#1890ff!important;color:#1890ff!important}
|
||||
body.dark .ant-tabs-tab-active,body.realdark .ant-tabs-tab-active{color:#1890ff!important}
|
||||
body.dark .ant-select-dropdown .ant-select-dropdown-menu-item-selected,body.realdark .ant-select-dropdown .ant-select-dropdown-menu-item-selected{background:rgba(24,144,255,.1)!important;color:#1890ff!important}
|
||||
.qt-header .qt-header-left .qt-icon[data-v-989be352]{color:#1890ff}
|
||||
.theme-dark[data-v-989be352] .ant-radio-group .ant-radio-button-wrapper.ant-radio-button-wrapper-checked{background:#1890ff;border-color:#1890ff}
|
||||
.theme-dark[data-v-989be352] .ant-slider-track{background:#1890ff}
|
||||
.profile-exchange-modal .exchange-account-form .exchange-api-doc-callout{border:1px solid rgba(24,144,255,.18);background:-webkit-gradient(linear,left top,right top,from(rgba(24,144,255,.06)),to(rgba(82,196,26,.04)));background:linear-gradient(90deg,rgba(24,144,255,.06),rgba(82,196,26,.04))}
|
||||
.profile-exchange-modal .exchange-account-form .exchange-api-doc-callout__icon{background:rgba(24,144,255,.12);color:#1890ff}
|
||||
.profile-exchange-modal--dark .exchange-account-form .exchange-api-doc-callout{background:-webkit-gradient(linear,left top,right top,from(rgba(24,144,255,.12)),to(rgba(82,196,26,.08)));background:linear-gradient(90deg,rgba(24,144,255,.12),rgba(82,196,26,.08))}
|
||||
.polymarket-analysis-modal .result-section .analysis-result .probability-comparison .prob-item .prob-value.ai-prob[data-v-67e891d2]{color:#1890ff}
|
||||
.polymarket-analysis-modal .result-section .analysis-result .recommendation-section .rec-card .rec-value[data-v-67e891d2]{color:#1890ff}
|
||||
.code-section .section-header .section-title[data-v-4fea1865]:before{background:#1890ff}
|
||||
@@ -92,24 +92,26 @@ body.dark .ant-select-dropdown .ant-select-dropdown-menu-item-selected,body.real
|
||||
.ant-layout.dark,.ant-layout.realdark,.ant-pro-layout.dark,.ant-pro-layout.realdark,.chart-container.theme-dark .chart-right .indicators-panel .indicator-card .action-icon.edit-icon[data-v-6bd239a6]:hover,body.dark,body.realdark{color:#40a9ff}
|
||||
.ant-layout.dark,.ant-layout.realdark,.ant-pro-layout.dark,.ant-pro-layout.realdark,.chart-container.theme-dark .chart-right .indicators-panel .indicator-card .action-icon.publish-icon[data-v-6bd239a6],body.dark,body.realdark{color:#1890ff}
|
||||
.ant-layout.dark,.ant-layout.realdark,.ant-pro-layout.dark,.ant-pro-layout.realdark,.chart-container.theme-dark .chart-right .indicators-panel .indicator-card .action-icon.publish-icon[data-v-6bd239a6]:hover,body.dark,body.realdark{color:#40a9ff}
|
||||
.ai-markdown-content[data-v-2a402d06] h3{border-left:3px solid #1890ff}
|
||||
.ai-markdown-content[data-v-2a402d06] blockquote{border-left:4px solid #91d5ff}
|
||||
.page-header .page-title .title-icon[data-v-6853ba40]{-webkit-text-fill-color:#1890ff}
|
||||
[data-v-6853ba40] .empty-result .empty-icon{background:linear-gradient(135deg,#e6f7ff,#f0f5ff);color:#1890ff}
|
||||
[data-v-6853ba40] .running-spinner .spinner-fill{border-top-color:#1890ff}
|
||||
[data-v-6853ba40] .running-elapsed{color:#1890ff}
|
||||
[data-v-6853ba40] .running-steps .step-item.active{background:#e6f7ff;border-color:#91d5ff}
|
||||
[data-v-6853ba40] .running-steps .step-item.active .step-icon{color:#1890ff}
|
||||
[data-v-6853ba40] .running-steps .step-item.active .step-label{color:#1890ff}
|
||||
[data-v-6853ba40] .result-ai-markdown-content/deep/h3{border-left:3px solid #1890ff}
|
||||
[data-v-6853ba40] .result-ai-markdown-content/deep/blockquote{border-left:3px solid #91d5ff}
|
||||
.add-item-active[data-v-6853ba40]{background:#e6f7ff!important}
|
||||
.theme-dark .page-header .page-title .title-icon[data-v-6853ba40]{color:#40a9ff!important}
|
||||
.trading-records[data-v-a5bdef7e] .ant-tag[color=blue]{background:linear-gradient(135deg,rgba(24,144,255,.15),rgba(24,144,255,.08));color:#1890ff;border:1px solid rgba(24,144,255,.3)}
|
||||
.trading-records[data-v-a5bdef7e] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
|
||||
.trading-records[data-v-a5bdef7e] .ant-pagination .ant-pagination-item:hover a{color:#1890ff}
|
||||
.trading-records[data-v-a5bdef7e] .ant-pagination .ant-pagination-item.ant-pagination-item-active{background:linear-gradient(135deg,#1890ff,#40a9ff);border-color:#1890ff}
|
||||
.trading-records[data-v-a5bdef7e] .ant-pagination .ant-pagination-next .ant-pagination-item-link:hover,.trading-records[data-v-a5bdef7e] .ant-pagination .ant-pagination-prev .ant-pagination-item-link:hover{border-color:#1890ff;color:#1890ff}
|
||||
.ai-markdown-content[data-v-9c4d8af2] h3{border-left:3px solid #1890ff}
|
||||
.ai-markdown-content[data-v-9c4d8af2] blockquote{border-left:4px solid #91d5ff}
|
||||
.backtest-history-drawer-wrap--dark .drawer-toolbar .ant-btn-primary.ant-btn-background-ghost{color:#69c0ff}
|
||||
.backtest-history-drawer-wrap--dark .drawer-toolbar .ant-btn-primary.ant-btn-background-ghost:focus:not(:disabled),.backtest-history-drawer-wrap--dark .drawer-toolbar .ant-btn-primary.ant-btn-background-ghost:hover:not(:disabled){color:#91d5ff}
|
||||
.page-header .page-title .title-icon[data-v-1daf6ac6]{-webkit-text-fill-color:#1890ff}
|
||||
[data-v-1daf6ac6] .empty-result .empty-icon{background:linear-gradient(135deg,#e6f7ff,#f0f5ff);color:#1890ff}
|
||||
[data-v-1daf6ac6] .running-spinner .spinner-fill{border-top-color:#1890ff}
|
||||
[data-v-1daf6ac6] .running-elapsed{color:#1890ff}
|
||||
[data-v-1daf6ac6] .running-steps .step-item.active{background:#e6f7ff;border-color:#91d5ff}
|
||||
[data-v-1daf6ac6] .running-steps .step-item.active .step-icon{color:#1890ff}
|
||||
[data-v-1daf6ac6] .running-steps .step-item.active .step-label{color:#1890ff}
|
||||
[data-v-1daf6ac6] .result-ai-markdown-content/deep/h3{border-left:3px solid #1890ff}
|
||||
[data-v-1daf6ac6] .result-ai-markdown-content/deep/blockquote{border-left:3px solid #91d5ff}
|
||||
.add-item-active[data-v-1daf6ac6]{background:#e6f7ff!important}
|
||||
.theme-dark .page-header .page-title .title-icon[data-v-1daf6ac6]{color:#40a9ff!important}
|
||||
.trading-records[data-v-bdf54cd6] .ant-tag[color=blue]{background:linear-gradient(135deg,rgba(24,144,255,.15),rgba(24,144,255,.08));color:#1890ff;border:1px solid rgba(24,144,255,.3)}
|
||||
.trading-records[data-v-bdf54cd6] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
|
||||
.trading-records[data-v-bdf54cd6] .ant-pagination .ant-pagination-item:hover a{color:#1890ff}
|
||||
.trading-records[data-v-bdf54cd6] .ant-pagination .ant-pagination-item.ant-pagination-item-active{background:linear-gradient(135deg,#1890ff,#40a9ff);border-color:#1890ff}
|
||||
.trading-records[data-v-bdf54cd6] .ant-pagination .ant-pagination-next .ant-pagination-item-link:hover,.trading-records[data-v-bdf54cd6] .ant-pagination .ant-pagination-prev .ant-pagination-item-link:hover{border-color:#1890ff;color:#1890ff}
|
||||
.theme-dark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover,body.dark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover,body.realdark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover{border-color:#1890ff!important}
|
||||
.theme-dark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover a,body.dark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover a,body.realdark .trading-records[data-v-8a68b65a] .ant-pagination-item:hover a{color:#1890ff!important}
|
||||
.theme-dark .trading-records[data-v-8a68b65a] .ant-pagination-item-active,body.dark .trading-records[data-v-8a68b65a] .ant-pagination-item-active,body.realdark .trading-records[data-v-8a68b65a] .ant-pagination-item-active{background:#1890ff!important;border-color:#1890ff!important}
|
||||
@@ -130,70 +132,77 @@ body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-next:hover .
|
||||
.theme-dark .trading-records * ::v-deep .ant-pagination .ant-pagination-item:hover a,.theme-dark .trading-records ::v-deep .ant-pagination .ant-pagination-item:hover a,body.dark .trading-records * ::v-deep .ant-pagination .ant-pagination-item:hover a,body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-item:hover a,body.realdark .trading-records * ::v-deep .ant-pagination .ant-pagination-item:hover a,body.realdark .trading-records ::v-deep .ant-pagination .ant-pagination-item:hover a{color:#1890ff!important}
|
||||
.theme-dark .trading-records * ::v-deep .ant-pagination .ant-pagination-item-active,.theme-dark .trading-records ::v-deep .ant-pagination .ant-pagination-item-active,body.dark .trading-records * ::v-deep .ant-pagination .ant-pagination-item-active,body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-item-active,body.realdark .trading-records * ::v-deep .ant-pagination .ant-pagination-item-active,body.realdark .trading-records ::v-deep .ant-pagination .ant-pagination-item-active{background:#1890ff!important;border-color:#1890ff!important}
|
||||
.theme-dark .trading-records * ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,.theme-dark .trading-records * ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link,.theme-dark .trading-records ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,.theme-dark .trading-records ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link,body.dark .trading-records * ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,body.dark .trading-records * ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link,body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,body.dark .trading-records ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link,body.realdark .trading-records * ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,body.realdark .trading-records * ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link,body.realdark .trading-records ::v-deep .ant-pagination .ant-pagination-next:hover .ant-pagination-item-link,body.realdark .trading-records ::v-deep .ant-pagination .ant-pagination-prev:hover .ant-pagination-item-link{border-color:#1890ff!important;color:#1890ff!important}
|
||||
.mode-card[data-v-1cfbbafc]:hover{border-color:#1890ff;-webkit-box-shadow:0 4px 16px rgba(24,144,255,.12);box-shadow:0 4px 16px rgba(24,144,255,.12)}
|
||||
.mode-card.selected[data-v-1cfbbafc]{border-color:#1890ff;background:rgba(24,144,255,.02)}
|
||||
.mode-card.selected[data-v-1cfbbafc]:after{color:#1890ff}
|
||||
.card-icon.signal-icon[data-v-1cfbbafc]{background:linear-gradient(135deg,#e6f7ff,#bae7ff);color:#1890ff}
|
||||
.card-body .card-badge[data-v-1cfbbafc]{background:rgba(24,144,255,.08);color:#1890ff}
|
||||
.template-tag[data-v-1cfbbafc]:hover{color:#1890ff;border-color:#1890ff;background:#e6f7ff}
|
||||
.theme-dark .mode-card.selected[data-v-1cfbbafc]:after{color:#40a9ff}
|
||||
.theme-dark .card-icon.signal-icon[data-v-1cfbbafc]{background:linear-gradient(135deg,rgba(24,144,255,.15),rgba(24,144,255,.08));color:#40a9ff}
|
||||
.theme-dark .card-body .card-badge[data-v-1cfbbafc]{color:#69c0ff}
|
||||
.theme-dark .card-btn[data-v-1cfbbafc]{border-color:rgba(24,144,255,.4);color:#40a9ff}
|
||||
.theme-dark .card-btn[data-v-1cfbbafc]:hover{border-color:#1890ff;color:#1890ff}
|
||||
.theme-dark .template-tag[data-v-1cfbbafc]:hover{color:#40a9ff}
|
||||
.code-editor-container[data-v-6c7c8660] .CodeMirror-cursor{border-left:2px solid #1890ff}
|
||||
.template-item.active[data-v-6c7c8660],.template-item[data-v-6c7c8660]:hover{border-color:#1890ff}
|
||||
.ai-status[data-v-6c7c8660]{color:#1890ff}
|
||||
.docs-content[data-v-6c7c8660] h5{color:#1890ff}
|
||||
.theme-dark .side-tabs[data-v-6c7c8660] .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#1890ff}
|
||||
.theme-dark .ai-status[data-v-6c7c8660]{color:#40a9ff}
|
||||
.theme-dark .docs-content[data-v-6c7c8660] h5{color:#40a9ff}
|
||||
.theme-dark .logs-toolbar[data-v-129fed65] .ant-radio-group .ant-radio-button-wrapper:hover{color:#40a9ff}
|
||||
.theme-dark .logs-toolbar[data-v-129fed65] .ant-radio-group .ant-radio-button-wrapper.ant-radio-button-wrapper-checked{background:#1890ff;border-color:#1890ff}
|
||||
.assistant-guide-bar[data-v-bbcac474]{border:1px solid rgba(24,144,255,.14);background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(114,46,209,.06))}
|
||||
.assistant-guide-bar .assistant-guide-eyebrow[data-v-bbcac474]{background:rgba(24,144,255,.12)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .card-title .ant-btn-primary[data-v-bbcac474]{background:linear-gradient(135deg,#1890ff,#40a9ff);-webkit-box-shadow:0 4px 12px rgba(24,144,255,.35);box-shadow:0 4px 12px rgba(24,144,255,.35)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .card-title .ant-btn-primary[data-v-bbcac474]:hover{-webkit-box-shadow:0 6px 16px rgba(24,144,255,.45);box-shadow:0 6px 16px rgba(24,144,255,.45)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-header .group-header-left .group-icon[data-v-bbcac474]{color:#1890ff}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-content .strategy-list-item[data-v-bbcac474]:hover{border-left-color:#1890ff}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-content .strategy-list-item.active[data-v-bbcac474]{border-left-color:#1890ff}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-list-item[data-v-bbcac474]:hover{border-color:rgba(24,144,255,.2);-webkit-box-shadow:0 2px 12px rgba(24,144,255,.1);box-shadow:0 2px 12px rgba(24,144,255,.1)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-list-item.active[data-v-bbcac474]{border-color:#1890ff;border-left:4px solid #1890ff;-webkit-box-shadow:0 4px 16px rgba(24,144,255,.15);box-shadow:0 4px 16px rgba(24,144,255,.15)}
|
||||
.trading-assistant .strategy-detail-col .empty-detail[data-v-bbcac474]:hover{border-color:#1890ff;background:linear-gradient(135deg,rgba(24,144,255,.02),rgba(24,144,255,.05))}
|
||||
.trading-assistant .strategy-detail-col .strategy-detail-panel .strategy-header-card .strategy-header .header-left .strategy-tags .tag-item .anticon[data-v-bbcac474]{color:#1890ff}
|
||||
.trading-assistant.theme-dark .creation-mode-toggle[data-v-bbcac474]{background:rgba(24,144,255,.08);border-color:rgba(24,144,255,.2)}
|
||||
.trading-assistant.theme-dark .strategy-list-col .strategy-list-item[data-v-bbcac474]:hover{background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(24,144,255,.04));border-color:rgba(24,144,255,.2)}
|
||||
.trading-assistant.theme-dark .strategy-list-col .strategy-list-item.active[data-v-bbcac474]{background:linear-gradient(135deg,rgba(24,144,255,.12),rgba(24,144,255,.06));border-color:#1890ff;-webkit-box-shadow:0 4px 20px rgba(24,144,255,.2);box-shadow:0 4px 20px rgba(24,144,255,.2)}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-header-card .strategy-tags .tag-item[data-v-bbcac474]:hover{background:rgba(24,144,255,.1);border-color:rgba(24,144,255,.3)}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-content-card[data-v-bbcac474] .ant-tabs .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#1890ff}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-content-card[data-v-bbcac474] .ant-tabs .ant-tabs-nav .ant-tabs-ink-bar{background:-webkit-gradient(linear,left top,right top,from(#1890ff),to(#40a9ff));background:linear-gradient(90deg,#1890ff,#40a9ff)}
|
||||
.theme-dark .assistant-guide-bar .assistant-guide-eyebrow[data-v-bbcac474]{color:#69c0ff}
|
||||
.ai-filter-title .anticon[data-v-bbcac474]{color:#1890ff}
|
||||
.creation-mode-toggle[data-v-bbcac474]{background:rgba(24,144,255,.04);border:1px solid rgba(24,144,255,.12)}
|
||||
.simple-mode-hero[data-v-bbcac474]{background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(114,46,209,.06));border:1px solid rgba(24,144,255,.14)}
|
||||
.simple-mode-kicker[data-v-bbcac474]{color:#1890ff}
|
||||
.advanced-settings-shell[data-v-bbcac474],.selected-indicator-card[data-v-bbcac474],.simple-essentials-card[data-v-bbcac474]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-step-hero[data-v-bbcac474]{background:linear-gradient(135deg,rgba(82,196,26,.08),rgba(24,144,255,.08));border:1px solid rgba(24,144,255,.14)}
|
||||
.execution-section-card[data-v-bbcac474]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-mode-card[data-v-bbcac474]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-mode-card[data-v-bbcac474]:hover{border-color:rgba(24,144,255,.35);-webkit-box-shadow:0 8px 24px rgba(24,144,255,.08);box-shadow:0 8px 24px rgba(24,144,255,.08)}
|
||||
.execution-mode-card.active[data-v-bbcac474]{border-color:#1890ff;background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(24,144,255,.03));-webkit-box-shadow:0 10px 28px rgba(24,144,255,.12);box-shadow:0 10px 28px rgba(24,144,255,.12)}
|
||||
.execution-mode-card.disabled[data-v-bbcac474]:hover{border-color:rgba(24,144,255,.12)}
|
||||
.execution-mode-card-icon.signal[data-v-bbcac474]{color:#1890ff;background:rgba(24,144,255,.1)}
|
||||
.execution-mode-card-check[data-v-bbcac474]{color:#1890ff}
|
||||
.strategy-type-selector .strategy-type-card[data-v-bbcac474]:hover{border-color:#1890ff;-webkit-box-shadow:0 2px 8px rgba(24,144,255,.2);box-shadow:0 2px 8px rgba(24,144,255,.2)}
|
||||
.strategy-type-selector .strategy-type-card.selected[data-v-bbcac474]{border-color:#1890ff;background-color:#e6f7ff;-webkit-box-shadow:0 2px 8px rgba(24,144,255,.3);box-shadow:0 2px 8px rgba(24,144,255,.3)}
|
||||
.strategy-type-selector .strategy-type-card .strategy-type-content .strategy-type-icon[data-v-bbcac474]{color:#1890ff}
|
||||
.theme-dark .simple-mode-hero[data-v-bbcac474]{background:linear-gradient(135deg,rgba(24,144,255,.14),rgba(114,46,209,.12))}
|
||||
.theme-dark .execution-step-hero[data-v-bbcac474]{background:linear-gradient(135deg,rgba(82,196,26,.14),rgba(24,144,255,.14))}
|
||||
.theme-dark .execution-mode-card[data-v-bbcac474]:hover{-webkit-box-shadow:0 8px 24px rgba(24,144,255,.12);box-shadow:0 8px 24px rgba(24,144,255,.12)}
|
||||
.theme-dark .execution-mode-card.active[data-v-bbcac474]{background:linear-gradient(135deg,rgba(24,144,255,.16),rgba(24,144,255,.08));border-color:#1890ff}
|
||||
.theme-dark .simple-mode-kicker[data-v-bbcac474]{color:#69c0ff}
|
||||
.ip-whitelist-tip[data-v-bbcac474]{background-color:#e6f7ff;border:1px solid #91d5ff;color:#1890ff}
|
||||
.ip-whitelist-tip .anticon[data-v-bbcac474]{color:#1890ff}
|
||||
.mode-card[data-v-8ec7ab14]:hover{border-color:#1890ff;-webkit-box-shadow:0 4px 16px rgba(24,144,255,.12);box-shadow:0 4px 16px rgba(24,144,255,.12)}
|
||||
.mode-card.selected[data-v-8ec7ab14]{border-color:#1890ff;background:rgba(24,144,255,.02)}
|
||||
.mode-card.selected[data-v-8ec7ab14]:after{color:#1890ff}
|
||||
.card-icon.signal-icon[data-v-8ec7ab14]{background:linear-gradient(135deg,#e6f7ff,#bae7ff);color:#1890ff}
|
||||
.card-body .card-badge[data-v-8ec7ab14]{background:rgba(24,144,255,.08);color:#1890ff}
|
||||
.template-tag[data-v-8ec7ab14]:hover{color:#1890ff;border-color:#1890ff;background:#e6f7ff}
|
||||
.theme-dark .mode-card.selected[data-v-8ec7ab14]:after{color:#40a9ff}
|
||||
.theme-dark .card-icon.signal-icon[data-v-8ec7ab14]{background:linear-gradient(135deg,rgba(24,144,255,.15),rgba(24,144,255,.08));color:#40a9ff}
|
||||
.theme-dark .card-body .card-badge[data-v-8ec7ab14]{color:#69c0ff}
|
||||
.theme-dark .card-btn[data-v-8ec7ab14]{border-color:rgba(24,144,255,.4);color:#40a9ff}
|
||||
.theme-dark .card-btn[data-v-8ec7ab14]:hover{border-color:#1890ff;color:#1890ff}
|
||||
.theme-dark .template-tag[data-v-8ec7ab14]:hover{color:#40a9ff}
|
||||
.code-editor-container[data-v-35a0a386] .CodeMirror-cursor{border-left:2px solid #1890ff}
|
||||
.template-item.active[data-v-35a0a386],.template-item[data-v-35a0a386]:hover{border-color:#1890ff}
|
||||
.ai-status[data-v-35a0a386]{color:#1890ff}
|
||||
.theme-dark .side-tabs[data-v-35a0a386] .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#1890ff}
|
||||
.theme-dark .ai-status[data-v-35a0a386]{color:#40a9ff}
|
||||
.theme-dark[data-v-35a0a386] .ant-alert-info{background:rgba(24,144,255,.08);border-color:rgba(24,144,255,.2)}
|
||||
.theme-dark .logs-toolbar[data-v-73589ce4] .ant-radio-group .ant-radio-button-wrapper:hover{color:#40a9ff}
|
||||
.theme-dark .logs-toolbar[data-v-73589ce4] .ant-radio-group .ant-radio-button-wrapper.ant-radio-button-wrapper-checked{background:#1890ff;border-color:#1890ff}
|
||||
.assistant-guide-bar[data-v-533cb131]{border:1px solid rgba(24,144,255,.14);background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(114,46,209,.06))}
|
||||
.assistant-guide-bar .assistant-guide-eyebrow[data-v-533cb131]{background:rgba(24,144,255,.12)}
|
||||
.assistant-guide-bar .assistant-guide-actions .assistant-guide-close[data-v-533cb131]:focus,.assistant-guide-bar .assistant-guide-actions .assistant-guide-close[data-v-533cb131]:hover{border-color:rgba(24,144,255,.35)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .card-title .ant-btn-primary[data-v-533cb131]{background:linear-gradient(135deg,#1890ff,#40a9ff);-webkit-box-shadow:0 4px 12px rgba(24,144,255,.35);box-shadow:0 4px 12px rgba(24,144,255,.35)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .card-title .ant-btn-primary[data-v-533cb131]:hover{-webkit-box-shadow:0 6px 16px rgba(24,144,255,.45);box-shadow:0 6px 16px rgba(24,144,255,.45)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-header .group-header-left .group-icon[data-v-533cb131]{color:#1890ff}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-content .strategy-list-item[data-v-533cb131]:hover{border-left-color:#1890ff;border-color:rgba(24,144,255,.22);-webkit-box-shadow:0 2px 8px rgba(24,144,255,.08);box-shadow:0 2px 8px rgba(24,144,255,.08)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-grouped-list .strategy-group .strategy-group-content .strategy-list-item.active[data-v-533cb131]{border-color:rgba(24,144,255,.35);border-left-color:#1890ff;-webkit-box-shadow:0 2px 12px rgba(24,144,255,.12);box-shadow:0 2px 12px rgba(24,144,255,.12)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-list-item[data-v-533cb131]:hover{border-color:rgba(24,144,255,.2);-webkit-box-shadow:0 2px 12px rgba(24,144,255,.1);box-shadow:0 2px 12px rgba(24,144,255,.1)}
|
||||
.trading-assistant .strategy-list-col .strategy-list-card .strategy-list-item.active[data-v-533cb131]{border-color:#1890ff;border-left:4px solid #1890ff;-webkit-box-shadow:0 4px 16px rgba(24,144,255,.15);box-shadow:0 4px 16px rgba(24,144,255,.15)}
|
||||
.trading-assistant .strategy-detail-col .strategy-empty-detail-icon[data-v-533cb131]{background:linear-gradient(135deg,rgba(24,144,255,.12),rgba(64,169,255,.08));color:#1890ff}
|
||||
.trading-assistant .strategy-detail-col .strategy-detail-panel .strategy-header-card .strategy-header .header-left .strategy-tags .tag-item .anticon[data-v-533cb131]{color:#1890ff}
|
||||
.trading-assistant.theme-dark .creation-mode-toggle[data-v-533cb131]{background:rgba(24,144,255,.08);border-color:rgba(24,144,255,.2)}
|
||||
.trading-assistant.theme-dark .strategy-list-col .group-mode-switch[data-v-533cb131] .ant-radio-button-wrapper:hover{color:#69c0ff}
|
||||
.trading-assistant.theme-dark .strategy-list-col .group-mode-switch[data-v-533cb131] .ant-radio-button-wrapper-checked{background:rgba(24,144,255,.25)!important;border-color:#1890ff!important}
|
||||
.trading-assistant.theme-dark .strategy-list-col .strategy-group-content .strategy-list-item[data-v-533cb131]:hover{border-left-color:#1890ff}
|
||||
.trading-assistant.theme-dark .strategy-list-col .strategy-group-content .strategy-list-item.active[data-v-533cb131]{background:rgba(24,144,255,.12);border-color:rgba(24,144,255,.28);border-left-color:#1890ff}
|
||||
.trading-assistant.theme-dark .strategy-list-col .strategy-list-item.active[data-v-533cb131]{background:rgba(24,144,255,.1);border-color:#1890ff;-webkit-box-shadow:0 4px 20px rgba(24,144,255,.2);box-shadow:0 4px 20px rgba(24,144,255,.2)}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-header-card .strategy-tags .tag-item[data-v-533cb131]:hover{background:rgba(24,144,255,.1);border-color:rgba(24,144,255,.3)}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-content-card[data-v-533cb131] .ant-tabs .ant-tabs-nav .ant-tabs-tab.ant-tabs-tab-active .ant-tabs-tab-btn{color:#1890ff}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-content-card[data-v-533cb131] .ant-tabs .ant-tabs-nav .ant-tabs-ink-bar{background:-webkit-gradient(linear,left top,right top,from(#1890ff),to(#40a9ff));background:linear-gradient(90deg,#1890ff,#40a9ff)}
|
||||
.trading-assistant.theme-dark .strategy-detail-col .strategy-empty-detail-icon[data-v-533cb131]{background:linear-gradient(135deg,rgba(24,144,255,.22),rgba(64,169,255,.1));color:#69c0ff}
|
||||
.theme-dark .assistant-guide-bar .assistant-guide-eyebrow[data-v-533cb131]{color:#69c0ff}
|
||||
.theme-dark .assistant-guide-bar .assistant-guide-close[data-v-533cb131]:focus,.theme-dark .assistant-guide-bar .assistant-guide-close[data-v-533cb131]:hover{color:#91d5ff}
|
||||
.ai-filter-title .anticon[data-v-533cb131]{color:#1890ff}
|
||||
.creation-mode-toggle[data-v-533cb131]{background:rgba(24,144,255,.04);border:1px solid rgba(24,144,255,.12)}
|
||||
.simple-mode-hero[data-v-533cb131]{background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(114,46,209,.06));border:1px solid rgba(24,144,255,.14)}
|
||||
.simple-mode-kicker[data-v-533cb131]{color:#1890ff}
|
||||
.advanced-settings-shell[data-v-533cb131],.selected-indicator-card[data-v-533cb131],.simple-essentials-card[data-v-533cb131]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-step-hero[data-v-533cb131]{background:linear-gradient(135deg,rgba(82,196,26,.08),rgba(24,144,255,.08));border:1px solid rgba(24,144,255,.14)}
|
||||
.execution-section-card[data-v-533cb131]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-mode-card[data-v-533cb131]{border:1px solid rgba(24,144,255,.12)}
|
||||
.execution-mode-card[data-v-533cb131]:hover{border-color:rgba(24,144,255,.35);-webkit-box-shadow:0 8px 24px rgba(24,144,255,.08);box-shadow:0 8px 24px rgba(24,144,255,.08)}
|
||||
.execution-mode-card.active[data-v-533cb131]{border-color:#1890ff;background:linear-gradient(135deg,rgba(24,144,255,.08),rgba(24,144,255,.03));-webkit-box-shadow:0 10px 28px rgba(24,144,255,.12);box-shadow:0 10px 28px rgba(24,144,255,.12)}
|
||||
.execution-mode-card.disabled[data-v-533cb131]:hover{border-color:rgba(24,144,255,.12)}
|
||||
.execution-mode-card-icon.signal[data-v-533cb131]{color:#1890ff;background:rgba(24,144,255,.1)}
|
||||
.execution-mode-card-check[data-v-533cb131]{color:#1890ff}
|
||||
.strategy-type-selector .strategy-type-card[data-v-533cb131]:hover{border-color:#1890ff;-webkit-box-shadow:0 2px 8px rgba(24,144,255,.2);box-shadow:0 2px 8px rgba(24,144,255,.2)}
|
||||
.strategy-type-selector .strategy-type-card.selected[data-v-533cb131]{border-color:#1890ff;background-color:#e6f7ff;-webkit-box-shadow:0 2px 8px rgba(24,144,255,.3);box-shadow:0 2px 8px rgba(24,144,255,.3)}
|
||||
.strategy-type-selector .strategy-type-card .strategy-type-content .strategy-type-icon[data-v-533cb131]{color:#1890ff}
|
||||
.theme-dark .simple-mode-hero[data-v-533cb131]{background:linear-gradient(135deg,rgba(24,144,255,.14),rgba(114,46,209,.12))}
|
||||
.theme-dark .execution-step-hero[data-v-533cb131]{background:linear-gradient(135deg,rgba(82,196,26,.14),rgba(24,144,255,.14))}
|
||||
.theme-dark .execution-mode-card[data-v-533cb131]:hover{-webkit-box-shadow:0 8px 24px rgba(24,144,255,.12);box-shadow:0 8px 24px rgba(24,144,255,.12)}
|
||||
.theme-dark .execution-mode-card.active[data-v-533cb131]{background:linear-gradient(135deg,rgba(24,144,255,.16),rgba(24,144,255,.08));border-color:#1890ff}
|
||||
.theme-dark .simple-mode-kicker[data-v-533cb131]{color:#69c0ff}
|
||||
.ip-whitelist-tip[data-v-533cb131]{background-color:#e6f7ff;border:1px solid #91d5ff;color:#1890ff}
|
||||
.ip-whitelist-tip .anticon[data-v-533cb131]{color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-modal-content .creation-mode-toggle{background:rgba(24,144,255,.08);border-color:rgba(24,144,255,.2)}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-steps .ant-steps-item-finish .ant-steps-item-icon,body.dark .strategy-form-modal.strategy-form-modal-dark .ant-steps .ant-steps-item-process .ant-steps-item-icon{border-color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-steps .ant-steps-item-process .ant-steps-item-icon{background:#1890ff;border-color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-steps .ant-steps-item-finish .ant-steps-item-icon{border-color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-steps .ant-steps-item-finish .ant-steps-item-icon .ant-steps-icon{color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-input-number:focus,body.dark .strategy-form-modal.strategy-form-modal-dark .ant-input-number:hover,body.dark .strategy-form-modal.strategy-form-modal-dark .ant-input:focus,body.dark .strategy-form-modal.strategy-form-modal-dark .ant-input:hover{border-color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-input-number-handler-wrap .ant-input-number-handler:hover{color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ant-select .ant-select-selection:hover{border-color:#1890ff}
|
||||
@@ -203,6 +212,10 @@ body.dark .strategy-form-modal.strategy-form-modal-dark .ant-switch.ant-switch-c
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .simple-mode-hero{background:linear-gradient(135deg,rgba(24,144,255,.16),rgba(114,46,209,.14))}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .execution-step-hero{background:linear-gradient(135deg,rgba(82,196,26,.16),rgba(24,144,255,.14))}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .ip-whitelist-tip{background:rgba(24,144,255,.08);border-color:rgba(24,144,255,.2);color:#40a9ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .execution-mode-card:hover{-webkit-box-shadow:0 8px 24px rgba(24,144,255,.12);box-shadow:0 8px 24px rgba(24,144,255,.12)}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .execution-mode-card.active{background:linear-gradient(135deg,rgba(24,144,255,.16),rgba(24,144,255,.08));border-color:#1890ff}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .execution-mode-card-icon.signal{color:#69c0ff;background:rgba(24,144,255,.15)}
|
||||
body.dark .strategy-form-modal.strategy-form-modal-dark .simple-mode-kicker{color:#69c0ff}
|
||||
body.dark .ant-select-dropdown .ant-select-dropdown-menu-item-active,body.dark .ant-select-dropdown .ant-select-dropdown-menu-item:hover{background:rgba(24,144,255,.1)}
|
||||
body.dark .ant-select-dropdown .ant-select-dropdown-menu-item-selected{background:rgba(24,144,255,.15);color:#1890ff}
|
||||
body.dark .ant-modal-wrap .ant-alert{background:rgba(24,144,255,.06);border-color:rgba(24,144,255,.2)}
|
||||
@@ -297,25 +310,22 @@ body.dark .ant-modal-wrap .ant-alert{background:rgba(24,144,255,.06);border-colo
|
||||
.theme-dark .position-checkbox-item[data-v-398d904c]:hover{background:rgba(24,144,255,.1)}
|
||||
.theme-dark .alert-symbol-info .current-price-info[data-v-398d904c]{background:linear-gradient(135deg,rgba(24,144,255,.15),rgba(114,46,209,.1))}
|
||||
.theme-dark .alert-symbol-info .current-price-info .price[data-v-398d904c]{color:#40a9ff}
|
||||
.profile-page .page-header .page-title .anticon[data-v-06c79ed5]{color:#1890ff}
|
||||
.profile-page .profile-card .profile-info .info-item .anticon[data-v-06c79ed5]{color:#1890ff}
|
||||
.profile-page.theme-dark .edit-card[data-v-06c79ed5] .ant-tabs-tab-active{color:#1890ff}
|
||||
.profile-page.theme-dark .edit-card[data-v-06c79ed5] .ant-input-password:focus,.profile-page.theme-dark .edit-card[data-v-06c79ed5] .ant-input-password:hover,.profile-page.theme-dark .edit-card[data-v-06c79ed5] .ant-input:focus,.profile-page.theme-dark .edit-card[data-v-06c79ed5] .ant-input:hover{border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-06c79ed5] .ant-table-wrapper .ant-tag-blue{background:rgba(24,144,255,.25);border-color:#1890ff;color:#69c0ff}
|
||||
.profile-page.theme-dark .notification-settings-form[data-v-06c79ed5] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .notification-settings-form[data-v-06c79ed5] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .password-form[data-v-06c79ed5] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .password-form[data-v-06c79ed5] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .profile-form[data-v-06c79ed5] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .profile-form[data-v-06c79ed5] .ant-radio-checked .ant-radio-inner{background-color:#1890ff;border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-06c79ed5] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-06c79ed5] .ant-pagination .ant-pagination-item-active{background:#1890ff;border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-06c79ed5] .ant-btn.ant-btn-default:hover{border-color:#1890ff;color:#1890ff}
|
||||
.profile-exchange-modal .exchange-account-form .exchange-api-doc-callout{border:1px solid rgba(24,144,255,.18);background:-webkit-gradient(linear,left top,right top,from(rgba(24,144,255,.06)),to(rgba(82,196,26,.04)));background:linear-gradient(90deg,rgba(24,144,255,.06),rgba(82,196,26,.04))}
|
||||
.profile-exchange-modal .exchange-account-form .exchange-api-doc-callout__icon{background:rgba(24,144,255,.12);color:#1890ff}
|
||||
.profile-exchange-modal--dark .exchange-account-form .exchange-api-doc-callout{background:-webkit-gradient(linear,left top,right top,from(rgba(24,144,255,.12)),to(rgba(82,196,26,.08)));background:linear-gradient(90deg,rgba(24,144,255,.12),rgba(82,196,26,.08))}
|
||||
.profile-page .page-header .page-title .anticon[data-v-2151383b]{color:#1890ff}
|
||||
.profile-page .profile-card .profile-info .info-item .anticon[data-v-2151383b]{color:#1890ff}
|
||||
.profile-page.theme-dark .edit-card[data-v-2151383b] .ant-tabs-tab-active{color:#1890ff}
|
||||
.profile-page.theme-dark .edit-card[data-v-2151383b] .ant-input-password:focus,.profile-page.theme-dark .edit-card[data-v-2151383b] .ant-input-password:hover,.profile-page.theme-dark .edit-card[data-v-2151383b] .ant-input:focus,.profile-page.theme-dark .edit-card[data-v-2151383b] .ant-input:hover{border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-2151383b] .ant-table-wrapper .ant-tag-blue{background:rgba(24,144,255,.25);border-color:#1890ff;color:#69c0ff}
|
||||
.profile-page.theme-dark .notification-settings-form[data-v-2151383b] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .notification-settings-form[data-v-2151383b] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .password-form[data-v-2151383b] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .password-form[data-v-2151383b] .ant-radio-checked .ant-radio-inner,.profile-page.theme-dark .profile-form[data-v-2151383b] .ant-checkbox-checked .ant-checkbox-inner,.profile-page.theme-dark .profile-form[data-v-2151383b] .ant-radio-checked .ant-radio-inner{background-color:#1890ff;border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-2151383b] .ant-pagination .ant-pagination-item:hover{border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-2151383b] .ant-pagination .ant-pagination-item-active{background:#1890ff;border-color:#1890ff}
|
||||
.profile-page.theme-dark[data-v-2151383b] .ant-btn.ant-btn-default:hover{border-color:#1890ff;color:#1890ff}
|
||||
.billing-page .plan-card.highlight[data-v-714c882a]{border:1px solid rgba(24,144,255,.35)}
|
||||
.usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,.usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover{background:rgba(24,144,255,.08);color:#1890ff}
|
||||
body.dark .usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,body.dark .usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover,body.realdark .usdt-checkout .checkout-body .info-section .addr-box .copy-btn:hover,body.realdark .usdt-checkout .checkout-body .info-section .amt-box .copy-btn:hover{background:rgba(24,144,255,.15);color:#40a9ff}
|
||||
.user-manage-page .page-header .page-title .anticon[data-v-4e5dc5e0]{color:#1890ff}
|
||||
.user-manage-page .address-text[data-v-4e5dc5e0],.user-manage-page .hash-text[data-v-4e5dc5e0]{color:#1890ff}
|
||||
.user-manage-page.theme-dark .manage-tabs[data-v-4e5dc5e0] .ant-tabs-tab-active{color:#1890ff}
|
||||
.user-manage-page .current-credits-info .value[data-v-4e5dc5e0],.user-manage-page .current-vip-info .value[data-v-4e5dc5e0]{color:#1890ff}
|
||||
.user-manage-page .page-header .page-title .anticon[data-v-7a022132]{color:#1890ff}
|
||||
.user-manage-page .address-text[data-v-7a022132],.user-manage-page .hash-text[data-v-7a022132]{color:#1890ff}
|
||||
.user-manage-page.theme-dark .manage-tabs[data-v-7a022132] .ant-tabs-tab-active{color:#1890ff}
|
||||
.user-manage-page .current-credits-info .value[data-v-7a022132],.user-manage-page .current-vip-info .value[data-v-7a022132]{color:#1890ff}
|
||||
.settings-page .settings-header .page-title .anticon[data-v-15e1ba02]{color:#1890ff}
|
||||
.settings-page .openrouter-balance-card .ant-card[data-v-15e1ba02]{background:linear-gradient(135deg,#e6f7ff,#f0f5ff);border:1px solid #91d5ff}
|
||||
.settings-page .openrouter-balance-card .balance-header .balance-title[data-v-15e1ba02]{color:#1890ff}
|
||||
Vendored
+1
-1
@@ -421,4 +421,4 @@
|
||||
.brand-text {
|
||||
font-size: 20px;
|
||||
}
|
||||
}</style><script defer="defer" src="/js/chunk-vendors.ebabb02b.js" type="module"></script><script defer="defer" src="/js/app.5b7d4d9e.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.9dca4aef.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.f624f594.js" nomodule></script><script defer="defer" src="/js/app-legacy.5c46ff8f.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
|
||||
}</style><script defer="defer" src="/js/chunk-vendors.7a650f0b.js" type="module"></script><script defer="defer" src="/js/app.1b0410d5.js" type="module"></script><link href="/css/chunk-vendors.b8cb9e53.css" rel="stylesheet"><link href="/css/app.9dca4aef.css" rel="stylesheet"><script defer="defer" src="/js/chunk-vendors-legacy.e8633248.js" nomodule></script><script defer="defer" src="/js/app-legacy.abf8ca7f.js" nomodule></script></head><body><noscript><strong>We're sorry but vue-antd-pro doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"><div class="first-loading-wrp"><h2>Landing</h2><div class="loading-wrp"><div class="pixel-cat-container"><div class="ground"></div><div class="pixel-cat"><div class="cat-head"><div class="cat-ear-left"></div><div class="cat-ear-right"></div><div class="cat-eye-left"></div><div class="cat-eye-right"></div><div class="cat-nose"></div><div class="cat-whiskers"></div></div><div class="cat-body"></div><div class="cat-leg-front-left"></div><div class="cat-leg-front-right"></div><div class="cat-leg-back-left"></div><div class="cat-leg-back-right"></div><div class="cat-tail"></div></div></div></div><div class="brand-text">QuantDinger</div></div></div></body></html>
|
||||
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user