Where to park idle cash in a buy-low/sell-high position while it has no better use - scans every ticker for real bid/ask spread and liquidity, scoring round-trip (buy-dip, sell-recovery) via the same empirical first-passage model as sell.py's optimal-ask fix, instead of an assumed random walk. Dropped the universe's top pick from ~94%/day to ~1%/day with real waits of 19-194+ days under the corrected model. Default --rate 0.0 matches the tool's own idle-cash premise (sell.py's rate of 0.05 assumes cash is earmarked, which isn't this tool's case). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
48 lines
3.1 KiB
Python
48 lines
3.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Where to park idle cash as a buy-low/sell-high position while ships are busy or cash has no
|
|
better immediate use. Scans every ticker for a big spread AND real trading liquidity, scored by
|
|
an EMPIRICAL round-trip estimate from the ticker's own price history (no random-walk assumption -
|
|
see puga/parkcash.py's docstring for the method and open gaps).
|
|
|
|
tools/parkcash.py # AI1, default filters, top 25 by pessimistic daily %
|
|
tools/parkcash.py --min-trade-days 15 # require real liquidity (half the window trading)
|
|
"""
|
|
import argparse, sys
|
|
from pathlib import Path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
from puga import config, parkcash
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("--cx", default=config.DEFAULT_CX)
|
|
ap.add_argument("--min-spread", type=float, default=20.0, help="minimum bid/ask spread %% to consider")
|
|
ap.add_argument("--min-trade-days", type=int, default=10, help="minimum trading days out of the window - filters stale-quote traps")
|
|
ap.add_argument("--days", type=int, default=30, help="calendar-day window for liquidity stats (the empirical round-trip model itself uses full history)")
|
|
ap.add_argument("--rate", type=float, default=0.0, help="opportunity cost of waiting on this cash - default 0.0 matches this tool's premise (idle cash, no better use right now); pass your realized capital growth rate (e.g. 0.05, see tools/sell.py --rate) if this money IS earmarked for something and waiting has a real cost")
|
|
ap.add_argument("--max-wait", type=float, help="drop anything with a combined round-trip median wait above this many days")
|
|
ap.add_argument("--top", type=int, default=25)
|
|
a = ap.parse_args()
|
|
|
|
rows = parkcash.scan(a.cx, a.min_spread, a.days, a.rate)
|
|
rows = [r for r in rows if r["trade_days"] >= a.min_trade_days]
|
|
if a.max_wait is not None:
|
|
rows = [r for r in rows if r["wait_days"] <= a.max_wait]
|
|
rows.sort(key=lambda r: -r["daily_pct"])
|
|
|
|
print(f"# {a.cx} spread>={a.min_spread:.0f}% trade_days>={a.min_trade_days}/{a.days}d rate={100*a.rate:.1f}%/day "
|
|
f"(round trip = empirical buy-dip + sell-recovery from real history, see puga/parkcash.py)")
|
|
print(f"{'daily%':>7} {'disc%':>6} {'margin%':>8} {'spread%':>8} {'days':>5} {'buy%':>5} {'sell%':>5} {'wait(d)':>7} {'bid':>9} {'ask':>9} tk")
|
|
for r in rows[:a.top]:
|
|
print(f"{r['daily_pct']:7.1f} {r['discounted_pct']:6.1f} {r['margin_pct']:8.1f} {r['spread_pct']:8.0f} {r['trade_days']:5d} "
|
|
f"{100*r['buy_touch']:5.0f} {100*r['sell_touch']:5.0f} {r['wait_days']:7.1f} {r['bid']:9.0f} {r['ask']:9.0f} {r['tk']}")
|
|
|
|
print("\nNOTE: disc% = margin% x real odds both legs ever happen (at --rate; a big gap from daily% "
|
|
"means the speed number is flattering a trade that may never clear). buy%/sell% are each leg's "
|
|
"real historical frequency. Not a compoundable rate - see docs/decisions.md. See puga/parkcash.py's "
|
|
"OPEN GAPS (no fee data; legs treated as independent).")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|