Py.Cafe

stackmagnus/

TornTravels

Torn City Travel Profit Analysis Tool

DocsPricing
  • app.py
  • requirements.txt
app.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
import streamlit as st
import pandas as pd
import requests

# === USER CONFIGURABLE SETTINGS ===
API_KEY = st.text_input("Torn API Key", value="gZWmfZJgegZFL2vN")
TRAVEL_TYPE = st.selectbox("Travel Type", ["Standard", "Airstrip", "WLT", "Business"], index=1)
ITEM_SLOTS = st.number_input("Item Slots", min_value=1, max_value=100, value=29)
HOURS_PER_DAY = st.number_input("Hours Per Day", min_value=1, max_value=24, value=12)
TORNEX_USER_IDS = st.text_area(
    "TornExchange User IDs (name:id, one per line)",
    value="Malvoiy:2290370"
)
# Parse user IDs
TORNEX_USER_IDS = {
    line.split(":")[0].strip(): int(line.split(":")[1].strip())
    for line in TORNEX_USER_IDS.strip().splitlines() if ":" in line
}

# Travel time data in minutes (one-way)
travel_data = {
    "Mexico": {"Standard": 26, "Airstrip": 18, "WLT": 13, "Business": 8, "Cost": 6500},
    "Cayman Islands": {"Standard": 35, "Airstrip": 25, "WLT": 18, "Business": 11, "Cost": 10000},
    "Canada": {"Standard": 41, "Airstrip": 29, "WLT": 20, "Business": 12, "Cost": 9000},
    "Hawaii": {"Standard": 134, "Airstrip": 94, "WLT": 67, "Business": 40, "Cost": 11000},
    "United Kingdom": {"Standard": 159, "Airstrip": 111, "WLT": 80, "Business": 48, "Cost": 18000},
    "Argentina": {"Standard": 167, "Airstrip": 117, "WLT": 83, "Business": 50, "Cost": 21000},
    "Switzerland": {"Standard": 175, "Airstrip": 123, "WLT": 88, "Business": 53, "Cost": 27000},
    "Japan": {"Standard": 225, "Airstrip": 158, "WLT": 113, "Business": 68, "Cost": 32000},
    "China": {"Standard": 242, "Airstrip": 169, "WLT": 121, "Business": 72, "Cost": 35000},
    "United Arab Emirates": {"Standard": 271, "Airstrip": 190, "WLT": 135, "Business": 81, "Cost": 32000},
    "South Africa": {"Standard": 297, "Airstrip": 208, "WLT": 149, "Business": 89, "Cost": 40000}
}

foreign_prices = {
    "South Africa": {"Xanax": 752508, "LSD": 33445, "Lion Plushie": 400, "African Violet": 2000, "Opium": 66890, "PCP": 100334, "Shrooms": 16720},
    "United Arab Emirates": {"Camel Plushie": 14000, "Tribulus Omanense": 6000},
    "China": {"Panda Plushie": 400, "Peony": 5000, "Ecstasy": 45840, "LSD": 35597, "Speed": 152801, "Opium": 72632, "PCP": 103619},
    "Argentina": {"Monkey Plushie": 400, "Ceibo Flower": 500, "Cannabis": 5000, "LSD": 41210, "Speed": 168292, "Ketamine": 100005, "Shrooms": 19801},
    "Japan": {"Cherry Blossom": 500, "Xanax": 819084, "Ecstasy": 46424, "Speed": 154716, "Opium": 73543, "Ketamine": 91929, "Shrooms": 18386, "Vicodin": 119507},
    "United Kingdom": {"Heather": 5000, "Red Fox Plushie": 1000, "Nessie Plushie": 200, "Cannabis": 5057, "Ecstasy": 50063, "PCP": 121365, "Ketamine": 100127, "Shrooms": 20228, "Vicodin": 132794, "Xanax": 910149},
    "Mexico": {"Jaguar Plushie": 10000, "Dahlia": 300},
    "Cayman Islands": {"Stingray Plushie": 400, "Banana Orchid": 4000},
    "Canada": {"Wolverine Plushie": 30, "Crocus": 600, "Ecstasy": 61002, "Cannabis": 6099, "PCP": 146389, "Vicodin": 165029, "Xanax": 1109126},
    "Hawaii": {"Orchid": 700},
    "Switzerland": {"Chamois Plushie": 400, "Edelweiss": 900, "Cannabis": 4936, "LSD": 39487, "Speed": 167818, "PCP": 118460, "Ketamine": 100701, "Shrooms": 19743}
}

