slab-cli 0.18.0__tar.gz → 0.20.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.
Files changed (34) hide show
  1. {slab_cli-0.18.0 → slab_cli-0.20.0}/PKG-INFO +2 -2
  2. {slab_cli-0.18.0 → slab_cli-0.20.0}/pyproject.toml +5 -2
  3. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/pricing.py +5 -5
  4. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/registry.py +2 -2
  5. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/display.py +83 -42
  6. {slab_cli-0.18.0 → slab_cli-0.20.0}/README.md +0 -0
  7. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/__init__.py +0 -0
  8. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/banner.py +0 -0
  9. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/browse.py +0 -0
  10. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/client.py +0 -0
  11. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/__init__.py +0 -0
  12. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/breaks.py +0 -0
  13. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/catalog.py +0 -0
  14. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/collection.py +0 -0
  15. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/custom_sets.py +0 -0
  16. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/export.py +0 -0
  17. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/lots.py +0 -0
  18. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/setup.py +0 -0
  19. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/commands/update.py +0 -0
  20. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/config.py +0 -0
  21. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/context.py +0 -0
  22. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/flags.py +0 -0
  23. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/help.py +0 -0
  24. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/main.py +0 -0
  25. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/paging.py +0 -0
  26. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/picker.py +0 -0
  27. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/platforms/__init__.py +0 -0
  28. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/platforms/base.py +0 -0
  29. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/platforms/posix.py +0 -0
  30. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/platforms/windows.py +0 -0
  31. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/prompts.py +0 -0
  32. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/sources.py +0 -0
  33. {slab_cli-0.18.0 → slab_cli-0.20.0}/src/slab_cli/theme.py +0 -0
  34. {slab_cli-0.18.0 → slab_cli-0.20.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.18.0
3
+ Version: 0.20.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.32.0
6
+ Requires-Dist: slab-schemas>=0.35.0
7
7
  Requires-Dist: httpx>=0.27
8
8
  Requires-Dist: rich>=13.0
9
9
  Requires-Dist: inquirerpy>=0.3
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "slab-cli"
3
- version = "0.18.0"
3
+ version = "0.20.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"}]
@@ -9,7 +9,10 @@ dependencies = [
9
9
  # 0.31: CardSearchQuery.collapse_parallels — `card search` sends it on every query.
10
10
  # 0.32 adds CompOut.grade_unconfirmed (the comps table's grade-sanity "?" marker) and the
11
11
  # grading-desk models `slab card grade` / `slab collection grade` parse responses into.
12
- "slab-schemas>=0.32.0",
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
+ # 0.35 adds Liquidity/LiquidityLabel, which the market view's Sells column renders.
15
+ "slab-schemas>=0.35.0",
13
16
  "httpx>=0.27",
14
17
  "rich>=13.0",
15
18
  "InquirerPy>=0.3",
@@ -105,9 +105,9 @@ def _split_desk_flags(args: list[str]) -> tuple[list[str], float | None, str | N
105
105
 
106
106
 
107
107
  def cmd_grade(args: list[str]) -> None:
108
- """Should I grade this card? Same player → set → card flow as `price`, then the grading desk:
109
- what each grade nets after the fee, and how sure you'd need to be it gems before grading
110
- beats keeping it raw. `--fee 19` prices at your rate; `--company SGC` switches grader."""
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
111
  args, fee, company = _split_desk_flags(args)
112
112
  card = _pick_card(args)
113
113
  if card is None:
@@ -116,8 +116,8 @@ def cmd_grade(args: list[str]) -> None:
116
116
 
117
117
 
118
118
  def cmd_grade_sweep(args: list[str]) -> None:
119
- """Which of your raw cards are worth grading? Ranks every ungraded copy best-case first —
120
- where the per-card bars become batch odds. `--fee 19` prices at your rate."""
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
121
  args, fee, company = _split_desk_flags(args)
122
122
  render_collection_grading_desk(
123
123
  ctx.client.get_collection_grading_desk(ctx.collector, fee=fee, company=company)
@@ -59,7 +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", "Should I grade it? Payoffs, the bar, a verdict", pricing.cmd_grade, "<query> [--fee N]"),
62
+ Command("grade", "The grading math: payoffs per grade + break-even", pricing.cmd_grade, "<query> [--fee N]"),
63
63
  Command("comps", "Recent raw sales behind the price", pricing.cmd_comps, "<query>"),
64
64
  Command("history", "Price history over time", pricing.cmd_history, "<query>"),
65
65
  ]),
@@ -80,7 +80,7 @@ GROUPS: list[Group] = [
80
80
  Command("export", "Export a set to a printable checklist", export.cmd_export),
81
81
  Command("dashboard", "Overview: counts, value, composition", collection.cmd_dashboard),
82
82
  Command("portfolio", "Portfolio: cost basis vs FMV + top movers", pricing.cmd_portfolio),
83
- Command("grade", "Which of your raw cards are worth grading", pricing.cmd_grade_sweep, "[--fee N]"),
83
+ Command("grade", "Grading math for every raw copy, best upside first", pricing.cmd_grade_sweep, "[--fee N]"),
84
84
  ]),
