Skip to content

Commit 23ac116

Browse files
authored
Merge pull request #494 from woleigegg/fix/compiled-plan-review-followup-pr
fix(indicator): compiled factor plan review follow-ups (#493)
2 parents dfa2392 + 1dd4938 commit 23ac116

9 files changed

Lines changed: 97 additions & 11 deletions

File tree

hikyuu_cpp/hikyuu/factor/FactorSet.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,11 @@ vector<IndicatorList> FactorSet::getValues(const StockList& stocks, const KQuery
240240
return executor.executeValues(kdata);
241241
};
242242

243+
// 这里直接向全局线程池 submit 范围任务,且有外层调用者自身就在 work 线程里 submit
244+
// 本函数时(嵌套调用)的可能。`wait_for_all_non_blocking` 必须支持在等待期间
245+
// work-steal 已提交的子任务(否则当池被外层任务占满时会死锁:外层等待本任务、
246+
// 本任务等待子任务、却没有空闲 worker 去执行子任务)。修改这一段前请确认
247+
// `GlobalStealThreadPool` 的 non_blocking 等待确有 steal 语义。
243248
auto* task_group = get_global_task_group();
244249
HKU_ASSERT(task_group);
245250
auto ranges = parallelIndexRange(0, stk_total, task_group->worker_num());

hikyuu_cpp/hikyuu/factor/imp/CompiledFactorPlan.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/*
22
* Copyright (c) 2026 hikyuu.org
33
*
4+
* Created on: 2026-07-15
5+
* Author: woleigegg
6+
*
47
* Internal compiled execution plan for stock-local factor formulas.
58
*/
69

hikyuu_cpp/hikyuu/factor/imp/CompiledFactorPlan.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
/*
22
* Copyright (c) 2026 hikyuu.org
33
*
4+
* Created on: 2026-07-15
5+
* Author: woleigegg
6+
*
47
* Internal compiled execution plan for stock-local factor formulas.
58
*/
69

hikyuu_cpp/hikyuu/indicator/IndicatorImp.cpp

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,14 @@ Indicator IndicatorImp::calculate() {
927927
_calculate(Indicator());
928928
}
929929
} else {
930+
// 动态周期叶子没有右子节点驱动 buffer 定长。当执行器跨股票重绑 context 时,
931+
// 空输入会让 _dyn_calculate 在 total == 0 处提前返回而完全不触碰 buffer,
932+
// 导致上一只股票的数据与长度残留在 buffer 中(脏缓冲区)。这里在调用前按
933+
// 当前 context 长度预尺寸并 null 填充 buffer,使得即便 _dyn_calculate 什么都不写,
934+
// size()/data() 也能返回正确(可能更短)的长度。
935+
if (isNeedContext()) {
936+
_readyBuffer(getContext().size(), m_result_num);
937+
}
930938
_dyn_calculate(Indicator());
931939
}
932940
break;
@@ -1977,18 +1985,20 @@ bool IndicatorImp::alike(const IndicatorImp &other) const {
19771985
m_ind_params.size() != other.m_ind_params.size() || m_params != other.m_params,
19781986
false);
19791987

1980-
if (needSelfAlikeCompare()) {
1981-
HKU_IF_RETURN(!selfAlike(other), false);
1982-
HKU_IF_RETURN(isLeaf(), true);
1983-
}
1984-
19851988
auto iter1 = m_ind_params.cbegin();
19861989
auto iter2 = other.m_ind_params.cbegin();
19871990
for (; iter1 != m_ind_params.cend() && iter2 != other.m_ind_params.cend(); ++iter1, ++iter2) {
19881991
HKU_IF_RETURN(iter1->first != iter2->first, false);
19891992
HKU_IF_RETURN(!iter1->second->alike(*(iter2->second)), false);
19901993
}
19911994

