equilibria-mcp-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +142 -0
- package/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/builders/YAMLBuilder.d.ts +77 -0
- package/dist/builders/YAMLBuilder.d.ts.map +1 -0
- package/dist/builders/YAMLBuilder.js +251 -0
- package/dist/builders/YAMLBuilder.js.map +1 -0
- package/dist/economics/constants.d.ts +39 -0
- package/dist/economics/constants.d.ts.map +1 -0
- package/dist/economics/constants.js +46 -0
- package/dist/economics/constants.js.map +1 -0
- package/dist/economics/formulas.d.ts +19 -0
- package/dist/economics/formulas.d.ts.map +1 -0
- package/dist/economics/formulas.js +260 -0
- package/dist/economics/formulas.js.map +1 -0
- package/dist/economics/index.d.ts +9 -0
- package/dist/economics/index.d.ts.map +1 -0
- package/dist/economics/index.js +9 -0
- package/dist/economics/index.js.map +1 -0
- package/dist/economics/validators.d.ts +18 -0
- package/dist/economics/validators.d.ts.map +1 -0
- package/dist/economics/validators.js +111 -0
- package/dist/economics/validators.js.map +1 -0
- package/dist/errors/index.d.ts +172 -0
- package/dist/errors/index.d.ts.map +1 -0
- package/dist/errors/index.js +313 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/formatters/index.d.ts +8 -0
- package/dist/formatters/index.d.ts.map +1 -0
- package/dist/formatters/index.js +8 -0
- package/dist/formatters/index.js.map +1 -0
- package/dist/formatters/redux.d.ts +15 -0
- package/dist/formatters/redux.d.ts.map +1 -0
- package/dist/formatters/redux.js +35 -0
- package/dist/formatters/redux.js.map +1 -0
- package/dist/formatters/yaml.d.ts +18 -0
- package/dist/formatters/yaml.d.ts.map +1 -0
- package/dist/formatters/yaml.js +40 -0
- package/dist/formatters/yaml.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +14 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +86 -0
- package/dist/server.js.map +1 -0
- package/dist/templates/advancedMicro.d.ts +8 -0
- package/dist/templates/advancedMicro.d.ts.map +1 -0
- package/dist/templates/advancedMicro.js +834 -0
- package/dist/templates/advancedMicro.js.map +1 -0
- package/dist/templates/consumer.d.ts +12 -0
- package/dist/templates/consumer.d.ts.map +1 -0
- package/dist/templates/consumer.js +1978 -0
- package/dist/templates/consumer.js.map +1 -0
- package/dist/templates/elasticity.d.ts +8 -0
- package/dist/templates/elasticity.d.ts.map +1 -0
- package/dist/templates/elasticity.js +500 -0
- package/dist/templates/elasticity.js.map +1 -0
- package/dist/templates/externalities.d.ts +11 -0
- package/dist/templates/externalities.d.ts.map +1 -0
- package/dist/templates/externalities.js +997 -0
- package/dist/templates/externalities.js.map +1 -0
- package/dist/templates/financeBehavioral.d.ts +8 -0
- package/dist/templates/financeBehavioral.d.ts.map +1 -0
- package/dist/templates/financeBehavioral.js +860 -0
- package/dist/templates/financeBehavioral.js.map +1 -0
- package/dist/templates/growth.d.ts +8 -0
- package/dist/templates/growth.d.ts.map +1 -0
- package/dist/templates/growth.js +740 -0
- package/dist/templates/growth.js.map +1 -0
- package/dist/templates/index.d.ts +31 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +91 -0
- package/dist/templates/index.js.map +1 -0
- package/dist/templates/inequality.d.ts +8 -0
- package/dist/templates/inequality.d.ts.map +1 -0
- package/dist/templates/inequality.js +562 -0
- package/dist/templates/inequality.js.map +1 -0
- package/dist/templates/intertemporalMacro.d.ts +8 -0
- package/dist/templates/intertemporalMacro.d.ts.map +1 -0
- package/dist/templates/intertemporalMacro.js +550 -0
- package/dist/templates/intertemporalMacro.js.map +1 -0
- package/dist/templates/isLM.d.ts +8 -0
- package/dist/templates/isLM.d.ts.map +1 -0
- package/dist/templates/isLM.js +747 -0
- package/dist/templates/isLM.js.map +1 -0
- package/dist/templates/macro.d.ts +8 -0
- package/dist/templates/macro.d.ts.map +1 -0
- package/dist/templates/macro.js +600 -0
- package/dist/templates/macro.js.map +1 -0
- package/dist/templates/marketStructures.d.ts +11 -0
- package/dist/templates/marketStructures.d.ts.map +1 -0
- package/dist/templates/marketStructures.js +1135 -0
- package/dist/templates/marketStructures.js.map +1 -0
- package/dist/templates/newKeynesian.d.ts +8 -0
- package/dist/templates/newKeynesian.d.ts.map +1 -0
- package/dist/templates/newKeynesian.js +633 -0
- package/dist/templates/newKeynesian.js.map +1 -0
- package/dist/templates/oligopoly.d.ts +11 -0
- package/dist/templates/oligopoly.d.ts.map +1 -0
- package/dist/templates/oligopoly.js +1113 -0
- package/dist/templates/oligopoly.js.map +1 -0
- package/dist/templates/ppf.d.ts +8 -0
- package/dist/templates/ppf.d.ts.map +1 -0
- package/dist/templates/ppf.js +439 -0
- package/dist/templates/ppf.js.map +1 -0
- package/dist/templates/producer.d.ts +11 -0
- package/dist/templates/producer.d.ts.map +1 -0
- package/dist/templates/producer.js +979 -0
- package/dist/templates/producer.js.map +1 -0
- package/dist/templates/production.d.ts +8 -0
- package/dist/templates/production.d.ts.map +1 -0
- package/dist/templates/production.js +574 -0
- package/dist/templates/production.js.map +1 -0
- package/dist/templates/supplyDemand.d.ts +8 -0
- package/dist/templates/supplyDemand.d.ts.map +1 -0
- package/dist/templates/supplyDemand.js +1282 -0
- package/dist/templates/supplyDemand.js.map +1 -0
- package/dist/templates/tradeGrowth.d.ts +8 -0
- package/dist/templates/tradeGrowth.d.ts.map +1 -0
- package/dist/templates/tradeGrowth.js +637 -0
- package/dist/templates/tradeGrowth.js.map +1 -0
- package/dist/tools/index.d.ts +25 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +54 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/models.d.ts +8 -0
- package/dist/tools/models.d.ts.map +1 -0
- package/dist/tools/models.js +828 -0
- package/dist/tools/models.js.map +1 -0
- package/dist/tools/output.d.ts +8 -0
- package/dist/tools/output.d.ts.map +1 -0
- package/dist/tools/output.js +236 -0
- package/dist/tools/output.js.map +1 -0
- package/dist/tools/templates.d.ts +8 -0
- package/dist/tools/templates.d.ts.map +1 -0
- package/dist/tools/templates.js +247 -0
- package/dist/tools/templates.js.map +1 -0
- package/dist/tools/validation.d.ts +8 -0
- package/dist/tools/validation.d.ts.map +1 -0
- package/dist/tools/validation.js +181 -0
- package/dist/tools/validation.js.map +1 -0
- package/dist/types/index.d.ts +187 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +7 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cache.d.ts +99 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +192 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/logger.d.ts +128 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +251 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/validation/index.d.ts +42 -0
- package/dist/validation/index.d.ts.map +1 -0
- package/dist/validation/index.js +282 -0
- package/dist/validation/index.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,1113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Oligopoly Templates
|
|
3
|
+
*
|
|
4
|
+
* Templates for oligopoly models including Cournot, Bertrand, Stackelberg,
|
|
5
|
+
* kinked demand, and monopolistic competition.
|
|
6
|
+
*
|
|
7
|
+
* Phase 3.5: Market Structures - Oligopoly
|
|
8
|
+
*/
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// PHASE 3.5: OLIGOPOLY
|
|
11
|
+
// ============================================================================
|
|
12
|
+
/**
|
|
13
|
+
* Cournot Duopoly
|
|
14
|
+
*
|
|
15
|
+
* Simultaneous quantity competition between two firms.
|
|
16
|
+
* Shows reaction functions and Nash equilibrium.
|
|
17
|
+
*/
|
|
18
|
+
const cournotDuopoly = {
|
|
19
|
+
id: 'cournot_duopoly',
|
|
20
|
+
name: 'Cournot Duopoly',
|
|
21
|
+
description: 'Simultaneous quantity competition. Each firm chooses output taking rival\'s output as given. Nash equilibrium at intersection of reaction functions.',
|
|
22
|
+
category: 'market_structures',
|
|
23
|
+
level: 'undergraduate',
|
|
24
|
+
tags: ['Cournot', 'oligopoly', 'quantity competition', 'reaction function', 'Nash equilibrium'],
|
|
25
|
+
parameters: {
|
|
26
|
+
demandIntercept: {
|
|
27
|
+
type: 'number',
|
|
28
|
+
default: 100,
|
|
29
|
+
min: 50,
|
|
30
|
+
max: 200,
|
|
31
|
+
description: 'Market demand intercept (a)',
|
|
32
|
+
},
|
|
33
|
+
demandSlope: {
|
|
34
|
+
type: 'number',
|
|
35
|
+
default: 1,
|
|
36
|
+
min: 0.5,
|
|
37
|
+
max: 3,
|
|
38
|
+
description: 'Market demand slope (b)',
|
|
39
|
+
},
|
|
40
|
+
mc1: {
|
|
41
|
+
type: 'number',
|
|
42
|
+
default: 10,
|
|
43
|
+
min: 0,
|
|
44
|
+
max: 40,
|
|
45
|
+
description: 'Firm 1 marginal cost',
|
|
46
|
+
},
|
|
47
|
+
mc2: {
|
|
48
|
+
type: 'number',
|
|
49
|
+
default: 10,
|
|
50
|
+
min: 0,
|
|
51
|
+
max: 40,
|
|
52
|
+
description: 'Firm 2 marginal cost',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
generate: (params) => {
|
|
56
|
+
const a = params.demandIntercept ?? 100;
|
|
57
|
+
const b = params.demandSlope ?? 1;
|
|
58
|
+
const c1 = params.mc1 ?? 10;
|
|
59
|
+
const c2 = params.mc2 ?? 10;
|
|
60
|
+
return {
|
|
61
|
+
metadata: {
|
|
62
|
+
specVersion: '1.3',
|
|
63
|
+
title: 'Cournot Duopoly',
|
|
64
|
+
description: 'Simultaneous quantity competition',
|
|
65
|
+
},
|
|
66
|
+
parameters: {
|
|
67
|
+
a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
|
|
68
|
+
b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
|
|
69
|
+
c1: { value: c1, label: 'MC₁', min: 0, max: 40 },
|
|
70
|
+
c2: { value: c2, label: 'MC₂', min: 0, max: 40 },
|
|
71
|
+
// Reaction functions:
|
|
72
|
+
// q1 = (a - c1 - b*q2) / (2b) = (a - c1)/(2b) - q2/2
|
|
73
|
+
// q2 = (a - c2 - b*q1) / (2b) = (a - c2)/(2b) - q1/2
|
|
74
|
+
// Nash equilibrium (solving simultaneously):
|
|
75
|
+
// q1* = (a - 2c1 + c2) / (3b)
|
|
76
|
+
// q2* = (a - 2c2 + c1) / (3b)
|
|
77
|
+
q1_star: {
|
|
78
|
+
expression: '(a - 2 * c1 + c2) / (3 * b)',
|
|
79
|
+
label: 'q₁*',
|
|
80
|
+
readonly: true,
|
|
81
|
+
},
|
|
82
|
+
q2_star: {
|
|
83
|
+
expression: '(a - 2 * c2 + c1) / (3 * b)',
|
|
84
|
+
label: 'q₂*',
|
|
85
|
+
readonly: true,
|
|
86
|
+
},
|
|
87
|
+
// Total output and price
|
|
88
|
+
Q_star: { expression: 'q1_star + q2_star', label: 'Q*', readonly: true },
|
|
89
|
+
P_star: { expression: 'a - b * Q_star', label: 'P*', readonly: true },
|
|
90
|
+
// Profits
|
|
91
|
+
profit1: {
|
|
92
|
+
expression: '(P_star - c1) * q1_star',
|
|
93
|
+
label: 'π₁',
|
|
94
|
+
readonly: true,
|
|
95
|
+
},
|
|
96
|
+
profit2: {
|
|
97
|
+
expression: '(P_star - c2) * q2_star',
|
|
98
|
+
label: 'π₂',
|
|
99
|
+
readonly: true,
|
|
100
|
+
},
|
|
101
|
+
// Reaction function intercepts
|
|
102
|
+
rf1_intercept: { expression: '(a - c1) / (2 * b)', hidden: true },
|
|
103
|
+
rf2_intercept: { expression: '(a - c2) / (2 * b)', hidden: true },
|
|
104
|
+
// Monopoly output for comparison
|
|
105
|
+
q_monopoly: { expression: '(a - c1) / (2 * b)', hidden: true },
|
|
106
|
+
// Competitive output
|
|
107
|
+
q_comp: { expression: '(a - c1) / b', hidden: true },
|
|
108
|
+
// Chart bounds
|
|
109
|
+
chartMax: { expression: 'max(rf1_intercept, rf2_intercept) * 1.2', hidden: true },
|
|
110
|
+
},
|
|
111
|
+
charts: [
|
|
112
|
+
{
|
|
113
|
+
id: 'reaction',
|
|
114
|
+
title: 'Reaction Functions',
|
|
115
|
+
xAxis: { label: 'Firm 1 Output (q₁)', min: 0, max: 'chartMax' },
|
|
116
|
+
yAxis: { label: 'Firm 2 Output (q₂)', min: 0, max: 'chartMax' },
|
|
117
|
+
elements: [
|
|
118
|
+
// Firm 1 reaction function: q1 = (a-c1)/(2b) - q2/2
|
|
119
|
+
// Rearranged as q2 in terms of q1: q2 = (a-c1)/b - 2*q1
|
|
120
|
+
{
|
|
121
|
+
id: 'rf1',
|
|
122
|
+
type: 'line',
|
|
123
|
+
equation: '(a - c1) / b - 2 * x',
|
|
124
|
+
color: '#2563EB',
|
|
125
|
+
strokeWidth: 3,
|
|
126
|
+
label: 'RF₁ (Firm 1 BR)',
|
|
127
|
+
domain: { min: 0, max: 'rf1_intercept' },
|
|
128
|
+
},
|
|
129
|
+
// Firm 2 reaction function: q2 = (a-c2)/(2b) - q1/2
|
|
130
|
+
{
|
|
131
|
+
id: 'rf2',
|
|
132
|
+
type: 'line',
|
|
133
|
+
equation: '(a - c2) / (2 * b) - x / 2',
|
|
134
|
+
color: '#DC2626',
|
|
135
|
+
strokeWidth: 3,
|
|
136
|
+
label: 'RF₂ (Firm 2 BR)',
|
|
137
|
+
domain: { min: 0, max: '(a - c2) / b' },
|
|
138
|
+
},
|
|
139
|
+
// Nash equilibrium
|
|
140
|
+
{
|
|
141
|
+
id: 'nash',
|
|
142
|
+
type: 'point',
|
|
143
|
+
x: 'q1_star',
|
|
144
|
+
y: 'q2_star',
|
|
145
|
+
color: '#16A34A',
|
|
146
|
+
size: 12,
|
|
147
|
+
label: 'Nash Equilibrium',
|
|
148
|
+
droplines: { x: true, y: true },
|
|
149
|
+
},
|
|
150
|
+
// 45-degree line (equal outputs)
|
|
151
|
+
{
|
|
152
|
+
id: 'equal-line',
|
|
153
|
+
type: 'line',
|
|
154
|
+
equation: 'x',
|
|
155
|
+
color: '#9CA3AF',
|
|
156
|
+
strokeWidth: 1,
|
|
157
|
+
lineStyle: 'dotted',
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
annotations: [
|
|
161
|
+
{
|
|
162
|
+
id: 'nash-label',
|
|
163
|
+
text: '(${q1_star:.1f}, ${q2_star:.1f})',
|
|
164
|
+
x: 'q1_star + chartMax * 0.05',
|
|
165
|
+
y: 'q2_star + chartMax * 0.05',
|
|
166
|
+
color: '#16A34A',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
id: 'rf1-label',
|
|
170
|
+
text: 'q₁ = (a-c₁)/2b - q₂/2',
|
|
171
|
+
x: 'chartMax * 0.6',
|
|
172
|
+
y: 'chartMax * 0.9',
|
|
173
|
+
color: '#2563EB',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
id: 'rf2-label',
|
|
177
|
+
text: 'q₂ = (a-c₂)/2b - q₁/2',
|
|
178
|
+
x: 'chartMax * 0.6',
|
|
179
|
+
y: 'chartMax * 0.8',
|
|
180
|
+
color: '#DC2626',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'market',
|
|
186
|
+
title: 'Market Outcome',
|
|
187
|
+
xAxis: { label: 'Total Quantity (Q)', min: 0, max: 'q_comp * 1.2' },
|
|
188
|
+
yAxis: { label: 'Price', min: 0, max: 'a * 1.1' },
|
|
189
|
+
elements: [
|
|
190
|
+
// Demand curve
|
|
191
|
+
{
|
|
192
|
+
id: 'demand',
|
|
193
|
+
type: 'line',
|
|
194
|
+
equation: 'a - b * x',
|
|
195
|
+
color: '#2563EB',
|
|
196
|
+
strokeWidth: 3,
|
|
197
|
+
label: 'Market Demand',
|
|
198
|
+
domain: { min: 0, max: 'a / b' },
|
|
199
|
+
},
|
|
200
|
+
// MC (average of both)
|
|
201
|
+
{
|
|
202
|
+
id: 'mc',
|
|
203
|
+
type: 'horizontalLine',
|
|
204
|
+
y: '(c1 + c2) / 2',
|
|
205
|
+
color: '#DC2626',
|
|
206
|
+
strokeWidth: 2,
|
|
207
|
+
lineStyle: 'dashed',
|
|
208
|
+
label: 'Avg MC',
|
|
209
|
+
},
|
|
210
|
+
// Cournot equilibrium
|
|
211
|
+
{
|
|
212
|
+
id: 'cournot-point',
|
|
213
|
+
type: 'point',
|
|
214
|
+
x: 'Q_star',
|
|
215
|
+
y: 'P_star',
|
|
216
|
+
color: '#16A34A',
|
|
217
|
+
size: 10,
|
|
218
|
+
label: 'Cournot',
|
|
219
|
+
droplines: { x: true, y: true },
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
annotations: [
|
|
223
|
+
{
|
|
224
|
+
id: 'outcome',
|
|
225
|
+
text: 'P* = $${P_star:.1f}, Q* = ${Q_star:.1f}',
|
|
226
|
+
x: 'q_comp * 0.5',
|
|
227
|
+
y: 'a * 0.9',
|
|
228
|
+
color: '#16A34A',
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: 'profits',
|
|
232
|
+
text: 'π₁ = $${profit1:.0f}, π₂ = $${profit2:.0f}',
|
|
233
|
+
x: 'q_comp * 0.5',
|
|
234
|
+
y: 'a * 0.8',
|
|
235
|
+
color: '#6B7280',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
};
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Bertrand Duopoly
|
|
245
|
+
*
|
|
246
|
+
* Simultaneous price competition with homogeneous products.
|
|
247
|
+
* Results in competitive (marginal cost) pricing.
|
|
248
|
+
*/
|
|
249
|
+
const bertrandDuopoly = {
|
|
250
|
+
id: 'bertrand_duopoly',
|
|
251
|
+
name: 'Bertrand Duopoly',
|
|
252
|
+
description: 'Simultaneous price competition with identical products. In equilibrium, both firms price at marginal cost (competitive outcome). The "Bertrand paradox."',
|
|
253
|
+
category: 'market_structures',
|
|
254
|
+
level: 'undergraduate',
|
|
255
|
+
tags: ['Bertrand', 'oligopoly', 'price competition', 'Bertrand paradox'],
|
|
256
|
+
parameters: {
|
|
257
|
+
demandIntercept: {
|
|
258
|
+
type: 'number',
|
|
259
|
+
default: 100,
|
|
260
|
+
description: 'Market demand intercept',
|
|
261
|
+
},
|
|
262
|
+
demandSlope: {
|
|
263
|
+
type: 'number',
|
|
264
|
+
default: 1,
|
|
265
|
+
description: 'Market demand slope',
|
|
266
|
+
},
|
|
267
|
+
marginalCost: {
|
|
268
|
+
type: 'number',
|
|
269
|
+
default: 20,
|
|
270
|
+
description: 'Common marginal cost',
|
|
271
|
+
},
|
|
272
|
+
firm1Price: {
|
|
273
|
+
type: 'number',
|
|
274
|
+
default: 30,
|
|
275
|
+
description: 'Firm 1 price choice (for illustration)',
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
generate: (params) => {
|
|
279
|
+
const a = params.demandIntercept ?? 100;
|
|
280
|
+
const b = params.demandSlope ?? 1;
|
|
281
|
+
const MC = params.marginalCost ?? 20;
|
|
282
|
+
const p1 = params.firm1Price ?? 30;
|
|
283
|
+
return {
|
|
284
|
+
metadata: {
|
|
285
|
+
specVersion: '1.3',
|
|
286
|
+
title: 'Bertrand Duopoly',
|
|
287
|
+
description: 'Price competition with homogeneous products',
|
|
288
|
+
},
|
|
289
|
+
parameters: {
|
|
290
|
+
a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
|
|
291
|
+
b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
|
|
292
|
+
MC: { value: MC, label: 'Marginal Cost', min: 5, max: 50 },
|
|
293
|
+
p1: { value: p1, label: 'Firm 1 Price', min: 'MC', max: 'a' },
|
|
294
|
+
// Bertrand equilibrium: P1 = P2 = MC
|
|
295
|
+
P_star: { expression: 'MC', label: 'P* (equilibrium)', readonly: true },
|
|
296
|
+
// Total quantity at equilibrium
|
|
297
|
+
Q_star: { expression: '(a - MC) / b', label: 'Q*', readonly: true },
|
|
298
|
+
// Each firm gets half (symmetric)
|
|
299
|
+
q_each: { expression: 'Q_star / 2', label: 'q each', readonly: true },
|
|
300
|
+
// Zero profit in equilibrium
|
|
301
|
+
profit_eq: { value: 0, label: 'π* (each)', readonly: true },
|
|
302
|
+
// Compare to monopoly
|
|
303
|
+
P_monopoly: { expression: '(a + MC) / 2', label: 'P (monopoly)', readonly: true },
|
|
304
|
+
Q_monopoly: { expression: '(a - MC) / (2 * b)', hidden: true },
|
|
305
|
+
// What happens at p1 (illustration)
|
|
306
|
+
Q_at_p1: { expression: '(a - p1) / b', hidden: true },
|
|
307
|
+
profit_at_p1: {
|
|
308
|
+
expression: '(p1 - MC) * Q_at_p1',
|
|
309
|
+
label: 'π if monopolist',
|
|
310
|
+
readonly: true,
|
|
311
|
+
},
|
|
312
|
+
// Chart bounds
|
|
313
|
+
chartMaxQ: { expression: 'Q_star * 1.3', hidden: true },
|
|
314
|
+
chartMaxP: { expression: 'a * 1.1', hidden: true },
|
|
315
|
+
},
|
|
316
|
+
charts: [
|
|
317
|
+
{
|
|
318
|
+
id: 'undercutting',
|
|
319
|
+
title: 'Bertrand Price Competition',
|
|
320
|
+
xAxis: { label: 'Firm 1 Price (P₁)', min: 0, max: 'chartMaxP' },
|
|
321
|
+
yAxis: { label: 'Firm 2 Best Response (P₂)', min: 0, max: 'chartMaxP' },
|
|
322
|
+
elements: [
|
|
323
|
+
// Firm 2 best response: undercut P1 if P1 > MC
|
|
324
|
+
// At P1 = MC, indifferent between MC and not entering
|
|
325
|
+
// For P1 > MC: BR is P2 = P1 - ε (just below P1)
|
|
326
|
+
// This creates the "undercut" dynamic
|
|
327
|
+
// 45-degree line (match price)
|
|
328
|
+
{
|
|
329
|
+
id: 'match-line',
|
|
330
|
+
type: 'line',
|
|
331
|
+
equation: 'x',
|
|
332
|
+
color: '#2563EB',
|
|
333
|
+
strokeWidth: 3,
|
|
334
|
+
label: 'Match Price (P₂ = P₁)',
|
|
335
|
+
domain: { min: 'MC', max: 'chartMaxP' },
|
|
336
|
+
},
|
|
337
|
+
// MC horizontal
|
|
338
|
+
{
|
|
339
|
+
id: 'mc-line',
|
|
340
|
+
type: 'horizontalLine',
|
|
341
|
+
y: 'MC',
|
|
342
|
+
color: '#DC2626',
|
|
343
|
+
strokeWidth: 3,
|
|
344
|
+
label: 'MC',
|
|
345
|
+
},
|
|
346
|
+
// MC vertical
|
|
347
|
+
{
|
|
348
|
+
id: 'mc-vertical',
|
|
349
|
+
type: 'verticalLine',
|
|
350
|
+
x: 'MC',
|
|
351
|
+
color: '#DC2626',
|
|
352
|
+
strokeWidth: 3,
|
|
353
|
+
},
|
|
354
|
+
// Nash equilibrium
|
|
355
|
+
{
|
|
356
|
+
id: 'nash',
|
|
357
|
+
type: 'point',
|
|
358
|
+
x: 'MC',
|
|
359
|
+
y: 'MC',
|
|
360
|
+
color: '#16A34A',
|
|
361
|
+
size: 12,
|
|
362
|
+
label: 'Nash: P₁ = P₂ = MC',
|
|
363
|
+
},
|
|
364
|
+
// Current p1 illustration
|
|
365
|
+
{
|
|
366
|
+
id: 'p1-point',
|
|
367
|
+
type: 'point',
|
|
368
|
+
x: 'p1',
|
|
369
|
+
y: 'p1',
|
|
370
|
+
color: '#7C3AED',
|
|
371
|
+
label: 'If P₁ = ${p1}',
|
|
372
|
+
droplines: { x: true, y: true },
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
annotations: [
|
|
376
|
+
{
|
|
377
|
+
id: 'undercutting-note',
|
|
378
|
+
text: 'Any P > MC invites undercutting',
|
|
379
|
+
x: 'chartMaxP * 0.5',
|
|
380
|
+
y: 'chartMaxP * 0.8',
|
|
381
|
+
color: '#6B7280',
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
id: 'paradox',
|
|
385
|
+
text: 'Bertrand Paradox: Only 2 firms → competitive price!',
|
|
386
|
+
x: 'chartMaxP * 0.4',
|
|
387
|
+
y: 'chartMaxP * 0.15',
|
|
388
|
+
color: '#DC2626',
|
|
389
|
+
},
|
|
390
|
+
],
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
id: 'market',
|
|
394
|
+
title: 'Market Outcome',
|
|
395
|
+
xAxis: { label: 'Quantity (Q)', min: 0, max: 'chartMaxQ' },
|
|
396
|
+
yAxis: { label: 'Price', min: 0, max: 'chartMaxP' },
|
|
397
|
+
elements: [
|
|
398
|
+
// Demand
|
|
399
|
+
{
|
|
400
|
+
id: 'demand',
|
|
401
|
+
type: 'line',
|
|
402
|
+
equation: 'a - b * x',
|
|
403
|
+
color: '#2563EB',
|
|
404
|
+
strokeWidth: 3,
|
|
405
|
+
label: 'Demand',
|
|
406
|
+
domain: { min: 0, max: 'a / b' },
|
|
407
|
+
},
|
|
408
|
+
// MC
|
|
409
|
+
{
|
|
410
|
+
id: 'mc',
|
|
411
|
+
type: 'horizontalLine',
|
|
412
|
+
y: 'MC',
|
|
413
|
+
color: '#DC2626',
|
|
414
|
+
strokeWidth: 3,
|
|
415
|
+
label: 'MC = P*',
|
|
416
|
+
},
|
|
417
|
+
// Bertrand equilibrium
|
|
418
|
+
{
|
|
419
|
+
id: 'bertrand-eq',
|
|
420
|
+
type: 'point',
|
|
421
|
+
x: 'Q_star',
|
|
422
|
+
y: 'MC',
|
|
423
|
+
color: '#16A34A',
|
|
424
|
+
size: 10,
|
|
425
|
+
label: 'Bertrand Eq.',
|
|
426
|
+
droplines: { x: true, y: true },
|
|
427
|
+
},
|
|
428
|
+
// Monopoly point for comparison
|
|
429
|
+
{
|
|
430
|
+
id: 'monopoly-point',
|
|
431
|
+
type: 'point',
|
|
432
|
+
x: 'Q_monopoly',
|
|
433
|
+
y: 'P_monopoly',
|
|
434
|
+
color: '#9CA3AF',
|
|
435
|
+
size: 6,
|
|
436
|
+
label: 'Monopoly',
|
|
437
|
+
},
|
|
438
|
+
],
|
|
439
|
+
annotations: [
|
|
440
|
+
{
|
|
441
|
+
id: 'bertrand-result',
|
|
442
|
+
text: 'P* = MC = $${MC}, Q* = ${Q_star:.0f}',
|
|
443
|
+
x: 'chartMaxQ * 0.5',
|
|
444
|
+
y: 'chartMaxP * 0.9',
|
|
445
|
+
color: '#16A34A',
|
|
446
|
+
},
|
|
447
|
+
{
|
|
448
|
+
id: 'zero-profit',
|
|
449
|
+
text: 'π₁ = π₂ = 0 (competitive outcome)',
|
|
450
|
+
x: 'chartMaxQ * 0.5',
|
|
451
|
+
y: 'chartMaxP * 0.8',
|
|
452
|
+
color: '#6B7280',
|
|
453
|
+
},
|
|
454
|
+
],
|
|
455
|
+
},
|
|
456
|
+
],
|
|
457
|
+
};
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
/**
|
|
461
|
+
* Stackelberg Duopoly
|
|
462
|
+
*
|
|
463
|
+
* Sequential quantity competition with a first-mover (leader)
|
|
464
|
+
* and a follower. Leader has first-mover advantage.
|
|
465
|
+
*/
|
|
466
|
+
const stackelbergDuopoly = {
|
|
467
|
+
id: 'stackelberg_duopoly',
|
|
468
|
+
name: 'Stackelberg Duopoly',
|
|
469
|
+
description: 'Sequential quantity competition. Leader moves first, follower observes and responds. Leader has first-mover advantage and produces more than follower.',
|
|
470
|
+
category: 'market_structures',
|
|
471
|
+
level: 'undergraduate',
|
|
472
|
+
tags: ['Stackelberg', 'oligopoly', 'sequential game', 'first-mover advantage', 'leader-follower'],
|
|
473
|
+
parameters: {
|
|
474
|
+
demandIntercept: {
|
|
475
|
+
type: 'number',
|
|
476
|
+
default: 100,
|
|
477
|
+
description: 'Demand intercept',
|
|
478
|
+
},
|
|
479
|
+
demandSlope: {
|
|
480
|
+
type: 'number',
|
|
481
|
+
default: 1,
|
|
482
|
+
description: 'Demand slope',
|
|
483
|
+
},
|
|
484
|
+
marginalCost: {
|
|
485
|
+
type: 'number',
|
|
486
|
+
default: 10,
|
|
487
|
+
description: 'Common marginal cost',
|
|
488
|
+
},
|
|
489
|
+
},
|
|
490
|
+
generate: (params) => {
|
|
491
|
+
const a = params.demandIntercept ?? 100;
|
|
492
|
+
const b = params.demandSlope ?? 1;
|
|
493
|
+
const c = params.marginalCost ?? 10;
|
|
494
|
+
return {
|
|
495
|
+
metadata: {
|
|
496
|
+
specVersion: '1.3',
|
|
497
|
+
title: 'Stackelberg Duopoly',
|
|
498
|
+
description: 'Sequential quantity competition',
|
|
499
|
+
},
|
|
500
|
+
parameters: {
|
|
501
|
+
a: { value: a, label: 'Demand Intercept', min: 50, max: 200 },
|
|
502
|
+
b: { value: b, label: 'Demand Slope', min: 0.5, max: 3, step: 0.1 },
|
|
503
|
+
c: { value: c, label: 'Marginal Cost', min: 0, max: 40 },
|
|
504
|
+
// Follower's reaction function: q2 = (a - c)/(2b) - q1/2
|
|
505
|
+
// Leader maximizes: π1 = (a - b*(q1 + q2) - c) * q1
|
|
506
|
+
// Substituting follower's RF: q1* = (a - c) / (2b)
|
|
507
|
+
q_leader: {
|
|
508
|
+
expression: '(a - c) / (2 * b)',
|
|
509
|
+
label: 'q_L (leader)',
|
|
510
|
+
readonly: true,
|
|
511
|
+
},
|
|
512
|
+
// Follower's response: q2 = (a-c)/(2b) - q1/2 = (a-c)/(4b)
|
|
513
|
+
q_follower: {
|
|
514
|
+
expression: '(a - c) / (4 * b)',
|
|
515
|
+
label: 'q_F (follower)',
|
|
516
|
+
readonly: true,
|
|
517
|
+
},
|
|
518
|
+
// Total output
|
|
519
|
+
Q_stack: { expression: 'q_leader + q_follower', label: 'Q (total)', readonly: true },
|
|
520
|
+
// Price
|
|
521
|
+
P_stack: { expression: 'a - b * Q_stack', label: 'P*', readonly: true },
|
|
522
|
+
// Profits
|
|
523
|
+
profit_leader: {
|
|
524
|
+
expression: '(P_stack - c) * q_leader',
|
|
525
|
+
label: 'π_L',
|
|
526
|
+
readonly: true,
|
|
527
|
+
},
|
|
528
|
+
profit_follower: {
|
|
529
|
+
expression: '(P_stack - c) * q_follower',
|
|
530
|
+
label: 'π_F',
|
|
531
|
+
readonly: true,
|
|
532
|
+
},
|
|
533
|
+
// Cournot for comparison
|
|
534
|
+
q_cournot: { expression: '(a - c) / (3 * b)', hidden: true },
|
|
535
|
+
// Chart bounds
|
|
536
|
+
chartMax: { expression: '(a - c) / b * 0.7', hidden: true },
|
|
537
|
+
},
|
|
538
|
+
charts: [
|
|
539
|
+
{
|
|
540
|
+
id: 'reaction',
|
|
541
|
+
title: 'Stackelberg Equilibrium',
|
|
542
|
+
xAxis: { label: 'Leader Output (q_L)', min: 0, max: 'chartMax' },
|
|
543
|
+
yAxis: { label: 'Follower Output (q_F)', min: 0, max: 'chartMax' },
|
|
544
|
+
elements: [
|
|
545
|
+
// Follower's reaction function
|
|
546
|
+
{
|
|
547
|
+
id: 'rf-follower',
|
|
548
|
+
type: 'line',
|
|
549
|
+
equation: '(a - c) / (2 * b) - x / 2',
|
|
550
|
+
color: '#DC2626',
|
|
551
|
+
strokeWidth: 3,
|
|
552
|
+
label: 'Follower RF',
|
|
553
|
+
domain: { min: 0, max: '(a - c) / b' },
|
|
554
|
+
},
|
|
555
|
+
// Leader's isoprofit curves (higher profit toward origin)
|
|
556
|
+
// π_L = (a - b*(q1 + q2) - c) * q1
|
|
557
|
+
// For a given profit level π: q2 = (a-c)/b - q1 - π/(b*q1)
|
|
558
|
+
{
|
|
559
|
+
id: 'isoprofit-leader',
|
|
560
|
+
type: 'line',
|
|
561
|
+
equation: '(a - c) / b - x - profit_leader / (b * x)',
|
|
562
|
+
color: '#2563EB',
|
|
563
|
+
strokeWidth: 2,
|
|
564
|
+
lineStyle: 'dashed',
|
|
565
|
+
label: 'Leader Isoprofit',
|
|
566
|
+
domain: { min: 'q_leader * 0.3', max: 'q_leader * 1.5' },
|
|
567
|
+
},
|
|
568
|
+
// Stackelberg equilibrium
|
|
569
|
+
{
|
|
570
|
+
id: 'stackelberg-eq',
|
|
571
|
+
type: 'point',
|
|
572
|
+
x: 'q_leader',
|
|
573
|
+
y: 'q_follower',
|
|
574
|
+
color: '#16A34A',
|
|
575
|
+
size: 12,
|
|
576
|
+
label: 'Stackelberg',
|
|
577
|
+
droplines: { x: true, y: true },
|
|
578
|
+
},
|
|
579
|
+
// Cournot for comparison
|
|
580
|
+
{
|
|
581
|
+
id: 'cournot-eq',
|
|
582
|
+
type: 'point',
|
|
583
|
+
x: 'q_cournot',
|
|
584
|
+
y: 'q_cournot',
|
|
585
|
+
color: '#9CA3AF',
|
|
586
|
+
size: 8,
|
|
587
|
+
label: 'Cournot',
|
|
588
|
+
},
|
|
589
|
+
// 45-degree line
|
|
590
|
+
{
|
|
591
|
+
id: 'equal-line',
|
|
592
|
+
type: 'line',
|
|
593
|
+
equation: 'x',
|
|
594
|
+
color: '#E5E7EB',
|
|
595
|
+
strokeWidth: 1,
|
|
596
|
+
lineStyle: 'dotted',
|
|
597
|
+
},
|
|
598
|
+
],
|
|
599
|
+
annotations: [
|
|
600
|
+
{
|
|
601
|
+
id: 'leader-advantage',
|
|
602
|
+
text: 'Leader: ${q_leader:.1f} > Follower: ${q_follower:.1f}',
|
|
603
|
+
x: 'chartMax * 0.5',
|
|
604
|
+
y: 'chartMax * 0.9',
|
|
605
|
+
color: '#16A34A',
|
|
606
|
+
},
|
|
607
|
+
{
|
|
608
|
+
id: 'tangency',
|
|
609
|
+
text: 'Tangency = optimal commitment',
|
|
610
|
+
x: 'q_leader * 1.1',
|
|
611
|
+
y: 'q_follower * 1.3',
|
|
612
|
+
color: '#2563EB',
|
|
613
|
+
},
|
|
614
|
+
],
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
id: 'profit-compare',
|
|
618
|
+
title: 'Profit Comparison',
|
|
619
|
+
xAxis: { label: 'Firm', min: 0, max: 3 },
|
|
620
|
+
yAxis: { label: 'Profit', min: 0, max: 'profit_leader * 1.5' },
|
|
621
|
+
elements: [
|
|
622
|
+
// Leader profit bar
|
|
623
|
+
{
|
|
624
|
+
id: 'leader-bar',
|
|
625
|
+
type: 'rectangle',
|
|
626
|
+
x1: 0.5,
|
|
627
|
+
y1: 0,
|
|
628
|
+
x2: 1.5,
|
|
629
|
+
y2: 'profit_leader',
|
|
630
|
+
fill: '#2563EB',
|
|
631
|
+
opacity: 0.7,
|
|
632
|
+
},
|
|
633
|
+
// Follower profit bar
|
|
634
|
+
{
|
|
635
|
+
id: 'follower-bar',
|
|
636
|
+
type: 'rectangle',
|
|
637
|
+
x1: 1.5,
|
|
638
|
+
y1: 0,
|
|
639
|
+
x2: 2.5,
|
|
640
|
+
y2: 'profit_follower',
|
|
641
|
+
fill: '#DC2626',
|
|
642
|
+
opacity: 0.7,
|
|
643
|
+
},
|
|
644
|
+
],
|
|
645
|
+
annotations: [
|
|
646
|
+
{
|
|
647
|
+
id: 'leader-label',
|
|
648
|
+
text: 'Leader: $${profit_leader:.0f}',
|
|
649
|
+
x: 1,
|
|
650
|
+
y: 'profit_leader * 1.1',
|
|
651
|
+
color: '#2563EB',
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
id: 'follower-label',
|
|
655
|
+
text: 'Follower: $${profit_follower:.0f}',
|
|
656
|
+
x: 2,
|
|
657
|
+
y: 'profit_follower * 1.1',
|
|
658
|
+
color: '#DC2626',
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
},
|
|
662
|
+
],
|
|
663
|
+
};
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
/**
|
|
667
|
+
* Kinked Demand Curve
|
|
668
|
+
*
|
|
669
|
+
* Oligopoly model explaining price rigidity through
|
|
670
|
+
* asymmetric responses to price changes.
|
|
671
|
+
*/
|
|
672
|
+
const kinkedDemand = {
|
|
673
|
+
id: 'kinked_demand_oligopoly',
|
|
674
|
+
name: 'Kinked Demand Curve',
|
|
675
|
+
description: 'Model explaining price rigidity in oligopoly. Rivals match price cuts but not increases, creating a kink in demand and discontinuity in MR.',
|
|
676
|
+
category: 'market_structures',
|
|
677
|
+
level: 'undergraduate',
|
|
678
|
+
tags: ['kinked demand', 'oligopoly', 'price rigidity', 'price stickiness'],
|
|
679
|
+
parameters: {
|
|
680
|
+
currentPrice: {
|
|
681
|
+
type: 'number',
|
|
682
|
+
default: 50,
|
|
683
|
+
description: 'Current (kinked) price',
|
|
684
|
+
},
|
|
685
|
+
currentQuantity: {
|
|
686
|
+
type: 'number',
|
|
687
|
+
default: 50,
|
|
688
|
+
description: 'Current quantity at kink',
|
|
689
|
+
},
|
|
690
|
+
elasticSlope: {
|
|
691
|
+
type: 'number',
|
|
692
|
+
default: 0.5,
|
|
693
|
+
description: 'Slope for price increases (more elastic)',
|
|
694
|
+
},
|
|
695
|
+
inelasticSlope: {
|
|
696
|
+
type: 'number',
|
|
697
|
+
default: 2,
|
|
698
|
+
description: 'Slope for price decreases (less elastic)',
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
generate: (params) => {
|
|
702
|
+
const P_kink = params.currentPrice ?? 50;
|
|
703
|
+
const Q_kink = params.currentQuantity ?? 50;
|
|
704
|
+
const b_up = params.elasticSlope ?? 0.5;
|
|
705
|
+
const b_down = params.inelasticSlope ?? 2;
|
|
706
|
+
return {
|
|
707
|
+
metadata: {
|
|
708
|
+
specVersion: '1.3',
|
|
709
|
+
title: 'Kinked Demand Curve',
|
|
710
|
+
description: 'Price rigidity in oligopoly',
|
|
711
|
+
},
|
|
712
|
+
parameters: {
|
|
713
|
+
P_kink: { value: P_kink, label: 'Current Price', min: 20, max: 100 },
|
|
714
|
+
Q_kink: { value: Q_kink, label: 'Current Quantity', min: 20, max: 100 },
|
|
715
|
+
b_up: { value: b_up, label: 'Elastic Slope', min: 0.2, max: 1, step: 0.1 },
|
|
716
|
+
b_down: { value: b_down, label: 'Inelastic Slope', min: 1, max: 4, step: 0.1 },
|
|
717
|
+
// Demand intercepts (solving P = a - bQ at kink point)
|
|
718
|
+
a_up: { expression: 'P_kink + b_up * Q_kink', hidden: true },
|
|
719
|
+
a_down: { expression: 'P_kink + b_down * Q_kink', hidden: true },
|
|
720
|
+
// MR discontinuity at kink
|
|
721
|
+
MR_upper: { expression: 'a_up - 2 * b_up * Q_kink', hidden: true },
|
|
722
|
+
MR_lower: { expression: 'a_down - 2 * b_down * Q_kink', hidden: true },
|
|
723
|
+
MR_gap: {
|
|
724
|
+
expression: 'MR_upper - MR_lower',
|
|
725
|
+
label: 'MR Gap',
|
|
726
|
+
readonly: true,
|
|
727
|
+
},
|
|
728
|
+
// Chart bounds
|
|
729
|
+
chartMaxQ: { expression: 'Q_kink * 2', hidden: true },
|
|
730
|
+
chartMaxP: { expression: 'P_kink * 2', hidden: true },
|
|
731
|
+
},
|
|
732
|
+
charts: [
|
|
733
|
+
{
|
|
734
|
+
id: 'main',
|
|
735
|
+
title: 'Kinked Demand Curve',
|
|
736
|
+
xAxis: { label: 'Quantity (Q)', min: 0, max: 'chartMaxQ' },
|
|
737
|
+
yAxis: { label: 'Price', min: 0, max: 'chartMaxP' },
|
|
738
|
+
elements: [
|
|
739
|
+
// Upper demand segment (elastic - for price increases)
|
|
740
|
+
{
|
|
741
|
+
id: 'demand-upper',
|
|
742
|
+
type: 'line',
|
|
743
|
+
equation: 'a_up - b_up * x',
|
|
744
|
+
color: '#2563EB',
|
|
745
|
+
strokeWidth: 3,
|
|
746
|
+
label: 'Elastic (↑P → rivals don\'t follow)',
|
|
747
|
+
domain: { min: 0, max: 'Q_kink' },
|
|
748
|
+
},
|
|
749
|
+
// Lower demand segment (inelastic - for price cuts)
|
|
750
|
+
{
|
|
751
|
+
id: 'demand-lower',
|
|
752
|
+
type: 'line',
|
|
753
|
+
equation: 'a_down - b_down * x',
|
|
754
|
+
color: '#2563EB',
|
|
755
|
+
strokeWidth: 3,
|
|
756
|
+
label: 'Inelastic (↓P → rivals match)',
|
|
757
|
+
domain: { min: 'Q_kink', max: 'a_down / b_down' },
|
|
758
|
+
},
|
|
759
|
+
// Upper MR segment
|
|
760
|
+
{
|
|
761
|
+
id: 'mr-upper',
|
|
762
|
+
type: 'line',
|
|
763
|
+
equation: 'a_up - 2 * b_up * x',
|
|
764
|
+
color: '#7C3AED',
|
|
765
|
+
strokeWidth: 2,
|
|
766
|
+
label: 'MR (upper)',
|
|
767
|
+
domain: { min: 0, max: 'Q_kink' },
|
|
768
|
+
},
|
|
769
|
+
// Lower MR segment
|
|
770
|
+
{
|
|
771
|
+
id: 'mr-lower',
|
|
772
|
+
type: 'line',
|
|
773
|
+
equation: 'a_down - 2 * b_down * x',
|
|
774
|
+
color: '#7C3AED',
|
|
775
|
+
strokeWidth: 2,
|
|
776
|
+
label: 'MR (lower)',
|
|
777
|
+
domain: { min: 'Q_kink', max: 'a_down / (2 * b_down)' },
|
|
778
|
+
},
|
|
779
|
+
// MR gap (vertical discontinuity)
|
|
780
|
+
{
|
|
781
|
+
id: 'mr-gap',
|
|
782
|
+
type: 'verticalLine',
|
|
783
|
+
x: 'Q_kink',
|
|
784
|
+
color: '#7C3AED',
|
|
785
|
+
strokeWidth: 3,
|
|
786
|
+
domain: { min: 'MR_lower', max: 'MR_upper' },
|
|
787
|
+
},
|
|
788
|
+
// Kink point
|
|
789
|
+
{
|
|
790
|
+
id: 'kink',
|
|
791
|
+
type: 'point',
|
|
792
|
+
x: 'Q_kink',
|
|
793
|
+
y: 'P_kink',
|
|
794
|
+
color: '#DC2626',
|
|
795
|
+
size: 12,
|
|
796
|
+
label: 'Kink',
|
|
797
|
+
droplines: { x: true, y: true },
|
|
798
|
+
},
|
|
799
|
+
// Example MC in the gap (explaining price rigidity)
|
|
800
|
+
{
|
|
801
|
+
id: 'mc-example',
|
|
802
|
+
type: 'horizontalLine',
|
|
803
|
+
y: '(MR_upper + MR_lower) / 2',
|
|
804
|
+
color: '#DC2626',
|
|
805
|
+
strokeWidth: 2,
|
|
806
|
+
lineStyle: 'dashed',
|
|
807
|
+
label: 'MC (in gap)',
|
|
808
|
+
},
|
|
809
|
+
],
|
|
810
|
+
annotations: [
|
|
811
|
+
{
|
|
812
|
+
id: 'rigidity-label',
|
|
813
|
+
text: 'MC can shift within gap',
|
|
814
|
+
x: 'Q_kink * 1.3',
|
|
815
|
+
y: '(MR_upper + MR_lower) / 2',
|
|
816
|
+
color: '#DC2626',
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
id: 'no-price-change',
|
|
820
|
+
text: '→ Price stays at $${P_kink}',
|
|
821
|
+
x: 'Q_kink * 1.3',
|
|
822
|
+
y: '(MR_upper + MR_lower) / 2 - chartMaxP * 0.1',
|
|
823
|
+
color: '#DC2626',
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
id: 'gap-size',
|
|
827
|
+
text: 'MR gap = $${MR_gap:.1f}',
|
|
828
|
+
x: 'Q_kink + chartMaxQ * 0.05',
|
|
829
|
+
y: 'MR_upper',
|
|
830
|
+
color: '#7C3AED',
|
|
831
|
+
},
|
|
832
|
+
],
|
|
833
|
+
},
|
|
834
|
+
],
|
|
835
|
+
};
|
|
836
|
+
},
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* Monopolistic Competition
|
|
840
|
+
*
|
|
841
|
+
* Market with many firms selling differentiated products.
|
|
842
|
+
* Shows short-run profit and long-run zero-profit tangency.
|
|
843
|
+
*/
|
|
844
|
+
const monopolisticCompetition = {
|
|
845
|
+
id: 'monopolistic_competition',
|
|
846
|
+
name: 'Monopolistic Competition',
|
|
847
|
+
description: 'Many firms with differentiated products. Short-run profits attract entry until long-run equilibrium where demand is tangent to ATC (zero profit).',
|
|
848
|
+
category: 'market_structures',
|
|
849
|
+
level: 'undergraduate',
|
|
850
|
+
tags: ['monopolistic competition', 'product differentiation', 'free entry', 'tangency'],
|
|
851
|
+
parameters: {
|
|
852
|
+
demandInterceptSR: {
|
|
853
|
+
type: 'number',
|
|
854
|
+
default: 100,
|
|
855
|
+
description: 'Short-run demand intercept',
|
|
856
|
+
},
|
|
857
|
+
demandSlope: {
|
|
858
|
+
type: 'number',
|
|
859
|
+
default: 2,
|
|
860
|
+
description: 'Demand slope',
|
|
861
|
+
},
|
|
862
|
+
minATC: {
|
|
863
|
+
type: 'number',
|
|
864
|
+
default: 30,
|
|
865
|
+
description: 'Minimum average total cost',
|
|
866
|
+
},
|
|
867
|
+
efficientScale: {
|
|
868
|
+
type: 'number',
|
|
869
|
+
default: 20,
|
|
870
|
+
description: 'Output at minimum ATC',
|
|
871
|
+
},
|
|
872
|
+
atcCurvature: {
|
|
873
|
+
type: 'number',
|
|
874
|
+
default: 0.05,
|
|
875
|
+
description: 'ATC curvature parameter',
|
|
876
|
+
},
|
|
877
|
+
},
|
|
878
|
+
generate: (params) => {
|
|
879
|
+
const a_sr = params.demandInterceptSR ?? 100;
|
|
880
|
+
const b = params.demandSlope ?? 2;
|
|
881
|
+
const minATC = params.minATC ?? 30;
|
|
882
|
+
const Q_eff = params.efficientScale ?? 20;
|
|
883
|
+
const c = params.atcCurvature ?? 0.05;
|
|
884
|
+
return {
|
|
885
|
+
metadata: {
|
|
886
|
+
specVersion: '1.3',
|
|
887
|
+
title: 'Monopolistic Competition',
|
|
888
|
+
description: 'Short-run and long-run equilibrium',
|
|
889
|
+
},
|
|
890
|
+
parameters: {
|
|
891
|
+
a_sr: { value: a_sr, label: 'SR Demand Intercept', min: 60, max: 150 },
|
|
892
|
+
b: { value: b, label: 'Demand Slope', min: 1, max: 5, step: 0.1 },
|
|
893
|
+
minATC: { value: minATC, label: 'Min ATC', min: 10, max: 60 },
|
|
894
|
+
Q_eff: { value: Q_eff, label: 'Efficient Scale', min: 10, max: 40 },
|
|
895
|
+
c: { value: c, label: 'ATC Curvature', min: 0.01, max: 0.1, step: 0.01 },
|
|
896
|
+
// ATC: minATC + c*(Q - Q_eff)²
|
|
897
|
+
// MC: derivative of TC = minATC + 2c*(Q - Q_eff) + 2*c*Q_eff (approximately)
|
|
898
|
+
// Short-run: MR = MC at Q_sr, then P_sr from demand
|
|
899
|
+
// For simplicity, calculate SR equilibrium numerically
|
|
900
|
+
Q_sr: { expression: '(a_sr - minATC) / (2 * b + 2 * c)', label: 'Q_SR', readonly: true },
|
|
901
|
+
P_sr: { expression: 'a_sr - b * Q_sr', label: 'P_SR', readonly: true },
|
|
902
|
+
ATC_sr: {
|
|
903
|
+
expression: 'minATC + c * ((Q_sr - Q_eff) ^ 2)',
|
|
904
|
+
label: 'ATC at Q_SR',
|
|
905
|
+
readonly: true,
|
|
906
|
+
},
|
|
907
|
+
profit_sr: {
|
|
908
|
+
expression: '(P_sr - ATC_sr) * Q_sr',
|
|
909
|
+
label: 'SR Profit',
|
|
910
|
+
readonly: true,
|
|
911
|
+
},
|
|
912
|
+
// Long-run: demand tangent to ATC at zero profit
|
|
913
|
+
// At tangency: slope of demand = slope of ATC
|
|
914
|
+
// -b = 2c(Q - Q_eff), and P = ATC
|
|
915
|
+
// Solving: Q_lr and then a_lr (new demand after entry)
|
|
916
|
+
Q_lr: { expression: 'Q_eff - b / (2 * c)', label: 'Q_LR', readonly: true },
|
|
917
|
+
P_lr: {
|
|
918
|
+
expression: 'minATC + c * ((Q_lr - Q_eff) ^ 2)',
|
|
919
|
+
label: 'P_LR',
|
|
920
|
+
readonly: true,
|
|
921
|
+
},
|
|
922
|
+
a_lr: { expression: 'P_lr + b * Q_lr', hidden: true },
|
|
923
|
+
// Chart bounds
|
|
924
|
+
chartMaxQ: { expression: 'max(Q_sr, Q_eff) * 2', hidden: true },
|
|
925
|
+
chartMaxP: { expression: 'max(P_sr, a_sr) * 1.2', hidden: true },
|
|
926
|
+
},
|
|
927
|
+
charts: [
|
|
928
|
+
{
|
|
929
|
+
id: 'short-run',
|
|
930
|
+
title: 'Short-Run: Positive Profit',
|
|
931
|
+
xAxis: { label: 'Quantity (q)', min: 0, max: 'chartMaxQ' },
|
|
932
|
+
yAxis: { label: 'Price, Cost', min: 0, max: 'chartMaxP' },
|
|
933
|
+
elements: [
|
|
934
|
+
// Profit rectangle
|
|
935
|
+
{
|
|
936
|
+
id: 'profit-area',
|
|
937
|
+
type: 'rectangle',
|
|
938
|
+
x1: 0,
|
|
939
|
+
y1: 'ATC_sr',
|
|
940
|
+
x2: 'Q_sr',
|
|
941
|
+
y2: 'P_sr',
|
|
942
|
+
fill: '#22C55E',
|
|
943
|
+
opacity: 0.3,
|
|
944
|
+
label: 'Profit',
|
|
945
|
+
},
|
|
946
|
+
// Demand
|
|
947
|
+
{
|
|
948
|
+
id: 'demand-sr',
|
|
949
|
+
type: 'line',
|
|
950
|
+
equation: 'a_sr - b * x',
|
|
951
|
+
color: '#2563EB',
|
|
952
|
+
strokeWidth: 3,
|
|
953
|
+
label: 'Demand',
|
|
954
|
+
domain: { min: 0, max: 'a_sr / b' },
|
|
955
|
+
},
|
|
956
|
+
// MR
|
|
957
|
+
{
|
|
958
|
+
id: 'mr-sr',
|
|
959
|
+
type: 'line',
|
|
960
|
+
equation: 'a_sr - 2 * b * x',
|
|
961
|
+
color: '#7C3AED',
|
|
962
|
+
strokeWidth: 2,
|
|
963
|
+
label: 'MR',
|
|
964
|
+
domain: { min: 0, max: 'a_sr / (2 * b)' },
|
|
965
|
+
},
|
|
966
|
+
// ATC
|
|
967
|
+
{
|
|
968
|
+
id: 'atc',
|
|
969
|
+
type: 'line',
|
|
970
|
+
equation: 'minATC + c * ((x - Q_eff) ^ 2)',
|
|
971
|
+
color: '#16A34A',
|
|
972
|
+
strokeWidth: 3,
|
|
973
|
+
label: 'ATC',
|
|
974
|
+
domain: { min: 1, max: 'chartMaxQ' },
|
|
975
|
+
},
|
|
976
|
+
// MC (approximate)
|
|
977
|
+
{
|
|
978
|
+
id: 'mc',
|
|
979
|
+
type: 'line',
|
|
980
|
+
equation: 'minATC + 2 * c * (x - Q_eff)',
|
|
981
|
+
color: '#DC2626',
|
|
982
|
+
strokeWidth: 2,
|
|
983
|
+
label: 'MC',
|
|
984
|
+
domain: { min: 'Q_eff * 0.3', max: 'chartMaxQ' },
|
|
985
|
+
},
|
|
986
|
+
// Optimal point
|
|
987
|
+
{
|
|
988
|
+
id: 'optimum-sr',
|
|
989
|
+
type: 'point',
|
|
990
|
+
x: 'Q_sr',
|
|
991
|
+
y: 'P_sr',
|
|
992
|
+
color: '#2563EB',
|
|
993
|
+
size: 8,
|
|
994
|
+
droplines: { x: true, y: true },
|
|
995
|
+
},
|
|
996
|
+
],
|
|
997
|
+
annotations: [
|
|
998
|
+
{
|
|
999
|
+
id: 'profit-label',
|
|
1000
|
+
text: 'π = $${profit_sr:.0f} → Entry',
|
|
1001
|
+
x: 'chartMaxQ * 0.6',
|
|
1002
|
+
y: 'chartMaxP * 0.9',
|
|
1003
|
+
color: '#16A34A',
|
|
1004
|
+
},
|
|
1005
|
+
],
|
|
1006
|
+
},
|
|
1007
|
+
{
|
|
1008
|
+
id: 'long-run',
|
|
1009
|
+
title: 'Long-Run: Zero Profit (Tangency)',
|
|
1010
|
+
xAxis: { label: 'Quantity (q)', min: 0, max: 'chartMaxQ' },
|
|
1011
|
+
yAxis: { label: 'Price, Cost', min: 0, max: 'chartMaxP' },
|
|
1012
|
+
elements: [
|
|
1013
|
+
// LR Demand (shifted in from entry)
|
|
1014
|
+
{
|
|
1015
|
+
id: 'demand-lr',
|
|
1016
|
+
type: 'line',
|
|
1017
|
+
equation: 'a_lr - b * x',
|
|
1018
|
+
color: '#2563EB',
|
|
1019
|
+
strokeWidth: 3,
|
|
1020
|
+
label: 'Demand (after entry)',
|
|
1021
|
+
domain: { min: 0, max: 'a_lr / b' },
|
|
1022
|
+
},
|
|
1023
|
+
// LR MR
|
|
1024
|
+
{
|
|
1025
|
+
id: 'mr-lr',
|
|
1026
|
+
type: 'line',
|
|
1027
|
+
equation: 'a_lr - 2 * b * x',
|
|
1028
|
+
color: '#7C3AED',
|
|
1029
|
+
strokeWidth: 2,
|
|
1030
|
+
label: 'MR',
|
|
1031
|
+
domain: { min: 0, max: 'a_lr / (2 * b)' },
|
|
1032
|
+
},
|
|
1033
|
+
// ATC
|
|
1034
|
+
{
|
|
1035
|
+
id: 'atc-lr',
|
|
1036
|
+
type: 'line',
|
|
1037
|
+
equation: 'minATC + c * ((x - Q_eff) ^ 2)',
|
|
1038
|
+
color: '#16A34A',
|
|
1039
|
+
strokeWidth: 3,
|
|
1040
|
+
label: 'ATC',
|
|
1041
|
+
domain: { min: 1, max: 'chartMaxQ' },
|
|
1042
|
+
},
|
|
1043
|
+
// MC
|
|
1044
|
+
{
|
|
1045
|
+
id: 'mc-lr',
|
|
1046
|
+
type: 'line',
|
|
1047
|
+
equation: 'minATC + 2 * c * (x - Q_eff)',
|
|
1048
|
+
color: '#DC2626',
|
|
1049
|
+
strokeWidth: 2,
|
|
1050
|
+
label: 'MC',
|
|
1051
|
+
domain: { min: 'Q_eff * 0.3', max: 'chartMaxQ' },
|
|
1052
|
+
},
|
|
1053
|
+
// Tangency point
|
|
1054
|
+
{
|
|
1055
|
+
id: 'tangency',
|
|
1056
|
+
type: 'point',
|
|
1057
|
+
x: 'Q_lr',
|
|
1058
|
+
y: 'P_lr',
|
|
1059
|
+
color: '#16A34A',
|
|
1060
|
+
size: 12,
|
|
1061
|
+
label: 'Tangency (π = 0)',
|
|
1062
|
+
droplines: { x: true, y: true },
|
|
1063
|
+
},
|
|
1064
|
+
// Efficient scale marker
|
|
1065
|
+
{
|
|
1066
|
+
id: 'efficient-point',
|
|
1067
|
+
type: 'point',
|
|
1068
|
+
x: 'Q_eff',
|
|
1069
|
+
y: 'minATC',
|
|
1070
|
+
color: '#9CA3AF',
|
|
1071
|
+
size: 6,
|
|
1072
|
+
label: 'Min ATC',
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
annotations: [
|
|
1076
|
+
{
|
|
1077
|
+
id: 'zero-profit',
|
|
1078
|
+
text: 'P = ATC → π = 0',
|
|
1079
|
+
x: 'chartMaxQ * 0.6',
|
|
1080
|
+
y: 'chartMaxP * 0.9',
|
|
1081
|
+
color: '#16A34A',
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
id: 'excess-capacity',
|
|
1085
|
+
text: 'Excess Capacity: Q_LR < Q_eff',
|
|
1086
|
+
x: 'chartMaxQ * 0.6',
|
|
1087
|
+
y: 'chartMaxP * 0.8',
|
|
1088
|
+
color: '#6B7280',
|
|
1089
|
+
},
|
|
1090
|
+
{
|
|
1091
|
+
id: 'markup',
|
|
1092
|
+
text: 'P > MC (markup over marginal cost)',
|
|
1093
|
+
x: 'chartMaxQ * 0.6',
|
|
1094
|
+
y: 'chartMaxP * 0.7',
|
|
1095
|
+
color: '#6B7280',
|
|
1096
|
+
},
|
|
1097
|
+
],
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
},
|
|
1102
|
+
};
|
|
1103
|
+
// ============================================================================
|
|
1104
|
+
// EXPORTS
|
|
1105
|
+
// ============================================================================
|
|
1106
|
+
export const oligopolyTemplates = [
|
|
1107
|
+
cournotDuopoly,
|
|
1108
|
+
bertrandDuopoly,
|
|
1109
|
+
stackelbergDuopoly,
|
|
1110
|
+
kinkedDemand,
|
|
1111
|
+
monopolisticCompetition,
|
|
1112
|
+
];
|
|
1113
|
+
//# sourceMappingURL=oligopoly.js.map
|