item_name_to_id = {
    "Cannabis": 196, "Ecstasy": 197, "Ketamine": 198, "LSD": 199, "Opium": 200,
    "PCP": 201, "Shrooms": 203, "Speed": 204, "Vicodin": 205, "Xanax": 206,
    "Jaguar Plushie": 258, "Dahlia": 260, "Wolverine Plushie": 261, "Crocus": 263, "Orchid": 264,
    "Nessie Plushie": 266, "Heather": 267, "Red Fox Plushie": 268, "Monkey Plushie": 269,
    "Ceibo Flower": 271, "Edelweiss": 272, "Chamois Plushie": 273, "Panda Plushie": 274,
    "Peony": 276, "Cherry Blossom": 277, "Lion Plushie": 281, "African Violet": 282,
    "Camel Plushie": 384, "Tribulus Omanense": 385, "Banana Orchid": 617, "Stingray Plushie": 618
}

def fetch_market_price(item_id):
    url = f"https://api.torn.com/v2/market/{item_id}/itemmarket?offset=0&key={API_KEY}"
    try:
        response = requests.get(url)
        data = response.json()
        return data.get("itemmarket", {}).get("item", {}).get("average_price")
    except Exception as e:
        st.warning(f"❌ Failed to fetch price for item ID {item_id}: {e}")
        return None

def fetch_tornexchange_user_price(user_id, item_id):
    url = f"https://tornexchange.com/api/price?user_id={user_id}&item_id={item_id}"
    try:
        response = requests.get(url)
        data = response.json()
        if data.get("status") == "error" and data.get("rate_limited"):
            retry_after = data.get("retry_after", 0)
            mins, secs = divmod(retry_after, 60)
            st.error(f"TornEx API Request limit reached. Try again in {mins} minutes {secs} seconds.")
            st.stop()
        if data.get("status") == "success":
            return data["data"]["price"]
    except Exception as e:
        st.warning(f"❌ Failed to fetch TornExchange price for user {user_id}, item {item_id}: {e}")
    return None

def fetch_tornexchange_best_listing(item_id):
    url = f"https://tornexchange.com/api/listings?item_id={item_id}&sort_by=price&page=1"
    try:
        response = requests.get(url)
        data = response.json()
        if data.get("status") == "error" and data.get("rate_limited"):
            retry_after = data.get("retry_after", 0)
            mins, secs = divmod(retry_after, 60)
            st.error(f"TornEx API Request limit reached. Try again in {mins} minutes {secs} seconds.")
            st.stop()
        if data.get("status") == "success":
            listings = data.get("data", {}).get("listings", [])
            if listings:
                return listings[0]["price"], listings[0]["trader"]
    except Exception as e:
        st.warning(f"❌ Failed to fetch TornExchange best listing for item {item_id}: {e}")
    return None, None