1995+
if (needSelfAlikeCompare()) {
1996+
HKU_IF_RETURN(!selfAlike(other), false);
1997+
// Special leaves use structural identity so an unevaluated operator template (such as
1998+
// CVAL(value)) can match a calculated input without comparing runtime buffers.
1999+
HKU_IF_RETURN(isLeaf(), true);
2000+
}
2001+
19922002
if (isLeaf() && other.isLeaf()) {
19932003
HKU_IF_RETURN(this->size() != other.size(), false);
19942004
auto const *d1 = this->data();

hikyuu_cpp/hikyuu/indicator/IndicatorImp.h

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,16 @@ class HKU_API IndicatorImp : public enable_shared_from_this<IndicatorImp> {
152152
bool isPythonObject() const noexcept;
153153

154154
/**
155-
* Whether this implementation can be fully recalculated on a reusable batch executor.
156-
157-
* * Custom C++ indicators that retain state outside IndicatorImp buffers should opt out.
158-
*/
155+
* 该实现是否可在可复用的批处理执行器上被反复重算。
156+
*
157+
* 默认放行(非 Python 实现、且未显式置 `_support_batch_reuse=false` 都视为可复用)。
158+
* 参与复用意味着同一节点的计算图会被 `CompiledFactorPlan` 跨股票反复重绑 context
159+
* 并重算,因此自定义 C++ 指标必须满足:除 IndicatorImp 自身的 result buffer 与
160+
* `m_params/m_ind_params` 外,不持有任何跨股票会残留的成员状态(如缓存统计量、可变
161+
* 缓冲、上次输入依赖的中间结果等)。凡是会在 `_calculate`/`_dyn_calculate` 外部保留
162+
* 上述状态、或无法通过 `scrubTemplateNode` 重置干净的实现,都应在构造时调用
163+
* `supportBatchReuse(false)` 主动退出快速路径,回退到旧行为。
164+
*/
159165
bool supportBatchReuse() const;
160166

161167
void supportBatchReuse(bool enable);

hikyuu_cpp/hikyuu/indicator/imp/IFactor.cpp

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@ namespace hku {
1515

1616
IFactor::IFactor() : IndicatorImp("FACTOR", 1) {
1717
m_need_context = true;
18+
m_need_self_alike_compare = true;
1819
}
1920

2021
IFactor::IFactor(const Factor& factor) : IndicatorImp("FACTOR", 1), m_factor(factor) {
2122
m_need_context = true;
23+
m_need_self_alike_compare = true;
2224
}
2325

2426
IFactor::~IFactor() {}
@@ -32,9 +34,15 @@ IndicatorImpPtr IFactor::_clone() {
3234
}
3335

3436
bool IFactor::selfAlike(const IndicatorImp& other) const noexcept {
37+
// Factor 以“名字 + K线类型”作为唯一标识(见 Factor.h 文档),故意不比较公式内容。
38+
// 因此两个 FACTOR 节点仅在名字和 K线类型都相同时,才会被 CompiledFactorPlan
39+
// 当作同一节点做 CSE 合并。上游 FactorSet::add 已校验 K线类型并按名字去重
40+
//(后加入者覆盖先加入者),所以结构良好的 FactorSet 不会把“标识相同但公式不同”
41+
// 的两个因子同时喂进 compiled 路径。dynamic_cast 保证非 IFactor 节点不会判等。
3542
const auto* other_ctx = dynamic_cast<const IFactor*>(&other);
3643
HKU_IF_RETURN(other_ctx == nullptr, false);
37-
return m_factor.name() == other_ctx->m_factor.name();
44+
return m_factor.name() == other_ctx->m_factor.name() &&
45+
m_factor.ktype() == other_ctx->m_factor.ktype();
3846
}
3947

4048
void IFactor::_calculate(const Indicator& data) {

hikyuu_cpp/unit_test/hikyuu/factor/test_FactorSet.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <hikyuu/indicator/crt/CORR.h>
1717
#include <hikyuu/indicator/crt/CVAL.h>
1818
#include <hikyuu/indicator/crt/EMA.h>
19+
#include <hikyuu/indicator/crt/FACTOR.h>
1920
#include <hikyuu/indicator/crt/MA.h>
2021
#include <hikyuu/indicator/crt/KDATA.h>
2122
#include <hikyuu/indicator/crt/REF.h>
@@ -1342,6 +1343,36 @@ TEST_CASE("test_FactorSet_compiled_values_match_legacy") {
13421343
}
13431344
}
13441345

1346+
TEST_CASE("test_FactorSet_compiled_values_keep_nested_factors_distinct") {
1347+
Stock stock = StockManager::instance().getStock("sh000001");
1348+
REQUIRE_FALSE(stock.isNull());
1349+
1350+
Factor close_ma5("INNER_CLOSE_MA5", MA(CLOSE(), 5), KQuery::DAY);
1351+
Factor high_ma3("INNER_HIGH_MA3", MA(HIGH(), 3), KQuery::DAY);
1352+
FactorSet factorset("NESTED_FACTORS", KQuery::DAY);
1353+
factorset.add("OUTER_CLOSE_MA5", FACTOR(close_ma5));
1354+
factorset.add("OUTER_HIGH_MA3", FACTOR(high_ma3));
1355+
1356+
KQuery query(0, 30, KQuery::DAY);
1357+
KData kdata = stock.getKData(query);
1358+
REQUIRE_FALSE(kdata.empty());
1359+
1360+
auto compiled = factorset.getValues({stock}, query, false, false, true);
1361+
auto legacy = factorset.getValues({stock}, query, false, false, false);
1362+
REQUIRE_EQ(compiled.size(), 1);
1363+
REQUIRE_EQ(compiled[0].size(), 2);
1364+
REQUIRE_EQ(legacy.size(), 1);
1365+
REQUIRE_EQ(legacy[0].size(), 2);
1366+
1367+
Indicator expected_close_ma5 = FACTOR(close_ma5)(kdata).getResult(0);
1368+
Indicator expected_high_ma3 = FACTOR(high_ma3)(kdata).getResult(0);
1369+
check_indicator(compiled[0][0], expected_close_ma5);
1370+
check_indicator(compiled[0][1], expected_high_ma3);
1371+
check_indicator(legacy[0][0].getResult(0), expected_close_ma5);
1372+
check_indicator(legacy[0][1].getResult(0), expected_high_ma3);
1373+
CHECK_FALSE(compiled[0][0].equal(compiled[0][1]));
1374+
}
1375+
13451376
TEST_CASE("test_FactorSet_formula_results_keep_independent_graphs") {
13461377
Stock stock = StockManager::instance().getStock("sh000001");
13471378
REQUIRE_FALSE(stock.isNull());

hikyuu_cpp/unit_test/hikyuu/indicator/test_FACTOR.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,17 @@ TEST_CASE("test_FACTOR_different_factors") {
9292
check_indicator(result2, ma3_result);
9393
}
9494

95+
TEST_CASE("test_FACTOR_alike_uses_factor_identity") {
96+
Factor close_ma5("CLOSE_MA5", MA(CLOSE(), 5), KQuery::DAY);
97+
Factor high_ma3("HIGH_MA3", MA(HIGH(), 3), KQuery::DAY);
98+
Factor same_identity_different_formula("CLOSE_MA5", MA(HIGH(), 3), KQuery::DAY);
99+
Factor same_name_different_ktype("CLOSE_MA5", MA(CLOSE(), 5), KQuery::WEEK);
100+
101+
CHECK_FALSE(FACTOR(close_ma5).alike(FACTOR(high_ma3)));
102+
CHECK_UNARY(FACTOR(close_ma5).alike(FACTOR(same_identity_different_formula)));
103+
CHECK_FALSE(FACTOR(close_ma5).alike(FACTOR(same_name_different_ktype)));
104+
}
105+
95106
//-----------------------------------------------------------------------------
96107
// test export
97108
//-----------------------------------------------------------------------------
@@ -136,4 +147,4 @@ TEST_CASE("test_FACTOR_export") {
136147
}
137148
#endif /* #if HKU_SUPPORT_SERIALIZATION */
138149

139-
/** @} */
150+
/** @} */

hikyuu_cpp/unit_test/hikyuu/indicator/test_Indicator.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ TEST_CASE("test_indicator_alike_dynamic_parameters") {
7171

7272
CHECK_FALSE(corr_close.alike(corr_high));
7373
CHECK_UNARY(corr_close.alike(corr_close_copy));
74+
75+
Indicator corr_open_template = CORR(OPEN(), 10);
76+
Indicator corr_high_template = CORR(HIGH(), 10);
77+
Indicator corr_open_template_copy = CORR(OPEN(), 10);
78+
79+
CHECK_FALSE(corr_open_template.alike(corr_high_template));
80+
CHECK_UNARY(corr_open_template.alike(corr_open_template_copy));
81+
82+
CHECK_FALSE(DROPNA(CLOSE()).alike(DROPNA(CLOSE())));
7483
}
7584

7685
/** @par 检测点 */

0 commit comments

Comments
 (0)