Refactor: restructure market module with services, stores, and utils

This commit is contained in:
guaiwoluo2020
2026-03-19 09:47:41 +08:00
parent ef15b92342
commit b7ea37ad09
78 changed files with 10594 additions and 7645 deletions
+4 -8
View File
@@ -1,6 +1,6 @@
<template>
<v-app>
<v-app-bar app color="primary" dark>
<v-app-bar color="primary" dark>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-toolbar-title>AITrader</v-toolbar-title>
<v-spacer></v-spacer>
@@ -9,20 +9,16 @@
</v-btn>
</v-app-bar>
<v-navigation-drawer v-model="drawer" app>
<v-navigation-drawer v-model="drawer">
<v-list>
<v-list-item
v-for="item in menuItems"
:key="item.title"
:to="item.path"
:prepend-icon="item.icon"
:title="item.title"
link
>
<v-list-item-icon>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ item.title }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list>
</v-navigation-drawer>
+40
View File
@@ -229,6 +229,46 @@ export const marketAPI = {
async getTradeHistoryStatistics() {
const response = await api.get('/trade_history/statistics')
return response.data
},
// ==================== 策略配置 ====================
// 获取所有策略配置
async getStrategies() {
const response = await api.get('/strategy')
return response.data
},
// 获取品种策略配置
async getStrategy(symbol) {
const response = await api.get(`/strategy/${encodeURIComponent(symbol)}`)
return response.data
},
// 更新品种策略配置
async updateStrategy(symbol, data) {
const response = await api.post(`/strategy/${encodeURIComponent(symbol)}`, data)
return response.data
},
// 删除品种策略配置
async deleteStrategy(symbol) {
const response = await api.delete(`/strategy/${encodeURIComponent(symbol)}`)
return response.data
},
// 获取决策历史
async getDecisions(symbol = null, count = 20) {
const params = { count }
if (symbol) params.symbol = symbol
const response = await api.get('/strategy/decisions', { params })
return response.data
},
// 手动触发策略决策
async triggerStrategyDecision(symbol) {
const response = await api.post(`/strategy/trigger/${encodeURIComponent(symbol)}`)
return response.data
}
}
+202 -493
View File
@@ -18,7 +18,7 @@
>
<div class="d-flex align-center">
<v-icon small class="mr-2">mdi-lightning-bolt</v-icon>
<v-chip v-if="latestFlashNews.speaker" color="primary" x-small class="mr-2">
<v-chip v-if="latestFlashNews.speaker" color="primary" size="x-small" class="mr-2">
{{ latestFlashNews.speaker }}
</v-chip>
<span class="text-body-2">{{ latestFlashNews.content }}</span>
@@ -31,7 +31,7 @@
v-for="(impact, symbol) in latestFlashNews.impact"
:key="symbol"
:color="getImpactColor(impact.direction)"
x-small
size="x-small"
class="mr-1"
>
{{ symbol }}: {{ impact.direction }}
@@ -53,10 +53,10 @@
>
<div class="d-flex align-center">
<v-icon small class="mr-2">mdi-calendar-alert</v-icon>
<v-chip color="error" x-small class="mr-2">
<v-chip color="error" size="x-small" class="mr-2">
重要性 {{ topCalendarEvent.importance }}
</v-chip>
<v-chip :color="getCurrencyColor(topCalendarEvent.currency)" x-small class="mr-2">
<v-chip :color="getCurrencyColor(topCalendarEvent.currency)" size="x-small" class="mr-2">
{{ topCalendarEvent.currency }}
</v-chip>
<span class="text-body-2 font-weight-medium">{{ topCalendarEvent.name }}</span>
@@ -81,7 +81,7 @@
<div v-if="topCalendarEvent.result" class="mt-1">
<v-chip
:color="getEventResultColor(topCalendarEvent.result)"
x-small
size="x-small"
>
{{ getEventResultLabel(topCalendarEvent.result) }}
</v-chip>
@@ -90,206 +90,68 @@
</v-col>
</v-row>
<!-- 转折点提醒通知 -->
<v-row v-if="pivotAlerts.length > 0">
<!-- 交易决策提醒 -->
<v-row v-if="decisionAlerts.length > 0">
<v-col cols="12">
<v-alert
v-for="(alert, index) in pivotAlerts"
v-for="(alert, index) in decisionAlerts"
:key="index"
:type="alert.direction === 'high' ? 'warning' : 'info'"
:type="alert.action === 'buy' ? 'success' : 'warning'"
dismissible
class="mb-2"
@input="removeAlert(index)"
@input="removeDecisionAlert(index)"
>
<div class="d-flex flex-wrap align-center">
<v-icon small class="mr-1">mdi-chart-line</v-icon>
<strong>{{ alert.symbol }}</strong>
<v-chip small :color="alert.direction === 'high' ? 'error' : 'success'" class="ml-2">
接近{{ alert.direction === 'high' ? '高点' : '低点' }}
<v-chip small :color="alert.action === 'buy' ? 'success' : 'error'" class="ml-2">
{{ alert.action === 'buy' ? '买入' : '卖出' }}
</v-chip>
<v-chip small :color="getPivotPeriodColor(alert.period)" class="ml-2">
{{ alert.period }}
<v-chip v-if="alert.confidence" small color="primary" class="ml-2">
置信度: {{ alert.confidence }}%
</v-chip>
</div>
<div class="mt-1">
<span class="text-caption">转折点: <strong>{{ alert.pivot_price }}</strong></span>
<span class="text-caption ml-4">当前价格: <strong>{{ alert.current_price }}</strong></span>
<span class="text-caption ml-4">距离: <strong>{{ (alert.distance_pct * 100).toFixed(2) }}%</strong></span>
</div>
<!-- 显示各周期信息如果有合并数据 -->
<div v-if="alert.periods && alert.periods.length > 0" class="mt-2">
<div class="mt-2">
<span class="text-caption mr-4">入场价: <strong>{{ alert.price?.toFixed(2) }}</strong></span>
<span class="text-caption mr-4">止损: <strong>{{ alert.sl?.toFixed(2) }}</strong></span>
<span class="text-caption mr-4">止盈: <strong>{{ alert.tp?.toFixed(2) }}</strong></span>
<span v-if="alert.risk_reward_ratio" class="text-caption">
盈亏比: <strong>{{ alert.risk_reward_ratio }}</strong>
</span>
</div>
<!-- 信号来源 -->
<div v-if="alert.signals && alert.signals.length > 0" class="mt-2">
<span class="text-caption mr-2">信号来源:</span>
<v-chip
v-for="p in alert.periods"
:key="p.period"
x-small
class="mr-1 mb-1"
:color="getPivotPeriodColor(p.period)"
v-for="(signal, sIdx) in getVisibleSignals(alert)"
:key="sIdx"
size="x-small"
class="mr-1"
:color="getSignalSourceColor(signal.source)"
>
{{ p.period }}: {{ p.price }} ({{ p.distance_pct }}%)
{{ formatSignalLabel(signal) }}
</v-chip>
<v-btn
v-if="alert.signals.length > 3"
size="x-small"
variant="text"
class="ml-1"
@click="toggleSignalExpand(index)"
>
{{ isSignalExpanded(index) ? '收起' : `+${alert.signals.length - 3} 更多` }}
<v-icon end small>{{ isSignalExpanded(index) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
</v-btn>
</div>
<!-- 如果有待确认订单显示订单信息和操作按钮 -->
<div v-if="alert.pending_order" class="mt-3 pa-2 grey lighten-4 rounded">
<!-- 待确认订单操作 -->
<div v-if="alert.pending_order && !alert.pending_order.confirmed" class="mt-3 pa-2 grey lighten-4 rounded">
<div class="text-subtitle-2 font-weight-bold mb-2">
<v-icon small color="primary" class="mr-1">mdi-file-document-edit</v-icon>
自动生成交易指令
待确认订单
</div>
<!-- AI与技术信号比较 -->
<div v-if="alert.pending_order.ai_driven" class="mb-2">
<div class="d-flex align-center mb-1">
<span class="text-caption mr-2">技术信号:</span>
<v-chip :color="alert.pending_order.tech_action === '买入' ? 'success' : 'error'" x-small>
{{ alert.pending_order.tech_action }}
</v-chip>
<span class="text-caption mx-2">AI建议:</span>
<v-chip :color="alert.pending_order.ai_direction === '买入' ? 'success' : 'error'" x-small>
{{ alert.pending_order.ai_direction }}
</v-chip>
</div>
</div>
<!-- AI冲突警告 -->
<v-alert v-if="alert.pending_order.ai_conflict" type="warning" dense class="mb-2">
<div class="d-flex align-center">
<v-icon small class="mr-1">mdi-alert-circle</v-icon>
<strong>AI与技术信号冲突以AI方向为准</strong>
</div>
<div v-if="alert.pending_order.ai_reason" class="text-caption mt-1">
AI理由: {{ alert.pending_order.ai_reason }}
</div>
</v-alert>
<!-- AI一致提示 -->
<v-alert v-if="alert.pending_order.ai_aligned" type="success" dense class="mb-2">
<div class="d-flex align-center">
<v-icon small class="mr-1">mdi-check-circle</v-icon>
<strong>AI与技术信号一致</strong>
</div>
<div v-if="alert.pending_order.ai_reason" class="text-caption mt-1">
AI理由: {{ alert.pending_order.ai_reason }}
</div>
</v-alert>
<div class="mb-2">
<v-chip :color="alert.pending_order.action === 'b' ? 'success' : 'error'" x-small class="mr-2">
{{ alert.pending_order.action === 'b' ? '买入' : '卖出' }}
</v-chip>
<span class="mr-3">价格: {{ alert.pending_order.price?.toFixed(2) }}</span>
</div>
<!-- 可编辑字段 -->
<v-row dense class="mb-2">
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.mount"
label="手数"
type="number"
step="0.01"
min="0.01"
dense
hide-details
outlined
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.sl"
label="止损"
type="number"
step="0.01"
dense
hide-details
outlined
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.tp"
label="止盈"
type="number"
step="0.01"
dense
hide-details
outlined
:hint="alert.pending_order.ai_aligned ? 'AI建议' : ''"
persistent-hint
></v-text-field>
</v-col>
<v-col cols="3" class="d-flex align-center">
<v-btn
color="success"
small
class="mr-1"
:loading="confirmingOrderId === alert.pending_order.order_id"
@click="confirmAlertOrder(alert.pending_order, index)"
>
<v-icon left small>mdi-check</v-icon>
确认
</v-btn>
<v-btn
color="error"
small
outlined
:loading="rejectingOrderId === alert.pending_order.order_id"
@click="rejectAlertOrder(alert.pending_order.order_id, index)"
>
放弃
</v-btn>
</v-col>
</v-row>
<div class="text-caption grey--text mb-1">
{{ alert.pending_order.reason }}
</div>
<div class="text-caption grey--text">
<v-icon small>mdi-clock-outline</v-icon>
3分钟内未操作将自动移除
</div>
</div>
</v-alert>
</v-col>
</v-row>
<!-- AI入场价提醒通知 -->
<v-row v-if="aiEntryAlerts.length > 0">
<v-col cols="12">
<v-alert
v-for="(alert, index) in aiEntryAlerts"
:key="'ai-'+index"
type="info"
dismissible
class="mb-2"
@input="removeAiEntryAlert(index)"
>
<div class="d-flex flex-wrap align-center">
<v-icon small color="white" class="mr-1">mdi-robot</v-icon>
<strong>{{ alert.symbol }} {{ alert.period }}</strong>
<span class="ml-2">AI建议入场</span>
<v-chip small class="ml-2" :color="alert.direction === 'buy' ? 'success' : 'error'">
{{ alert.direction === 'buy' ? '买入' : '卖出' }}
</v-chip>
<span class="ml-2">入场价: {{ alert.entry_price }}</span>
<span class="ml-2">当前价: {{ alert.current_price }}</span>
<span class="ml-2">差距: {{ alert.price_diff_pct }}%</span>
</div>
<!-- 如果有待确认订单显示订单信息和操作按钮 -->
<div v-if="alert.pending_order" class="mt-3 pa-2 grey lighten-4 rounded">
<div class="text-subtitle-2 font-weight-bold mb-2">
<v-icon small color="primary" class="mr-1">mdi-file-document-edit</v-icon>
AI交易建议
</div>
<div class="mb-2">
<v-chip :color="alert.pending_order.action === 'b' ? 'success' : 'error'" x-small class="mr-2">
{{ alert.pending_order.action === 'b' ? '买入' : '卖出' }}
</v-chip>
<span class="mr-3">入场价: {{ alert.pending_order.price?.toFixed(2) }}</span>
<span class="mr-3">止损: {{ alert.pending_order.sl }}</span>
<span class="mr-3">止盈: {{ alert.pending_order.tp }}</span>
</div>
<!-- 可编辑字段 -->
<v-row dense class="mb-2">
<v-col cols="3">
<v-text-field
@@ -331,9 +193,9 @@
small
class="mr-1"
:loading="confirmingOrderId === alert.pending_order.order_id"
@click="confirmAiEntryOrder(alert.pending_order, index)"
@click="confirmDecisionOrder(alert.pending_order, index)"
>
<v-icon left small>mdi-check</v-icon>
<v-icon start small>mdi-check</v-icon>
确认
</v-btn>
<v-btn
@@ -341,167 +203,28 @@
small
outlined
:loading="rejectingOrderId === alert.pending_order.order_id"
@click="rejectAiEntryOrder(alert.pending_order.order_id, index)"
@click="rejectDecisionOrder(alert.pending_order.order_id, index)"
>
放弃
</v-btn>
</v-col>
</v-row>
<div class="text-caption grey--text mb-1">
<v-icon small class="mr-1">mdi-lightbulb</v-icon>
{{ alert.reason }}
</div>
<div class="text-caption grey--text">
<v-icon small>mdi-clock-outline</v-icon>
3分钟内未操作将自动移除
{{ formatTime(alert.timestamp) }}
</div>
</div>
</v-alert>
</v-col>
</v-row>
<!-- 关键点位订单提醒 -->
<v-row v-if="keyLevelAlerts.length > 0">
<v-col cols="12">
<v-alert
v-for="(alert, index) in keyLevelAlerts"
:key="'key-'+index"
type="success"
dismissible
class="mb-2"
@input="removeKeyLevelAlert(index)"
>
<div class="d-flex flex-wrap align-center">
<v-icon small color="white" class="mr-1">mdi-chart-line</v-icon>
<strong>{{ alert.symbol }}</strong>
<v-chip small :color="alert.action === 'b' ? 'success' : 'error'" class="ml-2">
{{ alert.action_text }}
<!-- 已确认状态 -->
<div v-if="alert.pending_order?.confirmed" class="mt-2">
<v-chip small color="success">
<v-icon start small>mdi-check-circle</v-icon>
已确认等待执行
</v-chip>
<span class="ml-2">关键点位策略</span>
</div>
<div class="mt-1">
<span class="text-caption">关键位: <strong>{{ alert.key_level }}</strong></span>
<span class="text-caption ml-4">入场价: <strong>{{ alert.price?.toFixed(2) }}</strong></span>
<span class="text-caption ml-4">距离: <strong>{{ alert.distance_pct }}%</strong></span>
</div>
<!-- 如果有待确认订单显示订单信息和操作按钮 -->
<div v-if="alert.pending_order" class="mt-3 pa-2 grey lighten-4 rounded">
<div class="text-subtitle-2 font-weight-bold mb-2">
<v-icon small color="primary" class="mr-1">mdi-file-document-edit</v-icon>
关键点位交易建议
</div>
<div class="mb-2">
<v-chip :color="alert.pending_order.action === 'b' ? 'success' : 'error'" x-small class="mr-2">
{{ alert.pending_order.action === 'b' ? '买入' : '卖出' }}
</v-chip>
<span class="mr-3">入场价: {{ alert.pending_order.price?.toFixed(2) }}</span>
<span class="mr-3">止损: {{ alert.pending_order.sl }}</span>
<span class="mr-3">止盈: {{ alert.pending_order.tp }}</span>
</div>
<!-- AI方向对比 -->
<div class="mb-2 pa-2 white rounded">
<div class="text-caption font-weight-bold mb-1">
<v-icon small class="mr-1">mdi-robot</v-icon>
AI方向对比
</div>
<div v-if="alert.pending_order.ai_directions && Object.keys(alert.pending_order.ai_directions).length > 0" class="d-flex flex-wrap align-center">
<span class="text-caption mr-2">关键点位: <strong :class="alert.pending_order.action === 'b' ? 'success--text' : 'error--text'">{{ alert.pending_order.key_level_direction_text }}</strong></span>
<template v-for="(dirInfo, period) in alert.pending_order.ai_directions" :key="period">
<v-chip
:color="dirInfo.direction === 'buy' ? 'success' : 'error'"
x-small
outlined
class="mr-1"
>
{{ period }}: {{ dirInfo.text }}
</v-chip>
</template>
</div>
<div v-else class="text-caption grey--text">
关键点位: <strong :class="alert.pending_order.action === 'b' ? 'success--text' : 'error--text'">{{ alert.pending_order.key_level_direction_text }}</strong>
<span class="ml-2">AI暂无分析数据</span>
</div>
<div v-if="alert.pending_order.recommendation" class="mt-1">
<v-chip
:color="alert.pending_order.recommendation_color || 'grey'"
x-small
dark
>
<v-icon x-small left>mdi-lightbulb</v-icon>
{{ alert.pending_order.recommendation }}
</v-chip>
</div>
</div>
<!-- 可编辑字段 -->
<v-row dense class="mb-2">
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.mount"
label="手数"
type="number"
step="0.01"
min="0.01"
dense
hide-details
outlined
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.sl"
label="止损"
type="number"
step="0.01"
dense
hide-details
outlined
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="alert.pending_order.tp"
label="止盈"
type="number"
step="0.01"
dense
hide-details
outlined
></v-text-field>
</v-col>
<v-col cols="3" class="d-flex align-center">
<v-btn
color="success"
small
class="mr-1"
:loading="confirmingOrderId === alert.pending_order.order_id"
@click="confirmKeyLevelOrder(alert.pending_order, index)"
>
<v-icon left small>mdi-check</v-icon>
确认
</v-btn>
<v-btn
color="error"
small
outlined
:loading="rejectingOrderId === alert.pending_order.order_id"
@click="rejectKeyLevelOrder(alert.pending_order.order_id, index)"
>
放弃
</v-btn>
</v-col>
</v-row>
<div class="text-caption grey--text mb-1">
<v-icon small class="mr-1">mdi-lightbulb</v-icon>
{{ alert.reason }}
</div>
<div class="text-caption grey--text">
<v-icon small>mdi-clock-outline</v-icon>
3分钟内未操作将自动移除
</div>
</div>
</v-alert>
</v-col>
@@ -515,7 +238,7 @@
small
class="mr-2"
>
<v-icon left small>mdi-lan-connect</v-icon>
<v-icon start small>mdi-lan-connect</v-icon>
{{ wsConnected ? 'WebSocket 已连接' : 'WebSocket 断开' }}
</v-chip>
</v-col>
@@ -562,15 +285,15 @@
<div v-else>
<v-expansion-panels>
<v-expansion-panel v-for="(data, symbol) in llmAnalysis" :key="symbol">
<v-expansion-panel-header>
<v-expansion-panel-title>
<div class="d-flex align-center">
<strong class="mr-3">{{ symbol }}</strong>
<v-chip
v-if="data.analysis && data.analysis.overall_trend"
:color="getTrendChipColor(data.analysis.overall_trend.direction)"
v-if="data.overall_trend"
:color="getTrendChipColor(data.overall_trend.direction)"
small
>
{{ data.analysis.overall_trend.direction }}
{{ data.overall_trend.direction }}
</v-chip>
<!-- 休市状态 -->
<v-chip
@@ -579,7 +302,7 @@
small
class="ml-2"
>
<v-icon left x-small>mdi-pause-circle</v-icon>
<v-icon start size="x-small">mdi-pause-circle</v-icon>
休市中
</v-chip>
<!-- 数据未更新 -->
@@ -589,12 +312,12 @@
small
class="ml-2"
>
<v-icon left x-small>mdi-alert</v-icon>
<v-icon start size="x-small">mdi-alert</v-icon>
数据未更新
</v-chip>
</div>
</v-expansion-panel-header>
<v-expansion-panel-content>
</v-expansion-panel-title>
<v-expansion-panel-text>
<!-- 休市提示 -->
<v-alert
v-if="data.market_status === 'closed'"
@@ -626,9 +349,9 @@
</v-alert>
<!-- 各周期趋势休市时可能没有分析结果-->
<div v-if="data.analysis && data.analysis.trend_analysis" class="mb-4">
<div v-if="data.trend_analysis" class="mb-4">
<div class="text-subtitle-2 mb-2">各周期趋势</div>
<v-simple-table dense>
<v-table density="compact">
<template v-slot:default>
<thead>
<tr>
@@ -642,10 +365,10 @@
</tr>
</thead>
<tbody>
<tr v-for="(trend, period) in data.analysis.trend_analysis" :key="period">
<tr v-for="(trend, period) in data.trend_analysis" :key="period">
<td><strong>{{ period }}</strong></td>
<td>
<v-chip :color="getTrendChipColor(trend.trend)" x-small>
<v-chip :color="getTrendChipColor(trend.trend)" size="x-small">
{{ trend.trend }}
</v-chip>
</td>
@@ -659,7 +382,7 @@
<v-chip
v-if="getTechTrend(symbol, period)"
:color="getTrendColor(getTechTrend(symbol, period).trend)"
x-small
size="x-small"
>
{{ getTrendLabel(getTechTrend(symbol, period).trend) }}
</v-chip>
@@ -674,7 +397,7 @@
<td>
<v-chip
:color="getConclusionColor(symbol, period, trend)"
x-small
size="x-small"
>
{{ getConclusion(symbol, period, trend) }}
</v-chip>
@@ -682,32 +405,32 @@
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</div>
<!-- 关键价位 -->
<div v-if="data.analysis && data.analysis.key_levels" class="mb-4">
<div v-if="data.key_levels" class="mb-4">
<div class="text-subtitle-2 mb-2">关键价位</div>
<v-row>
<v-col cols="6">
<div class="text-caption grey--text">压力位</div>
<div v-for="(level, i) in data.analysis.key_levels.resistance" :key="'r'+i">
<v-chip color="error" x-small class="mr-1">{{ level }}</v-chip>
<div v-for="(level, i) in data.key_levels.resistance" :key="'r'+i">
<v-chip color="error" size="x-small" class="mr-1">{{ level }}</v-chip>
</div>
</v-col>
<v-col cols="6">
<div class="text-caption grey--text">支撑位</div>
<div v-for="(level, i) in data.analysis.key_levels.support" :key="'s'+i">
<v-chip color="success" x-small class="mr-1">{{ level }}</v-chip>
<div v-for="(level, i) in data.key_levels.support" :key="'s'+i">
<v-chip color="success" size="x-small" class="mr-1">{{ level }}</v-chip>
</div>
</v-col>
</v-row>
</div>
<!-- 交易建议 -->
<div v-if="data.analysis && data.analysis.trade_suggestions && data.analysis.trade_suggestions.length > 0">
<div v-if="data.trade_suggestions && data.trade_suggestions.length > 0">
<div class="text-subtitle-2 mb-2">交易建议</div>
<v-simple-table dense>
<v-table density="compact">
<template v-slot:default>
<thead>
<tr>
@@ -720,10 +443,10 @@
</tr>
</thead>
<tbody>
<tr v-for="(suggestion, i) in data.analysis.trade_suggestions" :key="i">
<tr v-for="(suggestion, i) in data.trade_suggestions" :key="i">
<td>{{ suggestion.period }}</td>
<td>
<v-chip :color="suggestion.direction === 'buy' ? 'success' : 'error'" x-small>
<v-chip :color="suggestion.direction === 'buy' ? 'success' : 'error'" size="x-small">
{{ suggestion.direction === 'buy' ? '买入' : '卖出' }}
</v-chip>
</td>
@@ -734,13 +457,13 @@
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
</div>
<div class="text-caption grey--text mt-2">
分析时间: {{ data.analyzed_at }}
</div>
</v-expansion-panel-content>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
</div>
@@ -767,9 +490,8 @@ export default {
const allPivots = ref([])
const marketStatus = ref({})
const thresholds = ref({})
const pivotAlerts = ref([])
const aiEntryAlerts = ref([])
const keyLevelAlerts = ref([]) // 关键点位订单提醒
const decisionAlerts = ref([]) // 统一的决策提醒列表
const expandedSignals = ref(new Set()) // 展开信号的状态
const loading = ref(false)
const showError = ref(false)
const errorMessage = ref('')
@@ -838,28 +560,54 @@ export default {
ws.value = marketAPI.createWebSocket(
// onMessage
(data) => {
if (data.type === 'pivot_alert') {
// 添加新的提醒到列表顶部
pivotAlerts.value.unshift(data)
// 只保留最近10条
if (pivotAlerts.value.length > 10) {
pivotAlerts.value.pop()
if (data.type === 'trading_decision') {
// 策略层产生的交易决策
console.log('收到交易决策:', data)
const decision = data.data
// 转换为统一的 alert 格式
const alert = {
type: 'trading_decision',
decision_id: decision.decision_id,
symbol: decision.symbol,
action: decision.action,
price: decision.entry_price,
sl: decision.sl,
tp: decision.tp,
volume: decision.volume,
reason: decision.decision_reason,
confidence: decision.confidence_score,
signals: decision.signals || [],
signal_summary: decision.signal_summary || {},
risk_reward_ratio: decision.risk_reward_ratio,
timestamp: decision.timestamp || new Date().toISOString(),
pending_order: {
order_id: decision.decision_id,
action: decision.action === 'buy' ? 'b' : 's',
price: decision.entry_price,
sl: decision.sl,
tp: decision.tp,
mount: decision.volume,
reason: decision.decision_reason
}
}
} else if (data.type === 'ai_entry_alert') {
// AI入场价提醒
console.log('收到AI入场价提醒:', data)
aiEntryAlerts.value.unshift(data)
// 只保留最近10条
if (aiEntryAlerts.value.length > 10) {
aiEntryAlerts.value.pop()
decisionAlerts.value.unshift(alert)
if (decisionAlerts.value.length > 10) {
decisionAlerts.value.pop()
}
} else if (data.type === 'key_level_alert') {
// 关键点位订单提醒
console.log('收到关键点位订单提醒:', data)
keyLevelAlerts.value.unshift(data)
// 只保留最近10条
if (keyLevelAlerts.value.length > 10) {
keyLevelAlerts.value.pop()
} else if (data.type === 'pending_order') {
// 订单状态更新(确认后)
console.log('收到订单更新:', data)
const order = data.data
// 更新对应的 alert
const alertIndex = decisionAlerts.value.findIndex(
a => a.pending_order?.order_id === order.order_id
)
if (alertIndex >= 0) {
decisionAlerts.value[alertIndex].pending_order = {
...decisionAlerts.value[alertIndex].pending_order,
...order,
confirmed: true
}
}
} else if (data.type === 'connected') {
wsConnected.value = true
@@ -1240,24 +988,21 @@ export default {
}
}
// 从提醒列表中移除
const removeAlert = (index) => {
pivotAlerts.value.splice(index, 1)
// 决策提醒操作
const removeDecisionAlert = (index) => {
decisionAlerts.value.splice(index, 1)
}
// 确认提醒中的订单(可修改参数)
const confirmAlertOrder = async (order, alertIndex) => {
const confirmDecisionOrder = async (order, alertIndex) => {
confirmingOrderId.value = order.order_id
try {
// 发送修改后的订单数据
const data = await marketAPI.confirmOrderWithUpdate(order.order_id, {
mount: order.mount,
sl: order.sl,
tp: order.tp
})
if (data.status === 'ok') {
// 移除该提醒
pivotAlerts.value.splice(alertIndex, 1)
decisionAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '确认订单失败'
@@ -1271,14 +1016,12 @@ export default {
}
}
// 放弃提醒中的订单
const rejectAlertOrder = async (orderId, alertIndex) => {
const rejectDecisionOrder = async (orderId, alertIndex) => {
rejectingOrderId.value = orderId
try {
const data = await marketAPI.rejectOrder(orderId)
if (data.status === 'ok') {
// 移除该提醒
pivotAlerts.value.splice(alertIndex, 1)
decisionAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '放弃订单失败'
@@ -1292,98 +1035,67 @@ export default {
}
}
// AI入场价提醒操作
const removeAiEntryAlert = (index) => {
aiEntryAlerts.value.splice(index, 1)
}
const confirmAiEntryOrder = async (order, alertIndex) => {
confirmingOrderId.value = order.order_id
try {
const data = await marketAPI.confirmOrderWithUpdate(order.order_id, {
mount: order.mount,
sl: order.sl,
tp: order.tp
})
if (data.status === 'ok') {
aiEntryAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '确认订单失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `确认订单失败: ${err.message}`
showError.value = true
} finally {
confirmingOrderId.value = null
// 辅助方法
const getSignalSourceColor = (source) => {
const colors = {
'pivot': 'primary',
'key_level': 'success',
'ai_entry': 'info'
}
return colors[source] || 'grey'
}
const rejectAiEntryOrder = async (orderId, alertIndex) => {
rejectingOrderId.value = orderId
try {
const data = await marketAPI.rejectOrder(orderId)
if (data.status === 'ok') {
aiEntryAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '放弃订单失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `放弃订单失败: ${err.message}`
showError.value = true
} finally {
rejectingOrderId.value = null
// 格式化信号标签(显示周期)
const formatSignalLabel = (signal) => {
const source = signal.source || ''
const period = signal.source_period || signal.ai_analysis_period || ''
const confidence = signal.confidence || 0
// 显示信号源名称
const sourceNames = {
'pivot': 'Pivot',
'key_level': 'KeyLevel',
'ai_entry': 'AI'
}
}
const sourceName = sourceNames[source] || source
// 关键点位订单提醒操作
const removeKeyLevelAlert = (index) => {
keyLevelAlerts.value.splice(index, 1)
}
const confirmKeyLevelOrder = async (order, alertIndex) => {
confirmingOrderId.value = order.order_id
try {
const data = await marketAPI.confirmOrderWithUpdate(order.order_id, {
mount: order.mount,
sl: order.sl,
tp: order.tp
})
if (data.status === 'ok') {
keyLevelAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '确认订单失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `确认订单失败: ${err.message}`
showError.value = true
} finally {
confirmingOrderId.value = null
// 如果有周期,显示周期
if (period) {
return `${sourceName}[${period}] (${confidence}%)`
}
return `${sourceName} (${confidence}%)`
}
const rejectKeyLevelOrder = async (orderId, alertIndex) => {
rejectingOrderId.value = orderId
try {
const data = await marketAPI.rejectOrder(orderId)
if (data.status === 'ok') {
keyLevelAlerts.value.splice(alertIndex, 1)
await loadPendingOrders()
} else {
errorMessage.value = data.message || '放弃订单失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `放弃订单失败: ${err.message}`
showError.value = true
} finally {
rejectingOrderId.value = null
// 获取可见的信号列表
const getVisibleSignals = (alert) => {
if (!alert.signals) return []
const alertIndex = decisionAlerts.value.indexOf(alert)
if (expandedSignals.value.has(alertIndex)) {
return alert.signals
}
return alert.signals.slice(0, 3)
}
// 切换信号展开状态
const toggleSignalExpand = (index) => {
if (expandedSignals.value.has(index)) {
expandedSignals.value.delete(index)
} else {
expandedSignals.value.add(index)
}
// 触发响应式更新
expandedSignals.value = new Set(expandedSignals.value)
}
// 检查信号是否展开
const isSignalExpanded = (index) => {
return expandedSignals.value.has(index)
}
const formatTime = (timestamp) => {
if (!timestamp) return ''
const date = new Date(timestamp)
return date.toLocaleString('zh-CN')
}
const getTrendColor = (trend) => {
@@ -1439,7 +1151,7 @@ export default {
lowPivots,
marketStatus,
thresholds,
pivotAlerts,
decisionAlerts,
loading,
showError,
errorMessage,
@@ -1455,22 +1167,19 @@ export default {
rejectOrder,
getTrendColor,
getTrendLabel,
// 提醒订单操作
// 决策订单操作
confirmingOrderId,
rejectingOrderId,
removeAlert,
confirmAlertOrder,
rejectAlertOrder,
// AI入场价提醒
aiEntryAlerts,
removeAiEntryAlert,
confirmAiEntryOrder,
rejectAiEntryOrder,
// 关键点位订单提醒
keyLevelAlerts,
removeKeyLevelAlert,
confirmKeyLevelOrder,
rejectKeyLevelOrder,
removeDecisionAlert,
confirmDecisionOrder,
rejectDecisionOrder,
getSignalSourceColor,
formatTime,
// 信号显示
formatSignalLabel,
getVisibleSignals,
toggleSignalExpand,
isSignalExpanded,
// 大模型分析
llmStatus,
llmAnalysis,
+14 -14
View File
@@ -62,11 +62,11 @@
<!-- 持仓列表 -->
<v-card-text>
<v-btn color="primary" small class="mb-3" @click="loadPositions" :loading="loading">
<v-icon left small>mdi-refresh</v-icon>
<v-icon start small>mdi-refresh</v-icon>
刷新
</v-btn>
<v-simple-table v-if="positions.length > 0">
<v-table v-if="positions.length > 0">
<template v-slot:default>
<thead>
<tr>
@@ -87,7 +87,7 @@
<td>{{ pos.ticket }}</td>
<td><strong>{{ pos.symbol }}</strong></td>
<td>
<v-chip x-small :color="pos.type === 'BUY' ? 'success' : 'error'">
<v-chip size="x-small" :color="pos.type === 'BUY' ? 'success' : 'error'">
{{ pos.type === 'BUY' ? '买入' : '卖出' }}
</v-chip>
</td>
@@ -100,14 +100,14 @@
<td>{{ pos.distance_tp || '-' }}</td>
<td>{{ formatTime(pos.updated_at) }}</td>
<td>
<v-btn x-small color="error" outlined @click="closePosition(pos)">
<v-btn size="x-small" color="error" outlined @click="closePosition(pos)">
平仓
</v-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
<div v-else class="text-center grey--text py-8">
<v-icon large>mdi-folder-open-outline</v-icon>
<div class="mt-2">暂无持仓</div>
@@ -119,7 +119,7 @@
<v-tab-item>
<v-card-text>
<v-btn color="primary" small class="mb-3" @click="loadTradeHistory" :loading="historyLoading">
<v-icon left small>mdi-refresh</v-icon>
<v-icon start small>mdi-refresh</v-icon>
刷新
</v-btn>
@@ -242,7 +242,7 @@
<!-- 成交列表 -->
<div class="text-subtitle-1 font-weight-bold mb-2">成交记录</div>
<v-simple-table v-if="tradeDeals.length > 0" fixed-header height="400">
<v-table v-if="tradeDeals.length > 0" fixed-header height="400">
<template v-slot:default>
<thead>
<tr>
@@ -263,12 +263,12 @@
<td>{{ deal.ticket }}</td>
<td><strong>{{ deal.symbol }}</strong></td>
<td>
<v-chip x-small :color="deal.type === 0 ? 'success' : 'error'">
<v-chip size="x-small" :color="deal.type === 0 ? 'success' : 'error'">
{{ deal.type_text }}
</v-chip>
</td>
<td>
<v-chip x-small outlined :color="deal.entry === 1 ? 'warning' : 'info'">
<v-chip size="x-small" outlined :color="deal.entry === 1 ? 'warning' : 'info'">
{{ deal.entry_text }}
</v-chip>
</td>
@@ -280,16 +280,16 @@
<td>{{ deal.commission.toFixed(2) }}</td>
<td>{{ deal.time }}</td>
<td>
<v-chip v-if="deal.order_source === '自动'" x-small color="primary">
<v-chip v-if="deal.order_source === '自动'" size="x-small" color="primary">
{{ deal.comment }}
</v-chip>
<v-chip v-else-if="deal.order_source === '止损触发'" x-small color="error" outlined>
<v-chip v-else-if="deal.order_source === '止损触发'" size="x-small" color="error" outlined>
{{ deal.comment }}
</v-chip>
<v-chip v-else-if="deal.order_source === '止盈触发'" x-small color="success" outlined>
<v-chip v-else-if="deal.order_source === '止盈触发'" size="x-small" color="success" outlined>
{{ deal.comment }}
</v-chip>
<v-chip v-else-if="deal.order_source === '强制平仓'" x-small color="error" dark>
<v-chip v-else-if="deal.order_source === '强制平仓'" size="x-small" color="error" dark>
{{ deal.comment }}
</v-chip>
<span v-else class="grey--text">{{ deal.order_source }}</span>
@@ -297,7 +297,7 @@
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
<div v-else class="text-center grey--text py-8">
<v-icon large>mdi-history</v-icon>
<div class="mt-2">暂无历史交易数据</div>
+596 -15
View File
@@ -27,7 +27,7 @@
<!-- 品种配置表格 -->
<div class="text-subtitle-2 mt-2 mb-2">品种配置</div>
<v-simple-table dense>
<v-table density="compact">
<template v-slot:default>
<thead>
<tr>
@@ -88,13 +88,13 @@
></v-text-field>
</td>
<td>
<v-btn x-small color="primary" @click="saveTradeConfig">保存</v-btn>
<v-btn x-small color="error" outlined class="ml-1" @click="removeSymbolConfig(symbol)">删除</v-btn>
<v-btn size="x-small" color="primary" @click="saveTradeConfig">保存</v-btn>
<v-btn size="x-small" color="error" outlined class="ml-1" @click="removeSymbolConfig(symbol)">删除</v-btn>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
<!-- 添加新品种配置 -->
<v-row class="mt-3" align="center">
@@ -153,7 +153,7 @@
</v-col>
<v-col cols="2">
<v-btn color="primary" small @click="addSymbolConfig">
<v-icon left small>mdi-plus</v-icon>
<v-icon start small>mdi-plus</v-icon>
添加
</v-btn>
</v-col>
@@ -161,8 +161,369 @@
<div class="text-caption grey--text mt-3">
<v-icon small>mdi-information</v-icon>
支撑压力策略: M1周期接近转折点时自动生成交易指令止损偏移为固定点数<br/>
关键点位策略: 价格接近关键点位时生成反向订单例如下降趋势接近5000时生成买单阈值表示触发距离默认0.0008
品种基础配置手数止损偏移关键点位等
</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
<!-- 策略配置 -->
<v-row class="mt-4">
<v-col cols="12">
<v-card>
<v-card-title>
<v-icon class="mr-2">mdi-strategy</v-icon>
策略配置
<v-btn icon small class="ml-2" @click="loadStrategies" :loading="strategiesLoading">
<v-icon small>mdi-refresh</v-icon>
</v-btn>
</v-card-title>
<v-card-text>
<!-- 策略列表 -->
<v-expansion-panels v-if="strategies.length > 0">
<v-expansion-panel v-for="strategy in strategies" :key="strategy.symbol">
<v-expansion-panel-title>
<div class="d-flex align-center">
<strong class="mr-3">{{ strategy.symbol }}</strong>
<v-chip :color="strategy.enabled ? 'success' : 'grey'" size="x-small">
{{ strategy.enabled ? '启用' : '禁用' }}
</v-chip>
<span class="text-caption grey--text ml-3">{{ strategy.strategy_name }}</span>
</div>
</v-expansion-panel-title>
<v-expansion-panel-text>
<!-- 基本信息 -->
<v-row class="mb-3">
<v-col cols="12" md="3">
<v-switch
v-model="strategy.enabled"
label="启用策略"
dense
hide-details
@change="updateStrategy(strategy)"
></v-switch>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model="strategy.strategy_name"
label="策略名称"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-text-field
v-model.number="strategy.min_confidence"
label="最低置信度(%)"
type="number"
min="0"
max="100"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="12" md="3">
<v-select
v-model="strategy.consistency_requirement"
:items="consistencyOptions"
label="一致性要求"
dense
hide-details
@change="updateStrategy(strategy)"
></v-select>
</v-col>
</v-row>
<!-- 信号权重 -->
<div class="text-subtitle-2 mb-2">信号源配置</div>
<v-row class="mb-3">
<v-col cols="12">
<v-table density="compact">
<template v-slot:default>
<thead>
<tr>
<th>信号源</th>
<th>启用</th>
<th>M1</th>
<th>M5</th>
<th>M15</th>
<th>H1</th>
<th>H4</th>
</tr>
</thead>
<tbody>
<!-- Pivot 信号 -->
<tr>
<td>
<v-chip size="x-small" color="primary">Pivot</v-chip>
<span class="ml-2 text-caption">转折点信号</span>
</td>
<td>
<v-checkbox
v-model="getSignalConfig(strategy, 'pivot').enabled"
density="compact"
hide-details
@change="onSignalConfigChange(strategy, 'pivot')"
></v-checkbox>
</td>
<td v-for="period in ['M1', 'M5', 'M15', 'H1', 'H4']" :key="period">
<div class="d-flex align-center">
<v-checkbox
v-model="getPeriodConfig(strategy, 'pivot', period).enabled"
density="compact"
hide-details
:disabled="!getSignalConfig(strategy, 'pivot').enabled"
@change="onSignalConfigChange(strategy, 'pivot')"
></v-checkbox>
<v-text-field
v-model.number="getPeriodConfig(strategy, 'pivot', period).weight"
type="number"
min="0"
max="100"
density="compact"
hide-details
style="width: 50px"
:disabled="!getSignalConfig(strategy, 'pivot').enabled || !getPeriodConfig(strategy, 'pivot', period).enabled"
@change="onSignalConfigChange(strategy, 'pivot')"
></v-text-field>
</div>
</td>
</tr>
<!-- KeyLevel 信号 -->
<tr>
<td>
<v-chip size="x-small" color="success">KeyLevel</v-chip>
<span class="ml-2 text-caption">关键点位信号</span>
</td>
<td>
<v-checkbox
v-model="getSignalConfig(strategy, 'key_level').enabled"
density="compact"
hide-details
@change="onSignalConfigChange(strategy, 'key_level')"
></v-checkbox>
</td>
<td colspan="5">
<v-text-field
v-model.number="getSignalConfig(strategy, 'key_level').weight"
label="权重"
type="number"
min="0"
max="100"
density="compact"
hide-details
style="width: 80px"
:disabled="!getSignalConfig(strategy, 'key_level').enabled"
@change="onSignalConfigChange(strategy, 'key_level')"
></v-text-field>
<span class="text-caption grey--text ml-2">不区分周期</span>
</td>
</tr>
<!-- AI Entry 信号 -->
<tr>
<td>
<v-chip size="x-small" color="info">AI Entry</v-chip>
<span class="ml-2 text-caption">AI入场信号</span>
</td>
<td>
<v-checkbox
v-model="getSignalConfig(strategy, 'ai_entry').enabled"
density="compact"
hide-details
@change="onSignalConfigChange(strategy, 'ai_entry')"
></v-checkbox>
</td>
<td v-for="period in ['M1', 'M5', 'M15', 'H1', 'H4']" :key="period">
<div class="d-flex align-center">
<v-checkbox
v-model="getPeriodConfig(strategy, 'ai_entry', period).enabled"
density="compact"
hide-details
:disabled="!getSignalConfig(strategy, 'ai_entry').enabled"
@change="onSignalConfigChange(strategy, 'ai_entry')"
></v-checkbox>
<v-text-field
v-model.number="getPeriodConfig(strategy, 'ai_entry', period).weight"
type="number"
min="0"
max="100"
density="compact"
hide-details
style="width: 50px"
:disabled="!getSignalConfig(strategy, 'ai_entry').enabled || !getPeriodConfig(strategy, 'ai_entry', period).enabled"
@change="onSignalConfigChange(strategy, 'ai_entry')"
></v-text-field>
</div>
</td>
</tr>
</tbody>
</template>
</v-table>
</v-col>
</v-row>
<!-- 兼容旧版信号权重隐藏但保留数据 -->
<div class="text-caption grey--text mb-2">
<v-icon small>mdi-information</v-icon>
勾选启用周期设置权重值KeyLevel信号不区分周期只需设置权重
</div>
<!-- 仓位管理 -->
<div class="text-subtitle-2 mb-2">仓位管理</div>
<v-row class="mb-3">
<v-col cols="3">
<v-text-field
v-model.number="strategy.fixed_volume"
label="固定手数"
type="number"
step="0.01"
min="0.01"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="strategy.max_positions"
label="最大持仓数"
type="number"
min="1"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="strategy.max_same_direction"
label="同向最大持仓"
type="number"
min="1"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="strategy.risk_percent"
label="风险百分比(%)"
type="number"
min="0.1"
step="0.1"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
</v-row>
<!-- 止损止盈 -->
<div class="text-subtitle-2 mb-2">止损止盈规则</div>
<v-row class="mb-3">
<v-col cols="3">
<v-select
v-model="strategy.sl_mode"
:items="slModeOptions"
label="止损模式"
dense
hide-details
@change="updateStrategy(strategy)"
></v-select>
</v-col>
<v-col cols="3">
<v-select
v-model="strategy.tp_mode"
:items="tpModeOptions"
label="止盈模式"
dense
hide-details
@change="updateStrategy(strategy)"
></v-select>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="strategy.min_risk_reward"
label="最小盈亏比"
type="number"
min="0.5"
step="0.5"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
<v-col cols="3">
<v-text-field
v-model.number="strategy.tp_risk_reward"
label="止盈盈亏比"
type="number"
min="1"
step="0.5"
dense
hide-details
@change="updateStrategy(strategy)"
></v-text-field>
</v-col>
</v-row>
<!-- 操作按钮 -->
<v-row>
<v-col cols="12">
<v-btn color="primary" small @click="updateStrategy(strategy)" :loading="strategySaving === strategy.symbol">
<v-icon start small>mdi-content-save</v-icon>
保存
</v-btn>
<v-btn color="error" small outlined class="ml-2" @click="deleteStrategy(strategy.symbol)">
<v-icon start small>mdi-delete</v-icon>
删除策略
</v-btn>
</v-col>
</v-row>
</v-expansion-panel-text>
</v-expansion-panel>
</v-expansion-panels>
<div v-else class="text-center grey--text py-4">
<v-icon large>mdi-strategy</v-icon>
<div class="mt-2">暂无策略配置添加品种后会自动创建默认策略</div>
</div>
<!-- 添加新策略 -->
<v-row class="mt-4" align="center">
<v-col cols="4">
<v-select
v-model="newStrategySymbol"
:items="symbolsWithoutStrategy"
label="选择品种添加策略"
dense
hide-details
></v-select>
</v-col>
<v-col cols="4">
<v-text-field
v-model="newStrategyName"
label="策略名称(可选)"
dense
hide-details
placeholder="默认:Strategy_{品种}"
></v-text-field>
</v-col>
<v-col cols="4">
<v-btn color="primary" small @click="addStrategy" :loading="strategySaving === 'new'">
<v-icon start small>mdi-plus</v-icon>
添加策略
</v-btn>
</v-col>
</v-row>
<div class="text-caption grey--text mt-3">
<v-icon small>mdi-information</v-icon>
策略配置信号权重过滤规则仓位管理止损止盈等每个品种绑定一个策略
</div>
</v-card-text>
</v-card>
@@ -214,7 +575,7 @@
<v-row class="mt-2">
<v-col cols="12">
<v-btn color="primary" @click="saveLLMConfig" :loading="llmSaving">
<v-icon left>mdi-content-save</v-icon>
<v-icon start>mdi-content-save</v-icon>
保存配置
</v-btn>
<v-chip
@@ -249,7 +610,7 @@
</v-btn>
</v-card-title>
<v-card-text>
<v-simple-table dense v-if="symbolStatus.length > 0">
<v-table dense v-if="symbolStatus.length > 0">
<template v-slot:default>
<thead>
<tr>
@@ -265,7 +626,7 @@
<tr v-for="item in symbolStatus" :key="item.symbol">
<td><strong>{{ item.symbol }}</strong></td>
<td>
<v-chip x-small :color="item.has_data ? 'success' : 'error'">
<v-chip size="x-small" :color="item.has_data ? 'success' : 'error'">
{{ item.has_data ? '有数据' : '无数据' }}
</v-chip>
</td>
@@ -276,14 +637,14 @@
<span v-else>-</span>
</td>
<td>
<v-chip x-small :color="getMarketStatusColor(item.market_status)">
<v-chip size="x-small" :color="getMarketStatusColor(item.market_status)">
{{ getMarketStatusText(item.market_status) }}
</v-chip>
</td>
</tr>
</tbody>
</template>
</v-simple-table>
</v-table>
<div v-else class="text-center grey--text py-4">
<v-icon large>mdi-database-off</v-icon>
<div class="mt-2">暂无已配置的品种</div>
@@ -299,12 +660,12 @@
</v-row>
<!-- 错误提示 -->
<v-snackbar v-model="showError" color="error" timeout="5000">
<v-snackbar v-model="showError" color="error" timeout="5000" location="top">
{{ errorMessage }}
</v-snackbar>
<!-- 成功提示 -->
<v-snackbar v-model="showSuccess" color="success" timeout="3000">
<v-snackbar v-model="showSuccess" color="success" timeout="3000" location="top">
{{ successMessage }}
</v-snackbar>
</v-container>
@@ -534,11 +895,213 @@ export default {
}
}
// ==================== 策略配置 ====================
// 策略数据
const strategies = ref([])
const strategiesLoading = ref(false)
const strategySaving = ref(null)
const newStrategySymbol = ref('')
const newStrategyName = ref('')
// 策略选项
const consistencyOptions = [
{ title: '任一信号即可', value: 'any' },
{ title: '多数信号一致', value: 'majority' },
{ title: '所有信号一致', value: 'all' }
]
const slModeOptions = [
{ title: '使用信号建议', value: 'signal' },
{ title: '固定点数', value: 'fixed_points' }
]
const tpModeOptions = [
{ title: '使用信号建议', value: 'signal' },
{ title: '固定点数', value: 'fixed_points' },
{ title: '风险回报比', value: 'risk_reward' }
]
// 没有策略的品种
const symbolsWithoutStrategy = computed(() => {
const strategySymbols = strategies.value.map(s => s.symbol)
return symbols.value.filter(s => !strategySymbols.includes(s))
})
// 加载策略列表
const loadStrategies = async () => {
strategiesLoading.value = true
try {
const data = await marketAPI.getStrategies()
if (data.status === 'ok') {
strategies.value = data.strategies || []
}
} catch (err) {
console.error('加载策略配置失败:', err)
} finally {
strategiesLoading.value = false
}
}
// 更新策略
const updateStrategy = async (strategy) => {
strategySaving.value = strategy.symbol
try {
// 确保signal_config存在
if (!strategy.signal_config) {
strategy.signal_config = {
pivot: {
enabled: true,
periods: {
M1: { enabled: true, weight: 15 },
M5: { enabled: true, weight: 20 },
M15: { enabled: false, weight: 25 },
H1: { enabled: false, weight: 20 },
H4: { enabled: false, weight: 20 }
}
},
key_level: { enabled: true, weight: 40 },
ai_entry: {
enabled: true,
periods: {
M1: { enabled: false, weight: 15 },
M5: { enabled: true, weight: 20 },
M15: { enabled: true, weight: 30 },
H1: { enabled: true, weight: 25 },
H4: { enabled: false, weight: 20 }
}
}
}
}
const data = await marketAPI.updateStrategy(strategy.symbol, {
enabled: strategy.enabled,
strategy_name: strategy.strategy_name,
min_confidence: strategy.min_confidence,
consistency_requirement: strategy.consistency_requirement,
signal_config: strategy.signal_config,
signal_weights: strategy.signal_weights,
fixed_volume: strategy.fixed_volume,
max_positions: strategy.max_positions,
max_same_direction: strategy.max_same_direction,
risk_percent: strategy.risk_percent,
sl_mode: strategy.sl_mode,
tp_mode: strategy.tp_mode,
min_risk_reward: strategy.min_risk_reward,
tp_risk_reward: strategy.tp_risk_reward
})
if (data.status === 'ok') {
successMessage.value = `${strategy.symbol} 策略配置已保存`
showSuccess.value = true
// 更新本地策略数据
if (data.strategy) {
Object.assign(strategy, data.strategy)
}
} else {
errorMessage.value = data.message || '保存失败'
showError.value = true
}
} catch (err) {
console.error('保存策略失败:', err)
errorMessage.value = `保存策略失败: ${err.message}`
showError.value = true
} finally {
strategySaving.value = null
}
}
// 获取信号源配置
const getSignalConfig = (strategy, source) => {
if (!strategy.signal_config) {
strategy.signal_config = {
pivot: { enabled: true, periods: {} },
key_level: { enabled: true, weight: 40 },
ai_entry: { enabled: true, periods: {} }
}
}
if (!strategy.signal_config[source]) {
if (source === 'key_level') {
strategy.signal_config[source] = { enabled: true, weight: 40 }
} else {
strategy.signal_config[source] = { enabled: true, periods: {} }
}
}
return strategy.signal_config[source]
}
// 获取周期配置
const getPeriodConfig = (strategy, source, period) => {
const config = getSignalConfig(strategy, source)
if (source === 'key_level') {
return { enabled: true, weight: config.weight || 40 }
}
if (!config.periods) {
config.periods = {}
}
if (!config.periods[period]) {
config.periods[period] = { enabled: false, weight: 20 }
}
return config.periods[period]
}
// 信号配置变更处理
const onSignalConfigChange = (strategy, source) => {
// 触发保存
updateStrategy(strategy)
}
// 删除策略
const deleteStrategy = async (symbol) => {
if (!confirm(`确定要删除 ${symbol} 的策略配置吗?`)) return
try {
const data = await marketAPI.deleteStrategy(symbol)
if (data.status === 'ok') {
successMessage.value = '策略已删除'
showSuccess.value = true
await loadStrategies()
} else {
errorMessage.value = data.message || '删除失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `删除策略失败: ${err.message}`
showError.value = true
}
}
// 添加策略
const addStrategy = async () => {
if (!newStrategySymbol.value) return
strategySaving.value = 'new'
try {
const data = await marketAPI.updateStrategy(newStrategySymbol.value, {
enabled: true,
strategy_name: newStrategyName.value || `Strategy_${newStrategySymbol.value}`
})
if (data.status === 'ok') {
successMessage.value = '策略已添加'
showSuccess.value = true
newStrategySymbol.value = ''
newStrategyName.value = ''
await loadStrategies()
} else {
errorMessage.value = data.message || '添加失败'
showError.value = true
}
} catch (err) {
errorMessage.value = `添加策略失败: ${err.message}`
showError.value = true
} finally {
strategySaving.value = null
}
}
onMounted(() => {
loadSymbols()
loadTradeConfig()
loadLLMConfig()
loadSymbolStatus()
loadStrategies()
})
return {
@@ -567,7 +1130,25 @@ export default {
symbolStatusLoading,
loadSymbolStatus,
getMarketStatusColor,
getMarketStatusText
getMarketStatusText,
// 策略配置
strategies,
strategiesLoading,
strategySaving,
newStrategySymbol,
newStrategyName,
consistencyOptions,
slModeOptions,
tpModeOptions,
symbolsWithoutStrategy,
loadStrategies,
updateStrategy,
deleteStrategy,
addStrategy,
// 信号配置
getSignalConfig,
getPeriodConfig,
onSignalConfigChange
}
}
}
+14 -15
View File
@@ -70,22 +70,21 @@ def create_app():
app.include_router(create_trader_routes(server))
app.include_router(create_system_routes(server))
app.include_router(create_market_routes(
server.market_store,
server.pivot_detector,
server.pivot_monitor,
server.trend_analyzer,
server.pending_orders,
server.llm_analyzer
server.kline_store,
server.kline_service,
server.pivot_service,
server.tech_service,
server.pending_order_service,
trading_server=server
))
app.include_router(create_position_routes())
app.include_router(create_position_routes(trading_server=server))
app.include_router(create_news_routes())
# 启动时设置事件循环
@app.on_event("startup")
async def startup_event():
loop = asyncio.get_running_loop()
server.llm_analyzer.set_event_loop(loop)
server.pivot_monitor.set_event_loop(loop)
server.set_event_loop(loop)
# 设置系统日志的事件循环
from market.system_log import get_system_log
@@ -95,14 +94,14 @@ def create_app():
# 记录系统启动日志
system_log.add_log("system_startup", message="服务已启动")
# 启动新闻监控后台任务
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
news_monitor.set_event_loop(loop)
asyncio.create_task(news_monitor.run())
# 启动市场事件监控后台任务
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
monitor.set_event_loop(loop)
asyncio.create_task(monitor.run())
print("[Startup] 事件循环已设置")
print("[Startup] 新闻监控已启动")
print("[Startup] 市场事件监控已启动")
return app
+35 -5
View File
@@ -4,9 +4,39 @@
行情分析模块
"""
from .store import MarketStore
from .pivot_detector import PivotDetector
from .monitor import PivotMonitor
from .trend_analyzer import TrendAnalyzer
# 数据模型
from .models import (
KlineData, PivotPoint,
LLMConfig, LLMAnalysisResult,
TechTrendState, TechTrendChange, TechResonanceResult, TechTradeSuggestion
)
__all__ = ['MarketStore', 'PivotDetector', 'PivotMonitor', 'TrendAnalyzer']
# 存储层
from .store import KlineStore, PivotStore, LLMStore, TechStore
# 服务层
from .services import KlineService, PivotService, LLMService, TechService
# 其他模块
from .llm_analyzer import LLMAnalyzer
from .trade_config import TradeConfig
# 兼容旧代码的别名
MarketStore = KlineStore
PivotDetector = PivotService
TrendAnalyzer = TechService
__all__ = [
# 数据模型
'KlineData', 'PivotPoint',
'LLMConfig', 'LLMAnalysisResult',
'TechTrendState', 'TechTrendChange', 'TechResonanceResult', 'TechTradeSuggestion',
# 存储层
'KlineStore', 'PivotStore', 'LLMStore', 'TechStore',
# 服务层
'KlineService', 'PivotService', 'LLMService', 'TechService',
# 其他模块
'LLMAnalyzer', 'TradeConfig',
# 兼容别名
'MarketStore', 'PivotDetector', 'TrendAnalyzer'
]
+67 -666
View File
@@ -1,108 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
大模型行情趋势分析模块
使用大语言模型分析K线数据,生成趋势判断和交易建议
LLM 分析器
负责定时调度和 WebSocket 广播
"""
import os
import json
import threading
import asyncio
import requests
import json
from datetime import datetime
from typing import List, Dict, Optional, Set
from collections import defaultdict
# 加载 .env 文件
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from typing import Dict, Set, Optional
from .services import LLMService
from .system_log import get_system_log
class LLMAnalyzer:
"""大模型行情分析器"""
"""LLM 分析器(调度 + WebSocket 广播)"""
# 分析间隔(秒)
ANALYZE_INTERVAL = 300 # 5分钟
# 趋势类型
TREND_TYPES = [
"单边上涨",
"单边下跌",
"区间震荡",
"震荡上升",
"震荡下跌",
"震荡收窄",
"震荡扩大"
]
def __init__(self, llm_service: LLMService):
self.llm_service = llm_service
# 各周期K线数量限制
KLINE_LIMITS = {
'H4': 20, # 4小时,发送最近20根
'H1': 24, # 1小时,发送最近24根(一天)
'M15': 32, # 15分钟,发送最近32根(8小时)
'M5': 48, # 5分钟,发送最近48根(4小时)
'M1': 60 # 1分钟,发送最近60根(1小时)
}
# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), "data", "llm_config.json")
def __init__(self, market_store):
"""
初始化大模型分析器
Args:
market_store: K线存储对象
"""
self.market_store = market_store
# 存储分析结果: {SYMBOL: analysis_result}
self._analysis_results = {}
self._last_analysis_time = None
self._lock = threading.RLock()
# WebSocket连接管理
# WebSocket 连接管理
self._ws_clients: Set = set()
self._ws_lock = threading.Lock()
# 主事件循环引用(在FastAPI启动时设置)
# 主事件循环引用
self._main_loop = None
# 已提醒的AI入场价记录(避免重复提醒)
# 结构: {(symbol, period, direction, entry_price): datetime}
self._alerted_entries: Dict[tuple, datetime] = {}
self._entry_alert_lock = threading.Lock()
# AI入场价提醒冷却时间(秒)
self.entry_alert_cooldown = 300 # 5分钟
# 配置(先从文件加载,再从环境变量补充)
self._api_key = ""
self._api_base = "https://api.openai.com/v1"
self._model = "gpt-4o-mini"
self._enabled = False
# 从文件加载配置
self._load_from_file()
# 环境变量覆盖(如果文件中没有配置)
if not self._api_key and os.environ.get("LLM_API_KEY"):
self._api_key = os.environ.get("LLM_API_KEY", "")
if not self._api_base or self._api_base == "https://api.openai.com/v1":
self._api_base = os.environ.get("LLM_API_BASE", "https://api.openai.com/v1")
if not self._model or self._model == "gpt-4o-mini":
self._model = os.environ.get("LLM_MODEL", "gpt-4o-mini")
self._enabled = bool(self._api_key)
# 启动定时分析线程
if self._enabled:
if self.llm_service.is_enabled():
self._start_analyze_thread()
print("[LLMAnalyzer] 大模型分析器已初始化(已启用)")
else:
@@ -113,61 +43,11 @@ class LLMAnalyzer:
self._main_loop = loop
print(f"[LLMAnalyzer] 已设置主事件循环")
def _load_from_file(self):
"""从文件加载配置"""
try:
if os.path.exists(self.CONFIG_FILE):
with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
self._api_key = data.get("api_key", "")
self._api_base = data.get("api_base", "https://api.openai.com/v1")
self._model = data.get("model", "gpt-4o-mini")
print(f"[LLMAnalyzer] 已从文件加载配置: {self.CONFIG_FILE}")
except Exception as e:
print(f"[LLMAnalyzer] 加载配置文件失败: {e}")
def _save_to_file(self):
"""保存配置到文件"""
try:
# 确保目录存在
config_dir = os.path.dirname(self.CONFIG_FILE)
os.makedirs(config_dir, exist_ok=True)
data = {
"api_key": self._api_key,
"api_base": self._api_base,
"model": self._model
}
with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"[LLMAnalyzer] 配置已保存到文件")
except Exception as e:
print(f"[LLMAnalyzer] 保存配置文件失败: {e}")
def get_config(self) -> Dict:
"""获取当前配置(API Key会脱敏显示)"""
# 脱敏API Key:只显示前4位和后4位
masked_key = ""
if self._api_key:
if len(self._api_key) > 8:
masked_key = self._api_key[:4] + "****" + self._api_key[-4:]
else:
masked_key = "****"
return {
"api_key": masked_key,
"api_key_set": bool(self._api_key),
"api_base": self._api_base,
"model": self._model,
"enabled": self._enabled
}
def _start_analyze_thread(self):
"""启动定时分析线程"""
def analyze_loop():
# 等待事件循环设置完成
import time
time.sleep(5) # 等待5秒让服务完全启动
time.sleep(5) # 等待服务启动
print("[LLMAnalyzer] 分析线程启动,开始第一次分析...")
while True:
@@ -177,7 +57,7 @@ class LLMAnalyzer:
print(f"[LLMAnalyzer] 分析异常: {e}")
import traceback
traceback.print_exc()
# 等待5分钟
threading.Event().wait(self.ANALYZE_INTERVAL)
thread = threading.Thread(target=analyze_loop, daemon=True)
@@ -185,465 +65,82 @@ class LLMAnalyzer:
print("[LLMAnalyzer] 分析线程已创建")
def _run_analysis(self):
"""执行分析 - 合并所有品种到一次请求(流式输出)"""
symbols = self.market_store.get_symbols()
print(f"[LLMAnalyzer] _run_analysis 调用,获取到 {len(symbols) if symbols else 0} 个品种")
"""执行分析"""
def on_status(status: str, message: str):
self._broadcast_analysis_status(status, message)
if not symbols:
print("[LLMAnalyzer] 没有品种数据,跳过分析")
return
def on_complete(response):
if response:
self._broadcast_analysis_update()
print(f"[LLMAnalyzer] 开始分析 {len(symbols)} 个品种: {symbols}")
self.llm_service.run_analysis(on_status=on_status, on_complete=on_complete)
# 广播分析开始
self._broadcast_analysis_status("analyzing", f"正在检查 {len(symbols)} 个品种的数据更新状态...")
# 检查每个品种的M1 K线更新状态(3分钟内有效)
STALE_THRESHOLD = 180 # 3分钟
active_symbols = [] # 有数据更新的品种
stale_symbols = [] # 数据过期的品种
for symbol in symbols:
m1_status = self.market_store.check_m1_updated_within(symbol, STALE_THRESHOLD)
market_status = m1_status.get("market_status", "closed")
if market_status == "active":
active_symbols.append(symbol)
print(f"[LLMAnalyzer] {symbol} M1数据有效,距今 {m1_status['seconds_ago']}")
elif market_status == "stale":
stale_symbols.append(symbol)
print(f"[LLMAnalyzer] {symbol} M1数据过期,距今 {m1_status['seconds_ago']} 秒,跳过分析")
else: # closed
stale_symbols.append(symbol)
print(f"[LLMAnalyzer] {symbol} 休市中,无新数据,跳过分析")
# 标记休市状态
with self._lock:
if symbol in self._analysis_results:
self._analysis_results[symbol]["market_status"] = "closed"
else:
# 没有历史分析结果,创建一个标记休市的记录
self._analysis_results[symbol] = {
"symbol": symbol,
"analysis": None,
"analyzed_at": None,
"market_status": "closed",
"data_stale": True
}
# 更新过期品种的状态标记(不包括休市品种,它们已经在上面处理了)
with self._lock:
for symbol in stale_symbols:
m1_status = self.market_store.check_m1_updated_within(symbol, STALE_THRESHOLD)
if m1_status.get("market_status") == "stale" and symbol in self._analysis_results:
# 保留上次分析结果,但标记为过期
self._analysis_results[symbol]["data_stale"] = True
self._analysis_results[symbol]["market_status"] = "stale"
self._analysis_results[symbol]["stale_seconds"] = m1_status.get("seconds_ago")
# 如果没有活跃品种,广播状态并返回
if not active_symbols:
print("[LLMAnalyzer] 所有品种数据均过期,跳过大模型调用")
self._broadcast_analysis_status("stale", "所有品种行情数据均未更新,使用上次分析结果")
self._last_analysis_time = datetime.now().isoformat()
self._broadcast_analysis_update()
return
# 广播实际分析的品种
if stale_symbols:
self._broadcast_analysis_status("analyzing",
f"分析 {len(active_symbols)} 个品种,{len(stale_symbols)} 个品种数据未更新")
else:
self._broadcast_analysis_status("analyzing",
f"正在分析 {len(active_symbols)} 个品种...")
# 收集活跃品种的K线数据
all_klines_data = {}
for symbol in active_symbols:
klines_data = {}
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
limit = self.KLINE_LIMITS.get(period, 30)
klines = self.market_store.get_klines(symbol, period, limit)
if klines:
klines_data[period] = klines
print(f"[LLMAnalyzer] {symbol} {period} 获取到 {len(klines)} 条K线")
if klines_data:
all_klines_data[symbol] = klines_data
print(f"[LLMAnalyzer] 共收集 {len(all_klines_data)} 个品种的K线数据: {list(all_klines_data.keys())}")
if not all_klines_data:
print("[LLMAnalyzer] 无K线数据可分析")
self._broadcast_analysis_status("error", "无K线数据可分析")
return
# 构建合并的提示词
prompt = self._build_combined_prompt(all_klines_data)
# 记录分析开始
system_log = get_system_log()
system_log.add_log(
"llm_analysis_start",
{"symbols": active_symbols, "symbol_count": len(active_symbols)},
message=f"开始分析 {len(active_symbols)} 个品种"
)
# 调用大模型(流式)
response = self._call_llm_stream(prompt)
print(f"[LLMAnalyzer] 大模型返回结果: {type(response)}, 内容长度: {len(response) if response else 0}")
if response:
print(f"[LLMAnalyzer] 返回的品种: {list(response.keys())}")
# 解析结果,按品种存储
with self._lock:
for symbol, analysis in response.items():
if isinstance(analysis, dict):
self._analysis_results[symbol] = {
"symbol": symbol,
"analysis": analysis,
"analyzed_at": datetime.now().isoformat(),
"data_stale": False # 标记数据是最新的
}
print(f"[LLMAnalyzer] 已存储 {symbol} 的分析结果")
# 记录分析完成
system_log.add_log(
"llm_analysis_complete",
{"symbols": list(response.keys()), "symbol_count": len(response)},
message=f"分析完成,{len(response)} 个品种"
)
else:
print(f"[LLMAnalyzer] 大模型返回为空,分析失败")
# 记录分析错误
system_log.add_log(
"llm_analysis_error",
{"reason": "大模型返回为空"},
message="分析失败"
)
self._last_analysis_time = datetime.now().isoformat()
print(f"[LLMAnalyzer] 分析完成,时间: {self._last_analysis_time}")
# 广播分析完成通知
self._broadcast_analysis_update()
def _build_combined_prompt(self, all_klines_data: Dict) -> str:
"""构建合并的分析提示词"""
prompt = """你是一位专业的金融分析师。请分析以下多个交易品种的K线数据,给出每个品种的趋势判断和交易建议。
## 分析要求
对于每个品种,请分析:
1. 各周期(H4、H1、M15、M5、M1)的趋势判断,包含趋势类型、置信度(0-100)和判断理由
2. 整体趋势方向、强度(0-100)和总结
3. 关键支撑位和压力位(请根据K线数据自行判断,各列出3个)
4. 交易建议:必须包含M1、M5、M15三个周期的具体交易建议
趋势类型可选值:单边上涨、单边下跌、区间震荡、震荡上升、震荡下跌、震荡收窄、震荡扩大
请按以下JSON格式输出(必须是有效的JSON格式,包含所有品种):
```json
{
"品种1": {
"trend_analysis": {
"H4": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"H1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M15": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M5": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"}
},
"overall_trend": {
"direction": "整体趋势方向",
"strength": 强度,
"summary": "整体趋势总结"
},
"key_levels": {
"resistance": [压力位1, 压力位2, 压力位3],
"support": [支撑位1, 支撑位2, 支撑位3]
},
"trade_suggestions": [
{
"period": "M15",
"direction": "buy或sell",
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"reason": "交易理由"
},
{
"period": "M5",
"direction": "buy或sell",
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"reason": "交易理由"
},
{
"period": "M1",
"direction": "buy或sell",
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"reason": "交易理由"
}
]
},
"品种2": { ... }
}
```
## K线数据
"""
# 添加各品种的K线数据
for symbol, klines_data in all_klines_data.items():
prompt += f"\n### {symbol}\n"
for period, klines in klines_data.items():
prompt += f"\n#### {period} 周期({len(klines)}根K线)\n"
prompt += "| 时间 | 开盘 | 最高 | 最低 | 收盘 |\n"
prompt += "|------|------|------|------|------|\n"
for k in klines:
prompt += f"| {k['timestamp']} | {k['open']:.2f} | {k['high']:.2f} | {k['low']:.2f} | {k['close']:.2f} |\n"
prompt += """
请确保输出是纯JSON格式,不要有其他文字说明。每个品种的分析结果都要完整,trade_suggestions必须包含M1、M5、M15三个周期的建议。
"""
return prompt
def _call_llm(self, prompt: str) -> Optional[Dict]:
"""调用大模型API(非流式,保留兼容)"""
if not self._api_key:
return None
try:
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json"
}
data = {
"model": self._model,
"messages": [
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 4000
}
response = requests.post(
f"{self._api_base}/chat/completions",
headers=headers,
json=data,
timeout=120
)
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
# 提取JSON部分
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
return json.loads(content.strip())
else:
print(f"[LLMAnalyzer] API调用失败: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"[LLMAnalyzer] 调用异常: {e}")
import traceback
traceback.print_exc()
return None
def _call_llm_stream(self, prompt: str) -> Optional[Dict]:
"""调用大模型API(流式输出)"""
if not self._api_key:
return None
try:
headers = {
"Authorization": f"Bearer {self._api_key}",
"Content-Type": "application/json"
}
data = {
"model": self._model,
"messages": [
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 4000,
"stream": True # 启用流式输出
}
response = requests.post(
f"{self._api_base}/chat/completions",
headers=headers,
json=data,
timeout=120,
stream=True # 流式响应
)
if response.status_code != 200:
print(f"[LLMAnalyzer] API调用失败: {response.status_code} - {response.text}")
self._broadcast_analysis_status("error", f"API调用失败: {response.status_code}")
return None
# 收集完整响应
full_content = ""
chunk_count = 0
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if line.startswith('data: '):
data_str = line[6:] # 去掉 'data: '
if data_str == '[DONE]':
break
try:
chunk_data = json.loads(data_str)
if 'choices' in chunk_data and len(chunk_data['choices']) > 0:
delta = chunk_data['choices'][0].get('delta', {})
content_piece = delta.get('content', '')
if content_piece:
full_content += content_piece
chunk_count += 1
# 每50个chunk广播一次进度
if chunk_count % 50 == 0:
self._broadcast_analysis_status(
"streaming",
f"正在接收分析结果... ({len(full_content)} 字符)"
)
except json.JSONDecodeError:
continue
print(f"[LLMAnalyzer] 流式接收完成,共 {chunk_count} 个chunk{len(full_content)} 字符")
# 提取JSON部分
if "```json" in full_content:
full_content = full_content.split("```json")[1].split("```")[0]
elif "```" in full_content:
full_content = full_content.split("```")[1].split("```")[0]
result = json.loads(full_content.strip())
return result
except json.JSONDecodeError as e:
print(f"[LLMAnalyzer] JSON解析失败: {e}")
self._broadcast_analysis_status("error", "JSON解析失败")
return None
except Exception as e:
print(f"[LLMAnalyzer] 流式调用异常: {e}")
import traceback
traceback.print_exc()
self._broadcast_analysis_status("error", f"调用异常: {str(e)}")
return None
def get_analysis(self, symbol: str = None) -> Dict:
"""
获取分析结果
Args:
symbol: 品种名称,不指定则返回所有
Returns:
分析结果
"""
with self._lock:
if symbol:
return self._analysis_results.get(symbol)
return dict(self._analysis_results)
def get_status(self) -> Dict:
"""获取分析器状态"""
with self._lock:
return {
"enabled": self._enabled,
"model": self._model,
"api_base": self._api_base,
"last_analysis_time": self._last_analysis_time,
"symbols_analyzed": list(self._analysis_results.keys()),
"interval_seconds": self.ANALYZE_INTERVAL
}
# ==================== 手动触发 ====================
def trigger_analysis(self) -> Dict:
"""手动触发分析"""
if not self._enabled:
if not self.llm_service.is_enabled():
return {"status": "error", "message": "大模型分析未启用"}
try:
print("[LLMAnalyzer] 手动触发分析...")
self._run_analysis()
return {"status": "ok", "message": "分析完成", "analyzed_at": self._last_analysis_time}
result = self.llm_service.run_analysis(
on_status=lambda s, m: self._broadcast_analysis_status(s, m),
on_complete=lambda r: self._broadcast_analysis_update()
)
return {
"status": "ok",
"message": "分析完成",
"analyzed_at": self.llm_service.llm_store.get_last_analysis_time()
}
except Exception as e:
print(f"[LLMAnalyzer] 手动触发分析失败: {e}")
import traceback
traceback.print_exc()
return {"status": "error", "message": str(e)}
# ==================== 配置 ====================
def configure(self, api_key: str = None, api_base: str = None, model: str = None) -> Dict:
"""
配置大模型参数
Args:
api_key: API密钥
api_base: API基础URL
model: 模型名称
Returns:
配置结果
"""
if api_key:
self._api_key = api_key
os.environ["LLM_API_KEY"] = api_key
if api_base:
self._api_base = api_base
os.environ["LLM_API_BASE"] = api_base
if model:
self._model = model
os.environ["LLM_MODEL"] = model
# 保存到文件
self._save_to_file()
# 检查是否可以启用
was_enabled = self._enabled
self._enabled = bool(self._api_key)
"""配置 LLM 参数"""
result = self.llm_service.configure(api_key, api_base, model)
# 如果从禁用变为启用,启动分析线程
if self._enabled and not was_enabled:
if result.get("enabled") and not hasattr(self, '_thread_started'):
self._start_analyze_thread()
self._thread_started = True
return {
"status": "ok",
"enabled": self._enabled,
"model": self._model,
"api_base": self._api_base
}
return result
# ==================== WebSocket管理 ====================
def get_config(self) -> Dict:
"""获取配置"""
return self.llm_service.get_config()
# ==================== 查询 ====================
def get_analysis(self, symbol: str = None) -> Dict:
"""获取分析结果"""
return self.llm_service.get_analysis(symbol)
def get_status(self) -> Dict:
"""获取状态"""
status = self.llm_service.get_status()
status["interval_seconds"] = self.ANALYZE_INTERVAL
return status
def check_entry_price_nearby(self, symbol: str, current_price: float,
threshold: float = 0.0001) -> list:
"""检查入场价是否接近"""
return self.llm_service.check_entry_price_nearby(symbol, current_price, threshold)
# ==================== WebSocket 管理 ====================
def add_ws_client(self, client):
"""添加WebSocket客户端"""
"""添加 WebSocket 客户端"""
with self._ws_lock:
self._ws_clients.add(client)
print(f"[LLMAnalyzer] WebSocket客户端已连接, 当前连接数: {len(self._ws_clients)}")
def remove_ws_client(self, client):
"""移除WebSocket客户端"""
"""移除 WebSocket 客户端"""
with self._ws_lock:
self._ws_clients.discard(client)
print(f"[LLMAnalyzer] WebSocket客户端已断开, 当前连接数: {len(self._ws_clients)}")
@@ -652,10 +149,9 @@ class LLMAnalyzer:
"""广播分析更新通知"""
message = json.dumps({
"type": "llm_analysis_update",
"timestamp": self._last_analysis_time,
"symbols": list(self._analysis_results.keys())
"timestamp": self.llm_service.llm_store.get_last_analysis_time(),
"symbols": self.llm_service.llm_store.get_analyzed_symbols()
})
self._broadcast_message(message)
def _broadcast_analysis_status(self, status: str, message: str):
@@ -666,18 +162,16 @@ class LLMAnalyzer:
"message": message,
"timestamp": datetime.now().isoformat()
})
self._broadcast_message(msg)
def _broadcast_message(self, message: str):
"""广播消息到所有WebSocket客户端"""
"""广播消息到所有 WebSocket 客户端"""
with self._ws_lock:
clients = list(self._ws_clients)
if not clients:
return
# 使用保存的主事件循环
if self._main_loop and self._main_loop.is_running():
for client in clients:
try:
@@ -688,7 +182,7 @@ class LLMAnalyzer:
except Exception as e:
print(f"[LLMAnalyzer] 广播消息失败: {e}")
else:
print(f"[LLMAnalyzer] 事件循环未就绪,跳过广播{len(clients)}个客户端)")
print(f"[LLMAnalyzer] 事件循环未就绪,跳过广播")
async def _send_to_client(self, client, message: str):
"""发送消息到客户端"""
@@ -697,97 +191,4 @@ class LLMAnalyzer:
except Exception as e:
print(f"[LLMAnalyzer] 发送消息到客户端失败: {e}")
with self._ws_lock:
self._ws_clients.discard(client)
def check_entry_price_nearby(self, symbol: str, current_price: float, threshold: float = 0.0001) -> List[Dict]:
"""
检查当前价格是否接近AI建议的入场价
Args:
symbol: 交易品种
current_price: 当前价格
threshold: 价格接近阈值,默认万分之一(0.0001)
Returns:
匹配的交易建议列表
"""
matched_suggestions = []
current_time = datetime.now()
with self._lock:
analysis_data = self._analysis_results.get(symbol)
if not analysis_data or 'analysis' not in analysis_data:
return matched_suggestions
trade_suggestions = analysis_data['analysis'].get('trade_suggestions', [])
if not trade_suggestions:
return matched_suggestions
for suggestion in trade_suggestions:
entry_price = suggestion.get('entry_price')
period = suggestion.get('period')
direction = suggestion.get('direction')
if not entry_price or entry_price <= 0:
continue
# 计算价格差距百分比
if entry_price > 0:
price_diff_pct = abs(current_price - entry_price) / entry_price
# 如果在阈值范围内
if price_diff_pct <= threshold:
# 检查冷却
alert_key = (symbol, period, direction, entry_price)
with self._entry_alert_lock:
should_alert = True
if alert_key in self._alerted_entries:
last_alert_time = self._alerted_entries[alert_key]
elapsed = (current_time - last_alert_time).total_seconds()
if elapsed < self.entry_alert_cooldown:
should_alert = False
print(f"[LLMAnalyzer] 跳过AI入场价提醒(冷却中): {symbol} {period} "
f"入场价 {entry_price:.2f}, 剩余 {self.entry_alert_cooldown - elapsed:.0f}")
if should_alert:
# 记录提醒时间
self._alerted_entries[alert_key] = current_time
matched = {
"symbol": symbol,
"period": period,
"direction": direction,
"entry_price": entry_price,
"current_price": current_price,
"price_diff_pct": round(price_diff_pct * 100, 4),
"stop_loss": suggestion.get('stop_loss'),
"take_profit": suggestion.get('take_profit'),
"reason": suggestion.get('reason'),
"analyzed_at": analysis_data.get('analyzed_at'),
"match_type": "ai_entry_nearby"
}
matched_suggestions.append(matched)
print(f"[LLMAnalyzer] 价格接近AI入场价: {symbol} {period} "
f"入场价 {entry_price:.2f}, 当前价 {current_price:.2f}, 差距 {price_diff_pct*100:.4f}%")
# 清理过期的提醒记录
self._cleanup_entry_alerts()
return matched_suggestions
def _cleanup_entry_alerts(self):
"""清理过期的AI入场价提醒记录"""
current_time = datetime.now()
with self._entry_alert_lock:
keys_to_remove = []
for key, alert_time in self._alerted_entries.items():
elapsed = (current_time - alert_time).total_seconds()
if elapsed > self.entry_alert_cooldown * 2:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._alerted_entries[key]
self._ws_clients.discard(client)
+263
View File
@@ -0,0 +1,263 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
市场事件监控模块
负责定时调度和 WebSocket 推送
包含财经日历事件和快讯事件
"""
import asyncio
from datetime import datetime
from typing import Dict, List
from .models import CalendarEvent, FlashNews
from .store import CalendarStore, FlashNewsStore
from .services import CalendarService, FlashNewsService
from .utils.ws_manager import WebSocketManager
from .news_crawler import get_jin10_crawler
from .system_log import get_system_log
class MarketEventMonitor:
"""市场事件监控器(调度 + WebSocket 推送)"""
def __init__(self,
calendar_store: CalendarStore = None,
flash_news_store: FlashNewsStore = None):
# 存储
self.calendar_store = calendar_store or CalendarStore()
self.flash_news_store = flash_news_store or FlashNewsStore()
# 服务
self.calendar_service = CalendarService(self.calendar_store)
self.flash_news_service = FlashNewsService(self.flash_news_store)
# 爬虫
self.crawler = get_jin10_crawler()
# WebSocket管理器
self.ws_manager = WebSocketManager("market_event")
# 系统日志
self.system_log = get_system_log()
# 运行状态
self._running = False
print("[MarketEventMonitor] 市场事件监控器已初始化")
def set_event_loop(self, loop):
"""设置主事件循环引用"""
self.ws_manager.set_event_loop(loop)
print("[MarketEventMonitor] 已设置主事件循环")
# ==================== 主循环 ====================
async def run(self):
"""主运行循环"""
if self._running:
print("[MarketEventMonitor] 已经在运行中")
return
self._running = True
print("[MarketEventMonitor] 开始运行...")
self.system_log.add_log("market_event_monitor_start", message="市场事件监控已启动")
await asyncio.gather(
self._flash_news_loop(),
self._event_reminder_loop(),
self._cleanup_loop(),
)
async def stop(self):
"""停止运行"""
self._running = False
await self.crawler.close()
print("[MarketEventMonitor] 已停止")
# ==================== 快讯监控循环 ====================
async def _flash_news_loop(self):
"""快讯监控循环"""
max_id = 0
check_count = 0
while self._running:
try:
check_count += 1
# 每10次检查记录一次日志
if check_count % 10 == 0:
self.system_log.add_log("flash_news_fetch", detail={
"check_count": check_count,
"max_id": max_id
}, message=f"快讯检查 #{check_count}")
# 获取最新快讯
news_list = await self.crawler.fetch_flash_news(max_id=max_id, count=20)
if news_list:
self.system_log.add_log("flash_news_fetch", detail={
"count": len(news_list)
}, message=f"获取到 {len(news_list)} 条快讯")
for news in reversed(news_list):
# 检查是否已处理
if self.flash_news_store.is_alerted(news.id):
continue
# 处理快讯(分析影响)
analysis = self.flash_news_service.process_news(news)
# 只推送有影响的快讯
if analysis:
alert = {
"type": "flash_news",
"news": news.to_dict(),
"analysis": analysis,
"timestamp": datetime.now().isoformat()
}
await self.ws_manager.broadcast(alert)
self.system_log.add_log("flash_news_impact", detail={
"news_id": news.id,
"speaker": news.speaker,
"impact": news.impact
}, message=f"快讯影响分析: {news.speaker or '事件'} -> {list(news.impact.keys())}")
# 标记已处理
self.flash_news_store.mark_alerted(news.id)
# 更新max_id
try:
if int(news.id) > max_id:
max_id = int(news.id)
except:
pass
# 每30秒检查一次
await asyncio.sleep(30)
except Exception as e:
self.system_log.add_log("flash_news_fetch_error", detail={
"error": str(e)
}, message=f"快讯监控异常: {e}")
print(f"[MarketEventMonitor] 快讯监控异常: {e}")
await asyncio.sleep(10)
# ==================== 事件提醒循环 ====================
async def _event_reminder_loop(self):
"""检查即将发布的财经日历事件并发送提醒"""
while self._running:
try:
# 获取需要提醒的事件
events = self.calendar_service.check_upcoming_reminders()
for event in events:
await self._send_event_reminder(event)
# 每分钟检查一次
await asyncio.sleep(60)
except Exception as e:
print(f"[MarketEventMonitor] 事件提醒检查异常: {e}")
await asyncio.sleep(30)
async def _send_event_reminder(self, event: CalendarEvent):
"""发送事件提醒"""
alert = {
"type": "calendar_event_reminder",
"event": event.to_dict(),
"message": f"重要数据 {event.name} 将在5分钟内发布",
"timestamp": datetime.now().isoformat()
}
await self.ws_manager.broadcast(alert)
self.system_log.add_log("calendar_event_reminder", detail={
"event_id": event.id,
"event_name": event.name,
"currency": event.currency
}, message=f"事件发布前提醒: {event.name}")
print(f"[MarketEventMonitor] 事件提醒: {event.name}")
# ==================== 清理循环 ====================
async def _cleanup_loop(self):
"""定期清理过期数据"""
while self._running:
try:
await asyncio.sleep(600) # 每10分钟
removed = self.calendar_store.cleanup_expired()
if removed > 0:
print(f"[MarketEventMonitor] 已清理 {removed} 条过期事件")
except Exception as e:
print(f"[MarketEventMonitor] 清理任务异常: {e}")
await asyncio.sleep(60)
# ==================== 数据更新接口 ====================
def update_calendar_from_mt5(self, events_data: List[Dict]) -> int:
"""
从MT5数据更新财经日历
Args:
events_data: MT5返回的事件列表
Returns:
更新的事件数量
"""
count = self.calendar_store.update_from_mt5(events_data)
# 广播日历更新
self.ws_manager.broadcast_sync({
"type": "calendar_update",
"data": self.calendar_store.get_events()
})
return count
# ==================== 查询接口 ====================
def get_calendar(self, date_str: str = None) -> List[Dict]:
"""获取财经日历"""
return self.calendar_service.get_calendar(date_str)
def get_upcoming_events(self, hours: int = 24) -> List[Dict]:
"""获取即将发布的重要事件"""
return self.calendar_service.get_upcoming_events(hours)
def get_recent_news(self, count: int = 20) -> List[Dict]:
"""获取最近快讯"""
return self.flash_news_service.get_recent_news(count)
def get_status(self) -> Dict:
"""获取监控状态"""
return {
"running": self._running,
"ws_status": self.ws_manager.get_status(),
"calendar_status": self.calendar_service.get_status(),
"flash_news_status": self.flash_news_service.get_status()
}
def clear_calendar(self) -> None:
"""清空财经日历数据"""
self.calendar_store.clear()
print("[MarketEventMonitor] 财经日历数据已清空")
# 全局单例
_market_event_monitor = None
def get_market_event_monitor() -> MarketEventMonitor:
"""获取市场事件监控器单例"""
global _market_event_monitor
if _market_event_monitor is None:
_market_event_monitor = MarketEventMonitor()
return _market_event_monitor
+1 -1
View File
@@ -8,7 +8,7 @@ K线合并模块
from typing import List, Dict, Optional
from datetime import datetime
from .store import KlineData
from .models import KlineData
class KlineMerger:
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
数据模型模块
"""
from .kline import KlineData
from .pivot import PivotPoint
from .llm_config import LLMConfig
from .llm_analysis import LLMAnalysisResult
from .tech_analysis import TechTrendState, TechTrendChange, TechResonanceResult, TechTradeSuggestion
from .calendar_event import CalendarEvent
from .flash_news import FlashNews
from .pending_order import PendingOrder
from .trading_instruction import TradingInstruction
from .trading_signal import TradingSignal, SignalSource, SignalStatus
from .trading_strategy import (
TradingStrategy, TradingDecision,
ConsistencyRequirement, ConflictResolution, VolumeMode,
StopLossMode, TakeProfitMode, PositionConflict
)
from .statistics import StatisticsData
from .position import PositionData
from .trade_history import TradeDeal
__all__ = [
'KlineData', 'PivotPoint',
'LLMConfig', 'LLMAnalysisResult',
'TechTrendState', 'TechTrendChange', 'TechResonanceResult', 'TechTradeSuggestion',
'CalendarEvent', 'FlashNews',
'PendingOrder', 'TradingInstruction',
'TradingSignal', 'SignalSource', 'SignalStatus',
'TradingStrategy', 'TradingDecision',
'ConsistencyRequirement', 'ConflictResolution', 'VolumeMode',
'StopLossMode', 'TakeProfitMode', 'PositionConflict',
'StatisticsData', 'PositionData', 'TradeDeal'
]
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
财经日历事件数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, List, Optional
import re
# MT5中表示无效值的特殊数值
MT5_INVALID_VALUE = -9223372036854775808.0
# MT5时区偏移(相对于北京时间)
# MT5服务器通常是 GMT+2,北京时间是 GMT+8
# 所以 MT5时间 + 6小时 = 北京时间
MT5_TIMEZONE_OFFSET_HOURS = 6
def clean_invalid_value(value: str) -> str:
"""清理MT5返回的无效值"""
if not value:
return ""
try:
num = float(value)
if num == MT5_INVALID_VALUE or num < -1e15:
return ""
if num == int(num):
return str(int(num))
return value
except (ValueError, TypeError):
return value
def clean_text(text: str) -> str:
"""清理文本中的控制字符和无效Unicode"""
if not text:
return ""
cleaned = re.sub(r'[\x00-\x1f\x7f]', '', text)
result = []
for char in cleaned:
code = ord(char)
if (0x20 <= code <= 0x7E or
0x4E00 <= code <= 0x9FFF or
0x3000 <= code <= 0x303F or
0xFF00 <= code <= 0xFFEF or
code > 0x9FFF):
result.append(char)
return ''.join(result)
def is_valid_name(name: str) -> bool:
"""检查名称是否有效(不是乱码)"""
if not name or len(name) < 2:
return False
printable_count = 0
for char in name:
code = ord(char)
if (0x20 <= code <= 0x7E or
0x4E00 <= code <= 0x9FFF or
0x3000 <= code <= 0x303F or
0xFF00 <= code <= 0xFFEF):
printable_count += 1
ratio = printable_count / len(name) if name else 0
return ratio >= 0.7
@dataclass
class CalendarEvent:
"""财经日历事件"""
id: str
name: str
name_en: str = ""
country: str = ""
currency: str = ""
importance: int = 0 # 0-3
publish_time: Optional[datetime] = None
forecast: str = ""
previous: str = ""
actual: str = ""
unit: str = ""
symbols: List[str] = field(default_factory=list)
event_type: str = ""
# 发布后填充
result: str = "" # better/worse/in_line
impact: Dict = field(default_factory=dict)
analyzed: bool = False
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"id": self.id,
"name": self.name,
"name_en": self.name_en,
"country": self.country,
"currency": self.currency,
"importance": self.importance,
"publish_time": self.publish_time.isoformat() if self.publish_time else None,
"forecast": self.forecast,
"previous": self.previous,
"actual": self.actual,
"unit": self.unit,
"symbols": self.symbols,
"event_type": self.event_type,
"result": self.result,
"impact": self.impact,
"analyzed": self.analyzed
}
@classmethod
def from_mt5_data(cls, event_data: Dict) -> Optional['CalendarEvent']:
"""
从MT5数据创建事件对象
Args:
event_data: MT5返回的事件数据
Returns:
CalendarEvent 或 None(如果数据无效)
"""
event_id = str(event_data.get('id', ''))
if not event_id:
return None
# 解析发布时间
publish_time = event_data.get('publish_time')
if isinstance(publish_time, str):
try:
publish_time = datetime.fromisoformat(publish_time.replace('Z', '+00:00'))
except:
try:
publish_time = datetime.strptime(publish_time, '%Y.%m.%d %H:%M:%S')
except:
return None
elif not isinstance(publish_time, datetime):
return None
# 将 MT5 时间转换为北京时间(GMT+8)
# MT5服务器时间通常是 GMT+2,北京时间是 GMT+8,差6小时
publish_time = publish_time + timedelta(hours=MT5_TIMEZONE_OFFSET_HOURS)
# 清理并检查名称有效性
cleaned_name = clean_text(event_data.get('name', ''))
if not is_valid_name(cleaned_name):
return None
return cls(
id=event_id,
name=cleaned_name,
name_en=clean_text(event_data.get('name_en', '')),
country=clean_text(event_data.get('country', '')),
currency=event_data.get('currency', ''),
importance=event_data.get('importance', 0),
publish_time=publish_time,
forecast=clean_invalid_value(event_data.get('forecast', '')),
previous=clean_invalid_value(event_data.get('previous', '')),
actual=clean_invalid_value(event_data.get('actual', '')),
unit=event_data.get('unit', ''),
symbols=event_data.get('symbols', []),
event_type=event_data.get('event_type', '')
)
+78
View File
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快讯数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class FlashNews:
"""快讯数据"""
id: str
content: str
source: str = ""
time: Optional[datetime] = None
importance: int = 0
keywords: List[str] = field(default_factory=list)
related_symbols: List[str] = field(default_factory=list)
# 分析后填充
speaker: str = ""
speaker_title: str = ""
impact: Dict = field(default_factory=dict)
analyzed: bool = False
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"id": self.id,
"content": self.content,
"source": self.source,
"time": self.time.isoformat() if self.time else None,
"importance": self.importance,
"keywords": self.keywords,
"related_symbols": self.related_symbols,
"speaker": self.speaker,
"speaker_title": self.speaker_title,
"impact": self.impact,
"analyzed": self.analyzed
}
@classmethod
def from_jin10_data(cls, data: Dict) -> 'FlashNews':
"""
从金十数据创建快讯对象
Args:
data: 金十API返回的快讯数据
Returns:
FlashNews
"""
# 解析时间
time = None
time_str = data.get('time') or data.get('publish_time')
if time_str:
if isinstance(time_str, datetime):
time = time_str
elif isinstance(time_str, (int, float)):
time = datetime.fromtimestamp(time_str)
else:
try:
time = datetime.fromisoformat(str(time_str).replace('Z', '+00:00'))
except:
pass
return cls(
id=str(data.get('id', '')),
content=data.get('content', ''),
source=data.get('source', 'jin10'),
time=time,
importance=data.get('importance', 0),
keywords=data.get('keywords', []),
related_symbols=data.get('related_symbols', [])
)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线数据结构
"""
from datetime import datetime
from typing import Dict
class KlineData:
"""K线数据结构"""
def __init__(self, symbol: str, period: str, timestamp, open_price: float,
high: float, low: float, close: float, volume: float = 0):
self.symbol = symbol
self.period = period # H4, H1, M15, M5, M1
self.timestamp = timestamp
self.open = open_price
self.high = high
self.low = low
self.close = close
self.volume = volume
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"open": self.open,
"high": self.high,
"low": self.low,
"close": self.close,
"volume": self.volume
}
@classmethod
def from_dict(cls, data: Dict) -> 'KlineData':
"""从字典创建"""
return cls(
symbol=data.get('symbol', ''),
period=data.get('period', ''),
timestamp=data.get('timestamp') or data.get('time'),
open_price=float(data.get('open', 0)),
high=float(data.get('high', 0)),
low=float(data.get('low', 0)),
close=float(data.get('close', 0)),
volume=float(data.get('volume', 0))
)
+56
View File
@@ -0,0 +1,56 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 分析结果数据结构
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class LLMAnalysisResult:
"""LLM 分析结果"""
symbol: str
trend_analysis: Dict = field(default_factory=dict)
overall_trend: Optional[Dict] = None
key_levels: Optional[Dict] = None
trade_suggestions: List[Dict] = field(default_factory=list)
analyzed_at: Optional[str] = None
data_stale: bool = False
market_status: str = "active" # active, stale, closed
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"symbol": self.symbol,
"trend_analysis": self.trend_analysis,
"overall_trend": self.overall_trend,
"key_levels": self.key_levels,
"trade_suggestions": self.trade_suggestions,
"analyzed_at": self.analyzed_at,
"data_stale": self.data_stale,
"market_status": self.market_status
}
@classmethod
def from_api_response(cls, symbol: str, data: Dict) -> 'LLMAnalysisResult':
"""从 LLM API 返回的字典创建"""
return cls(
symbol=symbol,
trend_analysis=data.get("trend_analysis", {}),
overall_trend=data.get("overall_trend"),
key_levels=data.get("key_levels"),
trade_suggestions=data.get("trade_suggestions", []),
analyzed_at=datetime.now().isoformat(),
data_stale=False,
market_status="active"
)
def get_trade_suggestion(self, period: str) -> Optional[Dict]:
"""获取指定周期的交易建议"""
for ts in self.trade_suggestions:
if ts.get("period") == period:
return ts
return None
+47
View File
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 配置数据结构
"""
from dataclasses import dataclass
from typing import Dict
@dataclass
class LLMConfig:
"""LLM 配置"""
api_key: str = ""
api_base: str = "https://api.openai.com/v1"
model: str = "gpt-4o-mini"
@property
def enabled(self) -> bool:
"""是否启用(有 API Key 才启用)"""
return bool(self.api_key)
def to_dict(self) -> Dict:
"""转换为字典(API Key 脱敏)"""
masked_key = ""
if self.api_key:
if len(self.api_key) > 8:
masked_key = self.api_key[:4] + "****" + self.api_key[-4:]
else:
masked_key = "****"
return {
"api_key": masked_key,
"api_key_set": bool(self.api_key),
"api_base": self.api_base,
"model": self.model,
"enabled": self.enabled
}
@classmethod
def from_dict(cls, data: Dict) -> 'LLMConfig':
"""从字典创建"""
return cls(
api_key=data.get("api_key", ""),
api_base=data.get("api_base", "https://api.openai.com/v1"),
model=data.get("model", "gpt-4o-mini")
)
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
待确认订单数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, Optional
import uuid
@dataclass
class PendingOrder:
"""待确认订单"""
# 必填字段
symbol: str
action: str # b=买入, s=卖出
price: float # 入场价
mount: float # 手数
sl: float # 止损
tp: float # 止盈
# 可选字段
reason: str = ""
description: str = ""
source: str = "" # auto_pivot_m1/key_level/ai_entry_nearby/manual
# 策略相关字段
pivot_price: Optional[float] = None # 转折点价格
key_level: Optional[float] = None # 关键点位
ai_period: Optional[str] = None # AI分析周期
ai_entry_price: Optional[float] = None # AI入场价
ai_direction: Optional[str] = None # AI方向
# AI方向一致性分析
ai_directions: Optional[Dict] = None
direction_consistent: bool = False
consistent_periods: list = field(default_factory=list)
inconsistent_periods: list = field(default_factory=list)
recommendation: str = ""
recommendation_color: str = ""
# 自动生成字段
order_id: str = ""
status: str = "pending" # pending/confirmed/rejected/expired
created_at: Optional[datetime] = None
expires_at: Optional[datetime] = None
confirmed_at: Optional[datetime] = None
# 超时时间(秒)
TIMEOUT_SECONDS: int = field(default=180, repr=False)
def __post_init__(self):
if not self.order_id:
self.order_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.expires_at:
self.expires_at = self.created_at + timedelta(seconds=self.TIMEOUT_SECONDS)
def is_expired(self) -> bool:
"""检查是否已过期"""
return datetime.now() > self.expires_at
def is_pending(self) -> bool:
"""检查是否待处理"""
return self.status == "pending" and not self.is_expired()
def confirm(self) -> None:
"""确认订单"""
self.status = "confirmed"
self.confirmed_at = datetime.now()
def reject(self) -> None:
"""拒绝订单"""
self.status = "rejected"
def mark_expired(self) -> None:
"""标记为过期"""
self.status = "expired"
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"order_id": self.order_id,
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": self.mount,
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"description": self.description,
"source": self.source,
"pivot_price": self.pivot_price,
"key_level": self.key_level,
"ai_period": self.ai_period,
"ai_entry_price": self.ai_entry_price,
"ai_direction": self.ai_direction,
"ai_directions": self.ai_directions,
"direction_consistent": self.direction_consistent,
"consistent_periods": self.consistent_periods,
"inconsistent_periods": self.inconsistent_periods,
"recommendation": self.recommendation,
"recommendation_color": self.recommendation_color,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"confirmed_at": self.confirmed_at.isoformat() if self.confirmed_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'PendingOrder':
"""从字典创建"""
# 处理时间字段
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
expires_at = data.get('expires_at')
if isinstance(expires_at, str):
expires_at = datetime.fromisoformat(expires_at)
confirmed_at = data.get('confirmed_at')
if isinstance(confirmed_at, str):
confirmed_at = datetime.fromisoformat(confirmed_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
price=data.get('price', 0.0),
mount=data.get('mount', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.0),
reason=data.get('reason', ''),
description=data.get('description', ''),
source=data.get('source', ''),
pivot_price=data.get('pivot_price'),
key_level=data.get('key_level'),
ai_period=data.get('ai_period'),
ai_entry_price=data.get('ai_entry_price'),
ai_direction=data.get('ai_direction'),
ai_directions=data.get('ai_directions'),
direction_consistent=data.get('direction_consistent', False),
consistent_periods=data.get('consistent_periods', []),
inconsistent_periods=data.get('inconsistent_periods', []),
recommendation=data.get('recommendation', ''),
recommendation_color=data.get('recommendation_color', ''),
order_id=data.get('order_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
expires_at=expires_at,
confirmed_at=confirmed_at,
)
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点数据结构
"""
from datetime import datetime
from typing import Dict
class PivotPoint:
"""转折点数据结构"""
def __init__(self, symbol: str, period: str, timestamp, price: float,
direction: str, strength: int = 3):
self.symbol = symbol
self.period = period
self.timestamp = timestamp
self.price = price
self.direction = direction # "high" 或 "low"
self.strength = strength # 转折强度(左右各N根K线)
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"price": self.price,
"direction": self.direction,
"strength": self.strength
}
+85
View File
@@ -0,0 +1,85 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓数据模型
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Optional
@dataclass
class PositionData:
"""
持仓数据
EA通过 /ea/positions 上报
"""
ticket: int
symbol: str
volume: float
price_open: float
position_type: str # "BUY" / "SELL"
profit: float
# 止损止盈
sl: float = 0.0
tp: float = 0.0
distance_sl: float = 0.0 # 距离止损的点数
distance_tp: float = 0.0 # 距离止盈的点数
# 元数据
updated_at: datetime = None
def __post_init__(self):
if self.updated_at is None:
self.updated_at = datetime.now()
@property
def is_buy(self) -> bool:
"""是否为买单"""
return self.position_type.upper() == "BUY"
@property
def is_sell(self) -> bool:
"""是否为卖单"""
return self.position_type.upper() == "SELL"
@property
def direction(self) -> str:
"""方向:buy / sell"""
return "buy" if self.is_buy else "sell"
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"ticket": self.ticket,
"symbol": self.symbol,
"volume": self.volume,
"price_open": self.price_open,
"type": self.position_type,
"profit": self.profit,
"sl": self.sl,
"tp": self.tp,
"distance_sl": self.distance_sl,
"distance_tp": self.distance_tp,
"direction": self.direction,
"updated_at": self.updated_at.isoformat() if self.updated_at else None
}
@classmethod
def from_ea_data(cls, data: Dict, symbol: str = None) -> 'PositionData':
"""从EA上报数据创建"""
return cls(
ticket=int(data.get('ticket', 0)),
symbol=data.get('symbol', symbol or ''),
volume=float(data.get('volume', 0)),
price_open=float(data.get('priceOpen', 0)),
position_type=data.get('type', 'BUY').upper(),
profit=float(data.get('profit', 0)),
sl=float(data.get('sl', 0)),
tp=float(data.get('tp', 0)),
distance_sl=float(data.get('distanceSL', 0)),
distance_tp=float(data.get('distanceTP', 0))
)
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统计数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional, Dict
@dataclass
class StatisticsData:
"""
EA上报的统计数据
用途:
1. 获取品种价差(TechService
2. 获取账户信息(前端展示)
3. 检查数据时效性
"""
symbol: str
timestamp: datetime
# 价格信息
bid_price: float
ask_price: float
spread: float
spread_points: float
# 账户信息
balance: float
equity: float
margin_level: float
# 其他
tick_count: int = 0
@property
def mid_price(self) -> float:
"""中间价"""
return (self.bid_price + self.ask_price) / 2
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"symbol": self.symbol,
"timestamp": self.timestamp.isoformat() if self.timestamp else None,
"bid_price": self.bid_price,
"ask_price": self.ask_price,
"spread": self.spread,
"spread_points": self.spread_points,
"balance": self.balance,
"equity": self.equity,
"margin_level": self.margin_level,
"tick_count": self.tick_count,
"mid_price": self.mid_price
}
@classmethod
def from_ea_data(cls, data: Dict) -> 'StatisticsData':
"""从EA上报数据创建"""
# 解析时间戳
timestamp = data.get('timestamp')
if isinstance(timestamp, str):
try:
timestamp = datetime.fromisoformat(timestamp)
except:
timestamp = datetime.now()
elif not isinstance(timestamp, datetime):
timestamp = datetime.now()
return cls(
symbol=data.get('symbol', ''),
timestamp=timestamp,
bid_price=float(data.get('bidPrice', 0)),
ask_price=float(data.get('askPrice', 0)),
spread=float(data.get('spread', 0)),
spread_points=float(data.get('spreadPoints', 0)),
balance=float(data.get('balance', 0)),
equity=float(data.get('equity', 0)),
margin_level=float(data.get('marginLevel', 0)),
tick_count=int(data.get('tickCount', 0))
)
+113
View File
@@ -0,0 +1,113 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
技术分析相关数据结构
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
@dataclass
class TechTrendState:
"""单周期趋势状态"""
symbol: str
period: str
trend: str = "unknown" # "up" / "down" / "sideways" / "unknown"
strength: int = 0 # 0-100
adx: float = 0.0
ma_fast: float = 0.0
ma_slow: float = 0.0
price: float = 0.0
reason: str = ""
timestamp: Optional[str] = None
previous_trend: Optional[str] = None
change_signal: bool = False
def to_dict(self) -> Dict:
return {
"trend": self.trend,
"strength": self.strength,
"adx": self.adx,
"ma_fast": self.ma_fast,
"ma_slow": self.ma_slow,
"price": self.price,
"reason": self.reason,
"timestamp": self.timestamp,
"previous_trend": self.previous_trend,
"change_signal": self.change_signal
}
@dataclass
class TechTrendChange:
"""趋势转换记录"""
period: str
from_trend: str
to_trend: str
price: float
timestamp: str
def to_dict(self) -> Dict:
return {
"period": self.period,
"from_trend": self.from_trend,
"to_trend": self.to_trend,
"price": self.price,
"timestamp": self.timestamp
}
@dataclass
class TechResonanceResult:
"""多周期共振结果"""
symbol: str
resonance: str = "none" # "up" / "down" / "none"
strength: int = 0
aligned_count: int = 0
up_count: int = 0
down_count: int = 0
sideways_count: int = 0
signal: str = "等待数据"
periods: Dict = field(default_factory=dict)
def to_dict(self) -> Dict:
return {
"resonance": self.resonance,
"strength": self.strength,
"aligned_count": self.aligned_count,
"up_count": self.up_count,
"down_count": self.down_count,
"sideways_count": self.sideways_count,
"signal": self.signal,
"periods": {p: s.to_dict() if hasattr(s, 'to_dict') else s for p, s in self.periods.items()}
}
@dataclass
class TechTradeSuggestion:
"""技术分析交易建议"""
symbol: str
action: str # "b" / "s"
price: float
sl: float
tp: float
reason: str
trend_strength: int
resonance_periods: int
generated_at: str
def to_dict(self) -> Dict:
return {
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": 0.01, # 默认手数
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"trend_strength": self.trend_strength,
"resonance_periods": self.resonance_periods,
"generated_at": self.generated_at
}
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易历史数据模型
"""
from dataclasses import dataclass
from datetime import datetime
from typing import Dict, Optional
import re
@dataclass
class TradeDeal:
"""
成交记录
EA通过 /trade_history 上报
"""
ticket: int
order: int
symbol: str
deal_type: int # 0=买入, 1=卖出
entry_type: int # 0=开仓, 1=平仓, 2=反向
volume: float
price: float
profit: float
swap: float
commission: float
time: datetime
comment: str
@property
def is_buy(self) -> bool:
"""是否为买入"""
return self.deal_type == 0
@property
def is_sell(self) -> bool:
"""是否为卖出"""
return self.deal_type == 1
@property
def is_entry(self) -> bool:
"""是否为开仓"""
return self.entry_type == 0
@property
def is_exit(self) -> bool:
"""是否为平仓"""
return self.entry_type == 1
@property
def deal_type_text(self) -> str:
"""成交类型文本"""
return "买入" if self.is_buy else "卖出"
@property
def entry_type_text(self) -> str:
"""入场类型文本"""
if self.entry_type == 0:
return "开仓"
elif self.entry_type == 1:
return "平仓"
elif self.entry_type == 2:
return "反向"
return "未知"
@property
def order_source(self) -> str:
"""订单来源"""
if not self.comment or not self.comment.strip():
return "手动"
comment = self.comment.strip()
if comment.startswith('[sl'):
return "止损触发"
if comment.startswith('[tp'):
return "止盈触发"
if comment.startswith('[so'):
return "强制平仓"
return "自动"
@property
def is_auto(self) -> bool:
"""是否为自动订单"""
if not self.comment or not self.comment.strip():
return False
comment = self.comment.strip()
# 排除MT5系统标记
if comment.startswith('[sl') or comment.startswith('[tp') or comment.startswith('[so'):
return False
return True
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"ticket": self.ticket,
"order": self.order,
"symbol": self.symbol,
"type": self.deal_type,
"type_text": self.deal_type_text,
"entry": self.entry_type,
"entry_text": self.entry_type_text,
"volume": self.volume,
"price": self.price,
"profit": self.profit,
"swap": self.swap,
"commission": self.commission,
"time": self.time.strftime("%Y-%m-%d %H:%M:%S") if self.time else None,
"comment": self.comment,
"is_auto": self.is_auto,
"order_source": self.order_source
}
@classmethod
def from_ea_data(cls, data: Dict) -> 'TradeDeal':
"""从EA上报数据创建"""
# 解析时间
deal_time = data.get('time')
if isinstance(deal_time, str):
for fmt in ["%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M:%S"]:
try:
deal_time = datetime.strptime(deal_time, fmt)
break
except:
pass
if not isinstance(deal_time, datetime):
deal_time = datetime.now()
return cls(
ticket=int(data.get('ticket', 0)),
order=int(data.get('order', 0)),
symbol=data.get('symbol', ''),
deal_type=int(data.get('type', 0)),
entry_type=int(data.get('entry', 0)),
volume=float(data.get('volume', 0)),
price=float(data.get('price', 0)),
profit=float(data.get('profit', 0)),
swap=float(data.get('swap', 0)),
commission=float(data.get('commission', 0)),
time=deal_time,
comment=data.get('comment', '')
)
+128
View File
@@ -0,0 +1,128 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易指令数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, Optional
import uuid
@dataclass
class TradingInstruction:
"""交易指令"""
# 必填字段
symbol: str
action: str # b=买入, s=卖出
price: float # 指令执行价格
mount: float # 手数
# 可选字段
sl: float = 0.0 # 止损
tp: float = 0.005 # 止盈(默认值)
reason: str = ""
description: str = ""
source: str = "" # manual/pending_order_confirm/key_level/ai_entry
# 来源追踪
order_id: Optional[str] = None # 来源订单ID(如果是确认订单转入)
# 自动生成字段
instruction_id: str = ""
status: str = "pending" # pending/sent/executed/cancelled
created_at: Optional[datetime] = None
sent_at: Optional[datetime] = None # 发送给EA的时间
executed_at: Optional[datetime] = None
def __post_init__(self):
if not self.instruction_id:
self.instruction_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
# 确保tp有默认值
if self.tp is None or self.tp <= 0:
self.tp = 0.005
def to_dict(self) -> Dict:
"""转换为字典(用于返回给EA"""
return {
"symbol": self.symbol.lower(),
"action": self.action.lower(),
"mount": self.mount,
"price": self.price,
"sl": self.sl,
"tp": self.tp,
}
def to_full_dict(self) -> Dict:
"""转换为完整字典(用于内部存储和查询)"""
return {
"instruction_id": self.instruction_id,
"symbol": self.symbol,
"action": self.action,
"price": self.price,
"mount": self.mount,
"sl": self.sl,
"tp": self.tp,
"reason": self.reason,
"description": self.description,
"source": self.source,
"order_id": self.order_id,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"sent_at": self.sent_at.isoformat() if self.sent_at else None,
"executed_at": self.executed_at.isoformat() if self.executed_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingInstruction':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
sent_at = data.get('sent_at')
if isinstance(sent_at, str):
sent_at = datetime.fromisoformat(sent_at)
executed_at = data.get('executed_at')
if isinstance(executed_at, str):
executed_at = datetime.fromisoformat(executed_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
price=data.get('price', 0.0),
mount=data.get('mount', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.005),
reason=data.get('reason', ''),
description=data.get('description', ''),
source=data.get('source', ''),
order_id=data.get('order_id'),
instruction_id=data.get('instruction_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
sent_at=sent_at,
executed_at=executed_at,
)
@classmethod
def from_pending_order(cls, order: 'PendingOrder') -> 'TradingInstruction':
"""从待确认订单创建"""
return cls(
symbol=order.symbol,
action=order.action,
price=order.price,
mount=order.mount,
sl=order.sl,
tp=order.tp,
reason=order.reason,
description=order.description,
source=f"pending_order_{order.source}",
order_id=order.order_id,
)
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易信号数据模型
纯分析结果,不含仓位资金
"""
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Dict, Optional
import uuid
class SignalSource:
"""信号来源"""
PIVOT = "pivot" # 转折点信号
KEY_LEVEL = "key_level" # 关键点位信号
AI_ENTRY = "ai_entry" # AI入场信号
class SignalStatus:
"""信号状态"""
ACTIVE = "active" # 活跃
EXPIRED = "expired" # 已过期
USED = "used" # 已被使用(生成决策)
@dataclass
class TradingSignal:
"""交易信号 - 纯分析结果,不含仓位资金"""
# ==================== 基本信息 ====================
symbol: str # 品种
action: str # 方向: buy/sell
confidence: int = 50 # 置信度 0-100
# ==================== 来源 ====================
source: str = "" # pivot/key_level/ai_entry
source_period: str = "" # 来源周期 (H4/H1/M15/M5/M1)
# ==================== 触发信息 ====================
trigger_price: float = 0.0 # 触发价格
trigger_time: datetime = None # 触发时间
trigger_reason: str = "" # 触发原因
# ==================== 建议参数 ====================
suggested_entry: float = 0.0 # 建议入场价
suggested_sl: float = 0.0 # 建议止损
suggested_tp: float = 0.0 # 建议止盈
risk_reward_ratio: float = 0.0 # 风险回报比
# ==================== 来源特有参数 ====================
# Pivot信号
pivot_price: Optional[float] = None
pivot_type: Optional[str] = None # high/low
# KeyLevel信号
key_level: Optional[float] = None
distance_pct: Optional[float] = None
# AI Entry信号
ai_analysis_period: Optional[str] = None
# ==================== 自动生成字段 ====================
signal_id: str = ""
status: str = SignalStatus.ACTIVE
created_at: datetime = None
expires_at: datetime = None
# 默认信号有效期(秒)
DEFAULT_TTL: int = field(default=300, repr=False) # 5分钟
def __post_init__(self):
if not self.signal_id:
self.signal_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.trigger_time:
self.trigger_time = self.created_at
if not self.expires_at:
self.expires_at = self.created_at + timedelta(seconds=self.DEFAULT_TTL)
def is_expired(self) -> bool:
"""检查是否已过期"""
return datetime.now() > self.expires_at
def is_active(self) -> bool:
"""检查是否活跃"""
return self.status == SignalStatus.ACTIVE and not self.is_expired()
def mark_used(self) -> None:
"""标记为已使用"""
self.status = SignalStatus.USED
def mark_expired(self) -> None:
"""标记为已过期"""
self.status = SignalStatus.EXPIRED
def get_risk_points(self) -> float:
"""获取风险点数"""
if self.action == "buy":
return abs(self.suggested_entry - self.suggested_sl)
else:
return abs(self.suggested_sl - self.suggested_entry)
def get_reward_points(self) -> float:
"""获取回报点数"""
if self.action == "buy":
return abs(self.suggested_tp - self.suggested_entry)
else:
return abs(self.suggested_entry - self.suggested_tp)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"signal_id": self.signal_id,
"symbol": self.symbol,
"action": self.action,
"confidence": self.confidence,
"source": self.source,
"source_period": self.source_period,
"trigger_price": self.trigger_price,
"trigger_time": self.trigger_time.isoformat() if self.trigger_time else None,
"trigger_reason": self.trigger_reason,
"suggested_entry": self.suggested_entry,
"suggested_sl": self.suggested_sl,
"suggested_tp": self.suggested_tp,
"risk_reward_ratio": self.risk_reward_ratio,
"pivot_price": self.pivot_price,
"pivot_type": self.pivot_type,
"key_level": self.key_level,
"distance_pct": self.distance_pct,
"ai_analysis_period": self.ai_analysis_period,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"risk_points": self.get_risk_points(),
"reward_points": self.get_reward_points(),
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingSignal':
"""从字典创建"""
trigger_time = data.get('trigger_time')
if isinstance(trigger_time, str):
trigger_time = datetime.fromisoformat(trigger_time)
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
elif created_at is None:
created_at = datetime.now()
expires_at = data.get('expires_at')
if isinstance(expires_at, str):
expires_at = datetime.fromisoformat(expires_at)
return cls(
symbol=data.get('symbol', ''),
action=data.get('action', ''),
confidence=data.get('confidence', 50),
source=data.get('source', ''),
source_period=data.get('source_period', ''),
trigger_price=data.get('trigger_price', 0.0),
trigger_time=trigger_time,
trigger_reason=data.get('trigger_reason', ''),
suggested_entry=data.get('suggested_entry', 0.0),
suggested_sl=data.get('suggested_sl', 0.0),
suggested_tp=data.get('suggested_tp', 0.0),
risk_reward_ratio=data.get('risk_reward_ratio', 0.0),
pivot_price=data.get('pivot_price'),
pivot_type=data.get('pivot_type'),
key_level=data.get('key_level'),
distance_pct=data.get('distance_pct'),
ai_analysis_period=data.get('ai_analysis_period'),
signal_id=data.get('signal_id', ''),
status=data.get('status', SignalStatus.ACTIVE),
created_at=created_at,
expires_at=expires_at,
)
+497
View File
@@ -0,0 +1,497 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易策略数据模型
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Optional
import json
import os
import uuid
class ConsistencyRequirement:
"""一致性要求"""
ANY = "any" # 任一信号即可
MAJORITY = "majority" # 多数信号一致
ALL = "all" # 所有信号一致
class ConflictResolution:
"""冲突解决策略"""
HIGHEST_CONFIDENCE = "highest_confidence" # 最高置信度
HIGHEST_WEIGHT = "highest_weight" # 最高权重
SKIP = "skip" # 跳过冲突
class VolumeMode:
"""手数模式"""
FIXED = "fixed" # 固定手数
RISK_PERCENT = "risk_percent" # 风险百分比
class StopLossMode:
"""止损模式"""
SIGNAL = "signal" # 使用信号建议
FIXED_POINTS = "fixed_points" # 固定点数
ATR_PERCENT = "atr_percent" # ATR百分比
class TakeProfitMode:
"""止盈模式"""
SIGNAL = "signal" # 使用信号建议
FIXED_POINTS = "fixed_points" # 固定点数
RISK_REWARD = "risk_reward" # 风险回报比
class PositionConflict:
"""持仓冲突处理"""
ALLOW_OPPOSITE = "allow_opposite" # 允许反向
ALLOW_SAME = "allow_same" # 允许同向
ALLOW_BOTH = "allow_both" # 都允许
BLOCK = "block" # 有持仓则阻止
@dataclass
class TradingStrategy:
"""交易策略 - 绑定品种,配置信号权重和决策规则"""
# ==================== 基本信息 ====================
symbol: str # 绑定的品种
strategy_name: str = "" # 策略名称
# ==================== 启用状态 ====================
enabled: bool = True # 是否启用
# ==================== 信号源配置(新版:支持周期级别控制)====================
# 信号源配置结构:
# {
# "pivot": {
# "enabled": true,
# "periods": {"M1": {"enabled": true, "weight": 15}, "M5": {"enabled": true, "weight": 20}, ...}
# },
# "key_level": {"enabled": true, "weight": 40}, # key_level 不区分周期
# "ai_entry": {
# "enabled": true,
# "periods": {"M5": {"enabled": true, "weight": 20}, ...}
# }
# }
signal_config: Dict = field(default_factory=lambda: {
"pivot": {
"enabled": True,
"periods": {
"M1": {"enabled": True, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": False, "weight": 25},
"H1": {"enabled": False, "weight": 20},
"H4": {"enabled": False, "weight": 20}
}
},
"key_level": {
"enabled": True,
"weight": 40
},
"ai_entry": {
"enabled": True,
"periods": {
"M1": {"enabled": False, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": True, "weight": 30},
"H1": {"enabled": True, "weight": 25},
"H4": {"enabled": False, "weight": 20}
}
}
})
# ==================== 信号权重配置(兼容旧版,已废弃)===================
signal_weights: Dict[str, int] = field(default_factory=lambda: {
"pivot": 30,
"key_level": 40,
"ai_entry": 30,
})
period_weights: Dict[str, int] = field(default_factory=lambda: {
"H4": 20,
"H1": 20,
"M15": 25,
"M5": 20,
"M1": 15,
})
# ==================== 信号过滤规则 ====================
min_confidence: int = 50
consistency_requirement: str = ConsistencyRequirement.MAJORITY
conflict_resolution: str = ConflictResolution.HIGHEST_WEIGHT
# ==================== 仓位管理 ====================
fixed_volume: float = 0.01
volume_mode: str = VolumeMode.FIXED
risk_percent: float = 1.0
max_risk_points: float = 50.0
max_positions: int = 3
max_same_direction: int = 2
# ==================== 止损止盈规则 ====================
sl_mode: str = StopLossMode.SIGNAL
sl_fixed_points: float = 20.0
sl_atr_multiplier: float = 1.5
tp_mode: str = TakeProfitMode.SIGNAL
tp_fixed_points: float = 40.0
tp_risk_reward: float = 2.0
# ==================== 过滤条件 ====================
min_risk_reward: float = 1.0
max_risk_reward: float = 5.0
min_sl_points: float = 5.0
max_sl_points: float = 100.0
# ==================== 时间过滤 ====================
trading_hours: Dict = field(default_factory=lambda: {
"start": "00:00",
"end": "23:59",
"exclude_hours": []
})
# ==================== 持仓冲突处理 ====================
position_conflict: str = PositionConflict.ALLOW_OPPOSITE
# ==================== 自动生成字段 ====================
strategy_id: str = ""
created_at: datetime = None
updated_at: datetime = None
def __post_init__(self):
if not self.strategy_id:
self.strategy_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
if not self.updated_at:
self.updated_at = self.created_at
if not self.strategy_name:
self.strategy_name = f"Strategy_{self.symbol}"
def update(self, data: Dict) -> None:
"""更新配置"""
if "enabled" in data:
self.enabled = bool(data["enabled"])
if "signal_config" in data:
self.signal_config = data["signal_config"]
if "signal_weights" in data:
self.signal_weights = data["signal_weights"]
if "period_weights" in data:
self.period_weights = data["period_weights"]
if "min_confidence" in data:
self.min_confidence = int(data["min_confidence"])
if "consistency_requirement" in data:
self.consistency_requirement = data["consistency_requirement"]
if "conflict_resolution" in data:
self.conflict_resolution = data["conflict_resolution"]
if "fixed_volume" in data:
self.fixed_volume = float(data["fixed_volume"])
if "volume_mode" in data:
self.volume_mode = data["volume_mode"]
if "risk_percent" in data:
self.risk_percent = float(data["risk_percent"])
if "max_positions" in data:
self.max_positions = int(data["max_positions"])
if "max_same_direction" in data:
self.max_same_direction = int(data["max_same_direction"])
if "sl_mode" in data:
self.sl_mode = data["sl_mode"]
if "tp_mode" in data:
self.tp_mode = data["tp_mode"]
if "min_risk_reward" in data:
self.min_risk_reward = float(data["min_risk_reward"])
if "max_risk_reward" in data:
self.max_risk_reward = float(data["max_risk_reward"])
if "position_conflict" in data:
self.position_conflict = data["position_conflict"]
if "trading_hours" in data:
self.trading_hours = data["trading_hours"]
self.updated_at = datetime.now()
def get_signal_weight(self, source: str, period: str = None) -> int:
"""
获取信号源权重(支持周期级别)
Args:
source: 信号源 (pivot/key_level/ai_entry)
period: 周期 (M1/M5/M15/H1/H4)key_level 不需要周期
Returns:
权重值
"""
# 优先使用新的 signal_config
if self.signal_config and source in self.signal_config:
config = self.signal_config[source]
if not config.get("enabled", True):
return 0
# key_level 不区分周期
if source == "key_level":
return config.get("weight", 0)
# 其他信号源区分周期
if period and "periods" in config:
period_config = config["periods"].get(period, {})
if not period_config.get("enabled", False):
return 0
return period_config.get("weight", 0)
# 如果没有 period 配置,返回 0
return 0
# 兼容旧版 signal_weights
return self.signal_weights.get(source, 0)
def is_signal_enabled(self, source: str, period: str = None) -> bool:
"""
检查信号源是否启用
Args:
source: 信号源
period: 周期(key_level 不需要)
Returns:
是否启用
"""
if not self.signal_config or source not in self.signal_config:
# 兼容旧版:signal_weights 中有配置就认为启用
return source in self.signal_weights and self.signal_weights[source] > 0
config = self.signal_config[source]
if not config.get("enabled", True):
return False
# key_level 不区分周期
if source == "key_level":
return True
# 其他信号源需要检查周期
if period and "periods" in config:
period_config = config["periods"].get(period, {})
return period_config.get("enabled", False)
return False
def get_period_weight(self, period: str) -> int:
"""获取周期权重(兼容旧版)"""
return self.period_weights.get(period, 0)
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"strategy_id": self.strategy_id,
"strategy_name": self.strategy_name,
"symbol": self.symbol,
"enabled": self.enabled,
"signal_config": self.signal_config,
"signal_weights": self.signal_weights,
"period_weights": self.period_weights,
"min_confidence": self.min_confidence,
"consistency_requirement": self.consistency_requirement,
"conflict_resolution": self.conflict_resolution,
"fixed_volume": self.fixed_volume,
"volume_mode": self.volume_mode,
"risk_percent": self.risk_percent,
"max_risk_points": self.max_risk_points,
"max_positions": self.max_positions,
"max_same_direction": self.max_same_direction,
"sl_mode": self.sl_mode,
"sl_fixed_points": self.sl_fixed_points,
"sl_atr_multiplier": self.sl_atr_multiplier,
"tp_mode": self.tp_mode,
"tp_fixed_points": self.tp_fixed_points,
"tp_risk_reward": self.tp_risk_reward,
"min_risk_reward": self.min_risk_reward,
"max_risk_reward": self.max_risk_reward,
"min_sl_points": self.min_sl_points,
"max_sl_points": self.max_sl_points,
"trading_hours": self.trading_hours,
"position_conflict": self.position_conflict,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingStrategy':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
updated_at = data.get('updated_at')
if isinstance(updated_at, str):
updated_at = datetime.fromisoformat(updated_at)
# 默认 signal_config
default_signal_config = {
"pivot": {
"enabled": True,
"periods": {
"M1": {"enabled": True, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": False, "weight": 25},
"H1": {"enabled": False, "weight": 20},
"H4": {"enabled": False, "weight": 20}
}
},
"key_level": {
"enabled": True,
"weight": 40
},
"ai_entry": {
"enabled": True,
"periods": {
"M1": {"enabled": False, "weight": 15},
"M5": {"enabled": True, "weight": 20},
"M15": {"enabled": True, "weight": 30},
"H1": {"enabled": True, "weight": 25},
"H4": {"enabled": False, "weight": 20}
}
}
}
return cls(
symbol=data.get('symbol', ''),
strategy_name=data.get('strategy_name', ''),
enabled=data.get('enabled', True),
signal_config=data.get('signal_config', default_signal_config),
signal_weights=data.get('signal_weights', {"pivot": 30, "key_level": 40, "ai_entry": 30}),
period_weights=data.get('period_weights', {"H4": 20, "H1": 20, "M15": 25, "M5": 20, "M1": 15}),
min_confidence=data.get('min_confidence', 50),
consistency_requirement=data.get('consistency_requirement', ConsistencyRequirement.MAJORITY),
conflict_resolution=data.get('conflict_resolution', ConflictResolution.HIGHEST_WEIGHT),
fixed_volume=data.get('fixed_volume', 0.01),
volume_mode=data.get('volume_mode', VolumeMode.FIXED),
risk_percent=data.get('risk_percent', 1.0),
max_risk_points=data.get('max_risk_points', 50.0),
max_positions=data.get('max_positions', 3),
max_same_direction=data.get('max_same_direction', 2),
sl_mode=data.get('sl_mode', StopLossMode.SIGNAL),
sl_fixed_points=data.get('sl_fixed_points', 20.0),
sl_atr_multiplier=data.get('sl_atr_multiplier', 1.5),
tp_mode=data.get('tp_mode', TakeProfitMode.SIGNAL),
tp_fixed_points=data.get('tp_fixed_points', 40.0),
tp_risk_reward=data.get('tp_risk_reward', 2.0),
min_risk_reward=data.get('min_risk_reward', 1.0),
max_risk_reward=data.get('max_risk_reward', 5.0),
min_sl_points=data.get('min_sl_points', 5.0),
max_sl_points=data.get('max_sl_points', 100.0),
trading_hours=data.get('trading_hours', {"start": "00:00", "end": "23:59", "exclude_hours": []}),
position_conflict=data.get('position_conflict', PositionConflict.ALLOW_OPPOSITE),
strategy_id=data.get('strategy_id', ''),
created_at=created_at,
updated_at=updated_at,
)
@dataclass
class TradingDecision:
"""交易决策 - 策略层输出"""
# ==================== 基本信息 ====================
symbol: str # 品种
strategy_id: str # 来源策略ID
# ==================== 决策结果 ====================
action: str = "" # buy/sell/none
decision_type: str = "" # signal_combined / single_signal / manual
# ==================== 信号汇总 ====================
signals: List[Dict] = field(default_factory=list)
signal_summary: Dict = field(default_factory=dict)
# ==================== 执行参数 ====================
entry_price: float = 0.0
sl: float = 0.0
tp: float = 0.0
volume: float = 0.01
risk_points: float = 0.0
reward_points: float = 0.0
risk_reward_ratio: float = 0.0
# ==================== 决策理由 ====================
decision_reason: str = ""
confidence_score: float = 0.0
# ==================== 检查结果 ====================
position_check: Dict = field(default_factory=dict)
risk_check: Dict = field(default_factory=dict)
# ==================== 状态 ====================
decision_id: str = ""
status: str = "pending" # pending/confirmed/rejected/expired
created_at: datetime = None
# ==================== 关联 ====================
order_id: Optional[str] = None
def __post_init__(self):
if not self.decision_id:
self.decision_id = str(uuid.uuid4())[:8]
if not self.created_at:
self.created_at = datetime.now()
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"decision_id": self.decision_id,
"symbol": self.symbol,
"strategy_id": self.strategy_id,
"action": self.action,
"decision_type": self.decision_type,
"signals": self.signals,
"signal_summary": self.signal_summary,
"entry_price": self.entry_price,
"sl": self.sl,
"tp": self.tp,
"volume": self.volume,
"risk_points": self.risk_points,
"reward_points": self.reward_points,
"risk_reward_ratio": self.risk_reward_ratio,
"decision_reason": self.decision_reason,
"confidence_score": self.confidence_score,
"position_check": self.position_check,
"risk_check": self.risk_check,
"status": self.status,
"created_at": self.created_at.isoformat() if self.created_at else None,
"order_id": self.order_id,
}
@classmethod
def from_dict(cls, data: Dict) -> 'TradingDecision':
"""从字典创建"""
created_at = data.get('created_at')
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at)
return cls(
symbol=data.get('symbol', ''),
strategy_id=data.get('strategy_id', ''),
action=data.get('action', ''),
decision_type=data.get('decision_type', ''),
signals=data.get('signals', []),
signal_summary=data.get('signal_summary', {}),
entry_price=data.get('entry_price', 0.0),
sl=data.get('sl', 0.0),
tp=data.get('tp', 0.0),
volume=data.get('volume', 0.01),
risk_points=data.get('risk_points', 0.0),
reward_points=data.get('reward_points', 0.0),
risk_reward_ratio=data.get('risk_reward_ratio', 0.0),
decision_reason=data.get('decision_reason', ''),
confidence_score=data.get('confidence_score', 0.0),
position_check=data.get('position_check', {}),
risk_check=data.get('risk_check', {}),
decision_id=data.get('decision_id', ''),
status=data.get('status', 'pending'),
created_at=created_at,
order_id=data.get('order_id'),
)
-1049
View File
File diff suppressed because it is too large Load Diff
+64 -1593
View File
File diff suppressed because it is too large Load Diff
-332
View File
@@ -1,332 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
新闻监控模块
分析影响、推送提醒
财经日历数据由EA端通过MT5 API获取后推送
"""
import asyncio
from datetime import datetime, timedelta
from typing import List, Dict, Optional, Set
import json
import threading
from .news_crawler import Jin10Crawler, get_jin10_crawler
from .news_store import CalendarEvent, FlashNews, get_news_store
from .event_config import get_high_impact_event_names
from .system_log import get_system_log
class NewsMonitor:
"""新闻监控器"""
def __init__(self):
self.crawler = get_jin10_crawler() # 仅用于快讯
self.store = get_news_store()
self.system_log = get_system_log()
# WebSocket客户端
self._ws_clients: Set = set()
self._ws_lock = threading.Lock()
# 主事件循环引用
self._main_loop = None
# 是否正在运行
self._running = False
# 高影响事件名称
self._high_impact_names = get_high_impact_event_names()
# 已调度的事件
self._scheduled_events: Dict[str, asyncio.Task] = {}
print("[NewsMonitor] 新闻监控器已初始化")
# 记录日志
self.system_log.add_log("news_crawler_start", message="新闻监控器已初始化(财经日历由EA推送)")
def set_event_loop(self, loop):
"""设置主事件循环引用"""
self._main_loop = loop
print("[NewsMonitor] 已设置主事件循环")
def add_ws_client(self, client):
"""添加WebSocket客户端"""
with self._ws_lock:
self._ws_clients.add(client)
print(f"[NewsMonitor] WebSocket客户端已连接, 当前连接数: {len(self._ws_clients)}")
def remove_ws_client(self, client):
"""移除WebSocket客户端"""
with self._ws_lock:
self._ws_clients.discard(client)
print(f"[NewsMonitor] WebSocket客户端已断开, 当前连接数: {len(self._ws_clients)}")
def get_ws_client_count(self) -> int:
"""获取WebSocket客户端数量"""
with self._ws_lock:
return len(self._ws_clients)
# ==================== 主循环 ====================
async def run(self):
"""主运行循环"""
if self._running:
print("[NewsMonitor] 已经在运行中")
return
self._running = True
print("[NewsMonitor] 开始运行...")
# 启动多个并行任务
await asyncio.gather(
self._flash_news_loop(), # 快讯监控(每30秒)
self._event_reminder_loop(), # 事件提醒(每分钟检查)
self._cleanup_loop(), # 过期数据清理(每10分钟)
)
async def stop(self):
"""停止运行"""
self._running = False
await self.crawler.close()
print("[NewsMonitor] 已停止")
# ==================== 事件提醒循环 ====================
async def _event_reminder_loop(self):
"""检查即将发布的事件并发送提醒"""
while self._running:
try:
now = datetime.now()
# 获取未来1小时内的重要事件
events = self.store.get_upcoming_events(hours=1)
for event in events:
if not event.publish_time:
continue
# 发布前5分钟提醒
time_to_publish = (event.publish_time - now).total_seconds()
if 0 < time_to_publish <= 300: # 5分钟内
if not self.store.is_event_alerted(f"{event.id}_reminder"):
await self._send_event_reminder(event)
self.store.mark_event_alerted(f"{event.id}_reminder")
# 每分钟检查一次
await asyncio.sleep(60)
except Exception as e:
print(f"[NewsMonitor] 事件提醒检查异常: {e}")
await asyncio.sleep(30)
async def _send_event_reminder(self, event: CalendarEvent):
"""发送事件提醒"""
alert = {
"type": "event_reminder",
"event": event.to_dict(),
"message": f"重要数据 {event.name} 将在5分钟内发布",
"timestamp": datetime.now().isoformat()
}
await self._broadcast_alert(alert)
self.system_log.add_log("news_event_reminder", detail={
"event_id": event.id,
"event_name": event.name,
"currency": event.currency
}, message=f"事件发布前提醒: {event.name}")
print(f"[NewsMonitor] 事件提醒: {event.name}")
# ==================== 快讯循环 ====================
async def _flash_news_loop(self):
"""快讯监控循环"""
max_id = 0
check_count = 0
while self._running:
try:
check_count += 1
# 每10次检查记录一次日志
if check_count % 10 == 0:
self.system_log.add_log("news_flash_fetch", detail={
"check_count": check_count,
"max_id": max_id
}, message=f"快讯检查 #{check_count}")
# 获取最新快讯
news_list = await self.crawler.fetch_flash_news(max_id=max_id, count=20)
if news_list:
self.system_log.add_log("news_flash_fetch", detail={
"count": len(news_list)
}, message=f"获取到 {len(news_list)} 条快讯")
for news in reversed(news_list): # 按时间顺序处理
# 检查是否已处理
if self.store.is_news_alerted(news.id):
continue
# 分析影响
analysis = self.crawler.analyze_news_impact(news)
# 只推送有影响的快讯
if analysis['impact'] or analysis['speaker']:
news.speaker = analysis['speaker']
news.speaker_title = analysis['speaker_title']
news.impact = analysis['impact']
news.analyzed = True
news.importance = 2 if analysis['speaker'] else 1
# 添加到存储
self.store.add_flash_news(news)
# 推送提醒
alert = {
"type": "flash_news",
"news": news.to_dict(),
"analysis": analysis,
"timestamp": datetime.now().isoformat()
}
await self._broadcast_alert(alert)
self.system_log.add_log("news_impact_analysis", detail={
"news_id": news.id,
"speaker": news.speaker,
"impact": news.impact
}, message=f"快讯影响分析: {news.speaker or '事件'} -> {list(news.impact.keys())}")
print(f"[NewsMonitor] 快讯已推送: {news.id} - {news.speaker}")
# 标记已处理
self.store.mark_news_alerted(news.id)
# 更新max_id
try:
if int(news.id) > max_id:
max_id = int(news.id)
except:
pass
# 每30秒检查一次
await asyncio.sleep(30)
except Exception as e:
self.system_log.add_log("news_flash_fetch_error", detail={
"error": str(e)
}, message=f"快讯监控异常: {e}")
print(f"[NewsMonitor] 快讯监控异常: {e}")
await asyncio.sleep(10)
# ==================== 清理循环 ====================
async def _cleanup_loop(self):
"""定期清理过期数据"""
while self._running:
try:
# 每10分钟清理一次
await asyncio.sleep(600)
removed = self.store.cleanup_expired_events()
if removed > 0:
print(f"[NewsMonitor] 已清理 {removed} 条过期事件")
except Exception as e:
print(f"[NewsMonitor] 清理任务异常: {e}")
await asyncio.sleep(60)
# ==================== 广播消息 ====================
async def _broadcast_alert(self, alert: Dict):
"""广播提醒到所有WebSocket客户端"""
message = json.dumps(alert, ensure_ascii=False)
with self._ws_lock:
clients = list(self._ws_clients)
if not clients:
return
if self._main_loop and self._main_loop.is_running():
for client in clients:
try:
asyncio.run_coroutine_threadsafe(
self._send_to_client(client, message),
self._main_loop
)
except Exception as e:
print(f"[NewsMonitor] 发送WebSocket消息失败: {e}")
else:
for client in clients:
try:
await self._send_to_client(client, message)
except Exception as e:
print(f"[NewsMonitor] 发送消息失败: {e}")
async def _send_to_client(self, client, message: str):
"""发送消息到客户端"""
try:
await client.send_text(message)
except Exception as e:
print(f"[NewsMonitor] 发送消息到客户端失败: {e}")
with self._ws_lock:
self._ws_clients.discard(client)
async def _broadcast_calendar_update(self):
"""广播日历更新"""
calendar = self.store.get_calendar()
message = json.dumps({
"type": "calendar_update",
"data": calendar
}, ensure_ascii=False)
with self._ws_lock:
clients = list(self._ws_clients)
for client in clients:
try:
await self._send_to_client(client, message)
except Exception:
pass
# ==================== 状态查询 ====================
def get_status(self) -> Dict:
"""获取监控状态"""
return {
"running": self._running,
"ws_clients": self.get_ws_client_count(),
"store_status": self.store.get_status(),
"scheduled_events": len(self._scheduled_events)
}
def get_calendar(self, date_str: str = None) -> List[Dict]:
"""获取财经日历"""
return self.store.get_calendar(date_str)
def get_upcoming_events(self, hours: int = 24) -> List[Dict]:
"""获取即将发布的事件"""
events = self.store.get_upcoming_events(hours)
return [e.to_dict() for e in events]
def get_recent_news(self, count: int = 20) -> List[Dict]:
"""获取最近快讯"""
return self.store.get_flash_news(count)
# 全局单例
_news_monitor = None
def get_news_monitor() -> NewsMonitor:
"""获取新闻监控器单例"""
global _news_monitor
if _news_monitor is None:
_news_monitor = NewsMonitor()
return _news_monitor
-441
View File
@@ -1,441 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
新闻数据存储模块
存储财经日历、快讯和事件数据
"""
from collections import deque
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import threading
from dataclasses import dataclass, field
import re
# MT5中表示无效值的特殊数值
MT5_INVALID_VALUE = -9223372036854775808.0
def _clean_invalid_value(value: str) -> str:
"""清理MT5返回的无效值"""
if not value:
return ""
try:
# 检查是否是无效值
num = float(value)
if num == MT5_INVALID_VALUE or num < -1e15:
return ""
# 格式化有效数值
if num == int(num):
return str(int(num))
return value
except (ValueError, TypeError):
return value
def _clean_text(text: str) -> str:
"""清理文本中的控制字符和无效Unicode"""
if not text:
return ""
# 移除控制字符 (0x00-0x1F 和 0x7F)
cleaned = re.sub(r'[\x00-\x1f\x7f]', '', text)
# 移除不可打印字符(保留ASCII、中文等常用字符)
# 保留:ASCII (0x20-0x7E), 中文 (0x4E00-0x9FFF), 其他常用Unicode
result = []
for char in cleaned:
code = ord(char)
if (0x20 <= code <= 0x7E or # ASCII可打印字符
0x4E00 <= code <= 0x9FFF or # CJK统一汉字
0x3000 <= code <= 0x303F or # CJK标点
0xFF00 <= code <= 0xFFEF or # 全角字符
code > 0x9FFF): # 其他Unicode字符
result.append(char)
return ''.join(result)
def _is_valid_name(name: str) -> bool:
"""检查名称是否有效(不是乱码)"""
if not name or len(name) < 2:
return False
# 计算可打印字符的比例
printable_count = 0
for char in name:
code = ord(char)
if (0x20 <= code <= 0x7E or # ASCII可打印字符
0x4E00 <= code <= 0x9FFF or # CJK统一汉字
0x3000 <= code <= 0x303F or # CJK标点
0xFF00 <= code <= 0xFFEF): # 全角字符
printable_count += 1
# 如果可打印字符比例低于70%,认为是乱码
ratio = printable_count / len(name) if name else 0
return ratio >= 0.7
@dataclass
class CalendarEvent:
"""财经日历事件"""
id: str
name: str
name_en: str = ""
country: str = ""
currency: str = "" # 货币代码
importance: int = 0 # 0-3
publish_time: datetime = None
forecast: str = ""
previous: str = ""
actual: str = ""
unit: str = ""
symbols: List[str] = field(default_factory=list)
event_type: str = "" # 事件类型(指标、讲话等)
# 发布后填充
result: str = "" # better/worse/in_line
impact: Dict = field(default_factory=dict) # {symbol: {direction, reason}}
analyzed: bool = False
def to_dict(self) -> Dict:
return {
"id": self.id,
"name": self.name,
"name_en": self.name_en,
"country": self.country,
"currency": self.currency,
"importance": self.importance,
"publish_time": self.publish_time.isoformat() if self.publish_time else None,
"forecast": self.forecast,
"previous": self.previous,
"actual": self.actual,
"unit": self.unit,
"symbols": self.symbols,
"event_type": self.event_type,
"result": self.result,
"impact": self.impact,
"analyzed": self.analyzed
}
@dataclass
class FlashNews:
"""快讯数据"""
id: str
content: str
source: str = ""
time: datetime = None
importance: int = 0
keywords: List[str] = field(default_factory=list)
related_symbols: List[str] = field(default_factory=list)
# 分析后填充
speaker: str = ""
speaker_title: str = ""
impact: Dict = field(default_factory=dict)
analyzed: bool = False
def to_dict(self) -> Dict:
return {
"id": self.id,
"content": self.content,
"source": self.source,
"time": self.time.isoformat() if self.time else None,
"importance": self.importance,
"keywords": self.keywords,
"related_symbols": self.related_symbols,
"speaker": self.speaker,
"speaker_title": self.speaker_title,
"impact": self.impact,
"analyzed": self.analyzed
}
class NewsStore:
"""新闻存储"""
# 过期数据清理阈值(小时)
EXPIRY_HOURS = 6
def __init__(self):
# 财经日历: 使用列表存储所有事件,按时间排序
# 不再按日期分片,直接存储在内存中
self._calendar_events: List[CalendarEvent] = []
self._calendar_lock = threading.RLock()
# 快讯历史: deque[FlashNews]
# 保留最新100条
self._flash_news: deque = deque(maxlen=100)
self._news_lock = threading.RLock()
# 已提醒的事件ID
self._alerted_events: set = set()
self._alerted_news: set = set()
# 即将发布的重要事件(用于调度)
self._upcoming_events: Dict[str, CalendarEvent] = {}
print("[NewsStore] 新闻存储已初始化")
# ==================== 财经日历 ====================
def update_calendar_from_mt5(self, events: List[Dict]) -> int:
"""
从MT5数据更新财经日历
Args:
events: MT5返回的事件列表
Returns:
更新的事件数量
"""
now = datetime.now()
expiry_threshold = now - timedelta(hours=self.EXPIRY_HOURS)
with self._calendar_lock:
# 1. 清理过期数据
self._calendar_events = [
e for e in self._calendar_events
if e.publish_time and e.publish_time > expiry_threshold
]
# 2. 构建现有事件的ID集合
existing_ids = {e.id for e in self._calendar_events}
# 3. 添加或更新事件
new_count = 0
update_count = 0
for event_data in events:
event_id = str(event_data.get('id', ''))
# 解析发布时间
publish_time = event_data.get('publish_time')
if isinstance(publish_time, str):
try:
# 尝试ISO格式
publish_time = datetime.fromisoformat(publish_time.replace('Z', '+00:00'))
except:
try:
# 尝试MQL5 TimeToString格式: "2026.03.16 20:30:00"
publish_time = datetime.strptime(publish_time, '%Y.%m.%d %H:%M:%S')
except Exception as e:
print(f"[NewsStore] 无法解析时间 '{publish_time}': {e}")
continue
elif not isinstance(publish_time, datetime):
print(f"[NewsStore] 事件 {event_id} 缺少有效的publish_time")
continue
# 跳过过期数据
if publish_time < expiry_threshold:
continue
# 清理并检查名称有效性
cleaned_name = _clean_text(event_data.get('name', ''))
if not _is_valid_name(cleaned_name):
# 名称无效(乱码),跳过此事件
continue
# 创建事件对象
event = CalendarEvent(
id=event_id,
name=cleaned_name,
name_en=_clean_text(event_data.get('name_en', '')),
country=_clean_text(event_data.get('country', '')),
currency=event_data.get('currency', ''),
importance=event_data.get('importance', 0),
publish_time=publish_time,
forecast=_clean_invalid_value(event_data.get('forecast', '')),
previous=_clean_invalid_value(event_data.get('previous', '')),
actual=_clean_invalid_value(event_data.get('actual', '')),
unit=event_data.get('unit', ''),
symbols=event_data.get('symbols', []),
event_type=event_data.get('event_type', '')
)
if event_id in existing_ids:
# 更新现有事件
for i, e in enumerate(self._calendar_events):
if e.id == event_id:
self._calendar_events[i] = event
update_count += 1
break
else:
# 添加新事件
self._calendar_events.append(event)
new_count += 1
# 4. 按时间排序
self._calendar_events.sort(key=lambda x: x.publish_time or datetime.min)
total = len(self._calendar_events)
print(f"[NewsStore] MT5财经日历更新: 新增{new_count}条, 更新{update_count}条, 当前共{total}")
return new_count + update_count
def get_calendar(self, date_str: str = None) -> List[Dict]:
"""
获取财经日历
Args:
date_str: 日期,None返回所有
Returns:
事件列表
"""
with self._calendar_lock:
if date_str:
# 过滤指定日期
filtered = [
e for e in self._calendar_events
if e.publish_time and e.publish_time.strftime('%Y-%m-%d') == date_str
]
return [e.to_dict() for e in filtered]
else:
return [e.to_dict() for e in self._calendar_events]
def get_upcoming_events(self, hours: int = 24) -> List[CalendarEvent]:
"""
获取即将发布的重要事件
Args:
hours: 未来多少小时内
Returns:
事件列表
"""
now = datetime.now()
upcoming = []
with self._calendar_lock:
for event in self._calendar_events:
if event.publish_time and event.importance >= 2:
delta = event.publish_time - now
if 0 < delta.total_seconds() <= hours * 3600:
upcoming.append(event)
return sorted(upcoming, key=lambda x: x.publish_time)
def get_event_by_id(self, event_id: str) -> Optional[CalendarEvent]:
"""根据ID获取事件"""
with self._calendar_lock:
for event in self._calendar_events:
if event.id == event_id:
return event
return None
def update_event_result(self, event_id: str, actual: str, result: str, impact: Dict) -> None:
"""更新事件结果"""
with self._calendar_lock:
event = self.get_event_by_id(event_id)
if event:
event.actual = actual
event.result = result
event.impact = impact
event.analyzed = True
print(f"[NewsStore] 更新事件结果: {event.name}, 实际值={actual}, 结果={result}")
def is_event_alerted(self, event_id: str) -> bool:
"""检查事件是否已提醒"""
return event_id in self._alerted_events
def mark_event_alerted(self, event_id: str) -> None:
"""标记事件已提醒"""
self._alerted_events.add(event_id)
def cleanup_expired_events(self) -> int:
"""
清理过期超过6小时的事件
Returns:
清理的事件数量
"""
now = datetime.now()
expiry_threshold = now - timedelta(hours=self.EXPIRY_HOURS)
with self._calendar_lock:
before_count = len(self._calendar_events)
self._calendar_events = [
e for e in self._calendar_events
if e.publish_time and e.publish_time > expiry_threshold
]
removed = before_count - len(self._calendar_events)
if removed > 0:
print(f"[NewsStore] 清理过期事件: {removed}")
return removed
# ==================== 快讯 ====================
def add_flash_news(self, news: FlashNews) -> bool:
"""
添加快讯
Returns:
是否新增(False表示已存在)
"""
with self._news_lock:
# 检查是否已存在
for existing in self._flash_news:
if existing.id == news.id:
return False
self._flash_news.appendleft(news)
print(f"[NewsStore] 新增快讯: {news.id}")
return True
def get_flash_news(self, count: int = 20) -> List[Dict]:
"""获取最新快讯"""
with self._news_lock:
news_list = list(self._flash_news)[:count]
return [n.to_dict() for n in news_list]
def is_news_alerted(self, news_id: str) -> bool:
"""检查快讯是否已提醒"""
return news_id in self._alerted_news
def mark_news_alerted(self, news_id: str) -> None:
"""标记快讯已提醒"""
self._alerted_news.add(news_id)
def update_news_analysis(self, news_id: str, speaker: str, speaker_title: str, impact: Dict) -> None:
"""更新快讯分析结果"""
with self._news_lock:
for news in self._flash_news:
if news.id == news_id:
news.speaker = speaker
news.speaker_title = speaker_title
news.impact = impact
news.analyzed = True
break
# ==================== 统计 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._calendar_lock, self._news_lock:
return {
"calendar_events": len(self._calendar_events),
"flash_news_count": len(self._flash_news),
"alerted_events": len(self._alerted_events),
"alerted_news": len(self._alerted_news)
}
def clear(self) -> None:
"""清空所有数据"""
with self._calendar_lock, self._news_lock:
self._calendar_events.clear()
self._flash_news.clear()
self._alerted_events.clear()
self._alerted_news.clear()
print("[NewsStore] 已清空所有数据")
# 全局单例
_news_store = None
def get_news_store() -> NewsStore:
"""获取新闻存储单例"""
global _news_store
if _news_store is None:
_news_store = NewsStore()
return _news_store
-457
View File
@@ -1,457 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点检测模块
识别K线的高点和低点(分型识别)
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional, Tuple
import threading
from .store import KlineData
class PivotPoint:
"""转折点数据结构"""
def __init__(self, symbol: str, period: str, timestamp, price: float,
direction: str, strength: int = 3):
self.symbol = symbol
self.period = period
self.timestamp = timestamp
self.price = price
self.direction = direction # "high" 或 "low"
self.strength = strength # 转折强度(左右各N根K线)
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"price": self.price,
"direction": self.direction,
"strength": self.strength
}
class PivotDetector:
"""转折点检测器"""
# 各周期接近阈值(千分比)
THRESHOLDS = {
'H4': 0.0015, # 千分之1.5
'H1': 0.0015, # 千分之1.5
'M15': 0.0015, # 千分之1.5
'M5': 0.0005, # 千分之0.5
'M1': 0.0002 # 千分之0.2
}
# 各周期转折强度(左右各N根K线)
# M1: 6根K线, M5: 4根K线, M15/H1/H4: 3根K线
PERIOD_STRENGTH = {
'M1': 6,
'M5': 4,
'M15': 3,
'H1': 3,
'H4': 3
}
def __init__(self):
# 存储转折点: {SYMBOL: {PERIOD: [PivotPoint, ...]}}
# 这是合并后的转折点,用于价格接近检测
self._pivots = defaultdict(lambda: defaultdict(list))
# 转折点时间线: {SYMBOL: {PERIOD: [PivotPoint, ...]}}
# 这是合并前的原始转折点,按时间排序,用于判断趋势方向
self._pivots_timeline = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 默认转折强度(左右各N根K线)- 仅作为后备值
self.default_strength = 3
print("[PivotDetector] 转折点检测器已初始化")
print(f"[PivotDetector] 周期强度配置: {self.PERIOD_STRENGTH}")
def detect_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> List[PivotPoint]:
"""
检测转折点
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
strength: 转折强度(左右各N根K线),None则使用周期默认值
Returns:
检测到的转折点列表
"""
# 优先使用传入的strength,否则使用周期配置的strength
if strength is None:
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
if len(klines) < 2 * strength + 1:
return []
pivots = []
# 遍历K线,检测分型
for i in range(strength, len(klines) - strength):
current = klines[i]
# 检查是否为高点(顶分型)
is_high = True
for j in range(1, strength + 1):
if klines[i - j].high >= current.high or klines[i + j].high >= current.high:
is_high = False
break
if is_high:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.high,
direction="high",
strength=strength
)
pivots.append(pivot)
# 检查是否为低点(底分型)
is_low = True
for j in range(1, strength + 1):
if klines[i - j].low <= current.low or klines[i + j].low <= current.low:
is_low = False
break
if is_low:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.low,
direction="low",
strength=strength
)
pivots.append(pivot)
return pivots
def _merge_pivots(self, pivots: List[PivotPoint], klines: List[KlineData]) -> List[PivotPoint]:
"""
合并相近的转折点
合并规则:
- 相邻两个同方向转折点
- 价格相差在万分之四范围内
- 高点合并:取较高的价格
- 低点合并:取较低的价格
Args:
pivots: 原始转折点列表
klines: K线数据(用于计算K线索引)
Returns:
合并后的转折点列表
"""
if len(pivots) < 2:
return pivots
# 分开处理高点和低点
high_pivots = [p for p in pivots if p.direction == "high"]
low_pivots = [p for p in pivots if p.direction == "low"]
# 合并高点
merged_highs = self._merge_same_direction(high_pivots, "high")
# 合并低点
merged_lows = self._merge_same_direction(low_pivots, "low")
# 合并结果
result = merged_highs + merged_lows
return result
def _merge_same_direction(self, pivots: List[PivotPoint], direction: str) -> List[PivotPoint]:
"""
合并同方向的转折点
合并规则:相邻两个转折点价格差距小于万分之四时合并
"""
if len(pivots) < 2:
return pivots
# 按时间排序
pivots = sorted(pivots, key=lambda p: str(p.timestamp))
merged = []
i = 0
while i < len(pivots):
current = pivots[i]
# 查找需要合并的转折点
group = [current]
j = i + 1
while j < len(pivots):
next_pivot = pivots[j]
# 检查价格差距(万分之四)
if current.price > 0:
price_diff_pct = abs(next_pivot.price - current.price) / current.price
if price_diff_pct <= 0.0004: # 万分之四
group.append(next_pivot)
j += 1
continue
break
# 从组中选择代表性转折点
if direction == "high":
# 高点:取价格最高的
best = max(group, key=lambda p: p.price)
else:
# 低点:取价格最低的
best = min(group, key=lambda p: p.price)
merged.append(best)
i = j
return merged
def update_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> int:
"""
更新转折点数据
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
strength: 转折强度,None则使用周期默认值
Returns:
更新后的转折点数量
"""
# 使用周期配置的strength
if strength is None:
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
pivots = self.detect_pivots(symbol, period, klines, strength)
with self._lock:
# 保存原始转折点到时间线(按时间排序,用于判断趋势)
# 高点和低点混合在一起,按时间戳排序
timeline = sorted(pivots, key=lambda p: self._normalize_timestamp(p.timestamp))
self._pivots_timeline[symbol][period] = timeline
# 合并相近的转折点(用于价格接近检测)
merged_pivots = self._merge_pivots(pivots, klines)
self._pivots[symbol][period] = merged_pivots
count = len(merged_pivots)
original_count = len(pivots)
timeline_count = len(timeline)
if original_count != count:
print(f"[PivotDetector] {symbol} {period} 检测到 {original_count} 个转折点,时间线 {timeline_count} 个,合并后 {count}")
else:
print(f"[PivotDetector] {symbol} {period} 检测到 {count} 个转折点")
return count
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳为字符串,用于排序比较"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts)
def get_pivots(self, symbol: str, period: str, direction: str = None,
count: int = 50) -> List[Dict]:
"""
获取转折点数据
Args:
symbol: 交易品种
period: 周期
direction: "high""low"None表示全部
count: 返回数量
Returns:
转折点列表
"""
with self._lock:
pivots = self._pivots[symbol][period]
if direction:
pivots = [p for p in pivots if p.direction == direction]
# 按时间排序,返回最新的
pivots = sorted(pivots, key=lambda x: str(x.timestamp), reverse=True)[:count]
return [p.to_dict() for p in pivots]
def get_recent_pivots(self, symbol: str, period: str, count: int = 10) -> List[Dict]:
"""获取最近的转折点(按时间倒序)"""
with self._lock:
pivots = self._pivots[symbol][period]
pivots = sorted(pivots, key=lambda x: str(x.timestamp), reverse=True)[:count]
return [p.to_dict() for p in pivots]
def check_near_pivot(self, symbol: str, current_price: float,
trend_filter: Dict[str, str] = None) -> List[Dict]:
"""
检查当前价格是否接近某个转折点
Args:
symbol: 交易品种
current_price: 当前价格
trend_filter: 趋势过滤,格式 {period: "up"/"down"}
- "up": 趋势向上,只检查高点
- "down": 趋势向下,只检查低点
- 不提供或"unknown": 检查所有
Returns:
接近的转折点列表,包含距离信息
预警逻辑:
- 接近高点:当前价格 < 高点价格 且 距离在阈值范围内
- 接近低点:当前价格 > 低点价格 且 距离在阈值范围内
"""
near_pivots = []
with self._lock:
for period in self._pivots[symbol]:
pivots = self._pivots[symbol][period]
threshold = self.THRESHOLDS.get(period, 0.001)
# 获取该周期的趋势方向
trend = trend_filter.get(period) if trend_filter else None
for pivot in pivots:
if pivot.price == 0 or current_price == 0:
continue
# 根据趋势过滤
if trend == 'up' and pivot.direction != 'high':
# 趋势向上,只检查高点
continue
elif trend == 'down' and pivot.direction != 'low':
# 趋势向下,只检查低点
continue
is_near = False
alert_type = ""
if pivot.direction == "high":
# 高点转折:当前价格低于高点
if current_price < pivot.price:
distance_pct = (pivot.price - current_price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_high"
elif pivot.direction == "low":
# 低点转折:当前价格高于低点
if current_price > pivot.price:
distance_pct = (current_price - pivot.price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_low"
if is_near:
distance_pct = abs(current_price - pivot.price) / current_price
near_pivots.append({
**pivot.to_dict(),
"current_price": current_price,
"distance_pct": round(distance_pct * 100, 4),
"threshold_pct": round(threshold * 100, 4),
"distance": round(current_price - pivot.price, 2),
"alert_type": alert_type,
"trend": trend # 记录趋势方向
})
# 按距离排序,最近的优先
near_pivots.sort(key=lambda x: x['distance_pct'])
return near_pivots
def get_threshold(self, period: str) -> float:
"""获取某个周期的接近阈值"""
return self.THRESHOLDS.get(period, 0.001)
def get_trend_direction(self, symbol: str, period: str = None) -> Dict[str, str]:
"""
根据最近的转折点判断趋势方向
原理:
- 最近是高点 → 价格刚从高点下来 → 趋势向下 → 应检查低点
- 最近是低点 → 价格刚从低点上去 → 趋势向上 → 应检查高点
Args:
symbol: 交易品种
period: 指定周期,如果为None则判断所有周期
Returns:
{period: "up"/"down"/"unknown"}
- up: 趋势向上,应检查高点
- down: 趋势向下,应检查低点
"""
result = {}
periods_to_check = [period] if period else list(self._pivots_timeline[symbol].keys())
with self._lock:
for p in periods_to_check:
timeline = self._pivots_timeline[symbol][p]
if not timeline:
result[p] = 'unknown'
continue
# 时间线已按时间排序,最后一个就是最近的转折点
latest_pivot = timeline[-1]
if latest_pivot.direction == 'high':
# 最近是高点,价格往下走,趋势向下
result[p] = 'down'
else:
# 最近是低点,价格往上走,趋势向上
result[p] = 'up'
return result
def clear_symbol(self, symbol: str):
"""清除某个Symbol的转折点数据"""
with self._lock:
if symbol in self._pivots:
del self._pivots[symbol]
if symbol in self._pivots_timeline:
del self._pivots_timeline[symbol]
def get_status(self) -> Dict:
"""获取状态"""
with self._lock:
status = {}
for symbol in self._pivots:
status[symbol] = {}
for period in self._pivots[symbol]:
count = len(self._pivots[symbol][period])
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
status[symbol][period] = {
"pivot_count": count,
"strength": strength
}
return status
def get_strength(self, period: str) -> int:
"""获取某个周期的转折强度"""
return self.PERIOD_STRENGTH.get(period, self.default_strength)
-198
View File
@@ -1,198 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓数据存储模块
接收和存储EA上报的持仓数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
import json
class PositionData:
"""持仓数据结构"""
def __init__(self, ticket: int, symbol: str, volume: float, price_open: float,
position_type: str, profit: float, distance_sl: float = 0,
distance_tp: float = 0, sl: float = 0, tp: float = 0):
self.ticket = ticket
self.symbol = symbol
self.volume = volume
self.price_open = price_open
self.type = position_type # "BUY" or "SELL"
self.profit = profit
self.distance_sl = distance_sl
self.distance_tp = distance_tp
self.sl = sl
self.tp = tp
self.updated_at = datetime.now()
def to_dict(self) -> Dict:
"""转换为字典"""
return {
"ticket": self.ticket,
"symbol": self.symbol,
"volume": self.volume,
"price_open": self.price_open,
"type": self.type,
"profit": self.profit,
"distance_sl": self.distance_sl,
"distance_tp": self.distance_tp,
"sl": self.sl,
"tp": self.tp,
"updated_at": self.updated_at.isoformat()
}
class PositionStore:
"""持仓数据存储"""
def __init__(self):
# 存储结构: {SYMBOL: {TICKET: PositionData}}
self._positions = defaultdict(dict)
self._lock = threading.RLock()
# 最后更新时间
self._last_update_time = {}
print("[PositionStore] 持仓存储已初始化")
def update_positions(self, symbol: str, positions: List[Dict]) -> Dict:
"""
更新持仓数据
Args:
symbol: 交易品种(上报的品种)
positions: 持仓列表,每个持仓包含symbol字段
Returns:
{"status": "ok", "count": N}
"""
with self._lock:
# 上报的品种
report_symbol = symbol
# 获取当前品种的持仓ticket列表
current_tickets = set(self._positions[report_symbol].keys())
new_tickets = set()
total_count = 0
total_closed = 0
for pos in positions:
pos_symbol = pos.get('symbol', symbol)
ticket = pos.get('ticket')
if not ticket:
continue
new_tickets.add(ticket)
position = PositionData(
ticket=ticket,
symbol=pos_symbol,
volume=pos.get('volume', 0),
price_open=pos.get('priceOpen', 0),
position_type=pos.get('type', 'BUY'),
profit=pos.get('profit', 0),
distance_sl=pos.get('distanceSL', 0),
distance_tp=pos.get('distanceTP', 0),
sl=pos.get('sl', 0),
tp=pos.get('tp', 0)
)
self._positions[pos_symbol][ticket] = position
# 删除已平仓的持仓(当前品种)
closed_tickets = current_tickets - new_tickets
for ticket in closed_tickets:
del self._positions[report_symbol][ticket]
# 更新最后更新时间
self._last_update_time[report_symbol] = datetime.now()
total_count = len(self._positions[report_symbol])
total_closed = len(closed_tickets)
print(f"[PositionStore] 更新持仓: {report_symbol}, {total_count} 个持仓, 平仓 {total_closed}")
return {"status": "ok", "count": total_count, "closed": total_closed}
def get_positions(self, symbol: str = None) -> List[Dict]:
"""
获取持仓数据
Args:
symbol: 交易品种,None表示获取所有
Returns:
持仓列表
"""
with self._lock:
if symbol:
positions = list(self._positions[symbol].values())
else:
positions = []
for sym in self._positions:
positions.extend(self._positions[sym].values())
return [p.to_dict() for p in positions]
def get_position(self, symbol: str, ticket: int) -> Optional[Dict]:
"""获取单个持仓"""
with self._lock:
pos = self._positions[symbol].get(ticket)
return pos.to_dict() if pos else None
def get_summary(self, symbol: str = None) -> Dict:
"""
获取持仓汇总
Returns:
{
"total_count": 总持仓数,
"total_profit": 总盈亏,
"buy_count": 买单数,
"sell_count": 卖单数,
"positions": [...]
}
"""
positions = self.get_positions(symbol)
total_profit = sum(p['profit'] for p in positions)
buy_count = sum(1 for p in positions if p['type'] == 'BUY')
sell_count = sum(1 for p in positions if p['type'] == 'SELL')
return {
"total_count": len(positions),
"total_profit": round(total_profit, 2),
"buy_count": buy_count,
"sell_count": sell_count,
"positions": positions,
"last_update": self._last_update_time.get(symbol, max(self._last_update_time.values()) if self._last_update_time else None)
}
def clear_symbol(self, symbol: str):
"""清除某个品种的持仓数据"""
with self._lock:
if symbol in self._positions:
del self._positions[symbol]
if symbol in self._last_update_time:
del self._last_update_time[symbol]
def get_symbols(self) -> List[str]:
"""获取所有有持仓的品种"""
with self._lock:
return [s for s in self._positions if self._positions[s]]
# 全局单例
_position_store = None
def get_position_store() -> PositionStore:
"""获取持仓存储单例"""
global _position_store
if _position_store is None:
_position_store = PositionStore()
return _position_store
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
服务模块
"""
from .kline_service import KlineService
from .pivot_service import PivotService
from .llm_service import LLMService
from .tech_indicators import calculate_ma, calculate_adx, calculate_rsi, calculate_macd, calculate_bollinger_bands
from .tech_service import TechService
from .calendar_service import CalendarService
from .flash_news_service import FlashNewsService
from .pending_order_service import PendingOrderService
from .trading_instruction_service import TradingInstructionService
# 信号服务
from .signal import SignalService, PivotSignalGenerator, KeyLevelSignalGenerator, AIEntrySignalGenerator
# 策略服务
from .strategy import StrategyService, RiskManager
# 统计、持仓、交易历史服务
from .statistics_service import StatisticsService
from .position_service import PositionService
from .trade_history_service import TradeHistoryService
__all__ = [
'KlineService', 'PivotService', 'LLMService', 'TechService',
'CalendarService', 'FlashNewsService',
'PendingOrderService', 'TradingInstructionService',
'SignalService', 'PivotSignalGenerator', 'KeyLevelSignalGenerator', 'AIEntrySignalGenerator',
'StrategyService', 'RiskManager',
'StatisticsService', 'PositionService', 'TradeHistoryService',
'calculate_ma', 'calculate_adx', 'calculate_rsi', 'calculate_macd', 'calculate_bollinger_bands'
]
+160
View File
@@ -0,0 +1,160 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
财经日历服务模块
处理事件影响分析、提醒等业务逻辑
"""
from datetime import datetime, timedelta
from typing import Dict, List, Optional
from ..models import CalendarEvent
from ..store import CalendarStore
from ..event_config import get_high_impact_event_names, DATA_IMPACT_RULES
class CalendarService:
"""财经日历服务(处理业务逻辑)"""
# 提醒时间(发布前多少秒)
REMINDER_SECONDS = 300 # 5分钟
def __init__(self, calendar_store: CalendarStore):
self.store = calendar_store
# 高影响事件名称
self._high_impact_names = get_high_impact_event_names()
print("[CalendarService] 财经日历服务已初始化")
# ==================== 事件查询 ====================
def get_calendar(self, date_str: str = None) -> List[Dict]:
"""获取财经日历"""
return self.store.get_events(date_str)
def get_upcoming_events(self, hours: int = 24) -> List[Dict]:
"""获取即将发布的重要事件"""
events = self.store.get_upcoming_events(hours)
return [e.to_dict() for e in events]
def get_event_by_id(self, event_id: str) -> Optional[CalendarEvent]:
"""根据ID获取事件"""
return self.store.get_event_by_id(event_id)
# ==================== 事件提醒 ====================
def check_upcoming_reminders(self) -> List[CalendarEvent]:
"""
检查即将发布的事件提醒
Returns:
需要提醒的事件列表
"""
now = datetime.now()
reminders = []
events = self.store.get_upcoming_events(hours=1, min_importance=2)
for event in events:
if not event.publish_time:
continue
time_to_publish = (event.publish_time - now).total_seconds()
# 发布前5分钟内
if 0 < time_to_publish <= self.REMINDER_SECONDS:
reminder_key = f"{event.id}_reminder"
if not self.store.is_alerted(reminder_key):
reminders.append(event)
self.store.mark_alerted(reminder_key)
return reminders
# ==================== 影响分析 ====================
def analyze_event_impact(self, event: CalendarEvent) -> Dict:
"""
分析事件对相关品种的影响
Args:
event: 事件对象
Returns:
影响分析结果 {symbol: {direction, reason}}
"""
impact = {}
# 检查是否有结果
if not event.result or not event.actual:
return impact
for symbol in event.symbols:
# 查找影响规则
rules = DATA_IMPACT_RULES.get(symbol, {})
event_rules = rules.get(event.name) or rules.get(event.name_en)
if not event_rules:
continue
direction = event_rules.get(event.result)
reason_key = f"reason_{event.result}"
reason = event_rules.get(reason_key, "")
if direction:
impact[symbol] = {
"direction": direction,
"reason": reason,
"event_name": event.name,
"actual": event.actual,
"forecast": event.forecast,
"result": event.result
}
return impact
def update_event_result(self, event_id: str, actual: str, result: str) -> bool:
"""
更新事件结果并分析影响
Args:
event_id: 事件ID
actual: 实际值
result: 结果类型 (better/worse/in_line)
Returns:
是否更新成功
"""
event = self.store.get_event_by_id(event_id)
if not event:
return False
# 分析影响
event.actual = actual
event.result = result
impact = self.analyze_event_impact(event)
# 更新存储
return self.store.update_event_result(event_id, actual, result, impact)
# ==================== 高影响事件判断 ====================
def is_high_impact_event(self, event: CalendarEvent) -> bool:
"""判断是否为高影响事件"""
if event.importance >= 3:
return True
if event.name in self._high_impact_names:
return True
if event.name_en in self._high_impact_names:
return True
return False
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"store_status": self.store.get_status(),
"high_impact_event_names": len(self._high_impact_names)
}
+168
View File
@@ -0,0 +1,168 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快讯服务模块
处理快讯影响分析等业务逻辑
"""
from typing import Dict, List, Optional
from ..models import FlashNews
from ..store import FlashNewsStore
from ..event_config import KEY_SPEAKERS, KEY_EVENTS, WATCH_SYMBOLS
class FlashNewsService:
"""快讯服务(处理业务逻辑)"""
def __init__(self, flash_news_store: FlashNewsStore):
self.store = flash_news_store
print("[FlashNewsService] 快讯服务已初始化")
# ==================== 快讯查询 ====================
def get_recent_news(self, count: int = 20) -> List[Dict]:
"""获取最近快讯"""
return self.store.get_news(count)
def get_news_by_id(self, news_id: str) -> Optional[FlashNews]:
"""根据ID获取快讯"""
return self.store.get_news_by_id(news_id)
# ==================== 影响分析 ====================
def analyze_news_impact(self, news: FlashNews) -> Dict:
"""
分析快讯对市场的影响
Args:
news: 快讯对象
Returns:
{
"speaker": 发言人名称,
"speaker_title": 发言人职位,
"impact": {symbol: direction},
"topics": [相关话题]
}
"""
result = {
"speaker": "",
"speaker_title": "",
"impact": {},
"topics": []
}
content = news.content.lower()
# 1. 检查关键人物讲话
for speaker_config in KEY_SPEAKERS:
keywords = speaker_config.get('keywords', [])
matched = False
for keyword in keywords:
if keyword.lower() in content:
matched = True
break
if matched:
result["speaker"] = speaker_config['name']
result["speaker_title"] = speaker_config['title']
# 分析关注话题
watch_topics = speaker_config.get('watch_topics', [])
impact_symbols = speaker_config.get('impact_symbols', [])
default_impact = speaker_config.get('default_impact', {})
for topic in watch_topics:
if topic in content:
result["topics"].append(topic)
# 应用默认影响
for symbol in impact_symbols:
if symbol in default_impact:
topic_impact = default_impact[symbol]
if topic in topic_impact:
result["impact"][symbol] = {
"direction": topic_impact[topic],
"reason": f"{speaker_config['name']}提及{topic}"
}
break
# 2. 检查关键事件
for event_config in KEY_EVENTS:
watch_keywords = event_config.get('watch_keywords', [])
matched = False
for keyword in watch_keywords:
if keyword.lower() in content:
matched = True
break
if matched:
for symbol in event_config.get('symbols', []):
if symbol not in result["impact"]:
result["impact"][symbol] = {
"direction": "不确定",
"reason": f"{event_config['name']}相关新闻"
}
break
return result
def process_news(self, news: FlashNews) -> Optional[Dict]:
"""
处理快讯(分析影响并存储)
Args:
news: 快讯对象
Returns:
如果有影响则返回分析结果,否则返回None
"""
# 分析影响
analysis = self.analyze_news_impact(news)
# 只处理有影响的快讯
if not analysis["impact"] and not analysis["speaker"]:
return None
# 更新快讯对象
news.speaker = analysis["speaker"]
news.speaker_title = analysis["speaker_title"]
news.impact = analysis["impact"]
news.analyzed = True
news.importance = 2 if analysis["speaker"] else 1
# 添加到存储
self.store.add_news(news)
return analysis
# ==================== 提醒状态 ====================
def should_alert(self, news_id: str) -> bool:
"""
判断是否应该推送提醒
Args:
news_id: 快讯ID
Returns:
是否应该提醒
"""
if self.store.is_alerted(news_id):
return False
self.store.mark_alerted(news_id)
return True
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"store_status": self.store.get_status()
}
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线服务模块
处理K线相关的业务逻辑:时效性检查、连续性检查、格式转换等
"""
from datetime import datetime, timedelta
from typing import List, Dict, Optional
from ..store import KlineStore
from ..models import KlineData
class KlineService:
"""K线服务(处理业务逻辑)"""
def __init__(self, store: KlineStore):
self.store = store
def process_kline_data(self, symbol: str, period: str, klines: List[Dict],
is_full: bool = False) -> Dict:
"""
处理K线数据(包含业务逻辑)
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
is_full: 是否为全量数据
Returns:
处理结果
"""
period = period.upper()
# 保存数据
result = self.store.save_klines(symbol, period, klines, is_full)
return result
def check_staleness(self, symbol: str, period: str, klines: List[Dict],
timezone_offset_hours: float = 0) -> Dict:
"""
检查K线时效性
Args:
symbol: 交易品种
period: 周期
klines: K线数据
timezone_offset_hours: 时区偏移
Returns:
{
"is_stale": bool,
"latest_kline_time": datetime,
"kline_time_local": datetime,
"time_diff_seconds": int
}
"""
if not klines:
return {"is_stale": False, "message": "无数据"}
period_interval = self.store.get_period_interval(period)
latest_kline = klines[-1] if klines else None
if not latest_kline:
return {"is_stale": False}
ts = latest_kline.get('timestamp') or latest_kline.get('time')
latest_kline_time = self._parse_timestamp(ts)
if not latest_kline_time:
return {"is_stale": False}
now_local = datetime.now()
kline_time_local = latest_kline_time - timedelta(hours=timezone_offset_hours)
time_diff = (now_local - kline_time_local).total_seconds()
return {
"is_stale": time_diff > period_interval,
"latest_kline_time": latest_kline_time,
"kline_time_local": kline_time_local,
"time_diff_seconds": int(time_diff),
"period_interval": period_interval
}
def check_continuity(self, symbol: str, period: str, new_klines: List[Dict]) -> Dict:
"""
检查增量K线数据是否连续
Args:
symbol: 品种名称
period: 周期
new_klines: 新推送的K线数据列表
Returns:
{
"is_continuous": bool,
"gap_count": int,
"last_existing_time": datetime,
"first_new_time": datetime
}
"""
period = period.upper()
if not new_klines:
return {"is_continuous": True, "gap_count": 0}
interval = self.store.get_period_interval(period)
existing = self.store.get_all_klines(symbol, period)
if not existing:
return {"is_continuous": True, "gap_count": 0}
# 获取现有数据最后时间
last_existing_time = self._parse_timestamp(
existing[-1].get('timestamp') or existing[-1].get('time')
)
if last_existing_time is None:
return {"is_continuous": True, "gap_count": 0}
# 获取新数据最早时间
first_new_time = None
for k in new_klines:
ts = self._parse_timestamp(k.get('timestamp') or k.get('time'))
if ts:
if first_new_time is None or ts < first_new_time:
first_new_time = ts
if first_new_time is None:
return {"is_continuous": True, "gap_count": 0}
# 计算时间差
time_diff = (first_new_time - last_existing_time).total_seconds()
if time_diff <= 0:
return {
"is_continuous": True,
"gap_count": 0,
"last_existing_time": last_existing_time,
"first_new_time": first_new_time
}
gap_periods = int(time_diff / interval)
return {
"is_continuous": gap_periods <= 1,
"gap_count": max(0, gap_periods - 1),
"last_existing_time": last_existing_time,
"first_new_time": first_new_time,
"expected_gap": gap_periods
}
def convert_to_kline_objects(self, klines: List[Dict], symbol: str, period: str) -> List[KlineData]:
"""
将K线字典列表转换为KlineData对象列表
Args:
klines: K线字典列表
symbol: 品种
period: 周期
Returns:
KlineData对象列表
"""
return [
KlineData(
symbol=symbol,
period=period,
timestamp=k.get('timestamp') or k.get('time'),
open_price=float(k.get('open', 0)),
high=float(k.get('high', 0)),
low=float(k.get('low', 0)),
close=float(k.get('close', 0)),
volume=float(k.get('volume', 0))
)
for k in klines
]
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
"""获取K线数据"""
return self.store.get_klines(symbol, period, count)
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
"""获取所有K线数据"""
return self.store.get_all_klines(symbol, period)
def get_all_kline_objects(self, symbol: str, period: str) -> List[KlineData]:
"""获取所有K线数据并转换为KlineData对象"""
klines = self.store.get_all_klines(symbol, period)
return self.convert_to_kline_objects(klines, symbol, period)
def get_latest_price(self, symbol: str) -> Optional[float]:
"""获取最新价格"""
return self.store.get_latest_price(symbol)
def is_initialized(self, symbol: str, period: str) -> bool:
"""检查是否已初始化"""
return self.store.is_initialized(symbol, period)
def get_symbols(self) -> List[str]:
"""获取所有品种"""
return self.store.get_symbols()
def get_status(self) -> Dict:
"""获取状态"""
return self.store.get_status()
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
"""检查M1数据更新情况"""
return self.store.check_m1_updated_within(symbol, seconds)
def get_period_interval(self, period: str) -> int:
"""获取周期时间间隔"""
return self.store.get_period_interval(period)
def check_symbols_status(self, symbols: List[str], stale_threshold: int = 180) -> Dict[str, List[str]]:
"""
检查多个品种的数据更新状态
Args:
symbols: 品种列表
stale_threshold: 过期阈值(秒),默认180秒(3分钟)
Returns:
{"active": [...], "stale": [...], "closed": [...]}
- active: 指定秒数内有数据更新
- stale: 超过指定秒数未更新
- closed: 无数据
"""
result = {"active": [], "stale": [], "closed": []}
for symbol in symbols:
m1_status = self.store.check_m1_updated_within(symbol, stale_threshold)
market_status = m1_status.get("market_status", "closed")
if market_status == "active":
result["active"].append(symbol)
elif market_status == "stale":
result["stale"].append(symbol)
else:
result["closed"].append(symbol)
return result
def _parse_timestamp(self, ts) -> Optional[datetime]:
"""解析时间戳"""
if ts is None:
return None
if isinstance(ts, datetime):
return ts
ts_str = str(ts)
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
return datetime.strptime(ts_str, fmt)
except:
continue
return None
+453
View File
@@ -0,0 +1,453 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 服务模块
处理 LLM 分析相关的业务逻辑
"""
import os
import json
import requests
from datetime import datetime
from typing import Dict, List, Optional
from ..models import LLMConfig, LLMAnalysisResult
from ..store import LLMStore
from .kline_service import KlineService
class LLMService:
"""LLM 服务(处理业务逻辑)"""
# 分析间隔(秒)
ANALYZE_INTERVAL = 300 # 5分钟
# 各周期K线数量限制
KLINE_LIMITS = {
'H4': 20,
'H1': 24,
'M15': 32,
'M5': 48,
'M1': 60
}
# 数据过期阈值(秒)
STALE_THRESHOLD = 180 # 3分钟
def __init__(self, llm_store: LLMStore, kline_service: KlineService):
self.llm_store = llm_store
self.kline_service = kline_service
# 从环境变量补充配置
self._load_env_config()
print("[LLMService] LLM服务已初始化")
def _load_env_config(self):
"""从环境变量加载配置"""
config = self.llm_store.get_config()
if not config.api_key and os.environ.get("LLM_API_KEY"):
self.llm_store.update_config(api_key=os.environ.get("LLM_API_KEY"))
if os.environ.get("LLM_API_BASE"):
self.llm_store.update_config(api_base=os.environ.get("LLM_API_BASE"))
if os.environ.get("LLM_MODEL"):
self.llm_store.update_config(model=os.environ.get("LLM_MODEL"))
# ==================== 配置管理 ====================
def get_config(self) -> Dict:
"""获取配置"""
return self.llm_store.get_config().to_dict()
def configure(self, api_key: str = None, api_base: str = None, model: str = None) -> Dict:
"""配置 LLM 参数"""
config = self.llm_store.update_config(api_key, api_base, model)
return {
"status": "ok",
"enabled": config.enabled,
"model": config.model,
"api_base": config.api_base
}
def is_enabled(self) -> bool:
"""是否启用"""
return self.llm_store.get_config().enabled
# ==================== 数据收集 ====================
def collect_klines_for_analysis(self, symbols: List[str]) -> Dict[str, Dict]:
"""
收集指定品种的K线数据用于分析
Returns:
{symbol: {period: [klines]}}
"""
all_klines = {}
for symbol in symbols:
klines_data = {}
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
limit = self.KLINE_LIMITS.get(period, 30)
klines = self.kline_service.get_klines(symbol, period, limit)
if klines:
klines_data[period] = klines
if klines_data:
all_klines[symbol] = klines_data
return all_klines
# ==================== Prompt 构建 ====================
def build_analysis_prompt(self, all_klines: Dict[str, Dict]) -> str:
"""构建分析提示词"""
prompt = """你是一位专业的金融分析师。请分析以下多个交易品种的K线数据,给出每个品种的趋势判断和交易建议。
## 分析要求
对于每个品种,请分析:
1. 各周期(H4、H1、M15、M5、M1)的趋势判断,包含趋势类型、置信度(0-100)和判断理由
2. 整体趋势方向、强度(0-100)和总结
3. 关键支撑位和压力位(请根据K线数据自行判断,各列出3个)
4. 交易建议:必须包含M1、M5、M15三个周期的具体交易建议
趋势类型可选值:单边上涨、单边下跌、区间震荡、震荡上升、震荡下跌、震荡收窄、震荡扩大
请按以下JSON格式输出(必须是有效的JSON格式,包含所有品种):
```json
{
"品种1": {
"trend_analysis": {
"H4": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"H1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M15": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M5": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"},
"M1": {"trend": "趋势类型", "confidence": 置信度, "reason": "判断理由"}
},
"overall_trend": {
"direction": "整体趋势方向",
"strength": 强度,
"summary": "整体趋势总结"
},
"key_levels": {
"resistance": [压力位1, 压力位2, 压力位3],
"support": [支撑位1, 支撑位2, 支撑位3]
},
"trade_suggestions": [
{
"period": "M15",
"direction": "buy或sell",
"entry_price": 入场价格,
"stop_loss": 止损价格,
"take_profit": 止盈价格,
"reason": "交易理由"
}
]
}
}
```
## K线数据
"""
# 添加各品种的K线数据
for symbol, klines_data in all_klines.items():
prompt += f"\n### {symbol}\n"
for period, klines in klines_data.items():
prompt += f"\n#### {period} 周期({len(klines)}根K线)\n"
prompt += "| 时间 | 开盘 | 最高 | 最低 | 收盘 |\n"
prompt += "|------|------|------|------|------|\n"
for k in klines:
prompt += f"| {k['timestamp']} | {k['open']:.2f} | {k['high']:.2f} | {k['low']:.2f} | {k['close']:.2f} |\n"
prompt += """
请确保输出是纯JSON格式,不要有其他文字说明。每个品种的分析结果都要完整,trade_suggestions必须包含M1、M5、M15三个周期的建议。
"""
return prompt
# ==================== LLM API 调用 ====================
def call_llm(self, prompt: str) -> Optional[Dict]:
"""调用 LLM API(非流式)"""
config = self.llm_store.get_config()
if not config.api_key:
return None
try:
headers = {
"Authorization": f"Bearer {config.api_key}",
"Content-Type": "application/json"
}
data = {
"model": config.model,
"messages": [
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 4000
}
response = requests.post(
f"{config.api_base}/chat/completions",
headers=headers,
json=data,
timeout=120
)
if response.status_code == 200:
result = response.json()
content = result["choices"][0]["message"]["content"]
return self._parse_llm_response(content)
else:
print(f"[LLMService] API调用失败: {response.status_code} - {response.text}")
return None
except Exception as e:
print(f"[LLMService] 调用异常: {e}")
return None
def call_llm_stream(self, prompt: str, on_chunk: callable = None) -> Optional[Dict]:
"""
调用 LLM API(流式)
Args:
prompt: 提示词
on_chunk: 回调函数,参数为 (chunk_count, full_content)
"""
config = self.llm_store.get_config()
if not config.api_key:
return None
try:
headers = {
"Authorization": f"Bearer {config.api_key}",
"Content-Type": "application/json"
}
data = {
"model": config.model,
"messages": [
{"role": "system", "content": "你是一位专业的金融分析师,擅长技术分析和趋势判断。请用JSON格式输出分析结果,不要有任何额外的文字说明。"},
{"role": "user", "content": prompt}
],
"temperature": 0.3,
"max_tokens": 4000,
"stream": True
}
response = requests.post(
f"{config.api_base}/chat/completions",
headers=headers,
json=data,
timeout=120,
stream=True
)
if response.status_code != 200:
print(f"[LLMService] API调用失败: {response.status_code} - {response.text}")
return None
# 收集完整响应
full_content = ""
chunk_count = 0
for line in response.iter_lines():
if not line:
continue
line = line.decode('utf-8')
if line.startswith('data: '):
data_str = line[6:]
if data_str == '[DONE]':
break
try:
chunk_data = json.loads(data_str)
if 'choices' in chunk_data and len(chunk_data['choices']) > 0:
delta = chunk_data['choices'][0].get('delta', {})
content_piece = delta.get('content', '')
if content_piece:
full_content += content_piece
chunk_count += 1
if on_chunk:
on_chunk(chunk_count, full_content)
except json.JSONDecodeError:
continue
print(f"[LLMService] 流式接收完成,共 {chunk_count} 个chunk{len(full_content)} 字符")
return self._parse_llm_response(full_content)
except Exception as e:
print(f"[LLMService] 流式调用异常: {e}")
return None
def _parse_llm_response(self, content: str) -> Optional[Dict]:
"""解析 LLM 响应"""
try:
# 提取JSON部分
if "```json" in content:
content = content.split("```json")[1].split("```")[0]
elif "```" in content:
content = content.split("```")[1].split("```")[0]
return json.loads(content.strip())
except json.JSONDecodeError as e:
print(f"[LLMService] JSON解析失败: {e}")
return None
# ==================== 入场价检测 ====================
def check_entry_price_nearby(self, symbol: str, current_price: float,
threshold: float = 0.0001) -> List[Dict]:
"""
检查当前价格是否接近 AI 建议的入场价
Args:
symbol: 交易品种
current_price: 当前价格
threshold: 价格接近阈值,默认万分之一
Returns:
匹配的交易建议列表
"""
matched = []
result = self.llm_store.get_analysis_result(symbol)
if not result or not result.trade_suggestions:
return matched
for suggestion in result.trade_suggestions:
entry_price = suggestion.get('entry_price')
period = suggestion.get('period')
direction = suggestion.get('direction')
stop_loss = suggestion.get('stop_loss')
take_profit = suggestion.get('take_profit')
if not entry_price or entry_price <= 0:
continue
# 验证止损止盈
if not stop_loss or not take_profit or stop_loss <= 0 or take_profit <= 0:
print(f"[LLMService] 跳过无效建议: {period} sl={stop_loss}, tp={take_profit}")
continue
price_diff_pct = abs(current_price - entry_price) / entry_price
if price_diff_pct <= threshold:
# 检查冷却
can_alert = self.llm_store.check_entry_alert_cooldown(
symbol, period, direction, entry_price
)
if can_alert:
matched.append({
"symbol": symbol,
"period": period,
"direction": direction,
"entry_price": entry_price,
"current_price": current_price,
"price_diff_pct": round(price_diff_pct * 100, 4),
"stop_loss": stop_loss,
"take_profit": take_profit,
"reason": suggestion.get('reason'),
"analyzed_at": result.analyzed_at
})
print(f"[LLMService] 价格接近AI入场价: {symbol} {period} "
f"入场价 {entry_price:.2f}, 当前价 {current_price:.2f}")
# 清理过期记录
self.llm_store.cleanup_entry_alerts()
return matched
# ==================== 分析执行 ====================
def run_analysis(self, on_status: callable = None, on_complete: callable = None) -> Dict:
"""
执行分析
Args:
on_status: 状态回调
on_complete: 完成回调
Returns:
分析结果
"""
if not self.is_enabled():
return {"status": "error", "message": "LLM 未启用"}
# 获取品种列表
symbols = self.kline_service.get_symbols()
if not symbols:
if on_status:
on_status("error", "没有品种数据")
return {"status": "error", "message": "没有品种数据"}
if on_status:
on_status("analyzing", f"正在检查 {len(symbols)} 个品种...")
# 检查数据状态
status = self.kline_service.check_symbols_status(symbols, self.STALE_THRESHOLD)
active_symbols = status["active"]
# 更新过期和休市品种状态
for symbol in status["stale"]:
self.llm_store.update_market_status(symbol, "stale", data_stale=True)
for symbol in status["closed"]:
self.llm_store.update_market_status(symbol, "closed", data_stale=True)
if not active_symbols:
if on_status:
on_status("stale", "所有品种数据均未更新")
return {"status": "ok", "message": "所有品种数据均未更新"}
if on_status:
on_status("analyzing", f"正在分析 {len(active_symbols)} 个品种...")
# 收集K线数据
all_klines = self.collect_klines_for_analysis(active_symbols)
if not all_klines:
if on_status:
on_status("error", "无K线数据可分析")
return {"status": "error", "message": "无K线数据可分析"}
# 构建提示词
prompt = self.build_analysis_prompt(all_klines)
# 调用 LLM
def on_chunk(count, content):
if on_status and count % 50 == 0:
on_status("streaming", f"正在接收分析结果... ({len(content)} 字符)")
response = self.call_llm_stream(prompt, on_chunk)
# 保存结果
if response:
for symbol, analysis in response.items():
if isinstance(analysis, dict):
self.llm_store.save_analysis_dict(symbol, analysis)
if on_complete:
on_complete(response)
return {
"status": "ok",
"analyzed_symbols": list(response.keys()) if response else []
}
# ==================== 查询 ====================
def get_analysis(self, symbol: str = None) -> Dict:
"""获取分析结果"""
return self.llm_store.get_analysis(symbol)
def get_status(self) -> Dict:
"""获取状态"""
return self.llm_store.get_status()
+175
View File
@@ -0,0 +1,175 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
待确认订单服务模块
"""
from typing import List, Dict, Optional, Callable
from datetime import datetime
import threading
from ..models import PendingOrder
from ..store import PendingOrderStore
class PendingOrderService:
"""待确认订单服务(处理业务逻辑)"""
def __init__(self, pending_order_store: PendingOrderStore = None):
self.store = pending_order_store or PendingOrderStore()
# 订单确认回调(确认后将指令加入交易队列)
self._confirm_callback: Optional[Callable] = None
# 启动超时清理线程
self._start_cleanup_thread()
print("[PendingOrderService] 待确认订单服务已初始化")
def set_confirm_callback(self, callback: Callable):
"""
设置订单确认回调函数
回调签名: callback(order: PendingOrder) -> None
"""
self._confirm_callback = callback
def _start_cleanup_thread(self):
"""启动超时清理线程"""
def cleanup_loop():
while True:
try:
expired = self.store.cleanup_expired()
for order in expired:
print(f"[PendingOrderService] 订单超时自动移除: {order.order_id}")
except Exception as e:
print(f"[PendingOrderService] 清理线程异常: {e}")
threading.Event().wait(10) # 每10秒检查一次
thread = threading.Thread(target=cleanup_loop, daemon=True)
thread.start()
# ==================== 创建订单 ====================
def create_order(self, symbol: str, action: str, price: float,
mount: float, sl: float, tp: float,
reason: str = "", description: str = "",
source: str = "", **kwargs) -> str:
"""
创建待确认订单
Args:
symbol: 品种
action: 方向 (b/s)
price: 入场价
mount: 手数
sl: 止损
tp: 止盈
reason: 原因
description: 描述
source: 来源
**kwargs: 其他字段pivot_price, key_level, ai_period等
Returns:
订单ID
"""
order = PendingOrder(
symbol=symbol,
action=action,
price=price,
mount=mount,
sl=sl,
tp=tp,
reason=reason,
description=description,
source=source,
**kwargs
)
return self.store.add_order(order)
def create_order_from_dict(self, data: Dict) -> str:
"""从字典创建订单"""
return self.store.add_order_from_dict(data)
# ==================== 查询订单 ====================
def get_order(self, order_id: str) -> Optional[PendingOrder]:
"""获取订单"""
return self.store.get_order_by_id(order_id)
def get_orders(self, symbol: str = None) -> List[PendingOrder]:
"""获取订单列表"""
return self.store.get_pending_orders(symbol)
def get_orders_dict(self, symbol: str = None) -> List[Dict]:
"""获取订单字典列表"""
return self.store.get_pending_orders_dict(symbol)
# 兼容旧方法名
def get_pending_orders_dict(self, symbol: str = None) -> List[Dict]:
"""获取订单字典列表(兼容旧方法名)"""
return self.get_orders_dict(symbol)
def get_pending_count(self, symbol: str = None) -> int:
"""获取待确认订单数量"""
return self.store.get_pending_count(symbol)
# ==================== 确认/拒绝订单 ====================
def confirm_order(self, order_id: str, updates: Dict = None) -> Optional[PendingOrder]:
"""
确认订单
Args:
order_id: 订单ID
updates: 更新字段 mount, sl, tp
Returns:
确认后的订单
"""
# 先获取订单
order = self.store.get_order_by_id(order_id)
if not order:
return None
# 应用更新
if updates:
if 'mount' in updates:
order.mount = updates['mount']
if 'sl' in updates:
order.sl = updates['sl']
if 'tp' in updates:
order.tp = updates['tp']
# 确认订单(从存储中移除)
confirmed_order = self.store.confirm_order(order_id)
if not confirmed_order:
return None
# 调用确认回调
if self._confirm_callback:
try:
self._confirm_callback(confirmed_order)
except Exception as e:
print(f"[PendingOrderService] 确认回调执行失败: {e}")
return confirmed_order
def reject_order(self, order_id: str) -> Optional[PendingOrder]:
"""拒绝订单"""
return self.store.reject_order(order_id)
# ==================== 清理 ====================
def clear_all(self) -> int:
"""清空所有待确认订单"""
return self.store.clear_all()
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"store": self.store.get_status(),
"callback_set": self._confirm_callback is not None,
}
+322
View File
@@ -0,0 +1,322 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点服务模块
处理转折点相关的业务逻辑检测合并接近检测等
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
from ..models import KlineData, PivotPoint
from ..store import KlineStore, PivotStore
class PivotService:
"""转折点服务(处理业务逻辑)"""
# 各周期接近阈值(千分比)
THRESHOLDS = {
'H4': 0.0015, # 千分之1.5
'H1': 0.0015, # 千分之1.5
'M15': 0.0015, # 千分之1.5
'M5': 0.0005, # 千分之0.5
'M1': 0.0002 # 千分之0.2
}
# 各周期转折强度(左右各N根K线)
PERIOD_STRENGTH = {
'M1': 6,
'M5': 4,
'M15': 3,
'H1': 3,
'H4': 3
}
def __init__(self, pivot_store: PivotStore, kline_store: KlineStore):
self.pivot_store = pivot_store
self.kline_store = kline_store
self.default_strength = 3
print("[PivotService] 转折点服务已初始化")
print(f"[PivotService] 周期强度配置: {self.PERIOD_STRENGTH}")
def detect_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> List[PivotPoint]:
"""
检测转折点
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
strength: 转折强度None则使用周期默认值
Returns:
检测到的转折点列表
"""
if strength is None:
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
if len(klines) < 2 * strength + 1:
return []
pivots = []
for i in range(strength, len(klines) - strength):
current = klines[i]
# 检查是否为高点(顶分型)
is_high = True
for j in range(1, strength + 1):
if klines[i - j].high >= current.high or klines[i + j].high >= current.high:
is_high = False
break
if is_high:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.high,
direction="high",
strength=strength
)
pivots.append(pivot)
# 检查是否为低点(底分型)
is_low = True
for j in range(1, strength + 1):
if klines[i - j].low <= current.low or klines[i + j].low <= current.low:
is_low = False
break
if is_low:
pivot = PivotPoint(
symbol=symbol,
period=period,
timestamp=current.timestamp,
price=current.low,
direction="low",
strength=strength
)
pivots.append(pivot)
return pivots
def merge_pivots(self, pivots: List[PivotPoint]) -> List[PivotPoint]:
"""
合并相近的转折点
合并规则相邻两个同方向转折点价格差距小于万分之四时合并
"""
if len(pivots) < 2:
return pivots
high_pivots = [p for p in pivots if p.direction == "high"]
low_pivots = [p for p in pivots if p.direction == "low"]
merged_highs = self._merge_same_direction(high_pivots, "high")
merged_lows = self._merge_same_direction(low_pivots, "low")
return merged_highs + merged_lows
def _merge_same_direction(self, pivots: List[PivotPoint], direction: str) -> List[PivotPoint]:
"""合并同方向的转折点"""
if len(pivots) < 2:
return pivots
pivots = sorted(pivots, key=lambda p: str(p.timestamp))
merged = []
i = 0
while i < len(pivots):
current = pivots[i]
group = [current]
j = i + 1
while j < len(pivots):
next_pivot = pivots[j]
if current.price > 0:
price_diff_pct = abs(next_pivot.price - current.price) / current.price
if price_diff_pct <= 0.0004:
group.append(next_pivot)
j += 1
continue
break
if direction == "high":
best = max(group, key=lambda p: p.price)
else:
best = min(group, key=lambda p: p.price)
merged.append(best)
i = j
return merged
def update_pivots(self, symbol: str, period: str, klines: List[KlineData],
strength: int = None) -> int:
"""
更新转折点数据
Args:
symbol: 交易品种
period: 周期
klines: K线数据列表
strength: 转折强度
Returns:
更新后的转折点数量
"""
if strength is None:
strength = self.PERIOD_STRENGTH.get(period, self.default_strength)
pivots = self.detect_pivots(symbol, period, klines, strength)
# 保存原始转折点到时间线
timeline = sorted(pivots, key=lambda p: self._normalize_timestamp(p.timestamp))
# 合并相近的转折点
merged_pivots = self.merge_pivots(pivots)
# 存储到 pivot_store
self.pivot_store.save_pivots(symbol, period, merged_pivots, timeline)
original_count = len(pivots)
count = len(merged_pivots)
if original_count != count:
print(f"[PivotService] {symbol} {period} 检测到 {original_count} 个转折点,合并后 {count}")
else:
print(f"[PivotService] {symbol} {period} 检测到 {count} 个转折点")
return count
def check_near_pivot(self, symbol: str, current_price: float,
trend_filter: Dict[str, str] = None) -> List[Dict]:
"""
检查当前价格是否接近某个转折点
Args:
symbol: 交易品种
current_price: 当前价格
trend_filter: 趋势过滤
Returns:
接近的转折点列表
"""
near_pivots = []
periods = self.pivot_store.get_all_periods(symbol)
for period in periods:
pivots = self.pivot_store.get_pivot_objects(symbol, period)
threshold = self.THRESHOLDS.get(period, 0.001)
trend = trend_filter.get(period) if trend_filter else None
for pivot in pivots:
if pivot.price == 0 or current_price == 0:
continue
if trend == 'up' and pivot.direction != 'high':
continue
elif trend == 'down' and pivot.direction != 'low':
continue
is_near = False
alert_type = ""
if pivot.direction == "high":
if current_price < pivot.price:
distance_pct = (pivot.price - current_price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_high"
elif pivot.direction == "low":
if current_price > pivot.price:
distance_pct = (current_price - pivot.price) / current_price
if distance_pct <= threshold:
is_near = True
alert_type = "near_low"
if is_near:
distance_pct = abs(current_price - pivot.price) / current_price
near_pivots.append({
**pivot.to_dict(),
"current_price": current_price,
"distance_pct": round(distance_pct * 100, 4),
"threshold_pct": round(threshold * 100, 4),
"distance": round(current_price - pivot.price, 2),
"alert_type": alert_type,
"trend": trend
})
near_pivots.sort(key=lambda x: x['distance_pct'])
return near_pivots
def get_trend_direction(self, symbol: str, period: str = None) -> Dict[str, str]:
"""
根据最近的转折点判断趋势方向
Returns:
{period: "up"/"down"/"unknown"}
"""
result = {}
periods_to_check = [period] if period else self.pivot_store.get_all_periods(symbol)
for p in periods_to_check:
timeline = self.pivot_store.get_timeline(symbol, p)
if not timeline:
result[p] = 'unknown'
continue
latest_pivot = timeline[-1]
if latest_pivot.direction == 'high':
result[p] = 'down'
else:
result[p] = 'up'
return result
def get_pivots(self, symbol: str, period: str, direction: str = None,
count: int = 50) -> List[Dict]:
"""获取转折点数据"""
return self.pivot_store.get_pivots(symbol, period, direction, count)
def find_nearest_pivot_price(self, symbol: str, direction: str,
current_price: float) -> Optional[float]:
"""找到离当前价格最近的转折点价格"""
return self.pivot_store.find_nearest_pivot_price(symbol, direction, current_price)
def get_threshold(self, period: str) -> float:
"""获取某个周期的接近阈值"""
return self.THRESHOLDS.get(period, 0.001)
def get_strength(self, period: str) -> int:
"""获取某个周期的转折强度"""
return self.PERIOD_STRENGTH.get(period, self.default_strength)
def get_status(self) -> Dict:
"""获取状态"""
return self.pivot_store.get_status()
def clear_symbol(self, symbol: str):
"""清除某个Symbol的转折点数据"""
self.pivot_store.clear_symbol(symbol)
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts)
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓数据服务模块
"""
from typing import Dict, Optional, List
from ..models.position import PositionData
from ..store.position_store import PositionStore
class PositionService:
"""
持仓数据服务
功能
1. 处理EA上报的持仓数据
2. 查询持仓信息
3. 为风险管理提供持仓数据
"""
def __init__(self, store: PositionStore = None):
self.store = store or PositionStore()
def update_positions(self, symbol: str, positions_data: List[Dict]) -> Dict:
"""
更新持仓数据
Args:
symbol: 品种
positions_data: EA上报的持仓数据列表
Returns:
{"status": "ok", "count": N, "closed": M}
"""
positions = [
PositionData.from_ea_data(data, symbol)
for data in positions_data
]
return self.store.update(symbol, positions)
def get_positions(self, symbol: str = None) -> List[Dict]:
"""获取持仓数据(字典格式)"""
return self.store.get_dict(symbol)
def get_position_objects(self, symbol: str = None) -> List[PositionData]:
"""获取持仓数据(对象格式)"""
return self.store.get(symbol)
def get_position(self, symbol: str, ticket: int) -> Optional[Dict]:
"""获取单个持仓"""
pos = self.store.get_by_ticket(symbol, ticket)
return pos.to_dict() if pos else None
def get_position_count(self, symbol: str) -> int:
"""获取持仓数量"""
return self.store.get_count(symbol)
def get_same_direction_count(self, symbol: str, direction: str) -> int:
"""获取同向持仓数量"""
return self.store.get_count_by_direction(symbol, direction)
def get_opposite_direction_count(self, symbol: str, direction: str) -> int:
"""获取反向持仓数量"""
opposite = "sell" if direction.lower() == "buy" else "buy"
return self.store.get_count_by_direction(symbol, opposite)
def get_summary(self, symbol: str = None) -> Dict:
"""获取持仓汇总"""
return self.store.get_summary(symbol)
def get_symbols(self) -> List[str]:
"""获取所有有持仓的品种"""
return self.store.get_symbols()
def get_status(self) -> Dict:
"""获取服务状态"""
return self.store.get_status()
+17
View File
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
信号生成器模块
"""
from .signal_service import SignalService
from .pivot_signal import PivotSignalGenerator
from .key_level_signal import KeyLevelSignalGenerator
from .ai_entry_signal import AIEntrySignalGenerator
__all__ = [
'SignalService',
'PivotSignalGenerator',
'KeyLevelSignalGenerator',
'AIEntrySignalGenerator',
]
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
AI入场信号生成器
根据AI分析生成交易信号
"""
from typing import Optional, List, Dict
from datetime import datetime
from ...models import TradingSignal, SignalSource
class AIEntrySignalGenerator:
"""AI入场信号生成器"""
def __init__(self):
# LLM分析器引用
self._llm_analyzer = None
# 阈值(价格距离AI入场价的百分比)
self.threshold = 0.0001 # 万分之一
# 信号冷却时间(秒)
self.cooldown = 300 # 5分钟
# 冷却记录
self._signal_cooldowns: Dict[str, datetime] = {}
print("[AIEntrySignalGenerator] AI入场信号生成器已初始化")
def set_llm_analyzer(self, analyzer) -> None:
"""设置LLM分析器"""
self._llm_analyzer = analyzer
def _check_cooldown(self, symbol: str, period: str, entry_price: float, direction: str) -> bool:
"""检查是否在冷却期"""
key = f"{symbol}_{period}_{entry_price}_{direction}"
if key in self._signal_cooldowns:
last_time = self._signal_cooldowns[key]
elapsed = (datetime.now() - last_time).total_seconds()
return elapsed < self.cooldown
return False
def _set_cooldown(self, symbol: str, period: str, entry_price: float, direction: str) -> None:
"""设置冷却"""
key = f"{symbol}_{period}_{entry_price}_{direction}"
self._signal_cooldowns[key] = datetime.now()
def generate_signal(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
"""
生成AI入场信号
Args:
symbol: 品种
current_price: 当前价格
Returns:
TradingSignal None
"""
if not self._llm_analyzer:
return None
# 检查价格是否接近AI入场价
matches = self._llm_analyzer.check_entry_price_nearby(
symbol, current_price, threshold=self.threshold
)
if not matches:
return None
# 使用第一个匹配
match = matches[0]
period = match.get('period', '')
entry_price = match.get('entry_price', 0)
direction = match.get('direction', 'buy')
sl = match.get('stop_loss', 0)
tp = match.get('take_profit', 0)
reason = match.get('reason', '')
# 检查冷却
if self._check_cooldown(symbol, period, entry_price, direction):
return None
# 设置冷却
self._set_cooldown(symbol, period, entry_price, direction)
# 确定方向
action = "buy" if direction == "buy" else "sell"
# 验证止损止盈
if not sl or not tp or sl <= 0 or tp <= 0:
print(f"[AIEntrySignalGenerator] 跳过无效信号: sl={sl}, tp={tp}")
return None
# 验证止损方向
if action == "buy" and sl >= current_price:
print(f"[AIEntrySignalGenerator] 买入止损无效: sl={sl} >= price={current_price}")
return None
if action == "sell" and sl <= current_price:
print(f"[AIEntrySignalGenerator] 卖出止损无效: sl={sl} <= price={current_price}")
return None
# 计算风险回报比
risk = abs(current_price - sl) if sl else 0
reward = abs(tp - current_price) if tp else 0
rr_ratio = reward / risk if risk > 0 else 0
# 验证风险回报比
if rr_ratio < 1.0:
print(f"[AIEntrySignalGenerator] 风险回报比过低: {rr_ratio:.2f}, 跳过信号")
return None
# 验证止损点数(最大为价格的 2%
max_risk = current_price * 0.02
if risk > max_risk:
print(f"[AIEntrySignalGenerator] 止损点数过大: {risk:.2f} > {max_risk:.2f}, 跳过信号")
return None
print(f"[AIEntrySignalGenerator] 生成信号: {action} @ {current_price:.2f}, SL={sl:.2f}, TP={tp:.2f}, risk={risk:.2f}, rr={rr_ratio:.2f}")
# 创建信号
signal = TradingSignal(
symbol=symbol,
action=action,
confidence=75, # AI信号置信度较高
source=SignalSource.AI_ENTRY,
source_period=period,
trigger_price=current_price,
trigger_reason=f"AI建议入场: {reason}",
suggested_entry=current_price,
suggested_sl=sl,
suggested_tp=tp,
risk_reward_ratio=round(rr_ratio, 2),
ai_analysis_period=period,
)
print(f"[AIEntrySignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, AI入场价={entry_price}")
return signal
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
"""生成所有匹配的信号"""
if not self._llm_analyzer:
return []
signals = []
matches = self._llm_analyzer.check_entry_price_nearby(
symbol, current_price, threshold=self.threshold
)
for match in matches:
period = match.get('period', '')
entry_price = match.get('entry_price', 0)
direction = match.get('direction', 'buy')
sl = match.get('stop_loss', 0)
tp = match.get('take_profit', 0)
reason = match.get('reason', '')
# 检查冷却
if self._check_cooldown(symbol, period, entry_price, direction):
continue
# 设置冷却
self._set_cooldown(symbol, period, entry_price, direction)
action = "buy" if direction == "buy" else "sell"
# 验证止损止盈
if not sl or not tp or sl <= 0 or tp <= 0:
print(f"[AIEntrySignalGenerator] 跳过无效信号: sl={sl}, tp={tp}")
continue
# 验证止损方向
if action == "buy" and sl >= current_price:
continue
if action == "sell" and sl <= current_price:
continue
risk = abs(current_price - sl) if sl else 0
reward = abs(tp - current_price) if tp else 0
rr_ratio = reward / risk if risk > 0 else 0
# 验证风险回报比
if rr_ratio < 1.0:
continue
# 验证止损点数(最大为价格的 2%
max_risk = current_price * 0.02
if risk > max_risk:
continue
signal = TradingSignal(
symbol=symbol,
action=action,
confidence=75,
source=SignalSource.AI_ENTRY,
source_period=period,
trigger_price=current_price,
trigger_reason=f"AI建议入场: {reason}",
suggested_entry=current_price,
suggested_sl=sl,
suggested_tp=tp,
risk_reward_ratio=round(rr_ratio, 2),
ai_analysis_period=period,
)
signals.append(signal)
return signals
def __call__(self, symbol: str, current_price: float) -> List[TradingSignal]:
"""使对象可调用"""
return self.generate_signals(symbol, current_price)
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
关键点位信号生成器
根据关键点位分析生成交易信号
"""
from typing import Optional, List, Dict
from datetime import datetime
from ...models import TradingSignal, SignalSource
class KeyLevelSignalGenerator:
"""关键点位信号生成器"""
def __init__(self):
# 关键点位配置
self._key_levels: Dict[str, List[float]] = {}
# 阈值(价格距离关键点位的百分比)
self.threshold = 0.0008 # 万分之八
# 信号冷却时间(秒)
self.cooldown = 180
# 冷却记录
self._signal_cooldowns: Dict[str, datetime] = {}
print("[KeyLevelSignalGenerator] 关键点位信号生成器已初始化")
def set_key_levels(self, symbol: str, levels: List[float]) -> None:
"""设置品种的关键点位"""
self._key_levels[symbol] = sorted(levels)
def get_key_levels(self, symbol: str, current_price: float) -> List[float]:
"""获取关键点位(如果没有配置则自动计算)"""
if symbol in self._key_levels:
return self._key_levels[symbol]
# 自动计算关键点位
return self._auto_calculate_key_levels(current_price)
def _auto_calculate_key_levels(self, current_price: float) -> List[float]:
"""自动计算关键点位"""
if current_price <= 0:
return []
int_part = int(current_price)
num_digits = len(str(int_part)) if int_part > 0 else 1
# 根据位数确定步长
if num_digits == 1:
step = 1
elif num_digits == 2:
step = 5
elif num_digits == 3:
step = 10
elif num_digits == 4:
step = 100
else:
step = 1000
# 计算基础点位
base_level = int(current_price / step) * step
# 生成上下各3个关键点位
levels = []
for i in range(-3, 4):
level = base_level + i * step
if level > 0:
levels.append(float(level))
return sorted(levels)
def _check_cooldown(self, symbol: str, key_level: float) -> bool:
"""检查是否在冷却期"""
key = f"{symbol}_{key_level}"
if key in self._signal_cooldowns:
last_time = self._signal_cooldowns[key]
elapsed = (datetime.now() - last_time).total_seconds()
return elapsed < self.cooldown
return False
def _set_cooldown(self, symbol: str, key_level: float) -> None:
"""设置冷却"""
key = f"{symbol}_{key_level}"
self._signal_cooldowns[key] = datetime.now()
def generate_signal(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
"""
生成关键点位信号
策略逻辑
- 价格在关键点位上方向下接近 买入支撑位
- 价格在关键点位下方向上接近 卖出压力位
"""
key_levels = self.get_key_levels(symbol, current_price)
if not key_levels:
return None
# 找到最近的关键点位
nearest_level = None
min_distance_pct = float('inf')
for level in key_levels:
distance_pct = abs(current_price - level) / current_price
if distance_pct < min_distance_pct:
min_distance_pct = distance_pct
nearest_level = level
if nearest_level is None:
return None
# 检查是否在阈值范围内
if min_distance_pct > self.threshold:
return None
# 检查冷却
if self._check_cooldown(symbol, nearest_level):
return None
# 设置冷却
self._set_cooldown(symbol, nearest_level)
# 确定方向
if current_price > nearest_level:
# 价格在关键点位上方 → 支撑位 → 买入
action = "buy"
sl = nearest_level - (nearest_level * 0.006) # 关键点位下方万分之六
risk = current_price - sl
tp = current_price + risk * 1.5
trigger_reason = f"价格向下接近 {nearest_level}(支撑位)"
else:
# 价格在关键点位下方 → 压力位 → 卖出
action = "sell"
sl = nearest_level + (nearest_level * 0.006)
risk = sl - current_price
tp = current_price - risk * 1.5
trigger_reason = f"价格向上接近 {nearest_level}(压力位)"
# 计算风险回报比
reward = abs(tp - current_price)
rr_ratio = reward / risk if risk > 0 else 0
print(f"[KeyLevelSignalGenerator] 生成信号: {action} @ {current_price:.2f}, key_level={nearest_level:.2f}, sl={sl:.2f}, tp={tp:.2f}, risk={risk:.2f}, rr={rr_ratio:.2f}")
# 创建信号
signal = TradingSignal(
symbol=symbol,
action=action,
confidence=65, # 基础置信度
source=SignalSource.KEY_LEVEL,
source_period="", # 关键点位不区分周期
trigger_price=current_price,
trigger_reason=trigger_reason,
suggested_entry=current_price,
suggested_sl=round(sl, 2),
suggested_tp=round(tp, 2),
risk_reward_ratio=round(rr_ratio, 2),
key_level=nearest_level,
distance_pct=round(min_distance_pct * 100, 4),
)
print(f"[KeyLevelSignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, 关键位={nearest_level}")
return signal
def __call__(self, symbol: str, current_price: float) -> Optional[TradingSignal]:
"""使对象可调用"""
signal = self.generate_signal(symbol, current_price)
return signal if signal else None
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点信号生成器
根据转折点分析生成交易信号
"""
from typing import Optional, List, Dict
from datetime import datetime
from ...models import TradingSignal, SignalSource
from ...store import PivotStore, KlineStore
from ...services import PivotService
class PivotSignalGenerator:
"""转折点信号生成器"""
def __init__(self, pivot_service: PivotService = None,
pivot_store: PivotStore = None,
kline_store: KlineStore = None):
self.pivot_service = pivot_service
self.pivot_store = pivot_store or PivotStore()
self.kline_store = kline_store or KlineStore()
# 信号冷却时间(秒)
self.cooldown = 180
# 已生成的信号冷却记录
self._signal_cooldowns: Dict[str, datetime] = {}
print("[PivotSignalGenerator] 转折点信号生成器已初始化")
def set_pivot_service(self, service: PivotService) -> None:
"""设置转折点服务"""
self.pivot_service = service
def _check_cooldown(self, symbol: str, period: str, pivot_price: float) -> bool:
"""检查是否在冷却期"""
key = f"{symbol}_{period}_{pivot_price}"
if key in self._signal_cooldowns:
last_time = self._signal_cooldowns[key]
elapsed = (datetime.now() - last_time).total_seconds()
return elapsed < self.cooldown
return False
def _set_cooldown(self, symbol: str, period: str, pivot_price: float) -> None:
"""设置冷却"""
key = f"{symbol}_{period}_{pivot_price}"
self._signal_cooldowns[key] = datetime.now()
def generate_signal(self, symbol: str, current_price: float,
period: str = "M1") -> Optional[TradingSignal]:
"""
生成转折点信号
Args:
symbol: 品种
current_price: 当前价格
period: 检测周期
Returns:
TradingSignal None
"""
if not self.pivot_service:
return None
# 检查是否接近转折点
near_pivots = self.pivot_service.check_near_pivot(symbol, current_price)
for pivot in near_pivots:
# 只处理指定周期
if pivot.get('period') != period:
continue
# 只处理接近类型(不是突破)
alert_type = pivot.get('alert_type', '')
if not alert_type.startswith('near_'):
continue
pivot_price = pivot.get('price', 0)
pivot_type = 'low' if 'low' in alert_type else 'high'
# 检查冷却
if self._check_cooldown(symbol, period, pivot_price):
continue
# 设置冷却
self._set_cooldown(symbol, period, pivot_price)
# 确定方向
if pivot_type == 'low':
action = "buy"
# 止损 = 低点 - 固定偏移
sl_offset = 10.0 # TODO: 从配置获取
sl = pivot_price - sl_offset
# 止盈 = 最近的高点
tp = self.pivot_service.find_nearest_pivot_price(symbol, 'high', current_price)
print(f"[PivotSignalGenerator] 买入信号: pivot_price={pivot_price:.2f}, sl={sl:.2f}, tp={tp}")
else:
action = "sell"
sl_offset = 10.0
sl = pivot_price + sl_offset
tp = self.pivot_service.find_nearest_pivot_price(symbol, 'low', current_price)
print(f"[PivotSignalGenerator] 卖出信号: pivot_price={pivot_price:.2f}, sl={sl:.2f}, tp={tp}")
# 如果没有找到反向转折点,或者止盈太近,使用风险回报比
risk = abs(current_price - sl)
min_reward = risk * 1.5 # 最小回报 = 1.5 倍风险
if tp is None:
if action == "buy":
tp = current_price + min_reward
else:
tp = current_price - min_reward
print(f"[PivotSignalGenerator] 未找到反向转折点,使用风险回报比: tp={tp:.2f}, risk={risk:.2f}")
else:
# 检查止盈是否太近
reward = abs(tp - current_price)
if reward < min_reward:
print(f"[PivotSignalGenerator] 止盈太近: reward={reward:.2f} < min_reward={min_reward:.2f}, 使用风险回报比")
if action == "buy":
tp = current_price + min_reward
else:
tp = current_price - min_reward
# 计算风险回报比
risk = abs(current_price - sl)
reward = abs(tp - current_price)
rr_ratio = reward / risk if risk > 0 else 0
# 止损点数验证 - 如果止损点数太大,跳过这个 pivot
risk_points = abs(current_price - sl)
max_allowed_risk = current_price * 0.02 # 最大风险 = 当前价格的 2%
if risk_points > max_allowed_risk:
print(f"[PivotSignalGenerator] 止损点数过大: {risk_points:.2f} > {max_allowed_risk:.2f}, 跳过信号")
continue
# 止损止盈验证
if sl <= 0 or tp <= 0:
print(f"[PivotSignalGenerator] 无效的止损止盈: sl={sl}, tp={tp}, 跳过信号")
continue
# 止损方向验证
if action == "buy" and sl >= current_price:
print(f"[PivotSignalGenerator] 买入止损无效: sl={sl} >= price={current_price}")
continue
if action == "sell" and sl <= current_price:
print(f"[PivotSignalGenerator] 卖出止损无效: sl={sl} <= price={current_price}")
continue
# 创建信号
signal = TradingSignal(
symbol=symbol,
action=action,
confidence=60, # 基础置信度
source=SignalSource.PIVOT,
source_period=period,
trigger_price=current_price,
trigger_reason=f"{period}接近{pivot_type}{pivot_price:.2f}",
suggested_entry=current_price,
suggested_sl=sl,
suggested_tp=tp,
risk_reward_ratio=round(rr_ratio, 2),
pivot_price=pivot_price,
pivot_type=pivot_type,
)
print(f"[PivotSignalGenerator] 生成信号: {signal.signal_id} {action} @ {current_price}, SL={sl:.2f}, TP={tp:.2f}")
return signal
return None
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
"""生成所有周期的信号"""
signals = []
for period in ['M1', 'M5']:
signal = self.generate_signal(symbol, current_price, period)
if signal:
signals.append(signal)
return signals
def __call__(self, symbol: str, current_price: float) -> List[TradingSignal]:
"""使对象可调用,用于注册到SignalService"""
return self.generate_signals(symbol, current_price)
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
信号服务模块
统一管理信号的生成存储和查询
"""
from typing import List, Dict, Optional
from datetime import datetime
import threading
from ...models import TradingSignal, SignalSource
from ...store import SignalStore
class SignalService:
"""信号服务(统一管理信号)"""
def __init__(self, signal_store: SignalStore = None):
self.store = signal_store or SignalStore()
# 信号生成器(注册后使用)
self._generators: Dict[str, callable] = {}
# 冷却管理(避免重复生成)
self._cooldowns: Dict[str, datetime] = {}
self._cooldown_lock = threading.Lock()
# 默认冷却时间(秒)
self.default_cooldown = 180 # 3分钟
# 启动清理线程
self._start_cleanup_thread()
print("[SignalService] 信号服务已初始化")
def _start_cleanup_thread(self):
"""启动清理线程"""
def cleanup_loop():
while True:
try:
self.store.cleanup_expired()
self._cleanup_cooldowns()
except Exception as e:
print(f"[SignalService] 清理线程异常: {e}")
threading.Event().wait(30)
thread = threading.Thread(target=cleanup_loop, daemon=True)
thread.start()
def _cleanup_cooldowns(self):
"""清理过期的冷却记录"""
current_time = datetime.now()
with self._cooldown_lock:
keys_to_remove = []
for key, last_time in self._cooldowns.items():
elapsed = (current_time - last_time).total_seconds()
if elapsed > self.default_cooldown * 2:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._cooldowns[key]
def _check_cooldown(self, key: str) -> bool:
"""检查是否在冷却期内"""
with self._cooldown_lock:
if key in self._cooldowns:
last_time = self._cooldowns[key]
elapsed = (datetime.now() - last_time).total_seconds()
return elapsed < self.default_cooldown
return False
def _set_cooldown(self, key: str) -> None:
"""设置冷却"""
with self._cooldown_lock:
self._cooldowns[key] = datetime.now()
# ==================== 信号生成器注册 ====================
def register_generator(self, source: str, generator: callable) -> None:
"""注册信号生成器"""
self._generators[source] = generator
print(f"[SignalService] 注册信号生成器: {source}")
# ==================== 信号生成 ====================
def generate_signals(self, symbol: str, current_price: float) -> List[TradingSignal]:
"""
生成信号调用所有注册的生成器
Args:
symbol: 品种
current_price: 当前价格
Returns:
生成的信号列表
"""
signals = []
for source, generator in self._generators.items():
try:
generated = generator(symbol, current_price)
if generated:
if isinstance(generated, list):
for signal in generated:
if isinstance(signal, TradingSignal):
signals.append(signal)
elif isinstance(generated, TradingSignal):
signals.append(generated)
except Exception as e:
print(f"[SignalService] 信号生成器 {source} 异常: {e}")
# 存储信号
for signal in signals:
self.store.add_signal(signal)
return signals
def add_signal(self, signal: TradingSignal) -> str:
"""添加信号"""
return self.store.add_signal(signal)
# ==================== 信号查询 ====================
def get_signal(self, signal_id: str) -> Optional[TradingSignal]:
"""获取信号"""
return self.store.get_signal_by_id(signal_id)
def get_active_signals(self, symbol: str = None) -> List[TradingSignal]:
"""获取活跃信号"""
return self.store.get_active_signals(symbol)
def get_active_signals_by_source(self, symbol: str, source: str) -> List[TradingSignal]:
"""获取指定来源的活跃信号"""
return self.store.get_active_signals_by_source(symbol, source)
def get_signals_dict(self, symbol: str = None) -> List[Dict]:
"""获取信号字典列表"""
return self.store.get_signals_dict(symbol)
def get_signal_count(self, symbol: str = None, source: str = None) -> int:
"""获取信号数量"""
return self.store.get_signal_count(symbol, source)
# ==================== 信号消费 ====================
def consume_signal(self, signal_id: str) -> Optional[TradingSignal]:
"""
消费信号标记为已使用
Args:
signal_id: 信号ID
Returns:
信号对象
"""
signal = self.store.get_signal_by_id(signal_id)
if signal and signal.is_active():
self.store.mark_signal_used(signal_id)
return signal
return None
def consume_signals_for_decision(self, symbol: str) -> List[TradingSignal]:
"""
消费品种的所有活跃信号用于决策
Args:
symbol: 品种
Returns:
信号列表
"""
signals = self.store.get_active_signals(symbol)
for signal in signals:
self.store.mark_signal_used(signal.signal_id)
return signals
# ==================== 统计 ====================
def get_signal_stats(self, symbol: str) -> Dict:
"""获取信号统计"""
return self.store.get_signal_stats(symbol)
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"store": self.store.get_status(),
"generators": list(self._generators.keys()),
}
def clear_all(self) -> int:
"""清空所有信号"""
return self.store.clear_all()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统计数据服务模块
"""
from typing import Dict, Optional, List
from ..models.statistics import StatisticsData
from ..store.statistics_store import StatisticsStore
class StatisticsService:
"""
统计数据服务
功能
1. 处理EA上报的统计数据
2. 获取品种价差
3. 获取账户信息
"""
def __init__(self, store: StatisticsStore = None):
self.store = store or StatisticsStore()
def process_statistics(self, data: Dict) -> None:
"""
处理EA上报的统计数据
Args:
data: EA上报的JSON数据
"""
stat = StatisticsData.from_ea_data(data)
self.store.add(stat)
def get_latest(self, symbol: str = None) -> Optional[StatisticsData]:
"""获取最新统计数据"""
return self.store.get_latest(symbol)
def get_spread(self, symbol: str) -> Optional[float]:
"""获取品种价差"""
return self.store.get_spread(symbol)
def get_mid_price(self, symbol: str) -> Optional[float]:
"""获取品种中间价"""
latest = self.store.get_latest(symbol)
if latest:
return latest.mid_price
return None
def get_account_info(self, symbol: str = None) -> Dict:
"""获取账户信息"""
return self.store.get_account_info(symbol)
def get_by_symbol(self, symbol: str, count: int = 10) -> List[StatisticsData]:
"""获取指定品种的统计数据"""
return self.store.get_by_symbol(symbol, count)
def get_status(self) -> Dict:
"""获取服务状态"""
return self.store.get_status()
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
策略服务模块
"""
from .strategy_service import StrategyService
from .risk_manager import RiskManager
__all__ = [
'StrategyService',
'RiskManager',
]
+257
View File
@@ -0,0 +1,257 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
风险管理服务
"""
from typing import Dict, Optional
from datetime import datetime
from ...models import TradingStrategy
class RiskManager:
"""风险管理服务"""
def __init__(self):
# 账户信息(从外部更新)
self._account_balance: float = 0.0
self._account_equity: float = 0.0
self._free_margin: float = 0.0
# 每日风险限制
self._daily_risk_limit: float = 5.0 # 每日最大风险百分比
self._daily_risk_used: float = 0.0 # 今日已使用风险
# 品种配置(点值、最小手数等)
self._symbol_config: Dict[str, Dict] = {}
# 统计服务引用(用于获取账户信息)
self._statistics_service = None
print("[RiskManager] 风险管理服务已初始化")
def set_statistics_service(self, service) -> None:
"""设置统计服务引用"""
self._statistics_service = service
def _refresh_account_info(self) -> None:
"""从统计服务刷新账户信息"""
if not self._statistics_service:
return
try:
account_info = self._statistics_service.get_account_info()
if account_info:
self._account_balance = account_info.get('balance', 0.0)
self._account_equity = account_info.get('equity', 0.0)
# free_margin 通常等于 equity - used_margin,这里用 equity 近似
self._free_margin = account_info.get('equity', 0.0)
except Exception as e:
print(f"[RiskManager] 刷新账户信息失败: {e}")
# ==================== 账户信息 ====================
def update_account_info(self, balance: float, equity: float, free_margin: float) -> None:
"""更新账户信息"""
self._account_balance = balance
self._account_equity = equity
self._free_margin = free_margin
def get_account_balance(self) -> float:
"""获取账户余额"""
return self._account_balance
def get_account_equity(self) -> float:
"""获取账户权益"""
return self._account_equity
# ==================== 品种配置 ====================
def set_symbol_config(self, symbol: str, config: Dict) -> None:
"""设置品种配置"""
self._symbol_config[symbol] = config
def get_symbol_config(self, symbol: str) -> Dict:
"""获取品种配置"""
return self._symbol_config.get(symbol, {
"point_value": 1.0, # 点值
"min_volume": 0.01, # 最小手数
"max_volume": 10.0, # 最大手数
"volume_step": 0.01, # 手数步长
})
# ==================== 手数计算 ====================
def calculate_volume(self, symbol: str, risk_points: float,
strategy: TradingStrategy) -> float:
"""
计算交易手数
Args:
symbol: 品种
risk_points: 风险点数
strategy: 策略配置
Returns:
计算的手数
"""
config = self.get_symbol_config(symbol)
point_value = config.get('point_value', 1.0)
min_volume = config.get('min_volume', 0.01)
max_volume = config.get('max_volume', 10.0)
volume_step = config.get('volume_step', 0.01)
if strategy.volume_mode == "fixed":
volume = strategy.fixed_volume
elif strategy.volume_mode == "risk_percent":
# 根据风险百分比计算手数
risk_amount = self._account_balance * (strategy.risk_percent / 100)
# 手数 = 风险金额 / (风险点数 * 点值)
if risk_points > 0 and point_value > 0:
volume = risk_amount / (risk_points * point_value)
else:
volume = min_volume
else:
volume = strategy.fixed_volume
# 应用最大风险点数限制
if risk_points > strategy.max_risk_points:
print(f"[RiskManager] 风险点数 {risk_points} 超过最大限制 {strategy.max_risk_points}")
return 0.0
# 限制手数范围
volume = max(min_volume, min(volume, max_volume))
# 按步长取整
volume = round(volume / volume_step) * volume_step
return volume
# ==================== 风险检查 ====================
def check_risk(self, symbol: str, volume: float, risk_points: float) -> Dict:
"""
检查交易风险
Args:
symbol: 品种
volume: 手数
risk_points: 风险点数
Returns:
检查结果
"""
# 刷新账户信息
self._refresh_account_info()
config = self.get_symbol_config(symbol)
point_value = config.get('point_value', 1.0)
# 计算风险金额
risk_amount = volume * risk_points * point_value
risk_percent = (risk_amount / self._account_balance * 100) if self._account_balance > 0 else 0
# 检查每日风险限制
remaining_risk = self._daily_risk_limit - self._daily_risk_used
allowed = True
warnings = []
# 账户信息是否已初始化
account_initialized = self._account_balance > 0 or self._free_margin > 0
if risk_percent > 5:
allowed = False
warnings.append(f"单笔风险 {risk_percent:.2f}% 超过5%")
if risk_percent + self._daily_risk_used > self._daily_risk_limit:
allowed = False
warnings.append(f"将超过每日风险限制 {self._daily_risk_limit}%")
# 只有账户信息已初始化时才检查保证金
if account_initialized and self._free_margin < risk_amount:
allowed = False
warnings.append(f"保证金不足 (可用: {self._free_margin:.2f}, 需要: {risk_amount:.2f})")
if not account_initialized:
warnings.append("账户信息未初始化,跳过保证金检查")
return {
"allowed": allowed,
"risk_amount": risk_amount,
"risk_percent": round(risk_percent, 2),
"daily_risk_used": self._daily_risk_used,
"daily_risk_limit": self._daily_risk_limit,
"remaining_risk": remaining_risk,
"warnings": warnings,
"account_initialized": account_initialized,
}
# ==================== 持仓检查 ====================
def check_position_limit(self, symbol: str, strategy: TradingStrategy,
current_positions: int, same_direction: int,
opposite_direction: int, action: str) -> Dict:
"""
检查持仓限制
Args:
symbol: 品种
strategy: 策略配置
current_positions: 当前持仓数
same_direction: 同向持仓数
opposite_direction: 反向持仓数
action: 交易方向 buy/sell
Returns:
检查结果
"""
allowed = True
warnings = []
# 检查最大持仓数
if current_positions >= strategy.max_positions:
allowed = False
warnings.append(f"已达到最大持仓数 {strategy.max_positions}")
# 检查同向持仓
new_same_direction = same_direction + 1
if new_same_direction > strategy.max_same_direction:
allowed = False
warnings.append(f"同向持仓将超过限制 {strategy.max_same_direction}")
# 检查持仓冲突策略
if opposite_direction > 0:
if strategy.position_conflict == "block":
allowed = False
warnings.append("有反向持仓,策略禁止新开仓")
elif strategy.position_conflict == "allow_same":
allowed = False
warnings.append("有反向持仓,策略只允许同向加仓")
elif strategy.position_conflict == "allow_opposite":
# 允许反向
pass
return {
"allowed": allowed,
"current_positions": current_positions,
"same_direction": same_direction,
"opposite_direction": opposite_direction,
"max_positions": strategy.max_positions,
"max_same_direction": strategy.max_same_direction,
"warnings": warnings,
}
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取状态"""
return {
"account_balance": self._account_balance,
"account_equity": self._account_equity,
"free_margin": self._free_margin,
"daily_risk_limit": self._daily_risk_limit,
"daily_risk_used": self._daily_risk_used,
"symbol_count": len(self._symbol_config),
}
@@ -0,0 +1,456 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
策略决策服务
综合信号持仓资金等做出交易决策
"""
from typing import List, Dict, Optional
from datetime import datetime
import threading
from ...models import TradingSignal, TradingStrategy, TradingDecision
from ...models import ConsistencyRequirement, ConflictResolution
from ...models import StopLossMode, TakeProfitMode
from ...store import StrategyStore
from ..signal import SignalService
from .risk_manager import RiskManager
class StrategyService:
"""策略决策服务"""
def __init__(self, strategy_store: StrategyStore = None,
signal_service: SignalService = None,
risk_manager: RiskManager = None):
self.strategy_store = strategy_store or StrategyStore()
self.signal_service = signal_service or SignalService()
self.risk_manager = risk_manager or RiskManager()
# 持仓服务引用(外部设置)
self._position_service = None
# 待确认订单服务引用(外部设置)
self._pending_order_service = None
# 决策冷却
self._decision_cooldowns: Dict[str, datetime] = {}
self._cooldown_lock = threading.Lock()
self.decision_cooldown = 60 # 60秒冷却
print("[StrategyService] 策略决策服务已初始化")
def set_position_service(self, service) -> None:
"""设置持仓服务"""
self._position_service = service
def set_pending_order_service(self, service) -> None:
"""设置待确认订单服务"""
self._pending_order_service = service
# ==================== 策略配置 ====================
def get_strategy(self, symbol: str) -> TradingStrategy:
"""获取品种策略配置"""
return self.strategy_store.get_or_create_strategy(symbol)
def update_strategy(self, symbol: str, data: Dict) -> TradingStrategy:
"""更新策略配置"""
return self.strategy_store.update_strategy(symbol, data)
def get_all_strategies(self) -> List[TradingStrategy]:
"""获取所有策略"""
return self.strategy_store.get_all_strategies()
# ==================== 信号综合分析 ====================
def analyze_signals(self, symbol: str, signals: List[TradingSignal],
strategy: TradingStrategy) -> Dict:
"""
综合分析信号
Args:
symbol: 品种
signals: 信号列表
strategy: 策略配置
Returns:
分析结果
"""
if not signals:
return {
"total_count": 0,
"buy_count": 0,
"sell_count": 0,
"buy_weighted_score": 0,
"sell_weighted_score": 0,
"consistency": 0,
"direction": None,
"action": "none",
}
# 过滤掉未启用的信号
filtered_signals = []
for s in signals:
period = s.source_period if s.source != "key_level" else None
if strategy.is_signal_enabled(s.source, period):
filtered_signals.append(s)
if not filtered_signals:
return {
"total_count": 0,
"buy_count": 0,
"sell_count": 0,
"buy_weighted_score": 0,
"sell_weighted_score": 0,
"consistency": 0,
"direction": None,
"action": "none",
"filtered_out": len(signals),
}
buy_signals = [s for s in filtered_signals if s.action == "buy"]
sell_signals = [s for s in filtered_signals if s.action == "sell"]
# 计算加权分数(使用新的周期级别权重)
buy_score = sum(
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
for s in buy_signals
)
sell_score = sum(
s.confidence * strategy.get_signal_weight(s.source, s.source_period) / 100
for s in sell_signals
)
# 计算一致性
total = len(filtered_signals)
majority_count = max(len(buy_signals), len(sell_signals))
consistency = majority_count / total if total > 0 else 0
# 确定方向
direction = None
if buy_score > sell_score:
direction = "buy"
elif sell_score > buy_score:
direction = "sell"
# 检查一致性要求
action = "none"
if direction:
if strategy.consistency_requirement == ConsistencyRequirement.ANY:
action = direction
elif strategy.consistency_requirement == ConsistencyRequirement.MAJORITY:
if consistency >= 0.5:
action = direction
elif strategy.consistency_requirement == ConsistencyRequirement.ALL:
if consistency == 1.0:
action = direction
return {
"total_count": total,
"buy_count": len(buy_signals),
"sell_count": len(sell_signals),
"buy_weighted_score": round(buy_score, 2),
"sell_weighted_score": round(sell_score, 2),
"consistency": round(consistency, 2),
"direction": direction,
"action": action,
"buy_signals": [s.signal_id for s in buy_signals],
"sell_signals": [s.signal_id for s in sell_signals],
"filtered_out": len(signals) - len(filtered_signals),
}
# ==================== 决策生成 ====================
def make_decision(self, symbol: str, current_price: float,
force_signals: List[TradingSignal] = None) -> Optional[TradingDecision]:
"""
做出交易决策
Args:
symbol: 品种
current_price: 当前价格
force_signals: 强制使用的信号用于测试
Returns:
TradingDecision None
"""
# 检查决策冷却
if self._is_in_cooldown(symbol):
return None
# 获取策略配置
strategy = self.get_strategy(symbol)
if not strategy.enabled:
return None
# 获取信号
signals = force_signals if force_signals else self.signal_service.get_active_signals(symbol)
# 过滤低置信度信号
signals = [s for s in signals if s.confidence >= strategy.min_confidence]
if not signals:
return None
# 分析信号
analysis = self.analyze_signals(symbol, signals, strategy)
if analysis["action"] == "none":
return None
action = analysis["action"]
# 选择最佳信号(用于止损止盈)
best_signal = self._select_best_signal(signals, action, strategy)
if not best_signal:
return None
# 计算止损止盈
entry_price = current_price
sl, tp = self._calculate_sl_tp(entry_price, best_signal, strategy)
if not sl or not tp or sl == 0 or tp == 0:
print(f"[StrategyService] 无效的止损止盈: sl={sl}, tp={tp}")
return None
# 计算风险
risk_points = abs(entry_price - sl)
reward_points = abs(tp - entry_price)
rr_ratio = reward_points / risk_points if risk_points > 0 else 0
# 检查风险回报比
if rr_ratio < strategy.min_risk_reward:
print(f"[StrategyService] 风险回报比 {rr_ratio:.2f} 低于最小要求 {strategy.min_risk_reward}")
return None
# 动态止损范围(根据价格调整)
# 最小止损 = 价格的 0.05% 或 5 点(取较大)
# 最大止损 = 价格的 2% 或 100 点(取较小)
price_min_sl = entry_price * 0.0005 # 价格的 0.05%
price_max_sl = entry_price * 0.02 # 价格的 2%
# 确保 min <= max
dynamic_min_sl = max(1.0, price_min_sl) # 最小至少 1 点
dynamic_max_sl = max(dynamic_min_sl, price_max_sl) # 最大至少等于最小
# 如果动态范围不合理,跳过
if dynamic_min_sl > dynamic_max_sl:
print(f"[StrategyService] 动态止损范围无效: [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}], 跳过决策")
return None
# 检查止损点数
if risk_points < dynamic_min_sl or risk_points > dynamic_max_sl:
print(f"[StrategyService] 止损点数 {risk_points:.2f} 不在动态范围 [{dynamic_min_sl:.2f}, {dynamic_max_sl:.2f}] (价格={entry_price:.2f})")
return None
# 计算手数
volume = self.risk_manager.calculate_volume(symbol, risk_points, strategy)
if volume <= 0:
return None
# 检查持仓限制
position_check = self._check_position_limits(symbol, strategy, action)
# 检查风险限制
risk_check = self.risk_manager.check_risk(symbol, volume, risk_points)
# 如果检查不通过,返回拒绝的决策
if not position_check.get("allowed", True) or not risk_check.get("allowed", True):
# 即使被拒绝也要设置冷却,避免频繁推送
self._set_cooldown(symbol)
decision = TradingDecision(
symbol=symbol,
strategy_id=strategy.strategy_id,
action="none",
decision_type="rejected",
signals=[s.to_dict() for s in signals],
signal_summary=analysis,
decision_reason="风控检查未通过",
confidence_score=0,
position_check=position_check,
risk_check=risk_check,
status="rejected",
)
return decision
# 设置决策冷却
self._set_cooldown(symbol)
# 生成决策理由
decision_reason = self._generate_decision_reason(analysis, best_signal)
# 创建决策
decision = TradingDecision(
symbol=symbol,
strategy_id=strategy.strategy_id,
action=action,
decision_type="signal_combined" if len(signals) > 1 else "single_signal",
signals=[s.to_dict() for s in signals],
signal_summary=analysis,
entry_price=entry_price,
sl=round(sl, 2),
tp=round(tp, 2),
volume=volume,
risk_points=round(risk_points, 2),
reward_points=round(reward_points, 2),
risk_reward_ratio=round(rr_ratio, 2),
decision_reason=decision_reason,
confidence_score=analysis["buy_weighted_score"] if action == "buy" else analysis["sell_weighted_score"],
position_check=position_check,
risk_check=risk_check,
)
print(f"[StrategyService] 生成决策: {decision.decision_id} {action} {symbol} @ {entry_price}")
return decision
def _select_best_signal(self, signals: List[TradingSignal],
action: str, strategy: TradingStrategy) -> Optional[TradingSignal]:
"""选择最佳信号"""
filtered = [s for s in signals if s.action == action]
if not filtered:
return None
if strategy.conflict_resolution == ConflictResolution.HIGHEST_CONFIDENCE:
return max(filtered, key=lambda s: s.confidence)
elif strategy.conflict_resolution == ConflictResolution.HIGHEST_WEIGHT:
return max(filtered, key=lambda s: s.confidence * strategy.get_signal_weight(s.source, s.source_period))
else:
return filtered[0]
def _calculate_sl_tp(self, entry_price: float, signal: TradingSignal,
strategy: TradingStrategy) -> tuple:
"""计算止损止盈"""
# 止损
if strategy.sl_mode == StopLossMode.SIGNAL:
sl = signal.suggested_sl
elif strategy.sl_mode == StopLossMode.FIXED_POINTS:
if signal.action == "buy":
sl = entry_price - strategy.sl_fixed_points
else:
sl = entry_price + strategy.sl_fixed_points
else:
sl = signal.suggested_sl
# 止盈
if strategy.tp_mode == TakeProfitMode.SIGNAL:
tp = signal.suggested_tp
elif strategy.tp_mode == TakeProfitMode.FIXED_POINTS:
if signal.action == "buy":
tp = entry_price + strategy.tp_fixed_points
else:
tp = entry_price - strategy.tp_fixed_points
elif strategy.tp_mode == TakeProfitMode.RISK_REWARD:
risk = abs(entry_price - sl)
if signal.action == "buy":
tp = entry_price + risk * strategy.tp_risk_reward
else:
tp = entry_price - risk * strategy.tp_risk_reward
else:
tp = signal.suggested_tp
return sl, tp
def _check_position_limits(self, symbol: str, strategy: TradingStrategy,
action: str) -> Dict:
"""检查持仓限制"""
current_positions = 0
same_direction = 0
opposite_direction = 0
if self._position_service:
positions = self._position_service.get_positions(symbol)
current_positions = len(positions)
for pos in positions:
# PositionData.to_dict() 返回 direction 字段
pos_direction = pos.get('direction', '')
if pos_direction == action:
same_direction += 1
else:
opposite_direction += 1
return self.risk_manager.check_position_limit(
symbol, strategy, current_positions, same_direction, opposite_direction, action
)
def _generate_decision_reason(self, analysis: Dict, signal: TradingSignal) -> str:
"""生成决策理由"""
reasons = []
total = analysis["total_count"]
buy_count = analysis["buy_count"]
sell_count = analysis["sell_count"]
direction = analysis["direction"]
if total == 1:
reasons.append(f"单一信号({signal.source})建议{direction}")
else:
reasons.append(f"{total}个信号中{buy_count}个买入、{sell_count}个卖出")
reasons.append(f"综合判断: {direction}")
reasons.append(f"风险回报比: {signal.risk_reward_ratio:.2f}")
return " | ".join(reasons)
def _is_in_cooldown(self, symbol: str) -> bool:
"""检查是否在冷却期"""
with self._cooldown_lock:
if symbol in self._decision_cooldowns:
last_time = self._decision_cooldowns[symbol]
elapsed = (datetime.now() - last_time).total_seconds()
return elapsed < self.decision_cooldown
return False
def _set_cooldown(self, symbol: str) -> None:
"""设置冷却"""
with self._cooldown_lock:
self._decision_cooldowns[symbol] = datetime.now()
# ==================== 执行决策 ====================
def execute_decision(self, decision: TradingDecision) -> Optional[str]:
"""
执行决策生成待确认订单
Args:
decision: 交易决策
Returns:
订单ID None
"""
if decision.action == "none":
return None
if not self._pending_order_service:
print("[StrategyService] 待确认订单服务未设置")
return None
# 创建订单
order_id = self._pending_order_service.create_order(
symbol=decision.symbol,
action=decision.action,
price=decision.entry_price,
mount=decision.volume,
sl=decision.sl,
tp=decision.tp,
reason=decision.decision_reason,
description=f"Strategy: {decision.strategy_id}",
source="strategy_decision",
)
decision.order_id = order_id
decision.status = "confirmed"
print(f"[StrategyService] 决策已执行,订单ID: {order_id}")
return order_id
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"strategy_store": self.strategy_store.get_status(),
"signal_service": self.signal_service.get_status(),
"risk_manager": self.risk_manager.get_status(),
}
+238
View File
@@ -0,0 +1,238 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
技术指标计算模块
纯函数实现不依赖外部状态
"""
from typing import List, Dict
from ..models import KlineData
def calculate_ma(data: List[float], period: int) -> float:
"""
计算移动平均线 (MA)
Args:
data: 数据列表如收盘价
period: 周期
Returns:
MA
"""
if not data:
return 0
if len(data) < period:
return data[-1]
return sum(data[-period:]) / period
def calculate_adx(klines: List[KlineData], period: int = 14) -> float:
"""
计算 ADX (Average Directional Index)
ADX > 25: 有趋势
ADX > 40: 强趋势
ADX < 20: 无明显趋势
Args:
klines: K线数据列表
period: 计算周期
Returns:
ADX
"""
if len(klines) < period + 1:
return 0
# 计算 +DM 和 -DM
plus_dm = []
minus_dm = []
tr_list = []
for i in range(1, len(klines)):
high = klines[i].high
low = klines[i].low
prev_high = klines[i - 1].high
prev_low = klines[i - 1].low
prev_close = klines[i - 1].close
# +DM
up_move = high - prev_high
down_move = prev_low - low
if up_move > down_move and up_move > 0:
plus_dm.append(up_move)
else:
plus_dm.append(0)
# -DM
if down_move > up_move and down_move > 0:
minus_dm.append(down_move)
else:
minus_dm.append(0)
# True Range
tr = max(
high - low,
abs(high - prev_close),
abs(low - prev_close)
)
tr_list.append(tr)
if len(tr_list) < period:
return 0
# 计算平滑值
atr = sum(tr_list[-period:]) / period
smoothed_plus_dm = sum(plus_dm[-period:]) / period
smoothed_minus_dm = sum(minus_dm[-period:]) / period
# 计算 +DI 和 -DI
if atr == 0:
return 0
plus_di = (smoothed_plus_dm / atr) * 100
minus_di = (smoothed_minus_dm / atr) * 100
# 计算 DX
di_sum = plus_di + minus_di
if di_sum == 0:
return 0
dx = abs(plus_di - minus_di) / di_sum * 100
return dx
def calculate_rsi(data: List[float], period: int = 14) -> float:
"""
计算 RSI (Relative Strength Index)
Args:
data: 数据列表如收盘价
period: 计算周期
Returns:
RSI (0-100)
"""
if len(data) < period + 1:
return 50 # 默认中性值
gains = []
losses = []
for i in range(1, len(data)):
change = data[i] - data[i - 1]
if change > 0:
gains.append(change)
losses.append(0)
else:
gains.append(0)
losses.append(abs(change))
if len(gains) < period:
return 50
avg_gain = sum(gains[-period:]) / period
avg_loss = sum(losses[-period:]) / period
if avg_loss == 0:
return 100
rs = avg_gain / avg_loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_macd(data: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Dict:
"""
计算 MACD (Moving Average Convergence Divergence)
Args:
data: 数据列表
fast: 快线周期
slow: 慢线周期
signal: 信号线周期
Returns:
{"macd": float, "signal": float, "histogram": float}
"""
if len(data) < slow + signal:
return {"macd": 0, "signal": 0, "histogram": 0}
# 计算快慢 EMA(简化用 SMA 近似)
ema_fast = calculate_ema_approx(data, fast)
ema_slow = calculate_ema_approx(data, slow)
# MACD 线
macd_line = ema_fast - ema_slow
# 信号线(MACD 的移动平均)
# 简化处理
signal_line = macd_line # 简化
# 柱状图
histogram = macd_line - signal_line
return {
"macd": macd_line,
"signal": signal_line,
"histogram": histogram
}
def calculate_ema_approx(data: List[float], period: int) -> float:
"""
计算指数移动平均线 (EMA) 的近似值
Args:
data: 数据列表
period: 周期
Returns:
EMA
"""
if not data:
return 0
if len(data) < period:
return data[-1]
# 简化:使用 SMA 近似
return sum(data[-period:]) / period
def calculate_bollinger_bands(data: List[float], period: int = 20, std_dev: float = 2.0) -> Dict:
"""
计算布林带
Args:
data: 数据列表
period: 周期
std_dev: 标准差倍数
Returns:
{"upper": float, "middle": float, "lower": float}
"""
if len(data) < period:
current = data[-1] if data else 0
return {"upper": current, "middle": current, "lower": current}
# 中轨(SMA
middle = sum(data[-period:]) / period
# 计算标准差
subset = data[-period:]
variance = sum((x - middle) ** 2 for x in subset) / period
std = variance ** 0.5
# 上下轨
upper = middle + std_dev * std
lower = middle - std_dev * std
return {
"upper": upper,
"middle": middle,
"lower": lower
}
+341
View File
@@ -0,0 +1,341 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
技术分析服务模块
处理趋势分析共振分析交易建议生成等业务逻辑
"""
from datetime import datetime
from typing import Dict, List, Optional
from ..models import KlineData, TechTrendState, TechTrendChange, TechResonanceResult, TechTradeSuggestion
from ..store import TechStore, KlineStore, PivotStore
from .tech_indicators import calculate_ma, calculate_adx
class TechService:
"""技术分析服务(处理业务逻辑)"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# ADX 阈值
ADX_TREND_THRESHOLD = 25
ADX_STRONG_THRESHOLD = 40
# 均线周期
MA_FAST = 10
MA_SLOW = 20
# 最小 K 线数量
MIN_KLINES = 30
def __init__(self, tech_store: TechStore, kline_store: KlineStore, pivot_store: PivotStore):
self.tech_store = tech_store
self.kline_store = kline_store
self.pivot_store = pivot_store
# 统计服务引用(用于获取价差)
self._statistics_service = None
print("[TechService] 技术分析服务已初始化")
def set_statistics_service(self, statistics_service):
"""设置统计服务引用"""
self._statistics_service = statistics_service
def _get_symbol_spread(self, symbol: str) -> Optional[float]:
"""获取品种价差"""
if not self._statistics_service:
return None
return self._statistics_service.get_spread(symbol)
# ==================== 趋势分析 ====================
def analyze_trend(self, symbol: str, period: str) -> Dict:
"""
分析单个周期的趋势
Args:
symbol: 交易品种
period: 周期
Returns:
趋势状态字典
"""
period = period.upper()
# 从 store 获取 K 线数据
klines_dict = self.kline_store.get_all_klines(symbol, period)
if not klines_dict:
return self._create_unknown_state(symbol, period, "无K线数据")
# 转换为 KlineData 对象
klines = [
KlineData(
symbol=k.get('symbol', symbol),
period=k.get('period', period),
timestamp=k.get('timestamp'),
open_price=float(k.get('open', 0)),
high=float(k.get('high', 0)),
low=float(k.get('low', 0)),
close=float(k.get('close', 0)),
volume=float(k.get('volume', 0))
)
for k in klines_dict
]
if len(klines) < self.MIN_KLINES:
return self._create_unknown_state(symbol, period, f"K线数据不足(需≥{self.MIN_KLINES}根)")
# 计算技术指标
closes = [k.close for k in klines]
ma_fast = calculate_ma(closes, self.MA_FAST)
ma_slow = calculate_ma(closes, self.MA_SLOW)
current_price = closes[-1]
adx = calculate_adx(klines)
# 判断趋势
trend, reason = self._determine_trend(adx, ma_fast, ma_slow, current_price)
# 计算强度
strength = self._calculate_strength(adx)
# 获取之前的状态
previous_state = self.tech_store.get_trend_state_object(symbol, period)
previous_trend = previous_state.trend if previous_state else None
change_signal = previous_trend and previous_trend != "unknown" and previous_trend != trend
# 创建状态对象
state = TechTrendState(
symbol=symbol,
period=period,
trend=trend,
strength=strength,
adx=round(adx, 2),
ma_fast=round(ma_fast, 4),
ma_slow=round(ma_slow, 4),
price=current_price,
reason=reason,
timestamp=datetime.now().isoformat(),
previous_trend=previous_trend,
change_signal=change_signal
)
# 保存状态
self.tech_store.save_trend_state(state)
# 记录趋势转换
if change_signal:
change = TechTrendChange(
period=period,
from_trend=previous_trend,
to_trend=trend,
price=current_price,
timestamp=datetime.now().isoformat()
)
self.tech_store.add_trend_change(symbol, change)
return state.to_dict()
def _create_unknown_state(self, symbol: str, period: str, reason: str) -> Dict:
"""创建未知状态"""
state = TechTrendState(
symbol=symbol,
period=period,
trend="unknown",
reason=reason,
timestamp=datetime.now().isoformat()
)
return state.to_dict()
def _determine_trend(self, adx: float, ma_fast: float, ma_slow: float, price: float) -> tuple:
"""判断趋势方向"""
reason_parts = []
if adx < self.ADX_TREND_THRESHOLD:
trend = "sideways"
reason_parts.append(f"ADX={adx:.1f}<25 无明显趋势")
else:
if ma_fast > ma_slow and price > ma_fast:
trend = "up"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({price:.2f}) > MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
elif ma_fast < ma_slow and price < ma_fast:
trend = "down"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({price:.2f}) < MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
else:
trend = "sideways"
if ma_fast > ma_slow:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"但价格({price:.2f})低于MA{self.MA_FAST}")
else:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"且价格({price:.2f})高于MA{self.MA_FAST}")
reason_parts.append("信号矛盾,判定震荡")
return trend, "".join(reason_parts)
def _calculate_strength(self, adx: float) -> int:
"""计算趋势强度"""
if adx >= self.ADX_STRONG_THRESHOLD:
return min(100, int(adx + 20))
elif adx >= self.ADX_TREND_THRESHOLD:
return int(adx + 10)
else:
return int(adx)
# ==================== 共振分析 ====================
def analyze_resonance(self, symbol: str) -> Dict:
"""
分析多周期共振
Args:
symbol: 交易品种
Returns:
共振分析结果
"""
states = self.tech_store.get_all_trend_states(symbol)
if not states:
result = TechResonanceResult(symbol=symbol)
return result.to_dict()
# 统计各趋势数量
up_count = sum(1 for s in states.values() if s.trend == 'up')
down_count = sum(1 for s in states.values() if s.trend == 'down')
sideways_count = sum(1 for s in states.values() if s.trend == 'sideways')
# 计算平均强度
strengths = [s.strength for s in states.values() if s.trend != 'sideways']
avg_strength = sum(strengths) / len(strengths) if strengths else 0
# 判断共振
total = len(states)
if up_count >= total * 0.6:
resonance = "up"
aligned_count = up_count
signal = f"多周期向上共振 ({up_count}/{total})"
elif down_count >= total * 0.6:
resonance = "down"
aligned_count = down_count
signal = f"多周期向下共振 ({down_count}/{total})"
else:
resonance = "none"
aligned_count = max(up_count, down_count)
signal = f"趋势分歧 (↑{up_count}{down_count}{sideways_count})"
result = TechResonanceResult(
symbol=symbol,
resonance=resonance,
strength=int(avg_strength),
aligned_count=aligned_count,
up_count=up_count,
down_count=down_count,
sideways_count=sideways_count,
signal=signal,
periods={p: s.to_dict() for p, s in states.items()}
)
return result.to_dict()
# ==================== 交易建议 ====================
def generate_trade_suggestion(self, symbol: str, current_price: float) -> Optional[Dict]:
"""
基于趋势和转折点生成交易建议
Args:
symbol: 交易品种
current_price: 当前实时价格
Returns:
交易建议 None
"""
# 获取共振分析
resonance = self.analyze_resonance(symbol)
if resonance['resonance'] == 'none':
return None
if resonance['strength'] < 30:
return None
trend = resonance['resonance']
# 从 pivot_store 获取转折点
pivots = self.pivot_store.get_pivot_objects(symbol)
if not pivots:
return None
# 按时间排序
recent_pivots = sorted(
[p.to_dict() for p in pivots],
key=lambda x: str(x.get('timestamp', '')),
reverse=True
)[:10]
sl = None
tp = None
action = None
reason = ""
if trend == "up":
action = "b"
low_pivots = [p for p in recent_pivots if p.get('direction') == 'low']
if low_pivots:
sl = low_pivots[0].get('price')
if sl and current_price > sl:
distance = current_price - sl
tp = current_price + distance * 1.5
reason = f"多周期向上共振,建议买入,止损参考最近低点 {sl}"
else:
return None
elif trend == "down":
action = "s"
high_pivots = [p for p in recent_pivots if p.get('direction') == 'high']
if high_pivots:
sl = high_pivots[0].get('price')
if sl and current_price < sl:
distance = sl - current_price
tp = current_price - distance * 1.5
reason = f"多周期向下共振,建议卖出,止损参考最近高点 {sl}"
else:
return None
if not all([action, sl, tp]):
return None
suggestion = TechTradeSuggestion(
symbol=symbol,
action=action,
price=current_price,
sl=round(sl, 4),
tp=round(tp, 4),
reason=reason,
trend_strength=resonance['strength'],
resonance_periods=resonance['aligned_count'],
generated_at=datetime.now().isoformat()
)
return suggestion.to_dict()
# ==================== 查询 ====================
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
"""获取趋势状态"""
return self.tech_store.get_trend_state(symbol, period)
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
"""获取趋势转换历史"""
return self.tech_store.get_trend_changes(symbol, count)
def get_status(self) -> Dict:
"""获取状态"""
return self.tech_store.get_status()
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易历史服务模块
"""
from typing import Dict, List
from ..models.trade_history import TradeDeal
from ..store.trade_history_store import TradeHistoryStore
class TradeHistoryService:
"""
交易历史服务
功能
1. 处理EA上报的交易历史
2. 提供交易统计分析
"""
def __init__(self, store: TradeHistoryStore = None):
self.store = store or TradeHistoryStore()
def process_deals(self, deals_data: List[Dict]) -> int:
"""
处理EA上报的成交记录
Args:
deals_data: EA上报的成交数据列表
Returns:
新增记录数
"""
deals = [TradeDeal.from_ea_data(data) for data in deals_data]
return self.store.add(deals)
def get_deals(self, symbol: str = None, hours: int = None) -> List[Dict]:
"""获取成交记录"""
return self.store.get_dict(symbol, hours)
def get_statistics(self, symbol: str = None) -> Dict:
"""获取交易统计"""
return self.store.get_statistics(symbol)
def get_recent_profit(self, symbol: str = None, hours: int = 24) -> float:
"""获取最近N小时的盈亏"""
return self.store.get_recent_profit(symbol, hours)
def get_status(self) -> Dict:
"""获取服务状态"""
return self.store.get_status()
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易指令服务模块
"""
from typing import List, Dict, Optional
from datetime import datetime
from ..models import TradingInstruction, PendingOrder
from ..store import TradingInstructionStore
class TradingInstructionService:
"""交易指令服务(处理业务逻辑)"""
def __init__(self, instruction_store: TradingInstructionStore = None):
self.store = instruction_store or TradingInstructionStore()
print("[TradingInstructionService] 交易指令服务已初始化")
# ==================== 创建指令 ====================
def create_instruction(self, symbol: str, action: str, price: float,
mount: float, sl: float = 0.0, tp: float = 0.005,
reason: str = "", description: str = "",
source: str = "", order_id: str = None) -> str:
"""
创建交易指令
Args:
symbol: 品种
action: 方向 (b/s)
price: 执行价格
mount: 手数
sl: 止损
tp: 止盈
reason: 原因
description: 描述
source: 来源
order_id: 来源订单ID
Returns:
指令ID
"""
instruction = TradingInstruction(
symbol=symbol,
action=action,
price=price,
mount=mount,
sl=sl,
tp=tp,
reason=reason,
description=description,
source=source,
order_id=order_id,
)
return self.store.add_instruction(instruction)
def create_instruction_from_dict(self, data: Dict) -> str:
"""从字典创建指令"""
instruction = TradingInstruction.from_dict(data)
return self.store.add_instruction(instruction)
def create_from_pending_order(self, order: PendingOrder) -> str:
"""从待确认订单创建指令"""
instruction = TradingInstruction.from_pending_order(order)
return self.store.add_instruction(instruction)
def create_instructions_batch(self, instructions_data: List[Dict]) -> int:
"""
批量创建指令
Args:
instructions_data: 指令字典列表
Returns:
创建数量
"""
count = 0
for data in instructions_data:
# 填充默认值
if data.get('sl') is None:
data['sl'] = 0.0
if data.get('tp') is None or data.get('tp', 0) <= 0:
data['tp'] = 0.005
self.create_instruction_from_dict(data)
count += 1
print(f"[TradingInstructionService] 批量创建指令: {count}")
return count
# ==================== 查询指令 ====================
def get_instruction(self, instruction_id: str) -> Optional[TradingInstruction]:
"""获取指令"""
return self.store.get_instruction_by_id(instruction_id)
def get_instructions_by_symbol(self, symbol: str) -> List[TradingInstruction]:
"""获取指定品种的指令"""
return self.store.get_instructions_by_symbol(symbol)
def get_all_instructions(self) -> List[TradingInstruction]:
"""获取所有指令"""
return self.store.get_all_instructions()
def get_all_instructions_dict(self) -> Dict[str, List[Dict]]:
"""获取所有指令(按品种分类)"""
return self.store.get_all_instructions_dict()
# ==================== EA获取指令 ====================
def fetch_instructions_for_ea(self, symbol: str, current_price: float = None) -> List[Dict]:
"""
EA获取满足条件的指令
Args:
symbol: 品种
current_price: 当前价格用于价格过滤
Returns:
满足条件的指令列表字典格式
"""
return self.store.fetch_and_remove_by_symbol(symbol, current_price)
# ==================== 清理指令 ====================
def remove_instruction(self, instruction_id: str) -> Optional[TradingInstruction]:
"""移除指令"""
return self.store.remove_instruction(instruction_id)
def clear_by_symbol(self, symbol: str) -> int:
"""清空指定品种的指令"""
return self.store.clear_by_symbol(symbol)
def clear_all(self) -> int:
"""清空所有指令"""
return self.store.clear_all()
# ==================== 统计 ====================
def get_count_by_symbol(self, symbol: str) -> int:
"""获取指定品种的指令数量"""
return self.store.get_count_by_symbol(symbol)
def get_total_count(self) -> int:
"""获取总指令数量"""
return self.store.get_total_count()
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return self.store.get_status()
-454
View File
@@ -1,454 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线数据存储模块
按周期和Symbol存储K线数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
class KlineData:
"""K线数据结构"""
def __init__(self, symbol: str, period: str, timestamp, open_price: float,
high: float, low: float, close: float, volume: float = 0):
self.symbol = symbol
self.period = period # H4, H1, M15, M5, M1
self.timestamp = timestamp
self.open = open_price
self.high = high
self.low = low
self.close = close
self.volume = volume
def to_dict(self) -> Dict:
"""转换为字典"""
ts = self.timestamp
if isinstance(ts, datetime):
ts_str = ts.strftime("%Y-%m-%d %H:%M:%S")
else:
ts_str = str(ts)
return {
"symbol": self.symbol,
"period": self.period,
"timestamp": ts_str,
"open": self.open,
"high": self.high,
"low": self.low,
"close": self.close,
"volume": self.volume
}
class MarketStore:
"""K线数据存储"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# 各周期最大存储条数
MAX_KLINES = {
'H4': 1500, # 4小时,6个月约1100根,留余量
'H1': 1000, # 1小时,1个月约720根
'M15': 500, # 15分钟,3天约288根
'M5': 400, # 5分钟,24小时288根
'M1': 100 # 1分钟,1小时60根
}
# 各周期时间间隔(秒)
PERIOD_INTERVALS = {
'H4': 4 * 60 * 60, # 4小时
'H1': 1 * 60 * 60, # 1小时
'M15': 15 * 60, # 15分钟
'M5': 5 * 60, # 5分钟
'M1': 1 * 60 # 1分钟
}
def __init__(self):
# 存储结构: {SYMBOL: {PERIOD: [KlineData, ...]}}
self._klines = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 标记每个symbol每个周期是否已收到全量数据
# 结构: {SYMBOL: {PERIOD: True/False}}
self._initialized = defaultdict(lambda: defaultdict(bool))
# 记录每个symbol的M1数据最后更新时间(本地时间,用于判断数据是否过期)
# 结构: {SYMBOL: datetime}
self._m1_update_time = {}
print("[MarketStore] K线存储已初始化")
def save_klines(self, symbol: str, period: str, klines: List[Dict],
is_full: bool = False) -> Dict:
"""
保存K线数据
Args:
symbol: 交易品种
period: 周期 (H4/H1/M15/M5/M1)
klines: K线数据列表
is_full: 是否为全量数据
Returns:
{"status": "ok", "count": N, "is_full": bool}
"""
period = period.upper()
if period not in self.PERIODS:
return {"status": "error", "message": f"不支持的周期: {period}"}
with self._lock:
# 注意:EA推送全量时会按顺序推送所有周期(H4→H1→M15→M5→M1
# 每个周期单独推送,is_full=true
# 所以这里只清空当前周期的数据,其他周期等待各自的推送
if is_full:
# 全量数据,清空该品种当前周期的历史数据
self._klines[symbol][period] = []
print(f"[MarketStore] 收到 {symbol} {period} 全量数据,清空该周期历史数据")
# 解析并存储K线数据
new_count = 0
update_count = 0 # 记录更新的数据条数
for k in klines:
kline = KlineData(
symbol=symbol,
period=period,
timestamp=k.get('timestamp') or k.get('time'),
open_price=float(k.get('open', 0)),
high=float(k.get('high', 0)),
low=float(k.get('low', 0)),
close=float(k.get('close', 0)),
volume=float(k.get('volume', 0))
)
# 检查是否已存在相同时间戳的数据
existing = self._klines[symbol][period]
ts = kline.timestamp
# 查找是否已存在
found_idx = -1
for i, existing_kline in enumerate(existing):
if self._normalize_timestamp(existing_kline.timestamp) == self._normalize_timestamp(ts):
found_idx = i
break
if found_idx >= 0:
# 更新已有数据
existing[found_idx] = kline
update_count += 1
else:
# 添加新数据
existing.append(kline)
new_count += 1
# 按时间排序
self._klines[symbol][period].sort(
key=lambda x: self._normalize_timestamp(x.timestamp)
)
# 限制最大条数,保留最新的
max_count = self.MAX_KLINES.get(period, 500)
if len(self._klines[symbol][period]) > max_count:
self._klines[symbol][period] = self._klines[symbol][period][-max_count:]
# 标记已初始化
self._initialized[symbol][period] = True
# 如果是M1数据,更新最后更新时间(有新数据或更新数据都算)
if period == 'M1' and (new_count > 0 or update_count > 0):
self._m1_update_time[symbol] = datetime.now()
total = len(self._klines[symbol][period])
print(f"[MarketStore] {symbol} {period} 保存了 {new_count} 条新数据, 当前共 {total}")
return {
"status": "ok",
"count": new_count,
"total": total,
"is_full": is_full
}
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
"""获取K线数据"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period][-count:]
return [k.to_dict() for k in klines]
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
"""获取所有K线数据"""
period = period.upper()
with self._lock:
return [k.to_dict() for k in self._klines[symbol][period]]
def get_latest_price(self, symbol: str) -> Optional[float]:
"""获取最新价格(从K线的最新close,优先M1,依次尝试其他周期)"""
with self._lock:
# 尝试找到匹配的symbol
actual_symbol = None
if symbol in self._klines:
actual_symbol = symbol
else:
# 尝试模糊匹配(去除#后缀)
symbol_base = symbol.replace('#', '')
for s in self._klines:
if s.replace('#', '') == symbol_base:
actual_symbol = s
break
if not actual_symbol:
return None
# 按优先级尝试各周期(M1优先,然后更短周期)
for period in ['M1', 'M5', 'M15', 'H1', 'H4']:
klines = self._klines[actual_symbol][period]
if klines:
return klines[-1].close
return None
def is_initialized(self, symbol: str, period: str) -> bool:
"""检查某个周期的数据是否已初始化"""
period = period.upper()
return self._initialized[symbol][period]
def check_all_initialized(self, symbol: str) -> bool:
"""检查所有周期是否都已初始化"""
return all(self._initialized[symbol][p] for p in self.PERIODS)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的数据"""
with self._lock:
if symbol in self._klines:
del self._klines[symbol]
if symbol in self._initialized:
del self._initialized[symbol]
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
status = {}
for symbol in self._klines:
status[symbol] = {}
for period in self.PERIODS:
count = len(self._klines[symbol][period])
initialized = self._initialized[symbol][period]
status[symbol][period] = {
"count": count,
"initialized": initialized
}
return status
def get_symbols(self) -> List[str]:
"""获取所有有实际数据的symbol列表"""
with self._lock:
symbols = []
for symbol in self._klines:
# 检查是否有实际数据(任一周期有K线数据)
has_data = False
for period in self.PERIODS:
if len(self._klines[symbol][period]) > 0:
has_data = True
break
if has_data:
symbols.append(symbol)
return symbols
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳为字符串"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts)
def get_latest_kline_time(self, symbol: str, period: str = 'M1') -> Optional[datetime]:
"""
获取指定品种和周期的最新K线时间戳
Args:
symbol: 品种名称
period: 周期默认M1
Returns:
最新K线时间戳如果没有数据返回None
"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period]
if not klines:
return None
latest_ts = klines[-1].timestamp
if isinstance(latest_ts, datetime):
return latest_ts
else:
# 尝试解析字符串时间戳(支持多种格式)
ts_str = str(latest_ts)
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
return datetime.strptime(ts_str, fmt)
except:
continue
return None
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
"""
检查M1 K线是否在指定秒数内更新
Args:
symbol: 品种名称
seconds: 秒数默认180秒3分钟
Returns:
{
"has_data": bool, # 是否有M1数据
"latest_time": datetime, # 最新K线时间(MT5服务器时间)
"update_time": datetime, # 服务端收到更新的时间(本地时间)
"seconds_ago": int, # 距今多少秒(基于本地更新时间)
"is_stale": bool, # 是否过期(超过指定秒数)
"market_status": str # 市场状态: "active", "stale", "closed"
}
"""
with self._lock:
# 检查是否有M1数据
has_m1_data = len(self._klines[symbol]['M1']) > 0
if not has_m1_data:
return {
"has_data": False,
"latest_time": None,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed" # 无数据,可能休市
}
# 获取最新K线时间(MT5服务器时间,仅用于显示)
latest_time = self.get_latest_kline_time(symbol, 'M1')
# 获取服务端收到更新的时间(本地时间,用于判断过期)
update_time = self._m1_update_time.get(symbol)
if update_time is None:
# 有数据但没有更新时间记录,说明是服务重启前的旧数据
# 这种情况也认为是休市,等下次推送数据时再处理
return {
"has_data": True,
"latest_time": latest_time,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed" # 无新数据推送,可能休市
}
now = datetime.now()
seconds_ago = int((now - update_time).total_seconds())
if seconds_ago > seconds:
market_status = "stale" # 数据过期
else:
market_status = "active" # 活跃
return {
"has_data": True,
"latest_time": latest_time,
"update_time": update_time,
"seconds_ago": seconds_ago,
"is_stale": seconds_ago > seconds,
"market_status": market_status
}
def check_kline_continuity(self, symbol: str, period: str, new_klines: List[Dict]) -> Dict:
"""
检查增量K线数据是否连续
Args:
symbol: 品种名称
period: 周期
new_klines: 新推送的K线数据列表
Returns:
{
"is_continuous": bool, # 是否连续
"gap_count": int, # 缺失的K线数量
"last_existing_time": datetime, # 现有数据最后时间
"first_new_time": datetime, # 新数据最早时间
"expected_gap": int # 期望的间隔(周期数)
}
"""
period = period.upper()
if not new_klines:
return {"is_continuous": True, "gap_count": 0}
# 获取周期时间间隔(秒)
interval = self.PERIOD_INTERVALS.get(period, 60)
# 允许的间隔倍数(现有数据+1周期)
max_allowed_gap = interval * 2 # 允许最多1个周期的间隔
with self._lock:
existing = self._klines[symbol][period]
if not existing:
# 没有历史数据,需要检查是否初始化
return {"is_continuous": True, "gap_count": 0}
# 获取现有数据最后时间
last_existing = existing[-1]
last_existing_time = self._parse_timestamp(last_existing.timestamp)
if last_existing_time is None:
return {"is_continuous": True, "gap_count": 0}
# 获取新数据最早时间(新数据可能有多条,取最早的)
first_new_time = None
for k in new_klines:
ts = self._parse_timestamp(k.get('timestamp') or k.get('time'))
if ts:
if first_new_time is None or ts < first_new_time:
first_new_time = ts
if first_new_time is None:
return {"is_continuous": True, "gap_count": 0}
# 计算时间差
time_diff = (first_new_time - last_existing_time).total_seconds()
# 如果新数据时间早于或等于现有数据,是更新操作,算连续
if time_diff <= 0:
return {
"is_continuous": True,
"gap_count": 0,
"last_existing_time": last_existing_time,
"first_new_time": first_new_time
}
# 计算间隔的周期数
gap_periods = int(time_diff / interval)
return {
"is_continuous": gap_periods <= 1, # 允许最多1个周期的间隔
"gap_count": max(0, gap_periods - 1), # 缺失的周期数
"last_existing_time": last_existing_time,
"first_new_time": first_new_time,
"expected_gap": gap_periods
}
def _parse_timestamp(self, ts) -> Optional[datetime]:
"""解析时间戳为datetime对象"""
if ts is None:
return None
if isinstance(ts, datetime):
return ts
ts_str = str(ts)
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
return datetime.strptime(ts_str, fmt)
except:
continue
return None
+27
View File
@@ -0,0 +1,27 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
存储模块
"""
from .kline_store import KlineStore
from .pivot_store import PivotStore
from .llm_store import LLMStore
from .tech_store import TechStore
from .calendar_store import CalendarStore
from .flash_news_store import FlashNewsStore
from .pending_order_store import PendingOrderStore
from .trading_instruction_store import TradingInstructionStore
from .signal_store import SignalStore
from .strategy_store import StrategyStore
from .statistics_store import StatisticsStore
from .position_store import PositionStore
from .trade_history_store import TradeHistoryStore
__all__ = [
'KlineStore', 'PivotStore', 'LLMStore', 'TechStore',
'CalendarStore', 'FlashNewsStore',
'PendingOrderStore', 'TradingInstructionStore',
'SignalStore', 'StrategyStore',
'StatisticsStore', 'PositionStore', 'TradeHistoryStore'
]
+259
View File
@@ -0,0 +1,259 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
财经日历存储模块
存储财经日历事件数据
"""
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import threading
from ..models import CalendarEvent
class CalendarStore:
"""财经日历存储(只负责数据CRUD"""
# 过期数据清理阈值(小时)
EXPIRY_HOURS = 6
def __init__(self):
# 事件列表,按时间排序
self._events: List[CalendarEvent] = []
self._lock = threading.RLock()
# 已提醒的事件ID集合
self._alerted_ids: set = set()
print("[CalendarStore] 财经日历存储已初始化")
# ==================== 事件管理 ====================
def save_events(self, events: List[CalendarEvent]) -> Dict:
"""
保存事件全量替换
Args:
events: 事件列表
Returns:
{"added": N, "updated": M, "total": T}
"""
with self._lock:
# 清理过期数据
self._cleanup_expired()
# 构建现有事件ID集合
existing_ids = {e.id for e in self._events}
new_count = 0
update_count = 0
for event in events:
if event.id in existing_ids:
# 更新现有事件
for i, e in enumerate(self._events):
if e.id == event.id:
self._events[i] = event
update_count += 1
break
else:
# 添加新事件
self._events.append(event)
new_count += 1
# 按时间排序
self._events.sort(key=lambda x: x.publish_time or datetime.min)
total = len(self._events)
print(f"[CalendarStore] 保存事件: 新增{new_count}条, 更新{update_count}条, 当前共{total}")
return {"added": new_count, "updated": update_count, "total": total}
def update_from_mt5(self, events_data: List[Dict]) -> int:
"""
从MT5数据更新财经日历
Args:
events_data: MT5返回的事件列表
Returns:
更新的事件数量
"""
now = datetime.now()
expiry_threshold = now - timedelta(hours=self.EXPIRY_HOURS)
with self._lock:
# 清理过期数据
self._events = [
e for e in self._events
if e.publish_time and e.publish_time > expiry_threshold
]
existing_ids = {e.id for e in self._events}
new_count = 0
update_count = 0
for event_data in events_data:
event = CalendarEvent.from_mt5_data(event_data)
if event is None:
continue
# 跳过过期数据
if event.publish_time and event.publish_time < expiry_threshold:
continue
if event.id in existing_ids:
for i, e in enumerate(self._events):
if e.id == event.id:
self._events[i] = event
update_count += 1
break
else:
self._events.append(event)
new_count += 1
self._events.sort(key=lambda x: x.publish_time or datetime.min)
total = len(self._events)
print(f"[CalendarStore] MT5更新: 新增{new_count}条, 更新{update_count}条, 当前共{total}")
return new_count + update_count
def get_events(self, date_str: str = None) -> List[Dict]:
"""
获取事件列表
Args:
date_str: 日期字符串None返回所有
Returns:
事件字典列表
"""
with self._lock:
if date_str:
filtered = [
e for e in self._events
if e.publish_time and e.publish_time.strftime('%Y-%m-%d') == date_str
]
return [e.to_dict() for e in filtered]
return [e.to_dict() for e in self._events]
def get_event_objects(self, date_str: str = None) -> List[CalendarEvent]:
"""获取事件对象列表"""
with self._lock:
if date_str:
return [
e for e in self._events
if e.publish_time and e.publish_time.strftime('%Y-%m-%d') == date_str
]
return list(self._events)
def get_event_by_id(self, event_id: str) -> Optional[CalendarEvent]:
"""根据ID获取事件"""
with self._lock:
for event in self._events:
if event.id == event_id:
return event
return None
def get_upcoming_events(self, hours: int = 24, min_importance: int = 2) -> List[CalendarEvent]:
"""
获取即将发布的重要事件
Args:
hours: 未来多少小时内
min_importance: 最小重要级别
Returns:
事件列表
"""
now = datetime.now()
upcoming = []
with self._lock:
for event in self._events:
if event.publish_time and event.importance >= min_importance:
delta = event.publish_time - now
if 0 < delta.total_seconds() <= hours * 3600:
upcoming.append(event)
return sorted(upcoming, key=lambda x: x.publish_time)
# ==================== 提醒状态 ====================
def is_alerted(self, event_id: str) -> bool:
"""检查事件是否已提醒"""
return event_id in self._alerted_ids
def mark_alerted(self, event_id: str) -> None:
"""标记事件已提醒"""
self._alerted_ids.add(event_id)
# ==================== 事件结果更新 ====================
def update_event_result(self, event_id: str, actual: str, result: str, impact: Dict) -> bool:
"""
更新事件结果
Args:
event_id: 事件ID
actual: 实际值
result: 结果类型 (better/worse/in_line)
impact: 影响分析
Returns:
是否更新成功
"""
with self._lock:
event = self.get_event_by_id(event_id)
if event:
event.actual = actual
event.result = result
event.impact = impact
event.analyzed = True
print(f"[CalendarStore] 更新事件结果: {event.name}, 实际值={actual}")
return True
return False
# ==================== 清理 ====================
def cleanup_expired(self) -> int:
"""清理过期事件"""
with self._lock:
return self._cleanup_expired()
def _cleanup_expired(self) -> int:
"""内部清理方法(不加锁)"""
now = datetime.now()
expiry_threshold = now - timedelta(hours=self.EXPIRY_HOURS)
before_count = len(self._events)
self._events = [
e for e in self._events
if e.publish_time and e.publish_time > expiry_threshold
]
removed = before_count - len(self._events)
if removed > 0:
print(f"[CalendarStore] 清理过期事件: {removed}")
return removed
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"total_events": len(self._events),
"alerted_events": len(self._alerted_ids)
}
def clear(self) -> None:
"""清空所有数据"""
with self._lock:
self._events.clear()
self._alerted_ids.clear()
print("[CalendarStore] 已清空所有数据")
+132
View File
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
快讯存储模块
存储快讯数据
"""
from collections import deque
from datetime import datetime
from typing import List, Dict, Optional
import threading
from ..models import FlashNews
class FlashNewsStore:
"""快讯存储(只负责数据CRUD"""
# 最大保留数量
MAX_NEWS = 100
def __init__(self):
# 快讯列表,最新的在前
self._news: deque = deque(maxlen=self.MAX_NEWS)
self._lock = threading.RLock()
# 已提醒的快讯ID集合
self._alerted_ids: set = set()
print("[FlashNewsStore] 快讯存储已初始化")
# ==================== 快讯管理 ====================
def add_news(self, news: FlashNews) -> bool:
"""
添加快讯
Args:
news: 快讯对象
Returns:
是否新增False表示已存在
"""
with self._lock:
# 检查是否已存在
for existing in self._news:
if existing.id == news.id:
return False
self._news.appendleft(news)
print(f"[FlashNewsStore] 新增快讯: {news.id}")
return True
def get_news(self, count: int = 20) -> List[Dict]:
"""
获取最新快讯
Args:
count: 获取数量
Returns:
快讯字典列表
"""
with self._lock:
news_list = list(self._news)[:count]
return [n.to_dict() for n in news_list]
def get_news_objects(self, count: int = 20) -> List[FlashNews]:
"""获取快讯对象列表"""
with self._lock:
return list(self._news)[:count]
def get_news_by_id(self, news_id: str) -> Optional[FlashNews]:
"""根据ID获取快讯"""
with self._lock:
for news in self._news:
if news.id == news_id:
return news
return None
# ==================== 提醒状态 ====================
def is_alerted(self, news_id: str) -> bool:
"""检查快讯是否已提醒"""
return news_id in self._alerted_ids
def mark_alerted(self, news_id: str) -> None:
"""标记快讯已提醒"""
self._alerted_ids.add(news_id)
# ==================== 分析结果更新 ====================
def update_analysis(self, news_id: str, speaker: str, speaker_title: str, impact: Dict) -> bool:
"""
更新快讯分析结果
Args:
news_id: 快讯ID
speaker: 发言人
speaker_title: 发言人职位
impact: 影响分析
Returns:
是否更新成功
"""
with self._lock:
for news in self._news:
if news.id == news_id:
news.speaker = speaker
news.speaker_title = speaker_title
news.impact = impact
news.analyzed = True
print(f"[FlashNewsStore] 更新快讯分析: {news_id}, 发言人={speaker}")
return True
return False
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"total_news": len(self._news),
"alerted_news": len(self._alerted_ids)
}
def clear(self) -> None:
"""清空所有数据"""
with self._lock:
self._news.clear()
self._alerted_ids.clear()
print("[FlashNewsStore] 已清空所有数据")
+285
View File
@@ -0,0 +1,285 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
K线数据存储模块
按周期和Symbol存储K线数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
class KlineStore:
"""K线数据存储(只负责数据CRUD,不包含业务逻辑)"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# 各周期最大存储条数
MAX_KLINES = {
'H4': 1500, # 4小时,6个月约1100根,留余量
'H1': 1000, # 1小时,1个月约720根
'M15': 500, # 15分钟,3天约288根
'M5': 400, # 5分钟,24小时288根
'M1': 100 # 1分钟,1小时60根
}
# 各周期时间间隔(秒)
PERIOD_INTERVALS = {
'H4': 4 * 60 * 60, # 4小时
'H1': 1 * 60 * 60, # 1小时
'M15': 15 * 60, # 15分钟
'M5': 5 * 60, # 5分钟
'M1': 1 * 60 # 1分钟
}
def __init__(self):
# 存储结构: {SYMBOL: {PERIOD: [KlineData, ...]}}
# 这里存储的是字典格式,由 KlineService 转换
self._klines = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
# 标记每个symbol每个周期是否已收到全量数据
self._initialized = defaultdict(lambda: defaultdict(bool))
# 记录每个symbol的M1数据最后更新时间(本地时间)
self._m1_update_time = {}
print("[KlineStore] K线存储已初始化")
def save_klines(self, symbol: str, period: str, klines: List[Dict],
is_full: bool = False) -> Dict:
"""
保存K线数据纯存储操作
Args:
symbol: 交易品种
period: 周期
klines: K线字典列表
is_full: 是否为全量数据
Returns:
{"status": "ok", "count": N, "total": M, "is_full": bool}
"""
period = period.upper()
if period not in self.PERIODS:
return {"status": "error", "message": f"不支持的周期: {period}"}
with self._lock:
if is_full:
self._klines[symbol][period] = []
print(f"[KlineStore] 收到 {symbol} {period} 全量数据,清空该周期历史数据")
new_count = 0
update_count = 0
for k in klines:
# 检查是否已存在相同时间戳的数据
existing = self._klines[symbol][period]
ts = self._normalize_timestamp(k.get('timestamp') or k.get('time'))
found_idx = -1
for i, existing_kline in enumerate(existing):
if self._normalize_timestamp(existing_kline.get('timestamp') or existing_kline.get('time')) == ts:
found_idx = i
break
if found_idx >= 0:
existing[found_idx] = k
update_count += 1
else:
existing.append(k)
new_count += 1
# 按时间排序
self._klines[symbol][period].sort(
key=lambda x: self._normalize_timestamp(x.get('timestamp') or x.get('time'))
)
# 限制最大条数
max_count = self.MAX_KLINES.get(period, 500)
if len(self._klines[symbol][period]) > max_count:
self._klines[symbol][period] = self._klines[symbol][period][-max_count:]
self._initialized[symbol][period] = True
if period == 'M1' and (new_count > 0 or update_count > 0):
self._m1_update_time[symbol] = datetime.now()
total = len(self._klines[symbol][period])
print(f"[KlineStore] {symbol} {period} 保存了 {new_count} 条新数据, 更新 {update_count} 条, 当前共 {total}")
return {
"status": "ok",
"count": new_count,
"total": total,
"is_full": is_full
}
def get_klines(self, symbol: str, period: str, count: int = 100) -> List[Dict]:
"""获取K线数据"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period][-count:]
return list(klines)
def get_all_klines(self, symbol: str, period: str) -> List[Dict]:
"""获取所有K线数据"""
period = period.upper()
with self._lock:
return list(self._klines[symbol][period])
def get_latest_price(self, symbol: str) -> Optional[float]:
"""获取最新价格(从K线的最新close,优先M1)"""
with self._lock:
actual_symbol = None
if symbol in self._klines:
actual_symbol = symbol
else:
symbol_base = symbol.replace('#', '')
for s in self._klines:
if s.replace('#', '') == symbol_base:
actual_symbol = s
break
if not actual_symbol:
return None
for period in ['M1', 'M5', 'M15', 'H1', 'H4']:
klines = self._klines[actual_symbol][period]
if klines:
return float(klines[-1].get('close', 0))
return None
def is_initialized(self, symbol: str, period: str) -> bool:
"""检查某个周期的数据是否已初始化"""
period = period.upper()
return self._initialized[symbol][period]
def check_all_initialized(self, symbol: str) -> bool:
"""检查所有周期是否都已初始化"""
return all(self._initialized[symbol][p] for p in self.PERIODS)
def clear_symbol(self, symbol: str):
"""清除某个Symbol的数据"""
with self._lock:
if symbol in self._klines:
del self._klines[symbol]
if symbol in self._initialized:
del self._initialized[symbol]
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
status = {}
for symbol in self._klines:
status[symbol] = {}
for period in self.PERIODS:
count = len(self._klines[symbol][period])
initialized = self._initialized[symbol][period]
status[symbol][period] = {
"count": count,
"initialized": initialized
}
return status
def get_symbols(self) -> List[str]:
"""获取所有有实际数据的symbol列表"""
with self._lock:
symbols = []
for symbol in self._klines:
has_data = False
for period in self.PERIODS:
if len(self._klines[symbol][period]) > 0:
has_data = True
break
if has_data:
symbols.append(symbol)
return symbols
def get_latest_kline_time(self, symbol: str, period: str = 'M1') -> Optional[datetime]:
"""获取指定品种和周期的最新K线时间戳"""
period = period.upper()
with self._lock:
klines = self._klines[symbol][period]
if not klines:
return None
latest_ts = klines[-1].get('timestamp') or klines[-1].get('time')
return self._parse_timestamp(latest_ts)
def check_m1_updated_within(self, symbol: str, seconds: int = 180) -> Dict:
"""检查M1 K线是否在指定秒数内更新"""
with self._lock:
has_m1_data = len(self._klines[symbol]['M1']) > 0
if not has_m1_data:
return {
"has_data": False,
"latest_time": None,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed"
}
latest_time = self.get_latest_kline_time(symbol, 'M1')
update_time = self._m1_update_time.get(symbol)
if update_time is None:
return {
"has_data": True,
"latest_time": latest_time,
"update_time": None,
"seconds_ago": None,
"is_stale": True,
"market_status": "closed"
}
now = datetime.now()
seconds_ago = int((now - update_time).total_seconds())
if seconds_ago > seconds:
market_status = "stale"
else:
market_status = "active"
return {
"has_data": True,
"latest_time": latest_time,
"update_time": update_time,
"seconds_ago": seconds_ago,
"is_stale": seconds_ago > seconds,
"market_status": market_status
}
def get_period_interval(self, period: str) -> int:
"""获取周期时间间隔(秒)"""
return self.PERIOD_INTERVALS.get(period.upper(), 60)
def get_m1_update_time(self, symbol: str) -> Optional[datetime]:
"""获取M1数据最后更新时间"""
return self._m1_update_time.get(symbol)
def _normalize_timestamp(self, ts) -> str:
"""标准化时间戳为字符串"""
if isinstance(ts, datetime):
return ts.strftime("%Y-%m-%d %H:%M:%S")
return str(ts) if ts else ""
def _parse_timestamp(self, ts) -> Optional[datetime]:
"""解析时间戳为datetime对象"""
if ts is None:
return None
if isinstance(ts, datetime):
return ts
ts_str = str(ts)
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
return datetime.strptime(ts_str, fmt)
except:
continue
return None
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LLM 分析结果存储模块
"""
import os
import json
from datetime import datetime
from typing import Dict, Optional, List
import threading
from ..models import LLMConfig, LLMAnalysisResult
class LLMStore:
"""LLM 分析结果存储(只负责数据CRUD)"""
# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "data", "llm_config.json")
# 入场价提醒冷却时间(秒)
ENTRY_ALERT_COOLDOWN = 300 # 5分钟
def __init__(self):
# 分析结果: {SYMBOL: LLMAnalysisResult}
self._analysis_results: Dict[str, LLMAnalysisResult] = {}
self._lock = threading.RLock()
# 配置
self._config = LLMConfig()
# 入场价提醒记录: {(symbol, period, direction, entry_price): datetime}
self._alerted_entries: Dict[tuple, datetime] = {}
self._entry_alert_lock = threading.Lock()
# 最后分析时间
self._last_analysis_time: Optional[str] = None
# 加载配置文件
self._load_config_from_file()
print("[LLMStore] LLM存储已初始化")
# ==================== 配置管理 ====================
def get_config(self) -> LLMConfig:
"""获取配置"""
return self._config
def update_config(self, api_key: str = None, api_base: str = None, model: str = None) -> LLMConfig:
"""更新配置"""
if api_key is not None:
self._config.api_key = api_key
if api_base is not None:
self._config.api_base = api_base
if model is not None:
self._config.model = model
self._save_config_to_file()
return self._config
def _load_config_from_file(self):
"""从文件加载配置"""
try:
if os.path.exists(self.CONFIG_FILE):
with open(self.CONFIG_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
self._config = LLMConfig.from_dict(data)
print(f"[LLMStore] 已从文件加载配置: {self.CONFIG_FILE}")
except Exception as e:
print(f"[LLMStore] 加载配置文件失败: {e}")
def _save_config_to_file(self):
"""保存配置到文件"""
try:
config_dir = os.path.dirname(self.CONFIG_FILE)
os.makedirs(config_dir, exist_ok=True)
data = {
"api_key": self._config.api_key,
"api_base": self._config.api_base,
"model": self._config.model
}
with open(self.CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"[LLMStore] 配置已保存到文件")
except Exception as e:
print(f"[LLMStore] 保存配置文件失败: {e}")
# ==================== 分析结果管理 ====================
def save_analysis(self, result: LLMAnalysisResult):
"""保存分析结果"""
with self._lock:
self._analysis_results[result.symbol] = result
self._last_analysis_time = datetime.now().isoformat()
def save_analysis_dict(self, symbol: str, analysis: Dict):
"""从字典保存分析结果"""
result = LLMAnalysisResult.from_api_response(symbol, analysis)
self.save_analysis(result)
def get_analysis(self, symbol: str = None) -> Optional[Dict]:
"""获取分析结果"""
with self._lock:
if symbol:
result = self._analysis_results.get(symbol)
return result.to_dict() if result else None
return {s: r.to_dict() for s, r in self._analysis_results.items()}
def get_analysis_result(self, symbol: str) -> Optional[LLMAnalysisResult]:
"""获取分析结果对象"""
with self._lock:
return self._analysis_results.get(symbol)
def update_market_status(self, symbol: str, market_status: str, data_stale: bool = False,
stale_seconds: int = None):
"""更新市场状态"""
with self._lock:
if symbol in self._analysis_results:
self._analysis_results[symbol].market_status = market_status
self._analysis_results[symbol].data_stale = data_stale
def set_stale_status(self, symbol: str, stale: bool, seconds_ago: int = None):
"""设置数据过期状态"""
with self._lock:
if symbol in self._analysis_results:
self._analysis_results[symbol].data_stale = stale
def get_analyzed_symbols(self) -> List[str]:
"""获取已分析的品种列表"""
with self._lock:
return list(self._analysis_results.keys())
def get_last_analysis_time(self) -> Optional[str]:
"""获取最后分析时间"""
return self._last_analysis_time
# ==================== 入场价提醒管理 ====================
def check_entry_alert_cooldown(self, symbol: str, period: str, direction: str,
entry_price: float) -> bool:
"""
检查入场价提醒是否在冷却期
Returns:
True 表示可以提醒False 表示在冷却期
"""
key = (symbol, period, direction, entry_price)
current_time = datetime.now()
with self._entry_alert_lock:
if key in self._alerted_entries:
last_alert = self._alerted_entries[key]
elapsed = (current_time - last_alert).total_seconds()
if elapsed < self.ENTRY_ALERT_COOLDOWN:
return False
# 记录提醒时间
self._alerted_entries[key] = current_time
return True
def cleanup_entry_alerts(self):
"""清理过期的入场价提醒记录"""
current_time = datetime.now()
with self._entry_alert_lock:
keys_to_remove = []
for key, alert_time in self._alerted_entries.items():
elapsed = (current_time - alert_time).total_seconds()
if elapsed > self.ENTRY_ALERT_COOLDOWN * 2:
keys_to_remove.append(key)
for key in keys_to_remove:
del self._alerted_entries[key]
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取状态"""
with self._lock:
return {
"enabled": self._config.enabled,
"model": self._config.model,
"api_base": self._config.api_base,
"last_analysis_time": self._last_analysis_time,
"symbols_analyzed": list(self._analysis_results.keys())
}
+212
View File
@@ -0,0 +1,212 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
待确认订单存储模块
"""
from typing import List, Dict, Optional
from datetime import datetime
import threading
from collections import defaultdict
from ..models import PendingOrder
class PendingOrderStore:
"""待确认订单存储(只负责数据CRUD"""
def __init__(self, timeout_seconds: int = 180):
# 按品种分类的订单: {symbol: [PendingOrder, ...]}
self._orders_by_symbol: Dict[str, List[PendingOrder]] = defaultdict(list)
# 按ID索引
self._orders_by_id: Dict[str, PendingOrder] = {}
# 线程锁
self._lock = threading.RLock()
# 超时时间
self.timeout_seconds = timeout_seconds
print("[PendingOrderStore] 待确认订单存储已初始化")
# ==================== 添加订单 ====================
def add_order(self, order: PendingOrder) -> str:
"""
添加待确认订单
Args:
order: 待确认订单对象
Returns:
订单ID
"""
with self._lock:
# 设置超时时间
order.TIMEOUT_SECONDS = self.timeout_seconds
order.expires_at = order.created_at + __import__('datetime').timedelta(seconds=self.timeout_seconds)
# 存储到两个字典
self._orders_by_symbol[order.symbol].append(order)
self._orders_by_id[order.order_id] = order
print(f"[PendingOrderStore] 添加订单: {order.order_id} {order.symbol} {order.action}")
return order.order_id
def add_order_from_dict(self, data: Dict) -> str:
"""从字典添加订单"""
order = PendingOrder.from_dict(data)
return self.add_order(order)
# ==================== 查询订单 ====================
def get_order_by_id(self, order_id: str) -> Optional[PendingOrder]:
"""根据ID获取订单"""
with self._lock:
return self._orders_by_id.get(order_id)
def get_pending_orders(self, symbol: str = None) -> List[PendingOrder]:
"""
获取待确认订单列表
Args:
symbol: 品种None返回所有
Returns:
订单列表
"""
with self._lock:
if symbol:
orders = list(self._orders_by_symbol.get(symbol, []))
else:
orders = list(self._orders_by_id.values())
# 按创建时间倒序
return sorted(orders, key=lambda x: x.created_at, reverse=True)
def get_pending_orders_dict(self, symbol: str = None) -> List[Dict]:
"""获取待确认订单字典列表"""
orders = self.get_pending_orders(symbol)
return [o.to_dict() for o in orders]
def get_pending_count(self, symbol: str = None) -> int:
"""获取待确认订单数量"""
with self._lock:
if symbol:
return len([o for o in self._orders_by_symbol.get(symbol, []) if o.is_pending()])
return len([o for o in self._orders_by_id.values() if o.is_pending()])
# ==================== 更新订单状态 ====================
def confirm_order(self, order_id: str) -> Optional[PendingOrder]:
"""
确认订单
Args:
order_id: 订单ID
Returns:
确认后的订单不存在返回None
"""
with self._lock:
order = self._orders_by_id.get(order_id)
if not order:
return None
# 从存储中移除
symbol = order.symbol
self._orders_by_symbol[symbol] = [
o for o in self._orders_by_symbol[symbol] if o.order_id != order_id
]
del self._orders_by_id[order_id]
# 标记确认
order.confirm()
print(f"[PendingOrderStore] 订单已确认: {order_id}")
return order
def reject_order(self, order_id: str) -> Optional[PendingOrder]:
"""
拒绝订单
Args:
order_id: 订单ID
Returns:
被拒绝的订单不存在返回None
"""
with self._lock:
order = self._orders_by_id.get(order_id)
if not order:
return None
# 从存储中移除
symbol = order.symbol
self._orders_by_symbol[symbol] = [
o for o in self._orders_by_symbol[symbol] if o.order_id != order_id
]
del self._orders_by_id[order_id]
# 标记拒绝
order.reject()
print(f"[PendingOrderStore] 订单已拒绝: {order_id}")
return order
# ==================== 清理过期订单 ====================
def cleanup_expired(self) -> List[PendingOrder]:
"""
清理过期订单
Returns:
过期的订单列表
"""
expired_orders = []
current_time = datetime.now()
with self._lock:
expired_ids = []
for order_id, order in self._orders_by_id.items():
if order.is_expired() and order.status == "pending":
order.mark_expired()
expired_orders.append(order)
expired_ids.append(order_id)
# 从存储中移除
for order in expired_orders:
symbol = order.symbol
self._orders_by_symbol[symbol] = [
o for o in self._orders_by_symbol[symbol] if o.order_id != order.order_id
]
del self._orders_by_id[order.order_id]
if expired_orders:
print(f"[PendingOrderStore] 清理过期订单: {len(expired_orders)}")
return expired_orders
# ==================== 清空 ====================
def clear_all(self) -> int:
"""清空所有待确认订单"""
with self._lock:
count = len(self._orders_by_id)
self._orders_by_symbol.clear()
self._orders_by_id.clear()
print(f"[PendingOrderStore] 已清空所有订单: {count}")
return count
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
pending_count = len([o for o in self._orders_by_id.values() if o.is_pending()])
return {
"total_orders": len(self._orders_by_id),
"pending_count": pending_count,
"symbols": list(self._orders_by_symbol.keys()),
}
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
转折点存储模块
存储和管理转折点数据
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
from ..models import PivotPoint
class PivotStore:
"""转折点存储(只负责数据CRUD"""
def __init__(self):
# 存储转折点: {SYMBOL: {PERIOD: [PivotPoint, ...]}}
self._pivots = defaultdict(lambda: defaultdict(list))
# 转折点时间线: {SYMBOL: {PERIOD: [PivotPoint, ...]}}
self._pivots_timeline = defaultdict(lambda: defaultdict(list))
self._lock = threading.RLock()
print("[PivotStore] 转折点存储已初始化")
def save_pivots(self, symbol: str, period: str, pivots: List[PivotPoint],
timeline: List[PivotPoint] = None):
"""
保存转折点数据
Args:
symbol: 交易品种
period: 周期
pivots: 合并后的转折点列表
timeline: 时间线转折点列表可选
"""
with self._lock:
self._pivots[symbol][period] = list(pivots)
if timeline is not None:
self._pivots_timeline[symbol][period] = list(timeline)
else:
self._pivots_timeline[symbol][period] = list(pivots)
def get_pivots(self, symbol: str, period: str, direction: str = None,
count: int = 50) -> List[Dict]:
"""获取转折点数据"""
with self._lock:
pivots = list(self._pivots[symbol][period])
if direction:
pivots = [p for p in pivots if p.direction == direction]
pivots = sorted(pivots, key=lambda x: str(x.timestamp), reverse=True)[:count]
return [p.to_dict() for p in pivots]
def get_pivot_objects(self, symbol: str, period: str = None) -> List[PivotPoint]:
"""获取转折点对象(用于内部计算)"""
with self._lock:
if period:
return list(self._pivots[symbol][period])
else:
# 返回所有周期
result = []
for p in self._pivots[symbol]:
result.extend(self._pivots[symbol][p])
return result
def get_timeline(self, symbol: str, period: str) -> List[PivotPoint]:
"""获取时间线转折点(用于判断趋势)"""
with self._lock:
return list(self._pivots_timeline[symbol][period])
def get_all_periods(self, symbol: str) -> List[str]:
"""获取有转折点数据的所有周期"""
with self._lock:
return list(self._pivots[symbol].keys())
def clear_symbol(self, symbol: str):
"""清除某个Symbol的转折点数据"""
with self._lock:
if symbol in self._pivots:
del self._pivots[symbol]
if symbol in self._pivots_timeline:
del self._pivots_timeline[symbol]
def get_status(self) -> Dict:
"""获取状态"""
with self._lock:
status = {}
for symbol in self._pivots:
status[symbol] = {}
for period in self._pivots[symbol]:
count = len(self._pivots[symbol][period])
status[symbol][period] = {"pivot_count": count}
return status
def find_nearest_pivot_price(self, symbol: str, direction: str,
current_price: float) -> Optional[float]:
"""
找到离当前价格最近的转折点价格
Args:
symbol: 交易品种
direction: 'high' 'low'
current_price: 当前价格
Returns:
最近的转折点价格如果没有返回None
"""
nearest_price = None
min_distance = float('inf')
total_pivots = 0
filtered_pivots = 0
with self._lock:
for period in self._pivots[symbol]:
pivots = self._pivots[symbol][period]
total_pivots += len(pivots)
for pivot in pivots:
if pivot.direction != direction:
continue
if direction == 'high' and pivot.price <= current_price:
filtered_pivots += 1
continue
if direction == 'low' and pivot.price >= current_price:
filtered_pivots += 1
continue
distance = abs(pivot.price - current_price)
if distance < min_distance:
min_distance = distance
nearest_price = pivot.price
if nearest_price is None and total_pivots > 0:
print(f"[PivotStore] find_nearest_pivot_price: 未找到 {direction} 转折点, "
f"current_price={current_price:.2f}, "
f"total_pivots={total_pivots}, filtered={filtered_pivots}")
return nearest_price
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓数据存储模块
"""
from collections import defaultdict
from datetime import datetime
from typing import List, Dict, Optional
import threading
from ..models.position import PositionData
class PositionStore:
"""
持仓数据存储
EA通过 /ea/positions 上报持仓数据
"""
def __init__(self):
# 存储结构: {symbol: {ticket: PositionData}}
self._positions: Dict[str, Dict[int, PositionData]] = defaultdict(dict)
self._lock = threading.RLock()
# 最后更新时间
self._last_update_time: Dict[str, datetime] = {}
print("[PositionStore] 持仓存储已初始化")
def update(self, symbol: str, positions: List[PositionData]) -> Dict:
"""
更新持仓数据
Args:
symbol: 上报的品种
positions: 持仓列表
Returns:
{"status": "ok", "count": N, "closed": M}
"""
with self._lock:
# 获取当前品种的持仓ticket
current_tickets = set(self._positions[symbol].keys())
new_tickets = set()
for pos in positions:
new_tickets.add(pos.ticket)
# 使用持仓数据中的symbol(可能与上报品种不同)
pos_symbol = pos.symbol or symbol
self._positions[pos_symbol][pos.ticket] = pos
# 删除已平仓的持仓
closed_tickets = current_tickets - new_tickets
for ticket in closed_tickets:
if ticket in self._positions[symbol]:
del self._positions[symbol][ticket]
# 更新最后更新时间
self._last_update_time[symbol] = datetime.now()
result = {
"status": "ok",
"count": len(self._positions[symbol]),
"closed": len(closed_tickets)
}
print(f"[PositionStore] {symbol}: {result['count']} 持仓, {result['closed']} 平仓")
return result
def get(self, symbol: str = None) -> List[PositionData]:
"""获取持仓数据"""
with self._lock:
if symbol:
return list(self._positions[symbol].values())
else:
result = []
for sym in self._positions:
result.extend(self._positions[sym].values())
return result
def get_dict(self, symbol: str = None) -> List[Dict]:
"""获取持仓数据(字典格式)"""
return [p.to_dict() for p in self.get(symbol)]
def get_by_ticket(self, symbol: str, ticket: int) -> Optional[PositionData]:
"""根据ticket获取持仓"""
with self._lock:
return self._positions[symbol].get(ticket)
def remove(self, symbol: str, ticket: int) -> bool:
"""删除持仓"""
with self._lock:
if ticket in self._positions[symbol]:
del self._positions[symbol][ticket]
return True
return False
def get_count(self, symbol: str = None) -> int:
"""获取持仓数量"""
return len(self.get(symbol))
def get_count_by_direction(self, symbol: str, direction: str) -> int:
"""获取指定方向的持仓数量"""
with self._lock:
count = 0
direction = direction.lower()
for pos in self._positions[symbol].values():
if direction == "buy" and pos.is_buy:
count += 1
elif direction == "sell" and pos.is_sell:
count += 1
return count
def get_summary(self, symbol: str = None) -> Dict:
"""获取持仓汇总"""
positions = self.get(symbol)
total_profit = sum(p.profit for p in positions)
buy_count = sum(1 for p in positions if p.is_buy)
sell_count = sum(1 for p in positions if p.is_sell)
return {
"total_count": len(positions),
"total_profit": round(total_profit, 2),
"buy_count": buy_count,
"sell_count": sell_count,
"positions": [p.to_dict() for p in positions],
"last_update": self._last_update_time.get(symbol) or max(self._last_update_time.values(), default=None)
}
def get_symbols(self) -> List[str]:
"""获取所有有持仓的品种"""
with self._lock:
return [s for s in self._positions if self._positions[s]]
def clear_symbol(self, symbol: str) -> None:
"""清除指定品种的持仓"""
with self._lock:
if symbol in self._positions:
del self._positions[symbol]
if symbol in self._last_update_time:
del self._last_update_time[symbol]
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
total_count = sum(len(pos) for pos in self._positions.values())
return {
"total_positions": total_count,
"symbol_count": len([s for s in self._positions if self._positions[s]]),
"symbols": self.get_symbols()
}
+179
View File
@@ -0,0 +1,179 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
信号存储模块
"""
from typing import List, Dict, Optional
from datetime import datetime
import threading
from collections import defaultdict
from ..models import TradingSignal, SignalSource, SignalStatus
class SignalStore:
"""信号存储(只负责数据CRUD"""
def __init__(self, default_ttl: int = 300):
# 按品种分类的信号: {symbol: [TradingSignal, ...]}
self._signals_by_symbol: Dict[str, List[TradingSignal]] = defaultdict(list)
# 按ID索引
self._signals_by_id: Dict[str, TradingSignal] = {}
# 线程锁
self._lock = threading.RLock()
# 默认信号有效期
self.default_ttl = default_ttl
print("[SignalStore] 信号存储已初始化")
# ==================== 添加信号 ====================
def add_signal(self, signal: TradingSignal) -> str:
"""添加信号"""
with self._lock:
signal.DEFAULT_TTL = self.default_ttl
self._signals_by_symbol[signal.symbol].append(signal)
self._signals_by_id[signal.signal_id] = signal
print(f"[SignalStore] 添加信号: {signal.signal_id} {signal.symbol} {signal.action} (来源: {signal.source})")
return signal.signal_id
# ==================== 查询信号 ====================
def get_signal_by_id(self, signal_id: str) -> Optional[TradingSignal]:
"""根据ID获取信号"""
with self._lock:
return self._signals_by_id.get(signal_id)
def get_active_signals(self, symbol: str = None) -> List[TradingSignal]:
"""获取活跃信号"""
with self._lock:
if symbol:
signals = self._signals_by_symbol.get(symbol, [])
else:
signals = list(self._signals_by_id.values())
# 过滤活跃信号
active = [s for s in signals if s.is_active()]
return sorted(active, key=lambda x: x.created_at, reverse=True)
def get_active_signals_by_source(self, symbol: str, source: str) -> List[TradingSignal]:
"""获取指定来源的活跃信号"""
signals = self.get_active_signals(symbol)
return [s for s in signals if s.source == source]
def get_signals_dict(self, symbol: str = None) -> List[Dict]:
"""获取信号字典列表"""
signals = self.get_active_signals(symbol)
return [s.to_dict() for s in signals]
def get_signal_count(self, symbol: str = None, source: str = None) -> int:
"""获取信号数量"""
if source:
return len(self.get_active_signals_by_source(symbol or "", source))
return len(self.get_active_signals(symbol))
# ==================== 更新信号状态 ====================
def mark_signal_used(self, signal_id: str) -> bool:
"""标记信号为已使用"""
with self._lock:
signal = self._signals_by_id.get(signal_id)
if signal:
signal.mark_used()
return True
return False
def mark_signal_expired(self, signal_id: str) -> bool:
"""标记信号为已过期"""
with self._lock:
signal = self._signals_by_id.get(signal_id)
if signal:
signal.mark_expired()
return True
return False
# ==================== 清理过期信号 ====================
def cleanup_expired(self) -> int:
"""清理过期信号"""
with self._lock:
expired_ids = []
for signal_id, signal in self._signals_by_id.items():
if signal.is_expired() and signal.status == SignalStatus.ACTIVE:
signal.mark_expired()
expired_ids.append(signal_id)
# 从存储中移除过期信号
for signal_id in expired_ids:
signal = self._signals_by_id[signal_id]
symbol = signal.symbol
self._signals_by_symbol[symbol] = [
s for s in self._signals_by_symbol[symbol] if s.signal_id != signal_id
]
del self._signals_by_id[signal_id]
if expired_ids:
print(f"[SignalStore] 清理过期信号: {len(expired_ids)}")
return len(expired_ids)
# ==================== 清空 ====================
def clear_by_symbol(self, symbol: str) -> int:
"""清空指定品种的信号"""
with self._lock:
signals = self._signals_by_symbol.get(symbol, [])
count = len(signals)
for signal in signals:
if signal.signal_id in self._signals_by_id:
del self._signals_by_id[signal.signal_id]
if symbol in self._signals_by_symbol:
del self._signals_by_symbol[symbol]
return count
def clear_all(self) -> int:
"""清空所有信号"""
with self._lock:
count = len(self._signals_by_id)
self._signals_by_symbol.clear()
self._signals_by_id.clear()
return count
# ==================== 统计 ====================
def get_signal_stats(self, symbol: str) -> Dict:
"""获取信号统计"""
signals = self.get_active_signals(symbol)
buy_signals = [s for s in signals if s.action == "buy"]
sell_signals = [s for s in signals if s.action == "sell"]
by_source = {}
for source in [SignalSource.PIVOT, SignalSource.KEY_LEVEL, SignalSource.AI_ENTRY]:
by_source[source] = len([s for s in signals if s.source == source])
return {
"symbol": symbol,
"total": len(signals),
"buy_count": len(buy_signals),
"sell_count": len(sell_signals),
"by_source": by_source,
}
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"total_signals": len(self._signals_by_id),
"symbols": list(self._signals_by_symbol.keys()),
}
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
统计数据存储模块
"""
from collections import deque, defaultdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import threading
from ..models.statistics import StatisticsData
class StatisticsStore:
"""
统计数据存储
保留最近的数据用于
1. 获取品种价差
2. 获取账户信息
"""
def __init__(self, max_per_symbol: int = 10, max_total: int = 100):
"""
Args:
max_per_symbol: 每个品种保留的最大记录数
max_total: 总共保留的最大记录数
"""
# 按品种分组存储
self._by_symbol: Dict[str, deque] = defaultdict(lambda: deque(maxlen=max_per_symbol))
# 全局存储(用于获取最新账户信息)
self._all_data: deque = deque(maxlen=max_total)
# 线程锁
self._lock = threading.RLock()
print(f"[StatisticsStore] 统计数据存储已初始化 (max_per_symbol={max_per_symbol}, max_total={max_total})")
def add(self, data: StatisticsData) -> None:
"""添加统计数据"""
with self._lock:
self._by_symbol[data.symbol].append(data)
self._all_data.append(data)
def get_latest(self, symbol: str = None) -> Optional[StatisticsData]:
"""获取最新的统计数据"""
with self._lock:
if symbol:
if self._by_symbol[symbol]:
return self._by_symbol[symbol][-1]
return None
else:
if self._all_data:
return self._all_data[-1]
return None
def get_by_symbol(self, symbol: str, count: int = 10) -> List[StatisticsData]:
"""获取指定品种的统计数据"""
with self._lock:
data = list(self._by_symbol[symbol])[-count:]
return data
def get_spread(self, symbol: str) -> Optional[float]:
"""获取品种价差"""
with self._lock:
# 规范化品种名称(去掉#后缀)
symbol_normalized = symbol.replace('#', '')
for stat in reversed(list(self._all_data)):
stat_normalized = stat.symbol.replace('#', '')
if stat_normalized == symbol_normalized:
if stat.spread > 0:
return stat.spread
return None
def get_account_info(self, symbol: str = None) -> Dict:
"""获取账户信息"""
latest = self.get_latest(symbol)
if not latest:
return {
"balance": 0,
"equity": 0,
"margin_level": 0
}
return {
"balance": latest.balance,
"equity": latest.equity,
"margin_level": latest.margin_level
}
def get_all_recent(self, count: int = 10) -> List[StatisticsData]:
"""获取最近的所有统计数据"""
with self._lock:
return list(self._all_data)[-count:]
def clear(self) -> None:
"""清空数据"""
with self._lock:
self._by_symbol.clear()
self._all_data.clear()
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"total_count": len(self._all_data),
"symbol_count": len(self._by_symbol),
"symbols": list(self._by_symbol.keys())
}
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
策略配置存储模块
"""
from typing import List, Dict, Optional
import threading
import json
import os
from ..models import TradingStrategy
# 配置文件路径
CONFIG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), 'data')
class StrategyStore:
"""策略配置存储"""
def __init__(self):
# 策略配置: {symbol: TradingStrategy}
self._strategies: Dict[str, TradingStrategy] = {}
# 线程锁
self._lock = threading.RLock()
# 从文件加载
self._load_from_file()
print("[StrategyStore] 策略配置存储已初始化")
def _get_config_file(self) -> str:
"""获取配置文件路径"""
return os.path.join(CONFIG_DIR, 'strategy_config.json')
def _load_from_file(self) -> None:
"""从文件加载配置"""
try:
config_file = self._get_config_file()
if os.path.exists(config_file):
with open(config_file, 'r', encoding='utf-8') as f:
data = json.load(f)
for symbol, strategy_data in data.get('strategies', {}).items():
self._strategies[symbol] = TradingStrategy.from_dict(strategy_data)
print(f"[StrategyStore] 从文件加载 {len(self._strategies)} 个策略配置")
except Exception as e:
print(f"[StrategyStore] 加载配置文件失败: {e}")
def save_to_file(self) -> bool:
"""保存配置到文件"""
try:
os.makedirs(CONFIG_DIR, exist_ok=True)
config_file = self._get_config_file()
data = {
"strategies": {
symbol: strategy.to_dict()
for symbol, strategy in self._strategies.items()
}
}
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=2, ensure_ascii=False)
print(f"[StrategyStore] 配置已保存到: {config_file}")
return True
except Exception as e:
print(f"[StrategyStore] 保存配置文件失败: {e}")
return False
# ==================== 策略管理 ====================
def get_strategy(self, symbol: str) -> Optional[TradingStrategy]:
"""获取品种的策略配置"""
with self._lock:
return self._strategies.get(symbol)
def get_or_create_strategy(self, symbol: str) -> TradingStrategy:
"""获取或创建策略配置"""
with self._lock:
if symbol not in self._strategies:
self._strategies[symbol] = TradingStrategy(symbol=symbol)
return self._strategies[symbol]
def set_strategy(self, strategy: TradingStrategy) -> None:
"""设置策略配置"""
with self._lock:
self._strategies[strategy.symbol] = strategy
self.save_to_file()
def update_strategy(self, symbol: str, data: Dict) -> Optional[TradingStrategy]:
"""更新策略配置"""
with self._lock:
strategy = self.get_or_create_strategy(symbol)
strategy.update(data)
self.save_to_file()
return strategy
def delete_strategy(self, symbol: str) -> bool:
"""删除策略配置"""
with self._lock:
if symbol in self._strategies:
del self._strategies[symbol]
self.save_to_file()
return True
return False
# ==================== 查询 ====================
def get_all_strategies(self) -> List[TradingStrategy]:
"""获取所有策略配置"""
with self._lock:
return list(self._strategies.values())
def get_all_strategies_dict(self) -> Dict[str, Dict]:
"""获取所有策略配置字典"""
with self._lock:
return {
symbol: strategy.to_dict()
for symbol, strategy in self._strategies.items()
}
def get_enabled_strategies(self) -> List[TradingStrategy]:
"""获取所有启用的策略"""
with self._lock:
return [s for s in self._strategies.values() if s.enabled]
def get_enabled_symbols(self) -> List[str]:
"""获取所有启用策略的品种"""
with self._lock:
return [symbol for symbol, strategy in self._strategies.items() if strategy.enabled]
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"total_strategies": len(self._strategies),
"enabled_strategies": len(self.get_enabled_strategies()),
"symbols": list(self._strategies.keys()),
}
+91
View File
@@ -0,0 +1,91 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
技术分析结果存储模块
"""
from collections import defaultdict
from typing import Dict, List, Optional
import threading
from ..models import TechTrendState, TechTrendChange
class TechStore:
"""技术分析结果存储(只负责数据CRUD"""
def __init__(self):
# 趋势状态: {SYMBOL: {PERIOD: TechTrendState}}
self._trend_states: Dict[str, Dict[str, TechTrendState]] = defaultdict(lambda: defaultdict(dict))
self._lock = threading.RLock()
# 趋势转换历史: {SYMBOL: [TechTrendChange, ...]}
self._trend_changes: Dict[str, List[TechTrendChange]] = defaultdict(list)
# 最大历史记录数
self.MAX_CHANGES = 20
print("[TechStore] 技术分析存储已初始化")
# ==================== 趋势状态 ====================
def save_trend_state(self, state: TechTrendState):
"""保存趋势状态"""
with self._lock:
self._trend_states[state.symbol][state.period] = state
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
"""获取趋势状态"""
with self._lock:
if period:
state = self._trend_states[symbol].get(period)
return state.to_dict() if state else {}
return {p: s.to_dict() for p, s in self._trend_states[symbol].items()}
def get_trend_state_object(self, symbol: str, period: str) -> Optional[TechTrendState]:
"""获取趋势状态对象"""
with self._lock:
return self._trend_states[symbol].get(period)
def get_all_trend_states(self, symbol: str) -> Dict[str, TechTrendState]:
"""获取某品种所有周期的趋势状态"""
with self._lock:
return dict(self._trend_states[symbol])
# ==================== 趋势转换历史 ====================
def add_trend_change(self, symbol: str, change: TechTrendChange):
"""添加趋势转换记录"""
with self._lock:
self._trend_changes[symbol].append(change)
# 限制数量
if len(self._trend_changes[symbol]) > self.MAX_CHANGES:
self._trend_changes[symbol] = self._trend_changes[symbol][-self.MAX_CHANGES:]
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
"""获取趋势转换历史"""
with self._lock:
changes = self._trend_changes[symbol][-count:]
return [c.to_dict() for c in changes]
# ==================== 清理 ====================
def clear_symbol(self, symbol: str):
"""清除某品种的数据"""
with self._lock:
if symbol in self._trend_states:
del self._trend_states[symbol]
if symbol in self._trend_changes:
del self._trend_changes[symbol]
# ==================== 状态 ====================
def get_status(self) -> Dict:
"""获取状态"""
with self._lock:
symbols = list(self._trend_states.keys())
total_states = sum(len(periods) for periods in self._trend_states.values())
return {
"symbols": symbols,
"total_states": total_states
}
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易历史存储模块
"""
from collections import defaultdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import threading
from ..models.trade_history import TradeDeal
class TradeHistoryStore:
"""
交易历史存储
EA通过 /trade_history 上报成交记录
"""
def __init__(self, retention_hours: int = 24):
"""
Args:
retention_hours: 数据保留时长小时
"""
self._deals: List[TradeDeal] = []
self._lock = threading.RLock()
self._retention_hours = retention_hours
self._last_update_time: Optional[datetime] = None
print(f"[TradeHistoryStore] 交易历史存储已初始化 (retention={retention_hours}h)")
def add(self, deals: List[TradeDeal]) -> int:
"""
添加成交记录
Args:
deals: 成交记录列表
Returns:
新增记录数
"""
if not deals:
return 0
now = datetime.now()
new_count = 0
with self._lock:
# 获取已有ticket
existing_tickets = {d.ticket for d in self._deals}
for deal in deals:
if deal.ticket not in existing_tickets:
self._deals.append(deal)
new_count += 1
# 按时间排序
self._deals.sort(key=lambda d: d.time or datetime.min, reverse=True)
# 清理过期数据
cutoff = now - timedelta(hours=self._retention_hours)
self._deals = [d for d in self._deals if d.time and d.time > cutoff]
self._last_update_time = now
if new_count > 0:
print(f"[TradeHistoryStore] 新增 {new_count} 条记录,当前共 {len(self._deals)}")
return new_count
def get(self, symbol: str = None, hours: int = None) -> List[TradeDeal]:
"""
获取成交记录
Args:
symbol: 品种None表示所有品种
hours: 最近N小时None表示使用默认值
Returns:
成交记录列表
"""
with self._lock:
deals = self._deals
if symbol:
deals = [d for d in deals if d.symbol == symbol]
if hours:
cutoff = datetime.now() - timedelta(hours=hours)
deals = [d for d in deals if d.time and d.time > cutoff]
return deals
def get_dict(self, symbol: str = None, hours: int = None) -> List[Dict]:
"""获取成交记录(字典格式)"""
return [d.to_dict() for d in self.get(symbol, hours)]
def get_statistics(self, symbol: str = None) -> Dict:
"""
获取交易统计
Returns:
统计数据
"""
deals = self.get(symbol)
if not deals:
return {
"total_count": 0,
"symbols": {},
"manual_count": 0,
"auto_count": 0,
"sl_tp_count": 0,
"so_count": 0,
"total_profit": 0,
"total_swap": 0,
"total_commission": 0,
"net_profit": 0
}
# 按品种统计
symbols = defaultdict(lambda: {"count": 0, "profit": 0, "volume": 0})
# 分类统计
manual_count = 0
auto_count = 0
sl_tp_count = 0
so_count = 0
total_profit = 0
total_swap = 0
total_commission = 0
for deal in deals:
# 品种统计
symbols[deal.symbol]["count"] += 1
symbols[deal.symbol]["profit"] += deal.profit
symbols[deal.symbol]["volume"] += deal.volume
# 分类统计
source = deal.order_source
if source == "手动":
manual_count += 1
elif source in ["止损触发", "止盈触发"]:
sl_tp_count += 1
elif source == "强制平仓":
so_count += 1
else:
auto_count += 1
# 总计
total_profit += deal.profit
total_swap += deal.swap
total_commission += deal.commission
# 转换symbols为普通字典
symbols_dict = {}
for sym, data in symbols.items():
symbols_dict[sym] = {
"count": data["count"],
"profit": round(data["profit"], 2),
"volume": round(data["volume"], 2)
}
return {
"total_count": len(deals),
"symbols": symbols_dict,
"manual_count": manual_count,
"auto_count": auto_count,
"sl_tp_count": sl_tp_count,
"so_count": so_count,
"total_profit": round(total_profit, 2),
"total_swap": round(total_swap, 2),
"total_commission": round(total_commission, 2),
"net_profit": round(total_profit + total_swap - total_commission, 2),
"last_update": self._last_update_time.isoformat() if self._last_update_time else None
}
def get_recent_profit(self, symbol: str = None, hours: int = 24) -> float:
"""获取最近N小时的盈亏"""
deals = self.get(symbol, hours)
return sum(d.profit for d in deals)
def clear(self) -> None:
"""清空数据"""
with self._lock:
self._deals.clear()
self._last_update_time = None
print("[TradeHistoryStore] 已清空")
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"deals_count": len(self._deals),
"last_update": self._last_update_time.isoformat() if self._last_update_time else None
}
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易指令存储模块
"""
from typing import List, Dict, Optional
from datetime import datetime
import threading
from collections import defaultdict
from ..models import TradingInstruction
class TradingInstructionStore:
"""交易指令存储(只负责数据CRUD"""
def __init__(self):
# 按品种分类的指令: {symbol: [TradingInstruction, ...]}
self._instructions_by_symbol: Dict[str, List[TradingInstruction]] = defaultdict(list)
# 按ID索引
self._instructions_by_id: Dict[str, TradingInstruction] = {}
# 线程锁
self._lock = threading.RLock()
print("[TradingInstructionStore] 交易指令存储已初始化")
# ==================== 添加指令 ====================
def add_instruction(self, instruction: TradingInstruction) -> str:
"""
添加交易指令
Args:
instruction: 交易指令对象
Returns:
指令ID
"""
with self._lock:
symbol = instruction.symbol.upper()
# 存储到两个字典
self._instructions_by_symbol[symbol].append(instruction)
self._instructions_by_id[instruction.instruction_id] = instruction
print(f"[TradingInstructionStore] 添加指令: {instruction.instruction_id} {symbol} {instruction.action}")
return instruction.instruction_id
def add_instruction_from_dict(self, data: Dict) -> str:
"""从字典添加指令"""
instruction = TradingInstruction.from_dict(data)
return self.add_instruction(instruction)
def add_instructions_batch(self, instructions: List[TradingInstruction]) -> int:
"""
批量添加指令
Args:
instructions: 指令列表
Returns:
添加数量
"""
count = 0
for inst in instructions:
self.add_instruction(inst)
count += 1
return count
# ==================== 获取指令 ====================
def get_instruction_by_id(self, instruction_id: str) -> Optional[TradingInstruction]:
"""根据ID获取指令"""
with self._lock:
return self._instructions_by_id.get(instruction_id)
def get_instructions_by_symbol(self, symbol: str) -> List[TradingInstruction]:
"""获取指定品种的指令列表"""
with self._lock:
return list(self._instructions_by_symbol.get(symbol.upper(), []))
def get_all_instructions(self) -> List[TradingInstruction]:
"""获取所有指令"""
with self._lock:
return list(self._instructions_by_id.values())
def get_all_instructions_dict(self) -> Dict[str, List[Dict]]:
"""
获取所有指令按品种分类
Returns:
{symbol: [instruction_dict, ...]}
"""
with self._lock:
result = {}
for symbol, instructions in self._instructions_by_symbol.items():
result[symbol] = [inst.to_dict() for inst in instructions]
return result
# ==================== 获取并发送指令(EA调用)====================
def fetch_and_remove_by_symbol(self, symbol: str, current_price: float = None) -> List[Dict]:
"""
获取满足条件的指令并移除EA轮询时调用
价格过滤逻辑
- 买入指令指令价格 <= 当前价格 发送
- 卖出指令指令价格 >= 当前价格 发送
Args:
symbol: 品种
current_price: 当前价格None时不做价格过滤
Returns:
满足条件的指令列表字典格式用于返回给EA
"""
with self._lock:
symbol = symbol.upper()
instructions = self._instructions_by_symbol.get(symbol, [])
if not instructions:
return []
result = []
remaining = []
for inst in instructions:
should_send = True
# 价格条件过滤
if current_price is not None:
if inst.action.lower() == 'b':
# 买入:指令价格需要 <= 当前价格
if inst.price > current_price:
should_send = False
elif inst.action.lower() == 's':
# 卖出:指令价格需要 >= 当前价格
if inst.price < current_price:
should_send = False
if should_send:
inst.status = "sent"
inst.sent_at = datetime.now()
result.append(inst.to_dict()) # 返回给EA的格式
del self._instructions_by_id[inst.instruction_id]
else:
remaining.append(inst)
# 更新存储
self._instructions_by_symbol[symbol] = remaining
if result:
print(f"[TradingInstructionStore] 发送指令给EA: {symbol} {len(result)}条 (当前价格: {current_price})")
if remaining:
print(f"[TradingInstructionStore] 缓存指令等待条件: {symbol} {len(remaining)}")
return result
# ==================== 移除指令 ====================
def remove_instruction(self, instruction_id: str) -> Optional[TradingInstruction]:
"""移除指定指令"""
with self._lock:
instruction = self._instructions_by_id.get(instruction_id)
if not instruction:
return None
symbol = instruction.symbol.upper()
self._instructions_by_symbol[symbol] = [
i for i in self._instructions_by_symbol[symbol] if i.instruction_id != instruction_id
]
del self._instructions_by_id[instruction_id]
return instruction
def clear_by_symbol(self, symbol: str) -> int:
"""清空指定品种的指令"""
with self._lock:
symbol = symbol.upper()
instructions = self._instructions_by_symbol.get(symbol, [])
count = len(instructions)
for inst in instructions:
if inst.instruction_id in self._instructions_by_id:
del self._instructions_by_id[inst.instruction_id]
if symbol in self._instructions_by_symbol:
del self._instructions_by_symbol[symbol]
print(f"[TradingInstructionStore] 清空 {symbol} 指令: {count}")
return count
def clear_all(self) -> int:
"""清空所有指令"""
with self._lock:
count = len(self._instructions_by_id)
self._instructions_by_symbol.clear()
self._instructions_by_id.clear()
print(f"[TradingInstructionStore] 已清空所有指令: {count}")
return count
# ==================== 统计 ====================
def get_count_by_symbol(self, symbol: str) -> int:
"""获取指定品种的指令数量"""
with self._lock:
return len(self._instructions_by_symbol.get(symbol.upper(), []))
def get_total_count(self) -> int:
"""获取总指令数量"""
with self._lock:
return len(self._instructions_by_id)
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
symbols_count = {symbol: len(instructions)
for symbol, instructions in self._instructions_by_symbol.items()}
return {
"total_instructions": len(self._instructions_by_id),
"symbols": symbols_count,
}
+144
View File
@@ -0,0 +1,144 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易配置模块
"""
from typing import Dict, List
import threading
import json
import os
# 配置文件路径
CONFIG_FILE = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'trade_config.json')
class TradeConfig:
"""
交易配置单例
管理交易相关的配置参数包括
- 默认手数止损偏移
- MT5时区偏移
- 品种配置手数止损偏移关键点位等
"""
_instance = None
_lock = threading.Lock()
def __init__(self):
self.enabled = True # 是否启用自动生成
# 默认配置
self.default_volume = 0.01 # 默认手数
self.default_sl_offset = 0.05 # 默认止损偏移(固定点数)
# MT5服务器时区偏移(单位:小时)
# 正数表示MT5时间比本地时间快,负数表示比本地时间慢
# 例如:MT5服务器时间是GMT+2,本地时间是GMT+8,则偏移为 -6
self.mt5_timezone_offset = 0
# 按品种配置: {symbol: {"volume": 0.01, "sl_offset": 0.05, "key_levels": "5000,5100", "key_level_threshold": 0.0008}}
self.symbol_config = {
"GOLD#": {"volume": 0.01, "sl_offset": 0.5},
"OILCASH#": {"volume": 0.01, "sl_offset": 0.05},
}
# 启动时自动加载配置文件
self._load_from_file()
def _load_from_file(self):
"""从配置文件加载配置"""
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
data = json.load(f)
self.update(data)
print(f"[TradeConfig] 已从配置文件加载: mt5_timezone_offset={self.mt5_timezone_offset}")
else:
print(f"[TradeConfig] 配置文件不存在: {CONFIG_FILE},使用默认配置")
except Exception as e:
print(f"[TradeConfig] 加载配置文件失败: {e},使用默认配置")
def save_to_file(self):
"""保存配置到文件"""
try:
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(self.to_dict(), f, indent=2, ensure_ascii=False)
print(f"[TradeConfig] 配置已保存到: {CONFIG_FILE}")
return True
except Exception as e:
print(f"[TradeConfig] 保存配置文件失败: {e}")
return False
@classmethod
def get_instance(cls):
if cls._instance is None:
with cls._lock:
if cls._instance is None:
cls._instance = cls()
return cls._instance
def get_symbol_config(self, symbol: str) -> Dict:
"""获取品种配置,如果未配置则返回默认值"""
if symbol in self.symbol_config:
config = self.symbol_config[symbol]
return {
"volume": config.get("volume", self.default_volume),
"sl_offset": config.get("sl_offset", self.default_sl_offset),
"key_levels": config.get("key_levels", ""),
"key_level_threshold": config.get("key_level_threshold", 0.0008)
}
return {
"volume": self.default_volume,
"sl_offset": self.default_sl_offset,
"key_levels": "",
"key_level_threshold": 0.0008
}
def get_key_levels(self, symbol: str) -> List[float]:
"""
获取品种的关键点位列表
Args:
symbol: 品种名称
Returns:
关键点位列表 [5000, 5100, 5200]
"""
config = self.get_symbol_config(symbol)
key_levels_str = config.get("key_levels", "")
if not key_levels_str:
return []
levels = []
for level_str in key_levels_str.split(","):
level_str = level_str.strip()
if level_str:
try:
levels.append(float(level_str))
except ValueError:
continue
return sorted(levels)
def to_dict(self) -> Dict:
return {
"enabled": self.enabled,
"default_volume": self.default_volume,
"default_sl_offset": self.default_sl_offset,
"mt5_timezone_offset": self.mt5_timezone_offset,
"symbol_config": self.symbol_config
}
def update(self, data: Dict):
if "enabled" in data:
self.enabled = bool(data["enabled"])
if "default_volume" in data:
self.default_volume = float(data["default_volume"])
if "default_sl_offset" in data:
self.default_sl_offset = float(data["default_sl_offset"])
if "mt5_timezone_offset" in data:
self.mt5_timezone_offset = float(data["mt5_timezone_offset"])
if "symbol_config" in data:
self.symbol_config = data["symbol_config"]
-302
View File
@@ -1,302 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
交易历史存储模块
存储EA上报的交易历史数据
"""
from collections import defaultdict
from datetime import datetime, timedelta
from typing import List, Dict, Optional
import threading
from dataclasses import dataclass, field
@dataclass
class TradeDeal:
"""成交记录"""
ticket: int
order: int
symbol: str
type: int # 0=买入, 1=卖出
entry: int # 0=开仓, 1=平仓, 2=反向
volume: float
price: float
profit: float
swap: float
commission: float
time: datetime
comment: str
def to_dict(self) -> Dict:
return {
"ticket": self.ticket,
"order": self.order,
"symbol": self.symbol,
"type": self.type,
"type_text": "买入" if self.type == 0 else "卖出",
"entry": self.entry,
"entry_text": self._get_entry_text(),
"volume": self.volume,
"price": self.price,
"profit": self.profit,
"swap": self.swap,
"commission": self.commission,
"time": self.time.strftime("%Y-%m-%d %H:%M:%S") if self.time else None,
"comment": self.comment,
"is_auto": self._is_auto_order(),
"order_source": self._get_order_source()
}
def _get_entry_text(self) -> str:
if self.entry == 0:
return "开仓"
elif self.entry == 1:
return "平仓"
elif self.entry == 2:
return "反向"
else:
return "未知"
def _is_auto_order(self) -> bool:
"""判断是否为自动下单(排除MT5系统标记)"""
if not self.comment or not self.comment.strip():
return False
# 排除MT5系统标记
comment = self.comment.strip()
if comment.startswith('[sl') or comment.startswith('[tp') or comment.startswith('[so'):
return False
return True
def _get_order_source(self) -> str:
"""获取订单来源"""
if not self.comment or not self.comment.strip():
return "手动"
comment = self.comment.strip()
if comment.startswith('[sl'):
return "止损触发"
if comment.startswith('[tp'):
return "止盈触发"
if comment.startswith('[so'):
return "强制平仓"
return "自动"
class TradeHistoryStore:
"""交易历史存储"""
def __init__(self):
# 存储成交记录
self._deals: List[TradeDeal] = []
self._lock = threading.RLock()
# 上次更新时间
self._last_update_time: Optional[datetime] = None
print("[TradeHistoryStore] 交易历史存储已初始化")
def update_from_ea(self, deals_data: List[Dict]) -> int:
"""
从EA数据更新交易历史
Args:
deals_data: EA返回的成交列表
Returns:
更新的成交数量
"""
if not deals_data:
return 0
now = datetime.now()
new_deals = []
with self._lock:
# 获取现有票据集合
existing_tickets = {d.ticket for d in self._deals}
for deal_data in deals_data:
ticket = deal_data.get('ticket')
if ticket in existing_tickets:
continue
# 解析时间
deal_time = deal_data.get('time')
if isinstance(deal_time, str):
try:
deal_time = datetime.strptime(deal_time, '%Y.%m.%d %H:%M:%S')
except:
try:
deal_time = datetime.strptime(deal_time, '%Y-%m-%d %H:%M:%S')
except:
deal_time = now
elif not isinstance(deal_time, datetime):
deal_time = now
deal = TradeDeal(
ticket=ticket,
order=deal_data.get('order', 0),
symbol=deal_data.get('symbol', ''),
type=deal_data.get('type', 0),
entry=deal_data.get('entry', 0),
volume=deal_data.get('volume', 0),
price=deal_data.get('price', 0),
profit=deal_data.get('profit', 0),
swap=deal_data.get('swap', 0),
commission=deal_data.get('commission', 0),
time=deal_time,
comment=deal_data.get('comment', '')
)
new_deals.append(deal)
# 添加新记录
self._deals.extend(new_deals)
# 按时间排序
self._deals.sort(key=lambda d: d.time or datetime.min, reverse=True)
# 保留最近24小时的数据
cutoff = now - timedelta(hours=24)
self._deals = [d for d in self._deals if d.time and d.time > cutoff]
self._last_update_time = now
if new_deals:
print(f"[TradeHistoryStore] 新增 {len(new_deals)} 条成交记录,当前共 {len(self._deals)}")
return len(new_deals)
def get_all_deals(self) -> List[Dict]:
"""获取所有成交记录"""
with self._lock:
return [d.to_dict() for d in self._deals]
def get_statistics(self) -> Dict:
"""
获取交易统计
Returns:
统计数据
"""
with self._lock:
total_count = len(self._deals)
if total_count == 0:
return {
"total_count": 0,
"symbols": {},
"manual_count": 0,
"auto_count": 0,
"sl_tp_count": 0,
"so_count": 0,
"auto_categories": {},
"total_profit": 0,
"total_swap": 0,
"total_commission": 0,
"net_profit": 0,
"last_update": None
}
# 按品种统计
symbols = defaultdict(lambda: {"count": 0, "profit": 0, "volume": 0})
# 手动/自动/止损止盈/强制平仓统计
manual_count = 0
auto_count = 0
sl_tp_count = 0 # 止损/止盈触发
so_count = 0 # 强制平仓
auto_categories = defaultdict(lambda: {"count": 0, "profit": 0})
total_profit = 0
total_swap = 0
total_commission = 0
for deal in self._deals:
# 品种统计
symbols[deal.symbol]["count"] += 1
symbols[deal.symbol]["profit"] += deal.profit
symbols[deal.symbol]["volume"] += deal.volume
# 分类统计
comment = deal.comment.strip() if deal.comment else ""
if not comment:
# 无备注:手动单
manual_count += 1
elif comment.startswith('[sl') or comment.startswith('[tp'):
# 止损/止盈触发
sl_tp_count += 1
elif comment.startswith('[so'):
# 强制平仓
so_count += 1
else:
# 自动单:使用完整备注作为分类
auto_count += 1
auto_categories[comment]["count"] += 1
auto_categories[comment]["profit"] += deal.profit
# 总计
total_profit += deal.profit
total_swap += deal.swap
total_commission += deal.commission
net_profit = total_profit + total_swap - total_commission
# 转换auto_categories为普通字典并计算
auto_categories_dict = {}
for cat, data in auto_categories.items():
auto_categories_dict[cat] = {
"count": data["count"],
"profit": round(data["profit"], 2),
"percentage": round(data["count"] / auto_count * 100, 1) if auto_count > 0 else 0
}
# 转换symbols为普通字典
symbols_dict = {}
for sym, data in symbols.items():
symbols_dict[sym] = {
"count": data["count"],
"profit": round(data["profit"], 2),
"volume": round(data["volume"], 2)
}
return {
"total_count": total_count,
"symbols": symbols_dict,
"manual_count": manual_count,
"auto_count": auto_count,
"sl_tp_count": sl_tp_count,
"so_count": so_count,
"auto_categories": auto_categories_dict,
"total_profit": round(total_profit, 2),
"total_swap": round(total_swap, 2),
"total_commission": round(total_commission, 2),
"net_profit": round(net_profit, 2),
"last_update": self._last_update_time.isoformat() if self._last_update_time else None
}
def get_status(self) -> Dict:
"""获取存储状态"""
with self._lock:
return {
"deals_count": len(self._deals),
"last_update": self._last_update_time.isoformat() if self._last_update_time else None
}
def clear(self) -> None:
"""清空数据"""
with self._lock:
self._deals.clear()
self._last_update_time = None
print("[TradeHistoryStore] 已清空交易历史数据")
# 全局单例
_trade_history_store = None
def get_trade_history_store() -> TradeHistoryStore:
"""获取交易历史存储单例"""
global _trade_history_store
if _trade_history_store is None:
_trade_history_store = TradeHistoryStore()
return _trade_history_store
-416
View File
@@ -1,416 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
趋势分析模块
基于均线和ADX判断趋势方向和强度
"""
from collections import defaultdict
from typing import List, Dict, Optional
from datetime import datetime
import threading
from .store import KlineData
class TrendAnalyzer:
"""趋势分析器"""
# 支持的周期
PERIODS = ['H4', 'H1', 'M15', 'M5', 'M1']
# ADX阈值
ADX_TREND_THRESHOLD = 25 # ADX > 25 表示有趋势
ADX_STRONG_THRESHOLD = 40 # ADX > 40 表示强趋势
# 均线周期
MA_FAST = 10 # 快线周期
MA_SLOW = 20 # 慢线周期
def __init__(self):
# 存储各周期趋势状态: {SYMBOL: {PERIOD: TrendState}}
self._trend_states = defaultdict(lambda: defaultdict(dict))
self._lock = threading.RLock()
# 趋势转换历史
self._trend_changes = defaultdict(list)
# 统计数据历史引用(用于获取价差)
self._statistics_history = None
print("[TrendAnalyzer] 趋势分析器已初始化")
def set_statistics_history(self, statistics_history):
"""设置统计数据历史引用(用于获取价差)"""
self._statistics_history = statistics_history
def _get_symbol_spread(self, symbol: str) -> Optional[float]:
"""
获取指定品种的最新价差
Args:
symbol: 品种名称
Returns:
价差金额如果没有返回None
"""
if not self._statistics_history:
return None
symbol_normalized = symbol.replace('#', '')
# 从最新的统计数据中查找该品种的价差
for stat in reversed(list(self._statistics_history)):
stat_symbol = stat.get('symbol', '')
stat_normalized = stat_symbol.replace('#', '')
if stat_normalized == symbol_normalized:
spread = stat.get('spread')
if spread is not None and spread > 0:
return spread
return None
def analyze_trend(self, symbol: str, period: str, klines: List[KlineData]) -> Dict:
"""
分析单个周期的趋势
Args:
symbol: 交易品种
period: 周期
klines: K线数据
Returns:
{
"trend": "up" / "down" / "sideways",
"strength": 0-100,
"adx": float,
"ma_fast": float,
"ma_slow": float,
"price": float,
"change_signal": bool, # 是否发生趋势转换
"timestamp": str
}
"""
if len(klines) < 30: # 至少需要30根K线
return {
"trend": "unknown",
"strength": 0,
"adx": 0,
"ma_fast": 0,
"ma_slow": 0,
"price": 0,
"change_signal": False,
"reason": "K线数据不足(需≥30根)",
"timestamp": datetime.now().isoformat()
}
# 计算均线
closes = [k.close for k in klines]
ma_fast = self._calculate_ma(closes, self.MA_FAST)
ma_slow = self._calculate_ma(closes, self.MA_SLOW)
current_price = closes[-1]
# 计算ADX
adx = self._calculate_adx(klines)
# 判断趋势方向和原因
reason_parts = []
if adx < self.ADX_TREND_THRESHOLD:
# ADX较低,震荡行情
trend = "sideways"
reason_parts.append(f"ADX={adx:.1f}<25 无明显趋势")
else:
# 根据均线和价格判断方向
if ma_fast > ma_slow and current_price > ma_fast:
trend = "up"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({current_price:.2f}) > MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
elif ma_fast < ma_slow and current_price < ma_fast:
trend = "down"
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"价格({current_price:.2f}) < MA{self.MA_FAST}")
reason_parts.append(f"ADX={adx:.1f}≥25 确认趋势")
else:
trend = "sideways"
if ma_fast > ma_slow:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) > MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"但价格({current_price:.2f})低于MA{self.MA_FAST}")
else:
reason_parts.append(f"MA{self.MA_FAST}({ma_fast:.2f}) < MA{self.MA_SLOW}({ma_slow:.2f})")
reason_parts.append(f"且价格({current_price:.2f})高于MA{self.MA_FAST}")
reason_parts.append("信号矛盾,判定震荡")
reason = "".join(reason_parts)
# 计算趋势强度 (基于ADX)
if adx >= self.ADX_STRONG_THRESHOLD:
strength = min(100, int(adx + 20))
elif adx >= self.ADX_TREND_THRESHOLD:
strength = int(adx + 10)
else:
strength = int(adx)
# 检查趋势转换
symbol_key = symbol
change_signal = False
previous_trend = None
with self._lock:
if period in self._trend_states[symbol_key]:
previous_trend = self._trend_states[symbol_key][period].get('trend')
if previous_trend and previous_trend != trend and previous_trend != "unknown":
change_signal = True
# 记录转换历史
self._trend_changes[symbol_key].append({
"period": period,
"from_trend": previous_trend,
"to_trend": trend,
"timestamp": datetime.now().isoformat(),
"price": current_price
})
# 只保留最近20条
if len(self._trend_changes[symbol_key]) > 20:
self._trend_changes[symbol_key] = self._trend_changes[symbol_key][-20:]
# 更新状态
self._trend_states[symbol_key][period] = {
"trend": trend,
"strength": strength,
"adx": round(adx, 2),
"ma_fast": round(ma_fast, 4),
"ma_slow": round(ma_slow, 4),
"price": current_price,
"change_signal": change_signal,
"previous_trend": previous_trend,
"reason": reason,
"timestamp": datetime.now().isoformat()
}
return self._trend_states[symbol_key][period]
def analyze_resonance(self, symbol: str) -> Dict:
"""
分析多周期共振
Returns:
{
"resonance": "up" / "down" / "none",
"strength": 0-100,
"periods": {period: trend_state},
"aligned_count": int,
"signal": str
}
"""
symbol_key = symbol
with self._lock:
states = dict(self._trend_states[symbol_key])
if not states:
return {
"resonance": "none",
"strength": 0,
"periods": {},
"aligned_count": 0,
"signal": "等待数据"
}
# 统计各趋势数量
up_count = sum(1 for s in states.values() if s.get('trend') == 'up')
down_count = sum(1 for s in states.values() if s.get('trend') == 'down')
sideways_count = sum(1 for s in states.values() if s.get('trend') == 'sideways')
# 计算平均强度
strengths = [s.get('strength', 0) for s in states.values() if s.get('trend') != 'sideways']
avg_strength = sum(strengths) / len(strengths) if strengths else 0
# 判断共振
total = len(states)
if up_count >= total * 0.6: # 60%以上周期趋势一致
resonance = "up"
aligned_count = up_count
signal = f"多周期向上共振 ({up_count}/{total})"
elif down_count >= total * 0.6:
resonance = "down"
aligned_count = down_count
signal = f"多周期向下共振 ({down_count}/{total})"
else:
resonance = "none"
aligned_count = max(up_count, down_count)
signal = f"趋势分歧 (↑{up_count}{down_count}{sideways_count})"
return {
"resonance": resonance,
"strength": int(avg_strength),
"periods": states,
"aligned_count": aligned_count,
"up_count": up_count,
"down_count": down_count,
"sideways_count": sideways_count,
"signal": signal
}
def get_trend_state(self, symbol: str, period: str = None) -> Dict:
"""获取趋势状态"""
with self._lock:
if period:
return self._trend_states[symbol].get(period, {})
return dict(self._trend_states[symbol])
def get_trend_changes(self, symbol: str, count: int = 10) -> List[Dict]:
"""获取趋势转换历史"""
with self._lock:
return self._trend_changes[symbol][-count:]
def _calculate_ma(self, data: List[float], period: int) -> float:
"""计算移动平均线"""
if len(data) < period:
return data[-1] if data else 0
return sum(data[-period:]) / period
def _calculate_adx(self, klines: List[KlineData], period: int = 14) -> float:
"""
计算ADX (Average Directional Index)
ADX > 25: 有趋势
ADX > 40: 强趋势
ADX < 20: 无明显趋势
"""
if len(klines) < period + 1:
return 0
# 计算 +DM 和 -DM
plus_dm = []
minus_dm = []
tr_list = []
for i in range(1, len(klines)):
high = klines[i].high
low = klines[i].low
prev_high = klines[i-1].high
prev_low = klines[i-1].low
prev_close = klines[i-1].close
# +DM
up_move = high - prev_high
down_move = prev_low - low
if up_move > down_move and up_move > 0:
plus_dm.append(up_move)
else:
plus_dm.append(0)
# -DM
if down_move > up_move and down_move > 0:
minus_dm.append(down_move)
else:
minus_dm.append(0)
# True Range
tr = max(
high - low,
abs(high - prev_close),
abs(low - prev_close)
)
tr_list.append(tr)
if len(tr_list) < period:
return 0
# 计算平滑值
atr = sum(tr_list[-period:]) / period
smoothed_plus_dm = sum(plus_dm[-period:]) / period
smoothed_minus_dm = sum(minus_dm[-period:]) / period
# 计算 +DI 和 -DI
if atr == 0:
return 0
plus_di = (smoothed_plus_dm / atr) * 100
minus_di = (smoothed_minus_dm / atr) * 100
# 计算 DX
di_sum = plus_di + minus_di
if di_sum == 0:
return 0
dx = abs(plus_di - minus_di) / di_sum * 100
return dx
def generate_trade_suggestion(self, symbol: str, pivots: List[Dict],
current_price: float) -> Optional[Dict]:
"""
基于趋势和转折点生成交易建议
Args:
symbol: 交易品种
pivots: 转折点数据
current_price: 当前价格
Returns:
交易建议 None
"""
# 获取趋势状态
resonance = self.analyze_resonance(symbol)
if resonance['resonance'] == 'none':
return None
if resonance['strength'] < 30:
return None
trend = resonance['resonance']
# 根据趋势找最近的转折点作为止损止盈
recent_pivots = sorted(pivots, key=lambda x: x['timestamp'], reverse=True)[:10]
sl = None
tp = None
action = None
reason = ""
if trend == "up":
# 上升趋势,找最近的低点作为止损
action = "b"
low_pivots = [p for p in recent_pivots if p['direction'] == 'low']
if low_pivots:
# 找最近的低点作为止损
sl = low_pivots[0]['price']
# 止盈设为止损的1.5-2倍距离
if sl and current_price > sl:
distance = current_price - sl
tp = current_price + distance * 1.5
reason = f"多周期向上共振,建议买入,止损参考最近低点 {sl}"
else:
return None
elif trend == "down":
# 下降趋势,找最近的高点作为止损
action = "s"
high_pivots = [p for p in recent_pivots if p['direction'] == 'high']
if high_pivots:
sl = high_pivots[0]['price']
if sl and current_price < sl:
distance = sl - current_price
tp = current_price - distance * 1.5
reason = f"多周期向下共振,建议卖出,止损参考最近高点 {sl}"
else:
return None
if not all([action, sl, tp]):
return None
return {
"symbol": symbol,
"action": action,
"price": current_price,
"sl": round(sl, 4),
"tp": round(tp, 4),
"reason": reason,
"trend_strength": resonance['strength'],
"resonance_periods": resonance['aligned_count'],
"generated_at": datetime.now().isoformat()
}
+9
View File
@@ -0,0 +1,9 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
工具模块
"""
from .ws_manager import WebSocketManager
__all__ = ['WebSocketManager']
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
WebSocket 连接管理器
可复用的 WebSocket 客户端管理和消息广播
"""
import asyncio
import json
import threading
from typing import Set, Dict, Any, Optional
class WebSocketManager:
"""
WebSocket 连接管理器
职责
- 管理客户端连接添加/移除
- 线程安全操作
- 异步广播消息
- 同步广播从非异步上下文
"""
def __init__(self, name: str = "default"):
self._name = name
self._clients: Set = set()
self._lock = threading.Lock()
self._main_loop: Optional[asyncio.AbstractEventLoop] = None
print(f"[WebSocketManager:{name}] 已初始化")
# ==================== 事件循环 ====================
def set_event_loop(self, loop: asyncio.AbstractEventLoop):
"""设置主事件循环引用"""
self._main_loop = loop
print(f"[WebSocketManager:{self._name}] 已设置事件循环")
# ==================== 客户端管理 ====================
def add_client(self, client) -> int:
"""
添加客户端连接
Args:
client: WebSocket 连接对象
Returns:
当前连接数
"""
with self._lock:
self._clients.add(client)
count = len(self._clients)
print(f"[WebSocketManager:{self._name}] 客户端已连接, 当前: {count}")
return count
def remove_client(self, client) -> int:
"""
移除客户端连接
Args:
client: WebSocket 连接对象
Returns:
当前连接数
"""
with self._lock:
self._clients.discard(client)
count = len(self._clients)
print(f"[WebSocketManager:{self._name}] 客户端已断开, 当前: {count}")
return count
def get_client_count(self) -> int:
"""获取客户端数量"""
with self._lock:
return len(self._clients)
# ==================== 消息广播 ====================
async def broadcast(self, message: Dict[str, Any]):
"""
异步广播消息到所有客户端
Args:
message: 消息字典自动转为 JSON
"""
with self._lock:
clients = list(self._clients)
if not clients:
return
text = json.dumps(message, ensure_ascii=False)
for client in clients:
try:
await client.send_text(text)
except Exception as e:
print(f"[WebSocketManager:{self._name}] 发送失败: {e}")
self.remove_client(client)
async def send_to_client(self, client, message: Dict[str, Any]):
"""
发送消息到单个客户端
Args:
client: WebSocket 连接对象
message: 消息字典
"""
try:
text = json.dumps(message, ensure_ascii=False)
await client.send_text(text)
except Exception as e:
print(f"[WebSocketManager:{self._name}] 发送到客户端失败: {e}")
self.remove_client(client)
def broadcast_sync(self, message: Dict[str, Any]):
"""
同步方式广播从非异步上下文调用
用于在线程中向主事件循环提交广播任务
Args:
message: 消息字典
"""
if self._main_loop and self._main_loop.is_running():
asyncio.run_coroutine_threadsafe(
self.broadcast(message),
self._main_loop
)
else:
print(f"[WebSocketManager:{self._name}] 事件循环未运行,无法广播")
# ==================== 状态查询 ====================
def get_status(self) -> Dict[str, Any]:
"""获取状态"""
return {
"name": self._name,
"clients": self.get_client_count(),
"loop_set": self._main_loop is not None
}
+10 -14
View File
@@ -309,11 +309,11 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
print("[calendar] 警告: events数组为空")
return {"status": "ok", "message": "无数据需要更新", "count": 0}
from market.news_store import get_news_store
news_store = get_news_store()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
# 更新财经日历
updated_count = news_store.update_calendar_from_mt5(events)
updated_count = monitor.update_calendar_from_mt5(events)
# 记录日志 - MT5上报财经日历
system_log = get_system_log()
@@ -322,7 +322,7 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
{
"events_received": len(events),
"events_updated": updated_count,
"total_events": news_store.get_status().get('calendar_events', 0)
"total_events": monitor.calendar_store.get_status().get('total_events', 0)
},
message=f"MT5上报财经日历: 收到{len(events)}条, 更新{updated_count}"
)
@@ -372,11 +372,11 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
if not event_id:
return {"status": "error", "message": "缺少事件ID"}
from market.news_store import get_news_store
news_store = get_news_store()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
# 获取事件
event = news_store.get_event_by_id(event_id)
event = monitor.calendar_store.get_event_by_id(event_id)
if not event:
return {"status": "error", "message": f"未找到事件: {event_id}"}
@@ -453,7 +453,6 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
}
```
"""
import json as json_module
try:
data = await request.json()
deals = data.get('deals', [])
@@ -463,11 +462,8 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
if not deals:
return {"status": "ok", "message": "无数据需要更新", "count": 0}
from market.trade_history_store import get_trade_history_store
store = get_trade_history_store()
# 更新交易历史
new_count = store.update_from_ea(deals)
# 使用新的交易历史服务
new_count = server.trade_history_service.process_deals(deals)
# 记录日志
system_log = get_system_log()
@@ -476,7 +472,7 @@ def create_ea_routes(server: TradingServer) -> APIRouter:
{
"deals_received": len(deals),
"deals_new": new_count,
"total_deals": len(store.get_all_deals())
"total_deals": len(server.trade_history_store.get())
},
message=f"交易历史上报: 收到{len(deals)}条, 新增{new_count}"
)
+214 -430
View File
@@ -12,29 +12,31 @@ from datetime import datetime, timedelta
import json
import random
from market.store import MarketStore
from market.pivot_detector import PivotDetector
from market.monitor import PivotMonitor, TradeConfig
from market.trend_analyzer import TrendAnalyzer
from market.pending_orders import PendingOrderManager
from market.llm_analyzer import LLMAnalyzer
from market.models import KlineData
from market.store import KlineStore
from market.services import KlineService, PivotService, TechService, PendingOrderService
from market.trade_config import TradeConfig
from market.system_log import get_system_log
def create_market_routes(store: MarketStore, detector: PivotDetector,
monitor: PivotMonitor, trend_analyzer: TrendAnalyzer,
pending_orders: PendingOrderManager,
llm_analyzer: LLMAnalyzer = None) -> APIRouter:
def create_market_routes(
kline_store: KlineStore,
kline_service: KlineService,
pivot_service: PivotService,
tech_service: TechService,
pending_order_service: PendingOrderService,
trading_server = None
) -> APIRouter:
"""
创建行情相关路由
Args:
store: K线存储
detector: 转折点检测器
monitor: 转折点监控器
trend_analyzer: 趋势分析器
pending_orders: 待确认订单管理器
llm_analyzer: 大模型分析器
kline_store: K线存储
kline_service: K线服务
pivot_service: 转折点服务
tech_service: 技术分析服务
pending_order_service: 待确认订单服务
trading_server: TradingServer 实例
"""
router = APIRouter()
@@ -47,35 +49,9 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
async def receive_kline(period: str, request: Request) -> Dict:
"""
EA推送K线数据
Args:
period: 周期 (H4/H1/M15/M5/M1)
请求体:
```json
{
"symbol": "GOLD",
"is_full": false, // 是否为全量数据
"klines": [
{
"timestamp": "2024-01-15 14:00:00",
"open": 2030.50,
"high": 2035.00,
"low": 2028.00,
"close": 2033.50,
"volume": 1234
}
]
}
```
返回:
- 成功: {"status": "ok", "count": N}
- 需要全量数据: {"status": "error", "code": 8888, "message": "需要全量数据"}
"""
period = period.upper()
# 验证周期
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
return JSONResponse(
status_code=400,
@@ -93,81 +69,42 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
# 全量数据时检查K线时效性
if is_full:
period_interval = store.PERIOD_INTERVALS.get(period.upper(), 60)
latest_kline_time = None
staleness = kline_service.check_staleness(symbol, period, klines)
# 获取最新K线时间(取最后一条)
latest_kline = klines[-1] if klines else None
if latest_kline:
ts = latest_kline.get('timestamp') or latest_kline.get('time')
if ts:
# 解析时间戳
if isinstance(ts, datetime):
latest_kline_time = ts
else:
for fmt in ["%Y-%m-%d %H:%M:%S", "%Y.%m.%d %H:%M", "%Y.%m.%d %H:%M:%S", "%Y-%m-%d %H:%M"]:
try:
latest_kline_time = datetime.strptime(str(ts), fmt)
break
except:
continue
if latest_kline_time:
# 获取MT5时区偏移配置
# mt5_timezone_offset: MT5时间与本地时间的差值
# 正数表示MT5时间比本地时间快,负数表示MT5时间比本地时间慢
# 例如:MT5(GMT+2) vs 本地(GMT+8)MT5比本地慢6小时,offset = -6
if staleness.get('latest_kline_time'):
trade_config = TradeConfig.get_instance()
timezone_offset_hours = trade_config.mt5_timezone_offset
now_local = datetime.now()
staleness = kline_service.check_staleness(
symbol, period, klines, timezone_offset_hours
)
# 将K线时间(MT5服务器时间)转换为本地时间进行比较
# 本地时间 = MT5时间 - offset(因为offset是MT5相对本地的偏移)
# 例如:MT5时间 08:00offset=-6,本地时间 = 08:00 - (-6) = 08:00 + 6 = 14:00
kline_time_local = latest_kline_time - timedelta(hours=timezone_offset_hours)
time_diff = (now_local - kline_time_local).total_seconds()
# 调试日志
print(f"[MarketAPI] {symbol} {period} K线时间检查:")
print(f" - K线时间(MT5): {latest_kline_time}")
print(f" - 转换后本地时间: {kline_time_local}")
print(f" - 当前本地时间: {now_local}")
print(f" - 时区偏移: {timezone_offset_hours}小时")
print(f" - 时间差: {int(time_diff)}秒, 阈值: {period_interval}")
# 如果超过一个周期,说明数据不是最新的,可能休市
if time_diff > period_interval:
if staleness.get('is_stale'):
system_log = get_system_log()
system_log.add_log(
"ea_kline_stale",
{
"period": period,
"latest_kline_time": latest_kline_time.isoformat(),
"kline_time_local": kline_time_local.isoformat(),
"now_local": now_local.isoformat(),
"timezone_offset_hours": timezone_offset_hours,
"time_diff_seconds": int(time_diff),
"period_interval": period_interval
"latest_kline_time": staleness.get('latest_kline_time').isoformat() if staleness.get('latest_kline_time') else None,
"kline_time_local": staleness.get('kline_time_local').isoformat() if staleness.get('kline_time_local') else None,
"time_diff_seconds": staleness.get('time_diff_seconds'),
"period_interval": staleness.get('period_interval')
},
symbol=symbol,
message=f"K线数据过期,最新K线距当前 {int(time_diff)}秒,可能休市"
message=f"K线数据过期,最新K线距当前 {staleness.get('time_diff_seconds')}秒,可能休市"
)
print(f"[MarketAPI] {symbol} {period} 全量K线数据过期K线时间(MT5) {latest_kline_time},转换为本地时间 {kline_time_local},距当前 {int(time_diff)}秒,丢弃数据")
print(f"[MarketAPI] {symbol} {period} 全量K线数据过期")
return {
"status": "ok",
"count": 0,
"message": "K线数据过期,可能休市",
"stale": True,
"latest_kline_time": latest_kline_time.isoformat(),
"kline_time_local": kline_time_local.isoformat(),
"time_diff_seconds": int(time_diff),
"timezone_offset_hours": timezone_offset_hours
"latest_kline_time": staleness.get('latest_kline_time').isoformat() if staleness.get('latest_kline_time') else None,
"time_diff_seconds": staleness.get('time_diff_seconds')
}
# 检查是否需要全量数据
if not is_full and not store.is_initialized(symbol, period):
if not is_full and not kline_service.is_initialized(symbol, period):
print(f"[MarketAPI] {symbol} {period} 未初始化,需要全量数据")
return JSONResponse(
status_code=400,
@@ -179,11 +116,10 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
)
# 增量数据时检查连续性
if not is_full and store.is_initialized(symbol, period):
continuity = store.check_kline_continuity(symbol, period, klines)
if not is_full and kline_service.is_initialized(symbol, period):
continuity = kline_service.check_continuity(symbol, period, klines)
if not continuity["is_continuous"]:
print(f"[MarketAPI] {symbol} {period} 数据不连续,缺失 {continuity['gap_count']} 个周期")
print(f"[MarketAPI] 现有最后时间: {continuity.get('last_existing_time')}, 新数据最早时间: {continuity.get('first_new_time')}")
return JSONResponse(
status_code=400,
content={
@@ -194,43 +130,24 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
)
# 保存K线数据
result = store.save_klines(symbol, period, klines, is_full)
result = kline_service.process_kline_data(symbol, period, klines, is_full)
# 记录日志 - 全量K线总是记录,增量K线5%概率记录
# 记录日志
if is_full or random.random() < KLINE_LOG_PROBABILITY:
system_log = get_system_log()
event_type = "ea_kline_full" if is_full else "ea_kline_incremental"
system_log.add_log(
event_type,
{
"period": period,
"count": len(klines),
"is_full": is_full
},
{"period": period, "count": len(klines), "is_full": is_full},
symbol=symbol,
message=f"{'全量' if is_full else '增量'} {period} {len(klines)}"
)
if result['status'] == 'ok':
# 更新转折点
all_klines = store.get_all_klines(symbol, period)
all_klines = kline_service.get_all_kline_objects(symbol, period)
if all_klines:
# 转换为KlineData对象
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
pivot_service.update_pivots(symbol, period, all_klines)
return result
@@ -243,24 +160,7 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.post("/ea/kline_batch")
async def receive_kline_batch(request: Request) -> Dict:
"""
EA批量推送多个周期的K线数据
请求体:
```json
{
"symbol": "GOLD",
"is_full": true,
"data": {
"H4": [{...}, {...}],
"H1": [{...}, {...}],
"M15": [{...}, {...}],
"M5": [{...}, {...}],
"M1": [{...}, {...}]
}
}
```
"""
"""EA批量推送多个周期的K线数据"""
try:
data = await request.json()
symbol = data.get('symbol', 'GOLD')
@@ -275,42 +175,22 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
if period not in ['H4', 'H1', 'M15', 'M5', 'M1']:
continue
result = store.save_klines(symbol, period, klines, is_full)
result = kline_service.process_kline_data(symbol, period, klines, is_full)
results[period] = result
# 记录日志 - 全量K线总是记录,增量K线5%概率记录
if is_full or random.random() < KLINE_LOG_PROBABILITY:
event_type = "ea_kline_full" if is_full else "ea_kline_incremental"
system_log.add_log(
event_type,
{
"period": period,
"count": len(klines),
"is_full": is_full
},
{"period": period, "count": len(klines), "is_full": is_full},
symbol=symbol,
message=f"{'全量' if is_full else '增量'} {period} {len(klines)}"
)
# 更新转折点
if result['status'] == 'ok':
all_klines = store.get_all_klines(symbol, period)
all_klines = kline_service.get_all_kline_objects(symbol, period)
if all_klines:
from market.store import KlineData
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
detector.update_pivots(symbol, period, kline_objs)
pivot_service.update_pivots(symbol, period, all_klines)
return {
"status": "ok",
@@ -333,12 +213,9 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
period: str = Query("M5", description="周期: H4/H1/M15/M5/M1"),
count: int = Query(100, description="返回条数")
) -> Dict:
"""
获取K线数据
"""
"""获取K线数据"""
period = period.upper()
klines = store.get_klines(symbol, period, count)
klines = kline_service.get_klines(symbol, period, count)
return {
"status": "ok",
@@ -355,12 +232,10 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
direction: str = Query(None, description="方向: high/low"),
count: int = Query(50, description="返回条数")
) -> Dict:
"""
获取转折点数据
"""
"""获取转折点数据"""
if period:
period = period.upper()
pivots = detector.get_pivots(symbol, period, direction, count)
pivots = pivot_service.get_pivots(symbol, period, direction, count)
return {
"status": "ok",
"symbol": symbol,
@@ -369,10 +244,9 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
"data": pivots
}
else:
# 返回所有周期的转折点
result = {}
for p in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivots = detector.get_pivots(symbol, p, direction, count)
pivots = pivot_service.get_pivots(symbol, p, direction, count)
if pivots:
result[p] = pivots
@@ -384,10 +258,8 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/market/symbols")
async def get_symbols() -> Dict:
"""
获取所有已存储数据的symbol列表
"""
symbols = store.get_symbols()
"""获取所有已存储数据的symbol列表"""
symbols = kline_service.get_symbols()
return {
"status": "ok",
"symbols": symbols,
@@ -396,31 +268,19 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/market/configured_symbols")
async def get_configured_symbols() -> Dict:
"""
获取配置的品种列表及其数据状态
返回系统配置中的品种以及每个品种的K线数据状态
"""
from market.monitor import TradeConfig
"""获取配置的品种列表及其数据状态"""
config = TradeConfig.get_instance()
# 获取配置的品种
configured_symbols = list(config.symbol_config.keys())
# 获取每个品种的状态
symbols_status = []
for symbol in configured_symbols:
# 检查是否有M1数据
m1_status = store.check_m1_updated_within(symbol, 180)
m1_status = kline_service.check_m1_updated_within(symbol, 180)
latest_m1_time = kline_store.get_latest_kline_time(symbol, 'M1')
# 获取最新M1 K线时间
latest_m1_time = store.get_latest_kline_time(symbol, 'M1')
# 获取各周期数据条数
period_counts = {}
with store._lock:
with kline_store._lock:
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
period_counts[period] = len(store._klines[symbol][period])
period_counts[period] = len(kline_store._klines[symbol][period])
symbols_status.append({
"symbol": symbol,
@@ -442,26 +302,24 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/market/status")
async def get_market_status() -> Dict:
"""
获取行情存储状态
"""
store_status = store.get_status()
detector_status = detector.get_status()
monitor_status = monitor.get_status()
"""获取行情存储状态"""
store_status = kline_service.get_status()
pivot_status = pivot_service.get_status()
# 使用 trading_server 获取状态
server_status = trading_server.get_status() if trading_server else {}
return {
"status": "ok",
"store": store_status,
"pivots": detector_status,
"monitor": monitor_status
"pivots": pivot_status,
"server": server_status
}
@router.get("/market/thresholds")
async def get_thresholds() -> Dict:
"""
获取各周期的接近阈值
"""
thresholds = detector.THRESHOLDS
"""获取各周期的接近阈值"""
thresholds = pivot_service.THRESHOLDS
return {
"status": "ok",
@@ -479,35 +337,12 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/trend/{symbol}")
async def get_trend(symbol: str) -> Dict:
"""
获取单个品种的趋势分析
"""
from market.store import KlineData
# 分析每个周期的趋势
"""获取单个品种的趋势分析"""
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
tech_service.analyze_trend(symbol, period)
# 获取共振分析
resonance = trend_analyzer.analyze_resonance(symbol)
# 获取趋势转换历史
changes = trend_analyzer.get_trend_changes(symbol, 10)
resonance = tech_service.analyze_resonance(symbol)
changes = tech_service.get_trend_changes(symbol, 10)
return {
"status": "ok",
@@ -518,53 +353,24 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.post("/trend/generate_order/{symbol}")
async def generate_trade_order(symbol: str) -> Dict:
"""
基于趋势分析生成交易建议
"""
from market.store import KlineData
# 更新趋势分析
"""基于趋势分析生成交易建议"""
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
all_klines = store.get_all_klines(symbol, period)
if all_klines:
kline_objs = [
KlineData(
symbol=k['symbol'],
period=k['period'],
timestamp=k['timestamp'],
open_price=k['open'],
high=k['high'],
low=k['low'],
close=k['close'],
volume=k['volume']
)
for k in all_klines
]
trend_analyzer.analyze_trend(symbol, period, kline_objs)
tech_service.analyze_trend(symbol, period)
# 获取所有周期的转折点
all_pivots = []
for period in ['H4', 'H1', 'M15', 'M5', 'M1']:
pivot_list = detector.get_pivots(symbol, period, None, 20)
all_pivots.extend(pivot_list)
# 获取当前价格
current_price = store.get_latest_price(symbol)
current_price = kline_service.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
# 生成交易建议
suggestion = trend_analyzer.generate_trade_suggestion(symbol, all_pivots, current_price)
suggestion = tech_service.generate_trade_suggestion(symbol, current_price)
if not suggestion:
return {
"status": "ok",
"message": "当前无交易建议",
"resonance": trend_analyzer.analyze_resonance(symbol)
"resonance": tech_service.analyze_resonance(symbol)
}
# 添加到待确认订单
order_id = pending_orders.add_order(suggestion)
order_id = pending_order_service.create_order_from_dict(suggestion)
return {
"status": "ok",
@@ -577,10 +383,8 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/pending_orders")
async def get_pending_orders(symbol: Optional[str] = None) -> Dict:
"""
获取待确认订单列表
"""
orders = pending_orders.get_pending_orders(symbol)
"""获取待确认订单列表"""
orders = pending_order_service.get_orders_dict(symbol)
return {
"status": "ok",
"count": len(orders),
@@ -589,10 +393,7 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.post("/pending_orders/{order_id}/confirm")
async def confirm_pending_order(order_id: str, request: Request = None) -> Dict:
"""
确认待确认订单可更新手数止损止盈
"""
# 获取更新数据
"""确认待确认订单"""
update_data = {}
if request:
try:
@@ -600,35 +401,24 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
except:
pass
# 更新订单参数
if update_data:
order = pending_orders.get_order_by_id(order_id)
if order:
if 'mount' in update_data:
order['mount'] = update_data['mount']
if 'sl' in update_data:
order['sl'] = update_data['sl']
if 'tp' in update_data:
order['tp'] = update_data['tp']
order = pending_orders.confirm_order(order_id)
# 获取订单并确认
order = pending_order_service.confirm_order(order_id, update_data)
if not order:
return {"status": "error", "message": "订单不存在"}
# 记录日志
system_log = get_system_log()
action_text = '买入' if order.get('action') == 'b' else '卖出'
symbol = order.get('symbol', '')
mount = order.get('mount')
price = order.get('price')
sl = order.get('sl')
tp = order.get('tp')
action_text = '买入' if order.action == 'b' else '卖出'
symbol = order.symbol
mount = order.mount
price = order.price
sl = order.sl
tp = order.tp
system_log.add_log(
"order_confirmed",
{
"order_id": order_id,
"action": order.get('action'),
"action": order.action,
"price": price,
"mount": mount,
"sl": sl,
@@ -638,36 +428,28 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
message=f"{action_text} @ {price}, 手数={mount}, SL={sl}, TP={tp}"
)
# 打印确认订单信息
print(f"[订单确认] {symbol} | {action_text} | 价格={price} | 手数={mount} | SL={sl} | TP={tp}")
return {
"status": "ok",
"message": "订单已确认",
"order": order
"order": order.to_dict()
}
@router.post("/pending_orders/{order_id}/reject")
async def reject_pending_order(order_id: str) -> Dict:
"""
拒绝待确认订单
"""
# 先获取订单信息用于日志
order = pending_orders.get_order_by_id(order_id)
success = pending_orders.reject_order(order_id)
if not success:
"""拒绝待确认订单"""
order = pending_order_service.reject_order(order_id)
if not order:
return {"status": "error", "message": "订单不存在"}
# 记录日志
if order:
system_log = get_system_log()
system_log.add_log(
"order_rejected",
{"order_id": order_id, "action": order.get('action'), "price": order.get('price')},
symbol=order.get('symbol'),
message=f"订单已拒绝"
)
system_log = get_system_log()
system_log.add_log(
"order_rejected",
{"order_id": order_id, "action": order.action, "price": order.price},
symbol=order.symbol,
message=f"订单已拒绝"
)
return {
"status": "ok",
@@ -678,10 +460,7 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/trade_config")
async def get_trade_config() -> Dict:
"""
获取交易配置
"""
from market.monitor import TradeConfig
"""获取交易配置"""
config = TradeConfig.get_instance()
return {
"status": "ok",
@@ -690,10 +469,7 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.post("/trade_config")
async def update_trade_config(request: Request) -> Dict:
"""
更新交易配置
"""
from market.monitor import TradeConfig
"""更新交易配置"""
config = TradeConfig.get_instance()
try:
@@ -707,22 +483,98 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
except Exception as e:
return {"status": "error", "message": str(e)}
# ==================== 策略决策接口 ====================
@router.get("/strategy")
async def get_all_strategies() -> Dict:
"""获取所有策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
strategies = trading_server.strategy_service.get_all_strategies()
return {
"status": "ok",
"count": len(strategies),
"strategies": [s.to_dict() for s in strategies]
}
@router.get("/strategy/decisions")
async def get_decisions(symbol: Optional[str] = None, count: int = 20) -> Dict:
"""获取决策历史"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
decisions = trading_server.get_decision_history(symbol, count)
return {
"status": "ok",
"count": len(decisions),
"decisions": decisions
}
@router.get("/strategy/{symbol}")
async def get_strategy(symbol: str) -> Dict:
"""获取品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
strategy = trading_server.strategy_service.get_strategy(symbol)
return {
"status": "ok",
"strategy": strategy.to_dict()
}
@router.post("/strategy/{symbol}")
async def update_strategy(symbol: str, request: Request) -> Dict:
"""更新品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
try:
data = await request.json()
strategy = trading_server.strategy_service.update_strategy(symbol, data)
return {
"status": "ok",
"message": "策略配置已更新",
"strategy": strategy.to_dict()
}
except Exception as e:
return {"status": "error", "message": str(e)}
@router.delete("/strategy/{symbol}")
async def delete_strategy(symbol: str) -> Dict:
"""删除品种策略配置"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
success = trading_server.strategy_service.strategy_store.delete_strategy(symbol)
if success:
return {"status": "ok", "message": "策略配置已删除"}
return {"status": "error", "message": "策略配置不存在"}
@router.post("/strategy/trigger/{symbol}")
async def trigger_strategy_decision(symbol: str) -> Dict:
"""手动触发策略决策"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
current_price = kline_service.get_latest_price(symbol)
if not current_price:
return {"status": "error", "message": "无法获取当前价格"}
result = trading_server.process_price(symbol, current_price)
return {
"status": "ok",
"result": result
}
# ==================== 系统日志接口 ====================
@router.get("/system/logs")
async def get_system_logs(count: int = 50, event_type: str = None,
symbol: str = None) -> Dict:
"""
获取系统运行日志
Args:
count: 获取数量默认50条
event_type: 过滤事件类型多个用逗号分隔 "order_generated,order_confirmed"
symbol: 过滤品种
"""
"""获取系统运行日志"""
system_log = get_system_log()
# 支持多个事件类型过滤
event_types = None
if event_type:
event_types = [et.strip() for et in event_type.split(',') if et.strip()]
@@ -745,30 +597,25 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.websocket("/ws/market")
async def websocket_market(websocket: WebSocket):
"""
WebSocket连接用于实时推送转折点提醒和大模型分析更新
"""
"""WebSocket连接"""
await websocket.accept()
monitor.add_ws_client(websocket)
if llm_analyzer:
llm_analyzer.add_ws_client(websocket)
# 添加到系统日志的WebSocket客户端列表
# 注册到 TradingServer(内部会自动注册到 llm_analyzer 和 system_log
if trading_server:
trading_server.add_ws_client(websocket)
system_log = get_system_log()
system_log.add_ws_client(websocket)
try:
# 发送欢迎消息
await websocket.send_text(json.dumps({
"type": "connected",
"message": "已连接到行情监控服务"
}))
# 保持连接,等待客户端消息或关闭
while True:
try:
data = await websocket.receive_text()
# 可以处理客户端发来的消息
msg = json.loads(data)
if msg.get('type') == 'ping':
@@ -781,38 +628,19 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
print(f"[WebSocket] 连接异常: {e}")
finally:
monitor.remove_ws_client(websocket)
if llm_analyzer:
llm_analyzer.remove_ws_client(websocket)
if trading_server:
trading_server.remove_ws_client(websocket)
system_log.remove_ws_client(websocket)
# ==================== 大模型分析接口 ====================
@router.get("/llm/analysis")
async def get_llm_analysis(symbol: Optional[str] = None) -> Dict:
"""
获取大模型分析结果
"""获取大模型分析结果"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
参数:
- symbol: 可选指定品种不提供则返回所有
返回:
```json
{
"status": "ok",
"data": {
"symbol": {
"analysis": {...},
"analyzed_at": "2024-01-01T00:00:00"
}
}
}
```
"""
if not llm_analyzer:
return {"status": "error", "message": "大模型分析器未初始化"}
result = llm_analyzer.get_analysis(symbol)
result = trading_server.get_llm_analysis(symbol)
return {
"status": "ok",
"data": result
@@ -820,87 +648,43 @@ def create_market_routes(store: MarketStore, detector: PivotDetector,
@router.get("/llm/status")
async def get_llm_status() -> Dict:
"""
获取大模型分析器状态
返回:
```json
{
"status": "ok",
"data": {
"enabled": true,
"model": "gpt-4o-mini",
"last_analysis_time": "2024-01-01T00:00:00",
"symbols_analyzed": ["GOLD", "EURUSD"]
}
}
```
"""
if not llm_analyzer:
return {"status": "ok", "data": {"enabled": False, "message": "大模型分析器未初始化"}}
"""获取大模型分析器状态"""
if not trading_server:
return {"status": "ok", "data": {"enabled": False, "message": "TradingServer 未初始化"}}
return {
"status": "ok",
"data": llm_analyzer.get_status()
"data": trading_server.get_llm_status()
}
@router.get("/llm/config")
async def get_llm_config() -> Dict:
"""
获取大模型配置API Key会脱敏显示
返回:
```json
{
"status": "ok",
"config": {
"api_key": "sk-****1234",
"api_key_set": true,
"api_base": "https://api.openai.com/v1",
"model": "gpt-4o-mini",
"enabled": true
}
}
```
"""
if not llm_analyzer:
return {"status": "ok", "config": {"enabled": False, "message": "大模型分析器未初始化"}}
"""获取大模型配置"""
if not trading_server:
return {"status": "ok", "config": {"enabled": False, "message": "TradingServer 未初始化"}}
return {
"status": "ok",
"config": llm_analyzer.get_config()
"config": trading_server.get_llm_config()
}
@router.post("/llm/trigger")
async def trigger_llm_analysis() -> Dict:
"""
手动触发大模型分析
"""
if not llm_analyzer:
return {"status": "error", "message": "大模型分析器未初始化"}
"""手动触发大模型分析"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
return llm_analyzer.trigger_analysis()
return trading_server.trigger_llm_analysis()
@router.post("/llm/configure")
async def configure_llm(request: Request) -> Dict:
"""
配置大模型参数
请求体:
```json
{
"api_key": "your-api-key",
"api_base": "https://api.openai.com/v1",
"model": "gpt-4o-mini"
}
```
"""
if not llm_analyzer:
return {"status": "error", "message": "大模型分析器未初始化"}
"""配置大模型参数"""
if not trading_server:
return {"status": "error", "message": "TradingServer 未初始化"}
try:
data = await request.json()
result = llm_analyzer.configure(
result = trading_server.configure_llm(
api_key=data.get("api_key"),
api_base=data.get("api_base"),
model=data.get("model")
+56 -25
View File
@@ -23,10 +23,10 @@ def create_news_routes():
返回指定日期或所有日期的财经事件
"""
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
calendar = news_monitor.get_calendar(date)
calendar = monitor.get_calendar(date)
return {
"status": "ok",
@@ -44,10 +44,10 @@ def create_news_routes():
默认返回未来24小时内的重要财经事件
"""
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
events = news_monitor.get_upcoming_events(hours)
events = monitor.get_upcoming_events(hours)
return {
"status": "ok",
@@ -65,10 +65,10 @@ def create_news_routes():
返回最近的有影响的快讯关键人物讲话重要事件
"""
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
news_list = news_monitor.get_recent_news(count)
news_list = monitor.get_recent_news(count)
return {
"status": "ok",
@@ -81,10 +81,10 @@ def create_news_routes():
"""
获取新闻模块状态
"""
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
status = news_monitor.get_status()
status = monitor.get_status()
return {
"status": "ok",
@@ -94,25 +94,24 @@ def create_news_routes():
@router.websocket("/ws")
async def news_websocket(websocket: WebSocket):
"""
新闻WebSocket推送
市场事件WebSocket推送
推送内容类型:
- event_reminder: 事件发布前提醒
- event_result: 事件发布结果
- flash_news: 重要快讯
- calendar_event_reminder: 财经日历事件发布前提醒
- calendar_update: 日历更新
- flash_news: 重要快讯
"""
from market.news_monitor import get_news_monitor
news_monitor = get_news_monitor()
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
await websocket.accept()
news_monitor.add_ws_client(websocket)
monitor.ws_manager.add_client(websocket)
try:
# 发送欢迎消息
await websocket.send_json({
"type": "connected",
"message": "已连接到新闻推送服务"
"message": "已连接到市场事件推送服务"
})
# 保持连接,等待客户端消息或断开
@@ -127,9 +126,9 @@ def create_news_routes():
except WebSocketDisconnect:
pass
except Exception as e:
print(f"[NewsWebSocket] 连接异常: {e}")
print(f"[MarketEventWebSocket] 连接异常: {e}")
finally:
news_monitor.remove_ws_client(websocket)
monitor.ws_manager.remove_client(websocket)
@router.get("/impact/{symbol}")
async def get_symbol_impact(symbol: str):
@@ -138,7 +137,7 @@ def create_news_routes():
返回影响该品种的即将发布事件
"""
from market.news_monitor import get_news_monitor
from market.market_event_monitor import get_market_event_monitor
from market.event_config import WATCH_SYMBOLS
if symbol not in WATCH_SYMBOLS:
@@ -148,8 +147,8 @@ def create_news_routes():
"supported_symbols": WATCH_SYMBOLS
}
news_monitor = get_news_monitor()
events = news_monitor.get_upcoming_events(72) # 未来3天
monitor = get_market_event_monitor()
events = monitor.get_upcoming_events(72) # 未来3天
# 过滤相关事件
related_events = [
@@ -164,4 +163,36 @@ def create_news_routes():
"data": related_events
}
@router.post("/calendar/clear")
async def clear_calendar():
"""
清空财经日历数据
清空后需要EA重新推送日历数据会应用时区转换
"""
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
monitor.clear_calendar()
return {
"status": "ok",
"message": "财经日历数据已清空,请让EA重新推送日历数据"
}
@router.delete("/calendar")
async def clear_calendar_delete():
"""
清空财经日历数据 (DELETE方法)
清空后需要EA重新推送日历数据会应用时区转换
"""
from market.market_event_monitor import get_market_event_monitor
monitor = get_market_event_monitor()
monitor.clear_calendar()
return {
"status": "ok",
"message": "财经日历数据已清空,请让EA重新推送日历数据"
}
return router
+79 -19
View File
@@ -6,18 +6,18 @@
from fastapi import APIRouter, Request
from typing import Dict, Optional
import json
from market.position_store import get_position_store
from market.system_log import get_system_log
def create_position_routes() -> APIRouter:
def create_position_routes(trading_server=None) -> APIRouter:
"""
创建仓位管理路由
Args:
trading_server: TradingServer 实例
"""
router = APIRouter()
position_store = get_position_store()
@router.post("/ea/positions")
async def receive_positions(request: Request) -> Dict:
@@ -50,7 +50,8 @@ def create_position_routes() -> APIRouter:
if not symbol:
return {"status": "error", "message": "缺少品种信息"}
result = position_store.update_positions(symbol, positions)
# 使用新的持仓服务
result = trading_server.position_service.update_positions(symbol, positions)
# 记录日志
if positions:
@@ -79,7 +80,7 @@ def create_position_routes() -> APIRouter:
参数:
- symbol: 可选指定品种不提供则返回所有
"""
positions = position_store.get_positions(symbol)
positions = trading_server.position_service.get_positions(symbol)
return {
"status": "ok",
"count": len(positions),
@@ -94,7 +95,7 @@ def create_position_routes() -> APIRouter:
参数:
- symbol: 可选指定品种不提供则返回所有
"""
summary = position_store.get_summary(symbol)
summary = trading_server.position_service.get_summary(symbol)
return {
"status": "ok",
**summary
@@ -105,7 +106,7 @@ def create_position_routes() -> APIRouter:
"""
获取单个持仓详情
"""
position = position_store.get_position(symbol, ticket)
position = trading_server.position_service.get_position(symbol, ticket)
if not position:
return {"status": "error", "message": "持仓不存在"}
return {
@@ -115,16 +116,75 @@ def create_position_routes() -> APIRouter:
# ==================== 交易历史接口 ====================
@router.post("/ea/trade_history")
async def receive_trade_history(request: Request) -> Dict:
"""
EA推送交易历史数据
请求体:
```json
{
"deals": [
{
"ticket": 123456,
"order": 789012,
"symbol": "GOLD#",
"type": 0,
"entry": 0,
"volume": 0.1,
"price": 2050.50,
"profit": 0,
"swap": 0,
"commission": -5.0,
"time": "2026.03.16 15:30:00",
"comment": ""
}
]
}
```
"""
try:
data = await request.json()
deals = data.get('deals', [])
if not deals:
return {"status": "ok", "message": "无数据", "count": 0}
# 使用新的交易历史服务
new_count = trading_server.trade_history_service.process_deals(deals)
# 记录日志
system_log = get_system_log()
system_log.add_log(
"trade_history_update",
{
"deals_received": len(deals),
"deals_new": new_count,
"total_deals": len(trading_server.trade_history_store.get())
},
message=f"交易历史上报: 收到{len(deals)}条, 新增{new_count}"
)
return {
"status": "ok",
"message": "交易历史已更新",
"count": new_count
}
except Exception as e:
print(f"[PositionAPI] 接收交易历史异常: {e}")
return {"status": "error", "message": str(e)}
@router.get("/trade_history")
async def get_trade_history() -> Dict:
async def get_trade_history(symbol: Optional[str] = None) -> Dict:
"""
获取交易历史数据
"""
from market.trade_history_store import get_trade_history_store
store = get_trade_history_store()
deals = store.get_all_deals()
statistics = store.get_statistics()
参数:
- symbol: 可选指定品种
"""
deals = trading_server.trade_history_service.get_deals(symbol)
statistics = trading_server.trade_history_service.get_statistics(symbol)
return {
"status": "ok",
@@ -133,14 +193,14 @@ def create_position_routes() -> APIRouter:
}
@router.get("/trade_history/statistics")
async def get_trade_history_statistics() -> Dict:
async def get_trade_history_statistics(symbol: Optional[str] = None) -> Dict:
"""
获取交易历史统计
"""
from market.trade_history_store import get_trade_history_store
store = get_trade_history_store()
statistics = store.get_statistics()
参数:
- symbol: 可选指定品种
"""
statistics = trading_server.trade_history_service.get_statistics(symbol)
return {
"status": "ok",
+420 -200
View File
@@ -2,256 +2,476 @@
# -*- coding: utf-8 -*-
"""
交易服务核心类
内部聚合信号层对外暴露策略层
"""
from collections import deque, defaultdict
from typing import List, Dict, Optional
from typing import List, Dict, Optional, Set
from datetime import datetime
import threading
import asyncio
import json
from models import TradeInstruction
from market.store import MarketStore
from market.pivot_detector import PivotDetector
from market.monitor import PivotMonitor, TradeConfig
from market.trend_analyzer import TrendAnalyzer
from market.pending_orders import PendingOrderManager
from market.models import KlineData, PivotPoint, LLMConfig, LLMAnalysisResult
from market.models import TechTrendState, TechResonanceResult, TechTradeSuggestion
from market.models import PendingOrder, TradingInstruction, TradingSignal, TradingDecision
from market.models import StatisticsData, PositionData, TradeDeal
from market.store import KlineStore, PivotStore, LLMStore, TechStore
from market.store import PendingOrderStore, TradingInstructionStore, SignalStore, StrategyStore
from market.store import StatisticsStore, PositionStore, TradeHistoryStore
from market.services import KlineService, PivotService, LLMService, TechService
from market.services import PendingOrderService, TradingInstructionService
from market.services import SignalService, StrategyService, RiskManager
from market.services import PivotSignalGenerator, KeyLevelSignalGenerator, AIEntrySignalGenerator
from market.services import StatisticsService, PositionService, TradeHistoryService
from market.trade_config import TradeConfig
from market.llm_analyzer import LLMAnalyzer
class TradingServer:
"""交易服务主类"""
"""
交易服务主类
架构
- 内部行情模块 信号层 策略层 订单/指令
- 对外只暴露策略服务接口
"""
def __init__(self):
# 交易指令队列 - 按SYMBOL分类
# 结构: {"SYMBOL1": [TradeInstruction1, ...], "SYMBOL2": [...]}
self.trade_instructions = defaultdict(list)
# 平仓指令队列 - 按SYMBOL分类
# 结构: {"SYMBOL1": [ticket1, ticket2, ...], ...}
self.close_position_instructions = defaultdict(list)
# 统计数据历史 - 保留最新10条
# 结构: deque([{stat_data1}, {stat_data2}, ...], maxlen=10)
self.statistics_history = deque(maxlen=10)
# 线程锁 - 确保线程安全
# 线程锁
self.lock = threading.RLock()
# ==================== 行情模块 ====================
# K线存储
self.market_store = MarketStore()
# 转折点检测器
self.pivot_detector = PivotDetector()
# 待确认订单管理器(需要在 PivotMonitor 之前初始化)
self.pending_orders = PendingOrderManager()
# 设置订单确认回调
self.pending_orders.set_confirm_callback(self._on_order_confirmed)
# 大模型分析器(需要在 PivotMonitor 之前初始化)
self.llm_analyzer = LLMAnalyzer(self.market_store)
# 转折点监控器
self.pivot_monitor = PivotMonitor(self.market_store, self.pivot_detector, self.pending_orders, self.llm_analyzer)
# 设置统计数据历史引用(用于获取价差)
self.pivot_monitor.set_statistics_history(self.statistics_history)
# 趋势分析器
self.trend_analyzer = TrendAnalyzer()
self.trend_analyzer.set_statistics_history(self.statistics_history)
# 交易配置
# ==================== 行情模块(内部) ====================
# 存储
self.kline_store = KlineStore()
self.pivot_store = PivotStore()
self.llm_store = LLMStore()
self.tech_store = TechStore()
# 服务层
self.kline_service = KlineService(self.kline_store)
self.pivot_service = PivotService(self.pivot_store, self.kline_store)
self.llm_service = LLMService(self.llm_store, self.kline_service)
self.tech_service = TechService(self.tech_store, self.kline_store, self.pivot_store)
# LLM 分析器
self.llm_analyzer = LLMAnalyzer(self.llm_service)
# ==================== 统计/持仓/交易历史模块 ====================
# 存储层
self.statistics_store = StatisticsStore()
self.position_store = PositionStore()
self.trade_history_store = TradeHistoryStore()
# 服务层
self.statistics_service = StatisticsService(self.statistics_store)
self.position_service = PositionService(self.position_store)
self.trade_history_service = TradeHistoryService(self.trade_history_store)
# TechService 使用统计服务获取价差
self.tech_service.set_statistics_service(self.statistics_service)
# ==================== 信号层(内部,不暴露) ====================
# 存储层
self._signal_store = SignalStore()
# 服务层
self._signal_service = SignalService(self._signal_store)
# ==================== 策略层(对外暴露) ====================
# 存储层
self._strategy_store = StrategyStore()
# 风险管理器
self._risk_manager = RiskManager()
# 服务层
self.strategy_service = StrategyService(
self._strategy_store,
self._signal_service,
self._risk_manager
)
# ==================== 交易配置 ====================
self.trade_config = TradeConfig.get_instance()
print("[信息] 交易服务已初始化")
# 注册信号生成器
self._setup_signal_generators()
def _on_order_confirmed(self, order: Dict):
# ==================== 订单/指令模块 ====================
# 存储层
self.pending_order_store = PendingOrderStore()
self.trading_instruction_store = TradingInstructionStore()
# 服务层
self.pending_order_service = PendingOrderService(self.pending_order_store)
self.trading_instruction_service = TradingInstructionService(self.trading_instruction_store)
# 设置订单确认回调
self.pending_order_service.set_confirm_callback(self._on_order_confirmed)
# 更新策略服务的订单服务引用
self.strategy_service.set_pending_order_service(self.pending_order_service)
# 策略服务使用持仓服务进行风险管理
self.strategy_service.set_position_service(self.position_service)
# 风险管理器使用统计服务获取账户信息
self._risk_manager.set_statistics_service(self.statistics_service)
# ==================== WebSocket 广播 ====================
self._ws_clients: Set = set()
self._ws_lock = threading.Lock()
self._main_loop = None
# ==================== 决策历史 ====================
self._decision_history: deque = deque(maxlen=50)
# 兼容旧代码的别名
self.market_store = self.kline_store
self.pivot_detector = self.pivot_service
self.trend_analyzer = self.tech_service
self.pending_orders = self.pending_order_service
self.trade_instructions = defaultdict(list)
# 统计数据历史兼容(已迁移到 statistics_store
self.statistics_history = self.statistics_store._all_data
print("[TradingServer] 交易服务已初始化")
def _setup_signal_generators(self):
"""设置信号生成器"""
# 转折点信号生成器
pivot_generator = PivotSignalGenerator(
pivot_service=self.pivot_service,
pivot_store=self.pivot_store,
kline_store=self.kline_store
)
self._signal_service.register_generator("pivot", pivot_generator)
# 关键点位信号生成器
key_level_generator = KeyLevelSignalGenerator()
self._signal_service.register_generator("key_level", key_level_generator)
# AI入场信号生成器
ai_entry_generator = AIEntrySignalGenerator()
ai_entry_generator.set_llm_analyzer(self.llm_analyzer)
self._signal_service.register_generator("ai_entry", ai_entry_generator)
# ==================== WebSocket 管理 ====================
def set_event_loop(self, loop):
"""设置主事件循环引用"""
self._main_loop = loop
print("[TradingServer] 已设置主事件循环")
# 同时设置内部模块的事件循环
self.llm_analyzer.set_event_loop(loop)
def add_ws_client(self, client):
"""添加WebSocket客户端"""
with self._ws_lock:
self._ws_clients.add(client)
# 同时注册到内部模块
self.llm_analyzer.add_ws_client(client)
print(f"[TradingServer] WebSocket客户端已连接, 当前连接数: {len(self._ws_clients)}")
def remove_ws_client(self, client):
"""移除WebSocket客户端"""
with self._ws_lock:
self._ws_clients.discard(client)
# 同时从内部模块移除
self.llm_analyzer.remove_ws_client(client)
print(f"[TradingServer] WebSocket客户端已断开, 当前连接数: {len(self._ws_clients)}")
def get_ws_client_count(self) -> int:
"""获取WebSocket客户端数量"""
with self._ws_lock:
return len(self._ws_clients)
def _broadcast(self, data: Dict):
"""广播数据到所有WebSocket客户端"""
message = json.dumps(data, ensure_ascii=False)
with self._ws_lock:
clients = list(self._ws_clients)
if not clients:
return
if self._main_loop and self._main_loop.is_running():
for client in clients:
try:
asyncio.run_coroutine_threadsafe(
self._send_to_client(client, message),
self._main_loop
)
except Exception as e:
print(f"[TradingServer] 发送WebSocket消息失败: {e}")
async def _send_to_client(self, client, message: str):
"""发送消息到客户端"""
try:
await client.send_text(message)
except Exception as e:
print(f"[TradingServer] 发送消息到客户端失败: {e}")
with self._ws_lock:
self._ws_clients.discard(client)
def _broadcast_decision(self, decision: TradingDecision):
"""广播交易决策"""
self._broadcast({
"type": "trading_decision",
"data": decision.to_dict()
})
def _broadcast_pending_order(self, order: PendingOrder):
"""广播待确认订单"""
self._broadcast({
"type": "pending_order",
"data": order.to_dict()
})
# ==================== 价格处理与决策 ====================
def process_price(self, symbol: str, current_price: float) -> Dict:
"""
订单确认回调 - 将确认的订单加入交易队列
处理价格变动生成决策
这是核心入口
1. 信号层生成信号
2. 策略层综合决策
3. 自动执行决策生成PendingOrder
Args:
symbol: 品种
current_price: 当前价格
Returns:
处理结果
"""
print(f"[TradingServer] _on_order_confirmed 被调用,订单: {order}")
result = {
"signals_generated": 0,
"decision": None,
"pending_order": None
}
if not self.trade_config.enabled:
return result
# 1. 信号层生成信号
signals = self._signal_service.generate_signals(symbol, current_price)
result["signals_generated"] = len(signals)
if signals:
print(f"[TradingServer] {symbol} 生成了 {len(signals)} 个信号")
# 2. 策略层做决策
decision = self.strategy_service.make_decision(symbol, current_price)
if decision:
# 记录决策历史
self._decision_history.append(decision)
result["decision"] = decision.to_dict()
# 广播决策
self._broadcast_decision(decision)
# 3. 自动执行决策(如果允许)
if decision.action != "none" and decision.status != "rejected":
order_id = self.strategy_service.execute_decision(decision)
if order_id:
result["pending_order"] = {
"order_id": order_id,
"symbol": decision.symbol,
"action": decision.action,
"price": decision.entry_price,
"volume": decision.volume,
"sl": decision.sl,
"tp": decision.tp
}
return result
# ==================== 订单确认回调 ====================
def _on_order_confirmed(self, order: PendingOrder):
"""订单确认回调"""
print(f"[TradingServer] 订单确认: {order.order_id}")
# 广播订单确认
self._broadcast_pending_order(order)
try:
# 创建交易指令
instruction = TradeInstruction(
symbol=order.get('symbol', ''),
action=order.get('action', 'b'),
mount=order.get('mount', 0.01),
price=order.get('price', 0),
sl=order.get('sl', 0),
tp=order.get('tp', 0),
description=order.get('description', '')
)
print(f"[TradingServer] 创建交易指令: symbol={instruction.symbol}, action={instruction.action}, mount={instruction.mount}, price={instruction.price}, sl={instruction.sl}, tp={instruction.tp}, description={instruction.description}")
# 添加到交易队列
result = self.add_trade_instruction([instruction])
print(f"[TradingServer] 订单已加入交易队列: {result}")
instruction_id = self.trading_instruction_service.create_from_pending_order(order)
print(f"[TradingServer] 交易指令已创建: {instruction_id}")
except Exception as e:
print(f"[TradingServer] 加入交易队列失败: {e}")
print(f"[TradingServer] 创建交易指令失败: {e}")
import traceback
traceback.print_exc()
def add_trade_instruction(self, instructions: List[TradeInstruction]) -> dict:
"""
添加交易指令
返回一个字典包含添加和拒绝的数量
在此处对缺失的 sl/tp 值进行补全
- sl 若未设置保持0.0
- tp 若未设置则默认 0.005
同时按照规则进行价格检查
* 买入指令: price 需大于 sl 且小于 tp
* 卖出指令: price 需小于 sl 且大于 tp
sl tp 未设置<=0则忽略检查
"""
with self.lock:
added = 0
rejected = 0
for instruction in instructions:
# 填充默认值
if instruction.sl is None:
instruction.sl = 0.0
if instruction.tp is None or instruction.tp <= 0.0:
instruction.tp = 0.005
# 验证价格与 SL/TP 关系
if instruction.sl > 0 and instruction.tp > 0:
if instruction.action.lower() == 'b':
if not (instruction.price > instruction.sl and instruction.price < instruction.tp):
print(f"[警告] 忽略无效买入指令: {instruction}")
rejected += 1
continue
elif instruction.action.lower() == 's':
if not (instruction.price < instruction.sl and instruction.price > instruction.tp):
print(f"[警告] 忽略无效卖出指令: {instruction}")
rejected += 1
continue
# 直接使用原始symbol,不做转换
symbol = instruction.symbol
self.trade_instructions[symbol].append(instruction)
added += 1
print(f"[信息] 已添加 {added} 条交易指令, 拒绝 {rejected}")
for symbol, trades in self.trade_instructions.items():
print(f" {symbol}: {len(trades)} 条待执行")
return {"added": added, "rejected": rejected}
# ==================== EA 接口 ====================
def get_trades_by_symbol(self, symbol: str, price: Optional[float] = None) -> Dict:
"""
获取指定SYMBOL的交易指令并删除
EA获取交易数据
同时调用策略检查 PivotMonitor 中执行
返回: {"trades": [...], "pivot_alerts": [...]}
返回
- trades: 待执行的交易指令
- pending_orders: 待确认的订单
- close_tickets: 平仓指令
"""
# 检查所有策略(关键点位、支撑压力、AI趋势
pivot_alerts = []
# 处理价格(生成信号和决策
process_result = {}
if price is not None:
pivot_alerts = self.pivot_monitor.check_and_alert(symbol, price)
if pivot_alerts:
print(f"[信息] {symbol} 当前价格 {price} 接近转折点")
process_result = self.process_price(symbol, price)
with self.lock:
# 调试:打印当前所有待执行指令
if len(self.trade_instructions) > 0:
print(f"[调试] get_trades_by_symbol 查询symbol={symbol}")
print(f"[调试] 当前trade_instructions keys: {list(self.trade_instructions.keys())}")
for k, v in self.trade_instructions.items():
print(f"[调试] {k}: {len(v)}")
# 获取交易指令
trades = self.trading_instruction_service.fetch_instructions_for_ea(symbol, price)
if symbol not in self.trade_instructions or len(self.trade_instructions[symbol]) == 0:
return {"trades": [], "pivot_alerts": pivot_alerts}
# 获取待确认订单
pending_orders = self.pending_order_service.get_pending_orders_dict(symbol)
# 获取所有指令并直接返回
trades = self.trade_instructions[symbol]
result = [{
"symbol": t.symbol,
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
"sl": t.sl,
"tp": t.tp,
"description": t.description or ""
} for t in trades]
# 获取平仓指令
close_tickets = self.get_close_position_instructions(symbol)
# 清空指令队列
self.trade_instructions[symbol] = []
return {
"trades": trades,
"pending_orders": pending_orders,
"close_tickets": close_tickets,
"process_result": process_result
}
if len(result) > 0:
print(f"[信息] 推送了 {len(result)}{symbol} 指令给EA")
# ==================== 交易员接口 ====================
return {"trades": result, "pivot_alerts": pivot_alerts}
def add_trade_instruction(self, instructions: List[TradeInstruction]) -> dict:
"""添加交易指令(交易员手动下单)"""
added = 0
rejected = 0
for instruction in instructions:
sl = instruction.sl if instruction.sl is not None else 0.0
tp = instruction.tp if instruction.tp is not None and instruction.tp > 0 else 0.005
# 验证止损止盈
if sl > 0 and tp > 0:
if instruction.action.lower() == 'b':
if not (instruction.price > sl and instruction.price < tp):
print(f"[警告] 忽略无效买入指令: {instruction}")
rejected += 1
continue
elif instruction.action.lower() == 's':
if not (instruction.price < sl and instruction.price > tp):
print(f"[警告] 忽略无效卖出指令: {instruction}")
rejected += 1
continue
self.trading_instruction_service.create_instruction(
symbol=instruction.symbol,
action=instruction.action,
price=instruction.price,
mount=instruction.mount,
sl=sl,
tp=tp,
description=instruction.description or "",
source="manual"
)
added += 1
print(f"[TradingServer] 已添加 {added} 条交易指令, 拒绝 {rejected}")
return {"added": added, "rejected": rejected}
# ==================== 统计数据 ====================
def save_statistics(self, stat_data: dict) -> None:
"""
保存统计数据
自动保留最新10条
"""
with self.lock:
self.statistics_history.append(stat_data)
print(f"[信息] 统计数据已记录 - {stat_data.get('timestamp', 'unknown')}")
print(f" 当前保存数据条数: {len(self.statistics_history)}")
"""保存统计数据"""
self.statistics_service.process_statistics(stat_data)
def get_latest_statistics(self, count: int = 10) -> List[Dict]:
"""
获取最新的统计数据
"""
with self.lock:
return list(self.statistics_history)[-count:]
"""获取最新的统计数据"""
stats = self.statistics_store.get_all_recent(count)
return [s.to_dict() for s in stats]
# ==================== 指令管理 ====================
def get_all_pending_trades(self) -> Dict[str, List[Dict]]:
"""
获取所有待执行的交易指令不删除
用于查询接口
"""
with self.lock:
result = {}
for symbol, trades in self.trade_instructions.items():
result[symbol] = [
{
"symbol": t.symbol,
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
"sl": t.sl,
"tp": t.tp,
"description": t.description or ""
}
for t in trades
]
return result
"""获取所有待执行的交易指令"""
return self.trading_instruction_service.get_all_instructions_dict()
def clear_trades(self, symbol: Optional[str] = None) -> int:
"""
清空交易指令
如果指定symbol则只清空该symbol
返回清空的指令数量
"""
with self.lock:
if symbol is None:
total = sum(len(trades) for trades in self.trade_instructions.values())
self.trade_instructions.clear()
print(f"[信息] 已清空所有交易指令,共 {total}")
return total
else:
count = len(self.trade_instructions.get(symbol, []))
if symbol in self.trade_instructions:
del self.trade_instructions[symbol]
print(f"[信息] 已清空 {symbol} 的交易指令,共 {count}")
return count
"""清空交易指令"""
if symbol is None:
count = self.trading_instruction_service.get_total_count()
self.trading_instruction_service.clear_all()
print(f"[TradingServer] 已清空所有交易指令,共 {count}")
return count
else:
return self.trading_instruction_service.clear_by_symbol(symbol)
def add_close_position_instruction(self, symbol: str, ticket: int) -> None:
"""
添加平仓指令
"""
"""添加平仓指令"""
with self.lock:
self.close_position_instructions[symbol].append(ticket)
print(f"[信息] 添加平仓指令: {symbol} ticket={ticket}")
if not hasattr(self, '_close_position_instructions'):
self._close_position_instructions = defaultdict(list)
self._close_position_instructions[symbol].append(ticket)
print(f"[TradingServer] 添加平仓指令: {symbol} ticket={ticket}")
def get_close_position_instructions(self, symbol: str) -> List[int]:
"""
获取并清空平仓指令
"""
"""获取并清空平仓指令"""
with self.lock:
tickets = self.close_position_instructions.get(symbol, [])
self.close_position_instructions[symbol] = []
if not hasattr(self, '_close_position_instructions'):
return []
tickets = self._close_position_instructions.get(symbol, [])
self._close_position_instructions[symbol] = []
if tickets:
print(f"[信息] 返回平仓指令: {symbol} tickets={tickets}")
print(f"[TradingServer] 返回平仓指令: {symbol} tickets={tickets}")
return tickets
# ==================== 决策历史 ====================
def get_decision_history(self, symbol: str = None, count: int = 20) -> List[Dict]:
"""获取决策历史"""
decisions = list(self._decision_history)
if symbol:
decisions = [d for d in decisions if d.symbol == symbol]
return [d.to_dict() for d in decisions[-count:]]
# ==================== LLM 分析接口(内部封装) ====================
def get_llm_analysis(self, symbol: str = None) -> Dict:
"""获取大模型分析结果"""
return self.llm_analyzer.get_analysis(symbol)
def get_llm_status(self) -> Dict:
"""获取大模型分析器状态"""
status = self.llm_analyzer.get_status()
status["interval_seconds"] = self.llm_analyzer.ANALYZE_INTERVAL
return status
def get_llm_config(self) -> Dict:
"""获取大模型配置"""
return self.llm_analyzer.get_config()
def trigger_llm_analysis(self) -> Dict:
"""手动触发大模型分析"""
return self.llm_analyzer.trigger_analysis()
def configure_llm(self, api_key: str = None, api_base: str = None, model: str = None) -> Dict:
"""配置大模型参数"""
return self.llm_analyzer.configure(api_key, api_base, model)
# ==================== 状态查询 ====================
def get_status(self) -> Dict:
"""获取服务状态"""
return {
"ws_clients": self.get_ws_client_count(),
"statistics": self.statistics_service.get_status(),
"positions": self.position_service.get_status(),
"trade_history": self.trade_history_service.get_status(),
"pending_orders": self.pending_order_service.get_status(),
"trading_instructions": self.trading_instruction_service.get_status(),
"strategy_service": self.strategy_service.get_status(),
}
-498
View File
@@ -1,498 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
高性能行情分析交易服务
支持
1. EA推送交易指令和接收统计数据
2. 交易员下发交易指令
3. 交易员查看统计数据
"""
from fastapi import FastAPI, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel
from typing import List, Dict, Optional
from collections import deque, defaultdict
from datetime import datetime
import json
import threading
import uvicorn
# ==================== 数据模型定义 ====================
class TradeInstruction(BaseModel):
"""交易指令模型"""
symbol: str # 交易品种,如 "gold"
action: str # b=买入, s=卖出
mount: float # 手数
price: float # 指令执行价格(买入时为买入价,卖出时为卖出价)
sl: Optional[float] = 0.0 # 止损点, 可以缺省
tp: Optional[float] = 0.0 # 止盈点, 可以缺省,若未指定将在服务端设置为0.005
class StatisticData(BaseModel):
"""统计数据模型"""
timestamp: str # 时间戳
tickCount: int # TICK计数
bidPrice: float # 买价
askPrice: float # 卖价
balance: float # 账户余额
equity: float # 账户权益
marginLevel: float # 预付款比例
positions: list # 持仓信息
trades: list # 交易记录
# ==================== 全局数据存储 ====================
class TradingServer:
"""交易服务主类"""
def __init__(self):
# 交易指令队列 - 按SYMBOL分类
# 结构: {"SYMBOL1": [TradeInstruction1, ...], "SYMBOL2": [...]}
self.trade_instructions = defaultdict(list)
# 统计数据历史 - 保留最新10条
# 结构: deque([{stat_data1}, {stat_data2}, ...], maxlen=10)
self.statistics_history = deque(maxlen=10)
# 线程锁 - 确保线程安全
self.lock = threading.RLock()
print("[信息] 交易服务已初始化")
def add_trade_instruction(self, instructions: List[TradeInstruction]) -> int:
"""
添加交易指令
返回添加的指令数量
在此处对缺失的 sl/tp 值进行补全
- sl 若未设置保持0.0
- tp 若未设置则默认 0.005
"""
with self.lock:
count = 0
for instruction in instructions:
# 填充默认值
if instruction.sl is None:
instruction.sl = 0.0
if instruction.tp is None or instruction.tp <= 0.0:
instruction.tp = 0.005
symbol = instruction.symbol.upper()
self.trade_instructions[symbol].append(instruction)
count += 1
print(f"[信息] 已添加 {count} 条交易指令")
for symbol, trades in self.trade_instructions.items():
print(f" {symbol}: {len(trades)} 条待执行")
return count
def get_trades_by_symbol(self, symbol: str, price: Optional[float] = None) -> List[Dict]:
"""
获取指定SYMBOL的交易指令并删除
根据价格条件过滤指令
- 买入指令action='b'如果指令价格 > 当前价格缓存等待价格下跌到指令价格
- 卖出指令action='s'如果指令价格 < 当前价格缓存等待价格上涨到指令价格
返回指令列表JSON格式
"""
with self.lock:
symbol = symbol.upper()
if symbol not in self.trade_instructions or len(self.trade_instructions[symbol]) == 0:
return []
# 获取所有指令
trades = self.trade_instructions[symbol]
result = []
cached_trades = []
for t in trades:
should_send = True
# 如果提供了价格,进行条件检查
if price is not None:
if t.action.lower() == 'b':
# 买入指令:如果指令价格 > 当前价格,则缓存
if t.price > price:
should_send = False
cached_trades.append(t)
elif t.action.lower() == 's':
# 卖出指令:如果指令价格 < 当前价格,则缓存
if t.price < price:
should_send = False
cached_trades.append(t)
if should_send:
result.append({
"symbol": t.symbol.lower(),
"action": t.action.lower(),
"mount": t.mount,
"price": t.price,
"sl": t.sl,
"tp": t.tp
})
# 更新指令队列:移除已发送的,保留已缓存的
self.trade_instructions[symbol] = cached_trades
if len(result) > 0:
print(f"[信息] 推送了 {len(result)}{symbol} 指令给EA (当前价格: {price})")
if len(cached_trades) > 0:
print(f"[信息] 缓存了 {len(cached_trades)}{symbol} 指令,等待价格条件满足")
return result
def save_statistics(self, stat_data: dict) -> None:
"""
保存统计数据
自动保留最新10条
"""
with self.lock:
self.statistics_history.append(stat_data)
print(f"[信息] 统计数据已记录 - {stat_data.get('timestamp', 'unknown')}")
print(f" 当前保存数据条数: {len(self.statistics_history)}")
def get_latest_statistics(self, count: int = 10) -> List[Dict]:
"""
获取最新的统计数据
"""
with self.lock:
return list(self.statistics_history)[-count:]
def get_all_pending_trades(self) -> Dict[str, List[Dict]]:
"""
获取所有待执行的交易指令不删除
用于查询接口
"""
with self.lock:
result = {}
for symbol, trades in self.trade_instructions.items():
result[symbol] = [
{
"symbol": t.symbol.lower(),
"action": t.action.lower(),
"mount": t.mount,
"sl": t.sl,
"tp": t.tp
}
for t in trades
]
return result
def clear_trades(self, symbol: Optional[str] = None) -> int:
"""
清空交易指令
如果指定symbol则只清空该symbol
返回清空的指令数量
"""
with self.lock:
if symbol is None:
total = sum(len(trades) for trades in self.trade_instructions.values())
self.trade_instructions.clear()
print(f"[信息] 已清空所有交易指令,共 {total}")
return total
else:
symbol = symbol.upper()
count = len(self.trade_instructions.get(symbol, []))
if symbol in self.trade_instructions:
del self.trade_instructions[symbol]
print(f"[信息] 已清空 {symbol} 的交易指令,共 {count}")
return count
# ==================== 创建应用 ====================
app = FastAPI(title="行情分析交易服务", version="1.0")
server = TradingServer()
# ==================== EA相关接口 ====================
@app.get("/get_trades")
async def get_trades(symbol: str = "gold", price: Optional[float] = None):
"""
EA调用获取待执行的交易指令
参数
- symbol: 交易品种 (e.g., "gold")
- price: 当前价格用于价格条件过滤 (可选)
返回JSON列表包含满足价格条件的待执行指令
价格过滤逻辑
- 买入指令(action='b')若目标价格(tp) > 当前价格则缓存不发送
- 卖出指令(action='s')若目标价格(tp) < 当前价格则缓存不发送
"""
try:
trades = server.get_trades_by_symbol(symbol, price)
return JSONResponse(content=trades)
except Exception as e:
print(f"[错误] get_trades 异常: {str(e)}")
return JSONResponse(status_code=500, content={"error": str(e)})
@app.post("/send_statistics")
async def send_statistics(data: dict):
"""
EA调用发送统计数据
参数data - 统计数据JSON
返回确认信息
"""
try:
server.save_statistics(data)
return JSONResponse(
status_code=200,
content={"status": "success", "message": "统计数据已记录"}
)
except Exception as e:
print(f"[错误] send_statistics 异常: {str(e)}")
return JSONResponse(status_code=500, content={"error": str(e)})
# ==================== 交易员相关接口 ====================
@app.post("/send_trade_instructions")
async def send_trade_instructions(instructions: List[TradeInstruction]):
"""
交易员调用下发交易指令
请求示例
POST /send_trade_instructions
[
{
"symbol": "gold",
"action": "b",
"mount": 0.01,
"sl": 5000,
"tp": 5100
},
{
"symbol": "eurusd",
"action": "s",
"mount": 0.02,
"sl": 1.0950,
"tp": 1.0850
}
]
返回
{
"status": "success",
"count": 2,
"message": "已添加 2 条交易指令"
}
"""
try:
if not instructions:
raise HTTPException(status_code=400, detail="指令列表不能为空")
count = server.add_trade_instruction(instructions)
return JSONResponse(
status_code=200,
content={
"status": "success",
"count": count,
"message": f"已添加 {count} 条交易指令"
}
)
except Exception as e:
print(f"[错误] send_trade_instructions 异常: {str(e)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
@app.get("/query_statistics")
async def query_statistics(count: int = 10):
"""
交易员调用查询统计数据
参数count - 获取最新N条数据默认10条
返回示例
[
{
"timestamp": "2026-03-04 14:30",
"tickCount": 1234,
"bidPrice": 2035.50,
"askPrice": 2035.60,
"balance": 50000.00,
"equity": 51234.56,
"marginLevel": 98.50,
"positions": [...],
"trades": [...]
},
...
]
"""
try:
if count <= 0 or count > 100:
count = 10
stats = server.get_latest_statistics(count)
return JSONResponse(
status_code=200,
content={
"status": "success",
"count": len(stats),
"data": stats
}
)
except Exception as e:
print(f"[错误] query_statistics 异常: {str(e)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
@app.get("/query_pending_trades")
async def query_pending_trades():
"""
交易员调用查询所有待执行的交易指令
返回示例
{
"status": "success",
"total": 5,
"data": {
"GOLD": [
{
"symbol": "gold",
"action": "b",
"mount": 0.01,
"sl": 5000,
"tp": 5100
}
],
"EURUSD": [...]
}
}
"""
try:
trades = server.get_all_pending_trades()
total = sum(len(t) for t in trades.values())
return JSONResponse(
status_code=200,
content={
"status": "success",
"total": total,
"data": trades
}
)
except Exception as e:
print(f"[错误] query_pending_trades 异常: {str(e)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
@app.delete("/clear_trades")
async def clear_trades(symbol: Optional[str] = None):
"""
交易员调用清空交易指令
参数
- symbol (可选): 指定品种不指定则清空所有
返回示例
{
"status": "success",
"cleared": 3,
"message": "已清空 3 条交易指令"
}
"""
try:
count = server.clear_trades(symbol)
return JSONResponse(
status_code=200,
content={
"status": "success",
"cleared": count,
"message": f"已清空 {count} 条交易指令"
}
)
except Exception as e:
print(f"[错误] clear_trades 异常: {str(e)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
# ==================== 系统接口 ====================
@app.get("/health")
async def health_check():
"""健康检查接口"""
return JSONResponse(
status_code=200,
content={
"status": "healthy",
"service": "Trading Analysis Server",
"timestamp": datetime.now().isoformat()
}
)
@app.get("/status")
async def get_status():
"""
获取服务状态
返回示例
{
"status": "running",
"pending_trades": {
"GOLD": 2,
"EURUSD": 1
},
"statistics_records": 5,
"timestamp": "2026-03-04T14:30:45.123456"
}
"""
try:
pending = server.get_all_pending_trades()
pending_count = {symbol: len(trades) for symbol, trades in pending.items()}
return JSONResponse(
status_code=200,
content={
"status": "running",
"pending_trades": pending_count,
"total_pending": sum(pending_count.values()),
"statistics_records": len(server.statistics_history),
"timestamp": datetime.now().isoformat()
}
)
except Exception as e:
print(f"[错误] get_status 异常: {str(e)}")
return JSONResponse(
status_code=500,
content={"status": "error", "message": str(e)}
)
# ==================== 应用启动 ====================
if __name__ == "__main__":
print("=" * 60)
print("启动行情分析交易服务")
print("=" * 60)
print(f"[INFO] 服务将运行在 http://localhost:5858")
print(f"[INFO] API文档: http://localhost:5858/docs")
print(f"[INFO] 备用文档: http://localhost:5858/redoc")
print("=" * 60)
# 使用uvicorn启动服务,设置高并发参数
uvicorn.run(
app,
host="127.0.0.1",
port=5858,
workers=4, # 多个worker进程
loop="uvloop", # 使用高性能事件循环
log_level="info"
)