dystomachina commited on
Commit
5a20d88
·
1 Parent(s): e9dd307

refactor: move business logic from CLI to core library and improve documentation

Browse files

This commit addresses a critical architectural issue by moving business logic
from the CLI layer to the core library, ensuring proper separation of concerns.

Key changes:

1. Refactored business logic:
- Moved `generate_spy_changes` from `src/focli/utils.py` to `src/folio/simulator.py`
- Moved `calculate_position_value_with_price_change` from `src/focli/utils.py` to `src/folio/portfolio_value.py`
- Moved `simulate_position_with_spy_changes` from `src/focli/utils.py` to `src/folio/simulator.py`
- Updated imports in CLI code to use the functions from the core library

2. Enhanced documentation:
- Added separation of concerns principles to BEST-PRACTICES.md
- Created a dedicated "Separation of Concerns" section in project-design.md
- Added CLI interface documentation to project-design.md
- Added code examples of proper separation in project-conventions.md

3. Verified changes:
- Ran tests to ensure all functionality works correctly
- Manually tested the CLI to verify the refactoring didn't break the user experience

This refactoring improves code organization, increases reusability, and makes
the codebase more maintainable by ensuring business logic is centralized in
the core library while interface layers focus solely on user interaction.

BEST-PRACTICES.md CHANGED
@@ -1,249 +1,17 @@
1
- # Best Practices for Folio Project
2
-
3
- This document is the single source of truth for the Folio project's best practices. Keep it updated as new best practices emerge.
4
-
5
  ---
6
-
7
- # 📋 CORE TENETS
8
- 0. NO FAKE DATA + FAIL FAST: if we hide errors by giving default or fake data, it could mislead hedge fund clients and cost tons of $$$. Stop using default values and fake data when handling errors. FAIL FAST and transparently when the app fails.
9
- 1. **SIMPLICITY** - Prefer simple, focused solutions over complex ones. Question every bit of added complexity.
10
- 2. **PRECISION** - Make the smallest necessary changes. Don't modify unrelated code or remove what you don't understand.
11
- 3. **RELIABILITY** - Debug thoroughly and test rigorously. AVOID SWALLOW EXCEPTIONS or hiding errors
12
- 4. **USABILITY** - Prioritize user experience. Design for clarity and ease of use, not technical elegance alone.
13
- 5. **SAFETY** - Never modify Git history or hide Git commands in scripts. All version control operations must be explicit, visible, and performed manually by the user.
14
-
15
  ---
