Skip to content

Commit 75dcdde

Browse files
v4.0.7
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent 31dde00 commit 75dcdde

24 files changed

Lines changed: 1062 additions & 71 deletions

.env.example

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
# POSTGRES_PASSWORD=password_from_old_container
2424
# POSTGRES_DB=quantdinger
2525

26-
# Global image source switch
26+
# Global image source switch for infrastructure images
2727
# Leave empty for official Docker Hub:
2828
# IMAGE_PREFIX=
2929
#
@@ -43,6 +43,19 @@
4343
# cn = use Aliyun apt/PyPI mirrors with fallback to official
4444
# BUILD_REGION=global
4545

46+
# Backend Python base image override for local builds.
47+
# docker-compose.yml defaults to docker.1ms.run/library/python:3.12-slim-bookworm
48+
# to reduce first-install failures caused by Docker Hub auth/base-image pulls.
49+
# Set this only when you want a different source.
50+
# PYTHON_BASE_IMAGE=python:3.12-slim-bookworm
51+
# PYTHON_BASE_IMAGE=docker.xuanyuan.me/library/python:3.12-slim-bookworm
52+
53+
# Recommended install paths:
54+
# - Regular users: use docker-compose.ghcr.yml. It pulls prebuilt backend,
55+
# frontend, and mobile images and does not build from python:3.12 locally.
56+
# - Developers/local source edits: use docker-compose.yml and set
57+
# PYTHON_BASE_IMAGE only if Docker Hub is slow.
58+
4659
# ----- Image tags (consumed by both docker-compose.yml and docker-compose.ghcr.yml) -----
4760
#
4861
# Resolution order (highest precedence first):

backend_api_python/app/routes/agent_v1/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def register(app) -> None:
4040
from . import backtests # noqa: F401
4141
from . import experiments # noqa: F401
4242
from . import portfolio # noqa: F401
43+
from . import runtime # noqa: F401
4344
from . import quick_trade # noqa: F401
4445
from . import jobs as jobs_module # noqa: F401
4546
from . import indicators # noqa: F401

backend_api_python/app/routes/agent_v1/backtests.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from app.utils.agent_jobs import submit_job
1717
from app.utils.logger import get_logger
18+
from flask import request
1819

1920
from . import agent_v1_bp
2021
from ._helpers import envelope, error, get_json_or_400
@@ -147,5 +148,6 @@ def create_backtest():
147148
kind="backtest",
148149
request_payload=payload,
149150
runner=_run_backtest,
151+
idempotency_key=request.headers.get("Idempotency-Key"),
150152
)
151153
return envelope(job, message="queued", status=202)

backend_api_python/app/routes/agent_v1/experiments.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
)
1111
from app.utils.agent_jobs import submit_job
1212
from app.utils.logger import get_logger
13+
from flask import request
1314

1415
from . import agent_v1_bp
1516
from ._helpers import envelope, error, get_json_or_400
@@ -61,6 +62,7 @@ def _run(p):
6162
kind="experiment_pipeline",
6263
request_payload=payload,
6364
runner=_run,
65+
idempotency_key=request.headers.get("Idempotency-Key"),
6466
)
6567
return envelope(job, message="queued", status=202)
6668

backend_api_python/app/routes/agent_v1/quick_trade.py

Lines changed: 163 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from __future__ import annotations
1313

1414
import os
15+
import time
1516
import uuid
1617
from typing import Any
1718

@@ -20,6 +21,7 @@
2021
SCOPE_T, agent_required, current_token, current_user_id,
2122
instrument_allowed, market_allowed, paper_only, with_idempotency,
2223
)
24+
from app.utils.agent_jobs import record_completed_job
2325
from app.utils.db import get_db_connection
2426
from app.utils.logger import get_logger
2527
from flask import request
@@ -53,6 +55,8 @@ def _last_price(market: str, symbol: str) -> float | None:
5355

5456

5557
def _record_paper_order(*, body: dict, fill_price: float | None, status: str, note: str = "") -> dict:
58+
import uuid
59+
5660
order_uid = uuid.uuid4().hex
5761
market = (body.get("market") or "").strip()
5862
symbol = (body.get("symbol") or "").strip()
@@ -99,6 +103,141 @@ def _record_paper_order(*, body: dict, fill_price: float | None, status: str, no
99103
}
100104

101105

