1212from __future__ import annotations
1313
1414import os
15+ import time
1516import uuid
1617from typing import Any
1718
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
2325from app .utils .db import get_db_connection
2426from app .utils .logger import get_logger
2527from flask import request
@@ -53,6 +55,8 @@ def _last_price(market: str, symbol: str) -> float | None:
5355
5456
5557def _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 )
104243def 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