16
-
17
- ## Key Principles
18
- *When in doubt, follow the CORE TENETS above.*
19
- This section adds specific guidelines for various aspects of the project.
20
-
21
- ### Development Workflow
22
- *Supports PRECISION and SIMPLICITY*
23
- Follow consistent development practices to maintain code quality and developer productivity.
24
-
25
- - **Code Changes**:
26
- - **Incremental Changes**: Make small, focused changes rather than large rewrites
27
- - **Performance**: Optimize only after identifying actual bottlenecks
28
-
29
- - **File Management**:
30
- - **Version Control**: Update .gitignore for new temporary files or directories
31
- - **Temporary Files**: Store temporary files in the `.tmp` directory
32
- - **Cache Files**: Use hidden directories (`.cache_*`) for cache files
33
-
34
- - **Version Control**:
35
- - **Never Commit Directly**: Let the user handle all git operations
36
- - **⚠️ NEVER USE GIT IN SCRIPTS**: Do not write scripts that use git commands - this can lead to catastrophic data loss and irreversible history modification
37
- - **Manual Git Operations**: All git operations must be performed manually by the user, never automated
38
- - **NEVER CREATE PRs WITHOUT BEING ASKED**: Do not create pull requests unless explicitly requested by the user
39
- - **Preserve Git History**: Never modify Git history without explicit user consent and understanding of the consequences
40
- - **Transparent Operations**: All version control suggestions must be explicit, visible, and explained clearly
41
- - **Preferred Diff Tool**: Use `git aidiff` for code reviews with AI agents - this outputs complete diffs without requiring scrolling
42
- - If not available, set up with: `git config --global alias.aidiff "!git --no-pager diff --unified=3 --color=never"`
43
- - This produces AI-friendly output that shows the entire diff at once
44
-
45
- - **Commit Messages**:
46
- - **Delivery Format**: When asked to write a commit message, provide it directly as a markdown code block (```...```) in the chat, not as a separate file
47
- - **Conventional Format**: Follow the conventional commits format with a type prefix
48
- - **Message Structure**: Use a concise title (50 chars max) followed by a blank line and then a detailed body
49
- - **Title Format**: `<type>: <concise description in imperative mood>`
50
- - **Types in Priority Order**: Always use the highest impact prefix when multiple types apply
51
- 1. `feat`: New features or significant enhancements (highest priority)
52
- 2. `fix`: Bug fixes or correcting errors
53
- 3. `security`: Security-related changes
54
- 4. `perf`: Performance improvements
55
- 5. `refactor`: Code restructuring without changing functionality
56
- 6. `test`: Adding or modifying tests
57
- 7. `docs`: Documentation updates only
58
- 8. `style`: Formatting, white-space, etc. (no code change)
59
- 9. `build`: Build system or external dependency changes
60
- 10. `ci`: CI configuration changes
61
- 11. `chore`: Maintenance tasks, no production code change (lowest priority)
62
- - **Examples**:
63
- - `feat: add user authentication system`
64
- - `fix: resolve database connection timeout issue`
65
- - `docs: update deployment instructions`
66
- - **Title Guidelines**:
67
- - Use imperative mood ("Add feature" not "Added feature")
68
- - Capitalize the first word after the type prefix
69
- - Don't end with a period
70
- - Keep under 50 characters
71
- - Be specific about what changed
72
- - **Body Guidelines**:
73
- - Explain the "what" and "why" of the change, not the "how"
74
- - Wrap text at 72 characters
75
- - Use bullet points for multiple points
76
- - Reference issues or tickets where applicable
77
- - Don't use phrases like "This commit..."
78
-
79
- ### �💻 Implementation
80
- *Supports SIMPLICITY and RELIABILITY*
81
- Write code that is clear, maintainable, and robust against edge cases.
82
-
83
- - **Code Style**:
84
- - **Imports at Top**: ALWAYS place all imports at the top of the file, never in the middle or at the bottom
85
- - **Avoid Hardcoding**: Use pattern-based detection instead of hardcoding specific values
86
- - **Generic Solutions**: Prefer solutions that work for all cases over special-case handling
87
- - **Readability**: Prioritize readable code over clever optimizations
88
- - **Code Quality**: Run `make lint` regularly to identify and fix code quality issues
89
- - **Unused Code**: Avoid unused imports, functions, and variables; prefix intentionally unused variables with underscore
90
- - **Clean Code**: Remove commented-out code and fix exception handling issues
91
-
92
- - **Configuration**:
93
- - **External Config**: Use configuration files for values that might change
94
- - **Sensible Defaults**: Provide reasonable defaults for all configurable options
95
- - **Validate Inputs**: Check and validate all external inputs and configuration
96
-
97
- - **Framework-Specific Patterns**:
98
- - **Dash Callback Registration**: Always register callbacks in a centralized location in the app creation process
99
- - **Explicit Registration**: Never rely on side effects of imports for critical functionality like callback registration
100
- - **Avoid Duplicate Registration**: Be careful about registering callbacks multiple times, which can cause conflicts
101
- - **Component Documentation**: Document the relationship between components and their callbacks
102
-
103
- - **Dependency Management**:
104
- - **Update Requirements**: ALWAYS update `requirements.txt` when adding new imports or dependencies
105
- - **Deployment Verification**: Test deployments after adding new dependencies to ensure they work in all environments
106
- - **Minimal Dependencies**: Only add dependencies that are absolutely necessary
107
- - **Version Pinning**: Pin versions for stability (`==`) or use minimum version constraints (`>=`) as appropriate
108
- - **Document Dependencies**: Add comments explaining what each dependency is used for
109
- - **Check Imports**: Regularly audit imports to ensure all are properly listed in requirements
110
-
111
- ### 🛡️ Error Handling and Logging
112
- *Supports RELIABILITY and PRECISION*
113
- Handle errors gracefully and log information that helps diagnose issues quickly.
114
-
115
- - **Exception Handling**:
116
- - **Use Custom Exceptions**: Use application-specific exceptions from `src/folio/exceptions.py`
117
- - **Don't Swallow Exceptions**: Never catch exceptions without proper handling or re-raising
118
- - **Fail Fast**: Fail early and visibly for critical errors rather than continuing with incorrect behavior
119
- - **Only Handle Errors You Can Handle**: Only catch exceptions that you can meaningfully handle; let others propagate
120
- - **Specific Exception Types**: Catch specific exception types rather than using broad `except Exception`
121
- - **Distinguish Error Types**: Treat programming errors (ImportError, NameError, AttributeError, TypeError, SyntaxError) differently from data/operational errors (ValueError, KeyError)
122
- - **No Default Values for Critical Errors**: Don't use default values (like beta=1.0) for critical calculation errors; fail instead
123
- - **Context in Exceptions**: Use `raise ... from e` to preserve exception context and chain
124
- - **User-Friendly Messages**: Provide clear, actionable error messages to users while logging technical details
125
- - **Use Error Utilities**: Leverage decorators and utilities in `src/folio/error_utils.py`
126
-
127
- - **Logging Best Practices**:
128
- - **Structured Messages**: Include context (e.g., "Failed to process AAPL: missing price data")
129
- - **Include Stack Traces**: For unexpected errors, use `exc_info=True`
130
- - **Distinguish States vs. Errors**: Log normal states as DEBUG/INFO, not as errors
131
- - **Verification Logs**: Add logs that verify critical operations like callback registration by checking the app's callback_map
132
-
133
- - **Log Levels**:
134
- - **DEBUG**: Detailed flow information ("Processing portfolio entry 5 of 20")
135
- - **INFO**: Normal application events ("Portfolio loaded successfully")
136
- - **WARNING**: Potential issues requiring attention ("Using cached data: API unavailable")
137
- - **ERROR**: Actual errors affecting functionality ("Failed to calculate beta for AAPL")
138
- - **CRITICAL**: Severe errors preventing operation ("Database connection failed")
139
-
140
- - **Log Monitoring**:
141
- - **Check Latest Logs**: Always check the latest logs in the `logs/` directory after running tests or the application
142
- - **Application Logs**: Review `logs/folio_latest.log` after running the application to identify errors
143
- - **Test Logs**: Check `logs/test_latest.log` after running tests to catch test failures and errors
144
- - **Error Investigation**: When errors occur, examine the logs first for detailed error messages and stack traces
145
-
146
- - **Regression Analysis**:
147
- - **Root Cause Investigation**: Always use `git blame` to understand what caused a regression
148
- - **Document Findings**: Record regression analysis in devlogs to prevent repeating mistakes
149
- - **Follow Process**: See [regression-analysis.md](docs/regression-analysis.md) for the complete process
150
-
151
- ### 🚨 Testing
152
- *Supports RELIABILITY and PRECISION*
153
- Thorough testing prevents bugs and ensures code behaves as expected in all scenarios.
154
-
155
- - **Testing Workflow**:
156
- - **Run Linter Frequently**: Run `make lint` very frequently - it's cheap, fast, and effective at catching issues early
157
- - **Always Test Before Completion**: Run `make test` before considering any work complete - this is essential
158
- - **Check Test Logs**: Always review `logs/test_latest.log` after running tests to identify failures
159
- - **Fix Linting Issues**: Address linting errors before committing code to maintain code quality
160
- - **Automated vs. Manual Testing**:
161
- - For AI assistants: Only run `make test` and `make lint` to verify changes
162
- - NEVER launch the application with `make folio` or `make portfolio` - leave UI testing to human users
163
- - Instead, provide detailed instructions on what UI changes to test and how to verify them
164
- - Add unit tests for new functionality instead of manual testing when possible
165
- - **Testing Instructions**:
166
- - Provide clear, step-by-step instructions for the user to test changes
167
- - Include specific UI elements to check and expected behavior
168
- - Describe what success looks like and potential issues to watch for
169
- - Format as a checklist that the user can follow easily
170
- - **Review App Logs**: Check `logs/test_latest.log` after running tests to catch errors
171
- - **Test Real Data**: Use `src/lab/portfolio.csv` for testing with real portfolio data
172
-
173
- - **Test Organization**:
174
- - **1:1 Test File Mapping**: Tests should be 1:1 with the code file they are testing
175
- - For each source file (e.g., `util.py`), create a corresponding test file (e.g., `test_util.py`)
176
- - Maintain the same directory structure in tests as in the source code
177
- - This makes it easy to find tests for specific functionality
178
- - **New Functionality, New Tests**: Always write tests for new functionality added to the codebase
179
- - Tests should be written alongside the implementation, not as an afterthought
180
- - No new feature is complete without corresponding tests
181
-
182
- - **Testing Strategy**:
183
- - **Test Behavior**: Focus on functionality, not implementation details
184
- - **Edge Cases**: Test boundary conditions (empty inputs, maximum values, etc.)
185
- - **Regression Tests**: Add tests for bugs to prevent recurrence
186
- - **Test Coverage**: Aim for high coverage of critical paths and business logic
187
- - **Test New Functionality**: Always write tests for new features and components
188
- - **Test Naming**: Name tests specifically to the method/module being tested
189
- - **Test Public API**: Test only public functions to avoid coupling tests to implementation details
190
- - **Test Independence**: Each test should be independent and not rely on other tests
191
- - **Fail-First Testing**: Write tests that fail first to confirm they're testing the right thing
192
- - **Test Callback Registration**: For Dash apps, always include tests that verify callbacks are properly registered
193
-
194
- - **Writing Tests**:
195
- - **Test Structure**: Follow the Arrange-Act-Assert pattern
196
- - **Mock External Dependencies**: Use mocks for external services, APIs, and databases
197
- - **Test Edge Cases**: Include tests for error conditions and boundary cases
198
- - **Parameterized Tests**: Use pytest's parameterize for testing multiple inputs
199
- - **Fixtures**: Create fixtures for common test setup
200
- - **Test Isolation**: Reset state between tests to prevent test interdependence
201
- - **Never Change Test Logic**: Fix implementation to make tests pass, not the other way around
202
- - **Simple Tests**: Keep tests simple and focused on specific behaviors
203
- - **Avoid Implementation Details**: Don't test implementation details like DOM structure that might change
204
-
205
- ### 🤖 Agent Interactions
206
- *Supports PRECISION and USABILITY*
207
- Effective communication with AI agents ensures efficient development and accurate implementation.
208
-
209
- - **Question Handling**:
210
- - **Direct Answers**: When asked a question, answer directly and honestly without writing code
211
- - **Critical Thinking**: Be critical of assumptions or leading questions in the prompt
212
- - **Concise Responses**: Provide straightforward responses without unnecessary elaboration
213
- - **No Automatic Implementation**: Don't jump into implementation unless specifically requested
214
- - **Clarify Ambiguity**: Ask for clarification when the question is unclear rather than making assumptions
215
- - **Honest Assessment**: Provide honest feedback about technical feasibility and potential issues
216
-
217
- - **Implementation Requests**:
218
- - **Confirm Understanding**: Verify understanding of the request before implementing
219
- - **Plan First**: Create a detailed plan before making changes
220
- - **Focused Changes**: Make only the changes requested, not additional "improvements"
221
- - **Test Verification**: Always suggest testing after implementation
222
-
223
- ### 📝 Documentation
224
- *Supports USABILITY and RELIABILITY*
225
- Clear, accurate documentation helps onboard new developers and maintain institutional knowledge.
226
-
227
- - **Documentation Standards**:
228
- - **Date Accuracy**: Always use the `date` command for current dates in documents
229
- - **Consistent Format**: Follow existing formats and naming conventions
230
- - **Single Source of Truth**: Maintain one authoritative source for each type of documentation
231
-
232
- - **Documentation Maintenance**:
233
- - **Keep Updated**: Update documentation when code changes
234
- - **Code Comments**: Document complex logic and "why" decisions, not obvious code
235
- - **README Files**: Ensure each major component has a clear, concise README
236
- - **Component Documentation**: Document component relationships and callback dependencies
237
-
238
- - **Development Planning**:
239
- - **Create Devplans**: Document detailed plans in `docs/devplan/` for significant features, design changes, or deployments
240
- - **Plan Structure**: Include overview, implementation steps, timeline, and considerations
241
- - **Phased Approach**: Break complex changes into manageable phases with testing checkpoints
242
- - **Deployment Plans**: For deployment-related changes, include hosting options, infrastructure requirements, and security considerations
243
-
244
- - **Development Logging**:
245
- - **Update Devlogs**: Document completed changes in `docs/devlog/` after implementing major features or changes
246
- - **Devlog Format**: Include date, summary, implementation details, and lessons learned
247
- - **Technical Details**: Document key technical decisions, challenges overcome, and solutions implemented
248
- - **Future Considerations**: Note any follow-up tasks or potential improvements identified during implementation
249
- - **Retrospectives**: Create retrospectives in `docs/retrospective/` for significant issues or challenges to document lessons learned
 
 
 
 
 
