trader.reporting.returns module¶
Percentage and rate-of-return math shared by the CLI and the LLM prompt.
position_return_pct is the exact function _position_lines (trader/llm/prompt.py) calls to describe a held position in the decision prompt — not a parallel reimplementation of the same formula. That is what guarantees an operator reading trader positions and the model reading the prompt see the same number for “up 7%” on the same position: the same function ran once, not two formulas that happen to agree today and can drift apart tomorrow.
Every percentage here is Decimal, rounded with an explicit quantize — never left to string formatting to round implicitly — and every function returns None exactly when its inputs cannot support a number (a zero cost basis, a hold too short to rate), never a fabricated 0. Money-adjacent zeros that were never measured are exactly the defect this project has been bitten by before; callers choose their own display fallback (a dash, or the prompt’s own 0.00% for a case that must never abort a response) rather than this module inventing one.
- trader.reporting.returns.MIN_HOLDING_DAYS_FOR_RATE = 1¶
avg_daily_return_pct returns None below this many whole days held.
A same-day or next-day round trip is exactly the case where “return divided by days held” looks authoritative and means nothing: the entire move happened inside a single session, so dividing it by “days” invents a rate the position never actually ran at. The threshold is a judgement call, not a derived value — 1 whole day is the smallest one for which “per day” describes more than the single day the position opened on.
- trader.reporting.returns.PERCENT_QUANTUM = Decimal('0.01')¶
Two decimal places on every displayed percentage in this module.
- trader.reporting.returns.avg_daily_return_pct(total_return_pct, days)[source]¶
Simple (not compounded) average daily return: total_return_pct / days.
Simple division over compounding ((1 + r) ** (1 / n) - 1) for two reasons. Compounding is undefined for a total return at or below -100% (1 + r <= 0), which a genuinely bad round trip can reach; simple division has no such domain restriction. And simple division is the figure an operator can verify by eye against the RETURN % column next to it — multiplying the two back together reproduces the total, which is not true of a compounded rate. Callers must label the column (e.g. AVG %/DAY (SIMPLE)) so it is never misread as annualized or compounded.
None below MIN_HOLDING_DAYS_FOR_RATE — never a rate computed from a same-day hold, which would divide by zero or invent false precision from a single session.
- Parameters:
total_return_pct (Decimal)
days (int)
- Return type:
Decimal | None
- trader.reporting.returns.cost_basis(entry_price, quantity)[source]¶
What was paid to open a position or round trip: price times shares.
- Parameters:
entry_price (Decimal)
quantity (Decimal)
- Return type:
Decimal
- trader.reporting.returns.days_held(opened_at, as_of)[source]¶
Whole days between opened_at and as_of, floored, never negative.
timedelta.days already floors towards zero for a positive delta; the max(…, 0) guards only a reversed pair or clock skew, not the ordinary case.
- Parameters:
opened_at (datetime)
as_of (datetime)
- Return type:
int
- trader.reporting.returns.position_return_pct(position)[source]¶
Unrealized return on cost basis for an open position.
Cost basis (avg_entry_price * quantity), not market value, as the denominator — the number a person means by “up 7%” is return on what was paid, and it stays stable while the market value moves. See the module docstring for why this is the one function both the CLI and the prompt call.
- Parameters:
position (Position)
- Return type:
Decimal | None
- trader.reporting.returns.return_pct(pl, basis)[source]¶
pl as a percentage of basis. None when basis is zero.
A zero cost basis is a real, if rare, broker state — a corporate action can hand out shares at avg_entry_price == 0 — and the return on nothing paid is undefined, not zero. A caller that needs a number to display (the LLM prompt, which must never raise) picks its own fallback; this function does not invent one.
- Parameters:
pl (Decimal)
basis (Decimal)
- Return type:
Decimal | None