Гайд: Калькулятор контрактов CS2: GUI-скрипт на Python для точного расчета шансов, EV
Важные примечания
| Аспект | Статус в скрипте |
|---|---|
| Математика шансов | Точная: вероятность коллекции = N/10, внутри коллекции равномерное распределение |
| Цены скинов | Вводятся вручную. Для автоподтягивания цен потребуется подключение к API торговых площадок |
| Float / StatTrak | Не учитывается в расчёте шансов, но влияет на цену входа/выхода |
| Комиссия Steam | Можно добавить, умножив итоговый EV на 0.87 в методе _calc_logic |
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
import json
import os
from collections import Counter
class TradeUpCalculator:
def __init__(self, root):
self.root = root
self.root.title("CS2 Trade Up Calculator")
self.root.geometry("950x720")
self.root.minsize(800, 600)
self.collections_db = {}
self.input_skins = []
self._load_or_init_db()
self._build_ui()
def _load_or_init_db(self):
db_file = "cs2_collections.json"
if os.path.exists(db_file):
with open(db_file, "r", encoding="utf-8") as f:
self.collections_db = json.load(f)
else:
self.collections_db = {
"The Bank Collection": [
{"name": "P250 | Boreal Forest", "price": 1.50},
{"name": "Nova | Predator", "price": 1.20}
],
"The Assault Collection": [
{"name": "AK-47 | Blue Laminate", "price": 5.00},
{"name": "MP7 | Forest DDPAT", "price": 1.80}
]
}
self._save_db(db_file)
def _save_db(self, path):
with open(path, "w", encoding="utf-8") as f:
json.dump(self.collections_db, f, indent=2, ensure_ascii=False)
def _build_ui(self):
top = ttk.Frame(self.root)
top.pack(fill="x", padx=10, pady=5)
ttk.Label(top, text="CS2 Trade Up Contract Calculator", font=("Segoe UI", 14, "bold")).pack(side="left")
ttk.Button(top, text="Load DB", command=self.load_db).pack(side="right", padx=5)
ttk.Button(top, text="Save DB", command=self.save_db_gui).pack(side="right", padx=5)
ttk.Button(top, text="Manage Collections", command=self.open_collection_manager).pack(side="right")
input_frame = ttk.LabelFrame(self.root, text="Input Skins (Exactly 10 required)")
input_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.input_tree = ttk.Treeview(input_frame, columns=("ID", "Collection", "Price"), show="headings", height=6)
self.input_tree.heading("ID", text="#")
self.input_tree.heading("Collection", text="Collection")
self.input_tree.heading("Price", text="Price ($)")
self.input_tree.column("ID", width=40, anchor="center")
self.input_tree.column("Collection", width=350)
self.input_tree.column("Price", width=100, anchor="e")
self.input_tree.pack(side="left", fill="both", expand=True)
ctrl = ttk.Frame(input_frame)
ctrl.pack(side="right", fill="y", padx=5, pady=5)
self.coll_var = tk.StringVar()
self.price_var = tk.StringVar()
ttk.Label(ctrl, text="Collection:").pack(anchor="w")
self.coll_cb = ttk.Combobox(ctrl, textvariable=self.coll_var, state="readonly")
self.coll_cb["values"] = list(self.collections_db.keys())
self.coll_cb.pack(fill="x", pady=2)
ttk.Label(ctrl, text="Price ($):").pack(anchor="w")
ttk.Entry(ctrl, textvariable=self.price_var).pack(fill="x", pady=2)
ttk.Button(ctrl, text="Add", command=self.add_input).pack(fill="x", pady=4)
ttk.Button(ctrl, text="Remove", command=self.remove_input).pack(fill="x", pady=2)
ttk.Button(ctrl, text="Clear", command=self.clear_input).pack(fill="x", pady=2)
ttk.Button(self.root, text="Calculate Trade Up", command=self.calculate).pack(pady=8)
res_frame = ttk.LabelFrame(self.root, text="Possible Outcomes")
res_frame.pack(fill="both", expand=True, padx=10, pady=5)
self.res_tree = ttk.Treeview(res_frame, columns=("Skin", "Prob", "Price", "EV"), show="headings", height=10)
self.res_tree.heading("Skin", text="Output Skin")
self.res_tree.heading("Prob", text="Chance (%)")
self.res_tree.heading("Price", text="Price ($)")
self.res_tree.heading("EV", text="EV Contribution ($)")
self.res_tree.column("Skin", width=320)
self.res_tree.column("Prob", width=90, anchor="e")
self.res_tree.column("Price", width=90, anchor="e")
self.res_tree.column("EV", width=110, anchor="e")
self.res_tree.pack(fill="both", expand=True)
sum_frame = ttk.Frame(self.root)
sum_frame.pack(fill="x", padx=10, pady=5)
self.lbl_cost = ttk.Label(sum_frame, text="Input Cost: $0.00")
self.lbl_cost.grid(row=0, column=0, sticky="w")
self.lbl_ev = ttk.Label(sum_frame, text="Expected Value: $0.00")
self.lbl_ev.grid(row=0, column=1, sticky="w")
self.lbl_profit = ttk.Label(sum_frame, text="Profit: $0.00", font=("Segoe UI", 10, "bold"))
self.lbl_profit.grid(row=0, column=2, sticky="w")
self.lbl_roi = ttk.Label(sum_frame, text="ROI: 0%", font=("Segoe UI", 10, "bold"))
self.lbl_roi.grid(row=0, column=3, sticky="w")
def add_input(self):
coll = self.coll_var.get()
try:
price = float(self.price_var.get())
if price <= 0: raise ValueError
except ValueError:
messagebox.showwarning("Warning", "Enter a valid positive price.")
return
if not coll:
messagebox.showwarning("Warning", "Select a collection.")
return
self.input_skins.append({"collection": coll, "price": price})
self._refresh_input()
self.price_var.set("")
def remove_input(self):
sel = self.input_tree.selection()
if not sel: return
idx = int(self.input_tree.item(sel[0])["values"][0]) - 1
self.input_skins.pop(idx)
self._refresh_input()
def clear_input(self):
self.input_skins.clear()
self._refresh_input()
def _refresh_input(self):
for i in self.input_tree.get_children(): self.input_tree.delete(i)
for i, s in enumerate(self.input_skins, 1):
self.input_tree.insert("", "end", values=(i, s["collection"], f"${s['price']:.2f}"))
def calculate(self):
if len(self.input_skins) != 10:
messagebox.showwarning("Warning", f"Exactly 10 skins required. Current: {len(self.input_skins)}")
return
try:
res = self._calc_logic()
self._show_results(res)
except Exception as e:
messagebox.showerror("Error", str(e))
def _calc_logic(self):
total_cost = sum(s["price"] for s in self.input_skins)
counts = Counter(s["collection"] for s in self.input_skins)
outcomes = []
total_ev = 0.0
for coll, cnt in counts.items():
if coll not in self.collections_db:
raise ValueError(f"Collection '{coll}' not found in database")
outs = self.collections_db[coll]
n_out = len(outs)
coll_prob = cnt / 10.0
for out in outs:
prob = coll_prob / n_out
ev_c = prob * out["price"]
total_ev += ev_c
outcomes.append({
"skin": f"{coll} | {out['name']}",
"probability": prob * 100,
"price": out["price"],
"ev": ev_c
})
outcomes.sort(key=lambda x: x["probability"], reverse=True)
profit = total_ev - total_cost
roi = (profit / total_cost) * 100 if total_cost > 0 else 0
return {"outcomes": outcomes, "total_cost": total_cost, "expected_value": total_ev, "profit": profit, "roi": roi}
def _show_results(self, res):
for i in self.res_tree.get_children(): self.res_tree.delete(i)
for o in res["outcomes"]:
self.res_tree.insert("", "end", values=(
o["skin"], f"{o['probability']:.2f}%", f"${o['price']:.2f}", f"${o['ev']:.4f}"
))
self.lbl_cost.config(text=f"Input Cost: ${res['total_cost']:.2f}")
self.lbl_ev.config(text=f"Expected Value: ${res['expected_value']:.2f}")
p_col = "green" if res["profit"] >= 0 else "red"
r_col = "green" if res["roi"] >= 0 else "red"
self.lbl_profit.config(text=f"Profit: ${res['profit']:.2f}", foreground=p_col)
self.lbl_roi.config(text=f"ROI: {res['roi']:.2f}%", foreground=r_col)
def load_db(self):
path = filedialog.askopenfilename(filetypes=[("JSON", "*.json")])
if path:
with open(path, "r", encoding="utf-8") as f:
self.collections_db = json.load(f)
self.coll_cb["values"] = list(self.collections_db.keys())
messagebox.showinfo("Success", "Database loaded")
def save_db_gui(self):
path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json")])
if path:
self._save_db(path)
messagebox.showinfo("Success", "Database saved")
def open_collection_manager(self):
win = tk.Toplevel(self.root)
win.title("Manage Collections DB")
win.geometry("650x520")
ttk.Label(win, text="Add Output to Collection", font=("Segoe UI", 11, "bold")).pack(pady=5)
f = ttk.Frame(win)
f.pack(fill="x", padx=10)
ttk.Label(f, text="Collection:").grid(row=0, column=0, sticky="w")
name_var = tk.StringVar()
ttk.Entry(f, textvariable=name_var).grid(row=0, column=1, padx=5)
ttk.Label(f, text="Skin:").grid(row=1, column=0, sticky="w")
skin_var = tk.StringVar()
ttk.Entry(f, textvariable=skin_var).grid(row=1, column=1, padx=5)
ttk.Label(f, text="Price ($):").grid(row=2, column=0, sticky="w")
price_var = tk.StringVar()
ttk.Entry(f, textvariable=price_var).grid(row=2, column=1, padx=5)
def add_out():
n, s, p_str = name_var.get().strip(), skin_var.get().strip(), price_var.get().strip()
try:
p = float(p_str)
if p <= 0: raise ValueError
except ValueError:
messagebox.showerror("Error", "Invalid price")
return
if not n or not s:
messagebox.showerror("Error", "Fill all fields")
return
self.collections_db.setdefault(n, []).append({"name": s, "price": p})
self.coll_cb["values"] = list(self.collections_db.keys())
_refresh_list()
skin_var.set("")
price_var.set("")
ttk.Button(f, text="Add", command=add_out).grid(row=3, column=0, columnspan=2, pady=10)
tree = ttk.Treeview(win, columns=("C", "S", "P"), show="headings", height=15)
tree.heading("C", text="Collection")
tree.heading("S", text="Output Skin")
tree.heading("P", text="Price")
tree.column("C", width=200)
tree.column("S", width=280)
tree.column("P", width=80, anchor="e")
tree.pack(fill="both", expand=True, padx=10, pady=5)
def _refresh_list():
for i in tree.get_children(): tree.delete(i)
for c, outs in self.collections_db.items():
for o in outs:
tree.insert("", "end", values=(c, o["name"], f"${o['price']:.2f}"))
def del_sel():
sel = tree.selection()
if not sel: return
c, s, _ = tree.item(sel[0])["values"]
c, s = c.strip(), s.strip()
if c in self.collections_db:
self.collections_db[c] = [x for x in self.collections_db[c] if x["name"] != s]
if not self.collections_db[c]: del self.collections_db[c]
self.coll_cb["values"] = list(self.collections_db.keys())
tree.delete(sel[0])
ttk.Button(win, text="Delete Selected", command=del_sel).pack(pady=5)
_refresh_list()
if __name__ == "__main__":
root = tk.Tk()
TradeUpCalculator(root)
root.mainloop()
Как использовать
- Сохраните код в файл
cs2_tradeup_gui.py. - Запустите:
python cs2_tradeup_gui.py(не требует дополнительных библиотек). - Добавьте нужные коллекции и скины через кнопку "Manage Collections" или загрузите готовый JSON через "Load DB".
- Введите 10 скинов в левую таблицу и нажмите "Calculate Trade Up".
Формат JSON базы данных
{
"The Bank Collection": [
{"name": "P250 | Boreal Forest", "price": 1.45},
{"name": "Nova | Predator", "price": 1.12}
],
"The Anubis Collection": [
{"name": "M4A4 | Temukau", "price": 12.50},
{"name": "USP-S | Ticket to Hell", "price": 8.20},
{"name": "MAC-10 | Light Box", "price": 0.85}
]
}