mx_zixuan.py 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. #!/usr/bin/env python3
  2. """
  3. 妙想自选管理skill (mx_zixuan)
  4. 支持查询、添加、删除东方财富自选股
  5. 统一输出到 /root/.openclaw/workspace/mx_data/output/
  6. """
  7. import os
  8. import sys
  9. import csv
  10. import json
  11. import requests
  12. import argparse
  13. from pathlib import Path
  14. from typing import Dict, List, Any
  15. # 接口配置
  16. QUERY_URL = "https://mkapi2.dfcfs.com/finskillshub/api/claw/self-select/get"
  17. MANAGE_URL = "https://mkapi2.dfcfs.com/finskillshub/api/claw/self-select/manage"
  18. def safe_filename(s: str, max_len: int = 80) -> str:
  19. """Convert query to safe filename - same as other mx_* skills"""
  20. s = s.replace(" ", "_").replace("/", "_").replace("\\", "_").replace(":", "_")
  21. s = s.replace("*", "_").replace("?", "_").replace('"', "_").replace("<", "_").replace(">", "_")
  22. s = s.replace("|", "_")[:max_len]
  23. return s or "query"
  24. def get_apikey() -> str:
  25. """从环境变量获取apikey"""
  26. apikey = os.environ.get("MX_APIKEY", "")
  27. if not apikey:
  28. # 尝试从.env文件读取
  29. env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env")
  30. if os.path.exists(env_file):
  31. try:
  32. with open(env_file, "r") as f:
  33. for line in f:
  34. line = line.strip()
  35. if line and not line.startswith("#") and "=" in line:
  36. key, value = line.split("=", 1)
  37. if key.strip() == "MX_APIKEY":
  38. apikey = value.strip()
  39. break
  40. except Exception as e:
  41. print(f"⚠️ 读取.env文件失败: {e}", file=sys.stderr)
  42. if not apikey:
  43. print("❌ 未找到MX_APIKEY,请设置环境变量:", file=sys.stderr)
  44. print(" export MX_APIKEY=your_apikey", file=sys.stderr)
  45. raise RuntimeError("MX_APIKEY 未配置")
  46. return apikey
  47. def query_self_select(apikey: str) -> Dict:
  48. """查询自选股列表"""
  49. headers = {
  50. "Content-Type": "application/json",
  51. "apikey": apikey
  52. }
  53. response = requests.post(QUERY_URL, headers=headers, json={}, timeout=30)
  54. response.raise_for_status()
  55. return response.json()
  56. def manage_self_select(apikey: str, query: str) -> Dict:
  57. """添加或删除自选股"""
  58. headers = {
  59. "Content-Type": "application/json",
  60. "apikey": apikey
  61. }
  62. data = {
  63. "query": query
  64. }
  65. response = requests.post(MANAGE_URL, headers=headers, json=data, timeout=30)
  66. response.raise_for_status()
  67. return response.json()
  68. def format_query_result(result: Dict, output_dir: Path):
  69. """格式化查询结果输出 + 保存 CSV"""
  70. if result.get("status") != 0 and result.get("code") != 0:
  71. print(f"❌ 查询失败: {result.get('message', '未知错误')}", file=sys.stderr)
  72. return
  73. data = result.get("data", {})
  74. all_results = data.get("allResults", {})
  75. result_data = all_results.get("result", {})
  76. columns = result_data.get("columns", [])
  77. data_list = result_data.get("dataList", [])
  78. if not data_list:
  79. print("ℹ️ 自选股列表为空,请到东方财富App查询")
  80. return
  81. # 提取需要显示的字段
  82. display_fields = [
  83. ("SECURITY_CODE", "股票代码", 8),
  84. ("SECURITY_SHORT_NAME", "股票名称", 8),
  85. ("NEWEST_PRICE", "最新价(元)", 10),
  86. ("CHG", "涨跌幅(%)", 10),
  87. ("PCHG", "涨跌额(元)", 10),
  88. ("010000_TURNOVER_RATE", "换手率(%)", 10),
  89. ("010000_LIANGBI", "量比", 6)
  90. ]
  91. # 打印表头
  92. print("📊 我的自选股列表")
  93. print("=" * 100)
  94. header = " | ".join([f"{name:<{width}}" for _, name, width in display_fields])
  95. print(header)
  96. print("-" * 100)
  97. # 打印数据行
  98. for stock in data_list:
  99. row = []
  100. for key, _, width in display_fields:
  101. value = stock.get(key, "-")
  102. # 处理涨跌幅颜色
  103. if key == "CHG" and value != "-":
  104. try:
  105. chg = float(value)
  106. if chg > 0:
  107. value = f"+{value}%"
  108. elif chg < 0:
  109. value = f"{value}%"
  110. else:
  111. value = f"{value}%"
  112. except:
  113. pass
  114. row.append(f"{str(value):<{width}}")
  115. print(" | ".join(row))
  116. print("-" * 100)
  117. print(f"共 {len(data_list)} 只自选股")
  118. # 保存到 CSV - same output convention as other mx_* skills
  119. safe_name = "我的自选股列表"
  120. csv_path = output_dir / f"mx_zixuan_{safe_filename(safe_name)}.csv"
  121. # Build CSV header from all columns (Chinese names)
  122. fieldnames = []
  123. csv_rows = []
  124. column_name_map = {}
  125. for col in columns:
  126. title = col.get("title", col.get("key", "unknown"))
  127. key = col.get("key", "unknown")
  128. column_name_map[key] = title
  129. fieldnames.append(title)
  130. for stock in data_list:
  131. csv_row = {}
  132. for key, title in column_name_map.items():
  133. csv_row[title] = stock.get(key, "")
  134. csv_rows.append(csv_row)
  135. with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
  136. writer = csv.DictWriter(f, fieldnames=fieldnames)
  137. writer.writeheader()
  138. for row in csv_rows:
  139. writer.writerow(row)
  140. # Save raw JSON
  141. json_path = output_dir / f"mx_zixuan_{safe_filename(safe_name)}_raw.json"
  142. with open(json_path, "w", encoding="utf-8") as f:
  143. json.dump(result, f, ensure_ascii=False, indent=2)
  144. print(f"\n✅ CSV 已保存: {csv_path}")
  145. print(f"📄 原始JSON: {json_path}")
  146. def format_manage_result(result: Dict, query: str):
  147. """格式化操作结果输出"""
  148. if result.get("status") != 0 and result.get("code") != 0:
  149. print(f"❌ 操作失败: {result.get('message', '未知错误')}", file=sys.stderr)
  150. return
  151. print(f"✅ 操作成功: {result.get('message', '已完成')}")
  152. def main():
  153. parser = argparse.ArgumentParser(description="妙想自选管理工具 (mx_zixuan)")
  154. parser.add_argument("command", nargs="?", help="命令: query/add/delete 或自然语言指令")
  155. parser.add_argument("stock", nargs="?", help="股票名称或代码(可选)")
  156. parser.add_argument("--output-dir", dest="output_dir", help=f"输出目录,默认 {Path('/root/.openclaw/workspace/mx_data/output')}")
  157. args = parser.parse_args()
  158. # Default output directory
  159. default_output = Path("/root/.openclaw/workspace/mx_data/output")
  160. output_dir = Path(args.output_dir) if args.output_dir else default_output
  161. output_dir.mkdir(parents=True, exist_ok=True)
  162. apikey = get_apikey()
  163. # 处理命令
  164. if not args.command:
  165. print("ℹ️ 使用方式:", file=sys.stderr)
  166. print(" 查询自选股: python scripts/mx_zixuan.py query", file=sys.stderr)
  167. print(" 添加自选股: python scripts/mx_zixuan.py add 贵州茅台", file=sys.stderr)
  168. print(" 删除自选股: python scripts/mx_zixuan.py delete 贵州茅台", file=sys.stderr)
  169. print(" 自然语言: python scripts/mx_zixuan.py \"把贵州茅台加入自选\"", file=sys.stderr)
  170. print(f"\n 默认输出目录: {output_dir}", file=sys.stderr)
  171. sys.exit(1)
  172. command = args.command.lower()
  173. if command in ["query", "list", "查询", "列表"]:
  174. # 查询自选股
  175. result = query_self_select(apikey)
  176. format_query_result(result, output_dir)
  177. elif command in ["add", "添加", "增加"] and args.stock:
  178. # 添加股票
  179. query = f"把{args.stock}添加到我的自选股列表"
  180. result = manage_self_select(apikey, query)
  181. format_manage_result(result, query)
  182. elif command in ["delete", "del", "remove", "删除", "移除"] and args.stock:
  183. # 删除股票
  184. query = f"把{args.stock}从我的自选股列表删除"
  185. result = manage_self_select(apikey, query)
  186. format_manage_result(result, query)
  187. else:
  188. # 自然语言处理
  189. query = args.command
  190. if args.stock:
  191. query += " " + args.stock
  192. # 判断是查询还是管理操作
  193. if any(keyword in query for keyword in ["查询", "列表", "我的自选", "有哪些"]):
  194. result = query_self_select(apikey)
  195. format_query_result(result, output_dir)
  196. else:
  197. result = manage_self_select(apikey, query)
  198. format_manage_result(result, query)
  199. if __name__ == "__main__":
  200. main()