85
85
  Group("break", "case / box breaks", [
86
86
  Command("list", "Your breaks — pick one to see value vs cost + top pulls", breaks.cmd_break_list),
@@ -20,7 +20,8 @@ from slab_schemas.community import (
20
20
  from slab_schemas.custom_sets import CustomSetDetail, CustomSetOut, CustomSetSearchResult
21
21
  from slab_schemas.dashboard import CatalogStats, DashboardStats, HighlightCard, LabeledCount
22
22
  from slab_schemas.grading_desk import CollectionGradingDesk, GradingDesk
23
- from slab_schemas.enums import Grade, GradingVerdict, SealedFormat
23
+ from slab_schemas.enums import BreakEvenRegion, Grade, LiquidityLabel, SealedFormat
24
+ from slab_schemas.liquidity import Liquidity
24
25
  from slab_schemas.pricing import CardComps, CardMarket, PortfolioSummary, SetTopCards
25
26
  from slab_schemas.sealed import SealedPriceHistory, SealedProductOut
26
27
  from slab_schemas.timeseries import CardPriceHistory
@@ -971,7 +972,8 @@ def render_card_market(m: CardMarket) -> None:
971
972
 
972
973
  # The Δ columns are FMV (appraisal) drift, not the window's sales — FMV smooths 90 days of
973
974
  # comps, so these lag the live market (two-lane rule, CLAUDE.md). Headers say FMV so a
974
- # "+$5" cell reads as the appraisal moving, not as last week's market.
975
+ # "+$5" cell reads as the appraisal moving, not as last week's market. "Sells" is the other
976
+ # lane: real sale cadence from comps (liquidity), so the price and its pace sit side by side.
975
977
  table = slab_table([
976
978
  ("Grade", {"style": "slab.value"}),
977
979
  ("FMV", {"justify": "right"}),
@@ -979,6 +981,7 @@ def render_card_market(m: CardMarket) -> None:
979
981
  ("FMV Δ7d", {"justify": "right"}),
980
982
  ("FMV Δ30d", {"justify": "right"}),
981
983
  ("Comps", {"justify": "right", "style": "slab.dim"}),
984
+ ("Sells", {"style": "slab.dim"}),
982
985
  ])
983
986
 
984
987
  for pp in sorted(m.price_points, key=lambda p: (p.finish or "", p.grade_key)):
@@ -990,14 +993,39 @@ def render_card_market(m: CardMarket) -> None:
990
993
  if pp.price_low is not None and pp.price_high is not None:
991
994
  rng = f"${pp.price_low:,.0f} – ${pp.price_high:,.0f}"
992
995
  table.add_row(label, fmv, rng, signed_money(pp.price_change_7d),
993
- signed_money(pp.price_change_30d), str(pp.sample_size))
996
+ signed_money(pp.price_change_30d), str(pp.sample_size),
997
+ _liquidity_text(pp.liquidity))
994
998
 
995
999
  window = m.price_points[0].window_days
996
1000
  console.print(heading("Market Price", f"{window}-day window, as of {m.as_of_date}"))
997
1001
  console.print(table)
1002
+ if any(pp.liquidity for pp in m.price_points):
1003
+ console.print(
1004
+ " [slab.dim]Sells = how often this exact printing and grade actually sells — the "
1005
+ "typical gap between real sales in the last 90 days, or the past year's count for "
1006
+ "slow movers. A rare seller isn't bad; it means the price is an estimate you may "
1007
+ "wait to realize.[/]"
1008
+ )
998
1009
  console.print()
999
1010
 
1000
1011
 
1012
+ def _liquidity_text(liq: Liquidity | None) -> str:
1013
+ """A liquidity reading as one short table cell — the pace, then the datum behind it.
1014
+ Data statements only, mirroring the `liquidity.*` glossary."""
1015
+ if liq is None:
1016
+ return "—"
1017
+ L = LiquidityLabel
1018
+ if liq.median_days_between_sales is not None:
1019
+ gap = liq.median_days_between_sales
1020
+ word = {L.sells_daily: "~daily", L.sells_weekly: "~weekly"}.get(liq.label, "~monthly")
1021
+ return f"{word} (every {gap}d)" if gap > 0 else f"{word} (several/day)"
1022
+ if liq.label == L.too_new_to_say:
1023
+ return f"too new to say ({liq.observed_days}d watched)"
1024
+ if liq.label == L.no_recent_sales:
1025
+ return "none this year"
1026
+ return f"{liq.sales_365d}×/year"
1027
+
1028
+
1001
1029
  def _format_label(fmt: str) -> str:
1002
1030
  """SealedFormat value -> display label: hobby_box -> Hobby Box."""
1003
1031
  return fmt.replace("_", " ").title()
@@ -1142,36 +1170,41 @@ def render_comps(c: CardComps) -> None:
1142
1170
  console.print()
1143
1171
 
1144
1172
 
1145
- def _desk_verdict_line(d: GradingDesk) -> str:
1146
- """The verdict as one plain sentence — the confidence-BAR framing (you grade a card once, so
1147
- the number is how good YOUR copy has to look, never "how often"). Wording mirrors the
1148
- `grading.verdict` glossary entry."""
1173
+ def _desk_reading_line(d: GradingDesk) -> str:
1174
+ """Where the math lands, as one plain DATA sentence — never an instruction. slab's principle:
1175
+ grading is subjective and the user is holding the card, so the desk states what the prices
1176
+ show and leaves the send/keep decision with them. Wording mirrors the `grading.break_even`
1177
+ glossary entry."""
1149
1178
  def payoff_of(grade_key: str | None):
1150
1179
  return next((l.payoff for l in d.lanes if l.grade_key == grade_key), None)
1151
1180
 
1152
1181
  gem = next((l for l in d.lanes if l.grade_key.startswith(f"{d.grading_company}-")), None)
1153
- if d.verdict == GradingVerdict.not_enough_data:
1182
+ if d.break_even == BreakEvenRegion.not_enough_data:
1154
1183
  return ("[slab.dim]No graded market to read yet — nobody has sold graded copies of this "
1155
1184
  "card recently. Not a no; an honest shrug.[/]")
1156
- if d.verdict == GradingVerdict.easy_no:
1157
- top = f"even a {gem.grade_key} only returns {signed_money(gem.payoff)}" if gem and gem.payoff is not None else "no grade covers the fee"
1158
- return f"[slab.loss]Keep it raw[/] — {top} after the {money(d.fee)} fee."
1185
+ if d.break_even == BreakEvenRegion.pays_at_no_grade:
1186
+ 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"
1187
+ return (f"[slab.loss]No grade covers the fee[/] — at today's prices {top} "
1188
+ f"after the {money(d.fee)} fee.")
1159
1189
  miss_pay = payoff_of(d.miss_grade)
1160
- if d.verdict == GradingVerdict.easy_yes:
1190
+ if d.break_even == BreakEvenRegion.pays_at_any_grade:
1161
1191
  miss_txt = (f"even a {d.miss_grade} nets {signed_money(miss_pay)}"
1162
- if miss_pay is not None else "even a realistic miss beats staying raw")
1163
- return f"[slab.gain]Send it[/] — {miss_txt} after the {money(d.fee)} fee."
1192
+ if miss_pay is not None else "even a realistic miss nets more than staying raw")
1193
+ return (f"[slab.gain]Every grade pays[/] — at today's prices {miss_txt} "
1194
+ f"after the {money(d.fee)} fee.")
1164
1195
  pct = f"{d.ten_confidence_needed:.0%}"
1165
1196
  gem_txt = f"a {gem.grade_key} nets {signed_money(gem.payoff)}" if gem else "a gem pays"
1166
1197
  miss_txt = (f"a {d.miss_grade} comes back {signed_money(miss_pay)}"
1167
1198
  if miss_pay is not None else f"a miss holds roughly raw value")
1168
- return (f"[slab.foil]Your call[/] — grade it only if you're more than {pct} sure it's a gem: "
1169
- f"{gem_txt}, {miss_txt}.")
1199
+ return (f"[slab.foil]Depends on the grade[/] — {gem_txt}, {miss_txt}. Break-even sits at "
1200
+ f"{pct}: below that chance of a gem, grading loses; above it, grading wins. "
1201
+ f"You can see the card; that read is yours.")
1170
1202
 
1171
1203
 
1172
1204
  def render_grading_desk(d: GradingDesk) -> None:
1173
- """One card's grading decision: raw baseline, payoff per graded lane, and the verdict in the
1174
- bar framing. Every sentence traces to a `grading.*` glossary entry."""
1205
+ """One card's grading math: raw baseline, payoff per graded lane, and where break-even lands.
1206
+ Data to decide with, never a recommendation. Every sentence traces to a `grading.*` glossary
1207
+ entry."""
1175
1208
  subjects = ", ".join(d.subjects) or "—"
1176
1209
  slot = f"{d.subset or '—'} · {d.finish}" if d.finish else (d.subset or "base")
1177
1210
  console.print()
@@ -1196,54 +1229,60 @@ def render_grading_desk(d: GradingDesk) -> None:
1196
1229
  ("Sells for", {"justify": "right", "style": "slab.foil"}),
1197
1230
  ("You clear", {"justify": "right"}),
1198
1231
  ("Sales", {"justify": "right", "style": "slab.dim"}),
1232
+ ("Sells", {"style": "slab.dim"}),
1199
1233
  ])
