simulation_service.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. """
  2. 智能股票分析助手 — 模拟交易服务层
  3. 封装模拟交易操作(持仓查询、资金查询、委托下单、撤单等),供API路由层调用。
  4. """
  5. import sys
  6. from pathlib import Path
  7. from typing import Optional
  8. # 确保skills路径可导入
  9. _PROJECT_ROOT = Path(__file__).parent.parent.parent.parent # backend/app/services -> project root
  10. _AGENTS_DIR = _PROJECT_ROOT / "agents"
  11. _SKILLS_MONI = _PROJECT_ROOT / "skills" / "模拟组合管理" / "mx-moni"
  12. for p in [_AGENTS_DIR, _SKILLS_MONI, str(_PROJECT_ROOT)]:
  13. if str(p) not in sys.path:
  14. sys.path.insert(0, str(p))
  15. import requests
  16. from app.config import settings
  17. from app.utils.mock_trading_normalize import extract_orders_dicts, normalize_mock_order_row
  18. # API基础地址
  19. MX_API_URL = "https://mkapi2.dfcfs.com/finskillshub"
  20. def _make_request(endpoint: str, body: dict) -> dict:
  21. """发送模拟交易API请求
  22. Args:
  23. endpoint: API端点路径
  24. body: 请求体
  25. Returns:
  26. API响应JSON
  27. """
  28. headers = {
  29. "apikey": settings.MX_APIKEY,
  30. "Content-Type": "application/json",
  31. }
  32. response = requests.post(
  33. f"{MX_API_URL}{endpoint}",
  34. headers=headers,
  35. json=body,
  36. timeout=30,
  37. )
  38. response.raise_for_status()
  39. return response.json()
  40. def _check_api_ready() -> Optional[dict]:
  41. """检查API是否就绪,未就绪返回错误字典"""
  42. if not settings.MX_APIKEY or settings.MX_APIKEY == "your-mx-apikey-here":
  43. return {"error": "MX_APIKEY 未配置"}
  44. return None
  45. def get_positions() -> dict:
  46. """查询模拟持仓
  47. Returns:
  48. {
  49. "success": True/False,
  50. "positions": [{"code": str, "name": str, "quantity": int, ...}, ...],
  51. "total": int,
  52. "error": str or None
  53. }
  54. """
  55. result = {
  56. "success": False,
  57. "positions": [],
  58. "total": 0,
  59. "error": None,
  60. }
  61. api_error = _check_api_ready()
  62. if api_error:
  63. result["error"] = api_error["error"]
  64. return result
  65. try:
  66. raw = _make_request("/api/claw/mockTrading/positions", {"moneyUnit": 1})
  67. if not raw.get("success") and str(raw.get("code")) != "200":
  68. result["error"] = raw.get("message", "查询持仓失败")
  69. return result
  70. data = raw.get("data", {})
  71. positions = data.get("positions", [])
  72. parsed = []
  73. for pos in (positions or []):
  74. parsed.append({
  75. "stock_code": pos.get("stockCode", ""),
  76. "stock_name": pos.get("stockName", ""),
  77. "quantity": pos.get("quantity", 0),
  78. "cost_price": pos.get("costPrice", 0),
  79. "current_price": pos.get("currentPrice", 0),
  80. "profit_loss": pos.get("profitLoss", 0),
  81. "profit_loss_rate": pos.get("profitLossRate", 0),
  82. "market_value": pos.get("marketValue", 0),
  83. })
  84. result["success"] = True
  85. result["positions"] = parsed
  86. result["total"] = len(parsed)
  87. return result
  88. except Exception as e:
  89. result["error"] = str(e)
  90. return result
  91. def get_balance() -> dict:
  92. """查询模拟账户资金
  93. Returns:
  94. {
  95. "success": True/False,
  96. "balance": {...},
  97. "error": str or None
  98. }
  99. """
  100. result = {
  101. "success": False,
  102. "balance": {},
  103. "error": None,
  104. }
  105. api_error = _check_api_ready()
  106. if api_error:
  107. result["error"] = api_error["error"]
  108. return result
  109. try:
  110. raw = _make_request("/api/claw/mockTrading/balance", {"moneyUnit": 1})
  111. if not raw.get("success") and str(raw.get("code")) != "200":
  112. result["error"] = raw.get("message", "查询资金失败")
  113. return result
  114. data = raw.get("data", {})
  115. result["success"] = True
  116. result["balance"] = {
  117. "total_assets": data.get("totalAssets", 0),
  118. "available_balance": data.get("availBalance", 0),
  119. "frozen_balance": data.get("frozenBalance", 0),
  120. "market_value": data.get("marketValue", 0),
  121. "total_profit_loss": data.get("totalProfitLoss", 0),
  122. }
  123. return result
  124. except Exception as e:
  125. result["error"] = str(e)
  126. return result
  127. def get_orders() -> dict:
  128. """查询委托记录
  129. Returns:
  130. {
  131. "success": True/False,
  132. "orders": [...],
  133. "total": int,
  134. "error": str or None
  135. }
  136. """
  137. result = {
  138. "success": False,
  139. "orders": [],
  140. "total": 0,
  141. "error": None,
  142. }
  143. api_error = _check_api_ready()
  144. if api_error:
  145. result["error"] = api_error["error"]
  146. return result
  147. try:
  148. raw = _make_request("/api/claw/mockTrading/orders", {
  149. "fltOrderDrt": 0,
  150. "fltOrderStatus": 0,
  151. })
  152. if not raw.get("success") and str(raw.get("code")) != "200":
  153. result["error"] = raw.get("message", "查询委托失败")
  154. return result
  155. data = raw.get("data", {}) or {}
  156. # 妙想可能返回 list,或 { rows: [] },或当日/历史分段字段
  157. orders = extract_orders_dicts(data)
  158. parsed = []
  159. for order in orders:
  160. if not isinstance(order, dict):
  161. continue
  162. # 统一解析字段名与枚举(数值买卖方向、委托状态等)
  163. parsed.append(normalize_mock_order_row(order))
  164. result["success"] = True
  165. result["orders"] = parsed
  166. result["total"] = len(parsed)
  167. return result
  168. except Exception as e:
  169. result["error"] = str(e)
  170. return result
  171. def place_order(
  172. trade_type: str,
  173. stock_code: str,
  174. quantity: int,
  175. price: Optional[float] = None,
  176. ) -> dict:
  177. """模拟下单(买入/卖出)
  178. Args:
  179. trade_type: 交易类型 "buy" 或 "sell"
  180. stock_code: 6位股票代码
  181. quantity: 委托数量(必须为100的整数倍)
  182. price: 委托价格(None表示市价委托)
  183. Returns:
  184. {
  185. "success": True/False,
  186. "order_id": str,
  187. "message": str,
  188. "error": str or None
  189. }
  190. """
  191. result = {
  192. "success": False,
  193. "order_id": "",
  194. "message": "",
  195. "error": None,
  196. }
  197. # 参数校验
  198. if trade_type not in ("buy", "sell"):
  199. result["error"] = "交易类型无效,请使用 buy 或 sell"
  200. return result
  201. if not stock_code or len(str(stock_code)) < 6:
  202. result["error"] = "请输入有效的6位股票代码"
  203. return result
  204. if quantity <= 0:
  205. result["error"] = "委托数量必须大于0"
  206. return result
  207. if quantity % 100 != 0:
  208. result["error"] = "A股交易数量必须为100股的整数倍"
  209. return result
  210. api_error = _check_api_ready()
  211. if api_error:
  212. result["error"] = api_error["error"]
  213. return result
  214. try:
  215. body = {
  216. "type": trade_type,
  217. "stockCode": str(stock_code),
  218. "quantity": int(quantity),
  219. "useMarketPrice": price is None,
  220. }
  221. if price is not None:
  222. body["price"] = float(price)
  223. raw = _make_request("/api/claw/mockTrading/trade", body)
  224. if not raw.get("success") and str(raw.get("code")) != "200":
  225. result["error"] = raw.get("message", "下单失败")
  226. return result
  227. data = raw.get("data", {})
  228. order_id = data.get("orderId", "")
  229. result["success"] = True
  230. result["order_id"] = order_id
  231. direction_cn = "买入" if trade_type == "buy" else "卖出"
  232. price_info = f"@{price}元" if price else "市价"
  233. result["message"] = f"{direction_cn}委托已提交: {stock_code} {quantity}股 {price_info}"
  234. return result
  235. except Exception as e:
  236. result["error"] = str(e)
  237. return result
  238. def cancel_order(order_id: str, stock_code: str = "") -> dict:
  239. """撤单
  240. Args:
  241. order_id: 委托编号
  242. stock_code: 股票代码(可选)
  243. Returns:
  244. {
  245. "success": True/False,
  246. "message": str,
  247. "error": str or None
  248. }
  249. """
  250. result = {
  251. "success": False,
  252. "message": "",
  253. "error": None,
  254. }
  255. if not order_id:
  256. result["error"] = "请提供委托编号"
  257. return result
  258. api_error = _check_api_ready()
  259. if api_error:
  260. result["error"] = api_error["error"]
  261. return result
  262. try:
  263. body = {
  264. "type": "order",
  265. "orderId": str(order_id),
  266. }
  267. if stock_code:
  268. body["stockCode"] = str(stock_code)
  269. raw = _make_request("/api/claw/mockTrading/cancel", body)
  270. if not raw.get("success") and str(raw.get("code")) != "200":
  271. result["error"] = raw.get("message", "撤单失败")
  272. return result
  273. result["success"] = True
  274. result["message"] = f"委托 {order_id} 已撤销"
  275. return result
  276. except Exception as e:
  277. result["error"] = str(e)
  278. return result
  279. def cancel_all_orders() -> dict:
  280. """一键撤单(撤销所有未成交委托)
  281. Returns:
  282. {
  283. "success": True/False,
  284. "message": str,
  285. "error": str or None
  286. }
  287. """
  288. result = {
  289. "success": False,
  290. "message": "",
  291. "error": None,
  292. }
  293. api_error = _check_api_ready()
  294. if api_error:
  295. result["error"] = api_error["error"]
  296. return result
  297. try:
  298. raw = _make_request("/api/claw/mockTrading/cancel", {"type": "all"})
  299. if not raw.get("success") and str(raw.get("code")) != "200":
  300. result["error"] = raw.get("message", "一键撤单失败")
  301. return result
  302. result["success"] = True
  303. result["message"] = "所有未成交委托已撤销"
  304. return result
  305. except Exception as e:
  306. result["error"] = str(e)
  307. return result