chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:36:27 +08:00
commit 05bc60394c
352 changed files with 344349 additions and 0 deletions
Vendored
BIN
View File
Binary file not shown.
+12
View File
@@ -0,0 +1,12 @@
# LLM Providers (set the one you use)
OPENAI_API_KEY=
SILICONFLOW_API_KEY=
GOOGLE_API_KEY=
ANTHROPIC_API_KEY=
XAI_API_KEY=
OPENROUTER_API_KEY=
# X Brief LLM (orchestrator,默认 SiliconFlow MiniMax-M2.5)
XBRIEF_ORCHESTRATOR_PROVIDER=siliconflow
XBRIEF_ORCHESTRATOR_MODEL=Pro/MiniMaxAI/MiniMax-M2.5
XBRIEF_ORCHESTRATOR_BASE_URL=https://api.siliconflow.cn/v1
+239
View File
@@ -0,0 +1,239 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[codz]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py.cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
# Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
# poetry.lock
# poetry.toml
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
# pdm.lock
# pdm.toml
.pdm-python
.pdm-build/
# pixi
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
# pixi.lock
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
# in the .venv directory. It is recommended not to include this directory in version control.
.pixi
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# Redis
*.rdb
*.aof
*.pid
# RabbitMQ
mnesia/
rabbitmq/
rabbitmq-data/
# ActiveMQ
activemq-data/
# SageMath parsed files
*.sage.py
# Environments
.env
.envrc
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
# .idea/
# Abstra
# Abstra is an AI-powered process automation framework.
# Ignore directories containing user credentials, local state, and settings.
# Learn more at https://abstra.io/docs
.abstra/
# Visual Studio Code
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
# and can be added to the global gitignore or merged into this file. However, if you prefer,
# you could uncomment the following to ignore the entire vscode folder
# .vscode/
# Ruff stuff:
.ruff_cache/
# PyPI configuration file
.pypirc
# Marimo
marimo/_static/
marimo/_lsp/
__marimo__/
# Streamlit
.streamlit/secrets.toml
# Cache
**/data_cache/
3月30日/*
images/*
# Runtime outputs
eval_results/
reports/
results/
# Runtime data
data/dashboard/history/
data/dashboard/latest.json
data/cache/
data/portfolio.json
eval_results/*
docs/data_report.md
docs/webui_prd.md
docs/strategy_module_future.md
docs/ui_shell.md
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+160
View File
@@ -0,0 +1,160 @@
# 个性化多智能体金融分析工具
## 基于TradingAgents底层框架打造
- 【2026-04】搭建Webui仪表盘作为观察大盘数据的入口,个股分析作为TradingAgents分析工具的入口
- 【2026-04】后续预告:交易策略工具搭建中,包括回测,开仓信号,平仓信号等
基于 TradingAgents 多智能体框架,提供:
- 交互式 CLI 分析流程
- Streamlit WebUI(市场总览、个股分析、报告回看、持仓管理)
- 本地报告与快照持久化能力
> 仅用于研究与学习,不构成任何投资建议。
---
## 当前功能(WebUI
WebUI 入口:`app.py`
一级页面(当前实现):
1. **仪表盘**
- 指数、宏观代理、板块 Top3、Top10 新闻
- LLM 新闻立场与摘要
- LLM 大盘总结
- 手动刷新并写入快照(latest + history
2. **个股分析**
- 启动分析任务、查看运行进度与日志
- 展示最新报告摘要
- 内嵌历史报告浏览
3. **策略**
- 当前为占位页(后续实现)
4. **股票筛选**
- 搜索/排序/查看 ticker
- 跳转股票详情页
5. **股票详情**
- 概览指标、最新分析引导、历史报告
6. **持仓**
- 本地持仓录入、同 ticker 合并、均价重算
产品规格文档见:`docs/webui_prd.md`
---
## 项目结构(核心目录)
```text
TradingAgents/
├── app.py # WebUI 入口
├── web/
│ ├── pages/ # 各页面实现
│ ├── services/ # 数据拉取/存储/LLM 服务
│ ├── theme.py # 全局主题样式
│ ├── sidebar_nav.py # 侧栏导航
│ ├── history.py # 报告历史扫描
│ └── report_viewer.py # 报告渲染
├── tradingagents/ # 多智能体核心框架
├── cli/ # CLI 入口与交互
├── docs/ # 文档
├── data/ # 本地运行数据(建议忽略运行产物)
├── reports/ # Web 分析报告输出(运行产物)
├── results/ # CLI 结果输出(运行产物)
└── eval_results/ # 评估日志输出(运行产物)
```
---
## 安装
```bash
git clone https://github.com/qchen34/TradingAgents.git
cd TradingAgents
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
---
## 环境变量
至少配置一个 LLM Provider 的 Key(按需):
```bash
export OPENAI_API_KEY=...
export SILICONFLOW_API_KEY=...
export GOOGLE_API_KEY=...
export ANTHROPIC_API_KEY=...
export XAI_API_KEY=...
export OPENROUTER_API_KEY=...
export ALPHA_VANTAGE_API_KEY=...
```
也可使用 `.env`
```bash
cp .env.example .env
```
---
## 启动方式
### 1) 启动 WebUI
```bash
streamlit run app.py
```
### 2) 启动 npm 前端 + Python API
```bash
# API
python -m uvicorn backend.api.main:app --host 127.0.0.1 --port 8000 --reload
# Frontend(新终端)
cd frontend
npm install
npm run dev
```
### 3) 一键并行启动(Streamlit + FastAPI + Next.js
```bash
bash scripts/start_dual_stack.sh
```
### 或
### 4) 启动 CLI
```bash
python -m cli.main
# 或(安装后)
tradingagents
```
---
## 运行数据与输出目录说明
以下目录/文件通常为运行自动生成,建议加入 `.gitignore`
- `eval_results/`
- `reports/`
- `results/`
- `data/dashboard/history/`
- `data/dashboard/latest.json`
- `data/cache/`
- `data/portfolio.json`
如果你希望仓库“干净可复现”,建议只保留代码与文档,运行产物不入库。
## 引用
本项目底层引用Tauric Research的TradingAgents框架:https://github.com/TauricResearch/TradingAgents
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`qchen34/TradingAgents`
- 原始仓库:https://github.com/qchen34/TradingAgents
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+43
View File
@@ -0,0 +1,43 @@
import streamlit as st
from web.navigation import PAGE_DASHBOARD
from web.pages import PAGES
from web.sidebar_nav import render_sidebar_navigation
from web.theme import apply_theme
def _init_session_state() -> None:
defaults = {
"current_page": PAGE_DASHBOARD,
"ui_mode": "idle",
"show_config": False,
"event_log": [],
"runtime_stage": "",
"latest_run_dir": "",
"last_params": None,
"selected_params_summary": "",
"active_output_dir": "",
"runtime_stats": {},
"selected_ticker": "QQQ",
"error": "",
}
for key, val in defaults.items():
if key not in st.session_state:
st.session_state[key] = val
st.set_page_config(page_title="TradingAgents 控制台", layout="wide")
_init_session_state()
apply_theme()
page_names = list(PAGES.keys())
if st.session_state.current_page not in page_names:
st.session_state.current_page = page_names[0]
with st.sidebar:
st.session_state.current_page = render_sidebar_navigation(
page_names,
st.session_state.current_page,
)
PAGES[st.session_state.current_page]()
Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 812 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 791 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

+2
View File
@@ -0,0 +1,2 @@
"""Backend services for API and future workers."""
+2
View File
@@ -0,0 +1,2 @@
"""FastAPI entry package for TradingAgents."""
+183
View File
@@ -0,0 +1,183 @@
from __future__ import annotations
import uuid
from typing import Any
from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from backend.api.schemas import (
ApiEnvelope,
BatchQuoteRequest,
BacktestJobRequest,
DashboardRefreshRequest,
LrsChatRequest,
WheelEvaluateRequest,
XBriefRefreshRequest,
)
from backend.api.services import (
DEFAULT_SESSION_STATE,
answer_lrs_chat,
batch_quotes,
create_backtest_job,
get_dashboard_snapshot,
get_backtest_job,
refresh_dashboard_snapshot,
refresh_x_brief,
strategy_metadata,
get_x_brief_latest,
wheel_evaluate,
wheel_profiles,
)
from web.services.backtesting_core import _PERIOD_ORDER, _PERIOD_SPECS
app = FastAPI(title="TradingAgents API", version="0.1.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
def _trace_id(req: Request) -> str:
return req.headers.get("x-trace-id") or uuid.uuid4().hex
def _ok(request: Request, data: dict[str, Any] | list[Any] | None = None) -> JSONResponse:
payload = ApiEnvelope(success=True, data=data, trace_id=_trace_id(request))
return JSONResponse(status_code=200, content=payload.model_dump())
def _err(request: Request, msg: str, code: int = 500) -> JSONResponse:
payload = ApiEnvelope(success=False, error=msg, data=None, trace_id=_trace_id(request))
return JSONResponse(status_code=code, content=payload.model_dump())
@app.get("/health")
def health(request: Request) -> JSONResponse:
return _ok(request, {"status": "ok"})
@app.get("/api/v1/state/defaults")
def state_defaults(request: Request) -> JSONResponse:
return _ok(request, DEFAULT_SESSION_STATE)
@app.get("/api/v1/strategy/metadata")
def get_strategy_metadata(request: Request) -> JSONResponse:
return _ok(request, strategy_metadata())
@app.get("/api/v1/dashboard")
def get_dashboard(request: Request) -> JSONResponse:
try:
return _ok(request, get_dashboard_snapshot())
except Exception as exc:
return _err(request, f"仪表盘数据读取失败:{exc}", 500)
@app.post("/api/v1/dashboard/refresh")
def post_dashboard_refresh(body: DashboardRefreshRequest, request: Request) -> JSONResponse:
try:
return _ok(request, refresh_dashboard_snapshot(limit=body.limit))
except Exception as exc:
return _err(request, f"仪表盘刷新失败:{exc}", 500)
@app.post("/api/v1/quotes/batch")
def post_batch_quotes(body: BatchQuoteRequest, request: Request) -> JSONResponse:
try:
return _ok(request, batch_quotes(body.codes))
except Exception as exc:
return _err(request, f"批量报价失败:{exc}", 500)
@app.get("/api/v1/x-brief/latest")
def get_xbrief_latest(request: Request) -> JSONResponse:
data = get_x_brief_latest()
if data is None:
return _ok(request, {"ready": False})
return _ok(request, {"ready": True, **data})
@app.post("/api/v1/x-brief/refresh")
def post_xbrief_refresh(body: XBriefRefreshRequest, request: Request) -> JSONResponse:
try:
data = refresh_x_brief(days=body.days, per_account_limit=body.per_account_limit)
return _ok(request, {"ready": True, **data})
except Exception as exc:
return _err(request, f"X资讯简报刷新失败:{exc}", 500)
@app.post("/api/v1/strategy/lrs/chat")
def post_lrs_chat(body: LrsChatRequest, request: Request) -> JSONResponse:
try:
answer = answer_lrs_chat(body.question, body.last_params)
return _ok(request, {"answer": answer})
except Exception as exc:
return _err(request, f"LLM 调用失败:{exc}", 500)
@app.get("/api/v1/wheel/profiles")
def get_wheel_profiles(request: Request) -> JSONResponse:
return _ok(request, wheel_profiles())
@app.post("/api/v1/wheel/evaluate")
def post_wheel_evaluate(body: WheelEvaluateRequest, request: Request) -> JSONResponse:
try:
data = wheel_evaluate(
style_key=body.style_key,
underlying_ticker=body.underlying_ticker,
signal_ticker=body.signal_ticker,
)
return _ok(request, data)
except Exception as exc:
return _err(request, f"Wheel 评估失败:{exc}", 500)
@app.get("/api/v1/backtest/periods")
def get_backtest_periods(request: Request) -> JSONResponse:
periods = [
{"key": key, "label": _PERIOD_SPECS[key]["label"]}
for key in _PERIOD_ORDER
]
return _ok(request, periods)
@app.post("/api/v1/backtest/jobs")
def post_backtest_job(body: BacktestJobRequest, request: Request) -> JSONResponse:
job = create_backtest_job(body.period_key)
return _ok(
request,
{
"job_id": job.job_id,
"status": job.status,
"period_key": job.period_key,
"created_at": job.created_at,
},
)
@app.get("/api/v1/backtest/jobs/{job_id}")
def get_job(job_id: str, request: Request) -> JSONResponse:
job = get_backtest_job(job_id)
if job is None:
return _err(request, "任务不存在", 404)
return _ok(
request,
{
"job_id": job.job_id,
"status": job.status,
"period_key": job.period_key,
"created_at": job.created_at,
"updated_at": job.updated_at,
"result": job.result,
"error": job.error,
},
)
+41
View File
@@ -0,0 +1,41 @@
from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field
class ApiEnvelope(BaseModel):
success: bool = True
data: dict[str, Any] | list[Any] | None = None
error: str | None = None
trace_id: str
class LrsChatRequest(BaseModel):
question: str = Field(min_length=1)
last_params: dict[str, Any] | None = None
class BacktestJobRequest(BaseModel):
period_key: Literal["2y", "3y", "5y", "10y", "2015_2020", "2010_2015"]
class DashboardRefreshRequest(BaseModel):
limit: int = Field(default=10, ge=1, le=30)
class WheelEvaluateRequest(BaseModel):
style_key: Literal["aggressive", "neutral", "conservative"] = "neutral"
underlying_ticker: str = Field(default="TQQQ", min_length=1)
signal_ticker: str = Field(default="QQQ", min_length=1)
class BatchQuoteRequest(BaseModel):
codes: list[str] = Field(min_length=1, max_length=200)
class XBriefRefreshRequest(BaseModel):
days: int = Field(default=7, ge=3, le=30)
per_account_limit: int = Field(default=80, ge=10, le=200)
+400
View File
@@ -0,0 +1,400 @@
from __future__ import annotations
import os
import threading
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
import numpy as np
import yfinance as yf
from web.services import backtesting_core as bt
from web.services import market_data as md
from web.services.x_brief_data import collect_x_rss_last_week, normalize_to_x_url, save_raw_payload
from web.services.x_brief_llm import build_fallback_modules, build_x_brief_modules, translate_display_content
from web.services.x_brief_renderer import (
build_monthly_analysis_markdown,
build_tweet_summary_markdown,
load_latest_view,
persist_outputs,
)
from web.pages.wheel.style_profiles import STYLE_PROFILES
from web.pages.wheel.strategy_shared import rsi
from web.services.dashboard_llm import build_llm_config
from web.services.strategy_api_service import answer_lrs_question, get_lrs_doc_markdown
DEFAULT_SESSION_STATE: dict[str, Any] = {
"current_page": "仪表盘",
"ui_mode": "idle",
"show_config": False,
"event_log": [],
"runtime_stage": "",
"latest_run_dir": "",
"last_params": None,
"selected_params_summary": "",
"active_output_dir": "",
"runtime_stats": {},
"selected_ticker": "QQQ",
"error": "",
"strategy_sub_menu": "LRS TQQQ策略",
}
def strategy_metadata() -> dict[str, Any]:
return {
"sub_strategies": ["LRS TQQQ策略", "Wheel策略"],
"lrs_doc_md": get_lrs_doc_markdown(),
}
def answer_lrs_chat(question: str, last_params: dict[str, Any] | None) -> str:
cfg = build_llm_config(last_params)
prompt = (
"你是量化策略研究助手。请围绕 LRS TQQQ 策略回答问题,"
"要求中文、结构化、可执行。\n\n用户问题:"
f"{question}"
)
return answer_lrs_question(prompt, cfg)
@dataclass
class BacktestJob:
job_id: str
period_key: str
status: str
created_at: str
result: dict[str, Any] | None = None
error: str | None = None
updated_at: str = field(default_factory=lambda: _now_iso())
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
_JOBS: dict[str, BacktestJob] = {}
_LOCK = threading.Lock()
def _run_backtest(period_key: str, job_id: str) -> None:
try:
qqq_df, tqqq_df = bt._fetch(period_key)
if qqq_df.empty or tqqq_df.empty:
raise RuntimeError("QQQ 或 TQQQ 数据为空")
qqq_full = qqq_df["Close"].squeeze().dropna()
tqqq_full = tqqq_df["Close"].squeeze().dropna()
if len(qqq_full) < 220:
raise RuntimeError(f"数据量不足({len(qqq_full)} 天),无法计算 MA200")
trade_start = bt._get_effective_start(qqq_full, tqqq_full, period_key)
qqq_eff = qqq_full[trade_start:]
sig = bt._build_signals(qqq_full)
trades, equity = bt._simulate(sig, tqqq_full, trade_start=trade_start)
metrics = bt._metrics(trades, equity, qqq_eff)
with _LOCK:
_JOBS[job_id].status = "completed"
_JOBS[job_id].updated_at = _now_iso()
_JOBS[job_id].result = {
"period_key": period_key,
"period_label": bt._PERIOD_SPECS[period_key]["label"],
"date_range": {
"start": str(qqq_eff.index[0].date()),
"end": str(qqq_eff.index[-1].date()),
},
"metrics": metrics,
"trades": [t.__dict__ for t in trades],
}
except Exception as exc: # pragma: no cover - best effort background task
with _LOCK:
_JOBS[job_id].status = "failed"
_JOBS[job_id].updated_at = _now_iso()
_JOBS[job_id].error = str(exc)
def create_backtest_job(period_key: str) -> BacktestJob:
job_id = uuid.uuid4().hex
job = BacktestJob(
job_id=job_id,
period_key=period_key,
status="running",
created_at=_now_iso(),
)
with _LOCK:
_JOBS[job_id] = job
th = threading.Thread(target=_run_backtest, args=(period_key, job_id), daemon=True)
th.start()
return job
def get_backtest_job(job_id: str) -> BacktestJob | None:
with _LOCK:
return _JOBS.get(job_id)
def _serialize_quote_row(row: Any) -> dict[str, Any]:
return {
"ticker": getattr(row, "ticker", ""),
"label": getattr(row, "label", ""),
"price": getattr(row, "price", None),
"prev_close": getattr(row, "prev_close", None),
"change": getattr(row, "change", None),
"change_pct": getattr(row, "change_pct", None),
}
def _serialize_snapshot(snapshot: dict[str, Any], news: list[dict[str, Any]] | None) -> dict[str, Any]:
top6_raw = snapshot.get("top6_sectors")
if top6_raw is None:
top6_raw = snapshot.get("top3_sectors", [])
return {
"market_status": snapshot.get("market_status", "N/A"),
"last_updated_et": snapshot.get("last_updated_et", "N/A"),
"fetched_at_utc": snapshot.get("fetched_at_utc", ""),
"llm_model": snapshot.get("llm_model", ""),
"llm_provider": snapshot.get("llm_provider", ""),
"llm_news_error": snapshot.get("llm_news_error", ""),
"llm_digest_error": snapshot.get("llm_digest_error", ""),
"market_digest_md": snapshot.get("market_digest_md", ""),
"indexes": [_serialize_quote_row(x) for x in snapshot.get("indexes", [])],
"top6_sectors": [_serialize_quote_row(x) for x in top6_raw],
"macro_strip": snapshot.get("macro_strip") or [],
"news": news or [],
}
def get_dashboard_snapshot() -> dict[str, Any]:
snapshot, news = md.load_dashboard_display()
if snapshot is None:
snapshot = md.get_dashboard_market_snapshot()
return _serialize_snapshot(snapshot, news)
def refresh_dashboard_snapshot(limit: int = 10) -> dict[str, Any]:
snapshot, news = md.refresh_dashboard_cache(limit=limit)
return _serialize_snapshot(snapshot, news)
def wheel_profiles() -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for key, p in STYLE_PROFILES.items():
out.append(
{
"key": key,
"label": p.label,
"dte": p.dte,
"ivr_min": p.ivr_min,
"rsi_max": p.rsi_max,
"put_otm": p.put_otm,
"call_otm": p.call_otm,
"put_tp": p.put_tp,
"call_tp": p.call_tp,
"put_sl": p.put_sl,
}
)
return out
def _fetch_close(ticker: str) -> Any:
df = yf.download(ticker, period="6mo", auto_adjust=True, progress=False)
if df.empty:
raise RuntimeError(f"{ticker} 无可用行情")
close = df["Close"]
if hasattr(close, "columns"):
close = close.iloc[:, 0]
return close.dropna()
def _estimate_ivr(underlying_close: Any) -> float:
ret = underlying_close.pct_change().dropna()
if len(ret) < 20:
return 50.0
hv = ret.rolling(20).std() * np.sqrt(252)
iv = (hv * 1.15).clip(lower=0.1, upper=2.5)
iv_min = iv.rolling(126, min_periods=20).min()
iv_max = iv.rolling(126, min_periods=20).max()
ivr = ((iv - iv_min) / (iv_max - iv_min).replace(0, np.nan) * 100.0).fillna(50.0)
return float(ivr.iloc[-1])
def wheel_evaluate(style_key: str, underlying_ticker: str, signal_ticker: str) -> dict[str, Any]:
if style_key not in STYLE_PROFILES:
raise RuntimeError(f"未知风格:{style_key}")
style = STYLE_PROFILES[style_key]
ul = _fetch_close(underlying_ticker.upper())
sig = _fetch_close(signal_ticker.upper())
rs = rsi(sig, 14).fillna(50.0)
latest_ul = float(ul.iloc[-1])
latest_sig = float(sig.iloc[-1])
latest_rsi = float(rs.iloc[-1])
latest_ivr = _estimate_ivr(ul)
put_strike = round(latest_ul * (1.0 - style.put_otm), 2)
call_strike = round(latest_ul * (1.0 + style.call_otm), 2)
put_entry_ok = latest_ivr >= style.ivr_min and latest_rsi <= style.rsi_max
regime = "bullish" if latest_sig >= float(sig.iloc[-20:].mean()) else "neutral_to_weak"
if put_entry_ok:
action = f"可考虑卖出现金担保Put(行权价约 {put_strike}"
elif regime == "bullish":
action = "等待更优波动率窗口(IVR 偏低),暂不卖Put"
else:
action = "信号偏弱,优先观望或缩小仓位"
return {
"style": {
"key": style.key,
"label": style.label,
"dte": style.dte,
"ivr_min": style.ivr_min,
"rsi_max": style.rsi_max,
"put_otm": style.put_otm,
"call_otm": style.call_otm,
"put_tp": style.put_tp,
"call_tp": style.call_tp,
"put_sl": style.put_sl,
},
"signal": {
"underlying_ticker": underlying_ticker.upper(),
"signal_ticker": signal_ticker.upper(),
"underlying_price": latest_ul,
"signal_price": latest_sig,
"rsi14": latest_rsi,
"ivr_est": latest_ivr,
"put_entry_ok": put_entry_ok,
"regime": regime,
},
"execution_hint": {
"suggested_action": action,
"put_strike_hint": put_strike,
"call_strike_hint": call_strike,
"dte_hint": style.dte,
"take_profit": {
"put": style.put_tp,
"call": style.call_tp,
},
},
}
def _normalize_quote_code(code: str) -> str:
c = (code or "").strip().upper()
if not c:
return ""
if c.startswith("US.."):
return f"^{c[4:]}"
if c.startswith("US."):
return c[3:]
return c
def batch_quotes(codes: list[str]) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for raw in codes:
normalized = _normalize_quote_code(raw)
if not normalized:
continue
q = md._quote(normalized, normalized)
out.append(
{
"request_code": raw,
"normalized_code": normalized,
"ticker": q.ticker,
"label": q.label,
"price": q.price,
"prev_close": q.prev_close,
"change": q.change,
"change_pct": q.change_pct,
}
)
return out
def get_x_brief_latest() -> dict[str, Any] | None:
return load_latest_view()
def refresh_x_brief(days: int = 7, per_account_limit: int = 80) -> dict[str, Any]:
payload = collect_x_rss_last_week(days=days, per_account_limit=per_account_limit)
cfg = build_llm_config(None)
# X Brief 两层模型:
# - 第一层编排:thinkingMiniMax
# - 第二层翻译:quick(沿用 quick_think_llm
cfg["xbrief_orchestrator_provider"] = os.environ.get("XBRIEF_ORCHESTRATOR_PROVIDER", "siliconflow")
cfg["xbrief_orchestrator_model"] = os.environ.get("XBRIEF_ORCHESTRATOR_MODEL", "Pro/MiniMaxAI/MiniMax-M2.5")
cfg["xbrief_orchestrator_base_url"] = os.environ.get("XBRIEF_ORCHESTRATOR_BASE_URL", "https://api.siliconflow.cn/v1")
cfg["xbrief_translate_provider"] = cfg.get("llm_provider")
cfg["xbrief_translate_model"] = cfg.get("quick_think_llm")
cfg["xbrief_translate_base_url"] = cfg.get("backend_url")
save_raw_payload(payload)
tweet_md = build_tweet_summary_markdown(payload)
modules: dict[str, Any]
try:
modules = build_x_brief_modules(payload, tweet_md, cfg)
except Exception as exc:
modules = build_fallback_modules(payload)
modules["risk_signals"] = modules.get("risk_signals", []) + [
{
"id": "llm-fallback-error",
"level": "MID",
"title": "LLM 编排暂不可用",
"detail": f"本次使用规则兜底:{exc}",
}
]
if not any(modules.get(k) for k in ("themes", "p0_events", "top_quotes", "category_updates", "risk_signals")):
modules = build_fallback_modules(payload)
# 最终落盘前统一做一遍 URL 规范化,避免历史链路残留 nitter 链接。
tweets = payload.get("tweets", []) or []
norm_tweets: list[dict[str, Any]] = []
for t in tweets:
d = dict(t)
d["url"] = normalize_to_x_url(str(d.get("url", "")), str(d.get("handle", "")))
norm_tweets.append(d)
payload["tweets"] = norm_tweets
for theme in modules.get("themes", []) or []:
for sample in theme.get("samples", []) or []:
sample["url"] = normalize_to_x_url(str(sample.get("url", "")), str(sample.get("handle", "")))
for e in modules.get("p0_events", []) or []:
e["url"] = normalize_to_x_url(str(e.get("url", "")))
for q in modules.get("top_quotes", []) or []:
speaker = str(q.get("speaker", "")).lstrip("@")
q["url"] = normalize_to_x_url(str(q.get("url", "")), speaker)
for c in modules.get("category_updates", []) or []:
for item in c.get("items", []) or []:
item["url"] = normalize_to_x_url(str(item.get("url", "")))
try:
payload = translate_display_content(payload, modules, cfg)
except Exception as exc:
modules["risk_signals"] = (modules.get("risk_signals") or []) + [
{
"id": "llm-translate-fallback-error",
"level": "MID",
"title": "前端内容翻译层异常",
"detail": f"本次展示内容使用原文兜底:{exc}",
}
]
analysis_md = build_monthly_analysis_markdown(payload)
# 统计 modules 实际引用的唯一推文数,写入 overview
_kept_urls: set[str] = set()
for _theme in modules.get("themes", []) or []:
for _s in _theme.get("samples", []) or []:
if _s.get("url"):
_kept_urls.add(_s["url"])
for _e in modules.get("p0_events", []) or []:
if _e.get("url"):
_kept_urls.add(_e["url"])
for _q in modules.get("top_quotes", []) or []:
if _q.get("url"):
_kept_urls.add(_q["url"])
overview = dict(payload.get("overview") or {})
overview["tweets_kept"] = len(_kept_urls)
payload = dict(payload)
payload["overview"] = overview
return persist_outputs(payload, tweet_md, analysis_md, modules=modules)
View File
+51
View File
@@ -0,0 +1,51 @@
import getpass
import requests
from rich.console import Console
from rich.panel import Panel
from cli.config import CLI_CONFIG
def fetch_announcements(url: str = None, timeout: float = None) -> dict:
"""Fetch announcements from endpoint. Returns dict with announcements and settings."""
endpoint = url or CLI_CONFIG["announcements_url"]
timeout = timeout or CLI_CONFIG["announcements_timeout"]
fallback = CLI_CONFIG["announcements_fallback"]
try:
response = requests.get(endpoint, timeout=timeout)
response.raise_for_status()
data = response.json()
return {
"announcements": data.get("announcements", [fallback]),
"require_attention": data.get("require_attention", False),
}
except Exception:
return {
"announcements": [fallback],
"require_attention": False,
}
def display_announcements(console: Console, data: dict) -> None:
"""Display announcements panel. Prompts for Enter if require_attention is True."""
announcements = data.get("announcements", [])
require_attention = data.get("require_attention", False)
if not announcements:
return
content = "\n".join(announcements)
panel = Panel(
content,
border_style="cyan",
padding=(1, 2),
title="Announcements",
)
console.print(panel)
if require_attention:
getpass.getpass("Press Enter to continue...")
else:
console.print()
+6
View File
@@ -0,0 +1,6 @@
CLI_CONFIG = {
# Announcements
"announcements_url": "https://api.tauric.ai/v1/announcements",
"announcements_timeout": 1.0,
"announcements_fallback": "[cyan]For more information, please visit[/cyan] [link=https://github.com/TauricResearch]https://github.com/TauricResearch[/link]",
}
+1208
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
from enum import Enum
from typing import List, Optional, Dict
from pydantic import BaseModel
class AnalystType(str, Enum):
MARKET = "market"
SOCIAL = "social"
NEWS = "news"
FUNDAMENTALS = "fundamentals"
+7
View File
@@ -0,0 +1,7 @@
______ ___ ___ __
/_ __/________ _____/ (_)___ ____ _/ | ____ ____ ____ / /______
/ / / ___/ __ `/ __ / / __ \/ __ `/ /| |/ __ `/ _ \/ __ \/ __/ ___/
/ / / / / /_/ / /_/ / / / / / /_/ / ___ / /_/ / __/ / / / /_(__ )
/_/ /_/ \__,_/\__,_/_/_/ /_/\__, /_/ |_\__, /\___/_/ /_/\__/____/
/____/ /____/
+119
View File
@@ -0,0 +1,119 @@
import threading
from typing import Any, Dict, List, Optional, Tuple
from langchain_core.callbacks import BaseCallbackHandler
from langchain_core.outputs import LLMResult
from langchain_core.messages import AIMessage
def _pick_int(d: Optional[Dict[str, Any]], *keys: str) -> int:
if not d:
return 0
for k in keys:
v = d.get(k)
if v is not None:
try:
return int(v)
except (TypeError, ValueError):
continue
return 0
def _extract_tokens_from_usage_dict(u: Dict[str, Any]) -> Tuple[int, int]:
"""兼容 OpenAI / Anthropic / Gemini 等字段命名。"""
tin = _pick_int(u, "input_tokens", "prompt_tokens", "cache_read_input_tokens")
tout = _pick_int(u, "output_tokens", "completion_tokens", "candidates_token_count")
return tin, tout
class StatsCallbackHandler(BaseCallbackHandler):
"""Callback handler that tracks LLM calls, tool calls, and token usage."""
def __init__(self) -> None:
super().__init__()
self._lock = threading.Lock()
self.llm_calls = 0
self.tool_calls = 0
self.tokens_in = 0
self.tokens_out = 0
def on_llm_start(
self,
serialized: Dict[str, Any],
prompts: List[str],
**kwargs: Any,
) -> None:
"""Increment LLM call counter when an LLM starts."""
with self._lock:
self.llm_calls += 1
def on_chat_model_start(
self,
serialized: Dict[str, Any],
messages: List[List[Any]],
**kwargs: Any,
) -> None:
"""Increment LLM call counter when a chat model starts."""
with self._lock:
self.llm_calls += 1
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> None:
"""Extract token usage from LLM response."""
tin, tout = 0, 0
try:
generation = response.generations[0][0]
except (IndexError, TypeError):
generation = None
if generation is not None and hasattr(generation, "message"):
message = generation.message
if isinstance(message, AIMessage):
if hasattr(message, "usage_metadata") and message.usage_metadata:
um = message.usage_metadata
if isinstance(um, dict):
tin = _pick_int(um, "input_tokens", "prompt_tokens")
tout = _pick_int(um, "output_tokens", "completion_tokens")
if tin == 0 and tout == 0 and hasattr(message, "response_metadata"):
rm = message.response_metadata or {}
u = rm.get("token_usage") or rm.get("usage_metadata") or rm.get("usage")
if isinstance(u, dict):
tin, tout = _extract_tokens_from_usage_dict(u)
if tin == 0 and tout == 0 and generation is not None:
gen_info = getattr(generation, "generation_info", None) or {}
if isinstance(gen_info, dict):
u = gen_info.get("token_usage") or gen_info.get("usage")
if isinstance(u, dict):
tin, tout = _extract_tokens_from_usage_dict(u)
if tin == 0 and tout == 0:
llm_out = getattr(response, "llm_output", None)
if isinstance(llm_out, dict):
u = llm_out.get("token_usage") or llm_out.get("usage")
if isinstance(u, dict):
tin, tout = _extract_tokens_from_usage_dict(u)
if tin or tout:
with self._lock:
self.tokens_in += tin
self.tokens_out += tout
def on_tool_start(
self,
serialized: Dict[str, Any],
input_str: str,
**kwargs: Any,
) -> None:
"""Increment tool call counter when a tool starts."""
with self._lock:
self.tool_calls += 1
def get_stats(self) -> Dict[str, Any]:
"""Return current statistics."""
with self._lock:
return {
"llm_calls": self.llm_calls,
"tool_calls": self.tool_calls,
"tokens_in": self.tokens_in,
"tokens_out": self.tokens_out,
}
+318
View File
@@ -0,0 +1,318 @@
import questionary
from typing import List, Optional, Tuple, Dict
from rich.console import Console
from cli.models import AnalystType
from tradingagents.llm_clients.model_catalog import get_model_options
console = Console()
TICKER_INPUT_EXAMPLES = "Examples: SPY, CNC.TO, 7203.T, 0700.HK"
ANALYST_ORDER = [
("Market Analyst", AnalystType.MARKET),
("Social Media Analyst", AnalystType.SOCIAL),
("News Analyst", AnalystType.NEWS),
("Fundamentals Analyst", AnalystType.FUNDAMENTALS),
]
def get_ticker() -> str:
"""Prompt the user to enter a ticker symbol."""
ticker = questionary.text(
f"Enter the exact ticker symbol to analyze ({TICKER_INPUT_EXAMPLES}):",
validate=lambda x: len(x.strip()) > 0 or "Please enter a valid ticker symbol.",
style=questionary.Style(
[
("text", "fg:green"),
("highlighted", "noinherit"),
]
),
).ask()
if not ticker:
console.print("\n[red]No ticker symbol provided. Exiting...[/red]")
exit(1)
return normalize_ticker_symbol(ticker)
def normalize_ticker_symbol(ticker: str) -> str:
"""Normalize ticker input while preserving exchange suffixes."""
return ticker.strip().upper()
def get_analysis_date() -> str:
"""Prompt the user to enter a date in YYYY-MM-DD format."""
import re
from datetime import datetime
def validate_date(date_str: str) -> bool:
if not re.match(r"^\d{4}-\d{2}-\d{2}$", date_str):
return False
try:
datetime.strptime(date_str, "%Y-%m-%d")
return True
except ValueError:
return False
date = questionary.text(
"Enter the analysis date (YYYY-MM-DD):",
validate=lambda x: validate_date(x.strip())
or "Please enter a valid date in YYYY-MM-DD format.",
style=questionary.Style(
[
("text", "fg:green"),
("highlighted", "noinherit"),
]
),
).ask()
if not date:
console.print("\n[red]No date provided. Exiting...[/red]")
exit(1)
return date.strip()
def select_analysts() -> List[AnalystType]:
"""Select analysts using an interactive checkbox."""
choices = questionary.checkbox(
"Select Your [Analysts Team]:",
choices=[
questionary.Choice(display, value=value) for display, value in ANALYST_ORDER
],
instruction="\n- Press Space to select/unselect analysts\n- Press 'a' to select/unselect all\n- Press Enter when done",
validate=lambda x: len(x) > 0 or "You must select at least one analyst.",
style=questionary.Style(
[
("checkbox-selected", "fg:green"),
("selected", "fg:green noinherit"),
("highlighted", "noinherit"),
("pointer", "noinherit"),
]
),
).ask()
if not choices:
console.print("\n[red]No analysts selected. Exiting...[/red]")
exit(1)
return choices
def select_research_depth() -> int:
"""Select research depth using an interactive selection."""
# Define research depth options with their corresponding values
DEPTH_OPTIONS = [
("Shallow - Quick research, few debate and strategy discussion rounds", 1),
("Medium - Middle ground, moderate debate rounds and strategy discussion", 3),
("Deep - Comprehensive research, in depth debate and strategy discussion", 5),
]
choice = questionary.select(
"Select Your [Research Depth]:",
choices=[
questionary.Choice(display, value=value) for display, value in DEPTH_OPTIONS
],
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
style=questionary.Style(
[
("selected", "fg:yellow noinherit"),
("highlighted", "fg:yellow noinherit"),
("pointer", "fg:yellow noinherit"),
]
),
).ask()
if choice is None:
console.print("\n[red]No research depth selected. Exiting...[/red]")
exit(1)
return choice
def select_shallow_thinking_agent(provider) -> str:
"""Select shallow thinking llm engine using an interactive selection."""
choice = questionary.select(
"Select Your [Quick-Thinking LLM Engine]:",
choices=[
questionary.Choice(display, value=value)
for display, value in get_model_options(provider, "quick")
],
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
style=questionary.Style(
[
("selected", "fg:magenta noinherit"),
("highlighted", "fg:magenta noinherit"),
("pointer", "fg:magenta noinherit"),
]
),
).ask()
if choice is None:
console.print(
"\n[red]No shallow thinking llm engine selected. Exiting...[/red]"
)
exit(1)
return choice
def select_deep_thinking_agent(provider) -> str:
"""Select deep thinking llm engine using an interactive selection."""
choice = questionary.select(
"Select Your [Deep-Thinking LLM Engine]:",
choices=[
questionary.Choice(display, value=value)
for display, value in get_model_options(provider, "deep")
],
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
style=questionary.Style(
[
("selected", "fg:magenta noinherit"),
("highlighted", "fg:magenta noinherit"),
("pointer", "fg:magenta noinherit"),
]
),
).ask()
if choice is None:
console.print("\n[red]No deep thinking llm engine selected. Exiting...[/red]")
exit(1)
return choice
def select_llm_provider() -> tuple[str, str]:
"""Select the OpenAI api url using interactive selection."""
# Define OpenAI api options with their corresponding endpoints
BASE_URLS = [
("OpenAI", "https://api.openai.com/v1"),
("SiliconFlow", "https://api.siliconflow.cn/v1"),
("Google", "https://generativelanguage.googleapis.com/v1"),
("Anthropic", "https://api.anthropic.com/"),
("xAI", "https://api.x.ai/v1"),
("Openrouter", "https://openrouter.ai/api/v1"),
("Ollama", "http://localhost:11434/v1"),
]
choice = questionary.select(
"Select your LLM Provider:",
choices=[
questionary.Choice(display, value=(display, value))
for display, value in BASE_URLS
],
instruction="\n- Use arrow keys to navigate\n- Press Enter to select",
style=questionary.Style(
[
("selected", "fg:magenta noinherit"),
("highlighted", "fg:magenta noinherit"),
("pointer", "fg:magenta noinherit"),
]
),
).ask()
if choice is None:
console.print("\n[red]no OpenAI backend selected. Exiting...[/red]")
exit(1)
display_name, url = choice
print(f"You selected: {display_name}\tURL: {url}")
return display_name, url
def ask_openai_reasoning_effort() -> str:
"""Ask for OpenAI reasoning effort level."""
choices = [
questionary.Choice("Medium (Default)", "medium"),
questionary.Choice("High (More thorough)", "high"),
questionary.Choice("Low (Faster)", "low"),
]
return questionary.select(
"Select Reasoning Effort:",
choices=choices,
style=questionary.Style([
("selected", "fg:cyan noinherit"),
("highlighted", "fg:cyan noinherit"),
("pointer", "fg:cyan noinherit"),
]),
).ask()
def ask_anthropic_effort() -> str | None:
"""Ask for Anthropic effort level.
Controls token usage and response thoroughness on Claude 4.5+ and 4.6 models.
"""
return questionary.select(
"Select Effort Level:",
choices=[
questionary.Choice("High (recommended)", "high"),
questionary.Choice("Medium (balanced)", "medium"),
questionary.Choice("Low (faster, cheaper)", "low"),
],
style=questionary.Style([
("selected", "fg:cyan noinherit"),
("highlighted", "fg:cyan noinherit"),
("pointer", "fg:cyan noinherit"),
]),
).ask()
def ask_gemini_thinking_config() -> str | None:
"""Ask for Gemini thinking configuration.
Returns thinking_level: "high" or "minimal".
Client maps to appropriate API param based on model series.
"""
return questionary.select(
"Select Thinking Mode:",
choices=[
questionary.Choice("Enable Thinking (recommended)", "high"),
questionary.Choice("Minimal/Disable Thinking", "minimal"),
],
style=questionary.Style([
("selected", "fg:green noinherit"),
("highlighted", "fg:green noinherit"),
("pointer", "fg:green noinherit"),
]),
).ask()
def ask_output_language() -> str:
"""Ask for report output language."""
choice = questionary.select(
"Select Output Language:",
choices=[
questionary.Choice("English (default)", "English"),
questionary.Choice("Chinese (中文)", "Chinese"),
questionary.Choice("Japanese (日本語)", "Japanese"),
questionary.Choice("Korean (한국어)", "Korean"),
questionary.Choice("Hindi (हिन्दी)", "Hindi"),
questionary.Choice("Spanish (Español)", "Spanish"),
questionary.Choice("Portuguese (Português)", "Portuguese"),
questionary.Choice("French (Français)", "French"),
questionary.Choice("German (Deutsch)", "German"),
questionary.Choice("Arabic (العربية)", "Arabic"),
questionary.Choice("Russian (Русский)", "Russian"),
questionary.Choice("Custom language", "custom"),
],
style=questionary.Style([
("selected", "fg:yellow noinherit"),
("highlighted", "fg:yellow noinherit"),
("pointer", "fg:yellow noinherit"),
]),
).ask()
if choice == "custom":
return questionary.text(
"Enter language name (e.g. Turkish, Vietnamese, Thai, Indonesian):",
validate=lambda x: len(x.strip()) > 0 or "Please enter a language name.",
).ask().strip()
return choice
+26
View File
@@ -0,0 +1,26 @@
{
"close_val": 633.1801147460938,
"ma200_val": 596.570202331543,
"ma50_val": 600.1725512695313,
"a1_pass": true,
"a1_metric": "QQQ = 633.18 | MA200 = 596.57",
"a1_verdict": "日收盘价已站上 200 日均线,信号有效。",
"a2_score": 1,
"a2_metric": "MA200 变化:过去 20 个交易日 +1.07%Rising",
"a2_verdict": "过去 20 个交易日上升 +1.07%,斜率向上 — 通过。",
"a3_pass": true,
"a3_metric": "连续 6 日收盘站上 MA200(至少需要 3 日)",
"a3_verdict": "连续 3 日以上收盘站上 MA200,突破有效。",
"b1_pass": false,
"b1_metric": "Breakout vol: 63.1M | 20d avg: 66.6M | Ratio: 0.95x",
"b1_verdict": "成交量低于均值,突破力度偏弱,建议减仓 25%。",
"b2_pass": true,
"b2_metric": "QQQ = 633.18 | MA50 = 600.17 | MA200 = 596.57",
"b2_verdict": "价格 > MA50 > MA200 — 黄金交叉区间(强势)",
"b3_pass": false,
"days_since": 5,
"cross_count": 1,
"cross_idx": "2026-04-08",
"signal_quality": "Weak",
"_updated_at": "2026-04-16 01:31:58"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

+10
View File
@@ -0,0 +1,10 @@
{
"macro_score_1": 1.0,
"macro_score_2": 1.0,
"macro_score_3": 1.0,
"macro_score_4": 1.0,
"macro_score_5": 1.0,
"macro_score_total": 5.0,
"macro_tested": true,
"_updated_at": "2026-04-26 03:28:14"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

+18
View File
@@ -0,0 +1,18 @@
{
"total_cash": 19000.0,
"tqqq_price": 54.61989974975586,
"macro_score": 4,
"ceiling_pct": 1.0,
"deployable": 19000.0,
"quality_adj": 0.75,
"deployable_adj": 14250.0,
"direct_alloc": 4750.0,
"wheel_margin": 4750.0,
"shares": 86,
"actual_cost": 4697.311378479004,
"put_contracts": 0,
"reserve_cash": 14302.688621520996,
"entry_mode": "Split",
"signal_quality": "Weak",
"_updated_at": "2026-04-15 23:27:11"
}
+11
View File
@@ -0,0 +1,11 @@
{
"cross_idx": "2026-04-08",
"cross_price": 48.0,
"current_price": 54.67499923706055,
"distance_pct": 13.906248410542807,
"entry_mode": "Split",
"mode_detail": "1/3 直接买入 + 1/3 Sell Put + 1/3 预留",
"rationale": "距离 13.9% — TQQQ 已有一定涨幅,分批入场降低追高风险。",
"macro_score": 5.0,
"_updated_at": "2026-04-16 01:34:30"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

@@ -0,0 +1,21 @@
{
"start": "2025-04-24",
"end": "2026-04-24",
"metrics": {
"total_return": 13.034483512918182,
"final_value": 11303.448351291818,
"cagr": 13.082052914455588,
"sharpe": 2.8013245352313114,
"max_dd": -2.075202619162914,
"n_trades": 33,
"wins": 31,
"losses": 2,
"win_rate": 93.93939393939394,
"avg_win": 28.028074659204595,
"avg_loss": -287.9317604510027,
"chain_quotes": 0,
"bs_quotes": 86
},
"n_trades": 33,
"_updated_at": "2026-04-24 16:21:43"
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,13 @@
{
"start": "2025-04-24",
"end": "2026-04-24",
"style": "neutral",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 9.0,
"call_otm_pct": 6.0,
"put_tp_pct": 50.0,
"call_tp_pct": 50.0,
"put_sl_pct": 200.0,
"call_sl_pct": null
}
@@ -0,0 +1,15 @@
{
"total_return": 3.2482335125425754,
"final_value": 10324.823351254257,
"cagr": 3.259568016903991,
"sharpe": 0.09896699284613594,
"max_dd": -2.619957219409812,
"n_trades": 9,
"wins": 7,
"losses": 2,
"win_rate": 77.77777777777779,
"avg_win": 55.310639861806635,
"avg_loss": -181.81293525279108,
"chain_quotes": 0,
"bs_quotes": 37
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
[
{
"date": "2025-08-26 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 39.74,
"spot": 45.12633514404297,
"premium": 0.5536146925726833,
"close_cost": 0.24599471329293987,
"pnl": 24.781997927974338,
"hold_days": 6,
"result": "Win"
},
{
"date": "2025-10-13 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 44.03,
"spot": 51.44924545288086,
"premium": 0.796619024896291,
"close_cost": 0.3509533977764301,
"pnl": 38.58656271198609,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-11-10 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 48.48,
"spot": 56.17401885986328,
"premium": 1.778766330702327,
"close_cost": 0.48237271307612684,
"pnl": 123.65936176262001,
"hold_days": 4,
"result": "Win"
},
{
"date": "2025-12-19 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.91,
"spot": 53.34812545776367,
"premium": 1.036592731680047,
"close_cost": 0.46879731298197225,
"pnl": 50.79954186980747,
"hold_days": 7,
"result": "Win"
},
{
"date": "2026-01-06 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.56,
"spot": 54.91919708251953,
"premium": 0.5702405787526548,
"close_cost": 0.19268997243538966,
"pnl": 31.775060631726507,
"hold_days": 4,
"result": "Win"
},
{
"date": "2026-02-05 00:00:00",
"type": "PUT",
"action": "SL_Close",
"strike": 47.71,
"spot": 47.561363220214844,
"premium": 0.721505715176253,
"close_cost": 2.419175603087396,
"pnl": -175.74698879111432,
"hold_days": 2,
"result": "Loss"
},
{
"date": "2026-03-05 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 45.23,
"spot": 49.717796325683594,
"premium": 1.061063730657704,
"close_cost": 0.5197564837331896,
"pnl": 48.15072469245143,
"hold_days": 9,
"result": "Win"
},
{
"date": "2026-03-20 00:00:00",
"type": "PUT",
"action": "SL_Close",
"strike": 43.75,
"spot": 43.00889205932617,
"premium": 0.8460148560320633,
"close_cost": 2.6650036731767415,
"pnl": -187.8788817144678,
"hold_days": 3,
"result": "Loss"
},
{
"date": "2026-04-09 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 40.18,
"spot": 48.959999084472656,
"premium": 1.4108038692109268,
"close_cost": 0.6567915748501205,
"pnl": 69.42122943608062,
"hold_days": 2,
"result": "Win"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,13 @@
{
"start": "2025-04-24",
"end": "2026-04-24",
"style": "neutral",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 9.0,
"call_otm_pct": 6.0,
"put_tp_pct": 80.0,
"call_tp_pct": 50.0,
"put_sl_pct": 200.0,
"call_sl_pct": null
}
@@ -0,0 +1,15 @@
{
"total_return": 5.328146325585448,
"final_value": 10532.814632558544,
"cagr": 5.34692422213614,
"sharpe": 0.5849740422340445,
"max_dd": -2.5196350839124597,
"n_trades": 9,
"wins": 7,
"losses": 2,
"win_rate": 77.77777777777779,
"avg_win": 84.73485015577744,
"avg_loss": -181.8129352527916,
"chain_quotes": 0,
"bs_quotes": 69
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,110 @@
[
{
"date": "2025-09-03 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 39.74,
"spot": 44.36949920654297,
"premium": 0.5536159877961406,
"close_cost": 0.03495383585154532,
"pnl": 45.886215194459524,
"hold_days": 14,
"result": "Win"
},
{
"date": "2025-10-20 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 44.03,
"spot": 53.771759033203125,
"premium": 0.7966187749534139,
"close_cost": 0.05749347645129843,
"pnl": 67.93252985021155,
"hold_days": 10,
"result": "Win"
},
{
"date": "2025-11-25 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 48.48,
"spot": 51.84297180175781,
"premium": 1.778766330702327,
"close_cost": 0.1473346610213957,
"pnl": 157.16316696809312,
"hold_days": 19,
"result": "Win"
},
{
"date": "2025-12-23 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.91,
"spot": 54.83333969116211,
"premium": 1.0365927316800434,
"close_cost": 0.06827979046142785,
"pnl": 90.85129412186156,
"hold_days": 11,
"result": "Win"
},
{
"date": "2026-01-09 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.56,
"spot": 55.6679573059082,
"premium": 0.5702405787526459,
"close_cost": 0.10914421850859846,
"pnl": 40.129636024404746,
"hold_days": 7,
"result": "Win"
},
{
"date": "2026-02-05 00:00:00",
"type": "PUT",
"action": "SL_Close",
"strike": 47.71,
"spot": 47.561363220214844,
"premium": 0.7215057151762458,
"close_cost": 2.419175603087396,
"pnl": -175.746988791115,
"hold_days": 2,
"result": "Loss"
},
{
"date": "2026-03-11 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 45.23,
"spot": 49.26853942871094,
"premium": 1.0610637306577004,
"close_cost": 0.17795843012527435,
"pnl": 82.33053005324261,
"hold_days": 15,
"result": "Win"
},
{
"date": "2026-03-20 00:00:00",
"type": "PUT",
"action": "SL_Close",
"strike": 43.75,
"spot": 43.00889205932617,
"premium": 0.8460148560320597,
"close_cost": 2.6650036731767415,
"pnl": -187.87888171446818,
"hold_days": 3,
"result": "Loss"
},
{
"date": "2026-04-13 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 40.18,
"spot": 50.65999984741211,
"premium": 1.4108038692109268,
"close_cost": 0.26249808042923695,
"pnl": 108.85057887816899,
"hold_days": 6,
"result": "Win"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -0,0 +1,13 @@
{
"start": "2025-04-24",
"end": "2026-04-24",
"style": "conservative",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 12.0,
"call_otm_pct": 0.0,
"put_tp_pct": 50.0,
"call_tp_pct": 50.0,
"put_sl_pct": 150.0,
"call_sl_pct": null
}
@@ -0,0 +1,15 @@
{
"total_return": 5.458408550097382,
"final_value": 10545.84085500974,
"cagr": 5.477657356105814,
"sharpe": 2.031983303515009,
"max_dd": -0.6003518878697439,
"n_trades": 3,
"wins": 3,
"losses": 0,
"win_rate": 100.0,
"avg_win": 81.69156838518819,
"avg_loss": 0.0,
"chain_quotes": 0,
"bs_quotes": 21
}
@@ -0,0 +1,38 @@
[
{
"date": "2025-11-26 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 41.65,
"spot": 53.198604583740234,
"premium": 2.066391470619516,
"close_cost": 0.9709011820930149,
"pnl": 103.56902885265012,
"hold_days": 5,
"result": "Win"
},
{
"date": "2026-02-20 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 41.85,
"spot": 49.97737121582031,
"premium": 1.264905721711207,
"close_cost": 0.5412533437233309,
"pnl": 66.3852377987876,
"hold_days": 15,
"result": "Win"
},
{
"date": "2026-04-06 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 34.13,
"spot": 44.099998474121094,
"premium": 1.4551086033202925,
"close_cost": 0.6441042182790238,
"pnl": 75.12043850412687,
"hold_days": 10,
"result": "Win"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,13 @@
{
"start": "2021-04-24",
"end": "2026-04-24",
"style": "aggressive",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 6.0,
"call_otm_pct": 2.0,
"put_tp_pct": 80.0,
"call_tp_pct": 80.0,
"put_sl_pct": null,
"call_sl_pct": null
}
@@ -0,0 +1,15 @@
{
"total_return": 22.733742227955787,
"final_value": 12273.374222795579,
"cagr": 4.189634814548815,
"sharpe": 0.1856543053754313,
"max_dd": -12.24209072332098,
"n_trades": 172,
"wins": 134,
"losses": 38,
"win_rate": 77.90697674418605,
"avg_win": 47.0994160151811,
"avg_loss": -164.8441877629063,
"chain_quotes": 0,
"bs_quotes": 771
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

@@ -0,0 +1,13 @@
{
"start": "2021-04-24",
"end": "2026-04-24",
"style": "aggressive",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 6.0,
"call_otm_pct": 2.0,
"put_tp_pct": 50.0,
"call_tp_pct": 80.0,
"put_sl_pct": null,
"call_sl_pct": null
}
@@ -0,0 +1,15 @@
{
"total_return": 21.203246816790934,
"final_value": 12120.324681679094,
"cagr": 3.928013990277779,
"sharpe": 0.15407662226007332,
"max_dd": -12.80745424951754,
"n_trades": 167,
"wins": 137,
"losses": 30,
"win_rate": 82.03592814371258,
"avg_win": 40.5684761051716,
"avg_loss": -146.16866231788606,
"chain_quotes": 0,
"bs_quotes": 649
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

@@ -0,0 +1,15 @@
{
"start": "2025-04-24",
"end": "2026-04-24",
"style": "aggressive",
"underlying_ticker": "TQQQ",
"signal_ticker": "QQQ",
"put_otm_pct": 6.0,
"call_otm_pct": 2.0,
"put_tp_pct": 50.0,
"call_tp_pct": 80.0,
"put_sl_pct": null,
"call_sl_pct": null,
"engine_version": "wheel-v3-split",
"rule_version": "tqqq-wheel-manual-v1.1"
}
@@ -0,0 +1,15 @@
{
"total_return": 13.682463255870015,
"final_value": 11368.246325587003,
"cagr": 13.73253788517028,
"sharpe": 2.857837373795672,
"max_dd": -2.073090936612936,
"n_trades": 36,
"wins": 34,
"losses": 2,
"win_rate": 94.44444444444444,
"avg_win": 27.45038810795333,
"avg_loss": -287.9317604510025,
"chain_quotes": 0,
"bs_quotes": 98
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,434 @@
[
{
"date": "2025-04-25 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 24.34,
"spot": 26.743024826049805,
"premium": 0.29771831494625456,
"close_cost": 0.11768716878504693,
"pnl": 12.023114616120761,
"hold_days": 1,
"result": "Win"
},
{
"date": "2025-06-23 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 34.33,
"spot": 37.09067153930664,
"premium": 0.25507430657319397,
"close_cost": 0.0003134096309214199,
"pnl": 19.496089694227255,
"hold_days": 6,
"result": "Win"
},
{
"date": "2025-08-04 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 38.81,
"spot": 43.547935485839844,
"premium": 0.07631244215648936,
"close_cost": 0.0012067786162033162,
"pnl": 1.530566354028604,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-08-12 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 42.76,
"spot": 47.16282272338867,
"premium": 0.20387439304942667,
"close_cost": 0.04814236063376276,
"pnl": 9.59320324156639,
"hold_days": 1,
"result": "Win"
},
{
"date": "2025-08-25 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 43.59,
"spot": 44.60850143432617,
"premium": 0.23350383928957275,
"close_cost": 0.0,
"pnl": 17.370383928957274,
"hold_days": 7,
"result": "Win"
},
{
"date": "2025-08-27 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 41.93,
"spot": 45.330482482910156,
"premium": 0.33365346119653694,
"close_cost": 0.12654794173393213,
"pnl": 14.73055194626048,
"hold_days": 2,
"result": "Win"
},
{
"date": "2025-09-03 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 40.76,
"spot": 44.36949920654297,
"premium": 0.20378308864153194,
"close_cost": 0.06833610823069369,
"pnl": 7.564698041083824,
"hold_days": 1,
"result": "Win"
},
{
"date": "2025-09-11 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 43.94,
"spot": 47.58605194091797,
"premium": 0.18230798811836024,
"close_cost": 0.03431214511036429,
"pnl": 8.819584300799594,
"hold_days": 2,
"result": "Win"
},
{
"date": "2025-09-26 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 46.87,
"spot": 50.46242141723633,
"premium": 0.09773871361597575,
"close_cost": 0.03636038484441828,
"pnl": 0.15783287715574623,
"hold_days": 1,
"result": "Win"
},
{
"date": "2025-10-13 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 45.48,
"spot": 51.44924545288086,
"premium": 0.3973731113950638,
"close_cost": 0.02545040672940546,
"pnl": 31.212270466565837,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-10-20 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 48.72,
"spot": 53.771759033203125,
"premium": 0.5350110743825365,
"close_cost": 0.08535188421012219,
"pnl": 38.98591901724143,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-10-27 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 51.8,
"spot": 58.028045654296875,
"premium": 0.6423018604945661,
"close_cost": 0.07661382961036578,
"pnl": 50.58880308842002,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-11-03 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 54.68,
"spot": 58.97001266479492,
"premium": 0.832744356436832,
"close_cost": 0.30740027044818063,
"pnl": 46.55440859886514,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-11-10 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 49.56,
"spot": 56.17401885986328,
"premium": 0.6046077804997161,
"close_cost": 0.03662807947514746,
"pnl": 50.81797010245687,
"hold_days": 3,
"result": "Win"
},
{
"date": "2025-11-21 00:00:00",
"type": "PUT",
"action": "Assigned",
"strike": 49.07,
"spot": 47.327518463134766,
"premium": 0.6957166706715263,
"close_cost": 1.7424815368652347,
"pnl": -107.66648661937083,
"hold_days": 7,
"result": "Loss"
},
{
"date": "2025-11-28 00:00:00",
"type": "CALL",
"action": "Call_Away",
"strike": 48.27,
"spot": 54.36484909057617,
"premium": 1.442778747749827,
"close_cost": 6.094849090576169,
"pnl": -468.19703428263415,
"hold_days": 7,
"result": "Loss"
},
{
"date": "2025-12-02 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 51.1,
"spot": 55.12240982055664,
"premium": 0.8885543779888518,
"close_cost": 0.22771800847670498,
"pnl": 60.10363695121468,
"hold_days": 4,
"result": "Win"
},
{
"date": "2025-12-09 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 52.61,
"spot": 55.83012390136719,
"premium": 0.7019645425702308,
"close_cost": 0.1930723601146358,
"pnl": 44.9092182455595,
"hold_days": 4,
"result": "Win"
},
{
"date": "2025-12-16 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 49.49,
"spot": 52.161949157714844,
"premium": 0.5255269728821705,
"close_cost": 0.22041313720210542,
"pnl": 24.53138356800651,
"hold_days": 4,
"result": "Win"
},
{
"date": "2025-12-22 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 50.15,
"spot": 54.075782775878906,
"premium": 0.47745539216248645,
"close_cost": 0.13864091225912745,
"pnl": 27.9014479903359,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-01-02 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 51.91,
"spot": 52.263587951660156,
"premium": 0.2968084430280751,
"close_cost": 0.0,
"pnl": 23.700844302807507,
"hold_days": 7,
"result": "Win"
},
{
"date": "2026-01-05 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 49.13,
"spot": 53.51152420043945,
"premium": 0.27308307351277783,
"close_cost": 0.041062341360688315,
"pnl": 17.22207321520895,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-01-12 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 52.33,
"spot": 55.79774475097656,
"premium": 0.369411761450964,
"close_cost": 0.14436308920966834,
"pnl": 16.524867224129565,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-01-22 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 50.81,
"spot": 53.811031341552734,
"premium": 0.1477798974439022,
"close_cost": 0.0022624023227785783,
"pnl": 8.57174951211236,
"hold_days": 6,
"result": "Win"
},
{
"date": "2026-01-26 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 51.03,
"spot": 54.98908615112305,
"premium": 0.2433979224470182,
"close_cost": 0.050428038206870696,
"pnl": 13.316988424014749,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-02-02 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 50.68,
"spot": 55.00904846191406,
"premium": 0.29612130729873165,
"close_cost": 0.05015753030630954,
"pnl": 18.61637769924221,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-02-09 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.48,
"spot": 51.654598236083984,
"premium": 0.563188229925693,
"close_cost": 0.13619518544464437,
"pnl": 36.71930444810485,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-02-17 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 45.49,
"spot": 48.27019119262695,
"premium": 0.5999675089350056,
"close_cost": 0.220282559906253,
"pnl": 31.988494902875264,
"hold_days": 4,
"result": "Win"
},
{
"date": "2026-02-24 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 46.98,
"spot": 49.69783020019531,
"premium": 0.4884002354982204,
"close_cost": 0.21242084272299522,
"pnl": 21.61793927752252,
"hold_days": 4,
"result": "Win"
},
{
"date": "2026-03-02 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 46.47,
"spot": 49.61796569824219,
"premium": 0.5888532941584685,
"close_cost": 0.25315686325419495,
"pnl": 27.589643090427362,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-03-09 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 44.61,
"spot": 49.308475494384766,
"premium": 0.4907682450346762,
"close_cost": 0.04880422279325147,
"pnl": 38.21640222414247,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-03-16 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 43.1,
"spot": 47.38166046142578,
"premium": 0.36589208886678115,
"close_cost": 0.05541615318789983,
"pnl": 25.06759356788813,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-03-23 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 40.43,
"spot": 44.496429443359375,
"premium": 0.4571711027535077,
"close_cost": 0.07888536647136002,
"pnl": 31.84857362821477,
"hold_days": 3,
"result": "Win"
},
{
"date": "2026-03-31 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 36.45,
"spot": 41.68000030517578,
"premium": 0.4899103648679617,
"close_cost": 0.03275982095700081,
"pnl": 39.735054391096085,
"hold_days": 4,
"result": "Win"
},
{
"date": "2026-04-08 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 41.45,
"spot": 48.0,
"premium": 0.782588368441985,
"close_cost": 0.12738446714409601,
"pnl": 59.54039012978889,
"hold_days": 2,
"result": "Win"
},
{
"date": "2026-04-14 00:00:00",
"type": "PUT",
"action": "TP_Close",
"strike": 47.62,
"spot": 53.40999984741211,
"premium": 1.0000270235383653,
"close_cost": 0.3787688574986472,
"pnl": 56.14581660397181,
"hold_days": 1,
"result": "Win"
}
]
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

Some files were not shown because too many files have changed in this diff Show More