File size: 14,236 Bytes
46d05c2 7c58c89 b1165b6 46d05c2 0ca00b4 88e9ec1 0ca00b4 2d9bd9a 0ca00b4 88e9ec1 0ca00b4 88e9ec1 0ca00b4 b1165b6 0ca00b4 b1165b6 7c58c89 46d05c2 7c58c89 f2700f2 7c58c89 b1165b6 7c58c89 b1165b6 7c58c89 b1165b6 7c58c89 b1165b6 46d05c2 b1165b6 46d05c2 b1165b6 46d05c2 7c58c89 b1165b6 7c58c89 b1165b6 7c58c89 46d05c2 7c58c89 46d05c2 7c58c89 4f897f3 b1165b6 7c58c89 b1165b6 7c58c89 46d05c2 7c58c89 b1165b6 7c58c89 46d05c2 b1165b6 88e9ec1 f2700f2 94ceba0 46d05c2 88e9ec1 b1165b6 88e9ec1 7c58c89 88e9ec1 0ca00b4 7c58c89 88e9ec1 b1165b6 7c58c89 b1165b6 7c58c89 f2700f2 b1165b6 092d89b f2700f2 092d89b 4f897f3 46d05c2 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 | # -*- coding: utf-8 -*-
import gradio as gr
import asyncio
import threading
import json
import sys
import os
import importlib
import random
from datetime import datetime
# ========== 顶栏主题与样式(浅色、中文) ==========
theme = gr.themes.Soft()
css = """
:root { --radius: 14px; }
#topbar {
display:flex; align-items:center; justify-content:space-between;
padding:12px 16px;
background:linear-gradient(90deg,#f8fafc,#eef2ff,#e0f2fe);
color:#0f172a;
border:1px solid #e5e7eb;
border-radius: var(--radius);
box-shadow: 0 4px 14px rgba(15,23,42,.06);
margin-bottom:12px;
width: 100%;
box-sizing: border-box;
}
#topbar h1 { font-size:18px; margin:0; font-weight:800; letter-spacing:.5px }
#topbar .right { display:flex; gap:8px; align-items:center }
.badge { padding:4px 10px; border-radius:999px; background:#e0f2fe; color:#0369a1; font-weight:700; border:1px solid #bae6fd }
.footer-note { opacity:.7; font-size:12px; }
"""
# ========== 路径修复与导入别名(兼容 from 策略基类 import 策略接口) ==========
def _修复导入路径与别名():
root = os.path.dirname(os.path.abspath(__file__))
if root not in sys.path:
sys.path.insert(0, root)
for d in ['插件_引擎', '插件_交易', '插件_行情', '插件_资产']:
p = os.path.join(root, d)
if os.path.isdir(p) and p not in sys.path:
sys.path.insert(0, p)
try:
mod = importlib.import_module('插件_交易.策略基类')
if '策略基类' not in sys.modules:
sys.modules['策略基类'] = mod
except Exception:
pass
_修复导入路径与别名()
# ========== 内置“轻量引擎”(后端缺失时自动兜底) ==========
class _LiteExchange:
def __init__(self):
self.净值 = 1.0
self.峰值 = 1.0
self.回撤 = 0.0
def tick(self):
drift = 0.0002
noise = random.gauss(0, 0.002)
self.净值 = max(0.5, self.净值 * (1 + drift + noise))
self.峰值 = max(self.峰值, self.净值)
if self.峰值 > 0:
self.回撤 = (self.峰值 - self.净值) / self.峰值
class 轻量自适应策略:
def __init__(self, 名称: str, 参数: dict):
self.名称 = 名称
self.参数 = 参数
class 轻量引擎:
def __init__(self, 记录函数):
self._停 = False
self.交易所 = _LiteExchange()
self._log = 记录函数
self._策略 = {}
def 注册策略(self, 策略ID: str, 策略对象, 合约: str, 周期分钟: int):
self._策略[策略ID] = {"对象": 策略对象, "合约": 合约, "周期": 周期分钟}
self._log(f"[轻量引擎] 已注册策略: {策略ID} -> {合约} / {周期分钟}m")
async def 启动(self, 合约列表):
self._log("[轻量引擎] 启动(演示模式,无真实行情订阅)")
try:
i = 0
while not self._停:
self.交易所.tick()
i += 1
if i % 5 == 0:
self._log(f"[轻量引擎] 心跳 | 净值={self.交易所.净值:.4f} 回撤={self.交易所.回撤:.2%}")
await asyncio.sleep(2)
finally:
self._log("[轻量引擎] 已停止")
def 停止(self):
self._停 = True
# ========== 全局状态 ==========
引擎实例 = None
运行线程 = None
状态锁 = threading.Lock()
运行状态 = {
"运行中": False,
"合约": "",
"周期": 0,
"净值": 1.0,
"回撤": 0.0,
"日志": []
}
默认参数 = {
"ver": "v1",
"adx_thr": 25,
"atr_buf_tr": 0.1,
"rsi_low": 30,
"rsi_high": 70,
"mr_stop_atr": 2.0,
"trend_logic": "adx",
"alloc_mode": "fixed"
}
def 从参数推断周期及选中(JSON文本: str):
try:
d = json.loads(JSON文本)
except Exception:
d = {}
tf = None
for k in ('tf', 'period', '周期', 'bar', 'bar_min', 'timeframe', 'k', 'k_min'):
v = d.get(k)
if isinstance(v, (int, float)) and int(v) > 0:
tf = int(v); break
if isinstance(v, str) and v.strip().isdigit():
tf = int(v.strip()); break
if not tf:
tf = 60
base = [30, 60, 120, 240]
if tf not in base:
base.append(tf)
base = sorted(set(base))
return [str(x) for x in base], str(tf)
def 参数变更更新周期(参数JSON文本: str):
choices, value = 从参数推断周期及选中(参数JSON文本)
import gradio as gr
return gr.update(choices=choices, value=value)
def _记录(msg: str):
with 状态锁:
运行状态["日志"].append(f"[{datetime.now().strftime('%H:%M:%S')}] {msg}")
if len(运行状态["日志"]) > 500:
运行状态["日志"] = 运行状态["日志"][-300:]
# ========== 真实/轻量 引擎跑法 ==========
async def _用真实引擎运行(合约: str, 周期: int, 参数: dict):
global 引擎实例
_修复导入路径与别名()
from 插件_引擎.多策略引擎 import 多策略引擎
from 插件_交易.策略_自适应 import 自适应策略
引擎实例 = 多策略引擎()
策略 = 自适应策略(f"{合约}_{周期}分", 参数)
引擎实例.注册策略(f"{合约}_{周期}", 策略, 合约, int(周期))
_记录(f"引擎启动:{合约} / {周期} 分(真实引擎)")
await 引擎实例.启动([合约])
async def _用轻量引擎运行(合约: str, 周期: int, 参数: dict):
global 引擎实例
引擎实例 = 轻量引擎(_记录)
策略 = 轻量自适应策略(f"{合约}_{周期}分", 参数)
引擎实例.注册策略(f"{合约}_{周期}", 策略, 合约, int(周期))
_记录(f"引擎启动:{合约} / {周期} 分(轻量引擎:演示模式)")
await 引擎实例.启动([合约])
def _后台任务(合约: str, 周期: int, 参数: dict):
global 引擎实例
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
async def 主循环():
try:
await _用真实引擎运行(合约, 周期, 参数)
except Exception as e:
_记录(f"真实引擎启动失败,切换至内置轻量引擎。原因:{e}")
try:
await _用轻量引擎运行(合约, 周期, 参数)
except Exception as e2:
_记录(f"轻量引擎也启动失败:{e2}")
finally:
with 状态锁:
运行状态["运行中"] = False
_记录("引擎结束")
loop.run_until_complete(主循环())
finally:
try:
loop.run_until_complete(asyncio.sleep(0))
except Exception:
pass
loop.close()
# ========== 业务函数(与UI绑定) ==========
def 启动模拟(合约: str, 周期: str, 参数json: str):
global 运行线程
with 状态锁:
if 运行状态["运行中"]:
return f"已有任务运行中:{运行状态['合约']} / {运行状态['周期']}分", "\n".join(运行状态["日志"][-100:])
if 参数json.strip():
try:
参数 = json.loads(参数json)
except Exception as e:
return f"参数JSON格式错误:{e}", "\n".join(运行状态["日志"][-100:])
else:
参数 = 默认参数
try:
p = int(周期)
except:
return "周期必须是数字(30/60/120/240)", "\n".join(运行状态["日志"][-100:])
with 状态锁:
运行状态["运行中"] = True
运行状态["合约"] = 合约
运行状态["周期"] = p
运行状态["净值"] = 1.0
运行状态["回撤"] = 0.0
运行状态["日志"].clear()
_记录(f"准备启动:合约={合约} 周期={p} 参数={参数}")
运行线程 = threading.Thread(target=_后台任务, args=(合约, p, 参数), daemon=True)
运行线程.start()
return "模拟启动成功", "\n".join(运行状态["日志"][-100:])
def 停止模拟():
global 引擎实例
if 引擎实例 is None:
_记录("当前没有运行中的任务")
with 状态锁:
运行状态["运行中"] = False
return "当前没有运行中的任务"
try:
引擎实例.停止()
_记录("已请求停止(引擎将不再处理新信号/演示将退出)")
return "已发送停止信号"
except Exception as e:
_记录(f"停止失败:{e}")
return f"停止失败:{e}"
def 获取状态():
with 状态锁:
try:
if 运行状态["运行中"] and 引擎实例 is not None and hasattr(引擎实例, '交易所') and 引擎实例.交易所 is not None:
nv = float(getattr(引擎实例.交易所, "净值", 1.0) or 1.0)
dd = float(getattr(引擎实例.交易所, "回撤", 0.0) or 0.0)
运行状态["净值"] = nv
运行状态["回撤"] = dd
except Exception as e:
_记录(f"读取状态失败:{e}")
状态文本 = f"运行中: {运行状态['运行中']} | 合约: {运行状态['合约']} | 周期: {运行状态['周期']}m | 净值: {运行状态['净值']:.4f} | 回撤: {运行状态['回撤']:.2%}"
日志文本 = "\n".join(运行状态["日志"][-120:])
return 状态文本, 日志文本
# ========== 引入设置面板(来自 配置.py) ==========
try:
import 配置 as 配置模块
except Exception:
配置模块 = None
# ========== Gradio 界面 ==========
with gr.Blocks(title="模拟实盘(OKX 公共WS + 多策略引擎)", theme=theme, css=css) as demo:
# 给顶栏外层添加 prose / gr-prose 类,使其宽度与 Markdown 一致
gr.HTML(
'<div id="topbar"><h1>📈 模拟实盘控制台</h1><div class="right"><span class="badge">中文界面</span></div></div>',
elem_id="topbar_wrap",
elem_classes=["prose", "gr-prose"]
)
gr.Markdown("提示:点击“⚙️ 设置”进入配置页(上传CSV、参数加载/保存)。若后端模块缺失将自动进入演示引擎。")
with gr.Row():
with gr.Column(scale=7):
with gr.Group(visible=True) as 运行面板:
with gr.Row():
with gr.Column(scale=1):
合约输入 = gr.Textbox(label="合约ID", value="UXLINK-USDT-SWAP", placeholder="例:BTC-USDT-SWAP / ETH-USDT-SWAP")
周期选择 = gr.Radio(label="周期(分钟)", choices=["30", "60", "120", "240"], value="60")
参数输入 = gr.Textbox(label="策略参数(JSON)", value=json.dumps(默认参数, ensure_ascii=False, indent=2), lines=12)
with gr.Row():
启动按钮 = gr.Button("🚀 启动模拟", variant="primary")
停止按钮 = gr.Button("⏹️ 停止模拟", variant="stop")
刷新按钮 = gr.Button("🔄 刷新状态")
设置按钮 = gr.Button("⚙️ 设置", variant="secondary")
with gr.Column(scale=1):
状态显示 = gr.Textbox(label="运行状态", lines=2, interactive=False)
日志显示 = gr.Textbox(label="实时日志(最近120行)", lines=22, interactive=False)
with gr.Group(visible=False) as 设置面板:
返回按钮 = gr.Button("← 返回运行页", variant="secondary")
gr.Markdown("## 设置")
if 配置模块 is not None:
构件 = 配置模块.构建设置面板(容器=设置面板, 默认参数=默认参数, 记录函数=_记录)
if isinstance(构件, dict):
# 参数回填
if "参数编辑框" in 构件 and ("复制按钮" in 构件 or "复制参数按钮" in 构件):
回填参数按钮 = 构件.get("复制按钮") or 构件.get("复制参数按钮")
回填参数按钮.click(lambda s: s, inputs=构件["参数编辑框"], outputs=参数输入)
# 合约回填
if "回填合约按钮" in 构件 and "合约ID框" in 构件:
构件["回填合约按钮"].click(lambda s: s.strip(), inputs=构件["合约ID框"], outputs=合约输入)
else:
gr.Markdown("未找到 配置.py,无法加载设置面板。")
with gr.Column(scale=5):
gr.Markdown("### 使用说明")
gr.Markdown("- 在设置页上传CSV并完成列映射与合约登记\n- 将参数与合约一键回填到运行页\n- 启动后可在右侧查看状态和日志\n- 空闲时 Space 会休眠,策略不会持续运行(演示环境)")
gr.Markdown("<div class='footer-note'>提示:若需长期运行,请在自有服务器部署引擎,Space 仅作前端/演示。</div>")
# 页面切换
设置按钮.click(lambda: (gr.update(visible=False), gr.update(visible=True)), outputs=[运行面板, 设置面板])
返回按钮.click(lambda: (gr.update(visible=True), gr.update(visible=False)), outputs=[运行面板, 设置面板])
# 事件绑定(运行控制)
启动按钮.click(fn=启动模拟, inputs=[合约输入, 周期选择, 参数输入], outputs=[状态显示, 日志显示])
停止按钮.click(fn=停止模拟, outputs=状态显示)
刷新按钮.click(fn=获取状态, outputs=[状态显示, 日志显示])
# 参数JSON变化时,自动更新周期选项与选中值
参数输入.change(fn=参数变更更新周期, inputs=参数输入, outputs=周期选择)
# 初始和定时刷新
demo.load(fn=获取状态, outputs=[状态显示, 日志显示])
# 页面加载时按参数JSON回填周期
demo.load(fn=lambda s: 参数变更更新周期(s), inputs=参数输入, outputs=周期选择)
timer = gr.Timer(2)
timer.tick(fn=获取状态, outputs=[状态显示, 日志显示])
if __name__ == "__main__":
demo.launch(server_name="0.0.0.0", server_port=7860) |