diff --git a/config.py b/config.py index 58ef29b..835ce7b 100644 --- a/config.py +++ b/config.py @@ -48,10 +48,9 @@ TIME_CONFIG = { "kline_limit": 100, # K线数据条数 } -MONITOR_CONFIG = { +OKX_MONITOR_CONFIG = { "volume_monitor":{ - "symbols": ["XCH-USDT", "BONK-USDT", "PENGU-USDT", - "CFX-USDT", "PUMP-USDT", "SOL-USDT", + "symbols": ["XCH-USDT","SOL-USDT", "BTC-USDT", "ETH-USDT", "DOGE-USDT"], "bars": ["5m", "15m", "30m", "1H"], "initial_date": "2025-05-15 00:00:00" @@ -71,6 +70,14 @@ MONITOR_CONFIG = { } } +US_STOCK_MONITOR_CONFIG = { + "volume_monitor":{ + "symbols": ["QQQ", "TQQQ", "MSFT", "AAPL", "GOOG", "NVDA", "META", "AMZN", "TSLA", "AVGO"], + "bars": ["5m"], + "initial_date": "2015-08-31 00:00:00" + } +} + WINDOW_SIZE = {"window_sizes":[50, 80, 100, 120]} BAR_THRESHOLD = { @@ -92,4 +99,6 @@ MYSQL_CONFIG = { WECHAT_CONFIG = { "key": "11e6f7ac-efa9-418a-904c-9325a9f5d324" -} \ No newline at end of file +} + +ITICK_API_KEY = "dfd4bc0caed148d6bc03b960224754ffb5356349e389431f828702b3a27e8a2b" \ No newline at end of file diff --git a/core/biz/__pycache__/market_data.cpython-312.pyc b/core/biz/__pycache__/market_data.cpython-312.pyc index ebfe61c..7b5991c 100644 Binary files a/core/biz/__pycache__/market_data.cpython-312.pyc and b/core/biz/__pycache__/market_data.cpython-312.pyc differ diff --git a/core/biz/market_data.py b/core/biz/market_data.py index 4607c5f..e968893 100644 --- a/core/biz/market_data.py +++ b/core/biz/market_data.py @@ -4,7 +4,8 @@ from typing import Optional import pandas as pd import okx.MarketData as Market import okx.TradingData as TradingData -from core.utils import transform_date_time_to_timestamp +from core.utils import transform_date_time_to_timestamp, timestamp_to_datetime +from core.biz.market_data_from_itick import MarketDataFromItick import core.logger as logging logger = logging.logger @@ -14,7 +15,8 @@ class MarketData: api_key: str, secret_key: str, passphrase: str, - sandbox: bool = True): + sandbox: bool = True, + is_us_stock: bool = False): self.flag = "1" if sandbox else "0" # 0:实盘环境 1:沙盒环境 self.api_key = api_key self.secret_key = secret_key @@ -23,6 +25,8 @@ class MarketData: api_key=api_key, api_secret_key=secret_key, passphrase=passphrase, flag=self.flag ) + self.is_us_stock = is_us_stock + # self.trade_api = TradingData.TradingDataAPI( # api_key=api_key, api_secret_key=secret_key, passphrase=passphrase, # flag=flag @@ -107,7 +111,11 @@ class MarketData: while start_time < end_time: try: # after,真实逻辑是获得指定时间之前的数据 !!! - response = self.get_historical_candlesticks_from_api(symbol, bar, end_time, limit) + if self.is_us_stock: + market_data_from_itick = MarketDataFromItick(symbol=symbol, bar=bar, end_time=end_time, limit=limit) + response = market_data_from_itick.get_historical_candlesticks_from_api() + else: + response = self.get_historical_candlesticks_from_api(symbol, bar, end_time, limit) if response is None: logger.warning(f"请求失败,请稍后再试") break @@ -115,9 +123,15 @@ class MarketData: logger.warning(f"请求失败或无数据: {response.get('msg', 'No message')}") break candles = response["data"] - - from_time = int(candles[-1][0]) - to_time = int(candles[0][0]) + if len(candles) == 0: + logger.warning(f"请求失败或无数据: {response.get('msg', 'No message')}") + break + if self.is_us_stock: + from_time = int(candles[-1]["timestamp"]) + to_time = int(candles[0]["timestamp"]) + else: + from_time = int(candles[-1][0]) + to_time = int(candles[0][0]) if latest_timestamp == -1: latest_timestamp = from_time else: @@ -126,14 +140,25 @@ class MarketData: else: logger.warning(f"上一次数据最早时间戳 {latest_timestamp} 小于等于 from_time {from_time}, 停止获取数据") break - from_time_str = pd.to_datetime(from_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') - to_time_str = pd.to_datetime(to_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') + if self.is_us_stock: + from_time_str = pd.to_datetime(from_time, unit='ms', utc=True).tz_convert('America/New_York') + to_time_str = pd.to_datetime(to_time, unit='ms', utc=True).tz_convert('America/New_York') + else: + from_time_str = pd.to_datetime(from_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') + to_time_str = pd.to_datetime(to_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') logger.info(f"已获取{symbol}, 周期:{bar} {len(candles)} 条数据,从: {from_time_str} 到: {to_time_str}") if from_time < start_time: - start_time_str = pd.to_datetime(start_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') + if self.is_us_stock: + start_time_str = pd.to_datetime(start_time, unit='ms', utc=True).tz_convert('America/New_York') + else: + start_time_str = pd.to_datetime(start_time, unit='ms', utc=True).tz_convert('Asia/Shanghai') logger.warning(f"本轮获取{symbol}, {bar} 数据最早时间 {from_time_str} 早于 此次数据获取起始时间 {start_time_str}, 停止获取数据") # candels中仅保留start_time之后的数据 - candles = [candle for candle in candles if int(candle[0]) >= start_time] + if self.is_us_stock: + if to_time > start_time: + candles = [candle for candle in candles if int(candle["timestamp"]) >= start_time] + else: + candles = [candle for candle in candles if int(candle[0]) >= start_time] if len(candles) > 0: candles_pd = pd.DataFrame(candles, columns=columns) all_data.append(candles_pd) @@ -141,6 +166,9 @@ class MarketData: else: break else: + if end_time + 1 == from_time or end_time == from_time: + logger.warning(f"本轮获取{symbol}, {bar} 数据最早时间 {from_time_str} 等于 此次数据获取结束时间, 停止获取数据") + break if len(candles) > 0: candles_pd = pd.DataFrame(candles, columns=columns) all_data.append(candles_pd) @@ -161,6 +189,8 @@ class MarketData: df[col] = pd.to_numeric(df[col], errors='coerce') dt_series = pd.to_datetime(df['timestamp'].astype(int), unit='ms', utc=True, errors='coerce').dt.tz_convert('Asia/Shanghai') df['date_time'] = dt_series.dt.strftime('%Y-%m-%d %H:%M:%S') + dt_us_series = pd.to_datetime(df['timestamp'].astype(int), unit='ms', utc=True, errors='coerce').dt.tz_convert('America/New_York') + df['date_time_us'] = dt_us_series.dt.strftime('%Y-%m-%d %H:%M:%S') # 将timestamp转换为整型 df['timestamp'] = df['timestamp'].astype(int) # 添加虚拟货币名称列,内容为symbol @@ -169,14 +199,35 @@ class MarketData: df['bar'] = bar df['create_time'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + if self.is_us_stock: + # 如果是美股数据,则仅保留date_time_us字中,在开盘时间内的数据,即开盘时间为美国时间9:30到16:00 + # 请将date_time_us转换为datetime对象,然后判断是否在开盘时间内的数据,如果是,则保留,否则删除 + df['date_time_us'] = pd.to_datetime(df['date_time_us'], errors='coerce') + # 使用 .loc 避免 SettingWithCopyWarning + mask = (df['date_time_us'].dt.hour >= 9) & (df['date_time_us'].dt.hour <= 16) + df = df.loc[mask].copy() + # 对于9点,只保留9:35与之后的数据 + mask_9 = ~((df['date_time_us'].dt.hour == 9) & (df['date_time_us'].dt.minute <= 30)) + df = df.loc[mask_9].copy() + # 对于16点,只保留16:00之前的数据 + mask_16 = ~((df['date_time_us'].dt.hour == 16) & (df['date_time_us'].dt.minute > 0)) + df = df.loc[mask_16].copy() + # 将date_time_us转换为字符串 + df.loc[:, 'date_time_us'] = df['date_time_us'].dt.strftime('%Y-%m-%d %H:%M:%S') + + # 获取df中date_time的最早时间与最新时间 并保存到df中 - df = df[['symbol', 'bar', 'timestamp', 'date_time', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCCyQuote', 'create_time']] + df = df[['symbol', 'bar', 'timestamp', 'date_time', 'date_time_us', 'open', 'high', 'low', 'close', 'volume', 'volCcy', 'volCCyQuote', 'create_time']] df.sort_values('timestamp', inplace=True) df.reset_index(drop=True, inplace=True) logger.info(f"总计获取 {len(df)} 条 K 线数据(仅confirm=1)") # 获取df中date_time的最早时间与最新时间 - earliest_time = df['date_time'].min() - latest_time = df['date_time'].max() + if self.is_us_stock: + earliest_time = df['date_time_us'].min() + latest_time = df['date_time_us'].max() + else: + earliest_time = df['date_time'].min() + latest_time = df['date_time'].max() logger.info(f"本轮更新{symbol}, {bar} 数据最早时间: {earliest_time}, 最新时间: {latest_time}") return df else: diff --git a/core/biz/market_data_from_itick.py b/core/biz/market_data_from_itick.py new file mode 100644 index 0000000..a26f3d3 --- /dev/null +++ b/core/biz/market_data_from_itick.py @@ -0,0 +1,138 @@ +import requests +import pandas as pd +from datetime import datetime, timedelta +import time +import json +from typing import Optional +import mplfinance as mpf +import random + +from core.utils import transform_date_time_to_timestamp, datetime_to_timestamp, timestamp_to_datetime + +import core.logger as logging +from config import ITICK_API_KEY + +logger = logging.logger + +class MarketDataFromItick: + """用于从 Yahoo Finance 下载 K 线数据的类,支持分段下载和数据处理。""" + + def __init__(self, symbol='QQQ', bar='5m', end_time: str=None, limit: int=1000): + """ + 初始化下载器。 + + 参数: + ticker (str): 股票或 ETF 代码,如 'QQQ'。 + start_date (datetime): 开始日期,默认为 1 年前。 + end_date (datetime): 结束日期,默认为当前日期。 + bar (str): K 线间隔,如 '5m'(5 分钟)。 + segment_days (int): 每次下载的天数,默认为 10 天。 + """ + self.symbol = symbol + self.time_stamp_unit = 0 + if bar == '1m': + self.ktype = '1' + self.time_stamp_unit = 1000 * 60 + elif bar == '5m': + self.ktype = '2' + self.time_stamp_unit = 1000 * 60 * 5 + elif bar == '15m': + self.ktype = '3' + self.time_stamp_unit = 1000 * 60 * 15 + elif bar == '30m': + self.ktype = '4' + self.time_stamp_unit = 1000 * 60 * 30 + elif bar == '1H': + self.ktype = '5' + self.time_stamp_unit = 1000 * 60 * 60 + elif bar == '2H': + self.ktype = '6' + self.time_stamp_unit = 1000 * 60 * 60 * 2 + elif bar == '4H': + self.ktype = '7' + self.time_stamp_unit = 1000 * 60 * 60 * 4 + elif bar == '1D': + self.ktype = '8' + elif bar == '1W': + self.ktype = '9' + elif bar == '1M': + self.ktype = '10' + else: + self.ktype = '2' + self.limit = limit + + # 设置默认时间范围 + if end_time is None: + end_time = int(datetime.now().strftime('%Y-%m-%d %H:%M:%S').timestamp()) + if isinstance(end_time, str): + end_time = int(datetime.strptime(end_time, '%Y-%m-%d %H:%M:%S').timestamp()) + self.end_time = end_time + + self.headers = { + "accept": "application/json", + "token": ITICK_API_KEY + } + # 初始化空 DataFrame + self.data = pd.DataFrame() + + # 重试配置 + self.max_retries = 3 + self.base_delay = 5 # 基础延迟秒数 + self.max_delay = 60 # 最大延迟秒数 + + def get_historical_candlesticks_from_api(self): + response = None + count = 0 + end_time_str = timestamp_to_datetime(self.end_time) + logger.info(f"请求数据: {self.symbol} {self.ktype} {end_time_str} {self.limit}") + while True: + try: + url = f"https://api.itick.org/stock/kline?region=US&code={self.symbol}&kType={self.ktype}&et={self.end_time}&limit={self.limit}" + response = requests.get(url, headers=self.headers) + if response: + response = json.loads(response.text) + if response["code"] != 0: + raise Exception(f"请求出错: {response["msg"]}") + data_list = response["data"] + """ + { + "tu": 1292317671.363, + "c": 560.78, + "t": 1752858600000, + "v": 2305857, + "h": 560.79, + "l": 560.06, + "o": 560.45 + } + """ + result_list = [] + # itick的时间,分钟数据与市面上的分钟数据有1个周期的差距,即itick的时间是市面上的时间的前1个周期 + # 因此需要将itick的时间加上1个周期的单位 + for data in data_list: + result_list.append({ + "timestamp": int(data["t"]) + self.time_stamp_unit, + "open": float(data["o"]), + "high": float(data["h"]), + "low": float(data["l"]), + "close": float(data["c"]), + "volume": int(data["v"]), + "volCcy": 0, + "volCCyQuote": 0, + "confirm": "1" + }) + # result_list按照timestamp降序排列 + result_list = sorted(result_list, key=lambda x: x["timestamp"], reverse=True) + response["data"] = result_list + response["code"] = "0" + response["msg"] = "success" + break + except Exception as e: + count += 1 + logger.error(f"请求出错: {e}, 第{count}次重试,30秒后重试") + if count > 3: + break + time.sleep(30) + logger.info(f"请求成功,等待12秒后返回") + time.sleep(12) + return response + \ No newline at end of file diff --git a/core/db/__pycache__/db_market_data.cpython-312.pyc b/core/db/__pycache__/db_market_data.cpython-312.pyc index cf78140..274f19e 100644 Binary files a/core/db/__pycache__/db_market_data.cpython-312.pyc and b/core/db/__pycache__/db_market_data.cpython-312.pyc differ diff --git a/core/db/db_market_data.py b/core/db/db_market_data.py index d922c95..8ced864 100644 --- a/core/db/db_market_data.py +++ b/core/db/db_market_data.py @@ -18,6 +18,7 @@ class DBMarketData: "bar", "timestamp", "date_time", + "date_time_us", "open", "high", "low", diff --git a/core/statistics/price_volume_stats.py b/core/statistics/price_volume_stats.py index 33e34fd..57b210a 100644 --- a/core/statistics/price_volume_stats.py +++ b/core/statistics/price_volume_stats.py @@ -11,7 +11,7 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from core.db.db_market_data import DBMarketData from core.db.db_huge_volume_data import DBHugeVolumeData from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp @@ -35,10 +35,10 @@ class PriceVolumeStats: self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" self.db_market_data = DBMarketData(self.db_url) self.db_huge_volume_data = DBHugeVolumeData(self.db_url) - self.symbol_list = MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", []) - self.bars_list = MONITOR_CONFIG.get("volume_monitor", {}).get("bars", []) + self.symbol_list = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get("symbols", []) + self.bars_list = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get("bars", []) self.windows_list = WINDOW_SIZE.get("window_sizes", []) - self.initial_date = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.initial_date = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-15 00:00:00" ) self.end_date = datetime.now().strftime("%Y-%m-%d %H:%M:%S") diff --git a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc index 10d11b4..db4662d 100644 Binary files a/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc and b/core/trade/__pycache__/ma_break_statistics.cpython-312.pyc differ diff --git a/core/trade/__pycache__/orb_trade.cpython-312.pyc b/core/trade/__pycache__/orb_trade.cpython-312.pyc new file mode 100644 index 0000000..741d5d7 Binary files /dev/null and b/core/trade/__pycache__/orb_trade.cpython-312.pyc differ diff --git a/core/trade/ma_break_statistics.py b/core/trade/ma_break_statistics.py index 1a4a155..27ffe2f 100644 --- a/core/trade/ma_break_statistics.py +++ b/core/trade/ma_break_statistics.py @@ -12,7 +12,7 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from core.db.db_market_data import DBMarketData from core.db.db_huge_volume_data import DBHugeVolumeData from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp @@ -44,10 +44,10 @@ class MaBreakStatistics: self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" self.db_market_data = DBMarketData(self.db_url) self.db_huge_volume_data = DBHugeVolumeData(self.db_url) - self.symbols = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "symbols", ["XCH-USDT"] ) - self.bars = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "bars", ["5m", "15m", "30m", "1H"] ) self.stats_output_dir = "./output/trade_sandbox/ma_strategy/excel/" diff --git a/core/trade/mean_reversion_sandbox.py b/core/trade/mean_reversion_sandbox.py index 9560000..e79c22f 100644 --- a/core/trade/mean_reversion_sandbox.py +++ b/core/trade/mean_reversion_sandbox.py @@ -11,7 +11,7 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE import core.logger as logging from core.db.db_merge_market_huge_volume import DBMergeMarketHugeVolume from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp diff --git a/core/trade/orb_trade.py b/core/trade/orb_trade.py new file mode 100644 index 0000000..d4759d0 --- /dev/null +++ b/core/trade/orb_trade.py @@ -0,0 +1,403 @@ +import yfinance as yf +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +import seaborn as sns +import core.logger as logging +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from core.db.db_market_data import DBMarketData +from core.db.db_huge_volume_data import DBHugeVolumeData +from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp + +# seaborn支持中文 +plt.rcParams["font.family"] = ["SimHei"] + +logger = logging.logger + + +class ORBStrategy: + def __init__( + self, + initial_capital=25000, + max_leverage=4, + risk_per_trade=0.01, + commission_per_share=0.0005, + ): + """ + 初始化ORB策略参数 + :param initial_capital: 初始账户资金(美元) + :param max_leverage: 最大杠杆倍数(默认4倍,符合FINRA规定) + :param risk_per_trade: 单次交易风险比例(默认1%) + :param commission_per_share: 每股交易佣金(美元,默认0.0005) + """ + logger.info(f"初始化ORB策略参数:初始账户资金={initial_capital},最大杠杆倍数={max_leverage},单次交易风险比例={risk_per_trade},每股交易佣金={commission_per_share}") + self.initial_capital = initial_capital + self.max_leverage = max_leverage + self.risk_per_trade = risk_per_trade + self.commission_per_share = commission_per_share + self.data = None # 存储K线数据 + self.trades = [] # 存储交易记录 + self.equity_curve = None # 存储账户净值曲线 + mysql_user = MYSQL_CONFIG.get("user", "xch") + mysql_password = MYSQL_CONFIG.get("password", "") + if not mysql_password: + raise ValueError("MySQL password is not set") + mysql_host = MYSQL_CONFIG.get("host", "localhost") + mysql_port = MYSQL_CONFIG.get("port", 3306) + mysql_database = MYSQL_CONFIG.get("database", "okx") + + self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" + self.db_market_data = DBMarketData(self.db_url) + + def fetch_intraday_data(self, symbol, start_date, end_date, interval="5m"): + """ + 获取日内5分钟K线数据(需yfinance支持,部分数据可能有延迟) + :param ticker: 股票代码(如QQQ、TQQQ) + :param start_date: 起始日期(格式:YYYY-MM-DD) + :param end_date: 结束日期(格式:YYYY-MM-DD) + :param interval: K线周期(默认5分钟) + """ + logger.info(f"开始获取{symbol}数据:{start_date}至{end_date},间隔{interval}") + # data = yf.download( + # symbol, start=start_date, end=end_date, interval=interval, progress=False + # ) + data = self.db_market_data.query_market_data_by_symbol_bar( + symbol, interval, start=start_date, end=end_date + ) + data = pd.DataFrame(data) + data.sort_values(by="date_time", inplace=True) + # 保留核心列:开盘价、最高价、最低价、收盘价、成交量 + data["Open"] = data["open"] + data["High"] = data["high"] + data["Low"] = data["low"] + data["Close"] = data["close"] + data["Volume"] = data["volume"] + # 将data["date_time"]从字符串类型转换为日期 + data["date_time"] = pd.to_datetime(data["date_time"]) + # data["Date"]为日期,不包括时分秒,即date_time如果是2025-01-01 10:00:00,则Date为2025-01-01 + data["Date"] = data["date_time"].dt.date + # 将Date转换为datetime64[ns]类型以确保类型一致 + data["Date"] = pd.to_datetime(data["Date"]) + + self.data = data[["Date", "date_time", "Open", "High", "Low", "Close", "Volume"]].copy() + logger.info(f"成功获取{symbol}数据:{len(self.data)}根{interval}K线") + + def calculate_shares(self, account_value, entry_price, stop_price): + """ + 根据ORB公式计算交易股数 + :param account_value: 当前账户价值(美元) + :param entry_price: 交易entry价格(第二根5分钟K线开盘价) + :param stop_price: 止损价格(多头=第一根K线最低价,空头=第一根K线最高价) + :return: 整数股数(Shares) + """ + logger.info(f"开始计算交易股数:账户价值={account_value},entry价格={entry_price},止损价格={stop_price}") + # 计算单交易风险金额($R) + risk_per_trade_dollar = abs(entry_price - stop_price) # 风险金额取绝对值 + if risk_per_trade_dollar <= 0: + return 0 # 无风险时不交易 + + # 公式1:基于风险预算的最大股数(风险控制优先) + shares_risk = (account_value * self.risk_per_trade) / risk_per_trade_dollar + # 公式2:基于杠杆限制的最大股数(杠杆约束) + shares_leverage = (self.max_leverage * account_value) / entry_price + # 取两者最小值(满足风险和杠杆双重约束) + max_shares = min(shares_risk, shares_leverage) + # 扣除佣金影响(简化计算:假设佣金从可用资金中扣除) + commission_cost = max_shares * self.commission_per_share + if (account_value - commission_cost) < 0: + return 0 # 扣除佣金后资金不足,不交易 + + return int(max_shares) # 股数取整 + + def generate_orb_signals(self): + """ + 生成ORB策略信号(每日仅1次交易机会) + - 第一根5分钟K线:确定开盘区间(High1, Low1) + - 第二根5分钟K线:根据第一根K线方向生成多空信号 + """ + logger.info("开始生成ORB策略信号") + if self.data is None: + raise ValueError("请先调用fetch_intraday_data获取数据") + + signals = [] + # 按日期分组处理每日数据 + for date, daily_data in self.data.groupby("Date"): + daily_data = daily_data.sort_index() # 按时间排序 + if len(daily_data) < 2: + continue # 当日K线不足2根,跳过 + + # 第一根5分钟K线(开盘区间) + first_candle = daily_data.iloc[0] + high1 = first_candle["High"] + low1 = first_candle["Low"] + open1 = first_candle["Open"] + close1 = first_candle["Close"] + + # 第二根5分钟K线(entry信号) + second_candle = daily_data.iloc[1] + entry_price = second_candle["Open"] # entry价格=第二根K线开盘价 + entry_time = second_candle.date_time # entry时间 + + # 生成信号:第一根K线方向决定多空(排除十字星:open1 == close1) + if open1 < close1: + # 第一根K线收涨→多头信号 + signal = "Long" + stop_price = low1 # 多头止损=第一根K线最低价 + elif open1 > close1: + # 第一根K线收跌→空头信号 + signal = "Short" + stop_price = high1 # 空头止损=第一根K线最高价 + else: + # 十字星→无信号 + signal = "None" + stop_price = None + + signals.append( + { + "Date": date, + "EntryTime": entry_time, + "Signal": signal, + "EntryPrice": entry_price, + "StopPrice": stop_price, + "High1": high1, + "Low1": low1, + } + ) + + # 将信号合并到原始数据 + signals_df = pd.DataFrame(signals) + # 确保Date列类型一致,将Date转换为datetime64[ns]类型 + signals_df['Date'] = pd.to_datetime(signals_df['Date']) + # 使用merge而不是join来合并数据,根据signals_df的EntryTime与self.data的date_time进行匹配 + # TODO: 这里需要优化 + self.data = self.data.merge(signals_df, left_on="date_time", right_on="EntryTime", how="left") + # 将Date_x和Date_y合并为Date + self.data["Date"] = self.data["Date_x"].combine_first(self.data["Date_y"]) + # 删除Date_x和Date_y + self.data.drop(columns=["Date_x", "Date_y"], inplace=True) + logger.info( + f"生成信号完成:共{len(signals_df)}个交易日,其中多头{sum(signals_df['Signal']=='Long')}次,空头{sum(signals_df['Signal']=='Short')}次" + ) + + def backtest(self, profit_target_multiple=10): + """ + 回测ORB策略 + :param profit_target_multiple: 盈利目标倍数(默认10倍$R,即10R) + """ + logger.info(f"开始回测ORB策略:盈利目标倍数={profit_target_multiple}") + if "Signal" not in self.data.columns: + raise ValueError("请先调用generate_orb_signals生成策略信号") + + account_value = self.initial_capital # 初始账户价值 + current_position = None # 当前持仓(None=空仓,Long/Short=持仓) + equity_history = [account_value] # 净值历史 + trade_id = 0 # 交易ID + + # 按时间遍历数据(每日仅处理第二根K线后的信号) + for date, daily_data in self.data.groupby("Date"): + daily_data = daily_data.sort_index() + if len(daily_data) < 2: + continue + + # 获取当日信号(第二根K线的信号) + signal_row = ( + daily_data[~pd.isna(daily_data["Signal"])].iloc[0] + if sum(~pd.isna(daily_data["Signal"])) > 0 + else None + ) + if signal_row is None: + # 无信号→当日不交易,净值保持不变 + equity_history.append(account_value) + continue + + # 提取信号参数 + signal = signal_row["Signal"] + if pd.isna(signal): + continue + entry_price = signal_row["EntryPrice"] + stop_price = signal_row["StopPrice"] + high1 = signal_row["High1"] + low1 = signal_row["Low1"] + risk_assumed = abs(entry_price - stop_price) # 计算$R + profit_target = ( + entry_price + (risk_assumed * profit_target_multiple) + if signal == "Long" + else entry_price - (risk_assumed * profit_target_multiple) + ) + + # 计算交易股数 + shares = self.calculate_shares(account_value, entry_price, stop_price) + if shares == 0: + # 股数为0→不交易 + equity_history.append(account_value) + continue + + # 计算佣金(买入/卖出各收一次) + total_commission = shares * self.commission_per_share * 2 # 往返佣金 + + # 模拟日内持仓:寻找止损/止盈触发点,或当日收盘平仓 + daily_prices = daily_data[ + daily_data.date_time > signal_row.date_time + ] # 从entry时间开始遍历 + exit_price = None + exit_time = None + exit_reason = None + + for idx, (time, row) in enumerate(daily_prices.iterrows()): + high = row["High"] + low = row["Low"] + close = row["Close"] + + # 检查止损/止盈条件 + if signal == "Long": + # 多头:跌破止损→止损;突破止盈→止盈 + if low <= stop_price: + exit_price = stop_price + exit_reason = "Stop Loss" + exit_time = time + break + elif high >= profit_target: + exit_price = profit_target + exit_reason = "Profit Target (10R)" + exit_time = time + break + elif signal == "Short": + # 空头:突破止损→止损;跌破止盈→止盈 + if high >= stop_price: + exit_price = stop_price + exit_reason = "Stop Loss" + exit_time = time + break + elif low <= profit_target: + exit_price = profit_target + exit_reason = "Profit Target (10R)" + exit_time = time + break + + # 若未触发止损/止盈,当日收盘平仓 + if exit_price is None: + exit_price = daily_prices.iloc[-1]["Close"] + exit_reason = "End of Day (EoD)" + exit_time = daily_prices.iloc[-1].date_time + + # 计算盈亏 + if signal == "Long": + profit_loss = (exit_price - entry_price) * shares - total_commission + else: # Short + profit_loss = (entry_price - exit_price) * shares - total_commission + + # 更新账户价值 + account_value += profit_loss + account_value = max(account_value, 0) # 账户价值不能为负 + + # 记录交易 + self.trades.append( + { + "TradeID": trade_id, + "Date": date, + "Signal": signal, + "EntryTime": signal_row.date_time, + "EntryPrice": entry_price, + "ExitTime": exit_time, + "ExitPrice": exit_price, + "Shares": shares, + "RiskAssumed": risk_assumed, + "ProfitLoss": profit_loss, + "ExitReason": exit_reason, + "AccountValueAfter": account_value, + } + ) + + # 记录净值 + equity_history.append(account_value) + trade_id += 1 + + # 生成净值曲线 + self.equity_curve = pd.Series( + equity_history, + index=pd.date_range( + start=self.data.index[0].date(), periods=len(equity_history), freq="D" + ), + ) + + # 输出回测结果 + trades_df = pd.DataFrame(self.trades) + total_return = ( + (account_value - self.initial_capital) / self.initial_capital * 100 + ) + win_rate = ( + (trades_df["ProfitLoss"] > 0).sum() / len(trades_df) * 100 + if len(trades_df) > 0 + else 0 + ) + + logger.info("\n" + "=" * 50) + logger.info("ORB策略回测结果") + logger.info("=" * 50) + logger.info(f"初始资金:${self.initial_capital:,.2f}") + logger.info(f"最终资金:${account_value:,.2f}") + logger.info(f"总收益率:{total_return:.2f}%") + logger.info(f"总交易次数:{len(trades_df)}") + logger.info(f"胜率:{win_rate:.2f}%") + if len(trades_df) > 0: + logger.info(f"平均每笔盈亏:${trades_df['ProfitLoss'].mean():.2f}") + logger.info(f"最大单笔盈利:${trades_df['ProfitLoss'].max():.2f}") + logger.info(f"最大单笔亏损:${trades_df['ProfitLoss'].min():.2f}") + + def plot_equity_curve(self): + """ + 绘制账户净值曲线 + """ + logger.info("开始绘制账户净值曲线") + if self.equity_curve is None: + raise ValueError("请先调用backtest进行回测") + + # seaborn风格设置 + sns.set_theme(style="whitegrid") + # plt.rcParams['font.family'] = "SimHei" + plt.rcParams["font.sans-serif"] = ["SimHei"] # 也可直接用字体名 + plt.rcParams["font.size"] = 11 # 设置字体大小 + plt.rcParams["axes.unicode_minus"] = False # 解决负号显示问题 + + plt.figure(figsize=(12, 6)) + plt.plot( + self.equity_curve.index, + self.equity_curve.values, + label="ORB策略净值", + color="blue", + ) + plt.axhline( + y=self.initial_capital, color="red", linestyle="--", label="初始资金" + ) + plt.title("ORB策略账户净值曲线", fontsize=14) + plt.xlabel("日期", fontsize=12) + plt.ylabel("账户价值(美元)", fontsize=12) + plt.legend() + plt.grid(True, alpha=0.3) + plt.show() + + +# ------------------- 策略示例:回测QQQ的ORB策略(2016-2023) ------------------- +if __name__ == "__main__": + + # 初始化ORB策略 + orb_strategy = ORBStrategy( + initial_capital=25000, + max_leverage=4, + risk_per_trade=0.01, + commission_per_share=0.0005, + ) + + # 1. 获取QQQ的5分钟日内数据(2024-2025,注意:yfinance免费版可能限制历史日内数据,建议用专业数据源) + orb_strategy.fetch_intraday_data( + symbol="ETH-USDT", start_date="2025-05-15", end_date="2025-08-20", interval="5m" + ) + + # 2. 生成ORB策略信号 + orb_strategy.generate_orb_signals() + + # 3. 回测策略(盈利目标10R) + orb_strategy.backtest(profit_target_multiple=10) + + # 4. 绘制净值曲线 + orb_strategy.plot_equity_curve() diff --git a/huge_volume_main.py b/huge_volume_main.py index 7e5b8aa..e77b2d2 100644 --- a/huge_volume_main.py +++ b/huge_volume_main.py @@ -6,7 +6,7 @@ from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp from market_data_main import MarketDataMain from core.wechat import Wechat import core.logger as logging -from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, US_STOCK_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE from datetime import datetime, timedelta import pandas as pd import os @@ -16,7 +16,7 @@ logger = logging.logger class HugeVolumeMain: - def __init__(self, threshold: float = 2.0): + def __init__(self, threshold: float = 2.0, is_us_stock: bool = False): mysql_user = MYSQL_CONFIG.get("user", "xch") mysql_password = MYSQL_CONFIG.get("password", "") if not mysql_password: @@ -29,7 +29,7 @@ class HugeVolumeMain: self.huge_volume = HugeVolume() self.db_market_data = DBMarketData(self.db_url) self.db_huge_volume_data = DBHugeVolumeData(self.db_url) - self.market_data_main = MarketDataMain() + self.market_data_main = MarketDataMain(is_us_stock=is_us_stock) self.threshold = threshold self.output_folder = "./output/huge_volume_statistics/" @@ -41,9 +41,14 @@ class HugeVolumeMain: for symbol in self.market_data_main.symbols: for bar in self.market_data_main.bars: if start is None: - start = MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2025-05-15 00:00:00" - ) + if self.is_us_stock: + start = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2015-08-30 00:00:00" + ) + else: + start = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2025-05-15 00:00:00" + ) data = self.detect_volume_spike( symbol, bar, @@ -68,9 +73,15 @@ class HugeVolumeMain: is_update: bool = False, ): if start is None: - start = MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2025-05-01 00:00:00" - ) + if self.is_us_stock: + start = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2015-08-31 00:00:00" + ) + else: + start = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2025-05-01 00:00:00" + ) + if end is None: end = datetime.now().strftime("%Y-%m-%d %H:%M:%S") logger.info( @@ -169,7 +180,7 @@ class HugeVolumeMain: logger.info(f"此次更新巨量交易数据为空") except Exception as e: logger.error( - f"更新巨量交易数据失败: {symbol} {bar} 窗口大小: {window_size} 从 {earliest_date_time} 到 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: {e}" + f"更新巨量交易数据失败: {symbol} {bar} 窗口大小: {window_size} 到 {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}: {e}" ) def get_seconds_by_bar(self, bar: str): @@ -226,7 +237,7 @@ class HugeVolumeMain: periods: list = [3, 5], ): if start is None: - start = MONITOR_CONFIG.get("volume_monitor", {}).get( + start = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-01 00:00:00" ) if end is None: @@ -286,7 +297,7 @@ class HugeVolumeMain: def send_huge_volume_data_to_wechat(self, start: str = None, end: str = None): if start is None: - start = MONITOR_CONFIG.get("volume_monitor", {}).get( + start = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-01 00:00:00" ) if end is None: @@ -395,7 +406,7 @@ class HugeVolumeMain: output_excel: bool = False, ): if start is None: - start = MONITOR_CONFIG.get("volume_monitor", {}).get( + start = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-01 00:00:00" ) if end is None: @@ -464,7 +475,7 @@ def batch_initial_detect_volume_spike(threshold: float = 2.0): ): window_sizes = [50, 80, 100, 120] huge_volume_main = HugeVolumeMain(threshold) - start_date = MONITOR_CONFIG.get("volume_monitor", {}).get( + start_date = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-15 00:00:00" ) for window_size in window_sizes: @@ -474,7 +485,7 @@ def batch_initial_detect_volume_spike(threshold: float = 2.0): ) -def batch_update_volume_spike(threshold: float = 2.0): +def batch_update_volume_spike(threshold: float = 2.0, is_us_stock: bool = False): window_sizes = WINDOW_SIZE.get("window_sizes", None) if ( window_sizes is None @@ -482,7 +493,7 @@ def batch_update_volume_spike(threshold: float = 2.0): or len(window_sizes) == 0 ): window_sizes = [50, 80, 100, 120] - huge_volume_main = HugeVolumeMain(threshold) + huge_volume_main = HugeVolumeMain(threshold, is_us_stock) for window_size in window_sizes: huge_volume_main.batch_update_volume_spike(window_size=window_size) @@ -501,7 +512,7 @@ def test_send_huge_volume_data_to_wechat(): if __name__ == "__main__": # test_send_huge_volume_data_to_wechat() # batch_initial_detect_volume_spike(threshold=2.0) - batch_update_volume_spike(threshold=2.0) + batch_update_volume_spike(threshold=2.0, is_us_stock=True) # huge_volume_main = HugeVolumeMain(threshold=2.0) # huge_volume_main.batch_next_periods_rise_or_fall(output_excel=True) # data_file_path = "./output/huge_volume_statistics/next_periods_rise_or_fall_stat_20250731200304.xlsx" diff --git a/market_data_from_itick_main.py b/market_data_from_itick_main.py new file mode 100644 index 0000000..88a0756 --- /dev/null +++ b/market_data_from_itick_main.py @@ -0,0 +1,60 @@ +import requests +import core.logger as logging + +from datetime import datetime, timedelta + +from futu import KLType + +logger = logging.logger + + +def main(): + # 配置参数 + symbol = "QQQ" + start_date = "2025-08-01 00:00:00" + end_date = "2025-08-20 00:00:00" + interval = "5m" + segment_days = 5 # 减少每段天数,降低单次请求的数据量 + + print(f"开始下载 {symbol} 数据") + print(f"时间范围: {start_date} 到 {end_date}") + print(f"数据间隔: {interval}") + print(f"分段天数: {segment_days}") + print("-" * 50) + + try: + market_data_from_futu = MarketDataFromAlphaVantage( + symbol=symbol, + start_date=start_date, + end_date=end_date, + interval=interval, + segment_days=segment_days, + ) + + # 下载数据 + market_data_from_futu.download_all() + + # 处理数据 + processed_data = market_data_from_futu.process_data() + + if not processed_data.empty: + logger.info(f"成功下载 {len(processed_data)} 条数据") + + # 保存数据 + filename = f"{symbol}_{interval}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + market_data_from_futu.save_to_csv(filename) + + # 显示数据统计 + market_data_from_futu.show_head() + market_data_from_futu.show_stats() + + else: + logger.warning("未获取到任何数据") + + except Exception as e: + logger.error(f"下载过程中发生错误: {e}") + print(f"错误详情: {e}") + + +if __name__ == "__main__": + main() diff --git a/market_data_main.py b/market_data_main.py index 49401d2..03c6963 100644 --- a/market_data_main.py +++ b/market_data_main.py @@ -16,7 +16,8 @@ from config import ( SECRET_KEY, PASSPHRASE, SANDBOX, - MONITOR_CONFIG, + OKX_MONITOR_CONFIG, + US_STOCK_MONITOR_CONFIG, MYSQL_CONFIG, BAR_THRESHOLD, ) @@ -25,22 +26,34 @@ logger = logging.logger class MarketDataMain: - def __init__(self): + def __init__(self, is_us_stock: bool = False): self.market_data = MarketData( api_key=API_KEY, secret_key=SECRET_KEY, passphrase=PASSPHRASE, sandbox=SANDBOX, + is_us_stock=is_us_stock, ) - self.symbols = MONITOR_CONFIG.get("volume_monitor", {}).get( - "symbols", ["XCH-USDT"] - ) - self.bars = MONITOR_CONFIG.get("volume_monitor", {}).get( - "bars", ["5m", "15m", "1H", "1D"] - ) - self.initial_date = MONITOR_CONFIG.get("volume_monitor", {}).get( - "initial_date", "2025-07-01 00:00:00" - ) + if is_us_stock: + self.symbols = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["QQQ"] + ) + self.bars = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "bars", ["5m"] + ) + self.initial_date = US_STOCK_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2015-08-30 00:00:00" + ) + else: + self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "symbols", ["XCH-USDT"] + ) + self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "bars", ["5m", "15m", "1H", "1D"] + ) + self.initial_date = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( + "initial_date", "2025-07-01 00:00:00" + ) mysql_user = MYSQL_CONFIG.get("user", "xch") mysql_password = MYSQL_CONFIG.get("password", "") if not mysql_password: @@ -52,6 +65,7 @@ class MarketDataMain: self.db_url = f"mysql+pymysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_database}" self.db_market_data = DBMarketData(self.db_url) self.trade_data_main = TradeDataMain() + self.is_us_stock = is_us_stock def initial_data(self): """ @@ -96,7 +110,15 @@ class MarketDataMain: # 获取数据,直到end_time_ts threshold = None if bar in ["5m", "15m", "30m"]: - threshold = 86400000 + if self.is_us_stock: + if bar == "5m": + threshold = 86400000 * 4 + elif bar == "15m": + threshold = 86400000 * 4 * 3 + elif bar == "30m": + threshold = 86400000 * 4 * 6 + else: + threshold = 86400000 elif bar in ["1H", "4H"]: threshold = 432000000 elif bar == "1D": @@ -105,7 +127,7 @@ class MarketDataMain: get_data = False min_start_time_ts = start_time_ts while start_time_ts < end_time_ts: - current_start_time_ts = end_time_ts - threshold + current_start_time_ts = int(end_time_ts - threshold) if current_start_time_ts < start_time_ts: current_start_time_ts = start_time_ts start_date_time = timestamp_to_datetime(current_start_time_ts) @@ -113,46 +135,21 @@ class MarketDataMain: logger.info( f"获取行情数据: {symbol} {bar} 从 {start_date_time} 到 {end_date_time}" ) + if self.is_us_stock: + limit = 1000 + else: + limit = 100 data = self.market_data.get_historical_kline_data( symbol=symbol, start=current_start_time_ts, bar=bar, end_time=end_time_ts, + limit=limit, ) if data is not None and len(data) > 0: data["buy_sz"] = -1 data["sell_sz"] = -1 - # 根据交易数据,设置buy_sz和sell_sz - # 比特币的数据获取过慢,暂时不获取交易数据 - # if not symbol.endswith("-SWAP"): - # # trade_data的end_time需要比market_data的end_time大一个周期 - # trade_data = self.trade_data_main.get_trade_data( - # symbol=symbol, start_time=current_start_time_ts, end_time=end_time_ts - # ) - # for index, row in data.iterrows(): - # try: - # current_from_time = int(row["timestamp"]) - # if index == len(data) - 1: - # current_to_time = current_from_time + BAR_THRESHOLD[bar] - # else: - # current_to_time = int(data.iloc[index + 1]["timestamp"]) - # current_trade_data = trade_data[ - # (trade_data["ts"] >= current_from_time) - # & (trade_data["ts"] <= current_to_time) - # ] - # if current_trade_data is not None and len(current_trade_data) > 0: - # current_buy_sz = current_trade_data[ - # current_trade_data["side"] == "buy" - # ]["sz"].sum() - # current_sell_sz = current_trade_data[ - # current_trade_data["side"] == "sell" - # ]["sz"].sum() - # data.loc[index, "buy_sz"] = current_buy_sz - # data.loc[index, "sell_sz"] = current_sell_sz - # except Exception as e: - # logger.error(f"设置buy_sz和sell_sz失败: {e}") - # continue if data is not None and len(data) > 0: data = data[ [ @@ -160,6 +157,7 @@ class MarketDataMain: "bar", "timestamp", "date_time", + "date_time_us", "open", "high", "low", @@ -174,13 +172,22 @@ class MarketDataMain: ] data = self.add_new_columns(data) self.db_market_data.insert_data_to_mysql(data) - current_min_start_time_ts = data["timestamp"].min() + current_min_start_time_ts = int(data["timestamp"].min()) if current_min_start_time_ts < min_start_time_ts: min_start_time_ts = current_min_start_time_ts + get_data = True + else: + logger.warning(f"获取行情数据为空: {symbol} {bar} 从 {start_date_time} 到 {end_date_time}") + break + if current_start_time_ts == start_time_ts: break - end_time_ts = current_start_time_ts + + if current_min_start_time_ts < current_start_time_ts: + end_time_ts = current_min_start_time_ts + else: + end_time_ts = current_start_time_ts if min_start_time_ts is not None and get_data: # 补充技术指标数据 # 获得min_start_time_ts之前30条数据 @@ -374,7 +381,7 @@ class MarketDataMain: 批量计算技术指标 """ logger.info("开始批量计算技术指标") - start_date_time = MONITOR_CONFIG.get("volume_monitor", {}).get( + start_date_time = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-15 00:00:00" ) start_timestamp = transform_date_time_to_timestamp(start_date_time) diff --git a/market_monitor_main.py b/market_monitor_main.py index bbbae87..50478c3 100644 --- a/market_monitor_main.py +++ b/market_monitor_main.py @@ -4,7 +4,7 @@ from huge_volume_main import HugeVolumeMain from core.biz.market_monitor import create_metrics_report from core.db.db_market_monitor import DBMarketMonitor from core.wechat import Wechat -from config import MONITOR_CONFIG, MYSQL_CONFIG +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp import core.logger as logging @@ -21,9 +21,9 @@ class MarketMonitorMain: self.market_data_main = MarketDataMain() self.huge_volume_main = HugeVolumeMain() self.wechat = Wechat() - self.monitor_config = MONITOR_CONFIG + self.monitor_config = OKX_MONITOR_CONFIG self.window_size = 100 - self.start_date = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.start_date = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "initial_date", "2025-05-01 00:00:00" ) self.latest_record_file_path = "./output/record/latest_record.json" diff --git a/orb_trade_main.py b/orb_trade_main.py new file mode 100644 index 0000000..d14a0d7 --- /dev/null +++ b/orb_trade_main.py @@ -0,0 +1,28 @@ +from core.trade.orb_trade import ORBStrategy + +def main(): + # 初始化ORB策略 + orb_strategy = ORBStrategy( + initial_capital=25000, + max_leverage=4, + risk_per_trade=0.01, + commission_per_share=0.0005, + ) + + # 1. 获取QQQ的5分钟日内数据(2024-2025,注意:yfinance免费版可能限制历史日内数据,建议用专业数据源) + orb_strategy.fetch_intraday_data( + symbol="ETH-USDT", start_date="2025-05-15", end_date="2025-08-20", interval="5m" + ) + + # 2. 生成ORB策略信号 + orb_strategy.generate_orb_signals() + + # 3. 回测策略(盈利目标10R) + orb_strategy.backtest(profit_target_multiple=10) + + # 4. 绘制净值曲线 + orb_strategy.plot_equity_curve() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/sql/query/sql_playground.sql b/sql/query/sql_playground.sql index 92664b0..41b21c5 100644 --- a/sql/query/sql_playground.sql +++ b/sql/query/sql_playground.sql @@ -1,12 +1,19 @@ -select * from crypto_market_monitor -order by date_time desc; +select * from crypto_market_data +where symbol = "TQQQ" and bar="5m" +order by date_time_us DESC; + +select * from crypto_huge_volume +where symbol = "QQQ" and bar="5m" +order by date_time; + + select symbol, bar, date_time, close, pct_chg, ma_cross, ma5, ma10, ma20, ma30, dif, dea, macd, kdj_k, kdj_d, kdj_k, kdj_pattern, rsi_14, rsi_signal, boll_upper, boll_middle, boll_lower, boll_pattern, boll_signal from crypto_market_data -WHERE close > boll_upper +WHERE ma_cross in ("20下穿10", "10下穿5") order by timestamp desc; select symbol, bar, window_size, date_time, close, @@ -14,8 +21,8 @@ volume, volume_ratio, huge_volume, close_20_low, low_20_low, close_10_low, low_10_low, close_80_high, close_90_high, high_80_high, high_90_high from crypto_huge_volume -WHERE symbol='XCH-USDT' and bar='5m' and window_size=120# and low_10_low=1 -order by timestamp desc; +WHERE symbol='BTC-USDT' and bar='5m' and window_size=120# and low_10_low=1 +order by timestamp; select * from crypto_huge_volume WHERE symbol='BTC-USDT' and bar='5m' #and date_time > '2025-08-04 15:00:00' diff --git a/sql/table/crypto_market_data.sql b/sql/table/crypto_market_data.sql index 5988f76..44876f4 100644 --- a/sql/table/crypto_market_data.sql +++ b/sql/table/crypto_market_data.sql @@ -7,6 +7,7 @@ CREATE TABLE IF NOT EXISTS crypto_market_data ( bar VARCHAR(20) NOT NULL, timestamp BIGINT NOT NULL, date_time VARCHAR(50) NOT NULL, + date_time_us VARCHAR(50) NULL COMMENT '美国时间格式的日期时间', open DECIMAL(20,10) NOT NULL, high DECIMAL(20,10) NOT NULL, low DECIMAL(20,10) NOT NULL, @@ -60,3 +61,6 @@ CREATE TABLE IF NOT EXISTS crypto_market_data ( --修改ma_cross字段长度为150 ALTER TABLE crypto_market_data MODIFY COLUMN ma_cross VARCHAR(150) DEFAULT NULL COMMENT '均线交叉信号'; + +--添加date_time_us字段 +ALTER TABLE crypto_market_data ADD COLUMN date_time_us VARCHAR(50) NULL COMMENT '美国时间格式的日期时间' AFTER date_time; diff --git a/test_ma_methods.py b/test_ma_methods.py index b475563..e399344 100644 --- a/test_ma_methods.py +++ b/test_ma_methods.py @@ -10,7 +10,7 @@ import matplotlib.pyplot as plt from core.db.db_market_data import DBMarketData from core.biz.metrics_calculation import MetricsCalculation import logging -from config import MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE +from config import OKX_MONITOR_CONFIG, MYSQL_CONFIG, WINDOW_SIZE # plt支持中文 plt.rcParams['font.family'] = ['SimHei'] diff --git a/trade_data_main.py b/trade_data_main.py index 49b7220..5a30ebe 100644 --- a/trade_data_main.py +++ b/trade_data_main.py @@ -9,7 +9,7 @@ from config import ( SECRET_KEY, PASSPHRASE, SANDBOX, - MONITOR_CONFIG, + OKX_MONITOR_CONFIG, MYSQL_CONFIG, ) @@ -48,7 +48,7 @@ class TradeDataMain: # 处理start参数 if start_time is None: # 默认两个月前 - start_time_str = MONITOR_CONFIG.get("volume_monitor", {}).get("initial_date", "2025-05-01 00:00:00") + start_time_str = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get("initial_date", "2025-05-01 00:00:00") start_time = transform_date_time_to_timestamp(start_time_str) else: start_time = transform_date_time_to_timestamp(start_time) diff --git a/trade_ma_strategy_main.py b/trade_ma_strategy_main.py index 7b815b8..128e88e 100644 --- a/trade_ma_strategy_main.py +++ b/trade_ma_strategy_main.py @@ -17,7 +17,7 @@ from config import ( SECRET_KEY, PASSPHRASE, SANDBOX, - MONITOR_CONFIG, + OKX_MONITOR_CONFIG, MYSQL_CONFIG, BAR_THRESHOLD, ) diff --git a/trade_sandbox_main.py b/trade_sandbox_main.py index 3714c00..59a9f04 100644 --- a/trade_sandbox_main.py +++ b/trade_sandbox_main.py @@ -12,7 +12,7 @@ from openpyxl.drawing.image import Image import openpyxl from openpyxl.styles import Font from PIL import Image as PILImage -from config import MONITOR_CONFIG +from config import OKX_MONITOR_CONFIG from core.trade.mean_reversion_sandbox import MeanReversionSandbox from core.utils import timestamp_to_datetime, transform_date_time_to_timestamp @@ -24,14 +24,14 @@ logger = logging.logger class MeanReversionSandboxMain: def __init__(self, start_date: str, end_date: str, window_size: int, only_5m: bool = False, solution_list: list = None): - self.symbols = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.symbols = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "symbols", ["XCH-USDT"] ) self.only_5m = only_5m if only_5m: self.bars = ["5m"] else: - self.bars = MONITOR_CONFIG.get("volume_monitor", {}).get( + self.bars = OKX_MONITOR_CONFIG.get("volume_monitor", {}).get( "bars", ["5m", "15m", "30m", "1H"] ) if solution_list is None: