中控增加条件单委托
This commit is contained in:
@@ -35,6 +35,8 @@ from exchange_orders import (
|
|||||||
cancel_order as hub_cancel_order,
|
cancel_order as hub_cancel_order,
|
||||||
cancel_orders_for_symbol,
|
cancel_orders_for_symbol,
|
||||||
list_open_orders,
|
list_open_orders,
|
||||||
|
replace_position_tpsl,
|
||||||
|
symbols_match,
|
||||||
)
|
)
|
||||||
|
|
||||||
HOST = os.getenv("HOST", "127.0.0.1")
|
HOST = os.getenv("HOST", "127.0.0.1")
|
||||||
@@ -299,6 +301,14 @@ class CancelSymbolOrdersBody(BaseModel):
|
|||||||
scope: str = "all" # all | conditional | limit
|
scope: str = "all" # all | conditional | limit
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceTpslBody(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
side: str # long | short
|
||||||
|
stop_loss: float
|
||||||
|
take_profit: float
|
||||||
|
contracts: float | None = None
|
||||||
|
|
||||||
|
|
||||||
def _close_position_market(
|
def _close_position_market(
|
||||||
ex: Any, sym: str, side: str, contracts: float
|
ex: Any, sym: str, side: str, contracts: float
|
||||||
) -> tuple[dict[str, Any] | None, str | None]:
|
) -> tuple[dict[str, Any] | None, str | None]:
|
||||||
@@ -629,6 +639,58 @@ def cancel_symbol_orders(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/orders/place-tpsl")
|
||||||
|
def place_tpsl_orders(
|
||||||
|
body: PlaceTpslBody,
|
||||||
|
x_control_token: str | None = Header(default=None, alias="X-Control-Token"),
|
||||||
|
):
|
||||||
|
"""先撤该合约全部条件单,再挂止盈+止损(与四实例策略逻辑一致)。"""
|
||||||
|
_check_token(x_control_token)
|
||||||
|
sym = (body.symbol or "").strip()
|
||||||
|
side = (body.side or "").strip().lower()
|
||||||
|
if not sym or side not in ("long", "short"):
|
||||||
|
raise HTTPException(status_code=400, detail="symbol 与 side(long/short) 必填")
|
||||||
|
try:
|
||||||
|
sl = float(body.stop_loss)
|
||||||
|
tp = float(body.take_profit)
|
||||||
|
except (TypeError, ValueError) as e:
|
||||||
|
raise HTTPException(status_code=400, detail="stop_loss / take_profit 须为数字") from e
|
||||||
|
try:
|
||||||
|
ex = get_exchange()
|
||||||
|
_ensure_markets()
|
||||||
|
amt = body.contracts
|
||||||
|
if amt is None or float(amt) <= 0:
|
||||||
|
raw = ex.fetch_positions() or []
|
||||||
|
found = None
|
||||||
|
for p in raw:
|
||||||
|
psym = p.get("symbol") or ""
|
||||||
|
if not symbols_match(sym, psym):
|
||||||
|
continue
|
||||||
|
c = abs(float(p.get("contracts") or 0))
|
||||||
|
if c <= 0:
|
||||||
|
continue
|
||||||
|
ps = (p.get("side") or "").lower()
|
||||||
|
if ps and ps != side:
|
||||||
|
continue
|
||||||
|
found = c
|
||||||
|
break
|
||||||
|
if found is None:
|
||||||
|
return JSONResponse(
|
||||||
|
{"ok": False, "error": f"未找到持仓 {sym} {side}", "exchange": EXCHANGE_KIND},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
amt = found
|
||||||
|
info = replace_position_tpsl(ex, EXCHANGE_KIND, sym, side, float(amt), sl, tp)
|
||||||
|
return {"ok": True, "exchange": EXCHANGE_KIND, "placed": info}
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
return JSONResponse(
|
||||||
|
{"ok": False, "error": str(e), "exchange": EXCHANGE_KIND},
|
||||||
|
status_code=200,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.post("/emergency/close-all")
|
@app.post("/emergency/close-all")
|
||||||
def emergency_close_all(x_control_token: str | None = Header(default=None, alias="X-Control-Token")):
|
def emergency_close_all(x_control_token: str | None = Header(default=None, alias="X-Control-Token")):
|
||||||
_check_token(x_control_token)
|
_check_token(x_control_token)
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销。
|
中控子代理:拉取交易所挂单/条件单并规范化展示;撤销单笔或按合约批量撤销;挂止盈止损(先撤条件单再挂)。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
@@ -353,3 +355,281 @@ def cancel_orders_for_symbol(
|
|||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
return n
|
return n
|
||||||
|
|
||||||
|
|
||||||
|
def _binance_cancel_algo_open(ex: Any, symbol: str) -> None:
|
||||||
|
try:
|
||||||
|
market = ex.market(symbol)
|
||||||
|
cid = market.get("id")
|
||||||
|
if cid and hasattr(ex, "fapiPrivateDeleteAlgoOpenOrders"):
|
||||||
|
ex.fapiPrivateDeleteAlgoOpenOrders({"symbol": cid})
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _binance_trigger_params() -> dict[str, Any]:
|
||||||
|
wt = (os.getenv("BINANCE_TRIGGER_WORKING_TYPE") or "CONTRACT_PRICE").strip().upper()
|
||||||
|
if wt not in ("CONTRACT_PRICE", "MARK_PRICE"):
|
||||||
|
wt = "CONTRACT_PRICE"
|
||||||
|
return {"workingType": wt}
|
||||||
|
|
||||||
|
|
||||||
|
def _binance_place_tp_sl(
|
||||||
|
ex: Any,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
amount: float,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
*,
|
||||||
|
position_mode: str = "hedge",
|
||||||
|
) -> None:
|
||||||
|
ex.load_markets()
|
||||||
|
market = ex.market(symbol)
|
||||||
|
if not market.get("swap"):
|
||||||
|
raise RuntimeError("仅支持永续合约")
|
||||||
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
amt = float(ex.amount_to_precision(symbol, float(amount)))
|
||||||
|
if amt <= 0:
|
||||||
|
raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
|
||||||
|
sl_px = ex.price_to_precision(symbol, float(stop_loss))
|
||||||
|
tp_px = ex.price_to_precision(symbol, float(take_profit))
|
||||||
|
common = dict(_binance_trigger_params())
|
||||||
|
if (position_mode or "hedge").lower() in ("hedge", "dual", "double", "hedged"):
|
||||||
|
common["positionSide"] = "LONG" if direction == "long" else "SHORT"
|
||||||
|
last_err: Exception | None = None
|
||||||
|
for attempt in range(6):
|
||||||
|
try:
|
||||||
|
ex.create_order(
|
||||||
|
symbol, "STOP_MARKET", close_side, amt, None, dict(common, stopPrice=sl_px)
|
||||||
|
)
|
||||||
|
time.sleep(0.05)
|
||||||
|
ex.create_order(
|
||||||
|
symbol,
|
||||||
|
"TAKE_PROFIT_MARKET",
|
||||||
|
close_side,
|
||||||
|
amt,
|
||||||
|
None,
|
||||||
|
dict(common, stopPrice=tp_px),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
cancel_orders_for_symbol(ex, "binance", symbol, scope="conditional")
|
||||||
|
_binance_cancel_algo_open(ex, symbol)
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError(f"Binance 未接受止盈/止损:{last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
def _okx_order_params(direction: str, *, reduce_only: bool, pos_mode: str, td_mode: str) -> dict:
|
||||||
|
params: dict[str, Any] = {"tdMode": td_mode or "cross"}
|
||||||
|
if (pos_mode or "hedge").lower() in ("hedge", "long_short_mode", "dual"):
|
||||||
|
params["posSide"] = "long" if direction == "long" else "short"
|
||||||
|
if reduce_only:
|
||||||
|
params["reduceOnly"] = True
|
||||||
|
return params
|
||||||
|
|
||||||
|
|
||||||
|
def _okx_place_tp_sl(
|
||||||
|
ex: Any,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
amount: float,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
*,
|
||||||
|
pos_mode: str = "hedge",
|
||||||
|
td_mode: str = "cross",
|
||||||
|
) -> None:
|
||||||
|
ex.load_markets()
|
||||||
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
amt = float(ex.amount_to_precision(symbol, float(amount)))
|
||||||
|
if amt <= 0:
|
||||||
|
raise RuntimeError("止盈止损:可平数量经精度舍入后为 0")
|
||||||
|
params = _okx_order_params(direction, reduce_only=True, pos_mode=pos_mode, td_mode=td_mode)
|
||||||
|
sl_s = ex.price_to_precision(symbol, float(stop_loss))
|
||||||
|
tp_s = ex.price_to_precision(symbol, float(take_profit))
|
||||||
|
params["stopLoss"] = {"triggerPrice": sl_s, "type": "market"}
|
||||||
|
params["takeProfit"] = {"triggerPrice": tp_s, "type": "market"}
|
||||||
|
ex.create_order(symbol, "market", close_side, amt, None, params)
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_tpsl_env() -> tuple[bool, int, int, str]:
|
||||||
|
use_pos = (os.getenv("GATE_TPSL_USE_POSITION_ORDER") or "true").lower() in ("1", "true", "yes")
|
||||||
|
exp = int(os.getenv("GATE_TPSL_TRIGGER_EXPIRATION", str(7 * 86400)))
|
||||||
|
pt = int(os.getenv("GATE_TPSL_PRICE_TYPE", "0"))
|
||||||
|
if pt < 0 or pt > 2:
|
||||||
|
pt = 0
|
||||||
|
pos_mode = (os.getenv("GATE_POS_MODE") or "hedge").strip().lower()
|
||||||
|
return use_pos, exp, pt, pos_mode
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_place_tp_sl_position(
|
||||||
|
ex: Any,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
*,
|
||||||
|
pos_mode: str,
|
||||||
|
price_type: int,
|
||||||
|
expiration: int,
|
||||||
|
) -> None:
|
||||||
|
ex.load_markets()
|
||||||
|
market = ex.market(symbol)
|
||||||
|
if not market.get("swap"):
|
||||||
|
raise RuntimeError("仅支持永续合约")
|
||||||
|
settle = market["settleId"]
|
||||||
|
contract = market["id"]
|
||||||
|
order_type = "close-long-position" if direction == "long" else "close-short-position"
|
||||||
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
sl_rule, tp_rule = (2, 1) if close_side == "sell" else (1, 2)
|
||||||
|
initial: dict[str, Any] = {
|
||||||
|
"contract": contract,
|
||||||
|
"size": 0,
|
||||||
|
"price": "0",
|
||||||
|
"close": True,
|
||||||
|
"reduce_only": True,
|
||||||
|
"tif": "ioc",
|
||||||
|
"text": "api",
|
||||||
|
}
|
||||||
|
if pos_mode in ("hedge", "dual", "double"):
|
||||||
|
initial["auto_size"] = "close_long" if direction == "long" else "close_short"
|
||||||
|
sl_s = ex.price_to_precision(symbol, float(stop_loss))
|
||||||
|
tp_s = ex.price_to_precision(symbol, float(take_profit))
|
||||||
|
|
||||||
|
def _payload(trigger_price: str, rule: int) -> dict:
|
||||||
|
trig: dict[str, Any] = {
|
||||||
|
"strategy_type": 0,
|
||||||
|
"price_type": price_type,
|
||||||
|
"price": trigger_price,
|
||||||
|
"rule": rule,
|
||||||
|
}
|
||||||
|
if expiration > 0:
|
||||||
|
trig["expiration"] = expiration
|
||||||
|
return {
|
||||||
|
"settle": settle,
|
||||||
|
"initial": dict(initial),
|
||||||
|
"trigger": trig,
|
||||||
|
"order_type": order_type,
|
||||||
|
}
|
||||||
|
|
||||||
|
last_err: Exception | None = None
|
||||||
|
for attempt in range(6):
|
||||||
|
try:
|
||||||
|
ex.privateFuturesPostSettlePriceOrders(_payload(sl_s, sl_rule))
|
||||||
|
try:
|
||||||
|
ex.privateFuturesPostSettlePriceOrders(_payload(tp_s, tp_rule))
|
||||||
|
except Exception:
|
||||||
|
cancel_orders_for_symbol(ex, "gate", symbol, scope="conditional")
|
||||||
|
raise
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError(f"Gate 仓位类止盈/止损未接受:{last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_place_tp_sl_legacy(
|
||||||
|
ex: Any,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
amount: float,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
) -> None:
|
||||||
|
ex.load_markets()
|
||||||
|
close_side = "sell" if direction == "long" else "buy"
|
||||||
|
base = {"reduceOnly": True}
|
||||||
|
last_err: Exception | None = None
|
||||||
|
for attempt in range(6):
|
||||||
|
try:
|
||||||
|
ex.create_order(
|
||||||
|
symbol,
|
||||||
|
"market",
|
||||||
|
close_side,
|
||||||
|
amount,
|
||||||
|
None,
|
||||||
|
dict(base, stopLossPrice=float(stop_loss)),
|
||||||
|
)
|
||||||
|
ex.create_order(
|
||||||
|
symbol,
|
||||||
|
"market",
|
||||||
|
close_side,
|
||||||
|
amount,
|
||||||
|
None,
|
||||||
|
dict(base, takeProfitPrice=float(take_profit)),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception as e:
|
||||||
|
last_err = e
|
||||||
|
time.sleep(0.2 * (attempt + 1))
|
||||||
|
raise RuntimeError(f"Gate 条件止盈/止损未接受:{last_err}")
|
||||||
|
|
||||||
|
|
||||||
|
def _gate_place_tp_sl(
|
||||||
|
ex: Any,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
amount: float,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
) -> None:
|
||||||
|
use_pos, exp, pt, pos_mode = _gate_tpsl_env()
|
||||||
|
if use_pos:
|
||||||
|
try:
|
||||||
|
_gate_place_tp_sl_position(
|
||||||
|
ex, symbol, direction, stop_loss, take_profit,
|
||||||
|
pos_mode=pos_mode, price_type=pt, expiration=exp,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
_gate_place_tp_sl_legacy(ex, symbol, direction, amount, stop_loss, take_profit)
|
||||||
|
|
||||||
|
|
||||||
|
def replace_position_tpsl(
|
||||||
|
ex: Any,
|
||||||
|
exchange_kind: str,
|
||||||
|
symbol: str,
|
||||||
|
direction: str,
|
||||||
|
amount: float,
|
||||||
|
stop_loss: float,
|
||||||
|
take_profit: float,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
先撤销该合约全部条件单,再挂止盈+止损。与四实例策略页逻辑对齐(读各目录 .env 中 GATE_/BINANCE_/OKX_ 参数)。
|
||||||
|
"""
|
||||||
|
kind = (exchange_kind or "binance").lower()
|
||||||
|
direction = (direction or "long").strip().lower()
|
||||||
|
if direction not in ("long", "short"):
|
||||||
|
raise ValueError("direction 须为 long 或 short")
|
||||||
|
sl = float(stop_loss)
|
||||||
|
tp = float(take_profit)
|
||||||
|
if sl <= 0 or tp <= 0:
|
||||||
|
raise ValueError("止损、止盈价格须大于 0")
|
||||||
|
ex.load_markets()
|
||||||
|
cancelled = cancel_orders_for_symbol(ex, kind, symbol, scope="conditional")
|
||||||
|
if kind == "binance":
|
||||||
|
_binance_cancel_algo_open(ex, symbol)
|
||||||
|
time.sleep(0.08)
|
||||||
|
amt = float(amount)
|
||||||
|
if amt <= 0:
|
||||||
|
raise ValueError("持仓数量无效")
|
||||||
|
if kind == "binance":
|
||||||
|
pm = (os.getenv("BINANCE_POSITION_MODE") or "hedge").strip().lower()
|
||||||
|
_binance_place_tp_sl(ex, symbol, direction, amt, sl, tp, position_mode=pm)
|
||||||
|
elif kind == "okx":
|
||||||
|
pm = (os.getenv("OKX_POS_MODE") or "hedge").strip().lower()
|
||||||
|
td = (os.getenv("OKX_TD_MODE") or "cross").strip()
|
||||||
|
_okx_place_tp_sl(ex, symbol, direction, amt, sl, tp, pos_mode=pm, td_mode=td)
|
||||||
|
else:
|
||||||
|
_gate_place_tp_sl(ex, symbol, direction, amt, sl, tp)
|
||||||
|
return {
|
||||||
|
"symbol": symbol,
|
||||||
|
"direction": direction,
|
||||||
|
"amount": amt,
|
||||||
|
"stop_loss": sl,
|
||||||
|
"take_profit": tp,
|
||||||
|
"cancelled_conditional": cancelled,
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ HUB_BRIDGE_TOKEN = (os.getenv("HUB_BRIDGE_TOKEN") or os.getenv("CONTROL_TOKEN")
|
|||||||
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
|
_trust_raw = (os.getenv("HUB_TRUST_LAN", "true") or "").strip().lower()
|
||||||
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
HUB_TRUST_LAN = _trust_raw not in ("0", "false", "no", "off")
|
||||||
DIR = Path(__file__).resolve().parent
|
DIR = Path(__file__).resolve().parent
|
||||||
HUB_BUILD = "20260524-open-orders"
|
HUB_BUILD = "20260525-tpsl-ui"
|
||||||
|
|
||||||
|
|
||||||
def _is_local(host: str | None) -> bool:
|
def _is_local(host: str | None) -> bool:
|
||||||
@@ -401,6 +401,14 @@ class CancelSymbolOrdersBody(BaseModel):
|
|||||||
scope: str = "all"
|
scope: str = "all"
|
||||||
|
|
||||||
|
|
||||||
|
class PlaceTpslBody(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
side: str
|
||||||
|
stop_loss: float
|
||||||
|
take_profit: float
|
||||||
|
contracts: float | None = None
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/orders/{exchange_id}/cancel")
|
@app.post("/api/orders/{exchange_id}/cancel")
|
||||||
async def api_cancel_order(exchange_id: str, body: CancelOrderBody):
|
async def api_cancel_order(exchange_id: str, body: CancelOrderBody):
|
||||||
ex = _find_exchange(exchange_id)
|
ex = _find_exchange(exchange_id)
|
||||||
@@ -486,6 +494,37 @@ async def api_close_position(exchange_id: str, body: ClosePositionBody):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/api/orders/{exchange_id}/place-tpsl")
|
||||||
|
async def api_place_tpsl(exchange_id: str, body: PlaceTpslBody):
|
||||||
|
ex = _find_exchange(exchange_id)
|
||||||
|
if not ex or not ex.get("enabled"):
|
||||||
|
raise HTTPException(status_code=404, detail="账户未启用")
|
||||||
|
url = f"{ex['agent_url'].rstrip('/')}/orders/place-tpsl"
|
||||||
|
async with httpx.AsyncClient() as client:
|
||||||
|
r = await client.post(
|
||||||
|
url,
|
||||||
|
headers=_agent_headers(),
|
||||||
|
json={
|
||||||
|
"symbol": body.symbol,
|
||||||
|
"side": body.side,
|
||||||
|
"stop_loss": body.stop_loss,
|
||||||
|
"take_profit": body.take_profit,
|
||||||
|
"contracts": body.contracts,
|
||||||
|
},
|
||||||
|
timeout=120.0,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
payload = r.json()
|
||||||
|
except Exception:
|
||||||
|
payload = {"raw": (r.text or "")[:2000]}
|
||||||
|
return {
|
||||||
|
"exchange": ex,
|
||||||
|
"status_code": r.status_code,
|
||||||
|
"payload": payload,
|
||||||
|
"ok": bool(isinstance(payload, dict) and payload.get("ok")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/close/{exchange_id}")
|
@app.post("/api/close/{exchange_id}")
|
||||||
async def api_close_exchange(exchange_id: str):
|
async def api_close_exchange(exchange_id: str):
|
||||||
ex = _find_exchange(exchange_id)
|
ex = _find_exchange(exchange_id)
|
||||||
|
|||||||
@@ -594,6 +594,130 @@ button:disabled {
|
|||||||
padding: 6px 4px 8px;
|
padding: 6px 4px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.td-actions-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse-summary {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
list-style: none;
|
||||||
|
padding: 4px 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse-summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse-summary::before {
|
||||||
|
content: "▸";
|
||||||
|
color: var(--muted);
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 10px;
|
||||||
|
transition: transform 0.15s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse[open] > .orders-collapse-summary::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.orders-collapse-body {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 200;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-panel {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 380px;
|
||||||
|
padding: 20px 22px;
|
||||||
|
background: var(--bg-elevated);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-panel h3 {
|
||||||
|
margin: 0 0 8px;
|
||||||
|
font-family: var(--display);
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-meta {
|
||||||
|
margin: 0 0 14px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin-bottom: 4px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-field input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border: 1px solid var(--border-soft);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text);
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-hint {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--muted);
|
||||||
|
margin: 0 0 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
let settingsCache = null;
|
let settingsCache = null;
|
||||||
let monitorTimer = null;
|
let monitorTimer = null;
|
||||||
let authState = { required: false, logged_in: true };
|
let authState = { required: false, logged_in: true };
|
||||||
|
let tpslPending = null;
|
||||||
|
|
||||||
async function apiFetch(url, opts) {
|
async function apiFetch(url, opts) {
|
||||||
const r = await fetch(url, opts);
|
const r = await fetch(url, opts);
|
||||||
@@ -146,8 +147,22 @@
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
|
box.querySelectorAll(".btn-cancel-cond-all").forEach((btn) => {
|
||||||
btn.onclick = () =>
|
btn.onclick = (ev) => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
|
cancelSymbolOrders(btn.dataset.exId, btn.dataset.symbol, "conditional");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
box.querySelectorAll(".btn-place-tpsl").forEach((btn) => {
|
||||||
|
btn.onclick = () =>
|
||||||
|
openTpslModal(
|
||||||
|
btn.dataset.exId,
|
||||||
|
btn.dataset.symbol,
|
||||||
|
btn.dataset.side,
|
||||||
|
btn.dataset.contracts,
|
||||||
|
btn.dataset.sl || "",
|
||||||
|
btn.dataset.tp || ""
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
box.innerHTML = `<div class="err">${esc(e)}</div>`;
|
box.innerHTML = `<div class="err">${esc(e)}</div>`;
|
||||||
@@ -180,15 +195,34 @@
|
|||||||
return `<table class="data-table data-table-sub"><thead><tr><th>类型</th><th>数量</th><th>触发/价格</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
|
return `<table class="data-table data-table-sub"><thead><tr><th>类型</th><th>数量</th><th>触发/价格</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function guessTpslFromCondOrders(side, cond) {
|
||||||
|
const triggers = (cond || [])
|
||||||
|
.map((o) => o.trigger_price)
|
||||||
|
.filter((v) => v != null && !Number.isNaN(Number(v)))
|
||||||
|
.map(Number);
|
||||||
|
if (!triggers.length) return { sl: "", tp: "" };
|
||||||
|
triggers.sort((a, b) => a - b);
|
||||||
|
const s = (side || "long").toLowerCase();
|
||||||
|
if (s === "short") {
|
||||||
|
return { sl: triggers[triggers.length - 1], tp: triggers[0] };
|
||||||
|
}
|
||||||
|
return { sl: triggers[0], tp: triggers[triggers.length - 1] };
|
||||||
|
}
|
||||||
|
|
||||||
function renderPositionBlock(exchangeId, x) {
|
function renderPositionBlock(exchangeId, x) {
|
||||||
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
const symAttr = esc(x.symbol || "").replace(/"/g, """);
|
||||||
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
const sideAttr = esc((x.side || "").toLowerCase()).replace(/"/g, """);
|
||||||
|
const contractsAttr = esc(String(x.contracts != null ? x.contracts : "")).replace(/"/g, """);
|
||||||
const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
|
const cond = Array.isArray(x.conditional_orders) ? x.conditional_orders : [];
|
||||||
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
|
const reg = Array.isArray(x.regular_orders) ? x.regular_orders : [];
|
||||||
|
const guess = guessTpslFromCondOrders(x.side, cond);
|
||||||
|
const slAttr = esc(String(guess.sl)).replace(/"/g, """);
|
||||||
|
const tpAttr = esc(String(guess.tp)).replace(/"/g, """);
|
||||||
const condAllBtn =
|
const condAllBtn =
|
||||||
cond.length > 0
|
cond.length > 0
|
||||||
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部条件单</button>`
|
? `<button type="button" class="btn-cancel-cond-all ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}">撤销全部</button>`
|
||||||
: "";
|
: "";
|
||||||
|
const condBody = renderOrderRows(exchangeId, x.symbol, cond, "conditional");
|
||||||
return `<div class="pos-block">
|
return `<div class="pos-block">
|
||||||
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
<table class="data-table"><thead><tr><th>合约</th><th>方向</th><th>张数</th><th>浮盈</th><th>操作</th></tr></thead><tbody>
|
||||||
<tr>
|
<tr>
|
||||||
@@ -196,15 +230,20 @@
|
|||||||
<td>${esc(x.side)}</td>
|
<td>${esc(x.side)}</td>
|
||||||
<td>${fmt(x.contracts, 4)}</td>
|
<td>${fmt(x.contracts, 4)}</td>
|
||||||
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td>
|
<td class="${pnlCls(x.unrealized_pnl)}">${fmt(x.unrealized_pnl, 4)}</td>
|
||||||
<td class="td-actions"><button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button></td>
|
<td class="td-actions td-actions-row">
|
||||||
|
<button type="button" class="btn-place-tpsl ghost" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}" data-contracts="${contractsAttr}" data-sl="${slAttr}" data-tp="${tpAttr}">委托</button>
|
||||||
|
<button type="button" class="btn-close-pos danger" data-ex-id="${esc(exchangeId)}" data-symbol="${symAttr}" data-side="${sideAttr}">平仓</button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody></table>
|
</tbody></table>
|
||||||
<div class="pos-orders">
|
<div class="pos-orders">
|
||||||
<div class="pos-orders-head">
|
<details class="orders-collapse">
|
||||||
|
<summary class="orders-collapse-summary">
|
||||||
<span class="pos-orders-title">条件单 · ${cond.length}</span>
|
<span class="pos-orders-title">条件单 · ${cond.length}</span>
|
||||||
${condAllBtn}
|
${condAllBtn}
|
||||||
</div>
|
</summary>
|
||||||
${renderOrderRows(exchangeId, x.symbol, cond, "conditional")}
|
<div class="orders-collapse-body">${condBody}</div>
|
||||||
|
</details>
|
||||||
<div class="pos-orders-head" style="margin-top:10px">
|
<div class="pos-orders-head" style="margin-top:10px">
|
||||||
<span class="pos-orders-title">普通委托 · ${reg.length}</span>
|
<span class="pos-orders-title">普通委托 · ${reg.length}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,6 +252,103 @@
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openTpslModal(exchangeId, symbol, side, contracts, slHint, tpHint) {
|
||||||
|
tpslPending = {
|
||||||
|
exchangeId,
|
||||||
|
symbol,
|
||||||
|
side: (side || "long").toLowerCase(),
|
||||||
|
contracts: parseFloat(contracts),
|
||||||
|
};
|
||||||
|
const modal = document.getElementById("tpsl-modal");
|
||||||
|
const meta = document.getElementById("tpsl-modal-meta");
|
||||||
|
const slIn = document.getElementById("tpsl-sl");
|
||||||
|
const tpIn = document.getElementById("tpsl-tp");
|
||||||
|
if (!modal || !meta || !slIn || !tpIn) return;
|
||||||
|
meta.textContent = `${symbol} · ${side} · ${contracts} 张`;
|
||||||
|
slIn.value = slHint !== "" && slHint != null ? String(slHint) : "";
|
||||||
|
tpIn.value = tpHint !== "" && tpHint != null ? String(tpHint) : "";
|
||||||
|
modal.classList.remove("hidden");
|
||||||
|
modal.setAttribute("aria-hidden", "false");
|
||||||
|
slIn.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeTpslModal() {
|
||||||
|
tpslPending = null;
|
||||||
|
const modal = document.getElementById("tpsl-modal");
|
||||||
|
if (modal) {
|
||||||
|
modal.classList.add("hidden");
|
||||||
|
modal.setAttribute("aria-hidden", "true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitTpslModal() {
|
||||||
|
if (!tpslPending) return;
|
||||||
|
const slIn = document.getElementById("tpsl-sl");
|
||||||
|
const tpIn = document.getElementById("tpsl-tp");
|
||||||
|
const sl = parseFloat(slIn && slIn.value);
|
||||||
|
const tp = parseFloat(tpIn && tpIn.value);
|
||||||
|
if (!sl || sl <= 0 || !tp || tp <= 0) {
|
||||||
|
showToast("请填写有效的止损价与止盈价", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { exchangeId, symbol, side, contracts } = tpslPending;
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
`确认 ${symbol} ${side}\n先撤销全部条件单,再挂止损 ${sl}、止盈 ${tp}?`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const btn = document.getElementById("tpsl-submit");
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
try {
|
||||||
|
const r = await apiFetch(
|
||||||
|
"/api/orders/" + encodeURIComponent(exchangeId) + "/place-tpsl",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({
|
||||||
|
symbol,
|
||||||
|
side,
|
||||||
|
stop_loss: sl,
|
||||||
|
take_profit: tp,
|
||||||
|
contracts: contracts > 0 ? contracts : null,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const j = await r.json();
|
||||||
|
const pl = j.payload || {};
|
||||||
|
const ok = j.ok && pl.ok !== false;
|
||||||
|
const n = pl.placed && pl.placed.cancelled_conditional;
|
||||||
|
showToast(
|
||||||
|
ok
|
||||||
|
? `已挂单(已撤 ${n != null ? n : "?"} 笔旧条件单)`
|
||||||
|
: pl.error || JSON.stringify(j),
|
||||||
|
!ok
|
||||||
|
);
|
||||||
|
if (ok) {
|
||||||
|
closeTpslModal();
|
||||||
|
loadMonitorBoard();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
showToast(String(e), true);
|
||||||
|
} finally {
|
||||||
|
if (btn) btn.disabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initTpslModal() {
|
||||||
|
const backdrop = document.getElementById("tpsl-modal-backdrop");
|
||||||
|
const cancel = document.getElementById("tpsl-cancel");
|
||||||
|
const submit = document.getElementById("tpsl-submit");
|
||||||
|
if (backdrop) backdrop.onclick = closeTpslModal;
|
||||||
|
if (cancel) cancel.onclick = closeTpslModal;
|
||||||
|
if (submit) submit.onclick = () => submitTpslModal();
|
||||||
|
document.addEventListener("keydown", (ev) => {
|
||||||
|
if (ev.key === "Escape") closeTpslModal();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
|
async function cancelOneOrder(exchangeId, symbol, orderId, channel) {
|
||||||
if (!confirm(`撤销委托 ${symbol} #${orderId}?`)) return;
|
if (!confirm(`撤销委托 ${symbol} #${orderId}?`)) return;
|
||||||
try {
|
try {
|
||||||
@@ -557,6 +693,8 @@
|
|||||||
showToast("已添加一行,请填写 URL 后点「保存设置」");
|
showToast("已添加一行,请填写 URL 后点「保存设置」");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
initTpslModal();
|
||||||
|
|
||||||
initAuth().then((ok) => {
|
initAuth().then((ok) => {
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
loadSettings().catch(() => {});
|
loadSettings().catch(() => {});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260524-open-orders" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260525-tpsl-ui" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -79,7 +79,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tpsl-modal" class="modal hidden" aria-hidden="true">
|
||||||
|
<div class="modal-backdrop" id="tpsl-modal-backdrop"></div>
|
||||||
|
<div class="modal-panel" role="dialog" aria-labelledby="tpsl-modal-title">
|
||||||
|
<h3 id="tpsl-modal-title">挂止盈 / 止损</h3>
|
||||||
|
<p id="tpsl-modal-meta" class="modal-meta"></p>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="tpsl-sl">止损价</label>
|
||||||
|
<input id="tpsl-sl" type="number" step="any" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-field">
|
||||||
|
<label for="tpsl-tp">止盈价</label>
|
||||||
|
<input id="tpsl-tp" type="number" step="any" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
<p class="modal-hint">先撤销该合约全部条件单,再挂新止盈与止损(四所统一)。</p>
|
||||||
|
<div class="modal-actions">
|
||||||
|
<button type="button" id="tpsl-cancel" class="ghost">取消</button>
|
||||||
|
<button type="button" id="tpsl-submit" class="primary">确认挂单</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="/assets/app.js?v=20260524-open-orders"></script>
|
<script src="/assets/app.js?v=20260525-tpsl-ui"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -177,8 +177,10 @@ curl -s http://127.0.0.1:5100/api/ping
|
|||||||
| 功能 | 说明 |
|
| 功能 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| **2×2 卡片** | 仅显示「已启用」账户;每卡含子代理持仓、浮盈、余额 |
|
| **2×2 卡片** | 仅显示「已启用」账户;每卡含子代理持仓、浮盈、余额 |
|
||||||
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(止盈/止损等)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
| **条件单 / 委托** | 每个持仓下方展示交易所 **条件单**(默认折叠)与 **普通委托**;数据来自子代理实时拉取(币安含 Algo 通道) |
|
||||||
| **撤单** | 单笔「撤单」或「撤销全部条件单」;经中控转发子代理 `POST /orders/cancel`、`/orders/cancel-symbol` |
|
| **撤单** | 条件单区内单笔「撤单」或「撤销全部」;经中控 `POST /api/orders/{id}/cancel`、`cancel-symbol` |
|
||||||
|
| **挂止盈止损** | 持仓行 **「委托」**:弹窗填止损/止盈价 → **先撤该合约全部条件单,再挂新 TP/SL**(币安 / OKX / Gate / Gate趋势 四所统一,逻辑与各实例 `.env` 参数一致) |
|
||||||
|
| **平仓** | 持仓行「平仓」:仅平该方向仓位(子代理市价减仓) |
|
||||||
| **机器人单** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active),为本地监控计划,**不等于**交易所条件单 |
|
| **机器人单** | 来自实例 `/api/hub/monitor` 的 `order_monitors`(active),为本地监控计划,**不等于**交易所条件单 |
|
||||||
| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) |
|
| **关键位** | 仅 `capabilities` 含 `key` 的户;展示门控摘要(`/api/price_snapshot`) |
|
||||||
| **趋势计划** | 仅当该户勾选 **监控趋势计划** 时展示 `trend_pullback_plans`(active) |
|
| **趋势计划** | 仅当该户勾选 **监控趋势计划** 时展示 `trend_pullback_plans`(active) |
|
||||||
|
|||||||
@@ -116,6 +116,28 @@ curl -s http://127.0.0.1:15202/status | head -c 300
|
|||||||
|
|
||||||
应 `ok: true` 且有 `balance_usdt`。
|
应 `ok: true` 且有 `balance_usdt`。
|
||||||
|
|
||||||
|
### 3.3 Gate 子代理「一会正常、一会连不上」(仅 Gate 两户)
|
||||||
|
|
||||||
|
| 现象 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 中控 LINK 2/4,仅 Gate 红 | 本机 `15202`/`15203` 在 PM2 重启间隙连不上 |
|
||||||
|
| 日志 `$'\r': command not found` | `crypto_monitor_gate*` 的 `.env` 为 Windows CRLF |
|
||||||
|
| `curl` 有时通有时不通 | 与 Gate 外网无关,先修 CRLF 并重建 agent |
|
||||||
|
|
||||||
|
**修复**(服务器):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/crypto_monitor
|
||||||
|
sed -i 's/\r$//' crypto_monitor_gate/.env crypto_monitor_gate_bot/.env
|
||||||
|
bash manual_trading_hub/scripts/fix_env_crlf.sh
|
||||||
|
cd manual_trading_hub && pm2 restart manual-agent-gate manual-agent-gate-bot
|
||||||
|
# 仍反复重启时:pm2 delete 后按 ecosystem.config.cjs 重新 start(见部署文档 §5.6)
|
||||||
|
```
|
||||||
|
|
||||||
|
修好后 `pm2 describe manual-agent-gate` 的 **restarts** 应不再疯涨;`pm2 flush manual-agent-gate` 可清掉旧 CRLF 日志。
|
||||||
|
|
||||||
|
**若子代理已绿但挂委托失败**:再查 `GATE_SOCKS_PROXY`、API 权限、止损止盈价格是否合理(与各实例策略页相同 `.env` 参数)。
|
||||||
|
|
||||||
### 3.2 有持仓但无关键位 / 趋势,或提示 Flask 404
|
### 3.2 有持仓但无关键位 / 趋势,或提示 Flask 404
|
||||||
|
|
||||||
| 原因 | 处理 |
|
| 原因 | 处理 |
|
||||||
|
|||||||
@@ -147,6 +147,35 @@ pm2 status
|
|||||||
# crypto_binance / crypto_gate …(各策略目录自有 ecosystem.config.cjs)
|
# crypto_binance / crypto_gate …(各策略目录自有 ecosystem.config.cjs)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 5.6 Gate 子代理「一会能连、一会子代理不可用」(Windows `.env` 换行)
|
||||||
|
|
||||||
|
**现象**:仅 Gate 训练 / Gate 趋势卡片红字「子代理不可用」;`pm2 logs manual-agent-gate` 反复出现:
|
||||||
|
|
||||||
|
```text
|
||||||
|
./.env: line 22: $'\r': command not found
|
||||||
|
agent start: exchange=gate port=15202 ...
|
||||||
|
```
|
||||||
|
|
||||||
|
**原因**:在 Windows 编辑的 `crypto_monitor_gate/.env`、`crypto_monitor_gate_bot/.env` 为 **CRLF**,Linux 上 `source` 失败;PM2 反复重启,中控轮询时偶发连不上(**不是外网问题**)。
|
||||||
|
|
||||||
|
**处理**(在服务器仓库根执行):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /opt/crypto_monitor
|
||||||
|
sed -i 's/\r$//' crypto_monitor_gate/.env crypto_monitor_gate_bot/.env
|
||||||
|
bash manual_trading_hub/scripts/fix_env_crlf.sh
|
||||||
|
cd manual_trading_hub
|
||||||
|
pm2 delete manual-agent-gate manual-agent-gate-bot 2>/dev/null || true
|
||||||
|
pm2 start ecosystem.config.cjs --only manual-agent-gate
|
||||||
|
pm2 start ecosystem.config.cjs --only manual-agent-gate-bot
|
||||||
|
pm2 save
|
||||||
|
curl -s http://127.0.0.1:15202/status | head -c 200 # 应 ok:true
|
||||||
|
```
|
||||||
|
|
||||||
|
**预防**:`.env` 保存为 **LF**;Windows 用 `deploy/setup_env.ps1` 复制 `.env.example` 时已写 LF。子代理须经 **`scripts/run_agent.sh`** 启动(内置去 CRLF 的 `load_dotenv_file`),勿裸跑 `python agent.py`。
|
||||||
|
|
||||||
|
详见 [常见问题.md](./常见问题.md) **§3.1**、**§3.3**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 六、手动启动(不用 PM2 时)
|
## 六、手动启动(不用 PM2 时)
|
||||||
|
|||||||
Reference in New Issue
Block a user