[verified] Fix bucket3 to rank by dividend yield (ignore score) — reviewer blocker
- allocate_capital _fill now takes sort_by; bucket3 uses dividend_yield, buckets 1/2 use combined_score - Previously bucket3 wrongly ranked by score (picked CPN/PTT); now picks highest-yield CRC — matches user rule 'bucket3 = highest dividend yield, ignoring score' - Added test_bucket3_ranks_by_yield_ignoring_score - Re-reviewed (deleg_43b165e0) passed=true, no logic/security errors; full suite 185 OK
This commit is contained in:
@@ -133,10 +133,16 @@ def allocate_capital(
|
|||||||
|
|
||||||
used = set()
|
used = set()
|
||||||
|
|
||||||
def _fill(bucket_idx: int, eligible: list[Candidate], require_dividend: bool):
|
def _fill(bucket_idx: int, eligible: list[Candidate], require_dividend: bool,
|
||||||
|
sort_by: str = "combined_score"):
|
||||||
nonlocal cash, used
|
nonlocal cash, used
|
||||||
remaining = cash[bucket_idx]
|
remaining = cash[bucket_idx]
|
||||||
for cand in sorted(eligible, key=lambda c: -c.combined_score):
|
# bucket 3 must rank by dividend_yield (ignoring score); others by score.
|
||||||
|
if sort_by == "dividend_yield":
|
||||||
|
key = lambda c: -c.dividend_yield
|
||||||
|
else:
|
||||||
|
key = lambda c: -c.combined_score
|
||||||
|
for cand in sorted(eligible, key=key):
|
||||||
if cand.symbol in used:
|
if cand.symbol in used:
|
||||||
continue
|
continue
|
||||||
if require_dividend and not cand.is_dividend:
|
if require_dividend and not cand.is_dividend:
|
||||||
@@ -164,8 +170,8 @@ def allocate_capital(
|
|||||||
_fill(0, [c for c in by_score if c.is_dividend], require_dividend=True)
|
_fill(0, [c for c in by_score if c.is_dividend], require_dividend=True)
|
||||||
# Bucket 2: non-dividend, highest score
|
# Bucket 2: non-dividend, highest score
|
||||||
_fill(1, [c for c in by_score if not c.is_dividend], require_dividend=False)
|
_fill(1, [c for c in by_score if not c.is_dividend], require_dividend=False)
|
||||||
# Bucket 3: highest dividend yield, excluding symbols already bought
|
# Bucket 3: highest dividend yield (ignoring score), excluding symbols bought
|
||||||
_fill(2, by_yield, require_dividend=True)
|
_fill(2, by_yield, require_dividend=True, sort_by="dividend_yield")
|
||||||
|
|
||||||
result.unallocated_cash = sum(cash)
|
result.unallocated_cash = sum(cash)
|
||||||
return result
|
return result
|
||||||
|
|||||||
@@ -50,6 +50,25 @@ class AllocationTest(unittest.TestCase):
|
|||||||
overlap = set(bucket3_syms) & set(bucket1_syms)
|
overlap = set(bucket3_syms) & set(bucket1_syms)
|
||||||
self.assertEqual(overlap, set())
|
self.assertEqual(overlap, set())
|
||||||
|
|
||||||
|
def test_bucket3_ranks_by_yield_ignoring_score(self):
|
||||||
|
# The spec: bucket3 = highest dividend yield, IGNORING score.
|
||||||
|
# A low-score but high-yield name must rank above a high-score low-yield name.
|
||||||
|
cands = [
|
||||||
|
Candidate("HIGH_SCORE", 10.0, 8.0, True, 2.0), # score 8, yield 2%
|
||||||
|
Candidate("HIGH_YIELD", 10.0, 0.1, True, 7.0), # score 0.1, yield 7%
|
||||||
|
Candidate("MID", 10.0, 5.0, True, 3.0),
|
||||||
|
]
|
||||||
|
# Big capital so bucket1 consumes only the top score name, leaving
|
||||||
|
# HIGH_YIELD (not HIGH_SCORE) to be the bucket3 top pick.
|
||||||
|
res = allocate_capital(1_000_000, cands)
|
||||||
|
b3 = [o.symbol for o in res.orders if o.bucket == 3]
|
||||||
|
# HIGH_YIELD (7%) should be selected in bucket3 before HIGH_SCORE (2%)
|
||||||
|
self.assertIn("HIGH_YIELD", b3)
|
||||||
|
if "HIGH_SCORE" in b3:
|
||||||
|
hi = b3.index("HIGH_SCORE")
|
||||||
|
hy = b3.index("HIGH_YIELD")
|
||||||
|
self.assertLess(hy, hi)
|
||||||
|
|
||||||
def test_invalid_capital_raises(self):
|
def test_invalid_capital_raises(self):
|
||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
allocate_capital(0, self.make_candidates())
|
allocate_capital(0, self.make_candidates())
|
||||||
|
|||||||
Reference in New Issue
Block a user