slab-cli 0.17.0__tar.gz → 0.19.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {slab_cli-0.17.0 → slab_cli-0.19.0}/PKG-INFO +2 -2
- {slab_cli-0.17.0 → slab_cli-0.19.0}/pyproject.toml +6 -4
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/client.py +26 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/pricing.py +47 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/registry.py +2 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/display.py +139 -1
- {slab_cli-0.17.0 → slab_cli-0.19.0}/README.md +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/__init__.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/banner.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/browse.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/__init__.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/breaks.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/catalog.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/collection.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/custom_sets.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/export.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/lots.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/setup.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/commands/update.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/config.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/context.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/flags.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/help.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/main.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/paging.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/picker.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/platforms/__init__.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/platforms/base.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/platforms/posix.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/platforms/windows.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/prompts.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/sources.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/theme.py +0 -0
- {slab_cli-0.17.0 → slab_cli-0.19.0}/src/slab_cli/updates.py +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: slab-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.19.0
|
|
4
4
|
Summary: CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal.
|
|
5
5
|
Author: dev_jeb
|
|
6
|
-
Requires-Dist: slab-schemas>=0.
|
|
6
|
+
Requires-Dist: slab-schemas>=0.34.0
|
|
7
7
|
Requires-Dist: httpx>=0.27
|
|
8
8
|
Requires-Dist: rich>=13.0
|
|
9
9
|
Requires-Dist: inquirerpy>=0.3
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "slab-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.19.0"
|
|
4
4
|
description = "CLI for the slab trading-card API — catalog, collect, and track your cards from the terminal."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{name = "dev_jeb"}]
|
|
7
7
|
requires-python = ">=3.13"
|
|
8
8
|
dependencies = [
|
|
9
9
|
# 0.31: CardSearchQuery.collapse_parallels — `card search` sends it on every query.
|
|
10
|
-
# 0.32 adds CompOut.grade_unconfirmed
|
|
11
|
-
#
|
|
12
|
-
|
|
10
|
+
# 0.32 adds CompOut.grade_unconfirmed (the comps table's grade-sanity "?" marker) and the
|
|
11
|
+
# grading-desk models `slab card grade` / `slab collection grade` parse responses into.
|
|
12
|
+
# 0.34 reframes the grading desk from verdicts to break-even readings (BreakEvenRegion,
|
|
13
|
+
# GradingDesk.break_even) — the desk shows data, never tells the user to grade.
|
|
14
|
+
"slab-schemas>=0.34.0",
|
|
13
15
|
"httpx>=0.27",
|
|
14
16
|
"rich>=13.0",
|
|
15
17
|
"InquirerPy>=0.3",
|
|
@@ -49,6 +49,7 @@ from slab_schemas.custom_sets import (
|
|
|
49
49
|
CustomSetUpdate,
|
|
50
50
|
)
|
|
51
51
|
from slab_schemas.dashboard import CatalogStats, DashboardStats
|
|
52
|
+
from slab_schemas.grading_desk import CollectionGradingDesk, GradingDesk
|
|
52
53
|
from slab_schemas.pricing import CardComps, CardMarket, SetTopCards
|
|
53
54
|
from slab_schemas.sealed import SealedPriceHistory, SealedProductOut
|
|
54
55
|
from slab_schemas.timeseries import CardPriceHistory
|
|
@@ -125,6 +126,31 @@ class SlabClient:
|
|
|
125
126
|
resp = self._request("GET", f"/cards/{card_uuid}/comps", params=params)
|
|
126
127
|
return CardComps.model_validate(resp.json())
|
|
127
128
|
|
|
129
|
+
def get_grading_desk(
|
|
130
|
+
self, card_uuid: str, fee: float | None = None, company: str | None = None
|
|
131
|
+
) -> GradingDesk:
|
|
132
|
+
params: dict[str, str] = {}
|
|
133
|
+
if fee is not None:
|
|
134
|
+
params["fee"] = str(fee)
|
|
135
|
+
if company:
|
|
136
|
+
params["company"] = company
|
|
137
|
+
resp = self._request("GET", f"/cards/{card_uuid}/grading-desk", params=params)
|
|
138
|
+
return GradingDesk.model_validate(resp.json())
|
|
139
|
+
|
|
140
|
+
def get_collection_grading_desk(
|
|
141
|
+
self, collector_uuid: str, fee: float | None = None,
|
|
142
|
+
company: str | None = None, limit: int = 50,
|
|
143
|
+
) -> CollectionGradingDesk:
|
|
144
|
+
params: dict[str, str] = {"limit": str(limit)}
|
|
145
|
+
if fee is not None:
|
|
146
|
+
params["fee"] = str(fee)
|
|
147
|
+
if company:
|
|
148
|
+
params["company"] = company
|
|
149
|
+
resp = self._request(
|
|
150
|
+
"GET", f"/collectors/{collector_uuid}/collection/grading-desk", params=params
|
|
151
|
+
)
|
|
152
|
+
return CollectionGradingDesk.model_validate(resp.json())
|
|
153
|
+
|
|
128
154
|
def get_card_price_history(
|
|
129
155
|
self,
|
|
130
156
|
card_uuid: str,
|
|
@@ -11,7 +11,9 @@ from ..context import MissingCollector, ctx
|
|
|
11
11
|
from ..display import (
|
|
12
12
|
console,
|
|
13
13
|
render_card_market,
|
|
14
|
+
render_collection_grading_desk,
|
|
14
15
|
render_comps,
|
|
16
|
+
render_grading_desk,
|
|
15
17
|
render_portfolio,
|
|
16
18
|
render_price_history,
|
|
17
19
|
render_sealed_price_history,
|
|
@@ -77,6 +79,51 @@ def cmd_comps(args: list[str]) -> None:
|
|
|
77
79
|
render_comps(ctx.client.get_card_comps(card.uuid))
|
|
78
80
|
|
|
79
81
|
|
|
82
|
+
def _split_desk_flags(args: list[str]) -> tuple[list[str], float | None, str | None]:
|
|
83
|
+
"""Pull `--fee <amount>` and `--company <name>` out of a grading query. Hand-rolled (the
|
|
84
|
+
shared flag grammar is search-specific); an unparseable fee errors loudly rather than
|
|
85
|
+
silently pricing off the default."""
|
|
86
|
+
rest: list[str] = []
|
|
87
|
+
fee: float | None = None
|
|
88
|
+
company: str | None = None
|
|
89
|
+
i = 0
|
|
90
|
+
while i < len(args):
|
|
91
|
+
a = args[i]
|
|
92
|
+
if a == "--fee" and i + 1 < len(args):
|
|
93
|
+
try:
|
|
94
|
+
fee = float(args[i + 1].lstrip("$"))
|
|
95
|
+
except ValueError:
|
|
96
|
+
raise SystemExit(f"--fee expects a number, got {args[i + 1]!r}")
|
|
97
|
+
i += 2
|
|
98
|
+
elif a == "--company" and i + 1 < len(args):
|
|
99
|
+
company = args[i + 1]
|
|
100
|
+
i += 2
|
|
101
|
+
else:
|
|
102
|
+
rest.append(a)
|
|
103
|
+
i += 1
|
|
104
|
+
return rest, fee, company
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def cmd_grade(args: list[str]) -> None:
|
|
108
|
+
"""The grading math for a card. Same player → set → card flow as `price`, then the grading
|
|
109
|
+
desk: what each grade nets after the fee and where break-even sits — the data to decide with;
|
|
110
|
+
the decision is yours. `--fee 19` prices at your rate; `--company SGC` switches grader."""
|
|
111
|
+
args, fee, company = _split_desk_flags(args)
|
|
112
|
+
card = _pick_card(args)
|
|
113
|
+
if card is None:
|
|
114
|
+
return
|
|
115
|
+
render_grading_desk(ctx.client.get_grading_desk(card.uuid, fee=fee, company=company))
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def cmd_grade_sweep(args: list[str]) -> None:
|
|
119
|
+
"""The grading math across your raw cards, biggest upside first — where the per-card
|
|
120
|
+
break-even bars become batch odds. `--fee 19` prices at your rate."""
|
|
121
|
+
args, fee, company = _split_desk_flags(args)
|
|
122
|
+
render_collection_grading_desk(
|
|
123
|
+
ctx.client.get_collection_grading_desk(ctx.collector, fee=fee, company=company)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
|
|
80
127
|
def _fetch_all_copies(page: int = 200, cap: int = 5000) -> tuple[list, PortfolioSummary | None]:
|
|
81
128
|
"""Page through the whole collection for the per-set/mover/break breakdowns, and grab the
|
|
82
129
|
server's summary block (full-result aggregates, identical on every page) from the first page —
|
|
@@ -59,6 +59,7 @@ GROUPS: list[Group] = [
|
|
|
59
59
|
Command("search", "Search the catalog (player, set, team…)", catalog.cmd_search, "[query] [flags]"),
|
|
60
60
|
Command("parallels", "Every parallel (the rainbow) of a card", catalog.cmd_rainbow, "<query>"),
|
|
61
61
|
Command("price", "Market price + 7d/30d trend", pricing.cmd_price, "<query>"),
|
|
62
|
+
Command("grade", "The grading math: payoffs per grade + break-even", pricing.cmd_grade, "<query> [--fee N]"),
|
|
62
63
|
Command("comps", "Recent raw sales behind the price", pricing.cmd_comps, "<query>"),
|
|
63
64
|
Command("history", "Price history over time", pricing.cmd_history, "<query>"),
|
|
64
65
|
]),
|
|
@@ -79,6 +80,7 @@ GROUPS: list[Group] = [
|
|
|
79
80
|
Command("export", "Export a set to a printable checklist", export.cmd_export),
|
|
80
81
|
Command("dashboard", "Overview: counts, value, composition", collection.cmd_dashboard),
|
|
81
82
|
Command("portfolio", "Portfolio: cost basis vs FMV + top movers", pricing.cmd_portfolio),
|
|
83
|
+
Command("grade", "Grading math for every raw copy, best upside first", pricing.cmd_grade_sweep, "[--fee N]"),
|
|
82
84
|
]),
|
|
83
85
|
Group("break", "case / box breaks", [
|
|
84
86
|
Command("list", "Your breaks — pick one to see value vs cost + top pulls", breaks.cmd_break_list),
|
|
@@ -19,7 +19,8 @@ from slab_schemas.community import (
|
|
|
19
19
|
)
|
|
20
20
|
from slab_schemas.custom_sets import CustomSetDetail, CustomSetOut, CustomSetSearchResult
|
|
21
21
|
from slab_schemas.dashboard import CatalogStats, DashboardStats, HighlightCard, LabeledCount
|
|
22
|
-
from slab_schemas.
|
|
22
|
+
from slab_schemas.grading_desk import CollectionGradingDesk, GradingDesk
|
|
23
|
+
from slab_schemas.enums import BreakEvenRegion, Grade, SealedFormat
|
|
23
24
|
from slab_schemas.pricing import CardComps, CardMarket, PortfolioSummary, SetTopCards
|
|
24
25
|
from slab_schemas.sealed import SealedPriceHistory, SealedProductOut
|
|
25
26
|
from slab_schemas.timeseries import CardPriceHistory
|
|
@@ -1141,6 +1142,143 @@ def render_comps(c: CardComps) -> None:
|
|
|
1141
1142
|
console.print()
|
|
1142
1143
|
|
|
1143
1144
|
|
|
1145
|
+
def _desk_reading_line(d: GradingDesk) -> str:
|
|
1146
|
+
"""Where the math lands, as one plain DATA sentence — never an instruction. slab's principle:
|
|
1147
|
+
grading is subjective and the user is holding the card, so the desk states what the prices
|
|
1148
|
+
show and leaves the send/keep decision with them. Wording mirrors the `grading.break_even`
|
|
1149
|
+
glossary entry."""
|
|
1150
|
+
def payoff_of(grade_key: str | None):
|
|
1151
|
+
return next((l.payoff for l in d.lanes if l.grade_key == grade_key), None)
|
|
1152
|
+
|
|
1153
|
+
gem = next((l for l in d.lanes if l.grade_key.startswith(f"{d.grading_company}-")), None)
|
|
1154
|
+
if d.break_even == BreakEvenRegion.not_enough_data:
|
|
1155
|
+
return ("[slab.dim]No graded market to read yet — nobody has sold graded copies of this "
|
|
1156
|
+
"card recently. Not a no; an honest shrug.[/]")
|
|
1157
|
+
if d.break_even == BreakEvenRegion.pays_at_no_grade:
|
|
1158
|
+
top = f"even a {gem.grade_key} returns {signed_money(gem.payoff)}" if gem and gem.payoff is not None else "no grade covers the fee"
|
|
1159
|
+
return (f"[slab.loss]No grade covers the fee[/] — at today's prices {top} "
|
|
1160
|
+
f"after the {money(d.fee)} fee.")
|
|
1161
|
+
miss_pay = payoff_of(d.miss_grade)
|
|
1162
|
+
if d.break_even == BreakEvenRegion.pays_at_any_grade:
|
|
1163
|
+
miss_txt = (f"even a {d.miss_grade} nets {signed_money(miss_pay)}"
|
|
1164
|
+
if miss_pay is not None else "even a realistic miss nets more than staying raw")
|
|
1165
|
+
return (f"[slab.gain]Every grade pays[/] — at today's prices {miss_txt} "
|
|
1166
|
+
f"after the {money(d.fee)} fee.")
|
|
1167
|
+
pct = f"{d.ten_confidence_needed:.0%}"
|
|
1168
|
+
gem_txt = f"a {gem.grade_key} nets {signed_money(gem.payoff)}" if gem else "a gem pays"
|
|
1169
|
+
miss_txt = (f"a {d.miss_grade} comes back {signed_money(miss_pay)}"
|
|
1170
|
+
if miss_pay is not None else f"a miss holds roughly raw value")
|
|
1171
|
+
return (f"[slab.foil]Depends on the grade[/] — {gem_txt}, {miss_txt}. Break-even sits at "
|
|
1172
|
+
f"{pct}: below that chance of a gem, grading loses; above it, grading wins. "
|
|
1173
|
+
f"You can see the card; that read is yours.")
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def render_grading_desk(d: GradingDesk) -> None:
|
|
1177
|
+
"""One card's grading math: raw baseline, payoff per graded lane, and where break-even lands.
|
|
1178
|
+
Data to decide with, never a recommendation. Every sentence traces to a `grading.*` glossary
|
|
1179
|
+
entry."""
|
|
1180
|
+
subjects = ", ".join(d.subjects) or "—"
|
|
1181
|
+
slot = f"{d.subset or '—'} · {d.finish}" if d.finish else (d.subset or "base")
|
|
1182
|
+
console.print()
|
|
1183
|
+
console.print(_card_headline(d.card_number, subjects, d.set_name, slot))
|
|
1184
|
+
|
|
1185
|
+
if d.raw is None:
|
|
1186
|
+
console.print("\n [slab.dim]This card has no raw market price yet, so there is no "
|
|
1187
|
+
"baseline to grade against.[/]\n")
|
|
1188
|
+
return
|
|
1189
|
+
|
|
1190
|
+
console.print(
|
|
1191
|
+
f"\n Raw value: [slab.value]{money(d.raw.fair_market_value)}[/] "
|
|
1192
|
+
f"[slab.dim](from {d.raw.sample_size} sales) · {d.grading_company} fee assumed: "
|
|
1193
|
+
f"{money(d.fee)}"
|
|
1194
|
+
+ (" — pass --fee to use your rate" if d.fee_source == "default" else " (your rate)")
|
|
1195
|
+
+ "[/]"
|
|
1196
|
+
)
|
|
1197
|
+
|
|
1198
|
+
if d.lanes:
|
|
1199
|
+
table = slab_table([
|
|
1200
|
+
("If it grades", {}),
|
|
1201
|
+
("Sells for", {"justify": "right", "style": "slab.foil"}),
|
|
1202
|
+
("You clear", {"justify": "right"}),
|
|
1203
|
+
("Sales", {"justify": "right", "style": "slab.dim"}),
|
|
1204
|
+
])
|
|
1205
|
+
for l in d.lanes:
|
|
1206
|
+
n = f"{l.sample_size}!" if l.low_confidence else str(l.sample_size)
|
|
1207
|
+
table.add_row(l.grade_key, money(l.fair_market_value), signed_money(l.payoff), n)
|
|
1208
|
+
console.print(heading("Payoff per Grade", "after the fee, vs staying raw"))
|
|
1209
|
+
console.print(table)
|
|
1210
|
+
if any(l.low_confidence for l in d.lanes):
|
|
1211
|
+
console.print(" [slab.dim]! = priced off very few sales — an anecdote, "
|
|
1212
|
+
"not a market.[/]")
|
|
1213
|
+
|
|
1214
|
+
console.print()
|
|
1215
|
+
console.print(f" {_desk_reading_line(d)}")
|
|
1216
|
+
if d.break_even in (BreakEvenRegion.pays_at_any_grade, BreakEvenRegion.depends_on_grade) \
|
|
1217
|
+
and d.ten_confidence_needed_bad_day is not None and d.bad_day_grade:
|
|
1218
|
+
console.print(
|
|
1219
|
+
f" [slab.dim]Cautious read: if misses come back {d.bad_day_grade} instead, "
|
|
1220
|
+
f"break-even moves to {d.ten_confidence_needed_bad_day:.0%}.[/]"
|
|
1221
|
+
)
|
|
1222
|
+
if d.thin_data:
|
|
1223
|
+
console.print(" [slab.warn]Thin data[/][slab.dim] — part of this reading rests on "
|
|
1224
|
+
"1-2 sales. Treat it as a hint, not a market reading.[/]")
|
|
1225
|
+
console.print()
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
def render_collection_grading_desk(sweep: CollectionGradingDesk) -> None:
|
|
1229
|
+
"""The batch view: raw copies with their grading math, biggest upside first. Here the per-card
|
|
1230
|
+
bars become real odds — graded as a group, wins at these bars pay for the losses (the rip-EV
|
|
1231
|
+
logic). Every cell is data; which cards to send, if any, stays the user's call."""
|
|
1232
|
+
console.print()
|
|
1233
|
+
covered = f"{sweep.deskable} of your {sweep.total_raw_copies} raw copies have a graded market to read"
|
|
1234
|
+
console.print(heading("Grading Desk", covered))
|
|
1235
|
+
if not sweep.entries:
|
|
1236
|
+
console.print("\n [slab.dim]None of your raw cards have graded sales to compare "
|
|
1237
|
+
"against yet — no market to read. That's the honest answer, not an "
|
|
1238
|
+
"error.[/]\n")
|
|
1239
|
+
return
|
|
1240
|
+
|
|
1241
|
+
table = slab_table([
|
|
1242
|
+
("Card", {"no_wrap": False}),
|
|
1243
|
+
("Raw", {"justify": "right", "style": "slab.dim"}),
|
|
1244
|
+
("Gem pays", {"justify": "right"}),
|
|
1245
|
+
("Miss pays", {"justify": "right"}),
|
|
1246
|
+
("Break-even", {"justify": "right", "style": "slab.foil"}),
|
|
1247
|
+
("Qty", {"justify": "right", "style": "slab.dim"}),
|
|
1248
|
+
])
|
|
1249
|
+
BREAK_EVEN_TXT = {
|
|
1250
|
+
BreakEvenRegion.pays_at_any_grade: "[slab.gain]any grade[/]",
|
|
1251
|
+
BreakEvenRegion.pays_at_no_grade: "[slab.loss]no grade[/]",
|
|
1252
|
+
}
|
|
1253
|
+
for e in sweep.entries:
|
|
1254
|
+
d = e.desk
|
|
1255
|
+
who = ", ".join(d.subjects) or f"#{d.card_number}"
|
|
1256
|
+
card = f"{who} · {d.set_name or '—'}" + (f" · {d.finish}" if d.finish else "")
|
|
1257
|
+
gem = next((l for l in d.lanes
|
|
1258
|
+
if l.grade_key.startswith(f"{d.grading_company}-") and l.payoff is not None),
|
|
1259
|
+
None)
|
|
1260
|
+
miss = next((l for l in d.lanes if l.grade_key == d.miss_grade), None)
|
|
1261
|
+
if d.break_even == BreakEvenRegion.depends_on_grade and d.ten_confidence_needed is not None:
|
|
1262
|
+
be = f"{d.ten_confidence_needed:.0%} gem"
|
|
1263
|
+
else:
|
|
1264
|
+
be = BREAK_EVEN_TXT.get(d.break_even, d.break_even.value)
|
|
1265
|
+
thin = " [slab.warn]!" if d.thin_data else ""
|
|
1266
|
+
table.add_row(
|
|
1267
|
+
card,
|
|
1268
|
+
money(d.raw.fair_market_value) if d.raw else "—",
|
|
1269
|
+
signed_money(gem.payoff) if gem else "[slab.dim]—[/]",
|
|
1270
|
+
signed_money(miss.payoff) if miss and miss.payoff is not None else "[slab.dim]—[/]",
|
|
1271
|
+
be + thin, str(e.quantity),
|
|
1272
|
+
)
|
|
1273
|
+
console.print(table)
|
|
1274
|
+
console.print(f" [slab.dim]{sweep.fee_note}[/]")
|
|
1275
|
+
console.print(" [slab.dim]Break-even = the chance of a gem at which grading and staying raw "
|
|
1276
|
+
"pay the same — below it grading loses, above it grading wins; 'any grade' = "
|
|
1277
|
+
"even a miss pays, 'no grade' = even a gem doesn't. You can see each card's "
|
|
1278
|
+
"corners; slab can't — the call is yours. ! = rests on very few sales.[/]")
|
|
1279
|
+
console.print()
|
|
1280
|
+
|
|
1281
|
+
|
|
1144
1282
|
def _render_portfolio_by_set(by_set: list | None) -> None:
|
|
1145
1283
|
"""Per-set breakdown, most valuable first. Highlights the top set and shows comp coverage."""
|
|
1146
1284
|
if not by_set:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|