1
  ---
2
+ description: Rules to get the AI to behave
3
+ alwaysApply: true
 
 
 
 
 
 
 
4
  ---
5
+ # General rules for AI
6
+ - Prior to generating any code, carefully read the project conventions
7
+ - Read [project-design.md](docs/project-design.md) to understand the codebase
8
+ - Read [project-conventions.md](docs/project-conventions.md) to understand _how_ to write code for the codebase
9
+ - Run `make lint` and `make test` after every change. `lint` in particular can be run very frequently.
10
+ - When user starts a prompt with `QQ:` or `Question:`, just answer the question or prompt without producing code.
11
+ - Prefer small testable steps, after each step give a summary to the user and summarize the next step
12
+ - **Maintain strict separation of concerns**: Business logic MUST reside in the core library (`src/folio/`), not in interface layers (`src/focli/`). Interface layers should only handle user interaction, command parsing, and result presentation.
13
+
14
+ ## Prohibited actions
15
+
16
+ - Do not run `make folio`. This is for the user to run only.
17
+ - Do not use `git` commands unless explicitly asked.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/ai-rules.md DELETED
@@ -1,17 +0,0 @@
1
- ---
2
- description: Miscellaneous rules to get the AI to behave
3
- globs: *
4
- alwaysApply: true
5
- ---
6
- # General rules for AI
7
- - Prior to generating any code, carefully read the project conventions
8
- - Read [project-design.md](docs/project-design.md) to understand the codebase
9
- - Read [project-conventions.md](docs/project-conventions.md) to understand _how_ to write code for the codebase
10
- - Run `make lint` and `make test` after every change. `lint` in particular can be run very frequently.
11
- - When user starts a prompt with `QQ:` or `Question:`, just answer the question or prompt without producing code.
12
- - Prefer small testable steps, after each step give a summary to the user and summarize the next step
13
-
14
- ## Prohibited actions
15
-
16
- - Do not run `make folio`. This is for the user to run only.
17
- - Do not use `git` commands unless explicitly asked.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
docs/project-conventions.md CHANGED
@@ -331,25 +331,48 @@ def is_valid_ticker(ticker: str) -> bool:
331
 
