Investment Trading Simulator
Technical Documentation — Project Handoff & Portfolio Reference
1. Technology Stack
Composer Dependencies
| Package | Version | Purpose |
|---|---|---|
| laravel/framework | ^12.0 | Core framework |
| laravel/tinker | ^2.10 | REPL for dev tasks |
| phpunit/phpunit | ^11.5 (dev) | Testing |
| laravel/pint | ^1.24 (dev) | Code style linter |
Note: No third-party packages are used for authentication, password hashing, sorting, searching, or graph traversal — all implemented from scratch.
2. High-Level Architecture
The application follows MVC (Model-View-Controller) augmented with a Service Layer and a custom Support layer for data structures and algorithms.
Data Flow: A Typical Trade
POST /market/trade → TradeController@storeuser_id in session.Data Flow: Market Tick (Price Simulation)
GET /market/tick every few seconds.MarketController@tick → PriceEngine@tick()closeCandle() → compute OHLCV → persist Candle to DB → reset tick buffer.JSON { "prices": { "assetId": latestPrice } } for DOM update.3. Directory Structure
investment-trading-simulator/ ├── app/ │ ├── Http/ │ │ ├── Controllers/ (10 controller files) │ │ └── Middleware/ ManualAuthenticate.php, EnsureUserHasRole.php │ ├── Models/ (8 Eloquent model files) │ ├── Providers/ AppServiceProvider.php │ ├── Services/ PriceEngine.php, MarketState.php, │ │ SimulationSessionManager.php, PasswordHasher.php │ └── Support/ │ ├── Algorithms/ BinarySearch.php, MergeSort.php │ ├── Collections/ SimpleList.php, SimpleQueue.php, SimpleStack.php │ └── Graphs/ TutorialGraph.php ├── config/ app, auth, cache, database, logging... ├── database/ │ ├── migrations/ (11 migration files) │ ├── seeders/ AssetSeeder.php, DatabaseSeeder.php │ └── database.sqlite SQLite database file ├── public/ Web root: index.php, compiled CSS/JS ├── resources/ │ ├── views/ │ ├── admin/ users.blade.php, assets/index.blade.php │ ├── auth/ login.blade.php, register.blade.php │ ├── dashboard/ user, admin, portfolio, session-summary │ ├── layouts/ app.blade.php (master layout) │ ├── market/ live, charts, chart, history │ ├── partials/ sidebar.blade.php │ └── tutorial/ index.blade.php, step.blade.php ├── routes/web.php All application routes ├── tests/ Feature/ and Unit/ stubs ├── .env.example ├── composer.json └── vite.config.js
4. Installation & Setup Guide
Prerequisites
PHP 8.2+ with: pdo, pdo_sqlite, mbstring, xml, openssl, tokenizer • Composer • Node.js 18+ & npm • Git
Clone the Repository
git clone <repository-url> investment-trading-simulator cd investment-trading-simulator
Automated Setup (Recommended)
composer run setup
Runs: composer install → copy .env → generate app key → migrate DB → npm install → npm run build
Seed the Database
php artisan db:seed
Inserts 4 practice assets: BLUE, TECH, REIT, BTC.
Start the Development Server
composer run dev
Starts PHP server at http://127.0.0.1:8000, queue worker, log watcher, Vite hot-reload.
Create an Admin User (via Tinker)
php artisan tinker
>>> $h = (new App\Services\PasswordHasher)->hash('password');
>>> App\Models\User::create([
... 'name' => 'Admin', 'email' => 'admin@example.com',
... 'password' => $h['hash'], 'password_salt' => $h['salt'],
... 'role' => 'admin', 'initial_balance' => 1000, 'current_balance' => 1000
... ]);
Regular users register at /register and receive $1,000 virtual cash automatically.
Key Environment Variables
| Variable | Default | Description |
|---|---|---|
| APP_KEY | (generated) | Application encryption key |
| DB_CONNECTION | sqlite | File-based SQLite database |
| SESSION_DRIVER | database | Session storage backend |
| QUEUE_CONNECTION | database | Job queue backend |
| APP_DEBUG | true | Set to false in production |
5. Database Design
Entity-Relationship Overview
USER ├── has one ──► PORTFOLIO ├── has many ──► HOLDING ──► belongs to ──► ASSET ├── has many ──► TRADE ──► belongs to ──► ASSET │ └───────── belongs to ──► SIMULATION_SESSION ├── has many ──► SIMULATION_SESSION └── has many ──► TUTORIAL_PROGRESS ASSET └── has many ──► CANDLE
Table: users
| Column | Type | Notes |
|---|---|---|
| id | bigint PK | Auto-increment |
| name | varchar | Display name |
| varchar UNIQUE | Login identifier | |
| password | varchar | SHA-256 iterated hash (1000 rounds) |
| password_salt | varchar(64) | Random 32-char hex salt (unique per user) |
| role | varchar(20) | ‘user’ or ‘admin’. Default: ‘user’ |
| initial_balance | decimal(15,2) | Starting budget. Default: $1,000.00 |
| current_balance | decimal(15,2) | Live cash balance |
Table: assets
| Column | Type | Notes |
|---|---|---|
| symbol | varchar UNIQUE | Ticker e.g. ‘BLUE’, ‘BTC’ |
| base_price | decimal(15,2) | Starting/reference price |
| volatility | decimal(8,4) | Max swing fraction. 0.07 = 7% per tick |
| liquidity | decimal(15,2) | Volume before significant price impact |
| behaviour_profile | varchar | ‘stable’,‘growth’,‘defensive’,‘crypto’ |
| is_crypto | boolean | True for cryptocurrency assets |
| is_active | boolean | False = disabled from trading |
Table: candles
| Column | Type | Notes |
|---|---|---|
| asset_id | bigint FK | References assets.id (cascade) |
| time_index | bigint uint | Sequential candle number (0,1,2…) |
| open | decimal(15,4) | First tick price in period |
| high | decimal(15,4) | Maximum tick price in period |
| low | decimal(15,4) | Minimum tick price in period |
| close | decimal(15,4) | Last tick price in period |
| volume | decimal(18,4) | Trade volume (currently 0, placeholder) |
| driver | varchar | ‘user’, ‘noise’, or ‘mixed’ |
| UNIQUE: (asset_id, time_index) | ||
Table: trades
| Column | Type | Notes |
|---|---|---|
| user_id | bigint FK | References users.id |
| asset_id | bigint FK | References assets.id |
| side | varchar | ‘buy’ or ‘sell’ |
| quantity | decimal(18,4) | Units traded (min 0.01) |
| price | decimal(15,4) | Execution price per unit |
| simulation_session_id | bigint nullable | Links to active session |
| status | varchar | ‘executed’,‘pending’,‘cancelled’ |
| executed_at | timestamp | Time of execution |
| INDEX: (user_id, asset_id), (simulation_session_id) | ||
Table: holdings
| Column | Type | Notes |
|---|---|---|
| user_id | bigint FK | References users.id |
| asset_id | bigint FK | References assets.id |
| quantity | decimal(18,4) | Units currently owned |
| average_cost | decimal(15,4) | Weighted average purchase price |
| UNIQUE: (user_id, asset_id) | ||
Table: simulation_sessions
| Column | Type | Notes |
|---|---|---|
| user_id | bigint FK | References users.id |
| starting_balance | decimal(15,2) | Balance when session began |
| current_balance | decimal(15,2) | Synced after every trade |
| mode | varchar | ‘standard’, ‘tutorial’, ‘scenario’ |
| started_at | timestamp | Session start time |
| ended_at | timestamp NULL | NULL = session currently active |
Table: tutorial_progress
| Column | Type | Notes |
|---|---|---|
| user_id | bigint FK | References users.id |
| step_key | varchar | ‘intro’,‘candles’,‘risk’,‘practice’ |
| completed | boolean | Has user marked this done? |
| completed_at | timestamp NULL | When it was completed |
| UNIQUE: (user_id, step_key) | ||
6. Routing Map
Guest Routes
| Method | URI | Handler |
|---|---|---|
| GET/ | Redirect to /login | |
| GET/register | AuthController@showRegister | |
| POST/register | AuthController@register | |
| GET/login | AuthController@showLogin | |
| POST/login | AuthController@login |
Authenticated Routes (middleware: auth.manual)
| Method | URI | Handler |
|---|---|---|
| GET/dashboard | DashboardController@index | |
| GET/market | MarketController@index | |
| GET/market/charts | MarketController@charts | |
| GET/market/tick | MarketController@tick — JSON | |
| GET/market/history/{asset} | MarketHistoryController@show | |
| GET/market/history/{asset}/json | MarketHistoryController@json — JSON | |
| GET/market/chart/{asset} | market.chart view | |
| POST/market/trade | TradeController@store | |
| GET/portfolio | PortfolioController@index | |
| GET/session/summary | SessionSummaryController@show | |
| POST/session/complete | SessionSummaryController@complete | |
| GET/tutorial | TutorialController@index | |
| GET/tutorial/step/{step} | TutorialController@show | |
| POST/tutorial/step/{step}/complete | TutorialController@complete | |
| POST/logout | AuthController@logout |
Admin Routes (middleware: auth.manual + role:admin)
| Method | URI | Handler |
|---|---|---|
| GET/admin/dashboard | DashboardController@admin | |
| GET/admin/users | AdminController@index | |
| GET/admin/assets | AssetAdminController@index | |
| POST/admin/assets | AssetAdminController@store | |
| PUT/admin/assets/{id} | AssetAdminController@update |
7. Controllers
| Controller | Methods & Responsibilities |
|---|---|
| AuthController | showRegister() — return form. register() — manual validation (name≥3, email regex, password≥6, match, unique), hash with PasswordHasher, create User ($1000 balance) + Portfolio. showLogin() — return form. login() — verify hash, store session keys, route by role. logout() — clear session, regenerate CSRF. |
| DashboardController | index() — load user+portfolio+holdings, calculate totalValue, unrealizedPnL, overallPnL&%, get/create session. admin() — admin welcome view. |
| MarketController | index() — live market view. charts() — charts hub. tick() — advance PriceEngine, return JSON prices. |
| TradeController | store() — validate, DB transaction: BUY checks balance, deducts cost, updates Holding (weighted avg), saves Trade; SELL checks qty, reduces Holding, adds proceeds, saves Trade. Syncs session. |
| PortfolioController | index() — all holdings, last 20 trades, compute unrealized/realized/overall P&L. |
| SessionSummaryController | show() — session trades, compute counts/cashflow/avg size/P&L. complete() — set ended_at=now(). |
| MarketHistoryController | show() — fetch candles, MergeSort by time_index, optional BinarySearch by ?time_index=N. json() — same + return JSON. |
| TutorialController | index() — TutorialGraph DFS order, load progress. show() — single step view. complete() — mark done, find next via DFS, redirect. |
| AdminController | index() — all users, platform totals (users, trades, assets, balances). |
| AssetAdminController | index() list assets. store() create asset. update() update price/volatility/liquidity/active. |
8. Models & Relationships
| Model | Relationships | Key Notes |
|---|---|---|
| User | hasOne Portfolio; hasMany Holding, Trade, SimulationSession | NOT Authenticatable. password+salt hidden from JSON. |
| Portfolio | belongsTo User | One per user. Total invested + P&L. |
| Asset | hasMany Candle, Trade, Holding | Volatility & liquidity drive price simulation. |
| Candle | belongsTo Asset | OHLCV. One per 5 ticks. Unique (asset_id, time_index). |
| Trade | belongsTo User, Asset, SimulationSession | Immutable execution record. |
| Holding | belongsTo User, Asset | Live ownership. avg_cost recalculated on buys. |
| SimulationSession | belongsTo User; hasMany Trade | ended_at=NULL means active. |
| TutorialProgress | belongsTo User | Per-user step completion. Unique (user_id, step_key). |
9. Services
PriceEngine
Core simulation engine. One MarketState per active asset. Uses SimpleQueue (trade queue) and SimpleList (tick buffer).
tick() for each active asset: noise = currentPrice * (mt_rand(-1000,1000)/1000 * volatility) impact = sum( direction * currentPrice * (qty/liquidity) ) per queued trade price = current + noise + impact (floor $0.01) append to currentCandleTicks (SimpleList) if ticks >= 5: closeCandle() -> OHLCV -> DB -> reset SimpleList
MarketState
Per-asset in-memory value object: asset, currentPrice, currentCandleTicks (SimpleList), recentCandles (SimpleList, max 200), nextTimeIndex.
SimulationSessionManager
| Method | Description |
|---|---|
| getOrCreateActiveSession(User) | Find open session (ended_at IS NULL) or create new ‘standard’ session. |
| syncBalance(Session, User) | Copy user.current_balance → session. Called after every trade. |
| completeSession(User) | Set ended_at = now() on active session. |
PasswordHasher
hash(password):
salt = bin2hex(random_bytes(16)) // 32-char hex
value = salt + "|" + password
loop 1000x: value = sha256(value)
return { hash: value, salt: salt }
verify(password, storedHash, salt):
computed = runHashLoop(password, salt)
status = 0
for each char i: status |= ord(computed[i]) XOR ord(storedHash[i])
return (status === 0) // constant-time comparison
10. Custom Data Structures
SimpleList
Manual dynamic ordered list.
add(v)— O(1) appendget(i)— O(1) index accessset(i,v)— O(1) replaceremoveAt(i)— O(n) left-shiftslice(s,l)— O(n) subsetsize()— O(1)last()— O(1)
SimpleQueue (FIFO)
Manual front-removal without array_shift.
enqueue(v)— O(1) add to backdequeue()— O(n) remove frontpeek()— O(1)isEmpty()— O(1)
SimpleStack (LIFO)
Push and pop from top.
push(v)— O(1)pop()— O(1)peek()— O(1)isEmpty()— O(1)
11. Custom Algorithms
Divide-and-conquer sort for arrays of associative arrays. No PHP built-in sort used.
sortByKey(items, key, ascending): if count <= 1: return unchanged left = items[0 .. mid-1] right = items[mid .. end] left = sortByKey(left) right = sortByKey(right) return merge(left, right) merge: two-pointer comparison, take smaller (asc) or larger (desc), append remainder
Used: MarketHistoryController@show() and @json() — sort candles by time_index.
Fast lookup in a sorted array. Input must be pre-sorted.
findIndexByKey(items, key, target): low=0, high=count-1 mid = floor((low+high)/2) items[mid][key] == target → return mid items[mid][key] < target → low = mid+1 items[mid][key] > target → high = mid-1 return -1 if not found
Used: MarketHistoryController@show() — find candle by ?time_index=N.
Directed adjacency-list graph with recursive depth-first traversal.
Graph: intro → candles → risk → practice
dfs(node, visited, order):
if visited: return
mark visited, append to order
for each neighbor: dfs(neighbor)
depthFirstOrder('intro') = ['intro', 'candles', 'risk', 'practice']
Used: TutorialController@index() for order, @complete() for next-step navigation.
value = salt + "|" + password for i = 0 to 999: value = sha256(value) 1000 iterations makes brute-force 1000x slower per attempt.
12. Middleware & Security
ManualAuthenticate (alias: auth.manual)
Checks session()->has('user_id'). If missing: redirect to login. Applied to all authenticated routes.
EnsureUserHasRole (alias: role)
Usage: middleware(['auth.manual', 'role:admin']). Checks session('user_role') === $required. If mismatch: abort(403). Applied to all /admin/* routes.
Security Measures
- 1000-iteration SHA-256 + unique random salt per user (brute-force resistant).
- Constant-time XOR comparison in password verify (prevents timing attacks).
- CSRF tokens on all forms (Laravel default VerifyCsrfToken middleware).
- Eloquent parameterized queries throughout (no SQL injection).
- DB::transaction() in TradeController (atomic writes, no partial state).
- Full server-side input validation before any database writes.
- password and password_salt in User $hidden (excluded from JSON output).
13. Views / Frontend
| View File | Description |
|---|---|
| layouts/app.blade.php | Master template. Navbar, sidebar, @yield(content). |
| partials/sidebar.blade.php | Role-aware navigation. Admin sees extra links. |
| auth/login.blade.php | Email + password login form with error display. |
| auth/register.blade.php | Registration form. Inline validation errors. Preserves old input. |
| dashboard/user.blade.php | Cash balance, total portfolio value, P&L, holdings table, session info. |
| dashboard/admin.blade.php | Admin welcome + quick links. |
| dashboard/portfolio.blade.php | Full holdings detail, realized/unrealized P&L, last 20 trades. |
| dashboard/session-summary.blade.php | Session report: balance P&L, trade counts, cash flow, full trade log. |
| market/live.blade.php | Live price table. JS polls /market/tick, updates DOM. Inline trade form. |
| market/charts.blade.php | Grid of asset cards linking to chart pages. |
| market/chart.blade.php | Candlestick chart. Fetches JSON from /market/history/{asset}/json. |
| market/history.blade.php | Candle history table (MergeSort sorted). Search uses BinarySearch. |
| tutorial/index.blade.php | All steps in DFS order with completion status. |
| tutorial/step.blade.php | Single step view with “Mark as complete” button. |
| admin/users.blade.php | All users table + platform totals. |
| admin/assets/index.blade.php | Asset list with inline edit forms. Add Asset form. |
14. Seeded Data
Run: php artisan db:seed — uses AssetSeeder with updateOrCreate (safe to re-run).
| Symbol | Name | Base Price | Volatility | Liquidity | Type | Profile |
|---|---|---|---|---|---|---|
| BLUE | Blue Chip Industries | $100.00 | 1.0% | 500,000 | Stock | stable |
| TECH | Tech Growth Corp | $50.00 | 3.5% | 250,000 | Stock | growth |
| REIT | City Real Estate Trust | $80.00 | 1.5% | 300,000 | Stock | defensive |
| BTC | Simulated Bitcoin | $30,000.00 | 7.0% | 150,000 | Crypto | crypto |
BLUE = lowest risk (1%). REIT = low-moderate (1.5%). TECH = medium-high (3.5%). BTC = highest risk (7%) — demonstrates crypto volatility to beginners.