1
+ import discord
2
+ import asyncio
3
+ import aiohttp
4
+ import json
5
+ from datetime import datetime
6
+ from discord import ui
7
+ from discord .ui import Button , button , View
8
+
9
+ from utils import DBClient
10
+
11
+ db = DBClient .db
12
+
13
+ # Configuration Constants
14
+ # TODO: put this in a config file
15
+ ALPHA_VANTAGE_API_KEY = os .getenv ('ALPHA_VANTAGE_API_KEY' )
16
+ MIN_TRADE_AMOUNT = 1
17
+ MAX_TRADE_AMOUNT = 1000
18
+ TRADE_COOLDOWN = 5
19
+ DEMO_MODE = True
20
+ MOCK_PRICES = {
21
+ "AAPL" : 175.50 ,
22
+ "GOOGL" : 140.25 ,
23
+ "MSFT" : 380.75 ,
24
+ "AMZN" : 145.30 ,
25
+ "TSLA" : 240.45 ,
26
+ "META" : 485.60 ,
27
+ "NVDA" : 820.30 ,
28
+ "AMD" : 175.25
29
+ }
30
+
31
+ class StockPortfolioView (View ):
32
+ def __init__ (self , authorid ):
33
+ super ().__init__ (timeout = None )
34
+ self .authorid = authorid
35
+ self .last_trade_time = {}
36
+
37
+ @button (label = "Buy Stocks" , style = discord .ButtonStyle .primary , custom_id = "buy_stocks" , emoji = "📈" )
38
+ async def buy_stocks (self , interaction : discord .Interaction , button : discord .ui .Button ):
39
+ if interaction .user .id != self .authorid :
40
+ return await interaction .response .send_message ("This isn't your trading session!" , ephemeral = True )
41
+
42
+ current_time = datetime .now ().timestamp ()
43
+ if self .authorid in self .last_trade_time :
44
+ time_diff = current_time - self .last_trade_time [self .authorid ]
45
+ if time_diff < TRADE_COOLDOWN :
46
+ return await interaction .response .send_message (
47
+ f"Please wait { TRADE_COOLDOWN - int (time_diff )} seconds before trading again!" ,
48
+ ephemeral = True
49
+ )
50
+
51
+ self .last_trade_time [self .authorid ] = current_time
52
+ await interaction .response .send_modal (BuyStocksModal (self .authorid ))
53
+
54
+ @button (label = "Sell Stocks" , style = discord .ButtonStyle .danger , custom_id = "sell_stocks" , emoji = "📉" )
55
+ async def sell_stocks (self , interaction : discord .Interaction , button : discord .ui .Button ):
56
+ if interaction .user .id != self .authorid :
57
+ return await interaction .response .send_message ("This isn't your trading session!" , ephemeral = True )
58
+
59
+ current_time = datetime .now ().timestamp ()
60
+ if self .authorid in self .last_trade_time :
61
+ time_diff = current_time - self .last_trade_time [self .authorid ]
62
+ if time_diff < TRADE_COOLDOWN :
63
+ return await interaction .response .send_message (
64
+ f"Please wait { TRADE_COOLDOWN - int (time_diff )} seconds before trading again!" ,
65
+ ephemeral = True
66
+ )
67
+
68
+ self .last_trade_time [self .authorid ] = current_time
69
+ await interaction .response .send_modal (SellStocksModal (self .authorid ))
70
+
71
+ @button (label = "View Portfolio" , style = discord .ButtonStyle .secondary , custom_id = "view_portfolio" , emoji = "📊" )
72
+ async def view_portfolio (self , interaction : discord .Interaction , button : discord .ui .Button ):
73
+ if interaction .user .id != self .authorid :
74
+ return await interaction .response .send_message ("This isn't your trading session!" , ephemeral = True )
75
+
76
+ c = db ["trading" ]
77
+ portfolio = c .find_one ({"user_id" : interaction .user .id , "guild_id" : interaction .guild .id })
78
+
79
+ if not portfolio or not portfolio .get ("positions" , {}):
80
+ return await interaction .response .send_message ("You don't have any positions yet!" , ephemeral = True )
81
+
82
+ c_users = db ["users" ]
83
+ user = c_users .find_one ({"id" : interaction .user .id , "guild_id" : interaction .guild .id })
84
+
85
+ embed = discord .Embed (title = "Your Portfolio" , color = 0x00ff00 )
86
+ embed .add_field (name = "Available Balance" , value = f"${ user ['wallet' ]:,.2f} " , inline = False )
87
+
88
+ total_value = 0
89
+
90
+ for symbol , position in portfolio ["positions" ].items ():
91
+ price = await get_stock_price (symbol )
92
+ if price :
93
+ current_value = position ["shares" ] * price
94
+ total_value += current_value
95
+ profit_loss = current_value - (position ["shares" ] * position ["average_price" ])
96
+
97
+ embed .add_field (
98
+ name = f"{ symbol } " ,
99
+ value = f"Shares: { position ['shares' ]} \n "
100
+ f"Avg Price: ${ position ['average_price' ]:.2f} \n "
101
+ f"Current Price: ${ price :.2f} \n "
102
+ f"P/L: ${ profit_loss :.2f} ({ (profit_loss / current_value )* 100 :.1f} %)" ,
103
+ inline = False
104
+ )
105
+
106
+ embed .add_field (name = "Total Portfolio Value" , value = f"${ total_value :.2f} " , inline = False )
107
+ embed .add_field (name = "Total Account Value" , value = f"${ (total_value + user ['wallet' ]):.2f} " , inline = False )
108
+ await interaction .response .send_message (embed = embed , ephemeral = True )
109
+
110
+ class BuyStocksModal (ui .Modal , title = "Buy Stocks" ):
111
+ def __init__ (self , authorid ):
112
+ super ().__init__ ()
113
+ self .authorid = authorid
114
+
115
+ symbol = ui .TextInput (label = "Stock Symbol" , placeholder = "e.g. AAPL" , min_length = 1 , max_length = 5 )
116
+ shares = ui .TextInput (label = "Number of Shares" , placeholder = "e.g. 10" )
117
+
118
+ async def on_submit (self , interaction : discord .Interaction ):
119
+ if interaction .user .id != self .authorid :
120
+ return await interaction .response .send_message ("This isn't your trading session!" , ephemeral = True )
121
+
122
+ symbol = self .symbol .value .upper ()
123
+ try :
124
+ shares = float (self .shares .value )
125
+ if not MIN_TRADE_AMOUNT <= shares <= MAX_TRADE_AMOUNT :
126
+ return await interaction .response .send_message (
127
+ f"Please enter between { MIN_TRADE_AMOUNT } and { MAX_TRADE_AMOUNT } shares!" ,
128
+ ephemeral = True
129
+ )
130
+ except ValueError :
131
+ return await interaction .response .send_message ("Please enter a valid number of shares!" , ephemeral = True )
132
+
133
+ price = await get_stock_price (symbol )
134
+ if not price :
135
+ return await interaction .response .send_message ("Invalid stock symbol or API error!" , ephemeral = True )
136
+
137
+ total_cost = price * shares
138
+
139
+ c = db ["users" ]
140
+ user = c .find_one ({"id" : interaction .user .id , "guild_id" : interaction .guild .id })
141
+ if not user or user ["wallet" ] < total_cost :
142
+ return await interaction .response .send_message (
143
+ f"Insufficient funds! You need ${ total_cost :,.2f} but have ${ user ['wallet' ]:,.2f} " ,
144
+ ephemeral = True
145
+ )
146
+
147
+ c = db ["trading" ]
148
+ portfolio = c .find_one ({"user_id" : interaction .user .id , "guild_id" : interaction .guild .id })
149
+
150
+ if not portfolio :
151
+ portfolio = {
152
+ "user_id" : interaction .user .id ,
153
+ "guild_id" : interaction .guild .id ,
154
+ "positions" : {}
155
+ }
156
+ c .insert_one (portfolio )
157
+
158
+ if symbol in portfolio ["positions" ]:
159
+ current_position = portfolio ["positions" ][symbol ]
160
+ new_shares = current_position ["shares" ] + shares
161
+ new_average_price = ((current_position ["shares" ] * current_position ["average_price" ]) + total_cost ) / new_shares
162
+ portfolio ["positions" ][symbol ] = {
163
+ "shares" : new_shares ,
164
+ "average_price" : new_average_price
165
+ }
166
+ else :
167
+ portfolio ["positions" ][symbol ] = {
168
+ "shares" : shares ,
169
+ "average_price" : price
170
+ }
171
+
172
+ c .update_one (
173
+ {"user_id" : interaction .user .id , "guild_id" : interaction .guild .id },
174
+ {"$set" : {"positions" : portfolio ["positions" ]}}
175
+ )
176
+
177
+ user ["wallet" ] -= total_cost
178
+ c = db ["users" ]
179
+ c .update_one (
180
+ {"id" : interaction .user .id , "guild_id" : interaction .guild .id },
181
+ {"$set" : {"wallet" : user ["wallet" ]}}
182
+ )
183
+
184
+ await interaction .response .send_message (
185
+ f"Successfully bought { shares } shares of { symbol } at ${ price :.2f} per share.\n "
186
+ f"Total cost: ${ total_cost :.2f} \n "
187
+ f"Remaining balance: ${ user ['wallet' ]:,.2f} " ,
188
+ ephemeral = True
189
+ )
190
+
191
+ class SellStocksModal (ui .Modal , title = "Sell Stocks" ):
192
+ def __init__ (self , authorid ):
193
+ super ().__init__ ()
194
+ self .authorid = authorid
195
+
196
+ symbol = ui .TextInput (label = "Stock Symbol" , placeholder = "e.g. AAPL" , min_length = 1 , max_length = 5 )
197
+ shares = ui .TextInput (label = "Number of Shares" , placeholder = "e.g. 10" )
198
+
199
+ async def on_submit (self , interaction : discord .Interaction ):
200
+ if interaction .user .id != self .authorid :
201
+ return await interaction .response .send_message ("This isn't your trading session!" , ephemeral = True )
202
+
203
+ symbol = self .symbol .value .upper ()
204
+ try :
205
+ shares = float (self .shares .value )
206
+ if shares <= 0 :
207
+ raise ValueError ("Shares must be positive" )
208
+ except ValueError :
209
+ return await interaction .response .send_message ("Please enter a valid number of shares!" , ephemeral = True )
210
+
211
+ c = db ["trading" ]
212
+ portfolio = c .find_one ({"user_id" : interaction .user .id , "guild_id" : interaction .guild .id })
213
+
214
+ if not portfolio or symbol not in portfolio ["positions" ]:
215
+ return await interaction .response .send_message ("You don't own this stock!" , ephemeral = True )
216
+
217
+ current_position = portfolio ["positions" ][symbol ]
218
+ if current_position ["shares" ] < shares :
219
+ return await interaction .response .send_message (
220
+ f"You don't have enough shares! You own { current_position ['shares' ]} shares." ,
221
+ ephemeral = True
222
+ )
223
+
224
+ price = await get_stock_price (symbol )
225
+ if not price :
226
+ return await interaction .response .send_message ("Invalid stock symbol or API error!" , ephemeral = True )
227
+
228
+ total_value = price * shares
229
+
230
+ new_shares = current_position ["shares" ] - shares
231
+ if new_shares == 0 :
232
+ del portfolio ["positions" ][symbol ]
233
+ else :
234
+ portfolio ["positions" ][symbol ]["shares" ] = new_shares
235
+
236
+ c .update_one (
237
+ {"user_id" : interaction .user .id , "guild_id" : interaction .guild .id },
238
+ {"$set" : {"positions" : portfolio ["positions" ]}}
239
+ )
240
+
241
+ c = db ["users" ]
242
+ user = c .find_one ({"id" : interaction .user .id , "guild_id" : interaction .guild .id })
243
+ user ["wallet" ] += total_value
244
+ c .update_one (
245
+ {"id" : interaction .user .id , "guild_id" : interaction .guild .id },
246
+ {"$set" : {"wallet" : user ["wallet" ]}}
247
+ )
248
+
249
+ profit_loss = (price - current_position ["average_price" ]) * shares
250
+
251
+ await interaction .response .send_message (
252
+ f"Successfully sold { shares } shares of { symbol } at ${ price :.2f} per share.\n "
253
+ f"Total value: ${ total_value :.2f} \n "
254
+ f"Profit/Loss: ${ profit_loss :.2f} \n "
255
+ f"New balance: ${ user ['wallet' ]:,.2f} " ,
256
+ ephemeral = True
257
+ )
258
+
259
+ async def get_stock_price (symbol ):
260
+ """Get current stock price using Alpha Vantage API or mock data"""
261
+ if DEMO_MODE and symbol in MOCK_PRICES :
262
+ return MOCK_PRICES [symbol ]
263
+
264
+ url = f"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={ symbol } &apikey={ ALPHA_VANTAGE_API_KEY } "
265
+
266
+ async with aiohttp .ClientSession () as session :
267
+ try :
268
+ async with session .get (url ) as response :
269
+ data = await response .json ()
270
+ if "Global Quote" in data and "05. price" in data ["Global Quote" ]:
271
+ return float (data ["Global Quote" ]["05. price" ])
272
+ return None
273
+ except :
274
+ return None
275
+
276
+ async def start_paper_trading (ctx ):
277
+ """
278
+ Command to start paper trading session
279
+ Usage: !trade or /trade
280
+ """
281
+ c = db ["users" ]
282
+ user = c .find_one ({"id" : ctx .author .id , "guild_id" : ctx .guild .id })
283
+
284
+ if not user :
285
+ return await ctx .send ("get outa here brokie" )
286
+
287
+ embed = discord .Embed (
288
+ title = "Paper Trading" ,
289
+ description = "Welcome to paper trading! Trade stocks with your existing balance.\n "
290
+ "Use the buttons below to buy/sell stocks and view your portfolio." ,
291
+ color = 0x00ff00
292
+ )
293
+ embed .add_field (
294
+ name = "Available Balance" ,
295
+ value = f"${ user ['wallet' ]:,.2f} " ,
296
+ inline = False
297
+ )
298
+ if DEMO_MODE :
299
+ embed .add_field (
300
+ name = "Available Demo Stocks" ,
301
+ value = "\n " .join ([f"{ symbol } : ${ price :.2f} " for symbol , price in MOCK_PRICES .items ()]),
302
+ inline = False
303
+ )
304
+
305
+ view = StockPortfolioView (ctx .author .id )
306
+ await ctx .send (embed = embed , view = view )
0 commit comments