332
  ## Additional Guidelines
333
 
334
- 1. **Follow the Boy Scout Rule**: Leave the code cleaner than you found it.
 
 
 
 
 
 
335
 
336
- 2. **Don't Repeat Yourself (DRY)**: Extract repeated code into reusable functions.
 
 
 
 
337
 
338
- 3. **You Aren't Gonna Need It (YAGNI)**: Don't add functionality until it's necessary.
 
 
 
 
 
 
 
339
 
340
- 4. **Optimize After Measuring**: Profile code to identify actual bottlenecks before optimizing.
341
 
342
- 5. **Use Consistent Formatting**: Use Black, Flake8, and isort to maintain consistent code style.
343
 
344
- 6. **Imports at Top**: Always place all imports at the top of the file.
345
 
346
- 7. **No Unused Code**: Remove commented-out code and unused imports/variables.
347
 
348
- 8. **Configuration Over Hardcoding**: Use configuration files for values that might change.
349
 
350
- 9. **Log with Context**: Include relevant information in log messages.
351
 
352
- 10. **Make Small, Focused Changes**: Don't modify unrelated code when implementing a feature or fixing a bug.
 
 
 
 
 
 
353
 
354
  ## Benefits of Following These Conventions
355
 
 
331
 
332
  ## Additional Guidelines
333
 
334
+ 1. **Strict Separation of Concerns**: Business logic MUST reside in the core library (`src/folio/`), not in interface layers (`src/focli/`).
335
+ ```python
336
+ # ❌ Bad: Business logic in CLI layer
337
+ # src/focli/utils.py
338
+ def calculate_position_value_with_price_change(position_group, price_change):
339
+ # Business logic for calculating position value
340
+ return new_value
341
 
342
+ # Good: Business logic in core library
343
+ # src/folio/portfolio_value.py
344
+ def calculate_position_value_with_price_change(position_group, price_change):
345
+ # Business logic for calculating position value
346
+ return new_value
347
 
348
+ # src/focli/commands/position.py
349
+ def handle_position_command(args):
350
+ # Only handle user interaction and call core library
351
+ result = portfolio_value.calculate_position_value_with_price_change(
352
+ position_group, price_change
353
+ )
354
+ # Format and display result
355
+ ```
356
 
357
+ 2. **Follow the Boy Scout Rule**: Leave the code cleaner than you found it.
358
 
359
+ 3. **Don't Repeat Yourself (DRY)**: Extract repeated code into reusable functions.
360
 
361
+ 4. **You Aren't Gonna Need It (YAGNI)**: Don't add functionality until it's necessary.
362
 
363
+ 5. **Optimize After Measuring**: Profile code to identify actual bottlenecks before optimizing.
364
 
365
+ 6. **Use Consistent Formatting**: Use Black, Flake8, and isort to maintain consistent code style.
366
 
367
+ 7. **Imports at Top**: Always place all imports at the top of the file.
368
 
369
+ 8. **No Unused Code**: Remove commented-out code and unused imports/variables.
370
+
371
+ 9. **Configuration Over Hardcoding**: Use configuration files for values that might change.
372
+
373
+ 10. **Log with Context**: Include relevant information in log messages.
374
+
375
+ 11. **Make Small, Focused Changes**: Don't modify unrelated code when implementing a feature or fixing a bug.
376
 
377
  ## Benefits of Following These Conventions