if st.button("Run Profit Analysis"):
    results = []

    for country, items in foreign_prices.items():
        travel_time = travel_data[country][TRAVEL_TYPE]
        round_trip = travel_time * 2
        trips_per_day = (HOURS_PER_DAY * 60) // round_trip

        for item_name, abroad_price in items.items():
            item_id = item_name_to_id.get(item_name)
            if not item_id:
                st.warning(f"⚠️ Missing item ID for {item_name}")
                continue

            market_price = fetch_market_price(item_id)
            if not market_price:
                st.warning(f"⚠️ Could not fetch price for {item_name} (ID {item_id})")
                continue

            # Apply 5% tax to market price
            taxed_market_price = market_price * 0.95

            # TornExchange best listing (now using /listings and highest price)
            best_listing_price, best_listing_trader = fetch_tornexchange_best_listing(item_id)

            # TornExchange user prices
            user_prices = {}
            for user, user_id in TORNEX_USER_IDS.items():
                user_price = fetch_tornexchange_user_price(user_id, item_id)
                if user_price:
                    user_prices[user] = user_price

            # Profit calculations
            profit_per_item_market = taxed_market_price - abroad_price
            profit_per_trip_market = profit_per_item_market * ITEM_SLOTS
            profit_per_minute_market = profit_per_trip_market / round_trip
            profit_per_day_market = profit_per_trip_market * trips_per_day

            # TornExchange best listing profit
            if best_listing_price:
                profit_per_item_best_listing = best_listing_price - abroad_price
                profit_per_trip_best_listing = profit_per_item_best_listing * ITEM_SLOTS
                profit_per_minute_best_listing = profit_per_trip_best_listing / round_trip
            else:
                profit_per_item_best_listing = profit_per_trip_best_listing = profit_per_minute_best_listing = None

            # TornExchange user profits
            user_profits = {}
            for user, price in user_prices.items():
                profit_per_item_user = price - abroad_price
                profit_per_trip_user = profit_per_item_user * ITEM_SLOTS
                profit_per_minute_user = profit_per_trip_user / round_trip
                user_profits[user] = {
                    "price": price,
                    "profit_per_item": profit_per_item_user,
                    "profit_per_trip": profit_per_trip_user,
                    "profit_per_minute": profit_per_minute_user
                }

            # Decide which is best
            best_method = "Market"
            best_profit_per_minute = profit_per_minute_market
            best_sell_price = round(taxed_market_price, 2)
            best_sell_to = "Market"

            if best_listing_price and profit_per_minute_best_listing and profit_per_minute_best_listing > best_profit_per_minute:
                best_method = "TornExchange Best"
                best_profit_per_minute = profit_per_minute_best_listing
                best_sell_price = best_listing_price
                best_sell_to = f"TornExchange ({best_listing_trader})"

            for user, profit_data in user_profits.items():
                if profit_data["profit_per_minute"] and profit_data["profit_per_minute"] > best_profit_per_minute:
                    best_method = f"TornExchange User ({user})"
                    best_profit_per_minute = profit_data["profit_per_minute"]
                    best_sell_price = profit_data["price"]
                    best_sell_to = f"TornExchange ({user})"

            results.append({
                "Destination": country,
                "Item": item_name,
                "Abroad Price": abroad_price,
                "Taxed Market Price": round(taxed_market_price, 2),
                "TornEx Best Price": best_listing_price,
                "TornEx Best Trader": best_listing_trader,
                **{f"TornEx {user} Price": profit_data["price"] for user, profit_data in user_profits.items()},
                "Best Sell Method": best_method,
                "Best Sell Price": best_sell_price,
                "Best Sell To": best_sell_to,
                "Profit/min Market": round(profit_per_minute_market, 2) if profit_per_minute_market is not None else None,
                "Profit/min TornEx Best": round(profit_per_minute_best_listing, 2) if profit_per_minute_best_listing is not None else None,
                **{f"Profit/min TornEx {user}": round(profit_data['profit_per_minute'], 2) for user, profit_data in user_profits.items()},
                "Best Profit/min": round(best_profit_per_minute, 2) if best_profit_per_minute is not None else None,
            })

    df = pd.DataFrame(results)
    df["Destination"] = df["Destination"].replace("United Arab Emirates", "UAE")

    if df.empty:
        st.error("🚫 No results. Check API key or internet connection.")
    else:
        df = df.sort_values(by="Best Profit/min", ascending=False).reset_index(drop=True)

        st.subheader("Top 10 Profitable Routes (Market, TornExchange)")
        main_cols = [
            "Destination",
            "Item",
            "Taxed Market Price",
            "Profit/min Market",
            "TornEx Best Price",
            "TornEx Best Trader",
            "Profit/min TornEx Best"
        ]
        main_df = df[main_cols].head(10)
        main_df = main_df.rename(columns={
            "Taxed Market Price": "Taxed Price",
            "Profit/min Market": "Taxed PPM",
            "TornEx Best Price": "TornEx Best Price",
            "TornEx Best Trader": "TornEx Best Trader",
            "Profit/min TornEx Best": "TornEx PPM"
        })
        st.dataframe(main_df)

        for user in TORNEX_USER_IDS:
            st.markdown(f"#### TornEx {user} Prices and PPM")
            user_cols = [
                "Item",
                f"TornEx {user} Price",
                f"Profit/min TornEx {user}"
            ]
            user_df = df[user_cols].head(10)
            user_df = user_df.rename(columns={
                f"TornEx {user} Price": f"{user} Price",
                f"Profit/min TornEx {user}": f"{user} PPM"
            })
            st.dataframe(user_df)

        st.markdown("#### Profit per day (Market, TornEx Best, and Users)")
        profit_day_cols = [
            "Item",
            "Profit/min Market",
            "Profit/min TornEx Best",
            *[f"Profit/min TornEx {user}" for user in TORNEX_USER_IDS]
        ]
        profit_day_df = df[profit_day_cols].head(10).copy()
        profit_day_df["Profit/day Market"] = profit_day_df["Profit/min Market"] * 60 * HOURS_PER_DAY
        profit_day_df["Profit/day TornEx Best"] = profit_day_df["Profit/min TornEx Best"] * 60 * HOURS_PER_DAY
        for user in TORNEX_USER_IDS:
            profit_day_df[f"Profit/day {user}"] = profit_day_df[f"Profit/min TornEx {user}"] * 60 * HOURS_PER_DAY

        show_cols = ["Item", "Profit/day Market", "Profit/day TornEx Best"] + [f"Profit/day {user}" for user in TORNEX_USER_IDS]
        st.dataframe(profit_day_df[show_cols])