From bfa0794d312a882ec5dc138a3cc9d55b2f5d6188 Mon Sep 17 00:00:00 2001 From: feedseawave Date: Wed, 31 Dec 2025 01:25:59 +0800 Subject: [PATCH 1/5] refactor: implement deterministic budget allocation in SoftTopkStrategy --- qlib/contrib/strategy/cost_control.py | 152 ++++++++++++++------------ tests/test_soft_topk_strategy.py | 51 +++++++++ 2 files changed, 136 insertions(+), 67 deletions(-) create mode 100644 tests/test_soft_topk_strategy.py diff --git a/qlib/contrib/strategy/cost_control.py b/qlib/contrib/strategy/cost_control.py index ff51f484f54..e12c165b749 100644 --- a/qlib/contrib/strategy/cost_control.py +++ b/qlib/contrib/strategy/cost_control.py @@ -1,101 +1,119 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -""" -This strategy is not well maintained -""" - +import numpy as np +import pandas as pd from .order_generator import OrderGenWInteract from .signal_strategy import WeightStrategyBase -import copy - class SoftTopkStrategy(WeightStrategyBase): def __init__( self, - model, - dataset, - topk, + model=None, + dataset=None, + topk=None, order_generator_cls_or_obj=OrderGenWInteract, max_sold_weight=1.0, + trade_impact_limit=None, + priority="IMPACT_FIRST", risk_degree=0.95, buy_method="first_fill", - trade_exchange=None, - level_infra=None, - common_infra=None, **kwargs, ): """ + Refactored SoftTopkStrategy with a budget-constrained rebalancing engine. + Parameters ---------- topk : int - top-N stocks to buy + The number of top-N stocks to be held in the portfolio. + trade_impact_limit : float + Maximum weight change for each stock in one trade. + priority : str + "COMPLIANCE_FIRST" or "IMPACT_FIRST". risk_degree : float - position percentage of total value buy_method: - - rank_fill: assign the weight stocks that rank high first(1/topk max) - average_fill: assign the weight to the stocks rank high averagely. + The target percentage of total value to be invested. """ super(SoftTopkStrategy, self).__init__( - model, dataset, order_generator_cls_or_obj, trade_exchange, level_infra, common_infra, **kwargs + model=model, + dataset=dataset, + order_generator_cls_or_obj=order_generator_cls_or_obj, + **kwargs ) + self.topk = topk - self.max_sold_weight = max_sold_weight + self.trade_impact_limit = trade_impact_limit if trade_impact_limit is not None else max_sold_weight + self.priority = priority.upper() self.risk_degree = risk_degree self.buy_method = buy_method def get_risk_degree(self, trade_step=None): - """get_risk_degree - Return the proportion of your total value you will used in investment. - Dynamically risk_degree will result in Market timing - """ - # It will use 95% amount of your total value by default return self.risk_degree - def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time): + def generate_target_weight_position(self, score, current, trade_start_time, trade_end_time, **kwargs): + """ + Generates target position using Proportional Budget Allocation. + Ensures deterministic sells and synchronized buys under impact limits. """ - Parameters - ---------- - score: - pred score for this trade date, pd.Series, index is stock_id, contain 'score' column - current: - current position, use Position() class - trade_date: - trade date - generate target position from score for this date and the current position + if self.topk is None or self.topk <= 0: + return {} - The cache is not considered in the position - """ - # TODO: - # If the current stock list is more than topk(eg. The weights are modified - # by risk control), the weight will not be handled correctly. - buy_signal_stocks = set(score.sort_values(ascending=False).iloc[: self.topk].index) - cur_stock_weight = current.get_stock_weight_dict(only_stock=True) + ideal_per_stock = self.risk_degree / self.topk + ideal_list = score.sort_values(ascending=False).iloc[:self.topk].index.tolist() + + cur_weights = current.get_stock_weight_dict(only_stock=True) + initial_total_weight = sum(cur_weights.values()) + + # --- Case A: Cold Start --- + if not cur_weights: + fill = ideal_per_stock if self.priority == "COMPLIANCE_FIRST" else min(ideal_per_stock, self.trade_impact_limit) + return {code: fill for code in ideal_list} + + # --- Case B: Rebalancing --- + all_tickers = set(cur_weights.keys()) | set(ideal_list) + next_weights = {t: cur_weights.get(t, 0.0) for t in all_tickers} + + # Phase 1: Deterministic Sell Phase + released_cash = 0.0 + for t in list(next_weights.keys()): + cur = next_weights[t] + if cur <= 1e-8: continue + + if t not in ideal_list: + sell = cur if self.priority == "COMPLIANCE_FIRST" else min(cur, self.trade_impact_limit) + next_weights[t] -= sell + released_cash += sell + elif cur > ideal_per_stock + 1e-8: + excess = cur - ideal_per_stock + sell = excess if self.priority == "COMPLIANCE_FIRST" else min(excess, self.trade_impact_limit) + next_weights[t] -= sell + released_cash += sell + + # Phase 2: Budget Calculation + # Budget = Cash from sells + Available space from target risk degree + total_budget = released_cash + (self.risk_degree - initial_total_weight) + + # Phase 3: Proportional Buy Allocation + if total_budget > 1e-8: + shortfalls = { + t: (ideal_per_stock - next_weights.get(t, 0.0)) + for t in ideal_list + if next_weights.get(t, 0.0) < ideal_per_stock - 1e-8 + } + + if shortfalls: + total_shortfall = sum(shortfalls.values()) + # Normalize total_budget to not exceed total_shortfall + available_to_spend = min(total_budget, total_shortfall) + + for t, shortfall in shortfalls.items(): + # Every stock gets its fair share based on its distance to target + share_of_budget = (shortfall / total_shortfall) * available_to_spend + + # Capped by impact limit or compliance priority + max_buy_cap = shortfall if self.priority == "COMPLIANCE_FIRST" else min(shortfall, self.trade_impact_limit) + + next_weights[t] += min(share_of_budget, max_buy_cap) - if len(cur_stock_weight) == 0: - final_stock_weight = {code: 1 / self.topk for code in buy_signal_stocks} - else: - final_stock_weight = copy.deepcopy(cur_stock_weight) - sold_stock_weight = 0.0 - for stock_id in final_stock_weight: - if stock_id not in buy_signal_stocks: - sw = min(self.max_sold_weight, final_stock_weight[stock_id]) - sold_stock_weight += sw - final_stock_weight[stock_id] -= sw - if self.buy_method == "first_fill": - for stock_id in buy_signal_stocks: - add_weight = min( - max(1 / self.topk - final_stock_weight.get(stock_id, 0), 0.0), - sold_stock_weight, - ) - final_stock_weight[stock_id] = final_stock_weight.get(stock_id, 0.0) + add_weight - sold_stock_weight -= add_weight - elif self.buy_method == "average_fill": - for stock_id in buy_signal_stocks: - final_stock_weight[stock_id] = final_stock_weight.get(stock_id, 0.0) + sold_stock_weight / len( - buy_signal_stocks - ) - else: - raise ValueError("Buy method not found") - return final_stock_weight + return {k: v for k, v in next_weights.items() if v > 1e-8} \ No newline at end of file diff --git a/tests/test_soft_topk_strategy.py b/tests/test_soft_topk_strategy.py new file mode 100644 index 00000000000..199cca29982 --- /dev/null +++ b/tests/test_soft_topk_strategy.py @@ -0,0 +1,51 @@ +import pandas as pd +import pytest +from qlib.contrib.strategy.cost_control import SoftTopkStrategy + +class MockPosition: + def __init__(self, weights): self.weights = weights + def get_stock_weight_dict(self, only_stock=True): return self.weights + +def test_soft_topk_logic(): + # Initial: A=0.8, B=0.2 (Total=1.0). Target Risk=0.95. + # Scores: A and B are low, C and D are topk. + scores = pd.Series({"C": 0.9, "D": 0.8, "A": 0.1, "B": 0.1}) + current_pos = MockPosition({"A": 0.8, "B": 0.2}) + + topk = 2 + risk_degree = 0.95 + impact_limit = 0.1 # Max change per step + + def create_test_strategy(priority): + strat = SoftTopkStrategy.__new__(SoftTopkStrategy) + strat.topk = topk + strat.risk_degree = risk_degree + strat.trade_impact_limit = impact_limit + strat.priority = priority.upper() + return strat + + # 1. Test IMPACT_FIRST: Expect deterministic sell and limited buy + strat_i = create_test_strategy("IMPACT_FIRST") + res_i = strat_i.generate_target_weight_position(scores, current_pos,None,None) + + # A should be exactly 0.8 - 0.1 = 0.7 + assert abs(res_i["A"] - 0.7) < 1e-8 + # B should be exactly 0.2 - 0.1 = 0.1 + assert abs(res_i["B"] - 0.1) < 1e-8 + # Total sells = 0.2 released. New budget = 0.2 + (0.95 - 1.0) = 0.15. + # C and D share 0.15 -> 0.075 each. + assert abs(res_i["C"] - 0.075) < 1e-8 + assert abs(res_i["D"] - 0.075) < 1e-8 + + # 2. Test COMPLIANCE_FIRST: Expect full liquidation and full target fill + strat_c = create_test_strategy("COMPLIANCE_FIRST") + res_c = strat_c.generate_target_weight_position(scores, current_pos,None,None) + + # A, B not in topk -> Liquidated + assert "A" not in res_c and "B" not in res_c + # C, D should reach ideal_per_stock (0.95/2 = 0.475) + assert abs(res_c["C"] - 0.475) < 1e-8 + assert abs(res_c["D"] - 0.475) < 1e-8 + +if __name__ == "__main__": + pytest.main([__file__]) \ No newline at end of file From 169ca63a85e30eb39cbb91c7ace5399678a96a0f Mon Sep 17 00:00:00 2001 From: feedseawave Date: Sun, 4 Jan 2026 14:09:44 +0800 Subject: [PATCH 2/5] style: fix formatting issues using black --- qlib/contrib/strategy/cost_control.py | 47 +++++++++++++++------------ tests/test_soft_topk_strategy.py | 26 +++++++++------ 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/qlib/contrib/strategy/cost_control.py b/qlib/contrib/strategy/cost_control.py index e12c165b749..1b2ed53bd51 100644 --- a/qlib/contrib/strategy/cost_control.py +++ b/qlib/contrib/strategy/cost_control.py @@ -6,6 +6,7 @@ from .order_generator import OrderGenWInteract from .signal_strategy import WeightStrategyBase + class SoftTopkStrategy(WeightStrategyBase): def __init__( self, @@ -22,25 +23,22 @@ def __init__( ): """ Refactored SoftTopkStrategy with a budget-constrained rebalancing engine. - + Parameters ---------- topk : int The number of top-N stocks to be held in the portfolio. trade_impact_limit : float - Maximum weight change for each stock in one trade. + Maximum weight change for each stock in one trade. priority : str "COMPLIANCE_FIRST" or "IMPACT_FIRST". risk_degree : float The target percentage of total value to be invested. """ super(SoftTopkStrategy, self).__init__( - model=model, - dataset=dataset, - order_generator_cls_or_obj=order_generator_cls_or_obj, - **kwargs + model=model, dataset=dataset, order_generator_cls_or_obj=order_generator_cls_or_obj, **kwargs ) - + self.topk = topk self.trade_impact_limit = trade_impact_limit if trade_impact_limit is not None else max_sold_weight self.priority = priority.upper() @@ -60,26 +58,31 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad return {} ideal_per_stock = self.risk_degree / self.topk - ideal_list = score.sort_values(ascending=False).iloc[:self.topk].index.tolist() - + ideal_list = score.sort_values(ascending=False).iloc[: self.topk].index.tolist() + cur_weights = current.get_stock_weight_dict(only_stock=True) initial_total_weight = sum(cur_weights.values()) - + # --- Case A: Cold Start --- if not cur_weights: - fill = ideal_per_stock if self.priority == "COMPLIANCE_FIRST" else min(ideal_per_stock, self.trade_impact_limit) + fill = ( + ideal_per_stock + if self.priority == "COMPLIANCE_FIRST" + else min(ideal_per_stock, self.trade_impact_limit) + ) return {code: fill for code in ideal_list} # --- Case B: Rebalancing --- all_tickers = set(cur_weights.keys()) | set(ideal_list) next_weights = {t: cur_weights.get(t, 0.0) for t in all_tickers} - + # Phase 1: Deterministic Sell Phase released_cash = 0.0 for t in list(next_weights.keys()): cur = next_weights[t] - if cur <= 1e-8: continue - + if cur <= 1e-8: + continue + if t not in ideal_list: sell = cur if self.priority == "COMPLIANCE_FIRST" else min(cur, self.trade_impact_limit) next_weights[t] -= sell @@ -93,7 +96,7 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad # Phase 2: Budget Calculation # Budget = Cash from sells + Available space from target risk degree total_budget = released_cash + (self.risk_degree - initial_total_weight) - + # Phase 3: Proportional Buy Allocation if total_budget > 1e-8: shortfalls = { @@ -101,19 +104,21 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad for t in ideal_list if next_weights.get(t, 0.0) < ideal_per_stock - 1e-8 } - + if shortfalls: total_shortfall = sum(shortfalls.values()) # Normalize total_budget to not exceed total_shortfall available_to_spend = min(total_budget, total_shortfall) - + for t, shortfall in shortfalls.items(): # Every stock gets its fair share based on its distance to target share_of_budget = (shortfall / total_shortfall) * available_to_spend - + # Capped by impact limit or compliance priority - max_buy_cap = shortfall if self.priority == "COMPLIANCE_FIRST" else min(shortfall, self.trade_impact_limit) - + max_buy_cap = ( + shortfall if self.priority == "COMPLIANCE_FIRST" else min(shortfall, self.trade_impact_limit) + ) + next_weights[t] += min(share_of_budget, max_buy_cap) - return {k: v for k, v in next_weights.items() if v > 1e-8} \ No newline at end of file + return {k: v for k, v in next_weights.items() if v > 1e-8} diff --git a/tests/test_soft_topk_strategy.py b/tests/test_soft_topk_strategy.py index 199cca29982..34a4bbd6dad 100644 --- a/tests/test_soft_topk_strategy.py +++ b/tests/test_soft_topk_strategy.py @@ -2,20 +2,25 @@ import pytest from qlib.contrib.strategy.cost_control import SoftTopkStrategy + class MockPosition: - def __init__(self, weights): self.weights = weights - def get_stock_weight_dict(self, only_stock=True): return self.weights + def __init__(self, weights): + self.weights = weights + + def get_stock_weight_dict(self, only_stock=True): + return self.weights + def test_soft_topk_logic(): # Initial: A=0.8, B=0.2 (Total=1.0). Target Risk=0.95. # Scores: A and B are low, C and D are topk. scores = pd.Series({"C": 0.9, "D": 0.8, "A": 0.1, "B": 0.1}) current_pos = MockPosition({"A": 0.8, "B": 0.2}) - + topk = 2 risk_degree = 0.95 - impact_limit = 0.1 # Max change per step - + impact_limit = 0.1 # Max change per step + def create_test_strategy(priority): strat = SoftTopkStrategy.__new__(SoftTopkStrategy) strat.topk = topk @@ -26,8 +31,8 @@ def create_test_strategy(priority): # 1. Test IMPACT_FIRST: Expect deterministic sell and limited buy strat_i = create_test_strategy("IMPACT_FIRST") - res_i = strat_i.generate_target_weight_position(scores, current_pos,None,None) - + res_i = strat_i.generate_target_weight_position(scores, current_pos, None, None) + # A should be exactly 0.8 - 0.1 = 0.7 assert abs(res_i["A"] - 0.7) < 1e-8 # B should be exactly 0.2 - 0.1 = 0.1 @@ -39,13 +44,14 @@ def create_test_strategy(priority): # 2. Test COMPLIANCE_FIRST: Expect full liquidation and full target fill strat_c = create_test_strategy("COMPLIANCE_FIRST") - res_c = strat_c.generate_target_weight_position(scores, current_pos,None,None) - + res_c = strat_c.generate_target_weight_position(scores, current_pos, None, None) + # A, B not in topk -> Liquidated assert "A" not in res_c and "B" not in res_c # C, D should reach ideal_per_stock (0.95/2 = 0.475) assert abs(res_c["C"] - 0.475) < 1e-8 assert abs(res_c["D"] - 0.475) < 1e-8 + if __name__ == "__main__": - pytest.main([__file__]) \ No newline at end of file + pytest.main([__file__]) From 6b05418b6ac72747e107b1d22b9f2eeef286d429 Mon Sep 17 00:00:00 2001 From: feedseawave Date: Sun, 4 Jan 2026 15:09:01 +0800 Subject: [PATCH 3/5] fix: remove unused imports and pass pylint --- qlib/contrib/strategy/cost_control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/qlib/contrib/strategy/cost_control.py b/qlib/contrib/strategy/cost_control.py index 1b2ed53bd51..a0be77c9755 100644 --- a/qlib/contrib/strategy/cost_control.py +++ b/qlib/contrib/strategy/cost_control.py @@ -1,8 +1,6 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -import numpy as np -import pandas as pd from .order_generator import OrderGenWInteract from .signal_strategy import WeightStrategyBase From 69ae86bdc8299c6feae6ba586e21631deb65efec Mon Sep 17 00:00:00 2001 From: feedseawave Date: Tue, 20 Jan 2026 22:52:24 +0800 Subject: [PATCH 4/5] refactor: simplify SoftTopkStrategy impact limit --- qlib/contrib/strategy/cost_control.py | 27 ++++++--------- tests/test_soft_topk_strategy.py | 13 ++++--- tests/test_soft_topk_strategy_cold_start.py | 38 +++++++++++++++++++++ 3 files changed, 55 insertions(+), 23 deletions(-) create mode 100644 tests/test_soft_topk_strategy_cold_start.py diff --git a/qlib/contrib/strategy/cost_control.py b/qlib/contrib/strategy/cost_control.py index a0be77c9755..fbeefb7b3c2 100644 --- a/qlib/contrib/strategy/cost_control.py +++ b/qlib/contrib/strategy/cost_control.py @@ -14,7 +14,6 @@ def __init__( order_generator_cls_or_obj=OrderGenWInteract, max_sold_weight=1.0, trade_impact_limit=None, - priority="IMPACT_FIRST", risk_degree=0.95, buy_method="first_fill", **kwargs, @@ -27,9 +26,9 @@ def __init__( topk : int The number of top-N stocks to be held in the portfolio. trade_impact_limit : float - Maximum weight change for each stock in one trade. - priority : str - "COMPLIANCE_FIRST" or "IMPACT_FIRST". + Maximum weight change for each stock in one trade. If None, fallback to max_sold_weight. + max_sold_weight : float + Backward-compatible alias for trade_impact_limit. Use 1.0 to effectively disable the limit. risk_degree : float The target percentage of total value to be invested. """ @@ -39,7 +38,6 @@ def __init__( self.topk = topk self.trade_impact_limit = trade_impact_limit if trade_impact_limit is not None else max_sold_weight - self.priority = priority.upper() self.risk_degree = risk_degree self.buy_method = buy_method @@ -55,6 +53,9 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad if self.topk is None or self.topk <= 0: return {} + def apply_impact_limit(weight): + return weight if self.trade_impact_limit is None else min(weight, self.trade_impact_limit) + ideal_per_stock = self.risk_degree / self.topk ideal_list = score.sort_values(ascending=False).iloc[: self.topk].index.tolist() @@ -63,11 +64,7 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad # --- Case A: Cold Start --- if not cur_weights: - fill = ( - ideal_per_stock - if self.priority == "COMPLIANCE_FIRST" - else min(ideal_per_stock, self.trade_impact_limit) - ) + fill = apply_impact_limit(ideal_per_stock) return {code: fill for code in ideal_list} # --- Case B: Rebalancing --- @@ -82,12 +79,12 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad continue if t not in ideal_list: - sell = cur if self.priority == "COMPLIANCE_FIRST" else min(cur, self.trade_impact_limit) + sell = apply_impact_limit(cur) next_weights[t] -= sell released_cash += sell elif cur > ideal_per_stock + 1e-8: excess = cur - ideal_per_stock - sell = excess if self.priority == "COMPLIANCE_FIRST" else min(excess, self.trade_impact_limit) + sell = apply_impact_limit(excess) next_weights[t] -= sell released_cash += sell @@ -112,10 +109,8 @@ def generate_target_weight_position(self, score, current, trade_start_time, trad # Every stock gets its fair share based on its distance to target share_of_budget = (shortfall / total_shortfall) * available_to_spend - # Capped by impact limit or compliance priority - max_buy_cap = ( - shortfall if self.priority == "COMPLIANCE_FIRST" else min(shortfall, self.trade_impact_limit) - ) + # Capped by impact limit + max_buy_cap = apply_impact_limit(shortfall) next_weights[t] += min(share_of_budget, max_buy_cap) diff --git a/tests/test_soft_topk_strategy.py b/tests/test_soft_topk_strategy.py index 34a4bbd6dad..0ca7f378c1e 100644 --- a/tests/test_soft_topk_strategy.py +++ b/tests/test_soft_topk_strategy.py @@ -21,16 +21,15 @@ def test_soft_topk_logic(): risk_degree = 0.95 impact_limit = 0.1 # Max change per step - def create_test_strategy(priority): + def create_test_strategy(impact_limit_value): strat = SoftTopkStrategy.__new__(SoftTopkStrategy) strat.topk = topk strat.risk_degree = risk_degree - strat.trade_impact_limit = impact_limit - strat.priority = priority.upper() + strat.trade_impact_limit = impact_limit_value return strat - # 1. Test IMPACT_FIRST: Expect deterministic sell and limited buy - strat_i = create_test_strategy("IMPACT_FIRST") + # 1. With impact limit: Expect deterministic sell and limited buy + strat_i = create_test_strategy(impact_limit) res_i = strat_i.generate_target_weight_position(scores, current_pos, None, None) # A should be exactly 0.8 - 0.1 = 0.7 @@ -42,8 +41,8 @@ def create_test_strategy(priority): assert abs(res_i["C"] - 0.075) < 1e-8 assert abs(res_i["D"] - 0.075) < 1e-8 - # 2. Test COMPLIANCE_FIRST: Expect full liquidation and full target fill - strat_c = create_test_strategy("COMPLIANCE_FIRST") + # 2. Without impact limit: Expect full liquidation and full target fill + strat_c = create_test_strategy(1.0) res_c = strat_c.generate_target_weight_position(scores, current_pos, None, None) # A, B not in topk -> Liquidated diff --git a/tests/test_soft_topk_strategy_cold_start.py b/tests/test_soft_topk_strategy_cold_start.py new file mode 100644 index 00000000000..5f7a29ac1e5 --- /dev/null +++ b/tests/test_soft_topk_strategy_cold_start.py @@ -0,0 +1,38 @@ +import pandas as pd +import pytest + +from qlib.contrib.strategy.cost_control import SoftTopkStrategy + + +class MockPosition: + def __init__(self, weights): + self.weights = weights + + def get_stock_weight_dict(self, only_stock=True): + return self.weights + + +def create_test_strategy(topk, risk_degree, impact_limit): + strat = SoftTopkStrategy.__new__(SoftTopkStrategy) + strat.topk = topk + strat.risk_degree = risk_degree + strat.trade_impact_limit = impact_limit + return strat + + +@pytest.mark.parametrize( + ("impact_limit", "expected_fill"), + [ + (0.1, 0.1), + (1.0, 0.475), + ], +) +def test_soft_topk_cold_start_impact_limit(impact_limit, expected_fill): + scores = pd.Series({"C": 0.9, "D": 0.8, "A": 0.1, "B": 0.1}) + current_pos = MockPosition({}) + + strat = create_test_strategy(topk=2, risk_degree=0.95, impact_limit=impact_limit) + res = strat.generate_target_weight_position(scores, current_pos, None, None) + + assert abs(res["C"] - expected_fill) < 1e-8 + assert abs(res["D"] - expected_fill) < 1e-8 From d681f8222a72946379e871f74fde2537fd13a3c8 Mon Sep 17 00:00:00 2001 From: feedseawave Date: Tue, 27 Jan 2026 15:08:46 +0800 Subject: [PATCH 5/5] style: relocate test files per maintainer request --- tests/{ => backtest}/test_soft_topk_strategy.py | 0 tests/{ => backtest}/test_soft_topk_strategy_cold_start.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/{ => backtest}/test_soft_topk_strategy.py (100%) rename tests/{ => backtest}/test_soft_topk_strategy_cold_start.py (100%) diff --git a/tests/test_soft_topk_strategy.py b/tests/backtest/test_soft_topk_strategy.py similarity index 100% rename from tests/test_soft_topk_strategy.py rename to tests/backtest/test_soft_topk_strategy.py diff --git a/tests/test_soft_topk_strategy_cold_start.py b/tests/backtest/test_soft_topk_strategy_cold_start.py similarity index 100% rename from tests/test_soft_topk_strategy_cold_start.py rename to tests/backtest/test_soft_topk_strategy_cold_start.py