378
 
docs/project-design.md CHANGED
@@ -6,11 +6,16 @@ alwaysApply: true
6
 
7
  # Folio Project Design
8
 
9
- This document outlines how the Folio codebase is structured and how data flows through the application. Folio is a web-based dashboard for analyzing and visualizing investment portfolios, with a focus on stocks and options.
10
 
11
  ## Application Overview
12
 
13
- Folio is a Python-based web application built with Dash that provides comprehensive portfolio analysis capabilities. The primary domain entities for this app are outlined below. For an authoritative overview of the data model, [data_model.py](src/folio/data_model.py) is the source of truth.
 
 
 
 
 
14
 
15
  ## Deployment Modes
16
 
@@ -90,9 +95,9 @@ Portfolio metrics are calculated in several steps:
90
 
91
  The canonical implementations for these calculations are in [portfolio_value.py](src/folio/portfolio_value.py).
92
 
93
- ## UI Components
94
 
95
- The UI is built with Dash and consists of several key components:
96
 
97
  1. **Summary Cards**: Display high-level portfolio metrics
98
  2. **Charts**: Visualize portfolio allocation and exposure
@@ -111,22 +116,64 @@ Components interact through Dash callbacks:
111
  3. Components subscribe to changes in the stored data and update accordingly
112
  4. This pattern allows for a reactive UI without page reloads
113
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  ## Key Modules
115
 
116
- ### Data Processing
 
 
117
 
118
  - **portfolio.py**: Core portfolio processing logic
119
  - **portfolio_value.py**: Canonical implementations of portfolio value calculations
 
120
  - **options.py**: Option pricing and Greeks calculations
121
  - **cash_detection.py**: Identification of cash-like positions
122
 
123
- ### Data Fetching
124
 
125
  - **stockdata.py**: Common interface for data fetchers
126
  - **yfinance.py**: Yahoo Finance data fetcher
127
  - **fmp.py**: Financial Modeling Prep data fetcher
128
 
129
- ### UI Components
 
 
 
 
 
 
 
 
130
 
