fix(memory): take the most recent lessons by decision date (#1603)

- get_past_context orders a ticker's settled decisions by their date, so a backfill of older dates no longer pushes out the latest lesson
- rotation drops the oldest-dated settled entries, not the first written
This commit is contained in:
Yijia-Xiao
2026-10-27 07:59:51 +00:00
parent ee06dcb961
commit caf6792bba
2 changed files with 35 additions and 14 deletions
+20
View File
@@ -300,6 +300,16 @@ class TestTradingMemoryLogCore:
assert "Buy entry 0" not in ctx
assert "Buy entry 5" in ctx
def test_the_most_recent_decisions_are_the_latest_dated_not_the_latest_written(self, tmp_path):
"""A backfill writes January after June; June is still the most recent lesson."""
log = make_log(tmp_path)
_seed_completed(tmp_path, "NVDA", "2026-06-01", "Buy entry June.", "Correct.")
for i in range(5):
_seed_completed(tmp_path, "NVDA", f"2026-01-{i+1:02d}", f"Buy entry {i}.", "Correct.")
ctx = log.get_past_context("NVDA", n_same=5)
assert "Buy entry June" in ctx and "Buy entry 0" not in ctx
assert ctx.index("Buy entry June") < ctx.index("Buy entry 4")
def test_n_cross_limit_respected(self, tmp_path):
"""Only the n_cross most recent cross-ticker entries are included."""
log = make_log(tmp_path)
@@ -341,6 +351,16 @@ class TestTradingMemoryLogCore:
dates = [e["date"] for e in entries]
assert dates == ["2026-01-03", "2026-01-04", "2026-01-05"]
def test_rotation_prunes_the_oldest_dated_not_the_first_written(self, tmp_path):
"""A backfill writes older dates last; rotation still keeps the newest decisions."""
log = TradingMemoryLog({
"memory_log_path": str(tmp_path / "trading_memory.md"),
"memory_log_max_entries": 3,
})
for date in ("2026-06-01", "2026-06-02", "2026-01-01", "2026-01-02", "2026-01-03"):
_resolve_entry(log, "NVDA", date, DECISION_BUY, f"Lesson {date}.")
assert sorted(e["date"] for e in log.load_entries()) == ["2026-01-03", "2026-06-01", "2026-06-02"]
def test_rotation_never_prunes_pending(self, tmp_path):
"""Pending entries (unresolved) are kept regardless of the cap."""
log = TradingMemoryLog({
+15 -14
View File
@@ -109,7 +109,10 @@ class TradingMemoryLog:
return ""
same, cross = [], []
for e in reversed(entries):
# Most recent by decision date, then by the later write: a backfill writes
# older decisions last.
newest = sorted(range(len(entries)), key=lambda i: (entries[i]["date"], i), reverse=True)
for e in (entries[i] for i in newest):
if len(same) >= n_same and len(cross) >= n_cross:
break
if e["ticker"] == ticker and len(same) < n_same:
@@ -271,7 +274,7 @@ class TradingMemoryLog:
return tag + "]"
def _apply_rotation(self, blocks: list[str]) -> list[str]:
"""Drop oldest resolved blocks when their count exceeds max_entries.
"""Drop the oldest-dated resolved blocks when their count exceeds max_entries.
Pending blocks are always kept (they represent unprocessed work).
Returns ``blocks`` unchanged when rotation is disabled or under cap.
@@ -279,12 +282,12 @@ class TradingMemoryLog:
if not self._max_entries or self._max_entries <= 0:
return blocks
# Tag each block with (kept, is_resolved) by parsing tag-line markers.
# Tag each block with (block, is_resolved, decision date) from its tag line.
decisions = []
for block in blocks:
stripped = block.strip()
if not stripped:
decisions.append((block, False))
decisions.append((block, False, ""))
continue
tag_line = stripped.splitlines()[0].strip()
is_resolved = (
@@ -292,20 +295,18 @@ class TradingMemoryLog:
and tag_line.endswith("]")
and not tag_line.endswith("| pending]")
)
decisions.append((block, is_resolved))
decisions.append((block, is_resolved, tag_line[1:].split("|")[0].strip()))
resolved_count = sum(1 for _, r in decisions if r)
resolved_count = sum(1 for _, r, _ in decisions if r)
if resolved_count <= self._max_entries:
return blocks
to_drop = resolved_count - self._max_entries
kept: list[str] = []
for block, is_resolved in decisions:
if is_resolved and to_drop > 0:
to_drop -= 1
continue
kept.append(block)
return kept
# The oldest by decision date, then by position: a backfill writes older
# decisions last.
resolved = [i for i, (_, is_resolved, _) in enumerate(decisions) if is_resolved]
dated = sorted(resolved, key=lambda i: (decisions[i][2], i))
dropped = set(dated[:resolved_count - self._max_entries])
return [block for i, (block, _, _) in enumerate(decisions) if i not in dropped]
def _parse_entry(self, raw: str) -> dict | None:
lines = raw.strip().splitlines()