106+
def _place_live_order(*, body: dict, user_id: int) -> dict:
107+
credential_id = int(body.get("credential_id") or body.get("credentialId") or 0)
108+
market = (body.get("market") or "").strip()
109+
symbol = (body.get("symbol") or "").strip()
110+
side = (body.get("side") or "").strip().lower()
111+
order_type = (body.get("order_type") or body.get("orderType") or "market").strip().lower()
112+
qty = float(body.get("qty") or body.get("quantity") or body.get("amount") or 0)
113+
limit_price = body.get("limit_price") or body.get("limitPrice") or body.get("price")
114+
limit_price_f = float(limit_price or 0)
115+
market_type = (body.get("market_type") or body.get("marketType") or "spot").strip().lower()
116+
leverage = int(body.get("leverage") or 1)
117+
margin_mode = (body.get("margin_mode") or body.get("marginMode") or "").strip().lower()
118+
if market_type in ("futures", "future", "perp", "perpetual"):
119+
market_type = "swap"
120+
if leverage > 1:
121+
market_type = "swap"
122+
if market_type not in ("spot", "swap"):
123+
market_type = "spot"
124+
if order_type == "limit" and limit_price_f <= 0:
125+
raise ValueError("limit_price is required for limit orders")
126+
if not credential_id:
127+
raise ValueError("credential_id is required for live agent trading")
128+
129+
from app.routes.quick_trade import _record_quick_trade, _reject_quick_trade_if_desktop_broker
130+
from app.services.quick_trade.credentials import build_exchange_config, create_exchange_client
131+
from app.services.quick_trade.orders import enrich_fill, limit_order_kwargs
132+
133+
cfg_overrides: dict[str, Any] = {"market_type": market_type}
134+
if margin_mode in ("cross", "crossed"):
135+
cfg_overrides["margin_mode"] = "cross"
136+
cfg_overrides["td_mode"] = "cross"
137+
elif margin_mode in ("iso", "isolated"):
138+
cfg_overrides["margin_mode"] = "isolated"
139+
cfg_overrides["td_mode"] = "isolated"
140+
141+
exchange_config = build_exchange_config(credential_id, user_id, cfg_overrides)
142+
exchange_id = (exchange_config.get("exchange_id") or "").strip().lower()
143+
if not exchange_id:
144+
raise ValueError("Invalid credential: missing exchange_id")
145+
reject = _reject_quick_trade_if_desktop_broker(exchange_id)
146+
if reject is not None:
147+
raise ValueError("Quick Trade currently supports crypto exchange API keys only.")
148+
149+
client = create_exchange_client(exchange_config, market_type=market_type)
150+
151+
if market_type != "spot" and leverage > 1 and hasattr(client, "set_leverage"):
152+
try:
153+
client.set_leverage(symbol=symbol, leverage=leverage)
154+
except TypeError:
155+
try:
156+
client.set_leverage(symbol=symbol, lever=leverage)
157+
except Exception:
158+
pass
159+
except Exception as exc:
160+
logger.warning(f"agent quick_trade set_leverage failed (non-fatal): {exc}")
161+
162+
client_order_id = f"qa{str(int(time.time()))[-6:]}{uuid.uuid4().hex[:8]}"
163+
if order_type == "market":
164+
from app.services.live_trading.execution import place_order_from_signal
165+
166+
if market_type == "spot":
167+
signal_type = "open_long" if side == "buy" else "close_long"
168+
else:
169+
signal_type = "open_long" if side == "buy" else "open_short"
170+
result = place_order_from_signal(
171+
client=client,
172+
signal_type=signal_type,
173+
symbol=symbol,
174+
amount=qty,
175+
market_type=market_type,
176+
exchange_config=exchange_config,
177+
client_order_id=client_order_id,
178+
)
179+
else:
180+
result = client.place_limit_order(
181+
symbol=symbol,
182+
side=side.upper() if "binance" in exchange_id else side,
183+
**limit_order_kwargs(client, symbol, qty, limit_price_f, side, market_type, client_order_id),
184+
)
185+
186+
exchange_order_id = str(getattr(result, "exchange_order_id", "") or "")
187+
filled = float(getattr(result, "filled", 0) or 0)
188+
avg_fill = float(getattr(result, "avg_price", 0) or 0)
189+
raw = getattr(result, "raw", {}) or {}
190+
commission = 0.0
191+
commission_ccy = ""
192+
if exchange_order_id:
193+
enrich = enrich_fill(client, order_id=exchange_order_id, symbol=symbol, market_type=market_type)
194+
if enrich.get("filled", 0.0) > 0:
195+
filled = float(enrich["filled"])
196+
if enrich.get("avg_price", 0.0) > 0:
197+
avg_fill = float(enrich["avg_price"])
198+
commission = float(enrich.get("fee") or 0.0)
199+
commission_ccy = str(enrich.get("fee_ccy") or "")
200+
201+
trade_id = _record_quick_trade(
202+
user_id=user_id,
203+
credential_id=credential_id,
204+
exchange_id=exchange_id,
205+
symbol=symbol,
206+
side=side,
207+
order_type=order_type,
208+
amount=qty,
209+
price=limit_price_f if order_type == "limit" else avg_fill,
210+
leverage=leverage,
211+
market_type=market_type,
212+
tp_price=float(body.get("tp_price") or body.get("tpPrice") or 0),
213+
sl_price=float(body.get("sl_price") or body.get("slPrice") or 0),
214+
status="filled" if filled > 0 else "submitted",
215+
exchange_order_id=exchange_order_id,
216+
filled=filled,
217+
avg_price=avg_fill,
218+
error_msg="",
219+
source="agent_mcp",
220+
raw_result=raw,
221+
commission=commission,
222+
commission_ccy=commission_ccy,
223+
)
224+
225+
return {
226+
"trade_id": trade_id,
227+
"exchange_order_id": exchange_order_id,
228+
"market": market,
229+
"symbol": symbol,
230+
"side": side,
231+
"order_type": order_type,
232+
"qty": qty,
233+
"limit_price": limit_price_f if order_type == "limit" else None,
234+
"filled": filled,
235+
"avg_price": avg_fill,
236+
"status": "filled" if filled > 0 else "submitted",
237+
"paper": False,
238+
}
239+
240+
102241
@agent_v1_bp.route("/quick-trade/orders", methods=["POST"])
103242
@agent_required(SCOPE_T)
104243
def place_order():
@@ -110,7 +249,7 @@ def place_order():
110249
market = (body.get("market") or "").strip()
111250
symbol = (body.get("symbol") or "").strip()
112251
side = (body.get("side") or "").strip().lower()
113-
qty = body.get("qty") or body.get("quantity")
252+
qty = body.get("qty") or body.get("quantity") or body.get("amount")
114253