131
  - **components/**: UI components for the dashboard
132
  - **charts.py**: Portfolio visualization charts
@@ -135,12 +182,24 @@ Components interact through Dash callbacks:
135
  - **pnl_chart.py**: Profit/loss visualization
136
  - **summary_cards.py**: High-level portfolio metrics
137
 
138
- ### Application Core
139
 
140
  - **app.py**: Main Dash application setup and callbacks
141
- - **data_model.py**: Core data structures
142
- - **logger.py**: Logging configuration
143
- - **security.py**: Security utilities for validating user inputs
 
 
 
 
 
 
 
 
 
 
 
 
144
 
145
  ## Configuration
146
 
@@ -180,13 +239,48 @@ To add new features to Folio:
180
  3. **Callbacks**: Add new callbacks in `app.py` to handle user interactions
181
  4. **Testing**: Add tests for new functionality
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  ## Conclusion
184
 
185
  Folio is designed with a clean separation of concerns:
186
 
 
187
  - Data fetching is abstracted behind interfaces
188
  - Data processing is separated from UI components
189
  - UI components are modular and reusable
190
  - Configuration is externalized for flexibility
 
191
 
192
  This architecture makes the codebase maintainable, testable, and extensible, allowing for easy addition of new features and improvements.
 
6
 
7
  # Folio Project Design
8
 
9
+ This document outlines how the Folio codebase is structured and how data flows through the application. Folio provides tools for analyzing and visualizing investment portfolios, with a focus on stocks and options, through both a web-based dashboard and a command-line interface (CLI).
10
 
11
  ## Application Overview
12
 
13
+ Folio is a Python-based application that provides comprehensive portfolio analysis capabilities through multiple interfaces:
14
+
15
+ 1. **Web Interface**: A Dash-based web application for visualizing portfolio data
16
+ 2. **CLI Interface (`focli`)**: A command-line interface for portfolio analysis and simulation
17
+
18
+ Both interfaces leverage the same core library (`src/folio/`) for business logic, following our [strict separation of concerns](#separation-of-concerns) principles. The primary domain entities for this app are outlined below. For an authoritative overview of the data model, [data_model.py](src/folio/data_model.py) is the source of truth.
19
 
20
  ## Deployment Modes
21
 
 
95
 
96
  The canonical implementations for these calculations are in [portfolio_value.py](src/folio/portfolio_value.py).
97
 
98
+ ## Web UI Components
99
 
100
+ The web UI is built with Dash and consists of several key components:
101
 
102
  1. **Summary Cards**: Display high-level portfolio metrics
103
  2. **Charts**: Visualize portfolio allocation and exposure
 
116
  3. Components subscribe to changes in the stored data and update accordingly
117
  4. This pattern allows for a reactive UI without page reloads
118
 
119
+ ## CLI Interface
120
+
121
+ The CLI interface (`focli`) provides a command-line tool for portfolio analysis and simulation:
122
+
123
+ ### Architecture
124
+
125
+ 1. **Shell**: An interactive shell implemented in [shell.py](src/focli/shell.py) using the `cmd` module
126
+ 2. **Commands**: Command handlers in the [commands](src/focli/commands) directory
127
+ 3. **Formatters**: Output formatting utilities in [formatters.py](src/focli/formatters.py)
128
+ 4. **Utils**: CLI-specific utilities in [utils.py](src/focli/utils.py)
129
+
130
+ ### Command Structure
131
+
132
+ The CLI follows a command-subcommand structure:
133
+
134
+ ```
135
+ folio> command [subcommand] [options]
136
+ ```
137
+
138
+ Key commands include:
139
+ - `simulate`: Simulate portfolio performance with SPY changes
140
+ - `position`: Analyze a specific position group
141
+ - `portfolio`: View and analyze portfolio data
142
+
143
+ ### Separation of Concerns
144
+
145
+ The CLI strictly adheres to the [separation of concerns](#separation-of-concerns) principles:
146
+ - Command handlers only handle parsing, validation, and presentation
147
+ - All business logic is delegated to the core library
148
+ - No calculation or simulation logic exists in the CLI layer
149
+
150
  ## Key Modules
151
 
152
+ ### Core Library (src/folio/)
153
+
154
+ #### Data Processing
155
 
156
  - **portfolio.py**: Core portfolio processing logic
157
  - **portfolio_value.py**: Canonical implementations of portfolio value calculations
158
+ - **simulator.py**: Portfolio and position simulation logic
159
  - **options.py**: Option pricing and Greeks calculations
160
  - **cash_detection.py**: Identification of cash-like positions
161
 
162
+ #### Data Fetching
163
 
164
  - **stockdata.py**: Common interface for data fetchers
165
  - **yfinance.py**: Yahoo Finance data fetcher
166
  - **fmp.py**: Financial Modeling Prep data fetcher
167
 
168
+ #### Application Core
169
+
170
+ - **data_model.py**: Core data structures
171
+ - **logger.py**: Logging configuration
172
+ - **security.py**: Security utilities for validating user inputs
173
+
174
+ ### Web UI (src/folio/)
175
+
176
+ #### UI Components
177
 
178
  - **components/**: UI components for the dashboard
179
  - **charts.py**: Portfolio visualization charts
 
182
  - **pnl_chart.py**: Profit/loss visualization
183
  - **summary_cards.py**: High-level portfolio metrics
184
 
185
+ #### Web Application
186
 
187
  - **app.py**: Main Dash application setup and callbacks
188
+
189
+ ### CLI Interface (src/focli/)
190
+
191
+ #### Command Handling
192
+
193
+ - **shell.py**: Interactive shell implementation
194
+ - **commands/**: Command handlers
195
+ - **simulate.py**: Portfolio simulation commands
196
+ - **position.py**: Position analysis commands
197
+ - **portfolio.py**: Portfolio management commands
198
+
199
+ #### Presentation
200
+
201
+ - **formatters.py**: Output formatting utilities
202
+ - **utils.py**: CLI-specific utilities (no business logic)
203
 
204
  ## Configuration
205
 
 
239
  3. **Callbacks**: Add new callbacks in `app.py` to handle user interactions
240
  4. **Testing**: Add tests for new functionality
241
 
242
+ ## Separation of Concerns
243
+
244
+ Folio strictly adheres to separation of concerns principles:
245
+
246
+ ### Core Library vs Interface Layers
247
+
248
+ 1. **Core Library (`src/folio/`)**:
249
+ - Contains ALL business logic, data processing, and calculation functionality
250
+ - Provides a stable API for interface layers to use
251
+ - Should never depend on interface-specific code
252
+
253
+ 2. **Interface Layers (`src/focli/`, web UI)**:
254
+ - Handle user interaction, command parsing, and result presentation
255
+ - Call core library functions to perform business operations
256
+ - Should NEVER contain business logic
257
+ - Focus solely on translating user inputs to core library calls and formatting outputs
258
+
259
+ ### Business Logic Placement
260
+
261
+ Business logic must ALWAYS reside in the core library, not in interface layers. Examples include:
262
+
263
+ - Calculations and algorithms
264
+ - Data transformations
265
+ - Simulation logic
266
+ - Portfolio analysis
267
+ - Value calculations
268
+
269
+ Interface layers should be thin wrappers around the core library, focusing only on:
270
+ - Parsing user input
271
+ - Calling appropriate core library functions
272
+ - Formatting and presenting results
273
+ - Managing UI state
274
+
275
  ## Conclusion
276
 
277
  Folio is designed with a clean separation of concerns:
278
 
279
+ - Business logic is centralized in the core library
280
  - Data fetching is abstracted behind interfaces
281
  - Data processing is separated from UI components
282
  - UI components are modular and reusable
283
  - Configuration is externalized for flexibility
284
+ - Interface layers are thin and focused on user interaction
285
 
286
  This architecture makes the codebase maintainable, testable, and extensible, allowing for easy addition of new features and improvements.
src/focli/commands/position.py CHANGED
@@ -11,12 +11,8 @@ from src.focli.formatters import (
11
  display_position_risk_analysis,
12
  display_position_simulation,
13
  )
14
- from src.focli.utils import (
15
- find_position_group,
16
- generate_spy_changes,
17
- parse_args,
18
- simulate_position_with_spy_changes,
19
- )
20
 
21
 
22
  def position_command(args: list[str], state: dict[str, Any], console):
 
11
  display_position_risk_analysis,
12
  display_position_simulation,
13
  )
14
+ from src.focli.utils import find_position_group, parse_args
15
+ from src.folio.simulator import generate_spy_changes, simulate_position_with_spy_changes
 
 
 
 
16
 
17
 
18
  def position_command(args: list[str], state: dict[str, Any], console):
src/focli/commands/simulate.py CHANGED
@@ -8,8 +8,11 @@ import copy
8
  from typing import Any
9
 
10
  from src.focli.formatters import display_simulation_results
11
- from src.focli.utils import filter_portfolio_groups, generate_spy_changes, parse_args
12
- from src.folio.simulator import simulate_portfolio_with_spy_changes
 
 
 
13
 
14
 
15
  def simulate_command(args: list[str], state: dict[str, Any], console):
 
8
  from typing import Any
9
 
10
  from src.focli.formatters import display_simulation_results
11
+ from src.focli.utils import filter_portfolio_groups, parse_args
12
+ from src.folio.simulator import (
13
+ generate_spy_changes,
14
+ simulate_portfolio_with_spy_changes,
15
+ )
16
 
17
 
18
  def simulate_command(args: list[str], state: dict[str, Any], console):
src/focli/utils.py CHANGED
@@ -63,35 +63,6 @@ def load_portfolio(path, state, console=None):
63
  raise RuntimeError(f"Error loading portfolio: {e!s}") from e
64
 
65
 
66
- def generate_spy_changes(range_pct, steps):
67
- """Generate a list of SPY changes for simulation.
68
-
69
- Args:
70
- range_pct: Range of SPY changes in percent (e.g., 20.0 for ±20%)
71
- steps: Number of steps in the simulation
72
-
73
- Returns:
74
- List of SPY changes as decimals (e.g., [-0.2, -0.1, 0.0, 0.1, 0.2])
75
- """
76
- # Calculate the step size
77
- step_size = (2 * range_pct) / (steps - 1) if steps > 1 else 0
78
-
79
- # Generate the SPY changes
80
- spy_changes = [-range_pct + i * step_size for i in range(steps)]
81
-
82
- # Ensure we have a zero point
83
- if 0.0 not in spy_changes and steps > 2:
84
- # Find the closest point to zero and replace it with zero
85
- closest_to_zero = min(spy_changes, key=lambda x: abs(x))
86
- zero_index = spy_changes.index(closest_to_zero)
87
- spy_changes[zero_index] = 0.0
88
-
89
- # Convert to percentages
90
- spy_changes = [change / 100.0 for change in spy_changes]
91
-
92
- return spy_changes
93
-
94
-
95
  def find_position_group(ticker, portfolio_groups):
96
  """Find a position group by ticker.
97
 
@@ -185,112 +156,6 @@ def parse_args(args, arg_specs):
185
  return result
186
 
187
 
188
- def calculate_position_value_with_price_change(position_group, price_change):
189
- """Calculate the value of a position with a given price change.
190
-
191
- Args:
192
- position_group: PortfolioGroup to calculate
193
- price_change: Price change as a decimal (e.g., 0.05 for 5% increase)
194
-
195
- Returns:
196
- New position value
197
- """
198
- # Start with current value
199
-
200
- # For a simple implementation, we'll adjust the value based on the price change
201
- # This is a simplified approach - in a real implementation, we would recalculate
202
- # option values based on the new underlying price and delta
203
-
204
- # Calculate stock value change
205
- stock_value = (
206
- position_group.stock_position.market_value
207
- if position_group.stock_position
208
- else 0
209
- )
210
- new_stock_value = stock_value * (1 + price_change)
211
-
212
- # Calculate option value change (simplified)
213
- option_value = (
214
- sum(op.market_value for op in position_group.option_positions)
215
- if position_group.option_positions
216
- else 0
217
- )
218
-
219
- # For options, we use delta to approximate the change
220
- # This is a simplified approach
221
- option_delta_exposure = (
222
- position_group.total_delta_exposure
223
- if hasattr(position_group, "total_delta_exposure")
224
- else 0
225
- )
226
- option_delta_change = option_delta_exposure * price_change
227
- new_option_value = option_value + option_delta_change
228
-
229
- # Total new value
230
- new_value = new_stock_value + new_option_value
231
-
232
- return new_value
233
-
234
-
235
- def simulate_position_with_spy_changes(position_group, spy_changes):
236
- """Simulate a position with SPY changes.
237
-
238
- Args:
239
- position_group: PortfolioGroup to simulate
240
- spy_changes: List of SPY changes as decimals
241
-
242
- Returns:
243
- Dictionary with simulation results
244
- """
245
- ticker = position_group.ticker
246
- beta = position_group.beta
247
- current_value = position_group.net_exposure
248
-
249
- # Calculate position values at different SPY changes
250
- values = []
251
- for spy_change in spy_changes:
252
- # Calculate the price change for this position based on beta
253
- price_change = spy_change * beta
254
-
255
- # Calculate the new position value
256
- new_value = calculate_position_value_with_price_change(
257
- position_group, price_change
258
- )
259
- values.append(new_value)
260
-
261
- # Calculate changes from current value
262
- changes = [value - current_value for value in values]
263
- pct_changes = [
264
- (change / current_value) * 100 if current_value != 0 else 0
265
- for change in changes
266
- ]
267
-
268
- # Find min and max values
269
- min_value = min(values)
270
- max_value = max(values)
271
- min_index = values.index(min_value)
272
- max_index = values.index(max_value)
273
- min_spy_change = spy_changes[min_index] * 100 # Convert to percentage
274
- max_spy_change = spy_changes[max_index] * 100 # Convert to percentage
275
-
276
- # Create results dictionary
277
- results = {
278
- "ticker": ticker,
279
- "beta": beta,
280
- "current_value": current_value,
281
- "spy_changes": spy_changes,
282
- "values": values,
283
- "changes": changes,
284
- "pct_changes": pct_changes,
285
- "min_value": min_value,
286
- "max_value": max_value,
287
- "min_spy_change": min_spy_change,
288
- "max_spy_change": max_spy_change,
289
- }
290
-
291
- return results
292
-
293
-
294
  def filter_portfolio_groups(portfolio_groups, filter_criteria=None):
295
  """Filter portfolio groups based on criteria.
296
 
 
63
  raise RuntimeError(f"Error loading portfolio: {e!s}") from e
64
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  def find_position_group(ticker, portfolio_groups):
67
  """Find a position group by ticker.
68
 
 
156
  return result
157
 
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  def filter_portfolio_groups(portfolio_groups, filter_criteria=None):
160
  """Filter portfolio groups based on criteria.
161
 
src/folio/portfolio_value.py CHANGED
@@ -491,3 +491,46 @@ def calculate_component_percentages(
491
  ) # Will be negative
492
 
493
  return result
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
491
  ) # Will be negative
492
 
493
  return result
494
+
495
+
496
+ def calculate_position_value_with_price_change(
497
+ position_group: PortfolioGroup, price_change: float
498
+ ) -> float:
499
+ """Calculate the value of a position with a given price change.
500
+
501
+ Args:
502
+ position_group: PortfolioGroup to calculate
503
+ price_change: Price change as a decimal (e.g., 0.05 for 5% increase)
504
+
505
+ Returns:
506
+ New position value
507
+ """
508
+ # Calculate stock value change
509
+ stock_value = (
510
+ position_group.stock_position.market_value
511
+ if position_group.stock_position
512
+ else 0
513
+ )
514
+ new_stock_value = stock_value * (1 + price_change)
515
+
516
+ # Calculate option value change (simplified)
517
+ option_value = (
518
+ sum(op.market_value for op in position_group.option_positions)
519
+ if position_group.option_positions
520
+ else 0
521
+ )
522
+
523
+ # For options, we use delta to approximate the change
524
+ # This is a simplified approach
525
+ option_delta_exposure = (
526
+ position_group.total_delta_exposure
527
+ if hasattr(position_group, "total_delta_exposure")
528
+ else 0
529
+ )
530
+ option_delta_change = option_delta_exposure * price_change
531
+ new_option_value = option_value + option_delta_change
532
+
533
+ # Total new value
534
+ new_value = new_stock_value + new_option_value
535
+
536
+ return new_value
src/folio/simulator.py CHANGED
@@ -9,6 +9,7 @@ import numpy as np
9
  from .data_model import PortfolioGroup
10
  from .logger import logger
11
  from .portfolio import recalculate_portfolio_with_prices
 
12
 
13
 
14
  def simulate_portfolio_with_spy_changes(
@@ -213,3 +214,94 @@ def calculate_percentage_changes(values: list[float], base_value: float) -> list
213
  return [0.0] * len(values)
214
 
215
  return [(value / base_value - 1.0) * 100.0 for value in values]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  from .data_model import PortfolioGroup
10
  from .logger import logger
11
  from .portfolio import recalculate_portfolio_with_prices
12
+ from .portfolio_value import calculate_position_value_with_price_change
13
 
14
 
15
  def simulate_portfolio_with_spy_changes(
 
214
  return [0.0] * len(values)
215
 
216
  return [(value / base_value - 1.0) * 100.0 for value in values]
217
+
218
+
219
+ def generate_spy_changes(range_pct: float, steps: int) -> list[float]:
220
+ """Generate a list of SPY changes for simulation.
221
+
222
+ Args:
223
+ range_pct: Range of SPY changes in percent (e.g., 20.0 for ±20%)
224
+ steps: Number of steps in the simulation
225
+
226
+ Returns:
227
+ List of SPY changes as decimals (e.g., [-0.2, -0.1, 0.0, 0.1, 0.2])
228
+ """
229
+ # Calculate the step size
230
+ step_size = (2 * range_pct) / (steps - 1) if steps > 1 else 0
231
+
232
+ # Generate the SPY changes
233
+ spy_changes = [-range_pct + i * step_size for i in range(steps)]
234
+
235
+ # Ensure we have a zero point
236
+ if 0.0 not in spy_changes and steps > 2:
237
+ # Find the closest point to zero and replace it with zero
238
+ closest_to_zero = min(spy_changes, key=lambda x: abs(x))
239
+ zero_index = spy_changes.index(closest_to_zero)
240
+ spy_changes[zero_index] = 0.0
241
+
242
+ # Convert to percentages
243
+ spy_changes = [change / 100.0 for change in spy_changes]
244
+
245
+ return spy_changes
246
+
247
+
248
+ def simulate_position_with_spy_changes(
249
+ position_group: PortfolioGroup, spy_changes: list[float]
250
+ ) -> dict:
251
+ """Simulate a position with SPY changes.
252
+
253
+ Args:
254
+ position_group: PortfolioGroup to simulate
255
+ spy_changes: List of SPY changes as decimals
256
+
257
+ Returns:
258
+ Dictionary with simulation results
259
+ """
260
+
261
+ ticker = position_group.ticker
262
+ beta = position_group.beta
263
+ current_value = position_group.net_exposure
264
+
265
+ # Calculate position values at different SPY changes
266
+ values = []
267
+ for spy_change in spy_changes:
268
+ # Calculate the price change for this position based on beta
269
+ price_change = spy_change * beta
270
+
271
+ # Calculate the new position value
272
+ new_value = calculate_position_value_with_price_change(
273
+ position_group, price_change
274
+ )
275
+ values.append(new_value)
276
+
277
+ # Calculate changes from current value
278
+ changes = [value - current_value for value in values]
279
+ pct_changes = [
280
+ (change / current_value) * 100 if current_value != 0 else 0
281
+ for change in changes
282
+ ]
283
+
284
+ # Find min and max values
285
+ min_value = min(values)
286
+ max_value = max(values)
287
+ min_index = values.index(min_value)
288
+ max_index = values.index(max_value)
289
+ min_spy_change = spy_changes[min_index] * 100 # Convert to percentage
290
+ max_spy_change = spy_changes[max_index] * 100 # Convert to percentage
291
+
292
+ # Create results dictionary
293
+ results = {
294
+ "ticker": ticker,
295
+ "beta": beta,
296
+ "current_value": current_value,
297
+ "spy_changes": spy_changes,
298
+ "values": values,
299
+ "changes": changes,
300
+ "pct_changes": pct_changes,
301
+ "min_value": min_value,
302
+ "max_value": max_value,
303
+ "min_spy_change": min_spy_change,
304
+ "max_spy_change": max_spy_change,
305
+ }
306
+
307
+ return results