feat: 自动重连, 并在重连失败时发送邮件警报
This commit is contained in:
parent
a9e074a116
commit
1c1b19383c
@ -3,17 +3,17 @@ import datetime
|
|||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
# Server settings
|
# Server settings
|
||||||
PORT = int(os.environ.get("PORT", 9527))
|
PORT = 9527
|
||||||
HOST = os.environ.get("HOST", "0.0.0.0")
|
HOST = "0.0.0.0"
|
||||||
DEBUG = os.environ.get("DEBUG", "False").lower() == "true"
|
DEBUG = False
|
||||||
|
|
||||||
# Trading settings
|
# Trading settings
|
||||||
TRADE_TIMEOUT = int(os.environ.get("TRADE_TIMEOUT", 5)) # 交易超时时间(秒)
|
TRADE_TIMEOUT = 5 # 交易超时时间(秒)
|
||||||
SIMULATION_MODE = True
|
SIMULATION_MODE = True
|
||||||
|
|
||||||
# Trading hours
|
# Trading hours
|
||||||
MARKET_OPEN_TIME = os.environ.get("MARKET_OPEN_TIME", "09:20")
|
MARKET_OPEN_TIME = "09:20"
|
||||||
MARKET_CLOSE_TIME = os.environ.get("MARKET_CLOSE_TIME", "15:10")
|
MARKET_CLOSE_TIME = "15:10"
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
LOG_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "logs")
|
||||||
@ -30,13 +30,28 @@ class Config:
|
|||||||
RATE_LIMIT_PERIOD = 60 # seconds
|
RATE_LIMIT_PERIOD = 60 # seconds
|
||||||
|
|
||||||
# XtQuant 相关配置
|
# XtQuant 相关配置
|
||||||
XT_ACCOUNT = os.environ.get("XT_ACCOUNT", "80391818")
|
XT_ACCOUNT = "80391818"
|
||||||
XT_PATH = os.environ.get("XT_PATH", r'C:\\江海证券QMT实盘_交易\\userdata_mini')
|
XT_PATH = r'C:\\江海证券QMT实盘_交易\\userdata_mini'
|
||||||
|
|
||||||
|
# 重连相关配置
|
||||||
|
XT_RECONNECT_INTERVAL = 3600 # 重连尝试间隔(秒)
|
||||||
|
XT_MAX_SESSION_ID = 999999 # 最大会话ID
|
||||||
|
XT_MIN_SESSION_ID = 100000 # 最小会话ID
|
||||||
|
XT_SESSION_ID_RANGE = 20 # 一次尝试的会话ID数量
|
||||||
|
|
||||||
|
# 邮件通知配置
|
||||||
|
MAIL_ENABLED = True
|
||||||
|
MAIL_SERVER = "mail.yushaoyou.com"
|
||||||
|
MAIL_PORT = 465
|
||||||
|
MAIL_USERNAME = "jq@yushaoyou.com"
|
||||||
|
MAIL_PASSWORD = "zhiyong214"
|
||||||
|
MAIL_FROM = "自动交易服务器"
|
||||||
|
MAIL_TO = ["jq@yushaoyou.com"] # 可以是多个邮箱
|
||||||
|
|
||||||
# RealTraderManager配置
|
# RealTraderManager配置
|
||||||
RTM_ORDER_TIMEOUT = int(os.environ.get("RTM_ORDER_TIMEOUT", 60)) # 订单超时时间(秒)
|
RTM_ORDER_TIMEOUT = 60 # 订单超时时间(秒)
|
||||||
RTM_MAX_RETRIES = int(os.environ.get("RTM_MAX_RETRIES", 3)) # 最大重试次数
|
RTM_MAX_RETRIES = 3 # 最大重试次数
|
||||||
RTM_USE_MARKET_ORDER = os.environ.get("RTM_USE_MARKET_ORDER", "True").lower() == "true" # 是否使用市价单进行补单
|
RTM_USE_MARKET_ORDER = True # 是否使用市价单进行补单
|
||||||
|
|
||||||
# 计划任务运行时间
|
# 计划任务运行时间
|
||||||
STRATEGY_SAVE_TIME = "15:10" # 每天保存策略数据的时间
|
STRATEGY_SAVE_TIME = "15:10" # 每天保存策略数据的时间
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from config import Config
|
from config import Config
|
||||||
from base_trader import BaseTrader
|
from base_trader import BaseTrader
|
||||||
from xtquant.xttrader import XtQuantTrader
|
from xtquant.xttrader import XtQuantTrader
|
||||||
@ -7,6 +9,7 @@ from xtquant.xttype import StockAccount
|
|||||||
from xtquant import xtconstant
|
from xtquant import xtconstant
|
||||||
from xtquant.xtdata import get_instrument_detail
|
from xtquant.xtdata import get_instrument_detail
|
||||||
from logger_config import get_logger
|
from logger_config import get_logger
|
||||||
|
from utils.mail_util import MailUtil
|
||||||
|
|
||||||
# 获取日志记录器
|
# 获取日志记录器
|
||||||
logger = get_logger('real_trader')
|
logger = get_logger('real_trader')
|
||||||
@ -17,11 +20,24 @@ class MyXtQuantTraderCallback:
|
|||||||
def on_connected(self):
|
def on_connected(self):
|
||||||
logger.info("连接成功")
|
logger.info("连接成功")
|
||||||
def on_disconnected(self):
|
def on_disconnected(self):
|
||||||
|
"""连接断开回调
|
||||||
|
|
||||||
|
当交易连接断开时调用,会自动尝试重连
|
||||||
|
如果重连失败,将设置连接状态为失败,并通过邮件通知
|
||||||
|
"""
|
||||||
logger.warning("连接断开")
|
logger.warning("连接断开")
|
||||||
if self.trader_instance:
|
if self.trader_instance:
|
||||||
|
# 设置连接状态
|
||||||
|
self.trader_instance.connected = False
|
||||||
|
self.trader_instance.subscribed = False
|
||||||
|
|
||||||
|
# 尝试重连
|
||||||
if not self.trader_instance.reconnect():
|
if not self.trader_instance.reconnect():
|
||||||
logger.error("重连失败")
|
logger.error("重连失败")
|
||||||
raise Exception("断线重连失败")
|
# 通知重连失败
|
||||||
|
self.trader_instance.connection_failed = True
|
||||||
|
self.trader_instance.last_reconnect_time = time.time()
|
||||||
|
self.trader_instance.notify_connection_failure()
|
||||||
def on_account_status(self, status):
|
def on_account_status(self, status):
|
||||||
logger.info(f"账号状态: {status.account_id} {status.status}")
|
logger.info(f"账号状态: {status.account_id} {status.status}")
|
||||||
def on_stock_asset(self, asset):
|
def on_stock_asset(self, asset):
|
||||||
@ -44,17 +60,26 @@ class MyXtQuantTraderCallback:
|
|||||||
logger.info(f"约券异步反馈: {response.seq}")
|
logger.info(f"约券异步反馈: {response.seq}")
|
||||||
|
|
||||||
class XtTrader(BaseTrader):
|
class XtTrader(BaseTrader):
|
||||||
def __init__(self):
|
def __init__(self, connect_failed_callback=None):
|
||||||
super().__init__(logger)
|
super().__init__(logger)
|
||||||
self.started = False
|
self.started = False
|
||||||
self.connected = False
|
self.connected = False
|
||||||
self.subscribed = False
|
self.subscribed = False
|
||||||
self._ACCOUNT = Config.XT_ACCOUNT
|
self._ACCOUNT = Config.XT_ACCOUNT
|
||||||
self._PATH = Config.XT_PATH
|
self._PATH = Config.XT_PATH
|
||||||
self._SESSION_ID = random.randint(100000, 99999999)
|
self._SESSION_ID = random.randint(Config.XT_MIN_SESSION_ID, Config.XT_MAX_SESSION_ID)
|
||||||
self._account_type = os.environ.get("XT_ACCOUNT_TYPE", "STOCK")
|
self._account_type = os.environ.get("XT_ACCOUNT_TYPE", "STOCK")
|
||||||
self._strategy_name = os.environ.get("XT_STRATEGY_NAME", "xt_strategy")
|
self._strategy_name = os.environ.get("XT_STRATEGY_NAME", "xt_strategy")
|
||||||
self._remark = os.environ.get("XT_REMARK", "remark")
|
self._remark = os.environ.get("XT_REMARK", "remark")
|
||||||
|
|
||||||
|
# 重连相关
|
||||||
|
self.connection_failed = False
|
||||||
|
self.last_reconnect_time = None
|
||||||
|
self.reconnect_interval = Config.XT_RECONNECT_INTERVAL
|
||||||
|
self.connect_failed_callback = connect_failed_callback
|
||||||
|
self.connection_error_message = None
|
||||||
|
|
||||||
|
# 初始化trader
|
||||||
self._callback = MyXtQuantTraderCallback(self)
|
self._callback = MyXtQuantTraderCallback(self)
|
||||||
self.xt_trader = XtQuantTrader(self._PATH, self._SESSION_ID)
|
self.xt_trader = XtQuantTrader(self._PATH, self._SESSION_ID)
|
||||||
self.account = StockAccount(self._ACCOUNT, self._account_type)
|
self.account = StockAccount(self._ACCOUNT, self._account_type)
|
||||||
@ -68,6 +93,14 @@ class XtTrader(BaseTrader):
|
|||||||
"""
|
"""
|
||||||
return self.started and self.connected and self.subscribed
|
return self.started and self.connected and self.subscribed
|
||||||
|
|
||||||
|
def is_available(self):
|
||||||
|
"""检查交易接口是否可用
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True表示可用,False表示不可用
|
||||||
|
"""
|
||||||
|
return self.is_logged_in() and not self.connection_failed
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
if not self.started:
|
if not self.started:
|
||||||
self.xt_trader.start()
|
self.xt_trader.start()
|
||||||
@ -88,6 +121,9 @@ class XtTrader(BaseTrader):
|
|||||||
self.subscribed = False
|
self.subscribed = False
|
||||||
|
|
||||||
def get_balance(self):
|
def get_balance(self):
|
||||||
|
if not self.is_available():
|
||||||
|
return None
|
||||||
|
|
||||||
asset = self.xt_trader.query_stock_asset(self.account)
|
asset = self.xt_trader.query_stock_asset(self.account)
|
||||||
if asset:
|
if asset:
|
||||||
return {
|
return {
|
||||||
@ -100,6 +136,9 @@ class XtTrader(BaseTrader):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_positions(self):
|
def get_positions(self):
|
||||||
|
if not self.is_available():
|
||||||
|
return []
|
||||||
|
|
||||||
positions = self.xt_trader.query_stock_positions(self.account)
|
positions = self.xt_trader.query_stock_positions(self.account)
|
||||||
if positions:
|
if positions:
|
||||||
return [
|
return [
|
||||||
@ -119,6 +158,9 @@ class XtTrader(BaseTrader):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_position(self, stock_code):
|
def get_position(self, stock_code):
|
||||||
|
if not self.is_available():
|
||||||
|
return None
|
||||||
|
|
||||||
position = self.xt_trader.query_stock_position(self.account, stock_code)
|
position = self.xt_trader.query_stock_position(self.account, stock_code)
|
||||||
if position:
|
if position:
|
||||||
return {
|
return {
|
||||||
@ -136,6 +178,9 @@ class XtTrader(BaseTrader):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def get_today_trades(self):
|
def get_today_trades(self):
|
||||||
|
if not self.is_available():
|
||||||
|
return []
|
||||||
|
|
||||||
trades = self.xt_trader.query_stock_trades(self.account)
|
trades = self.xt_trader.query_stock_trades(self.account)
|
||||||
if trades:
|
if trades:
|
||||||
return [
|
return [
|
||||||
@ -155,6 +200,9 @@ class XtTrader(BaseTrader):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_today_orders(self):
|
def get_today_orders(self):
|
||||||
|
if not self.is_available():
|
||||||
|
return []
|
||||||
|
|
||||||
orders = self.xt_trader.query_stock_orders(self.account)
|
orders = self.xt_trader.query_stock_orders(self.account)
|
||||||
if orders:
|
if orders:
|
||||||
return [
|
return [
|
||||||
@ -176,6 +224,9 @@ class XtTrader(BaseTrader):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_order(self, order_id):
|
def get_order(self, order_id):
|
||||||
|
if not self.is_available():
|
||||||
|
return None
|
||||||
|
|
||||||
order = self.xt_trader.query_stock_order(self.account, int(order_id))
|
order = self.xt_trader.query_stock_order(self.account, int(order_id))
|
||||||
if order:
|
if order:
|
||||||
return {
|
return {
|
||||||
@ -226,6 +277,9 @@ class XtTrader(BaseTrader):
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
def buy(self, code, price, amount, order_type='limit'):
|
def buy(self, code, price, amount, order_type='limit'):
|
||||||
|
if not self.is_available():
|
||||||
|
return {"error": self.connection_error_message or "交易系统连接失败"}
|
||||||
|
|
||||||
# 确定价格类型
|
# 确定价格类型
|
||||||
price_type = xtconstant.MARKET_BEST if order_type == 'market' else xtconstant.FIX_PRICE
|
price_type = xtconstant.MARKET_BEST if order_type == 'market' else xtconstant.FIX_PRICE
|
||||||
|
|
||||||
@ -239,6 +293,9 @@ class XtTrader(BaseTrader):
|
|||||||
return {"order_id": order_id}
|
return {"order_id": order_id}
|
||||||
|
|
||||||
def sell(self, code, price, amount, order_type='limit'):
|
def sell(self, code, price, amount, order_type='limit'):
|
||||||
|
if not self.is_available():
|
||||||
|
return {"error": self.connection_error_message or "交易系统连接失败"}
|
||||||
|
|
||||||
# 确定价格类型
|
# 确定价格类型
|
||||||
price_type = xtconstant.MARKET_BEST if order_type == 'market' else xtconstant.FIX_PRICE
|
price_type = xtconstant.MARKET_BEST if order_type == 'market' else xtconstant.FIX_PRICE
|
||||||
|
|
||||||
@ -252,6 +309,9 @@ class XtTrader(BaseTrader):
|
|||||||
return {"order_id": order_id}
|
return {"order_id": order_id}
|
||||||
|
|
||||||
def cancel(self, order_id):
|
def cancel(self, order_id):
|
||||||
|
if not self.is_available():
|
||||||
|
return {"success": False, "message": self.connection_error_message or "交易系统连接失败"}
|
||||||
|
|
||||||
# 撤单接口需要订单编号
|
# 撤单接口需要订单编号
|
||||||
result = self.xt_trader.cancel_order_stock(self.account, int(order_id))
|
result = self.xt_trader.cancel_order_stock(self.account, int(order_id))
|
||||||
return {"success": result == 0, "message": f"撤单结果: {result}"}
|
return {"success": result == 0, "message": f"撤单结果: {result}"}
|
||||||
@ -265,6 +325,9 @@ class XtTrader(BaseTrader):
|
|||||||
Returns:
|
Returns:
|
||||||
dict: 行情数据,如果获取失败则返回None
|
dict: 行情数据,如果获取失败则返回None
|
||||||
"""
|
"""
|
||||||
|
if not self.is_available():
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
quote = self.xt_trader.query_quote(code)
|
quote = self.xt_trader.query_quote(code)
|
||||||
if quote:
|
if quote:
|
||||||
@ -284,7 +347,36 @@ class XtTrader(BaseTrader):
|
|||||||
logger.error(f"获取行情失败: {code}, {str(e)}")
|
logger.error(f"获取行情失败: {code}, {str(e)}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def notify_connection_failure(self):
|
||||||
|
"""通知交易连接失败"""
|
||||||
|
self.connection_error_message = f"交易系统连接失败,将在{self.reconnect_interval//60}分钟后自动尝试重连"
|
||||||
|
|
||||||
|
# 调用回调通知上层应用
|
||||||
|
if self.connect_failed_callback:
|
||||||
|
self.connect_failed_callback()
|
||||||
|
|
||||||
|
# 发送邮件通知
|
||||||
|
trader_info = f"账户:{self._ACCOUNT},会话ID:{self._SESSION_ID}"
|
||||||
|
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
subject = f"[交易系统] 连接失败通知 - {time_str}"
|
||||||
|
body = f"""
|
||||||
|
交易系统连接失败,请尽快检查!
|
||||||
|
|
||||||
|
时间:{time_str}
|
||||||
|
{trader_info}
|
||||||
|
错误信息:交易连接断开且重连失败
|
||||||
|
|
||||||
|
系统将在{self.reconnect_interval//60}分钟后自动尝试重连。如需立即恢复,请手动重启交易系统。
|
||||||
|
"""
|
||||||
|
|
||||||
|
MailUtil.send_mail(subject, body)
|
||||||
|
|
||||||
def reconnect(self):
|
def reconnect(self):
|
||||||
|
"""尝试重新连接交易系统
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 重连是否成功
|
||||||
|
"""
|
||||||
# 关闭旧连接
|
# 关闭旧连接
|
||||||
if self.started:
|
if self.started:
|
||||||
self.xt_trader.stop()
|
self.xt_trader.stop()
|
||||||
@ -293,7 +385,9 @@ class XtTrader(BaseTrader):
|
|||||||
self.subscribed = False
|
self.subscribed = False
|
||||||
|
|
||||||
# 尝试范围内的新session_id
|
# 尝试范围内的新session_id
|
||||||
session_id_range = range(100000, 100020)
|
start_id = Config.XT_MIN_SESSION_ID
|
||||||
|
end_id = start_id + Config.XT_SESSION_ID_RANGE
|
||||||
|
session_id_range = range(start_id, end_id)
|
||||||
for session_id in random.sample(list(session_id_range), len(session_id_range)):
|
for session_id in random.sample(list(session_id_range), len(session_id_range)):
|
||||||
self._SESSION_ID = session_id
|
self._SESSION_ID = session_id
|
||||||
self.xt_trader = XtQuantTrader(self._PATH, self._SESSION_ID)
|
self.xt_trader = XtQuantTrader(self._PATH, self._SESSION_ID)
|
||||||
@ -311,7 +405,44 @@ class XtTrader(BaseTrader):
|
|||||||
if result == 0:
|
if result == 0:
|
||||||
self.subscribed = True
|
self.subscribed = True
|
||||||
logger.info(f"重连成功,使用session_id: {self._SESSION_ID}")
|
logger.info(f"重连成功,使用session_id: {self._SESSION_ID}")
|
||||||
|
|
||||||
|
# 重置连接失败状态
|
||||||
|
if self.connection_failed:
|
||||||
|
self.connection_failed = False
|
||||||
|
self.connection_error_message = None
|
||||||
|
|
||||||
|
# 通知连接已恢复
|
||||||
|
time_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
subject = f"[交易系统] 连接恢复通知 - {time_str}"
|
||||||
|
body = f"""
|
||||||
|
交易系统连接已恢复!
|
||||||
|
|
||||||
|
时间:{time_str}
|
||||||
|
账户:{self._ACCOUNT},会话ID:{self._SESSION_ID}
|
||||||
|
|
||||||
|
系统已自动恢复连接,交易功能现已正常。
|
||||||
|
"""
|
||||||
|
MailUtil.send_mail(subject, body)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
logger.error("所有尝试都失败,无法重连")
|
logger.error("所有尝试都失败,无法重连")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def check_reconnect(self):
|
||||||
|
"""检查是否需要尝试重连
|
||||||
|
|
||||||
|
此方法应在主程序循环中定期调用,检查是否需要尝试重连
|
||||||
|
"""
|
||||||
|
if (self.connection_failed and
|
||||||
|
self.last_reconnect_time and
|
||||||
|
(time.time() - self.last_reconnect_time) > self.reconnect_interval):
|
||||||
|
|
||||||
|
logger.info("尝试定期重连...")
|
||||||
|
if self.reconnect():
|
||||||
|
logger.info("定期重连成功")
|
||||||
|
self.connection_failed = False
|
||||||
|
self.last_reconnect_time = None
|
||||||
|
else:
|
||||||
|
logger.warning("定期重连失败")
|
||||||
|
self.last_reconnect_time = time.time()
|
@ -21,6 +21,12 @@ def is_real_mode():
|
|||||||
return not Config.SIMULATION_MODE
|
return not Config.SIMULATION_MODE
|
||||||
|
|
||||||
|
|
||||||
|
# 交易接口连接失败回调
|
||||||
|
def on_connect_failed():
|
||||||
|
"""交易连接失败的回调函数"""
|
||||||
|
logger.critical("交易系统连接失败,API将返回错误状态")
|
||||||
|
|
||||||
|
|
||||||
# 获取实盘交易管理器实例的辅助函数
|
# 获取实盘交易管理器实例的辅助函数
|
||||||
def get_real_trader_manager():
|
def get_real_trader_manager():
|
||||||
global _real_trader_manager_instance
|
global _real_trader_manager_instance
|
||||||
@ -38,7 +44,7 @@ def get_trader():
|
|||||||
global _trader_instance
|
global _trader_instance
|
||||||
|
|
||||||
if _trader_instance is None:
|
if _trader_instance is None:
|
||||||
_trader_instance = SimulationTrader() if Config.SIMULATION_MODE else XtTrader()
|
_trader_instance = SimulationTrader() if Config.SIMULATION_MODE else XtTrader(connect_failed_callback=on_connect_failed)
|
||||||
|
|
||||||
return _trader_instance
|
return _trader_instance
|
||||||
|
|
||||||
@ -52,7 +58,7 @@ def login():
|
|||||||
_trader_instance = None
|
_trader_instance = None
|
||||||
|
|
||||||
# 创建新的XtTrader实例
|
# 创建新的XtTrader实例
|
||||||
_trader_instance = SimulationTrader() if Config.SIMULATION_MODE else XtTrader()
|
_trader_instance = SimulationTrader() if Config.SIMULATION_MODE else XtTrader(connect_failed_callback=on_connect_failed)
|
||||||
|
|
||||||
logger.info("开始登录")
|
logger.info("开始登录")
|
||||||
_trader_instance.login()
|
_trader_instance.login()
|
||||||
@ -89,6 +95,12 @@ def setup_scheduler():
|
|||||||
scheduler_thread.start()
|
scheduler_thread.start()
|
||||||
logger.info("定时任务调度器已启动")
|
logger.info("定时任务调度器已启动")
|
||||||
|
|
||||||
|
# 启动重连检查线程(仅在实盘模式下)
|
||||||
|
if is_real_mode():
|
||||||
|
reconnect_thread = threading.Thread(target=_check_reconnect, daemon=True)
|
||||||
|
reconnect_thread.start()
|
||||||
|
logger.info("重连检查线程已启动")
|
||||||
|
|
||||||
return scheduler_thread
|
return scheduler_thread
|
||||||
|
|
||||||
|
|
||||||
@ -102,18 +114,35 @@ def _run_scheduler():
|
|||||||
logger.error(f"调度器执行错误: {str(e)}")
|
logger.error(f"调度器执行错误: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# 设置并启动调度器
|
def _check_reconnect():
|
||||||
setup_scheduler()
|
"""重连检查线程,定期检查是否需要重连"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if is_real_mode() and _trader_instance is not None:
|
||||||
|
_trader_instance.check_reconnect()
|
||||||
|
time.sleep(60) # 每分钟检查一次
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"重连检查错误: {str(e)}")
|
||||||
|
|
||||||
|
|
||||||
# 程序启动时初始化交易实例
|
# 程序启动时初始化交易实例
|
||||||
login()
|
login()
|
||||||
|
|
||||||
# 添加请求频率限制
|
|
||||||
|
# 设置并启动调度器
|
||||||
|
setup_scheduler()
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
@app.route("/yu/healthcheck", methods=["GET"])
|
@app.route("/yu/healthcheck", methods=["GET"])
|
||||||
def health_check():
|
def health_check():
|
||||||
return "ok", 200
|
if is_real_mode() and _trader_instance and not _trader_instance.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"status": "error",
|
||||||
|
"message": _trader_instance.connection_error_message or "交易系统连接失败"
|
||||||
|
}), 503
|
||||||
|
return jsonify({"status": "ok"}), 200
|
||||||
|
|
||||||
|
|
||||||
@app.route("/yu/buy", methods=["POST"])
|
@app.route("/yu/buy", methods=["POST"])
|
||||||
@ -140,8 +169,16 @@ def buy():
|
|||||||
if price <= 0 or amount <= 0:
|
if price <= 0 or amount <= 0:
|
||||||
raise ValueError("Price and amount must be positive")
|
raise ValueError("Price and amount must be positive")
|
||||||
|
|
||||||
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
# 检查是否在交易时间内
|
# 检查是否在交易时间内
|
||||||
if not get_trader().is_trading_time():
|
if not trader.is_trading_time():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}"
|
f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}"
|
||||||
)
|
)
|
||||||
@ -161,7 +198,7 @@ def buy():
|
|||||||
strategy_name, code, ORDER_DIRECTION_BUY, amount, price
|
strategy_name, code, ORDER_DIRECTION_BUY, amount, price
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = get_trader().buy(code, price, amount, strategy_name)
|
result = trader.buy(code, price, amount, strategy_name)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
logger.info(f"买入成功: {result}")
|
logger.info(f"买入成功: {result}")
|
||||||
@ -200,8 +237,16 @@ def sell():
|
|||||||
if price <= 0 or amount <= 0:
|
if price <= 0 or amount <= 0:
|
||||||
raise ValueError("Price and amount must be positive")
|
raise ValueError("Price and amount must be positive")
|
||||||
|
|
||||||
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
# 检查是否在交易时间内
|
# 检查是否在交易时间内
|
||||||
if not get_trader().is_trading_time():
|
if not trader.is_trading_time():
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}"
|
f"交易失败 - 非交易时间不能交易 - 代码: {code}, 价格: {price}, 数量: {amount}"
|
||||||
)
|
)
|
||||||
@ -213,16 +258,22 @@ def sell():
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_real_mode():
|
if is_real_mode():
|
||||||
|
logger.info(
|
||||||
|
f"使用RealTraderManager执行卖出: 代码={code}, 价格={price}, 数量={amount}, 策略={strategy_name}"
|
||||||
|
)
|
||||||
rtm = get_real_trader_manager()
|
rtm = get_real_trader_manager()
|
||||||
result = rtm.place_order(
|
result = rtm.place_order(
|
||||||
strategy_name, code, ORDER_DIRECTION_SELL, amount, price
|
strategy_name, code, ORDER_DIRECTION_SELL, amount, price
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = get_trader().sell(code, price, amount, strategy_name)
|
result = trader.sell(code, price, amount)
|
||||||
|
|
||||||
if result.get("success"):
|
if result.get("success"):
|
||||||
logger.info(f"卖出成功: {result}")
|
logger.info(f"卖出成功: {result}")
|
||||||
return jsonify({"success": True, "order_id": result.get("order_id")}), 200
|
return jsonify({"success": True, "order_id": result.get("order_id")}), 200
|
||||||
|
elif "error" in result:
|
||||||
|
logger.error(f"卖出失败: {result}")
|
||||||
|
return jsonify({"success": False, "error": result["error"]}), 400
|
||||||
else:
|
else:
|
||||||
logger.error(f"卖出失败: {result}")
|
logger.error(f"卖出失败: {result}")
|
||||||
return jsonify({"success": False, "error": result}), 400
|
return jsonify({"success": False, "error": result}), 400
|
||||||
@ -237,11 +288,21 @@ def sell():
|
|||||||
|
|
||||||
@app.route("/yu/cancel/<order_id>", methods=["DELETE"])
|
@app.route("/yu/cancel/<order_id>", methods=["DELETE"])
|
||||||
def cancel(order_id):
|
def cancel(order_id):
|
||||||
logger.info(f"Received cancel request for entrust_no={order_id}")
|
"""Cancel an order by order_id."""
|
||||||
try:
|
logger.info(f"Received cancel request for order {order_id}")
|
||||||
result = get_trader().cancel(order_id)
|
|
||||||
return jsonify({"success": True, "data": result}), 200
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
result = trader.cancel(order_id)
|
||||||
|
logger.info(f"撤单结果: {result}")
|
||||||
|
return jsonify(result), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing cancel request: {str(e)}")
|
logger.error(f"Error processing cancel request: {str(e)}")
|
||||||
abort(500, description="Internal server error")
|
abort(500, description="Internal server error")
|
||||||
@ -249,14 +310,20 @@ def cancel(order_id):
|
|||||||
|
|
||||||
@app.route("/yu/balance", methods=["GET"])
|
@app.route("/yu/balance", methods=["GET"])
|
||||||
def get_balance():
|
def get_balance():
|
||||||
"""Get the balance of the account."""
|
"""Get balance information."""
|
||||||
logger.info("Received balance request")
|
logger.info("Received balance request")
|
||||||
try:
|
|
||||||
# 直接使用实盘交易实例,不考虑模拟盘
|
|
||||||
balance = get_trader().get_balance()
|
|
||||||
|
|
||||||
logger.info(f"实盘交易余额: {balance}")
|
try:
|
||||||
return jsonify({"success": True, "data": balance}), 200
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
balance = trader.get_balance()
|
||||||
|
return jsonify(balance), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing balance request: {str(e)}")
|
logger.error(f"Error processing balance request: {str(e)}")
|
||||||
abort(500, description="Internal server error")
|
abort(500, description="Internal server error")
|
||||||
@ -264,26 +331,41 @@ def get_balance():
|
|||||||
|
|
||||||
@app.route("/yu/positions", methods=["GET"])
|
@app.route("/yu/positions", methods=["GET"])
|
||||||
def get_positions():
|
def get_positions():
|
||||||
"""Get the positions of the account."""
|
"""Get position information."""
|
||||||
logger.info("Received positions request")
|
logger.info("Received positions request")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = get_trader().get_positions()
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
return jsonify({"success": True, "data": result}), 200
|
positions = trader.get_positions()
|
||||||
|
return jsonify(positions), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing positions request: {str(e)}")
|
logger.error(f"Error processing positions request: {str(e)}")
|
||||||
abort(500, description="Internal server error")
|
abort(500, description="Internal server error")
|
||||||
|
|
||||||
|
|
||||||
@app.route("/yu/todaytrades", methods=["GET"])
|
@app.route("/yu/todaytrades", methods=["GET"])
|
||||||
def get_today_trades():
|
def get_today_trades():
|
||||||
"""Get the today's trades of the account."""
|
"""Get today's trade information."""
|
||||||
logger.info("Received today trades request")
|
logger.info("Received today trades request")
|
||||||
try:
|
|
||||||
trades = get_trader().get_today_trades()
|
|
||||||
logger.info(f"今日成交: {trades}")
|
|
||||||
|
|
||||||
return jsonify({"success": True, "data": trades}), 200
|
try:
|
||||||
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
trades = trader.get_today_trades()
|
||||||
|
return jsonify(trades), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing today trades request: {str(e)}")
|
logger.error(f"Error processing today trades request: {str(e)}")
|
||||||
abort(500, description="Internal server error")
|
abort(500, description="Internal server error")
|
||||||
@ -291,15 +373,22 @@ def get_today_trades():
|
|||||||
|
|
||||||
@app.route("/yu/todayorders", methods=["GET"])
|
@app.route("/yu/todayorders", methods=["GET"])
|
||||||
def get_today_orders():
|
def get_today_orders():
|
||||||
"""Get the today's entrust of the account."""
|
"""Get today's order information."""
|
||||||
logger.info("Received today entrust request")
|
logger.info("Received today orders request")
|
||||||
try:
|
|
||||||
entrust = get_trader().get_today_orders()
|
|
||||||
logger.info(f"今日委托: {entrust}")
|
|
||||||
|
|
||||||
return jsonify({"success": True, "data": entrust}), 200
|
try:
|
||||||
|
# 检查交易系统是否可用
|
||||||
|
trader = get_trader()
|
||||||
|
if is_real_mode() and not trader.is_available():
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"error": trader.connection_error_message or "交易系统连接失败,请稍后再试"
|
||||||
|
}), 503
|
||||||
|
|
||||||
|
orders = trader.get_today_orders()
|
||||||
|
return jsonify(orders), 200
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error processing today entrust request: {str(e)}")
|
logger.error(f"Error processing today orders request: {str(e)}")
|
||||||
abort(500, description="Internal server error")
|
abort(500, description="Internal server error")
|
||||||
|
|
||||||
|
|
||||||
|
4
src/utils/__init__.py
Normal file
4
src/utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""
|
||||||
|
工具模块
|
||||||
|
包含各种辅助功能
|
||||||
|
"""
|
68
src/utils/mail_util.py
Normal file
68
src/utils/mail_util.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import smtplib
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
from email.mime.text import MIMEText
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.header import Header
|
||||||
|
from email.utils import formataddr
|
||||||
|
|
||||||
|
# 添加项目根目录到sys.path
|
||||||
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
parent_dir = os.path.dirname(current_dir)
|
||||||
|
if parent_dir not in sys.path:
|
||||||
|
sys.path.insert(0, parent_dir)
|
||||||
|
|
||||||
|
from config import Config
|
||||||
|
from logger_config import get_logger
|
||||||
|
|
||||||
|
# 获取正确配置的日志记录器
|
||||||
|
logger = get_logger('mail_util')
|
||||||
|
|
||||||
|
class MailUtil:
|
||||||
|
@staticmethod
|
||||||
|
def send_mail(subject, body, recipients=None):
|
||||||
|
"""发送邮件
|
||||||
|
|
||||||
|
Args:
|
||||||
|
subject: 邮件主题
|
||||||
|
body: 邮件内容
|
||||||
|
recipients: 收件人列表,如果为None则使用配置中的默认收件人
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 发送是否成功
|
||||||
|
"""
|
||||||
|
if not Config.MAIL_ENABLED:
|
||||||
|
logger.info("邮件通知未启用")
|
||||||
|
return False
|
||||||
|
|
||||||
|
recipients = recipients or Config.MAIL_TO
|
||||||
|
if not recipients:
|
||||||
|
logger.warning("未配置收件人")
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
msg = MIMEMultipart()
|
||||||
|
msg['From'] = formataddr((Config.MAIL_FROM, Config.MAIL_USERNAME))
|
||||||
|
msg['To'] = ', '.join(recipients)
|
||||||
|
msg['Subject'] = subject
|
||||||
|
|
||||||
|
msg.attach(MIMEText(body, 'plain'))
|
||||||
|
|
||||||
|
server = smtplib.SMTP_SSL(Config.MAIL_SERVER, Config.MAIL_PORT)
|
||||||
|
|
||||||
|
if Config.MAIL_USERNAME and Config.MAIL_PASSWORD:
|
||||||
|
server.login(Config.MAIL_USERNAME, Config.MAIL_PASSWORD)
|
||||||
|
|
||||||
|
server.send_message(msg)
|
||||||
|
server.quit()
|
||||||
|
|
||||||
|
logger.info(f"邮件发送成功: {subject}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"邮件发送失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 测试邮件发送
|
||||||
|
result = MailUtil.send_mail("测试邮件", "这是一封测试邮件")
|
Loading…
x
Reference in New Issue
Block a user