Refactor: restructure market module with services, stores, and utils
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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', '')
|
||||
)
|
||||
@@ -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', [])
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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))
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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', '')
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+64
-1593
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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',
|
||||
]
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
策略服务模块
|
||||
"""
|
||||
|
||||
from .strategy_service import StrategyService
|
||||
from .risk_manager import RiskManager
|
||||
|
||||
__all__ = [
|
||||
'StrategyService',
|
||||
'RiskManager',
|
||||
]
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
@@ -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
@@ -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
|
||||
@@ -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'
|
||||
]
|
||||
@@ -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] 已清空所有数据")
|
||||
@@ -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] 已清空所有数据")
|
||||
@@ -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
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()),
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
|
||||
from .ws_manager import WebSocketManager
|
||||
|
||||
__all__ = ['WebSocketManager']
|
||||
@@ -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
@@ -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
@@ -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:00,offset=-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
@@ -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
@@ -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",
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user