|
|
|
|
@ -1,27 +1,345 @@ |
|
|
|
|
# -*- coding: utf-8 -*- |
|
|
|
|
import xmlrpc.client |
|
|
|
|
import os |
|
|
|
|
import sys |
|
|
|
|
import time |
|
|
|
|
import json |
|
|
|
|
import httpx |
|
|
|
|
from datetime import datetime, timedelta |
|
|
|
|
|
|
|
|
|
# Odoo 连接配置 |
|
|
|
|
|
|
|
|
|
class OdooClient: |
|
|
|
|
"""Odoo JSON-RPC 客户端类""" |
|
|
|
|
|
|
|
|
|
def __init__(self, url, db_name, username, password): |
|
|
|
|
""" |
|
|
|
|
初始化 Odoo 客户端并自动登录 |
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
url: Odoo 服务器地址 |
|
|
|
|
db_name: 数据库名称 |
|
|
|
|
username: 用户名 |
|
|
|
|
password: 密码 |
|
|
|
|
""" |
|
|
|
|
self.url = url |
|
|
|
|
self.db_name = db_name |
|
|
|
|
self.username = username |
|
|
|
|
self.password = password |
|
|
|
|
self.client = None |
|
|
|
|
self.uid = None |
|
|
|
|
|
|
|
|
|
# 自动登录 |
|
|
|
|
self.login() |
|
|
|
|
|
|
|
|
|
def login(self): |
|
|
|
|
"""登录并获取 uid 和 client""" |
|
|
|
|
try: |
|
|
|
|
self.client = httpx.Client(timeout=30.0) |
|
|
|
|
|
|
|
|
|
# Odoo 登录 |
|
|
|
|
payload = { |
|
|
|
|
"jsonrpc": "2.0", |
|
|
|
|
"method": "call", |
|
|
|
|
"params": { |
|
|
|
|
"service": "common", |
|
|
|
|
"method": "login", |
|
|
|
|
"args": [self.db_name, self.username, self.password] |
|
|
|
|
}, |
|
|
|
|
"id": 1 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
response = self.client.post(f"{self.url}/jsonrpc", json=payload) |
|
|
|
|
result = response.json() |
|
|
|
|
|
|
|
|
|
# 检查是否有错误 |
|
|
|
|
if "error" in result: |
|
|
|
|
raise Exception(f"登录失败: {result['error']}") |
|
|
|
|
|
|
|
|
|
# Odoo 的登录响应中,result 直接就是 uid |
|
|
|
|
self.uid = result.get("result") |
|
|
|
|
|
|
|
|
|
if not self.uid: |
|
|
|
|
raise Exception("登录失败:未获取到UID") |
|
|
|
|
|
|
|
|
|
print(f"登录成功,UID: {self.uid}") |
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
|
print(f"登录失败: {e}") |
|
|
|
|
if self.client: |
|
|
|
|
self.client.close() |
|
|
|
|
self.client = None |
|
|
|
|
self.uid = None |
|
|
|
|
raise |
|
|
|
|
|
|
|
|
|
def logout(self): |
|
|
|
|
"""退出登录并关闭连接""" |
|
|
|
|
if self.client: |
|
|
|
|
self.client.close() |
|
|
|
|
self.client = None |
|
|
|
|
self.uid = None |
|
|
|
|
print("已退出") |
|
|
|
|
|
|
|
|
|
def search_data(self, model, domain, fields=None, order=None, limit=None): |
|
|
|
|
""" |
|
|
|
|
通用搜索方法 |
|
|
|
|
|
|
|
|
|
Args: |
|
|
|
|
model: 模型名称 |
|
|
|
|
domain: 搜索条件列表 |
|
|
|
|
fields: 需要返回的字段列表 |
|
|
|
|
order: 排序规则 |
|
|
|
|
limit: 返回记录数量限制 |
|
|
|
|
|
|
|
|
|
Returns: |
|
|
|
|
查询结果列表,失败返回 None |
|
|
|
|
""" |
|
|
|
|
if not self.client or not self.uid: |
|
|
|
|
raise Exception("未登录或连接已断开") |
|
|
|
|
|
|
|
|
|
# 构建参数 |
|
|
|
|
args = [domain] |
|
|
|
|
if fields: |
|
|
|
|
args.append(fields) |
|
|
|
|
|
|
|
|
|
kwargs = {} |
|
|
|
|
if order: |
|
|
|
|
kwargs['order'] = order |
|
|
|
|
if limit: |
|
|
|
|
kwargs['limit'] = limit |
|
|
|
|
|
|
|
|
|
payload = { |
|
|
|
|
"jsonrpc": "2.0", |
|
|
|
|
"method": "call", |
|
|
|
|
"params": { |
|
|
|
|
"service": "object", |
|
|
|
|
"method": "execute_kw", |
|
|
|
|
"args": [ |
|
|
|
|
self.db_name, |
|
|
|
|
self.uid, |
|
|
|
|
self.password, |
|
|
|
|
model, |
|
|
|
|
"search_read", |
|
|
|
|
args, |
|
|
|
|
kwargs |
|
|
|
|
] |
|
|
|
|
}, |
|
|
|
|
"id": 2 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
response = self.client.post(f"{self.url}/jsonrpc", json=payload) |
|
|
|
|
result = response.json() |
|
|
|
|
|
|
|
|
|
if "error" in result: |
|
|
|
|
print(f"查询失败: {result['error']}") |
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
return result.get("result", []) |
|
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
|
print(f"查询异常: {e}") |
|
|
|
|
return None |
|
|
|
|
|
|
|
|
|
def __enter__(self): |
|
|
|
|
"""上下文管理器入口""" |
|
|
|
|
return self |
|
|
|
|
|
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb): |
|
|
|
|
"""上下文管理器出口,自动登出""" |
|
|
|
|
self.logout() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 使用 OdooClient 类重构后的函数 |
|
|
|
|
def fetch_local_performance(odoo_client, model, domain, fields, limit): |
|
|
|
|
"""获取本地表现数据""" |
|
|
|
|
# 执行搜索 |
|
|
|
|
result = odoo_client.search_data(model=model, domain=domain, fields=fields, order="id desc", limit=limit) |
|
|
|
|
|
|
|
|
|
if result: |
|
|
|
|
return result |
|
|
|
|
else: |
|
|
|
|
print("未获取到数据") |
|
|
|
|
exit(1) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def deconstruction_data(all_data): |
|
|
|
|
"""解构并输出数据""" |
|
|
|
|
for idx, data in enumerate(all_data): |
|
|
|
|
data = json.loads(data.get('performance')) |
|
|
|
|
print(f"\n{'=' * 60}") |
|
|
|
|
print(f"策略 #{idx + 1}") |
|
|
|
|
print(f"{'=' * 60}") |
|
|
|
|
|
|
|
|
|
# 基础信息 |
|
|
|
|
alpha_id = data.get("id") |
|
|
|
|
alpha_type = data.get("type") |
|
|
|
|
author = data.get("author") |
|
|
|
|
name = data.get("name") |
|
|
|
|
|
|
|
|
|
print(f"ID: {alpha_id}") |
|
|
|
|
print(f"类型: {alpha_type}") |
|
|
|
|
print(f"作者: {author}") |
|
|
|
|
print(f"名称: {name if name else '未命名'}") |
|
|
|
|
|
|
|
|
|
# Settings 配置 |
|
|
|
|
settings = data.get("settings", {}) |
|
|
|
|
print(f"\n【配置信息】") |
|
|
|
|
print(f" 品种类型: {settings.get('instrumentType')}") |
|
|
|
|
print(f" 地区: {settings.get('region')}") |
|
|
|
|
print(f" 股票池: {settings.get('universe')}") |
|
|
|
|
print(f" 延迟: {settings.get('delay')}") |
|
|
|
|
print(f" 衰减: {settings.get('decay')}") |
|
|
|
|
print(f" 中性化: {settings.get('neutralization')}") |
|
|
|
|
print(f" 截断: {settings.get('truncation')}") |
|
|
|
|
print(f" 时间区间: {settings.get('startDate')} -> {settings.get('endDate')}") |
|
|
|
|
|
|
|
|
|
# 因子代码 |
|
|
|
|
regular = data.get('regular', {}) |
|
|
|
|
code = regular.get('code') |
|
|
|
|
description = regular.get('description') |
|
|
|
|
|
|
|
|
|
print(f"\n【因子代码】") |
|
|
|
|
print(f" Code: {code}") |
|
|
|
|
if description: |
|
|
|
|
print(f" Desc: {description}") |
|
|
|
|
print(f" 操作符数量: {regular.get('operatorCount')}") |
|
|
|
|
|
|
|
|
|
# 样本内(IS)回测结果 - 重点 |
|
|
|
|
in_sample = data.get('is', {}) |
|
|
|
|
|
|
|
|
|
if in_sample: |
|
|
|
|
print(f"\n【样本内回测结果 (IS)】") |
|
|
|
|
print(f" 📉 PnL: {in_sample.get('pnl', 0):,.0f}") |
|
|
|
|
print(f" 📊 总收益率: {in_sample.get('returns', 0) * 100:.2f}%") |
|
|
|
|
print(f" 🎯 夏普比率: {in_sample.get('sharpe', 0):.2f}") |
|
|
|
|
print(f" 💪 Fitness: {in_sample.get('fitness', 0):.2f}") |
|
|
|
|
print(f" 📉 最大回撤: {in_sample.get('drawdown', 0) * 100:.2f}%") |
|
|
|
|
print(f" 🔄 换手率: {in_sample.get('turnover', 0) * 100:.2f}%") |
|
|
|
|
print(f" 📈 多头持仓数: {in_sample.get('longCount', 0)}") |
|
|
|
|
print(f" 📉 空头持仓数: {in_sample.get('shortCount', 0)}") |
|
|
|
|
print(f" 💰 保证金: {in_sample.get('margin', 0):.6f}") |
|
|
|
|
print(f" 📅 回测区间: {in_sample.get('startDate')} -> {in_sample.get('endDate')}") |
|
|
|
|
|
|
|
|
|
# 检查项结果 - 排序后输出 |
|
|
|
|
checks = in_sample.get('checks', []) |
|
|
|
|
if checks: |
|
|
|
|
print(f"\n 【检查项】(已排序:PASS → WARNING → FAIL → PENDING)") |
|
|
|
|
|
|
|
|
|
# 定义排序优先级 |
|
|
|
|
def sort_priority(check): |
|
|
|
|
result = check.get('result') |
|
|
|
|
priority_order = { |
|
|
|
|
'PASS': 1, |
|
|
|
|
'WARNING': 2, |
|
|
|
|
'FAIL': 3, |
|
|
|
|
'PENDING': 4 |
|
|
|
|
} |
|
|
|
|
return priority_order.get(result, 5) |
|
|
|
|
|
|
|
|
|
# 排序 |
|
|
|
|
sorted_checks = sorted(checks, key=sort_priority) |
|
|
|
|
|
|
|
|
|
# 输出排序后的检查项 |
|
|
|
|
for check in sorted_checks: |
|
|
|
|
name_check = check.get('name') |
|
|
|
|
result = check.get('result') |
|
|
|
|
limit = check.get('limit') |
|
|
|
|
value = check.get('value') |
|
|
|
|
|
|
|
|
|
# 根据结果用不同符号标记 |
|
|
|
|
if result == 'PASS': |
|
|
|
|
symbol = '✅' |
|
|
|
|
elif result == 'FAIL': |
|
|
|
|
symbol = '❌' |
|
|
|
|
elif result == 'WARNING': |
|
|
|
|
symbol = '⚠️' |
|
|
|
|
else: |
|
|
|
|
symbol = '⏳' |
|
|
|
|
|
|
|
|
|
if limit is not None and value is not None: |
|
|
|
|
print(f" {symbol} {name_check}: {result} (要求: {limit}, 实际: {value})") |
|
|
|
|
else: |
|
|
|
|
print(f" {symbol} {name_check}: {result}") |
|
|
|
|
else: |
|
|
|
|
print(f"\n【样本内回测结果 (IS)】") |
|
|
|
|
print(f" ⚠️ 无数据(因子可能在样本内阶段就失败了)") |
|
|
|
|
|
|
|
|
|
# 样本外(OS)结果 - 通常为空表示策略无效 |
|
|
|
|
out_sample = data.get('os', {}) |
|
|
|
|
train = data.get('train', {}) |
|
|
|
|
test = data.get('test', {}) |
|
|
|
|
|
|
|
|
|
if out_sample: |
|
|
|
|
print(f"\n【样本外回测结果 (OS)】") |
|
|
|
|
print(f" 夏普比率: {out_sample.get('sharpe', 0):.2f}") |
|
|
|
|
else: |
|
|
|
|
print(f"\n【样本外回测结果 (OS)】⚠️ 为空 - 因子可能未通过样本内测试") |
|
|
|
|
|
|
|
|
|
if train: |
|
|
|
|
print(f"\n【训练集结果 (Train)】") |
|
|
|
|
print(f" 夏普比率: {train.get('sharpe', 0):.2f}") |
|
|
|
|
|
|
|
|
|
if test: |
|
|
|
|
print(f"\n【测试集结果 (Test)】") |
|
|
|
|
print(f" 夏普比率: {test.get('sharpe', 0):.2f}") |
|
|
|
|
|
|
|
|
|
# 主题和竞赛信息 |
|
|
|
|
themes = data.get('themes', []) |
|
|
|
|
if themes: |
|
|
|
|
print(f"\n【关联主题】") |
|
|
|
|
for theme in themes: |
|
|
|
|
print(f" - {theme.get('name')} (乘数: {theme.get('multiplier')}x)") |
|
|
|
|
|
|
|
|
|
competitions = data.get('competitions', []) |
|
|
|
|
if competitions: |
|
|
|
|
print(f"\n【关联竞赛】") |
|
|
|
|
for comp in competitions: |
|
|
|
|
print(f" - {comp.get('name')}") |
|
|
|
|
|
|
|
|
|
# 状态信息 |
|
|
|
|
print(f"\n【状态】") |
|
|
|
|
print(f" 阶段: {data.get('stage')}") |
|
|
|
|
print(f" 状态: {data.get('status')}") |
|
|
|
|
print(f" 是否收藏: {data.get('favorite')}") |
|
|
|
|
print(f" 创建时间: {data.get('dateCreated')}") |
|
|
|
|
|
|
|
|
|
# 如果有分类信息 |
|
|
|
|
classifications = data.get('classifications', []) |
|
|
|
|
if classifications: |
|
|
|
|
print(f"\n【分类】") |
|
|
|
|
for cls in classifications: |
|
|
|
|
print(f" - {cls.get('name')}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 使用示例 |
|
|
|
|
if __name__ == "__main__": |
|
|
|
|
# ============================== Odoo 连接配置 ==================================== |
|
|
|
|
ODOO_URL = "http://192.168.31.41:32000" |
|
|
|
|
DB_NAME = "quantify" |
|
|
|
|
USERNAME = "rpc" |
|
|
|
|
PASSWORD = "aaaAAA111" |
|
|
|
|
|
|
|
|
|
# RPC 客户端 |
|
|
|
|
common = xmlrpc.client.ServerProxy('{}/xmlrpc/2/common'.format(ODOO_URL)) |
|
|
|
|
uid = common.authenticate(DB_NAME, USERNAME, PASSWORD, {}) |
|
|
|
|
# ============================== 搜索设置 ==================================== |
|
|
|
|
days = 7 |
|
|
|
|
now = datetime.now() |
|
|
|
|
today_zero = now.replace(hour=0, minute=0, second=0, microsecond=0) |
|
|
|
|
days_ago_zero = today_zero - timedelta(days=days) |
|
|
|
|
# 模型名称 |
|
|
|
|
model = "alpha.expression.line" |
|
|
|
|
# 搜索条件 |
|
|
|
|
domain = [('performance', '!=', '{}'), ('write_date', '>=', days_ago_zero.strftime('%Y-%m-%d %H:%M:%S'))] |
|
|
|
|
# 搜索字段 |
|
|
|
|
fields = ['id', 'performance'] |
|
|
|
|
# 搜索数量限制 |
|
|
|
|
limit = 1 |
|
|
|
|
|
|
|
|
|
try: |
|
|
|
|
odoo = OdooClient(ODOO_URL, DB_NAME, USERNAME, PASSWORD) |
|
|
|
|
|
|
|
|
|
all_data = fetch_local_performance(odoo, model, domain, fields, limit) |
|
|
|
|
deconstruction_data(all_data) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 搜索 alpha.expression.line 模型, 条件 字段 performance 不为空, 并按 id 升序排序 |
|
|
|
|
domain = [('performance', '!=', False)] |
|
|
|
|
order = 'id asc' |
|
|
|
|
expression_line_ids = xmlrpc.client.ServerProxy('{}/xmlrpc/2/object'.format(ODOO_URL)).execute_kw( |
|
|
|
|
DB_NAME, uid, PASSWORD, 'alpha.expression.line', 'search_read', |
|
|
|
|
[domain, ['id', 'performance', 'name', 'expression_id']], |
|
|
|
|
{'order': order} |
|
|
|
|
) |
|
|
|
|
except Exception as e: |
|
|
|
|
print(f"程序执行失败: {e}") |
|
|
|
|
|
|
|
|
|
print(expression_line_ids) |
|
|
|
|
with OdooClient(ODOO_URL, DB_NAME, USERNAME, PASSWORD) as odoo: |
|
|
|
|
all_data = fetch_local_performance(odoo) |
|
|
|
|
deconstruction_data(all_data) |
|
|
|
|
|