Initial commit

Made-with: Cursor
This commit is contained in:
Daniel
2026-02-28 18:39:00 +08:00
commit a94bd44c3a
49 changed files with 917 additions and 0 deletions

20
.gitignore vendored Normal file
View File

@@ -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

54
srde/README.md Normal file
View File

@@ -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 认证、风控引擎、仓位计算、回撤/锁仓规则

View File

@@ -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`,使用本地模拟数据,不依赖后端即可完整浏览流程。

27
srde/miniprogram/app.json Normal file
View File

@@ -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": "我的" }
]
}
}

32
srde/miniprogram/app.ts Normal file
View File

@@ -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<IAppOption>({
globalData: {
token: wx.getStorageSync('srde_token') || null,
account: null,
accountLoadedAt: 0,
},
onLaunch() {
const token = wx.getStorageSync('srde_token');
if (token) {
this.globalData.token = token;
}
},
});

27
srde/miniprogram/app.wxss Normal file
View File

@@ -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; }

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -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 });
},
},
});

View File

@@ -0,0 +1,8 @@
<button
class="cooldown-btn"
disabled="{{countdown > 0 || loading}}"
loading="{{loading}}"
bindtap="onTap"
>
{{countdown > 0 ? countdown + '秒' : (loading ? '提交中' : text)}}
</button>

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,7 @@
Component({
properties: {
title: { type: String, value: '' },
value: { type: String, value: '' },
sub: { type: String, value: '' },
},
});

View File

@@ -0,0 +1,5 @@
<view class="risk-card card">
<view class="card-title">{{title}}</view>
<view class="value">{{value}}</view>
<view class="sub" wx:if="{{sub}}">{{sub}}</view>
</view>

View File

@@ -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;
}

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,19 @@
const STATUS_TEXT: Record<string, string> = {
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 });
},
},
});

View File

@@ -0,0 +1 @@
<view class="badge status-{{status}}">{{text || (status === 'tradable' ? '可交易' : (status === 'compressed' ? '风险压缩' : '锁仓'))}}</view>

View File

@@ -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; }

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -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}` });
}
},
},
});

View File

@@ -0,0 +1,14 @@
<view class="trade-item card" bindtap="onTap">
<view class="row">
<text class="symbol">{{symbol}}</text>
<text class="dir {{direction}}">{{direction === 'long' ? '多' : '空'}}</text>
</view>
<view class="row sub">
<text>入场 {{entry_price}}</text>
<text class="status-{{status}}">{{status === 'open' ? '持仓' : '已平'}}</text>
</view>
<view class="row sub" wx:if="{{pnl !== undefined}}">
<text>盈亏</text>
<text class="{{pnl >= 0 ? 'profit' : 'loss'}}">{{pnl >= 0 ? '+' : ''}}{{pnl}}</text>
</view>
</view>

View File

@@ -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; }

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"cooldown-button": "/components/cooldown-button/cooldown-button"
},
"navigationBarTitleText": "创建交易"
}

View File

@@ -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);
},
});

View File

@@ -0,0 +1,33 @@
<view class="page">
<view class="step-indicator">步骤 {{step}} / 3</view>
<view wx:if="{{step === 1}}" class="section card">
<view class="label">交易逻辑≥30字</view>
<textarea class="textarea" value="{{logic}}" placeholder="请输入你的交易逻辑..." maxlength="500" bindinput="onLogicInput" />
<view class="count {{logic.length >= 30 ? 'ok' : ''}}">{{logic.length}}/30</view>
</view>
<view wx:if="{{step === 2}}" class="section card">
<view class="label">标的</view>
<input class="input" value="{{symbol}}" placeholder="如 BTCUSDT" bindinput="onSymbolInput" />
<view class="label">方向</view>
<view class="row">
<view class="option {{direction === 'long' ? 'active' : ''}}" bindtap="setDir" data-dir="long">做多</view>
<view class="option {{direction === 'short' ? 'active' : ''}}" bindtap="setDir" data-dir="short">做空</view>
</view>
<view class="label">入场价、止损价、止盈价</view>
<input class="input" type="digit" value="{{entry_price}}" placeholder="入场价" bindinput="onEntryInput" />
<input class="input" type="digit" value="{{stop_loss}}" placeholder="止损价" bindinput="onStopInput" />
<input class="input" type="digit" value="{{take_profit}}" placeholder="止盈价" bindinput="onTpInput" />
<view class="row calc">上涨% {{upPct}}% 下跌% {{downPct}}%</view>
</view>
<view wx:if="{{step === 3}}" class="section card">
<view class="label">建议仓位(来自后端)</view>
<view class="value">{{position_size || '-'}}</view>
<view class="label">赔率</view>
<view class="value">{{odds || '-'}}</view>
<cooldown-button text="确认提交" duration="3" bind:confirm="onConfirm" />
</view>
<view class="nav">
<button wx:if="{{step > 1}}" bindtap="prevStep">上一步</button>
<button wx:if="{{step < 3}}" bindtap="nextStep" disabled="{{step === 1 && logic.length < 30}}">下一步</button>
</view>
</view>

