commit a94bd44c3ad2375f64b9db3023d3ad9df39970a5 Author: Daniel Date: Sat Feb 28 18:39:00 2026 +0800 Initial commit Made-with: Cursor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..93f453b --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Python +.venv/ +venv/ +__pycache__/ +*.pyc +*.pyc +.pytest_cache/ +.coverage +*.egg-info/ + +# Node +node_modules/ + +# IDE +.vscode/ +.idea/ + +# Env +.env +.env.local diff --git a/srde/README.md b/srde/README.md new file mode 100644 index 0000000..b0b660a --- /dev/null +++ b/srde/README.md @@ -0,0 +1,54 @@ +# SRDE - Structured Risk Decision Engine + +Deterministic 风控投资系统 Demo + +## 项目结构 + +``` +srde/ +├── backend/ # FastAPI 后端(Python) +├── frontend/ # React Web 前端 +├── miniprogram/ # 微信小程序前端 +└── docker-compose.yml +``` + +## 快速启动 + +### 方式一:Docker(需 Docker Desktop) + +```bash +cd srde +docker compose up --build +``` + +- Web 前端: http://localhost +- API: http://localhost:8000 + +### 方式二:本地开发 + +**后端** +```bash +cd srde/backend +python -m venv .venv && source .venv/bin/activate +pip install -r requirements.txt +# 启动 PostgreSQL 后: +export DATABASE_URL=postgresql://user:pass@localhost:5432/srde +alembic upgrade head +uvicorn app.main:app --reload +``` + +**Web 前端** +```bash +cd srde/frontend && npm install && npm run dev +``` + +**微信小程序** +1. 用微信开发者工具打开 `srde/miniprogram` +2. 默认使用模拟数据,可离线浏览 +3. 对接后端:修改 `miniprogram/services/api.ts` 中 `USE_MOCK=false` 和 `BASE_URL` + +## Demo 说明 + +- **Web**:登录、账户、创建交易、交易列表、复盘 +- **小程序**:风控首页、分步创建交易、交易详情、复盘、统计、个人中心 +- **后端**:JWT 认证、风控引擎、仓位计算、回撤/锁仓规则 diff --git a/srde/miniprogram/README.md b/srde/miniprogram/README.md new file mode 100644 index 0000000..d1b562a --- /dev/null +++ b/srde/miniprogram/README.md @@ -0,0 +1,33 @@ +# SRDE Risk Control Mini Program + +微信小程序风控前端,对接 SRDE 后端 API。 + +## 运行方式 + +1. 安装 [微信开发者工具](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html) +2. 打开项目:选择 `srde/miniprogram` 目录 +3. 使用「测试号」或填写 AppID 后预览 + +## 对接真实后端 + +1. 修改 `services/api.ts`: + - 设置 `USE_MOCK = false` + - 设置 `BASE_URL = 'https://your-api.com'`(你的后端地址) + +2. 登录流程: + - 后端需提供 `POST /auth/login`,返回 `{ access_token }` + - 小程序将 token 存入 `wx.setStorageSync('srde_token', token)` + - 后续请求自动携带 `Authorization: Bearer {token}` + +3. API 映射(后端需实现): + - `GET /account/status` → 账户状态 + - `POST /account/capital` → 设置资金 + - `POST /trade/create` → 创建交易 + - `POST /trade/{id}/close` → 关闭交易 + - `GET /trade/` → 交易列表 + - `POST /review/{id}`、`GET /review/{id}` → 复盘 + - `GET /statistics` → 统计数据(胜率、盈亏比等) + +## 模拟模式 + +默认 `USE_MOCK = true`,使用本地模拟数据,不依赖后端即可完整浏览流程。 diff --git a/srde/miniprogram/app.json b/srde/miniprogram/app.json new file mode 100644 index 0000000..f624d6d --- /dev/null +++ b/srde/miniprogram/app.json @@ -0,0 +1,27 @@ +{ + "pages": [ + "pages/dashboard/dashboard", + "pages/create/create", + "pages/trade-detail/trade-detail", + "pages/review/review", + "pages/statistics/statistics", + "pages/profile/profile" + ], + "window": { + "backgroundTextStyle": "dark", + "navigationBarBackgroundColor": "#2a2a2a", + "navigationBarTitleText": "SRDE 风控", + "navigationBarTextStyle": "white" + }, + "tabBar": { + "color": "#999", + "selectedColor": "#5a9", + "backgroundColor": "#2a2a2a", + "list": [ + { "pagePath": "pages/dashboard/dashboard", "text": "首页" }, + { "pagePath": "pages/create/create", "text": "创建" }, + { "pagePath": "pages/statistics/statistics", "text": "统计" }, + { "pagePath": "pages/profile/profile", "text": "我的" } + ] + } +} diff --git a/srde/miniprogram/app.ts b/srde/miniprogram/app.ts new file mode 100644 index 0000000..67a57ed --- /dev/null +++ b/srde/miniprogram/app.ts @@ -0,0 +1,32 @@ +// SRDE Risk Control Mini Program +interface IAccountState { + total_capital: number; + current_capital: number; + current_drawdown: number; + max_drawdown: number; + consecutive_losses: number; + trading_locked_until: string | null; + status: 'tradable' | 'compressed' | 'locked'; +} + +interface IAppOption { + globalData: { + token: string | null; + account: IAccountState | null; + accountLoadedAt: number; + }; +} + +App({ + globalData: { + token: wx.getStorageSync('srde_token') || null, + account: null, + accountLoadedAt: 0, + }, + onLaunch() { + const token = wx.getStorageSync('srde_token'); + if (token) { + this.globalData.token = token; + } + }, +}); diff --git a/srde/miniprogram/app.wxss b/srde/miniprogram/app.wxss new file mode 100644 index 0000000..c07daee --- /dev/null +++ b/srde/miniprogram/app.wxss @@ -0,0 +1,27 @@ +page { + background-color: #2a2a2a; + color: #e0e0e0; + font-size: 14px; +} + +.card { + background: #fff; + border-radius: 16rpx; + padding: 24rpx; + margin: 20rpx; +} + +.card-title { + color: #333; + font-size: 28rpx; + font-weight: 500; + margin-bottom: 16rpx; +} + +.text-muted { + color: #888; +} + +.status-tradable { color: #5a9; } +.status-compressed { color: #b95; } +.status-locked { color: #a44; } diff --git a/srde/miniprogram/components/cooldown-button/cooldown-button.json b/srde/miniprogram/components/cooldown-button/cooldown-button.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/srde/miniprogram/components/cooldown-button/cooldown-button.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/srde/miniprogram/components/cooldown-button/cooldown-button.ts b/srde/miniprogram/components/cooldown-button/cooldown-button.ts new file mode 100644 index 0000000..15de85d --- /dev/null +++ b/srde/miniprogram/components/cooldown-button/cooldown-button.ts @@ -0,0 +1,22 @@ +Component({ + properties: { + text: { type: String, value: '确认' }, + duration: { type: Number, value: 3 }, + }, + data: { countdown: 0, loading: false }, + methods: { + onTap() { + if (this.data.countdown > 0 || this.data.loading) return; + this.setData({ countdown: this.properties.duration }); + const t = setInterval(() => { + const n = this.data.countdown - 1; + this.setData({ countdown: n }); + if (n <= 0) clearInterval(t); + }, 1000); + this.triggerEvent('confirm'); + }, + setLoading(v: boolean) { + this.setData({ loading: v }); + }, + }, +}); diff --git a/srde/miniprogram/components/cooldown-button/cooldown-button.wxml b/srde/miniprogram/components/cooldown-button/cooldown-button.wxml new file mode 100644 index 0000000..556f795 --- /dev/null +++ b/srde/miniprogram/components/cooldown-button/cooldown-button.wxml @@ -0,0 +1,8 @@ + diff --git a/srde/miniprogram/components/cooldown-button/cooldown-button.wxss b/srde/miniprogram/components/cooldown-button/cooldown-button.wxss new file mode 100644 index 0000000..6df7165 --- /dev/null +++ b/srde/miniprogram/components/cooldown-button/cooldown-button.wxss @@ -0,0 +1,12 @@ +.cooldown-btn { + width: 100%; + padding: 28rpx; + border-radius: 16rpx; + font-size: 32rpx; + background: #5a9; + color: #fff; + border: none; +} +.cooldown-btn[disabled] { + background: #888; +} diff --git a/srde/miniprogram/components/risk-card/risk-card.json b/srde/miniprogram/components/risk-card/risk-card.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/srde/miniprogram/components/risk-card/risk-card.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/srde/miniprogram/components/risk-card/risk-card.ts b/srde/miniprogram/components/risk-card/risk-card.ts new file mode 100644 index 0000000..ffd4d93 --- /dev/null +++ b/srde/miniprogram/components/risk-card/risk-card.ts @@ -0,0 +1,7 @@ +Component({ + properties: { + title: { type: String, value: '' }, + value: { type: String, value: '' }, + sub: { type: String, value: '' }, + }, +}); diff --git a/srde/miniprogram/components/risk-card/risk-card.wxml b/srde/miniprogram/components/risk-card/risk-card.wxml new file mode 100644 index 0000000..a3958cd --- /dev/null +++ b/srde/miniprogram/components/risk-card/risk-card.wxml @@ -0,0 +1,5 @@ + + {{title}} + {{value}} + {{sub}} + diff --git a/srde/miniprogram/components/risk-card/risk-card.wxss b/srde/miniprogram/components/risk-card/risk-card.wxss new file mode 100644 index 0000000..c838753 --- /dev/null +++ b/srde/miniprogram/components/risk-card/risk-card.wxss @@ -0,0 +1,14 @@ +.risk-card { + flex: 1; + min-width: 0; +} +.risk-card .value { + font-size: 36rpx; + font-weight: 600; + color: #333; +} +.risk-card .sub { + font-size: 24rpx; + color: #888; + margin-top: 8rpx; +} diff --git a/srde/miniprogram/components/status-badge/status-badge.json b/srde/miniprogram/components/status-badge/status-badge.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/srde/miniprogram/components/status-badge/status-badge.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/srde/miniprogram/components/status-badge/status-badge.ts b/srde/miniprogram/components/status-badge/status-badge.ts new file mode 100644 index 0000000..05a52d7 --- /dev/null +++ b/srde/miniprogram/components/status-badge/status-badge.ts @@ -0,0 +1,19 @@ +const STATUS_TEXT: Record = { + tradable: '可交易', + compressed: '风险压缩', + locked: '锁仓', +}; + +Component({ + properties: { + status: { type: String, value: 'tradable' }, + text: { type: String, value: '' }, + }, + lifetimes: { + attached() { + const s = this.properties.status; + const t = this.properties.text || STATUS_TEXT[s] || s; + this.setData({ text: t }); + }, + }, +}); diff --git a/srde/miniprogram/components/status-badge/status-badge.wxml b/srde/miniprogram/components/status-badge/status-badge.wxml new file mode 100644 index 0000000..5534a0c --- /dev/null +++ b/srde/miniprogram/components/status-badge/status-badge.wxml @@ -0,0 +1 @@ +{{text || (status === 'tradable' ? '可交易' : (status === 'compressed' ? '风险压缩' : '锁仓'))}} diff --git a/srde/miniprogram/components/status-badge/status-badge.wxss b/srde/miniprogram/components/status-badge/status-badge.wxss new file mode 100644 index 0000000..afc980c --- /dev/null +++ b/srde/miniprogram/components/status-badge/status-badge.wxss @@ -0,0 +1,9 @@ +.badge { + display: inline-block; + padding: 8rpx 20rpx; + border-radius: 24rpx; + font-size: 24rpx; +} +.status-tradable { background: rgba(85,170,153,0.2); color: #5a9; } +.status-compressed { background: rgba(187,153,85,0.2); color: #b95; } +.status-locked { background: rgba(170,68,68,0.2); color: #a44; } diff --git a/srde/miniprogram/components/trade-item/trade-item.json b/srde/miniprogram/components/trade-item/trade-item.json new file mode 100644 index 0000000..a89ef4d --- /dev/null +++ b/srde/miniprogram/components/trade-item/trade-item.json @@ -0,0 +1,4 @@ +{ + "component": true, + "usingComponents": {} +} diff --git a/srde/miniprogram/components/trade-item/trade-item.ts b/srde/miniprogram/components/trade-item/trade-item.ts new file mode 100644 index 0000000..8e7b8ee --- /dev/null +++ b/srde/miniprogram/components/trade-item/trade-item.ts @@ -0,0 +1,18 @@ +Component({ + properties: { + id: String, + symbol: String, + direction: String, + entry_price: [String, Number], + status: String, + position_size: [String, Number], + pnl: Number, + }, + methods: { + onTap() { + if (this.data.id) { + wx.navigateTo({ url: `/pages/trade-detail/trade-detail?id=${this.data.id}` }); + } + }, + }, +}); diff --git a/srde/miniprogram/components/trade-item/trade-item.wxml b/srde/miniprogram/components/trade-item/trade-item.wxml new file mode 100644 index 0000000..6e6c22d --- /dev/null +++ b/srde/miniprogram/components/trade-item/trade-item.wxml @@ -0,0 +1,14 @@ + + + {{symbol}} + {{direction === 'long' ? '多' : '空'}} + + + 入场 {{entry_price}} + {{status === 'open' ? '持仓' : '已平'}} + + + 盈亏 + {{pnl >= 0 ? '+' : ''}}{{pnl}} + + diff --git a/srde/miniprogram/components/trade-item/trade-item.wxss b/srde/miniprogram/components/trade-item/trade-item.wxss new file mode 100644 index 0000000..8c90eff --- /dev/null +++ b/srde/miniprogram/components/trade-item/trade-item.wxss @@ -0,0 +1,9 @@ +.trade-item { cursor: pointer; } +.trade-item .row { display: flex; justify-content: space-between; margin-bottom: 8rpx; } +.trade-item .symbol { font-weight: 600; color: #333; } +.trade-item .dir { font-size: 24rpx; } +.trade-item .dir.long { color: #5a9; } +.trade-item .dir.short { color: #95a; } +.trade-item .sub { font-size: 24rpx; color: #888; } +.trade-item .profit { color: #5a9; } +.trade-item .loss { color: #a44; } diff --git a/srde/miniprogram/pages/create/create.json b/srde/miniprogram/pages/create/create.json new file mode 100644 index 0000000..0de3e9e --- /dev/null +++ b/srde/miniprogram/pages/create/create.json @@ -0,0 +1,6 @@ +{ + "usingComponents": { + "cooldown-button": "/components/cooldown-button/cooldown-button" + }, + "navigationBarTitleText": "创建交易" +} diff --git a/srde/miniprogram/pages/create/create.ts b/srde/miniprogram/pages/create/create.ts new file mode 100644 index 0000000..41a26b8 --- /dev/null +++ b/srde/miniprogram/pages/create/create.ts @@ -0,0 +1,65 @@ +Page({ + data: { + step: 1, + logic: '', + symbol: '', + direction: 'long' as 'long' | 'short', + entry_price: '', + stop_loss: '', + take_profit: '', + upPct: 0, + downPct: 0, + odds: '', + position_size: '', + }, + onLogicInput(e: WechatMiniprogram.CustomEvent) { + this.setData({ logic: (e.detail.value as string).trim() }); + }, + onSymbolInput(e: WechatMiniprogram.CustomEvent) { + this.setData({ symbol: (e.detail.value as string).trim() }); + }, + setDir(e: WechatMiniprogram.CustomEvent) { + this.setData({ direction: e.currentTarget.dataset.dir as 'long' | 'short' }); + }, + onEntryInput(e: WechatMiniprogram.CustomEvent) { + this.setData({ entry_price: e.detail.value as string }, () => this.calc()); + }, + onStopInput(e: WechatMiniprogram.CustomEvent) { + this.setData({ stop_loss: e.detail.value as string }, () => this.calc()); + }, + onTpInput(e: WechatMiniprogram.CustomEvent) { + this.setData({ take_profit: e.detail.value as string }, () => this.calc()); + }, + calc() { + const { entry_price, stop_loss, take_profit, direction } = this.data; + const e = parseFloat(entry_price); + const s = parseFloat(stop_loss); + const t = parseFloat(take_profit); + if (!e || !s || !t) return; + const upPct = direction === 'long' ? ((t - e) / e * 100) : ((e - t) / e * 100); + const downPct = direction === 'long' ? ((e - s) / e * 100) : ((s - e) / e * 100); + const risk = direction === 'long' ? e - s : s - e; + const reward = direction === 'long' ? t - e : e - t; + const odds = risk > 0 && reward > 0 ? (reward / risk).toFixed(2) : ''; + this.setData({ upPct: upPct.toFixed(1), downPct: downPct.toFixed(1), odds }); + }, + nextStep() { + const { step, logic } = this.data; + if (step === 1 && logic.length < 30) return; + if (step === 2) { + this.setData({ position_size: '0.5', odds: this.data.odds || '-' }); + } + this.setData({ step: step + 1 }); + }, + prevStep() { + this.setData({ step: this.data.step - 1 }); + }, + onConfirm() { + wx.showLoading({ title: '提交中' }); + setTimeout(() => { + wx.hideLoading(); + wx.showToast({ title: '已创建' }); + wx.navigateBack(); + }, 800); + }, +}); diff --git a/srde/miniprogram/pages/create/create.wxml b/srde/miniprogram/pages/create/create.wxml new file mode 100644 index 0000000..0e87b33 --- /dev/null +++ b/srde/miniprogram/pages/create/create.wxml @@ -0,0 +1,33 @@ + + 步骤 {{step}} / 3 + + 交易逻辑(≥30字) +