115254
if not market or not symbol:
116255
return error(400, "market and symbol are required")
@@ -139,17 +278,35 @@ def place_order():
139278
# operator must enable AGENT_LIVE_TRADING_ENABLED to actually route to
140279
# exchange clients — keeping a final environment-level kill switch.
141280
if (not paper_only()) and _live_trading_kill_switch():
142-
return error(
143-
501,
144-
"Live agent trading is not implemented in this build. "
145-
"Use the human Quick Trade flow until live agent execution is enabled.",
146-
http=501,
281+
try:
282+
result = _place_live_order(body=body, user_id=current_user_id())
283+
except ValueError as exc:
284+
return error(400, str(exc), http=400)
285+
except Exception as exc:
286+
logger.error(f"agent_v1 live quick_trade failed: {exc}", exc_info=True)
287+
return error(500, "live quick_trade failed", details=str(exc), http=500)
288+
record_completed_job(
289+
user_id=current_user_id(),
290+
agent_token_id=int(current_token().get("id") or 0),
291+
kind="quick_trade_order",
292+
request_payload=body,
293+
result=result,
294+
idempotency_key=request.headers.get("Idempotency-Key"),
147295
)
296+
return envelope(result, message="live-order")
148297

149298
fill_price = _last_price(market, symbol)
150299
note = "" if fill_price is not None else "no last price available; recorded without fill"
151300
status = "filled" if fill_price is not None else "rejected"
152301
result = _record_paper_order(body=body, fill_price=fill_price, status=status, note=note)
302+
record_completed_job(
303+
user_id=current_user_id(),
304+
agent_token_id=int(current_token().get("id") or 0),
305+
kind="quick_trade_order",
306+
request_payload=body,
307+
result=result,
308+
idempotency_key=request.headers.get("Idempotency-Key"),
309+
)
153310
return envelope(result, message="paper-fill")
154311

155312

0 commit comments

Comments
 (0)