Гайд: Калькулятор контрактов CS2: GUI-скрипт на Python для точного расчета шансов, EV

Графический калькулятор контрактов обмена скинов CS2 на Tkinter. Рассчитывает шансы, EV, ROI. Интерфейс для тестирования стратегий перед реальным апгрейдом.

2026.05.19                  


Гайд: Калькулятор контрактов CS2: GUI-скрипт на Python для точного расчета шансов, EVГайд: Калькулятор контрактов 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()

Как использовать

  1. Сохраните код в файл cs2_tradeup_gui.py.
  2. Запустите: python cs2_tradeup_gui.py (не требует дополнительных библиотек).
  3. Добавьте нужные коллекции и скины через кнопку "Manage Collections" или загрузите готовый JSON через "Load DB".
  4. Введите 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}
  ]
}