1200
1234
  for l in d.lanes:
1201
1235
  n = f"{l.sample_size}!" if l.low_confidence else str(l.sample_size)
1202
- table.add_row(l.grade_key, money(l.fair_market_value), signed_money(l.payoff), n)
1236
+ table.add_row(l.grade_key, money(l.fair_market_value), signed_money(l.payoff), n,
1237
+ _liquidity_text(l.liquidity))
1203
1238
  console.print(heading("Payoff per Grade", "after the fee, vs staying raw"))
1204
1239
  console.print(table)
1205
1240
  if any(l.low_confidence for l in d.lanes):
1206
1241
  console.print(" [slab.dim]! = priced off very few sales — an anecdote, "
1207
1242
  "not a market.[/]")
1243
+ if any(l.liquidity for l in d.lanes):
1244
+ console.print(" [slab.dim]Sells = how often that grade actually trades — a payoff "
1245
+ "at a grade that sells twice a year is a longer wait than one that "
1246
+ "sells weekly.[/]")
1208
1247
 
1209
1248
  console.print()
1210
- console.print(f" {_desk_verdict_line(d)}")
1211
- if d.verdict in (GradingVerdict.easy_yes, GradingVerdict.judgment_call) \
1249
+ console.print(f" {_desk_reading_line(d)}")
1250
+ if d.break_even in (BreakEvenRegion.pays_at_any_grade, BreakEvenRegion.depends_on_grade) \
1212
1251
  and d.ten_confidence_needed_bad_day is not None and d.bad_day_grade:
1213
1252
  console.print(
1214
1253
  f" [slab.dim]Cautious read: if misses come back {d.bad_day_grade} instead, "
1215
- f"the bar is {d.ten_confidence_needed_bad_day:.0%}.[/]"
1254
+ f"break-even moves to {d.ten_confidence_needed_bad_day:.0%}.[/]"
1216
1255
  )
1217
1256
  if d.thin_data:
1218
- console.print(" [slab.warn]Thin data[/][slab.dim] — part of this verdict rests on "
1257
+ console.print(" [slab.warn]Thin data[/][slab.dim] — part of this reading rests on "
1219
1258
  "1-2 sales. Treat it as a hint, not a market reading.[/]")
1220
1259
  console.print()
1221
1260
 
1222
1261
 
1223
1262
  def render_collection_grading_desk(sweep: CollectionGradingDesk) -> None:
1224
- """The batch view: raw copies ranked best-case first. Here the per-card bars become real
1225
- odds — graded as a group, wins at these bars pay for the losses (the rip-EV logic)."""
1263
+ """The batch view: raw copies with their grading math, biggest upside first. Here the per-card
1264
+ bars become real odds — graded as a group, wins at these bars pay for the losses (the rip-EV
1265
+ logic). Every cell is data; which cards to send, if any, stays the user's call."""
1226
1266
  console.print()
1227
1267
  covered = f"{sweep.deskable} of your {sweep.total_raw_copies} raw copies have a graded market to read"
1228
1268
  console.print(heading("Grading Desk", covered))
1229
1269
  if not sweep.entries:
1230
1270
  console.print("\n [slab.dim]None of your raw cards have graded sales to compare "
1231
- "against yet — no verdicts to give. That's the honest answer, not an "
1271
+ "against yet — no market to read. That's the honest answer, not an "
1232
1272
  "error.[/]\n")
1233
1273
  return
1234
1274
 
1235
1275
  table = slab_table([
1236
- ("Verdict", {}),
1237
1276
  ("Card", {"no_wrap": False}),
1238
1277
  ("Raw", {"justify": "right", "style": "slab.dim"}),
1239
1278
  ("Gem pays", {"justify": "right"}),
1240
- ("Bar", {"justify": "right", "style": "slab.foil"}),
1279
+ ("Miss pays", {"justify": "right"}),
1280
+ ("Break-even", {"justify": "right", "style": "slab.foil"}),
1241
1281
  ("Qty", {"justify": "right", "style": "slab.dim"}),
1242
1282
  ])
1243
- VERDICT_TXT = {
1244
- GradingVerdict.easy_yes: "[slab.gain]send it[/]",
1245
- GradingVerdict.judgment_call: "[slab.foil]your call[/]",
1246
- GradingVerdict.easy_no: "[slab.loss]keep raw[/]",
1283
+ BREAK_EVEN_TXT = {
1284
+ BreakEvenRegion.pays_at_any_grade: "[slab.gain]any grade[/]",
1285
+ BreakEvenRegion.pays_at_no_grade: "[slab.loss]no grade[/]",
1247
1286
  }
1248
1287
  for e in sweep.entries:
1249
1288
  d = e.desk
@@ -1252,23 +1291,25 @@ def render_collection_grading_desk(sweep: CollectionGradingDesk) -> None:
1252
1291
  gem = next((l for l in d.lanes
1253
1292
  if l.grade_key.startswith(f"{d.grading_company}-") and l.payoff is not None),
1254
1293
  None)
1255
- bar = "—"
1256
- if d.verdict == GradingVerdict.judgment_call and d.ten_confidence_needed is not None:
1257
- bar = f"{d.ten_confidence_needed:.0%}"
1258
- elif d.verdict == GradingVerdict.easy_yes:
1259
- bar = "any"
1294
+ miss = next((l for l in d.lanes if l.grade_key == d.miss_grade), None)
1295
+ if d.break_even == BreakEvenRegion.depends_on_grade and d.ten_confidence_needed is not None:
1296
+ be = f"{d.ten_confidence_needed:.0%} gem"
1297
+ else:
1298
+ be = BREAK_EVEN_TXT.get(d.break_even, d.break_even.value)
1260
1299
  thin = " [slab.warn]!" if d.thin_data else ""
1261
1300
  table.add_row(
1262
- VERDICT_TXT.get(d.verdict, d.verdict.value) + thin, card,
1301
+ card,
1263
1302
  money(d.raw.fair_market_value) if d.raw else "—",
1264
1303
  signed_money(gem.payoff) if gem else "[slab.dim]—[/]",
1265
- bar, str(e.quantity),
1304
+ signed_money(miss.payoff) if miss and miss.payoff is not None else "[slab.dim]—[/]",
1305
+ be + thin, str(e.quantity),
1266
1306
  )
1267
1307
  console.print(table)
1268
1308
  console.print(f" [slab.dim]{sweep.fee_note}[/]")
1269
- console.print(" [slab.dim]Bar = how sure you'd need to be the card gems before grading "
1270
- "beats keeping it raw. You can see its corners; slab can't. "
1271
- "! = verdict rests on very few sales.[/]")
1309
+ console.print(" [slab.dim]Break-even = the chance of a gem at which grading and staying raw "
1310
+ "pay the same — below it grading loses, above it grading wins; 'any grade' = "
1311
+ "even a miss pays, 'no grade' = even a gem doesn't. You can see each card's "
1312
+ "corners; slab can't — the call is yours. ! = rests on very few sales.[/]")
1272
1313
  console.print()
1273
1314
 
1274
1315
 
File without changes