View File

@@ -0,0 +1,15 @@
.page { padding: 20rpx; }
.section { margin-bottom: 24rpx; }
.step-indicator { color: #888; margin-bottom: 24rpx; font-size: 28rpx; }
.label { color: #666; margin-bottom: 12rpx; font-size: 26rpx; }
.input, .textarea { width: 100%; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; box-sizing: border-box; }
.textarea { min-height: 160rpx; }
.count { font-size: 24rpx; color: #888; }
.count.ok { color: #5a9; }
.row { display: flex; gap: 24rpx; margin-bottom: 24rpx; }
.option { flex: 1; padding: 24rpx; text-align: center; border: 1rpx solid #ddd; border-radius: 12rpx; }
.option.active { background: #5a9; color: #fff; border-color: #5a9; }
.calc { justify-content: space-between; color: #666; }
.value { font-size: 36rpx; font-weight: 600; color: #333; margin-bottom: 24rpx; }
.nav { display: flex; gap: 24rpx; margin-top: 40rpx; }
.nav button { flex: 1; }

View File

@@ -0,0 +1,8 @@
{
"usingComponents": {
"risk-card": "/components/risk-card/risk-card",
"status-badge": "/components/status-badge/status-badge",
"trade-item": "/components/trade-item/trade-item"
},
"navigationBarTitleText": "风控首页"
}

View File

@@ -0,0 +1,37 @@
import { mockAccount, mockTrades } from '../../services/api';
Page({
data: {
loading: true,
error: '',
account: mockAccount,
trades: mockTrades.slice(0, 3),
},
onLoad() {
this.load();
},
onShow() {
const app = getApp<IAppOption>();
if (app.globalData.account && Date.now() - app.globalData.accountLoadedAt < 30000) {
this.setData({ account: app.globalData.account });
} else {
this.load();
}
},
load() {
this.setData({ loading: true, error: '' });
const app = getApp<IAppOption>();
setTimeout(() => {
app.globalData.account = mockAccount;
app.globalData.accountLoadedAt = Date.now();
this.setData({
loading: false,
account: mockAccount,
trades: mockTrades.slice(0, 3),
});
}, 300);
},
goCreate() {
wx.navigateTo({ url: '/pages/create/create' });
},
});

View File

@@ -0,0 +1,36 @@
<view class="page" wx:if="{{!loading && !error}}">
<view class="section">
<status-badge status="{{account.status}}" />
</view>
<view class="section card">
<view class="row wrap">
<risk-card title="总资金" value="{{account.total_capital}}" />
<risk-card title="当前回撤" value="{{account.current_drawdown}}%" />
<risk-card title="连续亏损" value="{{account.consecutive_losses}}" />
</view>
<view class="row wrap">
<risk-card title="今日最大风险" value="{{account.daily_risk_limit}}" />
<risk-card title="单笔最大风险" value="{{account.single_risk_limit}}" />
</view>
<view class="lock-msg" wx:if="{{account.status === 'locked'}}">当前处于锁仓期,请勿交易</view>
</view>
<view class="section" wx:if="{{account.status !== 'locked'}}">
<button class="btn-primary" bindtap="goCreate">创建交易计划</button>
</view>
<view class="section">
<view class="card-title">最近交易</view>
<trade-item
wx:for="{{trades}}"
wx:key="id"
id="{{item.id}}"
symbol="{{item.symbol}}"
direction="{{item.direction}}"
entry_price="{{item.entry_price}}"
status="{{item.status}}"
position_size="{{item.position_size}}"
pnl="{{item.pnl}}"
/>
</view>
</view>
<view class="loading" wx:if="{{loading}}">加载中...</view>
<view class="error" wx:if="{{error}}">{{error}}</view>

View File

@@ -0,0 +1,15 @@
.page { padding: 20rpx; }
.section { margin-bottom: 24rpx; }
.row.wrap { display: flex; flex-wrap: wrap; gap: 16rpx; }
.card-title { color: #333; font-size: 28rpx; margin-bottom: 16rpx; padding: 0 20rpx; }
.btn-primary {
width: 100%;
padding: 28rpx;
background: #5a9;
color: #fff;
border-radius: 16rpx;
font-size: 32rpx;
border: none;
}
.lock-msg { color: #a44; padding: 16rpx 20rpx; font-size: 26rpx; }
.loading, .error { padding: 80rpx; text-align: center; color: #888; }

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"status-badge": "/components/status-badge/status-badge"
},
"navigationBarTitleText": "我的"
}

View File

@@ -0,0 +1,49 @@
import { mockAccount } from '../../services/api';
Page({
data: {
account: mockAccount,
capital: '',
saving: false,
resetting: false,
},
onLoad() {
const app = getApp<IAppOption>();
const acc = app.globalData.account || mockAccount;
this.setData({ account: acc, capital: String(acc.total_capital) });
},
onCapitalInput(e: WechatMiniprogram.CustomEvent) {
this.setData({ capital: e.detail.value as string });
},
onSaveCapital() {
const cap = parseFloat(this.data.capital);
if (!cap || cap <= 0) {
wx.showToast({ title: '请输入有效金额', icon: 'none' });
return;
}
this.setData({ saving: true });
setTimeout(() => {
const acc = { ...mockAccount, total_capital: cap, current_capital: cap };
getApp<IAppOption>().globalData.account = acc;
this.setData({ saving: false, account: acc });
wx.showToast({ title: '已保存' });
}, 500);
},
onReset() {
wx.showModal({
title: '确认重置',
content: '将清空所有交易数据,确定吗?',
success: (res) => {
if (res.confirm) {
this.setData({ resetting: true });
setTimeout(() => {
const acc = { ...mockAccount, current_capital: mockAccount.total_capital, consecutive_losses: 0, trading_locked_until: null, status: 'tradable' as const };
getApp<IAppOption>().globalData.account = acc;
this.setData({ resetting: false, account: acc });
wx.showToast({ title: '已重置' });
}, 500);
}
},
});
},
});

View File

@@ -0,0 +1,15 @@
<view class="page">
<view class="card">
<view class="label">当前状态</view>
<status-badge status="{{account.status}}" />
<view class="lock-until" wx:if="{{account.trading_locked_until}}">锁仓至 {{account.trading_locked_until}}</view>
</view>
<view class="card">
<view class="label">修改总资金</view>
<input class="input" type="digit" value="{{capital}}" placeholder="输入新总资金" bindinput="onCapitalInput" />
<button class="btn" bindtap="onSaveCapital" loading="{{saving}}">保存</button>
</view>
<view class="card danger">
<button class="btn-reset" bindtap="onReset" loading="{{resetting}}">重置账户(危险操作)</button>
</view>
</view>

View File

@@ -0,0 +1,6 @@
.page { padding: 20rpx; }
.label { color: #888; margin-bottom: 12rpx; }
.input { width: 100%; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; box-sizing: border-box; margin-bottom: 24rpx; }
.btn { width: 100%; padding: 28rpx; background: #5a9; color: #fff; border-radius: 16rpx; border: none; }
.lock-until { color: #a44; margin-top: 16rpx; font-size: 26rpx; }
.btn-reset { width: 100%; padding: 28rpx; background: #a44; color: #fff; border-radius: 16rpx; border: none; }

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "复盘"
}

View File

@@ -0,0 +1,36 @@
Page({
data: {
id: '',
exit_price: '',
loading: false,
submitted: false,
pnlPct: 0,
violated: false,
score: 0,
aiSummary: '',
},
onLoad(opt: { id?: string }) {
this.setData({ id: opt.id || '' });
},
onExitInput(e: WechatMiniprogram.CustomEvent) {
this.setData({ exit_price: e.detail.value as string });
},
onSubmit() {
const exit = parseFloat(this.data.exit_price);
if (!exit) {
wx.showToast({ title: '请输入退出价', icon: 'none' });
return;
}
this.setData({ loading: true });
setTimeout(() => {
this.setData({
loading: false,
submitted: true,
pnlPct: 2.1,
violated: false,
score: 85,
aiSummary: '本次交易执行符合计划,止盈目标达成。建议继续保持当前风险控制节奏。',
});
}, 800);
},
});

View File

@@ -0,0 +1,16 @@
<view class="page">
<view class="card" wx:if="{{!submitted}}">
<view class="label">退出价</view>
<input class="input" type="digit" value="{{exit_price}}" bindinput="onExitInput" placeholder="输入实际退出价格" />
<button class="btn" bindtap="onSubmit" loading="{{loading}}">生成复盘</button>
</view>
<view class="card" wx:else>
<view class="row"><text class="label">盈亏 %</text><text class="{{pnlPct >= 0 ? 'profit' : 'loss'}}">{{pnlPct}}%</text></view>
<view class="row"><text class="label">是否违反止损</text><text>{{violated ? '是' : '否'}}</text></view>
<view class="row"><text class="label">纪律评分</text><text>{{score}}</text></view>
<view class="block">
<view class="label">AI 评语</view>
<text class="ai">{{aiSummary}}</text>
</view>
</view>
</view>

View File

@@ -0,0 +1,9 @@
.page { padding: 20rpx; }
.label { color: #888; margin-bottom: 12rpx; }
.input { width: 100%; padding: 20rpx; border: 1rpx solid #ddd; border-radius: 12rpx; box-sizing: border-box; margin-bottom: 24rpx; }
.btn { width: 100%; padding: 28rpx; background: #5a9; color: #fff; border-radius: 16rpx; border: none; }
.row { display: flex; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
.profit { color: #5a9; }
.loss { color: #a44; }
.block { padding: 24rpx 0; }
.ai { font-size: 28rpx; color: #333; line-height: 1.6; }

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {
"risk-card": "/components/risk-card/risk-card"
},
"navigationBarTitleText": "统计数据"
}

View File

@@ -0,0 +1,48 @@
import { mockStats } from '../../services/api';
Page({
data: {
loading: true,
stats: mockStats,
w: 300,
},
onLoad() {
const sys = wx.getSystemInfoSync();
this.setData({
stats: mockStats,
loading: false,
w: (sys.windowWidth || 320) - 40,
});
},
onReady() {
this.drawChart();
},
drawChart() {
const curve = mockStats.equity_curve;
const query = wx.createSelectorQuery().in(this);
query.select('#chart').fields({ node: true, size: true }).exec((res: any) => {
const canvas = res[0]?.node;
if (!canvas) return;
const ctx = canvas.getContext('2d');
const dpr = wx.getSystemInfoSync().pixelRatio || 2;
canvas.width = this.data.w * dpr;
canvas.height = 200 * dpr;
ctx.scale(dpr, dpr);
const w = this.data.w;
const h = 200;
const min = Math.min(...curve);
const max = Math.max(...curve);
const range = max - min || 1;
ctx.strokeStyle = '#5a9';
ctx.lineWidth = 2;
ctx.beginPath();
curve.forEach((v: number, i: number) => {
const x = (i / (curve.length - 1)) * (w - 20) + 10;
const y = h - 20 - ((v - min) / range) * (h - 40);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
});
ctx.stroke();
});
},
});

View File

@@ -0,0 +1,16 @@
<view class="page" wx:if="{{!loading}}">
<view class="section card">
<view class="row wrap">
<risk-card title="胜率" value="{{stats.win_rate}}%" />
<risk-card title="平均赔率" value="{{stats.avg_odds}}" />
<risk-card title="盈亏比" value="{{stats.profit_factor}}" />
<risk-card title="最大回撤" value="{{stats.max_drawdown}}%" />
<risk-card title="风险评分" value="{{stats.risk_score}}" />
</view>
</view>
<view class="section card">
<view class="card-title">权益曲线</view>
<canvas type="2d" id="chart" class="chart" style="width:{{w}}px;height:200px;"></canvas>
</view>
</view>
<view class="loading" wx:if="{{loading}}">加载中...</view>

View File

@@ -0,0 +1,5 @@
.page { padding: 20rpx; }
.row.wrap { display: flex; flex-wrap: wrap; gap: 16rpx; }
.card-title { color: #333; font-size: 28rpx; margin-bottom: 24rpx; }
.chart { width: 100%; background: #f8f8f8; border-radius: 12rpx; }
.loading { padding: 80rpx; text-align: center; color: #888; }

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationBarTitleText": "交易详情"
}

View File

@@ -0,0 +1,21 @@
import { mockTrades } from '../../services/api';
Page({
data: { trade: null as any },
onLoad(opt: { id?: string }) {
const id = opt.id;
const t = mockTrades.find((x: any) => x.id === id) || mockTrades[0];
this.setData({
trade: {
...t,
take_profit: (t as any).entry_price * 1.02,
stop_loss: (t as any).entry_price * 0.98,
position_size: 0.5,
logic: '示例交易逻辑文本,用于展示交易计划的结构与内容。',
},
});
},
onClose() {
wx.navigateTo({ url: '/pages/review/review?id=' + this.data.trade.id });
},
});

View File

@@ -0,0 +1,15 @@
<view class="page" wx:if="{{trade}}">
<view class="card">
<view class="row"><text class="label">标的</text><text>{{trade.symbol}}</text></view>
<view class="row"><text class="label">方向</text><text>{{trade.direction === 'long' ? '多' : '空'}}</text></view>
<view class="row"><text class="label">入场价</text><text>{{trade.entry_price}}</text></view>
<view class="row"><text class="label">目标价</text><text>{{trade.take_profit}}</text></view>
<view class="row"><text class="label">止损价</text><text>{{trade.stop_loss}}</text></view>
<view class="row"><text class="label">建议仓位</text><text>{{trade.position_size}}</text></view>
<view class="row"><text class="label">逻辑</text><text class="logic">{{trade.logic || '-'}}</text></view>
</view>
<view class="section" wx:if="{{trade.status === 'open'}}">
<button class="btn-danger" bindtap="onClose">关闭交易</button>
</view>
</view>
<view wx:else class="loading">加载中...</view>

View File

@@ -0,0 +1,6 @@
.page { padding: 20rpx; }
.row { display: flex; justify-content: space-between; padding: 20rpx 0; border-bottom: 1rpx solid #eee; }
.row .label { color: #888; }
.logic { flex: 1; margin-left: 24rpx; word-break: break-all; }
.btn-danger { width: 100%; padding: 28rpx; background: #a44; color: #fff; border-radius: 16rpx; border: none; }
.loading { padding: 80rpx; text-align: center; color: #888; }

View File

@@ -0,0 +1,12 @@
{
"miniprogramRoot": "./",
"appid": "touristappid",
"projectname": "srde-miniprogram",
"setting": {
"es6": true,
"enhance": true,
"postcss": true,
"minified": true
},
"compileType": "miniprogram"
}

View File

@@ -0,0 +1,64 @@
// API 层 - 对接后端 SRDE支持模拟数据
const USE_MOCK = true;
const BASE_URL = 'https://your-api.com'; // 对接时改为真实地址
function getToken(): string {
return wx.getStorageSync('srde_token') || '';
}
export function request<T>(opt: { url: string; method?: string; data?: object }): Promise<T> {
if (USE_MOCK) return Promise.reject(new Error('MOCK模式'));
const token = getToken();
return new Promise((resolve, reject) => {
wx.request({
url: BASE_URL + (opt.url.startsWith('/') ? opt.url : '/' + opt.url),
method: (opt.method as any) || 'GET',
data: opt.data,
header: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
success: (res) => {
if ((res.statusCode || 0) >= 200 && (res.statusCode || 0) < 300) {
resolve(res.data as T);
} else reject(new Error((res.data as any)?.detail || '请求失败'));
},
fail: reject,
});
});
}
export function setMockMode(use: boolean) {
(global as any).__SRDE_USE_MOCK__ = use;
}
export function isMockMode(): boolean {
return USE_MOCK || !!(global as any).__SRDE_USE_MOCK__;
}
export const mockAccount = {
total_capital: 100000,
current_capital: 98500,
current_drawdown: 1.5,
max_drawdown: 5.2,
consecutive_losses: 1,
trading_locked_until: null as string | null,
status: 'tradable' as 'tradable' | 'compressed' | 'locked',
daily_risk_limit: 2000,
single_risk_limit: 1000,
};
export const mockTrades = [
{ id: '1', symbol: 'BTCUSDT', direction: 'long', entry_price: 42000, status: 'open', position_size: 0.5 },
{ id: '2', symbol: 'ETHUSDT', direction: 'short', entry_price: 2200, status: 'closed', pnl: -120 },
{ id: '3', symbol: 'SOLUSDT', direction: 'long', entry_price: 98, status: 'closed', pnl: 85 },
];
export const mockStats = {
win_rate: 62,
avg_odds: 1.8,
profit_factor: 2.1,
max_drawdown: 5.2,
risk_score: 72,
equity_curve: [100000, 101200, 99800, 100500, 102100, 100800, 98500],
};

17
srde/miniprogram/typings/index.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
declare interface IAppOption {
globalData: {
token: string | null;
account: {
total_capital: number;
current_capital: number;
current_drawdown: number;
max_drawdown: number;
consecutive_losses: number;
trading_locked_until: string | null;
status: 'tradable' | 'compressed' | 'locked';
daily_risk_limit?: number;
single_risk_limit?: number;
} | null;
accountLoadedAt: number;
};
}