| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- #!/usr/bin/env python3
- """
- 妙想自选管理skill (mx_zixuan)
- 支持查询、添加、删除东方财富自选股
- 统一输出到 /root/.openclaw/workspace/mx_data/output/
- """
- import os
- import sys
- import csv
- import json
- import requests
- import argparse
- from pathlib import Path
- from typing import Dict, List, Any
- # 接口配置
- QUERY_URL = "https://mkapi2.dfcfs.com/finskillshub/api/claw/self-select/get"
- MANAGE_URL = "https://mkapi2.dfcfs.com/finskillshub/api/claw/self-select/manage"
- def safe_filename(s: str, max_len: int = 80) -> str:
- """Convert query to safe filename - same as other mx_* skills"""
- s = s.replace(" ", "_").replace("/", "_").replace("\\", "_").replace(":", "_")
- s = s.replace("*", "_").replace("?", "_").replace('"', "_").replace("<", "_").replace(">", "_")
- s = s.replace("|", "_")[:max_len]
- return s or "query"
- def get_apikey() -> str:
- """从环境变量获取apikey"""
- apikey = os.environ.get("MX_APIKEY", "")
- if not apikey:
- # 尝试从.env文件读取
- env_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env")
- if os.path.exists(env_file):
- try:
- with open(env_file, "r") as f:
- for line in f:
- line = line.strip()
- if line and not line.startswith("#") and "=" in line:
- key, value = line.split("=", 1)
- if key.strip() == "MX_APIKEY":
- apikey = value.strip()
- break
- except Exception as e:
- print(f"⚠️ 读取.env文件失败: {e}", file=sys.stderr)
-
- if not apikey:
- print("❌ 未找到MX_APIKEY,请设置环境变量:", file=sys.stderr)
- print(" export MX_APIKEY=your_apikey", file=sys.stderr)
- raise RuntimeError("MX_APIKEY 未配置")
-
- return apikey
- def query_self_select(apikey: str) -> Dict:
- """查询自选股列表"""
- headers = {
- "Content-Type": "application/json",
- "apikey": apikey
- }
-
- response = requests.post(QUERY_URL, headers=headers, json={}, timeout=30)
- response.raise_for_status()
- return response.json()
- def manage_self_select(apikey: str, query: str) -> Dict:
- """添加或删除自选股"""
- headers = {
- "Content-Type": "application/json",
- "apikey": apikey
- }
-
- data = {
- "query": query
- }
-
- response = requests.post(MANAGE_URL, headers=headers, json=data, timeout=30)
- response.raise_for_status()
- return response.json()
- def format_query_result(result: Dict, output_dir: Path):
- """格式化查询结果输出 + 保存 CSV"""
- if result.get("status") != 0 and result.get("code") != 0:
- print(f"❌ 查询失败: {result.get('message', '未知错误')}", file=sys.stderr)
- return
-
- data = result.get("data", {})
- all_results = data.get("allResults", {})
- result_data = all_results.get("result", {})
- columns = result_data.get("columns", [])
- data_list = result_data.get("dataList", [])
-
- if not data_list:
- print("ℹ️ 自选股列表为空,请到东方财富App查询")
- return
-
- # 提取需要显示的字段
- display_fields = [
- ("SECURITY_CODE", "股票代码", 8),
- ("SECURITY_SHORT_NAME", "股票名称", 8),
- ("NEWEST_PRICE", "最新价(元)", 10),
- ("CHG", "涨跌幅(%)", 10),
- ("PCHG", "涨跌额(元)", 10),
- ("010000_TURNOVER_RATE", "换手率(%)", 10),
- ("010000_LIANGBI", "量比", 6)
- ]
-
- # 打印表头
- print("📊 我的自选股列表")
- print("=" * 100)
- header = " | ".join([f"{name:<{width}}" for _, name, width in display_fields])
- print(header)
- print("-" * 100)
-
- # 打印数据行
- for stock in data_list:
- row = []
- for key, _, width in display_fields:
- value = stock.get(key, "-")
- # 处理涨跌幅颜色
- if key == "CHG" and value != "-":
- try:
- chg = float(value)
- if chg > 0:
- value = f"+{value}%"
- elif chg < 0:
- value = f"{value}%"
- else:
- value = f"{value}%"
- except:
- pass
- row.append(f"{str(value):<{width}}")
- print(" | ".join(row))
-
- print("-" * 100)
- print(f"共 {len(data_list)} 只自选股")
-
- # 保存到 CSV - same output convention as other mx_* skills
- safe_name = "我的自选股列表"
- csv_path = output_dir / f"mx_zixuan_{safe_filename(safe_name)}.csv"
-
- # Build CSV header from all columns (Chinese names)
- fieldnames = []
- csv_rows = []
- column_name_map = {}
- for col in columns:
- title = col.get("title", col.get("key", "unknown"))
- key = col.get("key", "unknown")
- column_name_map[key] = title
- fieldnames.append(title)
-
- for stock in data_list:
- csv_row = {}
- for key, title in column_name_map.items():
- csv_row[title] = stock.get(key, "")
- csv_rows.append(csv_row)
-
- with open(csv_path, "w", newline="", encoding="utf-8-sig") as f:
- writer = csv.DictWriter(f, fieldnames=fieldnames)
- writer.writeheader()
- for row in csv_rows:
- writer.writerow(row)
-
- # Save raw JSON
- json_path = output_dir / f"mx_zixuan_{safe_filename(safe_name)}_raw.json"
- with open(json_path, "w", encoding="utf-8") as f:
- json.dump(result, f, ensure_ascii=False, indent=2)
-
- print(f"\n✅ CSV 已保存: {csv_path}")
- print(f"📄 原始JSON: {json_path}")
- def format_manage_result(result: Dict, query: str):
- """格式化操作结果输出"""
- if result.get("status") != 0 and result.get("code") != 0:
- print(f"❌ 操作失败: {result.get('message', '未知错误')}", file=sys.stderr)
- return
-
- print(f"✅ 操作成功: {result.get('message', '已完成')}")
- def main():
- parser = argparse.ArgumentParser(description="妙想自选管理工具 (mx_zixuan)")
- parser.add_argument("command", nargs="?", help="命令: query/add/delete 或自然语言指令")
- parser.add_argument("stock", nargs="?", help="股票名称或代码(可选)")
- parser.add_argument("--output-dir", dest="output_dir", help=f"输出目录,默认 {Path('/root/.openclaw/workspace/mx_data/output')}")
-
- args = parser.parse_args()
-
- # Default output directory
- default_output = Path("/root/.openclaw/workspace/mx_data/output")
- output_dir = Path(args.output_dir) if args.output_dir else default_output
- output_dir.mkdir(parents=True, exist_ok=True)
-
- apikey = get_apikey()
-
- # 处理命令
- if not args.command:
- print("ℹ️ 使用方式:", file=sys.stderr)
- print(" 查询自选股: python scripts/mx_zixuan.py query", file=sys.stderr)
- print(" 添加自选股: python scripts/mx_zixuan.py add 贵州茅台", file=sys.stderr)
- print(" 删除自选股: python scripts/mx_zixuan.py delete 贵州茅台", file=sys.stderr)
- print(" 自然语言: python scripts/mx_zixuan.py \"把贵州茅台加入自选\"", file=sys.stderr)
- print(f"\n 默认输出目录: {output_dir}", file=sys.stderr)
- sys.exit(1)
-
- command = args.command.lower()
-
- if command in ["query", "list", "查询", "列表"]:
- # 查询自选股
- result = query_self_select(apikey)
- format_query_result(result, output_dir)
- elif command in ["add", "添加", "增加"] and args.stock:
- # 添加股票
- query = f"把{args.stock}添加到我的自选股列表"
- result = manage_self_select(apikey, query)
- format_manage_result(result, query)
- elif command in ["delete", "del", "remove", "删除", "移除"] and args.stock:
- # 删除股票
- query = f"把{args.stock}从我的自选股列表删除"
- result = manage_self_select(apikey, query)
- format_manage_result(result, query)
- else:
- # 自然语言处理
- query = args.command
- if args.stock:
- query += " " + args.stock
-
- # 判断是查询还是管理操作
- if any(keyword in query for keyword in ["查询", "列表", "我的自选", "有哪些"]):
- result = query_self_select(apikey)
- format_query_result(result, output_dir)
- else:
- result = manage_self_select(apikey, query)
- format_manage_result(result, query)
- if __name__ == "__main__":
- main()
|