Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- .claude/settings.local.json +13 -0
- README.md +162 -5
- app.py +704 -0
- chatbot.py +623 -0
- config.py +347 -0
- export_data.py +356 -0
- knowledge_base/ai_persona.txt +112 -0
- knowledge_base/companies.txt +161 -0
- knowledge_base/market_context.txt +94 -0
- knowledge_base/trading_basics.txt +133 -0
- requirements.txt +22 -0
- tracking.py +507 -0
- trading.py +294 -0
.claude/settings.local.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"permissions": {
|
| 3 |
+
"allow": [
|
| 4 |
+
"Bash(git pull:*)",
|
| 5 |
+
"Bash(git checkout:*)",
|
| 6 |
+
"Bash(git add:*)",
|
| 7 |
+
"Bash(git commit:*)",
|
| 8 |
+
"Bash(git push:*)",
|
| 9 |
+
"Bash(huggingface-cli whoami:*)",
|
| 10 |
+
"Bash(python3:*)"
|
| 11 |
+
]
|
| 12 |
+
}
|
| 13 |
+
}
|
README.md
CHANGED
|
@@ -1,12 +1,169 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 📈
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
---
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: TradeVerse AI Trading Experiment
|
| 3 |
emoji: 📈
|
| 4 |
+
colorFrom: green
|
| 5 |
+
colorTo: blue
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: 4.44.0
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# TradeVerse AI Trading Experiment
|
| 14 |
+
|
| 15 |
+
A psychology research tool for studying human-AI interaction in financial decision-making.
|
| 16 |
+
|
| 17 |
+
## Overview
|
| 18 |
+
|
| 19 |
+
This application combines:
|
| 20 |
+
- **Trading Game**: Scenario-based trading decisions in a fictional financial universe
|
| 21 |
+
- **AI Chatbot**: RAG-based advisor with tunable parameters
|
| 22 |
+
- **Experiment Tracking**: Comprehensive data collection for research analysis
|
| 23 |
+
|
| 24 |
+
## Features
|
| 25 |
+
|
| 26 |
+
### For Participants
|
| 27 |
+
- Interactive trading scenarios with fictional companies
|
| 28 |
+
- AI advisor that provides recommendations and answers questions
|
| 29 |
+
- Adjustable AI settings (explanation depth, communication style)
|
| 30 |
+
- Real-time portfolio tracking
|
| 31 |
+
|
| 32 |
+
### For Researchers
|
| 33 |
+
- Pre-defined experimental conditions for A/B testing
|
| 34 |
+
- Hidden parameters (accuracy rate, confidence framing, risk bias)
|
| 35 |
+
- Comprehensive tracking of:
|
| 36 |
+
- Trading decisions and outcomes
|
| 37 |
+
- AI consultation patterns
|
| 38 |
+
- Trust metrics and confidence changes
|
| 39 |
+
- Response timing
|
| 40 |
+
- Data export utilities (CSV, Excel reports)
|
| 41 |
+
|
| 42 |
+
## Experimental Conditions
|
| 43 |
+
|
| 44 |
+
| Condition | Accuracy | Confidence | Description |
|
| 45 |
+
|-----------|----------|------------|-------------|
|
| 46 |
+
| high_accuracy_high_confidence | 85% | High | Correct AI, assertive |
|
| 47 |
+
| high_accuracy_low_confidence | 85% | Low | Correct AI, hedging |
|
| 48 |
+
| low_accuracy_high_confidence | 40% | High | Often wrong, assertive |
|
| 49 |
+
| low_accuracy_low_confidence | 40% | Low | Often wrong, hedging |
|
| 50 |
+
| conservative_advisor | 70% | Medium | Risk-averse recommendations |
|
| 51 |
+
| aggressive_advisor | 70% | Medium | Risk-seeking recommendations |
|
| 52 |
+
| passive_advisor | 70% | Low proactivity | Rarely initiates |
|
| 53 |
+
| active_advisor | 70% | High proactivity | Frequently initiates |
|
| 54 |
+
|
| 55 |
+
## Setup
|
| 56 |
+
|
| 57 |
+
### LLM Provider Options
|
| 58 |
+
|
| 59 |
+
The chatbot supports multiple LLM providers (auto-detected based on available API keys):
|
| 60 |
+
|
| 61 |
+
| Provider | Env Variable | Cost | Notes |
|
| 62 |
+
|----------|--------------|------|-------|
|
| 63 |
+
| **HuggingFace** | `HF_TOKEN` | Free tier available | Recommended for HF Spaces |
|
| 64 |
+
| DeepSeek | `DEEPSEEK_API_KEY` | Pay-per-use | Higher quality |
|
| 65 |
+
| Fallback | None needed | Free | Rule-based, limited |
|
| 66 |
+
|
| 67 |
+
#### Recommended HuggingFace Models (free tier)
|
| 68 |
+
|
| 69 |
+
```python
|
| 70 |
+
# In chatbot.py, change DEFAULT_HF_MODEL to any of these:
|
| 71 |
+
"Qwen/Qwen2-1.5B-Instruct" # Smallest, fastest
|
| 72 |
+
"microsoft/Phi-3-mini-4k-instruct" # Small, good quality
|
| 73 |
+
"HuggingFaceH4/zephyr-7b-beta" # Default, best quality
|
| 74 |
+
"mistralai/Mistral-7B-Instruct-v0.2" # Popular alternative
|
| 75 |
+
```
|
| 76 |
+
|
| 77 |
+
### Environment Variables
|
| 78 |
+
|
| 79 |
+
**Option 1: HuggingFace (Recommended for HF Spaces)**
|
| 80 |
+
```bash
|
| 81 |
+
export HF_TOKEN="hf_your_token_here"
|
| 82 |
+
```
|
| 83 |
+
Get your token at: https://huggingface.co/settings/tokens
|
| 84 |
+
|
| 85 |
+
**Option 2: DeepSeek**
|
| 86 |
+
```bash
|
| 87 |
+
export DEEPSEEK_API_KEY="your-api-key-here"
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
### Local Development
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
pip install -r requirements.txt
|
| 94 |
+
python app.py
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### Hugging Face Spaces
|
| 98 |
+
|
| 99 |
+
1. Create a new Space with Gradio SDK
|
| 100 |
+
2. Upload all files
|
| 101 |
+
3. Add `HF_TOKEN` as a secret in Space settings (Settings → Repository secrets)
|
| 102 |
+
- Your HF token works automatically in Spaces
|
| 103 |
+
|
| 104 |
+
## Data Export
|
| 105 |
+
|
| 106 |
+
```bash
|
| 107 |
+
# Quick statistics
|
| 108 |
+
python export_data.py stats
|
| 109 |
+
|
| 110 |
+
# Export all data to CSV
|
| 111 |
+
python export_data.py export
|
| 112 |
+
|
| 113 |
+
# Generate full Excel report
|
| 114 |
+
python export_data.py report
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
## Project Structure
|
| 118 |
+
|
| 119 |
+
```
|
| 120 |
+
IntegratedGame/
|
| 121 |
+
├── app.py # Main Gradio application
|
| 122 |
+
├── trading.py # Trading engine and portfolio management
|
| 123 |
+
├── chatbot.py # RAG chatbot with parameter-aware responses
|
| 124 |
+
├── tracking.py # Database and experiment tracking
|
| 125 |
+
├── config.py # Scenarios, conditions, parameters
|
| 126 |
+
├── export_data.py # Data analysis and export
|
| 127 |
+
├── requirements.txt
|
| 128 |
+
├── knowledge_base/
|
| 129 |
+
│ ├── companies.txt # Fictional company profiles
|
| 130 |
+
│ ├── market_context.txt # TradeVerse economic backdrop
|
| 131 |
+
│ ├── trading_basics.txt # Trading fundamentals
|
| 132 |
+
│ └── ai_persona.txt # AI behavior guidelines
|
| 133 |
+
└── db/
|
| 134 |
+
└── experiment.db # SQLite database (auto-created)
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
## Database Schema
|
| 138 |
+
|
| 139 |
+
### sessions
|
| 140 |
+
- Participant sessions with portfolio tracking
|
| 141 |
+
- AI reliance metrics
|
| 142 |
+
|
| 143 |
+
### decisions
|
| 144 |
+
- Individual trading decisions
|
| 145 |
+
- AI parameters at decision time
|
| 146 |
+
- Timing and confidence data
|
| 147 |
+
|
| 148 |
+
### chat_interactions
|
| 149 |
+
- All AI-participant conversations
|
| 150 |
+
- Proactive vs reactive classification
|
| 151 |
+
- Engagement metrics
|
| 152 |
+
|
| 153 |
+
### trust_metrics
|
| 154 |
+
- Pre/post advice confidence
|
| 155 |
+
- Advice adherence
|
| 156 |
+
- Outcome tracking
|
| 157 |
+
|
| 158 |
+
## Research Applications
|
| 159 |
+
|
| 160 |
+
This tool supports research into:
|
| 161 |
+
- Trust calibration in AI advisors
|
| 162 |
+
- Effect of AI confidence framing on decision-making
|
| 163 |
+
- Proactive vs reactive AI assistance preferences
|
| 164 |
+
- Risk perception under AI guidance
|
| 165 |
+
- Learning and adaptation to AI accuracy
|
| 166 |
+
|
| 167 |
+
## License
|
| 168 |
+
|
| 169 |
+
MIT License
|
app.py
ADDED
|
@@ -0,0 +1,704 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Main Gradio application for the AI Trading Experiment.
|
| 3 |
+
Combines trading game, chatbot, and experiment tracking.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import gradio as gr
|
| 7 |
+
import uuid
|
| 8 |
+
import time
|
| 9 |
+
import random
|
| 10 |
+
from datetime import datetime
|
| 11 |
+
from typing import Optional, Tuple, List, Dict, Any
|
| 12 |
+
|
| 13 |
+
from config import (
|
| 14 |
+
ExperimentConfig, DEFAULT_CONFIG, SCENARIOS,
|
| 15 |
+
ParticipantVisibleParams, ResearcherControlledParams,
|
| 16 |
+
EXPERIMENT_CONDITIONS, get_condition
|
| 17 |
+
)
|
| 18 |
+
from trading import TradingEngine, format_currency, format_percentage
|
| 19 |
+
from chatbot import TradingChatbot, ChatResponse
|
| 20 |
+
from tracking import (
|
| 21 |
+
ExperimentTracker, DecisionRecord, ChatInteraction,
|
| 22 |
+
tracker
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# Global state management
|
| 27 |
+
class SessionState:
|
| 28 |
+
"""Manages session state for each participant."""
|
| 29 |
+
def __init__(self):
|
| 30 |
+
self.reset()
|
| 31 |
+
|
| 32 |
+
def reset(self):
|
| 33 |
+
self.participant_id: Optional[str] = None
|
| 34 |
+
self.trading_engine: Optional[TradingEngine] = None
|
| 35 |
+
self.chatbot: Optional[TradingChatbot] = None
|
| 36 |
+
self.visible_params = ParticipantVisibleParams()
|
| 37 |
+
self.hidden_params = ResearcherControlledParams()
|
| 38 |
+
self.current_scenario = None
|
| 39 |
+
self.scenario_start_time: float = 0
|
| 40 |
+
self.ai_advice_shown_time: float = 0
|
| 41 |
+
self.proactive_advice_shown: bool = False
|
| 42 |
+
self.proactive_advice_engaged: bool = False
|
| 43 |
+
self.chat_queries_this_scenario: int = 0
|
| 44 |
+
self.pre_advice_confidence: int = 50
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
# Initialize components
|
| 48 |
+
session = SessionState()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
def get_random_condition() -> ResearcherControlledParams:
|
| 52 |
+
"""Get a random experimental condition for A/B testing."""
|
| 53 |
+
condition_names = list(EXPERIMENT_CONDITIONS.keys())
|
| 54 |
+
selected = random.choice(condition_names)
|
| 55 |
+
return get_condition(selected)
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def start_experiment(condition_dropdown: str) -> Tuple:
|
| 59 |
+
"""Initialize a new experiment session."""
|
| 60 |
+
session.reset()
|
| 61 |
+
|
| 62 |
+
# Set experimental condition
|
| 63 |
+
if condition_dropdown == "Random (A/B Testing)":
|
| 64 |
+
session.hidden_params = get_random_condition()
|
| 65 |
+
else:
|
| 66 |
+
session.hidden_params = get_condition(condition_dropdown)
|
| 67 |
+
|
| 68 |
+
# Create participant session
|
| 69 |
+
session.participant_id = tracker.create_session(
|
| 70 |
+
condition_name=session.hidden_params.condition_name,
|
| 71 |
+
initial_portfolio=DEFAULT_CONFIG.initial_portfolio_value
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
# Initialize trading engine
|
| 75 |
+
session.trading_engine = TradingEngine(DEFAULT_CONFIG)
|
| 76 |
+
portfolio, first_scenario = session.trading_engine.start_new_game()
|
| 77 |
+
|
| 78 |
+
# Initialize chatbot
|
| 79 |
+
session.chatbot = TradingChatbot()
|
| 80 |
+
session.chatbot.clear_history()
|
| 81 |
+
|
| 82 |
+
# Set current scenario
|
| 83 |
+
session.current_scenario = first_scenario
|
| 84 |
+
session.scenario_start_time = time.time()
|
| 85 |
+
|
| 86 |
+
# Generate proactive advice (maybe)
|
| 87 |
+
proactive_message = ""
|
| 88 |
+
session.proactive_advice_shown = False
|
| 89 |
+
|
| 90 |
+
proactive_response = session.chatbot.generate_proactive_advice(
|
| 91 |
+
first_scenario,
|
| 92 |
+
session.visible_params,
|
| 93 |
+
session.hidden_params
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
if proactive_response:
|
| 97 |
+
proactive_message = f"**AI Advisor:** {proactive_response.message}"
|
| 98 |
+
session.proactive_advice_shown = True
|
| 99 |
+
session.ai_advice_shown_time = time.time()
|
| 100 |
+
|
| 101 |
+
# Record proactive interaction
|
| 102 |
+
tracker.record_chat_interaction(ChatInteraction(
|
| 103 |
+
interaction_id=str(uuid.uuid4())[:12],
|
| 104 |
+
participant_id=session.participant_id,
|
| 105 |
+
timestamp=datetime.now().isoformat(),
|
| 106 |
+
scenario_id=first_scenario.scenario_id,
|
| 107 |
+
interaction_type="proactive",
|
| 108 |
+
user_query=None,
|
| 109 |
+
ai_response=proactive_response.message,
|
| 110 |
+
explanation_depth=session.visible_params.explanation_depth,
|
| 111 |
+
communication_style=session.visible_params.communication_style,
|
| 112 |
+
confidence_framing=session.hidden_params.confidence_framing,
|
| 113 |
+
risk_bias=session.hidden_params.risk_bias,
|
| 114 |
+
response_time_ms=0,
|
| 115 |
+
user_engaged=False,
|
| 116 |
+
dismissed=False
|
| 117 |
+
))
|
| 118 |
+
|
| 119 |
+
# Generate main AI recommendation
|
| 120 |
+
ai_recommendation = session.chatbot.generate_ai_recommendation(
|
| 121 |
+
first_scenario,
|
| 122 |
+
session.visible_params,
|
| 123 |
+
session.hidden_params
|
| 124 |
+
)
|
| 125 |
+
|
| 126 |
+
# Format scenario display
|
| 127 |
+
scenario_text = format_scenario_display(first_scenario)
|
| 128 |
+
progress = session.trading_engine.get_progress_info()
|
| 129 |
+
|
| 130 |
+
return (
|
| 131 |
+
gr.update(visible=False), # Hide welcome
|
| 132 |
+
gr.update(visible=True), # Show game
|
| 133 |
+
gr.update(visible=False), # Hide results
|
| 134 |
+
f"Participant: {session.participant_id}",
|
| 135 |
+
format_currency(portfolio.total_value),
|
| 136 |
+
f"Scenario {progress['current_scenario']} of {progress['total_scenarios']}",
|
| 137 |
+
scenario_text,
|
| 138 |
+
f"**AI Recommendation:**\n\n{ai_recommendation.message}",
|
| 139 |
+
proactive_message,
|
| 140 |
+
[], # Clear chat history
|
| 141 |
+
gr.update(value=50), # Reset confidence slider
|
| 142 |
+
gr.update(value="HOLD"), # Reset decision
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
|
| 146 |
+
def format_scenario_display(scenario) -> str:
|
| 147 |
+
"""Format a scenario for display."""
|
| 148 |
+
return f"""
|
| 149 |
+
## {scenario.company_name} ({scenario.company_symbol})
|
| 150 |
+
|
| 151 |
+
**Sector:** {scenario.sector}
|
| 152 |
+
**Country:** {scenario.country}
|
| 153 |
+
**Current Price:** {scenario.current_price} credits
|
| 154 |
+
|
| 155 |
+
---
|
| 156 |
+
|
| 157 |
+
### Situation
|
| 158 |
+
|
| 159 |
+
{scenario.situation_description}
|
| 160 |
+
"""
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
def update_ai_params(explanation_depth: int, communication_style: int):
|
| 164 |
+
"""Update visible AI parameters and regenerate advice."""
|
| 165 |
+
session.visible_params.explanation_depth = explanation_depth
|
| 166 |
+
session.visible_params.communication_style = communication_style
|
| 167 |
+
|
| 168 |
+
if session.current_scenario and session.chatbot:
|
| 169 |
+
# Regenerate recommendation with new params
|
| 170 |
+
ai_recommendation = session.chatbot.generate_ai_recommendation(
|
| 171 |
+
session.current_scenario,
|
| 172 |
+
session.visible_params,
|
| 173 |
+
session.hidden_params
|
| 174 |
+
)
|
| 175 |
+
return f"**AI Recommendation:**\n\n{ai_recommendation.message}"
|
| 176 |
+
|
| 177 |
+
return ""
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def handle_chat_query(
|
| 181 |
+
message: str,
|
| 182 |
+
chat_history: List[Tuple[str, str]]
|
| 183 |
+
) -> Tuple[List[Tuple[str, str]], str]:
|
| 184 |
+
"""Handle a user query to the chatbot."""
|
| 185 |
+
if not message.strip() or not session.chatbot:
|
| 186 |
+
return chat_history, ""
|
| 187 |
+
|
| 188 |
+
query_start = time.time()
|
| 189 |
+
|
| 190 |
+
response = session.chatbot.answer_query(
|
| 191 |
+
message,
|
| 192 |
+
session.current_scenario,
|
| 193 |
+
session.visible_params,
|
| 194 |
+
session.hidden_params
|
| 195 |
+
)
|
| 196 |
+
|
| 197 |
+
response_time_ms = int((time.time() - query_start) * 1000)
|
| 198 |
+
|
| 199 |
+
# Record interaction
|
| 200 |
+
if session.participant_id:
|
| 201 |
+
tracker.record_chat_interaction(ChatInteraction(
|
| 202 |
+
interaction_id=str(uuid.uuid4())[:12],
|
| 203 |
+
participant_id=session.participant_id,
|
| 204 |
+
timestamp=datetime.now().isoformat(),
|
| 205 |
+
scenario_id=session.current_scenario.scenario_id if session.current_scenario else None,
|
| 206 |
+
interaction_type="reactive_query",
|
| 207 |
+
user_query=message,
|
| 208 |
+
ai_response=response.message,
|
| 209 |
+
explanation_depth=session.visible_params.explanation_depth,
|
| 210 |
+
communication_style=session.visible_params.communication_style,
|
| 211 |
+
confidence_framing=session.hidden_params.confidence_framing,
|
| 212 |
+
risk_bias=session.hidden_params.risk_bias,
|
| 213 |
+
response_time_ms=response_time_ms,
|
| 214 |
+
user_engaged=True,
|
| 215 |
+
dismissed=False
|
| 216 |
+
))
|
| 217 |
+
|
| 218 |
+
session.chat_queries_this_scenario += 1
|
| 219 |
+
|
| 220 |
+
chat_history.append((message, response.message))
|
| 221 |
+
return chat_history, ""
|
| 222 |
+
|
| 223 |
+
|
| 224 |
+
def engage_proactive_advice():
|
| 225 |
+
"""Mark that user engaged with proactive advice."""
|
| 226 |
+
session.proactive_advice_engaged = True
|
| 227 |
+
return "You engaged with the AI's initial observation."
|
| 228 |
+
|
| 229 |
+
|
| 230 |
+
def dismiss_proactive_advice():
|
| 231 |
+
"""Mark that user dismissed proactive advice."""
|
| 232 |
+
session.proactive_advice_engaged = False
|
| 233 |
+
return gr.update(visible=False)
|
| 234 |
+
|
| 235 |
+
|
| 236 |
+
def submit_decision(
|
| 237 |
+
decision: str,
|
| 238 |
+
confidence: int,
|
| 239 |
+
trade_amount: float
|
| 240 |
+
) -> Tuple:
|
| 241 |
+
"""Process the participant's trading decision."""
|
| 242 |
+
if not session.trading_engine or not session.current_scenario:
|
| 243 |
+
return (gr.update(),) * 8
|
| 244 |
+
|
| 245 |
+
# Calculate timing
|
| 246 |
+
decision_time_ms = int((time.time() - session.scenario_start_time) * 1000)
|
| 247 |
+
ai_viewing_time_ms = int((time.time() - session.ai_advice_shown_time) * 1000) if session.ai_advice_shown_time > 0 else 0
|
| 248 |
+
|
| 249 |
+
# Process the decision
|
| 250 |
+
outcome = session.trading_engine.process_decision(
|
| 251 |
+
session.current_scenario,
|
| 252 |
+
decision,
|
| 253 |
+
trade_amount,
|
| 254 |
+
session.current_scenario.ai_recommendation
|
| 255 |
+
)
|
| 256 |
+
|
| 257 |
+
# Record decision
|
| 258 |
+
tracker.record_decision(DecisionRecord(
|
| 259 |
+
decision_id=str(uuid.uuid4())[:12],
|
| 260 |
+
participant_id=session.participant_id,
|
| 261 |
+
timestamp=datetime.now().isoformat(),
|
| 262 |
+
scenario_id=session.current_scenario.scenario_id,
|
| 263 |
+
company_symbol=session.current_scenario.company_symbol,
|
| 264 |
+
explanation_depth=session.visible_params.explanation_depth,
|
| 265 |
+
communication_style=session.visible_params.communication_style,
|
| 266 |
+
confidence_framing=session.hidden_params.confidence_framing,
|
| 267 |
+
risk_bias=session.hidden_params.risk_bias,
|
| 268 |
+
ai_recommendation=session.current_scenario.ai_recommendation,
|
| 269 |
+
ai_was_correct=session.current_scenario.ai_is_correct,
|
| 270 |
+
participant_decision=decision,
|
| 271 |
+
followed_ai=outcome.followed_ai,
|
| 272 |
+
decision_confidence=confidence,
|
| 273 |
+
time_to_decision_ms=decision_time_ms,
|
| 274 |
+
time_viewing_ai_advice_ms=ai_viewing_time_ms,
|
| 275 |
+
outcome_percentage=outcome.outcome_percentage,
|
| 276 |
+
portfolio_before=outcome.portfolio_before,
|
| 277 |
+
portfolio_after=outcome.portfolio_after,
|
| 278 |
+
trade_amount=trade_amount,
|
| 279 |
+
proactive_advice_shown=session.proactive_advice_shown,
|
| 280 |
+
proactive_advice_engaged=session.proactive_advice_engaged
|
| 281 |
+
))
|
| 282 |
+
|
| 283 |
+
# Record trust metrics
|
| 284 |
+
tracker.record_trust_metric(
|
| 285 |
+
participant_id=session.participant_id,
|
| 286 |
+
scenario_id=session.current_scenario.scenario_id,
|
| 287 |
+
pre_confidence=session.pre_advice_confidence,
|
| 288 |
+
post_confidence=confidence,
|
| 289 |
+
advice_followed=outcome.followed_ai,
|
| 290 |
+
time_deliberating_ms=decision_time_ms,
|
| 291 |
+
queries_before_decision=session.chat_queries_this_scenario,
|
| 292 |
+
outcome_positive=(outcome.outcome_percentage > 0)
|
| 293 |
+
)
|
| 294 |
+
|
| 295 |
+
# Format outcome message
|
| 296 |
+
outcome_color = "green" if outcome.outcome_percentage > 0 else "red"
|
| 297 |
+
outcome_message = f"""
|
| 298 |
+
### Decision Outcome
|
| 299 |
+
|
| 300 |
+
You chose to **{decision}** with {trade_amount:,.0f} credits.
|
| 301 |
+
|
| 302 |
+
**Result:** {format_percentage(outcome.outcome_percentage)} ({format_currency(outcome.outcome_amount)})
|
| 303 |
+
|
| 304 |
+
**Portfolio:** {format_currency(outcome.portfolio_after)}
|
| 305 |
+
"""
|
| 306 |
+
|
| 307 |
+
# Check if game is complete
|
| 308 |
+
if session.trading_engine.is_game_complete():
|
| 309 |
+
return show_results()
|
| 310 |
+
|
| 311 |
+
# Move to next scenario
|
| 312 |
+
next_scenario = session.trading_engine.get_next_scenario()
|
| 313 |
+
session.current_scenario = next_scenario
|
| 314 |
+
session.scenario_start_time = time.time()
|
| 315 |
+
session.ai_advice_shown_time = 0
|
| 316 |
+
session.proactive_advice_shown = False
|
| 317 |
+
session.proactive_advice_engaged = False
|
| 318 |
+
session.chat_queries_this_scenario = 0
|
| 319 |
+
session.chatbot.clear_history()
|
| 320 |
+
|
| 321 |
+
# Generate content for next scenario
|
| 322 |
+
scenario_text = format_scenario_display(next_scenario)
|
| 323 |
+
|
| 324 |
+
# Maybe show proactive advice
|
| 325 |
+
proactive_message = ""
|
| 326 |
+
proactive_response = session.chatbot.generate_proactive_advice(
|
| 327 |
+
next_scenario,
|
| 328 |
+
session.visible_params,
|
| 329 |
+
session.hidden_params
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
if proactive_response:
|
| 333 |
+
proactive_message = f"**AI Advisor:** {proactive_response.message}"
|
| 334 |
+
session.proactive_advice_shown = True
|
| 335 |
+
session.ai_advice_shown_time = time.time()
|
| 336 |
+
|
| 337 |
+
tracker.record_chat_interaction(ChatInteraction(
|
| 338 |
+
interaction_id=str(uuid.uuid4())[:12],
|
| 339 |
+
participant_id=session.participant_id,
|
| 340 |
+
timestamp=datetime.now().isoformat(),
|
| 341 |
+
scenario_id=next_scenario.scenario_id,
|
| 342 |
+
interaction_type="proactive",
|
| 343 |
+
user_query=None,
|
| 344 |
+
ai_response=proactive_response.message,
|
| 345 |
+
explanation_depth=session.visible_params.explanation_depth,
|
| 346 |
+
communication_style=session.visible_params.communication_style,
|
| 347 |
+
confidence_framing=session.hidden_params.confidence_framing,
|
| 348 |
+
risk_bias=session.hidden_params.risk_bias,
|
| 349 |
+
response_time_ms=0,
|
| 350 |
+
user_engaged=False,
|
| 351 |
+
dismissed=False
|
| 352 |
+
))
|
| 353 |
+
|
| 354 |
+
# Generate main recommendation
|
| 355 |
+
ai_recommendation = session.chatbot.generate_ai_recommendation(
|
| 356 |
+
next_scenario,
|
| 357 |
+
session.visible_params,
|
| 358 |
+
session.hidden_params
|
| 359 |
+
)
|
| 360 |
+
session.ai_advice_shown_time = time.time()
|
| 361 |
+
|
| 362 |
+
progress = session.trading_engine.get_progress_info()
|
| 363 |
+
|
| 364 |
+
return (
|
| 365 |
+
gr.update(visible=True), # Keep game visible
|
| 366 |
+
gr.update(visible=False), # Keep results hidden
|
| 367 |
+
format_currency(session.trading_engine.portfolio.total_value),
|
| 368 |
+
f"Scenario {progress['current_scenario']} of {progress['total_scenarios']}",
|
| 369 |
+
scenario_text,
|
| 370 |
+
f"**AI Recommendation:**\n\n{ai_recommendation.message}",
|
| 371 |
+
proactive_message,
|
| 372 |
+
[], # Clear chat
|
| 373 |
+
gr.update(value=50),
|
| 374 |
+
gr.update(value="HOLD"),
|
| 375 |
+
outcome_message
|
| 376 |
+
)
|
| 377 |
+
|
| 378 |
+
|
| 379 |
+
def show_results() -> Tuple:
|
| 380 |
+
"""Show the final results screen."""
|
| 381 |
+
if not session.trading_engine:
|
| 382 |
+
return (gr.update(),) * 11
|
| 383 |
+
|
| 384 |
+
# Complete the session
|
| 385 |
+
summary = session.trading_engine.get_game_summary()
|
| 386 |
+
tracker.complete_session(
|
| 387 |
+
session.participant_id,
|
| 388 |
+
summary["final_portfolio"]
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# Get full session summary
|
| 392 |
+
session_summary = tracker.get_session_summary(session.participant_id)
|
| 393 |
+
|
| 394 |
+
results_text = f"""
|
| 395 |
+
# Experiment Complete
|
| 396 |
+
|
| 397 |
+
Thank you for participating!
|
| 398 |
+
|
| 399 |
+
---
|
| 400 |
+
|
| 401 |
+
## Your Results
|
| 402 |
+
|
| 403 |
+
**Participant ID:** {session.participant_id}
|
| 404 |
+
(Save this ID if you need to reference your data)
|
| 405 |
+
|
| 406 |
+
---
|
| 407 |
+
|
| 408 |
+
### Portfolio Performance
|
| 409 |
+
|
| 410 |
+
| Metric | Value |
|
| 411 |
+
|--------|-------|
|
| 412 |
+
| Starting Portfolio | {format_currency(summary['initial_portfolio'])} |
|
| 413 |
+
| Final Portfolio | {format_currency(summary['final_portfolio'])} |
|
| 414 |
+
| Total Return | {format_currency(summary['total_return'])} |
|
| 415 |
+
| Return Percentage | {summary['return_percentage']:.1f}% |
|
| 416 |
+
|
| 417 |
+
---
|
| 418 |
+
|
| 419 |
+
### Decision Analysis
|
| 420 |
+
|
| 421 |
+
| Metric | Value |
|
| 422 |
+
|--------|-------|
|
| 423 |
+
| Total Decisions | {summary['total_decisions']} |
|
| 424 |
+
| AI Follow Rate | {summary['ai_follow_rate']*100:.1f}% |
|
| 425 |
+
| Optimal Decision Rate | {summary['optimal_decision_rate']*100:.1f}% |
|
| 426 |
+
|
| 427 |
+
---
|
| 428 |
+
|
| 429 |
+
### AI Interaction Summary
|
| 430 |
+
|
| 431 |
+
| Metric | Value |
|
| 432 |
+
|--------|-------|
|
| 433 |
+
| Times Followed Correct AI | {summary['followed_correct_ai']} / {summary['ai_correct_scenarios']} |
|
| 434 |
+
| Times Followed Incorrect AI | {summary['followed_incorrect_ai']} / {summary['ai_incorrect_scenarios']} |
|
| 435 |
+
| Chat Queries Made | {session_summary.get('total_chat_queries', 0)} |
|
| 436 |
+
|
| 437 |
+
---
|
| 438 |
+
|
| 439 |
+
### Decision History
|
| 440 |
+
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
+
for d in summary['decisions']:
|
| 444 |
+
followed = "Followed AI" if d['followed_ai'] else "Disagreed with AI"
|
| 445 |
+
optimal = "Optimal" if d['was_optimal'] else "Suboptimal"
|
| 446 |
+
results_text += f"- **{d['scenario']}**: {d['decision']} → {d['outcome']} ({followed}, {optimal})\n"
|
| 447 |
+
|
| 448 |
+
return (
|
| 449 |
+
gr.update(visible=False), # Hide game
|
| 450 |
+
gr.update(visible=True), # Show results
|
| 451 |
+
results_text,
|
| 452 |
+
"", "", "", "", "", [], gr.update(), gr.update(), ""
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
|
| 456 |
+
# Build the Gradio interface
|
| 457 |
+
def create_app():
|
| 458 |
+
"""Create and return the Gradio application."""
|
| 459 |
+
|
| 460 |
+
with gr.Blocks(
|
| 461 |
+
title="TradeVerse AI Experiment",
|
| 462 |
+
theme=gr.themes.Soft(),
|
| 463 |
+
css="""
|
| 464 |
+
.container { max-width: 1200px; margin: auto; }
|
| 465 |
+
.scenario-box { background: #f8f9fa; padding: 20px; border-radius: 10px; }
|
| 466 |
+
.ai-advice { background: #e3f2fd; padding: 15px; border-radius: 8px; border-left: 4px solid #2196f3; }
|
| 467 |
+
.proactive-advice { background: #fff3e0; padding: 15px; border-radius: 8px; border-left: 4px solid #ff9800; }
|
| 468 |
+
.outcome-box { padding: 15px; border-radius: 8px; margin-top: 10px; }
|
| 469 |
+
"""
|
| 470 |
+
) as app:
|
| 471 |
+
|
| 472 |
+
# ==================== WELCOME SCREEN ====================
|
| 473 |
+
with gr.Column(visible=True) as welcome_section:
|
| 474 |
+
gr.Markdown("""
|
| 475 |
+
# TradeVerse AI Trading Experiment
|
| 476 |
+
|
| 477 |
+
Welcome to this research study on AI-assisted decision making.
|
| 478 |
+
|
| 479 |
+
---
|
| 480 |
+
|
| 481 |
+
## About This Experiment
|
| 482 |
+
|
| 483 |
+
You will participate in a simulated trading game set in the **TradeVerse**, a fictional
|
| 484 |
+
financial universe. You will make trading decisions for several companies while receiving
|
| 485 |
+
advice from an AI assistant.
|
| 486 |
+
|
| 487 |
+
**Important:** All companies, markets, and situations are fictional. No real-world
|
| 488 |
+
financial knowledge is required or applicable.
|
| 489 |
+
|
| 490 |
+
---
|
| 491 |
+
|
| 492 |
+
## What You'll Do
|
| 493 |
+
|
| 494 |
+
1. **Review Trading Scenarios** - Each scenario presents a company and market situation
|
| 495 |
+
2. **Consult the AI** - An AI advisor will offer recommendations and answer questions
|
| 496 |
+
3. **Make Decisions** - Choose to BUY, SELL, or HOLD for each scenario
|
| 497 |
+
4. **See Outcomes** - Learn the results of your decisions
|
| 498 |
+
|
| 499 |
+
---
|
| 500 |
+
|
| 501 |
+
## Data Collection
|
| 502 |
+
|
| 503 |
+
This experiment collects:
|
| 504 |
+
- Your trading decisions and confidence levels
|
| 505 |
+
- Your interactions with the AI advisor
|
| 506 |
+
- Timing information
|
| 507 |
+
|
| 508 |
+
All data is anonymized. Your participant ID contains no personal information.
|
| 509 |
+
|
| 510 |
+
---
|
| 511 |
+
|
| 512 |
+
## Consent
|
| 513 |
+
|
| 514 |
+
By clicking "Start Experiment", you consent to participate in this research study.
|
| 515 |
+
You may stop at any time.
|
| 516 |
+
""")
|
| 517 |
+
|
| 518 |
+
with gr.Row():
|
| 519 |
+
condition_dropdown = gr.Dropdown(
|
| 520 |
+
choices=["Random (A/B Testing)"] + list(EXPERIMENT_CONDITIONS.keys()),
|
| 521 |
+
value="Random (A/B Testing)",
|
| 522 |
+
label="Experimental Condition (Researcher Use)",
|
| 523 |
+
visible=True # Set to False in production
|
| 524 |
+
)
|
| 525 |
+
|
| 526 |
+
start_btn = gr.Button("Start Experiment", variant="primary", size="lg")
|
| 527 |
+
|
| 528 |
+
# ==================== GAME SCREEN ====================
|
| 529 |
+
with gr.Column(visible=False) as game_section:
|
| 530 |
+
|
| 531 |
+
# Status bar
|
| 532 |
+
with gr.Row():
|
| 533 |
+
participant_display = gr.Markdown("Participant: ---")
|
| 534 |
+
portfolio_display = gr.Markdown("Portfolio: ---")
|
| 535 |
+
progress_display = gr.Markdown("Progress: ---")
|
| 536 |
+
|
| 537 |
+
with gr.Row():
|
| 538 |
+
# Left column: Scenario and Decision
|
| 539 |
+
with gr.Column(scale=2):
|
| 540 |
+
scenario_display = gr.Markdown(
|
| 541 |
+
"Loading scenario...",
|
| 542 |
+
elem_classes=["scenario-box"]
|
| 543 |
+
)
|
| 544 |
+
|
| 545 |
+
# AI Recommendation
|
| 546 |
+
ai_recommendation_display = gr.Markdown(
|
| 547 |
+
"",
|
| 548 |
+
elem_classes=["ai-advice"]
|
| 549 |
+
)
|
| 550 |
+
|
| 551 |
+
# Proactive advice (may or may not show)
|
| 552 |
+
proactive_display = gr.Markdown(
|
| 553 |
+
"",
|
| 554 |
+
elem_classes=["proactive-advice"]
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
# Decision controls
|
| 558 |
+
gr.Markdown("### Your Decision")
|
| 559 |
+
|
| 560 |
+
with gr.Row():
|
| 561 |
+
decision_radio = gr.Radio(
|
| 562 |
+
choices=["BUY", "HOLD", "SELL"],
|
| 563 |
+
value="HOLD",
|
| 564 |
+
label="Action"
|
| 565 |
+
)
|
| 566 |
+
trade_amount = gr.Slider(
|
| 567 |
+
minimum=1000,
|
| 568 |
+
maximum=50000,
|
| 569 |
+
value=10000,
|
| 570 |
+
step=1000,
|
| 571 |
+
label="Trade Amount (credits)"
|
| 572 |
+
)
|
| 573 |
+
|
| 574 |
+
confidence_slider = gr.Slider(
|
| 575 |
+
minimum=0,
|
| 576 |
+
maximum=100,
|
| 577 |
+
value=50,
|
| 578 |
+
step=5,
|
| 579 |
+
label="How confident are you in this decision?"
|
| 580 |
+
)
|
| 581 |
+
|
| 582 |
+
submit_btn = gr.Button("Submit Decision", variant="primary")
|
| 583 |
+
|
| 584 |
+
outcome_display = gr.Markdown("")
|
| 585 |
+
|
| 586 |
+
# Right column: AI Chat and Controls
|
| 587 |
+
with gr.Column(scale=1):
|
| 588 |
+
gr.Markdown("### AI Advisor Settings")
|
| 589 |
+
|
| 590 |
+
explanation_slider = gr.Slider(
|
| 591 |
+
minimum=0,
|
| 592 |
+
maximum=100,
|
| 593 |
+
value=50,
|
| 594 |
+
step=10,
|
| 595 |
+
label="Explanation Depth",
|
| 596 |
+
info="Minimal ← → Detailed"
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
style_slider = gr.Slider(
|
| 600 |
+
minimum=0,
|
| 601 |
+
maximum=100,
|
| 602 |
+
value=50,
|
| 603 |
+
step=10,
|
| 604 |
+
label="Communication Style",
|
| 605 |
+
info="Formal ← → Casual"
|
| 606 |
+
)
|
| 607 |
+
|
| 608 |
+
update_params_btn = gr.Button("Update AI Settings", size="sm")
|
| 609 |
+
|
| 610 |
+
gr.Markdown("### Ask the AI")
|
| 611 |
+
|
| 612 |
+
chatbot_display = gr.Chatbot(
|
| 613 |
+
label="Chat with AI Advisor",
|
| 614 |
+
height=300
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
chat_input = gr.Textbox(
|
| 618 |
+
placeholder="Ask a question about this scenario...",
|
| 619 |
+
label="Your Question",
|
| 620 |
+
lines=2
|
| 621 |
+
)
|
| 622 |
+
|
| 623 |
+
chat_btn = gr.Button("Send", size="sm")
|
| 624 |
+
|
| 625 |
+
# ==================== RESULTS SCREEN ====================
|
| 626 |
+
with gr.Column(visible=False) as results_section:
|
| 627 |
+
results_display = gr.Markdown("Loading results...")
|
| 628 |
+
|
| 629 |
+
restart_btn = gr.Button("Start New Session", variant="primary")
|
| 630 |
+
|
| 631 |
+
# ==================== EVENT HANDLERS ====================
|
| 632 |
+
|
| 633 |
+
# Start experiment
|
| 634 |
+
start_btn.click(
|
| 635 |
+
fn=start_experiment,
|
| 636 |
+
inputs=[condition_dropdown],
|
| 637 |
+
outputs=[
|
| 638 |
+
welcome_section,
|
| 639 |
+
game_section,
|
| 640 |
+
results_section,
|
| 641 |
+
participant_display,
|
| 642 |
+
portfolio_display,
|
| 643 |
+
progress_display,
|
| 644 |
+
scenario_display,
|
| 645 |
+
ai_recommendation_display,
|
| 646 |
+
proactive_display,
|
| 647 |
+
chatbot_display,
|
| 648 |
+
confidence_slider,
|
| 649 |
+
decision_radio
|
| 650 |
+
]
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
# Update AI parameters
|
| 654 |
+
update_params_btn.click(
|
| 655 |
+
fn=update_ai_params,
|
| 656 |
+
inputs=[explanation_slider, style_slider],
|
| 657 |
+
outputs=[ai_recommendation_display]
|
| 658 |
+
)
|
| 659 |
+
|
| 660 |
+
# Chat interaction
|
| 661 |
+
chat_btn.click(
|
| 662 |
+
fn=handle_chat_query,
|
| 663 |
+
inputs=[chat_input, chatbot_display],
|
| 664 |
+
outputs=[chatbot_display, chat_input]
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
chat_input.submit(
|
| 668 |
+
fn=handle_chat_query,
|
| 669 |
+
inputs=[chat_input, chatbot_display],
|
| 670 |
+
outputs=[chatbot_display, chat_input]
|
| 671 |
+
)
|
| 672 |
+
|
| 673 |
+
# Submit decision
|
| 674 |
+
submit_btn.click(
|
| 675 |
+
fn=submit_decision,
|
| 676 |
+
inputs=[decision_radio, confidence_slider, trade_amount],
|
| 677 |
+
outputs=[
|
| 678 |
+
game_section,
|
| 679 |
+
results_section,
|
| 680 |
+
portfolio_display,
|
| 681 |
+
progress_display,
|
| 682 |
+
scenario_display,
|
| 683 |
+
ai_recommendation_display,
|
| 684 |
+
proactive_display,
|
| 685 |
+
chatbot_display,
|
| 686 |
+
confidence_slider,
|
| 687 |
+
decision_radio,
|
| 688 |
+
outcome_display
|
| 689 |
+
]
|
| 690 |
+
)
|
| 691 |
+
|
| 692 |
+
# Restart
|
| 693 |
+
restart_btn.click(
|
| 694 |
+
fn=lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)),
|
| 695 |
+
outputs=[welcome_section, game_section, results_section]
|
| 696 |
+
)
|
| 697 |
+
|
| 698 |
+
return app
|
| 699 |
+
|
| 700 |
+
|
| 701 |
+
# Launch the application
|
| 702 |
+
if __name__ == "__main__":
|
| 703 |
+
app = create_app()
|
| 704 |
+
app.launch(debug=True)
|
chatbot.py
ADDED
|
@@ -0,0 +1,623 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Chatbot engine with RAG pipeline and proactive/reactive logic.
|
| 3 |
+
Adapted for the AI Trading Experiment with parameter-aware responses.
|
| 4 |
+
|
| 5 |
+
Supports multiple LLM providers:
|
| 6 |
+
- HuggingFace Inference API (free tier available)
|
| 7 |
+
- DeepSeek API
|
| 8 |
+
- Fallback rule-based responses
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import os
|
| 12 |
+
import sys
|
| 13 |
+
import random
|
| 14 |
+
import requests
|
| 15 |
+
from typing import Optional, List, Tuple, Dict, Any
|
| 16 |
+
from dataclasses import dataclass
|
| 17 |
+
from enum import Enum
|
| 18 |
+
|
| 19 |
+
# Attempt pysqlite3 workaround for HF Spaces
|
| 20 |
+
try:
|
| 21 |
+
__import__('pysqlite3')
|
| 22 |
+
sys.modules['sqlite3'] = sys.modules.pop('pysqlite3')
|
| 23 |
+
except ImportError:
|
| 24 |
+
pass # Not in HF Spaces, use default sqlite3
|
| 25 |
+
|
| 26 |
+
# LangChain imports - using langchain_community for newer versions
|
| 27 |
+
try:
|
| 28 |
+
# Try newer import paths first (langchain >= 0.1.0)
|
| 29 |
+
from langchain_community.document_loaders import TextLoader
|
| 30 |
+
from langchain_community.vectorstores import Chroma
|
| 31 |
+
from langchain_community.embeddings import HuggingFaceEmbeddings
|
| 32 |
+
from langchain.text_splitter import CharacterTextSplitter
|
| 33 |
+
from langchain_core.language_models.llms import LLM
|
| 34 |
+
except ImportError:
|
| 35 |
+
# Fall back to older import paths (langchain < 0.1.0)
|
| 36 |
+
from langchain.document_loaders import TextLoader
|
| 37 |
+
from langchain.vectorstores import Chroma
|
| 38 |
+
from langchain.embeddings import HuggingFaceEmbeddings
|
| 39 |
+
from langchain.text_splitter import CharacterTextSplitter
|
| 40 |
+
from langchain.llms.base import LLM
|
| 41 |
+
|
| 42 |
+
from config import Scenario, ResearcherControlledParams, ParticipantVisibleParams
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# Configuration
|
| 46 |
+
KNOWLEDGE_BASE_DIR = "knowledge_base"
|
| 47 |
+
VECTOR_DB_DIR = "db/vectorstore"
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
class LLMProvider(Enum):
|
| 51 |
+
"""Available LLM providers."""
|
| 52 |
+
HUGGINGFACE = "huggingface"
|
| 53 |
+
DEEPSEEK = "deepseek"
|
| 54 |
+
FALLBACK = "fallback"
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
# ==================== LLM Provider Selection ====================
|
| 58 |
+
|
| 59 |
+
# Check which API keys are available and select provider
|
| 60 |
+
def get_llm_provider() -> LLMProvider:
|
| 61 |
+
"""Determine which LLM provider to use based on available credentials."""
|
| 62 |
+
if os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACE_TOKEN") or os.getenv("HF_API_KEY"):
|
| 63 |
+
return LLMProvider.HUGGINGFACE
|
| 64 |
+
elif os.getenv("DEEPSEEK_API_KEY"):
|
| 65 |
+
return LLMProvider.DEEPSEEK
|
| 66 |
+
else:
|
| 67 |
+
print("Warning: No LLM API key found. Using fallback responses.")
|
| 68 |
+
print("Set HF_TOKEN for HuggingFace or DEEPSEEK_API_KEY for DeepSeek.")
|
| 69 |
+
return LLMProvider.FALLBACK
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
# ==================== HuggingFace LLM ====================
|
| 73 |
+
|
| 74 |
+
# Recommended free/cheap models (smallest to largest):
|
| 75 |
+
# - "microsoft/Phi-3-mini-4k-instruct" # 3.8B params, very fast
|
| 76 |
+
# - "Qwen/Qwen2-1.5B-Instruct" # 1.5B params, smallest
|
| 77 |
+
# - "HuggingFaceH4/zephyr-7b-beta" # 7B params, good quality
|
| 78 |
+
# - "mistralai/Mistral-7B-Instruct-v0.2" # 7B params, popular
|
| 79 |
+
# - "meta-llama/Llama-2-7b-chat-hf" # 7B params, requires approval
|
| 80 |
+
|
| 81 |
+
DEFAULT_HF_MODEL = "HuggingFaceH4/zephyr-7b-beta"
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class HuggingFaceLLM(LLM):
|
| 85 |
+
"""LLM wrapper for HuggingFace Inference API (free tier available)."""
|
| 86 |
+
api_key: str = ""
|
| 87 |
+
model_id: str = DEFAULT_HF_MODEL
|
| 88 |
+
temperature: float = 0.7
|
| 89 |
+
max_tokens: int = 512
|
| 90 |
+
|
| 91 |
+
def __init__(self, model_id: str = None, **kwargs):
|
| 92 |
+
super().__init__(**kwargs)
|
| 93 |
+
# Try multiple possible env var names
|
| 94 |
+
self.api_key = (
|
| 95 |
+
os.getenv("HF_TOKEN") or
|
| 96 |
+
os.getenv("HUGGINGFACE_TOKEN") or
|
| 97 |
+
os.getenv("HF_API_KEY") or
|
| 98 |
+
""
|
| 99 |
+
)
|
| 100 |
+
if model_id:
|
| 101 |
+
self.model_id = model_id
|
| 102 |
+
|
| 103 |
+
if self.api_key:
|
| 104 |
+
print(f"Using HuggingFace model: {self.model_id}")
|
| 105 |
+
else:
|
| 106 |
+
print("Warning: No HuggingFace token found.")
|
| 107 |
+
|
| 108 |
+
def _call(self, prompt: str, stop: Optional[List[str]] = None, **kwargs) -> str:
|
| 109 |
+
if not self.api_key:
|
| 110 |
+
return self._fallback_response(prompt)
|
| 111 |
+
|
| 112 |
+
# HuggingFace Inference API endpoint
|
| 113 |
+
api_url = f"https://api-inference.huggingface.co/models/{self.model_id}"
|
| 114 |
+
|
| 115 |
+
headers = {
|
| 116 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 117 |
+
"Content-Type": "application/json"
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# Format prompt for instruction-tuned models
|
| 121 |
+
formatted_prompt = f"""<|system|>
|
| 122 |
+
You are an AI trading advisor in the TradeVerse financial ecosystem. Provide helpful, concise advice.</s>
|
| 123 |
+
<|user|>
|
| 124 |
+
{prompt}</s>
|
| 125 |
+
<|assistant|>
|
| 126 |
+
"""
|
| 127 |
+
|
| 128 |
+
payload = {
|
| 129 |
+
"inputs": formatted_prompt,
|
| 130 |
+
"parameters": {
|
| 131 |
+
"max_new_tokens": self.max_tokens,
|
| 132 |
+
"temperature": self.temperature,
|
| 133 |
+
"do_sample": True,
|
| 134 |
+
"return_full_text": False
|
| 135 |
+
}
|
| 136 |
+
}
|
| 137 |
+
|
| 138 |
+
try:
|
| 139 |
+
response = requests.post(api_url, headers=headers, json=payload, timeout=60)
|
| 140 |
+
|
| 141 |
+
# Handle model loading (HF free tier may need to load model)
|
| 142 |
+
if response.status_code == 503:
|
| 143 |
+
data = response.json()
|
| 144 |
+
wait_time = data.get("estimated_time", 20)
|
| 145 |
+
print(f"Model loading, waiting {wait_time}s...")
|
| 146 |
+
import time
|
| 147 |
+
time.sleep(min(wait_time, 30))
|
| 148 |
+
response = requests.post(api_url, headers=headers, json=payload, timeout=60)
|
| 149 |
+
|
| 150 |
+
response.raise_for_status()
|
| 151 |
+
data = response.json()
|
| 152 |
+
|
| 153 |
+
# Handle different response formats
|
| 154 |
+
if isinstance(data, list) and len(data) > 0:
|
| 155 |
+
return data[0].get("generated_text", "").strip()
|
| 156 |
+
elif isinstance(data, dict):
|
| 157 |
+
return data.get("generated_text", "").strip()
|
| 158 |
+
|
| 159 |
+
return self._fallback_response(prompt)
|
| 160 |
+
|
| 161 |
+
except Exception as e:
|
| 162 |
+
print(f"HuggingFace API error: {e}")
|
| 163 |
+
return self._fallback_response(prompt)
|
| 164 |
+
|
| 165 |
+
def _fallback_response(self, prompt: str) -> str:
|
| 166 |
+
"""Generate a basic response when API is unavailable."""
|
| 167 |
+
return FallbackLLM()._call(prompt)
|
| 168 |
+
|
| 169 |
+
@property
|
| 170 |
+
def _llm_type(self) -> str:
|
| 171 |
+
return "huggingface_inference"
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
# ==================== DeepSeek LLM ====================
|
| 175 |
+
|
| 176 |
+
DEEPSEEK_API_URL = "https://api.deepseek.com/v1/chat/completions"
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
class DeepSeekLLM(LLM):
|
| 180 |
+
"""LLM wrapper for DeepSeek API."""
|
| 181 |
+
api_key: str = ""
|
| 182 |
+
temperature: float = 0.7
|
| 183 |
+
max_tokens: int = 512
|
| 184 |
+
|
| 185 |
+
def __init__(self, **kwargs):
|
| 186 |
+
super().__init__(**kwargs)
|
| 187 |
+
self.api_key = os.getenv("DEEPSEEK_API_KEY", "")
|
| 188 |
+
if self.api_key:
|
| 189 |
+
print("Using DeepSeek API")
|
| 190 |
+
|
| 191 |
+
def _call(self, prompt: str, stop: Optional[List[str]] = None, **kwargs) -> str:
|
| 192 |
+
if not self.api_key:
|
| 193 |
+
return FallbackLLM()._call(prompt)
|
| 194 |
+
|
| 195 |
+
headers = {
|
| 196 |
+
"Authorization": f"Bearer {self.api_key}",
|
| 197 |
+
"Content-Type": "application/json"
|
| 198 |
+
}
|
| 199 |
+
payload = {
|
| 200 |
+
"model": "deepseek-chat",
|
| 201 |
+
"messages": [
|
| 202 |
+
{"role": "system", "content": "You are an AI trading advisor in the TradeVerse financial ecosystem."},
|
| 203 |
+
{"role": "user", "content": prompt}
|
| 204 |
+
],
|
| 205 |
+
"temperature": self.temperature,
|
| 206 |
+
"max_tokens": self.max_tokens
|
| 207 |
+
}
|
| 208 |
+
|
| 209 |
+
try:
|
| 210 |
+
response = requests.post(DEEPSEEK_API_URL, headers=headers, json=payload, timeout=30)
|
| 211 |
+
response.raise_for_status()
|
| 212 |
+
data = response.json()
|
| 213 |
+
return data["choices"][0]["message"]["content"].strip()
|
| 214 |
+
except Exception as e:
|
| 215 |
+
print(f"DeepSeek API error: {e}")
|
| 216 |
+
return FallbackLLM()._call(prompt)
|
| 217 |
+
|
| 218 |
+
@property
|
| 219 |
+
def _llm_type(self) -> str:
|
| 220 |
+
return "deepseek_api"
|
| 221 |
+
|
| 222 |
+
|
| 223 |
+
# ==================== Fallback LLM (Rule-based) ====================
|
| 224 |
+
|
| 225 |
+
class FallbackLLM(LLM):
|
| 226 |
+
"""
|
| 227 |
+
Rule-based fallback when no API is available.
|
| 228 |
+
Generates responses based on scenario context and parameters.
|
| 229 |
+
"""
|
| 230 |
+
|
| 231 |
+
def _call(self, prompt: str, stop: Optional[List[str]] = None, **kwargs) -> str:
|
| 232 |
+
"""Generate a context-aware response without an LLM."""
|
| 233 |
+
prompt_lower = prompt.lower()
|
| 234 |
+
|
| 235 |
+
# Detect recommendation requests
|
| 236 |
+
if "buy" in prompt_lower and "recommend" in prompt_lower:
|
| 237 |
+
return self._generate_buy_response(prompt)
|
| 238 |
+
elif "sell" in prompt_lower and "recommend" in prompt_lower:
|
| 239 |
+
return self._generate_sell_response(prompt)
|
| 240 |
+
elif "hold" in prompt_lower and "recommend" in prompt_lower:
|
| 241 |
+
return self._generate_hold_response(prompt)
|
| 242 |
+
|
| 243 |
+
# Detect question types
|
| 244 |
+
if "risk" in prompt_lower:
|
| 245 |
+
return "When evaluating risk, consider the company's debt levels, market volatility, and any red flags like insider selling or unusual trading volume. The current scenario presents factors that warrant careful consideration."
|
| 246 |
+
|
| 247 |
+
if "insider" in prompt_lower or "trading volume" in prompt_lower:
|
| 248 |
+
return "Unusual insider activity or trading volume can signal that informed parties have information not yet public. This is often a warning sign that warrants caution."
|
| 249 |
+
|
| 250 |
+
if "sector" in prompt_lower or "industry" in prompt_lower:
|
| 251 |
+
return "Sector trends significantly impact individual companies. Consider broader market conditions, regulatory environment, and competitive dynamics when making your decision."
|
| 252 |
+
|
| 253 |
+
# Default analytical response
|
| 254 |
+
return "Based on the available information, I'd encourage you to weigh the key factors mentioned in the scenario. Consider both the potential opportunities and the risk factors before making your decision."
|
| 255 |
+
|
| 256 |
+
def _generate_buy_response(self, prompt: str) -> str:
|
| 257 |
+
return "Based on my analysis, buying could be appropriate here. The positive signals suggest potential upside, though you should consider your risk tolerance and the size of your position carefully."
|
| 258 |
+
|
| 259 |
+
def _generate_sell_response(self, prompt: str) -> str:
|
| 260 |
+
return "Based on my analysis, selling may be prudent. The risk factors present suggest potential downside that could outweigh staying invested. Consider protecting your capital."
|
| 261 |
+
|
| 262 |
+
def _generate_hold_response(self, prompt: str) -> str:
|
| 263 |
+
return "Based on my analysis, holding your position seems reasonable. The situation shows mixed signals, and waiting for more clarity before acting could be the wisest approach."
|
| 264 |
+
|
| 265 |
+
@property
|
| 266 |
+
def _llm_type(self) -> str:
|
| 267 |
+
return "fallback_rules"
|
| 268 |
+
|
| 269 |
+
|
| 270 |
+
# ==================== LLM Factory ====================
|
| 271 |
+
|
| 272 |
+
def create_llm(provider: LLMProvider = None, model_id: str = None) -> LLM:
|
| 273 |
+
"""Factory function to create the appropriate LLM instance."""
|
| 274 |
+
if provider is None:
|
| 275 |
+
provider = get_llm_provider()
|
| 276 |
+
|
| 277 |
+
if provider == LLMProvider.HUGGINGFACE:
|
| 278 |
+
return HuggingFaceLLM(model_id=model_id)
|
| 279 |
+
elif provider == LLMProvider.DEEPSEEK:
|
| 280 |
+
return DeepSeekLLM()
|
| 281 |
+
else:
|
| 282 |
+
return FallbackLLM()
|
| 283 |
+
|
| 284 |
+
|
| 285 |
+
# ==================== Chat Response ====================
|
| 286 |
+
|
| 287 |
+
@dataclass
|
| 288 |
+
class ChatResponse:
|
| 289 |
+
"""Response from the chatbot."""
|
| 290 |
+
message: str
|
| 291 |
+
is_proactive: bool
|
| 292 |
+
confidence_level: str # "low", "medium", "high"
|
| 293 |
+
sources_used: List[str]
|
| 294 |
+
|
| 295 |
+
|
| 296 |
+
# ==================== Trading Chatbot ====================
|
| 297 |
+
|
| 298 |
+
class TradingChatbot:
|
| 299 |
+
"""
|
| 300 |
+
AI Chatbot for the trading experiment.
|
| 301 |
+
Supports both proactive advice and reactive queries.
|
| 302 |
+
"""
|
| 303 |
+
|
| 304 |
+
def __init__(self, llm_provider: LLMProvider = None, model_id: str = None):
|
| 305 |
+
self.llm = create_llm(llm_provider, model_id)
|
| 306 |
+
self.vectorstore = None
|
| 307 |
+
self.chat_history: List[Tuple[str, str]] = []
|
| 308 |
+
self._initialize_knowledge_base()
|
| 309 |
+
|
| 310 |
+
def _initialize_knowledge_base(self):
|
| 311 |
+
"""Load and index the knowledge base documents."""
|
| 312 |
+
docs = []
|
| 313 |
+
|
| 314 |
+
if os.path.exists(KNOWLEDGE_BASE_DIR):
|
| 315 |
+
for filename in os.listdir(KNOWLEDGE_BASE_DIR):
|
| 316 |
+
if filename.endswith(".txt"):
|
| 317 |
+
filepath = os.path.join(KNOWLEDGE_BASE_DIR, filename)
|
| 318 |
+
try:
|
| 319 |
+
loader = TextLoader(filepath)
|
| 320 |
+
docs.extend(loader.load())
|
| 321 |
+
except Exception as e:
|
| 322 |
+
print(f"Error loading {filename}: {e}")
|
| 323 |
+
|
| 324 |
+
if not docs:
|
| 325 |
+
print("Warning: No knowledge base documents found.")
|
| 326 |
+
return
|
| 327 |
+
|
| 328 |
+
# Split documents into chunks
|
| 329 |
+
splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
|
| 330 |
+
split_docs = splitter.split_documents(docs)
|
| 331 |
+
|
| 332 |
+
texts = [doc.page_content for doc in split_docs]
|
| 333 |
+
metadatas = [{"source": doc.metadata.get("source", "unknown")} for doc in split_docs]
|
| 334 |
+
|
| 335 |
+
# Create embeddings and vectorstore
|
| 336 |
+
try:
|
| 337 |
+
embedding_function = HuggingFaceEmbeddings(
|
| 338 |
+
model_name="sentence-transformers/all-MiniLM-L6-v2"
|
| 339 |
+
)
|
| 340 |
+
|
| 341 |
+
os.makedirs(VECTOR_DB_DIR, exist_ok=True)
|
| 342 |
+
self.vectorstore = Chroma(
|
| 343 |
+
persist_directory=VECTOR_DB_DIR,
|
| 344 |
+
embedding_function=embedding_function
|
| 345 |
+
)
|
| 346 |
+
self.vectorstore.add_texts(texts=texts, metadatas=metadatas)
|
| 347 |
+
self.vectorstore.persist()
|
| 348 |
+
print(f"Knowledge base initialized with {len(texts)} chunks.")
|
| 349 |
+
except Exception as e:
|
| 350 |
+
print(f"Error initializing vectorstore: {e}")
|
| 351 |
+
|
| 352 |
+
def _get_confidence_framing(self, level: int) -> Dict[str, str]:
|
| 353 |
+
"""Get language framing based on confidence parameter."""
|
| 354 |
+
if level < 34:
|
| 355 |
+
return {
|
| 356 |
+
"prefix": "Based on the available information, one possibility is that",
|
| 357 |
+
"verb": "might consider",
|
| 358 |
+
"qualifier": "though there is considerable uncertainty",
|
| 359 |
+
"level": "low"
|
| 360 |
+
}
|
| 361 |
+
elif level < 67:
|
| 362 |
+
return {
|
| 363 |
+
"prefix": "Looking at the situation,",
|
| 364 |
+
"verb": "suggests",
|
| 365 |
+
"qualifier": "while noting some risk factors",
|
| 366 |
+
"level": "medium"
|
| 367 |
+
}
|
| 368 |
+
else:
|
| 369 |
+
return {
|
| 370 |
+
"prefix": "Based on my analysis,",
|
| 371 |
+
"verb": "strongly recommend",
|
| 372 |
+
"qualifier": "with high confidence",
|
| 373 |
+
"level": "high"
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
def _get_depth_instructions(self, level: int) -> str:
|
| 377 |
+
"""Get explanation depth instructions based on parameter."""
|
| 378 |
+
if level < 34:
|
| 379 |
+
return "Provide a very brief response (1-2 sentences maximum). Focus only on the key point."
|
| 380 |
+
elif level < 67:
|
| 381 |
+
return "Provide a moderate explanation (3-4 sentences). Include the main reasoning and key factors."
|
| 382 |
+
else:
|
| 383 |
+
return "Provide a detailed analysis. Cover all relevant factors, risks, and opportunities comprehensively."
|
| 384 |
+
|
| 385 |
+
def _get_risk_framing(self, level: int) -> str:
|
| 386 |
+
"""Get risk perspective based on parameter."""
|
| 387 |
+
if level < 34:
|
| 388 |
+
return "Emphasize potential risks and downside scenarios. Favor capital preservation over potential gains."
|
| 389 |
+
elif level < 67:
|
| 390 |
+
return "Balance potential risks and opportunities. Present a measured risk-reward analysis."
|
| 391 |
+
else:
|
| 392 |
+
return "Emphasize potential opportunities and upside. Be willing to tolerate higher risk for potential gains."
|
| 393 |
+
|
| 394 |
+
def _get_style_instructions(self, level: int) -> str:
|
| 395 |
+
"""Get communication style instructions based on parameter."""
|
| 396 |
+
if level < 34:
|
| 397 |
+
return "Use formal, professional language. Be precise and measured in your statements."
|
| 398 |
+
elif level < 67:
|
| 399 |
+
return "Use clear, accessible language. Be professional but approachable."
|
| 400 |
+
else:
|
| 401 |
+
return "Use conversational, friendly language. Be direct and engaging."
|
| 402 |
+
|
| 403 |
+
def _retrieve_context(self, query: str, k: int = 4) -> str:
|
| 404 |
+
"""Retrieve relevant context from the knowledge base."""
|
| 405 |
+
if not self.vectorstore:
|
| 406 |
+
return ""
|
| 407 |
+
|
| 408 |
+
try:
|
| 409 |
+
docs = self.vectorstore.similarity_search(query, k=k)
|
| 410 |
+
return "\n\n".join([doc.page_content for doc in docs])
|
| 411 |
+
except Exception as e:
|
| 412 |
+
print(f"Error retrieving context: {e}")
|
| 413 |
+
return ""
|
| 414 |
+
|
| 415 |
+
def generate_proactive_advice(
|
| 416 |
+
self,
|
| 417 |
+
scenario: Scenario,
|
| 418 |
+
visible_params: ParticipantVisibleParams,
|
| 419 |
+
hidden_params: ResearcherControlledParams
|
| 420 |
+
) -> Optional[ChatResponse]:
|
| 421 |
+
"""
|
| 422 |
+
Generate proactive advice for a scenario.
|
| 423 |
+
Returns None if proactive advice should not be shown.
|
| 424 |
+
"""
|
| 425 |
+
# Check if we should show proactive advice based on proactivity level
|
| 426 |
+
proactive_threshold = hidden_params.proactivity_level / 100
|
| 427 |
+
if random.random() > proactive_threshold:
|
| 428 |
+
return None
|
| 429 |
+
|
| 430 |
+
# Build the prompt
|
| 431 |
+
confidence = self._get_confidence_framing(hidden_params.confidence_framing)
|
| 432 |
+
depth = self._get_depth_instructions(visible_params.explanation_depth)
|
| 433 |
+
risk = self._get_risk_framing(hidden_params.risk_bias)
|
| 434 |
+
style = self._get_style_instructions(visible_params.communication_style)
|
| 435 |
+
|
| 436 |
+
# Retrieve relevant context
|
| 437 |
+
context = self._retrieve_context(
|
| 438 |
+
f"{scenario.company_name} {scenario.sector} trading analysis"
|
| 439 |
+
)
|
| 440 |
+
|
| 441 |
+
# Determine what factors to highlight
|
| 442 |
+
factors_to_mention = []
|
| 443 |
+
if hidden_params.risk_bias < 50:
|
| 444 |
+
factors_to_mention = scenario.red_flags[:2] if scenario.red_flags else scenario.key_factors[:2]
|
| 445 |
+
else:
|
| 446 |
+
factors_to_mention = scenario.positive_signals[:2] if scenario.positive_signals else scenario.key_factors[:2]
|
| 447 |
+
|
| 448 |
+
prompt = f"""
|
| 449 |
+
{style}
|
| 450 |
+
{depth}
|
| 451 |
+
{risk}
|
| 452 |
+
|
| 453 |
+
You are an AI trading advisor. A participant is viewing this trading scenario:
|
| 454 |
+
|
| 455 |
+
Company: {scenario.company_name} ({scenario.company_symbol})
|
| 456 |
+
Sector: {scenario.sector}
|
| 457 |
+
Country: {scenario.country}
|
| 458 |
+
Current Price: {scenario.current_price} credits
|
| 459 |
+
|
| 460 |
+
Situation:
|
| 461 |
+
{scenario.situation_description}
|
| 462 |
+
|
| 463 |
+
Key factors to consider: {', '.join(factors_to_mention)}
|
| 464 |
+
|
| 465 |
+
Relevant knowledge:
|
| 466 |
+
{context}
|
| 467 |
+
|
| 468 |
+
You should proactively offer some initial observations about this situation.
|
| 469 |
+
{confidence['prefix']} the situation {confidence['verb']} careful attention {confidence['qualifier']}.
|
| 470 |
+
|
| 471 |
+
Your recommendation should lean toward: {scenario.ai_recommendation}
|
| 472 |
+
|
| 473 |
+
Generate a brief proactive message offering your initial take on this situation.
|
| 474 |
+
Do NOT explicitly tell them to BUY, SELL, or HOLD yet - this is an initial observation.
|
| 475 |
+
Keep it natural, as if you're an advisor noticing something they should be aware of.
|
| 476 |
+
"""
|
| 477 |
+
|
| 478 |
+
response_text = self.llm._call(prompt)
|
| 479 |
+
|
| 480 |
+
return ChatResponse(
|
| 481 |
+
message=response_text,
|
| 482 |
+
is_proactive=True,
|
| 483 |
+
confidence_level=confidence["level"],
|
| 484 |
+
sources_used=["market_context", "company_profile"]
|
| 485 |
+
)
|
| 486 |
+
|
| 487 |
+
def generate_ai_recommendation(
|
| 488 |
+
self,
|
| 489 |
+
scenario: Scenario,
|
| 490 |
+
visible_params: ParticipantVisibleParams,
|
| 491 |
+
hidden_params: ResearcherControlledParams
|
| 492 |
+
) -> ChatResponse:
|
| 493 |
+
"""
|
| 494 |
+
Generate the AI's recommendation for a scenario.
|
| 495 |
+
This is the main advice given before the participant decides.
|
| 496 |
+
"""
|
| 497 |
+
confidence = self._get_confidence_framing(hidden_params.confidence_framing)
|
| 498 |
+
depth = self._get_depth_instructions(visible_params.explanation_depth)
|
| 499 |
+
risk = self._get_risk_framing(hidden_params.risk_bias)
|
| 500 |
+
style = self._get_style_instructions(visible_params.communication_style)
|
| 501 |
+
|
| 502 |
+
context = self._retrieve_context(
|
| 503 |
+
f"{scenario.company_name} {scenario.sector} {scenario.ai_recommendation}"
|
| 504 |
+
)
|
| 505 |
+
|
| 506 |
+
prompt = f"""
|
| 507 |
+
{style}
|
| 508 |
+
{depth}
|
| 509 |
+
{risk}
|
| 510 |
+
|
| 511 |
+
You are an AI trading advisor. Analyze this situation and provide your recommendation:
|
| 512 |
+
|
| 513 |
+
Company: {scenario.company_name} ({scenario.company_symbol})
|
| 514 |
+
Sector: {scenario.sector}
|
| 515 |
+
Country: {scenario.country}
|
| 516 |
+
Current Price: {scenario.current_price} credits
|
| 517 |
+
|
| 518 |
+
Situation:
|
| 519 |
+
{scenario.situation_description}
|
| 520 |
+
|
| 521 |
+
Key factors: {', '.join(scenario.key_factors)}
|
| 522 |
+
Warning signs: {', '.join(scenario.red_flags) if scenario.red_flags else 'None identified'}
|
| 523 |
+
Positive signals: {', '.join(scenario.positive_signals) if scenario.positive_signals else 'None identified'}
|
| 524 |
+
|
| 525 |
+
Relevant market knowledge:
|
| 526 |
+
{context}
|
| 527 |
+
|
| 528 |
+
{confidence['prefix']} I {confidence['verb']} the participant to {scenario.ai_recommendation} {confidence['qualifier']}.
|
| 529 |
+
|
| 530 |
+
Generate your recommendation. Be clear about your suggested action ({scenario.ai_recommendation}).
|
| 531 |
+
Explain your reasoning according to the depth level specified.
|
| 532 |
+
Frame risks according to the risk perspective specified.
|
| 533 |
+
"""
|
| 534 |
+
|
| 535 |
+
response_text = self.llm._call(prompt)
|
| 536 |
+
|
| 537 |
+
return ChatResponse(
|
| 538 |
+
message=response_text,
|
| 539 |
+
is_proactive=False,
|
| 540 |
+
confidence_level=confidence["level"],
|
| 541 |
+
sources_used=["market_context", "company_profile", "trading_basics"]
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
def answer_query(
|
| 545 |
+
self,
|
| 546 |
+
query: str,
|
| 547 |
+
scenario: Optional[Scenario],
|
| 548 |
+
visible_params: ParticipantVisibleParams,
|
| 549 |
+
hidden_params: ResearcherControlledParams
|
| 550 |
+
) -> ChatResponse:
|
| 551 |
+
"""
|
| 552 |
+
Answer a participant's question (reactive query).
|
| 553 |
+
"""
|
| 554 |
+
depth = self._get_depth_instructions(visible_params.explanation_depth)
|
| 555 |
+
style = self._get_style_instructions(visible_params.communication_style)
|
| 556 |
+
risk = self._get_risk_framing(hidden_params.risk_bias)
|
| 557 |
+
confidence = self._get_confidence_framing(hidden_params.confidence_framing)
|
| 558 |
+
|
| 559 |
+
# Retrieve context based on the query
|
| 560 |
+
context = self._retrieve_context(query)
|
| 561 |
+
|
| 562 |
+
# Build scenario context if available
|
| 563 |
+
scenario_context = ""
|
| 564 |
+
if scenario:
|
| 565 |
+
scenario_context = f"""
|
| 566 |
+
Current scenario:
|
| 567 |
+
Company: {scenario.company_name} ({scenario.company_symbol})
|
| 568 |
+
Sector: {scenario.sector}
|
| 569 |
+
Situation: {scenario.situation_description}
|
| 570 |
+
"""
|
| 571 |
+
|
| 572 |
+
# Include chat history for context
|
| 573 |
+
history_context = ""
|
| 574 |
+
if self.chat_history:
|
| 575 |
+
recent_history = self.chat_history[-3:] # Last 3 exchanges
|
| 576 |
+
history_context = "Recent conversation:\n" + "\n".join(
|
| 577 |
+
[f"User: {q}\nAI: {a}" for q, a in recent_history]
|
| 578 |
+
)
|
| 579 |
+
|
| 580 |
+
prompt = f"""
|
| 581 |
+
{style}
|
| 582 |
+
{depth}
|
| 583 |
+
{risk}
|
| 584 |
+
|
| 585 |
+
You are an AI trading advisor in the TradeVerse. Answer the participant's question.
|
| 586 |
+
|
| 587 |
+
{scenario_context}
|
| 588 |
+
|
| 589 |
+
{history_context}
|
| 590 |
+
|
| 591 |
+
Relevant knowledge from your database:
|
| 592 |
+
{context}
|
| 593 |
+
|
| 594 |
+
User question: {query}
|
| 595 |
+
|
| 596 |
+
Guidelines:
|
| 597 |
+
- Only use information from the TradeVerse (fictional universe)
|
| 598 |
+
- If asked about real-world companies or markets, politely redirect to TradeVerse
|
| 599 |
+
- {confidence['prefix'].lower()} frame your response {confidence['qualifier']}
|
| 600 |
+
- Be helpful but don't make decisions for the participant
|
| 601 |
+
|
| 602 |
+
Provide your response:
|
| 603 |
+
"""
|
| 604 |
+
|
| 605 |
+
response_text = self.llm._call(prompt)
|
| 606 |
+
|
| 607 |
+
# Update chat history
|
| 608 |
+
self.chat_history.append((query, response_text))
|
| 609 |
+
|
| 610 |
+
return ChatResponse(
|
| 611 |
+
message=response_text,
|
| 612 |
+
is_proactive=False,
|
| 613 |
+
confidence_level=confidence["level"],
|
| 614 |
+
sources_used=["knowledge_base"]
|
| 615 |
+
)
|
| 616 |
+
|
| 617 |
+
def clear_history(self):
|
| 618 |
+
"""Clear the chat history for a new session."""
|
| 619 |
+
self.chat_history = []
|
| 620 |
+
|
| 621 |
+
|
| 622 |
+
# Singleton instance (uses auto-detected provider)
|
| 623 |
+
chatbot = TradingChatbot()
|
config.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Configuration module for the AI Trading Experiment.
|
| 3 |
+
Contains both participant-visible and researcher-controlled parameters.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from dataclasses import dataclass, field
|
| 7 |
+
from typing import Dict, List, Optional
|
| 8 |
+
import json
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@dataclass
|
| 12 |
+
class ParticipantVisibleParams:
|
| 13 |
+
"""Parameters that participants can adjust."""
|
| 14 |
+
explanation_depth: int = 50 # 0-100: Minimal to Detailed
|
| 15 |
+
communication_style: int = 50 # 0-100: Formal to Casual
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class ResearcherControlledParams:
|
| 20 |
+
"""Hidden parameters controlled by researcher for experimental conditions."""
|
| 21 |
+
accuracy_rate: float = 0.7 # 0-1: Probability AI advice leads to good outcome
|
| 22 |
+
proactivity_level: int = 50 # 0-100: How often AI initiates advice
|
| 23 |
+
confidence_framing: int = 50 # 0-100: How assertive advice sounds
|
| 24 |
+
risk_bias: int = 50 # 0-100: Conservative to Aggressive recommendations
|
| 25 |
+
|
| 26 |
+
# Experimental condition identifier
|
| 27 |
+
condition_name: str = "default"
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass
|
| 31 |
+
class ExperimentConfig:
|
| 32 |
+
"""Overall experiment configuration."""
|
| 33 |
+
# Starting conditions
|
| 34 |
+
initial_portfolio_value: float = 100000.0
|
| 35 |
+
currency_symbol: str = "credits"
|
| 36 |
+
|
| 37 |
+
# Session settings
|
| 38 |
+
scenarios_per_session: int = 8
|
| 39 |
+
min_decision_time_seconds: float = 5.0 # Minimum time before decision allowed
|
| 40 |
+
|
| 41 |
+
# Proactive AI settings
|
| 42 |
+
proactive_delay_seconds: float = 3.0 # Delay before AI offers proactive advice
|
| 43 |
+
proactive_probability: float = 0.7 # Base probability of proactive advice per scenario
|
| 44 |
+
|
| 45 |
+
# Data collection
|
| 46 |
+
track_mouse_movements: bool = False # Future feature
|
| 47 |
+
track_time_on_elements: bool = True
|
| 48 |
+
|
| 49 |
+
# UI settings
|
| 50 |
+
show_portfolio_history: bool = True
|
| 51 |
+
show_ai_accuracy_feedback: bool = False # Whether to reveal if AI was right
|
| 52 |
+
|
| 53 |
+
# Researcher params (can be overridden per experiment condition)
|
| 54 |
+
researcher_params: ResearcherControlledParams = field(
|
| 55 |
+
default_factory=ResearcherControlledParams
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# Pre-defined experimental conditions for A/B testing
|
| 60 |
+
EXPERIMENT_CONDITIONS: Dict[str, ResearcherControlledParams] = {
|
| 61 |
+
"high_accuracy_high_confidence": ResearcherControlledParams(
|
| 62 |
+
accuracy_rate=0.85,
|
| 63 |
+
proactivity_level=70,
|
| 64 |
+
confidence_framing=80,
|
| 65 |
+
risk_bias=50,
|
| 66 |
+
condition_name="high_accuracy_high_confidence"
|
| 67 |
+
),
|
| 68 |
+
"high_accuracy_low_confidence": ResearcherControlledParams(
|
| 69 |
+
accuracy_rate=0.85,
|
| 70 |
+
proactivity_level=70,
|
| 71 |
+
confidence_framing=20,
|
| 72 |
+
risk_bias=50,
|
| 73 |
+
condition_name="high_accuracy_low_confidence"
|
| 74 |
+
),
|
| 75 |
+
"low_accuracy_high_confidence": ResearcherControlledParams(
|
| 76 |
+
accuracy_rate=0.40,
|
| 77 |
+
proactivity_level=70,
|
| 78 |
+
confidence_framing=80,
|
| 79 |
+
risk_bias=50,
|
| 80 |
+
condition_name="low_accuracy_high_confidence"
|
| 81 |
+
),
|
| 82 |
+
"low_accuracy_low_confidence": ResearcherControlledParams(
|
| 83 |
+
accuracy_rate=0.40,
|
| 84 |
+
proactivity_level=70,
|
| 85 |
+
confidence_framing=20,
|
| 86 |
+
risk_bias=50,
|
| 87 |
+
condition_name="low_accuracy_low_confidence"
|
| 88 |
+
),
|
| 89 |
+
"conservative_advisor": ResearcherControlledParams(
|
| 90 |
+
accuracy_rate=0.70,
|
| 91 |
+
proactivity_level=50,
|
| 92 |
+
confidence_framing=50,
|
| 93 |
+
risk_bias=20,
|
| 94 |
+
condition_name="conservative_advisor"
|
| 95 |
+
),
|
| 96 |
+
"aggressive_advisor": ResearcherControlledParams(
|
| 97 |
+
accuracy_rate=0.70,
|
| 98 |
+
proactivity_level=50,
|
| 99 |
+
confidence_framing=50,
|
| 100 |
+
risk_bias=80,
|
| 101 |
+
condition_name="aggressive_advisor"
|
| 102 |
+
),
|
| 103 |
+
"passive_advisor": ResearcherControlledParams(
|
| 104 |
+
accuracy_rate=0.70,
|
| 105 |
+
proactivity_level=20,
|
| 106 |
+
confidence_framing=50,
|
| 107 |
+
risk_bias=50,
|
| 108 |
+
condition_name="passive_advisor"
|
| 109 |
+
),
|
| 110 |
+
"active_advisor": ResearcherControlledParams(
|
| 111 |
+
accuracy_rate=0.70,
|
| 112 |
+
proactivity_level=80,
|
| 113 |
+
confidence_framing=50,
|
| 114 |
+
risk_bias=50,
|
| 115 |
+
condition_name="active_advisor"
|
| 116 |
+
),
|
| 117 |
+
}
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
@dataclass
|
| 121 |
+
class Scenario:
|
| 122 |
+
"""A trading scenario presented to the participant."""
|
| 123 |
+
scenario_id: str
|
| 124 |
+
company_symbol: str
|
| 125 |
+
company_name: str
|
| 126 |
+
sector: str
|
| 127 |
+
country: str
|
| 128 |
+
current_price: float
|
| 129 |
+
situation_description: str
|
| 130 |
+
|
| 131 |
+
# Hidden from participant - determines outcomes
|
| 132 |
+
optimal_action: str # "BUY", "SELL", or "HOLD"
|
| 133 |
+
ai_recommendation: str # What AI will recommend (may differ from optimal)
|
| 134 |
+
ai_is_correct: bool # Whether AI recommendation matches optimal
|
| 135 |
+
|
| 136 |
+
# Outcome percentages based on decision
|
| 137 |
+
outcome_buy: float # e.g., -0.08 for -8%
|
| 138 |
+
outcome_sell: float
|
| 139 |
+
outcome_hold: float
|
| 140 |
+
|
| 141 |
+
# Additional context for AI reasoning
|
| 142 |
+
key_factors: List[str] = field(default_factory=list)
|
| 143 |
+
red_flags: List[str] = field(default_factory=list)
|
| 144 |
+
positive_signals: List[str] = field(default_factory=list)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
# Pre-defined scenarios
|
| 148 |
+
SCENARIOS: List[Scenario] = [
|
| 149 |
+
Scenario(
|
| 150 |
+
scenario_id="S001",
|
| 151 |
+
company_symbol="MRD",
|
| 152 |
+
company_name="Meridian Industries",
|
| 153 |
+
sector="Manufacturing",
|
| 154 |
+
country="Republic of Valdoria",
|
| 155 |
+
current_price=52.30,
|
| 156 |
+
situation_description="""Meridian Industries recently announced a 5-year defense contract worth 2.1B credits.
|
| 157 |
+
The stock has risen 12% over the past month on this news. Labor disputes at the southern facility
|
| 158 |
+
have been resolved. The company maintains a healthy balance sheet with low debt.""",
|
| 159 |
+
optimal_action="HOLD",
|
| 160 |
+
ai_recommendation="HOLD",
|
| 161 |
+
ai_is_correct=True,
|
| 162 |
+
outcome_buy=-0.035, # -3.5% (already priced in)
|
| 163 |
+
outcome_sell=-0.042, # Miss further gains
|
| 164 |
+
outcome_hold=0.032, # +3.2% steady growth
|
| 165 |
+
key_factors=["Defense contract secured", "Labor issues resolved", "Strong balance sheet"],
|
| 166 |
+
red_flags=[],
|
| 167 |
+
positive_signals=["Government contract", "Low debt", "Stable management"]
|
| 168 |
+
),
|
| 169 |
+
Scenario(
|
| 170 |
+
scenario_id="S002",
|
| 171 |
+
company_symbol="NGP",
|
| 172 |
+
company_name="Northgate Pharmaceuticals",
|
| 173 |
+
sector="Healthcare",
|
| 174 |
+
country="Federation of Eastholm",
|
| 175 |
+
current_price=85.50,
|
| 176 |
+
situation_description="""Northgate Pharmaceuticals has Phase 3 trial results expected next month for their
|
| 177 |
+
flagship rare disease treatment. However, unusual trading volume was detected last week, a board member
|
| 178 |
+
resigned citing "personal reasons," and the CFO sold shares worth 500K credits. Competitor Apex Biotech
|
| 179 |
+
announced a similar drug in development.""",
|
| 180 |
+
optimal_action="SELL",
|
| 181 |
+
ai_recommendation="BUY", # AI gives bad advice here
|
| 182 |
+
ai_is_correct=False,
|
| 183 |
+
outcome_buy=-0.153, # -15.3% (trial fails)
|
| 184 |
+
outcome_sell=0.153, # +15.3% (avoided loss)
|
| 185 |
+
outcome_hold=-0.153, # Same as buy
|
| 186 |
+
key_factors=["Phase 3 trial pending", "Insider activity", "Competitive pressure"],
|
| 187 |
+
red_flags=["Board member resignation", "CFO share sale", "Unusual trading volume"],
|
| 188 |
+
positive_signals=["Strong pipeline", "Rare disease focus"]
|
| 189 |
+
),
|
| 190 |
+
Scenario(
|
| 191 |
+
scenario_id="S003",
|
| 192 |
+
company_symbol="TFE",
|
| 193 |
+
company_name="Terraform Energy",
|
| 194 |
+
sector="Energy",
|
| 195 |
+
country="United Provinces of Somara",
|
| 196 |
+
current_price=132.80,
|
| 197 |
+
situation_description="""Terraform Energy's fusion prototype achieved a 40% efficiency milestone, a significant
|
| 198 |
+
breakthrough. The company has government subsidies locked in for 10 years and is awaiting regulatory
|
| 199 |
+
approval for expansion into Valdoria. Last quarter's earnings beat expectations by 12%.""",
|
| 200 |
+
optimal_action="BUY",
|
| 201 |
+
ai_recommendation="BUY",
|
| 202 |
+
ai_is_correct=True,
|
| 203 |
+
outcome_buy=0.124, # +12.4%
|
| 204 |
+
outcome_sell=-0.089, # Miss the rally
|
| 205 |
+
outcome_hold=0.056, # Partial gain
|
| 206 |
+
key_factors=["Fusion breakthrough", "Government support", "Expansion pending"],
|
| 207 |
+
red_flags=["Regulatory approval uncertainty"],
|
| 208 |
+
positive_signals=["Technology leadership", "Stable subsidies", "Earnings beat"]
|
| 209 |
+
),
|
| 210 |
+
Scenario(
|
| 211 |
+
scenario_id="S004",
|
| 212 |
+
company_symbol="NXC",
|
| 213 |
+
company_name="Nexus Communications",
|
| 214 |
+
sector="Technology",
|
| 215 |
+
country="Coastal Republic of Marinea",
|
| 216 |
+
current_price=218.40,
|
| 217 |
+
situation_description="""Nexus Communications holds a monopoly position in Marinea's telecommunications market.
|
| 218 |
+
There are unconfirmed rumors of an antitrust investigation. The company's 5G rollout is 60% complete,
|
| 219 |
+
and they're planning a satellite constellation launch. Subscriber growth is slowing in mature markets.""",
|
| 220 |
+
optimal_action="HOLD",
|
| 221 |
+
ai_recommendation="SELL", # AI is overly cautious
|
| 222 |
+
ai_is_correct=False,
|
| 223 |
+
outcome_buy=0.045, # Slight gain, rumors overblown
|
| 224 |
+
outcome_sell=-0.067, # Miss continued performance
|
| 225 |
+
outcome_hold=0.045, # Same as buy
|
| 226 |
+
key_factors=["Monopoly position", "Antitrust rumors", "5G expansion"],
|
| 227 |
+
red_flags=["Regulatory risk", "Slowing growth"],
|
| 228 |
+
positive_signals=["Strong cash reserves", "Infrastructure investments"]
|
| 229 |
+
),
|
| 230 |
+
Scenario(
|
| 231 |
+
scenario_id="S005",
|
| 232 |
+
company_symbol="APX",
|
| 233 |
+
company_name="Apex Biotech",
|
| 234 |
+
sector="Healthcare",
|
| 235 |
+
country="Republic of Valdoria",
|
| 236 |
+
current_price=71.20,
|
| 237 |
+
situation_description="""Apex Biotech announced a competing drug to Northgate's flagship product. The company
|
| 238 |
+
is integrating a recent acquisition and there are rumors they may be an acquisition target themselves.
|
| 239 |
+
The CFO sold shares worth 2M credits last month, though this was part of a pre-planned selling program.""",
|
| 240 |
+
optimal_action="HOLD",
|
| 241 |
+
ai_recommendation="HOLD",
|
| 242 |
+
ai_is_correct=True,
|
| 243 |
+
outcome_buy=-0.028, # Slight decline, uncertainty
|
| 244 |
+
outcome_sell=-0.015, # Miss potential upside
|
| 245 |
+
outcome_hold=0.018, # Modest gain
|
| 246 |
+
key_factors=["Competitive positioning", "M&A activity", "Insider sales"],
|
| 247 |
+
red_flags=["CFO share sale", "Integration risks"],
|
| 248 |
+
positive_signals=["Strong patent portfolio", "Acquisition rumors"]
|
| 249 |
+
),
|
| 250 |
+
Scenario(
|
| 251 |
+
scenario_id="S006",
|
| 252 |
+
company_symbol="ICS",
|
| 253 |
+
company_name="Ironclad Security",
|
| 254 |
+
sector="Defense",
|
| 255 |
+
country="Federation of Eastholm",
|
| 256 |
+
current_price=93.60,
|
| 257 |
+
situation_description="""Ironclad Security won a major cybersecurity contract with the Marinea government.
|
| 258 |
+
A cyber attack on a competitor has enhanced ICS's reputation. The company is planning to hire 500
|
| 259 |
+
new engineers. Their order backlog represents 18 months of revenue.""",
|
| 260 |
+
optimal_action="BUY",
|
| 261 |
+
ai_recommendation="BUY",
|
| 262 |
+
ai_is_correct=True,
|
| 263 |
+
outcome_buy=0.098, # +9.8%
|
| 264 |
+
outcome_sell=-0.065, # Miss rally
|
| 265 |
+
outcome_hold=0.042, # Partial gain
|
| 266 |
+
key_factors=["Major contract win", "Strong backlog", "Expansion hiring"],
|
| 267 |
+
red_flags=[],
|
| 268 |
+
positive_signals=["Government contracts", "Reputation boost", "Growth investment"]
|
| 269 |
+
),
|
| 270 |
+
Scenario(
|
| 271 |
+
scenario_id="S007",
|
| 272 |
+
company_symbol="CSC",
|
| 273 |
+
company_name="Coastal Shipping Corp",
|
| 274 |
+
sector="Transportation",
|
| 275 |
+
country="Coastal Republic of Marinea",
|
| 276 |
+
current_price=35.80,
|
| 277 |
+
situation_description="""Coastal Shipping Corp benefits from a new trade agreement with Somara. Their fuel
|
| 278 |
+
hedging strategy has reduced costs by 8%. However, there are ongoing port strike risks in Valdoria,
|
| 279 |
+
and trade treaty negotiations could impact several key routes. The company offers a high dividend yield.""",
|
| 280 |
+
optimal_action="HOLD",
|
| 281 |
+
ai_recommendation="BUY", # AI is too aggressive
|
| 282 |
+
ai_is_correct=False,
|
| 283 |
+
outcome_buy=-0.072, # Port strike materializes
|
| 284 |
+
outcome_sell=0.034, # Avoided loss
|
| 285 |
+
outcome_hold=-0.018, # Small loss
|
| 286 |
+
key_factors=["Trade agreements", "Fuel hedging", "Labor risks"],
|
| 287 |
+
red_flags=["Port strike risk", "Trade negotiations"],
|
| 288 |
+
positive_signals=["New trade deal", "Cost management", "High dividend"]
|
| 289 |
+
),
|
| 290 |
+
Scenario(
|
| 291 |
+
scenario_id="S008",
|
| 292 |
+
company_symbol="AUR",
|
| 293 |
+
company_name="Aurora Entertainment",
|
| 294 |
+
sector="Consumer Discretionary",
|
| 295 |
+
country="United Provinces of Somara",
|
| 296 |
+
current_price=61.40,
|
| 297 |
+
situation_description="""Aurora Entertainment's hit series "Valdoria Rising" broke viewership records.
|
| 298 |
+
However, their gaming division underperformed expectations. A new VR platform launches next quarter.
|
| 299 |
+
There are rumors Nexus Communications may acquire the company. Cash flow remains negative despite growth.""",
|
| 300 |
+
optimal_action="BUY",
|
| 301 |
+
ai_recommendation="HOLD", # AI is too conservative
|
| 302 |
+
ai_is_correct=False,
|
| 303 |
+
outcome_buy=0.186, # Acquisition announced
|
| 304 |
+
outcome_sell=-0.142, # Miss acquisition premium
|
| 305 |
+
outcome_hold=0.065, # Partial gain from rumors
|
| 306 |
+
key_factors=["Content success", "Gaming weakness", "Acquisition rumors"],
|
| 307 |
+
red_flags=["Negative cash flow", "Gaming underperformance"],
|
| 308 |
+
positive_signals=["Viewership records", "VR launch", "Acquisition interest"]
|
| 309 |
+
),
|
| 310 |
+
]
|
| 311 |
+
|
| 312 |
+
|
| 313 |
+
def get_scenario_by_id(scenario_id: str) -> Optional[Scenario]:
|
| 314 |
+
"""Retrieve a scenario by its ID."""
|
| 315 |
+
for scenario in SCENARIOS:
|
| 316 |
+
if scenario.scenario_id == scenario_id:
|
| 317 |
+
return scenario
|
| 318 |
+
return None
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def get_condition(condition_name: str) -> ResearcherControlledParams:
|
| 322 |
+
"""Get a pre-defined experimental condition."""
|
| 323 |
+
return EXPERIMENT_CONDITIONS.get(
|
| 324 |
+
condition_name,
|
| 325 |
+
ResearcherControlledParams()
|
| 326 |
+
)
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
def create_custom_condition(
|
| 330 |
+
accuracy_rate: float = 0.7,
|
| 331 |
+
proactivity_level: int = 50,
|
| 332 |
+
confidence_framing: int = 50,
|
| 333 |
+
risk_bias: int = 50,
|
| 334 |
+
name: str = "custom"
|
| 335 |
+
) -> ResearcherControlledParams:
|
| 336 |
+
"""Create a custom experimental condition."""
|
| 337 |
+
return ResearcherControlledParams(
|
| 338 |
+
accuracy_rate=accuracy_rate,
|
| 339 |
+
proactivity_level=proactivity_level,
|
| 340 |
+
confidence_framing=confidence_framing,
|
| 341 |
+
risk_bias=risk_bias,
|
| 342 |
+
condition_name=name
|
| 343 |
+
)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
# Default configuration instance
|
| 347 |
+
DEFAULT_CONFIG = ExperimentConfig()
|
export_data.py
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Data export utilities for the AI Trading Experiment.
|
| 3 |
+
Provides functions to export experiment data for statistical analysis.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import os
|
| 7 |
+
import sqlite3
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import Dict, List, Any, Optional
|
| 10 |
+
|
| 11 |
+
import pandas as pd
|
| 12 |
+
|
| 13 |
+
DATABASE_PATH = "db/experiment.db"
|
| 14 |
+
EXPORT_DIR = "exports"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def ensure_export_dir():
|
| 18 |
+
"""Ensure the export directory exists."""
|
| 19 |
+
os.makedirs(EXPORT_DIR, exist_ok=True)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def get_connection():
|
| 23 |
+
"""Get a database connection."""
|
| 24 |
+
return sqlite3.connect(DATABASE_PATH)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def export_sessions_csv() -> str:
|
| 28 |
+
"""Export all sessions to CSV."""
|
| 29 |
+
ensure_export_dir()
|
| 30 |
+
|
| 31 |
+
conn = get_connection()
|
| 32 |
+
df = pd.read_sql_query("SELECT * FROM sessions ORDER BY session_start", conn)
|
| 33 |
+
conn.close()
|
| 34 |
+
|
| 35 |
+
filename = f"{EXPORT_DIR}/sessions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 36 |
+
df.to_csv(filename, index=False)
|
| 37 |
+
print(f"Exported {len(df)} sessions to {filename}")
|
| 38 |
+
return filename
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def export_decisions_csv() -> str:
|
| 42 |
+
"""Export all decisions to CSV."""
|
| 43 |
+
ensure_export_dir()
|
| 44 |
+
|
| 45 |
+
conn = get_connection()
|
| 46 |
+
df = pd.read_sql_query("SELECT * FROM decisions ORDER BY timestamp", conn)
|
| 47 |
+
conn.close()
|
| 48 |
+
|
| 49 |
+
filename = f"{EXPORT_DIR}/decisions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 50 |
+
df.to_csv(filename, index=False)
|
| 51 |
+
print(f"Exported {len(df)} decisions to {filename}")
|
| 52 |
+
return filename
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def export_interactions_csv() -> str:
|
| 56 |
+
"""Export all chat interactions to CSV."""
|
| 57 |
+
ensure_export_dir()
|
| 58 |
+
|
| 59 |
+
conn = get_connection()
|
| 60 |
+
df = pd.read_sql_query("SELECT * FROM chat_interactions ORDER BY timestamp", conn)
|
| 61 |
+
conn.close()
|
| 62 |
+
|
| 63 |
+
filename = f"{EXPORT_DIR}/interactions_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 64 |
+
df.to_csv(filename, index=False)
|
| 65 |
+
print(f"Exported {len(df)} interactions to {filename}")
|
| 66 |
+
return filename
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def export_trust_metrics_csv() -> str:
|
| 70 |
+
"""Export trust metrics to CSV."""
|
| 71 |
+
ensure_export_dir()
|
| 72 |
+
|
| 73 |
+
conn = get_connection()
|
| 74 |
+
df = pd.read_sql_query("SELECT * FROM trust_metrics ORDER BY timestamp", conn)
|
| 75 |
+
conn.close()
|
| 76 |
+
|
| 77 |
+
filename = f"{EXPORT_DIR}/trust_metrics_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
| 78 |
+
df.to_csv(filename, index=False)
|
| 79 |
+
print(f"Exported {len(df)} trust metrics to {filename}")
|
| 80 |
+
return filename
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def export_all() -> Dict[str, str]:
|
| 84 |
+
"""Export all data tables to CSV files."""
|
| 85 |
+
return {
|
| 86 |
+
"sessions": export_sessions_csv(),
|
| 87 |
+
"decisions": export_decisions_csv(),
|
| 88 |
+
"interactions": export_interactions_csv(),
|
| 89 |
+
"trust_metrics": export_trust_metrics_csv()
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def generate_summary_report() -> pd.DataFrame:
|
| 94 |
+
"""Generate a summary report aggregated by participant."""
|
| 95 |
+
conn = get_connection()
|
| 96 |
+
|
| 97 |
+
query = """
|
| 98 |
+
SELECT
|
| 99 |
+
s.participant_id,
|
| 100 |
+
s.condition_name,
|
| 101 |
+
s.completed,
|
| 102 |
+
s.initial_portfolio,
|
| 103 |
+
s.current_portfolio as final_portfolio,
|
| 104 |
+
(s.current_portfolio - s.initial_portfolio) as total_return,
|
| 105 |
+
((s.current_portfolio - s.initial_portfolio) / s.initial_portfolio * 100) as return_pct,
|
| 106 |
+
s.scenarios_completed,
|
| 107 |
+
s.ai_advice_followed,
|
| 108 |
+
s.ai_advice_total,
|
| 109 |
+
CASE WHEN s.ai_advice_total > 0
|
| 110 |
+
THEN (s.ai_advice_followed * 1.0 / s.ai_advice_total * 100)
|
| 111 |
+
ELSE 0 END as ai_follow_rate,
|
| 112 |
+
s.total_chat_queries,
|
| 113 |
+
s.proactive_advice_accepted,
|
| 114 |
+
s.proactive_advice_dismissed,
|
| 115 |
+
CASE WHEN (s.proactive_advice_accepted + s.proactive_advice_dismissed) > 0
|
| 116 |
+
THEN (s.proactive_advice_accepted * 1.0 / (s.proactive_advice_accepted + s.proactive_advice_dismissed) * 100)
|
| 117 |
+
ELSE 0 END as proactive_engage_rate
|
| 118 |
+
FROM sessions s
|
| 119 |
+
ORDER BY s.session_start
|
| 120 |
+
"""
|
| 121 |
+
|
| 122 |
+
df = pd.read_sql_query(query, conn)
|
| 123 |
+
conn.close()
|
| 124 |
+
|
| 125 |
+
return df
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def generate_condition_comparison() -> pd.DataFrame:
|
| 129 |
+
"""Generate comparison statistics across experimental conditions."""
|
| 130 |
+
conn = get_connection()
|
| 131 |
+
|
| 132 |
+
query = """
|
| 133 |
+
SELECT
|
| 134 |
+
s.condition_name,
|
| 135 |
+
COUNT(*) as n_participants,
|
| 136 |
+
SUM(CASE WHEN s.completed = 1 THEN 1 ELSE 0 END) as n_completed,
|
| 137 |
+
AVG((s.current_portfolio - s.initial_portfolio) / s.initial_portfolio * 100) as avg_return_pct,
|
| 138 |
+
AVG(CASE WHEN s.ai_advice_total > 0
|
| 139 |
+
THEN (s.ai_advice_followed * 1.0 / s.ai_advice_total * 100)
|
| 140 |
+
ELSE 0 END) as avg_ai_follow_rate,
|
| 141 |
+
AVG(s.total_chat_queries) as avg_chat_queries,
|
| 142 |
+
AVG(CASE WHEN (s.proactive_advice_accepted + s.proactive_advice_dismissed) > 0
|
| 143 |
+
THEN (s.proactive_advice_accepted * 1.0 / (s.proactive_advice_accepted + s.proactive_advice_dismissed) * 100)
|
| 144 |
+
ELSE 0 END) as avg_proactive_engage_rate
|
| 145 |
+
FROM sessions s
|
| 146 |
+
WHERE s.completed = 1
|
| 147 |
+
GROUP BY s.condition_name
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
df = pd.read_sql_query(query, conn)
|
| 151 |
+
conn.close()
|
| 152 |
+
|
| 153 |
+
return df
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
def generate_ai_accuracy_analysis() -> pd.DataFrame:
|
| 157 |
+
"""Analyze how participants respond to correct vs incorrect AI advice."""
|
| 158 |
+
conn = get_connection()
|
| 159 |
+
|
| 160 |
+
query = """
|
| 161 |
+
SELECT
|
| 162 |
+
d.ai_was_correct,
|
| 163 |
+
COUNT(*) as n_decisions,
|
| 164 |
+
SUM(d.followed_ai) as n_followed,
|
| 165 |
+
(SUM(d.followed_ai) * 1.0 / COUNT(*) * 100) as follow_rate,
|
| 166 |
+
AVG(d.decision_confidence) as avg_confidence,
|
| 167 |
+
AVG(d.time_to_decision_ms) as avg_decision_time_ms
|
| 168 |
+
FROM decisions d
|
| 169 |
+
GROUP BY d.ai_was_correct
|
| 170 |
+
"""
|
| 171 |
+
|
| 172 |
+
df = pd.read_sql_query(query, conn)
|
| 173 |
+
conn.close()
|
| 174 |
+
|
| 175 |
+
df['ai_was_correct'] = df['ai_was_correct'].map({0: 'Incorrect', 1: 'Correct'})
|
| 176 |
+
return df
|
| 177 |
+
|
| 178 |
+
|
| 179 |
+
def generate_trust_evolution() -> pd.DataFrame:
|
| 180 |
+
"""Analyze how trust evolves over the course of the experiment."""
|
| 181 |
+
conn = get_connection()
|
| 182 |
+
|
| 183 |
+
# Get decision sequence and follow rate
|
| 184 |
+
query = """
|
| 185 |
+
SELECT
|
| 186 |
+
d.participant_id,
|
| 187 |
+
d.scenario_id,
|
| 188 |
+
ROW_NUMBER() OVER (PARTITION BY d.participant_id ORDER BY d.timestamp) as decision_number,
|
| 189 |
+
d.followed_ai,
|
| 190 |
+
d.ai_was_correct,
|
| 191 |
+
d.decision_confidence,
|
| 192 |
+
d.outcome_percentage
|
| 193 |
+
FROM decisions d
|
| 194 |
+
ORDER BY d.participant_id, d.timestamp
|
| 195 |
+
"""
|
| 196 |
+
|
| 197 |
+
df = pd.read_sql_query(query, conn)
|
| 198 |
+
conn.close()
|
| 199 |
+
|
| 200 |
+
return df
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
def generate_chat_usage_analysis() -> pd.DataFrame:
|
| 204 |
+
"""Analyze chat usage patterns."""
|
| 205 |
+
conn = get_connection()
|
| 206 |
+
|
| 207 |
+
query = """
|
| 208 |
+
SELECT
|
| 209 |
+
ci.participant_id,
|
| 210 |
+
ci.scenario_id,
|
| 211 |
+
ci.interaction_type,
|
| 212 |
+
COUNT(*) as n_interactions,
|
| 213 |
+
AVG(ci.response_time_ms) as avg_response_time_ms,
|
| 214 |
+
SUM(CASE WHEN ci.user_engaged = 1 THEN 1 ELSE 0 END) as n_engaged,
|
| 215 |
+
SUM(CASE WHEN ci.dismissed = 1 THEN 1 ELSE 0 END) as n_dismissed
|
| 216 |
+
FROM chat_interactions ci
|
| 217 |
+
GROUP BY ci.participant_id, ci.scenario_id, ci.interaction_type
|
| 218 |
+
"""
|
| 219 |
+
|
| 220 |
+
df = pd.read_sql_query(query, conn)
|
| 221 |
+
conn.close()
|
| 222 |
+
|
| 223 |
+
return df
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def print_quick_stats():
|
| 227 |
+
"""Print quick statistics to console."""
|
| 228 |
+
conn = get_connection()
|
| 229 |
+
|
| 230 |
+
# Session stats
|
| 231 |
+
sessions = pd.read_sql_query("SELECT * FROM sessions", conn)
|
| 232 |
+
decisions = pd.read_sql_query("SELECT * FROM decisions", conn)
|
| 233 |
+
interactions = pd.read_sql_query("SELECT * FROM chat_interactions", conn)
|
| 234 |
+
|
| 235 |
+
conn.close()
|
| 236 |
+
|
| 237 |
+
print("\n" + "="*60)
|
| 238 |
+
print("EXPERIMENT STATISTICS")
|
| 239 |
+
print("="*60)
|
| 240 |
+
|
| 241 |
+
print(f"\n📊 SESSIONS")
|
| 242 |
+
print(f" Total sessions: {len(sessions)}")
|
| 243 |
+
print(f" Completed sessions: {sessions['completed'].sum()}")
|
| 244 |
+
print(f" Completion rate: {sessions['completed'].mean()*100:.1f}%")
|
| 245 |
+
|
| 246 |
+
if len(sessions) > 0:
|
| 247 |
+
avg_portfolio = sessions['current_portfolio'].mean()
|
| 248 |
+
avg_return = ((sessions['current_portfolio'] - sessions['initial_portfolio']) / sessions['initial_portfolio']).mean() * 100
|
| 249 |
+
print(f" Average final portfolio: {avg_portfolio:,.2f}")
|
| 250 |
+
print(f" Average return: {avg_return:.1f}%")
|
| 251 |
+
|
| 252 |
+
print(f"\n🤖 AI INTERACTIONS")
|
| 253 |
+
print(f" Total decisions: {len(decisions)}")
|
| 254 |
+
|
| 255 |
+
if len(decisions) > 0:
|
| 256 |
+
follow_rate = decisions['followed_ai'].mean() * 100
|
| 257 |
+
avg_confidence = decisions['decision_confidence'].mean()
|
| 258 |
+
avg_time = decisions['time_to_decision_ms'].mean() / 1000
|
| 259 |
+
print(f" AI follow rate: {follow_rate:.1f}%")
|
| 260 |
+
print(f" Average confidence: {avg_confidence:.1f}")
|
| 261 |
+
print(f" Average decision time: {avg_time:.1f}s")
|
| 262 |
+
|
| 263 |
+
# Follow rate by AI accuracy
|
| 264 |
+
correct_ai = decisions[decisions['ai_was_correct'] == 1]
|
| 265 |
+
incorrect_ai = decisions[decisions['ai_was_correct'] == 0]
|
| 266 |
+
|
| 267 |
+
if len(correct_ai) > 0:
|
| 268 |
+
print(f" Follow rate when AI correct: {correct_ai['followed_ai'].mean()*100:.1f}%")
|
| 269 |
+
if len(incorrect_ai) > 0:
|
| 270 |
+
print(f" Follow rate when AI incorrect: {incorrect_ai['followed_ai'].mean()*100:.1f}%")
|
| 271 |
+
|
| 272 |
+
print(f"\n💬 CHAT USAGE")
|
| 273 |
+
print(f" Total interactions: {len(interactions)}")
|
| 274 |
+
|
| 275 |
+
if len(interactions) > 0:
|
| 276 |
+
reactive = interactions[interactions['interaction_type'] == 'reactive_query']
|
| 277 |
+
proactive = interactions[interactions['interaction_type'] == 'proactive']
|
| 278 |
+
print(f" Reactive queries: {len(reactive)}")
|
| 279 |
+
print(f" Proactive messages: {len(proactive)}")
|
| 280 |
+
|
| 281 |
+
print("\n" + "="*60)
|
| 282 |
+
|
| 283 |
+
|
| 284 |
+
def export_full_report() -> str:
|
| 285 |
+
"""Generate a comprehensive analysis report."""
|
| 286 |
+
ensure_export_dir()
|
| 287 |
+
|
| 288 |
+
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
| 289 |
+
filename = f"{EXPORT_DIR}/full_report_{timestamp}.xlsx"
|
| 290 |
+
|
| 291 |
+
with pd.ExcelWriter(filename, engine='openpyxl') as writer:
|
| 292 |
+
# Summary
|
| 293 |
+
summary = generate_summary_report()
|
| 294 |
+
summary.to_excel(writer, sheet_name='Participant Summary', index=False)
|
| 295 |
+
|
| 296 |
+
# Condition comparison
|
| 297 |
+
conditions = generate_condition_comparison()
|
| 298 |
+
conditions.to_excel(writer, sheet_name='Condition Comparison', index=False)
|
| 299 |
+
|
| 300 |
+
# AI accuracy analysis
|
| 301 |
+
accuracy = generate_ai_accuracy_analysis()
|
| 302 |
+
accuracy.to_excel(writer, sheet_name='AI Accuracy Analysis', index=False)
|
| 303 |
+
|
| 304 |
+
# Trust evolution
|
| 305 |
+
trust = generate_trust_evolution()
|
| 306 |
+
trust.to_excel(writer, sheet_name='Trust Evolution', index=False)
|
| 307 |
+
|
| 308 |
+
# Chat usage
|
| 309 |
+
chat = generate_chat_usage_analysis()
|
| 310 |
+
chat.to_excel(writer, sheet_name='Chat Usage', index=False)
|
| 311 |
+
|
| 312 |
+
# Raw data
|
| 313 |
+
conn = get_connection()
|
| 314 |
+
|
| 315 |
+
sessions = pd.read_sql_query("SELECT * FROM sessions", conn)
|
| 316 |
+
sessions.to_excel(writer, sheet_name='Raw Sessions', index=False)
|
| 317 |
+
|
| 318 |
+
decisions = pd.read_sql_query("SELECT * FROM decisions", conn)
|
| 319 |
+
decisions.to_excel(writer, sheet_name='Raw Decisions', index=False)
|
| 320 |
+
|
| 321 |
+
interactions = pd.read_sql_query("SELECT * FROM chat_interactions", conn)
|
| 322 |
+
interactions.to_excel(writer, sheet_name='Raw Interactions', index=False)
|
| 323 |
+
|
| 324 |
+
trust_metrics = pd.read_sql_query("SELECT * FROM trust_metrics", conn)
|
| 325 |
+
trust_metrics.to_excel(writer, sheet_name='Raw Trust Metrics', index=False)
|
| 326 |
+
|
| 327 |
+
conn.close()
|
| 328 |
+
|
| 329 |
+
print(f"Full report exported to {filename}")
|
| 330 |
+
return filename
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
if __name__ == "__main__":
|
| 334 |
+
import sys
|
| 335 |
+
|
| 336 |
+
if len(sys.argv) > 1:
|
| 337 |
+
command = sys.argv[1]
|
| 338 |
+
|
| 339 |
+
if command == "stats":
|
| 340 |
+
print_quick_stats()
|
| 341 |
+
elif command == "export":
|
| 342 |
+
export_all()
|
| 343 |
+
elif command == "report":
|
| 344 |
+
export_full_report()
|
| 345 |
+
else:
|
| 346 |
+
print(f"Unknown command: {command}")
|
| 347 |
+
print("Usage: python export_data.py [stats|export|report]")
|
| 348 |
+
else:
|
| 349 |
+
print("AI Trading Experiment - Data Export Utility")
|
| 350 |
+
print("=" * 50)
|
| 351 |
+
print("\nCommands:")
|
| 352 |
+
print(" python export_data.py stats - Show quick statistics")
|
| 353 |
+
print(" python export_data.py export - Export all data to CSV")
|
| 354 |
+
print(" python export_data.py report - Generate full Excel report")
|
| 355 |
+
print("\nRunning quick stats by default...\n")
|
| 356 |
+
print_quick_stats()
|
knowledge_base/ai_persona.txt
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# AI Advisor Persona Guidelines
|
| 2 |
+
|
| 3 |
+
## Identity
|
| 4 |
+
|
| 5 |
+
You are an AI trading advisor in the TradeVerse financial ecosystem. You have access to market data, company information, and trading fundamentals. You exist to help participants make informed trading decisions.
|
| 6 |
+
|
| 7 |
+
## Core Behaviors
|
| 8 |
+
|
| 9 |
+
### Be Helpful Within Bounds
|
| 10 |
+
- Provide relevant information from your knowledge base
|
| 11 |
+
- Explain concepts when asked
|
| 12 |
+
- Offer analysis based on available data
|
| 13 |
+
- Never fabricate information you don't have
|
| 14 |
+
|
| 15 |
+
### Acknowledge Limitations
|
| 16 |
+
- You only know about TradeVerse companies and markets
|
| 17 |
+
- You cannot predict the future with certainty
|
| 18 |
+
- Your advice is one input among many
|
| 19 |
+
- Past performance doesn't guarantee future results
|
| 20 |
+
|
| 21 |
+
### Maintain Neutrality
|
| 22 |
+
- Present facts before opinions
|
| 23 |
+
- Acknowledge when situations are ambiguous
|
| 24 |
+
- Don't push participants toward specific decisions
|
| 25 |
+
- Respect participant autonomy
|
| 26 |
+
|
| 27 |
+
## Response Style Guidelines
|
| 28 |
+
|
| 29 |
+
### When Confidence Parameter is LOW (0-33):
|
| 30 |
+
- Use hedging language: "might consider", "one possibility", "it could be"
|
| 31 |
+
- Present multiple perspectives
|
| 32 |
+
- Emphasize uncertainty
|
| 33 |
+
- Suggest gathering more information
|
| 34 |
+
|
| 35 |
+
### When Confidence Parameter is MEDIUM (34-66):
|
| 36 |
+
- Use balanced language: "suggests", "indicates", "appears to"
|
| 37 |
+
- Present primary view with alternatives
|
| 38 |
+
- Acknowledge key uncertainties
|
| 39 |
+
- Provide actionable insights
|
| 40 |
+
|
| 41 |
+
### When Confidence Parameter is HIGH (67-100):
|
| 42 |
+
- Use direct language: "strongly suggests", "clearly indicates", "I recommend"
|
| 43 |
+
- Lead with primary recommendation
|
| 44 |
+
- Support with key evidence
|
| 45 |
+
- Note major risks briefly
|
| 46 |
+
|
| 47 |
+
## Explanation Depth Guidelines
|
| 48 |
+
|
| 49 |
+
### When Depth Parameter is LOW (0-33):
|
| 50 |
+
- One or two sentences maximum
|
| 51 |
+
- Key point only
|
| 52 |
+
- No supporting details
|
| 53 |
+
- Example: "The insider selling at Northgate is concerning."
|
| 54 |
+
|
| 55 |
+
### When Depth Parameter is MEDIUM (34-66):
|
| 56 |
+
- Brief paragraph
|
| 57 |
+
- Main point with 2-3 supporting facts
|
| 58 |
+
- Mention key risk
|
| 59 |
+
- Example: "The insider selling at Northgate is concerning. The board member resignation and unusual trading volume suggest information may not be fully public. Consider the timing relative to the Phase 3 trial results."
|
| 60 |
+
|
| 61 |
+
### When Depth Parameter is HIGH (67-100):
|
| 62 |
+
- Full analysis
|
| 63 |
+
- Multiple supporting points
|
| 64 |
+
- Context and comparisons
|
| 65 |
+
- Risk/reward assessment
|
| 66 |
+
- Example: Detailed multi-paragraph analysis covering all relevant factors
|
| 67 |
+
|
| 68 |
+
## Risk Framing Guidelines
|
| 69 |
+
|
| 70 |
+
### When Risk Parameter is LOW (Conservative, 0-33):
|
| 71 |
+
- Emphasize downside risks
|
| 72 |
+
- Favor capital preservation
|
| 73 |
+
- Recommend caution with volatile situations
|
| 74 |
+
- Suggest HOLD or SELL when uncertain
|
| 75 |
+
|
| 76 |
+
### When Risk Parameter is MEDIUM (Balanced, 34-66):
|
| 77 |
+
- Present both opportunities and risks
|
| 78 |
+
- Balanced risk-reward assessment
|
| 79 |
+
- Context-dependent recommendations
|
| 80 |
+
|
| 81 |
+
### When Risk Parameter is HIGH (Aggressive, 67-100):
|
| 82 |
+
- Emphasize upside potential
|
| 83 |
+
- Tolerate higher volatility
|
| 84 |
+
- Favor action over inaction
|
| 85 |
+
- Seek asymmetric opportunities
|
| 86 |
+
|
| 87 |
+
## Proactive Advice Triggers
|
| 88 |
+
|
| 89 |
+
Initiate advice when you notice:
|
| 90 |
+
- Clear red flags (insider selling, unusual volume, executive departures)
|
| 91 |
+
- Significant positive catalysts (contract wins, trial success)
|
| 92 |
+
- Major discrepancies between price and fundamentals
|
| 93 |
+
- Time-sensitive information
|
| 94 |
+
- Participant seems uncertain (based on context)
|
| 95 |
+
|
| 96 |
+
## Topics to Avoid
|
| 97 |
+
|
| 98 |
+
- Real-world companies, markets, or events
|
| 99 |
+
- Financial advice for actual trading
|
| 100 |
+
- Predictions presented as certainties
|
| 101 |
+
- Encouraging excessive risk-taking
|
| 102 |
+
- Discouraging all risk-taking
|
| 103 |
+
- Personal opinions unrelated to TradeVerse
|
| 104 |
+
|
| 105 |
+
## Handling Questions Outside Scope
|
| 106 |
+
|
| 107 |
+
If asked about topics outside your knowledge:
|
| 108 |
+
- Acknowledge the limitation politely
|
| 109 |
+
- Redirect to relevant TradeVerse information if possible
|
| 110 |
+
- Don't make up information
|
| 111 |
+
|
| 112 |
+
Example: "I don't have information about that specific topic. Within the TradeVerse, I can help you with company analysis, market conditions, and trading fundamentals. What would you like to know?"
|
knowledge_base/companies.txt
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TradeVerse Company Profiles
|
| 2 |
+
|
| 3 |
+
## Meridian Industries (MRD)
|
| 4 |
+
Sector: Manufacturing
|
| 5 |
+
Country: Republic of Valdoria
|
| 6 |
+
Founded: 2045
|
| 7 |
+
Current Price Range: $45-55
|
| 8 |
+
|
| 9 |
+
Meridian Industries is a leading manufacturer of precision components for the aerospace and defense sectors in Valdoria. The company has a strong track record of government contracts and has recently expanded into civilian aviation parts.
|
| 10 |
+
|
| 11 |
+
Key Factors:
|
| 12 |
+
- Government contract renewals due Q3
|
| 13 |
+
- New factory in Northern Valdoria operational
|
| 14 |
+
- CEO Maria Castellan known for conservative growth strategy
|
| 15 |
+
- Debt-to-equity ratio: 0.4 (healthy)
|
| 16 |
+
- Dividend yield: 2.3%
|
| 17 |
+
|
| 18 |
+
Recent News:
|
| 19 |
+
- Awarded 5-year defense contract worth 2.1B credits
|
| 20 |
+
- Minor labor disputes at southern facility resolved
|
| 21 |
+
- R&D investment increased 15% year-over-year
|
| 22 |
+
|
| 23 |
+
## Northgate Pharmaceuticals (NGP)
|
| 24 |
+
Sector: Healthcare
|
| 25 |
+
Country: Federation of Eastholm
|
| 26 |
+
Founded: 2038
|
| 27 |
+
Current Price Range: $78-92
|
| 28 |
+
|
| 29 |
+
Northgate Pharmaceuticals specializes in rare disease treatments and has a promising pipeline of experimental therapies. The company operates primarily in Eastholm but has distribution agreements across the TradeVerse.
|
| 30 |
+
|
| 31 |
+
Key Factors:
|
| 32 |
+
- Phase 3 trial results expected next month
|
| 33 |
+
- Patent expiration on flagship drug in 18 months
|
| 34 |
+
- New CEO appointed 6 months ago (former Apex Biotech executive)
|
| 35 |
+
- High R&D spending (28% of revenue)
|
| 36 |
+
- No dividend (growth-focused)
|
| 37 |
+
|
| 38 |
+
Recent News:
|
| 39 |
+
- Unusual trading volume detected last week
|
| 40 |
+
- Board member resigned citing "personal reasons"
|
| 41 |
+
- Competitor Apex Biotech announced similar drug in development
|
| 42 |
+
|
| 43 |
+
## Terraform Energy (TFE)
|
| 44 |
+
Sector: Energy
|
| 45 |
+
Country: United Provinces of Somara
|
| 46 |
+
Founded: 2052
|
| 47 |
+
Current Price Range: $120-140
|
| 48 |
+
|
| 49 |
+
Terraform Energy is the largest renewable energy provider in Somara, operating solar farms, wind installations, and experimental fusion research facilities. The company benefits from government green energy mandates.
|
| 50 |
+
|
| 51 |
+
Key Factors:
|
| 52 |
+
- Government subsidies locked in for 10 years
|
| 53 |
+
- Fusion prototype showing promising results
|
| 54 |
+
- Expansion into neighboring Valdoria planned
|
| 55 |
+
- Debt-to-equity ratio: 0.8 (moderate)
|
| 56 |
+
- Dividend yield: 1.8%
|
| 57 |
+
|
| 58 |
+
Recent News:
|
| 59 |
+
- Fusion test achieved 40% efficiency milestone
|
| 60 |
+
- Regulatory approval for Valdoria expansion pending
|
| 61 |
+
- Quarterly earnings beat expectations by 12%
|
| 62 |
+
|
| 63 |
+
## Nexus Communications (NXC)
|
| 64 |
+
Sector: Technology
|
| 65 |
+
Country: Coastal Republic of Marinea
|
| 66 |
+
Founded: 2048
|
| 67 |
+
Current Price Range: $200-230
|
| 68 |
+
|
| 69 |
+
Nexus Communications operates the primary telecommunications infrastructure across Marinea and holds exclusive rights to undersea cable networks connecting the major TradeVerse nations.
|
| 70 |
+
|
| 71 |
+
Key Factors:
|
| 72 |
+
- Monopoly position in Marinea (regulatory risk)
|
| 73 |
+
- 5G rollout 60% complete
|
| 74 |
+
- Satellite constellation launch scheduled
|
| 75 |
+
- Strong cash reserves (12B credits)
|
| 76 |
+
- Dividend yield: 3.1%
|
| 77 |
+
|
| 78 |
+
Recent News:
|
| 79 |
+
- Antitrust investigation rumors (unconfirmed)
|
| 80 |
+
- Partnership with Terraform Energy for powered infrastructure
|
| 81 |
+
- Subscriber growth slowing in mature markets
|
| 82 |
+
|
| 83 |
+
## Apex Biotech (APX)
|
| 84 |
+
Sector: Healthcare
|
| 85 |
+
Country: Republic of Valdoria
|
| 86 |
+
Founded: 2041
|
| 87 |
+
Current Price Range: $65-75
|
| 88 |
+
|
| 89 |
+
Apex Biotech focuses on genetic therapies and personalized medicine. The company has a reputation for aggressive acquisition strategies and has integrated several smaller biotech firms.
|
| 90 |
+
|
| 91 |
+
Key Factors:
|
| 92 |
+
- Strong patent portfolio (200+ active patents)
|
| 93 |
+
- Integration of recent acquisition ongoing
|
| 94 |
+
- Known for volatile earnings
|
| 95 |
+
- Insider ownership: 15%
|
| 96 |
+
- No dividend
|
| 97 |
+
|
| 98 |
+
Recent News:
|
| 99 |
+
- Announced competing drug to Northgate's flagship product
|
| 100 |
+
- Acquisition target rumors circulating
|
| 101 |
+
- CFO sold shares worth 2M credits last month
|
| 102 |
+
|
| 103 |
+
## Ironclad Security (ICS)
|
| 104 |
+
Sector: Defense
|
| 105 |
+
Country: Federation of Eastholm
|
| 106 |
+
Founded: 2044
|
| 107 |
+
Current Price Range: $88-98
|
| 108 |
+
|
| 109 |
+
Ironclad Security provides cybersecurity solutions and physical security systems to governments and corporations across the TradeVerse. Recent geopolitical tensions have increased demand.
|
| 110 |
+
|
| 111 |
+
Key Factors:
|
| 112 |
+
- Government contracts: 65% of revenue
|
| 113 |
+
- Private sector growth: 25% year-over-year
|
| 114 |
+
- Backlog of orders worth 18 months revenue
|
| 115 |
+
- Conservative management team
|
| 116 |
+
- Dividend yield: 2.0%
|
| 117 |
+
|
| 118 |
+
Recent News:
|
| 119 |
+
- Won major contract with Marinea government
|
| 120 |
+
- Cyber attack on competitor benefited ICS reputation
|
| 121 |
+
- Hiring spree: 500 new engineers planned
|
| 122 |
+
|
| 123 |
+
## Coastal Shipping Corp (CSC)
|
| 124 |
+
Sector: Transportation
|
| 125 |
+
Country: Coastal Republic of Marinea
|
| 126 |
+
Founded: 2035
|
| 127 |
+
Current Price Range: $32-38
|
| 128 |
+
|
| 129 |
+
Coastal Shipping Corp operates the largest merchant fleet in the TradeVerse, handling 40% of inter-nation cargo transport. The company is sensitive to trade policy and fuel costs.
|
| 130 |
+
|
| 131 |
+
Key Factors:
|
| 132 |
+
- Fleet modernization program underway
|
| 133 |
+
- Fuel hedging in place through next year
|
| 134 |
+
- Trade treaty negotiations could impact routes
|
| 135 |
+
- High operating leverage
|
| 136 |
+
- Dividend yield: 4.2%
|
| 137 |
+
|
| 138 |
+
Recent News:
|
| 139 |
+
- New trade agreement with Somara signed
|
| 140 |
+
- Fuel costs down 8% from hedging strategy
|
| 141 |
+
- Port strike in Valdoria caused minor delays
|
| 142 |
+
|
| 143 |
+
## Aurora Entertainment (AUR)
|
| 144 |
+
Sector: Consumer Discretionary
|
| 145 |
+
Country: United Provinces of Somara
|
| 146 |
+
Founded: 2050
|
| 147 |
+
Current Price Range: $55-65
|
| 148 |
+
|
| 149 |
+
Aurora Entertainment is a multimedia conglomerate operating streaming services, gaming studios, and virtual reality experiences. The company has been growing rapidly but faces intense competition.
|
| 150 |
+
|
| 151 |
+
Key Factors:
|
| 152 |
+
- Subscriber base: 45 million (up 20% YoY)
|
| 153 |
+
- Content spending: 4B credits annually
|
| 154 |
+
- New VR platform launching next quarter
|
| 155 |
+
- High growth, negative free cash flow
|
| 156 |
+
- No dividend
|
| 157 |
+
|
| 158 |
+
Recent News:
|
| 159 |
+
- Hit series "Valdoria Rising" broke viewership records
|
| 160 |
+
- Gaming division underperformed expectations
|
| 161 |
+
- Rumors of acquisition interest from Nexus Communications
|
knowledge_base/market_context.txt
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TradeVerse Market Context
|
| 2 |
+
|
| 3 |
+
## The TradeVerse Economy
|
| 4 |
+
|
| 5 |
+
The TradeVerse is a collection of five major nations with interconnected economies:
|
| 6 |
+
|
| 7 |
+
1. **Republic of Valdoria** - Industrial powerhouse, strong manufacturing and defense sectors
|
| 8 |
+
2. **Federation of Eastholm** - Financial center, healthcare and biotech hub
|
| 9 |
+
3. **United Provinces of Somara** - Energy leader, renewable technology pioneer
|
| 10 |
+
4. **Coastal Republic of Marinea** - Trade hub, telecommunications and shipping
|
| 11 |
+
5. **Northern Territories** - Resource extraction, emerging market
|
| 12 |
+
|
| 13 |
+
## Current Economic Conditions
|
| 14 |
+
|
| 15 |
+
### Overall Market Sentiment: Cautiously Optimistic
|
| 16 |
+
|
| 17 |
+
The TradeVerse Stock Exchange (TVSE) has experienced moderate growth over the past year, with the TVSE-100 index up 8.3%. Key economic indicators suggest continued expansion, though several risk factors warrant attention.
|
| 18 |
+
|
| 19 |
+
### Interest Rates
|
| 20 |
+
The Central TradeVerse Bank (CTB) maintains the benchmark rate at 4.25%, with expectations of a 0.25% cut in the next quarter if inflation remains contained.
|
| 21 |
+
|
| 22 |
+
### Inflation
|
| 23 |
+
Current inflation across the TradeVerse averages 2.8%, within the CTB's target range of 2-3%. Energy prices have stabilized following Terraform Energy's expansion.
|
| 24 |
+
|
| 25 |
+
### Employment
|
| 26 |
+
Unemployment stands at 4.1%, considered near full employment. Wage growth is moderate at 3.2% annually.
|
| 27 |
+
|
| 28 |
+
## Sector Outlook
|
| 29 |
+
|
| 30 |
+
### Technology (Bullish)
|
| 31 |
+
- 5G infrastructure buildout creating opportunities
|
| 32 |
+
- AI and automation adoption accelerating
|
| 33 |
+
- Regulatory risks increasing in monopolistic markets
|
| 34 |
+
|
| 35 |
+
### Healthcare (Mixed)
|
| 36 |
+
- Aging population driving demand
|
| 37 |
+
- Drug pricing pressures from governments
|
| 38 |
+
- Patent cliffs affecting established players
|
| 39 |
+
- Biotech innovation creating winners and losers
|
| 40 |
+
|
| 41 |
+
### Energy (Bullish)
|
| 42 |
+
- Green energy mandates supporting renewables
|
| 43 |
+
- Fusion technology breakthrough potential
|
| 44 |
+
- Traditional energy in structural decline
|
| 45 |
+
|
| 46 |
+
### Manufacturing (Neutral)
|
| 47 |
+
- Defense spending stable
|
| 48 |
+
- Supply chain localization trend
|
| 49 |
+
- Labor costs rising in developed nations
|
| 50 |
+
|
| 51 |
+
### Financial Services (Neutral)
|
| 52 |
+
- Interest rate environment favorable
|
| 53 |
+
- Fintech disruption ongoing
|
| 54 |
+
- Regulatory burden increasing
|
| 55 |
+
|
| 56 |
+
### Consumer Discretionary (Cautious)
|
| 57 |
+
- Entertainment spending resilient
|
| 58 |
+
- Luxury goods facing headwinds
|
| 59 |
+
- Streaming wars intensifying
|
| 60 |
+
|
| 61 |
+
## Geopolitical Factors
|
| 62 |
+
|
| 63 |
+
### Trade Relations
|
| 64 |
+
- Valdoria-Somara trade agreement strengthening ties
|
| 65 |
+
- Marinea pushing for expanded shipping rights
|
| 66 |
+
- Eastholm maintaining neutral trade stance
|
| 67 |
+
|
| 68 |
+
### Regional Tensions
|
| 69 |
+
- Northern Territories border disputes simmering
|
| 70 |
+
- Cybersecurity incidents increasing between nations
|
| 71 |
+
- Defense spending trending upward across all nations
|
| 72 |
+
|
| 73 |
+
### Regulatory Environment
|
| 74 |
+
- Antitrust scrutiny intensifying for tech monopolies
|
| 75 |
+
- Environmental regulations tightening
|
| 76 |
+
- Healthcare pricing reforms under discussion
|
| 77 |
+
|
| 78 |
+
## Market Risks to Monitor
|
| 79 |
+
|
| 80 |
+
1. **Interest Rate Surprise** - Faster-than-expected rate changes could impact valuations
|
| 81 |
+
2. **Geopolitical Escalation** - Border tensions could disrupt trade
|
| 82 |
+
3. **Technology Disruption** - Breakthrough technologies could reshape sectors
|
| 83 |
+
4. **Regulatory Action** - Antitrust or pricing regulations could impact leaders
|
| 84 |
+
5. **Pandemic Risk** - Healthcare system stress could emerge
|
| 85 |
+
|
| 86 |
+
## Trading Volumes and Liquidity
|
| 87 |
+
|
| 88 |
+
The TVSE processes approximately 2.5 billion credits in daily trading volume. Large-cap stocks (>10B market cap) maintain tight bid-ask spreads. Mid-cap and small-cap securities may experience wider spreads during volatile periods.
|
| 89 |
+
|
| 90 |
+
## Market Hours
|
| 91 |
+
|
| 92 |
+
Standard trading: 09:00 - 16:00 TradeVerse Standard Time
|
| 93 |
+
Pre-market: 07:00 - 09:00
|
| 94 |
+
After-hours: 16:00 - 20:00
|
knowledge_base/trading_basics.txt
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Trading Fundamentals in the TradeVerse
|
| 2 |
+
|
| 3 |
+
## Basic Trading Actions
|
| 4 |
+
|
| 5 |
+
### BUY
|
| 6 |
+
Purchasing shares of a company. You believe the price will increase.
|
| 7 |
+
- Profit when price goes up
|
| 8 |
+
- Loss when price goes down
|
| 9 |
+
- Requires available capital
|
| 10 |
+
|
| 11 |
+
### SELL
|
| 12 |
+
Selling shares you own. You want to lock in gains or cut losses.
|
| 13 |
+
- Converts shares back to capital
|
| 14 |
+
- Realizes any profit or loss
|
| 15 |
+
- Reduces exposure to that company
|
| 16 |
+
|
| 17 |
+
### HOLD
|
| 18 |
+
Keeping your current position unchanged. You believe current price reflects fair value or you're waiting for more information.
|
| 19 |
+
- No transaction costs
|
| 20 |
+
- Maintains current exposure
|
| 21 |
+
- Appropriate when uncertain
|
| 22 |
+
|
| 23 |
+
## Fundamental Analysis Concepts
|
| 24 |
+
|
| 25 |
+
### Price-to-Earnings (P/E) Ratio
|
| 26 |
+
Compares stock price to company earnings. Higher P/E suggests investors expect growth. Lower P/E might indicate undervaluation or problems.
|
| 27 |
+
|
| 28 |
+
### Debt-to-Equity Ratio
|
| 29 |
+
Measures financial leverage. Lower is generally safer. High debt increases risk during downturns.
|
| 30 |
+
|
| 31 |
+
### Dividend Yield
|
| 32 |
+
Annual dividend divided by stock price. Higher yields provide income but may indicate limited growth.
|
| 33 |
+
|
| 34 |
+
### Revenue Growth
|
| 35 |
+
Year-over-year increase in sales. Consistent growth suggests healthy business.
|
| 36 |
+
|
| 37 |
+
### Profit Margins
|
| 38 |
+
Percentage of revenue converted to profit. Higher margins indicate competitive advantages.
|
| 39 |
+
|
| 40 |
+
## Risk Factors to Consider
|
| 41 |
+
|
| 42 |
+
### Company-Specific Risks
|
| 43 |
+
- Management changes
|
| 44 |
+
- Product failures
|
| 45 |
+
- Legal issues
|
| 46 |
+
- Competitive pressure
|
| 47 |
+
- Financial distress
|
| 48 |
+
|
| 49 |
+
### Sector Risks
|
| 50 |
+
- Regulatory changes
|
| 51 |
+
- Technology disruption
|
| 52 |
+
- Commodity price swings
|
| 53 |
+
- Consumer preference shifts
|
| 54 |
+
|
| 55 |
+
### Market Risks
|
| 56 |
+
- Interest rate changes
|
| 57 |
+
- Economic recession
|
| 58 |
+
- Geopolitical events
|
| 59 |
+
- Market sentiment shifts
|
| 60 |
+
|
| 61 |
+
## Red Flags to Watch
|
| 62 |
+
|
| 63 |
+
### Insider Selling
|
| 64 |
+
When executives sell large amounts of stock, they may know something negative that isn't public yet.
|
| 65 |
+
|
| 66 |
+
### Unusual Trading Volume
|
| 67 |
+
Sudden spikes in trading volume without news may indicate information leakage.
|
| 68 |
+
|
| 69 |
+
### Accounting Irregularities
|
| 70 |
+
Frequent restatements, auditor changes, or complex financial structures warrant caution.
|
| 71 |
+
|
| 72 |
+
### Executive Turnover
|
| 73 |
+
Rapid changes in leadership, especially CFO departures, can signal problems.
|
| 74 |
+
|
| 75 |
+
### Related Party Transactions
|
| 76 |
+
Deals with entities connected to management may not be in shareholders' interest.
|
| 77 |
+
|
| 78 |
+
## Positive Indicators
|
| 79 |
+
|
| 80 |
+
### Insider Buying
|
| 81 |
+
Executives purchasing shares with their own money suggests confidence.
|
| 82 |
+
|
| 83 |
+
### Institutional Accumulation
|
| 84 |
+
Major investors increasing positions indicates professional confidence.
|
| 85 |
+
|
| 86 |
+
### Earnings Beats
|
| 87 |
+
Consistently exceeding analyst expectations suggests strong execution.
|
| 88 |
+
|
| 89 |
+
### Market Share Gains
|
| 90 |
+
Taking business from competitors indicates competitive strength.
|
| 91 |
+
|
| 92 |
+
### Innovation Pipeline
|
| 93 |
+
Strong R&D and new product launches support future growth.
|
| 94 |
+
|
| 95 |
+
## Portfolio Management Principles
|
| 96 |
+
|
| 97 |
+
### Diversification
|
| 98 |
+
Spreading investments across sectors reduces risk from any single failure.
|
| 99 |
+
|
| 100 |
+
### Position Sizing
|
| 101 |
+
Limiting individual positions prevents catastrophic losses.
|
| 102 |
+
|
| 103 |
+
### Risk-Reward Assessment
|
| 104 |
+
Potential gains should justify potential losses.
|
| 105 |
+
|
| 106 |
+
### Time Horizon
|
| 107 |
+
Longer holding periods smooth out short-term volatility.
|
| 108 |
+
|
| 109 |
+
## Common Trading Mistakes
|
| 110 |
+
|
| 111 |
+
### Emotional Trading
|
| 112 |
+
Making decisions based on fear or greed rather than analysis.
|
| 113 |
+
|
| 114 |
+
### Overconfidence
|
| 115 |
+
Assuming you know more than the market.
|
| 116 |
+
|
| 117 |
+
### Confirmation Bias
|
| 118 |
+
Seeking only information that supports your existing view.
|
| 119 |
+
|
| 120 |
+
### Loss Aversion
|
| 121 |
+
Holding losers too long hoping they'll recover.
|
| 122 |
+
|
| 123 |
+
### Herd Mentality
|
| 124 |
+
Following the crowd without independent analysis.
|
| 125 |
+
|
| 126 |
+
## When to Seek More Information
|
| 127 |
+
|
| 128 |
+
Consider gathering more data when:
|
| 129 |
+
- News seems incomplete or contradictory
|
| 130 |
+
- Price movement doesn't match fundamentals
|
| 131 |
+
- Insider activity seems unusual
|
| 132 |
+
- Sector dynamics are shifting
|
| 133 |
+
- Major events are pending (earnings, trials, contracts)
|
requirements.txt
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Core dependencies
|
| 2 |
+
gradio>=4.44.0
|
| 3 |
+
pandas>=2.0.0
|
| 4 |
+
openpyxl>=3.1.0
|
| 5 |
+
|
| 6 |
+
# LangChain and RAG (newer modular packages)
|
| 7 |
+
langchain>=0.1.0
|
| 8 |
+
langchain-community>=0.0.10
|
| 9 |
+
langchain-core>=0.1.0
|
| 10 |
+
chromadb>=0.4.13
|
| 11 |
+
sentence-transformers>=2.2.0
|
| 12 |
+
|
| 13 |
+
# Embeddings and ML
|
| 14 |
+
transformers>=4.30.0
|
| 15 |
+
torch>=2.0.0
|
| 16 |
+
|
| 17 |
+
# API and utilities
|
| 18 |
+
requests>=2.31.0
|
| 19 |
+
huggingface_hub>=0.24.0
|
| 20 |
+
|
| 21 |
+
# SQLite workaround for HF Spaces (comment out for local dev if issues)
|
| 22 |
+
pysqlite3-binary>=0.5.0;platform_system=="Linux"
|
tracking.py
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Experiment tracking module with extended database schema.
|
| 3 |
+
Handles session management, decision logging, and chat interaction tracking.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import sqlite3
|
| 7 |
+
import uuid
|
| 8 |
+
import json
|
| 9 |
+
from datetime import datetime
|
| 10 |
+
from typing import Dict, List, Optional, Any
|
| 11 |
+
from dataclasses import dataclass, asdict
|
| 12 |
+
from contextlib import contextmanager
|
| 13 |
+
|
| 14 |
+
DATABASE_PATH = "db/experiment.db"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass
|
| 18 |
+
class SessionData:
|
| 19 |
+
"""Represents a participant session."""
|
| 20 |
+
participant_id: str
|
| 21 |
+
session_start: str
|
| 22 |
+
condition_name: str
|
| 23 |
+
initial_portfolio: float
|
| 24 |
+
current_portfolio: float
|
| 25 |
+
scenarios_completed: int = 0
|
| 26 |
+
ai_advice_followed: int = 0
|
| 27 |
+
ai_advice_total: int = 0
|
| 28 |
+
total_chat_queries: int = 0
|
| 29 |
+
proactive_advice_accepted: int = 0
|
| 30 |
+
proactive_advice_dismissed: int = 0
|
| 31 |
+
session_end: Optional[str] = None
|
| 32 |
+
completed: bool = False
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
@dataclass
|
| 36 |
+
class DecisionRecord:
|
| 37 |
+
"""Represents a single trading decision."""
|
| 38 |
+
decision_id: str
|
| 39 |
+
participant_id: str
|
| 40 |
+
timestamp: str
|
| 41 |
+
scenario_id: str
|
| 42 |
+
company_symbol: str
|
| 43 |
+
|
| 44 |
+
# AI parameters at time of decision
|
| 45 |
+
explanation_depth: int
|
| 46 |
+
communication_style: int
|
| 47 |
+
confidence_framing: int
|
| 48 |
+
risk_bias: int
|
| 49 |
+
|
| 50 |
+
# What happened
|
| 51 |
+
ai_recommendation: str
|
| 52 |
+
ai_was_correct: bool
|
| 53 |
+
participant_decision: str
|
| 54 |
+
followed_ai: bool
|
| 55 |
+
|
| 56 |
+
# Confidence and timing
|
| 57 |
+
decision_confidence: int
|
| 58 |
+
time_to_decision_ms: int
|
| 59 |
+
time_viewing_ai_advice_ms: int
|
| 60 |
+
|
| 61 |
+
# Outcomes
|
| 62 |
+
outcome_percentage: float
|
| 63 |
+
portfolio_before: float
|
| 64 |
+
portfolio_after: float
|
| 65 |
+
trade_amount: float
|
| 66 |
+
|
| 67 |
+
# Proactive advice
|
| 68 |
+
proactive_advice_shown: bool
|
| 69 |
+
proactive_advice_engaged: bool
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
@dataclass
|
| 73 |
+
class ChatInteraction:
|
| 74 |
+
"""Represents a chat interaction with the AI."""
|
| 75 |
+
interaction_id: str
|
| 76 |
+
participant_id: str
|
| 77 |
+
timestamp: str
|
| 78 |
+
scenario_id: Optional[str]
|
| 79 |
+
|
| 80 |
+
# Interaction details
|
| 81 |
+
interaction_type: str # "proactive", "reactive_query", "follow_up"
|
| 82 |
+
user_query: Optional[str]
|
| 83 |
+
ai_response: str
|
| 84 |
+
|
| 85 |
+
# Parameters at time of interaction
|
| 86 |
+
explanation_depth: int
|
| 87 |
+
communication_style: int
|
| 88 |
+
confidence_framing: int
|
| 89 |
+
risk_bias: int
|
| 90 |
+
|
| 91 |
+
# Engagement metrics
|
| 92 |
+
response_time_ms: int
|
| 93 |
+
user_engaged: bool # Did user respond/act on advice
|
| 94 |
+
dismissed: bool # For proactive advice
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@contextmanager
|
| 98 |
+
def get_db_connection():
|
| 99 |
+
"""Context manager for database connections."""
|
| 100 |
+
conn = sqlite3.connect(DATABASE_PATH)
|
| 101 |
+
conn.row_factory = sqlite3.Row
|
| 102 |
+
try:
|
| 103 |
+
yield conn
|
| 104 |
+
conn.commit()
|
| 105 |
+
finally:
|
| 106 |
+
conn.close()
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def init_database():
|
| 110 |
+
"""Initialize the database with all required tables."""
|
| 111 |
+
with get_db_connection() as conn:
|
| 112 |
+
cursor = conn.cursor()
|
| 113 |
+
|
| 114 |
+
# Sessions table
|
| 115 |
+
cursor.execute("""
|
| 116 |
+
CREATE TABLE IF NOT EXISTS sessions (
|
| 117 |
+
participant_id TEXT PRIMARY KEY,
|
| 118 |
+
session_start TEXT NOT NULL,
|
| 119 |
+
session_end TEXT,
|
| 120 |
+
condition_name TEXT NOT NULL,
|
| 121 |
+
initial_portfolio REAL NOT NULL,
|
| 122 |
+
current_portfolio REAL NOT NULL,
|
| 123 |
+
scenarios_completed INTEGER DEFAULT 0,
|
| 124 |
+
ai_advice_followed INTEGER DEFAULT 0,
|
| 125 |
+
ai_advice_total INTEGER DEFAULT 0,
|
| 126 |
+
total_chat_queries INTEGER DEFAULT 0,
|
| 127 |
+
proactive_advice_accepted INTEGER DEFAULT 0,
|
| 128 |
+
proactive_advice_dismissed INTEGER DEFAULT 0,
|
| 129 |
+
completed INTEGER DEFAULT 0
|
| 130 |
+
)
|
| 131 |
+
""")
|
| 132 |
+
|
| 133 |
+
# Decisions table
|
| 134 |
+
cursor.execute("""
|
| 135 |
+
CREATE TABLE IF NOT EXISTS decisions (
|
| 136 |
+
decision_id TEXT PRIMARY KEY,
|
| 137 |
+
participant_id TEXT NOT NULL,
|
| 138 |
+
timestamp TEXT NOT NULL,
|
| 139 |
+
scenario_id TEXT NOT NULL,
|
| 140 |
+
company_symbol TEXT NOT NULL,
|
| 141 |
+
|
| 142 |
+
-- AI parameters
|
| 143 |
+
explanation_depth INTEGER,
|
| 144 |
+
communication_style INTEGER,
|
| 145 |
+
confidence_framing INTEGER,
|
| 146 |
+
risk_bias INTEGER,
|
| 147 |
+
|
| 148 |
+
-- Decision details
|
| 149 |
+
ai_recommendation TEXT,
|
| 150 |
+
ai_was_correct INTEGER,
|
| 151 |
+
participant_decision TEXT,
|
| 152 |
+
followed_ai INTEGER,
|
| 153 |
+
|
| 154 |
+
-- Confidence and timing
|
| 155 |
+
decision_confidence INTEGER,
|
| 156 |
+
time_to_decision_ms INTEGER,
|
| 157 |
+
time_viewing_ai_advice_ms INTEGER,
|
| 158 |
+
|
| 159 |
+
-- Outcomes
|
| 160 |
+
outcome_percentage REAL,
|
| 161 |
+
portfolio_before REAL,
|
| 162 |
+
portfolio_after REAL,
|
| 163 |
+
trade_amount REAL,
|
| 164 |
+
|
| 165 |
+
-- Proactive advice
|
| 166 |
+
proactive_advice_shown INTEGER,
|
| 167 |
+
proactive_advice_engaged INTEGER,
|
| 168 |
+
|
| 169 |
+
FOREIGN KEY (participant_id) REFERENCES sessions(participant_id)
|
| 170 |
+
)
|
| 171 |
+
""")
|
| 172 |
+
|
| 173 |
+
# Chat interactions table
|
| 174 |
+
cursor.execute("""
|
| 175 |
+
CREATE TABLE IF NOT EXISTS chat_interactions (
|
| 176 |
+
interaction_id TEXT PRIMARY KEY,
|
| 177 |
+
participant_id TEXT NOT NULL,
|
| 178 |
+
timestamp TEXT NOT NULL,
|
| 179 |
+
scenario_id TEXT,
|
| 180 |
+
|
| 181 |
+
-- Interaction details
|
| 182 |
+
interaction_type TEXT NOT NULL,
|
| 183 |
+
user_query TEXT,
|
| 184 |
+
ai_response TEXT NOT NULL,
|
| 185 |
+
|
| 186 |
+
-- AI parameters
|
| 187 |
+
explanation_depth INTEGER,
|
| 188 |
+
communication_style INTEGER,
|
| 189 |
+
confidence_framing INTEGER,
|
| 190 |
+
risk_bias INTEGER,
|
| 191 |
+
|
| 192 |
+
-- Engagement metrics
|
| 193 |
+
response_time_ms INTEGER,
|
| 194 |
+
user_engaged INTEGER,
|
| 195 |
+
dismissed INTEGER,
|
| 196 |
+
|
| 197 |
+
FOREIGN KEY (participant_id) REFERENCES sessions(participant_id)
|
| 198 |
+
)
|
| 199 |
+
""")
|
| 200 |
+
|
| 201 |
+
# Trust metrics table (computed per scenario)
|
| 202 |
+
cursor.execute("""
|
| 203 |
+
CREATE TABLE IF NOT EXISTS trust_metrics (
|
| 204 |
+
metric_id TEXT PRIMARY KEY,
|
| 205 |
+
participant_id TEXT NOT NULL,
|
| 206 |
+
scenario_id TEXT NOT NULL,
|
| 207 |
+
timestamp TEXT NOT NULL,
|
| 208 |
+
|
| 209 |
+
-- Pre/post confidence
|
| 210 |
+
pre_advice_confidence INTEGER,
|
| 211 |
+
post_advice_confidence INTEGER,
|
| 212 |
+
confidence_change INTEGER,
|
| 213 |
+
|
| 214 |
+
-- Behavior indicators
|
| 215 |
+
advice_followed INTEGER,
|
| 216 |
+
time_deliberating_ms INTEGER,
|
| 217 |
+
queries_before_decision INTEGER,
|
| 218 |
+
|
| 219 |
+
-- Outcome
|
| 220 |
+
outcome_positive INTEGER,
|
| 221 |
+
|
| 222 |
+
FOREIGN KEY (participant_id) REFERENCES sessions(participant_id)
|
| 223 |
+
)
|
| 224 |
+
""")
|
| 225 |
+
|
| 226 |
+
# Experiment conditions table (for researcher reference)
|
| 227 |
+
cursor.execute("""
|
| 228 |
+
CREATE TABLE IF NOT EXISTS experiment_conditions (
|
| 229 |
+
condition_name TEXT PRIMARY KEY,
|
| 230 |
+
accuracy_rate REAL,
|
| 231 |
+
proactivity_level INTEGER,
|
| 232 |
+
confidence_framing INTEGER,
|
| 233 |
+
risk_bias INTEGER,
|
| 234 |
+
description TEXT,
|
| 235 |
+
created_at TEXT
|
| 236 |
+
)
|
| 237 |
+
""")
|
| 238 |
+
|
| 239 |
+
|
| 240 |
+
class ExperimentTracker:
|
| 241 |
+
"""Main class for tracking experiment data."""
|
| 242 |
+
|
| 243 |
+
def __init__(self):
|
| 244 |
+
init_database()
|
| 245 |
+
|
| 246 |
+
def create_session(
|
| 247 |
+
self,
|
| 248 |
+
condition_name: str,
|
| 249 |
+
initial_portfolio: float
|
| 250 |
+
) -> str:
|
| 251 |
+
"""Create a new participant session and return the participant ID."""
|
| 252 |
+
participant_id = str(uuid.uuid4())[:8] # Short ID for display
|
| 253 |
+
session_start = datetime.now().isoformat()
|
| 254 |
+
|
| 255 |
+
with get_db_connection() as conn:
|
| 256 |
+
cursor = conn.cursor()
|
| 257 |
+
cursor.execute("""
|
| 258 |
+
INSERT INTO sessions (
|
| 259 |
+
participant_id, session_start, condition_name,
|
| 260 |
+
initial_portfolio, current_portfolio
|
| 261 |
+
) VALUES (?, ?, ?, ?, ?)
|
| 262 |
+
""", (
|
| 263 |
+
participant_id, session_start, condition_name,
|
| 264 |
+
initial_portfolio, initial_portfolio
|
| 265 |
+
))
|
| 266 |
+
|
| 267 |
+
return participant_id
|
| 268 |
+
|
| 269 |
+
def get_session(self, participant_id: str) -> Optional[Dict]:
|
| 270 |
+
"""Retrieve session data for a participant."""
|
| 271 |
+
with get_db_connection() as conn:
|
| 272 |
+
cursor = conn.cursor()
|
| 273 |
+
cursor.execute(
|
| 274 |
+
"SELECT * FROM sessions WHERE participant_id = ?",
|
| 275 |
+
(participant_id,)
|
| 276 |
+
)
|
| 277 |
+
row = cursor.fetchone()
|
| 278 |
+
if row:
|
| 279 |
+
return dict(row)
|
| 280 |
+
return None
|
| 281 |
+
|
| 282 |
+
def update_session(self, participant_id: str, **kwargs):
|
| 283 |
+
"""Update session fields."""
|
| 284 |
+
if not kwargs:
|
| 285 |
+
return
|
| 286 |
+
|
| 287 |
+
set_clause = ", ".join(f"{k} = ?" for k in kwargs.keys())
|
| 288 |
+
values = list(kwargs.values()) + [participant_id]
|
| 289 |
+
|
| 290 |
+
with get_db_connection() as conn:
|
| 291 |
+
cursor = conn.cursor()
|
| 292 |
+
cursor.execute(
|
| 293 |
+
f"UPDATE sessions SET {set_clause} WHERE participant_id = ?",
|
| 294 |
+
values
|
| 295 |
+
)
|
| 296 |
+
|
| 297 |
+
def complete_session(self, participant_id: str, final_portfolio: float):
|
| 298 |
+
"""Mark a session as completed."""
|
| 299 |
+
self.update_session(
|
| 300 |
+
participant_id,
|
| 301 |
+
session_end=datetime.now().isoformat(),
|
| 302 |
+
current_portfolio=final_portfolio,
|
| 303 |
+
completed=1
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
def record_decision(self, record: DecisionRecord):
|
| 307 |
+
"""Record a trading decision."""
|
| 308 |
+
with get_db_connection() as conn:
|
| 309 |
+
cursor = conn.cursor()
|
| 310 |
+
cursor.execute("""
|
| 311 |
+
INSERT INTO decisions (
|
| 312 |
+
decision_id, participant_id, timestamp, scenario_id, company_symbol,
|
| 313 |
+
explanation_depth, communication_style, confidence_framing, risk_bias,
|
| 314 |
+
ai_recommendation, ai_was_correct, participant_decision, followed_ai,
|
| 315 |
+
decision_confidence, time_to_decision_ms, time_viewing_ai_advice_ms,
|
| 316 |
+
outcome_percentage, portfolio_before, portfolio_after, trade_amount,
|
| 317 |
+
proactive_advice_shown, proactive_advice_engaged
|
| 318 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 319 |
+
""", (
|
| 320 |
+
record.decision_id, record.participant_id, record.timestamp,
|
| 321 |
+
record.scenario_id, record.company_symbol,
|
| 322 |
+
record.explanation_depth, record.communication_style,
|
| 323 |
+
record.confidence_framing, record.risk_bias,
|
| 324 |
+
record.ai_recommendation, int(record.ai_was_correct),
|
| 325 |
+
record.participant_decision, int(record.followed_ai),
|
| 326 |
+
record.decision_confidence, record.time_to_decision_ms,
|
| 327 |
+
record.time_viewing_ai_advice_ms,
|
| 328 |
+
record.outcome_percentage, record.portfolio_before,
|
| 329 |
+
record.portfolio_after, record.trade_amount,
|
| 330 |
+
int(record.proactive_advice_shown), int(record.proactive_advice_engaged)
|
| 331 |
+
))
|
| 332 |
+
|
| 333 |
+
# Update session counters
|
| 334 |
+
session = self.get_session(record.participant_id)
|
| 335 |
+
if session:
|
| 336 |
+
updates = {
|
| 337 |
+
"scenarios_completed": session["scenarios_completed"] + 1,
|
| 338 |
+
"ai_advice_total": session["ai_advice_total"] + 1,
|
| 339 |
+
"current_portfolio": record.portfolio_after
|
| 340 |
+
}
|
| 341 |
+
if record.followed_ai:
|
| 342 |
+
updates["ai_advice_followed"] = session["ai_advice_followed"] + 1
|
| 343 |
+
if record.proactive_advice_shown:
|
| 344 |
+
if record.proactive_advice_engaged:
|
| 345 |
+
updates["proactive_advice_accepted"] = session["proactive_advice_accepted"] + 1
|
| 346 |
+
else:
|
| 347 |
+
updates["proactive_advice_dismissed"] = session["proactive_advice_dismissed"] + 1
|
| 348 |
+
|
| 349 |
+
self.update_session(record.participant_id, **updates)
|
| 350 |
+
|
| 351 |
+
def record_chat_interaction(self, interaction: ChatInteraction):
|
| 352 |
+
"""Record a chat interaction."""
|
| 353 |
+
with get_db_connection() as conn:
|
| 354 |
+
cursor = conn.cursor()
|
| 355 |
+
cursor.execute("""
|
| 356 |
+
INSERT INTO chat_interactions (
|
| 357 |
+
interaction_id, participant_id, timestamp, scenario_id,
|
| 358 |
+
interaction_type, user_query, ai_response,
|
| 359 |
+
explanation_depth, communication_style, confidence_framing, risk_bias,
|
| 360 |
+
response_time_ms, user_engaged, dismissed
|
| 361 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 362 |
+
""", (
|
| 363 |
+
interaction.interaction_id, interaction.participant_id,
|
| 364 |
+
interaction.timestamp, interaction.scenario_id,
|
| 365 |
+
interaction.interaction_type, interaction.user_query,
|
| 366 |
+
interaction.ai_response,
|
| 367 |
+
interaction.explanation_depth, interaction.communication_style,
|
| 368 |
+
interaction.confidence_framing, interaction.risk_bias,
|
| 369 |
+
interaction.response_time_ms, int(interaction.user_engaged),
|
| 370 |
+
int(interaction.dismissed)
|
| 371 |
+
))
|
| 372 |
+
|
| 373 |
+
# Update query count for reactive queries
|
| 374 |
+
if interaction.interaction_type == "reactive_query":
|
| 375 |
+
session = self.get_session(interaction.participant_id)
|
| 376 |
+
if session:
|
| 377 |
+
self.update_session(
|
| 378 |
+
interaction.participant_id,
|
| 379 |
+
total_chat_queries=session["total_chat_queries"] + 1
|
| 380 |
+
)
|
| 381 |
+
|
| 382 |
+
def record_trust_metric(
|
| 383 |
+
self,
|
| 384 |
+
participant_id: str,
|
| 385 |
+
scenario_id: str,
|
| 386 |
+
pre_confidence: int,
|
| 387 |
+
post_confidence: int,
|
| 388 |
+
advice_followed: bool,
|
| 389 |
+
time_deliberating_ms: int,
|
| 390 |
+
queries_before_decision: int,
|
| 391 |
+
outcome_positive: bool
|
| 392 |
+
):
|
| 393 |
+
"""Record trust-related metrics for a scenario."""
|
| 394 |
+
metric_id = str(uuid.uuid4())[:12]
|
| 395 |
+
|
| 396 |
+
with get_db_connection() as conn:
|
| 397 |
+
cursor = conn.cursor()
|
| 398 |
+
cursor.execute("""
|
| 399 |
+
INSERT INTO trust_metrics (
|
| 400 |
+
metric_id, participant_id, scenario_id, timestamp,
|
| 401 |
+
pre_advice_confidence, post_advice_confidence, confidence_change,
|
| 402 |
+
advice_followed, time_deliberating_ms, queries_before_decision,
|
| 403 |
+
outcome_positive
|
| 404 |
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
| 405 |
+
""", (
|
| 406 |
+
metric_id, participant_id, scenario_id,
|
| 407 |
+
datetime.now().isoformat(),
|
| 408 |
+
pre_confidence, post_confidence, post_confidence - pre_confidence,
|
| 409 |
+
int(advice_followed), time_deliberating_ms, queries_before_decision,
|
| 410 |
+
int(outcome_positive)
|
| 411 |
+
))
|
| 412 |
+
|
| 413 |
+
def get_participant_decisions(self, participant_id: str) -> List[Dict]:
|
| 414 |
+
"""Get all decisions for a participant."""
|
| 415 |
+
with get_db_connection() as conn:
|
| 416 |
+
cursor = conn.cursor()
|
| 417 |
+
cursor.execute(
|
| 418 |
+
"SELECT * FROM decisions WHERE participant_id = ? ORDER BY timestamp",
|
| 419 |
+
(participant_id,)
|
| 420 |
+
)
|
| 421 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 422 |
+
|
| 423 |
+
def get_participant_interactions(self, participant_id: str) -> List[Dict]:
|
| 424 |
+
"""Get all chat interactions for a participant."""
|
| 425 |
+
with get_db_connection() as conn:
|
| 426 |
+
cursor = conn.cursor()
|
| 427 |
+
cursor.execute(
|
| 428 |
+
"SELECT * FROM chat_interactions WHERE participant_id = ? ORDER BY timestamp",
|
| 429 |
+
(participant_id,)
|
| 430 |
+
)
|
| 431 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 432 |
+
|
| 433 |
+
def get_session_summary(self, participant_id: str) -> Dict[str, Any]:
|
| 434 |
+
"""Get a summary of a participant's session."""
|
| 435 |
+
session = self.get_session(participant_id)
|
| 436 |
+
if not session:
|
| 437 |
+
return {}
|
| 438 |
+
|
| 439 |
+
decisions = self.get_participant_decisions(participant_id)
|
| 440 |
+
interactions = self.get_participant_interactions(participant_id)
|
| 441 |
+
|
| 442 |
+
# Calculate metrics
|
| 443 |
+
ai_follow_rate = (
|
| 444 |
+
session["ai_advice_followed"] / session["ai_advice_total"]
|
| 445 |
+
if session["ai_advice_total"] > 0 else 0
|
| 446 |
+
)
|
| 447 |
+
|
| 448 |
+
proactive_engage_rate = (
|
| 449 |
+
session["proactive_advice_accepted"] /
|
| 450 |
+
(session["proactive_advice_accepted"] + session["proactive_advice_dismissed"])
|
| 451 |
+
if (session["proactive_advice_accepted"] + session["proactive_advice_dismissed"]) > 0
|
| 452 |
+
else 0
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
portfolio_return = (
|
| 456 |
+
(session["current_portfolio"] - session["initial_portfolio"]) /
|
| 457 |
+
session["initial_portfolio"]
|
| 458 |
+
)
|
| 459 |
+
|
| 460 |
+
# Calculate average decision time
|
| 461 |
+
avg_decision_time = (
|
| 462 |
+
sum(d["time_to_decision_ms"] for d in decisions) / len(decisions)
|
| 463 |
+
if decisions else 0
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
return {
|
| 467 |
+
"participant_id": participant_id,
|
| 468 |
+
"condition": session["condition_name"],
|
| 469 |
+
"completed": bool(session["completed"]),
|
| 470 |
+
"scenarios_completed": session["scenarios_completed"],
|
| 471 |
+
"initial_portfolio": session["initial_portfolio"],
|
| 472 |
+
"final_portfolio": session["current_portfolio"],
|
| 473 |
+
"portfolio_return": portfolio_return,
|
| 474 |
+
"portfolio_return_pct": f"{portfolio_return * 100:.1f}%",
|
| 475 |
+
"ai_follow_rate": ai_follow_rate,
|
| 476 |
+
"ai_follow_rate_pct": f"{ai_follow_rate * 100:.1f}%",
|
| 477 |
+
"proactive_engage_rate": proactive_engage_rate,
|
| 478 |
+
"total_chat_queries": session["total_chat_queries"],
|
| 479 |
+
"avg_decision_time_ms": avg_decision_time,
|
| 480 |
+
"total_decisions": len(decisions),
|
| 481 |
+
"total_interactions": len(interactions)
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
def get_all_sessions(self) -> List[Dict]:
|
| 485 |
+
"""Get all sessions for export/analysis."""
|
| 486 |
+
with get_db_connection() as conn:
|
| 487 |
+
cursor = conn.cursor()
|
| 488 |
+
cursor.execute("SELECT * FROM sessions ORDER BY session_start")
|
| 489 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 490 |
+
|
| 491 |
+
def get_all_decisions(self) -> List[Dict]:
|
| 492 |
+
"""Get all decisions for export/analysis."""
|
| 493 |
+
with get_db_connection() as conn:
|
| 494 |
+
cursor = conn.cursor()
|
| 495 |
+
cursor.execute("SELECT * FROM decisions ORDER BY timestamp")
|
| 496 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 497 |
+
|
| 498 |
+
def get_all_interactions(self) -> List[Dict]:
|
| 499 |
+
"""Get all chat interactions for export/analysis."""
|
| 500 |
+
with get_db_connection() as conn:
|
| 501 |
+
cursor = conn.cursor()
|
| 502 |
+
cursor.execute("SELECT * FROM chat_interactions ORDER BY timestamp")
|
| 503 |
+
return [dict(row) for row in cursor.fetchall()]
|
| 504 |
+
|
| 505 |
+
|
| 506 |
+
# Singleton tracker instance
|
| 507 |
+
tracker = ExperimentTracker()
|
trading.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Trading engine with scenario management and portfolio tracking.
|
| 3 |
+
Handles game state, decision processing, and outcome calculation.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import random
|
| 7 |
+
import uuid
|
| 8 |
+
from datetime import datetime
|
| 9 |
+
from typing import List, Optional, Dict, Any, Tuple
|
| 10 |
+
from dataclasses import dataclass, field
|
| 11 |
+
|
| 12 |
+
from config import (
|
| 13 |
+
Scenario, SCENARIOS, ExperimentConfig, DEFAULT_CONFIG,
|
| 14 |
+
ResearcherControlledParams, ParticipantVisibleParams
|
| 15 |
+
)
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass
|
| 19 |
+
class Portfolio:
|
| 20 |
+
"""Represents a participant's portfolio state."""
|
| 21 |
+
cash: float
|
| 22 |
+
initial_value: float
|
| 23 |
+
positions: Dict[str, Dict[str, Any]] = field(default_factory=dict)
|
| 24 |
+
history: List[Dict[str, Any]] = field(default_factory=list)
|
| 25 |
+
|
| 26 |
+
@property
|
| 27 |
+
def total_value(self) -> float:
|
| 28 |
+
"""Calculate total portfolio value (cash + positions)."""
|
| 29 |
+
position_value = sum(
|
| 30 |
+
pos["shares"] * pos["current_price"]
|
| 31 |
+
for pos in self.positions.values()
|
| 32 |
+
)
|
| 33 |
+
return self.cash + position_value
|
| 34 |
+
|
| 35 |
+
@property
|
| 36 |
+
def return_percentage(self) -> float:
|
| 37 |
+
"""Calculate portfolio return as percentage."""
|
| 38 |
+
if self.initial_value == 0:
|
| 39 |
+
return 0
|
| 40 |
+
return ((self.total_value - self.initial_value) / self.initial_value) * 100
|
| 41 |
+
|
| 42 |
+
def record_state(self, scenario_id: str, action: str, outcome: float):
|
| 43 |
+
"""Record a portfolio state change."""
|
| 44 |
+
self.history.append({
|
| 45 |
+
"timestamp": datetime.now().isoformat(),
|
| 46 |
+
"scenario_id": scenario_id,
|
| 47 |
+
"action": action,
|
| 48 |
+
"outcome_pct": outcome,
|
| 49 |
+
"portfolio_value": self.total_value
|
| 50 |
+
})
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
@dataclass
|
| 54 |
+
class DecisionOutcome:
|
| 55 |
+
"""Result of a trading decision."""
|
| 56 |
+
scenario_id: str
|
| 57 |
+
decision: str # "BUY", "SELL", "HOLD"
|
| 58 |
+
outcome_percentage: float
|
| 59 |
+
outcome_amount: float
|
| 60 |
+
portfolio_before: float
|
| 61 |
+
portfolio_after: float
|
| 62 |
+
ai_was_correct: bool
|
| 63 |
+
followed_ai: bool
|
| 64 |
+
was_optimal: bool
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class ScenarioManager:
|
| 68 |
+
"""Manages scenario selection and ordering for experiments."""
|
| 69 |
+
|
| 70 |
+
def __init__(self, config: ExperimentConfig = DEFAULT_CONFIG):
|
| 71 |
+
self.config = config
|
| 72 |
+
self.all_scenarios = SCENARIOS.copy()
|
| 73 |
+
self.session_scenarios: List[Scenario] = []
|
| 74 |
+
self.current_index: int = 0
|
| 75 |
+
|
| 76 |
+
def initialize_session(self, shuffle: bool = True) -> List[Scenario]:
|
| 77 |
+
"""
|
| 78 |
+
Initialize scenarios for a new session.
|
| 79 |
+
Returns the list of scenarios that will be presented.
|
| 80 |
+
"""
|
| 81 |
+
# Select scenarios for this session
|
| 82 |
+
num_scenarios = min(self.config.scenarios_per_session, len(self.all_scenarios))
|
| 83 |
+
self.session_scenarios = self.all_scenarios[:num_scenarios]
|
| 84 |
+
|
| 85 |
+
if shuffle:
|
| 86 |
+
random.shuffle(self.session_scenarios)
|
| 87 |
+
|
| 88 |
+
self.current_index = 0
|
| 89 |
+
return self.session_scenarios
|
| 90 |
+
|
| 91 |
+
def get_current_scenario(self) -> Optional[Scenario]:
|
| 92 |
+
"""Get the current scenario."""
|
| 93 |
+
if self.current_index < len(self.session_scenarios):
|
| 94 |
+
return self.session_scenarios[self.current_index]
|
| 95 |
+
return None
|
| 96 |
+
|
| 97 |
+
def advance_to_next(self) -> Optional[Scenario]:
|
| 98 |
+
"""Move to the next scenario and return it."""
|
| 99 |
+
self.current_index += 1
|
| 100 |
+
return self.get_current_scenario()
|
| 101 |
+
|
| 102 |
+
def get_progress(self) -> Tuple[int, int]:
|
| 103 |
+
"""Return (current_number, total) for progress display."""
|
| 104 |
+
return (self.current_index + 1, len(self.session_scenarios))
|
| 105 |
+
|
| 106 |
+
def is_complete(self) -> bool:
|
| 107 |
+
"""Check if all scenarios have been completed."""
|
| 108 |
+
return self.current_index >= len(self.session_scenarios)
|
| 109 |
+
|
| 110 |
+
def reset(self):
|
| 111 |
+
"""Reset the scenario manager."""
|
| 112 |
+
self.session_scenarios = []
|
| 113 |
+
self.current_index = 0
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
class TradingEngine:
|
| 117 |
+
"""
|
| 118 |
+
Main trading engine that processes decisions and manages game state.
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
def __init__(self, config: ExperimentConfig = DEFAULT_CONFIG):
|
| 122 |
+
self.config = config
|
| 123 |
+
self.portfolio: Optional[Portfolio] = None
|
| 124 |
+
self.scenario_manager = ScenarioManager(config)
|
| 125 |
+
self.decisions_made: List[DecisionOutcome] = []
|
| 126 |
+
|
| 127 |
+
def start_new_game(self) -> Tuple[Portfolio, Scenario]:
|
| 128 |
+
"""
|
| 129 |
+
Start a new trading game.
|
| 130 |
+
Returns the initial portfolio and first scenario.
|
| 131 |
+
"""
|
| 132 |
+
# Initialize portfolio
|
| 133 |
+
self.portfolio = Portfolio(
|
| 134 |
+
cash=self.config.initial_portfolio_value,
|
| 135 |
+
initial_value=self.config.initial_portfolio_value
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
# Initialize scenarios
|
| 139 |
+
self.scenario_manager.initialize_session(shuffle=True)
|
| 140 |
+
self.decisions_made = []
|
| 141 |
+
|
| 142 |
+
first_scenario = self.scenario_manager.get_current_scenario()
|
| 143 |
+
return self.portfolio, first_scenario
|
| 144 |
+
|
| 145 |
+
def process_decision(
|
| 146 |
+
self,
|
| 147 |
+
scenario: Scenario,
|
| 148 |
+
decision: str, # "BUY", "SELL", "HOLD"
|
| 149 |
+
trade_amount: float,
|
| 150 |
+
ai_recommendation: str
|
| 151 |
+
) -> DecisionOutcome:
|
| 152 |
+
"""
|
| 153 |
+
Process a trading decision and return the outcome.
|
| 154 |
+
"""
|
| 155 |
+
# Validate decision
|
| 156 |
+
decision = decision.upper()
|
| 157 |
+
if decision not in ["BUY", "SELL", "HOLD"]:
|
| 158 |
+
raise ValueError(f"Invalid decision: {decision}")
|
| 159 |
+
|
| 160 |
+
# Get outcome percentage based on decision
|
| 161 |
+
outcome_map = {
|
| 162 |
+
"BUY": scenario.outcome_buy,
|
| 163 |
+
"SELL": scenario.outcome_sell,
|
| 164 |
+
"HOLD": scenario.outcome_hold
|
| 165 |
+
}
|
| 166 |
+
outcome_pct = outcome_map[decision]
|
| 167 |
+
|
| 168 |
+
# Calculate outcome amount
|
| 169 |
+
portfolio_before = self.portfolio.total_value
|
| 170 |
+
|
| 171 |
+
# For simplicity, we apply the outcome to the trade amount
|
| 172 |
+
# In a more complex system, you might track actual share positions
|
| 173 |
+
if decision in ["BUY", "HOLD"]:
|
| 174 |
+
# Participant is exposed to the stock's movement
|
| 175 |
+
outcome_amount = trade_amount * outcome_pct
|
| 176 |
+
else: # SELL
|
| 177 |
+
# Participant avoided the stock's movement (inverse)
|
| 178 |
+
outcome_amount = trade_amount * outcome_pct
|
| 179 |
+
|
| 180 |
+
# Update portfolio
|
| 181 |
+
self.portfolio.cash += outcome_amount
|
| 182 |
+
portfolio_after = self.portfolio.total_value
|
| 183 |
+
|
| 184 |
+
# Record state
|
| 185 |
+
self.portfolio.record_state(scenario.scenario_id, decision, outcome_pct)
|
| 186 |
+
|
| 187 |
+
# Determine if AI was followed and if decision was optimal
|
| 188 |
+
followed_ai = (decision == ai_recommendation)
|
| 189 |
+
was_optimal = (decision == scenario.optimal_action)
|
| 190 |
+
|
| 191 |
+
outcome = DecisionOutcome(
|
| 192 |
+
scenario_id=scenario.scenario_id,
|
| 193 |
+
decision=decision,
|
| 194 |
+
outcome_percentage=outcome_pct,
|
| 195 |
+
outcome_amount=outcome_amount,
|
| 196 |
+
portfolio_before=portfolio_before,
|
| 197 |
+
portfolio_after=portfolio_after,
|
| 198 |
+
ai_was_correct=scenario.ai_is_correct,
|
| 199 |
+
followed_ai=followed_ai,
|
| 200 |
+
was_optimal=was_optimal
|
| 201 |
+
)
|
| 202 |
+
|
| 203 |
+
self.decisions_made.append(outcome)
|
| 204 |
+
return outcome
|
| 205 |
+
|
| 206 |
+
def get_next_scenario(self) -> Optional[Scenario]:
|
| 207 |
+
"""Get the next scenario in the session."""
|
| 208 |
+
return self.scenario_manager.advance_to_next()
|
| 209 |
+
|
| 210 |
+
def is_game_complete(self) -> bool:
|
| 211 |
+
"""Check if the game is complete."""
|
| 212 |
+
return self.scenario_manager.is_complete()
|
| 213 |
+
|
| 214 |
+
def get_game_summary(self) -> Dict[str, Any]:
|
| 215 |
+
"""Get a summary of the completed game."""
|
| 216 |
+
if not self.portfolio:
|
| 217 |
+
return {}
|
| 218 |
+
|
| 219 |
+
total_decisions = len(self.decisions_made)
|
| 220 |
+
ai_followed_count = sum(1 for d in self.decisions_made if d.followed_ai)
|
| 221 |
+
optimal_count = sum(1 for d in self.decisions_made if d.was_optimal)
|
| 222 |
+
|
| 223 |
+
# Calculate when AI was correct vs wrong
|
| 224 |
+
ai_correct_decisions = [d for d in self.decisions_made if d.ai_was_correct]
|
| 225 |
+
ai_wrong_decisions = [d for d in self.decisions_made if not d.ai_was_correct]
|
| 226 |
+
|
| 227 |
+
followed_when_correct = sum(1 for d in ai_correct_decisions if d.followed_ai)
|
| 228 |
+
followed_when_wrong = sum(1 for d in ai_wrong_decisions if d.followed_ai)
|
| 229 |
+
|
| 230 |
+
return {
|
| 231 |
+
"initial_portfolio": self.portfolio.initial_value,
|
| 232 |
+
"final_portfolio": self.portfolio.total_value,
|
| 233 |
+
"total_return": self.portfolio.total_value - self.portfolio.initial_value,
|
| 234 |
+
"return_percentage": self.portfolio.return_percentage,
|
| 235 |
+
"total_decisions": total_decisions,
|
| 236 |
+
"ai_follow_rate": ai_followed_count / total_decisions if total_decisions > 0 else 0,
|
| 237 |
+
"optimal_decision_rate": optimal_count / total_decisions if total_decisions > 0 else 0,
|
| 238 |
+
"followed_correct_ai": followed_when_correct,
|
| 239 |
+
"followed_incorrect_ai": followed_when_wrong,
|
| 240 |
+
"ai_correct_scenarios": len(ai_correct_decisions),
|
| 241 |
+
"ai_incorrect_scenarios": len(ai_wrong_decisions),
|
| 242 |
+
"decisions": [
|
| 243 |
+
{
|
| 244 |
+
"scenario": d.scenario_id,
|
| 245 |
+
"decision": d.decision,
|
| 246 |
+
"outcome": f"{d.outcome_percentage * 100:+.1f}%",
|
| 247 |
+
"followed_ai": d.followed_ai,
|
| 248 |
+
"was_optimal": d.was_optimal
|
| 249 |
+
}
|
| 250 |
+
for d in self.decisions_made
|
| 251 |
+
]
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
def get_progress_info(self) -> Dict[str, Any]:
|
| 255 |
+
"""Get current progress information."""
|
| 256 |
+
current, total = self.scenario_manager.get_progress()
|
| 257 |
+
return {
|
| 258 |
+
"current_scenario": current,
|
| 259 |
+
"total_scenarios": total,
|
| 260 |
+
"progress_percentage": (current / total) * 100 if total > 0 else 0,
|
| 261 |
+
"portfolio_value": self.portfolio.total_value if self.portfolio else 0,
|
| 262 |
+
"portfolio_return": self.portfolio.return_percentage if self.portfolio else 0
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
|
| 266 |
+
def calculate_suggested_trade_amount(
|
| 267 |
+
portfolio_value: float,
|
| 268 |
+
risk_level: int = 50
|
| 269 |
+
) -> float:
|
| 270 |
+
"""
|
| 271 |
+
Calculate a suggested trade amount based on portfolio and risk level.
|
| 272 |
+
"""
|
| 273 |
+
# Base: 10-30% of portfolio depending on risk level
|
| 274 |
+
min_pct = 0.10
|
| 275 |
+
max_pct = 0.30
|
| 276 |
+
risk_factor = risk_level / 100
|
| 277 |
+
|
| 278 |
+
suggested_pct = min_pct + (max_pct - min_pct) * risk_factor
|
| 279 |
+
return round(portfolio_value * suggested_pct, 2)
|
| 280 |
+
|
| 281 |
+
|
| 282 |
+
def format_currency(amount: float, symbol: str = "credits") -> str:
|
| 283 |
+
"""Format a currency amount for display."""
|
| 284 |
+
if amount >= 0:
|
| 285 |
+
return f"{amount:,.2f} {symbol}"
|
| 286 |
+
else:
|
| 287 |
+
return f"-{abs(amount):,.2f} {symbol}"
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
def format_percentage(value: float, include_sign: bool = True) -> str:
|
| 291 |
+
"""Format a percentage for display."""
|
| 292 |
+
if include_sign:
|
| 293 |
+
return f"{value * 100:+.1f}%"
|
| 294 |
+
return f"{value * 100:.1f}%"
|