feat: add Quant OS A-share baseline
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
import unittest
|
||||
|
||||
from quant60.portable_core import (
|
||||
build_rebalance_plan,
|
||||
capped_target_weights,
|
||||
momentum_score,
|
||||
normalize_symbol,
|
||||
order_deltas,
|
||||
target_quantities,
|
||||
)
|
||||
|
||||
|
||||
class PortableCoreTests(unittest.TestCase):
|
||||
def test_symbol_round_trip(self):
|
||||
inputs = [
|
||||
("600000.XSHG", "600000.SH", "SH600000"),
|
||||
("000001.XSHE", "000001.SZ", "SZ000001"),
|
||||
("830799.XBSE", "830799.BJ", "BJ830799"),
|
||||
]
|
||||
for canonical, qmt, qlib in inputs:
|
||||
self.assertEqual(normalize_symbol(qmt), canonical)
|
||||
self.assertEqual(normalize_symbol(qlib), canonical)
|
||||
self.assertEqual(normalize_symbol(canonical, "qmt"), qmt)
|
||||
self.assertEqual(normalize_symbol(canonical, "qlib"), qlib)
|
||||
|
||||
def test_momentum_uses_latest_completed_bar(self):
|
||||
self.assertAlmostEqual(momentum_score([10, 11, 12], 2, 0), 0.2)
|
||||
self.assertAlmostEqual(momentum_score([10, 11, 12], 1, 1), 0.1)
|
||||
|
||||
def test_caps_and_cash_buffer_are_not_double_counted(self):
|
||||
weights = capped_target_weights(
|
||||
{"600000.XSHG": 3, "000001.XSHE": 2, "300750.XSHE": 1},
|
||||
max_weight=0.4,
|
||||
gross_target=0.95,
|
||||
)
|
||||
self.assertAlmostEqual(sum(weights.values()), 0.95)
|
||||
self.assertLessEqual(max(weights.values()), 0.4 + 1e-12)
|
||||
quantities = target_quantities(
|
||||
weights,
|
||||
{symbol: 10 for symbol in weights},
|
||||
equity=1_000_000,
|
||||
lot_size=100,
|
||||
cash_buffer=0.02,
|
||||
)
|
||||
invested = sum(quantity * 10 for quantity in quantities.values())
|
||||
self.assertGreater(invested, 930_000)
|
||||
self.assertLessEqual(invested, 980_000)
|
||||
|
||||
def test_non_finite_or_out_of_range_portfolio_inputs_are_rejected(self):
|
||||
for value in (float("nan"), float("inf"), float("-inf")):
|
||||
with self.subTest(kind="score", value=value):
|
||||
with self.assertRaises(ValueError):
|
||||
capped_target_weights({"600000.SH": value})
|
||||
with self.subTest(kind="weight", value=value):
|
||||
with self.assertRaises(ValueError):
|
||||
target_quantities(
|
||||
{"600000.SH": value},
|
||||
{"600000.SH": 10.0},
|
||||
100_000,
|
||||
)
|
||||
with self.subTest(kind="equity", value=value):
|
||||
with self.assertRaises(ValueError):
|
||||
target_quantities(
|
||||
{"600000.SH": 0.5},
|
||||
{"600000.SH": 10.0},
|
||||
value,
|
||||
)
|
||||
for max_weight, gross_target in ((1.1, 0.9), (0.2, 1.1)):
|
||||
with self.subTest(max_weight=max_weight, gross_target=gross_target):
|
||||
with self.assertRaises(ValueError):
|
||||
capped_target_weights(
|
||||
{"600000.SH": 1.0},
|
||||
max_weight=max_weight,
|
||||
gross_target=gross_target,
|
||||
)
|
||||
|
||||
def test_sell_delta_respects_sellable(self):
|
||||
deltas = order_deltas(
|
||||
{"600000.XSHG": 0},
|
||||
{"600000.XSHG": 1000},
|
||||
{"600000.XSHG": 300},
|
||||
lot_size=100,
|
||||
)
|
||||
self.assertEqual(deltas, {"600000.XSHG": -300})
|
||||
|
||||
def test_star_targets_and_partial_orders_respect_200_share_minimum(self):
|
||||
self.assertEqual(
|
||||
target_quantities(
|
||||
{"688301.XSHG": 1.0},
|
||||
{"688301.XSHG": 300.0},
|
||||
equity=50_000,
|
||||
lot_size=100,
|
||||
cash_buffer=0.02,
|
||||
),
|
||||
{"688301.XSHG": 0},
|
||||
)
|
||||
self.assertEqual(
|
||||
target_quantities(
|
||||
{"688301.XSHG": 1.0},
|
||||
{"688301.XSHG": 200.0},
|
||||
equity=50_000,
|
||||
lot_size=100,
|
||||
cash_buffer=0.02,
|
||||
),
|
||||
{"688301.XSHG": 200},
|
||||
)
|
||||
self.assertEqual(
|
||||
order_deltas(
|
||||
{"688301.XSHG": 300},
|
||||
{"688301.XSHG": 200},
|
||||
{"688301.XSHG": 200},
|
||||
lot_size=100,
|
||||
),
|
||||
{},
|
||||
)
|
||||
self.assertEqual(
|
||||
order_deltas(
|
||||
{"688301.XSHG": 1300},
|
||||
{"688301.XSHG": 1400},
|
||||
{"688301.XSHG": 1400},
|
||||
lot_size=100,
|
||||
),
|
||||
{},
|
||||
)
|
||||
|
||||
def test_complete_odd_lot_liquidation_is_preserved(self):
|
||||
self.assertEqual(
|
||||
order_deltas(
|
||||
{"600000.XSHG": 0},
|
||||
{"600000.XSHG": 1050},
|
||||
{"600000.XSHG": 1050},
|
||||
lot_size=100,
|
||||
),
|
||||
{"600000.XSHG": -1050},
|
||||
)
|
||||
self.assertEqual(
|
||||
order_deltas(
|
||||
{"688301.XSHG": 0},
|
||||
{"688301.XSHG": 150},
|
||||
{"688301.XSHG": 150},
|
||||
lot_size=100,
|
||||
),
|
||||
{"688301.XSHG": -150},
|
||||
)
|
||||
|
||||
def test_complete_plan_is_deterministic(self):
|
||||
histories = {
|
||||
"600000.SH": [10 + index * 0.1 for index in range(25)],
|
||||
"000001.SZ": [10 + index * 0.05 for index in range(25)],
|
||||
}
|
||||
arguments = dict(
|
||||
price_history=histories,
|
||||
current={},
|
||||
sellable={},
|
||||
equity=100_000,
|
||||
lookback=20,
|
||||
skip=0,
|
||||
top_n=2,
|
||||
max_weight=0.5,
|
||||
gross_target=0.95,
|
||||
cash_buffer=0.02,
|
||||
lot_size=100,
|
||||
)
|
||||
self.assertEqual(
|
||||
build_rebalance_plan(**arguments),
|
||||
build_rebalance_plan(**arguments),
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user