<think>我们计划实现一个通达信DLL函数,用于计算所有股票每日涨幅的中位数。由于数据量巨大,我们需要考虑高效读取数据的方法。
步骤:
1. 遍历vipdoc目录下的所有日线数据(.day文件)
2. 对于每个股票,读取其日线数据(主要是日期和收盘价)
3. 按日期组织数据:将同一天的股票的涨幅(今日收盘价/昨日收盘价 - 1)收集起来
4. 对于每一天,计算所有股票涨幅的中位数
5. 将结果按日期顺序输出到pfOUT数组中
注意:我们只考虑2012年以来的数据。
由于通达信DLL函数的输出数组长度由DataLen指定(即需要计算的天数),我们只输出最近DataLen天的数据。
难点:
1. 数据量巨大:需要遍历所有股票,并且需要跨日期组织数据。
2. 效率:需要高效读取,可能需要缓存机制和内存映射。
3. 内存管理:需要合理设计数据结构,避免内存溢出。
实现思路:
我们将按以下步骤进行:
阶段1:数据收集
- 遍历sh和sz目录下的lday子目录,读取所有.day文件。
- 对于每个股票,读取其2012年以来的日线数据(日期和收盘价)。
- 构建一个按日期索引的字典(或map),每个日期对应一个列表,存储当天所有股票的涨幅(需要前一天的收盘价计算涨幅)。
阶段2:计算中位数
- 遍历每个交易日(从2012年1月1日到最新日期),对于每一天,计算当天所有股票涨幅的中位数。
- 注意:需要剔除新股(上市首日没有前收盘)和停牌股票(价格不变)。
阶段3:输出结果
- 将计算得到的中位数序列,按照时间由远及近的顺序(通达信默认是最近日期在最后)填充到pfOUT数组,但注意通达信公式要求输出数组是从最近日期倒序(即最新日期在数组最前面?)实际上,通达信的K线数组是时间倒序的,即索引0是最近一天,索引1是前一天,以此类推。所以我们的输出数组也要按照这个顺序:pfOUT[0]应该是最近一天的中位数,pfOUT[1]是前一天的中位数,...,直到DataLen指定的天数。
然而,由于数据量巨大,我们不可能在每次调用DLL函数时都重新遍历所有股票。因此,我们需要考虑缓存机制。但通达信DLL每次调用都是独立的,我们无法在多次调用之间保持状态(除非使用全局变量,但要注意线程安全)。
因此,我们设计为第一次调用时进行全量计算,并将结果缓存到内存中。后续调用直接使用缓存的数据。
具体实现:
1. 定义一个全局变量来存储计算结果(按日期排序的涨幅中位数数组,时间从旧到新,这样我们可以方便地取最近的数据)
2. 当DLL被加载时,全局变量为空。第一次调用时,执行全量计算并填充全局变量,同时记录计算的数据日期范围(起始日期和结束日期)。
3. 后续调用时,直接使用全局变量中的数据,根据DataLen要求输出最近的DataLen天的数据。
注意:如果用户更新了数据(添加了新的交易日),我们需要重新计算。但是,我们无法感知数据更新。因此,我们可以考虑在每次调用时检查最新日期是否已经超过缓存中的最新日期,如果是,则重新计算(或者重新计算新增部分)。但为了简化,我们可以每次启动通达信后第一次调用时计算,后续调用使用缓存。或者提供一个重置缓存的机制(比如在DLL函数中设置一个重置标志,通过输入参数控制)。
这里,我们使用一个简单的方案:每次调用都重新计算(这样简单但效率低,不适合实际使用)。但考虑到计算量巨大,重新计算可能需要几分钟,显然不可行。
因此,我们采用以下优化方案:
- 缓存计算结果到文件。每次启动时检查缓存文件的时间戳是否晚于数据目录的最新修改时间,如果不是,则重新计算。
- 或者,我们设计为手动触发重新计算(通过一个特殊的输入参数)。
为了简化,我们暂时不考虑缓存持久化,仅使用内存缓存,且当数据日期范围不够时重新计算(比如用户要求输出的日期范围超过了我们缓存的最新日期,我们就重新计算)。但这样仍然复杂。
考虑到实际使用中,我们可能只需要最近几年的数据(比如最近5年),我们可以每次只计算最近5年的数据,并且每次调用都重新计算(但这样每次调用都会重新遍历所有股票,效率低,可能造成通达信卡顿)。
因此,我们需要更高效的方案。我们决定采用以下策略:
- 使用内存映射文件读取.day文件,提高读取速度。
- 使用多线程并行处理多个股票(但通达信DLL调用可能要求单线程?需要测试)。
- 按日期组织数据时,使用预分配的大数组,避免频繁内存分配。
但即使如此,遍历4000多只股票,每只股票有2000多条记录(10年数据),总记录数约800万条,按日期分组后,每个交易日有4000多条记录。计算中位数需要排序,每个交易日排序4000个数的复杂度为O(n log n),总共约2500个交易日,总计算量较大。
因此,我们需要优化中位数的计算:使用快速选择算法(O(n))来求中位数。
具体步骤:
1. 遍历所有股票,读取其日线数据(只读日期和收盘价),并存储在内存中(每个股票一个数组)。
2. 确定日期范围:从2012年1月1日到当前日期(注意:通达信.day文件的最新日期)。
3. 创建一个二维数组(按日期索引,每个日期一个数组,存储该日所有股票的涨幅)。注意:我们只需要2012年以后的日期。
4. 对于每个股票,遍历其日线数据,对于每一天(除第一日外),计算涨幅: (今日收盘价/昨日收盘价 - 1) * 100(单位:%)
5. 将涨幅添加到对应日期的数组中。
6. 遍历每个交易日,对当天的涨幅数组使用快速选择算法找到中位数。
然而,步骤3需要一个大二维数组,日期作为索引,我们可能需要一个从2012-01-01到今天的连续日期数组。我们可以用从2012-01-01到最新日期的所有自然日(但股票交易日并非自然日),所以实际上我们只需要股票交易日(即所有股票数据中出现的日期)。但不同股票交易日可能不同(比如停牌),我们以所有股票共同存在的交易日为准?实际上,我们只需要收集在某个交易日有交易的股票即可。
因此,我们首先需要收集所有出现过的交易日(并集)。然后,对于每一天,只要有股票交易,我们就将其加入。
但这样会导致某些交易日(比如节假日)没有数据,所以实际上我们只需要从2012-01-01开始到最新日期,只考虑交易日(即.day文件中出现的日期)。我们可以通过一个集合来记录所有出现过的日期(从每个股票中读取日期,然后合并)。
为了避免重复,我们可以先收集所有日期,然后排序去重,得到所有交易日的列表。
然后,我们为每个交易日分配一个数组(用vector of vectors),但这样内存消耗很大(约2500天*4000只股票*4字节=40MB,不算大)。
实现上的优化:
- 我们不需要存储所有股票的历史数据,只需要在读取一个股票后,立即将其每天的涨幅添加到对应日期的数组中。这样,我们只需要一个按日期索引的字典(map),字典的值是一个vector,存储该日所有股票的涨幅。
步骤:
初始化:一个空的map(key: 日期(DWORD),value: vector<float>)
遍历每个股票:
读取该股票的所有日线数据(日期和收盘价)
按日期排序(通达信.day文件已经按日期升序排列,所以我们可以顺序处理)
从第二天开始,计算每日涨幅,并将涨幅添加到map中对应日期的vector中。
然后,将map的key(日期)排序,得到所有交易日的列表(升序)。
接着,遍历这个日期列表,对每个日期,计算该日期对应的涨幅数组的中位数,存储到一个数组中(按日期顺序)。
最后,将这个中位数数组按照时间倒序排列(因为通达信要求最近的数据在输出数组的前面),然后根据DataLen截取需要的部分。
但是,注意:我们只计算2012年以来的数据,所以2012年以前的数据忽略。
通达信DLL函数接口:
int FuncName(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc)
由于这个函数不需要输入参数(除了DataLen),所以我们可以将pfINa, pfINb, pfINc忽略。
然而,我们需要一个机制来触发重新计算。我们可以设置一个全局变量来缓存计算结果(中位数数组,按日期升序排列,即第一个元素是2012年第一个交易日的中位数,最后一个元素是最近一个交易日的中位数)。同时,我们缓存计算的最新日期。
每次调用时,我们检查当前最新日期(通过某个股票的最新日期)是否大于缓存的最新日期,如果是,则重新计算(或者追加计算新的交易日)。但为了简化,我们每次调用都重新计算(仅当缓存为空时计算,后续调用使用缓存)。这样,在通达信运行期间,第一次调用会计算,后续调用使用缓存。但是,如果用户更新了数据(比如下载了新的日线数据),缓存不会更新。因此,我们可以通过一个输入参数来强制重新计算。
我们设计pfINa[0]作为重新计算标志:0表示使用缓存(如果有),非0表示强制重新计算。
具体DLL函数实现步骤:
1. 解析输入参数:pfINa[0]表示是否强制重新计算(1表示强制,0表示不强制)
2. 如果缓存为空,或者强制重新计算,则执行全量计算。
3. 从缓存的中位数数组(按日期升序,即从过去到现在)中,取出最近的DataLen个数据(即数组的最后DataLen个),然后逆序(因为通达信要求最近一天在pfOUT[0])复制到pfOUT中。
注意:如果DataLen大于我们缓存的数据长度,则只能返回缓存的所有数据(并返回实际返回的数据个数)。
代码结构:
全局变量:
static std::vector<float> g_medianValues; // 按日期升序存储的每日涨幅中位数(从2012年第一个交易日到最新交易日)
static DWORD g_lastDate = 0; // 缓存的最新日期(YYYYMMDD)
计算函数:ComputeAllStockMedians(),执行全量计算。
ComputeAllStockMedians()函数实现:
1. 遍历sh和sz目录
2. 遍历每个.day文件
3. 读取股票日线数据(2012年以来的数据)
4. 计算涨幅并添加到按日期索引的map中
5. 收集所有日期,排序
6. 对每个日期,计算涨幅列表的中位数,存入g_medianValues(按日期升序)
7. 记录g_lastDate为最后一天的日期
注意:中位数计算时,如果某天没有股票交易(理论上不会,因为至少有一只股票交易),则中位数为0(或者用前值?但这里我们直接设为0,因为正常情况下不会出现)。
快速选择算法实现中位数:
我们可以使用std::nth_element,但注意中位数:
如果数组元素个数n为奇数,则中位数是第(n-1)/2个(0-based)
如果为偶数,则中位数是中间两个数的平均值。
代码:
std::vector<float>& v = dateMap[date];
if (v.empty()) {
median = 0.0f;
} else {
size_t n = v.size() / 2;
std::nth_element(v.begin(), v.begin() + n, v.end());
if (v.size() % 2 == 1) {
median = v[n];
} else {
// 偶数,需要找到中间两个数的平均值
float high = v[n];
// 找到前n-1个元素的最大值(即第n-1大的数)?实际上,nth_element后,第n个元素是第n小的,而前n个元素是前n小的,但不保证有序
// 所以,我们需要将前n个元素的最大值找出来?这样效率低。我们可以使用partial_sort,但更慢。
// 另一种方法:使用nth_element两次?第一次找到第n-1小的,第二次找到第n小的?但这样不高效。
// 我们可以使用std::nth_element后,再在左半部分找最大值?这样需要遍历,O(n)。或者使用std::partial_sort(v.begin(), v.begin()+n+1, v.end()),这样保证前n+1个有序,然后取v[n-1]和v[n]的平均值。
// 由于n可能很大,我们采用:先复制,然后只排序前n+1个元素?这样效率不高。
// 更高效:使用std::nth_element后,再对前n个元素使用std::max_element?这样是O(n)。总复杂度O(n)+O(n)=O(n),可以接受。
auto it = std::max_element(v.begin(), v.begin()+n);
float low = *it;
median = (low + high) / 2.0f;
}
}
但是,注意:我们要求的是整个数组的中位数,当数组元素个数为偶数时,中位数是中间两个数的平均值。而上面的方法中,我们通过nth_element(n)后,第n个元素是第n+1小的(因为0-based),然后我们要求第n-1小的元素(即第n小)?实际上,当n=数组长度/2(整数除法)时,如果数组长度为偶数,则中位数是第n-1小和第n小的平均值。
举例:数组[1,2,3,4](n=4),中位数是(2+3)/2=2.5。我们令n=2(即4/2=2),则nth_element(v.begin(), v.begin()+2, v.end())后,v[2]是第3小的数(即3),而前2个元素(v[0]和v[1])是1和2(但不保证顺序)。然后我们在前2个元素中找最大值,得到2,然后中位数=(2+3)/2=2.5。
所以,上面的方法正确。
但是,注意:nth_element会改变数组,但我们之后不再需要这个数组,所以没关系。
另外,我们也可以使用std::sort,但复杂度O(nlogn)比O(n)高。当n=4000时,O(4000*log4000)≈4000*12=48000,而O(n)的快速选择是4000,所以快速选择更快。
实现代码框架:
全局变量:
static bool g_cached = false;
static std::vector<float> g_medianValues; // 按日期升序(从2012年第一个交易日到最新交易日)
static CRITICAL_SECTION g_cs; // 用于多线程安全
在DLL入口函数中初始化临界区,在DLL卸载时删除。
DLL函数实现:
__declspec(dllexport) int CalcDailyMedian(int DataLen, float* pfOUT, float* pfINa, float* pfINb, float* pfINc)
{
EnterCriticalSection(&g_cs);
bool forceRecalc = (pfINa[0] != 0.0f); // 如果pfINa[0]非0,则强制重新计算
if (forceRecalc || !g_cached) {
ComputeAllStockMedians(); // 重新计算
g_cached = true;
}
// 计算需要输出的数据:g_medianValues是升序(索引0是2012年最早的数据,最后一个索引是最新一天)
// 但是通达信要求输出的是最近DataLen天的数据,并且时间倒序(即最新一天在pfOUT[0])
// 因此,我们从g_medianValues的末尾开始取DataLen个数据,然后逆序拷贝到pfOUT
int totalSize = static_cast<int>(g_medianValues.size());
int startIndex = totalSize - DataLen; // 如果DataLen<=totalSize,则从startIndex开始取DataLen个
if (startIndex < 0) {
startIndex = 0;
DataLen = totalSize; // 实际输出的数据量
}
// 将g_medianValues的[startIndex, startIndex+DataLen-1]这一段数据逆序拷贝到pfOUT
for (int i = 0; i < DataLen; i++) {
pfOUT[i] = g_medianValues[totalSize - 1 - i]; // 注意:totalSize-1是最后一个元素(最新一天),然后往前推
// 这样,pfOUT[0] = 最新一天的中位数,pfOUT[1] = 次新一天的中位数,...
}
LeaveCriticalSection(&g_cs);
return DataLen; // 返回实际输出的数据个数
}
注意:这个函数返回的是每天的涨幅中位数(单位:%),例如1.5表示1.5%。
但是,由于数据量巨大,计算可能需要较长时间(几秒到几十秒),所以第一次调用时会卡住通达信。因此,我们需要考虑进度提示,但通达信DLL无法输出进度。所以,我们只能尽量优化速度。
优化速度的方法:
- 使用内存映射文件读取.day文件
- 多线程读取股票文件(但要注意文件IO瓶颈,以及线程安全)
- 使用更高效的数据结构
由于时间关系,我们先实现单线程版本。
遍历目录的方法:
- 使用Windows API的FindFirstFile/FindNextFile遍历vipdoc下的sh和sz目录的lday子目录。
代码片段:
WIN32_FIND_DATA findFileData;
HANDLE hFind = FindFirstFile("C:\\new_tdx\\vipdoc\\sh\\lday\\*.day", &findFileData);
if (hFind != INVALID_HANDLE_VALUE) {
do {
if (!(findFileData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
// 处理文件:findFileData.cFileName
std::string filePath = "C:\\new_tdx\\vipdoc\\sh\\lday\\" + std::string(findFileData.cFileName);
// 读取这个文件
}
} while (FindNextFile(hFind, &findFileData));
FindClose(hFind);
}
同样处理sz目录。
注意:文件名为8个字符(如sh600000.day),其中前2位是市场代码,后6位是股票代码。但B股文件可能不同(如sh900901_B.day)?但根据之前的经验,通达信.day文件命名规则是:市场代码+股票代码+后缀,其中市场代码为sh或sz。所以,我们可以直接使用文件名。
但是,我们只需要文件名中的股票代码部分:从文件名中提取,去掉".day"后,从第2个字符开始取6位(如果是sh开头,则从第2个字符开始取6位,即跳过sh)。但是注意,文件名可能是"sh600000.day",所以股票代码为"600000"。
读取.day文件数据(2012年以来的):
我们只读取2012-01-01之后的日期。注意:通达信.day文件中的日期是DWORD,格式为YYYYMMDD。
代码:
std::ifstream file(filePath, std::ios::binary);
TdxDayData data;
std::vector<std::pair<DWORD, float>> stockData; // 日期和收盘价(注意:收盘价是DWORD,需要除以1000.0f得到实际价格?)
while (file.read((char*)&data, sizeof(TdxDayData))) {
if (data.date < 20120101) // 2012年以前的数据跳过
continue;
// 存储收盘价:注意,通达信存储的是整数,需要除以1000.0f
float close = data.close / 1000.0f;
stockData.push_back(std::make_pair(data.date, close));
}
// 注意:通达信.day文件是时间升序排列(从早到晚)
然后,计算涨幅:
对于第i天(i>=1),涨幅 = (stockData[i].second / stockData[i-1].second - 1) * 100.0f
注意:如果前一天停牌(即没有数据),则跳过?但这里我们连续读取,所以相邻两天在同一个股票中一定是连续的交易日(因为.day文件已经按日期排序,且只包含交易日)。
但是,不同股票可能在某些交易日停牌,所以同一个交易日,并不是所有股票都有数据。我们只计算有连续两天数据的股票。
将涨幅添加到对应日期的vector中:
日期 = stockData[i].first
涨幅 = (当天收盘价/前一天收盘价 - 1) * 100
注意:同一个股票,在计算涨幅时,如果遇到停牌(比如连续停牌多日),则.day文件中会缺失这些交易日,所以相邻两个记录之间可能不是连续的交易日。但通达信.day文件中,每个交易日都是连续的(即每个交易日都有记录,即使停牌,但停牌时价格是0?或者用前收盘?)
实际上,通达信.day文件中,停牌日的交易量为0,但价格是前收盘(即没有变化)。所以,我们仍然可以计算涨幅,但涨幅为0。因此,我们不需要跳过停牌日。
所以,对于每个股票,我们只需要从第二天开始计算涨幅。
代码:
for (size_t i = 1; i < stockData.size(); i++) {
DWORD date = stockData[i].first;
float prevClose = stockData[i-1].second;
float close = stockData[i].second;
float ret = (close / prevClose - 1.0f) * 100.0f;
// 将ret添加到dateMap[date]中
}
注意:prevClose和close都是浮点数,但注意除零问题(如果prevClose为0,则跳过)。但正常情况下,股票价格不会为0。
最后,我们需要注意内存使用:在计算过程中,dateMap会占用大量内存(约2500天,每天4000个浮点数,约2500*4000*4=40MB)。另外,每个股票的数据读取后就可以释放,所以我们可以一边读取股票文件,一边将涨幅添加到dateMap,然后释放股票数据。
代码实现细节较多,下面给出完整代码框架(注意:省略了错误处理和一些优化):</think>### 通达信DLL实现全市场日涨幅中位数计算方案
#### 核心实现思路
```mermaid
graph TD
A[遍历VIPOC目录] --> B[读取所有股票日线数据]
B --> C[按交易日分组涨幅数据]
C --> D[计算每日涨幅中位数]
D --> E[填充输出数组]
```
#### 关键数据结构
通达信.day文件格式(小端序):
```cpp
#pragma pack(push, 1)
struct TdxDayData {
DWORD date; // 日期(YYYYMMDD)
DWORD open; // 开盘价*1000
DWORD high; // 最高价*1000
DWORD low; // 最低价*1000
DWORD close; // 收盘价*1000
float amount; // 成交额(万元)
DWORD volume; // 成交量(手)
DWORD reserve; // 保留字段
};
#pragma pack(pop)
```
#### 完整DLL实现代码
```cpp
#include <windows.h>
#include <fstream>
#include <vector>
#include <string>
#include <map>
#include <algorithm>
#include <shlwapi.h> // PathFileExists
#pragma comment(lib, "shlwapi.lib")
// 通达信数据目录路径
const char* VIPOC_PATH = "C:\\new_tdx\\vipdoc\\";
// 全局缓存
static std::map<DWORD, std::vector<float>> g_dailyReturns; // 每日涨幅缓存<日期, 涨幅列表>
static bool g_cacheValid = false;
// 快速选择算法求中位数
float CalculateMedian(std::vector<float>& v) {
if (v.empty()) return 0.0f;
size_t n = v.size() / 2;
std::nth_element(v.begin(), v.begin() + n, v.end());
if (v.size() % 2 == 1) {
return v[n];
} else {
float high = v[n];
float low = *std::max_element(v.begin(), v.begin() + n);
return (low + high) / 2.0f;
}
}
// 读取单只股票日线数据
bool ReadStockData(const char* filePath, std::map<DWORD, float>& closes) {
std::ifstream file(filePath, std::ios::binary);
if (!file) return false;
while (!file.eof()) {
TdxDayData data;
file.read(reinterpret_cast<char*>(&data), sizeof(TdxDayData));
if (file.gcount() < sizeof(TdxDayData)) break;
// 只处理2012年及以后的数据
if (data.date >= 20120101) {
closes[data.date] = static_cast<float>(data.close) / 1000.0f;
}
}
return true;
}
// 遍历市场目录处理股票数据
void ProcessMarketData(const char* market) {
char path[MAX_PATH];
sprintf_s(path, "%s%s\\lday\\", VIPOC_PATH, market);
WIN32_FIND_DATA findData;
HANDLE hFind = FindFirstFile((std::string(path) + "*.day").c_str(), &findData);
if (hFind == INVALID_HANDLE_VALUE) return;
do {
if (!(findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
char filePath[MAX_PATH];
sprintf_s(filePath, "%s%s", path, findData.cFileName);
std::map<DWORD, float> stockCloses;
if (ReadStockData(filePath, stockCloses)) {
// 计算每日涨幅
auto it = stockCloses.begin();
float prevClose = it->second;
for (++it; it != stockCloses.end(); ++it) {
if (prevClose > 0.001f) { // 避免除零
float ret = (it->second / prevClose - 1.0f) * 100.0f;
g_dailyReturns[it->first].push_back(ret);
}
prevClose = it->second;
}
}
}
} while (FindNextFile(hFind, &findData));
FindClose(hFind);
}
// 主计算函数
void ComputeAllStockMedians() {
if (g_cacheValid) return;
g_dailyReturns.clear();
ProcessMarketData("sh"); // 处理上海市场
ProcessMarketData("sz"); // 处理深圳市场
g_cacheValid = true;
}
// 通达信DLL导出函数
extern "C" __declspec(dllexport) int CalcDailyMedian(
int DataLen, // 需要输出的数据长度
float* pfOUT, // 输出数组
float* pfINa, // 输入参数1 (强制刷新标志)
float* pfINb, // 输入参数2 (未使用)
float* pfINc // 输入参数3 (未使用)
) {
// 强制刷新缓存
if (pfINa[0] != 0) g_cacheValid = false;
// 计算全市场数据
ComputeAllStockMedians();
// 提取日期并排序
std::vector<DWORD> dates;
for (const auto& pair : g_dailyReturns) {
dates.push_back(pair.first);
}
std::sort(dates.begin(), dates.end());
// 计算每日中位数
std::map<DWORD, float> dailyMedians;
for (const auto& date : dates) {
std::vector<float> returns = g_dailyReturns[date];
dailyMedians[date] = CalculateMedian(returns);
}
// 填充输出数组(时间倒序)
int outputCount = 0;
for (auto rit = dates.rbegin(); rit != dates.rend() && outputCount < DataLen; ++rit) {
pfOUT[outputCount++] = dailyMedians[*rit];
}
return outputCount; // 返回实际输出数据个数
}
// DLL入口点
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
switch (ul_reason_for_call) {
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
```
#### 关键算法实现
1. **中位数计算(快速选择算法)**:
```cpp
float CalculateMedian(std::vector<float>& v) {
if (v.empty()) return 0.0f;
size_t n = v.size() / 2;
std::nth_element(v.begin(), v.begin() + n, v.end());
if (v.size() % 2 == 1) {
return v[n];
} else {
float high = v[n];
float low = *std::max_element(v.begin(), v.begin() + n);
return (low + high) / 2.0f;
}
}
```
时间复杂度:$O(n)$,空间复杂度:$O(1)$
2. **涨幅计算公式**:
$$ \text{日涨幅} = \left( \frac{\text{当日收盘价}}{\text{前日收盘价}} - 1 \right) \times 100\% $$
#### 性能优化策略
1. **缓存机制**:
```cpp
static std::map<DWORD, std::vector<float>> g_dailyReturns;
static bool g_cacheValid = false;
```
- 首次调用后缓存计算结果
- 通过`pfINa[0]`参数控制缓存刷新
2. **内存映射文件**:
```cpp
HANDLE hFile = CreateFile(filePath, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
HANDLE hMap = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL);
TdxDayData* pData = (TdxDayData*)MapViewOfFile(hMap, FILE_MAP_READ, 0, 0, 0);
```
3. **并行处理**:
```cpp
#pragma omp parallel for
for (int i = 0; i < fileCount; i++) {
// 并行处理不同股票文件
}
```
#### 特殊处理事项
1. **B股特殊处理**:
```cpp
if (strstr(fileName, "_B.day")) {
// 特殊处理B股数据
}
```
2. **退市股票处理**:
```cpp
if (!PathFileExists(filePath)) {
// 尝试在delisted目录查找
sprintf_s(altPath, "%sdelisted\\%s\\%s", VIPOC_PATH, market, fileName);
}
```
3. **异常数据处理**:
```cpp
if (prevClose < 0.01f) continue; // 跳过无效价格
if (abs(ret) > 30.0f) continue; // 过滤异常涨幅
```
#### 通达信公式调用方法
```javascript
// 公式名称: 全市场日涨幅中位数
MEDIAN_RET:TDXDLL3(3, FORCE_REFRESH, 0, 0);
```
参数说明:
- `3`:DLL函数编号
- `FORCE_REFRESH`:强制刷新缓存标志(0=使用缓存,1=重新计算)
- 返回:最近N日的全市场涨幅中位数(%)
#### 部署与测试
1. **编译要求**:
```bash
g++ -shared -o MarketMedian.dll MarketMedian.cpp -lshlwapi
```
2. **部署路径**:
```
通达信安装目录\T0002\dlls\MarketMedian.dll
```
3. **性能测试**:
```cpp
// 测试100次调用的平均耗时
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 100; i++) {
CalcDailyMedian(500, outBuf, forceRefresh, 0, 0);
}
auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start);
std::cout << "平均耗时: " << duration.count()/100.0 << "ms" << std::endl;
```
#### 数据验证方法
```cpp
void VerifyResults() {
// 验证特定日期数据
DWORD testDate = 20230601;
float median = dailyMedians[testDate];
// 人工验证该日涨幅中位数
std::vector<float> returns = g_dailyReturns[testDate];
std::sort(returns.begin(), returns.end());
float expected = (returns[returns.size()/2-1] + returns[returns.size()/2])/2;
assert(fabs(median - expected) < 0.001f);
}
```
#### 内存管理优化
```cpp
// 使用内存池管理临时向量
static boost::pool<> vectorPool(sizeof(std::vector<float>));
std::vector<float>* pReturns = new (vectorPool.malloc()) std::vector<float>();
// 使用后归还内存池
vectorPool.free(pReturns);
```